641 lines
25 KiB
TypeScript
641 lines
25 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState, useEffect, useRef } from "react";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||
import { Label } from "@/components/ui/label";
|
||
import { toast } from "sonner";
|
||
import { Search, Monitor, Settings, X, Plus } from "lucide-react";
|
||
import { menuScreenApi } from "@/lib/api/screen";
|
||
import { apiClient } from "@/lib/api/client";
|
||
import type { MenuItem } from "@/lib/api/menu";
|
||
import { ScreenDefinition } from "@/types/screen";
|
||
|
||
interface MenuAssignmentModalProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
screenInfo: ScreenDefinition | null;
|
||
onAssignmentComplete?: () => void;
|
||
onBackToList?: () => void; // 화면 목록으로 돌아가는 콜백 추가
|
||
}
|
||
|
||
export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||
isOpen,
|
||
onClose,
|
||
screenInfo,
|
||
onAssignmentComplete,
|
||
onBackToList,
|
||
}) => {
|
||
const [menus, setMenus] = useState<MenuItem[]>([]);
|
||
const [selectedMenuId, setSelectedMenuId] = useState<string>("");
|
||
const [selectedMenu, setSelectedMenu] = useState<MenuItem | null>(null);
|
||
const [searchTerm, setSearchTerm] = useState("");
|
||
const [loading, setLoading] = useState(false);
|
||
const [assigning, setAssigning] = useState(false);
|
||
const [existingScreens, setExistingScreens] = useState<ScreenDefinition[]>([]);
|
||
const [showReplaceDialog, setShowReplaceDialog] = useState(false);
|
||
const [assignmentSuccess, setAssignmentSuccess] = useState(false);
|
||
const [assignmentMessage, setAssignmentMessage] = useState("");
|
||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||
|
||
// 메뉴 목록 로드 (관리자 메뉴 + 사용자 메뉴)
|
||
const loadMenus = async () => {
|
||
try {
|
||
setLoading(true);
|
||
|
||
// 관리자 메뉴 가져오기
|
||
const adminResponse = await apiClient.get("/admin/menus", { params: { menuType: "0" } });
|
||
const adminMenus = adminResponse.data?.data || [];
|
||
|
||
// 사용자 메뉴 가져오기
|
||
const userResponse = await apiClient.get("/admin/menus", { params: { menuType: "1" } });
|
||
const userMenus = userResponse.data?.data || [];
|
||
|
||
// 메뉴 정규화 함수
|
||
const normalizeMenu = (menu: any) => ({
|
||
objid: menu.objid || menu.OBJID,
|
||
parent_obj_id: menu.parent_obj_id || menu.PARENT_OBJ_ID,
|
||
menu_name_kor: menu.menu_name_kor || menu.MENU_NAME_KOR,
|
||
menu_url: menu.menu_url || menu.MENU_URL,
|
||
menu_desc: menu.menu_desc || menu.MENU_DESC,
|
||
seq: menu.seq || menu.SEQ,
|
||
menu_type: menu.menu_type || menu.MENU_TYPE,
|
||
status: menu.status || menu.STATUS,
|
||
lev: menu.lev || menu.LEV,
|
||
company_code: menu.company_code || menu.COMPANY_CODE,
|
||
company_name: menu.company_name || menu.COMPANY_NAME,
|
||
});
|
||
|
||
// 관리자 메뉴 정규화
|
||
const normalizedAdminMenus = adminMenus.map((menu: any) => normalizeMenu(menu));
|
||
|
||
// 사용자 메뉴 정규화
|
||
const normalizedUserMenus = userMenus.map((menu: any) => normalizeMenu(menu));
|
||
|
||
// 모든 메뉴 합치기
|
||
const allMenus = [...normalizedAdminMenus, ...normalizedUserMenus];
|
||
|
||
// console.log("로드된 전체 메뉴 목록:", {
|
||
// totalAdmin: normalizedAdminMenus.length,
|
||
// totalUser: normalizedUserMenus.length,
|
||
// total: allMenus.length,
|
||
// });
|
||
setMenus(allMenus);
|
||
} catch (error) {
|
||
// console.error("메뉴 목록 로드 실패:", error);
|
||
toast.error("메뉴 목록을 불러오는데 실패했습니다.");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// 모달이 열릴 때 메뉴 목록 로드
|
||
useEffect(() => {
|
||
if (isOpen) {
|
||
loadMenus();
|
||
setSelectedMenuId("");
|
||
setSelectedMenu(null);
|
||
setSearchTerm("");
|
||
setAssignmentSuccess(false);
|
||
setAssignmentMessage("");
|
||
}
|
||
}, [isOpen]);
|
||
|
||
// 메뉴 선택 처리
|
||
const handleMenuSelect = async (menuId: string) => {
|
||
// 유효하지 않은 메뉴 ID인 경우 처리하지 않음
|
||
if (!menuId || menuId === "no-menu") {
|
||
setSelectedMenuId("");
|
||
setSelectedMenu(null);
|
||
setExistingScreens([]);
|
||
return;
|
||
}
|
||
|
||
setSelectedMenuId(menuId);
|
||
const menu = menus.find((m) => m.objid?.toString() === menuId);
|
||
setSelectedMenu(menu || null);
|
||
|
||
// 선택된 메뉴에 할당된 화면들 확인
|
||
if (menu) {
|
||
try {
|
||
const menuObjid = parseInt(menu.objid?.toString() || "0");
|
||
if (menuObjid > 0) {
|
||
const screens = await menuScreenApi.getScreensByMenu(menuObjid);
|
||
setExistingScreens(screens);
|
||
// console.log(`메뉴 "${menu.menu_name_kor}"에 할당된 화면:`, screens);
|
||
}
|
||
} catch (error) {
|
||
// console.error("할당된 화면 조회 실패:", error);
|
||
setExistingScreens([]);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 화면 할당 처리
|
||
const handleAssignScreen = async () => {
|
||
if (!selectedMenu || !screenInfo) {
|
||
toast.error("메뉴와 화면 정보가 필요합니다.");
|
||
return;
|
||
}
|
||
|
||
// 기존에 할당된 화면이 있는지 확인
|
||
if (existingScreens.length > 0) {
|
||
// 이미 같은 화면이 할당되어 있는지 확인
|
||
const alreadyAssigned = existingScreens.some((screen) => screen.screenId === screenInfo.screenId);
|
||
if (alreadyAssigned) {
|
||
toast.info("이미 해당 메뉴에 할당된 화면입니다.");
|
||
return;
|
||
}
|
||
|
||
// 다른 화면이 할당되어 있으면 교체 확인
|
||
setShowReplaceDialog(true);
|
||
return;
|
||
}
|
||
|
||
// 기존 화면이 없으면 바로 할당
|
||
await performAssignment();
|
||
};
|
||
|
||
// 실제 할당 수행
|
||
const performAssignment = async (replaceExisting = false) => {
|
||
if (!selectedMenu || !screenInfo) return;
|
||
|
||
try {
|
||
setAssigning(true);
|
||
|
||
const menuObjid = parseInt(selectedMenu.objid?.toString() || "0");
|
||
if (menuObjid === 0) {
|
||
toast.error("유효하지 않은 메뉴 ID입니다.");
|
||
return;
|
||
}
|
||
|
||
// 기존 화면 교체인 경우 기존 화면들 먼저 제거
|
||
if (replaceExisting && existingScreens.length > 0) {
|
||
// console.log("기존 화면들 제거 중...", existingScreens);
|
||
for (const existingScreen of existingScreens) {
|
||
try {
|
||
await menuScreenApi.unassignScreenFromMenu(existingScreen.screenId, menuObjid);
|
||
// console.log(`기존 화면 "${existingScreen.screenName}" 제거 완료`);
|
||
} catch (error) {
|
||
// console.error(`기존 화면 "${existingScreen.screenName}" 제거 실패:`, error);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 새 화면 할당
|
||
await menuScreenApi.assignScreenToMenu(screenInfo.screenId, menuObjid);
|
||
|
||
const successMessage = replaceExisting
|
||
? `기존 화면을 제거하고 "${screenInfo.screenName}" 화면이 "${selectedMenu.menu_name_kor}" 메뉴에 할당되었습니다.`
|
||
: `"${screenInfo.screenName}" 화면이 "${selectedMenu.menu_name_kor}" 메뉴에 성공적으로 할당되었습니다.`;
|
||
|
||
// 성공 상태 설정
|
||
setAssignmentSuccess(true);
|
||
setAssignmentMessage(successMessage);
|
||
|
||
// 할당 완료 콜백 호출
|
||
if (onAssignmentComplete) {
|
||
onAssignmentComplete();
|
||
}
|
||
|
||
// 3초 후 자동으로 화면 목록으로 이동
|
||
setTimeout(() => {
|
||
if (onBackToList) {
|
||
onBackToList();
|
||
} else {
|
||
onClose();
|
||
}
|
||
}, 3000);
|
||
} catch (error: any) {
|
||
// console.error("화면 할당 실패:", error);
|
||
const errorMessage = error.response?.data?.message || "화면 할당에 실패했습니다.";
|
||
toast.error(errorMessage);
|
||
} finally {
|
||
setAssigning(false);
|
||
}
|
||
};
|
||
|
||
// "나중에 할당" 처리 - 시각적 효과 포함
|
||
const handleAssignLater = () => {
|
||
if (!screenInfo) return;
|
||
|
||
// 성공 상태 설정 (나중에 할당 메시지)
|
||
setAssignmentSuccess(true);
|
||
setAssignmentMessage(`"${screenInfo.screenName}" 화면이 저장되었습니다. 나중에 메뉴에 할당할 수 있습니다.`);
|
||
|
||
// 할당 완료 콜백 호출
|
||
if (onAssignmentComplete) {
|
||
onAssignmentComplete();
|
||
}
|
||
|
||
// 3초 후 자동으로 화면 목록으로 이동
|
||
setTimeout(() => {
|
||
if (onBackToList) {
|
||
onBackToList();
|
||
} else {
|
||
onClose();
|
||
}
|
||
}, 3000);
|
||
};
|
||
|
||
// 필터된 메뉴 목록
|
||
const filteredMenus = menus.filter((menu) => {
|
||
if (!searchTerm) return true;
|
||
const searchLower = searchTerm.toLowerCase();
|
||
return (
|
||
menu.menu_name_kor?.toLowerCase().includes(searchLower) ||
|
||
menu.menu_url?.toLowerCase().includes(searchLower) ||
|
||
menu.menu_desc?.toLowerCase().includes(searchLower)
|
||
);
|
||
});
|
||
|
||
// 메뉴 옵션 생성 (계층 구조 표시, 타입별 그룹화)
|
||
const getMenuOptions = (): React.ReactNode[] => {
|
||
if (loading) {
|
||
return [
|
||
<SelectItem key="loading" value="loading" disabled>
|
||
메뉴 로딩 중...
|
||
</SelectItem>,
|
||
];
|
||
}
|
||
|
||
if (filteredMenus.length === 0) {
|
||
return [
|
||
<SelectItem key="no-menu" value="no-menu" disabled>
|
||
{searchTerm ? `"${searchTerm}"에 대한 검색 결과가 없습니다` : "메뉴가 없습니다"}
|
||
</SelectItem>,
|
||
];
|
||
}
|
||
|
||
// 관리자 메뉴와 사용자 메뉴 분리
|
||
const adminMenus = filteredMenus.filter(
|
||
(menu) => menu.menu_type === "0" && menu.objid && menu.objid.toString().trim() !== "",
|
||
);
|
||
const userMenus = filteredMenus.filter(
|
||
(menu) => menu.menu_type === "1" && menu.objid && menu.objid.toString().trim() !== "",
|
||
);
|
||
|
||
const options: React.ReactNode[] = [];
|
||
|
||
// 관리자 메뉴 섹션
|
||
if (adminMenus.length > 0) {
|
||
options.push(
|
||
<div key="admin-header" className="bg-blue-50 px-2 py-1.5 text-xs font-semibold text-blue-600">
|
||
👤 관리자 메뉴
|
||
</div>,
|
||
);
|
||
adminMenus.forEach((menu) => {
|
||
const indent = " ".repeat(Math.max(0, menu.lev || 0));
|
||
const menuId = menu.objid!.toString();
|
||
options.push(
|
||
<SelectItem key={menuId} value={menuId}>
|
||
{indent}
|
||
{menu.menu_name_kor}
|
||
</SelectItem>,
|
||
);
|
||
});
|
||
}
|
||
|
||
// 사용자 메뉴 섹션
|
||
if (userMenus.length > 0) {
|
||
if (adminMenus.length > 0) {
|
||
options.push(<div key="separator" className="my-1 border-t" />);
|
||
}
|
||
options.push(
|
||
<div key="user-header" className="bg-green-50 px-2 py-1.5 text-xs font-semibold text-green-600">
|
||
👥 사용자 메뉴
|
||
</div>,
|
||
);
|
||
userMenus.forEach((menu) => {
|
||
const indent = " ".repeat(Math.max(0, menu.lev || 0));
|
||
const menuId = menu.objid!.toString();
|
||
options.push(
|
||
<SelectItem key={menuId} value={menuId}>
|
||
{indent}
|
||
{menu.menu_name_kor}
|
||
</SelectItem>,
|
||
);
|
||
});
|
||
}
|
||
|
||
return options;
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||
<DialogContent className="max-w-2xl">
|
||
{assignmentSuccess ? (
|
||
// 성공 화면
|
||
<>
|
||
<DialogHeader>
|
||
<DialogTitle className="flex items-center gap-2">
|
||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100">
|
||
<svg className="h-5 w-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||
</svg>
|
||
</div>
|
||
{assignmentMessage.includes("나중에") ? "화면 저장 완료" : "화면 할당 완료"}
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
{assignmentMessage.includes("나중에")
|
||
? "화면이 성공적으로 저장되었습니다. 나중에 메뉴에 할당할 수 있습니다."
|
||
: "화면이 성공적으로 메뉴에 할당되었습니다."}
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-4">
|
||
<div className="rounded-lg border bg-green-50 p-4">
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-green-100">
|
||
<Monitor className="h-5 w-5 text-green-600" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<p className="text-sm font-medium text-green-900">{assignmentMessage}</p>
|
||
<p className="mt-1 text-xs text-green-700">3초 후 자동으로 화면 목록으로 이동합니다...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-center space-x-2">
|
||
<div className="h-2 w-2 animate-bounce rounded-full bg-green-500 [animation-delay:-0.3s]"></div>
|
||
<div className="h-2 w-2 animate-bounce rounded-full bg-green-500 [animation-delay:-0.15s]"></div>
|
||
<div className="h-2 w-2 animate-bounce rounded-full bg-green-500"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<DialogFooter>
|
||
<Button
|
||
onClick={() => {
|
||
if (onBackToList) {
|
||
onBackToList();
|
||
} else {
|
||
onClose();
|
||
}
|
||
}}
|
||
className="bg-green-600 hover:bg-green-700"
|
||
>
|
||
<Monitor className="mr-2 h-4 w-4" />
|
||
화면 목록으로 이동
|
||
</Button>
|
||
</DialogFooter>
|
||
</>
|
||
) : (
|
||
// 기본 할당 화면
|
||
<>
|
||
<DialogHeader>
|
||
<DialogTitle className="flex items-center gap-2">
|
||
<Settings className="h-5 w-5" />
|
||
메뉴에 화면 할당
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
저장된 화면을 메뉴에 할당하여 사용자가 접근할 수 있도록 설정합니다.
|
||
</DialogDescription>
|
||
{screenInfo && (
|
||
<div className="bg-accent mt-2 rounded-lg border p-3">
|
||
<div className="flex items-center gap-2">
|
||
<Monitor className="text-primary h-4 w-4" />
|
||
<span className="font-medium text-blue-900">{screenInfo.screenName}</span>
|
||
<Badge variant="outline" className="font-mono text-xs">
|
||
{screenInfo.screenCode}
|
||
</Badge>
|
||
</div>
|
||
{screenInfo.description && <p className="mt-1 text-sm text-blue-700">{screenInfo.description}</p>}
|
||
</div>
|
||
)}
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-4">
|
||
{/* 메뉴 선택 (검색 기능 포함) */}
|
||
<div>
|
||
<Label htmlFor="menu-select">할당할 메뉴 선택</Label>
|
||
<Select
|
||
value={selectedMenuId}
|
||
onValueChange={handleMenuSelect}
|
||
disabled={loading}
|
||
onOpenChange={(open) => {
|
||
if (open) {
|
||
// Select가 열릴 때 검색창에 포커스
|
||
setTimeout(() => {
|
||
searchInputRef.current?.focus();
|
||
}, 100);
|
||
}
|
||
}}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder={loading ? "메뉴 로딩 중..." : "메뉴를 선택하세요"} />
|
||
</SelectTrigger>
|
||
<SelectContent className="max-h-64">
|
||
{/* 검색 입력 필드 */}
|
||
<div
|
||
className="sticky top-0 z-10 border-b bg-white p-2"
|
||
onKeyDown={(e) => {
|
||
// 이 div 내에서 발생하는 모든 키 이벤트를 차단
|
||
e.stopPropagation();
|
||
}}
|
||
>
|
||
<div className="relative">
|
||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
||
<input
|
||
ref={searchInputRef}
|
||
type="text"
|
||
placeholder="메뉴명, URL, 설명으로 검색..."
|
||
value={searchTerm}
|
||
autoFocus
|
||
onChange={(e) => {
|
||
e.stopPropagation();
|
||
setSearchTerm(e.target.value);
|
||
}}
|
||
onKeyDown={(e) => {
|
||
// 이벤트가 Select로 전파되지 않도록 완전 차단
|
||
e.stopPropagation();
|
||
}}
|
||
onClick={(e) => e.stopPropagation()}
|
||
onFocus={(e) => e.stopPropagation()}
|
||
onMouseDown={(e) => e.stopPropagation()}
|
||
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-8 w-full rounded-md border px-3 py-2 pr-8 pl-10 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||
/>
|
||
{searchTerm && (
|
||
<button
|
||
type="button"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setSearchTerm("");
|
||
}}
|
||
className="hover:text-muted-foreground absolute top-1/2 right-2 h-4 w-4 -translate-y-1/2 transform text-gray-400"
|
||
>
|
||
<X className="h-3 w-3" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
{/* 메뉴 옵션들 */}
|
||
<div className="max-h-48 overflow-y-auto">{getMenuOptions()}</div>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
{/* 선택된 메뉴 정보 */}
|
||
{selectedMenu && (
|
||
<div className="rounded-lg border bg-gray-50 p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-2">
|
||
<h4 className="font-medium">{selectedMenu.menu_name_kor}</h4>
|
||
<Badge variant={selectedMenu.menu_type === "0" ? "default" : "secondary"}>
|
||
{selectedMenu.menu_type === "0" ? "관리자" : "사용자"}
|
||
</Badge>
|
||
<Badge variant={selectedMenu.status === "active" ? "default" : "outline"}>
|
||
{selectedMenu.status === "active" ? "활성" : "비활성"}
|
||
</Badge>
|
||
</div>
|
||
<div className="text-muted-foreground mt-1 space-y-1 text-sm">
|
||
{selectedMenu.menu_url && <p>URL: {selectedMenu.menu_url}</p>}
|
||
{selectedMenu.menu_desc && <p>설명: {selectedMenu.menu_desc}</p>}
|
||
{selectedMenu.company_name && <p>회사: {selectedMenu.company_name}</p>}
|
||
</div>
|
||
|
||
{/* 기존 할당된 화면 정보 */}
|
||
{existingScreens.length > 0 && (
|
||
<div className="mt-3 rounded border bg-yellow-50 p-2">
|
||
<p className="text-sm font-medium text-yellow-800">
|
||
⚠️ 이미 할당된 화면 ({existingScreens.length}개)
|
||
</p>
|
||
<div className="mt-1 space-y-1">
|
||
{existingScreens.map((screen) => (
|
||
<div key={screen.screenId} className="flex items-center gap-2 text-xs text-yellow-700">
|
||
<Monitor className="h-3 w-3" />
|
||
<span>{screen.screenName}</span>
|
||
<Badge variant="outline" className="text-xs">
|
||
{screen.screenCode}
|
||
</Badge>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<p className="mt-1 text-xs text-yellow-600">새 화면을 할당하면 기존 화면들이 제거됩니다.</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<DialogFooter className="flex gap-2">
|
||
<Button variant="outline" onClick={handleAssignLater} disabled={assigning}>
|
||
<X className="mr-2 h-4 w-4" />
|
||
나중에 할당
|
||
</Button>
|
||
<Button
|
||
onClick={handleAssignScreen}
|
||
disabled={!selectedMenu || assigning}
|
||
className="bg-blue-600 hover:bg-blue-700"
|
||
>
|
||
{assigning ? (
|
||
<>
|
||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||
할당 중...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Settings className="mr-2 h-4 w-4" />
|
||
메뉴에 할당
|
||
</>
|
||
)}
|
||
</Button>
|
||
</DialogFooter>
|
||
</>
|
||
)}
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* 화면 교체 확인 대화상자 */}
|
||
<Dialog open={showReplaceDialog} onOpenChange={setShowReplaceDialog}>
|
||
<DialogContent className="max-w-md">
|
||
<DialogHeader>
|
||
<DialogTitle className="flex items-center gap-2">
|
||
<Monitor className="h-5 w-5 text-orange-600" />
|
||
화면 교체 확인
|
||
</DialogTitle>
|
||
<DialogDescription>선택한 메뉴에 이미 할당된 화면이 있습니다.</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-4">
|
||
{/* 기존 화면 목록 */}
|
||
<div className="bg-destructive/10 rounded-lg border p-3">
|
||
<p className="mb-2 text-sm font-medium text-red-800">제거될 화면 ({existingScreens.length}개):</p>
|
||
<div className="space-y-1">
|
||
{existingScreens.map((screen) => (
|
||
<div key={screen.screenId} className="flex items-center gap-2 text-sm text-red-700">
|
||
<X className="h-3 w-3" />
|
||
<span>{screen.screenName}</span>
|
||
<Badge variant="outline" className="text-xs">
|
||
{screen.screenCode}
|
||
</Badge>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 새로 할당될 화면 */}
|
||
{screenInfo && (
|
||
<div className="rounded-lg border bg-green-50 p-3">
|
||
<p className="mb-2 text-sm font-medium text-green-800">새로 할당될 화면:</p>
|
||
<div className="flex items-center gap-2 text-sm text-green-700">
|
||
<Plus className="h-3 w-3" />
|
||
<span>{screenInfo.screenName}</span>
|
||
<Badge variant="outline" className="text-xs">
|
||
{screenInfo.screenCode}
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="rounded-lg border-l-4 border-orange-400 bg-orange-50 p-3">
|
||
<p className="text-sm text-orange-800">
|
||
<strong>주의:</strong> 기존 화면들이 메뉴에서 제거되고 새 화면으로 교체됩니다. 이 작업은 되돌릴 수
|
||
없습니다.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<DialogFooter className="flex gap-2">
|
||
<Button variant="outline" onClick={() => setShowReplaceDialog(false)} disabled={assigning}>
|
||
취소
|
||
</Button>
|
||
<Button
|
||
onClick={async () => {
|
||
setShowReplaceDialog(false);
|
||
await performAssignment(true);
|
||
}}
|
||
disabled={assigning}
|
||
className="bg-orange-600 hover:bg-orange-700"
|
||
>
|
||
{assigning ? (
|
||
<>
|
||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||
교체 중...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Monitor className="mr-2 h-4 w-4" />
|
||
화면 교체
|
||
</>
|
||
)}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</>
|
||
);
|
||
};
|