[BE-56] 단계 2 공통 계약 — 에러 코드·문서 의존성·저장 루트 설정·볼륨 마운트

작업 내용 (설계 의도)

근거 TDD: 20260808-지원관리-확장-tdd.md — “신규 에러 코드”, “Release Scenario 2단계”

변경 사항

단계 2의 유일한 병목입니다. 후행 4개 티켓이 참조하는 공통 산출물을 한 wave에 확정해 GlobalExceptionHandler.kt·build.gradle.kts·application.yml·docker-compose.yml 네 파일의 머지 충돌을 원천 차단합니다.

  1. 신규 예외·에러 코드DocumentSeriesNotFoundException·DocumentVersionNotFoundException·DocumentExtensionNotAllowedException·DocumentSizeExceededException·DocumentPathEscapeException·DocumentAlreadySubmittedException(domain/document·domain/application), ResumeProfileNotConfirmedException·InvalidRecommendationWeightException·RecommendationNotFoundException(domain/recommendation). 정의 + GlobalExceptionHandler 매핑을 함께 합니다.
  2. 빌드 의존성org.apache.pdfbox:pdfbox(PDF 텍스트 추출), org.apache.poi:poi-ooxml(DOCX). hwp는 신뢰할 만한 자바 파서가 없어 추출을 지원하지 않습니다 — 업로드·보관만 하고 프로필 초안은 extractionFailed: true로 수동 입력을 안내합니다.
  3. 설정recruitment.document.root-path(환경 변수 DOCUMENT_ROOT), spring.servlet.multipart.max-file-size: 20MB·max-request-size: 21MB. 저장 루트를 설정값으로 추상화하는 것은 FR-84 요구입니다.
  4. docker compose 볼륨 마운트 — 호스트 /Users/biuea/Desktop/dpdpdndn/private/이직/지원서류를 컨테이너 내부 경로에 마운트합니다(dev·prod 양쪽). FR-84가 “컨테이너에는 볼륨 마운트로 노출”을 명시했고, 마운트가 없으면 호스트 절대 경로가 컨테이너 안에서 도달 불가입니다. 4-1. nginx 프록시 타임아웃 상향 (C-5)web/nginx.conf:10-15/api/ location에 proxy_read_timeout 600s·proxy_send_timeout 600s·proxy_connect_timeout 5s를 추가합니다.
    • 문제: 현재 proxy_read_timeout이 없어 nginx 기본값 60초가 적용되는데, NFR-13은 관심 공고 200건 재평가에 5분을 허용합니다. 동기 API로 두면 502가 납니다. 같은 노출이 POST /api/resume-profiles/{id}/confirmations(확정 시 동기 재평가)에도 있습니다.
    • 비동기 잡 큐 미채택 — 잡 상태 테이블·폴링 엔드포인트·FE 폴링이 따라오는데, 200건 × 규칙 기반 계산(외부 호출 0회)은 초 단위라 그 비용을 정당화하지 못합니다. NFR-8(브로커·워커 미도입) 취지와도 어긋납니다.
    • web/nginx.conf는 이 티켓이 단독 소유합니다 — FE 티켓은 이 파일을 건드리지 않습니다(Single Writer). senior-fe에 전달된 사항입니다.
    • 단일 사용자 환경이라 워커 고갈 위험이 없어 경로별 location 분리를 하지 않습니다.
  5. 피처 플래그 시드 2행document.upload·recommendation.evaluation 전부 enabled=0.
  6. FeatureDisabledException + 409 FEATURE_DISABLED는 여기서 정의하지 않습니다watchlist.management(단계 1) 때문에 BE-45가 이미 정의·매핑합니다. 최초 분해에서 이 티켓에 둔 것은 순서 오류였습니다. 이 티켓은 단계 2 플래그(document.upload·recommendation.evaluation)가 그 예외를 사용하기만 합니다.

롤백: 볼륨 마운트 제거 + 의존성 되돌리기. 플래그 2행 DELETE.

의존

  • DB-03 (단계 2 스키마 마이그레이션 — senior-dba 몫)

다이어그램

처리 흐름

sequenceDiagram
    participant C as DocumentApiController
    participant D as DocumentDomainService
    participant H as GlobalExceptionHandler
    participant FE as web(SPA)
    C->>D: upload(input)
    D-->>C: DocumentSizeExceededException
    C->>H: 예외 전파
    H->>H: 예외 → (400, DOCUMENT_SIZE_EXCEEDED)
    H-->>FE: 400 { code, message }

클래스 의존

flowchart LR
    subgraph Config["config"]
        Handler[GlobalExceptionHandler]
    end
    subgraph Domain["domain 신규 예외"]
        Ext[DocumentExtensionNotAllowedException]
        Size[DocumentSizeExceededException]
        Escape[DocumentPathEscapeException]
        Profile[ResumeProfileNotConfirmedException]
        Weight[InvalidRecommendationWeightException]
        Disabled[FeatureDisabledException]
    end
    Handler --> Ext
    Handler --> Size
    Handler --> Escape
    Handler --> Profile
    Handler --> Weight
    Handler --> Disabled

테스트 케이스

  • DocumentExtensionNotAllowedException이 400 DOCUMENT_EXTENSION_NOT_ALLOWED로 매핑된다
  • DocumentSizeExceededException이 400 DOCUMENT_SIZE_EXCEEDED로 매핑된다
  • DocumentPathEscapeException이 400 DOCUMENT_PATH_ESCAPE로 매핑된다
  • DocumentAlreadySubmittedException이 409 DOCUMENT_ALREADY_SUBMITTED로 매핑된다
  • ResumeProfileNotConfirmedException이 409 RESUME_PROFILE_NOT_CONFIRMED로 매핑된다
  • InvalidRecommendationWeightException이 400 RECOMMENDATION_WEIGHT_INVALID로 매핑된다
  • RecommendationTargetLimitExceededException이 400 RECOMMENDATION_TARGET_LIMIT_EXCEEDED로 매핑되고 actualCount·limit이 채워진다
  • BE-45가 정의한 FeatureDisabledException이 단계 2 플래그 경로에서도 409로 매핑된다 (중복 정의 없음)
  • 20MB를 넘는 멀티파트 요청이 서블릿 단에서 거부되고 400으로 매핑된다
  • recruitment.document.root-path가 설정에서 바인딩된다
  • 기존 예외 매핑이 변경되지 않는다 (회귀)
  • 피처 플래그 2행이 enabled=0으로 시드된다
  • nginx /api/ location에 proxy_read_timeout 600s가 적용되어 60초를 넘는 응답이 502로 끊기지 않는다
  • nginx 변경 후 기존 API 프록시 동작(헤더 전달·SPA fallback)이 그대로다 (회귀)