Conversation
📝 WalkthroughWalkthrough사용자 엔티티에서 OAuth 관련 필드(provider, providerId, appleRefreshToken)를 제거하고 새로운 UserOAuthAccount 엔티티로 마이그레이션합니다. 리포지토리, 서비스, OAuth 핸들러를 일관되게 업데이트하고 데이터베이스 스키마를 변경하는 포괄적인 리팩토링입니다. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java (1)
175-180: 🧹 Nitpick | 🔵 Trivial[LEVEL: low] saveAndFlush 사용으로 단순화 가능
문제:
save()후 별도로entityManager.flush()호출 → 영향: 코드 복잡도 증가 → 제안:UserOAuthAccountService.saveWithConflictHandling처럼saveAndFlush()사용으로 일관성 확보.♻️ 수정 제안
try { - userOAuthAccountRepository.save(UserOAuthAccount.of(user, provider, providerId, oauthEmail, null)); - entityManager.flush(); + userOAuthAccountRepository.saveAndFlush(UserOAuthAccount.of(user, provider, providerId, oauthEmail, null)); } catch (DataIntegrityViolationException e) { throw CustomException.of(ApiResponseCode.OAUTH_PROVIDER_ALREADY_LINKED); }이 방식으로 변경 시
EntityManager의존성도 제거 가능합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java` around lines 175 - 180, Replace the separate userOAuthAccountRepository.save(...) followed by entityManager.flush() with a single userOAuthAccountRepository.saveAndFlush(...) call (or delegate to a service method like UserOAuthAccountService.saveWithConflictHandling that calls saveAndFlush) and keep the existing DataIntegrityViolationException catch throwing CustomException.of(ApiResponseCode.OAUTH_PROVIDER_ALREADY_LINKED); this lets you remove the EntityManager dependency from OAuthLoginHelper and simplifies the flow while preserving conflict handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.java`:
- Around line 60-61: The appleRefreshToken field in UserOAuthAccount is stored
in plaintext; change it to use JPA field-level encryption by implementing an
AttributeConverter (e.g., AppleTokenAttributeConverter) that encrypts on
convertToDatabaseColumn and decrypts on convertToEntityAttribute, then annotate
the appleRefreshToken field with `@Convert`(converter =
AppleTokenAttributeConverter.class); ensure the converter uses a secure key
management source (vault/KMS or environment-provided key) and handles nulls and
exceptions safely so tokens are encrypted at rest and transparently decrypted
for application use.
In
`@src/main/java/gg/agit/konect/domain/user/repository/UserOAuthAccountRepository.java`:
- Around line 49-59: The current repository method
UserOAuthAccountRepository.findUserByOauthEmailAndProvider(...) returns
Optional<User> while the underlying user_oauth_account table does not enforce
uniqueness on (provider, oauth_email), causing possible ambiguous matches; fix
by either (A) switching to a providerId-based lookup: change the query and
method signature to use the providerId field (e.g.,
findUserByProviderAndProviderId(`@Param`("provider") Provider provider,
`@Param`("providerId") String providerId)) and update callers accordingly, or (B)
preserve oauthEmail lookup but make it safe: change the repository method to
return List<User> (e.g., findUsersByOauthEmailAndProvider) and handle multiple
results, or enforce DB uniqueness by adding a unique constraint on (provider,
oauth_email) and then keep the Optional<User> signature—apply one of these
remedies and update tests/consuming code to match.
In
`@src/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.java`:
- Around line 179-182: The getPrimaryOAuthAccount method is nondeterministic
because userOAuthAccountRepository.findAllByUserId lacks an ORDER BY; update
data access to guarantee the earliest-linked account is returned by either
adding a repository method like findFirstByUserIdOrderByIdAsc(Integer userId) or
modifying the existing query in UserOAuthAccountRepository to include ORDER BY
id ASC and then call that ordered method from getPrimaryOAuthAccount so it
returns the first (oldest) account deterministically.
In `@src/main/java/gg/agit/konect/domain/user/service/UserService.java`:
- Around line 223-238: Replace the duplicate calls to
userOAuthAccountService.getPrimaryOAuthAccount by reusing the first result: keep
the initial UserOAuthAccount primaryAccount from the first call, use its
appleRefreshToken for appleTokenRevocationService.revoke if present, and derive
providerName from that same primaryAccount (falling back to "UNKNOWN") when
creating UserWithdrawnEvent.from; remove the second getPrimaryOAuthAccount call
and the primaryAccountForEvent variable so only one DB lookup occurs.
In
`@src/main/resources/db/migration/V45__add_apple_refresh_token_to_user_oauth_account.sql`:
- Around line 15-24: The INSERT in
V45__add_apple_refresh_token_to_user_oauth_account.sql currently only checks for
duplicates by (user_id, provider) which can violate the user_oauth_account
unique constraint on (provider, provider_id) for re-registered users; fix by
narrowing the backfill to active users or by excluding existing (provider,
provider_id) pairs — e.g., add AND u.deleted_at IS NULL to the WHERE clause or
change the NOT EXISTS subquery to also check ua.provider_id = u.provider_id
(WHERE ua.user_id = u.id AND ua.provider = u.provider AND ua.provider_id =
u.provider_id) so no existing (provider, provider_id) rows are inserted.
---
Outside diff comments:
In `@src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java`:
- Around line 175-180: Replace the separate userOAuthAccountRepository.save(...)
followed by entityManager.flush() with a single
userOAuthAccountRepository.saveAndFlush(...) call (or delegate to a service
method like UserOAuthAccountService.saveWithConflictHandling that calls
saveAndFlush) and keep the existing DataIntegrityViolationException catch
throwing CustomException.of(ApiResponseCode.OAUTH_PROVIDER_ALREADY_LINKED); this
lets you remove the EntityManager dependency from OAuthLoginHelper and
simplifies the flow while preserving conflict handling.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 228a4101-89e3-49a3-8777-627fb462115e
📒 Files selected for processing (15)
src/main/java/gg/agit/konect/domain/user/model/User.javasrc/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.javasrc/main/java/gg/agit/konect/domain/user/repository/UserOAuthAccountRepository.javasrc/main/java/gg/agit/konect/domain/user/repository/UserRepository.javasrc/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.javasrc/main/java/gg/agit/konect/domain/user/service/UserService.javasrc/main/java/gg/agit/konect/global/auth/oauth/OAuth2LoginSuccessHandler.javasrc/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.javasrc/main/java/gg/agit/konect/infrastructure/oauth/AppleOAuthServiceImpl.javasrc/main/java/gg/agit/konect/infrastructure/oauth/GoogleOAuthServiceImpl.javasrc/main/java/gg/agit/konect/infrastructure/oauth/KakaoOAuthServiceImpl.javasrc/main/java/gg/agit/konect/infrastructure/oauth/NaverOAuthServiceImpl.javasrc/main/resources/db/migration/V45__add_apple_refresh_token_to_user_oauth_account.sqlsrc/main/resources/db/migration/V46__remove_oauth_columns_from_users.sqlsrc/test/java/gg/agit/konect/support/fixture/UserFixture.java
💤 Files with no reviewable changes (1)
- src/test/java/gg/agit/konect/support/fixture/UserFixture.java
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
src/main/java/**/*.java
⚙️ CodeRabbit configuration file
src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.
- 코멘트는 반드시 한국어로 작성한다.
- 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
- 각 코멘트 첫 줄에 심각도를
[LEVEL: high|medium|low]형식으로 반드시 표기한다.- 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
- 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
- 가능하면 재현 조건 및 실패 시나리오도 포함한다.
- 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
- 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
- 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
- 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
- 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.
Files:
src/main/java/gg/agit/konect/domain/user/service/UserService.javasrc/main/java/gg/agit/konect/global/auth/oauth/OAuth2LoginSuccessHandler.javasrc/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.javasrc/main/java/gg/agit/konect/domain/user/model/User.javasrc/main/java/gg/agit/konect/domain/user/repository/UserOAuthAccountRepository.javasrc/main/java/gg/agit/konect/infrastructure/oauth/GoogleOAuthServiceImpl.javasrc/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.javasrc/main/java/gg/agit/konect/infrastructure/oauth/KakaoOAuthServiceImpl.javasrc/main/java/gg/agit/konect/infrastructure/oauth/NaverOAuthServiceImpl.javasrc/main/java/gg/agit/konect/domain/user/repository/UserRepository.javasrc/main/java/gg/agit/konect/infrastructure/oauth/AppleOAuthServiceImpl.javasrc/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java
**/*
⚙️ CodeRabbit configuration file
**/*: 공통 리뷰 톤 가이드:
- 모든 코멘트는 첫 줄에
[LEVEL: ...]태그를 포함한다.- 과장된 표현 없이 사실 기반으로 작성한다.
- 한 코멘트에는 하나의 이슈만 다룬다.
- 코드 예시가 필요하면 최소 수정 예시를 제시한다.
- 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.
Files:
src/main/java/gg/agit/konect/domain/user/service/UserService.javasrc/main/resources/db/migration/V45__add_apple_refresh_token_to_user_oauth_account.sqlsrc/main/java/gg/agit/konect/global/auth/oauth/OAuth2LoginSuccessHandler.javasrc/main/resources/db/migration/V46__remove_oauth_columns_from_users.sqlsrc/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.javasrc/main/java/gg/agit/konect/domain/user/model/User.javasrc/main/java/gg/agit/konect/domain/user/repository/UserOAuthAccountRepository.javasrc/main/java/gg/agit/konect/infrastructure/oauth/GoogleOAuthServiceImpl.javasrc/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.javasrc/main/java/gg/agit/konect/infrastructure/oauth/KakaoOAuthServiceImpl.javasrc/main/java/gg/agit/konect/infrastructure/oauth/NaverOAuthServiceImpl.javasrc/main/java/gg/agit/konect/domain/user/repository/UserRepository.javasrc/main/java/gg/agit/konect/infrastructure/oauth/AppleOAuthServiceImpl.javasrc/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java
src/main/resources/db/migration/**/*.sql
⚙️ CodeRabbit configuration file
src/main/resources/db/migration/**/*.sql: Flyway 마이그레이션 리뷰 규칙:
- 버전 파일명 규칙(V{number}__{description}.sql) 위반 여부를 우선 확인한다.
- 이미 배포된 마이그레이션 수정/재번호 부여 위험이 있으면 반드시 차단 코멘트를 남긴다.
- 파괴적 변경(drop, rename 등)은 롤백 가능성과 운영 영향 관점에서 검토한다.
Files:
src/main/resources/db/migration/V45__add_apple_refresh_token_to_user_oauth_account.sqlsrc/main/resources/db/migration/V46__remove_oauth_columns_from_users.sql
🔇 Additional comments (2)
src/main/java/gg/agit/konect/domain/user/model/User.java (1)
80-81: LGTM!
@OneToMany관계가LAZYfetch로 적절히 설정되었고, 서비스 레이어에서UserOAuthAccountRepository를 통해 명시적으로 조회하는 패턴과 일관성이 있습니다.final키워드로 컬렉션 재할당 방지도 좋습니다.src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java (1)
19-25: LGTM!
findById쿼리가id와deletedAt IS NULL조건으로 정확히 구성되어 있습니다. Provider 관련 쿼리 제거는 OAuth 계정 분리 리팩토링 목적에 부합합니다.
src/main/java/gg/agit/konect/domain/user/repository/UserOAuthAccountRepository.java
Show resolved
Hide resolved
src/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.java
Show resolved
Hide resolved
src/main/resources/db/migration/V46__add_apple_refresh_token_to_user_oauth_account.sql
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java (1)
50-72:⚠️ Potential issue | 🔴 Critical[LEVEL: high] Line 72는 이메일 fallback에서
restoreWithdrawnByEmail을 우회해 빈 이메일도 그대로 조회합니다.
영향: Apple처럼 이메일이 누락될 수 있는 로그인에서 잘못된 사용자 매칭이나 조회 예외가 날 수 있고, 복구 가능 기간의 탈퇴 계정도 정상 복구되지 않습니다.
제안:StringUtils.hasText(email)를 먼저 확인하고restoreWithdrawnByEmail(provider, email)을 선호한 뒤, 없을 때만 이메일 기반 조회를 수행하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java` around lines 50 - 72, The fallback path in findUserByProvider currently queries userOAuthAccountRepository.findUserByOauthEmailAndProvider(email, provider) even when email is blank and skips restoreWithdrawnByEmail; update findUserByProvider to first check StringUtils.hasText(email), and if true call restoreWithdrawnByEmail(provider, email) and if that returns a user call ensureLinkedAccount(...) and return it; only if restoreWithdrawnByEmail returns empty then fall back to userOAuthAccountRepository.findUserByOauthEmailAndProvider(email, provider). Ensure you keep the existing linked-account logic (ensureLinkedAccount and restoreOrCleanupWithdrawnByLinkedProvider) unchanged.src/main/resources/db/migration/V43__add_user_oauth_account_table.sql (1)
1-35:⚠️ Potential issue | 🔴 Critical[LEVEL: high] V43 버전 파일을 직접 수정하면 Flyway checksum과 환경별 스키마가 갈라집니다.
이미 V43가 적용된 환경에서는 이번 변경이 재실행되지 않아provider_idNULL 허용 여부와 backfill 조건이 환경마다 달라질 수 있습니다.
이 변경은 V43 수정 대신 새 버전 마이그레이션으로ALTER TABLE과 보정 DML을 분리해 추가하세요.
As per coding guidelines,src/main/resources/db/migration/**/*.sql: 이미 배포된 마이그레이션 수정/재번호 부여 위험이 있으면 반드시 차단 코멘트를 남긴다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/resources/db/migration/V43__add_user_oauth_account_table.sql` around lines 1 - 35, Do not modify the existing V43__add_user_oauth_account_table.sql; instead create a new numbered migration that performs the schema change and a separate migration (or same new migration with ordered statements) that runs the backfill DML. Specifically, leave the current V43 (which creates table user_oauth_account and constraints fk_user_oauth_account_user, uq_user_oauth_account_provider_provider_id, uq_user_oauth_account_user_provider) untouched, and add a new migration (e.g., next version) that runs ALTER TABLE user_oauth_account ... to change provider_id nullable/constraints as needed and a following DML that inserts missing rows (the SELECT ... FROM users logic) so already-applied environments get the ALTER + backfill applied uniformly rather than editing V43. Ensure the new migration includes both the ALTER TABLE and the corrective INSERT in the correct order and references the same table/constraint names.
♻️ Duplicate comments (3)
src/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.java (1)
190-192:⚠️ Potential issue | 🟠 Major[LEVEL: medium] Line 191-192는 정렬 없는 목록의 첫 원소를 primary로 반환해 결과가 비결정적입니다.
영향: DB 실행 계획이나 데이터 적재 순서에 따라 다른 OAuth 계정이 대표 계정으로 선택되어 토큰 처리나 이벤트 기준이 흔들릴 수 있습니다.
제안: Repository에findFirstByUserIdOrderByIdAsc같은 메서드를 추가하거나 조회 쿼리에ORDER BY uoa.id ASC를 명시하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.java` around lines 190 - 192, getPrimaryOAuthAccount currently calls userOAuthAccountRepository.findAllByUserId and returns the first element of an unordered list, making the primary selection non-deterministic; change this to use an ordered, single-row lookup such as adding a repository method like findFirstByUserIdOrderByIdAsc (or a query with ORDER BY uoa.id ASC LIMIT 1) in UserOAuthAccountRepository and update getPrimaryOAuthAccount to call that new method and return null when absent so the primary OAuth account selection is deterministic.src/main/resources/db/migration/V45__add_apple_refresh_token_to_user_oauth_account.sql (1)
15-23:⚠️ Potential issue | 🔴 Critical[LEVEL: high] Line 15-23의 보정 INSERT는
(user_id, provider)만 보고 중복을 걸러서(provider, provider_id)유니크 제약과 충돌할 수 있습니다.
탈퇴 후 같은 OAuth 계정으로 재가입한 이력이 있으면 V45가 중간에 실패하고 뒤의 V46까지 막힙니다.
보정 대상에서 이미 존재하는(provider, provider_id)조합을 추가로 제외하거나, 정책상 유지할 행을 먼저 정리한 뒤 INSERT 하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/resources/db/migration/V45__add_apple_refresh_token_to_user_oauth_account.sql` around lines 15 - 23, The INSERT in V45 into user_oauth_account only checks NOT EXISTS by (user_id, provider) and can violate the unique constraint on (provider, provider_id); update the INSERT (in src/main/resources/db/migration/V45__add_apple_refresh_token_to_user_oauth_account.sql) to also exclude rows where the (provider, provider_id) pair already exists (e.g., add an AND NOT EXISTS subquery that checks ua.provider = u.provider AND ua.provider_id = u.provider_id), or alternately add a pre-INSERT cleanup step that deterministically removes/archives conflicting rows for the (provider, provider_id) combination before running the INSERT to avoid failing the migration.src/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.java (1)
60-61:⚠️ Potential issue | 🟠 Major[LEVEL: medium] Line 60-61은 Apple refresh token을 평문으로 저장해 DB 유출 시 바로 재사용 가능한 자격증명이 노출됩니다.
영향: 토큰 탈취가 곧 계정 재연동이나 세션 악용으로 이어질 수 있어 사고 범위가 커집니다.
제안: 저장 시 암호화하거나 별도 비밀 저장소로 분리해 at-rest 보호를 적용하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.java` around lines 60 - 61, The appleRefreshToken field in UserOAuthAccount is stored in plaintext; update persistence to protect it at-rest by either encrypting the value with a reversible encryption layer or moving the secret to a secure secret store and persisting only a reference. Implement this by adding a JPA AttributeConverter (e.g., AppleTokenEncryptor) and annotate the appleRefreshToken field with `@Convert`(converter = AppleTokenEncryptor.class) or change setAppleRefreshToken/getAppleRefreshToken to call the encryption/decryption utility, or alternatively store a secretId from your secret manager instead of the raw token and add logic to fetch the token at runtime; ensure encryption keys or secret-store credentials are managed outside source control.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@src/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.java`:
- Around line 128-144: validatePrimaryProviderOwnership currently throws
OAUTH_PROVIDER_ALREADY_LINKED even when the existingAccount.getProviderId() is
null/blank, blocking the auto-fill logic; update the second check
(userOAuthAccountRepository.findByUserIdAndProvider(...).ifPresent(existingAccount
-> { ... })) to only throw when existingAccount.getProviderId() is non-empty and
not equal to the incoming providerId (i.e., skip the mismatch exception if
existingAccount.getProviderId() is null/blank so Line 108-119 can perform the
auto-fill). Reference: validatePrimaryProviderOwnership,
userOAuthAccountRepository.findByUserIdAndProvider, existingAccount,
getProviderId(), providerId.
---
Outside diff comments:
In `@src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java`:
- Around line 50-72: The fallback path in findUserByProvider currently queries
userOAuthAccountRepository.findUserByOauthEmailAndProvider(email, provider) even
when email is blank and skips restoreWithdrawnByEmail; update findUserByProvider
to first check StringUtils.hasText(email), and if true call
restoreWithdrawnByEmail(provider, email) and if that returns a user call
ensureLinkedAccount(...) and return it; only if restoreWithdrawnByEmail returns
empty then fall back to
userOAuthAccountRepository.findUserByOauthEmailAndProvider(email, provider).
Ensure you keep the existing linked-account logic (ensureLinkedAccount and
restoreOrCleanupWithdrawnByLinkedProvider) unchanged.
In `@src/main/resources/db/migration/V43__add_user_oauth_account_table.sql`:
- Around line 1-35: Do not modify the existing
V43__add_user_oauth_account_table.sql; instead create a new numbered migration
that performs the schema change and a separate migration (or same new migration
with ordered statements) that runs the backfill DML. Specifically, leave the
current V43 (which creates table user_oauth_account and constraints
fk_user_oauth_account_user, uq_user_oauth_account_provider_provider_id,
uq_user_oauth_account_user_provider) untouched, and add a new migration (e.g.,
next version) that runs ALTER TABLE user_oauth_account ... to change provider_id
nullable/constraints as needed and a following DML that inserts missing rows
(the SELECT ... FROM users logic) so already-applied environments get the ALTER
+ backfill applied uniformly rather than editing V43. Ensure the new migration
includes both the ALTER TABLE and the corrective INSERT in the correct order and
references the same table/constraint names.
---
Duplicate comments:
In `@src/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.java`:
- Around line 60-61: The appleRefreshToken field in UserOAuthAccount is stored
in plaintext; update persistence to protect it at-rest by either encrypting the
value with a reversible encryption layer or moving the secret to a secure secret
store and persisting only a reference. Implement this by adding a JPA
AttributeConverter (e.g., AppleTokenEncryptor) and annotate the
appleRefreshToken field with `@Convert`(converter = AppleTokenEncryptor.class) or
change setAppleRefreshToken/getAppleRefreshToken to call the
encryption/decryption utility, or alternatively store a secretId from your
secret manager instead of the raw token and add logic to fetch the token at
runtime; ensure encryption keys or secret-store credentials are managed outside
source control.
In
`@src/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.java`:
- Around line 190-192: getPrimaryOAuthAccount currently calls
userOAuthAccountRepository.findAllByUserId and returns the first element of an
unordered list, making the primary selection non-deterministic; change this to
use an ordered, single-row lookup such as adding a repository method like
findFirstByUserIdOrderByIdAsc (or a query with ORDER BY uoa.id ASC LIMIT 1) in
UserOAuthAccountRepository and update getPrimaryOAuthAccount to call that new
method and return null when absent so the primary OAuth account selection is
deterministic.
In
`@src/main/resources/db/migration/V45__add_apple_refresh_token_to_user_oauth_account.sql`:
- Around line 15-23: The INSERT in V45 into user_oauth_account only checks NOT
EXISTS by (user_id, provider) and can violate the unique constraint on
(provider, provider_id); update the INSERT (in
src/main/resources/db/migration/V45__add_apple_refresh_token_to_user_oauth_account.sql)
to also exclude rows where the (provider, provider_id) pair already exists
(e.g., add an AND NOT EXISTS subquery that checks ua.provider = u.provider AND
ua.provider_id = u.provider_id), or alternately add a pre-INSERT cleanup step
that deterministically removes/archives conflicting rows for the (provider,
provider_id) combination before running the INSERT to avoid failing the
migration.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 461115f0-3a6a-4cbc-98b0-c2bbcd8a66aa
📒 Files selected for processing (5)
src/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.javasrc/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.javasrc/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.javasrc/main/resources/db/migration/V43__add_user_oauth_account_table.sqlsrc/main/resources/db/migration/V45__add_apple_refresh_token_to_user_oauth_account.sql
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
src/main/resources/db/migration/**/*.sql
⚙️ CodeRabbit configuration file
src/main/resources/db/migration/**/*.sql: Flyway 마이그레이션 리뷰 규칙:
- 버전 파일명 규칙(V{number}__{description}.sql) 위반 여부를 우선 확인한다.
- 이미 배포된 마이그레이션 수정/재번호 부여 위험이 있으면 반드시 차단 코멘트를 남긴다.
- 파괴적 변경(drop, rename 등)은 롤백 가능성과 운영 영향 관점에서 검토한다.
Files:
src/main/resources/db/migration/V45__add_apple_refresh_token_to_user_oauth_account.sqlsrc/main/resources/db/migration/V43__add_user_oauth_account_table.sql
**/*
⚙️ CodeRabbit configuration file
**/*: 공통 리뷰 톤 가이드:
- 모든 코멘트는 첫 줄에
[LEVEL: ...]태그를 포함한다.- 과장된 표현 없이 사실 기반으로 작성한다.
- 한 코멘트에는 하나의 이슈만 다룬다.
- 코드 예시가 필요하면 최소 수정 예시를 제시한다.
- 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.
Files:
src/main/resources/db/migration/V45__add_apple_refresh_token_to_user_oauth_account.sqlsrc/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.javasrc/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.javasrc/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.javasrc/main/resources/db/migration/V43__add_user_oauth_account_table.sql
src/main/java/**/*.java
⚙️ CodeRabbit configuration file
src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.
- 코멘트는 반드시 한국어로 작성한다.
- 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
- 각 코멘트 첫 줄에 심각도를
[LEVEL: high|medium|low]형식으로 반드시 표기한다.- 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
- 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
- 가능하면 재현 조건 및 실패 시나리오도 포함한다.
- 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
- 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
- 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
- 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
- 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.
Files:
src/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.javasrc/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.javasrc/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.java
| private void validatePrimaryProviderOwnership(User user, Provider provider, String providerId) { | ||
| Integer userId = user.getId(); | ||
|
|
||
| userOAuthAccountRepository.findUserByProviderAndProviderId(provider, providerId) | ||
| .ifPresent(ownedUser -> { | ||
| if (!ownedUser.getId().equals(userId)) { | ||
| throw CustomException.of(ApiResponseCode.OAUTH_ACCOUNT_ALREADY_LINKED); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| private void validatePrimaryProviderOwnership(User user, Provider provider, String providerId) { | ||
| Integer userId = user.getId(); | ||
| userRepository.findByProviderIdAndProvider(providerId, provider) | ||
| .ifPresent(ownedUser -> { | ||
| if (!ownedUser.getId().equals(userId)) { | ||
| throw CustomException.of(ApiResponseCode.OAUTH_ACCOUNT_ALREADY_LINKED); | ||
| userOAuthAccountRepository.findByUserIdAndProvider(userId, provider) | ||
| .ifPresent(existingAccount -> { | ||
| if (!providerId.equals(existingAccount.getProviderId())) { | ||
| throw CustomException.of(ApiResponseCode.OAUTH_PROVIDER_ALREADY_LINKED); | ||
| } | ||
| }); | ||
|
|
||
| if (provider.equals(user.getProvider()) && !providerId.equals(user.getProviderId())) { | ||
| throw CustomException.of(ApiResponseCode.OAUTH_PROVIDER_ALREADY_LINKED); | ||
| } | ||
| } |
There was a problem hiding this comment.
[LEVEL: high] Line 138-142는 기존 계정의 providerId가 NULL/blank여도 즉시 불일치 예외를 던져 Line 108-119의 자동 채움 분기에 도달하지 못합니다.
영향: V43/V45로 NULL provider_id가 이관된 사용자는 이후 정상 로그인이나 재연동 시에도 식별자를 복구하지 못해 인증이 계속 실패합니다.
제안: 여기서는 다른 사용자 소유 충돌만 검사하고, 현재 사용자 계정의 NULL/blank 값 채움은 Line 108-119에서만 처리하세요.
최소 수정 예시
userOAuthAccountRepository.findByUserIdAndProvider(userId, provider)
.ifPresent(existingAccount -> {
- if (!providerId.equals(existingAccount.getProviderId())) {
+ if (StringUtils.hasText(existingAccount.getProviderId())
+ && !providerId.equals(existingAccount.getProviderId())) {
throw CustomException.of(ApiResponseCode.OAUTH_PROVIDER_ALREADY_LINKED);
}
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.java`
around lines 128 - 144, validatePrimaryProviderOwnership currently throws
OAUTH_PROVIDER_ALREADY_LINKED even when the existingAccount.getProviderId() is
null/blank, blocking the auto-fill logic; update the second check
(userOAuthAccountRepository.findByUserIdAndProvider(...).ifPresent(existingAccount
-> { ... })) to only throw when existingAccount.getProviderId() is non-empty and
not equal to the incoming providerId (i.e., skip the mismatch exception if
existingAccount.getProviderId() is null/blank so Line 108-119 can perform the
auto-fill). Reference: validatePrimaryProviderOwnership,
userOAuthAccountRepository.findByUserIdAndProvider, existingAccount,
getProviderId(), providerId.
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
src/main/resources/db/migration/V46__add_apple_refresh_token_to_user_oauth_account.sql (1)
15-23:⚠️ Potential issue | 🔴 Critical[LEVEL: high] 탈퇴 유저 보정 INSERT가 새 유니크 키와 다시 충돌할 수 있습니다.
문제: Line 15-23은 중복 여부를
(user_id, provider)로만 확인해서, 탈퇴 후 같은 OAuth 계정으로 재가입한 이력이 있으면(provider, provider_id)유니크 제약을 피하지 못합니다. 영향: 운영 데이터에 재가입 케이스가 있으면 이 마이그레이션이 중간에 실패하고 이후 버전 적용도 함께 막힙니다. 제안: 보정 대상에서 기존(provider, provider_id)조합을 추가로 제외하거나, 충돌 데이터를 먼저 정리한 뒤 INSERT 하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/resources/db/migration/V46__add_apple_refresh_token_to_user_oauth_account.sql` around lines 15 - 23, The INSERT checks duplicates only by (user_id, provider) which can still violate the (provider, provider_id) unique constraint for re-registered users; update the migration so the SELECT excludes rows where a user_oauth_account already exists with the same (provider, provider_id) (add a NOT EXISTS subquery referencing ua.provider = u.provider AND ua.provider_id = u.provider_id) or alternatively delete/merge conflicting user_oauth_account rows for those (provider, provider_id) before running the INSERT to prevent unique-key collisions; target the user_oauth_account INSERT/SELECT block and the users columns provider and provider_id when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/resources/db/migration/V43__add_user_oauth_account_table.sql`:
- Around line 33-34: Revert the changes made to
V43__add_user_oauth_account_table.sql so the original migration (including the
original WHERE clause) is restored rather than editing an already-applied
migration; then create a new migration file (e.g., V44__...) that implements the
modified condition (the change to "WHERE ua.user_id = u.id AND ua.provider =
u.provider") so environments that have already run V43 keep a consistent
checksum and new installs get the updated behavior—do not modify V43 in-place.
In `@src/main/resources/db/migration/V45__make_provider_id_nullable_in_users.sql`:
- Around line 1-2: The migration file
V45__make_provider_id_nullable_in_users.sql must not be inserted between
existing V44/V45 versions; instead revert this change and create a new migration
with the next sequential Flyway version (e.g., V46__) that makes provider_id
nullable, so you never alter or insert migrations into an already-deployed
version chain; update the PR to remove the mid-chain insertion and add the new
final-version migration file (reference:
V45__make_provider_id_nullable_in_users.sql and the new V46__... file).
In
`@src/main/resources/db/migration/V46__add_apple_refresh_token_to_user_oauth_account.sql`:
- Around line 7-12: The UPDATE and the corresponding INSERT must be restricted
to Apple provider rows: modify the UPDATE on user_oauth_account (aliases uoa and
u) to include a provider filter (e.g., AND u.provider = 'APPLE' and/or
uoa.provider = 'APPLE' as appropriate) so only Apple accounts get
u.apple_refresh_token copied, and likewise change the INSERT ... SELECT that
inserts apple_refresh_token (the later block referencing users and
user_oauth_account) to only select/insert rows where provider = 'APPLE'; target
the table aliases user_oauth_account (uoa) and users (u) in both statements to
ensure only Apple-provider records are updated/inserted.
In `@src/main/resources/db/migration/V47__remove_oauth_columns_from_users.sql`:
- Line 3: V47 currently issues ALTER TABLE users DROP INDEX
uq_users_provider_provider_id_active which duplicates the removal already
performed in V45__make_provider_id_nullable_in_users.sql; remove that DROP INDEX
from V47 to avoid failing in environments where V45 has run, or alternatively
replace it with a guarded drop (e.g., DROP INDEX IF EXISTS) for the index
uq_users_provider_provider_id_active so the migration no longer errors when the
index is absent.
In
`@src/main/resources/db/migration/V48__add_unique_constraint_provider_oauth_email.sql`:
- Around line 14-27: The DELETE currently joins user_oauth_account AS dup to the
subquery aliased ranked (which selects provider, oauth_email, id AS keep_id and
ROW_NUMBER() AS rn) using dup.id != ranked.keep_id, causing the wrong rows to be
removed; change the join condition so you delete only rows whose id corresponds
to ranked.id for rows with ranked.rn > 1 (i.e., select id in the ranked subquery
and join/compare dup.id = ranked.id or delete using WHERE id IN (SELECT id FROM
(...) WHERE rn > 1)) so that only duplicate rows (rn > 1) are deleted and the
intended keep_id (rn = 1) is preserved.
---
Duplicate comments:
In
`@src/main/resources/db/migration/V46__add_apple_refresh_token_to_user_oauth_account.sql`:
- Around line 15-23: The INSERT checks duplicates only by (user_id, provider)
which can still violate the (provider, provider_id) unique constraint for
re-registered users; update the migration so the SELECT excludes rows where a
user_oauth_account already exists with the same (provider, provider_id) (add a
NOT EXISTS subquery referencing ua.provider = u.provider AND ua.provider_id =
u.provider_id) or alternatively delete/merge conflicting user_oauth_account rows
for those (provider, provider_id) before running the INSERT to prevent
unique-key collisions; target the user_oauth_account INSERT/SELECT block and the
users columns provider and provider_id when making this change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 1f69ffa5-cef9-474a-ab41-805a2bddf10b
📒 Files selected for processing (6)
src/main/java/gg/agit/konect/domain/user/repository/UserOAuthAccountRepository.javasrc/main/resources/db/migration/V43__add_user_oauth_account_table.sqlsrc/main/resources/db/migration/V45__make_provider_id_nullable_in_users.sqlsrc/main/resources/db/migration/V46__add_apple_refresh_token_to_user_oauth_account.sqlsrc/main/resources/db/migration/V47__remove_oauth_columns_from_users.sqlsrc/main/resources/db/migration/V48__add_unique_constraint_provider_oauth_email.sql
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
src/main/resources/db/migration/**/*.sql
⚙️ CodeRabbit configuration file
src/main/resources/db/migration/**/*.sql: Flyway 마이그레이션 리뷰 규칙:
- 버전 파일명 규칙(V{number}__{description}.sql) 위반 여부를 우선 확인한다.
- 이미 배포된 마이그레이션 수정/재번호 부여 위험이 있으면 반드시 차단 코멘트를 남긴다.
- 파괴적 변경(drop, rename 등)은 롤백 가능성과 운영 영향 관점에서 검토한다.
Files:
src/main/resources/db/migration/V43__add_user_oauth_account_table.sqlsrc/main/resources/db/migration/V47__remove_oauth_columns_from_users.sqlsrc/main/resources/db/migration/V48__add_unique_constraint_provider_oauth_email.sqlsrc/main/resources/db/migration/V45__make_provider_id_nullable_in_users.sqlsrc/main/resources/db/migration/V46__add_apple_refresh_token_to_user_oauth_account.sql
**/*
⚙️ CodeRabbit configuration file
**/*: 공통 리뷰 톤 가이드:
- 모든 코멘트는 첫 줄에
[LEVEL: ...]태그를 포함한다.- 과장된 표현 없이 사실 기반으로 작성한다.
- 한 코멘트에는 하나의 이슈만 다룬다.
- 코드 예시가 필요하면 최소 수정 예시를 제시한다.
- 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.
Files:
src/main/resources/db/migration/V43__add_user_oauth_account_table.sqlsrc/main/resources/db/migration/V47__remove_oauth_columns_from_users.sqlsrc/main/resources/db/migration/V48__add_unique_constraint_provider_oauth_email.sqlsrc/main/resources/db/migration/V45__make_provider_id_nullable_in_users.sqlsrc/main/resources/db/migration/V46__add_apple_refresh_token_to_user_oauth_account.sqlsrc/main/java/gg/agit/konect/domain/user/repository/UserOAuthAccountRepository.java
src/main/java/**/*.java
⚙️ CodeRabbit configuration file
src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.
- 코멘트는 반드시 한국어로 작성한다.
- 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
- 각 코멘트 첫 줄에 심각도를
[LEVEL: high|medium|low]형식으로 반드시 표기한다.- 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
- 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
- 가능하면 재현 조건 및 실패 시나리오도 포함한다.
- 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
- 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
- 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
- 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
- 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.
Files:
src/main/java/gg/agit/konect/domain/user/repository/UserOAuthAccountRepository.java
src/main/resources/db/migration/V45__make_provider_id_nullable_in_users.sql
Show resolved
Hide resolved
| UPDATE user_oauth_account uoa | ||
| INNER JOIN users u | ||
| ON uoa.user_id = u.id | ||
| AND uoa.provider = u.provider | ||
| SET uoa.apple_refresh_token = u.apple_refresh_token | ||
| WHERE u.apple_refresh_token IS NOT NULL; |
There was a problem hiding this comment.
[LEVEL: medium] apple_refresh_token 이관 범위가 Apple provider로 제한되지 않았습니다.
문제: Line 7-12와 Line 15-23은 u.provider = 'APPLE' 조건 없이 users.apple_refresh_token 값을 그대로 복사합니다. 영향: 과거 데이터에 값이 남아 있으면 Google/Kakao/Naver 계정 행에도 Apple refresh token이 저장돼 이후 토큰 관리나 연동 해제 로직이 잘못된 계정을 기준으로 동작할 수 있습니다. 제안: UPDATE와 INSERT 둘 다 Apple provider만 대상으로 이관하세요.
Also applies to: 15-23
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/resources/db/migration/V46__add_apple_refresh_token_to_user_oauth_account.sql`
around lines 7 - 12, The UPDATE and the corresponding INSERT must be restricted
to Apple provider rows: modify the UPDATE on user_oauth_account (aliases uoa and
u) to include a provider filter (e.g., AND u.provider = 'APPLE' and/or
uoa.provider = 'APPLE' as appropriate) so only Apple accounts get
u.apple_refresh_token copied, and likewise change the INSERT ... SELECT that
inserts apple_refresh_token (the later block referencing users and
user_oauth_account) to only select/insert rows where provider = 'APPLE'; target
the table aliases user_oauth_account (uoa) and users (u) in both statements to
ensure only Apple-provider records are updated/inserted.
src/main/resources/db/migration/V47__remove_oauth_columns_from_users.sql
Show resolved
Hide resolved
| DELETE dup | ||
| FROM user_oauth_account dup | ||
| JOIN ( | ||
| SELECT | ||
| provider, | ||
| oauth_email, | ||
| id AS keep_id, | ||
| ROW_NUMBER() OVER (PARTITION BY provider, oauth_email ORDER BY created_at DESC) AS rn | ||
| FROM user_oauth_account | ||
| WHERE oauth_email IS NOT NULL | ||
| ) ranked ON dup.provider = ranked.provider | ||
| AND dup.oauth_email = ranked.oauth_email | ||
| AND dup.id != ranked.keep_id | ||
| WHERE ranked.rn > 1; |
There was a problem hiding this comment.
[LEVEL: high] 중복 정리 DELETE가 남길 행이 아니라 다른 행들을 지웁니다.
문제: Line 20-27은 각 행 자신의 id를 keep_id로 뽑아놓고 dup.id != ranked.keep_id로 조인해서, rn > 1인 중복 행 자체가 아니라 같은 그룹의 다른 행들을 삭제합니다. 영향: 2건 중복이면 최신 행 대신 오래된 행이 남고, 3건 이상 중복이면 그룹 전체가 삭제돼 OAuth 로그인 매핑 데이터가 유실될 수 있습니다. 제안: rn > 1인 행의 id만 별도로 뽑아서 dup.id = ranked.id 기준으로 삭제하도록 바꾸세요.
최소 수정 예시
DELETE dup
FROM user_oauth_account dup
JOIN (
- SELECT
- provider,
- oauth_email,
- id AS keep_id,
- ROW_NUMBER() OVER (PARTITION BY provider, oauth_email ORDER BY created_at DESC) AS rn
- FROM user_oauth_account
- WHERE oauth_email IS NOT NULL
-) ranked ON dup.provider = ranked.provider
- AND dup.oauth_email = ranked.oauth_email
- AND dup.id != ranked.keep_id
-WHERE ranked.rn > 1;
+ SELECT id
+ FROM (
+ SELECT
+ id,
+ ROW_NUMBER() OVER (
+ PARTITION BY provider, oauth_email
+ ORDER BY created_at DESC, id DESC
+ ) AS rn
+ FROM user_oauth_account
+ WHERE oauth_email IS NOT NULL
+ ) ranked
+ WHERE rn > 1
+) ranked ON dup.id = ranked.id;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/resources/db/migration/V48__add_unique_constraint_provider_oauth_email.sql`
around lines 14 - 27, The DELETE currently joins user_oauth_account AS dup to
the subquery aliased ranked (which selects provider, oauth_email, id AS keep_id
and ROW_NUMBER() AS rn) using dup.id != ranked.keep_id, causing the wrong rows
to be removed; change the join condition so you delete only rows whose id
corresponds to ranked.id for rows with ranked.rn > 1 (i.e., select id in the
ranked subquery and join/compare dup.id = ranked.id or delete using WHERE id IN
(SELECT id FROM (...) WHERE rn > 1)) so that only duplicate rows (rn > 1) are
deleted and the intended keep_id (rn = 1) is preserved.
🔍 개요
user_oauth_account테이블로 인해 사용자에 대한 OAuth 연동 상태를 명확히 관리할 수 있게 되어 유저 테이블을 정리한다.🚀 주요 변경 내용
users테이블에 있는 OAuth 관련 컬럼provider,provider_id,apple_refresh_token을user_oauth_account테이블로 옮겼습니다.(provider, provider_id)유니크와(user_id, provider)유니크를 동시에 적용하여 한 OAuth 계정은 한 사용자에게만, 한 사용자는 provider별 1개 연동만 가능하도록 제한했습니다.💬 참고 사항
✅ Checklist (완료 조건)