diff --git a/src/components/settings/PermissionManagement/PermissionDetailClient.tsx b/src/components/settings/PermissionManagement/PermissionDetailClient.tsx index 71476422..7bf37e81 100644 --- a/src/components/settings/PermissionManagement/PermissionDetailClient.tsx +++ b/src/components/settings/PermissionManagement/PermissionDetailClient.tsx @@ -55,6 +55,62 @@ import { resetPermissions, } from './actions'; +// 플랫 배열을 트리 구조로 변환 +interface FlatMenuItem { + id: number; + name: string; + code: string; + parent_id: number | null; + depth: number; + sort_order: number; + has_children?: boolean; +} + +function buildMenuTree(flatMenus: FlatMenuItem[]): MenuTreeItem[] { + const menuMap = new Map(); + const roots: MenuTreeItem[] = []; + + // 1. 모든 메뉴를 맵에 저장 + flatMenus.forEach(menu => { + menuMap.set(menu.id, { + id: menu.id, + name: menu.name, + code: menu.code, + parent_id: menu.parent_id, + depth: menu.depth, + sort_order: menu.sort_order, + children: [], + }); + }); + + // 2. 부모-자식 관계 설정 + flatMenus.forEach(menu => { + const treeItem = menuMap.get(menu.id)!; + if (menu.parent_id === null) { + roots.push(treeItem); + } else { + const parent = menuMap.get(menu.parent_id); + if (parent) { + parent.children = parent.children || []; + parent.children.push(treeItem); + } + } + }); + + // 3. 정렬 (sort_order 기준) + const sortMenus = (menus: MenuTreeItem[]) => { + menus.sort((a, b) => a.sort_order - b.sort_order); + menus.forEach(menu => { + if (menu.children && menu.children.length > 0) { + sortMenus(menu.children); + } + }); + }; + sortMenus(roots); + + return roots; +} + interface PermissionDetailClientProps { permissionId: string; isNew?: boolean; @@ -101,9 +157,17 @@ export function PermissionDetailClient({ permissionId, isNew = false }: Permissi try { // 메뉴 트리 로드 const menusResult = await fetchPermissionMenus(); + console.log('[PermissionDetail] menusResult:', menusResult); if (menusResult.success && menusResult.data) { - setMenuTree(menusResult.data.menus); + console.log('[PermissionDetail] menus (flat):', menusResult.data.menus); + // 플랫 배열을 트리 구조로 변환 + const treeMenus = buildMenuTree(menusResult.data.menus as FlatMenuItem[]); + console.log('[PermissionDetail] menus (tree):', treeMenus); + setMenuTree(treeMenus); setPermissionTypes(menusResult.data.permission_types); + } else { + console.error('[PermissionDetail] Failed to load menus:', menusResult.error); + toast.error(menusResult.error || '메뉴 트리 로드 실패'); } if (!isNew) { @@ -329,9 +393,9 @@ export function PermissionDetailClient({ permissionId, isNew = false }: Permissi // 메뉴의 권한 상태 가져오기 const getMenuPermission = (menuId: number, permType: string): boolean => { - if (!matrix) return false; - const menuPerm = matrix.menus.find(m => m.menu_id === menuId); - return menuPerm?.permissions[permType as PermissionType] || false; + if (!matrix || !matrix.permissions) return false; + const menuPerm = matrix.permissions[menuId]; + return menuPerm?.[permType as PermissionType] || false; }; // 메뉴 행 렌더링 (재귀) @@ -493,7 +557,27 @@ export function PermissionDetailClient({ permissionId, isNew = false }: Permissi { + setIsHidden(checked); + // 기존 역할인 경우 즉시 저장 + if (role) { + try { + const result = await updateRole(role.id, { is_hidden: checked }); + if (result.success && result.data) { + setRole(result.data); + toast.success(checked ? '역할이 숨김 처리되었습니다.' : '역할이 공개되었습니다.'); + } else { + // 실패 시 롤백 + setIsHidden(!checked); + toast.error(result.error || '숨김 설정 변경 실패'); + } + } catch (error) { + setIsHidden(!checked); + toast.error('숨김 설정 변경 중 오류 발생'); + } + } + }} + disabled={isSaving} /> {isHidden ? '숨김 (사용자에게 표시 안함)' : '공개'} diff --git a/src/components/settings/PermissionManagement/actions.ts b/src/components/settings/PermissionManagement/actions.ts index 7eb3a925..9beec7bb 100644 --- a/src/components/settings/PermissionManagement/actions.ts +++ b/src/components/settings/PermissionManagement/actions.ts @@ -1,9 +1,11 @@ 'use server'; import { revalidatePath } from 'next/cache'; -import { apiClient } from '@/lib/api/client'; +import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { Role, RoleStats, PermissionMatrix, MenuTreeItem, ApiResponse, PaginatedResponse } from './types'; +const API_URL = process.env.NEXT_PUBLIC_API_URL; + // ========== Role CRUD ========== /** @@ -16,8 +18,30 @@ export async function fetchRoles(params?: { is_hidden?: boolean; }): Promise>> { try { - const response = await apiClient.get>('/v1/roles', { params }); - return { success: true, data: response }; + const searchParams = new URLSearchParams(); + if (params?.page) searchParams.set('page', params.page.toString()); + if (params?.size) searchParams.set('per_page', params.size.toString()); + if (params?.q) searchParams.set('q', params.q); + if (params?.is_hidden !== undefined) searchParams.set('is_hidden', params.is_hidden.toString()); + + const url = `${API_URL}/api/v1/roles?${searchParams.toString()}`; + const { response, error } = await serverFetch(url, { method: 'GET' }); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '역할 목록 조회에 실패했습니다.' }; + } + + const result = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '역할 목록 조회 실패' }; + } + + return { success: true, data: result.data }; } catch (error) { console.error('Failed to fetch roles:', error); return { success: false, error: error instanceof Error ? error.message : '역할 목록 조회 실패' }; @@ -29,8 +53,23 @@ export async function fetchRoles(params?: { */ export async function fetchRole(id: number): Promise> { try { - const response = await apiClient.get(`/v1/roles/${id}`); - return { success: true, data: response }; + const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/${id}`, { method: 'GET' }); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '역할 조회에 실패했습니다.' }; + } + + const result = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '역할 조회 실패' }; + } + + return { success: true, data: result.data }; } catch (error) { console.error('Failed to fetch role:', error); return { success: false, error: error instanceof Error ? error.message : '역할 조회 실패' }; @@ -46,9 +85,27 @@ export async function createRole(data: { is_hidden?: boolean; }): Promise> { try { - const response = await apiClient.post('/v1/roles', data); + const { response, error } = await serverFetch(`${API_URL}/api/v1/roles`, { + method: 'POST', + body: JSON.stringify(data), + }); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '역할 생성에 실패했습니다.' }; + } + + const result = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '역할 생성 실패' }; + } + revalidatePath('/settings/permissions'); - return { success: true, data: response }; + return { success: true, data: result.data }; } catch (error) { console.error('Failed to create role:', error); return { success: false, error: error instanceof Error ? error.message : '역할 생성 실패' }; @@ -67,10 +124,28 @@ export async function updateRole( } ): Promise> { try { - const response = await apiClient.patch(`/v1/roles/${id}`, data); + const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/${id}`, { + method: 'PATCH', + body: JSON.stringify(data), + }); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '역할 수정에 실패했습니다.' }; + } + + const result = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '역할 수정 실패' }; + } + revalidatePath('/settings/permissions'); revalidatePath(`/settings/permissions/${id}`); - return { success: true, data: response }; + return { success: true, data: result.data }; } catch (error) { console.error('Failed to update role:', error); return { success: false, error: error instanceof Error ? error.message : '역할 수정 실패' }; @@ -82,7 +157,24 @@ export async function updateRole( */ export async function deleteRole(id: number): Promise> { try { - await apiClient.delete(`/v1/roles/${id}`); + const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/${id}`, { + method: 'DELETE', + }); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '역할 삭제에 실패했습니다.' }; + } + + const result = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '역할 삭제 실패' }; + } + revalidatePath('/settings/permissions'); return { success: true }; } catch (error) { @@ -96,8 +188,23 @@ export async function deleteRole(id: number): Promise> { */ export async function fetchRoleStats(): Promise> { try { - const response = await apiClient.get('/v1/roles/stats'); - return { success: true, data: response }; + const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/stats`, { method: 'GET' }); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '역할 통계 조회에 실패했습니다.' }; + } + + const result = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '역할 통계 조회 실패' }; + } + + return { success: true, data: result.data }; } catch (error) { console.error('Failed to fetch role stats:', error); return { success: false, error: error instanceof Error ? error.message : '역할 통계 조회 실패' }; @@ -109,8 +216,23 @@ export async function fetchRoleStats(): Promise> { */ export async function fetchActiveRoles(): Promise> { try { - const response = await apiClient.get('/v1/roles/active'); - return { success: true, data: response }; + const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/active`, { method: 'GET' }); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '활성 역할 목록 조회에 실패했습니다.' }; + } + + const result = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '활성 역할 목록 조회 실패' }; + } + + return { success: true, data: result.data }; } catch (error) { console.error('Failed to fetch active roles:', error); return { success: false, error: error instanceof Error ? error.message : '활성 역할 목록 조회 실패' }; @@ -127,11 +249,23 @@ export async function fetchPermissionMenus(): Promise> { try { - const response = await apiClient.get<{ - menus: MenuTreeItem[]; - permission_types: string[]; - }>('/v1/role-permissions/menus'); - return { success: true, data: response }; + const { response, error } = await serverFetch(`${API_URL}/api/v1/role-permissions/menus`, { method: 'GET' }); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '메뉴 트리 조회에 실패했습니다.' }; + } + + const result = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '메뉴 트리 조회 실패' }; + } + + return { success: true, data: result.data }; } catch (error) { console.error('Failed to fetch permission menus:', error); return { success: false, error: error instanceof Error ? error.message : '메뉴 트리 조회 실패' }; @@ -143,8 +277,23 @@ export async function fetchPermissionMenus(): Promise> { try { - const response = await apiClient.get(`/v1/roles/${roleId}/permissions/matrix`); - return { success: true, data: response }; + const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/${roleId}/permissions/matrix`, { method: 'GET' }); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '권한 매트릭스 조회에 실패했습니다.' }; + } + + const result = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '권한 매트릭스 조회 실패' }; + } + + return { success: true, data: result.data }; } catch (error) { console.error('Failed to fetch permission matrix:', error); return { success: false, error: error instanceof Error ? error.message : '권한 매트릭스 조회 실패' }; @@ -163,15 +312,30 @@ export async function togglePermission( propagated_to: number[]; }>> { try { - const response = await apiClient.post<{ - granted: boolean; - propagated_to: number[]; - }>(`/v1/roles/${roleId}/permissions/toggle`, { - menu_id: menuId, - permission_type: permissionType, + const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/${roleId}/permissions/toggle`, { + method: 'POST', + body: JSON.stringify({ + menu_id: menuId, + permission_type: permissionType, + }), }); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '권한 토글에 실패했습니다.' }; + } + + const result = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '권한 토글 실패' }; + } + revalidatePath(`/settings/permissions/${roleId}`); - return { success: true, data: response }; + return { success: true, data: result.data }; } catch (error) { console.error('Failed to toggle permission:', error); return { success: false, error: error instanceof Error ? error.message : '권한 토글 실패' }; @@ -183,9 +347,26 @@ export async function togglePermission( */ export async function allowAllPermissions(roleId: number): Promise> { try { - const response = await apiClient.post<{ count: number }>(`/v1/roles/${roleId}/permissions/allow-all`); + const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/${roleId}/permissions/allow-all`, { + method: 'POST', + }); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '전체 허용에 실패했습니다.' }; + } + + const result = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '전체 허용 실패' }; + } + revalidatePath(`/settings/permissions/${roleId}`); - return { success: true, data: response }; + return { success: true, data: result.data }; } catch (error) { console.error('Failed to allow all permissions:', error); return { success: false, error: error instanceof Error ? error.message : '전체 허용 실패' }; @@ -197,9 +378,26 @@ export async function allowAllPermissions(roleId: number): Promise> { try { - const response = await apiClient.post<{ count: number }>(`/v1/roles/${roleId}/permissions/deny-all`); + const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/${roleId}/permissions/deny-all`, { + method: 'POST', + }); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '전체 거부에 실패했습니다.' }; + } + + const result = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '전체 거부 실패' }; + } + revalidatePath(`/settings/permissions/${roleId}`); - return { success: true, data: response }; + return { success: true, data: result.data }; } catch (error) { console.error('Failed to deny all permissions:', error); return { success: false, error: error instanceof Error ? error.message : '전체 거부 실패' }; @@ -211,9 +409,26 @@ export async function denyAllPermissions(roleId: number): Promise> { try { - const response = await apiClient.post<{ count: number }>(`/v1/roles/${roleId}/permissions/reset`); + const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/${roleId}/permissions/reset`, { + method: 'POST', + }); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '권한 초기화에 실패했습니다.' }; + } + + const result = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '권한 초기화 실패' }; + } + revalidatePath(`/settings/permissions/${roleId}`); - return { success: true, data: response }; + return { success: true, data: result.data }; } catch (error) { console.error('Failed to reset permissions:', error); return { success: false, error: error instanceof Error ? error.message : '권한 초기화 실패' }; diff --git a/src/components/settings/PermissionManagement/types.ts b/src/components/settings/PermissionManagement/types.ts index 2d2bd103..25f55e95 100644 --- a/src/components/settings/PermissionManagement/types.ts +++ b/src/components/settings/PermissionManagement/types.ts @@ -56,12 +56,19 @@ export interface MenuTreeItem { children?: MenuTreeItem[]; } -// 권한 매트릭스 응답 +// 권한 매트릭스 응답 (API 응답 구조에 맞춤) export interface PermissionMatrix { - role_id: number; - role_name: string; - menus: MenuPermissionItem[]; + role: { + id: number; + name: string; + description?: string; + }; permission_types: PermissionType[]; + permissions: { + [menuId: number]: { + [key in PermissionType]?: boolean; + }; + }; } // 메뉴별 권한 상태