Skip to content

Commit

Permalink
Merge pull request #29 from KNU-HAEDAL-Website/feat-swagger-issue-24
Browse files Browse the repository at this point in the history
feat: Swagger 문서 작성을 위한 기능 구현
  • Loading branch information
tfer2442 authored May 4, 2024
2 parents 967c8b9 + d8abf66 commit 3a6b99e
Show file tree
Hide file tree
Showing 21 changed files with 388 additions and 19 deletions.
Binary file modified haedal-web-0.0.1-SNAPSHOT.jar
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {

http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login", "/", "/join/**", "reissue").permitAll()
.requestMatchers("/login", "/", "/join/**", "/reissue", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/admin").hasRole("CANDIDATE")
.anyRequest().authenticated());

Expand Down
214 changes: 214 additions & 0 deletions src/main/java/com/haedal/haedalweb/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package com.haedal.haedalweb.config;

import com.haedal.haedalweb.constants.ErrorCode;
import com.haedal.haedalweb.constants.ResponseCode;
import com.haedal.haedalweb.constants.SuccessCode;
import com.haedal.haedalweb.dto.ErrorResponse;
import com.haedal.haedalweb.dto.SuccessResponse;
import com.haedal.haedalweb.swagger.ApiErrorCodeExample;
import com.haedal.haedalweb.swagger.ApiErrorCodeExamples;
import com.haedal.haedalweb.swagger.ApiSuccessCodeExample;
import com.haedal.haedalweb.swagger.ApiSuccessCodeExamples;
import com.haedal.haedalweb.swagger.ExampleHolder;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.validation.FieldError;
import org.springframework.web.method.HandlerMethod;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;


@OpenAPIDefinition(
info = @Info(title = "HAEDAL-WEB API 명세서",
description = "해달 웹 백엔드 API",
version = "1.0")

)
@Configuration
public class SwaggerConfig {
private static final String BEARER_TOKEN_PREFIX = "Bearer";

@Bean
public OpenAPI openAPI() {
String accessToken = "Access Token (Bearer)";

SecurityRequirement securityRequirement = new SecurityRequirement()
.addList(accessToken);

SecurityScheme accessTokenSecurityScheme = new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme(BEARER_TOKEN_PREFIX)
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
.name(HttpHeaders.AUTHORIZATION);

Components components = new Components()
.addSecuritySchemes(accessToken, accessTokenSecurityScheme);

return new OpenAPI()
.addSecurityItem(securityRequirement)
.components(components);
}

@Bean
public OperationCustomizer customize() {
return (Operation operation, HandlerMethod handlerMethod) -> {
ApiErrorCodeExamples apiErrorCodeExamples = handlerMethod.getMethodAnnotation(
ApiErrorCodeExamples.class);

// @ApiErrorCodeExamples 어노테이션이 붙어있다면
if (apiErrorCodeExamples != null) {
generateResponseCodeResponseExample(operation, apiErrorCodeExamples.value());
} else {
ApiErrorCodeExample apiErrorCodeExample = handlerMethod.getMethodAnnotation(
ApiErrorCodeExample.class);

// @ApiErrorCodeExamples 어노테이션이 붙어있지 않고
// @ApiErrorCodeExample 어노테이션이 붙어있다면
if (apiErrorCodeExample != null) {
generateResponseCodeResponseExample(operation, apiErrorCodeExample.value());
}
}

ApiSuccessCodeExamples apiSuccessCodeExamples = handlerMethod.getMethodAnnotation(
ApiSuccessCodeExamples.class);

if (apiSuccessCodeExamples != null) {
generateResponseCodeResponseExample(operation, apiSuccessCodeExamples.value());
} else {
ApiSuccessCodeExample apiSuccessCodeExample = handlerMethod.getMethodAnnotation(
ApiSuccessCodeExample.class);

if (apiSuccessCodeExample != null) {
generateResponseCodeResponseExample(operation, apiSuccessCodeExample.value());
}
}

return operation;
};
}

private void generateResponseCodeResponseExample(Operation operation, ResponseCode[] responseCodes) {
ApiResponses responses = operation.getResponses();

Map<Integer, List<ExampleHolder>> statusWithExampleHolders = Arrays.stream(responseCodes)
.map(
responseCode -> ExampleHolder.builder()
.holder(getSwaggerExample(responseCode))
.httpStatus(responseCode.getHttpStatus().value())
.name(responseCode.name())
.build()
)
.collect(Collectors.groupingBy(ExampleHolder::getHttpStatus));

addExamplesToResponses(responses, statusWithExampleHolders);
}

// 단일 에러 응답값 예시 추가
private void generateResponseCodeResponseExample(Operation operation, ResponseCode responseCode) {
ApiResponses responses = operation.getResponses();

// ExampleHolder 객체 생성 및 ApiResponses에 추가
ExampleHolder exampleHolder = ExampleHolder.builder()
.holder(getSwaggerExample(responseCode))
.name(responseCode.name())
.httpStatus(responseCode.getHttpStatus().value())
.build();

addExamplesToResponses(responses, exampleHolder);
}

// ErrorResponseDto 형태의 예시 객체 생성
private Example getSwaggerExample(ResponseCode responseCode) {
Example example = new Example();

if (responseCode instanceof ErrorCode) {
List<ErrorResponse.ValidationError> validationErrorList = new ArrayList<>();

if (responseCode == ErrorCode.INVALID_PARAMETER) {
FieldError fieldError = new FieldError("objectName", "field", "defaultMessage");
validationErrorList.add(ErrorResponse.ValidationError.of(fieldError));
}

ErrorResponse errorResponse = ErrorResponse.builder()
.code(((ErrorCode) responseCode).getCode())
.message(responseCode.getMessage())
.errors(validationErrorList)
.build();

example.setValue(errorResponse);

return example;
}


SuccessResponse successResponse = SuccessResponse.builder()
.success(((SuccessCode) responseCode).getSuccess())
.message(responseCode.getMessage())
.build();

example.setValue(successResponse);

return example;
}

private void addExamplesToResponses(ApiResponses responses,
Map<Integer, List<ExampleHolder>> statusWithExampleHolders) {

if (responses != null && responses.containsKey("200")) {
responses.remove("200");
}

statusWithExampleHolders.forEach(
(status, v) -> {
Content content = new Content();
MediaType mediaType = new MediaType();
ApiResponse apiResponse = new ApiResponse();

v.forEach(
exampleHolder -> mediaType.addExamples(
exampleHolder.getName(),
exampleHolder.getHolder()
)
);
content.addMediaType("application/json", mediaType);
apiResponse.setContent(content);
responses.addApiResponse(String.valueOf(status), apiResponse);
}
);
}

private void addExamplesToResponses(ApiResponses responses, ExampleHolder exampleHolder) {
Content content = new Content();
MediaType mediaType = new MediaType();
ApiResponse apiResponse = new ApiResponse();

mediaType.addExamples(exampleHolder.getName(), exampleHolder.getHolder());
content.addMediaType("application/json", mediaType);
apiResponse.content(content);

if (responses != null && responses.containsKey("200")) {
responses.remove("200");
}

responses.addApiResponse(String.valueOf(exampleHolder.getHttpStatus()), apiResponse);
}
}
23 changes: 12 additions & 11 deletions src/main/java/com/haedal/haedalweb/constants/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@

@RequiredArgsConstructor
@Getter
public enum ErrorCode {
DUPLICATED_USER_ID(HttpStatus.CONFLICT, "중복된 아이디가 존재합니다."),
DUPLICATED_STUDENT_NUMBER(HttpStatus.CONFLICT, "중복된 학번이 존재합니다."),
INVALID_LOGIN_CONTENTS_TYPE(HttpStatus.BAD_REQUEST, "지원하지 않는 형식입니다."),
FAILED_LOGIN(HttpStatus.UNAUTHORIZED, "아이디 또는 비밀번호가 일치하지 않습니다."),
EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "Access Token has expired."),
INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "Access Token is invalid."),
NULL_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "Refresh Token is null."),
EXPIRED_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "Refresh Token has expired."),
INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "Refresh Token is invalid."),
INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "Parameter is invalid.");
public enum ErrorCode implements ResponseCode{
INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "001", "Parameter is invalid."),
DUPLICATED_USER_ID(HttpStatus.CONFLICT, "002", "중복된 아이디가 존재합니다."),
DUPLICATED_STUDENT_NUMBER(HttpStatus.CONFLICT, "003", "중복된 학번이 존재합니다."),
INVALID_LOGIN_CONTENTS_TYPE(HttpStatus.BAD_REQUEST, "004", "지원하지 않는 형식입니다."),
FAILED_LOGIN(HttpStatus.UNAUTHORIZED, "005", "아이디 또는 비밀번호가 일치하지 않습니다."),
EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "006", "Access Token has expired."),
INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "007", "Access Token is invalid."),
NULL_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "008", "Refresh Token is null."),
EXPIRED_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "009", "Refresh Token has expired."),
INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "010", "Refresh Token is invalid.");

private final HttpStatus httpStatus;
private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public final class LoginConstants {
public static final String ROLE_CLAIM = "role";
public static final String CATEGORY_CLAIM = "category";

public static final long ACCESS_TOKEN_EXPIRATION_TIME_MS = 36*1000;// 3600*1000;
public static final long REFRESH_TOKEN_EXPIRATION_TIME_MS = 86400*1000;
public static final long REFRESH_TOKEN_EXPIRATION_TIME_S = 86400; // 1 day
public static final long ACCESS_TOKEN_EXPIRATION_TIME_MS = 3600*1000; // 1 hour
public static final long REFRESH_TOKEN_EXPIRATION_TIME_MS = 86400*1000; // 1 day
public static final long REFRESH_TOKEN_EXPIRATION_TIME_S = 86400; // 1day
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.haedal.haedalweb.constants;

import org.springframework.http.HttpStatus;

public interface ResponseCode {
public HttpStatus getHttpStatus();
public String getMessage();
public String name();
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

@RequiredArgsConstructor
@Getter
public enum SuccessCode {
public enum SuccessCode implements ResponseCode{
JOIN_SUCCESS(HttpStatus.CREATED, true, "회원가입을 축하드립니다."),
UNIQUE_USER_ID(HttpStatus.OK, true, "사용 가능한 ID입니다."),
DUPLICATED_USER_ID(HttpStatus.OK, false, "중복된 ID입니다. 다른 ID를 입력해 주세요."),
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/haedal/haedalweb/controller/JoinController.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package com.haedal.haedalweb.controller;

import com.haedal.haedalweb.constants.ErrorCode;
import com.haedal.haedalweb.constants.SuccessCode;
import com.haedal.haedalweb.dto.JoinDTO;
import com.haedal.haedalweb.dto.SuccessResponse;
import com.haedal.haedalweb.swagger.ApiErrorCodeExamples;
import com.haedal.haedalweb.swagger.ApiSuccessCodeExample;
import com.haedal.haedalweb.swagger.ApiSuccessCodeExamples;
import com.haedal.haedalweb.util.ResponseUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -14,6 +21,7 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "회원가입 관련 API")
@RestController
@RequestMapping("/join")
public class JoinController {
Expand All @@ -23,13 +31,19 @@ public JoinController(JoinService joinService) {
this.joinService = joinService;
}

@Operation(summary = "회원가입")
@ApiSuccessCodeExample(SuccessCode.JOIN_SUCCESS)
@ApiErrorCodeExamples({ErrorCode.DUPLICATED_USER_ID, ErrorCode.DUPLICATED_STUDENT_NUMBER, ErrorCode.INVALID_PARAMETER})
@PostMapping
public ResponseEntity<SuccessResponse> resisterUser(@RequestBody @Valid JoinDTO joinDTO) {
joinService.createUserAccount(joinDTO);

return ResponseUtil.buildSuccessResponseEntity(SuccessCode.JOIN_SUCCESS);
}

@Operation(summary = "ID 중복확인")
@Parameter(name = "userId", description = "중복 확인할 ID")
@ApiSuccessCodeExamples({SuccessCode.UNIQUE_USER_ID, SuccessCode.DUPLICATED_USER_ID})
@GetMapping("/check-user-id")
public ResponseEntity<SuccessResponse> checkUserIdDuplicate(@RequestParam String userId) {
SuccessCode successCode = SuccessCode.UNIQUE_USER_ID;
Expand All @@ -40,6 +54,9 @@ public ResponseEntity<SuccessResponse> checkUserIdDuplicate(@RequestParam String
return ResponseUtil.buildSuccessResponseEntity(successCode);
}

@Operation(summary = "학번 중복확인")
@Parameter(name = "studentNumber", description = "중복 확인할 학번")
@ApiSuccessCodeExamples({SuccessCode.UNIQUE_STUDENT_NUMBER, SuccessCode.DUPLICATED_STUDENT_NUMBER})
@GetMapping("/check-student-number")
public ResponseEntity<SuccessResponse> checkStudentNumberDuplicate(@RequestParam Integer studentNumber) {
SuccessCode successCode = SuccessCode.UNIQUE_STUDENT_NUMBER;
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/com/haedal/haedalweb/controller/LoginController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.haedal.haedalweb.controller;

import com.haedal.haedalweb.constants.ErrorCode;
import com.haedal.haedalweb.constants.SuccessCode;
import com.haedal.haedalweb.dto.LoginDTO;
import com.haedal.haedalweb.swagger.ApiErrorCodeExamples;
import com.haedal.haedalweb.swagger.ApiSuccessCodeExample;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@Tag(name = "로그인 관련 API")
@Controller
public class LoginController {

@Operation(summary = "로그인 API")
@ApiSuccessCodeExample(SuccessCode.LOGIN_SUCCESS)
@ApiErrorCodeExamples({ErrorCode.INVALID_LOGIN_CONTENTS_TYPE, ErrorCode.FAILED_LOGIN})
@PostMapping("/login")
public void signIn(@RequestBody LoginDTO loginDTO) {
}

@Operation(summary = "로그아웃 API")
@ApiSuccessCodeExample(SuccessCode.LOGOUT_SUCCESS)
@ApiErrorCodeExamples({ErrorCode.NULL_REFRESH_TOKEN, ErrorCode.INVALID_REFRESH_TOKEN, ErrorCode.EXPIRED_REFRESH_TOKEN})
@PostMapping("/logout")
public void signIn() {
}
}
Loading

0 comments on commit 3a6b99e

Please sign in to comment.