Skip to content

행사 신청서 폼 조회/신청 API 추가 #23

Description

@sangrae2325

어떤 기능인가요?

추가하려는 기능에 대해 간결하게 설명해주세요

학생 앱(app-api)에서 쓰는 행사 신청 관련 API 2개를 추가합니다 — 신청서 폼 조회, 행사 신청. core:domain:event에 이미 있는 Event/EventQuestion/EventApplication/EventApplicationAnswer 도메인 객체·JPA 엔티티·events 테이블(#13)을 기반으로 Service/Repository/Controller 레이어를 붙입니다.

📝 작업 상세 내용

  • GET /v1/app/events/{eventId}/form — 신청서 폼 조회. 행사 요약(title/eventStartAt/place)과 displayOrder 오름차순 질문 목록 반환. 질문마다 유형별 maxLength 포함
  • POST /v1/app/events/{eventId}/applications — 행사 신청. 답변 저장 후 applicationId와 행사 요약 반환
  • EventcalculateRecruitStatus(now, appliedCount) 추가 — 모집 상태를 서버가 계산 (아래 규칙)
  • QuestionType enum에 유형별 maxLength 추가 (SHORT_TEXT 50, LONG_TEXT 500, 선택형 null) — 폼 조회 응답과 신청 검증이 같은 상수를 참조
  • core:domain:eventEventErrorCode/EventRepository/EventService·EventServiceImpl 추가
  • infrastructure:dbEventJpaRepository/EventQuestionJpaRepository/EventApplicationJpaRepository/EventApplicationAnswerJpaRepositoryEventRepositoryImpl 추가
  • api:app-apiAppEventController와 요청·응답 DTO 추가, build.gradle.ktscore:domain:event 의존 추가
  • 두 API 모두 STUDENT 인증 필요 (/v1/app/**), 신청자 memberIdPrincipalProvider에서 가져옴

모집 상태 계산 규칙

recruitStatus는 DB 컬럼을 그대로 쓰지 않고 서버가 계산합니다. 행사 상세 조회 API 명세와 동일한 규칙입니다.

조건 결과
DB recruit_status == CLOSED (관리자 강제 마감) CLOSED
now < applyStartAt BEFORE_OPEN
now > applyEndAt CLOSED
recruitType == FIRST_COME && 신청자수 >= capacity CLOSED
그 외 OPEN
  • 정원 마감은 FIRST_COME일 때만 적용합니다. OPEN 모집은 정원 제한이 없습니다.
  • 폼 조회·신청 모두 계산된 상태가 OPEN일 때만 허용합니다.
  • 이 계산은 행사 상세 조회 API에서도 똑같이 나와야 하므로 Event 도메인 객체의 메서드로 두고 재사용합니다. (상세 화면은 D-2인데 신청은 마감되는 불일치 방지)

신청 검증 규칙

  • 계산된 모집 상태가 OPEN이 아니면 ALREADY_CLOSED (BEFORE_OPEN도 여기 포함)
  • FIRST_COME 정원 초과면 CAPACITY_FULL
  • 같은 행사에 APPLIED 상태 신청이 이미 있으면 ALREADY_APPLIED. 취소(CANCELED) 후 재신청은 새 신청 기록으로 허용
  • 답변 형식 위반은 INVALID_ANSWER
    • required 질문 누락 (필수가 아닌 질문은 answers에서 생략 가능)
    • 해당 행사에 속하지 않는 questionId
    • SHORT_TEXT/LONG_TEXT: answerText 필수, selectedOptions 비어야 함
    • answerText 길이 초과 (SHORT_TEXT 50자, LONG_TEXT 500자)
    • SINGLE_CHOICE: selectedOptions 정확히 1개, answerText는 null
    • MULTIPLE_CHOICE: selectedOptions 1개 이상, answerText는 null
    • selectedOptions 값이 해당 질문 options 배열의 유효 인덱스 범위 밖

답변 길이 제한

질문 유형별 고정값이며 QuestionType enum에 상수로 둡니다. DB 컬럼이 아니라 마이그레이션은 없습니다.

questionType maxLength
SHORT_TEXT 50
LONG_TEXT 500
SINGLE_CHOICE null
MULTIPLE_CHOICE null

같은 answerText 필드가 질문 유형에 따라 한도가 달라지므로 요청 DTO의 @Size로는 검증할 수 없습니다. required 누락·인덱스 범위 검증과 함께 Service에서 질문을 조회한 뒤 처리합니다.

에러 코드

코드 HTTP 메시지
EVENT_NOT_FOUND 404 행사를 찾을 수 없습니다.
ALREADY_CLOSED 409 행사 마감되었습니다.
CAPACITY_FULL 409 모집 정원이 마감되었습니다.
ALREADY_APPLIED 409 이미 신청한 행사입니다.
INVALID_ANSWER 400 신청서 답변 형식이 올바르지 않습니다.

📁 참고 자료 (선택)

의존: 에러 응답이 ApiResponse 포맷으로 나가려면 GlobalExceptionHandler(#18)가 필요합니다.

명세 대비 변경 사항:

  • questions[].maxLength 추가: 폼 조회 Response 표에 없는 필드입니다. 단답형 50자·장문형 500자 제한을 클라이언트가 하드코딩하지 않도록 서버가 내려줍니다. 선택형 질문은 null입니다.

스코프 제외:

  • 신청 취소 API: EventApplicationStatus.CANCELED는 이번 작업에서 재신청 판별용으로만 쓰고, 취소 API 자체는 별도 이슈로 분리합니다.
  • 행사 상세 조회 API: 담당자가 따로 있습니다. 다만 calculateRecruitStatus는 이 이슈에서 제공하니 재사용하면 됩니다.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions