kmbin92_2025081302 #3

Merged
sohot8653 merged 5 commits from kmbin92_2025081302 into main 2025-08-15 00:54:15 +09:00
10 changed files with 188 additions and 160 deletions
Showing only changes of commit 77bc400888 - Show all commits

View File

@@ -32,14 +32,14 @@ src/main/java/com/bio/bio_backend/
└── BioBackendApplication.java
```
### 2. API 응답 표준화 (CustomApiResponse)
### 2. API 응답 표준화 (ApiResponseDto)
#### 응답 구조
모든 API 응답은 `CustomApiResponse<T>` 형태로 표준화되어 있습니다.
모든 API 응답은 `ApiResponseDto<T>` 형태로 표준화되어 있습니다.
```java
public class CustomApiResponse<T> {
public class ApiResponseDto<T> {
private int code; // HTTP 상태 코드
private String message; // 응답 메시지 (ApiResponseCode enum 값)
private String description; // 응답 설명
@@ -81,12 +81,12 @@ public class CustomApiResponse<T> {
```java
@PostMapping("/members")
public ResponseEntity<CustomApiResponse<CreateMemberResponseDto>> createMember(@RequestBody CreateMemberRequestDto requestDto) {
public ResponseEntity<ApiResponseDto<CreateMemberResponseDto>> createMember(@RequestBody CreateMemberRequestDto requestDto) {
// ... 비즈니스 로직 ...
// 성공 응답
CustomApiResponse<CreateMemberResponseDto> apiResponse =
CustomApiResponse.success(ApiResponseCode.COMMON_SUCCESS_CREATED, responseDto);
ApiResponseDto<CreateMemberResponseDto> apiResponse =
ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS_CREATED, responseDto);
return ResponseEntity.status(HttpStatus.CREATED).body(apiResponse);
}
@@ -114,7 +114,7 @@ public enum ApiResponseCode {
#### 핵심 규칙
- **모든 API 응답**: `CustomApiResponse<T>`로 감싸서 반환
- **모든 API 응답**: `ApiResponseDto<T>`로 감싸서 반환
- **공용 응답 코드**: `COMMON_` 접두사로 시작하는 범용 코드 사용
- **일관된 구조**: `code`, `message`, `description`, `data` 필드로 표준화
- **제네릭 활용**: `<T>`를 통해 다양한 데이터 타입 지원
@@ -132,12 +132,11 @@ public enum ApiResponseCode {
@Tag(name = "Member", description = "회원 관리 API")
@Operation(summary = "회원 가입", description = "새로운 회원을 등록합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "회원 가입 성공",
content = @Content(schema = @Schema(implementation = CustomApiResponse.class))),
@ApiResponse(responseCode = "201", description = "회원 가입 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터",
content = @Content(schema = @Schema(implementation = CustomApiResponse.class))),
content = @Content(schema = @Schema(implementation = ApiResponseDto.class))),
@ApiResponse(responseCode = "409", description = "중복된 사용자 정보",
content = @Content(schema = @Schema(implementation = CustomApiResponse.class)))
content = @Content(schema = @Schema(implementation = ApiResponseDto.class)))
})
```

View File

@@ -1,14 +1,11 @@
package com.bio.bio_backend.domain.user.member.controller;
import com.bio.bio_backend.global.dto.CustomApiResponse;
import com.bio.bio_backend.global.dto.ApiResponseDto;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import com.bio.bio_backend.domain.user.member.dto.MemberDto;
@@ -27,6 +24,7 @@ import com.bio.bio_backend.global.utils.ApiResponseCode;
@Tag(name = "Member", description = "회원 관련 API")
@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
@Slf4j
public class MemberController {
@@ -38,16 +36,15 @@ public class MemberController {
@Operation(summary = "회원 가입", description = "새로운 회원을 등록합니다.")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "회원 가입 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"),
@ApiResponse(responseCode = "409", description = "중복된 사용자 정보")
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content(schema = @Schema(implementation = ApiResponseDto.class))),
@ApiResponse(responseCode = "409", description = "중복된 사용자 정보", content = @Content(schema = @Schema(implementation = ApiResponseDto.class)))
})
@PostMapping("/members")
public ResponseEntity<CustomApiResponse<CreateMemberResponseDto>> createMember(@RequestBody @Valid CreateMemberRequestDto requestDto) {
@PostMapping
public ResponseEntity<ApiResponseDto<CreateMemberResponseDto>> createMember(@RequestBody @Valid CreateMemberRequestDto requestDto) {
MemberDto member = memberMapper.toMemberDto(requestDto);
MemberDto createdMember = memberService.createMember(member);
CreateMemberResponseDto responseDto = memberMapper.toCreateMemberResponseDto(createdMember);
CustomApiResponse<CreateMemberResponseDto> apiResponse = CustomApiResponse.success(ApiResponseCode.COMMON_SUCCESS_CREATED, responseDto);
ApiResponseDto<CreateMemberResponseDto> apiResponse = ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS_CREATED, responseDto);
return ResponseEntity.status(HttpStatus.CREATED).body(apiResponse);
}
@@ -81,7 +78,7 @@ public class MemberController {
// }
// @PutMapping("/member")
// public CustomApiResponse<Void> updateMember(@RequestBody @Valid CreateMemberRequestDTO requestMember, @AuthenticationPrincipal MemberDTO registrant) {
// public ApiResponseDto<Void> updateMember(@RequestBody @Valid CreateMemberRequestDTO requestMember, @AuthenticationPrincipal MemberDTO registrant) {
// // 현재 JWT는 사용자 id 값을 통하여 생성, 회원정보 변경 시 JWT 재발급 여부 검토
// MemberDTO member = mapper.map(requestMember, MemberDTO.class);
@@ -93,31 +90,31 @@ public class MemberController {
// member.setRegSeq(registrant.getSeq());
// memberService.updateMember(member);
// return CustomApiResponse.success(ApiResponseCode.USER_INFO_CHANGE, null);
// return ApiResponseDto.success(ApiResponseCode.USER_INFO_CHANGE, null);
// }
// @DeleteMapping("/member")
// public CustomApiResponse<Void> deleteMember(@RequestBody @Valid CreateMemberRequestDTO requestMember){
// public ApiResponseDto<Void> deleteMember(@RequestBody @Valid CreateMemberRequestDTO requestMember){
// MemberDTO member = mapper.map(requestMember, MemberDTO.class);
// memberService.deleteMember(member);
// return CustomApiResponse.success(ApiResponseCode.USER_DELETE_SUCCESSFUL, null);
// return ApiResponseDto.success(ApiResponseCode.USER_DELETE_SUCCESSFUL, null);
// }
// @PostMapping("/logout")
// public CustomApiResponse<Void> logout(@AuthenticationPrincipal MemberDTO member) {
// public ApiResponseDto<Void> logout(@AuthenticationPrincipal MemberDTO member) {
// String id = member.getId();
// try {
// memberService.deleteRefreshToken(id);
// } catch (Exception e) {
// return CustomApiResponse.fail(ApiResponseCode.INTERNAL_SERVER_ERROR, null);
// return ApiResponseDto.fail(ApiResponseCode.INTERNAL_SERVER_ERROR, null);
// }
// return CustomApiResponse.success(ApiResponseCode.LOGOUT_SUCCESSFUL, null);
// return ApiResponseDto.success(ApiResponseCode.LOGOUT_SUCCESSFUL, null);
// }
}

View File

@@ -2,7 +2,6 @@ package com.bio.bio_backend.global.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
import org.springframework.context.annotation.Bean;
@@ -22,10 +21,6 @@ public class SwaggerConfig {
.version("v1.0.0")
.license(new License()
.name("STAM License")
.url("https://stam.kr/")))
.servers(List.of(
new Server().url("http://localhost:8080/service").description("Local Development Server"),
new Server().url("https://api.bio.com/service").description("Production Server")
));
.url("https://stam.kr/")));
}
}

View File

@@ -7,11 +7,10 @@ import com.bio.bio_backend.global.utils.ApiResponseCode;
import lombok.Data;
import lombok.RequiredArgsConstructor;
//공통 response Class
@Data
@RequiredArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CustomApiResponse<T> {
public class ApiResponseDto<T> {
private int code;
private String message;
@@ -20,19 +19,19 @@ public class CustomApiResponse<T> {
private static final int SUCCESS = 200;
private CustomApiResponse(int code, String message, String description, T data){
private ApiResponseDto(int code, String message, String description, T data){
this.code = code;
this.message = message;
this.description = description;
this.data = data;
}
public static <T> CustomApiResponse<T> success(ApiResponseCode responseCode, T data) {
return new CustomApiResponse<T>(SUCCESS, responseCode.name(), responseCode.getDescription(), data);
public static <T> ApiResponseDto<T> success(ApiResponseCode responseCode, T data) {
return new ApiResponseDto<T>(SUCCESS, responseCode.name(), responseCode.getDescription(), data);
}
public static <T> CustomApiResponse<T> fail(ApiResponseCode responseCode, T data) {
return new CustomApiResponse<T>(responseCode.getStatusCode(), responseCode.name(), responseCode.getDescription(), data);
public static <T> ApiResponseDto<T> fail(ApiResponseCode responseCode, T data) {
return new ApiResponseDto<T>(responseCode.getStatusCode(), responseCode.name(), responseCode.getDescription(), data);
}
}

View File

@@ -14,7 +14,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import com.bio.bio_backend.global.dto.CustomApiResponse;
import com.bio.bio_backend.global.dto.ApiResponseDto;
import com.bio.bio_backend.global.utils.ApiResponseCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -36,14 +36,14 @@ public class CustomAuthenticationFailureHandler implements AuthenticationFailure
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
CustomApiResponse<String> apiResponse;
ApiResponseDto<String> apiResponse;
if (exception instanceof UsernameNotFoundException) {
apiResponse = CustomApiResponse.fail(ApiResponseCode.USER_NOT_FOUND, null);
apiResponse = ApiResponseDto.fail(ApiResponseCode.USER_NOT_FOUND, null);
} else if (exception instanceof BadCredentialsException) {
apiResponse = CustomApiResponse.fail(ApiResponseCode.COMMON_UNAUTHORIZED, null);
apiResponse = ApiResponseDto.fail(ApiResponseCode.COMMON_UNAUTHORIZED, null);
} else {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
apiResponse = CustomApiResponse.fail(ApiResponseCode.COMMON_INTERNAL_SERVER_ERROR, null);
apiResponse = ApiResponseDto.fail(ApiResponseCode.COMMON_INTERNAL_SERVER_ERROR, null);
}
String jsonResponse = objectMapper.writeValueAsString(apiResponse);

View File

@@ -1,92 +1,32 @@
package com.bio.bio_backend.global.exception;
import java.util.Objects;
import lombok.Getter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.SignatureException;
import com.bio.bio_backend.global.dto.CustomApiResponse;
import com.bio.bio_backend.global.dto.ApiResponseDto;
import com.bio.bio_backend.global.utils.ApiResponseCode;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(HttpMessageNotReadableException.class)
public CustomApiResponse<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
if (Objects.requireNonNull(e.getMessage()).contains("JSON parse error")) {
return CustomApiResponse.fail(ApiResponseCode.COMMON_FORMAT_WRONG, null);
}
return CustomApiResponse.fail(ApiResponseCode.COMMON_BAD_REQUEST, null);
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public CustomApiResponse<Void> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
return CustomApiResponse.fail(ApiResponseCode.COMMON_METHOD_NOT_ALLOWED, null);
}
@ExceptionHandler(NoResourceFoundException.class)
public CustomApiResponse<Void> handleNoResourceFoundException(NoResourceFoundException e){
return CustomApiResponse.fail(ApiResponseCode.COMMON_NOT_FOUND, null);
}
@ExceptionHandler(IllegalArgumentException.class)
public CustomApiResponse<Void> handleIllegalArgumentException(IllegalArgumentException e){
return CustomApiResponse.fail(ApiResponseCode.COMMON_ARGUMENT_NOT_VALID, null);
}
@ExceptionHandler(IndexOutOfBoundsException.class)
public CustomApiResponse<Void> handleIndexOutOfBoundsException(IndexOutOfBoundsException e){
return CustomApiResponse.fail(ApiResponseCode.COMMON_INDEX_OUT_OF_BOUND, null);
}
@ExceptionHandler(SignatureException.class)
public CustomApiResponse<Void> handleSignatureException(SignatureException e) {
return CustomApiResponse.fail(ApiResponseCode.JWT_SIGNATURE_MISMATCH, null);
}
@ExceptionHandler(MalformedJwtException.class)
public CustomApiResponse<Void> handleMalformedJwtException(MalformedJwtException e) {
return CustomApiResponse.fail(ApiResponseCode.JWT_SIGNATURE_MISMATCH, null);
}
@ExceptionHandler(JwtException.class)
public CustomApiResponse<Void> handleJwtExceptionException(JwtException e) {
return CustomApiResponse.fail(ApiResponseCode.JWT_TOKEN_NULL, null);
}
@ExceptionHandler(ExpiredJwtException.class)
public CustomApiResponse<Void> handleExpiredJwtException(ExpiredJwtException e) {
return CustomApiResponse.fail(ApiResponseCode.JWT_TOKEN_EXPIRED, null);
}
@ExceptionHandler(JsonProcessingException.class)
public CustomApiResponse<Void> handleExpiredJwtException(JsonProcessingException e) {
return CustomApiResponse.fail(ApiResponseCode.COMMON_JSON_PROCESSING_EXCEPTION, null);
}
@ExceptionHandler(ApiException.class)
public CustomApiResponse<Void> handleApiException(ApiException e) {
return CustomApiResponse.fail(e.getResponseCode(), null);
public ResponseEntity<ApiResponseDto<Void>> handleApiException(ApiException e) {
ApiResponseDto<Void> response = ApiResponseDto.fail(e.getResponseCode(), null);
return ResponseEntity.status(e.getResponseCode().getStatusCode()).body(response);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public CustomApiResponse<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
public ResponseEntity<ApiResponseDto<Object>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
// 검증 실패한 필드들의 상세 오류 정보 추출
var errors = e.getBindingResult().getFieldErrors().stream()
.map(error -> new ValidationError(error.getField(), error.getDefaultMessage()))
.toList();
return CustomApiResponse.fail(ApiResponseCode.COMMON_ARGUMENT_NOT_VALID, errors);
ApiResponseDto<Object> response = ApiResponseDto.fail(ApiResponseCode.COMMON_ARGUMENT_NOT_VALID, errors);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
// 검증 오류 상세 정보를 위한 내부 클래스

View File

@@ -14,7 +14,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import com.bio.bio_backend.global.dto.CustomApiResponse;
import com.bio.bio_backend.global.dto.ApiResponseDto;
import com.bio.bio_backend.global.utils.ApiResponseCode;
@Component
@@ -35,8 +35,7 @@ public class JwtAccessDeniedHandler implements AccessDeniedHandler {
} else {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getWriter(),
CustomApiResponse.fail(ApiResponseCode.COMMON_FORBIDDEN, null));
new ObjectMapper().writeValue(response.getWriter(), ApiResponseDto.fail(ApiResponseCode.COMMON_FORBIDDEN, null));
}
}
}

View File

@@ -6,7 +6,7 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import com.bio.bio_backend.global.dto.CustomApiResponse;
import com.bio.bio_backend.global.dto.ApiResponseDto;
import com.bio.bio_backend.domain.user.member.dto.LoginRequestDto;
import com.bio.bio_backend.domain.user.member.dto.LoginResponseDto;
import com.bio.bio_backend.domain.user.member.dto.MemberDto;
@@ -116,6 +116,6 @@ public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilte
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getWriter(),
CustomApiResponse.success(ApiResponseCode.LOGIN_SUCCESSFUL, memberData));
ApiResponseDto.success(ApiResponseCode.LOGIN_SUCCESSFUL, memberData));
}
}

View File

@@ -20,7 +20,7 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import com.bio.bio_backend.global.dto.CustomApiResponse;
import com.bio.bio_backend.global.dto.ApiResponseDto;
import com.bio.bio_backend.domain.user.member.service.MemberService;
import com.bio.bio_backend.global.utils.ApiResponseCode;
import com.bio.bio_backend.global.utils.JwtUtils;
@@ -50,7 +50,7 @@ public class JwtTokenFilter extends OncePerRequestFilter {
String refreshToken = jwtUtils.extractRefreshJwtFromCookie(request);
if(accessToken == null){
sendJsonResponse(response, CustomApiResponse.fail(ApiResponseCode.JWT_TOKEN_NULL, null));
sendJsonResponse(response, ApiResponseDto.fail(ApiResponseCode.JWT_TOKEN_NULL, null));
return;
}
@@ -83,7 +83,7 @@ public class JwtTokenFilter extends OncePerRequestFilter {
response.setHeader("Authorization", "Bearer " + newAccessToken);
filterChain.doFilter(request, response);
} else {
sendJsonResponse(response, CustomApiResponse.fail(ApiResponseCode.ALL_TOKEN_INVALID, null));
sendJsonResponse(response, ApiResponseDto.fail(ApiResponseCode.ALL_TOKEN_INVALID, null));
}
} catch (Exception e) {
request.setAttribute("exception", e);
@@ -92,7 +92,7 @@ public class JwtTokenFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response);
}
private void sendJsonResponse(HttpServletResponse response, CustomApiResponse<?> apiResponse) throws IOException {
private void sendJsonResponse(HttpServletResponse response, ApiResponseDto<?> apiResponse) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.setStatus(apiResponse.getCode());