Skip to content

Book 469 feature/#141#156

Merged
minwoo1999 merged 10 commits intodevelopfrom
BOOK-469-feature/#141
Feb 8, 2026
Merged

Book 469 feature/#141#156
minwoo1999 merged 10 commits intodevelopfrom
BOOK-469-feature/#141

Conversation

@minwoo1999
Copy link
Member

@minwoo1999 minwoo1999 commented Feb 8, 2026

🔗 관련 이슈

  • Close #

📘 작업 유형

  • ✨ Feature (기능 추가)
  • 🐞 Bugfix (버그 수정)
  • 🔧 Refactor (코드 리팩토링)
  • ⚙️ Chore (환경 설정)
  • 📝 Docs (문서 작성 및 수정)
  • ✅ Test (기능 테스트)
  • 🎨 style (코드 스타일 수정)

📙 작업 내역

🧪 테스트 내역

  • 브라우저/기기에서 동작 확인
  • 엣지 케이스 테스트 완료
  • 기존 기능 영향 없음

🎨 스크린샷 또는 시연 영상 (선택)

기능 미리보기 기능 미리보기
기능 설명 기능 설명

✅ PR 체크리스트

  • 커밋 메시지가 명확합니다
  • PR 제목이 컨벤션에 맞습니다
  • 관련 이슈 번호를 작성했습니다
  • 기능이 정상적으로 작동합니다
  • 불필요한 코드를 제거했습니다

💬 추가 설명 or 리뷰 포인트 (선택)

  • ..

Summary by CodeRabbit

  • 새로운 기능

    • Google OAuth에 인증 코드 교환 지원 추가 — Google 로그인을 인증 코드로 처리해 액세스/리프레시 토큰을 발급합니다.
    • 사용자 계정에 Google 리프레시 토큰 저장·갱신 기능 추가.
  • 변경사항

    • Google 로그인 흐름이 인증 코드를 요구하도록 명확화되어, 필요 시 리프레시 토큰을 자동으로 취득·연계합니다.
    • 토큰 교환 실패 시 보다 명확한 오류 응답 처리 추가.

@coderabbitai
Copy link

coderabbitai bot commented Feb 8, 2026

Walkthrough

Google OAuth 코드 교환 및 리프레시 토큰 저장 흐름을 도입합니다. 클라이언트에서 받은 authorizationCode를 여러 계층(GoogleApiManager → GoogleApi → GoogleRestClient)을 통해 Google 토큰 엔드포인트로 교환하고, 결과(GoogleTokenResponse)를 도메인 레이어로 전달해 사용자에 리프레시 토큰을 저장합니다.

Changes

Cohort / File(s) 요약
토큰 응답 모델
infra/src/main/kotlin/org/yapp/infra/external/oauth/google/response/GoogleTokenResponse.kt
Google OAuth 토큰 응답을 매핑하는 새로운 데이터 클래스 추가(access_token, refresh_token, expires_in, token_type, scope, id_token).
REST 클라이언트 레이어
infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt
exchangeAuthorizationCode(...) 메서드 추가: form-encoded POST로 Google 토큰 엔드포인트 호출 후 GoogleTokenResponse 반환. 기존 revoke 동작 유지.
API 래퍼 레이어
infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt
exchangeAuthorizationCode(...) 래핑 메서드 추가: googleRestClient 호출을 감싸 Result<GoogleTokenResponse> 반환. 관련 import 추가.
API 매니저 / 비즈니스 인터페이스
apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt
exchangeAuthorizationCode(authorizationCode: String): GoogleTokenResponse 공개 메서드 추가: 설정값(clientSecret, redirectUri, tokenUri) 검증, googleApi.exchangeAuthorizationCode 호출, 예외 매핑(AuthException) 및 성공 로깅.
인증 흐름 / 유스케이스
apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt
AuthUseCase 생성자에 GoogleApiManager 의존성 추가. Google용 authorizationCode 교환을 수행하는 fetchGoogleRefreshTokenIfNeeded 보조 메서드 추가 및 signIn 흐름에 googleRefreshToken 통합.
인증 자격증명 / DTO
apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInCredentials.kt, apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt
Google 자격증명에 authorizationCode 필드 추가 및 SocialLoginRequest 설명/유효성 흐름 수정(구글은 authorizationCode 필요).
사인인 서비스 변화
apis/src/main/kotlin/org/yapp/apis/auth/service/UserSignInService.kt
processSignIn 시그니처에 googleRefreshToken 추가. 애플/구글 리프레시 토큰 조건부 저장 순서로 업데이트 로직 확장.
리프레시 토큰 저장 DTO
apis/src/main/kotlin/org/yapp/apis/user/dto/request/SaveGoogleRefreshTokenRequest.kt
구글 리프레시 토큰 저장용 새로운 요청 DTO 추가(유효성 검사/팩토리 메서드 포함).
사용자 계정 서비스
apis/src/main/kotlin/org/yapp/apis/user/service/UserAccountService.kt
updateGoogleRefreshToken 퍼블릭 메서드 추가: SaveGoogleRefreshTokenRequest를 받아 도메인 업데이트 위임.
주의할 부분 AuthUseCase 생성자 변경과 SignInCredentials 시그니처 변경으로 호출자(테스트, DI 설정 등) 업데이트 필요. 예외 매핑과 설정값(redirectUri, clientSecret, tokenUri) 유효성 검사 로직 확인 권장.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant AuthUseCase
    participant GoogleApiManager
    participant GoogleApi
    participant GoogleRestClient
    participant GoogleOAuth as Google OAuth Server

    Client->>AuthUseCase: Sign-in request (authorizationCode)
    activate AuthUseCase
    AuthUseCase->>GoogleApiManager: fetchGoogleRefreshTokenIfNeeded(authorizationCode)
    activate GoogleApiManager
    GoogleApiManager->>GoogleApiManager: validate clientSecret/redirectUri/tokenUri
    GoogleApiManager->>GoogleApi: exchangeAuthorizationCode(code, clientId, clientSecret, redirectUri, tokenUri)
    deactivate GoogleApiManager

    activate GoogleApi
    GoogleApi->>GoogleRestClient: exchangeAuthorizationCode(code, clientId, clientSecret, redirectUri, url)
    deactivate GoogleApi

    activate GoogleRestClient
    GoogleRestClient->>GoogleOAuth: POST form (code, client_id, client_secret, redirect_uri, grant_type=authorization_code)
    activate GoogleOAuth
    GoogleOAuth-->>GoogleRestClient: GoogleTokenResponse (access_token, refresh_token, id_token, ...)
    deactivate GoogleOAuth
    GoogleRestClient-->>GoogleApi: Result<GoogleTokenResponse>
    deactivate GoogleRestClient

    activate GoogleApi
    GoogleApi-->>GoogleApiManager: Result<GoogleTokenResponse>
    deactivate GoogleApi

    activate GoogleApiManager
    GoogleApiManager->>GoogleApiManager: map/validate response, log success
    GoogleApiManager-->>AuthUseCase: GoogleTokenResponse (or null/throw)
    deactivate GoogleApiManager

    AuthUseCase->>UserSignInService: processSignIn(..., googleRefreshToken)
    deactivate AuthUseCase
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning PR 제목 'Book 469 feature/#141'은 변경 사항의 주요 내용을 설명하지 못하고 있습니다. Google OAuth 토큰 교환, 새로운 DTO 및 서비스 메서드 추가 등의 구체적인 변경 사항을 명확히 나타내지 않습니다. PR 제목을 'Add Google OAuth token exchange and refresh token management' 또는 유사한 구체적인 설명으로 변경하여 주요 변경 사항을 명확히 반영하도록 수정하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 19.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch BOOK-469-feature/#141

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt (1)

74-80: 🧹 Nitpick | 🔵 Trivial

revoke 엔드포인트 URL이 하드코딩되어 있습니다.

exchangeAuthorizationCodeexchangeIdToken은 URL을 파라미터로 받는 반면, revokehttps://oauth2.googleapis.com/revoke가 하드코딩되어 있어 일관성이 없습니다. 설정을 통해 관리하는 것이 좋습니다.

🤖 Fix all issues with AI agents
In `@apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt`:
- Around line 29-31: clientSecret이 null일 때 빈 문자열로 대체하는 대신 명시적 예외를 던져 문제를 빨리
발견하도록 수정하세요: GoogleApiManager.kt에서 googleOauthProperties.clientSecret에 대해 현재
사용중인 "?: \"\"" 패턴을 제거하고 tokenUri 처리와 동일하게 누락 시 IllegalStateException(또는 적절한 런타임
예외)으로 실패하도록 변경해 clientSecret 파라미터를 설정하는 코드(예: clientSecret = ...)가 올바른 값만 전달되게
하세요.

In `@infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt`:
- Around line 26-47: Remove the unused exchangeAuthorizationCode function from
GoogleApi.kt: delete the entire exchangeAuthorizationCode(...) declaration
(including its parameters and runCatching call) and any now-unused imports or
references; ensure there are no remaining callers (the codebase and tests use
only exchangeIdToken and GoogleApiManager), then run a quick build to confirm no
compilation or test breakage.

In
`@infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt`:
- Around line 52-72: The exchangeIdToken function in GoogleRestClient is using
the JWT-bearer grant to try to exchange an ID token at Google's token endpoint,
which is unsupported; replace this flow with a correct approach: either (A) if
you intended to exchange an authorization code, rename exchangeIdToken to
something like exchangeAuthorizationCode and change the request to use
grant_type=authorization_code with parameters code, client_id, client_secret and
redirect_uri and return GoogleTokenResponse; or (B) if you really have an ID
token from the client, remove the token-exchange call and instead validate/parse
the ID token server-side (e.g., with GoogleIdTokenVerifier or a JWT library) to
extract user info and avoid calling the token endpoint; update callers of
exchangeIdToken accordingly (or implement a new method for code exchange and a
separate method for ID token validation).

Comment on lines 26 to 47
fun exchangeAuthorizationCode(
code: String,
clientId: String,
clientSecret: String,
redirectUri: String,
tokenExchangeUrl: String,
): Result<GoogleTokenResponse> {
return runCatching {
googleRestClient.exchangeAuthorizationCode(code, clientId, clientSecret, redirectUri, tokenExchangeUrl)
}
}

fun exchangeIdToken(
idToken: String,
clientId: String,
clientSecret: String,
tokenExchangeUrl: String,
): Result<GoogleTokenResponse> {
return runCatching {
googleRestClient.exchangeIdToken(idToken, clientId, clientSecret, tokenExchangeUrl)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n 'exchangeAuthorizationCode' --type=kotlin

Repository: YAPP-Github/Reed-Server

Length of output: 484


🏁 Script executed:

rg -n 'GoogleApiManager' --type=kotlin -A 10 | head -50

Repository: YAPP-Github/Reed-Server

Length of output: 3243


🏁 Script executed:

rg -n 'exchangeIdToken|exchangeAuthorizationCode' --type=kotlin | grep -E '(Manager|Test|test)'

Repository: YAPP-Github/Reed-Server

Length of output: 179


🏁 Script executed:

# Final verification - check test files for any usage
find . -type f -name "*Test*.kt" -o -name "*test*.kt" | xargs rg -l 'exchangeAuthorizationCode' 2>/dev/null || echo "No test files found using exchangeAuthorizationCode"

Repository: YAPP-Github/Reed-Server

Length of output: 118


exchangeAuthorizationCode 메서드는 사용되지 않으므로 제거하세요.

코드베이스 검색 결과 이 메서드는 정의되기만 하고 외부에서 호출되지 않습니다. GoogleApiManager에서는 exchangeIdToken만 사용 중이며, 테스트 파일에서도 호출되지 않습니다. YAGNI 원칙에 따라 제거를 권장합니다.

🤖 Prompt for AI Agents
In `@infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt`
around lines 26 - 47, Remove the unused exchangeAuthorizationCode function from
GoogleApi.kt: delete the entire exchangeAuthorizationCode(...) declaration
(including its parameters and runCatching call) and any now-unused imports or
references; ensure there are no remaining callers (the codebase and tests use
only exchangeIdToken and GoogleApiManager), then run a quick build to confirm no
compilation or test breakage.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt (1)

57-73: 🧹 Nitpick | 🔵 Trivial

Apple과 Google의 authorizationCode 검증 로직이 중복됩니다.

두 분기의 null 체크 및 예외 처리 패턴이 동일합니다. 현재는 두 케이스뿐이라 허용 가능하지만, 향후 provider가 추가될 경우 헬퍼 함수 추출을 고려해보세요.

infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt (1)

55-61: 🧹 Nitpick | 🔵 Trivial

revoke 메서드의 URL이 하드코딩되어 있습니다.

exchangeAuthorizationCode는 URL을 파라미터로 받아 설정에서 관리하지만, revoke"https://oauth2.googleapis.com/revoke"가 직접 하드코딩되어 있습니다. 이 PR 범위 밖이지만, 향후 일관성을 위해 설정으로 외부화하는 것을 권장합니다.

apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt (1)

77-93: 🧹 Nitpick | 🔵 Trivial

Apple과 Google의 fetch 헬퍼가 동일 패턴 — 추후 통합 가능

fetchAppleRefreshTokenIfNeededfetchGoogleRefreshTokenIfNeeded가 구조적으로 동일합니다(타입 체크 → 외부 API 호출 → refreshToken 반환). 현재 2개 프로바이더이므로 큰 문제는 아니지만, UserSignInService 리팩토링과 함께 전략 패턴이나 공통 인터페이스로 통합하면 프로바이더 추가 시 이 파일의 변경을 줄일 수 있습니다.

🤖 Fix all issues with AI agents
In `@apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt`:
- Around line 50-52: onSuccess 콜백에서 전달받은 tokenResponse 파라미터가 사용되고 있지 않습니다;
GoogleApiManager.kt 내부의 .onSuccess { tokenResponse -> ... }를 찾아 파라미터 이름을 사용하지
않음을 명시하기 위해 언더스코어로 바꾸거나 파라미터를 완전히 생략하여 .onSuccess { _ -> ... } 또는 .onSuccess {
... } 형식으로 수정하세요.

In `@apis/src/main/kotlin/org/yapp/apis/auth/service/UserSignInService.kt`:
- Around line 18-45: 현재 processSignIn의 시그니처가 appleRefreshToken: String? ,
googleRefreshToken: String? 처럼 프로바이더별 nullable 파라미터로 되어 있어 프로바이더가 늘어나면 시그니처가 계속
커집니다; 이를 고치려면 FindOrCreateUserRequest를 그대로 받고 프로바이더별 refresh token을 하나의 인자(예:
sealed class SocialRefreshToken 또는 Map<String,String> socialRefreshTokens)로 통합한
뒤 processSignIn 내부에서 initialUserResponse를 받은 후 socialRefreshTokens를 검사하여 각
프로바이더에 맞는 update 메서드(userAccountService.updateAppleRefreshToken,
updateGoogleRefreshToken 등)를 호출하도록 분기 또는 반복 처리로 변경하세요; 새로운 프로바이더가 추가되면 sealed
class에 케이스를 추가하거나 맵 키만 추가하면 되도록 구현을 변경하면 됩니다.
- Around line 25-43: The code currently reassigns userResponse from
initialUserResponse after the Apple update and then uses that potentially
modified value for the Google update (symbols: userResponse,
initialUserResponse, appleRefreshToken, googleRefreshToken,
userAccountService.updateAppleRefreshToken, updateGoogleRefreshToken), which
causes two sequential DB updates if both tokens are provided; change this by
adding a defensive check that disallows both appleRefreshToken and
googleRefreshToken being non-blank (throw a clear IllegalArgumentException or
custom BadRequestException) before performing any updates, or explicitly
document and retain the dual-update behavior—implement the guard near the start
of the method to validate tokens and return/throw before calling
updateAppleRefreshToken/updateGoogleRefreshToken.

In
`@apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInCredentials.kt`:
- Around line 22-29: GoogleAuthCredentials uses a block-bodied override for
getProviderType(), which is inconsistent with KakaoAuthCredentials and
AppleAuthCredentials that use expression-body overrides; change
GoogleAuthCredentials.getProviderType() to an expression body returning
ProviderType.GOOGLE so the style matches (update the override in class
GoogleAuthCredentials to use = ProviderType.GOOGLE).

In `@apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt`:
- Around line 86-93: Verify the frontend's Google Sign-In flow includes
access_type=offline and prompt=consent so a refresh_token can be issued, and
then update fetchGoogleRefreshTokenIfNeeded to handle nullable
GoogleTokenResponse.refreshToken from
googleApiManager.exchangeAuthorizationCode: when credentials is
GoogleAuthCredentials call exchangeAuthorizationCode as now, but if
tokenResponse.refreshToken is null emit a warning via the existing logger
(include identifying info like user id or authorizationCode) and explicitly
return null (or throw a documented AuthException if your flow requires a refresh
token); reference fetchGoogleRefreshTokenIfNeeded, GoogleAuthCredentials,
googleApiManager.exchangeAuthorizationCode, and GoogleTokenResponse.refreshToken
when making the change.

In
`@apis/src/main/kotlin/org/yapp/apis/user/dto/request/SaveGoogleRefreshTokenRequest.kt`:
- Around line 21-26: The `@Schema` description for SaveGoogleRefreshTokenRequest
is inconsistent with the `@field`:NotBlank validation on googleRefreshToken and
the of() factory that creates non-null values; update the `@Schema` on
googleRefreshToken to indicate it is required/non-blank (remove "nullable if not
issued yet") and reflect that it contains a valid Google refresh token, so the
documentation matches the `@field`:NotBlank constraint and the of() factory
behavior.
- Around line 9-12: Remove the unnecessary Swagger annotation from the internal
DTO by deleting the `@Schema` annotation on the SaveGoogleRefreshTokenRequest
class; this DTO is constructed via its of() factory and not exposed as an API
request body, so simply remove the `@Schema`(...) declaration (leave class,
properties, and the of() factory intact).

Comment on lines +50 to +52
.onSuccess { tokenResponse ->
log.info { "Successfully exchanged Google authorization code for tokens" }
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

onSuccess 콜백에서 사용하지 않는 파라미터를 _로 변경하세요.

tokenResponse 파라미터가 사용되지 않습니다.

♻️ 수정 제안
-            .onSuccess { tokenResponse ->
+            .onSuccess { _ ->
                 log.info { "Successfully exchanged Google authorization code for tokens" }
             }

또는 파라미터를 완전히 생략:

-            .onSuccess { tokenResponse ->
+            .onSuccess {
                 log.info { "Successfully exchanged Google authorization code for tokens" }
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.onSuccess { tokenResponse ->
log.info { "Successfully exchanged Google authorization code for tokens" }
}
.onSuccess { _ ->
log.info { "Successfully exchanged Google authorization code for tokens" }
}
🤖 Prompt for AI Agents
In `@apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt` around
lines 50 - 52, onSuccess 콜백에서 전달받은 tokenResponse 파라미터가 사용되고 있지 않습니다;
GoogleApiManager.kt 내부의 .onSuccess { tokenResponse -> ... }를 찾아 파라미터 이름을 사용하지
않음을 명시하기 위해 언더스코어로 바꾸거나 파라미터를 완전히 생략하여 .onSuccess { _ -> ... } 또는 .onSuccess {
... } 형식으로 수정하세요.

Comment on lines 18 to +45
fun processSignIn(
@Valid request: FindOrCreateUserRequest, appleRefreshToken: String?
@Valid request: FindOrCreateUserRequest,
appleRefreshToken: String?,
googleRefreshToken: String?
): CreateUserResponse {
val initialUserResponse = userAccountService.findOrCreateUser(request)

return appleRefreshToken.takeIf { !it.isNullOrBlank() }
?.let { token ->
userAccountService.updateAppleRefreshToken(
SaveAppleRefreshTokenRequest.of(
initialUserResponse, token
)
var userResponse = initialUserResponse

// Update Apple refresh token if provided
if (!appleRefreshToken.isNullOrBlank()) {
userResponse = userAccountService.updateAppleRefreshToken(
SaveAppleRefreshTokenRequest.of(
userResponse, appleRefreshToken
)
)
}

// Update Google refresh token if provided
if (!googleRefreshToken.isNullOrBlank()) {
userResponse = userAccountService.updateGoogleRefreshToken(
SaveGoogleRefreshTokenRequest.of(
userResponse, googleRefreshToken
)
} ?: initialUserResponse
)
}

return userResponse
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

확장성을 고려한 리팩토링 제안: 소셜 프로바이더 추가 시 파라미터 증가 문제

현재 Apple/Google 두 개 프로바이더에 대해 각각 nullable 파라미터를 추가하는 방식은 명확하고 동작에 문제가 없습니다. 다만, 향후 프로바이더가 추가될 때마다 processSignIn의 시그니처가 계속 늘어나게 됩니다.

프로바이더별 refresh token을 하나의 sealed class 또는 map 기반 파라미터로 통합하면 확장 시 시그니처 변경 없이 대응할 수 있습니다.

예시: sealed class 활용
sealed class SocialRefreshToken {
    data class Apple(val refreshToken: String) : SocialRefreshToken()
    data class Google(val refreshToken: String) : SocialRefreshToken()
}

fun processSignIn(
    `@Valid` request: FindOrCreateUserRequest,
    socialRefreshToken: SocialRefreshToken?
): CreateUserResponse {
    val initialUserResponse = userAccountService.findOrCreateUser(request)
    return when (socialRefreshToken) {
        is SocialRefreshToken.Apple -> userAccountService.updateAppleRefreshToken(
            SaveAppleRefreshTokenRequest.of(initialUserResponse, socialRefreshToken.refreshToken)
        )
        is SocialRefreshToken.Google -> userAccountService.updateGoogleRefreshToken(
            SaveGoogleRefreshTokenRequest.of(initialUserResponse, socialRefreshToken.refreshToken)
        )
        null -> initialUserResponse
    }
}
🤖 Prompt for AI Agents
In `@apis/src/main/kotlin/org/yapp/apis/auth/service/UserSignInService.kt` around
lines 18 - 45, 현재 processSignIn의 시그니처가 appleRefreshToken: String? ,
googleRefreshToken: String? 처럼 프로바이더별 nullable 파라미터로 되어 있어 프로바이더가 늘어나면 시그니처가 계속
커집니다; 이를 고치려면 FindOrCreateUserRequest를 그대로 받고 프로바이더별 refresh token을 하나의 인자(예:
sealed class SocialRefreshToken 또는 Map<String,String> socialRefreshTokens)로 통합한
뒤 processSignIn 내부에서 initialUserResponse를 받은 후 socialRefreshTokens를 검사하여 각
프로바이더에 맞는 update 메서드(userAccountService.updateAppleRefreshToken,
updateGoogleRefreshToken 등)를 호출하도록 분기 또는 반복 처리로 변경하세요; 새로운 프로바이더가 추가되면 sealed
class에 케이스를 추가하거나 맵 키만 추가하면 되도록 구현을 변경하면 됩니다.

Comment on lines +25 to +43
var userResponse = initialUserResponse

// Update Apple refresh token if provided
if (!appleRefreshToken.isNullOrBlank()) {
userResponse = userAccountService.updateAppleRefreshToken(
SaveAppleRefreshTokenRequest.of(
userResponse, appleRefreshToken
)
)
}

// Update Google refresh token if provided
if (!googleRefreshToken.isNullOrBlank()) {
userResponse = userAccountService.updateGoogleRefreshToken(
SaveGoogleRefreshTokenRequest.of(
userResponse, googleRefreshToken
)
} ?: initialUserResponse
)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

var 재할당 체인이 정확하게 동작하는지 확인 필요

현재 userResponse가 Apple 토큰 업데이트 결과를 받고, 이어서 Google 토큰 업데이트에도 동일한 userResponse를 사용합니다. 실제 로그인 플로우에서는 Apple 또는 Google 중 하나만 non-null이므로 문제 없지만, 만약 두 토큰이 동시에 전달되면 두 번의 DB 업데이트가 순차 실행됩니다.

이 동작이 의도된 것인지 확인하거나, 방어적으로 둘 다 전달된 경우를 차단하는 것을 고려해 주세요.

방어 코드 예시
 fun processSignIn(
     `@Valid` request: FindOrCreateUserRequest,
     appleRefreshToken: String?,
     googleRefreshToken: String?
 ): CreateUserResponse {
+    require(!(appleRefreshToken != null && googleRefreshToken != null)) {
+        "Apple과 Google refresh token이 동시에 전달될 수 없습니다."
+    }
     val initialUserResponse = userAccountService.findOrCreateUser(request)
🤖 Prompt for AI Agents
In `@apis/src/main/kotlin/org/yapp/apis/auth/service/UserSignInService.kt` around
lines 25 - 43, The code currently reassigns userResponse from
initialUserResponse after the Apple update and then uses that potentially
modified value for the Google update (symbols: userResponse,
initialUserResponse, appleRefreshToken, googleRefreshToken,
userAccountService.updateAppleRefreshToken, updateGoogleRefreshToken), which
causes two sequential DB updates if both tokens are provided; change this by
adding a defensive check that disallows both appleRefreshToken and
googleRefreshToken being non-blank (throw a clear IllegalArgumentException or
custom BadRequestException) before performing any updates, or explicitly
document and retain the dual-update behavior—implement the guard near the start
of the method to validate tokens and return/throw before calling
updateAppleRefreshToken/updateGoogleRefreshToken.

Comment on lines 22 to 29
data class GoogleAuthCredentials(
val idToken: String,
val authorizationCode: String,
) : SignInCredentials() {
override fun getProviderType(): ProviderType {
return ProviderType.GOOGLE
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

getProviderType() 스타일이 다른 credentials 클래스와 일관되지 않습니다.

KakaoAuthCredentialsAppleAuthCredentials는 expression body(= ProviderType.X)를 사용하지만, GoogleAuthCredentials만 block body를 사용합니다.

♻️ 일관성을 위한 수정 제안
 data class GoogleAuthCredentials(
     val idToken: String,
     val authorizationCode: String,
 ) : SignInCredentials() {
-    override fun getProviderType(): ProviderType {
-        return ProviderType.GOOGLE
-    }
+    override fun getProviderType(): ProviderType = ProviderType.GOOGLE
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
data class GoogleAuthCredentials(
val idToken: String,
val authorizationCode: String,
) : SignInCredentials() {
override fun getProviderType(): ProviderType {
return ProviderType.GOOGLE
}
}
data class GoogleAuthCredentials(
val idToken: String,
val authorizationCode: String,
) : SignInCredentials() {
override fun getProviderType(): ProviderType = ProviderType.GOOGLE
}
🤖 Prompt for AI Agents
In `@apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInCredentials.kt`
around lines 22 - 29, GoogleAuthCredentials uses a block-bodied override for
getProviderType(), which is inconsistent with KakaoAuthCredentials and
AppleAuthCredentials that use expression-body overrides; change
GoogleAuthCredentials.getProviderType() to an expression body returning
ProviderType.GOOGLE so the style matches (update the override in class
GoogleAuthCredentials to use = ProviderType.GOOGLE).

Comment on lines +86 to +93
private fun fetchGoogleRefreshTokenIfNeeded(credentials: SignInCredentials): String? {
if (credentials is GoogleAuthCredentials) {
val tokenResponse = googleApiManager.exchangeAuthorizationCode(credentials.authorizationCode)
return tokenResponse.refreshToken
}

return null
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for GoogleApiManager implementation
echo "=== Searching for GoogleApiManager.kt ==="
fd "GoogleApiManager.kt" -type f

echo ""
echo "=== Searching for GoogleRestClient.kt ==="
fd "GoogleRestClient.kt" -type f

echo ""
echo "=== Searching for GoogleTokenResponse.kt ==="
fd "GoogleTokenResponse.kt" -type f

echo ""
echo "=== Searching for related API manager files ==="
fd -e ".kt" | grep -i "google.*api\|api.*manager" | head -20

Repository: YAPP-Github/Reed-Server

Length of output: 1050


🌐 Web query:

Google OAuth2 refresh_token response requirements access_type offline prompt consent

💡 Result:

To receive a refresh_token from Google’s OAuth 2.0 token response (when exchanging an authorization code):

  • Request offline access in the authorization request by setting access_type=offline. This instructs Google to return a refresh token on the first code exchange for that user+client. [1][2]
  • Expect refresh_token only on the first authorization for a given user ↔ OAuth client pairing; subsequent authorizations are often auto-approved and won’t include a new refresh token unless the user revoked access. [1][3]
  • If you must obtain a new refresh token (e.g., you lost it), force a new consent screen with prompt=consent in the authorization request. (prompt=select_account alone is not sufficient.) [1][4]

Notes:

  • include_granted_scopes=true is for incremental authorization; it’s commonly recommended but isn’t the key requirement for getting a refresh token. [1]

🏁 Script executed:

#!/bin/bash
echo "=== GoogleApiManager.kt ==="
cat "apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt"

echo ""
echo "=== GoogleApi.kt ==="
cat "infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt"

echo ""
echo "=== Searching for GoogleTokenResponse ==="
fd -e "kt" -path "*/google/*" | xargs grep -l "GoogleTokenResponse\|class.*Token.*Response" 2>/dev/null | head -10

Repository: YAPP-Github/Reed-Server

Length of output: 5652


🏁 Script executed:

#!/bin/bash
# Search for GoogleTokenResponse definition
echo "=== Searching for token response classes ==="
rg "data class|class.*TokenResponse" -A 5 -i --type kt | grep -i google -A 5 | head -40

Repository: YAPP-Github/Reed-Server

Length of output: 155


🏁 Script executed:

#!/bin/bash
echo "=== Searching for GoogleTokenResponse definition ==="
find . -name "*.kt" -path "*/google/*" -exec grep -l "GoogleTokenResponse" {} \;

echo ""
echo "=== Searching for GoogleRestClient ==="
find . -name "*.kt" | xargs grep -l "GoogleRestClient" | head -5

Repository: YAPP-Github/Reed-Server

Length of output: 562


🏁 Script executed:

#!/bin/bash
echo "=== Find all files in google folder ==="
find . -path "*/google/*" -name "*.kt" | head -20

echo ""
echo "=== Search for response classes in google oauth folder ==="
find . -path "*/google/*" -name "*.kt" -exec grep -H "data class\|class.*Response" {} \;

Repository: YAPP-Github/Reed-Server

Length of output: 824


🏁 Script executed:

#!/bin/bash
echo "=== GoogleTokenResponse.kt ==="
cat "./infra/src/main/kotlin/org/yapp/infra/external/oauth/google/response/GoogleTokenResponse.kt"

echo ""
echo "=== GoogleRestClient.kt ==="
cat "./infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt"

Repository: YAPP-Github/Reed-Server

Length of output: 2745


🏁 Script executed:

#!/bin/bash
echo "=== Searching for GoogleOauthProperties ==="
find . -name "*.kt" | xargs grep -l "GoogleOauthProperties"

echo ""
echo "=== Searching for authorization request construction ==="
find . -name "*.kt" | xargs grep -B 5 -A 5 "access_type\|prompt.*consent" | head -30

Repository: YAPP-Github/Reed-Server

Length of output: 453


🏁 Script executed:

#!/bin/bash
echo "=== GoogleOauthProperties.kt ==="
cat "./apis/src/main/kotlin/org/yapp/apis/config/GoogleOauthProperties.kt"

echo ""
echo "=== Searching for authorization request/login flow ==="
find . -name "*.kt" | xargs grep -l "authorize\|authorization.*url\|oauth.*login" | head -10

Repository: YAPP-Github/Reed-Server

Length of output: 647


🏁 Script executed:

#!/bin/bash
echo "=== Search for Google OAuth authorize endpoint construction ==="
rg -i "authorize|/o/oauth" --max-count 20

echo ""
echo "=== Search for OAuth login controller ==="
find . -name "*Controller.kt" | xargs grep -l "google\|oauth" | head -5

Repository: YAPP-Github/Reed-Server

Length of output: 1772


🏁 Script executed:

#!/bin/bash
echo "=== Search for authentication controller ==="
find . -path "*/auth/*" -name "*Controller.kt" -o -path "*/auth/*" -name "*Endpoint.kt"

echo ""
echo "=== Search for Google authorization URL construction ==="
rg -i "google.*authorize|createAuthorizationRequestUri|getAuthorizationUri" --max-count 20

echo ""
echo "=== Check AuthController ==="
find . -name "*AuthController.kt" -o -name "*LoginController.kt"

Repository: YAPP-Github/Reed-Server

Length of output: 341


🏁 Script executed:

#!/bin/bash
echo "=== Look at auth usecase and controller ==="
find ./apis/src/main/kotlin/org/yapp/apis/auth -name "*.kt" | head -20

echo ""
echo "=== Search for Google OAuth login/authorize endpoint ==="
rg -B 3 -A 10 "fun.*google.*login|fun.*sign.*google" -i --max-count 15

Repository: YAPP-Github/Reed-Server

Length of output: 4513


🏁 Script executed:

#!/bin/bash
echo "=== GoogleSignInStrategy.kt ==="
cat "./apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleSignInStrategy.kt"

echo ""
echo "=== AuthController.kt ==="
cat "./apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthController.kt"

Repository: YAPP-Github/Reed-Server

Length of output: 3874


🏁 Script executed:

#!/bin/bash
echo "=== Check SocialLoginRequest to understand credentials ==="
cat "./apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt"

echo ""
echo "=== Search for GoogleAuthCredentials ==="
find . -name "*.kt" | xargs grep -l "GoogleAuthCredentials" | head -5

Repository: YAPP-Github/Reed-Server

Length of output: 3400


🏁 Script executed:

#!/bin/bash
echo "=== SignInCredentials.kt and related ==="
cat "./apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInCredentials.kt"

echo ""
echo "=== Full AuthUseCase.kt to understand context ==="
cat "./apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt"

Repository: YAPP-Github/Reed-Server

Length of output: 5116


Google OAuth 토큰 교환 시 에러 핸들링과 refresh_token 가능성 검토 필요

현재 코드는 googleApiManager.exchangeAuthorizationCode에서 AuthException으로 에러를 변환하고 있어 기본 에러 처리는 구현되어 있습니다. 다만 두 가지 확인이 필요합니다:

  1. 클라이언트 인증 요청 확인 - Google OAuth에서 refresh_token을 받으려면 초기 인증 요청(authorization request)에 access_type=offline&prompt=consent이 포함되어야 합니다. 현재 HTML의 Google Sign-In 라이브러리가 이 파라미터를 설정하고 있는지 확인하세요.

  2. null refresh_token 처리 - GoogleTokenResponse.refreshToken은 nullable입니다. 클라이언트가 오프라인 접근을 요청하지 않으면 null이 반환될 수 있으며, 현재 코드는 이를 그대로 전달합니다. 이것이 의도된 동작인지, 아니면 null인 경우 로그 경고를 추가해야 하는지 검토 부탁드립니다.

🤖 Prompt for AI Agents
In `@apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt` around lines
86 - 93, Verify the frontend's Google Sign-In flow includes access_type=offline
and prompt=consent so a refresh_token can be issued, and then update
fetchGoogleRefreshTokenIfNeeded to handle nullable
GoogleTokenResponse.refreshToken from
googleApiManager.exchangeAuthorizationCode: when credentials is
GoogleAuthCredentials call exchangeAuthorizationCode as now, but if
tokenResponse.refreshToken is null emit a warning via the existing logger
(include identifying info like user id or authorizationCode) and explicitly
return null (or throw a documented AuthException if your flow requires a refresh
token); reference fetchGoogleRefreshTokenIfNeeded, GoogleAuthCredentials,
googleApiManager.exchangeAuthorizationCode, and GoogleTokenResponse.refreshToken
when making the change.

Comment on lines +9 to +12
@Schema(
name = "SaveGoogleRefreshTokenRequest",
description = "Request DTO for saving Google refresh token with user ID and authorization code"
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

내부 전용 DTO에 @Schema 어노테이션이 불필요해 보입니다.

이 클래스는 API 요청 바디로 직접 노출되지 않고, of() 팩토리를 통해 내부적으로만 생성됩니다. Swagger 문서에 노출할 필요가 없다면 @Schema 어노테이션을 제거하는 것이 좋습니다.

🤖 Prompt for AI Agents
In
`@apis/src/main/kotlin/org/yapp/apis/user/dto/request/SaveGoogleRefreshTokenRequest.kt`
around lines 9 - 12, Remove the unnecessary Swagger annotation from the internal
DTO by deleting the `@Schema` annotation on the SaveGoogleRefreshTokenRequest
class; this DTO is constructed via its of() factory and not exposed as an API
request body, so simply remove the `@Schema`(...) declaration (leave class,
properties, and the of() factory intact).

Comment on lines +21 to +26
@field:Schema(
description = "Google refresh token, nullable if not issued yet",
example = "1//0g_xxxxxxxxxxxxxxxxxxxxxx"
)
@field:NotBlank(message = "googleRefreshToken must not be blank")
val googleRefreshToken: String? = null
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

@Schema description과 @field:NotBlank 검증이 모순됩니다.

Line 22의 description에는 "nullable if not issued yet"이라고 되어 있지만, Line 25의 @field:NotBlank는 blank/null을 허용하지 않습니다. 이 DTO는 of() 팩토리 메서드를 통해 non-null 값으로만 생성되므로, description을 수정하는 것이 맞습니다.

📝 description 수정 제안
     `@field`:Schema(
-        description = "Google refresh token, nullable if not issued yet",
+        description = "Google refresh token",
         example = "1//0g_xxxxxxxxxxxxxxxxxxxxxx"
     )
🤖 Prompt for AI Agents
In
`@apis/src/main/kotlin/org/yapp/apis/user/dto/request/SaveGoogleRefreshTokenRequest.kt`
around lines 21 - 26, The `@Schema` description for SaveGoogleRefreshTokenRequest
is inconsistent with the `@field`:NotBlank validation on googleRefreshToken and
the of() factory that creates non-null values; update the `@Schema` on
googleRefreshToken to indicate it is required/non-blank (remove "nullable if not
issued yet") and reflect that it contains a valid Google refresh token, so the
documentation matches the `@field`:NotBlank constraint and the of() factory
behavior.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 8, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
2.1% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@minwoo1999 minwoo1999 merged commit 64d8d40 into develop Feb 8, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant