대시보드에 화면 할당 (중간)
This commit is contained in:
parent
71aaef7acb
commit
062efac47f
|
|
@ -6,6 +6,7 @@ import { DashboardCanvas } from "./DashboardCanvas";
|
|||
import { DashboardTopMenu } from "./DashboardTopMenu";
|
||||
import { ElementConfigModal } from "./ElementConfigModal";
|
||||
import { ListWidgetConfigModal } from "./widgets/ListWidgetConfigModal";
|
||||
import { MenuAssignmentModal } from "./MenuAssignmentModal";
|
||||
import { DashboardElement, ElementType, ElementSubtype } from "./types";
|
||||
import { GRID_CONFIG, snapToGrid, snapSizeToGrid, calculateCellSize } from "./gridUtils";
|
||||
import { Resolution, RESOLUTIONS, detectScreenResolution } from "./ResolutionSelector";
|
||||
|
|
@ -33,6 +34,10 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
const [canvasBackgroundColor, setCanvasBackgroundColor] = useState<string>("#f9fafb");
|
||||
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 [resolution, setResolution] = useState<Resolution>(screenResolution);
|
||||
|
|
@ -348,10 +353,12 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
console.log("✅ 저장된 대시보드:", savedDashboard);
|
||||
console.log("✅ 저장된 settings:", (savedDashboard as any).settings);
|
||||
|
||||
alert(`대시보드 "${savedDashboard.title}"이 업데이트되었습니다!`);
|
||||
|
||||
// Next.js 라우터로 뷰어 페이지 이동
|
||||
router.push(`/dashboard/${savedDashboard.id}`);
|
||||
// 메뉴 할당 모달 띄우기 (기존 대시보드 업데이트 시에도)
|
||||
setSavedDashboardInfo({
|
||||
id: savedDashboard.id,
|
||||
title: savedDashboard.title,
|
||||
});
|
||||
setMenuAssignmentModalOpen(true);
|
||||
} else {
|
||||
// 새 대시보드 생성
|
||||
const title = prompt("대시보드 제목을 입력하세요:", "새 대시보드");
|
||||
|
|
@ -372,11 +379,16 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
|
||||
savedDashboard = await dashboardApi.createDashboard(dashboardData);
|
||||
|
||||
const viewDashboard = confirm(`대시보드 "${title}"이 저장되었습니다!\n\n지금 확인해보시겠습니까?`);
|
||||
if (viewDashboard) {
|
||||
// Next.js 라우터로 뷰어 페이지 이동
|
||||
router.push(`/dashboard/${savedDashboard.id}`);
|
||||
}
|
||||
// 대시보드 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 : "알 수 없는 오류";
|
||||
|
|
@ -384,6 +396,58 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
}
|
||||
}, [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) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in New Issue