package com.erp.controller; import com.erp.dto.ApiResponse; import com.erp.service.DepartmentService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/departments") @RequiredArgsConstructor @Slf4j public class DepartmentController { private final DepartmentService departmentService; /** * 부서 목록 조회 (회사별). * 기본은 active 부서만. ?include_deleted=true 시 soft-delete 된 부서도 포함. * GET /api/departments/companies/{companyCode}/departments[?include_deleted=true] */ @GetMapping("/companies/{companyCode}/departments") public ResponseEntity>>> getDepartments( @PathVariable String companyCode, @RequestAttribute("company_code") String userCompanyCode, @RequestParam(value = "include_deleted", required = false, defaultValue = "false") boolean includeDeleted) { if (!isSuperAdmin(userCompanyCode) && !userCompanyCode.equals(companyCode)) { return ResponseEntity.status(403) .body(ApiResponse.error("해당 회사의 부서를 조회할 권한이 없습니다.")); } List> departments = departmentService.getDepartments(companyCode, includeDeleted); return ResponseEntity.ok(ApiResponse.success(departments, "부서 목록 조회 성공")); } /** * 부서 상세 조회. * - 기본: active 부서만 (DELETED_AT IS NULL) * - ?include_deleted=true: soft-delete 된 부서도 조회 가능 (복구·이력 화면용) * - 회사 격리: 본인 회사 부서만, SUPER_ADMIN 은 전체 * GET /api/departments/{deptCode}[?include_deleted=true] */ @GetMapping("/{deptCode}") public ResponseEntity>> getDepartment( @PathVariable String deptCode, @RequestAttribute("company_code") String userCompanyCode, @RequestParam(value = "include_deleted", required = false, defaultValue = "false") boolean includeDeleted) { Map department = includeDeleted ? departmentService.getDepartmentIncludingDeleted(deptCode) : departmentService.getDepartment(deptCode); if (department == null) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없습니다.")); } if (!canAccessDept(department, userCompanyCode)) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없습니다.")); } return ResponseEntity.ok(ApiResponse.success(department, "부서 조회 성공")); } /** * 부서 생성 * POST /api/departments/companies/{companyCode}/departments */ @PostMapping("/companies/{companyCode}/departments") public ResponseEntity>> createDepartment( @PathVariable String companyCode, @RequestAttribute("company_code") String userCompanyCode, @RequestAttribute("role") String role, @RequestBody Map body) { if (!isAdmin(role)) { return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); } if (!isSuperAdmin(userCompanyCode) && !userCompanyCode.equals(companyCode)) { return ResponseEntity.status(403).body(ApiResponse.error("해당 회사의 부서를 생성할 권한이 없습니다.")); } try { Map created = departmentService.createDepartment(companyCode, body); return ResponseEntity.status(201).body(ApiResponse.success(created, "부서가 생성되었습니다.")); } catch (DepartmentService.DuplicateDeptNameException e) { return ResponseEntity.status(409).body(ApiResponse.error(e.getMessage())); } catch (IllegalArgumentException e) { return ResponseEntity.status(400).body(ApiResponse.error(e.getMessage())); } } /** * 부서 수정 * PUT /api/departments/{deptCode} */ @PutMapping("/{deptCode}") public ResponseEntity>> updateDepartment( @PathVariable String deptCode, @RequestAttribute("role") String role, @RequestAttribute("company_code") String userCompanyCode, @RequestBody Map body) { if (!isAdmin(role)) { return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); } Map existing = departmentService.getDepartmentIncludingDeleted(deptCode); if (existing == null) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없습니다.")); } if (!canAccessDept(existing, userCompanyCode)) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없습니다.")); } try { Map updated = departmentService.updateDepartment(deptCode, body); if (updated == null) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없습니다.")); } return ResponseEntity.ok(ApiResponse.success(updated, "부서가 수정되었습니다.")); } catch (IllegalArgumentException e) { return ResponseEntity.status(400).body(ApiResponse.error(e.getMessage())); } } /** * 부서 삭제 (soft-delete, V1 slim scope). * - 기존 hard-delete → DELETED_AT = NOW() 마킹으로 변경 * - 응답 호환: 기존 { success, message } 에 data.soft_deleted=true 필드 추가 * - USER_DEPT 행은 보존되어 복구 시 멤버 그대로 살아남 * DELETE /api/departments/{deptCode} */ @DeleteMapping("/{deptCode}") public ResponseEntity>> deleteDepartment( @PathVariable String deptCode, @RequestAttribute("role") String role, @RequestAttribute("company_code") String userCompanyCode) { if (!isAdmin(role)) { return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); } Map existing = departmentService.getDepartmentIncludingDeleted(deptCode); if (existing == null) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없거나 이미 삭제된 부서입니다.")); } if (!canAccessDept(existing, userCompanyCode)) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없거나 이미 삭제된 부서입니다.")); } try { int result = departmentService.deleteDepartment(deptCode); if (result == -1) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없거나 이미 삭제된 부서입니다.")); } Map data = new java.util.HashMap<>(); data.put("soft_deleted", true); data.put("dept_code", deptCode); return ResponseEntity.ok(ApiResponse.success(data, "부서가 삭제되었습니다. (복구 가능)")); } catch (IllegalStateException e) { return ResponseEntity.status(400).body(ApiResponse.error(e.getMessage())); } } /** * 부서 복구 (V1 slim scope). * - DELETED_AT = NULL 로 되돌림 * - 부모도 deleted 상태면 차단 * POST /api/departments/{deptCode}/restore */ @PostMapping("/{deptCode}/restore") public ResponseEntity>> restoreDepartment( @PathVariable String deptCode, @RequestAttribute("role") String role, @RequestAttribute("company_code") String userCompanyCode) { if (!isAdmin(role)) { return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); } Map existing = departmentService.getDepartmentIncludingDeleted(deptCode); if (existing == null) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없습니다.")); } if (!canAccessDept(existing, userCompanyCode)) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없습니다.")); } DepartmentService.RestoreResult result = departmentService.restoreDepartment(deptCode); switch (result) { case OK: Map data = new java.util.HashMap<>(); data.put("dept_code", deptCode); data.put("restored", true); return ResponseEntity.ok(ApiResponse.success(data, "부서가 복구되었습니다.")); case NOT_FOUND: return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없습니다.")); case NOT_DELETED: return ResponseEntity.status(400).body(ApiResponse.error("이미 활성 상태인 부서입니다.")); case PARENT_DELETED: return ResponseEntity.status(400).body(ApiResponse.error("상위 부서가 삭제 상태입니다. 상위 부서를 먼저 복구해주세요.")); default: return ResponseEntity.status(500).body(ApiResponse.error("복구 처리 중 오류")); } } /** * 부서원 목록 조회 * GET /api/departments/{deptCode}/members */ @GetMapping("/{deptCode}/members") public ResponseEntity>>> getDeptMembers( @PathVariable String deptCode, @RequestAttribute("company_code") String userCompanyCode) { Map existing = departmentService.getDepartmentIncludingDeleted(deptCode); if (existing == null) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없습니다.")); } if (!canAccessDept(existing, userCompanyCode)) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없습니다.")); } List> members = departmentService.getDeptMembers(deptCode); return ResponseEntity.ok(ApiResponse.success(members, "부서원 목록 조회 성공")); } /** * 사용자 검색 (부서원 추가용) * GET /api/departments/companies/{companyCode}/users/search */ @GetMapping("/companies/{companyCode}/users/search") public ResponseEntity>>> searchUsers( @PathVariable String companyCode, @RequestParam(required = false) String search) { if (search == null || search.isBlank()) { return ResponseEntity.status(400).body(ApiResponse.error("검색어를 입력해주세요.")); } List> users = departmentService.searchUsers(companyCode, search); return ResponseEntity.ok(ApiResponse.success(users, "사용자 검색 성공")); } /** * 부서원 추가 * POST /api/departments/{deptCode}/members */ @PostMapping("/{deptCode}/members") public ResponseEntity> addDeptMember( @PathVariable String deptCode, @RequestAttribute("role") String role, @RequestAttribute("company_code") String userCompanyCode, @RequestBody Map body) { if (!isAdmin(role)) { return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); } Map existing = departmentService.getDepartmentIncludingDeleted(deptCode); if (existing == null) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없습니다.")); } if (!canAccessDept(existing, userCompanyCode)) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없습니다.")); } // 프론트엔드는 snake_case(user_id)로 전송 (Node.js 호환) Object userIdObj = body.get("user_id"); if (userIdObj == null) userIdObj = body.get("user_id"); if (userIdObj == null || userIdObj.toString().isBlank()) { return ResponseEntity.status(400).body(ApiResponse.error("사용자 ID를 입력해주세요.")); } String userId = userIdObj.toString(); try { departmentService.addDeptMember(deptCode, userId); return ResponseEntity.status(201).body(ApiResponse.success(null, "부서원이 추가되었습니다.")); } catch (DepartmentService.DuplicateMemberException e) { return ResponseEntity.status(409).body(ApiResponse.error(e.getMessage())); } catch (IllegalArgumentException e) { return ResponseEntity.status(404).body(ApiResponse.error(e.getMessage())); } } /** * 부서원 제거 * DELETE /api/departments/{deptCode}/members/{userId} */ @DeleteMapping("/{deptCode}/members/{userId}") public ResponseEntity> removeDeptMember( @PathVariable String deptCode, @PathVariable String userId, @RequestAttribute("role") String role, @RequestAttribute("company_code") String userCompanyCode) { if (!isAdmin(role)) { return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); } Map existing = departmentService.getDepartmentIncludingDeleted(deptCode); if (existing == null) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없습니다.")); } if (!canAccessDept(existing, userCompanyCode)) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없습니다.")); } boolean removed = departmentService.removeDeptMember(deptCode, userId); if (!removed) { return ResponseEntity.status(404).body(ApiResponse.error("해당 부서원을 찾을 수 없습니다.")); } return ResponseEntity.ok(ApiResponse.success(null, "부서원이 제거되었습니다.")); } /** * 주 부서 설정 * PUT /api/departments/{deptCode}/members/{userId}/primary */ @PutMapping("/{deptCode}/members/{userId}/primary") public ResponseEntity> setPrimaryDept( @PathVariable String deptCode, @PathVariable String userId, @RequestAttribute("role") String role, @RequestAttribute("company_code") String userCompanyCode) { if (!isAdmin(role)) { return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); } Map existing = departmentService.getDepartmentIncludingDeleted(deptCode); if (existing == null) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없습니다.")); } if (!canAccessDept(existing, userCompanyCode)) { return ResponseEntity.status(404).body(ApiResponse.error("부서를 찾을 수 없습니다.")); } departmentService.setPrimaryDept(deptCode, userId); return ResponseEntity.ok(ApiResponse.success(null, "주 부서가 설정되었습니다.")); } // ────────────────────────────────────────────────── // 내부 유틸 // ────────────────────────────────────────────────── private boolean isAdmin(String role) { return isSuperAdmin(role) || "COMPANY_ADMIN".equals(role); } private boolean isSuperAdmin(String companyCodeOrRole) { return "*".equals(companyCodeOrRole) || "SUPER_ADMIN".equals(companyCodeOrRole); } /** * 회사 격리 검증. SUPER_ADMIN ('*') 은 모든 회사 접근 가능. * 일반 ADMIN/USER 는 자기 회사 + 글로벌 ('*') 부서만. */ private boolean canAccessDept(Map dept, String userCompanyCode) { if (dept == null) return false; if (isSuperAdmin(userCompanyCode)) return true; String deptCompanyCode = dept.get("company_code") != null ? dept.get("company_code").toString() : null; if (deptCompanyCode == null) return false; return userCompanyCode.equals(deptCompanyCode) || "*".equals(deptCompanyCode); } }