ERP-node/frontend/components/admin/dashboard/DashboardSaveModal.tsx

337 lines
12 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import {
ResizableDialog,
ResizableDialogContent,
ResizableDialogHeader,
ResizableDialogTitle,
ResizableDialogDescription,
ResizableDialogFooter,
} from "@/components/ui/resizable-dialog";
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 { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
SelectGroup,
SelectLabel,
} from "@/components/ui/select";
import { Loader2, Save } from "lucide-react";
import { menuApi } from "@/lib/api/menu";
interface MenuItem {
id: string;
name: string;
url?: string;
parent_id?: string;
children?: MenuItem[];
}
interface DashboardSaveModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (data: {
title: string;
description: string;
assignToMenu: boolean;
menuType?: "admin" | "user";
menuId?: string;
}) => Promise<void>;
initialTitle?: string;
initialDescription?: string;
isEditing?: boolean;
}
export function DashboardSaveModal({
isOpen,
onClose,
onSave,
initialTitle = "",
initialDescription = "",
isEditing = false,
}: DashboardSaveModalProps) {
const [title, setTitle] = useState(initialTitle);
const [description, setDescription] = useState(initialDescription);
const [assignToMenu, setAssignToMenu] = useState(false);
const [menuType, setMenuType] = useState<"admin" | "user">("admin");
const [selectedMenuId, setSelectedMenuId] = useState<string>("");
const [adminMenus, setAdminMenus] = useState<MenuItem[]>([]);
const [userMenus, setUserMenus] = useState<MenuItem[]>([]);
const [loading, setLoading] = useState(false);
const [loadingMenus, setLoadingMenus] = useState(false);
useEffect(() => {
if (isOpen) {
setTitle(initialTitle);
setDescription(initialDescription);
setAssignToMenu(false);
setMenuType("admin");
setSelectedMenuId("");
loadMenus();
}
}, [isOpen, initialTitle, initialDescription]);
const loadMenus = async () => {
setLoadingMenus(true);
try {
const [adminData, userData] = await Promise.all([menuApi.getAdminMenus(), menuApi.getUserMenus()]);
// API 응답이 배열인지 확인하고 처리
const adminMenuList = Array.isArray(adminData) ? adminData : adminData?.data || [];
const userMenuList = Array.isArray(userData) ? userData : userData?.data || [];
setAdminMenus(adminMenuList);
setUserMenus(userMenuList);
} catch (error) {
// console.error("메뉴 목록 로드 실패:", error);
setAdminMenus([]);
setUserMenus([]);
} finally {
setLoadingMenus(false);
}
};
const flattenMenus = (
menus: MenuItem[],
prefix = "",
parentPath = "",
): { id: string; label: string; uniqueKey: string }[] => {
if (!Array.isArray(menus)) {
return [];
}
const result: { id: string; label: string; uniqueKey: string }[] = [];
menus.forEach((menu, index) => {
// 메뉴 ID 추출 (objid 또는 id)
const menuId = (menu as any).objid || menu.id || "";
const uniqueKey = `${parentPath}-${menuId}-${index}`;
// 메뉴 이름 추출
const menuName =
menu.name ||
(menu as any).menu_name_kor ||
(menu as any).MENU_NAME_KOR ||
(menu as any).menuNameKor ||
(menu as any).title ||
"이름없음";
// lev 필드로 레벨 확인 (lev > 1인 메뉴만 추가)
const menuLevel = (menu as any).lev || 0;
if (menuLevel > 1) {
result.push({
id: menuId,
label: prefix + menuName,
uniqueKey,
});
}
// 하위 메뉴가 있으면 재귀 호출
if (menu.children && Array.isArray(menu.children) && menu.children.length > 0) {
result.push(...flattenMenus(menu.children, prefix + menuName + " > ", uniqueKey));
}
});
return result;
};
const handleSave = async () => {
if (!title.trim()) {
alert("대시보드 이름을 입력해주세요.");
return;
}
if (assignToMenu && !selectedMenuId) {
alert("메뉴를 선택해주세요.");
return;
}
setLoading(true);
try {
await onSave({
title: title.trim(),
description: description.trim(),
assignToMenu,
menuType: assignToMenu ? menuType : undefined,
menuId: assignToMenu ? selectedMenuId : undefined,
});
onClose();
} catch (error) {
// console.error("저장 실패:", error);
} finally {
setLoading(false);
}
};
const currentMenus = menuType === "admin" ? adminMenus : userMenus;
const flatMenus = flattenMenus(currentMenus);
return (
<ResizableDialog open={isOpen} onOpenChange={onClose}>
<ResizableDialogContent className="max-h-[90vh] max-w-2xl overflow-y-auto">
<ResizableDialogHeader>
<ResizableDialogTitle>{isEditing ? "대시보드 수정" : "대시보드 저장"}</ResizableDialogTitle>
</ResizableDialogHeader>
<div className="space-y-6 py-4">
{/* 대시보드 이름 */}
<div className="space-y-2">
<Label htmlFor="title">
<span className="text-destructive">*</span>
</Label>
<Input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
onKeyDown={(e) => {
// 모든 키보드 이벤트를 input 필드 내부에서만 처리
e.stopPropagation();
}}
placeholder="예: 생산 현황 대시보드"
className="w-full"
/>
</div>
{/* 대시보드 설명 */}
<div className="space-y-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
onKeyDown={(e) => {
// 모든 키보드 이벤트를 textarea 내부에서만 처리
e.stopPropagation();
}}
placeholder="대시보드에 대한 간단한 설명을 입력하세요"
rows={3}
className="w-full resize-none"
/>
</div>
{/* 구분선 */}
<div className="border-t pt-4">
<h3 className="mb-3 text-sm font-semibold"> </h3>
{/* 메뉴 할당 여부 */}
<div className="space-y-4">
<RadioGroup
value={assignToMenu ? "yes" : "no"}
onValueChange={(value) => setAssignToMenu(value === "yes")}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="no" id="assign-no" />
<Label htmlFor="assign-no" className="cursor-pointer">
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="yes" id="assign-yes" />
<Label htmlFor="assign-yes" className="cursor-pointer">
</Label>
</div>
</RadioGroup>
{/* 메뉴 할당 옵션 */}
{assignToMenu && (
<div className="ml-6 space-y-4 border-l-2 border-border pl-4">
{/* 메뉴 타입 선택 */}
<div className="space-y-2">
<Label> </Label>
<RadioGroup value={menuType} onValueChange={(value) => setMenuType(value as "admin" | "user")}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="admin" id="menu-admin" />
<Label htmlFor="menu-admin" className="cursor-pointer">
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="user" id="menu-user" />
<Label htmlFor="menu-user" className="cursor-pointer">
</Label>
</div>
</RadioGroup>
</div>
{/* 메뉴 선택 */}
<div className="space-y-2">
<Label> </Label>
{loadingMenus ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
<span className="ml-2 text-sm text-muted-foreground"> ...</span>
</div>
) : (
<div className="space-y-2">
<Select value={selectedMenuId} onValueChange={setSelectedMenuId}>
<SelectTrigger className="w-full">
<SelectValue placeholder="메뉴를 선택하세요" />
</SelectTrigger>
<SelectContent className="z-[99999]">
<SelectGroup>
<SelectLabel>{menuType === "admin" ? "관리자 메뉴" : "사용자 메뉴"}</SelectLabel>
{flatMenus.length === 0 ? (
<div className="px-2 py-3 text-sm text-muted-foreground"> .</div>
) : (
flatMenus.map((menu) => (
<SelectItem key={menu.uniqueKey} value={menu.id}>
{menu.label}
</SelectItem>
))
)}
</SelectGroup>
</SelectContent>
</Select>
{selectedMenuId && (
<div className="rounded-md bg-muted p-2 text-sm text-foreground">
:{" "}
<span className="font-medium">{flatMenus.find((m) => m.id === selectedMenuId)?.label}</span>
</div>
)}
</div>
)}
{assignToMenu && selectedMenuId && (
<p className="mt-1 text-xs text-muted-foreground">
URL이 .
{menuType === "admin" && " (관리자 모드 파라미터 포함)"}
</p>
)}
</div>
</div>
)}
</div>
</div>
</div>
<ResizableDialogFooter>
<Button variant="outline" onClick={onClose} disabled={loading}>
</Button>
<Button onClick={handleSave} disabled={loading}>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
</>
)}
</Button>
</ResizableDialogFooter>
</ResizableDialogContent>
</ResizableDialog>
);
}