generated from pricelees/issue-pr-template
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9fa802cbc | |||
| 51ad28ddca | |||
| ddab3b18a6 | |||
| 83a5919e9c | |||
| 9b6bb91095 | |||
| 463f930b93 | |||
| 987b30b7b8 | |||
| 54f3c042ae | |||
| 85b318e4be | |||
| 14d68bb4fb | |||
| df5abf5cd4 | |||
| 1acad03e7b | |||
| 888a38c156 | |||
| 30eb2e3b03 | |||
| 1cbece032f | |||
| eeb87e1bc3 | |||
| 6cd269e772 | |||
| be19e57b61 | |||
| 9c4d75be2e | |||
| 152367cafb | |||
| 33406fbc93 | |||
| 51a0dab2b4 | |||
| 7c52460ac6 | |||
| 288b67518e | |||
| 715a0f979a | |||
| a7b3636410 | |||
| 00359f63d0 | |||
| 5b3f2f929b | |||
| eada35f1ee | |||
| 81572246d2 | |||
| 07869020be | |||
| 5b31672ebb | |||
| 89eeefbf0c | |||
| 4f0bbe096e | |||
| c524cc6fdf | |||
| 430630a02b | |||
| ab84b329fd | |||
| 2506105c56 | |||
| bcaffb6718 |
115
build.gradle.kts
115
build.gradle.kts
@ -1,99 +1,58 @@
|
||||
import org.jetbrains.kotlin.gradle.plugin.KaptExtension
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
val springBootVersion = "3.5.3"
|
||||
val kotlinVersion = "2.2.0"
|
||||
|
||||
id("org.springframework.boot") version springBootVersion
|
||||
id("io.spring.dependency-management") version "1.1.7"
|
||||
kotlin("jvm") version kotlinVersion
|
||||
kotlin("plugin.spring") version kotlinVersion
|
||||
kotlin("plugin.jpa") version kotlinVersion
|
||||
kotlin("kapt") version kotlinVersion
|
||||
id("io.spring.dependency-management") version "1.1.7" apply false
|
||||
id("org.springframework.boot") version springBootVersion apply false
|
||||
kotlin("jvm") version kotlinVersion apply false
|
||||
kotlin("kapt") version kotlinVersion apply false
|
||||
kotlin("plugin.spring") version kotlinVersion apply false
|
||||
kotlin("plugin.jpa") version kotlinVersion apply false
|
||||
}
|
||||
|
||||
group = "com.sangdol"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
enabled = false
|
||||
}
|
||||
subprojects {
|
||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||
apply(plugin = "org.jetbrains.kotlin.kapt")
|
||||
apply(plugin = "io.spring.dependency-management")
|
||||
|
||||
kapt {
|
||||
keepJavacAnnotationProcessors = true
|
||||
}
|
||||
extensions.configure<JavaPluginExtension> {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
extensions.configure<KaptExtension> {
|
||||
keepJavacAnnotationProcessors = true
|
||||
}
|
||||
|
||||
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")
|
||||
dependencies {
|
||||
add("implementation", "io.github.oshai:kotlin-logging-jvm:7.0.3")
|
||||
add("implementation", "io.kotest:kotest-runner-junit5:5.9.1")
|
||||
add("implementation", "ch.qos.logback:logback-classic:1.5.18")
|
||||
}
|
||||
|
||||
// API docs
|
||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9")
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
// 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> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xjsr305=strict",
|
||||
"-Xannotation-default-target=param-property"
|
||||
)
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
tasks.withType<KotlinCompile> {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xjsr305=strict",
|
||||
"-Xannotation-default-target=param-property"
|
||||
)
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
common/log/build.gradle.kts
Normal file
12
common/log/build.gradle.kts
Normal 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
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.sangdol.common.log.constant
|
||||
|
||||
enum class LogType {
|
||||
INCOMING_HTTP_REQUEST,
|
||||
CONTROLLER_INVOKED,
|
||||
SUCCEED,
|
||||
APPLICATION_FAILURE,
|
||||
UNHANDLED_EXCEPTION
|
||||
}
|
||||
@ -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.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.ObjectNode
|
||||
import com.fasterxml.jackson.databind.node.TextNode
|
||||
import roomescape.common.config.JacksonConfig
|
||||
|
||||
private const val MASK: String = "****"
|
||||
private val SENSITIVE_KEYS = setOf("password", "accessToken", "phone")
|
||||
private val objectMapper: ObjectMapper = JacksonConfig().objectMapper()
|
||||
abstract class AbstractLogMaskingConverter(
|
||||
val sensitiveKeys: Set<String>,
|
||||
val objectMapper: ObjectMapper
|
||||
) : MessageConverter() {
|
||||
|
||||
val mask: String = "****"
|
||||
|
||||
class RoomescapeLogMaskingConverter : MessageConverter() {
|
||||
override fun convert(event: ILoggingEvent): String {
|
||||
val message: String = event.formattedMessage
|
||||
|
||||
@ -35,13 +36,13 @@ class RoomescapeLogMaskingConverter : MessageConverter() {
|
||||
.toString()
|
||||
|
||||
private fun maskedPlainMessage(message: String): String {
|
||||
val keys: String = SENSITIVE_KEYS.joinToString("|")
|
||||
val regex = Regex("(?i)($keys)(\\s*=\\s*)([^(,|\"|?)\\s]+)")
|
||||
val keys: String = sensitiveKeys.joinToString("|")
|
||||
val regex = Regex("(?i)($keys)(\\s*[:=]\\s*)([^(,|\"|?)]+)")
|
||||
|
||||
return regex.replace(message) { matchResult ->
|
||||
val key = matchResult.groupValues[1]
|
||||
val delimiter = matchResult.groupValues[2]
|
||||
val maskedValue = maskValue(matchResult.groupValues[3])
|
||||
val maskedValue = maskValue(matchResult.groupValues[3].trim())
|
||||
|
||||
"${key}${delimiter}${maskedValue}"
|
||||
}
|
||||
@ -51,7 +52,7 @@ class RoomescapeLogMaskingConverter : MessageConverter() {
|
||||
node?.forEachEntry { key, childNode ->
|
||||
when {
|
||||
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)
|
||||
@ -72,10 +73,6 @@ class RoomescapeLogMaskingConverter : MessageConverter() {
|
||||
}
|
||||
|
||||
private fun maskValue(value: String): String {
|
||||
return if (value.length <= 2) {
|
||||
MASK
|
||||
} else {
|
||||
"${value.first()}$MASK${value.last()}"
|
||||
}
|
||||
return "${value.first()}$mask${value.last()}"
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package roomescape.common.log
|
||||
package com.sangdol.common.log.sql
|
||||
|
||||
import net.ttddyy.dsproxy.ExecutionInfo
|
||||
import net.ttddyy.dsproxy.QueryInfo
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -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.core.spec.style.StringSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
@ -18,9 +20,9 @@ class MDCAwareSlowQueryListenerWithoutParamsTest : StringSpec({
|
||||
val slowQueryPredicate = SlowQueryPredicate(thresholdMs = slowQueryThreshold)
|
||||
|
||||
assertSoftly(slowQueryPredicate) {
|
||||
it.test(slowQueryThreshold) shouldBe true
|
||||
it.test(slowQueryThreshold + 1) shouldBe true
|
||||
it.test(slowQueryThreshold - 1) shouldBe false
|
||||
this.test(slowQueryThreshold) shouldBe true
|
||||
this.test(slowQueryThreshold + 1) shouldBe true
|
||||
this.test(slowQueryThreshold - 1) shouldBe false
|
||||
}
|
||||
}
|
||||
})
|
||||
29
common/persistence/build.gradle.kts
Normal file
29
common/persistence/build.gradle.kts
Normal 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
|
||||
}
|
||||
@ -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.CreatedDate
|
||||
import org.springframework.data.annotation.LastModifiedBy
|
||||
import org.springframework.data.annotation.LastModifiedDate
|
||||
import org.springframework.data.domain.Persistable
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.jvm.Transient
|
||||
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener::class)
|
||||
@ -31,23 +31,3 @@ abstract class AuditingBaseEntity(
|
||||
@LastModifiedBy
|
||||
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
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
@ -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.TransactionDefinition
|
||||
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(
|
||||
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 {
|
||||
this.isReadOnly = isReadOnly
|
||||
this.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRES_NEW
|
||||
}
|
||||
|
||||
return transactionTemplate.execute { action() }
|
||||
?: run {
|
||||
log.error { "[TransactionExecutionUtil.withNewTransaction] 트랜잭션 작업 중 예상치 못한 null 반환 " }
|
||||
throw RoomescapeException(CommonErrorCode.UNEXPECTED_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -0,0 +1,6 @@
|
||||
package com.sangdol.common.persistence
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
|
||||
@SpringBootApplication
|
||||
class TestApplication
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
common/persistence/src/test/resources/application.yaml
Normal file
18
common/persistence/src/test/resources/application.yaml
Normal 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:
|
||||
3
common/types/build.gradle.kts
Normal file
3
common/types/build.gradle.kts
Normal file
@ -0,0 +1,3 @@
|
||||
tasks.named<Jar>("jar") {
|
||||
enabled = true
|
||||
}
|
||||
@ -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(
|
||||
override val httpStatus: HttpStatus,
|
||||
@ -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 {
|
||||
val httpStatus: HttpStatus
|
||||
@ -1,6 +1,6 @@
|
||||
package roomescape.common.exception
|
||||
package com.sangdol.common.types.exception
|
||||
|
||||
open class RoomescapeException(
|
||||
open val errorCode: ErrorCode,
|
||||
override val message: String = errorCode.message
|
||||
) : RuntimeException(message)
|
||||
) : RuntimeException(message)
|
||||
@ -1,9 +1,7 @@
|
||||
package roomescape.common.dto.response
|
||||
package com.sangdol.common.types.web
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import roomescape.common.exception.ErrorCode
|
||||
import com.sangdol.common.types.exception.ErrorCode
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
data class CommonApiResponse<T>(
|
||||
val data: T? = null,
|
||||
)
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
7
common/utils/build.gradle.kts
Normal file
7
common/utils/build.gradle.kts
Normal file
@ -0,0 +1,7 @@
|
||||
dependencies {
|
||||
implementation("org.slf4j:slf4j-api:2.0.17")
|
||||
}
|
||||
|
||||
tasks.named<Jar>("jar") {
|
||||
enabled = true
|
||||
}
|
||||
@ -1,11 +1,10 @@
|
||||
package roomescape.common.util
|
||||
package com.sangdol.common.utils
|
||||
|
||||
import org.slf4j.MDC
|
||||
import java.util.*
|
||||
|
||||
private const val MDC_PRINCIPAL_ID_KEY = "principal_id"
|
||||
|
||||
object MdcPrincipalId {
|
||||
object MdcPrincipalIdUtil {
|
||||
const val MDC_PRINCIPAL_ID_KEY = "principal_id"
|
||||
|
||||
fun extractAsLongOrNull(): Long? {
|
||||
return MDC.get(MDC_PRINCIPAL_ID_KEY)?.toLong()
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
28
common/web/build.gradle.kts
Normal file
28
common/web/build.gradle.kts
Normal 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
|
||||
}
|
||||
@ -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.KotlinLogging
|
||||
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.Pointcut
|
||||
import org.aspectj.lang.reflect.MethodSignature
|
||||
import org.slf4j.MDC
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
@ -21,19 +22,17 @@ private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@Aspect
|
||||
class ControllerLoggingAspect(
|
||||
private val messageConverter: ApiLogMessageConverter,
|
||||
private val messageConverter: WebLogMessageConverter,
|
||||
) {
|
||||
|
||||
@Pointcut("execution(* roomescape..web..*Controller*.*(..))")
|
||||
@Pointcut("execution(* com.sangdol..web..*Controller*.*(..))")
|
||||
fun allController() {
|
||||
}
|
||||
|
||||
@Around("allController()")
|
||||
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 controllerPayload: Map<String, Any> = parseControllerPayload(joinPoint)
|
||||
|
||||
log.info {
|
||||
messageConverter.convertToControllerInvokedMessage(servletRequest, controllerPayload)
|
||||
@ -41,29 +40,22 @@ class ControllerLoggingAspect(
|
||||
|
||||
try {
|
||||
return joinPoint.proceed()
|
||||
.also { logSuccess(servletRequest.getEndpoint(), startTime, it) }
|
||||
.also { logSuccess(servletRequest, it as ResponseEntity<*>) }
|
||||
} catch (e: Exception) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun logSuccess(endpoint: String, startTime: Long, result: Any) {
|
||||
val responseEntity = result as ResponseEntity<*>
|
||||
var convertResponseMessageRequest = ConvertResponseMessageRequest(
|
||||
type = LogType.CONTROLLER_SUCCESS,
|
||||
endpoint = endpoint,
|
||||
httpStatus = responseEntity.statusCode.value(),
|
||||
startTime = startTime,
|
||||
private fun logSuccess(servletRequest: HttpServletRequest, result: ResponseEntity<*>) {
|
||||
val body: Any? = if (log.isDebugEnabled()) result.body else null
|
||||
|
||||
val logMessage = messageConverter.convertToResponseMessage(
|
||||
type = LogType.SUCCEED,
|
||||
servletRequest = servletRequest,
|
||||
httpStatusCode = result.statusCode.value(),
|
||||
responseBody = body,
|
||||
)
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
convertResponseMessageRequest = convertResponseMessageRequest.copy(
|
||||
body = responseEntity.body
|
||||
)
|
||||
}
|
||||
|
||||
val logMessage = messageConverter.convertToResponseMessage(convertResponseMessageRequest)
|
||||
|
||||
log.info { logMessage }
|
||||
}
|
||||
|
||||
@ -71,14 +63,16 @@ class ControllerLoggingAspect(
|
||||
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 args = joinPoint.args
|
||||
val payload = mutableMapOf<String, Any>()
|
||||
payload["controller_method"] = joinPoint.signature.toShortString()
|
||||
val payload = mutableMapOf<String, Any>(
|
||||
"controller_method" to joinPoint.signature.toShortString()
|
||||
)
|
||||
|
||||
val requestParams: MutableMap<String, Any> = mutableMapOf()
|
||||
val pathVariables: MutableMap<String, Any> = mutableMapOf()
|
||||
|
||||
signature.method.parameters.forEachIndexed { index, parameter ->
|
||||
val arg = args[index]
|
||||
|
||||
@ -93,9 +87,10 @@ class ControllerLoggingAspect(
|
||||
parameter.getAnnotation(RequestParam::class.java)?.let {
|
||||
requestParams[parameter.name] = arg
|
||||
}
|
||||
}.also {
|
||||
if (pathVariables.isNotEmpty()) payload["path_variable"] = pathVariables
|
||||
if (requestParams.isNotEmpty()) payload["request_param"] = requestParams
|
||||
}
|
||||
if (pathVariables.isNotEmpty()) payload["path_variable"] = pathVariables
|
||||
if (requestParams.isNotEmpty()) payload["request_param"] = requestParams
|
||||
|
||||
return payload
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package roomescape.common.config
|
||||
package com.sangdol.common.web.config
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
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 org.springframework.context.annotation.Bean
|
||||
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
|
||||
|
||||
@Configuration
|
||||
@ -23,6 +27,9 @@ class JacksonConfig {
|
||||
companion object {
|
||||
private val ISO_OFFSET_DATE_TIME_FORMATTER: DateTimeFormatter =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX")
|
||||
|
||||
private val LOCAL_TIME_FORMATTER: DateTimeFormatter =
|
||||
DateTimeFormatter.ofPattern("HH:mm")
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ -43,11 +50,11 @@ class JacksonConfig {
|
||||
)
|
||||
.addSerializer(
|
||||
LocalTime::class.java,
|
||||
LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm"))
|
||||
LocalTimeSerializer(LOCAL_TIME_FORMATTER)
|
||||
)
|
||||
.addDeserializer(
|
||||
LocalTime::class.java,
|
||||
LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm"))
|
||||
LocalTimeDeserializer(LOCAL_TIME_FORMATTER)
|
||||
) as JavaTimeModule
|
||||
|
||||
private fun dateTimeModule(): SimpleModule {
|
||||
@ -80,4 +87,4 @@ class JacksonConfig {
|
||||
gen.writeString(value.format(ISO_OFFSET_DATE_TIME_FORMATTER))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,9 @@
|
||||
package roomescape.common.log
|
||||
package com.sangdol.common.web.config
|
||||
|
||||
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.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
@ -9,27 +12,27 @@ import org.springframework.core.Ordered
|
||||
import org.springframework.web.filter.OncePerRequestFilter
|
||||
|
||||
@Configuration
|
||||
class LogConfiguration {
|
||||
class WebLoggingConfig {
|
||||
|
||||
@Bean
|
||||
@DependsOn(value = ["apiLogMessageConverter"])
|
||||
@DependsOn(value = ["webLogMessageConverter"])
|
||||
fun filterRegistrationBean(
|
||||
apiLogMessageConverter: ApiLogMessageConverter
|
||||
webLogMessageConverter: WebLogMessageConverter
|
||||
): FilterRegistrationBean<OncePerRequestFilter> {
|
||||
val filter = HttpRequestLoggingFilter(apiLogMessageConverter)
|
||||
val filter = HttpRequestLoggingFilter(webLogMessageConverter)
|
||||
|
||||
return FilterRegistrationBean<OncePerRequestFilter>(filter)
|
||||
.apply { this.order = Ordered.HIGHEST_PRECEDENCE + 2 }
|
||||
}
|
||||
|
||||
@Bean
|
||||
@DependsOn(value = ["apiLogMessageConverter"])
|
||||
fun apiLoggingAspect(apiLogMessageConverter: ApiLogMessageConverter): ControllerLoggingAspect {
|
||||
return ControllerLoggingAspect(apiLogMessageConverter)
|
||||
@DependsOn(value = ["webLogMessageConverter"])
|
||||
fun apiLoggingAspect(webLogMessageConverter: WebLogMessageConverter): ControllerLoggingAspect {
|
||||
return ControllerLoggingAspect(webLogMessageConverter)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun apiLogMessageConverter(objectMapper: ObjectMapper): ApiLogMessageConverter {
|
||||
return ApiLogMessageConverter(objectMapper)
|
||||
fun webLogMessageConverter(objectMapper: ObjectMapper): WebLogMessageConverter {
|
||||
return WebLogMessageConverter(objectMapper)
|
||||
}
|
||||
}
|
||||
@ -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.KotlinLogging
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.slf4j.MDC
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
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 {}
|
||||
|
||||
@RestControllerAdvice
|
||||
class ExceptionControllerAdvice(
|
||||
private val messageConverter: ApiLogMessageConverter
|
||||
class GlobalExceptionHandler(
|
||||
private val messageConverter: WebLogMessageConverter
|
||||
) {
|
||||
@ExceptionHandler(value = [RoomescapeException::class])
|
||||
fun handleRoomException(
|
||||
@ -32,17 +31,10 @@ class ExceptionControllerAdvice(
|
||||
val httpStatus: HttpStatus = errorCode.httpStatus
|
||||
val errorResponse = CommonErrorResponse(errorCode)
|
||||
|
||||
val type = if (e is AuthException) LogType.AUTHENTICATION_FAILURE else LogType.APPLICATION_FAILURE
|
||||
logException(
|
||||
type = type,
|
||||
servletRequest = servletRequest,
|
||||
httpStatus = httpStatus.value(),
|
||||
errorResponse = errorResponse,
|
||||
exception = e
|
||||
)
|
||||
logException(servletRequest, httpStatus, errorResponse, e)
|
||||
|
||||
return ResponseEntity
|
||||
.status(httpStatus)
|
||||
.status(httpStatus.value())
|
||||
.body(errorResponse)
|
||||
}
|
||||
|
||||
@ -51,29 +43,24 @@ class ExceptionControllerAdvice(
|
||||
servletRequest: HttpServletRequest,
|
||||
e: Exception
|
||||
): ResponseEntity<CommonErrorResponse> {
|
||||
val message: String = if (e is MethodArgumentNotValidException) {
|
||||
if (e is MethodArgumentNotValidException) {
|
||||
e.bindingResult.allErrors
|
||||
.mapNotNull { it.defaultMessage }
|
||||
.joinToString(", ")
|
||||
} else {
|
||||
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 httpStatus: HttpStatus = errorCode.httpStatus
|
||||
val errorResponse = CommonErrorResponse(errorCode)
|
||||
|
||||
logException(
|
||||
type = LogType.APPLICATION_FAILURE,
|
||||
servletRequest = servletRequest,
|
||||
httpStatus = httpStatus.value(),
|
||||
errorResponse = errorResponse,
|
||||
exception = e
|
||||
)
|
||||
logException(servletRequest, httpStatus, errorResponse, e)
|
||||
|
||||
return ResponseEntity
|
||||
.status(httpStatus)
|
||||
.status(httpStatus.value())
|
||||
.body(errorResponse)
|
||||
}
|
||||
|
||||
@ -88,39 +75,29 @@ class ExceptionControllerAdvice(
|
||||
val httpStatus: HttpStatus = errorCode.httpStatus
|
||||
val errorResponse = CommonErrorResponse(errorCode)
|
||||
|
||||
logException(
|
||||
type = LogType.UNHANDLED_EXCEPTION,
|
||||
servletRequest = servletRequest,
|
||||
httpStatus = httpStatus.value(),
|
||||
errorResponse = errorResponse,
|
||||
exception = e
|
||||
)
|
||||
logException(servletRequest, httpStatus, errorResponse, e)
|
||||
|
||||
return ResponseEntity
|
||||
.status(httpStatus)
|
||||
.status(httpStatus.value())
|
||||
.body(errorResponse)
|
||||
}
|
||||
|
||||
private fun logException(
|
||||
type: LogType,
|
||||
servletRequest: HttpServletRequest,
|
||||
httpStatus: Int,
|
||||
httpStatus: HttpStatus,
|
||||
errorResponse: CommonErrorResponse,
|
||||
exception: Exception
|
||||
) {
|
||||
val commonRequest = ConvertResponseMessageRequest(
|
||||
type = type,
|
||||
endpoint = servletRequest.getEndpoint(),
|
||||
httpStatus = httpStatus,
|
||||
startTime = MDC.get("startTime")?.toLongOrNull(),
|
||||
body = errorResponse,
|
||||
)
|
||||
val type = if (httpStatus.isClientError()) LogType.APPLICATION_FAILURE else LogType.UNHANDLED_EXCEPTION
|
||||
val actualException: Exception? = if (errorResponse.message == exception.message) null else exception
|
||||
|
||||
val logMessage = if (errorResponse.message == exception.message) {
|
||||
messageConverter.convertToResponseMessage(commonRequest)
|
||||
} else {
|
||||
messageConverter.convertToResponseMessage(commonRequest.copy(exception = exception))
|
||||
}
|
||||
val logMessage = messageConverter.convertToResponseMessage(
|
||||
type = type,
|
||||
servletRequest = servletRequest,
|
||||
httpStatusCode = httpStatus.value(),
|
||||
responseBody = errorResponse,
|
||||
exception = actualException
|
||||
)
|
||||
|
||||
log.warn { logMessage }
|
||||
}
|
||||
@ -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.KotlinLogging
|
||||
import jakarta.servlet.FilterChain
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.slf4j.MDC
|
||||
import org.springframework.web.filter.OncePerRequestFilter
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper
|
||||
import roomescape.common.util.MdcPrincipalId
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
class HttpRequestLoggingFilter(
|
||||
private val messageConverter: ApiLogMessageConverter
|
||||
private val messageConverter: WebLogMessageConverter
|
||||
) : OncePerRequestFilter() {
|
||||
override fun doFilterInternal(
|
||||
request: HttpServletRequest,
|
||||
@ -26,15 +27,14 @@ class HttpRequestLoggingFilter(
|
||||
val cachedRequest = ContentCachingRequestWrapper(request)
|
||||
val cachedResponse = ContentCachingResponseWrapper(response)
|
||||
|
||||
val startTime = System.currentTimeMillis()
|
||||
MDC.put("startTime", startTime.toString())
|
||||
MdcStartTimeUtil.setCurrentTime()
|
||||
|
||||
try {
|
||||
filterChain.doFilter(cachedRequest, cachedResponse)
|
||||
cachedResponse.copyBodyToResponse()
|
||||
} finally {
|
||||
MDC.remove("startTime")
|
||||
MdcPrincipalId.clear()
|
||||
MdcStartTimeUtil.clear()
|
||||
MdcPrincipalIdUtil.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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.exc.InvalidFormatException
|
||||
@ -6,11 +6,15 @@ import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
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(
|
||||
private val objectMapper: ObjectMapper = JacksonConfig().objectMapper()
|
||||
) : FunSpec({
|
||||
class JacksonConfigTest : FunSpec({
|
||||
|
||||
val objectMapper: ObjectMapper = JacksonConfig().objectMapper()
|
||||
|
||||
context("날짜는 yyyy-mm-dd 형식이다.") {
|
||||
val date = "2025-07-14"
|
||||
@ -85,4 +89,4 @@ class JacksonConfigTest(
|
||||
serialized shouldBe "\"2025-07-14T12:30:00+09:00\""
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -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
63
service/build.gradle.kts
Normal 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
|
||||
}
|
||||
@ -1,10 +1,12 @@
|
||||
package roomescape
|
||||
package com.sangdol.roomescape
|
||||
|
||||
import org.springframework.boot.Banner
|
||||
import org.springframework.boot.SpringApplication
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
|
||||
@SpringBootApplication
|
||||
@SpringBootApplication(
|
||||
scanBasePackages = ["com.sangdol.roomescape", "com.sangdol.common"]
|
||||
)
|
||||
class RoomescapeApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
@ -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.KotlinLogging
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
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 {}
|
||||
|
||||
@ -35,16 +34,16 @@ class AdminService(
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findOperatorOrUnknown(id: Long): OperatorInfo {
|
||||
fun findOperatorOrUnknown(id: Long): Auditor {
|
||||
log.info { "[AdminService.findOperatorById] 작업자 정보 조회 시작: id=${id}" }
|
||||
|
||||
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}" }
|
||||
}
|
||||
} ?: run {
|
||||
log.warn { "[AdminService.findOperatorById] 작업자 정보 조회 실패. id=${id}" }
|
||||
AuditConstant.UNKNOWN_OPERATOR
|
||||
Auditor.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
@ -1,8 +1,8 @@
|
||||
package roomescape.admin.exception
|
||||
package com.sangdol.roomescape.admin.exception
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
import roomescape.common.exception.ErrorCode
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import com.sangdol.common.types.exception.ErrorCode
|
||||
import com.sangdol.common.types.exception.RoomescapeException
|
||||
import com.sangdol.common.types.web.HttpStatus
|
||||
|
||||
class AdminException(
|
||||
override val errorCode: AdminErrorCode,
|
||||
@ -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 org.springframework.data.jpa.domain.support.AuditingEntityListener
|
||||
import roomescape.common.entity.AuditingBaseEntity
|
||||
|
||||
@Entity
|
||||
@Table(name = "admin")
|
||||
@ -1,4 +1,4 @@
|
||||
package roomescape.admin.infrastructure.persistence
|
||||
package com.sangdol.roomescape.admin.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
@ -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.KotlinLogging
|
||||
import org.springframework.stereotype.Service
|
||||
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 {}
|
||||
|
||||
@ -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.KotlinLogging
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Propagation
|
||||
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 {}
|
||||
|
||||
@Service
|
||||
class LoginHistoryService(
|
||||
private val loginHistoryRepository: LoginHistoryRepository,
|
||||
private val tsidFactory: TsidFactory,
|
||||
private val idGenerator: IDGenerator,
|
||||
) {
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
fun createSuccessHistory(
|
||||
@ -47,7 +46,7 @@ class LoginHistoryService(
|
||||
|
||||
runCatching {
|
||||
LoginHistoryEntity(
|
||||
id = tsidFactory.next(),
|
||||
id = idGenerator.create(),
|
||||
principalId = principalId,
|
||||
principalType = principalType,
|
||||
success = success,
|
||||
@ -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.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses
|
||||
@ -8,12 +14,6 @@ import jakarta.servlet.http.HttpServletResponse
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.http.ResponseEntity
|
||||
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 {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package roomescape.auth.exception
|
||||
package com.sangdol.roomescape.auth.exception
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
import roomescape.common.exception.ErrorCode
|
||||
import com.sangdol.common.types.web.HttpStatus
|
||||
import com.sangdol.common.types.exception.ErrorCode
|
||||
|
||||
enum class AuthErrorCode(
|
||||
override val httpStatus: HttpStatus,
|
||||
@ -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(
|
||||
override val errorCode: AuthErrorCode,
|
||||
@ -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.KotlinLogging
|
||||
@ -8,8 +8,8 @@ import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.security.Keys
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Component
|
||||
import roomescape.auth.exception.AuthErrorCode
|
||||
import roomescape.auth.exception.AuthException
|
||||
import com.sangdol.roomescape.auth.exception.AuthErrorCode
|
||||
import com.sangdol.roomescape.auth.exception.AuthException
|
||||
import java.util.*
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
@ -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 org.springframework.data.annotation.CreatedDate
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener
|
||||
import roomescape.common.dto.PrincipalType
|
||||
import roomescape.common.entity.PersistableBaseEntity
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Entity
|
||||
@ -1,4 +1,4 @@
|
||||
package roomescape.auth.infrastructure.persistence
|
||||
package com.sangdol.roomescape.auth.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
@ -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.HttpServletResponse
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
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
|
||||
@RequestMapping("/auth")
|
||||
@ -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 roomescape.admin.infrastructure.persistence.AdminType
|
||||
import roomescape.common.dto.PrincipalType
|
||||
|
||||
enum class PrincipalType {
|
||||
USER, ADMIN
|
||||
}
|
||||
|
||||
data class LoginContext(
|
||||
val ipAddress: String,
|
||||
@ -25,14 +28,10 @@ abstract class LoginSuccessResponse {
|
||||
abstract val name: String
|
||||
}
|
||||
|
||||
data class UserLoginSuccessResponse(
|
||||
override val accessToken: String,
|
||||
override val name: String,
|
||||
) : LoginSuccessResponse()
|
||||
abstract class LoginCredentials {
|
||||
abstract val id: Long
|
||||
abstract val password: String
|
||||
abstract val name: String
|
||||
|
||||
data class AdminLoginSuccessResponse(
|
||||
override val accessToken: String,
|
||||
override val name: String,
|
||||
val type: AdminType,
|
||||
val storeId: Long?,
|
||||
) : LoginSuccessResponse()
|
||||
abstract fun toResponse(accessToken: String): LoginSuccessResponse
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
package roomescape.auth.web.support
|
||||
package com.sangdol.roomescape.auth.web.support
|
||||
|
||||
import roomescape.admin.infrastructure.persistence.AdminType
|
||||
import roomescape.admin.infrastructure.persistence.Privilege
|
||||
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType
|
||||
import com.sangdol.roomescape.admin.infrastructure.persistence.Privilege
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@ -1,4 +1,4 @@
|
||||
package roomescape.auth.web.support
|
||||
package com.sangdol.roomescape.auth.web.support
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
|
||||
@ -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.KotlinLogging
|
||||
@ -7,17 +7,17 @@ import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.method.HandlerMethod
|
||||
import org.springframework.web.servlet.HandlerInterceptor
|
||||
import roomescape.admin.infrastructure.persistence.AdminPermissionLevel
|
||||
import roomescape.admin.infrastructure.persistence.AdminType
|
||||
import roomescape.admin.infrastructure.persistence.Privilege
|
||||
import roomescape.auth.business.CLAIM_ADMIN_TYPE_KEY
|
||||
import roomescape.auth.business.CLAIM_PERMISSION_KEY
|
||||
import roomescape.auth.exception.AuthErrorCode
|
||||
import roomescape.auth.exception.AuthException
|
||||
import roomescape.auth.infrastructure.jwt.JwtUtils
|
||||
import roomescape.auth.web.support.AdminOnly
|
||||
import roomescape.auth.web.support.accessToken
|
||||
import roomescape.common.util.MdcPrincipalId
|
||||
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminPermissionLevel
|
||||
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType
|
||||
import com.sangdol.roomescape.admin.infrastructure.persistence.Privilege
|
||||
import com.sangdol.roomescape.auth.business.CLAIM_ADMIN_TYPE_KEY
|
||||
import com.sangdol.roomescape.auth.business.CLAIM_PERMISSION_KEY
|
||||
import com.sangdol.roomescape.auth.exception.AuthErrorCode
|
||||
import com.sangdol.roomescape.auth.exception.AuthException
|
||||
import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils
|
||||
import com.sangdol.roomescape.auth.web.support.AdminOnly
|
||||
import com.sangdol.roomescape.auth.web.support.accessToken
|
||||
import com.sangdol.common.utils.MdcPrincipalIdUtil
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@ -38,7 +38,7 @@ class AdminInterceptor(
|
||||
|
||||
try {
|
||||
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 permission: AdminPermissionLevel = validatePermissionAndGet(token, annotation.privilege)
|
||||
|
||||
@ -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.KotlinLogging
|
||||
@ -7,13 +7,13 @@ import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.method.HandlerMethod
|
||||
import org.springframework.web.servlet.HandlerInterceptor
|
||||
import roomescape.auth.business.CLAIM_ADMIN_TYPE_KEY
|
||||
import roomescape.auth.exception.AuthErrorCode
|
||||
import roomescape.auth.exception.AuthException
|
||||
import roomescape.auth.infrastructure.jwt.JwtUtils
|
||||
import roomescape.auth.web.support.UserOnly
|
||||
import roomescape.auth.web.support.accessToken
|
||||
import roomescape.common.util.MdcPrincipalId
|
||||
import com.sangdol.roomescape.auth.business.CLAIM_ADMIN_TYPE_KEY
|
||||
import com.sangdol.roomescape.auth.exception.AuthErrorCode
|
||||
import com.sangdol.roomescape.auth.exception.AuthException
|
||||
import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils
|
||||
import com.sangdol.roomescape.auth.web.support.UserOnly
|
||||
import com.sangdol.roomescape.auth.web.support.accessToken
|
||||
import com.sangdol.common.utils.MdcPrincipalIdUtil
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@ -33,7 +33,7 @@ class UserInterceptor(
|
||||
val token: String? = request.accessToken()
|
||||
|
||||
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 가 존재하면 관리자 토큰임
|
||||
@ -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.KotlinLogging
|
||||
@ -9,12 +9,12 @@ import org.springframework.web.bind.support.WebDataBinderFactory
|
||||
import org.springframework.web.context.request.NativeWebRequest
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver
|
||||
import org.springframework.web.method.support.ModelAndViewContainer
|
||||
import roomescape.auth.exception.AuthErrorCode
|
||||
import roomescape.auth.exception.AuthException
|
||||
import roomescape.auth.infrastructure.jwt.JwtUtils
|
||||
import roomescape.auth.web.support.User
|
||||
import roomescape.auth.web.support.accessToken
|
||||
import roomescape.user.business.UserService
|
||||
import com.sangdol.roomescape.auth.exception.AuthErrorCode
|
||||
import com.sangdol.roomescape.auth.exception.AuthException
|
||||
import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils
|
||||
import com.sangdol.roomescape.auth.web.support.User
|
||||
import com.sangdol.roomescape.auth.web.support.accessToken
|
||||
import com.sangdol.roomescape.user.business.UserService
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@ -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()
|
||||
)
|
||||
@ -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 net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel
|
||||
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
@ -23,15 +22,12 @@ class ProxyDataSourceConfig {
|
||||
fun dataSource(
|
||||
@Qualifier("actualDataSource") actualDataSource: DataSource,
|
||||
properties: SlowQueryProperties
|
||||
): DataSource = ProxyDataSourceBuilder.create(actualDataSource)
|
||||
.name(properties.loggerName)
|
||||
.listener(
|
||||
MDCAwareSlowQueryListenerWithoutParams(
|
||||
logLevel = SLF4JLogLevel.nullSafeValueOf(properties.logLevel.uppercase()),
|
||||
thresholdMs = properties.thresholdMs
|
||||
)
|
||||
)
|
||||
.buildProxy()
|
||||
): DataSource = SlowQueryDataSourceFactory.create(
|
||||
dataSource = actualDataSource,
|
||||
loggerName = properties.loggerName,
|
||||
logLevel = properties.logLevel,
|
||||
thresholdMs = properties.thresholdMs
|
||||
)
|
||||
|
||||
@Bean
|
||||
@ConfigurationProperties(prefix = "spring.datasource.hikari")
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,12 @@
|
||||
package roomescape.common.config
|
||||
package com.sangdol.roomescape.common.config
|
||||
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
import roomescape.auth.web.support.interceptors.AdminInterceptor
|
||||
import roomescape.auth.web.support.interceptors.UserInterceptor
|
||||
import roomescape.auth.web.support.resolver.UserContextResolver
|
||||
import com.sangdol.roomescape.auth.web.support.interceptors.AdminInterceptor
|
||||
import com.sangdol.roomescape.auth.web.support.interceptors.UserInterceptor
|
||||
import com.sangdol.roomescape.auth.web.support.resolver.UserContextResolver
|
||||
|
||||
@Configuration
|
||||
class WebMvcConfig(
|
||||
@ -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,
|
||||
)
|
||||
@ -0,0 +1,6 @@
|
||||
package com.sangdol.roomescape.common.types
|
||||
|
||||
data class CurrentUserContext(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
)
|
||||
@ -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.KotlinLogging
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.common.util.TransactionExecutionUtil
|
||||
import roomescape.payment.exception.PaymentErrorCode
|
||||
import roomescape.payment.exception.PaymentException
|
||||
import roomescape.payment.infrastructure.client.PaymentClientCancelResponse
|
||||
import roomescape.payment.infrastructure.client.PaymentClientConfirmResponse
|
||||
import roomescape.payment.infrastructure.client.TosspayClient
|
||||
import roomescape.payment.infrastructure.persistence.*
|
||||
import roomescape.payment.web.*
|
||||
import com.sangdol.common.persistence.TransactionExecutionUtil
|
||||
import com.sangdol.roomescape.payment.exception.PaymentErrorCode
|
||||
import com.sangdol.roomescape.payment.exception.PaymentException
|
||||
import com.sangdol.roomescape.payment.infrastructure.client.PaymentClientCancelResponse
|
||||
import com.sangdol.roomescape.payment.infrastructure.client.PaymentClientConfirmResponse
|
||||
import com.sangdol.roomescape.payment.infrastructure.client.TosspayClient
|
||||
import com.sangdol.roomescape.payment.infrastructure.persistence.*
|
||||
import com.sangdol.roomescape.payment.web.*
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@ -41,6 +41,9 @@ class PaymentService(
|
||||
val detail: PaymentDetailEntity = paymentWriter.createDetail(clientConfirmResponse, payment.id)
|
||||
|
||||
PaymentCreateResponse(paymentId = payment.id, detailId = detail.id)
|
||||
} ?: run {
|
||||
log.warn { "[PaymentService.confirm] 결제 확정 중 예상치 못한 null 반환" }
|
||||
throw PaymentException(PaymentErrorCode.PAYMENT_UNEXPECTED_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.KotlinLogging
|
||||
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
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
@ -20,7 +19,7 @@ class PaymentWriter(
|
||||
private val paymentRepository: PaymentRepository,
|
||||
private val paymentDetailRepository: PaymentDetailRepository,
|
||||
private val canceledPaymentRepository: CanceledPaymentRepository,
|
||||
private val tsidFactory: TsidFactory,
|
||||
private val idGenerator: IDGenerator,
|
||||
) {
|
||||
|
||||
fun createPayment(
|
||||
@ -32,7 +31,7 @@ class PaymentWriter(
|
||||
log.info { "[PaymentWriterV2.createPayment] 결제 승인 및 결제 정보 저장 시작: reservationId=${reservationId}, paymentKey=${paymentClientConfirmResponse.paymentKey}" }
|
||||
|
||||
return paymentClientConfirmResponse.toEntity(
|
||||
id = tsidFactory.next(), reservationId, orderId, paymentType
|
||||
id = idGenerator.create(), reservationId, orderId, paymentType
|
||||
).also {
|
||||
paymentRepository.save(it)
|
||||
log.info { "[PaymentWriterV2.createPayment] 결제 승인 및 결제 정보 저장 완료: reservationId=${reservationId}, payment.id=${it.id}" }
|
||||
@ -44,7 +43,7 @@ class PaymentWriter(
|
||||
paymentId: Long,
|
||||
): PaymentDetailEntity {
|
||||
val method: PaymentMethod = paymentResponse.method
|
||||
val id = tsidFactory.next()
|
||||
val id = idGenerator.create()
|
||||
|
||||
if (method == PaymentMethod.TRANSFER) {
|
||||
return paymentDetailRepository.save(paymentResponse.toTransferDetailEntity(id, paymentId))
|
||||
@ -69,7 +68,7 @@ class PaymentWriter(
|
||||
paymentRepository.save(payment.apply { this.cancel() })
|
||||
|
||||
return cancelResponse.cancels.toEntity(
|
||||
id = tsidFactory.next(),
|
||||
id = idGenerator.create(),
|
||||
paymentId = payment.id,
|
||||
cancelRequestedAt = requestedAt,
|
||||
canceledBy = userId
|
||||
@ -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.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses
|
||||
@ -7,13 +14,6 @@ import jakarta.validation.Valid
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
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 {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package roomescape.payment.exception
|
||||
package com.sangdol.roomescape.payment.exception
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
import roomescape.common.exception.ErrorCode
|
||||
import com.sangdol.common.types.web.HttpStatus
|
||||
import com.sangdol.common.types.exception.ErrorCode
|
||||
|
||||
enum class PaymentErrorCode(
|
||||
override val httpStatus: HttpStatus,
|
||||
@ -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(
|
||||
override val errorCode: PaymentErrorCode,
|
||||
@ -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.http.client.ClientHttpRequestFactoryBuilder
|
||||
@ -1,4 +1,4 @@
|
||||
package roomescape.payment.infrastructure.client
|
||||
package com.sangdol.roomescape.payment.infrastructure.client
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
|
||||
@ -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.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import roomescape.payment.infrastructure.common.PaymentStatus
|
||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
|
||||
import com.sangdol.roomescape.payment.infrastructure.common.PaymentStatus
|
||||
import com.sangdol.roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package roomescape.payment.infrastructure.client
|
||||
package com.sangdol.roomescape.payment.infrastructure.client
|
||||
|
||||
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.KotlinLogging
|
||||
import org.springframework.http.HttpMethod
|
||||
@ -10,8 +12,6 @@ import org.springframework.http.client.ClientHttpResponse
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.client.ResponseErrorHandler
|
||||
import org.springframework.web.client.RestClient
|
||||
import roomescape.payment.exception.PaymentErrorCode
|
||||
import roomescape.payment.exception.PaymentException
|
||||
import java.net.URI
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
@ -1,12 +1,12 @@
|
||||
package roomescape.payment.infrastructure.client
|
||||
package com.sangdol.roomescape.payment.infrastructure.client
|
||||
|
||||
import roomescape.payment.exception.PaymentErrorCode
|
||||
import roomescape.payment.exception.PaymentException
|
||||
import roomescape.payment.infrastructure.common.*
|
||||
import roomescape.payment.infrastructure.persistence.PaymentBankTransferDetailEntity
|
||||
import roomescape.payment.infrastructure.persistence.PaymentCardDetailEntity
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEasypayPrepaidDetailEntity
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||
import com.sangdol.roomescape.payment.exception.PaymentErrorCode
|
||||
import com.sangdol.roomescape.payment.exception.PaymentException
|
||||
import com.sangdol.roomescape.payment.infrastructure.common.*
|
||||
import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentBankTransferDetailEntity
|
||||
import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentCardDetailEntity
|
||||
import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentEasypayPrepaidDetailEntity
|
||||
import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
data class PaymentClientConfirmResponse(
|
||||
@ -1,4 +1,4 @@
|
||||
package roomescape.payment.infrastructure.client
|
||||
package com.sangdol.roomescape.payment.infrastructure.client
|
||||
|
||||
data class TosspayErrorResponse(
|
||||
val code: String,
|
||||
@ -1,10 +1,10 @@
|
||||
package roomescape.payment.infrastructure.common
|
||||
package com.sangdol.roomescape.payment.infrastructure.common
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import roomescape.payment.exception.PaymentErrorCode
|
||||
import roomescape.payment.exception.PaymentException
|
||||
import com.sangdol.roomescape.payment.exception.PaymentErrorCode
|
||||
import com.sangdol.roomescape.payment.exception.PaymentException
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@ -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.Table
|
||||
import roomescape.common.entity.PersistableBaseEntity
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package roomescape.payment.infrastructure.persistence
|
||||
package com.sangdol.roomescape.payment.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
@ -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 roomescape.common.entity.PersistableBaseEntity
|
||||
import roomescape.payment.infrastructure.common.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "payment_detail")
|
||||
@ -1,4 +1,4 @@
|
||||
package roomescape.payment.infrastructure.persistence
|
||||
package com.sangdol.roomescape.payment.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
@ -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.EnumType
|
||||
import jakarta.persistence.Enumerated
|
||||
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
|
||||
|
||||
@Entity
|
||||
@ -1,4 +1,4 @@
|
||||
package roomescape.payment.infrastructure.persistence
|
||||
package com.sangdol.roomescape.payment.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
@ -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 org.springframework.http.ResponseEntity
|
||||
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
|
||||
@RequestMapping("/payments")
|
||||
@ -1,11 +1,11 @@
|
||||
package roomescape.payment.web
|
||||
package com.sangdol.roomescape.payment.web
|
||||
|
||||
import roomescape.payment.exception.PaymentErrorCode
|
||||
import roomescape.payment.exception.PaymentException
|
||||
import roomescape.payment.infrastructure.common.PaymentStatus
|
||||
import roomescape.payment.infrastructure.common.PaymentType
|
||||
import roomescape.payment.infrastructure.persistence.*
|
||||
import roomescape.payment.web.PaymentDetailResponse.*
|
||||
import com.sangdol.roomescape.payment.exception.PaymentErrorCode
|
||||
import com.sangdol.roomescape.payment.exception.PaymentException
|
||||
import com.sangdol.roomescape.payment.infrastructure.common.PaymentStatus
|
||||
import com.sangdol.roomescape.payment.infrastructure.common.PaymentType
|
||||
import com.sangdol.roomescape.payment.infrastructure.persistence.*
|
||||
import com.sangdol.roomescape.payment.web.PaymentDetailResponse.*
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@ -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.KotlinLogging
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.region.exception.RegionErrorCode
|
||||
import roomescape.region.exception.RegionException
|
||||
import roomescape.region.infrastructure.persistence.RegionRepository
|
||||
import roomescape.region.web.*
|
||||
import com.sangdol.roomescape.region.exception.RegionErrorCode
|
||||
import com.sangdol.roomescape.region.exception.RegionException
|
||||
import com.sangdol.roomescape.region.infrastructure.persistence.RegionRepository
|
||||
import com.sangdol.roomescape.region.web.*
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@ -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.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses
|
||||
import org.springframework.http.ResponseEntity
|
||||
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 {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package roomescape.region.exception
|
||||
package com.sangdol.roomescape.region.exception
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
import roomescape.common.exception.ErrorCode
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import com.sangdol.common.types.web.HttpStatus
|
||||
import com.sangdol.common.types.exception.ErrorCode
|
||||
import com.sangdol.common.types.exception.RoomescapeException
|
||||
|
||||
class RegionException(
|
||||
override val errorCode: RegionErrorCode,
|
||||
@ -1,4 +1,4 @@
|
||||
package roomescape.region.infrastructure.persistence
|
||||
package com.sangdol.roomescape.region.infrastructure.persistence
|
||||
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.Id
|
||||
@ -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.Query
|
||||
@ -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.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.region.business.RegionService
|
||||
import roomescape.region.docs.RegionAPI
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/regions")
|
||||
@ -1,4 +1,4 @@
|
||||
package roomescape.region.web
|
||||
package com.sangdol.roomescape.region.web
|
||||
|
||||
data class SidoResponse(
|
||||
val code: String,
|
||||
@ -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.KotlinLogging
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
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
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
@ -35,7 +32,7 @@ class ReservationService(
|
||||
private val userService: UserService,
|
||||
private val themeService: ThemeService,
|
||||
private val canceledReservationRepository: CanceledReservationRepository,
|
||||
private val tsidFactory: TsidFactory,
|
||||
private val idGenerator: IDGenerator,
|
||||
private val paymentService: PaymentService
|
||||
) {
|
||||
|
||||
@ -48,7 +45,7 @@ class ReservationService(
|
||||
|
||||
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)
|
||||
.also { log.info { "[ReservationService.createPendingReservation] Pending 예약 생성 완료: reservationId=${it}, schedule=${request.scheduleId}" } }
|
||||
@ -143,7 +140,7 @@ class ReservationService(
|
||||
}
|
||||
|
||||
CanceledReservationEntity(
|
||||
id = tsidFactory.next(),
|
||||
id = idGenerator.create(),
|
||||
reservationId = reservation.id,
|
||||
canceledBy = user.id,
|
||||
cancelReason = cancelReason,
|
||||
@ -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.KotlinLogging
|
||||
import org.springframework.stereotype.Component
|
||||
import roomescape.reservation.exception.ReservationErrorCode
|
||||
import roomescape.reservation.exception.ReservationException
|
||||
import roomescape.reservation.web.PendingReservationCreateRequest
|
||||
import roomescape.schedule.infrastructure.persistence.ScheduleStatus
|
||||
import roomescape.schedule.web.ScheduleSummaryResponse
|
||||
import roomescape.theme.web.ThemeInfoResponse
|
||||
import com.sangdol.roomescape.reservation.exception.ReservationErrorCode
|
||||
import com.sangdol.roomescape.reservation.exception.ReservationException
|
||||
import com.sangdol.roomescape.reservation.web.PendingReservationCreateRequest
|
||||
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
|
||||
import com.sangdol.roomescape.schedule.web.ScheduleSummaryResponse
|
||||
import com.sangdol.roomescape.theme.web.ThemeInfoResponse
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@ -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.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses
|
||||
@ -7,13 +12,6 @@ import jakarta.validation.Valid
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
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 {
|
||||
@Operation(summary = "결제 전 임시 예약 저장")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user