Skip to content

Commit

Permalink
Feat: time format 설정 (#27)
Browse files Browse the repository at this point in the history
* ObjectMapper, SwaggerConfig에 수정
  • Loading branch information
Jaewon-pro authored Jul 31, 2024
1 parent f1e85ca commit 8a46335
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 66 deletions.
27 changes: 7 additions & 20 deletions src/main/java/com/dnd/runus/domain/example/ExampleController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,17 @@
import com.dnd.runus.global.exception.type.ErrorType;
import io.swagger.v3.oas.annotations.Operation;
import lombok.Getter;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.time.LocalTime;
import java.util.Map;

@RestController
@RequestMapping("/api/v1/examples")
public class ExampleController { // FIXME: 추후 삭제
@Operation(summary = "페이징 테스트, 복잡한 페이징(pageable) 결과는 나중에 정해서 간략화해도 좋을 것 같아요")
@GetMapping("/pagination")
public Page<?> a(Pageable pageable) {
List<String> list = List.of("a", "b", "c", "d", "e", "f", "g", "h", "i", "j");
List<String> sublist = list.subList(
(int) pageable.getOffset(), Math.min((int) pageable.getOffset() + pageable.getPageSize(), list.size()));
return new PageImpl<>(sublist, pageable, list.size());
}

@Operation(summary = "input 테스트, input을 반환합니다.")
@GetMapping("/input")
public String name(@RequestParam String input) {
Expand All @@ -41,18 +28,18 @@ public Map<String, String> headers(@RequestHeader(value = "authorization") Map<S
return headers;
}

@Operation(summary = "데이터 테스트, 시간 관련 형식도 정하면 좋을 것 같아요")
@Operation(summary = "시간, 날짜 데이터 테스트")
@GetMapping("/data")
public MyData data() {
return new MyData();
}

@Getter
public static class MyData {
int a;
String b = "b";
Instant c = Instant.now();
String d = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
int number = 0;
LocalTime time = LocalTime.now();
LocalDate date = LocalDate.now();
LocalDateTime dateTime = LocalDateTime.now();
}

@GetMapping("/empty")
Expand Down
89 changes: 43 additions & 46 deletions src/main/java/com/dnd/runus/global/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,11 @@
import io.swagger.v3.core.jackson.ModelResolver;
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.info.Info;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.QueryParameter;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
Expand All @@ -28,15 +25,14 @@
import org.springframework.core.env.Environment;

import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.Optional;
import java.util.stream.Stream;

import static com.dnd.runus.global.constant.TimeConstant.*;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import static org.springframework.security.config.Elements.JWT;
import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE;
Expand Down Expand Up @@ -70,11 +66,24 @@ ModelResolver modelResolver(ObjectMapper objectMapper) { // Load formats from Ob
}

@Bean
OpenApiCustomizer projectPageableCustomizer() {
return openApi -> customizeOperations(openApi, "pageable", (operation, parameter) -> {
operation.getParameters().remove(parameter);
pageableParameter().forEach(operation::addParametersItem);
});
OpenApiCustomizer timeFormatCustomizer() {
return openApi -> Optional.ofNullable(openApi.getComponents())
.map(Components::getSchemas)
.ifPresent(schemas -> schemas.forEach((name, schema) -> {
@SuppressWarnings("unchecked") // schema.getProperties() returns Map<String, Schema>
Map<String, Schema<?>> properties = Optional.ofNullable(schema.getProperties())
.map(props -> (Map<String, Schema<?>>) props)
.orElse(Collections.emptyMap());
properties.forEach((propertyName, propertySchema) -> {
if ("date-time".equals(propertySchema.getFormat())) {
updatePropertySchema(properties, propertyName, DATE_TIME_FORMAT_EXAMPLE);
} else if ("date".equals(propertySchema.getFormat())) {
updatePropertySchema(properties, propertyName, DATE_FORMAT_EXAMPLE);
} else if ("#/components/schemas/LocalTime".equals(propertySchema.get$ref())) {
updatePropertySchema(properties, propertyName, TIME_FORMAT_EXAMPLE);
}
});
}));
}

@Bean
Expand All @@ -100,18 +109,6 @@ OperationCustomizer successResponseBodyWrapper() {

@Bean
OperationCustomizer apiErrorTypeCustomizer() {
Function<ErrorType, Schema<?>> getErrorSchema = type -> {
Schema<?> errorSchema = new Schema<>();
errorSchema.properties(Map.of(
"statusCode",
new Schema<>().type("int").example(type.httpStatus().value()),
"code",
new Schema<>().type("string").example(type.code()),
"message",
new Schema<>().type("string").example(type.message())));
return errorSchema;
};

return (operation, handlerMethod) -> {
ApiErrorType apiErrorType = handlerMethod.getMethodAnnotation(ApiErrorType.class);
if (apiErrorType == null) {
Expand All @@ -121,9 +118,7 @@ OperationCustomizer apiErrorTypeCustomizer() {
Stream.of(apiErrorType.value())
.sorted(Comparator.comparingInt(t -> t.httpStatus().value()))
.forEach(type -> {
Content content = new Content()
.addMediaType(
APPLICATION_JSON_VALUE, new MediaType().schema(getErrorSchema.apply(type)));
Content content = new Content().addMediaType(APPLICATION_JSON_VALUE, errorMediaType(type));
operation
.getResponses()
.put(
Expand All @@ -148,28 +143,30 @@ private Info info() {
.version(buildProperties.getVersion());
}

private void customizeOperations(OpenAPI openApi, String paramName, BiConsumer<Operation, Parameter> customizer) {
openApi.getPaths().values().stream()
.flatMap(pathItem -> pathItem.readOperations().stream())
.filter(operation -> !ObjectUtils.isEmpty(operation.getParameters()))
.forEach(operation -> operation.getParameters().stream()
.filter(parameter -> paramName.equals(parameter.getName()))
.findFirst()
.ifPresent(parameter -> customizer.accept(operation, parameter)));
private String formatTime(Instant instant) {
return instant.atZone(SERVER_TIMEZONE_ID).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT));
}

private List<Parameter> pageableParameter() {
Schema<?> pageSchema = new StringSchema().example("0").description("페이지 번호 (0부터 시작)");
Schema<?> sizeSchema = new StringSchema().example("20").description("한 페이지에 보여줄 항목 수");
Schema<?> sortSchema = new StringSchema().example("id,asc").description("정렬 조건 (ex. id,asc or id,desc)");

return List.of(
new QueryParameter().name("page").schema(pageSchema),
new QueryParameter().name("size").schema(sizeSchema),
new QueryParameter().name("sort").schema(sortSchema));
private MediaType errorMediaType(ErrorType type) {
Schema<?> errorSchema = new Schema<>();
errorSchema.properties(Map.of(
"statusCode",
new Schema<>().type("int").example(type.httpStatus().value()),
"code",
new Schema<>().type("string").example(type.code()),
"message",
new Schema<>().type("string").example(type.message())));
return new MediaType().schema(errorSchema);
}

private String formatTime(Instant instant) {
return instant.atZone(ZoneId.of("Asia/Seoul")).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
private void updatePropertySchema(Map<String, Schema<?>> properties, String propertyName, String example) {
Schema<?> propertySchema = properties.get(propertyName);
properties.replace(
propertyName,
new StringSchema()
.type(propertySchema.getType())
.format(propertySchema.getFormat())
.example(example)
.description(propertySchema.getDescription()));
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/dnd/runus/global/constant/TimeConstant.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.dnd.runus.global.constant;

import java.time.ZoneId;

public final class TimeConstant {
TimeConstant() {}

public static final String TIME_FORMAT = "HH:mm:ss";
public static final String TIME_FORMAT_EXAMPLE = "01:23:34";
public static final String DATE_FORMAT = "yyyy-MM-dd";
public static final String DATE_FORMAT_EXAMPLE = "2024-01-12";
public static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DATE_TIME_FORMAT_EXAMPLE = "2024-01-12 01:23:34";
public static final String SERVER_TIMEZONE = "Asia/Seoul";
public static final ZoneId SERVER_TIMEZONE_ID = ZoneId.of(SERVER_TIMEZONE);
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,47 @@
package com.dnd.runus.presentation.config;

import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.ser.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.format.DateTimeFormatter;
import java.time.*;

import static com.dnd.runus.global.constant.TimeConstant.*;
import static java.time.format.DateTimeFormatter.ofPattern;

@Configuration
public class ObjectMapperConfig {
@Bean
ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());

DateTimeFormatter dateFormatter = ofPattern(DATE_FORMAT);
DateTimeFormatter dateTimeFormatter = ofPattern(DATE_TIME_FORMAT);

Module dateTimeModule = new SimpleModule()
.addSerializer(LocalTime.class, new LocalTimeSerializer(ofPattern(TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(dateFormatter))
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormatter))
.addSerializer(
OffsetDateTime.class,
new OffsetDateTimeSerializer(
OffsetDateTimeSerializer.INSTANCE, false, false, dateTimeFormatter) {})
.addSerializer(
Instant.class,
new InstantSerializer(
InstantSerializer.INSTANCE,
false,
false,
dateTimeFormatter.withZone(SERVER_TIMEZONE_ID)) {});

objectMapper.registerModule(dateTimeModule);

return objectMapper;
}
}

0 comments on commit 8a46335

Please sign in to comment.