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}
)}