407 lines
16 KiB
TypeScript
407 lines
16 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useState, useEffect, useCallback } from "react";
|
||
|
|
import { useSearchParams } from "next/navigation";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { Input } from "@/components/ui/input";
|
||
|
|
import { Plus, RefreshCw, Search, LayoutGrid, LayoutList, Smartphone, Tablet, Eye } from "lucide-react";
|
||
|
|
import { PopDesigner } from "@/components/pop/designer";
|
||
|
|
import { ScreenRelationFlow } from "@/components/screen/ScreenRelationFlow";
|
||
|
|
import { ScrollToTop } from "@/components/common/ScrollToTop";
|
||
|
|
import { ScreenDefinition } from "@/types/screen";
|
||
|
|
import { screenApi } from "@/lib/api/screen";
|
||
|
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||
|
|
import CreateScreenModal from "@/components/screen/CreateScreenModal";
|
||
|
|
import { Badge } from "@/components/ui/badge";
|
||
|
|
|
||
|
|
// 단계별 진행을 위한 타입 정의
|
||
|
|
type Step = "list" | "design";
|
||
|
|
type ViewMode = "tree" | "table";
|
||
|
|
type DevicePreview = "mobile" | "tablet";
|
||
|
|
|
||
|
|
export default function PopScreenManagementPage() {
|
||
|
|
const searchParams = useSearchParams();
|
||
|
|
const [currentStep, setCurrentStep] = useState<Step>("list");
|
||
|
|
const [selectedScreen, setSelectedScreen] = useState<ScreenDefinition | null>(null);
|
||
|
|
const [selectedGroup, setSelectedGroup] = useState<{ id: number; name: string; company_code?: string } | null>(null);
|
||
|
|
const [focusedScreenIdInGroup, setFocusedScreenIdInGroup] = useState<number | null>(null);
|
||
|
|
const [stepHistory, setStepHistory] = useState<Step[]>(["list"]);
|
||
|
|
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 [devicePreview, setDevicePreview] = useState<DevicePreview>("tablet");
|
||
|
|
|
||
|
|
// POP 레이아웃 존재 화면 ID
|
||
|
|
const [popLayoutScreenIds, setPopLayoutScreenIds] = useState<Set<number>>(new Set());
|
||
|
|
|
||
|
|
// 화면 목록 및 POP 레이아웃 존재 여부 로드
|
||
|
|
const loadScreens = useCallback(async () => {
|
||
|
|
try {
|
||
|
|
setLoading(true);
|
||
|
|
const [result, popScreenIds] = await Promise.all([
|
||
|
|
screenApi.getScreens({ page: 1, size: 1000, searchTerm: "" }),
|
||
|
|
screenApi.getScreenIdsWithPopLayout(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
if (result.data && result.data.length > 0) {
|
||
|
|
setScreens(result.data);
|
||
|
|
}
|
||
|
|
setPopLayoutScreenIds(new Set(popScreenIds));
|
||
|
|
} catch (error) {
|
||
|
|
console.error("POP 화면 목록 로드 실패:", error);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
loadScreens();
|
||
|
|
}, [loadScreens]);
|
||
|
|
|
||
|
|
// 화면 목록 새로고침 이벤트 리스너
|
||
|
|
useEffect(() => {
|
||
|
|
const handleScreenListRefresh = () => {
|
||
|
|
console.log("🔄 POP 화면 목록 새로고침 이벤트 수신");
|
||
|
|
loadScreens();
|
||
|
|
};
|
||
|
|
|
||
|
|
window.addEventListener("screen-list-refresh", handleScreenListRefresh);
|
||
|
|
return () => {
|
||
|
|
window.removeEventListener("screen-list-refresh", handleScreenListRefresh);
|
||
|
|
};
|
||
|
|
}, [loadScreens]);
|
||
|
|
|
||
|
|
// URL 쿼리 파라미터로 화면 디자이너 자동 열기
|
||
|
|
useEffect(() => {
|
||
|
|
const openDesignerId = searchParams.get("openDesigner");
|
||
|
|
if (openDesignerId && screens.length > 0) {
|
||
|
|
const screenId = parseInt(openDesignerId, 10);
|
||
|
|
const targetScreen = screens.find((s) => s.screenId === screenId);
|
||
|
|
if (targetScreen) {
|
||
|
|
setSelectedScreen(targetScreen);
|
||
|
|
setCurrentStep("design");
|
||
|
|
setStepHistory(["list", "design"]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, [searchParams, screens]);
|
||
|
|
|
||
|
|
// 화면 설계 모드일 때는 전체 화면 사용
|
||
|
|
const isDesignMode = currentStep === "design";
|
||
|
|
|
||
|
|
// 다음 단계로 이동
|
||
|
|
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));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 화면 선택 핸들러 (개별 화면 선택 시 그룹 선택 해제)
|
||
|
|
const handleScreenSelect = (screen: ScreenDefinition) => {
|
||
|
|
setSelectedScreen(screen);
|
||
|
|
setSelectedGroup(null);
|
||
|
|
};
|
||
|
|
|
||
|
|
// 화면 디자인 핸들러
|
||
|
|
const handleDesignScreen = (screen: ScreenDefinition) => {
|
||
|
|
setSelectedScreen(screen);
|
||
|
|
goToNextStep("design");
|
||
|
|
};
|
||
|
|
|
||
|
|
// POP 화면 미리보기 (새 탭에서 열기)
|
||
|
|
const handlePreviewScreen = (screen: ScreenDefinition) => {
|
||
|
|
const previewUrl = `/pop/screens/${screen.screenId}?preview=true&device=${devicePreview}`;
|
||
|
|
window.open(previewUrl, "_blank", "width=800,height=900");
|
||
|
|
};
|
||
|
|
|
||
|
|
// POP 화면만 필터링 (POP 레이아웃이 있는 화면만)
|
||
|
|
const popScreens = screens.filter((screen) => popLayoutScreenIds.has(screen.screenId));
|
||
|
|
|
||
|
|
// 검색어 필터링
|
||
|
|
const searchKeywords = searchTerm.toLowerCase().trim().split(/\s+/).filter(Boolean);
|
||
|
|
const filteredScreens = popScreens.filter((screen) => {
|
||
|
|
if (searchKeywords.length > 1) {
|
||
|
|
return true; // 폴더 계층 검색 시 화면 필터링 없음
|
||
|
|
}
|
||
|
|
if (!searchTerm) return true;
|
||
|
|
return (
|
||
|
|
screen.screenName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
|
|
screen.screenCode.toLowerCase().includes(searchTerm.toLowerCase())
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
// POP 화면 수
|
||
|
|
const popScreenCount = popLayoutScreenIds.size;
|
||
|
|
|
||
|
|
// 화면 설계 모드일 때는 POP 전용 디자이너 사용
|
||
|
|
if (isDesignMode && selectedScreen) {
|
||
|
|
return (
|
||
|
|
<div className="fixed inset-0 z-50 bg-background">
|
||
|
|
<PopDesigner
|
||
|
|
selectedScreen={selectedScreen}
|
||
|
|
onBackToList={() => goToStep("list")}
|
||
|
|
onScreenUpdate={(updatedFields) => {
|
||
|
|
setSelectedScreen({
|
||
|
|
...selectedScreen,
|
||
|
|
...updatedFields,
|
||
|
|
});
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex h-screen flex-col bg-background overflow-hidden">
|
||
|
|
{/* 페이지 헤더 */}
|
||
|
|
<div className="shrink-0 border-b bg-background px-6 py-4">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<div>
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<h1 className="text-2xl font-bold tracking-tight">POP 화면 관리</h1>
|
||
|
|
<Badge variant="secondary" className="text-xs">
|
||
|
|
모바일/태블릿
|
||
|
|
</Badge>
|
||
|
|
</div>
|
||
|
|
<p className="text-sm text-muted-foreground">POP 화면을 그룹별로 관리하고 모바일/태블릿에 최적화된 화면을 설계합니다</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
{/* 디바이스 미리보기 선택 */}
|
||
|
|
<Tabs value={devicePreview} onValueChange={(v) => setDevicePreview(v as DevicePreview)}>
|
||
|
|
<TabsList className="h-9">
|
||
|
|
<TabsTrigger value="mobile" className="gap-1.5 px-3">
|
||
|
|
<Smartphone className="h-4 w-4" />
|
||
|
|
모바일
|
||
|
|
</TabsTrigger>
|
||
|
|
<TabsTrigger value="tablet" className="gap-1.5 px-3">
|
||
|
|
<Tablet className="h-4 w-4" />
|
||
|
|
태블릿
|
||
|
|
</TabsTrigger>
|
||
|
|
</TabsList>
|
||
|
|
</Tabs>
|
||
|
|
<div className="w-px h-6 bg-border mx-1" />
|
||
|
|
{/* 뷰 모드 전환 */}
|
||
|
|
<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" />
|
||
|
|
새 POP 화면
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 메인 콘텐츠 */}
|
||
|
|
{popScreenCount === 0 ? (
|
||
|
|
// POP 화면이 없을 때 빈 상태 표시
|
||
|
|
<div className="flex-1 flex flex-col items-center justify-center text-center p-8">
|
||
|
|
<div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4">
|
||
|
|
<Smartphone className="h-8 w-8 text-muted-foreground" />
|
||
|
|
</div>
|
||
|
|
<h3 className="text-lg font-semibold mb-2">POP 화면이 없습니다</h3>
|
||
|
|
<p className="text-sm text-muted-foreground mb-6 max-w-md">
|
||
|
|
아직 생성된 POP 화면이 없습니다.<br />
|
||
|
|
"새 POP 화면" 버튼을 클릭하여 모바일/태블릿용 화면을 만들어보세요.
|
||
|
|
</p>
|
||
|
|
<Button onClick={() => setIsCreateOpen(true)} className="gap-2">
|
||
|
|
<Plus className="h-4 w-4" />
|
||
|
|
새 POP 화면 만들기
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
) : viewMode === "tree" ? (
|
||
|
|
<div className="flex-1 overflow-hidden flex">
|
||
|
|
{/* 왼쪽: POP 화면 목록 */}
|
||
|
|
<div className="w-[350px] min-w-[280px] max-w-[450px] flex flex-col border-r bg-background">
|
||
|
|
{/* 검색 */}
|
||
|
|
<div className="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="POP 화면 검색..."
|
||
|
|
value={searchTerm}
|
||
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||
|
|
className="pl-9 h-9"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
{/* POP 화면 수 표시 */}
|
||
|
|
<div className="flex items-center justify-between mt-2">
|
||
|
|
<span className="text-xs text-muted-foreground">POP 화면</span>
|
||
|
|
<Badge variant="outline" className="text-xs">
|
||
|
|
{popScreenCount}개
|
||
|
|
</Badge>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{/* POP 화면 리스트 */}
|
||
|
|
<div className="flex-1 overflow-auto p-2">
|
||
|
|
{filteredScreens.length === 0 ? (
|
||
|
|
<div className="text-center text-sm text-muted-foreground py-8">
|
||
|
|
검색 결과가 없습니다
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="space-y-1">
|
||
|
|
{filteredScreens.map((screen) => (
|
||
|
|
<div
|
||
|
|
key={screen.screenId}
|
||
|
|
className={`flex items-center justify-between p-3 rounded-lg cursor-pointer transition-colors ${
|
||
|
|
selectedScreen?.screenId === screen.screenId
|
||
|
|
? "bg-primary/10 border border-primary/20"
|
||
|
|
: "hover:bg-muted"
|
||
|
|
}`}
|
||
|
|
onClick={() => handleScreenSelect(screen)}
|
||
|
|
onDoubleClick={() => handleDesignScreen(screen)}
|
||
|
|
>
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<div className="font-medium text-sm truncate">{screen.screenName}</div>
|
||
|
|
<div className="text-xs text-muted-foreground truncate">
|
||
|
|
{screen.screenCode} {screen.tableName && `| ${screen.tableName}`}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center gap-1 ml-2 shrink-0">
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="sm"
|
||
|
|
onClick={(e) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
handlePreviewScreen(screen);
|
||
|
|
}}
|
||
|
|
title="POP 미리보기"
|
||
|
|
>
|
||
|
|
<Eye className="h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="sm"
|
||
|
|
onClick={(e) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
handleDesignScreen(screen);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
설계
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 오른쪽: 관계 시각화 (React Flow) */}
|
||
|
|
<div className="flex-1 overflow-hidden">
|
||
|
|
<ScreenRelationFlow
|
||
|
|
screen={selectedScreen}
|
||
|
|
selectedGroup={selectedGroup}
|
||
|
|
initialFocusedScreenId={focusedScreenIdInGroup}
|
||
|
|
isPop={true}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
// 테이블 뷰 - POP 화면만 표시
|
||
|
|
<div className="flex-1 overflow-auto p-6">
|
||
|
|
<div className="rounded-lg border">
|
||
|
|
<table className="w-full">
|
||
|
|
<thead className="bg-muted/50">
|
||
|
|
<tr>
|
||
|
|
<th className="text-left p-3 font-medium text-sm">화면명</th>
|
||
|
|
<th className="text-left p-3 font-medium text-sm">화면코드</th>
|
||
|
|
<th className="text-left p-3 font-medium text-sm">테이블명</th>
|
||
|
|
<th className="text-left p-3 font-medium text-sm">생성일</th>
|
||
|
|
<th className="text-right p-3 font-medium text-sm">작업</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
{filteredScreens.map((screen) => (
|
||
|
|
<tr
|
||
|
|
key={screen.screenId}
|
||
|
|
className={`border-t cursor-pointer transition-colors ${
|
||
|
|
selectedScreen?.screenId === screen.screenId
|
||
|
|
? "bg-primary/5"
|
||
|
|
: "hover:bg-muted/50"
|
||
|
|
}`}
|
||
|
|
onClick={() => handleScreenSelect(screen)}
|
||
|
|
>
|
||
|
|
<td className="p-3 text-sm font-medium">{screen.screenName}</td>
|
||
|
|
<td className="p-3 text-sm text-muted-foreground">{screen.screenCode}</td>
|
||
|
|
<td className="p-3 text-sm text-muted-foreground">{screen.tableName || "-"}</td>
|
||
|
|
<td className="p-3 text-sm text-muted-foreground">
|
||
|
|
{screen.createdDate ? new Date(screen.createdDate).toLocaleDateString("ko-KR") : "-"}
|
||
|
|
</td>
|
||
|
|
<td className="p-3 text-right">
|
||
|
|
<div className="flex items-center justify-end gap-2">
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="sm"
|
||
|
|
onClick={(e) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
handlePreviewScreen(screen);
|
||
|
|
}}
|
||
|
|
title="POP 미리보기"
|
||
|
|
>
|
||
|
|
<Eye className="h-4 w-4 mr-1" />
|
||
|
|
보기
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
onClick={(e) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
handleDesignScreen(screen);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
설계
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
))}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* 화면 생성 모달 */}
|
||
|
|
<CreateScreenModal
|
||
|
|
open={isCreateOpen}
|
||
|
|
onOpenChange={(open) => {
|
||
|
|
setIsCreateOpen(open);
|
||
|
|
if (!open) loadScreens();
|
||
|
|
}}
|
||
|
|
onCreated={() => {
|
||
|
|
setIsCreateOpen(false);
|
||
|
|
loadScreens();
|
||
|
|
}}
|
||
|
|
isPop={true}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Scroll to Top 버튼 */}
|
||
|
|
<ScrollToTop />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|