Skip to content

[Feat/#27] 학생 앱 행사 목록 조회 API 추가 - #28

Closed
leegain1 wants to merge 7 commits into
feat/#23-event-applicationfrom
feat/#27-event-list-api
Closed

[Feat/#27] 학생 앱 행사 목록 조회 API 추가#28
leegain1 wants to merge 7 commits into
feat/#23-event-applicationfrom
feat/#27-event-list-api

Conversation

@leegain1

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈

⚠️ 이 PR은 #26(feat/#23-event-application) 위에 쌓은 stacked PR입니다. base가 main이 아니라 feat/#23-event-application이라 diff에는 이 PR의 커밋 7개만 보입니다. #26이 머지되면 base를 main으로 옮기고 리베이스하겠습니다.

🎯 해결하려는 문제가 무엇인가요?

학생 앱 홈에서 행사를 훑을 수 있는 목록 API가 없습니다. 무한 스크롤로 행사 시작이 가까운 순서대로 내려주되, 각 행사가 지금 신청 가능한지(모집 상태)와 마감까지 며칠 남았는지(D-Day)를 함께 보여줘야 합니다.

또한 운영진이 준비 중인 행사가 학생에게 노출되면 안 되는데, events 테이블에 게시 여부를 나타내는 컬럼이 없습니다.

❓ 왜 해결해야 하나요?

행사 신청 플로우(#23)의 진입점입니다. 목록에서 행사를 고르지 못하면 신청서 폼 조회·신청 API를 쓸 수가 없습니다.

⭐ 어떻게 해결했나요?

GET /v1/app/eventscursor, size(1~100, 기본 20), recruitStatus 쿼리 파라미터를 받습니다.

모집 상태는 저장값이 아니라 조회 시점에 계산합니다. #23이 추가한 Event.calculateRecruitStatus(now, appliedCount)를 그대로 호출합니다. 목록에서 판정을 다시 구현하면 폼 조회·신청과 어긋날 수 있어 도메인 메서드 하나만 쓰도록 했습니다. D-Day는 OPEN일 때만 내려갑니다.

커서는 (event_start_at, event_id) keyset입니다. 정렬 키가 두 개라 nextCursorLong에 담을 수 없어 CursorSliceResult/CursorSliceResponse의 타입을 불투명 문자열로 바꿨습니다. 정렬 키의 문자열 표현은 도메인(EventCursor.format()/from())이, Base64 URL-safe 인코딩은 웹 계층(CursorCodec)이 맡습니다. coding-style.md 2-4절도 함께 갱신했습니다.

조회는 keyset + size + 1 로 다음 페이지 존재 여부를 판정하고, 신청자 수는 페이지의 eventId를 모아 IN 쿼리 한 번으로 집계해 N+1을 피했습니다.

게시 여부events.is_published(V5)와 (is_published, is_deleted, event_start_at) 복합 인덱스를 추가했습니다. 필터 컬럼을 앞, 정렬 컬럼을 뒤에 둔 형태로 flyway-migration.md 3-4절을 따랐고, InnoDB가 세컨더리 인덱스 끝에 PK를 붙이므로 보조 정렬 키(event_id)까지 커버됩니다.

🧩 이 PR의 한계 & 트레이드오프

1. 모집 상태 판정 규칙이 두 곳에 있습니다. 자바(Event.calculateRecruitStatus)와 JPQL(EventJpaRepository.findPublishedSlice)에 같은 규칙이 있습니다. 필터를 DB에서 걸지 않으면 페이지 크기를 맞출 수 없어 불가피했습니다. 한쪽을 고치면 반드시 다른 쪽도 고쳐야 하며, 주석으로 명시해뒀습니다.

2. 모집 상태 필터에 서브쿼리가 붙습니다. 필터가 주어지면 행마다 COUNT 서브쿼리로 잔여 정원을 따집니다. 커서 페이지네이션이라 페이지당 최대 101행만 평가하지만, 신청자가 크게 늘면 부담이 될 수 있습니다. events에 신청자 수를 비정규화하는 방법을 검토했으나 갱신 지점이 늘어나 이번 범위 밖으로 뒀습니다.

3. thumbnailUrl이 항상 null입니다. 대표 이미지 파일 id는 EventSummary.thumbnailFileId()로 알 수 있지만, 파일 키 → 공개 URL 조립이 #17에 있어 아직 못 채웁니다. 응답 스펙만 먼저 고정한 상태입니다.

4. JPQL이 런타임 검증되지 않습니다. 저장소에 JPA 통합 테스트 인프라(H2·Testcontainers)가 없어 gradle check로는 쿼리 문법·매핑이 검증되지 않습니다. 로컬 MySQL로 확인이 필요합니다.

⛓️ 기존 기능에 미치는 영향

CursorSliceResult/CursorSliceResponsenextCursor 타입이 LongString으로 바뀝니다. 현재 이 타입을 쓰는 API가 없어 깨지는 곳은 없습니다.

⚠️ #21(공지 목록)과 파일 3개가 겹칩니다CursorCodec, CursorSliceResult, CursorSliceResponse. 내용은 완전히 동일합니다. 먼저 머지되는 쪽 기준으로 나중 PR이 해당 커밋을 덜어내면 됩니다. 이번 PR에서 빼지 않은 이유는, 빼면 #21까지 머지되어야 컴파일이 되기 때문입니다.

Event.of(...) 시그니처에 published가 추가됩니다. 현재 호출부는 EventJpaEntity.toDomain() 하나뿐이라 함께 수정했습니다.

기존 행사가 전부 비공개로 바뀝니다. is_publishedDEFAULT 0이라 이미 들어있는 행사는 목록에 나오지 않습니다. 게시 상태를 켜는 운영진 API가 아직 없어 당분간 DB에서 직접 켜야 합니다. 운영 데이터가 있다면 백필이 필요한지 확인 부탁드립니다.

🔀 Edge Case & 실패 시나리오

상황 처리
잘못된 커서 (Base64 깨짐) CommonErrorCode.INVALID_INPUT (400)
잘못된 커서 (형식·파싱 실패) EventErrorCode.EVENT_INVALID_CURSOR (400)
잘못된 recruitStatus EventErrorCode.EVENT_INVALID_RECRUIT_STATUS (400). enum을 직접 바인딩하면 MethodArgumentTypeMismatchExceptionGlobalExceptionHandler에 없어 500이 나가서, String으로 받아 RecruitStatus.from으로 걸렀습니다
size 범위 밖 Bean Validation (400)
size 미지정 20
마지막 페이지 hasNext = false, nextCursor = null
결과 없음 content, hasNext = false
정원 0 + 선착순 신청자 0명이어도 CLOSED. Event.isCapacityFull(#23)의 판정을 그대로 따릅니다
상시 모집(recruitType = OPEN) capacity 값과 무관하게 정원으로 마감되지 않습니다
행사 시작 일시가 같은 행사 여러 건 event_id 오름차순으로 갈라 커서가 건너뛰거나 중복되지 않습니다

📋 검토한 대안과 선택 이유

오프셋 페이지네이션 — 무한 스크롤이라 전체 개수·페이지 번호가 필요 없고, 뒤 페이지로 갈수록 느려집니다. coding-style.md 2-4절 기준으로 커서를 택했습니다.

모집 상태를 컬럼에 저장하고 스케줄러로 갱신 — 신청 기간 경계와 정원 충족 시점에 목록이 실제와 어긋납니다. 조회 시점 계산이 정확하고, 갱신 작업도 필요 없습니다.

모집 상태 필터를 애플리케이션에서 적용 — DB에서 size + 1을 읽어 자바에서 거르면 걸러낸 만큼 페이지가 비어 크기를 맞출 수 없습니다.

nextCursor를 인코딩 없이 그대로 노출 — 정렬 키 구조가 API 계약이 되어 나중에 바꾸기 어렵습니다. Base64로 감쌌습니다.

💬 리뷰 포인트

  • [r] EventJpaRepository.findPublishedSlice의 JPQL 필터Event.calculateRecruitStatus와 판정이 정확히 일치하는지 봐주세요. 세 필터(BEFORE_OPEN/OPEN/CLOSED)가 겹치거나 빠지는 행 없이 나뉘어야 합니다.
  • [r] is_published DEFAULT 0 — 기존 행사가 전부 목록에서 사라집니다. 백필이 필요한지 판단 부탁드립니다.
  • [c] 시각 기준 — #23의 EventServiceImpl과 동일하게 LocalDateTime.now()(서버 타임존)를 씁니다. 목록은 D-Day를 날짜 단위로 계산해서 타임존이 하루를 가르는데, 서버 타임존을 KST로 고정하는 게 안전해 보입니다. 별도 이슈로 뺄까요?
  • [c] RecruitStatus.from(String) — 웹 입력 파싱 책임을 도메인 enum에 뒀습니다. api 레이어로 옮기는 게 나을지 의견 주세요.
  • [a] EventSummaryEventListItemResponse로 옮기는 매핑을 컨트롤러 private 메서드에 뒀습니다.

@leegain1 leegain1 closed this Sep 12, 2026
@leegain1
leegain1 deleted the feat/#27-event-list-api branch September 12, 2026 07:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant