파일업로드 수정

This commit is contained in:
leeheejin 2025-12-10 18:38:16 +09:00
parent fa6c00b6be
commit d09c8e0787
4 changed files with 353 additions and 55 deletions

View File

@ -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<any>(
`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({ res.json({
success: true, success: true,
message: `${files.length}개 파일 업로드 완료`, message: `${files.length}개 파일 업로드 완료`,
@ -405,6 +449,56 @@ export const deleteFile = async (
["DELETED", parseInt(objid)] ["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<any>(
`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({ res.json({
success: true, success: true,
message: "파일이 삭제되었습니다.", message: "파일이 삭제되었습니다.",

View File

@ -761,12 +761,19 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
}); });
} }
// 🔑 첨부파일 컴포넌트가 행(레코드) 단위로 파일을 저장할 수 있도록 tableName 추가
const enrichedFormData = {
...(groupData.length > 0 ? groupData[0] : formData),
tableName: screenData.screenInfo?.tableName, // 테이블명 추가
screenId: modalState.screenId, // 화면 ID 추가
};
return ( return (
<InteractiveScreenViewerDynamic <InteractiveScreenViewerDynamic
key={component.id} key={component.id}
component={adjustedComponent} component={adjustedComponent}
allComponents={screenData.components} allComponents={screenData.components}
formData={groupData.length > 0 ? groupData[0] : formData} formData={enrichedFormData}
onFormDataChange={(fieldName, value) => { onFormDataChange={(fieldName, value) => {
// 🆕 그룹 데이터가 있으면 처리 // 🆕 그룹 데이터가 있으면 처리
if (groupData.length > 0) { if (groupData.length > 0) {

View File

@ -10,6 +10,7 @@ import { apiClient } from "@/lib/api/client";
import { FileViewerModal } from "./FileViewerModal"; import { FileViewerModal } from "./FileViewerModal";
import { FileManagerModal } from "./FileManagerModal"; import { FileManagerModal } from "./FileManagerModal";
import { FileUploadConfig, FileInfo, FileUploadStatus, FileUploadResponse } from "./types"; import { FileUploadConfig, FileInfo, FileUploadStatus, FileUploadResponse } from "./types";
import { useAuth } from "@/hooks/useAuth";
import { import {
Upload, Upload,
File, File,
@ -92,6 +93,9 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
onDragEnd, onDragEnd,
onUpdate, onUpdate,
}) => { }) => {
// 🔑 인증 정보 가져오기
const { user } = useAuth();
const [uploadedFiles, setUploadedFiles] = useState<FileInfo[]>([]); const [uploadedFiles, setUploadedFiles] = useState<FileInfo[]>([]);
const [uploadStatus, setUploadStatus] = useState<FileUploadStatus>("idle"); const [uploadStatus, setUploadStatus] = useState<FileUploadStatus>("idle");
const [dragOver, setDragOver] = useState(false); const [dragOver, setDragOver] = useState(false);
@ -102,28 +106,86 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
const [representativeImageUrl, setRepresentativeImageUrl] = useState<string | null>(null); const [representativeImageUrl, setRepresentativeImageUrl] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(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<any>(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에서 파일 복원 // 컴포넌트 마운트 시 즉시 localStorage에서 파일 복원
useEffect(() => { useEffect(() => {
if (!component?.id) return; if (!component?.id) return;
try { try {
const backupKey = `fileUpload_${component.id}`; // 🔑 레코드별 고유 키 사용
const backupKey = getUniqueKey();
const backupFiles = localStorage.getItem(backupKey); const backupFiles = localStorage.getItem(backupKey);
if (backupFiles) { if (backupFiles) {
const parsedFiles = JSON.parse(backupFiles); const parsedFiles = JSON.parse(backupFiles);
if (parsedFiles.length > 0) { if (parsedFiles.length > 0) {
console.log("🚀 컴포넌트 마운트 시 파일 즉시 복원:", { console.log("🚀 컴포넌트 마운트 시 파일 즉시 복원:", {
uniqueKey: backupKey,
componentId: component.id, componentId: component.id,
recordId: recordId,
restoredFiles: parsedFiles.length, restoredFiles: parsedFiles.length,
files: parsedFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })), files: parsedFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })),
}); });
setUploadedFiles(parsedFiles); setUploadedFiles(parsedFiles);
// 전역 상태에도 복원 // 전역 상태에도 복원 (레코드별 고유 키 사용)
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
(window as any).globalFileState = { (window as any).globalFileState = {
...(window as any).globalFileState, ...(window as any).globalFileState,
[component.id]: parsedFiles, [backupKey]: parsedFiles,
}; };
} }
} }
@ -131,7 +193,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
} catch (e) { } catch (e) {
console.warn("컴포넌트 마운트 시 파일 복원 실패:", e); console.warn("컴포넌트 마운트 시 파일 복원 실패:", e);
} }
}, [component.id]); // component.id가 변경될 때만 실행 }, [component.id, getUniqueKey, recordId]); // 레코드별 고유 키 변경 시 재실행
// 🎯 화면설계 모드에서 실제 화면으로의 실시간 동기화 이벤트 리스너 // 🎯 화면설계 모드에서 실제 화면으로의 실시간 동기화 이벤트 리스너
useEffect(() => { useEffect(() => {
@ -152,12 +214,14 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
const newFiles = event.detail.files || []; const newFiles = event.detail.files || [];
setUploadedFiles(newFiles); setUploadedFiles(newFiles);
// localStorage 백업 업데이트 // localStorage 백업 업데이트 (레코드별 고유 키 사용)
try { try {
const backupKey = `fileUpload_${component.id}`; const backupKey = getUniqueKey();
localStorage.setItem(backupKey, JSON.stringify(newFiles)); localStorage.setItem(backupKey, JSON.stringify(newFiles));
console.log("💾 화면설계 모드 동기화 후 localStorage 백업 업데이트:", { console.log("💾 화면설계 모드 동기화 후 localStorage 백업 업데이트:", {
uniqueKey: backupKey,
componentId: component.id, componentId: component.id,
recordId: recordId,
fileCount: newFiles.length, fileCount: newFiles.length,
}); });
} catch (e) { } catch (e) {
@ -201,6 +265,16 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
if (!component?.id) return false; if (!component?.id) return false;
try { try {
// 🔑 레코드 모드: 해당 행의 파일만 조회
if (isRecordMode && recordTableName && recordId) {
console.log("📂 [FileUploadComponent] 레코드 모드 파일 조회:", {
tableName: recordTableName,
recordId: recordId,
columnName: columnName,
targetObjid: getRecordTargetObjid(),
});
}
// 1. formData에서 screenId 가져오기 // 1. formData에서 screenId 가져오기
let screenId = formData?.screenId; let screenId = formData?.screenId;
@ -232,11 +306,13 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
const params = { const params = {
screenId, screenId,
componentId: component.id, componentId: component.id,
tableName: formData?.tableName || component.tableName, tableName: recordTableName || formData?.tableName || component.tableName,
recordId: formData?.id, recordId: recordId || formData?.id,
columnName: component.columnName || component.id, // 🔑 columnName이 없으면 component.id 사용 columnName: columnName, // 🔑 레코드 모드에서 사용하는 columnName
}; };
console.log("📂 [FileUploadComponent] 파일 조회 파라미터:", params);
const response = await getComponentFiles(params); const response = await getComponentFiles(params);
if (response.success) { if (response.success) {
@ -255,11 +331,11 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
})); }));
// 🔄 localStorage의 기존 파일과 서버 파일 병합 // 🔄 localStorage의 기존 파일과 서버 파일 병합 (레코드별 고유 키 사용)
let finalFiles = formattedFiles; let finalFiles = formattedFiles;
const uniqueKey = getUniqueKey();
try { try {
const backupKey = `fileUpload_${component.id}`; const backupFiles = localStorage.getItem(uniqueKey);
const backupFiles = localStorage.getItem(backupKey);
if (backupFiles) { if (backupFiles) {
const parsedBackupFiles = JSON.parse(backupFiles); const parsedBackupFiles = JSON.parse(backupFiles);
@ -268,7 +344,12 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
const additionalFiles = parsedBackupFiles.filter((f: any) => !serverObjIds.has(f.objid)); const additionalFiles = parsedBackupFiles.filter((f: any) => !serverObjIds.has(f.objid));
finalFiles = [...formattedFiles, ...additionalFiles]; finalFiles = [...formattedFiles, ...additionalFiles];
console.log("📂 [FileUploadComponent] 파일 병합 완료:", {
uniqueKey,
serverFiles: formattedFiles.length,
localFiles: parsedBackupFiles.length,
finalFiles: finalFiles.length,
});
} }
} catch (e) { } catch (e) {
console.warn("파일 병합 중 오류:", e); console.warn("파일 병합 중 오류:", e);
@ -276,11 +357,11 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
setUploadedFiles(finalFiles); setUploadedFiles(finalFiles);
// 전역 상태에도 저장 // 전역 상태에도 저장 (레코드별 고유 키 사용)
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
(window as any).globalFileState = { (window as any).globalFileState = {
...(window as any).globalFileState, ...(window as any).globalFileState,
[component.id]: finalFiles, [uniqueKey]: finalFiles,
}; };
// 🌐 전역 파일 저장소에 등록 (페이지 간 공유용) // 🌐 전역 파일 저장소에 등록 (페이지 간 공유용)
@ -288,12 +369,12 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
uploadPage: window.location.pathname, uploadPage: window.location.pathname,
componentId: component.id, componentId: component.id,
screenId: formData?.screenId, screenId: formData?.screenId,
recordId: recordId,
}); });
// localStorage 백업도 병합된 파일로 업데이트 // localStorage 백업도 병합된 파일로 업데이트 (레코드별 고유 키 사용)
try { try {
const backupKey = `fileUpload_${component.id}`; localStorage.setItem(uniqueKey, JSON.stringify(finalFiles));
localStorage.setItem(backupKey, JSON.stringify(finalFiles));
} catch (e) { } catch (e) {
console.warn("localStorage 백업 업데이트 실패:", e); console.warn("localStorage 백업 업데이트 실패:", e);
} }
@ -304,7 +385,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
console.error("파일 조회 오류:", error); console.error("파일 조회 오류:", error);
} }
return false; // 기존 로직 사용 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는 보조) // 컴포넌트 파일 동기화 (DB 우선, localStorage는 보조)
useEffect(() => { useEffect(() => {
@ -316,6 +397,8 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
componentFiles: componentFiles.length, componentFiles: componentFiles.length,
formData: formData, formData: formData,
screenId: formData?.screenId, screenId: formData?.screenId,
tableName: formData?.tableName, // 🔍 테이블명 확인
recordId: formData?.id, // 🔍 레코드 ID 확인
currentUploadedFiles: uploadedFiles.length, currentUploadedFiles: uploadedFiles.length,
}); });
@ -371,9 +454,9 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
setUploadedFiles(files); setUploadedFiles(files);
setForceUpdate((prev) => prev + 1); setForceUpdate((prev) => prev + 1);
// localStorage 백업도 업데이트 // localStorage 백업도 업데이트 (레코드별 고유 키 사용)
try { try {
const backupKey = `fileUpload_${component.id}`; const backupKey = getUniqueKey();
localStorage.setItem(backupKey, JSON.stringify(files)); localStorage.setItem(backupKey, JSON.stringify(files));
} catch (e) { } catch (e) {
console.warn("localStorage 백업 실패:", e); console.warn("localStorage 백업 실패:", e);
@ -462,10 +545,10 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
toast.loading("파일을 업로드하는 중...", { id: "file-upload" }); toast.loading("파일을 업로드하는 중...", { id: "file-upload" });
try { try {
// targetObjid 생성 - 템플릿 vs 데이터 파일 구분 // 🔑 레코드 모드 우선 사용
const tableName = formData?.tableName || component.tableName || "default_table"; const effectiveTableName = recordTableName || formData?.tableName || component.tableName || "default_table";
const recordId = formData?.id; const effectiveRecordId = recordId || formData?.id;
const columnName = component.columnName || component.id; const effectiveColumnName = columnName;
// screenId 추출 (우선순위: formData > URL) // screenId 추출 (우선순위: formData > URL)
let screenId = formData?.screenId; let screenId = formData?.screenId;
@ -478,39 +561,56 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
} }
let targetObjid; 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) { if (effectiveIsRecordMode && effectiveTableName && effectiveRecordId) {
// 실제 데이터 파일 (진짜 레코드 ID가 있을 때만) // 🎯 레코드 모드: 특정 행에 파일 연결
targetObjid = `${tableName}:${recordId}:${columnName}`; targetObjid = `${effectiveTableName}:${effectiveRecordId}:${effectiveColumnName}`;
console.log("📁 실제 데이터 파일 업로드:", targetObjid); console.log("📁 [레코드 모드] 파일 업로드:", {
targetObjid,
tableName: effectiveTableName,
recordId: effectiveRecordId,
columnName: effectiveColumnName,
});
} else if (screenId) { } else if (screenId) {
// 🔑 템플릿 파일 (백엔드 조회 형식과 동일하게) // 🔑 템플릿 파일 (백엔드 조회 형식과 동일하게)
targetObjid = `screen_files:${screenId}:${component.id}:${columnName}`; targetObjid = `screen_files:${screenId}:${component.id}:${effectiveColumnName}`;
console.log("📝 [템플릿 모드] 파일 업로드:", targetObjid);
} else { } else {
// 기본값 (화면관리에서 사용) // 기본값 (화면관리에서 사용)
targetObjid = `temp_${component.id}`; 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 = { const uploadData = {
// 🎯 formData에서 백엔드 API 설정 가져오기 // 🎯 formData에서 백엔드 API 설정 가져오기
autoLink: formData?.autoLink || true, autoLink: formData?.autoLink || true,
linkedTable: formData?.linkedTable || tableName, linkedTable: formData?.linkedTable || effectiveTableName,
recordId: formData?.recordId || recordId || `temp_${component.id}`, recordId: effectiveRecordId || `temp_${component.id}`,
columnName: formData?.columnName || columnName, columnName: effectiveColumnName,
isVirtualFileColumn: formData?.isVirtualFileColumn || true, isVirtualFileColumn: formData?.isVirtualFileColumn || true,
docType: component.fileConfig?.docType || "DOCUMENT", docType: component.fileConfig?.docType || "DOCUMENT",
docTypeName: component.fileConfig?.docTypeName || "일반 문서", docTypeName: component.fileConfig?.docTypeName || "일반 문서",
companyCode: userCompanyCode, // 🔒 멀티테넌시: 회사 코드 명시적 전달 companyCode: userCompanyCode, // 🔒 멀티테넌시: 회사 코드 명시적 전달
// 호환성을 위한 기존 필드들 // 호환성을 위한 기존 필드들
tableName: tableName, tableName: effectiveTableName,
fieldName: columnName, fieldName: effectiveColumnName,
targetObjid: targetObjid, // InteractiveDataTable 호환을 위한 targetObjid 추가 targetObjid: targetObjid, // InteractiveDataTable 호환을 위한 targetObjid 추가
// 🆕 레코드 모드 플래그
isRecordMode: effectiveIsRecordMode,
}; };
@ -553,9 +653,9 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
setUploadedFiles(updatedFiles); setUploadedFiles(updatedFiles);
setUploadStatus("success"); setUploadStatus("success");
// localStorage 백업 // localStorage 백업 (레코드별 고유 키 사용)
try { try {
const backupKey = `fileUpload_${component.id}`; const backupKey = getUniqueKey();
localStorage.setItem(backupKey, JSON.stringify(updatedFiles)); localStorage.setItem(backupKey, JSON.stringify(updatedFiles));
} catch (e) { } catch (e) {
console.warn("localStorage 백업 실패:", e); console.warn("localStorage 백업 실패:", e);
@ -563,9 +663,10 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
// 전역 상태 업데이트 (모든 파일 컴포넌트 동기화) // 전역 상태 업데이트 (모든 파일 컴포넌트 동기화)
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
// 전역 파일 상태 업데이트 // 전역 파일 상태 업데이트 (레코드별 고유 키 사용)
const globalFileState = (window as any).globalFileState || {}; const globalFileState = (window as any).globalFileState || {};
globalFileState[component.id] = updatedFiles; const uniqueKey = getUniqueKey();
globalFileState[uniqueKey] = updatedFiles;
(window as any).globalFileState = globalFileState; (window as any).globalFileState = globalFileState;
// 🌐 전역 파일 저장소에 새 파일 등록 (페이지 간 공유용) // 🌐 전역 파일 저장소에 새 파일 등록 (페이지 간 공유용)
@ -573,12 +674,15 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
uploadPage: window.location.pathname, uploadPage: window.location.pathname,
componentId: component.id, componentId: component.id,
screenId: formData?.screenId, screenId: formData?.screenId,
recordId: recordId, // 🆕 레코드 ID 추가
}); });
// 모든 파일 컴포넌트에 동기화 이벤트 발생 // 모든 파일 컴포넌트에 동기화 이벤트 발생
const syncEvent = new CustomEvent("globalFileStateChanged", { const syncEvent = new CustomEvent("globalFileStateChanged", {
detail: { detail: {
componentId: component.id, componentId: component.id,
uniqueKey: uniqueKey, // 🆕 고유 키 추가
recordId: recordId, // 🆕 레코드 ID 추가
files: updatedFiles, files: updatedFiles,
fileCount: updatedFiles.length, fileCount: updatedFiles.length,
timestamp: Date.now(), timestamp: Date.now(),
@ -612,22 +716,54 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
console.warn("⚠️ onUpdate 콜백이 없습니다!"); 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") { if (typeof window !== "undefined") {
const refreshEvent = new CustomEvent("refreshFileStatus", { const refreshEvent = new CustomEvent("refreshFileStatus", {
detail: { detail: {
tableName: tableName, tableName: effectiveTableName,
recordId: recordId, recordId: effectiveRecordId,
columnName: columnName, columnName: effectiveColumnName,
targetObjid: targetObjid, targetObjid: targetObjid,
fileCount: updatedFiles.length, fileCount: updatedFiles.length,
}, },
}); });
window.dispatchEvent(refreshEvent); window.dispatchEvent(refreshEvent);
console.log("🔄 그리드 파일 상태 새로고침 이벤트 발생:", { console.log("🔄 그리드 파일 상태 새로고침 이벤트 발생:", {
tableName, tableName: effectiveTableName,
recordId, recordId: effectiveRecordId,
columnName, columnName: effectiveColumnName,
targetObjid, targetObjid,
fileCount: updatedFiles.length, fileCount: updatedFiles.length,
}); });
@ -705,9 +841,9 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
const updatedFiles = uploadedFiles.filter((f) => f.objid !== fileId); const updatedFiles = uploadedFiles.filter((f) => f.objid !== fileId);
setUploadedFiles(updatedFiles); setUploadedFiles(updatedFiles);
// localStorage 백업 업데이트 // localStorage 백업 업데이트 (레코드별 고유 키 사용)
try { try {
const backupKey = `fileUpload_${component.id}`; const backupKey = getUniqueKey();
localStorage.setItem(backupKey, JSON.stringify(updatedFiles)); localStorage.setItem(backupKey, JSON.stringify(updatedFiles));
} catch (e) { } catch (e) {
console.warn("localStorage 백업 업데이트 실패:", e); console.warn("localStorage 백업 업데이트 실패:", e);
@ -715,15 +851,18 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
// 전역 상태 업데이트 (모든 파일 컴포넌트 동기화) // 전역 상태 업데이트 (모든 파일 컴포넌트 동기화)
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
// 전역 파일 상태 업데이트 // 전역 파일 상태 업데이트 (레코드별 고유 키 사용)
const globalFileState = (window as any).globalFileState || {}; const globalFileState = (window as any).globalFileState || {};
globalFileState[component.id] = updatedFiles; const uniqueKey = getUniqueKey();
globalFileState[uniqueKey] = updatedFiles;
(window as any).globalFileState = globalFileState; (window as any).globalFileState = globalFileState;
// 모든 파일 컴포넌트에 동기화 이벤트 발생 // 모든 파일 컴포넌트에 동기화 이벤트 발생
const syncEvent = new CustomEvent("globalFileStateChanged", { const syncEvent = new CustomEvent("globalFileStateChanged", {
detail: { detail: {
componentId: component.id, componentId: component.id,
uniqueKey: uniqueKey, // 🆕 고유 키 추가
recordId: recordId, // 🆕 레코드 ID 추가
files: updatedFiles, files: updatedFiles,
fileCount: updatedFiles.length, fileCount: updatedFiles.length,
timestamp: Date.now(), timestamp: Date.now(),
@ -749,13 +888,42 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
}); });
} }
// 🆕 레코드 모드: 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} 삭제 완료`); toast.success(`${fileName} 삭제 완료`);
} catch (error) { } catch (error) {
console.error("파일 삭제 오류:", error); console.error("파일 삭제 오류:", error);
toast.error("파일 삭제에 실패했습니다."); toast.error("파일 삭제에 실패했습니다.");
} }
}, },
[uploadedFiles, onUpdate, component.id], [uploadedFiles, onUpdate, component.id, isRecordMode, onFormDataChange, recordTableName, recordId, columnName, getUniqueKey],
); );
// 대표 이미지 Blob URL 로드 // 대표 이미지 Blob URL 로드

View File

@ -3970,6 +3970,35 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
); );
} }
// 📎 첨부파일 타입: 파일 아이콘과 개수 표시
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 <span className="text-muted-foreground text-xs">-</span>;
}
// 파일 개수와 아이콘 표시
const { Paperclip } = require("lucide-react");
return (
<div className="flex items-center gap-1 text-sm">
<Paperclip className="h-4 w-4 text-gray-500" />
<span className="text-blue-600 font-medium">{files.length}</span>
<span className="text-muted-foreground text-xs"></span>
</div>
);
}
// 카테고리 타입: 배지로 표시 (배지 없음 옵션 지원, 다중 값 지원) // 카테고리 타입: 배지로 표시 (배지 없음 옵션 지원, 다중 값 지원)
if (inputType === "category") { if (inputType === "category") {
if (!value) return ""; if (!value) return "";