- Replaced existing toast error messages with the new `showErrorToast` utility across multiple components, improving consistency in error reporting. - Updated error messages to provide more specific guidance for users, enhancing the overall user experience during error scenarios. - Ensured that all relevant error handling in batch management, external call configurations, cascading management, and screen management components now utilizes the new utility for better maintainability.
1255 lines
50 KiB
TypeScript
1255 lines
50 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, cn } from "@/lib/utils";
|
|
import { toast } from "sonner";
|
|
import { showErrorToast } from "@/lib/utils/toastUtils";
|
|
|
|
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,
|
|
}) => {
|
|
// console.log("🎨🎨🎨 FileComponentConfigPanel 렌더링:", {
|
|
// componentId: component?.id,
|
|
// componentType: component?.type,
|
|
// hasOnUpdateProperty: !!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 백업에서 복원 (영구 저장된 파일 + 임시 파일 + FileUploadComponent 백업)
|
|
const backupKey = `fileComponent_${component.id}_files`;
|
|
const tempBackupKey = `fileComponent_${component.id}_files_temp`;
|
|
const fileUploadBackupKey = `fileUpload_${component.id}`; // 🎯 실제 화면과 동기화
|
|
|
|
const backupFiles = localStorage.getItem(backupKey);
|
|
const tempBackupFiles = localStorage.getItem(tempBackupKey);
|
|
const fileUploadBackupFiles = localStorage.getItem(fileUploadBackupKey); // 🎯 실제 화면 백업
|
|
|
|
let parsedBackupFiles: FileInfo[] = [];
|
|
let parsedTempFiles: FileInfo[] = [];
|
|
let parsedFileUploadFiles: 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);
|
|
}
|
|
}
|
|
|
|
// 🎯 실제 화면 FileUploadComponent 백업 파싱
|
|
if (fileUploadBackupFiles) {
|
|
try {
|
|
parsedFileUploadFiles = JSON.parse(fileUploadBackupFiles);
|
|
} catch (error) {
|
|
// console.error("FileUploadComponent 백업 파일 파싱 실패:", error);
|
|
}
|
|
}
|
|
|
|
// 🎯 우선순위: 전역 상태 > FileUploadComponent 백업 > localStorage > 임시 파일 > 컴포넌트 속성
|
|
const finalFiles = globalFiles.length > 0 ? globalFiles :
|
|
parsedFileUploadFiles.length > 0 ? parsedFileUploadFiles : // 🎯 실제 화면 우선
|
|
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,
|
|
// fileUploadFiles: parsedFileUploadFiles.length, // 🎯 실제 화면 파일 수
|
|
// finalFiles: finalFiles.length,
|
|
// source: globalFiles.length > 0 ? 'global' :
|
|
// parsedFileUploadFiles.length > 0 ? 'fileUploadComponent' : // 🎯 실제 화면 소스
|
|
// 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[]) => {
|
|
// console.log("🚀🚀🚀 FileComponentConfigPanel 파일 업로드 시작:", {
|
|
// filesCount: files?.length || 0,
|
|
// componentId: component?.id,
|
|
// componentType: component?.type,
|
|
// hasOnUpdateProperty: !!onUpdateProperty
|
|
// });
|
|
|
|
if (!files || files.length === 0) {
|
|
// console.log("❌ 파일이 없음");
|
|
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}개 파일 업로드 중...`);
|
|
|
|
// 🎯 여러 방법으로 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 (디자인 모드용)
|
|
// console.warn("⚠️ screenId를 찾을 수 없어 기본값(40) 사용");
|
|
}
|
|
|
|
const componentId = component.id;
|
|
const fieldName = component.columnName || component.id || 'file_attachment';
|
|
|
|
// 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'
|
|
// });
|
|
|
|
const response = await uploadFiles({
|
|
files: filesToUpload,
|
|
// 🎯 백엔드 API가 기대하는 정확한 형식으로 설정
|
|
autoLink: true, // 자동 연결 활성화
|
|
linkedTable: 'screen_files', // 연결 테이블
|
|
recordId: screenId, // 레코드 ID
|
|
columnName: fieldName, // 컬럼명
|
|
isVirtualFileColumn: true, // 가상 파일 컬럼
|
|
docType: localInputs.docType,
|
|
docTypeName: localInputs.docTypeName,
|
|
});
|
|
|
|
// 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 eventDetail = {
|
|
componentId: component.id,
|
|
files: updatedFiles,
|
|
fileCount: updatedFiles.length,
|
|
action: 'upload',
|
|
timestamp: Date.now(),
|
|
source: 'designMode' // 🎯 화면설계 모드에서 온 이벤트임을 표시
|
|
};
|
|
|
|
// console.log("🚀🚀🚀 FileComponentConfigPanel 이벤트 발생:", eventDetail);
|
|
// console.log("🔍 현재 컴포넌트 ID:", component.id);
|
|
// console.log("🔍 업로드된 파일 수:", updatedFiles.length);
|
|
// console.log("🔍 파일 목록:", updatedFiles.map(f => f.name));
|
|
|
|
const event = new CustomEvent('globalFileStateChanged', {
|
|
detail: eventDetail
|
|
});
|
|
|
|
// 이벤트 리스너가 있는지 확인
|
|
const listenerCount = window.getEventListeners ?
|
|
window.getEventListeners(window)?.globalFileStateChanged?.length || 0 :
|
|
'unknown';
|
|
// console.log("🔍 globalFileStateChanged 리스너 수:", listenerCount);
|
|
|
|
window.dispatchEvent(event);
|
|
|
|
// console.log("✅✅✅ globalFileStateChanged 이벤트 발생 완료");
|
|
|
|
// 강제로 모든 RealtimePreview 컴포넌트에게 알림 (여러 번)
|
|
setTimeout(() => {
|
|
// console.log("🔄 추가 이벤트 발생 (지연 100ms)");
|
|
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
|
|
detail: { ...eventDetail, delayed: true }
|
|
}));
|
|
}, 100);
|
|
|
|
setTimeout(() => {
|
|
// console.log("🔄 추가 이벤트 발생 (지연 300ms)");
|
|
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
|
|
detail: { ...eventDetail, delayed: true, attempt: 2 }
|
|
}));
|
|
}, 300);
|
|
|
|
setTimeout(() => {
|
|
// console.log("🔄 추가 이벤트 발생 (지연 500ms)");
|
|
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
|
|
detail: { ...eventDetail, delayed: true, attempt: 3 }
|
|
}));
|
|
}, 500);
|
|
|
|
// 직접 전역 상태 강제 업데이트
|
|
// console.log("🔄 전역 상태 강제 업데이트 시도");
|
|
if ((window as any).forceRealtimePreviewUpdate) {
|
|
(window as any).forceRealtimePreviewUpdate(component.id, updatedFiles);
|
|
}
|
|
}
|
|
|
|
// 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: any) {
|
|
// console.error('❌ 파일 업로드 오류:', {
|
|
// error,
|
|
// errorMessage: error?.message,
|
|
// errorResponse: error?.response?.data,
|
|
// errorStatus: error?.response?.status,
|
|
// componentId: component?.id,
|
|
// screenId,
|
|
// fieldName
|
|
// });
|
|
toast.dismiss();
|
|
showErrorToast("파일 업로드에 실패했습니다", error, { guidance: "파일 크기와 형식을 확인하고 다시 시도해 주세요." });
|
|
} 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);
|
|
showErrorToast("파일 다운로드에 실패했습니다", error, { guidance: "파일이 존재하는지 확인하고 다시 시도해 주세요." });
|
|
}
|
|
}, []);
|
|
|
|
// 파일 삭제 처리
|
|
const handleFileDelete = useCallback(async (fileId: string) => {
|
|
// console.log("🗑️🗑️🗑️ FileComponentConfigPanel 파일 삭제 시작:", {
|
|
// fileId,
|
|
// componentId: component?.id,
|
|
// currentFilesCount: uploadedFiles.length,
|
|
// hasOnUpdateProperty: !!onUpdateProperty
|
|
// });
|
|
|
|
try {
|
|
// console.log("📡 deleteFile API 호출 시작...");
|
|
await deleteFile(fileId, 'temp_record');
|
|
// console.log("✅ deleteFile API 호출 성공");
|
|
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
|
|
// });
|
|
|
|
// 🎯 RealtimePreview 동기화를 위한 전역 이벤트 발생
|
|
if (typeof window !== 'undefined') {
|
|
try {
|
|
const eventDetail = {
|
|
componentId: component.id,
|
|
files: updatedFiles,
|
|
fileCount: updatedFiles.length,
|
|
action: 'delete',
|
|
timestamp: timestamp,
|
|
source: 'designMode' // 🎯 화면설계 모드에서 온 이벤트임을 표시
|
|
};
|
|
|
|
// console.log("🚀🚀🚀 FileComponentConfigPanel 삭제 이벤트 발생:", eventDetail);
|
|
|
|
const event = new CustomEvent('globalFileStateChanged', {
|
|
detail: eventDetail
|
|
});
|
|
window.dispatchEvent(event);
|
|
|
|
// console.log("✅✅✅ globalFileStateChanged 삭제 이벤트 발생 완료");
|
|
|
|
// 추가 지연 이벤트들
|
|
setTimeout(() => {
|
|
try {
|
|
// console.log("🔄 추가 삭제 이벤트 발생 (지연 100ms)");
|
|
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
|
|
detail: { ...eventDetail, delayed: true }
|
|
}));
|
|
} catch (error) {
|
|
// console.warn("FileComponentConfigPanel 지연 이벤트 발생 실패:", error);
|
|
}
|
|
}, 100);
|
|
|
|
setTimeout(() => {
|
|
try {
|
|
// console.log("🔄 추가 삭제 이벤트 발생 (지연 300ms)");
|
|
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
|
|
detail: { ...eventDetail, delayed: true, attempt: 2 }
|
|
}));
|
|
} catch (error) {
|
|
// console.warn("FileComponentConfigPanel 지연 이벤트 발생 실패:", error);
|
|
}
|
|
}, 300);
|
|
} catch (error) {
|
|
// console.warn("FileComponentConfigPanel 이벤트 발생 실패:", error);
|
|
}
|
|
|
|
// 그리드 파일 상태 새로고침 이벤트도 유지
|
|
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) {
|
|
// console.warn("FileComponentConfigPanel refreshFileStatus 이벤트 발생 실패:", error);
|
|
}
|
|
// console.log("🔄 FileComponentConfigPanel 파일 삭제 후 그리드 새로고침:", {
|
|
// tableName,
|
|
// recordId,
|
|
// columnName,
|
|
// targetObjid,
|
|
// fileCount: updatedFiles.length
|
|
// });
|
|
}
|
|
|
|
toast.success('파일이 삭제되었습니다.');
|
|
} catch (error) {
|
|
// console.error('파일 삭제 오류:', error);
|
|
showErrorToast("파일 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." });
|
|
}
|
|
}, [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);
|
|
showErrorToast("파일 저장에 실패했습니다", error, { guidance: "파일 크기와 형식을 확인하고 다시 시도해 주세요." });
|
|
}
|
|
}, [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;
|
|
// console.log("📂 드래그앤드롭 이벤트:", {
|
|
// filesCount: files.length,
|
|
// files: files.length > 0 ? Array.from(files).map(f => f.name) : [],
|
|
// componentId: component?.id
|
|
// });
|
|
if (files.length > 0) {
|
|
handleFileUpload(files);
|
|
}
|
|
}, [handleFileUpload, component?.id]);
|
|
|
|
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
// console.log("📁 파일 선택 이벤트:", {
|
|
// filesCount: e.target.files?.length || 0,
|
|
// files: e.target.files ? Array.from(e.target.files).map(f => f.name) : []
|
|
// });
|
|
|
|
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, source } = event.detail;
|
|
|
|
if (componentId === component.id) {
|
|
// console.log("🌐 FileComponentConfigPanel 전역 상태 변경 감지:", {
|
|
// componentId,
|
|
// fileCount,
|
|
// isRestore: !!isRestore,
|
|
// source: source || 'unknown',
|
|
// files: files?.map((f: any) => ({ objid: f.objid, name: f.realFileName }))
|
|
// });
|
|
|
|
if (files && Array.isArray(files)) {
|
|
setUploadedFiles(files);
|
|
|
|
// 🎯 실제 화면에서 온 이벤트이거나 화면 복원인 경우 컴포넌트 속성도 업데이트
|
|
if (isRestore || source === 'realScreen') {
|
|
// console.log("✅✅✅ 실제 화면 → 화면설계 모드 동기화 적용:", {
|
|
// componentId,
|
|
// fileCount: files.length,
|
|
// source: source || 'restore'
|
|
// });
|
|
|
|
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));
|
|
// console.log("💾 실제 화면 동기화 후 localStorage 백업 업데이트:", {
|
|
// componentId: component.id,
|
|
// fileCount: files.length
|
|
// });
|
|
} catch (e) {
|
|
// console.warn("localStorage 백업 업데이트 실패:", e);
|
|
}
|
|
|
|
// 전역 상태 업데이트
|
|
setGlobalFileState(prev => ({
|
|
...prev,
|
|
[component.id]: files
|
|
}));
|
|
} else 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, onUpdateProperty]);
|
|
|
|
// 미리 정의된 문서 타입들
|
|
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-foreground">기본 설정</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-foreground">업로드 제한</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-foreground">허용 파일 타입</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-muted-foreground">모든 파일 타입 허용</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-foreground">파일 업로드</h4>
|
|
<Card className="border-border shadow-sm">
|
|
<CardContent>
|
|
<div className="p-6">
|
|
<div
|
|
className={cn(
|
|
"border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all duration-300",
|
|
dragOver ? 'border-primary bg-primary/10 shadow-sm' : 'border-border',
|
|
uploading ? 'opacity-50 cursor-not-allowed' : 'hover:border-primary/50 hover:bg-muted/50 hover:shadow-sm'
|
|
)}
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
onClick={() => {
|
|
// console.log("🖱️ 파일 업로드 영역 클릭:", {
|
|
// uploading,
|
|
// inputElement: document.getElementById('file-input-config'),
|
|
// componentId: component?.id
|
|
// });
|
|
if (!uploading) {
|
|
const input = document.getElementById('file-input-config');
|
|
if (input) {
|
|
// console.log("✅ 파일 input 클릭 실행");
|
|
input.click();
|
|
} else {
|
|
// console.log("❌ 파일 input 요소를 찾을 수 없음");
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
<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-primary"></div>
|
|
<p className="text-sm font-medium text-foreground">업로드 중...</p>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Upload className="w-6 h-6 text-muted-foreground" />
|
|
<p className="text-sm text-muted-foreground">파일 업로드</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-muted 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-foreground truncate">
|
|
{file.realFileName}
|
|
</p>
|
|
<div className="flex items-center space-x-1 text-xs text-muted-foreground">
|
|
<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-destructive hover:text-destructive/80"
|
|
title="삭제"
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 저장 버튼 - 주석처리 (파일이 자동으로 유지됨) */}
|
|
{/*
|
|
{uploadedFiles.length > 0 && (
|
|
<div className="mt-4 p-3 bg-accent border border-primary/20 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}
|
|
variant="default"
|
|
size="sm"
|
|
>
|
|
파일 저장
|
|
</Button>
|
|
</div>
|
|
<p className="text-xs text-primary/80 mt-1">
|
|
다른 컴포넌트로 이동하기 전에 파일을 저장해주세요.
|
|
</p>
|
|
</div>
|
|
)}
|
|
*/}
|
|
</div>
|
|
</div>
|
|
);
|
|
}; |