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
|
// Jwt
|
||||||
implementation("io.jsonwebtoken:jjwt:0.12.6")
|
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
|
// Kotlin
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
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.KLogger
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
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.LoginCheckResponse
|
||||||
import roomescape.auth.web.LoginRequest
|
import roomescape.auth.web.LoginRequest
|
||||||
import roomescape.auth.web.LoginResponse
|
import roomescape.auth.web.LoginResponse
|
||||||
|
import roomescape.common.exception.RoomescapeException
|
||||||
import roomescape.member.business.MemberService
|
import roomescape.member.business.MemberService
|
||||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||||
|
|
||||||
@ -17,40 +18,50 @@ private val log: KLogger = KotlinLogging.logger {}
|
|||||||
@Service
|
@Service
|
||||||
class AuthService(
|
class AuthService(
|
||||||
private val memberService: MemberService,
|
private val memberService: MemberService,
|
||||||
private val jwtHandler: JwtHandler
|
private val jwtHandler: JwtHandler,
|
||||||
) {
|
) {
|
||||||
fun login(request: LoginRequest): LoginResponse {
|
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)
|
memberService.findByEmailAndPassword(request.email, request.password)
|
||||||
}
|
}
|
||||||
|
|
||||||
val accessToken: String = jwtHandler.createToken(member.id!!)
|
val accessToken: String = jwtHandler.createToken(member.id!!)
|
||||||
|
|
||||||
return LoginResponse(accessToken)
|
return LoginResponse(accessToken)
|
||||||
|
.also { log.info { "[AuthService.login] 로그인 완료: memberId=${member.id}" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkLogin(memberId: Long): LoginCheckResponse {
|
fun checkLogin(memberId: Long): LoginCheckResponse {
|
||||||
val member: MemberEntity = fetchMemberOrThrow(AuthErrorCode.UNIDENTIFIABLE_MEMBER) {
|
log.debug { "[AuthService.checkLogin] 로그인 확인 시작: memberId=$memberId" }
|
||||||
memberService.findById(memberId)
|
val member: MemberEntity =
|
||||||
}
|
fetchMemberOrThrow(AuthErrorCode.UNIDENTIFIABLE_MEMBER, "memberId=$memberId", "checkLogin") {
|
||||||
|
memberService.findById(memberId)
|
||||||
|
}
|
||||||
|
|
||||||
return LoginCheckResponse(member.name, member.role.name)
|
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(
|
private fun fetchMemberOrThrow(
|
||||||
errorCode: AuthErrorCode,
|
errorCode: AuthErrorCode,
|
||||||
block: () -> MemberEntity
|
params: String,
|
||||||
|
calledBy: String,
|
||||||
|
block: () -> MemberEntity,
|
||||||
): MemberEntity {
|
): MemberEntity {
|
||||||
try {
|
try {
|
||||||
|
log.debug { "[AuthService.$calledBy] 회원 조회 시작: $params" }
|
||||||
return block()
|
return block()
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e !is RoomescapeException) {
|
||||||
|
log.warn(e) { "[AuthService.$calledBy] 회원 조회 실패: $params" }
|
||||||
|
}
|
||||||
throw AuthException(errorCode)
|
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.LoginCheckResponse
|
||||||
import roomescape.auth.web.LoginRequest
|
import roomescape.auth.web.LoginRequest
|
||||||
import roomescape.auth.web.LoginResponse
|
import roomescape.auth.web.LoginResponse
|
||||||
|
import roomescape.auth.web.support.LoginRequired
|
||||||
import roomescape.auth.web.support.MemberId
|
import roomescape.auth.web.support.MemberId
|
||||||
import roomescape.common.dto.response.CommonApiResponse
|
import roomescape.common.dto.response.CommonApiResponse
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ interface AuthAPI {
|
|||||||
@MemberId @Parameter(hidden = true) memberId: Long
|
@MemberId @Parameter(hidden = true) memberId: Long
|
||||||
): ResponseEntity<CommonApiResponse<LoginCheckResponse>>
|
): ResponseEntity<CommonApiResponse<LoginCheckResponse>>
|
||||||
|
|
||||||
|
@LoginRequired
|
||||||
@Operation(summary = "로그아웃", tags = ["로그인이 필요한 API"])
|
@Operation(summary = "로그아웃", tags = ["로그인이 필요한 API"])
|
||||||
@ApiResponses(
|
@ApiResponses(
|
||||||
ApiResponse(responseCode = "200", description = "로그아웃 성공시 쿠키에 저장된 토큰 정보를 삭제합니다."),
|
ApiResponse(responseCode = "200", description = "로그아웃 성공시 쿠키에 저장된 토큰 정보를 삭제합니다."),
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import org.springframework.http.HttpStatus
|
|||||||
import roomescape.common.exception.ErrorCode
|
import roomescape.common.exception.ErrorCode
|
||||||
|
|
||||||
enum class AuthErrorCode(
|
enum class AuthErrorCode(
|
||||||
override val httpStatus: HttpStatus,
|
override val httpStatus: HttpStatus,
|
||||||
override val errorCode: String,
|
override val errorCode: String,
|
||||||
override val message: String,
|
override val message: String,
|
||||||
) : ErrorCode {
|
) : ErrorCode {
|
||||||
TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "A001", "인증 토큰이 없어요."),
|
TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "A001", "인증 토큰이 없어요."),
|
||||||
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "A002", "유효하지 않은 토큰이에요."),
|
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "A002", "유효하지 않은 토큰이에요."),
|
||||||
|
|||||||
@ -3,6 +3,6 @@ package roomescape.auth.exception
|
|||||||
import roomescape.common.exception.RoomescapeException
|
import roomescape.common.exception.RoomescapeException
|
||||||
|
|
||||||
class AuthException(
|
class AuthException(
|
||||||
override val errorCode: AuthErrorCode,
|
override val errorCode: AuthErrorCode,
|
||||||
override val message: String = errorCode.message
|
override val message: String = errorCode.message
|
||||||
) : RoomescapeException(errorCode, message)
|
) : RoomescapeException(errorCode, message)
|
||||||
|
|||||||
@ -12,11 +12,11 @@ import javax.crypto.SecretKey
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
class JwtHandler(
|
class JwtHandler(
|
||||||
@Value("\${security.jwt.token.secret-key}")
|
@Value("\${security.jwt.token.secret-key}")
|
||||||
private val secretKeyString: String,
|
private val secretKeyString: String,
|
||||||
|
|
||||||
@Value("\${security.jwt.token.ttl-seconds}")
|
@Value("\${security.jwt.token.ttl-seconds}")
|
||||||
private val tokenTtlSeconds: Long
|
private val tokenTtlSeconds: Long
|
||||||
) {
|
) {
|
||||||
private val secretKey: SecretKey = Keys.hmacShaKeyFor(secretKeyString.toByteArray())
|
private val secretKey: SecretKey = Keys.hmacShaKeyFor(secretKeyString.toByteArray())
|
||||||
|
|
||||||
@ -25,22 +25,22 @@ class JwtHandler(
|
|||||||
val accessTokenExpiredAt = Date(date.time + tokenTtlSeconds)
|
val accessTokenExpiredAt = Date(date.time + tokenTtlSeconds)
|
||||||
|
|
||||||
return Jwts.builder()
|
return Jwts.builder()
|
||||||
.claim(MEMBER_ID_CLAIM_KEY, memberId)
|
.claim(MEMBER_ID_CLAIM_KEY, memberId)
|
||||||
.issuedAt(date)
|
.issuedAt(date)
|
||||||
.expiration(accessTokenExpiredAt)
|
.expiration(accessTokenExpiredAt)
|
||||||
.signWith(secretKey)
|
.signWith(secretKey)
|
||||||
.compact()
|
.compact()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMemberIdFromToken(token: String?): Long {
|
fun getMemberIdFromToken(token: String?): Long {
|
||||||
try {
|
try {
|
||||||
return Jwts.parser()
|
return Jwts.parser()
|
||||||
.verifyWith(secretKey)
|
.verifyWith(secretKey)
|
||||||
.build()
|
.build()
|
||||||
.parseSignedClaims(token)
|
.parseSignedClaims(token)
|
||||||
.payload
|
.payload
|
||||||
.get(MEMBER_ID_CLAIM_KEY, Number::class.java)
|
.get(MEMBER_ID_CLAIM_KEY, Number::class.java)
|
||||||
.toLong()
|
.toLong()
|
||||||
} catch (_: IllegalArgumentException) {
|
} catch (_: IllegalArgumentException) {
|
||||||
throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND)
|
throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND)
|
||||||
} catch (_: ExpiredJwtException) {
|
} catch (_: ExpiredJwtException) {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.PostMapping
|
|||||||
import org.springframework.web.bind.annotation.RequestBody
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import roomescape.auth.docs.AuthAPI
|
import roomescape.auth.docs.AuthAPI
|
||||||
import roomescape.auth.service.AuthService
|
import roomescape.auth.business.AuthService
|
||||||
import roomescape.auth.web.support.MemberId
|
import roomescape.auth.web.support.MemberId
|
||||||
import roomescape.common.dto.response.CommonApiResponse
|
import roomescape.common.dto.response.CommonApiResponse
|
||||||
|
|
||||||
|
|||||||
@ -18,24 +18,24 @@ class JacksonConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun objectMapper(): ObjectMapper = ObjectMapper()
|
fun objectMapper(): ObjectMapper = ObjectMapper()
|
||||||
.registerModule(javaTimeModule())
|
.registerModule(javaTimeModule())
|
||||||
.registerModule(kotlinModule())
|
.registerModule(kotlinModule())
|
||||||
|
|
||||||
private fun javaTimeModule(): JavaTimeModule = JavaTimeModule()
|
private fun javaTimeModule(): JavaTimeModule = JavaTimeModule()
|
||||||
.addSerializer(
|
.addSerializer(
|
||||||
LocalDate::class.java,
|
LocalDate::class.java,
|
||||||
LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE)
|
LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE)
|
||||||
)
|
)
|
||||||
.addDeserializer(
|
.addDeserializer(
|
||||||
LocalDate::class.java,
|
LocalDate::class.java,
|
||||||
LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE)
|
LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE)
|
||||||
)
|
)
|
||||||
.addSerializer(
|
.addSerializer(
|
||||||
LocalTime::class.java,
|
LocalTime::class.java,
|
||||||
LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm"))
|
LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm"))
|
||||||
)
|
)
|
||||||
.addDeserializer(
|
.addDeserializer(
|
||||||
LocalTime::class.java,
|
LocalTime::class.java,
|
||||||
LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm"))
|
LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm"))
|
||||||
) as JavaTimeModule
|
) as JavaTimeModule
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,8 +15,9 @@ class SwaggerConfig {
|
|||||||
|
|
||||||
private fun apiInfo(): Info {
|
private fun apiInfo(): Info {
|
||||||
return Info()
|
return Info()
|
||||||
.title("방탈출 예약 API 문서")
|
.title("방탈출 예약 API 문서")
|
||||||
.description("""
|
.description(
|
||||||
|
"""
|
||||||
## API 테스트는 '1. 인증 / 인가 API' 의 '/login' 을 통해 로그인 후 사용해주세요.
|
## API 테스트는 '1. 인증 / 인가 API' 의 '/login' 을 통해 로그인 후 사용해주세요.
|
||||||
|
|
||||||
### 테스트시 로그인 가능한 계정 정보
|
### 테스트시 로그인 가능한 계정 정보
|
||||||
@ -70,7 +71,8 @@ class SwaggerConfig {
|
|||||||
|
|
||||||
- 8 ~ 10: 예약 대기 상태
|
- 8 ~ 10: 예약 대기 상태
|
||||||
|
|
||||||
""".trimIndent())
|
""".trimIndent()
|
||||||
.version("1.0.0")
|
)
|
||||||
|
.version("1.0.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import roomescape.auth.web.support.MemberIdResolver
|
|||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
class WebMvcConfig(
|
class WebMvcConfig(
|
||||||
private val memberIdResolver: MemberIdResolver,
|
private val memberIdResolver: MemberIdResolver,
|
||||||
private val authInterceptor: AuthInterceptor
|
private val authInterceptor: AuthInterceptor
|
||||||
) : WebMvcConfigurer {
|
) : WebMvcConfigurer {
|
||||||
|
|
||||||
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
|
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
|
||||||
|
|||||||
@ -5,15 +5,15 @@ import roomescape.common.exception.ErrorCode
|
|||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
data class CommonApiResponse<T>(
|
data class CommonApiResponse<T>(
|
||||||
val data: T? = null,
|
val data: T? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class CommonErrorResponse(
|
data class CommonErrorResponse(
|
||||||
val code: String,
|
val code: String,
|
||||||
val message: String
|
val message: String
|
||||||
) {
|
) {
|
||||||
constructor(errorCode: ErrorCode, message: String = errorCode.message) : this(
|
constructor(errorCode: ErrorCode, message: String = errorCode.message) : this(
|
||||||
code = errorCode.errorCode,
|
code = errorCode.errorCode,
|
||||||
message = message
|
message = message
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,18 +3,18 @@ package roomescape.common.exception
|
|||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
|
|
||||||
enum class CommonErrorCode(
|
enum class CommonErrorCode(
|
||||||
override val httpStatus: HttpStatus,
|
override val httpStatus: HttpStatus,
|
||||||
override val errorCode: String,
|
override val errorCode: String,
|
||||||
override val message: String,
|
override val message: String,
|
||||||
) : ErrorCode {
|
) : ErrorCode {
|
||||||
INVALID_INPUT_VALUE(
|
INVALID_INPUT_VALUE(
|
||||||
httpStatus = HttpStatus.BAD_REQUEST,
|
httpStatus = HttpStatus.BAD_REQUEST,
|
||||||
errorCode = "C001",
|
errorCode = "C001",
|
||||||
message = "요청 값이 잘못되었어요."
|
message = "요청 값이 잘못되었어요."
|
||||||
),
|
),
|
||||||
UNEXPECTED_SERVER_ERROR(
|
UNEXPECTED_SERVER_ERROR(
|
||||||
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR,
|
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
errorCode = "C999",
|
errorCode = "C999",
|
||||||
message = "서버에 예상치 못한 오류가 발생했어요. 관리자에게 문의해주세요.",
|
message = "서버에 예상치 못한 오류가 발생했어요. 관리자에게 문의해주세요.",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,48 +11,46 @@ import roomescape.common.dto.response.CommonErrorResponse
|
|||||||
|
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
class ExceptionControllerAdvice(
|
class ExceptionControllerAdvice(
|
||||||
private val logger: KLogger = KotlinLogging.logger {}
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
) {
|
) {
|
||||||
@ExceptionHandler(value = [RoomescapeException::class])
|
@ExceptionHandler(value = [RoomescapeException::class])
|
||||||
fun handleRoomException(e: RoomescapeException): ResponseEntity<CommonErrorResponse> {
|
fun handleRoomException(e: RoomescapeException): ResponseEntity<CommonErrorResponse> {
|
||||||
logger.error(e) { "message: ${e.message}" }
|
|
||||||
|
|
||||||
val errorCode: ErrorCode = e.errorCode
|
val errorCode: ErrorCode = e.errorCode
|
||||||
return ResponseEntity
|
return ResponseEntity
|
||||||
.status(errorCode.httpStatus)
|
.status(errorCode.httpStatus)
|
||||||
.body(CommonErrorResponse(errorCode, e.message))
|
.body(CommonErrorResponse(errorCode, e.message))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = [HttpMessageNotReadableException::class])
|
@ExceptionHandler(value = [HttpMessageNotReadableException::class])
|
||||||
fun handleHttpMessageNotReadableException(e: HttpMessageNotReadableException): ResponseEntity<CommonErrorResponse> {
|
fun handleHttpMessageNotReadableException(e: HttpMessageNotReadableException): ResponseEntity<CommonErrorResponse> {
|
||||||
logger.error(e) { "message: ${e.message}" }
|
log.debug { "message: ${e.message}" }
|
||||||
|
|
||||||
val errorCode: ErrorCode = CommonErrorCode.INVALID_INPUT_VALUE
|
val errorCode: ErrorCode = CommonErrorCode.INVALID_INPUT_VALUE
|
||||||
return ResponseEntity
|
return ResponseEntity
|
||||||
.status(errorCode.httpStatus)
|
.status(errorCode.httpStatus)
|
||||||
.body(CommonErrorResponse(errorCode))
|
.body(CommonErrorResponse(errorCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = [MethodArgumentNotValidException::class])
|
@ExceptionHandler(value = [MethodArgumentNotValidException::class])
|
||||||
fun handleMethodArgumentNotValidException(e: MethodArgumentNotValidException): ResponseEntity<CommonErrorResponse> {
|
fun handleMethodArgumentNotValidException(e: MethodArgumentNotValidException): ResponseEntity<CommonErrorResponse> {
|
||||||
val message: String = e.bindingResult.allErrors
|
val message: String = e.bindingResult.allErrors
|
||||||
.mapNotNull { it.defaultMessage }
|
.mapNotNull { it.defaultMessage }
|
||||||
.joinToString(", ")
|
.joinToString(", ")
|
||||||
logger.error(e) { "message: $message" }
|
log.debug { "message: $message" }
|
||||||
|
|
||||||
val errorCode: ErrorCode = CommonErrorCode.INVALID_INPUT_VALUE
|
val errorCode: ErrorCode = CommonErrorCode.INVALID_INPUT_VALUE
|
||||||
return ResponseEntity
|
return ResponseEntity
|
||||||
.status(errorCode.httpStatus)
|
.status(errorCode.httpStatus)
|
||||||
.body(CommonErrorResponse(errorCode))
|
.body(CommonErrorResponse(errorCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = [Exception::class])
|
@ExceptionHandler(value = [Exception::class])
|
||||||
fun handleException(e: Exception): ResponseEntity<CommonErrorResponse> {
|
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
|
val errorCode: ErrorCode = CommonErrorCode.UNEXPECTED_SERVER_ERROR
|
||||||
return ResponseEntity
|
return ResponseEntity
|
||||||
.status(errorCode.httpStatus)
|
.status(errorCode.httpStatus)
|
||||||
.body(CommonErrorResponse(errorCode))
|
.body(CommonErrorResponse(errorCode))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package roomescape.common.exception
|
package roomescape.common.exception
|
||||||
|
|
||||||
open class RoomescapeException(
|
open class RoomescapeException(
|
||||||
open val errorCode: ErrorCode,
|
open val errorCode: ErrorCode,
|
||||||
override val message: String = errorCode.message
|
override val message: String = errorCode.message
|
||||||
) : RuntimeException(message)
|
) : RuntimeException(message)
|
||||||
|
|||||||
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
|
package roomescape.member.business
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
@ -10,26 +11,37 @@ import roomescape.member.infrastructure.persistence.MemberRepository
|
|||||||
import roomescape.member.infrastructure.persistence.Role
|
import roomescape.member.infrastructure.persistence.Role
|
||||||
import roomescape.member.web.*
|
import roomescape.member.web.*
|
||||||
|
|
||||||
|
private val log = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
class MemberService(
|
class MemberService(
|
||||||
private val memberRepository: MemberRepository
|
private val memberRepository: MemberRepository,
|
||||||
) {
|
) {
|
||||||
fun findMembers(): MemberRetrieveListResponse = MemberRetrieveListResponse(
|
fun findMembers(): MemberRetrieveListResponse {
|
||||||
members = memberRepository.findAll().map { it.toRetrieveResponse() }
|
log.debug { "[MemberService.findMembers] 회원 조회 시작" }
|
||||||
)
|
|
||||||
|
|
||||||
fun findById(memberId: Long): MemberEntity = fetchOrThrow {
|
return memberRepository.findAll()
|
||||||
memberRepository.findByIdOrNull(memberId)
|
.also { log.info { "[MemberService.findMembers] 회원 ${it.size}명 조회 완료" } }
|
||||||
|
.toRetrieveListResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findByEmailAndPassword(email: String, password: String): MemberEntity = fetchOrThrow {
|
fun findById(memberId: Long): MemberEntity {
|
||||||
memberRepository.findByEmailAndPassword(email, password)
|
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
|
@Transactional
|
||||||
fun create(request: SignupRequest): SignupResponse {
|
fun createMember(request: SignupRequest): SignupResponse {
|
||||||
memberRepository.findByEmail(request.email)?.let {
|
memberRepository.findByEmail(request.email)?.let {
|
||||||
|
log.info { "[MemberService.createMember] 회원가입 실패(이메일 중복): email=${request.email}" }
|
||||||
throw MemberException(MemberErrorCode.DUPLICATE_EMAIL)
|
throw MemberException(MemberErrorCode.DUPLICATE_EMAIL)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,10 +51,18 @@ class MemberService(
|
|||||||
password = request.password,
|
password = request.password,
|
||||||
role = Role.MEMBER
|
role = Role.MEMBER
|
||||||
)
|
)
|
||||||
|
|
||||||
return memberRepository.save(member).toSignupResponse()
|
return memberRepository.save(member).toSignupResponse()
|
||||||
|
.also { log.info { "[MemberService.create] 회원가입 완료: email=${request.email} memberId=${it.id}" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchOrThrow(block: () -> MemberEntity?): MemberEntity {
|
private fun fetchOrThrow(calledBy: String, params: String, block: () -> MemberEntity?): MemberEntity {
|
||||||
return block() ?: throw MemberException(MemberErrorCode.MEMBER_NOT_FOUND)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import org.springframework.http.HttpStatus
|
|||||||
import roomescape.common.exception.ErrorCode
|
import roomescape.common.exception.ErrorCode
|
||||||
|
|
||||||
enum class MemberErrorCode(
|
enum class MemberErrorCode(
|
||||||
override val httpStatus: HttpStatus,
|
override val httpStatus: HttpStatus,
|
||||||
override val errorCode: String,
|
override val errorCode: String,
|
||||||
override val message: String
|
override val message: String
|
||||||
) : ErrorCode {
|
) : ErrorCode {
|
||||||
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "M001", "회원을 찾을 수 없어요."),
|
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "M001", "회원을 찾을 수 없어요."),
|
||||||
DUPLICATE_EMAIL(HttpStatus.CONFLICT, "M002", "이미 가입된 이메일이에요.")
|
DUPLICATE_EMAIL(HttpStatus.CONFLICT, "M002", "이미 가입된 이메일이에요.")
|
||||||
|
|||||||
@ -3,6 +3,6 @@ package roomescape.member.exception
|
|||||||
import roomescape.common.exception.RoomescapeException
|
import roomescape.common.exception.RoomescapeException
|
||||||
|
|
||||||
class MemberException(
|
class MemberException(
|
||||||
override val errorCode: MemberErrorCode,
|
override val errorCode: MemberErrorCode,
|
||||||
override val message: String = errorCode.message
|
override val message: String = errorCode.message
|
||||||
) : RoomescapeException(errorCode, message)
|
) : RoomescapeException(errorCode, message)
|
||||||
|
|||||||
@ -5,15 +5,15 @@ import jakarta.persistence.*
|
|||||||
@Entity
|
@Entity
|
||||||
@Table(name = "members")
|
@Table(name = "members")
|
||||||
class MemberEntity(
|
class MemberEntity(
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
var id: Long? = null,
|
var id: Long? = null,
|
||||||
var name: String,
|
var name: String,
|
||||||
var email: String,
|
var email: String,
|
||||||
var password: String,
|
var password: String,
|
||||||
|
|
||||||
@Enumerated(value = EnumType.STRING)
|
@Enumerated(value = EnumType.STRING)
|
||||||
var role: Role
|
var role: Role
|
||||||
) {
|
) {
|
||||||
fun isAdmin(): Boolean = role == Role.ADMIN
|
fun isAdmin(): Boolean = role == Role.ADMIN
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ class MemberController(
|
|||||||
|
|
||||||
@PostMapping("/members")
|
@PostMapping("/members")
|
||||||
override fun signup(@RequestBody request: SignupRequest): ResponseEntity<CommonApiResponse<SignupResponse>> {
|
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}"))
|
return ResponseEntity.created(URI.create("/members/${response.id}"))
|
||||||
.body(CommonApiResponse(response))
|
.body(CommonApiResponse(response))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,10 @@ data class MemberRetrieveResponse(
|
|||||||
val name: String
|
val name: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun List<MemberEntity>.toRetrieveListResponse(): MemberRetrieveListResponse = MemberRetrieveListResponse(
|
||||||
|
members = this.map { it.toRetrieveResponse() }
|
||||||
|
)
|
||||||
|
|
||||||
data class MemberRetrieveListResponse(
|
data class MemberRetrieveListResponse(
|
||||||
val members: List<MemberRetrieveResponse>
|
val members: List<MemberRetrieveResponse>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package roomescape.payment.business
|
package roomescape.payment.business
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import roomescape.payment.exception.PaymentErrorCode
|
import roomescape.payment.exception.PaymentErrorCode
|
||||||
@ -16,85 +17,127 @@ import roomescape.payment.web.toCreateResponse
|
|||||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
private val log = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class PaymentService(
|
class PaymentService(
|
||||||
private val paymentRepository: PaymentRepository,
|
private val paymentRepository: PaymentRepository,
|
||||||
private val canceledPaymentRepository: CanceledPaymentRepository
|
private val canceledPaymentRepository: CanceledPaymentRepository,
|
||||||
) {
|
) {
|
||||||
@Transactional
|
@Transactional
|
||||||
fun createPayment(
|
fun createPayment(
|
||||||
approveResponse: PaymentApproveResponse,
|
approveResponse: PaymentApproveResponse,
|
||||||
reservation: ReservationEntity
|
reservation: ReservationEntity,
|
||||||
): PaymentCreateResponse {
|
): PaymentCreateResponse {
|
||||||
|
log.debug { "[PaymentService.createPayment] 결제 정보 저장 시작: request=$approveResponse, reservationId=${reservation.id}" }
|
||||||
val payment = PaymentEntity(
|
val payment = PaymentEntity(
|
||||||
orderId = approveResponse.orderId,
|
orderId = approveResponse.orderId,
|
||||||
paymentKey = approveResponse.paymentKey,
|
paymentKey = approveResponse.paymentKey,
|
||||||
totalAmount = approveResponse.totalAmount,
|
totalAmount = approveResponse.totalAmount,
|
||||||
reservation = reservation,
|
reservation = reservation,
|
||||||
approvedAt = approveResponse.approvedAt
|
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)
|
@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
|
@Transactional
|
||||||
fun createCanceledPayment(
|
fun createCanceledPayment(
|
||||||
cancelInfo: PaymentCancelResponse,
|
cancelInfo: PaymentCancelResponse,
|
||||||
approvedAt: OffsetDateTime,
|
approvedAt: OffsetDateTime,
|
||||||
paymentKey: String
|
paymentKey: String,
|
||||||
): CanceledPaymentEntity {
|
): CanceledPaymentEntity {
|
||||||
|
log.debug {
|
||||||
|
"[PaymentService.createCanceledPayment] 결제 취소 정보 저장 시작: paymentKey=$paymentKey" +
|
||||||
|
", cancelInfo=$cancelInfo"
|
||||||
|
}
|
||||||
val canceledPayment = CanceledPaymentEntity(
|
val canceledPayment = CanceledPaymentEntity(
|
||||||
paymentKey = paymentKey,
|
paymentKey = paymentKey,
|
||||||
cancelReason = cancelInfo.cancelReason,
|
cancelReason = cancelInfo.cancelReason,
|
||||||
cancelAmount = cancelInfo.cancelAmount,
|
cancelAmount = cancelInfo.cancelAmount,
|
||||||
approvedAt = approvedAt,
|
approvedAt = approvedAt,
|
||||||
canceledAt = cancelInfo.canceledAt
|
canceledAt = cancelInfo.canceledAt
|
||||||
)
|
)
|
||||||
|
|
||||||
return canceledPaymentRepository.save(canceledPayment)
|
return canceledPaymentRepository.save(canceledPayment)
|
||||||
|
.also {
|
||||||
|
log.info {
|
||||||
|
"[PaymentService.createCanceledPayment] 결제 취소 정보 생성 완료: canceledPaymentId=${it.id}" +
|
||||||
|
", paymentKey=${paymentKey}, amount=${cancelInfo.cancelAmount}, canceledAt=${it.canceledAt}"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun createCanceledPaymentByReservationId(reservationId: Long): PaymentCancelRequest {
|
fun createCanceledPaymentByReservationId(reservationId: Long): PaymentCancelRequest {
|
||||||
|
log.debug { "[PaymentService.createCanceledPaymentByReservationId] 예약 삭제 & 결제 취소 정보 저장 시작: reservationId=$reservationId" }
|
||||||
val paymentKey: String = paymentRepository.findPaymentKeyByReservationId(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)
|
val canceled: CanceledPaymentEntity = cancelPayment(paymentKey)
|
||||||
|
|
||||||
return PaymentCancelRequest(paymentKey, canceled.cancelAmount, canceled.cancelReason)
|
return PaymentCancelRequest(paymentKey, canceled.cancelAmount, canceled.cancelReason)
|
||||||
|
.also { log.info { "[PaymentService.createCanceledPaymentByReservationId] 예약 ID로 결제 취소 완료: reservationId=$reservationId" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelPayment(
|
private fun cancelPayment(
|
||||||
paymentKey: String,
|
paymentKey: String,
|
||||||
cancelReason: String = "고객 요청",
|
cancelReason: String = "고객 요청",
|
||||||
canceledAt: OffsetDateTime = OffsetDateTime.now()
|
canceledAt: OffsetDateTime = OffsetDateTime.now(),
|
||||||
): CanceledPaymentEntity {
|
): CanceledPaymentEntity {
|
||||||
|
log.debug { "[PaymentService.cancelPayment] 결제 취소 정보 저장 시작: paymentKey=$paymentKey" }
|
||||||
val payment: PaymentEntity = paymentRepository.findByPaymentKey(paymentKey)
|
val payment: PaymentEntity = paymentRepository.findByPaymentKey(paymentKey)
|
||||||
?.also { paymentRepository.delete(it) }
|
?.also {
|
||||||
?: throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
|
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(
|
val canceledPayment = CanceledPaymentEntity(
|
||||||
paymentKey = paymentKey,
|
paymentKey = paymentKey,
|
||||||
cancelReason = cancelReason,
|
cancelReason = cancelReason,
|
||||||
cancelAmount = payment.totalAmount,
|
cancelAmount = payment.totalAmount,
|
||||||
approvedAt = payment.approvedAt,
|
approvedAt = payment.approvedAt,
|
||||||
canceledAt = canceledAt
|
canceledAt = canceledAt
|
||||||
)
|
)
|
||||||
|
|
||||||
return canceledPaymentRepository.save(canceledPayment)
|
return canceledPaymentRepository.save(canceledPayment)
|
||||||
|
.also { log.info { "[PaymentService.cancelPayment] 결제 취소 정보 저장 완료: canceledPaymentId=${it.id}" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun updateCanceledTime(
|
fun updateCanceledTime(
|
||||||
paymentKey: String,
|
paymentKey: String,
|
||||||
canceledAt: OffsetDateTime
|
canceledAt: OffsetDateTime,
|
||||||
) {
|
) {
|
||||||
|
log.debug { "[PaymentService.updateCanceledTime] 취소 시간 업데이트 시작: paymentKey=$paymentKey, canceledAt=$canceledAt" }
|
||||||
canceledPaymentRepository.findByPaymentKey(paymentKey)
|
canceledPaymentRepository.findByPaymentKey(paymentKey)
|
||||||
?.apply { this.canceledAt = canceledAt }
|
?.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import org.springframework.http.HttpStatus
|
|||||||
import roomescape.common.exception.ErrorCode
|
import roomescape.common.exception.ErrorCode
|
||||||
|
|
||||||
enum class PaymentErrorCode(
|
enum class PaymentErrorCode(
|
||||||
override val httpStatus: HttpStatus,
|
override val httpStatus: HttpStatus,
|
||||||
override val errorCode: String,
|
override val errorCode: String,
|
||||||
override val message: String
|
override val message: String
|
||||||
) : ErrorCode {
|
) : ErrorCode {
|
||||||
PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "P001", "결제 정보를 찾을 수 없어요."),
|
PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "P001", "결제 정보를 찾을 수 없어요."),
|
||||||
CANCELED_PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "P002", "취소된 결제 정보를 찾을 수 없어요."),
|
CANCELED_PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "P002", "취소된 결제 정보를 찾을 수 없어요."),
|
||||||
|
|||||||
@ -3,6 +3,6 @@ package roomescape.payment.exception
|
|||||||
import roomescape.common.exception.RoomescapeException
|
import roomescape.common.exception.RoomescapeException
|
||||||
|
|
||||||
class PaymentException(
|
class PaymentException(
|
||||||
override val errorCode: PaymentErrorCode,
|
override val errorCode: PaymentErrorCode,
|
||||||
override val message: String = errorCode.message
|
override val message: String = errorCode.message
|
||||||
) : RoomescapeException(errorCode, message)
|
) : RoomescapeException(errorCode, message)
|
||||||
|
|||||||
@ -9,21 +9,21 @@ import roomescape.payment.web.PaymentCancelResponse
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
class PaymentCancelResponseDeserializer(
|
class PaymentCancelResponseDeserializer(
|
||||||
vc: Class<PaymentCancelResponse>? = null
|
vc: Class<PaymentCancelResponse>? = null
|
||||||
) : StdDeserializer<PaymentCancelResponse>(vc) {
|
) : StdDeserializer<PaymentCancelResponse>(vc) {
|
||||||
override fun deserialize(
|
override fun deserialize(
|
||||||
jsonParser: JsonParser,
|
jsonParser: JsonParser,
|
||||||
deserializationContext: DeserializationContext?
|
deserializationContext: DeserializationContext?
|
||||||
): PaymentCancelResponse {
|
): PaymentCancelResponse {
|
||||||
val cancels: JsonNode = jsonParser.codec.readTree<TreeNode>(jsonParser)
|
val cancels: JsonNode = jsonParser.codec.readTree<TreeNode>(jsonParser)
|
||||||
.get("cancels")
|
.get("cancels")
|
||||||
.get(0) as JsonNode
|
.get(0) as JsonNode
|
||||||
|
|
||||||
return PaymentCancelResponse(
|
return PaymentCancelResponse(
|
||||||
cancels.get("cancelStatus").asText(),
|
cancels.get("cancelStatus").asText(),
|
||||||
cancels.get("cancelReason").asText(),
|
cancels.get("cancelReason").asText(),
|
||||||
cancels.get("cancelAmount").asLong(),
|
cancels.get("cancelAmount").asLong(),
|
||||||
OffsetDateTime.parse(cancels.get("canceledAt").asText())
|
OffsetDateTime.parse(cancels.get("canceledAt").asText())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -16,7 +16,7 @@ class PaymentConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun tossPaymentClientBuilder(
|
fun tossPaymentClientBuilder(
|
||||||
paymentProperties: PaymentProperties,
|
paymentProperties: PaymentProperties,
|
||||||
): RestClient.Builder {
|
): RestClient.Builder {
|
||||||
val settings: ClientHttpRequestFactorySettings = ClientHttpRequestFactorySettings.defaults().also {
|
val settings: ClientHttpRequestFactorySettings = ClientHttpRequestFactorySettings.defaults().also {
|
||||||
it.withReadTimeout(Duration.ofSeconds(paymentProperties.readTimeout.toLong()))
|
it.withReadTimeout(Duration.ofSeconds(paymentProperties.readTimeout.toLong()))
|
||||||
@ -25,14 +25,14 @@ class PaymentConfig {
|
|||||||
val requestFactory = ClientHttpRequestFactoryBuilder.jdk().build(settings)
|
val requestFactory = ClientHttpRequestFactoryBuilder.jdk().build(settings)
|
||||||
|
|
||||||
return RestClient.builder()
|
return RestClient.builder()
|
||||||
.baseUrl(paymentProperties.apiBaseUrl)
|
.baseUrl(paymentProperties.apiBaseUrl)
|
||||||
.defaultHeader("Authorization", getAuthorizations(paymentProperties.confirmSecretKey))
|
.defaultHeader("Authorization", getAuthorizations(paymentProperties.confirmSecretKey))
|
||||||
.requestFactory(requestFactory)
|
.requestFactory(requestFactory)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAuthorizations(secretKey: String): String {
|
private fun getAuthorizations(secretKey: String): String {
|
||||||
val encodedSecretKey = Base64.getEncoder()
|
val encodedSecretKey = Base64.getEncoder()
|
||||||
.encodeToString("$secretKey:".toByteArray(StandardCharsets.UTF_8))
|
.encodeToString("$secretKey:".toByteArray(StandardCharsets.UTF_8))
|
||||||
|
|
||||||
return "Basic $encodedSecretKey"
|
return "Basic $encodedSecretKey"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties
|
|||||||
|
|
||||||
@ConfigurationProperties(prefix = "payment")
|
@ConfigurationProperties(prefix = "payment")
|
||||||
data class PaymentProperties(
|
data class PaymentProperties(
|
||||||
val apiBaseUrl: String,
|
val apiBaseUrl: String,
|
||||||
val confirmSecretKey: String,
|
val confirmSecretKey: String,
|
||||||
val readTimeout: Int,
|
val readTimeout: Int,
|
||||||
val connectTimeout: Int
|
val connectTimeout: Int
|
||||||
)
|
)
|
||||||
|
|||||||
@ -15,11 +15,12 @@ import roomescape.payment.web.PaymentCancelRequest
|
|||||||
import roomescape.payment.web.PaymentCancelResponse
|
import roomescape.payment.web.PaymentCancelResponse
|
||||||
import java.util.Map
|
import java.util.Map
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class TossPaymentClient(
|
class TossPaymentClient(
|
||||||
private val log: KLogger = KotlinLogging.logger {},
|
private val objectMapper: ObjectMapper,
|
||||||
private val objectMapper: ObjectMapper,
|
tossPaymentClientBuilder: RestClient.Builder,
|
||||||
tossPaymentClientBuilder: RestClient.Builder,
|
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val CONFIRM_URL: String = "/v1/payments/confirm"
|
private const val CONFIRM_URL: String = "/v1/payments/confirm"
|
||||||
@ -32,16 +33,19 @@ class TossPaymentClient(
|
|||||||
logPaymentInfo(paymentRequest)
|
logPaymentInfo(paymentRequest)
|
||||||
|
|
||||||
return tossPaymentClient.post()
|
return tossPaymentClient.post()
|
||||||
.uri(CONFIRM_URL)
|
.uri(CONFIRM_URL)
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.body(paymentRequest)
|
.body(paymentRequest)
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.onStatus(
|
.onStatus(
|
||||||
{ status: HttpStatusCode -> status.is4xxClientError || status.is5xxServerError },
|
{ status: HttpStatusCode -> status.is4xxClientError || status.is5xxServerError },
|
||||||
{ req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res) }
|
{ req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res, "confirm") }
|
||||||
)
|
)
|
||||||
.body(PaymentApproveResponse::class.java)
|
.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 {
|
fun cancel(cancelRequest: PaymentCancelRequest): PaymentCancelResponse {
|
||||||
@ -49,47 +53,49 @@ class TossPaymentClient(
|
|||||||
val param = Map.of<String, String>("cancelReason", cancelRequest.cancelReason)
|
val param = Map.of<String, String>("cancelReason", cancelRequest.cancelReason)
|
||||||
|
|
||||||
return tossPaymentClient.post()
|
return tossPaymentClient.post()
|
||||||
.uri(CANCEL_URL, cancelRequest.paymentKey)
|
.uri(CANCEL_URL, cancelRequest.paymentKey)
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.body(param)
|
.body(param)
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.onStatus(
|
.onStatus(
|
||||||
{ status: HttpStatusCode -> status.is4xxClientError || status.is5xxServerError },
|
{ status: HttpStatusCode -> status.is4xxClientError || status.is5xxServerError },
|
||||||
{ req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res) }
|
{ req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res, "cancel") }
|
||||||
)
|
)
|
||||||
.body(PaymentCancelResponse::class.java)
|
.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) {
|
private fun logPaymentInfo(paymentRequest: PaymentApproveRequest) {
|
||||||
log.info {
|
log.info {
|
||||||
"결제 승인 요청: paymentKey=${paymentRequest.paymentKey}, orderId=${paymentRequest.orderId}, " +
|
"[TossPaymentClient.confirm] 결제 승인 요청: request: $paymentRequest"
|
||||||
"amount=${paymentRequest.amount}, paymentType=${paymentRequest.paymentType}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logPaymentCancelInfo(cancelRequest: PaymentCancelRequest) {
|
private fun logPaymentCancelInfo(cancelRequest: PaymentCancelRequest) {
|
||||||
log.info {
|
log.info {
|
||||||
"결제 취소 요청: paymentKey=${cancelRequest.paymentKey}, amount=${cancelRequest.amount}, " +
|
"[TossPaymentClient.cancel] 결제 취소 요청: request: $cancelRequest"
|
||||||
"cancelReason=${cancelRequest.cancelReason}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePaymentError(
|
private fun handlePaymentError(
|
||||||
res: ClientHttpResponse
|
res: ClientHttpResponse,
|
||||||
|
calledBy: String
|
||||||
): Nothing {
|
): Nothing {
|
||||||
getErrorCodeByHttpStatus(res.statusCode).also {
|
getErrorCodeByHttpStatus(res.statusCode).also {
|
||||||
logTossPaymentError(res)
|
logTossPaymentError(res, calledBy)
|
||||||
throw PaymentException(it)
|
throw PaymentException(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logTossPaymentError(res: ClientHttpResponse): TossPaymentErrorResponse {
|
private fun logTossPaymentError(res: ClientHttpResponse, calledBy: String): TossPaymentErrorResponse {
|
||||||
val body = res.body
|
val body = res.body
|
||||||
val errorResponse = objectMapper.readValue(body, TossPaymentErrorResponse::class.java)
|
val errorResponse = objectMapper.readValue(body, TossPaymentErrorResponse::class.java)
|
||||||
body.close()
|
body.close()
|
||||||
|
|
||||||
log.error { "결제 실패. response: $errorResponse" }
|
log.error { "[TossPaymentClient.$calledBy] 요청 실패: response: $errorResponse" }
|
||||||
return errorResponse
|
return errorResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,21 +4,21 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
data class TossPaymentErrorResponse(
|
data class TossPaymentErrorResponse(
|
||||||
val code: String,
|
val code: String,
|
||||||
val message: String
|
val message: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class PaymentApproveRequest(
|
data class PaymentApproveRequest(
|
||||||
val paymentKey: String,
|
val paymentKey: String,
|
||||||
val orderId: String,
|
val orderId: String,
|
||||||
val amount: Long,
|
val amount: Long,
|
||||||
val paymentType: String
|
val paymentType: String
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class PaymentApproveResponse(
|
data class PaymentApproveResponse(
|
||||||
val paymentKey: String,
|
val paymentKey: String,
|
||||||
val orderId: String,
|
val orderId: String,
|
||||||
val totalAmount: Long,
|
val totalAmount: Long,
|
||||||
val approvedAt: OffsetDateTime
|
val approvedAt: OffsetDateTime
|
||||||
)
|
)
|
||||||
|
|||||||
@ -6,13 +6,13 @@ import java.time.OffsetDateTime
|
|||||||
@Entity
|
@Entity
|
||||||
@Table(name = "canceled_payments")
|
@Table(name = "canceled_payments")
|
||||||
class CanceledPaymentEntity(
|
class CanceledPaymentEntity(
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
var id: Long? = null,
|
var id: Long? = null,
|
||||||
|
|
||||||
var paymentKey: String,
|
var paymentKey: String,
|
||||||
var cancelReason: String,
|
var cancelReason: String,
|
||||||
var cancelAmount: Long,
|
var cancelAmount: Long,
|
||||||
var approvedAt: OffsetDateTime,
|
var approvedAt: OffsetDateTime,
|
||||||
var canceledAt: OffsetDateTime,
|
var canceledAt: OffsetDateTime,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -7,23 +7,23 @@ import java.time.OffsetDateTime
|
|||||||
@Entity
|
@Entity
|
||||||
@Table(name = "payments")
|
@Table(name = "payments")
|
||||||
class PaymentEntity(
|
class PaymentEntity(
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
var id: Long? = null,
|
var id: Long? = null,
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
var orderId: String,
|
var orderId: String,
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
var paymentKey: String,
|
var paymentKey: String,
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
var totalAmount: Long,
|
var totalAmount: Long,
|
||||||
|
|
||||||
@OneToOne(fetch = FetchType.LAZY)
|
@OneToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "reservation_id", nullable = false)
|
@JoinColumn(name = "reservation_id", nullable = false)
|
||||||
var reservation: ReservationEntity,
|
var reservation: ReservationEntity,
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
var approvedAt: OffsetDateTime
|
var approvedAt: OffsetDateTime
|
||||||
)
|
)
|
||||||
@ -8,33 +8,33 @@ import roomescape.reservation.web.toRetrieveResponse
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
data class PaymentCancelRequest(
|
data class PaymentCancelRequest(
|
||||||
val paymentKey: String,
|
val paymentKey: String,
|
||||||
val amount: Long,
|
val amount: Long,
|
||||||
val cancelReason: String
|
val cancelReason: String
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonDeserialize(using = PaymentCancelResponseDeserializer::class)
|
@JsonDeserialize(using = PaymentCancelResponseDeserializer::class)
|
||||||
data class PaymentCancelResponse(
|
data class PaymentCancelResponse(
|
||||||
val cancelStatus: String,
|
val cancelStatus: String,
|
||||||
val cancelReason: String,
|
val cancelReason: String,
|
||||||
val cancelAmount: Long,
|
val cancelAmount: Long,
|
||||||
val canceledAt: OffsetDateTime
|
val canceledAt: OffsetDateTime
|
||||||
)
|
)
|
||||||
|
|
||||||
data class PaymentCreateResponse(
|
data class PaymentCreateResponse(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val orderId: String,
|
val orderId: String,
|
||||||
val paymentKey: String,
|
val paymentKey: String,
|
||||||
val totalAmount: Long,
|
val totalAmount: Long,
|
||||||
val reservation: ReservationRetrieveResponse,
|
val reservation: ReservationRetrieveResponse,
|
||||||
val approvedAt: OffsetDateTime
|
val approvedAt: OffsetDateTime
|
||||||
)
|
)
|
||||||
|
|
||||||
fun PaymentEntity.toCreateResponse(): PaymentCreateResponse = PaymentCreateResponse(
|
fun PaymentEntity.toCreateResponse(): PaymentCreateResponse = PaymentCreateResponse(
|
||||||
id = this.id!!,
|
id = this.id!!,
|
||||||
orderId = this.orderId,
|
orderId = this.orderId,
|
||||||
paymentKey = this.paymentKey,
|
paymentKey = this.paymentKey,
|
||||||
totalAmount = this.totalAmount,
|
totalAmount = this.totalAmount,
|
||||||
reservation = this.reservation.toRetrieveResponse(),
|
reservation = this.reservation.toRetrieveResponse(),
|
||||||
approvedAt = this.approvedAt
|
approvedAt = this.approvedAt
|
||||||
)
|
)
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package roomescape.reservation.business
|
package roomescape.reservation.business
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.springframework.data.jpa.domain.Specification
|
import org.springframework.data.jpa.domain.Specification
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
@ -20,31 +21,37 @@ import roomescape.time.infrastructure.persistence.TimeEntity
|
|||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
private val log = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
class ReservationService(
|
class ReservationService(
|
||||||
private val reservationRepository: ReservationRepository,
|
private val reservationRepository: ReservationRepository,
|
||||||
private val timeService: TimeService,
|
private val timeService: TimeService,
|
||||||
private val memberService: MemberService,
|
private val memberService: MemberService,
|
||||||
private val themeService: ThemeService,
|
private val themeService: ThemeService,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun findReservations(): ReservationRetrieveListResponse {
|
fun findReservations(): ReservationRetrieveListResponse {
|
||||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||||
.confirmed()
|
.confirmed()
|
||||||
.build()
|
.build()
|
||||||
|
val reservations = findAllReservationByStatus(spec)
|
||||||
|
log.info { "[ReservationService.findReservations] ${reservations.size} 개의 확정 예약 조회 완료" }
|
||||||
|
|
||||||
return ReservationRetrieveListResponse(findAllReservationByStatus(spec))
|
return ReservationRetrieveListResponse(reservations)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun findAllWaiting(): ReservationRetrieveListResponse {
|
fun findAllWaiting(): ReservationRetrieveListResponse {
|
||||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||||
.waiting()
|
.waiting()
|
||||||
.build()
|
.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> {
|
private fun findAllReservationByStatus(spec: Specification<ReservationEntity>): List<ReservationRetrieveResponse> {
|
||||||
@ -52,102 +59,127 @@ class ReservationService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteReservation(reservationId: Long, memberId: Long) {
|
fun deleteReservation(reservationId: Long, memberId: Long) {
|
||||||
validateIsMemberAdmin(memberId)
|
validateIsMemberAdmin(memberId, "deleteReservation")
|
||||||
|
log.info { "[ReservationService.deleteReservation] 예약 삭제 시작: reservationId=$reservationId, memberId=$memberId" }
|
||||||
reservationRepository.deleteById(reservationId)
|
reservationRepository.deleteById(reservationId)
|
||||||
|
log.info { "[ReservationService.deleteReservation] 예약 삭제 완료: reservationId=$reservationId" }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createConfirmedReservation(
|
fun createConfirmedReservation(
|
||||||
request: ReservationCreateWithPaymentRequest,
|
request: ReservationCreateWithPaymentRequest,
|
||||||
memberId: Long
|
memberId: Long,
|
||||||
): ReservationEntity {
|
): ReservationEntity {
|
||||||
val themeId = request.themeId
|
val themeId = request.themeId
|
||||||
val timeId = request.timeId
|
val timeId = request.timeId
|
||||||
val date: LocalDate = request.date
|
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)
|
return reservationRepository.save(reservation)
|
||||||
|
.also { log.info { "[ReservationService.createConfirmedReservation] 예약 추가 완료: reservationId=${it.id}, status=${it.reservationStatus}" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createReservationByAdmin(request: AdminReservationCreateRequest): ReservationRetrieveResponse {
|
fun createReservationByAdmin(request: AdminReservationCreateRequest): ReservationRetrieveResponse {
|
||||||
validateIsReservationExist(request.themeId, request.timeId, request.date)
|
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(
|
return addReservationWithoutPayment(
|
||||||
request.themeId,
|
request.themeId,
|
||||||
request.timeId,
|
request.timeId,
|
||||||
request.date,
|
request.date,
|
||||||
request.memberId,
|
request.memberId,
|
||||||
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||||
)
|
).also {
|
||||||
|
log.info { "[ReservationService.createReservationByAdmin] 관리자 예약 추가 완료: reservationId=${it.id}" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createWaiting(request: WaitingCreateRequest, memberId: Long): ReservationRetrieveResponse {
|
fun createWaiting(request: WaitingCreateRequest, memberId: Long): ReservationRetrieveResponse {
|
||||||
validateMemberAlreadyReserve(request.themeId, request.timeId, request.date, memberId)
|
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(
|
return addReservationWithoutPayment(
|
||||||
request.themeId,
|
request.themeId,
|
||||||
request.timeId,
|
request.timeId,
|
||||||
request.date,
|
request.date,
|
||||||
memberId,
|
memberId,
|
||||||
ReservationStatus.WAITING
|
ReservationStatus.WAITING
|
||||||
)
|
).also {
|
||||||
|
log.info { "[ReservationService.createWaiting] 예약 대기 추가 완료: reservationId=${it.id}, status=${it.status}" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addReservationWithoutPayment(
|
private fun addReservationWithoutPayment(
|
||||||
themeId: Long,
|
themeId: Long,
|
||||||
timeId: Long,
|
timeId: Long,
|
||||||
date: LocalDate,
|
date: LocalDate,
|
||||||
memberId: Long,
|
memberId: Long,
|
||||||
status: ReservationStatus
|
status: ReservationStatus,
|
||||||
): ReservationRetrieveResponse = createEntity(timeId, themeId, date, memberId, status)
|
): ReservationRetrieveResponse = createEntity(timeId, themeId, date, memberId, status)
|
||||||
.also {
|
.also {
|
||||||
reservationRepository.save(it)
|
reservationRepository.save(it)
|
||||||
}.toRetrieveResponse()
|
}.toRetrieveResponse()
|
||||||
|
|
||||||
private fun validateMemberAlreadyReserve(themeId: Long, timeId: Long, date: LocalDate, memberId: Long) {
|
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()
|
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||||
.sameMemberId(memberId)
|
.sameMemberId(memberId)
|
||||||
.sameThemeId(themeId)
|
.sameThemeId(themeId)
|
||||||
.sameTimeId(timeId)
|
.sameTimeId(timeId)
|
||||||
.sameDate(date)
|
.sameDate(date)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
if (reservationRepository.exists(spec)) {
|
if (reservationRepository.exists(spec)) {
|
||||||
|
log.warn { "[ReservationService.validateMemberAlreadyReserve] 중복된 예약 존재: themeId=$themeId, timeId=$timeId, date=$date" }
|
||||||
throw ReservationException(ReservationErrorCode.ALREADY_RESERVE)
|
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()
|
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||||
.confirmed()
|
.confirmed()
|
||||||
.sameThemeId(themeId)
|
.sameThemeId(themeId)
|
||||||
.sameTimeId(timeId)
|
.sameTimeId(timeId)
|
||||||
.sameDate(date)
|
.sameDate(date)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
if (reservationRepository.exists(spec)) {
|
if (reservationRepository.exists(spec)) {
|
||||||
|
log.warn { "[ReservationService.$calledBy] 중복된 예약 존재: themeId=$themeId, timeId=$timeId, date=$date" }
|
||||||
throw ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED)
|
throw ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateDateAndTime(
|
private fun validateDateAndTime(
|
||||||
requestDate: LocalDate,
|
requestDate: LocalDate,
|
||||||
requestTime: TimeEntity
|
requestTime: TimeEntity,
|
||||||
) {
|
) {
|
||||||
val now = LocalDateTime.now()
|
val now = LocalDateTime.now()
|
||||||
val request = LocalDateTime.of(requestDate, requestTime.startAt)
|
val request = LocalDateTime.of(requestDate, requestTime.startAt)
|
||||||
|
|
||||||
if (request.isBefore(now)) {
|
if (request.isBefore(now)) {
|
||||||
|
log.info { "[ReservationService.validateDateAndTime] 날짜 범위 오류. request=$request, now=$now" }
|
||||||
throw ReservationException(ReservationErrorCode.PAST_REQUEST_DATETIME)
|
throw ReservationException(ReservationErrorCode.PAST_REQUEST_DATETIME)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createEntity(
|
private fun createEntity(
|
||||||
timeId: Long,
|
timeId: Long,
|
||||||
themeId: Long,
|
themeId: Long,
|
||||||
date: LocalDate,
|
date: LocalDate,
|
||||||
memberId: Long,
|
memberId: Long,
|
||||||
status: ReservationStatus
|
status: ReservationStatus,
|
||||||
): ReservationEntity {
|
): ReservationEntity {
|
||||||
val time: TimeEntity = timeService.findById(timeId)
|
val time: TimeEntity = timeService.findById(timeId)
|
||||||
val theme: ThemeEntity = themeService.findById(themeId)
|
val theme: ThemeEntity = themeService.findById(themeId)
|
||||||
@ -156,86 +188,132 @@ class ReservationService(
|
|||||||
validateDateAndTime(date, time)
|
validateDateAndTime(date, time)
|
||||||
|
|
||||||
return ReservationEntity(
|
return ReservationEntity(
|
||||||
date = date,
|
date = date,
|
||||||
time = time,
|
time = time,
|
||||||
theme = theme,
|
theme = theme,
|
||||||
member = member,
|
member = member,
|
||||||
reservationStatus = status
|
reservationStatus = status
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun searchReservations(
|
fun searchReservations(
|
||||||
themeId: Long?,
|
themeId: Long?,
|
||||||
memberId: Long?,
|
memberId: Long?,
|
||||||
dateFrom: LocalDate?,
|
dateFrom: LocalDate?,
|
||||||
dateTo: LocalDate?
|
dateTo: LocalDate?,
|
||||||
): ReservationRetrieveListResponse {
|
): ReservationRetrieveListResponse {
|
||||||
validateDateForSearch(dateFrom, dateTo)
|
log.debug { "[ReservationService.searchReservations] 예약 검색 시작: themeId=$themeId, memberId=$memberId, dateFrom=$dateFrom, dateTo=$dateTo" }
|
||||||
|
validateSearchDateRange(dateFrom, dateTo)
|
||||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||||
.confirmed()
|
.confirmed()
|
||||||
.sameThemeId(themeId)
|
.sameThemeId(themeId)
|
||||||
.sameMemberId(memberId)
|
.sameMemberId(memberId)
|
||||||
.dateStartFrom(dateFrom)
|
.dateStartFrom(dateFrom)
|
||||||
.dateEndAt(dateTo)
|
.dateEndAt(dateTo)
|
||||||
.build()
|
.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) {
|
if (startFrom == null || endAt == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (startFrom.isAfter(endAt)) {
|
if (startFrom.isAfter(endAt)) {
|
||||||
|
log.info { "[ReservationService.validateSearchDateRange] 조회 범위 오류: startFrom=$startFrom, endAt=$endAt" }
|
||||||
throw ReservationException(ReservationErrorCode.INVALID_SEARCH_DATE_RANGE)
|
throw ReservationException(ReservationErrorCode.INVALID_SEARCH_DATE_RANGE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun findReservationsByMemberId(memberId: Long): MyReservationRetrieveListResponse {
|
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) {
|
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)) {
|
if (reservationRepository.isExistConfirmedReservation(reservationId)) {
|
||||||
|
log.warn { "[ReservationService.confirmWaiting] 승인 실패(이미 확정된 예약 존재): reservationId=$reservationId" }
|
||||||
throw ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS)
|
throw ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.debug { "[ReservationService.confirmWaiting] 대기 예약 상태 변경 시작: reservationId=$reservationId" }
|
||||||
reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)
|
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) {
|
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()) {
|
if (!reservation.isWaiting()) {
|
||||||
|
log.warn {
|
||||||
|
"[ReservationService.deleteWaiting] 대기 취소 실패(대기 예약이 아님): reservationId=$reservationId" +
|
||||||
|
", currentStatus=${reservation.reservationStatus} memberId=$memberId"
|
||||||
|
}
|
||||||
throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
|
throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
|
||||||
}
|
}
|
||||||
if (!reservation.isReservedBy(memberId)) {
|
if (!reservation.isReservedBy(memberId)) {
|
||||||
|
log.error {
|
||||||
|
"[ReservationService.deleteWaiting] 대기 취소 실패(예약자 본인의 취소 요청이 아님): reservationId=$reservationId" +
|
||||||
|
", memberId=$memberId "
|
||||||
|
}
|
||||||
throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER)
|
throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER)
|
||||||
}
|
}
|
||||||
|
log.debug { "[ReservationService.deleteWaiting] 대기 예약 삭제 시작: reservationId=$reservationId" }
|
||||||
reservationRepository.delete(reservation)
|
reservationRepository.delete(reservation)
|
||||||
|
log.debug { "[ReservationService.deleteWaiting] 대기 예약 삭제 완료: reservationId=$reservationId" }
|
||||||
|
|
||||||
|
log.info { "[ReservationService.deleteWaiting] 대기 취소 완료: reservationId=$reservationId, memberId=$memberId" }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rejectWaiting(reservationId: Long, memberId: Long) {
|
fun rejectWaiting(reservationId: Long, memberId: Long) {
|
||||||
validateIsMemberAdmin(memberId)
|
validateIsMemberAdmin(memberId, "rejectWaiting")
|
||||||
val reservation: ReservationEntity = findReservationOrThrow(reservationId)
|
log.debug { "[ReservationService.rejectWaiting] 대기 예약 삭제 시작: reservationId=$reservationId (by adminId=$memberId)" }
|
||||||
|
val reservation: ReservationEntity = findReservationOrThrow(reservationId, "rejectWaiting")
|
||||||
|
|
||||||
if (!reservation.isWaiting()) {
|
if (!reservation.isWaiting()) {
|
||||||
|
log.warn {
|
||||||
|
"[ReservationService.rejectWaiting] 대기 예약 삭제 실패(이미 확정 상태): reservationId=$reservationId" +
|
||||||
|
", status=${reservation.reservationStatus}"
|
||||||
|
}
|
||||||
throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
|
throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
|
||||||
}
|
}
|
||||||
reservationRepository.delete(reservation)
|
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)
|
val member: MemberEntity = memberService.findById(memberId)
|
||||||
if (member.isAdmin()) {
|
if (member.isAdmin()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.warn { "[ReservationService.$calledBy] 관리자가 아님: memberId=$memberId, role=${member.role}" }
|
||||||
throw ReservationException(ReservationErrorCode.NO_PERMISSION)
|
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)
|
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
|
package roomescape.reservation.business
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import roomescape.payment.business.PaymentService
|
import roomescape.payment.business.PaymentService
|
||||||
@ -11,47 +12,57 @@ import roomescape.reservation.web.ReservationCreateWithPaymentRequest
|
|||||||
import roomescape.reservation.web.ReservationRetrieveResponse
|
import roomescape.reservation.web.ReservationRetrieveResponse
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
private val log = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
class ReservationWithPaymentService(
|
class ReservationWithPaymentService(
|
||||||
private val reservationService: ReservationService,
|
private val reservationService: ReservationService,
|
||||||
private val paymentService: PaymentService
|
private val paymentService: PaymentService,
|
||||||
) {
|
) {
|
||||||
fun createReservationAndPayment(
|
fun createReservationAndPayment(
|
||||||
request: ReservationCreateWithPaymentRequest,
|
request: ReservationCreateWithPaymentRequest,
|
||||||
paymentInfo: PaymentApproveResponse,
|
paymentInfo: PaymentApproveResponse,
|
||||||
memberId: Long
|
memberId: Long,
|
||||||
): ReservationRetrieveResponse {
|
): ReservationRetrieveResponse {
|
||||||
|
log.info { "[ReservationWithPaymentService.createReservationAndPayment] 예약 & 결제 정보 저장 시작: memberId=$memberId, paymentInfo=$paymentInfo" }
|
||||||
val reservation: ReservationEntity = reservationService.createConfirmedReservation(request, memberId)
|
val reservation: ReservationEntity = reservationService.createConfirmedReservation(request, memberId)
|
||||||
|
|
||||||
return paymentService.createPayment(paymentInfo, reservation)
|
return paymentService.createPayment(paymentInfo, reservation)
|
||||||
.reservation
|
.also { log.info { "[ReservationWithPaymentService.createReservationAndPayment] 예약 & 결제 정보 저장 완료: reservationId=${reservation.id}, paymentId=${it.id}" } }
|
||||||
|
.reservation
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createCanceledPayment(
|
fun createCanceledPayment(
|
||||||
cancelInfo: PaymentCancelResponse,
|
cancelInfo: PaymentCancelResponse,
|
||||||
approvedAt: OffsetDateTime,
|
approvedAt: OffsetDateTime,
|
||||||
paymentKey: String
|
paymentKey: String,
|
||||||
) {
|
) {
|
||||||
paymentService.createCanceledPayment(cancelInfo, approvedAt, paymentKey)
|
paymentService.createCanceledPayment(cancelInfo, approvedAt, paymentKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteReservationAndPayment(
|
fun deleteReservationAndPayment(
|
||||||
reservationId: Long,
|
reservationId: Long,
|
||||||
memberId: Long
|
memberId: Long,
|
||||||
): PaymentCancelRequest {
|
): PaymentCancelRequest {
|
||||||
|
log.info { "[ReservationWithPaymentService.deleteReservationAndPayment] 결제 취소 정보 저장 & 예약 삭제 시작: reservationId=$reservationId" }
|
||||||
val paymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId)
|
val paymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId)
|
||||||
reservationService.deleteReservation(reservationId, memberId)
|
|
||||||
|
|
||||||
|
reservationService.deleteReservation(reservationId, memberId)
|
||||||
|
log.info { "[ReservationWithPaymentService.deleteReservationAndPayment] 결제 취소 정보 저장 & 예약 삭제 완료: reservationId=$reservationId" }
|
||||||
return paymentCancelRequest
|
return paymentCancelRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@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(
|
fun updateCanceledTime(
|
||||||
paymentKey: String,
|
paymentKey: String,
|
||||||
canceledAt: OffsetDateTime
|
canceledAt: OffsetDateTime,
|
||||||
) {
|
) {
|
||||||
paymentService.updateCanceledTime(paymentKey, canceledAt)
|
paymentService.updateCanceledTime(paymentKey, canceledAt)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,58 +32,66 @@ interface ReservationAPI {
|
|||||||
@Operation(summary = "자신의 예약 및 대기 조회", tags = ["로그인이 필요한 API"])
|
@Operation(summary = "자신의 예약 및 대기 조회", tags = ["로그인이 필요한 API"])
|
||||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||||
fun findReservationsByMemberId(
|
fun findReservationsByMemberId(
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long
|
@MemberId @Parameter(hidden = true) memberId: Long
|
||||||
): ResponseEntity<CommonApiResponse<MyReservationRetrieveListResponse>>
|
): ResponseEntity<CommonApiResponse<MyReservationRetrieveListResponse>>
|
||||||
|
|
||||||
@Admin
|
@Admin
|
||||||
@Operation(summary = "관리자의 예약 검색", description = "특정 조건에 해당되는 예약 검색", tags = ["관리자 로그인이 필요한 API"])
|
@Operation(summary = "관리자의 예약 검색", description = "특정 조건에 해당되는 예약 검색", tags = ["관리자 로그인이 필요한 API"])
|
||||||
@ApiResponses(
|
@ApiResponses(
|
||||||
ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)
|
ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)
|
||||||
)
|
)
|
||||||
fun searchReservations(
|
fun searchReservations(
|
||||||
@RequestParam(required = false) themeId: Long?,
|
@RequestParam(required = false) themeId: Long?,
|
||||||
@RequestParam(required = false) memberId: Long?,
|
@RequestParam(required = false) memberId: Long?,
|
||||||
@RequestParam(required = false) dateFrom: LocalDate?,
|
@RequestParam(required = false) dateFrom: LocalDate?,
|
||||||
@RequestParam(required = false) dateTo: LocalDate?
|
@RequestParam(required = false) dateTo: LocalDate?
|
||||||
): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>>
|
): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>>
|
||||||
|
|
||||||
@Admin
|
@Admin
|
||||||
@Operation(summary = "관리자의 예약 취소", tags = ["관리자 로그인이 필요한 API"])
|
@Operation(summary = "관리자의 예약 취소", tags = ["관리자 로그인이 필요한 API"])
|
||||||
@ApiResponses(
|
@ApiResponses(
|
||||||
ApiResponse(responseCode = "204", description = "성공"),
|
ApiResponse(responseCode = "204", description = "성공"),
|
||||||
)
|
)
|
||||||
fun cancelReservationByAdmin(
|
fun cancelReservationByAdmin(
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||||
@PathVariable("id") reservationId: Long
|
@PathVariable("id") reservationId: Long
|
||||||
): ResponseEntity<CommonApiResponse<Unit>>
|
): ResponseEntity<CommonApiResponse<Unit>>
|
||||||
|
|
||||||
@LoginRequired
|
@LoginRequired
|
||||||
@Operation(summary = "예약 추가", tags = ["로그인이 필요한 API"])
|
@Operation(summary = "예약 추가", tags = ["로그인이 필요한 API"])
|
||||||
@ApiResponses(
|
@ApiResponses(
|
||||||
ApiResponse(
|
ApiResponse(
|
||||||
responseCode = "201",
|
responseCode = "201",
|
||||||
description = "성공",
|
description = "성공",
|
||||||
useReturnTypeSchema = true,
|
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(
|
fun createReservationWithPayment(
|
||||||
@Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest,
|
@Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest,
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long
|
@MemberId @Parameter(hidden = true) memberId: Long
|
||||||
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
|
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
|
||||||
|
|
||||||
@Admin
|
@Admin
|
||||||
@Operation(summary = "관리자 예약 추가", tags = ["관리자 로그인이 필요한 API"])
|
@Operation(summary = "관리자 예약 추가", tags = ["관리자 로그인이 필요한 API"])
|
||||||
@ApiResponses(
|
@ApiResponses(
|
||||||
ApiResponse(
|
ApiResponse(
|
||||||
responseCode = "201",
|
responseCode = "201",
|
||||||
description = "성공",
|
description = "성공",
|
||||||
useReturnTypeSchema = true,
|
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(
|
fun createReservationByAdmin(
|
||||||
@Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest,
|
@Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest,
|
||||||
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
|
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
|
||||||
|
|
||||||
@Admin
|
@Admin
|
||||||
@ -94,45 +102,49 @@ interface ReservationAPI {
|
|||||||
@LoginRequired
|
@LoginRequired
|
||||||
@Operation(summary = "예약 대기 신청", tags = ["로그인이 필요한 API"])
|
@Operation(summary = "예약 대기 신청", tags = ["로그인이 필요한 API"])
|
||||||
@ApiResponses(
|
@ApiResponses(
|
||||||
ApiResponse(
|
ApiResponse(
|
||||||
responseCode = "201",
|
responseCode = "201",
|
||||||
description = "성공",
|
description = "성공",
|
||||||
useReturnTypeSchema = true,
|
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(
|
fun createWaiting(
|
||||||
@Valid @RequestBody waitingCreateRequest: WaitingCreateRequest,
|
@Valid @RequestBody waitingCreateRequest: WaitingCreateRequest,
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||||
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
|
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
|
||||||
|
|
||||||
@LoginRequired
|
@LoginRequired
|
||||||
@Operation(summary = "예약 대기 취소", tags = ["로그인이 필요한 API"])
|
@Operation(summary = "예약 대기 취소", tags = ["로그인이 필요한 API"])
|
||||||
@ApiResponses(
|
@ApiResponses(
|
||||||
ApiResponse(responseCode = "204", description = "성공"),
|
ApiResponse(responseCode = "204", description = "성공"),
|
||||||
)
|
)
|
||||||
fun cancelWaitingByMember(
|
fun cancelWaitingByMember(
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||||
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
|
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
|
||||||
): ResponseEntity<CommonApiResponse<Unit>>
|
): ResponseEntity<CommonApiResponse<Unit>>
|
||||||
|
|
||||||
@Admin
|
@Admin
|
||||||
@Operation(summary = "대기 중인 예약 승인", tags = ["관리자 로그인이 필요한 API"])
|
@Operation(summary = "대기 중인 예약 승인", tags = ["관리자 로그인이 필요한 API"])
|
||||||
@ApiResponses(
|
@ApiResponses(
|
||||||
ApiResponse(responseCode = "200", description = "성공"),
|
ApiResponse(responseCode = "200", description = "성공"),
|
||||||
)
|
)
|
||||||
fun confirmWaiting(
|
fun confirmWaiting(
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||||
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
|
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
|
||||||
): ResponseEntity<CommonApiResponse<Unit>>
|
): ResponseEntity<CommonApiResponse<Unit>>
|
||||||
|
|
||||||
@Admin
|
@Admin
|
||||||
@Operation(summary = "대기 중인 예약 거절", tags = ["관리자 로그인이 필요한 API"])
|
@Operation(summary = "대기 중인 예약 거절", tags = ["관리자 로그인이 필요한 API"])
|
||||||
@ApiResponses(
|
@ApiResponses(
|
||||||
ApiResponse(responseCode = "204", description = "대기 중인 예약 거절 성공"),
|
ApiResponse(responseCode = "204", description = "대기 중인 예약 거절 성공"),
|
||||||
)
|
)
|
||||||
fun rejectWaiting(
|
fun rejectWaiting(
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||||
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
|
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
|
||||||
): ResponseEntity<CommonApiResponse<Unit>>
|
): ResponseEntity<CommonApiResponse<Unit>>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import org.springframework.http.HttpStatus
|
|||||||
import roomescape.common.exception.ErrorCode
|
import roomescape.common.exception.ErrorCode
|
||||||
|
|
||||||
enum class ReservationErrorCode(
|
enum class ReservationErrorCode(
|
||||||
override val httpStatus: HttpStatus,
|
override val httpStatus: HttpStatus,
|
||||||
override val errorCode: String,
|
override val errorCode: String,
|
||||||
override val message: String
|
override val message: String
|
||||||
) : ErrorCode {
|
) : ErrorCode {
|
||||||
RESERVATION_NOT_FOUND(HttpStatus.NOT_FOUND, "R001", "예약을 찾을 수 없어요."),
|
RESERVATION_NOT_FOUND(HttpStatus.NOT_FOUND, "R001", "예약을 찾을 수 없어요."),
|
||||||
RESERVATION_DUPLICATED(HttpStatus.BAD_REQUEST, "R002", "이미 같은 예약이 있어요."),
|
RESERVATION_DUPLICATED(HttpStatus.BAD_REQUEST, "R002", "이미 같은 예약이 있어요."),
|
||||||
|
|||||||
@ -4,6 +4,6 @@ import roomescape.common.exception.ErrorCode
|
|||||||
import roomescape.common.exception.RoomescapeException
|
import roomescape.common.exception.RoomescapeException
|
||||||
|
|
||||||
class ReservationException(
|
class ReservationException(
|
||||||
override val errorCode: ErrorCode,
|
override val errorCode: ErrorCode,
|
||||||
override val message: String = errorCode.message
|
override val message: String = errorCode.message
|
||||||
) : RoomescapeException(errorCode, message)
|
) : RoomescapeException(errorCode, message)
|
||||||
|
|||||||
@ -10,26 +10,26 @@ import java.time.LocalDate
|
|||||||
@Entity
|
@Entity
|
||||||
@Table(name = "reservations")
|
@Table(name = "reservations")
|
||||||
class ReservationEntity(
|
class ReservationEntity(
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
var id: Long? = null,
|
var id: Long? = null,
|
||||||
|
|
||||||
var date: LocalDate,
|
var date: LocalDate,
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "time_id", nullable = false)
|
@JoinColumn(name = "time_id", nullable = false)
|
||||||
var time: TimeEntity,
|
var time: TimeEntity,
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "theme_id", nullable = false)
|
@JoinColumn(name = "theme_id", nullable = false)
|
||||||
var theme: ThemeEntity,
|
var theme: ThemeEntity,
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "member_id", nullable = false)
|
@JoinColumn(name = "member_id", nullable = false)
|
||||||
var member: MemberEntity,
|
var member: MemberEntity,
|
||||||
|
|
||||||
@Enumerated(value = EnumType.STRING)
|
@Enumerated(value = EnumType.STRING)
|
||||||
var reservationStatus: ReservationStatus
|
var reservationStatus: ReservationStatus
|
||||||
) {
|
) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun isWaiting(): Boolean = reservationStatus == ReservationStatus.WAITING
|
fun isWaiting(): Boolean = reservationStatus == ReservationStatus.WAITING
|
||||||
|
|||||||
@ -16,17 +16,20 @@ interface ReservationRepository
|
|||||||
fun findByDateAndThemeId(date: LocalDate, themeId: Long): List<ReservationEntity>
|
fun findByDateAndThemeId(date: LocalDate, themeId: Long): List<ReservationEntity>
|
||||||
|
|
||||||
@Modifying
|
@Modifying
|
||||||
@Query("""
|
@Query(
|
||||||
|
"""
|
||||||
UPDATE ReservationEntity r
|
UPDATE ReservationEntity r
|
||||||
SET r.reservationStatus = :status
|
SET r.reservationStatus = :status
|
||||||
WHERE r.id = :id
|
WHERE r.id = :id
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
fun updateStatusByReservationId(
|
fun updateStatusByReservationId(
|
||||||
@Param(value = "id") reservationId: Long,
|
@Param(value = "id") reservationId: Long,
|
||||||
@Param(value = "status") statusForChange: ReservationStatus
|
@Param(value = "status") statusForChange: ReservationStatus
|
||||||
): Int
|
): Int
|
||||||
|
|
||||||
@Query("""
|
@Query(
|
||||||
|
"""
|
||||||
SELECT EXISTS (
|
SELECT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM ReservationEntity r2
|
FROM ReservationEntity r2
|
||||||
@ -39,10 +42,12 @@ interface ReservationRepository
|
|||||||
AND r.reservationStatus != 'WAITING'
|
AND r.reservationStatus != 'WAITING'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
fun isExistConfirmedReservation(@Param("id") reservationId: Long): Boolean
|
fun isExistConfirmedReservation(@Param("id") reservationId: Long): Boolean
|
||||||
|
|
||||||
@Query("""
|
@Query(
|
||||||
|
"""
|
||||||
SELECT new roomescape.reservation.web.MyReservationRetrieveResponse(
|
SELECT new roomescape.reservation.web.MyReservationRetrieveResponse(
|
||||||
r.id,
|
r.id,
|
||||||
t.name,
|
t.name,
|
||||||
@ -58,6 +63,7 @@ interface ReservationRepository
|
|||||||
LEFT JOIN PaymentEntity p
|
LEFT JOIN PaymentEntity p
|
||||||
ON p.reservation = r
|
ON p.reservation = r
|
||||||
WHERE r.member.id = :memberId
|
WHERE r.member.id = :memberId
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
fun findAllByMemberId(memberId: Long): List<MyReservationRetrieveResponse>
|
fun findAllByMemberId(memberId: Long): List<MyReservationRetrieveResponse>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import roomescape.time.infrastructure.persistence.TimeEntity
|
|||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
class ReservationSearchSpecification(
|
class ReservationSearchSpecification(
|
||||||
private var spec: Specification<ReservationEntity> = Specification { _, _, _ -> null }
|
private var spec: Specification<ReservationEntity> = Specification { _, _, _ -> null }
|
||||||
) {
|
) {
|
||||||
fun sameThemeId(themeId: Long?): ReservationSearchSpecification = andIfNotNull(themeId?.let {
|
fun sameThemeId(themeId: Long?): ReservationSearchSpecification = andIfNotNull(themeId?.let {
|
||||||
Specification { root, _, cb ->
|
Specification { root, _, cb ->
|
||||||
@ -35,21 +35,21 @@ class ReservationSearchSpecification(
|
|||||||
|
|
||||||
fun confirmed(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
|
fun confirmed(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
|
||||||
cb.or(
|
cb.or(
|
||||||
cb.equal(
|
cb.equal(
|
||||||
root.get<ReservationStatus>("reservationStatus"),
|
root.get<ReservationStatus>("reservationStatus"),
|
||||||
ReservationStatus.CONFIRMED
|
ReservationStatus.CONFIRMED
|
||||||
),
|
),
|
||||||
cb.equal(
|
cb.equal(
|
||||||
root.get<ReservationStatus>("reservationStatus"),
|
root.get<ReservationStatus>("reservationStatus"),
|
||||||
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun waiting(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
|
fun waiting(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
|
||||||
cb.equal(
|
cb.equal(
|
||||||
root.get<ReservationStatus>("reservationStatus"),
|
root.get<ReservationStatus>("reservationStatus"),
|
||||||
ReservationStatus.WAITING
|
ReservationStatus.WAITING
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,9 +18,9 @@ import java.time.LocalDate
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
class ReservationController(
|
class ReservationController(
|
||||||
private val reservationWithPaymentService: ReservationWithPaymentService,
|
private val reservationWithPaymentService: ReservationWithPaymentService,
|
||||||
private val reservationService: ReservationService,
|
private val reservationService: ReservationService,
|
||||||
private val paymentClient: TossPaymentClient
|
private val paymentClient: TossPaymentClient
|
||||||
) : ReservationAPI {
|
) : ReservationAPI {
|
||||||
@GetMapping("/reservations")
|
@GetMapping("/reservations")
|
||||||
override fun findReservations(): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
|
override fun findReservations(): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
|
||||||
@ -31,7 +31,7 @@ class ReservationController(
|
|||||||
|
|
||||||
@GetMapping("/reservations-mine")
|
@GetMapping("/reservations-mine")
|
||||||
override fun findReservationsByMemberId(
|
override fun findReservationsByMemberId(
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long
|
@MemberId @Parameter(hidden = true) memberId: Long
|
||||||
): ResponseEntity<CommonApiResponse<MyReservationRetrieveListResponse>> {
|
): ResponseEntity<CommonApiResponse<MyReservationRetrieveListResponse>> {
|
||||||
val response: MyReservationRetrieveListResponse = reservationService.findReservationsByMemberId(memberId)
|
val response: MyReservationRetrieveListResponse = reservationService.findReservationsByMemberId(memberId)
|
||||||
|
|
||||||
@ -40,16 +40,16 @@ class ReservationController(
|
|||||||
|
|
||||||
@GetMapping("/reservations/search")
|
@GetMapping("/reservations/search")
|
||||||
override fun searchReservations(
|
override fun searchReservations(
|
||||||
@RequestParam(required = false) themeId: Long?,
|
@RequestParam(required = false) themeId: Long?,
|
||||||
@RequestParam(required = false) memberId: Long?,
|
@RequestParam(required = false) memberId: Long?,
|
||||||
@RequestParam(required = false) dateFrom: LocalDate?,
|
@RequestParam(required = false) dateFrom: LocalDate?,
|
||||||
@RequestParam(required = false) dateTo: LocalDate?
|
@RequestParam(required = false) dateTo: LocalDate?
|
||||||
): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
|
): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
|
||||||
val response: ReservationRetrieveListResponse = reservationService.searchReservations(
|
val response: ReservationRetrieveListResponse = reservationService.searchReservations(
|
||||||
themeId,
|
themeId,
|
||||||
memberId,
|
memberId,
|
||||||
dateFrom,
|
dateFrom,
|
||||||
dateTo
|
dateTo
|
||||||
)
|
)
|
||||||
|
|
||||||
return ResponseEntity.ok(CommonApiResponse(response))
|
return ResponseEntity.ok(CommonApiResponse(response))
|
||||||
@ -57,8 +57,8 @@ class ReservationController(
|
|||||||
|
|
||||||
@DeleteMapping("/reservations/{id}")
|
@DeleteMapping("/reservations/{id}")
|
||||||
override fun cancelReservationByAdmin(
|
override fun cancelReservationByAdmin(
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||||
@PathVariable("id") reservationId: Long
|
@PathVariable("id") reservationId: Long
|
||||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||||
if (reservationWithPaymentService.isNotPaidReservation(reservationId)) {
|
if (reservationWithPaymentService.isNotPaidReservation(reservationId)) {
|
||||||
reservationService.deleteReservation(reservationId, memberId)
|
reservationService.deleteReservation(reservationId, memberId)
|
||||||
@ -67,47 +67,56 @@ class ReservationController(
|
|||||||
|
|
||||||
val paymentCancelRequest = reservationWithPaymentService.deleteReservationAndPayment(reservationId, memberId)
|
val paymentCancelRequest = reservationWithPaymentService.deleteReservationAndPayment(reservationId, memberId)
|
||||||
val paymentCancelResponse = paymentClient.cancel(paymentCancelRequest)
|
val paymentCancelResponse = paymentClient.cancel(paymentCancelRequest)
|
||||||
reservationWithPaymentService.updateCanceledTime(paymentCancelRequest.paymentKey,
|
reservationWithPaymentService.updateCanceledTime(
|
||||||
paymentCancelResponse.canceledAt)
|
paymentCancelRequest.paymentKey,
|
||||||
|
paymentCancelResponse.canceledAt
|
||||||
|
)
|
||||||
|
|
||||||
return ResponseEntity.noContent().build()
|
return ResponseEntity.noContent().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/reservations")
|
@PostMapping("/reservations")
|
||||||
override fun createReservationWithPayment(
|
override fun createReservationWithPayment(
|
||||||
@Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest,
|
@Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest,
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long
|
@MemberId @Parameter(hidden = true) memberId: Long
|
||||||
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
|
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
|
||||||
val paymentRequest: PaymentApproveRequest = reservationCreateWithPaymentRequest.toPaymentApproveRequest()
|
val paymentRequest: PaymentApproveRequest = reservationCreateWithPaymentRequest.toPaymentApproveRequest()
|
||||||
val paymentResponse: PaymentApproveResponse = paymentClient.confirm(paymentRequest)
|
val paymentResponse: PaymentApproveResponse = paymentClient.confirm(paymentRequest)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val reservationRetrieveResponse: ReservationRetrieveResponse = reservationWithPaymentService.createReservationAndPayment(
|
val reservationRetrieveResponse: ReservationRetrieveResponse =
|
||||||
|
reservationWithPaymentService.createReservationAndPayment(
|
||||||
reservationCreateWithPaymentRequest,
|
reservationCreateWithPaymentRequest,
|
||||||
paymentResponse,
|
paymentResponse,
|
||||||
memberId
|
memberId
|
||||||
)
|
)
|
||||||
return ResponseEntity.created(URI.create("/reservations/${reservationRetrieveResponse.id}"))
|
return ResponseEntity.created(URI.create("/reservations/${reservationRetrieveResponse.id}"))
|
||||||
.body(CommonApiResponse(reservationRetrieveResponse))
|
.body(CommonApiResponse(reservationRetrieveResponse))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val cancelRequest = PaymentCancelRequest(paymentRequest.paymentKey,
|
val cancelRequest = PaymentCancelRequest(
|
||||||
paymentRequest.amount, e.message!!)
|
paymentRequest.paymentKey,
|
||||||
|
paymentRequest.amount,
|
||||||
|
e.message!!
|
||||||
|
)
|
||||||
val paymentCancelResponse = paymentClient.cancel(cancelRequest)
|
val paymentCancelResponse = paymentClient.cancel(cancelRequest)
|
||||||
reservationWithPaymentService.createCanceledPayment(paymentCancelResponse, paymentResponse.approvedAt,
|
reservationWithPaymentService.createCanceledPayment(
|
||||||
paymentRequest.paymentKey)
|
paymentCancelResponse,
|
||||||
|
paymentResponse.approvedAt,
|
||||||
|
paymentRequest.paymentKey
|
||||||
|
)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/reservations/admin")
|
@PostMapping("/reservations/admin")
|
||||||
override fun createReservationByAdmin(
|
override fun createReservationByAdmin(
|
||||||
@Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest
|
@Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest
|
||||||
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
|
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
|
||||||
val response: ReservationRetrieveResponse =
|
val response: ReservationRetrieveResponse =
|
||||||
reservationService.createReservationByAdmin(adminReservationRequest)
|
reservationService.createReservationByAdmin(adminReservationRequest)
|
||||||
|
|
||||||
return ResponseEntity.created(URI.create("/reservations/${response.id}"))
|
return ResponseEntity.created(URI.create("/reservations/${response.id}"))
|
||||||
.body(CommonApiResponse(response))
|
.body(CommonApiResponse(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/reservations/waiting")
|
@GetMapping("/reservations/waiting")
|
||||||
@ -119,22 +128,22 @@ class ReservationController(
|
|||||||
|
|
||||||
@PostMapping("/reservations/waiting")
|
@PostMapping("/reservations/waiting")
|
||||||
override fun createWaiting(
|
override fun createWaiting(
|
||||||
@Valid @RequestBody waitingCreateRequest: WaitingCreateRequest,
|
@Valid @RequestBody waitingCreateRequest: WaitingCreateRequest,
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||||
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
|
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
|
||||||
val response: ReservationRetrieveResponse = reservationService.createWaiting(
|
val response: ReservationRetrieveResponse = reservationService.createWaiting(
|
||||||
waitingCreateRequest,
|
waitingCreateRequest,
|
||||||
memberId
|
memberId
|
||||||
)
|
)
|
||||||
|
|
||||||
return ResponseEntity.created(URI.create("/reservations/${response.id}"))
|
return ResponseEntity.created(URI.create("/reservations/${response.id}"))
|
||||||
.body(CommonApiResponse(response))
|
.body(CommonApiResponse(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/reservations/waiting/{id}")
|
@DeleteMapping("/reservations/waiting/{id}")
|
||||||
override fun cancelWaitingByMember(
|
override fun cancelWaitingByMember(
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||||
@PathVariable("id") reservationId: Long
|
@PathVariable("id") reservationId: Long
|
||||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||||
reservationService.deleteWaiting(reservationId, memberId)
|
reservationService.deleteWaiting(reservationId, memberId)
|
||||||
|
|
||||||
@ -143,8 +152,8 @@ class ReservationController(
|
|||||||
|
|
||||||
@PostMapping("/reservations/waiting/{id}/confirm")
|
@PostMapping("/reservations/waiting/{id}/confirm")
|
||||||
override fun confirmWaiting(
|
override fun confirmWaiting(
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||||
@PathVariable("id") reservationId: Long
|
@PathVariable("id") reservationId: Long
|
||||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||||
reservationService.confirmWaiting(reservationId, memberId)
|
reservationService.confirmWaiting(reservationId, memberId)
|
||||||
|
|
||||||
@ -153,8 +162,8 @@ class ReservationController(
|
|||||||
|
|
||||||
@PostMapping("/reservations/waiting/{id}/reject")
|
@PostMapping("/reservations/waiting/{id}/reject")
|
||||||
override fun rejectWaiting(
|
override fun rejectWaiting(
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||||
@PathVariable("id") reservationId: Long
|
@PathVariable("id") reservationId: Long
|
||||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||||
reservationService.rejectWaiting(reservationId, memberId)
|
reservationService.rejectWaiting(reservationId, memberId)
|
||||||
|
|
||||||
|
|||||||
@ -5,36 +5,36 @@ import roomescape.payment.infrastructure.client.PaymentApproveRequest
|
|||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
data class AdminReservationCreateRequest(
|
data class AdminReservationCreateRequest(
|
||||||
val date: LocalDate,
|
val date: LocalDate,
|
||||||
val timeId: Long,
|
val timeId: Long,
|
||||||
val themeId: Long,
|
val themeId: Long,
|
||||||
val memberId: Long
|
val memberId: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ReservationCreateWithPaymentRequest(
|
data class ReservationCreateWithPaymentRequest(
|
||||||
val date: LocalDate,
|
val date: LocalDate,
|
||||||
val timeId: Long,
|
val timeId: Long,
|
||||||
val themeId: Long,
|
val themeId: Long,
|
||||||
|
|
||||||
@Schema(description = "결제 위젯을 통해 받은 결제 키")
|
@Schema(description = "결제 위젯을 통해 받은 결제 키")
|
||||||
val paymentKey: String,
|
val paymentKey: String,
|
||||||
|
|
||||||
@Schema(description = "결제 위젯을 통해 받은 주문번호.")
|
@Schema(description = "결제 위젯을 통해 받은 주문번호.")
|
||||||
val orderId: String,
|
val orderId: String,
|
||||||
|
|
||||||
@Schema(description = "결제 위젯을 통해 받은 결제 금액")
|
@Schema(description = "결제 위젯을 통해 받은 결제 금액")
|
||||||
val amount: Long,
|
val amount: Long,
|
||||||
|
|
||||||
@Schema(description = "결제 타입", example = "NORMAL")
|
@Schema(description = "결제 타입", example = "NORMAL")
|
||||||
val paymentType: String
|
val paymentType: String
|
||||||
)
|
)
|
||||||
|
|
||||||
fun ReservationCreateWithPaymentRequest.toPaymentApproveRequest(): PaymentApproveRequest = PaymentApproveRequest(
|
fun ReservationCreateWithPaymentRequest.toPaymentApproveRequest(): PaymentApproveRequest = PaymentApproveRequest(
|
||||||
paymentKey, orderId, amount, paymentType
|
paymentKey, orderId, amount, paymentType
|
||||||
)
|
)
|
||||||
|
|
||||||
data class WaitingCreateRequest(
|
data class WaitingCreateRequest(
|
||||||
val date: LocalDate,
|
val date: LocalDate,
|
||||||
val timeId: Long,
|
val timeId: Long,
|
||||||
val themeId: Long
|
val themeId: Long
|
||||||
)
|
)
|
||||||
|
|||||||
@ -14,49 +14,49 @@ import java.time.LocalDate
|
|||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
|
|
||||||
data class MyReservationRetrieveResponse(
|
data class MyReservationRetrieveResponse(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val themeName: String,
|
val themeName: String,
|
||||||
val date: LocalDate,
|
val date: LocalDate,
|
||||||
val time: LocalTime,
|
val time: LocalTime,
|
||||||
val status: ReservationStatus,
|
val status: ReservationStatus,
|
||||||
@Schema(description = "대기 순번. 확정된 예약은 0의 값을 가집니다.")
|
@Schema(description = "대기 순번. 확정된 예약은 0의 값을 가집니다.")
|
||||||
val rank: Long,
|
val rank: Long,
|
||||||
@Schema(description = "결제 키. 결제가 완료된 예약에만 값이 존재합니다.")
|
@Schema(description = "결제 키. 결제가 완료된 예약에만 값이 존재합니다.")
|
||||||
val paymentKey: String?,
|
val paymentKey: String?,
|
||||||
@Schema(description = "결제 금액. 결제가 완료된 예약에만 값이 존재합니다.")
|
@Schema(description = "결제 금액. 결제가 완료된 예약에만 값이 존재합니다.")
|
||||||
val amount: Long?
|
val amount: Long?
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MyReservationRetrieveListResponse(
|
data class MyReservationRetrieveListResponse(
|
||||||
@Schema(description = "현재 로그인한 회원의 예약 및 대기 목록")
|
@Schema(description = "현재 로그인한 회원의 예약 및 대기 목록")
|
||||||
val reservations: List<MyReservationRetrieveResponse>
|
val reservations: List<MyReservationRetrieveResponse>
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ReservationRetrieveResponse(
|
data class ReservationRetrieveResponse(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val date: LocalDate,
|
val date: LocalDate,
|
||||||
|
|
||||||
@field:JsonProperty("member")
|
@field:JsonProperty("member")
|
||||||
val member: MemberRetrieveResponse,
|
val member: MemberRetrieveResponse,
|
||||||
|
|
||||||
@field:JsonProperty("time")
|
@field:JsonProperty("time")
|
||||||
val time: TimeCreateResponse,
|
val time: TimeCreateResponse,
|
||||||
|
|
||||||
@field:JsonProperty("theme")
|
@field:JsonProperty("theme")
|
||||||
val theme: ThemeRetrieveResponse,
|
val theme: ThemeRetrieveResponse,
|
||||||
|
|
||||||
val status: ReservationStatus
|
val status: ReservationStatus
|
||||||
)
|
)
|
||||||
|
|
||||||
fun ReservationEntity.toRetrieveResponse(): ReservationRetrieveResponse = ReservationRetrieveResponse(
|
fun ReservationEntity.toRetrieveResponse(): ReservationRetrieveResponse = ReservationRetrieveResponse(
|
||||||
id = this.id!!,
|
id = this.id!!,
|
||||||
date = this.date,
|
date = this.date,
|
||||||
member = this.member.toRetrieveResponse(),
|
member = this.member.toRetrieveResponse(),
|
||||||
time = this.time.toCreateResponse(),
|
time = this.time.toCreateResponse(),
|
||||||
theme = this.theme.toResponse(),
|
theme = this.theme.toResponse(),
|
||||||
status = this.reservationStatus
|
status = this.reservationStatus
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ReservationRetrieveListResponse(
|
data class ReservationRetrieveListResponse(
|
||||||
val reservations: List<ReservationRetrieveResponse>
|
val reservations: List<ReservationRetrieveResponse>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package roomescape.theme.business
|
package roomescape.theme.business
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
@ -10,44 +11,71 @@ import roomescape.theme.infrastructure.persistence.ThemeRepository
|
|||||||
import roomescape.theme.web.*
|
import roomescape.theme.web.*
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
private val log = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class ThemeService(
|
class ThemeService(
|
||||||
private val themeRepository: ThemeRepository
|
private val themeRepository: ThemeRepository,
|
||||||
) {
|
) {
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun findById(id: Long): ThemeEntity = themeRepository.findByIdOrNull(id)
|
fun findById(id: Long): ThemeEntity {
|
||||||
?: throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
|
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)
|
@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()
|
.toResponse()
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun findMostReservedThemes(count: Int): ThemeRetrieveListResponse {
|
fun findMostReservedThemes(count: Int): ThemeRetrieveListResponse {
|
||||||
|
log.debug { "[ThemeService.findMostReservedThemes] 인기 테마 조회 시작: count=$count" }
|
||||||
|
|
||||||
val today = LocalDate.now()
|
val today = LocalDate.now()
|
||||||
val startDate = today.minusDays(7)
|
val startDate = today.minusDays(7)
|
||||||
val endDate = today.minusDays(1)
|
val endDate = today.minusDays(1)
|
||||||
|
|
||||||
return themeRepository.findPopularThemes(startDate, endDate, count)
|
return themeRepository.findPopularThemes(startDate, endDate, count)
|
||||||
.toResponse()
|
.also { log.info { "[ThemeService.findMostReservedThemes] ${it.size} 개의 인기 테마 조회 완료" } }
|
||||||
|
.toResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun createTheme(request: ThemeCreateRequest): ThemeRetrieveResponse {
|
fun createTheme(request: ThemeCreateRequest): ThemeRetrieveResponse {
|
||||||
|
log.debug { "[ThemeService.createTheme] 테마 생성 시작: name=${request.name}" }
|
||||||
|
|
||||||
if (themeRepository.existsByName(request.name)) {
|
if (themeRepository.existsByName(request.name)) {
|
||||||
|
log.info { "[ThemeService.createTheme] 테마 생성 실패(이름 중복): name=${request.name}" }
|
||||||
throw ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED)
|
throw ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED)
|
||||||
}
|
}
|
||||||
|
|
||||||
val theme: ThemeEntity = request.toEntity()
|
val theme: ThemeEntity = request.toEntity()
|
||||||
return themeRepository.save(theme).toResponse()
|
return themeRepository.save(theme)
|
||||||
|
.also { log.info { "[ThemeService.createTheme] 테마 생성 완료: themeId=${it.id}" } }
|
||||||
|
.toResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun deleteTheme(id: Long) {
|
fun deleteTheme(id: Long) {
|
||||||
|
log.debug { "[ThemeService.deleteTheme] 테마 삭제 시작: themeId=$id" }
|
||||||
|
|
||||||
if (themeRepository.isReservedTheme(id)) {
|
if (themeRepository.isReservedTheme(id)) {
|
||||||
|
log.info { "[ThemeService.deleteTheme] 테마 삭제 실패(예약이 있는 테마): themeId=$id" }
|
||||||
throw ThemeException(ThemeErrorCode.THEME_ALREADY_RESERVED)
|
throw ThemeException(ThemeErrorCode.THEME_ALREADY_RESERVED)
|
||||||
}
|
}
|
||||||
|
|
||||||
themeRepository.deleteById(id)
|
themeRepository.deleteById(id)
|
||||||
|
.also { log.info { "[ThemeService.deleteTheme] 테마 삭제 완료: themeId=$id" } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,24 +28,24 @@ interface ThemeAPI {
|
|||||||
@Operation(summary = "가장 많이 예약된 테마 조회")
|
@Operation(summary = "가장 많이 예약된 테마 조회")
|
||||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||||
fun findMostReservedThemes(
|
fun findMostReservedThemes(
|
||||||
@RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int
|
@RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int
|
||||||
): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>>
|
): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>>
|
||||||
|
|
||||||
@Admin
|
@Admin
|
||||||
@Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"])
|
@Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"])
|
||||||
@ApiResponses(
|
@ApiResponses(
|
||||||
ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true),
|
ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true),
|
||||||
)
|
)
|
||||||
fun createTheme(
|
fun createTheme(
|
||||||
@Valid @RequestBody request: ThemeCreateRequest,
|
@Valid @RequestBody request: ThemeCreateRequest,
|
||||||
): ResponseEntity<CommonApiResponse<ThemeRetrieveResponse>>
|
): ResponseEntity<CommonApiResponse<ThemeRetrieveResponse>>
|
||||||
|
|
||||||
@Admin
|
@Admin
|
||||||
@Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"])
|
@Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"])
|
||||||
@ApiResponses(
|
@ApiResponses(
|
||||||
ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true),
|
ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true),
|
||||||
)
|
)
|
||||||
fun deleteTheme(
|
fun deleteTheme(
|
||||||
@PathVariable id: Long
|
@PathVariable id: Long
|
||||||
): ResponseEntity<CommonApiResponse<Unit>>
|
): ResponseEntity<CommonApiResponse<Unit>>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import org.springframework.http.HttpStatus
|
|||||||
import roomescape.common.exception.ErrorCode
|
import roomescape.common.exception.ErrorCode
|
||||||
|
|
||||||
enum class ThemeErrorCode(
|
enum class ThemeErrorCode(
|
||||||
override val httpStatus: HttpStatus,
|
override val httpStatus: HttpStatus,
|
||||||
override val errorCode: String,
|
override val errorCode: String,
|
||||||
override val message: String
|
override val message: String
|
||||||
) : ErrorCode {
|
) : ErrorCode {
|
||||||
THEME_NOT_FOUND(HttpStatus.NOT_FOUND, "TH001", "테마를 찾을 수 없어요."),
|
THEME_NOT_FOUND(HttpStatus.NOT_FOUND, "TH001", "테마를 찾을 수 없어요."),
|
||||||
THEME_NAME_DUPLICATED(HttpStatus.BAD_REQUEST, "TH002", "이미 같은 이름의 테마가 있어요."),
|
THEME_NAME_DUPLICATED(HttpStatus.BAD_REQUEST, "TH002", "이미 같은 이름의 테마가 있어요."),
|
||||||
|
|||||||
@ -3,6 +3,6 @@ package roomescape.theme.exception
|
|||||||
import roomescape.common.exception.RoomescapeException
|
import roomescape.common.exception.RoomescapeException
|
||||||
|
|
||||||
class ThemeException(
|
class ThemeException(
|
||||||
override val errorCode: ThemeErrorCode,
|
override val errorCode: ThemeErrorCode,
|
||||||
override val message: String = errorCode.message
|
override val message: String = errorCode.message
|
||||||
) : RoomescapeException(errorCode, message)
|
) : RoomescapeException(errorCode, message)
|
||||||
|
|||||||
@ -5,11 +5,11 @@ import jakarta.persistence.*
|
|||||||
@Entity
|
@Entity
|
||||||
@Table(name = "themes")
|
@Table(name = "themes")
|
||||||
class ThemeEntity(
|
class ThemeEntity(
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
var id: Long? = null,
|
var id: Long? = null,
|
||||||
|
|
||||||
var name: String,
|
var name: String,
|
||||||
var description: String,
|
var description: String,
|
||||||
var thumbnail: String
|
var thumbnail: String
|
||||||
)
|
)
|
||||||
@ -6,7 +6,8 @@ import java.time.LocalDate
|
|||||||
|
|
||||||
interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
|
interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
|
||||||
|
|
||||||
@Query(value = """
|
@Query(
|
||||||
|
value = """
|
||||||
SELECT t
|
SELECT t
|
||||||
FROM ThemeEntity t
|
FROM ThemeEntity t
|
||||||
RIGHT JOIN ReservationEntity r ON t.id = r.theme.id
|
RIGHT JOIN ReservationEntity r ON t.id = r.theme.id
|
||||||
@ -20,12 +21,14 @@ interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
|
|||||||
|
|
||||||
fun existsByName(name: String): Boolean
|
fun existsByName(name: String): Boolean
|
||||||
|
|
||||||
@Query(value = """
|
@Query(
|
||||||
|
value = """
|
||||||
SELECT EXISTS(
|
SELECT EXISTS(
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM ReservationEntity r
|
FROM ReservationEntity r
|
||||||
WHERE r.theme.id = :id
|
WHERE r.theme.id = :id
|
||||||
)
|
)
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
fun isReservedTheme(id: Long): Boolean
|
fun isReservedTheme(id: Long): Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import java.net.URI
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
class ThemeController(
|
class ThemeController(
|
||||||
private val themeService: ThemeService
|
private val themeService: ThemeService
|
||||||
) : ThemeAPI {
|
) : ThemeAPI {
|
||||||
|
|
||||||
@GetMapping("/themes")
|
@GetMapping("/themes")
|
||||||
@ -23,7 +23,7 @@ class ThemeController(
|
|||||||
|
|
||||||
@GetMapping("/themes/most-reserved-last-week")
|
@GetMapping("/themes/most-reserved-last-week")
|
||||||
override fun findMostReservedThemes(
|
override fun findMostReservedThemes(
|
||||||
@RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int
|
@RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int
|
||||||
): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>> {
|
): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>> {
|
||||||
val response: ThemeRetrieveListResponse = themeService.findMostReservedThemes(count)
|
val response: ThemeRetrieveListResponse = themeService.findMostReservedThemes(count)
|
||||||
|
|
||||||
@ -32,17 +32,17 @@ class ThemeController(
|
|||||||
|
|
||||||
@PostMapping("/themes")
|
@PostMapping("/themes")
|
||||||
override fun createTheme(
|
override fun createTheme(
|
||||||
@RequestBody @Valid request: ThemeCreateRequest
|
@RequestBody @Valid request: ThemeCreateRequest
|
||||||
): ResponseEntity<CommonApiResponse<ThemeRetrieveResponse>> {
|
): ResponseEntity<CommonApiResponse<ThemeRetrieveResponse>> {
|
||||||
val themeResponse: ThemeRetrieveResponse = themeService.createTheme(request)
|
val themeResponse: ThemeRetrieveResponse = themeService.createTheme(request)
|
||||||
|
|
||||||
return ResponseEntity.created(URI.create("/themes/${themeResponse.id}"))
|
return ResponseEntity.created(URI.create("/themes/${themeResponse.id}"))
|
||||||
.body(CommonApiResponse(themeResponse))
|
.body(CommonApiResponse(themeResponse))
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/themes/{id}")
|
@DeleteMapping("/themes/{id}")
|
||||||
override fun deleteTheme(
|
override fun deleteTheme(
|
||||||
@PathVariable id: Long
|
@PathVariable id: Long
|
||||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||||
themeService.deleteTheme(id)
|
themeService.deleteTheme(id)
|
||||||
|
|
||||||
|
|||||||
@ -7,45 +7,45 @@ import org.hibernate.validator.constraints.URL
|
|||||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||||
|
|
||||||
data class ThemeCreateRequest(
|
data class ThemeCreateRequest(
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Size(max = 20)
|
@Size(max = 20)
|
||||||
val name: String,
|
val name: String,
|
||||||
|
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Size(max = 100)
|
@Size(max = 100)
|
||||||
val description: String,
|
val description: String,
|
||||||
|
|
||||||
@URL
|
@URL
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Schema(description = "썸네일 이미지 주소(URL).")
|
@Schema(description = "썸네일 이미지 주소(URL).")
|
||||||
val thumbnail: String
|
val thumbnail: String
|
||||||
)
|
)
|
||||||
|
|
||||||
fun ThemeCreateRequest.toEntity(): ThemeEntity = ThemeEntity(
|
fun ThemeCreateRequest.toEntity(): ThemeEntity = ThemeEntity(
|
||||||
name = this.name,
|
name = this.name,
|
||||||
description = this.description,
|
description = this.description,
|
||||||
thumbnail = this.thumbnail
|
thumbnail = this.thumbnail
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ThemeRetrieveResponse(
|
data class ThemeRetrieveResponse(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
@Schema(description = "썸네일 이미지 주소(URL).")
|
@Schema(description = "썸네일 이미지 주소(URL).")
|
||||||
val thumbnail: String
|
val thumbnail: String
|
||||||
)
|
)
|
||||||
|
|
||||||
fun ThemeEntity.toResponse(): ThemeRetrieveResponse = ThemeRetrieveResponse(
|
fun ThemeEntity.toResponse(): ThemeRetrieveResponse = ThemeRetrieveResponse(
|
||||||
id = this.id!!,
|
id = this.id!!,
|
||||||
name = this.name,
|
name = this.name,
|
||||||
description = this.description,
|
description = this.description,
|
||||||
thumbnail = this.thumbnail
|
thumbnail = this.thumbnail
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ThemeRetrieveListResponse(
|
data class ThemeRetrieveListResponse(
|
||||||
val themes: List<ThemeRetrieveResponse>
|
val themes: List<ThemeRetrieveResponse>
|
||||||
)
|
)
|
||||||
|
|
||||||
fun List<ThemeEntity>.toResponse(): ThemeRetrieveListResponse = ThemeRetrieveListResponse(
|
fun List<ThemeEntity>.toResponse(): ThemeRetrieveListResponse = ThemeRetrieveListResponse(
|
||||||
themes = this.map { it.toResponse() }
|
themes = this.map { it.toResponse() }
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package roomescape.time.business
|
package roomescape.time.business
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
@ -13,50 +14,87 @@ import roomescape.time.web.*
|
|||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
private val log = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class TimeService(
|
class TimeService(
|
||||||
private val timeRepository: TimeRepository,
|
private val timeRepository: TimeRepository,
|
||||||
private val reservationRepository: ReservationRepository
|
private val reservationRepository: ReservationRepository,
|
||||||
) {
|
) {
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun findById(id: Long): TimeEntity = timeRepository.findByIdOrNull(id)
|
fun findById(id: Long): TimeEntity {
|
||||||
?: throw TimeException(TimeErrorCode.TIME_NOT_FOUND)
|
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)
|
@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()
|
.toResponse()
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun createTime(request: TimeCreateRequest): TimeCreateResponse {
|
fun createTime(request: TimeCreateRequest): TimeCreateResponse {
|
||||||
|
log.debug { "[TimeService.createTime] 시간 생성 시작: startAt=${request.startAt}" }
|
||||||
|
|
||||||
val startAt: LocalTime = request.startAt
|
val startAt: LocalTime = request.startAt
|
||||||
if (timeRepository.existsByStartAt(startAt)) {
|
if (timeRepository.existsByStartAt(startAt)) {
|
||||||
|
log.info { "[TimeService.createTime] 시간 생성 실패(시간 중복): startAt=$startAt" }
|
||||||
throw TimeException(TimeErrorCode.TIME_DUPLICATED)
|
throw TimeException(TimeErrorCode.TIME_DUPLICATED)
|
||||||
}
|
}
|
||||||
|
|
||||||
val time: TimeEntity = request.toEntity()
|
val time: TimeEntity = request.toEntity()
|
||||||
|
return timeRepository.save(time)
|
||||||
return timeRepository.save(time).toCreateResponse()
|
.also { log.info { "[TimeService.createTime] 시간 생성 완료: timeId=${it.id}" } }
|
||||||
|
.toCreateResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun deleteTime(id: Long) {
|
fun deleteTime(id: Long) {
|
||||||
|
log.debug { "[TimeService.deleteTime] 시간 삭제 시작: timeId=$id" }
|
||||||
|
|
||||||
val time: TimeEntity = findById(id)
|
val time: TimeEntity = findById(id)
|
||||||
|
|
||||||
|
log.debug { "[TimeService.deleteTime] 시간이 ${time.startAt}인 모든 예약 조회 시작" }
|
||||||
val reservations: List<ReservationEntity> = reservationRepository.findAllByTime(time)
|
val reservations: List<ReservationEntity> = reservationRepository.findAllByTime(time)
|
||||||
|
log.debug { "[TimeService.deleteTime] 시간이 ${time.startAt}인 모든 ${reservations.size} 개의 예약 조회 완료" }
|
||||||
|
|
||||||
if (reservations.isNotEmpty()) {
|
if (reservations.isNotEmpty()) {
|
||||||
|
log.info { "[TimeService.deleteTime] 시간 삭제 실패(예약이 있는 시간): timeId=$id" }
|
||||||
throw TimeException(TimeErrorCode.TIME_ALREADY_RESERVED)
|
throw TimeException(TimeErrorCode.TIME_ALREADY_RESERVED)
|
||||||
}
|
}
|
||||||
|
|
||||||
timeRepository.delete(time)
|
timeRepository.delete(time)
|
||||||
|
.also { log.info { "[TimeService.deleteTime] 시간 삭제 완료: timeId=$id" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun findTimesWithAvailability(date: LocalDate, themeId: Long): TimeWithAvailabilityListResponse {
|
fun findTimesWithAvailability(date: LocalDate, themeId: Long): TimeWithAvailabilityListResponse {
|
||||||
|
log.debug { "[TimeService.findTimesWithAvailability] 예약 가능 시간 조회 시작: date=$date, themeId=$themeId" }
|
||||||
|
|
||||||
|
log.debug { "[TimeService.findTimesWithAvailability] 모든 시간 조회 " }
|
||||||
val allTimes = timeRepository.findAll()
|
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)
|
val reservations: List<ReservationEntity> = reservationRepository.findByDateAndThemeId(date, themeId)
|
||||||
|
log.debug { "[TimeService.findTimesWithAvailability] date=$date, themeId=$themeId 인 ${reservations.size} 개의 예약 조회 완료" }
|
||||||
|
|
||||||
|
|
||||||
return TimeWithAvailabilityListResponse(allTimes.map { time ->
|
return TimeWithAvailabilityListResponse(allTimes.map { time ->
|
||||||
val isAvailable: Boolean = reservations.none { reservation -> reservation.time.id == time.id }
|
val isAvailable: Boolean = reservations.none { reservation -> reservation.time.id == time.id }
|
||||||
TimeWithAvailabilityResponse(time.id!!, time.startAt, isAvailable)
|
TimeWithAvailabilityResponse(time.id!!, time.startAt, isAvailable)
|
||||||
})
|
}).also {
|
||||||
|
log.info {
|
||||||
|
"[TimeService.findTimesWithAvailability] date=$date, themeId=$themeId 에 대한 예약 가능 여부가 담긴 모든 시간 조회 완료"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,21 +30,21 @@ interface TimeAPI {
|
|||||||
@Operation(summary = "시간 추가", tags = ["관리자 로그인이 필요한 API"])
|
@Operation(summary = "시간 추가", tags = ["관리자 로그인이 필요한 API"])
|
||||||
@ApiResponses(ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true))
|
@ApiResponses(ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true))
|
||||||
fun createTime(
|
fun createTime(
|
||||||
@Valid @RequestBody timeCreateRequest: TimeCreateRequest,
|
@Valid @RequestBody timeCreateRequest: TimeCreateRequest,
|
||||||
): ResponseEntity<CommonApiResponse<TimeCreateResponse>>
|
): ResponseEntity<CommonApiResponse<TimeCreateResponse>>
|
||||||
|
|
||||||
@Admin
|
@Admin
|
||||||
@Operation(summary = "시간 삭제", tags = ["관리자 로그인이 필요한 API"])
|
@Operation(summary = "시간 삭제", tags = ["관리자 로그인이 필요한 API"])
|
||||||
@ApiResponses(ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true))
|
@ApiResponses(ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true))
|
||||||
fun deleteTime(
|
fun deleteTime(
|
||||||
@PathVariable id: Long
|
@PathVariable id: Long
|
||||||
): ResponseEntity<CommonApiResponse<Unit>>
|
): ResponseEntity<CommonApiResponse<Unit>>
|
||||||
|
|
||||||
@LoginRequired
|
@LoginRequired
|
||||||
@Operation(summary = "예약 가능 여부를 포함한 모든 시간 조회", tags = ["로그인이 필요한 API"])
|
@Operation(summary = "예약 가능 여부를 포함한 모든 시간 조회", tags = ["로그인이 필요한 API"])
|
||||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||||
fun findTimesWithAvailability(
|
fun findTimesWithAvailability(
|
||||||
@RequestParam date: LocalDate,
|
@RequestParam date: LocalDate,
|
||||||
@RequestParam themeId: Long
|
@RequestParam themeId: Long
|
||||||
): ResponseEntity<CommonApiResponse<TimeWithAvailabilityListResponse>>
|
): ResponseEntity<CommonApiResponse<TimeWithAvailabilityListResponse>>
|
||||||
}
|
}
|
||||||
@ -4,9 +4,9 @@ import org.springframework.http.HttpStatus
|
|||||||
import roomescape.common.exception.ErrorCode
|
import roomescape.common.exception.ErrorCode
|
||||||
|
|
||||||
enum class TimeErrorCode(
|
enum class TimeErrorCode(
|
||||||
override val httpStatus: HttpStatus,
|
override val httpStatus: HttpStatus,
|
||||||
override val errorCode: String,
|
override val errorCode: String,
|
||||||
override val message: String
|
override val message: String
|
||||||
) : ErrorCode {
|
) : ErrorCode {
|
||||||
TIME_NOT_FOUND(HttpStatus.NOT_FOUND, "TM001", "시간을 찾을 수 없어요."),
|
TIME_NOT_FOUND(HttpStatus.NOT_FOUND, "TM001", "시간을 찾을 수 없어요."),
|
||||||
TIME_DUPLICATED(HttpStatus.BAD_REQUEST, "TM002", "이미 같은 시간이 있어요."),
|
TIME_DUPLICATED(HttpStatus.BAD_REQUEST, "TM002", "이미 같은 시간이 있어요."),
|
||||||
|
|||||||
@ -4,6 +4,6 @@ import roomescape.common.exception.ErrorCode
|
|||||||
import roomescape.common.exception.RoomescapeException
|
import roomescape.common.exception.RoomescapeException
|
||||||
|
|
||||||
class TimeException(
|
class TimeException(
|
||||||
override val errorCode: ErrorCode,
|
override val errorCode: ErrorCode,
|
||||||
override val message: String = errorCode.message
|
override val message: String = errorCode.message
|
||||||
) : RoomescapeException(errorCode, message)
|
) : RoomescapeException(errorCode, message)
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import java.time.LocalTime
|
|||||||
@Entity
|
@Entity
|
||||||
@Table(name = "times")
|
@Table(name = "times")
|
||||||
class TimeEntity(
|
class TimeEntity(
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
var id: Long? = null,
|
var id: Long? = null,
|
||||||
var startAt: LocalTime
|
var startAt: LocalTime
|
||||||
)
|
)
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import java.time.LocalDate
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
class TimeController(
|
class TimeController(
|
||||||
private val timeService: TimeService
|
private val timeService: TimeService
|
||||||
) : TimeAPI {
|
) : TimeAPI {
|
||||||
|
|
||||||
@GetMapping("/times")
|
@GetMapping("/times")
|
||||||
@ -23,13 +23,13 @@ class TimeController(
|
|||||||
|
|
||||||
@PostMapping("/times")
|
@PostMapping("/times")
|
||||||
override fun createTime(
|
override fun createTime(
|
||||||
@Valid @RequestBody timeCreateRequest: TimeCreateRequest,
|
@Valid @RequestBody timeCreateRequest: TimeCreateRequest,
|
||||||
): ResponseEntity<CommonApiResponse<TimeCreateResponse>> {
|
): ResponseEntity<CommonApiResponse<TimeCreateResponse>> {
|
||||||
val response: TimeCreateResponse = timeService.createTime(timeCreateRequest)
|
val response: TimeCreateResponse = timeService.createTime(timeCreateRequest)
|
||||||
|
|
||||||
return ResponseEntity
|
return ResponseEntity
|
||||||
.created(URI.create("/times/${response.id}"))
|
.created(URI.create("/times/${response.id}"))
|
||||||
.body(CommonApiResponse(response))
|
.body(CommonApiResponse(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/times/{id}")
|
@DeleteMapping("/times/{id}")
|
||||||
@ -41,8 +41,8 @@ class TimeController(
|
|||||||
|
|
||||||
@GetMapping("/times/search")
|
@GetMapping("/times/search")
|
||||||
override fun findTimesWithAvailability(
|
override fun findTimesWithAvailability(
|
||||||
@RequestParam date: LocalDate,
|
@RequestParam date: LocalDate,
|
||||||
@RequestParam themeId: Long
|
@RequestParam themeId: Long
|
||||||
): ResponseEntity<CommonApiResponse<TimeWithAvailabilityListResponse>> {
|
): ResponseEntity<CommonApiResponse<TimeWithAvailabilityListResponse>> {
|
||||||
val response: TimeWithAvailabilityListResponse = timeService.findTimesWithAvailability(date, themeId)
|
val response: TimeWithAvailabilityListResponse = timeService.findTimesWithAvailability(date, themeId)
|
||||||
|
|
||||||
|
|||||||
@ -6,52 +6,52 @@ import java.time.LocalTime
|
|||||||
|
|
||||||
@Schema(name = "예약 시간 저장 요청", description = "예약 시간 저장 요청시 사용됩니다.")
|
@Schema(name = "예약 시간 저장 요청", description = "예약 시간 저장 요청시 사용됩니다.")
|
||||||
data class TimeCreateRequest(
|
data class TimeCreateRequest(
|
||||||
@Schema(description = "시간", type = "string", example = "09:00")
|
@Schema(description = "시간", type = "string", example = "09:00")
|
||||||
val startAt: LocalTime
|
val startAt: LocalTime
|
||||||
)
|
)
|
||||||
|
|
||||||
fun TimeCreateRequest.toEntity(): TimeEntity = TimeEntity(startAt = this.startAt)
|
fun TimeCreateRequest.toEntity(): TimeEntity = TimeEntity(startAt = this.startAt)
|
||||||
|
|
||||||
@Schema(name = "예약 시간 정보", description = "예약 시간 추가 및 조회 응답시 사용됩니다.")
|
@Schema(name = "예약 시간 정보", description = "예약 시간 추가 및 조회 응답시 사용됩니다.")
|
||||||
data class TimeCreateResponse(
|
data class TimeCreateResponse(
|
||||||
@Schema(description = "시간 식별자")
|
@Schema(description = "시간 식별자")
|
||||||
val id: Long,
|
val id: Long,
|
||||||
|
|
||||||
@Schema(description = "시간")
|
@Schema(description = "시간")
|
||||||
val startAt: LocalTime
|
val startAt: LocalTime
|
||||||
)
|
)
|
||||||
|
|
||||||
fun TimeEntity.toCreateResponse(): TimeCreateResponse = TimeCreateResponse(this.id!!, this.startAt)
|
fun TimeEntity.toCreateResponse(): TimeCreateResponse = TimeCreateResponse(this.id!!, this.startAt)
|
||||||
|
|
||||||
data class TimeRetrieveResponse(
|
data class TimeRetrieveResponse(
|
||||||
@Schema(description = "시간 식별자.")
|
@Schema(description = "시간 식별자.")
|
||||||
val id: Long,
|
val id: Long,
|
||||||
|
|
||||||
@Schema(description = "시간")
|
@Schema(description = "시간")
|
||||||
val startAt: LocalTime
|
val startAt: LocalTime
|
||||||
)
|
)
|
||||||
|
|
||||||
fun TimeEntity.toResponse(): TimeRetrieveResponse = TimeRetrieveResponse(this.id!!, this.startAt)
|
fun TimeEntity.toResponse(): TimeRetrieveResponse = TimeRetrieveResponse(this.id!!, this.startAt)
|
||||||
|
|
||||||
data class TimeRetrieveListResponse(
|
data class TimeRetrieveListResponse(
|
||||||
val times: List<TimeRetrieveResponse>
|
val times: List<TimeRetrieveResponse>
|
||||||
)
|
)
|
||||||
|
|
||||||
fun List<TimeEntity>.toResponse(): TimeRetrieveListResponse = TimeRetrieveListResponse(
|
fun List<TimeEntity>.toResponse(): TimeRetrieveListResponse = TimeRetrieveListResponse(
|
||||||
this.map { it.toResponse() }
|
this.map { it.toResponse() }
|
||||||
)
|
)
|
||||||
|
|
||||||
data class TimeWithAvailabilityResponse(
|
data class TimeWithAvailabilityResponse(
|
||||||
@Schema(description = "시간 식별자")
|
@Schema(description = "시간 식별자")
|
||||||
val id: Long,
|
val id: Long,
|
||||||
|
|
||||||
@Schema(description = "시간")
|
@Schema(description = "시간")
|
||||||
val startAt: LocalTime,
|
val startAt: LocalTime,
|
||||||
|
|
||||||
@Schema(description = "예약 가능 여부")
|
@Schema(description = "예약 가능 여부")
|
||||||
val isAvailable: Boolean
|
val isAvailable: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
data class TimeWithAvailabilityListResponse(
|
data class TimeWithAvailabilityListResponse(
|
||||||
val times: List<TimeWithAvailabilityResponse>
|
val times: List<TimeWithAvailabilityResponse>
|
||||||
)
|
)
|
||||||
|
|||||||
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:
|
spring:
|
||||||
|
profiles:
|
||||||
|
active: ${ACTIVE_PROFILE:local}
|
||||||
jpa:
|
jpa:
|
||||||
show-sql: false
|
open-in-view: 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:
|
payment:
|
||||||
api-base-url: https://api.tosspayments.com
|
api-base-url: https://api.tosspayments.com
|
||||||
confirm-secret-key: test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6
|
|
||||||
read-timeout: 3
|
|
||||||
connect-timeout: 30
|
|
||||||
|
|
||||||
springdoc:
|
springdoc:
|
||||||
swagger-ui:
|
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.AuthErrorCode
|
||||||
import roomescape.auth.exception.AuthException
|
import roomescape.auth.exception.AuthException
|
||||||
import roomescape.auth.infrastructure.jwt.JwtHandler
|
import roomescape.auth.infrastructure.jwt.JwtHandler
|
||||||
import roomescape.auth.service.AuthService
|
|
||||||
import roomescape.member.business.MemberService
|
import roomescape.member.business.MemberService
|
||||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||||
import roomescape.member.infrastructure.persistence.MemberRepository
|
import roomescape.member.infrastructure.persistence.MemberRepository
|
||||||
|
|||||||
@ -47,11 +47,11 @@ class JwtHandlerTest : FunSpec({
|
|||||||
test("시크릿 키가 잘못된 경우 예외를 던진다.") {
|
test("시크릿 키가 잘못된 경우 예외를 던진다.") {
|
||||||
val now = Date()
|
val now = Date()
|
||||||
val invalidSignatureToken: String = Jwts.builder()
|
val invalidSignatureToken: String = Jwts.builder()
|
||||||
.claim("memberId", memberId)
|
.claim("memberId", memberId)
|
||||||
.issuedAt(now)
|
.issuedAt(now)
|
||||||
.expiration(Date(now.time + JwtFixture.EXPIRATION_TIME))
|
.expiration(Date(now.time + JwtFixture.EXPIRATION_TIME))
|
||||||
.signWith(Keys.hmacShaKeyFor(JwtFixture.SECRET_KEY_STRING.substring(1).toByteArray()))
|
.signWith(Keys.hmacShaKeyFor(JwtFixture.SECRET_KEY_STRING.substring(1).toByteArray()))
|
||||||
.compact()
|
.compact()
|
||||||
|
|
||||||
shouldThrow<AuthException> {
|
shouldThrow<AuthException> {
|
||||||
jwtHandler.getMemberIdFromToken(invalidSignatureToken)
|
jwtHandler.getMemberIdFromToken(invalidSignatureToken)
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import org.hamcrest.Matchers.equalTo
|
|||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
|
import roomescape.auth.business.AuthService
|
||||||
import roomescape.auth.exception.AuthErrorCode
|
import roomescape.auth.exception.AuthErrorCode
|
||||||
import roomescape.auth.service.AuthService
|
|
||||||
import roomescape.common.exception.CommonErrorCode
|
import roomescape.common.exception.CommonErrorCode
|
||||||
import roomescape.common.exception.ErrorCode
|
import roomescape.common.exception.ErrorCode
|
||||||
import roomescape.util.MemberFixture
|
import roomescape.util.MemberFixture
|
||||||
@ -133,6 +133,10 @@ class AuthControllerTest(
|
|||||||
jwtHandler.getMemberIdFromToken(any())
|
jwtHandler.getMemberIdFromToken(any())
|
||||||
} returns 1L
|
} returns 1L
|
||||||
|
|
||||||
|
every {
|
||||||
|
memberRepository.findByIdOrNull(1L)
|
||||||
|
} returns MemberFixture.create(id = 1L)
|
||||||
|
|
||||||
Then("정상 응답한다.") {
|
Then("정상 응답한다.") {
|
||||||
runPostTest(
|
runPostTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import java.time.LocalDate
|
|||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
|
|
||||||
class JacksonConfigTest(
|
class JacksonConfigTest(
|
||||||
private val objectMapper: ObjectMapper = JacksonConfig().objectMapper()
|
private val objectMapper: ObjectMapper = JacksonConfig().objectMapper()
|
||||||
) : FunSpec({
|
) : FunSpec({
|
||||||
|
|
||||||
context("날짜는 yyyy-mm-dd 형식이다.") {
|
context("날짜는 yyyy-mm-dd 형식이다.") {
|
||||||
|
|||||||
@ -65,10 +65,10 @@ class PaymentServiceTest : FunSpec({
|
|||||||
every {
|
every {
|
||||||
canceledPaymentRepository.save(any())
|
canceledPaymentRepository.save(any())
|
||||||
} returns PaymentFixture.createCanceled(
|
} returns PaymentFixture.createCanceled(
|
||||||
id = 1L,
|
id = 1L,
|
||||||
paymentKey = paymentKey,
|
paymentKey = paymentKey,
|
||||||
cancelReason = "Test",
|
cancelReason = "Test",
|
||||||
cancelAmount = paymentEntity.totalAmount,
|
cancelAmount = paymentEntity.totalAmount,
|
||||||
)
|
)
|
||||||
|
|
||||||
val result: PaymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId)
|
val result: PaymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId)
|
||||||
@ -99,8 +99,8 @@ class PaymentServiceTest : FunSpec({
|
|||||||
|
|
||||||
test("paymentKey로 canceledPaymentEntity를 찾고, canceledAt을 업데이트한다.") {
|
test("paymentKey로 canceledPaymentEntity를 찾고, canceledAt을 업데이트한다.") {
|
||||||
val canceledPaymentEntity = PaymentFixture.createCanceled(
|
val canceledPaymentEntity = PaymentFixture.createCanceled(
|
||||||
paymentKey = paymentKey,
|
paymentKey = paymentKey,
|
||||||
canceledAt = canceledAt.minusMinutes(1)
|
canceledAt = canceledAt.minusMinutes(1)
|
||||||
)
|
)
|
||||||
|
|
||||||
every {
|
every {
|
||||||
|
|||||||
@ -11,17 +11,17 @@ import roomescape.payment.web.PaymentCancelResponse
|
|||||||
class PaymentCancelResponseDeserializerTest : StringSpec({
|
class PaymentCancelResponseDeserializerTest : StringSpec({
|
||||||
|
|
||||||
val objectMapper: ObjectMapper = jacksonObjectMapper().registerModule(
|
val objectMapper: ObjectMapper = jacksonObjectMapper().registerModule(
|
||||||
SimpleModule().addDeserializer(
|
SimpleModule().addDeserializer(
|
||||||
PaymentCancelResponse::class.java,
|
PaymentCancelResponse::class.java,
|
||||||
PaymentCancelResponseDeserializer()
|
PaymentCancelResponseDeserializer()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
"결제 취소 응답을 역직렬화하여 PaymentCancelResponse 객체를 생성한다" {
|
"결제 취소 응답을 역직렬화하여 PaymentCancelResponse 객체를 생성한다" {
|
||||||
val cancelResponseJson: String = SampleTossPaymentConst.cancelJson
|
val cancelResponseJson: String = SampleTossPaymentConst.cancelJson
|
||||||
val cancelResponse: PaymentCancelResponse = objectMapper.readValue(
|
val cancelResponse: PaymentCancelResponse = objectMapper.readValue(
|
||||||
cancelResponseJson,
|
cancelResponseJson,
|
||||||
PaymentCancelResponse::class.java
|
PaymentCancelResponse::class.java
|
||||||
)
|
)
|
||||||
|
|
||||||
assertSoftly(cancelResponse) {
|
assertSoftly(cancelResponse) {
|
||||||
|
|||||||
@ -15,10 +15,10 @@ object SampleTossPaymentConst {
|
|||||||
val cancelReason: String = "테스트 결제 취소"
|
val cancelReason: String = "테스트 결제 취소"
|
||||||
|
|
||||||
val paymentRequest: PaymentApproveRequest = PaymentApproveRequest(
|
val paymentRequest: PaymentApproveRequest = PaymentApproveRequest(
|
||||||
paymentKey,
|
paymentKey,
|
||||||
orderId,
|
orderId,
|
||||||
amount,
|
amount,
|
||||||
paymentType
|
paymentType
|
||||||
)
|
)
|
||||||
|
|
||||||
val paymentRequestJson: String = """
|
val paymentRequestJson: String = """
|
||||||
@ -31,9 +31,9 @@ object SampleTossPaymentConst {
|
|||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
val cancelRequest: PaymentCancelRequest = PaymentCancelRequest(
|
val cancelRequest: PaymentCancelRequest = PaymentCancelRequest(
|
||||||
paymentKey,
|
paymentKey,
|
||||||
amount,
|
amount,
|
||||||
cancelReason
|
cancelReason
|
||||||
)
|
)
|
||||||
|
|
||||||
val cancelRequestJson: String = """
|
val cancelRequestJson: String = """
|
||||||
|
|||||||
@ -21,8 +21,8 @@ import roomescape.payment.web.PaymentCancelResponse
|
|||||||
|
|
||||||
@RestClientTest(TossPaymentClient::class)
|
@RestClientTest(TossPaymentClient::class)
|
||||||
class TossPaymentClientTest(
|
class TossPaymentClientTest(
|
||||||
@Autowired val client: TossPaymentClient,
|
@Autowired val client: TossPaymentClient,
|
||||||
@Autowired val mockServer: MockRestServiceServer
|
@Autowired val mockServer: MockRestServiceServer
|
||||||
) : FunSpec() {
|
) : FunSpec() {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -40,9 +40,9 @@ class TossPaymentClientTest(
|
|||||||
test("성공 응답") {
|
test("성공 응답") {
|
||||||
commonAction().andRespond {
|
commonAction().andRespond {
|
||||||
withSuccess()
|
withSuccess()
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.body(SampleTossPaymentConst.confirmJson)
|
.body(SampleTossPaymentConst.confirmJson)
|
||||||
.createResponse(it)
|
.createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@ -60,9 +60,9 @@ class TossPaymentClientTest(
|
|||||||
fun runTest(httpStatus: HttpStatus, expectedError: PaymentErrorCode) {
|
fun runTest(httpStatus: HttpStatus, expectedError: PaymentErrorCode) {
|
||||||
commonAction().andRespond {
|
commonAction().andRespond {
|
||||||
withStatus(httpStatus)
|
withStatus(httpStatus)
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.body(SampleTossPaymentConst.tossPaymentErrorJson)
|
.body(SampleTossPaymentConst.tossPaymentErrorJson)
|
||||||
.createResponse(it)
|
.createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@ -99,9 +99,9 @@ class TossPaymentClientTest(
|
|||||||
test("성공 응답") {
|
test("성공 응답") {
|
||||||
commonAction().andRespond {
|
commonAction().andRespond {
|
||||||
withSuccess()
|
withSuccess()
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.body(SampleTossPaymentConst.cancelJson)
|
.body(SampleTossPaymentConst.cancelJson)
|
||||||
.createResponse(it)
|
.createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@ -119,9 +119,9 @@ class TossPaymentClientTest(
|
|||||||
fun runTest(httpStatus: HttpStatus, expectedError: PaymentErrorCode) {
|
fun runTest(httpStatus: HttpStatus, expectedError: PaymentErrorCode) {
|
||||||
commonAction().andRespond {
|
commonAction().andRespond {
|
||||||
withStatus(httpStatus)
|
withStatus(httpStatus)
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.body(SampleTossPaymentConst.tossPaymentErrorJson)
|
.body(SampleTossPaymentConst.tossPaymentErrorJson)
|
||||||
.createResponse(it)
|
.createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val cancelRequest: PaymentCancelRequest = SampleTossPaymentConst.cancelRequest
|
val cancelRequest: PaymentCancelRequest = SampleTossPaymentConst.cancelRequest
|
||||||
|
|||||||
@ -10,14 +10,14 @@ import java.util.*
|
|||||||
|
|
||||||
@DataJpaTest
|
@DataJpaTest
|
||||||
class CanceledPaymentRepositoryTest(
|
class CanceledPaymentRepositoryTest(
|
||||||
@Autowired val canceledPaymentRepository: CanceledPaymentRepository,
|
@Autowired val canceledPaymentRepository: CanceledPaymentRepository,
|
||||||
) : FunSpec() {
|
) : FunSpec() {
|
||||||
init {
|
init {
|
||||||
context("paymentKey로 CanceledPaymentEntity 조회") {
|
context("paymentKey로 CanceledPaymentEntity 조회") {
|
||||||
val paymentKey = "test-payment-key"
|
val paymentKey = "test-payment-key"
|
||||||
beforeTest {
|
beforeTest {
|
||||||
PaymentFixture.createCanceled(paymentKey = paymentKey)
|
PaymentFixture.createCanceled(paymentKey = paymentKey)
|
||||||
.also { canceledPaymentRepository.save(it) }
|
.also { canceledPaymentRepository.save(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
test("정상 반환") {
|
test("정상 반환") {
|
||||||
@ -30,7 +30,7 @@ class CanceledPaymentRepositoryTest(
|
|||||||
|
|
||||||
test("null 반환") {
|
test("null 반환") {
|
||||||
canceledPaymentRepository.findByPaymentKey(UUID.randomUUID().toString())
|
canceledPaymentRepository.findByPaymentKey(UUID.randomUUID().toString())
|
||||||
.also { it shouldBe null }
|
.also { it shouldBe null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import roomescape.util.ReservationFixture
|
|||||||
|
|
||||||
@DataJpaTest
|
@DataJpaTest
|
||||||
class PaymentRepositoryTest(
|
class PaymentRepositoryTest(
|
||||||
@Autowired val paymentRepository: PaymentRepository,
|
@Autowired val paymentRepository: PaymentRepository,
|
||||||
@Autowired val entityManager: EntityManager
|
@Autowired val entityManager: EntityManager
|
||||||
) : FunSpec() {
|
) : FunSpec() {
|
||||||
|
|
||||||
lateinit var reservation: ReservationEntity
|
lateinit var reservation: ReservationEntity
|
||||||
@ -23,17 +23,17 @@ class PaymentRepositoryTest(
|
|||||||
beforeTest {
|
beforeTest {
|
||||||
reservation = setupReservation()
|
reservation = setupReservation()
|
||||||
PaymentFixture.create(reservation = reservation)
|
PaymentFixture.create(reservation = reservation)
|
||||||
.also { paymentRepository.save(it) }
|
.also { paymentRepository.save(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
test("true") {
|
test("true") {
|
||||||
paymentRepository.existsByReservationId(reservation.id!!)
|
paymentRepository.existsByReservationId(reservation.id!!)
|
||||||
.also { it shouldBe true }
|
.also { it shouldBe true }
|
||||||
}
|
}
|
||||||
|
|
||||||
test("false") {
|
test("false") {
|
||||||
paymentRepository.existsByReservationId(reservation.id!! + 1L)
|
paymentRepository.existsByReservationId(reservation.id!! + 1L)
|
||||||
.also { it shouldBe false }
|
.also { it shouldBe false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,19 +43,19 @@ class PaymentRepositoryTest(
|
|||||||
beforeTest {
|
beforeTest {
|
||||||
reservation = setupReservation()
|
reservation = setupReservation()
|
||||||
paymentKey = PaymentFixture.create(reservation = reservation)
|
paymentKey = PaymentFixture.create(reservation = reservation)
|
||||||
.also { paymentRepository.save(it) }
|
.also { paymentRepository.save(it) }
|
||||||
.paymentKey
|
.paymentKey
|
||||||
}
|
}
|
||||||
|
|
||||||
test("정상 반환") {
|
test("정상 반환") {
|
||||||
paymentRepository.findPaymentKeyByReservationId(reservation.id!!)
|
paymentRepository.findPaymentKeyByReservationId(reservation.id!!)
|
||||||
?.let { it shouldBe paymentKey }
|
?.let { it shouldBe paymentKey }
|
||||||
?: throw AssertionError("Unexpected null value")
|
?: throw AssertionError("Unexpected null value")
|
||||||
}
|
}
|
||||||
|
|
||||||
test("null 반환") {
|
test("null 반환") {
|
||||||
paymentRepository.findPaymentKeyByReservationId(reservation.id!! + 1)
|
paymentRepository.findPaymentKeyByReservationId(reservation.id!! + 1)
|
||||||
.also { it shouldBe null }
|
.also { it shouldBe null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,27 +65,27 @@ class PaymentRepositoryTest(
|
|||||||
beforeTest {
|
beforeTest {
|
||||||
reservation = setupReservation()
|
reservation = setupReservation()
|
||||||
payment = PaymentFixture.create(reservation = reservation)
|
payment = PaymentFixture.create(reservation = reservation)
|
||||||
.also { paymentRepository.save(it) }
|
.also { paymentRepository.save(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
test("정상 반환") {
|
test("정상 반환") {
|
||||||
paymentRepository.findByPaymentKey(payment.paymentKey)
|
paymentRepository.findByPaymentKey(payment.paymentKey)
|
||||||
?.also {
|
?.also {
|
||||||
assertSoftly(it) {
|
assertSoftly(it) {
|
||||||
this.id shouldBe payment.id
|
this.id shouldBe payment.id
|
||||||
this.orderId shouldBe payment.orderId
|
this.orderId shouldBe payment.orderId
|
||||||
this.paymentKey shouldBe payment.paymentKey
|
this.paymentKey shouldBe payment.paymentKey
|
||||||
this.totalAmount shouldBe payment.totalAmount
|
this.totalAmount shouldBe payment.totalAmount
|
||||||
this.reservation.id shouldBe payment.reservation.id
|
this.reservation.id shouldBe payment.reservation.id
|
||||||
this.approvedAt shouldBe payment.approvedAt
|
this.approvedAt shouldBe payment.approvedAt
|
||||||
}
|
|
||||||
}
|
}
|
||||||
?: throw AssertionError("Unexpected null value")
|
}
|
||||||
|
?: throw AssertionError("Unexpected null value")
|
||||||
}
|
}
|
||||||
|
|
||||||
test("null 반환") {
|
test("null 반환") {
|
||||||
paymentRepository.findByPaymentKey("non-existent-key")
|
paymentRepository.findByPaymentKey("non-existent-key")
|
||||||
.also { it shouldBe null }
|
.also { it shouldBe null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,10 +27,10 @@ class ReservationServiceTest : FunSpec({
|
|||||||
val memberService: MemberService = mockk()
|
val memberService: MemberService = mockk()
|
||||||
val themeService: ThemeService = mockk()
|
val themeService: ThemeService = mockk()
|
||||||
val reservationService = ReservationService(
|
val reservationService = ReservationService(
|
||||||
reservationRepository,
|
reservationRepository,
|
||||||
timeService,
|
timeService,
|
||||||
memberService,
|
memberService,
|
||||||
themeService
|
themeService
|
||||||
)
|
)
|
||||||
|
|
||||||
context("예약을 추가할 때") {
|
context("예약을 추가할 때") {
|
||||||
@ -64,7 +64,7 @@ class ReservationServiceTest : FunSpec({
|
|||||||
|
|
||||||
test("지난 날짜이면 예외를 던진다.") {
|
test("지난 날짜이면 예외를 던진다.") {
|
||||||
val reservationRequest = ReservationFixture.createRequest().copy(
|
val reservationRequest = ReservationFixture.createRequest().copy(
|
||||||
date = LocalDate.now().minusDays(1)
|
date = LocalDate.now().minusDays(1)
|
||||||
)
|
)
|
||||||
|
|
||||||
every {
|
every {
|
||||||
@ -80,13 +80,13 @@ class ReservationServiceTest : FunSpec({
|
|||||||
|
|
||||||
test("지난 시간이면 예외를 던진다.") {
|
test("지난 시간이면 예외를 던진다.") {
|
||||||
val reservationRequest = ReservationFixture.createRequest().copy(
|
val reservationRequest = ReservationFixture.createRequest().copy(
|
||||||
date = LocalDate.now(),
|
date = LocalDate.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
every {
|
every {
|
||||||
timeService.findById(reservationRequest.timeId)
|
timeService.findById(reservationRequest.timeId)
|
||||||
} returns TimeFixture.create(
|
} returns TimeFixture.create(
|
||||||
startAt = LocalTime.now().minusMinutes(1)
|
startAt = LocalTime.now().minusMinutes(1)
|
||||||
)
|
)
|
||||||
|
|
||||||
shouldThrow<ReservationException> {
|
shouldThrow<ReservationException> {
|
||||||
@ -101,9 +101,9 @@ class ReservationServiceTest : FunSpec({
|
|||||||
context("예약 대기를 걸 때") {
|
context("예약 대기를 걸 때") {
|
||||||
test("이미 예약한 회원이 같은 날짜와 테마로 대기를 걸면 예외를 던진다.") {
|
test("이미 예약한 회원이 같은 날짜와 테마로 대기를 걸면 예외를 던진다.") {
|
||||||
val reservationRequest = ReservationFixture.createRequest().copy(
|
val reservationRequest = ReservationFixture.createRequest().copy(
|
||||||
date = LocalDate.now(),
|
date = LocalDate.now(),
|
||||||
themeId = 1L,
|
themeId = 1L,
|
||||||
timeId = 1L,
|
timeId = 1L,
|
||||||
)
|
)
|
||||||
|
|
||||||
every {
|
every {
|
||||||
@ -112,9 +112,9 @@ class ReservationServiceTest : FunSpec({
|
|||||||
|
|
||||||
shouldThrow<ReservationException> {
|
shouldThrow<ReservationException> {
|
||||||
val waitingRequest = ReservationFixture.createWaitingRequest(
|
val waitingRequest = ReservationFixture.createWaitingRequest(
|
||||||
date = reservationRequest.date,
|
date = reservationRequest.date,
|
||||||
themeId = reservationRequest.themeId,
|
themeId = reservationRequest.themeId,
|
||||||
timeId = reservationRequest.timeId
|
timeId = reservationRequest.timeId
|
||||||
)
|
)
|
||||||
reservationService.createWaiting(waitingRequest, 1L)
|
reservationService.createWaiting(waitingRequest, 1L)
|
||||||
}.also {
|
}.also {
|
||||||
@ -140,8 +140,8 @@ class ReservationServiceTest : FunSpec({
|
|||||||
|
|
||||||
test("대기중인 해당 예약이 이미 확정된 상태라면 예외를 던진다.") {
|
test("대기중인 해당 예약이 이미 확정된 상태라면 예외를 던진다.") {
|
||||||
val alreadyConfirmed = ReservationFixture.create(
|
val alreadyConfirmed = ReservationFixture.create(
|
||||||
id = reservationId,
|
id = reservationId,
|
||||||
status = ReservationStatus.CONFIRMED
|
status = ReservationStatus.CONFIRMED
|
||||||
)
|
)
|
||||||
every {
|
every {
|
||||||
reservationRepository.findByIdOrNull(reservationId)
|
reservationRepository.findByIdOrNull(reservationId)
|
||||||
@ -156,9 +156,9 @@ class ReservationServiceTest : FunSpec({
|
|||||||
|
|
||||||
test("타인의 대기를 취소하려고 하면 예외를 던진다.") {
|
test("타인의 대기를 취소하려고 하면 예외를 던진다.") {
|
||||||
val otherMembersWaiting = ReservationFixture.create(
|
val otherMembersWaiting = ReservationFixture.create(
|
||||||
id = reservationId,
|
id = reservationId,
|
||||||
member = MemberFixture.create(id = member.id!! + 1L),
|
member = MemberFixture.create(id = member.id!! + 1L),
|
||||||
status = ReservationStatus.WAITING
|
status = ReservationStatus.WAITING
|
||||||
)
|
)
|
||||||
|
|
||||||
every {
|
every {
|
||||||
@ -180,10 +180,10 @@ class ReservationServiceTest : FunSpec({
|
|||||||
|
|
||||||
shouldThrow<ReservationException> {
|
shouldThrow<ReservationException> {
|
||||||
reservationService.searchReservations(
|
reservationService.searchReservations(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
startFrom,
|
startFrom,
|
||||||
endAt
|
endAt
|
||||||
)
|
)
|
||||||
}.also {
|
}.also {
|
||||||
it.errorCode shouldBe ReservationErrorCode.INVALID_SEARCH_DATE_RANGE
|
it.errorCode shouldBe ReservationErrorCode.INVALID_SEARCH_DATE_RANGE
|
||||||
@ -263,8 +263,8 @@ class ReservationServiceTest : FunSpec({
|
|||||||
test("이미 확정된 예약이면 예외를 던진다.") {
|
test("이미 확정된 예약이면 예외를 던진다.") {
|
||||||
val member = MemberFixture.create(id = 1L, role = Role.ADMIN)
|
val member = MemberFixture.create(id = 1L, role = Role.ADMIN)
|
||||||
val reservation = ReservationFixture.create(
|
val reservation = ReservationFixture.create(
|
||||||
id = 1L,
|
id = 1L,
|
||||||
status = ReservationStatus.CONFIRMED
|
status = ReservationStatus.CONFIRMED
|
||||||
)
|
)
|
||||||
|
|
||||||
every {
|
every {
|
||||||
|
|||||||
@ -22,27 +22,27 @@ class ReservationWithPaymentServiceTest : FunSpec({
|
|||||||
val paymentService: PaymentService = mockk()
|
val paymentService: PaymentService = mockk()
|
||||||
|
|
||||||
val reservationWithPaymentService = ReservationWithPaymentService(
|
val reservationWithPaymentService = ReservationWithPaymentService(
|
||||||
reservationService = reservationService,
|
reservationService = reservationService,
|
||||||
paymentService = paymentService
|
paymentService = paymentService
|
||||||
)
|
)
|
||||||
|
|
||||||
val reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest = ReservationFixture.createRequest()
|
val reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest = ReservationFixture.createRequest()
|
||||||
val paymentApproveResponse = PaymentFixture.createApproveResponse()
|
val paymentApproveResponse = PaymentFixture.createApproveResponse()
|
||||||
val memberId = 1L
|
val memberId = 1L
|
||||||
val reservationEntity: ReservationEntity = ReservationFixture.create(
|
val reservationEntity: ReservationEntity = ReservationFixture.create(
|
||||||
id = 1L,
|
id = 1L,
|
||||||
date = reservationCreateWithPaymentRequest.date,
|
date = reservationCreateWithPaymentRequest.date,
|
||||||
time = TimeFixture.create(id = reservationCreateWithPaymentRequest.timeId),
|
time = TimeFixture.create(id = reservationCreateWithPaymentRequest.timeId),
|
||||||
theme = ThemeFixture.create(id = reservationCreateWithPaymentRequest.themeId),
|
theme = ThemeFixture.create(id = reservationCreateWithPaymentRequest.themeId),
|
||||||
member = MemberFixture.create(id = memberId),
|
member = MemberFixture.create(id = memberId),
|
||||||
status = ReservationStatus.CONFIRMED
|
status = ReservationStatus.CONFIRMED
|
||||||
)
|
)
|
||||||
val paymentEntity: PaymentEntity = PaymentFixture.create(
|
val paymentEntity: PaymentEntity = PaymentFixture.create(
|
||||||
id = 1L,
|
id = 1L,
|
||||||
orderId = reservationCreateWithPaymentRequest.orderId,
|
orderId = reservationCreateWithPaymentRequest.orderId,
|
||||||
paymentKey = reservationCreateWithPaymentRequest.paymentKey,
|
paymentKey = reservationCreateWithPaymentRequest.paymentKey,
|
||||||
totalAmount = reservationCreateWithPaymentRequest.amount,
|
totalAmount = reservationCreateWithPaymentRequest.amount,
|
||||||
reservation = reservationEntity,
|
reservation = reservationEntity,
|
||||||
)
|
)
|
||||||
|
|
||||||
context("addReservationWithPayment") {
|
context("addReservationWithPayment") {
|
||||||
@ -56,9 +56,9 @@ class ReservationWithPaymentServiceTest : FunSpec({
|
|||||||
} returns paymentEntity.toCreateResponse()
|
} returns paymentEntity.toCreateResponse()
|
||||||
|
|
||||||
val result: ReservationRetrieveResponse = reservationWithPaymentService.createReservationAndPayment(
|
val result: ReservationRetrieveResponse = reservationWithPaymentService.createReservationAndPayment(
|
||||||
request = reservationCreateWithPaymentRequest,
|
request = reservationCreateWithPaymentRequest,
|
||||||
paymentInfo = paymentApproveResponse,
|
paymentInfo = paymentApproveResponse,
|
||||||
memberId = memberId
|
memberId = memberId
|
||||||
)
|
)
|
||||||
|
|
||||||
assertSoftly(result) {
|
assertSoftly(result) {
|
||||||
@ -75,9 +75,9 @@ class ReservationWithPaymentServiceTest : FunSpec({
|
|||||||
context("removeReservationWithPayment") {
|
context("removeReservationWithPayment") {
|
||||||
test("예약 및 결제 정보를 삭제하고, 결제 취소 정보를 저장한다.") {
|
test("예약 및 결제 정보를 삭제하고, 결제 취소 정보를 저장한다.") {
|
||||||
val paymentCancelRequest: PaymentCancelRequest = PaymentFixture.createCancelRequest().copy(
|
val paymentCancelRequest: PaymentCancelRequest = PaymentFixture.createCancelRequest().copy(
|
||||||
paymentKey = paymentEntity.paymentKey,
|
paymentKey = paymentEntity.paymentKey,
|
||||||
amount = paymentEntity.totalAmount,
|
amount = paymentEntity.totalAmount,
|
||||||
cancelReason = "고객 요청"
|
cancelReason = "고객 요청"
|
||||||
)
|
)
|
||||||
|
|
||||||
every {
|
every {
|
||||||
@ -89,8 +89,8 @@ class ReservationWithPaymentServiceTest : FunSpec({
|
|||||||
} just Runs
|
} just Runs
|
||||||
|
|
||||||
val result: PaymentCancelRequest = reservationWithPaymentService.deleteReservationAndPayment(
|
val result: PaymentCancelRequest = reservationWithPaymentService.deleteReservationAndPayment(
|
||||||
reservationId = reservationEntity.id!!,
|
reservationId = reservationEntity.id!!,
|
||||||
memberId = reservationEntity.member.id!!
|
memberId = reservationEntity.member.id!!
|
||||||
)
|
)
|
||||||
|
|
||||||
result shouldBe paymentCancelRequest
|
result shouldBe paymentCancelRequest
|
||||||
|
|||||||
@ -17,8 +17,8 @@ import roomescape.util.TimeFixture
|
|||||||
|
|
||||||
@DataJpaTest
|
@DataJpaTest
|
||||||
class ReservationRepositoryTest(
|
class ReservationRepositoryTest(
|
||||||
val entityManager: EntityManager,
|
val entityManager: EntityManager,
|
||||||
val reservationRepository: ReservationRepository,
|
val reservationRepository: ReservationRepository,
|
||||||
) : FunSpec() {
|
) : FunSpec() {
|
||||||
init {
|
init {
|
||||||
context("findByTime") {
|
context("findByTime") {
|
||||||
@ -26,10 +26,12 @@ class ReservationRepositoryTest(
|
|||||||
|
|
||||||
beforeTest {
|
beforeTest {
|
||||||
listOf(
|
listOf(
|
||||||
ReservationFixture.create(time = time),
|
ReservationFixture.create(time = time),
|
||||||
ReservationFixture.create(time = TimeFixture.create(
|
ReservationFixture.create(
|
||||||
startAt = time.startAt.plusSeconds(1)
|
time = TimeFixture.create(
|
||||||
))
|
startAt = time.startAt.plusSeconds(1)
|
||||||
|
)
|
||||||
|
)
|
||||||
).forEach {
|
).forEach {
|
||||||
persistReservation(it)
|
persistReservation(it)
|
||||||
}
|
}
|
||||||
@ -64,9 +66,9 @@ class ReservationRepositoryTest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
ReservationFixture.create(date = date, theme = theme1),
|
ReservationFixture.create(date = date, theme = theme1),
|
||||||
ReservationFixture.create(date = date.plusDays(1), theme = theme1),
|
ReservationFixture.create(date = date.plusDays(1), theme = theme1),
|
||||||
ReservationFixture.create(date = date, theme = theme2),
|
ReservationFixture.create(date = date, theme = theme2),
|
||||||
).forEach {
|
).forEach {
|
||||||
entityManager.persist(it.time)
|
entityManager.persist(it.time)
|
||||||
entityManager.persist(it.member)
|
entityManager.persist(it.member)
|
||||||
@ -124,9 +126,10 @@ class ReservationRepositoryTest(
|
|||||||
persistReservation(it)
|
persistReservation(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmedPaymentRequired = ReservationFixture.create(status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED).also {
|
confirmedPaymentRequired =
|
||||||
persistReservation(it)
|
ReservationFixture.create(status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED).also {
|
||||||
}
|
persistReservation(it)
|
||||||
|
}
|
||||||
|
|
||||||
entityManager.flush()
|
entityManager.flush()
|
||||||
entityManager.clear()
|
entityManager.clear()
|
||||||
@ -134,7 +137,7 @@ class ReservationRepositoryTest(
|
|||||||
|
|
||||||
test("예약이 없으면 false를 반환한다.") {
|
test("예약이 없으면 false를 반환한다.") {
|
||||||
val maxId: Long = listOf(waiting, confirmed, confirmedPaymentRequired)
|
val maxId: Long = listOf(waiting, confirmed, confirmedPaymentRequired)
|
||||||
.maxOfOrNull { it.id ?: 0L } ?: 0L
|
.maxOfOrNull { it.id ?: 0L } ?: 0L
|
||||||
reservationRepository.isExistConfirmedReservation(maxId + 1L) shouldBe false
|
reservationRepository.isExistConfirmedReservation(maxId + 1L) shouldBe false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,14 +164,15 @@ class ReservationRepositoryTest(
|
|||||||
|
|
||||||
test("결제 정보를 포함한 회원의 예약 목록을 반환한다.") {
|
test("결제 정보를 포함한 회원의 예약 목록을 반환한다.") {
|
||||||
val payment: PaymentEntity = PaymentFixture.create(
|
val payment: PaymentEntity = PaymentFixture.create(
|
||||||
reservation = reservation
|
reservation = reservation
|
||||||
).also {
|
).also {
|
||||||
entityManager.persist(it)
|
entityManager.persist(it)
|
||||||
entityManager.flush()
|
entityManager.flush()
|
||||||
entityManager.clear()
|
entityManager.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
val result: List<MyReservationRetrieveResponse> = reservationRepository.findAllByMemberId(reservation.member.id!!)
|
val result: List<MyReservationRetrieveResponse> =
|
||||||
|
reservationRepository.findAllByMemberId(reservation.member.id!!)
|
||||||
|
|
||||||
result shouldHaveSize 1
|
result shouldHaveSize 1
|
||||||
assertSoftly(result.first()) {
|
assertSoftly(result.first()) {
|
||||||
@ -179,7 +183,8 @@ class ReservationRepositoryTest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
test("결제 정보가 없다면 paymentKey와 amount는 null로 반환한다.") {
|
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
|
result shouldHaveSize 1
|
||||||
assertSoftly(result.first()) {
|
assertSoftly(result.first()) {
|
||||||
|
|||||||
@ -17,8 +17,8 @@ import java.time.LocalDate
|
|||||||
|
|
||||||
@DataJpaTest
|
@DataJpaTest
|
||||||
class ReservationSearchSpecificationTest(
|
class ReservationSearchSpecificationTest(
|
||||||
val entityManager: EntityManager,
|
val entityManager: EntityManager,
|
||||||
val reservationRepository: ReservationRepository
|
val reservationRepository: ReservationRepository
|
||||||
) : StringSpec() {
|
) : StringSpec() {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -31,8 +31,8 @@ class ReservationSearchSpecificationTest(
|
|||||||
|
|
||||||
"동일한 테마의 예약을 조회한다" {
|
"동일한 테마의 예약을 조회한다" {
|
||||||
val spec = ReservationSearchSpecification()
|
val spec = ReservationSearchSpecification()
|
||||||
.sameThemeId(theme.id)
|
.sameThemeId(theme.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||||
|
|
||||||
@ -44,8 +44,8 @@ class ReservationSearchSpecificationTest(
|
|||||||
|
|
||||||
"동일한 회원의 예약을 조회한다" {
|
"동일한 회원의 예약을 조회한다" {
|
||||||
val spec = ReservationSearchSpecification()
|
val spec = ReservationSearchSpecification()
|
||||||
.sameMemberId(member.id)
|
.sameMemberId(member.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||||
|
|
||||||
@ -57,8 +57,8 @@ class ReservationSearchSpecificationTest(
|
|||||||
|
|
||||||
"동일한 예약 시간의 예약을 조회한다" {
|
"동일한 예약 시간의 예약을 조회한다" {
|
||||||
val spec = ReservationSearchSpecification()
|
val spec = ReservationSearchSpecification()
|
||||||
.sameTimeId(time.id)
|
.sameTimeId(time.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||||
|
|
||||||
@ -70,8 +70,8 @@ class ReservationSearchSpecificationTest(
|
|||||||
|
|
||||||
"동일한 날짜의 예약을 조회한다" {
|
"동일한 날짜의 예약을 조회한다" {
|
||||||
val spec = ReservationSearchSpecification()
|
val spec = ReservationSearchSpecification()
|
||||||
.sameDate(LocalDate.now())
|
.sameDate(LocalDate.now())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||||
|
|
||||||
@ -83,8 +83,8 @@ class ReservationSearchSpecificationTest(
|
|||||||
|
|
||||||
"확정 상태인 예약을 조회한다" {
|
"확정 상태인 예약을 조회한다" {
|
||||||
val spec = ReservationSearchSpecification()
|
val spec = ReservationSearchSpecification()
|
||||||
.confirmed()
|
.confirmed()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||||
|
|
||||||
@ -96,8 +96,8 @@ class ReservationSearchSpecificationTest(
|
|||||||
|
|
||||||
"대기 상태인 예약을 조회한다" {
|
"대기 상태인 예약을 조회한다" {
|
||||||
val spec = ReservationSearchSpecification()
|
val spec = ReservationSearchSpecification()
|
||||||
.waiting()
|
.waiting()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||||
|
|
||||||
@ -109,8 +109,8 @@ class ReservationSearchSpecificationTest(
|
|||||||
|
|
||||||
"예약 날짜가 오늘 이후인 예약을 조회한다" {
|
"예약 날짜가 오늘 이후인 예약을 조회한다" {
|
||||||
val spec = ReservationSearchSpecification()
|
val spec = ReservationSearchSpecification()
|
||||||
.dateStartFrom(LocalDate.now())
|
.dateStartFrom(LocalDate.now())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||||
|
|
||||||
@ -122,8 +122,8 @@ class ReservationSearchSpecificationTest(
|
|||||||
|
|
||||||
"예약 날짜가 내일 이전인 예약을 조회한다" {
|
"예약 날짜가 내일 이전인 예약을 조회한다" {
|
||||||
val spec = ReservationSearchSpecification()
|
val spec = ReservationSearchSpecification()
|
||||||
.dateEndAt(LocalDate.now().plusDays(1))
|
.dateEndAt(LocalDate.now().plusDays(1))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||||
|
|
||||||
@ -145,31 +145,31 @@ class ReservationSearchSpecificationTest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
confirmedNow = ReservationFixture.create(
|
confirmedNow = ReservationFixture.create(
|
||||||
time = time,
|
time = time,
|
||||||
member = member,
|
member = member,
|
||||||
theme = theme,
|
theme = theme,
|
||||||
date = LocalDate.now(),
|
date = LocalDate.now(),
|
||||||
status = ReservationStatus.CONFIRMED
|
status = ReservationStatus.CONFIRMED
|
||||||
).also {
|
).also {
|
||||||
entityManager.persist(it)
|
entityManager.persist(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmedNotPaidYesterday = ReservationFixture.create(
|
confirmedNotPaidYesterday = ReservationFixture.create(
|
||||||
time = time,
|
time = time,
|
||||||
member = member,
|
member = member,
|
||||||
theme = theme,
|
theme = theme,
|
||||||
date = LocalDate.now().minusDays(1),
|
date = LocalDate.now().minusDays(1),
|
||||||
status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||||
).also {
|
).also {
|
||||||
entityManager.persist(it)
|
entityManager.persist(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
waitingTomorrow = ReservationFixture.create(
|
waitingTomorrow = ReservationFixture.create(
|
||||||
time = time,
|
time = time,
|
||||||
member = member,
|
member = member,
|
||||||
theme = theme,
|
theme = theme,
|
||||||
date = LocalDate.now().plusDays(1),
|
date = LocalDate.now().plusDays(1),
|
||||||
status = ReservationStatus.WAITING
|
status = ReservationStatus.WAITING
|
||||||
).also {
|
).also {
|
||||||
entityManager.persist(it)
|
entityManager.persist(it)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,9 +61,9 @@ class ThemeServiceTest : FunSpec({
|
|||||||
|
|
||||||
context("save") {
|
context("save") {
|
||||||
val request = ThemeCreateRequest(
|
val request = ThemeCreateRequest(
|
||||||
name = "New Theme",
|
name = "New Theme",
|
||||||
description = "Description",
|
description = "Description",
|
||||||
thumbnail = "http://example.com/thumbnail.jpg"
|
thumbnail = "http://example.com/thumbnail.jpg"
|
||||||
)
|
)
|
||||||
|
|
||||||
test("저장 성공") {
|
test("저장 성공") {
|
||||||
@ -74,10 +74,10 @@ class ThemeServiceTest : FunSpec({
|
|||||||
every {
|
every {
|
||||||
themeRepository.save(any())
|
themeRepository.save(any())
|
||||||
} returns ThemeFixture.create(
|
} returns ThemeFixture.create(
|
||||||
id = 1L,
|
id = 1L,
|
||||||
name = request.name,
|
name = request.name,
|
||||||
description = request.description,
|
description = request.description,
|
||||||
thumbnail = request.thumbnail
|
thumbnail = request.thumbnail
|
||||||
)
|
)
|
||||||
|
|
||||||
val response: ThemeRetrieveResponse = themeService.createTheme(request)
|
val response: ThemeRetrieveResponse = themeService.createTheme(request)
|
||||||
|
|||||||
@ -10,8 +10,8 @@ import java.time.LocalDate
|
|||||||
|
|
||||||
@DataJpaTest
|
@DataJpaTest
|
||||||
class ThemeRepositoryTest(
|
class ThemeRepositoryTest(
|
||||||
val themeRepository: ThemeRepository,
|
val themeRepository: ThemeRepository,
|
||||||
val entityManager: EntityManager
|
val entityManager: EntityManager
|
||||||
) : FunSpec() {
|
) : FunSpec() {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -19,65 +19,65 @@ class ThemeRepositoryTest(
|
|||||||
beforeTest {
|
beforeTest {
|
||||||
for (i in 1..10) {
|
for (i in 1..10) {
|
||||||
TestThemeCreateUtil.createThemeWithReservations(
|
TestThemeCreateUtil.createThemeWithReservations(
|
||||||
entityManager = entityManager,
|
entityManager = entityManager,
|
||||||
name = "테마$i",
|
name = "테마$i",
|
||||||
reservedCount = i,
|
reservedCount = i,
|
||||||
date = LocalDate.now().minusDays(i.toLong()),
|
date = LocalDate.now().minusDays(i.toLong()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("지난 10일간 예약 수가 가장 많은 테마 5개를 조회한다.") {
|
test("지난 10일간 예약 수가 가장 많은 테마 5개를 조회한다.") {
|
||||||
themeRepository.findPopularThemes(
|
themeRepository.findPopularThemes(
|
||||||
LocalDate.now().minusDays(10),
|
LocalDate.now().minusDays(10),
|
||||||
LocalDate.now().minusDays(1),
|
LocalDate.now().minusDays(1),
|
||||||
5
|
5
|
||||||
).also { themes ->
|
).also { themes ->
|
||||||
themes.size shouldBe 5
|
themes.size shouldBe 5
|
||||||
themes.map { it.name } shouldContainInOrder listOf(
|
themes.map { it.name } shouldContainInOrder listOf(
|
||||||
"테마10", "테마9", "테마8", "테마7", "테마6"
|
"테마10", "테마9", "테마8", "테마7", "테마6"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("8일 전부터 5일 전까지 예약 수가 가장 많은 테마 3개를 조회한다.") {
|
test("8일 전부터 5일 전까지 예약 수가 가장 많은 테마 3개를 조회한다.") {
|
||||||
themeRepository.findPopularThemes(
|
themeRepository.findPopularThemes(
|
||||||
LocalDate.now().minusDays(8),
|
LocalDate.now().minusDays(8),
|
||||||
LocalDate.now().minusDays(5),
|
LocalDate.now().minusDays(5),
|
||||||
3
|
3
|
||||||
).also { themes ->
|
).also { themes ->
|
||||||
themes.size shouldBe 3
|
themes.size shouldBe 3
|
||||||
themes.map { it.name } shouldContainInOrder listOf(
|
themes.map { it.name } shouldContainInOrder listOf(
|
||||||
"테마8", "테마7", "테마6"
|
"테마8", "테마7", "테마6"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("예약 수가 동일하면 먼저 생성된 테마를 우선 조회한다.") {
|
test("예약 수가 동일하면 먼저 생성된 테마를 우선 조회한다.") {
|
||||||
TestThemeCreateUtil.createThemeWithReservations(
|
TestThemeCreateUtil.createThemeWithReservations(
|
||||||
entityManager = entityManager,
|
entityManager = entityManager,
|
||||||
name = "테마11",
|
name = "테마11",
|
||||||
reservedCount = 5,
|
reservedCount = 5,
|
||||||
date = LocalDate.now().minusDays(5),
|
date = LocalDate.now().minusDays(5),
|
||||||
)
|
)
|
||||||
|
|
||||||
themeRepository.findPopularThemes(
|
themeRepository.findPopularThemes(
|
||||||
LocalDate.now().minusDays(6),
|
LocalDate.now().minusDays(6),
|
||||||
LocalDate.now().minusDays(4),
|
LocalDate.now().minusDays(4),
|
||||||
5
|
5
|
||||||
).also { themes ->
|
).also { themes ->
|
||||||
themes.size shouldBe 4
|
themes.size shouldBe 4
|
||||||
themes.map { it.name } shouldContainInOrder listOf(
|
themes.map { it.name } shouldContainInOrder listOf(
|
||||||
"테마6", "테마5", "테마11", "테마4"
|
"테마6", "테마5", "테마11", "테마4"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("입력된 갯수보다 조회된 갯수가 작으면, 조회된 갯수만큼 반환한다.") {
|
test("입력된 갯수보다 조회된 갯수가 작으면, 조회된 갯수만큼 반환한다.") {
|
||||||
themeRepository.findPopularThemes(
|
themeRepository.findPopularThemes(
|
||||||
LocalDate.now().minusDays(10),
|
LocalDate.now().minusDays(10),
|
||||||
LocalDate.now().minusDays(6),
|
LocalDate.now().minusDays(6),
|
||||||
10
|
10
|
||||||
).also { themes ->
|
).also { themes ->
|
||||||
themes.size shouldBe 5
|
themes.size shouldBe 5
|
||||||
}
|
}
|
||||||
@ -85,9 +85,9 @@ class ThemeRepositoryTest(
|
|||||||
|
|
||||||
test("입력된 갯수보다 조회된 갯수가 많으면, 입력된 갯수만큼 반환한다.") {
|
test("입력된 갯수보다 조회된 갯수가 많으면, 입력된 갯수만큼 반환한다.") {
|
||||||
themeRepository.findPopularThemes(
|
themeRepository.findPopularThemes(
|
||||||
LocalDate.now().minusDays(10),
|
LocalDate.now().minusDays(10),
|
||||||
LocalDate.now().minusDays(1),
|
LocalDate.now().minusDays(1),
|
||||||
15
|
15
|
||||||
).also { themes ->
|
).also { themes ->
|
||||||
themes.size shouldBe 10
|
themes.size shouldBe 10
|
||||||
}
|
}
|
||||||
@ -95,9 +95,9 @@ class ThemeRepositoryTest(
|
|||||||
|
|
||||||
test("입력된 날짜 범위에 예약된 테마가 없을 경우 빈 리스트를 반환한다.") {
|
test("입력된 날짜 범위에 예약된 테마가 없을 경우 빈 리스트를 반환한다.") {
|
||||||
themeRepository.findPopularThemes(
|
themeRepository.findPopularThemes(
|
||||||
LocalDate.now().plusDays(1),
|
LocalDate.now().plusDays(1),
|
||||||
LocalDate.now().plusDays(10),
|
LocalDate.now().plusDays(10),
|
||||||
5
|
5
|
||||||
).also { themes ->
|
).also { themes ->
|
||||||
themes.size shouldBe 0
|
themes.size shouldBe 0
|
||||||
}
|
}
|
||||||
@ -107,10 +107,10 @@ class ThemeRepositoryTest(
|
|||||||
val themeName = "test-theme"
|
val themeName = "test-theme"
|
||||||
beforeTest {
|
beforeTest {
|
||||||
TestThemeCreateUtil.createThemeWithReservations(
|
TestThemeCreateUtil.createThemeWithReservations(
|
||||||
entityManager = entityManager,
|
entityManager = entityManager,
|
||||||
name = themeName,
|
name = themeName,
|
||||||
reservedCount = 0,
|
reservedCount = 0,
|
||||||
date = LocalDate.now()
|
date = LocalDate.now()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
test("테마 이름이 존재하면 true를 반환한다.") {
|
test("테마 이름이 존재하면 true를 반환한다.") {
|
||||||
@ -125,20 +125,20 @@ class ThemeRepositoryTest(
|
|||||||
context("isReservedTheme") {
|
context("isReservedTheme") {
|
||||||
test("테마가 예약 중이면 true를 반환한다.") {
|
test("테마가 예약 중이면 true를 반환한다.") {
|
||||||
val theme = TestThemeCreateUtil.createThemeWithReservations(
|
val theme = TestThemeCreateUtil.createThemeWithReservations(
|
||||||
entityManager = entityManager,
|
entityManager = entityManager,
|
||||||
name = "예약된 테마",
|
name = "예약된 테마",
|
||||||
reservedCount = 1,
|
reservedCount = 1,
|
||||||
date = LocalDate.now()
|
date = LocalDate.now()
|
||||||
)
|
)
|
||||||
themeRepository.isReservedTheme(theme.id!!) shouldBe true
|
themeRepository.isReservedTheme(theme.id!!) shouldBe true
|
||||||
}
|
}
|
||||||
|
|
||||||
test("테마가 예약 중이 아니면 false를 반환한다.") {
|
test("테마가 예약 중이 아니면 false를 반환한다.") {
|
||||||
val theme = TestThemeCreateUtil.createThemeWithReservations(
|
val theme = TestThemeCreateUtil.createThemeWithReservations(
|
||||||
entityManager = entityManager,
|
entityManager = entityManager,
|
||||||
name = "예약되지 않은 테마",
|
name = "예약되지 않은 테마",
|
||||||
reservedCount = 0,
|
reservedCount = 0,
|
||||||
date = LocalDate.now()
|
date = LocalDate.now()
|
||||||
)
|
)
|
||||||
themeRepository.isReservedTheme(theme.id!!) shouldBe false
|
themeRepository.isReservedTheme(theme.id!!) shouldBe false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,25 +14,25 @@ import java.time.LocalTime
|
|||||||
|
|
||||||
object TestThemeCreateUtil {
|
object TestThemeCreateUtil {
|
||||||
fun createThemeWithReservations(
|
fun createThemeWithReservations(
|
||||||
entityManager: EntityManager,
|
entityManager: EntityManager,
|
||||||
name: String,
|
name: String,
|
||||||
reservedCount: Int,
|
reservedCount: Int,
|
||||||
date: LocalDate,
|
date: LocalDate,
|
||||||
): ThemeEntity {
|
): ThemeEntity {
|
||||||
val themeEntity: ThemeEntity = ThemeFixture.create(name = name).also { entityManager.persist(it) }
|
val themeEntity: ThemeEntity = ThemeFixture.create(name = name).also { entityManager.persist(it) }
|
||||||
val member: MemberEntity = MemberFixture.create().also { entityManager.persist(it) }
|
val member: MemberEntity = MemberFixture.create().also { entityManager.persist(it) }
|
||||||
|
|
||||||
for (i in 1..reservedCount) {
|
for (i in 1..reservedCount) {
|
||||||
val time: TimeEntity = TimeFixture.create(
|
val time: TimeEntity = TimeFixture.create(
|
||||||
startAt = LocalTime.now().plusMinutes(i.toLong())
|
startAt = LocalTime.now().plusMinutes(i.toLong())
|
||||||
).also { entityManager.persist(it) }
|
).also { entityManager.persist(it) }
|
||||||
|
|
||||||
ReservationFixture.create(
|
ReservationFixture.create(
|
||||||
date = date,
|
date = date,
|
||||||
theme = themeEntity,
|
theme = themeEntity,
|
||||||
member = member,
|
member = member,
|
||||||
time = time,
|
time = time,
|
||||||
status = ReservationStatus.CONFIRMED
|
status = ReservationStatus.CONFIRMED
|
||||||
).also { entityManager.persist(it) }
|
).also { entityManager.persist(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,9 +17,9 @@ import kotlin.random.Random
|
|||||||
|
|
||||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||||
class MostReservedThemeApiTest(
|
class MostReservedThemeApiTest(
|
||||||
@LocalServerPort val port: Int,
|
@LocalServerPort val port: Int,
|
||||||
val transactionTemplate: TransactionTemplate,
|
val transactionTemplate: TransactionTemplate,
|
||||||
val entityManager: EntityManager,
|
val entityManager: EntityManager,
|
||||||
) : FunSpec({
|
) : FunSpec({
|
||||||
extension(DatabaseCleanerExtension(mode = CleanerMode.AFTER_SPEC))
|
extension(DatabaseCleanerExtension(mode = CleanerMode.AFTER_SPEC))
|
||||||
}) {
|
}) {
|
||||||
@ -29,19 +29,19 @@ class MostReservedThemeApiTest(
|
|||||||
// 지난 7일간 예약된 테마 10개 생성
|
// 지난 7일간 예약된 테마 10개 생성
|
||||||
for (i in 1..10) {
|
for (i in 1..10) {
|
||||||
TestThemeCreateUtil.createThemeWithReservations(
|
TestThemeCreateUtil.createThemeWithReservations(
|
||||||
entityManager = entityManager,
|
entityManager = entityManager,
|
||||||
name = "테마$i",
|
name = "테마$i",
|
||||||
reservedCount = 1,
|
reservedCount = 1,
|
||||||
date = LocalDate.now().minusDays(Random.nextLong(1, 7))
|
date = LocalDate.now().minusDays(Random.nextLong(1, 7))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8일 전 예약된 테마 1개 생성
|
// 8일 전 예약된 테마 1개 생성
|
||||||
TestThemeCreateUtil.createThemeWithReservations(
|
TestThemeCreateUtil.createThemeWithReservations(
|
||||||
entityManager = entityManager,
|
entityManager = entityManager,
|
||||||
name = "테마11",
|
name = "테마11",
|
||||||
reservedCount = 1,
|
reservedCount = 1,
|
||||||
date = LocalDate.now().minusDays(8)
|
date = LocalDate.now().minusDays(8)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,8 +22,8 @@ class TimeServiceTest : FunSpec({
|
|||||||
val reservationRepository: ReservationRepository = mockk()
|
val reservationRepository: ReservationRepository = mockk()
|
||||||
|
|
||||||
val timeService = TimeService(
|
val timeService = TimeService(
|
||||||
timeRepository = timeRepository,
|
timeRepository = timeRepository,
|
||||||
reservationRepository = reservationRepository
|
reservationRepository = reservationRepository
|
||||||
)
|
)
|
||||||
|
|
||||||
context("findTimeById") {
|
context("findTimeById") {
|
||||||
@ -46,8 +46,8 @@ class TimeServiceTest : FunSpec({
|
|||||||
test("정상 저장") {
|
test("정상 저장") {
|
||||||
every { timeRepository.existsByStartAt(request.startAt) } returns false
|
every { timeRepository.existsByStartAt(request.startAt) } returns false
|
||||||
every { timeRepository.save(any()) } returns TimeFixture.create(
|
every { timeRepository.save(any()) } returns TimeFixture.create(
|
||||||
id = 1L,
|
id = 1L,
|
||||||
startAt = request.startAt
|
startAt = request.startAt
|
||||||
)
|
)
|
||||||
|
|
||||||
val response = timeService.createTime(request)
|
val response = timeService.createTime(request)
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import java.time.LocalTime
|
|||||||
|
|
||||||
@DataJpaTest
|
@DataJpaTest
|
||||||
class TimeRepositoryTest(
|
class TimeRepositoryTest(
|
||||||
val entityManager: EntityManager,
|
val entityManager: EntityManager,
|
||||||
val timeRepository: TimeRepository,
|
val timeRepository: TimeRepository,
|
||||||
) : FunSpec({
|
) : FunSpec({
|
||||||
|
|
||||||
context("existsByStartAt") {
|
context("existsByStartAt") {
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import org.springframework.stereotype.Component
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
class DatabaseCleaner(
|
class DatabaseCleaner(
|
||||||
val entityManager: EntityManager,
|
val entityManager: EntityManager,
|
||||||
val jdbcTemplate: JdbcTemplate,
|
val jdbcTemplate: JdbcTemplate,
|
||||||
) {
|
) {
|
||||||
val tables: List<String> by lazy {
|
val tables: List<String> by lazy {
|
||||||
jdbcTemplate.query("SHOW TABLES") { rs, _ ->
|
jdbcTemplate.query("SHOW TABLES") { rs, _ ->
|
||||||
@ -38,7 +38,7 @@ enum class CleanerMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DatabaseCleanerExtension(
|
class DatabaseCleanerExtension(
|
||||||
private val mode: CleanerMode
|
private val mode: CleanerMode
|
||||||
) : AfterTestListener, AfterSpecListener {
|
) : AfterTestListener, AfterSpecListener {
|
||||||
override suspend fun afterTest(testCase: TestCase, result: TestResult) {
|
override suspend fun afterTest(testCase: TestCase, result: TestResult) {
|
||||||
super.afterTest(testCase, result)
|
super.afterTest(testCase, result)
|
||||||
@ -58,7 +58,7 @@ class DatabaseCleanerExtension(
|
|||||||
|
|
||||||
private suspend fun getCleaner(): DatabaseCleaner {
|
private suspend fun getCleaner(): DatabaseCleaner {
|
||||||
return testContextManager().testContext
|
return testContextManager().testContext
|
||||||
.applicationContext
|
.applicationContext
|
||||||
.getBean(DatabaseCleaner::class.java)
|
.getBean(DatabaseCleaner::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,88 +24,88 @@ object MemberFixture {
|
|||||||
const val NOT_LOGGED_IN_USERID: Long = 0
|
const val NOT_LOGGED_IN_USERID: Long = 0
|
||||||
|
|
||||||
fun create(
|
fun create(
|
||||||
id: Long? = null,
|
id: Long? = null,
|
||||||
name: String = "sangdol",
|
name: String = "sangdol",
|
||||||
account: String = "default",
|
account: String = "default",
|
||||||
password: String = "password",
|
password: String = "password",
|
||||||
role: Role = Role.ADMIN
|
role: Role = Role.ADMIN
|
||||||
): MemberEntity = MemberEntity(id, name, "$account@email.com", password, role)
|
): MemberEntity = MemberEntity(id, name, "$account@email.com", password, role)
|
||||||
|
|
||||||
fun admin(): MemberEntity = create(
|
fun admin(): MemberEntity = create(
|
||||||
id = 2L,
|
id = 2L,
|
||||||
account = "admin",
|
account = "admin",
|
||||||
role = Role.ADMIN
|
role = Role.ADMIN
|
||||||
)
|
)
|
||||||
|
|
||||||
fun adminLoginRequest(): LoginRequest = LoginRequest(
|
fun adminLoginRequest(): LoginRequest = LoginRequest(
|
||||||
email = admin().email,
|
email = admin().email,
|
||||||
password = admin().password
|
password = admin().password
|
||||||
)
|
)
|
||||||
|
|
||||||
fun user(): MemberEntity = create(
|
fun user(): MemberEntity = create(
|
||||||
id = 1L,
|
id = 1L,
|
||||||
account = "user",
|
account = "user",
|
||||||
role = Role.MEMBER
|
role = Role.MEMBER
|
||||||
)
|
)
|
||||||
|
|
||||||
fun userLoginRequest(): LoginRequest = LoginRequest(
|
fun userLoginRequest(): LoginRequest = LoginRequest(
|
||||||
email = user().email,
|
email = user().email,
|
||||||
password = user().password
|
password = user().password
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
object TimeFixture {
|
object TimeFixture {
|
||||||
fun create(
|
fun create(
|
||||||
id: Long? = null,
|
id: Long? = null,
|
||||||
startAt: LocalTime = LocalTime.now().plusHours(1),
|
startAt: LocalTime = LocalTime.now().plusHours(1),
|
||||||
): TimeEntity = TimeEntity(id, startAt)
|
): TimeEntity = TimeEntity(id, startAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
object ThemeFixture {
|
object ThemeFixture {
|
||||||
fun create(
|
fun create(
|
||||||
id: Long? = null,
|
id: Long? = null,
|
||||||
name: String = "Default Theme",
|
name: String = "Default Theme",
|
||||||
description: String = "Default Description",
|
description: String = "Default Description",
|
||||||
thumbnail: String = "https://example.com/default-thumbnail.jpg"
|
thumbnail: String = "https://example.com/default-thumbnail.jpg"
|
||||||
): ThemeEntity = ThemeEntity(id, name, description, thumbnail)
|
): ThemeEntity = ThemeEntity(id, name, description, thumbnail)
|
||||||
}
|
}
|
||||||
|
|
||||||
object ReservationFixture {
|
object ReservationFixture {
|
||||||
fun create(
|
fun create(
|
||||||
id: Long? = null,
|
id: Long? = null,
|
||||||
date: LocalDate = LocalDate.now().plusWeeks(1),
|
date: LocalDate = LocalDate.now().plusWeeks(1),
|
||||||
theme: ThemeEntity = ThemeFixture.create(),
|
theme: ThemeEntity = ThemeFixture.create(),
|
||||||
time: TimeEntity = TimeFixture.create(),
|
time: TimeEntity = TimeFixture.create(),
|
||||||
member: MemberEntity = MemberFixture.create(),
|
member: MemberEntity = MemberFixture.create(),
|
||||||
status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||||
): ReservationEntity = ReservationEntity(id, date, time, theme, member, status)
|
): ReservationEntity = ReservationEntity(id, date, time, theme, member, status)
|
||||||
|
|
||||||
fun createRequest(
|
fun createRequest(
|
||||||
date: LocalDate = LocalDate.now().plusWeeks(1),
|
date: LocalDate = LocalDate.now().plusWeeks(1),
|
||||||
themeId: Long = 1L,
|
themeId: Long = 1L,
|
||||||
timeId: Long = 1L,
|
timeId: Long = 1L,
|
||||||
paymentKey: String = "paymentKey",
|
paymentKey: String = "paymentKey",
|
||||||
orderId: String = "orderId",
|
orderId: String = "orderId",
|
||||||
amount: Long = 10000L,
|
amount: Long = 10000L,
|
||||||
paymentType: String = "NORMAL",
|
paymentType: String = "NORMAL",
|
||||||
): ReservationCreateWithPaymentRequest = ReservationCreateWithPaymentRequest(
|
): ReservationCreateWithPaymentRequest = ReservationCreateWithPaymentRequest(
|
||||||
date = date,
|
date = date,
|
||||||
timeId = timeId,
|
timeId = timeId,
|
||||||
themeId = themeId,
|
themeId = themeId,
|
||||||
paymentKey = paymentKey,
|
paymentKey = paymentKey,
|
||||||
orderId = orderId,
|
orderId = orderId,
|
||||||
amount = amount,
|
amount = amount,
|
||||||
paymentType = paymentType
|
paymentType = paymentType
|
||||||
)
|
)
|
||||||
|
|
||||||
fun createWaitingRequest(
|
fun createWaitingRequest(
|
||||||
date: LocalDate = LocalDate.now().plusWeeks(1),
|
date: LocalDate = LocalDate.now().plusWeeks(1),
|
||||||
themeId: Long = 1L,
|
themeId: Long = 1L,
|
||||||
timeId: Long = 1L
|
timeId: Long = 1L
|
||||||
): WaitingCreateRequest = WaitingCreateRequest(
|
): WaitingCreateRequest = WaitingCreateRequest(
|
||||||
date = date,
|
date = date,
|
||||||
timeId = timeId,
|
timeId = timeId,
|
||||||
themeId = themeId
|
themeId = themeId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,8 +114,8 @@ object JwtFixture {
|
|||||||
const val EXPIRATION_TIME: Long = 1000 * 60 * 60
|
const val EXPIRATION_TIME: Long = 1000 * 60 * 60
|
||||||
|
|
||||||
fun create(
|
fun create(
|
||||||
secretKey: String = SECRET_KEY_STRING,
|
secretKey: String = SECRET_KEY_STRING,
|
||||||
expirationTime: Long = EXPIRATION_TIME
|
expirationTime: Long = EXPIRATION_TIME
|
||||||
): JwtHandler = JwtHandler(secretKey, expirationTime)
|
): JwtHandler = JwtHandler(secretKey, expirationTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,63 +125,63 @@ object PaymentFixture {
|
|||||||
const val AMOUNT: Long = 10000L
|
const val AMOUNT: Long = 10000L
|
||||||
|
|
||||||
fun create(
|
fun create(
|
||||||
id: Long? = null,
|
id: Long? = null,
|
||||||
orderId: String = ORDER_ID,
|
orderId: String = ORDER_ID,
|
||||||
paymentKey: String = PAYMENT_KEY,
|
paymentKey: String = PAYMENT_KEY,
|
||||||
totalAmount: Long = AMOUNT,
|
totalAmount: Long = AMOUNT,
|
||||||
reservation: ReservationEntity = ReservationFixture.create(id = 1L),
|
reservation: ReservationEntity = ReservationFixture.create(id = 1L),
|
||||||
approvedAt: OffsetDateTime = OffsetDateTime.now()
|
approvedAt: OffsetDateTime = OffsetDateTime.now()
|
||||||
): PaymentEntity = PaymentEntity(
|
): PaymentEntity = PaymentEntity(
|
||||||
id = id,
|
id = id,
|
||||||
orderId = orderId,
|
orderId = orderId,
|
||||||
paymentKey = paymentKey,
|
paymentKey = paymentKey,
|
||||||
totalAmount = totalAmount,
|
totalAmount = totalAmount,
|
||||||
reservation = reservation,
|
reservation = reservation,
|
||||||
approvedAt = approvedAt
|
approvedAt = approvedAt
|
||||||
)
|
)
|
||||||
|
|
||||||
fun createCanceled(
|
fun createCanceled(
|
||||||
id: Long? = null,
|
id: Long? = null,
|
||||||
paymentKey: String = PAYMENT_KEY,
|
paymentKey: String = PAYMENT_KEY,
|
||||||
cancelReason: String = "Test Cancel",
|
cancelReason: String = "Test Cancel",
|
||||||
cancelAmount: Long = AMOUNT,
|
cancelAmount: Long = AMOUNT,
|
||||||
approvedAt: OffsetDateTime = OffsetDateTime.now(),
|
approvedAt: OffsetDateTime = OffsetDateTime.now(),
|
||||||
canceledAt: OffsetDateTime = approvedAt.plusHours(1)
|
canceledAt: OffsetDateTime = approvedAt.plusHours(1)
|
||||||
): CanceledPaymentEntity = CanceledPaymentEntity(
|
): CanceledPaymentEntity = CanceledPaymentEntity(
|
||||||
id = id,
|
id = id,
|
||||||
paymentKey = paymentKey,
|
paymentKey = paymentKey,
|
||||||
cancelReason = cancelReason,
|
cancelReason = cancelReason,
|
||||||
cancelAmount = cancelAmount,
|
cancelAmount = cancelAmount,
|
||||||
approvedAt = approvedAt,
|
approvedAt = approvedAt,
|
||||||
canceledAt = canceledAt
|
canceledAt = canceledAt
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fun createApproveRequest(): PaymentApproveRequest = PaymentApproveRequest(
|
fun createApproveRequest(): PaymentApproveRequest = PaymentApproveRequest(
|
||||||
paymentKey = PAYMENT_KEY,
|
paymentKey = PAYMENT_KEY,
|
||||||
orderId = ORDER_ID,
|
orderId = ORDER_ID,
|
||||||
amount = AMOUNT,
|
amount = AMOUNT,
|
||||||
paymentType = "CARD"
|
paymentType = "CARD"
|
||||||
)
|
)
|
||||||
|
|
||||||
fun createApproveResponse(): PaymentApproveResponse = PaymentApproveResponse(
|
fun createApproveResponse(): PaymentApproveResponse = PaymentApproveResponse(
|
||||||
paymentKey = PAYMENT_KEY,
|
paymentKey = PAYMENT_KEY,
|
||||||
orderId = ORDER_ID,
|
orderId = ORDER_ID,
|
||||||
approvedAt = OffsetDateTime.now(),
|
approvedAt = OffsetDateTime.now(),
|
||||||
totalAmount = AMOUNT
|
totalAmount = AMOUNT
|
||||||
)
|
)
|
||||||
|
|
||||||
fun createCancelRequest(): PaymentCancelRequest = PaymentCancelRequest(
|
fun createCancelRequest(): PaymentCancelRequest = PaymentCancelRequest(
|
||||||
paymentKey = PAYMENT_KEY,
|
paymentKey = PAYMENT_KEY,
|
||||||
amount = AMOUNT,
|
amount = AMOUNT,
|
||||||
cancelReason = "Test Cancel"
|
cancelReason = "Test Cancel"
|
||||||
)
|
)
|
||||||
|
|
||||||
fun createCancelResponse(): PaymentCancelResponse = PaymentCancelResponse(
|
fun createCancelResponse(): PaymentCancelResponse = PaymentCancelResponse(
|
||||||
cancelStatus = "SUCCESS",
|
cancelStatus = "SUCCESS",
|
||||||
cancelReason = "Test Cancel",
|
cancelReason = "Test Cancel",
|
||||||
cancelAmount = AMOUNT,
|
cancelAmount = AMOUNT,
|
||||||
canceledAt = OffsetDateTime.now().plusMinutes(1)
|
canceledAt = OffsetDateTime.now().plusMinutes(1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
spring:
|
spring:
|
||||||
jpa:
|
jpa:
|
||||||
show-sql: true
|
show-sql: false
|
||||||
properties:
|
properties:
|
||||||
hibernate:
|
hibernate:
|
||||||
format_sql: true
|
format_sql: true
|
||||||
@ -24,5 +24,6 @@ payment:
|
|||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
org.springframework.orm.jpa: DEBUG
|
root: INFO
|
||||||
|
org.springframework.orm.jpa: INFO
|
||||||
org.springframework.transaction: DEBUG
|
org.springframework.transaction: DEBUG
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user