package com.erp.crosstenant; import com.erp.dto.ApiResponse; import com.erp.service.AdminService; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; /** * SUPER_ADMIN 의 cross-tenant USER WRITE 엔드포인트. * * 기본 패턴: * 1. {@link CrossTenantContext#isSuperAdmin} + {@link CrossTenantContext#isMetaContext} 가드 * 2. 요청 body/path/query 에서 target {@code company_code} 추출 (필수) * 3. {@link CrossTenantExecutor#runInCompany} 로 그 회사 DB 컨텍스트 임시 전환 * 4. 기존 {@link AdminService} 의 user write 메서드 호출 (재사용) * 5. finally 에서 컨텍스트 복원 * * 기존 {@code POST /api/admin/users} 등 단일 회사 모드 엔드포인트는 무수정 — 회사 도메인 * 컨텍스트에서 그대로 동작. * * @see CrossTenantExecutor * @see com.erp.controller.AdminController // 단일 회사 모드 원본 */ @RestController @RequestMapping("/api/admin/cross-tenant/users") @RequiredArgsConstructor @Slf4j public class CrossTenantUserController { private final CrossTenantExecutor executor; private final AdminService adminService; // ── 등록 / 수정 ───────────────────────────────────────────────────── /** * POST /api/admin/cross-tenant/users * SUPER_ADMIN 이 특정 회사 사용자 등록/수정 (회사는 body.company_code 로 명시). */ @PostMapping public ResponseEntity>> saveUser( HttpServletRequest request, @RequestBody Map body) { ResponseEntity>> guard = guard(request); if (guard != null) return guard; String targetCompanyCode = (String) body.get("company_code"); try { Map result = executor.runInCompany(targetCompanyCode, () -> adminService.saveUser(body)); return ResponseEntity.ok(ApiResponse.success(result, "사용자 저장 성공")); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); } } /** * POST /api/admin/cross-tenant/users/with-dept * 사원+부서 통합 저장 (cross-tenant). */ @PostMapping("/with-dept") public ResponseEntity>> saveUserWithDept( HttpServletRequest request, @RequestBody Map body) { ResponseEntity>> guard = guard(request); if (guard != null) return guard; String targetCompanyCode = (String) body.get("company_code"); try { Map result = executor.runInCompany(targetCompanyCode, () -> adminService.saveUserWithDept(body)); return ResponseEntity.ok(ApiResponse.success(result, "사원+부서 저장 성공")); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); } } /** * PUT /api/admin/cross-tenant/users/{userId} * 사용자 수정 (REST). target company_code 는 body 에 명시. */ @PutMapping("/{userId}") public ResponseEntity>> updateUser( HttpServletRequest request, @PathVariable String userId, @RequestBody Map body) { ResponseEntity>> guard = guard(request); if (guard != null) return guard; String targetCompanyCode = (String) body.get("company_code"); body.put("user_id", userId); try { Map result = executor.runInCompany(targetCompanyCode, () -> adminService.saveUser(body)); return ResponseEntity.ok(ApiResponse.success(result, "사용자 수정 성공")); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); } } /** * DELETE /api/admin/cross-tenant/users/{userId}?company_code=TEST01 * 사용자 삭제 (비활성화). target company_code 는 query param. */ @DeleteMapping("/{userId}") public ResponseEntity> deleteUser( HttpServletRequest request, @PathVariable String userId, @RequestParam("company_code") String companyCode) { ResponseEntity> guard = guardVoid(request); if (guard != null) return guard; try { executor.runInCompany(companyCode, () -> { Map existing = adminService.getUserInfo(userId); if (existing == null) { throw new IllegalArgumentException("사용자를 찾을 수 없습니다: " + userId); } adminService.changeUserStatus(userId, "inactive"); }); return ResponseEntity.ok(ApiResponse.success(null, "사용자 삭제 성공")); } catch (IllegalArgumentException e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.error(e.getMessage())); } } /** * PATCH /api/admin/cross-tenant/users/{userId}/status * 사용자 상태 변경. body: { "status": "active|inactive", "company_code": "TEST01" } */ @PatchMapping("/{userId}/status") public ResponseEntity> changeUserStatus( HttpServletRequest request, @PathVariable String userId, @RequestBody Map body) { ResponseEntity> guard = guardVoid(request); if (guard != null) return guard; String targetCompanyCode = (String) body.get("company_code"); String status = (String) body.get("status"); try { executor.runInCompany(targetCompanyCode, () -> adminService.changeUserStatus(userId, status)); return ResponseEntity.ok(ApiResponse.success(null, "사용자 상태 변경 성공")); } catch (IllegalArgumentException e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.error(e.getMessage())); } } /** * POST /api/admin/cross-tenant/users/reset-password * body: { "user_id": "...", "company_code": "TEST01" } */ @PostMapping("/reset-password") public ResponseEntity> resetUserPassword( HttpServletRequest request, @RequestBody Map body) { ResponseEntity> guard = guardVoid(request); if (guard != null) return guard; String targetCompanyCode = (String) body.get("company_code"); String userId = (String) body.get("user_id"); try { executor.runInCompany(targetCompanyCode, () -> adminService.resetUserPassword(userId)); return ResponseEntity.ok(ApiResponse.success(null, "비밀번호 초기화 성공")); } catch (IllegalArgumentException e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.error(e.getMessage())); } } // ── READ 보강 (단건 조회 / 중복확인 / 이력) ─────────────────────── /** * GET /api/admin/cross-tenant/users/{userId}?company_code=TEST01 * 단건 조회 — 회사 컨텍스트로 가서 USER_INFO 단건. */ @GetMapping("/{userId}") public ResponseEntity>> getUserInfo( HttpServletRequest request, @PathVariable String userId, @RequestParam("company_code") String companyCode) { ResponseEntity>> guard = guard(request); if (guard != null) return guard; try { Map info = executor.runInCompany(companyCode, () -> adminService.getUserInfo(userId)); if (info == null) { return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(ApiResponse.error("사용자를 찾을 수 없습니다.")); } return ResponseEntity.ok(ApiResponse.success(info)); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); } } /** * GET /api/admin/cross-tenant/users/{userId}/with-dept?company_code=TEST01 */ @GetMapping("/{userId}/with-dept") public ResponseEntity>> getUserWithDept( HttpServletRequest request, @PathVariable String userId, @RequestParam("company_code") String companyCode) { ResponseEntity>> guard = guard(request); if (guard != null) return guard; try { Map result = executor.runInCompany(companyCode, () -> { Map p = new HashMap<>(); p.put("company_code", companyCode); p.put("user_id", userId); return adminService.getUserWithDept(p); }); if (result == null) { return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(ApiResponse.error("사용자를 찾을 수 없습니다.")); } return ResponseEntity.ok(ApiResponse.success(result)); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); } } /** * POST /api/admin/cross-tenant/users/check-duplicate * body: { "user_id": "...", "company_code": "TEST01" } */ @PostMapping("/check-duplicate") public ResponseEntity>> checkDuplicateUserId( HttpServletRequest request, @RequestBody Map body) { ResponseEntity>> guard = guard(request); if (guard != null) return guard; String targetCompanyCode = (String) body.get("company_code"); String userId = (String) body.get("user_id"); try { Map result = executor.runInCompany(targetCompanyCode, () -> { Map existing = adminService.getUserInfo(userId); Map out = new HashMap<>(); out.put("is_duplicate", existing != null); return out; }); return ResponseEntity.ok(ApiResponse.success(result, "아이디 중복 확인 완료")); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); } } // ── 가드 헬퍼 ─────────────────────────────────────────────────── /** Map 응답용 가드 — null 이면 통과, 아니면 그대로 반환. */ private ResponseEntity>> guard(HttpServletRequest request) { if (!CrossTenantContext.isSuperAdmin(request)) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(ApiResponse.error("super_admin_required", request.getRequestURI())); } if (!CrossTenantContext.isMetaContext()) { return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(ApiResponse.error("cross_tenant_requires_meta_context", request.getRequestURI())); } return null; } /** Void 응답용 가드 (제네릭만 다름). */ private ResponseEntity> guardVoid(HttpServletRequest request) { if (!CrossTenantContext.isSuperAdmin(request)) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(ApiResponse.error("super_admin_required", request.getRequestURI())); } if (!CrossTenantContext.isMetaContext()) { return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(ApiResponse.error("cross_tenant_requires_meta_context", request.getRequestURI())); } return null; } }