ERP-node/frontend/components/pop/management/PopScreenSettingModal.tsx

443 lines
16 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
FileText,
Layers,
GitBranch,
Plus,
Trash2,
GripVertical,
Loader2,
Save,
} from "lucide-react";
import { toast } from "sonner";
import { ScreenDefinition } from "@/types/screen";
import { screenApi } from "@/lib/api/screen";
import { PopScreenGroup, getPopScreenGroups } from "@/lib/api/popScreenGroup";
import { PopScreenFlowView } from "./PopScreenFlowView";
// ============================================================
// 타입 정의
// ============================================================
interface PopScreenSettingModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
screen: ScreenDefinition | null;
onSave?: (updatedScreen: Partial<ScreenDefinition>) => void;
}
interface SubScreenItem {
id: string;
name: string;
type: "modal" | "drawer" | "fullscreen";
triggerFrom?: string;
}
// ============================================================
// 메인 컴포넌트
// ============================================================
export function PopScreenSettingModal({
open,
onOpenChange,
screen,
onSave,
}: PopScreenSettingModalProps) {
const [activeTab, setActiveTab] = useState("overview");
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
// 개요 탭 상태
const [screenName, setScreenName] = useState("");
const [screenDescription, setScreenDescription] = useState("");
const [selectedCategoryId, setSelectedCategoryId] = useState<string>("");
const [screenIcon, setScreenIcon] = useState("");
// 하위 화면 탭 상태
const [subScreens, setSubScreens] = useState<SubScreenItem[]>([]);
// 카테고리 목록
const [categories, setCategories] = useState<PopScreenGroup[]>([]);
// 초기 데이터 로드
useEffect(() => {
if (!open || !screen) return;
// 화면 정보 설정
setScreenName(screen.screenName || "");
setScreenDescription(screen.description || "");
setScreenIcon("");
setSelectedCategoryId("");
// 카테고리 목록 로드
loadCategories();
// 레이아웃에서 하위 화면 정보 로드
loadLayoutData();
}, [open, screen]);
const loadCategories = async () => {
try {
const data = await getPopScreenGroups();
setCategories(data.filter((g) => g.hierarchy_path?.startsWith("POP/")));
} catch (error) {
console.error("카테고리 로드 실패:", error);
}
};
const loadLayoutData = async () => {
if (!screen) return;
try {
setLoading(true);
const layout = await screenApi.getLayoutPop(screen.screenId);
if (layout && layout.subScreens) {
setSubScreens(
layout.subScreens.map((sub: any) => ({
id: sub.id || `sub-${Date.now()}`,
name: sub.name || "",
type: sub.type || "modal",
triggerFrom: sub.triggerFrom || "main",
}))
);
} else {
setSubScreens([]);
}
} catch (error) {
console.error("레이아웃 로드 실패:", error);
setSubScreens([]);
} finally {
setLoading(false);
}
};
// 하위 화면 추가
const addSubScreen = () => {
const newSubScreen: SubScreenItem = {
id: `sub-${Date.now()}`,
name: `새 모달 ${subScreens.length + 1}`,
type: "modal",
triggerFrom: "main",
};
setSubScreens([...subScreens, newSubScreen]);
};
// 하위 화면 삭제
const removeSubScreen = (id: string) => {
setSubScreens(subScreens.filter((s) => s.id !== id));
};
// 하위 화면 업데이트
const updateSubScreen = (id: string, field: keyof SubScreenItem, value: string) => {
setSubScreens(
subScreens.map((s) => (s.id === id ? { ...s, [field]: value } : s))
);
};
// 저장
const handleSave = async () => {
if (!screen) return;
try {
setSaving(true);
// 화면 기본 정보 업데이트
const screenUpdate: Partial<ScreenDefinition> = {
screenName,
description: screenDescription,
};
// 레이아웃에 하위 화면 정보 저장
const currentLayout = await screenApi.getLayoutPop(screen.screenId);
const updatedLayout = {
...currentLayout,
version: "pop-1.0",
subScreens: subScreens,
// flow 배열 자동 생성 (메인 → 각 서브)
flow: subScreens.map((sub) => ({
from: sub.triggerFrom || "main",
to: sub.id,
})),
};
await screenApi.saveLayoutPop(screen.screenId, updatedLayout);
toast.success("화면 설정이 저장되었습니다.");
onSave?.(screenUpdate);
onOpenChange(false);
} catch (error) {
console.error("저장 실패:", error);
toast.error("저장에 실패했습니다.");
} finally {
setSaving(false);
}
};
if (!screen) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[95vw] sm:max-w-[800px] max-h-[90vh] flex flex-col p-0">
<DialogHeader className="p-4 pb-0 shrink-0">
<DialogTitle className="text-base sm:text-lg">POP </DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
{screen.screenName} ({screen.screenCode})
</DialogDescription>
</DialogHeader>
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="flex-1 flex flex-col min-h-0"
>
<TabsList className="shrink-0 mx-4 justify-start border-b rounded-none bg-transparent h-auto p-0">
<TabsTrigger
value="overview"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
>
<FileText className="h-4 w-4 mr-2" />
</TabsTrigger>
<TabsTrigger
value="subscreens"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
>
<Layers className="h-4 w-4 mr-2" />
{subScreens.length > 0 && (
<Badge variant="secondary" className="ml-2 text-xs">
{subScreens.length}
</Badge>
)}
</TabsTrigger>
<TabsTrigger
value="flow"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
>
<GitBranch className="h-4 w-4 mr-2" />
</TabsTrigger>
</TabsList>
{/* 개요 탭 */}
<TabsContent value="overview" className="flex-1 m-0 p-4 overflow-auto">
{loading ? (
<div className="flex items-center justify-center h-full">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<div className="space-y-4 max-w-[500px]">
<div>
<Label htmlFor="screenName" className="text-xs sm:text-sm">
*
</Label>
<Input
id="screenName"
value={screenName}
onChange={(e) => setScreenName(e.target.value)}
placeholder="화면 이름"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<div>
<Label htmlFor="category" className="text-xs sm:text-sm">
</Label>
<Select value={selectedCategoryId} onValueChange={setSelectedCategoryId}>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="카테고리 선택" />
</SelectTrigger>
<SelectContent>
{categories.map((cat) => (
<SelectItem key={cat.id} value={String(cat.id)}>
{cat.group_name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="description" className="text-xs sm:text-sm">
</Label>
<Textarea
id="description"
value={screenDescription}
onChange={(e) => setScreenDescription(e.target.value)}
placeholder="화면에 대한 설명"
rows={3}
className="text-xs sm:text-sm resize-none"
/>
</div>
<div>
<Label htmlFor="icon" className="text-xs sm:text-sm">
</Label>
<Input
id="icon"
value={screenIcon}
onChange={(e) => setScreenIcon(e.target.value)}
placeholder="lucide 아이콘 이름 (예: Package)"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
<p className="text-[10px] text-muted-foreground mt-1">
lucide-react .
</p>
</div>
</div>
)}
</TabsContent>
{/* 하위 화면 탭 */}
<TabsContent value="subscreens" className="flex-1 m-0 p-4 overflow-auto">
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
, .
</p>
<Button size="sm" onClick={addSubScreen}>
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
<ScrollArea className="h-[300px]">
{subScreens.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
<Layers className="h-8 w-8 mx-auto mb-3 opacity-50" />
<p className="text-sm"> .</p>
<Button variant="link" className="text-xs" onClick={addSubScreen}>
</Button>
</div>
) : (
<div className="space-y-3">
{subScreens.map((subScreen, index) => (
<div
key={subScreen.id}
className="flex items-start gap-3 p-3 border rounded-lg bg-muted/30"
>
<GripVertical className="h-5 w-5 text-muted-foreground shrink-0 mt-1 cursor-grab" />
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<Input
value={subScreen.name}
onChange={(e) =>
updateSubScreen(subScreen.id, "name", e.target.value)
}
placeholder="화면 이름"
className="h-8 text-xs flex-1"
/>
<Select
value={subScreen.type}
onValueChange={(v) =>
updateSubScreen(subScreen.id, "type", v)
}
>
<SelectTrigger className="h-8 text-xs w-[100px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="modal"></SelectItem>
<SelectItem value="drawer"></SelectItem>
<SelectItem value="fullscreen"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground shrink-0">
:
</span>
<Select
value={subScreen.triggerFrom || "main"}
onValueChange={(v) =>
updateSubScreen(subScreen.id, "triggerFrom", v)
}
>
<SelectTrigger className="h-7 text-xs flex-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="main"> </SelectItem>
{subScreens
.filter((s) => s.id !== subScreen.id)
.map((s) => (
<SelectItem key={s.id} value={s.id}>
{s.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-muted-foreground hover:text-destructive"
onClick={() => removeSubScreen(subScreen.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</ScrollArea>
</div>
</TabsContent>
{/* 화면 흐름 탭 */}
<TabsContent value="flow" className="flex-1 m-0 overflow-hidden">
<PopScreenFlowView screen={screen} className="h-full" />
</TabsContent>
</Tabs>
{/* 푸터 */}
<div className="shrink-0 p-4 border-t flex items-center justify-end gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSave} disabled={saving}>
{saving ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Save className="h-4 w-4 mr-2" />
)}
</Button>
</div>
</DialogContent>
</Dialog>
);
}