[FE-50] 운영 확장 — 서류 업로드 실패 이력
사이즈 S · 단계 2 wave 2.
작업 내용 (설계 의도)
근거: 지원 관리 확장 FE 웹 설계 S-11 확장 (2단계) — 서류 업로드 실패 이력 · Possible Solutions 방안 14(배치 판단)·방안 15(세그먼트 라벨 축약) · 상태 표기 규칙(업로드 실패 사유 3행) · API 연동 > 2단계의 S-11 행 · Single Writer per File 검증 > 단계 2 wave 2 · Testing Plan 51·51-a~51-e, 지원 관리 확장 TDD 2단계 — 서류 업로드 실패 이력 조회 (A-3).
변경 사항
- 운영 화면(S-11)에 세그먼트 1개를 추가합니다 — 별도 화면·탭을 만들지 않습니다(방안 14). 발생 규모가 연 20행(3년 60행) 이라 별도 화면은 과하고, 서류 보관함(S-20)에 두면 보안 신호가 일상 화면에 묻힙니다. 업로드 실패 자체는 업로드 시트(S-22)가 즉시 400 인라인으로 알려 주므로, 사후 조회의 주 동기는 회고가 아니라 보안 점검이고 그건 운영 관심사입니다.
- 2단계 시점 세그먼트는 4개(수집 이력 / 알림 실패 / 웹훅 수신 / 서류 실패)이며, 가용 폭 416px(
max-w-md448px +px-4) 안에 약 384px로 들어갑니다. 라벨 축약은 세그먼트가 5개가 되는 3단계 FE-44의 담당이므로(방안 15) 이 티켓은 풀 라벨을 씁니다 — 아직 넘치지 않는 시점에 미리 줄이면 3단계 결정을 2단계로 앞당기는 것이고, 그동안 운영자는 이유 없이 짧은 라벨을 봅니다. - 보안 사유 2종을 시각적으로 분리합니다.
PATH_VALIDATION_FAILED·STORAGE_ROOT_ESCAPE는 사용자 조작 실수가 아니라 경로 이탈 시도 기록이므로, 확장자·용량 실패와 같은 무게로 보이면 운영자가 훑고 지나갑니다.Chip의fill × tone으로 filled danger + 자물쇠 기호를 씁니다. 사용자 입력 2종은 outlined neutral, 시스템 2종은 filled warning입니다. 신규 색 토큰 0건 원칙을 유지합니다(기존--danger·--danger-subtle재사용). originalFileName은 새니타이즈 전 원문이 저장돼 내려옵니다. 렌더 시 3가지를 처리합니다.- XSS — React가 텍스트 노드를 자동 이스케이프하므로
{fileName}으로 렌더하면 안전합니다. 이 화면에서dangerouslySetInnerHTML을 명시적으로 금지합니다. - 양방향 텍스트 위장 — RTL override(
U+202E)는exe.pdf를fdp.exe처럼 보이게 만드는 실제 공격 벡터입니다. 방향·제어 문자를 가시 기호로 치환해 렌더합니다. 치환은utils/documentFailure.ts(FE-30 소유)를 import만 해서 쓰고 컴포넌트에 문자 처리 로직을 두지 않습니다. - 길이 — 255자까지 오므로 2줄 말줄임 +
title속성으로 전체를 노출합니다.
- XSS — React가 텍스트 노드를 자동 이스케이프하므로
- 사유 코드 6종이 아직 확정 전이므로(BE가 dba와 정합 중), 목록에 없는
reasonCode는 원문 그대로 + 중립 톤으로 폴백합니다. 코드가 추가·변경돼도 화면이 깨지지 않아야 합니다. documentType·seriesId·seriesTitle은 유형 파싱 전 실패면null입니다 — 있을 때만 병기하고 없으면 그 줄을 생략합니다.-나 “알 수 없음”으로 채우지 않습니다.- 필터는
days(기본 30, 1~365)와reasonCode2종이고, 페이지네이션은 보관함과 같은hasNext기반 “더 보기”(useInfiniteQuery)입니다. - 빈 상태는 “실패한 업로드가 없어요” — 좋은 상태이므로 중립 톤(
--text-secondary)이고 에러톤·CTA가 없습니다. 3년 60행 규모라 대부분의 조회가 이 상태이며, 여기에 danger 톤이나 행동 유도를 붙이면 정상 상태가 매번 문제로 보입니다. 사유 필터가 걸린 0건만 “이 사유로 실패한 기록이 없어요” +[전체 보기]로 구분합니다. pages/operations/OperationsPage.tsx는 단계 2에서 이 티켓만 수정합니다(FE-28은 단계 1, FE-44는 단계 3) — Single Writer per File.
의존
- BE-81 — 업로드 실패 테이블·
GET /api/operations/document-upload-failures조회 API. 통합 검증의 선행 조건입니다. - FE-30 —
DocumentUploadFailureResponse타입,['operations','document-upload-failures', { days, reasonCode }]queryKey, MSW 목(사유 6종·알 수 없는 코드·documentType: null·0건),utils/documentFailure.ts. 이 티켓은 import만 하고 수정하지 않습니다. - FE-28 — 운영 화면의 세그먼트 구조를 단계 1에서 먼저 만듭니다. 다른 단계이므로 파일 충돌은 없습니다.
다이어그램
처리 흐름
sequenceDiagram participant User as 운영자 participant Page as OperationsPage participant Hook as useDocumentUploadFailures participant Api as GET document-upload-failures participant Util as utils/documentFailure User->>Page: 운영 > 서류 실패 열기 Page->>Hook: days 30 · reasonCode 전체 Hook->>Api: page 0 조회 Api-->>Hook: PageResponse + hasNext Page->>Util: 사유 라벨 · 보안 판정 · 파일명 치환 Util-->>Page: 표시 문자열 Page-->>User: 0건이면 중립 빈 상태 · 있으면 카드 목록 User->>Hook: 더 보기 Hook->>Api: 다음 page 조회
클래스 의존
flowchart LR Ops[OperationsPage] --> Section[DocumentUploadFailureSection] Section --> Hook[useDocumentUploadFailures] Hook --> Api[api/operations/documentUploadFailures] Section --> Filter[days · reasonCode 필터] Section --> Card[실패 카드] Card --> ReasonChip[DocumentFailureReasonChip] ReasonChip --> Util[utils/documentFailure · FE-30 소유] ReasonChip --> Chip[Chip 기존 UI] Card --> FileName[파일명 · 치환 + 2줄 말줄임 + title] FileName --> Util Section --> More[더 보기 · hasNext] Section --> Empty[중립 빈 상태]
테스트 케이스
- 실패 이력이 0건이면 “실패한 업로드가 없어요”가 중립 톤으로 보이고 에러톤·CTA가 렌더되지 않는다(설계 51).
PATH_VALIDATION_FAILED·STORAGE_ROOT_ESCAPE카드가 자물쇠 기호와 danger 칩으로,EXTENSION_NOT_ALLOWED·SIZE_EXCEEDED카드와 다른 톤으로 보인다(설계 51-a).- 파일명에 RTL override(
U+202E)가 포함된 실패 건에서 방향 제어 문자가 가시 기호로 치환되어 렌더되고 확장자 위장이 발생하지 않는다(설계 51-b). - 계약에 없는
reasonCode를 받으면 원문 그대로 + 중립 톤으로 보이고 화면이 깨지거나 예외가 발생하지 않는다(설계 51-c). documentType·seriesTitle이null인 실패 건은 해당 줄을 생략하고 나머지 필드가 정상 렌더된다(설계 51-d).- 2단계 시점 운영 세그먼트 4개가 풀 라벨로 가용 폭 안에 렌더되고 잘리지 않는다(설계 51-e).
- 사유 필터를 걸어 0건이면 “이 사유로 실패한 기록이 없어요”와
[전체 보기]가 보이고, 전체 0건 문구는 보이지 않는다. hasNext=true일 때만[더 보기]가 보이고, 누르면 다음 페이지가 기존 목록 아래에 이어 붙는다.days필터를 바꾸면 요청 쿼리에 그 값이 실리고 목록이 재조회된다.- 255자 파일명이 2줄 말줄임으로 렌더되고
title속성에 전체 문자열이 담긴다. - 조회가 실패하면
ErrorState와[다시 시도]가 보이고, 로딩 중에도 세그먼트·필터가 즉시 렌더된다. - 실패 목록 화면이
dangerouslySetInnerHTML을 사용하지 않고 파일명을 텍스트 노드로만 렌더한다. - 서류 실패 세그먼트를 다크 모드로 렌더하면 시맨틱 토큰 class만 사용하고 하드코딩 색이 0건이다.