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-muted-foreground">
|
||
URL: {selectedMenu.menu_url || selectedMenu.MENU_URL || (selectedMenu as any).menu_url || "없음"}
|
||
</p>
|
||
<p className="text-sm text-muted-foreground">
|
||
설명:{" "}
|
||
{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-muted-foreground">
|
||
테이블: {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-destructive 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-primary bg-accent" : "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-muted-foreground">테이블: {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>
|
||
);
|
||
};
|