import React, { useState, useRef, useEffect, DragEvent, ChangeEvent } from 'react'; import { Upload, Image as ImageIcon, X, AlertCircle, Link2, Loader2 } from 'lucide-react'; interface ImageDropzoneProps { value: string; onChange: (url: string) => void; onClear: () => void; /** * 파일을 받아 업로드하고 저장 URL 을 돌려준다(예: Azure Blob). * 주어지면 드롭/선택한 파일을 업로드해 그 URL 을 value 로 쓴다. * 없으면 과거처럼 base64 data URL 로 폴백한다(재사용 컴포넌트 호환). */ onUpload?: (file: File) => Promise; label?: string; className?: string; } const MAX_SIZE = 4 * 1024 * 1024; // 백엔드 max_image_mb=4 와 일치 export default function ImageDropzone({ value, onChange, onClear, onUpload, label = '상품 대표 이미지 업로드', className = '' }: ImageDropzoneProps) { const [isDragActive, setIsDragActive] = useState(false); const [error, setError] = useState(null); const [uploading, setUploading] = useState(false); const [urlDraft, setUrlDraft] = useState(''); const fileInputRef = useRef(null); // 현재 값이 일반 URL 이면 입력칸에 그대로 노출(보기/수정/복사 가능). // base64 data URL(파일 업로드 폴백)은 거대 문자열이라 칸엔 넣지 않는다. useEffect(() => { setUrlDraft(value && /^https?:\/\//i.test(value) ? value : ''); }, [value]); const processFile = async (file: File) => { setError(null); if (!file.type.startsWith('image/')) { setError('이미지 파일(*.png, *.jpg, *.jpeg, *.gif, *.webp)만 업로드할 수 있습니다.'); return; } if (file.size > MAX_SIZE) { setError('이미지 최댓값은 4MB입니다. 더 작고 최적화된 이미지를 권장합니다.'); return; } // 업로드 핸들러가 있으면 서버(blob)로 올리고 URL 을 받는다. if (onUpload) { setUploading(true); try { const url = await onUpload(file); onChange(url); } catch (err) { setError(err instanceof Error ? err.message : '이미지 업로드에 실패했습니다.'); } finally { setUploading(false); } return; } // 폴백: base64 data URL. const reader = new FileReader(); reader.onload = (e) => { if (e.target?.result && typeof e.target.result === 'string') { onChange(e.target.result); } else { setError('이미지 해독 과정에 이상이 발생했습니다.'); } }; reader.onerror = () => setError('파일을 수집하지 못했습니다.'); reader.readAsDataURL(file); }; const handleDragOver = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (!uploading) setIsDragActive(true); }; const handleDragLeave = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragActive(false); }; const handleDrop = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragActive(false); if (uploading) return; if (e.dataTransfer.files && e.dataTransfer.files[0]) { void processFile(e.dataTransfer.files[0]); } }; const handleFileChange = (e: ChangeEvent) => { if (e.target.files && e.target.files[0]) { void processFile(e.target.files[0]); } e.target.value = ''; // 같은 파일 재선택 허용 }; const handleButtonClick = () => { if (!uploading) fileInputRef.current?.click(); }; const applyUrl = () => { const url = urlDraft.trim(); if (!url) return; setError(null); onChange(url); // value 변경 → 위 effect 가 입력칸을 적용된 URL 로 다시 채움 }; return (
{value ? ( // 미리보기 (blob URL / 외부 URL / base64 모두 동일하게 표시)
Uploaded preview
) : ( // 드롭존
{uploading ? (

업로드 중…

) : ( <>
{isDragActive ? ( ) : ( )}

{isDragActive ? '여기에 드롭하여 업로드' : '이미지 드래그 앤 드롭 또는 파일 탐색'}

PNG, JPG, JPEG, GIF, WEBP 파일 지원 (최대 4MB)

)}
)} {/* 이미지 URL 직접 입력 */}
setUrlDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); applyUrl(); } }} placeholder="또는 이미지 URL 붙여넣기 (https://…)" className="w-full pl-6 pr-2 py-1.5 text-[11px] rounded border border-border bg-background text-foreground placeholder:text-muted-foreground/70 focus:outline-none focus:border-primary" />
{error && (
{error}
)}
); }