[BE-77] 연락 검토 화면 API · 확인형 반영 (FR-90)
작업 내용 (설계 의도)
근거 TDD: 20260808-지원관리-확장-tdd.md — “방안 10 연락 이벤트”, “상태 전이 표 연락 이벤트 검토”, “API 계약 3단계 연락 검토 화면”
변경 사항
GET /api/contact-events,GET /api/contact-events/{id},POST /api/contact-events/{id}/decisions를 신설합니다.반영을 선택한 경우에만 상태가 바뀝니다(PRD Non-Goals “수신 연락 자동 상태 변경 미지원”).IGNORE·REASSIGN도 지원합니다.- 전이 제약을 그대로 적용합니다(FR-90) —
ApplicationStatus.canTransitTo()(ApplicationStatus.kt:25)를 통과해야 하고, 종료 4종은emptySet()이라 어떤 반영도 거부됩니다(:40-43).- 종료 상태 대상은 응답에서 미리
terminal: true로 표시해 화면이 선택지를 막습니다(시나리오 15). - 응답의
allowedNextStatuses가 후보별 선택지를 확정합니다 — FE가 상수를 복제하지 않게 합니다.
- 종료 상태 대상은 응답에서 미리
- 면접 회차 규칙(FR-90) — 회차 번호는 해당 지원 건의 기존 최대 회차 + 1입니다(
uk_job_application_interviews_round유니크와 대응,baseline...sql:302). 레이블 기본값은 추출된 전형명, 없으면{N}차 면접이며 반영 전 수정 가능합니다. 종료된 지원에는 면접을 추가하지 않습니다. - 크로스 컨텍스트 조합 —
DecideContactEventUseCase가ContactEventDomainService(결정 기록)와ApplicationDomainService·InterviewDomainService(상태 전이·면접 등록)를 함께 호출합니다. UseCase가 UseCase를 부르지 않습니다. - 원본·파싱·후보·결정을 모두 저장합니다(FR-89) — 중복 수신과 오탐을 추적하기 위해서입니다. 결정은
PENDING에서만 가능하고, 재결정은 409입니다. - 이 화면은 터널을 통해 외부에서 열립니다(FR-71·90 A-3) — FR-70 인증을 통과해야만 접근됩니다. 웹훅 경로와 달리 인증 필수입니다.
- 목록 조회는
PageResponse를 씁니다.
의존
- BE-75 (파싱·후보 계산)
- BE-76 (알림 — 링크가 가리키는 화면)
다이어그램
처리 흐름
sequenceDiagram participant FE as web(SPA) participant C as ContactEventApiController participant U as DecideContactEventUseCase participant CD as ContactEventDomainService participant AD as ApplicationDomainService participant ID as InterviewDomainService FE->>C: GET /api/contact-events/{id} C-->>FE: 후보 + allowedNextStatuses + terminal + nextRound FE->>C: POST /decisions (APPLY) C->>U: execute(command) U->>CD: decide(eventId, decision) alt 이미 결정됨 CD-->>U: ContactEventAlreadyDecidedException (409) end U->>AD: 대상 지원 조회 alt 종료 상태 AD-->>U: ContactEventTargetTerminalException (409) else 전이 가능 U->>AD: transit(applicationId, targetStatus) opt 면접 지정 U->>ID: add(applicationId, 최대회차+1, label, scheduledAt) end end U-->>C: ContactEventDetailResponse
클래스 의존
flowchart LR subgraph Presentation["presentation/contact"] Api[ContactEventApiController] end subgraph Application["application/contact"] List[ListContactEventsUseCase] Get[GetContactEventUseCase] Decide[DecideContactEventUseCase] end subgraph Domain["domain"] CD[ContactEventDomainService] AD[ApplicationDomainService] ID[InterviewDomainService] Status[ApplicationStatus] end Api --> List Api --> Get Api --> Decide Decide --> CD Decide --> AD Decide --> ID AD --> Status
테스트 케이스
PENDING이벤트 목록이 페이지로 조회된다- 상세 조회 시 원문·파싱 결과·후보 목록·
autoSelectedJobApplicationId가 반환된다 - 상세 조회 시 첨부 메타데이터(파일명·MIME·크기)가 반환되고 바이너리는 포함되지 않는다 (B-1)
- 후보마다
allowedNextStatuses가canTransitTo결과와 일치한다 - 종료 상태 지원 건 후보는
terminal: true로 표시된다 APPLY로 반영하면 지원 상태가 전이되고 이벤트가APPLIED가 된다- 종료 상태 지원 건에 반영하면 409
CONTACT_EVENT_TARGET_TERMINAL이고 상태가 변하지 않는다 (시나리오 15) - 전이 규칙을 위반하는 상태로 반영하면 409
TRANSITION_NOT_ALLOWED다 IGNORE를 선택하면 이벤트가IGNORED가 되고 지원 상태는 그대로다REASSIGN으로 다른 지원 건을 지정해 반영할 수 있다- 이미 결정된 이벤트에 다시 결정하면 409
CONTACT_EVENT_ALREADY_DECIDED다 - 면접을 함께 등록하면 회차가 기존 최대 + 1이다
- 면접 레이블 기본값이 추출된 전형명이고, 없으면
{N}차 면접이다 - 종료된 지원에는 면접이 추가되지 않는다
- 반영 시 원본·파싱·후보·결정이 모두 저장된다
- 결정이 실패하면 상태 전이와 면접 등록이 모두 롤백된다 (원자성)
- 인증 없이 검토 화면 API를 호출하면 401이다 (웹훅과 달리 인증 필수)
- 존재하지 않는 이벤트 조회는 404
CONTACT_EVENT_NOT_FOUND다 APPLY인데jobApplicationId가 없으면 400이다