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:
@@ -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({
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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?: {
|
||||
|
||||
Reference in New Issue
Block a user