File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
package kr.itn.itnhub.config;
import kr.itn.itnhub.contact.ContactNotFoundException;
import kr.itn.itnhub.contact.InvalidContactException;
import kr.itn.itnhub.feed.ChannelNotReadyException;
import kr.itn.itnhub.feed.InvalidMessageException;
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로 돌려준다.
*
* <p>{@link OrgNotFoundException}은 {@code OrganizationService.updateContact}와
* {@code ChannelProvisionService.provision}이 존재하지 않는 기관 id에 대해 던진다 -
* URL에 오타가 있는 것뿐이므로 404가 맞다.</p>
*
* <p>{@link MethodArgumentNotValidException}은 {@code @Valid} 대상 요청 바디의 필드가
* 비어있거나 형식이 틀렸을 때 Spring이 던진다 - 원본 필드명을 그대로 보여주는 대신
* 화면에 쓰는 한글 라벨로 바꿔서 알려준다.</p>
*
* <p>{@link SeedParseException}은 비어 있거나, 형식이 틀렸거나, 시트가 잘못된 업로드에
* 대해 던진다 - 흔한 사용자 실수이므로 400과 함께 무엇이 잘못됐는지 알려줘야 한다.</p>
*
* <p>예외 메시지는 각 예외가 만든 그대로 내려준다(새로 지어내지 않는다) - 스택트레이스,
* SQL, 파일 경로는 어차피 이 메시지들에 담기지 않으므로 그대로 노출해도 안전하다.
* {@code server.error.include-message=NEVER} 기본값은 건드리지 않는다 - 여기서 직접
* {@link ApiError} 본문을 만들어 반환하므로 그 설정과는 무관하게 동작한다.</p>
*
* <p>{@link ChannelNotReadyException}은 {@code ChannelFeedService}가 존재하는 기관의
* 문정원/법률검토 채널이 아직 생성되지 않았거나 channel 파라미터가 잘못됐을 때 던진다 -
* 기관은 있고(404 아님) 요청 자체도 잘못되지 않았으니(400 아님) "아직 처리할 수 없는
* 상태"라는 의미로 409를 쓴다.</p>
*
* <p>{@link InvalidMessageException}은 채팅 탭에서 메시지 본문과 첨부파일이 둘 다
* 없는 채로 전송을 시도할 때 던진다 - 흔한 사용자 실수이므로 400을 쓴다.</p>
*
* <p>{@link ContactNotFoundException}은 존재하지 않는 담당자 id로 조회/수정/삭제를
* 시도했을 때 던진다 - URL의 id가 단순히 틀린 것뿐이므로 404가 맞다.</p>
*
* <p>{@link InvalidContactException}은 담당자 구분(category)이 APPLICANT/MJ/LAWYER/OPERATOR
* 중 하나가 아니거나, 기관에 배정하려는 담당자의 구분이 그 역할과 맞지 않을 때
* 던진다 - 흔한 사용자 실수이므로 400을 쓴다.</p>
*
* <p><b>여기에 {@code Exception.class} catch-all을 추가하지 말 것.</b> 예상하지 못한
* 예외까지 4xx로 감싸버리면 진짜 버그가 조용히 묻힌다. 예상 밖 예외는 기본 500 처리
* 그대로 두는 것이 의도다.</p>
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/** 검증 오류 메시지에 쓸 필드명 → 한글 라벨. 목록에 없는 필드는 원본 이름 그대로 보여준다. */
private static final Map<String, String> FIELD_LABELS = Map.of(
"deptName", "부서명",
"contactName", "담당자",
"body", "메모 내용",
"category", "구분",
"name", "성명");
@ExceptionHandler(OrgNotFoundException.class)
public ResponseEntity<ApiError> handleNotFound(OrgNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ApiError(e.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> 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<ApiError> handleSeedParse(SeedParseException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ApiError(e.getMessage()));
}
@ExceptionHandler(ChannelNotReadyException.class)
public ResponseEntity<ApiError> handleChannelNotReady(ChannelNotReadyException e) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(new ApiError(e.getMessage()));
}
@ExceptionHandler(InvalidMessageException.class)
public ResponseEntity<ApiError> handleInvalidMessage(InvalidMessageException e) {
return ResponseEntity.badRequest().body(new ApiError(e.getMessage()));
}
@ExceptionHandler(ContactNotFoundException.class)
public ResponseEntity<ApiError> handleContactNotFound(ContactNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ApiError(e.getMessage()));
}
@ExceptionHandler(InvalidContactException.class)
public ResponseEntity<ApiError> handleInvalidContact(InvalidContactException e) {
return ResponseEntity.badRequest().body(new ApiError(e.getMessage()));
}
}