ERP-node/frontend/components/admin/MenuFormModal.tsx

1297 lines
52 KiB
TypeScript
Raw Normal View History

2025-08-21 09:41:46 +09:00
"use client";
import React, { useState, useEffect } from "react";
import { MenuItem, MenuFormData, menuApi, LangKey } from "@/lib/api/menu";
import { companyAPI } from "@/lib/api/company";
2025-10-23 15:06:00 +09:00
import { screenApi, menuScreenApi } from "@/lib/api/screen";
2025-08-21 09:41:46 +09:00
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
2025-11-05 18:08:51 +09:00
import {
2025-12-05 10:46:10 +09:00
Dialog,
DialogContent,
DialogHeader,
DialogTitle
} from "@/components/ui/dialog";
2025-08-21 09:41:46 +09:00
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
2025-08-21 09:41:46 +09:00
import { toast } from "sonner";
import { ChevronDown, Search } from "lucide-react";
2025-08-25 17:56:33 +09:00
import { MENU_MANAGEMENT_KEYS } from "@/lib/utils/multilang";
import { ScreenDefinition } from "@/types/screen";
2025-08-21 09:41:46 +09:00
interface Company {
company_code: string;
company_name: string;
status: string;
}
interface MenuFormModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
menuId?: string;
parentId?: string;
menuType?: string;
level?: number;
parentCompanyCode?: string;
2025-08-25 17:56:33 +09:00
// 다국어 텍스트 props 추가
uiTexts: Record<string, string>;
2025-08-21 09:41:46 +09:00
}
export const MenuFormModal: React.FC<MenuFormModalProps> = ({
isOpen,
onClose,
onSuccess,
menuId,
parentId,
menuType,
level,
parentCompanyCode,
2025-08-25 17:56:33 +09:00
uiTexts,
2025-08-21 09:41:46 +09:00
}) => {
// console.log("🎯 MenuFormModal 렌더링 - Props:", {
// isOpen,
// menuId,
// parentId,
// menuType,
// level,
// parentCompanyCode,
// });
2025-08-25 17:56:33 +09:00
// 다국어 텍스트 가져오기 함수
const getText = (key: string, fallback?: string): string => {
return uiTexts[key] || fallback || key;
};
// console.log("🔍 MenuFormModal 컴포넌트 마운트됨");
2025-08-21 09:41:46 +09:00
const [formData, setFormData] = useState<MenuFormData>({
parentObjId: parentId || "0",
menuNameKor: "",
menuUrl: "",
menuDesc: "",
seq: 1,
menuType: "1",
status: "ACTIVE",
companyCode: parentCompanyCode || "none",
langKey: "",
2025-08-21 09:41:46 +09:00
});
// 화면 할당 관련 상태
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
const [urlType, setUrlType] = useState<"direct" | "screen" | "dashboard" | "pop">("screen");
const [selectedScreen, setSelectedScreen] = useState<ScreenDefinition | null>(null);
const [screens, setScreens] = useState<ScreenDefinition[]>([]);
const [screenSearchText, setScreenSearchText] = useState("");
const [isScreenDropdownOpen, setIsScreenDropdownOpen] = useState(false);
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
// POP 화면 할당 관련 상태
const [selectedPopScreen, setSelectedPopScreen] = useState<ScreenDefinition | null>(null);
const [popScreenSearchText, setPopScreenSearchText] = useState("");
const [isPopScreenDropdownOpen, setIsPopScreenDropdownOpen] = useState(false);
const [isPopLanding, setIsPopLanding] = useState(false);
const [hasOtherPopLanding, setHasOtherPopLanding] = useState(false);
// 대시보드 할당 관련 상태
const [selectedDashboard, setSelectedDashboard] = useState<any | null>(null);
const [dashboards, setDashboards] = useState<any[]>([]);
const [dashboardSearchText, setDashboardSearchText] = useState("");
const [isDashboardDropdownOpen, setIsDashboardDropdownOpen] = useState(false);
2025-08-21 09:41:46 +09:00
const [loading, setLoading] = useState(false);
const [isEdit, setIsEdit] = useState(false);
const [companies, setCompanies] = useState<Company[]>([]);
const [langKeys, setLangKeys] = useState<LangKey[]>([]);
const [isLangKeyDropdownOpen, setIsLangKeyDropdownOpen] = useState(false);
const [langKeySearchText, setLangKeySearchText] = useState("");
// 화면 목록 로드
const loadScreens = async () => {
try {
const response = await screenApi.getScreens({ size: 1000 }); // 모든 화면 가져오기
setScreens(response.data);
console.log("✅ 화면 목록 로드 완료:", response.data.length);
} catch (error) {
console.error("❌ 화면 목록 로드 실패:", error);
toast.error("화면 목록을 불러오는데 실패했습니다.");
}
};
// 대시보드 목록 로드
const loadDashboards = async () => {
try {
const { dashboardApi } = await import("@/lib/api/dashboard");
const response = await dashboardApi.getMyDashboards();
setDashboards(response.dashboards || []);
console.log("✅ 대시보드 목록 로드 완료:", response.dashboards?.length || 0);
} catch (error) {
console.error("❌ 대시보드 목록 로드 실패:", error);
toast.error("대시보드 목록을 불러오는데 실패했습니다.");
}
};
// 화면 선택 시 URL 자동 설정
const handleScreenSelect = (screen: ScreenDefinition) => {
// console.log("🖥️ 화면 선택 디버깅:", {
// screen,
// screenId: screen.screenId,
// screenIdType: typeof screen.screenId,
// legacyId: screen.id,
// allFields: Object.keys(screen),
// screenValues: Object.values(screen),
// });
// ScreenDefinition에서는 screenId 필드를 사용
const actualScreenId = screen.screenId || screen.id;
if (!actualScreenId) {
console.error("❌ 화면 ID를 찾을 수 없습니다:", screen);
toast.error("화면 ID를 찾을 수 없습니다. 다른 화면을 선택해주세요.");
return;
}
setSelectedScreen(screen);
setIsScreenDropdownOpen(false);
// 실제 라우팅 패턴에 맞게 URL 생성: /screens/[screenId] (복수형)
// 관리자 메뉴인 경우 mode=admin 파라미터 추가
let screenUrl = `/screens/${actualScreenId}`;
// 현재 메뉴 타입이 관리자인지 확인 (0 또는 "admin")
const isAdminMenu = menuType === "0" || menuType === "admin" || formData.menuType === "0";
if (isAdminMenu) {
screenUrl += "?mode=admin";
}
setFormData((prev) => ({
...prev,
menuUrl: screenUrl,
screenCode: screen.screenCode, // 화면 코드도 함께 저장
}));
// console.log("🖥️ 화면 선택 완료:", {
// screenId: screen.screenId,
// legacyId: screen.id,
// actualScreenId,
// screenName: screen.screenName,
// menuType: menuType,
// formDataMenuType: formData.menuType,
// isAdminMenu,
// generatedUrl: screenUrl,
// });
};
// 대시보드 선택 시 URL 자동 설정
const handleDashboardSelect = (dashboard: any) => {
setSelectedDashboard(dashboard);
setIsDashboardDropdownOpen(false);
// 대시보드 URL 생성
let dashboardUrl = `/dashboard/${dashboard.id}`;
// 현재 메뉴 타입이 관리자인지 확인 (0 또는 "admin")
const isAdminMenu = menuType === "0" || menuType === "admin" || formData.menuType === "0";
if (isAdminMenu) {
dashboardUrl += "?mode=admin";
}
setFormData((prev) => ({ ...prev, menuUrl: dashboardUrl }));
toast.success(`대시보드가 선택되었습니다: ${dashboard.title}`);
};
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
// POP 화면 선택 시 URL 자동 설정
const handlePopScreenSelect = (screen: ScreenDefinition) => {
const actualScreenId = screen.screenId || screen.id;
if (!actualScreenId) {
toast.error("화면 ID를 찾을 수 없습니다.");
return;
}
setSelectedPopScreen(screen);
setIsPopScreenDropdownOpen(false);
const popUrl = `/pop/screens/${actualScreenId}`;
setFormData((prev) => ({
...prev,
menuUrl: popUrl,
}));
};
// URL 타입 변경 시 처리
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
const handleUrlTypeChange = (type: "direct" | "screen" | "dashboard" | "pop") => {
// console.log("🔄 URL 타입 변경:", {
// from: urlType,
// to: type,
// currentSelectedScreen: selectedScreen?.screenName,
// currentUrl: formData.menuUrl,
// });
setUrlType(type);
if (type === "direct") {
setSelectedScreen(null);
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
setSelectedPopScreen(null);
setFormData((prev) => ({
...prev,
menuUrl: "",
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
screenCode: undefined,
}));
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
} else if (type === "pop") {
setSelectedScreen(null);
if (selectedPopScreen) {
const actualScreenId = selectedPopScreen.screenId || selectedPopScreen.id;
setFormData((prev) => ({
...prev,
menuUrl: `/pop/screens/${actualScreenId}`,
}));
} else {
setFormData((prev) => ({
...prev,
menuUrl: "",
}));
}
} else if (type === "screen") {
setSelectedPopScreen(null);
if (selectedScreen) {
const actualScreenId = selectedScreen.screenId || selectedScreen.id;
let screenUrl = `/screens/${actualScreenId}`;
const isAdminMenu = menuType === "0" || menuType === "admin" || formData.menuType === "0";
if (isAdminMenu) {
screenUrl += "?mode=admin";
}
setFormData((prev) => ({
...prev,
menuUrl: screenUrl,
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
screenCode: selectedScreen.screenCode,
}));
} else {
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
setFormData((prev) => ({
...prev,
menuUrl: "",
screenCode: undefined,
}));
}
} else {
// dashboard
setSelectedScreen(null);
setSelectedPopScreen(null);
if (!selectedDashboard) {
setFormData((prev) => ({
...prev,
menuUrl: "",
screenCode: undefined,
}));
}
}
};
// loadMenuData 함수를 먼저 정의
2025-08-21 09:41:46 +09:00
const loadMenuData = async () => {
console.log("loadMenuData 호출됨 - menuId:", menuId);
if (!menuId) {
console.log("menuId가 없어서 loadMenuData 종료");
return;
}
try {
setLoading(true);
console.log("API 호출 시작 - menuId:", menuId);
// console.log("API URL:", `/admin/menus/${menuId}`);
2025-08-21 09:41:46 +09:00
const response = await menuApi.getMenuInfo(menuId);
console.log("메뉴 정보 조회 응답:", response);
console.log("응답 success:", response.success);
console.log("응답 data:", response.data);
console.log("응답 message:", response.message);
console.log("응답 errorCode:", response.errorCode);
if (response.success && response.data) {
const menu = response.data;
console.log("메뉴 데이터:", menu);
console.log("메뉴 데이터 키들:", Object.keys(menu));
// 대문자 키와 소문자 키 모두 처리
const menuType = menu.menu_type || menu.MENU_TYPE || "1";
const status = menu.status || menu.STATUS || "active";
const companyCode = menu.company_code || menu.COMPANY_CODE || "";
const langKey = menu.lang_key || menu.LANG_KEY || "";
// 메뉴 타입 변환 (admin/user -> 0/1)
let convertedMenuType = menuType;
if (menuType === "admin" || menuType === "0") {
convertedMenuType = "0";
} else if (menuType === "user" || menuType === "1") {
convertedMenuType = "1";
}
// 상태 변환 (active/inactive/inActive -> ACTIVE/INACTIVE)
let convertedStatus = status;
if (status === "active") {
convertedStatus = "ACTIVE";
} else if (status === "inactive" || status === "inActive") {
convertedStatus = "INACTIVE";
}
const menuUrl = menu.menu_url || menu.MENU_URL || "";
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
const isPopScreenUrl = menuUrl.startsWith("/pop/screens/");
const isScreenUrl = !isPopScreenUrl && menuUrl.startsWith("/screens/");
2025-08-21 09:41:46 +09:00
setFormData({
objid: menu.objid || menu.OBJID,
parentObjId: menu.parent_obj_id || menu.PARENT_OBJ_ID || "0",
menuNameKor: menu.menu_name_kor || menu.MENU_NAME_KOR || "",
menuUrl: menuUrl,
2025-08-21 09:41:46 +09:00
menuDesc: menu.menu_desc || menu.MENU_DESC || "",
seq: menu.seq || menu.SEQ || 1,
menuType: convertedMenuType,
status: convertedStatus,
companyCode: companyCode,
langKey: langKey, // 다국어 키 설정
});
// URL 타입 설정
if (isScreenUrl) {
setUrlType("screen");
// "/screens/123" 또는 "/screens/123?mode=admin" 형태에서 ID 추출
const screenId = menuUrl.match(/\/screens\/(\d+)/)?.[1];
if (screenId) {
// console.log("🔍 기존 메뉴에서 화면 ID 추출:", {
// menuUrl,
// screenId,
// hasAdminParam: menuUrl.includes("mode=admin"),
// currentScreensCount: screens.length,
// });
// 화면 설정 함수
const setScreenFromId = () => {
const screen = screens.find((s) => s.screenId.toString() === screenId || s.id?.toString() === screenId);
if (screen) {
setSelectedScreen(screen);
// console.log("🖥️ 기존 메뉴의 할당된 화면 설정:", {
// screen,
// originalUrl: menuUrl,
// hasAdminParam: menuUrl.includes("mode=admin"),
// });
return true;
} else {
// console.warn("⚠️ 해당 ID의 화면을 찾을 수 없음:", {
// screenId,
// availableScreens: screens.map((s) => ({ screenId: s.screenId, id: s.id, name: s.screenName })),
// });
return false;
}
};
// 화면 목록이 이미 있으면 즉시 설정, 없으면 로드 완료 대기
if (screens.length > 0) {
console.log("📋 화면 목록이 이미 로드됨 - 즉시 설정");
setScreenFromId();
} else {
console.log("⏳ 화면 목록 로드 대기 중...");
// 화면 ID를 저장해두고, 화면 목록 로드 완료 후 설정
setTimeout(() => {
console.log("🔄 재시도: 화면 목록 로드 후 설정");
setScreenFromId();
}, 500);
}
}
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
} else if (isPopScreenUrl) {
setUrlType("pop");
setSelectedScreen(null);
// [POP_LANDING] 태그 감지
const menuDesc = menu.menu_desc || menu.MENU_DESC || "";
setIsPopLanding(menuDesc.includes("[POP_LANDING]"));
const popScreenId = menuUrl.match(/\/pop\/screens\/(\d+)/)?.[1];
if (popScreenId) {
const setPopScreenFromId = () => {
const screen = screens.find((s) => s.screenId.toString() === popScreenId || s.id?.toString() === popScreenId);
if (screen) {
setSelectedPopScreen(screen);
}
};
if (screens.length > 0) {
setPopScreenFromId();
} else {
setTimeout(setPopScreenFromId, 500);
}
}
} else if (menuUrl.startsWith("/dashboard/")) {
setUrlType("dashboard");
setSelectedScreen(null);
} else {
setUrlType("direct");
setSelectedScreen(null);
}
// console.log("설정된 폼 데이터:", {
// objid: menu.objid || menu.OBJID,
// parentObjId: menu.parent_obj_id || menu.PARENT_OBJ_ID || "0",
// menuNameKor: menu.menu_name_kor || menu.MENU_NAME_KOR || "",
// menuUrl: menu.menu_url || menu.MENU_URL || "",
// menuDesc: menu.menu_desc || menu.MENU_DESC || "",
// seq: menu.seq || menu.SEQ || 1,
// menuType: convertedMenuType,
// status: convertedStatus,
// companyCode: companyCode,
// langKey: langKey,
// });
2025-08-21 09:41:46 +09:00
}
} catch (error: any) {
console.error("메뉴 정보 로딩 오류:", error);
// console.error("오류 상세 정보:", {
// message: error?.message,
// stack: error?.stack,
// response: error?.response,
// });
2025-08-25 17:56:33 +09:00
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_INFO));
2025-08-21 09:41:46 +09:00
} finally {
setLoading(false);
}
};
// useEffect를 loadMenuData 함수 정의 후로 이동
useEffect(() => {
console.log("🚀 MenuFormModal useEffect 실행됨!");
console.log("📋 useEffect 파라미터:", { menuId, parentId, menuType });
console.log("MenuFormModal useEffect - menuId:", menuId, "parentId:", parentId, "menuType:", menuType);
if (menuId) {
console.log("메뉴 수정 모드 - menuId:", menuId);
setIsEdit(true);
loadMenuData();
} else {
console.log("메뉴 등록 모드 - parentId:", parentId, "menuType:", menuType);
setIsEdit(false);
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
setIsPopLanding(false);
// 메뉴 타입 변환 (0 -> 0, 1 -> 1, admin -> 0, user -> 1)
let defaultMenuType = "1"; // 기본값은 사용자
if (menuType === "0" || menuType === "admin") {
defaultMenuType = "0"; // 관리자
} else if (menuType === "1" || menuType === "user") {
defaultMenuType = "1"; // 사용자
}
setFormData({
parentObjId: parentId || "0",
menuNameKor: "",
menuUrl: "",
menuDesc: "",
seq: 1,
menuType: defaultMenuType,
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
status: "ACTIVE",
companyCode: parentCompanyCode || "none",
langKey: "",
});
// console.log("메뉴 등록 기본값 설정:", {
// parentObjId: parentId || "0",
// menuType: defaultMenuType,
// status: "ACTIVE",
// companyCode: "",
// langKey: "",
// });
}
}, [menuId, parentId, menuType]);
// 강제로 useEffect 실행시키기 위한 별도 useEffect
useEffect(() => {
console.log("🔧 강제 useEffect 실행 - 컴포넌트 마운트됨");
console.log("🔧 현재 props:", { isOpen, menuId, parentId, menuType });
// isOpen이 true일 때만 실행
if (isOpen && menuId) {
console.log("🔧 모달이 열렸고 menuId가 있음 - 강제 실행");
// 약간의 지연 후 실행
setTimeout(() => {
console.log("🔧 setTimeout으로 loadMenuData 실행");
loadMenuData();
}, 100);
}
}, [isOpen]); // isOpen만 의존성으로 설정
// 회사 목록 로드
useEffect(() => {
if (isOpen) {
loadCompanies();
}
}, [isOpen]);
// 다국어 키 목록 로드
useEffect(() => {
if (isOpen && formData.companyCode) {
loadLangKeys();
}
}, [isOpen, formData.companyCode]);
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
// POP 기본 화면 중복 체크: 같은 부모 하위에 이미 [POP_LANDING]이 있는 다른 메뉴가 있는지 확인
useEffect(() => {
if (!isOpen) return;
const checkOtherPopLanding = async () => {
try {
const res = await menuApi.getPopMenus();
if (res.success && res.data?.landingMenu) {
const landingObjId = res.data.landingMenu.objid?.toString();
const currentObjId = formData.objid?.toString();
// 현재 수정 중인 메뉴가 아닌 다른 메뉴에 [POP_LANDING]이 있으면 중복
setHasOtherPopLanding(!!landingObjId && landingObjId !== currentObjId);
} else {
setHasOtherPopLanding(false);
}
} catch {
setHasOtherPopLanding(false);
}
};
if (urlType === "pop") {
checkOtherPopLanding();
}
}, [isOpen, urlType, formData.objid]);
// 화면 목록 및 대시보드 목록 로드
useEffect(() => {
if (isOpen) {
loadScreens();
loadDashboards();
}
}, [isOpen]);
// 화면 목록 로드 완료 후 기존 메뉴의 할당된 화면 설정
useEffect(() => {
if (screens.length > 0 && isEdit && formData.menuUrl && urlType === "screen") {
const menuUrl = formData.menuUrl;
if (menuUrl.startsWith("/screens/")) {
const screenId = menuUrl.match(/\/screens\/(\d+)/)?.[1];
if (screenId && !selectedScreen) {
console.log("🔄 화면 목록 로드 완료 - 기존 할당 화면 자동 설정");
const screen = screens.find((s) => s.screenId.toString() === screenId || s.id?.toString() === screenId);
if (screen) {
setSelectedScreen(screen);
// console.log("✅ 기존 메뉴의 할당된 화면 자동 설정 완료:", {
// screenId,
// screenName: screen.screenName,
// menuUrl,
// });
}
}
}
}
}, [screens, isEdit, formData.menuUrl, urlType, selectedScreen]);
// 대시보드 목록 로드 완료 후 기존 메뉴의 할당된 대시보드 설정
useEffect(() => {
if (dashboards.length > 0 && isEdit && formData.menuUrl && urlType === "dashboard") {
const menuUrl = formData.menuUrl;
if (menuUrl.startsWith("/dashboard/")) {
const dashboardId = menuUrl.replace("/dashboard/", "");
if (dashboardId && !selectedDashboard) {
console.log("🔄 대시보드 목록 로드 완료 - 기존 할당 대시보드 자동 설정");
const dashboard = dashboards.find((d) => d.id === dashboardId);
if (dashboard) {
setSelectedDashboard(dashboard);
}
}
}
}
}, [dashboards, isEdit, formData.menuUrl, urlType, selectedDashboard]);
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
// POP 화면 목록 로드 완료 후 기존 할당 설정
useEffect(() => {
if (screens.length > 0 && isEdit && formData.menuUrl && urlType === "pop") {
const menuUrl = formData.menuUrl;
if (menuUrl.startsWith("/pop/screens/")) {
const popScreenId = menuUrl.match(/\/pop\/screens\/(\d+)/)?.[1];
if (popScreenId && !selectedPopScreen) {
const screen = screens.find((s) => s.screenId.toString() === popScreenId || s.id?.toString() === popScreenId);
if (screen) {
setSelectedPopScreen(screen);
}
}
}
}
}, [screens, isEdit, formData.menuUrl, urlType, selectedPopScreen]);
// 드롭다운 외부 클릭 시 닫기
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Element;
if (!target.closest(".langkey-dropdown")) {
setIsLangKeyDropdownOpen(false);
setLangKeySearchText("");
}
if (!target.closest(".screen-dropdown")) {
setIsScreenDropdownOpen(false);
setScreenSearchText("");
}
if (!target.closest(".dashboard-dropdown")) {
setIsDashboardDropdownOpen(false);
setDashboardSearchText("");
}
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
if (!target.closest(".pop-screen-dropdown")) {
setIsPopScreenDropdownOpen(false);
setPopScreenSearchText("");
}
};
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
if (isLangKeyDropdownOpen || isScreenDropdownOpen || isDashboardDropdownOpen || isPopScreenDropdownOpen) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
}, [isLangKeyDropdownOpen, isScreenDropdownOpen, isDashboardDropdownOpen, isPopScreenDropdownOpen]);
const loadCompanies = async () => {
try {
const companyList = await companyAPI.getList({ status: "active" });
setCompanies(companyList);
} catch (error) {
console.error("회사 목록 로딩 오류:", error);
2025-08-25 17:56:33 +09:00
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_COMPANY_LIST));
}
};
const loadLangKeys = async () => {
console.log("🔤 다국어 키 목록 조회 시작 - companyCode:", formData.companyCode);
try {
const response = await menuApi.getLangKeys({
companyCode: formData.companyCode === "none" ? "*" : formData.companyCode,
});
if (response.success && response.data) {
// 활성화된 다국어 키만 필터링
const activeKeys = response.data.filter((key) => key.isActive === "Y");
console.log("🔤 다국어 키 목록 조회 성공:", activeKeys.length, "개 (활성화된 키)");
setLangKeys(activeKeys);
}
} catch (error) {
console.error("❌ 다국어 키 목록 로딩 오류:", error);
2025-08-25 17:56:33 +09:00
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_LANG_KEY_LIST));
setLangKeys([]);
}
};
2025-08-21 09:41:46 +09:00
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.menuNameKor.trim()) {
2025-08-25 17:56:33 +09:00
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_MENU_NAME_REQUIRED));
2025-08-21 09:41:46 +09:00
return;
}
if (!formData.companyCode) {
2025-08-25 17:56:33 +09:00
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_COMPANY_REQUIRED));
2025-08-21 09:41:46 +09:00
return;
}
try {
setLoading(true);
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
// POP 기본 화면 태그 처리
let finalMenuDesc = formData.menuDesc;
if (urlType === "pop") {
const descWithoutTag = finalMenuDesc.replace(/\[POP_LANDING\]/g, "").trim();
finalMenuDesc = isPopLanding ? `${descWithoutTag} [POP_LANDING]`.trim() : descWithoutTag;
}
2025-08-21 09:41:46 +09:00
// 백엔드에 전송할 데이터 변환
const submitData = {
...formData,
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
menuDesc: finalMenuDesc,
2025-08-21 09:41:46 +09:00
status: formData.status.toLowerCase(),
};
console.log("저장할 데이터:", submitData);
let response;
if (isEdit && menuId) {
// 수정 모드: updateMenu API 호출
console.log("🔧 메뉴 수정 API 호출:", menuId);
response = await menuApi.updateMenu(menuId, submitData);
} else {
// 추가 모드: saveMenu API 호출
console.log(" 메뉴 추가 API 호출");
response = await menuApi.saveMenu(submitData);
}
2025-08-21 09:41:46 +09:00
if (response.success) {
2025-10-23 15:06:00 +09:00
// 화면 할당이 있는 경우 추가 처리
if (urlType === "screen" && selectedScreen) {
try {
// menuId는 response에서 반환되거나 기존 menuId 사용
const targetMenuId = menuId || response.data?.objid;
const menuObjid = parseInt(targetMenuId?.toString() || "0");
if (menuObjid > 0) {
console.log("📋 화면-메뉴 관계 테이블 업데이트 시작:", {
screenId: selectedScreen.screenId,
menuObjid,
});
// 1. 기존 할당된 화면들 먼저 조회
try {
const existingScreens = await menuScreenApi.getScreensByMenu(menuObjid);
console.log("📋 기존 할당된 화면:", existingScreens.length, "개");
// 2. 기존 화면들 모두 제거
for (const existingScreen of existingScreens) {
try {
await menuScreenApi.unassignScreenFromMenu(existingScreen.screenId, menuObjid);
console.log(`✅ 기존 화면 제거 완료: ${existingScreen.screenName}`);
} catch (unassignError) {
console.warn(`⚠️ 기존 화면 제거 실패: ${existingScreen.screenName}`, unassignError);
}
}
} catch (getError) {
console.warn("⚠️ 기존 화면 조회 실패 (계속 진행):", getError);
}
// 3. 새 화면 할당
await menuScreenApi.assignScreenToMenu(selectedScreen.screenId, menuObjid);
console.log("✅ 새 화면 할당 완료");
}
} catch (assignError) {
console.error("❌ 화면-메뉴 관계 테이블 할당 실패:", assignError);
// 할당 실패는 경고만 하고 메뉴 저장은 성공으로 처리
toast.warning("메뉴는 저장되었으나 화면 할당에 실패했습니다.");
}
}
2025-08-21 09:41:46 +09:00
toast.success(response.message);
onSuccess();
onClose();
} else {
toast.error(response.message);
}
} catch (error) {
console.error("메뉴 저장/수정 실패:", error);
2025-08-25 17:56:33 +09:00
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_FAILED));
2025-08-21 09:41:46 +09:00
} finally {
setLoading(false);
}
};
const handleInputChange = (field: keyof MenuFormData, value: string | number) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
// 선택된 다국어 키 정보 가져오기
const getSelectedLangKeyInfo = () => {
if (!formData.langKey) return null;
return langKeys.find((key) => key.langKey === formData.langKey);
};
const selectedLangKeyInfo = getSelectedLangKeyInfo();
2025-08-25 17:22:20 +09:00
// 전역 사용자 로케일 가져오기
const getCurrentUserLang = () => {
return (window as any).__GLOBAL_USER_LANG || localStorage.getItem("userLocale") || "KR";
};
2025-08-21 09:41:46 +09:00
return (
2025-12-05 10:46:10 +09:00
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>
2025-08-21 09:41:46 +09:00
{isEdit
2025-08-25 17:56:33 +09:00
? getText(MENU_MANAGEMENT_KEYS.MODAL_MENU_MODIFY_TITLE)
: getText(MENU_MANAGEMENT_KEYS.MODAL_MENU_REGISTER_TITLE)}
2025-12-05 10:46:10 +09:00
</DialogTitle>
</DialogHeader>
2025-08-21 09:41:46 +09:00
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
2025-08-25 17:56:33 +09:00
<Label htmlFor="menuType">{getText(MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE)}</Label>
2025-08-21 09:41:46 +09:00
<Select value={formData.menuType} onValueChange={(value) => handleInputChange("menuType", value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
2025-08-25 17:56:33 +09:00
<SelectItem value="0">{getText(MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE_ADMIN)}</SelectItem>
<SelectItem value="1">{getText(MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE_USER)}</SelectItem>
2025-08-21 09:41:46 +09:00
</SelectContent>
</Select>
</div>
<div className="space-y-2">
2025-08-25 17:56:33 +09:00
<Label htmlFor="status">{getText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_STATUS)}</Label>
2025-08-21 09:41:46 +09:00
<Select value={formData.status} onValueChange={(value) => handleInputChange("status", value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
2025-08-25 17:56:33 +09:00
<SelectItem value="ACTIVE">{getText(MENU_MANAGEMENT_KEYS.STATUS_ACTIVE)}</SelectItem>
<SelectItem value="INACTIVE">{getText(MENU_MANAGEMENT_KEYS.STATUS_INACTIVE)}</SelectItem>
2025-08-21 09:41:46 +09:00
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
2025-08-25 17:56:33 +09:00
<Label htmlFor="companyCode">{getText(MENU_MANAGEMENT_KEYS.FORM_COMPANY)} *</Label>
2025-08-21 09:41:46 +09:00
<Select
value={formData.companyCode}
onValueChange={(value) => handleInputChange("companyCode", value)}
disabled={!isEdit && level !== 1} // 수정 모드가 아니고 최상위 메뉴가 아니면 비활성화
>
<SelectTrigger>
2025-08-25 17:56:33 +09:00
<SelectValue placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_COMPANY_SELECT)} />
2025-08-21 09:41:46 +09:00
</SelectTrigger>
<SelectContent>
2025-08-25 17:56:33 +09:00
<SelectItem value="none">{getText(MENU_MANAGEMENT_KEYS.FORM_COMPANY_COMMON)}</SelectItem>
2025-08-21 09:41:46 +09:00
{companies.map((company) => (
<SelectItem key={company.company_code} value={company.company_code}>
{company.company_name}
</SelectItem>
))}
</SelectContent>
</Select>
{!isEdit && level !== 1 && (
2025-08-25 17:56:33 +09:00
<p className="text-xs text-gray-500">{getText(MENU_MANAGEMENT_KEYS.FORM_COMPANY_SUBMENU_NOTE)}</p>
2025-08-21 09:41:46 +09:00
)}
</div>
<div className="space-y-2">
2025-08-25 17:56:33 +09:00
<Label htmlFor="langKey">{getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY)}</Label>
2025-08-21 09:41:46 +09:00
<div className="langkey-dropdown relative">
<button
type="button"
onClick={() => setIsLangKeyDropdownOpen(!isLangKeyDropdownOpen)}
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
disabled={!formData.companyCode}
>
<span className={!formData.langKey ? "text-muted-foreground" : ""}>
2025-08-25 17:56:33 +09:00
{formData.langKey || getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SELECT)}
2025-08-21 09:41:46 +09:00
</span>
<svg
className={`h-4 w-4 transition-transform ${isLangKeyDropdownOpen ? "rotate-180" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{isLangKeyDropdownOpen && (
<div className="bg-popover text-popover-foreground absolute top-full left-0 z-50 mt-1 w-full rounded-md border shadow-md">
{/* 검색 입력 */}
<div className="border-b p-2">
<Input
2025-08-25 17:56:33 +09:00
placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SEARCH)}
2025-08-21 09:41:46 +09:00
value={langKeySearchText}
onChange={(e) => setLangKeySearchText(e.target.value)}
className="h-8 text-sm"
onClick={(e) => e.stopPropagation()}
/>
</div>
{/* 다국어 키 목록 */}
<div className="max-h-48 overflow-y-auto">
<div
className="hover:bg-accent hover:text-accent-foreground flex cursor-pointer items-center px-2 py-1.5 text-sm"
onClick={() => {
handleInputChange("langKey", "");
setIsLangKeyDropdownOpen(false);
setLangKeySearchText("");
}}
>
2025-08-25 17:56:33 +09:00
{getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_NONE)}
2025-08-21 09:41:46 +09:00
</div>
{langKeys
.filter(
(key) =>
key.langKey.toLowerCase().includes(langKeySearchText.toLowerCase()) ||
key.description.toLowerCase().includes(langKeySearchText.toLowerCase()),
)
.map((key) => (
<div
key={key.keyId}
className="hover:bg-accent hover:text-accent-foreground flex cursor-pointer flex-col px-2 py-1.5 text-sm"
onClick={() => {
handleInputChange("langKey", key.langKey);
setIsLangKeyDropdownOpen(false);
setLangKeySearchText("");
}}
>
<div className="font-medium">{key.langKey}</div>
{key.description && <div className="text-xs text-gray-500">{key.description}</div>}
</div>
))}
</div>
</div>
)}
</div>
{selectedLangKeyInfo && (
<p className="text-xs text-gray-500">
2025-08-25 17:56:33 +09:00
{getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SELECTED)
.replace("{key}", selectedLangKeyInfo.langKey)
.replace("{description}", selectedLangKeyInfo.description)}
2025-08-21 09:41:46 +09:00
</p>
)}
</div>
<div className="space-y-2">
2025-08-25 17:56:33 +09:00
<Label htmlFor="menuNameKor">{getText(MENU_MANAGEMENT_KEYS.FORM_MENU_NAME)} *</Label>
2025-08-21 09:41:46 +09:00
<Input
id="menuNameKor"
value={formData.menuNameKor}
onChange={(e) => handleInputChange("menuNameKor", e.target.value)}
2025-08-25 17:56:33 +09:00
placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_MENU_NAME_PLACEHOLDER)}
2025-08-21 09:41:46 +09:00
required
/>
</div>
<div className="space-y-2">
2025-08-25 17:56:33 +09:00
<Label htmlFor="menuUrl">{getText(MENU_MANAGEMENT_KEYS.FORM_MENU_URL)}</Label>
{/* URL 타입 선택 */}
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
<RadioGroup value={urlType} onValueChange={handleUrlTypeChange} className="mb-3 flex flex-wrap gap-x-6 gap-y-2">
<div className="flex items-center space-x-2">
<RadioGroupItem value="screen" id="screen" />
<Label htmlFor="screen" className="cursor-pointer">
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="dashboard" id="dashboard" />
<Label htmlFor="dashboard" className="cursor-pointer">
</Label>
</div>
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
<div className="flex items-center space-x-2">
<RadioGroupItem value="pop" id="pop" />
<Label htmlFor="pop" className="cursor-pointer">
POP
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="direct" id="direct" />
<Label htmlFor="direct" className="cursor-pointer">
URL
</Label>
</div>
</RadioGroup>
{/* 화면 할당 */}
{urlType === "screen" && (
<div className="space-y-2">
{/* 화면 선택 드롭다운 */}
<div className="relative">
<Button
type="button"
variant="outline"
onClick={() => setIsScreenDropdownOpen(!isScreenDropdownOpen)}
className="w-full justify-between"
>
<span className="text-left">
{selectedScreen ? selectedScreen.screenName : "화면을 선택하세요"}
</span>
<ChevronDown className="h-4 w-4" />
</Button>
{isScreenDropdownOpen && (
<div className="screen-dropdown absolute top-full right-0 left-0 z-50 mt-1 max-h-60 overflow-y-auto rounded-md border bg-white shadow-lg">
{/* 검색 입력 */}
<div className="sticky top-0 border-b bg-white p-2">
<div className="relative">
<Search className="absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Input
placeholder="화면 검색..."
value={screenSearchText}
onChange={(e) => setScreenSearchText(e.target.value)}
className="pl-8"
/>
</div>
</div>
{/* 화면 목록 */}
<div className="max-h-48 overflow-y-auto">
{screens
.filter(
(screen) =>
screen.screenName.toLowerCase().includes(screenSearchText.toLowerCase()) ||
screen.screenCode.toLowerCase().includes(screenSearchText.toLowerCase()),
)
.map((screen, index) => (
<div
key={`screen-${screen.screenId || screen.id || index}-${screen.screenCode || index}`}
onClick={() => handleScreenSelect(screen)}
className="cursor-pointer border-b px-3 py-2 last:border-b-0 hover:bg-gray-100"
>
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium">{screen.screenName}</div>
<div className="text-xs text-gray-500">{screen.screenCode}</div>
</div>
<div className="text-xs text-gray-400">ID: {screen.screenId || screen.id || "N/A"}</div>
</div>
</div>
))}
{screens.filter(
(screen) =>
screen.screenName.toLowerCase().includes(screenSearchText.toLowerCase()) ||
screen.screenCode.toLowerCase().includes(screenSearchText.toLowerCase()),
).length === 0 && <div className="px-3 py-2 text-sm text-gray-500"> .</div>}
</div>
</div>
)}
</div>
{/* 선택된 화면 정보 표시 */}
{selectedScreen && (
<div className="bg-accent rounded-md border p-3">
<div className="text-sm font-medium text-blue-900">{selectedScreen.screenName}</div>
<div className="text-primary text-xs">: {selectedScreen.screenCode}</div>
<div className="text-primary text-xs"> URL: {formData.menuUrl}</div>
</div>
)}
</div>
)}
{/* 대시보드 할당 */}
{urlType === "dashboard" && (
<div className="space-y-2">
{/* 대시보드 선택 드롭다운 */}
<div className="relative">
<Button
type="button"
variant="outline"
className="w-full justify-between"
onClick={() => setIsDashboardDropdownOpen(!isDashboardDropdownOpen)}
>
<span className="truncate">{selectedDashboard ? selectedDashboard.title : "대시보드 선택"}</span>
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
{/* 드롭다운 메뉴 */}
{isDashboardDropdownOpen && (
<div className="dashboard-dropdown absolute z-50 mt-1 max-h-60 w-full overflow-hidden rounded-md border bg-white shadow-lg">
{/* 검색창 */}
<div className="border-b p-2">
<div className="relative">
<Search className="absolute top-2.5 left-2 h-4 w-4 text-gray-400" />
<Input
type="text"
placeholder="대시보드 검색..."
value={dashboardSearchText}
onChange={(e) => setDashboardSearchText(e.target.value)}
className="pl-8"
/>
</div>
</div>
{/* 대시보드 목록 */}
<div className="max-h-48 overflow-y-auto">
{dashboards
.filter(
(dashboard) =>
dashboard.title.toLowerCase().includes(dashboardSearchText.toLowerCase()) ||
(dashboard.description &&
dashboard.description.toLowerCase().includes(dashboardSearchText.toLowerCase())),
)
.map((dashboard) => (
<div
key={dashboard.id}
onClick={() => handleDashboardSelect(dashboard)}
className="cursor-pointer border-b px-3 py-2 last:border-b-0 hover:bg-gray-100"
>
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium">{dashboard.title}</div>
{dashboard.description && (
<div className="text-xs text-gray-500">{dashboard.description}</div>
)}
</div>
</div>
</div>
))}
{dashboards.filter(
(dashboard) =>
dashboard.title.toLowerCase().includes(dashboardSearchText.toLowerCase()) ||
(dashboard.description &&
dashboard.description.toLowerCase().includes(dashboardSearchText.toLowerCase())),
).length === 0 && <div className="px-3 py-2 text-sm text-gray-500"> .</div>}
</div>
</div>
)}
</div>
{/* 선택된 대시보드 정보 표시 */}
{selectedDashboard && (
<div className="bg-accent rounded-md border p-3">
<div className="text-sm font-medium text-blue-900">{selectedDashboard.title}</div>
{selectedDashboard.description && (
<div className="text-primary text-xs">: {selectedDashboard.description}</div>
)}
<div className="text-primary text-xs"> URL: {formData.menuUrl}</div>
</div>
)}
</div>
)}
feat(pop): PC <-> POP 모드 전환 네비게이션 + POP 기본 화면(Landing) 기능 PC 모드에서 프로필 드롭다운을 통해 POP 화면으로 진입하고, POP에서 PC로 돌아오는 양방향 네비게이션을 구현한다. 기존 메뉴 시스템(menu_info)을 활용하여 POP 화면의 권한 제어와 회사별 관리가 가능하도록 한다. [백엔드: POP 메뉴 조회 API] - AdminService.getPopMenuList: L1 POP 메뉴(menu_desc [POP] 또는 menu_name_kor POP 포함) 하위의 active L2 메뉴 조회 - company_code 필터링 적용 (L1 + L2 모두) - landingMenu 반환: menu_desc에 [POP_LANDING] 태그가 있는 메뉴 - GET /admin/pop-menus 라우트 추가 [프론트: PC -> POP 진입] - AppLayout: handlePopModeClick 함수 추가 - landingMenu 있으면 해당 URL로 바로 이동 - 없으면 childMenus 수에 따라 단일 화면/대시보드/안내 분기 - UserDropdown: onPopModeClick prop + "POP 모드" 메뉴 항목 추가 - 사이드바 하단 + 모바일 헤더 프로필 드롭다운 2곳 모두 적용 [프론트: POP -> PC 복귀] - DashboardHeader: "PC 모드" 버튼 추가 (router.push "/") - POP 개별 화면 page.tsx: 상단 네비게이션 바 추가 (POP 대시보드 / PC 모드 버튼) [프론트: POP 대시보드 동적 메뉴] - PopDashboard: 하드코딩 MENU_ITEMS -> menuApi.getPopMenus() API 조회 - API 실패 시 하드코딩 fallback 유지 [프론트: POP 기본 화면 설정 (MenuFormModal)] - L2 POP 화면 수정 시 "POP 기본 화면으로 설정" 체크박스 추가 - 체크 시 menu_desc에 [POP_LANDING] 태그 자동 추가/제거 - 회사당 1개만 설정 가능 (다른 메뉴에 이미 설정 시 비활성화) [API 타입] - PopMenuItem, PopMenuResponse(landingMenu 포함) 인터페이스 추가 - menuApi.getPopMenus() 함수 추가
2026-03-09 12:16:26 +09:00
{/* POP 화면 할당 */}
{urlType === "pop" && (
<div className="space-y-2">
<div className="relative">
<Button
type="button"
variant="outline"
onClick={() => setIsPopScreenDropdownOpen(!isPopScreenDropdownOpen)}
className="w-full justify-between"
>
<span className="text-left">
{selectedPopScreen ? selectedPopScreen.screenName : "POP 화면을 선택하세요"}
</span>
<ChevronDown className="h-4 w-4" />
</Button>
{isPopScreenDropdownOpen && (
<div className="pop-screen-dropdown absolute top-full right-0 left-0 z-50 mt-1 max-h-60 overflow-y-auto rounded-md border bg-white shadow-lg">
<div className="sticky top-0 border-b bg-white p-2">
<div className="relative">
<Search className="absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Input
placeholder="POP 화면 검색..."
value={popScreenSearchText}
onChange={(e) => setPopScreenSearchText(e.target.value)}
className="pl-8"
/>
</div>
</div>
<div className="max-h-48 overflow-y-auto">
{screens
.filter(
(screen) =>
screen.screenName.toLowerCase().includes(popScreenSearchText.toLowerCase()) ||
screen.screenCode.toLowerCase().includes(popScreenSearchText.toLowerCase()),
)
.map((screen, index) => (
<div
key={`pop-screen-${screen.screenId || screen.id || index}-${screen.screenCode || index}`}
onClick={() => handlePopScreenSelect(screen)}
className="cursor-pointer border-b px-3 py-2 last:border-b-0 hover:bg-gray-100"
>
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium">{screen.screenName}</div>
<div className="text-xs text-gray-500">{screen.screenCode}</div>
</div>
<div className="text-xs text-gray-400">ID: {screen.screenId || screen.id || "N/A"}</div>
</div>
</div>
))}
{screens.filter(
(screen) =>
screen.screenName.toLowerCase().includes(popScreenSearchText.toLowerCase()) ||
screen.screenCode.toLowerCase().includes(popScreenSearchText.toLowerCase()),
).length === 0 && <div className="px-3 py-2 text-sm text-gray-500"> .</div>}
</div>
</div>
)}
</div>
{selectedPopScreen && (
<div className="bg-accent rounded-md border p-3">
<div className="text-sm font-medium text-blue-900">{selectedPopScreen.screenName}</div>
<div className="text-primary text-xs">: {selectedPopScreen.screenCode}</div>
<div className="text-primary text-xs"> URL: {formData.menuUrl}</div>
</div>
)}
{/* POP 기본 화면 설정 */}
<div className="flex items-center space-x-2 rounded-md border p-3">
<input
type="checkbox"
id="popLanding"
checked={isPopLanding}
disabled={!isPopLanding && hasOtherPopLanding}
onChange={(e) => setIsPopLanding(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 accent-primary disabled:cursor-not-allowed disabled:opacity-50"
/>
<label
htmlFor="popLanding"
className={`text-sm font-medium ${!isPopLanding && hasOtherPopLanding ? "text-muted-foreground" : ""}`}
>
POP
</label>
{!isPopLanding && hasOtherPopLanding && (
<span className="text-xs text-muted-foreground">
( )
</span>
)}
</div>
{isPopLanding && (
<p className="text-xs text-muted-foreground">
POP .
</p>
)}
</div>
)}
{/* URL 직접 입력 */}
{urlType === "direct" && (
<Input
id="menuUrl"
value={formData.menuUrl}
onChange={(e) => handleInputChange("menuUrl", e.target.value)}
placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_MENU_URL_PLACEHOLDER)}
/>
)}
2025-08-21 09:41:46 +09:00
</div>
<div className="space-y-2">
2025-08-25 17:56:33 +09:00
<Label htmlFor="menuDesc">{getText(MENU_MANAGEMENT_KEYS.FORM_MENU_DESCRIPTION)}</Label>
2025-08-21 09:41:46 +09:00
<Textarea
id="menuDesc"
value={formData.menuDesc}
onChange={(e) => handleInputChange("menuDesc", e.target.value)}
2025-08-25 17:56:33 +09:00
placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_MENU_DESCRIPTION_PLACEHOLDER)}
2025-08-21 09:41:46 +09:00
rows={3}
/>
</div>
<div className="space-y-2">
2025-08-25 17:56:33 +09:00
<Label htmlFor="seq">{getText(MENU_MANAGEMENT_KEYS.FORM_MENU_SEQUENCE)}</Label>
2025-08-21 09:41:46 +09:00
<Input
id="seq"
type="number"
value={formData.seq}
onChange={(e) => handleInputChange("seq", parseInt(e.target.value) || 1)}
min="1"
/>
</div>
<div className="flex justify-end space-x-2 pt-4">
<Button type="button" variant="outline" onClick={onClose}>
2025-08-25 17:56:33 +09:00
{getText(MENU_MANAGEMENT_KEYS.BUTTON_CANCEL)}
2025-08-21 09:41:46 +09:00
</Button>
<Button type="submit" disabled={loading}>
{loading
2025-08-25 17:56:33 +09:00
? getText(MENU_MANAGEMENT_KEYS.BUTTON_SAVE_PROCESSING)
2025-08-21 09:41:46 +09:00
: isEdit
2025-08-25 17:56:33 +09:00
? getText(MENU_MANAGEMENT_KEYS.BUTTON_MODIFY)
: getText(MENU_MANAGEMENT_KEYS.BUTTON_REGISTER)}
2025-08-21 09:41:46 +09:00
</Button>
</div>
</form>
2025-12-05 10:46:10 +09:00
</DialogContent>
</Dialog>
2025-08-21 09:41:46 +09:00
);
};