Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/ErrorCode.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
| M016 | MEETING_JOIN_NOT_ALLOWED | 약속 시작 24시간 이내에는 참가 신청할 수 없습니다. | 400 |
| M017 | MEETING_UPDATE_NOT_ALLOWED | 약속 시작 24시간 이내에는 수정할 수 없습니다. | 400 |
| M018 | MEETING_NOT_CONFIRMED | 약속이 확정된 경우에만 주제를 제안할 수 있습니다. | 400 |
| M019 | MEETING_DATE_REQUIRED | 약속 시작/종료 일시는 필수입니다. | 400 |

---

Expand Down
21 changes: 7 additions & 14 deletions src/main/java/com/dokdok/meeting/api/MeetingListApi.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
package com.dokdok.meeting.api;

import com.dokdok.global.response.ApiResponse;
import com.dokdok.global.response.CursorResponse;
import com.dokdok.global.response.PageResponse;
import com.dokdok.meeting.dto.MeetingListFilter;
import com.dokdok.meeting.dto.MeetingListItemCursorResponse;
import com.dokdok.meeting.dto.MeetingListItemPageResponse;
import com.dokdok.meeting.dto.MeetingListItemResponse;
import com.dokdok.meeting.dto.MeetingListCursor;
import com.dokdok.meeting.dto.MeetingListCursorRequest;
import com.dokdok.meeting.entity.MeetingStatus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
Expand Down Expand Up @@ -51,7 +47,7 @@ public interface MeetingListApi {
responseCode = "200",
description = "약속 리스트 조회 성공",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = MeetingListItemCursorResponse.class),
schema = @Schema(implementation = MeetingListItemPageResponse.class),
examples = @ExampleObject(value = """
{
"code": "SUCCESS",
Expand All @@ -69,12 +65,9 @@ public interface MeetingListApi {
}
],
"totalCount": 12,
"pageSize": 4,
"hasNext": true,
"nextCursor": {
"meetingId": 2,
"startDateTime": "2025-02-03T14:00:00"
}
"currentPage": 0,
"pageSize": 5,
"totalPages": 3
}
}
"""))
Expand All @@ -100,11 +93,11 @@ public interface MeetingListApi {
{"code": "E000", "message": "서버 에러가 발생했습니다. 담당자에게 문의 바랍니다.", "data": null}
""")))
})
ResponseEntity<ApiResponse<CursorResponse<MeetingListItemResponse, MeetingListCursor>>> getMeetingList(
ResponseEntity<ApiResponse<PageResponse<MeetingListItemResponse>>> getMeetingList(
@PathVariable Long gatheringId,
@RequestParam MeetingListFilter filter,
@ParameterObject MeetingListCursorRequest cursor,
@RequestParam(defaultValue = "4") int size
@ParameterObject
@PageableDefault(size = 5, sort = {"meetingStartDate", "id"}) Pageable pageable
);

@Operation(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package com.dokdok.meeting.controller;

import com.dokdok.global.response.ApiResponse;
import com.dokdok.global.response.CursorResponse;
import com.dokdok.global.response.PageResponse;
import com.dokdok.meeting.api.MeetingListApi;
import com.dokdok.meeting.dto.MeetingListCursor;
import com.dokdok.meeting.dto.MeetingListCursorRequest;
import com.dokdok.meeting.dto.MeetingListFilter;
import com.dokdok.meeting.dto.MeetingListItemResponse;
import com.dokdok.meeting.entity.MeetingStatus;
Expand All @@ -31,15 +28,14 @@ public class MeetingListController implements MeetingListApi {

@Override
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ApiResponse<CursorResponse<MeetingListItemResponse, MeetingListCursor>>> getMeetingList(
public ResponseEntity<ApiResponse<PageResponse<MeetingListItemResponse>>> getMeetingList(
@PathVariable Long gatheringId,
@RequestParam MeetingListFilter filter,
@ParameterObject MeetingListCursorRequest cursor,
@RequestParam(defaultValue = "4") int size
@ParameterObject
@PageableDefault(size = 5, sort = {"meetingStartDate", "id"}) Pageable pageable
) {
MeetingListCursor cursorValue = cursor == null ? null : cursor.toCursorOrNull();
CursorResponse<MeetingListItemResponse, MeetingListCursor> response =
meetingService.meetingList(gatheringId, filter, size, cursorValue);
PageResponse<MeetingListItemResponse> response =
meetingService.meetingList(gatheringId, filter, pageable);
return ApiResponse.success(response, "약속 리스트 조회에 성공했습니다.");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.dokdok.meeting.entity.MeetingLocation;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Builder;

Expand All @@ -21,9 +22,11 @@ public record MeetingCreateRequest(
String meetingName,

@Schema(description = "약속 시작 일시", example = "2025-02-01T14:00:00", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull
LocalDateTime meetingStartDate,

@Schema(description = "약속 종료 일시", example = "2025-02-01T16:00:00")
@Schema(description = "약속 종료 일시", example = "2025-02-01T16:00:00", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull
LocalDateTime meetingEndDate,

@Schema(description = "최대 참가 인원 (null 허용)", example = "10")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "약속 상세 진행 상태 (시간 기준) - PRE(약속 전), ONGOING(약속 중), POST(약속 후), UNKNOWN(일정 미정)")
@Schema(description = "약속 상세 진행 상태 (시간 기준) - PRE(약속 전), ONGOING(약속 중), POST(약속 후)")
public enum MeetingDetailProgressStatus {
PRE,
ONGOING,
POST,
UNKNOWN
POST
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ private static MeetingDetailProgressStatus resolveProgressStatus(
LocalDateTime now
) {
if (meetingStartDate == null || meetingEndDate == null) {
return MeetingDetailProgressStatus.UNKNOWN;
throw new IllegalStateException("meetingStartDate/meetingEndDate must be non-null");
}
if (now.isBefore(meetingStartDate)) {
return MeetingDetailProgressStatus.PRE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public enum MeetingErrorCode implements BaseErrorCode {
MEETING_JOIN_NOT_ALLOWED("M016", "약속 시작 24시간 이내에는 참가 신청할 수 없습니다.", HttpStatus.BAD_REQUEST),
MEETING_UPDATE_NOT_ALLOWED("M017", "약속 시작 24시간 이내에는 수정할 수 없습니다.", HttpStatus.BAD_REQUEST),
MEETING_NOT_CONFIRMED("M018", "약속이 확정된 경우에만 주제를 제안할 수 있습니다.", HttpStatus.BAD_REQUEST),
MEETING_DATE_REQUIRED("M019", "약속 시작/종료 일시는 필수입니다.", HttpStatus.BAD_REQUEST),

INVALID_MAX_PARTICIPANTS("M013", "최대 참가 인원은 1명 이상이어야 하며, 모임 전체 인원을 초과할 수 없습니다.", HttpStatus.BAD_REQUEST),
MAX_PARTICIPANTS_LESS_THAN_CURRENT("M014", "현재 참가 확정된 인원 수보다 적게 수정할 수 없습니다.", HttpStatus.BAD_REQUEST);
Expand Down
154 changes: 57 additions & 97 deletions src/main/java/com/dokdok/meeting/service/MeetingService.java
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ public MeetingResponse createMeeting(MeetingCreateRequest request) {
.countByGatheringIdAndRemovedAtIsNull(gathering.getId());
}

validateMeetingDatesRequired(request.meetingStartDate(), request.meetingEndDate());

// 최대 참가 인원 검증
validateMaxParticipants(maxParticipants, gathering.getId());

Expand Down Expand Up @@ -467,6 +469,7 @@ private void validateMeetingDates(MeetingUpdateRequest request, Meeting meeting)
LocalDateTime endDate = request.endDate() != null
? request.endDate()
: meeting.getMeetingEndDate();
validateMeetingDatesRequired(startDate, endDate);
if (startDate != null && endDate != null && endDate.isBefore(startDate)) {
throw new MeetingException(
MeetingErrorCode.INVALID_MEETING_STATUS_CHANGE,
Expand All @@ -475,6 +478,15 @@ private void validateMeetingDates(MeetingUpdateRequest request, Meeting meeting)
}
}

private void validateMeetingDatesRequired(LocalDateTime startDate, LocalDateTime endDate) {
if (startDate == null || endDate == null) {
throw new MeetingException(
MeetingErrorCode.MEETING_DATE_REQUIRED,
"약속 시작/종료 일시는 필수입니다."
);
}
}

/**
* 약속 리스트를 조회한다.
* 약속 : 모임에서 약속이 확정된 전체 약속
Expand All @@ -483,24 +495,22 @@ private void validateMeetingDates(MeetingUpdateRequest request, Meeting meeting)
* 내가 참여한 약속 : 완전히 끝난 약속 중 내가 참여한 약속
* @param gatheringId 모임 식별자
* @param filter 약속 리스트 필터
* @param size 페이지 크기
* @param cursor 커서
* @return CursorResponse
* @param pageable 페이지 정보
* @return PageResponse
*/
public CursorResponse<MeetingListItemResponse, MeetingListCursor> meetingList(
public PageResponse<MeetingListItemResponse> meetingList(
Long gatheringId,
MeetingListFilter filter,
int size,
MeetingListCursor cursor
Pageable pageable
) {
Long userId = SecurityUtil.getCurrentUserId();
gatheringValidator.validateMembership(gatheringId, userId);

return switch (filter) {
case ALL -> getAllMeetings(gatheringId, size, cursor, userId);
case UPCOMING -> getUpcomingMeetings(gatheringId, size, cursor, userId);
case DONE -> getDoneMeetings(gatheringId, size, cursor, userId);
case JOINED -> getJoinedMeetings(gatheringId, size, cursor, userId);
case ALL -> getAllMeetingsPage(gatheringId, pageable, userId);
case UPCOMING -> getUpcomingMeetingsPage(gatheringId, pageable, userId);
case DONE -> getDoneMeetingsPage(gatheringId, pageable, userId);
case JOINED -> getJoinedMeetingsPage(gatheringId, pageable, userId);
};

}
Expand Down Expand Up @@ -674,138 +684,88 @@ public MyMeetingTabCountsResponse getMyMeetingTabCounts() {
}

/**
* 모임의 약속 중 확정된 리스트를 전부 반환한다.
* 모임의 약속 중 확정된 리스트를 페이지로 반환한다.
*/
private CursorResponse<MeetingListItemResponse, MeetingListCursor> getAllMeetings(
private PageResponse<MeetingListItemResponse> getAllMeetingsPage(
Long gatheringId,
int size,
MeetingListCursor cursor,
Pageable pageable,
Long userId
) {
Pageable pageable = cursorPageable(size);
List<Meeting> meetings = meetingRepository.findByGatheringIdAndMeetingStatusAfterCursor(
Page<Meeting> meetingPage = meetingRepository.findByGatheringIdAndMeetingStatus(
gatheringId,
MeetingStatus.CONFIRMED,
cursorStartDateTime(cursor),
cursorMeetingId(cursor),
pageable
);
Integer totalCount = cursor == null
? meetingRepository.countByGatheringIdAndMeetingStatus(gatheringId, MeetingStatus.CONFIRMED)
: null;
return buildMeetingListResponse(meetings, size, userId, gatheringId, totalCount);
return buildMeetingPageResponse(meetingPage, userId, gatheringId);
}

/**
* 다가오는 약속 리스트를 반환한다.
* 다가오는 약속 리스트를 페이지로 반환한다.
*/
private CursorResponse<MeetingListItemResponse, MeetingListCursor> getUpcomingMeetings(
private PageResponse<MeetingListItemResponse> getUpcomingMeetingsPage(
Long gatheringId,
int size,
MeetingListCursor cursor,
Pageable pageable,
Long userId
) {
LocalDateTime now = LocalDateTime.now();
Pageable pageable = cursorPageable(size);

List<Meeting> meetings = meetingRepository
.findByGatheringIdAndMeetingStatusAndMeetingStartDateBetweenAfterCursor(
gatheringId,
MeetingStatus.CONFIRMED,
now,
now.plusDays(3),
cursorStartDateTime(cursor),
cursorMeetingId(cursor),
pageable
);

Integer totalCount = cursor == null
? meetingRepository.countUpcomingMeetings(
gatheringId,
MeetingStatus.CONFIRMED,
now,
now.plusDays(3)
)
: null;
return buildMeetingListResponse(meetings, size, userId, gatheringId, totalCount);
Page<Meeting> meetingPage = meetingRepository.findByGatheringIdAndMeetingStatusAndMeetingStartDateBetween(
gatheringId,
MeetingStatus.CONFIRMED,
now,
now.plusDays(3),
pageable
);
return buildMeetingPageResponse(meetingPage, userId, gatheringId);
}

/**
* 완료된 약속 리스트를 반환한다.
* 완료된 약속 리스트를 페이지로 반환한다.
*/
private CursorResponse<MeetingListItemResponse, MeetingListCursor> getDoneMeetings(
private PageResponse<MeetingListItemResponse> getDoneMeetingsPage(
Long gatheringId,
int size,
MeetingListCursor cursor,
Pageable pageable,
Long userId
) {
Pageable pageable = cursorPageable(size);
List<Meeting> meetings = meetingRepository.findByGatheringIdAndMeetingStatusAfterCursor(
Page<Meeting> meetingPage = meetingRepository.findByGatheringIdAndMeetingStatus(
gatheringId,
MeetingStatus.DONE,
cursorStartDateTime(cursor),
cursorMeetingId(cursor),
pageable
);
Integer totalCount = cursor == null
? meetingRepository.countByGatheringIdAndMeetingStatus(gatheringId, MeetingStatus.DONE)
: null;
return buildMeetingListResponse(meetings, size, userId, gatheringId, totalCount);
return buildMeetingPageResponse(meetingPage, userId, gatheringId);
}

/**
* 완료된 약속 중 내가 참여했던 약속 리스트를 반환한다.
* 완료된 약속 중 내가 참여했던 약속 리스트를 페이지로 반환한다.
*/
private CursorResponse<MeetingListItemResponse, MeetingListCursor> getJoinedMeetings(
private PageResponse<MeetingListItemResponse> getJoinedMeetingsPage(
Long gatheringId,
int size,
MeetingListCursor cursor,
Pageable pageable,
Long userId
) {
Pageable pageable = cursorPageable(size);
List<Meeting> meetings = meetingMemberRepository.findMeetingsByUserIdAndStatusAfterCursor(
Page<Meeting> meetingPage = meetingMemberRepository.findMeetingsByUserIdAndStatus(
userId,
gatheringId,
MeetingStatus.DONE,
cursorStartDateTime(cursor),
cursorMeetingId(cursor),
pageable
);
Integer totalCount = cursor == null
? meetingMemberRepository.countMeetingsByUserIdAndStatus(
userId,
gatheringId,
MeetingStatus.DONE
)
: null;
return buildMeetingListResponse(meetings, size, userId, gatheringId, totalCount);
return buildMeetingPageResponse(meetingPage, userId, gatheringId);
}

/**
* 약속 리스트 커서 응답을 구성한다.
* 약속 리스트 페이지 응답을 구성한다.
*/
private CursorResponse<MeetingListItemResponse, MeetingListCursor> buildMeetingListResponse(
List<Meeting> meetingCandidates,
int size,
private PageResponse<MeetingListItemResponse> buildMeetingPageResponse(
Page<Meeting> meetingPage,
Long userId,
Long gatheringId,
Integer totalCount
Long gatheringId
) {
boolean hasNext = meetingCandidates.size() > size;
List<Meeting> meetings = hasNext ? meetingCandidates.subList(0, size) : meetingCandidates;
if (meetings.isEmpty()) {
return CursorResponse.of(List.of(), size, false, null, totalCount);
}

List<MeetingListItemResponse> items = buildMeetingItems(meetings, userId, gatheringId);

MeetingListCursor nextCursor = null;
if (hasNext) {
Meeting last = meetings.get(meetings.size() - 1);
nextCursor = new MeetingListCursor(last.getMeetingStartDate(), last.getId());
}

return CursorResponse.of(items, size, hasNext, nextCursor, totalCount);
List<MeetingListItemResponse> items = buildMeetingItems(meetingPage.getContent(), userId, gatheringId);
return PageResponse.of(
items,
meetingPage.getTotalElements(),
meetingPage.getNumber(),
meetingPage.getSize()
);
}

/**
Expand Down
Loading