[agent-pipeline] pipe-20260315091327-kxyf round-1
This commit is contained in:
parent
27ce039fc8
commit
784dc73abf
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue