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 5a40bfbf..9fccdf41 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 @@ -15,12 +15,14 @@ import org.springframework.web.bind.annotation.RequestBody 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.globalutils.annotation.DisableSwaggerSecurity import org.yapp.globalutils.exception.ErrorResponse import java.util.* @Tag(name = "Authentication", description = "인증 관련 API") interface AuthControllerApi { + @DisableSwaggerSecurity @Operation( summary = "소셜 로그인", description = "카카오 또는 애플 계정으로 로그인합니다. 사용자가 존재하지 않으면 자동으로 회원가입됩니다." @@ -47,6 +49,7 @@ interface AuthControllerApi { @PostMapping("/signin") fun signIn(@RequestBody @Valid request: SocialLoginRequest): ResponseEntity + @DisableSwaggerSecurity @Operation( summary = "토큰 갱신", description = "리프레시 토큰을 사용하여 액세스 토큰을 갱신합니다. 새로운 액세스 토큰과 리프레시 토큰을 반환합니다." 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 index 4ea5c339..51e86d32 100644 --- 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 @@ -10,7 +10,7 @@ import org.yapp.apis.auth.dto.response.RefreshTokenResponse ) data class DeleteTokenRequest private constructor( @field:NotBlank(message = "Refresh token must not be blank.") - @Schema(description = "Refresh token to be deleted", example = "eyJhbGciOiJIUz...") + @field:Schema(description = "Refresh token to be deleted", example = "eyJhbGciOiJIUz...") val refreshToken: String? = null ) { fun validRefreshToken() = refreshToken!! diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt index ac8059ac..ed3eb354 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt @@ -12,14 +12,14 @@ import java.util.UUID description = "Request DTO to generate a new pair of access and refresh tokens" ) data class GenerateTokenPairRequest private constructor( - @Schema( + @field:Schema( description = "User ID", example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0" ) @field:NotNull(message = "userId must not be null") val userId: UUID? = null, - @Schema( + @field:Schema( description = "User role", example = "USER" ) 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 a85658a0..83e2c3ff 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt @@ -14,7 +14,7 @@ import org.yapp.domain.user.ProviderType description = "DTO for social login requests" ) data class SocialLoginRequest private constructor( - @Schema( + @field:Schema( description = "Type of social login provider", example = "KAKAO", required = true @@ -22,7 +22,7 @@ data class SocialLoginRequest private constructor( @field:NotBlank(message = "Provider type is required") val providerType: String? = null, - @Schema( + @field:Schema( description = "OAuth token issued by the social provider", example = "eyJ...", required = true @@ -30,7 +30,7 @@ data class SocialLoginRequest private constructor( @field:NotBlank(message = "OAuth token is required") val oauthToken: String? = null, - @Schema( + @field:Schema( description = "Authorization code used to issue Apple access/refresh tokens (required only for Apple login)", example = "c322a426...", required = false 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 index 37b71ede..f02d7b3b 100644 --- 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 @@ -11,15 +11,15 @@ import java.util.* ) data class TokenGenerateRequest private constructor( @field:NotNull(message = "userId must not be null") - @Schema(description = "User ID", example = "f6b7d490-1b1a-4b9f-8e8e-27f8e3a5dafa") + @field: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...") + @field: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") + @field:Schema(description = "Refresh token expiration time (in seconds)", example = "2592000") val expiration: Long? = null ) { fun validUserId() = userId!! 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 65fc200f..b631dae7 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,7 +8,7 @@ import jakarta.validation.constraints.NotBlank description = "DTO for requesting an access token using a refresh token" ) data class TokenRefreshRequest private constructor( - @Schema( + @field:Schema( description = "Valid refresh token issued during previous authentication", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", required = true diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/WithdrawStrategyRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/WithdrawStrategyRequest.kt index 0b0024ff..da1591df 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/WithdrawStrategyRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/WithdrawStrategyRequest.kt @@ -10,27 +10,27 @@ import java.util.* @Schema(description = "회원 탈퇴 처리 시 내부적으로 사용되는 요청 DTO") data class WithdrawStrategyRequest private constructor( @field:NotNull(message = "사용자 ID는 필수 값입니다.") - @Schema( + @field:Schema( description = "사용자 고유 ID", example = "123e4567-e89b-12d3-a456-426614174000" ) val userId: UUID, @field:NotNull(message = "소셜 로그인 제공자 타입은 필수 값입니다.") - @Schema( + @field:Schema( description = "소셜 로그인 제공자 타입", example = "KAKAO" ) val providerType: ProviderType, @field:NotBlank(message = "소셜 로그인 제공자로부터 발급받은 고유 ID는 필수 값입니다.") - @Schema( + @field:Schema( description = "소셜 로그인 제공자로부터 발급받은 고유 ID", example = "21412412412" ) val providerId: String, - @Schema( + @field:Schema( description = "Apple 로그인 시 발급받은 리프레시 토큰 (Apple 로그인 회원 탈퇴 시에만 필요)", example = "r_abc123def456ghi789jkl0mnopqrstu", required = false 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 81abe1d4..e5adb022 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 @@ -8,13 +8,13 @@ import io.swagger.v3.oas.annotations.media.Schema ) data class AuthResponse private constructor( - @Schema( + @field:Schema( description = "Access token for authorization", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ) val accessToken: String, - @Schema( + @field:Schema( description = "Refresh token used to obtain a new access token", example = "dGhpc2lzYXJlZnJlc2h0b2tlbg==" ) 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 index 7ff44874..34295fad 100644 --- 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 @@ -8,7 +8,7 @@ import org.yapp.domain.token.RefreshToken.Token description = "Response DTO containing the issued refresh token" ) data class RefreshTokenResponse( - @Schema(description = "The refresh token string", example = "eyJhbGciOiJIUz...") + @field:Schema(description = "The refresh token string", example = "eyJhbGciOiJIUz...") val refreshToken: String ) { companion object { diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/TokenPairResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/TokenPairResponse.kt index 0d00d0c3..ac340100 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/TokenPairResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/TokenPairResponse.kt @@ -8,13 +8,13 @@ import io.swagger.v3.oas.annotations.media.Schema ) data class TokenPairResponse private constructor( - @Schema( + @field:Schema( description = "Access token for user authorization", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ) val accessToken: String, - @Schema( + @field:Schema( description = "Refresh token to get new access token", example = "dGhpc2lzYXJlZnJlc2h0b2tlbg==" ) 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 index 8a8ebc9b..ab9970b0 100644 --- 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 @@ -8,32 +8,32 @@ import org.yapp.domain.user.ProviderType description = "Response DTO containing user information for newly registered users via social login" ) data class UserCreateInfoResponse private constructor( - @Schema( + @field:Schema( description = "사용자 이메일", example = "user@example.com", nullable = true ) val email: String?, - @Schema( + @field:Schema( description = "사용자 닉네임", example = "코딩하는곰", nullable = true ) val nickname: String?, - @Schema( + @field:Schema( description = "사용자 프로필 이미지 URL", example = "https://example.com/image.jpg", nullable = true ) val profileImageUrl: String? = null, - @Schema( + @field:Schema( description = "소셜 로그인 제공자", example = "KAKAO") val providerType: ProviderType, - @Schema( + @field:Schema( description = "소셜 제공자에서 발급한 식별자", example = "12345678901234567890" ) 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 index dcec8a39..2432fe93 100644 --- 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 @@ -9,7 +9,7 @@ import java.util.* 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") + @field:Schema(description = "User ID", example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0") val userId: UUID ) { companion object { diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/service/AppleAuthService.kt b/apis/src/main/kotlin/org/yapp/apis/auth/service/AppleAuthService.kt index 9b25fad6..dd18745b 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/service/AppleAuthService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/service/AppleAuthService.kt @@ -1,14 +1,12 @@ package org.yapp.apis.auth.service -import org.springframework.stereotype.Service -import org.springframework.validation.annotation.Validated import org.yapp.apis.auth.exception.AuthErrorCode import org.yapp.apis.auth.exception.AuthException import org.yapp.apis.auth.manager.AppleApiManager +import org.yapp.globalutils.annotation.ApplicationService import org.yapp.infra.external.oauth.apple.response.AppleTokenResponse -@Service -@Validated +@ApplicationService class AppleAuthService( private val appleApiManager: AppleApiManager, ) { diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/service/AuthTokenService.kt b/apis/src/main/kotlin/org/yapp/apis/auth/service/AuthTokenService.kt index d18e7afd..2f43cf26 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/service/AuthTokenService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/service/AuthTokenService.kt @@ -1,8 +1,6 @@ package org.yapp.apis.auth.service import jakarta.validation.Valid -import org.springframework.stereotype.Service -import org.springframework.validation.annotation.Validated import org.yapp.apis.auth.dto.request.DeleteTokenRequest import org.yapp.apis.auth.dto.request.GenerateTokenPairRequest import org.yapp.apis.auth.dto.request.TokenGenerateRequest @@ -10,9 +8,9 @@ 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.gateway.jwt.JwtTokenService +import org.yapp.globalutils.annotation.ApplicationService -@Service -@Validated +@ApplicationService class AuthTokenService( private val refreshTokenService: RefreshTokenService, private val jwtTokenService: JwtTokenService diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/service/RefreshTokenService.kt b/apis/src/main/kotlin/org/yapp/apis/auth/service/RefreshTokenService.kt index e9ea032a..e06cc214 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/service/RefreshTokenService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/service/RefreshTokenService.kt @@ -1,17 +1,15 @@ package org.yapp.apis.auth.service import jakarta.validation.Valid -import org.springframework.stereotype.Service -import org.springframework.validation.annotation.Validated 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.RefreshTokenDomainService +import org.yapp.globalutils.annotation.ApplicationService import java.util.* -@Service -@Validated +@ApplicationService class RefreshTokenService( private val refreshTokenDomainService: RefreshTokenDomainService, ) { diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/service/UserSignInService.kt b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserSignInService.kt index 0bedb924..8358bae8 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/service/UserSignInService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserSignInService.kt @@ -1,28 +1,31 @@ package org.yapp.apis.auth.service import jakarta.validation.Valid -import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional import org.yapp.apis.user.dto.request.FindOrCreateUserRequest +import org.yapp.apis.user.dto.request.SaveAppleRefreshTokenRequest import org.yapp.apis.user.dto.response.CreateUserResponse import org.yapp.apis.user.service.UserAccountService +import org.yapp.globalutils.annotation.ApplicationService -@Service +@ApplicationService class UserSignInService( private val userAccountService: UserAccountService, ) { @Transactional(propagation = Propagation.REQUIRES_NEW) fun processSignIn( - @Valid request: FindOrCreateUserRequest, - appleRefreshToken: String? + @Valid request: FindOrCreateUserRequest, appleRefreshToken: String? ): CreateUserResponse { - val createUserResponse = userAccountService.findOrCreateUser(request) + val initialUserResponse = userAccountService.findOrCreateUser(request) - appleRefreshToken?.let { - userAccountService.updateAppleRefreshToken(createUserResponse.id, it) - } - - return createUserResponse + return appleRefreshToken.takeIf { !it.isNullOrBlank() } + ?.let { token -> + userAccountService.updateAppleRefreshToken( + SaveAppleRefreshTokenRequest.of( + initialUserResponse, token + ) + ) + } ?: initialUserResponse } } diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/service/UserWithdrawalService.kt b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserWithdrawalService.kt index dcb011fa..88aebf31 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/service/UserWithdrawalService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserWithdrawalService.kt @@ -1,12 +1,12 @@ package org.yapp.apis.auth.service -import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional import org.yapp.apis.user.service.UserAccountService +import org.yapp.globalutils.annotation.ApplicationService import java.util.* -@Service +@ApplicationService class UserWithdrawalService( private val userAccountService: UserAccountService, private val refreshTokenService: RefreshTokenService diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInStrategyResolver.kt b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInStrategyResolver.kt index a9a32a90..41b40742 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInStrategyResolver.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInStrategyResolver.kt @@ -1,10 +1,10 @@ package org.yapp.apis.auth.strategy.signin -import org.springframework.stereotype.Service +import org.springframework.stereotype.Component import org.yapp.apis.auth.exception.AuthErrorCode import org.yapp.apis.auth.exception.AuthException -@Service +@Component class SignInStrategyResolver( private val strategies: List ) { 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 23447293..a491558b 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 @@ -71,8 +71,10 @@ class AuthUseCase( private fun fetchAppleRefreshTokenIfNeeded(credentials: SignInCredentials): String? { if (credentials is AppleAuthCredentials) { - return appleAuthService.fetchAppleOauthTokens(credentials.authorizationCode).refreshToken + val tokenResponse = appleAuthService.fetchAppleOauthTokens(credentials.authorizationCode) + return tokenResponse.refreshToken } + return null } } diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookCreateRequest.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookCreateRequest.kt index 3265d21f..1720801c 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookCreateRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookCreateRequest.kt @@ -6,7 +6,7 @@ import org.yapp.apis.book.dto.response.BookDetailResponse import org.yapp.globalutils.util.RegexUtils @Schema( - title = "책 생성 요청", + name = "BookCreateRequest", description = "시스템에 새로운 책 정보를 생성하는 요청 (주로 내부 API에서 사용)" ) data class BookCreateRequest private constructor( @@ -15,7 +15,7 @@ data class BookCreateRequest private constructor( regexp = RegexUtils.ISBN13_PATTERN, message = "유효한 13자리 ISBN13 형식이 아닙니다." ) - @Schema( + @field:Schema( description = "책의 13자리 ISBN13 코드", example = "9788932473901", required = true, @@ -26,7 +26,7 @@ data class BookCreateRequest private constructor( @field:NotBlank(message = "제목은 필수입니다.") @field:Size(max = 500, message = "제목은 500자 이내여야 합니다.") - @Schema( + @field:Schema( description = "책 제목", example = "해리 포터와 마법사의 돌", required = true, @@ -36,7 +36,7 @@ data class BookCreateRequest private constructor( @field:NotBlank(message = "저자는 필수입니다.") @field:Size(max = 200, message = "저자는 200자 이내여야 합니다.") - @Schema( + @field:Schema( description = "저자명 (여러 저자인 경우 쉼표로 구분)", example = "J.K. 롤링", required = true, @@ -46,7 +46,7 @@ data class BookCreateRequest private constructor( @field:NotBlank(message = "출판사는 필수입니다.") @field:Size(max = 200, message = "출판사는 200자 이내여야 합니다.") - @Schema( + @field:Schema( description = "출판사명", example = "문학수첩", required = true, @@ -56,7 +56,7 @@ data class BookCreateRequest private constructor( @field:Min(value = 1000, message = "출간연도는 1000년 이후여야 합니다.") @field:Max(value = 2100, message = "출간연도는 2100년 이전이어야 합니다.") - @Schema( + @field:Schema( description = "출간연도 (4자리 년도)", example = "2000", minimum = "1000", @@ -66,7 +66,7 @@ data class BookCreateRequest private constructor( @field:Size(max = 2048, message = "표지 URL은 2048자 이내여야 합니다.") @field:NotBlank(message = "표지 이미지 URL은 필수입니다.") - @Schema( + @field:Schema( description = "책 표지 이미지 URL", example = "https://image.aladin.co.kr/product/123/45/cover/1234567890123.jpg", required = true, @@ -76,7 +76,7 @@ data class BookCreateRequest private constructor( val coverImageUrl: String? = null, @field:Size(max = 2000, message = "책 설명은 2000자 이내여야 합니다.") - @Schema( + @field:Schema( description = "책 소개 및 줄거리", example = "11살 해리 포터는 이모네 집에서 갖은 구박을 당하며 지낸다...", maxLength = 2000 diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookDetailRequest.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookDetailRequest.kt index c4f3dafb..9203bc2a 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookDetailRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookDetailRequest.kt @@ -6,7 +6,7 @@ import jakarta.validation.constraints.Pattern import org.yapp.globalutils.util.RegexUtils @Schema( - title = "책 상세 정보 요청", + name = "BookDetailRequest", description = "특정 ISBN13을 통한 책 상세 정보 조회 요청" ) data class BookDetailRequest private constructor( @@ -15,7 +15,7 @@ data class BookDetailRequest private constructor( regexp = RegexUtils.ISBN13_PATTERN, message = "유효한 13자리 ISBN13 형식이 아닙니다." ) - @Schema( + @field:Schema( description = "조회할 책의 13자리 ISBN 코드", example = "9788932473901", required = true, diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookSearchRequest.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookSearchRequest.kt index c6e20006..279d5b78 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookSearchRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookSearchRequest.kt @@ -6,19 +6,19 @@ import jakarta.validation.constraints.Min import jakarta.validation.constraints.NotBlank @Schema( - title = "책 검색 요청", + name = "BookSearchRequest", description = "알라딘 API를 통한 책 검색 요청 정보" ) data class BookSearchRequest private constructor( @field:NotBlank(message = "검색어는 필수입니다.") - @Schema( + @field:Schema( description = "검색할 키워드 (제목, 저자, 출판사 등)", example = "해리포터", required = true ) val query: String? = null, - @Schema( + @field:Schema( description = "검색 유형", example = "Title", allowableValues = ["Title", "Author", "Publisher", "Keyword"], @@ -26,7 +26,7 @@ data class BookSearchRequest private constructor( ) val queryType: String? = null, - @Schema( + @field:Schema( description = "검색 대상", example = "Book", allowableValues = ["Book", "Foreign", "Music", "DVD"], @@ -36,7 +36,7 @@ data class BookSearchRequest private constructor( @field:Min(value = 1, message = "최대 결과 수는 1 이상이어야 합니다.") @field:Max(value = 100, message = "최대 결과 수는 100 이하여야 합니다.") - @Schema( + @field:Schema( description = "한 번에 가져올 최대 결과 수 (1-100)", example = "10", minimum = "1", @@ -46,7 +46,7 @@ data class BookSearchRequest private constructor( val maxResults: Int? = null, @field:Min(value = 1, message = "시작 인덱스는 1 이상이어야 합니다.") - @Schema( + @field:Schema( description = "검색 시작 인덱스 (페이징)", example = "1", minimum = "1", @@ -54,7 +54,7 @@ data class BookSearchRequest private constructor( ) val start: Int? = null, - @Schema( + @field:Schema( description = "정렬 방식", example = "Accuracy", allowableValues = ["Accuracy", "PublishTime", "Title", "SalesPoint"], @@ -62,7 +62,7 @@ data class BookSearchRequest private constructor( ) val sort: String? = null, - @Schema( + @field:Schema( description = "카테고리 ID (0: 전체)", example = "0", defaultValue = "0" diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UpsertUserBookRequest.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UpsertUserBookRequest.kt index a6259774..4cbf107c 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UpsertUserBookRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UpsertUserBookRequest.kt @@ -11,12 +11,12 @@ import org.yapp.globalutils.util.RegexUtils import java.util.UUID @Schema( - title = "사용자 도서 생성/수정 요청", + name = "UpsertUserBookRequest", description = "사용자 서재에 도서를 생성하거나 기존 도서 정보를 수정하는 내부 API 요청 (주로 내부 서비스에서 사용)" ) data class UpsertUserBookRequest private constructor( @field:NotNull(message = "사용자 ID는 필수입니다.") - @Schema( + @field:Schema( description = "사용자 고유 식별자", example = "550e8400-e29b-41d4-a716-446655440000", required = true, @@ -25,7 +25,7 @@ data class UpsertUserBookRequest private constructor( val userId: UUID? = null, @field:NotNull(message = "책 ID는 필수입니다.") - @Schema( + @field:Schema( description = "책 고유 식별자", example = "550e8400-e29b-41d4-a716-446655440001", required = true, @@ -38,7 +38,7 @@ data class UpsertUserBookRequest private constructor( regexp = RegexUtils.ISBN13_PATTERN, message = "유효한 13자리 ISBN13 형식이 아닙니다." ) - @Schema( + @field:Schema( description = "책의 13자리 ISBN13 코드", example = "9788932473901", required = true, @@ -49,7 +49,7 @@ data class UpsertUserBookRequest private constructor( @field:NotBlank(message = "책 제목은 필수입니다.") @field:Size(max = 500, message = "책 제목은 500자 이내여야 합니다.") - @Schema( + @field:Schema( description = "책 제목", example = "해리 포터와 마법사의 돌", required = true, @@ -59,7 +59,7 @@ data class UpsertUserBookRequest private constructor( @field:NotBlank(message = "저자는 필수입니다.") @field:Size(max = 200, message = "저자는 200자 이내여야 합니다.") - @Schema( + @field:Schema( description = "저자명", example = "J.K. 롤링", required = true, @@ -69,7 +69,7 @@ data class UpsertUserBookRequest private constructor( @field:NotBlank(message = "출판사는 필수입니다.") @field:Size(max = 200, message = "출판사는 200자 이내여야 합니다.") - @Schema( + @field:Schema( description = "출판사명", example = "문학수첩", required = true, @@ -79,7 +79,7 @@ data class UpsertUserBookRequest private constructor( @field:NotBlank(message = "표지 이미지 URL은 필수입니다.") @field:Size(max = 2048, message = "표지 이미지 URL은 2048자 이내여야 합니다.") - @Schema( + @field:Schema( description = "책 표지 이미지 URL", example = "https://image.aladin.co.kr/product/123/45/cover/1234567890123.jpg", required = true, @@ -89,7 +89,7 @@ data class UpsertUserBookRequest private constructor( val bookCoverImageUrl: String? = null, @field:NotNull(message = "도서 상태는 필수입니다.") - @Schema( + @field:Schema( description = "사용자의 도서 읽기 상태", example = "READING", required = true, diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UserBookRegisterRequest.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UserBookRegisterRequest.kt index fede450d..73c2f0db 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UserBookRegisterRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UserBookRegisterRequest.kt @@ -8,7 +8,7 @@ import org.yapp.domain.userbook.BookStatus import org.yapp.globalutils.util.RegexUtils @Schema( - title = "사용자 도서 등록 요청", + name = "UserBookRegisterRequest", description = "사용자의 서재에 도서를 등록하거나 상태를 변경하는 요청" ) data class UserBookRegisterRequest private constructor( @@ -17,7 +17,7 @@ data class UserBookRegisterRequest private constructor( regexp = RegexUtils.ISBN13_PATTERN, message = "유효한 13자리 ISBN13 형식이 아닙니다." ) - @Schema( + @field:Schema( description = "등록할 책의 13자리 ISBN13 코드", example = "9788932473901", required = true, @@ -27,7 +27,7 @@ data class UserBookRegisterRequest private constructor( val isbn13: String? = null, @field:NotNull(message = "도서 상태는 필수입니다.") - @Schema( + @field:Schema( description = "사용자의 도서 읽기 상태", example = "READING", required = true, diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UserBooksByIsbn13sRequest.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UserBooksByIsbn13sRequest.kt index 74b79f6c..9c13dcac 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UserBooksByIsbn13sRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UserBooksByIsbn13sRequest.kt @@ -10,14 +10,14 @@ import java.util.UUID description = "Request DTO for finding user books by user ID and a list of ISBN13s" ) data class UserBooksByIsbn13sRequest private constructor( - @Schema( + @field:Schema( description = "사용자 ID", example = "1" ) @field:NotNull(message = "userId는 필수입니다.") val userId: UUID? = null, - @Schema( + @field:Schema( description = "도서 ISBN 목록", example = "[\"9788966262429\", \"9791190412351\"]" ) 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 7235afdb..0674792f 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,16 +1,32 @@ package org.yapp.apis.book.dto.response +import io.swagger.v3.oas.annotations.media.Schema import org.yapp.domain.book.vo.BookInfoVO - import java.util.UUID +@Schema( + name = "BookCreateResponse", + description = "책 생성 요청 처리 후 반환되는 정보" +) data class BookCreateResponse private constructor( + + @field:Schema(description = "등록된 책의 UUID", example = "123e4567-e89b-12d3-a456-426614174000") val bookId: UUID, + + @field:Schema(description = "ISBN-13 번호", example = "9791164053353") val isbn13: String, + + @field:Schema(description = "책 제목", example = "데미안") val title: String, + + @field:Schema(description = "저자", example = "헤르만 헤세") val author: String, + + @field:Schema(description = "출판사", example = "북하우스") val publisher: String, - val coverImageUrl: String, + + @field:Schema(description = "책 표지 이미지 URL", example = "https://image.aladin.co.kr/product/36801/75/coversum/k692030806_1.jpg") + val coverImageUrl: String ) { companion object { fun from(bookVO: BookInfoVO): BookCreateResponse { diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookDetailResponse.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookDetailResponse.kt index 7765ac2f..93adeb98 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookDetailResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookDetailResponse.kt @@ -1,23 +1,60 @@ package org.yapp.apis.book.dto.response -import org.yapp.apis.util.AuthorExtractor -import org.yapp.apis.util.IsbnConverter +import io.swagger.v3.oas.annotations.media.Schema +import org.yapp.apis.book.util.AuthorExtractor +import org.yapp.apis.book.util.IsbnConverter import org.yapp.domain.userbook.BookStatus import org.yapp.infra.external.aladin.response.AladinBookDetailResponse +@Schema( + name = "BookDetailResponse", + description = "책 상세 정보" +) data class BookDetailResponse private constructor( + + @field:Schema(description = "알라딘 API 응답 버전", example = "20131101") val version: String?, + + @field:Schema(description = "책 제목", example = "데미안") val title: String, + + @field:Schema( + description = "검색 결과 상세 페이지 링크", + example = "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=368017530&partner=openAPI" + ) val link: String, + + @field:Schema(description = "저자", example = "헤르만 헤세") val author: String?, + + @field:Schema(description = "출간일", example = "2025-07-30") val pubDate: String, + + @field:Schema(description = "책 설명", example = "인간의 성장과 자아 탐구를 다룬 소설") val description: String, + + @field:Schema(description = "ISBN-13 번호", example = "9791164053353") val isbn13: String?, + + @field:Schema(description = "쇼핑몰 타입", example = "BOOK") val mallType: String, + + @field:Schema( + description = "책 표지 이미지 URL", + example = "https://image.aladin.co.kr/product/36801/75/coversum/k692030806_1.jpg" + ) val coverImageUrl: String, + + @field:Schema(description = "카테고리명", example = "국내도서>소설/시/희곡>독일소설") val categoryName: String, + + @field:Schema(description = "출판사", example = "북하우스") val publisher: String?, + + @field:Schema(description = "총 페이지 수", example = "344") val totalPage: Int?, + + @field:Schema(description = "사용자의 책 상태", example = "BEFORE_REGISTRATION") val userBookStatus: BookStatus ) { fun withUserBookStatus(newUserBookStatus: BookStatus): BookDetailResponse { @@ -27,7 +64,10 @@ data class BookDetailResponse private constructor( companion object { private const val DEFAULT_MAX_PAGE_COUNT = 4032 - fun from(response: AladinBookDetailResponse, userBookStatus: BookStatus = BookStatus.BEFORE_REGISTRATION): BookDetailResponse { + fun from( + response: AladinBookDetailResponse, + userBookStatus: BookStatus = BookStatus.BEFORE_REGISTRATION + ): BookDetailResponse { val item = response.item.firstOrNull() ?: throw IllegalArgumentException("No book item found in detail response.") diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookSearchResponse.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookSearchResponse.kt index 7dbfd7f9..72d1ab33 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookSearchResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookSearchResponse.kt @@ -1,21 +1,45 @@ package org.yapp.apis.book.dto.response -import org.yapp.apis.util.AuthorExtractor +import io.swagger.v3.oas.annotations.media.Schema +import org.yapp.apis.book.util.AuthorExtractor +import org.yapp.apis.book.util.IsbnConverter import org.yapp.domain.userbook.BookStatus -import org.yapp.apis.util.IsbnConverter import org.yapp.infra.external.aladin.response.AladinSearchResponse +@Schema( + name = "BookSearchResponse", + description = "알라딘 도서 검색 API 응답" +) data class BookSearchResponse private constructor( + + @field:Schema(description = "API 응답 버전", example = "20131101") val version: String?, + + @field:Schema(description = "검색 결과 제목", example = "데미안") val title: String?, - val link: String?, + + @field:Schema(description = "출간일", example = "2025-07-30") val pubDate: String?, + + @field:Schema(description = "총 검색 결과 개수", example = "42") val totalResults: Int?, + + @field:Schema(description = "검색 시작 인덱스", example = "1") val startIndex: Int?, + + @field:Schema(description = "한 페이지당 검색 결과 개수", example = "10") val itemsPerPage: Int?, + + @field:Schema(description = "검색 쿼리 문자열", example = "데미안") val query: String?, + + @field:Schema(description = "검색 카테고리 ID", example = "1") val searchCategoryId: Int?, + + @field:Schema(description = "검색 카테고리 이름", example = "소설/시/희곡") val searchCategoryName: String?, + + @field:Schema(description = "검색된 책 목록") val books: List ) { fun withUpdatedBooks(updatedBooks: List): BookSearchResponse { @@ -27,7 +51,6 @@ data class BookSearchResponse private constructor( return BookSearchResponse( version = response.version, title = response.title, - link = response.link, pubDate = response.pubDate, totalResults = response.totalResults, startIndex = response.startIndex, @@ -40,6 +63,7 @@ data class BookSearchResponse private constructor( isbn = it.isbn, isbn13 = it.isbn13, title = it.title, + link = it.link, author = AuthorExtractor.extractAuthors(it.author), publisher = it.publisher, coverImageUrl = it.cover @@ -49,12 +73,34 @@ data class BookSearchResponse private constructor( } } + @Schema(name = "BookSummary", description = "검색된 단일 책 요약 정보") data class BookSummary private constructor( + + @field:Schema(description = "ISBN-13 번호", example = "9781234567890") val isbn13: String, + + @field:Schema(description = "책 제목", example = "데미안") val title: String, + + @field:Schema( + description = "검색 결과 상세 페이지 링크", + example = "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=367872613&partner=openAPI&start=api" + ) + val link: String?, + + @field:Schema(description = "저자", example = "헤르만 헤세") val author: String?, + + @field:Schema(description = "출판사", example = "민음사") val publisher: String?, + + @field:Schema( + description = "책 표지 이미지 URL", + example = "https://image.aladin.co.kr/product/36801/75/coversum/k692030806_1.jpg" + ) val coverImageUrl: String, + + @field:Schema(description = "사용자의 책 상태", example = "BEFORE_REGISTRATION") val userBookStatus: BookStatus ) { fun updateStatus(newStatus: BookStatus): BookSummary { @@ -66,6 +112,7 @@ data class BookSearchResponse private constructor( isbn: String?, isbn13: String?, title: String?, + link: String?, author: String?, publisher: String?, coverImageUrl: String @@ -73,8 +120,10 @@ data class BookSearchResponse private constructor( require(!title.isNullOrBlank()) { "Title is required" } return BookSummary( - isbn13 = isbn13 ?: IsbnConverter.toIsbn13(isbn) ?: throw IllegalArgumentException("Either isbn13 or isbn must be provided"), + isbn13 = isbn13 ?: IsbnConverter.toIsbn13(isbn) + ?: throw IllegalArgumentException("Either isbn13 or isbn must be provided"), title = title, + link = link, author = author, publisher = publisher, coverImageUrl = coverImageUrl, diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookPageResponse.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookPageResponse.kt index 0f30d8ce..8fb158e5 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookPageResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookPageResponse.kt @@ -3,22 +3,25 @@ package org.yapp.apis.book.dto.response import io.swagger.v3.oas.annotations.media.Schema import org.springframework.data.domain.Page -@Schema(description = "사용자의 책 페이지 응답") +@Schema( + name = "UserBookPageResponse", + description = "사용자의 책 목록 페이징 응답 데이터" +) data class UserBookPageResponse private constructor( - @Schema(description = "책 목록 (페이지네이션)", implementation = UserBookResponse::class) + @field:Schema(description = "페이징된 책 목록") val books: Page, - @Schema(description = "읽기 전 상태의 책 개수") + @field:Schema(description = "읽기 전 상태의 책 개수", example = "5") val beforeReadingCount: Long, - @Schema(description = "읽고 있는 책 개수") + @field:Schema(description = "현재 읽고 있는 책 개수", example = "3") val readingCount: Long, - @Schema(description = "완독한 책 개수") + @field:Schema(description = "완독한 책 개수", example = "10") val completedCount: Long, - @Schema(description = "총 책 개수") + @field:Schema(description = "전체 책 개수", example = "18") val totalCount: Long ) { companion object { 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 2879a9b5..dfb89dcf 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,22 +1,52 @@ package org.yapp.apis.book.dto.response +import io.swagger.v3.oas.annotations.media.Schema import org.yapp.domain.userbook.BookStatus import org.yapp.domain.userbook.vo.UserBookInfoVO import org.yapp.globalutils.validator.BookDataValidator import java.time.format.DateTimeFormatter import java.util.UUID +@Schema( + name = "UserBookResponse", + description = "사용자 책 단일 정보 응답 DTO" +) data class UserBookResponse private constructor( + + @field:Schema(description = "사용자 책 고유 ID(UUID)", example = "123e4567-e89b-12d3-a456-426614174000") val userBookId: UUID, + + @field:Schema(description = "사용자 고유 ID(UUID)", example = "987e6543-e21b-34d3-a456-426614174999") val userId: UUID, + + @field:Schema(description = "ISBN-13 번호", example = "9791164053353") val isbn13: String, + + @field:Schema(description = "책 제목", example = "데미안") val bookTitle: String, + + @field:Schema(description = "저자", example = "헤르만 헤세") val bookAuthor: String, + + @field:Schema(description = "독서 상태", example = "READING") val status: BookStatus, + + @field:Schema( + description = "책 표지 이미지 URL", + example = "https://image.aladin.co.kr/product/36801/75/coversum/k692030806_1.jpg" + ) val coverImageUrl: String, + + @field:Schema(description = "출판사명", example = "북하우스") val publisher: String, + + @field:Schema(description = "등록 일시 (ISO_LOCAL_DATE_TIME 형식)", example = "2025-08-09T15:30:00") val createdAt: String, + + @field:Schema(description = "수정 일시 (ISO_LOCAL_DATE_TIME 형식)", example = "2025-08-10T10:15:30") val updatedAt: String, + + @field:Schema(description = "독서 기록 개수", example = "15", minimum = "0") val recordCount: Int, ) { companion object { diff --git a/apis/src/main/kotlin/org/yapp/apis/book/service/AladinBookQueryService.kt b/apis/src/main/kotlin/org/yapp/apis/book/service/AladinBookQueryService.kt index fb8619a4..34f5e6b9 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/service/AladinBookQueryService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/service/AladinBookQueryService.kt @@ -1,16 +1,14 @@ package org.yapp.apis.book.service -import jakarta.validation.Valid import mu.KotlinLogging -import org.springframework.stereotype.Service -import org.springframework.validation.annotation.Validated import org.yapp.apis.book.dto.request.BookDetailRequest import org.yapp.apis.book.dto.request.BookSearchRequest import org.yapp.apis.book.dto.response.BookDetailResponse import org.yapp.apis.book.dto.response.BookSearchResponse import org.yapp.apis.book.exception.BookErrorCode import org.yapp.apis.book.exception.BookException -import org.yapp.apis.util.IsbnConverter +import org.yapp.apis.book.util.IsbnConverter +import org.yapp.globalutils.annotation.ApplicationService import org.yapp.globalutils.validator.IsbnValidator import org.yapp.infra.external.aladin.AladinApi import org.yapp.infra.external.aladin.request.AladinBookLookupRequest @@ -19,8 +17,7 @@ import org.yapp.infra.external.aladin.response.AladinBookDetailResponse import org.yapp.infra.external.aladin.response.AladinSearchItem import org.yapp.infra.external.aladin.response.AladinSearchResponse -@Service -@Validated +@ApplicationService class AladinBookQueryService( private val aladinApi: AladinApi ) : BookQueryService { 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 3fbb382a..d87617f7 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 @@ -1,14 +1,12 @@ package org.yapp.apis.book.service import jakarta.validation.Valid -import org.springframework.stereotype.Service -import org.springframework.validation.annotation.Validated import org.yapp.apis.book.dto.request.BookCreateRequest import org.yapp.apis.book.dto.response.BookCreateResponse import org.yapp.domain.book.BookDomainService +import org.yapp.globalutils.annotation.ApplicationService -@Service -@Validated +@ApplicationService class BookManagementService( private val bookDomainService: BookDomainService ) { 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 6b0544cd..2965ad31 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 jakarta.validation.Valid import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable -import org.springframework.stereotype.Service -import org.springframework.validation.annotation.Validated -import org.yapp.apis.book.dto.request.UserBooksByIsbn13sRequest import org.yapp.apis.book.dto.request.UpsertUserBookRequest +import org.yapp.apis.book.dto.request.UserBooksByIsbn13sRequest import org.yapp.apis.book.dto.response.UserBookPageResponse import org.yapp.apis.book.dto.response.UserBookResponse import org.yapp.apis.book.exception.UserBookErrorCode @@ -14,10 +12,10 @@ import org.yapp.apis.book.exception.UserBookException import org.yapp.domain.userbook.BookStatus import org.yapp.domain.userbook.UserBookDomainService import org.yapp.domain.userbook.UserBookSortType +import org.yapp.globalutils.annotation.ApplicationService import java.util.* -@Service -@Validated +@ApplicationService class UserBookService( private val userBookDomainService: UserBookDomainService ) { diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/util/AuthorExtractor.kt b/apis/src/main/kotlin/org/yapp/apis/book/util/AuthorExtractor.kt similarity index 94% rename from apis/src/main/kotlin/org/yapp/apis/auth/util/AuthorExtractor.kt rename to apis/src/main/kotlin/org/yapp/apis/book/util/AuthorExtractor.kt index 85819fa5..db9a7121 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/util/AuthorExtractor.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/util/AuthorExtractor.kt @@ -1,4 +1,4 @@ -package org.yapp.apis.util +package org.yapp.apis.book.util object AuthorExtractor { private const val AUTHOR_MARKER = "(지은이)" diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/util/IsbnConverter.kt b/apis/src/main/kotlin/org/yapp/apis/book/util/IsbnConverter.kt similarity index 96% rename from apis/src/main/kotlin/org/yapp/apis/auth/util/IsbnConverter.kt rename to apis/src/main/kotlin/org/yapp/apis/book/util/IsbnConverter.kt index 90a005f9..b32d36aa 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/util/IsbnConverter.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/util/IsbnConverter.kt @@ -1,4 +1,4 @@ -package org.yapp.apis.util +package org.yapp.apis.book.util import org.yapp.globalutils.util.RegexUtils diff --git a/apis/src/main/kotlin/org/yapp/apis/config/SwaggerConfig.kt b/apis/src/main/kotlin/org/yapp/apis/config/SwaggerConfig.kt index c8abe679..c6fe45b8 100644 --- a/apis/src/main/kotlin/org/yapp/apis/config/SwaggerConfig.kt +++ b/apis/src/main/kotlin/org/yapp/apis/config/SwaggerConfig.kt @@ -6,10 +6,15 @@ import io.swagger.v3.oas.models.info.Info import io.swagger.v3.oas.models.security.SecurityRequirement import io.swagger.v3.oas.models.security.SecurityScheme import io.swagger.v3.oas.models.servers.Server +import org.springdoc.core.customizers.OperationCustomizer import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Profile +import org.springframework.core.Ordered +import org.springframework.core.annotation.AnnotatedElementUtils +import org.springframework.core.annotation.Order +import org.yapp.globalutils.annotation.DisableSwaggerSecurity @Configuration @EnableConfigurationProperties(SwaggerProperties::class) @@ -50,4 +55,30 @@ class SwaggerConfig( ) ) } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + fun securityOperationCustomizer(): OperationCustomizer { + return OperationCustomizer { operation, handlerMethod -> + val method = handlerMethod.method + val hasOnMethod = AnnotatedElementUtils + .findMergedAnnotation(method, DisableSwaggerSecurity::class.java) != null + + val hasOnInterface = if (!hasOnMethod) { + handlerMethod.beanType.interfaces + .flatMap { it.methods.asIterable() } + .any { ifaceMethod -> + ifaceMethod.name == method.name && + ifaceMethod.parameterTypes.contentEquals(method.parameterTypes) && + ifaceMethod.isAnnotationPresent(DisableSwaggerSecurity::class.java) + } + } else false + + if (hasOnMethod || hasOnInterface) { + operation.security = emptyList() + } + + operation + } + } } diff --git a/apis/src/main/kotlin/org/yapp/apis/home/dto/response/UserHomeResponse.kt b/apis/src/main/kotlin/org/yapp/apis/home/dto/response/UserHomeResponse.kt index 7afb8dc0..19b41e63 100644 --- a/apis/src/main/kotlin/org/yapp/apis/home/dto/response/UserHomeResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/home/dto/response/UserHomeResponse.kt @@ -6,10 +6,11 @@ import java.time.LocalDateTime import java.util.* @Schema( - description = "사용자 홈 화면 응답 DTO", name = "UserHomeResponse" + name = "UserHomeResponse", + description = "사용자 홈 화면 응답 DTO" ) data class UserHomeResponse private constructor( - @Schema( + @field:Schema( description = "사용자가 최근에 읽은 책 목록", name = "recentBooks" ) val recentBooks: List ) { @@ -17,42 +18,42 @@ data class UserHomeResponse private constructor( description = "최근 읽은 책 한 권의 상세 정보", name = "RecentBookResponse" ) data class RecentBookResponse private constructor( - @Schema( + @field:Schema( description = "사용자 서재에 등록된 책의 고유 ID", example = "123e4567-e89b-12d3-a456-426614174000", ) val userBookId: UUID, - @Schema( + @field:Schema( description = "책의 ISBN-13", example = "9788960777330", ) val isbn13: String, - @Schema( + @field:Schema( description = "책 제목", example = "모던 자바스크립트 Deep Dive", ) val title: String, - @Schema( + @field:Schema( description = "저자", example = "이웅모", ) val author: String, - @Schema( + @field:Schema( description = "출판사", example = "위키북스", ) val publisher: String, - @Schema( + @field:Schema( description = "책 표지 이미지 URL", example = "https://image.aladin.co.kr/product/2523/21/cover/8960777330_1.jpg", ) val coverImageUrl: String, - @Schema( + @field:Schema( description = "마지막 독서 기록 시간", example = "2025-08-07T10:00:00", ) val lastRecordedAt: LocalDateTime, - @Schema( + @field:Schema( description = "해당 책에 대한 총 독서 기록 수", example = "12", ) val recordCount: Int diff --git a/apis/src/main/kotlin/org/yapp/apis/home/service/HomeService.kt b/apis/src/main/kotlin/org/yapp/apis/home/service/HomeService.kt index aa98cbb7..d50eba3a 100644 --- a/apis/src/main/kotlin/org/yapp/apis/home/service/HomeService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/home/service/HomeService.kt @@ -1,12 +1,12 @@ package org.yapp.apis.home.service -import org.springframework.stereotype.Service import org.yapp.apis.home.dto.response.UserHomeResponse import org.yapp.domain.userbook.UserBookDomainService import org.yapp.domain.userbook.vo.HomeBookVO +import org.yapp.globalutils.annotation.ApplicationService import java.util.* -@Service +@ApplicationService class HomeService( private val userBookDomainService: UserBookDomainService ) { @@ -33,4 +33,4 @@ class HomeService( return booksWithRecords + booksWithoutRecords } -} \ No newline at end of file +} diff --git a/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt b/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt index 7cb7a658..fe999867 100644 --- a/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt @@ -7,6 +7,7 @@ import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.Size @Schema( + name = "CreateReadingRecordRequest", description = "독서 기록 생성 요청", example = """ { @@ -21,21 +22,21 @@ data class CreateReadingRecordRequest private constructor( @field:Min(1, message = "페이지 번호는 1 이상이어야 합니다.") @field:Max(9999, message = "페이지 번호는 9999 이하여야 합니다.") - @Schema(description = "현재 읽은 페이지 번호", example = "42", required = true) + @field:Schema(description = "현재 읽은 페이지 번호", example = "42", required = true) val pageNumber: Int? = null, @field:NotBlank(message = "기억에 남는 문장은 필수입니다.") @field:Size(max = 1000, message = "기억에 남는 문장은 1000자를 초과할 수 없습니다.") - @Schema(description = "기억에 남는 문장", example = "이것은 기억에 남는 문장입니다.", required = true) + @field:Schema(description = "기억에 남는 문장", example = "이것은 기억에 남는 문장입니다.", required = true) val quote: String? = null, @field:NotBlank(message = "감상평은 필수입니다.") @field:Size(max = 1000, message = "감상평은 1000자를 초과할 수 없습니다.") - @Schema(description = "감상평", example = "이 책은 매우 인상적이었습니다.", required = true) + @field:Schema(description = "감상평", example = "이 책은 매우 인상적이었습니다.", required = true) val review: String? = null, @field:Size(max = 1, message = "감정 태그는 최대 1개까지 가능합니다. (단일 감정만 받지만, 확장성을 위해 리스트 형태로 관리됩니다.)") - @Schema(description = "감정 태그 목록 (현재는 최대 1개, 확장 가능)", example = "[\"감동적\"]") + @field:Schema(description = "감정 태그 목록 (현재는 최대 1개, 확장 가능)", example = "[\"감동적\"]") val emotionTags: List<@Size(max = 10, message = "감정 태그는 10자를 초과할 수 없습니다.") String> = emptyList() ) { fun validPageNumber(): Int = pageNumber!! diff --git a/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt b/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt index 11e1e1a4..a075da1b 100644 --- a/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt @@ -6,42 +6,45 @@ import java.time.format.DateTimeFormatter import java.util.UUID -@Schema(description = "독서 기록 응답") +@Schema( + name = "ReadingRecordResponse", + description = "독서 기록 응답" +) data class ReadingRecordResponse private constructor( - @Schema(description = "독서 기록 ID", example = "123e4567-e89b-12d3-a456-426614174000") + @field:Schema(description = "독서 기록 ID", example = "123e4567-e89b-12d3-a456-426614174000") val id: UUID, - @Schema(description = "사용자 책 ID", example = "123e4567-e89b-12d3-a456-426614174000") + @field:Schema(description = "사용자 책 ID", example = "123e4567-e89b-12d3-a456-426614174000") val userBookId: UUID, - @Schema(description = "현재 읽은 페이지 번호", example = "42") + @field:Schema(description = "현재 읽은 페이지 번호", example = "42") val pageNumber: Int, - @Schema(description = "기억에 남는 문장", example = "이것은 기억에 남는 문장입니다.") + @field:Schema(description = "기억에 남는 문장", example = "이것은 기억에 남는 문장입니다.") val quote: String, - @Schema(description = "감상평", example = "이 책은 매우 인상적이었습니다.") + @field:Schema(description = "감상평", example = "이 책은 매우 인상적이었습니다.") val review: String, - @Schema(description = "감정 태그 목록", example = "[\"감동적\", \"슬픔\", \"희망\"]") + @field:Schema(description = "감정 태그 목록", example = "[\"감동적\", \"슬픔\", \"희망\"]") val emotionTags: List, - @Schema(description = "생성 일시", example = "2023-01-01T12:00:00") + @field:Schema(description = "생성 일시", example = "2023-01-01T12:00:00") val createdAt: String, - @Schema(description = "수정 일시", example = "2023-01-01T12:00:00") + @field:Schema(description = "수정 일시", example = "2023-01-01T12:00:00") val updatedAt: String, - @Schema(description = "도서 제목", example = "클린 코드") + @field:Schema(description = "도서 제목", example = "클린 코드") val bookTitle: String?, - @Schema(description = "출판사", example = "인사이트") + @field:Schema(description = "출판사", example = "인사이트") val bookPublisher: String?, - @Schema(description = "도서 썸네일 URL", example = "https://example.com/book-cover.jpg") + @field:Schema(description = "도서 썸네일 URL", example = "https://example.com/book-cover.jpg") val bookCoverImageUrl: String?, - @Schema(description = "저자", example = "로버트 C. 마틴") + @field:Schema(description = "저자", example = "로버트 C. 마틴") val author: String? ) { companion object { diff --git a/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/SeedStatsResponse.kt b/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/SeedStatsResponse.kt index 9e602000..ed71abbb 100644 --- a/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/SeedStatsResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/SeedStatsResponse.kt @@ -1,13 +1,39 @@ package org.yapp.apis.readingrecord.dto.response +import io.swagger.v3.oas.annotations.media.Schema import org.yapp.domain.readingrecordtag.vo.TagStatsVO import org.yapp.globalutils.tag.GeneralEmotionTagCategory +@Schema( + name = "SeedStatsResponse", + description = "감정 태그 카테고리별 씨앗 통계 응답" +) data class SeedStatsResponse private constructor( + + @field:Schema( + description = "각 감정 카테고리의 통계 리스트", + example = "[{\"name\":\"따뜻함\",\"count\":3},{\"name\":\"즐거움\",\"count\":1},{\"name\":\"슬픔\",\"count\":0},{\"name\":\"깨달음\",\"count\":0}]" + ) val categories: List ) { + + @Schema( + name = "SeedCategoryStats", + description = "단일 감정 카테고리의 통계 정보" + ) data class SeedCategoryStats private constructor( + + @field:Schema( + description = "감정 카테고리 이름", + example = "따뜻함" + ) val name: String, + + @field:Schema( + description = "해당 감정 카테고리의 씨앗 개수", + example = "3", + minimum = "0" + ) val count: Int ) { companion object { diff --git a/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt b/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt index 505764a5..46a75808 100644 --- a/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt @@ -2,17 +2,14 @@ package org.yapp.apis.readingrecord.service import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable -import org.springframework.stereotype.Service -import org.springframework.validation.annotation.Validated import org.yapp.apis.readingrecord.dto.request.CreateReadingRecordRequest import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponse import org.yapp.domain.readingrecord.ReadingRecordDomainService import org.yapp.domain.readingrecord.ReadingRecordSortType +import org.yapp.globalutils.annotation.ApplicationService import java.util.* - -@Service -@Validated +@ApplicationService class ReadingRecordService( private val readingRecordDomainService: ReadingRecordDomainService, ) { diff --git a/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordTagService.kt b/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordTagService.kt index 893765b6..83bb8101 100644 --- a/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordTagService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordTagService.kt @@ -1,12 +1,12 @@ package org.yapp.apis.readingrecord.service -import org.springframework.stereotype.Service import org.yapp.apis.readingrecord.dto.response.SeedStatsResponse import org.yapp.domain.readingrecordtag.ReadingRecordTagDomainService +import org.yapp.globalutils.annotation.ApplicationService import org.yapp.globalutils.tag.GeneralEmotionTagCategory import java.util.* -@Service +@ApplicationService class ReadingRecordTagService( private val readingRecordTagDomainService: ReadingRecordTagDomainService ) { diff --git a/apis/src/main/kotlin/org/yapp/apis/user/dto/request/FindOrCreateUserRequest.kt b/apis/src/main/kotlin/org/yapp/apis/user/dto/request/FindOrCreateUserRequest.kt index 8ce0a947..92a6459c 100644 --- a/apis/src/main/kotlin/org/yapp/apis/user/dto/request/FindOrCreateUserRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/user/dto/request/FindOrCreateUserRequest.kt @@ -12,33 +12,33 @@ import org.yapp.domain.user.ProviderType description = "Request DTO for finding an existing user or creating a new one during social login" ) data class FindOrCreateUserRequest private constructor( - @Schema( + @field:Schema( description = "사용자 이메일", example = "user@example.com", nullable = true) val email: String? = null, - @Schema( + @field:Schema( description = "사용자 닉네임", example = "코딩하는곰", nullable = true ) val nickname: String? = null, - @Schema( + @field:Schema( description = "사용자 프로필 이미지 URL", example = "https://example.com/image.jpg", nullable = true ) val profileImageUrl: String? = null, - @Schema( + @field:Schema( description = "소셜 로그인 제공자", example = "KAKAO" ) @field:NotNull(message = "providerType은 필수입니다.") val providerType: ProviderType? = null, - @Schema( + @field:Schema( description = "소셜 제공자에서 발급한 식별자", example = "12345678901234567890" ) diff --git a/apis/src/main/kotlin/org/yapp/apis/user/dto/request/FindUserIdentityRequest.kt b/apis/src/main/kotlin/org/yapp/apis/user/dto/request/FindUserIdentityRequest.kt index 4971a03a..19aa591a 100644 --- a/apis/src/main/kotlin/org/yapp/apis/user/dto/request/FindUserIdentityRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/user/dto/request/FindUserIdentityRequest.kt @@ -10,7 +10,7 @@ import java.util.* description = "Request DTO to retrieve user identity information using userId" ) data class FindUserIdentityRequest private constructor( - @Schema( + @field:Schema( description = "User ID (UUID format)", example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0" ) diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SaveAppleRefreshTokenRequest.kt b/apis/src/main/kotlin/org/yapp/apis/user/dto/request/SaveAppleRefreshTokenRequest.kt similarity index 62% rename from apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SaveAppleRefreshTokenRequest.kt rename to apis/src/main/kotlin/org/yapp/apis/user/dto/request/SaveAppleRefreshTokenRequest.kt index 800d4d9a..75f9854c 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SaveAppleRefreshTokenRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/user/dto/request/SaveAppleRefreshTokenRequest.kt @@ -1,49 +1,41 @@ -package org.yapp.apis.auth.dto.request +package org.yapp.apis.user.dto.request import io.swagger.v3.oas.annotations.media.Schema import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.NotNull import org.yapp.apis.user.dto.response.CreateUserResponse -import org.yapp.apis.auth.strategy.signin.AppleAuthCredentials -import java.util.UUID +import java.util.* @Schema( name = "SaveAppleRefreshTokenRequest", description = "Request DTO for saving Apple refresh token with user ID and authorization code" ) data class SaveAppleRefreshTokenRequest private constructor( - @Schema( + @field:Schema( description = "Unique identifier of the user", example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0" ) @field:NotNull(message = "userId must not be null") val userId: UUID? = null, - @Schema( - description = "Authorization code from Apple OAuth process", - example = "cdef1234-abcd-5678-efgh-9012ijklmnop" - ) - @field:NotBlank(message = "authorizationCode must not be blank") - val authorizationCode: String? = null, - - @Schema( + @field:Schema( description = "Apple refresh token, nullable if not issued yet", example = "apple-refresh-token-example" ) + @field:NotBlank(message = "appleRefreshToken must not be blank") val appleRefreshToken: String? = null ) { fun validUserId(): UUID = userId!! - fun validAuthorizationCode(): String = authorizationCode!! + fun validAppleRefreshToken(): String = appleRefreshToken!! companion object { fun of( userResponse: CreateUserResponse, - credentials: AppleAuthCredentials + appleRefreshToken: String ): SaveAppleRefreshTokenRequest { return SaveAppleRefreshTokenRequest( userId = userResponse.id, - authorizationCode = credentials.authorizationCode, - appleRefreshToken = userResponse.appleRefreshToken + appleRefreshToken = appleRefreshToken ) } } diff --git a/apis/src/main/kotlin/org/yapp/apis/user/dto/request/TermsAgreementRequest.kt b/apis/src/main/kotlin/org/yapp/apis/user/dto/request/TermsAgreementRequest.kt index 4edfea40..79a65318 100644 --- a/apis/src/main/kotlin/org/yapp/apis/user/dto/request/TermsAgreementRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/user/dto/request/TermsAgreementRequest.kt @@ -3,9 +3,16 @@ package org.yapp.apis.user.dto.request import io.swagger.v3.oas.annotations.media.Schema import jakarta.validation.constraints.NotNull -@Schema(description = "Request to update terms agreement status") +@Schema( + name = "TermsAgreementRequest", + description = "Request to update terms agreement status" +) data class TermsAgreementRequest private constructor( - @Schema(description = "Whether the user agrees to the terms of service", example = "true", required = true) + @field:Schema( + description = "Whether the user agrees to the terms of service", + example = "true", + required = true + ) @field:NotNull(message = "termsAgreed must not be null") val termsAgreed: Boolean? = null ) { diff --git a/apis/src/main/kotlin/org/yapp/apis/user/dto/response/CreateUserResponse.kt b/apis/src/main/kotlin/org/yapp/apis/user/dto/response/CreateUserResponse.kt index 6749c2f8..0be6f187 100644 --- a/apis/src/main/kotlin/org/yapp/apis/user/dto/response/CreateUserResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/user/dto/response/CreateUserResponse.kt @@ -10,19 +10,19 @@ import java.util.* description = "Response DTO returned after successful user registration" ) data class CreateUserResponse private constructor( - @Schema( + @field:Schema( description = "사용자 ID", example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0" ) val id: UUID, - @Schema( + @field:Schema( description = "사용자 역할", example = "USER" ) val role: Role, - @Schema( + @field:Schema( description = "Apple Refresh Token (Apple 유저인 경우에만 존재)", nullable = true ) diff --git a/apis/src/main/kotlin/org/yapp/apis/user/dto/response/UserAuthInfoResponse.kt b/apis/src/main/kotlin/org/yapp/apis/user/dto/response/UserAuthInfoResponse.kt index 03d5c542..3d6a3cc3 100644 --- a/apis/src/main/kotlin/org/yapp/apis/user/dto/response/UserAuthInfoResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/user/dto/response/UserAuthInfoResponse.kt @@ -10,13 +10,13 @@ import java.util.UUID description = "Response DTO containing minimal authentication information (ID and role)" ) data class UserAuthInfoResponse private constructor( - @Schema( + @field:Schema( description = "Unique identifier of the user", example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0" ) val id: UUID, - @Schema( + @field:Schema( description = "Role assigned to the user", example = "USER" ) diff --git a/apis/src/main/kotlin/org/yapp/apis/user/dto/response/UserProfileResponse.kt b/apis/src/main/kotlin/org/yapp/apis/user/dto/response/UserProfileResponse.kt index d840712b..3dcf4186 100644 --- a/apis/src/main/kotlin/org/yapp/apis/user/dto/response/UserProfileResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/user/dto/response/UserProfileResponse.kt @@ -11,31 +11,31 @@ import java.util.UUID ) data class UserProfileResponse( - @Schema( + @field:Schema( description = "Unique identifier of the user", example = "c4d46ff7-9f1b-4c5f-8262-9fa2f982a7f4" ) val id: UUID, - @Schema( + @field:Schema( description = "User email address", example = "user@example.com" ) val email: String, - @Schema( + @field:Schema( description = "User nickname", example = "HappyPanda" ) val nickname: String, - @Schema( + @field:Schema( description = "Social login provider type", example = "KAKAO" ) val provider: ProviderType, - @Schema( + @field:Schema( description = "Whether the user has agreed to the terms of service", example = "false" ) diff --git a/apis/src/main/kotlin/org/yapp/apis/user/dto/response/WithdrawTargetUserResponse.kt b/apis/src/main/kotlin/org/yapp/apis/user/dto/response/WithdrawTargetUserResponse.kt index 3f313a38..f1fd3929 100644 --- a/apis/src/main/kotlin/org/yapp/apis/user/dto/response/WithdrawTargetUserResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/user/dto/response/WithdrawTargetUserResponse.kt @@ -11,16 +11,16 @@ import java.util.UUID ) data class WithdrawTargetUserResponse private constructor( - @Schema(description = "사용자 ID") + @field:Schema(description = "사용자 ID") val id: UUID, - @Schema(description = "소셜 로그인 제공사 타입") + @field:Schema(description = "소셜 로그인 제공사 타입") val providerType: ProviderType, - @Schema(description = "소셜 제공사로부터 발급받은 고유 ID") + @field:Schema(description = "소셜 제공사로부터 발급받은 고유 ID") val providerId: String, - @Schema(description = "Apple Refresh Token (애플 회원 탈퇴 시 필요, 카카오는 null)") + @field:Schema(description = "Apple Refresh Token (애플 회원 탈퇴 시 필요, 카카오는 null)") val appleRefreshToken: String? = null ) { companion object { diff --git a/apis/src/main/kotlin/org/yapp/apis/user/service/UserAccountService.kt b/apis/src/main/kotlin/org/yapp/apis/user/service/UserAccountService.kt index 7a22a942..198c10a2 100644 --- a/apis/src/main/kotlin/org/yapp/apis/user/service/UserAccountService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/user/service/UserAccountService.kt @@ -1,8 +1,7 @@ package org.yapp.apis.user.service import jakarta.validation.Valid -import org.springframework.stereotype.Service -import org.springframework.validation.annotation.Validated +import org.yapp.apis.user.dto.request.SaveAppleRefreshTokenRequest import org.yapp.apis.auth.exception.AuthErrorCode import org.yapp.apis.auth.exception.AuthException import org.yapp.apis.user.dto.request.FindOrCreateUserRequest @@ -10,10 +9,10 @@ import org.yapp.apis.user.dto.response.CreateUserResponse import org.yapp.apis.user.dto.response.WithdrawTargetUserResponse import org.yapp.domain.user.UserDomainService import org.yapp.domain.user.vo.UserAuthVO +import org.yapp.globalutils.annotation.ApplicationService import java.util.* -@Service -@Validated +@ApplicationService class UserAccountService( private val userDomainService: UserDomainService ) { @@ -46,8 +45,13 @@ class UserAccountService( userDomainService.deleteUser(userId) } - fun updateAppleRefreshToken(userId: UUID, refreshToken: String) { - userDomainService.updateAppleRefreshToken(userId, refreshToken) + fun updateAppleRefreshToken(@Valid saveAppleRefreshTokenRequest: SaveAppleRefreshTokenRequest): CreateUserResponse { + val userAuthVO = userDomainService.updateAppleRefreshToken( + saveAppleRefreshTokenRequest.validUserId(), + saveAppleRefreshTokenRequest.validAppleRefreshToken() + ) + + return CreateUserResponse.from(userAuthVO) } private fun createNewUser(@Valid findOrCreateUserRequest: FindOrCreateUserRequest): UserAuthVO { diff --git a/apis/src/main/kotlin/org/yapp/apis/user/service/UserService.kt b/apis/src/main/kotlin/org/yapp/apis/user/service/UserService.kt index eae1ca53..4a1acd2b 100644 --- a/apis/src/main/kotlin/org/yapp/apis/user/service/UserService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/user/service/UserService.kt @@ -1,19 +1,17 @@ package org.yapp.apis.user.service import jakarta.validation.Valid -import org.springframework.stereotype.Service -import org.springframework.validation.annotation.Validated -import org.yapp.apis.user.dto.request.FindUserIdentityRequest -import org.yapp.apis.user.dto.response.UserAuthInfoResponse import org.yapp.apis.auth.exception.AuthErrorCode import org.yapp.apis.auth.exception.AuthException +import org.yapp.apis.user.dto.request.FindUserIdentityRequest import org.yapp.apis.user.dto.request.TermsAgreementRequest +import org.yapp.apis.user.dto.response.UserAuthInfoResponse import org.yapp.apis.user.dto.response.UserProfileResponse import org.yapp.domain.user.UserDomainService +import org.yapp.globalutils.annotation.ApplicationService import java.util.* -@Service -@Validated +@ApplicationService class UserService( private val userDomainService: UserDomainService ) { 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 6ddc6d65..78f65ca1 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt @@ -84,13 +84,13 @@ class UserDomainService( return UserProfileVO.newInstance(updatedUser) } - fun updateAppleRefreshToken(userId: UUID, refreshToken: String): UserIdentityVO { + fun updateAppleRefreshToken(userId: UUID, refreshToken: String): UserAuthVO { val user = userRepository.findById(userId) ?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND) val updatedUser = userRepository.save(user.updateAppleRefreshToken(refreshToken)) - return UserIdentityVO.newInstance(updatedUser) + return UserAuthVO.newInstance(updatedUser) } fun deleteUser(userId: UUID) { 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 eb140e68..2fabc727 100644 --- a/gateway/src/main/kotlin/org/yapp/gateway/security/SecurityConfig.kt +++ b/gateway/src/main/kotlin/org/yapp/gateway/security/SecurityConfig.kt @@ -49,9 +49,6 @@ class SecurityConfig( .authorizeHttpRequests { it.requestMatchers(*WHITELIST_URLS).permitAll() it.requestMatchers("/api/v1/admin/**").hasRole("ADMIN") - it.requestMatchers("/api/v1/user/**").hasAnyRole("USER", "ADMIN") - it.requestMatchers("/api/v1/auth/**").authenticated() - it.requestMatchers("/api/v1/books/**").authenticated() it.anyRequest().authenticated() } diff --git a/global-utils/src/main/kotlin/org/yapp/globalutils/annotation/ApplicationService.kt b/global-utils/src/main/kotlin/org/yapp/globalutils/annotation/ApplicationService.kt new file mode 100644 index 00000000..5e096fce --- /dev/null +++ b/global-utils/src/main/kotlin/org/yapp/globalutils/annotation/ApplicationService.kt @@ -0,0 +1,11 @@ +package org.yapp.globalutils.annotation + +import org.springframework.stereotype.Service +import org.springframework.validation.annotation.Validated + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@Service +@Validated +annotation class ApplicationService diff --git a/global-utils/src/main/kotlin/org/yapp/globalutils/annotation/DisableSwaggerSecurity.kt b/global-utils/src/main/kotlin/org/yapp/globalutils/annotation/DisableSwaggerSecurity.kt new file mode 100644 index 00000000..e9577d93 --- /dev/null +++ b/global-utils/src/main/kotlin/org/yapp/globalutils/annotation/DisableSwaggerSecurity.kt @@ -0,0 +1,6 @@ +package org.yapp.globalutils.annotation + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class DisableSwaggerSecurity