fix(WEB): 권한관리 상세 페이지 버그 수정
- types.ts: PermissionMatrix 인터페이스 수정 - API 응답 구조에 맞게 menus → permissions 객체로 변경 - PermissionDetailClient.tsx: - getMenuPermission 함수 수정 (matrix.permissions[menuId] 사용) - 숨김 스위치 토글 시 자동 저장 기능 추가 - actions.ts: API 연동 함수 개선
This commit is contained in:
@@ -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<number, MenuTreeItem>();
|
||||
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
|
||||
<Switch
|
||||
id="role-hidden"
|
||||
checked={isHidden}
|
||||
onCheckedChange={setIsHidden}
|
||||
onCheckedChange={async (checked) => {
|
||||
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}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{isHidden ? '숨김 (사용자에게 표시 안함)' : '공개'}
|
||||
|
||||
@@ -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<ApiResponse<PaginatedResponse<Role>>> {
|
||||
try {
|
||||
const response = await apiClient.get<PaginatedResponse<Role>>('/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<ApiResponse<Role>> {
|
||||
try {
|
||||
const response = await apiClient.get<Role>(`/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<ApiResponse<Role>> {
|
||||
try {
|
||||
const response = await apiClient.post<Role>('/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<ApiResponse<Role>> {
|
||||
try {
|
||||
const response = await apiClient.patch<Role>(`/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<ApiResponse<void>> {
|
||||
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<ApiResponse<void>> {
|
||||
*/
|
||||
export async function fetchRoleStats(): Promise<ApiResponse<RoleStats>> {
|
||||
try {
|
||||
const response = await apiClient.get<RoleStats>('/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<ApiResponse<RoleStats>> {
|
||||
*/
|
||||
export async function fetchActiveRoles(): Promise<ApiResponse<Role[]>> {
|
||||
try {
|
||||
const response = await apiClient.get<Role[]>('/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<ApiResponse<{
|
||||
permission_types: string[];
|
||||
}>> {
|
||||
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<ApiResponse<{
|
||||
*/
|
||||
export async function fetchPermissionMatrix(roleId: number): Promise<ApiResponse<PermissionMatrix>> {
|
||||
try {
|
||||
const response = await apiClient.get<PermissionMatrix>(`/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<ApiResponse<{ count: number }>> {
|
||||
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<ApiResponse<{
|
||||
*/
|
||||
export async function denyAllPermissions(roleId: number): Promise<ApiResponse<{ count: number }>> {
|
||||
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<ApiResponse<{
|
||||
*/
|
||||
export async function resetPermissions(roleId: number): Promise<ApiResponse<{ count: number }>> {
|
||||
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 : '권한 초기화 실패' };
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// 메뉴별 권한 상태
|
||||
|
||||
Reference in New Issue
Block a user