diff --git a/backend-node/src/controllers/authController.ts b/backend-node/src/controllers/authController.ts index 6f72eb10..1903d397 100644 --- a/backend-node/src/controllers/authController.ts +++ b/backend-node/src/controllers/authController.ts @@ -141,6 +141,110 @@ export class AuthController { } } + /** + * POST /api/auth/switch-company + * WACE 관리자 전용: 다른 회사로 전환 + */ + static async switchCompany(req: Request, res: Response): Promise { + try { + const { companyCode } = req.body; + const authHeader = req.get("Authorization"); + const token = authHeader && authHeader.split(" ")[1]; + + if (!token) { + res.status(401).json({ + success: false, + message: "인증 토큰이 필요합니다.", + error: { code: "TOKEN_MISSING" }, + }); + return; + } + + // 현재 사용자 정보 확인 + const currentUser = JwtUtils.verifyToken(token); + + // WACE 관리자 권한 체크 (userType = "SUPER_ADMIN"만 확인) + // 이미 다른 회사로 전환한 상태(companyCode != "*")에서도 다시 전환 가능해야 함 + if (currentUser.userType !== "SUPER_ADMIN") { + logger.warn(`회사 전환 권한 없음: userId=${currentUser.userId}, userType=${currentUser.userType}, companyCode=${currentUser.companyCode}`); + res.status(403).json({ + success: false, + message: "회사 전환은 최고 관리자(SUPER_ADMIN)만 가능합니다.", + error: { code: "FORBIDDEN" }, + }); + return; + } + + // 전환할 회사 코드 검증 + if (!companyCode || companyCode.trim() === "") { + res.status(400).json({ + success: false, + message: "전환할 회사 코드가 필요합니다.", + error: { code: "INVALID_INPUT" }, + }); + return; + } + + logger.info(`=== WACE 관리자 회사 전환 ===`, { + userId: currentUser.userId, + originalCompanyCode: currentUser.companyCode, + targetCompanyCode: companyCode, + }); + + // 회사 코드 존재 여부 확인 (company_code가 "*"가 아닌 경우만) + if (companyCode !== "*") { + const { query } = await import("../database/db"); + const companies = await query( + "SELECT company_code, company_name FROM company_mng WHERE company_code = $1", + [companyCode] + ); + + if (companies.length === 0) { + res.status(404).json({ + success: false, + message: "존재하지 않는 회사 코드입니다.", + error: { code: "COMPANY_NOT_FOUND" }, + }); + return; + } + } + + // 새로운 JWT 토큰 발급 (company_code만 변경) + const newPersonBean: PersonBean = { + ...currentUser, + companyCode: companyCode.trim(), // 전환할 회사 코드로 변경 + }; + + const newToken = JwtUtils.generateToken(newPersonBean); + + logger.info(`✅ 회사 전환 성공: ${currentUser.userId} → ${companyCode}`); + + res.status(200).json({ + success: true, + message: "회사 전환 완료", + data: { + token: newToken, + companyCode: companyCode.trim(), + }, + }); + } catch (error) { + logger.error( + `회사 전환 API 오류: ${error instanceof Error ? error.message : error}` + ); + res.status(500).json({ + success: false, + message: "회사 전환 중 오류가 발생했습니다.", + error: { + code: "SERVER_ERROR", + details: + error instanceof Error + ? error.message + : "알 수 없는 오류가 발생했습니다.", + }, + }); + } + } + /** * POST /api/auth/logout * 기존 Java ApiLoginController.logout() 메서드 포팅 @@ -226,13 +330,14 @@ export class AuthController { } // 프론트엔드 호환성을 위해 더 많은 사용자 정보 반환 + // ⚠️ JWT 토큰의 companyCode를 우선 사용 (회사 전환 기능 지원) const userInfoResponse: any = { userId: dbUserInfo.userId, userName: dbUserInfo.userName || "", deptName: dbUserInfo.deptName || "", - companyCode: dbUserInfo.companyCode || "ILSHIN", - company_code: dbUserInfo.companyCode || "ILSHIN", // 프론트엔드 호환성 - userType: dbUserInfo.userType || "USER", + companyCode: userInfo.companyCode || dbUserInfo.companyCode || "ILSHIN", // JWT 토큰 우선 + company_code: userInfo.companyCode || dbUserInfo.companyCode || "ILSHIN", // JWT 토큰 우선 + userType: userInfo.userType || dbUserInfo.userType || "USER", // JWT 토큰 우선 userTypeName: dbUserInfo.userTypeName || "일반사용자", email: dbUserInfo.email || "", photo: dbUserInfo.photo, diff --git a/backend-node/src/routes/authRoutes.ts b/backend-node/src/routes/authRoutes.ts index adba86e6..7ed87a06 100644 --- a/backend-node/src/routes/authRoutes.ts +++ b/backend-node/src/routes/authRoutes.ts @@ -47,4 +47,10 @@ router.post("/refresh", AuthController.refreshToken); */ router.post("/signup", AuthController.signup); +/** + * POST /api/auth/switch-company + * WACE 관리자 전용: 다른 회사로 전환 + */ +router.post("/switch-company", AuthController.switchCompany); + export default router; diff --git a/backend-node/src/services/adminService.ts b/backend-node/src/services/adminService.ts index 5ca6b392..1b9280db 100644 --- a/backend-node/src/services/adminService.ts +++ b/backend-node/src/services/adminService.ts @@ -412,9 +412,9 @@ export class AdminService { let queryParams: any[] = [userLang]; let paramIndex = 2; - if (userType === "SUPER_ADMIN" && userCompanyCode === "*") { - // SUPER_ADMIN: 권한 그룹 체크 없이 공통 메뉴만 표시 - logger.info("✅ 좌측 사이드바 (SUPER_ADMIN): 공통 메뉴만 표시"); + if (userType === "SUPER_ADMIN") { + // SUPER_ADMIN: 권한 그룹 체크 없이 해당 회사의 모든 메뉴 표시 + logger.info(`✅ 좌측 사이드바 (SUPER_ADMIN): 회사 ${userCompanyCode}의 모든 메뉴 표시`); authFilter = ""; unionFilter = ""; } else { diff --git a/frontend/app/(main)/admin/menu/page.tsx b/frontend/app/(main)/admin/menu/page.tsx index 4e9ff1d4..85d5b346 100644 --- a/frontend/app/(main)/admin/menu/page.tsx +++ b/frontend/app/(main)/admin/menu/page.tsx @@ -1,9 +1,880 @@ "use client"; -import { MenuManagement } from "@/components/admin/MenuManagement"; +import React, { useState, useEffect, useMemo } from "react"; +import { menuApi } from "@/lib/api/menu"; +import type { MenuItem } from "@/lib/api/menu"; +import { MenuTable } from "@/components/admin/MenuTable"; +import { MenuFormModal } from "@/components/admin/MenuFormModal"; +import { MenuCopyDialog } from "@/components/admin/MenuCopyDialog"; +import { Button } from "@/components/ui/button"; +import { LoadingSpinner, LoadingOverlay } from "@/components/common/LoadingSpinner"; +import { toast } from "sonner"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { useMenu } from "@/contexts/MenuContext"; +import { useMenuManagementText, setTranslationCache, getMenuTextSync } from "@/lib/utils/multilang"; +import { useMultiLang } from "@/hooks/useMultiLang"; +import { apiClient } from "@/lib/api/client"; +import { useAuth } from "@/hooks/useAuth"; // useAuth 추가 import { ScrollToTop } from "@/components/common/ScrollToTop"; +type MenuType = "admin" | "user"; + export default function MenuPage() { + const { adminMenus, userMenus, refreshMenus } = useMenu(); + const { user } = useAuth(); // 현재 사용자 정보 가져오기 + const [selectedMenuType, setSelectedMenuType] = useState("admin"); + const [loading, setLoading] = useState(false); + const [deleting, setDeleting] = useState(false); + const [formModalOpen, setFormModalOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [copyDialogOpen, setCopyDialogOpen] = useState(false); + const [selectedMenuId, setSelectedMenuId] = useState(""); + const [selectedMenuName, setSelectedMenuName] = useState(""); + const [selectedMenus, setSelectedMenus] = useState>(new Set()); + + // 메뉴 관리 화면용 로컬 상태 (모든 상태의 메뉴 표시) + const [localAdminMenus, setLocalAdminMenus] = useState([]); + const [localUserMenus, setLocalUserMenus] = useState([]); + + // 다국어 텍스트 훅 사용 + // getMenuText는 더 이상 사용하지 않음 - getUITextSync만 사용 + const { userLang } = useMultiLang({ companyCode: "*" }); + + // SUPER_ADMIN 여부 확인 + const isSuperAdmin = user?.userType === "SUPER_ADMIN"; + + // 다국어 텍스트 상태 + const [uiTexts, setUiTexts] = useState>({}); + const [uiTextsLoading, setUiTextsLoading] = useState(false); + + // 회사 목록 상태 + const [companies, setCompanies] = useState>([]); + const [selectedCompany, setSelectedCompany] = useState("all"); + const [searchText, setSearchText] = useState(""); + const [expandedMenus, setExpandedMenus] = useState>(new Set()); + const [companySearchText, setCompanySearchText] = useState(""); + const [isCompanyDropdownOpen, setIsCompanyDropdownOpen] = useState(false); + const [formData, setFormData] = useState({ + menuId: "", + parentId: "", + menuType: "", + level: 0, + parentCompanyCode: "", + }); + + // 언어별 텍스트 매핑 테이블 제거 - DB에서 직접 가져옴 + + // 메뉴관리 페이지에서 사용할 다국어 키들 (실제 DB에 등록된 키들) + const MENU_MANAGEMENT_LANG_KEYS = [ + // 페이지 제목 및 설명 + "menu.management.title", + "menu.management.description", + "menu.type.title", + "menu.type.admin", + "menu.type.user", + "menu.management.admin", + "menu.management.user", + "menu.management.admin.description", + "menu.management.user.description", + + // 버튼 + "button.add", + "button.add.top.level", + "button.add.sub", + "button.edit", + "button.delete", + "button.delete.selected", + "button.delete.selected.count", + "button.delete.processing", + "button.cancel", + "button.save", + "button.register", + "button.modify", + + // 필터 및 검색 + "filter.company", + "filter.company.all", + "filter.company.common", + "filter.company.search", + "filter.search", + "filter.search.placeholder", + "filter.reset", + + // 테이블 헤더 + "table.header.select", + "table.header.menu.name", + "table.header.menu.url", + "table.header.menu.type", + "table.header.status", + "table.header.company", + "table.header.sequence", + "table.header.actions", + + // 상태 + "status.active", + "status.inactive", + "status.unspecified", + + // 폼 + "form.menu.type", + "form.menu.type.admin", + "form.menu.type.user", + "form.company", + "form.company.select", + "form.company.common", + "form.company.submenu.note", + "form.lang.key", + "form.lang.key.select", + "form.lang.key.none", + "form.lang.key.search", + "form.lang.key.selected", + "form.menu.name", + "form.menu.name.placeholder", + "form.menu.url", + "form.menu.url.placeholder", + "form.menu.description", + "form.menu.description.placeholder", + "form.menu.sequence", + + // 모달 + "modal.menu.register.title", + "modal.menu.modify.title", + "modal.delete.title", + "modal.delete.description", + "modal.delete.batch.description", + + // 메시지 + "message.loading", + "message.menu.delete.processing", + "message.menu.save.success", + "message.menu.save.failed", + "message.menu.delete.success", + "message.menu.delete.failed", + "message.menu.delete.batch.success", + "message.menu.delete.batch.partial", + "message.menu.status.toggle.success", + "message.menu.status.toggle.failed", + "message.validation.menu.name.required", + "message.validation.company.required", + "message.validation.select.menu.delete", + "message.error.load.menu.list", + "message.error.load.menu.info", + "message.error.load.company.list", + "message.error.load.lang.key.list", + + // 리스트 정보 + "menu.list.title", + "menu.list.total", + "menu.list.search.result", + + // UI + "ui.expand", + "ui.collapse", + "ui.menu.collapse", + "ui.language", + ]; + + // 초기 로딩 + useEffect(() => { + loadCompanies(); + loadMenus(false); // 메뉴 목록 로드 (메뉴 관리 화면용 - 모든 상태 표시) + // 사용자 언어가 설정되지 않았을 때만 기본 텍스트 설정 + if (!userLang) { + initializeDefaultTexts(); + } + }, [userLang]); // userLang 변경 시마다 실행 + + // 초기 기본 텍스트 설정 함수 + const initializeDefaultTexts = () => { + const defaultTexts: Record = {}; + MENU_MANAGEMENT_LANG_KEYS.forEach((key) => { + // 기본 한국어 텍스트 제공 + const defaultText = getDefaultText(key); + defaultTexts[key] = defaultText; + }); + setUiTexts(defaultTexts); + // console.log("🌐 초기 기본 텍스트 설정 완료:", Object.keys(defaultTexts).length); + }; + + // 기본 텍스트 반환 함수 + const getDefaultText = (key: string): string => { + const defaultTexts: Record = { + "menu.management.title": "메뉴 관리", + "menu.management.description": "시스템의 메뉴 구조와 권한을 관리합니다.", + "menu.type.title": "메뉴 타입", + "menu.type.admin": "관리자", + "menu.type.user": "사용자", + "menu.management.admin": "관리자 메뉴", + "menu.management.user": "사용자 메뉴", + "menu.management.admin.description": "시스템 관리 및 설정 메뉴", + "menu.management.user.description": "일반 사용자 업무 메뉴", + "button.add": "추가", + "button.add.top.level": "최상위 메뉴 추가", + "button.add.sub": "하위 메뉴 추가", + "button.edit": "수정", + "button.delete": "삭제", + "button.delete.selected": "선택 삭제", + "button.delete.selected.count": "선택 삭제 ({count})", + "button.delete.processing": "삭제 중...", + "button.cancel": "취소", + "button.save": "저장", + "button.register": "등록", + "button.modify": "수정", + "filter.company": "회사", + "filter.company.all": "전체", + "filter.company.common": "공통", + "filter.company.search": "회사 검색", + "filter.search": "검색", + "filter.search.placeholder": "메뉴명 또는 URL로 검색...", + "filter.reset": "초기화", + "table.header.select": "선택", + "table.header.menu.name": "메뉴명", + "table.header.menu.url": "URL", + "table.header.menu.type": "메뉴 타입", + "table.header.status": "상태", + "table.header.company": "회사", + "table.header.sequence": "순서", + "table.header.actions": "작업", + "status.active": "활성화", + "status.inactive": "비활성화", + "status.unspecified": "미지정", + "form.menu.type": "메뉴 타입", + "form.menu.type.admin": "관리자", + "form.menu.type.user": "사용자", + "form.company": "회사", + "form.company.select": "회사를 선택하세요", + "form.company.common": "공통", + "form.company.submenu.note": "하위 메뉴는 상위 메뉴와 동일한 회사를 가져야 합니다.", + "form.lang.key": "다국어 키", + "form.lang.key.select": "다국어 키를 선택하세요", + "form.lang.key.none": "다국어 키 없음", + "form.lang.key.search": "다국어 키 검색...", + "form.lang.key.selected": "선택된 키: {key} - {description}", + "form.menu.name": "메뉴명", + "form.menu.name.placeholder": "메뉴명을 입력하세요", + "form.menu.url": "URL", + "form.menu.url.placeholder": "메뉴 URL을 입력하세요", + "form.menu.description": "설명", + "form.menu.description.placeholder": "메뉴 설명을 입력하세요", + "form.menu.sequence": "순서", + "modal.menu.register.title": "메뉴 등록", + "modal.menu.modify.title": "메뉴 수정", + "modal.delete.title": "메뉴 삭제", + "modal.delete.description": "해당 메뉴를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "modal.delete.batch.description": + "선택된 {count}개의 메뉴를 영구적으로 삭제하시겠습니까?\n\n⚠️ 주의: 상위 메뉴를 삭제하면 하위 메뉴들도 함께 삭제됩니다.\n이 작업은 되돌릴 수 없습니다.", + "message.loading": "로딩 중...", + "message.menu.delete.processing": "메뉴 삭제 중...", + "message.menu.save.success": "메뉴가 성공적으로 저장되었습니다.", + "message.menu.save.failed": "메뉴 저장에 실패했습니다.", + "message.menu.delete.success": "메뉴가 성공적으로 삭제되었습니다.", + "message.menu.delete.failed": "메뉴 삭제에 실패했습니다.", + "message.menu.delete.batch.success": "선택된 메뉴들이 성공적으로 삭제되었습니다.", + "message.menu.delete.batch.partial": "일부 메뉴 삭제에 실패했습니다.", + "message.menu.status.toggle.success": "메뉴 상태가 변경되었습니다.", + "message.menu.status.toggle.failed": "메뉴 상태 변경에 실패했습니다.", + "message.validation.menu.name.required": "메뉴명을 입력해주세요.", + "message.validation.company.required": "회사를 선택해주세요.", + "message.validation.select.menu.delete": "삭제할 메뉴를 선택해주세요.", + "message.error.load.menu.list": "메뉴 목록을 불러오는데 실패했습니다.", + "message.error.load.menu.info": "메뉴 정보를 불러오는데 실패했습니다.", + "message.error.load.company.list": "회사 목록을 불러오는데 실패했습니다.", + "message.error.load.lang.key.list": "다국어 키 목록을 불러오는데 실패했습니다.", + "menu.list.title": "메뉴 목록", + "menu.list.total": "총 {count}개", + "menu.list.search.result": "검색 결과: {count}개", + "ui.expand": "펼치기", + "ui.collapse": "접기", + "ui.menu.collapse": "메뉴 접기", + "ui.language": "언어", + }; + + return defaultTexts[key] || key; + }; + + // 컴포넌트 마운트 시 및 userLang 변경 시 다국어 텍스트 로드 + useEffect(() => { + if (userLang && !uiTextsLoading) { + loadUITexts(); + } + }, [userLang]); // userLang 변경 시마다 실행 + + // uiTexts 상태 변경 감지 + useEffect(() => { + // console.log("🔄 uiTexts 상태 변경됨:", { + // count: Object.keys(uiTexts).length, + // sampleKeys: Object.keys(uiTexts).slice(0, 5), + // sampleValues: Object.entries(uiTexts) + // .slice(0, 3) + // .map(([k, v]) => `${k}: ${v}`), + // }); + }, [uiTexts]); + + // 컴포넌트 마운트 후 다국어 텍스트 강제 로드 (userLang이 아직 설정되지 않았을 수 있음) + useEffect(() => { + const timer = setTimeout(() => { + if (userLang && !uiTextsLoading) { + // console.log("🔄 컴포넌트 마운트 후 다국어 텍스트 강제 로드"); + loadUITexts(); + } + }, 300); // 300ms 후 실행 + + return () => clearTimeout(timer); + }, [userLang]); // userLang이 설정된 후 실행 + + // 추가 안전장치: 컴포넌트 마운트 후 일정 시간이 지나면 강제로 다국어 텍스트 로드 + useEffect(() => { + const fallbackTimer = setTimeout(() => { + if (!uiTextsLoading && Object.keys(uiTexts).length === 0) { + // console.log("🔄 안전장치: 컴포넌트 마운트 후 강제 다국어 텍스트 로드"); + // 사용자 언어가 설정되지 않았을 때만 기본 텍스트 설정 + if (!userLang) { + initializeDefaultTexts(); + } else { + // 사용자 언어가 설정된 경우 다국어 텍스트 로드 + loadUITexts(); + } + } + }, 1000); // 1초 후 실행 + + return () => clearTimeout(fallbackTimer); + }, [userLang]); // userLang 변경 시마다 실행 + + // 번역 로드 이벤트 감지 + useEffect(() => { + const handleTranslationLoaded = (event: CustomEvent) => { + const { key, text, userLang: loadedLang } = event.detail; + if (loadedLang === userLang) { + setUiTexts((prev) => ({ ...prev, [key]: text })); + } + }; + + window.addEventListener("translation-loaded", handleTranslationLoaded as EventListener); + + return () => { + window.removeEventListener("translation-loaded", handleTranslationLoaded as EventListener); + }; + }, [userLang]); + + // 드롭다운 외부 클릭 시 닫기 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Element; + if (!target.closest(".company-dropdown")) { + setIsCompanyDropdownOpen(false); + setCompanySearchText(""); + } + }; + + if (isCompanyDropdownOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isCompanyDropdownOpen]); + + // 특정 메뉴 타입만 로드하는 함수 + const loadMenusForType = async (type: MenuType, showLoading = true) => { + try { + if (showLoading) { + setLoading(true); + } + + if (type === "admin") { + const adminResponse = await menuApi.getAdminMenusForManagement(); + if (adminResponse.success && adminResponse.data) { + setLocalAdminMenus(adminResponse.data); + } + } else { + const userResponse = await menuApi.getUserMenusForManagement(); + if (userResponse.success && userResponse.data) { + setLocalUserMenus(userResponse.data); + } + } + } catch (error) { + toast.error(getUITextSync("message.error.load.menu.list")); + } finally { + if (showLoading) { + setLoading(false); + } + } + }; + + const loadMenus = async (showLoading = true) => { + // console.log(`📋 메뉴 목록 조회 시작 (showLoading: ${showLoading})`); + try { + if (showLoading) { + setLoading(true); + } + + // 선택된 메뉴 타입에 해당하는 메뉴만 로드 + if (selectedMenuType === "admin") { + const adminResponse = await menuApi.getAdminMenusForManagement(); + if (adminResponse.success && adminResponse.data) { + setLocalAdminMenus(adminResponse.data); + } + } else { + const userResponse = await menuApi.getUserMenusForManagement(); + if (userResponse.success && userResponse.data) { + setLocalUserMenus(userResponse.data); + } + } + + // 전역 메뉴 상태도 업데이트 (좌측 사이드바용) + await refreshMenus(); + // console.log("📋 메뉴 목록 조회 성공"); + } catch (error) { + // console.error("❌ 메뉴 목록 조회 실패:", error); + toast.error(getUITextSync("message.error.load.menu.list")); + } finally { + if (showLoading) { + setLoading(false); + } + } + }; + + // 회사 목록 조회 + const loadCompanies = async () => { + // console.log("🏢 회사 목록 조회 시작"); + try { + const response = await apiClient.get("/admin/companies"); + + if (response.data.success) { + // console.log("🏢 회사 목록 응답:", response.data); + const companyList = response.data.data.map((company: any) => ({ + code: company.company_code || company.companyCode, + name: company.company_name || company.companyName, + })); + // console.log("🏢 변환된 회사 목록:", companyList); + setCompanies(companyList); + } + } catch (error) { + // console.error("❌ 회사 목록 조회 실패:", error); + } + }; + + // 다국어 텍스트 로드 함수 - 배치 API 사용 + const loadUITexts = async () => { + if (uiTextsLoading) return; // 이미 로딩 중이면 중단 + + // userLang이 설정되지 않았으면 기본값 설정 + if (!userLang) { + // console.log("🌐 사용자 언어가 설정되지 않음, 기본값 설정"); + const defaultTexts: Record = {}; + MENU_MANAGEMENT_LANG_KEYS.forEach((key) => { + defaultTexts[key] = getDefaultText(key); // 기본 한국어 텍스트 사용 + }); + setUiTexts(defaultTexts); + return; + } + + // 사용자 언어가 설정된 경우, 기존 uiTexts가 비어있으면 기본 텍스트로 초기화 + if (Object.keys(uiTexts).length === 0) { + // console.log("🌐 기존 uiTexts가 비어있음, 기본 텍스트로 초기화"); + const defaultTexts: Record = {}; + MENU_MANAGEMENT_LANG_KEYS.forEach((key) => { + defaultTexts[key] = getDefaultText(key); + }); + setUiTexts(defaultTexts); + } + + // console.log("🌐 UI 다국어 텍스트 로드 시작", { + // userLang, + // apiParams: { + // companyCode: "*", + // menuCode: "menu.management", + // userLang: userLang, + // }, + // }); + setUiTextsLoading(true); + + try { + // 배치 API를 사용하여 모든 다국어 키를 한 번에 조회 + const response = await apiClient.post( + "/multilang/batch", + { + langKeys: MENU_MANAGEMENT_LANG_KEYS, + companyCode: "*", // 모든 회사 + menuCode: "menu.management", // 메뉴관리 메뉴 + userLang: userLang, // body에 포함 + }, + { + params: {}, // query params는 비움 + }, + ); + + if (response.data.success) { + const translations = response.data.data; + // console.log("🌐 배치 다국어 텍스트 응답:", translations); + + // 번역 결과를 상태에 저장 (기존 uiTexts와 병합) + const mergedTranslations = { ...uiTexts, ...translations }; + // console.log("🔧 setUiTexts 호출 전:", { + // translationsCount: Object.keys(translations).length, + // mergedCount: Object.keys(mergedTranslations).length, + // }); + setUiTexts(mergedTranslations); + // console.log("🔧 setUiTexts 호출 후 - mergedTranslations:", mergedTranslations); + + // 번역 캐시에 저장 (다른 컴포넌트에서도 사용할 수 있도록) + setTranslationCache(userLang, mergedTranslations); + } else { + // console.error("❌ 다국어 텍스트 배치 조회 실패:", response.data.message); + // API 실패 시에도 기존 uiTexts는 유지 + // console.log("🔄 API 실패로 인해 기존 uiTexts 유지"); + } + } catch (error) { + // console.error("❌ UI 다국어 텍스트 로드 실패:", error); + // API 실패 시에도 기존 uiTexts는 유지 + // console.log("🔄 API 실패로 인해 기존 uiTexts 유지"); + } finally { + setUiTextsLoading(false); + } + }; + + // UI 텍스트 가져오기 함수 (동기 버전만 사용) + // getUIText 함수는 제거 - getUITextSync만 사용 + + // 동기 버전 (DB에서 가져온 번역 텍스트 사용) + const getUITextSync = (key: string, params?: Record, fallback?: string): string => { + // uiTexts에서 번역 텍스트 찾기 + let text = uiTexts[key]; + + // uiTexts에 없으면 getMenuTextSync로 기본 한글 텍스트 가져오기 + if (!text) { + text = getMenuTextSync(key, userLang) || fallback || key; + } + + // 파라미터 치환 + if (params && text) { + Object.entries(params).forEach(([paramKey, paramValue]) => { + text = text!.replace(`{${paramKey}}`, String(paramValue)); + }); + } + + return text || key; + }; + + // 다국어 API 테스트 함수 (getUITextSync 사용) + const testMultiLangAPI = async () => { + // console.log("🧪 다국어 API 테스트 시작"); + try { + const text = getUITextSync("menu.management.admin"); + // console.log("🧪 다국어 API 테스트 결과:", text); + } catch (error) { + // console.error("❌ 다국어 API 테스트 실패:", error); + } + }; + + // 대문자 키를 소문자 키로 변환하는 함수 + const convertMenuData = (data: any[]): MenuItem[] => { + return data.map((item) => ({ + objid: item.OBJID || item.objid, + parent_obj_id: item.PARENT_OBJ_ID || item.parent_obj_id, + menu_name_kor: item.MENU_NAME_KOR || item.menu_name_kor, + menu_url: item.MENU_URL || item.menu_url, + menu_desc: item.MENU_DESC || item.menu_desc, + seq: item.SEQ || item.seq, + menu_type: item.MENU_TYPE || item.menu_type, + status: item.STATUS || item.status, + lev: item.LEV || item.lev, + lpad_menu_name_kor: item.LPAD_MENU_NAME_KOR || item.lpad_menu_name_kor, + status_title: item.STATUS_TITLE || item.status_title, + writer: item.WRITER || item.writer, + regdate: item.REGDATE || item.regdate, + company_code: item.COMPANY_CODE || item.company_code, + company_name: item.COMPANY_NAME || item.company_name, + })); + }; + + const handleAddTopLevelMenu = () => { + setFormData({ + menuId: "", + parentId: "0", // 최상위 메뉴는 parentId가 0 + menuType: getMenuTypeValue(), + level: 1, // 최상위 메뉴는 level 1 + parentCompanyCode: "", // 최상위 메뉴는 상위 회사 정보 없음 + }); + setFormModalOpen(true); + }; + + const handleAddMenu = (parentId: string, menuType: string, level: number) => { + // 상위 메뉴의 회사 정보 찾기 + const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus; + const parentMenu = currentMenus.find((menu) => menu.objid === parentId); + + setFormData({ + menuId: "", + parentId, + menuType, + level: level + 1, + parentCompanyCode: parentMenu?.company_code || "", + }); + setFormModalOpen(true); + }; + + const handleEditMenu = (menuId: string) => { + // console.log("🔧 메뉴 수정 시작 - menuId:", menuId); + + // 현재 메뉴 정보 찾기 + const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus; + const menuToEdit = currentMenus.find((menu) => (menu.objid || menu.OBJID) === menuId); + + if (menuToEdit) { + // console.log("수정할 메뉴 정보:", menuToEdit); + + setFormData({ + menuId: menuId, + parentId: menuToEdit.parent_obj_id || menuToEdit.PARENT_OBJ_ID || "", + menuType: selectedMenuType, // 현재 선택된 메뉴 타입 + level: 0, // 기본값 + parentCompanyCode: menuToEdit.company_code || menuToEdit.COMPANY_CODE || "", + }); + + // console.log("설정된 formData:", { + // menuId: menuId, + // parentId: menuToEdit.parent_obj_id || menuToEdit.PARENT_OBJ_ID || "", + // menuType: selectedMenuType, + // level: 0, + // parentCompanyCode: menuToEdit.company_code || menuToEdit.COMPANY_CODE || "", + // }); + } else { + // console.error("수정할 메뉴를 찾을 수 없음:", menuId); + } + + setFormModalOpen(true); + }; + + const handleMenuSelectionChange = (menuId: string, checked: boolean) => { + const newSelected = new Set(selectedMenus); + if (checked) { + newSelected.add(menuId); + } else { + newSelected.delete(menuId); + } + setSelectedMenus(newSelected); + }; + + const handleSelectAllMenus = (checked: boolean) => { + const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus; + if (checked) { + // 모든 메뉴 선택 (최상위 메뉴 포함) + setSelectedMenus(new Set(currentMenus.map((menu) => menu.objid || menu.OBJID || ""))); + } else { + setSelectedMenus(new Set()); + } + }; + + const handleDeleteSelectedMenus = async () => { + if (selectedMenus.size === 0) { + toast.error(getUITextSync("message.validation.select.menu.delete")); + return; + } + + if (!confirm(getUITextSync("modal.delete.batch.description", { count: selectedMenus.size }))) { + return; + } + + setDeleting(true); + try { + const menuIds = Array.from(selectedMenus); + // console.log("삭제할 메뉴 IDs:", menuIds); + + toast.info(getUITextSync("message.menu.delete.processing")); + + const response = await menuApi.deleteMenusBatch(menuIds); + // console.log("삭제 API 응답:", response); + // console.log("응답 구조:", { + // success: response.success, + // data: response.data, + // message: response.message, + // }); + + if (response.success && response.data) { + const { deletedCount, failedCount } = response.data; + // console.log("삭제 결과:", { deletedCount, failedCount }); + + // 선택된 메뉴 초기화 + setSelectedMenus(new Set()); + + // 메뉴 목록 즉시 새로고침 (로딩 상태 없이) + // console.log("메뉴 목록 새로고침 시작"); + await loadMenus(false); + // 전역 메뉴 상태도 업데이트 + await refreshMenus(); + // console.log("메뉴 목록 새로고침 완료"); + + // 삭제 결과 메시지 + if (failedCount === 0) { + toast.success(getUITextSync("message.menu.delete.batch.success", { count: deletedCount })); + } else { + toast.success( + getUITextSync("message.menu.delete.batch.partial", { + success: deletedCount, + failed: failedCount, + }), + ); + } + } else { + // console.error("삭제 실패:", response); + toast.error(response.message || "메뉴 삭제에 실패했습니다."); + } + } catch (error) { + // console.error("메뉴 삭제 중 오류:", error); + toast.error(getUITextSync("message.menu.delete.failed")); + } finally { + setDeleting(false); + } + }; + + const confirmDelete = async () => { + try { + const response = await menuApi.deleteMenu(selectedMenuId); + if (response.success) { + toast.success(response.message); + await loadMenus(false); + } else { + toast.error(response.message); + } + } catch (error) { + toast.error("메뉴 삭제에 실패했습니다."); + } finally { + setDeleteDialogOpen(false); + setSelectedMenuId(""); + } + }; + + const handleCopyMenu = (menuId: string, menuName: string) => { + setSelectedMenuId(menuId); + setSelectedMenuName(menuName); + setCopyDialogOpen(true); + }; + + const handleCopyComplete = async () => { + // 복사 완료 후 메뉴 목록 새로고침 + await loadMenus(false); + toast.success("메뉴 복사가 완료되었습니다"); + }; + + const handleToggleStatus = async (menuId: string) => { + try { + const response = await menuApi.toggleMenuStatus(menuId); + if (response.success) { + toast.success(response.message); + await loadMenus(false); // 메뉴 목록 새로고침 + // 전역 메뉴 상태도 업데이트 + await refreshMenus(); + } else { + toast.error(response.message); + } + } catch (error) { + // console.error("메뉴 상태 토글 오류:", error); + toast.error(getUITextSync("message.menu.status.toggle.failed")); + } + }; + + const handleFormSuccess = () => { + loadMenus(false); + // 전역 메뉴 상태도 업데이트 + refreshMenus(); + }; + + const getCurrentMenus = () => { + // 메뉴 관리 화면용: 모든 상태의 메뉴 표시 (localAdminMenus/localUserMenus 사용) + const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus; + + // 검색어 필터링 + let filteredMenus = currentMenus; + if (searchText.trim()) { + const searchLower = searchText.toLowerCase(); + filteredMenus = currentMenus.filter((menu) => { + const menuName = (menu.menu_name_kor || menu.MENU_NAME_KOR || "").toLowerCase(); + const menuUrl = (menu.menu_url || menu.MENU_URL || "").toLowerCase(); + return menuName.includes(searchLower) || menuUrl.includes(searchLower); + }); + } + + // 회사 필터링 + if (selectedCompany !== "all") { + filteredMenus = filteredMenus.filter((menu) => { + const menuCompanyCode = menu.company_code || menu.COMPANY_CODE || ""; + return menuCompanyCode === selectedCompany; + }); + } + + return filteredMenus; + }; + + // 메뉴 타입 변경 시 선택된 메뉴 초기화 + const handleMenuTypeChange = (type: MenuType) => { + setSelectedMenuType(type); + setSelectedMenus(new Set()); // 선택된 메뉴 초기화 + setExpandedMenus(new Set()); // 메뉴 타입 변경 시 확장 상태 초기화 + + // 선택한 메뉴 타입에 해당하는 메뉴만 로드 + if (type === "admin" && localAdminMenus.length === 0) { + loadMenusForType("admin", false); + } else if (type === "user" && localUserMenus.length === 0) { + loadMenusForType("user", false); + } + }; + + const handleToggleExpand = (menuId: string) => { + const newExpandedMenus = new Set(expandedMenus); + if (newExpandedMenus.has(menuId)) { + newExpandedMenus.delete(menuId); + } else { + newExpandedMenus.add(menuId); + } + setExpandedMenus(newExpandedMenus); + }; + + const getMenuTypeString = () => { + return selectedMenuType === "admin" ? getUITextSync("menu.type.admin") : getUITextSync("menu.type.user"); + }; + + const getMenuTypeValue = () => { + return selectedMenuType === "admin" ? "0" : "1"; + }; + + // uiTextsCount를 useMemo로 계산하여 상태 변경 시에만 재계산 + const uiTextsCount = useMemo(() => Object.keys(uiTexts).length, [uiTexts]); + const adminMenusCount = useMemo(() => localAdminMenus?.length || 0, [localAdminMenus]); + const userMenusCount = useMemo(() => localUserMenus?.length || 0, [localUserMenus]); + + // 디버깅을 위한 간단한 상태 표시 + // console.log("🔍 MenuManagement 렌더링 상태:", { + // loading, + // uiTextsLoading, + // uiTextsCount, + // adminMenusCount, + // userMenusCount, + // selectedMenuType, + // userLang, + // }); + + if (loading) { + return ( +
+ +
+ ); + } + return (
@@ -14,7 +885,263 @@ export default function MenuPage() {
{/* 메인 컨텐츠 */} - + +
+ {/* 좌측 사이드바 - 메뉴 타입 선택 (20%) */} +
+
+

{getUITextSync("menu.type.title")}

+ + {/* 메뉴 타입 선택 카드들 */} +
+
handleMenuTypeChange("admin")} + > +
+
+

{getUITextSync("menu.management.admin")}

+

+ {getUITextSync("menu.management.admin.description")} +

+
+ + {localAdminMenus.length} + +
+
+ +
handleMenuTypeChange("user")} + > +
+
+

{getUITextSync("menu.management.user")}

+

+ {getUITextSync("menu.management.user.description")} +

+
+ + {localUserMenus.length} + +
+
+
+
+
+ + {/* 우측 메인 영역 - 메뉴 목록 (80%) */} +
+
+ {/* 상단 헤더: 제목 + 검색 + 버튼 */} +
+ {/* 왼쪽: 제목 */} +

+ {getMenuTypeString()} {getUITextSync("menu.list.title")} +

+ + {/* 오른쪽: 검색 + 버튼 */} +
+ {/* 회사 선택 */} +
+
+ + + {isCompanyDropdownOpen && ( +
+
+ setCompanySearchText(e.target.value)} + className="h-8 text-sm" + onClick={(e) => e.stopPropagation()} + /> +
+ +
+
{ + setSelectedCompany("all"); + setIsCompanyDropdownOpen(false); + setCompanySearchText(""); + }} + > + {getUITextSync("filter.company.all")} +
+
{ + setSelectedCompany("*"); + setIsCompanyDropdownOpen(false); + setCompanySearchText(""); + }} + > + {getUITextSync("filter.company.common")} +
+ + {companies + .filter((company) => company.code && company.code.trim() !== "") + .filter( + (company) => + company.name.toLowerCase().includes(companySearchText.toLowerCase()) || + company.code.toLowerCase().includes(companySearchText.toLowerCase()), + ) + .map((company, index) => ( +
{ + setSelectedCompany(company.code); + setIsCompanyDropdownOpen(false); + setCompanySearchText(""); + }} + > + {company.code === "*" ? getUITextSync("filter.company.common") : company.name} +
+ ))} +
+
+ )} +
+
+ + {/* 검색 입력 */} +
+ setSearchText(e.target.value)} + className="h-10 text-sm" + /> +
+ + {/* 초기화 버튼 */} + + + {/* 최상위 메뉴 추가 */} + + + {/* 선택 삭제 */} + {selectedMenus.size > 0 && ( + + )} +
+
+ + {/* 테이블 영역 */} +
+ +
+
+
+
+
+ + setFormModalOpen(false)} + onSuccess={handleFormSuccess} + menuId={formData.menuId} + parentId={formData.parentId} + menuType={formData.menuType} + level={formData.level} + parentCompanyCode={formData.parentCompanyCode} + uiTexts={uiTexts} + /> + + + + + 메뉴 삭제 + + 해당 메뉴를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. + + + + 취소 + 삭제 + + + + +
{/* Scroll to Top 버튼 (모바일/태블릿 전용) */} diff --git a/frontend/app/(main)/admin/monitoring/page.tsx b/frontend/app/(main)/admin/monitoring/page.tsx index ac70e9a4..7be9d405 100644 --- a/frontend/app/(main)/admin/monitoring/page.tsx +++ b/frontend/app/(main)/admin/monitoring/page.tsx @@ -1,9 +1,124 @@ "use client"; -import React from "react"; -import MonitoringDashboard from "@/components/admin/MonitoringDashboard"; +import React, { useState, useEffect } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Progress } from "@/components/ui/progress"; +import { RefreshCw, Play, Pause, AlertCircle, CheckCircle, Clock } from "lucide-react"; +import { toast } from "sonner"; +import { BatchAPI, BatchMonitoring } from "@/lib/api/batch"; export default function MonitoringPage() { + const [monitoring, setMonitoring] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [autoRefresh, setAutoRefresh] = useState(false); + + useEffect(() => { + loadMonitoringData(); + + let interval: NodeJS.Timeout; + if (autoRefresh) { + interval = setInterval(loadMonitoringData, 30000); // 30초마다 자동 새로고침 + } + + return () => { + if (interval) clearInterval(interval); + }; + }, [autoRefresh]); + + const loadMonitoringData = async () => { + setIsLoading(true); + try { + const data = await BatchAPI.getBatchMonitoring(); + setMonitoring(data); + } catch (error) { + console.error("모니터링 데이터 조회 오류:", error); + toast.error("모니터링 데이터를 불러오는데 실패했습니다."); + } finally { + setIsLoading(false); + } + }; + + const handleRefresh = () => { + loadMonitoringData(); + }; + + const toggleAutoRefresh = () => { + setAutoRefresh(!autoRefresh); + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case 'completed': + return ; + case 'failed': + return ; + case 'running': + return ; + case 'pending': + return ; + default: + return ; + } + }; + + const getStatusBadge = (status: string) => { + const variants = { + completed: "bg-green-100 text-green-800", + failed: "bg-destructive/20 text-red-800", + running: "bg-primary/20 text-blue-800", + pending: "bg-yellow-100 text-yellow-800", + cancelled: "bg-gray-100 text-gray-800", + }; + + const labels = { + completed: "완료", + failed: "실패", + running: "실행 중", + pending: "대기 중", + cancelled: "취소됨", + }; + + return ( + + {labels[status as keyof typeof labels] || status} + + ); + }; + + const formatDuration = (ms: number) => { + if (ms < 1000) return `${ms}ms`; + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; + return `${(ms / 60000).toFixed(1)}m`; + }; + + const getSuccessRate = () => { + if (!monitoring) return 0; + const total = monitoring.successful_jobs_today + monitoring.failed_jobs_today; + if (total === 0) return 100; + return Math.round((monitoring.successful_jobs_today / total) * 100); + }; + + if (!monitoring) { + return ( +
+
+ +

모니터링 데이터를 불러오는 중...

+
+
+ ); + } + return (
@@ -16,7 +131,170 @@ export default function MonitoringPage() {
{/* 모니터링 대시보드 */} - +
+ {/* 헤더 */} +
+

배치 모니터링

+
+ + +
+
+ + {/* 통계 카드 */} +
+ + + 총 작업 수 +
📋
+
+ +
{monitoring.total_jobs}
+

+ 활성: {monitoring.active_jobs}개 +

+
+
+ + + + 실행 중 +
🔄
+
+ +
{monitoring.running_jobs}
+

+ 현재 실행 중인 작업 +

+
+
+ + + + 오늘 성공 +
+
+ +
{monitoring.successful_jobs_today}
+

+ 성공률: {getSuccessRate()}% +

+
+
+ + + + 오늘 실패 +
+
+ +
{monitoring.failed_jobs_today}
+

+ 주의가 필요한 작업 +

+
+
+
+ + {/* 성공률 진행바 */} + + + 오늘 실행 성공률 + + +
+
+ 성공: {monitoring.successful_jobs_today}건 + 실패: {monitoring.failed_jobs_today}건 +
+ +
+ {getSuccessRate()}% 성공률 +
+
+
+
+ + {/* 최근 실행 이력 */} + + + 최근 실행 이력 + + + {monitoring.recent_executions.length === 0 ? ( +
+ 최근 실행 이력이 없습니다. +
+ ) : ( + + + + 상태 + 작업 ID + 시작 시간 + 완료 시간 + 실행 시간 + 오류 메시지 + + + + {monitoring.recent_executions.map((execution) => ( + + +
+ {getStatusIcon(execution.execution_status)} + {getStatusBadge(execution.execution_status)} +
+
+ #{execution.job_id} + + {execution.started_at + ? new Date(execution.started_at).toLocaleString() + : "-"} + + + {execution.completed_at + ? new Date(execution.completed_at).toLocaleString() + : "-"} + + + {execution.execution_time_ms + ? formatDuration(execution.execution_time_ms) + : "-"} + + + {execution.error_message ? ( + + {execution.error_message} + + ) : ( + "-" + )} + +
+ ))} +
+
+ )} +
+
+
); diff --git a/frontend/app/(main)/admin/page.tsx b/frontend/app/(main)/admin/page.tsx index 3060e0fc..8658d7c6 100644 --- a/frontend/app/(main)/admin/page.tsx +++ b/frontend/app/(main)/admin/page.tsx @@ -1,4 +1,4 @@ -import { Users, Shield, Settings, BarChart3, Palette, Layout, Database, Package } from "lucide-react"; +import { Users, Shield, Settings, BarChart3, Palette, Layout, Database, Package, Building2 } from "lucide-react"; import Link from "next/link"; import { GlobalFileViewer } from "@/components/GlobalFileViewer"; @@ -9,6 +9,7 @@ export default function AdminPage() { return (
+ {/* 주요 관리 기능 */}
diff --git a/frontend/app/(main)/admin/screenMng/dashboardList/DashboardListClient.tsx b/frontend/app/(main)/admin/screenMng/dashboardList/DashboardListClient.tsx deleted file mode 100644 index c50aaa51..00000000 --- a/frontend/app/(main)/admin/screenMng/dashboardList/DashboardListClient.tsx +++ /dev/null @@ -1,449 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { dashboardApi } from "@/lib/api/dashboard"; -import { Dashboard } from "@/lib/api/dashboard"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { useToast } from "@/hooks/use-toast"; -import { Pagination, PaginationInfo } from "@/components/common/Pagination"; -import { DeleteConfirmModal } from "@/components/common/DeleteConfirmModal"; -import { Plus, Search, Edit, Trash2, Copy, MoreVertical, AlertCircle, RefreshCw } from "lucide-react"; - -/** - * 대시보드 목록 클라이언트 컴포넌트 - * - CSR 방식으로 초기 데이터 로드 - * - 대시보드 목록 조회 - * - 대시보드 생성/수정/삭제/복사 - */ -export default function DashboardListClient() { - const router = useRouter(); - const { toast } = useToast(); - - // 상태 관리 - const [dashboards, setDashboards] = useState([]); - const [loading, setLoading] = useState(true); // CSR이므로 초기 로딩 true - const [error, setError] = useState(null); - const [searchTerm, setSearchTerm] = useState(""); - - // 페이지네이션 상태 - const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - const [totalCount, setTotalCount] = useState(0); - - // 모달 상태 - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [deleteTarget, setDeleteTarget] = useState<{ id: string; title: string } | null>(null); - - // 대시보드 목록 로드 - const loadDashboards = async () => { - try { - setLoading(true); - setError(null); - const result = await dashboardApi.getMyDashboards({ - search: searchTerm, - page: currentPage, - limit: pageSize, - }); - setDashboards(result.dashboards); - setTotalCount(result.pagination.total); - } catch (err) { - console.error("Failed to load dashboards:", err); - setError( - err instanceof Error - ? err.message - : "대시보드 목록을 불러오는데 실패했습니다. 네트워크 연결을 확인하거나 잠시 후 다시 시도해주세요.", - ); - } finally { - setLoading(false); - } - }; - - // 검색어/페이지 변경 시 fetch (초기 로딩 포함) - useEffect(() => { - loadDashboards(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchTerm, currentPage, pageSize]); - - // 페이지네이션 정보 계산 - const paginationInfo: PaginationInfo = { - currentPage, - totalPages: Math.ceil(totalCount / pageSize) || 1, - totalItems: totalCount, - itemsPerPage: pageSize, - startItem: totalCount === 0 ? 0 : (currentPage - 1) * pageSize + 1, - endItem: Math.min(currentPage * pageSize, totalCount), - }; - - // 페이지 변경 핸들러 - const handlePageChange = (page: number) => { - setCurrentPage(page); - }; - - // 페이지 크기 변경 핸들러 - const handlePageSizeChange = (size: number) => { - setPageSize(size); - setCurrentPage(1); // 페이지 크기 변경 시 첫 페이지로 - }; - - // 대시보드 삭제 확인 모달 열기 - const handleDeleteClick = (id: string, title: string) => { - setDeleteTarget({ id, title }); - setDeleteDialogOpen(true); - }; - - // 대시보드 삭제 실행 - const handleDeleteConfirm = async () => { - if (!deleteTarget) return; - - try { - await dashboardApi.deleteDashboard(deleteTarget.id); - setDeleteDialogOpen(false); - setDeleteTarget(null); - toast({ - title: "성공", - description: "대시보드가 삭제되었습니다.", - }); - loadDashboards(); - } catch (err) { - console.error("Failed to delete dashboard:", err); - setDeleteDialogOpen(false); - toast({ - title: "오류", - description: "대시보드 삭제에 실패했습니다.", - variant: "destructive", - }); - } - }; - - // 대시보드 복사 - const handleCopy = async (dashboard: Dashboard) => { - try { - const fullDashboard = await dashboardApi.getDashboard(dashboard.id); - - await dashboardApi.createDashboard({ - title: `${fullDashboard.title} (복사본)`, - description: fullDashboard.description, - elements: fullDashboard.elements || [], - isPublic: false, - tags: fullDashboard.tags, - category: fullDashboard.category, - settings: fullDashboard.settings as { resolution?: string; backgroundColor?: string }, - }); - toast({ - title: "성공", - description: "대시보드가 복사되었습니다.", - }); - loadDashboards(); - } catch (err) { - console.error("Failed to copy dashboard:", err); - toast({ - title: "오류", - description: "대시보드 복사에 실패했습니다.", - variant: "destructive", - }); - } - }; - - // 포맷팅 헬퍼 - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleDateString("ko-KR", { - year: "numeric", - month: "2-digit", - day: "2-digit", - }); - }; - - return ( - <> - {/* 검색 및 액션 */} -
-
-
- - setSearchTerm(e.target.value)} - className="h-10 pl-10 text-sm" - /> -
-
- 총 {totalCount.toLocaleString()} 건 -
-
- -
- - {/* 대시보드 목록 */} - {loading ? ( - <> - {/* 데스크톱 테이블 스켈레톤 */} -
- - - - 제목 - 설명 - 생성자 - 생성일 - 수정일 - 작업 - - - - {Array.from({ length: 10 }).map((_, index) => ( - - -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
- ))} -
-
-
- - {/* 모바일/태블릿 카드 스켈레톤 */} -
- {Array.from({ length: 6 }).map((_, index) => ( -
-
-
-
-
-
-
-
- {Array.from({ length: 3 }).map((_, i) => ( -
-
-
-
- ))} -
-
- ))} -
- - ) : error ? ( -
-
-
- -
-
-

데이터를 불러올 수 없습니다

-

{error}

-
- -
-
- ) : dashboards.length === 0 ? ( -
-
-

대시보드가 없습니다

-
-
- ) : ( - <> - {/* 데스크톱 테이블 뷰 (lg 이상) */} -
- - - - 제목 - 설명 - 생성자 - 생성일 - 수정일 - 작업 - - - - {dashboards.map((dashboard) => ( - - - - - - {dashboard.description || "-"} - - - {dashboard.createdByName || dashboard.createdBy || "-"} - - - {formatDate(dashboard.createdAt)} - - - {formatDate(dashboard.updatedAt)} - - - - - - - - router.push(`/admin/screenMng/dashboardList/edit/${dashboard.id}`)} - className="gap-2 text-sm" - > - - 편집 - - handleCopy(dashboard)} className="gap-2 text-sm"> - - 복사 - - handleDeleteClick(dashboard.id, dashboard.title)} - className="text-destructive focus:text-destructive gap-2 text-sm" - > - - 삭제 - - - - - - ))} - -
-
- - {/* 모바일/태블릿 카드 뷰 (lg 미만) */} -
- {dashboards.map((dashboard) => ( -
- {/* 헤더 */} -
-
- -

{dashboard.id}

-
-
- - {/* 정보 */} -
-
- 설명 - {dashboard.description || "-"} -
-
- 생성자 - {dashboard.createdByName || dashboard.createdBy || "-"} -
-
- 생성일 - {formatDate(dashboard.createdAt)} -
-
- 수정일 - {formatDate(dashboard.updatedAt)} -
-
- - {/* 액션 */} -
- - - -
-
- ))} -
- - )} - - {/* 페이지네이션 */} - {!loading && dashboards.length > 0 && ( - - )} - - {/* 삭제 확인 모달 */} - - "{deleteTarget?.title}" 대시보드를 삭제하시겠습니까? -
이 작업은 되돌릴 수 없습니다. - - } - onConfirm={handleDeleteConfirm} - /> - - ); -} diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/app/(main)/admin/screenMng/dashboardList/[id]/page.tsx similarity index 95% rename from frontend/components/admin/dashboard/DashboardDesigner.tsx rename to frontend/app/(main)/admin/screenMng/dashboardList/[id]/page.tsx index b945cb3d..63900c78 100644 --- a/frontend/components/admin/dashboard/DashboardDesigner.tsx +++ b/frontend/app/(main)/admin/screenMng/dashboardList/[id]/page.tsx @@ -1,17 +1,18 @@ "use client"; import React, { useState, useRef, useCallback } from "react"; +import { use } from "react"; import { useRouter } from "next/navigation"; -import { DashboardCanvas } from "./DashboardCanvas"; -import { DashboardTopMenu } from "./DashboardTopMenu"; -import { WidgetConfigSidebar } from "./WidgetConfigSidebar"; -import { DashboardSaveModal } from "./DashboardSaveModal"; -import { DashboardElement, ElementType, ElementSubtype } from "./types"; -import { GRID_CONFIG, snapToGrid, snapSizeToGrid, calculateCellSize, calculateBoxSize } from "./gridUtils"; -import { Resolution, RESOLUTIONS, detectScreenResolution } from "./ResolutionSelector"; +import { DashboardCanvas } from "@/components/admin/dashboard/DashboardCanvas"; +import { DashboardTopMenu } from "@/components/admin/dashboard/DashboardTopMenu"; +import { WidgetConfigSidebar } from "@/components/admin/dashboard/WidgetConfigSidebar"; +import { DashboardSaveModal } from "@/components/admin/dashboard/DashboardSaveModal"; +import { DashboardElement, ElementType, ElementSubtype } from "@/components/admin/dashboard/types"; +import { GRID_CONFIG, snapToGrid, snapSizeToGrid, calculateCellSize, calculateBoxSize } from "@/components/admin/dashboard/gridUtils"; +import { Resolution, RESOLUTIONS, detectScreenResolution } from "@/components/admin/dashboard/ResolutionSelector"; import { DashboardProvider } from "@/contexts/DashboardContext"; import { useMenu } from "@/contexts/MenuContext"; -import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts"; +import { useKeyboardShortcuts } from "@/components/admin/dashboard/hooks/useKeyboardShortcuts"; import { Dialog, DialogContent, @@ -32,18 +33,24 @@ import { import { Button } from "@/components/ui/button"; import { CheckCircle2 } from "lucide-react"; -interface DashboardDesignerProps { - dashboardId?: string; -} - /** - * 대시보드 설계 도구 메인 컴포넌트 + * 대시보드 생성/편집 페이지 + * URL: /admin/screenMng/dashboardList/[id] + * - id가 "new"면 새 대시보드 생성 + * - id가 숫자면 기존 대시보드 편집 + * + * 기능: * - 드래그 앤 드롭으로 차트/위젯 배치 * - 그리드 기반 레이아웃 (12 컬럼) * - 요소 이동, 크기 조절, 삭제 기능 * - 레이아웃 저장/불러오기 기능 */ -export default function DashboardDesigner({ dashboardId: initialDashboardId }: DashboardDesignerProps = {}) { +export default function DashboardDesignerPage({ params }: { params: Promise<{ id: string }> }) { + const { id: paramId } = use(params); + + // "new"면 생성 모드, 아니면 편집 모드 + const initialDashboardId = paramId === "new" ? undefined : paramId; + const router = useRouter(); const { refreshMenus } = useMenu(); const [elements, setElements] = useState([]); diff --git a/frontend/app/(main)/admin/screenMng/dashboardList/edit/[id]/page.tsx b/frontend/app/(main)/admin/screenMng/dashboardList/edit/[id]/page.tsx deleted file mode 100644 index 92220b6c..00000000 --- a/frontend/app/(main)/admin/screenMng/dashboardList/edit/[id]/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -"use client"; - -import React from "react"; -import { use } from "react"; -import DashboardDesigner from "@/components/admin/dashboard/DashboardDesigner"; - -interface PageProps { - params: Promise<{ id: string }>; -} - -/** - * 대시보드 편집 페이지 - * - 기존 대시보드 편집 - */ -export default function DashboardEditPage({ params }: PageProps) { - const { id } = use(params); - - return ( -
- -
- ); -} diff --git a/frontend/app/(main)/admin/screenMng/dashboardList/new/page.tsx b/frontend/app/(main)/admin/screenMng/dashboardList/new/page.tsx deleted file mode 100644 index 56d28f46..00000000 --- a/frontend/app/(main)/admin/screenMng/dashboardList/new/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import DashboardDesigner from "@/components/admin/dashboard/DashboardDesigner"; - -/** - * 새 대시보드 생성 페이지 - */ -export default function DashboardNewPage() { - return ( -
- -
- ); -} diff --git a/frontend/app/(main)/admin/screenMng/dashboardList/page.tsx b/frontend/app/(main)/admin/screenMng/dashboardList/page.tsx index 62587c54..c346dc54 100644 --- a/frontend/app/(main)/admin/screenMng/dashboardList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/dashboardList/page.tsx @@ -1,11 +1,167 @@ -import DashboardListClient from "@/app/(main)/admin/screenMng/dashboardList/DashboardListClient"; +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { dashboardApi } from "@/lib/api/dashboard"; +import { Dashboard } from "@/lib/api/dashboard"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useToast } from "@/hooks/use-toast"; +import { Pagination, PaginationInfo } from "@/components/common/Pagination"; +import { DeleteConfirmModal } from "@/components/common/DeleteConfirmModal"; +import { Plus, Search, Edit, Trash2, Copy, MoreVertical, AlertCircle, RefreshCw } from "lucide-react"; /** * 대시보드 관리 페이지 - * - 클라이언트 컴포넌트를 렌더링하는 래퍼 - * - 초기 로딩부터 CSR로 처리 + * - CSR 방식으로 초기 데이터 로드 + * - 대시보드 목록 조회 + * - 대시보드 생성/수정/삭제/복사 */ export default function DashboardListPage() { + const router = useRouter(); + const { toast } = useToast(); + + // 상태 관리 + const [dashboards, setDashboards] = useState([]); + const [loading, setLoading] = useState(true); // CSR이므로 초기 로딩 true + const [error, setError] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + + // 페이지네이션 상태 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [totalCount, setTotalCount] = useState(0); + + // 모달 상태 + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deleteTarget, setDeleteTarget] = useState<{ id: string; title: string } | null>(null); + + // 대시보드 목록 로드 + const loadDashboards = async () => { + try { + setLoading(true); + setError(null); + const result = await dashboardApi.getMyDashboards({ + search: searchTerm, + page: currentPage, + limit: pageSize, + }); + setDashboards(result.dashboards); + setTotalCount(result.pagination.total); + } catch (err) { + console.error("Failed to load dashboards:", err); + setError( + err instanceof Error + ? err.message + : "대시보드 목록을 불러오는데 실패했습니다. 네트워크 연결을 확인하거나 잠시 후 다시 시도해주세요.", + ); + } finally { + setLoading(false); + } + }; + + // 검색어/페이지 변경 시 fetch (초기 로딩 포함) + useEffect(() => { + loadDashboards(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchTerm, currentPage, pageSize]); + + // 페이지네이션 정보 계산 + const paginationInfo: PaginationInfo = { + currentPage, + totalPages: Math.ceil(totalCount / pageSize) || 1, + totalItems: totalCount, + itemsPerPage: pageSize, + startItem: totalCount === 0 ? 0 : (currentPage - 1) * pageSize + 1, + endItem: Math.min(currentPage * pageSize, totalCount), + }; + + // 페이지 변경 핸들러 + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + // 페이지 크기 변경 핸들러 + const handlePageSizeChange = (size: number) => { + setPageSize(size); + setCurrentPage(1); // 페이지 크기 변경 시 첫 페이지로 + }; + + // 대시보드 삭제 확인 모달 열기 + const handleDeleteClick = (id: string, title: string) => { + setDeleteTarget({ id, title }); + setDeleteDialogOpen(true); + }; + + // 대시보드 삭제 실행 + const handleDeleteConfirm = async () => { + if (!deleteTarget) return; + + try { + await dashboardApi.deleteDashboard(deleteTarget.id); + setDeleteDialogOpen(false); + setDeleteTarget(null); + toast({ + title: "성공", + description: "대시보드가 삭제되었습니다.", + }); + loadDashboards(); + } catch (err) { + console.error("Failed to delete dashboard:", err); + setDeleteDialogOpen(false); + toast({ + title: "오류", + description: "대시보드 삭제에 실패했습니다.", + variant: "destructive", + }); + } + }; + + // 대시보드 복사 + const handleCopy = async (dashboard: Dashboard) => { + try { + const fullDashboard = await dashboardApi.getDashboard(dashboard.id); + + await dashboardApi.createDashboard({ + title: `${fullDashboard.title} (복사본)`, + description: fullDashboard.description, + elements: fullDashboard.elements || [], + isPublic: false, + tags: fullDashboard.tags, + category: fullDashboard.category, + settings: fullDashboard.settings as { resolution?: string; backgroundColor?: string }, + }); + toast({ + title: "성공", + description: "대시보드가 복사되었습니다.", + }); + loadDashboards(); + } catch (err) { + console.error("Failed to copy dashboard:", err); + toast({ + title: "오류", + description: "대시보드 복사에 실패했습니다.", + variant: "destructive", + }); + } + }; + + // 포맷팅 헬퍼 + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("ko-KR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); + }; + return (
@@ -15,8 +171,287 @@ export default function DashboardListPage() {

대시보드를 생성하고 관리할 수 있습니다

- {/* 클라이언트 컴포넌트 */} - + {/* 검색 및 액션 */} +
+
+
+ + setSearchTerm(e.target.value)} + className="h-10 pl-10 text-sm" + /> +
+
+ 총 {totalCount.toLocaleString()} 건 +
+
+ +
+ + {/* 대시보드 목록 */} + {loading ? ( + <> + {/* 데스크톱 테이블 스켈레톤 */} +
+ + + + 제목 + 설명 + 생성자 + 생성일 + 수정일 + 작업 + + + + {Array.from({ length: 10 }).map((_, index) => ( + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ ))} +
+
+
+ + {/* 모바일/태블릿 카드 스켈레톤 */} +
+ {Array.from({ length: 6 }).map((_, index) => ( +
+
+
+
+
+
+
+
+ {Array.from({ length: 3 }).map((_, i) => ( +
+
+
+
+ ))} +
+
+ ))} +
+ + ) : error ? ( +
+
+
+ +
+
+

데이터를 불러올 수 없습니다

+

{error}

+
+ +
+
+ ) : dashboards.length === 0 ? ( +
+
+

대시보드가 없습니다

+
+
+ ) : ( + <> + {/* 데스크톱 테이블 뷰 (lg 이상) */} +
+ + + + 제목 + 설명 + 생성자 + 생성일 + 수정일 + 작업 + + + + {dashboards.map((dashboard) => ( + + + + + + {dashboard.description || "-"} + + + {dashboard.createdByName || dashboard.createdBy || "-"} + + + {formatDate(dashboard.createdAt)} + + + {formatDate(dashboard.updatedAt)} + + + + + + + + router.push(`/admin/screenMng/dashboardList/${dashboard.id}`)} + className="gap-2 text-sm" + > + + 편집 + + handleCopy(dashboard)} className="gap-2 text-sm"> + + 복사 + + handleDeleteClick(dashboard.id, dashboard.title)} + className="text-destructive focus:text-destructive gap-2 text-sm" + > + + 삭제 + + + + + + ))} + +
+
+ + {/* 모바일/태블릿 카드 뷰 (lg 미만) */} +
+ {dashboards.map((dashboard) => ( +
+ {/* 헤더 */} +
+
+ +

{dashboard.id}

+
+
+ + {/* 정보 */} +
+
+ 설명 + {dashboard.description || "-"} +
+
+ 생성자 + {dashboard.createdByName || dashboard.createdBy || "-"} +
+
+ 생성일 + {formatDate(dashboard.createdAt)} +
+
+ 수정일 + {formatDate(dashboard.updatedAt)} +
+
+ + {/* 액션 */} +
+ + + +
+
+ ))} +
+ + )} + + {/* 페이지네이션 */} + {!loading && dashboards.length > 0 && ( + + )} + + {/* 삭제 확인 모달 */} + + "{deleteTarget?.title}" 대시보드를 삭제하시겠습니까? +
이 작업은 되돌릴 수 없습니다. + + } + onConfirm={handleDeleteConfirm} + />
); diff --git a/frontend/app/(main)/admin/systemMng/i18nList/page.tsx b/frontend/app/(main)/admin/systemMng/i18nList/page.tsx index 48655de7..3acce6fb 100644 --- a/frontend/app/(main)/admin/systemMng/i18nList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/i18nList/page.tsx @@ -1,12 +1,823 @@ "use client"; -import MultiLang from "@/components/admin/MultiLang"; +import { useState, useEffect } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; + +import { DataTable } from "@/components/common/DataTable"; +import { LoadingSpinner } from "@/components/common/LoadingSpinner"; +import { useAuth } from "@/hooks/useAuth"; +import LangKeyModal from "@/components/admin/LangKeyModal"; +import LanguageModal from "@/components/admin/LanguageModal"; +import { apiClient } from "@/lib/api/client"; + +interface Language { + langCode: string; + langName: string; + langNative: string; + isActive: string; +} + +interface LangKey { + keyId: number; + companyCode: string; + menuName: string; + langKey: string; + description: string; + isActive: string; +} + +interface LangText { + textId: number; + keyId: number; + langCode: string; + langText: string; + isActive: string; +} export default function I18nPage() { + const { user } = useAuth(); + const [loading, setLoading] = useState(true); + const [languages, setLanguages] = useState([]); + const [langKeys, setLangKeys] = useState([]); + const [selectedKey, setSelectedKey] = useState(null); + const [langTexts, setLangTexts] = useState([]); + const [editingTexts, setEditingTexts] = useState([]); + const [selectedCompany, setSelectedCompany] = useState("all"); + const [searchText, setSearchText] = useState(""); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingKey, setEditingKey] = useState(null); + const [selectedKeys, setSelectedKeys] = useState>(new Set()); + + // 언어 관리 관련 상태 + const [isLanguageModalOpen, setIsLanguageModalOpen] = useState(false); + const [editingLanguage, setEditingLanguage] = useState(null); + const [selectedLanguages, setSelectedLanguages] = useState>(new Set()); + const [activeTab, setActiveTab] = useState<"keys" | "languages">("keys"); + + const [companies, setCompanies] = useState>([]); + + // 회사 목록 조회 + const fetchCompanies = async () => { + try { + const response = await apiClient.get("/admin/companies"); + const data = response.data; + if (data.success) { + const companyList = data.data.map((company: any) => ({ + code: company.company_code, + name: company.company_name, + })); + setCompanies(companyList); + } + } catch (error) { + // console.error("회사 목록 조회 실패:", error); + } + }; + + // 언어 목록 조회 + const fetchLanguages = async () => { + try { + const response = await apiClient.get("/multilang/languages"); + const data = response.data; + if (data.success) { + setLanguages(data.data); + } + } catch (error) { + // console.error("언어 목록 조회 실패:", error); + } + }; + + // 다국어 키 목록 조회 + const fetchLangKeys = async () => { + try { + const response = await apiClient.get("/multilang/keys"); + const data = response.data; + if (data.success) { + setLangKeys(data.data); + } + } catch (error) { + // console.error("다국어 키 목록 조회 실패:", error); + } + }; + + // 필터링된 데이터 계산 + const getFilteredLangKeys = () => { + let filteredKeys = langKeys; + + // 회사 필터링 + if (selectedCompany && selectedCompany !== "all") { + filteredKeys = filteredKeys.filter((key) => key.companyCode === selectedCompany); + } + + // 텍스트 검색 필터링 + if (searchText.trim()) { + const searchLower = searchText.toLowerCase(); + filteredKeys = filteredKeys.filter((key) => { + const langKey = (key.langKey || "").toLowerCase(); + const description = (key.description || "").toLowerCase(); + const menuName = (key.menuName || "").toLowerCase(); + const companyName = companies.find((c) => c.code === key.companyCode)?.name?.toLowerCase() || ""; + + return ( + langKey.includes(searchLower) || + description.includes(searchLower) || + menuName.includes(searchLower) || + companyName.includes(searchLower) + ); + }); + } + + return filteredKeys; + }; + + // 선택된 키의 다국어 텍스트 조회 + const fetchLangTexts = async (keyId: number) => { + try { + const response = await apiClient.get(`/multilang/keys/${keyId}/texts`); + const data = response.data; + if (data.success) { + setLangTexts(data.data); + const editingData = data.data.map((text: LangText) => ({ ...text })); + setEditingTexts(editingData); + } + } catch (error) { + // console.error("다국어 텍스트 조회 실패:", error); + } + }; + + // 언어 키 선택 처리 + const handleKeySelect = (key: LangKey) => { + setSelectedKey(key); + fetchLangTexts(key.keyId); + }; + + // 텍스트 변경 처리 + const handleTextChange = (langCode: string, value: string) => { + const newEditingTexts = [...editingTexts]; + const existingIndex = newEditingTexts.findIndex((t) => t.langCode === langCode); + + if (existingIndex >= 0) { + newEditingTexts[existingIndex].langText = value; + } else { + newEditingTexts.push({ + textId: 0, + keyId: selectedKey!.keyId, + langCode: langCode, + langText: value, + isActive: "Y", + }); + } + + setEditingTexts(newEditingTexts); + }; + + // 텍스트 저장 + const handleSave = async () => { + if (!selectedKey) return; + + try { + const requestData = { + texts: editingTexts.map((text) => ({ + langCode: text.langCode, + langText: text.langText, + isActive: text.isActive || "Y", + createdBy: user?.userId || "system", + updatedBy: user?.userId || "system", + })), + }; + + const response = await apiClient.post(`/multilang/keys/${selectedKey.keyId}/texts`, requestData); + const data = response.data; + if (data.success) { + alert("저장되었습니다."); + fetchLangTexts(selectedKey.keyId); + } + } catch (error) { + alert("저장에 실패했습니다."); + } + }; + + // 언어 키 추가/수정 모달 열기 + const handleAddKey = () => { + setEditingKey(null); + setIsModalOpen(true); + }; + + // 언어 추가/수정 모달 열기 + const handleAddLanguage = () => { + setEditingLanguage(null); + setIsLanguageModalOpen(true); + }; + + // 언어 수정 + const handleEditLanguage = (language: Language) => { + setEditingLanguage(language); + setIsLanguageModalOpen(true); + }; + + // 언어 저장 (추가/수정) + const handleSaveLanguage = async (languageData: any) => { + try { + const requestData = { + ...languageData, + createdBy: user?.userId || "admin", + updatedBy: user?.userId || "admin", + }; + + let response; + if (editingLanguage) { + response = await apiClient.put(`/multilang/languages/${editingLanguage.langCode}`, requestData); + } else { + response = await apiClient.post("/multilang/languages", requestData); + } + + const result = response.data; + + if (result.success) { + alert(editingLanguage ? "언어가 수정되었습니다." : "언어가 추가되었습니다."); + setIsLanguageModalOpen(false); + fetchLanguages(); + } else { + alert(`오류: ${result.message}`); + } + } catch (error) { + alert("언어 저장 중 오류가 발생했습니다."); + } + }; + + // 언어 삭제 + const handleDeleteLanguages = async () => { + if (selectedLanguages.size === 0) { + alert("삭제할 언어를 선택해주세요."); + return; + } + + if ( + !confirm( + `선택된 ${selectedLanguages.size}개의 언어를 영구적으로 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.`, + ) + ) { + return; + } + + try { + const deletePromises = Array.from(selectedLanguages).map((langCode) => + apiClient.delete(`/multilang/languages/${langCode}`), + ); + + const responses = await Promise.all(deletePromises); + const failedDeletes = responses.filter((response) => !response.data.success); + + if (failedDeletes.length === 0) { + alert("선택된 언어가 삭제되었습니다."); + setSelectedLanguages(new Set()); + fetchLanguages(); + } else { + alert(`${failedDeletes.length}개의 언어 삭제에 실패했습니다.`); + } + } catch (error) { + alert("언어 삭제 중 오류가 발생했습니다."); + } + }; + + // 언어 선택 체크박스 처리 + const handleLanguageCheckboxChange = (langCode: string, checked: boolean) => { + const newSelected = new Set(selectedLanguages); + if (checked) { + newSelected.add(langCode); + } else { + newSelected.delete(langCode); + } + setSelectedLanguages(newSelected); + }; + + // 언어 전체 선택/해제 + const handleSelectAllLanguages = (checked: boolean) => { + if (checked) { + setSelectedLanguages(new Set(languages.map((lang) => lang.langCode))); + } else { + setSelectedLanguages(new Set()); + } + }; + + // 언어 키 수정 모달 열기 + const handleEditKey = (key: LangKey) => { + setEditingKey(key); + setIsModalOpen(true); + }; + + // 언어 키 저장 (추가/수정) + const handleSaveKey = async (keyData: any) => { + try { + const requestData = { + ...keyData, + createdBy: user?.userId || "admin", + updatedBy: user?.userId || "admin", + }; + + let response; + if (editingKey) { + response = await apiClient.put(`/multilang/keys/${editingKey.keyId}`, requestData); + } else { + response = await apiClient.post("/multilang/keys", requestData); + } + + const data = response.data; + + if (data.success) { + alert(editingKey ? "언어 키가 수정되었습니다." : "언어 키가 추가되었습니다."); + fetchLangKeys(); + setIsModalOpen(false); + } else { + if (data.message && data.message.includes("이미 존재하는 언어키")) { + alert(data.message); + } else { + alert(data.message || "언어 키 저장에 실패했습니다."); + } + } + } catch (error) { + alert("언어 키 저장에 실패했습니다."); + } + }; + + // 체크박스 선택/해제 + const handleCheckboxChange = (keyId: number, checked: boolean) => { + const newSelectedKeys = new Set(selectedKeys); + if (checked) { + newSelectedKeys.add(keyId); + } else { + newSelectedKeys.delete(keyId); + } + setSelectedKeys(newSelectedKeys); + }; + + // 키 상태 토글 + const handleToggleStatus = async (keyId: number) => { + try { + const response = await apiClient.put(`/multilang/keys/${keyId}/toggle`); + const data = response.data; + if (data.success) { + alert(`키가 ${data.data}되었습니다.`); + fetchLangKeys(); + } else { + alert("상태 변경 중 오류가 발생했습니다."); + } + } catch (error) { + alert("키 상태 변경 중 오류가 발생했습니다."); + } + }; + + // 언어 상태 토글 + const handleToggleLanguageStatus = async (langCode: string) => { + try { + const response = await apiClient.put(`/multilang/languages/${langCode}/toggle`); + const data = response.data; + if (data.success) { + alert(`언어가 ${data.data}되었습니다.`); + fetchLanguages(); + } else { + alert("언어 상태 변경 중 오류가 발생했습니다."); + } + } catch (error) { + alert("언어 상태 변경 중 오류가 발생했습니다."); + } + }; + + // 전체 선택/해제 + const handleSelectAll = (checked: boolean) => { + if (checked) { + const allKeyIds = getFilteredLangKeys().map((key) => key.keyId); + setSelectedKeys(new Set(allKeyIds)); + } else { + setSelectedKeys(new Set()); + } + }; + + // 선택된 키들 일괄 삭제 + const handleDeleteSelectedKeys = async () => { + if (selectedKeys.size === 0) { + alert("삭제할 키를 선택해주세요."); + return; + } + + if ( + !confirm( + `선택된 ${selectedKeys.size}개의 언어 키를 영구적으로 삭제하시겠습니까?\n\n⚠️ 이 작업은 되돌릴 수 없습니다.`, + ) + ) { + return; + } + + try { + const deletePromises = Array.from(selectedKeys).map((keyId) => apiClient.delete(`/multilang/keys/${keyId}`)); + + const responses = await Promise.all(deletePromises); + const allSuccess = responses.every((response) => response.data.success); + + if (allSuccess) { + alert(`${selectedKeys.size}개의 언어 키가 영구적으로 삭제되었습니다.`); + setSelectedKeys(new Set()); + fetchLangKeys(); + + if (selectedKey && selectedKeys.has(selectedKey.keyId)) { + handleCancel(); + } + } else { + alert("일부 키 삭제에 실패했습니다."); + } + } catch (error) { + alert("선택된 키 삭제에 실패했습니다."); + } + }; + + // 개별 키 삭제 + const handleDeleteKey = async (keyId: number) => { + if (!confirm("정말로 이 언어 키를 영구적으로 삭제하시겠습니까?\n\n⚠️ 이 작업은 되돌릴 수 없습니다.")) { + return; + } + + try { + const response = await apiClient.delete(`/multilang/keys/${keyId}`); + const data = response.data; + if (data.success) { + alert("언어 키가 영구적으로 삭제되었습니다."); + fetchLangKeys(); + if (selectedKey && selectedKey.keyId === keyId) { + handleCancel(); + } + } + } catch (error) { + alert("언어 키 삭제에 실패했습니다."); + } + }; + + // 취소 처리 + const handleCancel = () => { + setSelectedKey(null); + setLangTexts([]); + setEditingTexts([]); + }; + + useEffect(() => { + const initializeData = async () => { + setLoading(true); + await Promise.all([fetchCompanies(), fetchLanguages(), fetchLangKeys()]); + setLoading(false); + }; + initializeData(); + }, []); + + const columns = [ + { + id: "select", + header: () => { + const filteredKeys = getFilteredLangKeys(); + return ( + 0} + onChange={(e) => handleSelectAll(e.target.checked)} + className="h-4 w-4" + /> + ); + }, + cell: ({ row }: any) => ( + handleCheckboxChange(row.original.keyId, e.target.checked)} + onClick={(e) => e.stopPropagation()} + className="h-4 w-4" + disabled={row.original.isActive === "N"} + /> + ), + }, + { + accessorKey: "companyCode", + header: "회사", + cell: ({ row }: any) => { + const companyName = + row.original.companyCode === "*" + ? "공통" + : companies.find((c) => c.code === row.original.companyCode)?.name || row.original.companyCode; + + return {companyName}; + }, + }, + { + accessorKey: "menuName", + header: "메뉴명", + cell: ({ row }: any) => ( + {row.original.menuName} + ), + }, + { + accessorKey: "langKey", + header: "언어 키", + cell: ({ row }: any) => ( +
handleEditKey(row.original)} + > + {row.original.langKey} +
+ ), + }, + { + accessorKey: "description", + header: "설명", + cell: ({ row }: any) => ( + {row.original.description} + ), + }, + { + accessorKey: "isActive", + header: "상태", + cell: ({ row }: any) => ( + + ), + }, + ]; + + // 언어 테이블 컬럼 정의 + const languageColumns = [ + { + id: "select", + header: () => ( + 0} + onChange={(e) => handleSelectAllLanguages(e.target.checked)} + className="h-4 w-4" + /> + ), + cell: ({ row }: any) => ( + handleLanguageCheckboxChange(row.original.langCode, e.target.checked)} + onClick={(e) => e.stopPropagation()} + className="h-4 w-4" + disabled={row.original.isActive === "N"} + /> + ), + }, + { + accessorKey: "langCode", + header: "언어 코드", + cell: ({ row }: any) => ( +
handleEditLanguage(row.original)} + > + {row.original.langCode} +
+ ), + }, + { + accessorKey: "langName", + header: "언어명 (영문)", + cell: ({ row }: any) => ( + {row.original.langName} + ), + }, + { + accessorKey: "langNative", + header: "언어명 (원어)", + cell: ({ row }: any) => ( + {row.original.langNative} + ), + }, + { + accessorKey: "isActive", + header: "상태", + cell: ({ row }: any) => ( + + ), + }, + ]; + + if (loading) { + return ; + } + return (
- +
+ {/* 탭 네비게이션 */} +
+ + +
+ + {/* 메인 콘텐츠 영역 */} +
+ {/* 언어 관리 탭 */} + {activeTab === "languages" && ( + + + 언어 관리 + + +
+
총 {languages.length}개의 언어가 등록되어 있습니다.
+
+ {selectedLanguages.size > 0 && ( + + )} + +
+
+ +
+
+ )} + + {/* 다국어 키 관리 탭 */} + {activeTab === "keys" && ( +
+ {/* 좌측: 언어 키 목록 (7/10) */} + + +
+ 언어 키 목록 +
+ + +
+
+
+ + {/* 검색 필터 영역 */} +
+
+ + +
+ +
+ + setSearchText(e.target.value)} + /> +
+ +
+
검색 결과: {getFilteredLangKeys().length}건
+
+
+ + {/* 테이블 영역 */} +
+
전체: {getFilteredLangKeys().length}건
+ +
+
+
+ + {/* 우측: 선택된 키의 다국어 관리 (3/10) */} + + + + {selectedKey ? ( + <> + 선택된 키:{" "} + + {selectedKey.companyCode}.{selectedKey.menuName}.{selectedKey.langKey} + + + ) : ( + "다국어 편집" + )} + + + + {selectedKey ? ( +
+ {/* 스크롤 가능한 텍스트 영역 */} +
+ {languages + .filter((lang) => lang.isActive === "Y") + .map((lang) => { + const text = editingTexts.find((t) => t.langCode === lang.langCode); + return ( +
+ + {lang.langName} + + handleTextChange(lang.langCode, e.target.value)} + className="flex-1" + /> +
+ ); + })} +
+ {/* 저장 버튼 - 고정 위치 */} +
+ + +
+
+ ) : ( +
+
+
언어 키를 선택하세요
+
좌측 목록에서 편집할 언어 키를 클릭하세요
+
+
+ )} +
+
+
+ )} +
+ + {/* 언어 키 추가/수정 모달 */} + setIsModalOpen(false)} + onSave={handleSaveKey} + keyData={editingKey} + companies={companies} + /> + + {/* 언어 추가/수정 모달 */} + setIsLanguageModalOpen(false)} + onSave={handleSaveLanguage} + languageData={editingLanguage} + /> +
); diff --git a/frontend/app/(main)/admin/userMng/companyList/[companyCode]/departments/page.tsx b/frontend/app/(main)/admin/userMng/companyList/[companyCode]/departments/page.tsx index 7854e6ee..a9cd747c 100644 --- a/frontend/app/(main)/admin/userMng/companyList/[companyCode]/departments/page.tsx +++ b/frontend/app/(main)/admin/userMng/companyList/[companyCode]/departments/page.tsx @@ -1,12 +1,115 @@ "use client"; -import { useParams } from "next/navigation"; -import { DepartmentManagement } from "@/components/admin/department/DepartmentManagement"; +import { useState, useEffect } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ArrowLeft } from "lucide-react"; +import { DepartmentStructure } from "@/components/admin/department/DepartmentStructure"; +import { DepartmentMembers } from "@/components/admin/department/DepartmentMembers"; +import type { Department } from "@/types/department"; +import { getCompanyList } from "@/lib/api/company"; +/** + * 부서 관리 메인 페이지 + * 좌측: 부서 구조, 우측: 부서 인원 + */ export default function DepartmentManagementPage() { const params = useParams(); + const router = useRouter(); const companyCode = params.companyCode as string; + const [selectedDepartment, setSelectedDepartment] = useState(null); + const [activeTab, setActiveTab] = useState("structure"); + const [companyName, setCompanyName] = useState(""); + const [refreshTrigger, setRefreshTrigger] = useState(0); - return ; + // 부서원 변경 시 부서 구조 새로고침 + const handleMemberChange = () => { + setRefreshTrigger((prev) => prev + 1); + }; + + // 회사 정보 로드 + useEffect(() => { + const loadCompanyInfo = async () => { + const response = await getCompanyList(); + if (response.success && response.data) { + const company = response.data.find((c) => c.company_code === companyCode); + if (company) { + setCompanyName(company.company_name); + } + } + }; + loadCompanyInfo(); + }, [companyCode]); + + const handleBackToList = () => { + router.push("/admin/userMng/companyList"); + }; + + return ( +
+ {/* 상단 헤더: 회사 정보 + 뒤로가기 */} +
+
+ +
+
+

{companyName || companyCode}

+

부서 관리

+
+
+
+ {/* 탭 네비게이션 (모바일용) */} +
+ + + 부서 구조 + 부서 인원 + + + + + + + + + + +
+ + {/* 좌우 레이아웃 (데스크톱) */} +
+ {/* 좌측: 부서 구조 (20%) */} +
+ +
+ + {/* 우측: 부서 인원 (80%) */} +
+ +
+
+
+ ); } - diff --git a/frontend/app/(main)/admin/userMng/companyList/page.tsx b/frontend/app/(main)/admin/userMng/companyList/page.tsx index c24afc7a..a36cd9c3 100644 --- a/frontend/app/(main)/admin/userMng/companyList/page.tsx +++ b/frontend/app/(main)/admin/userMng/companyList/page.tsx @@ -1,10 +1,56 @@ -import { CompanyManagement } from "@/components/admin/CompanyManagement"; +"use client"; + +import { useCompanyManagement } from "@/hooks/useCompanyManagement"; +import { CompanyToolbar } from "@/components/admin/CompanyToolbar"; +import { CompanyTable } from "@/components/admin/CompanyTable"; +import { CompanyFormModal } from "@/components/admin/CompanyFormModal"; +import { CompanyDeleteDialog } from "@/components/admin/CompanyDeleteDialog"; +import { DiskUsageSummary } from "@/components/admin/DiskUsageSummary"; import { ScrollToTop } from "@/components/common/ScrollToTop"; /** * 회사 관리 페이지 + * 모든 회사 관리 기능을 통합하여 제공 */ export default function CompanyPage() { + const { + // 데이터 + companies, + searchFilter, + isLoading, + error, + + // 디스크 사용량 관련 + diskUsageInfo, + isDiskUsageLoading, + loadDiskUsage, + + // 모달 상태 + modalState, + deleteState, + + // 검색 기능 + updateSearchFilter, + clearSearchFilter, + + // 모달 제어 + openCreateModal, + openEditModal, + closeModal, + updateFormData, + + // 삭제 다이얼로그 제어 + openDeleteDialog, + closeDeleteDialog, + + // CRUD 작업 + saveCompany, + deleteCompany, + + // 에러 처리 + clearError, + } = useCompanyManagement(); + return (
@@ -14,8 +60,42 @@ export default function CompanyPage() {

시스템에서 사용하는 회사 정보를 관리합니다

- {/* 메인 컨텐츠 */} - + {/* 디스크 사용량 요약 */} + + + {/* 툴바 - 검색, 필터, 등록 버튼 */} + + + {/* 회사 목록 테이블 */} + + + {/* 회사 등록/수정 모달 */} + + + {/* 회사 삭제 확인 다이얼로그 */} +
{/* Scroll to Top 버튼 */} diff --git a/frontend/app/(main)/admin/userMng/rolesList/[id]/page.tsx b/frontend/app/(main)/admin/userMng/rolesList/[id]/page.tsx index a1579bf2..30552af4 100644 --- a/frontend/app/(main)/admin/userMng/rolesList/[id]/page.tsx +++ b/frontend/app/(main)/admin/userMng/rolesList/[id]/page.tsx @@ -1,12 +1,20 @@ "use client"; +import React, { useState, useCallback, useEffect } from "react"; import { use } from "react"; -import { RoleDetailManagement } from "@/components/admin/RoleDetailManagement"; +import { Button } from "@/components/ui/button"; +import { ArrowLeft, Users, Menu as MenuIcon, Save, AlertCircle } from "lucide-react"; +import { roleAPI, RoleGroup } from "@/lib/api/role"; +import { useAuth } from "@/hooks/useAuth"; +import { useRouter } from "next/navigation"; +import { DualListBox } from "@/components/common/DualListBox"; +import { MenuPermissionsTable } from "@/components/admin/MenuPermissionsTable"; +import { useMenu } from "@/contexts/MenuContext"; import { ScrollToTop } from "@/components/common/ScrollToTop"; /** * 권한 그룹 상세 페이지 - * URL: /admin/roles/[id] + * URL: /admin/userMng/rolesList/[id] * * 기능: * - 권한 그룹 멤버 관리 (Dual List Box) @@ -14,13 +22,324 @@ import { ScrollToTop } from "@/components/common/ScrollToTop"; */ export default function RoleDetailPage({ params }: { params: Promise<{ id: string }> }) { // Next.js 15: params는 Promise이므로 React.use()로 unwrap - const { id } = use(params); + const { id: roleId } = use(params); + const { user: currentUser } = useAuth(); + const router = useRouter(); + const { refreshMenus } = useMenu(); + + const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN"; + + // 상태 관리 + const [roleGroup, setRoleGroup] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + // 탭 상태 + const [activeTab, setActiveTab] = useState<"members" | "permissions">("members"); + + // 멤버 관리 상태 + const [availableUsers, setAvailableUsers] = useState>([]); + const [selectedUsers, setSelectedUsers] = useState>([]); + const [isSavingMembers, setIsSavingMembers] = useState(false); + + // 메뉴 권한 상태 + const [menuPermissions, setMenuPermissions] = useState([]); + const [isSavingPermissions, setIsSavingPermissions] = useState(false); + + // 데이터 로드 + const loadRoleGroup = useCallback(async () => { + setIsLoading(true); + setError(null); + + try { + const response = await roleAPI.getById(parseInt(roleId, 10)); + + if (response.success && response.data) { + setRoleGroup(response.data); + } else { + setError(response.message || "권한 그룹 정보를 불러오는데 실패했습니다."); + } + } catch (err) { + console.error("권한 그룹 정보 로드 오류:", err); + setError("권한 그룹 정보를 불러오는 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }, [roleId]); + + // 멤버 목록 로드 + const loadMembers = useCallback(async () => { + if (!roleGroup) return; + + try { + // 1. 권한 그룹 멤버 조회 + const membersResponse = await roleAPI.getMembers(roleGroup.objid); + if (membersResponse.success && membersResponse.data) { + setSelectedUsers( + membersResponse.data.map((member: any) => ({ + id: member.userId, + label: member.userName || member.userId, + description: member.deptName, + })), + ); + } + + // 2. 전체 사용자 목록 조회 (같은 회사) + const userAPI = await import("@/lib/api/user"); + + console.log("🔍 사용자 목록 조회 요청:", { + companyCode: roleGroup.companyCode, + size: 1000, + }); + + const usersResponse = await userAPI.userAPI.getList({ + companyCode: roleGroup.companyCode, + size: 1000, // 대량 조회 + }); + + console.log("✅ 사용자 목록 응답:", { + success: usersResponse.success, + count: usersResponse.data?.length, + total: usersResponse.total, + }); + + if (usersResponse.success && usersResponse.data) { + setAvailableUsers( + usersResponse.data.map((user: any) => ({ + id: user.userId, + label: user.userName || user.userId, + description: user.deptName, + })), + ); + console.log("📋 설정된 전체 사용자 수:", usersResponse.data.length); + } + } catch (err) { + console.error("멤버 목록 로드 오류:", err); + } + }, [roleGroup]); + + // 메뉴 권한 로드 + const loadMenuPermissions = useCallback(async () => { + if (!roleGroup) return; + + console.log("🔍 [loadMenuPermissions] 메뉴 권한 로드 시작", { + roleGroupId: roleGroup.objid, + roleGroupName: roleGroup.authName, + companyCode: roleGroup.companyCode, + }); + + try { + const response = await roleAPI.getMenuPermissions(roleGroup.objid); + + console.log("✅ [loadMenuPermissions] API 응답", { + success: response.success, + dataCount: response.data?.length, + data: response.data, + }); + + if (response.success && response.data) { + setMenuPermissions(response.data); + console.log("✅ [loadMenuPermissions] 상태 업데이트 완료", { + count: response.data.length, + }); + } else { + console.warn("⚠️ [loadMenuPermissions] 응답 실패", { + message: response.message, + }); + } + } catch (err) { + console.error("❌ [loadMenuPermissions] 메뉴 권한 로드 오류:", err); + } + }, [roleGroup]); + + useEffect(() => { + loadRoleGroup(); + }, [loadRoleGroup]); + + useEffect(() => { + if (roleGroup && activeTab === "members") { + loadMembers(); + } else if (roleGroup && activeTab === "permissions") { + loadMenuPermissions(); + } + }, [roleGroup, activeTab, loadMembers, loadMenuPermissions]); + + // 멤버 저장 핸들러 + const handleSaveMembers = useCallback(async () => { + if (!roleGroup) return; + + setIsSavingMembers(true); + try { + // 현재 선택된 사용자 ID 목록 + const selectedUserIds = selectedUsers.map((user) => user.id); + + // 멤버 업데이트 API 호출 + const response = await roleAPI.updateMembers(roleGroup.objid, selectedUserIds); + + if (response.success) { + alert("멤버가 성공적으로 저장되었습니다."); + loadMembers(); // 새로고침 + + // 사이드바 메뉴 새로고침 (현재 사용자가 영향받을 수 있음) + await refreshMenus(); + } else { + alert(response.message || "멤버 저장에 실패했습니다."); + } + } catch (err) { + console.error("멤버 저장 오류:", err); + alert("멤버 저장 중 오류가 발생했습니다."); + } finally { + setIsSavingMembers(false); + } + }, [roleGroup, selectedUsers, loadMembers, refreshMenus]); + + // 메뉴 권한 저장 핸들러 + const handleSavePermissions = useCallback(async () => { + if (!roleGroup) return; + + setIsSavingPermissions(true); + try { + const response = await roleAPI.setMenuPermissions(roleGroup.objid, menuPermissions); + + if (response.success) { + alert("메뉴 권한이 성공적으로 저장되었습니다."); + loadMenuPermissions(); // 새로고침 + + // 사이드바 메뉴 새로고침 (권한 변경 즉시 반영) + await refreshMenus(); + } else { + alert(response.message || "메뉴 권한 저장에 실패했습니다."); + } + } catch (err) { + console.error("메뉴 권한 저장 오류:", err); + alert("메뉴 권한 저장 중 오류가 발생했습니다."); + } finally { + setIsSavingPermissions(false); + } + }, [roleGroup, menuPermissions, loadMenuPermissions, refreshMenus]); + + if (isLoading) { + return ( +
+
+
+

권한 그룹 정보를 불러오는 중...

+
+
+ ); + } + + if (error || !roleGroup) { + return ( +
+ +

오류 발생

+

{error || "권한 그룹을 찾을 수 없습니다."}

+ +
+ ); + } return (
- {/* 메인 컨텐츠 */} - + {/* 페이지 헤더 */} +
+
+ +
+

{roleGroup.authName}

+

+ {roleGroup.authCode} • {roleGroup.companyCode} +

+
+ + {roleGroup.status === "active" ? "활성" : "비활성"} + +
+
+ + {/* 탭 네비게이션 */} +
+ + +
+ + {/* 탭 컨텐츠 */} +
+ {activeTab === "members" && ( + <> +
+
+

멤버 관리

+

이 권한 그룹에 속한 사용자를 관리합니다

+
+ +
+ + + + )} + + {activeTab === "permissions" && ( + <> +
+
+

메뉴 권한 설정

+

이 권한 그룹에서 접근 가능한 메뉴와 권한을 설정합니다

+
+ +
+ + + + )} +
{/* Scroll to Top 버튼 (모바일/태블릿 전용) */} diff --git a/frontend/app/(main)/admin/userMng/rolesList/page.tsx b/frontend/app/(main)/admin/userMng/rolesList/page.tsx index 2b973ad5..eeac1dec 100644 --- a/frontend/app/(main)/admin/userMng/rolesList/page.tsx +++ b/frontend/app/(main)/admin/userMng/rolesList/page.tsx @@ -1,6 +1,16 @@ "use client"; -import { RoleManagement } from "@/components/admin/RoleManagement"; +import React, { useState, useCallback, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Plus, Edit, Trash2, Users, Menu, Filter, X } from "lucide-react"; +import { roleAPI, RoleGroup } from "@/lib/api/role"; +import { useAuth } from "@/hooks/useAuth"; +import { AlertCircle } from "lucide-react"; +import { RoleFormModal } from "@/components/admin/RoleFormModal"; +import { RoleDeleteModal } from "@/components/admin/RoleDeleteModal"; +import { useRouter } from "next/navigation"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { companyAPI } from "@/lib/api/company"; import { ScrollToTop } from "@/components/common/ScrollToTop"; /** @@ -14,21 +24,336 @@ import { ScrollToTop } from "@/components/common/ScrollToTop"; * - 권한 그룹 생성/수정/삭제 * - 멤버 관리 (Dual List Box) * - 메뉴 권한 설정 (CRUD 권한) + * - 상세 페이지로 이동 (멤버 관리 + 메뉴 권한 설정) */ export default function RolesPage() { + const { user: currentUser } = useAuth(); + const router = useRouter(); + + // 회사 관리자 또는 최고 관리자 여부 + const isAdmin = + (currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN") || + currentUser?.userType === "COMPANY_ADMIN"; + const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN"; + + // 상태 관리 + const [roleGroups, setRoleGroups] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + // 회사 필터 (최고 관리자 전용) + const [companies, setCompanies] = useState>([]); + const [selectedCompany, setSelectedCompany] = useState("all"); + + // 모달 상태 + const [formModal, setFormModal] = useState({ + isOpen: false, + editingRole: null as RoleGroup | null, + }); + + const [deleteModal, setDeleteModal] = useState({ + isOpen: false, + role: null as RoleGroup | null, + }); + + // 회사 목록 로드 (최고 관리자만) + const loadCompanies = useCallback(async () => { + if (!isSuperAdmin) return; + + try { + const companies = await companyAPI.getList(); + setCompanies(companies); + } catch (error) { + console.error("회사 목록 로드 오류:", error); + } + }, [isSuperAdmin]); + + // 데이터 로드 + const loadRoleGroups = useCallback(async () => { + setIsLoading(true); + setError(null); + + try { + // 최고 관리자: selectedCompany에 따라 필터링 (all이면 전체 조회) + // 회사 관리자: 자기 회사만 조회 + const companyFilter = + isSuperAdmin && selectedCompany !== "all" + ? selectedCompany + : isSuperAdmin + ? undefined + : currentUser?.companyCode; + + console.log("권한 그룹 목록 조회:", { isSuperAdmin, selectedCompany, companyFilter }); + + const response = await roleAPI.getList({ + companyCode: companyFilter, + }); + + if (response.success && response.data) { + setRoleGroups(response.data); + console.log("권한 그룹 조회 성공:", response.data.length, "개"); + } else { + setError(response.message || "권한 그룹 목록을 불러오는데 실패했습니다."); + } + } catch (err) { + console.error("권한 그룹 목록 로드 오류:", err); + setError("권한 그룹 목록을 불러오는 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }, [isSuperAdmin, selectedCompany, currentUser?.companyCode]); + + useEffect(() => { + if (isAdmin) { + if (isSuperAdmin) { + loadCompanies(); // 최고 관리자는 회사 목록 먼저 로드 + } + loadRoleGroups(); + } else { + setIsLoading(false); + } + }, [isAdmin, isSuperAdmin, loadRoleGroups, loadCompanies]); + + // 권한 그룹 생성 핸들러 + const handleCreateRole = useCallback(() => { + setFormModal({ isOpen: true, editingRole: null }); + }, []); + + // 권한 그룹 수정 핸들러 + const handleEditRole = useCallback((role: RoleGroup) => { + setFormModal({ isOpen: true, editingRole: role }); + }, []); + + // 권한 그룹 삭제 핸들러 + const handleDeleteRole = useCallback((role: RoleGroup) => { + setDeleteModal({ isOpen: true, role }); + }, []); + + // 폼 모달 닫기 + const handleFormModalClose = useCallback(() => { + setFormModal({ isOpen: false, editingRole: null }); + }, []); + + // 삭제 모달 닫기 + const handleDeleteModalClose = useCallback(() => { + setDeleteModal({ isOpen: false, role: null }); + }, []); + + // 모달 성공 후 새로고침 + const handleModalSuccess = useCallback(() => { + loadRoleGroups(); + }, [loadRoleGroups]); + + // 상세 페이지로 이동 + const handleViewDetail = useCallback( + (role: RoleGroup) => { + router.push(`/admin/userMng/rolesList/${role.objid}`); + }, + [router], + ); + + // 관리자가 아니면 접근 제한 + if (!isAdmin) { + return ( +
+
+
+

권한 그룹 관리

+

회사 내 권한 그룹을 생성하고 멤버를 관리합니다 (회사 관리자 이상)

+
+ +
+ +

접근 권한 없음

+

+ 권한 그룹 관리는 회사 관리자 이상만 접근할 수 있습니다. +

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

권한 그룹 관리

-

- 회사 내 권한 그룹을 생성하고 멤버를 관리합니다 (회사 관리자 이상) -

+

회사 내 권한 그룹을 생성하고 멤버를 관리합니다 (회사 관리자 이상)

- {/* 메인 컨텐츠 */} - + {/* 에러 메시지 */} + {error && ( +
+
+

오류가 발생했습니다

+ +
+

{error}

+
+ )} + + {/* 액션 버튼 영역 */} +
+
+

권한 그룹 목록 ({roleGroups.length})

+ + {/* 최고 관리자 전용: 회사 필터 */} + {isSuperAdmin && ( +
+ + + {selectedCompany !== "all" && ( + + )} +
+ )} +
+ + +
+ + {/* 권한 그룹 목록 */} + {isLoading ? ( +
+
+
+

권한 그룹 목록을 불러오는 중...

+
+
+ ) : roleGroups.length === 0 ? ( +
+
+

등록된 권한 그룹이 없습니다.

+

권한 그룹을 생성하여 멤버를 관리해보세요.

+
+
+ ) : ( +
+ {roleGroups.map((role) => ( +
+ {/* 헤더 (클릭 시 상세 페이지) */} +
handleViewDetail(role)} + > +
+
+

{role.authName}

+

{role.authCode}

+
+ + {role.status === "active" ? "활성" : "비활성"} + +
+ + {/* 정보 */} +
+ {/* 최고 관리자는 회사명 표시 */} + {isSuperAdmin && ( +
+ 회사 + + {companies.find((c) => c.company_code === role.companyCode)?.company_name || role.companyCode} + +
+ )} +
+ + + 멤버 수 + + {role.memberCount || 0}명 +
+
+ + + 메뉴 권한 + + {role.menuCount || 0}개 +
+
+
+ + {/* 액션 버튼 */} +
+ + +
+
+ ))} +
+ )} + + {/* 모달들 */} + + +
{/* Scroll to Top 버튼 (모바일/태블릿 전용) */} diff --git a/frontend/app/(main)/admin/userMng/userAuthList/page.tsx b/frontend/app/(main)/admin/userMng/userAuthList/page.tsx index 322bba64..4ad69183 100644 --- a/frontend/app/(main)/admin/userMng/userAuthList/page.tsx +++ b/frontend/app/(main)/admin/userMng/userAuthList/page.tsx @@ -1,6 +1,12 @@ "use client"; -import { UserAuthManagement } from "@/components/admin/UserAuthManagement"; +import React, { useState, useCallback, useEffect } from "react"; +import { UserAuthTable } from "@/components/admin/UserAuthTable"; +import { UserAuthEditModal } from "@/components/admin/UserAuthEditModal"; +import { userAPI } from "@/lib/api/user"; +import { useAuth } from "@/hooks/useAuth"; +import { AlertCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; import { ScrollToTop } from "@/components/common/ScrollToTop"; /** @@ -11,6 +17,119 @@ import { ScrollToTop } from "@/components/common/ScrollToTop"; * 사용자별 권한 레벨(SUPER_ADMIN, COMPANY_ADMIN, USER 등) 관리 */ export default function UserAuthPage() { + const { user: currentUser } = useAuth(); + + // 최고 관리자 여부 + const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN"; + + // 상태 관리 + const [users, setUsers] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [paginationInfo, setPaginationInfo] = useState({ + currentPage: 1, + pageSize: 20, + totalItems: 0, + totalPages: 0, + }); + + // 권한 변경 모달 + const [authEditModal, setAuthEditModal] = useState({ + isOpen: false, + user: null as any | null, + }); + + // 데이터 로드 + const loadUsers = useCallback( + async (page: number = 1) => { + setIsLoading(true); + setError(null); + + try { + const response = await userAPI.getList({ + page, + size: paginationInfo.pageSize, + }); + + if (response.success && response.data) { + setUsers(response.data); + setPaginationInfo({ + currentPage: response.currentPage || page, + pageSize: response.pageSize || paginationInfo.pageSize, + totalItems: response.total || 0, + totalPages: Math.ceil((response.total || 0) / (response.pageSize || paginationInfo.pageSize)), + }); + } else { + setError(response.message || "사용자 목록을 불러오는데 실패했습니다."); + } + } catch (err) { + console.error("사용자 목록 로드 오류:", err); + setError("사용자 목록을 불러오는 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }, + [paginationInfo.pageSize], + ); + + useEffect(() => { + loadUsers(1); + }, []); + + // 권한 변경 핸들러 + const handleEditAuth = (user: any) => { + setAuthEditModal({ + isOpen: true, + user, + }); + }; + + // 권한 변경 모달 닫기 + const handleAuthEditClose = () => { + setAuthEditModal({ + isOpen: false, + user: null, + }); + }; + + // 권한 변경 성공 + const handleAuthEditSuccess = () => { + loadUsers(paginationInfo.currentPage); + handleAuthEditClose(); + }; + + // 페이지 변경 + const handlePageChange = (page: number) => { + loadUsers(page); + }; + + // 최고 관리자가 아닌 경우 + if (!isSuperAdmin) { + return ( +
+
+
+

사용자 권한 관리

+

사용자별 권한 레벨을 관리합니다. (최고 관리자 전용)

+
+ +
+ +

접근 권한 없음

+

+ 권한 관리는 최고 관리자만 접근할 수 있습니다. +

+ +
+
+ + +
+ ); + } + return (
@@ -20,8 +139,39 @@ export default function UserAuthPage() {

사용자별 권한 레벨을 관리합니다. (최고 관리자 전용)

- {/* 메인 컨텐츠 */} - + {/* 에러 메시지 */} + {error && ( +
+
+

오류가 발생했습니다

+ +
+

{error}

+
+ )} + + {/* 사용자 권한 테이블 */} + + + {/* 권한 변경 모달 */} +
{/* Scroll to Top 버튼 (모바일/태블릿 전용) */} diff --git a/frontend/app/(main)/admin/userMng/userMngList/page.tsx b/frontend/app/(main)/admin/userMng/userMngList/page.tsx index 428e8986..828390d9 100644 --- a/frontend/app/(main)/admin/userMng/userMngList/page.tsx +++ b/frontend/app/(main)/admin/userMng/userMngList/page.tsx @@ -1,6 +1,12 @@ "use client"; -import { UserManagement } from "@/components/admin/UserManagement"; +import { useState } from "react"; +import { useUserManagement } from "@/hooks/useUserManagement"; +import { UserToolbar } from "@/components/admin/UserToolbar"; +import { UserTable } from "@/components/admin/UserTable"; +import { Pagination } from "@/components/common/Pagination"; +import { UserPasswordResetModal } from "@/components/admin/UserPasswordResetModal"; +import { UserFormModal } from "@/components/admin/UserFormModal"; import { ScrollToTop } from "@/components/common/ScrollToTop"; /** @@ -8,8 +14,100 @@ import { ScrollToTop } from "@/components/common/ScrollToTop"; * URL: /admin/userMng * * shadcn/ui 스타일 가이드 적용 + * - 원본 Spring + JSP 코드 패턴 기반 REST API 연동 + * - 실제 데이터베이스와 연동되어 작동 */ export default function UserMngPage() { + const { + // 데이터 + users, + searchFilter, + isLoading, + isSearching, + error, + paginationInfo, + + // 검색 기능 + updateSearchFilter, + + // 페이지네이션 + handlePageChange, + handlePageSizeChange, + + // 액션 핸들러 + handleStatusToggle, + + // 유틸리티 + clearError, + refreshData, + } = useUserManagement(); + + // 비밀번호 초기화 모달 상태 + const [passwordResetModal, setPasswordResetModal] = useState({ + isOpen: false, + userId: null as string | null, + userName: null as string | null, + }); + + // 사용자 등록/수정 모달 상태 + const [userFormModal, setUserFormModal] = useState({ + isOpen: false, + editingUser: null as any | null, + }); + + // 사용자 등록 핸들러 + const handleCreateUser = () => { + setUserFormModal({ + isOpen: true, + editingUser: null, + }); + }; + + // 사용자 수정 핸들러 + const handleEditUser = (user: any) => { + setUserFormModal({ + isOpen: true, + editingUser: user, + }); + }; + + // 사용자 등록/수정 모달 닫기 + const handleUserFormClose = () => { + setUserFormModal({ + isOpen: false, + editingUser: null, + }); + }; + + // 사용자 등록/수정 성공 핸들러 + const handleUserFormSuccess = () => { + refreshData(); + handleUserFormClose(); + }; + + // 비밀번호 초기화 핸들러 + const handlePasswordReset = (userId: string, userName: string) => { + setPasswordResetModal({ + isOpen: true, + userId, + userName, + }); + }; + + // 비밀번호 초기화 모달 닫기 + const handlePasswordResetClose = () => { + setPasswordResetModal({ + isOpen: false, + userId: null, + userName: null, + }); + }; + + // 비밀번호 초기화 성공 핸들러 + const handlePasswordResetSuccess = () => { + handlePasswordResetClose(); + }; + return (
@@ -19,8 +117,70 @@ export default function UserMngPage() {

시스템 사용자 계정 및 권한을 관리합니다

- {/* 메인 컨텐츠 */} - + {/* 툴바 - 검색, 필터, 등록 버튼 */} + + + {/* 에러 메시지 */} + {error && ( +
+
+

오류가 발생했습니다

+ +
+

{error}

+
+ )} + + {/* 사용자 목록 테이블 */} + + + {/* 페이지네이션 */} + {!isLoading && users.length > 0 && ( + + )} + + {/* 사용자 등록/수정 모달 */} + + + {/* 비밀번호 초기화 모달 */} +
{/* Scroll to Top 버튼 (모바일/태블릿 전용) */} diff --git a/frontend/app/(main)/main/page.tsx b/frontend/app/(main)/main/page.tsx index 00ef509b..56558f7e 100644 --- a/frontend/app/(main)/main/page.tsx +++ b/frontend/app/(main)/main/page.tsx @@ -10,7 +10,6 @@ import { Badge } from "@/components/ui/badge"; export default function MainPage() { return (
- {/* 메인 컨텐츠 */} {/* Welcome Message */} diff --git a/frontend/components/admin/CompanyManagement.tsx b/frontend/components/admin/CompanyManagement.tsx deleted file mode 100644 index 4e88e35a..00000000 --- a/frontend/components/admin/CompanyManagement.tsx +++ /dev/null @@ -1,93 +0,0 @@ -"use client"; - -import { useCompanyManagement } from "@/hooks/useCompanyManagement"; -import { CompanyToolbar } from "./CompanyToolbar"; -import { CompanyTable } from "./CompanyTable"; -import { CompanyFormModal } from "./CompanyFormModal"; -import { CompanyDeleteDialog } from "./CompanyDeleteDialog"; -import { DiskUsageSummary } from "./DiskUsageSummary"; - -/** - * 회사 관리 메인 컴포넌트 - * 모든 회사 관리 기능을 통합하여 제공 - */ -export function CompanyManagement() { - const { - // 데이터 - companies, - searchFilter, - isLoading, - error, - - // 디스크 사용량 관련 - diskUsageInfo, - isDiskUsageLoading, - loadDiskUsage, - - // 모달 상태 - modalState, - deleteState, - - // 검색 기능 - updateSearchFilter, - clearSearchFilter, - - // 모달 제어 - openCreateModal, - openEditModal, - closeModal, - updateFormData, - - // 삭제 다이얼로그 제어 - openDeleteDialog, - closeDeleteDialog, - - // CRUD 작업 - saveCompany, - deleteCompany, - - // 에러 처리 - clearError, - } = useCompanyManagement(); - - return ( -
- {/* 디스크 사용량 요약 */} - - - {/* 툴바 - 검색, 필터, 등록 버튼 */} - - - {/* 회사 목록 테이블 */} - - - {/* 회사 등록/수정 모달 */} - - - {/* 회사 삭제 확인 다이얼로그 */} - -
- ); -} diff --git a/frontend/components/admin/CompanySwitcher.tsx b/frontend/components/admin/CompanySwitcher.tsx new file mode 100644 index 00000000..3d53accc --- /dev/null +++ b/frontend/components/admin/CompanySwitcher.tsx @@ -0,0 +1,195 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Building2, Search } from "lucide-react"; +import { useAuth } from "@/hooks/useAuth"; +import { apiClient } from "@/lib/api/client"; +import { logger } from "@/lib/utils/logger"; + +interface Company { + company_code: string; + company_name: string; + status: string; +} + +interface CompanySwitcherProps { + onClose?: () => void; + isOpen?: boolean; // Dialog 열림 상태 (AppLayout에서 전달) +} + +/** + * WACE 관리자 전용: 회사 선택 및 전환 컴포넌트 + * + * - WACE 관리자(company_code = "*", userType = "SUPER_ADMIN")만 표시 + * - 회사 선택 시 해당 회사로 전환하여 시스템 사용 + * - JWT 토큰 재발급으로 company_code 변경 + */ +export function CompanySwitcher({ onClose, isOpen = false }: CompanySwitcherProps = {}) { + const { user, switchCompany } = useAuth(); + const [companies, setCompanies] = useState([]); + const [filteredCompanies, setFilteredCompanies] = useState([]); + const [searchText, setSearchText] = useState(""); + const [loading, setLoading] = useState(false); + + // WACE 관리자 권한 체크 (userType만 확인) + const isWaceAdmin = user?.userType === "SUPER_ADMIN"; + + // 현재 선택된 회사명 표시 + const currentCompanyName = React.useMemo(() => { + if (!user?.companyCode) return "로딩 중..."; + + if (user.companyCode === "*") { + return "WACE (최고 관리자)"; + } + + // companies 배열에서 현재 회사 찾기 + const currentCompany = companies.find(c => c.company_code === user.companyCode); + return currentCompany?.company_name || user.companyCode; + }, [user?.companyCode, companies]); + + // 회사 목록 조회 + useEffect(() => { + if (isWaceAdmin && isOpen) { + fetchCompanies(); + } + }, [isWaceAdmin, isOpen]); + + // 검색 필터링 + useEffect(() => { + if (searchText.trim() === "") { + setFilteredCompanies(companies); + } else { + const filtered = companies.filter(company => + company.company_name.toLowerCase().includes(searchText.toLowerCase()) || + company.company_code.toLowerCase().includes(searchText.toLowerCase()) + ); + setFilteredCompanies(filtered); + } + }, [searchText, companies]); + + const fetchCompanies = async () => { + try { + setLoading(true); + const response = await apiClient.get("/admin/companies/db"); + + if (response.data.success) { + // 활성 상태의 회사만 필터링 + company_code="*" 제외 (WACE는 별도 추가) + const activeCompanies = response.data.data + .filter((c: Company) => c.company_code !== "*") // DB의 "*" 제외 + .filter((c: Company) => c.status === "active" || !c.status) + .sort((a: Company, b: Company) => a.company_name.localeCompare(b.company_name)); + + // WACE 복귀 옵션 추가 + const companiesWithWace: Company[] = [ + { + company_code: "*", + company_name: "WACE (최고 관리자)", + status: "active", + }, + ...activeCompanies, + ]; + + setCompanies(companiesWithWace); + setFilteredCompanies(companiesWithWace); + } + } catch (error) { + logger.error("회사 목록 조회 실패", error); + } finally { + setLoading(false); + } + }; + + const handleCompanySwitch = async (companyCode: string) => { + try { + setLoading(true); + + const result = await switchCompany(companyCode); + + if (!result.success) { + alert(result.message || "회사 전환에 실패했습니다."); + setLoading(false); + return; + } + + logger.info("회사 전환 성공", { companyCode }); + + // 즉시 페이지 새로고침 (토큰이 이미 저장됨) + window.location.reload(); + } catch (error: any) { + logger.error("회사 전환 실패", error); + alert(error.message || "회사 전환 중 오류가 발생했습니다."); + setLoading(false); + } + }; + + // WACE 관리자가 아니면 렌더링하지 않음 + if (!isWaceAdmin) { + return null; + } + + return ( +
+ {/* 현재 회사 정보 */} +
+
+
+ +
+
+

현재 관리 회사

+

{currentCompanyName}

+
+
+
+ + {/* 회사 검색 */} +
+ + setSearchText(e.target.value)} + className="h-10 pl-10 text-sm" + /> +
+ + {/* 회사 목록 */} +
+ {loading ? ( +
+ 로딩 중... +
+ ) : filteredCompanies.length === 0 ? ( +
+ 검색 결과가 없습니다. +
+ ) : ( + filteredCompanies.map((company) => ( +
handleCompanySwitch(company.company_code)} + > +
+ {company.company_name} + + {company.company_code} + +
+ {company.company_code === user?.companyCode && ( + 현재 + )} +
+ )) + )} +
+
+ ); +} + diff --git a/frontend/components/admin/MenuManagement.tsx b/frontend/components/admin/MenuManagement.tsx deleted file mode 100644 index 67e8bab6..00000000 --- a/frontend/components/admin/MenuManagement.tsx +++ /dev/null @@ -1,1136 +0,0 @@ -"use client"; - -import React, { useState, useEffect, useMemo } from "react"; -import { menuApi } from "@/lib/api/menu"; -import type { MenuItem } from "@/lib/api/menu"; -import { MenuTable } from "./MenuTable"; -import { MenuFormModal } from "./MenuFormModal"; -import { MenuCopyDialog } from "./MenuCopyDialog"; -import { Button } from "@/components/ui/button"; -import { LoadingSpinner, LoadingOverlay } from "@/components/common/LoadingSpinner"; -import { toast } from "sonner"; -import { Badge } from "@/components/ui/badge"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; -import { useMenu } from "@/contexts/MenuContext"; -import { useMenuManagementText, setTranslationCache, getMenuTextSync } from "@/lib/utils/multilang"; -import { useMultiLang } from "@/hooks/useMultiLang"; -import { apiClient } from "@/lib/api/client"; -import { useAuth } from "@/hooks/useAuth"; // useAuth 추가 - -type MenuType = "admin" | "user"; - -export const MenuManagement: React.FC = () => { - const { adminMenus, userMenus, refreshMenus } = useMenu(); - const { user } = useAuth(); // 현재 사용자 정보 가져오기 - const [selectedMenuType, setSelectedMenuType] = useState("admin"); - const [loading, setLoading] = useState(false); - const [deleting, setDeleting] = useState(false); - const [formModalOpen, setFormModalOpen] = useState(false); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [copyDialogOpen, setCopyDialogOpen] = useState(false); - const [selectedMenuId, setSelectedMenuId] = useState(""); - const [selectedMenuName, setSelectedMenuName] = useState(""); - const [selectedMenus, setSelectedMenus] = useState>(new Set()); - - // 메뉴 관리 화면용 로컬 상태 (모든 상태의 메뉴 표시) - const [localAdminMenus, setLocalAdminMenus] = useState([]); - const [localUserMenus, setLocalUserMenus] = useState([]); - - // 다국어 텍스트 훅 사용 - // getMenuText는 더 이상 사용하지 않음 - getUITextSync만 사용 - const { userLang } = useMultiLang({ companyCode: "*" }); - - // SUPER_ADMIN 여부 확인 - const isSuperAdmin = user?.userType === "SUPER_ADMIN"; - - // 다국어 텍스트 상태 - const [uiTexts, setUiTexts] = useState>({}); - const [uiTextsLoading, setUiTextsLoading] = useState(false); - - // 회사 목록 상태 - const [companies, setCompanies] = useState>([]); - const [selectedCompany, setSelectedCompany] = useState("all"); - const [searchText, setSearchText] = useState(""); - const [expandedMenus, setExpandedMenus] = useState>(new Set()); - const [companySearchText, setCompanySearchText] = useState(""); - const [isCompanyDropdownOpen, setIsCompanyDropdownOpen] = useState(false); - const [formData, setFormData] = useState({ - menuId: "", - parentId: "", - menuType: "", - level: 0, - parentCompanyCode: "", - }); - - // 언어별 텍스트 매핑 테이블 제거 - DB에서 직접 가져옴 - - // 메뉴관리 페이지에서 사용할 다국어 키들 (실제 DB에 등록된 키들) - const MENU_MANAGEMENT_LANG_KEYS = [ - // 페이지 제목 및 설명 - "menu.management.title", - "menu.management.description", - "menu.type.title", - "menu.type.admin", - "menu.type.user", - "menu.management.admin", - "menu.management.user", - "menu.management.admin.description", - "menu.management.user.description", - - // 버튼 - "button.add", - "button.add.top.level", - "button.add.sub", - "button.edit", - "button.delete", - "button.delete.selected", - "button.delete.selected.count", - "button.delete.processing", - "button.cancel", - "button.save", - "button.register", - "button.modify", - - // 필터 및 검색 - "filter.company", - "filter.company.all", - "filter.company.common", - "filter.company.search", - "filter.search", - "filter.search.placeholder", - "filter.reset", - - // 테이블 헤더 - "table.header.select", - "table.header.menu.name", - "table.header.menu.url", - "table.header.menu.type", - "table.header.status", - "table.header.company", - "table.header.sequence", - "table.header.actions", - - // 상태 - "status.active", - "status.inactive", - "status.unspecified", - - // 폼 - "form.menu.type", - "form.menu.type.admin", - "form.menu.type.user", - "form.company", - "form.company.select", - "form.company.common", - "form.company.submenu.note", - "form.lang.key", - "form.lang.key.select", - "form.lang.key.none", - "form.lang.key.search", - "form.lang.key.selected", - "form.menu.name", - "form.menu.name.placeholder", - "form.menu.url", - "form.menu.url.placeholder", - "form.menu.description", - "form.menu.description.placeholder", - "form.menu.sequence", - - // 모달 - "modal.menu.register.title", - "modal.menu.modify.title", - "modal.delete.title", - "modal.delete.description", - "modal.delete.batch.description", - - // 메시지 - "message.loading", - "message.menu.delete.processing", - "message.menu.save.success", - "message.menu.save.failed", - "message.menu.delete.success", - "message.menu.delete.failed", - "message.menu.delete.batch.success", - "message.menu.delete.batch.partial", - "message.menu.status.toggle.success", - "message.menu.status.toggle.failed", - "message.validation.menu.name.required", - "message.validation.company.required", - "message.validation.select.menu.delete", - "message.error.load.menu.list", - "message.error.load.menu.info", - "message.error.load.company.list", - "message.error.load.lang.key.list", - - // 리스트 정보 - "menu.list.title", - "menu.list.total", - "menu.list.search.result", - - // UI - "ui.expand", - "ui.collapse", - "ui.menu.collapse", - "ui.language", - ]; - - // 초기 로딩 - useEffect(() => { - loadCompanies(); - loadMenus(false); // 메뉴 목록 로드 (메뉴 관리 화면용 - 모든 상태 표시) - // 사용자 언어가 설정되지 않았을 때만 기본 텍스트 설정 - if (!userLang) { - initializeDefaultTexts(); - } - }, [userLang]); // userLang 변경 시마다 실행 - - // 초기 기본 텍스트 설정 함수 - const initializeDefaultTexts = () => { - const defaultTexts: Record = {}; - MENU_MANAGEMENT_LANG_KEYS.forEach((key) => { - // 기본 한국어 텍스트 제공 - const defaultText = getDefaultText(key); - defaultTexts[key] = defaultText; - }); - setUiTexts(defaultTexts); - // console.log("🌐 초기 기본 텍스트 설정 완료:", Object.keys(defaultTexts).length); - }; - - // 기본 텍스트 반환 함수 - const getDefaultText = (key: string): string => { - const defaultTexts: Record = { - "menu.management.title": "메뉴 관리", - "menu.management.description": "시스템의 메뉴 구조와 권한을 관리합니다.", - "menu.type.title": "메뉴 타입", - "menu.type.admin": "관리자", - "menu.type.user": "사용자", - "menu.management.admin": "관리자 메뉴", - "menu.management.user": "사용자 메뉴", - "menu.management.admin.description": "시스템 관리 및 설정 메뉴", - "menu.management.user.description": "일반 사용자 업무 메뉴", - "button.add": "추가", - "button.add.top.level": "최상위 메뉴 추가", - "button.add.sub": "하위 메뉴 추가", - "button.edit": "수정", - "button.delete": "삭제", - "button.delete.selected": "선택 삭제", - "button.delete.selected.count": "선택 삭제 ({count})", - "button.delete.processing": "삭제 중...", - "button.cancel": "취소", - "button.save": "저장", - "button.register": "등록", - "button.modify": "수정", - "filter.company": "회사", - "filter.company.all": "전체", - "filter.company.common": "공통", - "filter.company.search": "회사 검색", - "filter.search": "검색", - "filter.search.placeholder": "메뉴명 또는 URL로 검색...", - "filter.reset": "초기화", - "table.header.select": "선택", - "table.header.menu.name": "메뉴명", - "table.header.menu.url": "URL", - "table.header.menu.type": "메뉴 타입", - "table.header.status": "상태", - "table.header.company": "회사", - "table.header.sequence": "순서", - "table.header.actions": "작업", - "status.active": "활성화", - "status.inactive": "비활성화", - "status.unspecified": "미지정", - "form.menu.type": "메뉴 타입", - "form.menu.type.admin": "관리자", - "form.menu.type.user": "사용자", - "form.company": "회사", - "form.company.select": "회사를 선택하세요", - "form.company.common": "공통", - "form.company.submenu.note": "하위 메뉴는 상위 메뉴와 동일한 회사를 가져야 합니다.", - "form.lang.key": "다국어 키", - "form.lang.key.select": "다국어 키를 선택하세요", - "form.lang.key.none": "다국어 키 없음", - "form.lang.key.search": "다국어 키 검색...", - "form.lang.key.selected": "선택된 키: {key} - {description}", - "form.menu.name": "메뉴명", - "form.menu.name.placeholder": "메뉴명을 입력하세요", - "form.menu.url": "URL", - "form.menu.url.placeholder": "메뉴 URL을 입력하세요", - "form.menu.description": "설명", - "form.menu.description.placeholder": "메뉴 설명을 입력하세요", - "form.menu.sequence": "순서", - "modal.menu.register.title": "메뉴 등록", - "modal.menu.modify.title": "메뉴 수정", - "modal.delete.title": "메뉴 삭제", - "modal.delete.description": "해당 메뉴를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", - "modal.delete.batch.description": - "선택된 {count}개의 메뉴를 영구적으로 삭제하시겠습니까?\n\n⚠️ 주의: 상위 메뉴를 삭제하면 하위 메뉴들도 함께 삭제됩니다.\n이 작업은 되돌릴 수 없습니다.", - "message.loading": "로딩 중...", - "message.menu.delete.processing": "메뉴 삭제 중...", - "message.menu.save.success": "메뉴가 성공적으로 저장되었습니다.", - "message.menu.save.failed": "메뉴 저장에 실패했습니다.", - "message.menu.delete.success": "메뉴가 성공적으로 삭제되었습니다.", - "message.menu.delete.failed": "메뉴 삭제에 실패했습니다.", - "message.menu.delete.batch.success": "선택된 메뉴들이 성공적으로 삭제되었습니다.", - "message.menu.delete.batch.partial": "일부 메뉴 삭제에 실패했습니다.", - "message.menu.status.toggle.success": "메뉴 상태가 변경되었습니다.", - "message.menu.status.toggle.failed": "메뉴 상태 변경에 실패했습니다.", - "message.validation.menu.name.required": "메뉴명을 입력해주세요.", - "message.validation.company.required": "회사를 선택해주세요.", - "message.validation.select.menu.delete": "삭제할 메뉴를 선택해주세요.", - "message.error.load.menu.list": "메뉴 목록을 불러오는데 실패했습니다.", - "message.error.load.menu.info": "메뉴 정보를 불러오는데 실패했습니다.", - "message.error.load.company.list": "회사 목록을 불러오는데 실패했습니다.", - "message.error.load.lang.key.list": "다국어 키 목록을 불러오는데 실패했습니다.", - "menu.list.title": "메뉴 목록", - "menu.list.total": "총 {count}개", - "menu.list.search.result": "검색 결과: {count}개", - "ui.expand": "펼치기", - "ui.collapse": "접기", - "ui.menu.collapse": "메뉴 접기", - "ui.language": "언어", - }; - - return defaultTexts[key] || key; - }; - - // 컴포넌트 마운트 시 및 userLang 변경 시 다국어 텍스트 로드 - useEffect(() => { - if (userLang && !uiTextsLoading) { - loadUITexts(); - } - }, [userLang]); // userLang 변경 시마다 실행 - - // uiTexts 상태 변경 감지 - useEffect(() => { - // console.log("🔄 uiTexts 상태 변경됨:", { - // count: Object.keys(uiTexts).length, - // sampleKeys: Object.keys(uiTexts).slice(0, 5), - // sampleValues: Object.entries(uiTexts) - // .slice(0, 3) - // .map(([k, v]) => `${k}: ${v}`), - // }); - }, [uiTexts]); - - // 컴포넌트 마운트 후 다국어 텍스트 강제 로드 (userLang이 아직 설정되지 않았을 수 있음) - useEffect(() => { - const timer = setTimeout(() => { - if (userLang && !uiTextsLoading) { - // console.log("🔄 컴포넌트 마운트 후 다국어 텍스트 강제 로드"); - loadUITexts(); - } - }, 300); // 300ms 후 실행 - - return () => clearTimeout(timer); - }, [userLang]); // userLang이 설정된 후 실행 - - // 추가 안전장치: 컴포넌트 마운트 후 일정 시간이 지나면 강제로 다국어 텍스트 로드 - useEffect(() => { - const fallbackTimer = setTimeout(() => { - if (!uiTextsLoading && Object.keys(uiTexts).length === 0) { - // console.log("🔄 안전장치: 컴포넌트 마운트 후 강제 다국어 텍스트 로드"); - // 사용자 언어가 설정되지 않았을 때만 기본 텍스트 설정 - if (!userLang) { - initializeDefaultTexts(); - } else { - // 사용자 언어가 설정된 경우 다국어 텍스트 로드 - loadUITexts(); - } - } - }, 1000); // 1초 후 실행 - - return () => clearTimeout(fallbackTimer); - }, [userLang]); // userLang 변경 시마다 실행 - - // 번역 로드 이벤트 감지 - useEffect(() => { - const handleTranslationLoaded = (event: CustomEvent) => { - const { key, text, userLang: loadedLang } = event.detail; - if (loadedLang === userLang) { - setUiTexts((prev) => ({ ...prev, [key]: text })); - } - }; - - window.addEventListener("translation-loaded", handleTranslationLoaded as EventListener); - - return () => { - window.removeEventListener("translation-loaded", handleTranslationLoaded as EventListener); - }; - }, [userLang]); - - // 드롭다운 외부 클릭 시 닫기 - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - const target = event.target as Element; - if (!target.closest(".company-dropdown")) { - setIsCompanyDropdownOpen(false); - setCompanySearchText(""); - } - }; - - if (isCompanyDropdownOpen) { - document.addEventListener("mousedown", handleClickOutside); - } - - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [isCompanyDropdownOpen]); - - // 특정 메뉴 타입만 로드하는 함수 - const loadMenusForType = async (type: MenuType, showLoading = true) => { - try { - if (showLoading) { - setLoading(true); - } - - if (type === "admin") { - const adminResponse = await menuApi.getAdminMenusForManagement(); - if (adminResponse.success && adminResponse.data) { - setLocalAdminMenus(adminResponse.data); - } - } else { - const userResponse = await menuApi.getUserMenusForManagement(); - if (userResponse.success && userResponse.data) { - setLocalUserMenus(userResponse.data); - } - } - } catch (error) { - toast.error(getUITextSync("message.error.load.menu.list")); - } finally { - if (showLoading) { - setLoading(false); - } - } - }; - - const loadMenus = async (showLoading = true) => { - // console.log(`📋 메뉴 목록 조회 시작 (showLoading: ${showLoading})`); - try { - if (showLoading) { - setLoading(true); - } - - // 선택된 메뉴 타입에 해당하는 메뉴만 로드 - if (selectedMenuType === "admin") { - const adminResponse = await menuApi.getAdminMenusForManagement(); - if (adminResponse.success && adminResponse.data) { - setLocalAdminMenus(adminResponse.data); - } - } else { - const userResponse = await menuApi.getUserMenusForManagement(); - if (userResponse.success && userResponse.data) { - setLocalUserMenus(userResponse.data); - } - } - - // 전역 메뉴 상태도 업데이트 (좌측 사이드바용) - await refreshMenus(); - // console.log("📋 메뉴 목록 조회 성공"); - } catch (error) { - // console.error("❌ 메뉴 목록 조회 실패:", error); - toast.error(getUITextSync("message.error.load.menu.list")); - } finally { - if (showLoading) { - setLoading(false); - } - } - }; - - // 회사 목록 조회 - const loadCompanies = async () => { - // console.log("🏢 회사 목록 조회 시작"); - try { - const response = await apiClient.get("/admin/companies"); - - if (response.data.success) { - // console.log("🏢 회사 목록 응답:", response.data); - const companyList = response.data.data.map((company: any) => ({ - code: company.company_code || company.companyCode, - name: company.company_name || company.companyName, - })); - // console.log("🏢 변환된 회사 목록:", companyList); - setCompanies(companyList); - } - } catch (error) { - // console.error("❌ 회사 목록 조회 실패:", error); - } - }; - - // 다국어 텍스트 로드 함수 - 배치 API 사용 - const loadUITexts = async () => { - if (uiTextsLoading) return; // 이미 로딩 중이면 중단 - - // userLang이 설정되지 않았으면 기본값 설정 - if (!userLang) { - // console.log("🌐 사용자 언어가 설정되지 않음, 기본값 설정"); - const defaultTexts: Record = {}; - MENU_MANAGEMENT_LANG_KEYS.forEach((key) => { - defaultTexts[key] = getDefaultText(key); // 기본 한국어 텍스트 사용 - }); - setUiTexts(defaultTexts); - return; - } - - // 사용자 언어가 설정된 경우, 기존 uiTexts가 비어있으면 기본 텍스트로 초기화 - if (Object.keys(uiTexts).length === 0) { - // console.log("🌐 기존 uiTexts가 비어있음, 기본 텍스트로 초기화"); - const defaultTexts: Record = {}; - MENU_MANAGEMENT_LANG_KEYS.forEach((key) => { - defaultTexts[key] = getDefaultText(key); - }); - setUiTexts(defaultTexts); - } - - // console.log("🌐 UI 다국어 텍스트 로드 시작", { - // userLang, - // apiParams: { - // companyCode: "*", - // menuCode: "menu.management", - // userLang: userLang, - // }, - // }); - setUiTextsLoading(true); - - try { - // 배치 API를 사용하여 모든 다국어 키를 한 번에 조회 - const response = await apiClient.post( - "/multilang/batch", - { - langKeys: MENU_MANAGEMENT_LANG_KEYS, - companyCode: "*", // 모든 회사 - menuCode: "menu.management", // 메뉴관리 메뉴 - userLang: userLang, // body에 포함 - }, - { - params: {}, // query params는 비움 - }, - ); - - if (response.data.success) { - const translations = response.data.data; - // console.log("🌐 배치 다국어 텍스트 응답:", translations); - - // 번역 결과를 상태에 저장 (기존 uiTexts와 병합) - const mergedTranslations = { ...uiTexts, ...translations }; - // console.log("🔧 setUiTexts 호출 전:", { - // translationsCount: Object.keys(translations).length, - // mergedCount: Object.keys(mergedTranslations).length, - // }); - setUiTexts(mergedTranslations); - // console.log("🔧 setUiTexts 호출 후 - mergedTranslations:", mergedTranslations); - - // 번역 캐시에 저장 (다른 컴포넌트에서도 사용할 수 있도록) - setTranslationCache(userLang, mergedTranslations); - } else { - // console.error("❌ 다국어 텍스트 배치 조회 실패:", response.data.message); - // API 실패 시에도 기존 uiTexts는 유지 - // console.log("🔄 API 실패로 인해 기존 uiTexts 유지"); - } - } catch (error) { - // console.error("❌ UI 다국어 텍스트 로드 실패:", error); - // API 실패 시에도 기존 uiTexts는 유지 - // console.log("🔄 API 실패로 인해 기존 uiTexts 유지"); - } finally { - setUiTextsLoading(false); - } - }; - - // UI 텍스트 가져오기 함수 (동기 버전만 사용) - // getUIText 함수는 제거 - getUITextSync만 사용 - - // 동기 버전 (DB에서 가져온 번역 텍스트 사용) - const getUITextSync = (key: string, params?: Record, fallback?: string): string => { - // uiTexts에서 번역 텍스트 찾기 - let text = uiTexts[key]; - - // uiTexts에 없으면 getMenuTextSync로 기본 한글 텍스트 가져오기 - if (!text) { - text = getMenuTextSync(key, userLang) || fallback || key; - } - - // 파라미터 치환 - if (params && text) { - Object.entries(params).forEach(([paramKey, paramValue]) => { - text = text!.replace(`{${paramKey}}`, String(paramValue)); - }); - } - - return text || key; - }; - - // 다국어 API 테스트 함수 (getUITextSync 사용) - const testMultiLangAPI = async () => { - // console.log("🧪 다국어 API 테스트 시작"); - try { - const text = getUITextSync("menu.management.admin"); - // console.log("🧪 다국어 API 테스트 결과:", text); - } catch (error) { - // console.error("❌ 다국어 API 테스트 실패:", error); - } - }; - - // 대문자 키를 소문자 키로 변환하는 함수 - const convertMenuData = (data: any[]): MenuItem[] => { - return data.map((item) => ({ - objid: item.OBJID || item.objid, - parent_obj_id: item.PARENT_OBJ_ID || item.parent_obj_id, - menu_name_kor: item.MENU_NAME_KOR || item.menu_name_kor, - menu_url: item.MENU_URL || item.menu_url, - menu_desc: item.MENU_DESC || item.menu_desc, - seq: item.SEQ || item.seq, - menu_type: item.MENU_TYPE || item.menu_type, - status: item.STATUS || item.status, - lev: item.LEV || item.lev, - lpad_menu_name_kor: item.LPAD_MENU_NAME_KOR || item.lpad_menu_name_kor, - status_title: item.STATUS_TITLE || item.status_title, - writer: item.WRITER || item.writer, - regdate: item.REGDATE || item.regdate, - company_code: item.COMPANY_CODE || item.company_code, - company_name: item.COMPANY_NAME || item.company_name, - })); - }; - - const handleAddTopLevelMenu = () => { - setFormData({ - menuId: "", - parentId: "0", // 최상위 메뉴는 parentId가 0 - menuType: getMenuTypeValue(), - level: 1, // 최상위 메뉴는 level 1 - parentCompanyCode: "", // 최상위 메뉴는 상위 회사 정보 없음 - }); - setFormModalOpen(true); - }; - - const handleAddMenu = (parentId: string, menuType: string, level: number) => { - // 상위 메뉴의 회사 정보 찾기 - const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus; - const parentMenu = currentMenus.find((menu) => menu.objid === parentId); - - setFormData({ - menuId: "", - parentId, - menuType, - level: level + 1, - parentCompanyCode: parentMenu?.company_code || "", - }); - setFormModalOpen(true); - }; - - const handleEditMenu = (menuId: string) => { - // console.log("🔧 메뉴 수정 시작 - menuId:", menuId); - - // 현재 메뉴 정보 찾기 - const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus; - const menuToEdit = currentMenus.find((menu) => (menu.objid || menu.OBJID) === menuId); - - if (menuToEdit) { - // console.log("수정할 메뉴 정보:", menuToEdit); - - setFormData({ - menuId: menuId, - parentId: menuToEdit.parent_obj_id || menuToEdit.PARENT_OBJ_ID || "", - menuType: selectedMenuType, // 현재 선택된 메뉴 타입 - level: 0, // 기본값 - parentCompanyCode: menuToEdit.company_code || menuToEdit.COMPANY_CODE || "", - }); - - // console.log("설정된 formData:", { - // menuId: menuId, - // parentId: menuToEdit.parent_obj_id || menuToEdit.PARENT_OBJ_ID || "", - // menuType: selectedMenuType, - // level: 0, - // parentCompanyCode: menuToEdit.company_code || menuToEdit.COMPANY_CODE || "", - // }); - } else { - // console.error("수정할 메뉴를 찾을 수 없음:", menuId); - } - - setFormModalOpen(true); - }; - - const handleMenuSelectionChange = (menuId: string, checked: boolean) => { - const newSelected = new Set(selectedMenus); - if (checked) { - newSelected.add(menuId); - } else { - newSelected.delete(menuId); - } - setSelectedMenus(newSelected); - }; - - const handleSelectAllMenus = (checked: boolean) => { - const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus; - if (checked) { - // 모든 메뉴 선택 (최상위 메뉴 포함) - setSelectedMenus(new Set(currentMenus.map((menu) => menu.objid || menu.OBJID || ""))); - } else { - setSelectedMenus(new Set()); - } - }; - - const handleDeleteSelectedMenus = async () => { - if (selectedMenus.size === 0) { - toast.error(getUITextSync("message.validation.select.menu.delete")); - return; - } - - if (!confirm(getUITextSync("modal.delete.batch.description", { count: selectedMenus.size }))) { - return; - } - - setDeleting(true); - try { - const menuIds = Array.from(selectedMenus); - // console.log("삭제할 메뉴 IDs:", menuIds); - - toast.info(getUITextSync("message.menu.delete.processing")); - - const response = await menuApi.deleteMenusBatch(menuIds); - // console.log("삭제 API 응답:", response); - // console.log("응답 구조:", { - // success: response.success, - // data: response.data, - // message: response.message, - // }); - - if (response.success && response.data) { - const { deletedCount, failedCount } = response.data; - // console.log("삭제 결과:", { deletedCount, failedCount }); - - // 선택된 메뉴 초기화 - setSelectedMenus(new Set()); - - // 메뉴 목록 즉시 새로고침 (로딩 상태 없이) - // console.log("메뉴 목록 새로고침 시작"); - await loadMenus(false); - // 전역 메뉴 상태도 업데이트 - await refreshMenus(); - // console.log("메뉴 목록 새로고침 완료"); - - // 삭제 결과 메시지 - if (failedCount === 0) { - toast.success(getUITextSync("message.menu.delete.batch.success", { count: deletedCount })); - } else { - toast.success( - getUITextSync("message.menu.delete.batch.partial", { - success: deletedCount, - failed: failedCount, - }), - ); - } - } else { - // console.error("삭제 실패:", response); - toast.error(response.message || "메뉴 삭제에 실패했습니다."); - } - } catch (error) { - // console.error("메뉴 삭제 중 오류:", error); - toast.error(getUITextSync("message.menu.delete.failed")); - } finally { - setDeleting(false); - } - }; - - const confirmDelete = async () => { - try { - const response = await menuApi.deleteMenu(selectedMenuId); - if (response.success) { - toast.success(response.message); - await loadMenus(false); - } else { - toast.error(response.message); - } - } catch (error) { - toast.error("메뉴 삭제에 실패했습니다."); - } finally { - setDeleteDialogOpen(false); - setSelectedMenuId(""); - } - }; - - const handleCopyMenu = (menuId: string, menuName: string) => { - setSelectedMenuId(menuId); - setSelectedMenuName(menuName); - setCopyDialogOpen(true); - }; - - const handleCopyComplete = async () => { - // 복사 완료 후 메뉴 목록 새로고침 - await loadMenus(false); - toast.success("메뉴 복사가 완료되었습니다"); - }; - - const handleToggleStatus = async (menuId: string) => { - try { - const response = await menuApi.toggleMenuStatus(menuId); - if (response.success) { - toast.success(response.message); - await loadMenus(false); // 메뉴 목록 새로고침 - // 전역 메뉴 상태도 업데이트 - await refreshMenus(); - } else { - toast.error(response.message); - } - } catch (error) { - // console.error("메뉴 상태 토글 오류:", error); - toast.error(getUITextSync("message.menu.status.toggle.failed")); - } - }; - - const handleFormSuccess = () => { - loadMenus(false); - // 전역 메뉴 상태도 업데이트 - refreshMenus(); - }; - - const getCurrentMenus = () => { - // 메뉴 관리 화면용: 모든 상태의 메뉴 표시 (localAdminMenus/localUserMenus 사용) - const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus; - - // 검색어 필터링 - let filteredMenus = currentMenus; - if (searchText.trim()) { - const searchLower = searchText.toLowerCase(); - filteredMenus = currentMenus.filter((menu) => { - const menuName = (menu.menu_name_kor || menu.MENU_NAME_KOR || "").toLowerCase(); - const menuUrl = (menu.menu_url || menu.MENU_URL || "").toLowerCase(); - return menuName.includes(searchLower) || menuUrl.includes(searchLower); - }); - } - - // 회사 필터링 - if (selectedCompany !== "all") { - filteredMenus = filteredMenus.filter((menu) => { - const menuCompanyCode = menu.company_code || menu.COMPANY_CODE || ""; - return menuCompanyCode === selectedCompany; - }); - } - - return filteredMenus; - }; - - // 메뉴 타입 변경 시 선택된 메뉴 초기화 - const handleMenuTypeChange = (type: MenuType) => { - setSelectedMenuType(type); - setSelectedMenus(new Set()); // 선택된 메뉴 초기화 - setExpandedMenus(new Set()); // 메뉴 타입 변경 시 확장 상태 초기화 - - // 선택한 메뉴 타입에 해당하는 메뉴만 로드 - if (type === "admin" && localAdminMenus.length === 0) { - loadMenusForType("admin", false); - } else if (type === "user" && localUserMenus.length === 0) { - loadMenusForType("user", false); - } - }; - - const handleToggleExpand = (menuId: string) => { - const newExpandedMenus = new Set(expandedMenus); - if (newExpandedMenus.has(menuId)) { - newExpandedMenus.delete(menuId); - } else { - newExpandedMenus.add(menuId); - } - setExpandedMenus(newExpandedMenus); - }; - - const getMenuTypeString = () => { - return selectedMenuType === "admin" ? getUITextSync("menu.type.admin") : getUITextSync("menu.type.user"); - }; - - const getMenuTypeValue = () => { - return selectedMenuType === "admin" ? "0" : "1"; - }; - - // uiTextsCount를 useMemo로 계산하여 상태 변경 시에만 재계산 - const uiTextsCount = useMemo(() => Object.keys(uiTexts).length, [uiTexts]); - const adminMenusCount = useMemo(() => localAdminMenus?.length || 0, [localAdminMenus]); - const userMenusCount = useMemo(() => localUserMenus?.length || 0, [localUserMenus]); - - // 디버깅을 위한 간단한 상태 표시 - // console.log("🔍 MenuManagement 렌더링 상태:", { - // loading, - // uiTextsLoading, - // uiTextsCount, - // adminMenusCount, - // userMenusCount, - // selectedMenuType, - // userLang, - // }); - - if (loading) { - return ( -
- -
- ); - } - - return ( - -
- {/* 좌측 사이드바 - 메뉴 타입 선택 (20%) */} -
-
-

{getUITextSync("menu.type.title")}

- - {/* 메뉴 타입 선택 카드들 */} -
-
handleMenuTypeChange("admin")} - > -
-
-

{getUITextSync("menu.management.admin")}

-

- {getUITextSync("menu.management.admin.description")} -

-
- - {localAdminMenus.length} - -
-
- -
handleMenuTypeChange("user")} - > -
-
-

{getUITextSync("menu.management.user")}

-

- {getUITextSync("menu.management.user.description")} -

-
- - {localUserMenus.length} - -
-
-
-
-
- - {/* 우측 메인 영역 - 메뉴 목록 (80%) */} -
-
- {/* 상단 헤더: 제목 + 검색 + 버튼 */} -
- {/* 왼쪽: 제목 */} -

- {getMenuTypeString()} {getUITextSync("menu.list.title")} -

- - {/* 오른쪽: 검색 + 버튼 */} -
- {/* 회사 선택 */} -
-
- - - {isCompanyDropdownOpen && ( -
-
- setCompanySearchText(e.target.value)} - className="h-8 text-sm" - onClick={(e) => e.stopPropagation()} - /> -
- -
-
{ - setSelectedCompany("all"); - setIsCompanyDropdownOpen(false); - setCompanySearchText(""); - }} - > - {getUITextSync("filter.company.all")} -
-
{ - setSelectedCompany("*"); - setIsCompanyDropdownOpen(false); - setCompanySearchText(""); - }} - > - {getUITextSync("filter.company.common")} -
- - {companies - .filter((company) => company.code && company.code.trim() !== "") - .filter( - (company) => - company.name.toLowerCase().includes(companySearchText.toLowerCase()) || - company.code.toLowerCase().includes(companySearchText.toLowerCase()), - ) - .map((company, index) => ( -
{ - setSelectedCompany(company.code); - setIsCompanyDropdownOpen(false); - setCompanySearchText(""); - }} - > - {company.code === "*" ? getUITextSync("filter.company.common") : company.name} -
- ))} -
-
- )} -
-
- - {/* 검색 입력 */} -
- setSearchText(e.target.value)} - className="h-10 text-sm" - /> -
- - {/* 초기화 버튼 */} - - - {/* 최상위 메뉴 추가 */} - - - {/* 선택 삭제 */} - {selectedMenus.size > 0 && ( - - )} -
-
- - {/* 테이블 영역 */} -
- -
-
-
-
- - setFormModalOpen(false)} - onSuccess={handleFormSuccess} - menuId={formData.menuId} - parentId={formData.parentId} - menuType={formData.menuType} - level={formData.level} - parentCompanyCode={formData.parentCompanyCode} - uiTexts={uiTexts} - /> - - - - - 메뉴 삭제 - - 해당 메뉴를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. - - - - 취소 - 삭제 - - - - - -
- ); -}; diff --git a/frontend/components/admin/MonitoringDashboard.tsx b/frontend/components/admin/MonitoringDashboard.tsx deleted file mode 100644 index 500dd4fb..00000000 --- a/frontend/components/admin/MonitoringDashboard.tsx +++ /dev/null @@ -1,288 +0,0 @@ -"use client"; - -import React, { useState, useEffect } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { Progress } from "@/components/ui/progress"; -import { RefreshCw, Play, Pause, AlertCircle, CheckCircle, Clock } from "lucide-react"; -import { toast } from "sonner"; -import { BatchAPI, BatchMonitoring, BatchExecution } from "@/lib/api/batch"; - -export default function MonitoringDashboard() { - const [monitoring, setMonitoring] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [autoRefresh, setAutoRefresh] = useState(false); - - useEffect(() => { - loadMonitoringData(); - - let interval: NodeJS.Timeout; - if (autoRefresh) { - interval = setInterval(loadMonitoringData, 30000); // 30초마다 자동 새로고침 - } - - return () => { - if (interval) clearInterval(interval); - }; - }, [autoRefresh]); - - const loadMonitoringData = async () => { - setIsLoading(true); - try { - const data = await BatchAPI.getBatchMonitoring(); - setMonitoring(data); - } catch (error) { - console.error("모니터링 데이터 조회 오류:", error); - toast.error("모니터링 데이터를 불러오는데 실패했습니다."); - } finally { - setIsLoading(false); - } - }; - - const handleRefresh = () => { - loadMonitoringData(); - }; - - const toggleAutoRefresh = () => { - setAutoRefresh(!autoRefresh); - }; - - const getStatusIcon = (status: string) => { - switch (status) { - case 'completed': - return ; - case 'failed': - return ; - case 'running': - return ; - case 'pending': - return ; - default: - return ; - } - }; - - const getStatusBadge = (status: string) => { - const variants = { - completed: "bg-green-100 text-green-800", - failed: "bg-destructive/20 text-red-800", - running: "bg-primary/20 text-blue-800", - pending: "bg-yellow-100 text-yellow-800", - cancelled: "bg-gray-100 text-gray-800", - }; - - const labels = { - completed: "완료", - failed: "실패", - running: "실행 중", - pending: "대기 중", - cancelled: "취소됨", - }; - - return ( - - {labels[status as keyof typeof labels] || status} - - ); - }; - - const formatDuration = (ms: number) => { - if (ms < 1000) return `${ms}ms`; - if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; - return `${(ms / 60000).toFixed(1)}m`; - }; - - const getSuccessRate = () => { - if (!monitoring) return 0; - const total = monitoring.successful_jobs_today + monitoring.failed_jobs_today; - if (total === 0) return 100; - return Math.round((monitoring.successful_jobs_today / total) * 100); - }; - - if (!monitoring) { - return ( -
-
- -

모니터링 데이터를 불러오는 중...

-
-
- ); - } - - return ( -
- {/* 헤더 */} -
-

배치 모니터링

-
- - -
-
- - {/* 통계 카드 */} -
- - - 총 작업 수 -
📋
-
- -
{monitoring.total_jobs}
-

- 활성: {monitoring.active_jobs}개 -

-
-
- - - - 실행 중 -
🔄
-
- -
{monitoring.running_jobs}
-

- 현재 실행 중인 작업 -

-
-
- - - - 오늘 성공 -
-
- -
{monitoring.successful_jobs_today}
-

- 성공률: {getSuccessRate()}% -

-
-
- - - - 오늘 실패 -
-
- -
{monitoring.failed_jobs_today}
-

- 주의가 필요한 작업 -

-
-
-
- - {/* 성공률 진행바 */} - - - 오늘 실행 성공률 - - -
-
- 성공: {monitoring.successful_jobs_today}건 - 실패: {monitoring.failed_jobs_today}건 -
- -
- {getSuccessRate()}% 성공률 -
-
-
-
- - {/* 최근 실행 이력 */} - - - 최근 실행 이력 - - - {monitoring.recent_executions.length === 0 ? ( -
- 최근 실행 이력이 없습니다. -
- ) : ( - - - - 상태 - 작업 ID - 시작 시간 - 완료 시간 - 실행 시간 - 오류 메시지 - - - - {monitoring.recent_executions.map((execution) => ( - - -
- {getStatusIcon(execution.execution_status)} - {getStatusBadge(execution.execution_status)} -
-
- #{execution.job_id} - - {execution.started_at - ? new Date(execution.started_at).toLocaleString() - : "-"} - - - {execution.completed_at - ? new Date(execution.completed_at).toLocaleString() - : "-"} - - - {execution.execution_time_ms - ? formatDuration(execution.execution_time_ms) - : "-"} - - - {execution.error_message ? ( - - {execution.error_message} - - ) : ( - "-" - )} - -
- ))} -
-
- )} -
-
-
- ); -} diff --git a/frontend/components/admin/MultiLang.tsx b/frontend/components/admin/MultiLang.tsx deleted file mode 100644 index abdadcdb..00000000 --- a/frontend/components/admin/MultiLang.tsx +++ /dev/null @@ -1,859 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Badge } from "@/components/ui/badge"; - -import { DataTable } from "@/components/common/DataTable"; -import { LoadingSpinner } from "@/components/common/LoadingSpinner"; -import { useAuth } from "@/hooks/useAuth"; -import LangKeyModal from "./LangKeyModal"; -import LanguageModal from "./LanguageModal"; -import { apiClient } from "@/lib/api/client"; - -interface Language { - langCode: string; - langName: string; - langNative: string; - isActive: string; -} - -interface LangKey { - keyId: number; - companyCode: string; - menuName: string; - langKey: string; - description: string; - isActive: string; -} - -interface LangText { - textId: number; - keyId: number; - langCode: string; - langText: string; - isActive: string; -} - -export default function MultiLangPage() { - const { user } = useAuth(); - const [loading, setLoading] = useState(true); - const [languages, setLanguages] = useState([]); - const [langKeys, setLangKeys] = useState([]); - const [selectedKey, setSelectedKey] = useState(null); - const [langTexts, setLangTexts] = useState([]); - const [editingTexts, setEditingTexts] = useState([]); - const [selectedCompany, setSelectedCompany] = useState("all"); - const [searchText, setSearchText] = useState(""); - const [isModalOpen, setIsModalOpen] = useState(false); - const [editingKey, setEditingKey] = useState(null); - const [selectedKeys, setSelectedKeys] = useState>(new Set()); - - // 언어 관리 관련 상태 - const [isLanguageModalOpen, setIsLanguageModalOpen] = useState(false); - const [editingLanguage, setEditingLanguage] = useState(null); - const [selectedLanguages, setSelectedLanguages] = useState>(new Set()); - const [activeTab, setActiveTab] = useState<"keys" | "languages">("keys"); - - const [companies, setCompanies] = useState>([]); - - // 회사 목록 조회 - const fetchCompanies = async () => { - try { - // console.log("회사 목록 조회 시작"); - const response = await apiClient.get("/admin/companies"); - // console.log("회사 목록 응답 데이터:", response.data); - - const data = response.data; - if (data.success) { - const companyList = data.data.map((company: any) => ({ - code: company.company_code, - name: company.company_name, - })); - // console.log("변환된 회사 목록:", companyList); - setCompanies(companyList); - } else { - // console.error("회사 목록 조회 실패:", data.message); - } - } catch (error) { - // console.error("회사 목록 조회 실패:", error); - } - }; - - // 언어 목록 조회 - const fetchLanguages = async () => { - try { - const response = await apiClient.get("/multilang/languages"); - const data = response.data; - if (data.success) { - setLanguages(data.data); - } - } catch (error) { - // console.error("언어 목록 조회 실패:", error); - } - }; - - // 다국어 키 목록 조회 - const fetchLangKeys = async () => { - try { - const response = await apiClient.get("/multilang/keys"); - const data = response.data; - if (data.success) { - // console.log("✅ 전체 키 목록 로드:", data.data.length, "개"); - setLangKeys(data.data); - } else { - // console.error("❌ 키 목록 로드 실패:", data.message); - } - } catch (error) { - // console.error("다국어 키 목록 조회 실패:", error); - } - }; - - // 필터링된 데이터 계산 - 메뉴관리와 동일한 방식 - const getFilteredLangKeys = () => { - let filteredKeys = langKeys; - - // 회사 필터링 - if (selectedCompany && selectedCompany !== "all") { - filteredKeys = filteredKeys.filter((key) => key.companyCode === selectedCompany); - } - - // 텍스트 검색 필터링 - if (searchText.trim()) { - const searchLower = searchText.toLowerCase(); - filteredKeys = filteredKeys.filter((key) => { - const langKey = (key.langKey || "").toLowerCase(); - const description = (key.description || "").toLowerCase(); - const menuName = (key.menuName || "").toLowerCase(); - const companyName = companies.find((c) => c.code === key.companyCode)?.name?.toLowerCase() || ""; - - return ( - langKey.includes(searchLower) || - description.includes(searchLower) || - menuName.includes(searchLower) || - companyName.includes(searchLower) - ); - }); - } - - return filteredKeys; - }; - - // 선택된 키의 다국어 텍스트 조회 - const fetchLangTexts = async (keyId: number) => { - try { - // console.log("다국어 텍스트 조회 시작: keyId =", keyId); - const response = await apiClient.get(`/multilang/keys/${keyId}/texts`); - const data = response.data; - // console.log("다국어 텍스트 조회 응답:", data); - if (data.success) { - setLangTexts(data.data); - // 편집용 텍스트 초기화 - const editingData = data.data.map((text: LangText) => ({ ...text })); - setEditingTexts(editingData); - // console.log("편집용 텍스트 설정:", editingData); - } - } catch (error) { - // console.error("다국어 텍스트 조회 실패:", error); - } - }; - - // 언어 키 선택 처리 - const handleKeySelect = (key: LangKey) => { - // console.log("언어 키 선택:", key); - setSelectedKey(key); - fetchLangTexts(key.keyId); - }; - - // 디버깅용 useEffect - useEffect(() => { - if (selectedKey) { - // console.log("선택된 키 변경:", selectedKey); - // console.log("언어 목록:", languages); - // console.log("편집 텍스트:", editingTexts); - } - }, [selectedKey, languages, editingTexts]); - - // 텍스트 변경 처리 - const handleTextChange = (langCode: string, value: string) => { - const newEditingTexts = [...editingTexts]; - const existingIndex = newEditingTexts.findIndex((t) => t.langCode === langCode); - - if (existingIndex >= 0) { - newEditingTexts[existingIndex].langText = value; - } else { - newEditingTexts.push({ - textId: 0, - keyId: selectedKey!.keyId, - langCode: langCode, - langText: value, - isActive: "Y", - }); - } - - setEditingTexts(newEditingTexts); - }; - - // 텍스트 저장 - const handleSave = async () => { - if (!selectedKey) return; - - try { - // 백엔드가 기대하는 형식으로 데이터 변환 - const requestData = { - texts: editingTexts.map((text) => ({ - langCode: text.langCode, - langText: text.langText, - isActive: text.isActive || "Y", - createdBy: user?.userId || "system", - updatedBy: user?.userId || "system", - })), - }; - - const response = await apiClient.post(`/multilang/keys/${selectedKey.keyId}/texts`, requestData); - const data = response.data; - if (data.success) { - alert("저장되었습니다."); - // 저장 후 다시 조회 - fetchLangTexts(selectedKey.keyId); - } - } catch (error) { - // console.error("텍스트 저장 실패:", error); - alert("저장에 실패했습니다."); - } - }; - - // 언어 키 추가/수정 모달 열기 - const handleAddKey = () => { - setEditingKey(null); // 새 키 추가는 null로 설정 - setIsModalOpen(true); - }; - - // 언어 추가/수정 모달 열기 - const handleAddLanguage = () => { - setEditingLanguage(null); - setIsLanguageModalOpen(true); - }; - - // 언어 수정 - const handleEditLanguage = (language: Language) => { - setEditingLanguage(language); - setIsLanguageModalOpen(true); - }; - - // 언어 저장 (추가/수정) - const handleSaveLanguage = async (languageData: any) => { - try { - const requestData = { - ...languageData, - createdBy: user?.userId || "admin", - updatedBy: user?.userId || "admin", - }; - - let response; - if (editingLanguage) { - response = await apiClient.put(`/multilang/languages/${editingLanguage.langCode}`, requestData); - } else { - response = await apiClient.post("/multilang/languages", requestData); - } - - const result = response.data; - - if (result.success) { - alert(editingLanguage ? "언어가 수정되었습니다." : "언어가 추가되었습니다."); - setIsLanguageModalOpen(false); - fetchLanguages(); // 언어 목록 새로고침 - } else { - alert(`오류: ${result.message}`); - } - } catch (error) { - // console.error("언어 저장 중 오류:", error); - alert("언어 저장 중 오류가 발생했습니다."); - } - }; - - // 언어 삭제 - const handleDeleteLanguages = async () => { - if (selectedLanguages.size === 0) { - alert("삭제할 언어를 선택해주세요."); - return; - } - - if ( - !confirm( - `선택된 ${selectedLanguages.size}개의 언어를 영구적으로 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.`, - ) - ) { - return; - } - - try { - const deletePromises = Array.from(selectedLanguages).map((langCode) => - apiClient.delete(`/multilang/languages/${langCode}`), - ); - - const responses = await Promise.all(deletePromises); - const failedDeletes = responses.filter((response) => !response.data.success); - - if (failedDeletes.length === 0) { - alert("선택된 언어가 삭제되었습니다."); - setSelectedLanguages(new Set()); - fetchLanguages(); // 언어 목록 새로고침 - } else { - alert(`${failedDeletes.length}개의 언어 삭제에 실패했습니다.`); - } - } catch (error) { - // console.error("언어 삭제 중 오류:", error); - alert("언어 삭제 중 오류가 발생했습니다."); - } - }; - - // 언어 선택 체크박스 처리 - const handleLanguageCheckboxChange = (langCode: string, checked: boolean) => { - const newSelected = new Set(selectedLanguages); - if (checked) { - newSelected.add(langCode); - } else { - newSelected.delete(langCode); - } - setSelectedLanguages(newSelected); - }; - - // 언어 전체 선택/해제 - const handleSelectAllLanguages = (checked: boolean) => { - if (checked) { - setSelectedLanguages(new Set(languages.map((lang) => lang.langCode))); - } else { - setSelectedLanguages(new Set()); - } - }; - - // 언어 키 수정 모달 열기 - const handleEditKey = (key: LangKey) => { - setEditingKey(key); - setIsModalOpen(true); - }; - - // 언어 키 저장 (추가/수정) - const handleSaveKey = async (keyData: any) => { - try { - const requestData = { - ...keyData, - createdBy: user?.userId || "admin", - updatedBy: user?.userId || "admin", - }; - - let response; - if (editingKey) { - response = await apiClient.put(`/multilang/keys/${editingKey.keyId}`, requestData); - } else { - response = await apiClient.post("/multilang/keys", requestData); - } - - const data = response.data; - - if (data.success) { - alert(editingKey ? "언어 키가 수정되었습니다." : "언어 키가 추가되었습니다."); - fetchLangKeys(); // 목록 새로고침 - setIsModalOpen(false); - } else { - // 중복 체크 오류 메시지 처리 - if (data.message && data.message.includes("이미 존재하는 언어키")) { - alert(data.message); - } else { - alert(data.message || "언어 키 저장에 실패했습니다."); - } - } - } catch (error) { - // console.error("언어 키 저장 실패:", error); - alert("언어 키 저장에 실패했습니다."); - } - }; - - // 체크박스 선택/해제 - const handleCheckboxChange = (keyId: number, checked: boolean) => { - const newSelectedKeys = new Set(selectedKeys); - if (checked) { - newSelectedKeys.add(keyId); - } else { - newSelectedKeys.delete(keyId); - } - setSelectedKeys(newSelectedKeys); - }; - - // 키 상태 토글 - const handleToggleStatus = async (keyId: number) => { - try { - const response = await apiClient.put(`/multilang/keys/${keyId}/toggle`); - const data = response.data; - if (data.success) { - alert(`키가 ${data.data}되었습니다.`); - fetchLangKeys(); - } else { - alert("상태 변경 중 오류가 발생했습니다."); - } - } catch (error) { - // console.error("키 상태 토글 실패:", error); - alert("키 상태 변경 중 오류가 발생했습니다."); - } - }; - - // 언어 상태 토글 - const handleToggleLanguageStatus = async (langCode: string) => { - try { - const response = await apiClient.put(`/multilang/languages/${langCode}/toggle`); - const data = response.data; - if (data.success) { - alert(`언어가 ${data.data}되었습니다.`); - fetchLanguages(); - } else { - alert("언어 상태 변경 중 오류가 발생했습니다."); - } - } catch (error) { - // console.error("언어 상태 토글 실패:", error); - alert("언어 상태 변경 중 오류가 발생했습니다."); - } - }; - - // 전체 선택/해제 - const handleSelectAll = (checked: boolean) => { - if (checked) { - const allKeyIds = getFilteredLangKeys().map((key) => key.keyId); - setSelectedKeys(new Set(allKeyIds)); - } else { - setSelectedKeys(new Set()); - } - }; - - // 선택된 키들 일괄 삭제 - const handleDeleteSelectedKeys = async () => { - if (selectedKeys.size === 0) { - alert("삭제할 키를 선택해주세요."); - return; - } - - if ( - !confirm( - `선택된 ${selectedKeys.size}개의 언어 키를 영구적으로 삭제하시겠습니까?\n\n⚠️ 이 작업은 되돌릴 수 없습니다.`, - ) - ) { - return; - } - - try { - const deletePromises = Array.from(selectedKeys).map((keyId) => apiClient.delete(`/multilang/keys/${keyId}`)); - - const responses = await Promise.all(deletePromises); - const allSuccess = responses.every((response) => response.data.success); - - if (allSuccess) { - alert(`${selectedKeys.size}개의 언어 키가 영구적으로 삭제되었습니다.`); - setSelectedKeys(new Set()); - fetchLangKeys(); // 목록 새로고침 - - // 선택된 키가 삭제된 경우 편집 영역 닫기 - if (selectedKey && selectedKeys.has(selectedKey.keyId)) { - handleCancel(); - } - } else { - alert("일부 키 삭제에 실패했습니다."); - } - } catch (error) { - // console.error("선택된 키 삭제 실패:", error); - alert("선택된 키 삭제에 실패했습니다."); - } - }; - - // 개별 키 삭제 (기존 함수 유지) - const handleDeleteKey = async (keyId: number) => { - if (!confirm("정말로 이 언어 키를 영구적으로 삭제하시겠습니까?\n\n⚠️ 이 작업은 되돌릴 수 없습니다.")) { - return; - } - - try { - const response = await apiClient.delete(`/multilang/keys/${keyId}`); - const data = response.data; - if (data.success) { - alert("언어 키가 영구적으로 삭제되었습니다."); - fetchLangKeys(); // 목록 새로고침 - if (selectedKey && selectedKey.keyId === keyId) { - handleCancel(); // 선택된 키가 삭제된 경우 편집 영역 닫기 - } - } - } catch (error) { - // console.error("언어 키 삭제 실패:", error); - alert("언어 키 삭제에 실패했습니다."); - } - }; - - // 취소 처리 - const handleCancel = () => { - setSelectedKey(null); - setLangTexts([]); - setEditingTexts([]); - }; - - useEffect(() => { - const initializeData = async () => { - setLoading(true); - await Promise.all([fetchCompanies(), fetchLanguages(), fetchLangKeys()]); - setLoading(false); - }; - initializeData(); - }, []); - - // 검색 관련 useEffect 제거 - 실시간 필터링만 사용 - - const columns = [ - { - id: "select", - header: () => { - const filteredKeys = getFilteredLangKeys(); - return ( - 0} - onChange={(e) => handleSelectAll(e.target.checked)} - className="h-4 w-4" - /> - ); - }, - cell: ({ row }: any) => ( - handleCheckboxChange(row.original.keyId, e.target.checked)} - onClick={(e) => e.stopPropagation()} - className="h-4 w-4" - disabled={row.original.isActive === "N"} - /> - ), - }, - { - accessorKey: "companyCode", - header: "회사", - cell: ({ row }: any) => { - const companyName = - row.original.companyCode === "*" - ? "공통" - : companies.find((c) => c.code === row.original.companyCode)?.name || row.original.companyCode; - - return {companyName}; - }, - }, - { - accessorKey: "menuName", - header: "메뉴명", - cell: ({ row }: any) => ( - {row.original.menuName} - ), - }, - - { - accessorKey: "langKey", - header: "언어 키", - cell: ({ row }: any) => ( -
handleEditKey(row.original)} - > - {row.original.langKey} -
- ), - }, - - { - accessorKey: "description", - header: "설명", - cell: ({ row }: any) => ( - {row.original.description} - ), - }, - { - accessorKey: "isActive", - header: "상태", - cell: ({ row }: any) => ( - - ), - }, - ]; - - // 언어 테이블 컬럼 정의 - const languageColumns = [ - { - id: "select", - header: () => ( - 0} - onChange={(e) => handleSelectAllLanguages(e.target.checked)} - className="h-4 w-4" - /> - ), - cell: ({ row }: any) => ( - handleLanguageCheckboxChange(row.original.langCode, e.target.checked)} - onClick={(e) => e.stopPropagation()} - className="h-4 w-4" - disabled={row.original.isActive === "N"} - /> - ), - }, - { - accessorKey: "langCode", - header: "언어 코드", - cell: ({ row }: any) => ( -
handleEditLanguage(row.original)} - > - {row.original.langCode} -
- ), - }, - { - accessorKey: "langName", - header: "언어명 (영문)", - cell: ({ row }: any) => ( - {row.original.langName} - ), - }, - { - accessorKey: "langNative", - header: "언어명 (원어)", - cell: ({ row }: any) => ( - {row.original.langNative} - ), - }, - { - accessorKey: "isActive", - header: "상태", - cell: ({ row }: any) => ( - - ), - }, - ]; - - if (loading) { - return ; - } - - return ( -
- {/* 탭 네비게이션 */} -
- - -
- - {/* 메인 콘텐츠 영역 */} -
- {/* 언어 관리 탭 */} - {activeTab === "languages" && ( - - - 언어 관리 - - -
-
총 {languages.length}개의 언어가 등록되어 있습니다.
-
- {selectedLanguages.size > 0 && ( - - )} - -
-
- -
-
- )} - - {/* 다국어 키 관리 탭의 메인 영역 */} - {activeTab === "keys" && ( -
- {/* 좌측: 언어 키 목록 (7/10) */} - - -
- 언어 키 목록 -
- - -
-
-
- - {/* 검색 필터 영역 */} -
-
- - -
- -
- - setSearchText(e.target.value)} - /> -
- -
-
검색 결과: {getFilteredLangKeys().length}건
-
-
- - {/* 테이블 영역 */} -
-
전체: {getFilteredLangKeys().length}건
- -
-
-
- - {/* 우측: 선택된 키의 다국어 관리 (3/10) */} - - - - {selectedKey ? ( - <> - 선택된 키:{" "} - - {selectedKey.companyCode}.{selectedKey.menuName}.{selectedKey.langKey} - - - ) : ( - "다국어 편집" - )} - - - - {selectedKey ? ( -
- {/* 스크롤 가능한 텍스트 영역 */} -
- {languages - .filter((lang) => lang.isActive === "Y") - .map((lang) => { - const text = editingTexts.find((t) => t.langCode === lang.langCode); - return ( -
- - {lang.langName} - - handleTextChange(lang.langCode, e.target.value)} - className="flex-1" - /> -
- ); - })} -
- {/* 저장 버튼 - 고정 위치 */} -
- - -
-
- ) : ( -
-
-
언어 키를 선택하세요
-
좌측 목록에서 편집할 언어 키를 클릭하세요
-
-
- )} -
-
-
- )} -
- - {/* 언어 키 추가/수정 모달 */} - setIsModalOpen(false)} - onSave={handleSaveKey} - keyData={editingKey} - companies={companies} - /> - - {/* 언어 추가/수정 모달 */} - setIsLanguageModalOpen(false)} - onSave={handleSaveLanguage} - languageData={editingLanguage} - /> -
- ); -} diff --git a/frontend/components/admin/RoleDetailManagement.tsx b/frontend/components/admin/RoleDetailManagement.tsx deleted file mode 100644 index 92d03143..00000000 --- a/frontend/components/admin/RoleDetailManagement.tsx +++ /dev/null @@ -1,345 +0,0 @@ -"use client"; - -import React, { useState, useCallback, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { ArrowLeft, Users, Menu as MenuIcon, Save } from "lucide-react"; -import { roleAPI, RoleGroup } from "@/lib/api/role"; -import { useAuth } from "@/hooks/useAuth"; -import { useRouter } from "next/navigation"; -import { AlertCircle } from "lucide-react"; -import { DualListBox } from "@/components/common/DualListBox"; -import { MenuPermissionsTable } from "./MenuPermissionsTable"; -import { useMenu } from "@/contexts/MenuContext"; - -interface RoleDetailManagementProps { - roleId: string; -} - -/** - * 권한 그룹 상세 관리 컴포넌트 - * - * 기능: - * - 권한 그룹 정보 표시 - * - 멤버 관리 (Dual List Box) - * - 메뉴 권한 설정 (CRUD 체크박스) - */ -export function RoleDetailManagement({ roleId }: RoleDetailManagementProps) { - const { user: currentUser } = useAuth(); - const router = useRouter(); - const { refreshMenus } = useMenu(); - - const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN"; - - // 상태 관리 - const [roleGroup, setRoleGroup] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - // 탭 상태 - const [activeTab, setActiveTab] = useState<"members" | "permissions">("members"); - - // 멤버 관리 상태 - const [availableUsers, setAvailableUsers] = useState>([]); - const [selectedUsers, setSelectedUsers] = useState>([]); - const [isSavingMembers, setIsSavingMembers] = useState(false); - - // 메뉴 권한 상태 - const [menuPermissions, setMenuPermissions] = useState([]); - const [isSavingPermissions, setIsSavingPermissions] = useState(false); - - // 데이터 로드 - const loadRoleGroup = useCallback(async () => { - setIsLoading(true); - setError(null); - - try { - const response = await roleAPI.getById(parseInt(roleId, 10)); - - if (response.success && response.data) { - setRoleGroup(response.data); - } else { - setError(response.message || "권한 그룹 정보를 불러오는데 실패했습니다."); - } - } catch (err) { - console.error("권한 그룹 정보 로드 오류:", err); - setError("권한 그룹 정보를 불러오는 중 오류가 발생했습니다."); - } finally { - setIsLoading(false); - } - }, [roleId]); - - // 멤버 목록 로드 - const loadMembers = useCallback(async () => { - if (!roleGroup) return; - - try { - // 1. 권한 그룹 멤버 조회 - const membersResponse = await roleAPI.getMembers(roleGroup.objid); - if (membersResponse.success && membersResponse.data) { - setSelectedUsers( - membersResponse.data.map((member: any) => ({ - id: member.userId, - label: member.userName || member.userId, - description: member.deptName, - })), - ); - } - - // 2. 전체 사용자 목록 조회 (같은 회사) - const userAPI = await import("@/lib/api/user"); - - console.log("🔍 사용자 목록 조회 요청:", { - companyCode: roleGroup.companyCode, - size: 1000, - }); - - const usersResponse = await userAPI.userAPI.getList({ - companyCode: roleGroup.companyCode, - size: 1000, // 대량 조회 - }); - - console.log("✅ 사용자 목록 응답:", { - success: usersResponse.success, - count: usersResponse.data?.length, - total: usersResponse.total, - }); - - if (usersResponse.success && usersResponse.data) { - setAvailableUsers( - usersResponse.data.map((user: any) => ({ - id: user.userId, - label: user.userName || user.userId, - description: user.deptName, - })), - ); - console.log("📋 설정된 전체 사용자 수:", usersResponse.data.length); - } - } catch (err) { - console.error("멤버 목록 로드 오류:", err); - } - }, [roleGroup]); - - // 메뉴 권한 로드 - const loadMenuPermissions = useCallback(async () => { - if (!roleGroup) return; - - console.log("🔍 [loadMenuPermissions] 메뉴 권한 로드 시작", { - roleGroupId: roleGroup.objid, - roleGroupName: roleGroup.authName, - companyCode: roleGroup.companyCode, - }); - - try { - const response = await roleAPI.getMenuPermissions(roleGroup.objid); - - console.log("✅ [loadMenuPermissions] API 응답", { - success: response.success, - dataCount: response.data?.length, - data: response.data, - }); - - if (response.success && response.data) { - setMenuPermissions(response.data); - console.log("✅ [loadMenuPermissions] 상태 업데이트 완료", { - count: response.data.length, - }); - } else { - console.warn("⚠️ [loadMenuPermissions] 응답 실패", { - message: response.message, - }); - } - } catch (err) { - console.error("❌ [loadMenuPermissions] 메뉴 권한 로드 오류:", err); - } - }, [roleGroup]); - - useEffect(() => { - loadRoleGroup(); - }, [loadRoleGroup]); - - useEffect(() => { - if (roleGroup && activeTab === "members") { - loadMembers(); - } else if (roleGroup && activeTab === "permissions") { - loadMenuPermissions(); - } - }, [roleGroup, activeTab, loadMembers, loadMenuPermissions]); - - // 멤버 저장 핸들러 - const handleSaveMembers = useCallback(async () => { - if (!roleGroup) return; - - setIsSavingMembers(true); - try { - // 현재 선택된 사용자 ID 목록 - const selectedUserIds = selectedUsers.map((user) => user.id); - - // 멤버 업데이트 API 호출 - const response = await roleAPI.updateMembers(roleGroup.objid, selectedUserIds); - - if (response.success) { - alert("멤버가 성공적으로 저장되었습니다."); - loadMembers(); // 새로고침 - - // 사이드바 메뉴 새로고침 (현재 사용자가 영향받을 수 있음) - await refreshMenus(); - } else { - alert(response.message || "멤버 저장에 실패했습니다."); - } - } catch (err) { - console.error("멤버 저장 오류:", err); - alert("멤버 저장 중 오류가 발생했습니다."); - } finally { - setIsSavingMembers(false); - } - }, [roleGroup, selectedUsers, loadMembers, refreshMenus]); - - // 메뉴 권한 저장 핸들러 - const handleSavePermissions = useCallback(async () => { - if (!roleGroup) return; - - setIsSavingPermissions(true); - try { - const response = await roleAPI.setMenuPermissions(roleGroup.objid, menuPermissions); - - if (response.success) { - alert("메뉴 권한이 성공적으로 저장되었습니다."); - loadMenuPermissions(); // 새로고침 - - // 사이드바 메뉴 새로고침 (권한 변경 즉시 반영) - await refreshMenus(); - } else { - alert(response.message || "메뉴 권한 저장에 실패했습니다."); - } - } catch (err) { - console.error("메뉴 권한 저장 오류:", err); - alert("메뉴 권한 저장 중 오류가 발생했습니다."); - } finally { - setIsSavingPermissions(false); - } - }, [roleGroup, menuPermissions, loadMenuPermissions, refreshMenus]); - - if (isLoading) { - return ( -
-
-
-

권한 그룹 정보를 불러오는 중...

-
-
- ); - } - - if (error || !roleGroup) { - return ( -
- -

오류 발생

-

{error || "권한 그룹을 찾을 수 없습니다."}

- -
- ); - } - - return ( - <> - {/* 페이지 헤더 */} -
-
- -
-

{roleGroup.authName}

-

- {roleGroup.authCode} • {roleGroup.companyCode} -

-
- - {roleGroup.status === "active" ? "활성" : "비활성"} - -
-
- - {/* 탭 네비게이션 */} -
- - -
- - {/* 탭 컨텐츠 */} -
- {activeTab === "members" && ( - <> -
-
-

멤버 관리

-

이 권한 그룹에 속한 사용자를 관리합니다

-
- -
- - - - )} - - {activeTab === "permissions" && ( - <> -
-
-

메뉴 권한 설정

-

이 권한 그룹에서 접근 가능한 메뉴와 권한을 설정합니다

-
- -
- - - - )} -
- - ); -} diff --git a/frontend/components/admin/RoleManagement.tsx b/frontend/components/admin/RoleManagement.tsx deleted file mode 100644 index 3834b2a4..00000000 --- a/frontend/components/admin/RoleManagement.tsx +++ /dev/null @@ -1,335 +0,0 @@ -"use client"; - -import React, { useState, useCallback, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { Plus, Edit, Trash2, Users, Menu, Filter, X } from "lucide-react"; -import { roleAPI, RoleGroup } from "@/lib/api/role"; -import { useAuth } from "@/hooks/useAuth"; -import { AlertCircle } from "lucide-react"; -import { RoleFormModal } from "./RoleFormModal"; -import { RoleDeleteModal } from "./RoleDeleteModal"; -import { useRouter } from "next/navigation"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { companyAPI } from "@/lib/api/company"; - -/** - * 권한 그룹 관리 메인 컴포넌트 - * - * 기능: - * - 권한 그룹 목록 조회 (회사별) - * - 권한 그룹 생성/수정/삭제 - * - 상세 페이지로 이동 (멤버 관리 + 메뉴 권한 설정) - */ -export function RoleManagement() { - const { user: currentUser } = useAuth(); - const router = useRouter(); - - // 회사 관리자 또는 최고 관리자 여부 - const isAdmin = - (currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN") || - currentUser?.userType === "COMPANY_ADMIN"; - const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN"; - - // 상태 관리 - const [roleGroups, setRoleGroups] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - // 회사 필터 (최고 관리자 전용) - const [companies, setCompanies] = useState>([]); - const [selectedCompany, setSelectedCompany] = useState("all"); - - // 모달 상태 - const [formModal, setFormModal] = useState({ - isOpen: false, - editingRole: null as RoleGroup | null, - }); - - const [deleteModal, setDeleteModal] = useState({ - isOpen: false, - role: null as RoleGroup | null, - }); - - // 회사 목록 로드 (최고 관리자만) - const loadCompanies = useCallback(async () => { - if (!isSuperAdmin) return; - - try { - const companies = await companyAPI.getList(); - setCompanies(companies); - } catch (error) { - console.error("회사 목록 로드 오류:", error); - } - }, [isSuperAdmin]); - - // 데이터 로드 - const loadRoleGroups = useCallback(async () => { - setIsLoading(true); - setError(null); - - try { - // 최고 관리자: selectedCompany에 따라 필터링 (all이면 전체 조회) - // 회사 관리자: 자기 회사만 조회 - const companyFilter = - isSuperAdmin && selectedCompany !== "all" - ? selectedCompany - : isSuperAdmin - ? undefined - : currentUser?.companyCode; - - console.log("권한 그룹 목록 조회:", { isSuperAdmin, selectedCompany, companyFilter }); - - const response = await roleAPI.getList({ - companyCode: companyFilter, - }); - - if (response.success && response.data) { - setRoleGroups(response.data); - console.log("권한 그룹 조회 성공:", response.data.length, "개"); - } else { - setError(response.message || "권한 그룹 목록을 불러오는데 실패했습니다."); - } - } catch (err) { - console.error("권한 그룹 목록 로드 오류:", err); - setError("권한 그룹 목록을 불러오는 중 오류가 발생했습니다."); - } finally { - setIsLoading(false); - } - }, [isSuperAdmin, selectedCompany, currentUser?.companyCode]); - - useEffect(() => { - if (isAdmin) { - if (isSuperAdmin) { - loadCompanies(); // 최고 관리자는 회사 목록 먼저 로드 - } - loadRoleGroups(); - } else { - setIsLoading(false); - } - }, [isAdmin, isSuperAdmin, loadRoleGroups, loadCompanies]); - - // 권한 그룹 생성 핸들러 - const handleCreateRole = useCallback(() => { - setFormModal({ isOpen: true, editingRole: null }); - }, []); - - // 권한 그룹 수정 핸들러 - const handleEditRole = useCallback((role: RoleGroup) => { - setFormModal({ isOpen: true, editingRole: role }); - }, []); - - // 권한 그룹 삭제 핸들러 - const handleDeleteRole = useCallback((role: RoleGroup) => { - setDeleteModal({ isOpen: true, role }); - }, []); - - // 폼 모달 닫기 - const handleFormModalClose = useCallback(() => { - setFormModal({ isOpen: false, editingRole: null }); - }, []); - - // 삭제 모달 닫기 - const handleDeleteModalClose = useCallback(() => { - setDeleteModal({ isOpen: false, role: null }); - }, []); - - // 모달 성공 후 새로고침 - const handleModalSuccess = useCallback(() => { - loadRoleGroups(); - }, [loadRoleGroups]); - - // 상세 페이지로 이동 - const handleViewDetail = useCallback( - (role: RoleGroup) => { - router.push(`/admin/userMng/rolesList/${role.objid}`); - }, - [router], - ); - - // 관리자가 아니면 접근 제한 - if (!isAdmin) { - return ( -
- -

접근 권한 없음

-

- 권한 그룹 관리는 회사 관리자 이상만 접근할 수 있습니다. -

- -
- ); - } - - return ( - <> - {/* 에러 메시지 */} - {error && ( -
-
-

오류가 발생했습니다

- -
-

{error}

-
- )} - - {/* 액션 버튼 영역 */} -
-
-

권한 그룹 목록 ({roleGroups.length})

- - {/* 최고 관리자 전용: 회사 필터 */} - {isSuperAdmin && ( -
- - - {selectedCompany !== "all" && ( - - )} -
- )} -
- - -
- - {/* 권한 그룹 목록 */} - {isLoading ? ( -
-
-
-

권한 그룹 목록을 불러오는 중...

-
-
- ) : roleGroups.length === 0 ? ( -
-
-

등록된 권한 그룹이 없습니다.

-

권한 그룹을 생성하여 멤버를 관리해보세요.

-
-
- ) : ( -
- {roleGroups.map((role) => ( -
- {/* 헤더 (클릭 시 상세 페이지) */} -
handleViewDetail(role)} - > -
-
-

{role.authName}

-

{role.authCode}

-
- - {role.status === "active" ? "활성" : "비활성"} - -
- - {/* 정보 */} -
- {/* 최고 관리자는 회사명 표시 */} - {isSuperAdmin && ( -
- 회사 - - {companies.find((c) => c.company_code === role.companyCode)?.company_name || role.companyCode} - -
- )} -
- - - 멤버 수 - - {role.memberCount || 0}명 -
-
- - - 메뉴 권한 - - {role.menuCount || 0}개 -
-
-
- - {/* 액션 버튼 */} -
- - -
-
- ))} -
- )} - - {/* 모달들 */} - - - - - ); -} diff --git a/frontend/components/admin/UserAuthManagement.tsx b/frontend/components/admin/UserAuthManagement.tsx deleted file mode 100644 index 27163ba5..00000000 --- a/frontend/components/admin/UserAuthManagement.tsx +++ /dev/null @@ -1,157 +0,0 @@ -"use client"; - -import React, { useState, useCallback, useEffect } from "react"; -import { UserAuthTable } from "./UserAuthTable"; -import { UserAuthEditModal } from "./UserAuthEditModal"; -import { userAPI } from "@/lib/api/user"; -import { useAuth } from "@/hooks/useAuth"; -import { AlertCircle } from "lucide-react"; -import { Button } from "@/components/ui/button"; - -/** - * 사용자 권한 관리 메인 컴포넌트 - * - * 기능: - * - 사용자 목록 조회 (권한 정보 포함) - * - 권한 변경 모달 - * - 최고 관리자 권한 체크 - */ -export function UserAuthManagement() { - const { user: currentUser } = useAuth(); - - // 최고 관리자 여부 - const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN"; - - // 상태 관리 - const [users, setUsers] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [paginationInfo, setPaginationInfo] = useState({ - currentPage: 1, - pageSize: 20, - totalItems: 0, - totalPages: 0, - }); - - // 권한 변경 모달 - const [authEditModal, setAuthEditModal] = useState({ - isOpen: false, - user: null as any | null, - }); - - // 데이터 로드 - const loadUsers = useCallback( - async (page: number = 1) => { - setIsLoading(true); - setError(null); - - try { - const response = await userAPI.getList({ - page, - size: paginationInfo.pageSize, - }); - - if (response.success && response.data) { - setUsers(response.data); - setPaginationInfo({ - currentPage: response.currentPage || page, - pageSize: response.pageSize || paginationInfo.pageSize, - totalItems: response.total || 0, - totalPages: Math.ceil((response.total || 0) / (response.pageSize || paginationInfo.pageSize)), - }); - } else { - setError(response.message || "사용자 목록을 불러오는데 실패했습니다."); - } - } catch (err) { - console.error("사용자 목록 로드 오류:", err); - setError("사용자 목록을 불러오는 중 오류가 발생했습니다."); - } finally { - setIsLoading(false); - } - }, - [paginationInfo.pageSize], - ); - - useEffect(() => { - loadUsers(1); - }, []); - - // 권한 변경 핸들러 - const handleEditAuth = (user: any) => { - setAuthEditModal({ - isOpen: true, - user, - }); - }; - - // 권한 변경 모달 닫기 - const handleAuthEditClose = () => { - setAuthEditModal({ - isOpen: false, - user: null, - }); - }; - - // 권한 변경 성공 - const handleAuthEditSuccess = () => { - loadUsers(paginationInfo.currentPage); - handleAuthEditClose(); - }; - - // 페이지 변경 - const handlePageChange = (page: number) => { - loadUsers(page); - }; - - // 최고 관리자가 아닌 경우 - if (!isSuperAdmin) { - return ( -
- -

접근 권한 없음

-

권한 관리는 최고 관리자만 접근할 수 있습니다.

- -
- ); - } - - return ( -
- {/* 에러 메시지 */} - {error && ( -
-
-

오류가 발생했습니다

- -
-

{error}

-
- )} - - {/* 사용자 권한 테이블 */} - - - {/* 권한 변경 모달 */} - -
- ); -} diff --git a/frontend/components/admin/UserManagement.tsx b/frontend/components/admin/UserManagement.tsx deleted file mode 100644 index 987b986e..00000000 --- a/frontend/components/admin/UserManagement.tsx +++ /dev/null @@ -1,176 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useUserManagement } from "@/hooks/useUserManagement"; -import { UserToolbar } from "./UserToolbar"; -import { UserTable } from "./UserTable"; -import { Pagination } from "@/components/common/Pagination"; -import { UserPasswordResetModal } from "./UserPasswordResetModal"; -import { UserFormModal } from "./UserFormModal"; - -/** - * 사용자 관리 메인 컴포넌트 - * - 원본 Spring + JSP 코드 패턴 기반 REST API 연동 - * - 실제 데이터베이스와 연동되어 작동 - */ -export function UserManagement() { - const { - // 데이터 - users, - searchFilter, - isLoading, - isSearching, - error, - paginationInfo, - - // 검색 기능 - updateSearchFilter, - - // 페이지네이션 - handlePageChange, - handlePageSizeChange, - - // 액션 핸들러 - handleStatusToggle, - - // 유틸리티 - clearError, - refreshData, - } = useUserManagement(); - - // 비밀번호 초기화 모달 상태 - const [passwordResetModal, setPasswordResetModal] = useState({ - isOpen: false, - userId: null as string | null, - userName: null as string | null, - }); - - // 사용자 등록/수정 모달 상태 - const [userFormModal, setUserFormModal] = useState({ - isOpen: false, - editingUser: null as any | null, - }); - - // 사용자 등록 핸들러 - const handleCreateUser = () => { - setUserFormModal({ - isOpen: true, - editingUser: null, - }); - }; - - // 사용자 수정 핸들러 - const handleEditUser = (user: any) => { - setUserFormModal({ - isOpen: true, - editingUser: user, - }); - }; - - // 사용자 등록/수정 모달 닫기 - const handleUserFormClose = () => { - setUserFormModal({ - isOpen: false, - editingUser: null, - }); - }; - - // 사용자 등록/수정 성공 핸들러 - const handleUserFormSuccess = () => { - refreshData(); // 목록 새로고침 - handleUserFormClose(); - }; - - // 비밀번호 초기화 핸들러 - const handlePasswordReset = (userId: string, userName: string) => { - setPasswordResetModal({ - isOpen: true, - userId, - userName, - }); - }; - - // 비밀번호 초기화 모달 닫기 - const handlePasswordResetClose = () => { - setPasswordResetModal({ - isOpen: false, - userId: null, - userName: null, - }); - }; - - // 비밀번호 초기화 성공 핸들러 - const handlePasswordResetSuccess = () => { - // refreshData(); // 비밀번호 변경은 목록에 영향을 주지 않으므로 새로고침 불필요 - handlePasswordResetClose(); - }; - - return ( -
- {/* 툴바 - 검색, 필터, 등록 버튼 */} - - - {/* 에러 메시지 */} - {error && ( -
-
-

오류가 발생했습니다

- -
-

{error}

-
- )} - - {/* 사용자 목록 테이블 */} - - - {/* 페이지네이션 */} - {!isLoading && users.length > 0 && ( - - )} - - {/* 사용자 등록/수정 모달 */} - - - {/* 비밀번호 초기화 모달 */} - -
- ); -} diff --git a/frontend/components/admin/department/DepartmentManagement.tsx b/frontend/components/admin/department/DepartmentManagement.tsx deleted file mode 100644 index 4939e24e..00000000 --- a/frontend/components/admin/department/DepartmentManagement.tsx +++ /dev/null @@ -1,117 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { ArrowLeft } from "lucide-react"; -import { DepartmentStructure } from "./DepartmentStructure"; -import { DepartmentMembers } from "./DepartmentMembers"; -import type { Department } from "@/types/department"; -import { getCompanyList } from "@/lib/api/company"; - -interface DepartmentManagementProps { - companyCode: string; -} - -/** - * 부서 관리 메인 컴포넌트 - * 좌측: 부서 구조, 우측: 부서 인원 - */ -export function DepartmentManagement({ companyCode }: DepartmentManagementProps) { - const router = useRouter(); - const [selectedDepartment, setSelectedDepartment] = useState(null); - const [activeTab, setActiveTab] = useState("structure"); - const [companyName, setCompanyName] = useState(""); - const [refreshTrigger, setRefreshTrigger] = useState(0); - - // 부서원 변경 시 부서 구조 새로고침 - const handleMemberChange = () => { - setRefreshTrigger((prev) => prev + 1); - }; - - // 회사 정보 로드 - useEffect(() => { - const loadCompanyInfo = async () => { - const response = await getCompanyList(); - if (response.success && response.data) { - const company = response.data.find((c) => c.company_code === companyCode); - if (company) { - setCompanyName(company.company_name); - } - } - }; - loadCompanyInfo(); - }, [companyCode]); - - const handleBackToList = () => { - router.push("/admin/userMng/companyList"); - }; - - return ( -
- {/* 상단 헤더: 회사 정보 + 뒤로가기 */} -
-
- -
-
-

{companyName || companyCode}

-

부서 관리

-
-
-
- {/* 탭 네비게이션 (모바일용) */} -
- - - 부서 구조 - 부서 인원 - - - - - - - - - - -
- - {/* 좌우 레이아웃 (데스크톱) */} -
- {/* 좌측: 부서 구조 (20%) */} -
- -
- - {/* 우측: 부서 인원 (80%) */} -
- -
-
-
- ); -} diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index 449a9c49..b28e4d01 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -17,12 +17,14 @@ import { UserCheck, LogOut, User, + Building2, } 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 { menuScreenApi } from "@/lib/api/screen"; +import { apiClient } from "@/lib/api/client"; import { toast } from "sonner"; import { ProfileModal } from "./ProfileModal"; import { Logo } from "./Logo"; @@ -35,6 +37,14 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { CompanySwitcher } from "@/components/admin/CompanySwitcher"; // useAuth의 UserInfo 타입을 확장 interface ExtendedUserInfo { @@ -206,11 +216,38 @@ function AppLayoutInner({ children }: AppLayoutProps) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); - const { user, logout, refreshUserData } = useAuth(); + const { user, logout, refreshUserData, switchCompany } = useAuth(); const { userMenus, adminMenus, loading, refreshMenus } = useMenu(); const [sidebarOpen, setSidebarOpen] = useState(true); const [expandedMenus, setExpandedMenus] = useState>(new Set()); const [isMobile, setIsMobile] = useState(false); + const [showCompanySwitcher, setShowCompanySwitcher] = useState(false); + const [currentCompanyName, setCurrentCompanyName] = useState(""); + + // 현재 회사명 조회 (SUPER_ADMIN 전용) + useEffect(() => { + const fetchCurrentCompanyName = async () => { + if ((user as ExtendedUserInfo)?.userType === "SUPER_ADMIN") { + const companyCode = (user as ExtendedUserInfo)?.companyCode; + + if (companyCode === "*") { + setCurrentCompanyName("WACE (최고 관리자)"); + } else if (companyCode) { + try { + const response = await apiClient.get("/admin/companies/db"); + if (response.data.success) { + const company = response.data.data.find((c: any) => c.company_code === companyCode); + setCurrentCompanyName(company?.company_name || companyCode); + } + } catch (error) { + setCurrentCompanyName(companyCode); + } + } + } + }; + + fetchCurrentCompanyName(); + }, [(user as ExtendedUserInfo)?.companyCode, (user as ExtendedUserInfo)?.userType]); // 화면 크기 감지 및 사이드바 초기 상태 설정 useEffect(() => { @@ -333,11 +370,32 @@ function AppLayoutInner({ children }: AppLayoutProps) { }; // 모드 전환 핸들러 - const handleModeSwitch = () => { + const handleModeSwitch = async () => { if (isAdminMode) { + // 관리자 → 사용자 모드: 선택한 회사 유지 router.push("/main"); } else { - router.push("/admin"); + // 사용자 → 관리자 모드: WACE로 복귀 필요 (SUPER_ADMIN만) + if ((user as ExtendedUserInfo)?.userType === "SUPER_ADMIN") { + const currentCompanyCode = (user as ExtendedUserInfo)?.companyCode; + + // 이미 WACE("*")가 아니면 WACE로 전환 후 관리자 페이지로 이동 + if (currentCompanyCode !== "*") { + const result = await switchCompany("*"); + if (result.success) { + // 페이지 새로고침 (관리자 페이지로 이동) + window.location.href = "/admin"; + } else { + toast.error("WACE로 전환 실패"); + } + } else { + // 이미 WACE면 바로 관리자 페이지로 이동 + router.push("/admin"); + } + } else { + // 일반 관리자는 바로 관리자 페이지로 이동 + router.push("/admin"); + } } }; @@ -498,11 +556,27 @@ function AppLayoutInner({ children }: AppLayoutProps) {
)} + {/* WACE 관리자: 현재 관리 회사 표시 */} + {(user as ExtendedUserInfo)?.userType === "SUPER_ADMIN" && ( +
+
+ +
+

현재 관리 회사

+

+ {currentCompanyName || "로딩 중..."} +

+
+
+
+ )} + {/* Admin/User 모드 전환 버튼 (관리자만) */} {((user as ExtendedUserInfo)?.userType === "SUPER_ADMIN" || (user as ExtendedUserInfo)?.userType === "COMPANY_ADMIN" || (user as ExtendedUserInfo)?.userType === "admin") && ( -
+
+ {/* 관리자/사용자 메뉴 전환 */} + + {/* WACE 관리자 전용: 회사 선택 버튼 */} + {(user as ExtendedUserInfo)?.userType === "SUPER_ADMIN" && ( + + )}
)} @@ -653,6 +738,21 @@ function AppLayoutInner({ children }: AppLayoutProps) { onSave={saveProfile} onAlertClose={closeAlert} /> + + {/* 회사 전환 모달 (WACE 관리자 전용) */} + + + + 회사 선택 + + 관리할 회사를 선택하면 해당 회사의 관점에서 시스템을 사용할 수 있습니다. + + +
+ setShowCompanySwitcher(false)} isOpen={showCompanySwitcher} /> +
+
+
); } diff --git a/frontend/contexts/MenuContext.tsx b/frontend/contexts/MenuContext.tsx index 1ced8546..88b15542 100644 --- a/frontend/contexts/MenuContext.tsx +++ b/frontend/contexts/MenuContext.tsx @@ -4,6 +4,7 @@ import React, { createContext, useContext, useState, useEffect, ReactNode } from import type { MenuItem } from "@/lib/api/menu"; import { menuApi } from "@/lib/api/menu"; // API 호출 활성화 import { toast } from "sonner"; +import { useAuth } from "@/hooks/useAuth"; // user 정보 가져오기 interface MenuContextType { adminMenus: MenuItem[]; @@ -18,6 +19,7 @@ export function MenuProvider({ children }: { children: ReactNode }) { const [adminMenus, setAdminMenus] = useState([]); const [userMenus, setUserMenus] = useState([]); const [loading, setLoading] = useState(true); + const { user } = useAuth(); // user 정보 가져오기 const convertMenuData = (data: any[]): MenuItem[] => { return data.map((item) => ({ @@ -96,8 +98,10 @@ export function MenuProvider({ children }: { children: ReactNode }) { }; useEffect(() => { + // user.companyCode가 변경되면 메뉴 다시 로드 + // console.log("🔄 MenuContext: user.companyCode 변경 감지, 메뉴 재로드", user?.companyCode); loadMenus(); - }, []); // 초기 로드만 + }, [user?.companyCode]); // companyCode 변경 시 재로드 return ( {children} diff --git a/frontend/hooks/useAuth.ts b/frontend/hooks/useAuth.ts index 854a9196..29752559 100644 --- a/frontend/hooks/useAuth.ts +++ b/frontend/hooks/useAuth.ts @@ -331,6 +331,61 @@ export const useAuth = () => { [apiCall, refreshUserData], ); + /** + * 회사 전환 처리 (WACE 관리자 전용) + */ + const switchCompany = useCallback( + async (companyCode: string): Promise<{ success: boolean; message: string }> => { + try { + // console.log("🔵 useAuth.switchCompany 시작:", companyCode); + setLoading(true); + setError(null); + + // console.log("🔵 API 호출: POST /auth/switch-company"); + const response = await apiCall("POST", "/auth/switch-company", { + companyCode, + }); + // console.log("🔵 API 응답:", response); + + if (response.success && response.data?.token) { + // console.log("🔵 새 토큰 받음:", response.data.token.substring(0, 20) + "..."); + + // 새로운 JWT 토큰 저장 + TokenManager.setToken(response.data.token); + // console.log("🔵 토큰 저장 완료"); + + // refreshUserData 호출하지 않고 바로 성공 반환 + // (페이지 새로고침 시 자동으로 갱신됨) + // console.log("🔵 회사 전환 완료 (페이지 새로고침 필요)"); + + return { + success: true, + message: response.message || "회사 전환에 성공했습니다.", + }; + } else { + // console.error("🔵 API 응답 실패:", response); + return { + success: false, + message: response.message || "회사 전환에 실패했습니다.", + }; + } + } catch (error: any) { + // console.error("🔵 switchCompany 에러:", error); + const errorMessage = error.message || "회사 전환 중 오류가 발생했습니다."; + setError(errorMessage); + + return { + success: false, + message: errorMessage, + }; + } finally { + setLoading(false); + // console.log("🔵 switchCompany 완료"); + } + }, + [apiCall] + ); + /** * 로그아웃 처리 */ @@ -493,6 +548,7 @@ export const useAuth = () => { // 함수 login, logout, + switchCompany, // 🆕 회사 전환 함수 checkMenuAuth, refreshUserData, diff --git a/frontend/lib/api/menu.ts b/frontend/lib/api/menu.ts index 82ab39ac..67de76ae 100644 --- a/frontend/lib/api/menu.ts +++ b/frontend/lib/api/menu.ts @@ -85,9 +85,9 @@ export const menuApi = { return response.data; }, - // 사용자 메뉴 목록 조회 (좌측 사이드바용 - active만 표시) + // 사용자 메뉴 목록 조회 (좌측 사이드바용 - active만 표시, 회사별 필터링) getUserMenus: async (): Promise> => { - const response = await apiClient.get("/admin/menus", { params: { menuType: "1" } }); + const response = await apiClient.get("/admin/user-menus"); return response.data; },