From d09c8e0787a1613117d2dc21fdf671b2d3a3548b Mon Sep 17 00:00:00 2001 From: leeheejin Date: Wed, 10 Dec 2025 18:38:16 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EC=97=85=EB=A1=9C=EB=93=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/fileController.ts | 94 ++++++ frontend/components/screen/EditModal.tsx | 9 +- .../file-upload/FileUploadComponent.tsx | 276 ++++++++++++++---- .../table-list/TableListComponent.tsx | 29 ++ 4 files changed, 353 insertions(+), 55 deletions(-) diff --git a/backend-node/src/controllers/fileController.ts b/backend-node/src/controllers/fileController.ts index b1e31e3b..fe3d5cfd 100644 --- a/backend-node/src/controllers/fileController.ts +++ b/backend-node/src/controllers/fileController.ts @@ -341,6 +341,50 @@ export const uploadFiles = async ( }); } + // πŸ†• λ ˆμ½”λ“œ λͺ¨λ“œ: ν•΄λ‹Ή ν–‰μ˜ attachments 컬럼 μžλ™ μ—…λ°μ΄νŠΈ + const isRecordMode = req.body.isRecordMode === "true" || req.body.isRecordMode === true; + if (isRecordMode && linkedTable && recordId && columnName) { + try { + // ν•΄λ‹Ή λ ˆμ½”λ“œμ˜ λͺ¨λ“  μ²¨λΆ€νŒŒμΌ 쑰회 + const allFiles = await query( + `SELECT objid, real_file_name, file_size, file_ext, file_path, regdate + FROM attach_file_info + WHERE target_objid = $1 AND status = 'ACTIVE' + ORDER BY regdate DESC`, + [finalTargetObjid] + ); + + // attachments JSONB ν˜•νƒœλ‘œ λ³€ν™˜ + const attachmentsJson = allFiles.map((f: any) => ({ + objid: f.objid.toString(), + realFileName: f.real_file_name, + fileSize: Number(f.file_size), + fileExt: f.file_ext, + filePath: f.file_path, + regdate: f.regdate?.toISOString(), + })); + + // ν•΄λ‹Ή ν…Œμ΄λΈ”μ˜ attachments 컬럼 μ—…λ°μ΄νŠΈ + // πŸ”’ λ©€ν‹°ν…Œλ„Œμ‹œ: company_code ν•„ν„° μΆ”κ°€ + await query( + `UPDATE ${linkedTable} + SET ${columnName} = $1::jsonb, updated_date = NOW() + WHERE id = $2 AND company_code = $3`, + [JSON.stringify(attachmentsJson), recordId, companyCode] + ); + + console.log("πŸ“Ž [λ ˆμ½”λ“œ λͺ¨λ“œ] attachments 컬럼 μžλ™ μ—…λ°μ΄νŠΈ:", { + tableName: linkedTable, + recordId: recordId, + columnName: columnName, + fileCount: attachmentsJson.length, + }); + } catch (updateError) { + // attachments 컬럼 μ—…λ°μ΄νŠΈ μ‹€νŒ¨ν•΄λ„ 파일 μ—…λ‘œλ“œλŠ” μ„±κ³΅μœΌλ‘œ 처리 + console.warn("⚠️ attachments 컬럼 μ—…λ°μ΄νŠΈ μ‹€νŒ¨ (λ¬΄μ‹œ):", updateError); + } + } + res.json({ success: true, message: `${files.length}개 파일 μ—…λ‘œλ“œ μ™„λ£Œ`, @@ -405,6 +449,56 @@ export const deleteFile = async ( ["DELETED", parseInt(objid)] ); + // πŸ†• λ ˆμ½”λ“œ λͺ¨λ“œ: ν•΄λ‹Ή ν–‰μ˜ attachments 컬럼 μžλ™ μ—…λ°μ΄νŠΈ + const targetObjid = fileRecord.target_objid; + if (targetObjid && !targetObjid.startsWith('screen_files:') && !targetObjid.startsWith('temp_')) { + // targetObjid νŒŒμ‹±: tableName:recordId:columnName ν˜•μ‹ + const parts = targetObjid.split(':'); + if (parts.length >= 3) { + const [tableName, recordId, columnName] = parts; + + try { + // ν•΄λ‹Ή λ ˆμ½”λ“œμ˜ 남은 μ²¨λΆ€νŒŒμΌ 쑰회 + const remainingFiles = await query( + `SELECT objid, real_file_name, file_size, file_ext, file_path, regdate + FROM attach_file_info + WHERE target_objid = $1 AND status = 'ACTIVE' + ORDER BY regdate DESC`, + [targetObjid] + ); + + // attachments JSONB ν˜•νƒœλ‘œ λ³€ν™˜ + const attachmentsJson = remainingFiles.map((f: any) => ({ + objid: f.objid.toString(), + realFileName: f.real_file_name, + fileSize: Number(f.file_size), + fileExt: f.file_ext, + filePath: f.file_path, + regdate: f.regdate?.toISOString(), + })); + + // ν•΄λ‹Ή ν…Œμ΄λΈ”μ˜ attachments 컬럼 μ—…λ°μ΄νŠΈ + // πŸ”’ λ©€ν‹°ν…Œλ„Œμ‹œ: company_code ν•„ν„° μΆ”κ°€ + await query( + `UPDATE ${tableName} + SET ${columnName} = $1::jsonb, updated_date = NOW() + WHERE id = $2 AND company_code = $3`, + [JSON.stringify(attachmentsJson), recordId, fileRecord.company_code] + ); + + console.log("πŸ“Ž [파일 μ‚­μ œ] attachments 컬럼 μžλ™ μ—…λ°μ΄νŠΈ:", { + tableName, + recordId, + columnName, + remainingFiles: attachmentsJson.length, + }); + } catch (updateError) { + // attachments 컬럼 μ—…λ°μ΄νŠΈ μ‹€νŒ¨ν•΄λ„ 파일 μ‚­μ œλŠ” μ„±κ³΅μœΌλ‘œ 처리 + console.warn("⚠️ attachments 컬럼 μ—…λ°μ΄νŠΈ μ‹€νŒ¨ (λ¬΄μ‹œ):", updateError); + } + } + } + res.json({ success: true, message: "파일이 μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.", diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 3815fc71..9dcb58bf 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -761,12 +761,19 @@ export const EditModal: React.FC = ({ className }) => { }); } + // πŸ”‘ μ²¨λΆ€νŒŒμΌ μ»΄ν¬λ„ŒνŠΈκ°€ ν–‰(λ ˆμ½”λ“œ) λ‹¨μœ„λ‘œ νŒŒμΌμ„ μ €μž₯ν•  수 μžˆλ„λ‘ tableName μΆ”κ°€ + const enrichedFormData = { + ...(groupData.length > 0 ? groupData[0] : formData), + tableName: screenData.screenInfo?.tableName, // ν…Œμ΄λΈ”λͺ… μΆ”κ°€ + screenId: modalState.screenId, // ν™”λ©΄ ID μΆ”κ°€ + }; + return ( 0 ? groupData[0] : formData} + formData={enrichedFormData} onFormDataChange={(fieldName, value) => { // πŸ†• κ·Έλ£Ή 데이터가 있으면 처리 if (groupData.length > 0) { diff --git a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx index 8dda7864..dc77ac93 100644 --- a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx +++ b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx @@ -10,6 +10,7 @@ import { apiClient } from "@/lib/api/client"; import { FileViewerModal } from "./FileViewerModal"; import { FileManagerModal } from "./FileManagerModal"; import { FileUploadConfig, FileInfo, FileUploadStatus, FileUploadResponse } from "./types"; +import { useAuth } from "@/hooks/useAuth"; import { Upload, File, @@ -92,6 +93,9 @@ const FileUploadComponent: React.FC = ({ onDragEnd, onUpdate, }) => { + // πŸ”‘ 인증 정보 κ°€μ Έμ˜€κΈ° + const { user } = useAuth(); + const [uploadedFiles, setUploadedFiles] = useState([]); const [uploadStatus, setUploadStatus] = useState("idle"); const [dragOver, setDragOver] = useState(false); @@ -102,28 +106,86 @@ const FileUploadComponent: React.FC = ({ const [representativeImageUrl, setRepresentativeImageUrl] = useState(null); const fileInputRef = useRef(null); + // πŸ”‘ λ ˆμ½”λ“œ λͺ¨λ“œ νŒλ‹¨: formData에 idκ°€ 있으면 νŠΉμ • 행에 μ—°κ²°λœ 파일 관리 + const isRecordMode = !!(formData?.id && !String(formData.id).startsWith('temp_')); + const recordTableName = formData?.tableName || component.tableName; + const recordId = formData?.id; + const columnName = component.columnName || component.id || 'attachments'; + + // πŸ”‘ λ ˆμ½”λ“œ λͺ¨λ“œμš© targetObjid 생성 + const getRecordTargetObjid = useCallback(() => { + if (isRecordMode && recordTableName && recordId) { + return `${recordTableName}:${recordId}:${columnName}`; + } + return null; + }, [isRecordMode, recordTableName, recordId, columnName]); + + // πŸ”‘ λ ˆμ½”λ“œλ³„ 고유 ν‚€ 생성 (localStorage, μ „μ—­ μƒνƒœμš©) + const getUniqueKey = useCallback(() => { + if (isRecordMode && recordTableName && recordId) { + // λ ˆμ½”λ“œ λͺ¨λ“œ: ν…Œμ΄λΈ”λͺ…:λ ˆμ½”λ“œID:μ»΄ν¬λ„ŒνŠΈID ν˜•νƒœλ‘œ 고유 ν‚€ 생성 + return `fileUpload_${recordTableName}_${recordId}_${component.id}`; + } + // κΈ°λ³Έ λͺ¨λ“œ: μ»΄ν¬λ„ŒνŠΈ ID만 μ‚¬μš© + return `fileUpload_${component.id}`; + }, [isRecordMode, recordTableName, recordId, component.id]); + + // πŸ” 디버깅: λ ˆμ½”λ“œ λͺ¨λ“œ μƒνƒœ λ‘œκΉ… + useEffect(() => { + console.log("πŸ“Ž [FileUploadComponent] λͺ¨λ“œ 확인:", { + isRecordMode, + recordTableName, + recordId, + columnName, + targetObjid: getRecordTargetObjid(), + uniqueKey: getUniqueKey(), + formDataKeys: formData ? Object.keys(formData) : [], + }); + }, [isRecordMode, recordTableName, recordId, columnName, getRecordTargetObjid, getUniqueKey, formData]); + + // πŸ†• λ ˆμ½”λ“œ ID λ³€κ²½ μ‹œ 파일 λͺ©λ‘ μ΄ˆκΈ°ν™” 및 μƒˆλ‘œ λ‘œλ“œ + const prevRecordIdRef = useRef(null); + useEffect(() => { + if (prevRecordIdRef.current !== recordId) { + console.log("πŸ“Ž [FileUploadComponent] λ ˆμ½”λ“œ ID λ³€κ²½ 감지:", { + prev: prevRecordIdRef.current, + current: recordId, + isRecordMode, + }); + prevRecordIdRef.current = recordId; + + // λ ˆμ½”λ“œ λͺ¨λ“œμ—μ„œ λ ˆμ½”λ“œ IDκ°€ λ³€κ²½λ˜λ©΄ 파일 λͺ©λ‘ μ΄ˆκΈ°ν™” + if (isRecordMode) { + setUploadedFiles([]); + } + } + }, [recordId, isRecordMode]); + // μ»΄ν¬λ„ŒνŠΈ 마운트 μ‹œ μ¦‰μ‹œ localStorageμ—μ„œ 파일 볡원 useEffect(() => { if (!component?.id) return; try { - const backupKey = `fileUpload_${component.id}`; + // πŸ”‘ λ ˆμ½”λ“œλ³„ 고유 ν‚€ μ‚¬μš© + const backupKey = getUniqueKey(); const backupFiles = localStorage.getItem(backupKey); if (backupFiles) { const parsedFiles = JSON.parse(backupFiles); if (parsedFiles.length > 0) { console.log("πŸš€ μ»΄ν¬λ„ŒνŠΈ 마운트 μ‹œ 파일 μ¦‰μ‹œ 볡원:", { + uniqueKey: backupKey, componentId: component.id, + recordId: recordId, restoredFiles: parsedFiles.length, files: parsedFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })), }); setUploadedFiles(parsedFiles); - // μ „μ—­ μƒνƒœμ—λ„ 볡원 + // μ „μ—­ μƒνƒœμ—λ„ 볡원 (λ ˆμ½”λ“œλ³„ 고유 ν‚€ μ‚¬μš©) if (typeof window !== "undefined") { (window as any).globalFileState = { ...(window as any).globalFileState, - [component.id]: parsedFiles, + [backupKey]: parsedFiles, }; } } @@ -131,7 +193,7 @@ const FileUploadComponent: React.FC = ({ } catch (e) { console.warn("μ»΄ν¬λ„ŒνŠΈ 마운트 μ‹œ 파일 볡원 μ‹€νŒ¨:", e); } - }, [component.id]); // component.idκ°€ 변경될 λ•Œλ§Œ μ‹€ν–‰ + }, [component.id, getUniqueKey, recordId]); // λ ˆμ½”λ“œλ³„ 고유 ν‚€ λ³€κ²½ μ‹œ μž¬μ‹€ν–‰ // 🎯 화면섀계 λͺ¨λ“œμ—μ„œ μ‹€μ œ ν™”λ©΄μœΌλ‘œμ˜ μ‹€μ‹œκ°„ 동기화 이벀트 λ¦¬μŠ€λ„ˆ useEffect(() => { @@ -152,12 +214,14 @@ const FileUploadComponent: React.FC = ({ const newFiles = event.detail.files || []; setUploadedFiles(newFiles); - // localStorage λ°±μ—… μ—…λ°μ΄νŠΈ + // localStorage λ°±μ—… μ—…λ°μ΄νŠΈ (λ ˆμ½”λ“œλ³„ 고유 ν‚€ μ‚¬μš©) try { - const backupKey = `fileUpload_${component.id}`; + const backupKey = getUniqueKey(); localStorage.setItem(backupKey, JSON.stringify(newFiles)); console.log("πŸ’Ύ 화면섀계 λͺ¨λ“œ 동기화 ν›„ localStorage λ°±μ—… μ—…λ°μ΄νŠΈ:", { + uniqueKey: backupKey, componentId: component.id, + recordId: recordId, fileCount: newFiles.length, }); } catch (e) { @@ -201,6 +265,16 @@ const FileUploadComponent: React.FC = ({ if (!component?.id) return false; try { + // πŸ”‘ λ ˆμ½”λ“œ λͺ¨λ“œ: ν•΄λ‹Ή ν–‰μ˜ 파일만 쑰회 + if (isRecordMode && recordTableName && recordId) { + console.log("πŸ“‚ [FileUploadComponent] λ ˆμ½”λ“œ λͺ¨λ“œ 파일 쑰회:", { + tableName: recordTableName, + recordId: recordId, + columnName: columnName, + targetObjid: getRecordTargetObjid(), + }); + } + // 1. formDataμ—μ„œ screenId κ°€μ Έμ˜€κΈ° let screenId = formData?.screenId; @@ -232,11 +306,13 @@ const FileUploadComponent: React.FC = ({ const params = { screenId, componentId: component.id, - tableName: formData?.tableName || component.tableName, - recordId: formData?.id, - columnName: component.columnName || component.id, // πŸ”‘ columnName이 μ—†μœΌλ©΄ component.id μ‚¬μš© + tableName: recordTableName || formData?.tableName || component.tableName, + recordId: recordId || formData?.id, + columnName: columnName, // πŸ”‘ λ ˆμ½”λ“œ λͺ¨λ“œμ—μ„œ μ‚¬μš©ν•˜λŠ” columnName }; + console.log("πŸ“‚ [FileUploadComponent] 파일 쑰회 νŒŒλΌλ―Έν„°:", params); + const response = await getComponentFiles(params); if (response.success) { @@ -255,11 +331,11 @@ const FileUploadComponent: React.FC = ({ })); - // πŸ”„ localStorage의 κΈ°μ‘΄ 파일과 μ„œλ²„ 파일 병합 + // πŸ”„ localStorage의 κΈ°μ‘΄ 파일과 μ„œλ²„ 파일 병합 (λ ˆμ½”λ“œλ³„ 고유 ν‚€ μ‚¬μš©) let finalFiles = formattedFiles; + const uniqueKey = getUniqueKey(); try { - const backupKey = `fileUpload_${component.id}`; - const backupFiles = localStorage.getItem(backupKey); + const backupFiles = localStorage.getItem(uniqueKey); if (backupFiles) { const parsedBackupFiles = JSON.parse(backupFiles); @@ -268,7 +344,12 @@ const FileUploadComponent: React.FC = ({ const additionalFiles = parsedBackupFiles.filter((f: any) => !serverObjIds.has(f.objid)); finalFiles = [...formattedFiles, ...additionalFiles]; - + console.log("πŸ“‚ [FileUploadComponent] 파일 병합 μ™„λ£Œ:", { + uniqueKey, + serverFiles: formattedFiles.length, + localFiles: parsedBackupFiles.length, + finalFiles: finalFiles.length, + }); } } catch (e) { console.warn("파일 병합 쀑 였λ₯˜:", e); @@ -276,11 +357,11 @@ const FileUploadComponent: React.FC = ({ setUploadedFiles(finalFiles); - // μ „μ—­ μƒνƒœμ—λ„ μ €μž₯ + // μ „μ—­ μƒνƒœμ—λ„ μ €μž₯ (λ ˆμ½”λ“œλ³„ 고유 ν‚€ μ‚¬μš©) if (typeof window !== "undefined") { (window as any).globalFileState = { ...(window as any).globalFileState, - [component.id]: finalFiles, + [uniqueKey]: finalFiles, }; // 🌐 μ „μ—­ 파일 μ €μž₯μ†Œμ— 등둝 (νŽ˜μ΄μ§€ κ°„ 곡유용) @@ -288,12 +369,12 @@ const FileUploadComponent: React.FC = ({ uploadPage: window.location.pathname, componentId: component.id, screenId: formData?.screenId, + recordId: recordId, }); - // localStorage 백업도 λ³‘ν•©λœ 파일둜 μ—…λ°μ΄νŠΈ + // localStorage 백업도 λ³‘ν•©λœ 파일둜 μ—…λ°μ΄νŠΈ (λ ˆμ½”λ“œλ³„ 고유 ν‚€ μ‚¬μš©) try { - const backupKey = `fileUpload_${component.id}`; - localStorage.setItem(backupKey, JSON.stringify(finalFiles)); + localStorage.setItem(uniqueKey, JSON.stringify(finalFiles)); } catch (e) { console.warn("localStorage λ°±μ—… μ—…λ°μ΄νŠΈ μ‹€νŒ¨:", e); } @@ -304,7 +385,7 @@ const FileUploadComponent: React.FC = ({ console.error("파일 쑰회 였λ₯˜:", error); } return false; // κΈ°μ‘΄ 둜직 μ‚¬μš© - }, [component.id, component.tableName, component.columnName, formData?.screenId, formData?.tableName, formData?.id]); + }, [component.id, component.tableName, component.columnName, formData?.screenId, formData?.tableName, formData?.id, getUniqueKey, recordId, isRecordMode, recordTableName, columnName]); // μ»΄ν¬λ„ŒνŠΈ 파일 동기화 (DB μš°μ„ , localStorageλŠ” 보쑰) useEffect(() => { @@ -316,6 +397,8 @@ const FileUploadComponent: React.FC = ({ componentFiles: componentFiles.length, formData: formData, screenId: formData?.screenId, + tableName: formData?.tableName, // πŸ” ν…Œμ΄λΈ”λͺ… 확인 + recordId: formData?.id, // πŸ” λ ˆμ½”λ“œ ID 확인 currentUploadedFiles: uploadedFiles.length, }); @@ -371,9 +454,9 @@ const FileUploadComponent: React.FC = ({ setUploadedFiles(files); setForceUpdate((prev) => prev + 1); - // localStorage 백업도 μ—…λ°μ΄νŠΈ + // localStorage 백업도 μ—…λ°μ΄νŠΈ (λ ˆμ½”λ“œλ³„ 고유 ν‚€ μ‚¬μš©) try { - const backupKey = `fileUpload_${component.id}`; + const backupKey = getUniqueKey(); localStorage.setItem(backupKey, JSON.stringify(files)); } catch (e) { console.warn("localStorage λ°±μ—… μ‹€νŒ¨:", e); @@ -462,10 +545,10 @@ const FileUploadComponent: React.FC = ({ toast.loading("νŒŒμΌμ„ μ—…λ‘œλ“œν•˜λŠ” 쀑...", { id: "file-upload" }); try { - // targetObjid 생성 - ν…œν”Œλ¦Ώ vs 데이터 파일 ꡬ뢄 - const tableName = formData?.tableName || component.tableName || "default_table"; - const recordId = formData?.id; - const columnName = component.columnName || component.id; + // πŸ”‘ λ ˆμ½”λ“œ λͺ¨λ“œ μš°μ„  μ‚¬μš© + const effectiveTableName = recordTableName || formData?.tableName || component.tableName || "default_table"; + const effectiveRecordId = recordId || formData?.id; + const effectiveColumnName = columnName; // screenId μΆ”μΆœ (μš°μ„ μˆœμœ„: formData > URL) let screenId = formData?.screenId; @@ -478,39 +561,56 @@ const FileUploadComponent: React.FC = ({ } let targetObjid; - // μš°μ„ μˆœμœ„: 1) μ‹€μ œ 데이터 (recordIdκ°€ 숫자/λ¬Έμžμ—΄μ΄κ³  temp_κ°€ μ•„λ‹˜) > 2) ν…œν”Œλ¦Ώ (screenId) > 3) κΈ°λ³Έκ°’ - const isRealRecord = recordId && typeof recordId !== 'undefined' && !String(recordId).startsWith('temp_'); + // πŸ”‘ λ ˆμ½”λ“œ λͺ¨λ“œ νŒλ‹¨ κ°œμ„  + const effectiveIsRecordMode = isRecordMode || (effectiveRecordId && !String(effectiveRecordId).startsWith('temp_')); - if (isRealRecord && tableName) { - // μ‹€μ œ 데이터 파일 (μ§„μ§œ λ ˆμ½”λ“œ IDκ°€ μžˆμ„ λ•Œλ§Œ) - targetObjid = `${tableName}:${recordId}:${columnName}`; - console.log("πŸ“ μ‹€μ œ 데이터 파일 μ—…λ‘œλ“œ:", targetObjid); + if (effectiveIsRecordMode && effectiveTableName && effectiveRecordId) { + // 🎯 λ ˆμ½”λ“œ λͺ¨λ“œ: νŠΉμ • 행에 파일 μ—°κ²° + targetObjid = `${effectiveTableName}:${effectiveRecordId}:${effectiveColumnName}`; + console.log("πŸ“ [λ ˆμ½”λ“œ λͺ¨λ“œ] 파일 μ—…λ‘œλ“œ:", { + targetObjid, + tableName: effectiveTableName, + recordId: effectiveRecordId, + columnName: effectiveColumnName, + }); } else if (screenId) { // πŸ”‘ ν…œν”Œλ¦Ώ 파일 (λ°±μ—”λ“œ 쑰회 ν˜•μ‹κ³Ό λ™μΌν•˜κ²Œ) - targetObjid = `screen_files:${screenId}:${component.id}:${columnName}`; + targetObjid = `screen_files:${screenId}:${component.id}:${effectiveColumnName}`; + console.log("πŸ“ [ν…œν”Œλ¦Ώ λͺ¨λ“œ] 파일 μ—…λ‘œλ“œ:", targetObjid); } else { // κΈ°λ³Έκ°’ (ν™”λ©΄κ΄€λ¦¬μ—μ„œ μ‚¬μš©) targetObjid = `temp_${component.id}`; - console.log("πŸ“ κΈ°λ³Έ 파일 μ—…λ‘œλ“œ:", targetObjid); + console.log("πŸ“ [κΈ°λ³Έ λͺ¨λ“œ] 파일 μ—…λ‘œλ“œ:", targetObjid); } // πŸ”’ ν˜„μž¬ μ‚¬μš©μžμ˜ νšŒμ‚¬ μ½”λ“œ κ°€μ Έμ˜€κΈ° (λ©€ν‹°ν…Œλ„Œμ‹œ 격리) - const userCompanyCode = (window as any).__user__?.companyCode; + const userCompanyCode = user?.companyCode || (window as any).__user__?.companyCode; + + console.log("πŸ“€ [FileUploadComponent] 파일 μ—…λ‘œλ“œ μ€€λΉ„:", { + userCompanyCode, + isRecordMode: effectiveIsRecordMode, + tableName: effectiveTableName, + recordId: effectiveRecordId, + columnName: effectiveColumnName, + targetObjid, + }); const uploadData = { // 🎯 formDataμ—μ„œ λ°±μ—”λ“œ API μ„€μ • κ°€μ Έμ˜€κΈ° autoLink: formData?.autoLink || true, - linkedTable: formData?.linkedTable || tableName, - recordId: formData?.recordId || recordId || `temp_${component.id}`, - columnName: formData?.columnName || columnName, + linkedTable: formData?.linkedTable || effectiveTableName, + recordId: effectiveRecordId || `temp_${component.id}`, + columnName: effectiveColumnName, isVirtualFileColumn: formData?.isVirtualFileColumn || true, docType: component.fileConfig?.docType || "DOCUMENT", docTypeName: component.fileConfig?.docTypeName || "일반 λ¬Έμ„œ", companyCode: userCompanyCode, // πŸ”’ λ©€ν‹°ν…Œλ„Œμ‹œ: νšŒμ‚¬ μ½”λ“œ λͺ…μ‹œμ  전달 // ν˜Έν™˜μ„±μ„ μœ„ν•œ κΈ°μ‘΄ ν•„λ“œλ“€ - tableName: tableName, - fieldName: columnName, + tableName: effectiveTableName, + fieldName: effectiveColumnName, targetObjid: targetObjid, // InteractiveDataTable ν˜Έν™˜μ„ μœ„ν•œ targetObjid μΆ”κ°€ + // πŸ†• λ ˆμ½”λ“œ λͺ¨λ“œ ν”Œλž˜κ·Έ + isRecordMode: effectiveIsRecordMode, }; @@ -553,9 +653,9 @@ const FileUploadComponent: React.FC = ({ setUploadedFiles(updatedFiles); setUploadStatus("success"); - // localStorage λ°±μ—… + // localStorage λ°±μ—… (λ ˆμ½”λ“œλ³„ 고유 ν‚€ μ‚¬μš©) try { - const backupKey = `fileUpload_${component.id}`; + const backupKey = getUniqueKey(); localStorage.setItem(backupKey, JSON.stringify(updatedFiles)); } catch (e) { console.warn("localStorage λ°±μ—… μ‹€νŒ¨:", e); @@ -563,9 +663,10 @@ const FileUploadComponent: React.FC = ({ // μ „μ—­ μƒνƒœ μ—…λ°μ΄νŠΈ (λͺ¨λ“  파일 μ»΄ν¬λ„ŒνŠΈ 동기화) if (typeof window !== "undefined") { - // μ „μ—­ 파일 μƒνƒœ μ—…λ°μ΄νŠΈ + // μ „μ—­ 파일 μƒνƒœ μ—…λ°μ΄νŠΈ (λ ˆμ½”λ“œλ³„ 고유 ν‚€ μ‚¬μš©) const globalFileState = (window as any).globalFileState || {}; - globalFileState[component.id] = updatedFiles; + const uniqueKey = getUniqueKey(); + globalFileState[uniqueKey] = updatedFiles; (window as any).globalFileState = globalFileState; // 🌐 μ „μ—­ 파일 μ €μž₯μ†Œμ— μƒˆ 파일 등둝 (νŽ˜μ΄μ§€ κ°„ 곡유용) @@ -573,12 +674,15 @@ const FileUploadComponent: React.FC = ({ uploadPage: window.location.pathname, componentId: component.id, screenId: formData?.screenId, + recordId: recordId, // πŸ†• λ ˆμ½”λ“œ ID μΆ”κ°€ }); // λͺ¨λ“  파일 μ»΄ν¬λ„ŒνŠΈμ— 동기화 이벀트 λ°œμƒ const syncEvent = new CustomEvent("globalFileStateChanged", { detail: { componentId: component.id, + uniqueKey: uniqueKey, // πŸ†• 고유 ν‚€ μΆ”κ°€ + recordId: recordId, // πŸ†• λ ˆμ½”λ“œ ID μΆ”κ°€ files: updatedFiles, fileCount: updatedFiles.length, timestamp: Date.now(), @@ -612,22 +716,54 @@ const FileUploadComponent: React.FC = ({ console.warn("⚠️ onUpdate 콜백이 μ—†μŠ΅λ‹ˆλ‹€!"); } + // πŸ†• λ ˆμ½”λ“œ λͺ¨λ“œ: attachments 컬럼 동기화 (formData μ—…λ°μ΄νŠΈ) + if (effectiveIsRecordMode && onFormDataChange) { + // 파일 정보λ₯Ό κ°„μ†Œν™”ν•˜μ—¬ attachments μ»¬λŸΌμ— μ €μž₯ν•  ν˜•νƒœλ‘œ λ³€ν™˜ + const attachmentsData = updatedFiles.map(file => ({ + objid: file.objid, + realFileName: file.realFileName, + fileSize: file.fileSize, + fileExt: file.fileExt, + filePath: file.filePath, + regdate: file.regdate || new Date().toISOString(), + })); + + console.log("πŸ“Ž [λ ˆμ½”λ“œ λͺ¨λ“œ] attachments 컬럼 동기화:", { + tableName: effectiveTableName, + recordId: effectiveRecordId, + columnName: effectiveColumnName, + fileCount: attachmentsData.length, + }); + + // onFormDataChangeλ₯Ό 톡해 λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ— attachments μ—…λ°μ΄νŠΈ μ•Œλ¦Ό + onFormDataChange({ + [effectiveColumnName]: attachmentsData, + // πŸ†• λ°±μ—”λ“œμ—μ„œ attachments 컬럼 μ—…λ°μ΄νŠΈλ₯Ό μœ„ν•œ 메타 정보 + __attachmentsUpdate: { + tableName: effectiveTableName, + recordId: effectiveRecordId, + columnName: effectiveColumnName, + files: attachmentsData, + } + }); + } + // κ·Έλ¦¬λ“œ 파일 μƒνƒœ μƒˆλ‘œκ³ μΉ¨ 이벀트 λ°œμƒ if (typeof window !== "undefined") { const refreshEvent = new CustomEvent("refreshFileStatus", { detail: { - tableName: tableName, - recordId: recordId, - columnName: columnName, + tableName: effectiveTableName, + recordId: effectiveRecordId, + columnName: effectiveColumnName, targetObjid: targetObjid, fileCount: updatedFiles.length, }, }); window.dispatchEvent(refreshEvent); console.log("πŸ”„ κ·Έλ¦¬λ“œ 파일 μƒνƒœ μƒˆλ‘œκ³ μΉ¨ 이벀트 λ°œμƒ:", { - tableName, - recordId, - columnName, + tableName: effectiveTableName, + recordId: effectiveRecordId, + columnName: effectiveColumnName, targetObjid, fileCount: updatedFiles.length, }); @@ -705,9 +841,9 @@ const FileUploadComponent: React.FC = ({ const updatedFiles = uploadedFiles.filter((f) => f.objid !== fileId); setUploadedFiles(updatedFiles); - // localStorage λ°±μ—… μ—…λ°μ΄νŠΈ + // localStorage λ°±μ—… μ—…λ°μ΄νŠΈ (λ ˆμ½”λ“œλ³„ 고유 ν‚€ μ‚¬μš©) try { - const backupKey = `fileUpload_${component.id}`; + const backupKey = getUniqueKey(); localStorage.setItem(backupKey, JSON.stringify(updatedFiles)); } catch (e) { console.warn("localStorage λ°±μ—… μ—…λ°μ΄νŠΈ μ‹€νŒ¨:", e); @@ -715,15 +851,18 @@ const FileUploadComponent: React.FC = ({ // μ „μ—­ μƒνƒœ μ—…λ°μ΄νŠΈ (λͺ¨λ“  파일 μ»΄ν¬λ„ŒνŠΈ 동기화) if (typeof window !== "undefined") { - // μ „μ—­ 파일 μƒνƒœ μ—…λ°μ΄νŠΈ + // μ „μ—­ 파일 μƒνƒœ μ—…λ°μ΄νŠΈ (λ ˆμ½”λ“œλ³„ 고유 ν‚€ μ‚¬μš©) const globalFileState = (window as any).globalFileState || {}; - globalFileState[component.id] = updatedFiles; + const uniqueKey = getUniqueKey(); + globalFileState[uniqueKey] = updatedFiles; (window as any).globalFileState = globalFileState; // λͺ¨λ“  파일 μ»΄ν¬λ„ŒνŠΈμ— 동기화 이벀트 λ°œμƒ const syncEvent = new CustomEvent("globalFileStateChanged", { detail: { componentId: component.id, + uniqueKey: uniqueKey, // πŸ†• 고유 ν‚€ μΆ”κ°€ + recordId: recordId, // πŸ†• λ ˆμ½”λ“œ ID μΆ”κ°€ files: updatedFiles, fileCount: updatedFiles.length, timestamp: Date.now(), @@ -749,13 +888,42 @@ const FileUploadComponent: React.FC = ({ }); } + // πŸ†• λ ˆμ½”λ“œ λͺ¨λ“œ: attachments 컬럼 동기화 (파일 μ‚­μ œ ν›„) + if (isRecordMode && onFormDataChange && recordTableName && recordId) { + const attachmentsData = updatedFiles.map(f => ({ + objid: f.objid, + realFileName: f.realFileName, + fileSize: f.fileSize, + fileExt: f.fileExt, + filePath: f.filePath, + regdate: f.regdate || new Date().toISOString(), + })); + + console.log("πŸ“Ž [λ ˆμ½”λ“œ λͺ¨λ“œ] 파일 μ‚­μ œ ν›„ attachments 동기화:", { + tableName: recordTableName, + recordId: recordId, + columnName: columnName, + remainingFiles: attachmentsData.length, + }); + + onFormDataChange({ + [columnName]: attachmentsData, + __attachmentsUpdate: { + tableName: recordTableName, + recordId: recordId, + columnName: columnName, + files: attachmentsData, + } + }); + } + toast.success(`${fileName} μ‚­μ œ μ™„λ£Œ`); } catch (error) { console.error("파일 μ‚­μ œ 였λ₯˜:", error); toast.error("파일 μ‚­μ œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); } }, - [uploadedFiles, onUpdate, component.id], + [uploadedFiles, onUpdate, component.id, isRecordMode, onFormDataChange, recordTableName, recordId, columnName, getUniqueKey], ); // λŒ€ν‘œ 이미지 Blob URL λ‘œλ“œ diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 9fec8fc5..f68b8383 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -3970,6 +3970,35 @@ export const TableListComponent: React.FC = ({ ); } + // πŸ“Ž μ²¨λΆ€νŒŒμΌ νƒ€μž…: 파일 μ•„μ΄μ½˜κ³Ό 개수 ν‘œμ‹œ + if (inputType === "file" || inputType === "attachment" || column.columnName === "attachments") { + // JSONB λ°°μ—΄ λ˜λŠ” JSON λ¬Έμžμ—΄ νŒŒμ‹± + let files: any[] = []; + try { + if (typeof value === "string") { + files = JSON.parse(value); + } else if (Array.isArray(value)) { + files = value; + } + } catch { + // νŒŒμ‹± μ‹€νŒ¨ μ‹œ 빈 λ°°μ—΄ + } + + if (!files || files.length === 0) { + return -; + } + + // 파일 κ°œμˆ˜μ™€ μ•„μ΄μ½˜ ν‘œμ‹œ + const { Paperclip } = require("lucide-react"); + return ( +
+ + {files.length} + 개 +
+ ); + } + // μΉ΄ν…Œκ³ λ¦¬ νƒ€μž…: λ°°μ§€λ‘œ ν‘œμ‹œ (λ°°μ§€ μ—†μŒ μ˜΅μ…˜ 지원, 닀쀑 κ°’ 지원) if (inputType === "category") { if (!value) return "";