대시보드에 화면 할당 (중간)
This commit is contained in:
parent
71aaef7acb
commit
062efac47f
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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