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

391 lines
14 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,
Smartphone,
Eye,
Settings,
LayoutGrid,
GitBranch,
} from "lucide-react";
import { PopDesigner } from "@/components/pop/designer";
import { ScrollToTop } from "@/components/common/ScrollToTop";
import { ScreenDefinition } from "@/types/screen";
import { screenApi } from "@/lib/api/screen";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import CreateScreenModal from "@/components/screen/CreateScreenModal";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import {
PopCategoryTree,
PopScreenPreview,
PopScreenFlowView,
PopScreenSettingModal,
} from "@/components/pop/management";
import { PopScreenGroup } from "@/lib/api/popScreenGroup";
// ============================================================
// 타입 정의
// ============================================================
type Step = "list" | "design";
type DevicePreview = "mobile" | "tablet";
type RightPanelView = "preview" | "flow";
// ============================================================
// 메인 컴포넌트
// ============================================================
export default function PopScreenManagementPage() {
const searchParams = useSearchParams();
// 단계 및 화면 상태
const [currentStep, setCurrentStep] = useState<Step>("list");
const [selectedScreen, setSelectedScreen] = useState<ScreenDefinition | null>(null);
const [selectedGroup, setSelectedGroup] = useState<PopScreenGroup | null>(null);
const [stepHistory, setStepHistory] = useState<Step[]>(["list"]);
// 화면 데이터
const [screens, setScreens] = useState<ScreenDefinition[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
// POP 레이아웃 존재 화면 ID
const [popLayoutScreenIds, setPopLayoutScreenIds] = useState<Set<number>>(new Set());
// UI 상태
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [isSettingModalOpen, setIsSettingModalOpen] = useState(false);
const [devicePreview, setDevicePreview] = useState<DevicePreview>("tablet");
const [rightPanelView, setRightPanelView] = useState<RightPanelView>("preview");
// ============================================================
// 데이터 로드
// ============================================================
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 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 handleGroupSelect = (group: PopScreenGroup | null) => {
setSelectedGroup(group);
// 그룹 선택 시 화면 선택 해제하지 않음 (미리보기 유지)
};
// 화면 디자인 모드 진입
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");
};
// 화면 설정 모달 열기
const handleOpenSettings = () => {
if (selectedScreen) {
setIsSettingModalOpen(true);
}
};
// ============================================================
// 필터링된 데이터
// ============================================================
// POP 레이아웃이 있는 화면만 필터링
const popScreens = screens.filter((screen) => popLayoutScreenIds.has(screen.screenId));
// 검색어 필터링
const filteredScreens = popScreens.filter((screen) => {
if (!searchTerm) return true;
return (
screen.screenName.toLowerCase().includes(searchTerm.toLowerCase()) ||
screen.screenCode.toLowerCase().includes(searchTerm.toLowerCase())
);
});
const popScreenCount = popLayoutScreenIds.size;
// ============================================================
// 디자인 모드
// ============================================================
const isDesignMode = currentStep === "design";
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">
<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>
) : (
<div className="flex-1 overflow-hidden flex">
{/* 왼쪽: 카테고리 트리 + 화면 목록 */}
<div className="w-[320px] min-w-[280px] max-w-[400px] 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>
<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>
{/* 카테고리 트리 */}
<PopCategoryTree
screens={filteredScreens}
selectedScreen={selectedScreen}
onScreenSelect={handleScreenSelect}
onScreenDesign={handleDesignScreen}
onGroupSelect={handleGroupSelect}
searchTerm={searchTerm}
/>
</div>
{/* 오른쪽: 미리보기 / 화면 흐름 */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* 오른쪽 패널 헤더 */}
<div className="shrink-0 px-4 py-2 border-b bg-background flex items-center justify-between">
<Tabs value={rightPanelView} onValueChange={(v) => setRightPanelView(v as RightPanelView)}>
<TabsList className="h-8">
<TabsTrigger value="preview" className="h-7 px-3 text-xs gap-1.5">
<LayoutGrid className="h-3.5 w-3.5" />
</TabsTrigger>
<TabsTrigger value="flow" className="h-7 px-3 text-xs gap-1.5">
<GitBranch className="h-3.5 w-3.5" />
</TabsTrigger>
</TabsList>
</Tabs>
{selectedScreen && (
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs"
onClick={() => handlePreviewScreen(selectedScreen)}
>
<Eye className="h-3.5 w-3.5 mr-1" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs"
onClick={handleOpenSettings}
>
<Settings className="h-3.5 w-3.5 mr-1" />
</Button>
<Button
variant="default"
size="sm"
className="h-7 px-3 text-xs"
onClick={() => handleDesignScreen(selectedScreen)}
>
</Button>
</div>
)}
</div>
{/* 오른쪽 패널 콘텐츠 */}
<div className="flex-1 overflow-hidden">
{rightPanelView === "preview" ? (
<PopScreenPreview screen={selectedScreen} className="h-full" />
) : (
<PopScreenFlowView screen={selectedScreen} className="h-full" />
)}
</div>
</div>
</div>
)}
{/* 화면 생성 모달 */}
<CreateScreenModal
open={isCreateOpen}
onOpenChange={(open) => {
setIsCreateOpen(open);
if (!open) loadScreens();
}}
onCreated={() => {
setIsCreateOpen(false);
loadScreens();
}}
isPop={true}
/>
{/* 화면 설정 모달 */}
<PopScreenSettingModal
open={isSettingModalOpen}
onOpenChange={setIsSettingModalOpen}
screen={selectedScreen}
onSave={(updatedFields) => {
if (selectedScreen) {
setSelectedScreen({ ...selectedScreen, ...updatedFields });
}
loadScreens();
}}
/>
{/* Scroll to Top 버튼 */}
<ScrollToTop />
</div>
);
}