Merge origin/main into ksh - resolve conflicts

This commit is contained in:
SeongHyun Kim
2026-01-09 14:55:16 +09:00
61 changed files with 10564 additions and 524 deletions

View File

@@ -871,7 +871,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
size: 1,
});
const detail = result.items && result.items.length > 0 ? result.items[0] : null;
// result.data가 EntityJoinResponse의 실제 배열 필드
const detail = result.data && result.data.length > 0 ? result.data[0] : null;
setRightData(detail);
} else if (relationshipType === "join") {
// 조인 모드: 다른 테이블의 관련 데이터 (여러 개)
@@ -940,26 +941,65 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
return;
}
// 🆕 복합키 지원
if (keys && keys.length > 0 && leftTable) {
// 🆕 엔티티 관계 자동 감지 로직 개선
// 1. 설정된 keys가 있으면 사용
// 2. 없으면 테이블 타입관리에서 정의된 엔티티 관계를 자동으로 조회
let effectiveKeys = keys || [];
if (effectiveKeys.length === 0 && leftTable && rightTableName) {
// 엔티티 관계 자동 감지
console.log("🔍 [분할패널] 엔티티 관계 자동 감지 시작:", leftTable, "->", rightTableName);
const { tableManagementApi } = await import("@/lib/api/tableManagement");
const relResponse = await tableManagementApi.getTableEntityRelations(leftTable, rightTableName);
if (relResponse.success && relResponse.data?.relations && relResponse.data.relations.length > 0) {
effectiveKeys = relResponse.data.relations.map((rel) => ({
leftColumn: rel.leftColumn,
rightColumn: rel.rightColumn,
}));
console.log("✅ [분할패널] 자동 감지된 관계:", effectiveKeys);
}
}
if (effectiveKeys.length > 0 && leftTable) {
// 복합키: 여러 조건으로 필터링
const { entityJoinApi } = await import("@/lib/api/entityJoin");
// 복합키 조건 생성
// 복합키 조건 생성 (다중 값 지원)
// 🆕 항상 배열로 전달하여 백엔드에서 다중 값 컬럼 검색을 지원하도록 함
// 예: 좌측에서 "2"를 선택해도, 우측에서 "2,3"을 가진 행이 표시되도록
const searchConditions: Record<string, any> = {};
keys.forEach((key) => {
effectiveKeys.forEach((key) => {
if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) {
searchConditions[key.rightColumn] = leftItem[key.leftColumn];
const leftValue = leftItem[key.leftColumn];
// 다중 값 지원: 모든 값을 배열로 변환하여 다중 값 컬럼 검색 활성화
if (typeof leftValue === "string") {
if (leftValue.includes(",")) {
// "2,3" 형태면 분리해서 배열로
const values = leftValue.split(",").map((v: string) => v.trim()).filter((v: string) => v);
searchConditions[key.rightColumn] = values;
console.log("🔗 [분할패널] 다중 값 검색 (분리):", key.rightColumn, "=", values);
} else {
// 단일 값도 배열로 변환 (우측에 "2,3" 같은 다중 값이 있을 수 있으므로)
searchConditions[key.rightColumn] = [leftValue.trim()];
console.log("🔗 [분할패널] 다중 값 검색 (단일):", key.rightColumn, "=", [leftValue.trim()]);
}
} else {
// 숫자나 다른 타입은 배열로 감싸기
searchConditions[key.rightColumn] = [leftValue];
console.log("🔗 [분할패널] 다중 값 검색 (기타):", key.rightColumn, "=", [leftValue]);
}
}
});
console.log("🔗 [분할패널] 복합키 조건:", searchConditions);
// 엔티티 조인 API로 데이터 조회
// 엔티티 조인 API로 데이터 조회 (🆕 deduplication 전달)
const result = await entityJoinApi.getTableDataWithJoins(rightTableName, {
search: searchConditions,
enableEntityJoin: true,
size: 1000,
deduplication: componentConfig.rightPanel?.deduplication, // 🆕 중복 제거 설정 전달
});
console.log("🔗 [분할패널] 복합키 조회 결과:", result);
@@ -988,7 +1028,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
setRightData(filteredData);
} else {
// 단일키 (하위 호환성)
// 단일키 (하위 호환성) 또는 관계를 찾지 못한 경우
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
const rightColumn = componentConfig.rightPanel?.relation?.foreignKey;
@@ -1006,6 +1046,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
componentConfig.rightPanel?.deduplication, // 🆕 중복 제거 설정 전달
);
setRightData(joinedData || []); // 모든 관련 레코드 (배열)
} else {
console.warn("⚠️ [분할패널] 테이블 관계를 찾을 수 없습니다:", leftTable, "->", rightTableName);
setRightData([]);
}
}
}
@@ -1589,13 +1632,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
// 수정 버튼 핸들러
const handleEditClick = useCallback(
(panel: "left" | "right", item: any) => {
async (panel: "left" | "right", item: any) => {
// 🆕 현재 활성 탭의 설정 가져오기
const currentTabConfig =
activeTabIndex === 0
? componentConfig.rightPanel
: componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1];
// 🆕 우측 패널 수정 버튼 설정 확인
if (panel === "right" && currentTabConfig?.editButton?.mode === "modal") {
const modalScreenId = currentTabConfig?.editButton?.modalScreenId;
@@ -1604,27 +1646,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
// 커스텀 모달 화면 열기
const rightTableName = currentTabConfig?.tableName || "";
// Primary Key 찾기 (우선순위: id > ID > 첫 번째 필드)
let primaryKeyName = "id";
let primaryKeyValue: any;
if (item.id !== undefined && item.id !== null) {
primaryKeyName = "id";
primaryKeyValue = item.id;
} else if (item.ID !== undefined && item.ID !== null) {
primaryKeyName = "ID";
primaryKeyValue = item.ID;
} else {
// 첫 번째 필드를 Primary Key로 간주
const firstKey = Object.keys(item)[0];
primaryKeyName = firstKey;
primaryKeyValue = item[firstKey];
}
console.log("✅ 수정 모달 열기:", {
tableName: rightTableName,
primaryKeyName,
primaryKeyValue,
screenId: modalScreenId,
fullItem: item,
});
@@ -1643,15 +1666,88 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
hasGroupByColumns: groupByColumns.length > 0,
});
// ScreenModal 열기 이벤트 발생 (URL 파라미터로 ID + groupByColumns 전달)
// 🆕 groupByColumns 기준으로 모든 관련 레코드 조회 (API 직접 호출)
let allRelatedRecords = [item]; // 기본값: 현재 아이템만
if (groupByColumns.length > 0) {
// groupByColumns 값으로 검색 조건 생성
const matchConditions: Record<string, any> = {};
groupByColumns.forEach((col: string) => {
if (item[col] !== undefined && item[col] !== null) {
matchConditions[col] = item[col];
}
});
console.log("🔍 [SplitPanel] 그룹 레코드 조회 시작:", {
테이블: rightTableName,
조건: matchConditions,
});
if (Object.keys(matchConditions).length > 0) {
// 🆕 deduplication 없이 원본 데이터 다시 조회 (API 직접 호출)
try {
const { entityJoinApi } = await import("@/lib/api/entityJoin");
// 🔧 dataFilter로 정확 매칭 조건 생성 (search는 LIKE 검색이라 부정확)
const exactMatchFilters = Object.entries(matchConditions).map(([key, value]) => ({
id: `exact-${key}`,
columnName: key,
operator: "equals",
value: value,
valueType: "text",
}));
console.log("🔍 [SplitPanel] 정확 매칭 필터:", exactMatchFilters);
const result = await entityJoinApi.getTableDataWithJoins(rightTableName, {
// search 대신 dataFilter 사용 (정확 매칭)
dataFilter: {
enabled: true,
matchType: "all",
filters: exactMatchFilters,
},
enableEntityJoin: true,
size: 1000,
// 🔧 명시적으로 deduplication 비활성화 (모든 레코드 가져오기)
deduplication: { enabled: false, groupByColumn: "", keepStrategy: "latest" },
});
// 🔍 디버깅: API 응답 구조 확인
console.log("🔍 [SplitPanel] API 응답 전체:", result);
console.log("🔍 [SplitPanel] result.data:", result.data);
console.log("🔍 [SplitPanel] result 타입:", typeof result);
// result 자체가 배열일 수도 있음 (entityJoinApi 응답 구조에 따라)
const dataArray = Array.isArray(result) ? result : (result.data || []);
if (dataArray.length > 0) {
allRelatedRecords = dataArray;
console.log("✅ [SplitPanel] 그룹 레코드 조회 완료:", {
조건: matchConditions,
결과수: allRelatedRecords.length,
레코드들: allRelatedRecords.map((r: any) => ({ id: r.id, supplier_item_code: r.supplier_item_code })),
});
} else {
console.warn("⚠️ [SplitPanel] 그룹 레코드 조회 결과 없음, 현재 아이템만 사용");
}
} catch (error) {
console.error("❌ [SplitPanel] 그룹 레코드 조회 실패:", error);
allRelatedRecords = [item];
}
} else {
console.warn("⚠️ [SplitPanel] groupByColumns 값이 없음, 현재 아이템만 사용");
}
}
// 🔧 수정: URL 파라미터 대신 editData로 직접 전달
// 이렇게 하면 테이블의 Primary Key가 무엇이든 상관없이 데이터가 정확히 전달됨
window.dispatchEvent(
new CustomEvent("openScreenModal", {
detail: {
screenId: modalScreenId,
editData: allRelatedRecords, // 🆕 모든 관련 레코드 전달 (배열)
urlParams: {
mode: "edit",
editId: primaryKeyValue,
tableName: rightTableName,
mode: "edit", // 🆕 수정 모드 표시
...(groupByColumns.length > 0 && {
groupByColumns: JSON.stringify(groupByColumns),
}),
@@ -1660,10 +1756,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
}),
);
console.log("✅ [SplitPanel] openScreenModal 이벤트 발생:", {
console.log("✅ [SplitPanel] openScreenModal 이벤트 발생 (editData 직접 전달):", {
screenId: modalScreenId,
editId: primaryKeyValue,
tableName: rightTableName,
editData: allRelatedRecords,
recordCount: allRelatedRecords.length,
groupByColumns: groupByColumns.length > 0 ? JSON.stringify(groupByColumns) : "없음",
});
@@ -1814,47 +1910,89 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
try {
console.log("🗑️ 데이터 삭제:", { tableName, primaryKey });
// 🔍 중복 제거 설정 디버깅
console.log("🔍 중복 제거 디버깅:", {
// 🔍 그룹 삭제 설정 확인 (editButton.groupByColumns 또는 deduplication)
const groupByColumns = componentConfig.rightPanel?.editButton?.groupByColumns || [];
const deduplication = componentConfig.rightPanel?.dataFilter?.deduplication;
console.log("🔍 삭제 설정 디버깅:", {
panel: deleteModalPanel,
dataFilter: componentConfig.rightPanel?.dataFilter,
deduplication: componentConfig.rightPanel?.dataFilter?.deduplication,
enabled: componentConfig.rightPanel?.dataFilter?.deduplication?.enabled,
groupByColumns,
deduplication,
deduplicationEnabled: deduplication?.enabled,
});
let result;
// 🔧 중복 제거가 활성화된 경우, groupByColumn 기준으로 모든 관련 레코드 삭제
if (deleteModalPanel === "right" && componentConfig.rightPanel?.dataFilter?.deduplication?.enabled) {
const deduplication = componentConfig.rightPanel.dataFilter.deduplication;
const groupByColumn = deduplication.groupByColumn;
if (groupByColumn && deleteModalItem[groupByColumn]) {
const groupValue = deleteModalItem[groupByColumn];
console.log(`🔗 중복 제거 활성화: ${groupByColumn} = ${groupValue} 기준으로 모든 레코드 삭제`);
// groupByColumn 값으로 필터링하여 삭제
const filterConditions: Record<string, any> = {
[groupByColumn]: groupValue,
};
// 좌측 패널의 선택된 항목 정보도 포함 (customer_id 등)
if (selectedLeftItem && componentConfig.rightPanel?.mode === "join") {
const leftColumn = componentConfig.rightPanel.join.leftColumn;
const rightColumn = componentConfig.rightPanel.join.rightColumn;
filterConditions[rightColumn] = selectedLeftItem[leftColumn];
// 🔧 우측 패널 삭제 시 그룹 삭제 조건 확인
if (deleteModalPanel === "right") {
// 1. groupByColumns가 설정된 경우 (패널 설정에서 선택된 컬럼들)
if (groupByColumns.length > 0) {
const filterConditions: Record<string, any> = {};
// 선택된 컬럼들의 값을 필터 조건으로 추가
for (const col of groupByColumns) {
if (deleteModalItem[col] !== undefined && deleteModalItem[col] !== null) {
filterConditions[col] = deleteModalItem[col];
}
}
console.log("🗑️ 그룹 삭제 조건:", filterConditions);
// 🔒 안전장치: 조인 모드에서 좌측 패널의 키 값도 필터 조건에 포함
// (다른 거래처의 같은 품목이 삭제되는 것을 방지)
if (selectedLeftItem && componentConfig.rightPanel?.mode === "join") {
const leftColumn = componentConfig.rightPanel.join?.leftColumn;
const rightColumn = componentConfig.rightPanel.join?.rightColumn;
if (leftColumn && rightColumn && selectedLeftItem[leftColumn]) {
// rightColumn이 filterConditions에 없으면 추가
if (!filterConditions[rightColumn]) {
filterConditions[rightColumn] = selectedLeftItem[leftColumn];
console.log(`🔒 안전장치: ${rightColumn} = ${selectedLeftItem[leftColumn]} 추가`);
}
}
}
// 그룹 삭제 API 호출
result = await dataApi.deleteGroupRecords(tableName, filterConditions);
} else {
// 단일 레코드 삭제
// 필터 조건이 있으면 그룹 삭제
if (Object.keys(filterConditions).length > 0) {
console.log(`🔗 그룹 삭제 (groupByColumns): ${groupByColumns.join(", ")} 기준`);
console.log("🗑️ 그룹 삭제 조건:", filterConditions);
result = await dataApi.deleteGroupRecords(tableName, filterConditions);
} else {
// 필터 조건이 없으면 단일 삭제
console.log("⚠️ groupByColumns 값이 없어 단일 삭제로 전환");
result = await dataApi.deleteRecord(tableName, primaryKey);
}
}
// 2. 중복 제거(deduplication)가 활성화된 경우
else if (deduplication?.enabled && deduplication?.groupByColumn) {
const groupByColumn = deduplication.groupByColumn;
const groupValue = deleteModalItem[groupByColumn];
if (groupValue) {
console.log(`🔗 중복 제거 활성화: ${groupByColumn} = ${groupValue} 기준으로 모든 레코드 삭제`);
const filterConditions: Record<string, any> = {
[groupByColumn]: groupValue,
};
// 좌측 패널의 선택된 항목 정보도 포함 (customer_id 등)
if (selectedLeftItem && componentConfig.rightPanel?.mode === "join") {
const leftColumn = componentConfig.rightPanel.join.leftColumn;
const rightColumn = componentConfig.rightPanel.join.rightColumn;
filterConditions[rightColumn] = selectedLeftItem[leftColumn];
}
console.log("🗑️ 그룹 삭제 조건:", filterConditions);
result = await dataApi.deleteGroupRecords(tableName, filterConditions);
} else {
result = await dataApi.deleteRecord(tableName, primaryKey);
}
}
// 3. 그 외: 단일 레코드 삭제
else {
result = await dataApi.deleteRecord(tableName, primaryKey);
}
} else {
// 단일 레코드 삭제
// 좌측 패널: 단일 레코드 삭제
result = await dataApi.deleteRecord(tableName, primaryKey);
}