[BE-51] 지원 대시보드 API — 상태별 칸반·다가오는 면접·장기 미변경 (FR-79)

작업 내용 (설계 의도)

근거 TDD: 20260808-지원관리-확장-tdd.md — “방안 5 (대시보드 → 컨텍스트 없음, application 레이어 조합)”, “API 계약 1단계 대시보드”

변경 사항

  1. 저장 데이터가 없는 순수 read model입니다. 신규 컨텍스트를 만들지 않고 application/dashboard UseCase가 application(지원·면접·이력)·posting(공고 제목·마감일)·company(회사명) DomainService를 호출해 조립합니다.
  2. 라운드트립 1회 — 3요소를 GET /api/dashboard 하나로 반환합니다. 화면 진입에 3번 왕복하지 않게 합니다.
  3. N+1 금지 — 지원 목록을 먼저 얻은 뒤 jobPostingId 집합으로 posting 배치 조회 1회, companyId 집합으로 company 배치 조회 1회만 수행합니다. 항목마다 조회하는 기존 목록 경로의 문제(JobPostingResponseMapper.kt:76·:86)를 반복하지 않습니다.
  4. 칸반 컬럼 안정성 — 진행 중 4종(APPLIED·DOCUMENT_SCREENING·INTERVIEWING·OFFERED)은 0건이어도 항상 포함하고, 종료 4종은 count > 0일 때만 포함합니다. 컬럼이 사라졌다 생겼다 하면 화면이 흔들립니다.
  5. 장기 미변경 판정job_application_status_histories의 마지막 transited_at 기준입니다(FR-79 ③, 기본 14일). 진행 중 상태만 대상입니다. 인덱스 (job_application_id, transited_at)가 이미 존재합니다(baseline...sql:287).
  6. 다가오는 면접scheduled_at >= now && 결과 미기록 && now + upcomingInterviewDays 이내, 오름차순입니다.
  7. 신규 파일만 추가합니다 — DashboardApiController.kt(신규), GetDashboardUseCase.kt, 응답 DTO. 기존 ApplicationQueryDomainService에 조회 메서드가 부족하면 신규 파일 DashboardQueryDomainService.kt를 추가해 기존 파일 수정을 피합니다(Single Writer).

의존

  • BE-45 (PageResponse는 쓰지 않으나 에러 코드 공통)

다이어그램

처리 흐름

sequenceDiagram
    participant FE as web(SPA)
    participant C as DashboardApiController
    participant U as GetDashboardUseCase
    participant A as DashboardQueryDomainService
    participant P as PostingDomainService
    participant Co as CompanyDomainService
    FE->>C: GET /api/dashboard
    C->>U: execute(command)
    U->>A: findAllForBoard()
    U->>A: findUpcomingInterviews(days)
    U->>A: findStaleApplications(days)
    U->>P: findAllBy(jobPostingIds) 배치 1회
    U->>Co: findAllBy(companyIds) 배치 1회
    U->>U: 3요소 조립 (칸반 컬럼 고정)
    U-->>C: DashboardResponse
    C-->>FE: 200

클래스 의존

flowchart LR
    subgraph Presentation["presentation/dashboard"]
        Api[DashboardApiController]
    end
    subgraph Application["application/dashboard"]
        UC[GetDashboardUseCase]
        Mapper[DashboardResponseMapper]
    end
    subgraph Domain["domain"]
        AQ[DashboardQueryDomainService]
        PD[PostingDomainService]
        CD[CompanyDomainService]
    end
    Api --> UC
    UC --> Mapper
    UC --> AQ
    UC --> PD
    UC --> CD

테스트 케이스

  • 지원 5건이 상태별로 칸반에 분배되어 반환된다
  • 진행 중 상태에 지원이 0건이어도 해당 컬럼이 count=0, items=[]로 존재한다
  • 종료 상태에 지원이 0건이면 해당 컬럼이 응답에 포함되지 않는다
  • 카드에 회사명·공고 제목·allowedNextStatuses가 채워진다
  • 지원 20건이어도 posting·company 조회가 각 1회만 발생한다 (N+1 회귀 방지)
  • 3일 뒤 면접이 다가오는 면접 목록에 daysUntil=3으로 포함된다
  • 이미 결과가 기록된 면접은 다가오는 면접에서 제외된다
  • 과거 일정 면접은 제외된다
  • upcomingInterviewDays=14일 때 15일 뒤 면접은 제외된다 (경계값)
  • 마지막 상태 전이가 15일 전인 진행 중 지원이 장기 미변경에 포함된다
  • 마지막 전이가 13일 전이면 장기 미변경에 포함되지 않는다 (경계값)
  • 종료 상태 지원은 장기 미변경 대상이 아니다
  • 지원이 0건이면 3요소가 모두 빈 배열로 반환된다 (0건 경계)
  • staleThresholdDays=0이면 400 BAD_REQUEST