파일 업로드 쪽 수정
This commit is contained in:
parent
36ea8115cb
commit
958aeb2d53
|
|
@ -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">
|
||||||
{file.realFileName}
|
<span className="text-sm font-medium text-gray-900 truncate">
|
||||||
</p>
|
{file.realFileName}
|
||||||
|
</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"
|
||||||
|
|
|
||||||
|
|
@ -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,148 +1045,72 @@ 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' : ''}
|
|
||||||
`}
|
|
||||||
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>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
{/* 업로드된 파일 목록 - 항상 표시 */}
|
return (
|
||||||
{(() => {
|
<>
|
||||||
const shouldShow = true; // 항상 표시하도록 강제
|
{isImage && representativeImageUrl ? (
|
||||||
console.log("🎯🎯🎯 파일 목록 렌더링 조건 체크:", {
|
<div className="relative h-full w-full flex items-center justify-center bg-muted/10">
|
||||||
uploadedFilesLength: uploadedFiles.length,
|
<img
|
||||||
isDesignMode: isDesignMode,
|
src={representativeImageUrl}
|
||||||
shouldShow: shouldShow,
|
alt={representativeFile.realFileName}
|
||||||
uploadedFiles: uploadedFiles.map((f) => ({ objid: f.objid, name: f.realFileName })),
|
className="h-full w-full object-contain"
|
||||||
"🚨 렌더링 여부": 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
|
|
||||||
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>
|
||||||
</div>
|
) : isImage && !representativeImageUrl ? (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
{uploadedFiles.length > 0 ? (
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mb-2"></div>
|
||||||
<div className="space-y-1">
|
<p className="text-sm text-muted-foreground">이미지 로딩 중...</p>
|
||||||
{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>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
className="flex flex-col items-center justify-center py-8 text-gray-500"
|
{getFileIcon(representativeFile.fileExt)}
|
||||||
style={{ textShadow: "none" }}
|
<p className="mt-3 text-sm font-medium text-center px-4">
|
||||||
>
|
{representativeFile.realFileName}
|
||||||
<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>
|
</p>
|
||||||
|
<Badge variant="secondary" className="mt-2">
|
||||||
|
대표 파일
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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 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>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="mt-4 h-8 px-3 text-xs"
|
||||||
|
onClick={() => setIsFileManagerOpen(true)}
|
||||||
|
>
|
||||||
|
파일 업로드
|
||||||
|
</Button>
|
||||||
</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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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; // 대표 이미지로 설정 여부
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue