391 lines
14 KiB
TypeScript
391 lines
14 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,
|
|
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>
|
|
);
|
|
}
|