2025-09-05 21:52:19 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
import React, { useState, useEffect, useCallback, useRef } from "react";
|
2025-09-05 21:52:19 +09:00
|
|
|
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";
|
2025-09-26 13:11:34 +09:00
|
|
|
import { Card, CardContent } from "@/components/ui/card";
|
2025-09-06 00:16:27 +09:00
|
|
|
import { FileComponent, TableInfo } from "@/types/screen";
|
2025-09-26 13:11:34 +09:00
|
|
|
import { Plus, X, Upload, File, Image, FileText, Download, Trash2 } from "lucide-react";
|
2025-09-05 21:52:19 +09:00
|
|
|
import { Button } from "@/components/ui/button";
|
2025-09-29 13:29:03 +09:00
|
|
|
import { FileInfo, FileUploadResponse } from "@/lib/registry/components/file-upload/types";
|
2025-09-26 13:11:34 +09:00
|
|
|
import { uploadFiles, downloadFile, deleteFile } from "@/lib/api/file";
|
|
|
|
|
import { formatFileSize } from "@/lib/utils";
|
|
|
|
|
import { toast } from "sonner";
|
2025-09-05 21:52:19 +09:00
|
|
|
|
|
|
|
|
interface FileComponentConfigPanelProps {
|
|
|
|
|
component: FileComponent;
|
|
|
|
|
onUpdateProperty: (componentId: string, path: string, value: any) => void;
|
2025-09-26 13:11:34 +09:00
|
|
|
currentTable?: TableInfo;
|
|
|
|
|
currentTableName?: string;
|
2025-09-05 21:52:19 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-06 00:16:27 +09:00
|
|
|
export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> = ({
|
|
|
|
|
component,
|
|
|
|
|
onUpdateProperty,
|
|
|
|
|
currentTable,
|
|
|
|
|
currentTableName,
|
|
|
|
|
}) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🎨🎨🎨 FileComponentConfigPanel 렌더링:", {
|
|
|
|
|
// componentId: component?.id,
|
|
|
|
|
// componentType: component?.type,
|
|
|
|
|
// hasOnUpdateProperty: !!onUpdateProperty,
|
|
|
|
|
// currentTable,
|
|
|
|
|
// currentTableName
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
// 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]);
|
|
|
|
|
|
2025-09-05 21:52:19 +09:00
|
|
|
// 로컬 상태
|
|
|
|
|
const [localInputs, setLocalInputs] = useState({
|
2025-09-26 13:11:34 +09:00
|
|
|
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 || "",
|
2025-09-05 21:52:19 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const [localValues, setLocalValues] = useState({
|
2025-09-26 13:11:34 +09:00
|
|
|
multiple: component.fileConfig?.multiple ?? true,
|
|
|
|
|
showPreview: component.fileConfig?.showPreview ?? true,
|
|
|
|
|
showProgress: component.fileConfig?.showProgress ?? true,
|
|
|
|
|
autoLink: component.fileConfig?.autoLink ?? false,
|
2025-09-05 21:52:19 +09:00
|
|
|
});
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
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;
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🌐 전역 파일 상태 업데이트:", {
|
|
|
|
|
// componentId: component.id,
|
|
|
|
|
// newFileCount: newState[component.id]?.length || 0,
|
|
|
|
|
// totalComponents: Object.keys(newState).length
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
|
|
|
// 강제 리렌더링을 위한 이벤트 발생
|
|
|
|
|
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
|
|
|
|
|
detail: { componentId: component.id, fileCount: newState[component.id]?.length || 0 }
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// 디버깅용 전역 함수 등록
|
|
|
|
|
(window as any).debugFileState = () => {
|
2025-10-01 18:17:30 +09:00
|
|
|
// 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) || '[]')
|
|
|
|
|
// }))
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 파일 업로드 관련 상태 - 초기화 시 전역 상태에서 복원
|
|
|
|
|
const initializeUploadedFiles = (): FileInfo[] => {
|
|
|
|
|
const componentFiles = component.uploadedFiles || [];
|
|
|
|
|
const globalFiles = getGlobalFileState()[component.id] || [];
|
|
|
|
|
|
2025-09-29 17:21:47 +09:00
|
|
|
// localStorage 백업에서 복원 (영구 저장된 파일 + 임시 파일 + FileUploadComponent 백업)
|
2025-09-26 13:11:34 +09:00
|
|
|
const backupKey = `fileComponent_${component.id}_files`;
|
|
|
|
|
const tempBackupKey = `fileComponent_${component.id}_files_temp`;
|
2025-09-29 17:21:47 +09:00
|
|
|
const fileUploadBackupKey = `fileUpload_${component.id}`; // 🎯 실제 화면과 동기화
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
const backupFiles = localStorage.getItem(backupKey);
|
|
|
|
|
const tempBackupFiles = localStorage.getItem(tempBackupKey);
|
2025-09-29 17:21:47 +09:00
|
|
|
const fileUploadBackupFiles = localStorage.getItem(fileUploadBackupKey); // 🎯 실제 화면 백업
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
let parsedBackupFiles: FileInfo[] = [];
|
|
|
|
|
let parsedTempFiles: FileInfo[] = [];
|
2025-09-29 17:21:47 +09:00
|
|
|
let parsedFileUploadFiles: FileInfo[] = []; // 🎯 실제 화면 파일
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
|
|
|
if (backupFiles) {
|
|
|
|
|
try {
|
|
|
|
|
parsedBackupFiles = JSON.parse(backupFiles);
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.error("백업 파일 파싱 실패:", error);
|
2025-09-26 13:11:34 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (tempBackupFiles) {
|
|
|
|
|
try {
|
|
|
|
|
parsedTempFiles = JSON.parse(tempBackupFiles);
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.error("임시 파일 파싱 실패:", error);
|
2025-09-26 13:11:34 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-29 17:21:47 +09:00
|
|
|
// 🎯 실제 화면 FileUploadComponent 백업 파싱
|
|
|
|
|
if (fileUploadBackupFiles) {
|
|
|
|
|
try {
|
|
|
|
|
parsedFileUploadFiles = JSON.parse(fileUploadBackupFiles);
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.error("FileUploadComponent 백업 파일 파싱 실패:", error);
|
2025-09-29 17:21:47 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 🎯 우선순위: 전역 상태 > FileUploadComponent 백업 > localStorage > 임시 파일 > 컴포넌트 속성
|
2025-09-26 13:11:34 +09:00
|
|
|
const finalFiles = globalFiles.length > 0 ? globalFiles :
|
2025-09-29 17:21:47 +09:00
|
|
|
parsedFileUploadFiles.length > 0 ? parsedFileUploadFiles : // 🎯 실제 화면 우선
|
2025-09-26 13:11:34 +09:00
|
|
|
parsedBackupFiles.length > 0 ? parsedBackupFiles :
|
|
|
|
|
parsedTempFiles.length > 0 ? parsedTempFiles :
|
|
|
|
|
componentFiles;
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🚀 FileComponentConfigPanel 초기화:", {
|
|
|
|
|
// componentId: component.id,
|
|
|
|
|
// componentFiles: componentFiles.length,
|
|
|
|
|
// globalFiles: globalFiles.length,
|
|
|
|
|
// backupFiles: parsedBackupFiles.length,
|
|
|
|
|
// tempFiles: parsedTempFiles.length,
|
|
|
|
|
// fileUploadFiles: parsedFileUploadFiles.length, // 🎯 실제 화면 파일 수
|
|
|
|
|
// finalFiles: finalFiles.length,
|
|
|
|
|
// source: globalFiles.length > 0 ? 'global' :
|
|
|
|
|
// parsedFileUploadFiles.length > 0 ? 'fileUploadComponent' : // 🎯 실제 화면 소스
|
|
|
|
|
// parsedBackupFiles.length > 0 ? 'localStorage' :
|
|
|
|
|
// parsedTempFiles.length > 0 ? 'temp' : 'component'
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
|
|
|
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());
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🔄 초기화 시 컴포넌트 속성 동기화:", {
|
|
|
|
|
// componentId: component.id,
|
|
|
|
|
// fileCount: initialFiles.length
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
}, 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[]) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🚀🚀🚀 FileComponentConfigPanel 파일 업로드 시작:", {
|
|
|
|
|
// filesCount: files?.length || 0,
|
|
|
|
|
// componentId: component?.id,
|
|
|
|
|
// componentType: component?.type,
|
|
|
|
|
// hasOnUpdateProperty: !!onUpdateProperty
|
|
|
|
|
// });
|
2025-09-29 17:21:47 +09:00
|
|
|
|
|
|
|
|
if (!files || files.length === 0) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("❌ 파일이 없음");
|
2025-09-29 17:21:47 +09:00
|
|
|
return;
|
|
|
|
|
}
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
|
|
|
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(', ')})`);
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log(`파일 검증 실패:`, {
|
|
|
|
|
// fileName: file.name,
|
|
|
|
|
// fileType: file.type,
|
|
|
|
|
// fileExt,
|
|
|
|
|
// acceptTypes,
|
|
|
|
|
// isAllowed
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log(`파일 검증 성공:`, {
|
|
|
|
|
// fileName: file.name,
|
|
|
|
|
// fileType: file.type,
|
|
|
|
|
// fileSize: file.size,
|
|
|
|
|
// acceptTypesCount: acceptTypes.length
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
|
|
|
validFiles.push(file);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (validFiles.length === 0) return;
|
|
|
|
|
|
2025-09-26 17:12:03 +09:00
|
|
|
// 중복 파일 체크
|
|
|
|
|
const existingFiles = uploadedFiles;
|
|
|
|
|
const existingFileNames = existingFiles.map(f => f.realFileName.toLowerCase());
|
|
|
|
|
const duplicates: string[] = [];
|
|
|
|
|
const uniqueFiles: File[] = [];
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🔍 중복 파일 체크:", {
|
|
|
|
|
// uploadedFiles: existingFiles.length,
|
|
|
|
|
// existingFileNames: existingFileNames,
|
|
|
|
|
// newFiles: validFiles.map(f => f.name.toLowerCase())
|
|
|
|
|
// });
|
2025-09-26 17:12:03 +09:00
|
|
|
|
|
|
|
|
validFiles.forEach(file => {
|
|
|
|
|
const fileName = file.name.toLowerCase();
|
|
|
|
|
if (existingFileNames.includes(fileName)) {
|
|
|
|
|
duplicates.push(file.name);
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("❌ 중복 파일 발견:", file.name);
|
2025-09-26 17:12:03 +09:00
|
|
|
} else {
|
|
|
|
|
uniqueFiles.push(file);
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("✅ 새로운 파일:", file.name);
|
2025-09-26 17:12:03 +09:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🔍 중복 체크 결과:", {
|
|
|
|
|
// duplicates: duplicates,
|
|
|
|
|
// uniqueFiles: uniqueFiles.map(f => f.name)
|
|
|
|
|
// });
|
2025-09-26 17:12:03 +09:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
try {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🔄 파일 업로드 시작:", {
|
|
|
|
|
// originalFiles: validFiles.length,
|
|
|
|
|
// filesToUpload: filesToUpload.length,
|
|
|
|
|
// uploading
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
setUploading(true);
|
2025-09-26 17:12:03 +09:00
|
|
|
toast.loading(`${filesToUpload.length}개 파일 업로드 중...`);
|
|
|
|
|
|
2025-09-29 17:21:47 +09:00
|
|
|
// 🎯 여러 방법으로 screenId 확인
|
|
|
|
|
let screenId = (window as any).__CURRENT_SCREEN_ID__;
|
|
|
|
|
|
|
|
|
|
// 1차: 전역 변수에서 가져오기
|
|
|
|
|
if (!screenId) {
|
|
|
|
|
// 2차: URL에서 추출 시도
|
|
|
|
|
if (typeof window !== 'undefined' && window.location.pathname.includes('/screens/')) {
|
|
|
|
|
const pathScreenId = window.location.pathname.split('/screens/')[1];
|
|
|
|
|
if (pathScreenId && !isNaN(parseInt(pathScreenId))) {
|
|
|
|
|
screenId = parseInt(pathScreenId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3차: 기본값 설정
|
|
|
|
|
if (!screenId) {
|
|
|
|
|
screenId = 40; // 기본 화면 ID (디자인 모드용)
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.warn("⚠️ screenId를 찾을 수 없어 기본값(40) 사용");
|
2025-09-29 17:21:47 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-26 17:12:03 +09:00
|
|
|
const componentId = component.id;
|
|
|
|
|
const fieldName = component.columnName || component.id || 'file_attachment';
|
2025-09-29 17:21:47 +09:00
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("📋 파일 업로드 기본 정보:", {
|
|
|
|
|
// screenId,
|
|
|
|
|
// screenIdSource: (window as any).__CURRENT_SCREEN_ID__ ? 'global' : 'url_or_default',
|
|
|
|
|
// componentId,
|
|
|
|
|
// fieldName,
|
|
|
|
|
// docType: localInputs.docType,
|
|
|
|
|
// docTypeName: localInputs.docTypeName,
|
|
|
|
|
// currentPath: typeof window !== 'undefined' ? window.location.pathname : 'unknown'
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
|
|
|
const response = await uploadFiles({
|
2025-09-26 17:12:03 +09:00
|
|
|
files: filesToUpload,
|
2025-09-29 17:21:47 +09:00
|
|
|
// 🎯 백엔드 API가 기대하는 정확한 형식으로 설정
|
|
|
|
|
autoLink: true, // 자동 연결 활성화
|
|
|
|
|
linkedTable: 'screen_files', // 연결 테이블
|
|
|
|
|
recordId: screenId, // 레코드 ID
|
|
|
|
|
columnName: fieldName, // 컬럼명
|
|
|
|
|
isVirtualFileColumn: true, // 가상 파일 컬럼
|
2025-09-26 13:11:34 +09:00
|
|
|
docType: localInputs.docType,
|
|
|
|
|
docTypeName: localInputs.docTypeName,
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("📤 파일 업로드 응답:", response);
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
|
|
|
if (response.success && (response.data || response.files)) {
|
|
|
|
|
const filesData = response.data || response.files;
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("📁 업로드된 파일 데이터:", filesData);
|
2025-09-26 13:11:34 +09:00
|
|
|
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));
|
|
|
|
|
|
2025-09-29 13:29:03 +09:00
|
|
|
// 전역 파일 상태 변경 이벤트 발생 (RealtimePreview 업데이트용)
|
|
|
|
|
if (typeof window !== 'undefined') {
|
2025-09-29 17:21:47 +09:00
|
|
|
const eventDetail = {
|
|
|
|
|
componentId: component.id,
|
|
|
|
|
files: updatedFiles,
|
|
|
|
|
fileCount: updatedFiles.length,
|
|
|
|
|
action: 'upload',
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
source: 'designMode' // 🎯 화면설계 모드에서 온 이벤트임을 표시
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🚀🚀🚀 FileComponentConfigPanel 이벤트 발생:", eventDetail);
|
|
|
|
|
// console.log("🔍 현재 컴포넌트 ID:", component.id);
|
|
|
|
|
// console.log("🔍 업로드된 파일 수:", updatedFiles.length);
|
|
|
|
|
// console.log("🔍 파일 목록:", updatedFiles.map(f => f.name));
|
2025-09-29 17:21:47 +09:00
|
|
|
|
2025-09-29 13:29:03 +09:00
|
|
|
const event = new CustomEvent('globalFileStateChanged', {
|
2025-09-29 17:21:47 +09:00
|
|
|
detail: eventDetail
|
2025-09-29 13:29:03 +09:00
|
|
|
});
|
2025-09-29 17:21:47 +09:00
|
|
|
|
|
|
|
|
// 이벤트 리스너가 있는지 확인
|
|
|
|
|
const listenerCount = window.getEventListeners ?
|
|
|
|
|
window.getEventListeners(window)?.globalFileStateChanged?.length || 0 :
|
|
|
|
|
'unknown';
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🔍 globalFileStateChanged 리스너 수:", listenerCount);
|
2025-09-29 17:21:47 +09:00
|
|
|
|
2025-09-29 13:29:03 +09:00
|
|
|
window.dispatchEvent(event);
|
2025-09-29 17:21:47 +09:00
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("✅✅✅ globalFileStateChanged 이벤트 발생 완료");
|
2025-09-29 17:21:47 +09:00
|
|
|
|
|
|
|
|
// 강제로 모든 RealtimePreview 컴포넌트에게 알림 (여러 번)
|
|
|
|
|
setTimeout(() => {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🔄 추가 이벤트 발생 (지연 100ms)");
|
2025-09-29 17:21:47 +09:00
|
|
|
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
|
|
|
|
|
detail: { ...eventDetail, delayed: true }
|
|
|
|
|
}));
|
|
|
|
|
}, 100);
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🔄 추가 이벤트 발생 (지연 300ms)");
|
2025-09-29 17:21:47 +09:00
|
|
|
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
|
|
|
|
|
detail: { ...eventDetail, delayed: true, attempt: 2 }
|
|
|
|
|
}));
|
|
|
|
|
}, 300);
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🔄 추가 이벤트 발생 (지연 500ms)");
|
2025-09-29 17:21:47 +09:00
|
|
|
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
|
|
|
|
|
detail: { ...eventDetail, delayed: true, attempt: 3 }
|
|
|
|
|
}));
|
|
|
|
|
}, 500);
|
|
|
|
|
|
|
|
|
|
// 직접 전역 상태 강제 업데이트
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🔄 전역 상태 강제 업데이트 시도");
|
2025-09-29 17:21:47 +09:00
|
|
|
if ((window as any).forceRealtimePreviewUpdate) {
|
|
|
|
|
(window as any).forceRealtimePreviewUpdate(component.id, updatedFiles);
|
|
|
|
|
}
|
2025-09-29 13:29:03 +09:00
|
|
|
}
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
// 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'
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
|
2025-09-26 17:12:03 +09:00
|
|
|
// 그리드 파일 상태 새로고침 이벤트 발생
|
|
|
|
|
if (typeof window !== 'undefined') {
|
2025-09-29 13:29:03 +09:00
|
|
|
const tableName = component.tableName || currentTableName || 'unknown';
|
|
|
|
|
const columnName = component.columnName || component.id;
|
|
|
|
|
const recordId = component.id; // 임시로 컴포넌트 ID 사용
|
|
|
|
|
const targetObjid = component.id;
|
|
|
|
|
|
2025-09-26 17:12:03 +09:00
|
|
|
const refreshEvent = new CustomEvent('refreshFileStatus', {
|
|
|
|
|
detail: {
|
|
|
|
|
tableName: tableName,
|
|
|
|
|
recordId: recordId,
|
|
|
|
|
columnName: columnName,
|
|
|
|
|
targetObjid: targetObjid,
|
|
|
|
|
fileCount: updatedFiles.length
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
window.dispatchEvent(refreshEvent);
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🔄 FileComponentConfigPanel 그리드 새로고침 이벤트 발생:", {
|
|
|
|
|
// tableName,
|
|
|
|
|
// recordId,
|
|
|
|
|
// columnName,
|
|
|
|
|
// targetObjid,
|
|
|
|
|
// fileCount: updatedFiles.length
|
|
|
|
|
// });
|
2025-09-26 17:12:03 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
toast.dismiss();
|
|
|
|
|
toast.success(`${validFiles.length}개 파일이 성공적으로 업로드되었습니다.`);
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("✅ 파일 업로드 성공:", {
|
|
|
|
|
// newFilesCount: newFiles.length,
|
|
|
|
|
// totalFiles: updatedFiles.length,
|
|
|
|
|
// componentId: component.id,
|
|
|
|
|
// updatedFiles: updatedFiles.map(f => ({ objid: f.objid, name: f.realFileName }))
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
} else {
|
|
|
|
|
throw new Error(response.message || '파일 업로드에 실패했습니다.');
|
|
|
|
|
}
|
2025-09-29 17:21:47 +09:00
|
|
|
} catch (error: any) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.error('❌ 파일 업로드 오류:', {
|
|
|
|
|
// error,
|
|
|
|
|
// errorMessage: error?.message,
|
|
|
|
|
// errorResponse: error?.response?.data,
|
|
|
|
|
// errorStatus: error?.response?.status,
|
|
|
|
|
// componentId: component?.id,
|
|
|
|
|
// screenId,
|
|
|
|
|
// fieldName
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
toast.dismiss();
|
2025-09-29 17:21:47 +09:00
|
|
|
toast.error(`파일 업로드에 실패했습니다: ${error?.message || '알 수 없는 오류'}`);
|
2025-09-26 13:11:34 +09:00
|
|
|
} finally {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🏁 파일 업로드 완료, 로딩 상태 해제");
|
2025-09-26 13:11:34 +09:00
|
|
|
setUploading(false);
|
|
|
|
|
}
|
|
|
|
|
}, [localInputs, localValues, uploadedFiles, onUpdateProperty, currentTableName, component, acceptTypes]);
|
|
|
|
|
|
|
|
|
|
// 파일 다운로드 처리
|
|
|
|
|
const handleFileDownload = useCallback(async (file: FileInfo) => {
|
|
|
|
|
try {
|
|
|
|
|
await downloadFile({
|
2025-09-29 13:29:03 +09:00
|
|
|
fileId: file.objid || file.id || '',
|
2025-09-26 13:11:34 +09:00
|
|
|
serverFilename: file.savedFileName,
|
|
|
|
|
originalName: file.realFileName || file.name || 'download',
|
|
|
|
|
});
|
|
|
|
|
toast.success(`${file.realFileName || file.name} 다운로드가 완료되었습니다.`);
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.error('파일 다운로드 오류:', error);
|
2025-09-26 13:11:34 +09:00
|
|
|
toast.error('파일 다운로드에 실패했습니다.');
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 파일 삭제 처리
|
|
|
|
|
const handleFileDelete = useCallback(async (fileId: string) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🗑️🗑️🗑️ FileComponentConfigPanel 파일 삭제 시작:", {
|
|
|
|
|
// fileId,
|
|
|
|
|
// componentId: component?.id,
|
|
|
|
|
// currentFilesCount: uploadedFiles.length,
|
|
|
|
|
// hasOnUpdateProperty: !!onUpdateProperty
|
|
|
|
|
// });
|
2025-09-29 17:21:47 +09:00
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
try {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("📡 deleteFile API 호출 시작...");
|
2025-09-29 13:29:03 +09:00
|
|
|
await deleteFile(fileId, 'temp_record');
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("✅ deleteFile API 호출 성공");
|
2025-09-26 13:11:34 +09:00
|
|
|
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));
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🗑️ FileComponentConfigPanel 파일 삭제:", {
|
|
|
|
|
// componentId: component.id,
|
|
|
|
|
// deletedFileId: fileId,
|
|
|
|
|
// remainingFiles: updatedFiles.length,
|
|
|
|
|
// timestamp: timestamp
|
|
|
|
|
// });
|
2025-09-26 17:12:03 +09:00
|
|
|
|
2025-09-29 17:21:47 +09:00
|
|
|
// 🎯 RealtimePreview 동기화를 위한 전역 이벤트 발생
|
|
|
|
|
if (typeof window !== 'undefined') {
|
2025-09-29 19:32:20 +09:00
|
|
|
try {
|
|
|
|
|
const eventDetail = {
|
|
|
|
|
componentId: component.id,
|
|
|
|
|
files: updatedFiles,
|
|
|
|
|
fileCount: updatedFiles.length,
|
|
|
|
|
action: 'delete',
|
|
|
|
|
timestamp: timestamp,
|
|
|
|
|
source: 'designMode' // 🎯 화면설계 모드에서 온 이벤트임을 표시
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🚀🚀🚀 FileComponentConfigPanel 삭제 이벤트 발생:", eventDetail);
|
2025-09-29 19:32:20 +09:00
|
|
|
|
|
|
|
|
const event = new CustomEvent('globalFileStateChanged', {
|
|
|
|
|
detail: eventDetail
|
|
|
|
|
});
|
|
|
|
|
window.dispatchEvent(event);
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("✅✅✅ globalFileStateChanged 삭제 이벤트 발생 완료");
|
2025-09-29 19:32:20 +09:00
|
|
|
|
|
|
|
|
// 추가 지연 이벤트들
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
try {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🔄 추가 삭제 이벤트 발생 (지연 100ms)");
|
2025-09-29 19:32:20 +09:00
|
|
|
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
|
|
|
|
|
detail: { ...eventDetail, delayed: true }
|
|
|
|
|
}));
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.warn("FileComponentConfigPanel 지연 이벤트 발생 실패:", error);
|
2025-09-29 19:32:20 +09:00
|
|
|
}
|
|
|
|
|
}, 100);
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
try {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🔄 추가 삭제 이벤트 발생 (지연 300ms)");
|
2025-09-29 19:32:20 +09:00
|
|
|
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
|
|
|
|
|
detail: { ...eventDetail, delayed: true, attempt: 2 }
|
|
|
|
|
}));
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.warn("FileComponentConfigPanel 지연 이벤트 발생 실패:", error);
|
2025-09-29 19:32:20 +09:00
|
|
|
}
|
|
|
|
|
}, 300);
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.warn("FileComponentConfigPanel 이벤트 발생 실패:", error);
|
2025-09-29 19:32:20 +09:00
|
|
|
}
|
2025-09-29 17:21:47 +09:00
|
|
|
|
|
|
|
|
// 그리드 파일 상태 새로고침 이벤트도 유지
|
2025-09-29 19:32:20 +09:00
|
|
|
try {
|
|
|
|
|
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);
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.warn("FileComponentConfigPanel refreshFileStatus 이벤트 발생 실패:", error);
|
2025-09-29 19:32:20 +09:00
|
|
|
}
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🔄 FileComponentConfigPanel 파일 삭제 후 그리드 새로고침:", {
|
|
|
|
|
// tableName,
|
|
|
|
|
// recordId,
|
|
|
|
|
// columnName,
|
|
|
|
|
// targetObjid,
|
|
|
|
|
// fileCount: updatedFiles.length
|
|
|
|
|
// });
|
2025-09-26 17:12:03 +09:00
|
|
|
}
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
|
|
|
toast.success('파일이 삭제되었습니다.');
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.error('파일 삭제 오류:', error);
|
2025-09-26 13:11:34 +09:00
|
|
|
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);
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("💾 파일 저장 완료:", {
|
|
|
|
|
// componentId: component.id,
|
|
|
|
|
// fileCount: uploadedFiles.length,
|
|
|
|
|
// timestamp: timestamp,
|
|
|
|
|
// files: uploadedFiles.map(f => ({ objid: f.objid, name: f.realFileName }))
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
|
|
|
toast.success(`${uploadedFiles.length}개 파일이 영구 저장되었습니다.`);
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.error('파일 저장 오류:', error);
|
2025-09-26 13:11:34 +09:00
|
|
|
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;
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("📂 드래그앤드롭 이벤트:", {
|
|
|
|
|
// filesCount: files.length,
|
|
|
|
|
// files: files.length > 0 ? Array.from(files).map(f => f.name) : [],
|
|
|
|
|
// componentId: component?.id
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
if (files.length > 0) {
|
|
|
|
|
handleFileUpload(files);
|
|
|
|
|
}
|
2025-09-29 17:21:47 +09:00
|
|
|
}, [handleFileUpload, component?.id]);
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
|
|
|
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("📁 파일 선택 이벤트:", {
|
|
|
|
|
// filesCount: e.target.files?.length || 0,
|
|
|
|
|
// files: e.target.files ? Array.from(e.target.files).map(f => f.name) : []
|
|
|
|
|
// });
|
2025-09-29 17:21:47 +09:00
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
const files = e.target.files;
|
|
|
|
|
if (files && files.length > 0) {
|
|
|
|
|
handleFileUpload(files);
|
|
|
|
|
}
|
|
|
|
|
e.target.value = '';
|
|
|
|
|
}, [handleFileUpload]);
|
2025-09-05 21:52:19 +09:00
|
|
|
|
|
|
|
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setLocalInputs({
|
2025-09-26 13:11:34 +09:00
|
|
|
docType: component.fileConfig?.docType || "DOCUMENT",
|
|
|
|
|
docTypeName: component.fileConfig?.docTypeName || "일반 문서",
|
|
|
|
|
dragDropText: component.fileConfig?.dragDropText || "파일을 드래그하거나 클릭하여 업로드하세요",
|
|
|
|
|
maxSize: component.fileConfig?.maxSize || 10,
|
|
|
|
|
maxFiles: component.fileConfig?.maxFiles || 5,
|
2025-09-05 21:52:19 +09:00
|
|
|
newAcceptType: "",
|
2025-09-26 13:11:34 +09:00
|
|
|
linkedTable: component.fileConfig?.linkedTable || "",
|
|
|
|
|
linkedField: component.fileConfig?.linkedField || "",
|
2025-09-05 21:52:19 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setLocalValues({
|
2025-09-26 13:11:34 +09:00
|
|
|
multiple: component.fileConfig?.multiple ?? true,
|
|
|
|
|
showPreview: component.fileConfig?.showPreview ?? true,
|
|
|
|
|
showProgress: component.fileConfig?.showProgress ?? true,
|
|
|
|
|
autoLink: component.fileConfig?.autoLink ?? false,
|
2025-09-05 21:52:19 +09:00
|
|
|
});
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
setAcceptTypes(component.fileConfig?.accept || []);
|
|
|
|
|
|
|
|
|
|
// 파일 목록 동기화 - 컴포넌트 ID가 변경되었을 때만 초기화
|
|
|
|
|
const componentFiles = component.uploadedFiles || [];
|
|
|
|
|
|
|
|
|
|
if (prevComponentIdRef.current !== component.id) {
|
|
|
|
|
// 새로운 컴포넌트로 변경된 경우
|
2025-10-01 18:17:30 +09:00
|
|
|
// 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'
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
|
|
|
// 1순위: 전역 상태에서 파일 복원
|
|
|
|
|
const globalFileState = getGlobalFileState();
|
|
|
|
|
const globalFiles = globalFileState[component.id];
|
|
|
|
|
|
|
|
|
|
if (globalFiles && globalFiles.length > 0) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🌐 전역 상태에서 파일 복원:", {
|
|
|
|
|
// componentId: component.id,
|
|
|
|
|
// globalFiles: globalFiles.length,
|
|
|
|
|
// action: "전역 상태 → 상태 복원"
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
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);
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("📂 localStorage에서 파일 복원:", {
|
|
|
|
|
// componentId: component.id,
|
|
|
|
|
// backupFiles: parsedBackupFiles.length,
|
|
|
|
|
// action: "백업 → 상태 복원"
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
setUploadedFiles(parsedBackupFiles);
|
|
|
|
|
// 전역 상태에도 저장
|
|
|
|
|
setGlobalFileState(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[component.id]: parsedBackupFiles
|
|
|
|
|
}));
|
|
|
|
|
// 컴포넌트 속성에도 복원
|
|
|
|
|
onUpdateProperty(component.id, "uploadedFiles", parsedBackupFiles);
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.error("백업 파일 복원 실패:", error);
|
2025-09-26 13:11:34 +09:00
|
|
|
setUploadedFiles(componentFiles);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
setUploadedFiles(componentFiles);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prevComponentIdRef.current = component.id;
|
|
|
|
|
} else if (componentFiles.length > 0 && JSON.stringify(componentFiles) !== JSON.stringify(uploadedFiles)) {
|
|
|
|
|
// 같은 컴포넌트에서 파일이 업데이트된 경우
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🔄 FileComponentConfigPanel 파일 동기화:", {
|
|
|
|
|
// componentId: component.id,
|
|
|
|
|
// componentFiles: componentFiles.length,
|
|
|
|
|
// currentFiles: uploadedFiles.length,
|
|
|
|
|
// action: "컴포넌트 → 상태 동기화"
|
|
|
|
|
// });
|
2025-09-26 13:11:34 +09:00
|
|
|
setUploadedFiles(componentFiles);
|
|
|
|
|
}
|
|
|
|
|
}, [component.id]); // 컴포넌트 ID가 변경될 때만 초기화
|
2025-09-05 21:52:19 +09:00
|
|
|
|
2025-09-26 17:12:03 +09:00
|
|
|
// 전역 파일 상태 변경 감지 (화면 복원 포함)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleGlobalFileStateChange = (event: CustomEvent) => {
|
2025-09-29 17:21:47 +09:00
|
|
|
const { componentId, files, fileCount, isRestore, source } = event.detail;
|
2025-09-26 17:12:03 +09:00
|
|
|
|
|
|
|
|
if (componentId === component.id) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🌐 FileComponentConfigPanel 전역 상태 변경 감지:", {
|
|
|
|
|
// componentId,
|
|
|
|
|
// fileCount,
|
|
|
|
|
// isRestore: !!isRestore,
|
|
|
|
|
// source: source || 'unknown',
|
|
|
|
|
// files: files?.map((f: any) => ({ objid: f.objid, name: f.realFileName }))
|
|
|
|
|
// });
|
2025-09-26 17:12:03 +09:00
|
|
|
|
|
|
|
|
if (files && Array.isArray(files)) {
|
|
|
|
|
setUploadedFiles(files);
|
|
|
|
|
|
2025-09-29 17:21:47 +09:00
|
|
|
// 🎯 실제 화면에서 온 이벤트이거나 화면 복원인 경우 컴포넌트 속성도 업데이트
|
|
|
|
|
if (isRestore || source === 'realScreen') {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("✅✅✅ 실제 화면 → 화면설계 모드 동기화 적용:", {
|
|
|
|
|
// componentId,
|
|
|
|
|
// fileCount: files.length,
|
|
|
|
|
// source: source || 'restore'
|
|
|
|
|
// });
|
2025-09-29 17:21:47 +09:00
|
|
|
|
|
|
|
|
onUpdateProperty(component.id, "uploadedFiles", files);
|
|
|
|
|
onUpdateProperty(component.id, "lastFileUpdate", Date.now());
|
|
|
|
|
|
|
|
|
|
// localStorage 백업도 업데이트
|
|
|
|
|
try {
|
|
|
|
|
const backupKey = `fileComponent_${component.id}_files`;
|
|
|
|
|
localStorage.setItem(backupKey, JSON.stringify(files));
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("💾 실제 화면 동기화 후 localStorage 백업 업데이트:", {
|
|
|
|
|
// componentId: component.id,
|
|
|
|
|
// fileCount: files.length
|
|
|
|
|
// });
|
2025-09-29 17:21:47 +09:00
|
|
|
} catch (e) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.warn("localStorage 백업 업데이트 실패:", e);
|
2025-09-29 17:21:47 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 전역 상태 업데이트
|
|
|
|
|
setGlobalFileState(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[component.id]: files
|
|
|
|
|
}));
|
|
|
|
|
} else if (isRestore) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("✅ 파일 컴포넌트 설정 패널 데이터 복원 완료:", {
|
|
|
|
|
// componentId,
|
|
|
|
|
// restoredFileCount: files.length
|
|
|
|
|
// });
|
2025-09-26 17:12:03 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
window.addEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-09-29 17:21:47 +09:00
|
|
|
}, [component.id, onUpdateProperty]);
|
2025-09-26 17:12:03 +09:00
|
|
|
|
2025-09-05 21:52:19 +09:00
|
|
|
// 미리 정의된 문서 타입들
|
|
|
|
|
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 = [
|
2025-09-26 13:11:34 +09:00
|
|
|
{ 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: "압축파일" },
|
2025-09-05 21:52:19 +09:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 파일 타입 추가
|
2025-09-26 13:11:34 +09:00
|
|
|
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]);
|
2025-09-05 21:52:19 +09:00
|
|
|
|
|
|
|
|
// 파일 타입 제거
|
2025-09-26 13:11:34 +09:00
|
|
|
const removeAcceptType = useCallback((typeToRemove: string) => {
|
|
|
|
|
const newTypes = acceptTypes.filter(type => type !== typeToRemove);
|
|
|
|
|
setAcceptTypes(newTypes);
|
|
|
|
|
onUpdateProperty(component.id, "fileConfig.accept", newTypes);
|
|
|
|
|
}, [acceptTypes, component.id, onUpdateProperty]);
|
2025-09-05 21:52:19 +09:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
2025-09-26 13:11:34 +09:00
|
|
|
{/* 기본 정보 */}
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<h4 className="text-sm font-medium text-gray-900">기본 설정</h4>
|
2025-09-05 21:52:19 +09:00
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="docType">문서 타입</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={localInputs.docType}
|
|
|
|
|
onValueChange={(value) => {
|
2025-09-26 13:11:34 +09:00
|
|
|
const selectedOption = docTypeOptions.find(option => option.value === value);
|
|
|
|
|
setLocalInputs((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
docType: value,
|
|
|
|
|
docTypeName: selectedOption?.label || value
|
|
|
|
|
}));
|
2025-09-05 21:52:19 +09:00
|
|
|
onUpdateProperty(component.id, "fileConfig.docType", value);
|
|
|
|
|
if (selectedOption) {
|
|
|
|
|
onUpdateProperty(component.id, "fileConfig.docTypeName", selectedOption.label);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger>
|
2025-09-26 13:11:34 +09:00
|
|
|
<SelectValue />
|
2025-09-05 21:52:19 +09:00
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{docTypeOptions.map((option) => (
|
|
|
|
|
<SelectItem key={option.value} value={option.value}>
|
|
|
|
|
{option.label}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
2025-09-26 13:11:34 +09:00
|
|
|
<Label htmlFor="docTypeName">문서 타입명</Label>
|
2025-09-05 21:52:19 +09:00
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
{/* 파일 업로드 제한 설정 */}
|
2025-09-26 13:11:34 +09:00
|
|
|
<div className="space-y-3">
|
|
|
|
|
<h4 className="text-sm font-medium text-gray-900">업로드 제한</h4>
|
2025-09-05 21:52:19 +09:00
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
<div className="grid grid-cols-2 gap-3">
|
2025-09-05 21:52:19 +09:00
|
|
|
<div className="space-y-2">
|
2025-09-26 13:11:34 +09:00
|
|
|
<Label htmlFor="maxSize">최대 크기 (MB)</Label>
|
2025-09-05 21:52:19 +09:00
|
|
|
<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">
|
2025-09-26 13:11:34 +09:00
|
|
|
<Label htmlFor="maxFiles">최대 개수</Label>
|
2025-09-05 21:52:19 +09:00
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
{/* 허용 파일 타입 설정 */}
|
2025-09-26 13:11:34 +09:00
|
|
|
<div className="space-y-3">
|
2025-09-05 21:52:19 +09:00
|
|
|
<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>
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
<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}
|
2025-09-05 21:52:19 +09:00
|
|
|
</Button>
|
2025-09-26 13:11:34 +09:00
|
|
|
))}
|
2025-09-05 21:52:19 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
{/* 파일 업로드 영역 */}
|
2025-09-05 21:52:19 +09:00
|
|
|
<div className="space-y-2">
|
2025-09-26 13:11:34 +09:00
|
|
|
<h4 className="text-sm font-medium text-gray-900">파일 업로드</h4>
|
2025-09-29 17:21:47 +09:00
|
|
|
<Card className="border-gray-200/60 shadow-sm">
|
|
|
|
|
<CardContent className="p-6">
|
2025-09-26 13:11:34 +09:00
|
|
|
<div
|
|
|
|
|
className={`
|
2025-09-29 17:21:47 +09:00
|
|
|
border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all duration-300
|
|
|
|
|
${dragOver ? 'border-blue-400 bg-gradient-to-br from-blue-50 to-indigo-50 shadow-sm' : 'border-gray-300/60'}
|
|
|
|
|
${uploading ? 'opacity-50 cursor-not-allowed' : 'hover:border-blue-400/60 hover:bg-gray-50/50 hover:shadow-sm'}
|
2025-09-26 13:11:34 +09:00
|
|
|
`}
|
|
|
|
|
onDragOver={handleDragOver}
|
|
|
|
|
onDragLeave={handleDragLeave}
|
|
|
|
|
onDrop={handleDrop}
|
2025-09-29 17:21:47 +09:00
|
|
|
onClick={() => {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("🖱️ 파일 업로드 영역 클릭:", {
|
|
|
|
|
// uploading,
|
|
|
|
|
// inputElement: document.getElementById('file-input-config'),
|
|
|
|
|
// componentId: component?.id
|
|
|
|
|
// });
|
2025-09-29 17:21:47 +09:00
|
|
|
if (!uploading) {
|
|
|
|
|
const input = document.getElementById('file-input-config');
|
|
|
|
|
if (input) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("✅ 파일 input 클릭 실행");
|
2025-09-29 17:21:47 +09:00
|
|
|
input.click();
|
|
|
|
|
} else {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.log("❌ 파일 input 요소를 찾을 수 없음");
|
2025-09-29 17:21:47 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}}
|
2025-09-26 13:11:34 +09:00
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
id="file-input-config"
|
|
|
|
|
type="file"
|
|
|
|
|
multiple={localValues.multiple}
|
|
|
|
|
accept={acceptTypes.join(",")}
|
|
|
|
|
onChange={handleFileSelect}
|
|
|
|
|
className="hidden"
|
|
|
|
|
disabled={uploading}
|
2025-09-06 00:16:27 +09:00
|
|
|
/>
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
|
|
|
<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" />
|
2025-10-02 14:34:15 +09:00
|
|
|
<p className="text-sm text-muted-foreground">파일 업로드</p>
|
2025-09-26 13:11:34 +09:00
|
|
|
<Button variant="outline" size="sm">
|
|
|
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
|
|
|
파일 선택
|
|
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-09-06 00:16:27 +09:00
|
|
|
</div>
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
{/* 업로드된 파일 목록 */}
|
|
|
|
|
{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>
|
2025-09-06 00:16:27 +09:00
|
|
|
</div>
|
2025-09-26 13:11:34 +09:00
|
|
|
<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>
|
2025-09-06 00:16:27 +09:00
|
|
|
</div>
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
<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"
|
2025-09-29 13:29:03 +09:00
|
|
|
onClick={() => handleFileDelete(file.objid || file.id || '')}
|
2025-10-02 14:34:15 +09:00
|
|
|
className="h-6 w-6 p-0 text-destructive hover:text-red-700"
|
2025-09-26 13:11:34 +09:00
|
|
|
title="삭제"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="w-3 h-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
2025-09-06 00:16:27 +09:00
|
|
|
</div>
|
2025-09-26 13:11:34 +09:00
|
|
|
</div>
|
2025-09-06 00:16:27 +09:00
|
|
|
)}
|
2025-09-26 13:11:34 +09:00
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* 저장 버튼 - 주석처리 (파일이 자동으로 유지됨) */}
|
|
|
|
|
{/*
|
|
|
|
|
{uploadedFiles.length > 0 && (
|
2025-10-02 14:34:15 +09:00
|
|
|
<div className="mt-4 p-3 bg-accent border border-primary/20 rounded-lg">
|
2025-09-26 13:11:34 +09:00
|
|
|
<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}
|
2025-10-02 14:34:15 +09:00
|
|
|
variant="default"
|
2025-09-26 13:11:34 +09:00
|
|
|
size="sm"
|
|
|
|
|
>
|
|
|
|
|
파일 저장
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-xs text-blue-700 mt-1">
|
|
|
|
|
다른 컴포넌트로 이동하기 전에 파일을 저장해주세요.
|
|
|
|
|
</p>
|
2025-09-06 00:16:27 +09:00
|
|
|
</div>
|
2025-09-26 13:11:34 +09:00
|
|
|
)}
|
|
|
|
|
*/}
|
2025-09-05 21:52:19 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-09-26 13:11:34 +09:00
|
|
|
};
|