Compare commits

...

39 Commits

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

View File

@ -1,94 +1,52 @@
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 {
extensions.configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
extensions.configure<KaptExtension> {
keepJavacAnnotationProcessors = true
}
}
repositories {
mavenCentral()
}
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")
}
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.9")
// DB
implementation("com.github.f4b6a3:tsid-creator:5.2.6")
runtimeOnly("com.h2database:h2")
runtimeOnly("com.mysql:mysql-connector-j")
// Jwt
implementation("io.jsonwebtoken:jjwt:0.12.6")
// Logging
implementation("io.github.oshai:kotlin-logging-jvm:7.0.3")
implementation("net.logstash.logback:logstash-logback-encoder:8.1")
implementation("com.github.loki4j:loki-logback-appender:2.0.0")
implementation("net.ttddyy.observation:datasource-micrometer-spring-boot:1.1.1")
// Observability
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-tracing-bridge-otel")
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
// Kotlin
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.github.oshai:kotlin-logging-jvm:7.0.3")
// Test
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.mockk:mockk:1.14.4")
testImplementation("com.ninja-squad:springmockk:4.0.2")
// Kotest
testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.3.0")
// RestAssured
testImplementation("io.rest-assured:rest-assured:5.5.5")
testImplementation("io.rest-assured:kotlin-extensions:5.5.5")
// etc
implementation("org.apache.poi:poi-ooxml:5.2.3")
}
tasks.withType<Test> {
tasks.withType<Test> {
useJUnitPlatform()
}
}
tasks.withType<KotlinCompile> {
tasks.withType<KotlinCompile> {
compilerOptions {
freeCompilerArgs.addAll(
"-Xjsr305=strict",
@ -96,4 +54,5 @@ tasks.withType<KotlinCompile> {
)
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package roomescape.common.log
package com.sangdol.common.log.message
import ch.qos.logback.classic.pattern.MessageConverter
import ch.qos.logback.classic.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()}"
}
}

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
package roomescape.common.log
package com.sangdol.common.log
import com.sangdol.common.log.sql.SlowQueryPredicate
import com.sangdol.common.log.sql.SqlLogFormatter
import io.kotest.assertions.assertSoftly
import io.kotest.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
}
}
})

View File

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

View File

@ -1,14 +1,14 @@
package roomescape.common.entity
package com.sangdol.common.persistence
import jakarta.persistence.*
import jakarta.persistence.Column
import jakarta.persistence.EntityListeners
import jakarta.persistence.MappedSuperclass
import org.springframework.data.annotation.CreatedBy
import org.springframework.data.annotation.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
}

View File

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

View File

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

View File

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

View File

@ -1,31 +1,19 @@
package roomescape.common.util
package com.sangdol.common.persistence
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.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)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,10 @@
package roomescape.common.util
package com.sangdol.common.utils
import org.slf4j.MDC
import 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()

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
package roomescape.common.log
package com.sangdol.common.web.asepct
import com.sangdol.common.log.constant.LogType
import com.sangdol.common.web.support.log.WebLogMessageConverter
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.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,28 +40,21 @@ 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
if (log.isDebugEnabled()) {
convertResponseMessageRequest = convertResponseMessageRequest.copy(
body = responseEntity.body
val logMessage = messageConverter.convertToResponseMessage(
type = LogType.SUCCEED,
servletRequest = servletRequest,
httpStatusCode = result.statusCode.value(),
responseBody = 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
}
return payload
}

View File

@ -1,4 +1,4 @@
package roomescape.common.config
package com.sangdol.common.web.config
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.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 {

View File

@ -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)
}
}

View File

@ -1,27 +1,26 @@
package roomescape.common.exception
package com.sangdol.common.web.exception
import com.sangdol.common.log.constant.LogType
import com.sangdol.common.types.exception.CommonErrorCode
import com.sangdol.common.types.exception.ErrorCode
import com.sangdol.common.types.exception.RoomescapeException
import com.sangdol.common.types.web.CommonErrorResponse
import com.sangdol.common.types.web.HttpStatus
import com.sangdol.common.web.support.log.WebLogMessageConverter
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.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 }
}

View File

@ -1,20 +1,21 @@
package roomescape.common.log
package com.sangdol.common.web.servlet
import com.sangdol.common.utils.MdcPrincipalIdUtil
import com.sangdol.common.utils.MdcStartTimeUtil
import com.sangdol.common.web.support.log.WebLogMessageConverter
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.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()
}
}
}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package roomescape.common.config
package com.sangdol.common.web.config
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.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"

View File

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

View File

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

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

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

View File

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

View File

@ -1,17 +1,16 @@
package roomescape.admin.business
package com.sangdol.roomescape.admin.business
import com.sangdol.roomescape.common.types.Auditor
import com.sangdol.roomescape.admin.business.dto.AdminLoginCredentials
import com.sangdol.roomescape.admin.business.dto.toCredentials
import com.sangdol.roomescape.admin.exception.AdminErrorCode
import com.sangdol.roomescape.admin.exception.AdminException
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminRepository
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.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
}
}
}

View File

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

View File

@ -1,8 +1,8 @@
package roomescape.admin.exception
package com.sangdol.roomescape.admin.exception
import org.springframework.http.HttpStatus
import 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,

View File

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

View File

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

View File

@ -1,19 +1,15 @@
package roomescape.auth.business
package com.sangdol.roomescape.auth.business
import com.sangdol.roomescape.admin.business.AdminService
import com.sangdol.roomescape.auth.exception.AuthErrorCode
import com.sangdol.roomescape.auth.exception.AuthException
import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils
import com.sangdol.roomescape.auth.web.*
import com.sangdol.roomescape.user.business.UserService
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.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 {}

View File

@ -1,23 +1,22 @@
package roomescape.auth.business
package com.sangdol.roomescape.auth.business
import com.github.f4b6a3.tsid.TsidFactory
import com.sangdol.common.persistence.IDGenerator
import com.sangdol.roomescape.auth.infrastructure.persistence.LoginHistoryEntity
import com.sangdol.roomescape.auth.infrastructure.persistence.LoginHistoryRepository
import com.sangdol.roomescape.auth.web.LoginContext
import com.sangdol.roomescape.auth.web.PrincipalType
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.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,

View File

@ -1,5 +1,11 @@
package roomescape.auth.docs
package com.sangdol.roomescape.auth.docs
import com.sangdol.common.types.web.CommonApiResponse
import com.sangdol.roomescape.common.types.CurrentUserContext
import com.sangdol.roomescape.auth.web.LoginRequest
import com.sangdol.roomescape.auth.web.LoginSuccessResponse
import com.sangdol.roomescape.auth.web.support.Public
import com.sangdol.roomescape.auth.web.support.User
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.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 {

View File

@ -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,

View File

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

View File

@ -1,4 +1,4 @@
package roomescape.auth.infrastructure.jwt
package com.sangdol.roomescape.auth.infrastructure.jwt
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.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

View File

@ -1,10 +1,10 @@
package roomescape.auth.infrastructure.persistence
package com.sangdol.roomescape.auth.infrastructure.persistence
import com.sangdol.common.persistence.PersistableBaseEntity
import com.sangdol.roomescape.auth.web.PrincipalType
import jakarta.persistence.*
import 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

View File

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

View File

@ -1,16 +1,16 @@
package roomescape.auth.web
package com.sangdol.roomescape.auth.web
import com.sangdol.common.types.web.CommonApiResponse
import com.sangdol.roomescape.auth.business.AuthService
import com.sangdol.roomescape.auth.docs.AuthAPI
import com.sangdol.roomescape.auth.web.support.User
import com.sangdol.roomescape.common.types.CurrentUserContext
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.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")

View File

@ -1,8 +1,11 @@
package roomescape.auth.web
package com.sangdol.roomescape.auth.web
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType
import jakarta.servlet.http.HttpServletRequest
import 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
}

View File

@ -1,7 +1,7 @@
package roomescape.auth.web.support
package com.sangdol.roomescape.auth.web.support
import roomescape.admin.infrastructure.persistence.AdminType
import 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)

View File

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

View File

@ -1,4 +1,4 @@
package roomescape.auth.web.support.interceptors
package com.sangdol.roomescape.auth.web.support.interceptors
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.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)

View File

@ -1,4 +1,4 @@
package roomescape.auth.web.support.interceptors
package com.sangdol.roomescape.auth.web.support.interceptors
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.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 존재하면 관리자 토큰임

View File

@ -1,4 +1,4 @@
package roomescape.auth.web.support.resolver
package com.sangdol.roomescape.auth.web.support.resolver
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.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 {}

View File

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

View File

@ -1,8 +1,7 @@
package roomescape.common.log
package com.sangdol.roomescape.common.config
import com.sangdol.common.log.sql.SlowQueryDataSourceFactory
import com.zaxxer.hikari.HikariDataSource
import 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()),
): DataSource = SlowQueryDataSourceFactory.create(
dataSource = actualDataSource,
loggerName = properties.loggerName,
logLevel = properties.logLevel,
thresholdMs = properties.thresholdMs
)
)
.buildProxy()
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")

View File

@ -0,0 +1,15 @@
package com.sangdol.roomescape.common.config
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class SwaggerConfig {
@Bean
fun openAPI(): OpenAPI {
return OpenAPI()
}
}

View File

@ -1,12 +1,12 @@
package roomescape.common.config
package com.sangdol.roomescape.common.config
import org.springframework.context.annotation.Configuration
import org.springframework.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(

View File

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

View File

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

View File

@ -1,17 +1,17 @@
package roomescape.payment.business
package com.sangdol.roomescape.payment.business
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.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)
}
}

View File

@ -1,16 +1,15 @@
package roomescape.payment.business
package com.sangdol.roomescape.payment.business
import com.github.f4b6a3.tsid.TsidFactory
import com.sangdol.common.persistence.IDGenerator
import com.sangdol.roomescape.payment.exception.PaymentErrorCode
import com.sangdol.roomescape.payment.exception.PaymentException
import com.sangdol.roomescape.payment.infrastructure.client.*
import com.sangdol.roomescape.payment.infrastructure.common.PaymentMethod
import com.sangdol.roomescape.payment.infrastructure.common.PaymentType
import com.sangdol.roomescape.payment.infrastructure.persistence.*
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.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

View File

@ -1,5 +1,12 @@
package roomescape.payment.docs
package com.sangdol.roomescape.payment.docs
import com.sangdol.common.types.web.CommonApiResponse
import com.sangdol.roomescape.auth.web.support.User
import com.sangdol.roomescape.auth.web.support.UserOnly
import com.sangdol.roomescape.common.types.CurrentUserContext
import com.sangdol.roomescape.payment.web.PaymentCancelRequest
import com.sangdol.roomescape.payment.web.PaymentConfirmRequest
import com.sangdol.roomescape.payment.web.PaymentCreateResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.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 {

View File

@ -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,

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
package roomescape.payment.infrastructure.client
package com.sangdol.roomescape.payment.infrastructure.client
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.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

View File

@ -1,6 +1,8 @@
package roomescape.payment.infrastructure.client
package com.sangdol.roomescape.payment.infrastructure.client
import com.fasterxml.jackson.databind.ObjectMapper
import com.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 {}

View File

@ -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(

View File

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

View File

@ -1,10 +1,10 @@
package roomescape.payment.infrastructure.common
package com.sangdol.roomescape.payment.infrastructure.common
import com.fasterxml.jackson.annotation.JsonCreator
import 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 {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,13 @@
package roomescape.payment.web
package com.sangdol.roomescape.payment.web
import com.sangdol.common.types.web.CommonApiResponse
import com.sangdol.roomescape.auth.web.support.User
import com.sangdol.roomescape.common.types.CurrentUserContext
import com.sangdol.roomescape.payment.business.PaymentService
import com.sangdol.roomescape.payment.docs.PaymentAPI
import jakarta.validation.Valid
import 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")

View File

@ -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

View File

@ -1,13 +1,13 @@
package roomescape.region.business
package com.sangdol.roomescape.region.business
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.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 {}

View File

@ -1,15 +1,15 @@
package roomescape.region.docs
package com.sangdol.roomescape.region.docs
import com.sangdol.common.types.web.CommonApiResponse
import com.sangdol.roomescape.auth.web.support.Public
import com.sangdol.roomescape.region.web.RegionCodeResponse
import com.sangdol.roomescape.region.web.SidoListResponse
import com.sangdol.roomescape.region.web.SigunguListResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.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 {

View File

@ -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,

View File

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

View File

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

View File

@ -1,13 +1,13 @@
package roomescape.region.web
package com.sangdol.roomescape.region.web
import com.sangdol.common.types.web.CommonApiResponse
import com.sangdol.roomescape.region.business.RegionService
import com.sangdol.roomescape.region.docs.RegionAPI
import org.springframework.http.ResponseEntity
import org.springframework.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")

View File

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

View File

@ -1,28 +1,25 @@
package roomescape.reservation.business
package com.sangdol.roomescape.reservation.business
import com.github.f4b6a3.tsid.TsidFactory
import com.sangdol.common.persistence.IDGenerator
import com.sangdol.roomescape.common.types.CurrentUserContext
import com.sangdol.roomescape.payment.business.PaymentService
import com.sangdol.roomescape.payment.web.PaymentWithDetailResponse
import com.sangdol.roomescape.reservation.exception.ReservationErrorCode
import com.sangdol.roomescape.reservation.exception.ReservationException
import com.sangdol.roomescape.reservation.infrastructure.persistence.*
import com.sangdol.roomescape.reservation.web.*
import com.sangdol.roomescape.schedule.business.ScheduleService
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
import com.sangdol.roomescape.schedule.web.ScheduleOverviewResponse
import com.sangdol.roomescape.schedule.web.ScheduleUpdateRequest
import com.sangdol.roomescape.theme.business.ThemeService
import com.sangdol.roomescape.user.business.UserService
import com.sangdol.roomescape.user.web.UserContactResponse
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.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,

View File

@ -1,14 +1,14 @@
package roomescape.reservation.business
package com.sangdol.roomescape.reservation.business
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.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 {}

View File

@ -1,5 +1,10 @@
package roomescape.reservation.docs
package com.sangdol.roomescape.reservation.docs
import com.sangdol.common.types.web.CommonApiResponse
import com.sangdol.roomescape.common.types.CurrentUserContext
import com.sangdol.roomescape.auth.web.support.User
import com.sangdol.roomescape.auth.web.support.UserOnly
import com.sangdol.roomescape.reservation.web.*
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.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