[BE-61] 지원 서류 업로드·조회 API (멀티파트) (FR-81~84)

작업 내용 (설계 의도)

근거 TDD: 20260808-지원관리-확장-tdd.md — “API 계약 2단계 지원 서류”

변경 사항

  1. POST /api/documents(multipart/form-data), GET /api/documents/series, GET /api/documents/series/{seriesId}, GET /api/documents/versions/{versionId}/content를 신설합니다.
  2. MultipartFile 타입은 presentation에만 존재합니다 — 컨트롤러가 바이트 배열·파일명·크기를 꺼내 Command로 변환하고, application·domain에는 MultipartFile이 노출되지 않습니다.
  3. 다운로드는 스트리밍 대신 바이트 반환입니다 — 최대 20MB이고 사용자가 1명이라 메모리 부담이 없습니다. Content-Disposition: attachment; filename*=UTF-8''{인코딩된 원본 파일명}으로 한글 파일명을 보존합니다.
  4. seriesId가 있으면 기존 계열에 새 버전을, 없으면 seriesTitle로 신규 계열을 만듭니다. 둘 다 없으면 400.
  5. 피처 플래그 document.upload가 OFF면 409 FEATURE_DISABLED입니다.
  6. isCurrentProfileSource는 recommendation 컨텍스트 조회가 필요하므로 application 레이어가 조합합니다. 2단계 배포 순서상 BE-62 이전에는 항상 false입니다.

파일 소유: DocumentApiController.kt(신규)만 만듭니다.

의존

  • BE-57 (문서 도메인·파일 게이트웨이)

다이어그램

처리 흐름

sequenceDiagram
    participant FE as web(SPA)
    participant C as DocumentApiController
    participant U as UploadDocumentUseCase
    participant D as DocumentDomainService
    participant G as DocumentFileGateway
    FE->>C: POST /api/documents (multipart)
    C->>C: MultipartFile → ByteArray + 메타 (Command 변환)
    C->>U: execute(command)
    U->>D: upload(input)
    D->>G: store(relativePath, bytes)
    D-->>U: ApplicationDocumentVersion
    U-->>C: DocumentVersionResponse
    C-->>FE: 201
    FE->>C: GET /versions/{id}/content
    C->>U: execute(versionId)
    U->>D: readContentOf(versionId)
    D->>G: read(relativePath)
    C-->>FE: 200 octet-stream + Content-Disposition

클래스 의존

flowchart LR
    subgraph Presentation["presentation/document"]
        Api[DocumentApiController]
        Req[DocumentUploadRequest]
    end
    subgraph Application["application/document"]
        Upload[UploadDocumentUseCase]
        ListS[ListDocumentSeriesUseCase]
        GetS[GetDocumentSeriesUseCase]
        Download[DownloadDocumentUseCase]
    end
    subgraph Domain["domain/document"]
        DS[DocumentDomainService]
        Flag[FeatureFlagGateway]
    end
    Api --> Req
    Api --> Upload
    Api --> ListS
    Api --> GetS
    Api --> Download
    Upload --> DS
    Upload --> Flag
    Download --> DS

테스트 케이스

  • PDF를 업로드하면 201과 버전 정보가 반환되고 실제 파일이 저장 루트에 생성된다
  • seriesId 없이 seriesTitle만 주면 신규 계열이 생성된다
  • seriesId를 주면 기존 계열에 다음 버전이 추가된다
  • seriesId·seriesTitle 둘 다 없으면 400이다
  • 존재하지 않는 seriesId면 404 DOCUMENT_SERIES_NOT_FOUND
  • 21MB 파일 업로드는 400 DOCUMENT_SIZE_EXCEEDED이고 파일이 생성되지 않는다
  • exe 확장자는 400 DOCUMENT_EXTENSION_NOT_ALLOWED
  • 문서 제목에 ../를 넣어도 저장 루트를 벗어나지 않는다
  • 계열 목록을 documentType 필터로 조회할 수 있다
  • 계열 상세 조회 시 버전이 번호 오름차순으로 반환된다
  • 다운로드 시 한글 원본 파일명이 Content-Disposition에 보존된다
  • 파일이 물리적으로 사라진 버전을 다운로드하면 404다
  • 피처 플래그 OFF면 업로드가 409 FEATURE_DISABLED
  • 계열이 0건이면 빈 배열을 반환한다 (0건 경계)