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 }}
>
+
{/* 관계 범례 */}