diff --git a/backend-node/src/controllers/fileController.ts b/backend-node/src/controllers/fileController.ts index 28a46232..66418099 100644 --- a/backend-node/src/controllers/fileController.ts +++ b/backend-node/src/controllers/fileController.ts @@ -431,7 +431,7 @@ export const deleteFile = async ( // 파일 정보 조회 const fileRecord = await queryOne( `SELECT * FROM attach_file_info WHERE objid = $1`, - [parseInt(objid)] + [objid] ); if (!fileRecord) { @@ -460,7 +460,7 @@ export const deleteFile = async ( // 파일 상태를 DELETED로 변경 (논리적 삭제) await query( "UPDATE attach_file_info SET status = $1 WHERE objid = $2", - ["DELETED", parseInt(objid)] + ["DELETED", objid] ); // 🆕 레코드 모드: 해당 행의 attachments 컬럼 자동 업데이트 @@ -708,6 +708,40 @@ export const getComponentFiles = async ( ); } + // 3. 레코드의 컬럼 값으로 파일 직접 조회 (수정 모달에서 기존 파일 로드) + // target_objid 매칭이 안 될 때, 테이블 레코드의 컬럼 값(파일 objid)으로 직접 찾기 + if (dataFiles.length === 0 && templateFiles.length === 0 && tableName && recordId && columnName) { + try { + // 레코드에서 해당 컬럼 값 조회 (파일 objid가 저장되어 있을 수 있음) + const safeTable = String(tableName).replace(/[^a-zA-Z0-9_]/g, ""); + const safeColumn = String(columnName).replace(/[^a-zA-Z0-9_]/g, ""); + const recordResult = await query( + `SELECT "${safeColumn}" FROM "${safeTable}" WHERE id = $1 LIMIT 1`, + [recordId] + ); + + if (recordResult.length > 0 && recordResult[0][safeColumn]) { + const columnValue = String(recordResult[0][safeColumn]); + // 숫자값인 경우 파일 objid로 간주하고 조회 + if (/^\d+$/.test(columnValue)) { + console.log("🔍 [getComponentFiles] 레코드 컬럼 값으로 파일 조회:", { table: safeTable, column: safeColumn, fileObjid: columnValue }); + const directFiles = await query( + `SELECT * FROM attach_file_info + WHERE objid = $1 AND status = $2 + ORDER BY regdate DESC`, + [columnValue, "ACTIVE"] + ); + if (directFiles.length > 0) { + console.log("✅ [getComponentFiles] 레코드 컬럼 값으로 파일 찾음:", directFiles.length, "건"); + dataFiles = directFiles; + } + } + } + } catch (lookupError) { + console.warn("⚠️ [getComponentFiles] 레코드 컬럼 값 조회 실패:", lookupError); + } + } + // 파일 정보 포맷팅 함수 const formatFileInfo = (file: any, isTemplate: boolean = false) => ({ objid: file.objid.toString(), @@ -782,7 +816,7 @@ export const previewFile = async ( const fileRecord = await queryOne( "SELECT * FROM attach_file_info WHERE objid = $1 LIMIT 1", - [parseInt(objid)] + [objid] ); if (!fileRecord || fileRecord.status !== "ACTIVE") { @@ -921,7 +955,7 @@ export const downloadFile = async ( const fileRecord = await queryOne( `SELECT * FROM attach_file_info WHERE objid = $1`, - [parseInt(objid)] + [objid] ); if (!fileRecord || fileRecord.status !== "ACTIVE") { @@ -1212,7 +1246,7 @@ export const setRepresentativeFile = async ( // 파일 존재 여부 및 권한 확인 const fileRecord = await queryOne( `SELECT * FROM attach_file_info WHERE objid = $1 AND status = $2`, - [parseInt(objid), "ACTIVE"] + [objid, "ACTIVE"] ); if (!fileRecord) { @@ -1237,7 +1271,7 @@ export const setRepresentativeFile = async ( `UPDATE attach_file_info SET is_representative = false WHERE target_objid = $1 AND objid != $2`, - [fileRecord.target_objid, parseInt(objid)] + [fileRecord.target_objid, objid] ); // 선택한 파일을 대표 파일로 설정 @@ -1245,7 +1279,7 @@ export const setRepresentativeFile = async ( `UPDATE attach_file_info SET is_representative = true WHERE objid = $1`, - [parseInt(objid)] + [objid] ); res.json({ @@ -1281,7 +1315,7 @@ export const getFileInfo = async (req: Request, res: Response) => { `SELECT objid, real_file_name, file_size, file_ext, file_path, regdate, is_representative FROM attach_file_info WHERE objid = $1 AND status = 'ACTIVE'`, - [parseInt(objid)] + [objid] ); if (!fileRecord) { diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 5fe2f242..6e62a541 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2273,6 +2273,9 @@ export class TableManagementService { const safeSortOrder = sortOrder.toLowerCase() === "desc" ? "DESC" : "ASC"; orderClause = `ORDER BY ${safeSortBy} ${safeSortOrder}`; + } else { + // sortBy가 없으면 created_date DESC를 기본 정렬로 사용 (신규 데이터가 최상단에 표시) + orderClause = `ORDER BY main.created_date DESC`; } // 안전한 테이블명 검증 @@ -3185,9 +3188,10 @@ export class TableManagementService { } // ORDER BY 절 구성 + // sortBy가 없으면 created_date DESC를 기본 정렬로 사용 (신규 데이터가 최상단에 표시) const orderBy = options.sortBy - ? `main.${options.sortBy} ${options.sortOrder === "desc" ? "DESC" : "ASC"}` - : ""; + ? `main."${options.sortBy}" ${options.sortOrder === "desc" ? "DESC" : "ASC"}` + : `main."created_date" DESC`; // 페이징 계산 const offset = (options.page - 1) * options.size; @@ -3403,8 +3407,8 @@ export class TableManagementService { selectColumns, "", // WHERE 절은 나중에 추가 options.sortBy - ? `main.${options.sortBy} ${options.sortOrder || "ASC"}` - : undefined, + ? `main."${options.sortBy}" ${options.sortOrder || "ASC"}` + : `main."created_date" DESC`, options.size, (options.page - 1) * options.size ); @@ -3591,8 +3595,8 @@ export class TableManagementService { const whereClause = whereConditions.join(" AND "); const orderBy = options.sortBy - ? `main.${options.sortBy} ${options.sortOrder === "desc" ? "DESC" : "ASC"}` - : ""; + ? `main."${options.sortBy}" ${options.sortOrder === "desc" ? "DESC" : "ASC"}` + : `main."created_date" DESC`; // 페이징 계산 const offset = (options.page - 1) * options.size; diff --git a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx index dd4f4c6b..f655ebe3 100644 --- a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx +++ b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx @@ -177,10 +177,18 @@ const FileUploadComponent: React.FC = ({ // 🔑 레코드별 고유 키 사용 const backupKey = getUniqueKey(); const backupFiles = localStorage.getItem(backupKey); + console.log("🔎 [DEBUG-MOUNT] localStorage 확인:", { + backupKey, + hasBackup: !!backupFiles, + componentId: component.id, + recordId: recordId, + formDataId: formData?.id, + stackTrace: new Error().stack?.split('\n').slice(1, 4).join(' <- '), + }); if (backupFiles) { const parsedFiles = JSON.parse(backupFiles); if (parsedFiles.length > 0) { - console.log("🚀 컴포넌트 마운트 시 파일 즉시 복원:", { + console.log("🚀 [DEBUG-MOUNT] 파일 즉시 복원:", { uniqueKey: backupKey, componentId: component.id, recordId: recordId, @@ -203,6 +211,50 @@ const FileUploadComponent: React.FC = ({ } }, [component.id, getUniqueKey, recordId]); // 레코드별 고유 키 변경 시 재실행 + // 🆕 모달 닫힘/저장 성공 시 localStorage 파일 캐시 정리 (등록 후 재등록 시 이전 파일 잔존 방지) + useEffect(() => { + const handleClearFileCache = (event: Event) => { + const backupKey = getUniqueKey(); + const eventType = event.type; + console.log("🧹 [DEBUG-CLEAR] 파일 캐시 정리 이벤트 수신:", { + eventType, + backupKey, + componentId: component.id, + currentFiles: uploadedFiles.length, + hasLocalStorage: !!localStorage.getItem(backupKey), + }); + try { + localStorage.removeItem(backupKey); + setUploadedFiles([]); + setRepresentativeImageUrl(null); + if (typeof window !== "undefined") { + const globalFileState = (window as any).globalFileState || {}; + delete globalFileState[backupKey]; + (window as any).globalFileState = globalFileState; + } + console.log("🧹 [DEBUG-CLEAR] 정리 완료:", backupKey); + } catch (e) { + console.warn("파일 캐시 정리 실패:", e); + } + }; + + // EditModal 닫힘, ScreenModal 연속 등록 저장 성공, 일반 저장 성공 모두 처리 + window.addEventListener("closeEditModal", handleClearFileCache); + window.addEventListener("saveSuccess", handleClearFileCache); + window.addEventListener("saveSuccessInModal", handleClearFileCache); + + console.log("🔎 [DEBUG-CLEAR] 이벤트 리스너 등록 완료:", { + componentId: component.id, + backupKey: getUniqueKey(), + }); + + return () => { + window.removeEventListener("closeEditModal", handleClearFileCache); + window.removeEventListener("saveSuccess", handleClearFileCache); + window.removeEventListener("saveSuccessInModal", handleClearFileCache); + }; + }, [getUniqueKey]); + // 🎯 화면설계 모드에서 실제 화면으로의 실시간 동기화 이벤트 리스너 useEffect(() => { const handleDesignModeFileChange = (event: CustomEvent) => { @@ -363,6 +415,12 @@ const FileUploadComponent: React.FC = ({ console.warn("파일 병합 중 오류:", e); } + console.log("🔎 [DEBUG-LOAD] API 응답 후 파일 설정:", { + componentId: component.id, + serverFiles: formattedFiles.length, + finalFiles: finalFiles.length, + files: finalFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })), + }); setUploadedFiles(finalFiles); // 전역 상태에도 저장 (레코드별 고유 키 사용) diff --git a/frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx b/frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx index 1f8232d8..8b9671c8 100644 --- a/frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx +++ b/frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx @@ -199,6 +199,12 @@ const FileUploadComponent: React.FC = ({ if (imageObjidFromFormData && /^\d+$/.test(String(imageObjidFromFormData))) { const objidStr = String(imageObjidFromFormData); + // 🆕 등록 모드(formData.id가 없는 경우)에서는 이전 파일 로드 스킵 + // 연속 등록 시 이전 저장의 image 값이 남아있어 다시 로드되는 것을 방지 + if (!formData?.id) { + return; + } + // 이미 같은 objid의 파일이 로드되어 있으면 스킵 const alreadyLoaded = uploadedFiles.some(f => String(f.objid) === objidStr); if (alreadyLoaded) { @@ -431,6 +437,11 @@ const FileUploadComponent: React.FC = ({ return; // DB 로드 성공 시 localStorage 무시 } + // 🆕 등록 모드(새 레코드)인 경우 fallback 로드도 스킵 - 항상 빈 상태 유지 + if (!isRecordMode || !recordId) { + return; + } + // DB 로드 실패 시에만 기존 로직 사용 (하위 호환성) // 전역 상태에서 최신 파일 정보 가져오기 (🆕 고유 키 사용)