feature/screen-management #95
|
|
@ -17,23 +17,28 @@ export async function getAdminMenus(
|
|||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.info("=== 관리자 메뉴 목록 조회 시작 ===");
|
||||
logger.info("=== 메뉴 목록 조회 시작 ===");
|
||||
|
||||
// 현재 로그인한 사용자의 회사 코드와 로케일 가져오기
|
||||
const userCompanyCode = req.user?.companyCode || "ILSHIN";
|
||||
const userLang = (req.query.userLang as string) || "ko";
|
||||
const menuType = req.query.menuType as string | undefined; // menuType 파라미터 추가
|
||||
|
||||
logger.info(`사용자 회사 코드: ${userCompanyCode}`);
|
||||
logger.info(`사용자 로케일: ${userLang}`);
|
||||
logger.info(`메뉴 타입: ${menuType || "전체"}`);
|
||||
|
||||
const paramMap = {
|
||||
userCompanyCode,
|
||||
userLang,
|
||||
menuType, // menuType 추가
|
||||
};
|
||||
|
||||
const menuList = await AdminService.getAdminMenuList(paramMap);
|
||||
|
||||
logger.info(`관리자 메뉴 조회 결과: ${menuList.length}개`);
|
||||
logger.info(
|
||||
`메뉴 조회 결과: ${menuList.length}개 (타입: ${menuType || "전체"})`
|
||||
);
|
||||
if (menuList.length > 0) {
|
||||
logger.info("첫 번째 메뉴:", menuList[0]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ export class AdminService {
|
|||
try {
|
||||
logger.info("AdminService.getAdminMenuList 시작 - 파라미터:", paramMap);
|
||||
|
||||
const { userLang = "ko" } = paramMap;
|
||||
const { userLang = "ko", menuType } = paramMap;
|
||||
|
||||
// menuType에 따른 WHERE 조건 생성
|
||||
const menuTypeCondition =
|
||||
menuType !== undefined ? `MENU_TYPE = ${parseInt(menuType)}` : "1 = 1";
|
||||
|
||||
// 기존 Java의 selectAdminMenuList 쿼리를 Raw Query로 포팅
|
||||
// WITH RECURSIVE 쿼리 구현
|
||||
|
|
@ -91,7 +95,7 @@ export class AdminService {
|
|||
MENU.MENU_DESC
|
||||
)
|
||||
FROM MENU_INFO MENU
|
||||
WHERE MENU_TYPE = 0
|
||||
WHERE ${menuTypeCondition}
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM MENU_INFO parent_menu
|
||||
WHERE parent_menu.OBJID = MENU.PARENT_OBJ_ID
|
||||
|
|
@ -159,11 +163,7 @@ export class AdminService {
|
|||
)
|
||||
SELECT
|
||||
LEVEL AS LEV,
|
||||
CASE MENU_TYPE
|
||||
WHEN '0' THEN 'admin'
|
||||
WHEN '1' THEN 'user'
|
||||
ELSE ''
|
||||
END AS MENU_TYPE,
|
||||
CAST(MENU_TYPE AS TEXT) AS MENU_TYPE,
|
||||
A.OBJID,
|
||||
A.PARENT_OBJ_ID,
|
||||
A.MENU_NAME_KOR,
|
||||
|
|
@ -193,7 +193,9 @@ export class AdminService {
|
|||
[userLang]
|
||||
);
|
||||
|
||||
logger.info(`관리자 메뉴 목록 조회 결과: ${menuList.length}개`);
|
||||
logger.info(
|
||||
`메뉴 목록 조회 결과: ${menuList.length}개 (menuType: ${menuType || "전체"})`
|
||||
);
|
||||
if (menuList.length > 0) {
|
||||
logger.info("첫 번째 메뉴:", menuList[0]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -240,8 +240,8 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
const isAdminMode = pathname.startsWith("/admin") || searchParams.get("mode") === "admin";
|
||||
|
||||
// 현재 모드에 따라 표시할 메뉴 결정
|
||||
// 관리자 모드에서는 관리자 메뉴 + 사용자 메뉴(툴 생성 메뉴 포함)를 모두 표시
|
||||
const currentMenus = isAdminMode ? [...adminMenus, ...userMenus] : userMenus;
|
||||
// 관리자 모드에서는 관리자 메뉴만, 사용자 모드에서는 사용자 메뉴만 표시
|
||||
const currentMenus = isAdminMode ? adminMenus : userMenus;
|
||||
|
||||
// 메뉴 토글 함수
|
||||
const toggleMenu = (menuId: string) => {
|
||||
|
|
@ -324,7 +324,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
<div
|
||||
className={`group flex h-10 cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 ease-in-out ${
|
||||
pathname === menu.url
|
||||
? "border-l-4 border-primary bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900"
|
||||
? "border-primary border-l-4 bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900"
|
||||
: isExpanded
|
||||
? "bg-slate-100 text-slate-900"
|
||||
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
|
||||
|
|
@ -352,7 +352,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
key={child.id}
|
||||
className={`flex cursor-pointer items-center rounded-lg px-3 py-2 text-sm transition-colors hover:cursor-pointer ${
|
||||
pathname === child.url
|
||||
? "border-l-4 border-primary bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900"
|
||||
? "border-primary border-l-4 bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900"
|
||||
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
|
||||
}`}
|
||||
onClick={() => handleMenuClick(child)}
|
||||
|
|
@ -376,7 +376,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
||||
<div className="border-primary mb-4 h-8 w-8 animate-spin rounded-full border-4 border-t-transparent"></div>
|
||||
<p>로딩중...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -423,7 +423,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
className={`flex w-full items-center justify-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 hover:cursor-pointer ${
|
||||
isAdminMode
|
||||
? "border border-orange-200 bg-orange-50 text-orange-700 hover:bg-orange-100"
|
||||
: "border border-primary/20 bg-accent text-blue-700 hover:bg-primary/20"
|
||||
: "border-primary/20 bg-accent hover:bg-primary/20 border text-blue-700"
|
||||
}`}
|
||||
>
|
||||
{isAdminMode ? (
|
||||
|
|
@ -486,7 +486,7 @@ export function AppLayout({ children }: AppLayoutProps) {
|
|||
fallback={
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
||||
<div className="border-primary mb-4 h-8 w-8 animate-spin rounded-full border-4 border-t-transparent"></div>
|
||||
<p>로딩중...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState, useRef } 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Search, X } from "lucide-react";
|
||||
import { screenApi, tableTypeApi } from "@/lib/api/screen";
|
||||
import { ScreenDefinition } from "@/types/screen";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
|
|
@ -24,6 +26,8 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
|||
const [description, setDescription] = useState("");
|
||||
const [tables, setTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [tableSearchTerm, setTableSearchTerm] = useState("");
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
// 화면 코드 자동 생성
|
||||
const generateCode = async () => {
|
||||
try {
|
||||
|
|
@ -65,6 +69,16 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
|||
return screenName.trim().length > 0 && screenCode.trim().length > 0 && tableName.trim().length > 0;
|
||||
}, [screenName, screenCode, tableName]);
|
||||
|
||||
// 테이블 필터링
|
||||
const filteredTables = useMemo(() => {
|
||||
if (!tableSearchTerm) return tables;
|
||||
const searchLower = tableSearchTerm.toLowerCase();
|
||||
return tables.filter(
|
||||
(table) =>
|
||||
table.displayName.toLowerCase().includes(searchLower) || table.tableName.toLowerCase().includes(searchLower),
|
||||
);
|
||||
}, [tables, tableSearchTerm]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!isValid || submitting) return;
|
||||
try {
|
||||
|
|
@ -124,19 +138,82 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
|||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tableName">테이블</Label>
|
||||
<select
|
||||
id="tableName"
|
||||
className="w-full rounded-md border px-3 py-2 text-sm"
|
||||
<Select
|
||||
value={tableName}
|
||||
onChange={(e) => setTableName(e.target.value)}
|
||||
onValueChange={setTableName}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
// Select가 열릴 때 검색창에 포커스
|
||||
setTimeout(() => {
|
||||
searchInputRef.current?.focus();
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">테이블 선택...</option>
|
||||
{tables.map((t) => (
|
||||
<option key={t.tableName} value={t.tableName}>
|
||||
{t.displayName} ({t.tableName})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="테이블을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-80">
|
||||
{/* 검색 입력 필드 */}
|
||||
<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="테이블명으로 검색..."
|
||||
value={tableSearchTerm}
|
||||
autoFocus
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
setTableSearchTerm(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"
|
||||
/>
|
||||
{tableSearchTerm && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setTableSearchTerm("");
|
||||
}}
|
||||
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-60 overflow-y-auto">
|
||||
{filteredTables.length === 0 ? (
|
||||
<div className="px-2 py-6 text-center text-sm text-gray-500">
|
||||
{tableSearchTerm ? `"${tableSearchTerm}"에 대한 검색 결과가 없습니다` : "테이블이 없습니다"}
|
||||
</div>
|
||||
) : (
|
||||
filteredTables.map((table) => (
|
||||
<SelectItem key={table.tableName} value={table.tableName}>
|
||||
{table.displayName} ({table.tableName})
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">설명</Label>
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export interface ApiResponse<T> {
|
|||
export const menuApi = {
|
||||
// 관리자 메뉴 목록 조회
|
||||
getAdminMenus: async (): Promise<ApiResponse<MenuItem[]>> => {
|
||||
const response = await apiClient.get("/admin/menus");
|
||||
const response = await apiClient.get("/admin/menus", { params: { menuType: "0" } });
|
||||
if (response.data.success && response.data.data && response.data.data.length > 0) {
|
||||
}
|
||||
return response.data;
|
||||
|
|
@ -86,7 +86,7 @@ export const menuApi = {
|
|||
|
||||
// 사용자 메뉴 목록 조회
|
||||
getUserMenus: async (): Promise<ApiResponse<MenuItem[]>> => {
|
||||
const response = await apiClient.get("/admin/user-menus");
|
||||
const response = await apiClient.get("/admin/menus", { params: { menuType: "1" } });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue