feat(cross-tenant): SUPER_ADMIN 의 회사별 권한관리 WRITE (Phase 2)
Phase 1(사용자관리) 패턴을 권한관리에 동일 적용. 권한 그룹 CRUD,
멤버 토글, 메뉴 권한 토글 모두 회사 컨텍스트 임시 전환 후 처리.
신규 백엔드
- crosstenant/CrossTenantRoleController.java
/api/admin/cross-tenant/roles/** — 8개 endpoint
· POST — 권한 그룹 생성 (body.company_code 필수)
· PUT /{id} — 권한 그룹 수정 (body.company_code 필수)
· DELETE /{id}?company_code= — 삭제
· GET /{id}/workspace?company_code= — 그룹 + 멤버 + 메뉴 통합 로드
· GET /menus/all?company_code= — 회사 메뉴 트리 (권한 설정용)
· POST /{id}/members/{userId}?company_code= — 멤버 1명 추가
· DELETE /{id}/members/{userId}?company_code= — 멤버 1명 제거
· PATCH /{id}/menu-permissions/{menuObjid} — 토글
CrossTenantExecutor 재사용. 기존 RoleController 무수정 (회귀 0).
중요: @RequestAttribute("user_id") 가 토큰 없을 때 missing 에러로 500
떨어지는 문제 — required=false 로 가드까지 안전하게 도달하도록.
프론트
- lib/api/role.ts — 7개 메서드(create/update/delete/getWorkspace/
getAllMenus/addSingleMember/removeSingleMember/toggleMenuPermission)에
isCrossTenantMode() 분기 + companyCode 인자 추가
- RoleFormModal — update 시 editingRole.company_code 같이 전달
- RoleDeleteModal — delete 시 role.company_code 같이 전달
- rolesList/page.tsx — loadWorkspace / addSingleMember / removeSingleMember /
toggleMenuPermission 호출 시 selectedRole.company_code 전달
검증 (curl, SUPER_ADMIN 토큰):
- 토큰 없음 → 403 super_admin_required
- POST 권한 그룹 (TEST02) → 201, /roles fan-out 에 by={TEST01:1, TEST02:1}
- DELETE → 200, fan-out by={TEST01:1} 로 복귀
미구현 (Phase 2 후속, 별도 작업):
- 일괄 멤버 추가/제거/diff (PUT/POST /members)
- 메뉴 권한 일괄 설정 (PUT /menu-permissions)
- 사용자별 권한 그룹 조회
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
package com.erp.crosstenant;
|
||||
|
||||
import com.erp.dto.ApiResponse;
|
||||
import com.erp.service.RoleService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* SUPER_ADMIN 의 cross-tenant ROLE WRITE/READ 엔드포인트 — Phase 2.
|
||||
*
|
||||
* 권한 그룹은 회사 DB 의 AUTHORITY_MASTER, 멤버/메뉴 권한도 회사 DB 내부 테이블.
|
||||
* 어느 회사의 권한 그룹인지 알아야 라우팅 가능 → 모든 endpoint 가 company_code 필수
|
||||
* (body 또는 query param).
|
||||
*
|
||||
* 단일 회사 모드 endpoint ({@link com.erp.controller.RoleController}) 는 무수정.
|
||||
*
|
||||
* @see CrossTenantExecutor
|
||||
* @see CrossTenantUserController // 같은 패턴, Phase 1
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/cross-tenant/roles")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CrossTenantRoleController {
|
||||
|
||||
private final CrossTenantExecutor executor;
|
||||
private final RoleService roleService;
|
||||
|
||||
// ── 권한 그룹 CRUD ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* POST /api/admin/cross-tenant/roles
|
||||
* body: { company_code, auth_name, auth_code, ... }
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> createRole(
|
||||
HttpServletRequest request,
|
||||
@RequestAttribute(value = "user_id", required = false) String writer,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
ResponseEntity<ApiResponse<Map<String, Object>>> g = guardMap(request);
|
||||
if (g != null) return g;
|
||||
|
||||
String targetCompany = (String) body.get("company_code");
|
||||
try {
|
||||
Map<String, Object> result = executor.runInCompany(targetCompany, () -> {
|
||||
Map<String, Object> params = new HashMap<>(body);
|
||||
params.put("writer", writer);
|
||||
params.put("objid", "AM" + System.currentTimeMillis());
|
||||
if (params.containsKey("role_name") && !params.containsKey("auth_name")) {
|
||||
params.put("auth_name", params.get("role_name"));
|
||||
}
|
||||
if (params.containsKey("role_code") && !params.containsKey("auth_code")) {
|
||||
params.put("auth_code", params.get("role_code"));
|
||||
}
|
||||
return roleService.createRoleGroup(params);
|
||||
});
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(result, "권한 그룹 생성 성공"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/admin/cross-tenant/roles/{id} body: { company_code, ... }
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> updateRole(
|
||||
HttpServletRequest request,
|
||||
@PathVariable String id,
|
||||
@RequestAttribute(value = "user_id", required = false) String writer,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
ResponseEntity<ApiResponse<Map<String, Object>>> g = guardMap(request);
|
||||
if (g != null) return g;
|
||||
|
||||
String targetCompany = (String) body.get("company_code");
|
||||
try {
|
||||
Map<String, Object> result = executor.runInCompany(targetCompany, () -> {
|
||||
Map<String, Object> params = new HashMap<>(body);
|
||||
params.put("objid", id);
|
||||
params.put("writer", writer);
|
||||
if (params.containsKey("role_name") && !params.containsKey("auth_name")) {
|
||||
params.put("auth_name", params.get("role_name"));
|
||||
}
|
||||
if (params.containsKey("role_code") && !params.containsKey("auth_code")) {
|
||||
params.put("auth_code", params.get("role_code"));
|
||||
}
|
||||
return roleService.updateRoleGroup(params);
|
||||
});
|
||||
return ResponseEntity.ok(ApiResponse.success(result, "권한 그룹 수정 성공"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/admin/cross-tenant/roles/{id}?company_code=TEST02
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Void>> deleteRole(
|
||||
HttpServletRequest request,
|
||||
@PathVariable String id,
|
||||
@RequestParam("company_code") String companyCode) {
|
||||
ResponseEntity<ApiResponse<Void>> g = guardVoid(request);
|
||||
if (g != null) return g;
|
||||
|
||||
try {
|
||||
executor.runInCompany(companyCode, () -> {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("objid", id);
|
||||
roleService.deleteRoleGroup(p);
|
||||
});
|
||||
return ResponseEntity.ok(ApiResponse.success(null, "권한 그룹 삭제 성공"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// ── 워크스페이스 / 메뉴 트리 ─────────────────────────────────
|
||||
|
||||
/**
|
||||
* GET /api/admin/cross-tenant/roles/{id}/workspace?company_code=TEST02
|
||||
* 그룹 + 멤버 + non-members + 메뉴 + 메뉴 권한 한 번에.
|
||||
*/
|
||||
@GetMapping("/{id}/workspace")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> getWorkspace(
|
||||
HttpServletRequest request,
|
||||
@PathVariable String id,
|
||||
@RequestParam("company_code") String companyCode) {
|
||||
ResponseEntity<ApiResponse<Map<String, Object>>> g = guardMap(request);
|
||||
if (g != null) return g;
|
||||
|
||||
try {
|
||||
Map<String, Object> ws = executor.runInCompany(companyCode,
|
||||
() -> roleService.getRoleWorkspace(id));
|
||||
if (ws == null) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(ApiResponse.error("권한 그룹을 찾을 수 없습니다."));
|
||||
}
|
||||
return ResponseEntity.ok(ApiResponse.success(ws, "워크스페이스 조회 성공"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/admin/cross-tenant/roles/menus/all?company_code=TEST02
|
||||
* 회사 메뉴 트리 (권한 설정용 원천).
|
||||
*/
|
||||
@GetMapping("/menus/all")
|
||||
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getAllMenus(
|
||||
HttpServletRequest request,
|
||||
@RequestParam("company_code") String companyCode) {
|
||||
ResponseEntity<ApiResponse<List<Map<String, Object>>>> g = guardList(request);
|
||||
if (g != null) return g;
|
||||
|
||||
try {
|
||||
List<Map<String, Object>> menus = executor.runInCompany(companyCode, () -> {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("company_code", companyCode);
|
||||
return roleService.getAllMenus(p);
|
||||
});
|
||||
return ResponseEntity.ok(ApiResponse.success(menus, "메뉴 목록 조회 성공"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// ── 멤버 토글 ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* POST /api/admin/cross-tenant/roles/{id}/members/{userId}?company_code=TEST02
|
||||
*/
|
||||
@PostMapping("/{id}/members/{userId}")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> addSingleMember(
|
||||
HttpServletRequest request,
|
||||
@PathVariable String id,
|
||||
@PathVariable String userId,
|
||||
@RequestAttribute(value = "user_id", required = false) String writer,
|
||||
@RequestParam("company_code") String companyCode) {
|
||||
ResponseEntity<ApiResponse<Map<String, Object>>> g = guardMap(request);
|
||||
if (g != null) return g;
|
||||
|
||||
try {
|
||||
Map<String, Object> result = executor.runInCompany(companyCode, () -> {
|
||||
boolean inserted = roleService.addSingleRoleMember(id, userId, writer);
|
||||
Map<String, Object> r = new HashMap<>();
|
||||
r.put("inserted", inserted);
|
||||
r.put("master_objid", id);
|
||||
r.put("user_id", userId);
|
||||
return r;
|
||||
});
|
||||
String msg = Boolean.TRUE.equals(result.get("inserted")) ? "멤버 추가 성공" : "이미 멤버입니다.";
|
||||
return ResponseEntity.ok(ApiResponse.success(result, msg));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/admin/cross-tenant/roles/{id}/members/{userId}?company_code=TEST02
|
||||
*/
|
||||
@DeleteMapping("/{id}/members/{userId}")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> removeSingleMember(
|
||||
HttpServletRequest request,
|
||||
@PathVariable String id,
|
||||
@PathVariable String userId,
|
||||
@RequestParam("company_code") String companyCode) {
|
||||
ResponseEntity<ApiResponse<Map<String, Object>>> g = guardMap(request);
|
||||
if (g != null) return g;
|
||||
|
||||
try {
|
||||
Map<String, Object> result = executor.runInCompany(companyCode, () -> {
|
||||
boolean deleted = roleService.removeSingleRoleMember(id, userId);
|
||||
Map<String, Object> r = new HashMap<>();
|
||||
r.put("deleted", deleted);
|
||||
r.put("master_objid", id);
|
||||
r.put("user_id", userId);
|
||||
return r;
|
||||
});
|
||||
String msg = Boolean.TRUE.equals(result.get("deleted")) ? "멤버 제거 성공" : "멤버가 존재하지 않습니다.";
|
||||
return ResponseEntity.ok(ApiResponse.success(result, msg));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// ── 메뉴 권한 토글 ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* PATCH /api/admin/cross-tenant/roles/{id}/menu-permissions/{menuObjid}
|
||||
* body: { company_code, create_yn?, read_yn?, update_yn?, delete_yn? }
|
||||
*/
|
||||
@PatchMapping("/{id}/menu-permissions/{menuObjid}")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> toggleMenuPermission(
|
||||
HttpServletRequest request,
|
||||
@PathVariable String id,
|
||||
@PathVariable String menuObjid,
|
||||
@RequestAttribute(value = "user_id", required = false) String writer,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
ResponseEntity<ApiResponse<Map<String, Object>>> g = guardMap(request);
|
||||
if (g != null) return g;
|
||||
|
||||
String targetCompany = (String) body.get("company_code");
|
||||
try {
|
||||
Map<String, Object> result = executor.runInCompany(targetCompany, () -> roleService.toggleMenuPermission(
|
||||
id, menuObjid,
|
||||
asYn(body.get("create_yn")),
|
||||
asYn(body.get("read_yn")),
|
||||
asYn(body.get("update_yn")),
|
||||
asYn(body.get("delete_yn")),
|
||||
writer));
|
||||
return ResponseEntity.ok(ApiResponse.success(result, "메뉴 권한 토글 성공"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// ── 가드 헬퍼 (응답 타입별로 3가지 — Map/Void/List) ────────
|
||||
|
||||
private ResponseEntity<ApiResponse<Map<String, Object>>> guardMap(HttpServletRequest request) {
|
||||
if (!CrossTenantContext.isSuperAdmin(request)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("super_admin_required", request.getRequestURI()));
|
||||
}
|
||||
if (!CrossTenantContext.isMetaContext()) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(ApiResponse.error("cross_tenant_requires_meta_context", request.getRequestURI()));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ResponseEntity<ApiResponse<Void>> guardVoid(HttpServletRequest request) {
|
||||
if (!CrossTenantContext.isSuperAdmin(request)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("super_admin_required", request.getRequestURI()));
|
||||
}
|
||||
if (!CrossTenantContext.isMetaContext()) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(ApiResponse.error("cross_tenant_requires_meta_context", request.getRequestURI()));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ResponseEntity<ApiResponse<List<Map<String, Object>>>> guardList(HttpServletRequest request) {
|
||||
if (!CrossTenantContext.isSuperAdmin(request)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("super_admin_required", request.getRequestURI()));
|
||||
}
|
||||
if (!CrossTenantContext.isMetaContext()) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(ApiResponse.error("cross_tenant_requires_meta_context", request.getRequestURI()));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** "Y"/"N"/null 정규화 — RoleController 의 동일 헬퍼 미러. */
|
||||
private String asYn(Object raw) {
|
||||
if (raw == null) return null;
|
||||
if (raw instanceof Boolean b) return b ? "Y" : "N";
|
||||
String s = String.valueOf(raw).trim();
|
||||
if (s.isEmpty()) return null;
|
||||
if ("Y".equalsIgnoreCase(s) || "true".equalsIgnoreCase(s) || "1".equals(s)) return "Y";
|
||||
if ("N".equalsIgnoreCase(s) || "false".equalsIgnoreCase(s) || "0".equals(s)) return "N";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -186,12 +186,12 @@ export default function RolesPage() {
|
||||
}, [roleGroups, selectedRole]);
|
||||
|
||||
// ─────────── 워크스페이스 로드 ───────────
|
||||
const loadWorkspace = useCallback(async (roleId: number | string) => {
|
||||
const loadWorkspace = useCallback(async (roleId: number | string, companyCode?: string) => {
|
||||
setIsLoadingWorkspace(true);
|
||||
setCheckedMembers(new Set());
|
||||
setCheckedNonMembers(new Set());
|
||||
try {
|
||||
const res = await roleAPI.getWorkspace(roleId);
|
||||
const res = await roleAPI.getWorkspace(roleId, companyCode);
|
||||
if (res.success && res.data) {
|
||||
setMembers(res.data.members || []);
|
||||
setNonMembers(res.data.nonMembers || []);
|
||||
@@ -216,7 +216,7 @@ export default function RolesPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRole) {
|
||||
loadWorkspace(selectedRole.objid);
|
||||
loadWorkspace(selectedRole.objid, selectedRole.company_code);
|
||||
} else {
|
||||
setMembers([]);
|
||||
setNonMembers([]);
|
||||
@@ -250,7 +250,7 @@ export default function RolesPage() {
|
||||
|
||||
try {
|
||||
for (const userId of ids) {
|
||||
const res = await roleAPI.addSingleMember(selectedRole.objid, userId);
|
||||
const res = await roleAPI.addSingleMember(selectedRole.objid, userId, selectedRole.company_code);
|
||||
if (!res.success) throw new Error(res.message);
|
||||
}
|
||||
await refreshMenus();
|
||||
@@ -258,7 +258,7 @@ export default function RolesPage() {
|
||||
} catch (err) {
|
||||
console.error("멤버 추가 오류:", err);
|
||||
alert("멤버 추가에 실패했습니다. 화면을 새로고침합니다.");
|
||||
loadWorkspace(selectedRole.objid);
|
||||
loadWorkspace(selectedRole.objid, selectedRole.company_code);
|
||||
}
|
||||
}, [selectedRole, checkedNonMembers, nonMembers, refreshMenus, loadRoleGroups, loadWorkspace]);
|
||||
|
||||
@@ -273,7 +273,7 @@ export default function RolesPage() {
|
||||
|
||||
try {
|
||||
for (const userId of ids) {
|
||||
const res = await roleAPI.removeSingleMember(selectedRole.objid, userId);
|
||||
const res = await roleAPI.removeSingleMember(selectedRole.objid, userId, selectedRole.company_code);
|
||||
if (!res.success) throw new Error(res.message);
|
||||
}
|
||||
await refreshMenus();
|
||||
@@ -281,7 +281,7 @@ export default function RolesPage() {
|
||||
} catch (err) {
|
||||
console.error("멤버 제거 오류:", err);
|
||||
alert("멤버 제거에 실패했습니다. 화면을 새로고침합니다.");
|
||||
loadWorkspace(selectedRole.objid);
|
||||
loadWorkspace(selectedRole.objid, selectedRole.company_code);
|
||||
}
|
||||
}, [selectedRole, checkedMembers, members, refreshMenus, loadRoleGroups, loadWorkspace]);
|
||||
|
||||
@@ -393,7 +393,7 @@ export default function RolesPage() {
|
||||
setPermissions((prev) => ({ ...prev, [menuId]: nextPerm }));
|
||||
|
||||
try {
|
||||
const res = await roleAPI.toggleMenuPermission(selectedRole.objid, menuId, changes);
|
||||
const res = await roleAPI.toggleMenuPermission(selectedRole.objid, menuId, changes, selectedRole.company_code);
|
||||
if (!res.success) throw new Error(res.message);
|
||||
|
||||
if (res.data) {
|
||||
@@ -471,14 +471,14 @@ export default function RolesPage() {
|
||||
|
||||
try {
|
||||
for (const id of flatMenuIds) {
|
||||
const res = await roleAPI.toggleMenuPermission(selectedRole.objid, id, change);
|
||||
const res = await roleAPI.toggleMenuPermission(selectedRole.objid, id, change, selectedRole.company_code);
|
||||
if (!res.success) throw new Error(res.message);
|
||||
}
|
||||
await refreshMenus();
|
||||
} catch (err) {
|
||||
console.error("일괄 변경 오류:", err);
|
||||
alert("일괄 변경 실패 — 화면을 새로고침합니다.");
|
||||
loadWorkspace(selectedRole.objid);
|
||||
loadWorkspace(selectedRole.objid, selectedRole.company_code);
|
||||
}
|
||||
},
|
||||
[selectedRole, flatMenuIds, refreshMenus, loadWorkspace],
|
||||
|
||||
@@ -49,7 +49,8 @@ export function RoleDeleteModal({ isOpen, onClose, onSuccess, role }: RoleDelete
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await roleAPI.delete(role.objid);
|
||||
// cross-tenant 모드에선 role.company_code 가 그 회사 DB 라우팅 키
|
||||
const response = await roleAPI.delete(role.objid, role.company_code);
|
||||
|
||||
if (response.success) {
|
||||
displayAlert("권한 그룹이 삭제되었습니다.", "success");
|
||||
|
||||
@@ -141,11 +141,12 @@ export function RoleFormModal({ isOpen, onClose, onSuccess, editingRole }: RoleF
|
||||
let response;
|
||||
|
||||
if (isEditMode && editingRole) {
|
||||
// 수정
|
||||
// 수정 — cross-tenant 모드에선 editingRole.company_code 가 그 회사 DB 라우팅 키
|
||||
response = await roleAPI.update(editingRole.objid, {
|
||||
auth_name: formData.authName,
|
||||
auth_code: formData.authCode,
|
||||
status: formData.status,
|
||||
company_code: editingRole.company_code,
|
||||
});
|
||||
} else {
|
||||
// 생성
|
||||
|
||||
+45
-20
@@ -114,10 +114,12 @@ export const roleAPI = {
|
||||
|
||||
/**
|
||||
* 권한 그룹 생성
|
||||
* cross-tenant 모드: data.company_code 가 가리키는 회사 DB 에 INSERT.
|
||||
*/
|
||||
async create(data: { auth_name: string; auth_code: string; company_code: string }): Promise<ApiResponse<RoleGroup>> {
|
||||
try {
|
||||
const response = await apiClient.post("/roles", data);
|
||||
const endpoint = isCrossTenantMode() ? "/admin/cross-tenant/roles" : "/roles";
|
||||
const response = await apiClient.post(endpoint, data);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
@@ -130,17 +132,20 @@ export const roleAPI = {
|
||||
|
||||
/**
|
||||
* 권한 그룹 수정
|
||||
* cross-tenant 모드: data.company_code 필수 (그 회사 DB 컨텍스트 라우팅).
|
||||
*/
|
||||
async update(
|
||||
id: number,
|
||||
id: number | string,
|
||||
data: {
|
||||
auth_name?: string;
|
||||
auth_code?: string;
|
||||
status?: string;
|
||||
company_code?: string;
|
||||
},
|
||||
): Promise<ApiResponse<RoleGroup>> {
|
||||
try {
|
||||
const response = await apiClient.put(`/roles/${id}`, data);
|
||||
const endpoint = isCrossTenantMode() ? `/admin/cross-tenant/roles/${id}` : `/roles/${id}`;
|
||||
const response = await apiClient.put(endpoint, data);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
@@ -153,10 +158,14 @@ export const roleAPI = {
|
||||
|
||||
/**
|
||||
* 권한 그룹 삭제
|
||||
* cross-tenant 모드: companyCode 필수 (어느 회사 DB 의 그룹인지).
|
||||
*/
|
||||
async delete(id: number): Promise<ApiResponse<null>> {
|
||||
async delete(id: number | string, companyCode?: string): Promise<ApiResponse<null>> {
|
||||
try {
|
||||
const response = await apiClient.delete(`/roles/${id}`);
|
||||
const url = isCrossTenantMode()
|
||||
? `/admin/cross-tenant/roles/${id}?company_code=${encodeURIComponent(companyCode ?? "")}`
|
||||
: `/roles/${id}`;
|
||||
const response = await apiClient.delete(url);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
@@ -233,12 +242,15 @@ export const roleAPI = {
|
||||
|
||||
/**
|
||||
* 전체 메뉴 목록 조회 (권한 설정용)
|
||||
* cross-tenant 모드: companyCode 필수 (어느 회사 메뉴 트리인지).
|
||||
*/
|
||||
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 url = isCrossTenantMode()
|
||||
? `/admin/cross-tenant/roles/menus/all?company_code=${encodeURIComponent(companyCode ?? "")}`
|
||||
: (companyCode ? `/roles/menus/all?company_code=${companyCode}` : "/roles/menus/all");
|
||||
|
||||
const response = await apiClient.get(url);
|
||||
|
||||
@@ -325,7 +337,7 @@ export const roleAPI = {
|
||||
* - menus: 전체 메뉴 (트리 원천)
|
||||
* - permissions: 현재 메뉴 CRUD 권한
|
||||
*/
|
||||
async getWorkspace(roleId: number | string): Promise<ApiResponse<{
|
||||
async getWorkspace(roleId: number | string, companyCode?: string): Promise<ApiResponse<{
|
||||
group: any;
|
||||
members: any[];
|
||||
nonMembers: any[];
|
||||
@@ -333,7 +345,10 @@ export const roleAPI = {
|
||||
permissions: any[];
|
||||
}>> {
|
||||
try {
|
||||
const response = await apiClient.get(`/roles/${roleId}/workspace`);
|
||||
const url = isCrossTenantMode()
|
||||
? `/admin/cross-tenant/roles/${roleId}/workspace?company_code=${encodeURIComponent(companyCode ?? "")}`
|
||||
: `/roles/${roleId}/workspace`;
|
||||
const response = await apiClient.get(url);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
@@ -345,11 +360,15 @@ export const roleAPI = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 개별 멤버 추가 (이미지: "<--추가" 체크 즉시 반영)
|
||||
* 개별 멤버 추가
|
||||
* cross-tenant 모드: companyCode 필수.
|
||||
*/
|
||||
async addSingleMember(roleId: number | string, userId: string): Promise<ApiResponse<any>> {
|
||||
async addSingleMember(roleId: number | string, userId: string, companyCode?: string): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
const response = await apiClient.post(`/roles/${roleId}/members/${encodeURIComponent(userId)}`);
|
||||
const url = isCrossTenantMode()
|
||||
? `/admin/cross-tenant/roles/${roleId}/members/${encodeURIComponent(userId)}?company_code=${encodeURIComponent(companyCode ?? "")}`
|
||||
: `/roles/${roleId}/members/${encodeURIComponent(userId)}`;
|
||||
const response = await apiClient.post(url);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
@@ -361,11 +380,15 @@ export const roleAPI = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 개별 멤버 제거 (이미지: "-->삭제" 체크 즉시 반영)
|
||||
* 개별 멤버 제거
|
||||
* cross-tenant 모드: companyCode 필수.
|
||||
*/
|
||||
async removeSingleMember(roleId: number | string, userId: string): Promise<ApiResponse<any>> {
|
||||
async removeSingleMember(roleId: number | string, userId: string, companyCode?: string): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
const response = await apiClient.delete(`/roles/${roleId}/members/${encodeURIComponent(userId)}`);
|
||||
const url = isCrossTenantMode()
|
||||
? `/admin/cross-tenant/roles/${roleId}/members/${encodeURIComponent(userId)}?company_code=${encodeURIComponent(companyCode ?? "")}`
|
||||
: `/roles/${roleId}/members/${encodeURIComponent(userId)}`;
|
||||
const response = await apiClient.delete(url);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
@@ -377,8 +400,8 @@ export const roleAPI = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 개별 메뉴 CRUD 권한 토글 (이미지: 체크 즉시 반영)
|
||||
* body: { create_yn?, read_yn?, update_yn?, delete_yn? } — 전달된 필드만 업데이트
|
||||
* 개별 메뉴 CRUD 권한 토글
|
||||
* body: { create_yn?, read_yn?, update_yn?, delete_yn? } + cross-tenant 시 company_code
|
||||
*/
|
||||
async toggleMenuPermission(
|
||||
roleId: number | string,
|
||||
@@ -389,12 +412,14 @@ export const roleAPI = {
|
||||
update_yn?: "Y" | "N";
|
||||
delete_yn?: "Y" | "N";
|
||||
},
|
||||
companyCode?: string,
|
||||
): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
const response = await apiClient.patch(
|
||||
`/roles/${roleId}/menu-permissions/${encodeURIComponent(String(menuObjid))}`,
|
||||
changes,
|
||||
);
|
||||
const endpoint = isCrossTenantMode()
|
||||
? `/admin/cross-tenant/roles/${roleId}/menu-permissions/${encodeURIComponent(String(menuObjid))}`
|
||||
: `/roles/${roleId}/menu-permissions/${encodeURIComponent(String(menuObjid))}`;
|
||||
const body = isCrossTenantMode() ? { ...changes, company_code: companyCode } : changes;
|
||||
const response = await apiClient.patch(endpoint, body);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user