[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 { 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() {
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// 카드 뷰 (기존 ScreenList 사용)
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<ScreenList
|
||||
onScreenSelect={handleScreenSelect}
|
||||
selectedScreen={selectedScreen}
|
||||
onDesignScreen={handleDesignScreen}
|
||||
/>
|
||||
<div className="grid grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
|
||||
{filteredScreens.map((screen) => (
|
||||
<div
|
||||
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>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
|||
<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 ${
|
||||
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
|
||||
? "opacity-50"
|
||||
: "hover:shadow-xl hover:border-border hover:-translate-y-0.5"
|
||||
? "opacity-40"
|
||||
: "hover:shadow-xl hover:border-border hover:-translate-y-0.5 hover:shadow-[0_0_15px_-3px_hsl(var(--primary)/0.2)]"
|
||||
}`}
|
||||
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",
|
||||
animation: isFocused ? "glow-pulse 2s ease-in-out infinite alternate" : "none",
|
||||
}}
|
||||
>
|
||||
{/* 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)]"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.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
|
||||
type="source"
|
||||
position={Position.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 { 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 }}
|
||||
>
|
||||
<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))" />
|
||||
<Controls position="bottom-right" />
|
||||
{/* 관계 범례 */}
|
||||
|
|
|
|||
Loading…
Reference in New Issue