Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Refactor] 모임 일정 시간 페이징 처리 및 베스트 타임 API #126

Merged
merged 9 commits into from
Oct 1, 2024
20 changes: 20 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
services:
mysql:
image: mysql:8.0
container_name: jjakkak-mysql
restart: always
ports:
- "3306:3306"
environment:
MYSQL_DATABASE: jjakkak
MYSQL_ROOT_PASSWORD: 1234
TZ: Asia/Seoul
command:
- --character-set-server=utf8mb4

redis:
image: redis:latest
container_name: jjakkak-redis
restart: always
ports:
- "6379:6379"
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@
import com.dnd.jjakkak.domain.meeting.dto.response.MeetingInfoResponseDto;
import com.dnd.jjakkak.domain.meeting.dto.response.MeetingParticipantResponseDto;
import com.dnd.jjakkak.domain.meeting.dto.response.MeetingTimeResponseDto;
import com.dnd.jjakkak.domain.meeting.enums.MeetingSort;
import com.dnd.jjakkak.domain.meeting.service.MeetingService;
import com.dnd.jjakkak.domain.member.dto.response.MemberResponseDto;
import com.dnd.jjakkak.global.common.PagedResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.List;

/**
Expand Down Expand Up @@ -60,16 +63,37 @@ public ResponseEntity<MeetingInfoResponseDto> getMeetingInfo(@PathVariable("meet
/**
* 모임 시간을 조회하는 메서드입니다.
*
* @param uuid 조회할 모임 UUID
* @param sort 정렬 기준 (COUNT: 인원 수, LATEST: 최신순)
* @param uuid 조회할 모임 UUID
* @param pageable 페이징 정보 (default: page = 0, size = 10, sort = count)
* @return 200 (OK), body: 모임 시간 응답 DTO
*/
@GetMapping("/{meetingUuid}/times")
public ResponseEntity<MeetingTimeResponseDto> getMeetingTimes(
public ResponseEntity<PagedResponse<MeetingTimeResponseDto>> getMeetingTimes(
@PathVariable("meetingUuid") String uuid,
@RequestParam(value = "sort", defaultValue = "COUNT") MeetingSort sort) {
@PageableDefault(sort = "count") Pageable pageable,
@RequestParam(value = "request_time", required = false) String requestTime) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 프론트에서 API 최초 요청 시간을 보내는 건가요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엇 네 맞습니다.

시나리오를 생각해보면,

  1. 프론트에서 첫 페이지 요청을 함. (request_time은 빼고)
  2. 서버에서 응답시, 응답 필드에 requestTime 을 추가하여 프론트가 요청한 시간을 보내줌
  3. 이후 프론트에서 다음 페이지 요청시, request_time 정보를 함께 서버로 요청함
  4. 서버에서는 요청 시간을 기준으로 이전에 저장된 일정 데이터만을 조회함. (일정 할당 시간 <= 요청 시간)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그러면 API의 최초 요청 시간은 파라미터에 request_time를 빼고 전송할 때 갱신되는건가요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞아요! 그 부분을 조금 주의해서 요청을 해야해요

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 알겠습니다. 머지하셔도 될 것 같습니다.


LocalDateTime time = LocalDateTime.now();

if (requestTime != null) {
time = LocalDateTime.parse(requestTime);
}

PagedResponse<MeetingTimeResponseDto> responseDto = meetingService.getMeetingTimes(uuid, pageable, time);
responseDto.getData().setRequestTime(time);

return ResponseEntity.ok(responseDto);
}

return ResponseEntity.ok(meetingService.getMeetingTimes(uuid, sort));
/**
* 모임의 최적 시간을 조회하는 메서드입니다.
*
* @param uuid 조회할 모임 UUID
* @return 200 (OK), body: 최적 시간
*/
@GetMapping("/{meetingUuid}/best-time")
public ResponseEntity<LocalDateTime> getBestTime(@PathVariable("meetingUuid") String uuid) {
return ResponseEntity.ok(meetingService.getBestTime(uuid));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lombok.Getter;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -15,23 +16,28 @@
@Getter
public class MeetingTimeResponseDto {

private final List<MeetingTime> meetingTimeList;
private final Integer numberOfPeople;
private final Boolean isAnonymous;
private final LocalDate meetingStartDate;
private final LocalDate meetingEndDate;
private final List<MeetingTime> meetingTimeList;
private LocalDateTime requestTime;


public MeetingTimeResponseDto(Integer numberOfPeople, Boolean isAnonymous,
LocalDate meetingStartDate, LocalDate meetingEndDate) {
this.meetingTimeList = new ArrayList<>();
this.numberOfPeople = numberOfPeople;
this.isAnonymous = isAnonymous;
this.meetingStartDate = meetingStartDate;
this.meetingEndDate = meetingEndDate;
this.meetingTimeList = new ArrayList<>();
}

public void addMeetingTimeList(List<MeetingTime> meetingTime) {
this.meetingTimeList.addAll(meetingTime);
}

public void setRequestTime(LocalDateTime requestTime) {
this.requestTime = requestTime;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import com.dnd.jjakkak.domain.meeting.dto.response.MeetingInfoResponseDto;
import com.dnd.jjakkak.domain.meeting.dto.response.MeetingParticipantResponseDto;
import com.dnd.jjakkak.domain.meeting.dto.response.MeetingTimeResponseDto;
import com.dnd.jjakkak.domain.meeting.enums.MeetingSort;
import com.dnd.jjakkak.global.common.PagedResponse;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.NoRepositoryBean;

import java.time.LocalDateTime;

/**
* 모임 Querydsl 메서드를 정의하는 인터페이스입니다.
*
Expand Down Expand Up @@ -39,14 +42,16 @@ public interface MeetingRepositoryCustom {
*/
MeetingInfoResponseDto getMeetingInfo(String uuid);


/**
* 모임의 UUID로 시간을 조회합니다.
*
* @param uuid 모임 UUID
* @param sort 정렬 기준
* @return 최적의 시간 응답 DTO 리스트
* @param uuid 모임 UUID
* @param pageable 페이지 정보
* @param requestTime 요청 시간
* @return 정렬된 시간 응답 DTO 리스트
*/
MeetingTimeResponseDto getMeetingTimes(String uuid, MeetingSort sort);
PagedResponse<MeetingTimeResponseDto> getMeetingTimes(String uuid, Pageable pageable, LocalDateTime requestTime);

/**
* 모임의 UUID로 참가자를 조회합니다.
Expand All @@ -64,4 +69,12 @@ public interface MeetingRepositoryCustom {
* @return 모임 일정 할당 여부
*/
boolean existsByMemberIdAndMeetingUuid(Long memberId, String meetingUuid);

/**
* 모임 UUID로 최적의 시간을 조회합니다.
*
* @param uuid 모임 UUID
* @return 최적의 시간
*/
LocalDateTime getBestTime(String uuid);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@
import com.dnd.jjakkak.domain.meeting.dto.response.MeetingTimeResponseDto;
import com.dnd.jjakkak.domain.meeting.entity.Meeting;
import com.dnd.jjakkak.domain.meeting.entity.QMeeting;
import com.dnd.jjakkak.domain.meeting.enums.MeetingSort;
import com.dnd.jjakkak.domain.meeting.exception.MeetingNotFoundException;
import com.dnd.jjakkak.domain.meetingcategory.entity.QMeetingCategory;
import com.dnd.jjakkak.domain.member.entity.QMember;
import com.dnd.jjakkak.domain.schedule.entity.QSchedule;
import com.dnd.jjakkak.global.common.PageInfo;
import com.dnd.jjakkak.global.common.PagedResponse;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Projections;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

/**
Expand Down Expand Up @@ -103,48 +108,87 @@ public MeetingInfoResponseDto getMeetingInfo(String uuid) {
* {@inheritDoc}
*/
@Override
public MeetingTimeResponseDto getMeetingTimes(String uuid, MeetingSort sort) {
public PagedResponse<MeetingTimeResponseDto> getMeetingTimes(String uuid, Pageable pageable, LocalDateTime requestTime) {

// TODO : 성능 개선 필요해보임
QMeeting meeting = QMeeting.meeting;
QSchedule schedule = QSchedule.schedule;
QDateOfSchedule dateOfSchedule = QDateOfSchedule.dateOfSchedule;

List<OrderSpecifier<?>> orderSpecifier = switch (sort) {
case COUNT -> List.of(dateOfSchedule.dateOfScheduleRank.count().desc(),
dateOfSchedule.dateOfScheduleRank.avg().asc(),
dateOfSchedule.dateOfScheduleStart.asc(),
dateOfSchedule.dateOfScheduleEnd.asc());
case LATEST -> List.of(dateOfSchedule.dateOfScheduleStart.asc());
};

// 우선순위 순으로 최적 시간 조회
// 1. order by 설정
List<OrderSpecifier<?>> orderSpecifiers = new ArrayList<>();

pageable.getSort().stream()
.forEach(sort -> {
switch (sort.getProperty()) {
case "count" -> orderSpecifiers.addAll(
List.of(
dateOfSchedule.dateOfScheduleRank.count().desc(),
dateOfSchedule.dateOfScheduleRank.avg().asc(),
dateOfSchedule.dateOfScheduleStart.asc(),
dateOfSchedule.dateOfScheduleEnd.asc()
)
);
case "latest" -> orderSpecifiers.add(dateOfSchedule.dateOfScheduleStart.asc());
default -> throw new MeetingNotFoundException();
}
});


// 2. 페이징 데이터 및 전체 요소 수 조회
List<MeetingTime> meetingTimeList = from(dateOfSchedule)
.join(dateOfSchedule.schedule, schedule)
.join(schedule.meeting, meeting)
.where(meeting.meetingUuid.eq(uuid))
.where(meeting.meetingUuid.eq(uuid)
.and(schedule.assignedAt.loe(requestTime)))
Comment on lines +142 to +143
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.groupBy(dateOfSchedule.dateOfScheduleStart, dateOfSchedule.dateOfScheduleEnd)
.orderBy(orderSpecifier.toArray(new OrderSpecifier[0]))
.orderBy(orderSpecifiers.toArray(new OrderSpecifier[orderSpecifiers.size()]))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.select(Projections.constructor(MeetingTime.class,
dateOfSchedule.dateOfScheduleStart,
dateOfSchedule.dateOfScheduleEnd,
dateOfSchedule.dateOfScheduleRank.avg()
))
.fetch();


long totalElements = from(dateOfSchedule)
.join(dateOfSchedule.schedule, schedule)
.join(schedule.meeting, meeting)
.where(meeting.meetingUuid.eq(uuid)
.and(schedule.assignedAt.isNotNull())
.and(schedule.assignedAt.loe(requestTime)))
.groupBy(dateOfSchedule.dateOfScheduleStart, dateOfSchedule.dateOfScheduleEnd)
.select(dateOfSchedule.dateOfScheduleRank.count())
.fetchCount();


// 3. 닉네임 조회
for (MeetingTime meetingTime : meetingTimeList) {
List<String> nicknames = from(dateOfSchedule)
.join(dateOfSchedule.schedule, schedule)
.join(schedule.meeting, meeting)
.where(meeting.meetingUuid.eq(uuid)
.and(dateOfSchedule.dateOfScheduleStart.eq(meetingTime.getStartTime()))
.and(dateOfSchedule.dateOfScheduleEnd.eq(meetingTime.getEndTime())))
.and(dateOfSchedule.dateOfScheduleEnd.eq(meetingTime.getEndTime()))
.and(schedule.assignedAt.loe(requestTime)))
.select(schedule.scheduleNickname)
.fetch();

meetingTime.addMemberNames(nicknames);
}

// 4. PageInfo 생성
int totalPages = (int) Math.ceil((double) totalElements / pageable.getPageSize());
PageInfo pageInfo = PageInfo.builder()
.page(pageable.getPageNumber())
.size(pageable.getPageSize())
.totalElements((int) totalElements)
.totalPages(totalPages)
.build();

// 5. 응답 DTO 생성
MeetingTimeResponseDto responseDto = from(meeting)
.where(meeting.meetingUuid.eq(uuid))
.select(Projections.constructor(MeetingTimeResponseDto.class,
Expand All @@ -157,7 +201,7 @@ public MeetingTimeResponseDto getMeetingTimes(String uuid, MeetingSort sort) {

responseDto.addMeetingTimeList(meetingTimeList);

return responseDto;
return new PagedResponse<>(responseDto, pageInfo);
}


Expand Down Expand Up @@ -209,4 +253,23 @@ public boolean existsByMemberIdAndMeetingUuid(Long memberId, String meetingUuid)
.fetchCount() > 0;

}

@Override
public LocalDateTime getBestTime(String uuid) {

QMeeting meeting = QMeeting.meeting;
QSchedule schedule = QSchedule.schedule;
QDateOfSchedule dateOfSchedule = QDateOfSchedule.dateOfSchedule;

return from(dateOfSchedule)
.join(dateOfSchedule.schedule, schedule)
.join(schedule.meeting, meeting)
.where(meeting.meetingUuid.eq(uuid))
.groupBy(dateOfSchedule.dateOfScheduleStart, dateOfSchedule.dateOfScheduleEnd)
.orderBy(dateOfSchedule.dateOfScheduleRank.count().desc(),
dateOfSchedule.dateOfScheduleRank.avg().asc(),
dateOfSchedule.dateOfScheduleStart.asc())
.select(dateOfSchedule.dateOfScheduleStart)
.fetchFirst();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import com.dnd.jjakkak.domain.meeting.dto.response.MeetingParticipantResponseDto;
import com.dnd.jjakkak.domain.meeting.dto.response.MeetingTimeResponseDto;
import com.dnd.jjakkak.domain.meeting.entity.Meeting;
import com.dnd.jjakkak.domain.meeting.enums.MeetingSort;
import com.dnd.jjakkak.domain.meeting.exception.MeetingNotFoundException;
import com.dnd.jjakkak.domain.meeting.exception.MeetingUnauthorizedException;
import com.dnd.jjakkak.domain.meeting.repository.MeetingRepository;
Expand All @@ -22,10 +21,13 @@
import com.dnd.jjakkak.domain.member.exception.MemberNotFoundException;
import com.dnd.jjakkak.domain.member.repository.MemberRepository;
import com.dnd.jjakkak.domain.schedule.service.ScheduleService;
import com.dnd.jjakkak.global.common.PagedResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;

Expand Down Expand Up @@ -156,21 +158,23 @@ public MeetingInfoResponseDto getMeetingInfo(String uuid) {
return meetingRepository.getMeetingInfo(uuid);
}


/**
* 모임의 시간을 조회하는 메서드입니다.
*
* @param uuid 조회할 모임 UUID
* @param sort 정렬 기준 (COUNT: 인원 수, LATEST: 최신순)
* @param uuid 조회할 모임 UUID
* @param pageable 페이지 정보
* @param requestTime 요청 시간
* @return 정렬된 시간 응답 DTO 리스트
*/
@Transactional(readOnly = true)
public MeetingTimeResponseDto getMeetingTimes(String uuid, MeetingSort sort) {
public PagedResponse<MeetingTimeResponseDto> getMeetingTimes(String uuid, Pageable pageable, LocalDateTime requestTime) {

if (!meetingRepository.existsByMeetingUuid(uuid)) {
throw new MeetingNotFoundException();
}

return meetingRepository.getMeetingTimes(uuid, sort);
return meetingRepository.getMeetingTimes(uuid, pageable, requestTime);
}

/**
Expand All @@ -189,6 +193,11 @@ public MeetingParticipantResponseDto getParticipants(String uuid) {
return meetingRepository.getParticipant(uuid);
}

@Transactional(readOnly = true)
public LocalDateTime getBestTime(String uuid) {
return meetingRepository.getBestTime(uuid);
}

/**
* UUID를 생성하는 메서드입니다.
*
Expand Down
Loading