diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..c224d55 --- /dev/null +++ b/docker/docker-compose.yml @@ -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" diff --git a/src/main/java/com/dnd/jjakkak/domain/meeting/controller/MeetingController.java b/src/main/java/com/dnd/jjakkak/domain/meeting/controller/MeetingController.java index 2bd772b..789e85f 100644 --- a/src/main/java/com/dnd/jjakkak/domain/meeting/controller/MeetingController.java +++ b/src/main/java/com/dnd/jjakkak/domain/meeting/controller/MeetingController.java @@ -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; /** @@ -60,16 +63,37 @@ public ResponseEntity 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 getMeetingTimes( + public ResponseEntity> 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) { + + LocalDateTime time = LocalDateTime.now(); + + if (requestTime != null) { + time = LocalDateTime.parse(requestTime); + } + + PagedResponse 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 getBestTime(@PathVariable("meetingUuid") String uuid) { + return ResponseEntity.ok(meetingService.getBestTime(uuid)); } /** diff --git a/src/main/java/com/dnd/jjakkak/domain/meeting/dto/response/MeetingTimeResponseDto.java b/src/main/java/com/dnd/jjakkak/domain/meeting/dto/response/MeetingTimeResponseDto.java index 0a6ae08..821fdeb 100644 --- a/src/main/java/com/dnd/jjakkak/domain/meeting/dto/response/MeetingTimeResponseDto.java +++ b/src/main/java/com/dnd/jjakkak/domain/meeting/dto/response/MeetingTimeResponseDto.java @@ -3,6 +3,7 @@ import lombok.Getter; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -15,23 +16,28 @@ @Getter public class MeetingTimeResponseDto { + private final List meetingTimeList; private final Integer numberOfPeople; private final Boolean isAnonymous; private final LocalDate meetingStartDate; private final LocalDate meetingEndDate; - private final List 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) { this.meetingTimeList.addAll(meetingTime); } + + public void setRequestTime(LocalDateTime requestTime) { + this.requestTime = requestTime; + } } diff --git a/src/main/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryCustom.java b/src/main/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryCustom.java index 5ea542f..831bcc2 100644 --- a/src/main/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryCustom.java +++ b/src/main/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryCustom.java @@ -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 메서드를 정의하는 인터페이스입니다. * @@ -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 getMeetingTimes(String uuid, Pageable pageable, LocalDateTime requestTime); /** * 모임의 UUID로 참가자를 조회합니다. @@ -64,4 +69,12 @@ public interface MeetingRepositoryCustom { * @return 모임 일정 할당 여부 */ boolean existsByMemberIdAndMeetingUuid(Long memberId, String meetingUuid); + + /** + * 모임 UUID로 최적의 시간을 조회합니다. + * + * @param uuid 모임 UUID + * @return 최적의 시간 + */ + LocalDateTime getBestTime(String uuid); } diff --git a/src/main/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryImpl.java b/src/main/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryImpl.java index 232126e..0ff11ec 100644 --- a/src/main/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryImpl.java +++ b/src/main/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryImpl.java @@ -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; /** @@ -103,28 +108,43 @@ public MeetingInfoResponseDto getMeetingInfo(String uuid) { * {@inheritDoc} */ @Override - public MeetingTimeResponseDto getMeetingTimes(String uuid, MeetingSort sort) { + public PagedResponse getMeetingTimes(String uuid, Pageable pageable, LocalDateTime requestTime) { // TODO : 성능 개선 필요해보임 QMeeting meeting = QMeeting.meeting; QSchedule schedule = QSchedule.schedule; QDateOfSchedule dateOfSchedule = QDateOfSchedule.dateOfSchedule; - List> 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> 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 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))) .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, @@ -132,19 +152,43 @@ public MeetingTimeResponseDto getMeetingTimes(String uuid, MeetingSort sort) { )) .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 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, @@ -157,7 +201,7 @@ public MeetingTimeResponseDto getMeetingTimes(String uuid, MeetingSort sort) { responseDto.addMeetingTimeList(meetingTimeList); - return responseDto; + return new PagedResponse<>(responseDto, pageInfo); } @@ -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(); + } } diff --git a/src/main/java/com/dnd/jjakkak/domain/meeting/service/MeetingService.java b/src/main/java/com/dnd/jjakkak/domain/meeting/service/MeetingService.java index 8ab75a4..7e88b77 100644 --- a/src/main/java/com/dnd/jjakkak/domain/meeting/service/MeetingService.java +++ b/src/main/java/com/dnd/jjakkak/domain/meeting/service/MeetingService.java @@ -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; @@ -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; @@ -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 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); } /** @@ -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를 생성하는 메서드입니다. * diff --git a/src/main/java/com/dnd/jjakkak/domain/schedule/entity/Schedule.java b/src/main/java/com/dnd/jjakkak/domain/schedule/entity/Schedule.java index 94c2de8..98ec50c 100644 --- a/src/main/java/com/dnd/jjakkak/domain/schedule/entity/Schedule.java +++ b/src/main/java/com/dnd/jjakkak/domain/schedule/entity/Schedule.java @@ -8,6 +8,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + /** * 일정 엔티티 클래스입니다. * @@ -49,6 +51,9 @@ public class Schedule { @Column(name = "is_assigned", nullable = false) private Boolean isAssigned; + @Column(name = "assigned_at") + private LocalDateTime assignedAt; + @Builder public Schedule(Meeting meeting, Member member, String scheduleNickname, String scheduleUuid) { this.meeting = meeting; @@ -83,4 +88,13 @@ public void assignMember(Member member) { public void scheduleAssign() { this.isAssigned = Boolean.TRUE; } + + /** + * 일정을 할당한 시점을 변경하는 메서드입니다. + * + * @param assignedAt 할당 시점 + */ + public void changeAssignedAt(LocalDateTime assignedAt) { + this.assignedAt = assignedAt; + } } diff --git a/src/main/java/com/dnd/jjakkak/domain/schedule/service/ScheduleService.java b/src/main/java/com/dnd/jjakkak/domain/schedule/service/ScheduleService.java index 417592d..6a272b3 100644 --- a/src/main/java/com/dnd/jjakkak/domain/schedule/service/ScheduleService.java +++ b/src/main/java/com/dnd/jjakkak/domain/schedule/service/ScheduleService.java @@ -23,6 +23,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.Optional; import java.util.UUID; @@ -77,6 +78,8 @@ public ScheduleAssignResponseDto assignScheduleToGuest(String meetingUuid, Sched Schedule schedule = scheduleRepository.findNotAssignedScheduleByMeetingUuid(meetingUuid) .orElseThrow(ScheduleNotFoundException::new); + schedule.changeAssignedAt(LocalDateTime.now()); + validateAndAssignSchedule(requestDto, schedule); return ScheduleAssignResponseDto.builder() @@ -107,6 +110,7 @@ public void assignScheduleToMember(Long memberId, String meetingUuid, ScheduleAs schedule.assignMember(member); schedule.updateScheduleNickname(member.getMemberNickname()); + schedule.changeAssignedAt(LocalDateTime.now()); validateAndAssignSchedule(requestDto, schedule); meetingMemberService.createMeetingMemberBySchedule(schedule.getScheduleId(), memberId); @@ -169,6 +173,7 @@ public ScheduleResponseDto getGuestSchedule(String meetingUuid, String scheduleU throw new MeetingNotFoundException(); } + schedule.changeAssignedAt(LocalDateTime.now()); return scheduleRepository.findScheduleWithDateOfSchedule(schedule.getScheduleId()); } diff --git a/src/main/java/com/dnd/jjakkak/global/common/PageInfo.java b/src/main/java/com/dnd/jjakkak/global/common/PageInfo.java new file mode 100644 index 0000000..9e38e25 --- /dev/null +++ b/src/main/java/com/dnd/jjakkak/global/common/PageInfo.java @@ -0,0 +1,27 @@ +package com.dnd.jjakkak.global.common; + +import lombok.Builder; +import lombok.Getter; + +/** + * 페이지 정보를 담는 클래스입니다. + * + * @author 정승조 + * @version 2024. 09. 27. + */ +@Getter +public class PageInfo { + + private final int page; + private final int size; + private final int totalPages; + private final int totalElements; + + @Builder + public PageInfo(int page, int size, int totalElements, int totalPages) { + this.page = page; + this.size = size; + this.totalPages = totalPages; + this.totalElements = totalElements; + } +} diff --git a/src/main/java/com/dnd/jjakkak/global/common/PagedResponse.java b/src/main/java/com/dnd/jjakkak/global/common/PagedResponse.java new file mode 100644 index 0000000..77da1dc --- /dev/null +++ b/src/main/java/com/dnd/jjakkak/global/common/PagedResponse.java @@ -0,0 +1,23 @@ +package com.dnd.jjakkak.global.common; + +import lombok.Builder; +import lombok.Getter; + +/** + * 페이징 처리된 응답을 담는 클래스입니다. + * + * @author 정승조 + * @version 2024. 09. 27. + */ +@Getter +public class PagedResponse { + + private final T data; + private final PageInfo pageInfo; + + @Builder + public PagedResponse(T data, PageInfo pageInfo) { + this.data = data; + this.pageInfo = pageInfo; + } +} diff --git a/src/main/java/com/dnd/jjakkak/global/config/security/SecurityEndpointPaths.java b/src/main/java/com/dnd/jjakkak/global/config/security/SecurityEndpointPaths.java index 4ab27cf..067f4ef 100644 --- a/src/main/java/com/dnd/jjakkak/global/config/security/SecurityEndpointPaths.java +++ b/src/main/java/com/dnd/jjakkak/global/config/security/SecurityEndpointPaths.java @@ -16,6 +16,7 @@ public class SecurityEndpointPaths { "/api/v1/meetings/*/participants", "/api/v1/meetings/*/schedules/guests/**", "/api/v1/meetings/*/schedules/*", + "/api/v1/meetings/*/best-time", }; public static final String[] USER_LIST = { diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index ff83b75..aa9e80b 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -572,7 +572,7 @@

1.1. 카테고리 목록 조회

HTTP Request
GET /api/v1/categories HTTP/1.1
-X-CSRF-TOKEN: jwQrpWGAp-X5RQXjivGKXC6ecUByXsnDjjuyLmpJtkoCHySbuDAcklfhn9PUcDaBu9y-ZR6rXHhFbKvuuFiFHFMs0ihmehz4
+X-CSRF-TOKEN: dLxjU26EqrgoKLOUY0fYILGa92Fwx0pgCsNsZMU9VO1WhboRF4tbZA-wmYgFToT1B2rsEYer2gBFo3lNOPJfAqFfYIk05oNz
 Host: localhost:8080
@@ -672,7 +672,7 @@

1.2. 카테고리 개별 조회 -
HTTP Request
GET /api/v1/categories/1 HTTP/1.1
-X-CSRF-TOKEN: Nsb3EyMXuheV8LV7Az6QWcWbjkm_FCAxKZ1tH1cuywW06dK5UvWRdRUijXW4loJIMROkYPauoyiOJ0QcT6sOLGZM_DGB0OOO
+X-CSRF-TOKEN: 4Ng-lvBpfKBY3N1z5rKRHcIYr9I_yKc3IY_NISC5q-Is3vsN1e5YoMUNH5d16uURh5-lefd-gutd-MQaEev5GBXYmNBP659v
 Host: localhost:8080
@@ -705,7 +705,7 @@

1.3. 카테고리 개별 조회 -
HTTP Request
GET /api/v1/categories/100 HTTP/1.1
-X-CSRF-TOKEN: mBtADXKbqbt60c0CANtLZseBAsO9rdAQnzelKdRgU2gpmVzYqyx0OBGiyNpX5axnY_Z_APDkL_vcmOM9p1PEGrZTMV9Pqmjh
+X-CSRF-TOKEN: y2fQU8Qvq89eDF2G3DWJZKblTaHxQA5YIaRKv3MWOrl_ybbt-F7iYfQYzftzbWnkvRi9VJPQYJmUIzd1FpMs3BJ1X4BMqILZ
 Host: localhost:8080
@@ -747,7 +747,7 @@

2.1. 모임 생성 - 성공

Content-Type: application/json;charset=UTF-8 Authorization: Bearer access_token Content-Length: 229 -X-CSRF-TOKEN: NGGd1Qof3cW9azGQxOgDaHkqR5oC69lwr7bf083-q7Vx2DxcVwCs5DN8v6eQCgT09MU3CUkYaqNj3OFdy4674q_Lm4FJ7F1t +X-CSRF-TOKEN: uuSRc0exuhSG1HERKCMw9JQzIcyS5EbhGl0-dLSfDvLj1DeR24WkFSXTiner5xR3SQ4ExPYDDK2m3HXMI2QKQNamb5bW4FWj Host: localhost:8080 { @@ -856,7 +856,7 @@

2.2. 모임 생성 - 실패

Content-Type: application/json;charset=UTF-8 Authorization: Bearer access_token Content-Length: 180 -X-CSRF-TOKEN: l53ZM9zuiaxVXG6HQBYV-_RW0xQTzkwdyC_2v-S4VNtEz4kvoazhVe7WuJl4a1_hJTshys1h_nUqq3Qw-hzE2teKZeMg_ugb +X-CSRF-TOKEN: UrXgAwN8jrzEArq78sDB8arV9GNtNpZr8uwjEo17igCo9tXqa4zUYTcau9rpYNuOk-31lcuz2VpZBfdGl98XdL5K6TLJk-Pe Host: localhost:8080 { @@ -890,9 +890,9 @@

2.2. 모임 생성 - 실패

"code" : "400", "message" : "잘못된 요청입니다.", "validation" : { - "categoryIds" : "카테고리는 최소 1개 이상 3개 이하로 선택해주세요.", "isAnonymous" : "익명 여부는 필수 값입니다.", "meetingEndDate" : "모임 일정 종료일은 필수 값입니다.", + "categoryIds" : "카테고리는 최소 1개 이상 3개 이하로 선택해주세요.", "meetingName" : "모임명은 필수 값입니다.", "dueDateTime" : "일정 입력 종료 시간은 필수 값입니다.", "numberOfPeople" : "인원수는 필수 값입니다.", @@ -994,7 +994,7 @@

2.3. 모임 삭제

DELETE /api/v1/meetings/1 HTTP/1.1
 Content-Type: application/json;charset=UTF-8
-X-CSRF-TOKEN: -lLKfVVBsaqs2JsqBHsryreTU2IA6cqosmLYnAYS-tIHg7J-wmDyGzYig8-Bvf5JPVYfr9ajfgBj3KiF0AG7qjR2nOEws9ZH
+X-CSRF-TOKEN: Svd-gnMNHxp5cyicDe8xVRVN5DbIvcyegIgB3GDDeYAW4G-pL5Ea5hA_J3tUEBH_NcIFY3d7yVTxi_-ztus2uFLyG-Qj2Aua
 Host: localhost:8080
@@ -1022,7 +1022,7 @@

2.4. 모임 정보 조회

GET /api/v1/meetings/123ABC/info HTTP/1.1
 Content-Type: application/json;charset=UTF-8
 Accept: application/json
-X-CSRF-TOKEN: iUtEXqUn9ObDqQdRzGPDf7SfdEM7rpxM0U9WL3l3gW8oaIoC7X11b8MWl9bumj8w9E73GYamWSFdn6Vh43ZuF0sT5V0bUb46
+X-CSRF-TOKEN: xqj6aywaX5X8cb7nVWsO0xj1xROuzG93KbQDOjpgIrp2szb39pnOU0opPKXRQ4zeMUY66nrG6HKYqFZaHoxgXlwFR4xG0FKW
 Host: localhost:8080
@@ -1123,10 +1123,10 @@

2.5. 모임 최적 시간 조회

HTTP Request
-
GET /api/v1/meetings/123ABC/times?sort=COUNT HTTP/1.1
+
GET /api/v1/meetings/123ABC/times?sort=COUNT&request_time=2024-09-30T23%3A31%3A51.788883 HTTP/1.1
 Content-Type: application/json;charset=UTF-8
 Accept: application/json
-X-CSRF-TOKEN: dr_RMf_WrenrML7pcFVjgwFJXoscfnmiJ2HABhESdFyb5-LWTozkV5m3m9nGVIePEXhXtWd8c7IkHE-PRVHzNHInFzqr19Hj
+X-CSRF-TOKEN: C5F4FvTeYVd9FmJFRDLoupJx5qMZI-4YRSEQi6X6-Ev2Ekn_PqdOLpG7UWFQIVN2cR_cjqsXy8J_FIs1cBRxupzCyXyVJ3zL
 Host: localhost:8080
@@ -1163,19 +1163,28 @@

2.5. 모임 최적 시간 조회

Pragma: no-cache Expires: 0 X-Frame-Options: DENY -Content-Length: 297 +Content-Length: 484 { - "numberOfPeople" : 2, - "isAnonymous" : false, - "meetingStartDate" : "2024-08-27", - "meetingEndDate" : "2024-08-29", - "meetingTimeList" : [ { - "memberNames" : [ "고래", "상어" ], - "startTime" : "2024-08-27T10:00:00", - "endTime" : "2024-08-27T12:00:00", - "rank" : 1.0 - } ] + "data" : { + "meetingTimeList" : [ { + "memberNames" : [ "고래", "상어" ], + "startTime" : "2024-08-27T10:00:00", + "endTime" : "2024-08-27T12:00:00", + "rank" : 1.0 + } ], + "numberOfPeople" : 2, + "isAnonymous" : false, + "meetingStartDate" : "2024-08-27", + "meetingEndDate" : "2024-08-29", + "requestTime" : "2024-09-30T23:31:51.788883" + }, + "pageInfo" : { + "page" : 1, + "size" : 1, + "totalPages" : 1, + "totalElements" : 1 + } }
@@ -1194,45 +1203,70 @@

2.5. 모임 최적 시간 조회

-

numberOfPeople

+

data.numberOfPeople

Number

총 인원 수

-

isAnonymous

+

data.isAnonymous

Boolean

익명 여부

-

meetingStartDate

+

data.meetingStartDate

String

모임 시작 날짜

-

meetingEndDate

+

data.meetingEndDate

String

모임 종료 날짜

-

meetingTimeList[].memberNames

+

data.requestTime

+

String

+

요청 시간

+ + +

data.meetingTimeList[].memberNames

Array

멤버 이름 리스트

-

meetingTimeList[].startTime

+

data.meetingTimeList[].startTime

String

시작 시간

-

meetingTimeList[].endTime

+

data.meetingTimeList[].endTime

String

종료 시간

-

meetingTimeList[].rank

+

data.meetingTimeList[].rank

Number

우선순위 (오름차순)

+ +

pageInfo.page

+

Number

+

현재 페이지

+ + +

pageInfo.size

+

Number

+

페이지 크기

+ + +

pageInfo.totalElements

+

Number

+

총 요소 수

+ + +

pageInfo.totalPages

+

Number

+

총 페이지 수

+ @@ -1244,7 +1278,7 @@

2.6. 모임 참가자 목록 조회

GET /api/v1/meetings/123ABC/participants HTTP/1.1 Content-Type: application/json;charset=UTF-8 Accept: application/json -X-CSRF-TOKEN: ztXLBHxHtlfRpnnLLwo3wMav25biHglvegLEyqZ_7q2eQU22qOP_ZU13jmX8kk6tTicD-afN9q7VejhCQzPx-MUcipj7dnTQ +X-CSRF-TOKEN: 8_4a_tGxjpNQqFB_0W4jtv9J18O1o8nmBLTDE39A3esUA1_vy5gqzOGG6_d9mTNIsEMXhsh--qHWl6vLZoH3IEgi69h1ND3a Host: localhost:8080 @@ -1352,7 +1386,7 @@

3.1. 회원이 속한 모임 조회

GET /api/v1/members/meetings HTTP/1.1
 Accept: application/json
-X-CSRF-TOKEN: QT1VIU6wmSuuPCWTGBxDTa1H4kWcraqOkKcYiwtaROwHPcENeVxjFHiJqRuDDRylfjF3K5klzySuzp-jocR9vDtqIN0xDKRs
+X-CSRF-TOKEN: isEBvbvzitBs_ELUpSIUGiK58kvFCz3uk5W6-efAfy2OixFY76U2hNmSvrRBmXOxlg8geEPd33OmP1zD8PGIzIGkHh25uHU7
 Host: localhost:8080
@@ -1457,7 +1491,7 @@

3.2. 회원 닉네임 수정

Content-Type: application/json;charset=UTF-8 Accept: application/json Content-Length: 34 -X-CSRF-TOKEN: -kVsnaHxpjXFOpxbXXde1qx1jUlBKIJMhm0wDjBmXKmNSa-UmCEJrJjAngToWKlpblpq78oQoHBzSbVhtQgDOQcHa568fJbx +X-CSRF-TOKEN: TrQsMJmMScn_bfocC5bRutymKNItNKGJWyUcKKdou_JYbjg4d40fU6q0La_SD8p4PrvljbnCBbBLAZmkaRR5TZMLg8M9DVwO Host: localhost:8080 { @@ -1516,7 +1550,7 @@

3.3. 회원 탈퇴

DELETE /api/v1/members HTTP/1.1
 Accept: application/json
-X-CSRF-TOKEN: ruDW7MUebcuta97h0qw0EeJ0aM8iQxLAjpMSS1oKbNXug8SMz9Hn2vcvX_iAW7rZ4YEAcoRARfcWJyTt7PJ3eD88VOOKuqHq
+X-CSRF-TOKEN: JHHvoDe1gKoFeeJFklS4oOyW98yPzq9ki3l6Yo7fh_2nVxsGFEncxgWHspooGtcgoXmMxY2g2vTp_cpJ7UxPULa7ssuSNSI_
 Host: localhost:8080
@@ -1556,7 +1590,7 @@

4.1. 회원의 일정 할당 - 성공POST /api/v1/meetings/met123/schedules/members HTTP/1.1 Content-Type: application/json;charset=UTF-8 Content-Length: 263 -X-CSRF-TOKEN: b-5kkQYw5vF1f5507ZAH5pVSJ_QfN9YZoRjM5rPSa058IkMTWt1V8zUChMhYRqlAjL0zh6wzCpV5UbI0w3v50YWzDywdEycl +X-CSRF-TOKEN: fHZC2TikfETI1o_ziV5lExPEamw9sHw2ZGmyOo68AgJo8GtuTURy71nHTHDl5bjE73NRIyumRw0KiRobXFHTC76KNjNexQhd Host: localhost:8080 { @@ -1672,7 +1706,7 @@

4.
POST /api/v1/meetings/met123/schedules/members HTTP/1.1
 Content-Type: application/json;charset=UTF-8
 Content-Length: 53
-X-CSRF-TOKEN: f0BiWfuLv9Qe7JFnfquBmkQ38y9RPuqR70e4Y8bWV2pTBin6SSJSbZm_iu0z3KZXR4a1rHcO3k5mXY6833CJAqTubgxhYkrC
+X-CSRF-TOKEN: emlRLEuuW6pD876EFxlB4W-liRgj6vL46G3WnVx2vaQvx9t_HlxgH3_MOJNuxtu3dTR11leUpHob3MDV2FSz_mVD2Z1K_7oc
 Host: localhost:8080
 
 {
@@ -1771,7 +1805,7 @@ 

4.3. 비회원 일정 할당 - 성공POST /api/v1/meetings/met123/schedules/guests HTTP/1.1 Content-Type: application/json;charset=UTF-8 Content-Length: 263 -X-CSRF-TOKEN: UvrdsviOghc1Jaj6YKwb1qtP3hNNM5xlpMngHLy8MMd51uF-N8O_h8josyEYHZDNBYEv5sp483J5VvlIkayGft6EVaMd7thP +X-CSRF-TOKEN: y0NSiO3yA78e79fBZUQ6JGBCsjTUp9pWxJEKLK6C3RHNuASy-iZmvo7KNdozirH1BmkOFFlyn1Xll-t7pqg5HJjkvyX1jjHX Host: localhost:8080 { @@ -1868,7 +1902,7 @@

4.
POST /api/v1/meetings/met123/schedules/guests HTTP/1.1
 Content-Type: application/json;charset=UTF-8
 Content-Length: 53
-X-CSRF-TOKEN: VKu_DhKySQT4o-Uy8U7LXqEPkaOhgeSr4sjTEoKAYa-Xjr-EN5rcOSKEeDbVkdBWxGP_bZk2vMHAtYKG1v7lIbW0AJ3x6Nq9
+X-CSRF-TOKEN: RPc99OA4GINfsI_vhn-k8cY-IJHdI5Z-6zmFjcMBDZF0JxJEc8UMzYINLbZygbja5VKQw_ULDfDoFaJT3li8vvIwPKZDH3Al
 Host: localhost:8080
 
 {
@@ -1946,7 +1980,7 @@ 

4.5. 회원 일정 조회 - 성공

HTTP Request
GET /api/v1/meetings/met123/schedules/members HTTP/1.1
-X-CSRF-TOKEN: lE8EQK1CkeOjNvMg_qxUq0pcfI6ATV-vhIW0DZ4A1s8pnrJjpnoxc8x784eOB8YWz4Fgyno9Ube3fmyCtbHQaPw5svsfqNFR
+X-CSRF-TOKEN: DzpISr3w-s-zvLooWDSHT8rmop4kCXZ-xyiYUM552Zc6GIBLbAh8KN_Jyv-ehI0aPRmzefjXj_wSbRBTpRj8YvscuqJYLbBz
 Host: localhost:8080
@@ -2040,7 +2074,7 @@

4.6.
HTTP Request
GET /api/v1/meetings/met123/schedules/members HTTP/1.1
-X-CSRF-TOKEN: VL-hXM4x1yvumB1MTuqU_HvghkBMzoJpt20tLQFPhJErDdVsNY-Wb6wC4xzDqisuKsegzxiGqyF6rOZEgV1IT2ArsvQZa7YI
+X-CSRF-TOKEN: U9TFiDzHZ5sZh6V6BbjDN02s9CSAmVd-iOK7Kh312uQFnfw2Yuf8v1r3Ba40vpJPMpX3B3Sc2UazoGNT64OPGCvNudMxr8gE
 Host: localhost:8080
@@ -2074,7 +2108,7 @@

4.7. 비회원 일정 조회 - 성공HTTP Request
GET /api/v1/meetings/met123/schedules/guests?scheduleUuid=sch123 HTTP/1.1
-X-CSRF-TOKEN: FC_IVScsMYyhdf8wv3BGSBf2jKClNvYG9u4qC9zZlJNz0aXlIUmsY0UUBLyMTM4Hil1yfSbOocKcBZUrztZPau_upfBG4sGA
+X-CSRF-TOKEN: 1C0Yhue6lRhHWNyTWoVICQve7i_zZCZWrYh5QzP5wK9-CQmo5xgos97brC9qYb-jaqh8P2i6w03HU0N7ne5MIQXN-JZPOm_M
 Host: localhost:8080
@@ -2154,7 +2188,7 @@

4.
HTTP Request
GET /api/v1/meetings/met123/schedules/guests?scheduleUuid=sch123 HTTP/1.1
-X-CSRF-TOKEN: 6S5Sk-AdthcMUkRBhn-vJu88uYaKoAWH7AtO0We_RHxhtYyv2Exi9dcq0ichNHAg41KbRYoJlOS7wzKq1Tot5VHZJk1XgrqX
+X-CSRF-TOKEN: b-Q1dPkP-O7E9YjomeYy5eJigXRt-gMFZ-ihexn6mdMieKKfX90FQp05zI3pwezY-ssGg4QErExewzIoAdiQSy3LqOERQcCm
 Host: localhost:8080
@@ -2228,7 +2262,7 @@

4.9. 회원 일정 수정 - 성공

PATCH /api/v1/meetings/met123/schedules HTTP/1.1
 Content-Type: application/json;charset=UTF-8
 Content-Length: 235
-X-CSRF-TOKEN: zwqj3n_zabB9UJzBKOEbH79_Fq_geENvVEhqJWGbqX-rTiWW9j6X6kvGC9ZQNKX2TcwvKIdJO83WHCBCbX5ZQ1P9n0bKfEPy
+X-CSRF-TOKEN: qGbPddT6JM6wlY6nb2nTmGTgdeIn2Ej9d77Hc39NpVmfRQtxzQSsELLJEPud8-qXXkTnrlXWWIBGuinQR4f_EUd1kz37dT4U
 Host: localhost:8080
 
 {
@@ -2336,7 +2370,7 @@ 

4.10.
PATCH /api/v1/meetings/met123/schedules HTTP/1.1
 Content-Type: application/json;charset=UTF-8
 Content-Length: 32
-X-CSRF-TOKEN: JTV_9CXw73qCrouDF9635pcj5aqjNukYl1ty0v8auMqhepFvRw0dxRXB3U2vmLniL_OD1PVByJKaD9g1rm1H4Ml8ja-WHKkK
+X-CSRF-TOKEN: m74KMPkbPzdPq_izflwftnCFmkWa6nE_n7YPoTuFxe982KcXrd8zA5x4DgdizcqHGHErjkm8tySp3BUS-tQ4xQ2399gYvMMn
 Host: localhost:8080
 
 {
@@ -2433,7 +2467,7 @@ 

4.11. 비회원 일정 수정 - 성공<
PATCH /api/v1/meetings/met123/schedules/sch123 HTTP/1.1
 Content-Type: application/json;charset=UTF-8
 Content-Length: 235
-X-CSRF-TOKEN: uXQMEldqNMWRKyKZALY9TPygDAwXDU_XSdtl9u8Q7MUAA7i22BY8cTJSBfC8TkGtNpsJLcuWITUkNX36LbhWldwk3KA3Zt2C
+X-CSRF-TOKEN: 40rEgGALCXYm5s-zfu1osRXJjq0NZGozSycVhrY_OizvfM4KgiuhsFY9ME4L3vnRScBcgiXxo88-UlMeKkV25NNaXByNTvs8
 Host: localhost:8080
 
 {
@@ -2545,7 +2579,7 @@ 

4.
PATCH /api/v1/meetings/met123/schedules/sch123 HTTP/1.1
 Content-Type: application/json;charset=UTF-8
 Content-Length: 32
-X-CSRF-TOKEN: j6aKfcvZoope4vUmnmdu7lfgapH7Vy1eFeDrXqRc5fxDMan8v8WySfrol71z1JQV-Epa3meCR_PPYxRzcdWJbJVs051zB8jM
+X-CSRF-TOKEN: jkbedfcIXIrRacqNcKGVTsBsw53yPTTfVw30dV0v_LMId7rA63K9Q8FpZLz8DPy_QoyhfKYN7v_ECgzyMjuXQmgWyYJuQIv4
 Host: localhost:8080
 
 {
diff --git a/src/test/java/com/dnd/jjakkak/domain/meeting/MeetingDummy.java b/src/test/java/com/dnd/jjakkak/domain/meeting/MeetingDummy.java
index df49914..d0f4266 100644
--- a/src/test/java/com/dnd/jjakkak/domain/meeting/MeetingDummy.java
+++ b/src/test/java/com/dnd/jjakkak/domain/meeting/MeetingDummy.java
@@ -3,6 +3,8 @@
 import com.dnd.jjakkak.domain.meeting.dto.request.MeetingCreateRequestDto;
 import com.dnd.jjakkak.domain.meeting.dto.response.*;
 import com.dnd.jjakkak.domain.meeting.entity.Meeting;
+import com.dnd.jjakkak.global.common.PageInfo;
+import com.dnd.jjakkak.global.common.PagedResponse;
 import org.springframework.test.util.ReflectionTestUtils;
 
 import java.time.LocalDate;
@@ -64,7 +66,7 @@ public static MeetingInfoResponseDto createInfoResponse() {
         return responseDto;
     }
 
-    public static MeetingTimeResponseDto createMeetingTimeResponseDto() {
+    public static PagedResponse createMeetingTimeResponseDto() {
         MeetingTime response = MeetingTime.builder()
                 .startTime(LocalDateTime.of(2024, 8, 27, 10, 0))
                 .endTime(LocalDateTime.of(2024, 8, 27, 12, 0))
@@ -79,7 +81,9 @@ public static MeetingTimeResponseDto createMeetingTimeResponseDto() {
         MeetingTimeResponseDto responseDto = new MeetingTimeResponseDto(2, false, startDate, endDate);
         responseDto.addMeetingTimeList(List.of(response));
 
-        return responseDto;
+        PageInfo pageInfo = new PageInfo(1, 1, 1, 1);
+
+        return new PagedResponse<>(responseDto, pageInfo);
     }
 
 
diff --git a/src/test/java/com/dnd/jjakkak/domain/meeting/controller/MeetingControllerTest.java b/src/test/java/com/dnd/jjakkak/domain/meeting/controller/MeetingControllerTest.java
index c78e018..cf07259 100644
--- a/src/test/java/com/dnd/jjakkak/domain/meeting/controller/MeetingControllerTest.java
+++ b/src/test/java/com/dnd/jjakkak/domain/meeting/controller/MeetingControllerTest.java
@@ -7,6 +7,7 @@
 import com.dnd.jjakkak.domain.meeting.service.MeetingService;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -15,6 +16,7 @@
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.http.MediaType;
 
+import java.time.LocalDateTime;
 import java.util.List;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -22,8 +24,7 @@
 import static org.mockito.Mockito.when;
 import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
 import static org.springframework.restdocs.payload.PayloadDocumentation.*;
-import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
-import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
+import static org.springframework.restdocs.request.RequestDocumentation.*;
 import static org.springframework.restdocs.snippet.Attributes.key;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -43,6 +44,8 @@ class MeetingControllerTest extends AbstractRestDocsTest {
 
     @Autowired
     ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
+    @Autowired
+    private ParameterNamesModule parameterNamesModule;
 
 
     @Test
@@ -151,37 +154,49 @@ void getMeetingInfo_success() throws Exception {
     void getBestTime_success() throws Exception {
 
         String meetingUuid = "123ABC";
-        when(meetingService.getMeetingTimes(anyString(), any()))
+        when(meetingService.getMeetingTimes(anyString(), any(), any()))
                 .thenReturn(MeetingDummy.createMeetingTimeResponseDto());
 
+        String now = LocalDateTime.now().toString();
+
         mockMvc.perform(get("/api/v1/meetings/{meetingUuid}/times", meetingUuid)
                         .param("sort", "COUNT")
+                        .param("request_time", now)
                         .contentType(MediaType.APPLICATION_JSON)
                         .accept(MediaType.APPLICATION_JSON))
                 .andExpectAll(
                         status().isOk(),
-                        jsonPath("$.numberOfPeople").value(2),
-                        jsonPath("$.isAnonymous").value(false),
-                        jsonPath("$.meetingStartDate").value("2024-08-27"),
-                        jsonPath("$.meetingEndDate").value("2024-08-29"),
-                        jsonPath("$.meetingTimeList[0].memberNames.[0]").value("고래"),
-                        jsonPath("$.meetingTimeList[0].memberNames.[1]").value("상어"),
-                        jsonPath("$.meetingTimeList[0].startTime").value("2024-08-27T10:00:00"),
-                        jsonPath("$.meetingTimeList[0].endTime").value("2024-08-27T12:00:00"),
-                        jsonPath("$.meetingTimeList[0].rank").value(1.0))
+                        jsonPath("$.data.numberOfPeople").value(2),
+                        jsonPath("$.data.isAnonymous").value(false),
+                        jsonPath("$.data.meetingStartDate").value("2024-08-27"),
+                        jsonPath("$.data.meetingEndDate").value("2024-08-29"),
+                        jsonPath("$.data.meetingTimeList[0].memberNames.[0]").value("고래"),
+                        jsonPath("$.data.meetingTimeList[0].memberNames.[1]").value("상어"),
+                        jsonPath("$.data.meetingTimeList[0].startTime").value("2024-08-27T10:00:00"),
+                        jsonPath("$.data.meetingTimeList[0].endTime").value("2024-08-27T12:00:00"),
+                        jsonPath("$.data.meetingTimeList[0].rank").value(1.0))
                 .andDo(restDocs.document(
                         pathParameters(
                                 parameterWithName("meetingUuid").description("모임 UUID")
                         ),
+                        queryParameters(
+                                parameterWithName("request_time").description("요청 시간"),
+                                parameterWithName("sort").description("정렬 기준 (COUNT, RANK)")
+                        ),
                         responseFields(
-                                fieldWithPath("numberOfPeople").description("총 인원 수"),
-                                fieldWithPath("isAnonymous").description("익명 여부"),
-                                fieldWithPath("meetingStartDate").description("모임 시작 날짜"),
-                                fieldWithPath("meetingEndDate").description("모임 종료 날짜"),
-                                fieldWithPath("meetingTimeList[].memberNames").description("멤버 이름 리스트"),
-                                fieldWithPath("meetingTimeList[].startTime").description("시작 시간"),
-                                fieldWithPath("meetingTimeList[].endTime").description("종료 시간"),
-                                fieldWithPath("meetingTimeList[].rank").description("우선순위 (오름차순)")
+                                fieldWithPath("data.numberOfPeople").description("총 인원 수"),
+                                fieldWithPath("data.isAnonymous").description("익명 여부"),
+                                fieldWithPath("data.meetingStartDate").description("모임 시작 날짜"),
+                                fieldWithPath("data.meetingEndDate").description("모임 종료 날짜"),
+                                fieldWithPath("data.requestTime").description("요청 시간"),
+                                fieldWithPath("data.meetingTimeList[].memberNames").description("멤버 이름 리스트"),
+                                fieldWithPath("data.meetingTimeList[].startTime").description("시작 시간"),
+                                fieldWithPath("data.meetingTimeList[].endTime").description("종료 시간"),
+                                fieldWithPath("data.meetingTimeList[].rank").description("우선순위 (오름차순)"),
+                                fieldWithPath("pageInfo.page").description("현재 페이지"),
+                                fieldWithPath("pageInfo.size").description("페이지 크기"),
+                                fieldWithPath("pageInfo.totalElements").description("총 요소 수"),
+                                fieldWithPath("pageInfo.totalPages").description("총 페이지 수")
                         ))
                 );
     }
diff --git a/src/test/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryTest.java b/src/test/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryTest.java
index df308f2..c6bd173 100644
--- a/src/test/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryTest.java
+++ b/src/test/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryTest.java
@@ -7,16 +7,18 @@
 import com.dnd.jjakkak.domain.meeting.dto.response.MeetingTime;
 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.meetingcategory.entity.MeetingCategory;
 import com.dnd.jjakkak.domain.member.entity.Member;
 import com.dnd.jjakkak.domain.schedule.entity.Schedule;
+import com.dnd.jjakkak.global.common.PageInfo;
+import com.dnd.jjakkak.global.common.PagedResponse;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
 import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
+import org.springframework.data.domain.Pageable;
 import org.springframework.test.util.ReflectionTestUtils;
 
 import java.time.LocalDate;
@@ -167,6 +169,8 @@ void getMeetingInfo() {
     @DisplayName("모임 시간 조회 - COUNT 기준 정렬")
     void getMeetingTimes_defaultSort() {
 
+        LocalDateTime now = LocalDateTime.now();
+
         // given
         Schedule schedule1 = Schedule.builder()
                 .meeting(testMeeting)
@@ -174,6 +178,8 @@ void getMeetingTimes_defaultSort() {
                 .scheduleUuid("123abc")
                 .build();
 
+        schedule1.changeAssignedAt(now);
+
         em.persist(schedule1);
 
 
@@ -200,6 +206,8 @@ void getMeetingTimes_defaultSort() {
                 .scheduleUuid("456def")
                 .build();
 
+        schedule2.changeAssignedAt(now);
+
         em.persist(schedule2);
 
         DateOfSchedule dateOfSchedule3 = DateOfSchedule.builder()
@@ -216,18 +224,30 @@ void getMeetingTimes_defaultSort() {
         String uuid = "123abc";
 
         // when
-        MeetingTimeResponseDto actual = meetingRepository.getMeetingTimes(uuid, MeetingSort.COUNT);
+        Pageable pageable = Pageable.ofSize(10);
+        PagedResponse actual = meetingRepository.getMeetingTimes(uuid, pageable, LocalDateTime.now());
 
         // then
-        assertEquals(2, actual.getMeetingTimeList().size());
 
-        MeetingTime primary = actual.getMeetingTimeList().get(0);
+        PageInfo pageInfo = actual.getPageInfo();
+        assertAll(
+                () -> assertEquals(2, pageInfo.getTotalElements()),
+                () -> assertEquals(1, pageInfo.getTotalPages())
+        );
+
+        MeetingTimeResponseDto data = actual.getData();
+        assertAll(
+                () -> assertEquals(2, data.getMeetingTimeList().size())
+        );
+
+
+        MeetingTime primary = data.getMeetingTimeList().get(0);
         assertAll(
                 () -> assertEquals(dateOfSchedule1.getDateOfScheduleStart(), primary.getStartTime()),
                 () -> assertEquals(dateOfSchedule1.getDateOfScheduleEnd(), primary.getEndTime())
         );
 
-        MeetingTime secondary = actual.getMeetingTimeList().get(1);
+        MeetingTime secondary = data.getMeetingTimeList().get(1);
         assertAll(
                 () -> assertEquals(dateOfSchedule2.getDateOfScheduleStart(), secondary.getStartTime()),
                 () -> assertEquals(dateOfSchedule2.getDateOfScheduleEnd(), secondary.getEndTime())
@@ -239,6 +259,7 @@ void getMeetingTimes_defaultSort() {
     @DisplayName("모임 시간 조회 - LATEST 기준 정렬")
     void getMeetingTimes_latestSort() {
         // given
+        LocalDateTime now = LocalDateTime.now();
 
         Schedule schedule1 = Schedule.builder()
                 .meeting(testMeeting)
@@ -246,6 +267,7 @@ void getMeetingTimes_latestSort() {
                 .scheduleUuid("123abc")
                 .build();
 
+        schedule1.changeAssignedAt(now);
 
         DateOfSchedule dateOfSchedule1 = DateOfSchedule.builder()
                 .schedule(schedule1)
@@ -263,6 +285,8 @@ void getMeetingTimes_latestSort() {
                 .scheduleUuid("456def")
                 .build();
 
+        schedule2.changeAssignedAt(now);
+
         DateOfSchedule dateOfSchedule2 = DateOfSchedule.builder()
                 .schedule(schedule2)
                 .dateOfScheduleRank(2)
@@ -278,18 +302,26 @@ void getMeetingTimes_latestSort() {
         String uuid = "123abc";
 
         // when
-        MeetingTimeResponseDto actual = meetingRepository.getMeetingTimes(uuid, MeetingSort.LATEST);
+        Pageable pageable = Pageable.ofSize(10);
+        PagedResponse actual = meetingRepository.getMeetingTimes(uuid, pageable, LocalDateTime.now());
 
         // then
-        assertEquals(2, actual.getMeetingTimeList().size());
+        PageInfo pageInfo = actual.getPageInfo();
+        assertAll(
+                () -> assertEquals(2, pageInfo.getTotalElements()),
+                () -> assertEquals(1, pageInfo.getTotalPages())
+        );
+
+        MeetingTimeResponseDto data = actual.getData();
+        assertEquals(2, data.getMeetingTimeList().size());
 
-        MeetingTime primary = actual.getMeetingTimeList().get(0);
+        MeetingTime primary = data.getMeetingTimeList().get(0);
         assertAll(
                 () -> assertEquals(dateOfSchedule2.getDateOfScheduleStart(), primary.getStartTime()),
                 () -> assertEquals(dateOfSchedule2.getDateOfScheduleEnd(), primary.getEndTime())
         );
 
-        MeetingTime secondary = actual.getMeetingTimeList().get(1);
+        MeetingTime secondary = data.getMeetingTimeList().get(1);
         assertAll(
                 () -> assertEquals(dateOfSchedule1.getDateOfScheduleStart(), secondary.getStartTime()),
                 () -> assertEquals(dateOfSchedule1.getDateOfScheduleEnd(), secondary.getEndTime())
@@ -328,7 +360,6 @@ void getParticipant() {
         em.persist(schedule2);
 
         em.flush();
-        // em.clear();  // clear를 사용하면 엔티티가 detached 상태가 되므로 주의 필요
 
         String uuid = "123abc";
 
diff --git a/src/test/java/com/dnd/jjakkak/domain/meeting/service/MeetingServiceTest.java b/src/test/java/com/dnd/jjakkak/domain/meeting/service/MeetingServiceTest.java
index 5f055e0..e3732ee 100644
--- a/src/test/java/com/dnd/jjakkak/domain/meeting/service/MeetingServiceTest.java
+++ b/src/test/java/com/dnd/jjakkak/domain/meeting/service/MeetingServiceTest.java
@@ -9,7 +9,6 @@
 import com.dnd.jjakkak.domain.meeting.dto.response.MeetingTime;
 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.repository.MeetingRepository;
 import com.dnd.jjakkak.domain.meetingcategory.repository.MeetingCategoryRepository;
@@ -17,14 +16,17 @@
 import com.dnd.jjakkak.domain.member.entity.Member;
 import com.dnd.jjakkak.domain.member.repository.MemberRepository;
 import com.dnd.jjakkak.domain.schedule.service.ScheduleService;
+import com.dnd.jjakkak.global.common.PagedResponse;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Pageable;
 import org.springframework.test.util.ReflectionTestUtils;
 
+import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Optional;
 
@@ -182,26 +184,28 @@ void testGetMeetingBestTime_Success() {
 
         // given
         String uuid = "1234abcd";
-        MeetingTimeResponseDto expected = MeetingDummy.createMeetingTimeResponseDto();
+        PagedResponse expected = MeetingDummy.createMeetingTimeResponseDto();
 
-        when(meetingRepository.getMeetingTimes(anyString(), any()))
+        when(meetingRepository.getMeetingTimes(anyString(), any(), any()))
                 .thenReturn(expected);
 
         when(meetingRepository.existsByMeetingUuid(anyString()))
                 .thenReturn(true);
 
         // when
-        MeetingTimeResponseDto actual = meetingService.getMeetingTimes(uuid, MeetingSort.COUNT);
+        Pageable pageable = Pageable.ofSize(10);
+        LocalDateTime now = LocalDateTime.now();
+        PagedResponse actual = meetingService.getMeetingTimes(uuid, pageable, now);
 
         // then
         assertAll(
-                () -> assertEquals(expected.getNumberOfPeople(), actual.getNumberOfPeople()),
-                () -> assertEquals(expected.getIsAnonymous(), actual.getIsAnonymous())
+                () -> assertEquals(expected.getData().getNumberOfPeople(), actual.getData().getNumberOfPeople()),
+                () -> assertEquals(expected.getData().getIsAnonymous(), actual.getData().getIsAnonymous())
         );
 
-        assertEquals(expected.getMeetingTimeList().size(), actual.getMeetingTimeList().size());
-        MeetingTime expectedTime = expected.getMeetingTimeList().get(0);
-        MeetingTime actualTime = actual.getMeetingTimeList().get(0);
+        assertEquals(expected.getData().getMeetingTimeList().size(), actual.getData().getMeetingTimeList().size());
+        MeetingTime expectedTime = expected.getData().getMeetingTimeList().get(0);
+        MeetingTime actualTime = actual.getData().getMeetingTimeList().get(0);
 
         assertAll(
                 () -> assertEquals(expectedTime.getMemberNames(), actualTime.getMemberNames()),
@@ -210,7 +214,7 @@ void testGetMeetingBestTime_Success() {
                 () -> assertEquals(expectedTime.getRank(), actualTime.getRank())
         );
 
-        verify(meetingRepository, times(1)).getMeetingTimes(uuid, MeetingSort.COUNT);
+        verify(meetingRepository, times(1)).getMeetingTimes(uuid, pageable, now);
         verify(meetingRepository, times(1)).existsByMeetingUuid(uuid);
     }
 
@@ -225,7 +229,7 @@ void testGetMeetingBestTime_Fail() {
 
         // expected
         assertThrows(MeetingNotFoundException.class,
-                () -> meetingService.getMeetingTimes(uuid, MeetingSort.COUNT));
+                () -> meetingService.getMeetingTimes(uuid, Pageable.ofSize(10), LocalDateTime.now()));
     }
 
     @Test