999 lines
37 KiB
TypeScript
999 lines
37 KiB
TypeScript
import React, { useState, useRef, useCallback, useEffect } from "react";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { toast } from "sonner";
|
|
import { uploadFiles, downloadFile, deleteFile, getComponentFiles } from "@/lib/api/file";
|
|
import { GlobalFileManager } from "@/lib/api/globalFile";
|
|
import { formatFileSize } from "@/lib/utils";
|
|
import { FileViewerModal } from "./FileViewerModal";
|
|
import { FileManagerModal } from "./FileManagerModal";
|
|
import { FileUploadConfig, FileInfo, FileUploadStatus, FileUploadResponse } from "./types";
|
|
import {
|
|
Upload,
|
|
File,
|
|
FileText,
|
|
Image,
|
|
Video,
|
|
Music,
|
|
Archive,
|
|
Download,
|
|
Eye,
|
|
Trash2,
|
|
AlertCircle,
|
|
FileImage,
|
|
FileVideo,
|
|
FileAudio,
|
|
Presentation,
|
|
} from "lucide-react";
|
|
|
|
// 파일 아이콘 매핑
|
|
const getFileIcon = (extension: string) => {
|
|
const ext = extension.toLowerCase().replace('.', '');
|
|
|
|
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)) {
|
|
return <FileImage className="w-6 h-6 text-blue-500" />;
|
|
}
|
|
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm'].includes(ext)) {
|
|
return <FileVideo className="w-6 h-6 text-purple-500" />;
|
|
}
|
|
if (['mp3', 'wav', 'flac', 'aac', 'ogg'].includes(ext)) {
|
|
return <FileAudio className="w-6 h-6 text-green-500" />;
|
|
}
|
|
if (['pdf'].includes(ext)) {
|
|
return <FileText className="w-6 h-6 text-red-500" />;
|
|
}
|
|
if (['doc', 'docx', 'hwp', 'hwpx', 'pages'].includes(ext)) {
|
|
return <FileText className="w-6 h-6 text-blue-600" />;
|
|
}
|
|
if (['xls', 'xlsx', 'hcell', 'numbers'].includes(ext)) {
|
|
return <FileText className="w-6 h-6 text-green-600" />;
|
|
}
|
|
if (['ppt', 'pptx', 'hanshow', 'keynote'].includes(ext)) {
|
|
return <Presentation className="w-6 h-6 text-orange-500" />;
|
|
}
|
|
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {
|
|
return <Archive className="w-6 h-6 text-gray-500" />;
|
|
}
|
|
|
|
return <File className="w-6 h-6 text-gray-400" />;
|
|
};
|
|
|
|
export interface FileUploadComponentProps {
|
|
component: any;
|
|
componentConfig: FileUploadConfig;
|
|
componentStyle: React.CSSProperties;
|
|
className: string;
|
|
isInteractive: boolean;
|
|
isDesignMode: boolean;
|
|
formData: any;
|
|
onFormDataChange: (data: any) => void;
|
|
onClick?: () => void;
|
|
onDragStart?: (e: React.DragEvent) => void;
|
|
onDragEnd?: (e: React.DragEvent) => void;
|
|
onUpdate?: (updates: Partial<any>) => void;
|
|
autoGeneration?: any;
|
|
hidden?: boolean;
|
|
onConfigChange?: (config: any) => void;
|
|
}
|
|
|
|
const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|
component,
|
|
componentConfig,
|
|
componentStyle,
|
|
className,
|
|
isInteractive,
|
|
isDesignMode = false, // 기본값 설정
|
|
formData,
|
|
onFormDataChange,
|
|
onClick,
|
|
onDragStart,
|
|
onDragEnd,
|
|
onUpdate,
|
|
}) => {
|
|
const [uploadedFiles, setUploadedFiles] = useState<FileInfo[]>([]);
|
|
const [uploadStatus, setUploadStatus] = useState<FileUploadStatus>('idle');
|
|
const [dragOver, setDragOver] = useState(false);
|
|
const [viewerFile, setViewerFile] = useState<FileInfo | null>(null);
|
|
const [isViewerOpen, setIsViewerOpen] = useState(false);
|
|
const [isFileManagerOpen, setIsFileManagerOpen] = useState(false);
|
|
const [forceUpdate, setForceUpdate] = useState(0);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
// 컴포넌트 마운트 시 즉시 localStorage에서 파일 복원
|
|
useEffect(() => {
|
|
if (!component?.id) return;
|
|
|
|
try {
|
|
const backupKey = `fileUpload_${component.id}`;
|
|
const backupFiles = localStorage.getItem(backupKey);
|
|
if (backupFiles) {
|
|
const parsedFiles = JSON.parse(backupFiles);
|
|
if (parsedFiles.length > 0) {
|
|
console.log("🚀 컴포넌트 마운트 시 파일 즉시 복원:", {
|
|
componentId: component.id,
|
|
restoredFiles: parsedFiles.length,
|
|
files: parsedFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName }))
|
|
});
|
|
setUploadedFiles(parsedFiles);
|
|
|
|
// 전역 상태에도 복원
|
|
if (typeof window !== 'undefined') {
|
|
(window as any).globalFileState = {
|
|
...(window as any).globalFileState,
|
|
[component.id]: parsedFiles
|
|
};
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn("컴포넌트 마운트 시 파일 복원 실패:", e);
|
|
}
|
|
}, [component.id]); // component.id가 변경될 때만 실행
|
|
|
|
// 템플릿 파일과 데이터 파일을 조회하는 함수
|
|
const loadComponentFiles = useCallback(async () => {
|
|
if (!component?.id) return;
|
|
|
|
try {
|
|
const screenId = formData?.screenId || (typeof window !== 'undefined' && window.location.pathname.includes('/screens/')
|
|
? parseInt(window.location.pathname.split('/screens/')[1])
|
|
: null);
|
|
|
|
if (!screenId) {
|
|
console.log("📂 화면 ID 없음, 기존 파일 로직 사용");
|
|
return false; // 기존 로직 사용
|
|
}
|
|
|
|
const params = {
|
|
screenId,
|
|
componentId: component.id,
|
|
tableName: formData?.tableName || component.tableName,
|
|
recordId: formData?.id,
|
|
columnName: component.columnName,
|
|
};
|
|
|
|
console.log("📂 컴포넌트 파일 조회:", params);
|
|
|
|
const response = await getComponentFiles(params);
|
|
|
|
if (response.success) {
|
|
console.log("📁 파일 조회 결과:", {
|
|
templateFiles: response.templateFiles.length,
|
|
dataFiles: response.dataFiles.length,
|
|
totalFiles: response.totalFiles.length,
|
|
summary: response.summary,
|
|
actualFiles: response.totalFiles
|
|
});
|
|
|
|
// 파일 데이터 형식 통일
|
|
const formattedFiles = response.totalFiles.map((file: any) => ({
|
|
objid: file.objid || file.id,
|
|
savedFileName: file.savedFileName || file.saved_file_name,
|
|
realFileName: file.realFileName || file.real_file_name,
|
|
fileSize: file.fileSize || file.file_size,
|
|
fileExt: file.fileExt || file.file_ext,
|
|
regdate: file.regdate,
|
|
status: file.status || 'ACTIVE',
|
|
uploadedAt: file.uploadedAt || new Date().toISOString(),
|
|
...file
|
|
}));
|
|
|
|
console.log("📁 형식 변환된 파일 데이터:", formattedFiles);
|
|
|
|
// 🔄 localStorage의 기존 파일과 서버 파일 병합
|
|
let finalFiles = formattedFiles;
|
|
try {
|
|
const backupKey = `fileUpload_${component.id}`;
|
|
const backupFiles = localStorage.getItem(backupKey);
|
|
if (backupFiles) {
|
|
const parsedBackupFiles = JSON.parse(backupFiles);
|
|
|
|
// 서버에 없는 localStorage 파일들을 추가 (objid 기준으로 중복 제거)
|
|
const serverObjIds = new Set(formattedFiles.map((f: any) => f.objid));
|
|
const additionalFiles = parsedBackupFiles.filter((f: any) => !serverObjIds.has(f.objid));
|
|
|
|
finalFiles = [...formattedFiles, ...additionalFiles];
|
|
|
|
console.log("🔄 파일 병합 완료:", {
|
|
서버파일: formattedFiles.length,
|
|
로컬파일: parsedBackupFiles.length,
|
|
추가파일: additionalFiles.length,
|
|
최종파일: finalFiles.length,
|
|
최종파일목록: finalFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName }))
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.warn("파일 병합 중 오류:", e);
|
|
}
|
|
|
|
setUploadedFiles(finalFiles);
|
|
|
|
// 전역 상태에도 저장
|
|
if (typeof window !== 'undefined') {
|
|
(window as any).globalFileState = {
|
|
...(window as any).globalFileState,
|
|
[component.id]: finalFiles
|
|
};
|
|
|
|
// 🌐 전역 파일 저장소에 등록 (페이지 간 공유용)
|
|
GlobalFileManager.registerFiles(finalFiles, {
|
|
uploadPage: window.location.pathname,
|
|
componentId: component.id,
|
|
screenId: formData?.screenId,
|
|
});
|
|
|
|
// localStorage 백업도 병합된 파일로 업데이트
|
|
try {
|
|
const backupKey = `fileUpload_${component.id}`;
|
|
localStorage.setItem(backupKey, JSON.stringify(finalFiles));
|
|
console.log("💾 localStorage 백업 업데이트 완료:", finalFiles.length);
|
|
} catch (e) {
|
|
console.warn("localStorage 백업 업데이트 실패:", e);
|
|
}
|
|
}
|
|
return true; // 새로운 로직 사용됨
|
|
}
|
|
} catch (error) {
|
|
console.error("파일 조회 오류:", error);
|
|
}
|
|
return false; // 기존 로직 사용
|
|
}, [component.id, component.tableName, component.columnName, formData?.screenId, formData?.tableName, formData?.id]);
|
|
|
|
// 컴포넌트 파일 동기화
|
|
useEffect(() => {
|
|
const componentFiles = (component as any)?.uploadedFiles || [];
|
|
const lastUpdate = (component as any)?.lastFileUpdate;
|
|
|
|
console.log("🔄 FileUploadComponent 파일 동기화 시작:", {
|
|
componentId: component.id,
|
|
componentFiles: componentFiles.length,
|
|
formData: formData,
|
|
screenId: formData?.screenId,
|
|
currentUploadedFiles: uploadedFiles.length
|
|
});
|
|
|
|
// 먼저 새로운 템플릿 파일 조회 시도
|
|
loadComponentFiles().then(useNewLogic => {
|
|
if (useNewLogic) {
|
|
console.log("✅ 새로운 템플릿 파일 로직 사용");
|
|
return; // 새로운 로직이 성공했으면 기존 로직 스킵
|
|
}
|
|
|
|
// 기존 로직 사용
|
|
console.log("📂 기존 파일 로직 사용");
|
|
|
|
// 전역 상태에서 최신 파일 정보 가져오기
|
|
const globalFileState = typeof window !== 'undefined' ? (window as any).globalFileState || {} : {};
|
|
const globalFiles = globalFileState[component.id] || [];
|
|
|
|
// 최신 파일 정보 사용 (전역 상태 > 컴포넌트 속성)
|
|
const currentFiles = globalFiles.length > 0 ? globalFiles : componentFiles;
|
|
|
|
console.log("🔄 FileUploadComponent 파일 동기화:", {
|
|
componentId: component.id,
|
|
componentFiles: componentFiles.length,
|
|
globalFiles: globalFiles.length,
|
|
currentFiles: currentFiles.length,
|
|
uploadedFiles: uploadedFiles.length,
|
|
lastUpdate: lastUpdate
|
|
});
|
|
|
|
// localStorage에서 백업 파일 복원 (새로고침 시 중요!)
|
|
try {
|
|
const backupKey = `fileUpload_${component.id}`;
|
|
const backupFiles = localStorage.getItem(backupKey);
|
|
if (backupFiles) {
|
|
const parsedFiles = JSON.parse(backupFiles);
|
|
if (parsedFiles.length > 0 && currentFiles.length === 0) {
|
|
console.log("🔄 localStorage에서 파일 복원:", {
|
|
componentId: component.id,
|
|
restoredFiles: parsedFiles.length,
|
|
files: parsedFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName }))
|
|
});
|
|
setUploadedFiles(parsedFiles);
|
|
|
|
// 전역 상태에도 복원
|
|
if (typeof window !== 'undefined') {
|
|
(window as any).globalFileState = {
|
|
...(window as any).globalFileState,
|
|
[component.id]: parsedFiles
|
|
};
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn("localStorage 백업 복원 실패:", e);
|
|
}
|
|
|
|
// 최신 파일과 현재 파일 비교
|
|
if (JSON.stringify(currentFiles) !== JSON.stringify(uploadedFiles)) {
|
|
console.log("🔄 useEffect에서 파일 목록 변경 감지:", {
|
|
currentFiles: currentFiles.length,
|
|
uploadedFiles: uploadedFiles.length,
|
|
currentFilesData: currentFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })),
|
|
uploadedFilesData: uploadedFiles.map(f => ({ objid: f.objid, name: f.realFileName }))
|
|
});
|
|
setUploadedFiles(currentFiles);
|
|
setForceUpdate(prev => prev + 1);
|
|
}
|
|
});
|
|
}, [loadComponentFiles, component.id, (component as any)?.uploadedFiles, (component as any)?.lastFileUpdate]);
|
|
|
|
// 전역 상태 변경 감지 (모든 파일 컴포넌트 동기화 + 화면 복원)
|
|
useEffect(() => {
|
|
const handleGlobalFileStateChange = (event: CustomEvent) => {
|
|
const { componentId, files, fileCount, timestamp, isRestore } = event.detail;
|
|
|
|
console.log("🔄 FileUploadComponent 전역 상태 변경 감지:", {
|
|
currentComponentId: component.id,
|
|
eventComponentId: componentId,
|
|
isForThisComponent: componentId === component.id,
|
|
newFileCount: fileCount,
|
|
currentFileCount: uploadedFiles.length,
|
|
timestamp,
|
|
isRestore: !!isRestore
|
|
});
|
|
|
|
// 같은 컴포넌트 ID인 경우에만 업데이트
|
|
if (componentId === component.id) {
|
|
const logMessage = isRestore ? "🔄 화면 복원으로 파일 상태 동기화" : "✅ 파일 상태 동기화 적용";
|
|
console.log(logMessage, {
|
|
componentId: component.id,
|
|
이전파일수: uploadedFiles?.length || 0,
|
|
새파일수: files?.length || 0,
|
|
files: files?.map((f: any) => ({ objid: f.objid, name: f.realFileName })) || []
|
|
});
|
|
|
|
setUploadedFiles(files);
|
|
setForceUpdate(prev => prev + 1);
|
|
|
|
// localStorage 백업도 업데이트
|
|
try {
|
|
const backupKey = `fileUpload_${component.id}`;
|
|
localStorage.setItem(backupKey, JSON.stringify(files));
|
|
} catch (e) {
|
|
console.warn("localStorage 백업 실패:", e);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (typeof window !== 'undefined') {
|
|
window.addEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
|
|
|
|
return () => {
|
|
window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
|
|
};
|
|
}
|
|
}, [component.id, uploadedFiles.length]);
|
|
|
|
// 파일 업로드 설정 - componentConfig가 undefined일 수 있으므로 안전하게 처리
|
|
const safeComponentConfig = componentConfig || {};
|
|
const fileConfig = {
|
|
accept: safeComponentConfig.accept || "*/*",
|
|
multiple: safeComponentConfig.multiple || false,
|
|
maxSize: safeComponentConfig.maxSize || 10 * 1024 * 1024, // 10MB
|
|
maxFiles: safeComponentConfig.maxFiles || 5,
|
|
...safeComponentConfig
|
|
} as FileUploadConfig;
|
|
|
|
// 파일 선택 핸들러
|
|
const handleFileSelect = useCallback(() => {
|
|
console.log("🎯 handleFileSelect 호출됨:", {
|
|
hasFileInputRef: !!fileInputRef.current,
|
|
fileInputRef: fileInputRef.current,
|
|
fileInputType: fileInputRef.current?.type,
|
|
fileInputHidden: fileInputRef.current?.className
|
|
});
|
|
|
|
if (fileInputRef.current) {
|
|
console.log("✅ fileInputRef.current.click() 호출");
|
|
fileInputRef.current.click();
|
|
} else {
|
|
console.log("❌ fileInputRef.current가 null입니다");
|
|
}
|
|
}, []);
|
|
|
|
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const files = Array.from(e.target.files || []);
|
|
if (files.length > 0) {
|
|
handleFileUpload(files);
|
|
}
|
|
}, []);
|
|
|
|
// 파일 업로드 처리
|
|
const handleFileUpload = useCallback(async (files: File[]) => {
|
|
if (!files.length) return;
|
|
|
|
// 중복 파일 체크
|
|
const existingFileNames = uploadedFiles.map(f => f.realFileName.toLowerCase());
|
|
const duplicates: string[] = [];
|
|
const uniqueFiles: File[] = [];
|
|
|
|
console.log("🔍 중복 파일 체크:", {
|
|
uploadedFiles: uploadedFiles.length,
|
|
existingFileNames: existingFileNames,
|
|
newFiles: files.map(f => f.name.toLowerCase())
|
|
});
|
|
|
|
files.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 : files;
|
|
setUploadStatus('uploading');
|
|
toast.loading("파일을 업로드하는 중...", { id: 'file-upload' });
|
|
|
|
try {
|
|
// targetObjid 생성 - 템플릿 vs 데이터 파일 구분
|
|
const tableName = formData?.tableName || component.tableName || 'default_table';
|
|
const recordId = formData?.id;
|
|
const screenId = formData?.screenId;
|
|
const columnName = component.columnName || component.id;
|
|
|
|
let targetObjid;
|
|
if (recordId && tableName) {
|
|
// 실제 데이터 파일
|
|
targetObjid = `${tableName}:${recordId}:${columnName}`;
|
|
console.log("📁 실제 데이터 파일 업로드:", targetObjid);
|
|
} else if (screenId) {
|
|
// 템플릿 파일
|
|
targetObjid = `screen_${screenId}:${component.id}`;
|
|
console.log("🎨 템플릿 파일 업로드:", targetObjid);
|
|
} else {
|
|
// 기본값 (화면관리에서 사용)
|
|
targetObjid = `temp_${component.id}`;
|
|
console.log("📝 기본 파일 업로드:", targetObjid);
|
|
}
|
|
|
|
const uploadData = {
|
|
tableName: tableName,
|
|
fieldName: columnName,
|
|
recordId: recordId || `temp_${component.id}`,
|
|
docType: component.fileConfig?.docType || 'DOCUMENT',
|
|
docTypeName: component.fileConfig?.docTypeName || '일반 문서',
|
|
targetObjid: targetObjid, // InteractiveDataTable 호환을 위한 targetObjid 추가
|
|
columnName: columnName, // 가상 파일 컬럼 지원
|
|
isVirtualFileColumn: true, // 가상 파일 컬럼으로 처리
|
|
};
|
|
|
|
console.log("📤 파일 업로드 시작:", {
|
|
originalFiles: files.length,
|
|
filesToUpload: filesToUpload.length,
|
|
files: filesToUpload.map(f => ({ name: f.name, size: f.size })),
|
|
uploadData
|
|
});
|
|
|
|
const response = await uploadFiles({
|
|
files: filesToUpload,
|
|
...uploadData
|
|
});
|
|
|
|
console.log("📤 파일 업로드 API 응답:", response);
|
|
|
|
if (response.success) {
|
|
// FileUploadResponse 타입에 맞게 files 배열 사용
|
|
const fileData = response.files || (response as any).data || [];
|
|
console.log("📁 파일 데이터 확인:", {
|
|
hasFiles: !!response.files,
|
|
hasData: !!(response as any).data,
|
|
fileDataLength: fileData.length,
|
|
fileData: fileData,
|
|
responseKeys: Object.keys(response)
|
|
});
|
|
|
|
if (fileData.length === 0) {
|
|
throw new Error("업로드된 파일 데이터를 받지 못했습니다.");
|
|
}
|
|
|
|
const newFiles = fileData.map((file: any) => ({
|
|
objid: file.objid || file.id,
|
|
savedFileName: file.saved_file_name || file.savedFileName,
|
|
realFileName: file.real_file_name || file.realFileName || file.name,
|
|
fileSize: file.file_size || file.fileSize || file.size,
|
|
fileExt: file.file_ext || file.fileExt || file.extension,
|
|
filePath: file.file_path || file.filePath || file.path,
|
|
docType: file.doc_type || file.docType,
|
|
docTypeName: file.doc_type_name || file.docTypeName,
|
|
targetObjid: file.target_objid || file.targetObjid,
|
|
parentTargetObjid: file.parent_target_objid || file.parentTargetObjid,
|
|
companyCode: file.company_code || file.companyCode,
|
|
writer: file.writer,
|
|
regdate: file.regdate,
|
|
status: file.status || 'ACTIVE',
|
|
uploadedAt: new Date().toISOString(),
|
|
...file
|
|
}));
|
|
|
|
console.log("📁 변환된 파일 데이터:", newFiles);
|
|
|
|
const updatedFiles = [...uploadedFiles, ...newFiles];
|
|
console.log("🔄 파일 상태 업데이트:", {
|
|
이전파일수: uploadedFiles.length,
|
|
새파일수: newFiles.length,
|
|
총파일수: updatedFiles.length,
|
|
updatedFiles: updatedFiles.map(f => ({ objid: f.objid, name: f.realFileName }))
|
|
});
|
|
|
|
setUploadedFiles(updatedFiles);
|
|
setUploadStatus('success');
|
|
|
|
// localStorage 백업
|
|
try {
|
|
const backupKey = `fileUpload_${component.id}`;
|
|
localStorage.setItem(backupKey, JSON.stringify(updatedFiles));
|
|
} catch (e) {
|
|
console.warn("localStorage 백업 실패:", e);
|
|
}
|
|
|
|
// 전역 상태 업데이트 (모든 파일 컴포넌트 동기화)
|
|
if (typeof window !== 'undefined') {
|
|
// 전역 파일 상태 업데이트
|
|
const globalFileState = (window as any).globalFileState || {};
|
|
globalFileState[component.id] = updatedFiles;
|
|
(window as any).globalFileState = globalFileState;
|
|
|
|
// 🌐 전역 파일 저장소에 새 파일 등록 (페이지 간 공유용)
|
|
GlobalFileManager.registerFiles(newFiles, {
|
|
uploadPage: window.location.pathname,
|
|
componentId: component.id,
|
|
screenId: formData?.screenId,
|
|
});
|
|
|
|
// 모든 파일 컴포넌트에 동기화 이벤트 발생
|
|
const syncEvent = new CustomEvent('globalFileStateChanged', {
|
|
detail: {
|
|
componentId: component.id,
|
|
files: updatedFiles,
|
|
fileCount: updatedFiles.length,
|
|
timestamp: Date.now()
|
|
}
|
|
});
|
|
window.dispatchEvent(syncEvent);
|
|
|
|
console.log("🌐 전역 파일 상태 업데이트 및 동기화 이벤트 발생:", {
|
|
componentId: component.id,
|
|
fileCount: updatedFiles.length,
|
|
globalState: Object.keys(globalFileState).map(id => ({
|
|
id,
|
|
fileCount: globalFileState[id]?.length || 0
|
|
}))
|
|
});
|
|
}
|
|
|
|
// 컴포넌트 업데이트
|
|
if (onUpdate) {
|
|
const timestamp = Date.now();
|
|
console.log("🔄 onUpdate 호출:", {
|
|
componentId: component.id,
|
|
uploadedFiles: updatedFiles.length,
|
|
timestamp: timestamp
|
|
});
|
|
onUpdate({
|
|
uploadedFiles: updatedFiles,
|
|
lastFileUpdate: timestamp
|
|
});
|
|
} else {
|
|
console.warn("⚠️ onUpdate 콜백이 없습니다!");
|
|
}
|
|
|
|
// 그리드 파일 상태 새로고침 이벤트 발생
|
|
if (typeof window !== 'undefined') {
|
|
const refreshEvent = new CustomEvent('refreshFileStatus', {
|
|
detail: {
|
|
tableName: tableName,
|
|
recordId: recordId,
|
|
columnName: columnName,
|
|
targetObjid: targetObjid,
|
|
fileCount: updatedFiles.length
|
|
}
|
|
});
|
|
window.dispatchEvent(refreshEvent);
|
|
console.log("🔄 그리드 파일 상태 새로고침 이벤트 발생:", {
|
|
tableName,
|
|
recordId,
|
|
columnName,
|
|
targetObjid,
|
|
fileCount: updatedFiles.length
|
|
});
|
|
}
|
|
|
|
// 폼 데이터 업데이트
|
|
if (onFormDataChange && component.columnName) {
|
|
const fileIds = updatedFiles.map(f => f.objid);
|
|
onFormDataChange({
|
|
...formData,
|
|
[component.columnName]: fileIds
|
|
});
|
|
}
|
|
|
|
// 컴포넌트 설정 콜백
|
|
if (safeComponentConfig.onFileUpload) {
|
|
safeComponentConfig.onFileUpload(newFiles);
|
|
}
|
|
|
|
// 성공 시 토스트 처리
|
|
setUploadStatus('idle');
|
|
toast.dismiss('file-upload');
|
|
toast.success(`${newFiles.length}개 파일 업로드 완료`);
|
|
} else {
|
|
console.error("❌ 파일 업로드 실패:", response);
|
|
throw new Error(response.message || (response as any).error || '파일 업로드에 실패했습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('파일 업로드 오류:', error);
|
|
setUploadStatus('error');
|
|
toast.dismiss('file-upload');
|
|
toast.error(`파일 업로드 오류: ${error instanceof Error ? error.message : '알 수 없는 오류'}`);
|
|
}
|
|
}, [safeComponentConfig, uploadedFiles, onFormDataChange, component.columnName, component.id, formData]);
|
|
|
|
// 파일 뷰어 열기
|
|
const handleFileView = useCallback((file: FileInfo) => {
|
|
setViewerFile(file);
|
|
setIsViewerOpen(true);
|
|
}, []);
|
|
|
|
// 파일 뷰어 닫기
|
|
const handleViewerClose = useCallback(() => {
|
|
setIsViewerOpen(false);
|
|
setViewerFile(null);
|
|
}, []);
|
|
|
|
// 파일 다운로드
|
|
const handleFileDownload = useCallback(async (file: FileInfo) => {
|
|
try {
|
|
await downloadFile({
|
|
fileId: file.objid,
|
|
serverFilename: file.savedFileName,
|
|
originalName: file.realFileName
|
|
});
|
|
toast.success(`${file.realFileName} 다운로드 완료`);
|
|
} catch (error) {
|
|
console.error('파일 다운로드 오류:', error);
|
|
toast.error('파일 다운로드에 실패했습니다.');
|
|
}
|
|
}, []);
|
|
|
|
// 파일 삭제
|
|
const handleFileDelete = useCallback(async (file: FileInfo | string) => {
|
|
try {
|
|
const fileId = typeof file === 'string' ? file : file.objid;
|
|
const fileName = typeof file === 'string' ? '파일' : file.realFileName;
|
|
|
|
const serverFilename = typeof file === 'string' ? 'temp_file' : file.savedFileName;
|
|
await deleteFile(fileId, serverFilename);
|
|
|
|
const updatedFiles = uploadedFiles.filter(f => f.objid !== fileId);
|
|
setUploadedFiles(updatedFiles);
|
|
|
|
// localStorage 백업 업데이트
|
|
try {
|
|
const backupKey = `fileUpload_${component.id}`;
|
|
localStorage.setItem(backupKey, JSON.stringify(updatedFiles));
|
|
} catch (e) {
|
|
console.warn("localStorage 백업 업데이트 실패:", e);
|
|
}
|
|
|
|
// 전역 상태 업데이트 (모든 파일 컴포넌트 동기화)
|
|
if (typeof window !== 'undefined') {
|
|
// 전역 파일 상태 업데이트
|
|
const globalFileState = (window as any).globalFileState || {};
|
|
globalFileState[component.id] = updatedFiles;
|
|
(window as any).globalFileState = globalFileState;
|
|
|
|
// 모든 파일 컴포넌트에 동기화 이벤트 발생
|
|
const syncEvent = new CustomEvent('globalFileStateChanged', {
|
|
detail: {
|
|
componentId: component.id,
|
|
files: updatedFiles,
|
|
fileCount: updatedFiles.length,
|
|
timestamp: Date.now()
|
|
}
|
|
});
|
|
window.dispatchEvent(syncEvent);
|
|
|
|
console.log("🗑️ 파일 삭제 후 전역 상태 동기화:", {
|
|
componentId: component.id,
|
|
deletedFile: fileName,
|
|
remainingFiles: updatedFiles.length
|
|
});
|
|
}
|
|
|
|
// 컴포넌트 업데이트
|
|
if (onUpdate) {
|
|
const timestamp = Date.now();
|
|
onUpdate({
|
|
uploadedFiles: updatedFiles,
|
|
lastFileUpdate: timestamp
|
|
});
|
|
}
|
|
|
|
toast.success(`${fileName} 삭제 완료`);
|
|
} catch (error) {
|
|
console.error('파일 삭제 오류:', error);
|
|
toast.error('파일 삭제에 실패했습니다.');
|
|
}
|
|
}, [uploadedFiles, onUpdate, component.id]);
|
|
|
|
// 드래그 앤 드롭 핸들러
|
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
console.log("🎯 드래그 오버 이벤트 감지:", {
|
|
readonly: safeComponentConfig.readonly,
|
|
disabled: safeComponentConfig.disabled,
|
|
dragOver: dragOver
|
|
});
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (!safeComponentConfig.readonly && !safeComponentConfig.disabled) {
|
|
setDragOver(true);
|
|
console.log("✅ 드래그 오버 활성화");
|
|
} else {
|
|
console.log("❌ 드래그 차단됨: readonly 또는 disabled");
|
|
}
|
|
}, [safeComponentConfig.readonly, safeComponentConfig.disabled, dragOver]);
|
|
|
|
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setDragOver(false);
|
|
}, []);
|
|
|
|
const handleDrop = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setDragOver(false);
|
|
|
|
if (!safeComponentConfig.readonly && !safeComponentConfig.disabled) {
|
|
const files = Array.from(e.dataTransfer.files);
|
|
if (files.length > 0) {
|
|
handleFileUpload(files);
|
|
}
|
|
}
|
|
}, [safeComponentConfig.readonly, safeComponentConfig.disabled, handleFileUpload]);
|
|
|
|
// 클릭 핸들러
|
|
const handleClick = useCallback((e: React.MouseEvent) => {
|
|
console.log("🖱️ 파일 업로드 영역 클릭:", {
|
|
readonly: safeComponentConfig.readonly,
|
|
disabled: safeComponentConfig.disabled,
|
|
hasHandleFileSelect: !!handleFileSelect
|
|
});
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (!safeComponentConfig.readonly && !safeComponentConfig.disabled) {
|
|
console.log("✅ 파일 선택 함수 호출");
|
|
handleFileSelect();
|
|
} else {
|
|
console.log("❌ 클릭 차단됨: readonly 또는 disabled");
|
|
}
|
|
onClick?.();
|
|
}, [safeComponentConfig.readonly, safeComponentConfig.disabled, handleFileSelect, onClick]);
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
...componentStyle,
|
|
border: 'none !important',
|
|
boxShadow: 'none !important',
|
|
outline: 'none !important',
|
|
backgroundColor: 'transparent !important',
|
|
padding: '0px !important',
|
|
borderRadius: '0px !important',
|
|
marginBottom: '8px !important'
|
|
}}
|
|
className={`${className} file-upload-container`}
|
|
>
|
|
{/* 라벨 렌더링 - 주석처리 */}
|
|
{/* {component.label && component.style?.labelDisplay !== false && (
|
|
<label
|
|
style={{
|
|
position: "absolute",
|
|
top: "-20px",
|
|
left: "0px",
|
|
fontSize: "12px",
|
|
color: "rgb(107, 114, 128)",
|
|
fontWeight: "400",
|
|
background: "transparent !important",
|
|
border: "none !important",
|
|
boxShadow: "none !important",
|
|
outline: "none !important",
|
|
padding: "0px !important",
|
|
margin: "0px !important"
|
|
}}
|
|
>
|
|
{component.label}
|
|
{component.required && (
|
|
<span style={{ color: "#ef4444" }}>*</span>
|
|
)}
|
|
</label>
|
|
)} */}
|
|
|
|
<div
|
|
className="w-full h-full flex flex-col space-y-2"
|
|
style={{ minHeight: '120px' }}
|
|
>
|
|
{/* 파일 업로드 영역 - 주석처리 */}
|
|
{/* {!isDesignMode && (
|
|
<div
|
|
className={`
|
|
border border-dashed rounded p-2 text-center cursor-pointer transition-colors
|
|
${dragOver ? 'border-blue-400 bg-blue-50' : 'border-gray-300'}
|
|
${safeComponentConfig.disabled ? 'opacity-50 cursor-not-allowed' : 'hover:border-gray-400'}
|
|
${uploadStatus === 'uploading' ? 'opacity-75' : ''}
|
|
`}
|
|
style={{ minHeight: '50px' }}
|
|
onClick={handleClick}
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
onDragStart={onDragStart}
|
|
onDragEnd={onDragEnd}
|
|
>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
multiple={safeComponentConfig.multiple}
|
|
accept={safeComponentConfig.accept}
|
|
onChange={handleInputChange}
|
|
className="hidden"
|
|
disabled={safeComponentConfig.disabled}
|
|
/>
|
|
|
|
{uploadStatus === 'uploading' ? (
|
|
<div className="flex flex-col items-center space-y-2">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
<span className="text-blue-600 font-medium">업로드 중...</span>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div>
|
|
<Upload className="mx-auto h-6 w-6 text-gray-400 mb-2" />
|
|
<p className="text-xs font-medium text-gray-600">
|
|
파일 업로드
|
|
</p>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)} */}
|
|
|
|
{/* 업로드된 파일 목록 - 항상 표시 */}
|
|
{(() => {
|
|
const shouldShow = true; // 항상 표시하도록 강제
|
|
console.log("🎯🎯🎯 파일 목록 렌더링 조건 체크:", {
|
|
uploadedFilesLength: uploadedFiles.length,
|
|
isDesignMode: isDesignMode,
|
|
shouldShow: shouldShow,
|
|
uploadedFiles: uploadedFiles.map(f => ({ objid: f.objid, name: f.realFileName })),
|
|
"🚨 렌더링 여부": shouldShow ? "✅ 렌더링됨" : "❌ 렌더링 안됨"
|
|
});
|
|
return shouldShow;
|
|
})() && (
|
|
<div className="flex-1 overflow-y-auto">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<h4 className="text-sm font-medium text-gray-700" style={{ textShadow: 'none', boxShadow: 'none' }}>
|
|
업로드된 파일 ({uploadedFiles.length})
|
|
</h4>
|
|
<div className="flex items-center space-x-2">
|
|
{uploadedFiles.length > 0 && (
|
|
<Badge variant="secondary" className="text-xs">
|
|
총 {formatFileSize(uploadedFiles.reduce((sum, file) => sum + file.fileSize, 0))}
|
|
</Badge>
|
|
)}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-7 px-2 text-xs"
|
|
onClick={() => setIsFileManagerOpen(true)}
|
|
style={{
|
|
boxShadow: 'none !important',
|
|
textShadow: 'none !important',
|
|
filter: 'none !important',
|
|
WebkitBoxShadow: 'none !important',
|
|
MozBoxShadow: 'none !important'
|
|
}}
|
|
>
|
|
자세히보기
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{uploadedFiles.length > 0 ? (
|
|
<div className="space-y-1">
|
|
{uploadedFiles.map((file) => (
|
|
<div key={file.objid} className="flex items-center space-x-3 p-2 bg-gray-50 rounded text-sm hover:bg-gray-100 transition-colors" style={{ boxShadow: 'none', textShadow: 'none' }}>
|
|
<div className="flex-shrink-0">
|
|
{getFileIcon(file.fileExt)}
|
|
</div>
|
|
<span className="flex-1 truncate text-gray-900 cursor-pointer" onClick={() => handleFileView(file)} style={{ textShadow: 'none' }}>
|
|
{file.realFileName}
|
|
</span>
|
|
<span className="text-xs text-gray-500" style={{ textShadow: 'none' }}>
|
|
{formatFileSize(file.fileSize)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
<div className="text-xs text-gray-500 mt-2 text-center" style={{ textShadow: 'none' }}>
|
|
💡 파일명 클릭으로 미리보기 또는 "전체 자세히보기"로 파일 관리
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-center justify-center py-8 text-gray-500" style={{ textShadow: 'none' }}>
|
|
<File className="w-12 h-12 mb-3 text-gray-300" />
|
|
<p className="text-sm font-medium" style={{ textShadow: 'none' }}>업로드된 파일이 없습니다</p>
|
|
<p className="text-xs text-gray-400 mt-1" style={{ textShadow: 'none' }}>상세설정에서 파일을 업로드하세요</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 도움말 텍스트 */}
|
|
{safeComponentConfig.helperText && (
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
{safeComponentConfig.helperText}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* 파일뷰어 모달 */}
|
|
<FileViewerModal
|
|
file={viewerFile}
|
|
isOpen={isViewerOpen}
|
|
onClose={handleViewerClose}
|
|
onDownload={handleFileDownload}
|
|
onDelete={!isDesignMode ? handleFileDelete : undefined}
|
|
/>
|
|
|
|
{/* 파일 관리 모달 */}
|
|
<FileManagerModal
|
|
isOpen={isFileManagerOpen}
|
|
onClose={() => setIsFileManagerOpen(false)}
|
|
uploadedFiles={uploadedFiles}
|
|
onFileUpload={handleFileUpload}
|
|
onFileDownload={handleFileDownload}
|
|
onFileDelete={handleFileDelete}
|
|
onFileView={handleFileView}
|
|
config={safeComponentConfig}
|
|
isDesignMode={isDesignMode}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export { FileUploadComponent };
|
|
export default FileUploadComponent;
|