[agent-pipeline] pipe-20260315091327-kxyf round-1

This commit is contained in:
DDD1542 2026-03-15 18:22:20 +09:00
parent 27ce039fc8
commit 784dc73abf
2 changed files with 102 additions and 50 deletions

View File

@ -4,7 +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 } 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 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";
@ -16,11 +16,17 @@ import { ScreenDefinition } from "@/types/screen";
import { screenApi } from "@/lib/api/screen"; import { screenApi } from "@/lib/api/screen";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import CreateScreenModal from "@/components/screen/CreateScreenModal"; import CreateScreenModal from "@/components/screen/CreateScreenModal";
// 단계별 진행을 위한 타입 정의 // 단계별 진행을 위한 타입 정의
type Step = "list" | "design" | "template" | "v2-test"; type Step = "list" | "design" | "template" | "v2-test";
type ViewMode = "tree" | "table"; type ViewMode = "flow" | "card";
export default function ScreenManagementPage() { export default function ScreenManagementPage() {
const searchParams = useSearchParams(); 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 [selectedGroup, setSelectedGroup] = useState<{ id: number; name: string; company_code?: string } | null>(null);
const [focusedScreenIdInGroup, setFocusedScreenIdInGroup] = useState<number | null>(null); const [focusedScreenIdInGroup, setFocusedScreenIdInGroup] = useState<number | null>(null);
const [stepHistory, setStepHistory] = useState<Step[]>(["list"]); const [stepHistory, setStepHistory] = useState<Step[]>(["list"]);
const [viewMode, setViewMode] = useState<ViewMode>("tree"); const [viewMode, setViewMode] = useState<ViewMode>("flow");
const [screens, setScreens] = useState<ScreenDefinition[]>([]); const [screens, setScreens] = useState<ScreenDefinition[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
@ -162,32 +168,23 @@ export default function ScreenManagementPage() {
return ( return (
<div className="flex h-screen flex-col bg-background overflow-hidden"> <div className="flex h-screen flex-col bg-background overflow-hidden">
{/* 페이지 헤더 */} {/* 페이지 헤더 */}
<div className="relative flex-shrink-0 border-b border-border/50 bg-background/95 backdrop-blur-md px-6 py-5"> <div className="flex-shrink-0 border-b border-border/50 bg-background/95 backdrop-blur-md px-6 py-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div className="flex items-center gap-3">
<h1 className="text-3xl font-bold tracking-tight"> </h1> <h1 className="text-xl font-bold tracking-tight"> </h1>
<p className="text-sm text-muted-foreground/80"> </p> <Badge variant="secondary" className="text-xs">{screens.length} </Badge>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* V2 컴포넌트 테스트 버튼 */}
<Button
variant="outline"
onClick={() => goToNextStep("v2-test")}
className="gap-2"
>
<TestTube2 className="h-4 w-4" />
V2
</Button>
{/* 뷰 모드 전환 */} {/* 뷰 모드 전환 */}
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as ViewMode)}> <Tabs value={viewMode} onValueChange={(v) => setViewMode(v as ViewMode)}>
<TabsList className="h-9 bg-muted/50 border border-border/50"> <TabsList className="h-9 bg-muted/50 border border-border/50">
<TabsTrigger value="tree" className="gap-1.5 px-3 text-xs"> <TabsTrigger value="flow" className="gap-1.5 px-3 text-xs">
<LayoutGrid className="h-4 w-4" /> <LayoutGrid className="h-4 w-4" />
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="table" className="gap-1.5 px-3 text-xs"> <TabsTrigger value="card" className="gap-1.5 px-3 text-xs">
<LayoutList className="h-4 w-4" /> <LayoutList className="h-4 w-4" />
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
</Tabs> </Tabs>
@ -198,40 +195,25 @@ export default function ScreenManagementPage() {
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
</Button> </Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => goToNextStep("v2-test")}>
<TestTube2 className="h-4 w-4 mr-2" />
V2
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</div> </div>
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-gradient-to-r from-transparent via-primary/50 to-transparent" />
</div>
{/* 통계 요약 바 */}
<div className="flex-shrink-0 flex items-center gap-6 border-b border-border/50 bg-muted/5 px-6 py-2.5">
<div className="flex items-center gap-2">
<Monitor className="h-4 w-4 text-primary" />
<span className="text-xs text-muted-foreground"></span>
<span className="text-sm font-bold">{screens.length}</span>
</div>
<div className="h-4 w-px bg-border" />
<div className="flex items-center gap-2">
<Database className="h-4 w-4 text-info" />
<span className="text-xs text-muted-foreground"></span>
<span className="text-sm font-bold">{tableCount}</span>
</div>
{(selectedGroup || selectedScreen) && (
<div className="ml-auto flex items-center gap-1.5 text-xs text-muted-foreground">
<span>:</span>
{selectedGroup && <Badge variant="outline" className="text-[10px]">{selectedGroup.name}</Badge>}
{selectedScreen && (
<>
<ChevronRight className="h-3 w-3" />
<Badge variant="secondary" className="text-[10px]">{selectedScreen.screenName}</Badge>
</>
)}
</div>
)}
</div> </div>
{/* 메인 콘텐츠 */} {/* 메인 콘텐츠 */}
{viewMode === "tree" ? ( {viewMode === "flow" ? (
<div className="flex-1 overflow-hidden flex"> <div className="flex-1 overflow-hidden flex">
{/* 왼쪽: 트리 구조 */} {/* 왼쪽: 트리 구조 */}
<div className="w-[350px] min-w-[280px] max-w-[450px] flex flex-col border-r border-border/50 bg-background/80 backdrop-blur-sm"> <div className="w-[350px] min-w-[280px] max-w-[450px] flex flex-col border-r border-border/50 bg-background/80 backdrop-blur-sm">
@ -306,7 +288,7 @@ export default function ScreenManagementPage() {
</div> </div>
</div> </div>
) : ( ) : (
// 테이블 뷰 (기존 ScreenList 사용) // 카드 뷰 (기존 ScreenList 사용)
<div className="flex-1 overflow-auto p-6"> <div className="flex-1 overflow-auto p-6">
<ScreenList <ScreenList
onScreenSelect={handleScreenSelect} onScreenSelect={handleScreenSelect}

View File

@ -0,0 +1,70 @@
"use client";
import React from "react";
import { BaseEdge, getBezierPath, type EdgeProps } from "@xyflow/react";
// 커스텀 애니메이션 엣지 — bezier 곡선 + 흐르는 파티클 + 글로우 레이어
export function AnimatedFlowEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style,
markerEnd,
data,
}: EdgeProps) {
const [edgePath] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
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 filterId = `edge-glow-${id}`;
return (
<>
{/* 글로우용 SVG 필터 정의 (엣지별 고유 ID) */}
<defs>
<filter id={filterId} x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="2" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
{/* 글로우 레이어 */}
<path
d={edgePath}
fill="none"
stroke={strokeColor}
strokeWidth={strokeW + 4}
strokeOpacity={0.12}
filter={`url(#${filterId})`}
/>
{/* 메인 엣지 */}
<BaseEdge id={id} path={edgePath} style={style} markerEnd={markerEnd} />
{/* 흐르는 파티클 */}
{isActive && (
<>
<circle r="3" fill={strokeColor} filter={`url(#${filterId})`}>
<animateMotion dur={duration} repeatCount="indefinite" path={edgePath} />
</circle>
<circle r="1.5" fill="white" opacity="0.85">
<animateMotion dur={duration} repeatCount="indefinite" path={edgePath} />
</circle>
</>
)}
</>
);
}