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)) } 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( 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/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/FindOrCreateUserRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindOrCreateUserRequest.kt new file mode 100644 index 00000000..f523deef --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindOrCreateUserRequest.kt @@ -0,0 +1,71 @@ +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, + + @Schema( + description = "소셜 로그인 제공자", + example = "KAKAO" + ) + @field:NotBlank(message = "providerType은 필수입니다.") + val providerType: ProviderType? = null, + + @Schema( + description = "소셜 제공자에서 발급한 식별자", + example = "12345678901234567890" + ) + @field:NotBlank(message = "providerId는 필수입니다.") + 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/FindUserIdentityRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindUserIdentityRequest.kt new file mode 100644 index 00000000..e52041c3 --- /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.NotNull +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:NotNull(message = "userId must not be null") + 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/GenerateTokenPairRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt new file mode 100644 index 00000000..cda082b7 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt @@ -0,0 +1,48 @@ +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.CreateUserResponse +import org.yapp.apis.auth.dto.response.UserAuthInfoResponse +import org.yapp.globalutils.auth.Role +import java.util.UUID + +@Schema( + name = "GenerateTokenPairRequest", + description = "Request DTO to generate a new pair of access and refresh tokens" +) +data class GenerateTokenPairRequest private constructor( + @Schema( + description = "User ID", + example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0" + ) + @field:NotBlank(message = "userId must not be null") + val userId: UUID? = null, + + @Schema( + description = "User role", + example = "USER" + ) + @field:NotBlank(message = "role must not be null") + val role: Role? = null +) { + + fun validUserId(): UUID = userId!! + fun validRole(): Role = role!! + + 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/request/SocialLoginRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt index b47727ae..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 @@ -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 @@ -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/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/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 new file mode 100644 index 00000000..a984ba39 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/CreateUserResponse.kt @@ -0,0 +1,33 @@ +package org.yapp.apis.auth.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import org.yapp.domain.user.vo.UserIdentityVO +import org.yapp.globalutils.auth.Role +import java.util.UUID + +@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: UserIdentityVO): CreateUserResponse { + return CreateUserResponse( + id = identity.id.value, + role = identity.role + ) + } + } +} 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/UserAuthInfoResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt new file mode 100644 index 00000000..6bdefaf1 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserAuthInfoResponse.kt @@ -0,0 +1,33 @@ +package org.yapp.apis.auth.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import org.yapp.domain.user.vo.UserIdentityVO +import org.yapp.globalutils.auth.Role +import java.util.UUID + +@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: UserIdentityVO): UserAuthInfoResponse { + return UserAuthInfoResponse( + id = identity.id.value, + 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() + ) + } + } +} 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) + } + } +} 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..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 @@ -2,7 +2,8 @@ package org.yapp.apis.auth.dto.response import io.swagger.v3.oas.annotations.media.Schema import org.yapp.domain.user.ProviderType -import java.util.* +import org.yapp.domain.user.vo.UserProfileVO +import java.util.UUID @Schema( name = "UserProfileResponse", @@ -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(userProfileVO: UserProfileVO): UserProfileResponse { return UserProfileResponse( - id = id, - email = email, - nickname = nickname, - provider = provider + 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/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/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 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/service/UserAuthService.kt b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt index 6adea306..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 @@ -1,65 +1,70 @@ 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.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 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.UserIdentityVO 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(findUserIdentityRequest: FindUserIdentityRequest): UserAuthInfoResponse { + val userIdentity = userDomainService.findUserIdentityById(findUserIdentityRequest.validUserId()) + 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.value) + ) + } - 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): UserIdentityVO { + 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.createNewUser( + 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/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/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 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, 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..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,7 +1,7 @@ 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.* import org.yapp.apis.auth.dto.response.TokenPairResponse import org.yapp.apis.auth.dto.response.UserProfileResponse import org.yapp.apis.auth.helper.AuthTokenHelper @@ -20,34 +20,38 @@ 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) } @Transactional - fun reissueTokenPair(refreshToken: String): TokenPairResponse { - val userId = authTokenHelper.validateAndGetUserIdFromRefreshToken(refreshToken) - authTokenHelper.deleteToken(refreshToken) - val user = userAuthService.findUserById(userId) - return authTokenHelper.generateTokenPair(user.id, user.role) + fun reissueTokenPair(tokenRefreshRequest: TokenRefreshRequest): TokenPairResponse { + val userIdResponse = authTokenHelper.validateAndGetUserIdFromRefreshToken(tokenRefreshRequest) + authTokenHelper.deleteTokenForReissue(tokenRefreshRequest) + + val findUserIdentityRequest = FindUserIdentityRequest.from(userIdResponse) + val userAuthInfoResponse = userAuthService.findUserIdentityByUserId(findUserIdentityRequest) + val generateTokenPairRequest = GenerateTokenPairRequest.from(userAuthInfoResponse) + + 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 { - val user = userAuthService.findUserById(userId) - return UserProfileResponse.of( - id = user.id, - email = user.email, - nickname = user.nickname, - provider = user.providerType - ) + return userAuthService.findUserProfileByUserId(userId) } } 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) } 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/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..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 @@ -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 @@ -17,15 +17,14 @@ data class UserBookResponse private constructor( val createdAt: String, val updatedAt: String, ) { - companion object { fun from( - userBook: UserBookVO, + 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/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/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/apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt b/apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt index c5977d63..e7ff72f9 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 @@ -42,7 +42,6 @@ class BookUseCase( userAuthService.validateUserExists(userId) val bookDetailResponse = bookQueryService.getBookDetail(BookDetailRequest.of(request.validBookIsbn())) - val bookCreateResponse = bookManagementService.findOrCreateBook(BookCreateRequest.from(bookDetailResponse)) val upsertUserBookRequest = UpsertUserBookRequest.of( userId = userId, @@ -54,7 +53,6 @@ class BookUseCase( return userBookResponse } - fun getUserLibraryBooks(userId: UUID): List { userAuthService.validateUserExists(userId) 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..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,12 +1,13 @@ 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. */ data class Book private constructor( - val isbn: String, + val isbn: Isbn, val title: String, val author: String, val publisher: String, @@ -17,8 +18,6 @@ data class Book private constructor( val updatedAt: LocalDateTime, val deletedAt: LocalDateTime? = null ) { - - companion object { fun create( isbn: String, @@ -31,7 +30,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 +44,7 @@ data class Book private constructor( } fun reconstruct( - isbn: String, + isbn: Isbn, title: String, author: String, publisher: String, @@ -70,4 +69,14 @@ data class Book private constructor( ) } } + + @JvmInline + value class Isbn(val value: String) { + companion object { + fun newInstance(value: String): Isbn { + 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/book/BookDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt index b8e22cde..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,14 +1,18 @@ package org.yapp.domain.book -import org.yapp.domain.book.vo.BookVO +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 @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 { + val book = bookRepository.findById(isbn) ?: throw BookNotFoundException(BookErrorCode.BOOK_NOT_FOUND) + return BookInfoVO.newInstance(book) } fun save( @@ -19,8 +23,12 @@ class BookDomainService( coverImageUrl: String, publicationYear: Int? = null, description: String? = null - ): BookVO { - val book = bookRepository.findByIsbn(isbn) ?: Book.create( + ): BookInfoVO { + if (bookRepository.existsById(isbn)) { + throw BookAlreadyExistsException(BookErrorCode.BOOK_ALREADY_EXISTS) + } + + val book = Book.create( isbn = isbn, title = title, author = author, @@ -31,6 +39,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/BookRepository.kt b/domain/src/main/kotlin/org/yapp/domain/book/BookRepository.kt index 0d531165..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,6 +1,7 @@ package org.yapp.domain.book interface BookRepository { - fun findByIsbn(isbn: String): Book? + fun findById(id: String): Book? + fun existsById(id: String): Boolean fun save(book: Book): Book } 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) 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) 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, 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/domain/src/main/kotlin/org/yapp/domain/token/TokenDomainRedisService.kt b/domain/src/main/kotlin/org/yapp/domain/token/TokenDomainRedisService.kt index fe816f56..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,40 +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 } } 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) + 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..b4989621 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,8 @@ 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.* @@ -19,12 +21,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, @@ -41,6 +43,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 = Id.newInstance(UuidGenerator.create()), + email = Email.newInstance(email), + nickname = nickname, + profileImageUrl = profileImageUrl, + providerType = providerType, + providerId = ProviderId.newInstance(providerId), + role = Role.USER, + createdAt = createdAt, + updatedAt = updatedAt, + deletedAt = deletedAt + ) + } + + // 추후 다른 역할 부여 시 사용 + fun createWithRole( email: String, nickname: String, profileImageUrl: String?, @@ -52,12 +79,12 @@ data class User private constructor( deletedAt: LocalDateTime? = null ): User { return User( - id = UUID.randomUUID(), - 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, @@ -66,12 +93,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, @@ -93,4 +120,31 @@ 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) + } + } + + @JvmInline + value class Email(val value: String) { + companion object { + fun newInstance(value: String): Email { + require(EmailValidator.isValidEmail(value)) { "This is not a valid email format." } + 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) + } + } + } } 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..b4cc7728 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt @@ -1,8 +1,10 @@ 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.UserIdentityVO +import org.yapp.domain.user.vo.UserProfileVO import org.yapp.globalutils.annotation.DomainService -import org.yapp.globalutils.auth.Role import org.yapp.globalutils.util.TimeProvider import java.util.UUID @@ -11,44 +13,60 @@ class UserDomainService( private val userRepository: UserRepository, private val timeProvider: TimeProvider ) { + fun findUserProfileById(id: UUID): UserProfileVO { + val user = userRepository.findById(id) ?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND) + return UserProfileVO.newInstance(user) + } - fun findById(id: UUID): User? = - userRepository.findById(id) + fun findUserIdentityById(id: UUID): UserIdentityVO { + val user = userRepository.findById(id) ?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND) + return UserIdentityVO.newInstance(user) + } - fun findByEmail(email: String): User? = - userRepository.findByEmail(email) + fun findUserByProviderTypeAndProviderId(providerType: ProviderType, providerId: String): UserIdentityVO? { + return userRepository.findByProviderTypeAndProviderId(providerType, providerId) + ?.let { UserIdentityVO.newInstance(it) } + } - fun findByProviderTypeAndProviderId(providerType: ProviderType, providerId: String): User? = - userRepository.findByProviderTypeAndProviderId(providerType, providerId) + fun findUserByProviderTypeAndProviderIdIncludingDeleted(providerType: ProviderType, providerId: String): UserIdentityVO? { + return userRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType, providerId) + ?.let { UserIdentityVO.newInstance(it) } + } - fun findByProviderTypeAndProviderIdIncludingDeleted(providerType: ProviderType, providerId: String): User? = - userRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType, providerId) + fun existsActiveUserByIdAndDeletedAtIsNull(id: UUID): Boolean { + return userRepository.existsById(id) + } - fun existsActiveByEmail(email: String): Boolean = - findByEmail(email) != null + fun existsActiveUserByEmailAndDeletedAtIsNull(email: String): Boolean { + return userRepository.existsByEmail(email) + } - fun create(profile: SocialUserProfile): User { + fun createNewUser( + email: String, + nickname: String, + profileImageUrl: String?, + providerType: ProviderType, + providerId: String + ): UserIdentityVO { 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 UserIdentityVO.newInstance(savedUser) } - fun restoreDeletedUser(deletedUser: User): User = - save(deletedUser.restore()) - - fun save(user: User): User = - userRepository.save(user) + fun restoreDeletedUser(userId: UUID): UserIdentityVO { + 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 UserIdentityVO.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..114dc130 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.* /** @@ -8,9 +7,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? @@ -20,4 +17,5 @@ interface UserRepository { fun existsById(id: UUID): Boolean + fun existsByEmail(email: String): Boolean } 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..2005fdf0 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/user/exception/UserNotFoundException.kt @@ -0,0 +1,9 @@ +package org.yapp.domain.user.exception + +import org.yapp.globalutils.exception.CommonException + +class UserNotFoundException ( + errorCode: UserErrorCode, + message: String? = null +) : CommonException(errorCode, message) + 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/UserIdentityVO.kt b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentityVO.kt new file mode 100644 index 00000000..a586202c --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserIdentityVO.kt @@ -0,0 +1,18 @@ +package org.yapp.domain.user.vo + +import org.yapp.domain.user.User +import org.yapp.globalutils.auth.Role + +data class UserIdentityVO( + val id: User.Id, + val role: Role +) { + companion object { + fun newInstance(user: User): UserIdentityVO { + return UserIdentityVO( + id = user.id, + role = user.role + ) + } + } +} 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 new file mode 100644 index 00000000..97aca94c --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/user/vo/UserProfileVO.kt @@ -0,0 +1,29 @@ +package org.yapp.domain.user.vo + +import org.yapp.domain.user.ProviderType +import org.yapp.domain.user.User + +data class UserProfileVO private constructor( + val id: User.Id, + val email: User.Email, + val nickname: String, + val provider: ProviderType +) { + init { + require(nickname.isNotBlank()) {"nickname은 비어 있을 수 없습니다."} + require(provider.name.isNotBlank()) { "providerType은 비어 있을 수 없습니다." } + } + + companion object { + fun newInstance(user: User?): UserProfileVO { + requireNotNull(user) { "User는 null일 수 없습니다." } + + return UserProfileVO( + id = user.id, + email = user.email, + nickname = user.nickname, + provider = user.providerType + ) + } + } +} 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..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,15 +1,14 @@ -package org.yapp.domain.userbook // UserBook 도메인 모델의 올바른 패키지 +package org.yapp.domain.userbook -import org.yapp.domain.book.Book import org.yapp.globalutils.util.UuidGenerator +import org.yapp.globalutils.validator.IsbnValidator 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 +22,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 +34,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(IsbnValidator.isValidIsbn(value)) { "ISBN must be a 10 or 13-digit number." } + return BookIsbn(value) + } + } + } } 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 84% 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..0c464781 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 @@ -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 UserBookVO private constructor( - val id: UUID, - val userId: UUID, - val bookIsbn: String, +data class UserBookInfoVO private constructor( + 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 UserBookVO private constructor( val updatedAt: LocalDateTime ) { init { - require(bookIsbn.isNotBlank()) { "도서 ISBN은 비어 있을 수 없습니다." } require(coverImageUrl.isNotBlank()) { "표지 이미지 URL은 비어 있을 수 없습니다." } require(publisher.isNotBlank()) { "출판사는 비어 있을 수 없습니다." } require(title.isNotBlank()) { "도서 제목은 비어 있을 수 없습니다." } @@ -31,8 +29,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, 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 ) { 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) + } +} 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, 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..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 @@ -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(id: String): Book? { + return jpaBookRepository.findByIdOrNull(id)?.toDomain() + } - override fun findByIsbn(isbn: String): Book? { - return jpaBookRepository.findByIsbn(isbn)?.toDomain() + override fun existsById(id: String): Boolean { + return jpaBookRepository.existsById(id) } override fun save(book: Book): Book { 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..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 @@ -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(this.id), + token = RefreshToken.Token.newInstance(this.token), + userId = RefreshToken.UserId.newInstance(this.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 ) 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..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 @@ -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) @@ -50,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, @@ -64,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 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..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,7 +13,7 @@ interface JpaUserRepository : JpaRepository { fun findByProviderTypeAndProviderId(providerType: ProviderType, providerId: String): UserEntity? - fun findByEmail(email: String): UserEntity? + 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 8866d24b..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 @@ -17,28 +18,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.findByIdOrNull(id)?.toDomain() } override fun existsById(id: UUID): Boolean { return jpaUserRepository.existsById(id) } + override fun existsByEmail(email: String): Boolean { + return jpaUserRepository.existsByEmail(email) + } + override fun findByProviderTypeAndProviderIdIncludingDeleted( - providerType: ProviderType, providerId: String + providerType: ProviderType, + providerId: String ): User? { return jpaUserRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType, providerId)?.toDomain() } 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 78% 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..0a144c27 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,10 +1,10 @@ -package org.yapp.infra.user.entity +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() } 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