대시보드에 화면 할당 구현
This commit is contained in:
parent
8e2c66e2a4
commit
5093d336c0
|
|
@ -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<DashboardElement[]>([]);
|
||||
const [selectedElement, setSelectedElement] = useState<string | null>(null);
|
||||
const [elementCounter, setElementCounter] = useState(0);
|
||||
|
|
@ -34,9 +39,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 [saveModalOpen, setSaveModalOpen] = useState(false);
|
||||
const [dashboardDescription, setDashboardDescription] = useState<string>("");
|
||||
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
||||
|
||||
// 화면 해상도 자동 감지
|
||||
const [screenResolution] = useState<Resolution>(() => 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 && (
|
||||
<MenuAssignmentModal
|
||||
isOpen={menuAssignmentModalOpen}
|
||||
onClose={() => {
|
||||
setMenuAssignmentModalOpen(false);
|
||||
setSavedDashboardInfo(null);
|
||||
// 모달을 그냥 닫으면 대시보드 뷰어로 이동
|
||||
router.push(`/dashboard/${savedDashboardInfo.id}`);
|
||||
}}
|
||||
onConfirm={handleMenuAssignment}
|
||||
dashboardId={savedDashboardInfo.id}
|
||||
dashboardTitle={savedDashboardInfo.title}
|
||||
/>
|
||||
)}
|
||||
{/* 저장 모달 */}
|
||||
<DashboardSaveModal
|
||||
isOpen={saveModalOpen}
|
||||
onClose={() => setSaveModalOpen(false)}
|
||||
onSave={handleSave}
|
||||
initialTitle={dashboardTitle}
|
||||
initialDescription={dashboardDescription}
|
||||
isEditing={!!dashboardId}
|
||||
/>
|
||||
|
||||
{/* 저장 성공 모달 */}
|
||||
<Dialog
|
||||
open={successModalOpen}
|
||||
onOpenChange={() => {
|
||||
setSuccessModalOpen(false);
|
||||
router.push("/admin/dashboard");
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
|
||||
<CheckCircle2 className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
<DialogTitle className="text-center">저장 완료</DialogTitle>
|
||||
<DialogDescription className="text-center">대시보드가 성공적으로 저장되었습니다.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSuccessModalOpen(false);
|
||||
router.push("/admin/dashboard");
|
||||
}}
|
||||
>
|
||||
확인
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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 (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-h-[90vh] max-w-2xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEditing ? "대시보드 수정" : "대시보드 저장"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
{/* 대시보드 이름 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">
|
||||
대시보드 이름 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
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)}
|
||||
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-gray-200 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-gray-400" />
|
||||
<span className="ml-2 text-sm text-gray-500">메뉴 목록 로딩 중...</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-gray-500">사용 가능한 메뉴가 없습니다.</div>
|
||||
) : (
|
||||
flatMenus.map((menu) => (
|
||||
<SelectItem key={menu.uniqueKey} value={menu.id}>
|
||||
{menu.label}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedMenuId && (
|
||||
<div className="rounded-md bg-gray-50 p-2 text-sm text-gray-700">
|
||||
선택된 메뉴:{" "}
|
||||
<span className="font-medium">{flatMenus.find((m) => m.id === selectedMenuId)?.label}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{assignToMenu && selectedMenuId && (
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
선택한 메뉴의 URL이 이 대시보드로 자동 설정됩니다.
|
||||
{menuType === "admin" && " (관리자 모드 파라미터 포함)"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<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>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -153,7 +153,8 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
|
|||
// customTitle이 변경되었는지 확인
|
||||
const isTitleChanged = customTitle.trim() !== (element.customTitle || "");
|
||||
|
||||
const canSave = isTitleChanged || // 제목만 변경해도 저장 가능
|
||||
const canSave =
|
||||
isTitleChanged || // 제목만 변경해도 저장 가능
|
||||
(isSimpleWidget
|
||||
? // 간단한 위젯: 2단계에서 쿼리 테스트 후 저장 가능
|
||||
currentStep === 2 && queryResult && queryResult.rows.length > 0
|
||||
|
|
@ -201,18 +202,16 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
|
|||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 커스텀 제목 입력 */}
|
||||
<div className="mt-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
위젯 제목 (선택사항)
|
||||
</label>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">위젯 제목 (선택사항)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customTitle}
|
||||
onChange={(e) => setCustomTitle(e.target.value)}
|
||||
placeholder={`예: 정비 일정 목록, 창고 위치 현황 등 (비워두면 자동 생성)`}
|
||||
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
placeholder={"예: 정비 일정 목록, 창고 위치 현황 등 (비워두면 자동 생성)"}
|
||||
className="focus:border-primary focus:ring-primary w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-1 focus:outline-none"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
💡 비워두면 테이블명으로 자동 생성됩니다 (예: "maintenance_schedules 목록")
|
||||
|
|
|
|||
Loading…
Reference in New Issue