396 lines
15 KiB
TypeScript
396 lines
15 KiB
TypeScript
|
|
"use client";
|
|||
|
|
|
|||
|
|
import React, { useState, useEffect } from "react";
|
|||
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||
|
|
import { Button } from "@/components/ui/button";
|
|||
|
|
import { Badge } from "@/components/ui/badge";
|
|||
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|||
|
|
import { Input } from "@/components/ui/input";
|
|||
|
|
import { Label } from "@/components/ui/label";
|
|||
|
|
import { Separator } from "@/components/ui/separator";
|
|||
|
|
import { toast } from "sonner";
|
|||
|
|
import {
|
|||
|
|
AlertDialog,
|
|||
|
|
AlertDialogAction,
|
|||
|
|
AlertDialogCancel,
|
|||
|
|
AlertDialogContent,
|
|||
|
|
AlertDialogDescription,
|
|||
|
|
AlertDialogFooter,
|
|||
|
|
AlertDialogHeader,
|
|||
|
|
AlertDialogTitle,
|
|||
|
|
} from "@/components/ui/alert-dialog";
|
|||
|
|
import { Search, Plus, X, Monitor, Settings } from "lucide-react";
|
|||
|
|
import { menuScreenApi, screenApi } from "@/lib/api/screen";
|
|||
|
|
import { ScreenDefinition } from "@/types/screen";
|
|||
|
|
import type { MenuItem } from "@/lib/api/menu";
|
|||
|
|
|
|||
|
|
interface ScreenAssignmentTabProps {
|
|||
|
|
menus: MenuItem[];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus }) => {
|
|||
|
|
const [selectedMenuId, setSelectedMenuId] = useState<string>("");
|
|||
|
|
const [selectedMenu, setSelectedMenu] = useState<MenuItem | null>(null);
|
|||
|
|
const [assignedScreens, setAssignedScreens] = useState<ScreenDefinition[]>([]);
|
|||
|
|
const [availableScreens, setAvailableScreens] = useState<ScreenDefinition[]>([]);
|
|||
|
|
const [loading, setLoading] = useState(false);
|
|||
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|||
|
|
const [showAssignDialog, setShowAssignDialog] = useState(false);
|
|||
|
|
const [showUnassignDialog, setShowUnassignDialog] = useState(false);
|
|||
|
|
const [selectedScreen, setSelectedScreen] = useState<ScreenDefinition | null>(null);
|
|||
|
|
|
|||
|
|
// 디버그: 전달받은 메뉴 데이터 확인
|
|||
|
|
console.log("ScreenAssignmentTab - 전달받은 메뉴 데이터:", {
|
|||
|
|
total: menus.length,
|
|||
|
|
sample: menus.slice(0, 3),
|
|||
|
|
keys: menus.length > 0 ? Object.keys(menus[0]) : [],
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 메뉴 선택
|
|||
|
|
const handleMenuSelect = async (menuId: string) => {
|
|||
|
|
console.log("메뉴 선택:", menuId);
|
|||
|
|
setSelectedMenuId(menuId);
|
|||
|
|
|
|||
|
|
// 다양한 형식의 objid 대응
|
|||
|
|
const menu = menus.find((m) => {
|
|||
|
|
const objid = m.objid || m.OBJID || (m as any).objid;
|
|||
|
|
return objid?.toString() === menuId;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log("선택된 메뉴:", menu);
|
|||
|
|
setSelectedMenu(menu || null);
|
|||
|
|
|
|||
|
|
if (menu) {
|
|||
|
|
const objid = menu.objid || menu.OBJID || (menu as any).objid;
|
|||
|
|
if (objid) {
|
|||
|
|
await loadAssignedScreens(parseInt(objid.toString()));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 할당된 화면 목록 로드
|
|||
|
|
const loadAssignedScreens = async (menuObjid: number) => {
|
|||
|
|
try {
|
|||
|
|
setLoading(true);
|
|||
|
|
const screens = await menuScreenApi.getScreensByMenu(menuObjid);
|
|||
|
|
setAssignedScreens(screens);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error("할당된 화면 로드 실패:", error);
|
|||
|
|
toast.error("할당된 화면 목록을 불러오는데 실패했습니다.");
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 할당 가능한 화면 목록 로드
|
|||
|
|
const loadAvailableScreens = async () => {
|
|||
|
|
try {
|
|||
|
|
const response = await screenApi.getScreens({});
|
|||
|
|
const allScreens = response.data;
|
|||
|
|
|
|||
|
|
// 이미 할당된 화면 제외
|
|||
|
|
const assignedScreenIds = assignedScreens.map((screen) => screen.screenId);
|
|||
|
|
const available = allScreens.filter((screen) => !assignedScreenIds.includes(screen.screenId));
|
|||
|
|
|
|||
|
|
setAvailableScreens(available);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error("사용 가능한 화면 로드 실패:", error);
|
|||
|
|
toast.error("사용 가능한 화면 목록을 불러오는데 실패했습니다.");
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 화면 할당
|
|||
|
|
const handleAssignScreen = async () => {
|
|||
|
|
if (!selectedScreen || !selectedMenu) return;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const objid = selectedMenu.objid || selectedMenu.OBJID || (selectedMenu as any).objid;
|
|||
|
|
if (!objid) {
|
|||
|
|
toast.error("메뉴 ID를 찾을 수 없습니다.");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await menuScreenApi.assignScreenToMenu(selectedScreen.screenId, parseInt(objid.toString()));
|
|||
|
|
toast.success("화면이 메뉴에 성공적으로 할당되었습니다.");
|
|||
|
|
|
|||
|
|
// 목록 새로고침
|
|||
|
|
await loadAssignedScreens(parseInt(objid.toString()));
|
|||
|
|
setShowAssignDialog(false);
|
|||
|
|
setSelectedScreen(null);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error("화면 할당 실패:", error);
|
|||
|
|
toast.error("화면 할당에 실패했습니다.");
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 화면 할당 해제
|
|||
|
|
const handleUnassignScreen = async () => {
|
|||
|
|
if (!selectedScreen || !selectedMenu) return;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const objid = selectedMenu.objid || selectedMenu.OBJID || (selectedMenu as any).objid;
|
|||
|
|
if (!objid) {
|
|||
|
|
toast.error("메뉴 ID를 찾을 수 없습니다.");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await menuScreenApi.unassignScreenFromMenu(selectedScreen.screenId, parseInt(objid.toString()));
|
|||
|
|
toast.success("화면-메뉴 할당이 해제되었습니다.");
|
|||
|
|
|
|||
|
|
// 목록 새로고침
|
|||
|
|
await loadAssignedScreens(parseInt(objid.toString()));
|
|||
|
|
setShowUnassignDialog(false);
|
|||
|
|
setSelectedScreen(null);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error("화면 할당 해제 실패:", error);
|
|||
|
|
toast.error("화면 할당 해제에 실패했습니다.");
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 화면 할당 대화상자 열기
|
|||
|
|
const openAssignDialog = async () => {
|
|||
|
|
await loadAvailableScreens();
|
|||
|
|
setShowAssignDialog(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 필터된 사용 가능한 화면
|
|||
|
|
const filteredAvailableScreens = availableScreens.filter(
|
|||
|
|
(screen) =>
|
|||
|
|
screen.screenName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|||
|
|
screen.screenCode.toLowerCase().includes(searchTerm.toLowerCase()),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 단순화된 메뉴 옵션 생성 (모든 메뉴를 평면적으로 표시)
|
|||
|
|
const getMenuOptions = (menuList: MenuItem[]): JSX.Element[] => {
|
|||
|
|
console.log("메뉴 옵션 생성:", {
|
|||
|
|
total: menuList.length,
|
|||
|
|
sample: menuList.slice(0, 3).map((m) => ({
|
|||
|
|
objid: m.objid || m.OBJID || (m as any).objid,
|
|||
|
|
name: m.menu_name_kor || m.MENU_NAME_KOR || (m as any).menu_name_kor,
|
|||
|
|
parent: m.parent_obj_id || m.PARENT_OBJ_ID || (m as any).parent_obj_id,
|
|||
|
|
})),
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (!menuList || menuList.length === 0) {
|
|||
|
|
return [
|
|||
|
|
<SelectItem key="no-menu" value="" disabled>
|
|||
|
|
메뉴가 없습니다
|
|||
|
|
</SelectItem>,
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return menuList.map((menu, index) => {
|
|||
|
|
// 현재 메뉴의 ID와 이름 추출 (다양한 형식 대응)
|
|||
|
|
const menuObjid = menu.objid || menu.OBJID || (menu as any).objid;
|
|||
|
|
const menuName = menu.menu_name_kor || menu.MENU_NAME_KOR || (menu as any).menu_name_kor;
|
|||
|
|
const lev = menu.lev || menu.LEV || (menu as any).lev || 0;
|
|||
|
|
|
|||
|
|
// 들여쓰기 (레벨에 따라)
|
|||
|
|
const indent = " ".repeat(Math.max(0, lev));
|
|||
|
|
|
|||
|
|
console.log("메뉴 항목:", { index, menuObjid, menuName, lev });
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<SelectItem key={menuObjid?.toString() || `menu-${index}`} value={menuObjid?.toString() || `menu-${index}`}>
|
|||
|
|
{indent}
|
|||
|
|
{menuName || `메뉴 ${index + 1}`}
|
|||
|
|
</SelectItem>
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-6">
|
|||
|
|
{/* 메뉴 선택 */}
|
|||
|
|
<Card>
|
|||
|
|
<CardHeader>
|
|||
|
|
<CardTitle className="flex items-center gap-2">
|
|||
|
|
<Settings className="h-5 w-5" />
|
|||
|
|
화면 할당 관리
|
|||
|
|
</CardTitle>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent>
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div>
|
|||
|
|
<Label htmlFor="menu-select">메뉴 선택</Label>
|
|||
|
|
<Select value={selectedMenuId} onValueChange={handleMenuSelect}>
|
|||
|
|
<SelectTrigger>
|
|||
|
|
<SelectValue placeholder="화면을 할당할 메뉴를 선택하세요" />
|
|||
|
|
</SelectTrigger>
|
|||
|
|
<SelectContent>{getMenuOptions(menus)}</SelectContent>
|
|||
|
|
</Select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{selectedMenu && (
|
|||
|
|
<div className="rounded-lg border bg-gray-50 p-4">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div>
|
|||
|
|
<h3 className="font-medium">
|
|||
|
|
{selectedMenu.menu_name_kor ||
|
|||
|
|
selectedMenu.MENU_NAME_KOR ||
|
|||
|
|
(selectedMenu as any).menu_name_kor ||
|
|||
|
|
"메뉴"}
|
|||
|
|
</h3>
|
|||
|
|
<p className="text-sm text-gray-600">
|
|||
|
|
URL: {selectedMenu.menu_url || selectedMenu.MENU_URL || (selectedMenu as any).menu_url || "없음"}
|
|||
|
|
</p>
|
|||
|
|
<p className="text-sm text-gray-600">
|
|||
|
|
설명:{" "}
|
|||
|
|
{selectedMenu.menu_desc || selectedMenu.MENU_DESC || (selectedMenu as any).menu_desc || "없음"}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
<Badge
|
|||
|
|
variant={
|
|||
|
|
(selectedMenu.status || selectedMenu.STATUS || (selectedMenu as any).status) === "active"
|
|||
|
|
? "default"
|
|||
|
|
: "secondary"
|
|||
|
|
}
|
|||
|
|
>
|
|||
|
|
{(selectedMenu.status || selectedMenu.STATUS || (selectedMenu as any).status) === "active"
|
|||
|
|
? "활성"
|
|||
|
|
: "비활성"}
|
|||
|
|
</Badge>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
{/* 할당된 화면 목록 */}
|
|||
|
|
{selectedMenu && (
|
|||
|
|
<Card>
|
|||
|
|
<CardHeader>
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<CardTitle className="flex items-center gap-2">
|
|||
|
|
<Monitor className="h-5 w-5" />
|
|||
|
|
할당된 화면 ({assignedScreens.length}개)
|
|||
|
|
</CardTitle>
|
|||
|
|
<Button onClick={openAssignDialog} className="bg-blue-600 hover:bg-blue-700">
|
|||
|
|
<Plus className="mr-2 h-4 w-4" />
|
|||
|
|
화면 할당
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent>
|
|||
|
|
{loading ? (
|
|||
|
|
<div className="py-8 text-center text-gray-500">로딩 중...</div>
|
|||
|
|
) : assignedScreens.length === 0 ? (
|
|||
|
|
<div className="py-8 text-center text-gray-500">할당된 화면이 없습니다. 화면을 할당해보세요.</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
{assignedScreens.map((screen) => (
|
|||
|
|
<div
|
|||
|
|
key={screen.screenId}
|
|||
|
|
className="flex items-center justify-between rounded-lg border p-4 hover:bg-gray-50"
|
|||
|
|
>
|
|||
|
|
<div className="flex-1">
|
|||
|
|
<div className="flex items-center gap-3">
|
|||
|
|
<h4 className="font-medium">{screen.screenName}</h4>
|
|||
|
|
<Badge variant="outline" className="font-mono">
|
|||
|
|
{screen.screenCode}
|
|||
|
|
</Badge>
|
|||
|
|
<Badge variant={screen.isActive === "Y" ? "default" : "secondary"}>
|
|||
|
|
{screen.isActive === "Y" ? "활성" : "비활성"}
|
|||
|
|
</Badge>
|
|||
|
|
</div>
|
|||
|
|
<p className="mt-1 text-sm text-gray-600">
|
|||
|
|
테이블: {screen.tableName} | 생성일: {screen.createdDate.toLocaleDateString()}
|
|||
|
|
</p>
|
|||
|
|
{screen.description && <p className="mt-1 text-sm text-gray-500">{screen.description}</p>}
|
|||
|
|
</div>
|
|||
|
|
<Button
|
|||
|
|
variant="outline"
|
|||
|
|
size="sm"
|
|||
|
|
onClick={() => {
|
|||
|
|
setSelectedScreen(screen);
|
|||
|
|
setShowUnassignDialog(true);
|
|||
|
|
}}
|
|||
|
|
className="text-red-600 hover:text-red-700"
|
|||
|
|
>
|
|||
|
|
<X className="h-4 w-4" />
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 화면 할당 대화상자 */}
|
|||
|
|
<AlertDialog open={showAssignDialog} onOpenChange={setShowAssignDialog}>
|
|||
|
|
<AlertDialogContent className="max-w-2xl">
|
|||
|
|
<AlertDialogHeader>
|
|||
|
|
<AlertDialogTitle>화면 할당</AlertDialogTitle>
|
|||
|
|
<AlertDialogDescription>{selectedMenu?.menu_name_kor}에 할당할 화면을 선택하세요.</AlertDialogDescription>
|
|||
|
|
</AlertDialogHeader>
|
|||
|
|
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
{/* 검색 */}
|
|||
|
|
<div className="relative">
|
|||
|
|
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
|||
|
|
<Input
|
|||
|
|
placeholder="화면명 또는 코드로 검색..."
|
|||
|
|
value={searchTerm}
|
|||
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|||
|
|
className="pl-10"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 화면 목록 */}
|
|||
|
|
<div className="max-h-64 space-y-2 overflow-y-auto">
|
|||
|
|
{filteredAvailableScreens.length === 0 ? (
|
|||
|
|
<div className="py-4 text-center text-gray-500">할당 가능한 화면이 없습니다.</div>
|
|||
|
|
) : (
|
|||
|
|
filteredAvailableScreens.map((screen) => (
|
|||
|
|
<div
|
|||
|
|
key={screen.screenId}
|
|||
|
|
className={`cursor-pointer rounded-lg border p-3 transition-colors ${
|
|||
|
|
selectedScreen?.screenId === screen.screenId ? "border-blue-500 bg-blue-50" : "hover:bg-gray-50"
|
|||
|
|
}`}
|
|||
|
|
onClick={() => setSelectedScreen(screen)}
|
|||
|
|
>
|
|||
|
|
<div className="flex items-center gap-3">
|
|||
|
|
<h4 className="font-medium">{screen.screenName}</h4>
|
|||
|
|
<Badge variant="outline" className="font-mono text-xs">
|
|||
|
|
{screen.screenCode}
|
|||
|
|
</Badge>
|
|||
|
|
</div>
|
|||
|
|
<p className="mt-1 text-sm text-gray-600">테이블: {screen.tableName}</p>
|
|||
|
|
</div>
|
|||
|
|
))
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<AlertDialogFooter>
|
|||
|
|
<AlertDialogCancel onClick={() => setSelectedScreen(null)}>취소</AlertDialogCancel>
|
|||
|
|
<AlertDialogAction onClick={handleAssignScreen} disabled={!selectedScreen}>
|
|||
|
|
할당
|
|||
|
|
</AlertDialogAction>
|
|||
|
|
</AlertDialogFooter>
|
|||
|
|
</AlertDialogContent>
|
|||
|
|
</AlertDialog>
|
|||
|
|
|
|||
|
|
{/* 화면 할당 해제 대화상자 */}
|
|||
|
|
<AlertDialog open={showUnassignDialog} onOpenChange={setShowUnassignDialog}>
|
|||
|
|
<AlertDialogContent>
|
|||
|
|
<AlertDialogHeader>
|
|||
|
|
<AlertDialogTitle>화면 할당 해제</AlertDialogTitle>
|
|||
|
|
<AlertDialogDescription>
|
|||
|
|
"{selectedScreen?.screenName}" 화면의 메뉴 할당을 해제하시겠습니까?
|
|||
|
|
</AlertDialogDescription>
|
|||
|
|
</AlertDialogHeader>
|
|||
|
|
<AlertDialogFooter>
|
|||
|
|
<AlertDialogCancel onClick={() => setSelectedScreen(null)}>취소</AlertDialogCancel>
|
|||
|
|
<AlertDialogAction onClick={handleUnassignScreen} className="bg-red-600 hover:bg-red-700">
|
|||
|
|
할당 해제
|
|||
|
|
</AlertDialogAction>
|
|||
|
|
</AlertDialogFooter>
|
|||
|
|
</AlertDialogContent>
|
|||
|
|
</AlertDialog>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|