문서뷰어기능구현

This commit is contained in:
leeheejin
2025-09-29 13:29:03 +09:00
parent 3600621554
commit e0143e9cba
27 changed files with 2812 additions and 247 deletions

View File

@@ -3,9 +3,11 @@ 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 } from "@/lib/api/file";
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,
@@ -75,13 +77,13 @@ export interface FileUploadComponentProps {
onConfigChange?: (config: any) => void;
}
export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
component,
componentConfig,
componentStyle,
className,
isInteractive,
isDesignMode,
isDesignMode = false, // 기본값 설정
formData,
onFormDataChange,
onClick,
@@ -94,55 +96,230 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
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;
// 전역 상태에서 최신 파일 정보 가져오기
const globalFileState = typeof window !== 'undefined' ? (window as any).globalFileState || {} : {};
const globalFiles = globalFileState[component.id] || [];
// 최신 파일 정보 사용 (전역 상태 > 컴포넌트 속성)
const currentFiles = globalFiles.length > 0 ? globalFiles : componentFiles;
console.log("🔄 FileUploadComponent 파일 동기화:", {
console.log("🔄 FileUploadComponent 파일 동기화 시작:", {
componentId: component.id,
componentFiles: componentFiles.length,
globalFiles: globalFiles.length,
currentFiles: currentFiles.length,
uploadedFiles: uploadedFiles.length,
lastUpdate: lastUpdate
formData: formData,
screenId: formData?.screenId,
currentUploadedFiles: uploadedFiles.length
});
// localStorage에서 백업 파일 복원
try {
const backupKey = `fileUpload_${component.id}`;
const backupFiles = localStorage.getItem(backupKey);
if (backupFiles && currentFiles.length === 0) {
const parsedFiles = JSON.parse(backupFiles);
setUploadedFiles(parsedFiles);
return;
// 먼저 새로운 템플릿 파일 조회 시도
loadComponentFiles().then(useNewLogic => {
if (useNewLogic) {
console.log("✅ 새로운 템플릿 파일 로직 사용");
return; // 새로운 로직이 성공했으면 기존 로직 스킵
}
} catch (e) {
console.warn("localStorage 백업 복원 실패:", e);
}
// 최신 파일과 현재 파일 비교
if (JSON.stringify(currentFiles) !== JSON.stringify(uploadedFiles)) {
console.log("🔄 useEffect에서 파일 목록 변경 감지:", {
// 기존 로직 사용
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,
currentFilesData: currentFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })),
uploadedFilesData: uploadedFiles.map(f => ({ objid: f.objid, name: f.realFileName }))
lastUpdate: lastUpdate
});
setUploadedFiles(currentFiles);
setForceUpdate(prev => prev + 1);
}
}, [component.id, (component as any)?.uploadedFiles, (component as any)?.lastFileUpdate]);
// 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(() => {
@@ -164,9 +341,9 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
const logMessage = isRestore ? "🔄 화면 복원으로 파일 상태 동기화" : "✅ 파일 상태 동기화 적용";
console.log(logMessage, {
componentId: component.id,
이전파일수: uploadedFiles.length,
새파일수: files.length,
files: files.map((f: any) => ({ objid: f.objid, name: f.realFileName }))
이전파일수: uploadedFiles?.length || 0,
새파일수: files?.length || 0,
files: files?.map((f: any) => ({ objid: f.objid, name: f.realFileName })) || []
});
setUploadedFiles(files);
@@ -203,8 +380,18 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
// 파일 선택 핸들러
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입니다");
}
}, []);
@@ -265,16 +452,31 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
toast.loading("파일을 업로드하는 중...", { id: 'file-upload' });
try {
// targetObjid 생성 (InteractiveDataTable과 호환)
// targetObjid 생성 - 템플릿 vs 데이터 파일 구분
const tableName = formData?.tableName || component.tableName || 'default_table';
const recordId = formData?.id || 'temp_record';
const recordId = formData?.id;
const screenId = formData?.screenId;
const columnName = component.columnName || component.id;
const targetObjid = `${tableName}:${recordId}:${columnName}`;
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,
recordId: recordId || `temp_${component.id}`,
docType: component.fileConfig?.docType || 'DOCUMENT',
docTypeName: component.fileConfig?.docTypeName || '일반 문서',
targetObjid: targetObjid, // InteractiveDataTable 호환을 위한 targetObjid 추가
@@ -358,6 +560,13 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
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: {
@@ -429,6 +638,11 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
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 || '파일 업로드에 실패했습니다.');
@@ -436,15 +650,31 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
} catch (error) {
console.error('파일 업로드 오류:', error);
setUploadStatus('error');
toast.dismiss();
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(file.objid, file.realFileName);
await downloadFile({
fileId: file.objid,
serverFilename: file.savedFileName,
originalName: file.realFileName
});
toast.success(`${file.realFileName} 다운로드 완료`);
} catch (error) {
console.error('파일 다운로드 오류:', error);
@@ -458,7 +688,8 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
const fileId = typeof file === 'string' ? file : file.objid;
const fileName = typeof file === 'string' ? '파일' : file.realFileName;
await deleteFile(fileId);
const serverFilename = typeof file === 'string' ? 'temp_file' : file.savedFileName;
await deleteFile(fileId, serverFilename);
const updatedFiles = uploadedFiles.filter(f => f.objid !== fileId);
setUploadedFiles(updatedFiles);
@@ -512,25 +743,23 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
}
}, [uploadedFiles, onUpdate, component.id]);
// 파일 뷰어
const handleFileView = useCallback((file: FileInfo) => {
setViewerFile(file);
setIsViewerOpen(true);
}, []);
const handleViewerClose = useCallback(() => {
setIsViewerOpen(false);
setViewerFile(null);
}, []);
// 드래그 앤 드롭 핸들러
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]);
}, [safeComponentConfig.readonly, safeComponentConfig.disabled, dragOver]);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
@@ -553,27 +782,53 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
// 클릭 핸들러
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} className={className}>
{/* 라벨 렌더링 */}
{component.label && component.style?.labelDisplay !== false && (
<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: "-25px",
top: "-20px",
left: "0px",
fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#3b83f6",
fontWeight: "500",
...(isInteractive && component.style ? component.style : {}),
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}
@@ -581,18 +836,22 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
<span style={{ color: "#ef4444" }}>*</span>
)}
</label>
)}
)} */}
<div className="w-full h-full flex flex-col space-y-2">
{/* 디자인 모드가 아닐 때만 파일 업로드 영역 표시 */}
{!isDesignMode && (
<div
className="w-full h-full flex flex-col space-y-2"
style={{ minHeight: '120px' }}
>
{/* 파일 업로드 영역 - 주석처리 */}
{/* {!isDesignMode && (
<div
className={`
border-2 border-dashed rounded-lg p-4 text-center cursor-pointer transition-colors
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}
@@ -603,13 +862,13 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
<input
ref={fileInputRef}
type="file"
multiple={safeComponentConfig.multiple}
accept={safeComponentConfig.accept}
onChange={handleInputChange}
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">
@@ -620,60 +879,82 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
) : (
<>
<div>
<Upload className="mx-auto h-12 w-12 text-gray-400 mb-4" />
<p className="text-lg font-medium text-gray-900 mb-2">
{safeComponentConfig.dragDropText || "파일을 드래그하거나 클릭하여 업로드하세요"}
</p>
<p className="text-xs text-gray-500 mt-1">
{safeComponentConfig.accept && `지원 형식: ${safeComponentConfig.accept}`}
{safeComponentConfig.maxSize && ` • 최대 ${formatFileSize(safeComponentConfig.maxSize)}`}
{safeComponentConfig.multiple && ' • 여러 파일 선택 가능'}
<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>
)}
)} */}
{/* 업로드된 파일 목록 - 디자인 모드에서는 항상 표시 */}
{(uploadedFiles.length > 0 || isDesignMode) && (
{/* 업로드된 파일 목록 - 항상 표시 */}
{(() => {
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">
<h4 className="text-sm font-medium text-gray-700" style={{ textShadow: 'none', boxShadow: 'none' }}>
({uploadedFiles.length})
</h4>
{uploadedFiles.length > 0 && (
<Badge variant="secondary" className="text-xs">
{formatFileSize(uploadedFiles.reduce((sum, file) => sum + file.fileSize, 0))}
</Badge>
)}
<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-2 p-2 bg-gray-50 rounded text-sm">
<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">
<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">
<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">
💡
<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">
<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"> </p>
<p className="text-xs text-gray-400 mt-1"> </p>
<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>
@@ -694,6 +975,20 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
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>
);