c123fd01ff
화면 (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>
391 lines
11 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
},
|
|
};
|