From 8215492eea0fabb9966240c7f9040d4b05e6bde0 Mon Sep 17 00:00:00 2001 From: pricelees Date: Mon, 6 Oct 2025 02:42:13 +0000 Subject: [PATCH] =?UTF-8?q?[#54]=20=EC=95=A0=ED=94=8C=EB=A6=AC=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=B0=B0=ED=8F=AC=20(#55)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📝 관련 이슈 및 PR **PR과 관련된 이슈 번호** - #54 ## ✨ 작업 내용 - 애플리케이션 배포 - 1차 배포에서 각 Service의 Trace가 구분이 되지 않아 XxxService 클래스에 \@Observation을 적용하는 AOP 추가 - 불필요하게 느껴지는 Prometheus Actuator 요청과 스케쥴링 작업 Tracing 제외 - 애플리케이션이 UTC로 배포됨에 따라 발생하는 문제 해결을 위해 LocalDateTime, OffsetDateTime -> Instant 타입 변경 및 LocalDate, LocalTime은 KST로 비교하도록 수정 - 기존 로그의 가독성이 좋지 않아, 로그 메시지가 가장 먼저 보이도록 형식 수정 ## 🧪 테스트 - 실제 웹에 접속하여 전체적인 기능 점검 - 예약 처리 로직에서 미숙한 부분이 발견되어 다음 작업은 예약 처리 로직 개선 예정 ## 📚 참고 자료 및 기타 Reviewed-on: https://gitea.pricelees.me/pricelees/roomescape-refactored/pulls/55 Co-authored-by: pricelees Co-committed-by: pricelees --- Dockerfile | 25 ++- .../log/AbstractLogMaskingConverterTest.kt | 6 +- .../common/persistence/AuditingBaseEntity.kt | 6 +- .../persistence/TestAuditingBaseEntity.kt | 4 +- .../persistence/TestPersistableBaseEntity.kt | 4 +- .../TransactionExecutionUtilTest.kt | 6 +- .../common/utils/KoreaDateTimeExtensions.kt | 21 +++ .../utils/KoreaDateTimeExtensionsTest.kt | 45 +++++ .../web/asepct/ServiceObservationAspect.kt | 26 +++ .../common/web/config/JacksonConfig.kt | 46 +---- .../common/web/config/ObservationConfig.kt | 41 ++++ .../web/support/log/WebLogMessageConverter.kt | 5 +- .../common/web/config/JacksonConfigTest.kt | 39 +--- .../web/support/log/LogPayloadBuilderTest.kt | 2 +- .../support/log/WebLogMessageConverterTest.kt | 12 +- docker/docker-compose.yaml | 2 +- frontend/.dockerignore | 18 +- frontend/Dockerfile | 15 +- frontend/src/components/AdminRoute.tsx | 28 --- .../src/pages/admin/AdminSchedulePage.tsx | 11 +- frontend/src/pages/admin/AdminStorePage.tsx | 19 +- .../src/pages/admin/AdminThemeEditPage.tsx | 7 +- frontend/tsconfig.app.json | 1 - .../roomescape/RoomescapeApplication.kt | 4 + .../roomescape/admin/business/AdminService.kt | 14 +- .../roomescape/auth/business/AuthService.kt | 10 +- .../auth/business/LoginHistoryService.kt | 6 +- .../sangdol/roomescape/auth/docs/AuthAPI.kt | 2 +- .../auth/exception/AuthErrorCode.kt | 2 +- .../auth/infrastructure/jwt/JwtUtils.kt | 4 +- .../persistence/LoginHistoryEntity.kt | 4 +- .../sangdol/roomescape/auth/web/AuthDTO.kt | 1 - .../support/interceptors/AdminInterceptor.kt | 21 ++- .../support/interceptors/UserInterceptor.kt | 19 +- .../support/resolver/UserContextResolver.kt | 12 +- .../config/RoomescapeLogMaskingConverter.kt | 4 +- .../common/config/SlowQueryLoggerConfig.kt | 3 - .../roomescape/common/config/SwaggerConfig.kt | 1 - .../roomescape/common/config/TraceConfig.kt | 20 ++ .../roomescape/common/config/WebMvcConfig.kt | 6 +- .../roomescape/common/types/AuditingInfo.kt | 6 +- .../payment/business/PaymentService.kt | 38 ++-- .../payment/business/PaymentWriter.kt | 4 +- .../payment/exception/PaymentErrorCode.kt | 2 +- .../infrastructure/client/TosspayCancelDTO.kt | 6 +- .../client/TosspayConfirmDTO.kt | 4 +- .../infrastructure/common/PaymentTypes.kt | 4 +- .../persistence/CanceledPaymentEntity.kt | 7 +- .../persistence/PaymentEntity.kt | 6 +- .../roomescape/payment/web/PaymentDTO.kt | 13 +- .../region/business/RegionService.kt | 32 ++-- .../region/exception/RegionException.kt | 2 +- .../business/ReservationService.kt | 32 ++-- .../business/ReservationValidator.kt | 6 +- .../IncompletedReservationScheduler.kt | 6 +- .../reservation/docs/ReservationAPI.kt | 2 +- .../exception/ReservationErrorCode.kt | 2 +- .../persistence/CanceledReservationEntity.kt | 4 +- .../persistence/ReservationRepository.kt | 10 +- .../reservation/web/ReservationController.kt | 2 +- .../reservation/web/ReservationDto.kt | 6 +- .../schedule/business/ScheduleService.kt | 62 +++--- .../schedule/business/ScheduleValidator.kt | 7 +- .../roomescape/schedule/docs/ScheduleAPI.kt | 2 +- .../schedule/exception/ScheduleErrorCode.kt | 2 +- .../persistence/ScheduleEntity.kt | 4 +- .../persistence/ScheduleRepository.kt | 19 +- .../schedule/web/AdminScheduleController.kt | 2 +- .../roomescape/store/business/StoreService.kt | 36 ++-- .../store/business/StoreValidator.kt | 6 +- .../store/exception/StoreException.kt | 2 +- .../roomescape/theme/business/ThemeService.kt | 46 ++--- .../theme/business/ThemeValidator.kt | 6 +- .../theme/exception/ThemeErrorCode.kt | 2 +- .../persistence/ThemeRepository.kt | 2 +- .../roomescape/user/business/UserService.kt | 24 +-- .../roomescape/user/business/UserValidator.kt | 6 +- .../user/exception/UserException.kt | 2 +- .../persistence/UserEntities.kt | 2 +- .../sangdol/roomescape/user/web/UserDTO.kt | 4 +- .../src/main/resources/application-local.yaml | 14 +- service/src/main/resources/application.yaml | 4 +- service/src/main/resources/logback-deploy.xml | 28 +-- .../sangdol/data/DefaultDataInitializer.kt | 177 +++++++++--------- .../com/sangdol/data/PopulationDataParser.kt | 18 +- .../sangdol/roomescape/auth/AuthApiTest.kt | 14 +- .../auth/FailOnSaveLoginHistoryTest.kt | 4 +- .../roomescape/payment/PaymentAPITest.kt | 8 +- .../roomescape/payment/PaymentTypeTest.kt | 9 +- .../payment/SampleTosspayConstant.kt | 22 +-- .../roomescape/payment/TosspayClientTest.kt | 14 +- .../roomescape/region/RegionApiFailTest.kt | 6 +- .../roomescape/region/RegionApiSuccessTest.kt | 4 +- .../IncompletedReservationSchedulerTest.kt | 10 +- .../reservation/ReservationApiTest.kt | 17 +- .../reservation/ReservationConcurrencyTest.kt | 4 +- .../schedule/AdminScheduleApiTest.kt | 89 ++++++--- .../roomescape/schedule/ScheduleApiTest.kt | 28 ++- .../schedule/ScheduleConcurrencyTest.kt | 2 - .../roomescape/store/AdminStoreApiTest.kt | 10 +- .../sangdol/roomescape/store/StoreApiTest.kt | 6 +- .../roomescape/supports/DummyInitializer.kt | 4 +- .../sangdol/roomescape/supports/Fixtures.kt | 8 +- .../roomescape/supports/KotestConfig.kt | 22 ++- .../roomescape/supports/RestAssuredUtils.kt | 2 +- .../roomescape/supports/TestAuthUtil.kt | 14 +- .../roomescape/supports/TestDatabaseUtil.kt | 5 +- .../sangdol/roomescape/supports/TestUtil.kt | 4 +- .../roomescape/theme/AdminThemeApiTest.kt | 10 +- .../sangdol/roomescape/theme/DateUtilsTest.kt | 2 +- .../sangdol/roomescape/theme/ThemeApiTest.kt | 9 +- .../sangdol/roomescape/user/UserApiTest.kt | 26 +-- .../src/test/resources/application-test.yaml | 9 +- .../tosspaymock/business/TosspayService.kt | 10 +- .../tosspaymock/business/domain/Payment.kt | 8 +- .../business/domain/cancel/Cancellation.kt | 3 +- .../tosspaymock/business/domain/card/Card.kt | 1 - .../exception/code/TosspayCancelErrorCode.kt | 18 +- .../exception/code/TosspayConfirmErrorCode.kt | 24 ++- .../tosspaymock/web/dto/TosspayRequest.kt | 3 +- .../tosspaymock/web/dto/TosspayResponse.kt | 5 - .../src/main/resources/logback-deploy.xml | 3 +- .../src/main/resources/schema/schema-h2.sql | 30 ++- .../parser/origin/TosspayErrorCodeParser.kt | 5 +- 124 files changed, 938 insertions(+), 727 deletions(-) create mode 100644 common/utils/src/main/kotlin/com/sangdol/common/utils/KoreaDateTimeExtensions.kt create mode 100644 common/utils/src/test/kotlin/com/sangdol/common/utils/KoreaDateTimeExtensionsTest.kt create mode 100644 common/web/src/main/kotlin/com/sangdol/common/web/asepct/ServiceObservationAspect.kt create mode 100644 common/web/src/main/kotlin/com/sangdol/common/web/config/ObservationConfig.kt delete mode 100644 frontend/src/components/AdminRoute.tsx create mode 100644 service/src/main/kotlin/com/sangdol/roomescape/common/config/TraceConfig.kt diff --git a/Dockerfile b/Dockerfile index 87d42727..3e9dc9dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,29 @@ -FROM gradle:8-jdk17 AS builder +FROM gradle:8-jdk17 AS dependencies WORKDIR /app + +COPY gradlew settings.gradle build.gradle.kts /app/ +COPY gradle /app/gradle +COPY service/build.gradle.kts /app/service/ +COPY tosspay-mock/build.gradle.kts /app/tosspay-mock/ +COPY common/log/build.gradle.kts /app/common/log/ +COPY common/persistence/build.gradle.kts /app/common/persistence/ +COPY common/types/build.gradle.kts /app/common/types/ +COPY common/utils/build.gradle.kts /app/common/utils/ +COPY common/web/build.gradle.kts /app/common/web/ + +RUN ./gradlew dependencies --no-daemon + +FROM dependencies AS builder +WORKDIR /app + COPY . . -RUN ./gradlew bootjar --no-daemon + +RUN ./gradlew :service:bootjar --no-daemon FROM amazoncorretto:17 WORKDIR /app EXPOSE 8080 -COPY --from=builder /app/build/libs/*.jar app.jar + +COPY --from=builder /app/service/build/libs/*.jar app.jar + ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/common/log/src/test/kotlin/com/sangdol/common/log/AbstractLogMaskingConverterTest.kt b/common/log/src/test/kotlin/com/sangdol/common/log/AbstractLogMaskingConverterTest.kt index eeb74b46..a9cb953c 100644 --- a/common/log/src/test/kotlin/com/sangdol/common/log/AbstractLogMaskingConverterTest.kt +++ b/common/log/src/test/kotlin/com/sangdol/common/log/AbstractLogMaskingConverterTest.kt @@ -7,7 +7,6 @@ import io.kotest.assertions.assertSoftly import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.equals.shouldBeEqual import io.kotest.matchers.shouldBe -import io.kotest.matchers.string.shouldContain import io.mockk.every import io.mockk.mockk @@ -48,7 +47,10 @@ class AbstractLogMaskingConverterTest : FunSpec({ event.formattedMessage } returns json.format(account, address) - converter.convert(event) shouldBeEqual json.format("${account.first()}${converter.mask}${account.last()}", "${address.first()}${converter.mask}${address.last()}") + converter.convert(event) shouldBeEqual json.format( + "${account.first()}${converter.mask}${account.last()}", + "${address.first()}${converter.mask}${address.last()}" + ) } } } diff --git a/common/persistence/src/main/kotlin/com/sangdol/common/persistence/AuditingBaseEntity.kt b/common/persistence/src/main/kotlin/com/sangdol/common/persistence/AuditingBaseEntity.kt index a9fca2c7..82b2d41f 100644 --- a/common/persistence/src/main/kotlin/com/sangdol/common/persistence/AuditingBaseEntity.kt +++ b/common/persistence/src/main/kotlin/com/sangdol/common/persistence/AuditingBaseEntity.kt @@ -8,7 +8,7 @@ import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.LastModifiedBy import org.springframework.data.annotation.LastModifiedDate import org.springframework.data.jpa.domain.support.AuditingEntityListener -import java.time.LocalDateTime +import java.time.Instant @MappedSuperclass @EntityListeners(AuditingEntityListener::class) @@ -17,7 +17,7 @@ abstract class AuditingBaseEntity( ) : PersistableBaseEntity(id) { @Column(updatable = false) @CreatedDate - lateinit var createdAt: LocalDateTime + lateinit var createdAt: Instant @Column(updatable = false) @CreatedBy @@ -25,7 +25,7 @@ abstract class AuditingBaseEntity( @Column @LastModifiedDate - lateinit var updatedAt: LocalDateTime + lateinit var updatedAt: Instant @Column @LastModifiedBy diff --git a/common/persistence/src/test/kotlin/com/sangdol/common/persistence/TestAuditingBaseEntity.kt b/common/persistence/src/test/kotlin/com/sangdol/common/persistence/TestAuditingBaseEntity.kt index f58ecf77..b687de9e 100644 --- a/common/persistence/src/test/kotlin/com/sangdol/common/persistence/TestAuditingBaseEntity.kt +++ b/common/persistence/src/test/kotlin/com/sangdol/common/persistence/TestAuditingBaseEntity.kt @@ -7,6 +7,6 @@ import org.springframework.data.jpa.repository.JpaRepository class TestAuditingBaseEntity( id: Long, val name: String -): AuditingBaseEntity(id) +) : AuditingBaseEntity(id) -interface TestAuditingBaseEntityRepository: JpaRepository \ No newline at end of file +interface TestAuditingBaseEntityRepository : JpaRepository \ No newline at end of file diff --git a/common/persistence/src/test/kotlin/com/sangdol/common/persistence/TestPersistableBaseEntity.kt b/common/persistence/src/test/kotlin/com/sangdol/common/persistence/TestPersistableBaseEntity.kt index ac97c1c2..5a29c4ea 100644 --- a/common/persistence/src/test/kotlin/com/sangdol/common/persistence/TestPersistableBaseEntity.kt +++ b/common/persistence/src/test/kotlin/com/sangdol/common/persistence/TestPersistableBaseEntity.kt @@ -7,6 +7,6 @@ import org.springframework.data.jpa.repository.JpaRepository class TestPersistableBaseEntity( id: Long, val name: String -): PersistableBaseEntity(id) +) : PersistableBaseEntity(id) -interface TestPersistableBaseEntityRepository: JpaRepository \ No newline at end of file +interface TestPersistableBaseEntityRepository : JpaRepository \ No newline at end of file diff --git a/common/persistence/src/test/kotlin/com/sangdol/common/persistence/TransactionExecutionUtilTest.kt b/common/persistence/src/test/kotlin/com/sangdol/common/persistence/TransactionExecutionUtilTest.kt index 64f4faa6..35516c3a 100644 --- a/common/persistence/src/test/kotlin/com/sangdol/common/persistence/TransactionExecutionUtilTest.kt +++ b/common/persistence/src/test/kotlin/com/sangdol/common/persistence/TransactionExecutionUtilTest.kt @@ -5,11 +5,7 @@ import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.equality.shouldBeEqualUsingFields import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe -import io.mockk.clearMocks -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import io.mockk.verify +import io.mockk.* import org.junit.jupiter.api.assertThrows import org.springframework.transaction.PlatformTransactionManager import org.springframework.transaction.TransactionDefinition diff --git a/common/utils/src/main/kotlin/com/sangdol/common/utils/KoreaDateTimeExtensions.kt b/common/utils/src/main/kotlin/com/sangdol/common/utils/KoreaDateTimeExtensions.kt new file mode 100644 index 00000000..b993081b --- /dev/null +++ b/common/utils/src/main/kotlin/com/sangdol/common/utils/KoreaDateTimeExtensions.kt @@ -0,0 +1,21 @@ +package com.sangdol.common.utils + +import java.time.* +import java.time.temporal.ChronoUnit + +private val KST_CLOCK = Clock.system(ZoneId.of("Asia/Seoul")) + +object KoreaDate { + fun today(): LocalDate = LocalDate.now(KST_CLOCK) +} + +object KoreaTime { + fun now(): LocalTime = LocalTime.now(KST_CLOCK).truncatedTo(ChronoUnit.MINUTES) +} + +object KoreaDateTime { + fun now(): LocalDateTime = LocalDateTime.now(KST_CLOCK) + fun nowWithOffset(): OffsetDateTime = OffsetDateTime.now(KST_CLOCK) +} + +fun Instant.toKoreaDateTime(): LocalDateTime = this.atZone(KST_CLOCK.zone).toLocalDateTime() diff --git a/common/utils/src/test/kotlin/com/sangdol/common/utils/KoreaDateTimeExtensionsTest.kt b/common/utils/src/test/kotlin/com/sangdol/common/utils/KoreaDateTimeExtensionsTest.kt new file mode 100644 index 00000000..321d1650 --- /dev/null +++ b/common/utils/src/test/kotlin/com/sangdol/common/utils/KoreaDateTimeExtensionsTest.kt @@ -0,0 +1,45 @@ +package com.sangdol.common.utils + +import io.kotest.assertions.assertSoftly +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import java.time.* + +class KoreaDateTimeExtensionsTest : FunSpec({ + + test("한국 시간 기준으로 현재 시간을 가져오며, 초 단위는 제외한다.") { + assertSoftly(KoreaTime.now()) { + val utcNow = LocalTime.now(ZoneId.of("UTC")) + + this.hour shouldBe utcNow.hour.plus(9) + this.minute shouldBe utcNow.minute + this.second shouldBe 0 + this.nano shouldBe 0 + } + } + + test("한국 시간 기준으로 현재 날짜 + 시간을 LocalDateTime 타입으로 가져온다.") { + assertSoftly(KoreaDateTime.now()) { + val utcNow = LocalDateTime.now(ZoneId.of("UTC")) + + this.withSecond(0).withNano(0) shouldBe utcNow.plusHours(9).withSecond(0).withNano(0) + } + } + + test("한국 시간 기준으로 현재 날짜 + 시간을 OffsetDateTime 타입으로 가져온다.") { + assertSoftly(KoreaDateTime.nowWithOffset()) { + val utcNow = OffsetDateTime.now(ZoneId.of("UTC")) + + this.toLocalDateTime().withSecond(0).withNano(0) shouldBe utcNow.toLocalDateTime().plusHours(9) + .withSecond(0).withNano(0) + } + } + + test("UTC 시간을 LocalDateTime 타입의 한국 시간으로 변환한다.") { + val now = Instant.now() + val kstConverted = now.toKoreaDateTime() + val utc = now.atZone(ZoneId.of("UTC")).toLocalDateTime() + + kstConverted.withSecond(0).withNano(0) shouldBe utc.plusHours(9).withSecond(0).withNano(0) + } +}) diff --git a/common/web/src/main/kotlin/com/sangdol/common/web/asepct/ServiceObservationAspect.kt b/common/web/src/main/kotlin/com/sangdol/common/web/asepct/ServiceObservationAspect.kt new file mode 100644 index 00000000..c41f730c --- /dev/null +++ b/common/web/src/main/kotlin/com/sangdol/common/web/asepct/ServiceObservationAspect.kt @@ -0,0 +1,26 @@ +package com.sangdol.common.web.asepct + +import io.micrometer.observation.Observation +import io.micrometer.observation.ObservationRegistry +import org.aspectj.lang.ProceedingJoinPoint +import org.aspectj.lang.annotation.Around +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Pointcut + +@Aspect +class ServiceObservationAspect( + private val observationRegistry: ObservationRegistry +) { + + @Pointcut("execution(* com.sangdol..business..*Service*.*(..))") + fun allServices() { + } + + @Around("allServices()") + fun runWithObserve(joinPoint: ProceedingJoinPoint): Any? { + val methodName: String = joinPoint.signature.toShortString() + + return Observation.createNotStarted(methodName, observationRegistry) + .observe { joinPoint.proceed() } + } +} diff --git a/common/web/src/main/kotlin/com/sangdol/common/web/config/JacksonConfig.kt b/common/web/src/main/kotlin/com/sangdol/common/web/config/JacksonConfig.kt index fb6eb907..a8843014 100644 --- a/common/web/src/main/kotlin/com/sangdol/common/web/config/JacksonConfig.kt +++ b/common/web/src/main/kotlin/com/sangdol/common/web/config/JacksonConfig.kt @@ -1,11 +1,8 @@ package com.sangdol.common.web.config -import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.JsonSerializer import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.SerializerProvider -import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer 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.Configuration import java.time.LocalDate -import java.time.LocalDateTime import java.time.LocalTime -import java.time.OffsetDateTime -import java.time.ZoneId 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") - private val LOCAL_TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm") } @@ -35,9 +26,9 @@ class JacksonConfig { @Bean fun objectMapper(): ObjectMapper = ObjectMapper() .registerModule(javaTimeModule()) - .registerModule(dateTimeModule()) .registerModule(kotlinModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) private fun javaTimeModule(): JavaTimeModule = JavaTimeModule() .addSerializer( @@ -56,35 +47,4 @@ class JacksonConfig { LocalTime::class.java, LocalTimeDeserializer(LOCAL_TIME_FORMATTER) ) 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() { - 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)) - } - } -} \ No newline at end of file +} diff --git a/common/web/src/main/kotlin/com/sangdol/common/web/config/ObservationConfig.kt b/common/web/src/main/kotlin/com/sangdol/common/web/config/ObservationConfig.kt new file mode 100644 index 00000000..68d8ed99 --- /dev/null +++ b/common/web/src/main/kotlin/com/sangdol/common/web/config/ObservationConfig.kt @@ -0,0 +1,41 @@ +package com.sangdol.common.web.config + +import com.sangdol.common.web.asepct.ServiceObservationAspect +import io.micrometer.observation.ObservationPredicate +import io.micrometer.observation.ObservationRegistry +import io.micrometer.observation.aop.ObservedAspect +import jakarta.servlet.http.HttpServletRequest +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.server.observation.ServerRequestObservationContext + +@Configuration +class ObservationConfig( + @Value("\${management.endpoints.web.base-path}") private val actuatorPath: String +) { + + @Bean + fun observedAspect(observationRegistry: ObservationRegistry): ObservedAspect { + return ObservedAspect(observationRegistry) + } + + @Bean + fun serviceObservationAspect(observationRegistry: ObservationRegistry): ServiceObservationAspect { + return ServiceObservationAspect(observationRegistry) + } + + @Bean + fun excludeActuatorPredicate(): ObservationPredicate { + return ObservationPredicate { _, context -> + if (context !is ServerRequestObservationContext) { + return@ObservationPredicate true + } + + val servletRequest: HttpServletRequest = context.carrier + val requestUri = servletRequest.requestURI + + !requestUri.contains(actuatorPath) + } + } +} diff --git a/common/web/src/main/kotlin/com/sangdol/common/web/support/log/WebLogMessageConverter.kt b/common/web/src/main/kotlin/com/sangdol/common/web/support/log/WebLogMessageConverter.kt index 1a2c47a7..48653a11 100644 --- a/common/web/src/main/kotlin/com/sangdol/common/web/support/log/WebLogMessageConverter.kt +++ b/common/web/src/main/kotlin/com/sangdol/common/web/support/log/WebLogMessageConverter.kt @@ -18,7 +18,10 @@ class WebLogMessageConverter( return objectMapper.writeValueAsString(payload) } - fun convertToControllerInvokedMessage(servletRequest: HttpServletRequest, controllerPayload: Map): String { + fun convertToControllerInvokedMessage( + servletRequest: HttpServletRequest, + controllerPayload: Map + ): String { val payload = LogPayloadBuilder(type = LogType.CONTROLLER_INVOKED, servletRequest = servletRequest) .endpoint() .principalId() diff --git a/common/web/src/test/kotlin/com/sangdol/common/web/config/JacksonConfigTest.kt b/common/web/src/test/kotlin/com/sangdol/common/web/config/JacksonConfigTest.kt index 19af86d1..037a78f5 100644 --- a/common/web/src/test/kotlin/com/sangdol/common/web/config/JacksonConfigTest.kt +++ b/common/web/src/test/kotlin/com/sangdol/common/web/config/JacksonConfigTest.kt @@ -7,10 +7,7 @@ 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 : FunSpec({ @@ -55,38 +52,4 @@ class JacksonConfigTest : FunSpec({ }.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\"" - } - } -}) \ No newline at end of file +}) diff --git a/common/web/src/test/kotlin/com/sangdol/common/web/support/log/LogPayloadBuilderTest.kt b/common/web/src/test/kotlin/com/sangdol/common/web/support/log/LogPayloadBuilderTest.kt index 0b905efa..a5a8022e 100644 --- a/common/web/src/test/kotlin/com/sangdol/common/web/support/log/LogPayloadBuilderTest.kt +++ b/common/web/src/test/kotlin/com/sangdol/common/web/support/log/LogPayloadBuilderTest.kt @@ -23,7 +23,7 @@ class LogPayloadBuilderTest : FunSpec({ beforeTest { method = "GET".also { every { servletRequest.method } returns it } requestUri = "/converter/test".also { every { servletRequest.requestURI } returns it } - remoteAddr = "localhost".also { every { servletRequest.remoteAddr } returns it } + remoteAddr = "localhost".also { every { servletRequest.remoteAddr } returns it } userAgent = "Mozilla/5.0".also { every { servletRequest.getHeader("User-Agent") } returns it } queryString = "key=value".also { every { servletRequest.queryString } returns it } } diff --git a/common/web/src/test/kotlin/com/sangdol/common/web/support/log/WebLogMessageConverterTest.kt b/common/web/src/test/kotlin/com/sangdol/common/web/support/log/WebLogMessageConverterTest.kt index 84a1f62d..4c0951fb 100644 --- a/common/web/src/test/kotlin/com/sangdol/common/web/support/log/WebLogMessageConverterTest.kt +++ b/common/web/src/test/kotlin/com/sangdol/common/web/support/log/WebLogMessageConverterTest.kt @@ -29,7 +29,7 @@ class WebLogMessageConverterTest : FunSpec({ beforeTest { method = "GET".also { every { servletRequest.method } returns it } requestUri = "/converter/test".also { every { servletRequest.requestURI } returns it } - remoteAddr = "localhost".also { every { servletRequest.remoteAddr } returns it } + remoteAddr = "localhost".also { every { servletRequest.remoteAddr } returns it } userAgent = "Mozilla/5.0".also { every { servletRequest.getHeader("User-Agent") } returns it } queryString = "key=value".also { every { servletRequest.queryString } returns it } } @@ -121,7 +121,10 @@ class WebLogMessageConverterTest : FunSpec({ this["duration_ms"].shouldNotBeNull() this["principal_id"] shouldBe principalId this["response_body"] shouldBe null - this["exception"] shouldBe mapOf("class" to exception.javaClass.simpleName, "message" to exception.message) + this["exception"] shouldBe mapOf( + "class" to exception.javaClass.simpleName, + "message" to exception.message + ) } } @@ -141,7 +144,10 @@ class WebLogMessageConverterTest : FunSpec({ this["duration_ms"].shouldNotBeNull() this["principal_id"] shouldBe principalId this["response_body"] shouldBe body - this["exception"] shouldBe mapOf("class" to exception.javaClass.simpleName, "message" to exception.message) + this["exception"] shouldBe mapOf( + "class" to exception.javaClass.simpleName, + "message" to exception.message + ) } } diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 83ce6703..995e40fb 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -8,7 +8,7 @@ services: environment: MYSQL_ROOT_PASSWORD: init MYSQL_DATABASE: roomescape_local - TZ: Asia/Seoul + TZ: UTC command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci diff --git a/frontend/.dockerignore b/frontend/.dockerignore index 3dd9fe5b..0fde024d 100644 --- a/frontend/.dockerignore +++ b/frontend/.dockerignore @@ -1,6 +1,18 @@ -node_modules .git -.DS_Store +.gitignore + +# Node.js +node_modules npm-debug.log + +# Build output +build dist -build \ No newline at end of file + +# Editor/OS specific +.vscode +.idea +.DS_Store + +# Environment variables +.env* \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile index cfc4d6d9..8afa50dd 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,18 +1,17 @@ -# Stage 1: Build the React app -FROM node:24 AS builder +FROM node:24-alpine AS builder WORKDIR /app -COPY package.json ./ -COPY package-lock.json ./ -RUN npm install --frozen-lockfile +COPY package.json package-lock.json ./ + +RUN npm ci COPY . . RUN npm run build +FROM nginx:1.27-alpine -# Stage 2: Serve with Nginx -FROM nginx:latest COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf + EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/frontend/src/components/AdminRoute.tsx b/frontend/src/components/AdminRoute.tsx deleted file mode 100644 index c238090d..00000000 --- a/frontend/src/components/AdminRoute.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import {Navigate, useLocation} from 'react-router-dom'; -import {useAuth} from '../context/AuthContext'; - -const AdminRoute: React.FC<{ children: JSX.Element }> = ({ children }) => { - const { loggedIn, role, loading } = useAuth(); - const location = useLocation(); - - if (loading) { - return
Loading...
; // Or a proper spinner component - } - - if (!loggedIn) { - // Not logged in, redirect to login page. No alert needed here - // as the user is simply redirected. - return ; - } - - if (role !== 'ADMIN') { - // Logged in but not an admin, show alert and redirect. - alert('접근 권한이 없어요. 관리자에게 문의해주세요.'); - return ; - } - - return children; -}; - -export default AdminRoute; diff --git a/frontend/src/pages/admin/AdminSchedulePage.tsx b/frontend/src/pages/admin/AdminSchedulePage.tsx index f169e0bc..98c995e7 100644 --- a/frontend/src/pages/admin/AdminSchedulePage.tsx +++ b/frontend/src/pages/admin/AdminSchedulePage.tsx @@ -10,10 +10,11 @@ import { import {type AdminScheduleSummaryResponse, ScheduleStatus,} from '@_api/schedule/scheduleTypes'; import {getStores} from '@_api/store/storeAPI'; import {type SimpleStoreResponse} from '@_api/store/storeTypes'; -import {fetchActiveThemes, fetchThemeById} from '@_api/theme/themeAPI'; +import {fetchActiveThemes} from '@_api/theme/themeAPI'; import {DifficultyKoreanMap, type SimpleActiveThemeResponse, type ThemeInfoResponse} from '@_api/theme/themeTypes'; import {useAdminAuth} from '@_context/AdminAuthContext'; import '@_css/admin-schedule-page.css'; +import {formatDisplayDateTime} from '@_util/DateTimeFormatter'; import React, {Fragment, useEffect, useState} from 'react'; import {useLocation, useNavigate} from 'react-router-dom'; @@ -53,8 +54,8 @@ const AdminSchedulePage: React.FC = () => { const [editingSchedule, setEditingSchedule] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); - const [selectedThemeDetails, setSelectedThemeDetails] = useState(null); - const [isLoadingThemeDetails, setIsLoadingThemeDetails] = useState(false); + const [selectedThemeDetails] = useState(null); + const [isLoadingThemeDetails] = useState(false); const navigate = useNavigate(); const location = useLocation(); @@ -332,10 +333,10 @@ const AdminSchedulePage: React.FC = () => {

감사 정보

- 생성일: {new Date(detailedSchedules[schedule.id].audit!.createdAt).toLocaleString()} + 생성일: {formatDisplayDateTime(detailedSchedules[schedule.id].audit!.createdAt)}

- 수정일: {new Date(detailedSchedules[schedule.id].audit!.updatedAt).toLocaleString()} + 수정일: {formatDisplayDateTime(detailedSchedules[schedule.id].audit!.updatedAt)}

생성자: {detailedSchedules[schedule.id].audit!.createdBy.name}({detailedSchedules[schedule.id].audit!.createdBy.id}) diff --git a/frontend/src/pages/admin/AdminStorePage.tsx b/frontend/src/pages/admin/AdminStorePage.tsx index 9caa92fb..428f6a6f 100644 --- a/frontend/src/pages/admin/AdminStorePage.tsx +++ b/frontend/src/pages/admin/AdminStorePage.tsx @@ -1,17 +1,18 @@ -import { isLoginRequiredError } from '@_api/apiClient'; -import { fetchSidoList, fetchSigunguList } from '@_api/region/regionAPI'; -import type { SidoResponse, SigunguResponse } from '@_api/region/regionTypes'; -import { createStore, deleteStore, getStoreDetail, getStores, updateStore } from '@_api/store/storeAPI'; +import {isLoginRequiredError} from '@_api/apiClient'; +import {fetchSidoList, fetchSigunguList} from '@_api/region/regionAPI'; +import type {SidoResponse, SigunguResponse} from '@_api/region/regionTypes'; +import {createStore, deleteStore, getStoreDetail, getStores, updateStore} from '@_api/store/storeAPI'; import { type SimpleStoreResponse, type StoreDetailResponse, type StoreRegisterRequest, type UpdateStoreRequest } from '@_api/store/storeTypes'; -import { useAdminAuth } from '@_context/AdminAuthContext'; +import {useAdminAuth} from '@_context/AdminAuthContext'; import '@_css/admin-store-page.css'; -import React, { Fragment, useEffect, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import {formatDisplayDateTime} from '@_util/DateTimeFormatter'; +import React, {Fragment, useEffect, useState} from 'react'; +import {useLocation, useNavigate} from 'react-router-dom'; const AdminStorePage: React.FC = () => { const [stores, setStores] = useState([]); @@ -297,10 +298,10 @@ const AdminStorePage: React.FC = () => { 코드: {detailedStores[store.id].region.code}

- 생성일: {new Date(detailedStores[store.id].audit.createdAt).toLocaleString()} + 생성일: {formatDisplayDateTime(detailedStores[store.id].audit.createdAt)}

- 수정일: {new Date(detailedStores[store.id].audit.updatedAt).toLocaleString()} + 수정일: {formatDisplayDateTime(detailedStores[store.id].audit.updatedAt)}

생성자: {detailedStores[store.id].audit.createdBy.name}({detailedStores[store.id].audit.createdBy.id}) diff --git a/frontend/src/pages/admin/AdminThemeEditPage.tsx b/frontend/src/pages/admin/AdminThemeEditPage.tsx index 0e6b6797..f258bc7a 100644 --- a/frontend/src/pages/admin/AdminThemeEditPage.tsx +++ b/frontend/src/pages/admin/AdminThemeEditPage.tsx @@ -9,7 +9,8 @@ import { import React, {useEffect, useState} from 'react'; import {useLocation, useNavigate, useParams} from 'react-router-dom'; 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 { name: string; @@ -256,8 +257,8 @@ const AdminThemeEditPage: React.FC = () => {

감사 정보

-

생성일: {new Date(auditInfo.createdAt).toLocaleString()}

-

수정일: {new Date(auditInfo.updatedAt).toLocaleString()}

+

생성일: {formatDisplayDateTime(auditInfo.createdAt)}

+

수정일: {formatDisplayDateTime(auditInfo.updatedAt)}

생성자: {auditInfo.createdBy.name}

수정자: {auditInfo.updatedBy.name}

diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 0edfa51c..4970e0b4 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -19,7 +19,6 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true, diff --git a/service/src/main/kotlin/com/sangdol/roomescape/RoomescapeApplication.kt b/service/src/main/kotlin/com/sangdol/roomescape/RoomescapeApplication.kt index c8b5682f..c8a3ee8a 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/RoomescapeApplication.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/RoomescapeApplication.kt @@ -3,6 +3,7 @@ package com.sangdol.roomescape import org.springframework.boot.Banner import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication +import java.util.* @SpringBootApplication( scanBasePackages = ["com.sangdol.roomescape", "com.sangdol.common"] @@ -10,6 +11,9 @@ import org.springframework.boot.autoconfigure.SpringBootApplication class RoomescapeApplication fun main(args: Array) { + System.setProperty("user.timezone", "UTC") + TimeZone.setDefault(TimeZone.getTimeZone("UTC")) + val springApplication = SpringApplication(RoomescapeApplication::class.java) springApplication.setBannerMode(Banner.Mode.OFF) springApplication.run() diff --git a/service/src/main/kotlin/com/sangdol/roomescape/admin/business/AdminService.kt b/service/src/main/kotlin/com/sangdol/roomescape/admin/business/AdminService.kt index e59c22d2..2c13e94b 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/admin/business/AdminService.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/admin/business/AdminService.kt @@ -1,11 +1,11 @@ package com.sangdol.roomescape.admin.business -import com.sangdol.roomescape.common.types.Auditor import com.sangdol.roomescape.admin.business.dto.AdminLoginCredentials import com.sangdol.roomescape.admin.business.dto.toCredentials import com.sangdol.roomescape.admin.exception.AdminErrorCode import com.sangdol.roomescape.admin.exception.AdminException import com.sangdol.roomescape.admin.infrastructure.persistence.AdminRepository +import com.sangdol.roomescape.common.types.Auditor import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.data.repository.findByIdOrNull @@ -20,29 +20,29 @@ class AdminService( ) { @Transactional(readOnly = true) fun findCredentialsByAccount(account: String): AdminLoginCredentials { - log.info { "[AdminService.findCredentialsByAccount] 관리자 조회 시작: account=${account}" } + log.info { "[findCredentialsByAccount] 관리자 조회 시작: account=${account}" } return adminRepository.findByAccount(account) ?.let { - log.info { "[AdminService.findCredentialsByAccount] 관리자 조회 완료: account=${account}, id=${it.id}" } + log.info { "[findCredentialsByAccount] 관리자 조회 완료: account=${account}, id=${it.id}" } it.toCredentials() } ?: run { - log.info { "[AdminService.findCredentialsByAccount] 관리자 조회 실패: account=${account}" } + log.info { "[findCredentialsByAccount] 관리자 조회 실패: account=${account}" } throw AdminException(AdminErrorCode.ADMIN_NOT_FOUND) } } @Transactional(readOnly = true) fun findOperatorOrUnknown(id: Long): Auditor { - log.info { "[AdminService.findOperatorById] 작업자 정보 조회 시작: id=${id}" } + log.info { "[findOperatorById] 작업자 정보 조회 시작: id=${id}" } return adminRepository.findByIdOrNull(id)?.let { admin -> Auditor(admin.id, admin.name).also { - log.info { "[AdminService.findOperatorById] 작업자 정보 조회 완료: id=${admin.id}, name=${admin.name}" } + log.info { "[findOperatorById] 작업자 정보 조회 완료: id=${admin.id}, name=${admin.name}" } } } ?: run { - log.warn { "[AdminService.findOperatorById] 작업자 정보 조회 실패. id=${id}" } + log.warn { "[findOperatorById] 작업자 정보 조회 실패. id=${id}" } Auditor.UNKNOWN } } diff --git a/service/src/main/kotlin/com/sangdol/roomescape/auth/business/AuthService.kt b/service/src/main/kotlin/com/sangdol/roomescape/auth/business/AuthService.kt index 22cff9a8..efa5ea1d 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/auth/business/AuthService.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/auth/business/AuthService.kt @@ -29,7 +29,7 @@ class AuthService( request: LoginRequest, context: LoginContext ): LoginSuccessResponse { - log.info { "[AuthService.login] 로그인 시작: account=${request.account}, type=${request.principalType}, context=${context}" } + log.info { "[login] 로그인 시작: account=${request.account}, type=${request.principalType}, context=${context}" } val (credentials, extraClaims) = getCredentials(request) try { @@ -40,7 +40,7 @@ class AuthService( loginHistoryService.createSuccessHistory(credentials.id, request.principalType, context) return credentials.toResponse(accessToken).also { - log.info { "[AuthService.login] 로그인 완료: account=${request.account}, context=${context}" } + log.info { "[login] 로그인 완료: account=${request.account}, context=${context}" } } } catch (e: Exception) { @@ -48,12 +48,12 @@ class AuthService( when (e) { is AuthException -> { - log.info { "[AuthService.login] 로그인 실패: account = ${request.account}" } + log.info { "[login] 로그인 실패: account = ${request.account}" } throw e } else -> { - log.warn { "[AuthService.login] 로그인 실패: message=${e.message} account = ${request.account}" } + log.warn { "[login] 로그인 실패: message=${e.message} account = ${request.account}" } throw AuthException(AuthErrorCode.TEMPORARY_AUTH_ERROR) } } @@ -65,7 +65,7 @@ class AuthService( credentials: LoginCredentials ) { if (credentials.password != request.password) { - log.info { "[AuthService.login] 비밀번호 불일치로 인한 로그인 실패: account = ${request.account}" } + log.info { "[login] 비밀번호 불일치로 인한 로그인 실패: account = ${request.account}" } throw AuthException(AuthErrorCode.LOGIN_FAILED) } } diff --git a/service/src/main/kotlin/com/sangdol/roomescape/auth/business/LoginHistoryService.kt b/service/src/main/kotlin/com/sangdol/roomescape/auth/business/LoginHistoryService.kt index 6cbed9bd..4dc31855 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/auth/business/LoginHistoryService.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/auth/business/LoginHistoryService.kt @@ -42,7 +42,7 @@ class LoginHistoryService( success: Boolean, context: LoginContext ) { - log.info { "[LoginHistoryService.createHistory] 로그인 이력 저장 시작: id=${principalId}, type=${principalType}, success=${success}" } + log.info { "[createHistory] 로그인 이력 저장 시작: id=${principalId}, type=${principalType}, success=${success}" } runCatching { LoginHistoryEntity( @@ -54,10 +54,10 @@ class LoginHistoryService( userAgent = context.userAgent, ).also { loginHistoryRepository.save(it) - log.info { "[LoginHistoryService.createHistory] 로그인 이력 저장 완료: principalId=${principalId}, historyId=${it.id}" } + log.info { "[createHistory] 로그인 이력 저장 완료: principalId=${principalId}, historyId=${it.id}" } } }.onFailure { - log.warn { "[LoginHistoryService] 로그인 이력 저장 중 예외 발생: message=${it.message} id=${principalId}, type=${principalType}, success=${success}, context=${context}" } + log.warn { "[createHistory] 로그인 이력 저장 중 예외 발생: message=${it.message} id=${principalId}, type=${principalType}, success=${success}, context=${context}" } } } } diff --git a/service/src/main/kotlin/com/sangdol/roomescape/auth/docs/AuthAPI.kt b/service/src/main/kotlin/com/sangdol/roomescape/auth/docs/AuthAPI.kt index 65a2c016..73fb472c 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/auth/docs/AuthAPI.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/auth/docs/AuthAPI.kt @@ -1,11 +1,11 @@ package com.sangdol.roomescape.auth.docs import com.sangdol.common.types.web.CommonApiResponse -import com.sangdol.roomescape.common.types.CurrentUserContext import com.sangdol.roomescape.auth.web.LoginRequest import com.sangdol.roomescape.auth.web.LoginSuccessResponse import com.sangdol.roomescape.auth.web.support.Public import com.sangdol.roomescape.auth.web.support.User +import com.sangdol.roomescape.common.types.CurrentUserContext import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses diff --git a/service/src/main/kotlin/com/sangdol/roomescape/auth/exception/AuthErrorCode.kt b/service/src/main/kotlin/com/sangdol/roomescape/auth/exception/AuthErrorCode.kt index 2da82e04..bd659a01 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/auth/exception/AuthErrorCode.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/auth/exception/AuthErrorCode.kt @@ -1,7 +1,7 @@ package com.sangdol.roomescape.auth.exception -import com.sangdol.common.types.web.HttpStatus import com.sangdol.common.types.exception.ErrorCode +import com.sangdol.common.types.web.HttpStatus enum class AuthErrorCode( override val httpStatus: HttpStatus, diff --git a/service/src/main/kotlin/com/sangdol/roomescape/auth/infrastructure/jwt/JwtUtils.kt b/service/src/main/kotlin/com/sangdol/roomescape/auth/infrastructure/jwt/JwtUtils.kt index ba34b687..28be6c61 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/auth/infrastructure/jwt/JwtUtils.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/auth/infrastructure/jwt/JwtUtils.kt @@ -1,5 +1,7 @@ package com.sangdol.roomescape.auth.infrastructure.jwt +import com.sangdol.roomescape.auth.exception.AuthErrorCode +import com.sangdol.roomescape.auth.exception.AuthException import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging import io.jsonwebtoken.Claims @@ -8,8 +10,6 @@ import io.jsonwebtoken.Jwts import io.jsonwebtoken.security.Keys import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component -import com.sangdol.roomescape.auth.exception.AuthErrorCode -import com.sangdol.roomescape.auth.exception.AuthException import java.util.* import javax.crypto.SecretKey diff --git a/service/src/main/kotlin/com/sangdol/roomescape/auth/infrastructure/persistence/LoginHistoryEntity.kt b/service/src/main/kotlin/com/sangdol/roomescape/auth/infrastructure/persistence/LoginHistoryEntity.kt index 8d8b2e67..86bcb168 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/auth/infrastructure/persistence/LoginHistoryEntity.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/auth/infrastructure/persistence/LoginHistoryEntity.kt @@ -5,7 +5,7 @@ import com.sangdol.roomescape.auth.web.PrincipalType import jakarta.persistence.* import org.springframework.data.annotation.CreatedDate import org.springframework.data.jpa.domain.support.AuditingEntityListener -import java.time.LocalDateTime +import java.time.Instant @Entity @Table(name = "login_history") @@ -24,5 +24,5 @@ class LoginHistoryEntity( @Column(updatable = false) @CreatedDate - var createdAt: LocalDateTime? = null, + var createdAt: Instant? = null, ) : PersistableBaseEntity(id) diff --git a/service/src/main/kotlin/com/sangdol/roomescape/auth/web/AuthDTO.kt b/service/src/main/kotlin/com/sangdol/roomescape/auth/web/AuthDTO.kt index 2069f004..4385358d 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/auth/web/AuthDTO.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/auth/web/AuthDTO.kt @@ -1,6 +1,5 @@ package com.sangdol.roomescape.auth.web -import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType import jakarta.servlet.http.HttpServletRequest enum class PrincipalType { diff --git a/service/src/main/kotlin/com/sangdol/roomescape/auth/web/support/interceptors/AdminInterceptor.kt b/service/src/main/kotlin/com/sangdol/roomescape/auth/web/support/interceptors/AdminInterceptor.kt index 7de82394..9578dd35 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/auth/web/support/interceptors/AdminInterceptor.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/auth/web/support/interceptors/AdminInterceptor.kt @@ -1,12 +1,6 @@ package com.sangdol.roomescape.auth.web.support.interceptors -import io.github.oshai.kotlinlogging.KLogger -import io.github.oshai.kotlinlogging.KotlinLogging -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse -import org.springframework.stereotype.Component -import org.springframework.web.method.HandlerMethod -import org.springframework.web.servlet.HandlerInterceptor +import com.sangdol.common.utils.MdcPrincipalIdUtil import com.sangdol.roomescape.admin.infrastructure.persistence.AdminPermissionLevel import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType import com.sangdol.roomescape.admin.infrastructure.persistence.Privilege @@ -17,7 +11,13 @@ import com.sangdol.roomescape.auth.exception.AuthException import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils import com.sangdol.roomescape.auth.web.support.AdminOnly import com.sangdol.roomescape.auth.web.support.accessToken -import com.sangdol.common.utils.MdcPrincipalIdUtil +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.stereotype.Component +import org.springframework.web.method.HandlerMethod +import org.springframework.web.servlet.HandlerInterceptor private val log: KLogger = KotlinLogging.logger {} @@ -47,7 +47,10 @@ class AdminInterceptor( return true } catch (e: Exception) { when (e) { - is AuthException -> { throw e } + is AuthException -> { + throw e + } + else -> { log.warn { "[AdminInterceptor] 예상치 못한 예외: message=${e.message}" } throw AuthException(AuthErrorCode.TEMPORARY_AUTH_ERROR) diff --git a/service/src/main/kotlin/com/sangdol/roomescape/auth/web/support/interceptors/UserInterceptor.kt b/service/src/main/kotlin/com/sangdol/roomescape/auth/web/support/interceptors/UserInterceptor.kt index 8be0d99c..c745681e 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/auth/web/support/interceptors/UserInterceptor.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/auth/web/support/interceptors/UserInterceptor.kt @@ -1,5 +1,12 @@ package com.sangdol.roomescape.auth.web.support.interceptors +import com.sangdol.common.utils.MdcPrincipalIdUtil +import com.sangdol.roomescape.auth.business.CLAIM_ADMIN_TYPE_KEY +import com.sangdol.roomescape.auth.exception.AuthErrorCode +import com.sangdol.roomescape.auth.exception.AuthException +import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils +import com.sangdol.roomescape.auth.web.support.UserOnly +import com.sangdol.roomescape.auth.web.support.accessToken import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging import jakarta.servlet.http.HttpServletRequest @@ -7,13 +14,6 @@ import jakarta.servlet.http.HttpServletResponse import org.springframework.stereotype.Component import org.springframework.web.method.HandlerMethod import org.springframework.web.servlet.HandlerInterceptor -import com.sangdol.roomescape.auth.business.CLAIM_ADMIN_TYPE_KEY -import com.sangdol.roomescape.auth.exception.AuthErrorCode -import com.sangdol.roomescape.auth.exception.AuthException -import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils -import com.sangdol.roomescape.auth.web.support.UserOnly -import com.sangdol.roomescape.auth.web.support.accessToken -import com.sangdol.common.utils.MdcPrincipalIdUtil private val log: KLogger = KotlinLogging.logger {} @@ -47,7 +47,10 @@ class UserInterceptor( return true } catch (e: Exception) { when (e) { - is AuthException -> { throw e } + is AuthException -> { + throw e + } + else -> { log.warn { "[UserInterceptor] 예상치 못한 예외: message=${e.message}" } throw AuthException(AuthErrorCode.TEMPORARY_AUTH_ERROR) diff --git a/service/src/main/kotlin/com/sangdol/roomescape/auth/web/support/resolver/UserContextResolver.kt b/service/src/main/kotlin/com/sangdol/roomescape/auth/web/support/resolver/UserContextResolver.kt index ea47d16b..e61270be 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/auth/web/support/resolver/UserContextResolver.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/auth/web/support/resolver/UserContextResolver.kt @@ -1,5 +1,11 @@ package com.sangdol.roomescape.auth.web.support.resolver +import com.sangdol.roomescape.auth.exception.AuthErrorCode +import com.sangdol.roomescape.auth.exception.AuthException +import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils +import com.sangdol.roomescape.auth.web.support.User +import com.sangdol.roomescape.auth.web.support.accessToken +import com.sangdol.roomescape.user.business.UserService import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging import jakarta.servlet.http.HttpServletRequest @@ -9,12 +15,6 @@ import org.springframework.web.bind.support.WebDataBinderFactory import org.springframework.web.context.request.NativeWebRequest import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.method.support.ModelAndViewContainer -import com.sangdol.roomescape.auth.exception.AuthErrorCode -import com.sangdol.roomescape.auth.exception.AuthException -import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils -import com.sangdol.roomescape.auth.web.support.User -import com.sangdol.roomescape.auth.web.support.accessToken -import com.sangdol.roomescape.user.business.UserService private val log: KLogger = KotlinLogging.logger {} diff --git a/service/src/main/kotlin/com/sangdol/roomescape/common/config/RoomescapeLogMaskingConverter.kt b/service/src/main/kotlin/com/sangdol/roomescape/common/config/RoomescapeLogMaskingConverter.kt index 9e6c10e0..cf3ffa3d 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/common/config/RoomescapeLogMaskingConverter.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/common/config/RoomescapeLogMaskingConverter.kt @@ -1,9 +1,9 @@ package com.sangdol.roomescape.common.config -import com.sangdol.common.web.config.JacksonConfig import com.sangdol.common.log.message.AbstractLogMaskingConverter +import com.sangdol.common.web.config.JacksonConfig -class RoomescapeLogMaskingConverter: AbstractLogMaskingConverter( +class RoomescapeLogMaskingConverter : AbstractLogMaskingConverter( sensitiveKeys = setOf("password", "accessToken", "phone"), objectMapper = JacksonConfig().objectMapper() ) diff --git a/service/src/main/kotlin/com/sangdol/roomescape/common/config/SlowQueryLoggerConfig.kt b/service/src/main/kotlin/com/sangdol/roomescape/common/config/SlowQueryLoggerConfig.kt index 3e599e9b..6482d7c0 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/common/config/SlowQueryLoggerConfig.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/common/config/SlowQueryLoggerConfig.kt @@ -9,11 +9,9 @@ import org.springframework.boot.jdbc.DataSourceBuilder import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Primary -import org.springframework.context.annotation.Profile import javax.sql.DataSource @Configuration -@Profile("deploy") @EnableConfigurationProperties(SlowQueryProperties::class) class ProxyDataSourceConfig { @@ -36,7 +34,6 @@ class ProxyDataSourceConfig { .build() } -@Profile("deploy") @ConfigurationProperties(prefix = "slow-query") data class SlowQueryProperties( val loggerName: String, diff --git a/service/src/main/kotlin/com/sangdol/roomescape/common/config/SwaggerConfig.kt b/service/src/main/kotlin/com/sangdol/roomescape/common/config/SwaggerConfig.kt index 509a0192..1585f482 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/common/config/SwaggerConfig.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/common/config/SwaggerConfig.kt @@ -1,7 +1,6 @@ package com.sangdol.roomescape.common.config import io.swagger.v3.oas.models.OpenAPI -import io.swagger.v3.oas.models.info.Info import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration diff --git a/service/src/main/kotlin/com/sangdol/roomescape/common/config/TraceConfig.kt b/service/src/main/kotlin/com/sangdol/roomescape/common/config/TraceConfig.kt new file mode 100644 index 00000000..5aeccca1 --- /dev/null +++ b/service/src/main/kotlin/com/sangdol/roomescape/common/config/TraceConfig.kt @@ -0,0 +1,20 @@ +package com.sangdol.roomescape.common.config + +import io.micrometer.observation.ObservationPredicate +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class TraceConfig { + + companion object { + val scheduleTaskName = "tasks.scheduled.execution" + } + + @Bean + fun excludeSchedulerPredicate(): ObservationPredicate { + return ObservationPredicate { name, context -> + !name.equals(scheduleTaskName) + } + } +} diff --git a/service/src/main/kotlin/com/sangdol/roomescape/common/config/WebMvcConfig.kt b/service/src/main/kotlin/com/sangdol/roomescape/common/config/WebMvcConfig.kt index 2bf615eb..1e64c387 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/common/config/WebMvcConfig.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/common/config/WebMvcConfig.kt @@ -1,12 +1,12 @@ package com.sangdol.roomescape.common.config +import com.sangdol.roomescape.auth.web.support.interceptors.AdminInterceptor +import com.sangdol.roomescape.auth.web.support.interceptors.UserInterceptor +import com.sangdol.roomescape.auth.web.support.resolver.UserContextResolver import org.springframework.context.annotation.Configuration import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.servlet.config.annotation.InterceptorRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer -import com.sangdol.roomescape.auth.web.support.interceptors.AdminInterceptor -import com.sangdol.roomescape.auth.web.support.interceptors.UserInterceptor -import com.sangdol.roomescape.auth.web.support.resolver.UserContextResolver @Configuration class WebMvcConfig( diff --git a/service/src/main/kotlin/com/sangdol/roomescape/common/types/AuditingInfo.kt b/service/src/main/kotlin/com/sangdol/roomescape/common/types/AuditingInfo.kt index 83693c60..94cc0998 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/common/types/AuditingInfo.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/common/types/AuditingInfo.kt @@ -1,6 +1,6 @@ package com.sangdol.roomescape.common.types -import java.time.LocalDateTime +import java.time.Instant data class Auditor( val id: Long, @@ -12,8 +12,8 @@ data class Auditor( } data class AuditingInfo( - val createdAt: LocalDateTime, + val createdAt: Instant, val createdBy: Auditor, - val updatedAt: LocalDateTime, + val updatedAt: Instant, val updatedBy: Auditor, ) diff --git a/service/src/main/kotlin/com/sangdol/roomescape/payment/business/PaymentService.kt b/service/src/main/kotlin/com/sangdol/roomescape/payment/business/PaymentService.kt index 8336919c..ec874e10 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/payment/business/PaymentService.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/payment/business/PaymentService.kt @@ -1,9 +1,5 @@ package com.sangdol.roomescape.payment.business -import io.github.oshai.kotlinlogging.KLogger -import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional import com.sangdol.common.persistence.TransactionExecutionUtil import com.sangdol.roomescape.payment.exception.PaymentErrorCode import com.sangdol.roomescape.payment.exception.PaymentException @@ -12,6 +8,10 @@ import com.sangdol.roomescape.payment.infrastructure.client.PaymentClientConfirm import com.sangdol.roomescape.payment.infrastructure.client.TosspayClient import com.sangdol.roomescape.payment.infrastructure.persistence.* import com.sangdol.roomescape.payment.web.* +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional private val log: KLogger = KotlinLogging.logger {} @@ -42,7 +42,7 @@ class PaymentService( PaymentCreateResponse(paymentId = payment.id, detailId = detail.id) } ?: run { - log.warn { "[PaymentService.confirm] 결제 확정 중 예상치 못한 null 반환" } + log.warn { "[confirm] 결제 확정 중 예상치 못한 null 반환" } throw PaymentException(PaymentErrorCode.PAYMENT_UNEXPECTED_ERROR) } } @@ -64,13 +64,13 @@ class PaymentService( cancelResponse = clientCancelResponse ) }.also { - log.info { "[PaymentService.cancel] 결제 취소 완료: paymentId=${payment.id}" } + log.info { "[cancel] 결제 취소 완료: paymentId=${payment.id}" } } } @Transactional(readOnly = true) fun findDetailByReservationId(reservationId: Long): PaymentWithDetailResponse? { - log.info { "[PaymentService.findDetailByReservationId] 예약 결제 정보 조회 시작: reservationId=$reservationId" } + log.info { "[findDetailByReservationId] 예약 결제 정보 조회 시작: reservationId=$reservationId" } val payment: PaymentEntity? = findByReservationIdOrNull(reservationId) val paymentDetail: PaymentDetailEntity? = payment?.let { findDetailByPaymentIdOrNull(it.id) } @@ -83,49 +83,49 @@ class PaymentService( } private fun findByReservationIdOrThrow(reservationId: Long): PaymentEntity { - log.info { "[PaymentService.findByReservationIdOrThrow] 결제 정보 조회 시작: reservationId=: $reservationId" } + log.info { "[findByReservationIdOrThrow] 결제 정보 조회 시작: reservationId=: $reservationId" } return paymentRepository.findByReservationId(reservationId) - ?.also { log.info { "[PaymentService.findByReservationIdOrThrow] 결제 정보 조회 완료: reservationId=$reservationId, paymentId=${it.id}" } } + ?.also { log.info { "[findByReservationIdOrThrow] 결제 정보 조회 완료: reservationId=$reservationId, paymentId=${it.id}" } } ?: run { - log.warn { "[PaymentService.findByReservationIdOrThrow] 결제 정보 조회 실패: reservationId=$reservationId" } + log.warn { "[findByReservationIdOrThrow] 결제 정보 조회 실패: reservationId=$reservationId" } throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND) } } private fun findByReservationIdOrNull(reservationId: Long): PaymentEntity? { - log.info { "[PaymentService.findByReservationIdOrThrow] 결제 정보 조회 시작: reservationId=: $reservationId" } + log.info { "[findByReservationIdOrThrow] 결제 정보 조회 시작: reservationId=: $reservationId" } return paymentRepository.findByReservationId(reservationId) .also { if (it != null) { - log.info { "[PaymentService.findByReservationIdOrThrow] 결제 정보 조회 완료: reservationId=$reservationId, paymentId=${it.id}" } + log.info { "[findByReservationIdOrThrow] 결제 정보 조회 완료: reservationId=$reservationId, paymentId=${it.id}" } } else { - log.warn { "[PaymentService.findByReservationIdOrThrow] 결제 정보 조회 실패: reservationId=$reservationId" } + log.warn { "[findByReservationIdOrThrow] 결제 정보 조회 실패: reservationId=$reservationId" } } } } private fun findDetailByPaymentIdOrNull(paymentId: Long): PaymentDetailEntity? { - log.info { "[PaymentService.findDetailByPaymentIdOrThrow] 결제 상세 정보 조회 시작: paymentId=$paymentId" } + log.info { "[findDetailByPaymentIdOrThrow] 결제 상세 정보 조회 시작: paymentId=$paymentId" } return paymentDetailRepository.findByPaymentId(paymentId).also { if (it != null) { - log.info { "[PaymentService.findDetailByPaymentIdOrThrow] 결제 상세 정보 조회 완료: paymentId=$paymentId, detailId=${it.id}}" } + log.info { "[findDetailByPaymentIdOrThrow] 결제 상세 정보 조회 완료: paymentId=$paymentId, detailId=${it.id}}" } } else { - log.warn { "[PaymentService.findDetailByPaymentIdOrThrow] 결제 상세 정보 조회 실패: paymentId=$paymentId" } + log.warn { "[findDetailByPaymentIdOrThrow] 결제 상세 정보 조회 실패: paymentId=$paymentId" } } } } private fun findCancelByPaymentIdOrNull(paymentId: Long): CanceledPaymentEntity? { - log.info { "[PaymentService.findDetailByReservationId] 취소 결제 정보 조회 시작: paymentId=${paymentId}" } + log.info { "[findDetailByReservationId] 취소 결제 정보 조회 시작: paymentId=${paymentId}" } return canceledPaymentRepository.findByPaymentId(paymentId).also { if (it == null) { - log.info { "[PaymentService.findDetailByReservationId] 취소 결제 정보가 없음: paymentId=${paymentId}" } + log.info { "[findDetailByReservationId] 취소 결제 정보가 없음: paymentId=${paymentId}" } } else { - log.info { "[PaymentService.findDetailByReservationId] 취소 결제 정보 조회 완료: paymentId=${paymentId}, cancelId=${it.id}" } + log.info { "[findDetailByReservationId] 취소 결제 정보 조회 완료: paymentId=${paymentId}, cancelId=${it.id}" } } } } diff --git a/service/src/main/kotlin/com/sangdol/roomescape/payment/business/PaymentWriter.kt b/service/src/main/kotlin/com/sangdol/roomescape/payment/business/PaymentWriter.kt index 06226ec4..bbeda832 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/payment/business/PaymentWriter.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/payment/business/PaymentWriter.kt @@ -10,7 +10,7 @@ import com.sangdol.roomescape.payment.infrastructure.persistence.* import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.stereotype.Component -import java.time.LocalDateTime +import java.time.Instant private val log: KLogger = KotlinLogging.logger {} @@ -60,7 +60,7 @@ class PaymentWriter( fun cancel( userId: Long, payment: PaymentEntity, - requestedAt: LocalDateTime, + requestedAt: Instant, cancelResponse: PaymentClientCancelResponse ): CanceledPaymentEntity { log.debug { "[PaymentWriterV2.cancelPayment] 결제 취소 정보 저장 시작: payment.id=${payment.id}" } diff --git a/service/src/main/kotlin/com/sangdol/roomescape/payment/exception/PaymentErrorCode.kt b/service/src/main/kotlin/com/sangdol/roomescape/payment/exception/PaymentErrorCode.kt index 9d2ea09a..db404e83 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/payment/exception/PaymentErrorCode.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/payment/exception/PaymentErrorCode.kt @@ -1,7 +1,7 @@ package com.sangdol.roomescape.payment.exception -import com.sangdol.common.types.web.HttpStatus import com.sangdol.common.types.exception.ErrorCode +import com.sangdol.common.types.web.HttpStatus enum class PaymentErrorCode( override val httpStatus: HttpStatus, diff --git a/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/client/TosspayCancelDTO.kt b/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/client/TosspayCancelDTO.kt index a4077b27..2ba4f6a1 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/client/TosspayCancelDTO.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/client/TosspayCancelDTO.kt @@ -6,7 +6,7 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.sangdol.roomescape.payment.infrastructure.common.PaymentStatus import com.sangdol.roomescape.payment.infrastructure.persistence.CanceledPaymentEntity -import java.time.LocalDateTime +import java.time.Instant import java.time.OffsetDateTime data class PaymentClientCancelResponse( @@ -28,10 +28,10 @@ fun CancelDetail.toEntity( id: Long, paymentId: Long, canceledBy: Long, - cancelRequestedAt: LocalDateTime + cancelRequestedAt: Instant ) = CanceledPaymentEntity( id = id, - canceledAt = this.canceledAt, + canceledAt = this.canceledAt.toInstant(), requestedAt = cancelRequestedAt, paymentId = paymentId, canceledBy = canceledBy, diff --git a/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/client/TosspayConfirmDTO.kt b/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/client/TosspayConfirmDTO.kt index ebcf0719..ca8f549a 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/client/TosspayConfirmDTO.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/client/TosspayConfirmDTO.kt @@ -34,8 +34,8 @@ fun PaymentClientConfirmResponse.toEntity( paymentKey = this.paymentKey, orderId = orderId, totalAmount = this.totalAmount, - requestedAt = this.requestedAt, - approvedAt = this.approvedAt, + requestedAt = this.requestedAt.toInstant(), + approvedAt = this.approvedAt.toInstant(), type = paymentType, method = this.method, status = this.status, diff --git a/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/common/PaymentTypes.kt b/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/common/PaymentTypes.kt index 44ef401f..5fd64a1d 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/common/PaymentTypes.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/common/PaymentTypes.kt @@ -1,10 +1,10 @@ package com.sangdol.roomescape.payment.infrastructure.common import com.fasterxml.jackson.annotation.JsonCreator -import io.github.oshai.kotlinlogging.KLogger -import io.github.oshai.kotlinlogging.KotlinLogging import com.sangdol.roomescape.payment.exception.PaymentErrorCode import com.sangdol.roomescape.payment.exception.PaymentException +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging private val log: KLogger = KotlinLogging.logger {} diff --git a/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/persistence/CanceledPaymentEntity.kt b/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/persistence/CanceledPaymentEntity.kt index 4db2e14d..d85a26ce 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/persistence/CanceledPaymentEntity.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/persistence/CanceledPaymentEntity.kt @@ -3,8 +3,7 @@ package com.sangdol.roomescape.payment.infrastructure.persistence import com.sangdol.common.persistence.PersistableBaseEntity import jakarta.persistence.Entity import jakarta.persistence.Table -import java.time.LocalDateTime -import java.time.OffsetDateTime +import java.time.Instant @Entity @Table(name = "canceled_payment") @@ -12,8 +11,8 @@ class CanceledPaymentEntity( id: Long, val paymentId: Long, - val requestedAt: LocalDateTime, - val canceledAt: OffsetDateTime, + val requestedAt: Instant, + val canceledAt: Instant, val canceledBy: Long, val cancelReason: String, val cancelAmount: Int, diff --git a/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/persistence/PaymentEntity.kt b/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/persistence/PaymentEntity.kt index 48aa5587..1571e15e 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/persistence/PaymentEntity.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/payment/infrastructure/persistence/PaymentEntity.kt @@ -8,7 +8,7 @@ import jakarta.persistence.Entity import jakarta.persistence.EnumType import jakarta.persistence.Enumerated import jakarta.persistence.Table -import java.time.OffsetDateTime +import java.time.Instant @Entity @Table(name = "payment") @@ -19,8 +19,8 @@ class PaymentEntity( val paymentKey: String, val orderId: String, val totalAmount: Int, - val requestedAt: OffsetDateTime, - val approvedAt: OffsetDateTime, + val requestedAt: Instant, + val approvedAt: Instant, @Enumerated(EnumType.STRING) val type: PaymentType, diff --git a/service/src/main/kotlin/com/sangdol/roomescape/payment/web/PaymentDTO.kt b/service/src/main/kotlin/com/sangdol/roomescape/payment/web/PaymentDTO.kt index 1e39f186..0ed997b0 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/payment/web/PaymentDTO.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/payment/web/PaymentDTO.kt @@ -6,8 +6,7 @@ import com.sangdol.roomescape.payment.infrastructure.common.PaymentStatus import com.sangdol.roomescape.payment.infrastructure.common.PaymentType import com.sangdol.roomescape.payment.infrastructure.persistence.* import com.sangdol.roomescape.payment.web.PaymentDetailResponse.* -import java.time.LocalDateTime -import java.time.OffsetDateTime +import java.time.Instant data class PaymentConfirmRequest( val paymentKey: String, @@ -24,7 +23,7 @@ data class PaymentCreateResponse( data class PaymentCancelRequest( val reservationId: Long, val cancelReason: String, - val requestedAt: LocalDateTime = LocalDateTime.now() + val requestedAt: Instant = Instant.now() ) data class PaymentWithDetailResponse( @@ -32,8 +31,8 @@ data class PaymentWithDetailResponse( val totalAmount: Int, val method: String, val status: PaymentStatus, - val requestedAt: OffsetDateTime, - val approvedAt: OffsetDateTime, + val requestedAt: Instant, + val approvedAt: Instant, val detail: PaymentDetailResponse?, val cancel: PaymentCancelDetailResponse?, ) @@ -120,8 +119,8 @@ fun PaymentEasypayPrepaidDetailEntity.toEasyPayPrepaidDetailResponse(): EasyPayP } data class PaymentCancelDetailResponse( - val cancellationRequestedAt: LocalDateTime, - val cancellationApprovedAt: OffsetDateTime?, + val cancellationRequestedAt: Instant, + val cancellationApprovedAt: Instant?, val cancelReason: String, val canceledBy: Long, ) diff --git a/service/src/main/kotlin/com/sangdol/roomescape/region/business/RegionService.kt b/service/src/main/kotlin/com/sangdol/roomescape/region/business/RegionService.kt index de6f1111..8da6c9fe 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/region/business/RegionService.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/region/business/RegionService.kt @@ -1,13 +1,13 @@ package com.sangdol.roomescape.region.business -import io.github.oshai.kotlinlogging.KLogger -import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional import com.sangdol.roomescape.region.exception.RegionErrorCode import com.sangdol.roomescape.region.exception.RegionException import com.sangdol.roomescape.region.infrastructure.persistence.RegionRepository import com.sangdol.roomescape.region.web.* +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional private val log: KLogger = KotlinLogging.logger {} @@ -17,56 +17,56 @@ class RegionService( ) { @Transactional(readOnly = true) fun readAllSido(): SidoListResponse { - log.info { "[RegionService.readAllSido] 모든 시/도 조회 시작" } + log.info { "[readAllSido] 모든 시/도 조회 시작" } val result: List> = regionRepository.readAllSido() if (result.isEmpty()) { - log.warn { "[RegionService.readAllSido] 시/도 조회 실패" } + log.warn { "[readAllSido] 시/도 조회 실패" } throw RegionException(RegionErrorCode.SIDO_CODE_NOT_FOUND) } return SidoListResponse(result.map { SidoResponse(code = it.first, name = it.second) }).also { - log.info { "[RegionService.readAllSido] ${it.sidoList.size}개의 시/도 조회 완료" } + log.info { "[readAllSido] ${it.sidoList.size}개의 시/도 조회 완료" } } } @Transactional(readOnly = true) fun findSigunguBySido(sidoCode: String): SigunguListResponse { - log.info { "[RegionService.findSigunguBySido] 시/군/구 조회 시작: sidoCode=${sidoCode}" } + log.info { "[findSigunguBySido] 시/군/구 조회 시작: sidoCode=${sidoCode}" } val result: List> = regionRepository.findAllSigunguBySido(sidoCode) if (result.isEmpty()) { - log.warn { "[RegionService.findSigunguBySido] 시/군/구 조회 실패: sidoCode=${sidoCode}" } + log.warn { "[findSigunguBySido] 시/군/구 조회 실패: sidoCode=${sidoCode}" } throw RegionException(RegionErrorCode.SIGUNGU_CODE_NOT_FOUND) } return SigunguListResponse(result.map { SigunguResponse(code = it.first, name = it.second) }).also { - log.info { "[RegionService.findSigunguBySido] sidoCode=${sidoCode}인 ${it.sigunguList.size}개의 시/군/구 조회 완료" } + log.info { "[findSigunguBySido] sidoCode=${sidoCode}인 ${it.sigunguList.size}개의 시/군/구 조회 완료" } } } @Transactional(readOnly = true) fun findRegionCode(sidoCode: String, sigunguCode: String): RegionCodeResponse { - log.info { "[RegionService.findRegionCode] 지역 코드 조회 시작: sidoCode=${sidoCode} / sigunguCode=${sigunguCode}" } + log.info { "[findRegionCode] 지역 코드 조회 시작: sidoCode=${sidoCode} / sigunguCode=${sigunguCode}" } return regionRepository.findRegionCode(sidoCode, sigunguCode)?.let { - log.info { "[RegionService.findRegionCode] 지역 코드 조회 완료: code=${it} sidoCode=${sidoCode} / sigunguCode=${sigunguCode}" } + log.info { "[findRegionCode] 지역 코드 조회 완료: code=${it} sidoCode=${sidoCode} / sigunguCode=${sigunguCode}" } RegionCodeResponse(it) } ?: run { - log.warn { "[RegionService.findRegionCode] 지역 코드 조회 실패: sidoCode=${sidoCode} / sigunguCode=${sigunguCode}" } + log.warn { "[findRegionCode] 지역 코드 조회 실패: sidoCode=${sidoCode} / sigunguCode=${sigunguCode}" } throw RegionException(RegionErrorCode.REGION_CODE_NOT_FOUND) } } @Transactional(readOnly = true) fun findRegionInfo(regionCode: String): RegionInfoResponse { - log.info { "[RegionService.findRegionInfo] 지역 정보 조회 시작: regionCode=${regionCode}" } + log.info { "[findRegionInfo] 지역 정보 조회 시작: regionCode=${regionCode}" } return regionRepository.findByCode(regionCode)?.let { - log.info { "[RegionService.findRegionInfo] 지역 정보 조회 완료: code=${it} regionCode=${regionCode}" } + log.info { "[findRegionInfo] 지역 정보 조회 완료: code=${it} regionCode=${regionCode}" } RegionInfoResponse(it.code, it.sidoName, it.sigunguName) } ?: run { - log.warn { "[RegionService.findRegionInfo] 지역 정보 조회 실패: regionCode=${regionCode}" } + log.warn { "[findRegionInfo] 지역 정보 조회 실패: regionCode=${regionCode}" } throw RegionException(RegionErrorCode.REGION_CODE_NOT_FOUND) } } diff --git a/service/src/main/kotlin/com/sangdol/roomescape/region/exception/RegionException.kt b/service/src/main/kotlin/com/sangdol/roomescape/region/exception/RegionException.kt index b7201c9b..9872add4 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/region/exception/RegionException.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/region/exception/RegionException.kt @@ -1,8 +1,8 @@ package com.sangdol.roomescape.region.exception -import com.sangdol.common.types.web.HttpStatus import com.sangdol.common.types.exception.ErrorCode import com.sangdol.common.types.exception.RoomescapeException +import com.sangdol.common.types.web.HttpStatus class RegionException( override val errorCode: RegionErrorCode, diff --git a/service/src/main/kotlin/com/sangdol/roomescape/reservation/business/ReservationService.kt b/service/src/main/kotlin/com/sangdol/roomescape/reservation/business/ReservationService.kt index 61587cde..315e1b6f 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/reservation/business/ReservationService.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/reservation/business/ReservationService.kt @@ -19,7 +19,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import java.time.LocalDateTime +import java.time.Instant private val log: KLogger = KotlinLogging.logger {} @@ -40,19 +40,19 @@ class ReservationService( user: CurrentUserContext, request: PendingReservationCreateRequest ): PendingReservationCreateResponse { - log.info { "[ReservationService.createPendingReservation] Pending 예약 생성 시작: schedule=${request.scheduleId}" } + log.info { "[createPendingReservation] Pending 예약 생성 시작: schedule=${request.scheduleId}" } validateCanCreate(request) val reservation: ReservationEntity = request.toEntity(id = idGenerator.create(), userId = user.id) return PendingReservationCreateResponse(reservationRepository.save(reservation).id) - .also { log.info { "[ReservationService.createPendingReservation] Pending 예약 생성 완료: reservationId=${it}, schedule=${request.scheduleId}" } } + .also { log.info { "[createPendingReservation] Pending 예약 생성 완료: reservationId=${it}, schedule=${request.scheduleId}" } } } @Transactional fun confirmReservation(id: Long) { - log.info { "[ReservationService.confirmReservation] Pending 예약 확정 시작: reservationId=${id}" } + log.info { "[confirmReservation] Pending 예약 확정 시작: reservationId=${id}" } val reservation: ReservationEntity = findOrThrow(id) run { @@ -63,13 +63,13 @@ class ReservationService( changeStatus = ScheduleStatus.RESERVED ) }.also { - log.info { "[ReservationService.confirmReservation] Pending 예약 확정 완료: reservationId=${id}" } + log.info { "[confirmReservation] Pending 예약 확정 완료: reservationId=${id}" } } } @Transactional fun cancelReservation(user: CurrentUserContext, reservationId: Long, request: ReservationCancelRequest) { - log.info { "[ReservationService.cancelReservation] 예약 취소 시작: userId=${user.id}, reservationId=${reservationId}" } + log.info { "[cancelReservation] 예약 취소 시작: userId=${user.id}, reservationId=${reservationId}" } val reservation: ReservationEntity = findOrThrow(reservationId) @@ -82,13 +82,13 @@ class ReservationService( saveCanceledReservation(user, reservation, request.cancelReason) reservation.cancel() }.also { - log.info { "[ReservationService.cancelReservation] 예약 취소 완료: reservationId=${reservationId}" } + log.info { "[cancelReservation] 예약 취소 완료: reservationId=${reservationId}" } } } @Transactional(readOnly = true) fun findAllUserReservationOverview(user: CurrentUserContext): ReservationOverviewListResponse { - log.info { "[ReservationService.findSummaryByMemberId] 예약 조회 시작: userId=${user.id}" } + log.info { "[findSummaryByMemberId] 예약 조회 시작: userId=${user.id}" } val reservations: List = reservationRepository.findAllByUserIdAndStatusIsIn( userId = user.id, @@ -99,13 +99,13 @@ class ReservationService( val schedule: ScheduleOverviewResponse = scheduleService.findScheduleOverviewById(it.scheduleId) it.toOverviewResponse(schedule) }).also { - log.info { "[ReservationService.findSummaryByMemberId] ${it.reservations.size}개의 예약 조회 완료: userId=${user.id}" } + log.info { "[findSummaryByMemberId] ${it.reservations.size}개의 예약 조회 완료: userId=${user.id}" } } } @Transactional(readOnly = true) fun findDetailById(id: Long): ReservationDetailResponse { - log.info { "[ReservationService.findDetailById] 예약 상세 조회 시작: reservationId=${id}" } + log.info { "[findDetailById] 예약 상세 조회 시작: reservationId=${id}" } val reservation: ReservationEntity = findOrThrow(id) val user: UserContactResponse = userService.findContactById(reservation.userId) @@ -115,17 +115,17 @@ class ReservationService( user = user, payment = paymentDetail ).also { - log.info { "[ReservationService.findDetailById] 예약 상세 조회 완료: reservationId=${id}" } + log.info { "[findDetailById] 예약 상세 조회 완료: reservationId=${id}" } } } private fun findOrThrow(id: Long): ReservationEntity { - log.info { "[ReservationService.findOrThrow] 예약 조회 시작: reservationId=${id}" } + log.info { "[findOrThrow] 예약 조회 시작: reservationId=${id}" } return reservationRepository.findByIdOrNull(id) - ?.also { log.info { "[ReservationService.findOrThrow] 예약 조회 완료: reservationId=${id}" } } + ?.also { log.info { "[findOrThrow] 예약 조회 완료: reservationId=${id}" } } ?: run { - log.warn { "[ReservationService.findOrThrow] 예약 조회 실패: reservationId=${id}" } + log.warn { "[findOrThrow] 예약 조회 실패: reservationId=${id}" } throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND) } } @@ -136,7 +136,7 @@ class ReservationService( cancelReason: String ) { if (reservation.userId != user.id) { - log.warn { "[ReservationService.createCanceledPayment] 예약자 본인 또는 관리자가 아닌 회원의 취소 요청: reservationId=${reservation.id}, userId=${user.id}" } + log.warn { "[createCanceledPayment] 예약자 본인 또는 관리자가 아닌 회원의 취소 요청: reservationId=${reservation.id}, userId=${user.id}" } throw ReservationException(ReservationErrorCode.NO_PERMISSION_TO_CANCEL_RESERVATION) } @@ -145,7 +145,7 @@ class ReservationService( reservationId = reservation.id, canceledBy = user.id, cancelReason = cancelReason, - canceledAt = LocalDateTime.now(), + canceledAt = Instant.now(), status = CanceledReservationStatus.COMPLETED ).also { canceledReservationRepository.save(it) diff --git a/service/src/main/kotlin/com/sangdol/roomescape/reservation/business/ReservationValidator.kt b/service/src/main/kotlin/com/sangdol/roomescape/reservation/business/ReservationValidator.kt index d8211272..84fea83b 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/reservation/business/ReservationValidator.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/reservation/business/ReservationValidator.kt @@ -1,14 +1,14 @@ package com.sangdol.roomescape.reservation.business -import io.github.oshai.kotlinlogging.KLogger -import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.stereotype.Component import com.sangdol.roomescape.reservation.exception.ReservationErrorCode import com.sangdol.roomescape.reservation.exception.ReservationException import com.sangdol.roomescape.reservation.web.PendingReservationCreateRequest import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus import com.sangdol.roomescape.schedule.web.ScheduleSummaryResponse import com.sangdol.roomescape.theme.web.ThemeInfoResponse +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Component private val log: KLogger = KotlinLogging.logger {} diff --git a/service/src/main/kotlin/com/sangdol/roomescape/reservation/business/scheduler/IncompletedReservationScheduler.kt b/service/src/main/kotlin/com/sangdol/roomescape/reservation/business/scheduler/IncompletedReservationScheduler.kt index b15876b1..01edf4ad 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/reservation/business/scheduler/IncompletedReservationScheduler.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/reservation/business/scheduler/IncompletedReservationScheduler.kt @@ -9,7 +9,7 @@ import org.springframework.scheduling.annotation.EnableScheduling import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional -import java.time.LocalDateTime +import java.time.Instant import java.util.concurrent.TimeUnit private val log: KLogger = KotlinLogging.logger {} @@ -26,7 +26,7 @@ class IncompletedReservationScheduler( fun processExpiredHoldSchedule() { log.info { "[IncompletedReservationScheduler] 만료 시간이 지난 ${ScheduleStatus.HOLD} 상태의 일정 재활성화 시작" } - scheduleRepository.releaseExpiredHolds(LocalDateTime.now()).also { + scheduleRepository.releaseExpiredHolds(Instant.now()).also { log.info { "[IncompletedReservationScheduler] ${it}개의 일정 재활성화 완료" } } } @@ -36,7 +36,7 @@ class IncompletedReservationScheduler( fun processExpiredReservation() { log.info { "[IncompletedReservationScheduler] 결제되지 않은 예약 만료 처리 시작 " } - reservationRepository.expirePendingReservations(LocalDateTime.now()).also { + reservationRepository.expirePendingReservations(Instant.now()).also { log.info { "[IncompletedReservationScheduler] ${it}개의 예약 및 일정 처리 완료" } } } diff --git a/service/src/main/kotlin/com/sangdol/roomescape/reservation/docs/ReservationAPI.kt b/service/src/main/kotlin/com/sangdol/roomescape/reservation/docs/ReservationAPI.kt index 2d53eecf..89191484 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/reservation/docs/ReservationAPI.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/reservation/docs/ReservationAPI.kt @@ -1,9 +1,9 @@ package com.sangdol.roomescape.reservation.docs import com.sangdol.common.types.web.CommonApiResponse -import com.sangdol.roomescape.common.types.CurrentUserContext import com.sangdol.roomescape.auth.web.support.User import com.sangdol.roomescape.auth.web.support.UserOnly +import com.sangdol.roomescape.common.types.CurrentUserContext import com.sangdol.roomescape.reservation.web.* import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse diff --git a/service/src/main/kotlin/com/sangdol/roomescape/reservation/exception/ReservationErrorCode.kt b/service/src/main/kotlin/com/sangdol/roomescape/reservation/exception/ReservationErrorCode.kt index 1e0b3829..82158113 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/reservation/exception/ReservationErrorCode.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/reservation/exception/ReservationErrorCode.kt @@ -1,7 +1,7 @@ package com.sangdol.roomescape.reservation.exception -import com.sangdol.common.types.web.HttpStatus import com.sangdol.common.types.exception.ErrorCode +import com.sangdol.common.types.web.HttpStatus enum class ReservationErrorCode( override val httpStatus: HttpStatus, diff --git a/service/src/main/kotlin/com/sangdol/roomescape/reservation/infrastructure/persistence/CanceledReservationEntity.kt b/service/src/main/kotlin/com/sangdol/roomescape/reservation/infrastructure/persistence/CanceledReservationEntity.kt index 81832831..ab12c7bc 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/reservation/infrastructure/persistence/CanceledReservationEntity.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/reservation/infrastructure/persistence/CanceledReservationEntity.kt @@ -5,7 +5,7 @@ import jakarta.persistence.Entity import jakarta.persistence.EnumType import jakarta.persistence.Enumerated import jakarta.persistence.Table -import java.time.LocalDateTime +import java.time.Instant @Entity @Table(name = "canceled_reservation") @@ -15,7 +15,7 @@ class CanceledReservationEntity( val reservationId: Long, val canceledBy: Long, val cancelReason: String, - val canceledAt: LocalDateTime, + val canceledAt: Instant, @Enumerated(value = EnumType.STRING) val status: CanceledReservationStatus, diff --git a/service/src/main/kotlin/com/sangdol/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt b/service/src/main/kotlin/com/sangdol/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt index 9ef52754..7d1827f9 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt @@ -4,14 +4,15 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param -import java.time.LocalDateTime +import java.time.Instant interface ReservationRepository : JpaRepository { fun findAllByUserIdAndStatusIsIn(userId: Long, statuses: List): List @Modifying - @Query(""" + @Query( + """ UPDATE reservation r JOIN @@ -23,6 +24,7 @@ interface ReservationRepository : JpaRepository { s.hold_expired_at = NULL WHERE r.status = 'PENDING' AND r.created_at <= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 5 MINUTE) - """, nativeQuery = true) - fun expirePendingReservations(@Param("now") now: LocalDateTime): Int + """, nativeQuery = true + ) + fun expirePendingReservations(@Param("now") now: Instant): Int } diff --git a/service/src/main/kotlin/com/sangdol/roomescape/reservation/web/ReservationController.kt b/service/src/main/kotlin/com/sangdol/roomescape/reservation/web/ReservationController.kt index b218ea2b..1a545826 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/reservation/web/ReservationController.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/reservation/web/ReservationController.kt @@ -1,8 +1,8 @@ package com.sangdol.roomescape.reservation.web import com.sangdol.common.types.web.CommonApiResponse -import com.sangdol.roomescape.common.types.CurrentUserContext import com.sangdol.roomescape.auth.web.support.User +import com.sangdol.roomescape.common.types.CurrentUserContext import com.sangdol.roomescape.reservation.business.ReservationService import com.sangdol.roomescape.reservation.docs.ReservationAPI import jakarta.validation.Valid diff --git a/service/src/main/kotlin/com/sangdol/roomescape/reservation/web/ReservationDto.kt b/service/src/main/kotlin/com/sangdol/roomescape/reservation/web/ReservationDto.kt index 2349b81b..6a950ff6 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/reservation/web/ReservationDto.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/reservation/web/ReservationDto.kt @@ -1,13 +1,13 @@ package com.sangdol.roomescape.reservation.web -import jakarta.validation.constraints.NotEmpty import com.sangdol.roomescape.payment.web.PaymentWithDetailResponse import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationEntity import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus import com.sangdol.roomescape.schedule.web.ScheduleOverviewResponse import com.sangdol.roomescape.user.web.UserContactResponse +import jakarta.validation.constraints.NotEmpty +import java.time.Instant import java.time.LocalDate -import java.time.LocalDateTime import java.time.LocalTime data class PendingReservationCreateRequest( @@ -79,7 +79,7 @@ data class ReservationDetailResponse( val id: Long, val reserver: ReserverInfo, val user: UserContactResponse, - val applicationDateTime: LocalDateTime, + val applicationDateTime: Instant, val payment: PaymentWithDetailResponse?, ) diff --git a/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleService.kt b/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleService.kt index d36ca16d..95be6d49 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleService.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleService.kt @@ -1,6 +1,8 @@ package com.sangdol.roomescape.schedule.business import com.sangdol.common.persistence.IDGenerator +import com.sangdol.common.utils.KoreaDate +import com.sangdol.common.utils.KoreaTime import com.sangdol.roomescape.admin.business.AdminService import com.sangdol.roomescape.common.types.AuditingInfo import com.sangdol.roomescape.common.types.Auditor @@ -43,21 +45,23 @@ class ScheduleService( // ======================================== @Transactional(readOnly = true) fun getStoreScheduleByDate(storeId: Long, date: LocalDate): ScheduleWithThemeListResponse { - log.info { "[ScheduleService.getStoreScheduleByDate] 매장 일정 조회: storeId=${storeId}, date=$date" } - val currentDate = LocalDate.now() + log.info { "[getStoreScheduleByDate] 매장 일정 조회: storeId=${storeId}, date=$date" } + + val currentDate: LocalDate = KoreaDate.today() + val currentTime: LocalTime = KoreaTime.now() if (date.isBefore(currentDate)) { - log.warn { "[ScheduleService.getStoreScheduleByDate] 이전 날짜 선택으로 인한 실패: date=${date}" } + log.warn { "[getStoreScheduleByDate] 이전 날짜 선택으로 인한 실패: date=${date}" } throw ScheduleException(ScheduleErrorCode.PAST_DATE_TIME) } val schedules: List = scheduleRepository.findStoreSchedulesWithThemeByDate(storeId, date) - .filter { it.date.isAfter(date) || (it.date.isEqual(date) && it.time.isAfter(LocalTime.now())) } + .filter { it.date.isAfter(currentDate) || it.time.isAfter(currentTime) } return schedules.toResponse() .also { - log.info { "[ScheduleService.getStoreScheduleByDate] storeId=${storeId}, date=$date 인 ${it.schedules.size}개 일정 조회 완료" } + log.info { "[getStoreScheduleByDate] storeId=${storeId}, date=$date 인 ${it.schedules.size}개 일정 조회 완료" } } } @@ -66,20 +70,20 @@ class ScheduleService( // ======================================== @Transactional fun holdSchedule(id: Long) { - log.info { "[ScheduleService.holdSchedule] 일정 Holding 시작: id=$id" } + log.info { "[holdSchedule] 일정 Holding 시작: id=$id" } val result: Int = scheduleRepository.changeStatus( id = id, currentStatus = ScheduleStatus.AVAILABLE, changeStatus = ScheduleStatus.HOLD ).also { - log.info { "[ScheduleService.holdSchedule] $it 개의 row 변경 완료" } + log.info { "[holdSchedule] $it 개의 row 변경 완료" } } if (result == 0) { throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE) } - log.info { "[ScheduleService.holdSchedule] 일정 Holding 완료: id=$id" } + log.info { "[holdSchedule] 일정 Holding 완료: id=$id" } } // ======================================== @@ -87,9 +91,9 @@ class ScheduleService( // ======================================== @Transactional(readOnly = true) fun searchSchedules(storeId: Long, date: LocalDate?, themeId: Long?): AdminScheduleSummaryListResponse { - log.info { "[ScheduleService.searchSchedules] 일정 검색 시작: storeId=$storeId, date=$date, themeId=$themeId" } + log.info { "[searchSchedules] 일정 검색 시작: storeId=$storeId, date=$date, themeId=$themeId" } - val searchDate = date ?: LocalDate.now() + val searchDate = date ?: KoreaDate.today() val schedules: List = scheduleRepository.findStoreSchedulesWithThemeByDate(storeId, searchDate) @@ -98,13 +102,13 @@ class ScheduleService( return schedules.toAdminSummaryListResponse() .also { - log.info { "[ScheduleService.searchSchedules] ${it.schedules.size} 개의 일정 조회 완료" } + log.info { "[searchSchedules] ${it.schedules.size} 개의 일정 조회 완료" } } } @Transactional(readOnly = true) fun findScheduleAudit(id: Long): AuditingInfo { - log.info { "[ScheduleService.findDetail] 일정 감사 정보 조회 시작: id=$id" } + log.info { "[findDetail] 일정 감사 정보 조회 시작: id=$id" } val schedule: ScheduleEntity = findOrThrow(id) @@ -112,7 +116,7 @@ class ScheduleService( val updatedBy: Auditor = adminService.findOperatorOrUnknown(schedule.updatedBy) return AuditingInfo(schedule.createdAt, createdBy, schedule.updatedAt, updatedBy) - .also { log.info { "[ScheduleService.findDetail] 일정 감사 정보 조회 완료: id=$id" } } + .also { log.info { "[findDetail] 일정 감사 정보 조회 완료: id=$id" } } } // ======================================== @@ -120,7 +124,7 @@ class ScheduleService( // ======================================== @Transactional fun createSchedule(storeId: Long, request: ScheduleCreateRequest): ScheduleCreateResponse { - log.info { "[ScheduleService.createSchedule] 일정 생성 시작: storeId=${storeId}, date=${request.date}, time=${request.time}, themeId=${request.themeId}" } + log.info { "[createSchedule] 일정 생성 시작: storeId=${storeId}, date=${request.date}, time=${request.time}, themeId=${request.themeId}" } scheduleValidator.validateCanCreate(storeId, request) @@ -136,16 +140,16 @@ class ScheduleService( return ScheduleCreateResponse(schedule.id) .also { - log.info { "[ScheduleService.createSchedule] 일정 생성 완료: id=${it.id}" } + log.info { "[createSchedule] 일정 생성 완료: id=${it.id}" } } } @Transactional fun updateSchedule(id: Long, request: ScheduleUpdateRequest) { - log.info { "[ScheduleService.updateSchedule] 일정 수정 시작: id=$id, request=${request}" } + log.info { "[updateSchedule] 일정 수정 시작: id=$id, request=${request}" } if (request.isAllParamsNull()) { - log.info { "[ScheduleService.updateSchedule] 일정 변경 사항 없음: id=$id" } + log.info { "[updateSchedule] 일정 변경 사항 없음: id=$id" } return } @@ -154,20 +158,20 @@ class ScheduleService( } schedule.modifyIfNotNull(request.time, request.status).also { - log.info { "[ScheduleService.updateSchedule] 일정 수정 완료: id=$id, request=${request}" } + log.info { "[updateSchedule] 일정 수정 완료: id=$id, request=${request}" } } } @Transactional fun deleteSchedule(id: Long) { - log.info { "[ScheduleService.deleteSchedule] 일정 삭제 시작: id=$id" } + log.info { "[deleteSchedule] 일정 삭제 시작: id=$id" } val schedule: ScheduleEntity = findOrThrow(id).also { scheduleValidator.validateCanDelete(it) } scheduleRepository.delete(schedule).also { - log.info { "[ScheduleService.deleteSchedule] 일정 삭제 완료: id=$id" } + log.info { "[deleteSchedule] 일정 삭제 완료: id=$id" } } } @@ -176,24 +180,24 @@ class ScheduleService( // ======================================== @Transactional(readOnly = true) fun findSummaryWithLock(id: Long): ScheduleSummaryResponse { - log.info { "[ScheduleService.findDateTimeById] 일정 개요 조회 시작 : id=$id" } + log.info { "[findDateTimeById] 일정 개요 조회 시작 : id=$id" } val schedule: ScheduleEntity = scheduleRepository.findByIdForUpdate(id) ?: run { - log.warn { "[ScheduleService.updateSchedule] 일정 조회 실패. id=$id" } + log.warn { "[updateSchedule] 일정 조회 실패. id=$id" } throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_FOUND) } return schedule.toSummaryResponse() .also { - log.info { "[ScheduleService.findDateTimeById] 일정 개요 조회 완료: id=$id" } + log.info { "[findDateTimeById] 일정 개요 조회 완료: id=$id" } } } @Transactional(readOnly = true) fun findScheduleOverviewById(id: Long): ScheduleOverviewResponse { val overview: ScheduleOverview = scheduleRepository.findOverviewByIdOrNull(id) ?: run { - log.warn { "[ScheduleService.findScheduleOverview] 일정 개요 조회 실패: id=$id" } + log.warn { "[findScheduleOverview] 일정 개요 조회 실패: id=$id" } throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_FOUND) } @@ -202,10 +206,10 @@ class ScheduleService( @Transactional fun changeStatus(scheduleId: Long, currentStatus: ScheduleStatus, changeStatus: ScheduleStatus) { - log.info { "[ScheduleService.reserveSchedule] 일정 상태 변경 시작: id=${scheduleId}, currentStatus=${currentStatus}, changeStatus=${changeStatus}" } + log.info { "[reserveSchedule] 일정 상태 변경 시작: id=${scheduleId}, currentStatus=${currentStatus}, changeStatus=${changeStatus}" } scheduleRepository.changeStatus(scheduleId, currentStatus, changeStatus).also { - log.info { "[ScheduleService.reserveSchedule] 일정 상태 변경 완료: id=${scheduleId}, currentStatus=${currentStatus}, changeStatus=${changeStatus}" } + log.info { "[reserveSchedule] 일정 상태 변경 완료: id=${scheduleId}, currentStatus=${currentStatus}, changeStatus=${changeStatus}" } } } @@ -213,12 +217,12 @@ class ScheduleService( // Common (공통 메서드) // ======================================== private fun findOrThrow(id: Long): ScheduleEntity { - log.info { "[ScheduleService.findOrThrow] 일정 조회 시작: id=$id" } + log.info { "[findOrThrow] 일정 조회 시작: id=$id" } return scheduleRepository.findByIdOrNull(id) - ?.also { log.info { "[ScheduleService.findOrThrow] 일정 조회 완료: id=$id" } } + ?.also { log.info { "[findOrThrow] 일정 조회 완료: id=$id" } } ?: run { - log.warn { "[ScheduleService.updateSchedule] 일정 조회 실패. id=$id" } + log.warn { "[updateSchedule] 일정 조회 실패. id=$id" } throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_FOUND) } } diff --git a/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleValidator.kt b/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleValidator.kt index 4b79cd1b..a9387348 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleValidator.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleValidator.kt @@ -1,5 +1,6 @@ package com.sangdol.roomescape.schedule.business +import com.sangdol.common.utils.KoreaDateTime import com.sangdol.roomescape.schedule.exception.ScheduleErrorCode import com.sangdol.roomescape.schedule.exception.ScheduleException import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleEntity @@ -13,6 +14,7 @@ import org.springframework.stereotype.Component import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime +import java.time.temporal.ChronoUnit private val log: KLogger = KotlinLogging.logger {} @@ -56,9 +58,10 @@ class ScheduleValidator( } private fun validateNotInPast(date: LocalDate, time: LocalTime) { - val dateTime = LocalDateTime.of(date, time) + val now = KoreaDateTime.now().truncatedTo(ChronoUnit.MINUTES) + val inputDateTime = LocalDateTime.of(date, time).truncatedTo(ChronoUnit.MINUTES) - if (dateTime.isBefore(LocalDateTime.now())) { + if (inputDateTime.isBefore(now)) { log.info { "[ScheduleValidator.validateDateTime] 이전 시간 선택으로 인한 실패: date=${date} / time=${time}" } diff --git a/service/src/main/kotlin/com/sangdol/roomescape/schedule/docs/ScheduleAPI.kt b/service/src/main/kotlin/com/sangdol/roomescape/schedule/docs/ScheduleAPI.kt index 9e2c4ea7..dfbd397e 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/schedule/docs/ScheduleAPI.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/schedule/docs/ScheduleAPI.kt @@ -1,12 +1,12 @@ package com.sangdol.roomescape.schedule.docs -import com.sangdol.roomescape.common.types.AuditingInfo import com.sangdol.common.types.web.CommonApiResponse import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType import com.sangdol.roomescape.admin.infrastructure.persistence.Privilege import com.sangdol.roomescape.auth.web.support.AdminOnly import com.sangdol.roomescape.auth.web.support.Public import com.sangdol.roomescape.auth.web.support.UserOnly +import com.sangdol.roomescape.common.types.AuditingInfo import com.sangdol.roomescape.schedule.web.* import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse diff --git a/service/src/main/kotlin/com/sangdol/roomescape/schedule/exception/ScheduleErrorCode.kt b/service/src/main/kotlin/com/sangdol/roomescape/schedule/exception/ScheduleErrorCode.kt index 0194f2fa..fd772285 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/schedule/exception/ScheduleErrorCode.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/schedule/exception/ScheduleErrorCode.kt @@ -1,7 +1,7 @@ package com.sangdol.roomescape.schedule.exception -import com.sangdol.common.types.web.HttpStatus import com.sangdol.common.types.exception.ErrorCode +import com.sangdol.common.types.web.HttpStatus enum class ScheduleErrorCode( override val httpStatus: HttpStatus, diff --git a/service/src/main/kotlin/com/sangdol/roomescape/schedule/infrastructure/persistence/ScheduleEntity.kt b/service/src/main/kotlin/com/sangdol/roomescape/schedule/infrastructure/persistence/ScheduleEntity.kt index bd6f2a6c..770a4592 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/schedule/infrastructure/persistence/ScheduleEntity.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/schedule/infrastructure/persistence/ScheduleEntity.kt @@ -3,8 +3,8 @@ package com.sangdol.roomescape.schedule.infrastructure.persistence import com.sangdol.common.persistence.AuditingBaseEntity import jakarta.persistence.* import org.springframework.data.jpa.domain.support.AuditingEntityListener +import java.time.Instant import java.time.LocalDate -import java.time.LocalDateTime import java.time.LocalTime @Entity @@ -20,7 +20,7 @@ class ScheduleEntity( @Enumerated(value = EnumType.STRING) var status: ScheduleStatus, - var holdExpiredAt: LocalDateTime? = null + var holdExpiredAt: Instant? = null ) : AuditingBaseEntity(id) { fun modifyIfNotNull( time: LocalTime?, diff --git a/service/src/main/kotlin/com/sangdol/roomescape/schedule/infrastructure/persistence/ScheduleRepository.kt b/service/src/main/kotlin/com/sangdol/roomescape/schedule/infrastructure/persistence/ScheduleRepository.kt index a30fbbed..87a38a58 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/schedule/infrastructure/persistence/ScheduleRepository.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/schedule/infrastructure/persistence/ScheduleRepository.kt @@ -7,21 +7,23 @@ import org.springframework.data.jpa.repository.Lock import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param +import java.time.Instant import java.time.LocalDate -import java.time.LocalDateTime import java.time.LocalTime interface ScheduleRepository : JpaRepository { @Lock(value = LockModeType.PESSIMISTIC_WRITE) - @Query(""" + @Query( + """ SELECT s FROM ScheduleEntity s WHERE s._id = :id - """) + """ + ) fun findByIdForUpdate(id: Long): ScheduleEntity? @Query( @@ -108,7 +110,7 @@ interface ScheduleRepository : JpaRepository { s.status = :changeStatus, s.holdExpiredAt = CASE WHEN :changeStatus = com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus.HOLD - THEN CURRENT_TIMESTAMP + 5 MINUTE + THEN :expiredAt ELSE NULL END WHERE @@ -117,7 +119,12 @@ interface ScheduleRepository : JpaRepository { s.status = :currentStatus """ ) - fun changeStatus(id: Long, currentStatus: ScheduleStatus, changeStatus: ScheduleStatus): Int + fun changeStatus( + id: Long, + currentStatus: ScheduleStatus, + changeStatus: ScheduleStatus, + expiredAt: Instant = Instant.now().plusSeconds(5 * 60) + ): Int @Modifying @Query( @@ -137,5 +144,5 @@ interface ScheduleRepository : JpaRepository { ) """ ) - fun releaseExpiredHolds(@Param("now") now: LocalDateTime): Int + fun releaseExpiredHolds(@Param("now") now: Instant): Int } diff --git a/service/src/main/kotlin/com/sangdol/roomescape/schedule/web/AdminScheduleController.kt b/service/src/main/kotlin/com/sangdol/roomescape/schedule/web/AdminScheduleController.kt index 3c7b89d4..54a38e6c 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/schedule/web/AdminScheduleController.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/schedule/web/AdminScheduleController.kt @@ -1,7 +1,7 @@ package com.sangdol.roomescape.schedule.web -import com.sangdol.roomescape.common.types.AuditingInfo import com.sangdol.common.types.web.CommonApiResponse +import com.sangdol.roomescape.common.types.AuditingInfo import com.sangdol.roomescape.schedule.business.ScheduleService import com.sangdol.roomescape.schedule.docs.AdminScheduleAPI import jakarta.validation.Valid diff --git a/service/src/main/kotlin/com/sangdol/roomescape/store/business/StoreService.kt b/service/src/main/kotlin/com/sangdol/roomescape/store/business/StoreService.kt index 8dd3acc9..4271767f 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/store/business/StoreService.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/store/business/StoreService.kt @@ -1,8 +1,8 @@ package com.sangdol.roomescape.store.business import com.sangdol.common.persistence.IDGenerator -import com.sangdol.roomescape.common.types.AuditingInfo import com.sangdol.roomescape.admin.business.AdminService +import com.sangdol.roomescape.common.types.AuditingInfo import com.sangdol.roomescape.region.business.RegionService import com.sangdol.roomescape.store.exception.StoreErrorCode import com.sangdol.roomescape.store.exception.StoreException @@ -27,19 +27,19 @@ class StoreService( ) { @Transactional(readOnly = true) fun getDetail(id: Long): DetailStoreResponse { - log.info { "[StoreService.getDetail] 매장 상세 조회 시작: id=${id}" } + log.info { "[getDetail] 매장 상세 조회 시작: id=${id}" } val store: StoreEntity = findOrThrow(id) val region = regionService.findRegionInfo(store.regionCode) val audit = getAuditInfo(store) return store.toDetailResponse(region, audit) - .also { log.info { "[StoreService.getDetail] 매장 상세 조회 완료: id=${id}" } } + .also { log.info { "[getDetail] 매장 상세 조회 완료: id=${id}" } } } @Transactional fun register(request: StoreRegisterRequest): StoreRegisterResponse { - log.info { "[StoreService.register] 매장 등록 시작: name=${request.name}" } + log.info { "[register] 매장 등록 시작: name=${request.name}" } storeValidator.validateCanRegister(request) @@ -56,37 +56,37 @@ class StoreService( } return StoreRegisterResponse(store.id).also { - log.info { "[StoreService.register] 매장 등록 완료: id=${store.id}, name=${request.name}" } + log.info { "[register] 매장 등록 완료: id=${store.id}, name=${request.name}" } } } @Transactional fun update(id: Long, request: StoreUpdateRequest) { - log.info { "[StoreService.update] 매장 수정 시작: id=${id}, request=${request}" } + log.info { "[update] 매장 수정 시작: id=${id}, request=${request}" } storeValidator.validateCanUpdate(request) findOrThrow(id).apply { this.modifyIfNotNull(request.name, request.address, request.contact) }.also { - log.info { "[StoreService.update] 매장 수정 완료: id=${id}" } + log.info { "[update] 매장 수정 완료: id=${id}" } } } @Transactional fun disableById(id: Long) { - log.info { "[StoreService.inactive] 매장 비활성화 시작: id=${id}" } + log.info { "[inactive] 매장 비활성화 시작: id=${id}" } findOrThrow(id).apply { this.disable() }.also { - log.info { "[StoreService.inactive] 매장 비활성화 완료: id=${id}" } + log.info { "[inactive] 매장 비활성화 완료: id=${id}" } } } @Transactional(readOnly = true) fun getAllActiveStores(sidoCode: String?, sigunguCode: String?): SimpleStoreListResponse { - log.info { "[StoreService.getAllActiveStores] 전체 매장 조회 시작" } + log.info { "[getAllActiveStores] 전체 매장 조회 시작" } val regionCode: String? = when { sidoCode == null && sigunguCode != null -> throw StoreException(StoreErrorCode.SIDO_CODE_REQUIRED) @@ -95,21 +95,21 @@ class StoreService( } return storeRepository.findAllActiveStoresByRegion(regionCode).toSimpleListResponse() - .also { log.info { "[StoreService.getAllActiveStores] 전체 매장 조회 완료: total=${it.stores.size}" } } + .also { log.info { "[getAllActiveStores] 전체 매장 조회 완료: total=${it.stores.size}" } } } @Transactional(readOnly = true) fun findStoreInfo(id: Long): StoreInfoResponse { - log.info { "[StoreService.findStoreInfo] 매장 정보 조회 시작: id=${id}" } + log.info { "[findStoreInfo] 매장 정보 조회 시작: id=${id}" } val store: StoreEntity = findOrThrow(id) return store.toInfoResponse() - .also { log.info { "[StoreService.findStoreInfo] 매장 정보 조회 완료: id=${id}" } } + .also { log.info { "[findStoreInfo] 매장 정보 조회 완료: id=${id}" } } } private fun getAuditInfo(store: StoreEntity): AuditingInfo { - log.info { "[StoreService.getAuditInfo] 감사 정보 조회 시작: storeId=${store.id}" } + log.info { "[getAuditInfo] 감사 정보 조회 시작: storeId=${store.id}" } val createdBy = adminService.findOperatorOrUnknown(store.createdBy) val updatedBy = adminService.findOperatorOrUnknown(store.updatedBy) @@ -119,19 +119,19 @@ class StoreService( updatedAt = store.updatedAt, updatedBy = updatedBy ).also { - log.info { "[StoreService.getAuditInfo] 감사 정보 조회 완료: storeId=${store.id}" } + log.info { "[getAuditInfo] 감사 정보 조회 완료: storeId=${store.id}" } } } private fun findOrThrow(id: Long): StoreEntity { - log.info { "[StoreService.findOrThrow] 매장 조회 시작: id=${id}" } + log.info { "[findOrThrow] 매장 조회 시작: id=${id}" } return storeRepository.findActiveStoreById(id) ?.also { - log.info { "[StoreService.findOrThrow] 매장 조회 완료: id=${id}" } + log.info { "[findOrThrow] 매장 조회 완료: id=${id}" } } ?: run { - log.warn { "[StoreService.findOrThrow] 매장 조회 실패: id=${id}" } + log.warn { "[findOrThrow] 매장 조회 실패: id=${id}" } throw StoreException(StoreErrorCode.STORE_NOT_FOUND) } } diff --git a/service/src/main/kotlin/com/sangdol/roomescape/store/business/StoreValidator.kt b/service/src/main/kotlin/com/sangdol/roomescape/store/business/StoreValidator.kt index cd1f3f69..65d5708e 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/store/business/StoreValidator.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/store/business/StoreValidator.kt @@ -1,13 +1,13 @@ package com.sangdol.roomescape.store.business -import io.github.oshai.kotlinlogging.KLogger -import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.stereotype.Component import com.sangdol.roomescape.store.exception.StoreErrorCode import com.sangdol.roomescape.store.exception.StoreException import com.sangdol.roomescape.store.infrastructure.persistence.StoreRepository import com.sangdol.roomescape.store.web.StoreRegisterRequest import com.sangdol.roomescape.store.web.StoreUpdateRequest +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Component private val log: KLogger = KotlinLogging.logger {} diff --git a/service/src/main/kotlin/com/sangdol/roomescape/store/exception/StoreException.kt b/service/src/main/kotlin/com/sangdol/roomescape/store/exception/StoreException.kt index fad3d383..5e55b3b1 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/store/exception/StoreException.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/store/exception/StoreException.kt @@ -1,8 +1,8 @@ package com.sangdol.roomescape.store.exception -import com.sangdol.common.types.web.HttpStatus import com.sangdol.common.types.exception.ErrorCode import com.sangdol.common.types.exception.RoomescapeException +import com.sangdol.common.types.web.HttpStatus class StoreException( override val errorCode: StoreErrorCode, diff --git a/service/src/main/kotlin/com/sangdol/roomescape/theme/business/ThemeService.kt b/service/src/main/kotlin/com/sangdol/roomescape/theme/business/ThemeService.kt index 5cf3b4ea..78788a17 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/theme/business/ThemeService.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/theme/business/ThemeService.kt @@ -1,8 +1,9 @@ package com.sangdol.roomescape.theme.business import com.sangdol.common.persistence.IDGenerator -import com.sangdol.roomescape.common.types.AuditingInfo +import com.sangdol.common.utils.KoreaDate import com.sangdol.roomescape.admin.business.AdminService +import com.sangdol.roomescape.common.types.AuditingInfo import com.sangdol.roomescape.theme.exception.ThemeErrorCode import com.sangdol.roomescape.theme.exception.ThemeException import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity @@ -13,7 +14,6 @@ import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import java.time.LocalDate private val log: KLogger = KotlinLogging.logger {} @@ -36,23 +36,23 @@ class ThemeService( // ======================================== @Transactional(readOnly = true) fun findInfoById(id: Long): ThemeInfoResponse { - log.info { "[ThemeService.findById] 테마 조회 시작: id=$id" } + log.info { "[findInfoById] 테마 조회 시작: id=$id" } return findOrThrow(id).toInfoResponse() - .also { log.info { "[ThemeService.findById] 테마 조회 완료: id=$id" } } + .also { log.info { "[findInfoById] 테마 조회 완료: id=$id" } } } @Transactional(readOnly = true) fun findMostReservedThemeLastWeek(count: Int): ThemeInfoListResponse { - log.info { "[ThemeService.findMostReservedThemeLastWeek] 인기 테마 조회 시작: count=$count" } + log.info { "[findMostReservedThemeLastWeek] 인기 테마 조회 시작: count=$count" } - val previousWeekSunday = DateUtils.getSundayOfPreviousWeek(LocalDate.now()) + val previousWeekSunday = DateUtils.getSundayOfPreviousWeek(KoreaDate.today()) val previousWeekSaturday = previousWeekSunday.plusDays(6) return themeRepository.findMostReservedThemeByDateAndCount(previousWeekSunday, previousWeekSaturday, count) .toListResponse() .also { - log.info { "[ThemeService.findMostReservedThemeLastWeek] ${it.themes.size} / $count 개의 인기 테마 조회 완료" } + log.info { "[findMostReservedThemeLastWeek] ${it.themes.size} / $count 개의 인기 테마 조회 완료" } } } @@ -62,16 +62,16 @@ class ThemeService( // ======================================== @Transactional(readOnly = true) fun findAdminThemes(): AdminThemeSummaryListResponse { - log.info { "[ThemeService.findAdminThemes] 관리자 페이지에서의 테마 목록 조회 시작" } + log.info { "[findAdminThemes] 관리자 페이지에서의 테마 목록 조회 시작" } return themeRepository.findAll() .toAdminThemeSummaryListResponse() - .also { log.info { "[ThemeService.findAdminThemes] ${it.themes.size}개 테마 조회 완료" } } + .also { log.info { "[findAdminThemes] ${it.themes.size}개 테마 조회 완료" } } } @Transactional(readOnly = true) fun findAdminThemeDetail(id: Long): AdminThemeDetailResponse { - log.info { "[ThemeService.findAdminThemeDetail] 관리자 페이지에서의 테마 상세 정보 조회 시작: id=${id}" } + log.info { "[findAdminThemeDetail] 관리자 페이지에서의 테마 상세 정보 조회 시작: id=${id}" } val theme: ThemeEntity = findOrThrow(id) @@ -80,12 +80,12 @@ class ThemeService( val audit = AuditingInfo(theme.createdAt, createdBy, theme.updatedAt, updatedBy) return theme.toAdminThemeDetailResponse(audit) - .also { log.info { "[ThemeService.findAdminThemeDetail] 테마 상세 조회 완료: id=$id, name=${theme.name}" } } + .also { log.info { "[findAdminThemeDetail] 테마 상세 조회 완료: id=$id, name=${theme.name}" } } } @Transactional fun createTheme(request: ThemeCreateRequest): ThemeCreateResponse { - log.info { "[ThemeService.createTheme] 테마 생성 시작: name=${request.name}" } + log.info { "[createTheme] 테마 생성 시작: name=${request.name}" } themeValidator.validateCanCreate(request) @@ -93,27 +93,27 @@ class ThemeService( .also { themeRepository.save(it) } return ThemeCreateResponse(theme.id).also { - log.info { "[ThemeService.createTheme] 테마 생성 완료: id=${theme.id}, name=${theme.name}" } + log.info { "[createTheme] 테마 생성 완료: id=${theme.id}, name=${theme.name}" } } } @Transactional fun deleteTheme(id: Long) { - log.info { "[ThemeService.deleteTheme] 테마 삭제 시작: id=${id}" } + log.info { "[deleteTheme] 테마 삭제 시작: id=${id}" } val theme: ThemeEntity = findOrThrow(id) themeRepository.delete(theme).also { - log.info { "[ThemeService.deleteTheme] 테마 삭제 완료: id=$id, name=${theme.name}" } + log.info { "[deleteTheme] 테마 삭제 완료: id=$id, name=${theme.name}" } } } @Transactional fun updateTheme(id: Long, request: ThemeUpdateRequest) { - log.info { "[ThemeService.updateTheme] 테마 수정 시작: id=${id}, request=${request}" } + log.info { "[updateTheme] 테마 수정 시작: id=${id}, request=${request}" } if (request.isAllParamsNull()) { - log.info { "[ThemeService.updateTheme] 테마 변경 사항 없음: id=${id}" } + log.info { "[updateTheme] 테마 변경 사항 없음: id=${id}" } return } @@ -134,7 +134,7 @@ class ThemeService( request.expectedMinutesTo, request.isActive, ).also { - log.info { "[ThemeService.updateTheme] 테마 수정 완료: id=$id, request=${request}" } + log.info { "[updateTheme] 테마 수정 완료: id=$id, request=${request}" } } } @@ -143,12 +143,12 @@ class ThemeService( // ======================================== @Transactional(readOnly = true) fun findActiveThemes(): SimpleActiveThemeListResponse { - log.info { "[ThemeService.findActiveThemes] open 상태인 모든 테마 조회 시작" } + log.info { "[findActiveThemes] open 상태인 모든 테마 조회 시작" } return themeRepository.findActiveThemes() .toSimpleActiveThemeResponse() .also { - log.info { "[ThemeService.findActiveThemes] ${it.themes.size}개 테마 조회 완료" } + log.info { "[findActiveThemes] ${it.themes.size}개 테마 조회 완료" } } } @@ -156,12 +156,12 @@ class ThemeService( // Common (공통 메서드) // ======================================== private fun findOrThrow(id: Long): ThemeEntity { - log.info { "[ThemeService.findOrThrow] 테마 조회 시작: id=$id" } + log.info { "[findOrThrow] 테마 조회 시작: id=$id" } return themeRepository.findByIdOrNull(id) - ?.also { log.info { "[ThemeService.findOrThrow] 테마 조회 완료: id=$id" } } + ?.also { log.info { "[findOrThrow] 테마 조회 완료: id=$id" } } ?: run { - log.warn { "[ThemeService.updateTheme] 테마 조회 실패: id=$id" } + log.warn { "[updateTheme] 테마 조회 실패: id=$id" } throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND) } } diff --git a/service/src/main/kotlin/com/sangdol/roomescape/theme/business/ThemeValidator.kt b/service/src/main/kotlin/com/sangdol/roomescape/theme/business/ThemeValidator.kt index 8d5b341f..e8a362f4 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/theme/business/ThemeValidator.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/theme/business/ThemeValidator.kt @@ -1,13 +1,13 @@ package com.sangdol.roomescape.theme.business -import io.github.oshai.kotlinlogging.KLogger -import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.stereotype.Component import com.sangdol.roomescape.theme.exception.ThemeErrorCode import com.sangdol.roomescape.theme.exception.ThemeException import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository import com.sangdol.roomescape.theme.web.ThemeCreateRequest import com.sangdol.roomescape.theme.web.ThemeUpdateRequest +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Component private val log: KLogger = KotlinLogging.logger {} diff --git a/service/src/main/kotlin/com/sangdol/roomescape/theme/exception/ThemeErrorCode.kt b/service/src/main/kotlin/com/sangdol/roomescape/theme/exception/ThemeErrorCode.kt index 03673549..11bd9fa4 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/theme/exception/ThemeErrorCode.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/theme/exception/ThemeErrorCode.kt @@ -1,7 +1,7 @@ package com.sangdol.roomescape.theme.exception -import com.sangdol.common.types.web.HttpStatus import com.sangdol.common.types.exception.ErrorCode +import com.sangdol.common.types.web.HttpStatus enum class ThemeErrorCode( override val httpStatus: HttpStatus, diff --git a/service/src/main/kotlin/com/sangdol/roomescape/theme/infrastructure/persistence/ThemeRepository.kt b/service/src/main/kotlin/com/sangdol/roomescape/theme/infrastructure/persistence/ThemeRepository.kt index 644f84b6..cef38dff 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/theme/infrastructure/persistence/ThemeRepository.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/theme/infrastructure/persistence/ThemeRepository.kt @@ -1,8 +1,8 @@ package com.sangdol.roomescape.theme.infrastructure.persistence +import com.sangdol.roomescape.theme.business.domain.ThemeInfo import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query -import com.sangdol.roomescape.theme.business.domain.ThemeInfo import java.time.LocalDate interface ThemeRepository : JpaRepository { diff --git a/service/src/main/kotlin/com/sangdol/roomescape/user/business/UserService.kt b/service/src/main/kotlin/com/sangdol/roomescape/user/business/UserService.kt index 7aa00234..e84a49ce 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/user/business/UserService.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/user/business/UserService.kt @@ -30,59 +30,59 @@ class UserService( ) { @Transactional(readOnly = true) fun findContextById(id: Long): CurrentUserContext { - log.info { "[UserService.findContextById] 현재 로그인된 회원 조회 시작: id=${id}" } + log.info { "[findContextById] 현재 로그인된 회원 조회 시작: id=${id}" } val user: UserEntity = findOrThrow(id) return CurrentUserContext(user.id, user.name) .also { - log.info { "[UserService.findContextById] 현재 로그인된 회원 조회 완료: id=${id}" } + log.info { "[findContextById] 현재 로그인된 회원 조회 완료: id=${id}" } } } @Transactional(readOnly = true) fun findCredentialsByAccount(email: String): UserLoginCredentials { - log.info { "[UserService.findCredentialsByAccount] 회원 조회 시작: email=${email}" } + log.info { "[findCredentialsByAccount] 회원 조회 시작: email=${email}" } return userRepository.findByEmail(email) ?.let { - log.info { "[UserService.findCredentialsByAccount] 회원 조회 완료: id=${it.id}" } + log.info { "[findCredentialsByAccount] 회원 조회 완료: id=${it.id}" } it.toCredentials() } ?: run { - log.info { "[UserService.findCredentialsByAccount] 회원 조회 실패" } + log.info { "[findCredentialsByAccount] 회원 조회 실패" } throw UserException(UserErrorCode.USER_NOT_FOUND) } } @Transactional(readOnly = true) - fun findContactById(id: Long) : UserContactResponse { - log.info { "[UserService.findContactById] 회원 연락 정보 조회 시작: id=${id}" } + fun findContactById(id: Long): UserContactResponse { + log.info { "[findContactById] 회원 연락 정보 조회 시작: id=${id}" } val user = findOrThrow(id) return UserContactResponse(user.id, user.name, user.phone) .also { - log.info { "[UserService.findContactById] 회원 연락 정보 조회 완료: id=${id}, name=${it.name}" } + log.info { "[findContactById] 회원 연락 정보 조회 완료: id=${id}, name=${it.name}" } } } @Transactional fun signup(request: UserCreateRequest): UserCreateResponse { - log.info { "[UserService.signup] 회원가입 시작: request:$request" } + log.info { "[signup] 회원가입 시작: request:$request" } userValidator.validateCanSignup(request.email, request.phone) val user: UserEntity = userRepository.save( request.toEntity(id = idGenerator.create(), status = UserStatus.ACTIVE) ).also { - log.info { "[UserService.signup] 회원 저장 완료: id:${it.id}" } + log.info { "[signup] 회원 저장 완료: id:${it.id}" } }.also { createHistory(user = it, reason = SIGNUP) } return UserCreateResponse(user.id, user.name) .also { - log.info { "[UserService.signup] 회원가입 완료: id:${it.id}" } + log.info { "[signup] 회원가입 완료: id:${it.id}" } } } @@ -95,7 +95,7 @@ class UserService( return userStatusHistoryRepository.save( UserStatusHistoryEntity(id = idGenerator.create(), userId = user.id, reason = reason, status = user.status) ).also { - log.info { "[UserService.signup] 회원 상태 이력 저장 완료: userStatusHistoryId:${it.id}" } + log.info { "[signup] 회원 상태 이력 저장 완료: userStatusHistoryId:${it.id}" } } } } diff --git a/service/src/main/kotlin/com/sangdol/roomescape/user/business/UserValidator.kt b/service/src/main/kotlin/com/sangdol/roomescape/user/business/UserValidator.kt index 400ff17d..ff7a4ec6 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/user/business/UserValidator.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/user/business/UserValidator.kt @@ -1,11 +1,11 @@ package com.sangdol.roomescape.user.business -import io.github.oshai.kotlinlogging.KLogger -import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.stereotype.Component import com.sangdol.roomescape.user.exception.UserErrorCode import com.sangdol.roomescape.user.exception.UserException import com.sangdol.roomescape.user.infrastructure.persistence.UserRepository +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Component private val log: KLogger = KotlinLogging.logger {} diff --git a/service/src/main/kotlin/com/sangdol/roomescape/user/exception/UserException.kt b/service/src/main/kotlin/com/sangdol/roomescape/user/exception/UserException.kt index 05f4963a..c1d0c589 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/user/exception/UserException.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/user/exception/UserException.kt @@ -1,8 +1,8 @@ package com.sangdol.roomescape.user.exception -import com.sangdol.common.types.web.HttpStatus import com.sangdol.common.types.exception.ErrorCode import com.sangdol.common.types.exception.RoomescapeException +import com.sangdol.common.types.web.HttpStatus class UserException( override val errorCode: UserErrorCode, diff --git a/service/src/main/kotlin/com/sangdol/roomescape/user/infrastructure/persistence/UserEntities.kt b/service/src/main/kotlin/com/sangdol/roomescape/user/infrastructure/persistence/UserEntities.kt index 89d3c1fc..291a4402 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/user/infrastructure/persistence/UserEntities.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/user/infrastructure/persistence/UserEntities.kt @@ -22,7 +22,7 @@ class UserEntity( @Enumerated(value = EnumType.STRING) var status: UserStatus -): AuditingBaseEntity(id) +) : AuditingBaseEntity(id) @Entity @Table(name = "user_status_history") diff --git a/service/src/main/kotlin/com/sangdol/roomescape/user/web/UserDTO.kt b/service/src/main/kotlin/com/sangdol/roomescape/user/web/UserDTO.kt index 4e300475..d77e3ba0 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/user/web/UserDTO.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/user/web/UserDTO.kt @@ -1,11 +1,11 @@ package com.sangdol.roomescape.user.web +import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity +import com.sangdol.roomescape.user.infrastructure.persistence.UserStatus import jakarta.validation.constraints.Email import jakarta.validation.constraints.NotEmpty import jakarta.validation.constraints.Pattern import jakarta.validation.constraints.Size -import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity -import com.sangdol.roomescape.user.infrastructure.persistence.UserStatus const val MIN_PASSWORD_LENGTH = 8 diff --git a/service/src/main/resources/application-local.yaml b/service/src/main/resources/application-local.yaml index 095736d2..101feddf 100644 --- a/service/src/main/resources/application-local.yaml +++ b/service/src/main/resources/application-local.yaml @@ -7,7 +7,7 @@ spring: ddl-auto: validate datasource: hikari: - jdbc-url: jdbc:mysql://localhost:23306/roomescape_local + jdbc-url: jdbc:mysql://localhost:23306/roomescape_local?useLegacyDatetimeCode=false&serverTimezone=UTC driver-class-name: com.mysql.cj.jdbc.Driver username: root password: init @@ -42,4 +42,14 @@ jdbc: management: tracing: sampling: - probability: 1 + probability: 1.0 + otlp: + tracing: + transport: http + endpoint: http://localhost:4318/v1/traces + +slow-query: + logger-name: local-slow-query-logger + log-level: info + threshold-ms: 5 + diff --git a/service/src/main/resources/application.yaml b/service/src/main/resources/application.yaml index 46a9706c..673b5538 100644 --- a/service/src/main/resources/application.yaml +++ b/service/src/main/resources/application.yaml @@ -5,6 +5,8 @@ server: forward-headers-strategy: framework spring: + application: + name: roomescape-backend profiles: active: ${ACTIVE_PROFILE:local} jpa: @@ -21,7 +23,7 @@ management: show-details: always payment: - api-base-url: https://api.tosspayments.com + api-base-url: ${PAYMENT_SERVER_ENDPOINT:/https://api.tosspayments.com} springdoc: swagger-ui: diff --git a/service/src/main/resources/logback-deploy.xml b/service/src/main/resources/logback-deploy.xml index ba0cf7f9..93faa529 100644 --- a/service/src/main/resources/logback-deploy.xml +++ b/service/src/main/resources/logback-deploy.xml @@ -6,20 +6,6 @@ - - timestamp - UTC - - - level - - - logger - - - thread - - { @@ -27,6 +13,10 @@ } + + logger + + stack_trace @@ -35,6 +25,16 @@ true + + level + + + thread + + + timestamp + UTC + diff --git a/service/src/test/kotlin/com/sangdol/data/DefaultDataInitializer.kt b/service/src/test/kotlin/com/sangdol/data/DefaultDataInitializer.kt index aa1ac10e..6ba28855 100644 --- a/service/src/test/kotlin/com/sangdol/data/DefaultDataInitializer.kt +++ b/service/src/test/kotlin/com/sangdol/data/DefaultDataInitializer.kt @@ -2,35 +2,36 @@ package com.sangdol.data import com.sangdol.common.persistence.IDGenerator import com.sangdol.common.persistence.TransactionExecutionUtil +import com.sangdol.common.utils.KoreaDateTime import com.sangdol.roomescape.admin.infrastructure.persistence.AdminEntity import com.sangdol.roomescape.admin.infrastructure.persistence.AdminPermissionLevel import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType import com.sangdol.roomescape.payment.infrastructure.common.* import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus +import com.sangdol.roomescape.store.infrastructure.persistence.StoreEntity import com.sangdol.roomescape.supports.AdminFixture import com.sangdol.roomescape.supports.FunSpecSpringbootTest import com.sangdol.roomescape.supports.randomPhoneNumber import com.sangdol.roomescape.supports.randomString import com.sangdol.roomescape.theme.infrastructure.persistence.Difficulty +import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity import com.sangdol.roomescape.user.business.SIGNUP import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity import com.sangdol.roomescape.user.infrastructure.persistence.UserStatus import com.sangdol.roomescape.user.web.UserContactResponse import io.kotest.core.test.TestCaseOrder import jakarta.persistence.EntityManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.joinAll -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import kotlinx.coroutines.sync.Semaphore import org.springframework.beans.factory.annotation.Autowired import org.springframework.jdbc.core.JdbcTemplate import org.springframework.test.context.ActiveProfiles import java.sql.Timestamp +import java.time.Instant import java.time.LocalDateTime import java.time.LocalTime -import java.time.OffsetDateTime +import java.time.ZoneId @ActiveProfiles("test", "data") abstract class AbstractDataInitializer( @@ -151,22 +152,22 @@ class DefaultDataInitializer : AbstractDataInitializer() { AdminPermissionLevel.READ_SUMMARY to 3 ) - val storeIds: List = transactionExecutionUtil.withNewTransaction(isReadOnly = true) { + val stores: List = transactionExecutionUtil.withNewTransaction(isReadOnly = true) { entityManager.createQuery( - "SELECT s.id FROM StoreEntity s", - Long::class.java + "SELECT s FROM StoreEntity s", + StoreEntity::class.java ).resultList - }!!.map { it as Long } + }!! transactionExecutionUtil.withNewTransaction(isReadOnly = false) { - storeIds.forEach { storeId -> + stores.forEach { store -> // StoreManager 1명 생성 val storeManager = AdminFixture.create( - account = "$storeId", + account = store.name, name = randomKoreanName(), phone = randomPhoneNumber(), type = AdminType.STORE, - storeId = storeId, + storeId = store.id, permissionLevel = AdminPermissionLevel.FULL_ACCESS ).apply { this.createdBy = superHQAdmin.id @@ -178,11 +179,11 @@ class DefaultDataInitializer : AbstractDataInitializer() { storeAdminCountsByPermissionLevel.forEach { (permissionLevel, count) -> repeat(count) { index -> AdminFixture.create( - account = randomString(), + account = "${store.name}-${permissionLevel.ordinal}${index}", name = randomKoreanName(), phone = randomPhoneNumber(), type = AdminType.STORE, - storeId = storeId, + storeId = store.id, permissionLevel = permissionLevel ).apply { this.createdBy = storeManager.id @@ -217,7 +218,7 @@ class DefaultDataInitializer : AbstractDataInitializer() { val batchArgs = mutableListOf>() repeat(500) { i -> - val randomDay = if (i <= 9) (1..30).random() else (1..365 * 2).random() + val randomDay = if (i <= 9) (7..30).random() else (30..365 * 2).random() val randomCreatedAt: LocalDateTime = LocalDateTime.now().minusDays(randomDay.toLong()) val randomThemeName = (1..7).random().let { repeat -> (1..repeat).joinToString("") { randomKoreanName() } } @@ -323,7 +324,7 @@ class UserDataInitializer : AbstractDataInitializer() { } while (true) user.phone = newPhone - user.updatedAt = LocalDateTime.now() + user.updatedAt = Instant.now() entityManager.merge(user) } } @@ -417,25 +418,49 @@ class UserDataInitializer : AbstractDataInitializer() { class ScheduleDataInitializer : AbstractDataInitializer() { init { context("일정 초기 데이터 생성") { - test("테마 생성일 기준으로 다음 3일차, 매일 5개의 일정을 모든 매장에 생성") { + test("테마 생성일 기준으로 다음 3일차, 매일 최대 10개의 일정을 모든 매장에 생성") { val stores: List> = getStoreWithManagers() - val themes: List> = getThemes() - val maxAvailableMinutes = themes.maxOf { it.second.toInt() } - val scheduleCountPerDay = 5 - + val themes: List = getThemes() + val maxScheduleCountPerDay = 10 val startTime = LocalTime.of(10, 0) - var lastTime = startTime - val times = mutableListOf() - repeat(scheduleCountPerDay) { - times.add(lastTime) - lastTime = lastTime.plusMinutes(maxAvailableMinutes.toLong() + 10L) + val themeWithTimes: Map> = themes.associateWith { theme -> + val times = mutableListOf() + val themeAvailableMinutes = theme.availableMinutes + var lastTime = startTime + + while (times.size <= maxScheduleCountPerDay && lastTime.hour in (10..23)) { + times.add(lastTime) + lastTime = lastTime.plusMinutes(themeAvailableMinutes + 10L) + } + + times } coroutineScope { - themes.forEach { theme -> + stores.map { store -> launch(Dispatchers.IO) { - processTheme(theme, stores, times) + processTheme(store, themeWithTimes) + } + } + } + } + + test("내일 ~ 일주일 뒤 까지의 일정 생성") { +// val stores: List> = getStoreWithManagers() +// val availableThemes: List = transactionExecutionUtil.withNewTransaction(isReadOnly = true) { +// entityManager.createQuery( +// "SELECT t FROM ThemeEntity t WHERE t.isActive = true AND t.createdAt >", ThemeEntity::class.java +// ).resultList +// }!!.take(10) + + coroutineScope { + val jobs = (1..100).map { i -> + launch(Dispatchers.IO) { + val threadName = Thread.currentThread().name + println("[$i] 시작: $threadName") + delay(1) + println("[$i] 완료: $threadName") } } } @@ -444,9 +469,8 @@ class ScheduleDataInitializer : AbstractDataInitializer() { } private suspend fun processTheme( - theme: Triple, - stores: List>, - times: List + store: Pair, + themeWithTimes: Map> ) { val sql = """ INSERT INTO schedule ( @@ -457,24 +481,33 @@ class ScheduleDataInitializer : AbstractDataInitializer() { val batchArgs = mutableListOf>() - val now = LocalDateTime.now() - stores.forEach { (storeId, adminId) -> - (1..3).forEach { dayOffset -> - val date = theme.third.toLocalDate().plusDays(dayOffset.toLong()) + val status = ScheduleStatus.RESERVED.name + themeWithTimes.forEach { (theme, times) -> + val themeCreatedAt = theme.createdAt + (1..3).forEach { + val themeCreatedDateTime = themeCreatedAt.atZone(ZoneId.systemDefault()) + val themeCreatedDate = themeCreatedDateTime.toLocalDate().plusDays(it.toLong()) + val themeCreatedTime = themeCreatedDateTime.toLocalTime() times.forEach { time -> - val scheduledAt = LocalDateTime.of(date, time) - val status = - if (scheduledAt.isAfter(now)) ScheduleStatus.AVAILABLE.name else ScheduleStatus.RESERVED.name - + val storeId = store.first + val storeAdminId = store.second batchArgs.add( arrayOf( - idGenerator.create(), storeId, theme.first, date, time, - status, adminId, adminId, Timestamp.valueOf(now), Timestamp.valueOf(now) + idGenerator.create(), + storeId, + theme.id, + themeCreatedDate, + time, + status, + storeAdminId, + storeAdminId, + themeCreatedTime.plusHours(1), + themeCreatedTime.plusHours(1) ) ) - if (batchArgs.size >= 500) { + if (batchArgs.size >= 300) { executeBatch(sql, batchArgs).also { batchArgs.clear() } } } @@ -500,17 +533,13 @@ class ScheduleDataInitializer : AbstractDataInitializer() { } } - private fun getThemes(): List> { + private fun getThemes(): List { return transactionExecutionUtil.withNewTransaction(isReadOnly = true) { entityManager.createQuery( - "SELECT t._id, t.availableMinutes, t.createdAt FROM ThemeEntity t", - List::class.java - ) - .resultList - }!!.map { - val array = it as List<*> - Triple(array[0] as Long, array[1] as Short, array[2] as LocalDateTime) - } + "SELECT t FROM ThemeEntity t", + ThemeEntity::class.java + ).resultList + }!! } } @@ -528,10 +557,10 @@ class ReservationDataInitializer : AbstractDataInitializer() { init { context("예약 초기 데이터 생성") { test("${ScheduleStatus.RESERVED}인 모든 일정에 예약을 1개씩 배정한다.") { - val chunkSize = 10_000 + val chunkSize = 500 val chunkedSchedules: List> = entityManager.createQuery( - "SELECT new com.sangdol.data.ScheduleWithThemeParticipants(s._id, t.minParticipants, t.maxParticipants) FROM ScheduleEntity s JOIN ThemeEntity t ON s.themeId = t.id WHERE s.status = :status", + "SELECT new com.sangdol.roomescape.data.ScheduleWithThemeParticipants(s._id, t.minParticipants, t.maxParticipants) FROM ScheduleEntity s JOIN ThemeEntity t ON s.themeId = t.id WHERE s.status = :status", ScheduleWithThemeParticipants::class.java ).setParameter("status", ScheduleStatus.RESERVED).resultList.chunked(chunkSize) @@ -587,10 +616,6 @@ class ReservationDataInitializer : AbstractDataInitializer() { user.id, ) ) - - if (batchArgs.size >= 1_000) { - executeBatch(sql, batchArgs).also { batchArgs.clear() } - } } if (batchArgs.isNotEmpty()) executeBatch(sql, batchArgs).also { batchArgs.clear() } @@ -613,8 +638,9 @@ data class PaymentWithMethods( class PaymentDataInitializer : AbstractDataInitializer() { companion object { - val requestedAtCache: Timestamp = Timestamp.valueOf(OffsetDateTime.now().toLocalDateTime()) - val approvedAtCache: Timestamp = Timestamp.valueOf(OffsetDateTime.now().plusSeconds(5).toLocalDateTime()) + val requestedAtCache: Timestamp = Timestamp.valueOf(KoreaDateTime.nowWithOffset().toLocalDateTime()) + val approvedAtCache: Timestamp = + Timestamp.valueOf(KoreaDateTime.nowWithOffset().plusSeconds(5).toLocalDateTime()) val supportedPaymentMethods = listOf(PaymentMethod.TRANSFER, PaymentMethod.EASY_PAY, PaymentMethod.CARD) val supportedCardType = listOf(CardType.CREDIT, CardType.CHECK) @@ -671,7 +697,7 @@ class PaymentDataInitializer : AbstractDataInitializer() { } coroutineScope { - allReservations.chunked(10_000).forEach { reservations -> + allReservations.chunked(500).forEach { reservations -> launch(Dispatchers.IO) { processPaymentAndDefaultDetail(reservations) } @@ -681,12 +707,12 @@ class PaymentDataInitializer : AbstractDataInitializer() { test("기존 결제 데이터에 상세 정보(계좌이체, 카드, 간편결제) 데이터를 생성한다.") { val allPayments: List = entityManager.createQuery( - "SELECT new com.sangdol.data.PaymentWithMethods(pd._id, p.totalAmount, p.method) FROM PaymentEntity p JOIN PaymentDetailEntity pd ON p._id = pd.paymentId", + "SELECT new com.sangdol.roomescape.data.PaymentWithMethods(pd._id, p.totalAmount, p.method) FROM PaymentEntity p JOIN PaymentDetailEntity pd ON p._id = pd.paymentId", PaymentWithMethods::class.java ).resultList coroutineScope { - allPayments.chunked(10_000).forEach { payments -> + allPayments.chunked(500).forEach { payments -> launch(Dispatchers.IO) { processPaymentDetail(payments) } @@ -731,9 +757,6 @@ class PaymentDataInitializer : AbstractDataInitializer() { approvedAtCache, ) ) - if (paymentBatchArgs.size >= 1_000) { - executeBatch(paymentSql, paymentBatchArgs).also { paymentBatchArgs.clear() } - } val suppliedAmount: Int = (totalPrice * 0.9).toInt() val vat: Int = (totalPrice - suppliedAmount) @@ -746,10 +769,6 @@ class PaymentDataInitializer : AbstractDataInitializer() { vat ) ) - - if (detailBatchArgs.size >= 1_000) { - executeBatch(paymentDetailSql, detailBatchArgs).also { detailBatchArgs.clear() } - } } if (paymentBatchArgs.isNotEmpty()) { @@ -780,9 +799,6 @@ class PaymentDataInitializer : AbstractDataInitializer() { settlementStatus ) ) - if (transferBatchArgs.size >= 1_000) { - executeBatch(paymentBankTransferDetailSql, transferBatchArgs).also { transferBatchArgs.clear() } - } } PaymentMethod.EASY_PAY -> { @@ -803,10 +819,6 @@ class PaymentDataInitializer : AbstractDataInitializer() { ) ) - if (cardBatchArgs.size >= 1_000) { - executeBatch(paymentCardDetailSql, cardBatchArgs).also { cardBatchArgs.clear() } - } - } else { easypayPrepaidBatchArgs.add( arrayOf( @@ -816,10 +828,6 @@ class PaymentDataInitializer : AbstractDataInitializer() { randomDiscountAmount, ) ) - - if (easypayPrepaidBatchArgs.size >= 1_000) { - executeBatch(paymentEasypayPrepaidDetailSql, easypayPrepaidBatchArgs).also { easypayPrepaidBatchArgs.clear() } - } } } @@ -839,10 +847,6 @@ class PaymentDataInitializer : AbstractDataInitializer() { 0, ) ) - - if (cardBatchArgs.size >= 1_000) { - executeBatch(paymentCardDetailSql, cardBatchArgs).also { cardBatchArgs.clear() } - } } else -> return@forEach @@ -855,7 +859,10 @@ class PaymentDataInitializer : AbstractDataInitializer() { executeBatch(paymentCardDetailSql, cardBatchArgs).also { cardBatchArgs.clear() } } if (easypayPrepaidBatchArgs.isNotEmpty()) { - executeBatch(paymentEasypayPrepaidDetailSql, easypayPrepaidBatchArgs).also { easypayPrepaidBatchArgs.clear() } + executeBatch( + paymentEasypayPrepaidDetailSql, + easypayPrepaidBatchArgs + ).also { easypayPrepaidBatchArgs.clear() } } } diff --git a/service/src/test/kotlin/com/sangdol/data/PopulationDataParser.kt b/service/src/test/kotlin/com/sangdol/data/PopulationDataParser.kt index 516490ca..784a518e 100644 --- a/service/src/test/kotlin/com/sangdol/data/PopulationDataParser.kt +++ b/service/src/test/kotlin/com/sangdol/data/PopulationDataParser.kt @@ -129,7 +129,12 @@ class StoreDataInitializer { val randomPositiveWord = positiveWords.random() storeName = "${parseSigunguName(region.sigunguName)}${randomPositiveWord}점" address = - "${region.sidoName} ${region.sigunguName} ${randomPositiveWord}${Random.nextInt(1, 10)}길 ${Random.nextInt(1, 100)}" + "${region.sidoName} ${region.sigunguName} ${randomPositiveWord}${ + Random.nextInt( + 1, + 10 + ) + }길 ${Random.nextInt(1, 100)}" } while (usedStoreName.contains(storeName)) usedStoreName.add(storeName) @@ -158,13 +163,17 @@ class StoreDataInitializer { } File("$BASE_DIR/store_data.txt").also { - if (it.exists()) { it.delete() } + if (it.exists()) { + it.delete() + } }.writeText( storeDataRows.joinToString("\n") ) return File("$BASE_DIR/store_data.sql").also { - if (it.exists()) { it.delete() } + if (it.exists()) { + it.delete() + } StringBuilder("INSERT INTO store (id, name, address, contact, business_reg_num, region_code, status, created_at, created_by, updated_at, updated_by) VALUES ") .append(storeSqlRows.joinToString(",\n")) @@ -195,8 +204,7 @@ private fun randomLocalDateTime(): String { return LocalDateTime.of(year, month, day, hour, minute, second) .atZone(ZoneId.systemDefault()) - .toOffsetDateTime() - .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX")) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS")) } private fun generateBusinessRegNum(): String { diff --git a/service/src/test/kotlin/com/sangdol/roomescape/auth/AuthApiTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/auth/AuthApiTest.kt index c69695f1..913bdf30 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/auth/AuthApiTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/auth/AuthApiTest.kt @@ -1,13 +1,6 @@ package com.sangdol.roomescape.auth import com.ninjasquad.springmockk.SpykBean -import io.kotest.assertions.assertSoftly -import io.kotest.matchers.collections.shouldHaveSize -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.mockk.every -import io.restassured.response.ValidatableResponse -import org.hamcrest.CoreMatchers.equalTo import com.sangdol.common.types.web.HttpStatus import com.sangdol.roomescape.admin.exception.AdminErrorCode import com.sangdol.roomescape.auth.business.CLAIM_ADMIN_TYPE_KEY @@ -24,6 +17,13 @@ import com.sangdol.roomescape.supports.UserFixture import com.sangdol.roomescape.supports.runTest import com.sangdol.roomescape.user.exception.UserErrorCode import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.mockk.every +import io.restassured.response.ValidatableResponse +import org.hamcrest.CoreMatchers.equalTo class AuthApiTest( @SpykBean private val jwtUtils: JwtUtils, diff --git a/service/src/test/kotlin/com/sangdol/roomescape/auth/FailOnSaveLoginHistoryTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/auth/FailOnSaveLoginHistoryTest.kt index 16d616f3..fb86ab54 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/auth/FailOnSaveLoginHistoryTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/auth/FailOnSaveLoginHistoryTest.kt @@ -1,8 +1,6 @@ package com.sangdol.roomescape.auth import com.ninjasquad.springmockk.MockkBean -import io.mockk.clearMocks -import io.mockk.every import com.sangdol.common.types.web.HttpStatus import com.sangdol.roomescape.auth.infrastructure.persistence.LoginHistoryRepository import com.sangdol.roomescape.auth.web.LoginRequest @@ -11,6 +9,8 @@ import com.sangdol.roomescape.supports.AdminFixture import com.sangdol.roomescape.supports.FunSpecSpringbootTest import com.sangdol.roomescape.supports.UserFixture import com.sangdol.roomescape.supports.runTest +import io.mockk.clearMocks +import io.mockk.every class FailOnSaveLoginHistoryTest( @MockkBean private val loginHistoryRepository: LoginHistoryRepository diff --git a/service/src/test/kotlin/com/sangdol/roomescape/payment/PaymentAPITest.kt b/service/src/test/kotlin/com/sangdol/roomescape/payment/PaymentAPITest.kt index fc5cc0d5..119d3a84 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/payment/PaymentAPITest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/payment/PaymentAPITest.kt @@ -1,10 +1,6 @@ package com.sangdol.roomescape.payment import com.ninjasquad.springmockk.MockkBean -import io.kotest.matchers.shouldBe -import io.mockk.every -import org.springframework.data.repository.findByIdOrNull -import org.springframework.http.HttpMethod import com.sangdol.common.types.web.HttpStatus import com.sangdol.roomescape.auth.exception.AuthErrorCode import com.sangdol.roomescape.payment.business.PaymentService @@ -18,6 +14,10 @@ import com.sangdol.roomescape.payment.infrastructure.persistence.* import com.sangdol.roomescape.payment.web.PaymentConfirmRequest import com.sangdol.roomescape.payment.web.PaymentCreateResponse import com.sangdol.roomescape.supports.* +import io.kotest.matchers.shouldBe +import io.mockk.every +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpMethod class PaymentAPITest( @MockkBean diff --git a/service/src/test/kotlin/com/sangdol/roomescape/payment/PaymentTypeTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/payment/PaymentTypeTest.kt index 5831bc95..5f3c95ff 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/payment/PaymentTypeTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/payment/PaymentTypeTest.kt @@ -2,14 +2,7 @@ package com.sangdol.roomescape.payment import com.sangdol.roomescape.payment.exception.PaymentErrorCode import com.sangdol.roomescape.payment.exception.PaymentException -import com.sangdol.roomescape.payment.infrastructure.common.BankCode -import com.sangdol.roomescape.payment.infrastructure.common.CardIssuerCode -import com.sangdol.roomescape.payment.infrastructure.common.CardOwnerType -import com.sangdol.roomescape.payment.infrastructure.common.CardType -import com.sangdol.roomescape.payment.infrastructure.common.EasyPayCompanyCode -import com.sangdol.roomescape.payment.infrastructure.common.PaymentMethod -import com.sangdol.roomescape.payment.infrastructure.common.PaymentStatus -import com.sangdol.roomescape.payment.infrastructure.common.PaymentType +import com.sangdol.roomescape.payment.infrastructure.common.* import io.kotest.assertions.assertSoftly import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe diff --git a/service/src/test/kotlin/com/sangdol/roomescape/payment/SampleTosspayConstant.kt b/service/src/test/kotlin/com/sangdol/roomescape/payment/SampleTosspayConstant.kt index aa1c8514..d8170312 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/payment/SampleTosspayConstant.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/payment/SampleTosspayConstant.kt @@ -1,6 +1,6 @@ package com.sangdol.roomescape.payment -import java.time.OffsetDateTime +import com.sangdol.common.utils.KoreaDateTime object SampleTosspayConstant { const val PAYMENT_KEY: String = "5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1" @@ -39,8 +39,8 @@ object SampleTosspayConstant { "orderName": "Sonya Aguirre 예약 결제", "taxExemptionAmount": 0, "status": "DONE", - "requestedAt": "${OffsetDateTime.now()}", - "approvedAt": "${OffsetDateTime.now().plusSeconds(5)}", + "requestedAt": "${KoreaDateTime.nowWithOffset()}", + "approvedAt": "${KoreaDateTime.nowWithOffset().plusSeconds(5)}", "useEscrow": false, "cultureExpense": false, "card": { @@ -102,8 +102,8 @@ object SampleTosspayConstant { "orderName": "Sonya Aguirre 예약 결제", "taxExemptionAmount": 0, "status": "CANCELED", - "requestedAt": "${OffsetDateTime.now()}", - "approvedAt": "${OffsetDateTime.now().plusSeconds(5)}", + "requestedAt": "${KoreaDateTime.nowWithOffset()}", + "approvedAt": "${KoreaDateTime.nowWithOffset().plusSeconds(5)}", "useEscrow": false, "cultureExpense": false, "card": { @@ -132,7 +132,7 @@ object SampleTosspayConstant { "transactionKey": "txrd_a01k4mtgh26vgrn1evbdckyqmdr", "cancelReason": "$CANCEL_REASON", "taxExemptionAmount": 0, - "canceledAt": "${OffsetDateTime.now().plusMinutes(1)}", + "canceledAt": "${KoreaDateTime.nowWithOffset().plusMinutes(1)}", "cardDiscountAmount": 0, "transferDiscountAmount": 0, "easyPayDiscountAmount": 0, @@ -181,8 +181,8 @@ object SampleTosspayConstant { "orderName": "Sonya Aguirre 예약 결제", "taxExemptionAmount": 0, "status": "DONE", - "requestedAt": "${OffsetDateTime.now()}", - "approvedAt": "${OffsetDateTime.now().plusSeconds(5)}", + "requestedAt": "${KoreaDateTime.nowWithOffset()}", + "approvedAt": "${KoreaDateTime.nowWithOffset().plusSeconds(5)}", "useEscrow": false, "cultureExpense": false, "card": null, @@ -230,8 +230,8 @@ object SampleTosspayConstant { "orderName": "Sonya Aguirre 예약 결제", "taxExemptionAmount": 0, "status": "DONE", - "requestedAt": "${OffsetDateTime.now()}", - "approvedAt": "${OffsetDateTime.now().plusSeconds(5)}", + "requestedAt": "${KoreaDateTime.nowWithOffset()}", + "approvedAt": "${KoreaDateTime.nowWithOffset().plusSeconds(5)}", "useEscrow": false, "cultureExpense": false, "card": null, @@ -250,7 +250,7 @@ object SampleTosspayConstant { "transactionKey": "txrd_a01k4mtgh26vgrn1evbdckyqmdr", "cancelReason": "$CANCEL_REASON", "taxExemptionAmount": 0, - "canceledAt": "${OffsetDateTime.now().plusMinutes(1)}", + "canceledAt": "${KoreaDateTime.nowWithOffset().plusMinutes(1)}", "cardDiscountAmount": 0, "transferDiscountAmount": 0, "easyPayDiscountAmount": 0, diff --git a/service/src/test/kotlin/com/sangdol/roomescape/payment/TosspayClientTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/payment/TosspayClientTest.kt index 1373d22a..6ad61b04 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/payment/TosspayClientTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/payment/TosspayClientTest.kt @@ -1,6 +1,12 @@ package com.sangdol.roomescape.payment import com.ninjasquad.springmockk.MockkBean +import com.sangdol.roomescape.payment.exception.PaymentErrorCode +import com.sangdol.roomescape.payment.exception.PaymentException +import com.sangdol.roomescape.payment.infrastructure.client.PaymentClientCancelResponse +import com.sangdol.roomescape.payment.infrastructure.client.PaymentClientConfirmResponse +import com.sangdol.roomescape.payment.infrastructure.client.TosspayClient +import com.sangdol.roomescape.payment.infrastructure.common.PaymentStatus import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec @@ -9,19 +15,13 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.client.RestClientTest import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.test.web.client.MockRestServiceServer import org.springframework.test.web.client.ResponseActions import org.springframework.test.web.client.match.MockRestRequestMatchers.* import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus import org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess -import com.sangdol.roomescape.payment.exception.PaymentErrorCode -import com.sangdol.roomescape.payment.exception.PaymentException -import com.sangdol.roomescape.payment.infrastructure.client.PaymentClientCancelResponse -import com.sangdol.roomescape.payment.infrastructure.client.PaymentClientConfirmResponse -import com.sangdol.roomescape.payment.infrastructure.client.TosspayClient -import com.sangdol.roomescape.payment.infrastructure.common.PaymentStatus -import org.springframework.http.HttpStatus @RestClientTest(TosspayClient::class) @MockkBean(JpaMetamodelMappingContext::class) diff --git a/service/src/test/kotlin/com/sangdol/roomescape/region/RegionApiFailTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/region/RegionApiFailTest.kt index 1621aab9..6b5fabd2 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/region/RegionApiFailTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/region/RegionApiFailTest.kt @@ -1,16 +1,16 @@ package com.sangdol.roomescape.region import com.ninjasquad.springmockk.MockkBean -import io.mockk.every -import org.springframework.http.HttpMethod import com.sangdol.roomescape.region.exception.RegionErrorCode import com.sangdol.roomescape.region.infrastructure.persistence.RegionRepository import com.sangdol.roomescape.supports.FunSpecSpringbootTest import com.sangdol.roomescape.supports.runExceptionTest +import io.mockk.every +import org.springframework.http.HttpMethod class RegionApiFailTest( @MockkBean private val regionRepository: RegionRepository -): FunSpecSpringbootTest() { +) : FunSpecSpringbootTest() { init { context("조회 실패") { test("시/도") { diff --git a/service/src/test/kotlin/com/sangdol/roomescape/region/RegionApiSuccessTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/region/RegionApiSuccessTest.kt index cbb3ff83..4d5dbdd1 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/region/RegionApiSuccessTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/region/RegionApiSuccessTest.kt @@ -1,11 +1,11 @@ package com.sangdol.roomescape.region -import io.kotest.matchers.shouldBe import com.sangdol.common.types.web.HttpStatus import com.sangdol.roomescape.supports.FunSpecSpringbootTest import com.sangdol.roomescape.supports.runTest +import io.kotest.matchers.shouldBe -class RegionApiSuccessTest: FunSpecSpringbootTest() { +class RegionApiSuccessTest : FunSpecSpringbootTest() { init { context("시/도 -> 시/군/구 -> 지역 코드 순으로 조회한다.") { test("정상 응답") { diff --git a/service/src/test/kotlin/com/sangdol/roomescape/reservation/IncompletedReservationSchedulerTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/reservation/IncompletedReservationSchedulerTest.kt index f71a240c..c317525f 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/reservation/IncompletedReservationSchedulerTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/reservation/IncompletedReservationSchedulerTest.kt @@ -13,7 +13,8 @@ import io.kotest.assertions.assertSoftly import io.kotest.matchers.shouldBe import org.springframework.data.repository.findByIdOrNull import org.springframework.jdbc.core.JdbcTemplate -import java.time.LocalDateTime +import java.time.Instant +import java.time.temporal.ChronoUnit /** * @see com.sangdol.roomescape.reservation.business.scheduler.IncompletedReservationScheduler @@ -31,7 +32,7 @@ class IncompletedReservationSchedulerTest( test("예약이 없고, hold_expired_at 시간이 지난 ${ScheduleStatus.HOLD} 일정을 ${ScheduleStatus.AVAILABLE} 상태로 바꾼다.") { val schedule: ScheduleEntity = dummyInitializer.createSchedule().apply { this.status = ScheduleStatus.HOLD - this.holdExpiredAt = LocalDateTime.now().minusSeconds(1) + this.holdExpiredAt = Instant.now().minusSeconds(1) }.also { scheduleRepository.saveAndFlush(it) } @@ -52,16 +53,13 @@ class IncompletedReservationSchedulerTest( jdbcTemplate.execute("UPDATE reservation SET created_at = DATE_SUB(NOW(), INTERVAL 5 MINUTE) WHERE id = ${it.id}") } - val now = LocalDateTime.now() - transactionExecutionUtil.withNewTransaction(isReadOnly = false) { incompletedReservationScheduler.processExpiredReservation() } assertSoftly(reservationRepository.findByIdOrNull(reservation.id)!!) { this.status shouldBe ReservationStatus.EXPIRED - this.updatedAt.hour shouldBe now.hour - this.updatedAt.minute shouldBe now.minute + this.updatedAt.truncatedTo(ChronoUnit.MINUTES) shouldBe Instant.now().truncatedTo(ChronoUnit.MINUTES) } assertSoftly(scheduleRepository.findByIdOrNull(reservation.scheduleId)!!) { diff --git a/service/src/test/kotlin/com/sangdol/roomescape/reservation/ReservationApiTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/reservation/ReservationApiTest.kt index 49b97aee..9d255626 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/reservation/ReservationApiTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/reservation/ReservationApiTest.kt @@ -1,13 +1,8 @@ package com.sangdol.roomescape.reservation -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import org.hamcrest.CoreMatchers.equalTo -import org.springframework.data.repository.findByIdOrNull -import org.springframework.http.HttpMethod +import com.sangdol.common.types.exception.CommonErrorCode import com.sangdol.common.types.web.HttpStatus import com.sangdol.roomescape.auth.exception.AuthErrorCode -import com.sangdol.common.types.exception.CommonErrorCode import com.sangdol.roomescape.payment.infrastructure.common.BankCode import com.sangdol.roomescape.payment.infrastructure.common.CardIssuerCode import com.sangdol.roomescape.payment.infrastructure.common.EasyPayCompanyCode @@ -25,6 +20,11 @@ import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus import com.sangdol.roomescape.supports.* import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import org.hamcrest.CoreMatchers.equalTo +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpMethod import java.time.LocalDate import java.time.LocalTime @@ -574,7 +574,10 @@ class ReservationApiTest( expect = { statusCode(HttpStatus.OK.value()) assertProperties(props = setOf("id", "reserver", "user", "applicationDateTime", "payment")) - assertProperties(props = setOf("name", "contact", "participantCount", "requirement"), propsNameIfList = "reserver") + assertProperties( + props = setOf("name", "contact", "participantCount", "requirement"), + propsNameIfList = "reserver" + ) assertProperties(props = setOf("id", "name", "phone"), propsNameIfList = "user") } ).also { diff --git a/service/src/test/kotlin/com/sangdol/roomescape/reservation/ReservationConcurrencyTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/reservation/ReservationConcurrencyTest.kt index 111c14e1..75fc7279 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/reservation/ReservationConcurrencyTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/reservation/ReservationConcurrencyTest.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.withContext import org.springframework.data.repository.findByIdOrNull import org.springframework.transaction.PlatformTransactionManager import org.springframework.transaction.support.TransactionTemplate -import java.time.LocalDateTime +import java.time.Instant class ReservationConcurrencyTest( private val transactionManager: PlatformTransactionManager, @@ -35,7 +35,7 @@ class ReservationConcurrencyTest( val user = testAuthUtil.defaultUserLogin().first val schedule = dummyInitializer.createSchedule().also { it.status = ScheduleStatus.HOLD - it.holdExpiredAt = LocalDateTime.now().minusMinutes(1) + it.holdExpiredAt = Instant.now().minusSeconds(1 * 60) scheduleRepository.save(it) } lateinit var response: PendingReservationCreateResponse diff --git a/service/src/test/kotlin/com/sangdol/roomescape/schedule/AdminScheduleApiTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/schedule/AdminScheduleApiTest.kt index 1576bbca..a47cca77 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/schedule/AdminScheduleApiTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/schedule/AdminScheduleApiTest.kt @@ -1,10 +1,13 @@ package com.sangdol.roomescape.schedule -import com.sangdol.roomescape.common.types.Auditor import com.sangdol.common.types.web.HttpStatus +import com.sangdol.common.utils.KoreaDate +import com.sangdol.common.utils.KoreaDateTime +import com.sangdol.common.utils.KoreaTime import com.sangdol.roomescape.admin.infrastructure.persistence.AdminPermissionLevel import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType import com.sangdol.roomescape.auth.exception.AuthErrorCode +import com.sangdol.roomescape.common.types.Auditor import com.sangdol.roomescape.schedule.exception.ScheduleErrorCode import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleEntity import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository @@ -54,7 +57,8 @@ class AdminScheduleApiTest( lateinit var token: String beforeTest { - val today = LocalDate.now() + val now = KoreaDateTime.now() + val today = now.toLocalDate() store = dummyInitializer.createStore() val admin = AdminFixture.createStoreAdmin(storeId = store.id) token = testAuthUtil.adminLogin(admin).second @@ -66,21 +70,21 @@ class AdminScheduleApiTest( storeId = store.id, request = ScheduleFixture.createRequest.copy( date = today, - time = LocalTime.now().plusHours(2) + time = now.toLocalTime().plusHours(2) ) ), dummyInitializer.createSchedule( storeId = store.id, request = ScheduleFixture.createRequest.copy( date = today, - time = LocalTime.now().plusHours(1) + time = now.toLocalTime().plusHours(1) ) ), dummyInitializer.createSchedule( storeId = store.id, request = ScheduleFixture.createRequest.copy( date = today.plusDays(1), - time = LocalTime.of(11, 0) + time = LocalTime.of(10, 0) ) ) ) @@ -95,7 +99,10 @@ class AdminScheduleApiTest( }, expect = { statusCode(HttpStatus.OK.value()) - body("data.schedules.size()", equalTo(schedules.filter { it.date.isEqual(LocalDate.now()) }.size)) + body( + "data.schedules.size()", + equalTo(schedules.filter { it.date.isEqual(KoreaDate.today()) }.size) + ) assertProperties( props = setOf("id", "themeName", "startFrom", "endAt", "status"), propsNameIfList = "schedules" @@ -386,8 +393,9 @@ class AdminScheduleApiTest( test("과거 시간을 선택하면 실패한다.") { val (admin, token) = testAuthUtil.defaultStoreAdminLogin() - val date = LocalDate.now() - val time = LocalTime.now().minusMinutes(1) + val now = KoreaDateTime.now() + val date = now.toLocalDate() + val time = now.toLocalTime().minusMinutes(1) val theme = dummyInitializer.createTheme() val request = ScheduleFixture.createRequest.copy(date = date, time = time, themeId = theme.id) @@ -490,29 +498,50 @@ class AdminScheduleApiTest( } context("정상 응답") { - test("시간만 변경한다.") { - val (admin, token) = testAuthUtil.defaultStoreAdminLogin() - val schedule = initialize("수정을 위한 일정 생성") { - dummyInitializer.createSchedule() - } - val updateTime = schedule.time.plusHours(1) - - runTest( - token = token, - using = { - body(ScheduleUpdateRequest(time = updateTime)) - }, - on = { - patch("/admin/schedules/${schedule.id}") - }, - expect = { - statusCode(HttpStatus.OK.value()) + context("시간만 변경한다.") { + test("성공") { + val (admin, token) = testAuthUtil.defaultStoreAdminLogin() + val schedule = initialize("수정을 위한 일정 생성") { + dummyInitializer.createSchedule() } - ).also { - val updated = scheduleRepository.findByIdOrNull(schedule.id)!! - updated.time shouldBe updateTime - updated.status shouldBe schedule.status - updated.updatedAt shouldNotBe schedule.updatedAt + val updateTime = schedule.time.plusHours(1) + + runTest( + token = token, + using = { + body(ScheduleUpdateRequest(time = updateTime)) + }, + on = { + patch("/admin/schedules/${schedule.id}") + }, + expect = { + statusCode(HttpStatus.OK.value()) + } + ).also { + val updated = scheduleRepository.findByIdOrNull(schedule.id)!! + updated.time shouldBe updateTime + updated.status shouldBe schedule.status + updated.updatedAt shouldNotBe schedule.updatedAt + } + } + + test("지난 시간을 선택하면 실패한다.") { + val (admin, token) = testAuthUtil.defaultStoreAdminLogin() + val schedule = initialize("수정을 위한 일정 생성") { + val request = ScheduleFixture.createRequest.copy( + date = KoreaDate.today(), + time = KoreaTime.now().plusHours(1) + ) + dummyInitializer.createSchedule(request = request) + } + + runExceptionTest( + token = token, + method = HttpMethod.PATCH, + endpoint = "/admin/schedules/${schedule.id}", + requestBody = ScheduleUpdateRequest(time = KoreaTime.now().minusMinutes(1)), + expectedErrorCode = ScheduleErrorCode.PAST_DATE_TIME + ) } } diff --git a/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleApiTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleApiTest.kt index 96453cff..2ad2c5a6 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleApiTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleApiTest.kt @@ -1,6 +1,8 @@ package com.sangdol.roomescape.schedule import com.sangdol.common.types.web.HttpStatus +import com.sangdol.common.utils.KoreaDate +import com.sangdol.common.utils.KoreaTime import com.sangdol.roomescape.admin.infrastructure.persistence.AdminPermissionLevel import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType import com.sangdol.roomescape.auth.exception.AuthErrorCode @@ -13,16 +15,18 @@ import org.hamcrest.CoreMatchers.equalTo import org.springframework.data.repository.findByIdOrNull import org.springframework.http.HttpMethod import java.time.LocalDate -import java.time.LocalTime class ScheduleApiTest( private val scheduleRepository: ScheduleRepository ) : FunSpecSpringbootTest() { init { context("특정 매장 + 날짜의 일정 및 테마 정보를 조회한다.") { + /** + * @throws 23시 57분 ~ 59분에 실행하면 실패함. + */ test("날짜가 당일이면 현재 시간 이후의 정보만 조회된다.") { val size = 3 - val date = LocalDate.now() + val date = KoreaDate.today() val store = dummyInitializer.createStore() initialize("조회를 위한 오늘 날짜의 현재 시간 이후인 ${size}개의 일정, 현재 시간 이전인 1개의 일정 생성") { @@ -31,7 +35,7 @@ class ScheduleApiTest( storeId = store.id, request = ScheduleFixture.createRequest.copy( date = date, - time = LocalTime.now().plusMinutes(i.toLong()) + time = KoreaTime.now().plusMinutes(i.toLong()) ) ) } @@ -40,24 +44,28 @@ class ScheduleApiTest( storeId = store.id, request = ScheduleFixture.createRequest.copy( date = date, - time = LocalTime.now().minusMinutes(1) + time = KoreaTime.now().minusMinutes(1) ) ) } - val expectedSize = scheduleRepository.findAll().takeIf { it.isNotEmpty() } - ?.let { it.count { schedule -> schedule.date.isEqual(date) && schedule.time.isAfter(LocalTime.now()) } } - ?: throw AssertionError("initialize 작업에서 레코드가 저장되지 않음.") - runTest( on = { get("/stores/${store.id}/schedules?date=${date}") }, expect = { statusCode(HttpStatus.OK.value()) - body("data.schedules.size()", equalTo(expectedSize)) + body("data.schedules.size()", equalTo(size)) assertProperties( - props = setOf("id", "startFrom", "endAt", "themeId", "themeName", "themeDifficulty", "status"), + props = setOf( + "id", + "startFrom", + "endAt", + "themeId", + "themeName", + "themeDifficulty", + "status" + ), propsNameIfList = "schedules" ) } diff --git a/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleConcurrencyTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleConcurrencyTest.kt index c237d11e..d447729c 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleConcurrencyTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleConcurrencyTest.kt @@ -7,8 +7,6 @@ import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus import com.sangdol.roomescape.supports.FunSpecSpringbootTest import com.sangdol.roomescape.supports.runTest import io.kotest.assertions.assertSoftly -import io.kotest.matchers.collections.shouldContainExactly -import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import kotlinx.coroutines.Dispatchers diff --git a/service/src/test/kotlin/com/sangdol/roomescape/store/AdminStoreApiTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/store/AdminStoreApiTest.kt index 8e74c6a4..9b1f77a8 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/store/AdminStoreApiTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/store/AdminStoreApiTest.kt @@ -1,10 +1,5 @@ package com.sangdol.roomescape.store -import io.kotest.assertions.assertSoftly -import io.kotest.matchers.date.shouldBeAfter -import io.kotest.matchers.shouldBe -import org.springframework.data.repository.findByIdOrNull -import org.springframework.http.HttpMethod import com.sangdol.common.types.web.HttpStatus import com.sangdol.roomescape.admin.infrastructure.persistence.AdminEntity import com.sangdol.roomescape.admin.infrastructure.persistence.AdminPermissionLevel @@ -16,6 +11,11 @@ import com.sangdol.roomescape.store.infrastructure.persistence.StoreRepository import com.sangdol.roomescape.store.infrastructure.persistence.StoreStatus import com.sangdol.roomescape.store.web.StoreUpdateRequest import com.sangdol.roomescape.supports.* +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.date.shouldBeAfter +import io.kotest.matchers.shouldBe +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpMethod class AdminStoreApiTest( private val storeRepository: StoreRepository, diff --git a/service/src/test/kotlin/com/sangdol/roomescape/store/StoreApiTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/store/StoreApiTest.kt index d8af0c5f..79839aa1 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/store/StoreApiTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/store/StoreApiTest.kt @@ -1,13 +1,13 @@ package com.sangdol.roomescape.store -import org.hamcrest.CoreMatchers.equalTo -import org.springframework.http.HttpMethod import com.sangdol.common.types.web.HttpStatus import com.sangdol.roomescape.store.exception.StoreErrorCode import com.sangdol.roomescape.store.infrastructure.persistence.StoreEntity import com.sangdol.roomescape.supports.* +import org.hamcrest.CoreMatchers.equalTo +import org.springframework.http.HttpMethod -class StoreApiTest: FunSpecSpringbootTest() { +class StoreApiTest : FunSpecSpringbootTest() { init { context("모든 매장의 id / 이름을 조회한다.") { diff --git a/service/src/test/kotlin/com/sangdol/roomescape/supports/DummyInitializer.kt b/service/src/test/kotlin/com/sangdol/roomescape/supports/DummyInitializer.kt index f1252bc3..3e71288a 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/supports/DummyInitializer.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/supports/DummyInitializer.kt @@ -30,7 +30,7 @@ import com.sangdol.roomescape.theme.web.ThemeCreateRequest import com.sangdol.roomescape.theme.web.toEntity import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity import org.springframework.data.repository.findByIdOrNull -import java.time.LocalDateTime +import java.time.Instant class DummyInitializer( private val storeRepository: StoreRepository, @@ -206,7 +206,7 @@ class DummyInitializer( return paymentWriter.cancel( userId, payment, - requestedAt = LocalDateTime.now(), + requestedAt = Instant.now(), clientCancelResponse ) } diff --git a/service/src/test/kotlin/com/sangdol/roomescape/supports/Fixtures.kt b/service/src/test/kotlin/com/sangdol/roomescape/supports/Fixtures.kt index bf59d52b..27ef1215 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/supports/Fixtures.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/supports/Fixtures.kt @@ -2,6 +2,7 @@ package com.sangdol.roomescape.supports import com.github.f4b6a3.tsid.TsidFactory import com.sangdol.common.persistence.TsidIDGenerator +import com.sangdol.common.utils.KoreaDateTime import com.sangdol.roomescape.admin.infrastructure.persistence.AdminEntity import com.sangdol.roomescape.admin.infrastructure.persistence.AdminPermissionLevel import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType @@ -25,7 +26,6 @@ import com.sangdol.roomescape.user.web.MIN_PASSWORD_LENGTH import com.sangdol.roomescape.user.web.UserCreateRequest import java.time.LocalDate import java.time.LocalTime -import java.time.OffsetDateTime const val INVALID_PK: Long = 9999L val IDGenerator = TsidIDGenerator(TsidFactory(0)) @@ -297,8 +297,8 @@ object PaymentFixture { card = cardDetail, easyPay = easyPayDetail, transfer = transferDetail, - requestedAt = OffsetDateTime.now(), - approvedAt = OffsetDateTime.now().plusSeconds(5) + requestedAt = KoreaDateTime.nowWithOffset(), + approvedAt = KoreaDateTime.nowWithOffset().plusSeconds(5) ) fun cancelResponse( @@ -314,7 +314,7 @@ object PaymentFixture { cardDiscountAmount = cardDiscountAmount, transferDiscountAmount = transferDiscountAmount, easyPayDiscountAmount = easypayDiscountAmount, - canceledAt = OffsetDateTime.now().plusSeconds(5), + canceledAt = KoreaDateTime.nowWithOffset().plusSeconds(5), cancelReason = cancelReason ), ) diff --git a/service/src/test/kotlin/com/sangdol/roomescape/supports/KotestConfig.kt b/service/src/test/kotlin/com/sangdol/roomescape/supports/KotestConfig.kt index fbbe81b0..a9eae9bf 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/supports/KotestConfig.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/supports/KotestConfig.kt @@ -1,5 +1,13 @@ package com.sangdol.roomescape.supports +import com.sangdol.roomescape.admin.infrastructure.persistence.AdminRepository +import com.sangdol.roomescape.payment.business.PaymentWriter +import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentRepository +import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationRepository +import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository +import com.sangdol.roomescape.store.infrastructure.persistence.StoreRepository +import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository +import com.sangdol.roomescape.user.infrastructure.persistence.UserRepository import io.kotest.core.config.AbstractProjectConfig import io.kotest.core.spec.Spec import io.kotest.core.spec.style.FunSpec @@ -13,17 +21,15 @@ import org.springframework.boot.test.web.server.LocalServerPort import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Import import org.springframework.test.context.ActiveProfiles -import com.sangdol.roomescape.admin.infrastructure.persistence.AdminRepository -import com.sangdol.roomescape.payment.business.PaymentWriter -import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentRepository -import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationRepository -import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository -import com.sangdol.roomescape.store.infrastructure.persistence.StoreRepository -import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository -import com.sangdol.roomescape.user.infrastructure.persistence.UserRepository +import java.util.* object KotestConfig : AbstractProjectConfig() { override fun extensions(): List = listOf(SpringExtension) + override suspend fun beforeProject() { + super.beforeProject() + System.setProperty("user.timezone", "UTC") + TimeZone.setDefault(TimeZone.getTimeZone("UTC")) + } } @Import(TestConfig::class) diff --git a/service/src/test/kotlin/com/sangdol/roomescape/supports/RestAssuredUtils.kt b/service/src/test/kotlin/com/sangdol/roomescape/supports/RestAssuredUtils.kt index af53b62e..62155a0a 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/supports/RestAssuredUtils.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/supports/RestAssuredUtils.kt @@ -1,7 +1,7 @@ package com.sangdol.roomescape.supports -import com.sangdol.common.web.config.JacksonConfig import com.sangdol.common.types.exception.ErrorCode +import com.sangdol.common.web.config.JacksonConfig import io.restassured.module.kotlin.extensions.Given import io.restassured.module.kotlin.extensions.Then import io.restassured.module.kotlin.extensions.When diff --git a/service/src/test/kotlin/com/sangdol/roomescape/supports/TestAuthUtil.kt b/service/src/test/kotlin/com/sangdol/roomescape/supports/TestAuthUtil.kt index a3340a19..90f111c4 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/supports/TestAuthUtil.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/supports/TestAuthUtil.kt @@ -1,12 +1,6 @@ package com.sangdol.roomescape.supports -import io.restassured.module.kotlin.extensions.Extract -import io.restassured.module.kotlin.extensions.Given -import io.restassured.module.kotlin.extensions.Then -import io.restassured.module.kotlin.extensions.When -import org.springframework.data.repository.findByIdOrNull import com.sangdol.common.types.web.HttpStatus -import org.springframework.http.MediaType import com.sangdol.roomescape.admin.infrastructure.persistence.AdminEntity import com.sangdol.roomescape.admin.infrastructure.persistence.AdminRepository import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType @@ -16,6 +10,12 @@ import com.sangdol.roomescape.store.infrastructure.persistence.StoreRepository import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity import com.sangdol.roomescape.user.infrastructure.persistence.UserRepository import com.sangdol.roomescape.user.web.UserCreateRequest +import io.restassured.module.kotlin.extensions.Extract +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.MediaType class TestAuthUtil( private val userRepository: UserRepository, @@ -50,7 +50,7 @@ class TestAuthUtil( } } - fun signup(request: UserCreateRequest): UserEntity { + fun signup(request: UserCreateRequest): UserEntity { println("[TestAuthUtil] 회원가입 시작: $request") val userId: Long = Given { contentType(MediaType.APPLICATION_JSON_VALUE) diff --git a/service/src/test/kotlin/com/sangdol/roomescape/supports/TestDatabaseUtil.kt b/service/src/test/kotlin/com/sangdol/roomescape/supports/TestDatabaseUtil.kt index 74f9285e..48e0b510 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/supports/TestDatabaseUtil.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/supports/TestDatabaseUtil.kt @@ -23,7 +23,8 @@ class TestDatabaseUtil( } fun initializeRegion() { - jdbcTemplate.queryForObject("SELECT EXISTS (SELECT 1 FROM region LIMIT 1)", Boolean::class.java)!!.also { isRegionTableEmpty -> + jdbcTemplate.queryForObject("SELECT EXISTS (SELECT 1 FROM region LIMIT 1)", Boolean::class.java)!! + .also { isRegionTableEmpty -> if (!isRegionTableEmpty) { this::class.java.getResource("/schema/region-data.sql")?.readText()?.let { regionInsertSql -> jdbcTemplate.execute(regionInsertSql) @@ -51,7 +52,7 @@ enum class CleanerMode { ALL } -class DatabaseCleanerExtension: BeforeSpecListener, AfterTestListener, AfterSpecListener { +class DatabaseCleanerExtension : BeforeSpecListener, AfterTestListener, AfterSpecListener { override suspend fun beforeSpec(spec: Spec) { super.beforeSpec(spec) diff --git a/service/src/test/kotlin/com/sangdol/roomescape/supports/TestUtil.kt b/service/src/test/kotlin/com/sangdol/roomescape/supports/TestUtil.kt index 0e262845..e0976c93 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/supports/TestUtil.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/supports/TestUtil.kt @@ -12,8 +12,8 @@ inline fun initialize(name: String, block: () -> T): T { fun randomPhoneNumber(): String { val prefix = "010" - val middle = (1..4).map { (0..9).random() }.joinToString("") - val last = (1..4).map { (0..9).random() }.joinToString("") + val middle = (1..4).map { (0..9).random() }.joinToString("") + val last = (1..4).map { (0..9).random() }.joinToString("") return "$prefix$middle$last" } diff --git a/service/src/test/kotlin/com/sangdol/roomescape/theme/AdminThemeApiTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/theme/AdminThemeApiTest.kt index de15aa00..f70cbecf 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/theme/AdminThemeApiTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/theme/AdminThemeApiTest.kt @@ -1,10 +1,5 @@ package com.sangdol.roomescape.theme -import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.matchers.shouldBe -import org.hamcrest.CoreMatchers.equalTo -import org.springframework.data.repository.findByIdOrNull -import org.springframework.http.HttpMethod import com.sangdol.common.types.web.HttpStatus import com.sangdol.roomescape.admin.infrastructure.persistence.AdminPermissionLevel import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType @@ -18,6 +13,11 @@ import com.sangdol.roomescape.theme.exception.ThemeErrorCode import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository import com.sangdol.roomescape.theme.web.ThemeUpdateRequest +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import org.hamcrest.CoreMatchers.equalTo +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpMethod class AdminThemeApiTest( private val themeRepository: ThemeRepository diff --git a/service/src/test/kotlin/com/sangdol/roomescape/theme/DateUtilsTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/theme/DateUtilsTest.kt index b17b6620..8aa1a215 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/theme/DateUtilsTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/theme/DateUtilsTest.kt @@ -9,7 +9,7 @@ class DateUtilsTest : StringSpec({ "입력된 날짜의 이전 주 일요일을 찾는다." { val expected = LocalDate.of(2025, 8, 31) - for (i in 7..13){ + for (i in 7..13) { DateUtils.getSundayOfPreviousWeek(LocalDate.of(2025, 9, i)) shouldBe expected } } diff --git a/service/src/test/kotlin/com/sangdol/roomescape/theme/ThemeApiTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/theme/ThemeApiTest.kt index f589b380..b99987d4 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/theme/ThemeApiTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/theme/ThemeApiTest.kt @@ -1,6 +1,7 @@ package com.sangdol.roomescape.theme import com.sangdol.common.types.web.HttpStatus +import com.sangdol.common.utils.KoreaDate import com.sangdol.roomescape.supports.* import com.sangdol.roomescape.theme.business.DateUtils import com.sangdol.roomescape.theme.exception.ThemeErrorCode @@ -89,7 +90,7 @@ class ThemeApiTest( user = user, storeId = store.id, scheduleRequest = ScheduleFixture.createRequest.copy( - date = DateUtils.getSundayOfPreviousWeek(LocalDate.now()).plusDays(it), + date = DateUtils.getSundayOfPreviousWeek(KoreaDate.today()).plusDays(it), themeId = themeIds[0], ) ) @@ -100,7 +101,7 @@ class ThemeApiTest( user = user, storeId = store.id, scheduleRequest = ScheduleFixture.createRequest.copy( - date = DateUtils.getSundayOfPreviousWeek(LocalDate.now()), + date = DateUtils.getSundayOfPreviousWeek(KoreaDate.today()), themeId = themeIds[1], ) ) @@ -111,7 +112,7 @@ class ThemeApiTest( user = user, storeId = store.id, scheduleRequest = ScheduleFixture.createRequest.copy( - date = DateUtils.getSundayOfPreviousWeek(LocalDate.now()).plusDays(it), + date = DateUtils.getSundayOfPreviousWeek(KoreaDate.today()).plusDays(it), themeId = themeIds[2], ) ) @@ -123,7 +124,7 @@ class ThemeApiTest( user = user, storeId = store.id, scheduleRequest = ScheduleFixture.createRequest.copy( - date = DateUtils.getSundayOfPreviousWeek(LocalDate.now()).plusDays(it), + date = DateUtils.getSundayOfPreviousWeek(KoreaDate.today()).plusDays(it), themeId = themeIds[3], ) ) diff --git a/service/src/test/kotlin/com/sangdol/roomescape/user/UserApiTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/user/UserApiTest.kt index db98d5d3..108152a6 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/user/UserApiTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/user/UserApiTest.kt @@ -1,6 +1,19 @@ package com.sangdol.roomescape.user import com.ninjasquad.springmockk.SpykBean +import com.sangdol.common.types.exception.CommonErrorCode +import com.sangdol.common.types.web.HttpStatus +import com.sangdol.roomescape.auth.exception.AuthErrorCode +import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils +import com.sangdol.roomescape.supports.FunSpecSpringbootTest +import com.sangdol.roomescape.supports.UserFixture +import com.sangdol.roomescape.supports.runExceptionTest +import com.sangdol.roomescape.supports.runTest +import com.sangdol.roomescape.user.business.SIGNUP +import com.sangdol.roomescape.user.exception.UserErrorCode +import com.sangdol.roomescape.user.infrastructure.persistence.* +import com.sangdol.roomescape.user.web.MIN_PASSWORD_LENGTH +import com.sangdol.roomescape.user.web.UserCreateRequest import io.kotest.assertions.assertSoftly import io.kotest.matchers.shouldBe import io.mockk.every @@ -11,20 +24,7 @@ import io.restassured.module.kotlin.extensions.When import org.hamcrest.CoreMatchers.equalTo import org.springframework.data.repository.findByIdOrNull import org.springframework.http.HttpMethod -import com.sangdol.common.types.web.HttpStatus import org.springframework.http.MediaType -import com.sangdol.roomescape.auth.exception.AuthErrorCode -import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils -import com.sangdol.common.types.exception.CommonErrorCode -import com.sangdol.roomescape.supports.FunSpecSpringbootTest -import com.sangdol.roomescape.supports.UserFixture -import com.sangdol.roomescape.supports.runExceptionTest -import com.sangdol.roomescape.supports.runTest -import com.sangdol.roomescape.user.business.SIGNUP -import com.sangdol.roomescape.user.exception.UserErrorCode -import com.sangdol.roomescape.user.infrastructure.persistence.* -import com.sangdol.roomescape.user.web.MIN_PASSWORD_LENGTH -import com.sangdol.roomescape.user.web.UserCreateRequest class UserApiTest( private val userRepository: UserRepository, diff --git a/service/src/test/resources/application-test.yaml b/service/src/test/resources/application-test.yaml index ae8ee583..c4e369dc 100644 --- a/service/src/test/resources/application-test.yaml +++ b/service/src/test/resources/application-test.yaml @@ -10,7 +10,7 @@ spring: ddl-auto: validate datasource: hikari: - jdbc-url: jdbc:mysql://localhost:23306/roomescape_local + jdbc-url: jdbc:mysql://localhost:23306/roomescape_local?useLegacyDatetimeCode=false&serverTimezone=UTC driver-class-name: com.mysql.cj.jdbc.Driver username: root password: init @@ -44,4 +44,9 @@ jdbc: management: tracing: sampling: - probability: 1 \ No newline at end of file + probability: 1 + +slow-query: + logger-name: test-slow-query-logger + log-level: info + threshold-ms: 5 diff --git a/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/business/TosspayService.kt b/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/business/TosspayService.kt index 4aa64069..f08b55b9 100644 --- a/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/business/TosspayService.kt +++ b/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/business/TosspayService.kt @@ -23,18 +23,18 @@ class TosspayService( ) { fun confirm(request: PaymentConfirmRequest): PaymentResponse { - log.info { "[TosspayService.confirm] 결제 확정 시작: paymentKey=${request.paymentKey}, amount=${request.amount}" } + log.info { "[confirm] 결제 확정 시작: paymentKey=${request.paymentKey}, amount=${request.amount}" } val payment = choosePayment(request).also { saveAmount(request.paymentKey, it) } return payment.toResponse() .also { - log.info { "[TosspayService.confirm] 결제 확정 완료: paymentKey=${request.paymentKey}, amount=${request.amount}" } + log.info { "[confirm] 결제 확정 완료: paymentKey=${request.paymentKey}, amount=${request.amount}" } } } fun cancel(paymentKey: String, request: PaymentCancelRequest): PaymentResponse { - log.info { "[TosspayService.cancel] 결제 취소 시작: paymentKey=${paymentKey}" } + log.info { "[cancel] 결제 취소 시작: paymentKey=${paymentKey}" } val orderAmount = orderAmountRepository.findByPaymentKey(paymentKey) ?: throw TosspayException(TosspayCancelErrorCode.NOT_FOUND_PAYMENT) @@ -50,12 +50,12 @@ class TosspayService( return Payment.randomForCancellation(paymentKey, cancellation) .toResponse() .also { - log.info { "[TosspayService.cancel] 결제 취소 완료: paymentKey=${paymentKey}" } + log.info { "[cancel] 결제 취소 완료: paymentKey=${paymentKey}" } } } private fun choosePayment(request: PaymentConfirmRequest): Payment { - log.info { "[TosspayService.choosePayment] 랜덤 결제 정보 생성 시작: paymentKey=${request.paymentKey}, amount=${request.amount}" } + log.info { "[choosePayment] 랜덤 결제 정보 생성 시작: paymentKey=${request.paymentKey}, amount=${request.amount}" } val randomValue = Math.random() // 70%는 간편결제에 배정 diff --git a/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/business/domain/Payment.kt b/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/business/domain/Payment.kt index 004b66c6..0f581fe9 100644 --- a/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/business/domain/Payment.kt +++ b/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/business/domain/Payment.kt @@ -1,5 +1,6 @@ package com.sangdol.tosspaymock.business.domain +import com.sangdol.common.utils.KoreaDateTime import com.sangdol.tosspaymock.business.domain.cancel.Cancellation import com.sangdol.tosspaymock.business.domain.card.Card import com.sangdol.tosspaymock.business.domain.easypay.Easypay @@ -46,7 +47,7 @@ class Payment( val version: String = "2022-11-16", val metadata: String? = null ) { - companion object { + companion object { fun randomWithCard( paymentKey: String, orderId: String, @@ -128,7 +129,7 @@ class Payment( return create( paymentKey = paymentKey, orderId = "orderId", - requestedAt = OffsetDateTime.now(), + requestedAt = KoreaDateTime.nowWithOffset(), amount = cancellation.cancelAmount, card = null, easyPay = null, @@ -165,7 +166,7 @@ class Payment( orderId = orderId, orderName = "테스트 결제", requestedAt = requestedAt, - approvedAt = OffsetDateTime.now(), + approvedAt = KoreaDateTime.nowWithOffset(), card = card, easyPay = easyPay, transfer = bankTransfer, @@ -178,7 +179,6 @@ class Payment( } } - fun toResponse() = PaymentResponse( mid = this.mid, lastTransactionKey = this.lastTransactionKey, diff --git a/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/business/domain/cancel/Cancellation.kt b/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/business/domain/cancel/Cancellation.kt index edd2e389..a237ec89 100644 --- a/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/business/domain/cancel/Cancellation.kt +++ b/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/business/domain/cancel/Cancellation.kt @@ -1,5 +1,6 @@ package com.sangdol.tosspaymock.business.domain.cancel +import com.sangdol.common.utils.KoreaDateTime import com.sangdol.tosspaymock.business.domain.RandomPaymentValueGenerator import com.sangdol.tosspaymock.web.dto.CancelResponse import java.time.OffsetDateTime @@ -29,7 +30,7 @@ class Cancellation( ) = Cancellation( transactionKey = RandomPaymentValueGenerator.transactionKey(), cancelReason = cancelReason, - canceledAt = OffsetDateTime.now(), + canceledAt = KoreaDateTime.nowWithOffset(), cardDiscountAmount = cardDiscountAmount, transferDiscountAmount = transferDiscountAmount, easyPayDiscountAmount = easyPayDiscountAmount, diff --git a/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/business/domain/card/Card.kt b/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/business/domain/card/Card.kt index 4bb0956b..0aee6e79 100644 --- a/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/business/domain/card/Card.kt +++ b/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/business/domain/card/Card.kt @@ -38,7 +38,6 @@ class Card( } } - fun toResponse() = CardResponse( issuerCode = this.issuerCode.code, acquirerCode = this.acquirerCode.code, diff --git a/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/exception/code/TosspayCancelErrorCode.kt b/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/exception/code/TosspayCancelErrorCode.kt index ed1fb31d..384486a4 100644 --- a/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/exception/code/TosspayCancelErrorCode.kt +++ b/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/exception/code/TosspayCancelErrorCode.kt @@ -25,14 +25,26 @@ enum class TosspayCancelErrorCode( NOT_CANCELABLE_PAYMENT(HttpStatus.FORBIDDEN, "CO016", "취소 할 수 없는 결제 입니다."), EXCEED_MAX_REFUND_DUE(HttpStatus.FORBIDDEN, "CO017", "환불 가능한 기간이 지났습니다."), NOT_ALLOWED_PARTIAL_REFUND_WAITING_DEPOSIT(HttpStatus.FORBIDDEN, "CO018", "입금 대기중인 결제는 부분 환불이 불가합니다."), - NOT_ALLOWED_PARTIAL_REFUND(HttpStatus.FORBIDDEN, "CO019", "에스크로 주문, 현금 카드 결제일 때는 부분 환불이 불가합니다. 이외 다른 결제 수단에서 부분 취소가 되지 않을 때는 토스페이먼츠에 문의해 주세요."), + NOT_ALLOWED_PARTIAL_REFUND( + HttpStatus.FORBIDDEN, + "CO019", + "에스크로 주문, 현금 카드 결제일 때는 부분 환불이 불가합니다. 이외 다른 결제 수단에서 부분 취소가 되지 않을 때는 토스페이먼츠에 문의해 주세요." + ), NOT_AVAILABLE_BANK(HttpStatus.FORBIDDEN, "CO020", "은행 서비스 시간이 아닙니다."), INCORRECT_BASIC_AUTH_FORMAT(HttpStatus.FORBIDDEN, "CO021", "잘못된 요청입니다. ':' 를 포함해 인코딩해주세요."), NOT_CANCELABLE_PAYMENT_FOR_DORMANT_USER(HttpStatus.FORBIDDEN, "CO022", "휴면 처리된 회원의 결제는 취소할 수 없습니다."), NOT_FOUND_PAYMENT(HttpStatus.NOT_FOUND, "CO023", "존재하지 않는 결제 정보 입니다."), - FAILED_INTERNAL_SYSTEM_PROCESSING(HttpStatus.INTERNAL_SERVER_ERROR, "CO024", "내부 시스템 처리 작업이 실패했습니다. 잠시 후 다시 시도해주세요."), + FAILED_INTERNAL_SYSTEM_PROCESSING( + HttpStatus.INTERNAL_SERVER_ERROR, + "CO024", + "내부 시스템 처리 작업이 실패했습니다. 잠시 후 다시 시도해주세요." + ), FAILED_REFUND_PROCESS(HttpStatus.INTERNAL_SERVER_ERROR, "CO025", "은행 응답시간 지연이나 일시적인 오류로 환불요청에 실패했습니다."), - FAILED_METHOD_HANDLING_CANCEL(HttpStatus.INTERNAL_SERVER_ERROR, "CO026", "취소 중 결제 시 사용한 결제 수단 처리과정에서 일시적인 오류가 발생했습니다."), + FAILED_METHOD_HANDLING_CANCEL( + HttpStatus.INTERNAL_SERVER_ERROR, + "CO026", + "취소 중 결제 시 사용한 결제 수단 처리과정에서 일시적인 오류가 발생했습니다." + ), FAILED_PARTIAL_REFUND(HttpStatus.INTERNAL_SERVER_ERROR, "CO027", "은행 점검, 해약 계좌 등의 사유로 부분 환불이 실패했습니다."), COMMON_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "CO028", "일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요."), FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING(HttpStatus.INTERNAL_SERVER_ERROR, "CO029", "결제가 완료되지 않았어요. 다시 시도해주세요."); diff --git a/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/exception/code/TosspayConfirmErrorCode.kt b/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/exception/code/TosspayConfirmErrorCode.kt index 253f8852..d04f2941 100644 --- a/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/exception/code/TosspayConfirmErrorCode.kt +++ b/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/exception/code/TosspayConfirmErrorCode.kt @@ -25,9 +25,17 @@ enum class TosspayConfirmErrorCode( NOT_FOUND_TERMINAL_ID(HttpStatus.BAD_REQUEST, "CO016", "단말기번호(Terminal Id)가 없습니다. 토스페이먼츠로 문의 바랍니다."), INVALID_AUTHORIZE_AUTH(HttpStatus.BAD_REQUEST, "CO017", "유효하지 않은 인증 방식입니다."), INVALID_CARD_LOST_OR_STOLEN(HttpStatus.BAD_REQUEST, "CO018", "분실 혹은 도난 카드입니다."), - RESTRICTED_TRANSFER_ACCOUNT(HttpStatus.BAD_REQUEST, "CO019", "계좌는 등록 후 12시간 뒤부터 결제할 수 있습니다. 관련 정책은 해당 은행으로 문의해주세요."), + RESTRICTED_TRANSFER_ACCOUNT( + HttpStatus.BAD_REQUEST, + "CO019", + "계좌는 등록 후 12시간 뒤부터 결제할 수 있습니다. 관련 정책은 해당 은행으로 문의해주세요." + ), INVALID_CARD_NUMBER(HttpStatus.BAD_REQUEST, "CO020", "카드번호를 다시 확인해주세요."), - INVALID_UNREGISTERED_SUBMALL(HttpStatus.BAD_REQUEST, "CO021", "등록되지 않은 서브몰입니다. 서브몰이 없는 가맹점이라면 안심클릭이나 ISP 결제가 필요합니다."), + INVALID_UNREGISTERED_SUBMALL( + HttpStatus.BAD_REQUEST, + "CO021", + "등록되지 않은 서브몰입니다. 서브몰이 없는 가맹점이라면 안심클릭이나 ISP 결제가 필요합니다." + ), NOT_REGISTERED_BUSINESS(HttpStatus.BAD_REQUEST, "CO022", "등록되지 않은 사업자 번호입니다."), EXCEED_MAX_ONE_DAY_WITHDRAW_AMOUNT(HttpStatus.BAD_REQUEST, "CO023", "1일 출금 한도를 초과했습니다."), EXCEED_MAX_ONE_TIME_WITHDRAW_AMOUNT(HttpStatus.BAD_REQUEST, "CO024", "1회 출금 한도를 초과했습니다."), @@ -48,10 +56,18 @@ enum class TosspayConfirmErrorCode( NOT_AVAILABLE_BANK(HttpStatus.FORBIDDEN, "CO039", "은행 서비스 시간이 아닙니다."), INVALID_PASSWORD(HttpStatus.FORBIDDEN, "CO040", "결제 비밀번호가 일치하지 않습니다."), INCORRECT_BASIC_AUTH_FORMAT(HttpStatus.FORBIDDEN, "CO041", "잘못된 요청입니다. ':' 를 포함해 인코딩해주세요."), - FDS_ERROR(HttpStatus.FORBIDDEN, "CO042", "[토스페이먼츠] 위험거래가 감지되어 결제가 제한됩니다. 발송된 문자에 포함된 링크를 통해 본인인증 후 결제가 가능합니다. (고객센터: 1644-8051)"), + FDS_ERROR( + HttpStatus.FORBIDDEN, + "CO042", + "[토스페이먼츠] 위험거래가 감지되어 결제가 제한됩니다. 발송된 문자에 포함된 링크를 통해 본인인증 후 결제가 가능합니다. (고객센터: 1644-8051)" + ), NOT_FOUND_PAYMENT(HttpStatus.NOT_FOUND, "CO043", "존재하지 않는 결제 정보 입니다."), NOT_FOUND_PAYMENT_SESSION(HttpStatus.NOT_FOUND, "CO044", "결제 시간이 만료되어 결제 진행 데이터가 존재하지 않습니다."), FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING(HttpStatus.INTERNAL_SERVER_ERROR, "CO045", "결제가 완료되지 않았어요. 다시 시도해주세요."), - FAILED_INTERNAL_SYSTEM_PROCESSING(HttpStatus.INTERNAL_SERVER_ERROR, "CO046", "내부 시스템 처리 작업이 실패했습니다. 잠시 후 다시 시도해주세요."), + FAILED_INTERNAL_SYSTEM_PROCESSING( + HttpStatus.INTERNAL_SERVER_ERROR, + "CO046", + "내부 시스템 처리 작업이 실패했습니다. 잠시 후 다시 시도해주세요." + ), UNKNOWN_PAYMENT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "CO047", "결제에 실패했어요. 같은 문제가 반복된다면 은행이나 카드사로 문의해주세요."); } diff --git a/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/web/dto/TosspayRequest.kt b/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/web/dto/TosspayRequest.kt index e38cf0eb..3d4b7df1 100644 --- a/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/web/dto/TosspayRequest.kt +++ b/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/web/dto/TosspayRequest.kt @@ -1,12 +1,13 @@ package com.sangdol.tosspaymock.web.dto +import com.sangdol.common.utils.KoreaDateTime import java.time.OffsetDateTime data class PaymentConfirmRequest( val paymentKey: String, val orderId: String, val amount: Int, - val requestedAt: OffsetDateTime = OffsetDateTime.now() + val requestedAt: OffsetDateTime = KoreaDateTime.nowWithOffset() ) data class PaymentCancelRequest( diff --git a/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/web/dto/TosspayResponse.kt b/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/web/dto/TosspayResponse.kt index 4e8b9bd1..96a68a67 100644 --- a/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/web/dto/TosspayResponse.kt +++ b/tosspay-mock/src/main/kotlin/com/sangdol/tosspaymock/web/dto/TosspayResponse.kt @@ -1,11 +1,6 @@ package com.sangdol.tosspaymock.web.dto -import com.sangdol.tosspaymock.business.domain.Payment import com.sangdol.tosspaymock.business.domain.PaymentType -import com.sangdol.tosspaymock.business.domain.cancel.Cancellation -import com.sangdol.tosspaymock.business.domain.card.Card -import com.sangdol.tosspaymock.business.domain.easypay.Easypay -import com.sangdol.tosspaymock.business.domain.transfer.BankTransfer import java.time.OffsetDateTime data class PaymentResponse( diff --git a/tosspay-mock/src/main/resources/logback-deploy.xml b/tosspay-mock/src/main/resources/logback-deploy.xml index b298ea1d..af3eb673 100644 --- a/tosspay-mock/src/main/resources/logback-deploy.xml +++ b/tosspay-mock/src/main/resources/logback-deploy.xml @@ -82,7 +82,8 @@ 512 - 0 false + 0 + false diff --git a/tosspay-mock/src/main/resources/schema/schema-h2.sql b/tosspay-mock/src/main/resources/schema/schema-h2.sql index 57277e11..d41abd69 100644 --- a/tosspay-mock/src/main/resources/schema/schema-h2.sql +++ b/tosspay-mock/src/main/resources/schema/schema-h2.sql @@ -1,10 +1,20 @@ -create table if not exists order_amount ( - id bigint primary key, - payment_key varchar(255) not null, - approved_amount integer not null, - easypay_discount_amount integer not null, - card_discount_amount integer not null, - transfer_discount_amount integer not null, - - constraint uk_order_amount__payment_key unique (payment_key) -); +create table if not exists order_amount +( + id + bigint + primary + key, + payment_key + varchar +( + 255 +) not null, + approved_amount integer not null, + easypay_discount_amount integer not null, + card_discount_amount integer not null, + transfer_discount_amount integer not null, + constraint uk_order_amount__payment_key unique +( + payment_key +) + ); diff --git a/tosspay-mock/src/test/kotlin/com/sangdol/tosspaymock/parser/origin/TosspayErrorCodeParser.kt b/tosspay-mock/src/test/kotlin/com/sangdol/tosspaymock/parser/origin/TosspayErrorCodeParser.kt index ee5766c6..2e8f8976 100644 --- a/tosspay-mock/src/test/kotlin/com/sangdol/tosspaymock/parser/origin/TosspayErrorCodeParser.kt +++ b/tosspay-mock/src/test/kotlin/com/sangdol/tosspaymock/parser/origin/TosspayErrorCodeParser.kt @@ -4,7 +4,7 @@ import com.sangdol.common.types.web.HttpStatus import io.kotest.core.spec.style.StringSpec import java.io.File -class TosspayErrorCodeParser: StringSpec() { +class TosspayErrorCodeParser : StringSpec() { init { "Tosspay API 문서에 있는 결제 승인 에러 코드 항목을 enum의 형태로 변환한다." { val basePath = "${File("").absolutePath}/src/test/resources" @@ -31,7 +31,8 @@ class TosspayErrorCodeParser: StringSpec() { return regex.replace(text) { matchResult -> val name = matchResult.groupValues[2] - val httpStatus = "HttpStatus.${HttpStatus.entries.first {it.code == matchResult.groupValues[1].trim().toInt()}.name}" + val httpStatus = + "HttpStatus.${HttpStatus.entries.first { it.code == matchResult.groupValues[1].trim().toInt() }.name}" val errorCode = "CO${(idx).toString().padStart(3, '0')}" val koreanMessage = matchResult.groupValues[3].split("\t")[0]