[BE-62] 이력서 텍스트 추출 · 프로필 초안·확정 API (FR-95)
작업 내용 (설계 의도)
근거 TDD: 20260808-지원관리-확장-tdd.md — “recommendation 인터페이스 시그니처(ResumeTextExtractor)”, “API 계약 2단계 이력서 프로필”
변경 사항
ResumeTextExtractor(domain interface) + 확장자별 infra 구현 —PdfBoxResumeTextExtractor(PDF),DocxResumeTextExtractor(POI),MarkdownResumeTextExtractor(plain).PDDocument·XWPFDocument타입이 domain/application에 노출되지 않습니다.- **추출 실패는 예외가 아니라
null**입니다(FR-95 B-22, 시나리오 13) — 스캔 이미지 PDF(텍스트 레이어 없음)·hwp는null을 반환하고, 이력서 등록 자체는 저장되며 응답에extractionFailed: true가 담깁니다. 사용자는 항목을 수동 입력해 프로필을 완성합니다. hwp는 추출을 지원하지 않습니다 — 자바 진영에 신뢰할 만한 파서가 없습니다.supports()가 false를 반환해 항상 수동 입력 경로로 갑니다.- 초안 → 검토·수정 → 확정 3단계입니다. 확정 전 초안은 추천도 계산에 쓰이지 않습니다(FR-95). 확정 시 새 기준 버전이 생성되고, 관심 공고 재평가를 동기 실행합니다(NFR-13: 200건 5분 이내라 동기로 충분).
- 텍스트 → 항목 초안 변환은 규칙 기반입니다 — 기술 스택 사전 대조 + 경력 정규식. 외부 AI 호출은 하지 않습니다(NFR-10, B-23). AI 보조 추출은 P2 선택 기능으로 이번 범위 밖입니다.
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가 되고 새 기준 버전이 반환된다 - 확정 시 관심 공고 재평가가 실행되고
reevaluatedCount와elapsedMillis(밀리초)가 반환된다 (B) - 이미 확정된 프로필을 다시 확정하면 기준 버전이 증가하지 않는다 (멱등)
- 확정된 프로필을 수정하려 하면 409다
- 확정 프로필이 없으면
GET /current가 404다 - 존재하지 않는 문서 버전으로 초안을 만들면 404
DOCUMENT_VERSION_NOT_FOUND다 - 텍스트 추출 중 라이브러리 예외가 나도 500이 아니라
extractionFailed: true로 흡수된다 PDDocument타입이 application·domain 레이어에 노출되지 않는다 (아키텍처 검증)