From 72068d003ac0773f8a8f8bd9eb8d20fbae6daaf1 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 24 Feb 2026 15:27:18 +0900 Subject: [PATCH] refactor: Enhance screen layout retrieval logic for multi-tenancy support - Updated the ScreenManagementService to prioritize fetching layouts based on layer_id, ensuring that only the default layer is retrieved for users. - Implemented logic for administrators to re-query layouts based on the screen definition's company_code when no layout is found. - Adjusted the BomItemEditorComponent to dynamically render table cells based on configuration, improving flexibility and usability in the BOM item editor. - Introduced category options loading for dynamic cell rendering, enhancing the user experience in item editing. --- .../src/services/screenManagementService.ts | 35 +- frontend/components/screen/EditModal.tsx | 20 +- .../BomItemEditorComponent.tsx | 466 +++++++++++------- 3 files changed, 331 insertions(+), 190 deletions(-) diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 2c25f7e0..6f412de5 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -1721,18 +1721,28 @@ export class ScreenManagementService { throw new Error("이 화면의 레이아웃을 조회할 권한이 없습니다."); } - // 🆕 V2 테이블 우선 조회 (회사별 → 공통(*)) + // V2 테이블 우선 조회: 기본 레이어(layer_id=1)만 가져옴 + // layer_id 필터 없이 queryOne 하면 조건부 레이어가 반환될 수 있음 let v2Layout = await queryOne<{ layout_data: any }>( `SELECT layout_data FROM screen_layouts_v2 - WHERE screen_id = $1 AND company_code = $2`, + WHERE screen_id = $1 AND company_code = $2 AND layer_id = 1`, [screenId, companyCode], ); - // 회사별 레이아웃 없으면 공통(*) 조회 + // 최고관리자(*): 화면 정의의 company_code로 재조회 + if (!v2Layout && companyCode === "*" && existingScreen.company_code && existingScreen.company_code !== "*") { + v2Layout = await queryOne<{ layout_data: any }>( + `SELECT layout_data FROM screen_layouts_v2 + WHERE screen_id = $1 AND company_code = $2 AND layer_id = 1`, + [screenId, existingScreen.company_code], + ); + } + + // 일반 사용자: 회사별 레이아웃 없으면 공통(*) 조회 if (!v2Layout && companyCode !== "*") { v2Layout = await queryOne<{ layout_data: any }>( `SELECT layout_data FROM screen_layouts_v2 - WHERE screen_id = $1 AND company_code = '*'`, + WHERE screen_id = $1 AND company_code = '*' AND layer_id = 1`, [screenId], ); } @@ -5302,7 +5312,22 @@ export class ScreenManagementService { [screenId, companyCode, layerId], ); - // 회사별 레이어가 없으면 공통(*) 조회 + // 최고관리자(*): 화면 정의의 company_code로 재조회 + if (!layout && companyCode === "*") { + const screenDef = await queryOne<{ company_code: string }>( + `SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`, + [screenId], + ); + if (screenDef && screenDef.company_code && screenDef.company_code !== "*") { + layout = await queryOne<{ layout_data: any; layer_name: string; condition_config: any }>( + `SELECT layout_data, layer_name, condition_config FROM screen_layouts_v2 + WHERE screen_id = $1 AND company_code = $2 AND layer_id = $3`, + [screenId, screenDef.company_code, layerId], + ); + } + } + + // 일반 사용자: 회사별 레이어가 없으면 공통(*) 조회 if (!layout && companyCode !== "*") { layout = await queryOne<{ layout_data: any; layer_name: string; condition_config: any }>( `SELECT layout_data, layer_name, condition_config FROM screen_layouts_v2 diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 096d50e9..8dad77db 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -376,12 +376,26 @@ export const EditModal: React.FC = ({ className }) => { try { setLoading(true); - // 화면 정보와 레이아웃 데이터 로딩 - const [screenInfo, layoutData] = await Promise.all([ + // 화면 정보와 레이아웃 데이터 로딩 (ScreenModal과 동일하게 V2 API 우선) + const [screenInfo, v2LayoutData] = await Promise.all([ screenApi.getScreen(screenId), - screenApi.getLayout(screenId), + screenApi.getLayoutV2(screenId), ]); + // V2 → Legacy 변환 (ScreenModal과 동일한 패턴) + let layoutData: any = null; + if (v2LayoutData && isValidV2Layout(v2LayoutData)) { + layoutData = convertV2ToLegacy(v2LayoutData); + if (layoutData) { + layoutData.screenResolution = v2LayoutData.screenResolution || layoutData.screenResolution; + } + } + + // V2 없으면 기존 API fallback + if (!layoutData) { + layoutData = await screenApi.getLayout(screenId); + } + if (screenInfo && layoutData) { const components = layoutData.components || []; diff --git a/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx b/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx index 8191e68b..16aebd59 100644 --- a/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx +++ b/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx @@ -35,21 +35,23 @@ import { apiClient } from "@/lib/api/client"; interface BomItemNode { tempId: string; id?: string; - bom_id?: string; parent_detail_id: string | null; seq_no: number; level: number; - child_item_id: string; - child_item_code: string; - child_item_name: string; - child_item_type: string; - quantity: string; - unit: string; - loss_rate: string; - remark: string; children: BomItemNode[]; _isNew?: boolean; _isDeleted?: boolean; + data: Record; +} + +interface BomColumnConfig { + key: string; + title: string; + width?: string; + visible?: boolean; + editable?: boolean; + isSourceDisplay?: boolean; + inputType?: string; } interface ItemInfo { @@ -211,13 +213,16 @@ function ItemSearchModal({ ); } -// ─── 트리 노드 행 렌더링 ─── +// ─── 트리 노드 행 렌더링 (config.columns 기반 동적 셀) ─── interface TreeNodeRowProps { node: BomItemNode; depth: number; expanded: boolean; hasChildren: boolean; + columns: BomColumnConfig[]; + categoryOptionsMap: Record; + mainTableName?: string; onToggle: () => void; onFieldChange: (tempId: string, field: string, value: string) => void; onDelete: (tempId: string) => void; @@ -229,12 +234,84 @@ function TreeNodeRow({ depth, expanded, hasChildren, + columns, + categoryOptionsMap, + mainTableName, onToggle, onFieldChange, onDelete, onAddChild, }: TreeNodeRowProps) { const indentPx = depth * 32; + const visibleColumns = columns.filter((c) => c.visible !== false); + + const renderCell = (col: BomColumnConfig) => { + const value = node.data[col.key] ?? ""; + + // 소스 표시 컬럼 (읽기 전용) + if (col.isSourceDisplay) { + return ( + + {value || "-"} + + ); + } + + // 카테고리 타입: API에서 로드한 옵션으로 Select 렌더링 + if (col.inputType === "category") { + const categoryRef = mainTableName ? `${mainTableName}.${col.key}` : ""; + const options = categoryOptionsMap[categoryRef] || []; + return ( + + ); + } + + // 편집 불가능 컬럼 + if (col.editable === false) { + return ( + + {value || "-"} + + ); + } + + // 숫자 입력 + if (col.inputType === "number" || col.inputType === "decimal") { + return ( + onFieldChange(node.tempId, col.key, e.target.value)} + className="h-7 w-full min-w-[50px] text-center text-xs" + placeholder={col.title} + /> + ); + } + + // 기본 텍스트 입력 + return ( + onFieldChange(node.tempId, col.key, e.target.value)} + className="h-7 w-full min-w-[50px] text-xs" + placeholder={col.title} + /> + ); + }; return (
- {/* 드래그 핸들 */} - {/* 펼침/접기 */} - {/* 삭제 버튼 */}
- {/* 더미 트리 미리보기 */} -
- {dummyRows.map((row, i) => ( -
0 && "border-l-2 border-l-primary/20", - i === 0 && "bg-accent/30", - )} - style={{ marginLeft: `${row.depth * 20}px` }} - > - - {row.depth === 0 ? ( - - ) : ( - - )} - - {i + 1} - - - {row.code} - - - {row.name} - - - {/* 소스 표시 컬럼 미리보기 */} - {sourceColumns.slice(0, 2).map((col: any) => ( - - {col.title} - - ))} - - {/* 입력 컬럼 미리보기 */} - {inputColumns.slice(0, 2).map((col: any) => ( -
- {col.key === "quantity" || col.title === "수량" - ? row.qty - : ""} -
- ))} - -
-
- -
-
- -
-
+ {/* 테이블 형태 미리보기 - config.columns 순서 그대로 */} +
+ {visibleColumns.length === 0 ? ( +
+ +

+ 컬럼 탭에서 표시할 컬럼을 선택하세요 +

- ))} + ) : ( + + + + + {visibleColumns.map((col: any) => ( + + ))} + + + + + {DUMMY_DEPTHS.map((depth, rowIdx) => ( + + + + {visibleColumns.map((col: any) => ( + + ))} + + + ))} + +
+ # + {col.title} + 액션
+
+ {depth === 0 ? ( + + ) : ( + + )} +
+
+ {rowIdx + 1} + + {col.isSourceDisplay ? ( + + {getDummyValue(col, rowIdx) || col.title} + + ) : col.editable !== false ? ( +
+ {getDummyValue(col, rowIdx)} +
+ ) : ( + + {getDummyValue(col, rowIdx)} + + )} +
+
+
+ +
+
+ +
+
+
+ )}
);