generated from pricelees/issue-pr-template
<!-- 제목 양식 --> <!-- [이슈번호] 작업 요약 (예시: [#10] Gitea 템플릿 생성) --> ## 📝 관련 이슈 및 PR **PR과 관련된 이슈 번호** - #54 ## ✨ 작업 내용 <!-- 어떤 작업을 했는지 알려주세요! --> - 애플리케이션 배포 - 1차 배포에서 각 Service의 Trace가 구분이 되지 않아 XxxService 클래스에 \@Observation을 적용하는 AOP 추가 - 불필요하게 느껴지는 Prometheus Actuator 요청과 스케쥴링 작업 Tracing 제외 - 애플리케이션이 UTC로 배포됨에 따라 발생하는 문제 해결을 위해 LocalDateTime, OffsetDateTime -> Instant 타입 변경 및 LocalDate, LocalTime은 KST로 비교하도록 수정 - 기존 로그의 가독성이 좋지 않아, 로그 메시지가 가장 먼저 보이도록 형식 수정 ## 🧪 테스트 <!-- 어떤 테스트를 생각했고 진행했는지 알려주세요! --> - 실제 웹에 접속하여 전체적인 기능 점검 - 예약 처리 로직에서 미숙한 부분이 발견되어 다음 작업은 예약 처리 로직 개선 예정 ## 📚 참고 자료 및 기타 <!-- 참고한 자료, 또는 논의할 사항이 있다면 알려주세요! --> Reviewed-on: #55 Co-authored-by: pricelees <priceelees@gmail.com> Co-committed-by: pricelees <priceelees@gmail.com>
This commit is contained in:
parent
186d6e118c
commit
8215492eea
25
Dockerfile
25
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"]
|
||||
@ -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()}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<TestAuditingBaseEntity, Long>
|
||||
interface TestAuditingBaseEntityRepository : JpaRepository<TestAuditingBaseEntity, Long>
|
||||
@ -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<TestPersistableBaseEntity, Long>
|
||||
interface TestPersistableBaseEntityRepository : JpaRepository<TestPersistableBaseEntity, Long>
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
@ -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<Any?> { joinPoint.proceed() }
|
||||
}
|
||||
}
|
||||
@ -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<LocalDateTime>() {
|
||||
override fun serialize(
|
||||
value: LocalDateTime,
|
||||
gen: JsonGenerator,
|
||||
serializers: SerializerProvider
|
||||
) {
|
||||
value.atZone(ZoneId.systemDefault())
|
||||
.toOffsetDateTime()
|
||||
.also {
|
||||
gen.writeString(it.format(ISO_OFFSET_DATE_TIME_FORMATTER))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OffsetDateTimeSerializer : JsonSerializer<OffsetDateTime>() {
|
||||
override fun serialize(
|
||||
value: OffsetDateTime,
|
||||
gen: JsonGenerator,
|
||||
serializers: SerializerProvider
|
||||
) {
|
||||
gen.writeString(value.format(ISO_OFFSET_DATE_TIME_FORMATTER))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18,7 +18,10 @@ class WebLogMessageConverter(
|
||||
return objectMapper.writeValueAsString(payload)
|
||||
}
|
||||
|
||||
fun convertToControllerInvokedMessage(servletRequest: HttpServletRequest, controllerPayload: Map<String, Any>): String {
|
||||
fun convertToControllerInvokedMessage(
|
||||
servletRequest: HttpServletRequest,
|
||||
controllerPayload: Map<String, Any>
|
||||
): String {
|
||||
val payload = LogPayloadBuilder(type = LogType.CONTROLLER_INVOKED, servletRequest = servletRequest)
|
||||
.endpoint()
|
||||
.principalId()
|
||||
|
||||
@ -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\""
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,6 +1,18 @@
|
||||
node_modules
|
||||
.git
|
||||
.DS_Store
|
||||
.gitignore
|
||||
|
||||
# Node.js
|
||||
node_modules
|
||||
npm-debug.log
|
||||
dist
|
||||
|
||||
# Build output
|
||||
build
|
||||
dist
|
||||
|
||||
# Editor/OS specific
|
||||
.vscode
|
||||
.idea
|
||||
.DS_Store
|
||||
|
||||
# Environment variables
|
||||
.env*
|
||||
@ -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;"]
|
||||
@ -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 <div>Loading...</div>; // 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 <Navigate to="/login" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
if (role !== 'ADMIN') {
|
||||
// Logged in but not an admin, show alert and redirect.
|
||||
alert('접근 권한이 없어요. 관리자에게 문의해주세요.');
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default AdminRoute;
|
||||
@ -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<EditingSchedule | null>(null);
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedThemeDetails, setSelectedThemeDetails] = useState<ThemeInfoResponse | null>(null);
|
||||
const [isLoadingThemeDetails, setIsLoadingThemeDetails] = useState<boolean>(false);
|
||||
const [selectedThemeDetails] = useState<ThemeInfoResponse | null>(null);
|
||||
const [isLoadingThemeDetails] = useState<boolean>(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
@ -332,10 +333,10 @@ const AdminSchedulePage: React.FC = () => {
|
||||
<h4 className="audit-title">감사 정보</h4>
|
||||
<div className="audit-body">
|
||||
<p>
|
||||
<strong>생성일:</strong> {new Date(detailedSchedules[schedule.id].audit!.createdAt).toLocaleString()}
|
||||
<strong>생성일:</strong> {formatDisplayDateTime(detailedSchedules[schedule.id].audit!.createdAt)}
|
||||
</p>
|
||||
<p>
|
||||
<strong>수정일:</strong> {new Date(detailedSchedules[schedule.id].audit!.updatedAt).toLocaleString()}
|
||||
<strong>수정일:</strong> {formatDisplayDateTime(detailedSchedules[schedule.id].audit!.updatedAt)}
|
||||
</p>
|
||||
<p>
|
||||
<strong>생성자:</strong> {detailedSchedules[schedule.id].audit!.createdBy.name}({detailedSchedules[schedule.id].audit!.createdBy.id})
|
||||
|
||||
@ -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<SimpleStoreResponse[]>([]);
|
||||
@ -297,10 +298,10 @@ const AdminStorePage: React.FC = () => {
|
||||
코드:</strong> {detailedStores[store.id].region.code}
|
||||
</p>
|
||||
<p>
|
||||
<strong>생성일:</strong> {new Date(detailedStores[store.id].audit.createdAt).toLocaleString()}
|
||||
<strong>생성일:</strong> {formatDisplayDateTime(detailedStores[store.id].audit.createdAt)}
|
||||
</p>
|
||||
<p>
|
||||
<strong>수정일:</strong> {new Date(detailedStores[store.id].audit.updatedAt).toLocaleString()}
|
||||
<strong>수정일:</strong> {formatDisplayDateTime(detailedStores[store.id].audit.updatedAt)}
|
||||
</p>
|
||||
<p>
|
||||
<strong>생성자:</strong> {detailedStores[store.id].audit.createdBy.name}({detailedStores[store.id].audit.createdBy.id})
|
||||
|
||||
@ -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 = () => {
|
||||
<div className="audit-info">
|
||||
<h4 className="audit-title">감사 정보</h4>
|
||||
<div className="audit-body">
|
||||
<p><strong>생성일:</strong> {new Date(auditInfo.createdAt).toLocaleString()}</p>
|
||||
<p><strong>수정일:</strong> {new Date(auditInfo.updatedAt).toLocaleString()}</p>
|
||||
<p><strong>생성일:</strong> {formatDisplayDateTime(auditInfo.createdAt)}</p>
|
||||
<p><strong>수정일:</strong> {formatDisplayDateTime(auditInfo.updatedAt)}</p>
|
||||
<p><strong>생성자:</strong> {auditInfo.createdBy.name}</p>
|
||||
<p><strong>수정자:</strong> {auditInfo.updatedBy.name}</p>
|
||||
</div>
|
||||
|
||||
@ -19,7 +19,6 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
|
||||
|
||||
@ -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<String>) {
|
||||
System.setProperty("user.timezone", "UTC")
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
|
||||
|
||||
val springApplication = SpringApplication(RoomescapeApplication::class.java)
|
||||
springApplication.setBannerMode(Banner.Mode.OFF)
|
||||
springApplication.run()
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {}
|
||||
|
||||
|
||||
@ -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()
|
||||
)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}" }
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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<Pair<String, String>> = 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<Pair<String, String>> = 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<ReservationEntity> = 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)
|
||||
|
||||
@ -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 {}
|
||||
|
||||
|
||||
@ -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}개의 예약 및 일정 처리 완료" }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<ReservationEntity, Long> {
|
||||
|
||||
fun findAllByUserIdAndStatusIsIn(userId: Long, statuses: List<ReservationStatus>): List<ReservationEntity>
|
||||
|
||||
@Modifying
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
UPDATE
|
||||
reservation r
|
||||
JOIN
|
||||
@ -23,6 +24,7 @@ interface ReservationRepository : JpaRepository<ReservationEntity, Long> {
|
||||
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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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?,
|
||||
)
|
||||
|
||||
|
||||
@ -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<ScheduleOverview> =
|
||||
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<ScheduleOverview> =
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}"
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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?,
|
||||
|
||||
@ -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<ScheduleEntity, Long> {
|
||||
|
||||
@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<ScheduleEntity, Long> {
|
||||
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<ScheduleEntity, Long> {
|
||||
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<ScheduleEntity, Long> {
|
||||
)
|
||||
"""
|
||||
)
|
||||
fun releaseExpiredHolds(@Param("now") now: LocalDateTime): Int
|
||||
fun releaseExpiredHolds(@Param("now") now: Instant): Int
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<ThemeEntity, Long> {
|
||||
|
||||
@ -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}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -22,7 +22,7 @@ class UserEntity(
|
||||
|
||||
@Enumerated(value = EnumType.STRING)
|
||||
var status: UserStatus
|
||||
): AuditingBaseEntity(id)
|
||||
) : AuditingBaseEntity(id)
|
||||
|
||||
@Entity
|
||||
@Table(name = "user_status_history")
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -6,20 +6,6 @@
|
||||
<appender name="CONSOLE_JSON" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
|
||||
<providers>
|
||||
<timestamp>
|
||||
<fieldName>timestamp</fieldName>
|
||||
<timeZone>UTC</timeZone>
|
||||
</timestamp>
|
||||
<logLevel>
|
||||
<fieldName>level</fieldName>
|
||||
</logLevel>
|
||||
<loggerName>
|
||||
<fieldName>logger</fieldName>
|
||||
</loggerName>
|
||||
<threadName>
|
||||
<fieldName>thread</fieldName>
|
||||
</threadName>
|
||||
<mdc/>
|
||||
<pattern>
|
||||
<pattern>
|
||||
{
|
||||
@ -27,6 +13,10 @@
|
||||
}
|
||||
</pattern>
|
||||
</pattern>
|
||||
<loggerName>
|
||||
<fieldName>logger</fieldName>
|
||||
</loggerName>
|
||||
<mdc/>
|
||||
<stackTrace>
|
||||
<fieldName>stack_trace</fieldName>
|
||||
<throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
|
||||
@ -35,6 +25,16 @@
|
||||
<rootCauseFirst>true</rootCauseFirst>
|
||||
</throwableConverter>
|
||||
</stackTrace>
|
||||
<logLevel>
|
||||
<fieldName>level</fieldName>
|
||||
</logLevel>
|
||||
<threadName>
|
||||
<fieldName>thread</fieldName>
|
||||
</threadName>
|
||||
<timestamp>
|
||||
<fieldName>timestamp</fieldName>
|
||||
<timeZone>UTC</timeZone>
|
||||
</timestamp>
|
||||
</providers>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
@ -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<Long> = transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
|
||||
val stores: List<StoreEntity> = 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<Array<Any>>()
|
||||
|
||||
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<Pair<Long, Long>> = getStoreWithManagers()
|
||||
val themes: List<Triple<Long, Short, LocalDateTime>> = getThemes()
|
||||
val maxAvailableMinutes = themes.maxOf { it.second.toInt() }
|
||||
val scheduleCountPerDay = 5
|
||||
|
||||
val themes: List<ThemeEntity> = getThemes()
|
||||
val maxScheduleCountPerDay = 10
|
||||
val startTime = LocalTime.of(10, 0)
|
||||
var lastTime = startTime
|
||||
val times = mutableListOf<LocalTime>()
|
||||
|
||||
repeat(scheduleCountPerDay) {
|
||||
val themeWithTimes: Map<ThemeEntity, List<LocalTime>> = themes.associateWith { theme ->
|
||||
val times = mutableListOf<LocalTime>()
|
||||
val themeAvailableMinutes = theme.availableMinutes
|
||||
var lastTime = startTime
|
||||
|
||||
while (times.size <= maxScheduleCountPerDay && lastTime.hour in (10..23)) {
|
||||
times.add(lastTime)
|
||||
lastTime = lastTime.plusMinutes(maxAvailableMinutes.toLong() + 10L)
|
||||
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<Pair<Long, Long>> = getStoreWithManagers()
|
||||
// val availableThemes: List<ThemeEntity> = 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<Long, Short, LocalDateTime>,
|
||||
stores: List<Pair<Long, Long>>,
|
||||
times: List<LocalTime>
|
||||
store: Pair<Long, Long>,
|
||||
themeWithTimes: Map<ThemeEntity, List<LocalTime>>
|
||||
) {
|
||||
val sql = """
|
||||
INSERT INTO schedule (
|
||||
@ -457,24 +481,33 @@ class ScheduleDataInitializer : AbstractDataInitializer() {
|
||||
|
||||
val batchArgs = mutableListOf<Array<Any>>()
|
||||
|
||||
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<Triple<Long, Short, LocalDateTime>> {
|
||||
private fun getThemes(): List<ThemeEntity> {
|
||||
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<List<ScheduleWithThemeParticipants>> = 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<PaymentWithMethods> = 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() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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("시/도") {
|
||||
|
||||
@ -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("정상 응답") {
|
||||
|
||||
@ -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)!!) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,7 +498,8 @@ class AdminScheduleApiTest(
|
||||
}
|
||||
|
||||
context("정상 응답") {
|
||||
test("시간만 변경한다.") {
|
||||
context("시간만 변경한다.") {
|
||||
test("성공") {
|
||||
val (admin, token) = testAuthUtil.defaultStoreAdminLogin()
|
||||
val schedule = initialize("수정을 위한 일정 생성") {
|
||||
dummyInitializer.createSchedule()
|
||||
@ -516,6 +525,26 @@ class AdminScheduleApiTest(
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
test("상태만 변경한다.") {
|
||||
val (admin, token) = testAuthUtil.defaultStoreAdminLogin()
|
||||
val schedule = initialize("수정을 위한 일정 생성") {
|
||||
|
||||
@ -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"
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 / 이름을 조회한다.") {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user