From bd85a3c3aa780a19d54cd2bf24112f67bab3aeb9 Mon Sep 17 00:00:00 2001 From: ljpark Date: Mon, 6 Apr 2026 16:12:10 +0900 Subject: [PATCH] =?UTF-8?q?wr:=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20=EC=9E=98?= =?UTF-8?q?=EB=A6=AC=EB=8A=94=20=ED=98=84=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/file-upload/index.tsx | 21 ++++++++++------ .../src/ui/file-upload/lib/file_upload.tsx | 24 +++++++++++++------ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/packages/common/business_components/src/ui/file-upload/index.tsx b/packages/common/business_components/src/ui/file-upload/index.tsx index af63af7e7..6836691e2 100644 --- a/packages/common/business_components/src/ui/file-upload/index.tsx +++ b/packages/common/business_components/src/ui/file-upload/index.tsx @@ -1,6 +1,7 @@ "use client"; import { CloudUpload, Paperclip } from "lucide-react"; import { FileInput, FileUploader, FileUploaderContent, FileUploaderItem } from "./lib/file_upload"; + const dropZoneConfig = { maxFiles: 5, maxSize: 1024 * 1024 * 100, @@ -9,14 +10,19 @@ const dropZoneConfig = { interface FileUploadProps { - files: File[]; // 파일 배열 - setFiles: (files: File[] | null) => void; // 파일 배열 세터 - fileTypes?: string; // 옵션 (파일 확장자 문자열) - onFileClick?:(index:number)=> void; - maxFiles?: number; // 파일 클릭 핸들러 - + files: File[]; // 화면에 표시할 현재 파일 목록 + setFiles: (files: File[] | null) => void; // 추가/삭제 후 변경된 파일 목록을 부모에 전달 + fileTypes?: string; // 허용할 확장자 문자열(ex: ".pdf,.jpg") + onFileClick?:(index:number)=> void; // 파일명 영역 클릭 시 현재 목록 기준 index 전달 + maxFiles?: number; // 업로드 가능한 최대 파일 수 + } +/** + * 공통 파일 업로드 UI + * - 실제 파일 상태는 부모가 관리하고, 이 컴포넌트는 목록 표시/추가/삭제 이벤트만 위임한다. + * - 삭제 아이콘 클릭 시 FileUploader 내부에서 index 기준으로 파일을 제외한 배열을 만든 뒤 setFiles로 전달한다. + */ export const FileUpload = ({ files, setFiles, fileTypes ,onFileClick, maxFiles}: FileUploadProps) => { if(maxFiles){ @@ -51,6 +57,7 @@ export const FileUpload = {files&& files.length > 0 && files.map((file:File, i:number) => ( + // i는 현재 files 배열 순번이며, 삭제/클릭 이벤트에서 같은 순번값을 사용한다. {file.name} @@ -59,4 +66,4 @@ export const FileUpload = ) -} \ No newline at end of file +} diff --git a/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx b/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx index ede52a538..9e47d296e 100644 --- a/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx +++ b/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx @@ -37,7 +37,7 @@ type FileUploaderContextType = { const FileUploaderContext = createContext(null); -//현재 value 배열에서 해당 index만 뺀 newFiles 생성 +// FileUploader 하위 컴포넌트들이 공통 상태와 핸들러를 사용할 수 있도록 context를 노출한다. export const useFileUpload = () => { const context = useContext(FileUploaderContext); if (!context) { @@ -55,7 +55,10 @@ type FileUploaderProps = { }; /** - * File upload Docs: {@link: https://localhost:3000/docs/file-upload} + * 파일 업로드의 공통 동작을 담당하는 컨테이너 컴포넌트 + * - 현재 파일 목록(value)을 기준으로 drag & drop / 선택 / 삭제를 처리한다. + * - 실제 파일 상태는 부모가 관리하며, 변경 결과는 onValueChange로만 전달한다. + * - FileInput, FileUploaderContent, FileUploaderItem은 이 context를 통해 같은 상태를 공유한다. */ export const FileUploader = forwardRef< @@ -81,7 +84,7 @@ export const FileUploader = forwardRef< const [isLOF, setIsLOF] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); - // MIME 타입 매핑 함수 추가 + // ".pdf,.jpg" 형태의 확장자 문자열을 react-dropzone accept 객체로 변환한다. const getAcceptObject = (fileTypes?: string): Record => { if (!fileTypes) { return { "image/*": [".jpg", ".jpeg", ".png", ".gif"] }; @@ -132,6 +135,7 @@ export const FileUploader = forwardRef< const reSelectAll = maxFiles === 1 ? true : reSelect; const direction: DirectionOptions = dir === "rtl" ? "rtl" : "ltr"; + // 현재 value 배열에서 index 위치 파일만 제외한 새 배열을 부모로 전달한다. const removeFileFromSet = useCallback( (i: number) => { if (!value) return; @@ -141,6 +145,7 @@ export const FileUploader = forwardRef< [value, onValueChange], ); + // 키보드로 파일 목록 탐색 및 삭제를 지원한다. const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { e.preventDefault(); @@ -197,6 +202,7 @@ export const FileUploader = forwardRef< [value, activeIndex, removeFileFromSet], ); + // 드롭/선택된 파일을 현재 목록과 합쳐서 부모 상태로 반영한다. const onDrop = useCallback( (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { const files = acceptedFiles; @@ -239,6 +245,7 @@ export const FileUploader = forwardRef< [reSelectAll, value], ); + // 최대 파일 수에 도달하면 입력 영역을 비활성화한다. useEffect(() => { if (!value) return; if (value.length === maxFiles) { @@ -248,6 +255,7 @@ export const FileUploader = forwardRef< setIsLOF(false); }, [value, maxFiles]); + // react-dropzone에 넘길 최종 옵션 구성 const opts = dropzoneOptions ? { ...dropzoneOptions, accept: getAcceptObject(acceptName) } : { accept, maxFiles, maxSize, multiple }; @@ -312,7 +320,7 @@ export const FileUploaderContent = forwardRef< {...props} ref={ref} className={cn( - "w-full flex rounded-xl gap-1", + "w-full flex rounded-xl gap-0", orientation === "horizontal" ? "flex-raw flex-wrap" : "flex-col", className, )} @@ -336,14 +344,14 @@ export const FileUploaderItem = forwardRef< ref={ref} className={cn( buttonVariants({ variant: "ghost" }), - "h-6 p-1 justify-between cursor-pointer relative w-full", + "h-9 justify-between cursor-pointer relative w-full", className, isSelected ? "bg-muted" : "", )} {...props} > -
{onFileClick?.(index) }}> {children} @@ -353,7 +361,9 @@ export const FileUploaderItem = forwardRef< className={cn( "absolute", "!w-1/5 z-50", - direction === "rtl" ? "top-1 left-1" : "top-1 right-1", + direction === "rtl" + ? "left-1 top-1/2 -translate-y-1/2" + : "right-1 top-1/2 -translate-y-1/2", )} onClick={(e) =>{