Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary, ; especially if it merges an updated upstream into a topic branch. ; ; Lines starting with ';' will be ignored, and an empty message aborts ; the commit.
This commit is contained in:
commit
a0a9253d2c
|
|
@ -34,7 +34,11 @@ const getApiBaseUrl = (): string => {
|
|||
export const API_BASE_URL = getApiBaseUrl();
|
||||
|
||||
// 이미지 URL을 완전한 URL로 변환하는 함수
|
||||
// 주의: 모듈 로드 시점이 아닌 런타임에 hostname을 확인해야 SSR 문제 방지
|
||||
export const getFullImageUrl = (imagePath: string): string => {
|
||||
// 빈 값 체크
|
||||
if (!imagePath) return "";
|
||||
|
||||
// 이미 전체 URL인 경우 그대로 반환
|
||||
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
||||
return imagePath;
|
||||
|
|
@ -42,10 +46,31 @@ export const getFullImageUrl = (imagePath: string): string => {
|
|||
|
||||
// /uploads로 시작하는 상대 경로인 경우 API 서버 주소 추가
|
||||
if (imagePath.startsWith("/uploads")) {
|
||||
const baseUrl = API_BASE_URL.replace("/api", ""); // /api 제거
|
||||
// 런타임에 현재 hostname 확인 (SSR 시점이 아닌 클라이언트에서 실행될 때)
|
||||
if (typeof window !== "undefined") {
|
||||
const currentHost = window.location.hostname;
|
||||
|
||||
// 프로덕션 환경: v1.vexplor.com → api.vexplor.com
|
||||
if (currentHost === "v1.vexplor.com") {
|
||||
return `https://api.vexplor.com${imagePath}`;
|
||||
}
|
||||
|
||||
// 로컬 개발환경
|
||||
if (currentHost === "localhost" || currentHost === "127.0.0.1") {
|
||||
return `http://localhost:8080${imagePath}`;
|
||||
}
|
||||
}
|
||||
|
||||
// SSR 또는 알 수 없는 환경에서는 API_BASE_URL 사용 (fallback)
|
||||
const baseUrl = API_BASE_URL.replace("/api", "");
|
||||
if (baseUrl.startsWith("http://") || baseUrl.startsWith("https://")) {
|
||||
return `${baseUrl}${imagePath}`;
|
||||
}
|
||||
|
||||
// 최종 fallback
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
return imagePath;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -247,10 +247,40 @@ export const getFileDownloadUrl = (fileId: string): string => {
|
|||
|
||||
/**
|
||||
* 직접 파일 경로 URL 생성 (정적 파일 서빙)
|
||||
* 주의: 모듈 로드 시점이 아닌 런타임에 hostname을 확인해야 SSR 문제 방지
|
||||
*/
|
||||
export const getDirectFileUrl = (filePath: string): string => {
|
||||
// 빈 값 체크
|
||||
if (!filePath) return "";
|
||||
|
||||
// 이미 전체 URL인 경우 그대로 반환
|
||||
if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
// 런타임에 현재 hostname 확인 (SSR 시점이 아닌 클라이언트에서 실행될 때)
|
||||
if (typeof window !== "undefined") {
|
||||
const currentHost = window.location.hostname;
|
||||
|
||||
// 프로덕션 환경: v1.vexplor.com → api.vexplor.com
|
||||
if (currentHost === "v1.vexplor.com") {
|
||||
return `https://api.vexplor.com${filePath}`;
|
||||
}
|
||||
|
||||
// 로컬 개발환경
|
||||
if (currentHost === "localhost" || currentHost === "127.0.0.1") {
|
||||
return `http://localhost:8080${filePath}`;
|
||||
}
|
||||
}
|
||||
|
||||
// SSR 또는 알 수 없는 환경에서는 환경변수 사용 (fallback)
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL?.replace("/api", "") || "";
|
||||
if (baseUrl.startsWith("http://") || baseUrl.startsWith("https://")) {
|
||||
return `${baseUrl}${filePath}`;
|
||||
}
|
||||
|
||||
// 최종 fallback
|
||||
return filePath;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -434,20 +434,50 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
|
||||
itemsList.forEach((item, itemIndex) => {
|
||||
// 각 그룹의 엔트리 배열들을 준비
|
||||
const groupEntriesArrays: GroupEntry[][] = groups.map((group) => item.fieldGroups[group.id] || []);
|
||||
// 🔧 빈 엔트리 필터링: id만 있고 실제 필드 값이 없는 엔트리는 제외
|
||||
const groupEntriesArrays: GroupEntry[][] = groups.map((group) => {
|
||||
const entries = item.fieldGroups[group.id] || [];
|
||||
const groupFields = additionalFields.filter((f) => f.groupId === group.id);
|
||||
|
||||
// 실제 필드 값이 하나라도 있는 엔트리만 포함
|
||||
return entries.filter((entry) => {
|
||||
const hasAnyFieldValue = groupFields.some((field) => {
|
||||
const value = entry[field.name];
|
||||
return value !== undefined && value !== null && value !== "";
|
||||
});
|
||||
|
||||
if (!hasAnyFieldValue && Object.keys(entry).length <= 1) {
|
||||
console.log("⏭️ [generateCartesianProduct] 빈 엔트리 필터링:", {
|
||||
entryId: entry.id,
|
||||
groupId: group.id,
|
||||
entryKeys: Object.keys(entry),
|
||||
});
|
||||
}
|
||||
|
||||
return hasAnyFieldValue;
|
||||
});
|
||||
});
|
||||
|
||||
// 🆕 모든 그룹이 비어있는지 확인
|
||||
const allGroupsEmpty = groupEntriesArrays.every((arr) => arr.length === 0);
|
||||
|
||||
if (allGroupsEmpty) {
|
||||
// 🆕 모든 그룹이 비어있으면 품목 기본 정보만으로 레코드 생성
|
||||
// (거래처 품번/품명, 기간별 단가 없이도 저장 가능)
|
||||
console.log("📝 [generateCartesianProduct] 모든 그룹이 비어있음 - 품목 기본 레코드 생성", {
|
||||
// 🔧 아이템이 1개뿐이면 기본 레코드 생성 (첫 저장 시)
|
||||
// 아이템이 여러 개면 빈 아이템은 건너뛰기 (불필요한 NULL 레코드 방지)
|
||||
if (itemsList.length === 1) {
|
||||
console.log("📝 [generateCartesianProduct] 단일 아이템, 모든 그룹 비어있음 - 기본 레코드 생성", {
|
||||
itemIndex,
|
||||
itemId: item.id,
|
||||
});
|
||||
// 빈 객체를 추가하면 parentKeys와 합쳐져서 기본 레코드가 됨
|
||||
allRecords.push({});
|
||||
} else {
|
||||
console.log("⏭️ [generateCartesianProduct] 다중 아이템 중 빈 아이템 - 건너뜀", {
|
||||
itemIndex,
|
||||
itemId: item.id,
|
||||
totalItems: itemsList.length,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -471,6 +501,11 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
currentGroupEntries.forEach((entry) => {
|
||||
const newCombination = { ...currentCombination };
|
||||
|
||||
// 🆕 기존 레코드의 id가 있으면 포함 (UPDATE를 위해)
|
||||
if (entry.id) {
|
||||
newCombination.id = entry.id;
|
||||
}
|
||||
|
||||
// 현재 그룹의 필드들을 조합에 추가
|
||||
const groupFields = additionalFields.filter((f) => f.groupId === groups[currentIndex].id);
|
||||
groupFields.forEach((field) => {
|
||||
|
|
@ -573,6 +608,27 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
|
||||
console.log("🔑 [SelectedItemsDetailInput] 부모 키:", parentKeys);
|
||||
|
||||
// 🔒 parentKeys 유효성 검증 - 빈 값이 있으면 저장 중단
|
||||
const parentKeyValues = Object.values(parentKeys);
|
||||
const hasEmptyParentKey = parentKeyValues.length === 0 ||
|
||||
parentKeyValues.some(v => v === null || v === undefined || v === "");
|
||||
|
||||
if (hasEmptyParentKey) {
|
||||
console.error("❌ [SelectedItemsDetailInput] parentKeys가 비어있거나 유효하지 않습니다!", parentKeys);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("formSaveError", {
|
||||
detail: { message: "부모 키 값이 비어있어 저장할 수 없습니다. 먼저 상위 데이터를 선택해주세요." },
|
||||
}),
|
||||
);
|
||||
|
||||
// 🔧 기본 저장 건너뛰기 - event.detail 객체 직접 수정
|
||||
if (event instanceof CustomEvent && event.detail) {
|
||||
(event.detail as any).skipDefaultSave = true;
|
||||
console.log("🚫 [SelectedItemsDetailInput] skipDefaultSave = true (parentKeys 검증 실패)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// items를 Cartesian Product로 변환
|
||||
const records = generateCartesianProduct(items);
|
||||
|
||||
|
|
@ -591,24 +647,21 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
}),
|
||||
);
|
||||
|
||||
// 🆕 기본 저장 건너뛰기
|
||||
// 🔧 기본 저장 건너뛰기 - event.detail 객체 직접 수정
|
||||
if (event instanceof CustomEvent && event.detail) {
|
||||
event.detail.skipDefaultSave = true;
|
||||
(event.detail as any).skipDefaultSave = true;
|
||||
console.log("🚫 [SelectedItemsDetailInput] skipDefaultSave = true (targetTable 없음)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 🆕 먼저 기본 저장 로직 건너뛰기 설정 (UPSERT 전에!)
|
||||
if (event instanceof CustomEvent) {
|
||||
if (!event.detail) {
|
||||
console.warn("⚠️ [SelectedItemsDetailInput] event.detail이 없습니다! 새로 생성합니다.");
|
||||
// @ts-ignore - detail 재정의
|
||||
event.detail = {};
|
||||
}
|
||||
event.detail.skipDefaultSave = true;
|
||||
console.log("🚫 [SelectedItemsDetailInput] skipDefaultSave = true (UPSERT 전)", event.detail);
|
||||
// 🔧 먼저 기본 저장 로직 건너뛰기 설정 (UPSERT 전에!)
|
||||
// buttonActions.ts에서 beforeSaveEventDetail 객체를 event.detail로 전달하므로 직접 수정 가능
|
||||
if (event instanceof CustomEvent && event.detail) {
|
||||
(event.detail as any).skipDefaultSave = true;
|
||||
console.log("🚫 [SelectedItemsDetailInput] skipDefaultSave = true (UPSERT 처리)", event.detail);
|
||||
} else {
|
||||
console.error("❌ [SelectedItemsDetailInput] event가 CustomEvent가 아닙니다!", event);
|
||||
console.error("❌ [SelectedItemsDetailInput] event.detail이 없습니다! 기본 저장이 실행될 수 있습니다.", event);
|
||||
}
|
||||
|
||||
console.log("🎯 [SelectedItemsDetailInput] targetTable:", componentConfig.targetTable);
|
||||
|
|
|
|||
|
|
@ -535,17 +535,26 @@ export class ButtonActionExecutor {
|
|||
|
||||
// 🆕 저장 전 이벤트 발생 (SelectedItemsDetailInput 등에서 최신 데이터 수집)
|
||||
// context.formData를 이벤트 detail에 포함하여 직접 수정 가능하게 함
|
||||
// skipDefaultSave 플래그를 통해 기본 저장 로직을 건너뛸 수 있음
|
||||
const beforeSaveEventDetail = {
|
||||
formData: context.formData,
|
||||
skipDefaultSave: false,
|
||||
};
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("beforeFormSave", {
|
||||
detail: {
|
||||
formData: context.formData,
|
||||
},
|
||||
detail: beforeSaveEventDetail,
|
||||
}),
|
||||
);
|
||||
|
||||
// 약간의 대기 시간을 주어 이벤트 핸들러가 formData를 업데이트할 수 있도록 함
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// 🔧 skipDefaultSave 플래그 확인 - SelectedItemsDetailInput 등에서 자체 UPSERT 처리 시 기본 저장 건너뛰기
|
||||
if (beforeSaveEventDetail.skipDefaultSave) {
|
||||
console.log("🚫 [handleSave] skipDefaultSave=true - 기본 저장 로직 건너뛰기 (컴포넌트에서 자체 처리)");
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log("📦 [handleSave] beforeFormSave 이벤트 후 formData:", context.formData);
|
||||
|
||||
// 🆕 렉 구조 컴포넌트 일괄 저장 감지
|
||||
|
|
|
|||
Loading…
Reference in New Issue