Files
invyone/backend-spring/src/main/java/com/erp/controller/SubstituteController.java
T
johngreen c4a62b7e35 fix(대무자): COMPANY_ADMIN 권한 허용 + 결재함 SQL 컬럼 오타 fix + UI 셀렉트 개선
운영 QA 에서 발견된 3가지 결함을 한 번에 수정.

1. SubstituteController.java:56 / SubstituteService.java:242 (requireAdmin)
   - role 비교에서 "COMPANY_ADMIN" 누락 → 운영 admin 이 대무자 지정 시 항상 403.
   - 운영 회사 admin 의 user_type 은 COMPANY_ADMIN 이 표준 (AdminAccountCreator 가 그렇게 생성).
   - "ADMIN" / "SUPER_ADMIN" 외 "COMPANY_ADMIN" 도 허용.

2. mapper/approval.xml (selectMyRequests, selectMyPendingLines)
   - ORDER BY / SELECT 의 R.CREATED_DATE 가 잘못된 컬럼명 (APPROVAL_REQUESTS 실제: created_at).
   - 결재함 /api/approval/my-pending, /api/approval/requests 가 항상 500.
   - 3군데 R.CREATED_DATE → R.CREATED_AT.

3. SubstituteSection.tsx
   - 대무자 ID 를 직접 타이핑하던 input 을 Select 로 교체.
   - getUserList 로 같은 회사 활성 사용자 목록 로드, 본인 + SUPER_ADMIN + 비활성 자동 제외.
   - 다이얼로그 열 때 한 번만 load (openDialog 시 loadCandidates).
   - 빈 결과/로딩 placeholder 처리.
2026-05-12 17:02:15 +09:00

176 lines
8.7 KiB
Java

package com.erp.controller;
import com.erp.dto.ApiResponse;
import com.erp.service.SubstituteService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* 대무자(代務者) 관리 API.
*
* Spec: .omc/specs/deep-dive-user-substitute-management.md
* Plan: .omc/plans/autopilot-impl.md (T4)
*
* 정책:
* - GET /mine 은 본인 read-only (누구나 가능)
* - 나머지는 관리자(ADMIN/SUPER_ADMIN) 만 — Service 의 requireAdmin 이 2차 방어
*/
@RestController
@RequestMapping("/api/substitutes")
@RequiredArgsConstructor
@Slf4j
public class SubstituteController {
private final SubstituteService substituteService;
// ─────────────────────────────────────────────────────────────
// 조회 — 관리자
// ─────────────────────────────────────────────────────────────
@GetMapping
public ResponseEntity<ApiResponse<Map<String, Object>>> getList(
@RequestParam Map<String, Object> params,
@RequestAttribute("company_code") String companyCode,
@RequestAttribute("role") String role) {
params.put("company_code", companyCode);
params.put("role", role);
try {
return ResponseEntity.ok(ApiResponse.success(substituteService.getSubstituteList(params)));
} catch (AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiResponse.error(e.getMessage()));
}
}
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<Map<String, Object>>> getOne(
@PathVariable("id") Long substituteId,
@RequestAttribute("company_code") String companyCode,
@RequestAttribute("role") String role) {
if (!"ADMIN".equals(role) && !"COMPANY_ADMIN".equals(role) && !"SUPER_ADMIN".equals(role)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ApiResponse.error("관리자만 조회할 수 있습니다."));
}
Map<String, Object> params = new HashMap<>();
params.put("substitute_id", substituteId);
params.put("company_code", companyCode);
try {
return ResponseEntity.ok(ApiResponse.success(substituteService.getSubstituteInfo(params)));
} catch (IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponse.error(e.getMessage()));
}
}
// ─────────────────────────────────────────────────────────────
// 본인 조회 — ProfileModal read-only
// ─────────────────────────────────────────────────────────────
@GetMapping("/mine")
public ResponseEntity<ApiResponse<Map<String, Object>>> getMine(
@RequestAttribute("user_id") String userId,
@RequestAttribute("company_code") String companyCode) {
Map<String, Object> params = new HashMap<>();
params.put("user_id", userId);
params.put("company_code", companyCode);
return ResponseEntity.ok(ApiResponse.success(substituteService.getMySubstitutes(params)));
}
// ─────────────────────────────────────────────────────────────
// 변경 — 관리자
// ─────────────────────────────────────────────────────────────
@PostMapping
public ResponseEntity<ApiResponse<Map<String, Object>>> create(
@RequestBody Map<String, Object> body,
@RequestAttribute("user_id") String userId,
@RequestAttribute("company_code") String companyCode,
@RequestAttribute("role") String role) {
body.put("company_code", companyCode);
body.put("role", role);
body.put("created_by", userId);
try {
Map<String, Object> created = substituteService.insertSubstitute(body);
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(created, "대무자가 지정되었습니다."));
} catch (AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiResponse.error(e.getMessage()));
} catch (IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.error(e.getMessage()));
} catch (Exception e) {
log.error("대무자 등록 오류", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("대무자 등록 중 오류가 발생했습니다."));
}
}
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<Map<String, Object>>> update(
@PathVariable("id") Long substituteId,
@RequestBody Map<String, Object> body,
@RequestAttribute("user_id") String userId,
@RequestAttribute("company_code") String companyCode,
@RequestAttribute("role") String role) {
body.put("substitute_id", substituteId);
body.put("company_code", companyCode);
body.put("role", role);
body.put("updated_by", userId);
try {
return ResponseEntity.ok(
ApiResponse.success(substituteService.updateSubstitute(body), "대무 설정이 수정되었습니다."));
} catch (AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiResponse.error(e.getMessage()));
} catch (IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.error(e.getMessage()));
} catch (Exception e) {
log.error("대무자 수정 오류", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("대무자 수정 중 오류가 발생했습니다."));
}
}
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> delete(
@PathVariable("id") Long substituteId,
@RequestAttribute("company_code") String companyCode,
@RequestAttribute("role") String role) {
Map<String, Object> params = new HashMap<>();
params.put("substitute_id", substituteId);
params.put("company_code", companyCode);
params.put("role", role);
try {
substituteService.deleteSubstitute(params);
return ResponseEntity.ok(ApiResponse.success(null, "대무 설정이 해지되었습니다."));
} catch (AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiResponse.error(e.getMessage()));
} catch (IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponse.error(e.getMessage()));
} catch (Exception e) {
log.error("대무자 해지 오류", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("대무자 해지 중 오류가 발생했습니다."));
}
}
// ─────────────────────────────────────────────────────────────
// 사전 검증 — UI 가 등록 직전 호출
// ─────────────────────────────────────────────────────────────
@PostMapping("/check-overlap")
public ResponseEntity<ApiResponse<Map<String, Object>>> checkOverlap(
@RequestBody Map<String, Object> body,
@RequestAttribute("company_code") String companyCode) {
body.put("company_code", companyCode);
int cnt = substituteService.checkOverlap(body);
Map<String, Object> result = new HashMap<>();
result.put("overlap", cnt > 0);
result.put("count", cnt);
return ResponseEntity.ok(ApiResponse.success(result));
}
}