From 2cb736dac1f9f013c7bdb57ae69c8f0dbdfc1b8e Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 15 Mar 2026 18:46:28 +0900 Subject: [PATCH] [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 }} > + + + + + + + + + + + {/* 관계 범례 */}