대시보드에 화면 할당 (중간)

This commit is contained in:
dohyeons 2025-10-16 14:53:06 +09:00
parent 71aaef7acb
commit 062efac47f
2 changed files with 299 additions and 9 deletions

View File

@ -6,6 +6,7 @@ import { DashboardCanvas } from "./DashboardCanvas";
import { DashboardTopMenu } from "./DashboardTopMenu"; import { DashboardTopMenu } from "./DashboardTopMenu";
import { ElementConfigModal } from "./ElementConfigModal"; import { ElementConfigModal } from "./ElementConfigModal";
import { ListWidgetConfigModal } from "./widgets/ListWidgetConfigModal"; import { ListWidgetConfigModal } from "./widgets/ListWidgetConfigModal";
import { MenuAssignmentModal } from "./MenuAssignmentModal";
import { DashboardElement, ElementType, ElementSubtype } from "./types"; import { DashboardElement, ElementType, ElementSubtype } from "./types";
import { GRID_CONFIG, snapToGrid, snapSizeToGrid, calculateCellSize } from "./gridUtils"; import { GRID_CONFIG, snapToGrid, snapSizeToGrid, calculateCellSize } from "./gridUtils";
import { Resolution, RESOLUTIONS, detectScreenResolution } from "./ResolutionSelector"; import { Resolution, RESOLUTIONS, detectScreenResolution } from "./ResolutionSelector";
@ -33,6 +34,10 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
const [canvasBackgroundColor, setCanvasBackgroundColor] = useState<string>("#f9fafb"); const [canvasBackgroundColor, setCanvasBackgroundColor] = useState<string>("#f9fafb");
const canvasRef = useRef<HTMLDivElement>(null); const canvasRef = useRef<HTMLDivElement>(null);
// 메뉴 할당 모달 상태
const [menuAssignmentModalOpen, setMenuAssignmentModalOpen] = useState(false);
const [savedDashboardInfo, setSavedDashboardInfo] = useState<{ id: string; title: string } | null>(null);
// 화면 해상도 자동 감지 // 화면 해상도 자동 감지
const [screenResolution] = useState<Resolution>(() => detectScreenResolution()); const [screenResolution] = useState<Resolution>(() => detectScreenResolution());
const [resolution, setResolution] = useState<Resolution>(screenResolution); const [resolution, setResolution] = useState<Resolution>(screenResolution);
@ -348,10 +353,12 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
console.log("✅ 저장된 대시보드:", savedDashboard); console.log("✅ 저장된 대시보드:", savedDashboard);
console.log("✅ 저장된 settings:", (savedDashboard as any).settings); console.log("✅ 저장된 settings:", (savedDashboard as any).settings);
alert(`대시보드 "${savedDashboard.title}"이 업데이트되었습니다!`); // 메뉴 할당 모달 띄우기 (기존 대시보드 업데이트 시에도)
setSavedDashboardInfo({
// Next.js 라우터로 뷰어 페이지 이동 id: savedDashboard.id,
router.push(`/dashboard/${savedDashboard.id}`); title: savedDashboard.title,
});
setMenuAssignmentModalOpen(true);
} else { } else {
// 새 대시보드 생성 // 새 대시보드 생성
const title = prompt("대시보드 제목을 입력하세요:", "새 대시보드"); const title = prompt("대시보드 제목을 입력하세요:", "새 대시보드");
@ -372,11 +379,16 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
savedDashboard = await dashboardApi.createDashboard(dashboardData); savedDashboard = await dashboardApi.createDashboard(dashboardData);
const viewDashboard = confirm(`대시보드 "${title}"이 저장되었습니다!\n\n지금 확인해보시겠습니까?`); // 대시보드 ID 설정 (다음 저장 시 업데이트 모드로 전환)
if (viewDashboard) { setDashboardId(savedDashboard.id);
// Next.js 라우터로 뷰어 페이지 이동 setDashboardTitle(savedDashboard.title);
router.push(`/dashboard/${savedDashboard.id}`);
} // 메뉴 할당 모달 띄우기
setSavedDashboardInfo({
id: savedDashboard.id,
title: savedDashboard.title,
});
setMenuAssignmentModalOpen(true);
} }
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류"; const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류";
@ -384,6 +396,58 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
} }
}, [elements, dashboardId, router, resolution, canvasBackgroundColor]); }, [elements, dashboardId, router, resolution, canvasBackgroundColor]);
// 메뉴 할당 처리
const handleMenuAssignment = useCallback(
async (assignToMenu: boolean, menuId?: string, menuType?: "0" | "1") => {
if (!savedDashboardInfo) return;
try {
if (assignToMenu && menuId) {
// 메뉴 API를 통해 대시보드 URL 할당
const { menuApi } = await import("@/lib/api/menu");
// 대시보드 URL 생성 (관리자 메뉴면 mode=admin 추가)
let dashboardUrl = `/dashboard/${savedDashboardInfo.id}`;
if (menuType === "0") {
dashboardUrl += "?mode=admin";
}
// 메뉴 정보 가져오기
const menuResponse = await menuApi.getMenuInfo(menuId);
if (menuResponse.success && menuResponse.data) {
const menu = menuResponse.data;
// 메뉴 URL 업데이트
await menuApi.updateMenu(menuId, {
menuUrl: dashboardUrl,
parentObjId: menu.parent_obj_id || menu.PARENT_OBJ_ID || "0",
menuNameKor: menu.menu_name_kor || menu.MENU_NAME_KOR || "",
menuDesc: menu.menu_desc || menu.MENU_DESC || "",
seq: menu.seq || menu.SEQ || 1,
menuType: menu.menu_type || menu.MENU_TYPE || "1",
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}"에 대시보드가 할당되었습니다!`);
}
}
// 모달 닫기
setMenuAssignmentModalOpen(false);
setSavedDashboardInfo(null);
// 대시보드 뷰어로 이동
router.push(`/dashboard/${savedDashboardInfo.id}`);
} catch (error) {
console.error("메뉴 할당 오류:", error);
alert("메뉴 할당 중 오류가 발생했습니다.");
}
},
[savedDashboardInfo, router],
);
// 로딩 중이면 로딩 화면 표시 // 로딩 중이면 로딩 화면 표시
if (isLoading) { if (isLoading) {
return ( return (
@ -458,6 +522,22 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
)} )}
</> </>
)} )}
{/* 메뉴 할당 모달 */}
{savedDashboardInfo && (
<MenuAssignmentModal
isOpen={menuAssignmentModalOpen}
onClose={() => {
setMenuAssignmentModalOpen(false);
setSavedDashboardInfo(null);
// 모달을 그냥 닫으면 대시보드 뷰어로 이동
router.push(`/dashboard/${savedDashboardInfo.id}`);
}}
onConfirm={handleMenuAssignment}
dashboardId={savedDashboardInfo.id}
dashboardTitle={savedDashboardInfo.title}
/>
)}
</div> </div>
); );
} }

View File

@ -0,0 +1,210 @@
"use client";
import React, { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { toast } from "sonner";
import { menuApi, MenuItem } from "@/lib/api/menu";
import { Loader2 } from "lucide-react";
interface MenuAssignmentModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: (assignToMenu: boolean, menuId?: string, menuType?: "0" | "1") => void;
dashboardId: string;
dashboardTitle: string;
}
export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
isOpen,
onClose,
onConfirm,
dashboardId,
dashboardTitle,
}) => {
const [assignToMenu, setAssignToMenu] = useState<boolean>(false);
const [selectedMenuId, setSelectedMenuId] = useState<string>("");
const [selectedMenuType, setSelectedMenuType] = useState<"0" | "1">("0");
const [adminMenus, setAdminMenus] = useState<MenuItem[]>([]);
const [userMenus, setUserMenus] = useState<MenuItem[]>([]);
const [loading, setLoading] = useState(false);
// 메뉴 목록 로드
useEffect(() => {
if (isOpen && assignToMenu) {
loadMenus();
}
}, [isOpen, assignToMenu]);
const loadMenus = async () => {
try {
setLoading(true);
const [adminResponse, userResponse] = await Promise.all([
menuApi.getAdminMenus(), // 관리자 메뉴
menuApi.getUserMenus(), // 사용자 메뉴
]);
if (adminResponse.success) {
setAdminMenus(adminResponse.data || []);
}
if (userResponse.success) {
setUserMenus(userResponse.data || []);
}
} catch (error) {
console.error("메뉴 목록 로드 실패:", error);
toast.error("메뉴 목록을 불러오는데 실패했습니다.");
} finally {
setLoading(false);
}
};
// 메뉴 트리를 평탄화하여 Select 옵션으로 변환
const flattenMenus = (menus: MenuItem[], level: number = 0): Array<{ id: string; name: string; level: number }> => {
const result: Array<{ id: string; name: string; level: number }> = [];
menus.forEach((menu) => {
const menuId = menu.objid || menu.OBJID || "";
const menuName = menu.menu_name_kor || menu.MENU_NAME_KOR || "";
const parentId = menu.parent_obj_id || menu.PARENT_OBJ_ID || "0";
// 메뉴 이름이 있고, 최상위가 아닌 경우에만 추가
if (menuName && parentId !== "0") {
result.push({
id: menuId,
name: " ".repeat(level) + menuName,
level,
});
// 하위 메뉴가 있으면 재귀 호출
const children = menus.filter((m) => (m.parent_obj_id || m.PARENT_OBJ_ID) === menuId);
if (children.length > 0) {
result.push(...flattenMenus(children, level + 1));
}
}
});
return result;
};
const currentMenus = selectedMenuType === "0" ? adminMenus : userMenus;
const flatMenus = flattenMenus(currentMenus);
const handleConfirm = () => {
if (assignToMenu && !selectedMenuId) {
toast.error("메뉴를 선택해주세요.");
return;
}
onConfirm(assignToMenu, selectedMenuId, selectedMenuType);
};
const handleClose = () => {
setAssignToMenu(false);
setSelectedMenuId("");
setSelectedMenuType("0");
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription>'{dashboardTitle}' .</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-3">
<Label> ?</Label>
<RadioGroup
value={assignToMenu ? "yes" : "no"}
onValueChange={(value) => setAssignToMenu(value === "yes")}
className="flex space-x-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="yes" id="yes" />
<Label htmlFor="yes" className="cursor-pointer font-normal">
,
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="no" id="no" />
<Label htmlFor="no" className="cursor-pointer font-normal">
,
</Label>
</div>
</RadioGroup>
</div>
{assignToMenu && (
<>
<div className="space-y-2">
<Label> </Label>
<RadioGroup
value={selectedMenuType}
onValueChange={(value) => {
setSelectedMenuType(value as "0" | "1");
setSelectedMenuId(""); // 메뉴 타입 변경 시 선택 초기화
}}
className="flex space-x-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="0" id="admin" />
<Label htmlFor="admin" className="cursor-pointer font-normal">
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="1" id="user" />
<Label htmlFor="user" className="cursor-pointer font-normal">
</Label>
</div>
</RadioGroup>
</div>
<div className="space-y-2">
<Label> </Label>
{loading ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
</div>
) : (
<Select value={selectedMenuId} onValueChange={setSelectedMenuId}>
<SelectTrigger>
<SelectValue placeholder="메뉴를 선택하세요" />
</SelectTrigger>
<SelectContent className="max-h-[300px]">
{flatMenus.map((menu) => (
<SelectItem key={menu.id} value={menu.id}>
{menu.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
</>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={handleClose}>
</Button>
<Button onClick={handleConfirm}>{assignToMenu ? "메뉴에 할당하고 완료" : "완료"}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};