[FE-33] 서류 업로드 시트

작업 내용 (설계 의도)

근거: 지원 관리 확장 FE 웹 설계 S-22 서류 업로드 시트·컴포넌트 트리·API 연동 > 2단계·Testing Plan > 반드시 커버할 실패·엣지 경로(20·21·22·23), 지원 관리 확장 TDD 2단계 — 지원 서류 API 계약(POST /api/documents multipart).

변경 사항

  • 서류 업로드 시트(S-22)를 components/document/에 공용 시트로 구현합니다. S-20 보관함과 S-21 계열 상세 두 곳에서 열리므로 공용화 기준을 충족합니다. 시트는 자기 mutation을 소유하는 예외 컴포넌트입니다(그 외 하위 컴포넌트는 props만 받음).
  • 흐름은 파일 선택 → 즉시 검증 → 배치 결정 → 단일 CTA 한 방향입니다. 실패는 서버 왕복 전에 그 자리에서 알려 주고, 검증을 통과한 경로만 CTA를 활성화합니다.
  • 클라이언트 선검증을 utils/documentFile.ts(FE-30 소유)로 수행합니다 — 확장자 4종(pdf·docx·md·hwp)과 20MB 상한을 파일 선택 즉시 판정해 인라인 danger 문구를 띄우고 CTA를 비활성합니다. 컴포넌트 안에 검증 로직을 두지 않습니다(no-logic-in-component). 20MB 초과 파일로 서버 요청이 나가는 것 자체가 낭비이자 대기 시간입니다.
  • 서버 에러 3종은 각각 다른 문구로 인라인 처리합니다 — DOCUMENT_EXTENSION_NOT_ALLOWED는 “pdf·docx·md·hwp만 올릴 수 있어요”, DOCUMENT_SIZE_EXCEEDED는 “20MB까지 올릴 수 있어요”, DOCUMENT_PATH_ESCAPE는 “파일 이름에 쓸 수 없는 문자가 있어요 / 이름을 바꿔 다시 올려 주세요”입니다. 세 번째는 사용자가 할 일(파일명 변경)이 다르므로 앞의 둘과 합치면 안 됩니다. 404 DOCUMENT_SERIES_NOT_FOUND는 인라인이 아니라 토스트 + 계열 목록 무효화입니다 — 시트 안에서 고칠 수 없는 문제이기 때문입니다.
  • 같은 유형의 기존 계열이 0개면 “새 계열 만들기”만 노출하고 라디오를 렌더하지 않습니다. 선택지가 1개면 선택지가 아니므로 묻지 않습니다. 1개 이상이면 “새 계열 만들기”와 “기존 계열에 v{N+1} 추가”를 라디오로 제시하고, 신규 계열 선택 시에만 제목 입력(최대 100자)을 노출합니다.
  • 업로드 요청은 FE-21이 소유한 apiUpload(FormData 전용)를 사용합니다. Content-Type을 직접 설정하지 않아야 브라우저가 multipart boundary를 붙입니다 — 이 티켓은 api/client.ts를 수정하지 않습니다.
  • 진행률 표시를 만들지 않습니다(설계 방안 9). 20MB 상한 로컬 업로드에서 진행률 UI의 비용이 이득보다 큽니다. 대신 CTA를 “올리는 중…” + disabled로 바꾸고 시트 닫기를 차단해 중복 제출을 막습니다.
  • 성공 시 시트를 닫고 토스트 “올렸어요”를 띄우며 ['documents','series'] 접두사를 무효화합니다. RESUME 유형 업로드 성공에는 토스트에 [프로필 만들기] 액션을 붙여 S-23으로 유도합니다 — 이력서는 올린 것으로 끝이 아니라 확정해야 추천도에 쓰이기 때문입니다.
  • 파일 선택은 네이티브 <input type="file" accept=".pdf,.docx,.md,.hwp">이고 드롭존 div를 <label>로 감싸 키보드로 접근 가능하게 합니다. 드래그 앤 드롭은 구현하지 않습니다(모바일 폭 기준 화면).
  • 이 티켓은 시트 컴포넌트와 mutation만 소유합니다. S-20·S-21의 CTA에 시트를 연결하는 배선은 wave 3의 FE-38이 수행합니다(같은 wave 파일 충돌 회피).

의존

  • FE-30 — DocumentType·DocumentVersionResponse 타입, utils/documentFile.ts·utils/fileSize.ts, FileField 프리미티브, multipart MSW 목.
  • FE-21 — apiUpload(FormData 전용 클라이언트 함수).
  • BE-61 — POST /api/documents multipart 구현.

다이어그램

처리 흐름

sequenceDiagram
    participant User as 사용자
    participant Sheet as DocumentUploadSheet
    participant Util as documentFile 검증
    participant Api as POST documents
    User->>Sheet: 파일 선택
    Sheet->>Util: 확장자·용량 선검증
    Util-->>Sheet: 통과 또는 사유
    User->>Sheet: 올리기
    Sheet->>Api: multipart 업로드
    Api-->>Sheet: 201 또는 코드 에러

컴포넌트 의존

flowchart LR
    Sheet[DocumentUploadSheet] --> Field[FileField]
    Sheet --> Segment[문서 유형 Segment]
    Sheet --> Radio[계열 선택 radiogroup]
    Sheet --> Hook[useUploadDocument]
    Hook --> Api[uploadDocument]
    Api --> Upload[apiUpload]
    Field --> Util[utils/documentFile]
    Field --> Size[utils/fileSize]

테스트 케이스

  • 유형·파일·신규 계열 제목을 채우고 올리기를 누르면 multipart 요청이 나가고 시트가 닫히며 “올렸어요” 토스트가 보인다.
  • 기존 계열을 선택해 올리면 요청에 seriesId가 실리고 seriesTitle은 실리지 않는다.
  • 20MB를 초과하는 파일을 선택하면 서버 요청 없이 인라인 에러가 보이고 CTA가 비활성된다.
  • .txt 파일을 선택하면 서버 요청 없이 “pdf·docx·md·hwp만 올릴 수 있어요”가 인라인으로 보인다.
  • 서버가 DOCUMENT_PATH_ESCAPE를 반환하면 “파일 이름에 쓸 수 없는 문자가 있어요”가 인라인으로 보이고 시트가 닫히지 않는다.
  • 서버가 DOCUMENT_EXTENSION_NOT_ALLOWED·DOCUMENT_SIZE_EXCEEDED를 반환하면 클라이언트 선검증과 같은 문구가 인라인으로 보인다.
  • 서버가 404 DOCUMENT_SERIES_NOT_FOUND를 반환하면 토스트가 뜨고 계열 목록 쿼리가 무효화되며 인라인 에러로 표시되지 않는다.
  • 같은 유형의 기존 계열이 0개면 “새 계열 만들기”만 보이고 라디오가 렌더되지 않는다.
  • 같은 유형의 기존 계열이 1개 이상이면 라디오가 보이고, “새 계열 만들기”를 고를 때만 제목 입력이 노출된다.
  • 업로드 진행 중에는 CTA가 “올리는 중…” + disabled이고 진행률 표시가 렌더되지 않으며 시트를 닫을 수 없다.
  • RESUME 유형 업로드가 성공하면 토스트에 [프로필 만들기] 액션이 함께 보인다.
  • RESUME이 아닌 유형 업로드가 성공하면 토스트에 [프로필 만들기] 액션이 보이지 않는다.
  • 파일을 선택하지 않은 상태에서는 CTA가 비활성이다.
  • .hwp 파일 업로드가 클라이언트 검증을 통과하고 201로 성공한다(추출 실패 안내는 S-23의 몫).
  • 시트가 .dark 클래스 환경에서 렌더되고 하드코딩 색을 0건 사용한다.