[BE-04] application 도메인 모델 — 지원 상태 전이 · 면접 회차 · 히스토리

작업 내용 (설계 의도)

근거 TDD: 20260722-타깃-공고-알림-및-지원-히스토리-tdd.md — “상태 전이 표 / Application”

변경 사항

지원 기록을 공고와 별개 개념으로 분리해 소유합니다. “미지원”은 상태값이 아니라 Application 레코드가 존재하지 않는 것입니다(FR-42) — job_applicationsunique(job_posting_id)를 걸어 공고당 지원 1건을 강제합니다.

핵심 설계 의도:

  • 상태 전이는 ApplicationStatus enum 내부 canTransitTo()로 캡슐화합니다. 서비스에 if (status == X && next == Y) 분기를 두지 않습니다. 종료 상태 4종(ACCEPTED/REJECTED/WITHDRAWN/OFFER_DECLINED)은 isTerminal()이 true이며 어떤 전이도 거부합니다(FR-43).
  • OFFERED의 허용 전이는 ACCEPTED·OFFER_DECLINED 2개뿐입니다. 처우협의 단계의 포기는 WITHDRAWN이 아니라 OFFER_DECLINED로 표현합니다(FR-44). WITHDRAWNAPPLIED·DOCUMENT_SCREENING·INTERVIEWING 3단계에서만 가능합니다(FR-45).
  • REJECTED 전이 시 탈락 시점 단계를 함께 기록합니다 — Application.rejectedAtStage = 이전 상태.
  • 면접 회차는 상태값이 아니라 별도 엔티티입니다(FR-46). Interview(roundNumber, label, scheduledAt, interviewResult, memo)이며 한 지원에 여러 회차를 가집니다. unique(application_id, round_number)로 회차 중복을 막습니다. 레이블은 사용자 지정 문자열입니다(“1차 기술면접” 등).
  • ApplicationStatusHistory가 히스토리 조회의 SSOT입니다(FR-47). 전이가 발생하면 Application.transitTo()가 이력 객체를 반환하고, 도메인 서비스가 저장합니다. 전이 없이 이력만 생기거나 그 반대가 되는 경로를 만들지 않습니다.
  • 생성 이력 규칙(확정): 지원 기록 생성 시 (previousStatus=null → nextStatus=APPLIED) 이력 1건을 함께 적재합니다. Application.create(cmd)Pair<Application, ApplicationStatusHistory>를 반환해 “지원 생성 = 이력 1건”이 도메인에서 강제되게 합니다. 이렇게 해야 히스토리가 지원 시작 시점부터 완결되고 FE 타임라인이 분기 없이 이력만으로 렌더링됩니다. previousStatusnull인 경우는 생성 이력뿐입니다. 따라서 이력 건수는 항상 1(생성) + 전이 횟수입니다.
  • allowedNextStatuses()를 도메인이 노출합니다 — ApplicationStatus.allowedNextStatuses(): Set<ApplicationStatus>. 전이 규칙의 SSOT를 BE에 유지하면서 FE가 선택지를 서버에서 받아갈 수 있게 하기 위함입니다(FE 계약 요청 #7 수용). 종료 상태는 빈 집합을 반환합니다.
  • 지원 이력은 공고가 CLOSED돼도 무기한 보존합니다(NFR-5, FR-18) — 삭제 API를 만들지 않습니다.
  • 배치와 사용자 API가 동시에 수정할 여지에 대비해 @Version 낙관적 잠금을 둡니다.

범위: 도메인 POJO + 상태 enum + Repository interface + JPA 엔티티/RepositoryImpl(Testcontainers 통합 테스트 포함). UseCase·API는 BE-14·BE-15에서 처리합니다.

테이블 명명: 도메인 클래스는 Application·ApplicationStatusHistory·Interview를 유지하되, 매핑 테이블은 job_applications / job_application_status_histories / job_application_interviews 입니다(DB 컨벤션이 applications 단독 명사를 금지 예시로 지목). 컬럼 상세는 20260722-공고알림앱-design-db.md가 SSOT입니다.

의존

  • BE-01

다이어그램

처리 흐름

sequenceDiagram
    participant C as 호출부(도메인 서비스)
    participant A as Application
    participant S as ApplicationStatus
    participant H as ApplicationStatusHistory
    participant I as Interview
    C->>A: transitTo(next, memo)
    A->>S: canTransitTo(next)
    alt 전이 불가
        S-->>A: false
        A-->>C: 도메인 예외 (이력 미생성)
    else 전이 가능
        S-->>A: true
        A->>A: status 갱신 · REJECTED면 rejectedAtStage 기록
        A->>H: of(previous, next, memo)
        A-->>C: ApplicationStatusHistory
    end
    C->>I: create(round, label, scheduledAt)

클래스 의존

flowchart LR
    subgraph Domain["domain/application"]
        App[Application]
        Status[ApplicationStatus]
        History[ApplicationStatusHistory]
        Interview[Interview]
        Result[InterviewResult]
        AppRepo[ApplicationRepository]
        IntRepo[InterviewRepository]
    end
    subgraph Infra["infrastructure/application"]
        AppImpl[ApplicationRepositoryImpl]
        IntImpl[InterviewRepositoryImpl]
    end
    App --> Status
    App --> History
    Interview --> Result
    AppImpl -.->|implements| AppRepo
    IntImpl -.->|implements| IntRepo

테스트 케이스

  • 지원 기록을 생성하면 (null → APPLIED) 생성 이력 1건이 함께 만들어진다
  • APPLIEDDOCUMENT_SCREENING 전이가 허용되고 이력이 총 2건(생성 1 + 전이 1)이 된다
  • APPLIEDINTERVIEWING 전이는 단계 건너뛰기로 거부된다
  • APPLIEDDOCUMENT_SCREENINGINTERVIEWINGOFFEREDACCEPTED 전체 경로가 성공하고 이력이 총 5건(생성 1 + 전이 4) 순서대로 쌓인다
  • previousStatus가 null인 이력은 생성 이력 1건뿐이다
  • OFFEREDWITHDRAWN 전이는 거부된다 (FR-44)
  • OFFEREDOFFER_DECLINED 전이는 허용된다
  • INTERVIEWINGWITHDRAWN 전이는 허용된다 (FR-45)
  • 종료 상태 4종 각각에서 모든 전이 시도가 거부되고 이력이 생성되지 않는다
  • allowedNextStatuses()APPLIED에서 3종, OFFERED에서 2종, 종료 상태에서 빈 집합을 반환한다
  • DOCUMENT_SCREENINGREJECTED 전이 시 rejectedAtStageDOCUMENT_SCREENING으로 기록된다
  • 같은 상태로의 전이는 거부된다
  • 한 지원에 1차·2차 면접 회차를 추가하면 2건이 조회되고, 같은 회차 번호 재등록은 유니크 제약으로 거부된다
  • 면접 결과를 PASSED로 기록하면 지원 상태는 자동 전이되지 않는다(면접 회차와 지원 상태는 독립)
  • Testcontainers: 같은 공고에 지원 기록을 두 번 생성하면 유니크 제약으로 거부된다
  • 공고가 CLOSED여도 지원 이력 조회가 정상 동작한다