From 5093d336c0f54b7b6f68b33f1d60d30d129166a9 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Thu, 16 Oct 2025 16:43:04 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=EC=97=90?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=ED=95=A0=EB=8B=B9=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/DashboardDesigner.tsx | 254 +++++++------- .../admin/dashboard/DashboardSaveModal.tsx | 321 ++++++++++++++++++ .../admin/dashboard/ElementConfigModal.tsx | 13 +- 3 files changed, 457 insertions(+), 131 deletions(-) create mode 100644 frontend/components/admin/dashboard/DashboardSaveModal.tsx diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx index 95eea2f2..7f19bbc4 100644 --- a/frontend/components/admin/dashboard/DashboardDesigner.tsx +++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx @@ -6,10 +6,14 @@ import { DashboardCanvas } from "./DashboardCanvas"; import { DashboardTopMenu } from "./DashboardTopMenu"; import { ElementConfigModal } from "./ElementConfigModal"; import { ListWidgetConfigModal } from "./widgets/ListWidgetConfigModal"; -import { MenuAssignmentModal } from "./MenuAssignmentModal"; +import { DashboardSaveModal } from "./DashboardSaveModal"; import { DashboardElement, ElementType, ElementSubtype } from "./types"; import { GRID_CONFIG, snapToGrid, snapSizeToGrid, calculateCellSize } from "./gridUtils"; import { Resolution, RESOLUTIONS, detectScreenResolution } from "./ResolutionSelector"; +import { useMenu } from "@/contexts/MenuContext"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { CheckCircle2 } from "lucide-react"; interface DashboardDesignerProps { dashboardId?: string; @@ -24,6 +28,7 @@ interface DashboardDesignerProps { */ export default function DashboardDesigner({ dashboardId: initialDashboardId }: DashboardDesignerProps = {}) { const router = useRouter(); + const { refreshMenus } = useMenu(); const [elements, setElements] = useState([]); const [selectedElement, setSelectedElement] = useState(null); const [elementCounter, setElementCounter] = useState(0); @@ -34,9 +39,10 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D const [canvasBackgroundColor, setCanvasBackgroundColor] = useState("#f9fafb"); const canvasRef = useRef(null); - // 메뉴 할당 모달 상태 - const [menuAssignmentModalOpen, setMenuAssignmentModalOpen] = useState(false); - const [savedDashboardInfo, setSavedDashboardInfo] = useState<{ id: string; title: string } | null>(null); + // 저장 모달 상태 + const [saveModalOpen, setSaveModalOpen] = useState(false); + const [dashboardDescription, setDashboardDescription] = useState(""); + const [successModalOpen, setSuccessModalOpen] = useState(false); // 화면 해상도 자동 감지 const [screenResolution] = useState(() => detectScreenResolution()); @@ -308,117 +314,93 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D ); // 레이아웃 저장 - const saveLayout = useCallback(async () => { + const saveLayout = useCallback(() => { if (elements.length === 0) { alert("저장할 요소가 없습니다. 차트나 위젯을 추가해주세요."); return; } - try { - // 실제 API 호출 - const { dashboardApi } = await import("@/lib/api/dashboard"); - - const elementsData = elements.map((el) => ({ - id: el.id, - type: el.type, - subtype: el.subtype, - position: el.position, - size: el.size, - title: el.title, - content: el.content, - dataSource: el.dataSource, - chartConfig: el.chartConfig, - })); - - let savedDashboard; - - if (dashboardId) { - // 기존 대시보드 업데이트 - console.log("💾 저장 시작 - 현재 resolution 상태:", resolution); - console.log("💾 저장 시작 - 현재 배경색 상태:", canvasBackgroundColor); - - const updateData = { - elements: elementsData, - settings: { - resolution, - backgroundColor: canvasBackgroundColor, - }, - }; - - console.log("💾 저장할 데이터:", updateData); - console.log("💾 저장할 settings:", updateData.settings); - - savedDashboard = await dashboardApi.updateDashboard(dashboardId, updateData); - - console.log("✅ 저장된 대시보드:", savedDashboard); - console.log("✅ 저장된 settings:", (savedDashboard as any).settings); - - // 메뉴 할당 모달 띄우기 (기존 대시보드 업데이트 시에도) - setSavedDashboardInfo({ - id: savedDashboard.id, - title: savedDashboard.title, - }); - setMenuAssignmentModalOpen(true); - } else { - // 새 대시보드 생성 - const title = prompt("대시보드 제목을 입력하세요:", "새 대시보드"); - if (!title) return; - - const description = prompt("대시보드 설명을 입력하세요 (선택사항):", ""); - - const dashboardData = { - title, - description: description || undefined, - isPublic: false, - elements: elementsData, - settings: { - resolution, - backgroundColor: canvasBackgroundColor, - }, - }; - - savedDashboard = await dashboardApi.createDashboard(dashboardData); - - // 대시보드 ID 설정 (다음 저장 시 업데이트 모드로 전환) - setDashboardId(savedDashboard.id); - setDashboardTitle(savedDashboard.title); - - // 메뉴 할당 모달 띄우기 - setSavedDashboardInfo({ - id: savedDashboard.id, - title: savedDashboard.title, - }); - setMenuAssignmentModalOpen(true); - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류"; - alert(`대시보드 저장 중 오류가 발생했습니다.\n\n오류: ${errorMessage}\n\n관리자에게 문의하세요.`); - } - }, [elements, dashboardId, router, resolution, canvasBackgroundColor]); - - // 메뉴 할당 처리 - const handleMenuAssignment = useCallback( - async (assignToMenu: boolean, menuId?: string, menuType?: "0" | "1") => { - if (!savedDashboardInfo) return; + // 저장 모달 열기 + setSaveModalOpen(true); + }, [elements]); + // 저장 처리 + const handleSave = useCallback( + async (data: { + title: string; + description: string; + assignToMenu: boolean; + menuType?: "admin" | "user"; + menuId?: string; + }) => { try { - if (assignToMenu && menuId) { - // 메뉴 API를 통해 대시보드 URL 할당 + const { dashboardApi } = await import("@/lib/api/dashboard"); + + const elementsData = elements.map((el) => ({ + id: el.id, + type: el.type, + subtype: el.subtype, + position: el.position, + size: el.size, + title: el.title, + content: el.content, + dataSource: el.dataSource, + chartConfig: el.chartConfig, + listConfig: el.listConfig, + })); + + let savedDashboard; + + if (dashboardId) { + // 기존 대시보드 업데이트 + const updateData = { + title: data.title, + description: data.description || undefined, + elements: elementsData, + settings: { + resolution, + backgroundColor: canvasBackgroundColor, + }, + }; + + savedDashboard = await dashboardApi.updateDashboard(dashboardId, updateData); + } else { + // 새 대시보드 생성 + const dashboardData = { + title: data.title, + description: data.description || undefined, + isPublic: false, + elements: elementsData, + settings: { + resolution, + backgroundColor: canvasBackgroundColor, + }, + }; + + savedDashboard = await dashboardApi.createDashboard(dashboardData); + setDashboardId(savedDashboard.id); + } + + setDashboardTitle(savedDashboard.title); + setDashboardDescription(data.description); + + // 메뉴 할당 처리 + if (data.assignToMenu && data.menuId) { const { menuApi } = await import("@/lib/api/menu"); // 대시보드 URL 생성 (관리자 메뉴면 mode=admin 추가) - let dashboardUrl = `/dashboard/${savedDashboardInfo.id}`; - if (menuType === "0") { + let dashboardUrl = `/dashboard/${savedDashboard.id}`; + if (data.menuType === "admin") { dashboardUrl += "?mode=admin"; } // 메뉴 정보 가져오기 - const menuResponse = await menuApi.getMenuInfo(menuId); + const menuResponse = await menuApi.getMenuInfo(data.menuId); + if (menuResponse.success && menuResponse.data) { const menu = menuResponse.data; - // 메뉴 URL 업데이트 - await menuApi.updateMenu(menuId, { + const updateData = { menuUrl: dashboardUrl, parentObjId: menu.parent_obj_id || menu.PARENT_OBJ_ID || "0", menuNameKor: menu.menu_name_kor || menu.MENU_NAME_KOR || "", @@ -428,24 +410,25 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D status: menu.status || menu.STATUS || "active", companyCode: menu.company_code || menu.COMPANY_CODE || "", langKey: menu.lang_key || menu.LANG_KEY || "", - }); + }; - alert(`메뉴 "${menu.menu_name_kor || menu.MENU_NAME_KOR}"에 대시보드가 할당되었습니다!`); + // 메뉴 URL 업데이트 + await menuApi.updateMenu(data.menuId, updateData); + + // 메뉴 목록 새로고침 + await refreshMenus(); } } - // 모달 닫기 - setMenuAssignmentModalOpen(false); - setSavedDashboardInfo(null); - - // 대시보드 뷰어로 이동 - router.push(`/dashboard/${savedDashboardInfo.id}`); + // 성공 모달 표시 + setSuccessModalOpen(true); } catch (error) { - console.error("메뉴 할당 오류:", error); - alert("메뉴 할당 중 오류가 발생했습니다."); + const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류"; + alert(`대시보드 저장 중 오류가 발생했습니다.\n\n오류: ${errorMessage}`); + throw error; } }, - [savedDashboardInfo, router], + [elements, dashboardId, resolution, canvasBackgroundColor, router], ); // 로딩 중이면 로딩 화면 표시 @@ -523,21 +506,44 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D )} - {/* 메뉴 할당 모달 */} - {savedDashboardInfo && ( - { - setMenuAssignmentModalOpen(false); - setSavedDashboardInfo(null); - // 모달을 그냥 닫으면 대시보드 뷰어로 이동 - router.push(`/dashboard/${savedDashboardInfo.id}`); - }} - onConfirm={handleMenuAssignment} - dashboardId={savedDashboardInfo.id} - dashboardTitle={savedDashboardInfo.title} - /> - )} + {/* 저장 모달 */} + setSaveModalOpen(false)} + onSave={handleSave} + initialTitle={dashboardTitle} + initialDescription={dashboardDescription} + isEditing={!!dashboardId} + /> + + {/* 저장 성공 모달 */} + { + setSuccessModalOpen(false); + router.push("/admin/dashboard"); + }} + > + + +
+ +
+ 저장 완료 + 대시보드가 성공적으로 저장되었습니다. +
+
+ +
+
+
); } diff --git a/frontend/components/admin/dashboard/DashboardSaveModal.tsx b/frontend/components/admin/dashboard/DashboardSaveModal.tsx new file mode 100644 index 00000000..34837384 --- /dev/null +++ b/frontend/components/admin/dashboard/DashboardSaveModal.tsx @@ -0,0 +1,321 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/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; + 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(""); + const [adminMenus, setAdminMenus] = useState([]); + const [userMenus, setUserMenus] = useState([]); + 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 ( + + + + {isEditing ? "대시보드 수정" : "대시보드 저장"} + + +
+ {/* 대시보드 이름 */} +
+ + setTitle(e.target.value)} + placeholder="예: 생산 현황 대시보드" + className="w-full" + /> +
+ + {/* 대시보드 설명 */} +
+ +