diff --git a/backend-node/src/controllers/screenGroupController.ts b/backend-node/src/controllers/screenGroupController.ts index f14f6532..c7c6023c 100644 --- a/backend-node/src/controllers/screenGroupController.ts +++ b/backend-node/src/controllers/screenGroupController.ts @@ -2058,6 +2058,119 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons }); }); + // 6. v2-repeater 컴포넌트에서 selectedTable/foreignKey 추출 + const v2RepeaterQuery = ` + SELECT DISTINCT + sd.screen_id, + sd.screen_name, + sd.table_name as main_table, + comp->'overrides'->>'type' as component_type, + comp->'overrides'->>'selectedTable' as sub_table, + comp->'overrides'->>'foreignKey' as foreign_key, + comp->'overrides'->>'parentTable' as parent_table + FROM screen_definitions sd + JOIN screen_layouts_v2 slv2 ON sd.screen_id = slv2.screen_id, + jsonb_array_elements(slv2.layout_data->'components') as comp + WHERE sd.screen_id = ANY($1) + AND comp->'overrides'->>'type' = 'v2-repeater' + AND comp->'overrides'->>'selectedTable' IS NOT NULL + `; + const v2RepeaterResult = await pool.query(v2RepeaterQuery, [screenIds]); + v2RepeaterResult.rows.forEach((row: any) => { + const screenId = row.screen_id; + const mainTable = row.main_table; + const subTable = row.sub_table; + const foreignKey = row.foreign_key; + if (!subTable || subTable === mainTable) return; + if (!screenSubTables[screenId]) { + screenSubTables[screenId] = { + screenId, + screenName: row.screen_name, + mainTable: mainTable || '', + subTables: [], + }; + } + const exists = screenSubTables[screenId].subTables.some( + (st) => st.tableName === subTable + ); + if (!exists) { + screenSubTables[screenId].subTables.push({ + tableName: subTable, + componentType: 'v2-repeater', + relationType: 'rightPanelRelation', + fieldMappings: foreignKey ? [{ + sourceField: 'id', + targetField: foreignKey, + sourceDisplayName: 'ID', + targetDisplayName: foreignKey, + }] : undefined, + }); + } + }); + logger.info("v2-repeater 서브 테이블 추출 완료", { + screenIds, + v2RepeaterCount: v2RepeaterResult.rows.length, + }); + + // 7. rightPanel.components 내부의 componentConfig.detailTable 추출 (v2-bom-tree 등) + const v2DetailTableQuery = ` + SELECT DISTINCT + sd.screen_id, + sd.screen_name, + sd.table_name as main_table, + inner_comp->>'type' as component_type, + inner_comp->'componentConfig'->>'detailTable' as sub_table, + inner_comp->'componentConfig'->>'foreignKey' as foreign_key + FROM screen_definitions sd + JOIN screen_layouts_v2 slv2 ON sd.screen_id = slv2.screen_id, + jsonb_array_elements(slv2.layout_data->'components') as comp, + jsonb_array_elements( + COALESCE( + comp->'overrides'->'rightPanel'->'components', + comp->'overrides'->'leftPanel'->'components', + '[]'::jsonb + ) + ) as inner_comp + WHERE sd.screen_id = ANY($1) + AND inner_comp->'componentConfig'->>'detailTable' IS NOT NULL + `; + const v2DetailTableResult = await pool.query(v2DetailTableQuery, [screenIds]); + v2DetailTableResult.rows.forEach((row: any) => { + const screenId = row.screen_id; + const mainTable = row.main_table; + const subTable = row.sub_table; + const foreignKey = row.foreign_key; + if (!subTable || subTable === mainTable) return; + if (!screenSubTables[screenId]) { + screenSubTables[screenId] = { + screenId, + screenName: row.screen_name, + mainTable: mainTable || '', + subTables: [], + }; + } + const exists = screenSubTables[screenId].subTables.some( + (st) => st.tableName === subTable + ); + if (!exists) { + screenSubTables[screenId].subTables.push({ + tableName: subTable, + componentType: row.component_type || 'v2-bom-tree', + relationType: 'rightPanelRelation', + fieldMappings: foreignKey ? [{ + sourceField: 'id', + targetField: foreignKey, + sourceDisplayName: 'ID', + targetDisplayName: foreignKey, + }] : undefined, + }); + } + }); + logger.info("v2-bom-tree/detailTable 서브 테이블 추출 완료", { + screenIds, + v2DetailTableCount: v2DetailTableResult.rows.length, + }); + // ============================================================ // 저장 테이블 정보 추출 // ============================================================ diff --git a/frontend/components/screen/ScreenNode.tsx b/frontend/components/screen/ScreenNode.tsx index ad124b94..1e763735 100644 --- a/frontend/components/screen/ScreenNode.tsx +++ b/frontend/components/screen/ScreenNode.tsx @@ -153,12 +153,12 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => { return (
= ({ data }) => { type="target" position={Position.Left} id="left" - className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/40 opacity-0 transition-all duration-300 group-hover:opacity-100 group-hover:shadow-[0_0_6px_hsl(var(--primary)/0.5)]" + className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/70 dark:!bg-muted-foreground/40 opacity-0 transition-all duration-300 group-hover:opacity-100 group-hover:shadow-[0_0_6px_hsl(var(--primary)/0.5)]" /> {/* 헤더: 그라디언트 제거, 모노크롬 */} -
+
@@ -199,7 +199,7 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
{label}
{tableName &&
{tableName}
}
- {(isMain || isFocused) && } + {(isMain || isFocused) && }
{/* 화면 미리보기 영역 (컴팩트) */} @@ -207,7 +207,7 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => { {layoutSummary ? ( ) : ( -
+
{getScreenTypeIcon(screenType)} 화면: {label}
@@ -215,7 +215,7 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
{/* 푸터 (타입 칩 + 컴포넌트 수) */} -
+
{getScreenTypeLabel(screenType)} {layoutSummary?.totalComponents ?? 0}개 컴포넌트
@@ -267,37 +267,37 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType: // 그리드 화면 일러스트 (모노크롬) if (screenType === "grid") { return ( -
+
{/* 상단 툴바 */}
-
+
-
-
-
+
+
+
{/* 테이블 헤더 */} -
+
{[...Array(5)].map((_, i) => ( -
+
))}
{/* 테이블 행들 */}
{[...Array(7)].map((_, i) => ( -
+
{[...Array(5)].map((_, j) => ( -
+
))}
))}
{/* 페이지네이션 */}
-
-
-
-
+
+
+
+
); @@ -306,18 +306,18 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType: // 폼 화면 일러스트 (모노크롬) if (screenType === "form") { return ( -
+
{/* 폼 필드들 */} {[...Array(6)].map((_, i) => (
-
-
+
+
))} {/* 버튼 영역 */} -
-
-
+
+
+
); @@ -326,23 +326,23 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType: // 대시보드 화면 일러스트 (모노크롬) if (screenType === "dashboard") { return ( -
+
{/* 카드/차트들 */} -
-
-
+
+
+
-
-
-
+
+
+
-
-
+
+
{[...Array(10)].map((_, i) => (
))} @@ -355,13 +355,13 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType: // 액션 화면 일러스트 (모노크롬) if (screenType === "action") { return ( -
-
+
+
-
-
+
+
액션 화면
@@ -370,8 +370,8 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType: // 기본 (알 수 없는 타입, 모노크롬) return ( -
-
+
+
{getScreenTypeIcon(screenType)}
{totalComponents}개 컴포넌트 @@ -506,7 +506,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { return (
= ({ data }) => { ? "border-primary/30 shadow-[0_0_0_1px_hsl(var(--primary)/0.3),0_0_24px_-8px_hsl(var(--primary)/0.1)] bg-card" // 4. 흐리게 처리 : isFaded - ? "opacity-60 bg-card border-border/10" + ? "opacity-60 bg-card border-border/40 dark:border-border/10" // 5. 기본 - : "border-border/10 hover:border-border/20" + : "border-border/40 dark:border-border/10 hover:border-border/50 dark:hover:border-border/20" }`} style={{ filter: isFaded ? "grayscale(80%)" : "none", @@ -548,7 +548,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { type="target" position={Position.Top} id="top" - className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100" + className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/70 dark:!bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100" /> {/* top source: 메인테이블 → 메인테이블 연결용 (위쪽으로 나가는 선) */} = ({ data }) => { position={Position.Top} id="top_source" style={{ top: -4 }} - className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100" + className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/70 dark:!bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100" /> {/* bottom target: 메인테이블 ← 메인테이블 연결용 (아래에서 들어오는 선) */} = ({ data }) => { position={Position.Bottom} id="bottom_target" style={{ bottom: -4 }} - className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100" + className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/70 dark:!bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100" /> {/* 헤더: 그라디언트 제거, bg-muted/30 + 아이콘 박스 */} -
+
{label}
{/* 필터 관계에 따른 문구 변경 */} -
+
{isFilterSource ? "마스터 테이블 (필터 소스)" : hasFilterRelation @@ -602,7 +602,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
{hasActiveColumns && ( - + {displayColumns.length} ref )} @@ -747,7 +747,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { )} {/* 타입 */} - {col.type} + {col.type}
); })} @@ -767,21 +767,21 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
{/* 푸터: cols + PK/FK 카운트 */} -
- +
+ {hasActiveColumns ? `${displayColumns.length}/${totalCount}` : totalCount} cols
{columns?.some(c => c.isPrimaryKey) && ( - PK {columns.filter(c => c.isPrimaryKey).length} + PK {columns.filter(c => c.isPrimaryKey).length} )} {columns?.some(c => c.isForeignKey) && ( - FK {columns.filter(c => c.isForeignKey).length} + FK {columns.filter(c => c.isForeignKey).length} )}