From 830d3712e8e5d4ed773c50c9a5e7a5cc63c13f82 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 10:55:06 +0900 Subject: [PATCH 01/37] =?UTF-8?q?refactor:=20JwtHandler=EB=82=B4=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=EB=AA=85=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=20=EB=B3=80=EC=88=98=20=EC=83=81=EC=88=98?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/infrastructure/jwt/JwtHandler.kt | 16 ++++++++++------ src/main/resources/application.yaml | 3 +-- src/test/resources/application.yaml | 3 +-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt b/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt index 649fe125..feaabcda 100644 --- a/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt +++ b/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt @@ -13,15 +13,15 @@ class JwtHandler( @Value("\${security.jwt.token.secret-key}") private val secretKey: String, - @Value("\${security.jwt.token.access.expire-length}") - private val accessTokenExpireTime: Long + @Value("\${security.jwt.token.ttl-seconds}") + private val tokenTtlSeconds: Long ) { fun createToken(memberId: Long): String { val date = Date() - val accessTokenExpiredAt = Date(date.time + accessTokenExpireTime) + val accessTokenExpiredAt = Date(date.time + tokenTtlSeconds) return Jwts.builder() - .claim("memberId", memberId) + .claim(MEMBER_ID_CLAIM_KEY, memberId) .setIssuedAt(date) .setExpiration(accessTokenExpiredAt) .signWith(SignatureAlgorithm.HS256, secretKey.toByteArray()) @@ -33,8 +33,8 @@ class JwtHandler( return Jwts.parser() .setSigningKey(secretKey.toByteArray()) .parseClaimsJws(token) - .getBody() - .get("memberId", Number::class.java) + .body + .get(MEMBER_ID_CLAIM_KEY, Number::class.java) .toLong() } catch (e: Exception) { when (e) { @@ -47,4 +47,8 @@ class JwtHandler( } } } + + companion object { + private const val MEMBER_ID_CLAIM_KEY = "memberId" + } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 82422963..378e93fd 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -21,8 +21,7 @@ security: jwt: token: secret-key: daijawligagaf@LIJ$@U)9nagnalkkgalijaddljfi - access: - expire-length: 1800000 # 30 분 + ttl-seconds: 1800000 payment: api-base-url: https://api.tosspayments.com diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml index 9bc65da2..e2a6e144 100644 --- a/src/test/resources/application.yaml +++ b/src/test/resources/application.yaml @@ -14,8 +14,7 @@ security: jwt: token: secret-key: daijawligagaf@LIJ$@U)9nagnalkkgalijaddljfi - access: - expire-length: 1800000 # 30 분 + ttl-seconds: 1800000 payment: api-base-url: https://api.tosspayments.com -- 2.47.2 From 7c30100f5ab4c54d7830425155b26940b85ae530 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 11:04:11 +0900 Subject: [PATCH 02/37] =?UTF-8?q?feat:=20=EC=83=88=EB=A1=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=A0=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EA=B0=9D=EC=B2=B4=20=EB=B0=8F=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/CommonErrorCode.kt | 20 +++++++++++++++++++ .../roomescape/common/exception/ErrorCode.kt | 9 +++++++++ .../common/exception/RoomException.kt | 6 ++++++ 3 files changed, 35 insertions(+) create mode 100644 src/main/kotlin/roomescape/common/exception/CommonErrorCode.kt create mode 100644 src/main/kotlin/roomescape/common/exception/ErrorCode.kt create mode 100644 src/main/kotlin/roomescape/common/exception/RoomException.kt diff --git a/src/main/kotlin/roomescape/common/exception/CommonErrorCode.kt b/src/main/kotlin/roomescape/common/exception/CommonErrorCode.kt new file mode 100644 index 00000000..b69fc022 --- /dev/null +++ b/src/main/kotlin/roomescape/common/exception/CommonErrorCode.kt @@ -0,0 +1,20 @@ +package roomescape.common.exception + +import org.springframework.http.HttpStatus + +enum class CommonErrorCode( + override val httpStatus: HttpStatus, + override val errorCode: String, + override val message: String, +) : ErrorCode { + INVALID_INPUT_VALUE( + httpStatus = HttpStatus.BAD_REQUEST, + errorCode = "C001", + message = "요청 값이 잘못되었어요." + ), + UNEXPECTED_SERVER_ERROR( + httpStatus = HttpStatus.INTERNAL_SERVER_ERROR, + errorCode = "C999", + message = "서버에 예상치 못한 오류가 발생했어요. 관리자에게 문의해주세요.", + ), +} diff --git a/src/main/kotlin/roomescape/common/exception/ErrorCode.kt b/src/main/kotlin/roomescape/common/exception/ErrorCode.kt new file mode 100644 index 00000000..1f08d3b5 --- /dev/null +++ b/src/main/kotlin/roomescape/common/exception/ErrorCode.kt @@ -0,0 +1,9 @@ +package roomescape.common.exception + +import org.springframework.http.HttpStatus + +interface ErrorCode { + val httpStatus: HttpStatus + val errorCode: String + val message: String +} diff --git a/src/main/kotlin/roomescape/common/exception/RoomException.kt b/src/main/kotlin/roomescape/common/exception/RoomException.kt new file mode 100644 index 00000000..c75a1519 --- /dev/null +++ b/src/main/kotlin/roomescape/common/exception/RoomException.kt @@ -0,0 +1,6 @@ +package roomescape.common.exception + +open class RoomException( + open val errorCode: ErrorCode, + override val message: String? = errorCode.message +) : RuntimeException(message) -- 2.47.2 From ef05a3ff9f10f1cab51fcac0a501f5a74ae0a085 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 11:04:43 +0900 Subject: [PATCH 03/37] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8(Auth)=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=A0=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/auth/exception/AuthErrorCode.kt | 18 ++++++++++++++++++ .../roomescape/auth/exception/AuthException.kt | 8 ++++++++ 2 files changed, 26 insertions(+) create mode 100644 src/main/kotlin/roomescape/auth/exception/AuthErrorCode.kt create mode 100644 src/main/kotlin/roomescape/auth/exception/AuthException.kt diff --git a/src/main/kotlin/roomescape/auth/exception/AuthErrorCode.kt b/src/main/kotlin/roomescape/auth/exception/AuthErrorCode.kt new file mode 100644 index 00000000..6a502945 --- /dev/null +++ b/src/main/kotlin/roomescape/auth/exception/AuthErrorCode.kt @@ -0,0 +1,18 @@ +package roomescape.auth.exception + +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, +): ErrorCode { + LOGIN_REQUIRED(HttpStatus.UNAUTHORIZED, "A001", "로그인이 필요해요."), + LOGIN_FAILED(HttpStatus.UNAUTHORIZED, "A002", "로그인에 실패했어요."), + TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "A003", "인증 토큰이 없어요."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "A004", "유효하지 않은 토큰이에요."), + EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "A005", "토큰이 만료됐어요."), + USER_NOT_FOUND_FROM_TOKEN(HttpStatus.UNAUTHORIZED, "A006", "토큰으로 회원 정보를 찾을 수 없어요."), + ACCESS_DENIED(HttpStatus.FORBIDDEN, "A007", "접근 권한이 없어요."), +} diff --git a/src/main/kotlin/roomescape/auth/exception/AuthException.kt b/src/main/kotlin/roomescape/auth/exception/AuthException.kt new file mode 100644 index 00000000..4704f64d --- /dev/null +++ b/src/main/kotlin/roomescape/auth/exception/AuthException.kt @@ -0,0 +1,8 @@ +package roomescape.auth.exception + +import roomescape.common.exception.RoomException + +class AuthException( + override val errorCode: AuthErrorCode, + override val message: String? = errorCode.message +) : RoomException(errorCode, message) -- 2.47.2 From 91edc7bb2929000bfdf9c692a592824fa6ea935d Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 11:31:21 +0900 Subject: [PATCH 04/37] =?UTF-8?q?feat:=20JwtHandler=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=9D=84=20=EC=83=88=EB=A1=9C=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=ED=95=9C=20AuthException=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/infrastructure/jwt/JwtHandler.kt | 22 ++++++++----------- .../auth/infrastructure/jwt/JwtHandlerTest.kt | 16 +++++++------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt b/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt index feaabcda..4437021b 100644 --- a/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt +++ b/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt @@ -1,11 +1,12 @@ package roomescape.auth.infrastructure.jwt -import io.jsonwebtoken.* +import io.jsonwebtoken.ExpiredJwtException +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.SignatureAlgorithm import org.springframework.beans.factory.annotation.Value -import org.springframework.http.HttpStatus import org.springframework.stereotype.Component -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException +import roomescape.auth.exception.AuthErrorCode +import roomescape.auth.exception.AuthException import java.util.* @Component @@ -36,15 +37,10 @@ class JwtHandler( .body .get(MEMBER_ID_CLAIM_KEY, Number::class.java) .toLong() - } catch (e: Exception) { - when (e) { - is ExpiredJwtException -> throw RoomescapeException(ErrorType.EXPIRED_TOKEN, HttpStatus.UNAUTHORIZED) - is UnsupportedJwtException -> throw RoomescapeException(ErrorType.UNSUPPORTED_TOKEN, HttpStatus.UNAUTHORIZED) - is MalformedJwtException -> throw RoomescapeException(ErrorType.MALFORMED_TOKEN, HttpStatus.UNAUTHORIZED) - is SignatureException -> throw RoomescapeException(ErrorType.INVALID_SIGNATURE_TOKEN, HttpStatus.UNAUTHORIZED) - is IllegalArgumentException -> throw RoomescapeException(ErrorType.INVALID_TOKEN, HttpStatus.UNAUTHORIZED) - else -> throw RoomescapeException(ErrorType.UNEXPECTED_ERROR, HttpStatus.INTERNAL_SERVER_ERROR) - } + } catch (_: ExpiredJwtException) { + throw AuthException(AuthErrorCode.EXPIRED_TOKEN) + } catch (_: Exception) { + throw AuthException(AuthErrorCode.INVALID_TOKEN) } } diff --git a/src/test/kotlin/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt b/src/test/kotlin/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt index 53f12279..91b9f376 100644 --- a/src/test/kotlin/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt +++ b/src/test/kotlin/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt @@ -5,8 +5,8 @@ import io.jsonwebtoken.SignatureAlgorithm import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException +import roomescape.auth.exception.AuthErrorCode +import roomescape.auth.exception.AuthException import roomescape.util.JwtFixture import java.util.* import kotlin.random.Random @@ -33,15 +33,15 @@ class JwtHandlerTest : FunSpec({ Thread.sleep(expirationTime) // 만료 시간 이후로 대기 // when & then - shouldThrow { + shouldThrow { shortExpirationTimeJwtHandler.getMemberIdFromToken(token) - }.errorType shouldBe ErrorType.EXPIRED_TOKEN + }.errorCode shouldBe AuthErrorCode.EXPIRED_TOKEN } test("토큰이 빈 값이면 예외를 던진다.") { - shouldThrow { + shouldThrow { jwtHandler.getMemberIdFromToken("") - }.errorType shouldBe ErrorType.INVALID_TOKEN + }.errorCode shouldBe AuthErrorCode.INVALID_TOKEN } test("시크릿 키가 잘못된 경우 예외를 던진다.") { @@ -53,9 +53,9 @@ class JwtHandlerTest : FunSpec({ .signWith(SignatureAlgorithm.HS256, JwtFixture.SECRET_KEY.substring(1).toByteArray()) .compact() - shouldThrow { + shouldThrow { jwtHandler.getMemberIdFromToken(invalidSignatureToken) - }.errorType shouldBe ErrorType.INVALID_SIGNATURE_TOKEN + }.errorCode shouldBe AuthErrorCode.INVALID_TOKEN } } }) -- 2.47.2 From 3551458086f0944d0c7b131877e08230971723f8 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 13:10:55 +0900 Subject: [PATCH 05/37] =?UTF-8?q?feat:=20=EC=83=88=EB=A1=9C=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=ED=95=9C=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=ED=95=84=EB=93=9C=20notnull=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=20=EB=B0=8F=20=EC=98=88=EC=99=B8=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/roomescape/auth/exception/AuthException.kt | 2 +- .../common/dto/response/CommonApiResponse.kt | 11 +++++++++++ .../roomescape/common/exception/RoomException.kt | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/roomescape/auth/exception/AuthException.kt b/src/main/kotlin/roomescape/auth/exception/AuthException.kt index 4704f64d..735dcaca 100644 --- a/src/main/kotlin/roomescape/auth/exception/AuthException.kt +++ b/src/main/kotlin/roomescape/auth/exception/AuthException.kt @@ -4,5 +4,5 @@ import roomescape.common.exception.RoomException class AuthException( override val errorCode: AuthErrorCode, - override val message: String? = errorCode.message + override val message: String = errorCode.message ) : RoomException(errorCode, message) diff --git a/src/main/kotlin/roomescape/common/dto/response/CommonApiResponse.kt b/src/main/kotlin/roomescape/common/dto/response/CommonApiResponse.kt index 6d589727..9cf1fc7f 100644 --- a/src/main/kotlin/roomescape/common/dto/response/CommonApiResponse.kt +++ b/src/main/kotlin/roomescape/common/dto/response/CommonApiResponse.kt @@ -1,6 +1,7 @@ package roomescape.common.dto.response import com.fasterxml.jackson.annotation.JsonInclude +import roomescape.common.exception.ErrorCode import roomescape.common.exception.ErrorType @JsonInclude(JsonInclude.Include.NON_NULL) @@ -12,3 +13,13 @@ data class CommonErrorResponse( val errorType: ErrorType, val message: String? = errorType.description ) + +data class CommonErrorResponseV2( + val code: String, + val message: String +) { + constructor(errorCode: ErrorCode, message: String = errorCode.message) : this( + code = errorCode.errorCode, + message = message + ) +} diff --git a/src/main/kotlin/roomescape/common/exception/RoomException.kt b/src/main/kotlin/roomescape/common/exception/RoomException.kt index c75a1519..51f6a6ec 100644 --- a/src/main/kotlin/roomescape/common/exception/RoomException.kt +++ b/src/main/kotlin/roomescape/common/exception/RoomException.kt @@ -2,5 +2,5 @@ package roomescape.common.exception open class RoomException( open val errorCode: ErrorCode, - override val message: String? = errorCode.message + override val message: String = errorCode.message ) : RuntimeException(message) -- 2.47.2 From b880d0631bf0839954854cc373cfe84d97b2858d Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 13:49:43 +0900 Subject: [PATCH 06/37] =?UTF-8?q?feat:=20=EC=83=88=EB=A1=9C=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=ED=95=9C=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=AA=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD(RoomException=20->=20RoomescapeExceptionV2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/roomescape/auth/exception/AuthException.kt | 4 ++-- .../kotlin/roomescape/common/exception/RoomException.kt | 6 ------ .../roomescape/common/exception/RoomescapeException.kt | 5 +++++ 3 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 src/main/kotlin/roomescape/common/exception/RoomException.kt diff --git a/src/main/kotlin/roomescape/auth/exception/AuthException.kt b/src/main/kotlin/roomescape/auth/exception/AuthException.kt index 735dcaca..55d9a839 100644 --- a/src/main/kotlin/roomescape/auth/exception/AuthException.kt +++ b/src/main/kotlin/roomescape/auth/exception/AuthException.kt @@ -1,8 +1,8 @@ package roomescape.auth.exception -import roomescape.common.exception.RoomException +import roomescape.common.exception.RoomescapeExceptionV2 class AuthException( override val errorCode: AuthErrorCode, override val message: String = errorCode.message -) : RoomException(errorCode, message) +) : RoomescapeExceptionV2(errorCode, message) diff --git a/src/main/kotlin/roomescape/common/exception/RoomException.kt b/src/main/kotlin/roomescape/common/exception/RoomException.kt deleted file mode 100644 index 51f6a6ec..00000000 --- a/src/main/kotlin/roomescape/common/exception/RoomException.kt +++ /dev/null @@ -1,6 +0,0 @@ -package roomescape.common.exception - -open class RoomException( - open val errorCode: ErrorCode, - override val message: String = errorCode.message -) : RuntimeException(message) diff --git a/src/main/kotlin/roomescape/common/exception/RoomescapeException.kt b/src/main/kotlin/roomescape/common/exception/RoomescapeException.kt index d6a8cb07..44d5673a 100644 --- a/src/main/kotlin/roomescape/common/exception/RoomescapeException.kt +++ b/src/main/kotlin/roomescape/common/exception/RoomescapeException.kt @@ -9,3 +9,8 @@ class RoomescapeException( ) : RuntimeException(errorType.description) { constructor(errorType: ErrorType, httpStatus: HttpStatusCode) : this(errorType, null, httpStatus) } + +open class RoomescapeExceptionV2( + open val errorCode: ErrorCode, + override val message: String = errorCode.message +) : RuntimeException(message) -- 2.47.2 From c6a5927b72f40b8e857d81370dafa0d9380ae16b Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 13:50:38 +0900 Subject: [PATCH 07/37] =?UTF-8?q?refactor:=20Admin=20/=20Login=20Intercept?= =?UTF-8?q?or=20=ED=86=B5=ED=95=A9=20=EB=B0=8F=20=EB=AF=B8=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20\@Throws=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/web/support/AuthInterceptors.kt | 98 ++++++------------- .../auth/web/support/MemberIdResolver.kt | 1 - .../roomescape/common/config/WebMvcConfig.kt | 9 +- 3 files changed, 33 insertions(+), 75 deletions(-) diff --git a/src/main/kotlin/roomescape/auth/web/support/AuthInterceptors.kt b/src/main/kotlin/roomescape/auth/web/support/AuthInterceptors.kt index 42c6e958..c7b92706 100644 --- a/src/main/kotlin/roomescape/auth/web/support/AuthInterceptors.kt +++ b/src/main/kotlin/roomescape/auth/web/support/AuthInterceptors.kt @@ -2,89 +2,51 @@ package roomescape.auth.web.support import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse -import org.springframework.http.HttpStatus import org.springframework.stereotype.Component import org.springframework.web.method.HandlerMethod import org.springframework.web.servlet.HandlerInterceptor +import roomescape.auth.exception.AuthErrorCode +import roomescape.auth.exception.AuthException import roomescape.auth.infrastructure.jwt.JwtHandler -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException import roomescape.member.business.MemberService import roomescape.member.infrastructure.persistence.MemberEntity -private fun Any.isIrrelevantWith(annotationType: Class): Boolean { - if (this !is HandlerMethod) { +@Component +class AuthInterceptor( + private val memberService: MemberService, + private val jwtHandler: JwtHandler +) : HandlerInterceptor { + override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { + if (handler !is HandlerMethod) { + return true + } + + val loginRequired = handler.getMethodAnnotation(LoginRequired::class.java) + val admin = handler.getMethodAnnotation(Admin::class.java) + + if (loginRequired == null && admin == null) { + return true + } + + val member: MemberEntity = findMember(request, response) + + if (admin != null && !member.isAdmin()) { + response.sendRedirect("/login") + throw AuthException(AuthErrorCode.ACCESS_DENIED) + } + return true } - return !this.hasMethodAnnotation(annotationType) -} - -@Component -class LoginInterceptor( - private val memberService: MemberService, - private val jwtHandler: JwtHandler -) : HandlerInterceptor { - - @Throws(Exception::class) - override fun preHandle( - request: HttpServletRequest, - response: HttpServletResponse, - handler: Any - ): Boolean { - if (handler.isIrrelevantWith(LoginRequired::class.java)) { - return true - } + private fun findMember(request: HttpServletRequest, response: HttpServletResponse): MemberEntity { try { val token: String? = request.accessTokenCookie().value val memberId: Long = jwtHandler.getMemberIdFromToken(token) - return memberService.existsById(memberId) - } catch (_: RoomescapeException) { + return memberService.findById(memberId) + } catch (e: Exception) { response.sendRedirect("/login") - throw RoomescapeException(ErrorType.LOGIN_REQUIRED, HttpStatus.FORBIDDEN) - } - } -} - -@Component -class AdminInterceptor( - private val memberService: MemberService, - private val jwtHandler: JwtHandler -) : HandlerInterceptor { - - @Throws(Exception::class) - override fun preHandle( - request: HttpServletRequest, - response: HttpServletResponse, - handler: Any - ): Boolean { - if (handler.isIrrelevantWith(Admin::class.java)) { - return true - } - - val member: MemberEntity? - - try { - val token: String? = request.accessTokenCookie().value - val memberId: Long = jwtHandler.getMemberIdFromToken(token) - member = memberService.findById(memberId) - } catch (_: RoomescapeException) { - response.sendRedirect("/login") - throw RoomescapeException(ErrorType.LOGIN_REQUIRED, HttpStatus.FORBIDDEN) - } - - with(member) { - if (this.isAdmin()) { - return true - } - - response.sendRedirect("/login") - throw RoomescapeException( - ErrorType.PERMISSION_DOES_NOT_EXIST, - String.format("[memberId: %d, Role: %s]", this.id, this.role), - HttpStatus.FORBIDDEN - ) + throw e } } } diff --git a/src/main/kotlin/roomescape/auth/web/support/MemberIdResolver.kt b/src/main/kotlin/roomescape/auth/web/support/MemberIdResolver.kt index dfd07804..57fec5ef 100644 --- a/src/main/kotlin/roomescape/auth/web/support/MemberIdResolver.kt +++ b/src/main/kotlin/roomescape/auth/web/support/MemberIdResolver.kt @@ -18,7 +18,6 @@ class MemberIdResolver( return parameter.hasParameterAnnotation(MemberId::class.java) } - @Throws(Exception::class) override fun resolveArgument( parameter: MethodParameter, mavContainer: ModelAndViewContainer?, diff --git a/src/main/kotlin/roomescape/common/config/WebMvcConfig.kt b/src/main/kotlin/roomescape/common/config/WebMvcConfig.kt index 2e1c2286..54739590 100644 --- a/src/main/kotlin/roomescape/common/config/WebMvcConfig.kt +++ b/src/main/kotlin/roomescape/common/config/WebMvcConfig.kt @@ -4,15 +4,13 @@ import org.springframework.context.annotation.Configuration import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.servlet.config.annotation.InterceptorRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer -import roomescape.auth.web.support.AdminInterceptor -import roomescape.auth.web.support.LoginInterceptor +import roomescape.auth.web.support.AuthInterceptor import roomescape.auth.web.support.MemberIdResolver @Configuration class WebMvcConfig( private val memberIdResolver: MemberIdResolver, - private val adminInterceptor: AdminInterceptor, - private val loginInterceptor: LoginInterceptor + private val authInterceptor: AuthInterceptor ) : WebMvcConfigurer { override fun addArgumentResolvers(resolvers: MutableList) { @@ -20,7 +18,6 @@ class WebMvcConfig( } override fun addInterceptors(registry: InterceptorRegistry) { - registry.addInterceptor(adminInterceptor) - registry.addInterceptor(loginInterceptor) + registry.addInterceptor(authInterceptor) } } -- 2.47.2 From 75afa45224c8375903a7b10d44049296edec984a Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 13:50:57 +0900 Subject: [PATCH 08/37] =?UTF-8?q?feat:=20=EC=83=88=EB=A1=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EB=90=9C=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=ED=95=B8=EB=93=A4=EB=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/ExceptionControllerAdvice.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/kotlin/roomescape/common/exception/ExceptionControllerAdvice.kt b/src/main/kotlin/roomescape/common/exception/ExceptionControllerAdvice.kt index 1f892236..90276979 100644 --- a/src/main/kotlin/roomescape/common/exception/ExceptionControllerAdvice.kt +++ b/src/main/kotlin/roomescape/common/exception/ExceptionControllerAdvice.kt @@ -9,11 +9,21 @@ import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice import roomescape.common.dto.response.CommonErrorResponse +import roomescape.common.dto.response.CommonErrorResponseV2 @RestControllerAdvice class ExceptionControllerAdvice( private val logger: KLogger = KotlinLogging.logger {} ) { + @ExceptionHandler(value = [RoomescapeExceptionV2::class]) + fun handleRoomException(e: RoomescapeExceptionV2): ResponseEntity { + logger.error(e) { "message: ${e.message}" } + + val errorCode: ErrorCode = e.errorCode + val httpStatus: Int = errorCode.httpStatus.value() + return ResponseEntity.status(httpStatus) + .body(CommonErrorResponseV2(errorCode, e.message)) + } @ExceptionHandler(value = [RoomescapeException::class]) fun handleRoomEscapeException(e: RoomescapeException): ResponseEntity { -- 2.47.2 From 3a3974f3446d18ac7f8316347740226230c3505d Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 14:02:35 +0900 Subject: [PATCH 09/37] =?UTF-8?q?feat:=20AuthInterceptor=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=B4=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=90=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...AuthInterceptors.kt => AuthInterceptor.kt} | 0 .../web/ReservationControllerTest.kt | 51 +++++++------------ .../theme/web/ThemeControllerTest.kt | 5 +- .../roomescape/util/RoomescapeApiTest.kt | 8 +-- 4 files changed, 24 insertions(+), 40 deletions(-) rename src/main/kotlin/roomescape/auth/web/support/{AuthInterceptors.kt => AuthInterceptor.kt} (100%) diff --git a/src/main/kotlin/roomescape/auth/web/support/AuthInterceptors.kt b/src/main/kotlin/roomescape/auth/web/support/AuthInterceptor.kt similarity index 100% rename from src/main/kotlin/roomescape/auth/web/support/AuthInterceptors.kt rename to src/main/kotlin/roomescape/auth/web/support/AuthInterceptor.kt diff --git a/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt b/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt index cf9afdef..01f5d2a7 100644 --- a/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt +++ b/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt @@ -17,11 +17,11 @@ import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.transaction.support.TransactionTemplate -import roomescape.auth.web.support.AdminInterceptor -import roomescape.auth.web.support.LoginInterceptor +import roomescape.auth.infrastructure.jwt.JwtHandler import roomescape.auth.web.support.MemberIdResolver import roomescape.common.exception.ErrorType import roomescape.common.exception.RoomescapeException +import roomescape.member.business.MemberService import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.member.infrastructure.persistence.Role import roomescape.payment.infrastructure.client.TossPaymentClient @@ -45,15 +45,15 @@ class ReservationControllerTest( @MockkBean lateinit var paymentClient: TossPaymentClient - @SpykBean - lateinit var loginInterceptor: LoginInterceptor - - @SpykBean - lateinit var adminInterceptor: AdminInterceptor - @SpykBean lateinit var memberIdResolver: MemberIdResolver + @SpykBean + lateinit var memberService: MemberService + + @MockkBean + lateinit var jwtHandler: JwtHandler + init { context("POST /reservations") { lateinit var member: MemberEntity @@ -737,31 +737,18 @@ class ReservationControllerTest( } } - if (member.isAdmin()) { - loginAsAdmin() - } else { - loginAsUser() - } - resolveMemberId(member.id!!) + every { + jwtHandler.getMemberIdFromToken(any()) + } returns member.id!! + + every { + memberService.findById(member.id!!) + } returns member + + every { + memberIdResolver.resolveArgument(any(), any(), any(), any()) + } returns member.id!! return member } - - private fun loginAsUser() { - every { - loginInterceptor.preHandle(any(), any(), any()) - } returns true - } - - private fun loginAsAdmin() { - every { - adminInterceptor.preHandle(any(), any(), any()) - } returns true - } - - private fun resolveMemberId(memberId: Long) { - every { - memberIdResolver.resolveArgument(any(), any(), any(), any()) - } returns memberId - } } diff --git a/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt b/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt index 817f5077..f4963788 100644 --- a/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt +++ b/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt @@ -11,6 +11,7 @@ import io.mockk.runs import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc +import roomescape.auth.exception.AuthErrorCode import roomescape.theme.business.ThemeService import roomescape.theme.infrastructure.persistence.ThemeRepository import roomescape.util.RoomescapeApiTest @@ -108,7 +109,7 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { body = request, ) { status { is3xxRedirection() } - jsonPath("$.errorType") { value("PERMISSION_DOES_NOT_EXIST") } + jsonPath("$.code") { value(AuthErrorCode.ACCESS_DENIED.errorCode) } } } } @@ -249,7 +250,7 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { endpoint = endpoint, ) { status { is3xxRedirection() } - jsonPath("$.errorType") { value("PERMISSION_DOES_NOT_EXIST") } + jsonPath("$.code") { value(AuthErrorCode.ACCESS_DENIED.errorCode) } } } } diff --git a/src/test/kotlin/roomescape/util/RoomescapeApiTest.kt b/src/test/kotlin/roomescape/util/RoomescapeApiTest.kt index e8a0088d..a35cdf23 100644 --- a/src/test/kotlin/roomescape/util/RoomescapeApiTest.kt +++ b/src/test/kotlin/roomescape/util/RoomescapeApiTest.kt @@ -11,8 +11,7 @@ import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.test.web.servlet.* import roomescape.auth.infrastructure.jwt.JwtHandler -import roomescape.auth.web.support.AdminInterceptor -import roomescape.auth.web.support.LoginInterceptor +import roomescape.auth.web.support.AuthInterceptor import roomescape.auth.web.support.MemberIdResolver import roomescape.common.config.JacksonConfig import roomescape.common.exception.ErrorType @@ -25,10 +24,7 @@ import roomescape.util.MemberFixture.NOT_LOGGED_IN_USERID abstract class RoomescapeApiTest : BehaviorSpec() { @SpykBean - private lateinit var AdminInterceptor: AdminInterceptor - - @SpykBean - private lateinit var loginInterceptor: LoginInterceptor + private lateinit var authInterceptor: AuthInterceptor @SpykBean private lateinit var memberIdResolver: MemberIdResolver -- 2.47.2 From 16426c9f99ed4cdfddeb28154af8d2de8c33f9e2 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 15:00:26 +0900 Subject: [PATCH 10/37] =?UTF-8?q?refactor:=20ExceptionHandler=EC=9D=98=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20=EC=98=88=EC=99=B8=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=A7=81=EC=9D=84=20=EC=83=88=EB=A1=9C=20=EC=A0=95=EC=9D=98?= =?UTF-8?q?=ED=95=9C=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/ExceptionControllerAdvice.kt | 30 ++++++------ .../roomescape/auth/web/AuthControllerTest.kt | 46 ++++++++----------- 2 files changed, 35 insertions(+), 41 deletions(-) diff --git a/src/main/kotlin/roomescape/common/exception/ExceptionControllerAdvice.kt b/src/main/kotlin/roomescape/common/exception/ExceptionControllerAdvice.kt index 90276979..ef6a0914 100644 --- a/src/main/kotlin/roomescape/common/exception/ExceptionControllerAdvice.kt +++ b/src/main/kotlin/roomescape/common/exception/ExceptionControllerAdvice.kt @@ -2,7 +2,6 @@ package roomescape.common.exception import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.http.converter.HttpMessageNotReadableException import org.springframework.web.bind.MethodArgumentNotValidException @@ -20,8 +19,8 @@ class ExceptionControllerAdvice( logger.error(e) { "message: ${e.message}" } val errorCode: ErrorCode = e.errorCode - val httpStatus: Int = errorCode.httpStatus.value() - return ResponseEntity.status(httpStatus) + return ResponseEntity + .status(errorCode.httpStatus) .body(CommonErrorResponseV2(errorCode, e.message)) } @@ -35,32 +34,35 @@ class ExceptionControllerAdvice( } @ExceptionHandler(value = [HttpMessageNotReadableException::class]) - fun handleHttpMessageNotReadableException(e: HttpMessageNotReadableException): ResponseEntity { + fun handleHttpMessageNotReadableException(e: HttpMessageNotReadableException): ResponseEntity { logger.error(e) { "message: ${e.message}" } + val errorCode: ErrorCode = CommonErrorCode.INVALID_INPUT_VALUE return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body(CommonErrorResponse(ErrorType.INVALID_REQUEST_DATA_TYPE)) + .status(errorCode.httpStatus) + .body(CommonErrorResponseV2(errorCode, e.message ?: errorCode.message)) } @ExceptionHandler(value = [MethodArgumentNotValidException::class]) - fun handleMethodArgumentNotValidException(e: MethodArgumentNotValidException): ResponseEntity { - val messages: String = e.bindingResult.allErrors + fun handleMethodArgumentNotValidException(e: MethodArgumentNotValidException): ResponseEntity { + val message: String = e.bindingResult.allErrors .mapNotNull { it.defaultMessage } .joinToString(", ") - logger.error(e) { "message: $messages" } + logger.error(e) { "message: $message" } + val errorCode: ErrorCode = CommonErrorCode.INVALID_INPUT_VALUE return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body(CommonErrorResponse(ErrorType.INVALID_REQUEST_DATA, messages)) + .status(errorCode.httpStatus) + .body(CommonErrorResponseV2(errorCode, message)) } @ExceptionHandler(value = [Exception::class]) - fun handleException(e: Exception): ResponseEntity { + fun handleException(e: Exception): ResponseEntity { logger.error(e) { "message: ${e.message}" } + val errorCode: ErrorCode = CommonErrorCode.UNEXPECTED_SERVER_ERROR return ResponseEntity - .status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(CommonErrorResponse(ErrorType.UNEXPECTED_ERROR)) + .status(errorCode.httpStatus) + .body(CommonErrorResponseV2(errorCode)) } } diff --git a/src/test/kotlin/roomescape/auth/web/AuthControllerTest.kt b/src/test/kotlin/roomescape/auth/web/AuthControllerTest.kt index 8fa954f9..d5aa150f 100644 --- a/src/test/kotlin/roomescape/auth/web/AuthControllerTest.kt +++ b/src/test/kotlin/roomescape/auth/web/AuthControllerTest.kt @@ -4,18 +4,19 @@ import com.ninjasquad.springmockk.SpykBean import io.mockk.every import org.hamcrest.Matchers.containsString import org.hamcrest.Matchers.equalTo -import org.springframework.beans.factory.annotation.Autowired 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.service.AuthService +import roomescape.common.exception.CommonErrorCode +import roomescape.common.exception.ErrorCode import roomescape.common.exception.ErrorType import roomescape.util.MemberFixture import roomescape.util.RoomescapeApiTest @WebMvcTest(controllers = [AuthController::class]) class AuthControllerTest( - @Autowired mockMvc: MockMvc + val mockMvc: MockMvc ) : RoomescapeApiTest() { @SpykBean @@ -71,32 +72,23 @@ class AuthControllerTest( } } } + When("입력 값이 잘못되면") { + val expectedErrorCode: ErrorCode = CommonErrorCode.INVALID_INPUT_VALUE - When("잘못된 요청을 보내면 400 에러를 응답한다.") { - - Then("이메일 형식이 잘못된 경우") { - val invalidRequest: LoginRequest = userRequest.copy(email = "invalid") - - runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = invalidRequest, - ) { - status { isBadRequest() } - jsonPath("$.message", containsString("이메일 형식이 일치하지 않습니다.")) - } - } - - Then("비밀번호가 공백인 경우") { - val invalidRequest = userRequest.copy(password = " ") - - runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = invalidRequest, - ) { - status { isBadRequest() } - jsonPath("$.message", containsString("비밀번호는 공백일 수 없습니다.")) + Then("400 에러를 응답한다") { + listOf( + userRequest.copy(email = "invalid"), + userRequest.copy(password = " "), + "{\"email\": \"null\", \"password\": \"null\"}" + ).forEach { + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = it, + ) { + status { isEqualTo(expectedErrorCode.httpStatus.value()) } + jsonPath("$.code", equalTo(expectedErrorCode.errorCode)) + } } } } -- 2.47.2 From 357f53deb477a377dd85181f76f61e1e64792697 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 15:01:38 +0900 Subject: [PATCH 11/37] =?UTF-8?q?refactor:=20AuthInterceptor=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20MemberService=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/roomescape/member/business/MemberService.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/kotlin/roomescape/member/business/MemberService.kt b/src/main/kotlin/roomescape/member/business/MemberService.kt index 3be407ed..2dda221c 100644 --- a/src/main/kotlin/roomescape/member/business/MemberService.kt +++ b/src/main/kotlin/roomescape/member/business/MemberService.kt @@ -36,8 +36,5 @@ class MemberService( String.format("[email: %s, password: %s]", email, password), HttpStatus.BAD_REQUEST ) - - fun existsById(memberId: Long): Boolean = memberRepository.existsById(memberId) - } -- 2.47.2 From 95e952c215ce561b305a8ca18f01179652068820 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 15:09:36 +0900 Subject: [PATCH 12/37] =?UTF-8?q?refactor:=20JwtHandler=EC=97=90=EC=84=9C?= =?UTF-8?q?=EC=9D=98=20=EB=B9=88=20=ED=86=A0=ED=81=B0=EA=B0=92=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt | 2 ++ .../kotlin/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt b/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt index 4437021b..29e7b38b 100644 --- a/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt +++ b/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt @@ -37,6 +37,8 @@ class JwtHandler( .body .get(MEMBER_ID_CLAIM_KEY, Number::class.java) .toLong() + } catch (_: IllegalArgumentException) { + throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND) } catch (_: ExpiredJwtException) { throw AuthException(AuthErrorCode.EXPIRED_TOKEN) } catch (_: Exception) { diff --git a/src/test/kotlin/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt b/src/test/kotlin/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt index 91b9f376..a7ee570d 100644 --- a/src/test/kotlin/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt +++ b/src/test/kotlin/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt @@ -41,7 +41,7 @@ class JwtHandlerTest : FunSpec({ test("토큰이 빈 값이면 예외를 던진다.") { shouldThrow { jwtHandler.getMemberIdFromToken("") - }.errorCode shouldBe AuthErrorCode.INVALID_TOKEN + }.errorCode shouldBe AuthErrorCode.TOKEN_NOT_FOUND } test("시크릿 키가 잘못된 경우 예외를 던진다.") { -- 2.47.2 From 8a5330b917af253fad509ecf73dceee02109efc8 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 15:10:42 +0900 Subject: [PATCH 13/37] =?UTF-8?q?refactor:=20AuthErrorCode=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=AF=B8=EC=82=AC=EC=9A=A9=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/roomescape/auth/exception/AuthErrorCode.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/roomescape/auth/exception/AuthErrorCode.kt b/src/main/kotlin/roomescape/auth/exception/AuthErrorCode.kt index 6a502945..9ebf8853 100644 --- a/src/main/kotlin/roomescape/auth/exception/AuthErrorCode.kt +++ b/src/main/kotlin/roomescape/auth/exception/AuthErrorCode.kt @@ -8,11 +8,8 @@ enum class AuthErrorCode( override val errorCode: String, override val message: String, ): ErrorCode { - LOGIN_REQUIRED(HttpStatus.UNAUTHORIZED, "A001", "로그인이 필요해요."), - LOGIN_FAILED(HttpStatus.UNAUTHORIZED, "A002", "로그인에 실패했어요."), - TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "A003", "인증 토큰이 없어요."), - INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "A004", "유효하지 않은 토큰이에요."), - EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "A005", "토큰이 만료됐어요."), - USER_NOT_FOUND_FROM_TOKEN(HttpStatus.UNAUTHORIZED, "A006", "토큰으로 회원 정보를 찾을 수 없어요."), - ACCESS_DENIED(HttpStatus.FORBIDDEN, "A007", "접근 권한이 없어요."), + TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "A001", "인증 토큰이 없어요."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "A002", "유효하지 않은 토큰이에요."), + EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "A003", "토큰이 만료됐어요."), + ACCESS_DENIED(HttpStatus.FORBIDDEN, "A004", "접근 권한이 없어요."), } -- 2.47.2 From 4820dcfca85b222cc6e281e2c4125a716defd9fb Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 15:12:38 +0900 Subject: [PATCH 14/37] =?UTF-8?q?refactor:=20Dto=EC=97=90=EC=84=9C?= =?UTF-8?q?=EC=9D=98=20annotation=20use-target=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/roomescape/auth/web/AuthDTO.kt | 2 +- .../kotlin/roomescape/member/web/MemberDTO.kt | 4 ++-- .../reservation/web/ReservationRequest.kt | 8 ++++---- .../reservation/web/ReservationResponse.kt | 8 ++++---- .../kotlin/roomescape/reservation/web/TimeDTO.kt | 16 ++++++++-------- src/main/kotlin/roomescape/theme/web/ThemeDTO.kt | 16 ++++++++-------- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/main/kotlin/roomescape/auth/web/AuthDTO.kt b/src/main/kotlin/roomescape/auth/web/AuthDTO.kt index 8b910d01..6910fe44 100644 --- a/src/main/kotlin/roomescape/auth/web/AuthDTO.kt +++ b/src/main/kotlin/roomescape/auth/web/AuthDTO.kt @@ -9,7 +9,7 @@ data class LoginResponse( ) data class LoginCheckResponse( - @field:Schema(description = "로그인된 회원의 이름") + @Schema(description = "로그인된 회원의 이름") val name: String ) diff --git a/src/main/kotlin/roomescape/member/web/MemberDTO.kt b/src/main/kotlin/roomescape/member/web/MemberDTO.kt index f6d8a43d..00c00c8f 100644 --- a/src/main/kotlin/roomescape/member/web/MemberDTO.kt +++ b/src/main/kotlin/roomescape/member/web/MemberDTO.kt @@ -9,10 +9,10 @@ fun MemberEntity.toRetrieveResponse(): MemberRetrieveResponse = MemberRetrieveRe ) data class MemberRetrieveResponse( - @field:Schema(description = "회원 식별자") + @Schema(description = "회원 식별자") val id: Long, - @field:Schema(description = "회원 이름") + @Schema(description = "회원 이름") val name: String ) diff --git a/src/main/kotlin/roomescape/reservation/web/ReservationRequest.kt b/src/main/kotlin/roomescape/reservation/web/ReservationRequest.kt index bb69cb31..2c5075bb 100644 --- a/src/main/kotlin/roomescape/reservation/web/ReservationRequest.kt +++ b/src/main/kotlin/roomescape/reservation/web/ReservationRequest.kt @@ -16,16 +16,16 @@ data class ReservationCreateWithPaymentRequest( val timeId: Long, val themeId: Long, - @field:Schema(description = "결제 위젯을 통해 받은 결제 키") + @Schema(description = "결제 위젯을 통해 받은 결제 키") val paymentKey: String, - @field:Schema(description = "결제 위젯을 통해 받은 주문번호.") + @Schema(description = "결제 위젯을 통해 받은 주문번호.") val orderId: String, - @field:Schema(description = "결제 위젯을 통해 받은 결제 금액") + @Schema(description = "결제 위젯을 통해 받은 결제 금액") val amount: Long, - @field:Schema(description = "결제 타입", example = "NORMAL") + @Schema(description = "결제 타입", example = "NORMAL") val paymentType: String ) diff --git a/src/main/kotlin/roomescape/reservation/web/ReservationResponse.kt b/src/main/kotlin/roomescape/reservation/web/ReservationResponse.kt index 69778c31..8cd2d26a 100644 --- a/src/main/kotlin/roomescape/reservation/web/ReservationResponse.kt +++ b/src/main/kotlin/roomescape/reservation/web/ReservationResponse.kt @@ -17,16 +17,16 @@ data class MyReservationRetrieveResponse( val date: LocalDate, val time: LocalTime, val status: ReservationStatus, - @field:Schema(description = "대기 순번. 확정된 예약은 0의 값을 가집니다.") + @Schema(description = "대기 순번. 확정된 예약은 0의 값을 가집니다.") val rank: Long, - @field:Schema(description = "결제 키. 결제가 완료된 예약에만 값이 존재합니다.") + @Schema(description = "결제 키. 결제가 완료된 예약에만 값이 존재합니다.") val paymentKey: String?, - @field:Schema(description = "결제 금액. 결제가 완료된 예약에만 값이 존재합니다.") + @Schema(description = "결제 금액. 결제가 완료된 예약에만 값이 존재합니다.") val amount: Long? ) data class MyReservationRetrieveListResponse( - @field:Schema(description = "현재 로그인한 회원의 예약 및 대기 목록") + @Schema(description = "현재 로그인한 회원의 예약 및 대기 목록") val reservations: List ) diff --git a/src/main/kotlin/roomescape/reservation/web/TimeDTO.kt b/src/main/kotlin/roomescape/reservation/web/TimeDTO.kt index 847fd855..b6f121eb 100644 --- a/src/main/kotlin/roomescape/reservation/web/TimeDTO.kt +++ b/src/main/kotlin/roomescape/reservation/web/TimeDTO.kt @@ -6,26 +6,26 @@ import java.time.LocalTime @Schema(name = "예약 시간 저장 요청", description = "예약 시간 저장 요청시 사용됩니다.") data class TimeCreateRequest( - @field:Schema(description = "시간", type = "string", example = "09:00") + @Schema(description = "시간", type = "string", example = "09:00") val startAt: LocalTime ) @Schema(name = "예약 시간 정보", description = "예약 시간 추가 및 조회 응답시 사용됩니다.") data class TimeCreateResponse( - @field:Schema(description = "시간 식별자") + @Schema(description = "시간 식별자") val id: Long, - @field:Schema(description = "시간") + @Schema(description = "시간") val startAt: LocalTime ) fun TimeEntity.toCreateResponse(): TimeCreateResponse = TimeCreateResponse(this.id!!, this.startAt) data class TimeRetrieveResponse( - @field:Schema(description = "시간 식별자.") + @Schema(description = "시간 식별자.") val id: Long, - @field:Schema(description = "시간") + @Schema(description = "시간") val startAt: LocalTime ) @@ -40,13 +40,13 @@ fun List.toRetrieveListResponse(): TimeRetrieveListResponse = TimeRe ) data class TimeWithAvailabilityResponse( - @field:Schema(description = "시간 식별자") + @Schema(description = "시간 식별자") val id: Long, - @field:Schema(description = "시간") + @Schema(description = "시간") val startAt: LocalTime, - @field:Schema(description = "예약 가능 여부") + @Schema(description = "예약 가능 여부") val isAvailable: Boolean ) diff --git a/src/main/kotlin/roomescape/theme/web/ThemeDTO.kt b/src/main/kotlin/roomescape/theme/web/ThemeDTO.kt index 5fa35711..303ab9e5 100644 --- a/src/main/kotlin/roomescape/theme/web/ThemeDTO.kt +++ b/src/main/kotlin/roomescape/theme/web/ThemeDTO.kt @@ -8,17 +8,17 @@ import roomescape.theme.infrastructure.persistence.ThemeEntity @Schema(name = "테마 저장 요청", description = "테마 정보를 저장할 때 사용합니다.") data class ThemeRequest( - @field:Schema(description = "필수 값이며, 최대 20글자까지 입력 가능합니다.") + @Schema(description = "필수 값이며, 최대 20글자까지 입력 가능합니다.") @NotBlank @Size(max = 20, message = "테마의 이름은 1~20글자 사이여야 합니다.") val name: String, - @field:Schema(description = "필수 값이며, 최대 100글자까지 입력 가능합니다.") + @Schema(description = "필수 값이며, 최대 100글자까지 입력 가능합니다.") @NotBlank @Size(max = 100, message = "테마의 설명은 1~100글자 사이여야 합니다.") val description: String, - @field:Schema(description = "필수 값이며, 썸네일 이미지 URL 을 입력해주세요.") + @Schema(description = "필수 값이며, 썸네일 이미지 URL 을 입력해주세요.") @NotBlank @URL val thumbnail: String @@ -26,16 +26,16 @@ data class ThemeRequest( @Schema(name = "테마 정보", description = "테마 추가 및 조회 응답에 사용됩니다.") data class ThemeResponse( - @field:Schema(description = "테마 번호. 테마를 식별할 때 사용합니다.") + @Schema(description = "테마 번호. 테마를 식별할 때 사용합니다.") val id: Long, - @field:Schema(description = "테마 이름. 중복을 허용하지 않습니다.") + @Schema(description = "테마 이름. 중복을 허용하지 않습니다.") val name: String, - @field:Schema(description = "테마 설명") + @Schema(description = "테마 설명") val description: String, - @field:Schema(description = "테마 썸네일 이미지 URL") + @Schema(description = "테마 썸네일 이미지 URL") val thumbnail: String ) @@ -48,7 +48,7 @@ fun ThemeEntity.toResponse(): ThemeResponse = ThemeResponse( @Schema(name = "테마 목록 조회 응답", description = "모든 테마 목록 조회 응답시 사용됩니다.") data class ThemesResponse( - @field:Schema(description = "모든 테마 목록") + @Schema(description = "모든 테마 목록") val themes: List ) -- 2.47.2 From f1ebc0f57db6d2a4ea75531c4a5cad02602f71d8 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 15:59:17 +0900 Subject: [PATCH 15/37] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EC=97=90=EC=84=9C=EC=9D=98=20?= =?UTF-8?q?=EC=83=88=EB=A1=9C=EC=9A=B4=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20=EB=A1=9C=EC=A7=81=20DSL=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/business/MemberService.kt | 33 ++++++++----------- .../member/exception/MemberErrorCode.kt | 12 +++++++ .../member/exception/MemberException.kt | 8 +++++ 3 files changed, 33 insertions(+), 20 deletions(-) create mode 100644 src/main/kotlin/roomescape/member/exception/MemberErrorCode.kt create mode 100644 src/main/kotlin/roomescape/member/exception/MemberException.kt diff --git a/src/main/kotlin/roomescape/member/business/MemberService.kt b/src/main/kotlin/roomescape/member/business/MemberService.kt index 2dda221c..8ff4072e 100644 --- a/src/main/kotlin/roomescape/member/business/MemberService.kt +++ b/src/main/kotlin/roomescape/member/business/MemberService.kt @@ -1,11 +1,10 @@ package roomescape.member.business import org.springframework.data.repository.findByIdOrNull -import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException +import roomescape.member.exception.MemberErrorCode +import roomescape.member.exception.MemberException import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.member.infrastructure.persistence.MemberRepository import roomescape.member.web.MemberRetrieveListResponse @@ -17,24 +16,18 @@ class MemberService( private val memberRepository: MemberRepository ) { fun findMembers(): MemberRetrieveListResponse = MemberRetrieveListResponse( - memberRepository.findAll() - .map { it.toRetrieveResponse() } - .toList() + members = memberRepository.findAll().map { it.toRetrieveResponse() } ) - fun findById(memberId: Long): MemberEntity = memberRepository.findByIdOrNull(memberId) - ?: throw RoomescapeException( - ErrorType.MEMBER_NOT_FOUND, - String.format("[memberId: %d]", memberId), - HttpStatus.BAD_REQUEST - ) + fun findById(memberId: Long): MemberEntity = fetchOrThrow { + memberRepository.findByIdOrNull(memberId) + } - fun findByEmailAndPassword(email: String, password: String): MemberEntity = - memberRepository.findByEmailAndPassword(email, password) - ?: throw RoomescapeException( - ErrorType.MEMBER_NOT_FOUND, - String.format("[email: %s, password: %s]", email, password), - HttpStatus.BAD_REQUEST - ) + fun findByEmailAndPassword(email: String, password: String): MemberEntity = fetchOrThrow { + memberRepository.findByEmailAndPassword(email, password) + } + + private fun fetchOrThrow(block: () -> MemberEntity?): MemberEntity { + return block() ?: 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 new file mode 100644 index 00000000..3b365311 --- /dev/null +++ b/src/main/kotlin/roomescape/member/exception/MemberErrorCode.kt @@ -0,0 +1,12 @@ +package roomescape.member.exception + +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 +) : ErrorCode { + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "M001", "회원을 찾을 수 없어요.") +} diff --git a/src/main/kotlin/roomescape/member/exception/MemberException.kt b/src/main/kotlin/roomescape/member/exception/MemberException.kt new file mode 100644 index 00000000..62bea601 --- /dev/null +++ b/src/main/kotlin/roomescape/member/exception/MemberException.kt @@ -0,0 +1,8 @@ +package roomescape.member.exception + +import roomescape.common.exception.RoomescapeExceptionV2 + +class MemberException( + override val errorCode: MemberErrorCode, + override val message: String = errorCode.message +) : RoomescapeExceptionV2(errorCode, message) -- 2.47.2 From 9683ed946e63441a1a72c2691f0800e2a1d05e77 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 16:00:10 +0900 Subject: [PATCH 16/37] =?UTF-8?q?refactor:=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EC=97=90=EC=84=9C=EC=9D=98=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=84=B8=EB=B6=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 현재는 회원 도메인의 예외를 그대로 던지는데, 이를 인증 예외로 처리 --- .../auth/exception/AuthErrorCode.kt | 4 +++- .../roomescape/auth/service/AuthService.kt | 24 +++++++++++++++---- .../auth/business/AuthServiceTest.kt | 12 +++++----- .../roomescape/auth/web/AuthControllerTest.kt | 16 +++++++------ 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/roomescape/auth/exception/AuthErrorCode.kt b/src/main/kotlin/roomescape/auth/exception/AuthErrorCode.kt index 9ebf8853..21318e51 100644 --- a/src/main/kotlin/roomescape/auth/exception/AuthErrorCode.kt +++ b/src/main/kotlin/roomescape/auth/exception/AuthErrorCode.kt @@ -7,9 +7,11 @@ enum class AuthErrorCode( override val httpStatus: HttpStatus, override val errorCode: String, override val message: String, -): ErrorCode { +) : ErrorCode { TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "A001", "인증 토큰이 없어요."), INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "A002", "유효하지 않은 토큰이에요."), EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "A003", "토큰이 만료됐어요."), ACCESS_DENIED(HttpStatus.FORBIDDEN, "A004", "접근 권한이 없어요."), + LOGIN_FAILED(HttpStatus.UNAUTHORIZED, "A005", "이메일과 비밀번호를 확인해주세요."), + UNIDENTIFIABLE_MEMBER(HttpStatus.UNAUTHORIZED, "A006", "회원 정보를 찾을 수 없어요."), } diff --git a/src/main/kotlin/roomescape/auth/service/AuthService.kt b/src/main/kotlin/roomescape/auth/service/AuthService.kt index ce3d0e81..2c07146b 100644 --- a/src/main/kotlin/roomescape/auth/service/AuthService.kt +++ b/src/main/kotlin/roomescape/auth/service/AuthService.kt @@ -1,6 +1,8 @@ package roomescape.auth.service import org.springframework.stereotype.Service +import roomescape.auth.exception.AuthErrorCode +import roomescape.auth.exception.AuthException import roomescape.auth.infrastructure.jwt.JwtHandler import roomescape.auth.web.LoginCheckResponse import roomescape.auth.web.LoginRequest @@ -14,10 +16,9 @@ class AuthService( private val jwtHandler: JwtHandler ) { fun login(request: LoginRequest): LoginResponse { - val member: MemberEntity = memberService.findByEmailAndPassword( - request.email, - request.password - ) + val member: MemberEntity = fetchMemberOrThrow(AuthErrorCode.LOGIN_FAILED) { + memberService.findByEmailAndPassword(request.email, request.password) + } val accessToken: String = jwtHandler.createToken(member.id!!) @@ -25,8 +26,21 @@ class AuthService( } fun checkLogin(memberId: Long): LoginCheckResponse { - val member = memberService.findById(memberId) + val member: MemberEntity = fetchMemberOrThrow(AuthErrorCode.UNIDENTIFIABLE_MEMBER) { + memberService.findById(memberId) + } return LoginCheckResponse(member.name) } + + private fun fetchMemberOrThrow( + errorCode: AuthErrorCode, + block: () -> MemberEntity + ): MemberEntity { + try { + return block() + } catch (_: Exception) { + throw AuthException(errorCode) + } + } } diff --git a/src/test/kotlin/roomescape/auth/business/AuthServiceTest.kt b/src/test/kotlin/roomescape/auth/business/AuthServiceTest.kt index 5f37c79e..2b4c6a5b 100644 --- a/src/test/kotlin/roomescape/auth/business/AuthServiceTest.kt +++ b/src/test/kotlin/roomescape/auth/business/AuthServiceTest.kt @@ -7,10 +7,10 @@ import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockk 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.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException import roomescape.member.business.MemberService import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.member.infrastructure.persistence.MemberRepository @@ -45,11 +45,11 @@ class AuthServiceTest : BehaviorSpec({ memberRepository.findByEmailAndPassword(request.email, request.password) } returns null - val exception = shouldThrow { + val exception = shouldThrow { authService.login(request) } - exception.errorType shouldBe ErrorType.MEMBER_NOT_FOUND + exception.errorCode shouldBe AuthErrorCode.LOGIN_FAILED } } } @@ -71,11 +71,11 @@ class AuthServiceTest : BehaviorSpec({ Then("회원이 없다면 예외를 던진다.") { every { memberRepository.findByIdOrNull(userId) } returns null - val exception = shouldThrow { + val exception = shouldThrow { authService.checkLogin(userId) } - exception.errorType shouldBe ErrorType.MEMBER_NOT_FOUND + exception.errorCode shouldBe AuthErrorCode.UNIDENTIFIABLE_MEMBER } } } diff --git a/src/test/kotlin/roomescape/auth/web/AuthControllerTest.kt b/src/test/kotlin/roomescape/auth/web/AuthControllerTest.kt index d5aa150f..9fc7b3f3 100644 --- a/src/test/kotlin/roomescape/auth/web/AuthControllerTest.kt +++ b/src/test/kotlin/roomescape/auth/web/AuthControllerTest.kt @@ -7,10 +7,10 @@ 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.exception.AuthErrorCode import roomescape.auth.service.AuthService import roomescape.common.exception.CommonErrorCode import roomescape.common.exception.ErrorCode -import roomescape.common.exception.ErrorType import roomescape.util.MemberFixture import roomescape.util.RoomescapeApiTest @@ -61,14 +61,15 @@ class AuthControllerTest( memberRepository.findByEmailAndPassword(userRequest.email, userRequest.password) } returns null - Then("400 에러를 응답한다") { + Then("에러 응답") { + val expectedError = AuthErrorCode.LOGIN_FAILED runPostTest( mockMvc = mockMvc, endpoint = endpoint, body = userRequest, ) { - status { isBadRequest() } - jsonPath("$.errorType", equalTo(ErrorType.MEMBER_NOT_FOUND.name)) + status { isEqualTo(expectedError.httpStatus.value()) } + jsonPath("$.code", equalTo(expectedError.errorCode)) } } } @@ -117,13 +118,14 @@ class AuthControllerTest( every { jwtHandler.getMemberIdFromToken(any()) } returns invalidMemberId every { memberRepository.findByIdOrNull(invalidMemberId) } returns null - Then("400 에러를 응답한다.") { + Then("에러 응답.") { + val expectedError = AuthErrorCode.UNIDENTIFIABLE_MEMBER runGetTest( mockMvc = mockMvc, endpoint = endpoint, ) { - status { isBadRequest() } - jsonPath("$.errorType", equalTo(ErrorType.MEMBER_NOT_FOUND.name)) + status { isEqualTo(expectedError.httpStatus.value()) } + jsonPath("$.code", equalTo(expectedError.errorCode)) } } } -- 2.47.2 From ee56e44bf8585888dd5fc511ae444317a27b1cae Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 16:03:09 +0900 Subject: [PATCH 17/37] =?UTF-8?q?refactor:=20RoomescapeApiTest=EC=97=90=20?= =?UTF-8?q?=EC=83=88=EB=A1=9C=EC=9A=B4=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/kotlin/roomescape/util/RoomescapeApiTest.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/test/kotlin/roomescape/util/RoomescapeApiTest.kt b/src/test/kotlin/roomescape/util/RoomescapeApiTest.kt index a35cdf23..e1874a72 100644 --- a/src/test/kotlin/roomescape/util/RoomescapeApiTest.kt +++ b/src/test/kotlin/roomescape/util/RoomescapeApiTest.kt @@ -7,15 +7,14 @@ import io.kotest.core.spec.style.BehaviorSpec import io.mockk.every import org.springframework.data.repository.findByIdOrNull import org.springframework.http.HttpHeaders -import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.test.web.servlet.* +import roomescape.auth.exception.AuthErrorCode +import roomescape.auth.exception.AuthException import roomescape.auth.infrastructure.jwt.JwtHandler import roomescape.auth.web.support.AuthInterceptor import roomescape.auth.web.support.MemberIdResolver import roomescape.common.config.JacksonConfig -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException import roomescape.member.business.MemberService import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.member.infrastructure.persistence.MemberRepository @@ -101,7 +100,7 @@ abstract class RoomescapeApiTest : BehaviorSpec() { fun doNotLogin() { every { jwtHandler.getMemberIdFromToken(any()) - } throws RoomescapeException(ErrorType.INVALID_TOKEN, HttpStatus.UNAUTHORIZED) + } throws AuthException(AuthErrorCode.INVALID_TOKEN) every { memberRepository.existsById(NOT_LOGGED_IN_USERID) } returns false every { memberRepository.findByIdOrNull(NOT_LOGGED_IN_USERID) } returns null -- 2.47.2 From 821f379e78aa71f2988ad8f0f6eba92c5246ff85 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 16:43:31 +0900 Subject: [PATCH 18/37] =?UTF-8?q?refactor:=20theme=20=EB=82=B4=EC=9D=98=20?= =?UTF-8?q?DTO=20=EC=9D=B4=EB=A6=84=20=EC=BB=A8=EB=B2=A4=EC=85=98=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/web/ReservationResponse.kt | 4 +- .../roomescape/theme/business/ThemeService.kt | 6 +-- .../kotlin/roomescape/theme/docs/ThemeAPI.kt | 14 +++---- .../roomescape/theme/web/ThemeController.kt | 14 +++---- .../kotlin/roomescape/theme/web/ThemeDTO.kt | 39 +++++++------------ .../theme/business/ThemeServiceTest.kt | 4 +- .../theme/web/ThemeControllerTest.kt | 12 +++--- 7 files changed, 42 insertions(+), 51 deletions(-) diff --git a/src/main/kotlin/roomescape/reservation/web/ReservationResponse.kt b/src/main/kotlin/roomescape/reservation/web/ReservationResponse.kt index 8cd2d26a..711eabd6 100644 --- a/src/main/kotlin/roomescape/reservation/web/ReservationResponse.kt +++ b/src/main/kotlin/roomescape/reservation/web/ReservationResponse.kt @@ -6,7 +6,7 @@ import roomescape.member.web.MemberRetrieveResponse import roomescape.member.web.toRetrieveResponse import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationStatus -import roomescape.theme.web.ThemeResponse +import roomescape.theme.web.ThemeRetrieveResponse import roomescape.theme.web.toResponse import java.time.LocalDate import java.time.LocalTime @@ -41,7 +41,7 @@ data class ReservationRetrieveResponse( val time: TimeCreateResponse, @field:JsonProperty("theme") - val theme: ThemeResponse, + val theme: ThemeRetrieveResponse, val status: ReservationStatus ) diff --git a/src/main/kotlin/roomescape/theme/business/ThemeService.kt b/src/main/kotlin/roomescape/theme/business/ThemeService.kt index 91708c13..8a31b170 100644 --- a/src/main/kotlin/roomescape/theme/business/ThemeService.kt +++ b/src/main/kotlin/roomescape/theme/business/ThemeService.kt @@ -27,11 +27,11 @@ class ThemeService( ) @Transactional(readOnly = true) - fun findThemes(): ThemesResponse = themeRepository.findAll() + fun findThemes(): ThemeRetrieveListResponse = themeRepository.findAll() .toResponse() @Transactional(readOnly = true) - fun findMostReservedThemes(count: Int): ThemesResponse { + fun findMostReservedThemes(count: Int): ThemeRetrieveListResponse { val today = LocalDate.now() val startDate = today.minusDays(7) val endDate = today.minusDays(1) @@ -41,7 +41,7 @@ class ThemeService( } @Transactional - fun createTheme(request: ThemeRequest): ThemeResponse { + fun createTheme(request: ThemeCreateRequest): ThemeRetrieveResponse { if (themeRepository.existsByName(request.name)) { throw RoomescapeException( ErrorType.THEME_DUPLICATED, diff --git a/src/main/kotlin/roomescape/theme/docs/ThemeAPI.kt b/src/main/kotlin/roomescape/theme/docs/ThemeAPI.kt index d971884b..3012aaa7 100644 --- a/src/main/kotlin/roomescape/theme/docs/ThemeAPI.kt +++ b/src/main/kotlin/roomescape/theme/docs/ThemeAPI.kt @@ -13,9 +13,9 @@ import org.springframework.web.bind.annotation.RequestParam import roomescape.auth.web.support.Admin import roomescape.auth.web.support.LoginRequired import roomescape.common.dto.response.CommonApiResponse -import roomescape.theme.web.ThemeRequest -import roomescape.theme.web.ThemeResponse -import roomescape.theme.web.ThemesResponse +import roomescape.theme.web.ThemeCreateRequest +import roomescape.theme.web.ThemeRetrieveResponse +import roomescape.theme.web.ThemeRetrieveListResponse @Tag(name = "5. 테마 API", description = "테마를 조회 / 추가 / 삭제할 때 사용합니다.") interface ThemeAPI { @@ -23,13 +23,13 @@ interface ThemeAPI { @LoginRequired @Operation(summary = "모든 테마 조회", description = "모든 테마를 조회합니다.", tags = ["로그인이 필요한 API"]) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) - fun findThemes(): ResponseEntity> + fun findThemes(): ResponseEntity> @Operation(summary = "가장 많이 예약된 테마 조회") @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) fun findMostReservedThemes( @RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int - ): ResponseEntity> + ): ResponseEntity> @Admin @Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"]) @@ -37,8 +37,8 @@ interface ThemeAPI { ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true), ) fun createTheme( - @Valid @RequestBody request: ThemeRequest, - ): ResponseEntity> + @Valid @RequestBody request: ThemeCreateRequest, + ): ResponseEntity> @Admin @Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"]) diff --git a/src/main/kotlin/roomescape/theme/web/ThemeController.kt b/src/main/kotlin/roomescape/theme/web/ThemeController.kt index 05820cb2..c0be7fc1 100644 --- a/src/main/kotlin/roomescape/theme/web/ThemeController.kt +++ b/src/main/kotlin/roomescape/theme/web/ThemeController.kt @@ -15,8 +15,8 @@ class ThemeController( ) : ThemeAPI { @GetMapping("/themes") - override fun findThemes(): ResponseEntity> { - val response: ThemesResponse = themeService.findThemes() + override fun findThemes(): ResponseEntity> { + val response: ThemeRetrieveListResponse = themeService.findThemes() return ResponseEntity.ok(CommonApiResponse(response)) } @@ -24,17 +24,17 @@ class ThemeController( @GetMapping("/themes/most-reserved-last-week") override fun findMostReservedThemes( @RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int - ): ResponseEntity> { - val response: ThemesResponse = themeService.findMostReservedThemes(count) + ): ResponseEntity> { + val response: ThemeRetrieveListResponse = themeService.findMostReservedThemes(count) return ResponseEntity.ok(CommonApiResponse(response)) } @PostMapping("/themes") override fun createTheme( - @RequestBody @Valid request: ThemeRequest - ): ResponseEntity> { - val themeResponse: ThemeResponse = themeService.createTheme(request) + @RequestBody @Valid request: ThemeCreateRequest + ): ResponseEntity> { + val themeResponse: ThemeRetrieveResponse = themeService.createTheme(request) return ResponseEntity.created(URI.create("/themes/${themeResponse.id}")) .body(CommonApiResponse(themeResponse)) diff --git a/src/main/kotlin/roomescape/theme/web/ThemeDTO.kt b/src/main/kotlin/roomescape/theme/web/ThemeDTO.kt index 303ab9e5..d183e56f 100644 --- a/src/main/kotlin/roomescape/theme/web/ThemeDTO.kt +++ b/src/main/kotlin/roomescape/theme/web/ThemeDTO.kt @@ -1,57 +1,48 @@ package roomescape.theme.web -import io.swagger.v3.oas.annotations.media.Schema import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.Size import org.hibernate.validator.constraints.URL import roomescape.theme.infrastructure.persistence.ThemeEntity -@Schema(name = "테마 저장 요청", description = "테마 정보를 저장할 때 사용합니다.") -data class ThemeRequest( - @Schema(description = "필수 값이며, 최대 20글자까지 입력 가능합니다.") +data class ThemeCreateRequest( @NotBlank - @Size(max = 20, message = "테마의 이름은 1~20글자 사이여야 합니다.") + @Size(max = 20) val name: String, - @Schema(description = "필수 값이며, 최대 100글자까지 입력 가능합니다.") @NotBlank - @Size(max = 100, message = "테마의 설명은 1~100글자 사이여야 합니다.") + @Size(max = 100) val description: String, - @Schema(description = "필수 값이며, 썸네일 이미지 URL 을 입력해주세요.") - @NotBlank @URL + @NotBlank val thumbnail: String ) -@Schema(name = "테마 정보", description = "테마 추가 및 조회 응답에 사용됩니다.") -data class ThemeResponse( - @Schema(description = "테마 번호. 테마를 식별할 때 사용합니다.") +fun ThemeCreateRequest.toEntity(): ThemeEntity = ThemeEntity( + name = this.name, + description = this.description, + thumbnail = this.thumbnail +) + +data class ThemeRetrieveResponse( val id: Long, - - @Schema(description = "테마 이름. 중복을 허용하지 않습니다.") val name: String, - - @Schema(description = "테마 설명") val description: String, - - @Schema(description = "테마 썸네일 이미지 URL") val thumbnail: String ) -fun ThemeEntity.toResponse(): ThemeResponse = ThemeResponse( +fun ThemeEntity.toResponse(): ThemeRetrieveResponse = ThemeRetrieveResponse( id = this.id!!, name = this.name, description = this.description, thumbnail = this.thumbnail ) -@Schema(name = "테마 목록 조회 응답", description = "모든 테마 목록 조회 응답시 사용됩니다.") -data class ThemesResponse( - @Schema(description = "모든 테마 목록") - val themes: List +data class ThemeRetrieveListResponse( + val themes: List ) -fun List.toResponse(): ThemesResponse = ThemesResponse( +fun List.toResponse(): ThemeRetrieveListResponse = ThemeRetrieveListResponse( themes = this.map { it.toResponse() } ) diff --git a/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt b/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt index c823c351..d22108b1 100644 --- a/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt +++ b/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt @@ -12,7 +12,7 @@ import roomescape.common.exception.ErrorType import roomescape.common.exception.RoomescapeException import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.theme.infrastructure.persistence.ThemeRepository -import roomescape.theme.web.ThemeRequest +import roomescape.theme.web.ThemeCreateRequest import roomescape.util.ThemeFixture class ThemeServiceTest : FunSpec({ @@ -68,7 +68,7 @@ class ThemeServiceTest : FunSpec({ } returns true val exception = shouldThrow { - themeService.createTheme(ThemeRequest( + themeService.createTheme(ThemeCreateRequest( name = name, description = "Description", thumbnail = "http://example.com/thumbnail.jpg" diff --git a/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt b/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt index f4963788..c151a9f8 100644 --- a/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt +++ b/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt @@ -58,7 +58,7 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { ThemeFixture.create(id = 3, name = "theme3") ) - val response: ThemesResponse = runGetTest( + val response: ThemeRetrieveListResponse = runGetTest( mockMvc = mockMvc, endpoint = endpoint, ) { @@ -66,7 +66,7 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { content { contentType(MediaType.APPLICATION_JSON) } - }.andReturn().readValue(ThemesResponse::class.java) + }.andReturn().readValue(ThemeRetrieveListResponse::class.java) assertSoftly(response.themes) { it.size shouldBe 3 @@ -78,7 +78,7 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { Given("테마를 추가할 때") { val endpoint = "/themes" - val request = ThemeRequest( + val request = ThemeCreateRequest( name = "theme1", description = "description1", thumbnail = "http://example.com/thumbnail1.jpg" @@ -138,13 +138,13 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { loginAsAdmin() } - val request = ThemeRequest( + val request = ThemeCreateRequest( name = "theme1", description = "description1", thumbnail = "http://example.com/thumbnail1.jpg" ) - fun runTest(request: ThemeRequest) { + fun runTest(request: ThemeCreateRequest) { runPostTest( mockMvc = mockMvc, endpoint = endpoint, @@ -197,7 +197,7 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { every { themeService.createTheme(request) - } returns ThemeResponse( + } returns ThemeRetrieveResponse( id = theme.id!!, name = theme.name, description = theme.description, -- 2.47.2 From e3ba8f210862e920498c1ac823f450212b209e8b Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 16:43:59 +0900 Subject: [PATCH 19/37] =?UTF-8?q?feat:=20=ED=85=8C=EB=A7=88=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=A0=95=EC=9D=98=20=EB=B0=8F=20Service=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/theme/business/ThemeService.kt | 38 +++++-------------- .../theme/exception/ThemeErrorCode.kt | 14 +++++++ .../theme/exception/ThemeException.kt | 8 ++++ 3 files changed, 31 insertions(+), 29 deletions(-) create mode 100644 src/main/kotlin/roomescape/theme/exception/ThemeErrorCode.kt create mode 100644 src/main/kotlin/roomescape/theme/exception/ThemeException.kt diff --git a/src/main/kotlin/roomescape/theme/business/ThemeService.kt b/src/main/kotlin/roomescape/theme/business/ThemeService.kt index 8a31b170..0f968e65 100644 --- a/src/main/kotlin/roomescape/theme/business/ThemeService.kt +++ b/src/main/kotlin/roomescape/theme/business/ThemeService.kt @@ -1,17 +1,13 @@ package roomescape.theme.business import org.springframework.data.repository.findByIdOrNull -import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException +import roomescape.theme.exception.ThemeErrorCode +import roomescape.theme.exception.ThemeException import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.theme.infrastructure.persistence.ThemeRepository -import roomescape.theme.web.ThemeRequest -import roomescape.theme.web.ThemeResponse -import roomescape.theme.web.ThemesResponse -import roomescape.theme.web.toResponse +import roomescape.theme.web.* import java.time.LocalDate @Service @@ -20,11 +16,7 @@ class ThemeService( ) { @Transactional(readOnly = true) fun findById(id: Long): ThemeEntity = themeRepository.findByIdOrNull(id) - ?: throw RoomescapeException( - ErrorType.THEME_NOT_FOUND, - "[themeId: $id]", - HttpStatus.BAD_REQUEST - ) + ?: throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND) @Transactional(readOnly = true) fun findThemes(): ThemeRetrieveListResponse = themeRepository.findAll() @@ -43,31 +35,19 @@ class ThemeService( @Transactional fun createTheme(request: ThemeCreateRequest): ThemeRetrieveResponse { if (themeRepository.existsByName(request.name)) { - throw RoomescapeException( - ErrorType.THEME_DUPLICATED, - "[name: ${request.name}]", - HttpStatus.CONFLICT - ) + throw ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED) } - return ThemeEntity( - name = request.name, - description = request.description, - thumbnail = request.thumbnail - ).also { - themeRepository.save(it) - }.toResponse() + val theme: ThemeEntity = request.toEntity() + return themeRepository.save(theme).toResponse() } @Transactional fun deleteTheme(id: Long) { if (themeRepository.isReservedTheme(id)) { - throw RoomescapeException( - ErrorType.THEME_IS_USED_CONFLICT, - "[themeId: %d]", - HttpStatus.CONFLICT - ) + throw ThemeException(ThemeErrorCode.THEME_ALREADY_RESERVED) } + themeRepository.deleteById(id) } } diff --git a/src/main/kotlin/roomescape/theme/exception/ThemeErrorCode.kt b/src/main/kotlin/roomescape/theme/exception/ThemeErrorCode.kt new file mode 100644 index 00000000..7f71250e --- /dev/null +++ b/src/main/kotlin/roomescape/theme/exception/ThemeErrorCode.kt @@ -0,0 +1,14 @@ +package roomescape.theme.exception + +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 +) : ErrorCode { + THEME_NOT_FOUND(HttpStatus.NOT_FOUND, "TH001", "테마를 찾을 수 없어요."), + THEME_NAME_DUPLICATED(HttpStatus.BAD_REQUEST, "TH002", "이미 같은 이름의 테마가 있어요."), + THEME_ALREADY_RESERVED(HttpStatus.CONFLICT, "TH003", "예약된 테마라 삭제할 수 없어요.") +} diff --git a/src/main/kotlin/roomescape/theme/exception/ThemeException.kt b/src/main/kotlin/roomescape/theme/exception/ThemeException.kt new file mode 100644 index 00000000..9129f93e --- /dev/null +++ b/src/main/kotlin/roomescape/theme/exception/ThemeException.kt @@ -0,0 +1,8 @@ +package roomescape.theme.exception + +import roomescape.common.exception.RoomescapeExceptionV2 + +class ThemeException( + override val errorCode: ThemeErrorCode, + override val message: String = errorCode.message +) : RoomescapeExceptionV2(errorCode, message) -- 2.47.2 From fa6464c97f108520fda4aabaf9a02f9bf21ffba1 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 16:55:08 +0900 Subject: [PATCH 20/37] =?UTF-8?q?refactor:=20=EC=83=88=EB=A1=9C=EC=9A=B4?= =?UTF-8?q?=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=98=88=EC=99=B8=EC=99=80?= =?UTF-8?q?=EC=9D=98=20=ED=98=B8=ED=99=98=EC=84=B1=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20=EC=98=88=EC=99=B8=20=ED=83=80=EC=9E=85=EC=9D=84=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=EB=A1=9C=20Exception=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/roomescape/reservation/web/ReservationController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/roomescape/reservation/web/ReservationController.kt b/src/main/kotlin/roomescape/reservation/web/ReservationController.kt index 47b4a720..aa41864f 100644 --- a/src/main/kotlin/roomescape/reservation/web/ReservationController.kt +++ b/src/main/kotlin/roomescape/reservation/web/ReservationController.kt @@ -90,7 +90,7 @@ class ReservationController( ) return ResponseEntity.created(URI.create("/reservations/${reservationRetrieveResponse.id}")) .body(CommonApiResponse(reservationRetrieveResponse)) - } catch (e: RoomescapeException) { + } catch (e: Exception) { val cancelRequest = PaymentCancelRequest(paymentRequest.paymentKey, paymentRequest.amount, e.message!!) val paymentCancelResponse = paymentClient.cancel(cancelRequest) -- 2.47.2 From 0cbdcde8c78787c1d28018d5d940f34a91b236e5 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 16:55:45 +0900 Subject: [PATCH 21/37] =?UTF-8?q?test:=20=ED=85=8C=EB=A7=88=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EC=97=90=EC=84=9C=EC=9D=98=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=ED=83=80=EC=9E=85=20=EB=B3=80=ED=99=94=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/ReservationControllerTest.kt | 5 ++-- .../theme/business/ThemeServiceTest.kt | 23 +++++++------------ .../theme/web/ThemeControllerTest.kt | 18 +++++++++------ 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt b/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt index 01f5d2a7..c7a1802b 100644 --- a/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt +++ b/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt @@ -29,6 +29,7 @@ import roomescape.payment.infrastructure.persistence.PaymentEntity import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationStatus import roomescape.reservation.infrastructure.persistence.TimeEntity +import roomescape.theme.exception.ThemeErrorCode import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.util.* import java.time.LocalDate @@ -123,7 +124,7 @@ class ReservationControllerTest( // 예약 저장 과정에서 테마가 없는 예외 val invalidRequest = reservationRequest.copy(themeId = reservationRequest.themeId + 1) - val expectedException = RoomescapeException(ErrorType.THEME_NOT_FOUND, HttpStatus.BAD_REQUEST) + val expectedException = ThemeErrorCode.THEME_NOT_FOUND every { paymentClient.cancel(any()) @@ -142,7 +143,7 @@ class ReservationControllerTest( post("/reservations") }.Then { statusCode(expectedException.httpStatus.value()) - body("errorType", equalTo(expectedException.errorType.name)) + body("code", equalTo(expectedException.errorCode)) } val canceledPaymentSizeAfterApiCall: Long = entityManager.createQuery( diff --git a/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt b/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt index d22108b1..8386bd5b 100644 --- a/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt +++ b/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt @@ -7,9 +7,8 @@ import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockk import org.springframework.data.repository.findByIdOrNull -import org.springframework.http.HttpStatus -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException +import roomescape.theme.exception.ThemeErrorCode +import roomescape.theme.exception.ThemeException import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.theme.infrastructure.persistence.ThemeRepository import roomescape.theme.web.ThemeCreateRequest @@ -36,11 +35,11 @@ class ThemeServiceTest : FunSpec({ themeRepository.findByIdOrNull(themeId) } returns null - val exception = shouldThrow { + val exception = shouldThrow { themeService.findById(themeId) } - exception.errorType shouldBe ErrorType.THEME_NOT_FOUND + exception.errorCode shouldBe ThemeErrorCode.THEME_NOT_FOUND } } @@ -67,7 +66,7 @@ class ThemeServiceTest : FunSpec({ themeRepository.existsByName(name) } returns true - val exception = shouldThrow { + val exception = shouldThrow { themeService.createTheme(ThemeCreateRequest( name = name, description = "Description", @@ -75,10 +74,7 @@ class ThemeServiceTest : FunSpec({ )) } - assertSoftly(exception) { - this.errorType shouldBe ErrorType.THEME_DUPLICATED - this.httpStatus shouldBe HttpStatus.CONFLICT - } + exception.errorCode shouldBe ThemeErrorCode.THEME_NAME_DUPLICATED } } @@ -90,14 +86,11 @@ class ThemeServiceTest : FunSpec({ themeRepository.isReservedTheme(themeId) } returns true - val exception = shouldThrow { + val exception = shouldThrow { themeService.deleteTheme(themeId) } - assertSoftly(exception) { - this.errorType shouldBe ErrorType.THEME_IS_USED_CONFLICT - this.httpStatus shouldBe HttpStatus.CONFLICT - } + exception.errorCode shouldBe ThemeErrorCode.THEME_ALREADY_RESERVED } } }) diff --git a/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt b/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt index c151a9f8..eb959887 100644 --- a/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt +++ b/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt @@ -13,6 +13,7 @@ import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc import roomescape.auth.exception.AuthErrorCode import roomescape.theme.business.ThemeService +import roomescape.theme.exception.ThemeErrorCode import roomescape.theme.infrastructure.persistence.ThemeRepository import roomescape.util.RoomescapeApiTest import roomescape.util.ThemeFixture @@ -117,7 +118,9 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { When("동일한 이름의 테마가 있으면") { loginAsAdmin() - Then("409 에러를 응답한다.") { + val expectedError = ThemeErrorCode.THEME_NAME_DUPLICATED + + Then("에러 응답.") { every { themeRepository.existsByName(request.name) } returns true @@ -127,8 +130,8 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { endpoint = endpoint, body = request, ) { - status { isConflict() } - jsonPath("$.errorType") { value("THEME_DUPLICATED") } + status { isEqualTo(expectedError.httpStatus.value()) } + jsonPath("$.code") { value(expectedError.errorCode) } } } } @@ -255,10 +258,11 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { } } - When("입력된 ID에 해당하는 테마가 없으면") { + When("이미 예약된 테마이면") { loginAsAdmin() + val expectedError = ThemeErrorCode.THEME_ALREADY_RESERVED - Then("409 에러를 응답한다.") { + Then("에러 응답") { every { themeRepository.isReservedTheme(themeId) } returns true @@ -267,8 +271,8 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { mockMvc = mockMvc, endpoint = endpoint, ) { - status { isConflict() } - jsonPath("$.errorType") { value("THEME_IS_USED_CONFLICT") } + status { isEqualTo(expectedError.httpStatus.value()) } + jsonPath("$.code") { value(expectedError.errorCode) } } } } -- 2.47.2 From 50e8bbf66822471fafc6f32782d4609cd6e51082 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 16:57:40 +0900 Subject: [PATCH 22/37] =?UTF-8?q?refactor:=20=ED=85=8C=EB=A7=88=20DTO?= =?UTF-8?q?=EC=9D=98=20thumbnail=EC=97=90=EB=8A=94=20Schema=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/roomescape/theme/web/ThemeDTO.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/kotlin/roomescape/theme/web/ThemeDTO.kt b/src/main/kotlin/roomescape/theme/web/ThemeDTO.kt index d183e56f..a273fa7e 100644 --- a/src/main/kotlin/roomescape/theme/web/ThemeDTO.kt +++ b/src/main/kotlin/roomescape/theme/web/ThemeDTO.kt @@ -1,5 +1,6 @@ package roomescape.theme.web +import io.swagger.v3.oas.annotations.media.Schema import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.Size import org.hibernate.validator.constraints.URL @@ -16,6 +17,7 @@ data class ThemeCreateRequest( @URL @NotBlank + @Schema(description = "썸네일 이미지 주소(URL).") val thumbnail: String ) @@ -29,6 +31,7 @@ data class ThemeRetrieveResponse( val id: Long, val name: String, val description: String, + @Schema(description = "썸네일 이미지 주소(URL).") val thumbnail: String ) -- 2.47.2 From 42780f1eed56712b70a46a9e9c8d8b39001d5fec Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 17:04:31 +0900 Subject: [PATCH 23/37] =?UTF-8?q?refactor:=20ThemeService=EC=97=90?= =?UTF-8?q?=EC=84=9C=EC=9D=98=20=EC=A0=95=EC=83=81=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../theme/business/ThemeServiceTest.kt | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt b/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt index 8386bd5b..8b56460b 100644 --- a/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt +++ b/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt @@ -12,6 +12,7 @@ import roomescape.theme.exception.ThemeException import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.theme.infrastructure.persistence.ThemeRepository import roomescape.theme.web.ThemeCreateRequest +import roomescape.theme.web.ThemeRetrieveResponse import roomescape.util.ThemeFixture class ThemeServiceTest : FunSpec({ @@ -59,19 +60,43 @@ class ThemeServiceTest : FunSpec({ } context("save") { - test("테마 이름이 중복되면 409 예외를 던진다.") { - val name = "Duplicate Theme" + val request = ThemeCreateRequest( + name = "New Theme", + description = "Description", + thumbnail = "http://example.com/thumbnail.jpg" + ) + + test("저장 성공") { + every { + themeRepository.existsByName(request.name) + } returns false every { - themeRepository.existsByName(name) + themeRepository.save(any()) + } returns ThemeFixture.create( + id = 1L, + name = request.name, + description = request.description, + thumbnail = request.thumbnail + ) + + val response: ThemeRetrieveResponse = themeService.createTheme(request) + + assertSoftly(response) { + this.id shouldBe 1L + this.name shouldBe request.name + this.description shouldBe request.description + this.thumbnail shouldBe request.thumbnail + } + } + + test("테마 이름이 중복되면 409 예외를 던진다.") { + every { + themeRepository.existsByName(request.name) } returns true val exception = shouldThrow { - themeService.createTheme(ThemeCreateRequest( - name = name, - description = "Description", - thumbnail = "http://example.com/thumbnail.jpg" - )) + themeService.createTheme(request) } exception.errorCode shouldBe ThemeErrorCode.THEME_NAME_DUPLICATED -- 2.47.2 From 7b2c3c32be6e7e11895926b8702cb68bfd502e2f Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 17:13:54 +0900 Subject: [PATCH 24/37] =?UTF-8?q?refactor:=20time=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/business/ReservationService.kt | 7 ++++++- .../infrastructure/persistence/ReservationEntity.kt | 1 + .../persistence/ReservationRepository.kt | 1 + .../persistence/ReservationSearchSpecification.kt | 1 + .../reservation/web/ReservationResponse.kt | 2 ++ .../{reservation => time}/business/TimeService.kt | 10 +++++----- .../roomescape/{reservation => time}/docs/TimeAPI.kt | 12 ++++++------ .../infrastructure/persistence/TimeEntity.kt | 2 +- .../infrastructure/persistence/TimeRepository.kt | 2 +- .../{reservation => time}/web/TimeController.kt | 6 +++--- .../roomescape/{reservation => time}/web/TimeDTO.kt | 4 ++-- .../reservation/business/ReservationServiceTest.kt | 1 + .../ReservationSearchSpecificationTest.kt | 1 + .../reservation/web/ReservationControllerTest.kt | 2 +- .../roomescape/theme/util/TestThemeCreateUtil.kt | 2 +- .../business/TimeServiceTest.kt | 8 ++++---- .../infrastructure/persistence/TimeRepositoryTest.kt | 4 ++-- .../{reservation => time}/web/TimeControllerTest.kt | 10 +++++----- src/test/kotlin/roomescape/util/Fixtures.kt | 2 +- 19 files changed, 45 insertions(+), 33 deletions(-) rename src/main/kotlin/roomescape/{reservation => time}/business/TimeService.kt (92%) rename src/main/kotlin/roomescape/{reservation => time}/docs/TimeAPI.kt (89%) rename src/main/kotlin/roomescape/{reservation => time}/infrastructure/persistence/TimeEntity.kt (80%) rename src/main/kotlin/roomescape/{reservation => time}/infrastructure/persistence/TimeRepository.kt (78%) rename src/main/kotlin/roomescape/{reservation => time}/web/TimeController.kt (92%) rename src/main/kotlin/roomescape/{reservation => time}/web/TimeDTO.kt (94%) rename src/test/kotlin/roomescape/{reservation => time}/business/TimeServiceTest.kt (94%) rename src/test/kotlin/roomescape/{reservation => time}/infrastructure/persistence/TimeRepositoryTest.kt (93%) rename src/test/kotlin/roomescape/{reservation => time}/web/TimeControllerTest.kt (97%) diff --git a/src/main/kotlin/roomescape/reservation/business/ReservationService.kt b/src/main/kotlin/roomescape/reservation/business/ReservationService.kt index 95f6d8ca..7e8f5f94 100644 --- a/src/main/kotlin/roomescape/reservation/business/ReservationService.kt +++ b/src/main/kotlin/roomescape/reservation/business/ReservationService.kt @@ -8,9 +8,14 @@ import org.springframework.transaction.annotation.Transactional import roomescape.common.exception.ErrorType import roomescape.common.exception.RoomescapeException import roomescape.member.business.MemberService -import roomescape.reservation.infrastructure.persistence.* +import roomescape.reservation.infrastructure.persistence.ReservationEntity +import roomescape.reservation.infrastructure.persistence.ReservationRepository +import roomescape.reservation.infrastructure.persistence.ReservationSearchSpecification +import roomescape.reservation.infrastructure.persistence.ReservationStatus import roomescape.reservation.web.* import roomescape.theme.business.ThemeService +import roomescape.time.business.TimeService +import roomescape.time.infrastructure.persistence.TimeEntity import java.time.LocalDate import java.time.LocalDateTime diff --git a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationEntity.kt b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationEntity.kt index b7912f5b..31acb5f6 100644 --- a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationEntity.kt +++ b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationEntity.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore import jakarta.persistence.* import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.time.infrastructure.persistence.TimeEntity import java.time.LocalDate @Entity diff --git a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt index 4c563eea..a0a4f499 100644 --- a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt +++ b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param import roomescape.reservation.web.MyReservationRetrieveResponse +import roomescape.time.infrastructure.persistence.TimeEntity import java.time.LocalDate interface ReservationRepository diff --git a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecification.kt b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecification.kt index f42892c3..0de81420 100644 --- a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecification.kt +++ b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecification.kt @@ -3,6 +3,7 @@ package roomescape.reservation.infrastructure.persistence import org.springframework.data.jpa.domain.Specification import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.time.infrastructure.persistence.TimeEntity import java.time.LocalDate class ReservationSearchSpecification( diff --git a/src/main/kotlin/roomescape/reservation/web/ReservationResponse.kt b/src/main/kotlin/roomescape/reservation/web/ReservationResponse.kt index 711eabd6..3a6b7577 100644 --- a/src/main/kotlin/roomescape/reservation/web/ReservationResponse.kt +++ b/src/main/kotlin/roomescape/reservation/web/ReservationResponse.kt @@ -8,6 +8,8 @@ import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationStatus import roomescape.theme.web.ThemeRetrieveResponse import roomescape.theme.web.toResponse +import roomescape.time.web.TimeCreateResponse +import roomescape.time.web.toCreateResponse import java.time.LocalDate import java.time.LocalTime diff --git a/src/main/kotlin/roomescape/reservation/business/TimeService.kt b/src/main/kotlin/roomescape/time/business/TimeService.kt similarity index 92% rename from src/main/kotlin/roomescape/reservation/business/TimeService.kt rename to src/main/kotlin/roomescape/time/business/TimeService.kt index edde1923..7b8d956a 100644 --- a/src/main/kotlin/roomescape/reservation/business/TimeService.kt +++ b/src/main/kotlin/roomescape/time/business/TimeService.kt @@ -1,4 +1,4 @@ -package roomescape.reservation.business +package roomescape.time.business import org.springframework.data.repository.findByIdOrNull import org.springframework.http.HttpStatus @@ -8,9 +8,9 @@ import roomescape.common.exception.ErrorType import roomescape.common.exception.RoomescapeException import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationRepository -import roomescape.reservation.infrastructure.persistence.TimeEntity -import roomescape.reservation.infrastructure.persistence.TimeRepository -import roomescape.reservation.web.* +import roomescape.time.infrastructure.persistence.TimeEntity +import roomescape.time.infrastructure.persistence.TimeRepository +import roomescape.time.web.* import java.time.LocalDate import java.time.LocalTime @@ -70,4 +70,4 @@ class TimeService( TimeWithAvailabilityResponse(time.id!!, time.startAt, isAvailable) }) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/roomescape/reservation/docs/TimeAPI.kt b/src/main/kotlin/roomescape/time/docs/TimeAPI.kt similarity index 89% rename from src/main/kotlin/roomescape/reservation/docs/TimeAPI.kt rename to src/main/kotlin/roomescape/time/docs/TimeAPI.kt index 067137f3..6e1280c2 100644 --- a/src/main/kotlin/roomescape/reservation/docs/TimeAPI.kt +++ b/src/main/kotlin/roomescape/time/docs/TimeAPI.kt @@ -1,4 +1,4 @@ -package roomescape.reservation.docs +package roomescape.time.docs import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse @@ -12,10 +12,10 @@ import org.springframework.web.bind.annotation.RequestParam import roomescape.auth.web.support.Admin import roomescape.auth.web.support.LoginRequired import roomescape.common.dto.response.CommonApiResponse -import roomescape.reservation.web.TimeCreateRequest -import roomescape.reservation.web.TimeCreateResponse -import roomescape.reservation.web.TimeRetrieveListResponse -import roomescape.reservation.web.TimeWithAvailabilityListResponse +import roomescape.time.web.TimeCreateRequest +import roomescape.time.web.TimeCreateResponse +import roomescape.time.web.TimeRetrieveListResponse +import roomescape.time.web.TimeWithAvailabilityListResponse import java.time.LocalDate @Tag(name = "4. 예약 시간 API", description = "예약 시간을 조회 / 추가 / 삭제할 때 사용합니다.") @@ -47,4 +47,4 @@ interface TimeAPI { @RequestParam date: LocalDate, @RequestParam themeId: Long ): ResponseEntity> -} +} \ No newline at end of file diff --git a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/TimeEntity.kt b/src/main/kotlin/roomescape/time/infrastructure/persistence/TimeEntity.kt similarity index 80% rename from src/main/kotlin/roomescape/reservation/infrastructure/persistence/TimeEntity.kt rename to src/main/kotlin/roomescape/time/infrastructure/persistence/TimeEntity.kt index 38f1f361..d0fc306d 100644 --- a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/TimeEntity.kt +++ b/src/main/kotlin/roomescape/time/infrastructure/persistence/TimeEntity.kt @@ -1,4 +1,4 @@ -package roomescape.reservation.infrastructure.persistence +package roomescape.time.infrastructure.persistence import jakarta.persistence.* import java.time.LocalTime diff --git a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/TimeRepository.kt b/src/main/kotlin/roomescape/time/infrastructure/persistence/TimeRepository.kt similarity index 78% rename from src/main/kotlin/roomescape/reservation/infrastructure/persistence/TimeRepository.kt rename to src/main/kotlin/roomescape/time/infrastructure/persistence/TimeRepository.kt index df798889..5e726669 100644 --- a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/TimeRepository.kt +++ b/src/main/kotlin/roomescape/time/infrastructure/persistence/TimeRepository.kt @@ -1,4 +1,4 @@ -package roomescape.reservation.infrastructure.persistence +package roomescape.time.infrastructure.persistence import org.springframework.data.jpa.repository.JpaRepository import java.time.LocalTime diff --git a/src/main/kotlin/roomescape/reservation/web/TimeController.kt b/src/main/kotlin/roomescape/time/web/TimeController.kt similarity index 92% rename from src/main/kotlin/roomescape/reservation/web/TimeController.kt rename to src/main/kotlin/roomescape/time/web/TimeController.kt index d697bafa..67a6b494 100644 --- a/src/main/kotlin/roomescape/reservation/web/TimeController.kt +++ b/src/main/kotlin/roomescape/time/web/TimeController.kt @@ -1,11 +1,11 @@ -package roomescape.reservation.web +package roomescape.time.web import jakarta.validation.Valid import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import roomescape.common.dto.response.CommonApiResponse -import roomescape.reservation.business.TimeService -import roomescape.reservation.docs.TimeAPI +import roomescape.time.business.TimeService +import roomescape.time.docs.TimeAPI import java.net.URI import java.time.LocalDate diff --git a/src/main/kotlin/roomescape/reservation/web/TimeDTO.kt b/src/main/kotlin/roomescape/time/web/TimeDTO.kt similarity index 94% rename from src/main/kotlin/roomescape/reservation/web/TimeDTO.kt rename to src/main/kotlin/roomescape/time/web/TimeDTO.kt index b6f121eb..257d7ff8 100644 --- a/src/main/kotlin/roomescape/reservation/web/TimeDTO.kt +++ b/src/main/kotlin/roomescape/time/web/TimeDTO.kt @@ -1,7 +1,7 @@ -package roomescape.reservation.web +package roomescape.time.web import io.swagger.v3.oas.annotations.media.Schema -import roomescape.reservation.infrastructure.persistence.TimeEntity +import roomescape.time.infrastructure.persistence.TimeEntity import java.time.LocalTime @Schema(name = "예약 시간 저장 요청", description = "예약 시간 저장 요청시 사용됩니다.") diff --git a/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt b/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt index 745cc89d..408718d4 100644 --- a/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt +++ b/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt @@ -11,6 +11,7 @@ import roomescape.member.business.MemberService import roomescape.member.infrastructure.persistence.Role import roomescape.reservation.infrastructure.persistence.ReservationRepository import roomescape.theme.business.ThemeService +import roomescape.time.business.TimeService import roomescape.util.MemberFixture import roomescape.util.ReservationFixture import roomescape.util.TimeFixture diff --git a/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecificationTest.kt b/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecificationTest.kt index 6d764f42..d1e63d9d 100644 --- a/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecificationTest.kt +++ b/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecificationTest.kt @@ -8,6 +8,7 @@ import jakarta.persistence.EntityManager import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.time.infrastructure.persistence.TimeEntity import roomescape.util.MemberFixture import roomescape.util.ReservationFixture import roomescape.util.ThemeFixture diff --git a/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt b/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt index c7a1802b..5a94cae9 100644 --- a/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt +++ b/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt @@ -28,9 +28,9 @@ import roomescape.payment.infrastructure.client.TossPaymentClient import roomescape.payment.infrastructure.persistence.PaymentEntity import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationStatus -import roomescape.reservation.infrastructure.persistence.TimeEntity import roomescape.theme.exception.ThemeErrorCode import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.time.infrastructure.persistence.TimeEntity import roomescape.util.* import java.time.LocalDate import java.time.LocalTime diff --git a/src/test/kotlin/roomescape/theme/util/TestThemeCreateUtil.kt b/src/test/kotlin/roomescape/theme/util/TestThemeCreateUtil.kt index 0dacd49b..43c46762 100644 --- a/src/test/kotlin/roomescape/theme/util/TestThemeCreateUtil.kt +++ b/src/test/kotlin/roomescape/theme/util/TestThemeCreateUtil.kt @@ -3,8 +3,8 @@ package roomescape.theme.util import jakarta.persistence.EntityManager import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.reservation.infrastructure.persistence.ReservationStatus -import roomescape.reservation.infrastructure.persistence.TimeEntity import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.time.infrastructure.persistence.TimeEntity import roomescape.util.MemberFixture import roomescape.util.ReservationFixture import roomescape.util.ThemeFixture diff --git a/src/test/kotlin/roomescape/reservation/business/TimeServiceTest.kt b/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt similarity index 94% rename from src/test/kotlin/roomescape/reservation/business/TimeServiceTest.kt rename to src/test/kotlin/roomescape/time/business/TimeServiceTest.kt index 83ed7417..58d8a17a 100644 --- a/src/test/kotlin/roomescape/reservation/business/TimeServiceTest.kt +++ b/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt @@ -1,4 +1,4 @@ -package roomescape.reservation.business +package roomescape.time.business import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec @@ -10,8 +10,8 @@ import org.springframework.http.HttpStatus import roomescape.common.exception.ErrorType import roomescape.common.exception.RoomescapeException import roomescape.reservation.infrastructure.persistence.ReservationRepository -import roomescape.reservation.infrastructure.persistence.TimeRepository -import roomescape.reservation.web.TimeCreateRequest +import roomescape.time.infrastructure.persistence.TimeRepository +import roomescape.time.web.TimeCreateRequest import roomescape.util.TimeFixture import java.time.LocalTime @@ -84,4 +84,4 @@ class TimeServiceTest : FunSpec({ } } } -}) +}) \ No newline at end of file diff --git a/src/test/kotlin/roomescape/reservation/infrastructure/persistence/TimeRepositoryTest.kt b/src/test/kotlin/roomescape/time/infrastructure/persistence/TimeRepositoryTest.kt similarity index 93% rename from src/test/kotlin/roomescape/reservation/infrastructure/persistence/TimeRepositoryTest.kt rename to src/test/kotlin/roomescape/time/infrastructure/persistence/TimeRepositoryTest.kt index d3ef18c5..34f1b421 100644 --- a/src/test/kotlin/roomescape/reservation/infrastructure/persistence/TimeRepositoryTest.kt +++ b/src/test/kotlin/roomescape/time/infrastructure/persistence/TimeRepositoryTest.kt @@ -1,4 +1,4 @@ -package roomescape.reservation.infrastructure.persistence +package roomescape.time.infrastructure.persistence import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe @@ -30,4 +30,4 @@ class TimeRepositoryTest( timeRepository.existsByStartAt(startAt.plusSeconds(1)) shouldBe false } } -}) +}) \ No newline at end of file diff --git a/src/test/kotlin/roomescape/reservation/web/TimeControllerTest.kt b/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt similarity index 97% rename from src/test/kotlin/roomescape/reservation/web/TimeControllerTest.kt rename to src/test/kotlin/roomescape/time/web/TimeControllerTest.kt index 1161b705..4ce52364 100644 --- a/src/test/kotlin/roomescape/reservation/web/TimeControllerTest.kt +++ b/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt @@ -1,4 +1,4 @@ -package roomescape.reservation.web +package roomescape.time.web import com.ninjasquad.springmockk.MockkBean import com.ninjasquad.springmockk.SpykBean @@ -13,10 +13,10 @@ import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc import roomescape.common.config.JacksonConfig import roomescape.common.exception.ErrorType -import roomescape.reservation.business.TimeService import roomescape.reservation.infrastructure.persistence.ReservationRepository -import roomescape.reservation.infrastructure.persistence.TimeEntity -import roomescape.reservation.infrastructure.persistence.TimeRepository +import roomescape.time.business.TimeService +import roomescape.time.infrastructure.persistence.TimeEntity +import roomescape.time.infrastructure.persistence.TimeRepository import roomescape.util.ReservationFixture import roomescape.util.RoomescapeApiTest import roomescape.util.ThemeFixture @@ -292,4 +292,4 @@ class TimeControllerTest( } } } -} +} \ No newline at end of file diff --git a/src/test/kotlin/roomescape/util/Fixtures.kt b/src/test/kotlin/roomescape/util/Fixtures.kt index 21316345..5413b676 100644 --- a/src/test/kotlin/roomescape/util/Fixtures.kt +++ b/src/test/kotlin/roomescape/util/Fixtures.kt @@ -12,10 +12,10 @@ import roomescape.payment.web.PaymentCancelRequest import roomescape.payment.web.PaymentCancelResponse import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationStatus -import roomescape.reservation.infrastructure.persistence.TimeEntity import roomescape.reservation.web.ReservationCreateWithPaymentRequest import roomescape.reservation.web.WaitingCreateRequest import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.time.infrastructure.persistence.TimeEntity import java.time.LocalDate import java.time.LocalTime import java.time.OffsetDateTime -- 2.47.2 From 0d33579d3fedf6f8460974d0226a5c9575a4e40a Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 17:38:24 +0900 Subject: [PATCH 25/37] =?UTF-8?q?feat:=20time=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=9D=98=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/ReservationRepository.kt | 2 +- .../roomescape/time/business/TimeService.kt | 47 +++++++------------ .../time/exception/TimeErrorCode.kt | 14 ++++++ .../time/exception/TimeException.kt | 9 ++++ .../kotlin/roomescape/time/web/TimeDTO.kt | 8 ++-- 5 files changed, 47 insertions(+), 33 deletions(-) create mode 100644 src/main/kotlin/roomescape/time/exception/TimeErrorCode.kt create mode 100644 src/main/kotlin/roomescape/time/exception/TimeException.kt diff --git a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt index a0a4f499..3dd73807 100644 --- a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt +++ b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt @@ -11,7 +11,7 @@ import java.time.LocalDate interface ReservationRepository : JpaRepository, JpaSpecificationExecutor { - fun findByTime(time: TimeEntity): List + fun findAllByTime(time: TimeEntity): List fun findByDateAndThemeId(date: LocalDate, themeId: Long): List diff --git a/src/main/kotlin/roomescape/time/business/TimeService.kt b/src/main/kotlin/roomescape/time/business/TimeService.kt index 7b8d956a..799a363f 100644 --- a/src/main/kotlin/roomescape/time/business/TimeService.kt +++ b/src/main/kotlin/roomescape/time/business/TimeService.kt @@ -1,13 +1,12 @@ package roomescape.time.business import org.springframework.data.repository.findByIdOrNull -import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationRepository +import roomescape.time.exception.TimeErrorCode +import roomescape.time.exception.TimeException import roomescape.time.infrastructure.persistence.TimeEntity import roomescape.time.infrastructure.persistence.TimeRepository import roomescape.time.web.* @@ -21,42 +20,33 @@ class TimeService( ) { @Transactional(readOnly = true) fun findById(id: Long): TimeEntity = timeRepository.findByIdOrNull(id) - ?: throw RoomescapeException( - ErrorType.TIME_NOT_FOUND, - "[timeId: $id]", - HttpStatus.BAD_REQUEST - ) + ?: throw TimeException(TimeErrorCode.TIME_NOT_FOUND) @Transactional(readOnly = true) - fun findTimes(): TimeRetrieveListResponse = timeRepository.findAll().toRetrieveListResponse() + fun findTimes(): TimeRetrieveListResponse = timeRepository.findAll() + .toResponse() @Transactional - fun createTime(timeCreateRequest: TimeCreateRequest): TimeCreateResponse { - val startAt: LocalTime = timeCreateRequest.startAt - + fun createTime(request: TimeCreateRequest): TimeCreateResponse { + val startAt: LocalTime = request.startAt if (timeRepository.existsByStartAt(startAt)) { - throw RoomescapeException( - ErrorType.TIME_DUPLICATED, "[startAt: $startAt]", HttpStatus.CONFLICT - ) + throw TimeException(TimeErrorCode.TIME_DUPLICATED) } - return TimeEntity(startAt = startAt) - .also { timeRepository.save(it) } - .toCreateResponse() + val time: TimeEntity = request.toEntity() + + return timeRepository.save(time).toCreateResponse() } @Transactional fun deleteTime(id: Long) { val time: TimeEntity = findById(id) - reservationRepository.findByTime(time) - .also { - if (it.isNotEmpty()) { - throw RoomescapeException( - ErrorType.TIME_IS_USED_CONFLICT, "[timeId: $id]", HttpStatus.CONFLICT - ) - } - timeRepository.deleteById(id) - } + val reservations: List = reservationRepository.findAllByTime(time) + + if (reservations.isNotEmpty()) { + throw TimeException(TimeErrorCode.TIME_ALREADY_RESERVED) + } + timeRepository.delete(time) } @Transactional(readOnly = true) @@ -66,8 +56,7 @@ class TimeService( return TimeWithAvailabilityListResponse(allTimes.map { time -> val isAvailable: Boolean = reservations.none { reservation -> reservation.time.id == time.id } - TimeWithAvailabilityResponse(time.id!!, time.startAt, isAvailable) }) } -} \ 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 new file mode 100644 index 00000000..c0c967f3 --- /dev/null +++ b/src/main/kotlin/roomescape/time/exception/TimeErrorCode.kt @@ -0,0 +1,14 @@ +package roomescape.time.exception + +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 +) : ErrorCode { + TIME_NOT_FOUND(HttpStatus.NOT_FOUND, "TM001", "시간을 찾을 수 없어요."), + TIME_DUPLICATED(HttpStatus.BAD_REQUEST, "TM002", "이미 같은 시간이 있어요."), + TIME_ALREADY_RESERVED(HttpStatus.CONFLICT, "TM003", "예약된 시간이라 삭제할 수 없어요.") +} diff --git a/src/main/kotlin/roomescape/time/exception/TimeException.kt b/src/main/kotlin/roomescape/time/exception/TimeException.kt new file mode 100644 index 00000000..a073fdc6 --- /dev/null +++ b/src/main/kotlin/roomescape/time/exception/TimeException.kt @@ -0,0 +1,9 @@ +package roomescape.time.exception + +import roomescape.common.exception.ErrorCode +import roomescape.common.exception.RoomescapeExceptionV2 + +class TimeException( + override val errorCode: ErrorCode, + override val message: String = errorCode.message +) : RoomescapeExceptionV2(errorCode, message) diff --git a/src/main/kotlin/roomescape/time/web/TimeDTO.kt b/src/main/kotlin/roomescape/time/web/TimeDTO.kt index 257d7ff8..7c36ebed 100644 --- a/src/main/kotlin/roomescape/time/web/TimeDTO.kt +++ b/src/main/kotlin/roomescape/time/web/TimeDTO.kt @@ -10,6 +10,8 @@ data class TimeCreateRequest( val startAt: LocalTime ) +fun TimeCreateRequest.toEntity(): TimeEntity = TimeEntity(startAt = this.startAt) + @Schema(name = "예약 시간 정보", description = "예약 시간 추가 및 조회 응답시 사용됩니다.") data class TimeCreateResponse( @Schema(description = "시간 식별자") @@ -29,14 +31,14 @@ data class TimeRetrieveResponse( val startAt: LocalTime ) -fun TimeEntity.toRetrieveResponse(): TimeRetrieveResponse = TimeRetrieveResponse(this.id!!, this.startAt) +fun TimeEntity.toResponse(): TimeRetrieveResponse = TimeRetrieveResponse(this.id!!, this.startAt) data class TimeRetrieveListResponse( val times: List ) -fun List.toRetrieveListResponse(): TimeRetrieveListResponse = TimeRetrieveListResponse( - this.map { it.toRetrieveResponse() } +fun List.toResponse(): TimeRetrieveListResponse = TimeRetrieveListResponse( + this.map { it.toResponse() } ) data class TimeWithAvailabilityResponse( -- 2.47.2 From ac7f9fa330bb88ad4a569c4f42211764668d90d3 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 17:38:38 +0900 Subject: [PATCH 26/37] =?UTF-8?q?test:=20time=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=97=90=20=EC=B6=94=EA=B0=80=EB=90=9C=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=EC=98=88=EC=99=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/ReservationRepositoryTest.kt | 2 +- .../time/business/TimeServiceTest.kt | 60 ++++++++++--------- .../roomescape/time/web/TimeControllerTest.kt | 25 ++++---- 3 files changed, 48 insertions(+), 39 deletions(-) diff --git a/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt b/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt index f911261b..0ae319ae 100644 --- a/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt +++ b/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt @@ -39,7 +39,7 @@ class ReservationRepositoryTest( } test("입력된 시간과 일치하는 예약을 반환한다.") { - assertSoftly(reservationRepository.findByTime(time)) { + assertSoftly(reservationRepository.findAllByTime(time)) { it shouldHaveSize 1 assertSoftly(it.first().time.startAt) { result -> result.hour shouldBe time.startAt.hour diff --git a/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt b/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt index 58d8a17a..ba12249e 100644 --- a/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt +++ b/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt @@ -6,10 +6,9 @@ import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockk import org.springframework.data.repository.findByIdOrNull -import org.springframework.http.HttpStatus -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException import roomescape.reservation.infrastructure.persistence.ReservationRepository +import roomescape.time.exception.TimeErrorCode +import roomescape.time.exception.TimeException import roomescape.time.infrastructure.persistence.TimeRepository import roomescape.time.web.TimeCreateRequest import roomescape.util.TimeFixture @@ -25,63 +24,70 @@ class TimeServiceTest : FunSpec({ ) context("findTimeById") { - test("시간을 찾을 수 없으면 400 에러를 던진다.") { + test("시간을 찾을 수 없으면 예외 응답") { val id = 1L every { timeRepository.findByIdOrNull(id) } returns null - shouldThrow { + shouldThrow { timeService.findById(id) - }.apply { - errorType shouldBe ErrorType.TIME_NOT_FOUND - httpStatus shouldBe HttpStatus.BAD_REQUEST + }.also { + it.errorCode shouldBe TimeErrorCode.TIME_NOT_FOUND } } } - context("addTime") { - test("중복된 시간이 있으면 409 에러를 던진다.") { - val request = TimeCreateRequest(startAt = LocalTime.of(10, 0)) + context("createTime") { + val request = TimeCreateRequest(startAt = LocalTime.of(10, 0)) + test("정상 저장") { + every { timeRepository.existsByStartAt(request.startAt) } returns false + every { timeRepository.save(any()) } returns TimeFixture.create( + id = 1L, + startAt = request.startAt + ) + + val response = timeService.createTime(request) + response.id shouldBe 1L + } + + test("중복된 시간이 있으면 예외 응답") { every { timeRepository.existsByStartAt(request.startAt) } returns true - shouldThrow { + shouldThrow { timeService.createTime(request) - }.apply { - errorType shouldBe ErrorType.TIME_DUPLICATED - httpStatus shouldBe HttpStatus.CONFLICT + }.also { + it.errorCode shouldBe TimeErrorCode.TIME_DUPLICATED } } } context("removeTimeById") { - test("시간을 찾을 수 없으면 400 에러를 던진다.") { + test("시간을 찾을 수 없으면 예외 응답") { val id = 1L every { timeRepository.findByIdOrNull(id) } returns null - shouldThrow { + shouldThrow { timeService.deleteTime(id) - }.apply { - errorType shouldBe ErrorType.TIME_NOT_FOUND - httpStatus shouldBe HttpStatus.BAD_REQUEST + }.also { + it.errorCode shouldBe TimeErrorCode.TIME_NOT_FOUND } } - test("예약이 있는 시간이면 409 에러를 던진다.") { + test("예약이 있는 시간이면 예외 응답") { val id = 1L val time = TimeFixture.create() every { timeRepository.findByIdOrNull(id) } returns time - every { reservationRepository.findByTime(time) } returns listOf(mockk()) + every { reservationRepository.findAllByTime(time) } returns listOf(mockk()) - shouldThrow { + shouldThrow { timeService.deleteTime(id) - }.apply { - errorType shouldBe ErrorType.TIME_IS_USED_CONFLICT - httpStatus shouldBe HttpStatus.CONFLICT + }.also { + it.errorCode shouldBe TimeErrorCode.TIME_ALREADY_RESERVED } } } -}) \ No newline at end of file +}) diff --git a/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt b/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt index 4ce52364..19ae8465 100644 --- a/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt +++ b/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt @@ -12,9 +12,9 @@ import org.springframework.data.repository.findByIdOrNull import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc import roomescape.common.config.JacksonConfig -import roomescape.common.exception.ErrorType import roomescape.reservation.infrastructure.persistence.ReservationRepository import roomescape.time.business.TimeService +import roomescape.time.exception.TimeErrorCode import roomescape.time.infrastructure.persistence.TimeEntity import roomescape.time.infrastructure.persistence.TimeRepository import roomescape.util.ReservationFixture @@ -129,7 +129,8 @@ class TimeControllerTest( } } - Then("동일한 시간이 존재하면 409 응답") { + Then("동일한 시간이 존재하면 예외 응답") { + val expectedError = TimeErrorCode.TIME_DUPLICATED every { timeRepository.existsByStartAt(time) } returns true @@ -139,10 +140,10 @@ class TimeControllerTest( endpoint = endpoint, body = request, ) { - status { isConflict() } + status { isEqualTo(expectedError.httpStatus.value()) } content { contentType(MediaType.APPLICATION_JSON) - jsonPath("$.errorType") { value(ErrorType.TIME_DUPLICATED.name) } + jsonPath("$.code") { value(expectedError.errorCode) } } } } @@ -185,8 +186,9 @@ class TimeControllerTest( } } - Then("없는 시간을 조회하면 400 응답") { + Then("없는 시간을 조회하면 예외 응답") { val id = 1L + val expectedError = TimeErrorCode.TIME_NOT_FOUND every { timeRepository.findByIdOrNull(id) } returns null @@ -195,32 +197,33 @@ class TimeControllerTest( mockMvc = mockMvc, endpoint = "/times/$id", ) { - status { isBadRequest() } + status { isEqualTo(expectedError.httpStatus.value()) } content { contentType(MediaType.APPLICATION_JSON) - jsonPath("$.errorType") { value(ErrorType.TIME_NOT_FOUND.name) } + jsonPath("$.code") { value(expectedError.errorCode) } } } } - Then("예약이 있는 시간을 삭제하면 409 응답") { + Then("예약이 있는 시간을 삭제하면 예외 응답") { val id = 1L + val expectedError = TimeErrorCode.TIME_ALREADY_RESERVED every { timeRepository.findByIdOrNull(id) } returns TimeFixture.create(id = id) every { - reservationRepository.findByTime(any()) + reservationRepository.findAllByTime(any()) } returns listOf(ReservationFixture.create()) runDeleteTest( mockMvc = mockMvc, endpoint = "/times/$id", ) { - status { isConflict() } + status { isEqualTo(expectedError.httpStatus.value()) } content { contentType(MediaType.APPLICATION_JSON) - jsonPath("$.errorType") { value(ErrorType.TIME_IS_USED_CONFLICT.name) } + jsonPath("$.code") { value(expectedError.errorCode) } } } } -- 2.47.2 From 52bc1940f1aa219c2da3bd385ca5b2bc02920e38 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 18:01:31 +0900 Subject: [PATCH 27/37] =?UTF-8?q?refactor:=20=EB=AF=B8=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=20\@Throws=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/client/PaymentCancelResponseDeserializer.kt | 2 -- .../payment/infrastructure/client/TossPaymentClient.kt | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/main/kotlin/roomescape/payment/infrastructure/client/PaymentCancelResponseDeserializer.kt b/src/main/kotlin/roomescape/payment/infrastructure/client/PaymentCancelResponseDeserializer.kt index 4ffb1e93..01f7930a 100644 --- a/src/main/kotlin/roomescape/payment/infrastructure/client/PaymentCancelResponseDeserializer.kt +++ b/src/main/kotlin/roomescape/payment/infrastructure/client/PaymentCancelResponseDeserializer.kt @@ -6,13 +6,11 @@ import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.deser.std.StdDeserializer import roomescape.payment.web.PaymentCancelResponse -import java.io.IOException import java.time.OffsetDateTime class PaymentCancelResponseDeserializer( vc: Class? = null ) : StdDeserializer(vc) { - @Throws(IOException::class) override fun deserialize( jsonParser: JsonParser, deserializationContext: DeserializationContext? diff --git a/src/main/kotlin/roomescape/payment/infrastructure/client/TossPaymentClient.kt b/src/main/kotlin/roomescape/payment/infrastructure/client/TossPaymentClient.kt index e8ff0abe..5fa16a83 100644 --- a/src/main/kotlin/roomescape/payment/infrastructure/client/TossPaymentClient.kt +++ b/src/main/kotlin/roomescape/payment/infrastructure/client/TossPaymentClient.kt @@ -77,7 +77,6 @@ class TossPaymentClient( } } - @Throws(IOException::class) private fun handlePaymentError( res: ClientHttpResponse ): Nothing { @@ -92,7 +91,6 @@ class TossPaymentClient( ) } - @Throws(IOException::class) private fun getErrorResponse( res: ClientHttpResponse ): TossPaymentErrorResponse { -- 2.47.2 From 498340f173b213112d0516b3b0742849d0c992ff Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 18:21:13 +0900 Subject: [PATCH 28/37] =?UTF-8?q?feat:=20Payment=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=9D=98=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment/business/PaymentService.kt | 86 +++++++++---------- .../payment/exception/PaymentErrorCode.kt | 16 ++++ .../payment/exception/PaymentException.kt | 8 ++ .../payment/business/PaymentServiceTest.kt | 31 +++---- 4 files changed, 74 insertions(+), 67 deletions(-) create mode 100644 src/main/kotlin/roomescape/payment/exception/PaymentErrorCode.kt create mode 100644 src/main/kotlin/roomescape/payment/exception/PaymentException.kt diff --git a/src/main/kotlin/roomescape/payment/business/PaymentService.kt b/src/main/kotlin/roomescape/payment/business/PaymentService.kt index a7343ef8..4f2d9c83 100644 --- a/src/main/kotlin/roomescape/payment/business/PaymentService.kt +++ b/src/main/kotlin/roomescape/payment/business/PaymentService.kt @@ -1,10 +1,9 @@ package roomescape.payment.business -import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException +import roomescape.payment.exception.PaymentErrorCode +import roomescape.payment.exception.PaymentException import roomescape.payment.infrastructure.client.PaymentApproveResponse import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository @@ -24,44 +23,45 @@ class PaymentService( ) { @Transactional fun createPayment( - paymentResponse: PaymentApproveResponse, + approveResponse: PaymentApproveResponse, reservation: ReservationEntity - ): PaymentCreateResponse = PaymentEntity( - orderId = paymentResponse.orderId, - paymentKey = paymentResponse.paymentKey, - totalAmount = paymentResponse.totalAmount, - reservation = reservation, - approvedAt = paymentResponse.approvedAt - ).also { - paymentRepository.save(it) - }.toCreateResponse() + ): PaymentCreateResponse { + val payment = PaymentEntity( + orderId = approveResponse.orderId, + paymentKey = approveResponse.paymentKey, + totalAmount = approveResponse.totalAmount, + reservation = reservation, + approvedAt = approveResponse.approvedAt + ) + + return paymentRepository.save(payment).toCreateResponse() + } @Transactional(readOnly = true) - fun isReservationPaid( - reservationId: Long - ): Boolean = paymentRepository.existsByReservationId(reservationId) + fun isReservationPaid(reservationId: Long): Boolean = paymentRepository.existsByReservationId(reservationId) @Transactional fun createCanceledPayment( cancelInfo: PaymentCancelResponse, approvedAt: OffsetDateTime, paymentKey: String - ): CanceledPaymentEntity = CanceledPaymentEntity( - paymentKey = paymentKey, - cancelReason = cancelInfo.cancelReason, - cancelAmount = cancelInfo.cancelAmount, - approvedAt = approvedAt, - canceledAt = cancelInfo.canceledAt - ).also { canceledPaymentRepository.save(it) } + ): CanceledPaymentEntity { + val canceledPayment = CanceledPaymentEntity( + paymentKey = paymentKey, + cancelReason = cancelInfo.cancelReason, + cancelAmount = cancelInfo.cancelAmount, + approvedAt = approvedAt, + canceledAt = cancelInfo.canceledAt + ) + + return canceledPaymentRepository.save(canceledPayment) + } @Transactional fun createCanceledPaymentByReservationId(reservationId: Long): PaymentCancelRequest { val paymentKey: String = paymentRepository.findPaymentKeyByReservationId(reservationId) - ?: throw RoomescapeException( - ErrorType.PAYMENT_NOT_FOUND, - "[reservationId: $reservationId]", - HttpStatus.NOT_FOUND - ) + ?: throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND) + // 취소 시간은 현재 시간으로 일단 생성한 뒤, 결제 취소 완료 후 해당 시간으로 변경합니다. val canceled: CanceledPaymentEntity = cancelPayment(paymentKey) @@ -73,23 +73,19 @@ class PaymentService( cancelReason: String = "고객 요청", canceledAt: OffsetDateTime = OffsetDateTime.now() ): CanceledPaymentEntity { - val paymentEntity: PaymentEntity = paymentRepository.findByPaymentKey(paymentKey) + val payment: PaymentEntity = paymentRepository.findByPaymentKey(paymentKey) ?.also { paymentRepository.delete(it) } - ?: throw RoomescapeException( - ErrorType.PAYMENT_NOT_FOUND, - "[paymentKey: $paymentKey]", - HttpStatus.NOT_FOUND - ) + ?: throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND) - return CanceledPaymentEntity( + val canceledPayment = CanceledPaymentEntity( paymentKey = paymentKey, cancelReason = cancelReason, - cancelAmount = paymentEntity.totalAmount, - approvedAt = paymentEntity.approvedAt, + cancelAmount = payment.totalAmount, + approvedAt = payment.approvedAt, canceledAt = canceledAt - ).also { - canceledPaymentRepository.save(it) - } + ) + + return canceledPaymentRepository.save(canceledPayment) } @Transactional @@ -97,12 +93,8 @@ class PaymentService( paymentKey: String, canceledAt: OffsetDateTime ) { - canceledPaymentRepository.findByPaymentKey(paymentKey)?.let { - it.canceledAt = canceledAt - } ?: throw RoomescapeException( - ErrorType.PAYMENT_NOT_FOUND, - "[paymentKey: $paymentKey]", - HttpStatus.NOT_FOUND - ) + canceledPaymentRepository.findByPaymentKey(paymentKey) + ?.apply { this.canceledAt = canceledAt } + ?: 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 new file mode 100644 index 00000000..1cad1ba4 --- /dev/null +++ b/src/main/kotlin/roomescape/payment/exception/PaymentErrorCode.kt @@ -0,0 +1,16 @@ +package roomescape.payment.exception + +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 +) : ErrorCode { + PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "P001", "결제 정보를 찾을 수 없어요."), + CANCELED_PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "P002", "취소된 결제 정보를 찾을 수 없어요."), + PAYMENT_CLIENT_ERROR(HttpStatus.BAD_REQUEST, "P003", "결제에 실패했어요. 결제 수단을 확인한 후 다시 시도해주세요."), + + PAYMENT_PROVIDER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "P999", "시스템에 일시적인 오류가 발생했어요. 잠시 후 다시 시도해주세요.") +} diff --git a/src/main/kotlin/roomescape/payment/exception/PaymentException.kt b/src/main/kotlin/roomescape/payment/exception/PaymentException.kt new file mode 100644 index 00000000..44c59be0 --- /dev/null +++ b/src/main/kotlin/roomescape/payment/exception/PaymentException.kt @@ -0,0 +1,8 @@ +package roomescape.payment.exception + +import roomescape.common.exception.RoomescapeExceptionV2 + +class PaymentException( + override val errorCode: PaymentErrorCode, + override val message: String = errorCode.message +) : RoomescapeExceptionV2(errorCode, message) diff --git a/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt b/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt index a6ceaab2..d8dd1b7d 100644 --- a/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt +++ b/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt @@ -11,6 +11,8 @@ import io.mockk.runs import org.springframework.http.HttpStatus import roomescape.common.exception.ErrorType import roomescape.common.exception.RoomescapeException +import roomescape.payment.exception.PaymentErrorCode +import roomescape.payment.exception.PaymentException import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository import roomescape.payment.infrastructure.persistence.PaymentRepository import roomescape.payment.web.PaymentCancelRequest @@ -23,19 +25,15 @@ class PaymentServiceTest : FunSpec({ val paymentService = PaymentService(paymentRepository, canceledPaymentRepository) - context("cancelPaymentByAdmin") { + context("createCanceledPaymentByReservationId") { val reservationId = 1L test("reservationId로 paymentKey를 찾을 수 없으면 예외를 던진다.") { every { paymentRepository.findPaymentKeyByReservationId(reservationId) } returns null - val exception = shouldThrow { + val exception = shouldThrow { paymentService.createCanceledPaymentByReservationId(reservationId) } - - assertSoftly(exception) { - this.errorType shouldBe ErrorType.PAYMENT_NOT_FOUND - this.httpStatus shouldBe HttpStatus.NOT_FOUND - } + exception.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND } context("reservationId로 paymentKey를 찾고난 후") { @@ -50,14 +48,10 @@ class PaymentServiceTest : FunSpec({ paymentRepository.findByPaymentKey(paymentKey) } returns null - val exception = shouldThrow { + val exception = shouldThrow { paymentService.createCanceledPaymentByReservationId(reservationId) } - - assertSoftly(exception) { - this.errorType shouldBe ErrorType.PAYMENT_NOT_FOUND - this.httpStatus shouldBe HttpStatus.NOT_FOUND - } + exception.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND } test("해당 paymentKey로 paymentEntity를 찾고, cancelPaymentEntity를 저장한다.") { @@ -76,6 +70,7 @@ class PaymentServiceTest : FunSpec({ } returns PaymentFixture.createCanceled( id = 1L, paymentKey = paymentKey, + cancelReason = "Test", cancelAmount = paymentEntity.totalAmount, ) @@ -84,7 +79,7 @@ class PaymentServiceTest : FunSpec({ assertSoftly(result) { this.paymentKey shouldBe paymentKey this.amount shouldBe paymentEntity.totalAmount - this.cancelReason shouldBe "고객 요청" + this.cancelReason shouldBe "Test" } } } @@ -99,14 +94,10 @@ class PaymentServiceTest : FunSpec({ canceledPaymentRepository.findByPaymentKey(paymentKey) } returns null - val exception = shouldThrow { + val exception = shouldThrow { paymentService.updateCanceledTime(paymentKey, canceledAt) } - - assertSoftly(exception) { - this.errorType shouldBe ErrorType.PAYMENT_NOT_FOUND - this.httpStatus shouldBe HttpStatus.NOT_FOUND - } + exception.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND } test("paymentKey로 canceledPaymentEntity를 찾고, canceledAt을 업데이트한다.") { -- 2.47.2 From e9fcc31dea893f9e8a3212dd27d174c48739f35a Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 23 Jul 2025 18:37:41 +0900 Subject: [PATCH 29/37] =?UTF-8?q?refactor:=20PaymentClient=20=EB=B0=8F=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=97=90=20=EC=83=88=EB=A1=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EB=90=9C=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/TossPaymentClient.kt | 37 ++++----- .../client/TossPaymentClientTest.kt | 77 ++++++++++--------- 2 files changed, 56 insertions(+), 58 deletions(-) diff --git a/src/main/kotlin/roomescape/payment/infrastructure/client/TossPaymentClient.kt b/src/main/kotlin/roomescape/payment/infrastructure/client/TossPaymentClient.kt index 5fa16a83..baa6594e 100644 --- a/src/main/kotlin/roomescape/payment/infrastructure/client/TossPaymentClient.kt +++ b/src/main/kotlin/roomescape/payment/infrastructure/client/TossPaymentClient.kt @@ -4,17 +4,15 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.http.HttpRequest -import org.springframework.http.HttpStatus import org.springframework.http.HttpStatusCode import org.springframework.http.MediaType import org.springframework.http.client.ClientHttpResponse import org.springframework.stereotype.Component import org.springframework.web.client.RestClient -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException +import roomescape.payment.exception.PaymentErrorCode +import roomescape.payment.exception.PaymentException import roomescape.payment.web.PaymentCancelRequest import roomescape.payment.web.PaymentCancelResponse -import java.io.IOException import java.util.Map @Component @@ -43,7 +41,7 @@ class TossPaymentClient( { req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res) } ) .body(PaymentApproveResponse::class.java) - ?: throw RoomescapeException(ErrorType.PAYMENT_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR) + ?: throw PaymentException(PaymentErrorCode.PAYMENT_PROVIDER_ERROR) } fun cancel(cancelRequest: PaymentCancelRequest): PaymentCancelResponse { @@ -60,7 +58,7 @@ class TossPaymentClient( { req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res) } ) .body(PaymentCancelResponse::class.java) - ?: throw RoomescapeException(ErrorType.PAYMENT_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR) + ?: throw PaymentException(PaymentErrorCode.PAYMENT_PROVIDER_ERROR) } private fun logPaymentInfo(paymentRequest: PaymentApproveRequest) { @@ -80,32 +78,25 @@ class TossPaymentClient( private fun handlePaymentError( res: ClientHttpResponse ): Nothing { - val statusCode = res.statusCode - val errorType = getErrorTypeByStatusCode(statusCode) - val errorResponse = getErrorResponse(res) - - throw RoomescapeException( - errorType, - "[ErrorCode = ${errorResponse.code}, ErrorMessage = ${errorResponse.message}]", - statusCode - ) + getErrorCodeByHttpStatus(res.statusCode).also { + logTossPaymentError(res) + throw PaymentException(it) + } } - private fun getErrorResponse( - res: ClientHttpResponse - ): TossPaymentErrorResponse { + private fun logTossPaymentError(res: ClientHttpResponse): TossPaymentErrorResponse { val body = res.body val errorResponse = objectMapper.readValue(body, TossPaymentErrorResponse::class.java) body.close() + + log.error { "결제 실패. response: $errorResponse" } return errorResponse } - private fun getErrorTypeByStatusCode( - statusCode: HttpStatusCode - ): ErrorType { + private fun getErrorCodeByHttpStatus(statusCode: HttpStatusCode): PaymentErrorCode { if (statusCode.is4xxClientError) { - return ErrorType.PAYMENT_ERROR + return PaymentErrorCode.PAYMENT_CLIENT_ERROR } - return ErrorType.PAYMENT_SERVER_ERROR + return PaymentErrorCode.PAYMENT_PROVIDER_ERROR } } diff --git a/src/test/kotlin/roomescape/payment/infrastructure/client/TossPaymentClientTest.kt b/src/test/kotlin/roomescape/payment/infrastructure/client/TossPaymentClientTest.kt index b4f6e5c4..8f6eae96 100644 --- a/src/test/kotlin/roomescape/payment/infrastructure/client/TossPaymentClientTest.kt +++ b/src/test/kotlin/roomescape/payment/infrastructure/client/TossPaymentClientTest.kt @@ -14,8 +14,8 @@ import org.springframework.test.web.client.ResponseActions import org.springframework.test.web.client.match.MockRestRequestMatchers.* import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus import org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException +import roomescape.payment.exception.PaymentErrorCode +import roomescape.payment.exception.PaymentException import roomescape.payment.web.PaymentCancelRequest import roomescape.payment.web.PaymentCancelResponse @@ -56,28 +56,32 @@ class TossPaymentClientTest( } } - test("400 에러 발생") { - commonAction().andRespond { - withStatus(HttpStatus.BAD_REQUEST) - .contentType(MediaType.APPLICATION_JSON) - .body(SampleTossPaymentConst.tossPaymentErrorJson) - .createResponse(it) + context("실패 응답") { + fun runTest(httpStatus: HttpStatus, expectedError: PaymentErrorCode) { + commonAction().andRespond { + withStatus(httpStatus) + .contentType(MediaType.APPLICATION_JSON) + .body(SampleTossPaymentConst.tossPaymentErrorJson) + .createResponse(it) + } + + // when + val paymentRequest = SampleTossPaymentConst.paymentRequest + + // then + val exception = shouldThrow { + client.confirm(paymentRequest) + } + exception.errorCode shouldBe expectedError } - // when - val paymentRequest = SampleTossPaymentConst.paymentRequest - - // then - val exception = shouldThrow { - client.confirm(paymentRequest) + test("결제 서버에서 4XX 응답 시") { + runTest(HttpStatus.BAD_REQUEST, PaymentErrorCode.PAYMENT_CLIENT_ERROR) } - assertSoftly(exception) { - this.errorType shouldBe ErrorType.PAYMENT_ERROR - this.invalidValue shouldBe "[ErrorCode = ERROR_CODE, ErrorMessage = Error message]" - this.httpStatus shouldBe HttpStatus.BAD_REQUEST + test("결제 서버에서 5XX 응답 시") { + runTest(HttpStatus.INTERNAL_SERVER_ERROR, PaymentErrorCode.PAYMENT_PROVIDER_ERROR) } - } } @@ -111,26 +115,29 @@ class TossPaymentClientTest( } } - test("500 에러 발생") { - commonAction().andRespond { - withStatus(HttpStatus.INTERNAL_SERVER_ERROR) - .contentType(MediaType.APPLICATION_JSON) - .body(SampleTossPaymentConst.tossPaymentErrorJson) - .createResponse(it) + context("실패 응답") { + fun runTest(httpStatus: HttpStatus, expectedError: PaymentErrorCode) { + commonAction().andRespond { + withStatus(httpStatus) + .contentType(MediaType.APPLICATION_JSON) + .body(SampleTossPaymentConst.tossPaymentErrorJson) + .createResponse(it) + } + + val cancelRequest: PaymentCancelRequest = SampleTossPaymentConst.cancelRequest + + val exception = shouldThrow { + client.cancel(cancelRequest) + } + exception.errorCode shouldBe expectedError } - // when - val cancelRequest: PaymentCancelRequest = SampleTossPaymentConst.cancelRequest - - // then - val exception = shouldThrow { - client.cancel(cancelRequest) + test("결제 서버에서 4XX 응답 시") { + runTest(HttpStatus.BAD_REQUEST, PaymentErrorCode.PAYMENT_CLIENT_ERROR) } - assertSoftly(exception) { - this.errorType shouldBe ErrorType.PAYMENT_SERVER_ERROR - this.invalidValue shouldBe "[ErrorCode = ERROR_CODE, ErrorMessage = Error message]" - this.httpStatus shouldBe HttpStatus.INTERNAL_SERVER_ERROR + test("결제 서버에서 5XX 응답 시") { + runTest(HttpStatus.INTERNAL_SERVER_ERROR, PaymentErrorCode.PAYMENT_PROVIDER_ERROR) } } } -- 2.47.2 From 2d4b67ad9880d42554245ec1709482cd031cc0a8 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Jul 2025 09:56:59 +0900 Subject: [PATCH 30/37] =?UTF-8?q?feat:=20reservation=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=9D=98=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/ReservationErrorCode.kt | 20 +++++++++++++++++++ .../exception/ReservationException.kt | 9 +++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/main/kotlin/roomescape/reservation/exception/ReservationErrorCode.kt create mode 100644 src/main/kotlin/roomescape/reservation/exception/ReservationException.kt diff --git a/src/main/kotlin/roomescape/reservation/exception/ReservationErrorCode.kt b/src/main/kotlin/roomescape/reservation/exception/ReservationErrorCode.kt new file mode 100644 index 00000000..fc61f9a3 --- /dev/null +++ b/src/main/kotlin/roomescape/reservation/exception/ReservationErrorCode.kt @@ -0,0 +1,20 @@ +package roomescape.reservation.exception + +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 +) : ErrorCode { + RESERVATION_NOT_FOUND(HttpStatus.NOT_FOUND, "R001", "예약을 찾을 수 없어요."), + RESERVATION_DUPLICATED(HttpStatus.BAD_REQUEST, "R002", "이미 같은 예약이 있어요."), + ALREADY_RESERVE(HttpStatus.BAD_REQUEST, "R003", "같은 날짜, 시간, 테마에 대한 예약(대기)는 한 번만 가능해요."), + ALREADY_CONFIRMED(HttpStatus.CONFLICT, "R004", "이미 확정된 예약이에요"), + CONFIRMED_RESERVATION_ALREADY_EXISTS(HttpStatus.CONFLICT, "R005", "이미 확정된 예약이 있어서 승인할 수 없어요."), + PAST_REQUEST_DATETIME(HttpStatus.BAD_REQUEST, "R005", "과거 시간으로 예약할 수 없어요."), + NOT_RESERVATION_OWNER(HttpStatus.FORBIDDEN, "R006", "타인의 예약은 취소할 수 없어요."), + INVALID_SEARCH_DATE_RANGE(HttpStatus.BAD_REQUEST, "R007", "종료 날짜는 시작 날짜 이후여야 해요."), + NO_PERMISSION(HttpStatus.FORBIDDEN, "R008", "접근 권한이 없어요."), +} diff --git a/src/main/kotlin/roomescape/reservation/exception/ReservationException.kt b/src/main/kotlin/roomescape/reservation/exception/ReservationException.kt new file mode 100644 index 00000000..885d39c5 --- /dev/null +++ b/src/main/kotlin/roomescape/reservation/exception/ReservationException.kt @@ -0,0 +1,9 @@ +package roomescape.reservation.exception + +import roomescape.common.exception.ErrorCode +import roomescape.common.exception.RoomescapeExceptionV2 + +class ReservationException( + override val errorCode: ErrorCode, + override val message: String = errorCode.message +) : RoomescapeExceptionV2(errorCode, message) -- 2.47.2 From faf6e408b6f2949cfe591887ab86c8f9002c92b1 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Jul 2025 09:57:46 +0900 Subject: [PATCH 31/37] =?UTF-8?q?refactor:=20=EC=83=88=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=EB=90=9C=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=EB=A5=BC=20Service=EC=97=90=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/ReservationService.kt | 107 ++++++++---------- .../business/ReservationWithPaymentService.kt | 2 +- .../persistence/ReservationEntity.kt | 2 +- .../persistence/ReservationRepository.kt | 2 +- 4 files changed, 53 insertions(+), 60 deletions(-) diff --git a/src/main/kotlin/roomescape/reservation/business/ReservationService.kt b/src/main/kotlin/roomescape/reservation/business/ReservationService.kt index 7e8f5f94..b7a17aaa 100644 --- a/src/main/kotlin/roomescape/reservation/business/ReservationService.kt +++ b/src/main/kotlin/roomescape/reservation/business/ReservationService.kt @@ -2,18 +2,19 @@ package roomescape.reservation.business import org.springframework.data.jpa.domain.Specification import org.springframework.data.repository.findByIdOrNull -import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException import roomescape.member.business.MemberService +import roomescape.member.infrastructure.persistence.MemberEntity +import roomescape.reservation.exception.ReservationErrorCode +import roomescape.reservation.exception.ReservationException import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationRepository import roomescape.reservation.infrastructure.persistence.ReservationSearchSpecification import roomescape.reservation.infrastructure.persistence.ReservationStatus import roomescape.reservation.web.* import roomescape.theme.business.ThemeService +import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.time.business.TimeService import roomescape.time.infrastructure.persistence.TimeEntity import java.time.LocalDate @@ -34,7 +35,6 @@ class ReservationService( .confirmed() .build() - return ReservationRetrieveListResponse(findAllReservationByStatus(spec)) } @@ -56,17 +56,18 @@ class ReservationService( reservationRepository.deleteById(reservationId) } - fun addReservation(request: ReservationCreateWithPaymentRequest, memberId: Long): ReservationEntity { - validateIsReservationExist(request.themeId, request.timeId, request.date) - return getReservationForSave( - request.timeId, - request.themeId, - request.date, - memberId, - ReservationStatus.CONFIRMED - ).also { - reservationRepository.save(it) - } + fun createConfirmedReservation( + request: ReservationCreateWithPaymentRequest, + memberId: Long + ): ReservationEntity { + val themeId = request.themeId + val timeId = request.timeId + val date: LocalDate = request.date + validateIsReservationExist(themeId, timeId, date) + + val reservation: ReservationEntity = createEntity(timeId, themeId, date, memberId, ReservationStatus.CONFIRMED) + + return reservationRepository.save(reservation) } fun createReservationByAdmin(request: AdminReservationCreateRequest): ReservationRetrieveResponse { @@ -98,12 +99,12 @@ class ReservationService( date: LocalDate, memberId: Long, status: ReservationStatus - ): ReservationRetrieveResponse = getReservationForSave(timeId, themeId, date, memberId, status) + ): ReservationRetrieveResponse = createEntity(timeId, themeId, date, memberId, status) .also { reservationRepository.save(it) }.toRetrieveResponse() - private fun validateMemberAlreadyReserve(themeId: Long?, timeId: Long?, date: LocalDate?, memberId: Long?) { + private fun validateMemberAlreadyReserve(themeId: Long, timeId: Long, date: LocalDate, memberId: Long) { val spec: Specification = ReservationSearchSpecification() .sameMemberId(memberId) .sameThemeId(themeId) @@ -112,7 +113,7 @@ class ReservationService( .build() if (reservationRepository.exists(spec)) { - throw RoomescapeException(ErrorType.HAS_RESERVATION_OR_WAITING, HttpStatus.BAD_REQUEST) + throw ReservationException(ReservationErrorCode.ALREADY_RESERVE) } } @@ -125,7 +126,7 @@ class ReservationService( .build() if (reservationRepository.exists(spec)) { - throw RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT) + throw ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED) } } @@ -137,24 +138,20 @@ class ReservationService( val request = LocalDateTime.of(requestDate, requestTime.startAt) if (request.isBefore(now)) { - throw RoomescapeException( - ErrorType.RESERVATION_PERIOD_IN_PAST, - "[now: $now | request: $request]", - HttpStatus.BAD_REQUEST - ) + throw ReservationException(ReservationErrorCode.PAST_REQUEST_DATETIME) } } - private fun getReservationForSave( + private fun createEntity( timeId: Long, themeId: Long, date: LocalDate, memberId: Long, status: ReservationStatus ): ReservationEntity { - val time = timeService.findById(timeId) - val theme = themeService.findById(themeId) - val member = memberService.findById(memberId) + val time: TimeEntity = timeService.findById(timeId) + val theme: ThemeEntity = themeService.findById(themeId) + val member: MemberEntity = memberService.findById(memberId) validateDateAndTime(date, time) @@ -191,58 +188,54 @@ class ReservationService( return } if (startFrom.isAfter(endAt)) { - throw RoomescapeException( - ErrorType.INVALID_DATE_RANGE, - "[startFrom: $startFrom, endAt: $endAt", HttpStatus.BAD_REQUEST - ) + throw ReservationException(ReservationErrorCode.INVALID_SEARCH_DATE_RANGE) } } @Transactional(readOnly = true) fun findReservationsByMemberId(memberId: Long): MyReservationRetrieveListResponse { - return MyReservationRetrieveListResponse(reservationRepository.findAllById(memberId)) + return MyReservationRetrieveListResponse(reservationRepository.findAllByMemberId(memberId)) } fun confirmWaiting(reservationId: Long, memberId: Long) { validateIsMemberAdmin(memberId) if (reservationRepository.isExistConfirmedReservation(reservationId)) { - throw RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT) + throw ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS) } reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED) } fun deleteWaiting(reservationId: Long, memberId: Long) { - reservationRepository.findByIdOrNull(reservationId)?.takeIf { - it.isWaiting() && it.isSameMember(memberId) - }?.let { - reservationRepository.delete(it) - } ?: throw throwReservationNotFound(reservationId) + val reservation: ReservationEntity = findReservationOrThrow(reservationId) + if (!reservation.isWaiting()) { + throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED) + } + if (!reservation.isReservedBy(memberId)) { + throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER) + } + reservationRepository.delete(reservation) } fun rejectWaiting(reservationId: Long, memberId: Long) { validateIsMemberAdmin(memberId) - reservationRepository.findByIdOrNull(reservationId)?.takeIf { - it.isWaiting() - }?.let { - reservationRepository.delete(it) - } ?: throw throwReservationNotFound(reservationId) + val reservation: ReservationEntity = findReservationOrThrow(reservationId) + + if (!reservation.isWaiting()) { + throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED) + } + reservationRepository.delete(reservation) } private fun validateIsMemberAdmin(memberId: Long) { - memberService.findById(memberId).takeIf { - it.isAdmin() - } ?: throw RoomescapeException( - ErrorType.PERMISSION_DOES_NOT_EXIST, - "[memberId: $memberId]", - HttpStatus.FORBIDDEN - ) + val member: MemberEntity = memberService.findById(memberId) + if (member.isAdmin()) { + return + } + throw ReservationException(ReservationErrorCode.NO_PERMISSION) } - private fun throwReservationNotFound(reservationId: Long?): RoomescapeException { - return RoomescapeException( - ErrorType.RESERVATION_NOT_FOUND, - "[reservationId: $reservationId]", - HttpStatus.NOT_FOUND - ) + private fun findReservationOrThrow(reservationId: Long): ReservationEntity { + return reservationRepository.findByIdOrNull(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 3d48e276..f8705f38 100644 --- a/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt +++ b/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt @@ -22,7 +22,7 @@ class ReservationWithPaymentService( paymentInfo: PaymentApproveResponse, memberId: Long ): ReservationRetrieveResponse { - val reservation: ReservationEntity = reservationService.addReservation(request, memberId) + val reservation: ReservationEntity = reservationService.createConfirmedReservation(request, memberId) return paymentService.createPayment(paymentInfo, reservation) .reservation diff --git a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationEntity.kt b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationEntity.kt index 31acb5f6..6153ef28 100644 --- a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationEntity.kt +++ b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationEntity.kt @@ -35,7 +35,7 @@ class ReservationEntity( fun isWaiting(): Boolean = reservationStatus == ReservationStatus.WAITING @JsonIgnore - fun isSameMember(memberId: Long): Boolean { + fun isReservedBy(memberId: Long): Boolean { return this.member.id == memberId } } diff --git a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt index 3dd73807..43e9cf42 100644 --- a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt +++ b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt @@ -59,5 +59,5 @@ interface ReservationRepository ON p.reservation = r WHERE r.member.id = :memberId """) - fun findAllById(memberId: Long): List + fun findAllByMemberId(memberId: Long): List } -- 2.47.2 From 31538c2632d3803f653125c907fccd7dc490c4f9 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Jul 2025 09:58:04 +0900 Subject: [PATCH 32/37] =?UTF-8?q?refactor:=20service=EC=97=90=EC=84=9C?= =?UTF-8?q?=EC=9D=98=20=EB=B3=80=EA=B2=BD=20=EC=82=AC=ED=95=AD=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B0=98=EC=98=81=20=EB=B0=8F=20=EB=AF=B8?= =?UTF-8?q?=EB=B9=84=EB=90=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/ReservationServiceTest.kt | 38 +++++------ .../ReservationWithPaymentServiceTest.kt | 2 +- .../persistence/ReservationRepositoryTest.kt | 4 +- .../web/ReservationControllerTest.kt | 67 ++++++++++++++----- 4 files changed, 74 insertions(+), 37 deletions(-) diff --git a/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt b/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt index 408718d4..32efd17d 100644 --- a/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt +++ b/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt @@ -5,10 +5,10 @@ import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockk -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException import roomescape.member.business.MemberService import roomescape.member.infrastructure.persistence.Role +import roomescape.reservation.exception.ReservationErrorCode +import roomescape.reservation.exception.ReservationException import roomescape.reservation.infrastructure.persistence.ReservationRepository import roomescape.theme.business.ThemeService import roomescape.time.business.TimeService @@ -39,10 +39,10 @@ class ReservationServiceTest : FunSpec({ val reservationRequest = ReservationFixture.createRequest() - shouldThrow { - reservationService.addReservation(reservationRequest, 1L) + shouldThrow { + reservationService.createConfirmedReservation(reservationRequest, 1L) }.also { - it.errorType shouldBe ErrorType.RESERVATION_DUPLICATED + it.errorCode shouldBe ReservationErrorCode.RESERVATION_DUPLICATED } } @@ -69,10 +69,10 @@ class ReservationServiceTest : FunSpec({ timeService.findById(any()) } returns TimeFixture.create() - shouldThrow { - reservationService.addReservation(reservationRequest, 1L) + shouldThrow { + reservationService.createConfirmedReservation(reservationRequest, 1L) }.also { - it.errorType shouldBe ErrorType.RESERVATION_PERIOD_IN_PAST + it.errorCode shouldBe ReservationErrorCode.PAST_REQUEST_DATETIME } } @@ -87,10 +87,10 @@ class ReservationServiceTest : FunSpec({ startAt = LocalTime.now().minusMinutes(1) ) - shouldThrow { - reservationService.addReservation(reservationRequest, 1L) + shouldThrow { + reservationService.createConfirmedReservation(reservationRequest, 1L) }.also { - it.errorType shouldBe ErrorType.RESERVATION_PERIOD_IN_PAST + it.errorCode shouldBe ReservationErrorCode.PAST_REQUEST_DATETIME } } } @@ -108,7 +108,7 @@ class ReservationServiceTest : FunSpec({ reservationRepository.exists(any()) } returns true - shouldThrow { + shouldThrow { val waitingRequest = ReservationFixture.createWaitingRequest( date = reservationRequest.date, themeId = reservationRequest.themeId, @@ -116,7 +116,7 @@ class ReservationServiceTest : FunSpec({ ) reservationService.createWaiting(waitingRequest, 1L) }.also { - it.errorType shouldBe ErrorType.HAS_RESERVATION_OR_WAITING + it.errorCode shouldBe ReservationErrorCode.ALREADY_RESERVE } } } @@ -126,7 +126,7 @@ class ReservationServiceTest : FunSpec({ val startFrom = LocalDate.now() val endAt = startFrom.minusDays(1) - shouldThrow { + shouldThrow { reservationService.searchReservations( null, null, @@ -134,7 +134,7 @@ class ReservationServiceTest : FunSpec({ endAt ) }.also { - it.errorType shouldBe ErrorType.INVALID_DATE_RANGE + it.errorCode shouldBe ReservationErrorCode.INVALID_SEARCH_DATE_RANGE } } } @@ -147,10 +147,10 @@ class ReservationServiceTest : FunSpec({ memberService.findById(any()) } returns member - shouldThrow { + shouldThrow { reservationService.confirmWaiting(1L, member.id!!) }.also { - it.errorType shouldBe ErrorType.PERMISSION_DOES_NOT_EXIST + it.errorCode shouldBe ReservationErrorCode.NO_PERMISSION } } @@ -166,10 +166,10 @@ class ReservationServiceTest : FunSpec({ reservationRepository.isExistConfirmedReservation(reservationId) } returns true - shouldThrow { + shouldThrow { reservationService.confirmWaiting(reservationId, member.id!!) }.also { - it.errorType shouldBe ErrorType.RESERVATION_DUPLICATED + it.errorCode shouldBe ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS } } } diff --git a/src/test/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt b/src/test/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt index dcbf069b..c9c5af68 100644 --- a/src/test/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt +++ b/src/test/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt @@ -48,7 +48,7 @@ class ReservationWithPaymentServiceTest : FunSpec({ context("addReservationWithPayment") { test("예약 및 결제 정보를 저장한다.") { every { - reservationService.addReservation(reservationCreateWithPaymentRequest, memberId) + reservationService.createConfirmedReservation(reservationCreateWithPaymentRequest, memberId) } returns reservationEntity every { diff --git a/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt b/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt index 0ae319ae..c41da6b3 100644 --- a/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt +++ b/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt @@ -168,7 +168,7 @@ class ReservationRepositoryTest( entityManager.clear() } - val result: List = reservationRepository.findAllById(reservation.member.id!!) + val result: List = reservationRepository.findAllByMemberId(reservation.member.id!!) result shouldHaveSize 1 assertSoftly(result.first()) { @@ -179,7 +179,7 @@ class ReservationRepositoryTest( } test("결제 정보가 없다면 paymentKey와 amount는 null로 반환한다.") { - val result: List = reservationRepository.findAllById(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/web/ReservationControllerTest.kt b/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt index 5a94cae9..04e3fb2a 100644 --- a/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt +++ b/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt @@ -19,13 +19,14 @@ import org.springframework.http.MediaType import org.springframework.transaction.support.TransactionTemplate import roomescape.auth.infrastructure.jwt.JwtHandler import roomescape.auth.web.support.MemberIdResolver -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException import roomescape.member.business.MemberService import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.member.infrastructure.persistence.Role +import roomescape.payment.exception.PaymentErrorCode +import roomescape.payment.exception.PaymentException import roomescape.payment.infrastructure.client.TossPaymentClient import roomescape.payment.infrastructure.persistence.PaymentEntity +import roomescape.reservation.exception.ReservationErrorCode import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationStatus import roomescape.theme.exception.ThemeErrorCode @@ -89,10 +90,7 @@ class ReservationControllerTest( test("결제 과정에서 발생하는 에러는 그대로 응답") { val reservationRequest = createRequest() - val paymentException = RoomescapeException( - ErrorType.PAYMENT_SERVER_ERROR, - HttpStatus.INTERNAL_SERVER_ERROR - ) + val paymentException = PaymentException(PaymentErrorCode.PAYMENT_PROVIDER_ERROR) every { paymentClient.confirm(any()) @@ -105,8 +103,8 @@ class ReservationControllerTest( }.When { post("/reservations") }.Then { - statusCode(paymentException.httpStatus.value()) - body("errorType", equalTo(paymentException.errorType.name)) + statusCode(paymentException.errorCode.httpStatus.value()) + body("code", equalTo(paymentException.errorCode.errorCode)) } } @@ -235,6 +233,7 @@ class ReservationControllerTest( val startDate = LocalDate.now().plusDays(1) val endDate = LocalDate.now() + val expectedError = ReservationErrorCode.INVALID_SEARCH_DATE_RANGE Given { port(port) @@ -244,8 +243,8 @@ class ReservationControllerTest( }.When { get("/reservations/search") }.Then { - statusCode(HttpStatus.BAD_REQUEST.value()) - body("errorType", equalTo(ErrorType.INVALID_DATE_RANGE.name)) + statusCode(expectedError.httpStatus.value()) + body("code", equalTo(expectedError.errorCode)) } } @@ -501,6 +500,7 @@ class ReservationControllerTest( themeId = reservationRequest.themeId, timeId = reservationRequest.timeId ) + val expectedError = ReservationErrorCode.ALREADY_RESERVE Given { port(port) @@ -509,8 +509,8 @@ class ReservationControllerTest( }.When { post("/reservations/waiting") }.Then { - statusCode(HttpStatus.BAD_REQUEST.value()) - body("errorType", equalTo(ErrorType.HAS_RESERVATION_OR_WAITING.name)) + statusCode(expectedError.httpStatus.value()) + body("code", equalTo(expectedError.errorCode)) } } } @@ -544,20 +544,21 @@ class ReservationControllerTest( } } - test("이미 완료된 예약은 삭제할 수 없다.") { + test("이미 확정된 예약을 삭제하면 예외 응답") { val member = login(MemberFixture.create(role = Role.MEMBER)) val reservation: ReservationEntity = createSingleReservation( member = member, status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED ) + val expectedError = ReservationErrorCode.ALREADY_CONFIRMED Given { port(port) }.When { delete("/reservations/waiting/{id}", reservation.id) }.Then { - body("errorType", equalTo(ErrorType.RESERVATION_NOT_FOUND.name)) - statusCode(HttpStatus.NOT_FOUND.value()) + statusCode(expectedError.httpStatus.value()) + body("code", equalTo(expectedError.errorCode)) } } } @@ -600,6 +601,42 @@ class ReservationControllerTest( } ?: throw AssertionError("Reservation not found") } } + + test("다른 확정된 예약을 승인하면 예외 응답") { + val admin = login(MemberFixture.create(role = Role.ADMIN)) + val alreadyReserved = createSingleReservation( + member = admin, + status = ReservationStatus.CONFIRMED + ) + + val member = MemberFixture.create(account = "account", role = Role.MEMBER).also { it -> + transactionTemplate.executeWithoutResult { _ -> + entityManager.persist(it) + } + } + val waiting = ReservationFixture.create( + date = alreadyReserved.date, + time = alreadyReserved.time, + theme = alreadyReserved.theme, + member = member, + status = ReservationStatus.WAITING + ).also { + transactionTemplate.executeWithoutResult { _ -> + entityManager.persist(it) + } + } + + val expectedError = ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS + Given { + port(port) + }.When { + post("/reservations/waiting/${waiting.id!!}/confirm") + }.Then { + log().all() + statusCode(expectedError.httpStatus.value()) + body("code", equalTo(expectedError.errorCode)) + } + } } context("POST /reservations/waiting/{id}/reject") { -- 2.47.2 From 74f87e4a2ab0430e1252b34d52473615e9c6f4b4 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Jul 2025 10:02:18 +0900 Subject: [PATCH 33/37] =?UTF-8?q?refactor:=20=EC=83=88=EB=A1=9C=EC=9A=B4?= =?UTF-8?q?=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=98=88=EC=99=B8=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=EC=9C=BC=EB=A1=9C=20=EA=B8=B0=EC=A1=B4=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=EC=98=88=EC=99=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/exception/AuthException.kt | 4 +- .../common/dto/response/CommonApiResponse.kt | 6 --- .../roomescape/common/exception/ErrorType.kt | 53 ------------------- .../exception/ExceptionControllerAdvice.kt | 28 ++++------ .../common/exception/RoomescapeException.kt | 12 +---- .../member/exception/MemberException.kt | 4 +- .../payment/exception/PaymentException.kt | 4 +- .../exception/ReservationException.kt | 4 +- .../theme/exception/ThemeException.kt | 4 +- .../time/exception/TimeException.kt | 4 +- 10 files changed, 22 insertions(+), 101 deletions(-) delete mode 100644 src/main/kotlin/roomescape/common/exception/ErrorType.kt diff --git a/src/main/kotlin/roomescape/auth/exception/AuthException.kt b/src/main/kotlin/roomescape/auth/exception/AuthException.kt index 55d9a839..97fb8410 100644 --- a/src/main/kotlin/roomescape/auth/exception/AuthException.kt +++ b/src/main/kotlin/roomescape/auth/exception/AuthException.kt @@ -1,8 +1,8 @@ package roomescape.auth.exception -import roomescape.common.exception.RoomescapeExceptionV2 +import roomescape.common.exception.RoomescapeException class AuthException( override val errorCode: AuthErrorCode, override val message: String = errorCode.message -) : RoomescapeExceptionV2(errorCode, message) +) : RoomescapeException(errorCode, message) diff --git a/src/main/kotlin/roomescape/common/dto/response/CommonApiResponse.kt b/src/main/kotlin/roomescape/common/dto/response/CommonApiResponse.kt index 9cf1fc7f..1c9e1f17 100644 --- a/src/main/kotlin/roomescape/common/dto/response/CommonApiResponse.kt +++ b/src/main/kotlin/roomescape/common/dto/response/CommonApiResponse.kt @@ -2,7 +2,6 @@ package roomescape.common.dto.response import com.fasterxml.jackson.annotation.JsonInclude import roomescape.common.exception.ErrorCode -import roomescape.common.exception.ErrorType @JsonInclude(JsonInclude.Include.NON_NULL) data class CommonApiResponse( @@ -10,11 +9,6 @@ data class CommonApiResponse( ) data class CommonErrorResponse( - val errorType: ErrorType, - val message: String? = errorType.description -) - -data class CommonErrorResponseV2( val code: String, val message: String ) { diff --git a/src/main/kotlin/roomescape/common/exception/ErrorType.kt b/src/main/kotlin/roomescape/common/exception/ErrorType.kt deleted file mode 100644 index d002f14d..00000000 --- a/src/main/kotlin/roomescape/common/exception/ErrorType.kt +++ /dev/null @@ -1,53 +0,0 @@ -package roomescape.common.exception - -enum class ErrorType( - val description: String -) { - // 400 Bad Request - REQUEST_DATA_BLANK("요청 데이터에 유효하지 않은 값(null OR 공백)이 포함되어있습니다."), - INVALID_REQUEST_DATA_TYPE("요청 데이터 형식이 올바르지 않습니다."), - INVALID_REQUEST_DATA("요청 데이터 값이 올바르지 않습니다."), - INVALID_DATE_RANGE("종료 날짜는 시작 날짜 이전일 수 없습니다."), - HAS_RESERVATION_OR_WAITING("같은 테마에 대한 예약(대기)는 한 번만 가능합니다."), - - // 401 Unauthorized - EXPIRED_TOKEN("토큰이 만료되었습니다. 다시 로그인 해주세요."), - UNSUPPORTED_TOKEN("지원하지 않는 JWT 토큰입니다."), - MALFORMED_TOKEN("형식이 맞지 않는 JWT 토큰입니다."), - INVALID_SIGNATURE_TOKEN("잘못된 JWT 토큰 Signature 입니다."), - ILLEGAL_TOKEN("JWT 토큰의 Claim 이 비어있습니다."), - INVALID_TOKEN("JWT 토큰이 존재하지 않거나 유효하지 않습니다."), - NOT_EXIST_COOKIE("쿠키가 존재하지 않습니다. 로그인이 필요한 서비스입니다."), - - // 403 Forbidden - LOGIN_REQUIRED("로그인이 필요한 서비스입니다."), - PERMISSION_DOES_NOT_EXIST("접근 권한이 존재하지 않습니다."), - - // 404 Not Found - MEMBER_NOT_FOUND("회원(Member) 정보가 존재하지 않습니다."), - RESERVATION_NOT_FOUND("예약(Reservation) 정보가 존재하지 않습니다."), - TIME_NOT_FOUND("예약 시간(Time) 정보가 존재하지 않습니다."), - THEME_NOT_FOUND("테마(Theme) 정보가 존재하지 않습니다."), - PAYMENT_NOT_FOUND("결제(Payment) 정보가 존재하지 않습니다."), - - // 405 Method Not Allowed - METHOD_NOT_ALLOWED("지원하지 않는 HTTP Method 입니다."), - - // 409 Conflict - TIME_IS_USED_CONFLICT("삭제할 수 없는 시간대입니다. 예약이 존재하는지 확인해주세요."), - THEME_IS_USED_CONFLICT("삭제할 수 없는 테마입니다. 예약이 존재하는지 확인해주세요."), - TIME_DUPLICATED("이미 해당 시간이 존재합니다."), - THEME_DUPLICATED("같은 이름의 테마가 존재합니다."), - RESERVATION_DUPLICATED("해당 시간에 이미 예약이 존재합니다."), - RESERVATION_PERIOD_IN_PAST("이미 지난 시간대는 예약할 수 없습니다."), - CANCELED_BEFORE_PAYMENT("취소 시간이 결제 시간 이전일 수 없습니다."), - - // 500 Internal Server Error, - INTERNAL_SERVER_ERROR("서버 내부에서 에러가 발생하였습니다."), - UNEXPECTED_ERROR("예상치 못한 에러가 발생하였습니다. 잠시 후 다시 시도해주세요."), - - // Payment Error - PAYMENT_ERROR("결제(취소)에 실패했습니다. 결제(취소) 정보를 확인해주세요."), - PAYMENT_SERVER_ERROR("결제 서버에서 에러가 발생하였습니다. 잠시 후 다시 시도해주세요.") - ; -} diff --git a/src/main/kotlin/roomescape/common/exception/ExceptionControllerAdvice.kt b/src/main/kotlin/roomescape/common/exception/ExceptionControllerAdvice.kt index ef6a0914..2a877347 100644 --- a/src/main/kotlin/roomescape/common/exception/ExceptionControllerAdvice.kt +++ b/src/main/kotlin/roomescape/common/exception/ExceptionControllerAdvice.kt @@ -8,43 +8,33 @@ import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice import roomescape.common.dto.response.CommonErrorResponse -import roomescape.common.dto.response.CommonErrorResponseV2 @RestControllerAdvice class ExceptionControllerAdvice( private val logger: KLogger = KotlinLogging.logger {} ) { - @ExceptionHandler(value = [RoomescapeExceptionV2::class]) - fun handleRoomException(e: RoomescapeExceptionV2): ResponseEntity { + @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(CommonErrorResponseV2(errorCode, e.message)) - } - - @ExceptionHandler(value = [RoomescapeException::class]) - fun handleRoomEscapeException(e: RoomescapeException): ResponseEntity { - logger.error(e) { "message: ${e.message}, invalidValue: ${e.invalidValue}" } - - return ResponseEntity - .status(e.httpStatus) - .body(CommonErrorResponse(e.errorType)) + .body(CommonErrorResponse(errorCode, e.message)) } @ExceptionHandler(value = [HttpMessageNotReadableException::class]) - fun handleHttpMessageNotReadableException(e: HttpMessageNotReadableException): ResponseEntity { + fun handleHttpMessageNotReadableException(e: HttpMessageNotReadableException): ResponseEntity { logger.error(e) { "message: ${e.message}" } val errorCode: ErrorCode = CommonErrorCode.INVALID_INPUT_VALUE return ResponseEntity .status(errorCode.httpStatus) - .body(CommonErrorResponseV2(errorCode, e.message ?: errorCode.message)) + .body(CommonErrorResponse(errorCode)) } @ExceptionHandler(value = [MethodArgumentNotValidException::class]) - fun handleMethodArgumentNotValidException(e: MethodArgumentNotValidException): ResponseEntity { + fun handleMethodArgumentNotValidException(e: MethodArgumentNotValidException): ResponseEntity { val message: String = e.bindingResult.allErrors .mapNotNull { it.defaultMessage } .joinToString(", ") @@ -53,16 +43,16 @@ class ExceptionControllerAdvice( val errorCode: ErrorCode = CommonErrorCode.INVALID_INPUT_VALUE return ResponseEntity .status(errorCode.httpStatus) - .body(CommonErrorResponseV2(errorCode, message)) + .body(CommonErrorResponse(errorCode)) } @ExceptionHandler(value = [Exception::class]) - fun handleException(e: Exception): ResponseEntity { + fun handleException(e: Exception): ResponseEntity { logger.error(e) { "message: ${e.message}" } val errorCode: ErrorCode = CommonErrorCode.UNEXPECTED_SERVER_ERROR return ResponseEntity .status(errorCode.httpStatus) - .body(CommonErrorResponseV2(errorCode)) + .body(CommonErrorResponse(errorCode)) } } diff --git a/src/main/kotlin/roomescape/common/exception/RoomescapeException.kt b/src/main/kotlin/roomescape/common/exception/RoomescapeException.kt index 44d5673a..dcd8353f 100644 --- a/src/main/kotlin/roomescape/common/exception/RoomescapeException.kt +++ b/src/main/kotlin/roomescape/common/exception/RoomescapeException.kt @@ -1,16 +1,6 @@ package roomescape.common.exception -import org.springframework.http.HttpStatusCode - -class RoomescapeException( - val errorType: ErrorType, - val invalidValue: String? = "", - val httpStatus: HttpStatusCode, -) : RuntimeException(errorType.description) { - constructor(errorType: ErrorType, httpStatus: HttpStatusCode) : this(errorType, null, httpStatus) -} - -open class RoomescapeExceptionV2( +open class RoomescapeException( open val errorCode: ErrorCode, override val message: String = errorCode.message ) : RuntimeException(message) diff --git a/src/main/kotlin/roomescape/member/exception/MemberException.kt b/src/main/kotlin/roomescape/member/exception/MemberException.kt index 62bea601..0102f394 100644 --- a/src/main/kotlin/roomescape/member/exception/MemberException.kt +++ b/src/main/kotlin/roomescape/member/exception/MemberException.kt @@ -1,8 +1,8 @@ package roomescape.member.exception -import roomescape.common.exception.RoomescapeExceptionV2 +import roomescape.common.exception.RoomescapeException class MemberException( override val errorCode: MemberErrorCode, override val message: String = errorCode.message -) : RoomescapeExceptionV2(errorCode, message) +) : RoomescapeException(errorCode, message) diff --git a/src/main/kotlin/roomescape/payment/exception/PaymentException.kt b/src/main/kotlin/roomescape/payment/exception/PaymentException.kt index 44c59be0..33cac713 100644 --- a/src/main/kotlin/roomescape/payment/exception/PaymentException.kt +++ b/src/main/kotlin/roomescape/payment/exception/PaymentException.kt @@ -1,8 +1,8 @@ package roomescape.payment.exception -import roomescape.common.exception.RoomescapeExceptionV2 +import roomescape.common.exception.RoomescapeException class PaymentException( override val errorCode: PaymentErrorCode, override val message: String = errorCode.message -) : RoomescapeExceptionV2(errorCode, message) +) : RoomescapeException(errorCode, message) diff --git a/src/main/kotlin/roomescape/reservation/exception/ReservationException.kt b/src/main/kotlin/roomescape/reservation/exception/ReservationException.kt index 885d39c5..9f251380 100644 --- a/src/main/kotlin/roomescape/reservation/exception/ReservationException.kt +++ b/src/main/kotlin/roomescape/reservation/exception/ReservationException.kt @@ -1,9 +1,9 @@ package roomescape.reservation.exception import roomescape.common.exception.ErrorCode -import roomescape.common.exception.RoomescapeExceptionV2 +import roomescape.common.exception.RoomescapeException class ReservationException( override val errorCode: ErrorCode, override val message: String = errorCode.message -) : RoomescapeExceptionV2(errorCode, message) +) : RoomescapeException(errorCode, message) diff --git a/src/main/kotlin/roomescape/theme/exception/ThemeException.kt b/src/main/kotlin/roomescape/theme/exception/ThemeException.kt index 9129f93e..d678e4ea 100644 --- a/src/main/kotlin/roomescape/theme/exception/ThemeException.kt +++ b/src/main/kotlin/roomescape/theme/exception/ThemeException.kt @@ -1,8 +1,8 @@ package roomescape.theme.exception -import roomescape.common.exception.RoomescapeExceptionV2 +import roomescape.common.exception.RoomescapeException class ThemeException( override val errorCode: ThemeErrorCode, override val message: String = errorCode.message -) : RoomescapeExceptionV2(errorCode, message) +) : RoomescapeException(errorCode, message) diff --git a/src/main/kotlin/roomescape/time/exception/TimeException.kt b/src/main/kotlin/roomescape/time/exception/TimeException.kt index a073fdc6..ac97905e 100644 --- a/src/main/kotlin/roomescape/time/exception/TimeException.kt +++ b/src/main/kotlin/roomescape/time/exception/TimeException.kt @@ -1,9 +1,9 @@ package roomescape.time.exception import roomescape.common.exception.ErrorCode -import roomescape.common.exception.RoomescapeExceptionV2 +import roomescape.common.exception.RoomescapeException class TimeException( override val errorCode: ErrorCode, override val message: String = errorCode.message -) : RoomescapeExceptionV2(errorCode, message) +) : RoomescapeException(errorCode, message) -- 2.47.2 From 5a424d2f036bd6532af88fb093723f0317e62c41 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Jul 2025 10:02:47 +0900 Subject: [PATCH 34/37] style: optimize imports & reformat --- .../kotlin/roomescape/reservation/web/ReservationController.kt | 1 - src/main/kotlin/roomescape/theme/docs/ThemeAPI.kt | 2 +- .../kotlin/roomescape/payment/business/PaymentServiceTest.kt | 3 --- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/kotlin/roomescape/reservation/web/ReservationController.kt b/src/main/kotlin/roomescape/reservation/web/ReservationController.kt index aa41864f..858f210f 100644 --- a/src/main/kotlin/roomescape/reservation/web/ReservationController.kt +++ b/src/main/kotlin/roomescape/reservation/web/ReservationController.kt @@ -6,7 +6,6 @@ import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import roomescape.auth.web.support.MemberId import roomescape.common.dto.response.CommonApiResponse -import roomescape.common.exception.RoomescapeException import roomescape.payment.infrastructure.client.PaymentApproveRequest import roomescape.payment.infrastructure.client.PaymentApproveResponse import roomescape.payment.infrastructure.client.TossPaymentClient diff --git a/src/main/kotlin/roomescape/theme/docs/ThemeAPI.kt b/src/main/kotlin/roomescape/theme/docs/ThemeAPI.kt index 3012aaa7..6195e3fa 100644 --- a/src/main/kotlin/roomescape/theme/docs/ThemeAPI.kt +++ b/src/main/kotlin/roomescape/theme/docs/ThemeAPI.kt @@ -14,8 +14,8 @@ import roomescape.auth.web.support.Admin import roomescape.auth.web.support.LoginRequired import roomescape.common.dto.response.CommonApiResponse import roomescape.theme.web.ThemeCreateRequest -import roomescape.theme.web.ThemeRetrieveResponse import roomescape.theme.web.ThemeRetrieveListResponse +import roomescape.theme.web.ThemeRetrieveResponse @Tag(name = "5. 테마 API", description = "테마를 조회 / 추가 / 삭제할 때 사용합니다.") interface ThemeAPI { diff --git a/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt b/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt index d8dd1b7d..2136cdda 100644 --- a/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt +++ b/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt @@ -8,9 +8,6 @@ import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.runs -import org.springframework.http.HttpStatus -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException import roomescape.payment.exception.PaymentErrorCode import roomescape.payment.exception.PaymentException import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository -- 2.47.2 From ac0e0e361eb585f4dd27ca256218d533e289fae4 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Jul 2025 11:28:44 +0900 Subject: [PATCH 35/37] =?UTF-8?q?chore:=20build.gradle=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EC=9D=BC=EB=B6=80=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EB=B2=84=EC=A0=84=20=EC=B5=9C=EC=8B=A0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 67 +++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 7598679a..5b4be923 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,11 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + plugins { val springBootVersion = "3.5.3" val kotlinVersion = "2.2.0" - java id("org.springframework.boot") version springBootVersion id("io.spring.dependency-management") version "1.1.7" - - //kotlin plugins kotlin("jvm") version kotlinVersion kotlin("plugin.spring") version kotlinVersion kotlin("plugin.jpa") version kotlinVersion @@ -22,61 +21,59 @@ java { } } +kapt { + keepJavacAnnotationProcessors = true +} + repositories { mavenCentral() } dependencies { - // spring + // Spring implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-validation") - implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0") + + // API docs + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9") + + // DB runtimeOnly("com.h2database:h2") - // jwt - implementation("io.jsonwebtoken:jjwt:0.9.1") - implementation("javax.xml.bind:jaxb-api:2.3.1") + // Jwt + implementation("io.jsonwebtoken:jjwt:0.12.6") - // kotlin + // Kotlin implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("io.github.oshai:kotlin-logging-jvm:7.0.3") - // test + // Test + testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("io.mockk:mockk:1.14.4") + testImplementation("com.ninja-squad:springmockk:4.0.2") + + // Kotest testImplementation("io.kotest:kotest-runner-junit5:5.9.1") testImplementation("io.kotest.extensions:kotest-extensions-spring:1.3.0") - testImplementation("com.ninja-squad:springmockk:4.0.2") - testImplementation("org.springframework.boot:spring-boot-starter-test") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") - testImplementation("io.rest-assured:rest-assured:5.3.1") + + // RestAssured + testImplementation("io.rest-assured:rest-assured:5.5.5") testImplementation("io.rest-assured:kotlin-extensions:5.5.5") } -kapt { - keepJavacAnnotationProcessors = true -} - -tasks.withType().configureEach { +tasks.withType { useJUnitPlatform() } -tasks { - compileKotlin { - compilerOptions { - freeCompilerArgs.add("-Xjsr305=strict") - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) - freeCompilerArgs.set(listOf("-Xannotation-default-target=param-property")) - - } +tasks.withType { + compilerOptions { + freeCompilerArgs.addAll( + "-Xjsr305=strict", + "-Xannotation-default-target=param-property" + ) + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) } - - compileTestKotlin { - compilerOptions { - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) - freeCompilerArgs.set(listOf("-Xannotation-default-target=param-property")) - } - } -} \ No newline at end of file +} -- 2.47.2 From 27a20bbe16f1893398085e92e86e701bbff89806 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Jul 2025 11:29:08 +0900 Subject: [PATCH 36/37] =?UTF-8?q?refactor:=20Jwt=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=EC=97=85=EC=9C=BC=EB=A1=9C=20Deprecated=EB=90=9C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/infrastructure/jwt/JwtHandler.kt | 20 +++++++++++-------- .../auth/infrastructure/jwt/JwtHandlerTest.kt | 10 +++++----- src/test/kotlin/roomescape/util/Fixtures.kt | 4 ++-- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt b/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt index 29e7b38b..a3b89cf3 100644 --- a/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt +++ b/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtHandler.kt @@ -2,39 +2,43 @@ package roomescape.auth.infrastructure.jwt import io.jsonwebtoken.ExpiredJwtException import io.jsonwebtoken.Jwts -import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.security.Keys import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import roomescape.auth.exception.AuthErrorCode import roomescape.auth.exception.AuthException import java.util.* +import javax.crypto.SecretKey @Component class JwtHandler( @Value("\${security.jwt.token.secret-key}") - private val secretKey: String, + private val secretKeyString: String, @Value("\${security.jwt.token.ttl-seconds}") private val tokenTtlSeconds: Long ) { + private val secretKey: SecretKey = Keys.hmacShaKeyFor(secretKeyString.toByteArray()) + fun createToken(memberId: Long): String { val date = Date() val accessTokenExpiredAt = Date(date.time + tokenTtlSeconds) return Jwts.builder() .claim(MEMBER_ID_CLAIM_KEY, memberId) - .setIssuedAt(date) - .setExpiration(accessTokenExpiredAt) - .signWith(SignatureAlgorithm.HS256, secretKey.toByteArray()) + .issuedAt(date) + .expiration(accessTokenExpiredAt) + .signWith(secretKey) .compact() } fun getMemberIdFromToken(token: String?): Long { try { return Jwts.parser() - .setSigningKey(secretKey.toByteArray()) - .parseClaimsJws(token) - .body + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .payload .get(MEMBER_ID_CLAIM_KEY, Number::class.java) .toLong() } catch (_: IllegalArgumentException) { diff --git a/src/test/kotlin/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt b/src/test/kotlin/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt index a7ee570d..66af9c03 100644 --- a/src/test/kotlin/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt +++ b/src/test/kotlin/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt @@ -1,7 +1,7 @@ package roomescape.auth.infrastructure.jwt import io.jsonwebtoken.Jwts -import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.security.Keys import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe @@ -45,12 +45,12 @@ class JwtHandlerTest : FunSpec({ } test("시크릿 키가 잘못된 경우 예외를 던진다.") { - val now: Date = Date() + val now = Date() val invalidSignatureToken: String = Jwts.builder() .claim("memberId", memberId) - .setIssuedAt(now) - .setExpiration(Date(now.time + JwtFixture.EXPIRATION_TIME)) - .signWith(SignatureAlgorithm.HS256, JwtFixture.SECRET_KEY.substring(1).toByteArray()) + .issuedAt(now) + .expiration(Date(now.time + JwtFixture.EXPIRATION_TIME)) + .signWith(Keys.hmacShaKeyFor(JwtFixture.SECRET_KEY_STRING.substring(1).toByteArray())) .compact() shouldThrow { diff --git a/src/test/kotlin/roomescape/util/Fixtures.kt b/src/test/kotlin/roomescape/util/Fixtures.kt index 5413b676..8e372f41 100644 --- a/src/test/kotlin/roomescape/util/Fixtures.kt +++ b/src/test/kotlin/roomescape/util/Fixtures.kt @@ -110,11 +110,11 @@ object ReservationFixture { } object JwtFixture { - const val SECRET_KEY: String = "daijawligagaf@LIJ$@U)9nagnalkkgalijaddljfi" + const val SECRET_KEY_STRING: String = "daijawligagaf@LIJ$@U)9nagnalkkgalijaddljfi" const val EXPIRATION_TIME: Long = 1000 * 60 * 60 fun create( - secretKey: String = SECRET_KEY, + secretKey: String = SECRET_KEY_STRING, expirationTime: Long = EXPIRATION_TIME ): JwtHandler = JwtHandler(secretKey, expirationTime) } -- 2.47.2 From 8a791dc9c3eb6e6cd5c84770211fd551fb7893c2 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Jul 2025 11:29:41 +0900 Subject: [PATCH 37/37] =?UTF-8?q?test:=20reservation=EA=B3=BC=20time?= =?UTF-8?q?=EC=97=90=EC=84=9C=EC=9D=98=20Service=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/ReservationServiceTest.kt | 109 ++++++++++++++++++ .../time/business/TimeServiceTest.kt | 16 +++ 2 files changed, 125 insertions(+) diff --git a/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt b/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt index 32efd17d..cef5672c 100644 --- a/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt +++ b/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt @@ -5,11 +5,13 @@ import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockk +import org.springframework.data.repository.findByIdOrNull import roomescape.member.business.MemberService import roomescape.member.infrastructure.persistence.Role import roomescape.reservation.exception.ReservationErrorCode import roomescape.reservation.exception.ReservationException import roomescape.reservation.infrastructure.persistence.ReservationRepository +import roomescape.reservation.infrastructure.persistence.ReservationStatus import roomescape.theme.business.ThemeService import roomescape.time.business.TimeService import roomescape.util.MemberFixture @@ -121,6 +123,56 @@ class ReservationServiceTest : FunSpec({ } } + context("예약 대기를 취소할 때") { + val reservationId = 1L + val member = MemberFixture.create(id = 1L, role = Role.MEMBER) + test("예약을 찾을 수 없으면 예외를 던진다.") { + every { + reservationRepository.findByIdOrNull(reservationId) + } returns null + + shouldThrow { + reservationService.deleteWaiting(reservationId, member.id!!) + }.also { + it.errorCode shouldBe ReservationErrorCode.RESERVATION_NOT_FOUND + } + } + + test("대기중인 해당 예약이 이미 확정된 상태라면 예외를 던진다.") { + val alreadyConfirmed = ReservationFixture.create( + id = reservationId, + status = ReservationStatus.CONFIRMED + ) + every { + reservationRepository.findByIdOrNull(reservationId) + } returns alreadyConfirmed + + shouldThrow { + reservationService.deleteWaiting(reservationId, member.id!!) + }.also { + it.errorCode shouldBe ReservationErrorCode.ALREADY_CONFIRMED + } + } + + test("타인의 대기를 취소하려고 하면 예외를 던진다.") { + val otherMembersWaiting = ReservationFixture.create( + id = reservationId, + member = MemberFixture.create(id = member.id!! + 1L), + status = ReservationStatus.WAITING + ) + + every { + reservationRepository.findByIdOrNull(reservationId) + } returns otherMembersWaiting + + shouldThrow { + reservationService.deleteWaiting(reservationId, member.id!!) + }.also { + it.errorCode shouldBe ReservationErrorCode.NOT_RESERVATION_OWNER + } + } + } + context("예약을 조회할 때") { test("종료 날짜가 시작 날짜보다 이전이면 예외를 던진다.") { val startFrom = LocalDate.now() @@ -173,4 +225,61 @@ class ReservationServiceTest : FunSpec({ } } } + + context("대기중인 예약을 거절할 때") { + test("관리자가 아니면 예외를 던진다.") { + val member = MemberFixture.create(id = 1L, role = Role.MEMBER) + + every { + memberService.findById(any()) + } returns member + + shouldThrow { + reservationService.rejectWaiting(1L, member.id!!) + }.also { + it.errorCode shouldBe ReservationErrorCode.NO_PERMISSION + } + } + + test("예약을 찾을 수 없으면 예외를 던진다.") { + val member = MemberFixture.create(id = 1L, role = Role.ADMIN) + val reservationId = 1L + + every { + memberService.findById(member.id!!) + } returns member + + every { + reservationRepository.findByIdOrNull(reservationId) + } returns null + + shouldThrow { + reservationService.rejectWaiting(reservationId, member.id!!) + }.also { + it.errorCode shouldBe ReservationErrorCode.RESERVATION_NOT_FOUND + } + } + + test("이미 확정된 예약이면 예외를 던진다.") { + val member = MemberFixture.create(id = 1L, role = Role.ADMIN) + val reservation = ReservationFixture.create( + id = 1L, + status = ReservationStatus.CONFIRMED + ) + + every { + memberService.findById(member.id!!) + } returns member + + every { + reservationRepository.findByIdOrNull(reservation.id!!) + } returns reservation + + shouldThrow { + reservationService.rejectWaiting(reservation.id!!, member.id!!) + }.also { + it.errorCode shouldBe ReservationErrorCode.ALREADY_CONFIRMED + } + } + } }) diff --git a/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt b/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt index ba12249e..a3d9801a 100644 --- a/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt +++ b/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt @@ -1,9 +1,12 @@ package roomescape.time.business +import io.kotest.assertions.throwables.shouldNotThrow import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe +import io.mockk.Runs import io.mockk.every +import io.mockk.just import io.mockk.mockk import org.springframework.data.repository.findByIdOrNull import roomescape.reservation.infrastructure.persistence.ReservationRepository @@ -63,6 +66,19 @@ class TimeServiceTest : FunSpec({ } context("removeTimeById") { + test("정상 제거 및 응답") { + val id = 1L + val time = TimeFixture.create(id = id) + + every { timeRepository.findByIdOrNull(id) } returns time + every { reservationRepository.findAllByTime(time) } returns emptyList() + every { timeRepository.delete(time) } just Runs + + shouldNotThrow { + timeService.deleteTime(id) + } + } + test("시간을 찾을 수 없으면 예외 응답") { val id = 1L -- 2.47.2