From 87ce1b74d4ae973614f48253ba6180d83975aeb9 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 8 Sep 2025 10:02:30 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/tableManagementService.ts | 96 +++++++++++++++++-- .../screen/InteractiveDataTable.tsx | 62 ++++++++++++ .../components/screen/widgets/FileUpload.tsx | 23 ++--- 3 files changed, 159 insertions(+), 22 deletions(-) diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index bfb392c6..e7307650 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -551,14 +551,33 @@ export class TableManagementService { for (const fileColumn of fileColumns) { const filePath = row[fileColumn]; if (filePath && typeof filePath === "string") { - // 파일 경로에서 실제 파일 정보 조회 - const fileInfo = await this.getFileInfoByPath(filePath); - if (fileInfo) { + // 🎯 컴포넌트별 파일 정보 조회 + // 파일 경로에서 컴포넌트 ID 추출하거나 컬럼명 사용 + const componentId = + this.extractComponentIdFromPath(filePath) || fileColumn; + const fileInfos = await this.getFileInfoByColumnAndTarget( + componentId, + row.id || row.objid || row.seq, // 기본키 값 + tableName + ); + + if (fileInfos && fileInfos.length > 0) { // 파일 정보를 JSON 형태로 저장 + const totalSize = fileInfos.reduce( + (sum, file) => sum + (file.size || 0), + 0 + ); enrichedRow[fileColumn] = JSON.stringify({ - files: [fileInfo], - totalCount: 1, - totalSize: fileInfo.size, + files: fileInfos, + totalCount: fileInfos.length, + totalSize: totalSize, + }); + } else { + // 파일이 없으면 빈 상태로 설정 + enrichedRow[fileColumn] = JSON.stringify({ + files: [], + totalCount: 0, + totalSize: 0, }); } } @@ -577,7 +596,70 @@ export class TableManagementService { } /** - * 파일 경로로 파일 정보 조회 + * 파일 경로에서 컴포넌트 ID 추출 (현재는 사용하지 않음) + */ + private extractComponentIdFromPath(filePath: string): string | null { + // 현재는 파일 경로에서 컴포넌트 ID를 추출할 수 없으므로 null 반환 + // 추후 필요시 구현 + return null; + } + + /** + * 컬럼별 파일 정보 조회 (컬럼명과 target_objid로 구분) + */ + private async getFileInfoByColumnAndTarget( + columnName: string, + targetObjid: any, + tableName: string + ): Promise { + try { + logger.info( + `컬럼별 파일 정보 조회: ${tableName}.${columnName}, target: ${targetObjid}` + ); + + // 🎯 컬럼명을 doc_type으로 사용하여 파일 구분 + const fileInfos = await prisma.attach_file_info.findMany({ + where: { + target_objid: String(targetObjid), + doc_type: columnName, // 컬럼명으로 파일 구분 + status: "ACTIVE", + }, + select: { + objid: true, + real_file_name: true, + file_size: true, + file_ext: true, + file_path: true, + doc_type: true, + doc_type_name: true, + regdate: true, + writer: true, + }, + orderBy: { + regdate: "desc", + }, + }); + + // 파일 정보 포맷팅 + return fileInfos.map((fileInfo) => ({ + name: fileInfo.real_file_name, + size: Number(fileInfo.file_size) || 0, + path: fileInfo.file_path, + ext: fileInfo.file_ext, + objid: String(fileInfo.objid), + docType: fileInfo.doc_type, + docTypeName: fileInfo.doc_type_name, + regdate: fileInfo.regdate?.toISOString(), + writer: fileInfo.writer, + })); + } catch (error) { + logger.warn(`컬럼별 파일 정보 조회 실패: ${columnName}`, error); + return []; + } + } + + /** + * 파일 경로로 파일 정보 조회 (기존 메서드 - 호환성 유지) */ private async getFileInfoByPath(filePath: string): Promise { try { diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index 4f9ab6e1..37f85957 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -1521,6 +1521,58 @@ export const InteractiveDataTable: React.FC = ({ } }, []); + // 🗑️ 연결된 파일 삭제 함수 + const handleDeleteLinkedFile = useCallback( + async (fileId: string, fileName: string) => { + try { + console.log("🗑️ 파일 삭제 시작:", { fileId, fileName }); + + // 삭제 확인 다이얼로그 + if (!confirm(`"${fileName}" 파일을 삭제하시겠습니까?`)) { + return; + } + + // API 호출로 파일 삭제 (논리적 삭제) - apiClient 사용으로 JWT 토큰 자동 추가 + const apiClient = (await import("@/lib/api/client")).apiClient; + const response = await apiClient.delete(`/files/${fileId}`, { + data: { + writer: "current_user", // 현재 사용자 정보 + }, + }); + + const result = response.data; + console.log("📡 파일 삭제 API 응답:", result); + + if (!result.success) { + throw new Error(result.message || "파일 삭제 실패"); + } + + // 성공 메시지 + toast.success(`"${fileName}" 파일이 삭제되었습니다.`); + + // 파일 목록 새로고침 + if (showFileManagementModal && selectedRowForFiles && component.tableName) { + const primaryKeyField = Object.keys(selectedRowForFiles)[0]; + const recordId = selectedRowForFiles[primaryKeyField]; + + try { + const response = await getLinkedFiles(component.tableName, recordId); + setLinkedFiles(response.files || []); + console.log("📁 파일 목록 새로고침 완료:", response.files?.length || 0); + } catch (error) { + console.error("파일 목록 새로고침 실패:", error); + } + } + + console.log("✅ 파일 삭제 완료:", fileName); + } catch (error) { + console.error("❌ 파일 삭제 실패:", error); + toast.error(`"${fileName}" 파일 삭제에 실패했습니다.`); + } + }, + [showFileManagementModal, selectedRowForFiles, component.tableName], + ); + // 셀 값 포맷팅 const formatCellValue = (value: any, column: DataTableColumn, rowData?: Record): React.ReactNode => { // 가상 파일 컬럼의 경우 value가 없어도 파일 아이콘을 표시해야 함 @@ -2301,6 +2353,16 @@ export const InteractiveDataTable: React.FC = ({ > + + {/* 🗑️ 파일 삭제 버튼 */} + ))} diff --git a/frontend/components/screen/widgets/FileUpload.tsx b/frontend/components/screen/widgets/FileUpload.tsx index 961c0fe6..c25dbaac 100644 --- a/frontend/components/screen/widgets/FileUpload.tsx +++ b/frontend/components/screen/widgets/FileUpload.tsx @@ -239,8 +239,9 @@ export function FileUpload({ component, onUpdateComponent, onFileUpload, userInf const formData = new FormData(); formData.append("files", file); - formData.append("docType", fileConfig.docType); - formData.append("docTypeName", fileConfig.docTypeName); + // 🎯 컴포넌트 ID를 doc_type으로 사용하여 파일 컴포넌트별로 구분 + formData.append("docType", component.id); + formData.append("docTypeName", component.label || fileConfig.docTypeName); // 🎯 최신 사용자 정보 참조 (ref를 통해 실시간 값 접근) const currentUser = userRef.current; @@ -487,22 +488,14 @@ export function FileUpload({ component, onUpdateComponent, onFileUpload, userInf const deleteFile = async (fileInfo: AttachedFileInfo) => { console.log("🗑️ 파일 삭제:", fileInfo.realFileName); try { - // 실제 API 호출 (논리적 삭제) - const response = await fetch(`/api/files/${fileInfo.objid}`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ + // 실제 API 호출 (논리적 삭제) - apiClient 사용으로 JWT 토큰 자동 추가 + const response = await apiClient.delete(`/files/${fileInfo.objid}`, { + data: { writer: fileInfo.writer || "current_user", - }), + }, }); - if (!response.ok) { - throw new Error(`파일 삭제 실패: ${response.status}`); - } - - const result = await response.json(); + const result = response.data; console.log("📡 파일 삭제 API 응답:", result); if (!result.success) {