'use client'; /** * ImageUpload - 이미지 업로드 컴포넌트 (미리보기 포함) * * 프로필 이미지, 로고, 썸네일 등에 사용 * * 사용 예시: * handleUpload(file)} * onRemove={() => setProfileImageUrl(null)} * aspectRatio="square" * maxSize={5} * /> */ import { useState, useRef, useCallback, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Camera, X, Upload, AlertCircle, Image as ImageIcon } from 'lucide-react'; import { cn } from '@/lib/utils'; export interface ImageUploadProps { /** 현재 이미지 URL (기존 이미지 또는 미리보기) */ value?: string | null; /** 이미지 선택 시 콜백 (File 객체 전달) */ onChange: (file: File) => void; /** 이미지 제거 시 콜백 */ onRemove?: () => void; /** 비활성화 */ disabled?: boolean; /** 최대 파일 크기 (MB) */ maxSize?: number; /** 허용 이미지 타입 */ accept?: string; /** 가로세로 비율 */ aspectRatio?: 'square' | 'video' | 'wide' | 'portrait'; /** 컴포넌트 크기 */ size?: 'sm' | 'md' | 'lg'; /** 추가 클래스 */ className?: string; /** 안내 텍스트 */ hint?: string; /** 에러 상태 */ error?: boolean; /** 에러 메시지 */ errorMessage?: string; /** 원형 (프로필용) */ rounded?: boolean; } const sizeClasses = { sm: 'w-24 h-24', md: 'w-32 h-32', lg: 'w-40 h-40', }; const aspectRatioClasses = { square: 'aspect-square', video: 'aspect-video', wide: 'aspect-[2/1]', portrait: 'aspect-[3/4]', }; export function ImageUpload({ value, onChange, onRemove, disabled = false, maxSize = 10, accept = 'image/png,image/jpeg,image/gif,image/webp', aspectRatio = 'square', size = 'md', className, hint, error = false, errorMessage, rounded = false, }: ImageUploadProps) { const [previewUrl, setPreviewUrl] = useState(null); const [validationError, setValidationError] = useState(null); const [isDragging, setIsDragging] = useState(false); const inputRef = useRef(null); // 외부 value 또는 내부 preview 사용 const displayUrl = value || previewUrl; const displayError = errorMessage || validationError; // value가 변경되면 previewUrl 초기화 useEffect(() => { if (value) { setPreviewUrl(null); } }, [value]); const handleClick = useCallback(() => { if (!disabled) { inputRef.current?.click(); } }, [disabled]); const validateAndProcessFile = useCallback((file: File) => { // 이미지 타입 검증 if (!file.type.startsWith('image/')) { setValidationError('이미지 파일만 업로드할 수 있습니다'); return false; } // 파일 크기 검증 const fileSizeMB = file.size / (1024 * 1024); if (fileSizeMB > maxSize) { setValidationError(`파일 크기는 ${maxSize}MB 이하여야 합니다`); return false; } setValidationError(null); // 미리보기 URL 생성 const objectUrl = URL.createObjectURL(file); setPreviewUrl(objectUrl); onChange(file); return true; }, [maxSize, onChange]); const handleChange = useCallback((e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { validateAndProcessFile(file); } // input 초기화 if (inputRef.current) { inputRef.current.value = ''; } }, [validateAndProcessFile]); const handleRemove = useCallback((e: React.MouseEvent) => { e.stopPropagation(); if (previewUrl) { URL.revokeObjectURL(previewUrl); } setPreviewUrl(null); setValidationError(null); if (inputRef.current) { inputRef.current.value = ''; } onRemove?.(); }, [previewUrl, onRemove]); // 드래그 앤 드롭 핸들러 const handleDragEnter = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (!disabled) { setIsDragging(true); } }, [disabled]); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); }, []); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); }, []); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); if (disabled) return; const file = e.dataTransfer.files?.[0]; if (file) { validateAndProcessFile(file); } }, [disabled, validateAndProcessFile]); // 컴포넌트 언마운트 시 미리보기 URL 정리 useEffect(() => { return () => { if (previewUrl) { URL.revokeObjectURL(previewUrl); } }; }, [previewUrl]); return (
{/* 업로드 영역 */}
{/* 숨겨진 파일 input */} {displayUrl ? ( // 이미지 미리보기 <> 미리보기 {/* 호버 오버레이 */} {!disabled && (
)} ) : ( // 업로드 안내
{isDragging ? ( ) : ( )} {isDragging ? '놓으세요' : '클릭 또는 드래그'}
)}
{/* 에러 메시지 */} {displayError && (
{displayError}
)} {/* 안내 텍스트 */} {!displayError && hint && (

{hint}

)}
); } export default ImageUpload;