ERP-node/frontend/components/v2/V2Media.tsx

925 lines
33 KiB
TypeScript
Raw Permalink Normal View History

2025-12-19 15:44:38 +09:00
"use client";
/**
* V2Media
2025-12-19 15:44:38 +09:00
*
* ( FileUploadComponent )
2025-12-19 15:44:38 +09:00
* - file: 파일
* - image: 이미지 /
* - video: 비디오
* - audio: 오디오
*
* :
* - FileViewerModal / FileManagerModal ()
* -
* - (/ )
* -
* - /
* - DB에서
2025-12-19 15:44:38 +09:00
*/
import React, { forwardRef, useCallback, useRef, useState, useEffect } from "react";
2025-12-19 15:44:38 +09:00
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
2025-12-19 15:44:38 +09:00
import { cn } from "@/lib/utils";
import { V2MediaProps } from "@/types/v2-components";
import {
Upload, X, File, Image as ImageIcon, Video, Music, Eye, Download, Trash2, Plus,
FileText, Archive, Presentation, FileImage, FileVideo, FileAudio
} from "lucide-react";
import { apiClient } from "@/lib/api/client";
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 { useAuth } from "@/hooks/useAuth";
2025-12-19 15:44:38 +09:00
// 레거시 모달 컴포넌트 import
import { FileViewerModal } from "@/lib/registry/components/file-upload/FileViewerModal";
import { FileManagerModal } from "@/lib/registry/components/file-upload/FileManagerModal";
import type { FileInfo, FileUploadConfig } from "@/lib/registry/components/file-upload/types";
2025-12-19 15:44:38 +09:00
/**
*
2025-12-19 15:44:38 +09:00
*/
const getFileIcon = (extension: string) => {
const ext = extension.toLowerCase().replace(".", "");
if (["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(ext)) {
return <FileImage className="h-6 w-6 text-blue-500" />;
}
if (["mp4", "avi", "mov", "wmv", "flv", "webm"].includes(ext)) {
return <FileVideo className="h-6 w-6 text-purple-500" />;
}
if (["mp3", "wav", "flac", "aac", "ogg"].includes(ext)) {
return <FileAudio className="h-6 w-6 text-green-500" />;
}
if (["pdf"].includes(ext)) {
return <FileText className="h-6 w-6 text-red-500" />;
}
if (["doc", "docx", "hwp", "hwpx", "pages"].includes(ext)) {
return <FileText className="h-6 w-6 text-blue-600" />;
}
if (["xls", "xlsx", "hcell", "numbers"].includes(ext)) {
return <FileText className="h-6 w-6 text-green-600" />;
}
if (["ppt", "pptx", "hanshow", "keynote"].includes(ext)) {
return <Presentation className="h-6 w-6 text-orange-500" />;
}
if (["zip", "rar", "7z", "tar", "gz"].includes(ext)) {
return <Archive className="h-6 w-6 text-gray-500" />;
}
return <File className="h-6 w-6 text-gray-400" />;
};
2025-12-19 15:44:38 +09:00
/**
* V2 ( )
2025-12-19 15:44:38 +09:00
*/
export const V2Media = forwardRef<HTMLDivElement, V2MediaProps>(
(props, ref) => {
const {
id,
label,
required,
readonly,
disabled,
style,
size,
config: configProp,
value,
onChange,
formData,
columnName,
tableName,
onFormDataChange,
isDesignMode = false,
isInteractive = true,
onUpdate,
...restProps
} = props;
// 인증 정보
const { user } = useAuth();
// config 기본값
const config = configProp || { type: "file" as const };
const mediaType = config.type || "file";
// 파일 상태
const [uploadedFiles, setUploadedFiles] = useState<FileInfo[]>([]);
const [uploadStatus, setUploadStatus] = useState<"idle" | "uploading" | "success" | "error">("idle");
const [dragOver, setDragOver] = useState(false);
const [representativeImageUrl, setRepresentativeImageUrl] = useState<string | null>(null);
// 모달 상태
const [viewerFile, setViewerFile] = useState<FileInfo | null>(null);
const [isViewerOpen, setIsViewerOpen] = useState(false);
const [isFileManagerOpen, setIsFileManagerOpen] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// 레코드 모드 판단
const isRecordMode = !!(formData?.id && !String(formData.id).startsWith('temp_'));
const recordTableName = formData?.tableName || tableName;
const recordId = formData?.id;
// 🔑 columnName 우선 사용 (실제 DB 컬럼명), 없으면 id, 최후에 attachments
const effectiveColumnName = columnName || id || 'attachments';
// 레코드용 targetObjid 생성
const getRecordTargetObjid = useCallback(() => {
if (isRecordMode && recordTableName && recordId) {
return `${recordTableName}:${recordId}:${effectiveColumnName}`;
2025-12-19 15:44:38 +09:00
}
return null;
}, [isRecordMode, recordTableName, recordId, effectiveColumnName]);
2025-12-19 15:44:38 +09:00
// 레코드별 고유 키 생성
const getUniqueKey = useCallback(() => {
if (isRecordMode && recordTableName && recordId) {
return `v2media_${recordTableName}_${recordId}_${id}`;
}
return `v2media_${id}`;
}, [isRecordMode, recordTableName, recordId, id]);
// 레코드 ID 변경 시 파일 목록 초기화
const prevRecordIdRef = useRef<any>(null);
useEffect(() => {
if (prevRecordIdRef.current !== recordId) {
prevRecordIdRef.current = recordId;
if (isRecordMode) {
setUploadedFiles([]);
}
}
}, [recordId, isRecordMode]);
// 컴포넌트 마운트 시 localStorage에서 파일 복원
useEffect(() => {
if (!id) return;
try {
const backupKey = getUniqueKey();
const backupFiles = localStorage.getItem(backupKey);
if (backupFiles) {
const parsedFiles = JSON.parse(backupFiles);
if (parsedFiles.length > 0) {
setUploadedFiles(parsedFiles);
if (typeof window !== "undefined") {
(window as any).globalFileState = {
...(window as any).globalFileState,
[backupKey]: parsedFiles,
};
}
}
}
} catch (e) {
console.warn("파일 복원 실패:", e);
}
}, [id, getUniqueKey, recordId]);
2025-12-19 15:44:38 +09:00
// DB에서 파일 목록 로드
const loadComponentFiles = useCallback(async () => {
if (!id) return false;
2025-12-19 15:44:38 +09:00
try {
let screenId = formData?.screenId;
if (!screenId && typeof window !== "undefined") {
const pathname = window.location.pathname;
const screenMatch = pathname.match(/\/screens\/(\d+)/);
if (screenMatch) {
screenId = parseInt(screenMatch[1]);
}
}
2025-12-19 15:44:38 +09:00
if (!screenId && isDesignMode) {
screenId = 999999;
}
2025-12-19 15:44:38 +09:00
if (!screenId) {
screenId = 0;
2025-12-19 15:44:38 +09:00
}
const params = {
screenId,
componentId: id,
tableName: recordTableName || formData?.tableName || tableName,
recordId: recordId || formData?.id,
columnName: effectiveColumnName,
};
const response = await getComponentFiles(params);
if (response.success) {
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(),
targetObjid: file.targetObjid || file.target_objid,
filePath: file.filePath || file.file_path,
...file,
}));
// localStorage와 병합
let finalFiles = formattedFiles;
const uniqueKey = getUniqueKey();
try {
const backupFiles = localStorage.getItem(uniqueKey);
if (backupFiles) {
const parsedBackupFiles = JSON.parse(backupFiles);
const serverObjIds = new Set(formattedFiles.map((f: any) => f.objid));
const additionalFiles = parsedBackupFiles.filter((f: any) => !serverObjIds.has(f.objid));
finalFiles = [...formattedFiles, ...additionalFiles];
}
} catch (e) {
console.warn("파일 병합 오류:", e);
}
setUploadedFiles(finalFiles);
if (typeof window !== "undefined") {
(window as any).globalFileState = {
...(window as any).globalFileState,
[uniqueKey]: finalFiles,
};
GlobalFileManager.registerFiles(finalFiles, {
uploadPage: window.location.pathname,
componentId: id,
screenId: formData?.screenId,
recordId: recordId,
});
try {
localStorage.setItem(uniqueKey, JSON.stringify(finalFiles));
} catch (e) {
console.warn("localStorage 백업 실패:", e);
}
}
return true;
}
} catch (error) {
console.error("파일 조회 오류:", error);
2025-12-19 15:44:38 +09:00
}
return false;
}, [id, tableName, columnName, formData?.screenId, formData?.tableName, formData?.id, getUniqueKey, recordId, isRecordMode, recordTableName, effectiveColumnName, isDesignMode]);
// 파일 동기화
useEffect(() => {
loadComponentFiles();
}, [loadComponentFiles]);
// 전역 상태 변경 감지
useEffect(() => {
const handleGlobalFileStateChange = (event: CustomEvent) => {
const { componentId, files, isRestore } = event.detail;
if (componentId === id) {
setUploadedFiles(files);
try {
const backupKey = getUniqueKey();
localStorage.setItem(backupKey, JSON.stringify(files));
} catch (e) {
console.warn("localStorage 백업 실패:", e);
}
}
};
2025-12-19 15:44:38 +09:00
if (typeof window !== "undefined") {
window.addEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener);
return () => {
window.removeEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener);
};
}
}, [id, getUniqueKey]);
// 파일 업로드 처리
const handleFileUpload = useCallback(
async (files: File[]) => {
if (!files.length) return;
// 중복 체크
const existingFileNames = uploadedFiles.map((f) => f.realFileName.toLowerCase());
const duplicates: string[] = [];
const uniqueFiles: File[] = [];
files.forEach((file) => {
const fileName = file.name.toLowerCase();
if (existingFileNames.includes(fileName)) {
duplicates.push(file.name);
} else {
uniqueFiles.push(file);
}
});
2025-12-19 15:44:38 +09:00
if (duplicates.length > 0) {
toast.error(`중복된 파일: ${duplicates.join(", ")}`);
if (uniqueFiles.length === 0) return;
toast.info(`${uniqueFiles.length}개의 새로운 파일만 업로드합니다.`);
2025-12-19 15:44:38 +09:00
}
const filesToUpload = uniqueFiles.length > 0 ? uniqueFiles : files;
setUploadStatus("uploading");
toast.loading("파일 업로드 중...", { id: "file-upload" });
try {
const effectiveTableName = recordTableName || formData?.tableName || tableName || "default_table";
const effectiveRecordId = recordId || formData?.id;
let screenId = formData?.screenId;
if (!screenId && typeof window !== "undefined") {
const pathname = window.location.pathname;
const screenMatch = pathname.match(/\/screens\/(\d+)/);
if (screenMatch) {
screenId = parseInt(screenMatch[1]);
}
}
let targetObjid;
const effectiveIsRecordMode = isRecordMode || (effectiveRecordId && !String(effectiveRecordId).startsWith('temp_'));
if (effectiveIsRecordMode && effectiveTableName && effectiveRecordId) {
targetObjid = `${effectiveTableName}:${effectiveRecordId}:${effectiveColumnName}`;
} else if (screenId) {
targetObjid = `screen_files:${screenId}:${id}:${effectiveColumnName}`;
} else {
targetObjid = `temp_${id}`;
}
const userCompanyCode = user?.companyCode || (window as any).__user__?.companyCode;
const finalLinkedTable = effectiveIsRecordMode
? effectiveTableName
: (formData?.linkedTable || effectiveTableName);
const uploadData = {
autoLink: formData?.autoLink || true,
linkedTable: finalLinkedTable,
recordId: effectiveRecordId || `temp_${id}`,
columnName: effectiveColumnName,
isVirtualFileColumn: formData?.isVirtualFileColumn || true,
docType: config?.docType || "DOCUMENT",
docTypeName: config?.docTypeName || "일반 문서",
companyCode: userCompanyCode,
tableName: effectiveTableName,
fieldName: effectiveColumnName,
targetObjid: targetObjid,
isRecordMode: effectiveIsRecordMode,
};
const response = await uploadFiles({
files: filesToUpload,
...uploadData,
});
if (response.success) {
const fileData = response.files || (response as any).data || [];
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,
}));
const updatedFiles = [...uploadedFiles, ...newFiles];
setUploadedFiles(updatedFiles);
setUploadStatus("success");
// localStorage 백업
try {
const backupKey = getUniqueKey();
localStorage.setItem(backupKey, JSON.stringify(updatedFiles));
} catch (e) {
console.warn("localStorage 백업 실패:", e);
}
// 전역 상태 업데이트
if (typeof window !== "undefined") {
const globalFileState = (window as any).globalFileState || {};
const uniqueKey = getUniqueKey();
globalFileState[uniqueKey] = updatedFiles;
(window as any).globalFileState = globalFileState;
GlobalFileManager.registerFiles(newFiles, {
uploadPage: window.location.pathname,
componentId: id,
screenId: formData?.screenId,
recordId: recordId,
});
const syncEvent = new CustomEvent("globalFileStateChanged", {
detail: {
componentId: id,
uniqueKey: uniqueKey,
recordId: recordId,
files: updatedFiles,
fileCount: updatedFiles.length,
timestamp: Date.now(),
},
});
window.dispatchEvent(syncEvent);
}
// 부모 컴포넌트 업데이트
if (onUpdate) {
onUpdate({
uploadedFiles: updatedFiles,
lastFileUpdate: Date.now(),
});
}
// onChange 콜백 (objid 배열 또는 단일 값)
const fileIds = updatedFiles.map((f) => f.objid);
const finalValue = config.multiple ? fileIds : fileIds[0] || "";
const targetColumn = columnName || effectiveColumnName;
console.log("📤 [V2Media] 파일 업로드 완료 - 값 전달:", {
columnName: targetColumn,
fileIds,
finalValue,
hasOnChange: !!onChange,
hasOnFormDataChange: !!onFormDataChange,
});
if (onChange) {
onChange(finalValue);
}
// 폼 데이터 업데이트 - 부모 컴포넌트 시그니처에 맞게 (fieldName, value) 형식
if (onFormDataChange && targetColumn) {
// 🔑 단일 파일: 첫 번째 objid만 전달 (DB 컬럼에 저장될 값)
// 복수 파일: 콤마 구분 문자열로 전달
const formValue = config.multiple
? fileIds.join(',')
: (fileIds[0] || '');
console.log("📝 [V2Media] formData 업데이트:", {
columnName: targetColumn,
fileIds,
formValue,
isMultiple: config.multiple,
isRecordMode: effectiveIsRecordMode,
});
// (fieldName: string, value: any) 형식으로 호출
onFormDataChange(targetColumn, formValue);
}
// 그리드 파일 상태 새로고침 이벤트 발생
if (typeof window !== "undefined") {
const refreshEvent = new CustomEvent("refreshFileStatus", {
detail: {
tableName: effectiveTableName,
recordId: effectiveRecordId,
columnName: targetColumn,
targetObjid: targetObjid,
fileCount: updatedFiles.length,
},
});
window.dispatchEvent(refreshEvent);
}
toast.dismiss("file-upload");
toast.success(`${newFiles.length}개 파일 업로드 완료`);
} else {
throw new Error(response.message || (response as any).error || "파일 업로드 실패");
2025-12-19 15:44:38 +09:00
}
} catch (error) {
console.error("파일 업로드 오류:", error);
setUploadStatus("error");
toast.dismiss("file-upload");
toast.error(`업로드 오류: ${error instanceof Error ? error.message : "알 수 없는 오류"}`);
2025-12-19 15:44:38 +09:00
}
},
[config, uploadedFiles, onChange, id, getUniqueKey, recordId, isRecordMode, recordTableName, effectiveColumnName, tableName, onUpdate, onFormDataChange, user, columnName],
);
2025-12-19 15:44:38 +09:00
// 파일 뷰어 열기/닫기
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("파일 다운로드 실패");
2025-12-19 15:44:38 +09:00
}
}, []);
// 파일 삭제
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;
2025-12-19 15:44:38 +09:00
await deleteFile(fileId, serverFilename);
2025-12-19 15:44:38 +09:00
const updatedFiles = uploadedFiles.filter((f) => f.objid !== fileId);
setUploadedFiles(updatedFiles);
2025-12-19 15:44:38 +09:00
// localStorage 백업
try {
const backupKey = getUniqueKey();
localStorage.setItem(backupKey, JSON.stringify(updatedFiles));
} catch (e) {
console.warn("localStorage 백업 실패:", e);
}
2025-12-19 15:44:38 +09:00
// 전역 상태 업데이트
if (typeof window !== "undefined") {
const globalFileState = (window as any).globalFileState || {};
const uniqueKey = getUniqueKey();
globalFileState[uniqueKey] = updatedFiles;
(window as any).globalFileState = globalFileState;
const syncEvent = new CustomEvent("globalFileStateChanged", {
detail: {
componentId: id,
uniqueKey: uniqueKey,
recordId: recordId,
files: updatedFiles,
fileCount: updatedFiles.length,
timestamp: Date.now(),
action: "delete",
},
});
window.dispatchEvent(syncEvent);
}
2025-12-19 15:44:38 +09:00
if (onUpdate) {
onUpdate({
uploadedFiles: updatedFiles,
lastFileUpdate: Date.now(),
});
}
2025-12-19 15:44:38 +09:00
// onChange 콜백
const fileIds = updatedFiles.map((f) => f.objid);
const finalValue = config.multiple ? fileIds : fileIds[0] || "";
const targetColumn = columnName || effectiveColumnName;
2025-12-19 15:44:38 +09:00
console.log("🗑️ [V2Media] 파일 삭제 완료 - 값 전달:", {
columnName: targetColumn,
fileIds,
finalValue,
});
2025-12-19 15:44:38 +09:00
if (onChange) {
onChange(finalValue);
}
2025-12-19 15:44:38 +09:00
// 폼 데이터 업데이트 - 부모 컴포넌트 시그니처에 맞게 (fieldName, value) 형식
if (onFormDataChange && targetColumn) {
// 🔑 단일 파일: 첫 번째 objid만 전달 (DB 컬럼에 저장될 값)
// 복수 파일: 콤마 구분 문자열로 전달
const formValue = config.multiple
? fileIds.join(',')
: (fileIds[0] || '');
console.log("🗑️ [V2Media] 삭제 후 formData 업데이트:", {
columnName: targetColumn,
fileIds,
formValue,
});
// (fieldName: string, value: any) 형식으로 호출
onFormDataChange(targetColumn, formValue);
}
2025-12-19 15:44:38 +09:00
toast.success(`${fileName} 삭제 완료`);
} catch (error) {
console.error("파일 삭제 오류:", error);
toast.error("파일 삭제 실패");
}
},
[uploadedFiles, onUpdate, id, isRecordMode, onFormDataChange, recordTableName, recordId, effectiveColumnName, getUniqueKey, onChange, config.multiple, columnName],
);
// 대표 이미지 로드
const loadRepresentativeImage = useCallback(
async (file: FileInfo) => {
try {
const isImage = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(
file.fileExt.toLowerCase().replace(".", "")
2025-12-19 15:44:38 +09:00
);
if (!isImage) {
setRepresentativeImageUrl(null);
return;
}
if (!file.objid || file.objid === "0" || file.objid === "") {
setRepresentativeImageUrl(null);
return;
}
const response = await apiClient.get(`/files/download/${file.objid}`, {
params: { serverFilename: file.savedFileName },
responseType: "blob",
});
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
if (representativeImageUrl) {
window.URL.revokeObjectURL(representativeImageUrl);
}
setRepresentativeImageUrl(url);
} catch (error) {
console.error("대표 이미지 로드 실패:", error);
setRepresentativeImageUrl(null);
}
},
[representativeImageUrl],
);
// 대표 이미지 설정
const handleSetRepresentative = useCallback(
async (file: FileInfo) => {
try {
const { setRepresentativeFile } = await import("@/lib/api/file");
await setRepresentativeFile(file.objid);
const updatedFiles = uploadedFiles.map((f) => ({
...f,
isRepresentative: f.objid === file.objid,
}));
setUploadedFiles(updatedFiles);
loadRepresentativeImage(file);
} catch (e) {
console.error("대표 파일 설정 실패:", e);
}
},
[uploadedFiles, loadRepresentativeImage]
);
// uploadedFiles 변경 시 대표 이미지 로드
useEffect(() => {
const representativeFile = uploadedFiles.find(f => f.isRepresentative) || uploadedFiles[0];
if (representativeFile) {
loadRepresentativeImage(representativeFile);
} else {
setRepresentativeImageUrl(null);
2025-12-19 15:44:38 +09:00
}
return () => {
if (representativeImageUrl) {
window.URL.revokeObjectURL(representativeImageUrl);
}
};
}, [uploadedFiles]);
// 드래그 앤 드롭 핸들러
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (!readonly && !disabled) {
setDragOver(true);
}
}, [readonly, disabled]);
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 (!readonly && !disabled) {
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
handleFileUpload(files);
}
}
}, [readonly, disabled, handleFileUpload]);
// 파일 선택
const handleFileSelect = useCallback(() => {
if (fileInputRef.current) {
fileInputRef.current.click();
}
}, []);
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []);
if (files.length > 0) {
handleFileUpload(files);
}
e.target.value = '';
}, [handleFileUpload]);
// 파일 설정
const fileConfig: FileUploadConfig = {
accept: config.accept || "*/*",
multiple: config.multiple || false,
maxSize: config.maxSize || 10 * 1024 * 1024,
disabled: disabled,
readonly: readonly,
2025-12-19 15:44:38 +09:00
};
const showLabel = label && style?.labelDisplay !== false;
const componentWidth = size?.width || style?.width;
const componentHeight = size?.height || style?.height;
return (
<div
ref={ref}
id={id}
className="flex w-full flex-col"
style={{ width: componentWidth }}
2025-12-19 15:44:38 +09:00
>
{/* 라벨 */}
2025-12-19 15:44:38 +09:00
{showLabel && (
<Label
htmlFor={id}
style={{
fontSize: style?.labelFontSize,
color: style?.labelColor,
fontWeight: style?.labelFontWeight,
marginBottom: style?.labelMarginBottom,
}}
className="text-sm font-medium shrink-0"
2025-12-19 15:44:38 +09:00
>
{label}
{required && <span className="text-orange-500 ml-0.5">*</span>}
</Label>
)}
{/* 메인 컨테이너 */}
<div
className="min-h-0"
style={{ height: componentHeight }}
>
<div
className="border-border bg-card relative flex h-full w-full flex-col rounded-lg border overflow-hidden"
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{/* 숨겨진 파일 입력 */}
<input
ref={fileInputRef}
type="file"
multiple={config.multiple}
accept={config.accept}
onChange={handleInputChange}
className="hidden"
disabled={disabled || readonly}
/>
{/* 파일이 있는 경우: 대표 이미지/파일 표시 */}
{uploadedFiles.length > 0 ? (() => {
const representativeFile = uploadedFiles.find(f => f.isRepresentative) || uploadedFiles[0];
const isImage = representativeFile && ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(
representativeFile.fileExt.toLowerCase().replace(".", "")
);
return (
<>
{isImage && representativeImageUrl ? (
<div className="relative h-full w-full flex items-center justify-center bg-muted/10">
<img
src={representativeImageUrl}
alt={representativeFile.realFileName}
className="h-full w-full object-contain"
/>
</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 className="flex h-full w-full flex-col items-center justify-center">
{getFileIcon(representativeFile.fileExt)}
<p className="mt-3 text-sm font-medium text-center px-4">
{representativeFile.realFileName}
</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
className={cn(
"flex h-full w-full flex-col items-center justify-center text-muted-foreground cursor-pointer",
dragOver && "border-primary bg-primary/5",
(disabled || readonly) && "opacity-50 cursor-not-allowed"
)}
onClick={() => !disabled && !readonly && handleFileSelect()}
>
<Upload className="mb-3 h-12 w-12" />
<p className="text-sm font-medium"> </p>
<p className="text-xs text-muted-foreground mt-1">
{formatFileSize(config.maxSize || 10 * 1024 * 1024)}
{config.accept && config.accept !== "*/*" && ` (${config.accept})`}
</p>
<Button
variant="outline"
size="sm"
className="mt-4 h-8 px-3 text-xs"
onClick={(e) => {
e.stopPropagation();
setIsFileManagerOpen(true);
}}
disabled={disabled || readonly}
>
</Button>
</div>
)}
</div>
2025-12-19 15:44:38 +09:00
</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}
onSetRepresentative={handleSetRepresentative}
config={fileConfig}
isDesignMode={isDesignMode}
/>
2025-12-19 15:44:38 +09:00
</div>
);
}
);
V2Media.displayName = "V2Media";
2025-12-19 15:44:38 +09:00
export default V2Media;