[#54] 애플리케이션 배포 #55

Merged
pricelees merged 20 commits from infra/#54 into main 2025-10-06 02:42:13 +00:00
5 changed files with 30 additions and 104 deletions
Showing only changes of commit c270ca9f67 - Show all commits

View File

@ -1,11 +1,8 @@
package com.sangdol.common.web.config package com.sangdol.common.web.config
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer
@ -15,19 +12,13 @@ import com.fasterxml.jackson.module.kotlin.kotlinModule
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
import java.time.OffsetDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@Configuration @Configuration
class JacksonConfig { class JacksonConfig {
companion object { companion object {
private val ISO_OFFSET_DATE_TIME_FORMATTER: DateTimeFormatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX")
private val LOCAL_TIME_FORMATTER: DateTimeFormatter = private val LOCAL_TIME_FORMATTER: DateTimeFormatter =
DateTimeFormatter.ofPattern("HH:mm") DateTimeFormatter.ofPattern("HH:mm")
} }
@ -35,9 +26,9 @@ class JacksonConfig {
@Bean @Bean
fun objectMapper(): ObjectMapper = ObjectMapper() fun objectMapper(): ObjectMapper = ObjectMapper()
.registerModule(javaTimeModule()) .registerModule(javaTimeModule())
.registerModule(dateTimeModule())
.registerModule(kotlinModule()) .registerModule(kotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
private fun javaTimeModule(): JavaTimeModule = JavaTimeModule() private fun javaTimeModule(): JavaTimeModule = JavaTimeModule()
.addSerializer( .addSerializer(
@ -56,35 +47,4 @@ class JacksonConfig {
LocalTime::class.java, LocalTime::class.java,
LocalTimeDeserializer(LOCAL_TIME_FORMATTER) LocalTimeDeserializer(LOCAL_TIME_FORMATTER)
) as JavaTimeModule ) as JavaTimeModule
private fun dateTimeModule(): SimpleModule {
val simpleModule = SimpleModule()
simpleModule.addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer())
simpleModule.addSerializer(OffsetDateTime::class.java, OffsetDateTimeSerializer())
return simpleModule
}
class LocalDateTimeSerializer : JsonSerializer<LocalDateTime>() {
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<OffsetDateTime>() {
override fun serialize(
value: OffsetDateTime,
gen: JsonGenerator,
serializers: SerializerProvider
) {
gen.writeString(value.format(ISO_OFFSET_DATE_TIME_FORMATTER))
}
}
} }

View File

@ -7,10 +7,7 @@ import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldContain
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
class JacksonConfigTest : FunSpec({ class JacksonConfigTest : FunSpec({
@ -55,38 +52,4 @@ class JacksonConfigTest : FunSpec({
}.message shouldContain "Text '$hour:$minute:$sec' could not be parsed" }.message shouldContain "Text '$hour:$minute:$sec' could not be parsed"
} }
} }
context("Long 타입은 문자열로 (역)직렬화된다.") {
val number = 1234567890L
val serialized: String = objectMapper.writeValueAsString(number)
val deserialized: Long = objectMapper.readValue(serialized, Long::class.java)
test("Long 직렬화") {
serialized shouldBe "$number"
}
test("Long 역직렬화") {
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\""
}
}
}) })

View File

@ -1,5 +1,5 @@
import { isLoginRequiredError } from '@_api/apiClient'; import {isLoginRequiredError} from '@_api/apiClient';
import type { AuditInfo } from '@_api/common/commonTypes'; import type {AuditInfo} from '@_api/common/commonTypes';
import { import {
createSchedule, createSchedule,
deleteSchedule, deleteSchedule,
@ -7,15 +7,16 @@ import {
fetchScheduleAudit, fetchScheduleAudit,
updateSchedule updateSchedule
} from '@_api/schedule/scheduleAPI'; } from '@_api/schedule/scheduleAPI';
import { type AdminScheduleSummaryResponse, ScheduleStatus, } from '@_api/schedule/scheduleTypes'; import {type AdminScheduleSummaryResponse, ScheduleStatus,} from '@_api/schedule/scheduleTypes';
import { getStores } from '@_api/store/storeAPI'; import {getStores} from '@_api/store/storeAPI';
import { type SimpleStoreResponse } from '@_api/store/storeTypes'; import {type SimpleStoreResponse} from '@_api/store/storeTypes';
import { fetchActiveThemes } from '@_api/theme/themeAPI'; import {fetchActiveThemes} from '@_api/theme/themeAPI';
import { DifficultyKoreanMap, type SimpleActiveThemeResponse, type ThemeInfoResponse } from '@_api/theme/themeTypes'; import {DifficultyKoreanMap, type SimpleActiveThemeResponse, type ThemeInfoResponse} from '@_api/theme/themeTypes';
import { useAdminAuth } from '@_context/AdminAuthContext'; import {useAdminAuth} from '@_context/AdminAuthContext';
import '@_css/admin-schedule-page.css'; import '@_css/admin-schedule-page.css';
import React, { Fragment, useEffect, useState } from 'react'; import {formatDisplayDateTime} from '@_util/DateTimeFormatter';
import { useLocation, useNavigate } from 'react-router-dom'; import React, {Fragment, useEffect, useState} from 'react';
import {useLocation, useNavigate} from 'react-router-dom';
const getScheduleStatusText = (status: ScheduleStatus): string => { const getScheduleStatusText = (status: ScheduleStatus): string => {
switch (status) { switch (status) {
@ -332,10 +333,10 @@ const AdminSchedulePage: React.FC = () => {
<h4 className="audit-title"> </h4> <h4 className="audit-title"> </h4>
<div className="audit-body"> <div className="audit-body">
<p> <p>
<strong>:</strong> {new Date(detailedSchedules[schedule.id].audit!.createdAt).toLocaleString()} <strong>:</strong> {formatDisplayDateTime(detailedSchedules[schedule.id].audit!.createdAt)}
</p> </p>
<p> <p>
<strong>:</strong> {new Date(detailedSchedules[schedule.id].audit!.updatedAt).toLocaleString()} <strong>:</strong> {formatDisplayDateTime(detailedSchedules[schedule.id].audit!.updatedAt)}
</p> </p>
<p> <p>
<strong>:</strong> {detailedSchedules[schedule.id].audit!.createdBy.name}({detailedSchedules[schedule.id].audit!.createdBy.id}) <strong>:</strong> {detailedSchedules[schedule.id].audit!.createdBy.name}({detailedSchedules[schedule.id].audit!.createdBy.id})

View File

@ -1,17 +1,18 @@
import { isLoginRequiredError } from '@_api/apiClient'; import {isLoginRequiredError} from '@_api/apiClient';
import { fetchSidoList, fetchSigunguList } from '@_api/region/regionAPI'; import {fetchSidoList, fetchSigunguList} from '@_api/region/regionAPI';
import type { SidoResponse, SigunguResponse } from '@_api/region/regionTypes'; import type {SidoResponse, SigunguResponse} from '@_api/region/regionTypes';
import { createStore, deleteStore, getStoreDetail, getStores, updateStore } from '@_api/store/storeAPI'; import {createStore, deleteStore, getStoreDetail, getStores, updateStore} from '@_api/store/storeAPI';
import { import {
type SimpleStoreResponse, type SimpleStoreResponse,
type StoreDetailResponse, type StoreDetailResponse,
type StoreRegisterRequest, type StoreRegisterRequest,
type UpdateStoreRequest type UpdateStoreRequest
} from '@_api/store/storeTypes'; } from '@_api/store/storeTypes';
import { useAdminAuth } from '@_context/AdminAuthContext'; import {useAdminAuth} from '@_context/AdminAuthContext';
import '@_css/admin-store-page.css'; import '@_css/admin-store-page.css';
import React, { Fragment, useEffect, useState } from 'react'; import {formatDisplayDateTime} from '@_util/DateTimeFormatter';
import { useLocation, useNavigate } from 'react-router-dom'; import React, {Fragment, useEffect, useState} from 'react';
import {useLocation, useNavigate} from 'react-router-dom';
const AdminStorePage: React.FC = () => { const AdminStorePage: React.FC = () => {
const [stores, setStores] = useState<SimpleStoreResponse[]>([]); const [stores, setStores] = useState<SimpleStoreResponse[]>([]);
@ -297,10 +298,10 @@ const AdminStorePage: React.FC = () => {
:</strong> {detailedStores[store.id].region.code} :</strong> {detailedStores[store.id].region.code}
</p> </p>
<p> <p>
<strong>:</strong> {new Date(detailedStores[store.id].audit.createdAt).toLocaleString()} <strong>:</strong> {formatDisplayDateTime(detailedStores[store.id].audit.createdAt)}
</p> </p>
<p> <p>
<strong>:</strong> {new Date(detailedStores[store.id].audit.updatedAt).toLocaleString()} <strong>:</strong> {formatDisplayDateTime(detailedStores[store.id].audit.updatedAt)}
</p> </p>
<p> <p>
<strong>:</strong> {detailedStores[store.id].audit.createdBy.name}({detailedStores[store.id].audit.createdBy.id}) <strong>:</strong> {detailedStores[store.id].audit.createdBy.name}({detailedStores[store.id].audit.createdBy.id})

View File

@ -9,7 +9,8 @@ import {
import React, {useEffect, useState} from 'react'; import React, {useEffect, useState} from 'react';
import {useLocation, useNavigate, useParams} from 'react-router-dom'; import {useLocation, useNavigate, useParams} from 'react-router-dom';
import '@_css/admin-theme-edit-page.css'; import '@_css/admin-theme-edit-page.css';
import type { AuditInfo } from '@_api/common/commonTypes'; import type {AuditInfo} from '@_api/common/commonTypes';
import {formatDisplayDateTime} from '@_util/DateTimeFormatter';
interface ThemeFormData { interface ThemeFormData {
name: string; name: string;
@ -256,8 +257,8 @@ const AdminThemeEditPage: React.FC = () => {
<div className="audit-info"> <div className="audit-info">
<h4 className="audit-title"> </h4> <h4 className="audit-title"> </h4>
<div className="audit-body"> <div className="audit-body">
<p><strong>:</strong> {new Date(auditInfo.createdAt).toLocaleString()}</p> <p><strong>:</strong> {formatDisplayDateTime(auditInfo.createdAt)}</p>
<p><strong>:</strong> {new Date(auditInfo.updatedAt).toLocaleString()}</p> <p><strong>:</strong> {formatDisplayDateTime(auditInfo.updatedAt)}</p>
<p><strong>:</strong> {auditInfo.createdBy.name}</p> <p><strong>:</strong> {auditInfo.createdBy.name}</p>
<p><strong>:</strong> {auditInfo.updatedBy.name}</p> <p><strong>:</strong> {auditInfo.updatedBy.name}</p>
</div> </div>