Compare commits

...

39 Commits

Author SHA1 Message Date
c9fa802cbc test: PaymentType enum 테스트 추가 2025-09-29 22:00:45 +09:00
51ad28ddca test: TransactionExecutionUtil 테스트 추가 2025-09-29 15:43:58 +09:00
ddab3b18a6 refactor: TransactionExecutionUtil의 nullable 반환타입 변경으로 인한 기존 코드 수정 2025-09-29 15:43:48 +09:00
83a5919e9c refactor: TransactionExecutionUtil 반환타입 Nullable 수정 2025-09-29 15:43:35 +09:00
9b6bb91095 remove: API 수정으로 인해 미사용되는 타입 제거 2025-09-28 18:21:48 +09:00
463f930b93 feat: 누락된 settings.gradle 추가 2025-09-28 18:02:06 +09:00
987b30b7b8 refactor: Interceptor 등 서비스에서 이동하는 타입은 common -> service로 이동 2025-09-28 13:59:53 +09:00
54f3c042ae rename: 클래스명 수정 (ProxyDataSourceConfig -> SlowQueryLoggerConfig) 2025-09-28 13:49:09 +09:00
85b318e4be refactor: 테마 도메인에서만 사용하는 DateUtils 패키지 이전 (common -> theme) 2025-09-28 13:42:48 +09:00
14d68bb4fb refactor: 이전 CommonAuth 타입 분리를 기존 클래스에 반영 2025-09-28 13:40:39 +09:00
df5abf5cd4 refactor: 기존 service/common에 있는 CommonAuth.kt의 타입 분리 2025-09-28 13:39:43 +09:00
1acad03e7b refactor: 서비스에서의 로그 관련 설정 클래스 패키지 이동 2025-09-28 13:24:33 +09:00
888a38c156 refactor: TransactionExecutionUtil 모듈 이동(service -> persistence) 2025-09-28 13:24:16 +09:00
30eb2e3b03 chore: 초기 데이터 추가를 위한 테스트 클래스 패키지 이동 2025-09-28 13:16:15 +09:00
1cbece032f refactor: 기존 로깅 필터, AOP, 예외 핸들러에 새로 정의된 WebLogMessageConverter 반영 2025-09-28 13:15:54 +09:00
eeb87e1bc3 feat: LogPayloadBuilder를 기반으로 기존의 ApiLogMessageConverter 클래스 개선 2025-09-28 13:14:52 +09:00
6cd269e772 feat: 기존 웹 로그에서 사용하던 payload map을 빌더 형식으로 만드는 클래스 및 테스트 추가 2025-09-28 13:14:12 +09:00
be19e57b61 feat: StartTime 기록용 MDC 유틸 및 테스트 추가 2025-09-28 13:12:47 +09:00
9c4d75be2e refactor: config -> web 서브모듈 이름 변경 및 이로 인한 log 모듈에서의 패키지명 수정(config -> constant) 2025-09-28 13:07:09 +09:00
152367cafb refactor: 모든 모듈의 dependencies 정리 및 jar 설정 지정 2025-09-28 13:04:05 +09:00
33406fbc93 refactor: 기존 log 관련 클래스 모듈 분리 및 일부 클래스는 재사용성 고려 리팩터링 2025-09-27 22:31:56 +09:00
51a0dab2b4 refactor: 기존 log 관련 클래스 모듈 분리 및 일부 클래스는 재사용성 고려 리팩터링 2025-09-27 22:31:41 +09:00
7c52460ac6 refactor: CommonApiResponse 및 Audit 타입 모듈 이전으로 인한 기존 코드 반영 2025-09-27 21:04:56 +09:00
288b67518e refactor: CommonApiResponse 및 Audit 타입 모듈 이전 2025-09-27 21:04:39 +09:00
715a0f979a refactor: 커스텀 예외 타입 공통 모듈 이전으로 인한 기존 테스트 코드 수정 2025-09-27 20:48:28 +09:00
a7b3636410 refactor: 커스텀 예외 타입 공통 모듈 이전으로 인한 기존 프로덕션 코드 수정 2025-09-27 20:48:17 +09:00
00359f63d0 refactor: 커스텀 예외 타입 공통 모듈 이전 2025-09-27 20:47:41 +09:00
5b3f2f929b feat: common.types 모듈에서 사용할 스프링 독립 HttpStatus 타입 정의 2025-09-27 20:46:59 +09:00
eada35f1ee refactor: common.utils에 분리된 MDC Util 서비스 반영 2025-09-27 20:27:58 +09:00
81572246d2 refactor: common.persistence 모듈 분리로 인한 기존 테스트 코드 수정 2025-09-27 20:19:00 +09:00
07869020be refactor: common.persistence 모듈 분리로 인한 기존 서비스 코드 수정 2025-09-27 20:18:50 +09:00
5b31672ebb refactor: kotest 의존성은 공통 모듈 설정으로 이동 2025-09-27 20:18:17 +09:00
89eeefbf0c refactor: 분리한 common.persistence 모듈 테스트 코드 추가 2025-09-27 20:17:58 +09:00
4f0bbe096e refactor: JPA Audit 및 ID Generator 설정 모듈 이전(service -> common.persistence) 2025-09-27 20:17:36 +09:00
c524cc6fdf refactor: 새로 분리된 persistence 모듈에 기존 TsidFactory 추상화 및 재정의 2025-09-27 20:16:33 +09:00
430630a02b refactor: MDC 유틸 모듈 이동(service -> common.utils) 2025-09-27 20:15:54 +09:00
ab84b329fd refactor: BaseEntity 모듈 분리 및 기존 서비스 적용 2025-09-27 20:15:20 +09:00
2506105c56 refactor: JacksonConfig 모듈 분리 2025-09-27 18:53:44 +09:00
bcaffb6718 refactor: 메인 서비스 모듈 분리 2025-09-27 18:53:32 +09:00
203 changed files with 2383 additions and 1608 deletions

View File

@ -1,87 +1,45 @@
import org.jetbrains.kotlin.gradle.plugin.KaptExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
val springBootVersion = "3.5.3" val springBootVersion = "3.5.3"
val kotlinVersion = "2.2.0" val kotlinVersion = "2.2.0"
id("org.springframework.boot") version springBootVersion id("io.spring.dependency-management") version "1.1.7" apply false
id("io.spring.dependency-management") version "1.1.7" id("org.springframework.boot") version springBootVersion apply false
kotlin("jvm") version kotlinVersion kotlin("jvm") version kotlinVersion apply false
kotlin("plugin.spring") version kotlinVersion kotlin("kapt") version kotlinVersion apply false
kotlin("plugin.jpa") version kotlinVersion kotlin("plugin.spring") version kotlinVersion apply false
kotlin("kapt") version kotlinVersion kotlin("plugin.jpa") version kotlinVersion apply false
} }
group = "com.sangdol" group = "com.sangdol"
version = "0.0.1-SNAPSHOT" version = "0.0.1-SNAPSHOT"
java { allprojects {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
tasks.jar {
enabled = false
}
kapt {
keepJavacAnnotationProcessors = true
}
repositories { repositories {
mavenCentral() mavenCentral()
} }
}
subprojects {
apply(plugin = "org.jetbrains.kotlin.jvm")
apply(plugin = "org.jetbrains.kotlin.kapt")
apply(plugin = "io.spring.dependency-management")
extensions.configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
extensions.configure<KaptExtension> {
keepJavacAnnotationProcessors = true
}
dependencies { dependencies {
// Spring add("implementation", "io.github.oshai:kotlin-logging-jvm:7.0.3")
implementation("org.springframework.boot:spring-boot-starter-web") add("implementation", "io.kotest:kotest-runner-junit5:5.9.1")
implementation("org.springframework.boot:spring-boot-starter-data-jpa") add("implementation", "ch.qos.logback:logback-classic:1.5.18")
implementation("org.springframework.boot:spring-boot-starter-validation")
// API docs
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9")
// DB
implementation("com.github.f4b6a3:tsid-creator:5.2.6")
runtimeOnly("com.h2database:h2")
runtimeOnly("com.mysql:mysql-connector-j")
// Jwt
implementation("io.jsonwebtoken:jjwt:0.12.6")
// Logging
implementation("io.github.oshai:kotlin-logging-jvm:7.0.3")
implementation("net.logstash.logback:logstash-logback-encoder:8.1")
implementation("com.github.loki4j:loki-logback-appender:2.0.0")
implementation("net.ttddyy.observation:datasource-micrometer-spring-boot:1.1.1")
// Observability
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-tracing-bridge-otel")
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
// Kotlin
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.github.oshai:kotlin-logging-jvm:7.0.3")
// Test
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.mockk:mockk:1.14.4")
testImplementation("com.ninja-squad:springmockk:4.0.2")
// Kotest
testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.3.0")
// RestAssured
testImplementation("io.rest-assured:rest-assured:5.5.5")
testImplementation("io.rest-assured:kotlin-extensions:5.5.5")
// etc
implementation("org.apache.poi:poi-ooxml:5.2.3")
} }
tasks.withType<Test> { tasks.withType<Test> {
@ -97,3 +55,4 @@ tasks.withType<KotlinCompile> {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
} }
} }
}

View File

@ -0,0 +1,12 @@
dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.20.0")
implementation("net.ttddyy.observation:datasource-micrometer-spring-boot:1.1.2")
testImplementation("io.mockk:mockk:1.14.4")
implementation(project(":common:utils"))
}
tasks.named<Jar>("jar") {
enabled = true
}

View File

@ -0,0 +1,9 @@
package com.sangdol.common.log.constant
enum class LogType {
INCOMING_HTTP_REQUEST,
CONTROLLER_INVOKED,
SUCCEED,
APPLICATION_FAILURE,
UNHANDLED_EXCEPTION
}

View File

@ -1,4 +1,4 @@
package roomescape.common.log package com.sangdol.common.log.message
import ch.qos.logback.classic.pattern.MessageConverter import ch.qos.logback.classic.pattern.MessageConverter
import ch.qos.logback.classic.spi.ILoggingEvent import ch.qos.logback.classic.spi.ILoggingEvent
@ -7,13 +7,14 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.databind.node.TextNode import com.fasterxml.jackson.databind.node.TextNode
import roomescape.common.config.JacksonConfig
private const val MASK: String = "****" abstract class AbstractLogMaskingConverter(
private val SENSITIVE_KEYS = setOf("password", "accessToken", "phone") val sensitiveKeys: Set<String>,
private val objectMapper: ObjectMapper = JacksonConfig().objectMapper() val objectMapper: ObjectMapper
) : MessageConverter() {
val mask: String = "****"
class RoomescapeLogMaskingConverter : MessageConverter() {
override fun convert(event: ILoggingEvent): String { override fun convert(event: ILoggingEvent): String {
val message: String = event.formattedMessage val message: String = event.formattedMessage
@ -35,13 +36,13 @@ class RoomescapeLogMaskingConverter : MessageConverter() {
.toString() .toString()
private fun maskedPlainMessage(message: String): String { private fun maskedPlainMessage(message: String): String {
val keys: String = SENSITIVE_KEYS.joinToString("|") val keys: String = sensitiveKeys.joinToString("|")
val regex = Regex("(?i)($keys)(\\s*=\\s*)([^(,|\"|?)\\s]+)") val regex = Regex("(?i)($keys)(\\s*[:=]\\s*)([^(,|\"|?)]+)")
return regex.replace(message) { matchResult -> return regex.replace(message) { matchResult ->
val key = matchResult.groupValues[1] val key = matchResult.groupValues[1]
val delimiter = matchResult.groupValues[2] val delimiter = matchResult.groupValues[2]
val maskedValue = maskValue(matchResult.groupValues[3]) val maskedValue = maskValue(matchResult.groupValues[3].trim())
"${key}${delimiter}${maskedValue}" "${key}${delimiter}${maskedValue}"
} }
@ -51,7 +52,7 @@ class RoomescapeLogMaskingConverter : MessageConverter() {
node?.forEachEntry { key, childNode -> node?.forEachEntry { key, childNode ->
when { when {
childNode.isValueNode -> { childNode.isValueNode -> {
if (key in SENSITIVE_KEYS) (node as ObjectNode).put(key, maskValue(childNode.asText())) if (key in sensitiveKeys) (node as ObjectNode).put(key, maskValue(childNode.asText()))
} }
childNode.isObject -> maskRecursive(childNode) childNode.isObject -> maskRecursive(childNode)
@ -72,10 +73,6 @@ class RoomescapeLogMaskingConverter : MessageConverter() {
} }
private fun maskValue(value: String): String { private fun maskValue(value: String): String {
return if (value.length <= 2) { return "${value.first()}$mask${value.last()}"
MASK
} else {
"${value.first()}$MASK${value.last()}"
}
} }
} }

View File

@ -1,4 +1,4 @@
package roomescape.common.log package com.sangdol.common.log.sql
import net.ttddyy.dsproxy.ExecutionInfo import net.ttddyy.dsproxy.ExecutionInfo
import net.ttddyy.dsproxy.QueryInfo import net.ttddyy.dsproxy.QueryInfo

View File

@ -0,0 +1,20 @@
package com.sangdol.common.log.sql
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder
import javax.sql.DataSource
object SlowQueryDataSourceFactory {
fun create(dataSource: DataSource, loggerName: String, logLevel: String, thresholdMs: Long): DataSource {
val mdcAwareListener = MDCAwareSlowQueryListenerWithoutParams(
logLevel = SLF4JLogLevel.nullSafeValueOf(logLevel.uppercase()),
thresholdMs = thresholdMs
)
return ProxyDataSourceBuilder.create(dataSource)
.name(loggerName)
.listener(mdcAwareListener)
.buildProxy()
}
}

View File

@ -0,0 +1,55 @@
package com.sangdol.common.log
import ch.qos.logback.classic.spi.ILoggingEvent
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.sangdol.common.log.message.AbstractLogMaskingConverter
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
class TestLogMaskingConverter : AbstractLogMaskingConverter(
sensitiveKeys = setOf("account", "address"),
objectMapper = jacksonObjectMapper()
)
class AbstractLogMaskingConverterTest : FunSpec({
val converter = TestLogMaskingConverter()
val event: ILoggingEvent = mockk()
val account = "sangdol@example.com"
val address = "서울특별시 강북구 수유1동 123-456"
context("sensitiveKeys=${converter.sensitiveKeys}에 있는 항목은 가린다.") {
context("평문 로그를 처리할 때, 여러 key / value가 있는 경우 서로 간의 구분자는 trim 처리한다.") {
listOf(":", "=", " : ", " = ").forEach { keyValueDelimiter ->
listOf(",", ", ").forEach { valueDelimiter ->
test("key1${keyValueDelimiter}value1${valueDelimiter}key2${keyValueDelimiter}value2 형식을 처리한다.") {
every {
event.formattedMessage
} returns "account$keyValueDelimiter$account${valueDelimiter}address$keyValueDelimiter$address"
assertSoftly(converter.convert(event)) {
this shouldBe "account${keyValueDelimiter}${account.first()}${converter.mask}${account.last()}${valueDelimiter}address${keyValueDelimiter}${address.first()}${converter.mask}${address.last()}"
}
}
}
}
}
context("JSON 로그") {
test("정상 처리") {
val json = "{\"request_body\":{\"account\":\"%s\",\"address\":\"%s\"}}"
every {
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()}")
}
}
}
})

View File

@ -1,5 +1,7 @@
package roomescape.common.log package com.sangdol.common.log
import com.sangdol.common.log.sql.SlowQueryPredicate
import com.sangdol.common.log.sql.SqlLogFormatter
import io.kotest.assertions.assertSoftly import io.kotest.assertions.assertSoftly
import io.kotest.core.spec.style.StringSpec import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
@ -18,9 +20,9 @@ class MDCAwareSlowQueryListenerWithoutParamsTest : StringSpec({
val slowQueryPredicate = SlowQueryPredicate(thresholdMs = slowQueryThreshold) val slowQueryPredicate = SlowQueryPredicate(thresholdMs = slowQueryThreshold)
assertSoftly(slowQueryPredicate) { assertSoftly(slowQueryPredicate) {
it.test(slowQueryThreshold) shouldBe true this.test(slowQueryThreshold) shouldBe true
it.test(slowQueryThreshold + 1) shouldBe true this.test(slowQueryThreshold + 1) shouldBe true
it.test(slowQueryThreshold - 1) shouldBe false this.test(slowQueryThreshold - 1) shouldBe false
} }
} }
}) })

View File

@ -0,0 +1,29 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar
plugins {
id("org.springframework.boot")
kotlin("plugin.spring")
kotlin("plugin.jpa")
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("com.github.f4b6a3:tsid-creator:5.2.6")
testRuntimeOnly("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.3.0")
testImplementation("io.mockk:mockk:1.14.4")
implementation(project(":common:utils"))
implementation(project(":common:types"))
}
tasks.named<BootJar>("bootJar") {
enabled = false
}
tasks.named<Jar>("jar") {
enabled = true
}

View File

@ -1,14 +1,14 @@
package roomescape.common.entity package com.sangdol.common.persistence
import jakarta.persistence.* import jakarta.persistence.Column
import jakarta.persistence.EntityListeners
import jakarta.persistence.MappedSuperclass
import org.springframework.data.annotation.CreatedBy import org.springframework.data.annotation.CreatedBy
import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedBy import org.springframework.data.annotation.LastModifiedBy
import org.springframework.data.annotation.LastModifiedDate import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.domain.Persistable
import org.springframework.data.jpa.domain.support.AuditingEntityListener import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.LocalDateTime import java.time.LocalDateTime
import kotlin.jvm.Transient
@MappedSuperclass @MappedSuperclass
@EntityListeners(AuditingEntityListener::class) @EntityListeners(AuditingEntityListener::class)
@ -31,23 +31,3 @@ abstract class AuditingBaseEntity(
@LastModifiedBy @LastModifiedBy
var updatedBy: Long = 0L var updatedBy: Long = 0L
} }
@MappedSuperclass
abstract class PersistableBaseEntity(
@Id
@Column(name = "id")
private val _id: Long,
@Transient
private var isNewEntity: Boolean = true
) : Persistable<Long> {
@PostLoad
@PrePersist
fun markNotNew() {
isNewEntity = false
}
override fun getId(): Long = _id
override fun isNew(): Boolean = isNewEntity
}

View File

@ -0,0 +1,13 @@
package com.sangdol.common.persistence
import com.github.f4b6a3.tsid.TsidFactory
interface IDGenerator {
fun create(): Long
}
class TsidIDGenerator(
private val tsidFactory: TsidFactory
) : IDGenerator {
override fun create(): Long = tsidFactory.create().toLong()
}

View File

@ -0,0 +1,25 @@
package com.sangdol.common.persistence
import jakarta.persistence.*
import org.springframework.data.domain.Persistable
import kotlin.jvm.Transient
@MappedSuperclass
abstract class PersistableBaseEntity(
@Id
@Column(name = "id")
private val _id: Long,
@Transient
private var isNewEntity: Boolean = true
) : Persistable<Long> {
@PostLoad
@PrePersist
fun markNotNew() {
isNewEntity = false
}
override fun getId(): Long = _id
override fun isNew(): Boolean = isNewEntity
}

View File

@ -0,0 +1,44 @@
package com.sangdol.common.persistence
import com.github.f4b6a3.tsid.TsidFactory
import com.sangdol.common.utils.MdcPrincipalIdUtil
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import org.springframework.data.domain.AuditorAware
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
import org.springframework.transaction.PlatformTransactionManager
import java.util.*
@Configuration
@EnableJpaAuditing
class PersistenceConfig {
@Value("\${POD_NAME:app-0}")
private lateinit var podName: String
@Bean
fun auditorAware(): AuditorAware<Long> = MdcAuditorAware()
@Bean
@Primary
fun idGenerator(): IDGenerator {
val node = podName.substringAfterLast("-").toInt()
val tsidFactory = TsidFactory.builder().withNode(node).build()
return TsidIDGenerator(tsidFactory)
}
@Bean
fun transactionExecutionUtil(
transactionManager: PlatformTransactionManager
): TransactionExecutionUtil {
return TransactionExecutionUtil(transactionManager)
}
}
class MdcAuditorAware : AuditorAware<Long> {
override fun getCurrentAuditor(): Optional<Long> = MdcPrincipalIdUtil.extractAsOptionalLongOrEmpty()
}

View File

@ -1,31 +1,19 @@
package roomescape.common.util package com.sangdol.common.persistence
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component
import org.springframework.transaction.PlatformTransactionManager import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.TransactionDefinition import org.springframework.transaction.TransactionDefinition
import org.springframework.transaction.support.TransactionTemplate import org.springframework.transaction.support.TransactionTemplate
import roomescape.common.exception.CommonErrorCode
import roomescape.common.exception.RoomescapeException
private val log: KLogger = KotlinLogging.logger {}
@Component
class TransactionExecutionUtil( class TransactionExecutionUtil(
private val transactionManager: PlatformTransactionManager private val transactionManager: PlatformTransactionManager
) { ) {
fun <T> withNewTransaction(isReadOnly: Boolean, action: () -> T): T { fun <T> withNewTransaction(isReadOnly: Boolean, action: () -> T?): T? {
val transactionTemplate = TransactionTemplate(transactionManager).apply { val transactionTemplate = TransactionTemplate(transactionManager).apply {
this.isReadOnly = isReadOnly this.isReadOnly = isReadOnly
this.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRES_NEW this.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRES_NEW
} }
return transactionTemplate.execute { action() } return transactionTemplate.execute { action() }
?: run {
log.error { "[TransactionExecutionUtil.withNewTransaction] 트랜잭션 작업 중 예상치 못한 null 반환 " }
throw RoomescapeException(CommonErrorCode.UNEXPECTED_SERVER_ERROR)
}
} }
} }

View File

@ -0,0 +1,53 @@
package com.sangdol.common.persistence
import com.sangdol.common.utils.MdcPrincipalIdUtil
import io.kotest.assertions.assertSoftly
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.date.shouldBeBefore
import io.kotest.matchers.equality.shouldBeEqualUsingFields
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import org.springframework.boot.test.context.SpringBootTest
import java.time.LocalDateTime
@SpringBootTest
class BaseEntityTest(
private val testPersistableBaseEntityRepository: TestPersistableBaseEntityRepository,
private val testAuditingBaseEntityRepository: TestAuditingBaseEntityRepository,
private val idGenerator: IDGenerator
) : FunSpec({
context("TestPersistableBaseEntityRepository") {
test("PK를 지정하여 INSERT 쿼리를 한번만 전송한다.") {
val entity = TestPersistableBaseEntity(idGenerator.create(), "hello").also {
testPersistableBaseEntityRepository.saveAndFlush(it)
}
testPersistableBaseEntityRepository.findById(entity.id).also {
it.shouldNotBeNull()
it.get() shouldBeEqualUsingFields entity
}
}
}
context("TestAuditingBaseEntityRepository") {
test("Entity 저장 후 Audit 정보를 확인한다.") {
val id = idGenerator.create()
.also {
MdcPrincipalIdUtil.set(it.toString())
}.also {
testAuditingBaseEntityRepository.saveAndFlush(TestAuditingBaseEntity(it, "hello"))
}
testAuditingBaseEntityRepository.findById(id).also {
it.shouldNotBeNull()
assertSoftly(it.get()) {
this.createdAt shouldBeBefore LocalDateTime.now()
this.updatedAt shouldBeBefore LocalDateTime.now()
this.createdBy shouldBe id
this.updatedBy shouldBe id
}
}
}
}
})

View File

@ -0,0 +1,6 @@
package com.sangdol.common.persistence
import org.springframework.boot.autoconfigure.SpringBootApplication
@SpringBootApplication
class TestApplication

View File

@ -0,0 +1,12 @@
package com.sangdol.common.persistence
import jakarta.persistence.Entity
import org.springframework.data.jpa.repository.JpaRepository
@Entity
class TestAuditingBaseEntity(
id: Long,
val name: String
): AuditingBaseEntity(id)
interface TestAuditingBaseEntityRepository: JpaRepository<TestAuditingBaseEntity, Long>

View File

@ -0,0 +1,12 @@
package com.sangdol.common.persistence
import jakarta.persistence.Entity
import org.springframework.data.jpa.repository.JpaRepository
@Entity
class TestPersistableBaseEntity(
id: Long,
val name: String
): PersistableBaseEntity(id)
interface TestPersistableBaseEntityRepository: JpaRepository<TestPersistableBaseEntity, Long>

View File

@ -0,0 +1,75 @@
package com.sangdol.common.persistence
import io.kotest.assertions.assertSoftly
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 org.junit.jupiter.api.assertThrows
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.TransactionDefinition
import org.springframework.transaction.TransactionStatus
import org.springframework.transaction.support.DefaultTransactionDefinition
class TransactionExecutionUtilTest() : FunSpec() {
private val transactionManager = mockk<PlatformTransactionManager>(relaxed = true)
private val transactionExecutionUtil = TransactionExecutionUtil(transactionManager)
init {
context("withNewTransaction") {
beforeTest {
clearMocks(transactionManager)
}
val body = TestPersistableBaseEntity(123458192L, "hello")
test("지정한 action이 성공하면, 해당 값을 반환하고 트랜잭션을 커밋한다.") {
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
body
}.also {
it.shouldNotBeNull()
it shouldBeEqualUsingFields body
verify { transactionManager.commit(any()) }
verify(exactly = 0) { transactionManager.rollback(any()) }
}
}
test("지정한 action 실행 도중 예외가 발생하면, 예외를 던지고 트랜잭션을 롤백한다.") {
assertThrows<RuntimeException> {
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
throw RuntimeException()
}
}.also {
verify { transactionManager.rollback(any()) }
verify(exactly = 0) { transactionManager.commit(any()) }
}
}
test("isReadOnly=true 지정시 읽기 전용 트랜잭션으로 실행한다.") {
val transactionStatus = mockk<TransactionStatus>(relaxed = true)
val transactionDefinitionSlot = slot<TransactionDefinition>()
every {
transactionManager.getTransaction(capture(transactionDefinitionSlot))
} returns transactionStatus
transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
"hello"
}.also {
assertSoftly(transactionDefinitionSlot.captured) {
this.isReadOnly shouldBe true
this.propagationBehavior shouldBe DefaultTransactionDefinition.PROPAGATION_REQUIRES_NEW
}
verify { transactionManager.commit(any()) }
}
}
}
}
}

View File

@ -0,0 +1,18 @@
spring:
jpa:
properties:
hibernate:
format_sql: true
hibernate:
ddl-auto: create-drop
show-sql: true
h2:
console:
enabled: true
path: /h2-console
datasource:
hikari:
jdbc-url: jdbc:h2:mem:database
driver-class-name: org.h2.Driver
username: sa
password:

View File

@ -0,0 +1,3 @@
tasks.named<Jar>("jar") {
enabled = true
}

View File

@ -1,6 +1,6 @@
package roomescape.common.exception package com.sangdol.common.types.exception
import org.springframework.http.HttpStatus import com.sangdol.common.types.web.HttpStatus
enum class CommonErrorCode( enum class CommonErrorCode(
override val httpStatus: HttpStatus, override val httpStatus: HttpStatus,

View File

@ -1,6 +1,6 @@
package roomescape.common.exception package com.sangdol.common.types.exception
import org.springframework.http.HttpStatus import com.sangdol.common.types.web.HttpStatus
interface ErrorCode { interface ErrorCode {
val httpStatus: HttpStatus val httpStatus: HttpStatus

View File

@ -1,4 +1,4 @@
package roomescape.common.exception package com.sangdol.common.types.exception
open class RoomescapeException( open class RoomescapeException(
open val errorCode: ErrorCode, open val errorCode: ErrorCode,

View File

@ -1,9 +1,7 @@
package roomescape.common.dto.response package com.sangdol.common.types.web
import com.fasterxml.jackson.annotation.JsonInclude import com.sangdol.common.types.exception.ErrorCode
import roomescape.common.exception.ErrorCode
@JsonInclude(JsonInclude.Include.NON_NULL)
data class CommonApiResponse<T>( data class CommonApiResponse<T>(
val data: T? = null, val data: T? = null,
) )

View File

@ -0,0 +1,24 @@
package com.sangdol.common.types.web
enum class HttpStatus(
val code: Int
) {
OK(200),
CREATED(201),
NO_CONTENT(204),
BAD_REQUEST(400),
UNAUTHORIZED(401),
FORBIDDEN(403),
NOT_FOUND(404),
CONFLICT(409),
INTERNAL_SERVER_ERROR(500)
;
fun isClientError(): Boolean {
return code in 400..<500
}
fun value(): Int {
return code
}
}

View File

@ -0,0 +1,7 @@
dependencies {
implementation("org.slf4j:slf4j-api:2.0.17")
}
tasks.named<Jar>("jar") {
enabled = true
}

View File

@ -1,11 +1,10 @@
package roomescape.common.util package com.sangdol.common.utils
import org.slf4j.MDC import org.slf4j.MDC
import java.util.* import java.util.*
private const val MDC_PRINCIPAL_ID_KEY = "principal_id" object MdcPrincipalIdUtil {
const val MDC_PRINCIPAL_ID_KEY = "principal_id"
object MdcPrincipalId {
fun extractAsLongOrNull(): Long? { fun extractAsLongOrNull(): Long? {
return MDC.get(MDC_PRINCIPAL_ID_KEY)?.toLong() return MDC.get(MDC_PRINCIPAL_ID_KEY)?.toLong()

View File

@ -0,0 +1,25 @@
package com.sangdol.common.utils
import org.slf4j.MDC
object MdcStartTimeUtil {
const val MDC_START_TIME_KEY = "start_time"
fun extractDurationMsOrNull(): Long? {
return extractOrNull()?.let { System.currentTimeMillis() - it }
}
fun setCurrentTime() {
extractOrNull() ?: run {
MDC.put(MDC_START_TIME_KEY, System.currentTimeMillis().toString())
}
}
fun clear() {
MDC.remove(MDC_START_TIME_KEY)
}
private fun extractOrNull(): Long? {
return MDC.get(MDC_START_TIME_KEY)?.toLong()
}
}

View File

@ -0,0 +1,28 @@
package com.sangdol.common.utils
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import java.util.*
class MdcPrincipalIdUtilTest : StringSpec({
val id = 1872847943L
"값을 설정한다." {
MdcPrincipalIdUtil.set(id.toString()).also {
MdcPrincipalIdUtil.extractAsLongOrNull() shouldBe id
MdcPrincipalIdUtil.extractAsOptionalLongOrEmpty() shouldBe Optional.of(id)
}
}
"값을 제거한다." {
MdcPrincipalIdUtil.set(id.toString()).also {
MdcPrincipalIdUtil.extractAsLongOrNull() shouldBe id
}
MdcPrincipalIdUtil.clear().also {
MdcPrincipalIdUtil.extractAsLongOrNull() shouldBe null
MdcPrincipalIdUtil.extractAsOptionalLongOrEmpty() shouldBe Optional.empty()
}
}
})

View File

@ -0,0 +1,34 @@
package com.sangdol.common.utils
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
class MdcStartTimeUtilTest : FunSpec({
test("기존에 등록된 startTime 값을 기준으로 duration_ms를 구한다.") {
MdcStartTimeUtil.setCurrentTime()
MdcStartTimeUtil.extractDurationMsOrNull().shouldNotBeNull()
MdcStartTimeUtil.clear()
}
test("기존에 등록된 startTime 값이 없으면 duration_ms는 null이다.") {
MdcStartTimeUtil.extractDurationMsOrNull() shouldBe null
}
test("현재 시간을 등록한다.") {
MdcStartTimeUtil.setCurrentTime()
MdcStartTimeUtil.extractDurationMsOrNull().shouldNotBeNull()
MdcStartTimeUtil.clear()
}
test("등록된 시간을 지운다.") {
MdcStartTimeUtil.setCurrentTime().also {
MdcStartTimeUtil.extractDurationMsOrNull().shouldNotBeNull()
}
MdcStartTimeUtil.clear().also {
MdcStartTimeUtil.extractDurationMsOrNull() shouldBe null
}
}
})

View File

@ -0,0 +1,28 @@
import org.gradle.kotlin.dsl.named
import org.springframework.boot.gradle.tasks.bundling.BootJar
plugins {
id("org.springframework.boot")
kotlin("plugin.spring")
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-aop")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
testImplementation("io.mockk:mockk:1.14.4")
implementation(project(":common:log"))
implementation(project(":common:utils"))
implementation(project(":common:types"))
}
tasks.named<BootJar>("bootJar") {
enabled = false
}
tasks.named<Jar>("jar") {
enabled = true
}

View File

@ -1,5 +1,7 @@
package roomescape.common.log package com.sangdol.common.web.asepct
import com.sangdol.common.log.constant.LogType
import com.sangdol.common.web.support.log.WebLogMessageConverter
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
@ -9,7 +11,6 @@ import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Pointcut import org.aspectj.lang.annotation.Pointcut
import org.aspectj.lang.reflect.MethodSignature import org.aspectj.lang.reflect.MethodSignature
import org.slf4j.MDC
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
@ -21,19 +22,17 @@ private val log: KLogger = KotlinLogging.logger {}
@Aspect @Aspect
class ControllerLoggingAspect( class ControllerLoggingAspect(
private val messageConverter: ApiLogMessageConverter, private val messageConverter: WebLogMessageConverter,
) { ) {
@Pointcut("execution(* roomescape..web..*Controller*.*(..))") @Pointcut("execution(* com.sangdol..web..*Controller*.*(..))")
fun allController() { fun allController() {
} }
@Around("allController()") @Around("allController()")
fun logAPICalls(joinPoint: ProceedingJoinPoint): Any? { fun logAPICalls(joinPoint: ProceedingJoinPoint): Any? {
val startTime: Long = MDC.get("startTime").toLongOrNull() ?: System.currentTimeMillis()
val controllerPayload: Map<String, Any> = parsePayload(joinPoint)
val servletRequest: HttpServletRequest = servletRequest() val servletRequest: HttpServletRequest = servletRequest()
val controllerPayload: Map<String, Any> = parseControllerPayload(joinPoint)
log.info { log.info {
messageConverter.convertToControllerInvokedMessage(servletRequest, controllerPayload) messageConverter.convertToControllerInvokedMessage(servletRequest, controllerPayload)
@ -41,28 +40,21 @@ class ControllerLoggingAspect(
try { try {
return joinPoint.proceed() return joinPoint.proceed()
.also { logSuccess(servletRequest.getEndpoint(), startTime, it) } .also { logSuccess(servletRequest, it as ResponseEntity<*>) }
} catch (e: Exception) { } catch (e: Exception) {
throw e throw e
} }
} }
private fun logSuccess(endpoint: String, startTime: Long, result: Any) { private fun logSuccess(servletRequest: HttpServletRequest, result: ResponseEntity<*>) {
val responseEntity = result as ResponseEntity<*> val body: Any? = if (log.isDebugEnabled()) result.body else null
var convertResponseMessageRequest = ConvertResponseMessageRequest(
type = LogType.CONTROLLER_SUCCESS,
endpoint = endpoint,
httpStatus = responseEntity.statusCode.value(),
startTime = startTime,
)
if (log.isDebugEnabled()) { val logMessage = messageConverter.convertToResponseMessage(
convertResponseMessageRequest = convertResponseMessageRequest.copy( type = LogType.SUCCEED,
body = responseEntity.body servletRequest = servletRequest,
httpStatusCode = result.statusCode.value(),
responseBody = body,
) )
}
val logMessage = messageConverter.convertToResponseMessage(convertResponseMessageRequest)
log.info { logMessage } log.info { logMessage }
} }
@ -71,14 +63,16 @@ class ControllerLoggingAspect(
return (RequestContextHolder.currentRequestAttributes() as ServletRequestAttributes).request return (RequestContextHolder.currentRequestAttributes() as ServletRequestAttributes).request
} }
private fun parsePayload(joinPoint: JoinPoint): Map<String, Any> { private fun parseControllerPayload(joinPoint: JoinPoint): Map<String, Any> {
val signature = joinPoint.signature as MethodSignature val signature = joinPoint.signature as MethodSignature
val args = joinPoint.args val args = joinPoint.args
val payload = mutableMapOf<String, Any>() val payload = mutableMapOf<String, Any>(
payload["controller_method"] = joinPoint.signature.toShortString() "controller_method" to joinPoint.signature.toShortString()
)
val requestParams: MutableMap<String, Any> = mutableMapOf() val requestParams: MutableMap<String, Any> = mutableMapOf()
val pathVariables: MutableMap<String, Any> = mutableMapOf() val pathVariables: MutableMap<String, Any> = mutableMapOf()
signature.method.parameters.forEachIndexed { index, parameter -> signature.method.parameters.forEachIndexed { index, parameter ->
val arg = args[index] val arg = args[index]
@ -93,9 +87,10 @@ class ControllerLoggingAspect(
parameter.getAnnotation(RequestParam::class.java)?.let { parameter.getAnnotation(RequestParam::class.java)?.let {
requestParams[parameter.name] = arg requestParams[parameter.name] = arg
} }
} }.also {
if (pathVariables.isNotEmpty()) payload["path_variable"] = pathVariables if (pathVariables.isNotEmpty()) payload["path_variable"] = pathVariables
if (requestParams.isNotEmpty()) payload["request_param"] = requestParams if (requestParams.isNotEmpty()) payload["request_param"] = requestParams
}
return payload return payload
} }

View File

@ -1,4 +1,4 @@
package roomescape.common.config package com.sangdol.common.web.config
import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
@ -14,7 +14,11 @@ import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer
import com.fasterxml.jackson.module.kotlin.kotlinModule import com.fasterxml.jackson.module.kotlin.kotlinModule
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import java.time.* 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 import java.time.format.DateTimeFormatter
@Configuration @Configuration
@ -23,6 +27,9 @@ class JacksonConfig {
companion object { companion object {
private val ISO_OFFSET_DATE_TIME_FORMATTER: DateTimeFormatter = private val ISO_OFFSET_DATE_TIME_FORMATTER: DateTimeFormatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX") DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX")
private val LOCAL_TIME_FORMATTER: DateTimeFormatter =
DateTimeFormatter.ofPattern("HH:mm")
} }
@Bean @Bean
@ -43,11 +50,11 @@ class JacksonConfig {
) )
.addSerializer( .addSerializer(
LocalTime::class.java, LocalTime::class.java,
LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm")) LocalTimeSerializer(LOCAL_TIME_FORMATTER)
) )
.addDeserializer( .addDeserializer(
LocalTime::class.java, LocalTime::class.java,
LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm")) LocalTimeDeserializer(LOCAL_TIME_FORMATTER)
) as JavaTimeModule ) as JavaTimeModule
private fun dateTimeModule(): SimpleModule { private fun dateTimeModule(): SimpleModule {

View File

@ -1,6 +1,9 @@
package roomescape.common.log package com.sangdol.common.web.config
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.sangdol.common.web.asepct.ControllerLoggingAspect
import com.sangdol.common.web.servlet.HttpRequestLoggingFilter
import com.sangdol.common.web.support.log.WebLogMessageConverter
import org.springframework.boot.web.servlet.FilterRegistrationBean import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
@ -9,27 +12,27 @@ import org.springframework.core.Ordered
import org.springframework.web.filter.OncePerRequestFilter import org.springframework.web.filter.OncePerRequestFilter
@Configuration @Configuration
class LogConfiguration { class WebLoggingConfig {
@Bean @Bean
@DependsOn(value = ["apiLogMessageConverter"]) @DependsOn(value = ["webLogMessageConverter"])
fun filterRegistrationBean( fun filterRegistrationBean(
apiLogMessageConverter: ApiLogMessageConverter webLogMessageConverter: WebLogMessageConverter
): FilterRegistrationBean<OncePerRequestFilter> { ): FilterRegistrationBean<OncePerRequestFilter> {
val filter = HttpRequestLoggingFilter(apiLogMessageConverter) val filter = HttpRequestLoggingFilter(webLogMessageConverter)
return FilterRegistrationBean<OncePerRequestFilter>(filter) return FilterRegistrationBean<OncePerRequestFilter>(filter)
.apply { this.order = Ordered.HIGHEST_PRECEDENCE + 2 } .apply { this.order = Ordered.HIGHEST_PRECEDENCE + 2 }
} }
@Bean @Bean
@DependsOn(value = ["apiLogMessageConverter"]) @DependsOn(value = ["webLogMessageConverter"])
fun apiLoggingAspect(apiLogMessageConverter: ApiLogMessageConverter): ControllerLoggingAspect { fun apiLoggingAspect(webLogMessageConverter: WebLogMessageConverter): ControllerLoggingAspect {
return ControllerLoggingAspect(apiLogMessageConverter) return ControllerLoggingAspect(webLogMessageConverter)
} }
@Bean @Bean
fun apiLogMessageConverter(objectMapper: ObjectMapper): ApiLogMessageConverter { fun webLogMessageConverter(objectMapper: ObjectMapper): WebLogMessageConverter {
return ApiLogMessageConverter(objectMapper) return WebLogMessageConverter(objectMapper)
} }
} }

View File

@ -1,27 +1,26 @@
package roomescape.common.exception package com.sangdol.common.web.exception
import com.sangdol.common.log.constant.LogType
import com.sangdol.common.types.exception.CommonErrorCode
import com.sangdol.common.types.exception.ErrorCode
import com.sangdol.common.types.exception.RoomescapeException
import com.sangdol.common.types.web.CommonErrorResponse
import com.sangdol.common.types.web.HttpStatus
import com.sangdol.common.web.support.log.WebLogMessageConverter
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import org.slf4j.MDC
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.http.converter.HttpMessageNotReadableException import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice import org.springframework.web.bind.annotation.RestControllerAdvice
import roomescape.auth.exception.AuthException
import roomescape.common.dto.response.CommonErrorResponse
import roomescape.common.log.ApiLogMessageConverter
import roomescape.common.log.ConvertResponseMessageRequest
import roomescape.common.log.LogType
import roomescape.common.log.getEndpoint
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@RestControllerAdvice @RestControllerAdvice
class ExceptionControllerAdvice( class GlobalExceptionHandler(
private val messageConverter: ApiLogMessageConverter private val messageConverter: WebLogMessageConverter
) { ) {
@ExceptionHandler(value = [RoomescapeException::class]) @ExceptionHandler(value = [RoomescapeException::class])
fun handleRoomException( fun handleRoomException(
@ -32,17 +31,10 @@ class ExceptionControllerAdvice(
val httpStatus: HttpStatus = errorCode.httpStatus val httpStatus: HttpStatus = errorCode.httpStatus
val errorResponse = CommonErrorResponse(errorCode) val errorResponse = CommonErrorResponse(errorCode)
val type = if (e is AuthException) LogType.AUTHENTICATION_FAILURE else LogType.APPLICATION_FAILURE logException(servletRequest, httpStatus, errorResponse, e)
logException(
type = type,
servletRequest = servletRequest,
httpStatus = httpStatus.value(),
errorResponse = errorResponse,
exception = e
)
return ResponseEntity return ResponseEntity
.status(httpStatus) .status(httpStatus.value())
.body(errorResponse) .body(errorResponse)
} }
@ -51,29 +43,24 @@ class ExceptionControllerAdvice(
servletRequest: HttpServletRequest, servletRequest: HttpServletRequest,
e: Exception e: Exception
): ResponseEntity<CommonErrorResponse> { ): ResponseEntity<CommonErrorResponse> {
val message: String = if (e is MethodArgumentNotValidException) { if (e is MethodArgumentNotValidException) {
e.bindingResult.allErrors e.bindingResult.allErrors
.mapNotNull { it.defaultMessage } .mapNotNull { it.defaultMessage }
.joinToString(", ") .joinToString(", ")
} else { } else {
e.message!! e.message!!
}.also {
log.warn { "[ExceptionControllerAdvice] Invalid Request Value Exception occurred: $it" }
} }
log.debug { "[ExceptionControllerAdvice] Invalid Request Value Exception occurred: $message" }
val errorCode: ErrorCode = CommonErrorCode.INVALID_INPUT_VALUE val errorCode: ErrorCode = CommonErrorCode.INVALID_INPUT_VALUE
val httpStatus: HttpStatus = errorCode.httpStatus val httpStatus: HttpStatus = errorCode.httpStatus
val errorResponse = CommonErrorResponse(errorCode) val errorResponse = CommonErrorResponse(errorCode)
logException( logException(servletRequest, httpStatus, errorResponse, e)
type = LogType.APPLICATION_FAILURE,
servletRequest = servletRequest,
httpStatus = httpStatus.value(),
errorResponse = errorResponse,
exception = e
)
return ResponseEntity return ResponseEntity
.status(httpStatus) .status(httpStatus.value())
.body(errorResponse) .body(errorResponse)
} }
@ -88,39 +75,29 @@ class ExceptionControllerAdvice(
val httpStatus: HttpStatus = errorCode.httpStatus val httpStatus: HttpStatus = errorCode.httpStatus
val errorResponse = CommonErrorResponse(errorCode) val errorResponse = CommonErrorResponse(errorCode)
logException( logException(servletRequest, httpStatus, errorResponse, e)
type = LogType.UNHANDLED_EXCEPTION,
servletRequest = servletRequest,
httpStatus = httpStatus.value(),
errorResponse = errorResponse,
exception = e
)
return ResponseEntity return ResponseEntity
.status(httpStatus) .status(httpStatus.value())
.body(errorResponse) .body(errorResponse)
} }
private fun logException( private fun logException(
type: LogType,
servletRequest: HttpServletRequest, servletRequest: HttpServletRequest,
httpStatus: Int, httpStatus: HttpStatus,
errorResponse: CommonErrorResponse, errorResponse: CommonErrorResponse,
exception: Exception exception: Exception
) { ) {
val commonRequest = ConvertResponseMessageRequest( val type = if (httpStatus.isClientError()) LogType.APPLICATION_FAILURE else LogType.UNHANDLED_EXCEPTION
type = type, val actualException: Exception? = if (errorResponse.message == exception.message) null else exception
endpoint = servletRequest.getEndpoint(),
httpStatus = httpStatus,
startTime = MDC.get("startTime")?.toLongOrNull(),
body = errorResponse,
)
val logMessage = if (errorResponse.message == exception.message) { val logMessage = messageConverter.convertToResponseMessage(
messageConverter.convertToResponseMessage(commonRequest) type = type,
} else { servletRequest = servletRequest,
messageConverter.convertToResponseMessage(commonRequest.copy(exception = exception)) httpStatusCode = httpStatus.value(),
} responseBody = errorResponse,
exception = actualException
)
log.warn { logMessage } log.warn { logMessage }
} }

View File

@ -1,20 +1,21 @@
package roomescape.common.log package com.sangdol.common.web.servlet
import com.sangdol.common.utils.MdcPrincipalIdUtil
import com.sangdol.common.utils.MdcStartTimeUtil
import com.sangdol.common.web.support.log.WebLogMessageConverter
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.servlet.FilterChain import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse import jakarta.servlet.http.HttpServletResponse
import org.slf4j.MDC
import org.springframework.web.filter.OncePerRequestFilter import org.springframework.web.filter.OncePerRequestFilter
import org.springframework.web.util.ContentCachingRequestWrapper import org.springframework.web.util.ContentCachingRequestWrapper
import org.springframework.web.util.ContentCachingResponseWrapper import org.springframework.web.util.ContentCachingResponseWrapper
import roomescape.common.util.MdcPrincipalId
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
class HttpRequestLoggingFilter( class HttpRequestLoggingFilter(
private val messageConverter: ApiLogMessageConverter private val messageConverter: WebLogMessageConverter
) : OncePerRequestFilter() { ) : OncePerRequestFilter() {
override fun doFilterInternal( override fun doFilterInternal(
request: HttpServletRequest, request: HttpServletRequest,
@ -26,15 +27,14 @@ class HttpRequestLoggingFilter(
val cachedRequest = ContentCachingRequestWrapper(request) val cachedRequest = ContentCachingRequestWrapper(request)
val cachedResponse = ContentCachingResponseWrapper(response) val cachedResponse = ContentCachingResponseWrapper(response)
val startTime = System.currentTimeMillis() MdcStartTimeUtil.setCurrentTime()
MDC.put("startTime", startTime.toString())
try { try {
filterChain.doFilter(cachedRequest, cachedResponse) filterChain.doFilter(cachedRequest, cachedResponse)
cachedResponse.copyBodyToResponse() cachedResponse.copyBodyToResponse()
} finally { } finally {
MDC.remove("startTime") MdcStartTimeUtil.clear()
MdcPrincipalId.clear() MdcPrincipalIdUtil.clear()
} }
} }
} }

View File

@ -0,0 +1,75 @@
package com.sangdol.common.web.support.log
import com.sangdol.common.log.constant.LogType
import com.sangdol.common.utils.MdcPrincipalIdUtil
import com.sangdol.common.utils.MdcStartTimeUtil
import jakarta.servlet.http.HttpServletRequest
class LogPayloadBuilder(
private val type: LogType,
private val servletRequest: HttpServletRequest,
private val payload: MutableMap<String, Any> = mutableMapOf("type" to type)
) {
fun endpoint(): LogPayloadBuilder {
payload["endpoint"] = "${servletRequest.method} ${servletRequest.requestURI}"
return this
}
fun clientIp(): LogPayloadBuilder {
servletRequest.remoteAddr?.let { payload["client_ip"] = it }
return this
}
fun userAgent(): LogPayloadBuilder {
servletRequest.getHeader("User-Agent")?.let { payload["user_agent"] = it }
return this
}
fun queryString(): LogPayloadBuilder {
servletRequest.queryString?.let { payload["query_params"] = it }
return this
}
fun httpStatus(statusCode: Int?): LogPayloadBuilder {
statusCode?.let { payload["status_code"] = it }
return this
}
fun responseBody(body: Any?): LogPayloadBuilder {
body?.let { payload["response_body"] = it }
return this
}
fun durationMs(): LogPayloadBuilder {
MdcStartTimeUtil.extractDurationMsOrNull()?.let { payload["duration_ms"] = it }
return this
}
fun principalId(): LogPayloadBuilder {
MdcPrincipalIdUtil.extractAsLongOrNull()
?.let { payload["principal_id"] = it }
?: run { payload["principal_id"] = "UNKNOWN" }
return this
}
fun exception(exception: Exception?): LogPayloadBuilder {
exception?.let {
payload["exception"] = mapOf(
"class" to it.javaClass.simpleName,
"message" to it.message
)
}
return this
}
fun additionalPayloads(payload: Map<String, Any>): LogPayloadBuilder {
this.payload.putAll(payload)
return this
}
fun build(): Map<String, Any> = payload
}

View File

@ -0,0 +1,49 @@
package com.sangdol.common.web.support.log
import com.fasterxml.jackson.databind.ObjectMapper
import com.sangdol.common.log.constant.LogType
import jakarta.servlet.http.HttpServletRequest
class WebLogMessageConverter(
private val objectMapper: ObjectMapper
) {
fun convertToHttpRequestMessage(servletRequest: HttpServletRequest): String {
val payload = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.endpoint()
.queryString()
.clientIp()
.userAgent()
.build()
return objectMapper.writeValueAsString(payload)
}
fun convertToControllerInvokedMessage(servletRequest: HttpServletRequest, controllerPayload: Map<String, Any>): String {
val payload = LogPayloadBuilder(type = LogType.CONTROLLER_INVOKED, servletRequest = servletRequest)
.endpoint()
.principalId()
.additionalPayloads(controllerPayload)
.build()
return objectMapper.writeValueAsString(payload)
}
fun convertToResponseMessage(
type: LogType,
servletRequest: HttpServletRequest,
httpStatusCode: Int,
responseBody: Any? = null,
exception: Exception? = null,
): String {
val payload = LogPayloadBuilder(type = type, servletRequest = servletRequest)
.endpoint()
.httpStatus(httpStatusCode)
.durationMs()
.principalId()
.responseBody(responseBody)
.exception(exception)
.build()
return objectMapper.writeValueAsString(payload)
}
}

View File

@ -1,4 +1,4 @@
package roomescape.common.config package com.sangdol.common.web.config
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.exc.InvalidFormatException import com.fasterxml.jackson.databind.exc.InvalidFormatException
@ -6,11 +6,15 @@ import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldContain
import java.time.* import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
class JacksonConfigTest( class JacksonConfigTest : FunSpec({
private val objectMapper: ObjectMapper = JacksonConfig().objectMapper()
) : FunSpec({ val objectMapper: ObjectMapper = JacksonConfig().objectMapper()
context("날짜는 yyyy-mm-dd 형식이다.") { context("날짜는 yyyy-mm-dd 형식이다.") {
val date = "2025-07-14" val date = "2025-07-14"

View File

@ -0,0 +1,233 @@
package com.sangdol.common.web.support.log
import com.sangdol.common.log.constant.LogType
import com.sangdol.common.utils.MdcPrincipalIdUtil
import com.sangdol.common.utils.MdcStartTimeUtil
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
import jakarta.servlet.http.HttpServletRequest
class LogPayloadBuilderTest : FunSpec({
val servletRequest: HttpServletRequest = mockk()
lateinit var method: String
lateinit var requestUri: String
lateinit var remoteAddr: String
lateinit var userAgent: String
lateinit var queryString: String
beforeTest {
method = "GET".also { every { servletRequest.method } returns it }
requestUri = "/converter/test".also { every { servletRequest.requestURI } returns it }
remoteAddr = "localhost".also { every { servletRequest.remoteAddr } returns it }
userAgent = "Mozilla/5.0".also { every { servletRequest.getHeader("User-Agent") } returns it }
queryString = "key=value".also { every { servletRequest.queryString } returns it }
}
afterSpec {
clearMocks(servletRequest)
}
context("endpoint") {
test("정상 응답") {
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.endpoint()
.build()
result["endpoint"] shouldBe "$method $requestUri"
}
test("ServletRequest에서 null이 반환되면 그대로 들어간다.") {
every { servletRequest.method } returns null
every { servletRequest.requestURI } returns null
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.endpoint()
.build()
result["endpoint"] shouldBe "null null"
}
}
context("clientIp") {
test("정상 응답") {
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.clientIp()
.build()
result["client_ip"] shouldBe remoteAddr
}
test("ServletRequest에서 null이 반환되면 추가되지 않는다.") {
every { servletRequest.remoteAddr } returns null
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.clientIp()
.build()
result["client_ip"] shouldBe null
}
}
context("userAgent") {
test("정상 응답") {
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.userAgent()
.build()
result["user_agent"] shouldBe userAgent
}
test("ServletRequest에서 null이 반환되면 추가되지 않는다.") {
every { servletRequest.getHeader("User-Agent") } returns null
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.userAgent()
.build()
result["user_agent"] shouldBe null
}
}
context("queryString") {
test("정상 응답") {
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.queryString()
.build()
result["query_params"] shouldBe queryString
}
test("ServletRequest에서 null이 반환되면 추가되지 않는다.") {
every { servletRequest.queryString } returns null
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.queryString()
.build()
result["query_params"] shouldBe null
}
}
context("httpStatus") {
test("정상 응답") {
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.httpStatus(200)
.build()
result["status_code"] shouldBe 200
}
test("null을 입력하면 추가되지 않는다.") {
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.httpStatus(null)
.build()
result["status_code"] shouldBe null
}
}
context("responseBody") {
test("정상 응답") {
val body = mapOf("key" to "value")
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.responseBody(body)
.build()
result["response_body"] shouldBe body
}
test("null을 입력하면 추가되지 않는다.") {
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.responseBody(null)
.build()
result["response_body"] shouldBe null
}
}
context("durationMs") {
test("정상 응답") {
MdcStartTimeUtil.setCurrentTime()
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.durationMs()
.build()
result["duration_ms"].shouldNotBeNull()
MdcStartTimeUtil.clear()
}
test("MDC에서 값을 가져올 수 없으면 추가되지 않는다.") {
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.durationMs()
.build()
result["duration_ms"] shouldBe null
}
}
context("principalId") {
test("정상 응답") {
val principalId = 759980174446956066L.also {
MdcPrincipalIdUtil.set(it.toString())
}
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.principalId()
.build()
result["principal_id"] shouldBe principalId
MdcPrincipalIdUtil.clear()
}
test("MDC에서 값을 가져올 수 없으면 UNKNOWN 으로 표기된다.") {
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.principalId()
.build()
result["principal_id"] shouldBe "UNKNOWN"
}
}
context("exception") {
test("정상 응답") {
val exception = RuntimeException("hello")
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.exception(exception)
.build()
result["exception"] shouldBe mapOf(
"class" to exception.javaClass.simpleName,
"message" to exception.message
)
}
test("null을 입력하면 추가되지 않는다.") {
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.exception(null)
.build()
result["exception"] shouldBe null
}
}
context("additionalPayloads") {
test("정상 응답") {
val payload = mapOf(
"key1" to "value1",
"key2" to "value2"
)
val result = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
.additionalPayloads(payload)
.build()
result["key1"] shouldBe "value1"
result["key2"] shouldBe "value2"
}
}
})

View File

@ -0,0 +1,166 @@
package com.sangdol.common.web.support.log
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.sangdol.common.log.constant.LogType
import com.sangdol.common.types.web.HttpStatus
import com.sangdol.common.utils.MdcPrincipalIdUtil
import com.sangdol.common.utils.MdcStartTimeUtil
import io.kotest.assertions.assertSoftly
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
import jakarta.servlet.http.HttpServletRequest
class WebLogMessageConverterTest : FunSpec({
val objectMapper = jacksonObjectMapper()
val converter = WebLogMessageConverter(objectMapper)
val servletRequest: HttpServletRequest = mockk()
lateinit var method: String
lateinit var requestUri: String
lateinit var remoteAddr: String
lateinit var userAgent: String
lateinit var queryString: String
beforeTest {
method = "GET".also { every { servletRequest.method } returns it }
requestUri = "/converter/test".also { every { servletRequest.requestURI } returns it }
remoteAddr = "localhost".also { every { servletRequest.remoteAddr } returns it }
userAgent = "Mozilla/5.0".also { every { servletRequest.getHeader("User-Agent") } returns it }
queryString = "key=value".also { every { servletRequest.queryString } returns it }
}
afterSpec {
clearMocks(servletRequest)
}
context("Http 요청 메시지를 변환한다.") {
test("정상 응답") {
val result = converter.convertToHttpRequestMessage(servletRequest)
result shouldBe """
{"type":"${LogType.INCOMING_HTTP_REQUEST.name}","endpoint":"$method $requestUri","query_params":"$queryString","client_ip":"$remoteAddr","user_agent":"$userAgent"}
""".trimIndent()
}
}
context("Controller 요청 메시지를 변환한다") {
val principalId = 759980174446956066L.also {
MdcPrincipalIdUtil.set(it.toString())
}
test("정상 응답") {
val controllerPayload: Map<String, Any> = mapOf(
"controller_method" to "ThemeController.findThemeById(..)",
"path_variable" to mapOf("id" to "7599801744469560667")
)
val result = converter.convertToControllerInvokedMessage(servletRequest, controllerPayload)
result shouldBe """
{"type":"${LogType.CONTROLLER_INVOKED.name}","endpoint":"$method $requestUri","principal_id":$principalId,"controller_method":"${controllerPayload["controller_method"]}","path_variable":{"id":"${7599801744469560667}"}}
""".trimIndent()
}
}
context("응답 메시지를 변환한다.") {
val principalId = 7599801744469560666
val body = mapOf(
"id" to 7599801744469560667,
"name" to "sangdol"
)
val exception = RuntimeException("hello")
beforeTest {
MdcPrincipalIdUtil.set(principalId.toString())
MdcStartTimeUtil.setCurrentTime()
}
afterTest {
MdcPrincipalIdUtil.clear()
MdcStartTimeUtil.clear()
}
test("응답 본문을 포함한다.") {
val result = converter.convertToResponseMessage(
type = LogType.SUCCEED,
servletRequest = servletRequest,
httpStatusCode = HttpStatus.OK.value(),
responseBody = body
)
assertSoftly(objectMapper.readValue(result, LinkedHashMap::class.java)) {
this["type"] shouldBe LogType.SUCCEED.name
this["endpoint"] shouldBe "$method $requestUri"
this["status_code"] shouldBe HttpStatus.OK.value()
this["duration_ms"].shouldNotBeNull()
this["principal_id"] shouldBe principalId
this["response_body"] shouldBe body
this["exception"] shouldBe null
}
}
test("예외를 포함한다.") {
val result = converter.convertToResponseMessage(
type = LogType.SUCCEED,
servletRequest = servletRequest,
httpStatusCode = HttpStatus.OK.value(),
exception = exception
)
assertSoftly(objectMapper.readValue(result, LinkedHashMap::class.java)) {
this["type"] shouldBe LogType.SUCCEED.name
this["endpoint"] shouldBe "$method $requestUri"
this["status_code"] shouldBe HttpStatus.OK.value()
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)
}
}
test("예외 + 응답 본문을 모두 포함한다.") {
val result = converter.convertToResponseMessage(
type = LogType.SUCCEED,
servletRequest = servletRequest,
httpStatusCode = HttpStatus.OK.value(),
responseBody = body,
exception = exception
)
assertSoftly(objectMapper.readValue(result, LinkedHashMap::class.java)) {
this["type"] shouldBe LogType.SUCCEED.name
this["endpoint"] shouldBe "$method $requestUri"
this["status_code"] shouldBe HttpStatus.OK.value()
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)
}
}
test("예외, 응답 본문 모두 제외한다.") {
val result = converter.convertToResponseMessage(
type = LogType.SUCCEED,
servletRequest = servletRequest,
httpStatusCode = HttpStatus.OK.value(),
)
assertSoftly(objectMapper.readValue(result, LinkedHashMap::class.java)) {
this["type"] shouldBe LogType.SUCCEED.name
this["endpoint"] shouldBe "$method $requestUri"
this["status_code"] shouldBe HttpStatus.OK.value()
this["duration_ms"].shouldNotBeNull()
this["principal_id"] shouldBe principalId
this["response_body"] shouldBe null
this["exception"] shouldBe null
}
}
}
})

63
service/build.gradle.kts Normal file
View File

@ -0,0 +1,63 @@
plugins {
id("org.springframework.boot")
kotlin("plugin.spring")
kotlin("plugin.jpa")
}
dependencies {
// Spring
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
// API docs
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13")
// DB
implementation("com.github.f4b6a3:tsid-creator:5.2.6")
runtimeOnly("com.h2database:h2")
runtimeOnly("com.mysql:mysql-connector-j")
// Jwt
implementation("io.jsonwebtoken:jjwt:0.12.6")
// Logging
implementation("net.logstash.logback:logstash-logback-encoder:8.1")
implementation("com.github.loki4j:loki-logback-appender:2.0.0")
implementation("net.ttddyy.observation:datasource-micrometer-spring-boot:1.1.1")
// Observability
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-tracing-bridge-otel")
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
// Kotlin
implementation("org.jetbrains.kotlin:kotlin-reflect")
// Test
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.mockk:mockk:1.14.4")
testImplementation("com.ninja-squad:springmockk:4.0.2")
// Kotest
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.3.0")
// RestAssured
testImplementation("io.rest-assured:rest-assured:5.5.5")
testImplementation("io.rest-assured:kotlin-extensions:5.5.5")
// etc
implementation("org.apache.poi:poi-ooxml:5.2.3")
// submodules
implementation(project(":common:persistence"))
implementation(project(":common:utils"))
implementation(project(":common:types"))
implementation(project(":common:log"))
implementation(project(":common:web"))
}
tasks.named<Jar>("jar") {
enabled = false
}

View File

@ -1,10 +1,12 @@
package roomescape package com.sangdol.roomescape
import org.springframework.boot.Banner import org.springframework.boot.Banner
import org.springframework.boot.SpringApplication import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
@SpringBootApplication @SpringBootApplication(
scanBasePackages = ["com.sangdol.roomescape", "com.sangdol.common"]
)
class RoomescapeApplication class RoomescapeApplication
fun main(args: Array<String>) { fun main(args: Array<String>) {

View File

@ -1,17 +1,16 @@
package roomescape.admin.business 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 io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import roomescape.admin.exception.AdminErrorCode
import roomescape.admin.exception.AdminException
import roomescape.admin.infrastructure.persistence.AdminRepository
import roomescape.common.dto.AdminLoginCredentials
import roomescape.common.dto.AuditConstant
import roomescape.common.dto.OperatorInfo
import roomescape.common.dto.toCredentials
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@ -35,16 +34,16 @@ class AdminService(
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findOperatorOrUnknown(id: Long): OperatorInfo { fun findOperatorOrUnknown(id: Long): Auditor {
log.info { "[AdminService.findOperatorById] 작업자 정보 조회 시작: id=${id}" } log.info { "[AdminService.findOperatorById] 작업자 정보 조회 시작: id=${id}" }
return adminRepository.findByIdOrNull(id)?.let { admin -> return adminRepository.findByIdOrNull(id)?.let { admin ->
OperatorInfo(admin.id, admin.name).also { Auditor(admin.id, admin.name).also {
log.info { "[AdminService.findOperatorById] 작업자 정보 조회 완료: id=${admin.id}, name=${admin.name}" } log.info { "[AdminService.findOperatorById] 작업자 정보 조회 완료: id=${admin.id}, name=${admin.name}" }
} }
} ?: run { } ?: run {
log.warn { "[AdminService.findOperatorById] 작업자 정보 조회 실패. id=${id}" } log.warn { "[AdminService.findOperatorById] 작업자 정보 조회 실패. id=${id}" }
AuditConstant.UNKNOWN_OPERATOR Auditor.UNKNOWN
} }
} }
} }

View File

@ -0,0 +1,39 @@
package com.sangdol.roomescape.admin.business.dto
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.auth.web.LoginCredentials
import com.sangdol.roomescape.auth.web.LoginSuccessResponse
data class AdminLoginCredentials(
override val id: Long,
override val password: String,
override val name: String,
val type: AdminType,
val storeId: Long?,
val permissionLevel: AdminPermissionLevel,
) : LoginCredentials() {
override fun toResponse(accessToken: String) = AdminLoginSuccessResponse(
accessToken = accessToken,
name = name,
type = type,
storeId = storeId
)
}
fun AdminEntity.toCredentials() = AdminLoginCredentials(
id = this.id,
password = this.password,
name = this.name,
type = this.type,
storeId = this.storeId,
permissionLevel = this.permissionLevel
)
data class AdminLoginSuccessResponse(
override val accessToken: String,
override val name: String,
val type: AdminType,
val storeId: Long?,
) : LoginSuccessResponse()

View File

@ -1,8 +1,8 @@
package roomescape.admin.exception package com.sangdol.roomescape.admin.exception
import org.springframework.http.HttpStatus import com.sangdol.common.types.exception.ErrorCode
import roomescape.common.exception.ErrorCode import com.sangdol.common.types.exception.RoomescapeException
import roomescape.common.exception.RoomescapeException import com.sangdol.common.types.web.HttpStatus
class AdminException( class AdminException(
override val errorCode: AdminErrorCode, override val errorCode: AdminErrorCode,

View File

@ -1,8 +1,8 @@
package roomescape.admin.infrastructure.persistence package com.sangdol.roomescape.admin.infrastructure.persistence
import com.sangdol.common.persistence.AuditingBaseEntity
import jakarta.persistence.* import jakarta.persistence.*
import org.springframework.data.jpa.domain.support.AuditingEntityListener import org.springframework.data.jpa.domain.support.AuditingEntityListener
import roomescape.common.entity.AuditingBaseEntity
@Entity @Entity
@Table(name = "admin") @Table(name = "admin")

View File

@ -1,4 +1,4 @@
package roomescape.admin.infrastructure.persistence package com.sangdol.roomescape.admin.infrastructure.persistence
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository

View File

@ -1,19 +1,15 @@
package roomescape.auth.business package com.sangdol.roomescape.auth.business
import com.sangdol.roomescape.admin.business.AdminService
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.*
import com.sangdol.roomescape.user.business.UserService
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import roomescape.admin.business.AdminService
import roomescape.auth.exception.AuthErrorCode
import roomescape.auth.exception.AuthException
import roomescape.auth.infrastructure.jwt.JwtUtils
import roomescape.auth.web.LoginContext
import roomescape.auth.web.LoginRequest
import roomescape.auth.web.LoginSuccessResponse
import roomescape.common.dto.LoginCredentials
import roomescape.common.dto.PrincipalType
import roomescape.user.business.UserService
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}

View File

@ -1,23 +1,22 @@
package roomescape.auth.business package com.sangdol.roomescape.auth.business
import com.github.f4b6a3.tsid.TsidFactory import com.sangdol.common.persistence.IDGenerator
import com.sangdol.roomescape.auth.infrastructure.persistence.LoginHistoryEntity
import com.sangdol.roomescape.auth.infrastructure.persistence.LoginHistoryRepository
import com.sangdol.roomescape.auth.web.LoginContext
import com.sangdol.roomescape.auth.web.PrincipalType
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import roomescape.auth.infrastructure.persistence.LoginHistoryEntity
import roomescape.auth.infrastructure.persistence.LoginHistoryRepository
import roomescape.auth.web.LoginContext
import roomescape.common.config.next
import roomescape.common.dto.PrincipalType
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@Service @Service
class LoginHistoryService( class LoginHistoryService(
private val loginHistoryRepository: LoginHistoryRepository, private val loginHistoryRepository: LoginHistoryRepository,
private val tsidFactory: TsidFactory, private val idGenerator: IDGenerator,
) { ) {
@Transactional(propagation = Propagation.REQUIRES_NEW) @Transactional(propagation = Propagation.REQUIRES_NEW)
fun createSuccessHistory( fun createSuccessHistory(
@ -47,7 +46,7 @@ class LoginHistoryService(
runCatching { runCatching {
LoginHistoryEntity( LoginHistoryEntity(
id = tsidFactory.next(), id = idGenerator.create(),
principalId = principalId, principalId = principalId,
principalType = principalType, principalType = principalType,
success = success, success = success,

View File

@ -1,5 +1,11 @@
package roomescape.auth.docs 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 io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.responses.ApiResponses
@ -8,12 +14,6 @@ import jakarta.servlet.http.HttpServletResponse
import jakarta.validation.Valid import jakarta.validation.Valid
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import roomescape.auth.web.LoginRequest
import roomescape.auth.web.LoginSuccessResponse
import roomescape.auth.web.support.Public
import roomescape.auth.web.support.User
import roomescape.common.dto.CurrentUserContext
import roomescape.common.dto.response.CommonApiResponse
interface AuthAPI { interface AuthAPI {

View File

@ -1,7 +1,7 @@
package roomescape.auth.exception package com.sangdol.roomescape.auth.exception
import org.springframework.http.HttpStatus import com.sangdol.common.types.web.HttpStatus
import roomescape.common.exception.ErrorCode import com.sangdol.common.types.exception.ErrorCode
enum class AuthErrorCode( enum class AuthErrorCode(
override val httpStatus: HttpStatus, override val httpStatus: HttpStatus,

View File

@ -1,6 +1,6 @@
package roomescape.auth.exception package com.sangdol.roomescape.auth.exception
import roomescape.common.exception.RoomescapeException import com.sangdol.common.types.exception.RoomescapeException
class AuthException( class AuthException(
override val errorCode: AuthErrorCode, override val errorCode: AuthErrorCode,

View File

@ -1,4 +1,4 @@
package roomescape.auth.infrastructure.jwt package com.sangdol.roomescape.auth.infrastructure.jwt
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
@ -8,8 +8,8 @@ import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys import io.jsonwebtoken.security.Keys
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import roomescape.auth.exception.AuthErrorCode import com.sangdol.roomescape.auth.exception.AuthErrorCode
import roomescape.auth.exception.AuthException import com.sangdol.roomescape.auth.exception.AuthException
import java.util.* import java.util.*
import javax.crypto.SecretKey import javax.crypto.SecretKey

View File

@ -1,10 +1,10 @@
package roomescape.auth.infrastructure.persistence package com.sangdol.roomescape.auth.infrastructure.persistence
import com.sangdol.common.persistence.PersistableBaseEntity
import com.sangdol.roomescape.auth.web.PrincipalType
import jakarta.persistence.* import jakarta.persistence.*
import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.CreatedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener import org.springframework.data.jpa.domain.support.AuditingEntityListener
import roomescape.common.dto.PrincipalType
import roomescape.common.entity.PersistableBaseEntity
import java.time.LocalDateTime import java.time.LocalDateTime
@Entity @Entity

View File

@ -1,4 +1,4 @@
package roomescape.auth.infrastructure.persistence package com.sangdol.roomescape.auth.infrastructure.persistence
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository

View File

@ -1,16 +1,16 @@
package roomescape.auth.web package com.sangdol.roomescape.auth.web
import com.sangdol.common.types.web.CommonApiResponse
import com.sangdol.roomescape.auth.business.AuthService
import com.sangdol.roomescape.auth.docs.AuthAPI
import com.sangdol.roomescape.auth.web.support.User
import com.sangdol.roomescape.common.types.CurrentUserContext
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import roomescape.auth.business.AuthService
import roomescape.auth.docs.AuthAPI
import roomescape.auth.web.support.User
import roomescape.common.dto.CurrentUserContext
import roomescape.common.dto.response.CommonApiResponse
@RestController @RestController
@RequestMapping("/auth") @RequestMapping("/auth")

View File

@ -1,8 +1,11 @@
package roomescape.auth.web package com.sangdol.roomescape.auth.web
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import roomescape.admin.infrastructure.persistence.AdminType
import roomescape.common.dto.PrincipalType enum class PrincipalType {
USER, ADMIN
}
data class LoginContext( data class LoginContext(
val ipAddress: String, val ipAddress: String,
@ -25,14 +28,10 @@ abstract class LoginSuccessResponse {
abstract val name: String abstract val name: String
} }
data class UserLoginSuccessResponse( abstract class LoginCredentials {
override val accessToken: String, abstract val id: Long
override val name: String, abstract val password: String
) : LoginSuccessResponse() abstract val name: String
data class AdminLoginSuccessResponse( abstract fun toResponse(accessToken: String): LoginSuccessResponse
override val accessToken: String, }
override val name: String,
val type: AdminType,
val storeId: Long?,
) : LoginSuccessResponse()

View File

@ -1,7 +1,7 @@
package roomescape.auth.web.support package com.sangdol.roomescape.auth.web.support
import roomescape.admin.infrastructure.persistence.AdminType import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType
import roomescape.admin.infrastructure.persistence.Privilege import com.sangdol.roomescape.admin.infrastructure.persistence.Privilege
@Target(AnnotationTarget.FUNCTION) @Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)

View File

@ -1,4 +1,4 @@
package roomescape.auth.web.support package com.sangdol.roomescape.auth.web.support
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest

View File

@ -1,4 +1,4 @@
package roomescape.auth.web.support.interceptors package com.sangdol.roomescape.auth.web.support.interceptors
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
@ -7,17 +7,17 @@ import jakarta.servlet.http.HttpServletResponse
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.web.method.HandlerMethod import org.springframework.web.method.HandlerMethod
import org.springframework.web.servlet.HandlerInterceptor import org.springframework.web.servlet.HandlerInterceptor
import roomescape.admin.infrastructure.persistence.AdminPermissionLevel import com.sangdol.roomescape.admin.infrastructure.persistence.AdminPermissionLevel
import roomescape.admin.infrastructure.persistence.AdminType import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType
import roomescape.admin.infrastructure.persistence.Privilege import com.sangdol.roomescape.admin.infrastructure.persistence.Privilege
import roomescape.auth.business.CLAIM_ADMIN_TYPE_KEY import com.sangdol.roomescape.auth.business.CLAIM_ADMIN_TYPE_KEY
import roomescape.auth.business.CLAIM_PERMISSION_KEY import com.sangdol.roomescape.auth.business.CLAIM_PERMISSION_KEY
import roomescape.auth.exception.AuthErrorCode import com.sangdol.roomescape.auth.exception.AuthErrorCode
import roomescape.auth.exception.AuthException import com.sangdol.roomescape.auth.exception.AuthException
import roomescape.auth.infrastructure.jwt.JwtUtils import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils
import roomescape.auth.web.support.AdminOnly import com.sangdol.roomescape.auth.web.support.AdminOnly
import roomescape.auth.web.support.accessToken import com.sangdol.roomescape.auth.web.support.accessToken
import roomescape.common.util.MdcPrincipalId import com.sangdol.common.utils.MdcPrincipalIdUtil
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@ -38,7 +38,7 @@ class AdminInterceptor(
try { try {
run { run {
val id: String = jwtUtils.extractSubject(token).also { MdcPrincipalId.set(it) } val id: String = jwtUtils.extractSubject(token).also { MdcPrincipalIdUtil.set(it) }
val type: AdminType = validateTypeAndGet(token, annotation.type) val type: AdminType = validateTypeAndGet(token, annotation.type)
val permission: AdminPermissionLevel = validatePermissionAndGet(token, annotation.privilege) val permission: AdminPermissionLevel = validatePermissionAndGet(token, annotation.privilege)

View File

@ -1,4 +1,4 @@
package roomescape.auth.web.support.interceptors package com.sangdol.roomescape.auth.web.support.interceptors
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
@ -7,13 +7,13 @@ import jakarta.servlet.http.HttpServletResponse
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.web.method.HandlerMethod import org.springframework.web.method.HandlerMethod
import org.springframework.web.servlet.HandlerInterceptor import org.springframework.web.servlet.HandlerInterceptor
import roomescape.auth.business.CLAIM_ADMIN_TYPE_KEY import com.sangdol.roomescape.auth.business.CLAIM_ADMIN_TYPE_KEY
import roomescape.auth.exception.AuthErrorCode import com.sangdol.roomescape.auth.exception.AuthErrorCode
import roomescape.auth.exception.AuthException import com.sangdol.roomescape.auth.exception.AuthException
import roomescape.auth.infrastructure.jwt.JwtUtils import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils
import roomescape.auth.web.support.UserOnly import com.sangdol.roomescape.auth.web.support.UserOnly
import roomescape.auth.web.support.accessToken import com.sangdol.roomescape.auth.web.support.accessToken
import roomescape.common.util.MdcPrincipalId import com.sangdol.common.utils.MdcPrincipalIdUtil
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@ -33,7 +33,7 @@ class UserInterceptor(
val token: String? = request.accessToken() val token: String? = request.accessToken()
try { try {
val id: String = jwtUtils.extractSubject(token).also { MdcPrincipalId.set(it) } val id: String = jwtUtils.extractSubject(token).also { MdcPrincipalIdUtil.set(it) }
/** /**
* CLAIM_ADMIN_TYPE_KEY 존재하면 관리자 토큰임 * CLAIM_ADMIN_TYPE_KEY 존재하면 관리자 토큰임

View File

@ -1,4 +1,4 @@
package roomescape.auth.web.support.resolver package com.sangdol.roomescape.auth.web.support.resolver
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
@ -9,12 +9,12 @@ import org.springframework.web.bind.support.WebDataBinderFactory
import org.springframework.web.context.request.NativeWebRequest import org.springframework.web.context.request.NativeWebRequest
import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.method.support.ModelAndViewContainer import org.springframework.web.method.support.ModelAndViewContainer
import roomescape.auth.exception.AuthErrorCode import com.sangdol.roomescape.auth.exception.AuthErrorCode
import roomescape.auth.exception.AuthException import com.sangdol.roomescape.auth.exception.AuthException
import roomescape.auth.infrastructure.jwt.JwtUtils import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils
import roomescape.auth.web.support.User import com.sangdol.roomescape.auth.web.support.User
import roomescape.auth.web.support.accessToken import com.sangdol.roomescape.auth.web.support.accessToken
import roomescape.user.business.UserService import com.sangdol.roomescape.user.business.UserService
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}

View File

@ -0,0 +1,9 @@
package com.sangdol.roomescape.common.config
import com.sangdol.common.web.config.JacksonConfig
import com.sangdol.common.log.message.AbstractLogMaskingConverter
class RoomescapeLogMaskingConverter: AbstractLogMaskingConverter(
sensitiveKeys = setOf("password", "accessToken", "phone"),
objectMapper = JacksonConfig().objectMapper()
)

View File

@ -1,8 +1,7 @@
package roomescape.common.log package com.sangdol.roomescape.common.config
import com.sangdol.common.log.sql.SlowQueryDataSourceFactory
import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.HikariDataSource
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder
import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties
@ -23,15 +22,12 @@ class ProxyDataSourceConfig {
fun dataSource( fun dataSource(
@Qualifier("actualDataSource") actualDataSource: DataSource, @Qualifier("actualDataSource") actualDataSource: DataSource,
properties: SlowQueryProperties properties: SlowQueryProperties
): DataSource = ProxyDataSourceBuilder.create(actualDataSource) ): DataSource = SlowQueryDataSourceFactory.create(
.name(properties.loggerName) dataSource = actualDataSource,
.listener( loggerName = properties.loggerName,
MDCAwareSlowQueryListenerWithoutParams( logLevel = properties.logLevel,
logLevel = SLF4JLogLevel.nullSafeValueOf(properties.logLevel.uppercase()),
thresholdMs = properties.thresholdMs thresholdMs = properties.thresholdMs
) )
)
.buildProxy()
@Bean @Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari") @ConfigurationProperties(prefix = "spring.datasource.hikari")

View File

@ -0,0 +1,15 @@
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
@Configuration
class SwaggerConfig {
@Bean
fun openAPI(): OpenAPI {
return OpenAPI()
}
}

View File

@ -1,12 +1,12 @@
package roomescape.common.config package com.sangdol.roomescape.common.config
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.servlet.config.annotation.InterceptorRegistry import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import roomescape.auth.web.support.interceptors.AdminInterceptor import com.sangdol.roomescape.auth.web.support.interceptors.AdminInterceptor
import roomescape.auth.web.support.interceptors.UserInterceptor import com.sangdol.roomescape.auth.web.support.interceptors.UserInterceptor
import roomescape.auth.web.support.resolver.UserContextResolver import com.sangdol.roomescape.auth.web.support.resolver.UserContextResolver
@Configuration @Configuration
class WebMvcConfig( class WebMvcConfig(

View File

@ -0,0 +1,19 @@
package com.sangdol.roomescape.common.types
import java.time.LocalDateTime
data class Auditor(
val id: Long,
val name: String,
) {
companion object {
val UNKNOWN = Auditor(0, "Unknown")
}
}
data class AuditingInfo(
val createdAt: LocalDateTime,
val createdBy: Auditor,
val updatedAt: LocalDateTime,
val updatedBy: Auditor,
)

View File

@ -0,0 +1,6 @@
package com.sangdol.roomescape.common.types
data class CurrentUserContext(
val id: Long,
val name: String,
)

View File

@ -1,17 +1,17 @@
package roomescape.payment.business package com.sangdol.roomescape.payment.business
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import roomescape.common.util.TransactionExecutionUtil import com.sangdol.common.persistence.TransactionExecutionUtil
import roomescape.payment.exception.PaymentErrorCode import com.sangdol.roomescape.payment.exception.PaymentErrorCode
import roomescape.payment.exception.PaymentException import com.sangdol.roomescape.payment.exception.PaymentException
import roomescape.payment.infrastructure.client.PaymentClientCancelResponse import com.sangdol.roomescape.payment.infrastructure.client.PaymentClientCancelResponse
import roomescape.payment.infrastructure.client.PaymentClientConfirmResponse import com.sangdol.roomescape.payment.infrastructure.client.PaymentClientConfirmResponse
import roomescape.payment.infrastructure.client.TosspayClient import com.sangdol.roomescape.payment.infrastructure.client.TosspayClient
import roomescape.payment.infrastructure.persistence.* import com.sangdol.roomescape.payment.infrastructure.persistence.*
import roomescape.payment.web.* import com.sangdol.roomescape.payment.web.*
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@ -41,6 +41,9 @@ class PaymentService(
val detail: PaymentDetailEntity = paymentWriter.createDetail(clientConfirmResponse, payment.id) val detail: PaymentDetailEntity = paymentWriter.createDetail(clientConfirmResponse, payment.id)
PaymentCreateResponse(paymentId = payment.id, detailId = detail.id) PaymentCreateResponse(paymentId = payment.id, detailId = detail.id)
} ?: run {
log.warn { "[PaymentService.confirm] 결제 확정 중 예상치 못한 null 반환" }
throw PaymentException(PaymentErrorCode.PAYMENT_UNEXPECTED_ERROR)
} }
} }

View File

@ -1,16 +1,15 @@
package roomescape.payment.business package com.sangdol.roomescape.payment.business
import com.github.f4b6a3.tsid.TsidFactory import com.sangdol.common.persistence.IDGenerator
import com.sangdol.roomescape.payment.exception.PaymentErrorCode
import com.sangdol.roomescape.payment.exception.PaymentException
import com.sangdol.roomescape.payment.infrastructure.client.*
import com.sangdol.roomescape.payment.infrastructure.common.PaymentMethod
import com.sangdol.roomescape.payment.infrastructure.common.PaymentType
import com.sangdol.roomescape.payment.infrastructure.persistence.*
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import roomescape.common.config.next
import roomescape.payment.exception.PaymentErrorCode
import roomescape.payment.exception.PaymentException
import roomescape.payment.infrastructure.client.*
import roomescape.payment.infrastructure.common.PaymentMethod
import roomescape.payment.infrastructure.common.PaymentType
import roomescape.payment.infrastructure.persistence.*
import java.time.LocalDateTime import java.time.LocalDateTime
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@ -20,7 +19,7 @@ class PaymentWriter(
private val paymentRepository: PaymentRepository, private val paymentRepository: PaymentRepository,
private val paymentDetailRepository: PaymentDetailRepository, private val paymentDetailRepository: PaymentDetailRepository,
private val canceledPaymentRepository: CanceledPaymentRepository, private val canceledPaymentRepository: CanceledPaymentRepository,
private val tsidFactory: TsidFactory, private val idGenerator: IDGenerator,
) { ) {
fun createPayment( fun createPayment(
@ -32,7 +31,7 @@ class PaymentWriter(
log.info { "[PaymentWriterV2.createPayment] 결제 승인 및 결제 정보 저장 시작: reservationId=${reservationId}, paymentKey=${paymentClientConfirmResponse.paymentKey}" } log.info { "[PaymentWriterV2.createPayment] 결제 승인 및 결제 정보 저장 시작: reservationId=${reservationId}, paymentKey=${paymentClientConfirmResponse.paymentKey}" }
return paymentClientConfirmResponse.toEntity( return paymentClientConfirmResponse.toEntity(
id = tsidFactory.next(), reservationId, orderId, paymentType id = idGenerator.create(), reservationId, orderId, paymentType
).also { ).also {
paymentRepository.save(it) paymentRepository.save(it)
log.info { "[PaymentWriterV2.createPayment] 결제 승인 및 결제 정보 저장 완료: reservationId=${reservationId}, payment.id=${it.id}" } log.info { "[PaymentWriterV2.createPayment] 결제 승인 및 결제 정보 저장 완료: reservationId=${reservationId}, payment.id=${it.id}" }
@ -44,7 +43,7 @@ class PaymentWriter(
paymentId: Long, paymentId: Long,
): PaymentDetailEntity { ): PaymentDetailEntity {
val method: PaymentMethod = paymentResponse.method val method: PaymentMethod = paymentResponse.method
val id = tsidFactory.next() val id = idGenerator.create()
if (method == PaymentMethod.TRANSFER) { if (method == PaymentMethod.TRANSFER) {
return paymentDetailRepository.save(paymentResponse.toTransferDetailEntity(id, paymentId)) return paymentDetailRepository.save(paymentResponse.toTransferDetailEntity(id, paymentId))
@ -69,7 +68,7 @@ class PaymentWriter(
paymentRepository.save(payment.apply { this.cancel() }) paymentRepository.save(payment.apply { this.cancel() })
return cancelResponse.cancels.toEntity( return cancelResponse.cancels.toEntity(
id = tsidFactory.next(), id = idGenerator.create(),
paymentId = payment.id, paymentId = payment.id,
cancelRequestedAt = requestedAt, cancelRequestedAt = requestedAt,
canceledBy = userId canceledBy = userId

View File

@ -1,5 +1,12 @@
package roomescape.payment.docs package com.sangdol.roomescape.payment.docs
import com.sangdol.common.types.web.CommonApiResponse
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.payment.web.PaymentCancelRequest
import com.sangdol.roomescape.payment.web.PaymentConfirmRequest
import com.sangdol.roomescape.payment.web.PaymentCreateResponse
import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.responses.ApiResponses
@ -7,13 +14,6 @@ import jakarta.validation.Valid
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import roomescape.auth.web.support.User
import roomescape.auth.web.support.UserOnly
import roomescape.common.dto.CurrentUserContext
import roomescape.common.dto.response.CommonApiResponse
import roomescape.payment.web.PaymentCancelRequest
import roomescape.payment.web.PaymentConfirmRequest
import roomescape.payment.web.PaymentCreateResponse
interface PaymentAPI { interface PaymentAPI {

View File

@ -1,7 +1,7 @@
package roomescape.payment.exception package com.sangdol.roomescape.payment.exception
import org.springframework.http.HttpStatus import com.sangdol.common.types.web.HttpStatus
import roomescape.common.exception.ErrorCode import com.sangdol.common.types.exception.ErrorCode
enum class PaymentErrorCode( enum class PaymentErrorCode(
override val httpStatus: HttpStatus, override val httpStatus: HttpStatus,

View File

@ -1,6 +1,6 @@
package roomescape.payment.exception package com.sangdol.roomescape.payment.exception
import roomescape.common.exception.RoomescapeException import com.sangdol.common.types.exception.RoomescapeException
class PaymentException( class PaymentException(
override val errorCode: PaymentErrorCode, override val errorCode: PaymentErrorCode,

View File

@ -1,4 +1,4 @@
package roomescape.payment.infrastructure.client package com.sangdol.roomescape.payment.infrastructure.client
import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder

View File

@ -1,4 +1,4 @@
package roomescape.payment.infrastructure.client package com.sangdol.roomescape.payment.infrastructure.client
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties

View File

@ -1,11 +1,11 @@
package roomescape.payment.infrastructure.client package com.sangdol.roomescape.payment.infrastructure.client
import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import roomescape.payment.infrastructure.common.PaymentStatus import com.sangdol.roomescape.payment.infrastructure.common.PaymentStatus
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity import com.sangdol.roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.OffsetDateTime import java.time.OffsetDateTime

View File

@ -1,6 +1,8 @@
package roomescape.payment.infrastructure.client package com.sangdol.roomescape.payment.infrastructure.client
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
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.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.http.HttpMethod import org.springframework.http.HttpMethod
@ -10,8 +12,6 @@ import org.springframework.http.client.ClientHttpResponse
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.web.client.ResponseErrorHandler import org.springframework.web.client.ResponseErrorHandler
import org.springframework.web.client.RestClient import org.springframework.web.client.RestClient
import roomescape.payment.exception.PaymentErrorCode
import roomescape.payment.exception.PaymentException
import java.net.URI import java.net.URI
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}

View File

@ -1,12 +1,12 @@
package roomescape.payment.infrastructure.client package com.sangdol.roomescape.payment.infrastructure.client
import roomescape.payment.exception.PaymentErrorCode import com.sangdol.roomescape.payment.exception.PaymentErrorCode
import roomescape.payment.exception.PaymentException import com.sangdol.roomescape.payment.exception.PaymentException
import roomescape.payment.infrastructure.common.* import com.sangdol.roomescape.payment.infrastructure.common.*
import roomescape.payment.infrastructure.persistence.PaymentBankTransferDetailEntity import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentBankTransferDetailEntity
import roomescape.payment.infrastructure.persistence.PaymentCardDetailEntity import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentCardDetailEntity
import roomescape.payment.infrastructure.persistence.PaymentEasypayPrepaidDetailEntity import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentEasypayPrepaidDetailEntity
import roomescape.payment.infrastructure.persistence.PaymentEntity import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentEntity
import java.time.OffsetDateTime import java.time.OffsetDateTime
data class PaymentClientConfirmResponse( data class PaymentClientConfirmResponse(

View File

@ -1,4 +1,4 @@
package roomescape.payment.infrastructure.client package com.sangdol.roomescape.payment.infrastructure.client
data class TosspayErrorResponse( data class TosspayErrorResponse(
val code: String, val code: String,

View File

@ -1,10 +1,10 @@
package roomescape.payment.infrastructure.common package com.sangdol.roomescape.payment.infrastructure.common
import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonCreator
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import roomescape.payment.exception.PaymentErrorCode import com.sangdol.roomescape.payment.exception.PaymentErrorCode
import roomescape.payment.exception.PaymentException import com.sangdol.roomescape.payment.exception.PaymentException
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}

View File

@ -1,8 +1,8 @@
package roomescape.payment.infrastructure.persistence package com.sangdol.roomescape.payment.infrastructure.persistence
import com.sangdol.common.persistence.PersistableBaseEntity
import jakarta.persistence.Entity import jakarta.persistence.Entity
import jakarta.persistence.Table import jakarta.persistence.Table
import roomescape.common.entity.PersistableBaseEntity
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.OffsetDateTime import java.time.OffsetDateTime

View File

@ -1,4 +1,4 @@
package roomescape.payment.infrastructure.persistence package com.sangdol.roomescape.payment.infrastructure.persistence
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository

View File

@ -1,8 +1,8 @@
package roomescape.payment.infrastructure.persistence package com.sangdol.roomescape.payment.infrastructure.persistence
import com.sangdol.common.persistence.PersistableBaseEntity
import com.sangdol.roomescape.payment.infrastructure.common.*
import jakarta.persistence.* import jakarta.persistence.*
import roomescape.common.entity.PersistableBaseEntity
import roomescape.payment.infrastructure.common.*
@Entity @Entity
@Table(name = "payment_detail") @Table(name = "payment_detail")

View File

@ -1,4 +1,4 @@
package roomescape.payment.infrastructure.persistence package com.sangdol.roomescape.payment.infrastructure.persistence
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository

View File

@ -1,13 +1,13 @@
package roomescape.payment.infrastructure.persistence package com.sangdol.roomescape.payment.infrastructure.persistence
import com.sangdol.common.persistence.PersistableBaseEntity
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 jakarta.persistence.Entity import jakarta.persistence.Entity
import jakarta.persistence.EnumType import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated import jakarta.persistence.Enumerated
import jakarta.persistence.Table import jakarta.persistence.Table
import roomescape.common.entity.PersistableBaseEntity
import roomescape.payment.infrastructure.common.PaymentMethod
import roomescape.payment.infrastructure.common.PaymentStatus
import roomescape.payment.infrastructure.common.PaymentType
import java.time.OffsetDateTime import java.time.OffsetDateTime
@Entity @Entity

View File

@ -1,4 +1,4 @@
package roomescape.payment.infrastructure.persistence package com.sangdol.roomescape.payment.infrastructure.persistence
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository

View File

@ -1,13 +1,13 @@
package roomescape.payment.web package com.sangdol.roomescape.payment.web
import com.sangdol.common.types.web.CommonApiResponse
import com.sangdol.roomescape.auth.web.support.User
import com.sangdol.roomescape.common.types.CurrentUserContext
import com.sangdol.roomescape.payment.business.PaymentService
import com.sangdol.roomescape.payment.docs.PaymentAPI
import jakarta.validation.Valid import jakarta.validation.Valid
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import roomescape.auth.web.support.User
import roomescape.common.dto.CurrentUserContext
import roomescape.common.dto.response.CommonApiResponse
import roomescape.payment.business.PaymentService
import roomescape.payment.docs.PaymentAPI
@RestController @RestController
@RequestMapping("/payments") @RequestMapping("/payments")

View File

@ -1,11 +1,11 @@
package roomescape.payment.web package com.sangdol.roomescape.payment.web
import roomescape.payment.exception.PaymentErrorCode import com.sangdol.roomescape.payment.exception.PaymentErrorCode
import roomescape.payment.exception.PaymentException import com.sangdol.roomescape.payment.exception.PaymentException
import roomescape.payment.infrastructure.common.PaymentStatus import com.sangdol.roomescape.payment.infrastructure.common.PaymentStatus
import roomescape.payment.infrastructure.common.PaymentType import com.sangdol.roomescape.payment.infrastructure.common.PaymentType
import roomescape.payment.infrastructure.persistence.* import com.sangdol.roomescape.payment.infrastructure.persistence.*
import roomescape.payment.web.PaymentDetailResponse.* import com.sangdol.roomescape.payment.web.PaymentDetailResponse.*
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.OffsetDateTime import java.time.OffsetDateTime

View File

@ -1,13 +1,13 @@
package roomescape.region.business package com.sangdol.roomescape.region.business
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import roomescape.region.exception.RegionErrorCode import com.sangdol.roomescape.region.exception.RegionErrorCode
import roomescape.region.exception.RegionException import com.sangdol.roomescape.region.exception.RegionException
import roomescape.region.infrastructure.persistence.RegionRepository import com.sangdol.roomescape.region.infrastructure.persistence.RegionRepository
import roomescape.region.web.* import com.sangdol.roomescape.region.web.*
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}

View File

@ -1,15 +1,15 @@
package roomescape.region.docs package com.sangdol.roomescape.region.docs
import com.sangdol.common.types.web.CommonApiResponse
import com.sangdol.roomescape.auth.web.support.Public
import com.sangdol.roomescape.region.web.RegionCodeResponse
import com.sangdol.roomescape.region.web.SidoListResponse
import com.sangdol.roomescape.region.web.SigunguListResponse
import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.responses.ApiResponses
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import roomescape.auth.web.support.Public
import roomescape.common.dto.response.CommonApiResponse
import roomescape.region.web.RegionCodeResponse
import roomescape.region.web.SidoListResponse
import roomescape.region.web.SigunguListResponse
interface RegionAPI { interface RegionAPI {

View File

@ -1,8 +1,8 @@
package roomescape.region.exception package com.sangdol.roomescape.region.exception
import org.springframework.http.HttpStatus import com.sangdol.common.types.web.HttpStatus
import roomescape.common.exception.ErrorCode import com.sangdol.common.types.exception.ErrorCode
import roomescape.common.exception.RoomescapeException import com.sangdol.common.types.exception.RoomescapeException
class RegionException( class RegionException(
override val errorCode: RegionErrorCode, override val errorCode: RegionErrorCode,

View File

@ -1,4 +1,4 @@
package roomescape.region.infrastructure.persistence package com.sangdol.roomescape.region.infrastructure.persistence
import jakarta.persistence.Entity import jakarta.persistence.Entity
import jakarta.persistence.Id import jakarta.persistence.Id

View File

@ -1,4 +1,4 @@
package roomescape.region.infrastructure.persistence package com.sangdol.roomescape.region.infrastructure.persistence
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query import org.springframework.data.jpa.repository.Query

View File

@ -1,13 +1,13 @@
package roomescape.region.web package com.sangdol.roomescape.region.web
import com.sangdol.common.types.web.CommonApiResponse
import com.sangdol.roomescape.region.business.RegionService
import com.sangdol.roomescape.region.docs.RegionAPI
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import roomescape.common.dto.response.CommonApiResponse
import roomescape.region.business.RegionService
import roomescape.region.docs.RegionAPI
@RestController @RestController
@RequestMapping("/regions") @RequestMapping("/regions")

View File

@ -1,4 +1,4 @@
package roomescape.region.web package com.sangdol.roomescape.region.web
data class SidoResponse( data class SidoResponse(
val code: String, val code: String,

View File

@ -1,28 +1,25 @@
package roomescape.reservation.business package com.sangdol.roomescape.reservation.business
import com.github.f4b6a3.tsid.TsidFactory import com.sangdol.common.persistence.IDGenerator
import com.sangdol.roomescape.common.types.CurrentUserContext
import com.sangdol.roomescape.payment.business.PaymentService
import com.sangdol.roomescape.payment.web.PaymentWithDetailResponse
import com.sangdol.roomescape.reservation.exception.ReservationErrorCode
import com.sangdol.roomescape.reservation.exception.ReservationException
import com.sangdol.roomescape.reservation.infrastructure.persistence.*
import com.sangdol.roomescape.reservation.web.*
import com.sangdol.roomescape.schedule.business.ScheduleService
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
import com.sangdol.roomescape.schedule.web.ScheduleOverviewResponse
import com.sangdol.roomescape.schedule.web.ScheduleUpdateRequest
import com.sangdol.roomescape.theme.business.ThemeService
import com.sangdol.roomescape.user.business.UserService
import com.sangdol.roomescape.user.web.UserContactResponse
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import roomescape.common.config.next
import roomescape.common.dto.CurrentUserContext
import roomescape.common.util.DateUtils
import roomescape.payment.business.PaymentService
import roomescape.payment.web.PaymentWithDetailResponse
import roomescape.reservation.exception.ReservationErrorCode
import roomescape.reservation.exception.ReservationException
import roomescape.reservation.infrastructure.persistence.*
import roomescape.reservation.web.*
import roomescape.schedule.business.ScheduleService
import roomescape.schedule.infrastructure.persistence.ScheduleStatus
import roomescape.schedule.web.ScheduleOverviewResponse
import roomescape.schedule.web.ScheduleUpdateRequest
import roomescape.theme.business.ThemeService
import roomescape.user.business.UserService
import roomescape.user.web.UserContactResponse
import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@ -35,7 +32,7 @@ class ReservationService(
private val userService: UserService, private val userService: UserService,
private val themeService: ThemeService, private val themeService: ThemeService,
private val canceledReservationRepository: CanceledReservationRepository, private val canceledReservationRepository: CanceledReservationRepository,
private val tsidFactory: TsidFactory, private val idGenerator: IDGenerator,
private val paymentService: PaymentService private val paymentService: PaymentService
) { ) {
@ -48,7 +45,7 @@ class ReservationService(
validateCanCreate(request) validateCanCreate(request)
val reservation: ReservationEntity = request.toEntity(id = tsidFactory.next(), userId = user.id) val reservation: ReservationEntity = request.toEntity(id = idGenerator.create(), userId = user.id)
return PendingReservationCreateResponse(reservationRepository.save(reservation).id) return PendingReservationCreateResponse(reservationRepository.save(reservation).id)
.also { log.info { "[ReservationService.createPendingReservation] Pending 예약 생성 완료: reservationId=${it}, schedule=${request.scheduleId}" } } .also { log.info { "[ReservationService.createPendingReservation] Pending 예약 생성 완료: reservationId=${it}, schedule=${request.scheduleId}" } }
@ -143,7 +140,7 @@ class ReservationService(
} }
CanceledReservationEntity( CanceledReservationEntity(
id = tsidFactory.next(), id = idGenerator.create(),
reservationId = reservation.id, reservationId = reservation.id,
canceledBy = user.id, canceledBy = user.id,
cancelReason = cancelReason, cancelReason = cancelReason,

View File

@ -1,14 +1,14 @@
package roomescape.reservation.business package com.sangdol.roomescape.reservation.business
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import roomescape.reservation.exception.ReservationErrorCode import com.sangdol.roomescape.reservation.exception.ReservationErrorCode
import roomescape.reservation.exception.ReservationException import com.sangdol.roomescape.reservation.exception.ReservationException
import roomescape.reservation.web.PendingReservationCreateRequest import com.sangdol.roomescape.reservation.web.PendingReservationCreateRequest
import roomescape.schedule.infrastructure.persistence.ScheduleStatus import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
import roomescape.schedule.web.ScheduleSummaryResponse import com.sangdol.roomescape.schedule.web.ScheduleSummaryResponse
import roomescape.theme.web.ThemeInfoResponse import com.sangdol.roomescape.theme.web.ThemeInfoResponse
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}

View File

@ -1,5 +1,10 @@
package roomescape.reservation.docs 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.reservation.web.*
import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.responses.ApiResponses
@ -7,13 +12,6 @@ import jakarta.validation.Valid
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam
import roomescape.auth.web.support.Public
import roomescape.auth.web.support.User
import roomescape.auth.web.support.UserOnly
import roomescape.common.dto.CurrentUserContext
import roomescape.common.dto.response.CommonApiResponse
import roomescape.reservation.web.*
interface ReservationAPI { interface ReservationAPI {
@Operation(summary = "결제 전 임시 예약 저장") @Operation(summary = "결제 전 임시 예약 저장")

Some files were not shown because too many files have changed in this diff Show More