fix(WEB): 권한관리 상세 페이지 버그 수정

- types.ts: PermissionMatrix 인터페이스 수정
  - API 응답 구조에 맞게 menus → permissions 객체로 변경
- PermissionDetailClient.tsx:
  - getMenuPermission 함수 수정 (matrix.permissions[menuId] 사용)
  - 숨김 스위치 토글 시 자동 저장 기능 추가
- actions.ts: API 연동 함수 개선
This commit is contained in:
2025-12-30 20:46:06 +09:00
parent f8dbc6b2ae
commit 2a14ae72ff
3 changed files with 349 additions and 43 deletions

View File

@@ -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 ? '숨김 (사용자에게 표시 안함)' : '공개'}

View File

@@ -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 : '권한 초기화 실패' };

View File

@@ -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;
};
};
}
// 메뉴별 권한 상태