From 90271b7bc4c14d5e7bba10b79970d5079d990c53 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:40:26 +0900 Subject: [PATCH 01/49] =?UTF-8?q?[BOOK-140]=20chore:=20apis=20-=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/yapp/apis/auth/{dto => strategy}/AuthCredentials.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename apis/src/main/kotlin/org/yapp/apis/auth/{dto => strategy}/AuthCredentials.kt (92%) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/AuthCredentials.kt b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/AuthCredentials.kt similarity index 92% rename from apis/src/main/kotlin/org/yapp/apis/auth/dto/AuthCredentials.kt rename to apis/src/main/kotlin/org/yapp/apis/auth/strategy/AuthCredentials.kt index 2a98ba6e..fd89cb96 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/AuthCredentials.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/AuthCredentials.kt @@ -1,4 +1,4 @@ -package org.yapp.apis.auth.dto +package org.yapp.apis.auth.strategy import org.yapp.domain.user.ProviderType From e9cb5b476316b40f3373d1bf0fa297ff39560feb Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:41:33 +0900 Subject: [PATCH 02/49] =?UTF-8?q?[BOOK-140]=20refactor:=20apis=20-=20vo?= =?UTF-8?q?=EA=B0=80=20=EC=95=84=EB=8B=8C=20dto=EB=A5=BC=20=EB=B0=98?= =?UTF-8?q?=ED=99=94=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/yapp/apis/auth/dto/UserCreateInfo.kt | 35 ------------------- .../apis/auth/strategy/AppleAuthStrategy.kt | 10 +++--- .../yapp/apis/auth/strategy/AuthStrategy.kt | 5 ++- .../apis/auth/strategy/KakaoAuthStrategy.kt | 10 +++--- 4 files changed, 10 insertions(+), 50 deletions(-) delete mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/dto/UserCreateInfo.kt diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/UserCreateInfo.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/UserCreateInfo.kt deleted file mode 100644 index 0bc3c70a..00000000 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/UserCreateInfo.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.yapp.apis.auth.dto - -import org.yapp.domain.user.ProviderType - -data class UserCreateInfo private constructor( - val email: String?, - val nickname: String?, - val profileImageUrl: String? = null, - val providerType: ProviderType, - val providerId: String -) { - companion object { - fun of( - email: String?, - nickname: String?, - profileImageUrl: String? = null, - providerType: ProviderType?, - providerId: String? - ): UserCreateInfo { - val validEmail = email?.takeIf { it.isNotBlank() }?.trim() - val validNickname = nickname?.takeIf { it.isNotBlank() }?.trim() - - requireNotNull(providerType) { "ProviderType은 필수입니다." } - require(!providerId.isNullOrBlank()) { "providerId는 필수입니다." } - - return UserCreateInfo( - email = validEmail, - nickname = validNickname, - profileImageUrl = profileImageUrl, - providerType = providerType, - providerId = providerId.trim() - ) - } - } -} diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/AppleAuthStrategy.kt b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/AppleAuthStrategy.kt index 3ee08acb..894452ee 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/AppleAuthStrategy.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/AppleAuthStrategy.kt @@ -2,9 +2,7 @@ package org.yapp.apis.auth.strategy import mu.KotlinLogging import org.springframework.stereotype.Component -import org.yapp.apis.auth.dto.AppleAuthCredentials -import org.yapp.apis.auth.dto.AuthCredentials -import org.yapp.apis.auth.dto.UserCreateInfo +import org.yapp.apis.auth.dto.response.UserCreateInfoResponse import org.yapp.apis.auth.exception.AuthErrorCode import org.yapp.apis.auth.exception.AuthException import org.yapp.apis.auth.helper.AppleJwtHelper @@ -23,7 +21,7 @@ class AppleAuthStrategy( override fun getProviderType(): ProviderType = ProviderType.APPLE - override fun authenticate(credentials: AuthCredentials): UserCreateInfo { + override fun authenticate(credentials: AuthCredentials): UserCreateInfoResponse { return try { val appleCredentials = validateCredentials(credentials) val payload = appleJwtHelper.parseIdToken(appleCredentials.idToken) @@ -45,8 +43,8 @@ class AppleAuthStrategy( ) } - private fun createUserInfo(payload: AppleJwtHelper.AppleIdTokenPayload): UserCreateInfo { - return UserCreateInfo.of( + private fun createUserInfo(payload: AppleJwtHelper.AppleIdTokenPayload): UserCreateInfoResponse { + return UserCreateInfoResponse.of( email = payload.email, nickname = NicknameGenerator.generate(), profileImageUrl = null, // Apple doesn't provide profile image diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/AuthStrategy.kt b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/AuthStrategy.kt index 8b0313b1..6bf7e028 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/AuthStrategy.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/AuthStrategy.kt @@ -1,7 +1,6 @@ package org.yapp.apis.auth.strategy -import org.yapp.apis.auth.dto.AuthCredentials -import org.yapp.apis.auth.dto.UserCreateInfo +import org.yapp.apis.auth.dto.response.UserCreateInfoResponse import org.yapp.domain.user.ProviderType /** @@ -12,5 +11,5 @@ interface AuthStrategy { fun getProviderType(): ProviderType - fun authenticate(credentials: AuthCredentials): UserCreateInfo + fun authenticate(credentials: AuthCredentials): UserCreateInfoResponse } diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/KakaoAuthStrategy.kt b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/KakaoAuthStrategy.kt index 1899fcce..f6f98b94 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/KakaoAuthStrategy.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/KakaoAuthStrategy.kt @@ -2,9 +2,7 @@ package org.yapp.apis.auth.strategy import mu.KotlinLogging import org.springframework.stereotype.Component -import org.yapp.apis.auth.dto.AuthCredentials -import org.yapp.apis.auth.dto.KakaoAuthCredentials -import org.yapp.apis.auth.dto.UserCreateInfo +import org.yapp.apis.auth.dto.response.UserCreateInfoResponse import org.yapp.apis.auth.exception.AuthErrorCode import org.yapp.apis.auth.exception.AuthException import org.yapp.apis.auth.helper.KakaoApiHelper @@ -24,7 +22,7 @@ class KakaoAuthStrategy( override fun getProviderType(): ProviderType = ProviderType.KAKAO - override fun authenticate(credentials: AuthCredentials): UserCreateInfo { + override fun authenticate(credentials: AuthCredentials): UserCreateInfoResponse { return try { val kakaoCredentials = validateCredentials(credentials) val kakaoUser = kakaoApiHelper.getUserInfo(kakaoCredentials.accessToken) @@ -46,8 +44,8 @@ class KakaoAuthStrategy( ) } - private fun createUserInfo(kakaoUser: KakaoUserInfo): UserCreateInfo { - return UserCreateInfo.of( + private fun createUserInfo(kakaoUser: KakaoUserInfo): UserCreateInfoResponse { + return UserCreateInfoResponse.of( email = kakaoUser.email ?: ("kakao_${kakaoUser.id}@kakao.com"), nickname = NicknameGenerator.generate(), profileImageUrl = kakaoUser.profileImageUrl, From 29e62705b24a94e46e35a0280b90801eb2ccd626 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:41:54 +0900 Subject: [PATCH 03/49] =?UTF-8?q?[BOOK-140]=20chore:=20apis=20-=20?= =?UTF-8?q?=EA=B0=9C=ED=96=89=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt b/apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt index e0e2d713..7c6263c2 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt @@ -39,15 +39,11 @@ class BookUseCase( userAuthService.validateUserExists(userId) val detail = bookQueryService.getBookDetail(BookDetailRequest.of(request.validBookIsbn())) - val book = bookManagementService.findOrCreateBook(BookCreateRequest.create(detail)) - val userBook = userBookService.upsertUserBook(userId, book, request.bookStatus) - return UserBookResponse.from(userBook) } - fun getUserLibraryBooks(userId: UUID): List { userAuthService.validateUserExists(userId) From 81d9a01da18520a7f5d225ce7213d5c2e51884ff Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:42:21 +0900 Subject: [PATCH 04/49] =?UTF-8?q?[BOOK-140]=20chore:=20apis=20-=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=EC=97=86=EB=8A=94=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt b/apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt index d10cb837..5ac0cb0a 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt @@ -12,16 +12,14 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping import org.yapp.apis.auth.dto.request.SocialLoginRequest import org.yapp.apis.auth.dto.request.TokenRefreshRequest import org.yapp.apis.auth.dto.response.AuthResponse import org.yapp.apis.auth.dto.response.UserProfileResponse import org.yapp.globalutils.exception.ErrorResponse -import java.util.UUID +import java.util.* @Tag(name = "Authentication", description = "Authentication API") -@RequestMapping("/api/v1/auth") interface AuthControllerApi { @Operation( From 37c54f6898d380a7ff357ce28081c846f7d96722 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:48:47 +0900 Subject: [PATCH 05/49] =?UTF-8?q?[BOOK-140]=20feat:=20apis=20-=20=EC=95=A0?= =?UTF-8?q?=ED=94=8C=EB=A6=AC=EC=BC=80=EC=9D=B4=EC=85=98=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=EC=97=90=20=EC=82=AC=EC=9A=A9=EB=90=A0=20DTO?= =?UTF-8?q?=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/FindOrCreateUserRequest.kt | 68 +++++++++++++++++++ .../dto/request/GenerateTokenPairRequest.kt | 44 ++++++++++++ .../auth/dto/response/CreateUserResponse.kt | 27 ++++++++ .../auth/dto/response/UserAuthInfoResponse.kt | 27 ++++++++ .../dto/response/UserCreateInfoResponse.kt | 59 ++++++++++++++++ 5 files changed, 225 insertions(+) create mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindOrCreateUserRequest.kt create mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt create mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt create mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt create mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserCreateInfoResponse.kt diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindOrCreateUserRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindOrCreateUserRequest.kt new file mode 100644 index 00000000..ba03d49e --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindOrCreateUserRequest.kt @@ -0,0 +1,68 @@ +package org.yapp.apis.auth.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import org.yapp.apis.auth.dto.response.UserCreateInfoResponse +import org.yapp.apis.util.NicknameGenerator +import org.yapp.domain.user.ProviderType + +@Schema( + name = "FindOrCreateUserRequest", + description = "Request DTO for finding an existing user or creating a new one during social login" +) +data class FindOrCreateUserRequest private constructor( + @Schema( + description = "사용자 이메일", + example = "user@example.com", nullable = true) + val email: String? = null, + + @Schema( + description = "사용자 닉네임", + example = "코딩하는곰", + nullable = true + ) + val nickname: String? = null, + + @Schema( + description = "사용자 프로필 이미지 URL", + example = "https://example.com/image.jpg", + nullable = true + ) val profileImageUrl: String? = null, + + @field:NotBlank(message = "providerType은 필수입니다.") + @Schema( + description = "소셜 로그인 제공자", + example = "KAKAO" + ) val providerType: ProviderType? = null, + + @field:NotBlank(message = "providerId는 필수입니다.") + @Schema( + description = "소셜 제공자에서 발급한 식별자", + example = "12345678901234567890" + ) val providerId: String? = null +) { + fun getOrDefaultEmail(): String { + return email?.takeIf { it.isNotBlank() } ?: "${validProviderId()}@${validProviderType().name.lowercase()}.local" + } + + fun getOrDefaultNickname(): String { + return nickname?.takeIf { it.isNotBlank() } ?: NicknameGenerator.generate() + } + + fun validProviderType(): ProviderType = providerType!! + fun validProviderId(): String = providerId!! + + companion object { + fun from( + userCreateInfoResponse: UserCreateInfoResponse + ): FindOrCreateUserRequest { + return FindOrCreateUserRequest( + email = userCreateInfoResponse.email, + nickname = userCreateInfoResponse.nickname, + profileImageUrl = userCreateInfoResponse.profileImageUrl, + providerType = userCreateInfoResponse.providerType, + providerId = userCreateInfoResponse.providerId + ) + } + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt new file mode 100644 index 00000000..d991051e --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt @@ -0,0 +1,44 @@ +package org.yapp.apis.auth.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotNull +import org.yapp.apis.auth.dto.response.CreateUserResponse +import org.yapp.apis.auth.dto.response.UserAuthInfoResponse +import org.yapp.globalutils.auth.Role +import java.util.* + +@Schema( + name = "GenerateTokenPairRequest", + description = "Request DTO to generate a new pair of access and refresh tokens" +) +data class GenerateTokenPairRequest private constructor( + @field:NotNull(message = "userId must not be null") + @Schema( + description = "User ID", + example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0" + ) + val userId: UUID? = null, + + @field:NotNull(message = "role must not be null") + @Schema( + description = "User role", + example = "USER" + ) + val role: Role? = null +) { + companion object { + fun from(response: CreateUserResponse): GenerateTokenPairRequest { + return GenerateTokenPairRequest( + userId = response.id, + role = response.role + ) + } + + fun from(response: UserAuthInfoResponse): GenerateTokenPairRequest { + return GenerateTokenPairRequest( + userId = response.id, + role = response.role + ) + } + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt new file mode 100644 index 00000000..aa9af5fb --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt @@ -0,0 +1,27 @@ +package org.yapp.apis.auth.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import org.yapp.domain.user.vo.UserIdentity +import org.yapp.globalutils.auth.Role +import java.util.* + +@Schema( + name = "CreateUserResponse", + description = "Response DTO returned after successful user registration" +) +data class CreateUserResponse private constructor( + @Schema(description = "사용자 ID", example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0") + val id: UUID, + + @Schema(description = "사용자 역할", example = "USER") + val role: Role +) { + companion object { + fun from(identity: UserIdentity): CreateUserResponse { + return CreateUserResponse( + id = identity.id, + role = identity.role + ) + } + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt new file mode 100644 index 00000000..17dcf30d --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt @@ -0,0 +1,27 @@ +package org.yapp.apis.auth.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import org.yapp.domain.user.vo.UserIdentity +import org.yapp.globalutils.auth.Role +import java.util.* + +@Schema( + name = "UserAuthInfoResponse", + description = "Response DTO containing minimal authentication information (ID and role)" +) +data class UserAuthInfoResponse private constructor( + @Schema(description = "Unique identifier of the user", example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0") + val id: UUID, + + @Schema(description = "Role assigned to the user", example = "USER") + val role: Role +) { + companion object { + fun from(identity: UserIdentity): UserAuthInfoResponse { + return UserAuthInfoResponse( + id = identity.id, + role = identity.role + ) + } + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserCreateInfoResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserCreateInfoResponse.kt new file mode 100644 index 00000000..8a8ebc9b --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserCreateInfoResponse.kt @@ -0,0 +1,59 @@ +package org.yapp.apis.auth.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import org.yapp.domain.user.ProviderType + +@Schema( + name = "UserCreateInfoResponse", + description = "Response DTO containing user information for newly registered users via social login" +) +data class UserCreateInfoResponse private constructor( + @Schema( + description = "사용자 이메일", + example = "user@example.com", + nullable = true + ) + val email: String?, + + @Schema( + description = "사용자 닉네임", + example = "코딩하는곰", nullable = true + ) + val nickname: String?, + + @Schema( + description = "사용자 프로필 이미지 URL", + example = "https://example.com/image.jpg", nullable = true + ) + val profileImageUrl: String? = null, + + @Schema( + description = "소셜 로그인 제공자", + example = "KAKAO") + + val providerType: ProviderType, + + @Schema( + description = "소셜 제공자에서 발급한 식별자", + example = "12345678901234567890" + ) + val providerId: String +) { + companion object { + fun of( + email: String?, + nickname: String?, + profileImageUrl: String? = null, + providerType: ProviderType, + providerId: String + ): UserCreateInfoResponse { + return UserCreateInfoResponse( + email = email, + nickname = nickname, + profileImageUrl = profileImageUrl, + providerType = providerType, + providerId = providerId.trim() + ) + } + } +} From f01cab425f75589ec52ee6999c7ee6cdc16e3379 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:49:13 +0900 Subject: [PATCH 06/49] =?UTF-8?q?[BOOK-140]=20feat:=20domain=20-=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=A0=84=EC=9A=A9=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yapp/domain/user/exception/UserErrorCode.kt | 16 ++++++++++++++++ .../user/exception/UserNotFoundException.kt | 10 ++++++++++ 2 files changed, 26 insertions(+) create mode 100644 domain/src/main/kotlin/org/yapp/domain/user/exception/UserErrorCode.kt create mode 100644 domain/src/main/kotlin/org/yapp/domain/user/exception/UserNotFoundException.kt diff --git a/domain/src/main/kotlin/org/yapp/domain/user/exception/UserErrorCode.kt b/domain/src/main/kotlin/org/yapp/domain/user/exception/UserErrorCode.kt new file mode 100644 index 00000000..3ee966e6 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/user/exception/UserErrorCode.kt @@ -0,0 +1,16 @@ +package org.yapp.domain.user.exception + +import org.springframework.http.HttpStatus +import org.yapp.globalutils.exception.BaseErrorCode + +enum class UserErrorCode( + private val status: HttpStatus, + private val code: String, + private val message: String +) : BaseErrorCode { + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_001", "유저 정보를 찾을 수 없습니다."); + + override fun getHttpStatus(): HttpStatus = status + override fun getCode(): String = code + override fun getMessage(): String = message +} diff --git a/domain/src/main/kotlin/org/yapp/domain/user/exception/UserNotFoundException.kt b/domain/src/main/kotlin/org/yapp/domain/user/exception/UserNotFoundException.kt new file mode 100644 index 00000000..b9312670 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/user/exception/UserNotFoundException.kt @@ -0,0 +1,10 @@ +package org.yapp.domain.user.exception + +import org.yapp.globalutils.exception.CommonException + +class UserNotFoundException ( + errorCode: UserErrorCode, + message: String? = null +) : CommonException(errorCode, message) { +} + From baf7c6cc3206279909453b95ea6729960c945de9 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:50:04 +0900 Subject: [PATCH 07/49] =?UTF-8?q?[BOOK-140]=20refactor:=20domain=20-=20?= =?UTF-8?q?=EC=97=AD=ED=95=A0=20=EB=B6=80=EC=97=AC=20=EC=97=AC=EB=B6=80?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EC=A0=95=EC=A0=81=20=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/org/yapp/domain/user/User.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/domain/src/main/kotlin/org/yapp/domain/user/User.kt b/domain/src/main/kotlin/org/yapp/domain/user/User.kt index a0be5fbe..21293785 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/User.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/User.kt @@ -41,6 +41,31 @@ data class User private constructor( companion object { fun create( + email: String, + nickname: String, + profileImageUrl: String?, + providerType: ProviderType, + providerId: String, + createdAt: LocalDateTime, + updatedAt: LocalDateTime, + deletedAt: LocalDateTime? = null + ): User { + return User( + id = UUID.randomUUID(), + email = email, + nickname = nickname, + profileImageUrl = profileImageUrl, + providerType = providerType, + providerId = providerId, + role = Role.USER, + createdAt = createdAt, + updatedAt = updatedAt, + deletedAt = deletedAt + ) + } + + // 추후 다른 역할 부여 시 사용 + fun createWithRole( email: String, nickname: String, profileImageUrl: String?, From 7b93e3213d23e8f7480ac936d25ef36303f26d2e Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:50:19 +0900 Subject: [PATCH 08/49] =?UTF-8?q?[BOOK-140]=20chore:=20apis=20-=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/yapp/apis/auth/dto/request/SocialLoginRequest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt index b47727ae..009f3b5b 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt @@ -2,9 +2,9 @@ package org.yapp.apis.auth.dto.request import io.swagger.v3.oas.annotations.media.Schema import jakarta.validation.constraints.NotBlank -import org.yapp.apis.auth.dto.AppleAuthCredentials -import org.yapp.apis.auth.dto.AuthCredentials -import org.yapp.apis.auth.dto.KakaoAuthCredentials +import org.yapp.apis.auth.strategy.AppleAuthCredentials +import org.yapp.apis.auth.strategy.AuthCredentials +import org.yapp.apis.auth.strategy.KakaoAuthCredentials import org.yapp.apis.auth.exception.AuthErrorCode import org.yapp.apis.auth.exception.AuthException import org.yapp.domain.user.ProviderType From be678800502727fcb902e1d1dda5b1874435134a Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:50:29 +0900 Subject: [PATCH 09/49] =?UTF-8?q?[BOOK-140]=20chore:=20apis=20-=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/org/yapp/apis/auth/service/SocialAuthService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/service/SocialAuthService.kt b/apis/src/main/kotlin/org/yapp/apis/auth/service/SocialAuthService.kt index c82e73a7..6e6c1fa6 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/service/SocialAuthService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/service/SocialAuthService.kt @@ -1,7 +1,7 @@ package org.yapp.apis.auth.service import org.springframework.stereotype.Service -import org.yapp.apis.auth.dto.AuthCredentials +import org.yapp.apis.auth.strategy.AuthCredentials import org.yapp.apis.auth.exception.AuthErrorCode import org.yapp.apis.auth.exception.AuthException import org.yapp.apis.auth.strategy.AuthStrategy From 8499053806d6eae81177419a166a72fa55e8f198 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:50:55 +0900 Subject: [PATCH 10/49] =?UTF-8?q?[BOOK-140]=20refactor:=20apis=20-=20?= =?UTF-8?q?=EC=A0=95=EC=A0=81=ED=8C=A9=ED=86=A0=EB=A6=AC=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=9D=B8=EC=9E=90=EB=A1=9C=20vo=EB=A5=BC?= =?UTF-8?q?=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/dto/response/UserProfileResponse.kt | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt index 79c311f9..a17fe060 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt @@ -2,6 +2,7 @@ package org.yapp.apis.auth.dto.response import io.swagger.v3.oas.annotations.media.Schema import org.yapp.domain.user.ProviderType +import org.yapp.domain.user.vo.UserProfile import java.util.* @Schema( @@ -35,17 +36,12 @@ data class UserProfileResponse( val provider: ProviderType ) { companion object { - fun of( - id: UUID, - email: String, - nickname: String, - provider: ProviderType - ): UserProfileResponse { + fun from(userProfile: UserProfile): UserProfileResponse { return UserProfileResponse( - id = id, - email = email, - nickname = nickname, - provider = provider + id = userProfile.id, + email = userProfile.email, + nickname = userProfile.nickname, + provider = userProfile.provider ) } } From fc5114c5c5fa24f14e0f0aa53d8ea7d4efc5dbd4 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:51:58 +0900 Subject: [PATCH 11/49] =?UTF-8?q?[BOOK-140]=20refactor:=20apis=20-=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=EC=97=90=EC=84=9C=20usecas?= =?UTF-8?q?e=EB=A1=9C=20=EA=B0=88=EB=95=8C=20dto=EB=A5=BC=20=EB=B0=9B?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/org/yapp/apis/auth/controller/AuthController.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthController.kt b/apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthController.kt index 348b00c7..0765b737 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthController.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthController.kt @@ -22,14 +22,13 @@ class AuthController( @PostMapping("/signin") override fun signIn(@RequestBody @Valid request: SocialLoginRequest): ResponseEntity { - val credentials = SocialLoginRequest.toCredentials(request) - val tokenPair = authUseCase.signIn(credentials) + val tokenPair = authUseCase.signIn(request) return ResponseEntity.ok(AuthResponse.fromTokenPair(tokenPair)) } @PostMapping("/refresh") override fun refreshToken(@RequestBody @Valid request: TokenRefreshRequest): ResponseEntity { - val tokenPair = authUseCase.reissueTokenPair(request.validRefreshToken()) + val tokenPair = authUseCase.reissueTokenPair(request) return ResponseEntity.ok(AuthResponse.fromTokenPair(tokenPair)) } From d7017d44aecbe45ddf1af58d2fd0292be162014b Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:54:04 +0900 Subject: [PATCH 12/49] =?UTF-8?q?[BOOK-140]=20refactor:=20apis=20-=20valid?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt index d991051e..bc985d32 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt @@ -26,6 +26,10 @@ data class GenerateTokenPairRequest private constructor( ) val role: Role? = null ) { + + fun validUserId(): UUID = userId!! + fun validRole(): Role = role!! + companion object { fun from(response: CreateUserResponse): GenerateTokenPairRequest { return GenerateTokenPairRequest( From 5a4f5ab1bc6399a67b5ed57c2edd48284b65701c Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:55:15 +0900 Subject: [PATCH 13/49] =?UTF-8?q?[BOOK-140]=20refactor:=20domain=20-=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=A1=9C=EC=A7=81=EC=97=90=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=90=98=EB=8A=94=20vo=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/yapp/domain/user/vo/UserIdentity.kt | 25 +++++++++++++++ .../org/yapp/domain/user/vo/UserProfile.kt | 31 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentity.kt create mode 100644 domain/src/main/kotlin/org/yapp/domain/user/vo/UserProfile.kt diff --git a/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentity.kt b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentity.kt new file mode 100644 index 00000000..e3b2ac9e --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentity.kt @@ -0,0 +1,25 @@ +package org.yapp.domain.user.vo + +import org.yapp.domain.user.User +import org.yapp.globalutils.auth.Role +import java.util.UUID + +data class UserIdentity( + val id: UUID, + val role: Role +) { + init { + requireNotNull(id) { "User ID must not be null." } + requireNotNull(role) { "User role must not be null." } + } + + companion object { + fun newInstance(user: User?): UserIdentity { + requireNotNull(user) { "User must not be null." } + return UserIdentity( + id = user.id, + role = user.role + ) + } + } +} diff --git a/domain/src/main/kotlin/org/yapp/domain/user/vo/UserProfile.kt b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserProfile.kt new file mode 100644 index 00000000..d0885d2d --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserProfile.kt @@ -0,0 +1,31 @@ +package org.yapp.domain.user.vo + +import org.yapp.domain.user.ProviderType +import org.yapp.domain.user.User +import java.util.UUID + +data class UserProfile private constructor( + val id: UUID, + val email: String, + val nickname: String, + val provider: ProviderType +) { + init { + require(email.isNotBlank()) { "email은 비어 있을 수 없습니다." } + require(nickname.isNotBlank()) {"nickname은 비어 있을 수 없습니다."} + require(provider.name.isNotBlank()) { "providerType은 비어 있을 수 없습니다." } + } + + companion object { + fun newInstance(user: User?): UserProfile { + requireNotNull(user) { "User는 null일 수 없습니다." } + + return UserProfile( + id = user.id, + email = user.email, + nickname = user.nickname, + provider = user.providerType + ) + } + } +} From 8db150fd7bece2625c2e4e819549a2c0baf9dab9 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:56:04 +0900 Subject: [PATCH 14/49] =?UTF-8?q?[BOOK-140]=20refactor:=20domain,=20infra?= =?UTF-8?q?=20-=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EC=97=90=EC=84=9C=20=ED=95=B4=EB=8B=B9=ED=95=98?= =?UTF-8?q?=EB=8A=94=20vo=EB=A5=BC=20=EB=A6=AC=ED=84=B4=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/yapp/domain/user/UserDomainService.kt | 74 ++++++++++++------- .../org/yapp/domain/user/UserRepository.kt | 7 +- .../repository/impl/UserRepositoryImpl.kt | 21 +++--- 3 files changed, 59 insertions(+), 43 deletions(-) diff --git a/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt index 545945ec..f888e0e3 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt @@ -1,54 +1,72 @@ package org.yapp.domain.user -import org.yapp.domain.user.vo.SocialUserProfile +import org.yapp.domain.user.exception.UserErrorCode +import org.yapp.domain.user.exception.UserNotFoundException +import org.yapp.domain.user.vo.UserIdentity +import org.yapp.domain.user.vo.UserProfile import org.yapp.globalutils.annotation.DomainService -import org.yapp.globalutils.auth.Role import org.yapp.globalutils.util.TimeProvider -import java.util.UUID +import java.util.* @DomainService class UserDomainService( private val userRepository: UserRepository, private val timeProvider: TimeProvider ) { + fun findUserProfileById(id: UUID): UserProfile { + val user = userRepository.findById(id) ?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND) + return UserProfile.newInstance(user) + } - fun findById(id: UUID): User? = - userRepository.findById(id) + fun findUserIdentityById(id: UUID): UserIdentity { + val user = userRepository.findById(id) ?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND) + return UserIdentity.newInstance(user) + } - fun findByEmail(email: String): User? = - userRepository.findByEmail(email) + fun findUserByProviderTypeAndProviderId(providerType: ProviderType, providerId: String): UserIdentity? { + return userRepository.findByProviderTypeAndProviderId(providerType, providerId) + ?.let { UserIdentity.newInstance(it) } + } - fun findByProviderTypeAndProviderId(providerType: ProviderType, providerId: String): User? = - userRepository.findByProviderTypeAndProviderId(providerType, providerId) + fun findUserByProviderTypeAndProviderIdIncludingDeleted(providerType: ProviderType, providerId: String): UserIdentity? { + return userRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType, providerId) + ?.let { UserIdentity.newInstance(it) } + } - fun findByProviderTypeAndProviderIdIncludingDeleted(providerType: ProviderType, providerId: String): User? = - userRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType, providerId) + fun existsActiveUserByIdAndDeletedAtIsNull(userId: UUID): Boolean { + return userRepository.existsByIdAndDeletedAtIsNull(userId) + } - fun existsActiveByEmail(email: String): Boolean = - findByEmail(email) != null + fun existsActiveUserByEmailAndDeletedAtIsNull(email: String): Boolean { + return userRepository.existsByEmailAndDeletedAtIsNull(email) + } - fun create(profile: SocialUserProfile): User { + fun create( + email: String, + nickname: String, + profileImageUrl: String?, + providerType: ProviderType, + providerId: String + ): UserIdentity { val now = timeProvider.now() val user = User.create( - email = profile.email, - nickname = profile.nickname, - profileImageUrl = profile.profileImageUrl, - providerType = profile.providerType, - providerId = profile.providerId, - role = Role.USER, + email = email, + nickname = nickname, + profileImageUrl = profileImageUrl, + providerType = providerType, + providerId = providerId, createdAt = now, updatedAt = now ) - return save(user) + val savedUser = userRepository.save(user) + return UserIdentity.newInstance(savedUser) } - fun restoreDeletedUser(deletedUser: User): User = - save(deletedUser.restore()) - - fun save(user: User): User = - userRepository.save(user) + fun restoreDeletedUser(userId: UUID): UserIdentity { + val deletedUser = userRepository.findById(userId) + ?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND) - fun existsById(userId: UUID): Boolean { - return userRepository.existsById(userId) + val restoredUser = userRepository.save(deletedUser.restore()) + return UserIdentity.newInstance(restoredUser) } } diff --git a/domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt b/domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt index a2f761a0..7e280d2a 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt @@ -8,9 +8,7 @@ import java.util.* */ interface UserRepository { - fun findById(id: UUID): User - - fun findByEmail(email: String): User? + fun findById(id: UUID): User? fun findByProviderTypeAndProviderId(providerType: ProviderType, providerId: String): User? @@ -18,6 +16,7 @@ interface UserRepository { fun save(user: User): User - fun existsById(id: UUID): Boolean + fun existsByIdAndDeletedAtIsNull(userId: UUID): Boolean + fun existsByEmailAndDeletedAtIsNull(email: String): Boolean } diff --git a/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt index 8866d24b..5ce3efa7 100644 --- a/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt +++ b/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt @@ -17,28 +17,27 @@ class UserRepositoryImpl( return jpaUserRepository.findByProviderTypeAndProviderId(providerType, providerId)?.toDomain() } - override fun findByEmail(email: String): User? { - return jpaUserRepository.findByEmail(email)?.toDomain() - } - override fun save(user: User): User { val userEntity = UserEntity.fromDomain(user) val savedEntity = jpaUserRepository.save(userEntity) return savedEntity.toDomain() } - override fun findById(id: UUID): User { - return jpaUserRepository.findById(id).orElseThrow { - NoSuchElementException("User not found with id: $id") - }.toDomain() + override fun findById(id: UUID): User? { + return jpaUserRepository.findById(id).orElse(null)?.toDomain() + } + + override fun existsByEmailAndDeletedAtIsNull(email: String): Boolean { + return jpaUserRepository.existsByEmailAndDeletedAtIsNull(email) } - override fun existsById(id: UUID): Boolean { - return jpaUserRepository.existsById(id) + override fun existsByIdAndDeletedAtIsNull(userId: UUID): Boolean { + return jpaUserRepository.existsByIdAndDeletedAtIsNull(userId) } override fun findByProviderTypeAndProviderIdIncludingDeleted( - providerType: ProviderType, providerId: String + providerType: ProviderType, + providerId: String ): User? { return jpaUserRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType, providerId)?.toDomain() } From 92fcf50ccf2891ca727f5dd763171928cf967051 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:56:41 +0900 Subject: [PATCH 15/49] =?UTF-8?q?[BOOK-140]=20chore:=20infra=20-=20?= =?UTF-8?q?=EC=83=88=EB=A1=9C=EC=9A=B4=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/yapp/infra/user/repository/JpaUserRepository.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt b/infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt index 4cc0906b..3bdd1550 100644 --- a/infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt +++ b/infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt @@ -13,7 +13,9 @@ interface JpaUserRepository : JpaRepository { fun findByProviderTypeAndProviderId(providerType: ProviderType, providerId: String): UserEntity? - fun findByEmail(email: String): UserEntity? + fun existsByIdAndDeletedAtIsNull(id: UUID): Boolean + + fun existsByEmailAndDeletedAtIsNull(email: String): Boolean @Query("SELECT u FROM UserEntity u WHERE u.providerType = :providerType AND u.providerId = :providerId") fun findByProviderTypeAndProviderIdIncludingDeleted(providerType: ProviderType, providerId: String): UserEntity? From d5d00468a3af1757a9274ad0bf67c9fedc9a4f86 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:57:33 +0900 Subject: [PATCH 16/49] =?UTF-8?q?[BOOK-140]=20refactor:=20apis=20-=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=ED=81=B4?= =?UTF-8?q?=EB=A6=B0=20=EC=95=84=ED=82=A4=ED=85=8D=EC=B2=98=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EA=B2=8C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yapp/apis/auth/service/UserAuthService.kt | 82 ++++++++++--------- .../org/yapp/apis/auth/usecase/AuthUseCase.kt | 42 ++++++---- 2 files changed, 70 insertions(+), 54 deletions(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt index 6adea306..74e4a08b 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt @@ -1,65 +1,69 @@ package org.yapp.apis.auth.service import org.springframework.stereotype.Service -import org.yapp.apis.auth.dto.UserCreateInfo +import org.yapp.apis.auth.dto.request.FindOrCreateUserRequest +import org.yapp.apis.auth.dto.response.CreateUserResponse +import org.yapp.apis.auth.dto.response.UserAuthInfoResponse +import org.yapp.apis.auth.dto.response.UserProfileResponse import org.yapp.apis.auth.exception.AuthErrorCode import org.yapp.apis.auth.exception.AuthException -import org.yapp.apis.util.NicknameGenerator import org.yapp.domain.user.UserDomainService -import org.yapp.domain.user.User -import org.yapp.domain.user.vo.SocialUserProfile +import org.yapp.domain.user.vo.UserIdentity import java.util.* @Service class UserAuthService( private val userDomainService: UserDomainService ) { - - fun findUserById(userId: UUID): User { - return userDomainService.findById(userId) - ?: throw AuthException(AuthErrorCode.USER_NOT_FOUND, "User not found: $userId") + fun findUserProfileByUserId(userId: UUID): UserProfileResponse { + val userProfile = userDomainService.findUserProfileById(userId) + return UserProfileResponse.from(userProfile) } - fun findOrCreateUser(userInfo: UserCreateInfo): User { - // 1. providerId로 기존 유저 조회 - userDomainService.findByProviderTypeAndProviderId(userInfo.providerType, userInfo.providerId) - ?.let { return it } - - // 2. soft-deleted 유저 조회 및 복구 - userDomainService.findByProviderTypeAndProviderIdIncludingDeleted(userInfo.providerType, userInfo.providerId) - ?.let { return userDomainService.restoreDeletedUser(it) } + fun validateUserExists(userId: UUID) { + if (!userDomainService.existsActiveUserByIdAndDeletedAtIsNull(userId)) { + throw AuthException(AuthErrorCode.USER_NOT_FOUND, "User not found: $userId") + } + } - // 3. 새 유저 생성할 때만 SocialUserProfile 생성 - return createNewUser(userInfo) + fun findUserIdentityByUserId(userId: UUID): UserAuthInfoResponse { + val userIdentity = userDomainService.findUserIdentityById(userId) + return UserAuthInfoResponse.from(userIdentity) } + fun findOrCreateUser(findOrCreateUserRequest: FindOrCreateUserRequest): CreateUserResponse { + userDomainService.findUserByProviderTypeAndProviderId( + findOrCreateUserRequest.validProviderType(), + findOrCreateUserRequest.validProviderId() + )?.let { return CreateUserResponse.from(it) } - private fun createNewUser(userInfo: UserCreateInfo): User { - val finalEmail = userInfo.email?.takeIf { it.isNotBlank() } - ?: "${userInfo.providerId}@${userInfo.providerType.name.lowercase()}.local" + userDomainService.findUserByProviderTypeAndProviderIdIncludingDeleted( + findOrCreateUserRequest.validProviderType(), + findOrCreateUserRequest.validProviderId() + )?.let { deletedUserIdentity -> + return CreateUserResponse.from( + userDomainService.restoreDeletedUser(deletedUserIdentity.id) + ) + } - val finalNickname = userInfo.nickname?.takeIf { it.isNotBlank() } - ?: NicknameGenerator.generate() + val createdUser = createNewUser(findOrCreateUserRequest) + return CreateUserResponse.from(createdUser) + } - // 이메일 중복 체크 (생성 시에만) - if (userDomainService.existsActiveByEmail(finalEmail)) { + private fun createNewUser(findOrCreateUserRequest: FindOrCreateUserRequest): UserIdentity { + val email = findOrCreateUserRequest.getOrDefaultEmail() + val nickname = findOrCreateUserRequest.getOrDefaultNickname() + + if (userDomainService.existsActiveUserByEmailAndDeletedAtIsNull(email)) { throw AuthException(AuthErrorCode.EMAIL_ALREADY_IN_USE, "Email already in use") } - val profile = SocialUserProfile.newInstance( - email = finalEmail, - nickname = finalNickname, - profileImageUrl = userInfo.profileImageUrl, - providerType = userInfo.providerType, - providerId = userInfo.providerId + return userDomainService.create( + email = email, + nickname = nickname, + profileImageUrl = findOrCreateUserRequest.profileImageUrl, + providerType = findOrCreateUserRequest.validProviderType(), + providerId = findOrCreateUserRequest.validProviderId() ) - - return userDomainService.create(profile) - } - - fun validateUserExists(userId: UUID) { - if (!userDomainService.existsById(userId)) { - throw AuthException(AuthErrorCode.USER_NOT_FOUND, "User not found: $userId") - } } } diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt b/apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt index 7e1532fa..1db1dea6 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt @@ -1,7 +1,10 @@ package org.yapp.apis.auth.usecase import org.springframework.transaction.annotation.Transactional -import org.yapp.apis.auth.dto.AuthCredentials +import org.yapp.apis.auth.dto.request.FindOrCreateUserRequest +import org.yapp.apis.auth.dto.request.GenerateTokenPairRequest +import org.yapp.apis.auth.dto.request.SocialLoginRequest +import org.yapp.apis.auth.dto.request.TokenRefreshRequest import org.yapp.apis.auth.dto.response.TokenPairResponse import org.yapp.apis.auth.dto.response.UserProfileResponse import org.yapp.apis.auth.helper.AuthTokenHelper @@ -20,19 +23,34 @@ class AuthUseCase( private val authTokenHelper: AuthTokenHelper ) { @Transactional - fun signIn(credentials: AuthCredentials): TokenPairResponse { + fun signIn(socialLoginRequest: SocialLoginRequest): TokenPairResponse { + val credentials = SocialLoginRequest.toCredentials(socialLoginRequest) val strategy = socialAuthService.resolve(credentials) - val userInfo = strategy.authenticate(credentials) - val user = userAuthService.findOrCreateUser(userInfo) - return authTokenHelper.generateTokenPair(user.id, user.role) + + val userCreateInfoResponse = strategy.authenticate(credentials) + val findOrCreateUserRequest = FindOrCreateUserRequest.from(userCreateInfoResponse) + val createUserResponse = userAuthService.findOrCreateUser(findOrCreateUserRequest) + val generateTokenPairRequest = GenerateTokenPairRequest.from(createUserResponse) + + return authTokenHelper.generateTokenPair( + generateTokenPairRequest.validUserId(), + generateTokenPairRequest.validRole() + ) } @Transactional - fun reissueTokenPair(refreshToken: String): TokenPairResponse { + fun reissueTokenPair(tokenRefreshRequest: TokenRefreshRequest): TokenPairResponse { + val refreshToken = tokenRefreshRequest.validRefreshToken() val userId = authTokenHelper.validateAndGetUserIdFromRefreshToken(refreshToken) authTokenHelper.deleteToken(refreshToken) - val user = userAuthService.findUserById(userId) - return authTokenHelper.generateTokenPair(user.id, user.role) + + val userAuthInfoResponse = userAuthService.findUserIdentityByUserId(userId) + val generateTokenPairRequest = GenerateTokenPairRequest.from(userAuthInfoResponse) + + return authTokenHelper.generateTokenPair( + generateTokenPairRequest.validUserId(), + generateTokenPairRequest.validRole() + ) } @Transactional @@ -42,12 +60,6 @@ class AuthUseCase( } fun getUserProfile(userId: UUID): UserProfileResponse { - val user = userAuthService.findUserById(userId) - return UserProfileResponse.of( - id = user.id, - email = user.email, - nickname = user.nickname, - provider = user.providerType - ) + return userAuthService.findUserProfileByUserId(userId) } } From becef68173022cd010ef82177c034bd73c0262f0 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 03:59:46 +0900 Subject: [PATCH 17/49] =?UTF-8?q?[BOOK-140]=20chore:=20apis=20-=20dto=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EA=B2=80=EC=A6=9D=20NotBlank=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 --- .../yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt index bc985d32..752207b7 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt @@ -1,7 +1,7 @@ package org.yapp.apis.auth.dto.request import io.swagger.v3.oas.annotations.media.Schema -import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.NotBlank import org.yapp.apis.auth.dto.response.CreateUserResponse import org.yapp.apis.auth.dto.response.UserAuthInfoResponse import org.yapp.globalutils.auth.Role @@ -12,14 +12,14 @@ import java.util.* description = "Request DTO to generate a new pair of access and refresh tokens" ) data class GenerateTokenPairRequest private constructor( - @field:NotNull(message = "userId must not be null") + @field:NotBlank(message = "userId must not be null") @Schema( description = "User ID", example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0" ) val userId: UUID? = null, - @field:NotNull(message = "role must not be null") + @field:NotBlank(message = "role must not be null") @Schema( description = "User role", example = "USER" From c7ece94d1f17a223bcdc1eff2a015439c9d7a33e Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 04:12:15 +0900 Subject: [PATCH 18/49] =?UTF-8?q?[BOOK-140]=20chore:=20apis=20-=20?= =?UTF-8?q?=EA=B0=80=EC=8B=9C=EC=84=B1=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EA=B0=9C=ED=96=89=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/dto/request/FindOrCreateUserRequest.kt | 13 ++++++++----- .../auth/dto/request/GenerateTokenPairRequest.kt | 4 ++-- .../apis/auth/dto/request/SocialLoginRequest.kt | 12 ++++++++++-- .../apis/auth/dto/request/TokenRefreshRequest.kt | 3 +-- .../org/yapp/apis/auth/dto/response/AuthResponse.kt | 3 +-- .../apis/auth/dto/response/CreateUserResponse.kt | 10 ++++++++-- .../apis/auth/dto/response/UserAuthInfoResponse.kt | 10 ++++++++-- 7 files changed, 38 insertions(+), 17 deletions(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindOrCreateUserRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindOrCreateUserRequest.kt index ba03d49e..f523deef 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindOrCreateUserRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindOrCreateUserRequest.kt @@ -27,19 +27,22 @@ data class FindOrCreateUserRequest private constructor( description = "사용자 프로필 이미지 URL", example = "https://example.com/image.jpg", nullable = true - ) val profileImageUrl: String? = null, + ) + val profileImageUrl: String? = null, - @field:NotBlank(message = "providerType은 필수입니다.") @Schema( description = "소셜 로그인 제공자", example = "KAKAO" - ) val providerType: ProviderType? = null, + ) + @field:NotBlank(message = "providerType은 필수입니다.") + val providerType: ProviderType? = null, - @field:NotBlank(message = "providerId는 필수입니다.") @Schema( description = "소셜 제공자에서 발급한 식별자", example = "12345678901234567890" - ) val providerId: String? = null + ) + @field:NotBlank(message = "providerId는 필수입니다.") + val providerId: String? = null ) { fun getOrDefaultEmail(): String { return email?.takeIf { it.isNotBlank() } ?: "${validProviderId()}@${validProviderType().name.lowercase()}.local" diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt index 752207b7..1ecc7abb 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt @@ -12,18 +12,18 @@ import java.util.* description = "Request DTO to generate a new pair of access and refresh tokens" ) data class GenerateTokenPairRequest private constructor( - @field:NotBlank(message = "userId must not be null") @Schema( description = "User ID", example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0" ) + @field:NotBlank(message = "userId must not be null") val userId: UUID? = null, - @field:NotBlank(message = "role must not be null") @Schema( description = "User role", example = "USER" ) + @field:NotBlank(message = "role must not be null") val role: Role? = null ) { diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt index 009f3b5b..3adae361 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt @@ -14,11 +14,19 @@ import org.yapp.domain.user.ProviderType description = "DTO for social login requests" ) data class SocialLoginRequest private constructor( - @Schema(description = "Type of social login provider", example = "KAKAO", required = true) + @Schema( + description = "Type of social login provider", + example = "KAKAO", + required = true + ) @field:NotBlank(message = "Provider type is required") val providerType: String? = null, - @Schema(description = "OAuth token issued by the social provider", example = "eyJ...", required = true) + @Schema( + description = "OAuth token issued by the social provider", + example = "eyJ...", + required = true + ) @field:NotBlank(message = "OAuth token is required") val oauthToken: String? = null ) { diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenRefreshRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenRefreshRequest.kt index 5d5511cc..65fc200f 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenRefreshRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenRefreshRequest.kt @@ -8,13 +8,12 @@ import jakarta.validation.constraints.NotBlank description = "DTO for requesting an access token using a refresh token" ) data class TokenRefreshRequest private constructor( - - @field:NotBlank(message = "Refresh token is required") @Schema( description = "Valid refresh token issued during previous authentication", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", required = true ) + @field:NotBlank(message = "Refresh token is required") val refreshToken: String? = null ) { fun validRefreshToken(): String = refreshToken!! diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/AuthResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/AuthResponse.kt index 10bd77c7..81abe1d4 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/AuthResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/AuthResponse.kt @@ -21,8 +21,7 @@ data class AuthResponse private constructor( val refreshToken: String ) { companion object { - fun fromTokenPair( - tokenPairResponse: TokenPairResponse) + fun fromTokenPair(tokenPairResponse: TokenPairResponse) : AuthResponse { return AuthResponse( accessToken = tokenPairResponse.accessToken, diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt index aa9af5fb..f2d543a7 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt @@ -10,10 +10,16 @@ import java.util.* description = "Response DTO returned after successful user registration" ) data class CreateUserResponse private constructor( - @Schema(description = "사용자 ID", example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0") + @Schema( + description = "사용자 ID", + example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0" + ) val id: UUID, - @Schema(description = "사용자 역할", example = "USER") + @Schema( + description = "사용자 역할", + example = "USER" + ) val role: Role ) { companion object { diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt index 17dcf30d..2300e96f 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt @@ -10,10 +10,16 @@ import java.util.* description = "Response DTO containing minimal authentication information (ID and role)" ) data class UserAuthInfoResponse private constructor( - @Schema(description = "Unique identifier of the user", example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0") + @Schema( + description = "Unique identifier of the user", + example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0" + ) val id: UUID, - @Schema(description = "Role assigned to the user", example = "USER") + @Schema( + description = "Role assigned to the user", + example = "USER" + ) val role: Role ) { companion object { From 4d4ad7b1d85a21ba2b41fddd27c5c70a54256aa2 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 13:41:24 +0900 Subject: [PATCH 19/49] =?UTF-8?q?[BOOK-140]=20refactor:=20apis,=20domain?= =?UTF-8?q?=20-=20=EC=BD=94=EB=93=9C=EB=A6=AC=EB=B7=B0=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 --- .../dto/request/GenerateTokenPairRequest.kt | 2 +- .../auth/dto/response/CreateUserResponse.kt | 6 ++-- .../auth/dto/response/UserAuthInfoResponse.kt | 6 ++-- .../auth/dto/response/UserProfileResponse.kt | 14 ++++---- .../yapp/apis/auth/service/UserAuthService.kt | 4 +-- .../org/yapp/domain/user/UserDomainService.kt | 28 +++++++-------- .../yapp/domain/user/vo/SocialUserProfile.kt | 35 ------------------- .../vo/{UserIdentity.kt => UserIdentityVO.kt} | 6 ++-- .../vo/{UserProfile.kt => UserProfileVO.kt} | 6 ++-- 9 files changed, 36 insertions(+), 71 deletions(-) delete mode 100644 domain/src/main/kotlin/org/yapp/domain/user/vo/SocialUserProfile.kt rename domain/src/main/kotlin/org/yapp/domain/user/vo/{UserIdentity.kt => UserIdentityVO.kt} (80%) rename domain/src/main/kotlin/org/yapp/domain/user/vo/{UserProfile.kt => UserProfileVO.kt} (85%) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt index 1ecc7abb..cda082b7 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt @@ -5,7 +5,7 @@ import jakarta.validation.constraints.NotBlank import org.yapp.apis.auth.dto.response.CreateUserResponse import org.yapp.apis.auth.dto.response.UserAuthInfoResponse import org.yapp.globalutils.auth.Role -import java.util.* +import java.util.UUID @Schema( name = "GenerateTokenPairRequest", diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt index f2d543a7..d2c6afdf 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt @@ -1,9 +1,9 @@ package org.yapp.apis.auth.dto.response import io.swagger.v3.oas.annotations.media.Schema -import org.yapp.domain.user.vo.UserIdentity +import org.yapp.domain.user.vo.UserIdentityVO import org.yapp.globalutils.auth.Role -import java.util.* +import java.util.UUID @Schema( name = "CreateUserResponse", @@ -23,7 +23,7 @@ data class CreateUserResponse private constructor( val role: Role ) { companion object { - fun from(identity: UserIdentity): CreateUserResponse { + fun from(identity: UserIdentityVO): CreateUserResponse { return CreateUserResponse( id = identity.id, role = identity.role diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt index 2300e96f..c92b3a6c 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt @@ -1,9 +1,9 @@ package org.yapp.apis.auth.dto.response import io.swagger.v3.oas.annotations.media.Schema -import org.yapp.domain.user.vo.UserIdentity +import org.yapp.domain.user.vo.UserIdentityVO import org.yapp.globalutils.auth.Role -import java.util.* +import java.util.UUID @Schema( name = "UserAuthInfoResponse", @@ -23,7 +23,7 @@ data class UserAuthInfoResponse private constructor( val role: Role ) { companion object { - fun from(identity: UserIdentity): UserAuthInfoResponse { + fun from(identity: UserIdentityVO): UserAuthInfoResponse { return UserAuthInfoResponse( id = identity.id, role = identity.role diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt index a17fe060..ec2dca8c 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt @@ -2,8 +2,8 @@ package org.yapp.apis.auth.dto.response import io.swagger.v3.oas.annotations.media.Schema import org.yapp.domain.user.ProviderType -import org.yapp.domain.user.vo.UserProfile -import java.util.* +import org.yapp.domain.user.vo.UserProfileVO +import java.util.UUID @Schema( name = "UserProfileResponse", @@ -36,12 +36,12 @@ data class UserProfileResponse( val provider: ProviderType ) { companion object { - fun from(userProfile: UserProfile): UserProfileResponse { + fun from(userProfileVO: UserProfileVO): UserProfileResponse { return UserProfileResponse( - id = userProfile.id, - email = userProfile.email, - nickname = userProfile.nickname, - provider = userProfile.provider + id = userProfileVO.id, + email = userProfileVO.email, + nickname = userProfileVO.nickname, + provider = userProfileVO.provider ) } } diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt index 74e4a08b..68e93bd3 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt @@ -8,7 +8,7 @@ import org.yapp.apis.auth.dto.response.UserProfileResponse import org.yapp.apis.auth.exception.AuthErrorCode import org.yapp.apis.auth.exception.AuthException import org.yapp.domain.user.UserDomainService -import org.yapp.domain.user.vo.UserIdentity +import org.yapp.domain.user.vo.UserIdentityVO import java.util.* @Service @@ -50,7 +50,7 @@ class UserAuthService( return CreateUserResponse.from(createdUser) } - private fun createNewUser(findOrCreateUserRequest: FindOrCreateUserRequest): UserIdentity { + private fun createNewUser(findOrCreateUserRequest: FindOrCreateUserRequest): UserIdentityVO { val email = findOrCreateUserRequest.getOrDefaultEmail() val nickname = findOrCreateUserRequest.getOrDefaultNickname() diff --git a/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt index f888e0e3..f54e5c18 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt @@ -2,8 +2,8 @@ package org.yapp.domain.user import org.yapp.domain.user.exception.UserErrorCode import org.yapp.domain.user.exception.UserNotFoundException -import org.yapp.domain.user.vo.UserIdentity -import org.yapp.domain.user.vo.UserProfile +import org.yapp.domain.user.vo.UserIdentityVO +import org.yapp.domain.user.vo.UserProfileVO import org.yapp.globalutils.annotation.DomainService import org.yapp.globalutils.util.TimeProvider import java.util.* @@ -13,24 +13,24 @@ class UserDomainService( private val userRepository: UserRepository, private val timeProvider: TimeProvider ) { - fun findUserProfileById(id: UUID): UserProfile { + fun findUserProfileById(id: UUID): UserProfileVO { val user = userRepository.findById(id) ?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND) - return UserProfile.newInstance(user) + return UserProfileVO.newInstance(user) } - fun findUserIdentityById(id: UUID): UserIdentity { + fun findUserIdentityById(id: UUID): UserIdentityVO { val user = userRepository.findById(id) ?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND) - return UserIdentity.newInstance(user) + return UserIdentityVO.newInstance(user) } - fun findUserByProviderTypeAndProviderId(providerType: ProviderType, providerId: String): UserIdentity? { + fun findUserByProviderTypeAndProviderId(providerType: ProviderType, providerId: String): UserIdentityVO? { return userRepository.findByProviderTypeAndProviderId(providerType, providerId) - ?.let { UserIdentity.newInstance(it) } + ?.let { UserIdentityVO.newInstance(it) } } - fun findUserByProviderTypeAndProviderIdIncludingDeleted(providerType: ProviderType, providerId: String): UserIdentity? { + fun findUserByProviderTypeAndProviderIdIncludingDeleted(providerType: ProviderType, providerId: String): UserIdentityVO? { return userRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType, providerId) - ?.let { UserIdentity.newInstance(it) } + ?.let { UserIdentityVO.newInstance(it) } } fun existsActiveUserByIdAndDeletedAtIsNull(userId: UUID): Boolean { @@ -47,7 +47,7 @@ class UserDomainService( profileImageUrl: String?, providerType: ProviderType, providerId: String - ): UserIdentity { + ): UserIdentityVO { val now = timeProvider.now() val user = User.create( email = email, @@ -59,14 +59,14 @@ class UserDomainService( updatedAt = now ) val savedUser = userRepository.save(user) - return UserIdentity.newInstance(savedUser) + return UserIdentityVO.newInstance(savedUser) } - fun restoreDeletedUser(userId: UUID): UserIdentity { + fun restoreDeletedUser(userId: UUID): UserIdentityVO { val deletedUser = userRepository.findById(userId) ?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND) val restoredUser = userRepository.save(deletedUser.restore()) - return UserIdentity.newInstance(restoredUser) + return UserIdentityVO.newInstance(restoredUser) } } diff --git a/domain/src/main/kotlin/org/yapp/domain/user/vo/SocialUserProfile.kt b/domain/src/main/kotlin/org/yapp/domain/user/vo/SocialUserProfile.kt deleted file mode 100644 index 4af4dcc3..00000000 --- a/domain/src/main/kotlin/org/yapp/domain/user/vo/SocialUserProfile.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.yapp.domain.user.vo - -import org.yapp.domain.user.ProviderType -import org.yapp.globalutils.util.RegexUtils - -data class SocialUserProfile private constructor( - val email: String, - val nickname: String, - val profileImageUrl: String?, - val providerType: ProviderType, - val providerId: String -) { - init { - require(email.isNotBlank()) { "Email must not be blank" } - require(RegexUtils.isValidEmail(email)) { "Email format is invalid" } - require(nickname.isNotBlank()) { "Nickname must not be blank" } - require(nickname.length in 2..30) { "Nickname length must be between 2 and 30" } - require(providerId.isNotBlank()) { "ProviderId must not be blank" } - profileImageUrl?.let { - require(RegexUtils.isValidProfileImageUrl(it)) { "ProfileImageUrl must be a valid URL" } - } - } - - companion object { - fun newInstance( - email: String, - nickname: String, - profileImageUrl: String?, - providerType: ProviderType, - providerId: String - ): SocialUserProfile { - return SocialUserProfile(email, nickname, profileImageUrl, providerType, providerId) - } - } -} diff --git a/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentity.kt b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentityVO.kt similarity index 80% rename from domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentity.kt rename to domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentityVO.kt index e3b2ac9e..dff16312 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentity.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentityVO.kt @@ -4,7 +4,7 @@ import org.yapp.domain.user.User import org.yapp.globalutils.auth.Role import java.util.UUID -data class UserIdentity( +data class UserIdentityVO( val id: UUID, val role: Role ) { @@ -14,9 +14,9 @@ data class UserIdentity( } companion object { - fun newInstance(user: User?): UserIdentity { + fun newInstance(user: User?): UserIdentityVO { requireNotNull(user) { "User must not be null." } - return UserIdentity( + return UserIdentityVO( id = user.id, role = user.role ) diff --git a/domain/src/main/kotlin/org/yapp/domain/user/vo/UserProfile.kt b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserProfileVO.kt similarity index 85% rename from domain/src/main/kotlin/org/yapp/domain/user/vo/UserProfile.kt rename to domain/src/main/kotlin/org/yapp/domain/user/vo/UserProfileVO.kt index d0885d2d..28c5f1f2 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/vo/UserProfile.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserProfileVO.kt @@ -4,7 +4,7 @@ import org.yapp.domain.user.ProviderType import org.yapp.domain.user.User import java.util.UUID -data class UserProfile private constructor( +data class UserProfileVO private constructor( val id: UUID, val email: String, val nickname: String, @@ -17,10 +17,10 @@ data class UserProfile private constructor( } companion object { - fun newInstance(user: User?): UserProfile { + fun newInstance(user: User?): UserProfileVO { requireNotNull(user) { "User는 null일 수 없습니다." } - return UserProfile( + return UserProfileVO( id = user.id, email = user.email, nickname = user.nickname, From dc25553e02ddf0763ef663c1e7d6a61afb574d77 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 13:54:25 +0900 Subject: [PATCH 20/49] =?UTF-8?q?[BOOK-140]=20refactor:=20apis,=20domain,?= =?UTF-8?q?=20infra=20-=20=EC=BD=94=EB=93=9C=EB=A6=AC=EB=B7=B0=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 --- .../main/kotlin/org/yapp/domain/user/UserDomainService.kt | 6 +++--- .../main/kotlin/org/yapp/domain/user/UserRepository.kt | 5 ++--- .../main/kotlin/org/yapp/infra/user/entity/UserEntity.kt | 6 ++++-- .../org/yapp/infra/user/repository/JpaUserRepository.kt | 4 +--- .../yapp/infra/user/repository/impl/UserRepositoryImpl.kt | 8 ++++---- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt index f54e5c18..b46fa69e 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt @@ -6,7 +6,7 @@ import org.yapp.domain.user.vo.UserIdentityVO import org.yapp.domain.user.vo.UserProfileVO import org.yapp.globalutils.annotation.DomainService import org.yapp.globalutils.util.TimeProvider -import java.util.* +import java.util.UUID @DomainService class UserDomainService( @@ -34,11 +34,11 @@ class UserDomainService( } fun existsActiveUserByIdAndDeletedAtIsNull(userId: UUID): Boolean { - return userRepository.existsByIdAndDeletedAtIsNull(userId) + return userRepository.existsById(userId) } fun existsActiveUserByEmailAndDeletedAtIsNull(email: String): Boolean { - return userRepository.existsByEmailAndDeletedAtIsNull(email) + return userRepository.existsByEmail(email) } fun create( diff --git a/domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt b/domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt index 7e280d2a..b49251c2 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt @@ -1,6 +1,5 @@ package org.yapp.domain.user -import org.yapp.domain.user.ProviderType import java.util.* /** @@ -16,7 +15,7 @@ interface UserRepository { fun save(user: User): User - fun existsByIdAndDeletedAtIsNull(userId: UUID): Boolean + fun existsById(userId: UUID): Boolean - fun existsByEmailAndDeletedAtIsNull(email: String): Boolean + fun existsByEmail(email: String): Boolean } diff --git a/infra/src/main/kotlin/org/yapp/infra/user/entity/UserEntity.kt b/infra/src/main/kotlin/org/yapp/infra/user/entity/UserEntity.kt index 8b98c2c1..e0cdd9ec 100644 --- a/infra/src/main/kotlin/org/yapp/infra/user/entity/UserEntity.kt +++ b/infra/src/main/kotlin/org/yapp/infra/user/entity/UserEntity.kt @@ -3,16 +3,18 @@ package org.yapp.infra.user.entity import jakarta.persistence.* import org.hibernate.annotations.JdbcTypeCode import org.hibernate.annotations.SQLDelete -import org.yapp.domain.user.ProviderType +import org.hibernate.annotations.SQLRestriction import org.yapp.domain.common.BaseTimeEntity -import org.yapp.globalutils.auth.Role +import org.yapp.domain.user.ProviderType import org.yapp.domain.user.User +import org.yapp.globalutils.auth.Role import java.sql.Types import java.util.* @Entity @Table(name = "users") @SQLDelete(sql = "UPDATE users SET deleted_at = NOW() WHERE id = ?") +@SQLRestriction("deleted_at IS NULL") class UserEntity private constructor( @Id @JdbcTypeCode(Types.VARCHAR) diff --git a/infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt b/infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt index 3bdd1550..9453cffd 100644 --- a/infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt +++ b/infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt @@ -13,9 +13,7 @@ interface JpaUserRepository : JpaRepository { fun findByProviderTypeAndProviderId(providerType: ProviderType, providerId: String): UserEntity? - fun existsByIdAndDeletedAtIsNull(id: UUID): Boolean - - fun existsByEmailAndDeletedAtIsNull(email: String): Boolean + fun existsByEmail(email: String): Boolean @Query("SELECT u FROM UserEntity u WHERE u.providerType = :providerType AND u.providerId = :providerId") fun findByProviderTypeAndProviderIdIncludingDeleted(providerType: ProviderType, providerId: String): UserEntity? diff --git a/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt index 5ce3efa7..0a1ff9c0 100644 --- a/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt +++ b/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt @@ -27,12 +27,12 @@ class UserRepositoryImpl( return jpaUserRepository.findById(id).orElse(null)?.toDomain() } - override fun existsByEmailAndDeletedAtIsNull(email: String): Boolean { - return jpaUserRepository.existsByEmailAndDeletedAtIsNull(email) + override fun existsById(userId: UUID): Boolean { + return jpaUserRepository.existsById(userId) } - override fun existsByIdAndDeletedAtIsNull(userId: UUID): Boolean { - return jpaUserRepository.existsByIdAndDeletedAtIsNull(userId) + override fun existsByEmail(email: String): Boolean { + return jpaUserRepository.existsByEmail(email) } override fun findByProviderTypeAndProviderIdIncludingDeleted( From d42517b6bd636f19484f5ca34b97bd549875a46a Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 13:55:58 +0900 Subject: [PATCH 21/49] =?UTF-8?q?[BOOK-140]=20refactor:=20domain=20-=20Uui?= =?UTF-8?q?dGenerator=20=EC=9C=A0=ED=8B=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EB=A1=9C=20=EC=95=84=EC=9D=B4=EB=94=94=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- domain/src/main/kotlin/org/yapp/domain/user/User.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/domain/src/main/kotlin/org/yapp/domain/user/User.kt b/domain/src/main/kotlin/org/yapp/domain/user/User.kt index 21293785..3fac8f9a 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/User.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/User.kt @@ -1,6 +1,7 @@ package org.yapp.domain.user import org.yapp.globalutils.auth.Role +import org.yapp.globalutils.util.UuidGenerator import java.time.LocalDateTime import java.util.* @@ -51,7 +52,7 @@ data class User private constructor( deletedAt: LocalDateTime? = null ): User { return User( - id = UUID.randomUUID(), + id = UuidGenerator.create(), email = email, nickname = nickname, profileImageUrl = profileImageUrl, @@ -77,7 +78,7 @@ data class User private constructor( deletedAt: LocalDateTime? = null ): User { return User( - id = UUID.randomUUID(), + id = UuidGenerator.create(), email = email, nickname = nickname, profileImageUrl = profileImageUrl, From 873ecb303a544208e45783cf370234dd5f3ed2f5 Mon Sep 17 00:00:00 2001 From: DONGHOON LEE <125895298+hoonyworld@users.noreply.github.com> Date: Tue, 15 Jul 2025 16:44:04 +0900 Subject: [PATCH 22/49] =?UTF-8?q?fix:=20sub=20=ED=81=B4=EB=A0=88=EC=9E=84?= =?UTF-8?q?=20=EA=B0=92=EC=9D=84=20UUID=EB=A1=9C=20=EB=B3=80=ED=99=98?= =?UTF-8?q?=ED=95=B4=20Authentication=20=EA=B0=9D=EC=B2=B4=EC=9D=98=20prin?= =?UTF-8?q?cipal=EB=A1=9C=20=EC=84=A4=EC=A0=95=ED=95=98=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(#44)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [BOOK-143] fix: gateway - sub 클레임을 UUID 타입으로 변환하여 인증 객체의 principal로 설정하도록 수정 * [BOOK-143] fix: gateway - SecurityConfig에서 의존성 타입 일치하도록 수정 --- .../org/yapp/gateway/config/JwtConfig.kt | 33 ++++++++++++------- .../yapp/gateway/security/SecurityConfig.kt | 6 ++-- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/gateway/src/main/kotlin/org/yapp/gateway/config/JwtConfig.kt b/gateway/src/main/kotlin/org/yapp/gateway/config/JwtConfig.kt index 18b5ef26..769e01d0 100644 --- a/gateway/src/main/kotlin/org/yapp/gateway/config/JwtConfig.kt +++ b/gateway/src/main/kotlin/org/yapp/gateway/config/JwtConfig.kt @@ -8,11 +8,14 @@ import com.nimbusds.jose.proc.SecurityContext import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.core.convert.converter.Converter +import org.springframework.security.authentication.AbstractAuthenticationToken +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.oauth2.jose.jws.MacAlgorithm import org.springframework.security.oauth2.jwt.* -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter import org.yapp.gateway.constants.JwtConstants +import java.util.UUID import javax.crypto.spec.SecretKeySpec @Configuration @@ -23,6 +26,7 @@ class JwtConfig( companion object { private val SIGNATURE_ALGORITHM = MacAlgorithm.HS256 private const val PRINCIPAL_CLAIM = "sub" + private const val NO_AUTHORITY_PREFIX = "" } /** @@ -65,19 +69,24 @@ class JwtConfig( } /** - * 유효성이 검증된 JWT를 Spring Security의 `Authentication` 객체로 변환하는 `JwtAuthenticationConverter`를 등록합니다. - * JWT의 'roles' 클레임을 애플리케이션의 권한 정보(`GrantedAuthority`)로 매핑하고, 'sub' 클레임을 사용자의 주체(Principal)로 설정합니다. + * 유효한 JWT를 Spring Security의 `Authentication` 객체로 변환하는 `JwtAuthenticationConverter` 빈을 설정합니다. * - * @return 생성된 `JwtAuthenticationConverter` 객체 + * - JWT의 `roles` 클레임을 추출하여 `GrantedAuthority` 리스트로 변환합니다. + * - 기본적으로 붙는 권한 접두어(`SCOPE_`)를 제거하기 위해 빈 문자열로 설정합니다. + * - JWT의 `sub` 클레임(문자열 형태의 UUID)을 `UUID` 타입으로 변환하여 인증 주체(Principal)로 사용합니다. + * + * @return JWT를 `UsernamePasswordAuthenticationToken` 으로 변환하는 Converter */ @Bean - fun jwtAuthenticationConverter(): JwtAuthenticationConverter { - val converter = JwtAuthenticationConverter() - converter.setJwtGrantedAuthoritiesConverter { jwt -> - val roles = jwt.getClaimAsStringList(JwtConstants.ROLES_CLAIM) ?: emptyList() - roles.map { role -> SimpleGrantedAuthority(role) } + fun jwtAuthenticationConverter(): Converter { + val authoritiesConverter = JwtGrantedAuthoritiesConverter() + authoritiesConverter.setAuthoritiesClaimName(JwtConstants.ROLES_CLAIM) + authoritiesConverter.setAuthorityPrefix(NO_AUTHORITY_PREFIX) + + return Converter { jwt -> + val authorities = authoritiesConverter.convert(jwt) + val principal = UUID.fromString(jwt.getClaimAsString(PRINCIPAL_CLAIM)) + UsernamePasswordAuthenticationToken(principal, null, authorities) } - converter.setPrincipalClaimName(PRINCIPAL_CLAIM) - return converter } } diff --git a/gateway/src/main/kotlin/org/yapp/gateway/security/SecurityConfig.kt b/gateway/src/main/kotlin/org/yapp/gateway/security/SecurityConfig.kt index 15433464..69be2a5e 100644 --- a/gateway/src/main/kotlin/org/yapp/gateway/security/SecurityConfig.kt +++ b/gateway/src/main/kotlin/org/yapp/gateway/security/SecurityConfig.kt @@ -5,7 +5,9 @@ import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.http.SessionCreationPolicy -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter +import org.springframework.core.convert.converter.Converter +import org.springframework.security.authentication.AbstractAuthenticationToken +import org.springframework.security.oauth2.jwt.Jwt import org.springframework.security.web.SecurityFilterChain /** @@ -14,7 +16,7 @@ import org.springframework.security.web.SecurityFilterChain @Configuration @EnableWebSecurity class SecurityConfig( - private val jwtAuthenticationConverter: JwtAuthenticationConverter, + private val jwtAuthenticationConverter: Converter, private val customAuthenticationEntryPoint: CustomAuthenticationEntryPoint, private val customAccessDeniedHandler: CustomAccessDeniedHandler ) { From 0b5f8a4eefef2405834b374dc214f9d1a222afcf Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 15 Jul 2025 23:54:38 +0900 Subject: [PATCH 23/49] =?UTF-8?q?[BOOK-140]=20refactor:=20domain,=20infra?= =?UTF-8?q?=20-=20jpa=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9E=AC=EC=A0=95?= =?UTF-8?q?=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/yapp/domain/token/TokenDomainRedisService.kt | 1 - .../main/kotlin/org/yapp/domain/user/UserDomainService.kt | 4 ++-- .../src/main/kotlin/org/yapp/domain/user/UserRepository.kt | 2 +- .../yapp/infra/user/repository/impl/UserRepositoryImpl.kt | 7 ++++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/domain/src/main/kotlin/org/yapp/domain/token/TokenDomainRedisService.kt b/domain/src/main/kotlin/org/yapp/domain/token/TokenDomainRedisService.kt index fe816f56..0be24998 100644 --- a/domain/src/main/kotlin/org/yapp/domain/token/TokenDomainRedisService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/token/TokenDomainRedisService.kt @@ -8,7 +8,6 @@ import java.util.UUID class TokenDomainRedisService( private val refreshTokenRepository: RefreshTokenRepository ) { - fun saveRefreshToken(userId: UUID, refreshToken: String, expiration: Long) { val expiresAt = LocalDateTime.now().plusSeconds(expiration) val token = RefreshToken.create( diff --git a/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt index b46fa69e..5acb6244 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt @@ -33,8 +33,8 @@ class UserDomainService( ?.let { UserIdentityVO.newInstance(it) } } - fun existsActiveUserByIdAndDeletedAtIsNull(userId: UUID): Boolean { - return userRepository.existsById(userId) + fun existsActiveUserByIdAndDeletedAtIsNull(id: UUID): Boolean { + return userRepository.existsById(id) } fun existsActiveUserByEmailAndDeletedAtIsNull(email: String): Boolean { diff --git a/domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt b/domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt index b49251c2..114dc130 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt @@ -15,7 +15,7 @@ interface UserRepository { fun save(user: User): User - fun existsById(userId: UUID): Boolean + fun existsById(id: UUID): Boolean fun existsByEmail(email: String): Boolean } diff --git a/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt index 0a1ff9c0..42692ec9 100644 --- a/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt +++ b/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt @@ -1,5 +1,6 @@ package org.yapp.infra.user.repository.impl +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Repository import org.yapp.domain.user.ProviderType import org.yapp.domain.user.User @@ -24,11 +25,11 @@ class UserRepositoryImpl( } override fun findById(id: UUID): User? { - return jpaUserRepository.findById(id).orElse(null)?.toDomain() + return jpaUserRepository.findByIdOrNull(id)?.toDomain() } - override fun existsById(userId: UUID): Boolean { - return jpaUserRepository.existsById(userId) + override fun existsById(id: UUID): Boolean { + return jpaUserRepository.existsById(id) } override fun existsByEmail(email: String): Boolean { From f9c38e3262dd24a2a121f6be03a2824ff8592fbb Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 00:13:12 +0900 Subject: [PATCH 24/49] =?UTF-8?q?[BOOK-140]=20feat:=20domain=20-=20?= =?UTF-8?q?=EB=A6=AC=ED=94=84=EB=A0=88=EC=89=AC=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/token/exception/TokenErrorCode.kt | 18 ++++++++++++++++++ .../token/exception/TokenNotFoundException.kt | 9 +++++++++ 2 files changed, 27 insertions(+) create mode 100644 domain/src/main/kotlin/org/yapp/domain/token/exception/TokenErrorCode.kt create mode 100644 domain/src/main/kotlin/org/yapp/domain/token/exception/TokenNotFoundException.kt diff --git a/domain/src/main/kotlin/org/yapp/domain/token/exception/TokenErrorCode.kt b/domain/src/main/kotlin/org/yapp/domain/token/exception/TokenErrorCode.kt new file mode 100644 index 00000000..934ec4d0 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/token/exception/TokenErrorCode.kt @@ -0,0 +1,18 @@ +package org.yapp.domain.token.exception + +import org.springframework.http.HttpStatus +import org.yapp.globalutils.exception.BaseErrorCode + +enum class TokenErrorCode( + private val status: HttpStatus, + private val code: String, + private val message: String +) : BaseErrorCode { + TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "TOKEN_001", "토큰 정보를 찾을 수 없습니다."), + INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN_002", "유효하지 않은 리프레시 토큰입니다."), + EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN_003", "리프레시 토큰이 만료되었습니다."); + + override fun getHttpStatus(): HttpStatus = status + override fun getCode(): String = code + override fun getMessage(): String = message +} diff --git a/domain/src/main/kotlin/org/yapp/domain/token/exception/TokenNotFoundException.kt b/domain/src/main/kotlin/org/yapp/domain/token/exception/TokenNotFoundException.kt new file mode 100644 index 00000000..8e9f8be9 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/token/exception/TokenNotFoundException.kt @@ -0,0 +1,9 @@ +package org.yapp.domain.token.exception + +import org.yapp.globalutils.exception.CommonException + +class TokenNotFoundException( + errorCode: TokenErrorCode, + message: String? = null +) : CommonException(errorCode, message) + From 7c8eb799795b56ad2d02d0a3f7320efa461c9ecb Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 00:13:42 +0900 Subject: [PATCH 25/49] =?UTF-8?q?[BOOK-140]=20chore:=20domain,=20apis=20-?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=84=A4=EC=9D=B4=EB=B0=8D=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 --- .../main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt | 2 +- .../src/main/kotlin/org/yapp/domain/user/UserDomainService.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt index 68e93bd3..c7ce356c 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt @@ -58,7 +58,7 @@ class UserAuthService( throw AuthException(AuthErrorCode.EMAIL_ALREADY_IN_USE, "Email already in use") } - return userDomainService.create( + return userDomainService.createNewUser( email = email, nickname = nickname, profileImageUrl = findOrCreateUserRequest.profileImageUrl, diff --git a/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt index 5acb6244..b4cc7728 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt @@ -41,7 +41,7 @@ class UserDomainService( return userRepository.existsByEmail(email) } - fun create( + fun createNewUser( email: String, nickname: String, profileImageUrl: String?, From 491594dfe895ab2b25f1c4edbb1bc79b44d9e814 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 00:13:57 +0900 Subject: [PATCH 26/49] =?UTF-8?q?[BOOK-140]=20chore:=20domain=20-=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=20=EC=97=86=EB=8A=94=20=EC=A4=91=EA=B4=84?= =?UTF-8?q?=ED=98=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/yapp/domain/user/exception/UserNotFoundException.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/domain/src/main/kotlin/org/yapp/domain/user/exception/UserNotFoundException.kt b/domain/src/main/kotlin/org/yapp/domain/user/exception/UserNotFoundException.kt index b9312670..2005fdf0 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/exception/UserNotFoundException.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/exception/UserNotFoundException.kt @@ -5,6 +5,5 @@ import org.yapp.globalutils.exception.CommonException class UserNotFoundException ( errorCode: UserErrorCode, message: String? = null -) : CommonException(errorCode, message) { -} +) : CommonException(errorCode, message) From 0e274624fe622ae62b7f724495676dc957191dcc Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 02:22:04 +0900 Subject: [PATCH 27/49] =?UTF-8?q?[BOOK-140]=20feat:=20doamin,=20infra=20-?= =?UTF-8?q?=20RefreshToken=20=EB=8F=84=EB=A9=94=EC=9D=B8=EC=97=90=20Value?= =?UTF-8?q?=20Class=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=9E=90=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RefreshToken 내 id, token, userId를 각각 Value Class(Id, Token, UserId)로 분리 - Value Class 내 newInstance 정적 팩토리 메서드 구현 및 검증 로직 추가 --- .../org/yapp/domain/token/RefreshToken.kt | 57 +++++++++++++++---- .../external/redis/entity/RefreshToken.kt | 17 +++--- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/domain/src/main/kotlin/org/yapp/domain/token/RefreshToken.kt b/domain/src/main/kotlin/org/yapp/domain/token/RefreshToken.kt index b597b5a7..87802a0b 100644 --- a/domain/src/main/kotlin/org/yapp/domain/token/RefreshToken.kt +++ b/domain/src/main/kotlin/org/yapp/domain/token/RefreshToken.kt @@ -4,14 +4,17 @@ import org.yapp.globalutils.util.UuidGenerator import java.time.LocalDateTime import java.util.* - data class RefreshToken private constructor( - val id: UUID?, - val token: String, - val userId: UUID, + val id: Id?, + val token: Token, + val userId: UserId, val expiresAt: LocalDateTime, val createdAt: LocalDateTime ) { + fun isExpired(): Boolean { + return expiresAt.isBefore(LocalDateTime.now()) + } + companion object { fun create( token: String, @@ -20,18 +23,18 @@ data class RefreshToken private constructor( createdAt: LocalDateTime ): RefreshToken { return RefreshToken( - id = UuidGenerator.create(), - token = token, - userId = userId, + id = Id.newInstance(UuidGenerator.create()), + token = Token.newInstance(token), + userId = UserId.newInstance(userId), expiresAt = expiresAt, createdAt = createdAt ) } fun reconstruct( - id: UUID, - token: String, - userId: UUID, + id: Id, + token: Token, + userId: UserId, expiresAt: LocalDateTime, createdAt: LocalDateTime ): RefreshToken { @@ -44,4 +47,38 @@ data class RefreshToken private constructor( ) } } + + @JvmInline + value class Id(val value: UUID) { + override fun toString(): String = value.toString() + + companion object { + fun newInstance(value: UUID): Id { + return Id(value) + } + } + } + + @JvmInline + value class Token(val value: String) { + override fun toString(): String = value + + companion object { + fun newInstance(value: String): Token { + require(value.isNotBlank()) { "Token must not be blank" } + return Token(value) + } + } + } + + @JvmInline + value class UserId(val value: UUID) { + override fun toString(): String = value.toString() + + companion object { + fun newInstance(value: UUID): UserId { + return UserId(value) + } + } + } } diff --git a/infra/src/main/kotlin/org/yapp/infra/external/redis/entity/RefreshToken.kt b/infra/src/main/kotlin/org/yapp/infra/external/redis/entity/RefreshToken.kt index 433a227d..dd226d77 100644 --- a/infra/src/main/kotlin/org/yapp/infra/external/redis/entity/RefreshToken.kt +++ b/infra/src/main/kotlin/org/yapp/infra/external/redis/entity/RefreshToken.kt @@ -8,12 +8,11 @@ import org.yapp.globalutils.util.UuidGenerator import java.time.LocalDateTime import java.util.* - @RedisHash( value = "refreshToken", timeToLive = 1209600 ) -data class RefreshTokenEntity private constructor( +class RefreshTokenEntity private constructor( @Id val id: UUID, @Indexed @@ -23,22 +22,20 @@ data class RefreshTokenEntity private constructor( val expiresAt: LocalDateTime, val createdAt: LocalDateTime ) { - fun toDomain(): RefreshToken = RefreshToken.reconstruct( - id = id, - token = token, - userId = userId, + id = RefreshToken.Id.newInstance(id), + token = RefreshToken.Token.newInstance(token), + userId = RefreshToken.UserId.newInstance(userId), expiresAt = expiresAt, createdAt = createdAt ) companion object { - fun fromDomain(refreshToken: RefreshToken): RefreshTokenEntity { return RefreshTokenEntity( - id = refreshToken.id ?: UuidGenerator.create(), - token = refreshToken.token, - userId = refreshToken.userId, + id = refreshToken.id?.value ?: UuidGenerator.create(), + token = refreshToken.token.value, + userId = refreshToken.userId.value, expiresAt = refreshToken.expiresAt, createdAt = refreshToken.createdAt ) From 4842ba6b0798a75df20714dea27887e597c61d59 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 02:22:41 +0900 Subject: [PATCH 28/49] =?UTF-8?q?[BOOK-140]=20feat:=20apis=20-=20=EB=B9=84?= =?UTF-8?q?=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=EC=97=90=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EB=90=A0=20=EC=9A=94=EC=B2=AD=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20dto=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/dto/request/DeleteTokenRequest.kt | 23 +++++++++++ .../dto/request/FindUserIdentityRequest.kt | 27 ++++++++++++ .../auth/dto/request/TokenGenerateRequest.kt | 41 +++++++++++++++++++ .../auth/dto/response/RefreshTokenResponse.kt | 19 +++++++++ .../apis/auth/dto/response/UserIdResponse.kt | 20 +++++++++ 5 files changed, 130 insertions(+) create mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/dto/request/DeleteTokenRequest.kt create mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindUserIdentityRequest.kt create mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenGenerateRequest.kt create mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/dto/response/RefreshTokenResponse.kt create mode 100644 apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserIdResponse.kt diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/DeleteTokenRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/DeleteTokenRequest.kt new file mode 100644 index 00000000..05d3e75a --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/DeleteTokenRequest.kt @@ -0,0 +1,23 @@ +package org.yapp.apis.auth.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import org.yapp.apis.auth.dto.response.RefreshTokenResponse + +@Schema( + name = "DeleteTokenRequest", + description = "Request DTO for deleting a refresh token" +) +data class DeleteTokenRequest( + @field:NotBlank(message = "Refresh token must not be blank.") + @Schema(description = "Refresh token to be deleted", example = "eyJhbGciOiJIUz...") + val refreshToken: String? = null +) { + fun validRefreshToken() = refreshToken!! + + companion object { + fun from(refreshTokenResponse: RefreshTokenResponse): DeleteTokenRequest { + return DeleteTokenRequest(refreshToken = refreshTokenResponse.refreshToken) + } + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindUserIdentityRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindUserIdentityRequest.kt new file mode 100644 index 00000000..084bffc8 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindUserIdentityRequest.kt @@ -0,0 +1,27 @@ +package org.yapp.apis.auth.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import org.yapp.apis.auth.dto.response.UserIdResponse +import java.util.* + +@Schema( + name = "FindUserIdentityRequest", + description = "Request DTO to retrieve user identity information using userId" +) +data class FindUserIdentityRequest( + @Schema( + description = "User ID (UUID format)", + example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0" + ) + @field:NotBlank(message = "userId must not be blank") + val userId: UUID? = null +) { + fun validUserId(): UUID = userId!! + + companion object { + fun from(userIdResponse: UserIdResponse): FindUserIdentityRequest { + return FindUserIdentityRequest(userIdResponse.userId) + } + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenGenerateRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenGenerateRequest.kt new file mode 100644 index 00000000..69a59bc9 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenGenerateRequest.kt @@ -0,0 +1,41 @@ +package org.yapp.apis.auth.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import java.util.* + +@Schema( + name = "TokenGenerateRequest", + description = "DTO containing information required to save the generated refresh token" +) +data class TokenGenerateRequest( + @field:NotNull(message = "userId must not be null") + @Schema(description = "User ID", example = "f6b7d490-1b1a-4b9f-8e8e-27f8e3a5dafa") + val userId: UUID? = null, + + @field:NotBlank(message = "refreshToken must not be blank") + @Schema(description = "Generated refresh token", example = "eyJhbGciOiJIUzI1NiIsInR...") + val refreshToken: String? = null, + + @field:NotNull(message = "expiration must not be null") + @Schema(description = "Refresh token expiration time (in seconds)", example = "2592000") + val expiration: Long? = null +) { + fun validUserId() = userId!! + fun validRefreshToken() = refreshToken!! + fun validExpiration() = expiration!! + + companion object { + fun of(userId: UUID, refreshToken: String, expiration: Long): TokenGenerateRequest { + require(refreshToken.isNotBlank()) { "Refresh token must not be blank." } + require(expiration > 0) { "Expiration must be greater than 0." } + + return TokenGenerateRequest( + userId = userId, + refreshToken = refreshToken, + expiration = expiration + ) + } + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/RefreshTokenResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/RefreshTokenResponse.kt new file mode 100644 index 00000000..7ff44874 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/RefreshTokenResponse.kt @@ -0,0 +1,19 @@ +package org.yapp.apis.auth.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import org.yapp.domain.token.RefreshToken.Token + +@Schema( + name = "RefreshTokenResponse", + description = "Response DTO containing the issued refresh token" +) +data class RefreshTokenResponse( + @Schema(description = "The refresh token string", example = "eyJhbGciOiJIUz...") + val refreshToken: String +) { + companion object { + fun from(token: Token): RefreshTokenResponse { + return RefreshTokenResponse(refreshToken = token.value) + } + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserIdResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserIdResponse.kt new file mode 100644 index 00000000..dcec8a39 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserIdResponse.kt @@ -0,0 +1,20 @@ +package org.yapp.apis.auth.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import org.yapp.domain.token.RefreshToken.UserId +import java.util.* + +@Schema( + name = "UserIdResponse", + description = "Response DTO that contains the user ID extracted from a refresh token" +) +data class UserIdResponse( + @Schema(description = "User ID", example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0") + val userId: UUID +) { + companion object { + fun from(userId: UserId): UserIdResponse { + return UserIdResponse(userId.value) + } + } +} From 6ae0a6ecf6cf2bb7921ce933469d3b9add67b9bc Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 02:23:25 +0900 Subject: [PATCH 29/49] =?UTF-8?q?[BOOK-140]=20refactor:=20domain=20-=20?= =?UTF-8?q?=EB=AC=B4=EC=A1=B0=EA=B1=B4=20true=EC=9D=B8=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/org/yapp/domain/user/vo/UserIdentityVO.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentityVO.kt b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentityVO.kt index dff16312..4f563d7f 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentityVO.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentityVO.kt @@ -8,14 +8,8 @@ data class UserIdentityVO( val id: UUID, val role: Role ) { - init { - requireNotNull(id) { "User ID must not be null." } - requireNotNull(role) { "User role must not be null." } - } - companion object { - fun newInstance(user: User?): UserIdentityVO { - requireNotNull(user) { "User must not be null." } + fun newInstance(user: User): UserIdentityVO { return UserIdentityVO( id = user.id, role = user.role From b876e9d6ecf58f26d0c63da593c21b9cf8c7fb99 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 02:23:43 +0900 Subject: [PATCH 30/49] =?UTF-8?q?[BOOK-140]=20refactor:=20requestDTO?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/org/yapp/apis/auth/service/UserAuthService.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt index c7ce356c..a475589b 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt @@ -2,6 +2,7 @@ package org.yapp.apis.auth.service import org.springframework.stereotype.Service import org.yapp.apis.auth.dto.request.FindOrCreateUserRequest +import org.yapp.apis.auth.dto.request.FindUserIdentityRequest import org.yapp.apis.auth.dto.response.CreateUserResponse import org.yapp.apis.auth.dto.response.UserAuthInfoResponse import org.yapp.apis.auth.dto.response.UserProfileResponse @@ -26,8 +27,8 @@ class UserAuthService( } } - fun findUserIdentityByUserId(userId: UUID): UserAuthInfoResponse { - val userIdentity = userDomainService.findUserIdentityById(userId) + fun findUserIdentityByUserId(findUserIdentityRequest: FindUserIdentityRequest): UserAuthInfoResponse { + val userIdentity = userDomainService.findUserIdentityById(findUserIdentityRequest.validUserId()) return UserAuthInfoResponse.from(userIdentity) } From 0250e3a2d4c1fd0ee45a17955673f3f17c49f4d9 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 02:24:53 +0900 Subject: [PATCH 31/49] =?UTF-8?q?[BOOK-140]=20refactor:=20apis=20-=20redis?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=9D=B8=EC=A6=9D=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yapp/apis/auth/helper/AuthTokenHelper.kt | 33 +++++++++++----- .../yapp/apis/auth/service/TokenService.kt | 38 ++++++++++--------- .../org/yapp/apis/auth/usecase/AuthUseCase.kt | 28 +++++--------- .../domain/token/TokenDomainRedisService.kt | 37 ++++++++++++------ 4 files changed, 78 insertions(+), 58 deletions(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/helper/AuthTokenHelper.kt b/apis/src/main/kotlin/org/yapp/apis/auth/helper/AuthTokenHelper.kt index 3e5c2727..8c3d96f4 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/helper/AuthTokenHelper.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/helper/AuthTokenHelper.kt @@ -1,32 +1,45 @@ package org.yapp.apis.auth.helper +import org.yapp.apis.auth.dto.request.DeleteTokenRequest +import org.yapp.apis.auth.dto.request.GenerateTokenPairRequest +import org.yapp.apis.auth.dto.request.TokenGenerateRequest +import org.yapp.apis.auth.dto.request.TokenRefreshRequest import org.yapp.apis.auth.dto.response.TokenPairResponse +import org.yapp.apis.auth.dto.response.UserIdResponse import org.yapp.apis.auth.service.TokenService import org.yapp.gateway.jwt.JwtTokenService import org.yapp.globalutils.annotation.Helper -import org.yapp.globalutils.auth.Role -import java.util.* @Helper class AuthTokenHelper( private val tokenService: TokenService, private val jwtTokenService: JwtTokenService ) { - fun generateTokenPair(userId: UUID, role: Role): TokenPairResponse { + fun generateTokenPair(generateTokenPairRequest: GenerateTokenPairRequest): TokenPairResponse { + val userId = generateTokenPairRequest.validUserId() + val role = generateTokenPairRequest.validRole() + val accessToken = jwtTokenService.generateAccessToken(userId, role) val refreshToken = jwtTokenService.generateRefreshToken(userId) val expiration = jwtTokenService.getRefreshTokenExpiration() - tokenService.save(userId, refreshToken, expiration) - return TokenPairResponse.of(accessToken, refreshToken) + val refreshTokenResponse = tokenService.saveRefreshToken( + TokenGenerateRequest.of(userId, refreshToken, expiration) + ) + + return TokenPairResponse.of(accessToken, refreshTokenResponse.refreshToken) + } + + fun validateAndGetUserIdFromRefreshToken(tokenRefreshRequest: TokenRefreshRequest): UserIdResponse { + tokenService.validateRefreshToken(tokenRefreshRequest.validRefreshToken()) + return tokenService.getUserIdByToken(tokenRefreshRequest) } - fun validateAndGetUserIdFromRefreshToken(refreshToken: String): UUID { - tokenService.validateRefreshTokenByTokenOrThrow(refreshToken) - return tokenService.getUserIdFromToken(refreshToken) + fun deleteTokenForReissue(tokenRefreshRequest: TokenRefreshRequest) { + tokenService.deleteRefreshTokenByToken(tokenRefreshRequest.validRefreshToken()) } - fun deleteToken(token: String) { - tokenService.deleteByToken(token) + fun deleteTokenForSignOut(deleteTokenRequest: DeleteTokenRequest) { + tokenService.deleteRefreshTokenByToken(deleteTokenRequest.validRefreshToken()) } } diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/service/TokenService.kt b/apis/src/main/kotlin/org/yapp/apis/auth/service/TokenService.kt index 10e5f45d..7249f8a3 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/service/TokenService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/service/TokenService.kt @@ -1,39 +1,41 @@ package org.yapp.apis.auth.service import org.springframework.stereotype.Service -import org.yapp.apis.auth.exception.AuthErrorCode -import org.yapp.apis.auth.exception.AuthException +import org.yapp.apis.auth.dto.request.TokenGenerateRequest +import org.yapp.apis.auth.dto.request.TokenRefreshRequest +import org.yapp.apis.auth.dto.response.RefreshTokenResponse +import org.yapp.apis.auth.dto.response.UserIdResponse import org.yapp.domain.token.TokenDomainRedisService -import org.yapp.domain.token.RefreshToken import java.util.* @Service class TokenService( private val tokenDomainRedisService: TokenDomainRedisService, ) { - - fun deleteByToken(token: String) { + fun deleteRefreshTokenByToken(token: String) { tokenDomainRedisService.deleteRefreshTokenByToken(token) } - fun save(userId: UUID, refreshToken: String, expiration: Long) { - tokenDomainRedisService.saveRefreshToken(userId, refreshToken, expiration) + fun saveRefreshToken(tokenGenerateRequest: TokenGenerateRequest): RefreshTokenResponse { + val token = tokenDomainRedisService.saveRefreshToken( + tokenGenerateRequest.validUserId(), + tokenGenerateRequest.validRefreshToken(), + tokenGenerateRequest.validExpiration() + ) + return RefreshTokenResponse.from(token) } - fun getRefreshTokenByUserId(userId: UUID): RefreshToken { - return tokenDomainRedisService.getRefreshTokenByUserId(userId) - ?: throw AuthException(AuthErrorCode.REFRESH_TOKEN_NOT_FOUND) + fun getRefreshTokenByUserId(userId: UUID): RefreshTokenResponse { + val token = tokenDomainRedisService.getRefreshTokenByUserId(userId) + return RefreshTokenResponse.from(token) } - fun validateRefreshTokenByTokenOrThrow(refreshToken: String) { - val exists = tokenDomainRedisService.validateRefreshTokenByToken(refreshToken) - if (!exists) { - throw AuthException(AuthErrorCode.REFRESH_TOKEN_NOT_FOUND) - } + fun validateRefreshToken(refreshToken: String) { + tokenDomainRedisService.validateRefreshTokenByToken(refreshToken) } - fun getUserIdFromToken(refreshToken: String): UUID { - return tokenDomainRedisService.getUserIdFromToken(refreshToken) - ?: throw AuthException(AuthErrorCode.REFRESH_TOKEN_NOT_FOUND) + fun getUserIdByToken(tokenRefreshRequest: TokenRefreshRequest): UserIdResponse { + val userId = tokenDomainRedisService.getUserIdByToken(tokenRefreshRequest.validRefreshToken()) + return UserIdResponse.from(userId) } } diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt b/apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt index 1db1dea6..35e2bd08 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt @@ -1,10 +1,7 @@ package org.yapp.apis.auth.usecase import org.springframework.transaction.annotation.Transactional -import org.yapp.apis.auth.dto.request.FindOrCreateUserRequest -import org.yapp.apis.auth.dto.request.GenerateTokenPairRequest -import org.yapp.apis.auth.dto.request.SocialLoginRequest -import org.yapp.apis.auth.dto.request.TokenRefreshRequest +import org.yapp.apis.auth.dto.request.* import org.yapp.apis.auth.dto.response.TokenPairResponse import org.yapp.apis.auth.dto.response.UserProfileResponse import org.yapp.apis.auth.helper.AuthTokenHelper @@ -32,31 +29,26 @@ class AuthUseCase( val createUserResponse = userAuthService.findOrCreateUser(findOrCreateUserRequest) val generateTokenPairRequest = GenerateTokenPairRequest.from(createUserResponse) - return authTokenHelper.generateTokenPair( - generateTokenPairRequest.validUserId(), - generateTokenPairRequest.validRole() - ) + return authTokenHelper.generateTokenPair(generateTokenPairRequest) } @Transactional fun reissueTokenPair(tokenRefreshRequest: TokenRefreshRequest): TokenPairResponse { - val refreshToken = tokenRefreshRequest.validRefreshToken() - val userId = authTokenHelper.validateAndGetUserIdFromRefreshToken(refreshToken) - authTokenHelper.deleteToken(refreshToken) + val userIdResponse = authTokenHelper.validateAndGetUserIdFromRefreshToken(tokenRefreshRequest) + authTokenHelper.deleteTokenForReissue(tokenRefreshRequest) - val userAuthInfoResponse = userAuthService.findUserIdentityByUserId(userId) + val findUserIdentityRequest = FindUserIdentityRequest.from(userIdResponse) + val userAuthInfoResponse = userAuthService.findUserIdentityByUserId(findUserIdentityRequest) val generateTokenPairRequest = GenerateTokenPairRequest.from(userAuthInfoResponse) - return authTokenHelper.generateTokenPair( - generateTokenPairRequest.validUserId(), - generateTokenPairRequest.validRole() - ) + return authTokenHelper.generateTokenPair(generateTokenPairRequest) } @Transactional fun signOut(userId: UUID) { - val refreshToken = tokenService.getRefreshTokenByUserId(userId) - authTokenHelper.deleteToken(refreshToken.token) + val refreshTokenResponse = tokenService.getRefreshTokenByUserId(userId) + val deleteTokenRequest = DeleteTokenRequest.from(refreshTokenResponse) + authTokenHelper.deleteTokenForSignOut(deleteTokenRequest) } fun getUserProfile(userId: UUID): UserProfileResponse { diff --git a/domain/src/main/kotlin/org/yapp/domain/token/TokenDomainRedisService.kt b/domain/src/main/kotlin/org/yapp/domain/token/TokenDomainRedisService.kt index 0be24998..723d9e27 100644 --- a/domain/src/main/kotlin/org/yapp/domain/token/TokenDomainRedisService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/token/TokenDomainRedisService.kt @@ -1,39 +1,52 @@ package org.yapp.domain.token +import org.yapp.domain.token.RefreshToken.Token +import org.yapp.domain.token.RefreshToken.UserId +import org.yapp.domain.token.exception.TokenErrorCode +import org.yapp.domain.token.exception.TokenNotFoundException import org.yapp.globalutils.annotation.DomainService import java.time.LocalDateTime -import java.util.UUID +import java.util.* @DomainService class TokenDomainRedisService( private val refreshTokenRepository: RefreshTokenRepository ) { - fun saveRefreshToken(userId: UUID, refreshToken: String, expiration: Long) { - val expiresAt = LocalDateTime.now().plusSeconds(expiration) + fun saveRefreshToken(userId: UUID, refreshToken: String, expiration: Long): Token { + val now = LocalDateTime.now() val token = RefreshToken.create( token = refreshToken, userId = userId, - expiresAt = expiresAt, - createdAt = LocalDateTime.now() + expiresAt = now.plusSeconds(expiration), + createdAt = now ) - refreshTokenRepository.save(token) + val savedRefreshToken = refreshTokenRepository.save(token) + return savedRefreshToken.token } fun deleteRefreshTokenByToken(token: String) { refreshTokenRepository.deleteByToken(token) } - fun validateRefreshTokenByToken(refreshToken: String): Boolean { + fun validateRefreshTokenByToken(refreshToken: String) { val storedToken = refreshTokenRepository.findByToken(refreshToken) - return storedToken != null && storedToken.expiresAt.isAfter(LocalDateTime.now()) + ?: throw TokenNotFoundException(TokenErrorCode.TOKEN_NOT_FOUND) + + if (storedToken.isExpired()) { + throw TokenNotFoundException(TokenErrorCode.EXPIRED_REFRESH_TOKEN) + } } - fun getUserIdFromToken(refreshToken: String): UUID? { + fun getUserIdByToken(refreshToken: String): UserId { val storedToken = refreshTokenRepository.findByToken(refreshToken) - return storedToken?.userId + ?: throw TokenNotFoundException(TokenErrorCode.TOKEN_NOT_FOUND) + return storedToken.userId } - fun getRefreshTokenByUserId(userId: UUID): RefreshToken? { - return refreshTokenRepository.findByUserId(userId) + fun getRefreshTokenByUserId(userId: UUID): Token { + val refreshToken = refreshTokenRepository.findByUserId(userId) + ?: throw TokenNotFoundException(TokenErrorCode.TOKEN_NOT_FOUND) + + return refreshToken.token } } From be840766cf79bbb5d556640566959ede5dfb95a5 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 02:44:39 +0900 Subject: [PATCH 32/49] =?UTF-8?q?[BOOK-140]=20refactor:=20apis,=20infra=20?= =?UTF-8?q?-=20=EA=B0=92=20=EA=B8=B0=EB=B0=98=20=EB=B9=84=EA=B5=90?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=B4=20=EA=B0=9D=EC=B2=B4=EB=93=A4?= =?UTF-8?q?=EC=9D=84=20VO(Value=20Object)=EB=A1=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ID, EMAIL, PROVIDERID는 동일한 값을 가지면 같은 객체임 --- .../main/kotlin/org/yapp/domain/user/User.kt | 50 ++++++++++++++----- .../org/yapp/infra/user/entity/UserEntity.kt | 12 ++--- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/domain/src/main/kotlin/org/yapp/domain/user/User.kt b/domain/src/main/kotlin/org/yapp/domain/user/User.kt index 3fac8f9a..24491732 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/User.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/User.kt @@ -20,12 +20,12 @@ import java.util.* * @property deletedAt The timestamp when the user was soft-deleted, or null if the user is not deleted. */ data class User private constructor( - val id: UUID, - val email: String, + val id: Id, + val email: Email, val nickname: String, val profileImageUrl: String?, val providerType: ProviderType, - val providerId: String, + val providerId: ProviderId, val role: Role, val createdAt: LocalDateTime, val updatedAt: LocalDateTime, @@ -52,12 +52,12 @@ data class User private constructor( deletedAt: LocalDateTime? = null ): User { return User( - id = UuidGenerator.create(), - email = email, + id = Id.newInstance(UuidGenerator.create()), + email = Email.newInstance(email), nickname = nickname, profileImageUrl = profileImageUrl, providerType = providerType, - providerId = providerId, + providerId = ProviderId.newInstance(providerId), role = Role.USER, createdAt = createdAt, updatedAt = updatedAt, @@ -78,12 +78,12 @@ data class User private constructor( deletedAt: LocalDateTime? = null ): User { return User( - id = UuidGenerator.create(), - email = email, + id = Id.newInstance(UuidGenerator.create()), + email = Email.newInstance(email), nickname = nickname, profileImageUrl = profileImageUrl, providerType = providerType, - providerId = providerId, + providerId = ProviderId.newInstance(providerId), role = role, createdAt = createdAt, updatedAt = updatedAt, @@ -92,12 +92,12 @@ data class User private constructor( } fun reconstruct( - id: UUID, - email: String, + id: Id, + email: Email, nickname: String, profileImageUrl: String?, providerType: ProviderType, - providerId: String, + providerId: ProviderId, role: Role, createdAt: LocalDateTime, updatedAt: LocalDateTime, @@ -118,5 +118,31 @@ data class User private constructor( } } + @JvmInline + value class Id(val value: UUID) { + companion object { fun newInstance(value: UUID) = Id(value) } + } + + @JvmInline + value class Email(val value: String) { + companion object { + private val EMAIL_REGEX = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$".toRegex() + fun newInstance(value: String): Email { + require(value.matches(EMAIL_REGEX)) { "올바른 이메일 형식이 아닙니다." } + return Email(value) + } + } + } + + @JvmInline + value class ProviderId(val value: String) { + companion object { + fun newInstance(value: String): ProviderId { + require(value.isNotBlank()) { "Provider ID는 필수입니다." } + return ProviderId(value) + } + } + } + private fun isDeleted(): Boolean = deletedAt != null } diff --git a/infra/src/main/kotlin/org/yapp/infra/user/entity/UserEntity.kt b/infra/src/main/kotlin/org/yapp/infra/user/entity/UserEntity.kt index e0cdd9ec..d8e06180 100644 --- a/infra/src/main/kotlin/org/yapp/infra/user/entity/UserEntity.kt +++ b/infra/src/main/kotlin/org/yapp/infra/user/entity/UserEntity.kt @@ -52,12 +52,12 @@ class UserEntity private constructor( protected set fun toDomain(): User = User.reconstruct( - id = id, - email = email, + id = User.Id.newInstance(this.id), + email = User.Email.newInstance(this.email), nickname = nickname, profileImageUrl = profileImageUrl, providerType = providerType, - providerId = providerId, + providerId = User.ProviderId.newInstance(this.providerId), role = role, createdAt = createdAt, updatedAt = updatedAt, @@ -66,12 +66,12 @@ class UserEntity private constructor( companion object { fun fromDomain(user: User): UserEntity = UserEntity( - id = user.id, - email = user.email, + id = user.id.value, + email = user.email.value, nickname = user.nickname, profileImageUrl = user.profileImageUrl, providerType = user.providerType, - providerId = user.providerId, + providerId = user.providerId.value, role = user.role ).apply { this.createdAt = user.createdAt From 681db36cea02c493e43646799e61c6534be8ef7d Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 02:44:58 +0900 Subject: [PATCH 33/49] =?UTF-8?q?[BOOK-140]=20refactor:=20infra=20-=20this?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/yapp/infra/external/redis/entity/RefreshToken.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infra/src/main/kotlin/org/yapp/infra/external/redis/entity/RefreshToken.kt b/infra/src/main/kotlin/org/yapp/infra/external/redis/entity/RefreshToken.kt index dd226d77..3fea9c13 100644 --- a/infra/src/main/kotlin/org/yapp/infra/external/redis/entity/RefreshToken.kt +++ b/infra/src/main/kotlin/org/yapp/infra/external/redis/entity/RefreshToken.kt @@ -23,9 +23,9 @@ class RefreshTokenEntity private constructor( val createdAt: LocalDateTime ) { fun toDomain(): RefreshToken = RefreshToken.reconstruct( - id = RefreshToken.Id.newInstance(id), - token = RefreshToken.Token.newInstance(token), - userId = RefreshToken.UserId.newInstance(userId), + id = RefreshToken.Id.newInstance(this.id), + token = RefreshToken.Token.newInstance(this.token), + userId = RefreshToken.UserId.newInstance(this.userId), expiresAt = expiresAt, createdAt = createdAt ) From 563117d7c1cd93e712a42f518f3b3e9f093ad5b7 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 02:45:50 +0900 Subject: [PATCH 34/49] =?UTF-8?q?[BOOK-140]=20refactor:=20apis,=20domain?= =?UTF-8?q?=20-=20vo=20=EB=A7=A4=ED=95=91=EC=9C=BC=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EB=B3=80=EB=8F=99=EC=82=AC=ED=95=AD=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 --- .../org/yapp/apis/auth/dto/response/CreateUserResponse.kt | 2 +- .../org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt | 2 +- .../org/yapp/apis/auth/dto/response/UserProfileResponse.kt | 4 ++-- .../kotlin/org/yapp/apis/auth/service/UserAuthService.kt | 2 +- .../main/kotlin/org/yapp/domain/user/vo/UserIdentityVO.kt | 3 +-- .../main/kotlin/org/yapp/domain/user/vo/UserProfileVO.kt | 6 ++---- 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt index d2c6afdf..a984ba39 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt @@ -25,7 +25,7 @@ data class CreateUserResponse private constructor( companion object { fun from(identity: UserIdentityVO): CreateUserResponse { return CreateUserResponse( - id = identity.id, + id = identity.id.value, role = identity.role ) } diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt index c92b3a6c..6bdefaf1 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt @@ -25,7 +25,7 @@ data class UserAuthInfoResponse private constructor( companion object { fun from(identity: UserIdentityVO): UserAuthInfoResponse { return UserAuthInfoResponse( - id = identity.id, + id = identity.id.value, role = identity.role ) } diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt index ec2dca8c..dc26ce8d 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt @@ -38,8 +38,8 @@ data class UserProfileResponse( companion object { fun from(userProfileVO: UserProfileVO): UserProfileResponse { return UserProfileResponse( - id = userProfileVO.id, - email = userProfileVO.email, + id = userProfileVO.id.value, + email = userProfileVO.email.value, nickname = userProfileVO.nickname, provider = userProfileVO.provider ) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt index a475589b..9b77a7f0 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt @@ -43,7 +43,7 @@ class UserAuthService( findOrCreateUserRequest.validProviderId() )?.let { deletedUserIdentity -> return CreateUserResponse.from( - userDomainService.restoreDeletedUser(deletedUserIdentity.id) + userDomainService.restoreDeletedUser(deletedUserIdentity.id.value) ) } diff --git a/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentityVO.kt b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentityVO.kt index 4f563d7f..a586202c 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentityVO.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentityVO.kt @@ -2,10 +2,9 @@ package org.yapp.domain.user.vo import org.yapp.domain.user.User import org.yapp.globalutils.auth.Role -import java.util.UUID data class UserIdentityVO( - val id: UUID, + val id: User.Id, val role: Role ) { companion object { diff --git a/domain/src/main/kotlin/org/yapp/domain/user/vo/UserProfileVO.kt b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserProfileVO.kt index 28c5f1f2..97aca94c 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/vo/UserProfileVO.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserProfileVO.kt @@ -2,16 +2,14 @@ package org.yapp.domain.user.vo import org.yapp.domain.user.ProviderType import org.yapp.domain.user.User -import java.util.UUID data class UserProfileVO private constructor( - val id: UUID, - val email: String, + val id: User.Id, + val email: User.Email, val nickname: String, val provider: ProviderType ) { init { - require(email.isNotBlank()) { "email은 비어 있을 수 없습니다." } require(nickname.isNotBlank()) {"nickname은 비어 있을 수 없습니다."} require(provider.name.isNotBlank()) { "providerType은 비어 있을 수 없습니다." } } From b300a525e7b60c261c6adab03c197c361fd88d1e Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 02:52:56 +0900 Subject: [PATCH 35/49] =?UTF-8?q?[BOOK-140]=20refactor:=20domain,=20infra?= =?UTF-8?q?=20-=20=EA=B0=92=20=EA=B8=B0=EB=B0=98=20=EB=B9=84=EA=B5=90?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=B4=20=EA=B0=9D=EC=B2=B4=EB=93=A4?= =?UTF-8?q?=EC=9D=84=20VO(Value=20Object)=EB=A1=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/kotlin/org/yapp/domain/book/Book.kt | 16 +++++++++++++--- .../org/yapp/infra/book/entity/BookEntity.kt | 4 ++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/domain/src/main/kotlin/org/yapp/domain/book/Book.kt b/domain/src/main/kotlin/org/yapp/domain/book/Book.kt index d22acef9..9129b208 100644 --- a/domain/src/main/kotlin/org/yapp/domain/book/Book.kt +++ b/domain/src/main/kotlin/org/yapp/domain/book/Book.kt @@ -6,7 +6,7 @@ import java.time.LocalDateTime // Import LocalDateTime * Represents a book in the domain model. */ data class Book private constructor( - val isbn: String, + val isbn: Isbn, val title: String, val author: String, val publisher: String, @@ -31,7 +31,7 @@ data class Book private constructor( ): Book { val now = LocalDateTime.now() return Book( - isbn = isbn, + isbn = Isbn.newInstance(isbn), title = title, author = author, publisher = publisher, @@ -45,7 +45,7 @@ data class Book private constructor( } fun reconstruct( - isbn: String, + isbn: Isbn, title: String, author: String, publisher: String, @@ -70,4 +70,14 @@ data class Book private constructor( ) } } + + @JvmInline + value class Isbn(val value: String) { + companion object { + fun newInstance(value: String): Isbn { + require(value.matches(Regex("^(\\d{10}|\\d{13})$"))) { "ISBN은 10자리 또는 13자리 숫자여야 합니다." } + return Isbn(value) + } + } + } } diff --git a/infra/src/main/kotlin/org/yapp/infra/book/entity/BookEntity.kt b/infra/src/main/kotlin/org/yapp/infra/book/entity/BookEntity.kt index a0b3b4b6..a7a6ef88 100644 --- a/infra/src/main/kotlin/org/yapp/infra/book/entity/BookEntity.kt +++ b/infra/src/main/kotlin/org/yapp/infra/book/entity/BookEntity.kt @@ -53,7 +53,7 @@ class BookEntity private constructor( protected set fun toDomain(): Book = Book.reconstruct( - isbn = isbn, + isbn = Book.Isbn.newInstance(this.isbn), title = title, author = author, publisher = publisher, @@ -67,7 +67,7 @@ class BookEntity private constructor( companion object { fun fromDomain(book: Book): BookEntity = BookEntity( - isbn = book.isbn, + isbn = book.isbn.value, title = book.title, author = book.author, publisher = book.publisher, From 914d72b240762310ae20fc877e8681c4562ace86 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 02:53:43 +0900 Subject: [PATCH 36/49] =?UTF-8?q?[BOOK-140]=20refactor:=20apis,=20domain?= =?UTF-8?q?=20-=20=EA=B0=92=20=EA=B0=9D=EC=B2=B4=20=EB=A7=A4=ED=95=91?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EB=B3=80=EB=8F=99?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yapp/apis/book/dto/response/BookCreateResponse.kt | 6 +++--- .../kotlin/org/yapp/domain/book/BookDomainService.kt | 10 +++++----- .../yapp/domain/book/vo/{BookVO.kt => BookInfoVO.kt} | 9 ++++----- 3 files changed, 12 insertions(+), 13 deletions(-) rename domain/src/main/kotlin/org/yapp/domain/book/vo/{BookVO.kt => BookInfoVO.kt} (81%) diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookCreateResponse.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookCreateResponse.kt index 2da8a59e..2cb3a052 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookCreateResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookCreateResponse.kt @@ -1,6 +1,6 @@ package org.yapp.apis.book.dto.response -import org.yapp.domain.book.vo.BookVO +import org.yapp.domain.book.vo.BookInfoVO data class BookCreateResponse private constructor( val isbn: String, @@ -10,9 +10,9 @@ data class BookCreateResponse private constructor( val coverImageUrl: String ) { companion object { - fun from(bookVO: BookVO): BookCreateResponse { + fun from(bookVO: BookInfoVO): BookCreateResponse { return BookCreateResponse( - isbn = bookVO.isbn, + isbn = bookVO.isbn.value, title = bookVO.title, author = bookVO.author, publisher = bookVO.publisher, diff --git a/domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt index b8e22cde..d60b7c4b 100644 --- a/domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt @@ -1,14 +1,14 @@ package org.yapp.domain.book -import org.yapp.domain.book.vo.BookVO +import org.yapp.domain.book.vo.BookInfoVO import org.yapp.globalutils.annotation.DomainService @DomainService class BookDomainService( private val bookRepository: BookRepository ) { - fun findByIsbn(isbn: String): BookVO? { - return bookRepository.findByIsbn(isbn)?.let { BookVO.newInstance(it) } + fun findByIsbn(isbn: String): BookInfoVO? { + return bookRepository.findByIsbn(isbn)?.let { BookInfoVO.newInstance(it) } } fun save( @@ -19,7 +19,7 @@ class BookDomainService( coverImageUrl: String, publicationYear: Int? = null, description: String? = null - ): BookVO { + ): BookInfoVO { val book = bookRepository.findByIsbn(isbn) ?: Book.create( isbn = isbn, title = title, @@ -31,6 +31,6 @@ class BookDomainService( ) val savedBook = bookRepository.save(book) - return BookVO.newInstance(savedBook) + return BookInfoVO.newInstance(savedBook) } } diff --git a/domain/src/main/kotlin/org/yapp/domain/book/vo/BookVO.kt b/domain/src/main/kotlin/org/yapp/domain/book/vo/BookInfoVO.kt similarity index 81% rename from domain/src/main/kotlin/org/yapp/domain/book/vo/BookVO.kt rename to domain/src/main/kotlin/org/yapp/domain/book/vo/BookInfoVO.kt index 2262ecd5..34de4174 100644 --- a/domain/src/main/kotlin/org/yapp/domain/book/vo/BookVO.kt +++ b/domain/src/main/kotlin/org/yapp/domain/book/vo/BookInfoVO.kt @@ -2,8 +2,8 @@ package org.yapp.domain.book.vo import org.yapp.domain.book.Book -data class BookVO private constructor( - val isbn: String, +data class BookInfoVO private constructor( + val isbn: Book.Isbn, val title: String, val author: String, val publisher: String, @@ -12,7 +12,6 @@ data class BookVO private constructor( val description: String? ) { init { - require(isbn.isNotBlank()) { "ISBN은 비어 있을 수 없습니다." } require(title.isNotBlank()) { "제목은 비어 있을 수 없습니다." } require(author.isNotBlank()) { "저자는 비어 있을 수 없습니다." } publicationYear?.let { require(it > 0) { "출판 연도는 0보다 커야 합니다." } } @@ -21,8 +20,8 @@ data class BookVO private constructor( companion object { fun newInstance( book: Book - ): BookVO { - return BookVO( + ): BookInfoVO { + return BookInfoVO( book.isbn, book.title, book.author, From 7c2c699a3a14d5d46aed756e972aece65700890a Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 03:08:51 +0900 Subject: [PATCH 37/49] =?UTF-8?q?[BOOK-140]=20feat:=20domain=20-=20?= =?UTF-8?q?=EC=B1=85=20=EA=B4=80=EB=A0=A8=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yapp/domain/book/exception/BookErrorCode.kt | 17 +++++++++++++++++ .../book/exception/BookNotFoundException.kt | 8 ++++++++ 2 files changed, 25 insertions(+) create mode 100644 domain/src/main/kotlin/org/yapp/domain/book/exception/BookErrorCode.kt create mode 100644 domain/src/main/kotlin/org/yapp/domain/book/exception/BookNotFoundException.kt diff --git a/domain/src/main/kotlin/org/yapp/domain/book/exception/BookErrorCode.kt b/domain/src/main/kotlin/org/yapp/domain/book/exception/BookErrorCode.kt new file mode 100644 index 00000000..b5302256 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/book/exception/BookErrorCode.kt @@ -0,0 +1,17 @@ +package org.yapp.domain.book.exception + +import org.springframework.http.HttpStatus +import org.yapp.globalutils.exception.BaseErrorCode + +enum class BookErrorCode( + private val status: HttpStatus, + private val code: String, + private val message: String +) : BaseErrorCode { + BOOK_NOT_FOUND(HttpStatus.NOT_FOUND, "BOOK_001", "도서 정보를 찾을 수 없습니다."), + BOOK_ALREADY_EXISTS(HttpStatus.CONFLICT, "BOOK_002", "이미 존재하는 도서입니다."); + + override fun getHttpStatus(): HttpStatus = status + override fun getCode(): String = code + override fun getMessage(): String = message +} diff --git a/domain/src/main/kotlin/org/yapp/domain/book/exception/BookNotFoundException.kt b/domain/src/main/kotlin/org/yapp/domain/book/exception/BookNotFoundException.kt new file mode 100644 index 00000000..1eccb311 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/book/exception/BookNotFoundException.kt @@ -0,0 +1,8 @@ +package org.yapp.domain.book.exception + +import org.yapp.globalutils.exception.CommonException + +class BookNotFoundException( + errorCode: BookErrorCode, + message: String? = null +) : CommonException(errorCode, message) From 187496fc5f0df92983a07f1c230b7adbdfbbba10 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 03:08:58 +0900 Subject: [PATCH 38/49] =?UTF-8?q?[BOOK-140]=20feat:=20domain=20-=20?= =?UTF-8?q?=EC=B1=85=20=EA=B4=80=EB=A0=A8=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/book/exception/BookAlreadyExistsException.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 domain/src/main/kotlin/org/yapp/domain/book/exception/BookAlreadyExistsException.kt diff --git a/domain/src/main/kotlin/org/yapp/domain/book/exception/BookAlreadyExistsException.kt b/domain/src/main/kotlin/org/yapp/domain/book/exception/BookAlreadyExistsException.kt new file mode 100644 index 00000000..79b6c89d --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/book/exception/BookAlreadyExistsException.kt @@ -0,0 +1,8 @@ +package org.yapp.domain.book.exception + +import org.yapp.globalutils.exception.CommonException + +class BookAlreadyExistsException( + errorCode: BookErrorCode, + message: String? = null +) : CommonException(errorCode, message) From eeec87e76b52fe733039f39fb932be9d9d1e8bba Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 03:09:32 +0900 Subject: [PATCH 39/49] =?UTF-8?q?[BOOK-140]=20refactor:=20domain,=20infra?= =?UTF-8?q?=20-=20=EC=83=9D=EC=84=B1=EB=90=9C=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=98=88=EC=99=B8=EB=A5=BC=20=EC=9D=B4=EC=9A=A9?= =?UTF-8?q?=ED=95=B4=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/kotlin/org/yapp/domain/book/Book.kt | 2 -- .../org/yapp/domain/book/BookDomainService.kt | 14 +++++++++++--- .../kotlin/org/yapp/domain/book/BookRepository.kt | 3 ++- .../infra/book/repository/JpaBookRepository.kt | 2 -- .../book/repository/impl/BookRepositoryImpl.kt | 8 ++++++-- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/domain/src/main/kotlin/org/yapp/domain/book/Book.kt b/domain/src/main/kotlin/org/yapp/domain/book/Book.kt index 9129b208..0010201e 100644 --- a/domain/src/main/kotlin/org/yapp/domain/book/Book.kt +++ b/domain/src/main/kotlin/org/yapp/domain/book/Book.kt @@ -17,8 +17,6 @@ data class Book private constructor( val updatedAt: LocalDateTime, val deletedAt: LocalDateTime? = null ) { - - companion object { fun create( isbn: String, diff --git a/domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt index d60b7c4b..2a440efb 100644 --- a/domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt @@ -1,5 +1,8 @@ package org.yapp.domain.book +import org.yapp.domain.book.exception.BookAlreadyExistsException +import org.yapp.domain.book.exception.BookErrorCode +import org.yapp.domain.book.exception.BookNotFoundException import org.yapp.domain.book.vo.BookInfoVO import org.yapp.globalutils.annotation.DomainService @@ -7,8 +10,9 @@ import org.yapp.globalutils.annotation.DomainService class BookDomainService( private val bookRepository: BookRepository ) { - fun findByIsbn(isbn: String): BookInfoVO? { - return bookRepository.findByIsbn(isbn)?.let { BookInfoVO.newInstance(it) } + fun findByIsbn(isbn: String): BookInfoVO { + val book = bookRepository.findById(isbn) ?: throw BookNotFoundException(BookErrorCode.BOOK_NOT_FOUND) + return BookInfoVO.newInstance(book) } fun save( @@ -20,7 +24,11 @@ class BookDomainService( publicationYear: Int? = null, description: String? = null ): BookInfoVO { - val book = bookRepository.findByIsbn(isbn) ?: Book.create( + if (bookRepository.existsById(isbn)) { + throw BookAlreadyExistsException(BookErrorCode.BOOK_ALREADY_EXISTS) + } + + val book = Book.create( isbn = isbn, title = title, author = author, diff --git a/domain/src/main/kotlin/org/yapp/domain/book/BookRepository.kt b/domain/src/main/kotlin/org/yapp/domain/book/BookRepository.kt index 0d531165..6da40714 100644 --- a/domain/src/main/kotlin/org/yapp/domain/book/BookRepository.kt +++ b/domain/src/main/kotlin/org/yapp/domain/book/BookRepository.kt @@ -1,6 +1,7 @@ package org.yapp.domain.book interface BookRepository { - fun findByIsbn(isbn: String): Book? + fun findById(isbn: String): Book? + fun existsById(isbn: String): Boolean fun save(book: Book): Book } diff --git a/infra/src/main/kotlin/org/yapp/infra/book/repository/JpaBookRepository.kt b/infra/src/main/kotlin/org/yapp/infra/book/repository/JpaBookRepository.kt index 827a7a3c..f8fe92ad 100644 --- a/infra/src/main/kotlin/org/yapp/infra/book/repository/JpaBookRepository.kt +++ b/infra/src/main/kotlin/org/yapp/infra/book/repository/JpaBookRepository.kt @@ -7,6 +7,4 @@ import org.yapp.infra.book.entity.BookEntity * JPA repository for BookEntity. */ interface JpaBookRepository : JpaRepository { - - fun findByIsbn(isbn: String): BookEntity? } diff --git a/infra/src/main/kotlin/org/yapp/infra/book/repository/impl/BookRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/book/repository/impl/BookRepositoryImpl.kt index 7062e205..bcd00030 100644 --- a/infra/src/main/kotlin/org/yapp/infra/book/repository/impl/BookRepositoryImpl.kt +++ b/infra/src/main/kotlin/org/yapp/infra/book/repository/impl/BookRepositoryImpl.kt @@ -1,5 +1,6 @@ package org.yapp.infra.book.repository.impl +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Repository import org.yapp.domain.book.Book import org.yapp.domain.book.BookRepository @@ -10,9 +11,12 @@ import org.yapp.infra.book.repository.JpaBookRepository class BookRepositoryImpl( private val jpaBookRepository: JpaBookRepository ) : BookRepository { + override fun findById(isbn: String): Book? { + return jpaBookRepository.findByIdOrNull(isbn)?.toDomain() + } - override fun findByIsbn(isbn: String): Book? { - return jpaBookRepository.findByIsbn(isbn)?.toDomain() + override fun existsById(isbn: String): Boolean { + return jpaBookRepository.existsById(isbn) } override fun save(book: Book): Book { From 0da7e95fc65bfd66e8860913c32dcf4c1f3f26cd Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 03:11:04 +0900 Subject: [PATCH 40/49] =?UTF-8?q?[BOOK-140]=20chore:=20apis,=20domain=20-?= =?UTF-8?q?=20vo=20=EC=9D=B4=EB=A6=84=20=EA=B5=AC=EC=B2=B4=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yapp/apis/book/dto/response/UserBookResponse.kt | 4 ++-- .../org/yapp/apis/book/service/UserBookService.kt | 8 +++----- .../org/yapp/domain/userbook/UserBookDomainService.kt | 11 +++++------ .../userbook/vo/{UserBookVO.kt => UserBookInfoVO.kt} | 6 +++--- 4 files changed, 13 insertions(+), 16 deletions(-) rename domain/src/main/kotlin/org/yapp/domain/userbook/vo/{UserBookVO.kt => UserBookInfoVO.kt} (93%) diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookResponse.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookResponse.kt index c12d8e6c..6ba74edb 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookResponse.kt @@ -1,7 +1,7 @@ package org.yapp.apis.book.dto.response import org.yapp.domain.userbook.BookStatus -import org.yapp.domain.userbook.vo.UserBookVO +import org.yapp.domain.userbook.vo.UserBookInfoVO import java.time.format.DateTimeFormatter import java.util.UUID @@ -20,7 +20,7 @@ data class UserBookResponse private constructor( companion object { fun from( - userBook: UserBookVO, + userBook: UserBookInfoVO, ): UserBookResponse { return UserBookResponse( userBookId = userBook.id, diff --git a/apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt b/apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt index df9690db..0655bc6d 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt @@ -3,10 +3,8 @@ package org.yapp.apis.book.service import org.springframework.stereotype.Service import org.yapp.apis.book.dto.request.UpsertUserBookRequest import org.yapp.apis.book.dto.response.UserBookResponse -import org.yapp.domain.book.Book import org.yapp.domain.userbook.UserBookDomainService -import org.yapp.domain.userbook.BookStatus -import org.yapp.domain.userbook.vo.UserBookVO +import org.yapp.domain.userbook.vo.UserBookInfoVO import java.util.* @Service @@ -27,8 +25,8 @@ class UserBookService( ) fun findAllUserBooks(userId: UUID): List { - val userBooks: List = userBookDomainService.findAllUserBooks(userId) - return userBooks.map { userBook: UserBookVO -> + val userBooks: List = userBookDomainService.findAllUserBooks(userId) + return userBooks.map { userBook: UserBookInfoVO -> UserBookResponse.from(userBook) } } diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt index 78db78c0..d7313614 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt @@ -1,7 +1,6 @@ package org.yapp.domain.userbook -import org.yapp.domain.book.Book -import org.yapp.domain.userbook.vo.UserBookVO +import org.yapp.domain.userbook.vo.UserBookInfoVO import org.yapp.globalutils.annotation.DomainService import java.util.UUID @@ -17,7 +16,7 @@ class UserBookDomainService( bookPublisher: String, bookCoverImageUrl: String, status: BookStatus - ): UserBookVO { + ): UserBookInfoVO { val userBook = userBookRepository.findByUserIdAndBookIsbn(userId, bookIsbn) ?.apply { updateStatus(status) } ?: UserBook.create( @@ -30,11 +29,11 @@ class UserBookDomainService( ) val savedUserBook = userBookRepository.save(userBook) - return UserBookVO.newInstance(savedUserBook) + return UserBookInfoVO.newInstance(savedUserBook) } - fun findAllUserBooks(userId: UUID): List { + fun findAllUserBooks(userId: UUID): List { return userBookRepository.findAllByUserId(userId) - .map(UserBookVO::newInstance) + .map(UserBookInfoVO::newInstance) } } diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookVO.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt similarity index 93% rename from domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookVO.kt rename to domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt index 99d6fa0a..bf25ee1a 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookVO.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt @@ -5,7 +5,7 @@ import org.yapp.domain.userbook.UserBook import java.time.LocalDateTime import java.util.UUID -data class UserBookVO private constructor( +data class UserBookInfoVO private constructor( val id: UUID, val userId: UUID, val bookIsbn: String, @@ -31,8 +31,8 @@ data class UserBookVO private constructor( companion object { fun newInstance( userBook: UserBook, - ): UserBookVO { - return UserBookVO( + ): UserBookInfoVO { + return UserBookInfoVO( id = userBook.id, userId = userBook.userId, bookIsbn = userBook.bookIsbn, From 0126cbfb2fe16c2acaf36f466a603e25e3e8b77d Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 03:12:46 +0900 Subject: [PATCH 41/49] =?UTF-8?q?[BOOK-140]=20chore:=20infra=20-=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yapp/infra/{user => userbook}/entity/UserBookEntity.kt | 2 +- .../{user => userbook}/repository/JpaUserBookRepository.kt | 4 ++-- .../repository/impl/UserBookRepositoryImpl.kt | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) rename infra/src/main/kotlin/org/yapp/infra/{user => userbook}/entity/UserBookEntity.kt (98%) rename infra/src/main/kotlin/org/yapp/infra/{user => userbook}/repository/JpaUserBookRepository.kt (75%) rename infra/src/main/kotlin/org/yapp/infra/{user => userbook}/repository/impl/UserBookRepositoryImpl.kt (82%) diff --git a/infra/src/main/kotlin/org/yapp/infra/user/entity/UserBookEntity.kt b/infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt similarity index 98% rename from infra/src/main/kotlin/org/yapp/infra/user/entity/UserBookEntity.kt rename to infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt index 84a02046..02f7d0cd 100644 --- a/infra/src/main/kotlin/org/yapp/infra/user/entity/UserBookEntity.kt +++ b/infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt @@ -1,4 +1,4 @@ -package org.yapp.infra.user.entity +package org.yapp.infra.userbook.entity import jakarta.persistence.* import org.hibernate.annotations.JdbcTypeCode diff --git a/infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserBookRepository.kt b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookRepository.kt similarity index 75% rename from infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserBookRepository.kt rename to infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookRepository.kt index 9214f00c..f90fc89e 100644 --- a/infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserBookRepository.kt +++ b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookRepository.kt @@ -1,7 +1,7 @@ -package org.yapp.infra.user.repository +package org.yapp.infra.userbook.repository import org.springframework.data.jpa.repository.JpaRepository -import org.yapp.infra.user.entity.UserBookEntity +import org.yapp.infra.userbook.entity.UserBookEntity import java.util.* interface JpaUserBookRepository : JpaRepository { diff --git a/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserBookRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/UserBookRepositoryImpl.kt similarity index 82% rename from infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserBookRepositoryImpl.kt rename to infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/UserBookRepositoryImpl.kt index 6483af81..1fd01a2d 100644 --- a/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserBookRepositoryImpl.kt +++ b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/UserBookRepositoryImpl.kt @@ -1,10 +1,10 @@ -package org.yapp.infra.user.repository.impl +package org.yapp.infra.userbook.repository.impl import org.springframework.stereotype.Repository import org.yapp.domain.userbook.UserBookRepository import org.yapp.domain.userbook.UserBook -import org.yapp.infra.user.entity.UserBookEntity -import org.yapp.infra.user.repository.JpaUserBookRepository +import org.yapp.infra.userbook.entity.UserBookEntity +import org.yapp.infra.userbook.repository.JpaUserBookRepository import java.util.* @Repository From 902106e93bff9f89079ff09ef05e248c8af9249b Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 03:22:30 +0900 Subject: [PATCH 42/49] =?UTF-8?q?[BOOK-140]=20refactor:=20domain,=20infra?= =?UTF-8?q?=20-=20=EA=B0=92=20=EA=B8=B0=EB=B0=98=20=EB=B9=84=EA=B5=90?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=B4=20=EA=B0=9D=EC=B2=B4=EB=93=A4?= =?UTF-8?q?=EC=9D=84=20VO(Value=20Object)=EB=A1=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/yapp/domain/userbook/UserBook.kt | 64 ++++++++++++------- .../infra/userbook/entity/UserBookEntity.kt | 48 +++++++------- 2 files changed, 66 insertions(+), 46 deletions(-) diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt index 5c8d00e7..7a7ab57d 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt @@ -1,15 +1,13 @@ -package org.yapp.domain.userbook // UserBook 도메인 모델의 올바른 패키지 +package org.yapp.domain.userbook -import org.yapp.domain.book.Book import org.yapp.globalutils.util.UuidGenerator import java.time.LocalDateTime import java.util.* - data class UserBook private constructor( - val id: UUID, - val userId: UUID, - val bookIsbn: String, + val id: Id, + val userId: UserId, + val bookIsbn: BookIsbn, val coverImageUrl: String, val publisher: String, val title: String, @@ -23,12 +21,11 @@ data class UserBook private constructor( return this.copy(status = newStatus, updatedAt = LocalDateTime.now()) } - companion object { fun create( userId: UUID, - coverImageUrl: String, bookIsbn: String, + coverImageUrl: String, publisher: String, title: String, author: String, @@ -36,46 +33,69 @@ data class UserBook private constructor( ): UserBook { val now = LocalDateTime.now() return UserBook( - id = UuidGenerator.create(), + id = Id.newInstance(UuidGenerator.create()), + userId = UserId.newInstance(userId), + bookIsbn = BookIsbn.newInstance(bookIsbn), coverImageUrl = coverImageUrl, publisher = publisher, title = title, author = author, - userId = userId, - bookIsbn = bookIsbn, status = initialStatus, createdAt = now, - updatedAt = now, - deletedAt = null, + updatedAt = now ) } fun reconstruct( - id: UUID, - userId: UUID, - bookIsbn: String, - status: BookStatus, + id: Id, + userId: UserId, + bookIsbn: BookIsbn, coverImageUrl: String, + publisher: String, title: String, author: String, - publisher: String, + status: BookStatus, createdAt: LocalDateTime, updatedAt: LocalDateTime, - deletedAt: LocalDateTime? = null, + deletedAt: LocalDateTime? ): UserBook { return UserBook( id = id, userId = userId, bookIsbn = bookIsbn, - status = status, coverImageUrl = coverImageUrl, + publisher = publisher, title = title, author = author, - publisher = publisher, + status = status, createdAt = createdAt, updatedAt = updatedAt, - deletedAt = deletedAt, + deletedAt = deletedAt ) } } + + @JvmInline + value class Id(val value: UUID) { + companion object { + fun newInstance(value: UUID) = Id(value) + } + } + + @JvmInline + value class UserId(val value: UUID) { + companion object { + fun newInstance(value: UUID) = UserId(value) + } + } + + @JvmInline + value class BookIsbn(val value: String) { + companion object { + fun newInstance(value: String): BookIsbn { + require(value.matches(Regex("^(\\d{10}|\\d{13})$"))) { "ISBN은 10자리 또는 13자리 숫자여야 합니다." } + return BookIsbn(value) + } + } + } } diff --git a/infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt b/infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt index 02f7d0cd..0a144c27 100644 --- a/infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt +++ b/infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt @@ -3,8 +3,8 @@ package org.yapp.infra.userbook.entity import jakarta.persistence.* import org.hibernate.annotations.JdbcTypeCode import org.hibernate.annotations.SQLDelete -import org.yapp.domain.userbook.BookStatus import org.yapp.domain.common.BaseTimeEntity +import org.yapp.domain.userbook.BookStatus import org.yapp.domain.userbook.UserBook import java.sql.Types import java.util.* @@ -54,38 +54,30 @@ class UserBookEntity( protected set fun toDomain(): UserBook = UserBook.reconstruct( - id = id, - userId = userId, - bookIsbn = bookIsbn, - status = status, - coverImageUrl = coverImageUrl, - publisher = publisher, - title = title, - author = author, - createdAt = createdAt, - updatedAt = updatedAt, - deletedAt = deletedAt + id = UserBook.Id.newInstance(this.id), + userId = UserBook.UserId.newInstance(this.userId), + bookIsbn = UserBook.BookIsbn.newInstance(this.bookIsbn), + coverImageUrl = this.coverImageUrl, + publisher = this.publisher, + title = this.title, + author = this.author, + status = this.status, + createdAt = this.createdAt, + updatedAt = this.updatedAt, + deletedAt = this.deletedAt ) - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is UserBookEntity) return false - return id == other.id - } - - override fun hashCode(): Int = id.hashCode() - companion object { fun fromDomain(userBook: UserBook): UserBookEntity { return UserBookEntity( - id = userBook.id, - userId = userBook.userId, - bookIsbn = userBook.bookIsbn, - status = userBook.status, + id = userBook.id.value, + userId = userBook.userId.value, + bookIsbn = userBook.bookIsbn.value, coverImageUrl = userBook.coverImageUrl, publisher = userBook.publisher, title = userBook.title, author = userBook.author, + status = userBook.status, ).apply { this.createdAt = userBook.createdAt this.updatedAt = userBook.updatedAt @@ -93,4 +85,12 @@ class UserBookEntity( } } } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is UserBookEntity) return false + return id == other.id + } + + override fun hashCode(): Int = id.hashCode() } From c8421a80637185fbe4b4251986cc1bb309d84dc5 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 03:22:47 +0900 Subject: [PATCH 43/49] =?UTF-8?q?[BOOK-140]=20refactor:=20domain,=20apis?= =?UTF-8?q?=20-=20VO=20=EB=9E=98=ED=95=91=EC=9C=BC=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EB=B3=80=EB=8F=99=EC=82=AC=ED=95=AD=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 --- .../org/yapp/apis/book/dto/response/UserBookResponse.kt | 7 +++---- .../kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt | 8 +++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookResponse.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookResponse.kt index 6ba74edb..2819998f 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookResponse.kt @@ -17,15 +17,14 @@ data class UserBookResponse private constructor( val createdAt: String, val updatedAt: String, ) { - companion object { fun from( userBook: UserBookInfoVO, ): UserBookResponse { return UserBookResponse( - userBookId = userBook.id, - userId = userBook.userId, - bookIsbn = userBook.bookIsbn, + userBookId = userBook.id.value, + userId = userBook.userId.value, + bookIsbn = userBook.bookIsbn.value, bookTitle = userBook.title, bookAuthor = userBook.author, status = userBook.status, diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt index bf25ee1a..0c464781 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt @@ -3,12 +3,11 @@ package org.yapp.domain.userbook.vo import org.yapp.domain.userbook.BookStatus import org.yapp.domain.userbook.UserBook import java.time.LocalDateTime -import java.util.UUID data class UserBookInfoVO private constructor( - val id: UUID, - val userId: UUID, - val bookIsbn: String, + val id: UserBook.Id, + val userId: UserBook.UserId, + val bookIsbn: UserBook.BookIsbn, val coverImageUrl: String, val publisher: String, val title: String, @@ -18,7 +17,6 @@ data class UserBookInfoVO private constructor( val updatedAt: LocalDateTime ) { init { - require(bookIsbn.isNotBlank()) { "도서 ISBN은 비어 있을 수 없습니다." } require(coverImageUrl.isNotBlank()) { "표지 이미지 URL은 비어 있을 수 없습니다." } require(publisher.isNotBlank()) { "출판사는 비어 있을 수 없습니다." } require(title.isNotBlank()) { "도서 제목은 비어 있을 수 없습니다." } From 66602802041b813b6fbaaff08b4d30dfd0d502ed Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 03:31:23 +0900 Subject: [PATCH 44/49] =?UTF-8?q?[BOOK-140]=20refactor:=20apis,=20domain,?= =?UTF-8?q?=20infra=20-=20=EC=BD=94=EB=93=9C=EB=A0=88=EB=B9=97=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/yapp/apis/book/service/BookManagementService.kt | 9 --------- .../main/kotlin/org/yapp/domain/book/BookRepository.kt | 4 ++-- .../infra/book/repository/impl/BookRepositoryImpl.kt | 8 ++++---- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/book/service/BookManagementService.kt b/apis/src/main/kotlin/org/yapp/apis/book/service/BookManagementService.kt index 97ce18dc..a8071004 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/service/BookManagementService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/service/BookManagementService.kt @@ -13,15 +13,6 @@ class BookManagementService( val isbn = request.validIsbn() val bookVO = bookDomainService.findByIsbn(isbn) - ?: bookDomainService.save( - isbn = isbn, - title = request.validTitle(), - author = request.validAuthor(), - publisher = request.validPublisher(), - coverImageUrl = request.coverImageUrl, - publicationYear = request.publicationYear, - description = request.description - ) return BookCreateResponse.from(bookVO) } diff --git a/domain/src/main/kotlin/org/yapp/domain/book/BookRepository.kt b/domain/src/main/kotlin/org/yapp/domain/book/BookRepository.kt index 6da40714..b89d132d 100644 --- a/domain/src/main/kotlin/org/yapp/domain/book/BookRepository.kt +++ b/domain/src/main/kotlin/org/yapp/domain/book/BookRepository.kt @@ -1,7 +1,7 @@ package org.yapp.domain.book interface BookRepository { - fun findById(isbn: String): Book? - fun existsById(isbn: String): Boolean + fun findById(id: String): Book? + fun existsById(id: String): Boolean fun save(book: Book): Book } diff --git a/infra/src/main/kotlin/org/yapp/infra/book/repository/impl/BookRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/book/repository/impl/BookRepositoryImpl.kt index bcd00030..f40f7b86 100644 --- a/infra/src/main/kotlin/org/yapp/infra/book/repository/impl/BookRepositoryImpl.kt +++ b/infra/src/main/kotlin/org/yapp/infra/book/repository/impl/BookRepositoryImpl.kt @@ -11,12 +11,12 @@ import org.yapp.infra.book.repository.JpaBookRepository class BookRepositoryImpl( private val jpaBookRepository: JpaBookRepository ) : BookRepository { - override fun findById(isbn: String): Book? { - return jpaBookRepository.findByIdOrNull(isbn)?.toDomain() + override fun findById(id: String): Book? { + return jpaBookRepository.findByIdOrNull(id)?.toDomain() } - override fun existsById(isbn: String): Boolean { - return jpaBookRepository.existsById(isbn) + override fun existsById(id: String): Boolean { + return jpaBookRepository.existsById(id) } override fun save(book: Book): Book { From 0d428b4ce07f274640ab36d33ab4aed709ebd9d2 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 03:35:36 +0900 Subject: [PATCH 45/49] =?UTF-8?q?[BOOK-140]=20refactor:=20apis=20-=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A0=88=EB=B9=97=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/yapp/apis/auth/dto/request/FindUserIdentityRequest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindUserIdentityRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindUserIdentityRequest.kt index 084bffc8..e52041c3 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindUserIdentityRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindUserIdentityRequest.kt @@ -1,7 +1,7 @@ package org.yapp.apis.auth.dto.request import io.swagger.v3.oas.annotations.media.Schema -import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull import org.yapp.apis.auth.dto.response.UserIdResponse import java.util.* @@ -14,7 +14,7 @@ data class FindUserIdentityRequest( description = "User ID (UUID format)", example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0" ) - @field:NotBlank(message = "userId must not be blank") + @field:NotNull(message = "userId must not be null") val userId: UUID? = null ) { fun validUserId(): UUID = userId!! From 332e7a1e5b7080e26bc5bdc925800e3c89c8fef7 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 03:45:13 +0900 Subject: [PATCH 46/49] =?UTF-8?q?[BOOK-140]=20chore:=20domain=20-=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9C=84=EC=B9=98=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- domain/src/main/kotlin/org/yapp/domain/user/User.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/domain/src/main/kotlin/org/yapp/domain/user/User.kt b/domain/src/main/kotlin/org/yapp/domain/user/User.kt index 24491732..448d8e23 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/User.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/User.kt @@ -118,9 +118,13 @@ data class User private constructor( } } + private fun isDeleted(): Boolean = deletedAt != null + @JvmInline value class Id(val value: UUID) { - companion object { fun newInstance(value: UUID) = Id(value) } + companion object { + fun newInstance(value: UUID) = Id(value) + } } @JvmInline @@ -143,6 +147,4 @@ data class User private constructor( } } } - - private fun isDeleted(): Boolean = deletedAt != null } From 1a2844fb038f565ed45b70bf1cb9b8d3cb23a2db Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Wed, 16 Jul 2025 03:49:23 +0900 Subject: [PATCH 47/49] =?UTF-8?q?[BOOK-140]=20chore:=20apis=20-=20?= =?UTF-8?q?=EA=B0=80=EB=8F=85=EC=84=B1=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EA=B0=9C=ED=96=89=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/org/yapp/apis/book/controller/BookController.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apis/src/main/kotlin/org/yapp/apis/book/controller/BookController.kt b/apis/src/main/kotlin/org/yapp/apis/book/controller/BookController.kt index ff9f9ca4..827d3912 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/controller/BookController.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/controller/BookController.kt @@ -25,7 +25,9 @@ class BookController( ) : BookControllerApi { @GetMapping("/search") - override fun searchBooks(@Valid @ModelAttribute request: BookSearchRequest): ResponseEntity { + override fun searchBooks( + @Valid @ModelAttribute request: BookSearchRequest + ): ResponseEntity { val response = bookUseCase.searchBooks(request) return ResponseEntity.ok(response) } From ffe911e46bc695d81a7c84dfa5c70ae5468c69bf Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Thu, 17 Jul 2025 19:16:28 +0900 Subject: [PATCH 48/49] =?UTF-8?q?[BOOK-140]=20feat:=20global-utils=20-=20e?= =?UTF-8?q?mail,=20isbn=20=EC=A0=84=EC=97=AD=20validator=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/yapp/globalutils/validator/EmailValidator.kt | 9 +++++++++ .../org/yapp/globalutils/validator/IsbnValidator.kt | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 global-utils/src/main/kotlin/org/yapp/globalutils/validator/EmailValidator.kt create mode 100644 global-utils/src/main/kotlin/org/yapp/globalutils/validator/IsbnValidator.kt diff --git a/global-utils/src/main/kotlin/org/yapp/globalutils/validator/EmailValidator.kt b/global-utils/src/main/kotlin/org/yapp/globalutils/validator/EmailValidator.kt new file mode 100644 index 00000000..3e45c025 --- /dev/null +++ b/global-utils/src/main/kotlin/org/yapp/globalutils/validator/EmailValidator.kt @@ -0,0 +1,9 @@ +package org.yapp.globalutils.validator + +object EmailValidator { + private val EMAIL_REGEX = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$".toRegex() + + fun isValidEmail(email: String): Boolean { + return email.matches(EMAIL_REGEX) + } +} diff --git a/global-utils/src/main/kotlin/org/yapp/globalutils/validator/IsbnValidator.kt b/global-utils/src/main/kotlin/org/yapp/globalutils/validator/IsbnValidator.kt new file mode 100644 index 00000000..aaaff82d --- /dev/null +++ b/global-utils/src/main/kotlin/org/yapp/globalutils/validator/IsbnValidator.kt @@ -0,0 +1,9 @@ +package org.yapp.globalutils.validator + +object IsbnValidator { + private val ISBN_REGEX = Regex("^(\\d{10}|\\d{13})$") + + fun isValidIsbn(isbn: String): Boolean { + return isbn.matches(ISBN_REGEX) + } +} From 75d80e83fd5f8659f3bbe3eec9e053cdc89169b9 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Thu, 17 Jul 2025 19:17:12 +0900 Subject: [PATCH 49/49] =?UTF-8?q?[BOOK-140]=20refactor:=20domain=20-=20ema?= =?UTF-8?q?il,=20isbn=20=EC=A0=84=EC=97=AD=20validator=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- domain/src/main/kotlin/org/yapp/domain/book/Book.kt | 5 +++-- domain/src/main/kotlin/org/yapp/domain/user/User.kt | 4 ++-- domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/domain/src/main/kotlin/org/yapp/domain/book/Book.kt b/domain/src/main/kotlin/org/yapp/domain/book/Book.kt index 0010201e..4a4251f3 100644 --- a/domain/src/main/kotlin/org/yapp/domain/book/Book.kt +++ b/domain/src/main/kotlin/org/yapp/domain/book/Book.kt @@ -1,6 +1,7 @@ package org.yapp.domain.book -import java.time.LocalDateTime // Import LocalDateTime +import org.yapp.globalutils.validator.IsbnValidator +import java.time.LocalDateTime /** * Represents a book in the domain model. @@ -73,7 +74,7 @@ data class Book private constructor( value class Isbn(val value: String) { companion object { fun newInstance(value: String): Isbn { - require(value.matches(Regex("^(\\d{10}|\\d{13})$"))) { "ISBN은 10자리 또는 13자리 숫자여야 합니다." } + require(IsbnValidator.isValidIsbn(value)) { "ISBN must be a 10 or 13-digit number." } return Isbn(value) } } diff --git a/domain/src/main/kotlin/org/yapp/domain/user/User.kt b/domain/src/main/kotlin/org/yapp/domain/user/User.kt index 448d8e23..b4989621 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/User.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/User.kt @@ -2,6 +2,7 @@ package org.yapp.domain.user import org.yapp.globalutils.auth.Role import org.yapp.globalutils.util.UuidGenerator +import org.yapp.globalutils.validator.EmailValidator import java.time.LocalDateTime import java.util.* @@ -130,9 +131,8 @@ data class User private constructor( @JvmInline value class Email(val value: String) { companion object { - private val EMAIL_REGEX = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$".toRegex() fun newInstance(value: String): Email { - require(value.matches(EMAIL_REGEX)) { "올바른 이메일 형식이 아닙니다." } + require(EmailValidator.isValidEmail(value)) { "This is not a valid email format." } return Email(value) } } diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt index 7a7ab57d..3a170206 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt @@ -1,6 +1,7 @@ package org.yapp.domain.userbook import org.yapp.globalutils.util.UuidGenerator +import org.yapp.globalutils.validator.IsbnValidator import java.time.LocalDateTime import java.util.* @@ -93,7 +94,7 @@ data class UserBook private constructor( value class BookIsbn(val value: String) { companion object { fun newInstance(value: String): BookIsbn { - require(value.matches(Regex("^(\\d{10}|\\d{13})$"))) { "ISBN은 10자리 또는 13자리 숫자여야 합니다." } + require(IsbnValidator.isValidIsbn(value)) { "ISBN must be a 10 or 13-digit number." } return BookIsbn(value) } }