2025-09-01 11:48:12 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2026-01-05 10:05:31 +09:00
|
|
|
import { useState, useEffect, useCallback } from "react";
|
2025-09-01 11:48:12 +09:00
|
|
|
import { Button } from "@/components/ui/button";
|
2026-01-05 10:05:31 +09:00
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { ArrowLeft, Plus, RefreshCw, Search, LayoutGrid, LayoutList } from "lucide-react";
|
2025-09-01 11:48:12 +09:00
|
|
|
import ScreenList from "@/components/screen/ScreenList";
|
|
|
|
|
import ScreenDesigner from "@/components/screen/ScreenDesigner";
|
|
|
|
|
import TemplateManager from "@/components/screen/TemplateManager";
|
2026-01-05 10:05:31 +09:00
|
|
|
import { ScreenGroupTreeView } from "@/components/screen/ScreenGroupTreeView";
|
|
|
|
|
import { ScreenRelationFlow } from "@/components/screen/ScreenRelationFlow";
|
2025-10-22 14:52:13 +09:00
|
|
|
import { ScrollToTop } from "@/components/common/ScrollToTop";
|
2025-09-01 11:48:12 +09:00
|
|
|
import { ScreenDefinition } from "@/types/screen";
|
2026-01-05 10:05:31 +09:00
|
|
|
import { screenApi } from "@/lib/api/screen";
|
|
|
|
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
|
|
|
import CreateScreenModal from "@/components/screen/CreateScreenModal";
|
2025-09-01 11:48:12 +09:00
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
// 단계별 진행을 위한 타입 정의
|
|
|
|
|
type Step = "list" | "design" | "template";
|
2026-01-05 10:05:31 +09:00
|
|
|
type ViewMode = "tree" | "table";
|
2025-09-01 14:00:31 +09:00
|
|
|
|
2025-09-01 11:48:12 +09:00
|
|
|
export default function ScreenManagementPage() {
|
2025-09-01 14:00:31 +09:00
|
|
|
const [currentStep, setCurrentStep] = useState<Step>("list");
|
2025-09-01 11:48:12 +09:00
|
|
|
const [selectedScreen, setSelectedScreen] = useState<ScreenDefinition | null>(null);
|
2026-01-09 18:26:37 +09:00
|
|
|
const [selectedGroup, setSelectedGroup] = useState<{ id: number; name: string; company_code?: string } | null>(null);
|
2026-01-05 18:18:26 +09:00
|
|
|
const [focusedScreenIdInGroup, setFocusedScreenIdInGroup] = useState<number | null>(null);
|
2025-09-01 14:00:31 +09:00
|
|
|
const [stepHistory, setStepHistory] = useState<Step[]>(["list"]);
|
2026-01-05 10:05:31 +09:00
|
|
|
const [viewMode, setViewMode] = useState<ViewMode>("tree");
|
|
|
|
|
const [screens, setScreens] = useState<ScreenDefinition[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
|
|
|
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
// 화면 목록 로드
|
|
|
|
|
const loadScreens = useCallback(async () => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
const result = await screenApi.getScreens({ page: 1, size: 1000, searchTerm: "" });
|
|
|
|
|
// screenApi.getScreens는 { data: ScreenDefinition[], total, page, size, totalPages } 형태 반환
|
|
|
|
|
if (result.data && result.data.length > 0) {
|
|
|
|
|
setScreens(result.data);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("화면 목록 로드 실패:", error);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
loadScreens();
|
|
|
|
|
}, [loadScreens]);
|
2025-09-01 14:00:31 +09:00
|
|
|
|
2025-10-15 10:24:33 +09:00
|
|
|
// 화면 설계 모드일 때는 전체 화면 사용
|
|
|
|
|
const isDesignMode = currentStep === "design";
|
|
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
// 다음 단계로 이동
|
|
|
|
|
const goToNextStep = (nextStep: Step) => {
|
|
|
|
|
setStepHistory((prev) => [...prev, nextStep]);
|
|
|
|
|
setCurrentStep(nextStep);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 특정 단계로 이동
|
|
|
|
|
const goToStep = (step: Step) => {
|
|
|
|
|
setCurrentStep(step);
|
|
|
|
|
const stepIndex = stepHistory.findIndex((s) => s === step);
|
|
|
|
|
if (stepIndex !== -1) {
|
|
|
|
|
setStepHistory(stepHistory.slice(0, stepIndex + 1));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-05 18:18:26 +09:00
|
|
|
// 화면 선택 핸들러 (개별 화면 선택 시 그룹 선택 해제)
|
2026-01-05 10:05:31 +09:00
|
|
|
const handleScreenSelect = (screen: ScreenDefinition) => {
|
|
|
|
|
setSelectedScreen(screen);
|
2026-01-05 18:18:26 +09:00
|
|
|
setSelectedGroup(null); // 그룹 선택 해제
|
2026-01-05 10:05:31 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 화면 디자인 핸들러
|
|
|
|
|
const handleDesignScreen = (screen: ScreenDefinition) => {
|
|
|
|
|
setSelectedScreen(screen);
|
|
|
|
|
goToNextStep("design");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 검색어로 필터링된 화면
|
|
|
|
|
const filteredScreens = screens.filter((screen) =>
|
|
|
|
|
screen.screenName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
|
|
screen.screenCode.toLowerCase().includes(searchTerm.toLowerCase())
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 화면 설계 모드일 때는 레이아웃 없이 전체 화면 사용
|
2025-10-15 10:24:33 +09:00
|
|
|
if (isDesignMode) {
|
2025-10-15 10:44:05 +09:00
|
|
|
return (
|
2025-10-22 14:52:13 +09:00
|
|
|
<div className="fixed inset-0 z-50 bg-background">
|
2025-10-15 10:44:05 +09:00
|
|
|
<ScreenDesigner selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-10-15 10:24:33 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-01 11:48:12 +09:00
|
|
|
return (
|
2026-01-05 10:05:31 +09:00
|
|
|
<div className="flex h-screen flex-col bg-background overflow-hidden">
|
|
|
|
|
{/* 페이지 헤더 */}
|
|
|
|
|
<div className="flex-shrink-0 border-b bg-background px-6 py-4">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-2xl font-bold tracking-tight">화면 관리</h1>
|
|
|
|
|
<p className="text-sm text-muted-foreground">화면을 그룹별로 관리하고 데이터 관계를 확인합니다</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
{/* 뷰 모드 전환 */}
|
|
|
|
|
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as ViewMode)}>
|
|
|
|
|
<TabsList className="h-9">
|
|
|
|
|
<TabsTrigger value="tree" className="gap-1.5 px-3">
|
|
|
|
|
<LayoutGrid className="h-4 w-4" />
|
|
|
|
|
트리
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
<TabsTrigger value="table" className="gap-1.5 px-3">
|
|
|
|
|
<LayoutList className="h-4 w-4" />
|
|
|
|
|
테이블
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
</TabsList>
|
|
|
|
|
</Tabs>
|
|
|
|
|
<Button variant="outline" size="icon" onClick={loadScreens}>
|
|
|
|
|
<RefreshCw className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button onClick={() => setIsCreateOpen(true)} className="gap-2">
|
|
|
|
|
<Plus className="h-4 w-4" />
|
|
|
|
|
새 화면
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-09-24 18:07:36 +09:00
|
|
|
</div>
|
2026-01-05 10:05:31 +09:00
|
|
|
</div>
|
2025-09-24 18:07:36 +09:00
|
|
|
|
2026-01-05 10:05:31 +09:00
|
|
|
{/* 메인 콘텐츠 */}
|
|
|
|
|
{viewMode === "tree" ? (
|
|
|
|
|
<div className="flex-1 overflow-hidden flex">
|
|
|
|
|
{/* 왼쪽: 트리 구조 */}
|
|
|
|
|
<div className="w-[350px] min-w-[280px] max-w-[450px] flex flex-col border-r bg-background">
|
|
|
|
|
{/* 검색 */}
|
|
|
|
|
<div className="flex-shrink-0 p-3 border-b">
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="화면 검색..."
|
|
|
|
|
value={searchTerm}
|
|
|
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
|
|
|
className="pl-9 h-9"
|
|
|
|
|
/>
|
2025-09-01 11:48:12 +09:00
|
|
|
</div>
|
2025-09-24 18:07:36 +09:00
|
|
|
</div>
|
2026-01-05 10:05:31 +09:00
|
|
|
{/* 트리 뷰 */}
|
|
|
|
|
<div className="flex-1 overflow-hidden">
|
|
|
|
|
<ScreenGroupTreeView
|
|
|
|
|
screens={filteredScreens}
|
|
|
|
|
selectedScreen={selectedScreen}
|
|
|
|
|
onScreenSelect={handleScreenSelect}
|
|
|
|
|
onScreenDesign={handleDesignScreen}
|
2026-01-05 18:18:26 +09:00
|
|
|
onGroupSelect={(group) => {
|
|
|
|
|
setSelectedGroup(group);
|
|
|
|
|
setSelectedScreen(null); // 화면 선택 해제
|
|
|
|
|
setFocusedScreenIdInGroup(null); // 포커스 초기화
|
|
|
|
|
}}
|
|
|
|
|
onScreenSelectInGroup={(group, screenId) => {
|
2026-01-09 17:03:00 +09:00
|
|
|
// 그룹 내 화면 클릭 시
|
|
|
|
|
const isNewGroup = selectedGroup?.id !== group.id;
|
|
|
|
|
|
|
|
|
|
if (isNewGroup) {
|
|
|
|
|
// 새 그룹 진입: 포커싱 없이 시작 (첫 진입 시 망가지는 문제 방지)
|
2026-01-07 14:50:03 +09:00
|
|
|
setSelectedGroup(group);
|
2026-01-09 17:03:00 +09:00
|
|
|
setFocusedScreenIdInGroup(null);
|
|
|
|
|
} else {
|
|
|
|
|
// 같은 그룹 내에서 다른 화면 클릭: 포커싱 유지
|
|
|
|
|
setFocusedScreenIdInGroup(screenId);
|
2026-01-07 14:50:03 +09:00
|
|
|
}
|
2026-01-05 18:18:26 +09:00
|
|
|
setSelectedScreen(null);
|
|
|
|
|
}}
|
2026-01-05 10:05:31 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 오른쪽: 관계 시각화 (React Flow) */}
|
|
|
|
|
<div className="flex-1 overflow-hidden">
|
2026-01-05 18:18:26 +09:00
|
|
|
<ScreenRelationFlow
|
|
|
|
|
screen={selectedScreen}
|
|
|
|
|
selectedGroup={selectedGroup}
|
|
|
|
|
initialFocusedScreenId={focusedScreenIdInGroup}
|
|
|
|
|
/>
|
2026-01-05 10:05:31 +09:00
|
|
|
</div>
|
2025-09-24 18:07:36 +09:00
|
|
|
</div>
|
2026-01-05 10:05:31 +09:00
|
|
|
) : (
|
|
|
|
|
// 테이블 뷰 (기존 ScreenList 사용)
|
|
|
|
|
<div className="flex-1 overflow-auto p-6">
|
|
|
|
|
<ScreenList
|
|
|
|
|
onScreenSelect={handleScreenSelect}
|
|
|
|
|
selectedScreen={selectedScreen}
|
|
|
|
|
onDesignScreen={handleDesignScreen}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 화면 생성 모달 */}
|
|
|
|
|
<CreateScreenModal
|
|
|
|
|
isOpen={isCreateOpen}
|
|
|
|
|
onClose={() => setIsCreateOpen(false)}
|
|
|
|
|
onSuccess={() => {
|
|
|
|
|
setIsCreateOpen(false);
|
|
|
|
|
loadScreens();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2025-10-22 14:52:13 +09:00
|
|
|
|
|
|
|
|
{/* Scroll to Top 버튼 */}
|
|
|
|
|
<ScrollToTop />
|
2025-09-01 11:48:12 +09:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|