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\"" + } + } })