lhj #277
|
|
@ -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<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({
|
||||
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<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({
|
||||
success: true,
|
||||
message: "파일이 삭제되었습니다.",
|
||||
|
|
|
|||
|
|
@ -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 타입에 맞게 캐스팅
|
||||
|
|
|
|||
|
|
@ -678,12 +678,13 @@ export const EditModal: React.FC<EditModalProps> = ({ 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<EditModalProps> = ({ 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<EditModalProps> = ({ 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<EditModalProps> = ({ 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 (
|
||||
<InteractiveScreenViewerDynamic
|
||||
key={component.id}
|
||||
component={adjustedComponent}
|
||||
allComponents={screenData.components}
|
||||
formData={groupData.length > 0 ? groupData[0] : formData}
|
||||
formData={enrichedFormData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
// 🆕 그룹 데이터가 있으면 처리
|
||||
if (groupData.length > 0) {
|
||||
|
|
|
|||
|
|
@ -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<FileUploadComponentProps> = ({
|
|||
onDragEnd,
|
||||
onUpdate,
|
||||
}) => {
|
||||
// 🔑 인증 정보 가져오기
|
||||
const { user } = useAuth();
|
||||
|
||||
const [uploadedFiles, setUploadedFiles] = useState<FileInfo[]>([]);
|
||||
const [uploadStatus, setUploadStatus] = useState<FileUploadStatus>("idle");
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
|
|
@ -102,28 +106,94 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
const [representativeImageUrl, setRepresentativeImageUrl] = useState<string | null>(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;
|
||||
// 🔑 컬럼명 결정: 레코드 모드에서는 무조건 '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<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에서 파일 복원
|
||||
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<FileUploadComponentProps> = ({
|
|||
} catch (e) {
|
||||
console.warn("컴포넌트 마운트 시 파일 복원 실패:", e);
|
||||
}
|
||||
}, [component.id]); // component.id가 변경될 때만 실행
|
||||
}, [component.id, getUniqueKey, recordId]); // 레코드별 고유 키 변경 시 재실행
|
||||
|
||||
// 🎯 화면설계 모드에서 실제 화면으로의 실시간 동기화 이벤트 리스너
|
||||
useEffect(() => {
|
||||
|
|
@ -152,12 +222,14 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
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<FileUploadComponentProps> = ({
|
|||
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<FileUploadComponentProps> = ({
|
|||
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<FileUploadComponentProps> = ({
|
|||
}));
|
||||
|
||||
|
||||
// 🔄 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<FileUploadComponentProps> = ({
|
|||
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<FileUploadComponentProps> = ({
|
|||
|
||||
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<FileUploadComponentProps> = ({
|
|||
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<FileUploadComponentProps> = ({
|
|||
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<FileUploadComponentProps> = ({
|
|||
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<FileUploadComponentProps> = ({
|
|||
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<FileUploadComponentProps> = ({
|
|||
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<FileUploadComponentProps> = ({
|
|||
}
|
||||
|
||||
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<FileUploadComponentProps> = ({
|
|||
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<FileUploadComponentProps> = ({
|
|||
|
||||
// 전역 상태 업데이트 (모든 파일 컴포넌트 동기화)
|
||||
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<FileUploadComponentProps> = ({
|
|||
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<FileUploadComponentProps> = ({
|
|||
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<FileUploadComponentProps> = ({
|
|||
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<FileUploadComponentProps> = ({
|
|||
|
||||
// 전역 상태 업데이트 (모든 파일 컴포넌트 동기화)
|
||||
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<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} 삭제 완료`);
|
||||
} catch (error) {
|
||||
console.error("파일 삭제 오류:", error);
|
||||
toast.error("파일 삭제에 실패했습니다.");
|
||||
}
|
||||
},
|
||||
[uploadedFiles, onUpdate, component.id],
|
||||
[uploadedFiles, onUpdate, component.id, isRecordMode, onFormDataChange, recordTableName, recordId, columnName, getUniqueKey],
|
||||
);
|
||||
|
||||
// 대표 이미지 Blob URL 로드
|
||||
|
|
|
|||
|
|
@ -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 (!value) return "";
|
||||
|
|
|
|||
Loading…
Reference in New Issue