[Feat/#27] 학생 앱 행사 목록 조회 API 추가 - #28
Closed
leegain1 wants to merge 7 commits into
Closed
Conversation
github-actions
Bot
requested review from
jjunh33,
sangrae2325 and
tnals0924
September 12, 2026 07:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#️⃣연관된 이슈
🎯 해결하려는 문제가 무엇인가요?
학생 앱 홈에서 행사를 훑을 수 있는 목록 API가 없습니다. 무한 스크롤로 행사 시작이 가까운 순서대로 내려주되, 각 행사가 지금 신청 가능한지(모집 상태)와 마감까지 며칠 남았는지(D-Day)를 함께 보여줘야 합니다.
또한 운영진이 준비 중인 행사가 학생에게 노출되면 안 되는데,
events테이블에 게시 여부를 나타내는 컬럼이 없습니다.❓ 왜 해결해야 하나요?
행사 신청 플로우(#23)의 진입점입니다. 목록에서 행사를 고르지 못하면 신청서 폼 조회·신청 API를 쓸 수가 없습니다.
⭐ 어떻게 해결했나요?
GET /v1/app/events—cursor,size(1~100, 기본 20),recruitStatus쿼리 파라미터를 받습니다.모집 상태는 저장값이 아니라 조회 시점에 계산합니다. #23이 추가한
Event.calculateRecruitStatus(now, appliedCount)를 그대로 호출합니다. 목록에서 판정을 다시 구현하면 폼 조회·신청과 어긋날 수 있어 도메인 메서드 하나만 쓰도록 했습니다. D-Day는OPEN일 때만 내려갑니다.커서는
(event_start_at, event_id)keyset입니다. 정렬 키가 두 개라nextCursor를Long에 담을 수 없어CursorSliceResult/CursorSliceResponse의 타입을 불투명 문자열로 바꿨습니다. 정렬 키의 문자열 표현은 도메인(EventCursor.format()/from())이, Base64 URL-safe 인코딩은 웹 계층(CursorCodec)이 맡습니다.coding-style.md2-4절도 함께 갱신했습니다.조회는 keyset +
size + 1로 다음 페이지 존재 여부를 판정하고, 신청자 수는 페이지의eventId를 모아IN쿼리 한 번으로 집계해 N+1을 피했습니다.게시 여부 —
events.is_published(V5)와(is_published, is_deleted, event_start_at)복합 인덱스를 추가했습니다. 필터 컬럼을 앞, 정렬 컬럼을 뒤에 둔 형태로flyway-migration.md3-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/CursorSliceResponse의nextCursor타입이Long→String으로 바뀝니다. 현재 이 타입을 쓰는 API가 없어 깨지는 곳은 없습니다.CursorCodec,CursorSliceResult,CursorSliceResponse. 내용은 완전히 동일합니다. 먼저 머지되는 쪽 기준으로 나중 PR이 해당 커밋을 덜어내면 됩니다. 이번 PR에서 빼지 않은 이유는, 빼면 #21까지 머지되어야 컴파일이 되기 때문입니다.Event.of(...)시그니처에published가 추가됩니다. 현재 호출부는EventJpaEntity.toDomain()하나뿐이라 함께 수정했습니다.기존 행사가 전부 비공개로 바뀝니다.
is_published가DEFAULT 0이라 이미 들어있는 행사는 목록에 나오지 않습니다. 게시 상태를 켜는 운영진 API가 아직 없어 당분간 DB에서 직접 켜야 합니다. 운영 데이터가 있다면 백필이 필요한지 확인 부탁드립니다.🔀 Edge Case & 실패 시나리오
CommonErrorCode.INVALID_INPUT(400)EventErrorCode.EVENT_INVALID_CURSOR(400)recruitStatus값EventErrorCode.EVENT_INVALID_RECRUIT_STATUS(400). enum을 직접 바인딩하면MethodArgumentTypeMismatchException이GlobalExceptionHandler에 없어 500이 나가서,String으로 받아RecruitStatus.from으로 걸렀습니다size범위 밖size미지정hasNext = false,nextCursor = nullcontent,hasNext = falseCLOSED.Event.isCapacityFull(#23)의 판정을 그대로 따릅니다recruitType = OPEN)capacity값과 무관하게 정원으로 마감되지 않습니다event_id오름차순으로 갈라 커서가 건너뛰거나 중복되지 않습니다📋 검토한 대안과 선택 이유
오프셋 페이지네이션 — 무한 스크롤이라 전체 개수·페이지 번호가 필요 없고, 뒤 페이지로 갈수록 느려집니다.
coding-style.md2-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]EventSummary를EventListItemResponse로 옮기는 매핑을 컨트롤러 private 메서드에 뒀습니다.