[BE-62] 이력서 텍스트 추출 · 프로필 초안·확정 API (FR-95)

작업 내용 (설계 의도)

근거 TDD: 20260808-지원관리-확장-tdd.md — “recommendation 인터페이스 시그니처(ResumeTextExtractor)”, “API 계약 2단계 이력서 프로필”

변경 사항

  1. ResumeTextExtractor(domain interface) + 확장자별 infra 구현PdfBoxResumeTextExtractor(PDF), DocxResumeTextExtractor(POI), MarkdownResumeTextExtractor(plain). PDDocument·XWPFDocument 타입이 domain/application에 노출되지 않습니다.
  2. **추출 실패는 예외가 아니라 null**입니다(FR-95 B-22, 시나리오 13) — 스캔 이미지 PDF(텍스트 레이어 없음)·hwpnull을 반환하고, 이력서 등록 자체는 저장되며 응답에 extractionFailed: true가 담깁니다. 사용자는 항목을 수동 입력해 프로필을 완성합니다.
  3. hwp는 추출을 지원하지 않습니다 — 자바 진영에 신뢰할 만한 파서가 없습니다. supports()가 false를 반환해 항상 수동 입력 경로로 갑니다.
  4. 초안 → 검토·수정 → 확정 3단계입니다. 확정 전 초안은 추천도 계산에 쓰이지 않습니다(FR-95). 확정 시 새 기준 버전이 생성되고, 관심 공고 재평가를 동기 실행합니다(NFR-13: 200건 5분 이내라 동기로 충분).
  5. 텍스트 → 항목 초안 변환은 규칙 기반입니다 — 기술 스택 사전 대조 + 경력 정규식. 외부 AI 호출은 하지 않습니다(NFR-10, B-23). AI 보조 추출은 P2 선택 기능으로 이번 범위 밖입니다.
  6. GET /api/resume-profiles/current가 확정 프로필을 반환합니다. 없으면 404.

파일 소유: presentation/recommendation/ResumeProfileApiController.kt(신규). BE-65가 만들 RecommendationApiController.kt별개 파일로 분리해 같은 wave 충돌을 피합니다.

의존

  • BE-57 (문서 파일 읽기)
  • BE-58 (프로필 도메인·기준 버전)

다이어그램

처리 흐름

sequenceDiagram
    participant FE as web(SPA)
    participant C as ResumeProfileApiController
    participant U as CreateResumeProfileDraftUseCase
    participant D as DocumentDomainService
    participant E as ResumeTextExtractor
    participant P as ResumeProfileDomainService
    FE->>C: POST /api/resume-profiles/drafts
    C->>U: execute(documentVersionId)
    U->>D: readContentOf(versionId)
    U->>E: extract(content)
    alt 추출 실패 (스캔 PDF·hwp)
        E-->>U: null
        U->>P: draft(versionId, null) — extractionFailed=true
    else 추출 성공
        E-->>U: text
        U->>P: draft(versionId, text) — 규칙 기반 항목 초안
    end
    U-->>C: ResumeProfileResponse
    C-->>FE: 201 (실패해도 201)

클래스 의존

flowchart LR
    subgraph Presentation["presentation/recommendation"]
        Api[ResumeProfileApiController]
    end
    subgraph Application["application/recommendation"]
        Draft[CreateResumeProfileDraftUseCase]
        Update[UpdateResumeProfileUseCase]
        Confirm[ConfirmResumeProfileUseCase]
        Get[GetCurrentResumeProfileUseCase]
    end
    subgraph Domain["domain/recommendation"]
        PDS[ResumeProfileDomainService]
        Extractor[ResumeTextExtractor]
    end
    subgraph Infra["infrastructure/recommendation"]
        Pdf[PdfBoxResumeTextExtractor]
        Docx[DocxResumeTextExtractor]
        Md[MarkdownResumeTextExtractor]
    end
    Api --> Draft
    Api --> Update
    Api --> Confirm
    Api --> Get
    Draft --> PDS
    Draft --> Extractor
    Pdf -.implements.-> Extractor
    Docx -.implements.-> Extractor
    Md -.implements.-> Extractor

테스트 케이스

  • 텍스트 레이어가 있는 PDF를 업로드해 초안을 만들면 기술 항목이 추출된다
  • DOCX에서도 텍스트가 추출된다
  • Markdown 파일은 그대로 텍스트로 읽힌다
  • 스캔 이미지 PDF는 201 + extractionFailed: true + 빈 항목이고 프로필 행은 저장된다
  • hwp는 201 + extractionFailed: true
  • 초안 항목을 수정하면 저장되고 isConfirmed는 여전히 false다
  • 확정하면 isConfirmed=true가 되고 새 기준 버전이 반환된다
  • 확정 시 관심 공고 재평가가 실행되고 reevaluatedCountelapsedMillis(밀리초)가 반환된다 (B)
  • 이미 확정된 프로필을 다시 확정하면 기준 버전이 증가하지 않는다 (멱등)
  • 확정된 프로필을 수정하려 하면 409다
  • 확정 프로필이 없으면 GET /current가 404다
  • 존재하지 않는 문서 버전으로 초안을 만들면 404 DOCUMENT_VERSION_NOT_FOUND
  • 텍스트 추출 중 라이브러리 예외가 나도 500이 아니라 extractionFailed: true로 흡수된다
  • PDDocument 타입이 application·domain 레이어에 노출되지 않는다 (아키텍처 검증)