[agent-pipeline] pipe-20260315091327-kxyf round-3
This commit is contained in:
parent
ffc7cb7933
commit
2cb736dac1
|
|
@ -4,8 +4,7 @@ import { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
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 { Plus, RefreshCw, Search, LayoutGrid, LayoutList, TestTube2, Monitor, MoreHorizontal, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
||||||
import ScreenList from "@/components/screen/ScreenList";
|
|
||||||
import ScreenDesigner from "@/components/screen/ScreenDesigner";
|
import ScreenDesigner from "@/components/screen/ScreenDesigner";
|
||||||
import TemplateManager from "@/components/screen/TemplateManager";
|
import TemplateManager from "@/components/screen/TemplateManager";
|
||||||
import { ScreenGroupTreeView } from "@/components/screen/ScreenGroupTreeView";
|
import { ScreenGroupTreeView } from "@/components/screen/ScreenGroupTreeView";
|
||||||
|
|
@ -288,13 +287,36 @@ export default function ScreenManagementPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// 카드 뷰 (기존 ScreenList 사용)
|
|
||||||
<div className="flex-1 overflow-auto p-6">
|
<div className="flex-1 overflow-auto p-6">
|
||||||
<ScreenList
|
<div className="grid grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
|
||||||
onScreenSelect={handleScreenSelect}
|
{filteredScreens.map((screen) => (
|
||||||
selectedScreen={selectedScreen}
|
<div
|
||||||
onDesignScreen={handleDesignScreen}
|
key={screen.screenId}
|
||||||
/>
|
className={`group rounded-lg border bg-card hover:border-primary/50 hover:shadow-md transition-all cursor-pointer overflow-hidden ${
|
||||||
|
selectedScreen?.screenId === screen.screenId ? "border-primary ring-2 ring-primary/20" : "border-border/50"
|
||||||
|
}`}
|
||||||
|
onClick={() => handleScreenSelect(screen)}
|
||||||
|
onDoubleClick={() => handleDesignScreen(screen)}
|
||||||
|
>
|
||||||
|
<div className="h-[100px] bg-gradient-to-br from-muted/30 to-muted/60 flex items-center justify-center">
|
||||||
|
<Monitor className="h-8 w-8 text-muted-foreground/40 group-hover:text-primary/40 transition-colors" />
|
||||||
|
</div>
|
||||||
|
<div className="p-3">
|
||||||
|
<div className="text-sm font-semibold truncate">{screen.screenName}</div>
|
||||||
|
<div className="text-xs font-mono text-muted-foreground truncate mt-0.5">{screen.screenCode}</div>
|
||||||
|
<div className="flex items-center justify-between mt-2 text-xs text-muted-foreground">
|
||||||
|
<span>{screen.tableName || "테이블 없음"}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{filteredScreens.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
||||||
|
<Search className="h-8 w-8 mb-3 opacity-30" />
|
||||||
|
<p className="text-sm">검색 결과가 없습니다</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,22 @@ import {
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { ScreenLayoutSummary } from "@/lib/api/screenGroup";
|
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 }) => {
|
||||||
<div
|
<div
|
||||||
className={`group relative flex h-[240px] w-[240px] flex-col overflow-hidden rounded-xl border border-border/50 bg-card/80 backdrop-blur-sm shadow-lg transition-all cursor-pointer ${
|
className={`group relative flex h-[240px] w-[240px] flex-col overflow-hidden rounded-xl border border-border/50 bg-card/80 backdrop-blur-sm shadow-lg transition-all cursor-pointer ${
|
||||||
isFocused
|
isFocused
|
||||||
? "border-2 border-primary ring-4 ring-primary/30 shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] scale-[1.03]"
|
? "border-2 border-primary scale-[1.03]"
|
||||||
: isFaded
|
: isFaded
|
||||||
? "opacity-50"
|
? "opacity-40"
|
||||||
: "hover:shadow-xl hover:border-border hover:-translate-y-0.5"
|
: "hover:shadow-xl hover:border-border hover:-translate-y-0.5 hover:shadow-[0_0_15px_-3px_hsl(var(--primary)/0.2)]"
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
filter: isFaded ? "grayscale(100%)" : "none",
|
filter: isFaded
|
||||||
|
? "grayscale(100%)"
|
||||||
|
: isFocused
|
||||||
|
? "drop-shadow(0 0 8px hsl(var(--primary) / 0.5)) drop-shadow(0 0 20px hsl(var(--primary) / 0.25))"
|
||||||
|
: "none",
|
||||||
transition: "all 0.3s ease",
|
transition: "all 0.3s ease",
|
||||||
|
animation: isFocused ? "glow-pulse 2s ease-in-out infinite alternate" : "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Handles */}
|
{/* Handles */}
|
||||||
|
|
@ -196,19 +217,19 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id="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)]"
|
||||||
/>
|
/>
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
id="right"
|
id="right"
|
||||||
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)]"
|
||||||
/>
|
/>
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Bottom}
|
position={Position.Bottom}
|
||||||
id="bottom"
|
id="bottom"
|
||||||
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)]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 헤더 (컬러) */}
|
{/* 헤더 (컬러) */}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import {
|
||||||
import { getTableColumns, ColumnTypeInfo } from "@/lib/api/tableManagement";
|
import { getTableColumns, ColumnTypeInfo } from "@/lib/api/tableManagement";
|
||||||
import { ScreenSettingModal } from "./ScreenSettingModal";
|
import { ScreenSettingModal } from "./ScreenSettingModal";
|
||||||
import { TableSettingModal } from "./TableSettingModal";
|
import { TableSettingModal } from "./TableSettingModal";
|
||||||
|
import { AnimatedFlowEdge } from "./AnimatedFlowEdge";
|
||||||
import { Monitor, Database, FolderOpen } from "lucide-react";
|
import { Monitor, Database, FolderOpen } from "lucide-react";
|
||||||
|
|
||||||
// 관계 유형별 색상 정의 (CSS 변수 기반 - 다크모드 자동 대응)
|
// 관계 유형별 색상 정의 (CSS 변수 기반 - 다크모드 자동 대응)
|
||||||
|
|
@ -51,6 +52,10 @@ const nodeTypes = {
|
||||||
tableNode: TableNode,
|
tableNode: TableNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const edgeTypes = {
|
||||||
|
animatedFlow: AnimatedFlowEdge,
|
||||||
|
};
|
||||||
|
|
||||||
// 레이아웃 상수
|
// 레이아웃 상수
|
||||||
const SCREEN_Y = 50; // 화면 노드 Y 위치 (상단)
|
const SCREEN_Y = 50; // 화면 노드 Y 위치 (상단)
|
||||||
const TABLE_Y = 420; // 메인 테이블 노드 Y 위치 (중단)
|
const TABLE_Y = 420; // 메인 테이블 노드 Y 위치 (중단)
|
||||||
|
|
@ -688,7 +693,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
target: `screen-${nextScreen.screenId}`,
|
target: `screen-${nextScreen.screenId}`,
|
||||||
sourceHandle: "right",
|
sourceHandle: "right",
|
||||||
targetHandle: "left",
|
targetHandle: "left",
|
||||||
type: "smoothstep",
|
type: "animatedFlow",
|
||||||
label: `${i + 1}`,
|
label: `${i + 1}`,
|
||||||
labelStyle: { fontSize: 11, fill: "hsl(var(--info))", fontWeight: 600 },
|
labelStyle: { fontSize: 11, fill: "hsl(var(--info))", fontWeight: 600 },
|
||||||
labelBgStyle: { fill: "hsl(var(--card))", stroke: "hsl(var(--border))", strokeWidth: 1 },
|
labelBgStyle: { fill: "hsl(var(--card))", stroke: "hsl(var(--border))", strokeWidth: 1 },
|
||||||
|
|
@ -710,7 +715,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
target: `table-${scr.tableName}`,
|
target: `table-${scr.tableName}`,
|
||||||
sourceHandle: "bottom",
|
sourceHandle: "bottom",
|
||||||
targetHandle: "top",
|
targetHandle: "top",
|
||||||
type: "smoothstep",
|
type: "animatedFlow",
|
||||||
animated: true, // 모든 메인 테이블 연결은 애니메이션
|
animated: true, // 모든 메인 테이블 연결은 애니메이션
|
||||||
style: {
|
style: {
|
||||||
stroke: "hsl(var(--primary))",
|
stroke: "hsl(var(--primary))",
|
||||||
|
|
@ -749,7 +754,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
target: targetNodeId,
|
target: targetNodeId,
|
||||||
sourceHandle: "bottom",
|
sourceHandle: "bottom",
|
||||||
targetHandle: "top",
|
targetHandle: "top",
|
||||||
type: "smoothstep",
|
type: "animatedFlow",
|
||||||
animated: true,
|
animated: true,
|
||||||
style: {
|
style: {
|
||||||
stroke: "hsl(var(--primary))",
|
stroke: "hsl(var(--primary))",
|
||||||
|
|
@ -794,7 +799,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
target: refTargetNodeId,
|
target: refTargetNodeId,
|
||||||
sourceHandle: "bottom",
|
sourceHandle: "bottom",
|
||||||
targetHandle: "bottom_target",
|
targetHandle: "bottom_target",
|
||||||
type: "smoothstep",
|
type: "animatedFlow",
|
||||||
animated: false,
|
animated: false,
|
||||||
style: {
|
style: {
|
||||||
stroke: RELATION_COLORS.join.strokeLight, // 초기값 (연한색)
|
stroke: RELATION_COLORS.join.strokeLight, // 초기값 (연한색)
|
||||||
|
|
@ -902,7 +907,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
target: `table-${referencedTable}`, // 참조당하는 테이블
|
target: `table-${referencedTable}`, // 참조당하는 테이블
|
||||||
sourceHandle: "bottom", // 하단에서 나감 (서브테이블 구간으로)
|
sourceHandle: "bottom", // 하단에서 나감 (서브테이블 구간으로)
|
||||||
targetHandle: "bottom_target", // 하단으로 들어감
|
targetHandle: "bottom_target", // 하단으로 들어감
|
||||||
type: "smoothstep",
|
type: "animatedFlow",
|
||||||
animated: false,
|
animated: false,
|
||||||
style: {
|
style: {
|
||||||
stroke: relationColor.strokeLight, // 관계 유형별 연한 색상
|
stroke: relationColor.strokeLight, // 관계 유형별 연한 색상
|
||||||
|
|
@ -945,7 +950,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
target: `subtable-${subTable.tableName}`,
|
target: `subtable-${subTable.tableName}`,
|
||||||
sourceHandle: "bottom",
|
sourceHandle: "bottom",
|
||||||
targetHandle: "top",
|
targetHandle: "top",
|
||||||
type: "smoothstep",
|
type: "animatedFlow",
|
||||||
markerEnd: {
|
markerEnd: {
|
||||||
type: MarkerType.ArrowClosed,
|
type: MarkerType.ArrowClosed,
|
||||||
color: relationColor.strokeLight
|
color: relationColor.strokeLight
|
||||||
|
|
@ -974,7 +979,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
target: `table-${join.join_table}`,
|
target: `table-${join.join_table}`,
|
||||||
sourceHandle: "bottom",
|
sourceHandle: "bottom",
|
||||||
targetHandle: "bottom_target",
|
targetHandle: "bottom_target",
|
||||||
type: "smoothstep",
|
type: "animatedFlow",
|
||||||
markerEnd: {
|
markerEnd: {
|
||||||
type: MarkerType.ArrowClosed,
|
type: MarkerType.ArrowClosed,
|
||||||
color: RELATION_COLORS.join.strokeLight
|
color: RELATION_COLORS.join.strokeLight
|
||||||
|
|
@ -1006,7 +1011,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
target: `table-${rel.table_name}`,
|
target: `table-${rel.table_name}`,
|
||||||
sourceHandle: "bottom",
|
sourceHandle: "bottom",
|
||||||
targetHandle: "top",
|
targetHandle: "top",
|
||||||
type: "smoothstep",
|
type: "animatedFlow",
|
||||||
label: rel.relation_type === "join" ? "조인" : rel.crud_operations || "",
|
label: rel.relation_type === "join" ? "조인" : rel.crud_operations || "",
|
||||||
labelStyle: { fontSize: 9, fill: "hsl(var(--success))" },
|
labelStyle: { fontSize: 9, fill: "hsl(var(--success))" },
|
||||||
labelBgStyle: { fill: "hsl(var(--card))", stroke: "hsl(var(--border))", strokeWidth: 1 },
|
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}`,
|
target: `screen-${flow.target_screen_id}`,
|
||||||
sourceHandle: "right",
|
sourceHandle: "right",
|
||||||
targetHandle: "left",
|
targetHandle: "left",
|
||||||
type: "smoothstep",
|
type: "animatedFlow",
|
||||||
animated: true,
|
animated: true,
|
||||||
label: flow.flow_label || flow.flow_type || "이동",
|
label: flow.flow_label || flow.flow_type || "이동",
|
||||||
labelStyle: { fontSize: 10, fill: "hsl(var(--primary))", fontWeight: 500 },
|
labelStyle: { fontSize: 10, fill: "hsl(var(--primary))", fontWeight: 500 },
|
||||||
|
|
@ -1994,7 +1999,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
target: targetNodeId,
|
target: targetNodeId,
|
||||||
sourceHandle: 'bottom', // 고정: 서브테이블 구간 통과
|
sourceHandle: 'bottom', // 고정: 서브테이블 구간 통과
|
||||||
targetHandle: 'bottom_target', // 고정: 서브테이블 구간 통과
|
targetHandle: 'bottom_target', // 고정: 서브테이블 구간 통과
|
||||||
type: 'smoothstep',
|
type: "animatedFlow",
|
||||||
animated: true,
|
animated: true,
|
||||||
style: {
|
style: {
|
||||||
stroke: relationColor.stroke, // 관계 유형별 색상
|
stroke: relationColor.stroke, // 관계 유형별 색상
|
||||||
|
|
@ -2372,10 +2377,22 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
onNodeClick={handleNodeClick}
|
onNodeClick={handleNodeClick}
|
||||||
onNodeContextMenu={handleNodeContextMenu}
|
onNodeContextMenu={handleNodeContextMenu}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
|
edgeTypes={edgeTypes}
|
||||||
minZoom={0.3}
|
minZoom={0.3}
|
||||||
maxZoom={1.5}
|
maxZoom={1.5}
|
||||||
proOptions={{ hideAttribution: true }}
|
proOptions={{ hideAttribution: true }}
|
||||||
>
|
>
|
||||||
|
<svg style={{ position: "absolute", width: 0, height: 0 }}>
|
||||||
|
<defs>
|
||||||
|
<filter id="edge-glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="3" result="blur" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="blur" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="hsl(var(--border))" />
|
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="hsl(var(--border))" />
|
||||||
<Controls position="bottom-right" />
|
<Controls position="bottom-right" />
|
||||||
{/* 관계 범례 */}
|
{/* 관계 범례 */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue