This commit is contained in:
@@ -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<String, Object> params = new HashMap<>();
|
||||
params.put("objid", parseLong(id));
|
||||
params.put("objid", id);
|
||||
|
||||
Map<String, Object> group = roleService.getRoleGroupById(params);
|
||||
if (group == null) {
|
||||
@@ -97,9 +95,9 @@ public class RoleController {
|
||||
|
||||
Map<String, Object> 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<String, Object> body) {
|
||||
|
||||
if (!isAdmin(role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
Map<String, Object> findParams = Map.of("objid", parseLong(id));
|
||||
Map<String, Object> findParams = Map.of("objid", id);
|
||||
Map<String, Object> existing = roleService.getRoleGroupById(findParams);
|
||||
if (existing == null) {
|
||||
return ResponseEntity.status(404).body(ApiResponse.error("권한 그룹을 찾을 수 없습니다."));
|
||||
@@ -137,9 +136,9 @@ public class RoleController {
|
||||
}
|
||||
|
||||
Map<String, Object> 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<String, Object> findParams = Map.of("objid", parseLong(id));
|
||||
Map<String, Object> findParams = Map.of("objid", id);
|
||||
Map<String, Object> existing = roleService.getRoleGroupById(findParams);
|
||||
if (existing == null) {
|
||||
return ResponseEntity.status(404).body(ApiResponse.error("권한 그룹을 찾을 수 없습니다."));
|
||||
@@ -176,13 +175,50 @@ public class RoleController {
|
||||
}
|
||||
|
||||
Map<String, Object> 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<ApiResponse<Map<String, Object>>> getRoleWorkspace(
|
||||
@PathVariable String id,
|
||||
@RequestAttribute("company_code") String companyCode,
|
||||
@RequestAttribute("role") String role) {
|
||||
|
||||
if (!isAdmin(role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
Map<String, Object> workspace = roleService.getRoleWorkspace(id);
|
||||
if (workspace == null) {
|
||||
return ResponseEntity.status(404).body(ApiResponse.error("권한 그룹을 찾을 수 없습니다."));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> group = (Map<String, Object>) 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<ApiResponse<List<Map<String, Object>>>> getAllMenus(
|
||||
@@ -216,7 +252,6 @@ public class RoleController {
|
||||
|
||||
Map<String, Object> 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<ApiResponse<List<Map<String, Object>>>> getRoleMembers(
|
||||
@@ -239,8 +274,7 @@ public class RoleController {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
Map<String, Object> findParams = Map.of("objid", parseLong(id));
|
||||
Map<String, Object> group = roleService.getRoleGroupById(findParams);
|
||||
Map<String, Object> 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<String, Object> 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<ApiResponse<List<Map<String, Object>>>> getRoleNonMembers(
|
||||
@PathVariable String id,
|
||||
@RequestAttribute("company_code") String companyCode,
|
||||
@RequestAttribute("role") String role) {
|
||||
|
||||
if (!isAdmin(role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
Map<String, Object> 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<String, Object> 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<ApiResponse<Void>> addRoleMembers(
|
||||
@@ -269,8 +332,7 @@ public class RoleController {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
Map<String, Object> findParams = Map.of("objid", parseLong(id));
|
||||
Map<String, Object> group = roleService.getRoleGroupById(findParams);
|
||||
Map<String, Object> 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<String, Object> 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<ApiResponse<Map<String, Object>>> 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<String, Object> 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<String, Object> 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<String, Object> findParams = Map.of("objid", parseLong(id));
|
||||
Map<String, Object> group = roleService.getRoleGroupById(findParams);
|
||||
Map<String, Object> 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<String, Object> 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<ApiResponse<Void>> removeRoleMembers(
|
||||
@@ -332,8 +426,7 @@ public class RoleController {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
Map<String, Object> findParams = Map.of("objid", parseLong(id));
|
||||
Map<String, Object> group = roleService.getRoleGroupById(findParams);
|
||||
Map<String, Object> 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<String, Object> 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<ApiResponse<Map<String, Object>>> 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<String, Object> 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<String, Object> 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<String, Object> findParams = Map.of("objid", parseLong(id));
|
||||
Map<String, Object> group = roleService.getRoleGroupById(findParams);
|
||||
Map<String, Object> 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<String, Object> 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<ApiResponse<Void>> setMenuPermissions(
|
||||
@@ -395,8 +519,7 @@ public class RoleController {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
Map<String, Object> findParams = Map.of("objid", parseLong(id));
|
||||
Map<String, Object> group = roleService.getRoleGroupById(findParams);
|
||||
Map<String, Object> 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<String, Object> 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<ApiResponse<Map<String, Object>>> 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<String, Object> body) {
|
||||
|
||||
if (!isAdmin(role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,15 @@ public class AdminService extends BaseService {
|
||||
|
||||
public List<Map<String, Object>> getUserMenuList(Map<String, Object> 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<String, Object> getPopMenuList(Map<String, Object> params) {
|
||||
|
||||
@@ -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<String, Object> 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<String, Object> 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 {
|
||||
|
||||
@@ -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<String, Object> createRoleGroup(Map<String, Object> params) {
|
||||
sqlSession.insert("role.insertRoleGroup", params);
|
||||
// insertRoleGroup이 useGeneratedKeys=true → params에 objid 주입됨
|
||||
Object objid = params.get("objid");
|
||||
Map<String, Object> 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<String, Object> menuParams = new HashMap<>();
|
||||
menuParams.put("auth_objid", objid);
|
||||
sqlSession.delete("role.deleteMenuPermissions", menuParams);
|
||||
|
||||
sqlSession.delete("role.deleteRoleGroup", params);
|
||||
log.info("권한 그룹 삭제 완료: objid={}", objid);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// 멤버 관리
|
||||
// 멤버 관리 (권한있는 / 권한없는 직원)
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
public List<Map<String, Object>> getRoleMembers(Map<String, Object> params) {
|
||||
return sqlSession.selectList("role.getRoleMemberList", params);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getRoleNonMembers(Map<String, Object> params) {
|
||||
return sqlSession.selectList("role.getRoleNonMemberList", params);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void addRoleMembers(Map<String, Object> params) {
|
||||
List<String> userIds = getStringList(params, "user_ids");
|
||||
if (userIds.isEmpty()) return;
|
||||
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String> toAdd = newUserIds.stream()
|
||||
.filter(id -> !existingIds.contains(id))
|
||||
.collect(Collectors.toList());
|
||||
// 제거할 멤버
|
||||
List<String> toRemove = existingIds.stream()
|
||||
.filter(id -> !newUserIds.contains(id))
|
||||
.collect(Collectors.toList());
|
||||
@@ -98,14 +132,14 @@ public class RoleService extends BaseService {
|
||||
if (!toAdd.isEmpty()) {
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> toggleMenuPermission(
|
||||
String authObjid, String menuObjid,
|
||||
String createYn, String readYn, String updateYn, String deleteYn,
|
||||
String writer) {
|
||||
|
||||
// 기존 row 조회
|
||||
Map<String, Object> findParams = new HashMap<>();
|
||||
findParams.put("auth_objid", authObjid);
|
||||
findParams.put("menu_objid", menuObjid);
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> getRoleWorkspace(String roleId) {
|
||||
Map<String, Object> findParams = new HashMap<>();
|
||||
findParams.put("objid", roleId);
|
||||
Map<String, Object> group = sqlSession.selectOne("role.getRoleGroupInfo", findParams);
|
||||
if (group == null) return null;
|
||||
|
||||
String companyCode = (String) group.get("company_code");
|
||||
|
||||
// 권한있는 직원
|
||||
Map<String, Object> memberParams = new HashMap<>();
|
||||
memberParams.put("master_objid", roleId);
|
||||
List<Map<String, Object>> members = sqlSession.selectList("role.getRoleMemberList", memberParams);
|
||||
|
||||
// 권한없는 직원 (같은 회사 기준)
|
||||
Map<String, Object> nonMemberParams = new HashMap<>();
|
||||
nonMemberParams.put("master_objid", roleId);
|
||||
nonMemberParams.put("company_code", companyCode);
|
||||
nonMemberParams.put("status_active", true);
|
||||
List<Map<String, Object>> nonMembers = sqlSession.selectList("role.getRoleNonMemberList", nonMemberParams);
|
||||
|
||||
// 전체 메뉴 (해당 회사 + 공통 '*')
|
||||
Map<String, Object> menuParams = new HashMap<>();
|
||||
menuParams.put("company_code", companyCode);
|
||||
List<Map<String, Object>> allMenus = sqlSession.selectList("role.getMenuList", menuParams);
|
||||
|
||||
// 현재 권한
|
||||
Map<String, Object> permParams = new HashMap<>();
|
||||
permParams.put("auth_objid", roleId);
|
||||
List<Map<String, Object>> permissions = sqlSession.selectList("role.getMenuPermissionList", permParams);
|
||||
|
||||
Map<String, Object> 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;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// 내부 유틸
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
@@ -262,6 +262,140 @@
|
||||
ORDER BY V.PATH, V.SEQ
|
||||
</select>
|
||||
|
||||
<!-- ================================================================
|
||||
사용자 메뉴 목록 (권한 그룹 기반 필터링)
|
||||
- 로그인 사용자가 속한 권한 그룹의 메뉴만 반환 (READ_YN='Y')
|
||||
- 부모 메뉴는 트리 구조 유지 위해 자동 포함
|
||||
- 관리자가 아닌 일반 사용자 전용
|
||||
================================================================ -->
|
||||
<select id="selectUserMenuListByRole" parameterType="map" resultType="map">
|
||||
WITH RECURSIVE
|
||||
-- 1단계: 사용자의 권한 그룹에서 READ_YN='Y'인 메뉴
|
||||
PERMITTED_MENU AS (
|
||||
SELECT DISTINCT MI.*
|
||||
FROM MENU_INFO MI
|
||||
INNER JOIN AUTHORITY_SUB_MENU ASM ON ASM.MENU_OBJID = MI.OBJID
|
||||
INNER JOIN AUTHORITY_SUB_USER ASU ON ASU.MASTER_OBJID = ASM.MASTER_OBJID
|
||||
WHERE ASU.USER_ID = #{user_id}
|
||||
AND ASM.READ_YN = 'Y'
|
||||
AND MI.STATUS = 'active'
|
||||
AND MI.MENU_TYPE = '1'
|
||||
),
|
||||
-- 2단계: 부모 체인 포함 (트리 구조 유지)
|
||||
ALLOWED_MENU AS (
|
||||
SELECT * FROM PERMITTED_MENU
|
||||
|
||||
UNION
|
||||
|
||||
SELECT P.*
|
||||
FROM MENU_INFO P
|
||||
INNER JOIN ALLOWED_MENU A ON A.PARENT_OBJ_ID = P.OBJID
|
||||
WHERE P.OBJID != '0'
|
||||
AND P.STATUS = 'active'
|
||||
),
|
||||
V_MENU AS (
|
||||
SELECT
|
||||
1 AS LEV
|
||||
, MENU.MENU_TYPE
|
||||
, MENU.OBJID
|
||||
, MENU.PARENT_OBJ_ID
|
||||
, MENU.MENU_NAME_KOR
|
||||
, MENU.MENU_URL
|
||||
, MENU.MENU_DESC
|
||||
, MENU.SEQ
|
||||
, MENU.WRITER
|
||||
, MENU.CREATED_DATE
|
||||
, MENU.STATUS
|
||||
, MENU.COMPANY_CODE
|
||||
, MENU.LANG_KEY
|
||||
, MENU.LANG_KEY_DESC
|
||||
, MENU.MENU_ICON
|
||||
, ARRAY[MENU.OBJID] AS PATH
|
||||
FROM ALLOWED_MENU MENU
|
||||
WHERE MENU.PARENT_OBJ_ID = '0'
|
||||
AND MENU.MENU_TYPE = '1'
|
||||
<choose>
|
||||
<when test='company_code == "*"'>
|
||||
AND MENU.COMPANY_CODE = '*'
|
||||
</when>
|
||||
<otherwise>
|
||||
AND MENU.COMPANY_CODE = #{company_code}
|
||||
</otherwise>
|
||||
</choose>
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
V.LEV + 1
|
||||
, S.MENU_TYPE
|
||||
, S.OBJID
|
||||
, S.PARENT_OBJ_ID
|
||||
, S.MENU_NAME_KOR
|
||||
, S.MENU_URL
|
||||
, S.MENU_DESC
|
||||
, S.SEQ
|
||||
, S.WRITER
|
||||
, S.CREATED_DATE
|
||||
, S.STATUS
|
||||
, S.COMPANY_CODE
|
||||
, S.LANG_KEY
|
||||
, S.LANG_KEY_DESC
|
||||
, S.MENU_ICON
|
||||
, V.PATH || S.OBJID
|
||||
FROM ALLOWED_MENU S
|
||||
JOIN V_MENU V
|
||||
ON S.PARENT_OBJ_ID = V.OBJID
|
||||
WHERE S.OBJID != ALL(V.PATH)
|
||||
)
|
||||
SELECT
|
||||
V.LEV
|
||||
, CASE V.MENU_TYPE WHEN '0' THEN 'admin' WHEN '1' THEN 'user' ELSE '' END AS MENU_TYPE
|
||||
, CAST(V.OBJID AS TEXT) AS OBJID
|
||||
, CAST(V.PARENT_OBJ_ID AS TEXT) AS PARENT_OBJ_ID
|
||||
, V.MENU_NAME_KOR
|
||||
, LPAD(' ', 3 * (V.LEV - 1)) || V.MENU_NAME_KOR AS LPAD_MENU_NAME_KOR
|
||||
, COALESCE(V.MENU_URL, '') AS MENU_URL
|
||||
, COALESCE(V.MENU_DESC, '') AS MENU_DESC
|
||||
, CAST(V.SEQ AS TEXT) AS SEQ
|
||||
, V.WRITER
|
||||
, TO_CHAR(V.CREATED_DATE, 'YYYY-MM-DD') AS REGDATE
|
||||
, V.STATUS
|
||||
, V.COMPANY_CODE
|
||||
, COALESCE(V.LANG_KEY, '') AS LANG_KEY
|
||||
, COALESCE(V.LANG_KEY_DESC, '') AS LANG_KEY_DESC
|
||||
, COALESCE(V.MENU_ICON, '') AS MENU_ICON
|
||||
, COALESCE(CM.COMPANY_NAME, '미지정') AS COMPANY_NAME
|
||||
, COALESCE(
|
||||
(SELECT MLT.LANG_TEXT
|
||||
FROM MULTI_LANG_KEY_MASTER MLKM
|
||||
JOIN MULTI_LANG_TEXT MLT
|
||||
ON MLKM.KEY_ID = MLT.KEY_ID
|
||||
WHERE MLKM.LANG_KEY = V.LANG_KEY
|
||||
AND MLT.LANG_CODE = #{user_lang}
|
||||
LIMIT 1),
|
||||
V.MENU_NAME_KOR
|
||||
) AS TRANSLATED_NAME
|
||||
, COALESCE(
|
||||
(SELECT MLT.LANG_TEXT
|
||||
FROM MULTI_LANG_KEY_MASTER MLKM
|
||||
JOIN MULTI_LANG_TEXT MLT
|
||||
ON MLKM.KEY_ID = MLT.KEY_ID
|
||||
WHERE MLKM.LANG_KEY = V.LANG_KEY_DESC
|
||||
AND MLT.LANG_CODE = #{user_lang}
|
||||
LIMIT 1),
|
||||
COALESCE(V.MENU_DESC, '')
|
||||
) AS TRANSLATED_DESC
|
||||
, CASE UPPER(V.STATUS)
|
||||
WHEN 'ACTIVE' THEN '활성화'
|
||||
WHEN 'INACTIVE' THEN '비활성화'
|
||||
ELSE ''
|
||||
END AS STATUS_TITLE
|
||||
FROM V_MENU V
|
||||
LEFT JOIN COMPANY_MNG CM
|
||||
ON V.COMPANY_CODE = CM.COMPANY_CODE
|
||||
ORDER BY V.PATH, V.SEQ
|
||||
</select>
|
||||
|
||||
<!-- POP 메뉴: 부모 메뉴 조회 -->
|
||||
<select id="selectPopParentMenu" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
|
||||
@@ -10,12 +10,31 @@
|
||||
D.DEPT_NAME,
|
||||
D.COMPANY_CODE,
|
||||
D.PARENT_DEPT_CODE,
|
||||
D.SHORT_NAME,
|
||||
D.DEPT_TYPE,
|
||||
D.ORG_SYSTEM,
|
||||
D.APPROVAL_MANAGER,
|
||||
D.DEPT_MANAGER,
|
||||
D.ORG_HEAD,
|
||||
D.ZIPCODE,
|
||||
D.ADDRESS1,
|
||||
D.ADDRESS2,
|
||||
D.START_DATE,
|
||||
D.END_DATE,
|
||||
D.ERP_MANAGED,
|
||||
D.SHOW_IN_CHART,
|
||||
D.SORT_ORDER,
|
||||
D.STATUS,
|
||||
COUNT(DISTINCT UD.USER_ID) AS MEMBER_COUNT
|
||||
FROM DEPT_INFO D
|
||||
LEFT JOIN USER_DEPT UD ON D.DEPT_CODE = UD.DEPT_CODE
|
||||
WHERE (D.COMPANY_CODE = #{company_code} OR D.COMPANY_CODE = '*')
|
||||
GROUP BY D.DEPT_CODE, D.DEPT_NAME, D.COMPANY_CODE, D.PARENT_DEPT_CODE
|
||||
ORDER BY D.DEPT_NAME
|
||||
GROUP BY
|
||||
D.DEPT_CODE, D.DEPT_NAME, D.COMPANY_CODE, D.PARENT_DEPT_CODE,
|
||||
D.SHORT_NAME, D.DEPT_TYPE, D.ORG_SYSTEM, D.APPROVAL_MANAGER, D.DEPT_MANAGER, D.ORG_HEAD,
|
||||
D.ZIPCODE, D.ADDRESS1, D.ADDRESS2, D.START_DATE, D.END_DATE,
|
||||
D.ERP_MANAGED, D.SHOW_IN_CHART, D.SORT_ORDER, D.STATUS
|
||||
ORDER BY COALESCE(D.SORT_ORDER, 9999), D.DEPT_NAME
|
||||
</select>
|
||||
|
||||
<!-- 부서 단건 조회 -->
|
||||
@@ -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}
|
||||
</select>
|
||||
@@ -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()
|
||||
)
|
||||
</insert>
|
||||
@@ -76,8 +138,23 @@
|
||||
<update id="updateDepartment" parameterType="map">
|
||||
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}
|
||||
</update>
|
||||
|
||||
|
||||
@@ -8,22 +8,24 @@
|
||||
────────────────────────────────────────────────── -->
|
||||
<select id="getRoleGroupList" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
OBJID
|
||||
, AUTH_NAME
|
||||
, AUTH_CODE
|
||||
, COMPANY_CODE
|
||||
, STATUS
|
||||
, WRITER
|
||||
, CREATED_DATE
|
||||
FROM AUTHORITY_MASTER
|
||||
AM.OBJID
|
||||
, AM.AUTH_NAME
|
||||
, AM.AUTH_CODE
|
||||
, AM.COMPANY_CODE
|
||||
, AM.STATUS
|
||||
, AM.WRITER
|
||||
, AM.CREATED_DATE
|
||||
, (SELECT COUNT(*) FROM AUTHORITY_SUB_USER WHERE MASTER_OBJID = AM.OBJID) AS member_count
|
||||
, (SELECT COUNT(*) FROM AUTHORITY_SUB_MENU WHERE MASTER_OBJID = AM.OBJID) AS menu_count
|
||||
FROM AUTHORITY_MASTER AM
|
||||
WHERE 1=1
|
||||
<if test="company_code != null and company_code != ''">
|
||||
AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*')
|
||||
AND (AM.COMPANY_CODE = #{company_code} OR AM.COMPANY_CODE = '*')
|
||||
</if>
|
||||
<if test="search != null and search != ''">
|
||||
AND AUTH_NAME ILIKE '%' || #{search} || '%'
|
||||
AND AM.AUTH_NAME ILIKE '%' || #{search} || '%'
|
||||
</if>
|
||||
ORDER BY CREATED_DATE DESC
|
||||
ORDER BY AM.CREATED_DATE DESC
|
||||
</select>
|
||||
|
||||
<!-- 권한 그룹 단건 조회 -->
|
||||
@@ -40,7 +42,7 @@
|
||||
WHERE OBJID = #{objid}
|
||||
</select>
|
||||
|
||||
<!-- 권한 그룹 생성 (생성된 objid를 params에 반환) -->
|
||||
<!-- 권한 그룹 생성 -->
|
||||
<insert id="insertRoleGroup" parameterType="map">
|
||||
INSERT INTO AUTHORITY_MASTER (
|
||||
OBJID
|
||||
@@ -85,8 +87,10 @@
|
||||
</delete>
|
||||
|
||||
<!-- ──────────────────────────────────────────────────
|
||||
멤버 관리
|
||||
멤버 관리 (권한있는 직원)
|
||||
────────────────────────────────────────────────── -->
|
||||
|
||||
<!-- 권한있는 직원 목록 조회 -->
|
||||
<select id="getRoleMemberList" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
ASU.OBJID
|
||||
@@ -95,37 +99,97 @@
|
||||
, ASU.WRITER
|
||||
, ASU.CREATED_DATE
|
||||
, UI.USER_NAME
|
||||
, UI.DEPT_CODE
|
||||
, UI.DEPT_NAME
|
||||
, UI.POSITION_CODE
|
||||
, UI.POSITION_NAME
|
||||
, UI.EMAIL
|
||||
, UI.COMPANY_CODE
|
||||
FROM AUTHORITY_SUB_USER ASU
|
||||
LEFT JOIN USER_INFO UI ON ASU.USER_ID = UI.USER_ID
|
||||
WHERE ASU.MASTER_OBJID = #{master_objid}
|
||||
ORDER BY ASU.CREATED_DATE ASC
|
||||
</select>
|
||||
|
||||
<!-- 멤버 일괄 추가 -->
|
||||
<!-- 권한없는 직원 목록 조회 (해당 그룹의 비멤버, 같은 회사) -->
|
||||
<select id="getRoleNonMemberList" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
UI.USER_ID
|
||||
, UI.USER_NAME
|
||||
, UI.DEPT_CODE
|
||||
, UI.DEPT_NAME
|
||||
, UI.POSITION_CODE
|
||||
, UI.POSITION_NAME
|
||||
, UI.EMAIL
|
||||
, UI.COMPANY_CODE
|
||||
FROM USER_INFO UI
|
||||
WHERE UI.USER_ID NOT IN (
|
||||
SELECT USER_ID FROM AUTHORITY_SUB_USER WHERE MASTER_OBJID = #{master_objid}
|
||||
)
|
||||
<if test="company_code != null and company_code != '' and company_code != '*'">
|
||||
AND (UI.COMPANY_CODE = #{company_code} OR UI.COMPANY_CODE = '*')
|
||||
</if>
|
||||
<if test="status_active != null and status_active == true">
|
||||
AND (UI.STATUS IS NULL OR UI.STATUS = 'active')
|
||||
</if>
|
||||
ORDER BY UI.USER_NAME ASC
|
||||
</select>
|
||||
|
||||
<!-- 중복 체크 -->
|
||||
<select id="existsRoleMember" parameterType="map" resultType="int">
|
||||
SELECT COUNT(*)
|
||||
FROM AUTHORITY_SUB_USER
|
||||
WHERE MASTER_OBJID = #{master_objid}
|
||||
AND USER_ID = #{user_id}
|
||||
</select>
|
||||
|
||||
<!-- 멤버 일괄 추가 (userIds: List<String>) -->
|
||||
<insert id="insertRoleMembers" parameterType="map">
|
||||
INSERT INTO AUTHORITY_SUB_USER (
|
||||
MASTER_OBJID
|
||||
OBJID
|
||||
, MASTER_OBJID
|
||||
, USER_ID
|
||||
, WRITER
|
||||
, CREATED_DATE
|
||||
) VALUES
|
||||
<foreach collection="userIds" item="userId" separator=",">
|
||||
(#{master_objid}, #{user_id}, #{writer}, NOW())
|
||||
<foreach collection="userIds" item="user_id" separator=",">
|
||||
(#{master_objid} || '_' || #{user_id}, #{master_objid}, #{user_id}, #{writer}, NOW())
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<!-- 개별 멤버 추가 (중복 회피 — UPSERT 대신 NOT EXISTS) -->
|
||||
<insert id="insertSingleRoleMember" parameterType="map">
|
||||
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}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<!-- 지정 멤버 일괄 삭제 -->
|
||||
<delete id="deleteRoleMembers" parameterType="map">
|
||||
DELETE FROM AUTHORITY_SUB_USER
|
||||
WHERE MASTER_OBJID = #{master_objid}
|
||||
AND USER_ID IN
|
||||
<foreach collection="userIds" item="userId" open="(" separator="," close=")">
|
||||
<foreach collection="userIds" item="user_id" open="(" separator="," close=")">
|
||||
#{user_id}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
<!-- 개별 멤버 제거 -->
|
||||
<delete id="deleteSingleRoleMember" parameterType="map">
|
||||
DELETE FROM AUTHORITY_SUB_USER
|
||||
WHERE MASTER_OBJID = #{master_objid}
|
||||
AND USER_ID = #{user_id}
|
||||
</delete>
|
||||
|
||||
<!-- 그룹의 전체 멤버 삭제 -->
|
||||
<delete id="deleteAllRoleMembers" parameterType="map">
|
||||
DELETE FROM AUTHORITY_SUB_USER
|
||||
@@ -133,23 +197,47 @@
|
||||
</delete>
|
||||
|
||||
<!-- ──────────────────────────────────────────────────
|
||||
메뉴 권한 관리
|
||||
메뉴 권한 관리 (CRUD 세분화)
|
||||
────────────────────────────────────────────────── -->
|
||||
|
||||
<!-- 권한 그룹의 메뉴 권한 목록 조회 -->
|
||||
<select id="getMenuPermissionList" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
ASM.OBJID
|
||||
, ASM.MASTER_OBJID
|
||||
, ASM.MENU_OBJID
|
||||
, ASM.CREATE_YN
|
||||
, ASM.READ_YN
|
||||
, ASM.UPDATE_YN
|
||||
, ASM.DELETE_YN
|
||||
, ASM.WRITER
|
||||
, ASM.CREATED_DATE
|
||||
, MI.MENU_NAME
|
||||
, MI.MENU_NAME_KOR AS menu_name
|
||||
, MI.MENU_NAME_ENG
|
||||
, MI.MENU_URL
|
||||
, MI.LEV
|
||||
, MI.SORT_ORDER
|
||||
, MI.PARENT_OBJ_ID AS parent_objid
|
||||
, MI.SEQ AS sort_order
|
||||
, MI.MENU_TYPE
|
||||
, MI.COMPANY_CODE
|
||||
FROM AUTHORITY_SUB_MENU ASM
|
||||
INNER JOIN MENU_INFO MI ON ASM.MENU_OBJID = MI.OBJID
|
||||
WHERE ASM.MASTER_OBJID = #{auth_objid}
|
||||
ORDER BY MI.SORT_ORDER ASC
|
||||
ORDER BY MI.SEQ ASC
|
||||
</select>
|
||||
|
||||
<!-- 단건 조회 (upsert 확인용) -->
|
||||
<select id="getMenuPermissionByMenu" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
OBJID
|
||||
, MASTER_OBJID
|
||||
, MENU_OBJID
|
||||
, CREATE_YN
|
||||
, READ_YN
|
||||
, UPDATE_YN
|
||||
, DELETE_YN
|
||||
FROM AUTHORITY_SUB_MENU
|
||||
WHERE MASTER_OBJID = #{auth_objid}
|
||||
AND MENU_OBJID = #{menu_objid}
|
||||
</select>
|
||||
|
||||
<!-- 해당 권한 그룹의 메뉴 권한 전체 삭제 -->
|
||||
@@ -158,38 +246,95 @@
|
||||
WHERE MASTER_OBJID = #{auth_objid}
|
||||
</delete>
|
||||
|
||||
<!-- 메뉴 권한 일괄 추가 -->
|
||||
<!-- 메뉴 권한 일괄 추가 (permissions: List<Map>) -->
|
||||
<insert id="insertMenuPermissions" parameterType="map">
|
||||
INSERT INTO AUTHORITY_SUB_MENU (
|
||||
MASTER_OBJID
|
||||
OBJID
|
||||
, MASTER_OBJID
|
||||
, MENU_OBJID
|
||||
, CREATE_YN
|
||||
, READ_YN
|
||||
, UPDATE_YN
|
||||
, DELETE_YN
|
||||
, WRITER
|
||||
, CREATED_DATE
|
||||
) VALUES
|
||||
<foreach collection="permissions" item="perm" separator=",">
|
||||
(#{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()
|
||||
)
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<!-- 개별 메뉴 권한 upsert (체크 즉시 반영용) -->
|
||||
<insert id="upsertMenuPermission" parameterType="map">
|
||||
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()
|
||||
</insert>
|
||||
|
||||
<!-- 개별 메뉴 권한 삭제 (모든 CRUD N 이 되어 사실상 해제 시 row 삭제도 가능) -->
|
||||
<delete id="deleteSingleMenuPermission" parameterType="map">
|
||||
DELETE FROM AUTHORITY_SUB_MENU
|
||||
WHERE MASTER_OBJID = #{auth_objid}
|
||||
AND MENU_OBJID = #{menu_objid}
|
||||
</delete>
|
||||
|
||||
<!-- ──────────────────────────────────────────────────
|
||||
전체 메뉴 목록 (권한 설정용)
|
||||
전체 메뉴 목록 (권한 설정용, 트리 구조 원천)
|
||||
────────────────────────────────────────────────── -->
|
||||
<select id="getMenuList" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
OBJID
|
||||
, MENU_NAME
|
||||
, MENU_NAME_KOR AS menu_name
|
||||
, MENU_NAME_ENG
|
||||
, MENU_URL
|
||||
, PARENT_OBJID
|
||||
, PARENT_OBJ_ID AS parent_objid
|
||||
, COMPANY_CODE
|
||||
, LEV
|
||||
, SORT_ORDER
|
||||
, USE_YN
|
||||
, SEQ AS sort_order
|
||||
, MENU_TYPE
|
||||
, STATUS
|
||||
, MENU_ICON
|
||||
FROM MENU_INFO
|
||||
WHERE 1=1
|
||||
AND (STATUS IS NULL OR STATUS = 'active')
|
||||
<if test="company_code != null and company_code != ''">
|
||||
AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*')
|
||||
</if>
|
||||
ORDER BY SORT_ORDER ASC, LEV ASC
|
||||
ORDER BY SEQ ASC
|
||||
</select>
|
||||
|
||||
<!-- ──────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,952 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Building2,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
Globe,
|
||||
History,
|
||||
Info,
|
||||
Play,
|
||||
Plus,
|
||||
Search,
|
||||
Star,
|
||||
Upload,
|
||||
Users,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as departmentAPI from "@/lib/api/department";
|
||||
import { getCompanyList } from "@/lib/api/company";
|
||||
import type { Department, DepartmentMember } from "@/types/department";
|
||||
import type { Company } from "@/types/company";
|
||||
|
||||
interface DeptDetailDraft {
|
||||
dept_code: string;
|
||||
dept_name: string;
|
||||
parent_dept_code: string | null;
|
||||
dept_type: string;
|
||||
org_system: string;
|
||||
short_name: string;
|
||||
approval_manager: string;
|
||||
dept_manager: string;
|
||||
org_head: string;
|
||||
zipcode: string;
|
||||
address1: string;
|
||||
address2: string;
|
||||
status: "active" | "inactive";
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
erp_managed: "Y" | "N";
|
||||
show_in_chart: "Y" | "N";
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
const emptyDraft = (companyCode = ""): DeptDetailDraft => ({
|
||||
dept_code: "",
|
||||
dept_name: "",
|
||||
parent_dept_code: null,
|
||||
dept_type: "dept",
|
||||
org_system: "",
|
||||
short_name: "",
|
||||
approval_manager: "",
|
||||
dept_manager: "",
|
||||
org_head: "",
|
||||
zipcode: "",
|
||||
address1: "",
|
||||
address2: "",
|
||||
status: "active",
|
||||
start_date: new Date().toISOString().slice(0, 10),
|
||||
end_date: "",
|
||||
erp_managed: "Y",
|
||||
show_in_chart: "Y",
|
||||
sort_order: 10,
|
||||
});
|
||||
|
||||
export default function DeptMngListPage() {
|
||||
const { toast } = useToast();
|
||||
const { user } = useAuth();
|
||||
|
||||
// ── 회사 선택 / 기준일 ────────────────────────────────
|
||||
const [companies, setCompanies] = useState<Company[]>([]);
|
||||
const [selectedCompanyCode, setSelectedCompanyCode] = useState<string>("");
|
||||
const [periodMode, setPeriodMode] = useState<"all" | "date">("date");
|
||||
const [baseDate, setBaseDate] = useState<string>(new Date().toISOString().slice(0, 10));
|
||||
const [searchKeyword, setSearchKeyword] = useState("");
|
||||
|
||||
// ── 부서 트리 ─────────────────────────────────────────
|
||||
const [departments, setDepartments] = useState<Department[]>([]);
|
||||
const [expandedSet, setExpandedSet] = useState<Set<string>>(new Set());
|
||||
const [selectedCode, setSelectedCode] = useState<string | null>(null);
|
||||
const [isTreeLoading, setIsTreeLoading] = useState(false);
|
||||
|
||||
// ── 상세정보 ─────────────────────────────────────────
|
||||
const [draft, setDraft] = useState<DeptDetailDraft>(() => emptyDraft());
|
||||
const [originalDraft, setOriginalDraft] = useState<DeptDetailDraft | null>(null);
|
||||
const [isNewMode, setIsNewMode] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<"info" | "members">("info");
|
||||
const [members, setMembers] = useState<DepartmentMember[]>([]);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
|
||||
const selectedCompany = useMemo(
|
||||
() => companies.find((c) => c.company_code === selectedCompanyCode) || null,
|
||||
[companies, selectedCompanyCode],
|
||||
);
|
||||
|
||||
// ── 회사 목록 로드 (SUPER_ADMIN 은 전체, 그 외엔 본인 회사) ──
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const list = await getCompanyList();
|
||||
setCompanies(list);
|
||||
const userCompany = (user as any)?.company_code;
|
||||
if (userCompany && userCompany !== "*") {
|
||||
setSelectedCompanyCode(userCompany);
|
||||
} else if (list.length > 0) {
|
||||
setSelectedCompanyCode(list[0].company_code);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("회사 목록 로드 실패", err);
|
||||
}
|
||||
})();
|
||||
}, [user]);
|
||||
|
||||
// ── 부서 목록 로드 ───────────────────────────────────
|
||||
const loadDepartments = useCallback(async () => {
|
||||
if (!selectedCompanyCode) return;
|
||||
setIsTreeLoading(true);
|
||||
try {
|
||||
const res = await departmentAPI.getDepartments(selectedCompanyCode);
|
||||
if (res.success && (res as any).data) {
|
||||
setDepartments((res as any).data);
|
||||
} else {
|
||||
setDepartments([]);
|
||||
}
|
||||
} finally {
|
||||
setIsTreeLoading(false);
|
||||
}
|
||||
}, [selectedCompanyCode]);
|
||||
|
||||
useEffect(() => {
|
||||
loadDepartments();
|
||||
}, [loadDepartments]);
|
||||
|
||||
// ── 부서원 로드 ──────────────────────────────────────
|
||||
useEffect(() => {
|
||||
if (activeTab !== "members" || !selectedCode || isNewMode) {
|
||||
setMembers([]);
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
const res = await departmentAPI.getDepartmentMembers(selectedCode);
|
||||
if (res.success && (res as any).data) setMembers((res as any).data);
|
||||
})();
|
||||
}, [activeTab, selectedCode, isNewMode]);
|
||||
|
||||
// ── 트리 구성 ────────────────────────────────────────
|
||||
const filteredDepts = useMemo(() => {
|
||||
if (!searchKeyword.trim()) return departments;
|
||||
const kw = searchKeyword.toLowerCase();
|
||||
return departments.filter(
|
||||
(d) =>
|
||||
d.dept_name?.toLowerCase().includes(kw) ||
|
||||
d.dept_code?.toLowerCase().includes(kw),
|
||||
);
|
||||
}, [departments, searchKeyword]);
|
||||
|
||||
const childrenOf = useCallback(
|
||||
(parent: string | null) =>
|
||||
filteredDepts
|
||||
.filter((d) => (d.parent_dept_code ?? null) === parent)
|
||||
.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0) || (a.dept_name || "").localeCompare(b.dept_name || "")),
|
||||
[filteredDepts],
|
||||
);
|
||||
|
||||
const expandAll = () => {
|
||||
setExpandedSet(new Set(filteredDepts.map((d) => d.dept_code)));
|
||||
};
|
||||
const collapseAll = () => setExpandedSet(new Set());
|
||||
|
||||
const toggleExpand = (code: string) => {
|
||||
setExpandedSet((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.has(code) ? next.delete(code) : next.add(code);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// ── 선택 시 상세정보 채우기 ───────────────────────────
|
||||
const handleSelectDepartment = (dept: Department) => {
|
||||
setSelectedCode(dept.dept_code);
|
||||
setIsNewMode(false);
|
||||
const loaded: DeptDetailDraft = {
|
||||
...emptyDraft(selectedCompanyCode),
|
||||
dept_code: dept.dept_code,
|
||||
dept_name: dept.dept_name,
|
||||
parent_dept_code: dept.parent_dept_code ?? null,
|
||||
short_name: dept.short_name ?? "",
|
||||
dept_type: dept.dept_type ?? "dept",
|
||||
org_system: dept.org_system ?? "",
|
||||
approval_manager: dept.approval_manager ?? "",
|
||||
dept_manager: dept.dept_manager ?? "",
|
||||
org_head: dept.org_head ?? "",
|
||||
zipcode: dept.zipcode ?? "",
|
||||
address1: dept.address1 ?? "",
|
||||
address2: dept.address2 ?? "",
|
||||
start_date: (dept.start_date ?? "").slice(0, 10),
|
||||
end_date: (dept.end_date ?? "").slice(0, 10),
|
||||
erp_managed: (dept.erp_managed as "Y" | "N") ?? "Y",
|
||||
show_in_chart: (dept.show_in_chart as "Y" | "N") ?? "Y",
|
||||
sort_order: dept.sort_order ?? 10,
|
||||
status: (dept.status as "active" | "inactive") ?? "active",
|
||||
};
|
||||
setDraft(loaded);
|
||||
setOriginalDraft(loaded);
|
||||
};
|
||||
|
||||
const handleAddNew = (parentCode: string | null = null) => {
|
||||
setSelectedCode(null);
|
||||
setIsNewMode(true);
|
||||
setActiveTab("info");
|
||||
setDraft({ ...emptyDraft(selectedCompanyCode), parent_dept_code: parentCode });
|
||||
setOriginalDraft(null);
|
||||
};
|
||||
|
||||
const handleClearDetail = () => {
|
||||
setSelectedCode(null);
|
||||
setIsNewMode(false);
|
||||
setDraft(emptyDraft(selectedCompanyCode));
|
||||
setOriginalDraft(null);
|
||||
};
|
||||
|
||||
// ── 저장 ─────────────────────────────────────────────
|
||||
const handleSave = async () => {
|
||||
if (!draft.dept_name.trim()) {
|
||||
toast({ title: "부서명을 입력해주세요", variant: "destructive" });
|
||||
return;
|
||||
}
|
||||
if (!selectedCompanyCode) {
|
||||
toast({ title: "회사를 선택해주세요", variant: "destructive" });
|
||||
return;
|
||||
}
|
||||
|
||||
// 기본정보 탭 전체 필드를 payload 로 전달 — DepartmentFormData 와 1:1
|
||||
const payload = {
|
||||
dept_name: draft.dept_name,
|
||||
parent_dept_code: draft.parent_dept_code,
|
||||
short_name: draft.short_name,
|
||||
dept_type: draft.dept_type,
|
||||
org_system: draft.org_system || null,
|
||||
approval_manager: draft.approval_manager,
|
||||
dept_manager: draft.dept_manager,
|
||||
org_head: draft.org_head,
|
||||
zipcode: draft.zipcode,
|
||||
address1: draft.address1,
|
||||
address2: draft.address2,
|
||||
start_date: draft.start_date || null,
|
||||
end_date: draft.end_date || null,
|
||||
erp_managed: draft.erp_managed,
|
||||
show_in_chart: draft.show_in_chart,
|
||||
sort_order: draft.sort_order,
|
||||
status: draft.status,
|
||||
};
|
||||
|
||||
try {
|
||||
if (isNewMode) {
|
||||
const res = await departmentAPI.createDepartment(selectedCompanyCode, payload);
|
||||
if (res.success) {
|
||||
toast({ title: "부서가 생성되었습니다" });
|
||||
await loadDepartments();
|
||||
const created = (res as any).data as Department | undefined;
|
||||
if (created) handleSelectDepartment(created);
|
||||
else handleClearDetail();
|
||||
} else {
|
||||
toast({ title: "생성 실패", description: (res as any).error, variant: "destructive" });
|
||||
}
|
||||
} else if (selectedCode) {
|
||||
const res = await departmentAPI.updateDepartment(selectedCode, payload);
|
||||
if (res.success) {
|
||||
toast({ title: "부서가 수정되었습니다" });
|
||||
await loadDepartments();
|
||||
setOriginalDraft(draft);
|
||||
} else {
|
||||
toast({ title: "수정 실패", description: (res as any).error, variant: "destructive" });
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast({ title: "오류", description: err?.message, variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
// ── 삭제 ─────────────────────────────────────────────
|
||||
const handleDelete = async () => {
|
||||
if (!selectedCode) return;
|
||||
try {
|
||||
const res = await departmentAPI.deleteDepartment(selectedCode);
|
||||
if (res.success) {
|
||||
toast({ title: "부서가 삭제되었습니다" });
|
||||
await loadDepartments();
|
||||
handleClearDetail();
|
||||
} else {
|
||||
toast({ title: "삭제 실패", description: (res as any).error, variant: "destructive" });
|
||||
}
|
||||
} finally {
|
||||
setDeleteConfirmOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isDirty = originalDraft
|
||||
? JSON.stringify(originalDraft) !== JSON.stringify(draft)
|
||||
: isNewMode && (draft.dept_name.trim() !== "" || draft.parent_dept_code !== null);
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// 렌더
|
||||
// ─────────────────────────────────────────────────────
|
||||
return (
|
||||
<div className="flex h-full min-h-0 w-full flex-col bg-background text-sm">
|
||||
{/* 상단 타이틀 바 */}
|
||||
<div className="flex items-center justify-between border-b px-5 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-xl font-bold tracking-tight">부서관리</h1>
|
||||
<Button size="icon" variant="ghost" className="h-7 w-7 text-sky-600">
|
||||
<Info className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" className="h-7 w-7 text-sky-600">
|
||||
<Play className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="h-8 gap-1.5 text-xs">
|
||||
<Upload className="h-3.5 w-3.5" />
|
||||
일괄등록
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="h-8 gap-1.5 text-xs" onClick={() => handleAddNew(null)}>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
추가
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="h-8 gap-1.5 text-xs">
|
||||
<History className="h-3.5 w-3.5" />
|
||||
변경이력
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<Star className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 안내 배너 */}
|
||||
<div className="flex items-center gap-2 border-b bg-sky-50/60 px-5 py-2 text-xs text-sky-800 dark:bg-sky-950/30 dark:text-sky-200">
|
||||
<Info className="h-3.5 w-3.5 shrink-0" />
|
||||
<span>회사별 조직도(부서)를 등록할 수 있으며, '부서/팀/임시' 유형을 선택하여 등록할 수 있습니다.</span>
|
||||
<Button variant="ghost" size="icon" className="ml-auto h-5 w-5">
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 본문 */}
|
||||
<div className="flex min-h-0 flex-1">
|
||||
{/* 좌측 트리 패널 */}
|
||||
<aside className="flex w-[340px] shrink-0 flex-col border-r">
|
||||
{/* 기준일 / 회사 / 검색 */}
|
||||
<div className="space-y-3 border-b p-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="w-[60px] shrink-0 text-xs font-semibold">사용기간</Label>
|
||||
<RadioGroup
|
||||
value={periodMode}
|
||||
onValueChange={(v) => setPeriodMode(v as "all" | "date")}
|
||||
className="flex items-center gap-3"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<RadioGroupItem value="all" id="period-all" className="h-3.5 w-3.5" />
|
||||
<Label htmlFor="period-all" className="text-xs">전체</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<RadioGroupItem value="date" id="period-date" className="h-3.5 w-3.5" />
|
||||
<Label htmlFor="period-date" className="text-xs">기준일</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<Input
|
||||
type="date"
|
||||
value={baseDate}
|
||||
onChange={(e) => setBaseDate(e.target.value)}
|
||||
disabled={periodMode !== "date"}
|
||||
className="h-7 flex-1 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select value={selectedCompanyCode} onValueChange={setSelectedCompanyCode}>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="회사 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{companies.map((c) => (
|
||||
<SelectItem key={c.company_code} value={c.company_code}>
|
||||
{c.company_code}. {c.company_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
placeholder="코드/사업장/부서명을 입력하세요."
|
||||
className="h-8 pl-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-1 text-xs">
|
||||
<Button variant="ghost" size="sm" className="h-6 px-2 text-xs text-muted-foreground" onClick={expandAll}>
|
||||
전체 펼치기 <ChevronDown className="ml-0.5 h-3 w-3" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-6 px-2 text-xs text-muted-foreground" onClick={collapseAll}>
|
||||
필터 <ChevronDown className="ml-0.5 h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 트리 */}
|
||||
<div className="flex-1 overflow-auto p-2">
|
||||
{isTreeLoading ? (
|
||||
<div className="py-6 text-center text-xs text-muted-foreground">로딩 중...</div>
|
||||
) : !selectedCompany ? (
|
||||
<div className="py-6 text-center text-xs text-muted-foreground">회사를 선택하세요</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 회사 루트 노드 */}
|
||||
<div className="flex items-center gap-1.5 rounded px-1.5 py-1 text-xs font-bold text-sky-700 dark:text-sky-300">
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
<Building2 className="h-3.5 w-3.5" />
|
||||
<span>{selectedCompany.company_code}. {selectedCompany.company_name}</span>
|
||||
</div>
|
||||
{/* 사업장 (현재는 회사=사업장 1:1 가정) */}
|
||||
<div className="ml-3">
|
||||
<div className="flex items-center gap-1.5 rounded px-1.5 py-1 text-xs font-semibold text-sky-700 dark:text-sky-300">
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
<Building2 className="h-3.5 w-3.5" />
|
||||
<span>{selectedCompany.company_code}. {selectedCompany.company_name}</span>
|
||||
</div>
|
||||
|
||||
{/* 부서 트리 */}
|
||||
<div className="ml-3">
|
||||
<DeptTree
|
||||
items={childrenOf(null)}
|
||||
allDepts={filteredDepts}
|
||||
expanded={expandedSet}
|
||||
selectedCode={selectedCode}
|
||||
onToggle={toggleExpand}
|
||||
onSelect={handleSelectDepartment}
|
||||
/>
|
||||
{childrenOf(null).length === 0 && (
|
||||
<div className="px-2 py-3 text-xs text-muted-foreground">등록된 부서가 없습니다.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* 우측 상세 패널 */}
|
||||
<section className="flex min-w-0 flex-1 flex-col">
|
||||
{/* 상세 헤더 */}
|
||||
<div className="flex items-center justify-between border-b px-5 py-2.5">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold">
|
||||
<span className="inline-block h-1.5 w-1.5 rounded-full bg-sky-500" />
|
||||
상세정보
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 bg-sky-500 text-xs text-white hover:bg-sky-600"
|
||||
onClick={handleSave}
|
||||
disabled={!isNewMode && !selectedCode}
|
||||
>
|
||||
저장
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 text-xs"
|
||||
onClick={() => setDeleteConfirmOpen(true)}
|
||||
disabled={isNewMode || !selectedCode}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={handleClearDetail}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 탭 */}
|
||||
<div className="flex items-center gap-5 border-b px-5">
|
||||
<button
|
||||
className={cn(
|
||||
"relative py-2.5 text-sm font-medium transition-colors",
|
||||
activeTab === "info" ? "text-sky-600 dark:text-sky-400" : "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
onClick={() => setActiveTab("info")}
|
||||
>
|
||||
기본정보
|
||||
{activeTab === "info" && <span className="absolute inset-x-0 -bottom-px h-0.5 bg-sky-500" />}
|
||||
</button>
|
||||
<button
|
||||
className={cn(
|
||||
"relative py-2.5 text-sm font-medium transition-colors",
|
||||
activeTab === "members" ? "text-sky-600 dark:text-sky-400" : "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
onClick={() => setActiveTab("members")}
|
||||
disabled={isNewMode}
|
||||
>
|
||||
부서원 정보
|
||||
{activeTab === "members" && <span className="absolute inset-x-0 -bottom-px h-0.5 bg-sky-500" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 탭 바디 */}
|
||||
<div className="min-h-0 flex-1 overflow-auto p-6">
|
||||
{activeTab === "info" ? (
|
||||
<BasicInfoForm
|
||||
draft={draft}
|
||||
setDraft={setDraft}
|
||||
companyLabel={
|
||||
selectedCompany ? `${selectedCompany.company_code}. ${selectedCompany.company_name}` : ""
|
||||
}
|
||||
parentLabel={
|
||||
draft.parent_dept_code
|
||||
? (departments.find((d) => d.dept_code === draft.parent_dept_code)?.dept_name ?? draft.parent_dept_code)
|
||||
: "-"
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<MembersPanel members={members} />
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* 삭제 확인 */}
|
||||
<Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
||||
<DialogContent className="max-w-[420px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>부서 삭제</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-sm">
|
||||
<span className="font-semibold">{draft.dept_name}</span> 부서를 삭제하시겠습니까?
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">부서원은 자동으로 제외됩니다. 하위 부서가 있으면 삭제할 수 없습니다.</p>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDeleteConfirmOpen(false)}>취소</Button>
|
||||
<Button variant="destructive" onClick={handleDelete}>삭제</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────
|
||||
// 트리 (재귀)
|
||||
// ───────────────────────────────────────────────────────
|
||||
function DeptTree({
|
||||
items,
|
||||
allDepts,
|
||||
expanded,
|
||||
selectedCode,
|
||||
onToggle,
|
||||
onSelect,
|
||||
}: {
|
||||
items: Department[];
|
||||
allDepts: Department[];
|
||||
expanded: Set<string>;
|
||||
selectedCode: string | null;
|
||||
onToggle: (code: string) => void;
|
||||
onSelect: (d: Department) => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
{items.map((dept) => {
|
||||
const sub = allDepts.filter((d) => d.parent_dept_code === dept.dept_code);
|
||||
const hasSub = sub.length > 0;
|
||||
const isOpen = expanded.has(dept.dept_code);
|
||||
const isActive = selectedCode === dept.dept_code;
|
||||
|
||||
return (
|
||||
<div key={dept.dept_code}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center gap-1 rounded px-1.5 py-1 text-xs",
|
||||
isActive ? "bg-sky-100 font-semibold text-sky-700 dark:bg-sky-900/40 dark:text-sky-300" : "hover:bg-muted",
|
||||
)}
|
||||
onClick={() => onSelect(dept)}
|
||||
>
|
||||
{hasSub ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle(dept.dept_code);
|
||||
}}
|
||||
className="flex h-3.5 w-3.5 items-center justify-center"
|
||||
>
|
||||
{isOpen ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
||||
</button>
|
||||
) : (
|
||||
<span className="inline-block h-3.5 w-3.5" />
|
||||
)}
|
||||
{hasSub && isOpen ? (
|
||||
<FolderOpen className="h-3.5 w-3.5 text-amber-500" />
|
||||
) : (
|
||||
<Folder className="h-3.5 w-3.5 text-amber-500" />
|
||||
)}
|
||||
<span className="truncate">
|
||||
{dept.dept_code}. {dept.dept_name}
|
||||
</span>
|
||||
{typeof dept.member_count === "number" && dept.member_count > 0 && (
|
||||
<span className="ml-auto flex items-center gap-0.5 text-[10px] text-muted-foreground">
|
||||
<Users className="h-2.5 w-2.5" /> {dept.member_count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{hasSub && isOpen && (
|
||||
<div className="ml-4">
|
||||
<DeptTree
|
||||
items={sub}
|
||||
allDepts={allDepts}
|
||||
expanded={expanded}
|
||||
selectedCode={selectedCode}
|
||||
onToggle={onToggle}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────
|
||||
// 기본정보 폼
|
||||
// ───────────────────────────────────────────────────────
|
||||
function BasicInfoForm({
|
||||
draft,
|
||||
setDraft,
|
||||
companyLabel,
|
||||
parentLabel,
|
||||
}: {
|
||||
draft: DeptDetailDraft;
|
||||
setDraft: React.Dispatch<React.SetStateAction<DeptDetailDraft>>;
|
||||
companyLabel: string;
|
||||
parentLabel: string;
|
||||
}) {
|
||||
const update = <K extends keyof DeptDetailDraft>(key: K, value: DeptDetailDraft[K]) =>
|
||||
setDraft((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<div className="grid grid-cols-[120px_1fr] gap-x-4 gap-y-0 rounded-md border bg-card">
|
||||
<Row label="회사">
|
||||
<div className="py-1 text-sm">{companyLabel || "-"}</div>
|
||||
</Row>
|
||||
<Row label="사업장">
|
||||
<div className="py-1 text-sm">{companyLabel || "-"}</div>
|
||||
</Row>
|
||||
|
||||
<Row label="상위부서">
|
||||
<div className="flex items-center gap-1">
|
||||
<Input value={parentLabel} readOnly className="h-8 flex-1 bg-muted/30 text-sm" />
|
||||
<Button variant="outline" size="icon" className="h-8 w-8" type="button">
|
||||
<Building2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<Row label="부서코드">
|
||||
<Input
|
||||
value={draft.dept_code}
|
||||
onChange={(e) => update("dept_code", e.target.value)}
|
||||
placeholder="저장 시 자동 부여 (DEPT_n)"
|
||||
className="h-8 text-sm"
|
||||
readOnly={!!draft.dept_code}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
<Row label="부서유형">
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={draft.dept_type} onValueChange={(v) => update("dept_type", v)}>
|
||||
<SelectTrigger className="h-8 w-[140px] text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="dept">부서</SelectItem>
|
||||
<SelectItem value="team">팀</SelectItem>
|
||||
<SelectItem value="temp">임시</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={draft.org_system || "none"} onValueChange={(v) => update("org_system", v === "none" ? "" : v)}>
|
||||
<SelectTrigger className="h-8 w-[180px] text-sm">
|
||||
<SelectValue placeholder="조직체계 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">조직체계 선택</SelectItem>
|
||||
<SelectItem value="hr">인사조직</SelectItem>
|
||||
<SelectItem value="sales">영업조직</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<Row label="부서명" required>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
value={draft.dept_name}
|
||||
onChange={(e) => update("dept_name", e.target.value)}
|
||||
className="h-8 flex-1 bg-rose-50/40 text-sm dark:bg-rose-950/20"
|
||||
placeholder="부서명을 입력하세요"
|
||||
/>
|
||||
<Button variant="outline" size="icon" className="h-8 w-8" type="button" title="다국어">
|
||||
<Globe className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<Row label="부서약칭">
|
||||
<Input
|
||||
value={draft.short_name}
|
||||
onChange={(e) => update("short_name", e.target.value)}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</Row>
|
||||
|
||||
<Row label="결재 관리자" hint>
|
||||
<PickerField
|
||||
value={draft.approval_manager}
|
||||
onChange={(v) => update("approval_manager", v)}
|
||||
placeholder="사용자 이름을 입력해주세요."
|
||||
/>
|
||||
</Row>
|
||||
<Row label="부서 관리자">
|
||||
<PickerField
|
||||
value={draft.dept_manager}
|
||||
onChange={(v) => update("dept_manager", v)}
|
||||
placeholder="사용자 이름을 입력해주세요."
|
||||
/>
|
||||
</Row>
|
||||
<Row label="조직장" hint>
|
||||
<PickerField
|
||||
value={draft.org_head}
|
||||
onChange={(v) => update("org_head", v)}
|
||||
placeholder="사용자 이름을 입력해주세요."
|
||||
/>
|
||||
</Row>
|
||||
|
||||
<Row label="부서주소">
|
||||
<div className="space-y-1">
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
value={draft.zipcode}
|
||||
onChange={(e) => update("zipcode", e.target.value)}
|
||||
className="h-8 w-[100px] text-sm"
|
||||
/>
|
||||
<Button variant="outline" size="sm" className="h-8 text-xs" type="button">우편번호</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={draft.address1}
|
||||
onChange={(e) => update("address1", e.target.value)}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
<Input
|
||||
value={draft.address2}
|
||||
onChange={(e) => update("address2", e.target.value)}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<Row label="사용여부">
|
||||
<RadioGroup
|
||||
value={draft.status}
|
||||
onValueChange={(v) => update("status", v as "active" | "inactive")}
|
||||
className="flex items-center gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<RadioGroupItem value="active" id="status-active" className="h-3.5 w-3.5" />
|
||||
<Label htmlFor="status-active" className="text-sm">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<RadioGroupItem value="inactive" id="status-inactive" className="h-3.5 w-3.5" />
|
||||
<Label htmlFor="status-inactive" className="text-sm">미사용</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</Row>
|
||||
|
||||
<Row label="시작일">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Input
|
||||
type="date"
|
||||
value={draft.start_date}
|
||||
onChange={(e) => update("start_date", e.target.value)}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="w-[50px] text-xs">종료일</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={draft.end_date}
|
||||
onChange={(e) => update("end_date", e.target.value)}
|
||||
className="h-8 flex-1 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<Row label="ERP관리부서" hint>
|
||||
<div className="flex items-center gap-8">
|
||||
<RadioGroup
|
||||
value={draft.erp_managed}
|
||||
onValueChange={(v) => update("erp_managed", v as "Y" | "N")}
|
||||
className="flex items-center gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<RadioGroupItem value="Y" id="erp-y" className="h-3.5 w-3.5" />
|
||||
<Label htmlFor="erp-y" className="text-sm">설정</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<RadioGroupItem value="N" id="erp-n" className="h-3.5 w-3.5" />
|
||||
<Label htmlFor="erp-n" className="text-sm">미설정</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-xs text-muted-foreground">조직도표시</Label>
|
||||
<RadioGroup
|
||||
value={draft.show_in_chart}
|
||||
onValueChange={(v) => update("show_in_chart", v as "Y" | "N")}
|
||||
className="flex items-center gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<RadioGroupItem value="Y" id="chart-y" className="h-3.5 w-3.5" />
|
||||
<Label htmlFor="chart-y" className="text-sm">표시</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<RadioGroupItem value="N" id="chart-n" className="h-3.5 w-3.5" />
|
||||
<Label htmlFor="chart-n" className="text-sm">미표시</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<Row label="정렬">
|
||||
<Input
|
||||
type="number"
|
||||
value={draft.sort_order}
|
||||
onChange={(e) => update("sort_order", Number(e.target.value) || 0)}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({
|
||||
label,
|
||||
children,
|
||||
required,
|
||||
hint,
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
required?: boolean;
|
||||
hint?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-1 border-b bg-muted/30 px-3 py-2.5 text-xs font-medium last:border-b-0">
|
||||
{hint && <Info className="h-3 w-3 text-sky-500" />}
|
||||
<span>{label}</span>
|
||||
{required && <span className="text-destructive">*</span>}
|
||||
</div>
|
||||
<div className="flex items-center border-b px-3 py-1.5 last:border-b-0">{children}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PickerField({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="h-8 flex-1 text-sm"
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<Button variant="outline" size="icon" className="h-8 w-8" type="button">
|
||||
<Users className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────
|
||||
// 부서원 패널
|
||||
// ───────────────────────────────────────────────────────
|
||||
function MembersPanel({ members }: { members: DepartmentMember[] }) {
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">부서원 {members.length}명</div>
|
||||
</div>
|
||||
<div className="divide-y rounded-md border bg-card">
|
||||
{members.length === 0 ? (
|
||||
<div className="py-10 text-center text-xs text-muted-foreground">부서원이 없습니다.</div>
|
||||
) : (
|
||||
members.map((m) => (
|
||||
<div key={m.user_id} className="flex items-center justify-between px-4 py-2.5">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="font-medium">{m.user_name}</span>
|
||||
<span className="text-xs text-muted-foreground">({m.user_id})</span>
|
||||
{m.is_primary && (
|
||||
<Badge variant="default" className="h-4 gap-0.5 px-1.5 text-[10px]">
|
||||
<Star className="h-2.5 w-2.5" />주부서
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-0.5 flex gap-3 text-[11px] text-muted-foreground">
|
||||
{m.position_name && <span>{m.position_name}</span>}
|
||||
{m.email && <span>{m.email}</span>}
|
||||
{m.phone && <span>{m.phone}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<any[]>([]);
|
||||
const [roleGroups, setRoleGroups] = useState<RoleGroup[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [paginationInfo, setPaginationInfo] = useState({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
totalItems: 0,
|
||||
totalPages: 0,
|
||||
|
||||
// 회사 필터 (최고 관리자 전용)
|
||||
const [companies, setCompanies] = useState<Array<{ company_code: string; company_name: string }>>([]);
|
||||
const [selectedCompany, setSelectedCompany] = useState<string>("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 (
|
||||
<div className="bg-background flex min-h-screen flex-col">
|
||||
<div className="flex min-h-screen flex-col bg-background">
|
||||
<div className="space-y-6 p-6">
|
||||
<div className="space-y-2 border-b pb-4">
|
||||
<h1 className="text-3xl font-bold tracking-tight">사용자 권한 관리</h1>
|
||||
<p className="text-muted-foreground text-sm">사용자별 권한 레벨을 관리합니다. (최고 관리자 전용)</p>
|
||||
<h1 className="text-3xl font-bold tracking-tight">권한 그룹 관리</h1>
|
||||
<p className="text-sm text-muted-foreground">회사 내 권한 그룹을 생성하고 멤버를 관리합니다 (회사 관리자 이상)</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border p-6 shadow-sm">
|
||||
<AlertCircle className="text-destructive mb-4 h-12 w-12" />
|
||||
<h3 className="mb-2 text-lg font-semibold">접근 권한 없음</h3>
|
||||
<p className="text-muted-foreground mb-4 text-center text-sm">
|
||||
권한 관리는 최고 관리자만 접근할 수 있습니다.
|
||||
권한 그룹 관리는 회사 관리자 이상만 접근할 수 있습니다.
|
||||
</p>
|
||||
<Button variant="outline" onClick={() => window.history.back()}>
|
||||
뒤로 가기
|
||||
@@ -131,12 +180,12 @@ export default function UserAuthPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-background flex min-h-screen flex-col">
|
||||
<div className="flex min-h-screen flex-col bg-background">
|
||||
<div className="space-y-6 p-6">
|
||||
{/* 페이지 헤더 */}
|
||||
<div className="space-y-2 border-b pb-4">
|
||||
<h1 className="text-3xl font-bold tracking-tight">사용자 권한 관리</h1>
|
||||
<p className="text-muted-foreground text-sm">사용자별 권한 레벨을 관리합니다. (최고 관리자 전용)</p>
|
||||
<h1 className="text-3xl font-bold tracking-tight">권한 그룹 관리</h1>
|
||||
<p className="text-sm text-muted-foreground">회사 내 권한 그룹을 생성하고 멤버를 관리합니다 (회사 관리자 이상)</p>
|
||||
</div>
|
||||
|
||||
{/* 에러 메시지 */}
|
||||
@@ -156,21 +205,154 @@ export default function UserAuthPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 사용자 권한 테이블 */}
|
||||
<UserAuthTable
|
||||
users={users}
|
||||
isLoading={isLoading}
|
||||
paginationInfo={paginationInfo}
|
||||
onEditAuth={handleEditAuth}
|
||||
onPageChange={handlePageChange}
|
||||
{/* 액션 버튼 영역 */}
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<h2 className="text-xl font-semibold">권한 그룹 목록 ({roleGroups.length})</h2>
|
||||
|
||||
{/* 최고 관리자 전용: 회사 필터 */}
|
||||
{isSuperAdmin && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="text-muted-foreground h-4 w-4" />
|
||||
<Select value={selectedCompany} onValueChange={(value) => setSelectedCompany(value)}>
|
||||
<SelectTrigger className="h-10 w-[200px]">
|
||||
<SelectValue placeholder="회사 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">전체 회사</SelectItem>
|
||||
{companies.map((company) => (
|
||||
<SelectItem key={company.company_code} value={company.company_code}>
|
||||
{company.company_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedCompany !== "all" && (
|
||||
<Button variant="ghost" size="sm" onClick={() => setSelectedCompany("all")} className="h-8 w-8 p-0">
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button onClick={handleCreateRole} className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
권한 그룹 생성
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 권한 그룹 목록 */}
|
||||
{isLoading ? (
|
||||
<div className="bg-card rounded-lg border p-12 shadow-sm">
|
||||
<div className="flex flex-col items-center justify-center gap-4">
|
||||
<div className="border-primary h-8 w-8 animate-spin rounded-full border-4 border-t-transparent"></div>
|
||||
<p className="text-muted-foreground text-sm">권한 그룹 목록을 불러오는 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : roleGroups.length === 0 ? (
|
||||
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border shadow-sm">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<p className="text-muted-foreground text-sm">등록된 권한 그룹이 없습니다.</p>
|
||||
<p className="text-muted-foreground text-xs">권한 그룹을 생성하여 멤버를 관리해보세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{roleGroups.map((role) => (
|
||||
<div key={role.objid} className="bg-card rounded-lg border shadow-sm transition-colors">
|
||||
{/* 헤더 (클릭 시 상세 페이지) */}
|
||||
<div
|
||||
className="hover:bg-muted/50 cursor-pointer p-4 transition-colors"
|
||||
onClick={() => handleViewDetail(role)}
|
||||
>
|
||||
<div className="mb-4 flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold">{role.auth_name}</h3>
|
||||
<p className="text-muted-foreground mt-1 font-mono text-sm">{role.auth_code}</p>
|
||||
</div>
|
||||
<span
|
||||
className={`rounded-full px-2 py-1 text-xs font-medium ${
|
||||
role.status === "active" ? "bg-emerald-100 text-emerald-800" : "bg-muted text-foreground"
|
||||
}`}
|
||||
>
|
||||
{role.status === "active" ? "활성" : "비활성"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 정보 */}
|
||||
<div className="space-y-2 border-t pt-4">
|
||||
{/* 최고 관리자는 회사명 표시 */}
|
||||
{isSuperAdmin && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">회사</span>
|
||||
<span className="font-medium">
|
||||
{companies.find((c) => c.company_code === role.company_code)?.company_name || role.company_code}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground flex items-center gap-1">
|
||||
<Users className="h-3 w-3" />
|
||||
멤버 수
|
||||
</span>
|
||||
<span className="font-medium">{role.member_count || 0}명</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground flex items-center gap-1">
|
||||
<Menu className="h-3 w-3" />
|
||||
메뉴 권한
|
||||
</span>
|
||||
<span className="font-medium">{role.menu_count || 0}개</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 액션 버튼 */}
|
||||
<div className="flex gap-2 border-t p-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditRole(role);
|
||||
}}
|
||||
className="flex-1 gap-1 text-xs"
|
||||
>
|
||||
<Edit className="h-3 w-3" />
|
||||
수정
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteRole(role);
|
||||
}}
|
||||
className="text-destructive hover:bg-destructive hover:text-destructive-foreground gap-1 text-xs"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 모달들 */}
|
||||
<RoleFormModal
|
||||
isOpen={formModal.isOpen}
|
||||
onClose={handleFormModalClose}
|
||||
onSuccess={handleModalSuccess}
|
||||
editingRole={formModal.editingRole}
|
||||
/>
|
||||
|
||||
{/* 권한 변경 모달 */}
|
||||
<UserAuthEditModal
|
||||
isOpen={authEditModal.isOpen}
|
||||
onClose={handleAuthEditClose}
|
||||
onSuccess={handleAuthEditSuccess}
|
||||
user={authEditModal.user}
|
||||
<RoleDeleteModal
|
||||
isOpen={deleteModal.isOpen}
|
||||
onClose={handleDeleteModalClose}
|
||||
onSuccess={handleModalSuccess}
|
||||
role={deleteModal.role}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -179,3 +361,4 @@ export default function UserAuthPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ interface UserAuthEditModalProps {
|
||||
/**
|
||||
* 사용자 권한 변경 모달
|
||||
*
|
||||
* 권한 레벨만 변경 가능 (최고 관리자 전용)
|
||||
* 권한 레벨만 변경 가능 (관리자 이상 전용)
|
||||
*/
|
||||
export function UserAuthEditModal({ isOpen, onClose, onSuccess, user }: UserAuthEditModalProps) {
|
||||
const [selectedUserType, setSelectedUserType] = useState<string>("");
|
||||
|
||||
@@ -65,6 +65,7 @@ const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
|
||||
"/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 }),
|
||||
|
||||
@@ -850,17 +850,19 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
|
||||
const uiMenus = user ? convertMenuToUI(currentMenus, user as ExtendedUserInfo) : [];
|
||||
|
||||
// 활성 탭에 해당하는 메뉴가 속한 부모 메뉴 자동 확장
|
||||
// 활성 탭이 바뀔 때 한 번만 부모 메뉴 자동 확장.
|
||||
// expandedMenus 를 의존성에 넣으면 사용자가 수동으로 닫은 즉시 다시 펼쳐져 "닫히지 않는" 버그가 남.
|
||||
const autoExpandedForTabRef = useRef<string | number | null>(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 (
|
||||
|
||||
@@ -286,4 +286,93 @@ export const roleAPI = {
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 권한 그룹 통합 워크스페이스 조회
|
||||
* 권한 그룹 선택 시 필요한 모든 정보 한 번에 반환
|
||||
* - 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,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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용)
|
||||
|
||||
Reference in New Issue
Block a user