Skip to content

Commit

Permalink
✨ feat: API 응답 정규화 (#37)
Browse files Browse the repository at this point in the history
✨ feat: API 응답 정규화 (#37)
  • Loading branch information
DDonghyeo authored May 9, 2024
2 parents 8784ce3 + b5151ef commit 1585522
Show file tree
Hide file tree
Showing 10 changed files with 289 additions and 78 deletions.
Original file line number Diff line number Diff line change
@@ -1,52 +1,47 @@
package com.waither.notiservice.api;

import com.waither.notiservice.api.response.NotificationResponse;
import com.waither.notiservice.dto.LocationDto;
import com.waither.notiservice.api.request.LocationDto;
import com.waither.notiservice.global.response.ApiResponse;
import com.waither.notiservice.service.NotificationService;
import com.waither.notiservice.utils.RedisUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequiredArgsConstructor
@RequestMapping("/api/v1/noti")
@RestController
public class NotificationController {

private final NotificationService notificationService;

@Operation(summary = "get notification", description = "알림 목록 조회하기")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK",
content = @Content(schema = @Schema(implementation = NotificationResponse.class))),
@ApiResponse(responseCode = "400", description = "BAD REQUEST"),
@ApiResponse(responseCode = "404", description = "NOT FOUND"),
@ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR")
})
private final RedisUtils redisUtils;

@Operation(summary = "Get notification", description = "알림 목록 조회하기")
@GetMapping("")
public List<NotificationResponse> getNotifications(Long userId) {
return notificationService.getNotifications(userId);
public ApiResponse<?> getNotifications(Long userId) {
return ApiResponse.onSuccess(notificationService.getNotifications(userId));
}

@Operation(summary = "Delete notification", description = "알림 삭제하기")
@DeleteMapping("")
public ApiResponse<?> deleteNotification(@RequestParam("id") String notificationId) {
notificationService.deleteNotification(notificationId);
return ApiResponse.onSuccess(HttpStatus.OK);
}

@Operation(summary = "Send Go Out Alarm", description = "외출 알림 전송하기")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "400", description = "BAD REQUEST"),
@ApiResponse(responseCode = "404", description = "NOT FOUND"),
@ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR")
})
@PostMapping("/goOut")
public void sendGoOutAlarm(Long userId) {
notificationService.sendGoOutAlarm(userId);
}

@PostMapping("/")
public void checkCurrentAlarm(@RequestBody LocationDto locationDto) {
@Operation(summary = "Current Location", description = "현재 위치 전송")
@PostMapping("/location")
public void checkCurrentAlarm(@RequestBody @Valid LocationDto locationDto) {
notificationService.checkCurrentAlarm(locationDto);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.waither.notiservice.api.request;

import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class LocationDto {

@NotBlank(message = " 위도(x) 값은 필수입니다.")
@DecimalMax(value = "132.0", inclusive = true, message = "위도(x)는 대한민국 내에서만 가능합니다.")
@DecimalMin(value = "124.0", inclusive = true, message = "위도(x)는 대한민국 내에서만 가능합니다.")
public double x;

@NotBlank(message = " 경도(y) 값은 필수입니다.")
@DecimalMax(value = "43.0", inclusive = true, message = "경도(y)는 대한민국 내에서만 가능합니다.")
@DecimalMin(value = "33.0", inclusive = true, message = "경도(y)는 대한민국 내에서만 가능합니다.")
public double y;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.waither.notiservice.global.exception;


import com.waither.notiservice.global.response.status.BaseErrorCode;
import lombok.Getter;

@Getter
public class CustomException extends RuntimeException {

private final BaseErrorCode errorCode;

public CustomException(BaseErrorCode errorCode) {
this.errorCode = errorCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.waither.notiservice.global.exception;


import com.waither.notiservice.global.response.ApiResponse;
import com.waither.notiservice.global.response.ErrorCode;
import com.waither.notiservice.global.response.status.BaseErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

// @Valid 유효성 검사를 실패했을 시
@ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<ApiResponse<Map<String, String>>> handleMethodArgumentNotValidException(
MethodArgumentNotValidException ex
) {
// 실패한 validation 을 담을 Map
Map<String, String> failedValidations = new HashMap<>();
List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
// fieldErrors 를 순회하며 failedValidations 에 담는다.
fieldErrors.forEach(error -> failedValidations.put(error.getField(), error.getDefaultMessage()));
ApiResponse<Map<String, String>> errorResponse = ApiResponse.onFailure(
ErrorCode.VALIDATION_FAILED.getCode(),
ErrorCode.VALIDATION_FAILED.getMessage(),
failedValidations);
return ResponseEntity.status(ex.getStatusCode()).body(errorResponse);
}

// Custom 예외에 대한 처리
@ExceptionHandler({CustomException.class})
public ResponseEntity<ApiResponse<Void>> handleCustomException(CustomException e) {
log.warn("[WARNING] Custom Exception : {}", e.getErrorCode());
BaseErrorCode errorCode = e.getErrorCode();
return ResponseEntity.
status(errorCode.getHttpStatus())
.body(errorCode.getErrorResponse());
}

// 그 외의 정의되지 않은 모든 예외 처리
@ExceptionHandler({Exception.class})
public ResponseEntity<ApiResponse<String>> handleAllException(Exception e) {
log.error("[WARNING] Internal Server Error : {} ", e.getMessage());
BaseErrorCode errorCode = ErrorCode.INTERNAL_SERVER_ERROR_500;
ApiResponse<String> errorResponse = ApiResponse.onFailure(
errorCode.getCode(),
errorCode.getMessage(),
e.getMessage()
);
return ResponseEntity
.status(errorCode.getHttpStatus())
.body(errorResponse);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.waither.notiservice.global.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;


@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@JsonPropertyOrder({"code", "message", "result"})
public class ApiResponse<T> {

// HTTP 상태 코드나 사용자 정의 코드
private final String code;
// API 요청에 대한 설명이나 상태 메시지
private final String message;
// result 값은 null이 아닐 때만 응답에 포함시킨다.
@JsonInclude(JsonInclude.Include.NON_NULL)
// 결과 데이터. (보통 dto -> json 파싱 될 예정)
private T result;

// 성공한 경우 응답 생성
public static <T> ApiResponse<T> onSuccess(T result) {
return new ApiResponse<>(String.valueOf(HttpStatus.OK.value()), HttpStatus.OK.getReasonPhrase(), result);
}

// 성공한 경우 응답 생성 (상태 코드 지정 가능)
public static <T> ApiResponse<T> onSuccess(HttpStatus status, T result) {
return new ApiResponse<>(String.valueOf(status.value()), status.getReasonPhrase(), result);
}

// 실패한 경우 응답 생성
public static <T> ApiResponse<T> onFailure(String code, String message, T result) {
return new ApiResponse<>(code, message, result);
}

// 실패한 경우 응답 생성 (데이터 없음)
public static <T> ApiResponse<T> onFailure(String statusCode, String message) {
return new ApiResponse<>(statusCode, message, null);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.waither.notiservice.global.response;


import com.waither.notiservice.global.response.status.BaseErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum ErrorCode implements BaseErrorCode {

// 일반적인 ERROR 응답
BAD_REQUEST_400(HttpStatus.BAD_REQUEST,
"COMMON400",
HttpStatus.BAD_REQUEST.getReasonPhrase()),
UNAUTHORIZED_401(HttpStatus.UNAUTHORIZED,
"COMMON401",
HttpStatus.UNAUTHORIZED.getReasonPhrase()),
FORBIDDEN_403(HttpStatus.FORBIDDEN,
"COMMON403",
HttpStatus.FORBIDDEN.getReasonPhrase()),
NOT_FOUND_404(HttpStatus.NOT_FOUND,
"COMMON404",
HttpStatus.NOT_FOUND.getReasonPhrase()),
INTERNAL_SERVER_ERROR_500(
HttpStatus.INTERNAL_SERVER_ERROR,
"COMMON500",
HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()),

// 유효성 검사
VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "VALID400_0", "잘못된 파라미터 입니다."),

// 데이터 관련 에러
NO_USER_MEDIAN_REGISTERED(HttpStatus.NOT_FOUND, "USER404_0", "사용자 설정값이 존재하지 않습니다."),
NO_USER_DATA_REGISTERED(HttpStatus.NOT_FOUND, "USER404_1", "사용자 데이터 값이 존재하지 않습니다."),
FIREBASE_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "TOKEN404", "푸시알림 토큰이 존재하지 않습니다."),

//통신 과정 에러
COMMUNICATION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500_1", "통신 과정에서 문제가 발생했습니다.")

;


private final HttpStatus httpStatus;
private final String code;
private final String message;

@Override
public ApiResponse<Void> getErrorResponse() {
return ApiResponse.onFailure(code, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.waither.notiservice.global.response.status;

import com.waither.notiservice.global.response.ApiResponse;
import org.springframework.http.HttpStatus;

public interface BaseErrorCode {

HttpStatus getHttpStatus();

String getCode();

String getMessage();

ApiResponse<Void> getErrorResponse();
}
Loading

0 comments on commit 1585522

Please sign in to comment.