/** * File 필드 컴포넌트 * 파일/이미지 첨부 (드래그 앤 드롭 + 파일 선택) * * API 연동 전: File 객체를 로컬 상태로 관리, URL.createObjectURL로 미리보기 * API 연동 시: POST /v1/files/upload (multipart) */ 'use client'; import { useState, useRef, useCallback } from 'react'; import { Upload, X, FileIcon, ImageIcon } from 'lucide-react'; import { Label } from '@/components/ui/label'; import { Button } from '@/components/ui/button'; import type { DynamicFieldRendererProps, FileConfig } from '../types'; interface LocalFile { file: File; previewUrl?: string; } function formatFileSize(bytes: number): string { if (bytes < 1024) return `${bytes}B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`; return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; } function isImageFile(file: File): boolean { return file.type.startsWith('image/'); } export function FileField({ field, value, onChange, error, disabled, }: DynamicFieldRendererProps) { const fieldKey = field.field_key || `field_${field.id}`; const fileInputRef = useRef(null); const [localFiles, setLocalFiles] = useState([]); const [isDragOver, setIsDragOver] = useState(false); const config = (field.properties || {}) as FileConfig; const accept = config.accept || '*'; const maxSize = config.maxSize || 10485760; // 10MB const maxFiles = config.maxFiles || 1; const showPreview = config.preview !== false; // 파일 추가 const addFiles = useCallback((newFiles: FileList | File[]) => { const files = Array.from(newFiles); const validFiles: LocalFile[] = []; for (const file of files) { if (localFiles.length + validFiles.length >= maxFiles) break; if (file.size > maxSize) continue; const previewUrl = showPreview && isImageFile(file) ? URL.createObjectURL(file) : undefined; validFiles.push({ file, previewUrl }); } if (validFiles.length === 0) return; const updated = [...localFiles, ...validFiles]; setLocalFiles(updated); // 단일 파일이면 File 객체, 복수면 배열 이름을 저장 onChange(maxFiles === 1 ? updated[0]?.file.name || null : updated.map(f => f.file.name) ); }, [localFiles, maxFiles, maxSize, showPreview, onChange]); // 파일 제거 const removeFile = (index: number) => { const file = localFiles[index]; if (file.previewUrl) URL.revokeObjectURL(file.previewUrl); const updated = localFiles.filter((_, i) => i !== index); setLocalFiles(updated); onChange(updated.length === 0 ? null : ( maxFiles === 1 ? updated[0]?.file.name : updated.map(f => f.file.name) )); }; // 드래그 핸들러 const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); if (!disabled) setIsDragOver(true); }; const handleDragLeave = () => setIsDragOver(false); const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); if (!disabled && e.dataTransfer.files.length > 0) { addFiles(e.dataTransfer.files); } }; const canAddMore = localFiles.length < maxFiles; // value가 이미 있으면 (서버에서 받은 파일명) 기존 파일 표시 const existingFileName = typeof value === 'string' && value && localFiles.length === 0 ? value : null; return (
{/* 드래그 앤 드롭 영역 */} {canAddMore && !disabled && (
fileInputRef.current?.click()} >

파일을 드래그하거나 클릭하여 선택

최대 {formatFileSize(maxSize)} {accept !== '*' && ` (${accept})`}

)} 1} className="hidden" onChange={(e) => e.target.files && addFiles(e.target.files)} /> {/* 기존 파일 (서버에서 받은) */} {existingFileName && (
{existingFileName}
)} {/* 선택된 파일 목록 */} {localFiles.length > 0 && (
{localFiles.map((lf, index) => (
{/* 이미지 미리보기 */} {lf.previewUrl ? ( {lf.file.name} ) : ( isImageFile(lf.file) ? : )}

{lf.file.name}

{formatFileSize(lf.file.size)}

{!disabled && ( )}
))}
)} {error && (

{error}

)} {!error && field.description && (

* {field.description}

)}
); }