파일 업로드 쪽 수정

This commit is contained in:
dohyeons 2025-11-04 17:32:46 +09:00
parent 36ea8115cb
commit 958aeb2d53
3 changed files with 194 additions and 136 deletions

View File

@ -17,7 +17,8 @@ import {
Music, Music,
Archive, Archive,
Presentation, Presentation,
X X,
Star
} from "lucide-react"; } from "lucide-react";
import { formatFileSize } from "@/lib/utils"; import { formatFileSize } from "@/lib/utils";
import { FileViewerModal } from "./FileViewerModal"; import { FileViewerModal } from "./FileViewerModal";
@ -30,6 +31,7 @@ interface FileManagerModalProps {
onFileDownload: (file: FileInfo) => void; onFileDownload: (file: FileInfo) => void;
onFileDelete: (file: FileInfo) => void; onFileDelete: (file: FileInfo) => void;
onFileView: (file: FileInfo) => void; onFileView: (file: FileInfo) => void;
onSetRepresentative?: (file: FileInfo) => void; // 대표 이미지 설정 콜백
config: FileUploadConfig; config: FileUploadConfig;
isDesignMode?: boolean; isDesignMode?: boolean;
} }
@ -42,6 +44,7 @@ export const FileManagerModal: React.FC<FileManagerModalProps> = ({
onFileDownload, onFileDownload,
onFileDelete, onFileDelete,
onFileView, onFileView,
onSetRepresentative,
config, config,
isDesignMode = false, isDesignMode = false,
}) => { }) => {
@ -228,14 +231,32 @@ export const FileManagerModal: React.FC<FileManagerModalProps> = ({
{getFileIcon(file.fileExt)} {getFileIcon(file.fileExt)}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate"> <div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900 truncate">
{file.realFileName} {file.realFileName}
</p> </span>
{file.isRepresentative && (
<Badge variant="default" className="h-5 px-1.5 text-xs">
</Badge>
)}
</div>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
{formatFileSize(file.fileSize)} {file.fileExt.toUpperCase()} {formatFileSize(file.fileSize)} {file.fileExt.toUpperCase()}
</p> </p>
</div> </div>
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
{onSetRepresentative && (
<Button
variant={file.isRepresentative ? "default" : "ghost"}
size="sm"
className="h-8 w-8 p-0"
onClick={() => onSetRepresentative(file)}
title={file.isRepresentative ? "현재 대표 파일" : "대표 파일로 설정"}
>
<Star className={`w-4 h-4 ${file.isRepresentative ? "fill-white" : ""}`} />
</Button>
)}
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"

View File

@ -6,6 +6,7 @@ import { toast } from "sonner";
import { uploadFiles, downloadFile, deleteFile, getComponentFiles } from "@/lib/api/file"; import { uploadFiles, downloadFile, deleteFile, getComponentFiles } from "@/lib/api/file";
import { GlobalFileManager } from "@/lib/api/globalFile"; import { GlobalFileManager } from "@/lib/api/globalFile";
import { formatFileSize } from "@/lib/utils"; import { formatFileSize } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
import { FileViewerModal } from "./FileViewerModal"; import { FileViewerModal } from "./FileViewerModal";
import { FileManagerModal } from "./FileManagerModal"; import { FileManagerModal } from "./FileManagerModal";
import { FileUploadConfig, FileInfo, FileUploadStatus, FileUploadResponse } from "./types"; import { FileUploadConfig, FileInfo, FileUploadStatus, FileUploadResponse } from "./types";
@ -98,6 +99,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
const [isViewerOpen, setIsViewerOpen] = useState(false); const [isViewerOpen, setIsViewerOpen] = useState(false);
const [isFileManagerOpen, setIsFileManagerOpen] = useState(false); const [isFileManagerOpen, setIsFileManagerOpen] = useState(false);
const [forceUpdate, setForceUpdate] = useState(0); const [forceUpdate, setForceUpdate] = useState(0);
const [representativeImageUrl, setRepresentativeImageUrl] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
// 컴포넌트 마운트 시 즉시 localStorage에서 파일 복원 // 컴포넌트 마운트 시 즉시 localStorage에서 파일 복원
@ -832,6 +834,113 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
[uploadedFiles, onUpdate, component.id], [uploadedFiles, onUpdate, component.id],
); );
// 대표 이미지 Blob URL 로드
const loadRepresentativeImage = useCallback(
async (file: FileInfo) => {
try {
const isImage = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(
file.fileExt.toLowerCase().replace(".", "")
);
if (!isImage) {
setRepresentativeImageUrl(null);
return;
}
console.log("🖼️ 대표 이미지 로드 시작:", file.realFileName);
// API 클라이언트를 통해 Blob으로 다운로드 (인증 토큰 포함)
const response = await apiClient.get(`/files/download/${file.objid}`, {
params: {
serverFilename: file.savedFileName,
},
responseType: "blob",
});
// Blob URL 생성
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
// 이전 URL 정리
if (representativeImageUrl) {
window.URL.revokeObjectURL(representativeImageUrl);
}
setRepresentativeImageUrl(url);
console.log("✅ 대표 이미지 로드 성공:", url);
} catch (error) {
console.error("❌ 대표 이미지 로드 실패:", error);
setRepresentativeImageUrl(null);
}
},
[representativeImageUrl],
);
// 대표 이미지 설정 핸들러
const handleSetRepresentative = useCallback(
(file: FileInfo) => {
const updatedFiles = uploadedFiles.map((f) => ({
...f,
isRepresentative: f.objid === file.objid,
}));
setUploadedFiles(updatedFiles);
// 대표 이미지 로드
loadRepresentativeImage(file);
// localStorage 백업
try {
const backupKey = `fileUpload_${component.id}`;
localStorage.setItem(backupKey, JSON.stringify(updatedFiles));
console.log("📌 대표 파일 설정 완료:", {
componentId: component.id,
representativeFile: file.realFileName,
});
} catch (e) {
console.warn("localStorage 저장 실패:", e);
}
// 전역 상태 동기화
if (typeof window !== "undefined") {
(window as any).globalFileState = {
...(window as any).globalFileState,
[component.id]: updatedFiles,
};
// 실시간 동기화 이벤트 발송
const syncEvent = new CustomEvent("fileStateChanged", {
detail: {
componentId: component.id,
files: updatedFiles,
action: "setRepresentative",
},
});
window.dispatchEvent(syncEvent);
}
toast.success(`${file.realFileName}을(를) 대표 파일로 설정했습니다.`);
},
[uploadedFiles, component.id, loadRepresentativeImage],
);
// uploadedFiles 변경 시 대표 이미지 로드
useEffect(() => {
const representativeFile = uploadedFiles.find(f => f.isRepresentative) || uploadedFiles[0];
if (representativeFile) {
loadRepresentativeImage(representativeFile);
} else {
setRepresentativeImageUrl(null);
}
// 컴포넌트 언마운트 시 Blob URL 정리
return () => {
if (representativeImageUrl) {
window.URL.revokeObjectURL(representativeImageUrl);
}
};
}, [uploadedFiles]);
// 드래그 앤 드롭 핸들러 // 드래그 앤 드롭 핸들러
const handleDragOver = useCallback( const handleDragOver = useCallback(
(e: React.DragEvent) => { (e: React.DragEvent) => {
@ -913,8 +1022,8 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
}} }}
className={`${className} file-upload-container`} className={`${className} file-upload-container`}
> >
{/* 라벨 렌더링 - 주석처리 */} {/* 라벨 렌더링 */}
{/* {component.label && component.style?.labelDisplay !== false && ( {component.label && component.style?.labelDisplay !== false && (
<label <label
style={{ style={{
position: "absolute", position: "absolute",
@ -936,147 +1045,71 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
<span style={{ color: "#ef4444" }}>*</span> <span style={{ color: "#ef4444" }}>*</span>
)} )}
</label> </label>
)} */} )}
<div <div
className="border-border bg-card flex h-full w-full flex-col space-y-3 rounded-lg border p-3 transition-all duration-200 hover:shadow-sm" className="border-border bg-card relative flex h-full w-full flex-col rounded-lg border overflow-hidden"
> >
{/* 파일 업로드 영역 - 주석처리 */} {/* 대표 이미지 전체 화면 표시 */}
{/* {!isDesignMode && ( {uploadedFiles.length > 0 ? (() => {
<div const representativeFile = uploadedFiles.find(f => f.isRepresentative) || uploadedFiles[0];
className={` const isImage = representativeFile && ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(
border border-dashed rounded p-2 text-center cursor-pointer transition-colors representativeFile.fileExt.toLowerCase().replace(".", "")
${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' : ''} return (
`} <>
style={{ minHeight: '50px' }} {isImage && representativeImageUrl ? (
onClick={handleClick} <div className="relative h-full w-full flex items-center justify-center bg-muted/10">
onDragOver={handleDragOver} <img
onDragLeave={handleDragLeave} src={representativeImageUrl}
onDrop={handleDrop} alt={representativeFile.realFileName}
onDragStart={onDragStart} className="h-full w-full object-contain"
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>
) : isImage && !representativeImageUrl ? (
<div className="flex h-full w-full flex-col items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mb-2"></div>
<p className="text-sm text-muted-foreground"> ...</p>
</div> </div>
) : ( ) : (
<> <div className="flex h-full w-full flex-col items-center justify-center">
<div> {getFileIcon(representativeFile.fileExt)}
<Upload className="mx-auto h-6 w-6 text-gray-400 mb-2" /> <p className="mt-3 text-sm font-medium text-center px-4">
<p className="text-xs font-medium text-gray-600"> {representativeFile.realFileName}
</p> </p>
<Badge variant="secondary" className="mt-2">
</Badge>
</div>
)}
{/* 우측 하단 자세히보기 버튼 */}
<div className="absolute bottom-3 right-3">
<Button
variant="secondary"
size="sm"
className="h-8 px-3 text-xs shadow-md"
onClick={() => setIsFileManagerOpen(true)}
>
({uploadedFiles.length})
</Button>
</div> </div>
</> </>
)} );
</div> })() : (
)} */} <div className="flex h-full w-full flex-col items-center justify-center text-muted-foreground">
<File className="mb-3 h-12 w-12" />
{/* 업로드된 파일 목록 - 항상 표시 */} <p className="text-sm font-medium"> </p>
{(() => {
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="flex items-center gap-2 text-sm font-semibold">
<File className="text-primary h-4 w-4" />
({uploadedFiles.length})
</h4>
<div className="flex items-center space-x-2">
{uploadedFiles.length > 0 && (
<Badge variant="secondary" className="h-5 px-1.5 text-xs">
{formatFileSize(uploadedFiles.reduce((sum, file) => sum + file.fileSize, 0))}
</Badge>
)}
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="h-7 px-2 text-xs" className="mt-4 h-8 px-3 text-xs"
onClick={() => setIsFileManagerOpen(true)} onClick={() => setIsFileManagerOpen(true)}
style={{
boxShadow: "none !important",
textShadow: "none !important",
filter: "none !important",
WebkitBoxShadow: "none !important",
MozBoxShadow: "none !important",
}}
> >
</Button> </Button>
</div> </div>
</div>
{uploadedFiles.length > 0 ? (
<div className="space-y-1">
{uploadedFiles.map((file) => (
<div
key={file.objid}
className="hover:bg-accent flex items-center space-x-3 rounded p-2 text-sm transition-colors"
style={{ boxShadow: "none", textShadow: "none" }}
>
<div className="flex-shrink-0">{getFileIcon(file.fileExt)}</div>
<span
className="flex-1 cursor-pointer truncate text-gray-900"
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="mt-2 text-center text-xs text-gray-500" style={{ textShadow: "none" }}>
💡 "전체 자세히보기"
</div>
</div>
) : (
<div
className="flex flex-col items-center justify-center py-8 text-gray-500"
style={{ textShadow: "none" }}
>
<File className="mb-3 h-12 w-12 text-gray-300" />
<p className="text-sm font-medium" style={{ textShadow: "none" }}>
</p>
<p className="mt-1 text-xs text-gray-400" style={{ textShadow: "none" }}>
</p>
</div>
)}
</div>
</div>
)}
{/* 도움말 텍스트 */}
{safeComponentConfig.helperText && (
<p className="mt-1 text-xs text-gray-500">{safeComponentConfig.helperText}</p>
)} )}
</div> </div>
@ -1098,6 +1131,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
onFileDownload={handleFileDownload} onFileDownload={handleFileDownload}
onFileDelete={handleFileDelete} onFileDelete={handleFileDelete}
onFileView={handleFileView} onFileView={handleFileView}
onSetRepresentative={handleSetRepresentative}
config={safeComponentConfig} config={safeComponentConfig}
isDesignMode={isDesignMode} isDesignMode={isDesignMode}
/> />

View File

@ -30,6 +30,9 @@ export interface FileInfo {
type?: string; // docType과 동일 type?: string; // docType과 동일
uploadedAt?: string; // regdate와 동일 uploadedAt?: string; // regdate와 동일
_file?: File; // 로컬 파일 객체 (업로드 전) _file?: File; // 로컬 파일 객체 (업로드 전)
// 대표 이미지 설정
isRepresentative?: boolean; // 대표 이미지로 설정 여부
} }
/** /**