generated from pricelees/issue-pr-template
Merge branch 'refactor/#20-1'
# Conflicts: # src/main/kotlin/roomescape/auth/docs/AuthAPI.kt # src/main/kotlin/roomescape/auth/service/AuthService.kt # src/main/kotlin/roomescape/member/business/MemberService.kt # src/main/kotlin/roomescape/member/web/MemberController.kt # src/main/kotlin/roomescape/member/web/MemberDTO.kt # src/test/kotlin/roomescape/auth/web/AuthControllerTest.kt
This commit is contained in:
commit
6149b8a563
@ -44,6 +44,11 @@ dependencies {
|
||||
// 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("net.ttddyy.observation:datasource-micrometer-spring-boot:1.1.1")
|
||||
|
||||
// Kotlin
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package roomescape.auth.service
|
||||
package roomescape.auth.business
|
||||
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
@ -9,6 +9,7 @@ import roomescape.auth.infrastructure.jwt.JwtHandler
|
||||
import roomescape.auth.web.LoginCheckResponse
|
||||
import roomescape.auth.web.LoginRequest
|
||||
import roomescape.auth.web.LoginResponse
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.member.business.MemberService
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
|
||||
@ -17,40 +18,50 @@ private val log: KLogger = KotlinLogging.logger {}
|
||||
@Service
|
||||
class AuthService(
|
||||
private val memberService: MemberService,
|
||||
private val jwtHandler: JwtHandler
|
||||
private val jwtHandler: JwtHandler,
|
||||
) {
|
||||
fun login(request: LoginRequest): LoginResponse {
|
||||
val member: MemberEntity = fetchMemberOrThrow(AuthErrorCode.LOGIN_FAILED) {
|
||||
log.debug { "[AuthService.login] 로그인 시작: email=${request.email}" }
|
||||
val params = "email=${request.email}, password=${request.password}"
|
||||
|
||||
val member: MemberEntity = fetchMemberOrThrow(AuthErrorCode.LOGIN_FAILED, params, "login") {
|
||||
memberService.findByEmailAndPassword(request.email, request.password)
|
||||
}
|
||||
|
||||
val accessToken: String = jwtHandler.createToken(member.id!!)
|
||||
|
||||
return LoginResponse(accessToken)
|
||||
.also { log.info { "[AuthService.login] 로그인 완료: memberId=${member.id}" } }
|
||||
}
|
||||
|
||||
fun checkLogin(memberId: Long): LoginCheckResponse {
|
||||
val member: MemberEntity = fetchMemberOrThrow(AuthErrorCode.UNIDENTIFIABLE_MEMBER) {
|
||||
log.debug { "[AuthService.checkLogin] 로그인 확인 시작: memberId=$memberId" }
|
||||
val member: MemberEntity =
|
||||
fetchMemberOrThrow(AuthErrorCode.UNIDENTIFIABLE_MEMBER, "memberId=$memberId", "checkLogin") {
|
||||
memberService.findById(memberId)
|
||||
}
|
||||
|
||||
return LoginCheckResponse(member.name, member.role.name)
|
||||
.also { log.info { "[AuthService.checkLogin] 로그인 확인 완료: memberId=$memberId" } }
|
||||
}
|
||||
|
||||
fun logout(memberId: Long) {
|
||||
log.info { "[AuthService.logout] 로그아웃: memberId=$memberId" }
|
||||
}
|
||||
|
||||
private fun fetchMemberOrThrow(
|
||||
errorCode: AuthErrorCode,
|
||||
block: () -> MemberEntity
|
||||
params: String,
|
||||
calledBy: String,
|
||||
block: () -> MemberEntity,
|
||||
): MemberEntity {
|
||||
try {
|
||||
log.debug { "[AuthService.$calledBy] 회원 조회 시작: $params" }
|
||||
return block()
|
||||
} catch (_: Exception) {
|
||||
} catch (e: Exception) {
|
||||
if (e !is RoomescapeException) {
|
||||
log.warn(e) { "[AuthService.$calledBy] 회원 조회 실패: $params" }
|
||||
}
|
||||
throw AuthException(errorCode)
|
||||
}
|
||||
}
|
||||
|
||||
fun logout(memberId: Long?) {
|
||||
if (memberId != null) {
|
||||
log.info { "requested logout for $memberId" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RequestBody
|
||||
import roomescape.auth.web.LoginCheckResponse
|
||||
import roomescape.auth.web.LoginRequest
|
||||
import roomescape.auth.web.LoginResponse
|
||||
import roomescape.auth.web.support.LoginRequired
|
||||
import roomescape.auth.web.support.MemberId
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
|
||||
@ -36,6 +37,7 @@ interface AuthAPI {
|
||||
@MemberId @Parameter(hidden = true) memberId: Long
|
||||
): ResponseEntity<CommonApiResponse<LoginCheckResponse>>
|
||||
|
||||
@LoginRequired
|
||||
@Operation(summary = "로그아웃", tags = ["로그인이 필요한 API"])
|
||||
@ApiResponses(
|
||||
ApiResponse(responseCode = "200", description = "로그아웃 성공시 쿠키에 저장된 토큰 정보를 삭제합니다."),
|
||||
|
||||
@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import roomescape.auth.docs.AuthAPI
|
||||
import roomescape.auth.service.AuthService
|
||||
import roomescape.auth.business.AuthService
|
||||
import roomescape.auth.web.support.MemberId
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
|
||||
|
||||
@ -16,7 +16,8 @@ class SwaggerConfig {
|
||||
private fun apiInfo(): Info {
|
||||
return Info()
|
||||
.title("방탈출 예약 API 문서")
|
||||
.description("""
|
||||
.description(
|
||||
"""
|
||||
## API 테스트는 '1. 인증 / 인가 API' 의 '/login' 을 통해 로그인 후 사용해주세요.
|
||||
|
||||
### 테스트시 로그인 가능한 계정 정보
|
||||
@ -70,7 +71,8 @@ class SwaggerConfig {
|
||||
|
||||
- 8 ~ 10: 예약 대기 상태
|
||||
|
||||
""".trimIndent())
|
||||
""".trimIndent()
|
||||
)
|
||||
.version("1.0.0")
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,12 +11,10 @@ import roomescape.common.dto.response.CommonErrorResponse
|
||||
|
||||
@RestControllerAdvice
|
||||
class ExceptionControllerAdvice(
|
||||
private val logger: KLogger = KotlinLogging.logger {}
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
) {
|
||||
@ExceptionHandler(value = [RoomescapeException::class])
|
||||
fun handleRoomException(e: RoomescapeException): ResponseEntity<CommonErrorResponse> {
|
||||
logger.error(e) { "message: ${e.message}" }
|
||||
|
||||
val errorCode: ErrorCode = e.errorCode
|
||||
return ResponseEntity
|
||||
.status(errorCode.httpStatus)
|
||||
@ -25,7 +23,7 @@ class ExceptionControllerAdvice(
|
||||
|
||||
@ExceptionHandler(value = [HttpMessageNotReadableException::class])
|
||||
fun handleHttpMessageNotReadableException(e: HttpMessageNotReadableException): ResponseEntity<CommonErrorResponse> {
|
||||
logger.error(e) { "message: ${e.message}" }
|
||||
log.debug { "message: ${e.message}" }
|
||||
|
||||
val errorCode: ErrorCode = CommonErrorCode.INVALID_INPUT_VALUE
|
||||
return ResponseEntity
|
||||
@ -38,7 +36,7 @@ class ExceptionControllerAdvice(
|
||||
val message: String = e.bindingResult.allErrors
|
||||
.mapNotNull { it.defaultMessage }
|
||||
.joinToString(", ")
|
||||
logger.error(e) { "message: $message" }
|
||||
log.debug { "message: $message" }
|
||||
|
||||
val errorCode: ErrorCode = CommonErrorCode.INVALID_INPUT_VALUE
|
||||
return ResponseEntity
|
||||
@ -48,7 +46,7 @@ class ExceptionControllerAdvice(
|
||||
|
||||
@ExceptionHandler(value = [Exception::class])
|
||||
fun handleException(e: Exception): ResponseEntity<CommonErrorResponse> {
|
||||
logger.error(e) { "message: ${e.message}" }
|
||||
log.error(e) { "message: ${e.message}" }
|
||||
|
||||
val errorCode: ErrorCode = CommonErrorCode.UNEXPECTED_SERVER_ERROR
|
||||
return ResponseEntity
|
||||
|
||||
74
src/main/kotlin/roomescape/common/log/LoggingFilter.kt
Normal file
74
src/main/kotlin/roomescape/common/log/LoggingFilter.kt
Normal file
@ -0,0 +1,74 @@
|
||||
package roomescape.common.log
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import jakarta.servlet.FilterChain
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.core.Ordered
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.filter.OncePerRequestFilter
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
class LoggingFilter(
|
||||
private val objectMapper: ObjectMapper
|
||||
) : OncePerRequestFilter() {
|
||||
|
||||
override fun doFilterInternal(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
filterChain: FilterChain
|
||||
) {
|
||||
val cachedRequest = ContentCachingRequestWrapper(request)
|
||||
val cachedResponse = ContentCachingResponseWrapper(response)
|
||||
|
||||
val startTime = System.currentTimeMillis()
|
||||
filterChain.doFilter(cachedRequest, cachedResponse)
|
||||
val duration = System.currentTimeMillis() - startTime
|
||||
|
||||
logAPISummary(cachedRequest, cachedResponse, duration)
|
||||
cachedResponse.copyBodyToResponse()
|
||||
|
||||
}
|
||||
|
||||
private fun logAPISummary(
|
||||
request: ContentCachingRequestWrapper,
|
||||
response: ContentCachingResponseWrapper,
|
||||
duration: Long
|
||||
) {
|
||||
val payload = linkedMapOf<String, Any>(
|
||||
"type" to "API_LOG",
|
||||
"method" to request.method,
|
||||
"url" to request.requestURL.toString(),
|
||||
)
|
||||
request.queryString?.let { payload["query_params"] = it }
|
||||
payload["remote_ip"] = request.remoteAddr
|
||||
payload["status_code"] = response.status
|
||||
payload["duration_ms"] = duration
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
request.contentAsByteArray.takeIf { it.isNotEmpty() }
|
||||
?.let { payload["request_body"] = parseContent(it) }
|
||||
|
||||
response.contentAsByteArray.takeIf { it.isNotEmpty() }
|
||||
?.let { payload["response_body"] = parseContent(it) }
|
||||
}
|
||||
|
||||
log.info { objectMapper.writeValueAsString(payload) }
|
||||
}
|
||||
|
||||
private fun parseContent(content: ByteArray): Any {
|
||||
return try {
|
||||
objectMapper.readValue(content, Map::class.java)
|
||||
} catch (_: Exception) {
|
||||
String(content, StandardCharsets.UTF_8)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package roomescape.common.log
|
||||
|
||||
import ch.qos.logback.classic.pattern.MessageConverter
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
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")
|
||||
|
||||
class RoomescapeLogMaskingConverter(
|
||||
private val objectMapper: ObjectMapper = JacksonConfig().objectMapper()
|
||||
) : MessageConverter() {
|
||||
override fun convert(event: ILoggingEvent): String {
|
||||
val message: String = event.formattedMessage
|
||||
|
||||
return if (isJsonString(message)) {
|
||||
maskedJsonString(message)
|
||||
} else {
|
||||
maskedPlainMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isJsonString(message: String): Boolean {
|
||||
val trimmed = message.trim()
|
||||
|
||||
return trimmed.startsWith("{") && trimmed.endsWith("}")
|
||||
}
|
||||
|
||||
private fun maskedJsonString(body: String): String = objectMapper.readValue(body, JsonNode::class.java)
|
||||
.apply { maskRecursive(this) }
|
||||
.toString()
|
||||
|
||||
private fun maskedPlainMessage(message: String): String {
|
||||
val keys: String = SENSITIVE_KEYS.joinToString("|")
|
||||
val regex = Regex("(?i)($keys)(\\s*=\\s*)(\\S+)")
|
||||
|
||||
return regex.replace(message) { matchResult ->
|
||||
val key = matchResult.groupValues[1]
|
||||
val delimiter = matchResult.groupValues[2]
|
||||
|
||||
"${key}${delimiter}${MASK}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun maskRecursive(node: JsonNode?) {
|
||||
node?.forEachEntry { key, childNode ->
|
||||
when {
|
||||
childNode.isValueNode -> {
|
||||
if (key in SENSITIVE_KEYS) (node as ObjectNode).put(key, MASK)
|
||||
}
|
||||
|
||||
childNode.isObject -> maskRecursive(childNode)
|
||||
childNode.isArray -> {
|
||||
val arrayNode = childNode as ArrayNode
|
||||
val originSize = arrayNode.size()
|
||||
if (originSize > 1) {
|
||||
val first = arrayNode.first()
|
||||
arrayNode.removeAll()
|
||||
arrayNode.add(first)
|
||||
arrayNode.add(TextNode("(...logged only first of $originSize elements)"))
|
||||
}
|
||||
|
||||
arrayNode.forEach { maskRecursive(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package roomescape.member.business
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
@ -10,26 +11,37 @@ import roomescape.member.infrastructure.persistence.MemberRepository
|
||||
import roomescape.member.infrastructure.persistence.Role
|
||||
import roomescape.member.web.*
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
class MemberService(
|
||||
private val memberRepository: MemberRepository
|
||||
private val memberRepository: MemberRepository,
|
||||
) {
|
||||
fun findMembers(): MemberRetrieveListResponse = MemberRetrieveListResponse(
|
||||
members = memberRepository.findAll().map { it.toRetrieveResponse() }
|
||||
)
|
||||
fun findMembers(): MemberRetrieveListResponse {
|
||||
log.debug { "[MemberService.findMembers] 회원 조회 시작" }
|
||||
|
||||
fun findById(memberId: Long): MemberEntity = fetchOrThrow {
|
||||
memberRepository.findByIdOrNull(memberId)
|
||||
return memberRepository.findAll()
|
||||
.also { log.info { "[MemberService.findMembers] 회원 ${it.size}명 조회 완료" } }
|
||||
.toRetrieveListResponse()
|
||||
}
|
||||
|
||||
fun findByEmailAndPassword(email: String, password: String): MemberEntity = fetchOrThrow {
|
||||
fun findById(memberId: Long): MemberEntity {
|
||||
return fetchOrThrow("findById", "memberId=$memberId") {
|
||||
memberRepository.findByIdOrNull(memberId)
|
||||
}
|
||||
}
|
||||
|
||||
fun findByEmailAndPassword(email: String, password: String): MemberEntity {
|
||||
return fetchOrThrow("findByEmailAndPassword", "email=$email, password=$password") {
|
||||
memberRepository.findByEmailAndPassword(email, password)
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun create(request: SignupRequest): SignupResponse {
|
||||
fun createMember(request: SignupRequest): SignupResponse {
|
||||
memberRepository.findByEmail(request.email)?.let {
|
||||
log.info { "[MemberService.createMember] 회원가입 실패(이메일 중복): email=${request.email}" }
|
||||
throw MemberException(MemberErrorCode.DUPLICATE_EMAIL)
|
||||
}
|
||||
|
||||
@ -39,10 +51,18 @@ class MemberService(
|
||||
password = request.password,
|
||||
role = Role.MEMBER
|
||||
)
|
||||
|
||||
return memberRepository.save(member).toSignupResponse()
|
||||
.also { log.info { "[MemberService.create] 회원가입 완료: email=${request.email} memberId=${it.id}" } }
|
||||
}
|
||||
|
||||
private fun fetchOrThrow(block: () -> MemberEntity?): MemberEntity {
|
||||
return block() ?: throw MemberException(MemberErrorCode.MEMBER_NOT_FOUND)
|
||||
private fun fetchOrThrow(calledBy: String, params: String, block: () -> MemberEntity?): MemberEntity {
|
||||
log.debug { "[MemberService.$calledBy] 회원 조회 시작: params=$params" }
|
||||
return block()
|
||||
?.also { log.info { "[MemberService.$calledBy] 회원 조회 완료: memberId=${it.id}" } }
|
||||
?: run {
|
||||
log.info { "[MemberService.$calledBy] 회원 조회 실패: $params" }
|
||||
throw MemberException(MemberErrorCode.MEMBER_NOT_FOUND)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ class MemberController(
|
||||
|
||||
@PostMapping("/members")
|
||||
override fun signup(@RequestBody request: SignupRequest): ResponseEntity<CommonApiResponse<SignupResponse>> {
|
||||
val response: SignupResponse = memberService.create(request)
|
||||
val response: SignupResponse = memberService.createMember(request)
|
||||
return ResponseEntity.created(URI.create("/members/${response.id}"))
|
||||
.body(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@ -16,6 +16,10 @@ data class MemberRetrieveResponse(
|
||||
val name: String
|
||||
)
|
||||
|
||||
fun List<MemberEntity>.toRetrieveListResponse(): MemberRetrieveListResponse = MemberRetrieveListResponse(
|
||||
members = this.map { it.toRetrieveResponse() }
|
||||
)
|
||||
|
||||
data class MemberRetrieveListResponse(
|
||||
val members: List<MemberRetrieveResponse>
|
||||
)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package roomescape.payment.business
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.payment.exception.PaymentErrorCode
|
||||
@ -16,16 +17,19 @@ import roomescape.payment.web.toCreateResponse
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class PaymentService(
|
||||
private val paymentRepository: PaymentRepository,
|
||||
private val canceledPaymentRepository: CanceledPaymentRepository
|
||||
private val canceledPaymentRepository: CanceledPaymentRepository,
|
||||
) {
|
||||
@Transactional
|
||||
fun createPayment(
|
||||
approveResponse: PaymentApproveResponse,
|
||||
reservation: ReservationEntity
|
||||
reservation: ReservationEntity,
|
||||
): PaymentCreateResponse {
|
||||
log.debug { "[PaymentService.createPayment] 결제 정보 저장 시작: request=$approveResponse, reservationId=${reservation.id}" }
|
||||
val payment = PaymentEntity(
|
||||
orderId = approveResponse.orderId,
|
||||
paymentKey = approveResponse.paymentKey,
|
||||
@ -34,18 +38,29 @@ class PaymentService(
|
||||
approvedAt = approveResponse.approvedAt
|
||||
)
|
||||
|
||||
return paymentRepository.save(payment).toCreateResponse()
|
||||
return paymentRepository.save(payment)
|
||||
.toCreateResponse()
|
||||
.also { log.info { "[PaymentService.createPayment] 결제 정보 저장 완료: paymentId=${it.id}, reservationId=${reservation.id}" } }
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun isReservationPaid(reservationId: Long): Boolean = paymentRepository.existsByReservationId(reservationId)
|
||||
fun isReservationPaid(reservationId: Long): Boolean {
|
||||
log.debug { "[PaymentService.isReservationPaid] 예약 결제 여부 확인 시작: reservationId=$reservationId" }
|
||||
|
||||
return paymentRepository.existsByReservationId(reservationId)
|
||||
.also { log.info { "[PaymentService.isReservationPaid] 예약 결제 여부 확인 완료: reservationId=$reservationId, isPaid=$it" } }
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun createCanceledPayment(
|
||||
cancelInfo: PaymentCancelResponse,
|
||||
approvedAt: OffsetDateTime,
|
||||
paymentKey: String
|
||||
paymentKey: String,
|
||||
): CanceledPaymentEntity {
|
||||
log.debug {
|
||||
"[PaymentService.createCanceledPayment] 결제 취소 정보 저장 시작: paymentKey=$paymentKey" +
|
||||
", cancelInfo=$cancelInfo"
|
||||
}
|
||||
val canceledPayment = CanceledPaymentEntity(
|
||||
paymentKey = paymentKey,
|
||||
cancelReason = cancelInfo.cancelReason,
|
||||
@ -55,27 +70,44 @@ class PaymentService(
|
||||
)
|
||||
|
||||
return canceledPaymentRepository.save(canceledPayment)
|
||||
.also {
|
||||
log.info {
|
||||
"[PaymentService.createCanceledPayment] 결제 취소 정보 생성 완료: canceledPaymentId=${it.id}" +
|
||||
", paymentKey=${paymentKey}, amount=${cancelInfo.cancelAmount}, canceledAt=${it.canceledAt}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun createCanceledPaymentByReservationId(reservationId: Long): PaymentCancelRequest {
|
||||
log.debug { "[PaymentService.createCanceledPaymentByReservationId] 예약 삭제 & 결제 취소 정보 저장 시작: reservationId=$reservationId" }
|
||||
val paymentKey: String = paymentRepository.findPaymentKeyByReservationId(reservationId)
|
||||
?: throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
|
||||
?: run {
|
||||
log.warn { "[PaymentService.createCanceledPaymentByReservationId] 예약 조회 실패: reservationId=$reservationId" }
|
||||
throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
|
||||
}
|
||||
|
||||
// 취소 시간은 현재 시간으로 일단 생성한 뒤, 결제 취소 완료 후 해당 시간으로 변경합니다.
|
||||
val canceled: CanceledPaymentEntity = cancelPayment(paymentKey)
|
||||
|
||||
return PaymentCancelRequest(paymentKey, canceled.cancelAmount, canceled.cancelReason)
|
||||
.also { log.info { "[PaymentService.createCanceledPaymentByReservationId] 예약 ID로 결제 취소 완료: reservationId=$reservationId" } }
|
||||
}
|
||||
|
||||
private fun cancelPayment(
|
||||
paymentKey: String,
|
||||
cancelReason: String = "고객 요청",
|
||||
canceledAt: OffsetDateTime = OffsetDateTime.now()
|
||||
canceledAt: OffsetDateTime = OffsetDateTime.now(),
|
||||
): CanceledPaymentEntity {
|
||||
log.debug { "[PaymentService.cancelPayment] 결제 취소 정보 저장 시작: paymentKey=$paymentKey" }
|
||||
val payment: PaymentEntity = paymentRepository.findByPaymentKey(paymentKey)
|
||||
?.also { paymentRepository.delete(it) }
|
||||
?: throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
|
||||
?.also {
|
||||
paymentRepository.delete(it)
|
||||
log.info { "[PaymentService.cancelPayment] 결제 정보 삭제 완료: paymentId=${it.id}, paymentKey=$paymentKey" }
|
||||
}
|
||||
?: run {
|
||||
log.warn { "[PaymentService.cancelPayment] 결제 정보 조회 실패: paymentKey=$paymentKey" }
|
||||
throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
|
||||
}
|
||||
|
||||
val canceledPayment = CanceledPaymentEntity(
|
||||
paymentKey = paymentKey,
|
||||
@ -86,15 +118,26 @@ class PaymentService(
|
||||
)
|
||||
|
||||
return canceledPaymentRepository.save(canceledPayment)
|
||||
.also { log.info { "[PaymentService.cancelPayment] 결제 취소 정보 저장 완료: canceledPaymentId=${it.id}" } }
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun updateCanceledTime(
|
||||
paymentKey: String,
|
||||
canceledAt: OffsetDateTime
|
||||
canceledAt: OffsetDateTime,
|
||||
) {
|
||||
log.debug { "[PaymentService.updateCanceledTime] 취소 시간 업데이트 시작: paymentKey=$paymentKey, canceledAt=$canceledAt" }
|
||||
canceledPaymentRepository.findByPaymentKey(paymentKey)
|
||||
?.apply { this.canceledAt = canceledAt }
|
||||
?: throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
|
||||
?.also {
|
||||
log.info {
|
||||
"[PaymentService.updateCanceledTime] 취소 시간 업데이트 완료: paymentKey=$paymentKey" +
|
||||
", canceledAt=$canceledAt"
|
||||
}
|
||||
}
|
||||
?: run {
|
||||
log.warn { "[PaymentService.updateCanceledTime] 결제 정보 조회 실패: paymentKey=$paymentKey" }
|
||||
throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,9 +15,10 @@ import roomescape.payment.web.PaymentCancelRequest
|
||||
import roomescape.payment.web.PaymentCancelResponse
|
||||
import java.util.Map
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@Component
|
||||
class TossPaymentClient(
|
||||
private val log: KLogger = KotlinLogging.logger {},
|
||||
private val objectMapper: ObjectMapper,
|
||||
tossPaymentClientBuilder: RestClient.Builder,
|
||||
) {
|
||||
@ -38,10 +39,13 @@ class TossPaymentClient(
|
||||
.retrieve()
|
||||
.onStatus(
|
||||
{ status: HttpStatusCode -> status.is4xxClientError || status.is5xxServerError },
|
||||
{ req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res) }
|
||||
{ req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res, "confirm") }
|
||||
)
|
||||
.body(PaymentApproveResponse::class.java)
|
||||
?: throw PaymentException(PaymentErrorCode.PAYMENT_PROVIDER_ERROR)
|
||||
?: run {
|
||||
log.error { "[TossPaymentClient] 응답 변환 오류" }
|
||||
throw PaymentException(PaymentErrorCode.PAYMENT_PROVIDER_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel(cancelRequest: PaymentCancelRequest): PaymentCancelResponse {
|
||||
@ -55,41 +59,43 @@ class TossPaymentClient(
|
||||
.retrieve()
|
||||
.onStatus(
|
||||
{ status: HttpStatusCode -> status.is4xxClientError || status.is5xxServerError },
|
||||
{ req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res) }
|
||||
{ req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res, "cancel") }
|
||||
)
|
||||
.body(PaymentCancelResponse::class.java)
|
||||
?: throw PaymentException(PaymentErrorCode.PAYMENT_PROVIDER_ERROR)
|
||||
?: run {
|
||||
log.error { "[TossPaymentClient] 응답 변환 오류" }
|
||||
throw PaymentException(PaymentErrorCode.PAYMENT_PROVIDER_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
private fun logPaymentInfo(paymentRequest: PaymentApproveRequest) {
|
||||
log.info {
|
||||
"결제 승인 요청: paymentKey=${paymentRequest.paymentKey}, orderId=${paymentRequest.orderId}, " +
|
||||
"amount=${paymentRequest.amount}, paymentType=${paymentRequest.paymentType}"
|
||||
"[TossPaymentClient.confirm] 결제 승인 요청: request: $paymentRequest"
|
||||
}
|
||||
}
|
||||
|
||||
private fun logPaymentCancelInfo(cancelRequest: PaymentCancelRequest) {
|
||||
log.info {
|
||||
"결제 취소 요청: paymentKey=${cancelRequest.paymentKey}, amount=${cancelRequest.amount}, " +
|
||||
"cancelReason=${cancelRequest.cancelReason}"
|
||||
"[TossPaymentClient.cancel] 결제 취소 요청: request: $cancelRequest"
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePaymentError(
|
||||
res: ClientHttpResponse
|
||||
res: ClientHttpResponse,
|
||||
calledBy: String
|
||||
): Nothing {
|
||||
getErrorCodeByHttpStatus(res.statusCode).also {
|
||||
logTossPaymentError(res)
|
||||
logTossPaymentError(res, calledBy)
|
||||
throw PaymentException(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun logTossPaymentError(res: ClientHttpResponse): TossPaymentErrorResponse {
|
||||
private fun logTossPaymentError(res: ClientHttpResponse, calledBy: String): TossPaymentErrorResponse {
|
||||
val body = res.body
|
||||
val errorResponse = objectMapper.readValue(body, TossPaymentErrorResponse::class.java)
|
||||
body.close()
|
||||
|
||||
log.error { "결제 실패. response: $errorResponse" }
|
||||
log.error { "[TossPaymentClient.$calledBy] 요청 실패: response: $errorResponse" }
|
||||
return errorResponse
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package roomescape.reservation.business
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.data.jpa.domain.Specification
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
@ -20,6 +21,8 @@ import roomescape.time.infrastructure.persistence.TimeEntity
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
class ReservationService(
|
||||
@ -34,8 +37,10 @@ class ReservationService(
|
||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||
.confirmed()
|
||||
.build()
|
||||
val reservations = findAllReservationByStatus(spec)
|
||||
log.info { "[ReservationService.findReservations] ${reservations.size} 개의 확정 예약 조회 완료" }
|
||||
|
||||
return ReservationRetrieveListResponse(findAllReservationByStatus(spec))
|
||||
return ReservationRetrieveListResponse(reservations)
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@ -43,8 +48,10 @@ class ReservationService(
|
||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||
.waiting()
|
||||
.build()
|
||||
val reservations = findAllReservationByStatus(spec)
|
||||
log.info { "[ReservationService.findAllWaiting] ${reservations.size} 개의 대기 예약 조회 완료" }
|
||||
|
||||
return ReservationRetrieveListResponse(findAllReservationByStatus(spec))
|
||||
return ReservationRetrieveListResponse(reservations)
|
||||
}
|
||||
|
||||
private fun findAllReservationByStatus(spec: Specification<ReservationEntity>): List<ReservationRetrieveResponse> {
|
||||
@ -52,45 +59,56 @@ class ReservationService(
|
||||
}
|
||||
|
||||
fun deleteReservation(reservationId: Long, memberId: Long) {
|
||||
validateIsMemberAdmin(memberId)
|
||||
validateIsMemberAdmin(memberId, "deleteReservation")
|
||||
log.info { "[ReservationService.deleteReservation] 예약 삭제 시작: reservationId=$reservationId, memberId=$memberId" }
|
||||
reservationRepository.deleteById(reservationId)
|
||||
log.info { "[ReservationService.deleteReservation] 예약 삭제 완료: reservationId=$reservationId" }
|
||||
}
|
||||
|
||||
fun createConfirmedReservation(
|
||||
request: ReservationCreateWithPaymentRequest,
|
||||
memberId: Long
|
||||
memberId: Long,
|
||||
): ReservationEntity {
|
||||
val themeId = request.themeId
|
||||
val timeId = request.timeId
|
||||
val date: LocalDate = request.date
|
||||
validateIsReservationExist(themeId, timeId, date)
|
||||
validateIsReservationExist(themeId, timeId, date, "createConfirmedReservation")
|
||||
|
||||
val reservation: ReservationEntity = createEntity(timeId, themeId, date, memberId, ReservationStatus.CONFIRMED)
|
||||
log.debug { "[ReservationService.createConfirmedReservation] 예약 추가 시작: memberId=$memberId, themeId=${request.themeId}, timeId=${request.timeId}, date=${request.date}" }
|
||||
val reservation: ReservationEntity =
|
||||
createEntity(timeId, themeId, date, memberId, ReservationStatus.CONFIRMED)
|
||||
|
||||
return reservationRepository.save(reservation)
|
||||
.also { log.info { "[ReservationService.createConfirmedReservation] 예약 추가 완료: reservationId=${it.id}, status=${it.reservationStatus}" } }
|
||||
}
|
||||
|
||||
fun createReservationByAdmin(request: AdminReservationCreateRequest): ReservationRetrieveResponse {
|
||||
validateIsReservationExist(request.themeId, request.timeId, request.date)
|
||||
|
||||
log.debug { "[ReservationService.createReservationByAdmin] 관리자의 예약 추가: memberId=${request.memberId}, themeId=${request.themeId}, timeId=${request.timeId}, date=${request.date}" }
|
||||
return addReservationWithoutPayment(
|
||||
request.themeId,
|
||||
request.timeId,
|
||||
request.date,
|
||||
request.memberId,
|
||||
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
)
|
||||
).also {
|
||||
log.info { "[ReservationService.createReservationByAdmin] 관리자 예약 추가 완료: reservationId=${it.id}" }
|
||||
}
|
||||
}
|
||||
|
||||
fun createWaiting(request: WaitingCreateRequest, memberId: Long): ReservationRetrieveResponse {
|
||||
validateMemberAlreadyReserve(request.themeId, request.timeId, request.date, memberId)
|
||||
log.debug { "[ReservationService.createWaiting] 예약 대기 추가 시작: memberId=$memberId, themeId=${request.themeId}, timeId=${request.timeId}, date=${request.date}" }
|
||||
return addReservationWithoutPayment(
|
||||
request.themeId,
|
||||
request.timeId,
|
||||
request.date,
|
||||
memberId,
|
||||
ReservationStatus.WAITING
|
||||
)
|
||||
).also {
|
||||
log.info { "[ReservationService.createWaiting] 예약 대기 추가 완료: reservationId=${it.id}, status=${it.status}" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun addReservationWithoutPayment(
|
||||
@ -98,13 +116,16 @@ class ReservationService(
|
||||
timeId: Long,
|
||||
date: LocalDate,
|
||||
memberId: Long,
|
||||
status: ReservationStatus
|
||||
status: ReservationStatus,
|
||||
): ReservationRetrieveResponse = createEntity(timeId, themeId, date, memberId, status)
|
||||
.also {
|
||||
reservationRepository.save(it)
|
||||
}.toRetrieveResponse()
|
||||
|
||||
private fun validateMemberAlreadyReserve(themeId: Long, timeId: Long, date: LocalDate, memberId: Long) {
|
||||
log.debug {
|
||||
"[ReservationService.validateMemberAlreadyReserve] 회원의 중복 예약 여부 확인: themeId=$themeId, timeId=$timeId, date=$date, memberId=$memberId"
|
||||
}
|
||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||
.sameMemberId(memberId)
|
||||
.sameThemeId(themeId)
|
||||
@ -113,11 +134,20 @@ class ReservationService(
|
||||
.build()
|
||||
|
||||
if (reservationRepository.exists(spec)) {
|
||||
log.warn { "[ReservationService.validateMemberAlreadyReserve] 중복된 예약 존재: themeId=$themeId, timeId=$timeId, date=$date" }
|
||||
throw ReservationException(ReservationErrorCode.ALREADY_RESERVE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateIsReservationExist(themeId: Long, timeId: Long, date: LocalDate) {
|
||||
private fun validateIsReservationExist(
|
||||
themeId: Long,
|
||||
timeId: Long,
|
||||
date: LocalDate,
|
||||
calledBy: String = "validateIsReservationExist"
|
||||
) {
|
||||
log.debug {
|
||||
"[ReservationService.$calledBy] 예약 존재 여부 확인: themeId=$themeId, timeId=$timeId, date=$date"
|
||||
}
|
||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||
.confirmed()
|
||||
.sameThemeId(themeId)
|
||||
@ -126,18 +156,20 @@ class ReservationService(
|
||||
.build()
|
||||
|
||||
if (reservationRepository.exists(spec)) {
|
||||
log.warn { "[ReservationService.$calledBy] 중복된 예약 존재: themeId=$themeId, timeId=$timeId, date=$date" }
|
||||
throw ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateDateAndTime(
|
||||
requestDate: LocalDate,
|
||||
requestTime: TimeEntity
|
||||
requestTime: TimeEntity,
|
||||
) {
|
||||
val now = LocalDateTime.now()
|
||||
val request = LocalDateTime.of(requestDate, requestTime.startAt)
|
||||
|
||||
if (request.isBefore(now)) {
|
||||
log.info { "[ReservationService.validateDateAndTime] 날짜 범위 오류. request=$request, now=$now" }
|
||||
throw ReservationException(ReservationErrorCode.PAST_REQUEST_DATETIME)
|
||||
}
|
||||
}
|
||||
@ -147,7 +179,7 @@ class ReservationService(
|
||||
themeId: Long,
|
||||
date: LocalDate,
|
||||
memberId: Long,
|
||||
status: ReservationStatus
|
||||
status: ReservationStatus,
|
||||
): ReservationEntity {
|
||||
val time: TimeEntity = timeService.findById(timeId)
|
||||
val theme: ThemeEntity = themeService.findById(themeId)
|
||||
@ -169,9 +201,10 @@ class ReservationService(
|
||||
themeId: Long?,
|
||||
memberId: Long?,
|
||||
dateFrom: LocalDate?,
|
||||
dateTo: LocalDate?
|
||||
dateTo: LocalDate?,
|
||||
): ReservationRetrieveListResponse {
|
||||
validateDateForSearch(dateFrom, dateTo)
|
||||
log.debug { "[ReservationService.searchReservations] 예약 검색 시작: themeId=$themeId, memberId=$memberId, dateFrom=$dateFrom, dateTo=$dateTo" }
|
||||
validateSearchDateRange(dateFrom, dateTo)
|
||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||
.confirmed()
|
||||
.sameThemeId(themeId)
|
||||
@ -179,63 +212,108 @@ class ReservationService(
|
||||
.dateStartFrom(dateFrom)
|
||||
.dateEndAt(dateTo)
|
||||
.build()
|
||||
val reservations = findAllReservationByStatus(spec)
|
||||
|
||||
return ReservationRetrieveListResponse(findAllReservationByStatus(spec))
|
||||
return ReservationRetrieveListResponse(reservations)
|
||||
.also { log.info { "[ReservationService.searchReservations] 예약 ${reservations.size}개 조회 완료: themeId=$themeId, memberId=$memberId, dateFrom=$dateFrom, dateTo=$dateTo" } }
|
||||
}
|
||||
|
||||
private fun validateDateForSearch(startFrom: LocalDate?, endAt: LocalDate?) {
|
||||
private fun validateSearchDateRange(startFrom: LocalDate?, endAt: LocalDate?) {
|
||||
if (startFrom == null || endAt == null) {
|
||||
return
|
||||
}
|
||||
if (startFrom.isAfter(endAt)) {
|
||||
log.info { "[ReservationService.validateSearchDateRange] 조회 범위 오류: startFrom=$startFrom, endAt=$endAt" }
|
||||
throw ReservationException(ReservationErrorCode.INVALID_SEARCH_DATE_RANGE)
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findReservationsByMemberId(memberId: Long): MyReservationRetrieveListResponse {
|
||||
return MyReservationRetrieveListResponse(reservationRepository.findAllByMemberId(memberId))
|
||||
val reservations = reservationRepository.findAllByMemberId(memberId)
|
||||
log.info { "[ReservationService.findReservationsByMemberId] memberId=${memberId}인 ${reservations.size}개의 예약 조회 완료" }
|
||||
return MyReservationRetrieveListResponse(reservations)
|
||||
}
|
||||
|
||||
fun confirmWaiting(reservationId: Long, memberId: Long) {
|
||||
validateIsMemberAdmin(memberId)
|
||||
log.debug { "[ReservationService.confirmWaiting] 대기 예약 승인 시작: reservationId=$reservationId (by adminId=$memberId)" }
|
||||
validateIsMemberAdmin(memberId, "confirmWaiting")
|
||||
|
||||
log.debug { "[ReservationService.confirmWaiting] 대기 여부 확인 시작: reservationId=$reservationId" }
|
||||
if (reservationRepository.isExistConfirmedReservation(reservationId)) {
|
||||
log.warn { "[ReservationService.confirmWaiting] 승인 실패(이미 확정된 예약 존재): reservationId=$reservationId" }
|
||||
throw ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS)
|
||||
}
|
||||
|
||||
log.debug { "[ReservationService.confirmWaiting] 대기 예약 상태 변경 시작: reservationId=$reservationId" }
|
||||
reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)
|
||||
log.debug { "[ReservationService.confirmWaiting] 대기 예약 상태 변경 완료: reservationId=$reservationId, status=${ReservationStatus.CONFIRMED_PAYMENT_REQUIRED}" }
|
||||
|
||||
log.info { "[ReservationService.confirmWaiting] 대기 예약 승인 완료: reservationId=$reservationId" }
|
||||
}
|
||||
|
||||
fun deleteWaiting(reservationId: Long, memberId: Long) {
|
||||
val reservation: ReservationEntity = findReservationOrThrow(reservationId)
|
||||
log.debug { "[ReservationService.deleteWaiting] 대기 취소 시작: reservationId=$reservationId, memberId=$memberId" }
|
||||
|
||||
val reservation: ReservationEntity = findReservationOrThrow(reservationId, "deleteWaiting")
|
||||
if (!reservation.isWaiting()) {
|
||||
log.warn {
|
||||
"[ReservationService.deleteWaiting] 대기 취소 실패(대기 예약이 아님): reservationId=$reservationId" +
|
||||
", currentStatus=${reservation.reservationStatus} memberId=$memberId"
|
||||
}
|
||||
throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
|
||||
}
|
||||
if (!reservation.isReservedBy(memberId)) {
|
||||
log.error {
|
||||
"[ReservationService.deleteWaiting] 대기 취소 실패(예약자 본인의 취소 요청이 아님): reservationId=$reservationId" +
|
||||
", memberId=$memberId "
|
||||
}
|
||||
throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER)
|
||||
}
|
||||
log.debug { "[ReservationService.deleteWaiting] 대기 예약 삭제 시작: reservationId=$reservationId" }
|
||||
reservationRepository.delete(reservation)
|
||||
log.debug { "[ReservationService.deleteWaiting] 대기 예약 삭제 완료: reservationId=$reservationId" }
|
||||
|
||||
log.info { "[ReservationService.deleteWaiting] 대기 취소 완료: reservationId=$reservationId, memberId=$memberId" }
|
||||
}
|
||||
|
||||
fun rejectWaiting(reservationId: Long, memberId: Long) {
|
||||
validateIsMemberAdmin(memberId)
|
||||
val reservation: ReservationEntity = findReservationOrThrow(reservationId)
|
||||
validateIsMemberAdmin(memberId, "rejectWaiting")
|
||||
log.debug { "[ReservationService.rejectWaiting] 대기 예약 삭제 시작: reservationId=$reservationId (by adminId=$memberId)" }
|
||||
val reservation: ReservationEntity = findReservationOrThrow(reservationId, "rejectWaiting")
|
||||
|
||||
if (!reservation.isWaiting()) {
|
||||
log.warn {
|
||||
"[ReservationService.rejectWaiting] 대기 예약 삭제 실패(이미 확정 상태): reservationId=$reservationId" +
|
||||
", status=${reservation.reservationStatus}"
|
||||
}
|
||||
throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
|
||||
}
|
||||
reservationRepository.delete(reservation)
|
||||
log.info { "[ReservationService.rejectWaiting] 대기 예약 삭제 완료: reservationId=$reservationId" }
|
||||
}
|
||||
|
||||
private fun validateIsMemberAdmin(memberId: Long) {
|
||||
private fun validateIsMemberAdmin(memberId: Long, calledBy: String = "validateIsMemberAdmin") {
|
||||
log.debug { "[ReservationService.$calledBy] 관리자 여부 확인: memberId=$memberId" }
|
||||
val member: MemberEntity = memberService.findById(memberId)
|
||||
if (member.isAdmin()) {
|
||||
return
|
||||
}
|
||||
log.warn { "[ReservationService.$calledBy] 관리자가 아님: memberId=$memberId, role=${member.role}" }
|
||||
throw ReservationException(ReservationErrorCode.NO_PERMISSION)
|
||||
}
|
||||
|
||||
private fun findReservationOrThrow(reservationId: Long): ReservationEntity {
|
||||
private fun findReservationOrThrow(
|
||||
reservationId: Long,
|
||||
calledBy: String = "findReservationOrThrow"
|
||||
): ReservationEntity {
|
||||
log.debug { "[ReservationService.$calledBy] 예약 조회: reservationId=$reservationId" }
|
||||
return reservationRepository.findByIdOrNull(reservationId)
|
||||
?: throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND)
|
||||
?.also { log.info { "[ReservationService.$calledBy] 예약 조회 완료: reservationId=$reservationId" } }
|
||||
?: run {
|
||||
log.warn { "[ReservationService.$calledBy] 예약 조회 실패: reservationId=$reservationId" }
|
||||
throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package roomescape.reservation.business
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.payment.business.PaymentService
|
||||
@ -11,47 +12,57 @@ import roomescape.reservation.web.ReservationCreateWithPaymentRequest
|
||||
import roomescape.reservation.web.ReservationRetrieveResponse
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
class ReservationWithPaymentService(
|
||||
private val reservationService: ReservationService,
|
||||
private val paymentService: PaymentService
|
||||
private val paymentService: PaymentService,
|
||||
) {
|
||||
fun createReservationAndPayment(
|
||||
request: ReservationCreateWithPaymentRequest,
|
||||
paymentInfo: PaymentApproveResponse,
|
||||
memberId: Long
|
||||
memberId: Long,
|
||||
): ReservationRetrieveResponse {
|
||||
log.info { "[ReservationWithPaymentService.createReservationAndPayment] 예약 & 결제 정보 저장 시작: memberId=$memberId, paymentInfo=$paymentInfo" }
|
||||
val reservation: ReservationEntity = reservationService.createConfirmedReservation(request, memberId)
|
||||
|
||||
return paymentService.createPayment(paymentInfo, reservation)
|
||||
.also { log.info { "[ReservationWithPaymentService.createReservationAndPayment] 예약 & 결제 정보 저장 완료: reservationId=${reservation.id}, paymentId=${it.id}" } }
|
||||
.reservation
|
||||
}
|
||||
|
||||
fun createCanceledPayment(
|
||||
cancelInfo: PaymentCancelResponse,
|
||||
approvedAt: OffsetDateTime,
|
||||
paymentKey: String
|
||||
paymentKey: String,
|
||||
) {
|
||||
paymentService.createCanceledPayment(cancelInfo, approvedAt, paymentKey)
|
||||
}
|
||||
|
||||
fun deleteReservationAndPayment(
|
||||
reservationId: Long,
|
||||
memberId: Long
|
||||
memberId: Long,
|
||||
): PaymentCancelRequest {
|
||||
log.info { "[ReservationWithPaymentService.deleteReservationAndPayment] 결제 취소 정보 저장 & 예약 삭제 시작: reservationId=$reservationId" }
|
||||
val paymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId)
|
||||
reservationService.deleteReservation(reservationId, memberId)
|
||||
|
||||
reservationService.deleteReservation(reservationId, memberId)
|
||||
log.info { "[ReservationWithPaymentService.deleteReservationAndPayment] 결제 취소 정보 저장 & 예약 삭제 완료: reservationId=$reservationId" }
|
||||
return paymentCancelRequest
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun isNotPaidReservation(reservationId: Long): Boolean = !paymentService.isReservationPaid(reservationId)
|
||||
fun isNotPaidReservation(reservationId: Long): Boolean {
|
||||
log.debug { "[ReservationWithPaymentService.isNotPaidReservation] 예약 결제 여부 확인: reservationId=$reservationId" }
|
||||
return !paymentService.isReservationPaid(reservationId)
|
||||
.also { log.info { "[ReservationWithPaymentService.isNotPaidReservation] 결제 여부 확인 완료: reservationId=$reservationId, 결제 여부=${!it}" } }
|
||||
}
|
||||
|
||||
fun updateCanceledTime(
|
||||
paymentKey: String,
|
||||
canceledAt: OffsetDateTime
|
||||
canceledAt: OffsetDateTime,
|
||||
) {
|
||||
paymentService.updateCanceledTime(paymentKey, canceledAt)
|
||||
}
|
||||
|
||||
@ -64,7 +64,11 @@ interface ReservationAPI {
|
||||
responseCode = "201",
|
||||
description = "성공",
|
||||
useReturnTypeSchema = true,
|
||||
headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))]
|
||||
headers = [Header(
|
||||
name = HttpHeaders.LOCATION,
|
||||
description = "생성된 예약 정보 URL",
|
||||
schema = Schema(example = "/reservations/1")
|
||||
)]
|
||||
)
|
||||
)
|
||||
fun createReservationWithPayment(
|
||||
@ -79,7 +83,11 @@ interface ReservationAPI {
|
||||
responseCode = "201",
|
||||
description = "성공",
|
||||
useReturnTypeSchema = true,
|
||||
headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))],
|
||||
headers = [Header(
|
||||
name = HttpHeaders.LOCATION,
|
||||
description = "생성된 예약 정보 URL",
|
||||
schema = Schema(example = "/reservations/1")
|
||||
)],
|
||||
)
|
||||
)
|
||||
fun createReservationByAdmin(
|
||||
@ -98,7 +106,11 @@ interface ReservationAPI {
|
||||
responseCode = "201",
|
||||
description = "성공",
|
||||
useReturnTypeSchema = true,
|
||||
headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))]
|
||||
headers = [Header(
|
||||
name = HttpHeaders.LOCATION,
|
||||
description = "생성된 예약 정보 URL",
|
||||
schema = Schema(example = "/reservations/1")
|
||||
)]
|
||||
)
|
||||
)
|
||||
fun createWaiting(
|
||||
|
||||
@ -16,17 +16,20 @@ interface ReservationRepository
|
||||
fun findByDateAndThemeId(date: LocalDate, themeId: Long): List<ReservationEntity>
|
||||
|
||||
@Modifying
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
UPDATE ReservationEntity r
|
||||
SET r.reservationStatus = :status
|
||||
WHERE r.id = :id
|
||||
""")
|
||||
"""
|
||||
)
|
||||
fun updateStatusByReservationId(
|
||||
@Param(value = "id") reservationId: Long,
|
||||
@Param(value = "status") statusForChange: ReservationStatus
|
||||
): Int
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM ReservationEntity r2
|
||||
@ -39,10 +42,12 @@ interface ReservationRepository
|
||||
AND r.reservationStatus != 'WAITING'
|
||||
)
|
||||
)
|
||||
""")
|
||||
"""
|
||||
)
|
||||
fun isExistConfirmedReservation(@Param("id") reservationId: Long): Boolean
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT new roomescape.reservation.web.MyReservationRetrieveResponse(
|
||||
r.id,
|
||||
t.name,
|
||||
@ -58,6 +63,7 @@ interface ReservationRepository
|
||||
LEFT JOIN PaymentEntity p
|
||||
ON p.reservation = r
|
||||
WHERE r.member.id = :memberId
|
||||
""")
|
||||
"""
|
||||
)
|
||||
fun findAllByMemberId(memberId: Long): List<MyReservationRetrieveResponse>
|
||||
}
|
||||
|
||||
@ -67,8 +67,10 @@ class ReservationController(
|
||||
|
||||
val paymentCancelRequest = reservationWithPaymentService.deleteReservationAndPayment(reservationId, memberId)
|
||||
val paymentCancelResponse = paymentClient.cancel(paymentCancelRequest)
|
||||
reservationWithPaymentService.updateCanceledTime(paymentCancelRequest.paymentKey,
|
||||
paymentCancelResponse.canceledAt)
|
||||
reservationWithPaymentService.updateCanceledTime(
|
||||
paymentCancelRequest.paymentKey,
|
||||
paymentCancelResponse.canceledAt
|
||||
)
|
||||
|
||||
return ResponseEntity.noContent().build()
|
||||
}
|
||||
@ -82,7 +84,8 @@ class ReservationController(
|
||||
val paymentResponse: PaymentApproveResponse = paymentClient.confirm(paymentRequest)
|
||||
|
||||
try {
|
||||
val reservationRetrieveResponse: ReservationRetrieveResponse = reservationWithPaymentService.createReservationAndPayment(
|
||||
val reservationRetrieveResponse: ReservationRetrieveResponse =
|
||||
reservationWithPaymentService.createReservationAndPayment(
|
||||
reservationCreateWithPaymentRequest,
|
||||
paymentResponse,
|
||||
memberId
|
||||
@ -90,11 +93,17 @@ class ReservationController(
|
||||
return ResponseEntity.created(URI.create("/reservations/${reservationRetrieveResponse.id}"))
|
||||
.body(CommonApiResponse(reservationRetrieveResponse))
|
||||
} catch (e: Exception) {
|
||||
val cancelRequest = PaymentCancelRequest(paymentRequest.paymentKey,
|
||||
paymentRequest.amount, e.message!!)
|
||||
val cancelRequest = PaymentCancelRequest(
|
||||
paymentRequest.paymentKey,
|
||||
paymentRequest.amount,
|
||||
e.message!!
|
||||
)
|
||||
val paymentCancelResponse = paymentClient.cancel(cancelRequest)
|
||||
reservationWithPaymentService.createCanceledPayment(paymentCancelResponse, paymentResponse.approvedAt,
|
||||
paymentRequest.paymentKey)
|
||||
reservationWithPaymentService.createCanceledPayment(
|
||||
paymentCancelResponse,
|
||||
paymentResponse.approvedAt,
|
||||
paymentRequest.paymentKey
|
||||
)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package roomescape.theme.business
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
@ -10,44 +11,71 @@ import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||
import roomescape.theme.web.*
|
||||
import java.time.LocalDate
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class ThemeService(
|
||||
private val themeRepository: ThemeRepository
|
||||
private val themeRepository: ThemeRepository,
|
||||
) {
|
||||
@Transactional(readOnly = true)
|
||||
fun findById(id: Long): ThemeEntity = themeRepository.findByIdOrNull(id)
|
||||
?: throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
|
||||
fun findById(id: Long): ThemeEntity {
|
||||
log.debug { "[ThemeService.findById] 테마 조회 시작: themeId=$id" }
|
||||
|
||||
return themeRepository.findByIdOrNull(id)
|
||||
?.also { log.info { "[ThemeService.findById] 테마 조회 완료: themeId=$id" } }
|
||||
?: run {
|
||||
log.warn { "[ThemeService.findById] 테마 조회 실패: themeId=$id" }
|
||||
throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findThemes(): ThemeRetrieveListResponse = themeRepository.findAll()
|
||||
fun findThemes(): ThemeRetrieveListResponse {
|
||||
log.debug { "[ThemeService.findThemes] 모든 테마 조회 시작" }
|
||||
|
||||
return themeRepository.findAll()
|
||||
.also { log.info { "[ThemeService.findThemes] ${it.size}개의 테마 조회 완료" } }
|
||||
.toResponse()
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findMostReservedThemes(count: Int): ThemeRetrieveListResponse {
|
||||
log.debug { "[ThemeService.findMostReservedThemes] 인기 테마 조회 시작: count=$count" }
|
||||
|
||||
val today = LocalDate.now()
|
||||
val startDate = today.minusDays(7)
|
||||
val endDate = today.minusDays(1)
|
||||
|
||||
return themeRepository.findPopularThemes(startDate, endDate, count)
|
||||
.also { log.info { "[ThemeService.findMostReservedThemes] ${it.size} 개의 인기 테마 조회 완료" } }
|
||||
.toResponse()
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun createTheme(request: ThemeCreateRequest): ThemeRetrieveResponse {
|
||||
log.debug { "[ThemeService.createTheme] 테마 생성 시작: name=${request.name}" }
|
||||
|
||||
if (themeRepository.existsByName(request.name)) {
|
||||
log.info { "[ThemeService.createTheme] 테마 생성 실패(이름 중복): name=${request.name}" }
|
||||
throw ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED)
|
||||
}
|
||||
|
||||
val theme: ThemeEntity = request.toEntity()
|
||||
return themeRepository.save(theme).toResponse()
|
||||
return themeRepository.save(theme)
|
||||
.also { log.info { "[ThemeService.createTheme] 테마 생성 완료: themeId=${it.id}" } }
|
||||
.toResponse()
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun deleteTheme(id: Long) {
|
||||
log.debug { "[ThemeService.deleteTheme] 테마 삭제 시작: themeId=$id" }
|
||||
|
||||
if (themeRepository.isReservedTheme(id)) {
|
||||
log.info { "[ThemeService.deleteTheme] 테마 삭제 실패(예약이 있는 테마): themeId=$id" }
|
||||
throw ThemeException(ThemeErrorCode.THEME_ALREADY_RESERVED)
|
||||
}
|
||||
|
||||
themeRepository.deleteById(id)
|
||||
.also { log.info { "[ThemeService.deleteTheme] 테마 삭제 완료: themeId=$id" } }
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,8 @@ import java.time.LocalDate
|
||||
|
||||
interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
|
||||
|
||||
@Query(value = """
|
||||
@Query(
|
||||
value = """
|
||||
SELECT t
|
||||
FROM ThemeEntity t
|
||||
RIGHT JOIN ReservationEntity r ON t.id = r.theme.id
|
||||
@ -20,12 +21,14 @@ interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
|
||||
|
||||
fun existsByName(name: String): Boolean
|
||||
|
||||
@Query(value = """
|
||||
@Query(
|
||||
value = """
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM ReservationEntity r
|
||||
WHERE r.theme.id = :id
|
||||
)
|
||||
""")
|
||||
"""
|
||||
)
|
||||
fun isReservedTheme(id: Long): Boolean
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package roomescape.time.business
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
@ -13,50 +14,87 @@ import roomescape.time.web.*
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class TimeService(
|
||||
private val timeRepository: TimeRepository,
|
||||
private val reservationRepository: ReservationRepository
|
||||
private val reservationRepository: ReservationRepository,
|
||||
) {
|
||||
@Transactional(readOnly = true)
|
||||
fun findById(id: Long): TimeEntity = timeRepository.findByIdOrNull(id)
|
||||
?: throw TimeException(TimeErrorCode.TIME_NOT_FOUND)
|
||||
fun findById(id: Long): TimeEntity {
|
||||
log.debug { "[TimeService.findById] 시간 조회 시작: timeId=$id" }
|
||||
return timeRepository.findByIdOrNull(id)
|
||||
?.also { log.info { "[TimeService.findById] 시간 조회 완료: timeId=$id" } }
|
||||
?: run {
|
||||
log.warn { "[TimeService.findById] 시간 조회 실패: timeId=$id" }
|
||||
throw TimeException(TimeErrorCode.TIME_NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findTimes(): TimeRetrieveListResponse = timeRepository.findAll()
|
||||
fun findTimes(): TimeRetrieveListResponse {
|
||||
log.debug { "[TimeService.findTimes] 모든 시간 조회 시작" }
|
||||
return timeRepository.findAll()
|
||||
.also { log.info { "[TimeService.findTimes] ${it.size}개의 시간 조회 완료" } }
|
||||
.toResponse()
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun createTime(request: TimeCreateRequest): TimeCreateResponse {
|
||||
log.debug { "[TimeService.createTime] 시간 생성 시작: startAt=${request.startAt}" }
|
||||
|
||||
val startAt: LocalTime = request.startAt
|
||||
if (timeRepository.existsByStartAt(startAt)) {
|
||||
log.info { "[TimeService.createTime] 시간 생성 실패(시간 중복): startAt=$startAt" }
|
||||
throw TimeException(TimeErrorCode.TIME_DUPLICATED)
|
||||
}
|
||||
|
||||
val time: TimeEntity = request.toEntity()
|
||||
|
||||
return timeRepository.save(time).toCreateResponse()
|
||||
return timeRepository.save(time)
|
||||
.also { log.info { "[TimeService.createTime] 시간 생성 완료: timeId=${it.id}" } }
|
||||
.toCreateResponse()
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun deleteTime(id: Long) {
|
||||
log.debug { "[TimeService.deleteTime] 시간 삭제 시작: timeId=$id" }
|
||||
|
||||
val time: TimeEntity = findById(id)
|
||||
|
||||
log.debug { "[TimeService.deleteTime] 시간이 ${time.startAt}인 모든 예약 조회 시작" }
|
||||
val reservations: List<ReservationEntity> = reservationRepository.findAllByTime(time)
|
||||
log.debug { "[TimeService.deleteTime] 시간이 ${time.startAt}인 모든 ${reservations.size} 개의 예약 조회 완료" }
|
||||
|
||||
if (reservations.isNotEmpty()) {
|
||||
log.info { "[TimeService.deleteTime] 시간 삭제 실패(예약이 있는 시간): timeId=$id" }
|
||||
throw TimeException(TimeErrorCode.TIME_ALREADY_RESERVED)
|
||||
}
|
||||
|
||||
timeRepository.delete(time)
|
||||
.also { log.info { "[TimeService.deleteTime] 시간 삭제 완료: timeId=$id" } }
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findTimesWithAvailability(date: LocalDate, themeId: Long): TimeWithAvailabilityListResponse {
|
||||
log.debug { "[TimeService.findTimesWithAvailability] 예약 가능 시간 조회 시작: date=$date, themeId=$themeId" }
|
||||
|
||||
log.debug { "[TimeService.findTimesWithAvailability] 모든 시간 조회 " }
|
||||
val allTimes = timeRepository.findAll()
|
||||
log.debug { "[TimeService.findTimesWithAvailability] ${allTimes.size}개의 시간 조회 완료" }
|
||||
|
||||
log.debug { "[TimeService.findTimesWithAvailability] date=$date, themeId=$themeId 인 모든 예약 조회 시작" }
|
||||
val reservations: List<ReservationEntity> = reservationRepository.findByDateAndThemeId(date, themeId)
|
||||
log.debug { "[TimeService.findTimesWithAvailability] date=$date, themeId=$themeId 인 ${reservations.size} 개의 예약 조회 완료" }
|
||||
|
||||
|
||||
return TimeWithAvailabilityListResponse(allTimes.map { time ->
|
||||
val isAvailable: Boolean = reservations.none { reservation -> reservation.time.id == time.id }
|
||||
TimeWithAvailabilityResponse(time.id!!, time.startAt, isAvailable)
|
||||
})
|
||||
}).also {
|
||||
log.info {
|
||||
"[TimeService.findTimesWithAvailability] date=$date, themeId=$themeId 에 대한 예약 가능 여부가 담긴 모든 시간 조회 완료"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
40
src/main/resources/application-local.yaml
Normal file
40
src/main/resources/application-local.yaml
Normal file
@ -0,0 +1,40 @@
|
||||
spring:
|
||||
jpa:
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
ddl-auto: create-drop
|
||||
defer-datasource-initialization: true
|
||||
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
path: /h2-console
|
||||
datasource:
|
||||
driver-class-name: org.h2.Driver
|
||||
url: jdbc:h2:mem:database
|
||||
username: sa
|
||||
password:
|
||||
|
||||
security:
|
||||
jwt:
|
||||
token:
|
||||
secret-key: daijawligagaf@LIJ$@U)9nagnalkkgalijaddljfi
|
||||
ttl-seconds: 1800000
|
||||
|
||||
payment:
|
||||
confirm-secret-key: test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6
|
||||
read-timeout: 3
|
||||
connect-timeout: 30
|
||||
|
||||
jdbc:
|
||||
datasource-proxy:
|
||||
enabled: true
|
||||
include-parameter-values: false
|
||||
query:
|
||||
enable-logging: true
|
||||
log-level: DEBUG
|
||||
logger-name: query-logger
|
||||
multiline: true
|
||||
includes: connection,query,keys,fetch
|
||||
@ -1,33 +1,11 @@
|
||||
spring:
|
||||
profiles:
|
||||
active: ${ACTIVE_PROFILE:local}
|
||||
jpa:
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
ddl-auto: create-drop
|
||||
defer-datasource-initialization: true
|
||||
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
path: /h2-console
|
||||
datasource:
|
||||
driver-class-name: org.h2.Driver
|
||||
url: jdbc:h2:mem:database
|
||||
username: sa
|
||||
password:
|
||||
|
||||
security:
|
||||
jwt:
|
||||
token:
|
||||
secret-key: daijawligagaf@LIJ$@U)9nagnalkkgalijaddljfi
|
||||
ttl-seconds: 1800000
|
||||
open-in-view: false
|
||||
|
||||
payment:
|
||||
api-base-url: https://api.tosspayments.com
|
||||
confirm-secret-key: test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6
|
||||
read-timeout: 3
|
||||
connect-timeout: 30
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
|
||||
26
src/main/resources/logback-local.xml
Normal file
26
src/main/resources/logback-local.xml
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<included>
|
||||
<conversionRule conversionWord="maskedMessage"
|
||||
class="roomescape.common.log.RoomescapeLogMaskingConverter"/>
|
||||
|
||||
<property name="CONSOLE_LOG_PATTERN"
|
||||
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %green(${PID:- }) --- [%15.15thread] %cyan(%-40logger{36}) : %maskedMessage%n%throwable"/>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
|
||||
<logger name="roomescape" level="debug" additivity="false">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
|
||||
<logger name="query-logger" level="debug" additivity="false">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
</included>
|
||||
10
src/main/resources/logback-spring.xml
Normal file
10
src/main/resources/logback-spring.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration scan="true">
|
||||
<springProfile name="local">
|
||||
<include resource="logback-local.xml"/>
|
||||
</springProfile>
|
||||
|
||||
<springProfile name="default">
|
||||
<include resource="logback-local.xml"/>
|
||||
</springProfile>
|
||||
</configuration>
|
||||
@ -10,7 +10,6 @@ import org.springframework.data.repository.findByIdOrNull
|
||||
import roomescape.auth.exception.AuthErrorCode
|
||||
import roomescape.auth.exception.AuthException
|
||||
import roomescape.auth.infrastructure.jwt.JwtHandler
|
||||
import roomescape.auth.service.AuthService
|
||||
import roomescape.member.business.MemberService
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository
|
||||
|
||||
@ -6,8 +6,8 @@ import org.hamcrest.Matchers.equalTo
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import roomescape.auth.business.AuthService
|
||||
import roomescape.auth.exception.AuthErrorCode
|
||||
import roomescape.auth.service.AuthService
|
||||
import roomescape.common.exception.CommonErrorCode
|
||||
import roomescape.common.exception.ErrorCode
|
||||
import roomescape.util.MemberFixture
|
||||
@ -133,6 +133,10 @@ class AuthControllerTest(
|
||||
jwtHandler.getMemberIdFromToken(any())
|
||||
} returns 1L
|
||||
|
||||
every {
|
||||
memberRepository.findByIdOrNull(1L)
|
||||
} returns MemberFixture.create(id = 1L)
|
||||
|
||||
Then("정상 응답한다.") {
|
||||
runPostTest(
|
||||
mockMvc = mockMvc,
|
||||
|
||||
@ -27,9 +27,11 @@ class ReservationRepositoryTest(
|
||||
beforeTest {
|
||||
listOf(
|
||||
ReservationFixture.create(time = time),
|
||||
ReservationFixture.create(time = TimeFixture.create(
|
||||
ReservationFixture.create(
|
||||
time = TimeFixture.create(
|
||||
startAt = time.startAt.plusSeconds(1)
|
||||
))
|
||||
)
|
||||
)
|
||||
).forEach {
|
||||
persistReservation(it)
|
||||
}
|
||||
@ -124,7 +126,8 @@ class ReservationRepositoryTest(
|
||||
persistReservation(it)
|
||||
}
|
||||
|
||||
confirmedPaymentRequired = ReservationFixture.create(status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED).also {
|
||||
confirmedPaymentRequired =
|
||||
ReservationFixture.create(status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED).also {
|
||||
persistReservation(it)
|
||||
}
|
||||
|
||||
@ -168,7 +171,8 @@ class ReservationRepositoryTest(
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
val result: List<MyReservationRetrieveResponse> = reservationRepository.findAllByMemberId(reservation.member.id!!)
|
||||
val result: List<MyReservationRetrieveResponse> =
|
||||
reservationRepository.findAllByMemberId(reservation.member.id!!)
|
||||
|
||||
result shouldHaveSize 1
|
||||
assertSoftly(result.first()) {
|
||||
@ -179,7 +183,8 @@ class ReservationRepositoryTest(
|
||||
}
|
||||
|
||||
test("결제 정보가 없다면 paymentKey와 amount는 null로 반환한다.") {
|
||||
val result: List<MyReservationRetrieveResponse> = reservationRepository.findAllByMemberId(reservation.member.id!!)
|
||||
val result: List<MyReservationRetrieveResponse> =
|
||||
reservationRepository.findAllByMemberId(reservation.member.id!!)
|
||||
|
||||
result shouldHaveSize 1
|
||||
assertSoftly(result.first()) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
spring:
|
||||
jpa:
|
||||
show-sql: true
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
@ -24,5 +24,6 @@ payment:
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.springframework.orm.jpa: DEBUG
|
||||
root: INFO
|
||||
org.springframework.orm.jpa: INFO
|
||||
org.springframework.transaction: DEBUG
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user