feat: enhance v2-split-panel-layout component

SplitPanelLayoutComponent, ConfigPanel, types 개선

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kjs
2026-03-26 12:12:56 +09:00
parent 8c80c854cc
commit 0a6922edeb
3 changed files with 224 additions and 33 deletions

View File

@@ -667,8 +667,62 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const [showAddModal, setShowAddModal] = useState(false);
const [addModalPanel, setAddModalPanel] = useState<"left" | "right" | "left-item" | null>(null);
const [addModalFormData, setAddModalFormData] = useState<Record<string, any>>({});
// 엔티티 관계 자동 감지 캐시 (좌측↔우측 테이블 간 FK 매핑)
const [autoDetectedTabRelations, setAutoDetectedTabRelations] = useState<
Record<string, Array<{ leftColumn: string; rightColumn: string }>>
>({});
const [bomExcelUploadOpen, setBomExcelUploadOpen] = useState(false);
// 좌측↔우측 테이블 간 엔티티 관계 자동 감지 (table_type_columns 기반)
useEffect(() => {
const leftTable = componentConfig.leftPanel?.tableName;
if (!leftTable) return;
const detectAll = async () => {
const { tableManagementApi } = await import("@/lib/api/tableManagement");
const cache: Record<string, Array<{ leftColumn: string; rightColumn: string }>> = {};
// 기본 우측 패널
const rightTable = componentConfig.rightPanel?.tableName;
if (rightTable && rightTable !== leftTable) {
try {
const res = await tableManagementApi.getTableEntityRelations(leftTable, rightTable);
if (res.success && res.data?.relations?.length) {
cache[rightTable] = res.data.relations.map((r: any) => ({
leftColumn: r.leftColumn,
rightColumn: r.rightColumn,
}));
}
} catch { /* ignore */ }
}
// 추가 탭들
const tabs = componentConfig.rightPanel?.additionalTabs || [];
for (const tab of tabs) {
const tabTable = tab.tableName;
if (!tabTable || cache[tabTable] !== undefined) continue;
try {
const res = await tableManagementApi.getTableEntityRelations(leftTable, tabTable);
if (res.success && res.data?.relations?.length) {
cache[tabTable] = res.data.relations.map((r: any) => ({
leftColumn: r.leftColumn,
rightColumn: r.rightColumn,
}));
} else {
cache[tabTable] = [];
}
} catch {
cache[tabTable] = [];
}
}
setAutoDetectedTabRelations(cache);
};
detectAll();
}, [componentConfig.leftPanel?.tableName, componentConfig.rightPanel?.tableName, componentConfig.rightPanel?.additionalTabs]);
// 수정 모달 상태
const [showEditModal, setShowEditModal] = useState(false);
const [editModalPanel, setEditModalPanel] = useState<"left" | "right" | null>(null);
@@ -2518,6 +2572,15 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
}
}
// table_type_columns 기반 엔티티 관계 자동 감지 (패널 설정 없어도 동작)
if (currentTableName && autoDetectedTabRelations[currentTableName]) {
for (const rel of autoDetectedTabRelations[currentTableName]) {
if (parentData[rel.rightColumn] == null && selectedLeftItem[rel.leftColumn] != null) {
parentData[rel.rightColumn] = selectedLeftItem[rel.leftColumn];
}
}
}
window.dispatchEvent(
new CustomEvent("openScreenModal", {
detail: {
@@ -2539,23 +2602,73 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
setAddModalPanel(panel);
// 우측 패널 추가 시, 좌측에서 선택된 항목의 조인 컬럼 값을 자동으로 채움
if (
panel === "right" &&
selectedLeftItem &&
componentConfig.leftPanel?.leftColumn &&
componentConfig.rightPanel?.rightColumn
) {
const leftColumnValue = selectedLeftItem[componentConfig.leftPanel.leftColumn];
setAddModalFormData({
[componentConfig.rightPanel.rightColumn]: leftColumnValue,
});
if (panel === "right" && selectedLeftItem) {
const prefill: Record<string, any> = {};
// 현재 활성 탭의 설정 가져오기 (기본 탭 or 추가 탭)
const currentAddConfig =
activeTabIndex === 0
? componentConfig.rightPanel?.addConfig
: (componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1] as any)?.addConfig;
const currentRelation =
activeTabIndex === 0
? componentConfig.rightPanel?.relation
: (componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1] as any)?.relation;
// 1) relation.keys 기반 FK 자동 채움
if (currentRelation?.keys && Array.isArray(currentRelation.keys)) {
for (const key of currentRelation.keys) {
if (key.leftColumn && key.rightColumn && selectedLeftItem[key.leftColumn] != null) {
prefill[key.rightColumn] = selectedLeftItem[key.leftColumn];
}
}
} else if (currentRelation) {
const leftCol = currentRelation.leftColumn || componentConfig.leftPanel?.leftColumn;
const rightCol = currentRelation.foreignKey || currentRelation.rightColumn || componentConfig.rightPanel?.rightColumn;
if (leftCol && rightCol && selectedLeftItem[leftCol] != null) {
prefill[rightCol] = selectedLeftItem[leftCol];
}
} else if (componentConfig.leftPanel?.leftColumn && componentConfig.rightPanel?.rightColumn) {
// 하위호환: leftPanel.leftColumn → rightPanel.rightColumn
prefill[componentConfig.rightPanel.rightColumn] = selectedLeftItem[componentConfig.leftPanel.leftColumn];
}
// 2) addConfig.leftPanelColumn → targetColumn (단일 키, 하위호환)
if (currentAddConfig?.leftPanelColumn && currentAddConfig?.targetColumn) {
const val = selectedLeftItem[currentAddConfig.leftPanelColumn];
if (val != null) prefill[currentAddConfig.targetColumn] = val;
}
// 3) addConfig.autoFillFromLeft — 복수 컬럼 자동 채움
if (currentAddConfig?.autoFillFromLeft) {
for (const mapping of currentAddConfig.autoFillFromLeft) {
const val = selectedLeftItem[mapping.source];
if (val != null) prefill[mapping.target] = val;
}
}
// 4) table_type_columns 기반 엔티티 관계 자동 감지 (패널 설정 없어도 동작)
const currentTableName =
activeTabIndex === 0
? componentConfig.rightPanel?.tableName
: (componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1] as any)?.tableName;
if (currentTableName && autoDetectedTabRelations[currentTableName]) {
for (const rel of autoDetectedTabRelations[currentTableName]) {
// 이미 다른 방식으로 채워진 값이 없을 때만 자동 채움
if (prefill[rel.rightColumn] == null && selectedLeftItem[rel.leftColumn] != null) {
prefill[rel.rightColumn] = selectedLeftItem[rel.leftColumn];
}
}
}
setAddModalFormData(prefill);
} else {
setAddModalFormData({});
}
setShowAddModal(true);
},
[selectedLeftItem, componentConfig, activeTabIndex],
[selectedLeftItem, componentConfig, activeTabIndex, autoDetectedTabRelations],
);
// 수정 버튼 핸들러
@@ -3234,21 +3347,38 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
tableName = componentConfig.leftPanel?.tableName;
modalColumns = componentConfig.leftPanel?.addModalColumns;
} else if (addModalPanel === "right") {
// 우측 패널: 중계 테이블 설정이 있는지 확인
const addConfig = componentConfig.rightPanel?.addConfig;
// 현재 활성 탭의 설정 가져오기 (기본 탭 or 추가 탭)
const isAdditionalTab = activeTabIndex > 0;
const tabConfig = isAdditionalTab
? (componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1] as any)
: null;
const addConfig = isAdditionalTab
? tabConfig?.addConfig
: componentConfig.rightPanel?.addConfig;
if (addConfig?.targetTable) {
// 중계 테이블 모드
tableName = addConfig.targetTable;
modalColumns = componentConfig.rightPanel?.addModalColumns;
modalColumns = isAdditionalTab
? tabConfig?.addModalColumns
: componentConfig.rightPanel?.addModalColumns;
// 좌측 패널에서 선택된 값 자동 채우기
// 좌측 패널에서 선택된 값 자동 채우기 (단일 키, 하위호환)
if (addConfig.leftPanelColumn && addConfig.targetColumn && selectedLeftItem) {
const leftValue = selectedLeftItem[addConfig.leftPanelColumn];
finalData[addConfig.targetColumn] = leftValue;
}
// 자동 채움 컬럼 추가
// autoFillFromLeft — 복수 컬럼 자동 채움
if (addConfig.autoFillFromLeft && selectedLeftItem) {
for (const mapping of addConfig.autoFillFromLeft) {
const val = selectedLeftItem[mapping.source];
if (val != null) finalData[mapping.target] = val;
}
}
// 자동 채움 컬럼 추가 (정적 값)
if (addConfig.autoFillColumns) {
Object.entries(addConfig.autoFillColumns).forEach(([key, value]) => {
finalData[key] = value;
@@ -3256,8 +3386,29 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
}
} else {
// 일반 테이블 모드
tableName = componentConfig.rightPanel?.tableName;
modalColumns = componentConfig.rightPanel?.addModalColumns;
tableName = isAdditionalTab
? tabConfig?.tableName
: componentConfig.rightPanel?.tableName;
modalColumns = isAdditionalTab
? tabConfig?.addModalColumns
: componentConfig.rightPanel?.addModalColumns;
// 일반 모드에서도 autoFillFromLeft 적용
if (addConfig?.autoFillFromLeft && selectedLeftItem) {
for (const mapping of addConfig.autoFillFromLeft) {
const val = selectedLeftItem[mapping.source];
if (val != null) finalData[mapping.target] = val;
}
}
}
// table_type_columns 기반 엔티티 관계 자동 감지 (패널 설정 없어도 동작)
if (tableName && autoDetectedTabRelations[tableName] && selectedLeftItem) {
for (const rel of autoDetectedTabRelations[tableName]) {
if (finalData[rel.rightColumn] == null && selectedLeftItem[rel.leftColumn] != null) {
finalData[rel.rightColumn] = selectedLeftItem[rel.leftColumn];
}
}
}
} else if (addModalPanel === "left-item") {
// 하위 항목 추가 (좌측 테이블에 추가)
@@ -3305,8 +3456,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
// 좌측 패널 데이터 새로고침 (일반 추가 또는 하위 항목 추가)
loadLeftData();
} else if (addModalPanel === "right") {
// 우측 패널 데이터 새로고침
loadRightData(selectedLeftItem);
// 우측 패널 데이터 새로고침 (추가 탭이면 loadTabData)
if (activeTabIndex > 0) {
loadTabData(activeTabIndex, selectedLeftItem);
} else {
loadRightData(selectedLeftItem);
}
}
} else {
toast({