[BE-14] 지원 기록 생성 · 상태 전이 · 히스토리 조회 API
작업 내용 (설계 의도)
근거 TDD: 20260722-타깃-공고-알림-및-지원-히스토리-tdd.md — “API 계약”, “상태 전이 표 / Application”
변경 사항
사용자가 공고에 지원 기록을 남기고 진행 단계를 전이하는 API입니다. 전이 규칙 판정은 BE-04의 ApplicationStatus.canTransitTo()가 담당하고, 이 티켓은 오케스트레이션과 계약을 책임집니다.
핵심 설계 의도:
- 지원 대상은 자동 수집 공고와 수동 등록 공고 모두입니다(FR-21).
jobPostingId만 받으므로 origin에 따른 분기가 없습니다 — 수동 공고가 자동 공고와 동일한 지원 관리 대상이 되게 하는 것이 이 계약의 목적입니다. - “미지원”은 상태값이 아닙니다(FR-42). 지원 기록이 없으면 조회 응답에서
application: null로 표현하고,NOT_APPLIED같은 상태값을 만들지 않습니다. - UseCase에
if + throw비즈니스 검증을 두지 않습니다. 전이 거부는ApplicationStatus가 판정하고 도메인 예외를 던지며, 컨트롤러는 이를 409로 변환합니다. UseCaseexecute()는 10줄 이내로 DomainService만 호출합니다. - 모든 전이는 이력을 남깁니다(FR-47) — 전이와 이력 생성이 분리될 수 없도록
Application.transitTo()가 이력 객체를 반환하는 구조를 그대로 사용합니다. REJECTED전이 시 응답에rejectedAtStage를 포함해 탈락 시점 단계를 사용자가 확인할 수 있게 합니다(FR-43).- 히스토리 조회는 지원 + 이력 + 면접 회차를 한 응답으로 반환합니다(FR-47, FR-46). 회사별 필터를 지원합니다(FR-50).
- 생성 이력 규칙: 지원 생성 시
(null → APPLIED)이력 1건이 함께 적재됩니다(BE-04 도메인이 강제). 따라서 이력 건수는 항상1(생성) + 전이 횟수입니다.
FE 계약 요청 수용분
- 지원 목록 응답 필드 확정(요청 #8, blocking) — 아이템에
applicationId,jobPostingId,companyName,jobPostingTitle,currentStatus,rejectedAtStage,appliedAt,lastTransitedAt을 포함합니다. 회사명·공고명이 없으면 목록 카드에서 “무엇에 지원했는지” 식별할 수 없습니다. 두 필드는job_postings·companies조인으로 채우며(전체 300행 규모라 조인 비용 무시 가능),company_id비정규화 컬럼은 두지 않습니다. allowedNextStatuses[]추가(요청 #7) —GET /api/applications/{id}와 상태 전이 응답에 포함합니다. 전이 규칙의 SSOT는 BE(ApplicationStatus.allowedNextStatuses())이며, 서버가 선택지를 내려주면 FE의 전이 맵 상수가 사라져 규칙 이중 관리와 드리프트가 원천 제거됩니다. 이미 도메인이 보유한 정보를 노출만 하는 것이라 추가 비용이 사실상 0입니다. 409 방어선은 그대로 유지합니다(동시 전이 대비).- 낙관적 잠금 충돌(동시 전이)은 409로 변환합니다.
- 삭제 API는 만들지 않습니다 — 지원 이력은 무기한 보존합니다(NFR-5).
범위: ApplicationApiController, CreateApplicationUseCase, TransitApplicationStatusUseCase, GetApplicationUseCase, ListApplicationsUseCase, ApplicationDomainService.
롤백: 상태 전이는 이력이 남으므로 잘못된 전이는 역전이가 아니라 이력 확인으로 추적합니다(종료 상태 전이는 되돌릴 수 없음을 API 문서에 명시).
의존
- BE-04
다이어그램
처리 흐름
sequenceDiagram participant U as 사용자 participant C as ApplicationApiController participant UC as TransitApplicationStatusUseCase participant D as ApplicationDomainService participant A as Application participant R as ApplicationRepository U->>C: POST /api/applications/{id}/status-transitions C->>UC: execute(command) UC->>D: transit(applicationId, next, memo) D->>R: findBy(id) D->>A: transitTo(next, memo) alt 전이 불가 (종료 상태 · 규칙 위반) A-->>D: 도메인 예외 C-->>U: 409 Conflict else 전이 가능 A-->>D: ApplicationStatusHistory D->>R: save(application + history) C-->>U: 200 {status, rejectedAtStage?, historyId} end
클래스 의존
flowchart LR subgraph Presentation["presentation/application"] Api[ApplicationApiController] end subgraph Application["application/application"] UC1[CreateApplicationUseCase] UC2[TransitApplicationStatusUseCase] UC3[GetApplicationUseCase] UC4[ListApplicationsUseCase] end subgraph Domain["domain/application"] DS[ApplicationDomainService] App[Application] Status[ApplicationStatus] History[ApplicationStatusHistory] Repo[ApplicationRepository] end Api --> UC1 Api --> UC2 Api --> UC3 Api --> UC4 UC1 --> DS UC2 --> DS UC3 --> DS UC4 --> DS DS --> App App --> Status App --> History DS --> Repo
테스트 케이스
- 공고에 지원 기록을 생성하면
APPLIED상태와(null → APPLIED)생성 이력 1건이 만들어진다 - 수동 등록 공고에도 동일하게 지원 기록을 생성할 수 있다
- 같은 공고에 지원 기록을 두 번 생성하면 409를 반환한다
- 존재하지 않는 공고 ID로 생성하면 404를 반환한다
APPLIED→DOCUMENT_SCREENING전이가 200을 반환하고 이력이 총 2건(생성 1 + 전이 1)이 된다- 전이 응답과 상세 응답에
allowedNextStatuses가 현재 상태 기준으로 포함된다 - 종료 상태 지원의
allowedNextStatuses가 빈 배열로 반환된다 - 지원 목록 아이템에
companyName과jobPostingTitle이 포함된다 - 지원 목록에
currentStatus·rejectedAtStage·lastTransitedAt이 포함된다 OFFERED→WITHDRAWN전이는 409를 반환한다- 종료 상태(
ACCEPTED)에서 전이 시도는 409를 반환하고 이력이 늘지 않는다 REJECTED전이 응답에rejectedAtStage가 포함된다- 지원 조회 응답에 이력이 시간순으로 포함된다
- 지원 기록이 없는 공고 조회 시
application이 null로 표현된다(별도 상태값 없음) - 회사별 필터로 지원 목록을 조회할 수 있다
- 낙관적 잠금 충돌 시 409를 반환한다
- 지원 삭제 엔드포인트가 존재하지 않는다