From 477415b3baa698728149965f25edeed0ec3e1399 Mon Sep 17 00:00:00 2001 From: pricelees Date: Mon, 18 Aug 2025 15:40:49 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20LocalDateTime=20/=20OffsetDateTime?= =?UTF-8?q?=EC=9D=84=20ISO=208601=20=ED=98=95=EC=8B=9D=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EC=A7=81=EB=A0=AC=ED=99=94=ED=95=98=EB=8A=94=20ObjectMapper?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/common/config/JacksonConfig.kt | 82 +++++++++++++------ .../common/config/JacksonConfigTest.kt | 23 ++++++ 2 files changed, 82 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/roomescape/common/config/JacksonConfig.kt b/src/main/kotlin/roomescape/common/config/JacksonConfig.kt index 9af1bf32..ce1d335e 100644 --- a/src/main/kotlin/roomescape/common/config/JacksonConfig.kt +++ b/src/main/kotlin/roomescape/common/config/JacksonConfig.kt @@ -14,16 +14,21 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import roomescape.common.exception.CommonErrorCode import roomescape.common.exception.RoomescapeException -import java.time.LocalDate -import java.time.LocalTime +import java.time.* import java.time.format.DateTimeFormatter @Configuration class JacksonConfig { + companion object { + private val ISO_OFFSET_DATE_TIME_FORMATTER: DateTimeFormatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX") + } + @Bean fun objectMapper(): ObjectMapper = ObjectMapper() .registerModule(javaTimeModule()) + .registerModule(dateTimeModule()) .registerModule(kotlinModule()) .registerModule(longIdModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) @@ -52,28 +57,59 @@ class JacksonConfig { simpleModule.addDeserializer(Long::class.java, StringToLongDeserializer()) return simpleModule } -} -class LongToStringSerializer : JsonSerializer() { - override fun serialize(value: Long?, gen: JsonGenerator, serializers: SerializerProvider) { - if (value == null) { - gen.writeNull() - } else { - gen.writeString(value.toString()) - } - } -} - -class StringToLongDeserializer : JsonDeserializer() { - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Long? { - val text = p.text - if (text.isNullOrBlank()) { - return null - } - return try { - text.toLong() - } catch (_: NumberFormatException) { - throw RoomescapeException(CommonErrorCode.INVALID_INPUT_VALUE) + private fun dateTimeModule(): SimpleModule { + val simpleModule = SimpleModule() + simpleModule.addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer()) + simpleModule.addSerializer(OffsetDateTime::class.java, OffsetDateTimeSerializer()) + return simpleModule + } + + class LongToStringSerializer : JsonSerializer() { + override fun serialize(value: Long?, gen: JsonGenerator, serializers: SerializerProvider) { + if (value == null) { + gen.writeNull() + } else { + gen.writeString(value.toString()) + } + } + } + + class StringToLongDeserializer : JsonDeserializer() { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Long? { + val text = p.text + if (text.isNullOrBlank()) { + return null + } + return try { + text.toLong() + } catch (_: NumberFormatException) { + throw RoomescapeException(CommonErrorCode.INVALID_INPUT_VALUE) + } + } + } + + class LocalDateTimeSerializer : JsonSerializer() { + override fun serialize( + value: LocalDateTime, + gen: JsonGenerator, + serializers: SerializerProvider + ) { + value.atZone(ZoneId.systemDefault()) + .toOffsetDateTime() + .also { + gen.writeString(it.format(ISO_OFFSET_DATE_TIME_FORMATTER)) + } + } + } + + class OffsetDateTimeSerializer : JsonSerializer() { + override fun serialize( + value: OffsetDateTime, + gen: JsonGenerator, + serializers: SerializerProvider + ) { + gen.writeString(value.format(ISO_OFFSET_DATE_TIME_FORMATTER)) } } } diff --git a/src/test/kotlin/roomescape/common/config/JacksonConfigTest.kt b/src/test/kotlin/roomescape/common/config/JacksonConfigTest.kt index 98ca4e91..1f6e5305 100644 --- a/src/test/kotlin/roomescape/common/config/JacksonConfigTest.kt +++ b/src/test/kotlin/roomescape/common/config/JacksonConfigTest.kt @@ -7,7 +7,10 @@ import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain import java.time.LocalDate +import java.time.LocalDateTime import java.time.LocalTime +import java.time.OffsetDateTime +import java.time.ZoneOffset class JacksonConfigTest( private val objectMapper: ObjectMapper = JacksonConfig().objectMapper() @@ -66,4 +69,24 @@ class JacksonConfigTest( deserialized shouldBe number } } + + context("OffsetDateTime은 ISO 8601 형식으로 직렬화된다.") { + val date = LocalDate.of(2025, 7, 14) + val time = LocalTime.of(12, 30, 0) + val dateTime = OffsetDateTime.of(date, time, ZoneOffset.ofHours(9)) + val serialized: String = objectMapper.writeValueAsString(dateTime) + + test("OffsetDateTime 직렬화") { + serialized shouldBe "\"2025-07-14T12:30:00+09:00\"" + } + } + + context("LocalDateTime은 ISO 8601 형식으로 직렬화된다.") { + val dateTime = LocalDateTime.of(2025, 7, 14, 12, 30, 0) + val serialized: String = objectMapper.writeValueAsString(dateTime) + + test("LocalDateTime 직렬화") { + serialized shouldBe "\"2025-07-14T12:30:00+09:00\"" + } + } })