[BE-65] 추천도 조회 API · 관심 공고 일괄 재평가 (FR-96·97)

작업 내용 (설계 의도)

근거 TDD: 20260808-지원관리-확장-tdd.md — “API 계약 2단계 지원 추천도”, “Sequence Diagram 프로필 확정과 관심 공고 재평가”

변경 사항

  1. GET /api/job-postings/{jobPostingId}/recommendationPOST /api/recommendations/re-evaluations를 신설합니다.
  2. 재평가 대상은 관심 공고로 한정합니다(FR-97 B-20) — 관리 상태가 INTERESTED 또는 PLANNED인 그룹의 대표 공고입니다. “전체 공고 일괄 평가”는 PRD Non-Goals입니다.
  3. 크로스 컨텍스트 조합(application 레이어) — watchlist에서 그룹 id 집합 → posting에서 대표 공고·JD·태그 → matching에서 근무형태 판정 → RecommendationTarget으로 변환 → recommendation DomainService. 소비 컨텍스트 시그니처에 posting·matching 타입을 두지 않습니다.
  4. 대상별 REQUIRES_NEW 트랜잭션 격리 — 1건 실패가 나머지를 막지 않고, 실패는 failures[]에 담아 계속 진행합니다. 기존 EvaluateJobPostingsUseCase.kt:48-50·:109-118 패턴을 그대로 씁니다.
  5. 동기 실행입니다 — NFR-13(관심 공고 200건 5분 이내)이고 규칙 기반이라 외부 호출이 없습니다. 잡 상태 추적 테이블·비동기 워커를 만들지 않습니다.
    • 프록시 타임아웃은 BE-56이 nginx proxy_read_timeout 600s로 해소합니다(C-5).
    • 서버 측 방어: 재평가 대상이 1,000건을 넘으면 400 RECOMMENDATION_TARGET_LIMIT_EXCEEDED 로 거부하고, 응답에 actualCount·limit을 실어 FE가 “N건 → 1,000건 이하로 좁혀 주세요”를 조립하게 합니다(A). 상한 없이 열어두면 타임아웃을 아무리 늘려도 언젠가 넘습니다.
    • 응답 스키마에 elapsedMillis: number(밀리초) 를 포함합니다(B) — 3회 연속 60초를 넘기면 비동기 잡 큐를 재검토합니다(TDD Open Questions #13).
  6. 확정 프로필이 없으면 409 RESUME_PROFILE_NOT_CONFIRMED입니다 — 확정 전 초안은 계산에 쓰이지 않습니다.
  7. Operations 요구 충족 — 응답의 notEvaluableCount·failures[{jobPostingId, reason}]가 PRD Operations “재평가 실패 공고 수와 사유 집계”를 만족합니다. 별도 조회 화면을 만들지 않습니다.
  8. 피처 플래그 recommendation.evaluation이 OFF면 409 FEATURE_DISABLED입니다.

파일 소유: RecommendationApiController.kt(신규). BE-62의 ResumeProfileApiController.kt와 별개 파일입니다.

의존

  • BE-63 (추천도 계산 도메인)
  • BE-50 (관심 공고 그룹 조회 — 1단계 산출물)

다이어그램

처리 흐름

sequenceDiagram
    participant FE as web(SPA)
    participant C as RecommendationApiController
    participant U as EvaluateInterestedPostingsUseCase
    participant W as WatchStateDomainService
    participant P as PostingDomainService
    participant M as MatchResultQueryDomainService
    participant R as RecommendationDomainService
    FE->>C: POST /api/recommendations/re-evaluations
    C->>U: execute(command)
    U->>R: planEvaluation(force) — 확정 프로필·가중치·revision
    alt 확정 프로필 없음
        R-->>U: ResumeProfileNotConfirmedException (409)
    end
    U->>W: findGroupIdsBy([INTERESTED, PLANNED])
    U->>P: 그룹 → 대표 공고 + JD + 태그 배치 조회
    U->>M: findAllBy(jobPostingIds) 근무형태 배치 조회
    loop 대상마다 (REQUIRES_NEW)
        U->>R: evaluateOne(target, plan)
        alt 실패
            U->>U: failures[]에 추가하고 계속
        end
    end
    U-->>C: evaluated / skipped / notEvaluable / failures

클래스 의존

flowchart LR
    subgraph Presentation["presentation/recommendation"]
        Api[RecommendationApiController]
    end
    subgraph Application["application/recommendation"]
        Eval[EvaluateInterestedPostingsUseCase]
        Get[GetRecommendationUseCase]
        Mapper[RecommendationTargetMapper]
    end
    subgraph Domain["domain"]
        RDS[RecommendationDomainService]
        WDS[WatchStateDomainService]
        PDS[PostingDomainService]
        MDS[MatchResultQueryDomainService]
        Flag[FeatureFlagGateway]
    end
    Api --> Eval
    Api --> Get
    Eval --> Mapper
    Eval --> RDS
    Eval --> WDS
    Eval --> PDS
    Eval --> MDS
    Eval --> Flag

테스트 케이스

  • 관심 공고 3건을 재평가하면 evaluatedCount=3이다
  • 관리 상태가 EXCLUDED인 그룹의 공고는 재평가 대상이 아니다
  • 관리 상태가 없는 공고는 재평가 대상이 아니다 (전체 일괄 평가 미지원)
  • 확정 프로필이 없으면 409 RESUME_PROFILE_NOT_CONFIRMED
  • 같은 기준 버전으로 이미 평가된 공고는 force=false면 skip되고 skippedCount에 반영된다
  • force=true면 이미 평가된 공고도 다시 계산된다
  • JD 없는 공고는 notEvaluableCount에 잡히고 failures[]JD_UNAVAILABLE로 담긴다
  • 대상 1건이 예외로 실패해도 나머지가 계속 처리되고 failures[]에 1건이 담긴다
  • 관심 공고 200건 재평가가 5분 이내에 완료된다 (NFR-13)
  • 추천도 조회 시 축별 judgeable·normalizedWeight·fulfillmentRate·항목 근거가 반환된다
  • 평가 결과가 없는 공고를 조회하면 404 RECOMMENDATION_NOT_FOUND
  • 판단 불가 축의 fulfillmentRate가 null로 응답된다
  • 대상이 0건이면 즉시 200과 0 카운트를 반환한다 (0건 경계)
  • 대상이 1,001건이면 400 RECOMMENDATION_TARGET_LIMIT_EXCEEDED이고 actualCount=1001·limit=1000이 응답에 담긴다 (경계값)
  • 대상이 정확히 1,000건이면 실행된다 (경계값)
  • 응답 스키마에 elapsedMillis(밀리초)가 포함되고 실제 소요 시간이 담긴다
  • 피처 플래그 OFF면 409 FEATURE_DISABLED
  • 대상별 트랜잭션이 REQUIRES_NEW로 격리되어 1건 롤백이 다른 건에 전파되지 않는다