From 8ca1890fc0ef1430684f709edb64007e34d27eb1 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Fri, 13 Mar 2026 17:45:12 +0900 Subject: [PATCH 01/39] .. --- docker/dev/docker-compose.backend.mac.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/dev/docker-compose.backend.mac.yml b/docker/dev/docker-compose.backend.mac.yml index ed4602dd..4d862d9e 100644 --- a/docker/dev/docker-compose.backend.mac.yml +++ b/docker/dev/docker-compose.backend.mac.yml @@ -12,7 +12,7 @@ services: environment: - NODE_ENV=development - PORT=8080 - - DATABASE_URL=postgresql://postgres:vexplor0909!!@211.115.91.141:11134/vexplor + - DATABASE_URL=postgresql://postgres:ph0909!!@39.117.244.52:11132/plm - JWT_SECRET=ilshin-plm-super-secret-jwt-key-2024 - JWT_EXPIRES_IN=24h - CORS_ORIGIN=http://localhost:9771 From c3a43179e35d6e2bc3cfdc075c0cee7ec8081f19 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 15:15:44 +0900 Subject: [PATCH 02/39] refactor: update color schemes and improve component styling - Changed color schemes for various screen types and roles to align with the new design guidelines, enhancing visual consistency across the application. - Updated background colors for components based on their types, such as changing 'bg-slate-400' to 'bg-muted-foreground' and adjusting other color mappings for better clarity. - Improved the styling of the ScreenNode and V2PropertiesPanel components to ensure a more cohesive user experience. - Enhanced the DynamicComponentRenderer to support dynamic loading of column metadata with cache invalidation for better performance. These changes aim to refine the UI and improve the overall aesthetic of the application, ensuring a more modern and user-friendly interface. --- frontend/components/screen/ScreenNode.tsx | 174 +++++++++--------- .../screen/panels/V2PropertiesPanel.tsx | 29 ++- .../v2/config-panels/V2FieldConfigPanel.tsx | 12 +- .../lib/registry/DynamicComponentRenderer.tsx | 155 ++++++++++++---- ...creen-149-field-type-verification-guide.md | 165 +++++++++++++++++ 5 files changed, 404 insertions(+), 131 deletions(-) create mode 100644 test-output/screen-149-field-type-verification-guide.md diff --git a/frontend/components/screen/ScreenNode.tsx b/frontend/components/screen/ScreenNode.tsx index ff5ade46..70930e21 100644 --- a/frontend/components/screen/ScreenNode.tsx +++ b/frontend/components/screen/ScreenNode.tsx @@ -109,14 +109,14 @@ const getScreenTypeIcon = (screenType?: string) => { // 화면 타입별 색상 (헤더) const getScreenTypeColor = (screenType?: string, isMain?: boolean) => { - if (!isMain) return "bg-slate-400"; + if (!isMain) return "bg-muted-foreground"; switch (screenType) { case "grid": - return "bg-violet-500"; + return "bg-primary"; case "dashboard": - return "bg-amber-500"; + return "bg-warning"; case "action": - return "bg-rose-500"; + return "bg-destructive"; default: return "bg-primary"; } @@ -124,25 +124,25 @@ const getScreenTypeColor = (screenType?: string, isMain?: boolean) => { // 화면 역할(screenRole)에 따른 색상 const getScreenRoleColor = (screenRole?: string) => { - if (!screenRole) return "bg-slate-400"; + if (!screenRole) return "bg-muted-foreground"; // 역할명에 포함된 키워드로 색상 결정 const role = screenRole.toLowerCase(); if (role.includes("그리드") || role.includes("grid") || role.includes("메인") || role.includes("main") || role.includes("list")) { - return "bg-violet-500"; // 보라색 - 메인 그리드 + return "bg-primary"; // 메인 그리드 } if (role.includes("등록") || role.includes("폼") || role.includes("form") || role.includes("register") || role.includes("input")) { - return "bg-primary"; // 파란색 - 등록 폼 + return "bg-primary"; // 등록 폼 } if (role.includes("액션") || role.includes("action") || role.includes("이벤트") || role.includes("event") || role.includes("클릭")) { - return "bg-rose-500"; // 빨간색 - 액션/이벤트 + return "bg-destructive"; // 액션/이벤트 } if (role.includes("상세") || role.includes("detail") || role.includes("popup") || role.includes("팝업")) { - return "bg-amber-500"; // 주황색 - 상세/팝업 + return "bg-warning"; // 상세/팝업 } - return "bg-slate-400"; // 기본 회색 + return "bg-muted-foreground"; // 기본 회색 }; // 화면 타입별 라벨 @@ -246,17 +246,17 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => { ?.filter(item => item.label && !item.componentKind?.includes('button')) ?.slice(0, 6) ?.map((item, idx) => ( -
+
- {item.label} - {item.componentKind?.split('-')[0] || 'field'} + {item.label} + {item.componentKind?.split('-')[0] || 'field'}
)) || ( -
필드 정보 없음
+
필드 정보 없음
)}
@@ -280,33 +280,33 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => { const getComponentColor = (componentKind: string) => { // 테이블/그리드 관련 if (componentKind === "table-list" || componentKind === "data-grid") { - return "bg-violet-200 border-violet-400"; + return "bg-primary/20 border-primary/40"; } // 검색 필터 if (componentKind === "table-search-widget" || componentKind === "search-filter") { - return "bg-pink-200 border-pink-400"; + return "bg-destructive/20 border-destructive/40"; } // 버튼 관련 if (componentKind?.includes("button")) { - return "bg-blue-300 border-primary"; + return "bg-primary/30 border-primary"; } // 입력 필드 if (componentKind?.includes("input") || componentKind?.includes("text")) { - return "bg-slate-200 border-slate-400"; + return "bg-muted border-border"; } // 셀렉트/드롭다운 if (componentKind?.includes("select") || componentKind?.includes("dropdown")) { - return "bg-amber-200 border-amber-400"; + return "bg-warning/20 border-warning/40"; } // 차트 if (componentKind?.includes("chart")) { - return "bg-emerald-200 border-emerald-400"; + return "bg-success/20 border-success/40"; } // 커스텀 위젯 if (componentKind === "custom") { - return "bg-pink-200 border-pink-400"; + return "bg-destructive/20 border-destructive/40"; } - return "bg-slate-100 border-slate-300"; + return "bg-muted/50 border-border"; }; // ========== 화면 미리보기 컴포넌트 - 화면 타입별 간단한 일러스트 ========== @@ -322,16 +322,16 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
{/* 상단 툴바 */}
-
+
-
+
{/* 테이블 헤더 */} -
+
{[...Array(5)].map((_, i) => ( -
+
))}
{/* 테이블 행들 */} @@ -352,7 +352,7 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
{/* 컴포넌트 수 */} -
+
{totalComponents}개
@@ -376,7 +376,7 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
{/* 컴포넌트 수 */} -
+
{totalComponents}개
@@ -388,13 +388,13 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType: return (
{/* 카드/차트들 */} -
-
-
+
+
+
-
-
-
+
+
+
@@ -402,14 +402,14 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType: {[...Array(10)].map((_, i) => (
))}
{/* 컴포넌트 수 */} -
+
{totalComponents}개
@@ -429,7 +429,7 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
액션 화면
{/* 컴포넌트 수 */} -
+
{totalComponents}개
@@ -438,8 +438,8 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType: // 기본 (알 수 없는 타입) return ( -
-
+
+
{getScreenTypeIcon(screenType)}
{totalComponents}개 컴포넌트 @@ -575,20 +575,20 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { return (
= ({ data }) => { className="absolute -left-1.5 top-1 bottom-1 w-0.5 z-20 rounded-full transition-all duration-500 ease-out" title={hasSaveTarget ? "저장 대상 테이블" : undefined} style={{ - background: 'linear-gradient(to bottom, transparent 0%, #f472b6 15%, #f472b6 85%, transparent 100%)', + background: `linear-gradient(to bottom, transparent 0%, hsl(var(--destructive)) 15%, hsl(var(--destructive)) 85%, transparent 100%)`, opacity: hasSaveTarget ? 1 : 0, transform: hasSaveTarget ? 'scaleY(1)' : 'scaleY(0)', transformOrigin: 'top', @@ -616,7 +616,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { type="target" position={Position.Top} id="top" - className="!h-2 !w-2 !border-2 !border-background !bg-emerald-500 opacity-0 transition-opacity group-hover:opacity-100" + className="!h-2 !w-2 !border-2 !border-background !bg-primary 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-2 !border-background !bg-amber-500 opacity-0 transition-opacity group-hover:opacity-100" + className="!h-2 !w-2 !border-2 !border-background !bg-warning 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-2 !border-background !bg-amber-500 opacity-0 transition-opacity group-hover:opacity-100" + className="!h-2 !w-2 !border-2 !border-background !bg-warning opacity-0 transition-opacity group-hover:opacity-100" /> - {/* 헤더 (필터 관계: 보라색, 필터 소스: 보라색, 메인: 초록색, 기본: 슬레이트) */} + {/* 헤더 (필터 관계: primary, 필터 소스: primary, 메인: primary, 기본: muted-foreground) */}
@@ -679,7 +679,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { {/* 컬럼 목록 - 컴팩트하게 (부드러운 높이 전환 + 스크롤) */} {/* 뱃지도 이 영역 안에 포함되어 높이 계산에 반영됨 */}
= ({ data }) => { {/* 필터 뱃지 */} {filterRefs.length > 0 && ( `${r.fromTable}.${r.fromColumn || 'id'} → ${r.toColumn}`).join('\n')}`} > @@ -707,14 +707,14 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { )} {filterRefs.length > 0 && ( - + {filterRefs.map(r => `${r.fromTableLabel || r.fromTable}.${r.fromColumnLabel || r.fromColumn || 'id'}`).join(', ')} )} {/* 참조 뱃지 */} {lookupRefs.length > 0 && ( `${r.fromTable} → ${r.toColumn}`).join('\n')}`} > {lookupRefs.length}곳 참조 @@ -745,14 +745,14 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { key={col.name} className={`flex items-center gap-1 rounded px-1.5 py-0.5 transition-all duration-300 ${ isJoinColumn - ? "bg-amber-100 border border-orange-300 shadow-sm" + ? "bg-warning/10 border border-warning/30 shadow-sm" : isFilterColumn || isFilterSourceColumn - ? "bg-violet-100 border border-violet-300 shadow-sm" // 필터 컬럼/필터 소스: 보라색 + ? "bg-primary/10 border border-primary/30 shadow-sm" // 필터 컬럼/필터 소스 : isHighlighted ? "bg-primary/10 border border-primary/40 shadow-sm" : hasActiveColumns - ? "bg-slate-100" - : "bg-slate-50 hover:bg-slate-100" + ? "bg-muted" + : "bg-muted/50 hover:bg-muted" }`} style={{ animation: hasActiveColumns ? `fadeIn 0.5s ease-out ${idx * 80}ms forwards` : undefined, @@ -760,18 +760,18 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { }} > {/* PK/FK/조인/필터 아이콘 */} - {isJoinColumn && } - {(isFilterColumn || isFilterSourceColumn) && !isJoinColumn && } - {!isJoinColumn && !isFilterColumn && !isFilterSourceColumn && col.isPrimaryKey && } + {isJoinColumn && } + {(isFilterColumn || isFilterSourceColumn) && !isJoinColumn && } + {!isJoinColumn && !isFilterColumn && !isFilterSourceColumn && col.isPrimaryKey && } {!isJoinColumn && !isFilterColumn && !isFilterSourceColumn && col.isForeignKey && !col.isPrimaryKey && } {!isJoinColumn && !isFilterColumn && !isFilterSourceColumn && !col.isPrimaryKey && !col.isForeignKey &&
} {/* 컬럼명 */} {col.name} @@ -781,51 +781,51 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { <> {/* 조인 참조 테이블 표시 (joinColumnRefs에서) */} {joinRefMap.has(colOriginal) && ( - + ← {joinRefMap.get(colOriginal)?.refTableLabel} )} {/* 필드 매핑 참조 표시 (fieldMappingMap에서, joinRefMap에 없는 경우) */} {!joinRefMap.has(colOriginal) && fieldMappingMap.has(colOriginal) && ( - + ← {fieldMappingMap.get(colOriginal)?.sourceDisplayName} )} - 조인 + 조인 )} {isFilterColumn && !isJoinColumn && ( - 필터 + 필터 )} {/* 메인 테이블에서 필터 소스로 사용되는 컬럼: "필터" + "사용" 둘 다 표시 */} {isFilterSourceColumn && !isJoinColumn && !isFilterColumn && ( <> - 필터 + 필터 {isHighlighted && ( - 사용 + 사용 )} )} {isHighlighted && !isJoinColumn && !isFilterColumn && !isFilterSourceColumn && ( - 사용 + 사용 )} {/* 타입 */} - {col.type} + {col.type}
); })} {/* 더 많은 컬럼이 있을 경우 표시 */} {remainingCount > 0 && ( -
+
+ {remainingCount}개 더
)}
) : (
- - 컬럼 정보 없음 + + 컬럼 정보 없음
)}
@@ -861,10 +861,10 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { export const LegacyScreenNode = ScreenNode; export const AggregateNode: React.FC<{ data: any }> = ({ data }) => { return ( -
- - -
+
+ + +
{data.label || "Aggregate"}
diff --git a/frontend/components/screen/panels/V2PropertiesPanel.tsx b/frontend/components/screen/panels/V2PropertiesPanel.tsx index cf148e6e..ef739b27 100644 --- a/frontend/components/screen/panels/V2PropertiesPanel.tsx +++ b/frontend/components/screen/panels/V2PropertiesPanel.tsx @@ -47,6 +47,7 @@ import { ColorPickerWithTransparent } from "../common/ColorPickerWithTransparent // ComponentRegistry import (동적 ConfigPanel 가져오기용) import { ComponentRegistry } from "@/lib/registry/ComponentRegistry"; +import { columnMetaCache } from "@/lib/registry/DynamicComponentRenderer"; import { DynamicComponentConfigPanel, hasComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel"; import StyleEditor from "../StyleEditor"; import { Slider } from "@/components/ui/slider"; @@ -207,28 +208,36 @@ export const V2PropertiesPanel: React.FC = ({ onUpdateProperty(selectedComponent.id, "componentConfig", { ...currentConfig, ...newConfig }); }; - // 컬럼의 inputType 가져오기 (entity 타입인지 확인용) - const inputType = currentConfig.inputType || currentConfig.webType || (selectedComponent as any).inputType; - // 현재 화면의 테이블명 가져오기 const currentTableName = tables?.[0]?.tableName; + // DB input_type 가져오기 (columnMetaCache에서 최신값 조회) + const colName = selectedComponent.columnName || currentConfig.fieldKey || currentConfig.columnName; + const tblName = selectedComponent.tableName || currentTable?.tableName || currentTableName; + const dbMeta = colName && tblName && !colName.includes(".") ? columnMetaCache[tblName]?.[colName] : undefined; + const dbInputType = dbMeta ? (() => { const raw = dbMeta.input_type || dbMeta.inputType; return raw === "direct" || raw === "auto" ? undefined : raw; })() : undefined; + const inputType = dbInputType || currentConfig.inputType || currentConfig.webType || (selectedComponent as any).inputType; + // 컴포넌트별 추가 props const extraProps: Record = {}; - if (componentId === "v2-select") { + const resolvedTableName = selectedComponent.tableName || currentTable?.tableName || currentTableName; + const resolvedColumnName = selectedComponent.columnName || currentConfig.fieldKey || currentConfig.columnName; + + if (componentId === "v2-input" || componentId === "v2-select") { extraProps.inputType = inputType; - extraProps.tableName = selectedComponent.tableName || currentTable?.tableName || currentTableName; - extraProps.columnName = selectedComponent.columnName || currentConfig.fieldKey || currentConfig.columnName; + extraProps.tableName = resolvedTableName; + extraProps.columnName = resolvedColumnName; + extraProps.screenTableName = resolvedTableName; + } + if (componentId === "v2-input") { + extraProps.allComponents = allComponents; } if (componentId === "v2-list") { extraProps.currentTableName = currentTableName; } if (componentId === "v2-bom-item-editor" || componentId === "v2-bom-tree") { extraProps.currentTableName = currentTableName; - extraProps.screenTableName = selectedComponent.tableName || currentTable?.tableName || currentTableName; - } - if (componentId === "v2-input") { - extraProps.allComponents = allComponents; + extraProps.screenTableName = resolvedTableName; } return ( diff --git a/frontend/components/v2/config-panels/V2FieldConfigPanel.tsx b/frontend/components/v2/config-panels/V2FieldConfigPanel.tsx index 7dfe8834..2f2b8011 100644 --- a/frontend/components/v2/config-panels/V2FieldConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2FieldConfigPanel.tsx @@ -78,7 +78,15 @@ interface CategoryValueOption { } // ─── 하위 호환: 기존 config에서 fieldType 추론 ─── -function resolveFieldType(config: Record, componentType?: string): FieldType { +function resolveFieldType(config: Record, componentType?: string, metaInputType?: string): FieldType { + // DB input_type이 전달된 경우 (데이터타입관리에서 변경 시) 우선 적용 + if (metaInputType && metaInputType !== "direct" && metaInputType !== "auto") { + const dbType = metaInputType as FieldType; + if (["text", "number", "textarea", "numbering", "select", "category", "entity"].includes(dbType)) { + return dbType; + } + } + if (config.fieldType) return config.fieldType as FieldType; // v2-select 계열 @@ -207,7 +215,7 @@ export const V2FieldConfigPanel: React.FC = ({ inputType: metaInputType, componentType, }) => { - const fieldType = resolveFieldType(config, componentType); + const fieldType = resolveFieldType(config, componentType, metaInputType); const isSelectGroup = ["select", "category", "entity"].includes(fieldType); // ─── 채번 관련 상태 (테이블 기반) ─── diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 873b7408..859d136f 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -13,13 +13,34 @@ import { apiClient } from "@/lib/api/client"; import { getAdaptiveLabelColor } from "@/lib/utils/darkModeColor"; // 컬럼 메타데이터 캐시 (테이블명 → 컬럼 설정 맵) -const columnMetaCache: Record> = {}; +export const columnMetaCache: Record> = {}; const columnMetaLoading: Record> = {}; +const columnMetaTimestamp: Record = {}; +const CACHE_TTL_MS = 5000; -async function loadColumnMeta(tableName: string): Promise { - if (columnMetaCache[tableName]) return; +export function invalidateColumnMetaCache(tableName?: string): void { + if (tableName) { + delete columnMetaCache[tableName]; + delete columnMetaLoading[tableName]; + delete columnMetaTimestamp[tableName]; + } else { + for (const key of Object.keys(columnMetaCache)) delete columnMetaCache[key]; + for (const key of Object.keys(columnMetaLoading)) delete columnMetaLoading[key]; + for (const key of Object.keys(columnMetaTimestamp)) delete columnMetaTimestamp[key]; + } +} + +async function loadColumnMeta(tableName: string, forceReload = false): Promise { + const now = Date.now(); + const isStale = columnMetaTimestamp[tableName] && (now - columnMetaTimestamp[tableName] > CACHE_TTL_MS); + + if (!forceReload && !isStale && columnMetaCache[tableName]) return; + + if (forceReload || isStale) { + delete columnMetaCache[tableName]; + delete columnMetaLoading[tableName]; + } - // 이미 로딩 중이면 해당 Promise를 대기 (race condition 방지) if (columnMetaLoading[tableName]) { await columnMetaLoading[tableName]; return; @@ -36,6 +57,7 @@ async function loadColumnMeta(tableName: string): Promise { if (name) map[name] = col; } columnMetaCache[tableName] = map; + columnMetaTimestamp[tableName] = Date.now(); } catch (e) { console.error(`[columnMeta] ${tableName} 로드 실패:`, e); columnMetaCache[tableName] = {}; @@ -56,43 +78,59 @@ export function isColumnRequiredByMeta(tableName?: string, columnName?: string): return nullable === "NO" || nullable === "N"; } -// table_type_columns 기반 componentConfig 병합 (기존 설정이 없을 때만 DB 메타데이터로 보완) +// table_type_columns 기반 componentConfig 병합 (DB input_type 우선 적용) function mergeColumnMeta(tableName: string | undefined, columnName: string | undefined, componentConfig: any): any { if (!tableName || !columnName) return componentConfig; const meta = columnMetaCache[tableName]?.[columnName]; if (!meta) return componentConfig; - const inputType = meta.input_type || meta.inputType; - if (!inputType) return componentConfig; - - // 이미 source가 올바르게 설정된 경우 건드리지 않음 - const existingSource = componentConfig?.source; - if (existingSource && existingSource !== "static" && existingSource !== "distinct" && existingSource !== "select") { - return componentConfig; - } + const rawType = meta.input_type || meta.inputType; + const dbInputType = rawType === "direct" || rawType === "auto" ? undefined : rawType; + if (!dbInputType) return componentConfig; const merged = { ...componentConfig }; + const savedFieldType = merged.fieldType; - // source가 미설정/기본값일 때만 DB 메타데이터로 보완 - if (inputType === "entity") { + // savedFieldType이 있고 DB와 같으면 변경 불필요 + if (savedFieldType && savedFieldType === dbInputType) return merged; + // savedFieldType이 있고 DB와 다르면 — 사용자가 V2FieldConfigPanel에서 설정한 값 존중 + if (savedFieldType) return merged; + + // savedFieldType이 없으면: DB input_type 기준으로 동기화 + // 기존 overrides의 source/inputType이 DB와 불일치하면 덮어씀 + if (dbInputType === "entity") { const refTable = meta.reference_table || meta.referenceTable; const refColumn = meta.reference_column || meta.referenceColumn; const displayCol = meta.display_column || meta.displayColumn; - if (refTable && !merged.entityTable) { + if (refTable) { merged.source = "entity"; merged.entityTable = refTable; merged.entityValueColumn = refColumn || "id"; merged.entityLabelColumn = displayCol || "name"; + merged.fieldType = "entity"; + merged.inputType = "entity"; } - } else if (inputType === "category" && !existingSource) { + } else if (dbInputType === "category") { merged.source = "category"; - } else if (inputType === "select" && !existingSource) { + merged.fieldType = "category"; + merged.inputType = "category"; + } else if (dbInputType === "select") { + if (!merged.source || merged.source === "category" || merged.source === "entity") { + merged.source = "static"; + } const detail = typeof meta.detail_settings === "string" ? JSON.parse(meta.detail_settings || "{}") : meta.detail_settings || {}; if (detail.options && !merged.options?.length) { merged.options = detail.options; } + merged.fieldType = "select"; + merged.inputType = "select"; + } else { + // text, number, textarea 등 input 계열 — 카테고리 잔류 속성 제거 + merged.fieldType = dbInputType; + merged.inputType = dbInputType; + delete merged.source; } return merged; @@ -266,15 +304,27 @@ export const DynamicComponentRenderer: React.FC = children, ...props }) => { - // 컬럼 메타데이터 로드 트리거 (테이블명이 있으면 비동기 로드) + // 컬럼 메타데이터 로드 트리거 (TTL 기반 자동 갱신) const screenTableName = props.tableName || (component as any).tableName; - const [, forceUpdate] = React.useState(0); + const [metaVersion, forceUpdate] = React.useState(0); React.useEffect(() => { if (screenTableName) { loadColumnMeta(screenTableName).then(() => forceUpdate((v) => v + 1)); } }, [screenTableName]); + // table-columns-refresh 이벤트 수신 시 캐시 무효화 후 최신 메타 다시 로드 + React.useEffect(() => { + const handler = () => { + if (screenTableName) { + invalidateColumnMetaCache(screenTableName); + loadColumnMeta(screenTableName, true).then(() => forceUpdate((v) => v + 1)); + } + }; + window.addEventListener("table-columns-refresh", handler); + return () => window.removeEventListener("table-columns-refresh", handler); + }, [screenTableName]); + // 컴포넌트 타입 추출 - 새 시스템에서는 componentType 속성 사용, 레거시는 type 사용 // 🆕 V2 레이아웃의 경우 url에서 컴포넌트 타입 추출 (예: "@/lib/registry/components/v2-input" → "v2-input") const extractTypeFromUrl = (url: string | undefined): string | undefined => { @@ -306,12 +356,40 @@ export const DynamicComponentRenderer: React.FC = const mappedComponentType = mapToV2ComponentType(rawComponentType); - // fieldType 기반 동적 컴포넌트 전환 (통합 필드 설정 패널에서 설정된 값) + // fieldType 기반 동적 컴포넌트 전환 (사용자 설정 > DB input_type > 기본값) const componentType = (() => { - const ft = (component as any).componentConfig?.fieldType; - if (!ft) return mappedComponentType; - if (["text", "number", "password", "textarea", "slider", "color", "numbering"].includes(ft)) return "v2-input"; - if (["select", "category", "entity"].includes(ft)) return "v2-select"; + const configFieldType = (component as any).componentConfig?.fieldType; + const fieldName = (component as any).columnName || (component as any).componentConfig?.fieldKey || (component as any).componentConfig?.columnName; + const isEntityJoin = fieldName?.includes("."); + const baseCol = isEntityJoin ? undefined : fieldName; + const rawDbType = baseCol && screenTableName + ? (columnMetaCache[screenTableName]?.[baseCol]?.input_type || columnMetaCache[screenTableName]?.[baseCol]?.inputType) + : undefined; + const dbInputType = rawDbType === "direct" || rawDbType === "auto" ? undefined : rawDbType; + + // 디버그 (division, unit 필드만) - 문제 확인 후 제거 + if (baseCol && (baseCol === "division" || baseCol === "unit")) { + const result = configFieldType + ? (["text","number","password","textarea","slider","color","numbering"].includes(configFieldType) ? "v2-input" : "v2-select") + : dbInputType + ? (["text","number","password","textarea","slider","color","numbering"].includes(dbInputType) ? "v2-input" : "v2-select") + : mappedComponentType; + const skipCat = dbInputType && !["category", "entity", "select"].includes(dbInputType); + console.log(`[DCR] ${baseCol}: dbInputType=${dbInputType}, RESULT=${result}, skipCat=${skipCat}`); + } + + // 사용자가 V2FieldConfigPanel에서 명시적으로 설정한 fieldType 최우선 + if (configFieldType) { + if (["text", "number", "password", "textarea", "slider", "color", "numbering"].includes(configFieldType)) return "v2-input"; + if (["select", "category", "entity"].includes(configFieldType)) return "v2-select"; + } + + // componentConfig.fieldType 없으면 DB input_type 참조 (초기 로드 시) + if (dbInputType) { + if (["text", "number", "password", "textarea", "slider", "color", "numbering"].includes(dbInputType)) return "v2-input"; + if (["select", "category", "entity"].includes(dbInputType)) return "v2-select"; + } + return mappedComponentType; })(); @@ -376,15 +454,24 @@ export const DynamicComponentRenderer: React.FC = // (v2-input, v2-select, v2-repeat-container 등 모두 동일하게 처리) // 🎯 카테고리 타입 우선 처리 (inputType 또는 webType 확인) - const inputType = (component as any).componentConfig?.inputType || (component as any).inputType; + // DB input_type이 "text" 등 비-카테고리로 변경된 경우 이 분기를 건너뜀 + const savedInputType = (component as any).componentConfig?.inputType || (component as any).inputType; const webType = (component as any).componentConfig?.webType; const tableName = (component as any).tableName; const columnName = (component as any).columnName; - // 카테고리 셀렉트: webType이 "category"이고 tableName과 columnName이 있는 경우만 - // ⚠️ 단, 다음 경우는 V2SelectRenderer로 직접 처리 (고급 모드 지원): - // 1. componentType이 "select-basic" 또는 "v2-select"인 경우 - // 2. config.mode가 dropdown이 아닌 경우 (radio, check, tagbox 등) + // DB input_type 확인: 데이터타입관리에서 변경한 최신 값이 레이아웃 저장값보다 우선 + const dbMetaForField = columnName && screenTableName && !columnName.includes(".") + ? columnMetaCache[screenTableName]?.[columnName] + : undefined; + const dbFieldInputType = dbMetaForField + ? (() => { const raw = dbMetaForField.input_type || dbMetaForField.inputType; return raw === "direct" || raw === "auto" ? undefined : raw; })() + : undefined; + // DB에서 확인된 타입이 있으면 그걸 사용, 없으면 저장된 값 사용 + const inputType = dbFieldInputType || savedInputType; + // webType도 DB 값으로 대체 (레이아웃에 webType: "category" 하드코딩되어 있을 수 있음) + const effectiveWebType = dbFieldInputType || webType; + const componentMode = (component as any).componentConfig?.mode || (component as any).config?.mode; const isMultipleSelect = (component as any).componentConfig?.multiple; const nonDropdownModes = ["radio", "check", "checkbox", "tag", "tagbox", "toggle", "swap", "combobox"]; @@ -392,7 +479,11 @@ export const DynamicComponentRenderer: React.FC = const shouldUseV2Select = componentType === "select-basic" || componentType === "v2-select" || isNonDropdownMode || isMultipleSelect; - if ((inputType === "category" || webType === "category") && tableName && columnName && shouldUseV2Select) { + // DB input_type이 비-카테고리(text 등)로 확인된 경우, 레이아웃에 category가 남아있어도 카테고리 분기 강제 스킵 + // dbFieldInputType이 있으면(캐시 로드됨) 그 값으로 판단, 없으면 기존 로직 유지 + const isDbConfirmedNonCategory = dbFieldInputType && !["category", "entity", "select"].includes(dbFieldInputType); + + if (!isDbConfirmedNonCategory && (inputType === "category" || effectiveWebType === "category") && tableName && columnName && shouldUseV2Select) { // V2SelectRenderer로 직접 렌더링 (카테고리 + 고급 모드) try { const { V2SelectRenderer } = require("@/lib/registry/components/v2-select/V2SelectRenderer"); @@ -491,7 +582,7 @@ export const DynamicComponentRenderer: React.FC = } catch (error) { console.error("❌ V2SelectRenderer 로드 실패:", error); } - } else if ((inputType === "category" || webType === "category") && tableName && columnName) { + } else if (!isDbConfirmedNonCategory && (inputType === "category" || effectiveWebType === "category") && tableName && columnName) { try { const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent"); const fieldName = columnName || component.id; diff --git a/test-output/screen-149-field-type-verification-guide.md b/test-output/screen-149-field-type-verification-guide.md new file mode 100644 index 00000000..a18bfb11 --- /dev/null +++ b/test-output/screen-149-field-type-verification-guide.md @@ -0,0 +1,165 @@ +# Screen 149 필드 타입 검증 가이드 + +## 배경 +- **화면 149**: 품목정보 (item_info 테이블) 폼 +- **division 컬럼**: DB에서 `input_type = 'text'`로 변경했으나 화면에는 여전히 SELECT 드롭다운으로 표시 +- **unit 컬럼**: `input_type = 'category'` → SELECT 드롭다운으로 표시되어야 함 + +## DB 현황 (vexplor-dev 조회 결과) + +| column_name | company_code | input_type | +|-------------|--------------|------------| +| division | * | category | +| division | COMPANY_7 | **text** | +| division | COMPANY_8, 9, 10, 18, 19, 20, 21 | category | +| unit | * | text | +| unit | COMPANY_18, 19, 20, 21, 7, 8, 9 | **category** | + +**주의:** `wace` 사용자는 `company_code = '*'` (최고 관리자)입니다. +- division: company * → **category** (text 아님) +- unit: company * → **text** (category 아님) + +**회사별로 다릅니다.** 예: COMPANY_7의 division은 text, unit은 category. + +--- + +## 수동 검증 절차 + +### 1. 로그인 +- URL: `http://localhost:9771/login` +- User ID: `wace` +- Password: `wace0909!!` +- 회사: "탑씰" (해당 회사 코드 확인 필요) + +### 2. 화면 149 접속 +- URL: `http://localhost:9771/screens/149` +- 페이지 로드 대기 + +### 3. 필드 확인 + +#### 구분 (division) +- **예상 (DB 기준):** + - company *: SELECT (category) + - COMPANY_7: TEXT INPUT (text) +- **실제:** TEXT INPUT 또는 SELECT 중 어느 쪽인지 확인 + +#### 단위 (unit) +- **예상 (DB 기준):** + - company *: TEXT INPUT (text) + - COMPANY_18~21, 7~9: SELECT (category) +- **실제:** TEXT INPUT 또는 SELECT 중 어느 쪽인지 확인 + +### 4. 스크린샷 +- 구분, 단위 필드가 함께 보이도록 캡처 + +--- + +## 코드 흐름 (input_type → 렌더링) + +### 1. 컬럼 메타 로드 +``` +DynamicComponentRenderer + → loadColumnMeta(screenTableName) + → GET /api/table-management/tables/item_info/columns?size=1000 + → columnMetaCache[tableName][columnName] = { inputType, ... } +``` + +### 2. 렌더 타입 결정 (357~369행) +```javascript +const dbInputType = columnMetaCache[screenTableName]?.[baseCol]?.inputType; +const ft = dbInputType || componentConfig?.fieldType; + +if (["text", "number", ...].includes(ft)) return "v2-input"; // 텍스트 입력 +if (["select", "category", "entity"].includes(ft)) return "v2-select"; // 드롭다운 +``` + +### 3. mergeColumnMeta (81~130행) +- DB `input_type`이 화면 저장값보다 우선 +- `needsSync`이면 DB 값으로 덮어씀 + +--- + +## 캐시 관련 + +### 1. 프론트엔드 (DynamicComponentRenderer) +- `columnMetaCache`: TTL 5초 +- `table-columns-refresh` 이벤트 시 즉시 무효화 및 재로드 + +### 2. 백엔드 (tableManagementService) +- 컬럼 목록: 5분 TTL +- `updateColumnInputType` 호출 시 해당 테이블 캐시 삭제 + +### 3. 캐시 무효화가 필요한 경우 +- 데이터 타입 관리에서 변경 후 화면이 갱신되지 않을 때 +- **대응:** 페이지 새로고침 또는 `?_t=timestamp`로 API 재요청 + +--- + +## 가능한 원인 + +### 1. 회사 코드 불일치 +- 로그인한 사용자 회사와 DB의 `company_code`가 다를 수 있음 +- `wace`는 `company_code = '*'` → division은 category, unit은 text + +### 2. 화면 레이아웃에 저장된 값 +- `componentConfig.fieldType`이 있으면 DB보다 우선될 수 있음 +- 코드상으로는 `dbInputType`이 우선이므로, DB가 제대로 로드되면 덮어씀 + +### 3. 캐시 +- 백엔드 5분, 프론트 5초 +- 데이터 타입 변경 후 곧바로 화면을 열면 이전 캐시가 사용될 수 있음 + +### 4. API 응답 구조 +- `columnMetaCache`에 넣을 때 `col.column_name || col.columnName` 사용 +- `mergeColumnMeta`는 `meta.input_type || meta.inputType` 사용 +- 백엔드는 `inputType`(camelCase) 반환 → `columnMetaCache`에 `inputType` 유지 + +--- + +## 디버깅용 Console 스크립트 + +화면 149 로드 후 브라우저 Console에서 실행: + +```javascript +// 1. columnMetaCache 조회 (DynamicComponentRenderer 내부) +// React DevTools로 DynamicComponentRenderer 선택 후 +// 또는 전역에 노출해 둔 경우: +const meta = window.__COLUMN_META_CACHE__?.item_info; +if (meta) { + console.log("division:", meta.division?.inputType || meta.division?.input_type); + console.log("unit:", meta.unit?.inputType || meta.unit?.input_type); +} + +// 2. API 직접 호출 +fetch("/api/table-management/tables/item_info/columns?size=1000", { + credentials: "include" +}) + .then(r => r.json()) + .then(d => { + const cols = d.data?.columns || d.columns || []; + const div = cols.find(c => (c.columnName || c.column_name) === "division"); + const unit = cols.find(c => (c.columnName || c.column_name) === "unit"); + console.log("API division:", div?.inputType || div?.input_type); + console.log("API unit:", unit?.inputType || unit?.input_type); + }); +``` + +--- + +## 권장 사항 + +1. **회사 코드 확인** + - 로그인한 사용자의 `company_code` 확인 + - `division`/`unit`을 text/category로 바꾼 회사가 맞는지 확인 + +2. **캐시 우회** + - 데이터 타입 변경 후 페이지 새로고침 + - 또는 5초 이상 대기 후 다시 접속 + +3. **데이터 타입 관리에서 변경 시** + - 저장 후 `table-columns-refresh` 이벤트 발생 여부 확인 + - 화면 디자이너의 V2FieldConfigPanel에서 변경 시에는 이벤트가 발생함 + +4. **테이블 관리 UI에서 변경 시** + - `table-columns-refresh` 이벤트가 발생하는지 확인 + - 없으면 해당 화면에서 수동으로 `window.dispatchEvent(new CustomEvent("table-columns-refresh"))` 호출 후 재검증 From 92cd07074966753b497a2881a3bca6a2f464aacb Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 15:22:16 +0900 Subject: [PATCH 03/39] [agent-pipeline] pipe-20260315061036-2tnn round-2 --- .../components/screen/ScreenRelationFlow.tsx | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/frontend/components/screen/ScreenRelationFlow.tsx b/frontend/components/screen/ScreenRelationFlow.tsx index cad4fc1f..a95e05d0 100644 --- a/frontend/components/screen/ScreenRelationFlow.tsx +++ b/frontend/components/screen/ScreenRelationFlow.tsx @@ -35,13 +35,13 @@ import { getTableColumns, ColumnTypeInfo } from "@/lib/api/tableManagement"; import { ScreenSettingModal } from "./ScreenSettingModal"; import { TableSettingModal } from "./TableSettingModal"; -// 관계 유형별 색상 정의 +// 관계 유형별 색상 정의 (CSS 변수 기반 - 다크모드 자동 대응) const RELATION_COLORS: Record = { - filter: { stroke: '#8b5cf6', strokeLight: '#c4b5fd', label: '마스터-디테일' }, // 보라색 - hierarchy: { stroke: '#06b6d4', strokeLight: '#a5f3fc', label: '계층 구조' }, // 시안색 - lookup: { stroke: '#f59e0b', strokeLight: '#fcd34d', label: '코드 참조' }, // 주황색 (기존) - mapping: { stroke: '#10b981', strokeLight: '#6ee7b7', label: '데이터 매핑' }, // 녹색 - join: { stroke: '#f97316', strokeLight: '#fdba74', label: '엔티티 조인' }, // orange-500 (기존 주황색) + filter: { stroke: 'hsl(var(--primary))', strokeLight: 'hsl(var(--primary) / 0.4)', label: '마스터-디테일' }, + hierarchy: { stroke: 'hsl(var(--info))', strokeLight: 'hsl(var(--info) / 0.4)', label: '계층 구조' }, + lookup: { stroke: 'hsl(var(--warning))', strokeLight: 'hsl(var(--warning) / 0.4)', label: '코드 참조' }, + mapping: { stroke: 'hsl(var(--success))', strokeLight: 'hsl(var(--success) / 0.4)', label: '데이터 매핑' }, + join: { stroke: 'hsl(var(--warning))', strokeLight: 'hsl(var(--warning) / 0.4)', label: '엔티티 조인' }, }; // 노드 타입 등록 @@ -689,12 +689,12 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId targetHandle: "left", type: "smoothstep", label: `${i + 1}`, - labelStyle: { fontSize: 11, fill: "#0ea5e9", fontWeight: 600 }, + labelStyle: { fontSize: 11, fill: "hsl(var(--info))", fontWeight: 600 }, labelBgStyle: { fill: "hsl(var(--card))", stroke: "hsl(var(--border))", strokeWidth: 1 }, labelBgPadding: [4, 2] as [number, number], - markerEnd: { type: MarkerType.ArrowClosed, color: "#0ea5e9" }, + markerEnd: { type: MarkerType.ArrowClosed, color: "hsl(var(--info))" }, animated: true, - style: { stroke: "#0ea5e9", strokeWidth: 2 }, + style: { stroke: "hsl(var(--info))", strokeWidth: 2 }, }); } } @@ -712,7 +712,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId type: "smoothstep", animated: true, // 모든 메인 테이블 연결은 애니메이션 style: { - stroke: "#3b82f6", + stroke: "hsl(var(--primary))", strokeWidth: 2, }, }); @@ -751,7 +751,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId type: "smoothstep", animated: true, style: { - stroke: "#3b82f6", + stroke: "hsl(var(--primary))", strokeWidth: 2, strokeDasharray: "5,5", // 점선으로 필터 관계 표시 }, @@ -1006,10 +1006,10 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId targetHandle: "top", type: "smoothstep", label: rel.relation_type === "join" ? "조인" : rel.crud_operations || "", - labelStyle: { fontSize: 9, fill: "#10b981" }, + labelStyle: { fontSize: 9, fill: "hsl(var(--success))" }, labelBgStyle: { fill: "hsl(var(--card))", stroke: "hsl(var(--border))", strokeWidth: 1 }, labelBgPadding: [3, 2] as [number, number], - style: { stroke: "#10b981", strokeWidth: 1.5 }, + style: { stroke: "hsl(var(--success))", strokeWidth: 1.5 }, }); } } @@ -1029,11 +1029,11 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId type: "smoothstep", animated: true, label: flow.flow_label || flow.flow_type || "이동", - labelStyle: { fontSize: 10, fill: "#8b5cf6", fontWeight: 500 }, + labelStyle: { fontSize: 10, fill: "hsl(var(--primary))", fontWeight: 500 }, labelBgStyle: { fill: "hsl(var(--card))", stroke: "hsl(var(--border))", strokeWidth: 1 }, labelBgPadding: [4, 2] as [number, number], - markerEnd: { type: MarkerType.ArrowClosed, color: "#8b5cf6" }, - style: { stroke: "#8b5cf6", strokeWidth: 2 }, + markerEnd: { type: MarkerType.ArrowClosed, color: "hsl(var(--primary))" }, + style: { stroke: "hsl(var(--primary))", strokeWidth: 2 }, }); } }); @@ -1903,7 +1903,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId animated: isConnected, style: { ...edge.style, - stroke: isConnected ? "#8b5cf6" : "#d1d5db", + stroke: isConnected ? "hsl(var(--primary))" : "hsl(var(--border))", strokeWidth: isConnected ? 2 : 1, opacity: isConnected ? 1 : 0.3, }, @@ -1920,7 +1920,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId animated: isMyConnection, style: { ...edge.style, - stroke: isMyConnection ? "#3b82f6" : "#d1d5db", + stroke: isMyConnection ? "hsl(var(--primary))" : "hsl(var(--border))", strokeWidth: isMyConnection ? 2 : 1, strokeDasharray: isMyConnection ? undefined : "5,5", opacity: isMyConnection ? 1 : 0.3, @@ -2040,7 +2040,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId animated: isConnected, style: { ...edge.style, - stroke: isConnected ? "#8b5cf6" : "#d1d5db", + stroke: isConnected ? "hsl(var(--primary))" : "hsl(var(--border))", strokeWidth: isConnected ? 2 : 1, opacity: isConnected ? 1 : 0.3, }, @@ -2076,7 +2076,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId animated: true, style: { ...edge.style, - stroke: "#3b82f6", + stroke: "hsl(var(--primary))", strokeWidth: 2, strokeDasharray: "5,5", opacity: 1, @@ -2095,7 +2095,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId animated: isMyConnection, style: { ...edge.style, - stroke: isMyConnection ? "#3b82f6" : "#d1d5db", + stroke: isMyConnection ? "hsl(var(--primary))" : "hsl(var(--border))", strokeWidth: isMyConnection ? 2 : 1, strokeDasharray: isMyConnection ? undefined : "5,5", opacity: isMyConnection ? 1 : 0.3, From 501325e4b49574b66d40e23197b3f176c19e4d78 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 15:27:14 +0900 Subject: [PATCH 04/39] [agent-pipeline] pipe-20260315061036-2tnn round-3 --- .../components/screen/ScreenRelationFlow.tsx | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/frontend/components/screen/ScreenRelationFlow.tsx b/frontend/components/screen/ScreenRelationFlow.tsx index a95e05d0..a2ad00c6 100644 --- a/frontend/components/screen/ScreenRelationFlow.tsx +++ b/frontend/components/screen/ScreenRelationFlow.tsx @@ -414,7 +414,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId isFaded = focusedScreenId !== null && !isFocused; } else { // 개별 화면 모드: 메인 화면(선택된 화면)만 포커스, 연결 화면은 흐리게 - isFocused = isMain; + isFocused = !!isMain; isFaded = !isMain && screenList.length > 1; } @@ -426,7 +426,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId label: scr.screenName, subLabel: selectedGroup ? `${roleLabel} (#${scr.displayOrder || idx + 1})` : (isMain ? "메인 화면" : "연결 화면"), type: "screen", - isMain: selectedGroup ? idx === 0 : isMain, + isMain: selectedGroup ? idx === 0 : !!isMain, tableName: scr.tableName, layoutSummary: summary, // 화면 포커스 관련 속성 (그룹 모드 & 개별 모드 공통) @@ -990,17 +990,18 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId } }); - // 테이블 관계 엣지 (추가 관계) + // 테이블 관계 엣지 (추가 관계) - 참조용 화면(개별 모드: screen, 그룹 모드: screenList[0]) + const refScreen = screen ?? screenList[0]; relations.forEach((rel: any, idx: number) => { - if (rel.table_name && rel.table_name !== screen.tableName) { + if (rel.table_name && rel.table_name !== refScreen.tableName) { // 화면 → 연결 테이블 const edgeExists = newEdges.some( - (e) => e.source === `screen-${screen.screenId}` && e.target === `table-${rel.table_name}` + (e) => e.source === `screen-${refScreen.screenId}` && e.target === `table-${rel.table_name}` ); if (!edgeExists) { newEdges.push({ id: `edge-rel-${idx}`, - source: `screen-${screen.screenId}`, + source: `screen-${refScreen.screenId}`, target: `table-${rel.table_name}`, sourceHandle: "bottom", targetHandle: "top", @@ -1017,12 +1018,12 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId // 데이터 흐름 엣지 (화면 간) flows - .filter((flow: any) => flow.source_screen_id === screen.screenId) + .filter((flow: any) => flow.source_screen_id === refScreen.screenId) .forEach((flow: any, idx: number) => { if (flow.target_screen_id) { newEdges.push({ id: `edge-flow-${idx}`, - source: `screen-${screen.screenId}`, + source: `screen-${refScreen.screenId}`, target: `screen-${flow.target_screen_id}`, sourceHandle: "right", targetHandle: "left", @@ -1134,7 +1135,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId // 화면 노드 우클릭 if (node.id.startsWith("screen-")) { const screenId = parseInt(node.id.replace("screen-", "")); - const nodeData = node.data as ScreenNodeData; + const nodeData = node.data as unknown as ScreenNodeData; const mainTable = screenTableMap[screenId]; // 해당 화면의 서브 테이블 (필터 테이블) 정보 @@ -1248,7 +1249,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId // 메인 테이블 노드 더블클릭 if (node.id.startsWith("table-") && !node.id.startsWith("table-sub-")) { const tableName = node.id.replace("table-", ""); - const nodeData = node.data as TableNodeData; + const nodeData = node.data as unknown as TableNodeData; // 이 테이블을 사용하는 화면 찾기 const screenId = Object.entries(screenTableMap).find( @@ -1293,7 +1294,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId // 서브 테이블 노드 더블클릭 if (node.id.startsWith("subtable-")) { const tableName = node.id.replace("subtable-", ""); - const nodeData = node.data as TableNodeData; + const nodeData = node.data as unknown as TableNodeData; // 이 서브 테이블을 사용하는 화면 찾기 const screenId = Object.entries(screenSubTableMap).find( @@ -2353,7 +2354,6 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId fieldMappings={settingModalNode.existingConfig?.fieldMappings} componentCount={0} onSaveSuccess={handleRefreshVisualization} - isPop={isPop} /> )} @@ -2367,7 +2367,13 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId screenId={settingModalNode.screenId} joinColumnRefs={settingModalNode.existingConfig?.joinColumnRefs} referencedBy={settingModalNode.existingConfig?.referencedBy} - columns={settingModalNode.existingConfig?.columns} + columns={settingModalNode.existingConfig?.columns?.map((col) => ({ + column: col.originalName ?? col.name, + label: col.name, + type: col.type, + isPK: col.isPrimaryKey, + isFK: col.isForeignKey, + }))} filterColumns={settingModalNode.existingConfig?.filterColumns} onSaveSuccess={handleRefreshVisualization} /> From 245580117e96713699f460bc346dd925a9df6069 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 15:30:47 +0900 Subject: [PATCH 05/39] [agent-pipeline] pipe-20260315061036-2tnn round-4 --- .../components/screen/ScreenGroupTreeView.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/components/screen/ScreenGroupTreeView.tsx b/frontend/components/screen/ScreenGroupTreeView.tsx index ead6ddd3..a4400db6 100644 --- a/frontend/components/screen/ScreenGroupTreeView.tsx +++ b/frontend/components/screen/ScreenGroupTreeView.tsx @@ -1119,9 +1119,9 @@ export function ScreenGroupTreeView({ )} {isExpanded ? ( - + ) : ( - + )} {group.group_name} @@ -1247,9 +1247,9 @@ export function ScreenGroupTreeView({ )} {isGrandExpanded ? ( - + ) : ( - + )} {grandChild.group_name} @@ -2080,7 +2080,7 @@ export function ScreenGroupTreeView({ onClick={() => handleSync("screen-to-menu")} disabled={isSyncing} variant="outline" - className="w-full justify-start gap-2 border-primary/20 bg-primary/10/50 hover:bg-primary/10/70 hover:border-primary/40" + className="w-full justify-start gap-2 border-primary/20 bg-primary/5 hover:bg-primary/10 hover:border-primary/40" > {isSyncing && syncDirection === "screen-to-menu" ? ( @@ -2096,15 +2096,15 @@ export function ScreenGroupTreeView({ onClick={() => handleSync("menu-to-screen")} disabled={isSyncing} variant="outline" - className="w-full justify-start gap-2 border-emerald-200 bg-emerald-50/50 hover:bg-emerald-100/70 hover:border-emerald-300" + className="w-full justify-start gap-2 border-success/20 bg-success/5 hover:bg-success/10 hover:border-success/30" > {isSyncing && syncDirection === "menu-to-screen" ? ( - + ) : ( - + )} - 메뉴 → 화면관리 동기화 - + 메뉴 → 화면관리 동기화 + 메뉴 구조를 폴더에 반영 From 542663e9e66e1eea048bf915d2063adc45cc3b1a Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 15:30:47 +0900 Subject: [PATCH 06/39] [agent-pipeline] rollback to 501325e4 --- .../components/screen/ScreenGroupTreeView.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/components/screen/ScreenGroupTreeView.tsx b/frontend/components/screen/ScreenGroupTreeView.tsx index a4400db6..ead6ddd3 100644 --- a/frontend/components/screen/ScreenGroupTreeView.tsx +++ b/frontend/components/screen/ScreenGroupTreeView.tsx @@ -1119,9 +1119,9 @@ export function ScreenGroupTreeView({ )} {isExpanded ? ( - + ) : ( - + )} {group.group_name} @@ -1247,9 +1247,9 @@ export function ScreenGroupTreeView({ )} {isGrandExpanded ? ( - + ) : ( - + )} {grandChild.group_name} @@ -2080,7 +2080,7 @@ export function ScreenGroupTreeView({ onClick={() => handleSync("screen-to-menu")} disabled={isSyncing} variant="outline" - className="w-full justify-start gap-2 border-primary/20 bg-primary/5 hover:bg-primary/10 hover:border-primary/40" + className="w-full justify-start gap-2 border-primary/20 bg-primary/10/50 hover:bg-primary/10/70 hover:border-primary/40" > {isSyncing && syncDirection === "screen-to-menu" ? ( @@ -2096,15 +2096,15 @@ export function ScreenGroupTreeView({ onClick={() => handleSync("menu-to-screen")} disabled={isSyncing} variant="outline" - className="w-full justify-start gap-2 border-success/20 bg-success/5 hover:bg-success/10 hover:border-success/30" + className="w-full justify-start gap-2 border-emerald-200 bg-emerald-50/50 hover:bg-emerald-100/70 hover:border-emerald-300" > {isSyncing && syncDirection === "menu-to-screen" ? ( - + ) : ( - + )} - 메뉴 → 화면관리 동기화 - + 메뉴 → 화면관리 동기화 + 메뉴 구조를 폴더에 반영 From f375252db1e88ee81bd601de6be59517868837f8 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 15:33:19 +0900 Subject: [PATCH 07/39] [agent-pipeline] pipe-20260315061036-2tnn round-5 --- frontend/components/screen/ScreenGroupTreeView.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/components/screen/ScreenGroupTreeView.tsx b/frontend/components/screen/ScreenGroupTreeView.tsx index ead6ddd3..7172d100 100644 --- a/frontend/components/screen/ScreenGroupTreeView.tsx +++ b/frontend/components/screen/ScreenGroupTreeView.tsx @@ -37,7 +37,8 @@ import { import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { useAuth } from "@/hooks/useAuth"; -import { getCompanyList, Company } from "@/lib/api/company"; +import { getCompanyList } from "@/lib/api/company"; +import type { Company } from "@/types/company"; import { DropdownMenu, DropdownMenuContent, From e963129e637407ee7c3ee0077a5d27652e9f421d Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 15:36:53 +0900 Subject: [PATCH 08/39] [agent-pipeline] pipe-20260315061036-2tnn round-6 --- frontend/app/(main)/admin/screenMng/screenMngList/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 76d1b91f..43c8b758 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -162,7 +162,7 @@ export default function ScreenManagementPage() {
-

화면 관리

+

화면 관리

화면을 그룹별로 관리하고 데이터 관계를 확인합니다

From ea6aa6921cabbc08fa7f973c73197f6d7167cbd0 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 15:36:53 +0900 Subject: [PATCH 09/39] [agent-pipeline] rollback to f375252d --- frontend/app/(main)/admin/screenMng/screenMngList/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 43c8b758..76d1b91f 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -162,7 +162,7 @@ export default function ScreenManagementPage() {
-

화면 관리

+

화면 관리

화면을 그룹별로 관리하고 데이터 관계를 확인합니다

From 015cd2c3eda42266536795fb0420e1593656872f Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 15:54:04 +0900 Subject: [PATCH 10/39] [agent-pipeline] pipe-20260315065015-rei8 round-1 --- frontend/components/screen/ScreenNode.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/components/screen/ScreenNode.tsx b/frontend/components/screen/ScreenNode.tsx index 70930e21..105eced0 100644 --- a/frontend/components/screen/ScreenNode.tsx +++ b/frontend/components/screen/ScreenNode.tsx @@ -214,10 +214,10 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => { /> {/* 헤더 (컬러) */} -
+
{label} - {(isMain || isFocused) && } + {(isMain || isFocused) && }
{/* 화면 미리보기 영역 (컴팩트) */} @@ -352,7 +352,7 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
{/* 컴포넌트 수 */} -
+
{totalComponents}개
@@ -376,7 +376,7 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
{/* 컴포넌트 수 */} -
+
{totalComponents}개
@@ -409,7 +409,7 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
{/* 컴포넌트 수 */} -
+
{totalComponents}개
@@ -429,7 +429,7 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
액션 화면
{/* 컴포넌트 수 */} -
+
{totalComponents}개
@@ -654,7 +654,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { /> {/* 헤더 (필터 관계: primary, 필터 소스: primary, 메인: primary, 기본: muted-foreground) */} -
@@ -670,7 +670,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
{hasActiveColumns && ( - + {displayColumns.length}개 활성 )} @@ -699,7 +699,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { {/* 필터 뱃지 */} {filterRefs.length > 0 && ( `${r.fromTable}.${r.fromColumn || 'id'} → ${r.toColumn}`).join('\n')}`} > @@ -714,7 +714,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { {/* 참조 뱃지 */} {lookupRefs.length > 0 && ( `${r.fromTable} → ${r.toColumn}`).join('\n')}`} > {lookupRefs.length}곳 참조 From c4db3fbfd4c13cce37638201c2255b4fb48b9a28 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 15:58:02 +0900 Subject: [PATCH 11/39] [agent-pipeline] pipe-20260315065015-rei8 round-2 --- frontend/components/screen/ScreenRelationFlow.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/frontend/components/screen/ScreenRelationFlow.tsx b/frontend/components/screen/ScreenRelationFlow.tsx index a2ad00c6..6296c326 100644 --- a/frontend/components/screen/ScreenRelationFlow.tsx +++ b/frontend/components/screen/ScreenRelationFlow.tsx @@ -1799,12 +1799,6 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId } }); } - - // 디버깅 로그 - console.log(`서브테이블 ${subTableName} (${subTableInfo?.relationType}):`, { - fieldMappings: subTableInfo?.fieldMappings, - extractedJoinColumns: subTableJoinColumns - }); } // 서브 테이블의 highlightedColumns도 추가 (화면에서 서브테이블 컬럼을 직접 사용하는 경우) From b1afe1bc8da8d7793f6395e9f322132892301247 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 16:01:43 +0900 Subject: [PATCH 12/39] [agent-pipeline] pipe-20260315065015-rei8 round-3 --- .../components/screen/ScreenGroupTreeView.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/components/screen/ScreenGroupTreeView.tsx b/frontend/components/screen/ScreenGroupTreeView.tsx index 7172d100..6b120501 100644 --- a/frontend/components/screen/ScreenGroupTreeView.tsx +++ b/frontend/components/screen/ScreenGroupTreeView.tsx @@ -1120,9 +1120,9 @@ export function ScreenGroupTreeView({ )} {isExpanded ? ( - + ) : ( - + )} {group.group_name} @@ -1248,9 +1248,9 @@ export function ScreenGroupTreeView({ )} {isGrandExpanded ? ( - + ) : ( - + )} {grandChild.group_name} @@ -2097,15 +2097,15 @@ export function ScreenGroupTreeView({ onClick={() => handleSync("menu-to-screen")} disabled={isSyncing} variant="outline" - className="w-full justify-start gap-2 border-emerald-200 bg-emerald-50/50 hover:bg-emerald-100/70 hover:border-emerald-300" + className="w-full justify-start gap-2 border-success/20 bg-success/5 hover:bg-success/10 hover:border-success/30" > {isSyncing && syncDirection === "menu-to-screen" ? ( - + ) : ( - + )} - 메뉴 → 화면관리 동기화 - + 메뉴 → 화면관리 동기화 + 메뉴 구조를 폴더에 반영 From 0db57fe01af5866f1e921409beb1257785115eed Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 16:05:06 +0900 Subject: [PATCH 13/39] [agent-pipeline] pipe-20260315065015-rei8 round-4 --- frontend/app/(main)/admin/screenMng/screenMngList/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 76d1b91f..43c8b758 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -162,7 +162,7 @@ export default function ScreenManagementPage() {
-

화면 관리

+

화면 관리

화면을 그룹별로 관리하고 데이터 관계를 확인합니다

From bad3a002f334f4001a925f994689e502c597f41c Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 16:05:07 +0900 Subject: [PATCH 14/39] [agent-pipeline] rollback to b1afe1bc --- frontend/app/(main)/admin/screenMng/screenMngList/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 43c8b758..76d1b91f 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -162,7 +162,7 @@ export default function ScreenManagementPage() {
-

화면 관리

+

화면 관리

화면을 그룹별로 관리하고 데이터 관계를 확인합니다

From 265d79cc5ab58ee5d1637d70ee10f4842a1f7c21 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 16:07:43 +0900 Subject: [PATCH 15/39] [agent-pipeline] pipe-20260315065015-rei8 round-5 --- frontend/app/(main)/admin/screenMng/screenMngList/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 76d1b91f..c59b7e4c 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -269,9 +269,9 @@ export default function ScreenManagementPage() { {/* 화면 생성 모달 */} setIsCreateOpen(false)} - onSuccess={() => { + open={isCreateOpen} + onOpenChange={setIsCreateOpen} + onCreated={() => { setIsCreateOpen(false); loadScreens(); }} From 21ca0f3a3ce14c43fb11e7878b85488d0296858a Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 16:27:14 +0900 Subject: [PATCH 16/39] [agent-pipeline] pipe-20260315072335-zb1m round-1 --- frontend/components/screen/ScreenNode.tsx | 69 ++++++++++++----------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/frontend/components/screen/ScreenNode.tsx b/frontend/components/screen/ScreenNode.tsx index 105eced0..792dba8e 100644 --- a/frontend/components/screen/ScreenNode.tsx +++ b/frontend/components/screen/ScreenNode.tsx @@ -107,42 +107,42 @@ const getScreenTypeIcon = (screenType?: string) => { } }; -// 화면 타입별 색상 (헤더) +// 화면 타입별 색상 (헤더) - 그라데이션 const getScreenTypeColor = (screenType?: string, isMain?: boolean) => { - if (!isMain) return "bg-muted-foreground"; + if (!isMain) return "bg-gradient-to-r from-muted-foreground to-muted-foreground/80"; switch (screenType) { case "grid": - return "bg-primary"; + return "bg-gradient-to-r from-primary to-primary/80"; case "dashboard": - return "bg-warning"; + return "bg-gradient-to-r from-warning to-warning/80"; case "action": - return "bg-destructive"; + return "bg-gradient-to-r from-destructive to-destructive/80"; default: - return "bg-primary"; + return "bg-gradient-to-r from-primary to-primary/80"; } }; -// 화면 역할(screenRole)에 따른 색상 +// 화면 역할(screenRole)에 따른 색상 - 그라데이션 const getScreenRoleColor = (screenRole?: string) => { - if (!screenRole) return "bg-muted-foreground"; + if (!screenRole) return "bg-gradient-to-r from-muted-foreground to-muted-foreground/80"; // 역할명에 포함된 키워드로 색상 결정 const role = screenRole.toLowerCase(); if (role.includes("그리드") || role.includes("grid") || role.includes("메인") || role.includes("main") || role.includes("list")) { - return "bg-primary"; // 메인 그리드 + return "bg-gradient-to-r from-primary to-primary/80"; // 메인 그리드 } if (role.includes("등록") || role.includes("폼") || role.includes("form") || role.includes("register") || role.includes("input")) { - return "bg-primary"; // 등록 폼 + return "bg-gradient-to-r from-primary to-primary/80"; // 등록 폼 } if (role.includes("액션") || role.includes("action") || role.includes("이벤트") || role.includes("event") || role.includes("클릭")) { - return "bg-destructive"; // 액션/이벤트 + return "bg-gradient-to-r from-destructive to-destructive/80"; // 액션/이벤트 } if (role.includes("상세") || role.includes("detail") || role.includes("popup") || role.includes("팝업")) { - return "bg-warning"; // 상세/팝업 + return "bg-gradient-to-r from-warning to-warning/80"; // 상세/팝업 } - return "bg-muted-foreground"; // 기본 회색 + return "bg-gradient-to-r from-muted-foreground to-muted-foreground/80"; // 기본 회색 }; // 화면 타입별 라벨 @@ -169,7 +169,7 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => { let headerColor: string; if (isInGroup) { if (isFaded) { - headerColor = "bg-muted/60"; // 흑백 처리 - 더 확실한 회색 + headerColor = "bg-gradient-to-r from-muted to-muted/60"; // 흑백 처리 - 더 확실한 회색 } else { // 포커스되었거나 아직 아무것도 선택 안됐을 때: 역할별 색상 headerColor = getScreenRoleColor(screenRole); @@ -180,17 +180,16 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => { return (
{/* Handles */} @@ -214,14 +213,14 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => { /> {/* 헤더 (컬러) */} -
+
- {label} + {label} {(isMain || isFocused) && }
{/* 화면 미리보기 영역 (컴팩트) */} -
+
{layoutSummary ? ( ) : ( @@ -262,7 +261,7 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
{/* 푸터 (테이블 정보) */} -
+
{tableName || "No Table"} @@ -574,7 +573,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { return (
= ({ data }) => { ? "border-2 border-primary ring-4 ring-primary/30 shadow-xl bg-card" // 4. 흐리게 처리 : isFaded - ? "border-border opacity-60 bg-card" + ? "opacity-60 bg-card" // 5. 기본 - : "border-border hover:shadow-lg hover:ring-2 hover:ring-primary/20 bg-card" + : "hover:shadow-xl hover:ring-2 hover:ring-primary/20" }`} style={{ filter: isFaded ? "grayscale(80%)" : "none", @@ -653,9 +652,15 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { className="!h-2 !w-2 !border-2 !border-background !bg-warning opacity-0 transition-opacity group-hover:opacity-100" /> - {/* 헤더 (필터 관계: primary, 필터 소스: primary, 메인: primary, 기본: muted-foreground) */} + {/* 헤더 (필터 관계: primary, 필터 소스: primary, 메인: primary, 기본: muted-foreground) - 그라데이션 */}
@@ -670,7 +675,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
{hasActiveColumns && ( - + {displayColumns.length}개 활성 )} @@ -745,14 +750,14 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { key={col.name} className={`flex items-center gap-1 rounded px-1.5 py-0.5 transition-all duration-300 ${ isJoinColumn - ? "bg-warning/10 border border-warning/30 shadow-sm" + ? "bg-warning/10 border border-warning/20 shadow-sm" : isFilterColumn || isFilterSourceColumn - ? "bg-primary/10 border border-primary/30 shadow-sm" // 필터 컬럼/필터 소스 + ? "bg-primary/10 border border-primary/20 shadow-sm" // 필터 컬럼/필터 소스 : isHighlighted ? "bg-primary/10 border border-primary/40 shadow-sm" : hasActiveColumns ? "bg-muted" - : "bg-muted/50 hover:bg-muted" + : "bg-muted/50 hover:bg-muted/80 transition-colors" }`} style={{ animation: hasActiveColumns ? `fadeIn 0.5s ease-out ${idx * 80}ms forwards` : undefined, From bafc81b2a38f51885a301c9db4c73f22a65f445c Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 16:30:14 +0900 Subject: [PATCH 17/39] [agent-pipeline] pipe-20260315072335-zb1m round-2 --- .../admin/screenMng/screenMngList/page.tsx | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index c59b7e4c..63949a44 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -159,11 +159,11 @@ export default function ScreenManagementPage() { return (
{/* 페이지 헤더 */} -
+
-

화면 관리

-

화면을 그룹별로 관리하고 데이터 관계를 확인합니다

+

화면 관리

+

화면을 그룹별로 관리하고 데이터 관계를 확인합니다

{/* V2 컴포넌트 테스트 버튼 */} @@ -177,12 +177,12 @@ export default function ScreenManagementPage() { {/* 뷰 모드 전환 */} setViewMode(v as ViewMode)}> - - + + 트리 - + 테이블 @@ -191,28 +191,29 @@ export default function ScreenManagementPage() { -
+
{/* 메인 콘텐츠 */} {viewMode === "tree" ? (
{/* 왼쪽: 트리 구조 */} -
+
{/* 검색 */} -
+
setSearchTerm(e.target.value)} - className="pl-9 h-9" + className="pl-9 h-9 bg-muted/30 border-border/50 focus:bg-background transition-colors" />
@@ -248,7 +249,7 @@ export default function ScreenManagementPage() {
{/* 오른쪽: 관계 시각화 (React Flow) */} -
+
Date: Sun, 15 Mar 2026 16:32:53 +0900 Subject: [PATCH 18/39] [agent-pipeline] pipe-20260315072335-zb1m round-3 --- frontend/app/(main)/admin/screenMng/screenMngList/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 63949a44..e367e242 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -204,7 +204,7 @@ export default function ScreenManagementPage() { {viewMode === "tree" ? (
{/* 왼쪽: 트리 구조 */} -
+
{/* 검색 */}
@@ -213,7 +213,7 @@ export default function ScreenManagementPage() { placeholder="화면 검색..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} - className="pl-9 h-9 bg-muted/30 border-border/50 focus:bg-background transition-colors" + className="pl-9 h-9 rounded-xl bg-muted/30 border-border/50 focus:bg-background focus:ring-2 focus:ring-primary/30 transition-colors" />
From bf509171db80a81e2d892e7d7990e36e1511f27a Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 16:38:00 +0900 Subject: [PATCH 19/39] [agent-pipeline] pipe-20260315072335-zb1m round-4 --- .../components/screen/ScreenGroupTreeView.tsx | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/frontend/components/screen/ScreenGroupTreeView.tsx b/frontend/components/screen/ScreenGroupTreeView.tsx index 6b120501..65375419 100644 --- a/frontend/components/screen/ScreenGroupTreeView.tsx +++ b/frontend/components/screen/ScreenGroupTreeView.tsx @@ -1120,12 +1120,12 @@ export function ScreenGroupTreeView({ )} {isExpanded ? ( - + ) : ( - + )} {group.group_name} - + {groupScreens.length} {/* 그룹 메뉴 버튼 */} @@ -1186,12 +1186,12 @@ export function ScreenGroupTreeView({ )} {isChildExpanded ? ( - + ) : ( - + )} {childGroup.group_name} - + {childScreens.length} @@ -1248,12 +1248,12 @@ export function ScreenGroupTreeView({ )} {isGrandExpanded ? ( - + ) : ( - + )} {grandChild.group_name} - + {grandScreens.length} @@ -1295,9 +1295,9 @@ export function ScreenGroupTreeView({
handleScreenClickInGroup(screen, grandChild)} onDoubleClick={() => handleScreenDoubleClick(screen)} @@ -1331,9 +1331,9 @@ export function ScreenGroupTreeView({
handleScreenClickInGroup(screen, childGroup)} onDoubleClick={() => handleScreenDoubleClick(screen)} @@ -1367,9 +1367,9 @@ export function ScreenGroupTreeView({
handleScreenClickInGroup(screen, group)} onDoubleClick={() => handleScreenDoubleClick(screen)} @@ -1406,7 +1406,7 @@ export function ScreenGroupTreeView({ )} 미분류 - + {ungroupedScreens.length}
@@ -1417,9 +1417,9 @@ export function ScreenGroupTreeView({
handleScreenClick(screen)} onDoubleClick={() => handleScreenDoubleClick(screen)} From 24b53b5b33b36e1e92108c554b57b4d74602f937 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 16:42:20 +0900 Subject: [PATCH 20/39] [agent-pipeline] pipe-20260315072335-zb1m round-5 --- .../components/screen/ScreenRelationFlow.tsx | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/frontend/components/screen/ScreenRelationFlow.tsx b/frontend/components/screen/ScreenRelationFlow.tsx index 6296c326..2bdc069d 100644 --- a/frontend/components/screen/ScreenRelationFlow.tsx +++ b/frontend/components/screen/ScreenRelationFlow.tsx @@ -1899,8 +1899,8 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId style: { ...edge.style, stroke: isConnected ? "hsl(var(--primary))" : "hsl(var(--border))", - strokeWidth: isConnected ? 2 : 1, - opacity: isConnected ? 1 : 0.3, + strokeWidth: isConnected ? 2.5 : 1, + opacity: isConnected ? 1 : 0.2, }, }; } @@ -1916,9 +1916,9 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId style: { ...edge.style, stroke: isMyConnection ? "hsl(var(--primary))" : "hsl(var(--border))", - strokeWidth: isMyConnection ? 2 : 1, + strokeWidth: isMyConnection ? 2.5 : 1, strokeDasharray: isMyConnection ? undefined : "5,5", - opacity: isMyConnection ? 1 : 0.3, + opacity: isMyConnection ? 1 : 0.2, }, }; } @@ -1997,7 +1997,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId animated: true, style: { stroke: relationColor.stroke, // 관계 유형별 색상 - strokeWidth: 2, + strokeWidth: 2.5, strokeDasharray: '8,4', }, markerEnd: { @@ -2036,8 +2036,8 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId style: { ...edge.style, stroke: isConnected ? "hsl(var(--primary))" : "hsl(var(--border))", - strokeWidth: isConnected ? 2 : 1, - opacity: isConnected ? 1 : 0.3, + strokeWidth: isConnected ? 2.5 : 1, + opacity: isConnected ? 1 : 0.2, }, }; } @@ -2072,7 +2072,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId style: { ...edge.style, stroke: "hsl(var(--primary))", - strokeWidth: 2, + strokeWidth: 2.5, strokeDasharray: "5,5", opacity: 1, }, @@ -2091,9 +2091,9 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId style: { ...edge.style, stroke: isMyConnection ? "hsl(var(--primary))" : "hsl(var(--border))", - strokeWidth: isMyConnection ? 2 : 1, + strokeWidth: isMyConnection ? 2.5 : 1, strokeDasharray: isMyConnection ? undefined : "5,5", - opacity: isMyConnection ? 1 : 0.3, + opacity: isMyConnection ? 1 : 0.2, }, }; } @@ -2150,7 +2150,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId stroke: isActive ? relationColor.stroke : relationColor.strokeLight, strokeWidth: isActive ? 2.5 : 1.5, strokeDasharray: "8,4", - opacity: isActive ? 1 : 0.3, + opacity: isActive ? 1 : 0.2, }, markerEnd: { type: MarkerType.ArrowClosed, @@ -2174,7 +2174,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId stroke: RELATION_COLORS.join.strokeLight, strokeWidth: 1.5, strokeDasharray: "6,4", - opacity: 0.3, + opacity: 0.2, }, markerEnd: { type: MarkerType.ArrowClosed, @@ -2201,7 +2201,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId style: { ...edge.style, stroke: RELATION_COLORS.join.stroke, - strokeWidth: 2, + strokeWidth: 2.5, strokeDasharray: "6,4", opacity: 1, }, @@ -2317,6 +2317,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId {/* isViewReady가 false면 숨김 처리하여 깜빡임 방지 */}
Date: Sun, 15 Mar 2026 17:10:04 +0900 Subject: [PATCH 21/39] [agent-pipeline] pipe-20260315080636-1tpd round-1 --- .../admin/screenMng/screenMngList/page.tsx | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index e367e242..862ded32 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -1,10 +1,10 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useMemo } from "react"; import { useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { ArrowLeft, Plus, RefreshCw, Search, LayoutGrid, LayoutList, TestTube2 } from "lucide-react"; +import { ArrowLeft, Plus, RefreshCw, Search, LayoutGrid, LayoutList, TestTube2, ChevronRight, Monitor, Database, FolderOpen } from "lucide-react"; import ScreenList from "@/components/screen/ScreenList"; import ScreenDesigner from "@/components/screen/ScreenDesigner"; import TemplateManager from "@/components/screen/TemplateManager"; @@ -15,6 +15,7 @@ import { ScrollToTop } from "@/components/common/ScrollToTop"; import { ScreenDefinition } from "@/types/screen"; import { screenApi } from "@/lib/api/screen"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Badge } from "@/components/ui/badge"; import CreateScreenModal from "@/components/screen/CreateScreenModal"; // 단계별 진행을 위한 타입 정의 @@ -34,6 +35,8 @@ export default function ScreenManagementPage() { const [searchTerm, setSearchTerm] = useState(""); const [isCreateOpen, setIsCreateOpen] = useState(false); + const tableCount = useMemo(() => new Set(screens.map((s) => s.tableName).filter(Boolean)).size, [screens]); + // 화면 목록 로드 const loadScreens = useCallback(async () => { try { @@ -200,6 +203,33 @@ export default function ScreenManagementPage() {
+ {/* 통계 요약 바 */} +
+
+ + 화면 + {screens.length} +
+
+
+ + 테이블 + {tableCount} +
+ {(selectedGroup || selectedScreen) && ( +
+ 현재: + {selectedGroup && {selectedGroup.name}} + {selectedScreen && ( + <> + + {selectedScreen.screenName} + + )} +
+ )} +
+ {/* 메인 콘텐츠 */} {viewMode === "tree" ? (
@@ -246,6 +276,24 @@ export default function ScreenManagementPage() { }} />
+ {/* 선택 미리보기 */} + {selectedScreen && ( +
+
+ + {selectedScreen.screenName} +
+
+ {selectedScreen.screenCode} + {selectedScreen.tableName || "테이블 없음"} +
+
+ +
+
+ )}
{/* 오른쪽: 관계 시각화 (React Flow) */} From 94a95b7dc1d0fe64c814b87ba581b4798b330c0b Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 17:13:37 +0900 Subject: [PATCH 22/39] [agent-pipeline] pipe-20260315080636-1tpd round-2 --- .../components/screen/ScreenRelationFlow.tsx | 79 +++++++++++++++++-- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/frontend/components/screen/ScreenRelationFlow.tsx b/frontend/components/screen/ScreenRelationFlow.tsx index 2bdc069d..05a6ed04 100644 --- a/frontend/components/screen/ScreenRelationFlow.tsx +++ b/frontend/components/screen/ScreenRelationFlow.tsx @@ -34,6 +34,7 @@ import { import { getTableColumns, ColumnTypeInfo } from "@/lib/api/tableManagement"; import { ScreenSettingModal } from "./ScreenSettingModal"; import { TableSettingModal } from "./TableSettingModal"; +import { Monitor, Database, FolderOpen } from "lucide-react"; // 관계 유형별 색상 정의 (CSS 변수 기반 - 다크모드 자동 대응) const RELATION_COLORS: Record = { @@ -2295,10 +2296,38 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId // 조건부 렌더링 (모든 훅 선언 후에 위치해야 함) if (!screen && !selectedGroup) { return ( -
-
-

그룹 또는 화면을 선택하면

-

데이터 관계가 시각화됩니다

+
+
+
+
+ +
+
+
+ +
+
+
+
+

화면 관계 시각화

+

+ 좌측에서 그룹 또는 화면을 선택하면
+ 테이블 관계가 자동으로 시각화됩니다. +

+
+
+
+ 1 + 그룹 선택 +
+
+ 2 + 관계 확인 +
+
+ 3 + 화면 편집 +
); @@ -2313,7 +2342,25 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId } return ( -
+
+ {/* 선택 정보 바 (캔버스 상단) */} + {(screen || selectedGroup) && ( +
+ {selectedGroup && ( + <> + + {selectedGroup.name} + + )} + {screen && !selectedGroup && ( + <> + + {screen.screenName} + {screen.screenCode} + + )} +
+ )} {/* isViewReady가 false면 숨김 처리하여 깜빡임 방지 */}
+ {/* 관계 범례 */} +
+

관계 유형

+
+
+
+ 메인 테이블 +
+
+
+ 마스터-디테일 +
+
+
+ 코드 참조 +
+
+
+ 엔티티 조인 +
+
+
From 4c19d3a6eb92cdae9e0041335798511ae788a28e Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 17:18:18 +0900 Subject: [PATCH 23/39] [agent-pipeline] pipe-20260315080636-1tpd round-3 --- .../components/screen/ScreenGroupTreeView.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/frontend/components/screen/ScreenGroupTreeView.tsx b/frontend/components/screen/ScreenGroupTreeView.tsx index 65375419..46a96847 100644 --- a/frontend/components/screen/ScreenGroupTreeView.tsx +++ b/frontend/components/screen/ScreenGroupTreeView.tsx @@ -1107,7 +1107,7 @@ export function ScreenGroupTreeView({ {/* 그룹 헤더 */}
0 && ( -
+
+
{childGroups.map((childGroup) => { const childGroupId = String(childGroup.id); const isChildExpanded = expandedGroups.has(childGroupId) || shouldAutoExpandForSearch.has(childGroup.id); // 검색 시 상위 그룹만 자동 확장 @@ -1173,7 +1174,7 @@ export function ScreenGroupTreeView({ {/* 중분류 헤더 */}
0 && ( -
+
+
{grandChildGroups.map((grandChild) => { const grandChildId = String(grandChild.id); const isGrandExpanded = expandedGroups.has(grandChildId) || shouldAutoExpandForSearch.has(grandChild.id); // 검색 시 상위 그룹만 자동 확장 @@ -1235,7 +1237,7 @@ export function ScreenGroupTreeView({ {/* 소분류 헤더 */}
handleScreenClickInGroup(screen, grandChild)} onDoubleClick={() => handleScreenDoubleClick(screen)} @@ -1333,7 +1335,7 @@ export function ScreenGroupTreeView({ className={cn( "flex items-center gap-2 rounded-md px-2 py-1.5 cursor-pointer transition-colors duration-150", "text-xs hover:bg-muted/60", - selectedScreen?.screenId === screen.screenId && "bg-accent shadow-sm" + selectedScreen?.screenId === screen.screenId && "bg-primary/10 border-l-2 border-primary" )} onClick={() => handleScreenClickInGroup(screen, childGroup)} onDoubleClick={() => handleScreenDoubleClick(screen)} @@ -1369,7 +1371,7 @@ export function ScreenGroupTreeView({ className={cn( "flex items-center gap-2 rounded-md px-2 py-1.5 cursor-pointer transition-colors duration-150", "text-sm hover:bg-muted/60 group/screen", - selectedScreen?.screenId === screen.screenId && "bg-accent shadow-sm" + selectedScreen?.screenId === screen.screenId && "bg-primary/10 border-l-2 border-primary" )} onClick={() => handleScreenClickInGroup(screen, group)} onDoubleClick={() => handleScreenDoubleClick(screen)} @@ -1394,7 +1396,7 @@ export function ScreenGroupTreeView({
toggleGroup("ungrouped")} @@ -1419,7 +1421,7 @@ export function ScreenGroupTreeView({ className={cn( "flex items-center gap-2 rounded-md px-2 py-1.5 cursor-pointer transition-colors duration-150", "text-sm hover:bg-muted/60", - selectedScreen?.screenId === screen.screenId && "bg-accent shadow-sm" + selectedScreen?.screenId === screen.screenId && "bg-primary/10 border-l-2 border-primary" )} onClick={() => handleScreenClick(screen)} onDoubleClick={() => handleScreenDoubleClick(screen)} From 27ce039fc85d66ce1b6d8bb82ada4615f1b8e8ef Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 17:22:24 +0900 Subject: [PATCH 24/39] [agent-pipeline] pipe-20260315080636-1tpd round-4 --- frontend/components/screen/ScreenNode.tsx | 77 +++++------------------ 1 file changed, 15 insertions(+), 62 deletions(-) diff --git a/frontend/components/screen/ScreenNode.tsx b/frontend/components/screen/ScreenNode.tsx index 792dba8e..119b24e3 100644 --- a/frontend/components/screen/ScreenNode.tsx +++ b/frontend/components/screen/ScreenNode.tsx @@ -11,7 +11,6 @@ import { MousePointer2, Key, Link2, - Columns3, } from "lucide-react"; import { ScreenLayoutSummary } from "@/lib/api/screenGroup"; @@ -180,7 +179,7 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => { return (
= ({ data }) => { /> {/* 헤더 (컬러) */} -
- - {label} +
+
+ +
+
+
{label}
+ {tableName &&
{tableName}
} +
{(isMain || isFocused) && }
{/* 화면 미리보기 영역 (컴팩트) */} -
+
{layoutSummary ? ( ) : ( @@ -231,44 +235,10 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => { )}
- {/* 필드 매핑 영역 */} -
-
- - 필드 매핑 - - {layoutSummary?.layoutItems?.filter(i => i.label && !i.componentKind?.includes('button')).length || 0}개 - -
-
- {layoutSummary?.layoutItems - ?.filter(item => item.label && !item.componentKind?.includes('button')) - ?.slice(0, 6) - ?.map((item, idx) => ( -
-
- {item.label} - {item.componentKind?.split('-')[0] || 'field'} -
- )) || ( -
필드 정보 없음
- )} -
-
- - {/* 푸터 (테이블 정보) */} -
-
- - {tableName || "No Table"} -
- - {getScreenTypeLabel(screenType)} - + {/* 푸터 (타입 + 컴포넌트 수) */} +
+ {getScreenTypeLabel(screenType)} + {layoutSummary?.totalComponents ?? 0}개 컴포넌트
); @@ -350,10 +320,6 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
- {/* 컴포넌트 수 */} -
- {totalComponents}개 -
); } @@ -374,10 +340,6 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
- {/* 컴포넌트 수 */} -
- {totalComponents}개 -
); } @@ -406,10 +368,6 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType: /> ))}
-
- {/* 컴포넌트 수 */} -
- {totalComponents}개
); @@ -427,10 +385,6 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
액션 화면
- {/* 컴포넌트 수 */} -
- {totalComponents}개 -
); } @@ -836,8 +790,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
{/* 푸터 (컴팩트) */} -
- PostgreSQL +
{columns && ( {hasActiveColumns ? `${displayColumns.length}/${totalCount}` : totalCount}개 컬럼 From 784dc73abf0edcff95db830938dc69d9467c424d Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 18:22:20 +0900 Subject: [PATCH 25/39] [agent-pipeline] pipe-20260315091327-kxyf round-1 --- .../admin/screenMng/screenMngList/page.tsx | 82 ++++++++----------- .../components/screen/AnimatedFlowEdge.tsx | 70 ++++++++++++++++ 2 files changed, 102 insertions(+), 50 deletions(-) create mode 100644 frontend/components/screen/AnimatedFlowEdge.tsx diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 862ded32..3631aa19 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -4,7 +4,7 @@ import { useState, useEffect, useCallback, useMemo } from "react"; import { useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { ArrowLeft, Plus, RefreshCw, Search, LayoutGrid, LayoutList, TestTube2, ChevronRight, Monitor, Database, FolderOpen } from "lucide-react"; +import { ArrowLeft, Plus, RefreshCw, Search, LayoutGrid, LayoutList, TestTube2, ChevronRight, Monitor, Database, FolderOpen, MoreHorizontal, PanelLeftClose, PanelLeftOpen } from "lucide-react"; import ScreenList from "@/components/screen/ScreenList"; import ScreenDesigner from "@/components/screen/ScreenDesigner"; import TemplateManager from "@/components/screen/TemplateManager"; @@ -16,11 +16,17 @@ import { ScreenDefinition } from "@/types/screen"; import { screenApi } from "@/lib/api/screen"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Badge } from "@/components/ui/badge"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import CreateScreenModal from "@/components/screen/CreateScreenModal"; // 단계별 진행을 위한 타입 정의 type Step = "list" | "design" | "template" | "v2-test"; -type ViewMode = "tree" | "table"; +type ViewMode = "flow" | "card"; export default function ScreenManagementPage() { const searchParams = useSearchParams(); @@ -29,7 +35,7 @@ export default function ScreenManagementPage() { const [selectedGroup, setSelectedGroup] = useState<{ id: number; name: string; company_code?: string } | null>(null); const [focusedScreenIdInGroup, setFocusedScreenIdInGroup] = useState(null); const [stepHistory, setStepHistory] = useState(["list"]); - const [viewMode, setViewMode] = useState("tree"); + const [viewMode, setViewMode] = useState("flow"); const [screens, setScreens] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); @@ -162,32 +168,23 @@ export default function ScreenManagementPage() { return (
{/* 페이지 헤더 */} -
+
-
-

화면 관리

-

화면을 그룹별로 관리하고 데이터 관계를 확인합니다

+
+

화면 관리

+ {screens.length}개 화면
- {/* V2 컴포넌트 테스트 버튼 */} - {/* 뷰 모드 전환 */} setViewMode(v as ViewMode)}> - + - 트리 + 관계도 - + - 테이블 + 카드 @@ -198,40 +195,25 @@ export default function ScreenManagementPage() { 새 화면 + + + + + + goToNextStep("v2-test")}> + + V2 테스트 + + +
-
-
- - {/* 통계 요약 바 */} -
-
- - 화면 - {screens.length} -
-
-
- - 테이블 - {tableCount} -
- {(selectedGroup || selectedScreen) && ( -
- 현재: - {selectedGroup && {selectedGroup.name}} - {selectedScreen && ( - <> - - {selectedScreen.screenName} - - )} -
- )}
{/* 메인 콘텐츠 */} - {viewMode === "tree" ? ( + {viewMode === "flow" ? (
{/* 왼쪽: 트리 구조 */}
@@ -306,7 +288,7 @@ export default function ScreenManagementPage() {
) : ( - // 테이블 뷰 (기존 ScreenList 사용) + // 카드 뷰 (기존 ScreenList 사용)
+ {/* 글로우용 SVG 필터 정의 (엣지별 고유 ID) */} + + + + + + + + + + {/* 글로우 레이어 */} + + {/* 메인 엣지 */} + + {/* 흐르는 파티클 */} + {isActive && ( + <> + + + + + + + + )} + + ); +} From d542e92021e933072fbeee131c0dc024f949d418 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 18:31:12 +0900 Subject: [PATCH 26/39] [agent-pipeline] pipe-20260315091327-kxyf round-2 --- .../admin/screenMng/screenMngList/page.tsx | 147 +++++++++++------- .../components/screen/AnimatedFlowEdge.tsx | 2 +- 2 files changed, 93 insertions(+), 56 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 3631aa19..e5d26eef 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -40,6 +40,7 @@ export default function ScreenManagementPage() { const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); const [isCreateOpen, setIsCreateOpen] = useState(false); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const tableCount = useMemo(() => new Set(screens.map((s) => s.tableName).filter(Boolean)).size, [screens]); @@ -215,64 +216,100 @@ export default function ScreenManagementPage() { {/* 메인 콘텐츠 */} {viewMode === "flow" ? (
- {/* 왼쪽: 트리 구조 */} -
- {/* 검색 */} -
-
- - setSearchTerm(e.target.value)} - className="pl-9 h-9 rounded-xl bg-muted/30 border-border/50 focus:bg-background focus:ring-2 focus:ring-primary/30 transition-colors" - /> -
+ {/* 왼쪽: 트리 구조 (접기/펼치기) */} +
+ {/* 사이드바 토글 */} +
+ {!sidebarCollapsed && 탐색} +
- {/* 트리 뷰 */} -
- { - setSelectedGroup(group); - setSelectedScreen(null); // 화면 선택 해제 - setFocusedScreenIdInGroup(null); // 포커스 초기화 - }} - onScreenSelectInGroup={(group, screenId) => { - // 그룹 내 화면 클릭 시 - const isNewGroup = selectedGroup?.id !== group.id; - - if (isNewGroup) { - // 새 그룹 진입: 포커싱 없이 시작 (첫 진입 시 망가지는 문제 방지) - setSelectedGroup(group); - setFocusedScreenIdInGroup(null); - } else { - // 같은 그룹 내에서 다른 화면 클릭: 포커싱 유지 - setFocusedScreenIdInGroup(screenId); - } - setSelectedScreen(null); - }} - /> -
- {/* 선택 미리보기 */} - {selectedScreen && ( -
-
- - {selectedScreen.screenName} + + {!sidebarCollapsed && ( + <> + {/* 검색 */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-9 h-9 rounded-xl bg-muted/30 border-border/50 focus:bg-background focus:ring-2 focus:ring-primary/30 transition-colors" + /> +
-
- {selectedScreen.screenCode} - {selectedScreen.tableName || "테이블 없음"} + {/* 트리 뷰 */} +
+ { + setSelectedGroup(group); + setSelectedScreen(null); + setFocusedScreenIdInGroup(null); + }} + onScreenSelectInGroup={(group, screenId) => { + const isNewGroup = selectedGroup?.id !== group.id; + if (isNewGroup) { + setSelectedGroup(group); + setFocusedScreenIdInGroup(null); + } else { + setFocusedScreenIdInGroup(screenId); + } + setSelectedScreen(null); + }} + />
-
- + {/* 선택 미리보기 */} + {selectedScreen && ( +
+
+ + {selectedScreen.screenName} +
+
+ {selectedScreen.screenCode} + {selectedScreen.tableName || "테이블 없음"} +
+
+ +
+
+ )} + + )} + + {/* 접힌 상태: 검색 아이콘 + 화면 수 배지 */} + {sidebarCollapsed && ( +
+ +
+ {screens.length}
)} diff --git a/frontend/components/screen/AnimatedFlowEdge.tsx b/frontend/components/screen/AnimatedFlowEdge.tsx index dc33dcfa..f5d8781a 100644 --- a/frontend/components/screen/AnimatedFlowEdge.tsx +++ b/frontend/components/screen/AnimatedFlowEdge.tsx @@ -28,7 +28,7 @@ export function AnimatedFlowEdge({ const strokeColor = (style?.stroke as string) || "hsl(var(--primary))"; const strokeW = (style?.strokeWidth as number) || 2; const isActive = data?.active !== false; - const duration = data?.duration || "3s"; + const duration: string = typeof data?.duration === "string" ? data.duration : "3s"; const filterId = `edge-glow-${id}`; return ( From ffc7cb7933914adde8dddd48e4e26f95e4c32eb3 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 18:31:12 +0900 Subject: [PATCH 27/39] [agent-pipeline] rollback to 784dc73a --- .../admin/screenMng/screenMngList/page.tsx | 147 +++++++----------- .../components/screen/AnimatedFlowEdge.tsx | 2 +- 2 files changed, 56 insertions(+), 93 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index e5d26eef..3631aa19 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -40,7 +40,6 @@ export default function ScreenManagementPage() { const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); const [isCreateOpen, setIsCreateOpen] = useState(false); - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const tableCount = useMemo(() => new Set(screens.map((s) => s.tableName).filter(Boolean)).size, [screens]); @@ -216,100 +215,64 @@ export default function ScreenManagementPage() { {/* 메인 콘텐츠 */} {viewMode === "flow" ? (
- {/* 왼쪽: 트리 구조 (접기/펼치기) */} -
- {/* 사이드바 토글 */} -
- {!sidebarCollapsed && 탐색} - + {/* 왼쪽: 트리 구조 */} +
+ {/* 검색 */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-9 h-9 rounded-xl bg-muted/30 border-border/50 focus:bg-background focus:ring-2 focus:ring-primary/30 transition-colors" + /> +
- - {!sidebarCollapsed && ( - <> - {/* 검색 */} -
-
- - setSearchTerm(e.target.value)} - className="pl-9 h-9 rounded-xl bg-muted/30 border-border/50 focus:bg-background focus:ring-2 focus:ring-primary/30 transition-colors" - /> -
+ {/* 트리 뷰 */} +
+ { + setSelectedGroup(group); + setSelectedScreen(null); // 화면 선택 해제 + setFocusedScreenIdInGroup(null); // 포커스 초기화 + }} + onScreenSelectInGroup={(group, screenId) => { + // 그룹 내 화면 클릭 시 + const isNewGroup = selectedGroup?.id !== group.id; + + if (isNewGroup) { + // 새 그룹 진입: 포커싱 없이 시작 (첫 진입 시 망가지는 문제 방지) + setSelectedGroup(group); + setFocusedScreenIdInGroup(null); + } else { + // 같은 그룹 내에서 다른 화면 클릭: 포커싱 유지 + setFocusedScreenIdInGroup(screenId); + } + setSelectedScreen(null); + }} + /> +
+ {/* 선택 미리보기 */} + {selectedScreen && ( +
+
+ + {selectedScreen.screenName}
- {/* 트리 뷰 */} -
- { - setSelectedGroup(group); - setSelectedScreen(null); - setFocusedScreenIdInGroup(null); - }} - onScreenSelectInGroup={(group, screenId) => { - const isNewGroup = selectedGroup?.id !== group.id; - if (isNewGroup) { - setSelectedGroup(group); - setFocusedScreenIdInGroup(null); - } else { - setFocusedScreenIdInGroup(screenId); - } - setSelectedScreen(null); - }} - /> +
+ {selectedScreen.screenCode} + {selectedScreen.tableName || "테이블 없음"}
- {/* 선택 미리보기 */} - {selectedScreen && ( -
-
- - {selectedScreen.screenName} -
-
- {selectedScreen.screenCode} - {selectedScreen.tableName || "테이블 없음"} -
-
- -
-
- )} - - )} - - {/* 접힌 상태: 검색 아이콘 + 화면 수 배지 */} - {sidebarCollapsed && ( -
- -
- {screens.length} +
+
)} diff --git a/frontend/components/screen/AnimatedFlowEdge.tsx b/frontend/components/screen/AnimatedFlowEdge.tsx index f5d8781a..dc33dcfa 100644 --- a/frontend/components/screen/AnimatedFlowEdge.tsx +++ b/frontend/components/screen/AnimatedFlowEdge.tsx @@ -28,7 +28,7 @@ export function AnimatedFlowEdge({ const strokeColor = (style?.stroke as string) || "hsl(var(--primary))"; const strokeW = (style?.strokeWidth as number) || 2; const isActive = data?.active !== false; - const duration: string = typeof data?.duration === "string" ? data.duration : "3s"; + const duration = data?.duration || "3s"; const filterId = `edge-glow-${id}`; return ( From 2cb736dac1f9f013c7bdb57ae69c8f0dbdfc1b8e Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 18:46:28 +0900 Subject: [PATCH 28/39] [agent-pipeline] pipe-20260315091327-kxyf round-3 --- .../admin/screenMng/screenMngList/page.tsx | 38 +++++++++++++++---- frontend/components/screen/ScreenNode.tsx | 35 +++++++++++++---- .../components/screen/ScreenRelationFlow.tsx | 37 +++++++++++++----- 3 files changed, 85 insertions(+), 25 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 3631aa19..2ff3096b 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -4,8 +4,7 @@ import { useState, useEffect, useCallback, useMemo } from "react"; import { useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { ArrowLeft, Plus, RefreshCw, Search, LayoutGrid, LayoutList, TestTube2, ChevronRight, Monitor, Database, FolderOpen, MoreHorizontal, PanelLeftClose, PanelLeftOpen } from "lucide-react"; -import ScreenList from "@/components/screen/ScreenList"; +import { Plus, RefreshCw, Search, LayoutGrid, LayoutList, TestTube2, Monitor, MoreHorizontal, PanelLeftClose, PanelLeftOpen } from "lucide-react"; import ScreenDesigner from "@/components/screen/ScreenDesigner"; import TemplateManager from "@/components/screen/TemplateManager"; import { ScreenGroupTreeView } from "@/components/screen/ScreenGroupTreeView"; @@ -288,13 +287,36 @@ export default function ScreenManagementPage() {
) : ( - // 카드 뷰 (기존 ScreenList 사용)
- +
+ {filteredScreens.map((screen) => ( +
handleScreenSelect(screen)} + onDoubleClick={() => handleDesignScreen(screen)} + > +
+ +
+
+
{screen.screenName}
+
{screen.screenCode}
+
+ {screen.tableName || "테이블 없음"} +
+
+
+ ))} +
+ {filteredScreens.length === 0 && ( +
+ +

검색 결과가 없습니다

+
+ )}
)} diff --git a/frontend/components/screen/ScreenNode.tsx b/frontend/components/screen/ScreenNode.tsx index 119b24e3..119f6944 100644 --- a/frontend/components/screen/ScreenNode.tsx +++ b/frontend/components/screen/ScreenNode.tsx @@ -14,6 +14,22 @@ import { } from "lucide-react"; import { ScreenLayoutSummary } from "@/lib/api/screenGroup"; +// 글로우 펄스 애니메이션 CSS 주입 +if (typeof document !== "undefined") { + const styleId = "glow-pulse-animation"; + if (!document.getElementById(styleId)) { + const style = document.createElement("style"); + style.id = styleId; + style.textContent = ` + @keyframes glow-pulse { + from { filter: drop-shadow(0 0 6px hsl(221.2 83.2% 53.3% / 0.4)) drop-shadow(0 0 14px hsl(221.2 83.2% 53.3% / 0.2)); } + to { filter: drop-shadow(0 0 10px hsl(221.2 83.2% 53.3% / 0.6)) drop-shadow(0 0 22px hsl(221.2 83.2% 53.3% / 0.3)); } + } + `; + document.head.appendChild(style); + } +} + // ========== 타입 정의 ========== // 화면 노드 데이터 인터페이스 @@ -181,14 +197,19 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
{/* Handles */} @@ -196,19 +217,19 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => { type="target" position={Position.Left} id="left" - className="!h-2 !w-2 !border-2 !border-background !bg-primary opacity-0 transition-opacity group-hover:opacity-100" + className="!h-2.5 !w-2.5 !border-2 !border-background !bg-primary opacity-0 transition-all duration-300 group-hover:opacity-100 group-hover:shadow-[0_0_6px_hsl(var(--primary)/0.5)]" /> {/* 헤더 (컬러) */} diff --git a/frontend/components/screen/ScreenRelationFlow.tsx b/frontend/components/screen/ScreenRelationFlow.tsx index 05a6ed04..59ced0ec 100644 --- a/frontend/components/screen/ScreenRelationFlow.tsx +++ b/frontend/components/screen/ScreenRelationFlow.tsx @@ -34,6 +34,7 @@ import { import { getTableColumns, ColumnTypeInfo } from "@/lib/api/tableManagement"; import { ScreenSettingModal } from "./ScreenSettingModal"; import { TableSettingModal } from "./TableSettingModal"; +import { AnimatedFlowEdge } from "./AnimatedFlowEdge"; import { Monitor, Database, FolderOpen } from "lucide-react"; // 관계 유형별 색상 정의 (CSS 변수 기반 - 다크모드 자동 대응) @@ -51,6 +52,10 @@ const nodeTypes = { tableNode: TableNode, }; +const edgeTypes = { + animatedFlow: AnimatedFlowEdge, +}; + // 레이아웃 상수 const SCREEN_Y = 50; // 화면 노드 Y 위치 (상단) const TABLE_Y = 420; // 메인 테이블 노드 Y 위치 (중단) @@ -688,7 +693,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId target: `screen-${nextScreen.screenId}`, sourceHandle: "right", targetHandle: "left", - type: "smoothstep", + type: "animatedFlow", label: `${i + 1}`, labelStyle: { fontSize: 11, fill: "hsl(var(--info))", fontWeight: 600 }, labelBgStyle: { fill: "hsl(var(--card))", stroke: "hsl(var(--border))", strokeWidth: 1 }, @@ -710,7 +715,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId target: `table-${scr.tableName}`, sourceHandle: "bottom", targetHandle: "top", - type: "smoothstep", + type: "animatedFlow", animated: true, // 모든 메인 테이블 연결은 애니메이션 style: { stroke: "hsl(var(--primary))", @@ -749,7 +754,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId target: targetNodeId, sourceHandle: "bottom", targetHandle: "top", - type: "smoothstep", + type: "animatedFlow", animated: true, style: { stroke: "hsl(var(--primary))", @@ -794,7 +799,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId target: refTargetNodeId, sourceHandle: "bottom", targetHandle: "bottom_target", - type: "smoothstep", + type: "animatedFlow", animated: false, style: { stroke: RELATION_COLORS.join.strokeLight, // 초기값 (연한색) @@ -902,7 +907,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId target: `table-${referencedTable}`, // 참조당하는 테이블 sourceHandle: "bottom", // 하단에서 나감 (서브테이블 구간으로) targetHandle: "bottom_target", // 하단으로 들어감 - type: "smoothstep", + type: "animatedFlow", animated: false, style: { stroke: relationColor.strokeLight, // 관계 유형별 연한 색상 @@ -945,7 +950,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId target: `subtable-${subTable.tableName}`, sourceHandle: "bottom", targetHandle: "top", - type: "smoothstep", + type: "animatedFlow", markerEnd: { type: MarkerType.ArrowClosed, color: relationColor.strokeLight @@ -974,7 +979,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId target: `table-${join.join_table}`, sourceHandle: "bottom", targetHandle: "bottom_target", - type: "smoothstep", + type: "animatedFlow", markerEnd: { type: MarkerType.ArrowClosed, color: RELATION_COLORS.join.strokeLight @@ -1006,7 +1011,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId target: `table-${rel.table_name}`, sourceHandle: "bottom", targetHandle: "top", - type: "smoothstep", + type: "animatedFlow", label: rel.relation_type === "join" ? "조인" : rel.crud_operations || "", labelStyle: { fontSize: 9, fill: "hsl(var(--success))" }, labelBgStyle: { fill: "hsl(var(--card))", stroke: "hsl(var(--border))", strokeWidth: 1 }, @@ -1028,7 +1033,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId target: `screen-${flow.target_screen_id}`, sourceHandle: "right", targetHandle: "left", - type: "smoothstep", + type: "animatedFlow", animated: true, label: flow.flow_label || flow.flow_type || "이동", labelStyle: { fontSize: 10, fill: "hsl(var(--primary))", fontWeight: 500 }, @@ -1994,7 +1999,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId target: targetNodeId, sourceHandle: 'bottom', // 고정: 서브테이블 구간 통과 targetHandle: 'bottom_target', // 고정: 서브테이블 구간 통과 - type: 'smoothstep', + type: "animatedFlow", animated: true, style: { stroke: relationColor.stroke, // 관계 유형별 색상 @@ -2372,10 +2377,22 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId onNodeClick={handleNodeClick} onNodeContextMenu={handleNodeContextMenu} nodeTypes={nodeTypes} + edgeTypes={edgeTypes} minZoom={0.3} maxZoom={1.5} proOptions={{ hideAttribution: true }} > + + + + + + + + + + + {/* 관계 범례 */} From beb95bf2aa4cda501c5d7df84d5d4bd7e48ef1f8 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 18:57:12 +0900 Subject: [PATCH 29/39] [agent-pipeline] pipe-20260315091327-kxyf round-4 --- .../admin/screenMng/screenMngList/page.tsx | 53 ++++++++++++------- .../components/screen/ScreenRelationFlow.tsx | 23 +++++++- 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 2ff3096b..4be1e746 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -22,6 +22,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import CreateScreenModal from "@/components/screen/CreateScreenModal"; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from "@/components/ui/sheet"; // 단계별 진행을 위한 타입 정의 type Step = "list" | "design" | "template" | "v2-test"; @@ -39,6 +40,7 @@ export default function ScreenManagementPage() { const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); const [isCreateOpen, setIsCreateOpen] = useState(false); + const [isDetailOpen, setIsDetailOpen] = useState(false); const tableCount = useMemo(() => new Set(screens.map((s) => s.tableName).filter(Boolean)).size, [screens]); @@ -110,6 +112,7 @@ export default function ScreenManagementPage() { // 화면 선택 핸들러 (개별 화면 선택 시 그룹 선택 해제) const handleScreenSelect = (screen: ScreenDefinition) => { setSelectedScreen(screen); + setIsDetailOpen(true); setSelectedGroup(null); // 그룹 선택 해제 }; @@ -257,24 +260,6 @@ export default function ScreenManagementPage() { }} />
- {/* 선택 미리보기 */} - {selectedScreen && ( -
-
- - {selectedScreen.screenName} -
-
- {selectedScreen.screenCode} - {selectedScreen.tableName || "테이블 없음"} -
-
- -
-
- )}
{/* 오른쪽: 관계 시각화 (React Flow) */} @@ -320,6 +305,38 @@ export default function ScreenManagementPage() {
)} + {/* 화면 디테일 Sheet */} + + + + {selectedScreen?.screenName || "화면 상세"} + {selectedScreen?.screenCode} + + {selectedScreen && ( +
+
+
+ 테이블 + {selectedScreen.tableName || "없음"} +
+
+ 화면 ID + {selectedScreen.screenId} +
+
+
+ + +
+
+ )} +
+
+ {/* 화면 생성 모달 */} - - + + + + { + if (node.type === "screenNode") return "hsl(var(--primary))"; + if (node.type === "tableNode") return "hsl(var(--warning))"; + return "hsl(var(--muted-foreground))"; + }} + nodeStrokeWidth={2} + zoomable + pannable + style={{ + background: "hsl(var(--card) / 0.8)", + border: "1px solid hsl(var(--border) / 0.5)", + borderRadius: "8px", + marginBottom: "8px", + }} + /> {/* 관계 범례 */}

관계 유형

From c0be2f352823670458d416c98dc7e4fccd78b2cb Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 19:57:17 +0900 Subject: [PATCH 30/39] =?UTF-8?q?feat:=20=EC=A0=91=EB=8A=94=20=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=EB=B0=94=20=EA=B5=AC=ED=98=84=20(v5=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=ED=9B=84?= =?UTF-8?q?=EC=86=8D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sidebarCollapsed 상태 + 조건부 렌더링 - PanelLeftOpen/PanelLeftClose 아이콘 토글 - collapsed 시 아이콘 컬럼 표시 Made-with: Cursor --- .../admin/screenMng/screenMngList/page.tsx | 111 +++++++++++------- 1 file changed, 69 insertions(+), 42 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 4be1e746..0b74b2ee 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -41,6 +41,7 @@ export default function ScreenManagementPage() { const [searchTerm, setSearchTerm] = useState(""); const [isCreateOpen, setIsCreateOpen] = useState(false); const [isDetailOpen, setIsDetailOpen] = useState(false); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const tableCount = useMemo(() => new Set(screens.map((s) => s.tableName).filter(Boolean)).size, [screens]); @@ -217,49 +218,75 @@ export default function ScreenManagementPage() { {/* 메인 콘텐츠 */} {viewMode === "flow" ? (
- {/* 왼쪽: 트리 구조 */} -
- {/* 검색 */} -
-
- - setSearchTerm(e.target.value)} - className="pl-9 h-9 rounded-xl bg-muted/30 border-border/50 focus:bg-background focus:ring-2 focus:ring-primary/30 transition-colors" - /> + {/* 왼쪽: 트리 구조 (접기/펼기 지원) */} +
+ {/* 사이드바 헤더 */} +
+ {!sidebarCollapsed && 탐색} + +
+ {/* 사이드바 접힘 시 아이콘 컬럼 */} + {sidebarCollapsed && ( +
+ +
+ {screens.length} +
-
- {/* 트리 뷰 */} -
- { - setSelectedGroup(group); - setSelectedScreen(null); // 화면 선택 해제 - setFocusedScreenIdInGroup(null); // 포커스 초기화 - }} - onScreenSelectInGroup={(group, screenId) => { - // 그룹 내 화면 클릭 시 - const isNewGroup = selectedGroup?.id !== group.id; - - if (isNewGroup) { - // 새 그룹 진입: 포커싱 없이 시작 (첫 진입 시 망가지는 문제 방지) - setSelectedGroup(group); - setFocusedScreenIdInGroup(null); - } else { - // 같은 그룹 내에서 다른 화면 클릭: 포커싱 유지 - setFocusedScreenIdInGroup(screenId); - } - setSelectedScreen(null); - }} - /> -
+ )} + {/* 사이드바 펼침 시 전체 UI */} + {!sidebarCollapsed && ( + <> + {/* 검색 */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-9 h-9 rounded-xl bg-muted/30 border-border/50 focus:bg-background focus:ring-2 focus:ring-primary/30 transition-colors" + /> +
+
+ {/* 트리 뷰 */} +
+ { + setSelectedGroup(group); + setSelectedScreen(null); + setFocusedScreenIdInGroup(null); + }} + onScreenSelectInGroup={(group, screenId) => { + const isNewGroup = selectedGroup?.id !== group.id; + if (isNewGroup) { + setSelectedGroup(group); + setFocusedScreenIdInGroup(null); + } else { + setFocusedScreenIdInGroup(screenId); + } + setSelectedScreen(null); + }} + /> +
+ + )}
{/* 오른쪽: 관계 시각화 (React Flow) */} From 558acd1f9b14c9111ad307afca564187ac9fd8b4 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 20:09:41 +0900 Subject: [PATCH 31/39] [agent-pipeline] pipe-20260315110231-zn60 round-1 --- .../admin/screenMng/screenMngList/page.tsx | 41 ++-- frontend/components/screen/ScreenNode.tsx | 186 +++++++----------- 2 files changed, 101 insertions(+), 126 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 0b74b2ee..450e836a 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -4,7 +4,7 @@ import { useState, useEffect, useCallback, useMemo } from "react"; import { useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Plus, RefreshCw, Search, LayoutGrid, LayoutList, TestTube2, Monitor, MoreHorizontal, PanelLeftClose, PanelLeftOpen } from "lucide-react"; +import { Plus, RefreshCw, Search, LayoutGrid, LayoutList, TestTube2, Database, MoreHorizontal, PanelLeftClose, PanelLeftOpen } from "lucide-react"; import ScreenDesigner from "@/components/screen/ScreenDesigner"; import TemplateManager from "@/components/screen/TemplateManager"; import { ScreenGroupTreeView } from "@/components/screen/ScreenGroupTreeView"; @@ -300,25 +300,42 @@ export default function ScreenManagementPage() {
) : (
-
+
{filteredScreens.map((screen) => (
handleScreenSelect(screen)} onDoubleClick={() => handleDesignScreen(screen)} > -
- -
-
-
{screen.screenName}
-
{screen.screenCode}
-
- {screen.tableName || "테이블 없음"} + {/* 상단: 상태 dot + 이름 + 호버 편집 */} +
+ +
+
{screen.screenName}
+
{screen.screenCode}
+ 편집 +
+ {/* 중단: 메타 정보 */} +
+ + + {screen.tableName || "—"} + +
+ {/* 하단: 타입 칩 + 날짜 */} +
+ + {(screen as { screenType?: string }).screenType === "grid" ? "그리드" : (screen as { screenType?: string }).screenType === "dashboard" ? "대시보드" : "폼"} + + + {screen.createdDate ? new Date(screen.createdDate).toLocaleDateString("ko-KR", { month: "2-digit", day: "2-digit" }) : ""} +
))} diff --git a/frontend/components/screen/ScreenNode.tsx b/frontend/components/screen/ScreenNode.tsx index 119f6944..4e7bfbb5 100644 --- a/frontend/components/screen/ScreenNode.tsx +++ b/frontend/components/screen/ScreenNode.tsx @@ -22,8 +22,8 @@ if (typeof document !== "undefined") { style.id = styleId; style.textContent = ` @keyframes glow-pulse { - from { filter: drop-shadow(0 0 6px hsl(221.2 83.2% 53.3% / 0.4)) drop-shadow(0 0 14px hsl(221.2 83.2% 53.3% / 0.2)); } - to { filter: drop-shadow(0 0 10px hsl(221.2 83.2% 53.3% / 0.6)) drop-shadow(0 0 22px hsl(221.2 83.2% 53.3% / 0.3)); } + from { filter: drop-shadow(0 0 4px hsl(var(--primary) / 0.25)) drop-shadow(0 0 10px hsl(var(--primary) / 0.12)); } + to { filter: drop-shadow(0 0 6px hsl(var(--primary) / 0.35)) drop-shadow(0 0 16px hsl(var(--primary) / 0.18)); } } `; document.head.appendChild(style); @@ -122,42 +122,14 @@ const getScreenTypeIcon = (screenType?: string) => { } }; -// 화면 타입별 색상 (헤더) - 그라데이션 -const getScreenTypeColor = (screenType?: string, isMain?: boolean) => { - if (!isMain) return "bg-gradient-to-r from-muted-foreground to-muted-foreground/80"; - switch (screenType) { - case "grid": - return "bg-gradient-to-r from-primary to-primary/80"; - case "dashboard": - return "bg-gradient-to-r from-warning to-warning/80"; - case "action": - return "bg-gradient-to-r from-destructive to-destructive/80"; - default: - return "bg-gradient-to-r from-primary to-primary/80"; - } +// 화면 타입별 색상 (헤더) - 더 이상 그라데이션 미사용 +const getScreenTypeColor = (_screenType?: string, _isMain?: boolean) => { + return ""; }; -// 화면 역할(screenRole)에 따른 색상 - 그라데이션 -const getScreenRoleColor = (screenRole?: string) => { - if (!screenRole) return "bg-gradient-to-r from-muted-foreground to-muted-foreground/80"; - - // 역할명에 포함된 키워드로 색상 결정 - const role = screenRole.toLowerCase(); - - if (role.includes("그리드") || role.includes("grid") || role.includes("메인") || role.includes("main") || role.includes("list")) { - return "bg-gradient-to-r from-primary to-primary/80"; // 메인 그리드 - } - if (role.includes("등록") || role.includes("폼") || role.includes("form") || role.includes("register") || role.includes("input")) { - return "bg-gradient-to-r from-primary to-primary/80"; // 등록 폼 - } - if (role.includes("액션") || role.includes("action") || role.includes("이벤트") || role.includes("event") || role.includes("클릭")) { - return "bg-gradient-to-r from-destructive to-destructive/80"; // 액션/이벤트 - } - if (role.includes("상세") || role.includes("detail") || role.includes("popup") || role.includes("팝업")) { - return "bg-gradient-to-r from-warning to-warning/80"; // 상세/팝업 - } - - return "bg-gradient-to-r from-muted-foreground to-muted-foreground/80"; // 기본 회색 +// 화면 역할(screenRole)에 따른 색상 - 더 이상 그라데이션 미사용 +const getScreenRoleColor = (_screenRole?: string) => { + return ""; }; // 화면 타입별 라벨 @@ -176,31 +148,17 @@ const getScreenTypeLabel = (screenType?: string) => { // ========== 화면 노드 (상단) - 미리보기 표시 ========== export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => { - const { label, subLabel, isMain, tableName, layoutSummary, isInGroup, isFocused, isFaded, screenRole } = data; + const { label, isMain, tableName, layoutSummary, isFocused, isFaded } = data; const screenType = layoutSummary?.screenType || "form"; - - // 그룹 모드에서는 screenRole 기반 색상, 그렇지 않으면 screenType 기반 색상 - // isFocused일 때 색상 활성화, isFaded일 때 회색 - let headerColor: string; - if (isInGroup) { - if (isFaded) { - headerColor = "bg-gradient-to-r from-muted to-muted/60"; // 흑백 처리 - 더 확실한 회색 - } else { - // 포커스되었거나 아직 아무것도 선택 안됐을 때: 역할별 색상 - headerColor = getScreenRoleColor(screenRole); - } - } else { - headerColor = getScreenTypeColor(screenType, isMain); - } return (
= ({ data }) => { type="target" position={Position.Left} id="left" - className="!h-2.5 !w-2.5 !border-2 !border-background !bg-primary 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/40 opacity-0 transition-all duration-300 group-hover:opacity-100 group-hover:shadow-[0_0_6px_hsl(var(--primary)/0.5)]" /> - {/* 헤더 (컬러) */} -
-
+ {/* 헤더: 그라디언트 제거, 모노크롬 */} +
+
-
{label}
- {tableName &&
{tableName}
} +
{label}
+ {tableName &&
{tableName}
}
- {(isMain || isFocused) && } + {(isMain || isFocused) && }
{/* 화면 미리보기 영역 (컴팩트) */} -
+
{layoutSummary ? ( ) : ( -
+
{getScreenTypeIcon(screenType)} 화면: {label}
)}
- {/* 푸터 (타입 + 컴포넌트 수) */} -
- {getScreenTypeLabel(screenType)} + {/* 푸터 (타입 칩 + 컴포넌트 수) */} +
+ {getScreenTypeLabel(screenType)} {layoutSummary?.totalComponents ?? 0}개 컴포넌트
@@ -306,114 +264,114 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType: }) => { const { totalComponents, widgetCounts } = layoutSummary; - // 그리드 화면 일러스트 + // 그리드 화면 일러스트 (모노크롬) if (screenType === "grid") { - return ( -
+ return ( +
{/* 상단 툴바 */}
-
+
-
-
-
+
+
+
{/* 테이블 헤더 */} -
+
{[...Array(5)].map((_, i) => ( -
+
))}
{/* 테이블 행들 */}
{[...Array(7)].map((_, i) => ( -
+
{[...Array(5)].map((_, j) => ( -
+
))}
))}
{/* 페이지네이션 */}
-
-
-
-
+
+
+
+
); } - // 폼 화면 일러스트 + // 폼 화면 일러스트 (모노크롬) if (screenType === "form") { return ( -
+
{/* 폼 필드들 */} {[...Array(6)].map((_, i) => (
-
-
+
+
))} {/* 버튼 영역 */} -
-
-
+
+
+
); } - // 대시보드 화면 일러스트 + // 대시보드 화면 일러스트 (모노크롬) if (screenType === "dashboard") { return ( -
+
{/* 카드/차트들 */} -
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
{[...Array(10)].map((_, i) => (
))}
-
+
); } - // 액션 화면 일러스트 (버튼 중심) + // 액션 화면 일러스트 (모노크롬) if (screenType === "action") { return ( -
-
+
+
-
+
-
-
-
+
+
+
액션 화면
); } - // 기본 (알 수 없는 타입) + // 기본 (알 수 없는 타입, 모노크롬) return ( -
-
+
+
{getScreenTypeIcon(screenType)}
{totalComponents}개 컴포넌트 From 3ef8cebf1acb461158b620f3eb92deefef3985fb Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 20:14:51 +0900 Subject: [PATCH 32/39] [agent-pipeline] pipe-20260315110231-zn60 round-2 --- frontend/components/screen/ScreenNode.tsx | 102 ++++++++++++---------- 1 file changed, 56 insertions(+), 46 deletions(-) diff --git a/frontend/components/screen/ScreenNode.tsx b/frontend/components/screen/ScreenNode.tsx index 4e7bfbb5..ad124b94 100644 --- a/frontend/components/screen/ScreenNode.tsx +++ b/frontend/components/screen/ScreenNode.tsx @@ -506,21 +506,21 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { return (
= ({ data }) => { type="target" position={Position.Top} id="top" - className="!h-2 !w-2 !border-2 !border-background !bg-primary opacity-0 transition-opacity group-hover:opacity-100" + className="!h-2 !w-2 !border-[1.5px] !border-card !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-2 !border-background !bg-warning opacity-0 transition-opacity group-hover:opacity-100" + className="!h-2 !w-2 !border-[1.5px] !border-card !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-2 !border-background !bg-warning opacity-0 transition-opacity group-hover:opacity-100" + className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100" /> - {/* 헤더 (필터 관계: primary, 필터 소스: primary, 메인: primary, 기본: muted-foreground) - 그라데이션 */} -
- + {/* 헤더: 그라디언트 제거, bg-muted/30 + 아이콘 박스 */} +
+
+ +
-
{label}
+
{label}
{/* 필터 관계에 따른 문구 변경 */} -
+
{isFilterSource ? "마스터 테이블 (필터 소스)" : hasFilterRelation @@ -608,8 +602,8 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
{hasActiveColumns && ( - - {displayColumns.length}개 활성 + + {displayColumns.length} ref )}
@@ -697,18 +691,22 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { opacity: hasActiveColumns ? 0 : 1, }} > - {/* PK/FK/조인/필터 아이콘 */} - {isJoinColumn && } - {(isFilterColumn || isFilterSourceColumn) && !isJoinColumn && } - {!isJoinColumn && !isFilterColumn && !isFilterSourceColumn && col.isPrimaryKey && } - {!isJoinColumn && !isFilterColumn && !isFilterSourceColumn && col.isForeignKey && !col.isPrimaryKey && } - {!isJoinColumn && !isFilterColumn && !isFilterSourceColumn && !col.isPrimaryKey && !col.isForeignKey &&
} + {/* 3px 세로 마커 (PK/FK/조인/필터) */} +
{/* 컬럼명 */} {col.name} @@ -749,7 +747,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { )} {/* 타입 */} - {col.type} + {col.type}
); })} @@ -768,13 +766,25 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { )}
- {/* 푸터 (컴팩트) */} -
- {columns && ( - - {hasActiveColumns ? `${displayColumns.length}/${totalCount}` : totalCount}개 컬럼 - - )} + {/* 푸터: cols + PK/FK 카운트 */} +
+ + {hasActiveColumns ? `${displayColumns.length}/${totalCount}` : totalCount} cols + +
+ {columns?.some(c => c.isPrimaryKey) && ( + + + PK {columns.filter(c => c.isPrimaryKey).length} + + )} + {columns?.some(c => c.isForeignKey) && ( + + + FK {columns.filter(c => c.isForeignKey).length} + + )} +
{/* CSS 애니메이션 정의 */} From 009607f3f11b48dd909b512dd23dac253b697e2a Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 21:18:08 +0900 Subject: [PATCH 33/39] [agent-pipeline] pipe-20260315121506-3c5c round-1 --- .../components/screen/ScreenRelationFlow.tsx | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/frontend/components/screen/ScreenRelationFlow.tsx b/frontend/components/screen/ScreenRelationFlow.tsx index 24ce0ac1..9f75874a 100644 --- a/frontend/components/screen/ScreenRelationFlow.tsx +++ b/frontend/components/screen/ScreenRelationFlow.tsx @@ -47,6 +47,9 @@ const RELATION_COLORS: Record(null); + // 엣지 필터 상태 (유형별 표시/숨김) + const [edgeFilterState, setEdgeFilterState] = useState>({ + main: true, + filter: true, + join: true, + lookup: false, + flow: true, + }); + // 노드 설정 모달 상태 const [isSettingModalOpen, setIsSettingModalOpen] = useState(false); const [settingModalNode, setSettingModalNode] = useState<{ @@ -702,6 +714,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId markerEnd: { type: MarkerType.ArrowClosed, color: "hsl(var(--info))" }, animated: true, style: { stroke: "hsl(var(--info))", strokeWidth: 2 }, + data: { edgeCategory: 'flow' as EdgeCategory }, }); } } @@ -722,6 +735,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId stroke: "hsl(var(--primary))", strokeWidth: 2, }, + data: { edgeCategory: 'main' as EdgeCategory }, }); } }); @@ -764,6 +778,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId }, data: { sourceScreenId, + edgeCategory: 'filter' as EdgeCategory, }, }); @@ -816,6 +831,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId sourceScreenId, isFilterJoin: true, visualRelationType: 'join', + edgeCategory: 'join' as EdgeCategory, }, }); }); @@ -926,6 +942,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId referrerTable, referencedTable, visualRelationType, // 관계 유형 저장 + edgeCategory: (visualRelationType === 'lookup' ? 'lookup' : 'join') as EdgeCategory, }, }); } @@ -966,6 +983,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId data: { sourceScreenId, visualRelationType, + edgeCategory: (visualRelationType === 'lookup' ? 'lookup' : 'join') as EdgeCategory, }, }); }); @@ -992,7 +1010,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId strokeDasharray: "8,4", opacity: 0.5, }, - data: { visualRelationType: 'join' }, + data: { visualRelationType: 'join', edgeCategory: 'join' as EdgeCategory }, }); } }); @@ -1018,6 +1036,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId labelBgStyle: { fill: "hsl(var(--card))", stroke: "hsl(var(--border))", strokeWidth: 1 }, labelBgPadding: [3, 2] as [number, number], style: { stroke: "hsl(var(--success))", strokeWidth: 1.5 }, + data: { edgeCategory: (rel.relation_type === 'lookup' ? 'lookup' : 'join') as EdgeCategory }, }); } } @@ -1042,6 +1061,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId labelBgPadding: [4, 2] as [number, number], markerEnd: { type: MarkerType.ArrowClosed, color: "hsl(var(--primary))" }, style: { stroke: "hsl(var(--primary))", strokeWidth: 2 }, + data: { edgeCategory: 'flow' as EdgeCategory }, }); } }); From 8ed7faf5177f6c35b900d09d16eabb379310cef4 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 21:22:36 +0900 Subject: [PATCH 34/39] [agent-pipeline] pipe-20260315121506-3c5c round-2 --- .../components/screen/ScreenRelationFlow.tsx | 100 +++++++++++++----- 1 file changed, 73 insertions(+), 27 deletions(-) diff --git a/frontend/components/screen/ScreenRelationFlow.tsx b/frontend/components/screen/ScreenRelationFlow.tsx index 9f75874a..09c980df 100644 --- a/frontend/components/screen/ScreenRelationFlow.tsx +++ b/frontend/components/screen/ScreenRelationFlow.tsx @@ -1488,6 +1488,32 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId }); } + // lookup 필터 OFF일 때: lookup 연결만 있는 테이블 노드를 dim 처리 + const lookupOnlyNodes = new Set(); + if (!edgeFilterState.lookup) { + const nodeEdgeCategories = new Map>(); + edges.forEach((edge) => { + const category = (edge.data as any)?.edgeCategory as EdgeCategory | undefined; + if (!category) return; + [edge.source, edge.target].forEach((nodeId) => { + if (!nodeEdgeCategories.has(nodeId)) { + nodeEdgeCategories.set(nodeId, new Set()); + } + nodeEdgeCategories.get(nodeId)!.add(category); + }); + }); + nodeEdgeCategories.forEach((categories, nodeId) => { + if (nodeId.startsWith("table-") || nodeId.startsWith("subtable-")) { + const hasVisibleCategory = Array.from(categories).some( + (cat) => cat !== "lookup" && edgeFilterState[cat] + ); + if (!hasVisibleCategory) { + lookupOnlyNodes.add(nodeId); + } + } + }); + } + return nodes.map((node) => { // 화면 노드 스타일링 (포커스가 있을 때만) if (node.id.startsWith("screen-")) { @@ -1783,7 +1809,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId ...node.data, isFocused: isFocusedTable, isRelated: isRelatedTable, - isFaded: focusedScreenId !== null && !isActiveTable, + isFaded: (focusedScreenId !== null && !isActiveTable) || lookupOnlyNodes.has(node.id), highlightedColumns: isActiveTable ? highlightedColumns : [], joinColumns: isActiveTable ? joinColumns : [], joinColumnRefs: focusedJoinColumnRefs.length > 0 ? focusedJoinColumnRefs : undefined, // 조인 컬럼 참조 정보 @@ -1894,7 +1920,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId data: { ...node.data, isFocused: isActiveSubTable, - isFaded: !isActiveSubTable, + isFaded: !isActiveSubTable || lookupOnlyNodes.has(node.id), highlightedColumns: isActiveSubTable ? subTableHighlightedColumns : [], joinColumns: isActiveSubTable ? subTableJoinColumns : [], fieldMappings: isActiveSubTable ? displayFieldMappings : [], @@ -1905,7 +1931,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId return node; }); - }, [nodes, selectedGroup, focusedScreenId, screenSubTableMap, subTablesDataMap, screenUsedColumnsMap, screenTableMap, tableColumns]); + }, [nodes, selectedGroup, focusedScreenId, screenSubTableMap, subTablesDataMap, screenUsedColumnsMap, screenTableMap, tableColumns, edgeFilterState, edges]); // 포커스에 따른 엣지 스타일링 (그룹 모드 & 개별 화면 모드) const styledEdges = React.useMemo(() => { @@ -2304,8 +2330,19 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId }); // 기존 엣지 + 조인 관계 엣지 합치기 - return [...styledOriginalEdges, ...joinEdges]; - }, [edges, nodes, selectedGroup, focusedScreenId, screen, screenSubTableMap, subTablesDataMap, screenTableMap]); + const allEdges = [...styledOriginalEdges, ...joinEdges]; + // 엣지 필터 적용 (edgeFilterState에 따라 숨김) + return allEdges.map((edge) => { + const category = (edge.data as any)?.edgeCategory as EdgeCategory | undefined; + if (category && !edgeFilterState[category]) { + return { + ...edge, + hidden: true, + }; + } + return edge; + }); + }, [edges, nodes, selectedGroup, focusedScreenId, screen, screenSubTableMap, subTablesDataMap, screenTableMap, edgeFilterState]); // 그룹의 화면 목록 (데이터 흐름 설정용) - 모든 조건부 return 전에 선언해야 함 const groupScreensList = React.useMemo(() => { @@ -2385,6 +2422,37 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId {screen.screenCode} )} + +
+ 연결 + + {( + [ + { key: "main" as EdgeCategory, label: "메인", color: "bg-primary", defaultOn: true }, + { key: "filter" as EdgeCategory, label: "마스터-디테일", color: "bg-[hsl(var(--info))]", defaultOn: true }, + { key: "join" as EdgeCategory, label: "엔티티 조인", color: "bg-amber-400", defaultOn: true }, + { key: "lookup" as EdgeCategory, label: "코드 참조", color: "bg-warning", defaultOn: false }, + ] as const + ).map(({ key, label, color, defaultOn }) => { + const isOn = edgeFilterState[key]; + const count = edges.filter((e) => (e.data as any)?.edgeCategory === key).length; + return ( + + ); + })}
)} {/* isViewReady가 false면 숨김 처리하여 깜빡임 방지 */} @@ -2434,28 +2502,6 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId marginBottom: "8px", }} /> - {/* 관계 범례 */} -
-

관계 유형

-
-
-
- 메인 테이블 -
-
-
- 마스터-디테일 -
-
-
- 코드 참조 -
-
-
- 엔티티 조인 -
-
-
From 232650bc0774890ba6343a455b88c45580e68228 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 22:19:35 +0900 Subject: [PATCH 35/39] [agent-pipeline] pipe-20260315131310-l8kw round-1 --- .../src/controllers/screenGroupController.ts | 113 ++++++++++++++++ frontend/components/screen/ScreenNode.tsx | 124 +++++++++--------- 2 files changed, 175 insertions(+), 62 deletions(-) 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} )}
From 015706b95ac67e25a1e1455f5cdbf0de5dbab23c Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 22:29:56 +0900 Subject: [PATCH 36/39] [agent-pipeline] pipe-20260315131310-l8kw round-2 --- .../admin/screenMng/screenMngList/page.tsx | 86 +++++++++++++------ .../components/screen/ScreenRelationFlow.tsx | 16 ++-- 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 450e836a..e6bbf480 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -4,7 +4,7 @@ import { useState, useEffect, useCallback, useMemo } from "react"; import { useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Plus, RefreshCw, Search, LayoutGrid, LayoutList, TestTube2, Database, MoreHorizontal, PanelLeftClose, PanelLeftOpen } from "lucide-react"; +import { Plus, RefreshCw, Search, X, LayoutGrid, LayoutList, TestTube2, Database, MoreHorizontal, PanelLeftClose, PanelLeftOpen } from "lucide-react"; import ScreenDesigner from "@/components/screen/ScreenDesigner"; import TemplateManager from "@/components/screen/TemplateManager"; import { ScreenGroupTreeView } from "@/components/screen/ScreenGroupTreeView"; @@ -300,42 +300,76 @@ export default function ScreenManagementPage() {
) : (
+ {/* 카드 뷰 상단: 검색 + 카운트 */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-9 h-9 rounded-xl bg-muted/30 dark:bg-muted/30 border-border/50 dark:border-border/50 focus:bg-background focus:ring-2 focus:ring-primary/30 transition-colors" + /> + {searchTerm && ( + + )} +
+ {filteredScreens.length}개 화면 +
{filteredScreens.map((screen) => (
handleScreenSelect(screen)} onDoubleClick={() => handleDesignScreen(screen)} > - {/* 상단: 상태 dot + 이름 + 호버 편집 */} -
- -
-
{screen.screenName}
-
{screen.screenCode}
+ {/* 좌측 타입별 컬러 바 */} +
+
+ {/* 상단: 이름 + 호버 편집 */} +
+
+
{screen.screenName}
+
{screen.screenCode}
+
+ 편집 +
+ {/* 설명 (있으면) */} + {screen.description && ( +
{screen.description}
+ )} + {/* 중단: 메타 정보 */} +
+ + + {screen.tableLabel || screen.tableName || "—"} + +
+ {/* 하단: 타입 칩 + 날짜 */} +
+ + {(screen as { screenType?: string }).screenType === "grid" ? "그리드" : (screen as { screenType?: string }).screenType === "dashboard" ? "대시보드" : "폼"} + + + {screen.updatedDate ? new Date(screen.updatedDate).toLocaleDateString("ko-KR", { month: "2-digit", day: "2-digit" }) : screen.createdDate ? new Date(screen.createdDate).toLocaleDateString("ko-KR", { month: "2-digit", day: "2-digit" }) : ""} +
- 편집 -
- {/* 중단: 메타 정보 */} -
- - - {screen.tableName || "—"} - -
- {/* 하단: 타입 칩 + 날짜 */} -
- - {(screen as { screenType?: string }).screenType === "grid" ? "그리드" : (screen as { screenType?: string }).screenType === "dashboard" ? "대시보드" : "폼"} - - - {screen.createdDate ? new Date(screen.createdDate).toLocaleDateString("ko-KR", { month: "2-digit", day: "2-digit" }) : ""} -
))} diff --git a/frontend/components/screen/ScreenRelationFlow.tsx b/frontend/components/screen/ScreenRelationFlow.tsx index 09c980df..87484840 100644 --- a/frontend/components/screen/ScreenRelationFlow.tsx +++ b/frontend/components/screen/ScreenRelationFlow.tsx @@ -2408,7 +2408,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
{/* 선택 정보 바 (캔버스 상단) */} {(screen || selectedGroup) && ( -
+
{selectedGroup && ( <> @@ -2419,12 +2419,12 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId <> {screen.screenName} - {screen.screenCode} + {screen.screenCode} )} -
- 연결 +
+ 연결 {( [ @@ -2443,13 +2443,13 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId onClick={() => setEdgeFilterState((prev) => ({ ...prev, [key]: !prev[key] }))} className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-medium transition-all duration-200 ${ isOn - ? "bg-foreground/5 border border-border/20 text-foreground/80" - : `border text-muted-foreground/40 ${!defaultOn ? "border-dashed border-border/20" : "border-border/10"}` + ? "bg-foreground/[0.08] dark:bg-foreground/5 border border-border/40 dark:border-border/20 text-foreground/80" + : `border text-muted-foreground/70 dark:text-muted-foreground/40 ${!defaultOn ? "border-dashed border-border/40 dark:border-border/20" : "border-border/40 dark:border-border/10"}` }`} > - + {label} - {count} + {count} ); })} From fe3c6d3bce61b1dde2af40f4400a587d54ef9670 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 22:29:56 +0900 Subject: [PATCH 37/39] [agent-pipeline] rollback to 232650bc --- .../admin/screenMng/screenMngList/page.tsx | 86 ++++++------------- .../components/screen/ScreenRelationFlow.tsx | 16 ++-- 2 files changed, 34 insertions(+), 68 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index e6bbf480..450e836a 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -4,7 +4,7 @@ import { useState, useEffect, useCallback, useMemo } from "react"; import { useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Plus, RefreshCw, Search, X, LayoutGrid, LayoutList, TestTube2, Database, MoreHorizontal, PanelLeftClose, PanelLeftOpen } from "lucide-react"; +import { Plus, RefreshCw, Search, LayoutGrid, LayoutList, TestTube2, Database, MoreHorizontal, PanelLeftClose, PanelLeftOpen } from "lucide-react"; import ScreenDesigner from "@/components/screen/ScreenDesigner"; import TemplateManager from "@/components/screen/TemplateManager"; import { ScreenGroupTreeView } from "@/components/screen/ScreenGroupTreeView"; @@ -300,76 +300,42 @@ export default function ScreenManagementPage() {
) : (
- {/* 카드 뷰 상단: 검색 + 카운트 */} -
-
- - setSearchTerm(e.target.value)} - className="pl-9 h-9 rounded-xl bg-muted/30 dark:bg-muted/30 border-border/50 dark:border-border/50 focus:bg-background focus:ring-2 focus:ring-primary/30 transition-colors" - /> - {searchTerm && ( - - )} -
- {filteredScreens.length}개 화면 -
{filteredScreens.map((screen) => (
handleScreenSelect(screen)} onDoubleClick={() => handleDesignScreen(screen)} > - {/* 좌측 타입별 컬러 바 */} -
-
- {/* 상단: 이름 + 호버 편집 */} -
-
-
{screen.screenName}
-
{screen.screenCode}
-
- 편집 -
- {/* 설명 (있으면) */} - {screen.description && ( -
{screen.description}
- )} - {/* 중단: 메타 정보 */} -
- - - {screen.tableLabel || screen.tableName || "—"} - -
- {/* 하단: 타입 칩 + 날짜 */} -
- - {(screen as { screenType?: string }).screenType === "grid" ? "그리드" : (screen as { screenType?: string }).screenType === "dashboard" ? "대시보드" : "폼"} - - - {screen.updatedDate ? new Date(screen.updatedDate).toLocaleDateString("ko-KR", { month: "2-digit", day: "2-digit" }) : screen.createdDate ? new Date(screen.createdDate).toLocaleDateString("ko-KR", { month: "2-digit", day: "2-digit" }) : ""} - + {/* 상단: 상태 dot + 이름 + 호버 편집 */} +
+ +
+
{screen.screenName}
+
{screen.screenCode}
+ 편집 +
+ {/* 중단: 메타 정보 */} +
+ + + {screen.tableName || "—"} + +
+ {/* 하단: 타입 칩 + 날짜 */} +
+ + {(screen as { screenType?: string }).screenType === "grid" ? "그리드" : (screen as { screenType?: string }).screenType === "dashboard" ? "대시보드" : "폼"} + + + {screen.createdDate ? new Date(screen.createdDate).toLocaleDateString("ko-KR", { month: "2-digit", day: "2-digit" }) : ""} +
))} diff --git a/frontend/components/screen/ScreenRelationFlow.tsx b/frontend/components/screen/ScreenRelationFlow.tsx index 87484840..09c980df 100644 --- a/frontend/components/screen/ScreenRelationFlow.tsx +++ b/frontend/components/screen/ScreenRelationFlow.tsx @@ -2408,7 +2408,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
{/* 선택 정보 바 (캔버스 상단) */} {(screen || selectedGroup) && ( -
+
{selectedGroup && ( <> @@ -2419,12 +2419,12 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId <> {screen.screenName} - {screen.screenCode} + {screen.screenCode} )} -
- 연결 +
+ 연결 {( [ @@ -2443,13 +2443,13 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId onClick={() => setEdgeFilterState((prev) => ({ ...prev, [key]: !prev[key] }))} className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-medium transition-all duration-200 ${ isOn - ? "bg-foreground/[0.08] dark:bg-foreground/5 border border-border/40 dark:border-border/20 text-foreground/80" - : `border text-muted-foreground/70 dark:text-muted-foreground/40 ${!defaultOn ? "border-dashed border-border/40 dark:border-border/20" : "border-border/40 dark:border-border/10"}` + ? "bg-foreground/5 border border-border/20 text-foreground/80" + : `border text-muted-foreground/40 ${!defaultOn ? "border-dashed border-border/20" : "border-border/10"}` }`} > - + {label} - {count} + {count} ); })} From cbd47184e7b2eac9ebe284726d8f98713a45c6a5 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Mon, 16 Mar 2026 09:17:52 +0900 Subject: [PATCH 38/39] Enhance Screen Management UI - Updated the search input to include a clear button for easier user interaction. - Improved the layout with a muted background for better visibility. - Enhanced screen card display with dynamic type color and glow effects based on screen type. - Adjusted text colors for better contrast and readability in dark mode. - Refined connection indicators and button styles for improved UX. Made-with: Cursor --- .../admin/screenMng/screenMngList/page.tsx | 86 +++++++++++++------ .../components/screen/ScreenRelationFlow.tsx | 16 ++-- 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 450e836a..e6bbf480 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -4,7 +4,7 @@ import { useState, useEffect, useCallback, useMemo } from "react"; import { useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Plus, RefreshCw, Search, LayoutGrid, LayoutList, TestTube2, Database, MoreHorizontal, PanelLeftClose, PanelLeftOpen } from "lucide-react"; +import { Plus, RefreshCw, Search, X, LayoutGrid, LayoutList, TestTube2, Database, MoreHorizontal, PanelLeftClose, PanelLeftOpen } from "lucide-react"; import ScreenDesigner from "@/components/screen/ScreenDesigner"; import TemplateManager from "@/components/screen/TemplateManager"; import { ScreenGroupTreeView } from "@/components/screen/ScreenGroupTreeView"; @@ -300,42 +300,76 @@ export default function ScreenManagementPage() {
) : (
+ {/* 카드 뷰 상단: 검색 + 카운트 */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-9 h-9 rounded-xl bg-muted/30 dark:bg-muted/30 border-border/50 dark:border-border/50 focus:bg-background focus:ring-2 focus:ring-primary/30 transition-colors" + /> + {searchTerm && ( + + )} +
+ {filteredScreens.length}개 화면 +
{filteredScreens.map((screen) => (
handleScreenSelect(screen)} onDoubleClick={() => handleDesignScreen(screen)} > - {/* 상단: 상태 dot + 이름 + 호버 편집 */} -
- -
-
{screen.screenName}
-
{screen.screenCode}
+ {/* 좌측 타입별 컬러 바 */} +
+
+ {/* 상단: 이름 + 호버 편집 */} +
+
+
{screen.screenName}
+
{screen.screenCode}
+
+ 편집 +
+ {/* 설명 (있으면) */} + {screen.description && ( +
{screen.description}
+ )} + {/* 중단: 메타 정보 */} +
+ + + {screen.tableLabel || screen.tableName || "—"} + +
+ {/* 하단: 타입 칩 + 날짜 */} +
+ + {(screen as { screenType?: string }).screenType === "grid" ? "그리드" : (screen as { screenType?: string }).screenType === "dashboard" ? "대시보드" : "폼"} + + + {screen.updatedDate ? new Date(screen.updatedDate).toLocaleDateString("ko-KR", { month: "2-digit", day: "2-digit" }) : screen.createdDate ? new Date(screen.createdDate).toLocaleDateString("ko-KR", { month: "2-digit", day: "2-digit" }) : ""} +
- 편집 -
- {/* 중단: 메타 정보 */} -
- - - {screen.tableName || "—"} - -
- {/* 하단: 타입 칩 + 날짜 */} -
- - {(screen as { screenType?: string }).screenType === "grid" ? "그리드" : (screen as { screenType?: string }).screenType === "dashboard" ? "대시보드" : "폼"} - - - {screen.createdDate ? new Date(screen.createdDate).toLocaleDateString("ko-KR", { month: "2-digit", day: "2-digit" }) : ""} -
))} diff --git a/frontend/components/screen/ScreenRelationFlow.tsx b/frontend/components/screen/ScreenRelationFlow.tsx index 09c980df..87484840 100644 --- a/frontend/components/screen/ScreenRelationFlow.tsx +++ b/frontend/components/screen/ScreenRelationFlow.tsx @@ -2408,7 +2408,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
{/* 선택 정보 바 (캔버스 상단) */} {(screen || selectedGroup) && ( -
+
{selectedGroup && ( <> @@ -2419,12 +2419,12 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId <> {screen.screenName} - {screen.screenCode} + {screen.screenCode} )} -
- 연결 +
+ 연결 {( [ @@ -2443,13 +2443,13 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId onClick={() => setEdgeFilterState((prev) => ({ ...prev, [key]: !prev[key] }))} className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-medium transition-all duration-200 ${ isOn - ? "bg-foreground/5 border border-border/20 text-foreground/80" - : `border text-muted-foreground/40 ${!defaultOn ? "border-dashed border-border/20" : "border-border/10"}` + ? "bg-foreground/[0.08] dark:bg-foreground/5 border border-border/40 dark:border-border/20 text-foreground/80" + : `border text-muted-foreground/70 dark:text-muted-foreground/40 ${!defaultOn ? "border-dashed border-border/40 dark:border-border/20" : "border-border/40 dark:border-border/10"}` }`} > - + {label} - {count} + {count} ); })} From 6395f4d032dc3fa9d4b8fe3b8f1c150d8e6d300b Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Mon, 16 Mar 2026 09:17:59 +0900 Subject: [PATCH 39/39] Implement Card Pulse Animation and UI Enhancements - Added a new pulse animation for screen cards to enhance visual feedback. - Updated the background of the screen management list for improved aesthetics. - Refined the search input styling for better integration with the overall UI. - Enhanced screen card hover effects with dynamic glow based on screen type. - Adjusted layout spacing for a more consistent user experience. Made-with: Cursor --- .../admin/screenMng/screenMngList/page.tsx | 121 +++++++++++------- frontend/app/globals.css | 15 +++ 2 files changed, 87 insertions(+), 49 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index e6bbf480..2978e025 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -299,16 +299,16 @@ export default function ScreenManagementPage() {
) : ( -
+
{/* 카드 뷰 상단: 검색 + 카운트 */} -
+
setSearchTerm(e.target.value)} - className="pl-9 h-9 rounded-xl bg-muted/30 dark:bg-muted/30 border-border/50 dark:border-border/50 focus:bg-background focus:ring-2 focus:ring-primary/30 transition-colors" + className="pl-9 h-9 rounded-xl bg-card dark:bg-card border-border/50 shadow-sm focus:bg-card focus:ring-2 focus:ring-primary/30 transition-colors" /> {searchTerm && (
- {filteredScreens.map((screen) => ( -
handleScreenSelect(screen)} - onDoubleClick={() => handleDesignScreen(screen)} - > - {/* 좌측 타입별 컬러 바 */} -
-
- {/* 상단: 이름 + 호버 편집 */} -
-
-
{screen.screenName}
-
{screen.screenCode}
-
- 편집 -
- {/* 설명 (있으면) */} - {screen.description && ( -
{screen.description}
+ {filteredScreens.map((screen) => { + const screenType = (screen as { screenType?: string }).screenType || "form"; + const isSelected = selectedScreen?.screenId === screen.screenId; + const isRecentlyModified = screen.updatedDate && (Date.now() - new Date(screen.updatedDate).getTime()) < 7 * 24 * 60 * 60 * 1000; + + const typeColorClass = screenType === "grid" + ? "from-primary to-primary/20" + : screenType === "dashboard" + ? "from-warning to-warning/20" + : "from-success to-success/20"; + + const glowClass = screenType === "grid" + ? "hover:shadow-[0_8px_24px_-4px_rgba(0,0,0,0.1),0_0_20px_hsl(var(--primary)/0.1)] dark:hover:shadow-[0_8px_24px_-4px_rgba(0,0,0,0.4),0_0_24px_hsl(var(--primary)/0.15)]" + : screenType === "dashboard" + ? "hover:shadow-[0_8px_24px_-4px_rgba(0,0,0,0.1),0_0_20px_hsl(var(--warning)/0.1)] dark:hover:shadow-[0_8px_24px_-4px_rgba(0,0,0,0.4),0_0_24px_hsl(var(--warning)/0.12)]" + : "hover:shadow-[0_8px_24px_-4px_rgba(0,0,0,0.1),0_0_20px_hsl(var(--success)/0.1)] dark:hover:shadow-[0_8px_24px_-4px_rgba(0,0,0,0.4),0_0_24px_hsl(var(--success)/0.12)]"; + + const badgeBgClass = screenType === "grid" + ? "bg-primary/8 dark:bg-primary/15 text-primary" + : screenType === "dashboard" + ? "bg-warning/8 dark:bg-warning/15 text-warning" + : "bg-success/8 dark:bg-success/15 text-success"; + + return ( +
handleScreenSelect(screen)} + onDoubleClick={() => handleDesignScreen(screen)} + > + {/* 좌측 그라데이션 액센트 바 */} +
+ {isSelected && ( +
)} - {/* 중단: 메타 정보 */} -
- - - {screen.tableLabel || screen.tableName || "—"} - -
- {/* 하단: 타입 칩 + 날짜 */} -
- - {(screen as { screenType?: string }).screenType === "grid" ? "그리드" : (screen as { screenType?: string }).screenType === "dashboard" ? "대시보드" : "폼"} - - - {screen.updatedDate ? new Date(screen.updatedDate).toLocaleDateString("ko-KR", { month: "2-digit", day: "2-digit" }) : screen.createdDate ? new Date(screen.createdDate).toLocaleDateString("ko-KR", { month: "2-digit", day: "2-digit" }) : ""} - +
+ {/* Row 1: 이름 + 타입 뱃지 */} +
+
{screen.screenName}
+ + {screenType === "grid" ? "그리드" : screenType === "dashboard" ? "대시보드" : "폼"} + +
+ {/* Row 2: 스크린 코드 */} +
{screen.screenCode}
+ {/* Row 3: 테이블 칩 + 메타 */} +
+ + + {screen.tableLabel || screen.tableName || "—"} + +
+ {/* Row 4: 날짜 + 수정 상태 */} +
+ + {screen.updatedDate ? new Date(screen.updatedDate).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }) : screen.createdDate ? new Date(screen.createdDate).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }) : ""} + + {isRecentlyModified && ( + + + 수정됨 + + )} +
-
- ))} + ); + })}
{filteredScreens.length === 0 && (
diff --git a/frontend/app/globals.css b/frontend/app/globals.css index b3dbab89..8bbfd108 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -427,6 +427,21 @@ select { border-spacing: 0 !important; } +/* ===== 카드 펄스 도트 애니메이션 ===== */ +@keyframes screen-card-pulse { + 0%, 100% { opacity: 0; transform: scale(1); } + 50% { opacity: 0.35; transform: scale(2); } +} +.screen-card-pulse-dot::after { + content: ''; + position: absolute; + inset: -3px; + border-radius: 50%; + background: hsl(var(--success)); + opacity: 0; + animation: screen-card-pulse 2.5s ease-in-out infinite; +} + /* ===== 저장 테이블 막대기 애니메이션 ===== */ @keyframes saveBarDrop { 0% {