diff --git a/backend-node/src/controllers/adminController.ts b/backend-node/src/controllers/adminController.ts index 4af01653..b1638403 100644 --- a/backend-node/src/controllers/adminController.ts +++ b/backend-node/src/controllers/adminController.ts @@ -25,19 +25,22 @@ export async function getAdminMenus( const userType = req.user?.userType; const userLang = (req.query.userLang as string) || "ko"; const menuType = req.query.menuType as string | undefined; // menuType 파라미터 추가 + const includeInactive = req.query.includeInactive === "true"; // includeInactive 파라미터 추가 logger.info(`사용자 ID: ${userId}`); logger.info(`사용자 회사 코드: ${userCompanyCode}`); logger.info(`사용자 유형: ${userType}`); logger.info(`사용자 로케일: ${userLang}`); logger.info(`메뉴 타입: ${menuType || "전체"}`); + logger.info(`비활성 메뉴 포함: ${includeInactive}`); const paramMap = { userId, userCompanyCode, userType, userLang, - menuType, // menuType 추가 + menuType, // includeInactive와 관계없이 menuType 유지 (관리자/사용자 구분) + includeInactive, // includeInactive 추가 }; const menuList = await AdminService.getAdminMenuList(paramMap); @@ -1081,9 +1084,41 @@ export async function saveMenu( return; } + const userCompanyCode = req.user.companyCode; + const userType = req.user.userType; + let requestCompanyCode = menuData.companyCode || menuData.company_code; + + // "none"이나 빈 값은 undefined로 처리하여 사용자 회사 코드 사용 + if (requestCompanyCode === "none" || requestCompanyCode === "" || !requestCompanyCode) { + requestCompanyCode = undefined; + } + + // 공통 메뉴(company_code = '*')는 최고 관리자만 생성 가능 + if (requestCompanyCode === "*") { + if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") { + res.status(403).json({ + success: false, + message: "공통 메뉴는 최고 관리자만 생성할 수 있습니다.", + error: "Unauthorized to create common menu", + }); + return; + } + } else if (userCompanyCode !== "*") { + // 회사 관리자는 자기 회사 메뉴만 생성 가능 + // requestCompanyCode가 undefined면 사용자 회사 코드 사용 (권한 체크 통과) + if (requestCompanyCode && requestCompanyCode !== userCompanyCode) { + res.status(403).json({ + success: false, + message: "해당 회사의 메뉴를 생성할 권한이 없습니다.", + error: "Unauthorized to create menu for this company", + }); + return; + } + } + // Raw Query를 사용한 메뉴 저장 const objid = Date.now(); // 고유 ID 생성 - const companyCode = req.user.companyCode; + const companyCode = requestCompanyCode || userCompanyCode; const [savedMenu] = await query( `INSERT INTO menu_info ( @@ -1164,7 +1199,73 @@ export async function updateMenu( return; } - const companyCode = req.user.companyCode; + const userCompanyCode = req.user.companyCode; + const userType = req.user.userType; + + // 수정하려는 메뉴 조회 + const currentMenu = await queryOne( + `SELECT objid, company_code FROM menu_info WHERE objid = $1`, + [Number(menuId)] + ); + + if (!currentMenu) { + res.status(404).json({ + success: false, + message: `메뉴를 찾을 수 없습니다: ${menuId}`, + error: "Menu not found", + }); + return; + } + + // 공통 메뉴(company_code = '*')는 최고 관리자만 수정 가능 + if (currentMenu.company_code === "*") { + if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") { + res.status(403).json({ + success: false, + message: "공통 메뉴는 최고 관리자만 수정할 수 있습니다.", + error: "Unauthorized to update common menu", + }); + return; + } + } else if (userCompanyCode !== "*") { + // 회사 관리자는 자기 회사 메뉴만 수정 가능 + if (currentMenu.company_code !== userCompanyCode) { + res.status(403).json({ + success: false, + message: "해당 회사의 메뉴를 수정할 권한이 없습니다.", + error: "Unauthorized to update menu for this company", + }); + return; + } + } + + const requestCompanyCode = menuData.companyCode || menuData.company_code || currentMenu.company_code; + + // company_code 변경 시도하는 경우 권한 체크 + if (requestCompanyCode !== currentMenu.company_code) { + // 공통 메뉴로 변경하려는 경우 최고 관리자만 가능 + if (requestCompanyCode === "*") { + if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") { + res.status(403).json({ + success: false, + message: "공통 메뉴로 변경할 권한이 없습니다.", + error: "Unauthorized to change to common menu", + }); + return; + } + } + // 회사 관리자는 자기 회사로만 변경 가능 + else if (userCompanyCode !== "*" && requestCompanyCode !== userCompanyCode) { + res.status(403).json({ + success: false, + message: "해당 회사로 변경할 권한이 없습니다.", + error: "Unauthorized to change company", + }); + return; + } + } + + const companyCode = requestCompanyCode; // Raw Query를 사용한 메뉴 수정 const [updatedMenu] = await query( @@ -1239,6 +1340,56 @@ export async function deleteMenu( const { menuId } = req.params; logger.info(`메뉴 삭제 요청: menuId = ${menuId}`, { user: req.user }); + // 사용자의 company_code 확인 + if (!req.user?.companyCode) { + res.status(400).json({ + success: false, + message: "사용자의 회사 코드를 찾을 수 없습니다.", + error: "Missing company_code", + }); + return; + } + + const userCompanyCode = req.user.companyCode; + const userType = req.user.userType; + + // 삭제하려는 메뉴 조회 + const currentMenu = await queryOne( + `SELECT objid, company_code FROM menu_info WHERE objid = $1`, + [Number(menuId)] + ); + + if (!currentMenu) { + res.status(404).json({ + success: false, + message: `메뉴를 찾을 수 없습니다: ${menuId}`, + error: "Menu not found", + }); + return; + } + + // 공통 메뉴(company_code = '*')는 최고 관리자만 삭제 가능 + if (currentMenu.company_code === "*") { + if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") { + res.status(403).json({ + success: false, + message: "공통 메뉴는 최고 관리자만 삭제할 수 있습니다.", + error: "Unauthorized to delete common menu", + }); + return; + } + } else if (userCompanyCode !== "*") { + // 회사 관리자는 자기 회사 메뉴만 삭제 가능 + if (currentMenu.company_code !== userCompanyCode) { + res.status(403).json({ + success: false, + message: "해당 회사의 메뉴를 삭제할 권한이 없습니다.", + error: "Unauthorized to delete menu for this company", + }); + return; + } + } + // Raw Query를 사용한 메뉴 삭제 const [deletedMenu] = await query( `DELETE FROM menu_info WHERE objid = $1 RETURNING *`, @@ -1292,6 +1443,51 @@ export async function deleteMenusBatch( return; } + // 사용자의 company_code 확인 + if (!req.user?.companyCode) { + res.status(400).json({ + success: false, + message: "사용자의 회사 코드를 찾을 수 없습니다.", + error: "Missing company_code", + }); + return; + } + + const userCompanyCode = req.user.companyCode; + const userType = req.user.userType; + + // 삭제하려는 메뉴들의 company_code 확인 + const menusToDelete = await query( + `SELECT objid, company_code FROM menu_info WHERE objid = ANY($1::bigint[])`, + [menuIds.map((id) => Number(id))] + ); + + // 권한 체크: 공통 메뉴 포함 여부 확인 + const hasCommonMenu = menusToDelete.some((menu: any) => menu.company_code === "*"); + if (hasCommonMenu && (userCompanyCode !== "*" || userType !== "SUPER_ADMIN")) { + res.status(403).json({ + success: false, + message: "공통 메뉴는 최고 관리자만 삭제할 수 있습니다.", + error: "Unauthorized to delete common menu", + }); + return; + } + + // 회사 관리자는 자기 회사 메뉴만 삭제 가능 + if (userCompanyCode !== "*") { + const unauthorizedMenus = menusToDelete.filter( + (menu: any) => menu.company_code !== userCompanyCode && menu.company_code !== "*" + ); + if (unauthorizedMenus.length > 0) { + res.status(403).json({ + success: false, + message: "다른 회사의 메뉴를 삭제할 권한이 없습니다.", + error: "Unauthorized to delete menus for other companies", + }); + return; + } + } + // Raw Query를 사용한 메뉴 일괄 삭제 let deletedCount = 0; let failedCount = 0; @@ -1354,6 +1550,103 @@ export async function deleteMenusBatch( } } +/** + * 메뉴 활성/비활성 토글 + */ +export async function toggleMenuStatus( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { menuId } = req.params; + logger.info(`메뉴 상태 토글 요청: menuId = ${menuId}`, { user: req.user }); + + // 사용자의 company_code 확인 + if (!req.user?.companyCode) { + res.status(400).json({ + success: false, + message: "사용자의 회사 코드를 찾을 수 없습니다.", + error: "Missing company_code", + }); + return; + } + + const userCompanyCode = req.user.companyCode; + const userType = req.user.userType; + + // 현재 상태 및 회사 코드 조회 + const currentMenu = await queryOne( + `SELECT objid, status, company_code FROM menu_info WHERE objid = $1`, + [Number(menuId)] + ); + + if (!currentMenu) { + res.status(404).json({ + success: false, + message: `메뉴를 찾을 수 없습니다: ${menuId}`, + error: "Menu not found", + }); + return; + } + + // 공통 메뉴(company_code = '*')는 최고 관리자만 상태 변경 가능 + if (currentMenu.company_code === "*") { + if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") { + res.status(403).json({ + success: false, + message: "공통 메뉴는 최고 관리자만 상태를 변경할 수 있습니다.", + error: "Unauthorized to toggle common menu status", + }); + return; + } + } else if (userCompanyCode !== "*") { + // 회사 관리자는 자기 회사 메뉴만 상태 변경 가능 + if (currentMenu.company_code !== userCompanyCode) { + res.status(403).json({ + success: false, + message: "해당 회사의 메뉴 상태를 변경할 권한이 없습니다.", + error: "Unauthorized to toggle menu status for this company", + }); + return; + } + } + + // 상태 토글 (active <-> inactive) + const currentStatus = currentMenu.status; + const newStatus = currentStatus === "active" ? "inactive" : "active"; + + // 상태 업데이트 + const [updatedMenu] = await query( + `UPDATE menu_info SET status = $1 WHERE objid = $2 RETURNING *`, + [newStatus, Number(menuId)] + ); + + logger.info("메뉴 상태 토글 성공", { + menuId, + oldStatus: currentStatus, + newStatus, + }); + + const result = newStatus === "active" ? "활성화" : "비활성화"; + + const response: ApiResponse = { + success: true, + message: `메뉴가 ${result}되었습니다.`, + data: result, + }; + + res.status(200).json(response); + } catch (error) { + logger.error("메뉴 상태 토글 실패:", error); + res.status(500).json({ + success: false, + message: "메뉴 상태 변경에 실패하였습니다.", + error: error instanceof Error ? error.message : "Unknown error", + errorCode: "MENU_TOGGLE_ERROR", + }); + } +} + /** * 회사 목록 조회 (실제 데이터베이스에서) */ diff --git a/backend-node/src/routes/adminRoutes.ts b/backend-node/src/routes/adminRoutes.ts index c6ae0bfc..ccca89b0 100644 --- a/backend-node/src/routes/adminRoutes.ts +++ b/backend-node/src/routes/adminRoutes.ts @@ -7,6 +7,7 @@ import { updateMenu, // 메뉴 수정 deleteMenu, // 메뉴 삭제 deleteMenusBatch, // 메뉴 일괄 삭제 + toggleMenuStatus, // 메뉴 상태 토글 getUserList, getUserInfo, // 사용자 상세 조회 getUserHistory, // 사용자 변경이력 조회 @@ -37,6 +38,7 @@ router.get("/user-menus", getUserMenus); router.get("/menus/:menuId", getMenuInfo); router.post("/menus", saveMenu); // 메뉴 추가 router.put("/menus/:menuId", updateMenu); // 메뉴 수정 +router.put("/menus/:menuId/toggle", toggleMenuStatus); // 메뉴 상태 토글 router.delete("/menus/batch", deleteMenusBatch); // 메뉴 일괄 삭제 (순서 중요!) router.delete("/menus/:menuId", deleteMenu); // 메뉴 삭제 diff --git a/backend-node/src/services/adminService.ts b/backend-node/src/services/adminService.ts index 4f2e926c..c6ab17c6 100644 --- a/backend-node/src/services/adminService.ts +++ b/backend-node/src/services/adminService.ts @@ -19,7 +19,15 @@ export class AdminService { // menuType에 따른 WHERE 조건 생성 const menuTypeCondition = - menuType !== undefined ? `MENU_TYPE = ${parseInt(menuType)}` : "1 = 1"; + menuType !== undefined ? `MENU.MENU_TYPE = ${parseInt(menuType)}` : "1 = 1"; + + // 메뉴 관리 화면인지 좌측 사이드바인지 구분 + // includeInactive가 true거나 menuType이 undefined면 메뉴 관리 화면 + const includeInactive = paramMap.includeInactive === true; + const isManagementScreen = includeInactive || menuType === undefined; + // 메뉴 관리 화면: 모든 상태의 메뉴 표시, 좌측 사이드바: active만 표시 + const statusCondition = isManagementScreen ? "1 = 1" : "MENU.STATUS = 'active'"; + const subStatusCondition = isManagementScreen ? "1 = 1" : "MENU_SUB.STATUS = 'active'"; // 1. 권한 그룹 기반 필터링 (좌측 사이드바인 경우만) let authFilter = ""; @@ -27,8 +35,8 @@ export class AdminService { let queryParams: any[] = [userLang]; let paramIndex = 2; - if (menuType !== undefined && userType !== "SUPER_ADMIN") { - // 좌측 사이드바 + SUPER_ADMIN이 아닌 경우 + if (menuType !== undefined && userType !== "SUPER_ADMIN" && !isManagementScreen) { + // 좌측 사이드바 + SUPER_ADMIN이 아닌 경우만 권한 체크 const userRoleGroups = await query( ` SELECT DISTINCT am.objid AS role_objid, am.auth_name @@ -123,7 +131,7 @@ export class AdminService { return []; } } - } else if (menuType !== undefined && userType === "SUPER_ADMIN") { + } else if (menuType !== undefined && userType === "SUPER_ADMIN" && !isManagementScreen) { // 좌측 사이드바 + SUPER_ADMIN: 권한 그룹 체크 없이 모든 공통 메뉴 표시 logger.info(`✅ 최고 관리자는 권한 그룹 체크 없이 모든 공통 메뉴 표시`); // unionFilter는 비워둠 (하위 메뉴도 공통 메뉴만) @@ -136,7 +144,7 @@ export class AdminService { // SUPER_ADMIN과 COMPANY_ADMIN 구분 if (userType === "SUPER_ADMIN" && userCompanyCode === "*") { // SUPER_ADMIN - if (menuType === undefined) { + if (isManagementScreen) { // 메뉴 관리 화면: 모든 메뉴 logger.info("✅ 메뉴 관리 화면 (SUPER_ADMIN): 모든 메뉴 표시"); companyFilter = ""; @@ -145,16 +153,34 @@ export class AdminService { logger.info("✅ 좌측 사이드바 (SUPER_ADMIN): 공통 메뉴만 표시"); companyFilter = `AND MENU.COMPANY_CODE = '*'`; } - } else if (menuType === undefined) { - // 메뉴 관리 화면: 자기 회사 + 공통 메뉴 + } else if (isManagementScreen) { + // 메뉴 관리 화면: 회사별 필터링 + if (userType === "SUPER_ADMIN" && userCompanyCode === "*") { + // 최고 관리자: 모든 메뉴 (공통 + 모든 회사) + logger.info("✅ 메뉴 관리 화면 (SUPER_ADMIN): 모든 메뉴 표시"); + companyFilter = ""; + } else { + // 회사 관리자: 자기 회사 메뉴만 (공통 메뉴 제외) + logger.info( + `✅ 메뉴 관리 화면 (${userType}): 회사 ${userCompanyCode} 메뉴만 표시 (공통 메뉴 제외)` + ); + companyFilter = `AND MENU.COMPANY_CODE = $${paramIndex}`; + queryParams.push(userCompanyCode); + paramIndex++; + + // 하위 메뉴에도 회사 필터링 적용 (공통 메뉴 제외) + if (unionFilter === "") { + unionFilter = `AND MENU_SUB.COMPANY_CODE = $${paramIndex - 1}`; + } + } + } else if (menuType !== undefined) { + // 좌측 사이드바: authFilter에서 이미 회사 필터링 포함 + // 회사 관리자는 좌측 사이드바에서도 자기 회사 메뉴 조회 가능 logger.info( - `✅ 메뉴 관리 화면: 회사 ${userCompanyCode} + 공통 메뉴 표시` + `✅ 좌측 사이드바: 회사 ${userCompanyCode} 메뉴 표시 (${userType})` ); - companyFilter = `AND (MENU.COMPANY_CODE = $${paramIndex} OR MENU.COMPANY_CODE = '*')`; - queryParams.push(userCompanyCode); - paramIndex++; + // companyFilter는 authFilter에서 이미 처리됨 } - // menuType이 정의된 경우 (좌측 사이드바)는 authFilter에서 이미 회사 필터링 포함 // 기존 Java의 selectAdminMenuList 쿼리를 Raw Query로 포팅 // WITH RECURSIVE 쿼리 구현 @@ -237,7 +263,7 @@ export class AdminService { ) FROM MENU_INFO MENU WHERE ${menuTypeCondition} - AND STATUS = 'active' + AND ${statusCondition} ${companyFilter} ${authFilter} AND NOT EXISTS ( @@ -304,7 +330,7 @@ export class AdminService { FROM MENU_INFO MENU_SUB JOIN V_MENU ON MENU_SUB.PARENT_OBJ_ID = V_MENU.OBJID WHERE MENU_SUB.OBJID != ANY(V_MENU.PATH) - AND MENU_SUB.STATUS = 'active' + AND ${subStatusCondition} ${unionFilter} ) SELECT diff --git a/frontend/app/(main)/admin/external-connections/page.tsx b/frontend/app/(main)/admin/external-connections/page.tsx index 125710a2..88754ac4 100644 --- a/frontend/app/(main)/admin/external-connections/page.tsx +++ b/frontend/app/(main)/admin/external-connections/page.tsx @@ -302,20 +302,20 @@ export default function ExternalConnectionsPage() { {/* 연결 목록 */} {loading ? ( -
+
로딩 중...
) : connections.length === 0 ? ( -
+

등록된 연결이 없습니다

) : ( -
+
- + 연결명 DB 타입 호스트:포트 diff --git a/frontend/app/(main)/admin/standards/page.tsx b/frontend/app/(main)/admin/standards/page.tsx index 673d3970..8c5e7e82 100644 --- a/frontend/app/(main)/admin/standards/page.tsx +++ b/frontend/app/(main)/admin/standards/page.tsx @@ -117,7 +117,7 @@ export default function WebTypesManagePage() { return (
-
웹타입 목록을 불러오는데 실패했습니다.
+
웹타입 목록을 불러오는데 실패했습니다.
@@ -127,13 +127,13 @@ export default function WebTypesManagePage() { } return ( -
+
{/* 페이지 제목 */} -
+
-

웹타입 관리

-

화면관리에서 사용할 웹타입들을 관리합니다

+

웹타입 관리

+

화면관리에서 사용할 웹타입들을 관리합니다

- - - handleSort("sort_order")}> -
- 순서 - {sortField === "sort_order" && - (sortDirection === "asc" ? : )} -
-
- handleSort("web_type")}> -
- 웹타입 코드 - {sortField === "web_type" && - (sortDirection === "asc" ? : )} -
-
- handleSort("type_name")}> -
- 웹타입명 - {sortField === "type_name" && - (sortDirection === "asc" ? : )} -
-
- handleSort("category")}> -
- 카테고리 - {sortField === "category" && - (sortDirection === "asc" ? : )} -
-
- 설명 - handleSort("component_name")}> -
- 연결된 컴포넌트 - {sortField === "component_name" && - (sortDirection === "asc" ? : )} -
-
- handleSort("config_panel")}> -
- 설정 패널 - {sortField === "config_panel" && - (sortDirection === "asc" ? : )} -
-
- handleSort("is_active")}> -
- 상태 - {sortField === "is_active" && - (sortDirection === "asc" ? : )} -
-
- handleSort("updated_date")}> -
- 최종 수정일 - {sortField === "updated_date" && - (sortDirection === "asc" ? : )} -
-
- 작업 +
+
+ + + handleSort("sort_order")}> +
+ 순서 + {sortField === "sort_order" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("web_type")}> +
+ 웹타입 코드 + {sortField === "web_type" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("type_name")}> +
+ 웹타입명 + {sortField === "type_name" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("category")}> +
+ 카테고리 + {sortField === "category" && + (sortDirection === "asc" ? : )} +
+
+ 설명 + handleSort("component_name")}> +
+ 연결된 컴포넌트 + {sortField === "component_name" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("config_panel")}> +
+ 설정 패널 + {sortField === "config_panel" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("is_active")}> +
+ 상태 + {sortField === "is_active" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("updated_date")}> +
+ 최종 수정일 + {sortField === "updated_date" && + (sortDirection === "asc" ? : )} +
+
+ 작업
@@ -325,7 +324,7 @@ export default function WebTypesManagePage() { @@ -341,7 +340,7 @@ export default function WebTypesManagePage() { handleDelete(webType.web_type, webType.type_name)} disabled={isDeleting} - className="bg-red-600 hover:bg-red-700" + className="bg-destructive hover:bg-destructive/90" > {isDeleting ? "삭제 중..." : "삭제"} @@ -355,12 +354,11 @@ export default function WebTypesManagePage() { )}
- - +
{deleteError && ( -
-

+

+

삭제 중 오류가 발생했습니다: {deleteError instanceof Error ? deleteError.message : "알 수 없는 오류"}

diff --git a/frontend/app/(main)/admin/tableMng/page.tsx b/frontend/app/(main)/admin/tableMng/page.tsx index 0a5f205b..353e487c 100644 --- a/frontend/app/(main)/admin/tableMng/page.tsx +++ b/frontend/app/(main)/admin/tableMng/page.tsx @@ -829,7 +829,9 @@ export default function TableManagementPage() {
{!selectedTable ? ( -
+

{getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")} diff --git a/frontend/components/admin/CompanyTable.tsx b/frontend/components/admin/CompanyTable.tsx index 6ba78cc0..86ccdee3 100644 --- a/frontend/components/admin/CompanyTable.tsx +++ b/frontend/components/admin/CompanyTable.tsx @@ -129,7 +129,7 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company return ( <> {/* 데스크톱 테이블 뷰 (lg 이상) */} -

+
diff --git a/frontend/components/admin/MenuManagement.tsx b/frontend/components/admin/MenuManagement.tsx index 4e948651..0dc30248 100644 --- a/frontend/components/admin/MenuManagement.tsx +++ b/frontend/components/admin/MenuManagement.tsx @@ -37,6 +37,10 @@ export const MenuManagement: React.FC = () => { const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [selectedMenuId, setSelectedMenuId] = useState(""); const [selectedMenus, setSelectedMenus] = useState>(new Set()); + + // 메뉴 관리 화면용 로컬 상태 (모든 상태의 메뉴 표시) + const [localAdminMenus, setLocalAdminMenus] = useState([]); + const [localUserMenus, setLocalUserMenus] = useState([]); // 다국어 텍스트 훅 사용 // getMenuText는 더 이상 사용하지 않음 - getUITextSync만 사용 @@ -176,6 +180,7 @@ export const MenuManagement: React.FC = () => { // 초기 로딩 useEffect(() => { loadCompanies(); + loadMenus(false); // 메뉴 목록 로드 (메뉴 관리 화면용 - 모든 상태 표시) // 사용자 언어가 설정되지 않았을 때만 기본 텍스트 설정 if (!userLang) { initializeDefaultTexts(); @@ -373,12 +378,54 @@ export const MenuManagement: React.FC = () => { }; }, [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) { @@ -558,7 +605,7 @@ export const MenuManagement: React.FC = () => { const handleAddMenu = (parentId: string, menuType: string, level: number) => { // 상위 메뉴의 회사 정보 찾기 - const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus; + const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus; const parentMenu = currentMenus.find((menu) => menu.objid === parentId); setFormData({ @@ -575,7 +622,7 @@ export const MenuManagement: React.FC = () => { // console.log("🔧 메뉴 수정 시작 - menuId:", menuId); // 현재 메뉴 정보 찾기 - const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus; + const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus; const menuToEdit = currentMenus.find((menu) => (menu.objid || menu.OBJID) === menuId); if (menuToEdit) { @@ -614,7 +661,7 @@ export const MenuManagement: React.FC = () => { }; const handleSelectAllMenus = (checked: boolean) => { - const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus; + const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus; if (checked) { // 모든 메뉴 선택 (최상위 메뉴 포함) setSelectedMenus(new Set(currentMenus.map((menu) => menu.objid || menu.OBJID || ""))); @@ -726,7 +773,8 @@ export const MenuManagement: React.FC = () => { }; const getCurrentMenus = () => { - const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus; + // 메뉴 관리 화면용: 모든 상태의 메뉴 표시 (localAdminMenus/localUserMenus 사용) + const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus; // 검색어 필터링 let filteredMenus = currentMenus; @@ -755,6 +803,13 @@ export const MenuManagement: React.FC = () => { 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) => { @@ -777,8 +832,8 @@ export const MenuManagement: React.FC = () => { // uiTextsCount를 useMemo로 계산하여 상태 변경 시에만 재계산 const uiTextsCount = useMemo(() => Object.keys(uiTexts).length, [uiTexts]); - const adminMenusCount = useMemo(() => adminMenus?.length || 0, [adminMenus]); - const userMenusCount = useMemo(() => userMenus?.length || 0, [userMenus]); + const adminMenusCount = useMemo(() => localAdminMenus?.length || 0, [localAdminMenus]); + const userMenusCount = useMemo(() => localUserMenus?.length || 0, [localUserMenus]); // 디버깅을 위한 간단한 상태 표시 // console.log("🔍 MenuManagement 렌더링 상태:", { @@ -823,7 +878,7 @@ export const MenuManagement: React.FC = () => {

- {adminMenus.length} + {localAdminMenus.length} @@ -842,7 +897,7 @@ export const MenuManagement: React.FC = () => {

- {userMenus.length} + {localUserMenus.length} diff --git a/frontend/components/admin/MenuTable.tsx b/frontend/components/admin/MenuTable.tsx index 1eb31b8c..eb790e93 100644 --- a/frontend/components/admin/MenuTable.tsx +++ b/frontend/components/admin/MenuTable.tsx @@ -145,10 +145,10 @@ export const MenuTable: React.FC = ({ return (
{title &&

{title}

} -
+
- + -
로딩 중...
+
+
로딩 중...
) : connections.length === 0 ? ( -
+
-

등록된 REST API 연결이 없습니다

+

등록된 REST API 연결이 없습니다

) : ( -
+
- - 연결명 - 기본 URL - 인증 타입 - 헤더 수 - 상태 - 마지막 테스트 - 연결 테스트 - 작업 + + 연결명 + 기본 URL + 인증 타입 + 헤더 수 + 상태 + 마지막 테스트 + 연결 테스트 + 작업 {connections.map((connection) => ( - - + +
{connection.connection_name} @@ -291,23 +291,23 @@ export function RestApiConnectionList() { )}
- +
{connection.base_url}
- + {AUTH_TYPE_LABELS[connection.auth_type] || connection.auth_type} - + {Object.keys(connection.default_headers || {}).length} - + {connection.is_active === "Y" ? "활성" : "비활성"} - + {connection.last_test_date ? (
{new Date(connection.last_test_date).toLocaleDateString()}
@@ -322,7 +322,7 @@ export function RestApiConnectionList() { - )} - +
@@ -123,7 +123,7 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on // 빈 상태 if (users.length === 0) { return ( -
+

등록된 사용자가 없습니다.

); @@ -133,7 +133,7 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on return (
{/* 데스크톱 테이블 */} -
+
@@ -182,7 +182,7 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on return (
{/* 헤더 */}
diff --git a/frontend/components/admin/UserTable.tsx b/frontend/components/admin/UserTable.tsx index 5c1e16f5..1d6d6d46 100644 --- a/frontend/components/admin/UserTable.tsx +++ b/frontend/components/admin/UserTable.tsx @@ -186,7 +186,7 @@ export function UserTable({ return ( <> {/* 데스크톱 테이블 뷰 (lg 이상) */} -
+
diff --git a/frontend/components/dataflow/DataFlowList.tsx b/frontend/components/dataflow/DataFlowList.tsx index a9dfcdc4..fa96bb99 100644 --- a/frontend/components/dataflow/DataFlowList.tsx +++ b/frontend/components/dataflow/DataFlowList.tsx @@ -146,12 +146,6 @@ export default function DataFlowList({ onLoadFlow }: DataFlowListProps) { return (
- {/* 섹션 제목 */} -
-

플로우 목록

-

저장된 노드 플로우를 불러오거나 새로운 플로우를 생성합니다

-
- {/* 검색 및 액션 영역 */}
{/* 검색 영역 */} @@ -184,33 +178,33 @@ export default function DataFlowList({ onLoadFlow }: DataFlowListProps) { {loading ? ( <> {/* 데스크톱 테이블 스켈레톤 */} -
+
- - 플로우명 - 설명 - 생성일 - 최근 수정 - 작업 + + 플로우명 + 설명 + 생성일 + 최근 수정 + 작업 {Array.from({ length: 5 }).map((_, index) => ( - - + +
- +
- +
- +
- +
@@ -264,46 +258,46 @@ export default function DataFlowList({ onLoadFlow }: DataFlowListProps) { ) : ( <> {/* 데스크톱 테이블 뷰 (lg 이상) */} -
+
- - 플로우명 - 설명 - 생성일 - 최근 수정 - 작업 + + 플로우명 + 설명 + 생성일 + 최근 수정 + 작업 {filteredFlows.map((flow) => ( onLoadFlow(flow.flowId)} > - +
{flow.flowName}
- +
{flow.flowDescription || "설명 없음"}
- +
{new Date(flow.createdAt).toLocaleDateString()}
- +
{new Date(flow.updatedAt).toLocaleDateString()}
- e.stopPropagation()}> + e.stopPropagation()}>
diff --git a/frontend/components/ui/tabs.tsx b/frontend/components/ui/tabs.tsx index 497ba5ea..95fdc082 100644 --- a/frontend/components/ui/tabs.tsx +++ b/frontend/components/ui/tabs.tsx @@ -26,7 +26,7 @@ function TabsList({ { } export const menuApi = { - // 관리자 메뉴 목록 조회 + // 관리자 메뉴 목록 조회 (좌측 사이드바용 - active만 표시) getAdminMenus: async (): Promise> => { const response = await apiClient.get("/admin/menus", { params: { menuType: "0" } }); if (response.data.success && response.data.data && response.data.data.length > 0) { @@ -84,12 +84,24 @@ export const menuApi = { return response.data; }, - // 사용자 메뉴 목록 조회 + // 사용자 메뉴 목록 조회 (좌측 사이드바용 - active만 표시) getUserMenus: async (): Promise> => { const response = await apiClient.get("/admin/menus", { params: { menuType: "1" } }); return response.data; }, + // 관리자 메뉴 목록 조회 (메뉴 관리 화면용 - 모든 상태 표시) + getAdminMenusForManagement: async (): Promise> => { + const response = await apiClient.get("/admin/menus", { params: { menuType: "0", includeInactive: "true" } }); + return response.data; + }, + + // 사용자 메뉴 목록 조회 (메뉴 관리 화면용 - 모든 상태 표시) + getUserMenusForManagement: async (): Promise> => { + const response = await apiClient.get("/admin/menus", { params: { menuType: "1", includeInactive: "true" } }); + return response.data; + }, + // 메뉴 정보 조회 getMenuInfo: async (menuId: string): Promise> => { const response = await apiClient.get(`/admin/menus/${menuId}`);