ERP-node/frontend/app/(main)/admin/screenMng/popScreenMngList/page.tsx

407 lines
16 KiB
TypeScript
Raw Normal View History

"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>
);
}