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