ComponentsP3 본문

FileUpload

파일 선택·드래그앤드롭 표면 — dropzone/button 변형, list/picture-card 목록. 업로드 전송은 소비자 책임(엔드포인트 결합 없음), items 주입으로 제어형 업로드 상태(진행률/성공/실패+재시도) 표현. 제약 위반은 인라인 오류(role=alert) + onReject(한국어 reason + 표준 code). 붙여넣기(enablePaste)·폴더(directory) 추가 지원 (DK File/* 사양의 결합 제거판).

마지막 업데이트 2026-06-29

한눈에#

파일 선택·드래그앤드롭 표면입니다 — 업로드 전송은 결합하지 않고(엔드포인트 비결합) 표현만 담당합니다.

파일을 끌어다 놓거나 클릭해 선택
  • dropzone·button 변형
  • list·picture-card 목록
  • 전송 비결합
  • 제약 위반 인라인 오류

선택 파일은 내부 목록으로 유지되고 onFilesChange로 통지 — items 주입 시 진행률/성공/실패 제어

업로드 전송은 결합하지 않습니다 — 선택된 파일은 내부 목록으로만 유지되고 onFilesChange로 통지합니다(데모는 핸들러 없이도 목록 추가/제거가 동작). 같은 파일(이름+크기 동일)을 다시 넣으면 목록에 추가되지 않고 onReject(코드 FILE_EXISTS)로만 통지됩니다.

사용 시점#

  • 이럴 때: 사용자가 파일을 선택·드래그앤드롭으로 첨부해야 할 때 — 표현(선택·목록·제약 위반 표시)만 담당하고 업로드 전송은 소비자가 소유합니다(엔드포인트 비결합).
  • 전송은 소비자 책임: 선택 파일은 내부 목록으로 유지되고 onFilesChange로 통지됩니다 — 실제 업로드·진행률은 items 주입으로 제어합니다.
  • 대량 가져오기: 검증·부분 실패·복구가 필요한 흐름은 File Import / Upload Review 패턴처럼 선택 표면과 검증 리뷰 표면을 분리합니다.
  • 대신 단일 값 입력: 파일이 아닌 텍스트·숫자 입력은 Input을 씁니다.

플레이그라운드#

컨트롤로 props를 조작하면 미리보기와 코드가 실시간 갱신됩니다.

FileUpload를 직접 조작해 보세요

속성·테마·토큰을 바꾸고 React·Flutter 코드를 확인하는 풀스크린 빌더로 엽니다.

빌더 열기

플레이그라운드는 넓은 작업 영역이 필요해 웹·태블릿에서 편집할 수 있어요.

변형#

변형 축은 둘이고 서로 자유롭게 조합됩니다 — variant는 트리거 표현만 (dropzone 넓은 점선 영역 · tile 표시 안 + 셀 · button 버튼만), listType은 파일 표시 밀도만(list 행 · picture-card 그리드 카드) 담당합니다. 카드 언어는 하나입니다: 행도 카드와 같은 요소(컬러 글리프/썸네일 · 파일명 · 용량 · 액션)를 눕힌 배치일 뿐입니다. 어떤 변형이든 루트 전체가 드롭 타깃이고, 파일이 있으면 요약 라인(n개 파일 · 용량 / 한도)이 공통으로 붙습니다. 기본값: tile은 picture-card, 그 외는 list.

button — 클릭 선택만 (드롭 영역 없음)
파일을 끌어다 놓거나 클릭해 선택
제어형 상태(items) — 추가 시 0→100 업로드 시뮬레이션, 짝수 번째 파일은 실패→재시도

items를 주입하면 제어 모드가 됩니다 — 내부 목록 대신 items를 렌더하고, uploading은 Progress 합성+퍼센트, success는 체크 아이콘, error는 경고 아이콘+errorMessage+재시도 버튼(onRetry 제공 시)을 표시합니다. 전송·상태 전이는 전부 소비자 소유이며 컴포넌트는 표현만 담당합니다.

이미지·문서를 끌어다 놓거나 클릭해 선택
picture-card — 이미지는 Object URL 썸네일, 비이미지는 확장자 라벨 카드

picture-card의 썸네일 Object URL은 항목 제거·언마운트 시 자동으로 revokeObjectURL 정리됩니다. 카드에도 상태 오버레이(업로드 중/실패)와 제거 버튼이 합성됩니다.

tile — 트리거가 그리드 안 '+ 타일'로 통합된 표면 + 요약 라인

variant='tile'은 별도 드롭존 없이 + 셀이 곧 트리거인 컴팩트 표면입니다 —

  • 셀이 파일 표시의 첫 자리에 들어가고(그리드=+ 타일, listType='list'면 + 행), 각 카드는 정사각 미디어 + 파일명 푸터의 일관된 해부를 가집니다 (이미지=썸네일, 그 외=컬러 파일타입 글리프 — 행에서는 미니 글리프).

미리보기는 전 표면 기본 켜짐입니다(preview={false}로 끔) — 항목의 미리보기 액션은 **통합 FileViewer**를 열어 이미지·영상·PDF· 텍스트·마크다운·CSV를 한 오버레이에서 렌더하고, 파일 종류에 상관없이 한 필름스트립 으로 순회하며 안에서 바로 삭제할 수 있습니다. docx·office·hwp 등은 previewRenderers 로 opt-in 어댑터(@wds/ui-web/viewers/*)를 주입하면 인라인 렌더되고, 미주입·미지원 형식은 메타카드로 폴백합니다. onPreview를 주면 내장 뷰어 대신 커스텀 뷰어를 연결합니다. 액션 노출 규칙도 하나입니다 — 데스크톱은 항목 호버/포커스에서 심볼이 드러나고(카드=코너, 행=우측), 모바일은 항목 탭이 BottomSheet 액션 시트(미리보기/삭제/재시도)를 엽니다.

모바일(responsive, ADR-012) 에서는 dropzone 문구가 “탭하여 파일 선택”으로 바뀝니다(터치엔 드래그 개념이 없으므로). responsive='auto'(기본)는 compact 뷰포트에서 자동 전환, mobile/desktop은 강제 override입니다.

파일을 끌어다 놓거나, 클릭하거나, 붙여넣기(⌘V / Ctrl+V)
enablePaste + directory — 드롭존 포커스 중 ⌘V / Ctrl+V 붙여넣기, 폴더 선택/드롭 재귀 수집

enablePaste는 드롭존이 포커스된 상태의 붙여넣기를 받습니다 — 이름 없는 클립보드 이미지(스크린샷)는 clipboard-{n}.png로 보정됩니다. directory는 보조 버튼(webkitdirectory)과 폴더 드롭 재귀 수집을 켭니다 — 폴더 안의 accept 불일치 파일은 소음을 줄이기 위해 조용히 건너뜁니다. directorymultiple과 함께 쓰는 것을 권장합니다.

이미지만 (최대 3개 · 각 1MB)
제약 — accept/maxFiles/maxFileSize 위반은 인라인 오류로 거부

제약 위반은 role="alert" 인라인 오류로 안내되고 onReject로 통지됩니다. 거부 사유는 형식 불일치(accept) · 파일 용량 초과(maxFileSize) · 총 용량 초과(maxTotalSize) · 개수 초과(maxFiles) · 중복 파일이며, 프로그래밍 분기용 표준 코드(FILE_INVALID_TYPE · FILE_TOO_LARGE · TOTAL_TOO_LARGE · TOO_MANY_FILES · FILE_EXISTS)가 함께 실립니다. 단일 모드(기본, multiple 미지정)는 새 파일이 기존 선택을 대체합니다.

크기#

dropzone은 최소 높이 space.16에 점선 보더(radius.lg)이고, button 변형은 Button(secondary)을 그대로 씁니다. list 항목은 이름(말줄임)·크기·제거 버튼(+상태 블록)으로 구성되며 폭은 부모 100%, picture-card는 7rem 이상 정사각 카드(radius.md)의 자동 채움 그리드입니다.

상태#

  • Default — dropzone color.surface-muted 배경 + 점선 color.border-strong
  • Hovercolor.surface-hover
  • Focus — dropzone(role=button) :focus-visiblecolor.primary 2px 아웃라인
  • Dragging(드래그 오버)color.primary 실선 보더 + color.primary-subtle 배경
  • 오류role="alert" 영역에 color.error-text 메시지
  • 목록 항목color.surface 카드 + 제거 버튼(hover color.surface-hover)
  • 업로드 중(items) — Progress(primary 바) + 퍼센트
  • 업로드 완료(items)color.success-text 체크 아이콘
  • 업로드 실패(items)color.error-text 경고 아이콘 + 메시지 + 재시도(primary 텍스트 버튼)

Props#

Prop타입기본값설명
variant'dropzone' | 'button' | 'tile'dropzone트리거 표현 — dropzone(기본): 넓은 점선 영역 / button: 버튼만 / tile: 파일 표시 안 '+ 셀'. 셋 다 같은 피커·드롭 로직 (루트 전체가 드롭 타깃)
listType'list' | 'picture-card'파일 표시 밀도 — list(행) | picture-card(그리드 카드). 모든 variant와 조합 가능. 기본: tile→picture-card, 그 외→list
previewboolean항목 미리보기 — 통합 FileViewer로 전 파일을 한 오버레이에서 미리봄 (기본 켜짐 — 끄려면 false)
onPreview(item: FileUploadItem) => void미리보기 커스텀 — 제공 시 내장 FileViewer 대신 호출
previewRenderersPartial<Record<FileViewerItemType, FileRendererEntry>>미리보기 FileViewer 어댑터 passthrough — docx/pdf/hwp 등 opt-in 렌더러(@wds/ui-web/viewers/*)를 등록하면 해당 문서도 인라인 렌더된다. 미주입 타입은 FileViewer 메타카드로 폴백.
responsive'auto' | 'mobile' | 'desktop'적응형 표시 (ADR-012) — auto(기본): compact 뷰포트에서 카드 액션이 BottomSheet 시트로 전환, dropzone 문구가 탭 안내로 교체. mobile/desktop은 강제 override
multiplebooleanfalse다중 선택 — false면 새 파일이 기존 선택을 대체
acceptstring네이티브 accept 문법 — '.png', 'image/png', 'image/*' 콤마 구분
maxFileSizenumber파일당 최대 바이트 — 초과 시 '파일 용량 초과' 거부
maxTotalSizenumber목록 합계 최대 바이트 — 초과 시 '총 용량 초과' 거부
maxFilesnumber최대 파일 수 — 초과 시 '개수 초과' 거부
itemsreadonly FileUploadItem[]제어형 업로드 상태 목록 — 제공 시 내부 목록 대신 items를 렌더(상태는 소비자 소유)
onRetry(file: File) => voiderror 항목 재시도 — 제공 시에만 재시도 버튼 렌더
enablePastebooleanfalse드롭존 포커스 중 붙여넣기(Ctrl+V)로 파일 추가 — 이름 없는 이미지는 'clipboard-{n}.png'
directorybooleanfalse폴더 업로드 — 보조 버튼(webkitdirectory) + 드롭 시 하위 폴더 재귀 수집
directoryLabelstring폴더 선택폴더 선택 버튼 라벨 (기본 '폴더 선택')
onFilesChange(files: File[]) => void목록 변경 통지 — 항상 새 배열
onReject(rejections: FileRejection[]) => void제약 위반 통지 — {file, reason, code}[]
labelstring안내 라벨 (기본: dropzone '파일을 끌어다 놓거나 클릭해 선택' / button '파일 선택')
removeFileLabel(file: File) => string제거 버튼 aria-label (기본 '{파일명} 제거')
refRef<HTMLInputElement>숨김 <input type="file">로 병합 전달되는 ref — openPicker() 로직과 공존 (React 19 ref-as-prop)

FileUploadItem(items 항목):

Prop타입기본값설명
fileFile네이티브 File
statusFileUploadStatus미지정이면 상태 표시 없음(비제어 목록과 동일한 외형)
progressnumber0-100 — status가 'uploading'일 때 진행률 바에 반영
errorMessagestringstatus가 'error'일 때 표시 (기본 '업로드 실패')

FileRejection(onReject 인자):

Prop타입기본값설명
fileFile거부된 네이티브 File
reasonFileRejectionReason'파일 용량 초과' | '총 용량 초과' | '개수 초과' | '형식 불일치' | '중복 파일'
codeFileRejectionCode'FILE_TOO_LARGE' | 'TOTAL_TOO_LARGE' | 'TOO_MANY_FILES' | 'FILE_INVALID_TYPE' | 'FILE_EXISTS'

접근성#

dropzone은 role="button" + tabIndex=0으로 포커스·키보드 경로를 가지고, 네이티브 <input type="file">은 시각 숨김 + 탭 순서 제외(aria-hidden)로 영역/버튼 클릭과 연결됩니다. FormField나 소비자가 넘긴 id, aria-label, aria-labelledby, aria-describedby, aria-invalid, aria-required는 루트 래퍼가 아니라 실제 조작 표면(dropzone 또는 button)에 전달됩니다.

동작
dropzone Enter / Space파일 선택 창 열기
dropzone Ctrl+V / ⌘V클립보드 파일 추가 (enablePaste일 때)
Tabdropzone(또는 button) → 폴더 선택 → 재시도/제거 버튼 순회
제거·재시도 버튼 Enter / Space해당 동작 실행 (네이티브 button)
  • 제약 위반 안내는 role="alert"로 즉시 낭독되고, 파일 목록은 aria-label="선택된 파일" 리스트입니다
  • 제어 모드 상태 변화는 role="status" 라이브 리전으로 요약 낭독됩니다 — 진행률 틱은 요약을 바꾸지 않아 소음이 없습니다(예: ‘업로드 중 1건, 업로드 실패 1건’)
  • 업로드 진행률은 role="progressbar"(aria-valuenow, 라벨 ‘{파일명} 업로드 진행률’)로 전달됩니다
  • 재시도 버튼은 ‘{파일명} 재시도’, 제거 버튼은 ‘{파일명} 제거’ 라벨을 갖습니다. 제거 버튼은 시각 24px보다 넓은 48px(≥44px, WCAG 2.5.8) 히트 영역으로 확장됩니다

토큰#

component 토큰 없이 semantic(+Button·Progress 합성)을 직접 소비합니다(신설 기준 §4 미충족).

속성토큰
dropzonecolor.surface-muted · 점선 color.border-strong · radius.lg · 최소 높이 space.16
dropzone hover/activecolor.surface-hover / color.surface-pressed
draggingcolor.primary(실선) · color.primary-subtle
Focuscolor.focus-ring(2px 아웃라인)
오류color.error-text · font-size.caption
목록 항목color.surface · color.border · radius.md
picture-cardcolor.surface-muted · color.border · radius.md · 카드명/오버레이 color.surface 반투명
업로드 완료color.success-text
업로드 실패color.error-text · 재시도 color.primary
제거 버튼color.text-muted — hover color.surface-hover + color.text → active color.surface-pressed
모션duration.fast + ease.standard