From 5eab4669f0a0ddeae018f500db0b93dda106e8a9 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 13 Feb 2026 14:25:12 +0900 Subject: [PATCH] feat: Update screen management service and UI components for main table handling - Enhanced the `ScreenManagementService` to update the main table name in the database when saving layout data, improving data integrity and tracking. - Modified the `ScreenDesigner` component to include the main table name in the save request, ensuring the correct table is referenced. - Updated the `TablesPanel` to generate unique keys for join tables based on source columns, preventing key collisions and improving rendering performance. - Refactored the `TabsWidget` to streamline screen information loading and removed redundant screen info loading logic, enhancing efficiency and user experience. --- .../src/services/screenManagementService.ts | 12 ++- frontend/components/screen/ScreenDesigner.tsx | 1 + .../components/screen/panels/TablesPanel.tsx | 14 +++- .../components/screen/widgets/TabsWidget.tsx | 61 +++++--------- frontend/components/v2/V2Select.tsx | 80 ++++++++++++++----- .../components/v2-select/V2SelectRenderer.tsx | 10 +-- 6 files changed, 108 insertions(+), 70 deletions(-) diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 87e2ece6..2c25f7e0 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -5177,8 +5177,18 @@ export class ScreenManagementService { throw new Error("이 화면의 레이아웃을 저장할 권한이 없습니다."); } + // 화면의 기본 테이블 업데이트 (테이블이 선택된 경우) + const mainTableName = layoutData.mainTableName; + if (mainTableName) { + await query( + `UPDATE screen_definitions SET table_name = $1, updated_date = NOW() WHERE screen_id = $2`, + [mainTableName, screenId], + ); + console.log(`✅ [saveLayoutV2] 화면 기본 테이블 업데이트: ${mainTableName}`); + } + // 저장할 layout_data에서 레이어 메타 정보 제거 (순수 레이아웃만 저장) - const { layerId: _lid, layerName: _ln, conditionConfig: _cc, ...pureLayoutData } = layoutData; + const { layerId: _lid, layerName: _ln, conditionConfig: _cc, mainTableName: _mtn, ...pureLayoutData } = layoutData; const dataToSave = { version: "2.0", ...pureLayoutData, diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 76bd8973..75937daa 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -2062,6 +2062,7 @@ export default function ScreenDesigner({ await screenApi.saveLayoutV2(selectedScreen.screenId, { ...v2Layout, layerId: currentLayerId, + mainTableName: currentMainTableName, // 화면의 기본 테이블 (DB 업데이트용) }); } else { await screenApi.saveLayout(selectedScreen.screenId, layoutWithResolution); diff --git a/frontend/components/screen/panels/TablesPanel.tsx b/frontend/components/screen/panels/TablesPanel.tsx index 12dcc19a..3cbae41e 100644 --- a/frontend/components/screen/panels/TablesPanel.tsx +++ b/frontend/components/screen/panels/TablesPanel.tsx @@ -44,6 +44,11 @@ interface EntityJoinTable { tableName: string; currentDisplayColumn: string; availableColumns: EntityJoinColumn[]; + // 같은 테이블이 여러 FK로 조인될 수 있으므로 소스 컬럼으로 구분 + joinConfig?: { + sourceColumn: string; + [key: string]: unknown; + }; } interface TablesPanelProps { @@ -414,7 +419,11 @@ export const TablesPanel: React.FC = ({ - {entityJoinTables.map((joinTable) => { + {entityJoinTables.map((joinTable, idx) => { + // 같은 테이블이 여러 FK로 조인될 수 있으므로 sourceColumn으로 고유 키 생성 + const uniqueKey = joinTable.joinConfig?.sourceColumn + ? `entity-join-${joinTable.tableName}-${joinTable.joinConfig.sourceColumn}` + : `entity-join-${joinTable.tableName}-${idx}`; const isExpanded = expandedJoinTables.has(joinTable.tableName); // 검색어로 필터링 const filteredColumns = searchTerm @@ -431,8 +440,7 @@ export const TablesPanel: React.FC = ({ } return ( - // 엔티티 조인 테이블에 고유 접두사 추가 (메인 테이블과 키 중복 방지) -
+
{/* 조인 테이블 헤더 */}
>({}); const [screenLoadingStates, setScreenLoadingStates] = useState>({}); const [screenErrors, setScreenErrors] = useState>({}); - // 탭별 화면 정보 (screenId, tableName) 저장 - const [screenInfoMap, setScreenInfoMap] = useState>({}); + // 탭별 화면 정보 (screenId, tableName) - 인라인 컴포넌트의 테이블 설정에서 추출 + const screenInfoMap = React.useMemo(() => { + const map: Record = {}; + for (const tab of tabs as ExtendedTabItem[]) { + const inlineComponents = tab.components || []; + if (inlineComponents.length > 0) { + // 인라인 컴포넌트에서 테이블 컴포넌트의 selectedTable 추출 + const tableComp = inlineComponents.find( + (c) => c.componentType === "v2-table-list" || c.componentType === "table-list", + ); + const selectedTable = tableComp?.componentConfig?.selectedTable; + if (selectedTable || tab.screenId) { + map[tab.id] = { + id: tab.screenId, + tableName: selectedTable, + }; + } + } + } + return map; + }, [tabs]); // 컴포넌트 탭 목록 변경 시 동기화 useEffect(() => { @@ -157,21 +176,10 @@ export function TabsWidget({ ) { setScreenLoadingStates((prev) => ({ ...prev, [tab.id]: true })); try { - // 레이아웃과 화면 정보를 병렬로 로드 - const [layoutData, screenDef] = await Promise.all([ - screenApi.getLayout(extTab.screenId), - screenApi.getScreen(extTab.screenId), - ]); + const layoutData = await screenApi.getLayout(extTab.screenId); if (layoutData && layoutData.components) { setScreenLayouts((prev) => ({ ...prev, [tab.id]: layoutData.components })); } - // 탭의 화면 정보 저장 (tableName 포함) - if (screenDef) { - setScreenInfoMap((prev) => ({ - ...prev, - [tab.id]: { id: extTab.screenId!, tableName: screenDef.tableName }, - })); - } } catch (error) { console.error(`탭 "${tab.label}" 화면 로드 실패:`, error); setScreenErrors((prev) => ({ ...prev, [tab.id]: "화면을 불러올 수 없습니다." })); @@ -185,31 +193,6 @@ export function TabsWidget({ loadScreenLayouts(); }, [visibleTabs, screenLayouts, screenLoadingStates]); - // screenInfoMap이 없는 탭의 화면 정보 보충 로드 - // screenId가 있지만 screenInfoMap에 아직 없는 탭의 화면 정보를 로드 - useEffect(() => { - const loadMissingScreenInfo = async () => { - for (const tab of visibleTabs) { - const extTab = tab as ExtendedTabItem; - // screenId가 있고 screenInfoMap에 아직 없는 경우 로드 - if (extTab.screenId && !screenInfoMap[tab.id]) { - try { - const screenDef = await screenApi.getScreen(extTab.screenId); - if (screenDef) { - setScreenInfoMap((prev) => ({ - ...prev, - [tab.id]: { id: extTab.screenId!, tableName: screenDef.tableName }, - })); - } - } catch (error) { - console.error(`탭 "${tab.label}" 화면 정보 로드 실패:`, error); - } - } - } - }; - loadMissingScreenInfo(); - }, [visibleTabs, screenInfoMap]); - // 선택된 탭 변경 시 localStorage에 저장 + ActiveTab Context 업데이트 useEffect(() => { if (persistSelection && typeof window !== "undefined") { diff --git a/frontend/components/v2/V2Select.tsx b/frontend/components/v2/V2Select.tsx index c7ea8c94..2029f473 100644 --- a/frontend/components/v2/V2Select.tsx +++ b/frontend/components/v2/V2Select.tsx @@ -58,28 +58,56 @@ const DropdownSelect = forwardRef { const [open, setOpen] = useState(false); + // 현재 선택된 값 존재 여부 + const hasValue = useMemo(() => { + if (!value) return false; + if (Array.isArray(value)) return value.length > 0; + return value !== ""; + }, [value]); + // 단일 선택 + 검색 불가능 → 기본 Select 사용 if (!searchable && !multiple) { return ( - +
+ + {/* 초기화 버튼 (값이 있을 때만 표시) */} + {allowClear && hasValue && !disabled && ( + { + e.stopPropagation(); + e.preventDefault(); + onChange?.(""); + }} + onPointerDown={(e) => { + e.stopPropagation(); + e.preventDefault(); + }} + > + + + )} +
); } @@ -142,10 +170,18 @@ const DropdownSelect = forwardRef
{allowClear && selectedValues.length > 0 && ( - + onPointerDown={(e) => { + // Radix Popover가 onPointerDown으로 팝오버를 여는 것을 방지 + e.stopPropagation(); + e.preventDefault(); + }} + > + + )}
diff --git a/frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx b/frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx index 5ab010f2..18898198 100644 --- a/frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx +++ b/frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx @@ -70,18 +70,18 @@ export class V2SelectRenderer extends AutoRegisteringComponentRenderer { } // 🆕 formData에 값이 없고 기본값이 설정된 경우, 기본값 적용 + // 단, formData에 해당 키가 이미 존재하면(사용자가 명시적으로 초기화한 경우) 기본값을 재적용하지 않음 + const hasKeyInFormData = formData !== undefined && formData !== null && columnName in (formData || {}); if ( (currentValue === "" || currentValue === undefined || currentValue === null) && defaultValue && isInteractive && onFormDataChange && - columnName + columnName && + !hasKeyInFormData // formData에 키 자체가 없을 때만 기본값 적용 (초기 렌더링) ) { - // 초기 렌더링 시 기본값을 formData에 설정 setTimeout(() => { - if (!formData?.[columnName]) { - onFormDataChange(columnName, defaultValue); - } + onFormDataChange(columnName, defaultValue); }, 0); currentValue = defaultValue; }