Files
wace_rps/frontend/lib/api/role.ts
T
hjjeong c123fd01ff 권한관리 화면 4분할 레이아웃 안정화 + 메뉴 권한 토글 race condition 가드 + 토큰 무효화 본인 제외
화면 (frontend/app/(main)/admin/userMng/userAuthList/page.tsx):
- 4분할 그리드를 inline flex 레이아웃으로 강제 (Tailwind v4 arbitrary value 파싱 이슈 우회)
- 권한있는/없는 직원 리스트 height 폭주 수정 (flex-1 + height 충돌 → 명시적 flex-basis)
- 직원 항목을 가로 배치(이름·부서)로 변경해 한 화면 표시 인원 증가
- 메뉴 트리 영역 자체 스크롤 + sticky thead 적용, 페이지 전체 스크롤 제거
- 빠른 연속 클릭 시 같은 메뉴-필드 토글이 백엔드 SELECT→UPDATE race로 충돌하던 문제를 프론트 inFlight 가드로 차단

API/인프라:
- 단건 메뉴 권한 토글 엔드포인트 신설: PATCH /roles/:id/menu-permissions/:menuId
- RoleService.upsertSingleMenuPermission 추가 (변경된 yn 필드만 머지하는 멱등 UPSERT)
- frontend/lib/api/role.ts: toggleMenuPermission · getWorkspace 함수 추가 (워크스페이스 통합 로드)

보안:
- 권한 변경 시 그룹 멤버 토큰 무효화 로직에서 작업자 본인을 제외 (자기 자신 로그아웃 방지)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:59:20 +09:00

391 lines
11 KiB
TypeScript

import { apiClient } from "./client";
import { ApiResponse } from "@/types/api";
/**
* 권한 그룹 인터페이스
*/
export interface RoleGroup {
objid: number;
authName: string;
authCode: string;
companyCode: string;
status: string;
writer: string;
regdate: string;
memberCount?: number;
menuCount?: number;
memberNames?: string;
}
/**
* 권한 그룹 멤버 인터페이스
*/
export interface RoleMember {
objid: number;
masterObjid: number;
userId: string;
userName: string;
deptName?: string;
positionName?: string;
writer: string;
regdate: string;
}
/**
* 메뉴 권한 인터페이스
*/
export interface MenuPermission {
objid: number;
menuObjid: number;
authObjid: number;
menuName?: string;
createYn: string;
readYn: string;
updateYn: string;
deleteYn: string;
writer: string;
regdate: string;
}
/**
* 권한 그룹 API
*/
export const roleAPI = {
/**
* 권한 그룹 목록 조회
*/
async getList(params?: { companyCode?: string; search?: string }): Promise<ApiResponse<RoleGroup[]>> {
try {
const response = await apiClient.get("/roles", { params });
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "권한 그룹 목록 조회 실패",
error: error.response?.data?.error || error.message,
};
}
},
/**
* 권한 그룹 상세 조회
*/
async getById(id: number): Promise<ApiResponse<RoleGroup>> {
try {
const response = await apiClient.get(`/roles/${id}`);
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "권한 그룹 상세 조회 실패",
error: error.response?.data?.error || error.message,
};
}
},
/**
* 권한 그룹 생성
*/
async create(data: { authName: string; authCode: string; companyCode: string }): Promise<ApiResponse<RoleGroup>> {
try {
const response = await apiClient.post("/roles", data);
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "권한 그룹 생성 실패",
error: error.response?.data?.error || error.message,
};
}
},
/**
* 권한 그룹 수정
*/
async update(
id: number,
data: {
authName?: string;
authCode?: string;
status?: string;
},
): Promise<ApiResponse<RoleGroup>> {
try {
const response = await apiClient.put(`/roles/${id}`, data);
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "권한 그룹 수정 실패",
error: error.response?.data?.error || error.message,
};
}
},
/**
* 권한 그룹 삭제
*/
async delete(id: number): Promise<ApiResponse<null>> {
try {
const response = await apiClient.delete(`/roles/${id}`);
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "권한 그룹 삭제 실패",
error: error.response?.data?.error || error.message,
};
}
},
/**
* 권한 그룹 멤버 목록 조회
*/
async getMembers(roleId: number): Promise<ApiResponse<RoleMember[]>> {
try {
const response = await apiClient.get(`/roles/${roleId}/members`);
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "멤버 목록 조회 실패",
error: error.response?.data?.error || error.message,
};
}
},
/**
* 권한 그룹 멤버 추가 (여러 명)
*/
async addMembers(roleId: number, userIds: string[]): Promise<ApiResponse<null>> {
try {
const response = await apiClient.post(`/roles/${roleId}/members`, { userIds });
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "멤버 추가 실패",
error: error.response?.data?.error || error.message,
};
}
},
/**
* 권한 그룹 멤버 제거 (여러 명)
*/
async removeMembers(roleId: number, userIds: string[]): Promise<ApiResponse<null>> {
try {
const response = await apiClient.delete(`/roles/${roleId}/members`, { data: { userIds } });
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "멤버 제거 실패",
error: error.response?.data?.error || error.message,
};
}
},
/**
* 권한 그룹 멤버 일괄 업데이트 (기존 멤버 전체 교체)
*/
async updateMembers(roleId: number, userIds: string[]): Promise<ApiResponse<null>> {
try {
const response = await apiClient.put(`/roles/${roleId}/members`, { userIds });
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "멤버 업데이트 실패",
error: error.response?.data?.error || error.message,
};
}
},
/**
* 전체 메뉴 목록 조회 (권한 설정용)
*/
async getAllMenus(companyCode?: string): Promise<ApiResponse<any[]>> {
try {
console.log("🔍 [roleAPI.getAllMenus] API 호출", { companyCode });
const url = companyCode ? `/roles/menus/all?companyCode=${companyCode}` : "/roles/menus/all";
const response = await apiClient.get(url);
console.log("✅ [roleAPI.getAllMenus] API 응답", {
success: response.data.success,
count: response.data.data?.length,
});
return response.data;
} catch (error: any) {
console.error("❌ [roleAPI.getAllMenus] API 에러", error);
return {
success: false,
message: error.response?.data?.message || "전체 메뉴 목록 조회 실패",
error: error.response?.data?.error || error.message,
};
}
},
/**
* 메뉴 권한 목록 조회
*/
async getMenuPermissions(roleId: number): Promise<ApiResponse<MenuPermission[]>> {
try {
const response = await apiClient.get(`/roles/${roleId}/menu-permissions`);
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "메뉴 권한 조회 실패",
error: error.response?.data?.error || error.message,
};
}
},
/**
* 메뉴 권한 설정
*/
async setMenuPermissions(
roleId: number,
permissions: Array<{
menuObjid: number;
createYn: string;
readYn: string;
updateYn: string;
deleteYn: string;
}>,
): Promise<ApiResponse<null>> {
try {
const response = await apiClient.put(`/roles/${roleId}/menu-permissions`, { permissions });
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "메뉴 권한 설정 실패",
error: error.response?.data?.error || error.message,
};
}
},
/**
* 권한 그룹 단일 멤버 추가 (체크박스 즉시 반영용 얇은 래퍼)
*/
async addSingleMember(roleId: number | string, userId: string): Promise<ApiResponse<null>> {
return this.addMembers(Number(roleId), [userId]);
},
/**
* 권한 그룹 단일 멤버 제거 (체크박스 즉시 반영용 얇은 래퍼)
*/
async removeSingleMember(roleId: number | string, userId: string): Promise<ApiResponse<null>> {
return this.removeMembers(Number(roleId), [userId]);
},
/**
* 메뉴 권한 단건 토글 (PATCH /roles/:id/menu-permissions/:menuId)
* - changes: 부분 업데이트 (createYn/readYn/updateYn/deleteYn 중 일부)
* - 응답: 서버가 머지한 최종 권한 row
*/
async toggleMenuPermission(
roleId: number | string,
menuId: number | string,
changes: Partial<{
createYn: string;
readYn: string;
updateYn: string;
deleteYn: string;
}>,
): Promise<ApiResponse<{ create_yn: string; read_yn: string; update_yn: string; delete_yn: string }>> {
try {
const response = await apiClient.patch(`/roles/${roleId}/menu-permissions/${menuId}`, changes);
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "메뉴 권한 토글 실패",
error: error.response?.data?.error || error.message,
};
}
},
/**
* 권한 관리 통합 화면용 워크스페이스 데이터를 한 번에 로드
* - members: 권한 그룹 멤버
* - nonMembers: 회사 사용자 - members
* - menus: 권한 설정용 전체 메뉴
* - permissions: 현재 권한 그룹의 메뉴 권한 row들
*/
async getWorkspace(
roleId: number | string,
companyCode: string,
): Promise<
ApiResponse<{
members: Array<{ userId: string; userName?: string; deptName?: string }>;
nonMembers: Array<{ userId: string; userName?: string; deptName?: string }>;
menus: any[];
permissions: any[];
}>
> {
try {
const { userAPI } = await import("@/lib/api/user");
const [membersRes, allUsersRes, menusRes, permsRes] = await Promise.all([
this.getMembers(Number(roleId)),
userAPI.getList({ companyCode, size: 1000 }),
this.getAllMenus(companyCode),
this.getMenuPermissions(Number(roleId)),
]);
const members = (membersRes.success && membersRes.data ? membersRes.data : []).map((m: any) => ({
userId: m.userId,
userName: m.userName,
deptName: m.deptName,
}));
const memberIdSet = new Set(members.map((m) => m.userId));
const allUsers: any[] = (allUsersRes as any)?.success
? (allUsersRes as any).data || []
: Array.isArray(allUsersRes)
? (allUsersRes as any)
: [];
const nonMembers = allUsers
.filter((u: any) => !memberIdSet.has(u.userId))
.map((u: any) => ({ userId: u.userId, userName: u.userName, deptName: u.deptName }));
return {
success: true,
data: {
members,
nonMembers,
menus: menusRes.success && menusRes.data ? menusRes.data : [],
permissions: permsRes.success && permsRes.data ? permsRes.data : [],
},
};
} catch (error: any) {
return {
success: false,
message: error?.message || "워크스페이스 로드 실패",
error: error?.message,
};
}
},
/**
* 사용자가 속한 권한 그룹 목록 조회
*/
async getUserRoleGroups(userId?: string): Promise<ApiResponse<RoleGroup[]>> {
try {
const url = userId ? `/roles/user/${userId}/groups` : "/roles/user/my-groups";
const response = await apiClient.get(url);
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "사용자 권한 그룹 조회 실패",
error: error.response?.data?.error || error.message,
};
}
},
};