ERP-node/frontend/components/screen/panels/FileComponentConfigPanel.tsx

1023 lines
39 KiB
TypeScript

"use client";
import React, { useState, useEffect, useCallback, useRef } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { FileComponent, TableInfo } from "@/types/screen";
import { Plus, X, Upload, File, Image, FileText, Download, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { FileInfo, FileUploadResponse } from "@/lib/registry/components/file-upload/types";
import { uploadFiles, downloadFile, deleteFile } from "@/lib/api/file";
import { formatFileSize } from "@/lib/utils";
import { toast } from "sonner";
interface FileComponentConfigPanelProps {
component: FileComponent;
onUpdateProperty: (componentId: string, path: string, value: any) => void;
currentTable?: TableInfo;
currentTableName?: string;
}
export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> = ({
component,
onUpdateProperty,
currentTable,
currentTableName,
}) => {
// fileConfig가 없는 경우 초기화
React.useEffect(() => {
if (!component.fileConfig) {
const defaultFileConfig = {
docType: "DOCUMENT",
docTypeName: "일반 문서",
dragDropText: "파일을 드래그하거나 클릭하여 업로드하세요",
maxSize: 10,
maxFiles: 5,
multiple: true,
showPreview: true,
showProgress: true,
autoLink: false,
accept: [],
linkedTable: "",
linkedField: "",
};
onUpdateProperty(component.id, "fileConfig", defaultFileConfig);
}
}, [component.fileConfig, component.id, onUpdateProperty]);
// 로컬 상태
const [localInputs, setLocalInputs] = useState({
docType: component.fileConfig?.docType || "DOCUMENT",
docTypeName: component.fileConfig?.docTypeName || "일반 문서",
dragDropText: component.fileConfig?.dragDropText || "파일을 드래그하거나 클릭하여 업로드하세요",
maxSize: component.fileConfig?.maxSize || 10,
maxFiles: component.fileConfig?.maxFiles || 5,
newAcceptType: "",
linkedTable: component.fileConfig?.linkedTable || "",
linkedField: component.fileConfig?.linkedField || "",
});
const [localValues, setLocalValues] = useState({
multiple: component.fileConfig?.multiple ?? true,
showPreview: component.fileConfig?.showPreview ?? true,
showProgress: component.fileConfig?.showProgress ?? true,
autoLink: component.fileConfig?.autoLink ?? false,
});
const [acceptTypes, setAcceptTypes] = useState<string[]>(component.fileConfig?.accept || []);
// 전역 파일 상태 관리를 window 객체에 저장 (컴포넌트 언마운트 시에도 유지)
const getGlobalFileState = (): {[key: string]: FileInfo[]} => {
if (typeof window !== 'undefined') {
return (window as any).globalFileState || {};
}
return {};
};
const setGlobalFileState = (updater: (prev: {[key: string]: FileInfo[]}) => {[key: string]: FileInfo[]}) => {
if (typeof window !== 'undefined') {
const currentState = getGlobalFileState();
const newState = updater(currentState);
(window as any).globalFileState = newState;
console.log("🌐 전역 파일 상태 업데이트:", {
componentId: component.id,
newFileCount: newState[component.id]?.length || 0,
totalComponents: Object.keys(newState).length
});
// 강제 리렌더링을 위한 이벤트 발생
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { componentId: component.id, fileCount: newState[component.id]?.length || 0 }
}));
// 디버깅용 전역 함수 등록
(window as any).debugFileState = () => {
console.log("🔍 전역 파일 상태 디버깅:", {
globalState: (window as any).globalFileState,
localStorage: Object.keys(localStorage).filter(key => key.startsWith('fileComponent_')).map(key => ({
key,
data: JSON.parse(localStorage.getItem(key) || '[]')
}))
});
};
}
};
// 파일 업로드 관련 상태 - 초기화 시 전역 상태에서 복원
const initializeUploadedFiles = (): FileInfo[] => {
const componentFiles = component.uploadedFiles || [];
const globalFiles = getGlobalFileState()[component.id] || [];
// localStorage 백업에서 복원 (영구 저장된 파일 + 임시 파일)
const backupKey = `fileComponent_${component.id}_files`;
const tempBackupKey = `fileComponent_${component.id}_files_temp`;
const backupFiles = localStorage.getItem(backupKey);
const tempBackupFiles = localStorage.getItem(tempBackupKey);
let parsedBackupFiles: FileInfo[] = [];
let parsedTempFiles: FileInfo[] = [];
if (backupFiles) {
try {
parsedBackupFiles = JSON.parse(backupFiles);
} catch (error) {
console.error("백업 파일 파싱 실패:", error);
}
}
if (tempBackupFiles) {
try {
parsedTempFiles = JSON.parse(tempBackupFiles);
} catch (error) {
console.error("임시 파일 파싱 실패:", error);
}
}
// 우선순위: 전역 상태 > localStorage > 임시 파일 > 컴포넌트 속성
const finalFiles = globalFiles.length > 0 ? globalFiles :
parsedBackupFiles.length > 0 ? parsedBackupFiles :
parsedTempFiles.length > 0 ? parsedTempFiles :
componentFiles;
console.log("🚀 FileComponentConfigPanel 초기화:", {
componentId: component.id,
componentFiles: componentFiles.length,
globalFiles: globalFiles.length,
backupFiles: parsedBackupFiles.length,
tempFiles: parsedTempFiles.length,
finalFiles: finalFiles.length,
source: globalFiles.length > 0 ? 'global' : parsedBackupFiles.length > 0 ? 'localStorage' : parsedTempFiles.length > 0 ? 'temp' : 'component'
});
return finalFiles;
};
const [uploadedFiles, setUploadedFiles] = useState<FileInfo[]>(() => {
const initialFiles = initializeUploadedFiles();
// 초기화된 파일이 있고 컴포넌트 속성과 다르면 즉시 동기화
if (initialFiles.length > 0 && JSON.stringify(initialFiles) !== JSON.stringify(component.uploadedFiles || [])) {
setTimeout(() => {
onUpdateProperty(component.id, "uploadedFiles", initialFiles);
onUpdateProperty(component.id, "lastFileUpdate", Date.now());
console.log("🔄 초기화 시 컴포넌트 속성 동기화:", {
componentId: component.id,
fileCount: initialFiles.length
});
}, 0);
}
return initialFiles;
});
const [dragOver, setDragOver] = useState(false);
const [uploading, setUploading] = useState(false);
// 이전 컴포넌트 ID 추적용 ref
const prevComponentIdRef = useRef(component.id);
// 파일 타입별 아이콘 반환
const getFileIcon = (fileExt: string) => {
const ext = fileExt.toLowerCase();
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) {
return <Image className="w-5 h-5" />;
}
if (['pdf', 'doc', 'docx', 'txt', 'rtf'].includes(ext)) {
return <FileText className="w-5 h-5" />;
}
return <File className="w-5 h-5" />;
};
// 파일 업로드 처리
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 > localInputs.maxSize * 1024 * 1024) {
toast.error(`${file.name}: 파일 크기가 ${localInputs.maxSize}MB를 초과합니다.`);
continue;
}
// 파일 타입 검증 (acceptTypes가 설정된 경우에만)
if (acceptTypes.length > 0) {
const fileExt = '.' + file.name.split('.').pop()?.toLowerCase();
const isAllowed = acceptTypes.some(type =>
type === '*/*' ||
type === file.type ||
type === fileExt ||
(type.startsWith('.') && fileExt === type) ||
(type.includes('/*') && file.type.startsWith(type.split('/')[0]))
);
if (!isAllowed) {
toast.error(`${file.name}: 허용되지 않는 파일 형식입니다. (허용: ${acceptTypes.join(', ')})`);
console.log(`파일 검증 실패:`, {
fileName: file.name,
fileType: file.type,
fileExt,
acceptTypes,
isAllowed
});
continue;
}
}
console.log(`파일 검증 성공:`, {
fileName: file.name,
fileType: file.type,
fileSize: file.size,
acceptTypesCount: acceptTypes.length
});
validFiles.push(file);
}
if (validFiles.length === 0) return;
// 중복 파일 체크
const existingFiles = uploadedFiles;
const existingFileNames = existingFiles.map(f => f.realFileName.toLowerCase());
const duplicates: string[] = [];
const uniqueFiles: File[] = [];
console.log("🔍 중복 파일 체크:", {
uploadedFiles: existingFiles.length,
existingFileNames: existingFileNames,
newFiles: validFiles.map(f => f.name.toLowerCase())
});
validFiles.forEach(file => {
const fileName = file.name.toLowerCase();
if (existingFileNames.includes(fileName)) {
duplicates.push(file.name);
console.log("❌ 중복 파일 발견:", file.name);
} else {
uniqueFiles.push(file);
console.log("✅ 새로운 파일:", file.name);
}
});
console.log("🔍 중복 체크 결과:", {
duplicates: duplicates,
uniqueFiles: uniqueFiles.map(f => f.name)
});
if (duplicates.length > 0) {
toast.error(`중복된 파일이 있습니다: ${duplicates.join(', ')}`, {
description: "같은 이름의 파일이 이미 업로드되어 있습니다.",
duration: 4000
});
if (uniqueFiles.length === 0) {
return; // 모든 파일이 중복이면 업로드 중단
}
// 일부만 중복인 경우 고유한 파일만 업로드
toast.info(`${uniqueFiles.length}개의 새로운 파일만 업로드합니다.`);
}
const filesToUpload = uniqueFiles.length > 0 ? uniqueFiles : validFiles;
try {
console.log("🔄 파일 업로드 시작:", {
originalFiles: validFiles.length,
filesToUpload: filesToUpload.length,
uploading
});
setUploading(true);
toast.loading(`${filesToUpload.length}개 파일 업로드 중...`);
// 그리드와 연동되는 targetObjid 생성 (화면 복원 시스템과 통일)
const tableName = 'screen_files';
const screenId = (window as any).__CURRENT_SCREEN_ID__ || 'unknown'; // 현재 화면 ID
const componentId = component.id;
const fieldName = component.columnName || component.id || 'file_attachment';
const targetObjid = `${tableName}:${screenId}:${componentId}:${fieldName}`;
const response = await uploadFiles({
files: filesToUpload,
tableName: tableName,
fieldName: fieldName,
recordId: `screen_${screenId}:${componentId}`, // 템플릿 파일 형태
docType: localInputs.docType,
docTypeName: localInputs.docTypeName,
targetObjid: targetObjid, // 그리드 연동을 위한 targetObjid
columnName: fieldName,
isVirtualFileColumn: true, // 가상 파일 컬럼으로 처리
});
console.log("📤 파일 업로드 응답:", response);
if (response.success && (response.data || response.files)) {
const filesData = response.data || response.files;
console.log("📁 업로드된 파일 데이터:", filesData);
const newFiles: FileInfo[] = filesData.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: localInputs.docType,
docTypeName: localInputs.docTypeName,
targetObjid: file.target_objid || file.targetObjid || component.id,
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: localInputs.docType,
uploadedAt: file.regdate || new Date().toISOString(),
}));
const updatedFiles = localValues.multiple ? [...uploadedFiles, ...newFiles] : newFiles;
setUploadedFiles(updatedFiles);
// 자동으로 영구 저장 (저장 버튼 없이 바로 저장)
const timestamp = Date.now();
// 전역 상태에 저장
setGlobalFileState(prev => ({
...prev,
[component.id]: updatedFiles
}));
// 컴포넌트 속성에 저장
onUpdateProperty(component.id, "uploadedFiles", updatedFiles);
onUpdateProperty(component.id, "lastFileUpdate", timestamp);
// localStorage에 영구 저장
const backupKey = `fileComponent_${component.id}_files`;
localStorage.setItem(backupKey, JSON.stringify(updatedFiles));
// 전역 파일 상태 변경 이벤트 발생 (RealtimePreview 업데이트용)
if (typeof window !== 'undefined') {
const event = new CustomEvent('globalFileStateChanged', {
detail: {
componentId: component.id,
files: updatedFiles,
action: 'upload',
timestamp: Date.now()
}
});
window.dispatchEvent(event);
}
console.log("🔄 FileComponentConfigPanel 자동 저장:", {
componentId: component.id,
uploadedFiles: updatedFiles.length,
status: "자동 영구 저장됨",
onUpdatePropertyExists: typeof onUpdateProperty === 'function',
globalFileStateUpdated: getGlobalFileState()[component.id]?.length || 0,
localStorageBackup: localStorage.getItem(`fileComponent_${component.id}_files`) ? 'saved' : 'not saved'
});
// 그리드 파일 상태 새로고침 이벤트 발생
if (typeof window !== 'undefined') {
const tableName = component.tableName || currentTableName || 'unknown';
const columnName = component.columnName || component.id;
const recordId = component.id; // 임시로 컴포넌트 ID 사용
const targetObjid = component.id;
const refreshEvent = new CustomEvent('refreshFileStatus', {
detail: {
tableName: tableName,
recordId: recordId,
columnName: columnName,
targetObjid: targetObjid,
fileCount: updatedFiles.length
}
});
window.dispatchEvent(refreshEvent);
console.log("🔄 FileComponentConfigPanel 그리드 새로고침 이벤트 발생:", {
tableName,
recordId,
columnName,
targetObjid,
fileCount: updatedFiles.length
});
}
toast.dismiss();
toast.success(`${validFiles.length}개 파일이 성공적으로 업로드되었습니다.`);
console.log("✅ 파일 업로드 성공:", {
newFilesCount: newFiles.length,
totalFiles: updatedFiles.length,
componentId: component.id,
updatedFiles: updatedFiles.map(f => ({ objid: f.objid, name: f.realFileName }))
});
} else {
throw new Error(response.message || '파일 업로드에 실패했습니다.');
}
} catch (error) {
console.error('❌ 파일 업로드 오류:', error);
toast.dismiss();
toast.error('파일 업로드에 실패했습니다.');
} finally {
console.log("🏁 파일 업로드 완료, 로딩 상태 해제");
setUploading(false);
}
}, [localInputs, localValues, uploadedFiles, onUpdateProperty, currentTableName, component, acceptTypes]);
// 파일 다운로드 처리
const handleFileDownload = useCallback(async (file: FileInfo) => {
try {
await downloadFile({
fileId: file.objid || file.id || '',
serverFilename: file.savedFileName,
originalName: file.realFileName || file.name || 'download',
});
toast.success(`${file.realFileName || file.name} 다운로드가 완료되었습니다.`);
} catch (error) {
console.error('파일 다운로드 오류:', error);
toast.error('파일 다운로드에 실패했습니다.');
}
}, []);
// 파일 삭제 처리
const handleFileDelete = useCallback(async (fileId: string) => {
try {
await deleteFile(fileId, 'temp_record');
const updatedFiles = uploadedFiles.filter(file => file.objid !== fileId && file.id !== fileId);
setUploadedFiles(updatedFiles);
// 전역 상태에도 업데이트
setGlobalFileState(prev => ({
...prev,
[component.id]: updatedFiles
}));
// 컴포넌트 속성 업데이트 (RealtimePreview 강제 리렌더링용)
const timestamp = Date.now();
onUpdateProperty(component.id, "uploadedFiles", updatedFiles);
onUpdateProperty(component.id, "lastFileUpdate", timestamp);
// localStorage 백업도 업데이트 (영구 저장소와 임시 저장소 모두)
const backupKey = `fileComponent_${component.id}_files`;
const tempBackupKey = `fileComponent_${component.id}_files_temp`;
localStorage.setItem(backupKey, JSON.stringify(updatedFiles));
localStorage.setItem(tempBackupKey, JSON.stringify(updatedFiles));
console.log("🗑️ FileComponentConfigPanel 파일 삭제:", {
componentId: component.id,
deletedFileId: fileId,
remainingFiles: updatedFiles.length,
timestamp: timestamp
});
// 그리드 파일 상태 새로고침 이벤트 발생
if (typeof window !== 'undefined') {
const tableName = currentTableName || 'screen_files';
const recordId = component.id;
const columnName = component.columnName || component.id || 'file_attachment';
const targetObjid = `${tableName}:${recordId}:${columnName}`;
const refreshEvent = new CustomEvent('refreshFileStatus', {
detail: {
tableName: tableName,
recordId: recordId,
columnName: columnName,
targetObjid: targetObjid,
fileCount: updatedFiles.length
}
});
window.dispatchEvent(refreshEvent);
console.log("🔄 FileComponentConfigPanel 파일 삭제 후 그리드 새로고침:", {
tableName,
recordId,
columnName,
targetObjid,
fileCount: updatedFiles.length
});
}
toast.success('파일이 삭제되었습니다.');
} catch (error) {
console.error('파일 삭제 오류:', error);
toast.error('파일 삭제에 실패했습니다.');
}
}, [uploadedFiles, onUpdateProperty, component.id]);
// 파일 저장 처리 (임시 → 영구 저장)
const handleSaveFiles = useCallback(() => {
try {
// 컴포넌트 속성에 영구 저장
const timestamp = Date.now();
onUpdateProperty(component.id, "uploadedFiles", uploadedFiles);
onUpdateProperty(component.id, "lastFileUpdate", timestamp);
// 전역 상태에도 저장
setGlobalFileState(prev => ({
...prev,
[component.id]: uploadedFiles
}));
// localStorage에도 백업
const backupKey = `fileComponent_${component.id}_files`;
localStorage.setItem(backupKey, JSON.stringify(uploadedFiles));
// 임시 파일 삭제
const tempBackupKey = `fileComponent_${component.id}_files_temp`;
localStorage.removeItem(tempBackupKey);
console.log("💾 파일 저장 완료:", {
componentId: component.id,
fileCount: uploadedFiles.length,
timestamp: timestamp,
files: uploadedFiles.map(f => ({ objid: f.objid, name: f.realFileName }))
});
toast.success(`${uploadedFiles.length}개 파일이 영구 저장되었습니다.`);
} catch (error) {
console.error('파일 저장 오류:', error);
toast.error('파일 저장에 실패했습니다.');
}
}, [uploadedFiles, onUpdateProperty, component.id, setGlobalFileState]);
// 드래그앤드롭 이벤트 핸들러
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
setDragOver(true);
}, []);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
}, []);
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFileUpload(files);
}
}, [handleFileUpload]);
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
handleFileUpload(files);
}
e.target.value = '';
}, [handleFileUpload]);
// 컴포넌트 변경 시 로컬 상태 동기화
useEffect(() => {
setLocalInputs({
docType: component.fileConfig?.docType || "DOCUMENT",
docTypeName: component.fileConfig?.docTypeName || "일반 문서",
dragDropText: component.fileConfig?.dragDropText || "파일을 드래그하거나 클릭하여 업로드하세요",
maxSize: component.fileConfig?.maxSize || 10,
maxFiles: component.fileConfig?.maxFiles || 5,
newAcceptType: "",
linkedTable: component.fileConfig?.linkedTable || "",
linkedField: component.fileConfig?.linkedField || "",
});
setLocalValues({
multiple: component.fileConfig?.multiple ?? true,
showPreview: component.fileConfig?.showPreview ?? true,
showProgress: component.fileConfig?.showProgress ?? true,
autoLink: component.fileConfig?.autoLink ?? false,
});
setAcceptTypes(component.fileConfig?.accept || []);
// 파일 목록 동기화 - 컴포넌트 ID가 변경되었을 때만 초기화
const componentFiles = component.uploadedFiles || [];
if (prevComponentIdRef.current !== component.id) {
// 새로운 컴포넌트로 변경된 경우
console.log("🔄 FileComponentConfigPanel 새 컴포넌트 선택:", {
prevComponentId: prevComponentIdRef.current,
newComponentId: component.id,
componentFiles: componentFiles.length,
action: "새 컴포넌트 → 상태 초기화",
globalFileStateExists: !!getGlobalFileState()[component.id],
globalFileStateLength: getGlobalFileState()[component.id]?.length || 0,
localStorageExists: !!localStorage.getItem(`fileComponent_${component.id}_files`),
onUpdatePropertyExists: typeof onUpdateProperty === 'function'
});
// 1순위: 전역 상태에서 파일 복원
const globalFileState = getGlobalFileState();
const globalFiles = globalFileState[component.id];
if (globalFiles && globalFiles.length > 0) {
console.log("🌐 전역 상태에서 파일 복원:", {
componentId: component.id,
globalFiles: globalFiles.length,
action: "전역 상태 → 상태 복원"
});
setUploadedFiles(globalFiles);
onUpdateProperty(component.id, "uploadedFiles", globalFiles);
}
// 2순위: localStorage에서 백업 파일 복원
else {
const backupKey = `fileComponent_${component.id}_files`;
const backupFiles = localStorage.getItem(backupKey);
if (backupFiles && componentFiles.length === 0) {
try {
const parsedBackupFiles = JSON.parse(backupFiles);
console.log("📂 localStorage에서 파일 복원:", {
componentId: component.id,
backupFiles: parsedBackupFiles.length,
action: "백업 → 상태 복원"
});
setUploadedFiles(parsedBackupFiles);
// 전역 상태에도 저장
setGlobalFileState(prev => ({
...prev,
[component.id]: parsedBackupFiles
}));
// 컴포넌트 속성에도 복원
onUpdateProperty(component.id, "uploadedFiles", parsedBackupFiles);
} catch (error) {
console.error("백업 파일 복원 실패:", error);
setUploadedFiles(componentFiles);
}
} else {
setUploadedFiles(componentFiles);
}
}
prevComponentIdRef.current = component.id;
} else if (componentFiles.length > 0 && JSON.stringify(componentFiles) !== JSON.stringify(uploadedFiles)) {
// 같은 컴포넌트에서 파일이 업데이트된 경우
console.log("🔄 FileComponentConfigPanel 파일 동기화:", {
componentId: component.id,
componentFiles: componentFiles.length,
currentFiles: uploadedFiles.length,
action: "컴포넌트 → 상태 동기화"
});
setUploadedFiles(componentFiles);
}
}, [component.id]); // 컴포넌트 ID가 변경될 때만 초기화
// 전역 파일 상태 변경 감지 (화면 복원 포함)
useEffect(() => {
const handleGlobalFileStateChange = (event: CustomEvent) => {
const { componentId, files, fileCount, isRestore } = event.detail;
if (componentId === component.id) {
console.log("🌐 FileComponentConfigPanel 전역 상태 변경 감지:", {
componentId,
fileCount,
isRestore: !!isRestore,
files: files?.map((f: any) => ({ objid: f.objid, name: f.realFileName }))
});
if (files && Array.isArray(files)) {
setUploadedFiles(files);
if (isRestore) {
console.log("✅ 파일 컴포넌트 설정 패널 데이터 복원 완료:", {
componentId,
restoredFileCount: files.length
});
}
}
}
};
if (typeof window !== 'undefined') {
window.addEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
return () => {
window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
};
}
}, [component.id]);
// 미리 정의된 문서 타입들
const docTypeOptions = [
{ value: "CONTRACT", label: "계약서" },
{ value: "DRAWING", label: "도면" },
{ value: "PHOTO", label: "사진" },
{ value: "DOCUMENT", label: "일반 문서" },
{ value: "REPORT", label: "보고서" },
{ value: "SPECIFICATION", label: "사양서" },
{ value: "MANUAL", label: "매뉴얼" },
{ value: "CERTIFICATE", label: "인증서" },
{ value: "OTHER", label: "기타" },
];
// 미리 정의된 파일 타입들
const commonFileTypes = [
{ value: "image/*", label: "이미지" },
{ value: ".pdf", label: "PDF" },
{ value: ".doc,.docx", label: "Word" },
{ value: ".xls,.xlsx", label: "Excel" },
{ value: ".ppt,.pptx", label: "PowerPoint" },
{ value: ".hwp,.hwpx,.hwpml", label: "한글" },
{ value: ".hcdt", label: "한셀" },
{ value: ".hpt", label: "한쇼" },
{ value: ".pages", label: "Pages" },
{ value: ".numbers", label: "Numbers" },
{ value: ".keynote", label: "Keynote" },
{ value: ".txt,.md,.rtf", label: "텍스트" },
{ value: "video/*", label: "비디오" },
{ value: "audio/*", label: "오디오" },
{ value: ".zip,.rar,.7z", label: "압축파일" },
];
// 파일 타입 추가
const addCommonFileType = useCallback((fileType: string) => {
const types = fileType.split(',');
const newTypes = [...acceptTypes];
types.forEach(type => {
if (!newTypes.includes(type.trim())) {
newTypes.push(type.trim());
}
});
setAcceptTypes(newTypes);
onUpdateProperty(component.id, "fileConfig.accept", newTypes);
}, [acceptTypes, component.id, onUpdateProperty]);
// 파일 타입 제거
const removeAcceptType = useCallback((typeToRemove: string) => {
const newTypes = acceptTypes.filter(type => type !== typeToRemove);
setAcceptTypes(newTypes);
onUpdateProperty(component.id, "fileConfig.accept", newTypes);
}, [acceptTypes, component.id, onUpdateProperty]);
return (
<div className="space-y-4">
{/* 기본 정보 */}
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-900"> </h4>
<div className="space-y-2">
<Label htmlFor="docType"> </Label>
<Select
value={localInputs.docType}
onValueChange={(value) => {
const selectedOption = docTypeOptions.find(option => option.value === value);
setLocalInputs((prev) => ({
...prev,
docType: value,
docTypeName: selectedOption?.label || value
}));
onUpdateProperty(component.id, "fileConfig.docType", value);
if (selectedOption) {
onUpdateProperty(component.id, "fileConfig.docTypeName", selectedOption.label);
}
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{docTypeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="docTypeName"> </Label>
<Input
id="docTypeName"
value={localInputs.docTypeName}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, docTypeName: newValue }));
onUpdateProperty(component.id, "fileConfig.docTypeName", newValue);
}}
/>
</div>
</div>
{/* 파일 업로드 제한 설정 */}
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-900"> </h4>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label htmlFor="maxSize"> (MB)</Label>
<Input
id="maxSize"
type="number"
min="1"
max="100"
value={localInputs.maxSize}
onChange={(e) => {
const newValue = parseInt(e.target.value) || 10;
setLocalInputs((prev) => ({ ...prev, maxSize: newValue }));
onUpdateProperty(component.id, "fileConfig.maxSize", newValue);
}}
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxFiles"> </Label>
<Input
id="maxFiles"
type="number"
min="1"
max="20"
value={localInputs.maxFiles}
onChange={(e) => {
const newValue = parseInt(e.target.value) || 5;
setLocalInputs((prev) => ({ ...prev, maxFiles: newValue }));
onUpdateProperty(component.id, "fileConfig.maxFiles", newValue);
}}
/>
</div>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="multiple"
checked={localValues.multiple}
onCheckedChange={(checked) => {
setLocalValues((prev) => ({ ...prev, multiple: checked as boolean }));
onUpdateProperty(component.id, "fileConfig.multiple", checked);
}}
/>
<Label htmlFor="multiple"> </Label>
</div>
</div>
{/* 허용 파일 타입 설정 */}
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-900"> </h4>
<div className="flex flex-wrap gap-2">
{acceptTypes.map((type, index) => (
<Badge key={index} variant="secondary" className="flex items-center space-x-1">
<span>{type}</span>
<Button
variant="ghost"
size="sm"
onClick={() => removeAcceptType(type)}
className="h-4 w-4 p-0 hover:bg-transparent"
>
<X className="h-3 w-3" />
</Button>
</Badge>
))}
{acceptTypes.length === 0 && <span className="text-sm text-gray-500"> </span>}
</div>
<div className="flex flex-wrap gap-1">
{commonFileTypes.map((fileType) => (
<Button
key={fileType.value}
variant="outline"
size="sm"
onClick={() => addCommonFileType(fileType.value)}
className="text-xs h-7"
>
{fileType.label}
</Button>
))}
</div>
</div>
{/* 파일 업로드 영역 */}
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-900"> </h4>
<Card>
<CardContent className="p-4">
<div
className={`
border-2 border-dashed rounded-lg p-4 text-center cursor-pointer transition-colors
${dragOver ? 'border-blue-400 bg-blue-50' : 'border-gray-300'}
${uploading ? 'opacity-50 cursor-not-allowed' : 'hover:border-gray-400'}
`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => !uploading && document.getElementById('file-input-config')?.click()}
>
<input
id="file-input-config"
type="file"
multiple={localValues.multiple}
accept={acceptTypes.join(",")}
onChange={handleFileSelect}
className="hidden"
disabled={uploading}
/>
<div className="flex flex-col items-center space-y-2">
{uploading ? (
<>
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<p className="text-sm font-medium text-gray-700"> ...</p>
</>
) : (
<>
<Upload className="w-6 h-6 text-gray-400" />
<p className="text-sm text-gray-600"> </p>
<Button variant="outline" size="sm">
<Plus className="w-4 h-4 mr-2" />
</Button>
</>
)}
</div>
</div>
{/* 업로드된 파일 목록 */}
{uploadedFiles.length > 0 && (
<div className="mt-4 space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium"> ({uploadedFiles.length})</Label>
<Badge variant="secondary">
{formatFileSize(uploadedFiles.reduce((sum, file) => sum + file.fileSize, 0))}
</Badge>
</div>
<div className="space-y-2 max-h-40 overflow-y-auto">
{uploadedFiles.map((file) => (
<div key={file.objid} className="flex items-center justify-between p-2 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-2 flex-1 min-w-0">
<div className="flex-shrink-0">
{getFileIcon(file.fileExt)}
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-gray-900 truncate">
{file.realFileName}
</p>
<div className="flex items-center space-x-1 text-xs text-gray-500">
<span>{formatFileSize(file.fileSize)}</span>
<span></span>
<span>{file.fileExt.toUpperCase()}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-1 flex-shrink-0">
<Button
variant="ghost"
size="sm"
onClick={() => handleFileDownload(file)}
className="h-6 w-6 p-0"
title="다운로드"
>
<Download className="w-3 h-3" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleFileDelete(file.objid || file.id || '')}
className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
title="삭제"
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
{/* 저장 버튼 - 주석처리 (파일이 자동으로 유지됨) */}
{/*
{uploadedFiles.length > 0 && (
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-orange-500 rounded-full animate-pulse"></div>
<span className="text-sm font-medium text-blue-900">
{uploadedFiles.length}개 파일이 임시 저장됨
</span>
</div>
<Button
onClick={handleSaveFiles}
className="bg-blue-600 hover:bg-blue-700 text-white"
size="sm"
>
파일 저장
</Button>
</div>
<p className="text-xs text-blue-700 mt-1">
다른 컴포넌트로 이동하기 전에 파일을 저장해주세요.
</p>
</div>
)}
*/}
</div>
</div>
);
};