diff --git a/backend-node/src/controllers/fileController.ts b/backend-node/src/controllers/fileController.ts index b1e31e3b..d4e8d0cf 100644 --- a/backend-node/src/controllers/fileController.ts +++ b/backend-node/src/controllers/fileController.ts @@ -341,6 +341,64 @@ export const uploadFiles = async ( }); } + // πŸ†• λ ˆμ½”λ“œ λͺ¨λ“œ: ν•΄λ‹Ή ν–‰μ˜ attachments 컬럼 μžλ™ μ—…λ°μ΄νŠΈ + const isRecordMode = req.body.isRecordMode === "true" || req.body.isRecordMode === true; + + // πŸ” 디버깅: λ ˆμ½”λ“œ λͺ¨λ“œ 쑰건 확인 + console.log("πŸ” [파일 μ—…λ‘œλ“œ] λ ˆμ½”λ“œ λͺ¨λ“œ 쑰건 확인:", { + isRecordMode, + linkedTable, + recordId, + columnName, + finalTargetObjid, + "req.body.isRecordMode": req.body.isRecordMode, + "req.body.linkedTable": req.body.linkedTable, + "req.body.recordId": req.body.recordId, + "req.body.columnName": req.body.columnName, + }); + + 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 +463,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/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 77593fa1..65efcd1b 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -903,6 +903,9 @@ export class DynamicFormService { return `${key} = $${index + 1}::numeric`; } else if (dataType === "boolean") { return `${key} = $${index + 1}::boolean`; + } else if (dataType === 'jsonb' || dataType === 'json') { + // πŸ†• JSONB/JSON νƒ€μž…μ€ λͺ…μ‹œμ  μΊμŠ€νŒ… + return `${key} = $${index + 1}::jsonb`; } else { // λ¬Έμžμ—΄ νƒ€μž…μ€ μΊμŠ€νŒ… λΆˆν•„μš” return `${key} = $${index + 1}`; @@ -910,7 +913,17 @@ export class DynamicFormService { }) .join(", "); - const values: any[] = Object.values(changedFields); + // πŸ†• JSONB νƒ€μž… 값은 JSON λ¬Έμžμ—΄λ‘œ λ³€ν™˜ + const values: any[] = Object.keys(changedFields).map((key) => { + const value = changedFields[key]; + const dataType = columnTypes[key]; + + // JSONB/JSON νƒ€μž…μ΄κ³  λ°°μ—΄/객체인 경우 JSON λ¬Έμžμ—΄λ‘œ λ³€ν™˜ + if ((dataType === 'jsonb' || dataType === 'json') && (Array.isArray(value) || (typeof value === 'object' && value !== null))) { + return JSON.stringify(value); + } + return value; + }); values.push(id); // WHERE 쑰건용 ID μΆ”κ°€ // πŸ”‘ Primary Key νƒ€μž…μ— 맞게 μΊμŠ€νŒ… diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 2a3050fc..0a87db3f 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -678,12 +678,13 @@ export const EditModal: React.FC = ({ className }) => { } // ν™”λ©΄κ΄€λ¦¬μ—μ„œ μ„€μ •ν•œ 크기 = 컨텐츠 μ˜μ—­ 크기 - // μ‹€μ œ λͺ¨λ‹¬ 크기 = 컨텐츠 + 헀더 + gap + padding + // μ‹€μ œ λͺ¨λ‹¬ 크기 = 컨텐츠 + 헀더 + gap + padding + 라벨 곡간 const headerHeight = 52; // DialogHeader (타이틀 + border-b + py-3) const dialogGap = 16; // DialogContent gap-4 const extraPadding = 24; // μΆ”κ°€ μ—¬λ°± (μ•ˆμ „ λ§ˆμ§„) + const labelSpace = 30; // μž…λ ₯ ν•„λ“œ μœ„ 라벨 곡간 (-top-6 = 24px + μ—¬μœ ) - const totalHeight = screenDimensions.height + headerHeight + dialogGap + extraPadding; + const totalHeight = screenDimensions.height + headerHeight + dialogGap + extraPadding + labelSpace; return { className: "overflow-hidden p-0", @@ -729,7 +730,7 @@ export const EditModal: React.FC = ({ className }) => { className="relative bg-white" style={{ width: screenDimensions?.width || 800, - height: screenDimensions?.height || 600, + height: (screenDimensions?.height || 600) + 30, // 라벨 곡간 μΆ”κ°€ transformOrigin: "center center", maxWidth: "100%", maxHeight: "100%", @@ -739,13 +740,14 @@ export const EditModal: React.FC = ({ className }) => { // μ»΄ν¬λ„ŒνŠΈ μœ„μΉ˜λ₯Ό offset만큼 μ‘°μ • const offsetX = screenDimensions?.offsetX || 0; const offsetY = screenDimensions?.offsetY || 0; + const labelSpace = 30; // 라벨 곡간 (μž…λ ₯ ν•„λ“œ μœ„ -top-6 라벨용) const adjustedComponent = { ...component, position: { ...component.position, x: parseFloat(component.position?.x?.toString() || "0") - offsetX, - y: parseFloat(component.position?.y?.toString() || "0") - offsetY, + y: parseFloat(component.position?.y?.toString() || "0") - offsetY + labelSpace, // 라벨 곡간 μΆ”κ°€ }, }; @@ -759,12 +761,27 @@ export const EditModal: React.FC = ({ className }) => { }); } + // πŸ”‘ μ²¨λΆ€νŒŒμΌ μ»΄ν¬λ„ŒνŠΈκ°€ ν–‰(λ ˆμ½”λ“œ) λ‹¨μœ„λ‘œ νŒŒμΌμ„ μ €μž₯ν•  수 μžˆλ„λ‘ tableName μΆ”κ°€ + const enrichedFormData = { + ...(groupData.length > 0 ? groupData[0] : formData), + tableName: screenData.screenInfo?.tableName, // ν…Œμ΄λΈ”λͺ… μΆ”κ°€ + screenId: modalState.screenId, // ν™”λ©΄ ID μΆ”κ°€ + }; + + // πŸ” 디버깅: enrichedFormData 확인 + console.log("πŸ”‘ [EditModal] enrichedFormData 생성:", { + "screenData.screenInfo": screenData.screenInfo, + "screenData.screenInfo?.tableName": screenData.screenInfo?.tableName, + "enrichedFormData.tableName": enrichedFormData.tableName, + "enrichedFormData.id": enrichedFormData.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..805fe755 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,94 @@ 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; + // πŸ”‘ 컬럼λͺ… κ²°μ •: λ ˆμ½”λ“œ λͺ¨λ“œμ—μ„œλŠ” 무쑰건 'attachments' μ‚¬μš© + // component.columnNameμ΄λ‚˜ component.idλŠ” '파일_μ—…λ‘œλ“œ' 같은 ν•œκΈ€ 라벨일 수 μžˆμ–΄μ„œ DB 컬럼λͺ…μœΌλ‘œ 뢀적합 + // λ ˆμ½”λ“œ λͺ¨λ“œκ°€ 아닐 λ•Œλ§Œ component.columnName λ˜λŠ” component.id μ‚¬μš© + const columnName = isRecordMode ? 'attachments' : (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) : [], + // πŸ” μΆ”κ°€ 디버깅: μ–΄λ””μ„œ tableName이 μ˜€λŠ”μ§€ 확인 + "formData.tableName": formData?.tableName, + "component.tableName": component.tableName, + "component.columnName": component.columnName, + "component.id": component.id, + }); + }, [isRecordMode, recordTableName, recordId, columnName, getRecordTargetObjid, getUniqueKey, formData, component.tableName, component.columnName, component.id]); + + // πŸ†• λ ˆμ½”λ“œ 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 +201,7 @@ const FileUploadComponent: React.FC = ({ } catch (e) { console.warn("μ»΄ν¬λ„ŒνŠΈ 마운트 μ‹œ 파일 볡원 μ‹€νŒ¨:", e); } - }, [component.id]); // component.idκ°€ 변경될 λ•Œλ§Œ μ‹€ν–‰ + }, [component.id, getUniqueKey, recordId]); // λ ˆμ½”λ“œλ³„ 고유 ν‚€ λ³€κ²½ μ‹œ μž¬μ‹€ν–‰ // 🎯 화면섀계 λͺ¨λ“œμ—μ„œ μ‹€μ œ ν™”λ©΄μœΌλ‘œμ˜ μ‹€μ‹œκ°„ 동기화 이벀트 λ¦¬μŠ€λ„ˆ useEffect(() => { @@ -152,12 +222,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 +273,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 +314,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 +339,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 +352,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 +365,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 +377,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 +393,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 +405,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 +462,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 +553,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 +569,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 +661,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 +671,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 +682,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 +724,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 +849,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 +859,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 +896,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 "";