Files
invyone/frontend/lib/api/role.ts
T
hjjeong cdc55dfd48 feat(cross-tenant): truncated/failed 안내 배너 (READ 트랙 마무리)
SUPER_ADMIN cross-tenant 모드에서 회사당 cap 200 에 걸리거나 한 회사
조회 실패 시 화면 상단에 안내 배너 노출. 아무 메타 없으면 자리 안 잡음.

신규
- components/common/CrossTenantBanner.tsx — amber(truncated) + red(failed)
  v5 토큰 (surface-solid + glow-sm) 기반 솔리드 배너. blur 안 씀

API 클라이언트 4개에 cross_tenant_meta 노출
- lib/api/user.ts        — userAPI.getList 응답에 cross_tenant_meta 추가
- lib/api/role.ts        — roleAPI.getList 동일
- lib/api/batch.ts       — BatchAPI.getBatchConfigs 동일
- lib/api/multilang.ts   — getLangKeys 동일 (i18nList 페이지는 아직 직접
  호출 패턴이라 자동 적용 X — 후속에서 페이지를 getLangKeys 로 통일하면 동작)

페이지 마운트 (3개)
- userMng/userMngList — useUserManagement hook 에 crossTenantMeta state 추가
- userMng/rolesList   — loadRoleGroups 에서 메타 set
- automaticMng/batchmngList — loadBatchConfigs 에서 메타 set
- systemMng/i18nList — 스킵 (cross-tenant aggregation 미적용 상태, 별도 작업)

설계서 §11 검증 (직전 §11.2 부분 실패 시뮬) 결과: failed 배너가
header X-CrossTenant-Failed 와 동일 정보로 화면에 노출됨.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 17:14:48 +09:00

408 lines
12 KiB
TypeScript

import { apiClient } from "./client";
import { ApiResponse } from "@/types/commonCode";
import { isCrossTenantMode } from "@/lib/auth/crossTenantMode";
/**
* 권한 그룹 인터페이스
*/
export interface RoleGroup {
objid: number;
auth_name: string;
auth_code: string;
company_code: string;
status: string;
writer: string;
regdate: string;
member_count?: number;
menu_count?: number;
member_names?: string;
}
/**
* 권한 그룹 멤버 인터페이스
*/
export interface RoleMember {
objid: number;
master_objid: number;
user_id: string;
user_name: string;
dept_name?: string;
position_name?: string;
writer: string;
regdate: string;
}
/**
* 메뉴 권한 인터페이스
*/
export interface MenuPermission {
objid: number;
menu_objid: number;
auth_objid: number;
menu_name?: string;
create_yn: string;
read_yn: string;
update_yn: string;
delete_yn: string;
writer: string;
regdate: string;
}
/**
* 권한 그룹 API
*/
export const roleAPI = {
/**
* 권한 그룹 목록 조회
*
* 분기:
* - cross-tenant 모드 (SUPER_ADMIN + company_code="*") → /admin/cross-tenant/roles
* 응답 행마다 company_code 박혀있어 화면에서 회사 컬럼/필터 가능.
* - 단일 회사 모드 → /roles (기존)
*
* 두 응답을 동일 shape `{ success, data: RoleGroup[] }` 로 정규화.
*/
async getList(params?: { company_code?: string; search?: string }): Promise<ApiResponse<RoleGroup[]>> {
try {
if (isCrossTenantMode()) {
const response = await apiClient.get("/admin/cross-tenant/roles", { params });
const ct = response.data;
if (ct && ct.success && ct.data) {
return {
success: true,
data: (ct.data.rows || []) as RoleGroup[],
message: ct.message,
// CrossTenantBanner 메타 — ApiResponse 타입엔 없지만 페이지가 캐스팅으로 접근
cross_tenant_meta: {
companies_queried: ct.data.companies_queried,
companies_failed: ct.data.companies_failed,
failed_company_codes: ct.data.failed_company_codes ?? [],
truncated: ct.data.truncated,
truncated_company_codes: ct.data.truncated_company_codes ?? [],
per_company_limit: ct.data.per_company_limit,
},
} as ApiResponse<RoleGroup[]>;
}
return ct;
}
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: { auth_name: string; auth_code: string; company_code: 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: {
auth_name?: string;
auth_code?: 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`, { user_ids: 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: { user_ids: 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`, { user_ids: 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?company_code=${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<{
menu_objid: number;
create_yn: string;
read_yn: string;
update_yn: string;
delete_yn: 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 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,
};
}
},
/**
* 권한 그룹 통합 워크스페이스 조회
* 권한 그룹 선택 시 필요한 모든 정보 한 번에 반환
* - group: 권한 그룹 정보
* - members: 권한있는 직원
* - nonMembers: 권한없는 직원
* - menus: 전체 메뉴 (트리 원천)
* - permissions: 현재 메뉴 CRUD 권한
*/
async getWorkspace(roleId: number | string): Promise<ApiResponse<{
group: any;
members: any[];
nonMembers: any[];
menus: any[];
permissions: any[];
}>> {
try {
const response = await apiClient.get(`/roles/${roleId}/workspace`);
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<any>> {
try {
const response = await apiClient.post(`/roles/${roleId}/members/${encodeURIComponent(userId)}`);
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "멤버 추가 실패",
error: error.response?.data?.error || error.message,
};
}
},
/**
* 개별 멤버 제거 (이미지: "-->삭제" 체크 즉시 반영)
*/
async removeSingleMember(roleId: number | string, userId: string): Promise<ApiResponse<any>> {
try {
const response = await apiClient.delete(`/roles/${roleId}/members/${encodeURIComponent(userId)}`);
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "멤버 제거 실패",
error: error.response?.data?.error || error.message,
};
}
},
/**
* 개별 메뉴 CRUD 권한 토글 (이미지: 체크 즉시 반영)
* body: { create_yn?, read_yn?, update_yn?, delete_yn? } — 전달된 필드만 업데이트
*/
async toggleMenuPermission(
roleId: number | string,
menuObjid: number | string,
changes: {
create_yn?: "Y" | "N";
read_yn?: "Y" | "N";
update_yn?: "Y" | "N";
delete_yn?: "Y" | "N";
},
): Promise<ApiResponse<any>> {
try {
const response = await apiClient.patch(
`/roles/${roleId}/menu-permissions/${encodeURIComponent(String(menuObjid))}`,
changes,
);
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "메뉴 권한 토글 실패",
error: error.response?.data?.error || error.message,
};
}
},
};