From 2c57dc8cdabcd2b26a68faeb5adea6d882298b68 Mon Sep 17 00:00:00 2001 From: chpark Date: Wed, 22 Apr 2026 03:14:01 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B6=80=EC=84=9C=20=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EA=B7=B8=EB=A3=B9=20=EA=B6=8C=ED=95=9C=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/erp/controller/RoleController.java | 250 +++- .../java/com/erp/service/AdminService.java | 10 +- .../com/erp/service/DepartmentService.java | 45 +- .../java/com/erp/service/RoleService.java | 192 ++- .../src/main/resources/mapper/admin.xml | 134 ++ .../src/main/resources/mapper/department.xml | 89 +- .../src/main/resources/mapper/role.xml | 211 +++- .../(main)/admin/userMng/deptMngList/page.tsx | 952 +++++++++++++++ .../(main)/admin/userMng/rolesList/page.tsx | 1073 +++++++++++++---- .../admin/userMng/userAuthList/page.tsx | 369 ++++-- .../components/admin/UserAuthEditModal.tsx | 2 +- .../components/layout/AdminPageRenderer.tsx | 1 + frontend/components/layout/AppLayout.tsx | 12 +- frontend/lib/api/role.ts | 89 ++ frontend/types/department.ts | 40 +- 15 files changed, 3031 insertions(+), 438 deletions(-) create mode 100644 frontend/app/(main)/admin/userMng/deptMngList/page.tsx diff --git a/backend-spring/src/main/java/com/erp/controller/RoleController.java b/backend-spring/src/main/java/com/erp/controller/RoleController.java index 2d55477f..1d8948ea 100644 --- a/backend-spring/src/main/java/com/erp/controller/RoleController.java +++ b/backend-spring/src/main/java/com/erp/controller/RoleController.java @@ -37,8 +37,6 @@ public class RoleController { return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); } - // SUPER_ADMIN: 요청 파라미터의 companyCode 사용 (없으면 전체) - // 그 외: 자신의 companyCode 강제 적용 if (!isSuperAdmin(role)) { params.put("company_code", companyCode); } @@ -61,7 +59,7 @@ public class RoleController { } Map params = new HashMap<>(); - params.put("objid", parseLong(id)); + params.put("objid", id); Map group = roleService.getRoleGroupById(params); if (group == null) { @@ -97,9 +95,9 @@ public class RoleController { Map params = new HashMap<>(body); params.put("writer", userId); - params.put("objid", (int)(System.currentTimeMillis() % Integer.MAX_VALUE)); + // objid는 varchar — 타임스탬프 기반 유니크 문자열 + params.put("objid", "AM" + System.currentTimeMillis()); - // API 필드명(role_name/role_code) → DB 필드명(auth_name/auth_code) 매핑 if (params.containsKey("role_name") && !params.containsKey("auth_name")) { params.put("auth_name", params.get("role_name")); } @@ -120,13 +118,14 @@ public class RoleController { @PathVariable String id, @RequestAttribute("company_code") String companyCode, @RequestAttribute("role") String role, + @RequestAttribute("user_id") String userId, @RequestBody Map body) { if (!isAdmin(role)) { return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); } - Map findParams = Map.of("objid", parseLong(id)); + Map findParams = Map.of("objid", id); Map existing = roleService.getRoleGroupById(findParams); if (existing == null) { return ResponseEntity.status(404).body(ApiResponse.error("권한 그룹을 찾을 수 없습니다.")); @@ -137,9 +136,9 @@ public class RoleController { } Map params = new HashMap<>(body); - params.put("objid", parseLong(id)); + params.put("objid", id); + params.put("writer", userId); - // API 필드명(role_name/role_code) → DB 필드명(auth_name/auth_code) 매핑 if (params.containsKey("role_name") && !params.containsKey("auth_name")) { params.put("auth_name", params.get("role_name")); } @@ -165,7 +164,7 @@ public class RoleController { return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); } - Map findParams = Map.of("objid", parseLong(id)); + Map findParams = Map.of("objid", id); Map existing = roleService.getRoleGroupById(findParams); if (existing == null) { return ResponseEntity.status(404).body(ApiResponse.error("권한 그룹을 찾을 수 없습니다.")); @@ -176,13 +175,50 @@ public class RoleController { } Map params = new HashMap<>(); - params.put("objid", parseLong(id)); + params.put("objid", id); roleService.deleteRoleGroup(params); return ResponseEntity.ok(ApiResponse.success(null, "권한 그룹 삭제 성공")); } // ────────────────────────────────────────────────── - // 멤버 관리 — /{id}/members 보다 먼저 정의된 2단계 경로 + // 통합 워크스페이스 (이미지 요구사항: 선택 시 한 번에 로드) + // ────────────────────────────────────────────────── + + /** + * GET /api/roles/{id}/workspace + * 권한 그룹 선택 시 필요한 모든 정보 한 번에 반환: + * - group: 권한 그룹 정보 + * - members: 권한있는 직원 + * - nonMembers: 권한없는 직원 + * - menus: 전체 메뉴 (트리 원천) + * - permissions: 현재 메뉴 CRUD 권한 + */ + @GetMapping("/{id}/workspace") + public ResponseEntity>> getRoleWorkspace( + @PathVariable String id, + @RequestAttribute("company_code") String companyCode, + @RequestAttribute("role") String role) { + + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } + + Map workspace = roleService.getRoleWorkspace(id); + if (workspace == null) { + return ResponseEntity.status(404).body(ApiResponse.error("권한 그룹을 찾을 수 없습니다.")); + } + + @SuppressWarnings("unchecked") + Map group = (Map) workspace.get("group"); + if (!isSuperAdmin(role) && !companyCode.equals(group.get("company_code"))) { + return ResponseEntity.status(403).body(ApiResponse.error("권한이 없습니다.")); + } + + return ResponseEntity.ok(ApiResponse.success(workspace, "권한 그룹 워크스페이스 조회 성공")); + } + + // ────────────────────────────────────────────────── + // 멤버 관리 // ────────────────────────────────────────────────── /** @@ -202,7 +238,7 @@ public class RoleController { /** * GET /api/roles/menus/all - * 전체 메뉴 목록 조회 (권한 설정용) + * 전체 메뉴 목록 조회 (권한 설정용, 트리 원천) */ @GetMapping("/menus/all") public ResponseEntity>>> getAllMenus( @@ -216,7 +252,6 @@ public class RoleController { Map params = new HashMap<>(); if (isSuperAdmin(role)) { - // SUPER_ADMIN: 쿼리 파라미터로 받은 companyCode 사용 (없으면 전체) params.put("company_code", requestedCompanyCode); } else { params.put("company_code", companyCode); @@ -227,7 +262,7 @@ public class RoleController { /** * GET /api/roles/{id}/members - * 권한 그룹 멤버 목록 조회 + * 권한있는 직원 목록 조회 */ @GetMapping("/{id}/members") public ResponseEntity>>> getRoleMembers( @@ -239,8 +274,7 @@ public class RoleController { return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); } - Map findParams = Map.of("objid", parseLong(id)); - Map group = roleService.getRoleGroupById(findParams); + Map group = roleService.getRoleGroupById(Map.of("objid", id)); if (group == null) { return ResponseEntity.status(404).body(ApiResponse.error("권한 그룹을 찾을 수 없습니다.")); } @@ -249,13 +283,42 @@ public class RoleController { } Map params = new HashMap<>(); - params.put("master_objid", parseLong(id)); - return ResponseEntity.ok(ApiResponse.success(roleService.getRoleMembers(params), "권한 그룹 멤버 조회 성공")); + params.put("master_objid", id); + return ResponseEntity.ok(ApiResponse.success(roleService.getRoleMembers(params), "권한있는 직원 조회 성공")); + } + + /** + * GET /api/roles/{id}/non-members + * 권한없는 직원 목록 조회 (같은 회사) + */ + @GetMapping("/{id}/non-members") + public ResponseEntity>>> getRoleNonMembers( + @PathVariable String id, + @RequestAttribute("company_code") String companyCode, + @RequestAttribute("role") String role) { + + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } + + Map group = roleService.getRoleGroupById(Map.of("objid", id)); + if (group == null) { + return ResponseEntity.status(404).body(ApiResponse.error("권한 그룹을 찾을 수 없습니다.")); + } + if (!isSuperAdmin(role) && !companyCode.equals(group.get("company_code"))) { + return ResponseEntity.status(403).body(ApiResponse.error("권한이 없습니다.")); + } + + Map params = new HashMap<>(); + params.put("master_objid", id); + params.put("company_code", group.get("company_code")); + params.put("status_active", true); + return ResponseEntity.ok(ApiResponse.success(roleService.getRoleNonMembers(params), "권한없는 직원 조회 성공")); } /** * POST /api/roles/{id}/members - * 권한 그룹 멤버 추가 + * 권한 그룹 멤버 일괄 추가 (body: { user_ids: [...] }) */ @PostMapping("/{id}/members") public ResponseEntity> addRoleMembers( @@ -269,8 +332,7 @@ public class RoleController { return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); } - Map findParams = Map.of("objid", parseLong(id)); - Map group = roleService.getRoleGroupById(findParams); + Map group = roleService.getRoleGroupById(Map.of("objid", id)); if (group == null) { return ResponseEntity.status(404).body(ApiResponse.error("권한 그룹을 찾을 수 없습니다.")); } @@ -279,12 +341,45 @@ public class RoleController { } Map params = new HashMap<>(body); - params.put("master_objid", parseLong(id)); + params.put("master_objid", id); params.put("writer", currentUserId); roleService.addRoleMembers(params); return ResponseEntity.ok(ApiResponse.success(null, "권한 그룹 멤버 추가 성공")); } + /** + * POST /api/roles/{id}/members/{userId} + * 개별 멤버 추가 (이미지: "<--추가" 체크 즉시 반영) + */ + @PostMapping("/{id}/members/{userId}") + public ResponseEntity>> addSingleRoleMember( + @PathVariable String id, + @PathVariable String userId, + @RequestAttribute("company_code") String companyCode, + @RequestAttribute("role") String role, + @RequestAttribute("user_id") String currentUserId) { + + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } + + Map group = roleService.getRoleGroupById(Map.of("objid", id)); + if (group == null) { + return ResponseEntity.status(404).body(ApiResponse.error("권한 그룹을 찾을 수 없습니다.")); + } + if (!isSuperAdmin(role) && !companyCode.equals(group.get("company_code"))) { + return ResponseEntity.status(403).body(ApiResponse.error("권한이 없습니다.")); + } + + boolean inserted = roleService.addSingleRoleMember(id, userId, currentUserId); + Map result = new HashMap<>(); + result.put("inserted", inserted); + result.put("master_objid", id); + result.put("user_id", userId); + return ResponseEntity.ok(ApiResponse.success(result, + inserted ? "멤버 추가 성공" : "이미 멤버입니다.")); + } + /** * PUT /api/roles/{id}/members * 권한 그룹 멤버 일괄 업데이트 (diff 방식) @@ -301,8 +396,7 @@ public class RoleController { return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); } - Map findParams = Map.of("objid", parseLong(id)); - Map group = roleService.getRoleGroupById(findParams); + Map group = roleService.getRoleGroupById(Map.of("objid", id)); if (group == null) { return ResponseEntity.status(404).body(ApiResponse.error("권한 그룹을 찾을 수 없습니다.")); } @@ -311,7 +405,7 @@ public class RoleController { } Map params = new HashMap<>(body); - params.put("master_objid", parseLong(id)); + params.put("master_objid", id); params.put("writer", currentUserId); roleService.updateRoleMembers(params); return ResponseEntity.ok(ApiResponse.success(null, "권한 그룹 멤버 업데이트 성공")); @@ -319,7 +413,7 @@ public class RoleController { /** * DELETE /api/roles/{id}/members - * 권한 그룹 멤버 제거 + * 권한 그룹 멤버 일괄 제거 (body: { user_ids: [...] }) */ @DeleteMapping("/{id}/members") public ResponseEntity> removeRoleMembers( @@ -332,8 +426,7 @@ public class RoleController { return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); } - Map findParams = Map.of("objid", parseLong(id)); - Map group = roleService.getRoleGroupById(findParams); + Map group = roleService.getRoleGroupById(Map.of("objid", id)); if (group == null) { return ResponseEntity.status(404).body(ApiResponse.error("권한 그룹을 찾을 수 없습니다.")); } @@ -342,11 +435,43 @@ public class RoleController { } Map params = new HashMap<>(body); - params.put("master_objid", parseLong(id)); + params.put("master_objid", id); roleService.removeRoleMembers(params); return ResponseEntity.ok(ApiResponse.success(null, "권한 그룹 멤버 제거 성공")); } + /** + * DELETE /api/roles/{id}/members/{userId} + * 개별 멤버 제거 (이미지: "-->삭제" 체크 즉시 반영) + */ + @DeleteMapping("/{id}/members/{userId}") + public ResponseEntity>> removeSingleRoleMember( + @PathVariable String id, + @PathVariable String userId, + @RequestAttribute("company_code") String companyCode, + @RequestAttribute("role") String role) { + + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } + + Map group = roleService.getRoleGroupById(Map.of("objid", id)); + if (group == null) { + return ResponseEntity.status(404).body(ApiResponse.error("권한 그룹을 찾을 수 없습니다.")); + } + if (!isSuperAdmin(role) && !companyCode.equals(group.get("company_code"))) { + return ResponseEntity.status(403).body(ApiResponse.error("권한이 없습니다.")); + } + + boolean deleted = roleService.removeSingleRoleMember(id, userId); + Map result = new HashMap<>(); + result.put("deleted", deleted); + result.put("master_objid", id); + result.put("user_id", userId); + return ResponseEntity.ok(ApiResponse.success(result, + deleted ? "멤버 제거 성공" : "멤버가 존재하지 않습니다.")); + } + // ────────────────────────────────────────────────── // 메뉴 권한 관리 // ────────────────────────────────────────────────── @@ -365,8 +490,7 @@ public class RoleController { return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); } - Map findParams = Map.of("objid", parseLong(id)); - Map group = roleService.getRoleGroupById(findParams); + Map group = roleService.getRoleGroupById(Map.of("objid", id)); if (group == null) { return ResponseEntity.status(404).body(ApiResponse.error("권한 그룹을 찾을 수 없습니다.")); } @@ -375,13 +499,13 @@ public class RoleController { } Map params = new HashMap<>(); - params.put("auth_objid", parseLong(id)); + params.put("auth_objid", id); return ResponseEntity.ok(ApiResponse.success(roleService.getMenuPermissions(params), "메뉴 권한 조회 성공")); } /** * PUT /api/roles/{id}/menu-permissions - * 메뉴 권한 설정 (전체 교체) + * 메뉴 권한 일괄 설정 (전체 교체) */ @PutMapping("/{id}/menu-permissions") public ResponseEntity> setMenuPermissions( @@ -395,8 +519,7 @@ public class RoleController { return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); } - Map findParams = Map.of("objid", parseLong(id)); - Map group = roleService.getRoleGroupById(findParams); + Map group = roleService.getRoleGroupById(Map.of("objid", id)); if (group == null) { return ResponseEntity.status(404).body(ApiResponse.error("권한 그룹을 찾을 수 없습니다.")); } @@ -405,12 +528,50 @@ public class RoleController { } Map params = new HashMap<>(body); - params.put("auth_objid", parseLong(id)); + params.put("auth_objid", id); params.put("writer", currentUserId); roleService.setMenuPermissions(params); return ResponseEntity.ok(ApiResponse.success(null, "메뉴 권한 설정 성공")); } + /** + * PATCH /api/roles/{id}/menu-permissions/{menuObjid} + * 개별 메뉴 CRUD 권한 토글 (이미지: 체크 즉시 반영) + * body: { create_yn?, read_yn?, update_yn?, delete_yn? } — 전달된 필드만 업데이트 + */ + @PatchMapping("/{id}/menu-permissions/{menuObjid}") + public ResponseEntity>> toggleMenuPermission( + @PathVariable String id, + @PathVariable String menuObjid, + @RequestAttribute("company_code") String companyCode, + @RequestAttribute("role") String role, + @RequestAttribute("user_id") String currentUserId, + @RequestBody(required = false) Map body) { + + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } + + Map group = roleService.getRoleGroupById(Map.of("objid", id)); + if (group == null) { + return ResponseEntity.status(404).body(ApiResponse.error("권한 그룹을 찾을 수 없습니다.")); + } + if (!isSuperAdmin(role) && !companyCode.equals(group.get("company_code"))) { + return ResponseEntity.status(403).body(ApiResponse.error("권한이 없습니다.")); + } + + Map payload = body != null ? body : new HashMap<>(); + String createYn = asYn(payload.get("create_yn")); + String readYn = asYn(payload.get("read_yn")); + String updateYn = asYn(payload.get("update_yn")); + String deleteYn = asYn(payload.get("delete_yn")); + + Map result = roleService.toggleMenuPermission( + id, menuObjid, createYn, readYn, updateYn, deleteYn, currentUserId); + + return ResponseEntity.ok(ApiResponse.success(result, "메뉴 권한 토글 성공")); + } + /** * GET /api/roles/user/{userId}/groups * 특정 사용자의 권한 그룹 조회 @@ -443,11 +604,16 @@ public class RoleController { return "SUPER_ADMIN".equals(role) || "ADMIN".equals(role) || "COMPANY_ADMIN".equals(role); } - private Long parseLong(String value) { - try { - return Long.parseLong(value); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("유효하지 않은 ID입니다: " + value); - } + /** + * 다양한 입력(true/false/"Y"/"N"/null)을 "Y"/"N"/null 로 정규화 + */ + 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; } } diff --git a/backend-spring/src/main/java/com/erp/service/AdminService.java b/backend-spring/src/main/java/com/erp/service/AdminService.java index d114c911..a83fc7a6 100644 --- a/backend-spring/src/main/java/com/erp/service/AdminService.java +++ b/backend-spring/src/main/java/com/erp/service/AdminService.java @@ -34,7 +34,15 @@ public class AdminService extends BaseService { public List> getUserMenuList(Map params) { params.putIfAbsent("user_lang", "ko"); - return sqlSession.selectList("admin.selectUserMenuList", params); + // 관리자(SUPER_ADMIN/COMPANY_ADMIN/ADMIN)는 모든 메뉴, 일반 사용자는 권한 그룹 연결 메뉴만 + String userType = (String) params.get("user_type"); + boolean isAdmin = "SUPER_ADMIN".equals(userType) + || "COMPANY_ADMIN".equals(userType) + || "ADMIN".equals(userType); + if (isAdmin) { + return sqlSession.selectList("admin.selectUserMenuList", params); + } + return sqlSession.selectList("admin.selectUserMenuListByRole", params); } public Map getPopMenuList(Map params) { diff --git a/backend-spring/src/main/java/com/erp/service/DepartmentService.java b/backend-spring/src/main/java/com/erp/service/DepartmentService.java index 68e130eb..a5eb1119 100644 --- a/backend-spring/src/main/java/com/erp/service/DepartmentService.java +++ b/backend-spring/src/main/java/com/erp/service/DepartmentService.java @@ -70,14 +70,28 @@ public class DepartmentService extends BaseService { long nextNumber = codeResult != null ? ((Number) codeResult.get("next_number")).longValue() : 1L; String deptCode = "DEPT_" + nextNumber; - // 부서 생성 - Object parentDeptCode = bodyParam(body, "parent_dept_code", "parent_dept_code"); + // 부서 생성 (전체 필드) Map insertParams = new HashMap<>(); insertParams.put("dept_code", deptCode); insertParams.put("dept_name", deptName); insertParams.put("company_code", companyCode); insertParams.put("company_name", companyName); - insertParams.put("parent_dept_code", parentDeptCode); + insertParams.put("parent_dept_code", nullIfBlank(bodyParam(body, "parent_dept_code", "parent_dept_code"))); + insertParams.put("short_name", nullIfBlank(bodyParam(body, "short_name", "short_name"))); + insertParams.put("dept_type", bodyParam(body, "dept_type", "dept_type")); + insertParams.put("org_system", nullIfBlank(bodyParam(body, "org_system", "org_system"))); + insertParams.put("approval_manager", nullIfBlank(bodyParam(body, "approval_manager", "approval_manager"))); + insertParams.put("dept_manager", nullIfBlank(bodyParam(body, "dept_manager", "dept_manager"))); + insertParams.put("org_head", nullIfBlank(bodyParam(body, "org_head", "org_head"))); + insertParams.put("zipcode", nullIfBlank(bodyParam(body, "zipcode", "zipcode"))); + insertParams.put("address1", nullIfBlank(bodyParam(body, "address1", "address1"))); + insertParams.put("address2", nullIfBlank(bodyParam(body, "address2", "address2"))); + insertParams.put("start_date", nullIfBlank(bodyParam(body, "start_date", "start_date"))); + insertParams.put("end_date", nullIfBlank(bodyParam(body, "end_date", "end_date"))); + insertParams.put("erp_managed", bodyParam(body, "erp_managed", "erp_managed")); + insertParams.put("show_in_chart", bodyParam(body, "show_in_chart", "show_in_chart")); + insertParams.put("sort_order", bodyParam(body, "sort_order", "sort_order")); + insertParams.put("status", bodyParam(body, "status", "status")); sqlSession.insert("department.insertDepartment", insertParams); log.info("부서 생성 성공: deptCode={}, deptName={}", deptCode, deptName); @@ -94,11 +108,25 @@ public class DepartmentService extends BaseService { throw new IllegalArgumentException("부서명을 입력해주세요."); } - Object parentDeptCode = bodyParam(body, "parent_dept_code", "parent_dept_code"); Map params = new HashMap<>(); params.put("dept_code", deptCode); params.put("dept_name", deptName); - params.put("parent_dept_code", parentDeptCode); + params.put("parent_dept_code", nullIfBlank(bodyParam(body, "parent_dept_code", "parent_dept_code"))); + params.put("short_name", nullIfBlank(bodyParam(body, "short_name", "short_name"))); + params.put("dept_type", bodyParam(body, "dept_type", "dept_type")); + params.put("org_system", nullIfBlank(bodyParam(body, "org_system", "org_system"))); + params.put("approval_manager", nullIfBlank(bodyParam(body, "approval_manager", "approval_manager"))); + params.put("dept_manager", nullIfBlank(bodyParam(body, "dept_manager", "dept_manager"))); + params.put("org_head", nullIfBlank(bodyParam(body, "org_head", "org_head"))); + params.put("zipcode", nullIfBlank(bodyParam(body, "zipcode", "zipcode"))); + params.put("address1", nullIfBlank(bodyParam(body, "address1", "address1"))); + params.put("address2", nullIfBlank(bodyParam(body, "address2", "address2"))); + params.put("start_date", nullIfBlank(bodyParam(body, "start_date", "start_date"))); + params.put("end_date", nullIfBlank(bodyParam(body, "end_date", "end_date"))); + params.put("erp_managed", bodyParam(body, "erp_managed", "erp_managed")); + params.put("show_in_chart", bodyParam(body, "show_in_chart", "show_in_chart")); + params.put("sort_order", bodyParam(body, "sort_order", "sort_order")); + params.put("status", bodyParam(body, "status", "status")); int updated = sqlSession.update("department.updateDepartment", params); if (updated == 0) { @@ -235,6 +263,13 @@ public class DepartmentService extends BaseService { return val != null ? val : body.get(camelCase); } + /** 빈 문자열을 null 로 치환 — DATE 컬럼에 '' 바인딩 시 pg cast 에러 나는 걸 방지 */ + private Object nullIfBlank(Object value) { + if (value == null) return null; + if (value instanceof String s && s.trim().isEmpty()) return null; + return value; + } + // ── 중복 예외 클래스 ──────────────────────────────── public static class DuplicateDeptNameException extends RuntimeException { diff --git a/backend-spring/src/main/java/com/erp/service/RoleService.java b/backend-spring/src/main/java/com/erp/service/RoleService.java index 1b4004ad..8c49f751 100644 --- a/backend-spring/src/main/java/com/erp/service/RoleService.java +++ b/backend-spring/src/main/java/com/erp/service/RoleService.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -29,7 +30,6 @@ public class RoleService extends BaseService { @Transactional public Map createRoleGroup(Map params) { sqlSession.insert("role.insertRoleGroup", params); - // insertRoleGroup이 useGeneratedKeys=true → params에 objid 주입됨 Object objid = params.get("objid"); Map findParams = new HashMap<>(); findParams.put("objid", objid); @@ -50,28 +50,64 @@ public class RoleService extends BaseService { memberParams.put("master_objid", objid); sqlSession.delete("role.deleteAllRoleMembers", memberParams); + Map menuParams = new HashMap<>(); + menuParams.put("auth_objid", objid); + sqlSession.delete("role.deleteMenuPermissions", menuParams); + sqlSession.delete("role.deleteRoleGroup", params); log.info("권한 그룹 삭제 완료: objid={}", objid); } // ────────────────────────────────────────────────── - // 멤버 관리 + // 멤버 관리 (권한있는 / 권한없는 직원) // ────────────────────────────────────────────────── public List> getRoleMembers(Map params) { return sqlSession.selectList("role.getRoleMemberList", params); } + public List> getRoleNonMembers(Map params) { + return sqlSession.selectList("role.getRoleNonMemberList", params); + } + @Transactional public void addRoleMembers(Map params) { List userIds = getStringList(params, "user_ids"); if (userIds.isEmpty()) return; Map insertParams = new HashMap<>(params); - insertParams.put("user_ids", userIds); + insertParams.put("userIds", userIds); sqlSession.insert("role.insertRoleMembers", insertParams); } + /** + * 개별 멤버 추가 (이미지: "<--추가" 1건) + */ + @Transactional + public boolean addSingleRoleMember(String masterObjid, String userId, String writer) { + Map params = new HashMap<>(); + params.put("master_objid", masterObjid); + params.put("user_id", userId); + params.put("writer", writer != null ? writer : "SYSTEM"); + params.put("objid", masterObjid + "_" + userId); + int affected = sqlSession.insert("role.insertSingleRoleMember", params); + log.info("개별 멤버 추가: master={}, user={}, inserted={}", masterObjid, userId, affected); + return affected > 0; + } + + /** + * 개별 멤버 제거 (이미지: "-->삭제" 1건) + */ + @Transactional + public boolean removeSingleRoleMember(String masterObjid, String userId) { + Map params = new HashMap<>(); + params.put("master_objid", masterObjid); + params.put("user_id", userId); + int affected = sqlSession.delete("role.deleteSingleRoleMember", params); + log.info("개별 멤버 제거: master={}, user={}, deleted={}", masterObjid, userId, affected); + return affected > 0; + } + @Transactional public void updateRoleMembers(Map params) { Object masterObjid = params.get("master_objid"); @@ -86,11 +122,9 @@ public class RoleService extends BaseService { .map(m -> (String) m.get("user_id")) .collect(Collectors.toList()); - // 추가할 멤버 List toAdd = newUserIds.stream() .filter(id -> !existingIds.contains(id)) .collect(Collectors.toList()); - // 제거할 멤버 List toRemove = existingIds.stream() .filter(id -> !newUserIds.contains(id)) .collect(Collectors.toList()); @@ -98,14 +132,14 @@ public class RoleService extends BaseService { if (!toAdd.isEmpty()) { Map addParams = new HashMap<>(); addParams.put("master_objid", masterObjid); - addParams.put("user_ids", toAdd); + addParams.put("userIds", toAdd); addParams.put("writer", writer); sqlSession.insert("role.insertRoleMembers", addParams); } if (!toRemove.isEmpty()) { Map removeParams = new HashMap<>(); removeParams.put("master_objid", masterObjid); - removeParams.put("user_ids", toRemove); + removeParams.put("userIds", toRemove); sqlSession.delete("role.deleteRoleMembers", removeParams); } log.info("권한 그룹 멤버 업데이트: masterObjid={}, added={}, removed={}", @@ -118,7 +152,7 @@ public class RoleService extends BaseService { if (userIds.isEmpty()) return; Map deleteParams = new HashMap<>(params); - deleteParams.put("user_ids", userIds); + deleteParams.put("userIds", userIds); sqlSession.delete("role.deleteRoleMembers", deleteParams); } @@ -130,6 +164,10 @@ public class RoleService extends BaseService { return sqlSession.selectList("role.getMenuPermissionList", params); } + /** + * 메뉴 권한 전체 설정 (기존 삭제 후 재등록) + * permissions 각 요소에 menuObjid, createYn, readYn, updateYn, deleteYn 포함 + */ @Transactional public void setMenuPermissions(Map params) { Object authObjid = params.get("auth_objid"); @@ -142,16 +180,93 @@ public class RoleService extends BaseService { sqlSession.delete("role.deleteMenuPermissions", deleteParams); if (permissions != null && !permissions.isEmpty()) { - Map insertParams = new HashMap<>(); - insertParams.put("auth_objid", authObjid); - insertParams.put("permissions", permissions); - insertParams.put("writer", writer); - sqlSession.insert("role.insertMenuPermissions", insertParams); + // CRUD 중 하나라도 Y인 것만 저장 (모두 N이면 의미 없음) + List nonEmpty = permissions.stream() + .filter(p -> p instanceof Map) + .filter(p -> { + Map m = (Map) p; + return "Y".equals(m.get("createYn")) + || "Y".equals(m.get("readYn")) + || "Y".equals(m.get("updateYn")) + || "Y".equals(m.get("deleteYn")); + }) + .collect(Collectors.toList()); + + if (!nonEmpty.isEmpty()) { + Map insertParams = new HashMap<>(); + insertParams.put("auth_objid", authObjid); + insertParams.put("permissions", nonEmpty); + insertParams.put("writer", writer); + sqlSession.insert("role.insertMenuPermissions", insertParams); + } } log.info("메뉴 권한 설정 완료: authObjid={}, count={}", authObjid, permissions != null ? permissions.size() : 0); } + /** + * 개별 메뉴 권한 upsert (이미지: 체크 즉시 반영) + * 전달된 필드만 업데이트, 미전달 필드는 기존값 유지 (null-safe) + * 모든 CRUD가 N이면 row 제거 + */ + @Transactional + public Map toggleMenuPermission( + String authObjid, String menuObjid, + String createYn, String readYn, String updateYn, String deleteYn, + String writer) { + + // 기존 row 조회 + Map findParams = new HashMap<>(); + findParams.put("auth_objid", authObjid); + findParams.put("menu_objid", menuObjid); + Map existing = sqlSession.selectOne("role.getMenuPermissionByMenu", findParams); + + // 최종값 계산: 전달값 우선, 없으면 기존값, 없으면 N + String finalCreate = resolve(createYn, existing, "create_yn"); + String finalRead = resolve(readYn, existing, "read_yn"); + String finalUpdate = resolve(updateYn, existing, "update_yn"); + String finalDelete = resolve(deleteYn, existing, "delete_yn"); + + // 모두 N이면 row 제거 + if ("N".equals(finalCreate) && "N".equals(finalRead) + && "N".equals(finalUpdate) && "N".equals(finalDelete)) { + if (existing != null) { + sqlSession.delete("role.deleteSingleMenuPermission", findParams); + } + Map result = new LinkedHashMap<>(); + result.put("master_objid", authObjid); + result.put("menu_objid", menuObjid); + result.put("create_yn", "N"); + result.put("read_yn", "N"); + result.put("update_yn", "N"); + result.put("delete_yn", "N"); + return result; + } + + // upsert + Map params = new HashMap<>(); + params.put("objid", authObjid + "_" + menuObjid); + params.put("auth_objid", authObjid); + params.put("menu_objid", menuObjid); + params.put("create_yn", finalCreate); + params.put("read_yn", finalRead); + params.put("update_yn", finalUpdate); + params.put("delete_yn", finalDelete); + params.put("writer", writer != null ? writer : "SYSTEM"); + sqlSession.insert("role.upsertMenuPermission", params); + + log.info("메뉴 권한 토글: auth={}, menu={}, CRUD={}/{}/{}/{}", + authObjid, menuObjid, finalCreate, finalRead, finalUpdate, finalDelete); + + return sqlSession.selectOne("role.getMenuPermissionByMenu", findParams); + } + + private String resolve(String newVal, Map existing, String key) { + if (newVal != null) return newVal; + if (existing != null && existing.get(key) != null) return (String) existing.get(key); + return "N"; + } + // ────────────────────────────────────────────────── // 메뉴 목록 / 사용자 권한 그룹 // ────────────────────────────────────────────────── @@ -164,6 +279,57 @@ public class RoleService extends BaseService { return sqlSession.selectList("role.getUserRoleGroupList", params); } + // ────────────────────────────────────────────────── + // 통합 워크스페이스 (이미지: 권한 그룹 선택 시 한 번에 로드) + // ────────────────────────────────────────────────── + + /** + * 권한 그룹 선택 시 화면에 필요한 모든 정보를 한 번에 조회 + * - 권한 그룹 정보 + * - 권한있는 직원 + * - 권한없는 직원 + * - 전체 메뉴 (트리 원천) + * - 현재 권한 그룹의 메뉴 권한 + */ + public Map getRoleWorkspace(String roleId) { + Map findParams = new HashMap<>(); + findParams.put("objid", roleId); + Map group = sqlSession.selectOne("role.getRoleGroupInfo", findParams); + if (group == null) return null; + + String companyCode = (String) group.get("company_code"); + + // 권한있는 직원 + Map memberParams = new HashMap<>(); + memberParams.put("master_objid", roleId); + List> members = sqlSession.selectList("role.getRoleMemberList", memberParams); + + // 권한없는 직원 (같은 회사 기준) + Map nonMemberParams = new HashMap<>(); + nonMemberParams.put("master_objid", roleId); + nonMemberParams.put("company_code", companyCode); + nonMemberParams.put("status_active", true); + List> nonMembers = sqlSession.selectList("role.getRoleNonMemberList", nonMemberParams); + + // 전체 메뉴 (해당 회사 + 공통 '*') + Map menuParams = new HashMap<>(); + menuParams.put("company_code", companyCode); + List> allMenus = sqlSession.selectList("role.getMenuList", menuParams); + + // 현재 권한 + Map permParams = new HashMap<>(); + permParams.put("auth_objid", roleId); + List> permissions = sqlSession.selectList("role.getMenuPermissionList", permParams); + + Map result = new LinkedHashMap<>(); + result.put("group", group); + result.put("members", members); + result.put("nonMembers", nonMembers); + result.put("menus", allMenus); + result.put("permissions", permissions); + return result; + } + // ────────────────────────────────────────────────── // 내부 유틸 // ────────────────────────────────────────────────── diff --git a/backend-spring/src/main/resources/mapper/admin.xml b/backend-spring/src/main/resources/mapper/admin.xml index ff0bf30c..d4fbdd8a 100644 --- a/backend-spring/src/main/resources/mapper/admin.xml +++ b/backend-spring/src/main/resources/mapper/admin.xml @@ -262,6 +262,140 @@ ORDER BY V.PATH, V.SEQ + + + @@ -24,7 +43,22 @@ DEPT_CODE, DEPT_NAME, COMPANY_CODE, - PARENT_DEPT_CODE + PARENT_DEPT_CODE, + SHORT_NAME, + DEPT_TYPE, + ORG_SYSTEM, + APPROVAL_MANAGER, + DEPT_MANAGER, + ORG_HEAD, + ZIPCODE, + ADDRESS1, + ADDRESS2, + START_DATE, + END_DATE, + ERP_MANAGED, + SHOW_IN_CHART, + SORT_ORDER, + STATUS FROM DEPT_INFO WHERE DEPT_CODE = #{dept_code} @@ -59,6 +93,20 @@ COMPANY_CODE, COMPANY_NAME, PARENT_DEPT_CODE, + SHORT_NAME, + DEPT_TYPE, + ORG_SYSTEM, + APPROVAL_MANAGER, + DEPT_MANAGER, + ORG_HEAD, + ZIPCODE, + ADDRESS1, + ADDRESS2, + START_DATE, + END_DATE, + ERP_MANAGED, + SHOW_IN_CHART, + SORT_ORDER, STATUS, CREATED_DATE ) VALUES ( @@ -67,7 +115,21 @@ #{company_code}, #{company_name}, #{parent_dept_code}, - 'active', + #{short_name}, + COALESCE(#{dept_type}, 'dept'), + #{org_system}, + #{approval_manager}, + #{dept_manager}, + #{org_head}, + #{zipcode}, + #{address1}, + #{address2}, + #{start_date}::date, + #{end_date}::date, + COALESCE(#{erp_managed}, 'Y'), + COALESCE(#{show_in_chart}, 'Y'), + COALESCE(#{sort_order}, 10), + COALESCE(#{status}, 'active'), NOW() ) @@ -76,8 +138,23 @@ UPDATE DEPT_INFO SET - DEPT_NAME = #{dept_name}, - PARENT_DEPT_CODE = #{parent_dept_code} + DEPT_NAME = #{dept_name}, + PARENT_DEPT_CODE = #{parent_dept_code}, + SHORT_NAME = #{short_name}, + DEPT_TYPE = #{dept_type}, + ORG_SYSTEM = #{org_system}, + APPROVAL_MANAGER = #{approval_manager}, + DEPT_MANAGER = #{dept_manager}, + ORG_HEAD = #{org_head}, + ZIPCODE = #{zipcode}, + ADDRESS1 = #{address1}, + ADDRESS2 = #{address2}, + START_DATE = #{start_date}::date, + END_DATE = #{end_date}::date, + ERP_MANAGED = #{erp_managed}, + SHOW_IN_CHART = #{show_in_chart}, + SORT_ORDER = #{sort_order}, + STATUS = #{status} WHERE DEPT_CODE = #{dept_code} diff --git a/backend-spring/src/main/resources/mapper/role.xml b/backend-spring/src/main/resources/mapper/role.xml index b7ff197c..ae977344 100644 --- a/backend-spring/src/main/resources/mapper/role.xml +++ b/backend-spring/src/main/resources/mapper/role.xml @@ -8,22 +8,24 @@ ────────────────────────────────────────────────── --> @@ -40,7 +42,7 @@ WHERE OBJID = #{objid} - + INSERT INTO AUTHORITY_MASTER ( OBJID @@ -85,8 +87,10 @@ + + - + + + + + + + INSERT INTO AUTHORITY_SUB_USER ( - MASTER_OBJID + OBJID + , MASTER_OBJID , USER_ID , WRITER , CREATED_DATE ) VALUES - - (#{master_objid}, #{user_id}, #{writer}, NOW()) + + (#{master_objid} || '_' || #{user_id}, #{master_objid}, #{user_id}, #{writer}, NOW()) + + + INSERT INTO AUTHORITY_SUB_USER ( + OBJID + , MASTER_OBJID + , USER_ID + , WRITER + , CREATED_DATE + ) + SELECT #{objid}, #{master_objid}, #{user_id}, #{writer}, NOW() + WHERE NOT EXISTS ( + SELECT 1 FROM AUTHORITY_SUB_USER + WHERE MASTER_OBJID = #{master_objid} AND USER_ID = #{user_id} + ) + + DELETE FROM AUTHORITY_SUB_USER WHERE MASTER_OBJID = #{master_objid} AND USER_ID IN - + #{user_id} + + + DELETE FROM AUTHORITY_SUB_USER + WHERE MASTER_OBJID = #{master_objid} + AND USER_ID = #{user_id} + + DELETE FROM AUTHORITY_SUB_USER @@ -133,23 +197,47 @@ + + + + + @@ -158,38 +246,95 @@ WHERE MASTER_OBJID = #{auth_objid} - + INSERT INTO AUTHORITY_SUB_MENU ( - MASTER_OBJID + OBJID + , MASTER_OBJID , MENU_OBJID + , CREATE_YN + , READ_YN + , UPDATE_YN + , DELETE_YN , WRITER , CREATED_DATE ) VALUES - (#{auth_objid}, #{perm.menuObjid}, #{writer}, NOW()) + ( + #{auth_objid} || '_' || #{perm.menuObjid} + , #{auth_objid} + , #{perm.menuObjid} + , COALESCE(#{perm.createYn}, 'N') + , COALESCE(#{perm.readYn}, 'N') + , COALESCE(#{perm.updateYn}, 'N') + , COALESCE(#{perm.deleteYn}, 'N') + , #{writer} + , NOW() + ) + + + INSERT INTO AUTHORITY_SUB_MENU ( + OBJID + , MASTER_OBJID + , MENU_OBJID + , CREATE_YN + , READ_YN + , UPDATE_YN + , DELETE_YN + , WRITER + , CREATED_DATE + ) VALUES ( + #{objid} + , #{auth_objid} + , #{menu_objid} + , COALESCE(#{create_yn}, 'N') + , COALESCE(#{read_yn}, 'N') + , COALESCE(#{update_yn}, 'N') + , COALESCE(#{delete_yn}, 'N') + , #{writer} + , NOW() + ) + ON CONFLICT (MASTER_OBJID, MENU_OBJID) DO UPDATE SET + CREATE_YN = COALESCE(EXCLUDED.CREATE_YN, AUTHORITY_SUB_MENU.CREATE_YN) + , READ_YN = COALESCE(EXCLUDED.READ_YN, AUTHORITY_SUB_MENU.READ_YN) + , UPDATE_YN = COALESCE(EXCLUDED.UPDATE_YN, AUTHORITY_SUB_MENU.UPDATE_YN) + , DELETE_YN = COALESCE(EXCLUDED.DELETE_YN, AUTHORITY_SUB_MENU.DELETE_YN) + , WRITER = EXCLUDED.WRITER + , UPDATED_DATE = NOW() + + + + + DELETE FROM AUTHORITY_SUB_MENU + WHERE MASTER_OBJID = #{auth_objid} + AND MENU_OBJID = #{menu_objid} + + 삭제 / <-- 추가) ─────────── + const handleAddMembers = useCallback(async () => { + if (!selectedRole || checkedNonMembers.size === 0) return; + const ids = Array.from(checkedNonMembers); + + // 낙관적 UI 업데이트 + const moving = nonMembers.filter((u) => checkedNonMembers.has(u.user_id)); + setMembers((prev) => [...prev, ...moving]); + setNonMembers((prev) => prev.filter((u) => !checkedNonMembers.has(u.user_id))); + setCheckedNonMembers(new Set()); + + try { + for (const userId of ids) { + const res = await roleAPI.addSingleMember(selectedRole.objid, userId); + if (!res.success) throw new Error(res.message); + } + await refreshMenus(); + loadRoleGroups(); + } catch (err) { + console.error("멤버 추가 오류:", err); + alert("멤버 추가에 실패했습니다. 화면을 새로고침합니다."); + loadWorkspace(selectedRole.objid); + } + }, [selectedRole, checkedNonMembers, nonMembers, refreshMenus, loadRoleGroups, loadWorkspace]); + + const handleRemoveMembers = useCallback(async () => { + if (!selectedRole || checkedMembers.size === 0) return; + const ids = Array.from(checkedMembers); + + const moving = members.filter((u) => checkedMembers.has(u.user_id)); + setNonMembers((prev) => [...prev, ...moving]); + setMembers((prev) => prev.filter((u) => !checkedMembers.has(u.user_id))); + setCheckedMembers(new Set()); + + try { + for (const userId of ids) { + const res = await roleAPI.removeSingleMember(selectedRole.objid, userId); + if (!res.success) throw new Error(res.message); + } + await refreshMenus(); + loadRoleGroups(); + } catch (err) { + console.error("멤버 제거 오류:", err); + alert("멤버 제거에 실패했습니다. 화면을 새로고침합니다."); + loadWorkspace(selectedRole.objid); + } + }, [selectedRole, checkedMembers, members, refreshMenus, loadRoleGroups, loadWorkspace]); + + // 리스트 필터링 (멤버/비멤버 검색) + const filteredMembers = useMemo(() => { + if (!memberSearch.trim()) return members; + const q = memberSearch.toLowerCase(); + return members.filter( + (u) => + (u.user_name || "").toLowerCase().includes(q) || + u.user_id.toLowerCase().includes(q) || + (u.dept_name || "").toLowerCase().includes(q), + ); + }, [members, memberSearch]); + + const filteredNonMembers = useMemo(() => { + if (!nonMemberSearch.trim()) return nonMembers; + const q = nonMemberSearch.toLowerCase(); + return nonMembers.filter( + (u) => + (u.user_name || "").toLowerCase().includes(q) || + u.user_id.toLowerCase().includes(q) || + (u.dept_name || "").toLowerCase().includes(q), + ); + }, [nonMembers, nonMemberSearch]); + + const toggleMemberCheck = useCallback((id: string) => { + setCheckedMembers((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }, []); + const toggleNonMemberCheck = useCallback((id: string) => { + setCheckedNonMembers((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }, []); + + const handleMembersSelectAll = useCallback( + (checked: boolean) => { + setCheckedMembers(checked ? new Set(filteredMembers.map((u) => u.user_id)) : new Set()); }, - [router], + [filteredMembers], + ); + const handleNonMembersSelectAll = useCallback( + (checked: boolean) => { + setCheckedNonMembers( + checked ? new Set(filteredNonMembers.map((u) => u.user_id)) : new Set(), + ); + }, + [filteredNonMembers], ); - // 관리자가 아니면 접근 제한 + // ─────────── 메뉴 트리 빌드 ─────────── + const menuTree = useMemo(() => { + const map = new Map(); + menus.forEach((m) => { + map.set(String(m.objid), { ...m, children: [], level: 0 }); + }); + const roots: MenuTreeNode[] = []; + map.forEach((node) => { + const parentId = + node.parent_objid && String(node.parent_objid) !== "0" + ? String(node.parent_objid) + : null; + if (parentId && map.has(parentId)) { + const parent = map.get(parentId)!; + node.level = parent.level + 1; + parent.children.push(node); + } else { + roots.push(node); + } + }); + const sortTree = (nodes: MenuTreeNode[]) => { + nodes.sort((a, b) => { + const sa = Number(a.sort_order ?? 0); + const sb = Number(b.sort_order ?? 0); + if (!isNaN(sa) && !isNaN(sb) && sa !== sb) return sa - sb; + return (a.menu_name || "").localeCompare(b.menu_name || ""); + }); + nodes.forEach((n) => sortTree(n.children)); + }; + sortTree(roots); + return roots; + }, [menus]); + + const toggleExpand = useCallback((id: string) => { + setExpandedMenus((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }, []); + + // ─────────── 메뉴 권한 즉시 반영 (PATCH) ─────────── + const applyMenuPermission = useCallback( + async (menuId: string, changes: Partial) => { + if (!selectedRole) return; + const prevPerm = permissions[menuId] || EMPTY_PERM; + const nextPerm: PermMap = { ...prevPerm, ...changes }; + + // 낙관적 UI 업데이트 + setPermissions((prev) => ({ ...prev, [menuId]: nextPerm })); + + try { + const res = await roleAPI.toggleMenuPermission(selectedRole.objid, menuId, changes); + if (!res.success) throw new Error(res.message); + + if (res.data) { + setPermissions((prev) => ({ + ...prev, + [menuId]: { + create_yn: res.data.create_yn === "Y" ? "Y" : "N", + read_yn: res.data.read_yn === "Y" ? "Y" : "N", + update_yn: res.data.update_yn === "Y" ? "Y" : "N", + delete_yn: res.data.delete_yn === "Y" ? "Y" : "N", + }, + })); + } + await refreshMenus(); + } catch (err) { + console.error("메뉴 권한 저장 오류:", err); + setPermissions((prev) => ({ ...prev, [menuId]: prevPerm })); + alert("권한 변경에 실패했습니다."); + } + }, + [selectedRole, permissions, refreshMenus], + ); + + // 이미지 3컬럼: 등록/수정(C+U 동시), 삭제(D), 조회(R) + const handleEditCol = useCallback( + (menuId: string, checked: boolean) => { + const v: "Y" | "N" = checked ? "Y" : "N"; + applyMenuPermission(menuId, { create_yn: v, update_yn: v }); + }, + [applyMenuPermission], + ); + const handleDeleteCol = useCallback( + (menuId: string, checked: boolean) => { + applyMenuPermission(menuId, { delete_yn: checked ? "Y" : "N" }); + }, + [applyMenuPermission], + ); + const handleReadCol = useCallback( + (menuId: string, checked: boolean) => { + applyMenuPermission(menuId, { read_yn: checked ? "Y" : "N" }); + }, + [applyMenuPermission], + ); + + const flatMenuIds = useMemo(() => { + const ids: string[] = []; + const walk = (nodes: MenuTreeNode[]) => { + nodes.forEach((n) => { + ids.push(String(n.objid)); + walk(n.children); + }); + }; + walk(menuTree); + return ids; + }, [menuTree]); + + const handleBulkColumn = useCallback( + async (column: "edit" | "delete" | "read", checked: boolean) => { + if (!selectedRole) return; + const v: "Y" | "N" = checked ? "Y" : "N"; + const change: Partial = + column === "edit" + ? { create_yn: v, update_yn: v } + : column === "delete" + ? { delete_yn: v } + : { read_yn: v }; + + setPermissions((prev) => { + const next = { ...prev }; + flatMenuIds.forEach((id) => { + next[id] = { ...(next[id] || EMPTY_PERM), ...change }; + }); + return next; + }); + + try { + for (const id of flatMenuIds) { + const res = await roleAPI.toggleMenuPermission(selectedRole.objid, id, change); + if (!res.success) throw new Error(res.message); + } + await refreshMenus(); + } catch (err) { + console.error("일괄 변경 오류:", err); + alert("일괄 변경 실패 — 화면을 새로고침합니다."); + loadWorkspace(selectedRole.objid); + } + }, + [selectedRole, flatMenuIds, refreshMenus, loadWorkspace], + ); + + const isColumnAllChecked = useCallback( + (column: "edit" | "delete" | "read"): boolean => { + if (flatMenuIds.length === 0) return false; + return flatMenuIds.every((id) => { + const p = permissions[id] || EMPTY_PERM; + if (column === "edit") return p.create_yn === "Y" && p.update_yn === "Y"; + if (column === "delete") return p.delete_yn === "Y"; + return p.read_yn === "Y"; + }); + }, + [flatMenuIds, permissions], + ); + + const renderMenuRow = (node: MenuTreeNode): React.ReactNode => { + const perm = permissions[String(node.objid)] || EMPTY_PERM; + const hasChildren = node.children.length > 0; + const isExpanded = expandedMenus.has(String(node.objid)); + const editChecked = perm.create_yn === "Y" && perm.update_yn === "Y"; + + return ( + + + +
+ {hasChildren ? ( + + ) : ( + + )} + + {node.menu_name} + +
+ + + handleEditCol(String(node.objid), c === true)} + /> + + + handleDeleteCol(String(node.objid), c === true)} + /> + + + handleReadCol(String(node.objid), c === true)} + /> + + + {hasChildren && isExpanded && node.children.map((c) => renderMenuRow(c))} +
+ ); + }; + + const handleCreateRole = () => setFormModal({ isOpen: true, editingRole: null }); + const handleEditRole = (r: RoleGroup) => setFormModal({ isOpen: true, editingRole: r }); + const handleDeleteRole = (r: RoleGroup) => setDeleteModal({ isOpen: true, role: r }); + const handleFormClose = () => setFormModal({ isOpen: false, editingRole: null }); + const handleDeleteClose = () => setDeleteModal({ isOpen: false, role: null }); + const handleModalSuccess = () => loadRoleGroups(); + if (!isAdmin) { return (
-

권한 그룹 관리

-

회사 내 권한 그룹을 생성하고 멤버를 관리합니다 (회사 관리자 이상)

+

권한 관리

-

접근 권한 없음

- 권한 그룹 관리는 회사 관리자 이상만 접근할 수 있습니다. + 권한 관리는 관리자만 접근할 수 있습니다.

-
); @@ -181,184 +579,391 @@ export default function RolesPage() { return (
-
- {/* 페이지 헤더 */} +
-

권한 그룹 관리

-

회사 내 권한 그룹을 생성하고 멤버를 관리합니다 (회사 관리자 이상)

+

권한 관리

+

+ 권한 그룹 선택 시 권한있는/없는 직원과 메뉴 권한이 로드되고, 체크 즉시 반영됩니다. +

- {/* 에러 메시지 */} {error && ( -
+
-

오류가 발생했습니다

-
-

{error}

)} - {/* 액션 버튼 영역 */} -
-
-

권한 그룹 목록 ({roleGroups.length})

- - {/* 최고 관리자 전용: 회사 필터 */} - {isSuperAdmin && ( + {/* 상단 4분할: 권한목록 | 권한있는직원 | 이동버튼 | 권한없는직원 */} +
+ {/* 권한 목록 */} +
+
- - - {selectedCompany !== "all" && ( - - )} + +

권한 목록

- )} + +
+
+
+ + setSearchText(e.target.value)} + className="h-8 pl-8 text-xs" + /> +
+ {isSuperAdmin && ( +
+ + +
+ )} +
+
+ {isLoading ? ( +
+
+
+ ) : filteredRoleGroups.length === 0 ? ( +

+ 등록된 권한 그룹이 없습니다 +

+ ) : ( +
    + {filteredRoleGroups.map((role) => { + const isSelected = selectedRole?.objid === role.objid; + return ( +
  • setSelectedRole(role)} + className={cn( + "group cursor-pointer p-2.5 transition-colors", + isSelected ? "bg-primary/10" : "hover:bg-muted/50", + )} + > +
    +
    +
    + {role.auth_name} +
    +
    + {role.auth_code} +
    +
    +
    + + +
    +
    +
  • + ); + })} +
+ )} +
- + {/* 권한있는 직원 */} +
+
+
+ +

권한있는 직원 ({members.length})

+
+
+ 0 && + checkedMembers.size === filteredMembers.length + } + onCheckedChange={(c) => handleMembersSelectAll(c === true)} + disabled={!selectedRole} + /> + 전체선택 +
+ + setMemberSearch(e.target.value)} + className="h-7 pl-7 text-xs" + disabled={!selectedRole} + /> +
+
+
+
+ {!selectedRole ? ( +
+

권한 그룹을 선택하세요

+
+ ) : isLoadingWorkspace ? ( +
+
+
+ ) : filteredMembers.length === 0 ? ( +

+ {memberSearch ? "검색 결과 없음" : "권한있는 직원이 없습니다"} +

+ ) : ( +
    + {filteredMembers.map((u) => ( +
  • toggleMemberCheck(u.user_id)} + className={cn( + "flex cursor-pointer items-center gap-2 p-2 transition-colors", + checkedMembers.has(u.user_id) ? "bg-muted" : "hover:bg-muted/50", + )} + > + toggleMemberCheck(u.user_id)} + /> +
    +
    {u.user_name || u.user_id}
    + {u.dept_name && ( +
    + {u.dept_name} +
    + )} +
    +
  • + ))} +
+ )} +
+
+ + {/* 이동 버튼: --> 삭제 / <-- 추가 */} +
+ + +
+ + {/* 권한없는 직원 */} +
+
+
+ +

권한없는 직원 ({nonMembers.length})

+
+
+ 0 && + checkedNonMembers.size === filteredNonMembers.length + } + onCheckedChange={(c) => handleNonMembersSelectAll(c === true)} + disabled={!selectedRole} + /> + 전체선택 +
+ + setNonMemberSearch(e.target.value)} + className="h-7 pl-7 text-xs" + disabled={!selectedRole} + /> +
+
+
+
+ {!selectedRole ? ( +
+

권한 그룹을 선택하세요

+
+ ) : isLoadingWorkspace ? ( +
+
+
+ ) : filteredNonMembers.length === 0 ? ( +

+ {nonMemberSearch ? "검색 결과 없음" : "권한없는 직원이 없습니다"} +

+ ) : ( +
    + {filteredNonMembers.map((u) => ( +
  • toggleNonMemberCheck(u.user_id)} + className={cn( + "flex cursor-pointer items-center gap-2 p-2 transition-colors", + checkedNonMembers.has(u.user_id) ? "bg-muted" : "hover:bg-muted/50", + )} + > + toggleNonMemberCheck(u.user_id)} + /> +
    +
    {u.user_name || u.user_id}
    + {u.dept_name && ( +
    + {u.dept_name} +
    + )} +
    +
  • + ))} +
+ )} +
+
- {/* 권한 그룹 목록 */} - {isLoading ? ( -
-
-
-

권한 그룹 목록을 불러오는 중...

-
+ {/* 하단: 메뉴 권한 트리 (등록/수정, 삭제, 조회 3컬럼) */} +
+
+

+ 메뉴 전체 트리구조{" "} + {selectedRole && ( + ({selectedRole.auth_name}) + )} +

+

+ 체크된 것들만 시스템에서 해당 버튼이 노출됩니다 · 체크 즉시 서버 반영 +

- ) : roleGroups.length === 0 ? ( -
-
-

등록된 권한 그룹이 없습니다.

-

권한 그룹을 생성하여 멤버를 관리해보세요.

+ {!selectedRole ? ( +
+

권한 그룹을 선택하세요

-
- ) : ( -
- {roleGroups.map((role) => ( -
- {/* 헤더 (클릭 시 상세 페이지) */} -
handleViewDetail(role)} - > -
-
-

{role.auth_name}

-

{role.auth_code}

-
- - {role.status === "active" ? "활성" : "비활성"} - -
- - {/* 정보 */} -
- {/* 최고 관리자는 회사명 표시 */} - {isSuperAdmin && ( -
- 회사 - - {companies.find((c) => c.company_code === role.company_code)?.company_name || role.company_code} - + ) : isLoadingWorkspace ? ( +
+
+
+ ) : menuTree.length === 0 ? ( +

+ 등록된 메뉴가 없습니다 +

+ ) : ( +
+ + + + + + + + + + {menuTree.map((n) => renderMenuRow(n))} +
+ 메뉴 전체 트리구조 + +
+ 등록/수정 + handleBulkColumn("edit", c === true)} + />
- )} -
- - - 멤버 수 - - {role.member_count || 0}명 -
-
- - - 메뉴 권한 - - {role.menu_count || 0}개 -
- - +
+
+ 삭제 + handleBulkColumn("delete", c === true)} + /> +
+
+
+ 조회 + handleBulkColumn("read", c === true)} + /> +
+
+
+ )} +
- {/* 액션 버튼 */} -
- - -
-
- ))} -
- )} - - {/* 모달들 */} -
- - {/* Scroll to Top 버튼 (모바일/태블릿 전용) */}
); } - diff --git a/frontend/app/(main)/admin/userMng/userAuthList/page.tsx b/frontend/app/(main)/admin/userMng/userAuthList/page.tsx index cee2ca53..44f6d23d 100644 --- a/frontend/app/(main)/admin/userMng/userAuthList/page.tsx +++ b/frontend/app/(main)/admin/userMng/userAuthList/page.tsx @@ -1,123 +1,172 @@ "use client"; import React, { useState, useCallback, useEffect } from "react"; -import { UserAuthTable } from "@/components/admin/UserAuthTable"; -import { UserAuthEditModal } from "@/components/admin/UserAuthEditModal"; -import { userAPI } from "@/lib/api/user"; +import { Button } from "@/components/ui/button"; +import { Plus, Edit, Trash2, Users, Menu, Filter, X } from "lucide-react"; +import { roleAPI, RoleGroup } from "@/lib/api/role"; import { useAuth } from "@/hooks/useAuth"; import { AlertCircle } from "lucide-react"; -import { Button } from "@/components/ui/button"; +import { RoleFormModal } from "@/components/admin/RoleFormModal"; +import { RoleDeleteModal } from "@/components/admin/RoleDeleteModal"; +import { useRouter } from "next/navigation"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { companyAPI } from "@/lib/api/company"; import { ScrollToTop } from "@/components/common/ScrollToTop"; /** - * 사용자 권한 관리 페이지 - * URL: /admin/userAuth + * 권한 그룹 관리 페이지 + * URL: /admin/roles * - * 최고 관리자만 접근 가능 - * 사용자별 권한 레벨(SUPER_ADMIN, COMPANY_ADMIN, USER 등) 관리 + * shadcn/ui 스타일 가이드 적용 + * + * 기능: + * - 회사별 권한 그룹 목록 조회 + * - 권한 그룹 생성/수정/삭제 + * - 멤버 관리 (Dual List Box) + * - 메뉴 권한 설정 (CRUD 권한) + * - 상세 페이지로 이동 (멤버 관리 + 메뉴 권한 설정) */ -export default function UserAuthPage() { +export default function RolesPage() { const { user: currentUser } = useAuth(); + const router = useRouter(); - // 최고 관리자 여부 + // 회사 관리자 또는 최고 관리자 여부 + const isAdmin = + (currentUser?.company_code === "*" && currentUser?.user_type === "SUPER_ADMIN") || + currentUser?.user_type === "COMPANY_ADMIN"; const isSuperAdmin = currentUser?.company_code === "*" && currentUser?.user_type === "SUPER_ADMIN"; // 상태 관리 - const [users, setUsers] = useState([]); + const [roleGroups, setRoleGroups] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const [paginationInfo, setPaginationInfo] = useState({ - currentPage: 1, - pageSize: 20, - totalItems: 0, - totalPages: 0, + + // 회사 필터 (최고 관리자 전용) + const [companies, setCompanies] = useState>([]); + const [selectedCompany, setSelectedCompany] = useState("all"); + + // 모달 상태 + const [formModal, setFormModal] = useState({ + isOpen: false, + editingRole: null as RoleGroup | null, }); - // 권한 변경 모달 - const [authEditModal, setAuthEditModal] = useState({ + const [deleteModal, setDeleteModal] = useState({ isOpen: false, - user: null as any | null, + role: null as RoleGroup | null, }); + // 회사 목록 로드 (최고 관리자만) + const loadCompanies = useCallback(async () => { + if (!isSuperAdmin) return; + + try { + const companies = await companyAPI.getList(); + setCompanies(companies); + } catch (error) { + console.error("회사 목록 로드 오류:", error); + } + }, [isSuperAdmin]); + // 데이터 로드 - const loadUsers = useCallback( - async (page: number = 1) => { - setIsLoading(true); - setError(null); + const loadRoleGroups = useCallback(async () => { + setIsLoading(true); + setError(null); - try { - const response = await userAPI.getList({ - page, - size: paginationInfo.pageSize, - }); + try { + // 최고 관리자: selectedCompany에 따라 필터링 (all이면 전체 조회) + // 회사 관리자: 자기 회사만 조회 + const companyFilter = + isSuperAdmin && selectedCompany !== "all" + ? selectedCompany + : isSuperAdmin + ? undefined + : currentUser?.company_code; - if (response.success && response.data) { - setUsers(response.data); - setPaginationInfo({ - currentPage: response.currentPage || page, - pageSize: response.pageSize || paginationInfo.pageSize, - totalItems: response.total || 0, - totalPages: Math.ceil((response.total || 0) / (response.pageSize || paginationInfo.pageSize)), - }); - } else { - setError(response.message || "사용자 목록을 불러오는데 실패했습니다."); - } - } catch (err) { - console.error("사용자 목록 로드 오류:", err); - setError("사용자 목록을 불러오는 중 오류가 발생했습니다."); - } finally { - setIsLoading(false); + console.log("권한 그룹 목록 조회:", { isSuperAdmin, selectedCompany, companyFilter }); + + const response = await roleAPI.getList({ + companyCode: companyFilter, + }); + + if (response.success && response.data) { + setRoleGroups(response.data); + console.log("권한 그룹 조회 성공:", response.data.length, "개"); + } else { + setError(response.message || "권한 그룹 목록을 불러오는데 실패했습니다."); } - }, - [paginationInfo.pageSize], - ); + } catch (err) { + console.error("권한 그룹 목록 로드 오류:", err); + setError("권한 그룹 목록을 불러오는 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }, [isSuperAdmin, selectedCompany, currentUser?.company_code]); useEffect(() => { - loadUsers(1); + if (isAdmin) { + if (isSuperAdmin) { + loadCompanies(); // 최고 관리자는 회사 목록 먼저 로드 + } + loadRoleGroups(); + } else { + setIsLoading(false); + } + }, [isAdmin, isSuperAdmin, loadRoleGroups, loadCompanies]); + + // 권한 그룹 생성 핸들러 + const handleCreateRole = useCallback(() => { + setFormModal({ isOpen: true, editingRole: null }); }, []); - // 권한 변경 핸들러 - const handleEditAuth = (user: any) => { - setAuthEditModal({ - isOpen: true, - user, - }); - }; + // 권한 그룹 수정 핸들러 + const handleEditRole = useCallback((role: RoleGroup) => { + setFormModal({ isOpen: true, editingRole: role }); + }, []); - // 권한 변경 모달 닫기 - const handleAuthEditClose = () => { - setAuthEditModal({ - isOpen: false, - user: null, - }); - }; + // 권한 그룹 삭제 핸들러 + const handleDeleteRole = useCallback((role: RoleGroup) => { + setDeleteModal({ isOpen: true, role }); + }, []); - // 권한 변경 성공 - const handleAuthEditSuccess = () => { - loadUsers(paginationInfo.currentPage); - handleAuthEditClose(); - }; + // 폼 모달 닫기 + const handleFormModalClose = useCallback(() => { + setFormModal({ isOpen: false, editingRole: null }); + }, []); - // 페이지 변경 - const handlePageChange = (page: number) => { - loadUsers(page); - }; + // 삭제 모달 닫기 + const handleDeleteModalClose = useCallback(() => { + setDeleteModal({ isOpen: false, role: null }); + }, []); - // 최고 관리자가 아닌 경우 - if (!isSuperAdmin) { + // 모달 성공 후 새로고침 + const handleModalSuccess = useCallback(() => { + loadRoleGroups(); + }, [loadRoleGroups]); + + // 상세 페이지로 이동 + const handleViewDetail = useCallback( + (role: RoleGroup) => { + router.push(`/admin/userMng/rolesList/${role.objid}`); + }, + [router], + ); + + // 관리자가 아니면 접근 제한 + if (!isAdmin) { return ( -
+
-

사용자 권한 관리

-

사용자별 권한 레벨을 관리합니다. (최고 관리자 전용)

+

권한 그룹 관리

+

회사 내 권한 그룹을 생성하고 멤버를 관리합니다 (회사 관리자 이상)

접근 권한 없음

- 권한 관리는 최고 관리자만 접근할 수 있습니다. + 권한 그룹 관리는 회사 관리자 이상만 접근할 수 있습니다.

+ )} +
+ )} +
+ + +
+ + {/* 권한 그룹 목록 */} + {isLoading ? ( +
+
+
+

권한 그룹 목록을 불러오는 중...

+
+
+ ) : roleGroups.length === 0 ? ( +
+
+

등록된 권한 그룹이 없습니다.

+

권한 그룹을 생성하여 멤버를 관리해보세요.

+
+
+ ) : ( +
+ {roleGroups.map((role) => ( +
+ {/* 헤더 (클릭 시 상세 페이지) */} +
handleViewDetail(role)} + > +
+
+

{role.auth_name}

+

{role.auth_code}

+
+ + {role.status === "active" ? "활성" : "비활성"} + +
+ + {/* 정보 */} +
+ {/* 최고 관리자는 회사명 표시 */} + {isSuperAdmin && ( +
+ 회사 + + {companies.find((c) => c.company_code === role.company_code)?.company_name || role.company_code} + +
+ )} +
+ + + 멤버 수 + + {role.member_count || 0}명 +
+
+ + + 메뉴 권한 + + {role.menu_count || 0}개 +
+
+
+ + {/* 액션 버튼 */} +
+ + +
+
+ ))} +
+ )} + + {/* 모달들 */} + - {/* 권한 변경 모달 */} -
@@ -179,3 +361,4 @@ export default function UserAuthPage() {
); } + diff --git a/frontend/components/admin/UserAuthEditModal.tsx b/frontend/components/admin/UserAuthEditModal.tsx index 6cbbd836..d1609ff3 100644 --- a/frontend/components/admin/UserAuthEditModal.tsx +++ b/frontend/components/admin/UserAuthEditModal.tsx @@ -25,7 +25,7 @@ interface UserAuthEditModalProps { /** * 사용자 권한 변경 모달 * - * 권한 레벨만 변경 가능 (최고 관리자 전용) + * 권한 레벨만 변경 가능 (관리자 이상 전용) */ export function UserAuthEditModal({ isOpen, onClose, onSuccess, user }: UserAuthEditModalProps) { const [selectedUserType, setSelectedUserType] = useState(""); diff --git a/frontend/components/layout/AdminPageRenderer.tsx b/frontend/components/layout/AdminPageRenderer.tsx index f9a2c97b..fdc1b99e 100644 --- a/frontend/components/layout/AdminPageRenderer.tsx +++ b/frontend/components/layout/AdminPageRenderer.tsx @@ -65,6 +65,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/admin/userMng/rolesList": dynamic(() => import("@/app/(main)/admin/userMng/rolesList/page"), { ssr: false, loading: LoadingFallback }), "/admin/userMng/userAuthList": dynamic(() => import("@/app/(main)/admin/userMng/userAuthList/page"), { ssr: false, loading: LoadingFallback }), "/admin/userMng/companyList": dynamic(() => import("@/app/(main)/admin/userMng/companyList/page"), { ssr: false, loading: LoadingFallback }), + "/admin/userMng/deptMngList": dynamic(() => import("@/app/(main)/admin/userMng/deptMngList/page"), { ssr: false, loading: LoadingFallback }), // 화면 관리 "/admin/screenMng/screenMngList": dynamic(() => import("@/app/(main)/admin/screenMng/screenMngList/page"), { ssr: false, loading: LoadingFallback }), diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index 56d600c0..0326afe5 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -850,17 +850,19 @@ function AppLayoutInner({ children }: AppLayoutProps) { const uiMenus = user ? convertMenuToUI(currentMenus, user as ExtendedUserInfo) : []; - // 활성 탭에 해당하는 메뉴가 속한 부모 메뉴 자동 확장 + // 활성 탭이 바뀔 때 한 번만 부모 메뉴 자동 확장. + // expandedMenus 를 의존성에 넣으면 사용자가 수동으로 닫은 즉시 다시 펼쳐져 "닫히지 않는" 버그가 남. + const autoExpandedForTabRef = useRef(null); useEffect(() => { if (!activeTab || uiMenus.length === 0) return; + if (autoExpandedForTabRef.current === activeTab.id) return; + autoExpandedForTabRef.current = activeTab.id; const toExpand: string[] = []; for (const menu of uiMenus) { if (menu.hasChildren && menu.children) { const hasActiveChild = menu.children.some((child: any) => isMenuActive(child)); - if (hasActiveChild && !expandedMenus.has(menu.id)) { - toExpand.push(menu.id); - } + if (hasActiveChild) toExpand.push(menu.id); } } if (toExpand.length > 0) { @@ -870,7 +872,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { return next; }); } - }, [activeTab, uiMenus, isMenuActive, expandedMenus]); + }, [activeTab, uiMenus, isMenuActive]); if (!user) { return ( diff --git a/frontend/lib/api/role.ts b/frontend/lib/api/role.ts index 57a30569..efd04cd4 100644 --- a/frontend/lib/api/role.ts +++ b/frontend/lib/api/role.ts @@ -286,4 +286,93 @@ export const roleAPI = { }; } }, + + /** + * 권한 그룹 통합 워크스페이스 조회 + * 권한 그룹 선택 시 필요한 모든 정보 한 번에 반환 + * - group: 권한 그룹 정보 + * - members: 권한있는 직원 + * - nonMembers: 권한없는 직원 + * - menus: 전체 메뉴 (트리 원천) + * - permissions: 현재 메뉴 CRUD 권한 + */ + async getWorkspace(roleId: number | string): Promise> { + 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> { + 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> { + 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> { + 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, + }; + } + }, }; diff --git a/frontend/types/department.ts b/frontend/types/department.ts index f2a0c87f..9134a746 100644 --- a/frontend/types/department.ts +++ b/frontend/types/department.ts @@ -8,12 +8,27 @@ export interface Department { dept_name: string; // 부서명 company_code: string; // 회사 코드 parent_dept_code?: string | null; // 상위 부서 코드 - sort_order?: number; // 정렬 순서 + // dept_info 확장 컬럼 + short_name?: string | null; // 부서약칭 + dept_type?: string | null; // 부서유형 (dept/team/temp) + org_system?: string | null; // 조직체계 + approval_manager?: string | null; // 결재관리자 user_id + dept_manager?: string | null; // 부서관리자 user_id + org_head?: string | null; // 조직장 user_id + zipcode?: string | null; + address1?: string | null; + address2?: string | null; + start_date?: string | null; // YYYY-MM-DD + end_date?: string | null; // YYYY-MM-DD + erp_managed?: "Y" | "N" | null; + show_in_chart?: "Y" | "N" | null; + sort_order?: number | null; + status?: "active" | "inactive" | null; created_at?: string; updated_at?: string; // UI용 추가 필드 - children?: Department[]; // 하위 부서 목록 - member_count?: number; // 부서원 수 + children?: Department[]; + member_count?: number; } // 부서원 정보 @@ -37,10 +52,25 @@ export interface UserDepartmentMapping { created_at?: string; } -// 부서 등록/수정 폼 데이터 +// 부서 등록/수정 폼 데이터 — 기본정보 탭 모든 필드 전달 가능 export interface DepartmentFormData { dept_name: string; // 부서명 (필수) - parent_dept_code?: string | null; // 상위 부서 코드 + parent_dept_code?: string | null; + short_name?: string | null; + dept_type?: string | null; + org_system?: string | null; + approval_manager?: string | null; + dept_manager?: string | null; + org_head?: string | null; + zipcode?: string | null; + address1?: string | null; + address2?: string | null; + start_date?: string | null; + end_date?: string | null; + erp_managed?: "Y" | "N" | null; + show_in_chart?: "Y" | "N" | null; + sort_order?: number | null; + status?: "active" | "inactive" | null; } // 부서 트리 노드 (UI용)