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({

View File

@@ -422,6 +422,7 @@ interface AdditionalTabConfigPanelProps {
availableRightTables: TableInfo[];
leftTableColumns: ColumnInfo[];
menuObjid?: number;
screenTableName?: string;
// 공유 컬럼 로드 상태
loadedTableColumns: Record<string, ColumnInfo[]>;
loadTableColumns: (tableName: string) => Promise<void>;
@@ -466,14 +467,45 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
loadTableColumns,
loadingColumns,
entityJoinColumns: entityJoinColumnsMap,
screenTableName,
}) => {
// 탭 테이블 변경 시 컬럼 로드
// 탭 테이블 변경 시 컬럼 로드 + 엔티티 관계 자동 감지
useEffect(() => {
if (tab.tableName && !loadedTableColumns[tab.tableName] && !loadingColumns[tab.tableName]) {
loadTableColumns(tab.tableName);
}
}, [tab.tableName, loadedTableColumns, loadingColumns, loadTableColumns]);
// 탭 테이블 변경 시 좌측 테이블과의 관계 자동 감지
useEffect(() => {
const leftTable = config.leftPanel?.tableName || screenTableName;
const rightTable = tab.tableName;
if (!leftTable || !rightTable) return;
// 이미 relation이 설정되어 있으면 스킵
if (tab.relation?.keys && tab.relation.keys.length > 0) return;
const detectRelations = async () => {
try {
const { tableManagementApi } = await import("@/lib/api/tableManagement");
const response = await tableManagementApi.getTableEntityRelations(leftTable, rightTable);
if (response.success && response.data?.relations?.length > 0) {
const firstRel = response.data.relations[0];
updateTab({
relation: {
type: "join",
keys: [{ leftColumn: firstRel.leftColumn, rightColumn: firstRel.rightColumn }],
},
});
console.log(`✅ 추가 탭 [${tab.label}] 엔티티 관계 자동 감지:`, firstRel);
}
} catch (error) {
console.warn(`추가 탭 [${tab.label}] 관계 감지 실패:`, error);
}
};
detectRelations();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tab.tableName, config.leftPanel?.tableName, screenTableName]);
// 현재 탭의 컬럼 목록
const tabColumns = useMemo(() => {
return tab.tableName ? loadedTableColumns[tab.tableName] || [] : [];
@@ -3707,6 +3739,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
availableRightTables={availableRightTables}
leftTableColumns={leftTableColumns}
menuObjid={menuObjid}
screenTableName={screenTableName}
loadedTableColumns={loadedTableColumns}
loadTableColumns={loadTableColumns}
loadingColumns={loadingColumns}

View File

@@ -10,6 +10,19 @@ import { DataFilterConfig, TabInlineComponent } from "@/types/screen-management"
*/
export type PanelInlineComponent = TabInlineComponent;
/** 우측 패널 추가 시 좌측 데이터 자동 채움 설정 */
export interface AddConfig {
targetTable?: string; // 실제로 INSERT할 테이블 (중계 테이블)
autoFillColumns?: Record<string, any>; // 자동으로 채워질 컬럼과 기본값
leftPanelColumn?: string; // 좌측 패널의 어떤 컬럼값을 가져올지 (단일 키, 하위호환)
targetColumn?: string; // targetTable의 어떤 컬럼에 넣을지 (단일 키, 하위호환)
/** 좌측 선택 데이터에서 복수 컬럼을 자동으로 채움 (엔티티 관계 포함) */
autoFillFromLeft?: Array<{
source: string; // 좌측 데이터의 컬럼명
target: string; // 저장할 테이블의 컬럼명
}>;
}
/** 페이징 처리 설정 (좌측/우측 패널 공통) */
export interface PaginationConfig {
enabled: boolean;
@@ -88,12 +101,7 @@ export interface AdditionalTabConfig {
}>;
};
addConfig?: {
targetTable?: string;
autoFillColumns?: Record<string, any>;
leftPanelColumn?: string;
targetColumn?: string;
};
addConfig?: AddConfig;
tableConfig?: {
showCheckbox?: boolean;
@@ -304,12 +312,7 @@ export interface SplitPanelLayoutConfig {
};
// 우측 패널 추가 시 중계 테이블 설정 (N:M 관계)
addConfig?: {
targetTable?: string; // 실제로 INSERT할 테이블 (중계 테이블)
autoFillColumns?: Record<string, any>; // 자동으로 채워질 컬럼과 기본값
leftPanelColumn?: string; // 좌측 패널의 어떤 컬럼값을 가져올지
targetColumn?: string; // targetTable의 어떤 컬럼에 넣을지
};
addConfig?: AddConfig;
// 테이블 모드 설정
tableConfig?: {