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