"use client"; import React, { useState, useCallback, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Upload, Download, Trash2, Eye, FileText, Image, FileVideo, FileAudio, File, X, Plus, Save, AlertCircle } from "lucide-react"; import { ComponentData, FileComponent } from "@/types/screen"; import { FileInfo } from "@/lib/registry/components/file-upload/types"; import { FileViewerModal } from "@/lib/registry/components/file-upload/FileViewerModal"; import { uploadFiles, downloadFile, deleteFile } from "@/lib/api/file"; import { formatFileSize } from "@/lib/utils"; import { toast } from "sonner"; interface FileAttachmentDetailModalProps { isOpen: boolean; onClose: () => void; component: FileComponent | null; onUpdateComponent?: (updates: Partial) => void; screenId?: string; tableName?: string; recordId?: string; } /** * 파일 타입별 아이콘 반환 */ const getFileIcon = (fileExt: string) => { const ext = fileExt.toLowerCase(); if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) { return ; } if (['mp4', 'avi', 'mov', 'wmv', 'flv'].includes(ext)) { return ; } if (['mp3', 'wav', 'aac', 'flac'].includes(ext)) { return ; } if (['pdf', 'doc', 'docx', 'txt', 'rtf'].includes(ext)) { return ; } return ; }; /** * 파일첨부 상세 모달 * 화면관리에서 템플릿의 파일첨부 버튼을 클릭했을 때 열리는 상세 관리 모달 */ export const FileAttachmentDetailModal: React.FC = ({ isOpen, onClose, component, onUpdateComponent, screenId, tableName, recordId, }) => { // State 관리 const [uploadedFiles, setUploadedFiles] = useState([]); const [dragOver, setDragOver] = useState(false); const [uploading, setUploading] = useState(false); const [viewerFile, setViewerFile] = useState(null); const [isViewerOpen, setIsViewerOpen] = useState(false); // 파일 설정 상태 const [fileConfig, setFileConfig] = useState({ docType: "DOCUMENT", docTypeName: "일반 문서", accept: "*/*", maxSize: 10 * 1024 * 1024, // 10MB multiple: true, }); // 컴포넌트가 변경될 때 파일 목록 초기화 useEffect(() => { if (component?.uploadedFiles) { setUploadedFiles(component.uploadedFiles); } else { setUploadedFiles([]); } if (component?.fileConfig) { setFileConfig({ docType: component.fileConfig.docType || "DOCUMENT", docTypeName: component.fileConfig.docTypeName || "일반 문서", accept: component.fileConfig.accept?.join(",") || "*/*", maxSize: (component.fileConfig.maxSize || 10) * 1024 * 1024, multiple: component.fileConfig.multiple !== false, }); } }, [component]); // 파일 업로드 처리 const handleFileUpload = useCallback(async (files: FileList | File[]) => { if (!files || files.length === 0) return; const fileArray = Array.from(files); // 파일 검증 const validFiles: File[] = []; for (const file of fileArray) { // 크기 체크 if (file.size > fileConfig.maxSize) { toast.error(`${file.name}: 파일 크기가 너무 큽니다. (최대 ${formatFileSize(fileConfig.maxSize)})`); continue; } // 타입 체크 if (fileConfig.accept && fileConfig.accept !== "*/*") { const acceptedTypes = fileConfig.accept.split(",").map(type => type.trim()); const isValid = acceptedTypes.some(type => { if (type.startsWith(".")) { return file.name.toLowerCase().endsWith(type.toLowerCase()); } else { return file.type.includes(type); } }); if (!isValid) { toast.error(`${file.name}: 지원하지 않는 파일 형식입니다.`); continue; } } validFiles.push(file); if (!fileConfig.multiple) break; } if (validFiles.length === 0) return; try { setUploading(true); toast.loading(`${validFiles.length}개 파일 업로드 중...`); // API를 통한 파일 업로드 const response = await uploadFiles({ files: validFiles, tableName: tableName || 'screen_files', fieldName: component?.columnName || component?.id || 'file_attachment', recordId: recordId || screenId, }); if (response.success && response.data) { const newFiles: FileInfo[] = response.data.map((file: any) => ({ objid: file.objid || `temp_${Date.now()}_${Math.random()}`, savedFileName: file.saved_file_name || file.savedFileName, realFileName: file.real_file_name || file.realFileName, fileSize: file.file_size || file.fileSize, fileExt: file.file_ext || file.fileExt, filePath: file.file_path || file.filePath, docType: fileConfig.docType, docTypeName: fileConfig.docTypeName, targetObjid: file.target_objid || file.targetObjid || recordId || screenId || '', parentTargetObjid: file.parent_target_objid || file.parentTargetObjid, companyCode: file.company_code || file.companyCode || 'DEFAULT', writer: file.writer || 'user', regdate: file.regdate || new Date().toISOString(), status: file.status || 'ACTIVE', // 호환성 속성들 path: file.file_path || file.filePath, name: file.real_file_name || file.realFileName, id: file.objid, size: file.file_size || file.fileSize, type: fileConfig.docType, uploadedAt: file.regdate || new Date().toISOString(), })); const updatedFiles = fileConfig.multiple ? [...uploadedFiles, ...newFiles] : newFiles; setUploadedFiles(updatedFiles); // 컴포넌트 업데이트 if (onUpdateComponent) { onUpdateComponent({ uploadedFiles: updatedFiles, fileConfig: { ...component?.fileConfig, docType: fileConfig.docType, docTypeName: fileConfig.docTypeName, } }); } toast.dismiss(); toast.success(`${validFiles.length}개 파일이 성공적으로 업로드되었습니다.`); } else { throw new Error(response.message || '파일 업로드에 실패했습니다.'); } } catch (error) { // console.error('파일 업로드 오류:', error); toast.dismiss(); toast.error('파일 업로드에 실패했습니다.'); } finally { setUploading(false); } }, [fileConfig, uploadedFiles, onUpdateComponent, component, tableName, recordId, screenId]); // 파일 다운로드 처리 const handleFileDownload = useCallback(async (file: FileInfo) => { try { toast.loading(`${file.realFileName} 다운로드 중...`); // 로컬 파일인 경우 if (file._file) { const url = URL.createObjectURL(file._file); const link = document.createElement("a"); link.href = url; link.download = file.realFileName || file._file.name; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); toast.dismiss(); toast.success(`${file.realFileName} 다운로드가 완료되었습니다.`); return; } // 서버 파일인 경우 await downloadFile({ fileId: file.objid, serverFilename: file.savedFileName, originalName: file.realFileName, }); toast.dismiss(); toast.success(`${file.realFileName} 다운로드가 완료되었습니다.`); } catch (error) { // console.error('파일 다운로드 오류:', error); toast.dismiss(); toast.error('파일 다운로드에 실패했습니다.'); } }, []); // 파일 삭제 처리 const handleFileDelete = useCallback(async (file: FileInfo) => { if (!confirm(`${file.realFileName}을(를) 삭제하시겠습니까?`)) return; try { toast.loading(`${file.realFileName} 삭제 중...`); // 서버 파일인 경우 API 호출 if (!file._file) { await deleteFile(file.objid, file.savedFileName); } // 상태에서 파일 제거 const updatedFiles = uploadedFiles.filter(f => f.objid !== file.objid); setUploadedFiles(updatedFiles); // 컴포넌트 업데이트 if (onUpdateComponent) { onUpdateComponent({ uploadedFiles: updatedFiles, }); } toast.dismiss(); toast.success(`${file.realFileName}이 삭제되었습니다.`); } catch (error) { // console.error('파일 삭제 오류:', error); toast.dismiss(); toast.error('파일 삭제에 실패했습니다.'); } }, [uploadedFiles, onUpdateComponent]); // 파일뷰어 열기 const handleFileView = useCallback((file: FileInfo) => { setViewerFile(file); setIsViewerOpen(true); }, []); // 파일뷰어 닫기 const handleViewerClose = useCallback(() => { setIsViewerOpen(false); setViewerFile(null); }, []); // 드래그 앤 드롭 처리 const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragOver(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragOver(false); }, []); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragOver(false); const files = e.dataTransfer.files; if (files && files.length > 0) { handleFileUpload(files); } }, [handleFileUpload]); // 파일 선택 처리 const handleFileSelect = useCallback((e: React.ChangeEvent) => { const files = e.target.files; if (files && files.length > 0) { handleFileUpload(files); } // 같은 파일을 다시 선택할 수 있도록 value 초기화 e.target.value = ''; }, [handleFileUpload]); // 설정 저장 const handleSaveSettings = useCallback(() => { if (onUpdateComponent) { onUpdateComponent({ fileConfig: { ...component?.fileConfig, docType: fileConfig.docType, docTypeName: fileConfig.docTypeName, accept: fileConfig.accept.split(",").map(type => type.trim()), maxSize: Math.floor(fileConfig.maxSize / (1024 * 1024)), // MB로 변환 multiple: fileConfig.multiple, } }); } toast.success('설정이 저장되었습니다.'); }, [fileConfig, onUpdateComponent, component]); if (!component) return null; return ( <>
파일 첨부 관리 - {component.label || component.id}
파일 관리 설정 {/* 파일 관리 탭 */} {/* 파일 업로드 영역 */} 파일 업로드
!uploading && document.getElementById('file-input')?.click()} >
{uploading ? ( <>

업로드 중...

) : ( <>

파일을 선택하거나 드래그하세요

{fileConfig.accept && `지원 형식: ${fileConfig.accept}`} {fileConfig.maxSize && ` • 최대 ${formatFileSize(fileConfig.maxSize)}`} {fileConfig.multiple && ' • 여러 파일 선택 가능'}

)}
{/* 업로드된 파일 목록 */}
업로드된 파일 ({uploadedFiles.length}) {uploadedFiles.length > 0 && ( 총 {formatFileSize(uploadedFiles.reduce((sum, file) => sum + file.fileSize, 0))} )}
{uploadedFiles.length === 0 ? (

업로드된 파일이 없습니다.

위의 업로드 영역을 사용해 파일을 추가하세요.

) : (
{uploadedFiles.map((file) => (
{getFileIcon(file.fileExt)}

{file.realFileName}

{formatFileSize(file.fileSize)} {file.fileExt.toUpperCase()} {file.uploadedAt && ( <> {new Date(file.uploadedAt).toLocaleDateString()} )}
{/* 파일뷰어 버튼 */} {/* 다운로드 버튼 */} {/* 삭제 버튼 */}
))}
)}
{/* 설정 탭 */} 파일 첨부 설정
setFileConfig(prev => ({ ...prev, docType: e.target.value }))} placeholder="예: DOCUMENT, IMAGE, etc." />
setFileConfig(prev => ({ ...prev, docTypeName: e.target.value }))} placeholder="예: 일반 문서, 이미지 파일" />
setFileConfig(prev => ({ ...prev, accept: e.target.value }))} placeholder="예: image/*,.pdf,.doc,.docx" />

쉼표로 구분하여 입력 (예: image/*,.pdf,.doc)

setFileConfig(prev => ({ ...prev, maxSize: parseInt(e.target.value) * 1024 * 1024 }))} min="1" max="100" />
setFileConfig(prev => ({ ...prev, multiple: e.target.checked }))} className="rounded border-gray-300" />
{/* 파일뷰어 모달 */} ); };