package kr.itn.itnhub.config; import kr.itn.itnhub.org.OrgNotFoundException; import kr.itn.itnhub.seed.SeedParseException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.Map; import java.util.stream.Collectors; /** * 운영자의 흔한 실수를 서버 장애(500)가 아니라 4xx로 돌려준다. * *

{@link OrgNotFoundException}은 {@code OrganizationService.updateContact}와 * {@code ChannelProvisionService.provision}이 존재하지 않는 기관 id에 대해 던진다 - * URL에 오타가 있는 것뿐이므로 404가 맞다.

* *

{@link MethodArgumentNotValidException}은 {@code @Valid} 대상 요청 바디의 필드가 * 비어있거나 형식이 틀렸을 때 Spring이 던진다 - 원본 필드명을 그대로 보여주는 대신 * 화면에 쓰는 한글 라벨로 바꿔서 알려준다.

* *

{@link SeedParseException}은 비어 있거나, 형식이 틀렸거나, 시트가 잘못된 업로드에 * 대해 던진다 - 흔한 사용자 실수이므로 400과 함께 무엇이 잘못됐는지 알려줘야 한다.

* *

예외 메시지는 각 예외가 만든 그대로 내려준다(새로 지어내지 않는다) - 스택트레이스, * SQL, 파일 경로는 어차피 이 메시지들에 담기지 않으므로 그대로 노출해도 안전하다. * {@code server.error.include-message=NEVER} 기본값은 건드리지 않는다 - 여기서 직접 * {@link ApiError} 본문을 만들어 반환하므로 그 설정과는 무관하게 동작한다.

* *

여기에 {@code Exception.class} catch-all을 추가하지 말 것. 예상하지 못한 * 예외까지 4xx로 감싸버리면 진짜 버그가 조용히 묻힌다. 예상 밖 예외는 기본 500 처리 * 그대로 두는 것이 의도다.

*/ @RestControllerAdvice public class GlobalExceptionHandler { /** 검증 오류 메시지에 쓸 필드명 → 한글 라벨. 목록에 없는 필드는 원본 이름 그대로 보여준다. */ private static final Map FIELD_LABELS = Map.of( "deptName", "부서명", "managerName", "담당자명", "managerTitle", "직급/직함", "managerPhone", "연락처", "managerEmail", "이메일"); @ExceptionHandler(OrgNotFoundException.class) public ResponseEntity handleNotFound(OrgNotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ApiError(e.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleValidation(MethodArgumentNotValidException e) { String fields = e.getBindingResult().getFieldErrors().stream() .map(fe -> FIELD_LABELS.getOrDefault(fe.getField(), fe.getField())) .distinct() .collect(Collectors.joining(", ")); return ResponseEntity.badRequest().body(new ApiError(fields + " 값을 확인해 주세요.")); } @ExceptionHandler(SeedParseException.class) public ResponseEntity handleSeedParse(SeedParseException e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ApiError(e.getMessage())); } }