화면관리 수정

This commit is contained in:
kjs
2025-10-13 19:18:01 +09:00
parent 2754be3250
commit dadd49b98f
6 changed files with 228 additions and 67 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import {
Dialog,
DialogContent,
@@ -12,7 +12,6 @@ import {
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 { toast } from "sonner";
import { Search, Monitor, Settings, X, Plus } from "lucide-react";
@@ -46,36 +45,51 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
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 normalizedAdminMenus = adminMenus.map((menu: any) => ({
// 사용자 메뉴 가져오기
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: "0", // 관리자 메뉴
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,
}));
});
// console.log("로드된 관리자 메뉴 목록:", {
// total: normalizedAdminMenus.length,
// sample: normalizedAdminMenus.slice(0, 3),
// 관리자 메뉴 정규화
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(normalizedAdminMenus);
setMenus(allMenus);
} catch (error) {
// console.error("메뉴 목록 로드 실패:", error);
toast.error("메뉴 목록을 불러오는데 실패했습니다.");
@@ -244,8 +258,8 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
);
});
// 메뉴 옵션 생성 (계층 구조 표시)
const getMenuOptions = (): JSX.Element[] => {
// 메뉴 옵션 생성 (계층 구조 표시, 타입별 그룹화)
const getMenuOptions = (): React.ReactNode[] => {
if (loading) {
return [
<SelectItem key="loading" value="loading" disabled>
@@ -262,19 +276,58 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
];
}
return filteredMenus
.filter((menu) => menu.objid && menu.objid.toString().trim() !== "") // objid가 유효한 메뉴만 필터링
.map((menu) => {
const indent = " ".repeat(Math.max(0, menu.lev || 0));
const menuId = menu.objid!.toString(); // 이미 필터링했으므로 non-null assertion 사용
// 관리자 메뉴와 사용자 메뉴 분리
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() !== "",
);
return (
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>
</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 (
@@ -348,9 +401,9 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
.
</DialogDescription>
{screenInfo && (
<div className="mt-2 rounded-lg border bg-accent p-3">
<div className="bg-accent mt-2 rounded-lg border p-3">
<div className="flex items-center gap-2">
<Monitor className="h-4 w-4 text-primary" />
<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}
@@ -365,29 +418,51 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
{/* 메뉴 선택 (검색 기능 포함) */}
<div>
<Label htmlFor="menu-select"> </Label>
<Select value={selectedMenuId} onValueChange={handleMenuSelect} disabled={loading}>
<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">
<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
<input
ref={searchInputRef}
type="text"
placeholder="메뉴명, URL, 설명으로 검색..."
value={searchTerm}
autoFocus
onChange={(e) => {
e.stopPropagation(); // 이벤트 전파 방지
e.stopPropagation();
setSearchTerm(e.target.value);
}}
onKeyDown={(e) => {
e.stopPropagation(); // 키보드 이벤트 전파 방지
// 이벤트가 Select로 전파되지 않도록 완전 차단
e.stopPropagation();
}}
onClick={(e) => {
e.stopPropagation(); // 클릭 이벤트 전파 방지
}}
className="h-8 pr-8 pl-10 text-sm"
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
@@ -396,7 +471,7 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
e.stopPropagation();
setSearchTerm("");
}}
className="absolute top-1/2 right-2 h-4 w-4 -translate-y-1/2 transform text-gray-400 hover:text-muted-foreground"
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>
@@ -416,12 +491,14 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
<div className="flex-1">
<div className="flex items-center gap-2">
<h4 className="font-medium">{selectedMenu.menu_name_kor}</h4>
<Badge variant="default"></Badge>
<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="mt-1 space-y-1 text-sm text-muted-foreground">
<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>}
@@ -494,7 +571,7 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
<div className="space-y-4">
{/* 기존 화면 목록 */}
<div className="rounded-lg border bg-destructive/10 p-3">
<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) => (