- Replaced existing toast error messages with the new `showErrorToast` utility across multiple components, improving consistency in error reporting. - Updated error messages to provide more specific guidance for users, enhancing the overall user experience during error scenarios. - Ensured that all relevant error handling in batch management, external call configurations, cascading management, and screen management components now utilizes the new utility for better maintainability.
444 lines
16 KiB
TypeScript
444 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 { showErrorToast } from "@/lib/utils/toastUtils";
|
|
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);
|
|
showErrorToast("POP 화면 설정 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." });
|
|
} 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>
|
|
);
|
|
}
|