Merge origin/main into ksh - resolve conflicts
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1272,6 +1272,71 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
}
|
||||
}, [config.rightPanel?.tableName]);
|
||||
|
||||
// 🆕 좌측/우측 테이블이 모두 선택되면 엔티티 관계 자동 감지
|
||||
const [autoDetectedRelations, setAutoDetectedRelations] = useState<
|
||||
Array<{
|
||||
leftColumn: string;
|
||||
rightColumn: string;
|
||||
direction: "left_to_right" | "right_to_left";
|
||||
inputType: string;
|
||||
displayColumn?: string;
|
||||
}>
|
||||
>([]);
|
||||
const [isDetectingRelations, setIsDetectingRelations] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const detectRelations = async () => {
|
||||
const leftTable = config.leftPanel?.tableName || screenTableName;
|
||||
const rightTable = config.rightPanel?.tableName;
|
||||
|
||||
// 조인 모드이고 양쪽 테이블이 모두 있을 때만 감지
|
||||
if (relationshipType !== "join" || !leftTable || !rightTable) {
|
||||
setAutoDetectedRelations([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDetectingRelations(true);
|
||||
try {
|
||||
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
||||
const response = await tableManagementApi.getTableEntityRelations(leftTable, rightTable);
|
||||
|
||||
if (response.success && response.data?.relations) {
|
||||
console.log("🔍 엔티티 관계 자동 감지:", response.data.relations);
|
||||
setAutoDetectedRelations(response.data.relations);
|
||||
|
||||
// 감지된 관계가 있고, 현재 설정된 키가 없으면 자동으로 첫 번째 관계를 설정
|
||||
const currentKeys = config.rightPanel?.relation?.keys || [];
|
||||
if (response.data.relations.length > 0 && currentKeys.length === 0) {
|
||||
// 첫 번째 관계만 자동 설정 (사용자가 추가로 설정 가능)
|
||||
const firstRel = response.data.relations[0];
|
||||
console.log("✅ 첫 번째 엔티티 관계 자동 설정:", firstRel);
|
||||
updateRightPanel({
|
||||
relation: {
|
||||
...config.rightPanel?.relation,
|
||||
type: "join",
|
||||
useMultipleKeys: true,
|
||||
keys: [
|
||||
{
|
||||
leftColumn: firstRel.leftColumn,
|
||||
rightColumn: firstRel.rightColumn,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 엔티티 관계 감지 실패:", error);
|
||||
setAutoDetectedRelations([]);
|
||||
} finally {
|
||||
setIsDetectingRelations(false);
|
||||
}
|
||||
};
|
||||
|
||||
detectRelations();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config.leftPanel?.tableName, config.rightPanel?.tableName, screenTableName, relationshipType]);
|
||||
|
||||
console.log("🔧 SplitPanelLayoutConfigPanel 렌더링");
|
||||
console.log(" - config:", config);
|
||||
console.log(" - tables:", tables);
|
||||
@@ -2476,234 +2541,50 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 컬럼 매핑 - 조인 모드에서만 표시 */}
|
||||
{/* 엔티티 관계 자동 감지 (읽기 전용) - 조인 모드에서만 표시 */}
|
||||
{relationshipType !== "detail" && (
|
||||
<div className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-sm font-semibold">컬럼 매핑 (외래키 관계)</Label>
|
||||
<p className="text-xs text-gray-600">좌측 테이블의 컬럼을 우측 테이블의 컬럼과 연결합니다</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 text-xs"
|
||||
onClick={() => {
|
||||
const currentKeys = config.rightPanel?.relation?.keys || [];
|
||||
// 단일키에서 복합키로 전환 시 기존 값 유지
|
||||
if (
|
||||
currentKeys.length === 0 &&
|
||||
config.rightPanel?.relation?.leftColumn &&
|
||||
config.rightPanel?.relation?.foreignKey
|
||||
) {
|
||||
updateRightPanel({
|
||||
relation: {
|
||||
...config.rightPanel?.relation,
|
||||
keys: [
|
||||
{
|
||||
leftColumn: config.rightPanel.relation.leftColumn,
|
||||
rightColumn: config.rightPanel.relation.foreignKey,
|
||||
},
|
||||
{ leftColumn: "", rightColumn: "" },
|
||||
],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
updateRightPanel({
|
||||
relation: {
|
||||
...config.rightPanel?.relation,
|
||||
keys: [...currentKeys, { leftColumn: "", rightColumn: "" }],
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
조인 키 추가
|
||||
</Button>
|
||||
<div className="space-y-3 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||
<div>
|
||||
<Label className="text-sm font-semibold">테이블 관계 (자동 감지)</Label>
|
||||
<p className="text-xs text-gray-600">테이블 타입관리에서 정의된 엔티티 관계입니다</p>
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-blue-600">복합키: 여러 컬럼으로 조인 (예: item_code + lot_number)</p>
|
||||
|
||||
{/* 복합키가 설정된 경우 */}
|
||||
{(config.rightPanel?.relation?.keys || []).length > 0 ? (
|
||||
<>
|
||||
{(config.rightPanel?.relation?.keys || []).map((key, index) => (
|
||||
<div key={index} className="space-y-2 rounded-md border bg-white p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium">조인 키 {index + 1}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-destructive h-6 w-6 p-0"
|
||||
onClick={() => {
|
||||
const newKeys = (config.rightPanel?.relation?.keys || []).filter((_, i) => i !== index);
|
||||
updateRightPanel({
|
||||
relation: { ...config.rightPanel?.relation, keys: newKeys },
|
||||
});
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label className="text-xs">좌측 컬럼</Label>
|
||||
<Select
|
||||
value={key.leftColumn || ""}
|
||||
onValueChange={(value) => {
|
||||
const newKeys = [...(config.rightPanel?.relation?.keys || [])];
|
||||
newKeys[index] = { ...newKeys[index], leftColumn: value };
|
||||
updateRightPanel({
|
||||
relation: { ...config.rightPanel?.relation, keys: newKeys },
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="좌측 컬럼" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{leftTableColumns.map((column) => (
|
||||
<SelectItem key={column.columnName} value={column.columnName}>
|
||||
{column.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">우측 컬럼</Label>
|
||||
<Select
|
||||
value={key.rightColumn || ""}
|
||||
onValueChange={(value) => {
|
||||
const newKeys = [...(config.rightPanel?.relation?.keys || [])];
|
||||
newKeys[index] = { ...newKeys[index], rightColumn: value };
|
||||
updateRightPanel({
|
||||
relation: { ...config.rightPanel?.relation, keys: newKeys },
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="우측 컬럼" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{rightTableColumns.map((column) => (
|
||||
<SelectItem key={column.columnName} value={column.columnName}>
|
||||
{column.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{isDetectingRelations ? (
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||
<div className="h-3 w-3 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
||||
관계 감지 중...
|
||||
</div>
|
||||
) : autoDetectedRelations.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{autoDetectedRelations.map((rel, index) => (
|
||||
<div key={index} className="flex items-center gap-2 rounded-md border border-blue-300 bg-white p-2">
|
||||
<span className="rounded bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700">
|
||||
{leftTableName}.{rel.leftColumn}
|
||||
</span>
|
||||
<ArrowRight className="h-3 w-3 text-blue-400" />
|
||||
<span className="rounded bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">
|
||||
{rightTableName}.{rel.rightColumn}
|
||||
</span>
|
||||
<span className="ml-auto text-[10px] text-gray-500">
|
||||
{rel.inputType === "entity" ? "엔티티" : "카테고리"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
<p className="text-[10px] text-blue-600">
|
||||
테이블 타입관리에서 엔티티/카테고리 설정을 변경하면 자동으로 적용됩니다
|
||||
</p>
|
||||
</div>
|
||||
) : config.rightPanel?.tableName ? (
|
||||
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
|
||||
<p className="text-xs text-gray-500">감지된 엔티티 관계가 없습니다</p>
|
||||
<p className="mt-1 text-[10px] text-gray-400">
|
||||
테이블 타입관리에서 엔티티 타입과 참조 테이블을 설정하세요
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
/* 단일키 (하위 호환성) */
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">좌측 컬럼</Label>
|
||||
<Popover open={leftColumnOpen} onOpenChange={setLeftColumnOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={leftColumnOpen}
|
||||
className="w-full justify-between"
|
||||
disabled={!config.leftPanel?.tableName}
|
||||
>
|
||||
{config.rightPanel?.relation?.leftColumn || "좌측 컬럼 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼 검색..." />
|
||||
<CommandEmpty>컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||
{leftTableColumns.map((column) => (
|
||||
<CommandItem
|
||||
key={column.columnName}
|
||||
value={column.columnName}
|
||||
onSelect={(value) => {
|
||||
updateRightPanel({
|
||||
relation: { ...config.rightPanel?.relation, leftColumn: value },
|
||||
});
|
||||
setLeftColumnOpen(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
config.rightPanel?.relation?.leftColumn === column.columnName
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{column.columnName}
|
||||
<span className="ml-2 text-xs text-gray-500">({column.columnLabel || ""})</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center">
|
||||
<ArrowRight className="h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">우측 컬럼 (외래키)</Label>
|
||||
<Popover open={rightColumnOpen} onOpenChange={setRightColumnOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={rightColumnOpen}
|
||||
className="w-full justify-between"
|
||||
disabled={!config.rightPanel?.tableName}
|
||||
>
|
||||
{config.rightPanel?.relation?.foreignKey || "우측 컬럼 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼 검색..." />
|
||||
<CommandEmpty>컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||
{rightTableColumns.map((column) => (
|
||||
<CommandItem
|
||||
key={column.columnName}
|
||||
value={column.columnName}
|
||||
onSelect={(value) => {
|
||||
updateRightPanel({
|
||||
relation: { ...config.rightPanel?.relation, foreignKey: value },
|
||||
});
|
||||
setRightColumnOpen(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
config.rightPanel?.relation?.foreignKey === column.columnName
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{column.columnName}
|
||||
<span className="ml-2 text-xs text-gray-500">({column.columnLabel || ""})</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</>
|
||||
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
|
||||
<p className="text-xs text-gray-500">우측 테이블을 선택하면 관계를 자동 감지합니다</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user