데이터 테이블 첨부파일 연계

This commit is contained in:
kjs
2025-09-06 00:16:27 +09:00
parent d73be8a4d3
commit 0b38f349aa
10 changed files with 1015 additions and 166 deletions

View File

@@ -35,24 +35,42 @@ import {
ZoomIn,
ZoomOut,
RotateCw,
Folder,
FolderOpen,
} from "lucide-react";
import { tableTypeApi } from "@/lib/api/screen";
import { getCurrentUser, UserInfo } from "@/lib/api/client";
import { DataTableComponent, DataTableColumn, DataTableFilter } from "@/types/screen";
import { DataTableComponent, DataTableColumn, DataTableFilter, AttachedFileInfo } from "@/types/screen";
import { cn } from "@/lib/utils";
import { downloadFile } from "@/lib/api/file";
import { downloadFile, getLinkedFiles } from "@/lib/api/file";
import { toast } from "sonner";
import { FileUpload } from "@/components/screen/widgets/FileUpload";
// 파일 데이터 타입 정의
// 파일 데이터 타입 정의 (AttachedFileInfo와 호환)
interface FileInfo {
id: string;
name: string;
size: number;
type: string;
extension: string;
uploadedAt: string;
lastModified: string;
serverFilename?: string; // 서버에 저장된 파일명 (다운로드용)
// AttachedFileInfo 기본 속성들
objid: string;
savedFileName: string;
realFileName: string;
fileSize: number;
fileExt: string;
filePath: string;
docType: string;
docTypeName: string;
targetObjid: string;
parentTargetObjid?: string;
companyCode: string;
writer: string;
regdate: string;
status: string;
// 추가 호환성 속성들
path?: string; // filePath와 동일
name?: string; // realFileName과 동일
id?: string; // objid와 동일
size?: number; // fileSize와 동일
type?: string; // docType과 동일
uploadedAt?: string; // regdate와 동일
}
interface FileColumnData {
@@ -94,6 +112,112 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
const [zoom, setZoom] = useState(1);
const [rotation, setRotation] = useState(0);
// 파일 관리 상태
const [fileStatusMap, setFileStatusMap] = useState<Record<string, { hasFiles: boolean; fileCount: number }>>({}); // 행별 파일 상태
const [showFileManagementModal, setShowFileManagementModal] = useState(false);
const [selectedRowForFiles, setSelectedRowForFiles] = useState<Record<string, any> | null>(null);
const [selectedColumnForFiles, setSelectedColumnForFiles] = useState<DataTableColumn | null>(null); // 선택된 컬럼 정보
const [linkedFiles, setLinkedFiles] = useState<any[]>([]);
// 파일 상태 확인 함수
const checkFileStatus = useCallback(
async (rowData: Record<string, any>) => {
if (!component.tableName) return;
// 첫 번째 컬럼을 기본키로 사용 (실제로는 더 정교한 로직 필요)
const primaryKeyField = Object.keys(rowData)[0]; // 임시로 첫 번째 컬럼 사용
const recordId = rowData[primaryKeyField];
if (!recordId) return;
try {
const response = await getLinkedFiles(component.tableName, recordId);
const hasFiles = response.files && response.files.length > 0;
const fileCount = response.files ? response.files.length : 0;
return { hasFiles, fileCount, files: response.files || [] };
} catch (error) {
console.error("파일 상태 확인 오류:", error);
return { hasFiles: false, fileCount: 0, files: [] };
}
},
[component.tableName],
);
// 파일 폴더 아이콘 클릭 핸들러 (전체 행 파일 관리)
const handleFileIconClick = useCallback(
async (rowData: Record<string, any>) => {
const fileStatus = await checkFileStatus(rowData);
if (fileStatus) {
setSelectedRowForFiles(rowData);
setLinkedFiles(fileStatus.files);
setShowFileManagementModal(true);
}
},
[checkFileStatus],
);
// 컬럼별 파일 상태 확인
const checkColumnFileStatus = useCallback(
async (rowData: Record<string, any>, column: DataTableColumn) => {
if (!component.tableName) return null;
const primaryKeyField = Object.keys(rowData)[0];
const recordId = rowData[primaryKeyField];
if (!recordId) return null;
try {
// 가상 파일 컬럼의 경우: tableName:recordId:columnName 형태로 target_objid 생성
const targetObjid = column.isVirtualFileColumn
? `${component.tableName}:${recordId}:${column.columnName}`
: `${component.tableName}:${recordId}`;
const response = await getLinkedFiles(component.tableName, recordId);
// 가상 파일 컬럼의 경우 해당 컬럼의 파일만 필터링
let files = response.files || [];
if (column.isVirtualFileColumn) {
// 현재 컬럼명으로 먼저 시도
files = files.filter(
(file: any) => file.targetObjid === targetObjid || file.targetObjid?.endsWith(`:${column.columnName}`), // target_objid → targetObjid
);
// 파일이 없는 경우 fallback: 모든 파일 컬럼 패턴 시도
if (files.length === 0) {
// 해당 테이블:레코드의 모든 파일 컬럼 파일들을 가져옴
files = (response.files || []).filter(
(file: any) => file.targetObjid?.startsWith(`${component.tableName}:${recordId}:file_column_`), // target_objid → targetObjid
);
}
}
const hasFiles = files.length > 0;
const fileCount = files.length;
return { hasFiles, fileCount, files, targetObjid };
} catch (error) {
console.error("컬럼별 파일 상태 확인 오류:", error);
return { hasFiles: false, fileCount: 0, files: [], targetObjid: null };
}
},
[component.tableName],
);
// 컬럼별 파일 클릭 핸들러
const handleColumnFileClick = useCallback(
async (rowData: Record<string, any>, column: DataTableColumn) => {
// 컬럼별 파일 상태 확인
const fileStatus = await checkColumnFileStatus(rowData, column);
setSelectedRowForFiles(rowData);
setSelectedColumnForFiles(column); // 선택된 컬럼 정보 저장
setLinkedFiles(fileStatus?.files || []);
setShowFileManagementModal(true);
// TODO: 모달에 컬럼 정보 전달하여 해당 컬럼 전용 파일 업로드 가능하게 하기
},
[checkColumnFileStatus],
);
// 이미지 미리보기 핸들러들
const handlePreviewImage = useCallback((fileInfo: FileInfo) => {
setPreviewImage(fileInfo);
@@ -200,25 +324,92 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
setLoading(true);
try {
console.log("🔍 테이블 데이터 조회:", {
tableName: component.tableName,
page,
pageSize,
searchParams,
});
const result = await tableTypeApi.getTableData(component.tableName, {
page,
size: pageSize,
search: searchParams,
});
console.log("✅ 테이블 데이터 조회 결과:", result);
setData(result.data);
setTotal(result.total);
setTotalPages(result.totalPages);
setCurrentPage(result.page);
// 각 행의 파일 상태 확인 (전체 행 + 가상 파일 컬럼별)
const fileStatusPromises = result.data.map(async (rowData: Record<string, any>) => {
const primaryKeyField = Object.keys(rowData)[0];
const recordId = rowData[primaryKeyField];
if (!recordId) return { rowKey: recordId, statuses: {} };
try {
const fileResponse = await getLinkedFiles(component.tableName, recordId);
const allFiles = fileResponse.files || [];
// 전체 행에 대한 파일 상태
const rowStatus = {
hasFiles: allFiles.length > 0,
fileCount: allFiles.length,
};
// 가상 파일 컬럼별 파일 상태
const columnStatuses: Record<string, { hasFiles: boolean; fileCount: number }> = {};
// 가상 파일 컬럼 찾기
const virtualFileColumns = component.columns.filter((col) => col.isVirtualFileColumn);
virtualFileColumns.forEach((column) => {
// 해당 컬럼의 파일만 필터링 (targetObjid로 수정)
let columnFiles = allFiles.filter((file: any) => file.targetObjid?.endsWith(`:${column.columnName}`));
// fallback: 컬럼명으로 찾지 못한 경우 모든 파일 컬럼 파일 포함
if (columnFiles.length === 0) {
columnFiles = allFiles.filter((file: any) =>
file.targetObjid?.startsWith(`${component.tableName}:${recordId}:file_column_`),
);
}
const columnKey = `${recordId}_${column.columnName}`;
columnStatuses[columnKey] = {
hasFiles: columnFiles.length > 0,
fileCount: columnFiles.length,
};
});
return {
rowKey: recordId,
statuses: {
[recordId]: rowStatus, // 전체 행 상태
...columnStatuses, // 컬럼별 상태
},
};
} catch {
// 에러 시 기본값
const defaultStatuses: Record<string, { hasFiles: boolean; fileCount: number }> = {
[recordId]: { hasFiles: false, fileCount: 0 },
};
// 가상 파일 컬럼에 대해서도 기본값 설정
const virtualFileColumns = component.columns.filter((col) => col.isVirtualFileColumn);
virtualFileColumns.forEach((column) => {
const columnKey = `${recordId}_${column.columnName}`;
defaultStatuses[columnKey] = { hasFiles: false, fileCount: 0 };
});
return { rowKey: recordId, statuses: defaultStatuses };
}
});
// 파일 상태 업데이트
Promise.all(fileStatusPromises).then((results) => {
const statusMap: Record<string, { hasFiles: boolean; fileCount: number }> = {};
results.forEach((result) => {
Object.assign(statusMap, result.statuses);
});
setFileStatusMap(statusMap);
});
} catch (error) {
console.error("❌ 테이블 데이터 조회 실패:", error);
setData([]);
@@ -251,10 +442,8 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
useEffect(() => {
const fetchTableColumns = async () => {
try {
console.log("🔄 테이블 컬럼 정보 로드 시작:", component.tableName);
const columns = await tableTypeApi.getColumns(component.tableName);
setTableColumns(columns);
console.log("✅ 테이블 컬럼 정보 로드 완료:", columns);
} catch (error) {
console.error("테이블 컬럼 정보 로드 실패:", error);
}
@@ -272,7 +461,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
// 검색 실행
const handleSearch = useCallback(() => {
console.log("🔍 검색 실행:", searchValues);
loadData(1, searchValues);
}, [searchValues, loadData]);
@@ -512,8 +700,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
} else {
handleAddFormChange(columnName, fileNames);
}
console.log("✅ 파일 업로드 완료:", validFiles);
} catch (error) {
console.error("파일 업로드 실패:", error);
alert("파일 업로드에 실패했습니다.");
@@ -1280,25 +1466,15 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
}
};
// 파일 모달 열기
const openFileModal = (fileData: FileColumnData, column: DataTableColumn) => {
setCurrentFileData(fileData);
setCurrentFileColumn(column);
setShowFileModal(true);
};
// 파일 다운로드
const handleDownloadFile = useCallback(async (fileInfo: FileInfo) => {
try {
console.log("📥 파일 다운로드 시작:", fileInfo);
// serverFilename이 없는 경우 파일 경로에서 추출 시도
const serverFilename = fileInfo.serverFilename || (fileInfo.path ? fileInfo.path.split("/").pop() : null);
// savedFileName이 없는 경우 파일 경로에서 추출 시도
const serverFilename = fileInfo.savedFileName || (fileInfo.path ? fileInfo.path.split("/").pop() : null);
if (!serverFilename) {
// _file 속성이 있는 경우 로컬 파일로 다운로드
if ((fileInfo as any)._file) {
console.log("📁 로컬 파일 다운로드 시도:", fileInfo.name);
try {
const file = (fileInfo as any)._file;
@@ -1309,12 +1485,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
return;
}
console.log("📁 유효한 파일 객체 확인됨:", {
name: file.name || fileInfo.name,
size: file.size,
type: file.type,
});
const url = URL.createObjectURL(file);
const link = document.createElement("a");
link.href = url;
@@ -1352,108 +1522,50 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
}, []);
// 셀 값 포맷팅
const formatCellValue = (value: any, column: DataTableColumn): React.ReactNode => {
if (value === null || value === undefined) return "";
const formatCellValue = (value: any, column: DataTableColumn, rowData?: Record<string, any>): React.ReactNode => {
// 가상 파일 컬럼의 경우 value가 없어도 파일 아이콘을 표시해야 함
if (!column.isVirtualFileColumn && (value === null || value === undefined)) return "";
// 디버깅을 위한 로그 추가
if (column.columnName === "file_path") {
console.log("📊 formatCellValue (file_path 컬럼):", {
columnName: column.columnName,
widgetType: column.widgetType,
value: value,
valueType: typeof value,
fullColumn: column,
});
}
// 파일 타입 컬럼 처리 (가상 파일 컬럼 포함)
const isFileColumn =
column.widgetType === "file" || column.columnName === "file_path" || column.isVirtualFileColumn;
// file_path 컬럼은 강제로 파일 타입으로 처리 (임시 해결책)
const isFileColumn = column.widgetType === "file" || column.columnName === "file_path";
// 파일 타입 컬럼은 파일 아이콘으로 표시 (컬럼별 파일 관리)
if (isFileColumn && rowData) {
// 현재 행의 기본키 값 가져오기
const primaryKeyField = Object.keys(rowData)[0];
const recordId = rowData[primaryKeyField];
// file_path 컬럼도 파일 타입으로 처리
if (isFileColumn) {
console.log("🗂️ 파일 타입 컬럼 처리 중:", value);
if (value) {
try {
let fileData;
// 해당 컬럼에 대한 파일 상태 확인
const columnFileKey = `${recordId}_${column.columnName}`;
const columnFileStatus = fileStatusMap[columnFileKey];
const hasFiles = columnFileStatus?.hasFiles || false;
const fileCount = columnFileStatus?.fileCount || 0;
// 파일 경로 문자열인지 확인 (/uploads/로 시작하는 경우)
if (typeof value === "string" && value.startsWith("/uploads/")) {
// 파일 경로 문자열인 경우 단일 파일로 처리
const fileName = value.split("/").pop() || "파일";
const fileExt = fileName.split(".").pop()?.toLowerCase() || "";
fileData = {
files: [
{
name: fileName.replace(/^\d+_/, ""), // 타임스탬프 제거
path: value,
objid: Date.now().toString(), // 임시 objid
size: 0, // 크기 정보 없음
type:
fileExt === "jpg" || fileExt === "jpeg"
? "image/jpeg"
: fileExt === "png"
? "image/png"
: fileExt === "gif"
? "image/gif"
: fileExt === "pdf"
? "application/pdf"
: "application/octet-stream",
extension: fileExt,
regdate: new Date().toISOString(), // 등록일 추가
writer: "시스템", // 기본 등록자
},
],
totalCount: 1,
totalSize: 0,
regdate: new Date().toISOString(), // 파일 데이터 전체에도 등록일 추가
};
} else {
// JSON 문자열이면 파싱
fileData = typeof value === "string" ? JSON.parse(value) : value;
// regdate가 없는 경우 기본값 설정
if (!fileData.regdate) {
fileData.regdate = new Date().toISOString();
}
// 개별 파일들에도 regdate와 writer가 없는 경우 추가
if (fileData.files && Array.isArray(fileData.files)) {
fileData.files.forEach((file: any) => {
if (!file.regdate) {
file.regdate = new Date().toISOString();
}
if (!file.writer) {
file.writer = "시스템";
}
});
}
}
console.log("📁 파싱된 파일 데이터:", fileData);
if (fileData?.files && Array.isArray(fileData.files) && fileData.files.length > 0) {
return (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
className="h-8 px-2 text-blue-600 hover:bg-blue-50 hover:text-blue-800"
onClick={() => openFileModal(fileData, column)}
>
<File className="mr-1 h-4 w-4" />
{fileData.totalCount === 1 ? "파일 1개" : `파일 ${fileData.totalCount}`}
</Button>
<Badge variant="secondary" className="text-xs">
{(fileData.totalSize / 1024 / 1024).toFixed(1)}MB
</Badge>
return (
<div className="flex justify-center">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 hover:bg-blue-50"
onClick={() => handleColumnFileClick(rowData, column)}
title={hasFiles ? `${fileCount}개 파일 보기` : "파일 업로드"}
>
{hasFiles ? (
<div className="relative">
<FolderOpen className="h-4 w-4 text-blue-600" />
{fileCount > 0 && (
<div className="absolute -top-1 -right-1 flex h-3 w-3 items-center justify-center rounded-full bg-blue-600 text-[10px] text-white">
{fileCount > 9 ? "9+" : fileCount}
</div>
)}
</div>
);
}
} catch (error) {
console.warn("파일 데이터 파싱 오류:", error);
}
}
return <span className="text-sm text-gray-400 italic"> </span>;
) : (
<Folder className="h-4 w-4 text-gray-400" />
)}
</Button>
</div>
);
}
switch (column.widgetType) {
@@ -1614,13 +1726,26 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
{column.label}
</TableHead>
))}
{/* 기본 파일 컬럼은 가상 파일 컬럼이 있으면 완전히 숨김 */}
{!visibleColumns.some((col) => col.widgetType === "file") && (
<TableHead className="w-16 px-4 text-center">
<div className="flex items-center justify-center gap-1">
<Folder className="h-4 w-4" />
<span className="text-xs"></span>
</div>
</TableHead>
)}
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell
colSpan={visibleColumns.length + (component.enableDelete ? 1 : 0)}
colSpan={
visibleColumns.length +
(component.enableDelete ? 1 : 0) +
(!visibleColumns.some((col) => col.widgetType === "file") ? 1 : 0)
}
className="h-32 text-center"
>
<div className="text-muted-foreground flex items-center justify-center gap-2">
@@ -1643,15 +1768,54 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
)}
{visibleColumns.map((column: DataTableColumn) => (
<TableCell key={column.id} className="px-4 font-mono text-sm">
{formatCellValue(row[column.columnName], column)}
{formatCellValue(row[column.columnName], column, row)}
</TableCell>
))}
{/* 기본 파일 셀은 가상 파일 컬럼이 있으면 완전히 숨김 */}
{!visibleColumns.some((col) => col.widgetType === "file") && (
<TableCell className="w-16 px-4 text-center">
{(() => {
const primaryKeyField = Object.keys(row)[0];
const recordId = row[primaryKeyField];
const fileStatus = fileStatusMap[recordId];
const hasFiles = fileStatus?.hasFiles || false;
const fileCount = fileStatus?.fileCount || 0;
return (
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 hover:bg-blue-50"
onClick={() => handleFileIconClick(row)}
title={hasFiles ? `${fileCount}개 파일 보기` : "파일 업로드"}
>
{hasFiles ? (
<div className="relative">
<FolderOpen className="h-4 w-4 text-blue-600" />
{fileCount > 0 && (
<div className="absolute -top-1 -right-1 flex h-3 w-3 items-center justify-center rounded-full bg-blue-600 text-[10px] text-white">
{fileCount > 9 ? "9+" : fileCount}
</div>
)}
</div>
) : (
<Folder className="h-4 w-4 text-gray-400" />
)}
</Button>
);
})()}
</TableCell>
)}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={visibleColumns.length + (component.enableDelete ? 1 : 0)}
colSpan={
visibleColumns.length +
(component.enableDelete ? 1 : 0) +
(!visibleColumns.some((col) => col.widgetType === "file") ? 1 : 0)
}
className="h-32 text-center"
>
<div className="text-muted-foreground flex flex-col items-center gap-2">
@@ -2032,7 +2196,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
<div className="flex flex-1 items-center justify-center overflow-auto rounded-lg bg-gray-50 p-4">
{previewImage && (
<img
src={`${process.env.NEXT_PUBLIC_API_URL}/files/preview/${previewImage.id}?serverFilename=${previewImage.serverFilename}`}
src={`${process.env.NEXT_PUBLIC_API_URL}/files/preview/${previewImage.id}?serverFilename=${previewImage.savedFileName}`}
alt={previewImage.name}
className="max-h-full max-w-full object-contain transition-transform duration-200"
style={{
@@ -2055,6 +2219,205 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
)}
</DialogContent>
</Dialog>
{/* 파일 관리 모달 */}
<Dialog open={showFileManagementModal} onOpenChange={setShowFileManagementModal}>
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Folder className="h-5 w-5" />
{selectedRowForFiles && (
<Badge variant="outline" className="ml-2">
{Object.keys(selectedRowForFiles)[0]}: {selectedRowForFiles[Object.keys(selectedRowForFiles)[0]]}
</Badge>
)}
</DialogTitle>
<DialogDescription>
{linkedFiles.length > 0
? `${linkedFiles.length}개의 파일이 연결되어 있습니다.`
: "연결된 파일이 없습니다. 새 파일을 업로드하세요."}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 기존 파일 목록 */}
{linkedFiles.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-900"> </h4>
{linkedFiles.map((file: any, index: number) => (
<div key={index} className="flex items-center justify-between rounded-lg border p-3">
<div className="flex items-center space-x-3">
<File className="h-5 w-5 text-blue-600" />
<div>
<div className="font-medium">{file.realFileName}</div>
<div className="text-sm text-gray-500">
{(Number(file.fileSize) / 1024 / 1024).toFixed(2)} MB {file.docTypeName}
{file.regdate && <span> {new Date(file.regdate).toLocaleString("ko-KR")}</span>}
{file.writer && <span> {file.writer}</span>}
</div>
</div>
</div>
<div className="flex items-center space-x-2">
{file.fileExt && ["jpg", "jpeg", "png", "gif"].includes(file.fileExt.toLowerCase()) && (
<Button
size="sm"
variant="outline"
onClick={() => {
// 이미지 미리보기 (기존 로직 재사용)
const fileInfo: FileInfo = {
id: file.objid,
name: file.realFileName,
size: Number(file.fileSize),
type: `image/${file.fileExt}`,
path: file.filePath,
objid: file.objid,
extension: file.fileExt,
uploadedAt: file.regdate || new Date().toISOString(),
lastModified: file.regdate || new Date().toISOString(),
};
handlePreviewImage(fileInfo);
}}
>
<Eye className="h-4 w-4" />
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={() => {
// 파일 다운로드 (기존 로직 재사용)
const fileInfo: FileInfo = {
id: file.objid,
name: file.realFileName,
size: Number(file.fileSize),
type: `application/${file.fileExt}`,
path: file.filePath,
objid: file.objid,
savedFileName: file.savedFileName,
};
handleDownloadFile(fileInfo);
}}
>
<Download className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
)}
{/* 파일 업로드 섹션 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-gray-900">{selectedColumnForFiles?.label || "파일"} </h4>
{selectedColumnForFiles?.isVirtualFileColumn && (
<Badge variant="secondary" className="text-xs">
{selectedColumnForFiles.fileColumnConfig?.docTypeName || "문서"}
</Badge>
)}
</div>
{selectedRowForFiles && selectedColumnForFiles && component.tableName && (
<div className="rounded-lg border p-4">
<FileUpload
component={{
id: `modal-file-upload-${selectedColumnForFiles.id}`,
type: "file",
position: { x: 0, y: 0 },
size: { width: 400, height: 300 },
uploadedFiles: [], // 빈 배열로 초기화
fileConfig: {
maxSize: selectedColumnForFiles.fileColumnConfig?.maxFiles || 10,
maxFiles: selectedColumnForFiles.fileColumnConfig?.maxFiles || 5,
multiple: true,
showPreview: true,
showProgress: true,
autoUpload: true, // 자동 업로드 활성화
chunkedUpload: false, // 기본 업로드 방식
dragDropText: `${selectedColumnForFiles.label} 파일을 드래그하여 업로드하거나 클릭하세요`,
uploadButtonText: "파일 업로드", // 업로드 버튼 텍스트
accept: selectedColumnForFiles.fileColumnConfig?.accept || ["*/*"],
// 문서 분류 설정
docType: selectedColumnForFiles.fileColumnConfig?.docType || "DOCUMENT",
docTypeName: selectedColumnForFiles.fileColumnConfig?.docTypeName || "일반 문서",
// 자동 연결 설정
autoLink: true,
linkedTable: component.tableName,
linkedField: Object.keys(selectedRowForFiles)[0], // 기본키 필드
recordId: selectedRowForFiles[Object.keys(selectedRowForFiles)[0]], // 기본키 값
// 가상 파일 컬럼별 구분을 위한 추가 정보
columnName: selectedColumnForFiles.columnName,
isVirtualFileColumn: selectedColumnForFiles.isVirtualFileColumn,
},
}}
onUpdateComponent={() => {
// 모달에서는 컴포넌트 업데이트가 필요 없으므로 빈 함수 제공
}}
onFileUpload={async () => {
// 파일 업로드 완료 후 연결된 파일 목록 새로고침
if (selectedRowForFiles && selectedColumnForFiles) {
const result = await checkColumnFileStatus(selectedRowForFiles, selectedColumnForFiles);
if (result) {
setLinkedFiles(result.files);
// 파일 상태 맵도 업데이트
const primaryKeyField = Object.keys(selectedRowForFiles)[0];
const recordId = selectedRowForFiles[primaryKeyField];
const columnFileKey = `${recordId}_${selectedColumnForFiles.columnName}`;
setFileStatusMap((prev) => {
const newFileStatusMap = {
...prev,
[columnFileKey]: {
hasFiles: result.hasFiles,
fileCount: result.fileCount,
},
};
return newFileStatusMap;
});
// 전체 테이블의 해당 컬럼 파일 상태도 강제 새로고침
setTimeout(() => {
// 테이블 데이터 새로고침을 위해 loadData 호출
if (data && data.length > 0) {
// 현재 데이터를 그대로 사용하되 파일 상태만 새로고침
const refreshPromises = data.map(async (row) => {
const pk = Object.keys(row)[0];
const rowId = row[pk];
const fileKey = `${rowId}_${selectedColumnForFiles.columnName}`;
const columnStatus = await checkColumnFileStatus(row, selectedColumnForFiles);
if (columnStatus) {
setFileStatusMap((prev) => ({
...prev,
[fileKey]: {
hasFiles: columnStatus.hasFiles,
fileCount: columnStatus.fileCount,
},
}));
}
});
Promise.all(refreshPromises);
}
}, 100);
}
}
}}
/>
</div>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowFileManagementModal(false)}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
);
};