From 014688974e6df6e5199cacec3f4212a5276a59d4 Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Tue, 26 Aug 2025 17:20:45 +0900 Subject: [PATCH 1/5] =?UTF-8?q?admin=20=EB=A0=8C=EB=8D=94=EB=A7=81=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/(auth)/layout.tsx | 3 + frontend/app/(main)/admin/layout.tsx | 624 -------------------- frontend/app/(main)/admin/page.tsx | 163 ++--- frontend/app/(main)/layout.tsx | 5 +- frontend/app/(main)/main/layout.tsx | 160 ----- frontend/app/(main)/page.tsx | 44 +- frontend/components/layout/AppLayout.tsx | 402 +++++++++++++ frontend/components/layout/MainHeader.tsx | 6 +- frontend/components/layout/MainLayout.tsx | 407 ------------- frontend/components/layout/UserDropdown.tsx | 12 +- frontend/components/ui/dropdown-menu.tsx | 2 +- 11 files changed, 533 insertions(+), 1295 deletions(-) create mode 100644 frontend/app/(auth)/layout.tsx delete mode 100644 frontend/app/(main)/admin/layout.tsx delete mode 100644 frontend/app/(main)/main/layout.tsx create mode 100644 frontend/components/layout/AppLayout.tsx delete mode 100644 frontend/components/layout/MainLayout.tsx diff --git a/frontend/app/(auth)/layout.tsx b/frontend/app/(auth)/layout.tsx new file mode 100644 index 00000000..9143d65c --- /dev/null +++ b/frontend/app/(auth)/layout.tsx @@ -0,0 +1,3 @@ +export default function AuthLayout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/frontend/app/(main)/admin/layout.tsx b/frontend/app/(main)/admin/layout.tsx deleted file mode 100644 index 84882137..00000000 --- a/frontend/app/(main)/admin/layout.tsx +++ /dev/null @@ -1,624 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { Settings, Menu, X } from "lucide-react"; -import { cn } from "@/lib/utils"; -import { menuApi, MenuItem } from "@/lib/api/menu"; -import { LoadingSpinner } from "@/components/common/LoadingSpinner"; -import { apiClient } from "@/lib/api/client"; -import { toast } from "sonner"; -import { useRouter, usePathname } from "next/navigation"; -import { - getMenuTextSync, - MENU_MANAGEMENT_KEYS, - useMenuManagementText, - setTranslationCache, -} from "@/lib/utils/multilang"; -import { useMultiLang } from "@/hooks/useMultiLang"; -import { tokenSync } from "@/lib/sessionManager"; - -// 아이콘 매핑 -const ICON_MAP: { [key: string]: any } = { - Settings, -}; - -// 기본 관리자 메뉴 (메뉴관리에서 등록된 메뉴가 없을 때 사용) -// 동적으로 생성되는 메뉴들만 사용하므로 기본 메뉴 제거 -const DEFAULT_ADMIN_MENUS: any[] = []; - -/** - * 관리자 팝업 전용 레이아웃 - */ -export default function AdminLayout({ children }: { children: React.ReactNode }) { - const router = useRouter(); - const pathname = usePathname(); - const { userLang, setGlobalChangeLangCallback } = useMultiLang({ companyCode: "*" }); - const { getMenuText } = useMenuManagementText(); - const [adminMenus, setAdminMenus] = useState([]); - const [loading, setLoading] = useState(true); - const [activeMenu, setActiveMenu] = useState("company"); - const [translationsLoaded, setTranslationsLoaded] = useState(false); - const [forceUpdate, setForceUpdate] = useState(0); - const [sidebarOpen, setSidebarOpen] = useState(true); // 사이드바 토글 상태 추가 - const [isAuthorized, setIsAuthorized] = useState(null); - const [menuTranslations, setMenuTranslations] = useState<{ - title: string; - description: string; - }>({ - title: "메뉴 관리", - description: "시스템의 메뉴 구조와 권한을 관리합니다.", - }); - - // 토큰 확인 및 인증 상태 체크 - useEffect(() => { - console.log("=== AdminLayout 토큰 확인 ==="); - console.log("현재 경로:", pathname); - console.log("현재 URL:", window.location.href); - - const checkToken = () => { - const token = localStorage.getItem("authToken"); - console.log("localStorage 토큰:", token ? "존재" : "없음"); - console.log("토큰 길이:", token ? token.length : 0); - console.log("토큰 시작:", token ? token.substring(0, 30) + "..." : "없음"); - - // sessionStorage도 확인 - const sessionToken = sessionStorage.getItem("authToken"); - console.log("sessionStorage 토큰:", sessionToken ? "존재" : "없음"); - - // 현재 인증 상태도 확인 - console.log("현재 isAuthorized 상태:", isAuthorized); - - // 토큰이 없으면 sessionStorage에서 복원 시도 - if (!token && sessionToken) { - console.log("🔄 sessionStorage에서 토큰 복원 시도"); - const restored = tokenSync.restoreFromSession(); - if (restored) { - console.log("✅ 토큰 복원 성공"); - setIsAuthorized(true); - return; - } - } - - // 토큰 유효성 검증 - if (token && !tokenSync.validateToken(token)) { - console.log("❌ 토큰 유효성 검증 실패"); - localStorage.removeItem("authToken"); - sessionStorage.removeItem("authToken"); - setIsAuthorized(false); - setTimeout(() => { - console.log("리다이렉트 실행: /login"); - window.location.href = "/login"; - }, 5000); - return; - } - - if (!token) { - console.log("❌ 토큰이 없음 - 로그인 페이지로 이동"); - setIsAuthorized(false); - // 5초 후 리다이렉트 (디버깅을 위해 시간 늘림) - setTimeout(() => { - console.log("리다이렉트 실행: /login"); - window.location.href = "/login"; - }, 5000); - return; - } - - // 토큰이 있으면 인증된 것으로 간주 - console.log("✅ 토큰 존재 - 인증된 것으로 간주"); - setIsAuthorized(true); - - // 토큰 강제 동기화 (다른 탭과 동기화) - tokenSync.forceSync(); - }; - - // 초기 토큰 확인 - checkToken(); - - // localStorage 변경 이벤트 리스너 추가 - const handleStorageChange = (e: StorageEvent) => { - if (e.key === "authToken") { - console.log("🔄 localStorage authToken 변경 감지:", e.newValue ? "설정됨" : "제거됨"); - checkToken(); - } - }; - - // 페이지 포커스 시 토큰 재확인 - const handleFocus = () => { - console.log("🔄 페이지 포커스 - 토큰 재확인"); - checkToken(); - }; - - window.addEventListener("storage", handleStorageChange); - window.addEventListener("focus", handleFocus); - - return () => { - window.removeEventListener("storage", handleStorageChange); - window.removeEventListener("focus", handleFocus); - }; - }, [pathname]); - - // 관리자 메뉴 로드 - useEffect(() => { - if (isAuthorized) { - loadAdminMenus(); - } - }, [isAuthorized]); - - // pathname 변경 시 활성 메뉴 업데이트 - useEffect(() => { - if (adminMenus.length > 0) { - const currentPath = pathname; - console.log("🔄 경로 변경 감지:", currentPath); - - // 정확한 URL 매칭을 위한 함수 - const isExactMatch = (menuUrl: string, currentPath: string) => { - // 메뉴 URL이 현재 경로와 정확히 일치하거나 - // 메뉴 URL이 현재 경로의 시작 부분과 일치하는지 확인 - // 단, 메뉴 관리 페이지는 제외 (별도 처리) - if (currentPath === "/admin/menu" || currentPath.startsWith("/admin/menu/")) { - return false; // 메뉴 관리 페이지는 별도 로직에서 처리 - } - - // 정확한 매칭: URL이 정확히 일치하거나 현재 경로가 메뉴 URL로 시작하는 경우 - // 단, 메뉴 URL이 "/admin"이고 현재 경로가 "/admin/menu"인 경우는 제외 - if (menuUrl === "/admin" && currentPath.startsWith("/admin/menu")) { - return false; - } - - // 정확한 매칭: URL이 정확히 일치하는 경우 - if (menuUrl === currentPath) { - return true; - } - - // 경로 매칭: 현재 경로가 메뉴 URL로 시작하는 경우 - // 단, 메뉴 URL이 슬래시로 끝나지 않으면 정확한 경로 매칭만 허용 - if (currentPath.startsWith(menuUrl + "/")) { - return true; - } - - return false; - }; - - // 메뉴 관리 페이지인지 확인 (정확한 매칭) - if (currentPath === "/admin/menu" || currentPath.startsWith("/admin/menu/")) { - // 메뉴 관리 페이지는 URL 매칭과 메뉴명 매칭 모두 시도 - const menuManagementMenu = adminMenus.find((menu) => { - console.log("🔍 메뉴 검사:", { - id: menu.id, - name: menu.name, - url: menu.url, - level: menu.lev, - }); - - // URL 매칭 (가장 우선) - if (menu.url && (menu.url === "/admin/menu" || menu.url === "/admin/menu/")) { - console.log("✅ URL 매칭 성공:", menu.url); - return true; - } - - // 메뉴명 매칭 (메뉴 관리 관련 메뉴) - if (menu.name && (menu.name.includes("메뉴 관리") || menu.name.includes("Menu Management"))) { - console.log("✅ 메뉴명 매칭 성공:", menu.name); - return true; - } - - return false; - }); - - if (menuManagementMenu && menuManagementMenu.id) { - console.log("✅ 메뉴 관리 메뉴 활성화:", menuManagementMenu.id, "이름:", menuManagementMenu.name); - setActiveMenu(menuManagementMenu.id); - } else { - console.log("❌ 메뉴 관리 메뉴를 찾을 수 없음"); - } - } else { - // 다른 메뉴들에 대한 정확한 매칭 - console.log("🔍 다른 메뉴 매칭 시도:", currentPath); - - // 모든 메뉴에 대해 매칭 시도 - const matchingMenus = adminMenus.filter((menu) => menu.url && isExactMatch(menu.url, currentPath)); - - if (matchingMenus.length > 0) { - // 가장 구체적인 매칭을 찾기 (가장 긴 URL이 우선) - const bestMatch = matchingMenus.reduce((best, current) => { - return current.url && current.url.length > (best.url ? best.url.length : 0) ? current : best; - }); - - console.log("✅ 매칭된 메뉴 활성화:", bestMatch.id, "URL:", bestMatch.url, "레벨:", bestMatch.lev); - setActiveMenu(bestMatch.id); - } else { - // URL 매칭이 실패한 경우, 경로 기반으로 가장 적절한 메뉴 찾기 - console.log("🔍 URL 매칭 실패, 경로 기반 매칭 시도"); - - // 현재 경로의 세그먼트 수 계산 - const pathSegments = currentPath.split("/").filter((segment: string) => segment.length > 0); - const currentPathLevel = pathSegments.length; - - console.log("현재 경로 레벨:", currentPathLevel, "세그먼트:", pathSegments); - - // 현재 경로 레벨과 일치하는 메뉴 찾기 - const levelMatchingMenus = adminMenus.filter((menu) => { - if (!menu.url) return false; - - const menuPathSegments = menu.url.split("/").filter((segment: string) => segment.length > 0); - const menuLevel = menuPathSegments.length; - - // 현재 경로가 메뉴 URL로 시작하고, 레벨이 일치하는 경우 - return currentPath.startsWith(menu.url) && menuLevel === currentPathLevel; - }); - - if (levelMatchingMenus.length > 0) { - const bestLevelMatch = levelMatchingMenus.reduce((best, current) => { - return current.url && current.url.length > (best.url ? best.url.length : 0) ? current : best; - }); - - console.log( - "✅ 레벨 기반 매칭 성공:", - bestLevelMatch.id, - "URL:", - bestLevelMatch.url, - "레벨:", - bestLevelMatch.lev, - ); - setActiveMenu(bestLevelMatch.id); - } else { - console.log("❌ 매칭되는 메뉴를 찾을 수 없음:", currentPath); - // 매칭되는 메뉴가 없으면 첫 번째 메뉴를 기본값으로 설정 - if (adminMenus.length > 0) { - console.log("🔧 기본 메뉴로 설정:", adminMenus[0].id); - setActiveMenu(adminMenus[0].id); - } - } - } - } - } - }, [pathname, adminMenus]); - - // 전역 언어 변경 콜백 등록 - useEffect(() => { - setGlobalChangeLangCallback((newLang: string) => { - console.log("🔄 전역 언어 변경 감지:", newLang); - // 번역 상태 리셋 - setMenuTranslations({ - title: "메뉴 관리", - description: "시스템의 메뉴 구조와 권한을 관리합니다.", - }); - - // 새로운 번역 로드 및 메뉴 데이터 재로드 - setTimeout(() => { - loadTranslations(); - // 언어 변경 시 메뉴 데이터를 다시 로드하여 번역된 메뉴명 가져오기 - loadAdminMenus(); - }, 100); - }); - - return () => { - setGlobalChangeLangCallback(() => {}); - }; - }, [adminMenus]); - - // 번역 데이터 로드 - useEffect(() => { - if (userLang) { - console.log("🔄 userLang 변경 감지:", userLang); - // 번역 상태 리셋 - setTranslationsLoaded(false); - setMenuTranslations({ - title: "메뉴 관리", - description: "시스템의 메뉴 구조와 권한을 관리합니다.", - }); - // 새로운 번역 로드 - loadTranslations(); - } - }, [userLang]); - - // 컴포넌트 마운트 시 userLang이 설정될 때까지 대기 - useEffect(() => { - if (userLang) { - console.log("🔄 userLang 설정됨, 번역 로드 시작:", userLang); - loadTranslations(); - } - }, [userLang]); // userLang이 설정될 때마다 실행 - - // 키보드 단축키로 사이드바 토글 - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - // Ctrl+B 또는 Cmd+B로 사이드바 토글 - if ((event.ctrlKey || event.metaKey) && event.key === "b") { - event.preventDefault(); - setSidebarOpen((prev) => !prev); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, []); - - // 번역 로드 완료 이벤트 감지 - useEffect(() => { - const handleTranslationLoaded = (event: any) => { - const { key, text } = event.detail; - - if (key === MENU_MANAGEMENT_KEYS.TITLE) { - setMenuTranslations((prev) => ({ ...prev, title: text })); - } else if (key === MENU_MANAGEMENT_KEYS.DESCRIPTION) { - setMenuTranslations((prev) => ({ ...prev, description: text })); - } - - setForceUpdate((prev) => prev + 1); - }; - - window.addEventListener("translation-loaded", handleTranslationLoaded); - return () => window.removeEventListener("translation-loaded", handleTranslationLoaded); - }, []); - - const loadTranslations = async () => { - try { - // userLang이 설정되지 않았으면 번역 로드하지 않음 - if (!userLang) { - console.log("⏳ userLang이 설정되지 않음, 번역 로드 대기"); - return; - } - - console.log("🌐 Admin Layout 번역 로드 시작", { - userLang, - }); - - // API 직접 호출로 현재 언어 사용 (배치 조회 방식) - const companyCode = "*"; - - try { - // 배치 조회 API 사용 - const response = await apiClient.post( - "/multilang/batch", - { - langKeys: [MENU_MANAGEMENT_KEYS.TITLE, MENU_MANAGEMENT_KEYS.DESCRIPTION], - }, - { - params: { - companyCode, - menuCode: "MENU_MANAGEMENT", - userLang: userLang, - }, - }, - ); - - if (response.data.success && response.data.data) { - const translations = response.data.data; - const title = translations[MENU_MANAGEMENT_KEYS.TITLE] || "메뉴 관리"; - const description = - translations[MENU_MANAGEMENT_KEYS.DESCRIPTION] || "시스템의 메뉴 구조와 권한을 관리합니다."; - - // 번역 캐시에 저장 - setTranslationCache(userLang, translations); - - // 상태 업데이트 - setMenuTranslations({ title, description }); - - console.log("🌐 Admin Layout 번역 로드 완료 (배치)", { title, description, userLang }); - } else { - // 전역 사용자 로케일 확인하여 기본값 설정 - const globalUserLang = (window as any).__GLOBAL_USER_LANG || localStorage.getItem("userLocale") || "KR"; - console.log("🌐 전역 사용자 로케일 확인:", globalUserLang); - - // 사용자 로케일에 따른 기본값 설정 - let title, description; - if (globalUserLang === "US") { - title = "Menu Management"; - description = "Manage system menu structure and permissions"; - } else { - title = "메뉴 관리"; - description = "시스템의 메뉴 구조와 권한을 관리합니다."; - } - - setMenuTranslations({ title, description }); - console.log("🌐 Admin Layout 기본값 사용", { title, description, userLang: globalUserLang }); - } - } catch (error) { - console.error("❌ Admin Layout 배치 번역 로드 실패:", error); - // 오류 시에도 전역 사용자 로케일 확인하여 기본값 설정 - const globalUserLang = (window as any).__GLOBAL_USER_LANG || localStorage.getItem("userLocale") || "KR"; - console.log("🌐 오류 시 전역 사용자 로케일 확인:", globalUserLang); - - let title, description; - if (globalUserLang === "US") { - title = "Menu Management"; - description = "Manage system menu structure and permissions"; - } else { - title = "메뉴 관리"; - description = "시스템의 메뉴 구조와 권한을 관리합니다."; - } - - setMenuTranslations({ title, description }); - } - } catch (error) { - console.error("❌ Admin Layout 번역 로드 실패:", error); - } - }; - - const loadAdminMenus = async () => { - try { - setLoading(true); - const response = await menuApi.getAdminMenus(); - - if (response.success && response.data) { - // 메뉴 데이터를 관리자 메뉴 형식으로 변환 - const convertedMenus = response.data.map((menu: MenuItem) => ({ - id: menu.objid || menu.OBJID, - name: - menu.translated_name || menu.TRANSLATED_NAME || menu.menu_name_kor || menu.MENU_NAME_KOR || "메뉴명 없음", - icon: Settings, // 기본 아이콘 - description: menu.translated_desc || menu.TRANSLATED_DESC || menu.menu_desc || menu.MENU_DESC || "메뉴 설명", - url: menu.menu_url || menu.MENU_URL, - menuType: menu.menu_type || menu.MENU_TYPE, - // 번역 키 정보 저장 (언어 변경 시 사용) - lang_key: menu.lang_key || menu.LANG_KEY, - lang_key_desc: menu.lang_key_desc || menu.LANG_KEY_DESC, - // 레벨 정보 추가 - lev: (menu as any).lev || (menu as any).LEV, - })); - - // 동적으로 생성된 메뉴들만 사용 - console.log("📋 로드된 메뉴 목록:"); - convertedMenus.forEach((menu, index) => { - console.log(` ${index + 1}. ID: ${menu.id}, 이름: ${menu.name}, URL: ${menu.url}, 레벨: ${menu.lev}`); - }); - - setAdminMenus(convertedMenus); - - // 백엔드에서 이미 번역된 데이터를 제공하므로 별도 번역 로드 불필요 - // await loadDynamicMenuTranslations(convertedMenus); - } - } catch (error) { - console.error("관리자 메뉴 로드 오류:", error); - } finally { - setLoading(false); - } - }; - - const currentMenu = adminMenus.find((menu) => menu.id === activeMenu) || adminMenus[0]; - - // 메뉴 클릭 시 URL 이동 처리 - const handleMenuClick = (menu: any) => { - console.log("=== 메뉴 클릭 ==="); - console.log("클릭된 메뉴:", menu); - - // 메뉴 클릭 시 토큰 재확인 - const token = localStorage.getItem("authToken"); - console.log("메뉴 클릭 시 토큰 확인:", token ? "존재" : "없음"); - - if (!token) { - console.log("❌ 메뉴 클릭 시 토큰이 없음 - 경고 표시"); - alert("인증 토큰이 없습니다. 다시 로그인해주세요."); - window.location.href = "/login"; - return; - } - - setActiveMenu(menu.id); - - if (menu.url) { - // 외부 URL인 경우 새 탭에서 열기 - if (menu.url.startsWith("http://") || menu.url.startsWith("https://")) { - console.log("외부 URL 열기:", menu.url); - window.open(menu.url, "_blank"); - } else { - // 내부 URL인 경우 라우터로 이동 - console.log("내부 URL 이동:", menu.url); - router.push(menu.url); - } - } else { - // URL이 없는 메뉴는 현재 페이지에서 컨텐츠만 변경 - console.log("URL이 없는 메뉴:", menu); - } - }; - - return ( -
- {/* 인증 상태 확인 */} - {isAuthorized === null && ( -
-
-

로딩 중...

- -
-
- )} - - {isAuthorized === false && ( -
-
-

인증 실패

-

토큰이 없습니다. 3초 후 로그인 페이지로 이동합니다.

-
-
- )} - - {isAuthorized === true && ( - <> - {/* 왼쪽 사이드바 */} -
-
- {sidebarOpen && ( - <> -
-

관리자 설정

-

시스템 관리 도구

-
- - - )} - {!sidebarOpen && ( - - )} -
- - -
- - {/* 오른쪽 컨텐츠 영역 */} -
-
-
-

- {pathname === "/admin/menu" || pathname.startsWith("/admin/menu/") - ? menuTranslations.title - : currentMenu?.name || "관리자 설정"} -

-

- {pathname === "/admin/menu" || pathname.startsWith("/admin/menu/") - ? menuTranslations.description - : currentMenu?.description || "시스템 관리 도구"} -

-
- {children} -
-
- - )} -
- ); -} diff --git a/frontend/app/(main)/admin/page.tsx b/frontend/app/(main)/admin/page.tsx index 535b27ab..956792fd 100644 --- a/frontend/app/(main)/admin/page.tsx +++ b/frontend/app/(main)/admin/page.tsx @@ -1,90 +1,103 @@ "use client"; -import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { RefreshCw, Users, Shield, Settings, BarChart3 } from "lucide-react"; /** - * 관리자 메인 페이지 (회사관리) - * 단순한 토큰 확인만 수행 + * 관리자 메인 페이지 */ export default function AdminPage() { - const [tokenInfo, setTokenInfo] = useState({}); - const [isAuthorized, setIsAuthorized] = useState(null); - - useEffect(() => { - console.log("=== AdminPage 시작 ==="); - - const token = localStorage.getItem("authToken"); - console.log("localStorage 토큰:", token ? "존재" : "없음"); - - const info = { - hasToken: !!token, - tokenLength: token ? token.length : 0, - tokenStart: token ? token.substring(0, 30) + "..." : "없음", - currentUrl: window.location.href, - timestamp: new Date().toISOString(), - }; - - setTokenInfo(info); - console.log("토큰 정보:", info); - - if (!token) { - console.log("토큰이 없음 - 로그인 페이지로 이동"); - setIsAuthorized(false); - // 3초 후 리다이렉트 - setTimeout(() => { - window.location.href = "/login"; - }, 3000); - return; - } - - // 토큰이 있으면 인증된 것으로 간주 - console.log("토큰 존재 - 인증된 것으로 간주"); - setIsAuthorized(true); - }, []); - - if (isAuthorized === null) { - return ( -
-

로딩 중...

+ return ( +
+ {/* 페이지 헤더 */} +
+
+

관리자 대시보드

+

시스템 관리 및 모니터링

+
+
- ); - } - if (isAuthorized === false) { - return ( -
-

인증 실패

-

토큰이 없습니다. 3초 후 로그인 페이지로 이동합니다.

+ {/* 관리자 기능 카드들 */} +
+
+
+
+ +
+
+

사용자 관리

+

사용자 계정 및 권한 관리

+
+
+
-
-

디버깅 정보

-
{JSON.stringify(tokenInfo, null, 2)}
+
+
+
+ +
+
+

권한 관리

+

메뉴 및 기능 권한 설정

+
+
+
+ +
+
+
+ +
+
+

시스템 설정

+

기본 설정 및 환경 구성

+
+
+
+ +
+
+
+ +
+
+

통계 및 리포트

+

시스템 사용 현황 분석

+
+
- ); - } - return ( -
-

관리자 페이지

-

✅ 인증 성공! 관리자 페이지에 접근할 수 있습니다.

- -
-

토큰 정보

-
{JSON.stringify(tokenInfo, null, 2)}
-
- -
-

관리자 기능

-

여기에 실제 관리자 기능들이 들어갈 예정입니다.

- + {/* 최근 활동 */} +
+

최근 관리자 활동

+
+
+
+

새로운 사용자 추가

+

김철수 사용자가 생성되었습니다.

+
+ 2분 전 +
+
+
+

권한 변경

+

이영희 사용자의 권한이 수정되었습니다.

+
+ 15분 전 +
+
+
+

시스템 설정 변경

+

비밀번호 정책이 업데이트되었습니다.

+
+ 1시간 전 +
+
); diff --git a/frontend/app/(main)/layout.tsx b/frontend/app/(main)/layout.tsx index 69573ef4..a0ac6c89 100644 --- a/frontend/app/(main)/layout.tsx +++ b/frontend/app/(main)/layout.tsx @@ -1,10 +1,13 @@ import { AuthProvider } from "@/contexts/AuthContext"; import { MenuProvider } from "@/contexts/MenuContext"; +import { AppLayout } from "@/components/layout/AppLayout"; export default function MainLayout({ children }: { children: React.ReactNode }) { return ( - {children} + + {children} + ); } diff --git a/frontend/app/(main)/main/layout.tsx b/frontend/app/(main)/main/layout.tsx deleted file mode 100644 index 213360bf..00000000 --- a/frontend/app/(main)/main/layout.tsx +++ /dev/null @@ -1,160 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { useAuth } from "@/hooks/useAuth"; -import { useMenu } from "@/hooks/useMenu"; -import { useProfile } from "@/hooks/useProfile"; -import { MainHeader } from "@/components/layout/MainHeader"; -import { MainSidebar } from "@/components/layout/MainSidebar"; -import { ProfileModal } from "@/components/layout/ProfileModal"; -import { LAYOUT_CONFIG } from "@/constants/layout"; - -/** - * 메인 레이아웃 컴포넌트 - * 비즈니스 로직은 커스텀 훅들에서 처리하고, UI 컴포넌트들을 조합하여 구성 - */ -export default function MainLayout({ children }: { children: React.ReactNode }) { - const { user, logout, refreshUserData, loading: authLoading } = useAuth(); - const [isSidebarOpen, setIsSidebarOpen] = useState(false); // 사이드바 상태 - const [isMobile, setIsMobile] = useState(false); // 모바일 여부 - - // 화면 크기 감지 - useEffect(() => { - const checkMobile = () => { - const mobile = window.innerWidth < 768; // md breakpoint - setIsMobile(mobile); - - // 모바일에서는 사이드바를 기본적으로 닫아둠 - if (mobile) { - setIsSidebarOpen(false); - } - // PC에서는 사이드바 상태를 건드리지 않음 (항상 표시) - }; - - checkMobile(); - window.addEventListener("resize", checkMobile); - return () => window.removeEventListener("resize", checkMobile); - }, []); - - // 메뉴 관련 로직 - const { menuList, expandedMenus, handleMenuClick } = useMenu(user, authLoading); - - // 프로필 관련 로직 - const { - isModalOpen, - formData, - selectedImage, - isSaving, - alertModal, - closeAlert, - openProfileModal, - closeProfileModal, - updateFormData, - selectImage, - removeImage, - saveProfile, - } = useProfile(user, refreshUserData); - - /** - * 사이드바 토글 처리 (모바일에서만 동작) - */ - const handleSidebarToggle = () => { - if (isMobile) { - // 모바일에서만 토글 동작 - setIsSidebarOpen(!isSidebarOpen); - } - // PC에서는 아무것도 하지 않음 (항상 표시) - }; - - /** - * 로그아웃 처리 - */ - const handleLogout = async () => { - await logout(); - }; - - return ( -
- - -
- {/* 데스크톱 고정 사이드바 - md 이상에서 항상 표시 */} - - - {/* 모바일 오버레이 사이드바 - md 미만에서만 표시 */} - {isMobile && ( -
- {/* 배경 오버레이 */} -
setIsSidebarOpen(false)} - /> - {/* 사이드바 */} - -
- )} - - {/* Main Content - 반응형 여백 적용 */} -
-
{children}
-
-
- - {/* 프로필 수정 모달 */} - -
- ); -} diff --git a/frontend/app/(main)/page.tsx b/frontend/app/(main)/page.tsx index 0cfb72e0..72539e31 100644 --- a/frontend/app/(main)/page.tsx +++ b/frontend/app/(main)/page.tsx @@ -1,21 +1,39 @@ "use client"; -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { RefreshCw } from "lucide-react"; export default function MainHomePage() { - const router = useRouter(); - - useEffect(() => { - // (main) 그룹의 루트 접속 시 대시보드로 리다이렉트 - router.push("/dashboard"); - }, [router]); - return ( -
-
-
-

대시보드로 이동 중...

+
+ {/* 페이지 헤더 */} +
+
+

대시보드

+

PLM 시스템의 주요 현황을 확인하세요

+
+ +
+ + {/* 대시보드 컨텐츠 */} +
+

PLM 솔루션에 오신 것을 환영합니다!

+

제품 수명 주기 관리 시스템을 통해 효율적인 업무를 시작하세요.

+ +
+ + Spring Boot + + + Next.js + + + Shadcn/ui + +
); diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx new file mode 100644 index 00000000..bc5473c5 --- /dev/null +++ b/frontend/components/layout/AppLayout.tsx @@ -0,0 +1,402 @@ +"use client"; + +import { useState } from "react"; +import { useRouter, usePathname } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { + Shield, + Menu, + Home, + Settings, + BarChart3, + FileText, + Users, + Package, + ChevronDown, + ChevronRight, + UserCheck, +} from "lucide-react"; +import { useMenu } from "@/contexts/MenuContext"; +import { useAuth } from "@/hooks/useAuth"; +import { useProfile } from "@/hooks/useProfile"; +import { MenuItem } from "@/lib/api/menu"; +import { MainHeader } from "./MainHeader"; +import { ProfileModal } from "./ProfileModal"; + +// useAuth의 UserInfo 타입을 확장 +interface ExtendedUserInfo { + userId: string; + userName: string; + userNameEng?: string; + userNameCn?: string; + deptCode?: string; + deptName?: string; + positionCode?: string; + positionName?: string; + email?: string; + tel?: string; + cellPhone?: string; + userType?: string; + userTypeName?: string; + authName?: string; + partnerCd?: string; + isAdmin: boolean; + sabun?: string; + photo?: string | null; + companyCode?: string; + locale?: string; +} + +interface AppLayoutProps { + children: React.ReactNode; +} + +// 메뉴 아이콘 매핑 함수 +const getMenuIcon = (menuName: string) => { + const name = menuName.toLowerCase(); + if (name.includes("대시보드") || name.includes("dashboard")) return ; + if (name.includes("관리자") || name.includes("admin")) return ; + if (name.includes("사용자") || name.includes("user")) return ; + if (name.includes("프로젝트") || name.includes("project")) return ; + if (name.includes("제품") || name.includes("product")) return ; + if (name.includes("설정") || name.includes("setting")) return ; + if (name.includes("로그") || name.includes("log")) return ; + if (name.includes("메뉴") || name.includes("menu")) return ; + return ; +}; + +// 메뉴 데이터를 UI용으로 변환하는 함수 (최상위 "사용자", "관리자" 제외) +const convertMenuToUI = (menus: MenuItem[], userInfo: ExtendedUserInfo | null, parentId: string = "0"): any[] => { + const filteredMenus = menus + .filter((menu) => (menu.parent_obj_id || menu.PARENT_OBJ_ID) === parentId) + .filter((menu) => (menu.status || menu.STATUS) === "active") + .sort((a, b) => (a.seq || a.SEQ || 0) - (b.seq || b.SEQ || 0)); + + // 최상위 레벨에서 "사용자", "관리자" 카테고리가 있으면 그 하위 메뉴들을 직접 표시 + if (parentId === "0") { + const allMenus: any[] = []; + + for (const menu of filteredMenus) { + const menuName = (menu.menu_name_kor || menu.MENU_NAME_KOR || "").toLowerCase(); + + // "사용자" 또는 "관리자" 카테고리면 하위 메뉴들을 직접 추가 + if (menuName.includes("사용자") || menuName.includes("관리자")) { + const childMenus = convertMenuToUI(menus, userInfo, menu.objid || menu.OBJID); + allMenus.push(...childMenus); + } else { + // 일반 메뉴는 그대로 추가 + allMenus.push(convertSingleMenu(menu, menus, userInfo)); + } + } + + return allMenus; + } + + return filteredMenus.map((menu) => convertSingleMenu(menu, menus, userInfo)); +}; + +// 단일 메뉴 변환 함수 +const convertSingleMenu = (menu: MenuItem, allMenus: MenuItem[], userInfo: ExtendedUserInfo | null): any => { + const menuId = menu.objid || menu.OBJID; + + // 사용자 locale 기준으로 번역 처리 + const getDisplayText = (menu: MenuItem) => { + // 다국어 텍스트가 있으면 사용, 없으면 기본 텍스트 사용 + if (menu.translated_name || menu.TRANSLATED_NAME) { + return menu.translated_name || menu.TRANSLATED_NAME; + } + + const baseName = menu.menu_name_kor || menu.MENU_NAME_KOR || "메뉴명 없음"; + + // 사용자 정보에서 locale 가져오기 + const userLocale = userInfo?.locale || "ko"; + + if (userLocale === "EN") { + // 영어 번역 + const translations: { [key: string]: string } = { + 관리자: "Administrator", + 사용자: "User Management", + 메뉴: "Menu Management", + 대시보드: "Dashboard", + 권한: "Permission Management", + 코드: "Code Management", + 설정: "Settings", + 로그: "Log Management", + 프로젝트: "Project Management", + 제품: "Product Management", + }; + + for (const [korean, english] of Object.entries(translations)) { + if (baseName.includes(korean)) { + return baseName.replace(korean, english); + } + } + } else if (userLocale === "JA") { + // 일본어 번역 + const translations: { [key: string]: string } = { + 관리자: "管理者", + 사용자: "ユーザー管理", + 메뉴: "メニュー管理", + 대시보드: "ダッシュボード", + 권한: "権限管理", + 코드: "コード管理", + 설정: "設定", + 로그: "ログ管理", + 프로젝트: "プロジェクト管理", + 제품: "製品管理", + }; + + for (const [korean, japanese] of Object.entries(translations)) { + if (baseName.includes(korean)) { + return baseName.replace(korean, japanese); + } + } + } else if (userLocale === "ZH") { + // 중국어 번역 + const translations: { [key: string]: string } = { + 관리자: "管理员", + 사용자: "用户管理", + 메뉴: "菜单管理", + 대시보드: "仪表板", + 권한: "权限管理", + 코드: "代码管理", + 설정: "设置", + 로그: "日志管理", + 프로젝트: "项目管理", + 제품: "产品管理", + }; + + for (const [korean, chinese] of Object.entries(translations)) { + if (baseName.includes(korean)) { + return baseName.replace(korean, chinese); + } + } + } + + return baseName; + }; + + const children = convertMenuToUI(allMenus, userInfo, menuId); + + return { + id: menuId, + name: getDisplayText(menu), + icon: getMenuIcon(menu.menu_name_kor || menu.MENU_NAME_KOR || ""), + url: menu.menu_url || menu.MENU_URL || "#", + children: children.length > 0 ? children : undefined, + hasChildren: children.length > 0, + }; +}; + +export function AppLayout({ children }: AppLayoutProps) { + const router = useRouter(); + const pathname = usePathname(); + const { user, logout, refreshUserData } = useAuth(); + const { userMenus, adminMenus, loading } = useMenu(); + const [sidebarOpen, setSidebarOpen] = useState(false); + const [expandedMenus, setExpandedMenus] = useState>(new Set()); + + // 프로필 관련 로직 + const { + isModalOpen, + formData, + selectedImage, + isSaving, + alertModal, + closeAlert, + openProfileModal, + closeProfileModal, + updateFormData, + selectImage, + removeImage, + saveProfile, + } = useProfile(user, refreshUserData); + + // 현재 경로에 따라 어드민 모드인지 판단 + const isAdminMode = pathname.startsWith("/admin"); + + // 현재 모드에 따라 표시할 메뉴 결정 + const currentMenus = isAdminMode ? adminMenus : userMenus; + + // 메뉴 토글 함수 + const toggleMenu = (menuId: string) => { + const newExpanded = new Set(expandedMenus); + if (newExpanded.has(menuId)) { + newExpanded.delete(menuId); + } else { + newExpanded.add(menuId); + } + setExpandedMenus(newExpanded); + }; + + // 메뉴 클릭 핸들러 + const handleMenuClick = (menu: any) => { + if (menu.hasChildren) { + toggleMenu(menu.id); + } else if (menu.url && menu.url !== "#") { + router.push(menu.url); + setSidebarOpen(false); + } + }; + + // 모드 전환 핸들러 + const handleModeSwitch = () => { + if (isAdminMode) { + router.push("/main"); + } else { + router.push("/admin"); + } + }; + + // 로그아웃 핸들러 + const handleLogout = async () => { + try { + await logout(); + router.push("/login"); + } catch (error) { + console.error("로그아웃 실패:", error); + } + }; + + // 메뉴 트리 렌더링 (기존 MainLayout 스타일 적용) + const renderMenu = (menu: any, level: number = 0) => { + const isExpanded = expandedMenus.has(menu.id); + + return ( +
+
0 ? "ml-6" : ""}`} + onClick={() => handleMenuClick(menu)} + > +
+ {menu.icon} + {menu.name} +
+ {menu.hasChildren && ( +
+ {isExpanded ? : } +
+ )} +
+ + {/* 서브메뉴 */} + {menu.hasChildren && isExpanded && ( +
+ {menu.children?.map((child: any) => ( +
handleMenuClick(child)} + > + {child.icon} + {child.name} +
+ ))} +
+ )} +
+ ); + }; + + // 사용자 정보가 없으면 로딩 표시 + if (!user) { + return ( +
+
+
+

로딩 중...

+
+
+ ); + } + + // UI 변환된 메뉴 데이터 + const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo); + + return ( +
+ {/* MainHeader 컴포넌트 사용 */} + setSidebarOpen(!sidebarOpen)} + onProfileClick={openProfileModal} + onLogout={handleLogout} + /> + +
+ {/* 모바일 사이드바 오버레이 */} + {sidebarOpen && ( +
setSidebarOpen(false)} /> + )} + + {/* 왼쪽 사이드바 */} + + + {/* 가운데 컨텐츠 영역 */} +
{children}
+
+ + {/* 프로필 수정 모달 */} + +
+ ); +} diff --git a/frontend/components/layout/MainHeader.tsx b/frontend/components/layout/MainHeader.tsx index 6813d5c2..cfad594e 100644 --- a/frontend/components/layout/MainHeader.tsx +++ b/frontend/components/layout/MainHeader.tsx @@ -1,6 +1,5 @@ import { Logo } from "./Logo"; import { SideMenu } from "./SideMenu"; -import { AdminButton } from "./AdminButton"; import { UserDropdown } from "./UserDropdown"; interface MainHeaderProps { @@ -19,8 +18,8 @@ export function MainHeader({ user, onSidebarToggle, onProfileClick, onLogout }:
{/* Left side - Side Menu + Logo */}
- {/* 햄버거 메뉴 버튼 - 모바일에서만 표시 */} -
+ {/* 햄버거 메뉴 버튼 - 1024px 미만에서 표시 */} +
@@ -28,7 +27,6 @@ export function MainHeader({ user, onSidebarToggle, onProfileClick, onLogout }: {/* Right side - Admin Button + User Menu */}
-
diff --git a/frontend/components/layout/MainLayout.tsx b/frontend/components/layout/MainLayout.tsx deleted file mode 100644 index d6c5af17..00000000 --- a/frontend/components/layout/MainLayout.tsx +++ /dev/null @@ -1,407 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { - Shield, - User, - LogOut, - Menu, - X, - Home, - Settings, - BarChart3, - FileText, - Users, - Package, - Wrench, - ChevronDown, - ChevronRight, -} from "lucide-react"; -import { useMenu } from "@/contexts/MenuContext"; -import { useAuth } from "@/hooks/useAuth"; -import { MenuItem } from "@/lib/api/menu"; - -// useAuth의 UserInfo 타입을 확장 -interface ExtendedUserInfo { - userId: string; - userName: string; - userNameEng?: string; - userNameCn?: string; - deptCode?: string; - deptName?: string; - positionCode?: string; - positionName?: string; - email?: string; - tel?: string; - cellPhone?: string; - userType?: string; - userTypeName?: string; - authName?: string; - partnerCd?: string; - isAdmin: boolean; - sabun?: string; - photo?: string | null; - companyCode?: string; - locale?: string; -} - -// useAuth의 UserInfo 타입을 사용하므로 별도 정의 불필요 - -interface MainLayoutProps { - children: React.ReactNode; -} - -// 메뉴 아이콘 매핑 함수 -const getMenuIcon = (menuName: string) => { - const name = menuName.toLowerCase(); - if (name.includes("대시보드") || name.includes("dashboard")) return ; - if (name.includes("관리자") || name.includes("admin")) return ; - if (name.includes("사용자") || name.includes("user")) return ; - if (name.includes("프로젝트") || name.includes("project")) return ; - if (name.includes("제품") || name.includes("product")) return ; - if (name.includes("설정") || name.includes("setting")) return ; - if (name.includes("로그") || name.includes("log")) return ; - if (name.includes("메뉴") || name.includes("menu")) return ; - return ; -}; - -// 메뉴 데이터를 UI용으로 변환하는 함수 -const convertMenuToUI = (menus: MenuItem[], userInfo: ExtendedUserInfo | null, parentId: string = "0"): any[] => { - return menus - .filter((menu) => (menu.parent_obj_id || menu.PARENT_OBJ_ID) === parentId) - .filter((menu) => (menu.status || menu.STATUS) === "active") - .sort((a, b) => (a.seq || a.SEQ || 0) - (b.seq || b.SEQ || 0)) - .map((menu) => { - const menuId = menu.objid || menu.OBJID; - - // 사용자 locale 기준으로 번역 처리 - const getDisplayText = (menu: MenuItem) => { - // 다국어 텍스트가 있으면 사용, 없으면 기본 텍스트 사용 - if (menu.translated_name || menu.TRANSLATED_NAME) { - return menu.translated_name || menu.TRANSLATED_NAME; - } - - const baseName = menu.menu_name_kor || menu.MENU_NAME_KOR || "메뉴명 없음"; - - // 사용자 정보에서 locale 가져오기 - const userLocale = userInfo?.locale || "ko"; - - if (userLocale === "EN") { - // 영어 번역 - const translations: { [key: string]: string } = { - 관리자: "Administrator", - 사용자: "User Management", - 메뉴: "Menu Management", - 대시보드: "Dashboard", - 권한: "Permission Management", - 코드: "Code Management", - 설정: "Settings", - 로그: "Log Management", - 프로젝트: "Project Management", - 제품: "Product Management", - }; - - for (const [korean, english] of Object.entries(translations)) { - if (baseName.includes(korean)) { - return baseName.replace(korean, english); - } - } - } else if (userLocale === "JA") { - // 일본어 번역 - const translations: { [key: string]: string } = { - 관리자: "管理者", - 사용자: "ユーザー管理", - 메뉴: "メニュー管理", - 대시보드: "ダッシュボード", - 권한: "権限管理", - 코드: "コード管理", - 설정: "設定", - 로그: "ログ管理", - 프로젝트: "プロジェクト管理", - 제품: "製品管理", - }; - - for (const [korean, japanese] of Object.entries(translations)) { - if (baseName.includes(korean)) { - return baseName.replace(korean, japanese); - } - } - } else if (userLocale === "ZH") { - // 중국어 번역 - const translations: { [key: string]: string } = { - 관리자: "管理员", - 사용자: "用户管理", - 메뉴: "菜单管理", - 대시보드: "仪表板", - 권한: "权限管理", - 코드: "代码管理", - 설정: "设置", - 로그: "日志管理", - 프로젝트: "项目管理", - 제품: "产品管理", - }; - - for (const [korean, chinese] of Object.entries(translations)) { - if (baseName.includes(korean)) { - return baseName.replace(korean, chinese); - } - } - } - - return baseName; - }; - - const menuName = getDisplayText(menu); - const menuUrl = menu.menu_url || menu.MENU_URL; - - console.log( - `메뉴 ${menuId}: 번역명="${menu.translated_name || menu.TRANSLATED_NAME}", 기본명="${menu.menu_name_kor || menu.MENU_NAME_KOR}", 최종명="${menuName}"`, - ); - - const children = convertMenuToUI(menus, userInfo, menuId); - - return { - id: menuId, - title: menuName, - icon: getMenuIcon(menuName || ""), - path: menuUrl, - children: children.length > 0 ? children : undefined, - }; - }); -}; - -export default function MainLayout({ children }: MainLayoutProps) { - const router = useRouter(); - const { adminMenus, userMenus, loading: menuLoading } = useMenu(); - const { user, loading: authLoading, logout } = useAuth(); - const [sidebarOpen, setSidebarOpen] = useState(true); - const [expandedMenus, setExpandedMenus] = useState([]); - - // 사용자의 회사 코드에 따라 메뉴 필터링 - const filterMenusByCompany = (menus: MenuItem[]) => { - console.log("=== 메뉴 필터링 시작 ==="); - console.log("사용자 정보:", user); - console.log("사용자 회사 코드:", (user as ExtendedUserInfo)?.companyCode); - console.log("전체 메뉴 수:", menus.length); - console.log("현재 경로:", window.location.pathname); - - // 모든 메뉴 표시 (원래 상태로 복원) - console.log("모든 메뉴 표시"); - return menus; - }; - - // 활성 메뉴만 필터링하여 UI용으로 변환 (언어 변경 시 재계산) - const activeMenus = [...adminMenus, ...userMenus].filter((menu) => (menu.status || menu.STATUS) === "active"); - const filteredMenus = filterMenusByCompany(activeMenus); - const menuItems = convertMenuToUI(filteredMenus, user); - - // 디버깅 로그 추가 - console.log("=== MainLayout 메뉴 디버깅 ==="); - console.log("adminMenus 개수:", adminMenus.length); - console.log("userMenus 개수:", userMenus.length); - console.log("activeMenus 개수:", activeMenus.length); - console.log("filteredMenus 개수:", filteredMenus.length); - console.log("menuItems 개수:", menuItems.length); - - // 번역 데이터 확인 - if (menuItems.length > 0) { - console.log("첫 번째 메뉴 아이템:", { - id: menuItems[0].id, - title: menuItems[0].title, - hasChildren: !!menuItems[0].children, - }); - } - - if (adminMenus.length > 0) { - console.log("첫 번째 adminMenu:", adminMenus[0]); - } - if (userMenus.length > 0) { - console.log("첫 번째 userMenu:", userMenus[0]); - } - - // useAuth 훅에서 인증 상태를 관리하므로 별도 인증 확인 불필요 - // useEffect(() => { - // checkAuthStatus(); - // }, []); - - // 키보드 단축키로 사이드바 토글 - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - // Ctrl+B 또는 Cmd+B로 사이드바 토글 - if ((event.ctrlKey || event.metaKey) && event.key === "b") { - event.preventDefault(); - setSidebarOpen((prev) => !prev); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, []); - - const handleLogout = async () => { - await logout(); - }; - - const toggleMenu = (menuId: string) => { - setExpandedMenus((prev) => (prev.includes(menuId) ? prev.filter((id) => id !== menuId) : [...prev, menuId])); - }; - - const handleMenuClick = (path?: string) => { - if (path) { - router.push(path); - } - }; - - if (authLoading) { - return ( -
-
- -

로딩 중...

-
-
- ); - } - - if (!user) { - return null; - } - - return ( -
- {/* 상단 헤더 */} -
-
-
- - -
- -

PLM 솔루션

-
-
- -
-
- - {user.userName} - {user.deptName && ({user.deptName})} -
- - -
-
-
- -
- {/* 모바일 사이드바 오버레이 */} - {sidebarOpen && ( -
setSidebarOpen(false)} /> - )} - - {/* 왼쪽 사이드바 */} - - - {/* 가운데 컨텐츠 영역 */} -
-
{children}
-
-
-
- ); -} diff --git a/frontend/components/layout/UserDropdown.tsx b/frontend/components/layout/UserDropdown.tsx index 68555155..386a4334 100644 --- a/frontend/components/layout/UserDropdown.tsx +++ b/frontend/components/layout/UserDropdown.tsx @@ -8,7 +8,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { LogOut, User, Settings } from "lucide-react"; +import { LogOut, User } from "lucide-react"; interface UserDropdownProps { user: any; @@ -50,12 +50,9 @@ export function UserDropdown({ user, onProfileClick, onLogout }: UserDropdownPro

{user.deptName && user.positionName ? `${user.deptName}, ${user.positionName}` - : user.deptName || user.positionName || "정보 없음"} + : user.deptName || user.positionName || "부서 정보 없음"}

{/* 사진 상태 표시 */} -

- {user.photo ? "프로필 사진 있음" : "프로필 사진이 없습니다"} -

@@ -64,11 +61,6 @@ export function UserDropdown({ user, onProfileClick, onLogout }: UserDropdownPro 프로필 - - - 설정 - - 로그아웃 diff --git a/frontend/components/ui/dropdown-menu.tsx b/frontend/components/ui/dropdown-menu.tsx index 90c19821..7aaa6d81 100644 --- a/frontend/components/ui/dropdown-menu.tsx +++ b/frontend/components/ui/dropdown-menu.tsx @@ -81,7 +81,7 @@ const DropdownMenuItem = React.forwardRef< Date: Tue, 26 Aug 2025 17:21:27 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=EC=9E=90=EB=B0=94=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=EC=9A=A9=20no-unused-vars=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=EB=81=84=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/eslint.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 3e63a960..4c35d9c3 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -24,7 +24,7 @@ const eslintConfig = [ "react/no-unescaped-entities": "warn", // 기타 자주 발생하는 오류들을 경고로 변경 "prefer-const": "warn", - "no-unused-vars": "warn", + "no-unused-vars": "off", // TypeScript에서는 @typescript-eslint/no-unused-vars 사용 // Prettier와 일치하는 quote 규칙 quotes: ["warn", "double", { avoidEscape: true }], semi: ["warn", "always"], -- 2.43.0 From 4799e9597f0d2736dac7a9c8fc669d5b57f7cca5 Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Tue, 26 Aug 2025 17:51:23 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/(main)/admin/page.tsx | 18 +------ frontend/app/(main)/page.tsx | 17 ------ frontend/components/layout/AppLayout.tsx | 11 ++-- frontend/components/layout/PageHeader.tsx | 21 ++++++++ frontend/constants/pageInfo.ts | 64 +++++++++++++++++++++++ 5 files changed, 94 insertions(+), 37 deletions(-) create mode 100644 frontend/components/layout/PageHeader.tsx create mode 100644 frontend/constants/pageInfo.ts diff --git a/frontend/app/(main)/admin/page.tsx b/frontend/app/(main)/admin/page.tsx index 956792fd..3f54bb3a 100644 --- a/frontend/app/(main)/admin/page.tsx +++ b/frontend/app/(main)/admin/page.tsx @@ -1,26 +1,10 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { RefreshCw, Users, Shield, Settings, BarChart3 } from "lucide-react"; - +import { Users, Shield, Settings, BarChart3 } from "lucide-react"; /** * 관리자 메인 페이지 */ export default function AdminPage() { return (
- {/* 페이지 헤더 */} -
-
-

관리자 대시보드

-

시스템 관리 및 모니터링

-
- -
- {/* 관리자 기능 카드들 */}
diff --git a/frontend/app/(main)/page.tsx b/frontend/app/(main)/page.tsx index 72539e31..3d1739a6 100644 --- a/frontend/app/(main)/page.tsx +++ b/frontend/app/(main)/page.tsx @@ -1,23 +1,6 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { RefreshCw } from "lucide-react"; - export default function MainHomePage() { return (
- {/* 페이지 헤더 */} -
-
-

대시보드

-

PLM 시스템의 주요 현황을 확인하세요

-
- -
- {/* 대시보드 컨텐츠 */}

PLM 솔루션에 오신 것을 환영합니다!

diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index bc5473c5..35597eb3 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -22,6 +22,8 @@ import { useProfile } from "@/hooks/useProfile"; import { MenuItem } from "@/lib/api/menu"; import { MainHeader } from "./MainHeader"; import { ProfileModal } from "./ProfileModal"; +import { PageHeader } from "./PageHeader"; +import { getPageInfo } from "@/constants/pageInfo"; // useAuth의 UserInfo 타입을 확장 interface ExtendedUserInfo { @@ -304,9 +306,9 @@ export function AppLayout({ children }: AppLayoutProps) { if (!user) { return (
-
+
-

로딩 중...

+

로딩중...

); @@ -379,7 +381,10 @@ export function AppLayout({ children }: AppLayoutProps) { {/* 가운데 컨텐츠 영역 */} -
{children}
+
+ + {children} +
{/* 프로필 수정 모달 */} diff --git a/frontend/components/layout/PageHeader.tsx b/frontend/components/layout/PageHeader.tsx new file mode 100644 index 00000000..63318807 --- /dev/null +++ b/frontend/components/layout/PageHeader.tsx @@ -0,0 +1,21 @@ +interface PageHeaderProps { + title: string; + description?: string; +} + +/** + * 페이지 상단 헤더 컴포넌트 + * 제목, 설명, 추가 버튼 등을 표시 + */ +export function PageHeader({ title, description }: PageHeaderProps) { + return ( +
+
+
+

{title}

+ {description &&

{description}

} +
+
+
+ ); +} diff --git a/frontend/constants/pageInfo.ts b/frontend/constants/pageInfo.ts new file mode 100644 index 00000000..3652cdbf --- /dev/null +++ b/frontend/constants/pageInfo.ts @@ -0,0 +1,64 @@ +/** + * 페이지별 제목과 설명 정보 + */ +export interface PageInfo { + title: string; + description?: string; +} + +export const PAGE_INFO: Record = { + // 메인 대시보드 + "/main": { + title: "대시보드", + description: "PLM 시스템의 주요 현황을 확인하세요", + }, + + // 관리자 페이지들 + "/admin": { + title: "관리자 대시보드", + description: "시스템 관리 및 모니터링", + }, + "/admin/company": { + title: "회사 관리", + description: "회사 정보를 등록하고 관리합니다", + }, + "/admin/userMng": { + title: "사용자 관리", + description: "시스템 사용자를 관리합니다", + }, + "/admin/menu": { + title: "메뉴 관리", + description: "시스템 메뉴를 관리합니다", + }, + "/admin/i18n": { + title: "다국어 관리", + description: "다국어 번역을 관리합니다", + }, + "/admin/tableMng": { + title: "테이블 타입 관리", + description: "데이터베이스 테이블 타입을 관리합니다", + }, + + // 기타 페이지들 + "/multilang": { + title: "다국어 설정", + description: "언어 설정을 변경합니다", + }, + "/dashboard": { + title: "대시보드", + description: "PLM 시스템 현황", + }, + + // 기본값 (매핑되지 않은 페이지) + default: { + title: "PLM 솔루션", + description: "제품 수명 주기 관리 시스템", + }, +}; + +/** + * 현재 경로에 맞는 페이지 정보를 반환 + */ +export function getPageInfo(pathname: string): PageInfo { + return PAGE_INFO[pathname] || PAGE_INFO["default"]; +} -- 2.43.0 From 11edbb2d185b160e89e491169d5d6cc39cd7e16c Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Tue, 26 Aug 2025 18:02:37 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=EB=A1=9C=EB=94=A9=20=EC=8A=A4=ED=94=BC?= =?UTF-8?q?=EB=84=88=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/(main)/main/page.tsx | 19 ------------------- frontend/components/auth/AuthGuard.tsx | 1 - 2 files changed, 20 deletions(-) diff --git a/frontend/app/(main)/main/page.tsx b/frontend/app/(main)/main/page.tsx index 8e5a1969..7cecbefb 100644 --- a/frontend/app/(main)/main/page.tsx +++ b/frontend/app/(main)/main/page.tsx @@ -2,8 +2,6 @@ import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { RefreshCw } from "lucide-react"; /** * 메인 페이지 컴포넌트 @@ -12,23 +10,6 @@ import { RefreshCw } from "lucide-react"; export default function MainPage() { return (
- {/* Header */} -
-
-

대시보드

-

PLM 시스템의 주요 현황을 확인하세요

-
- -
- {/* 메인 컨텐츠 */} {/* Welcome Message */} diff --git a/frontend/components/auth/AuthGuard.tsx b/frontend/components/auth/AuthGuard.tsx index 8ad13be0..4a47de35 100644 --- a/frontend/components/auth/AuthGuard.tsx +++ b/frontend/components/auth/AuthGuard.tsx @@ -3,7 +3,6 @@ import { useEffect, ReactNode, useState } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/hooks/useAuth"; -import { LoadingSpinner } from "@/components/common/LoadingSpinner"; interface AuthGuardProps { children: ReactNode; -- 2.43.0 From c5fe88a911513588c8d0effd87a8ef51474c0d20 Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Tue, 26 Aug 2025 18:33:04 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A7=80?= =?UTF-8?q?=EC=97=AD=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/contexts/MenuContext.tsx | 24 ++++++++++++++++++++++++ frontend/lib/api/client.ts | 26 +++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/frontend/contexts/MenuContext.tsx b/frontend/contexts/MenuContext.tsx index f1aa7bb1..99084c41 100644 --- a/frontend/contexts/MenuContext.tsx +++ b/frontend/contexts/MenuContext.tsx @@ -49,6 +49,30 @@ export function MenuProvider({ children }: { children: ReactNode }) { setLoading(true); console.log("=== MenuContext API 호출 시작 ==="); + // 사용자 로케일이 로드될 때까지 잠시 대기 + let retryCount = 0; + const maxRetries = 20; // 최대 2초 대기 (100ms * 20) + + while (retryCount < maxRetries) { + if (typeof window !== "undefined") { + const hasGlobalLang = !!(window as any).__GLOBAL_USER_LANG; + const hasStoredLang = !!localStorage.getItem("userLocale"); + + if (hasGlobalLang || hasStoredLang) { + console.log("✅ 사용자 로케일 로드 완료, 메뉴 API 호출 진행"); + break; + } + } + + console.log(`⏳ 사용자 로케일 로드 대기 중... (${retryCount + 1}/${maxRetries})`); + await new Promise((resolve) => setTimeout(resolve, 100)); + retryCount++; + } + + if (retryCount >= maxRetries) { + console.warn("⚠️ 사용자 로케일 로드 타임아웃, 기본값으로 진행"); + } + // 관리자 메뉴와 사용자 메뉴를 병렬로 로드 const [adminResponse, userResponse] = await Promise.all([menuApi.getAdminMenus(), menuApi.getUserMenus()]); diff --git a/frontend/lib/api/client.ts b/frontend/lib/api/client.ts index f3dea2bf..ada8a972 100644 --- a/frontend/lib/api/client.ts +++ b/frontend/lib/api/client.ts @@ -61,9 +61,29 @@ apiClient.interceptors.request.use( // 언어 정보를 쿼리 파라미터에 추가 (GET 요청 시에만) if (config.method?.toUpperCase() === "GET") { - // 전역 언어 상태에서 현재 언어 가져오기 (DB 값 그대로 사용) - const currentLang = typeof window !== "undefined" ? (window as any).__GLOBAL_USER_LANG || "KR" : "KR"; - console.log("🌐 API 요청 시 언어 정보:", currentLang); + // 우선순위: 전역 변수 > localStorage > 기본값 + let currentLang = "KR"; // 기본값 + + if (typeof window !== "undefined") { + // 1순위: 전역 변수에서 확인 + if ((window as any).__GLOBAL_USER_LANG) { + currentLang = (window as any).__GLOBAL_USER_LANG; + } + // 2순위: localStorage에서 확인 (새 창이나 페이지 새로고침 시) + else { + const storedLocale = localStorage.getItem("userLocale"); + if (storedLocale) { + currentLang = storedLocale; + } + } + } + + console.log("🌐 API 요청 시 언어 정보:", { + currentLang, + globalVar: (window as any).__GLOBAL_USER_LANG, + localStorage: typeof window !== "undefined" ? localStorage.getItem("userLocale") : null, + url: config.url, + }); if (config.params) { config.params.userLang = currentLang; -- 2.43.0