Skip to content

refactor: 유저 테이블 구조 리팩토링#348

Open
dh2906 wants to merge 9 commits intodevelopfrom
refactor/user-oauth-column
Open

refactor: 유저 테이블 구조 리팩토링#348
dh2906 wants to merge 9 commits intodevelopfrom
refactor/user-oauth-column

Conversation

@dh2906
Copy link
Contributor

@dh2906 dh2906 commented Mar 6, 2026

🔍 개요

  • user_oauth_account 테이블로 인해 사용자에 대한 OAuth 연동 상태를 명확히 관리할 수 있게 되어 유저 테이블을 정리한다.
  • close #이슈번호

🚀 주요 변경 내용

  • users 테이블에 있는 OAuth 관련 컬럼provider, provider_id, apple_refresh_tokenuser_oauth_account 테이블로 옮겼습니다.

  • (provider, provider_id) 유니크와 (user_id, provider) 유니크를 동시에 적용하여 한 OAuth 계정은 한 사용자에게만, 한 사용자는 provider별 1개 연동만 가능하도록 제한했습니다.


💬 참고 사항


✅ Checklist (완료 조건)

  • 코드 스타일 가이드 준수
  • 테스트 코드 포함됨
  • Reviewers / Assignees / Labels 지정 완료
  • 보안 및 민감 정보 검증 (API 키, 환경 변수, 개인정보 등)

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

📝 Walkthrough

Walkthrough

사용자 엔티티에서 OAuth 관련 필드(provider, providerId, appleRefreshToken)를 제거하고 새로운 UserOAuthAccount 엔티티로 마이그레이션합니다. 리포지토리, 서비스, OAuth 핸들러를 일관되게 업데이트하고 데이터베이스 스키마를 변경하는 포괄적인 리팩토링입니다.

Changes

Cohort / File(s) Summary
Core Domain Model
src/main/java/gg/agit/konect/domain/user/model/User.java
provider, providerId 필드 제거; UserOAuthAccount와의 일대다 관계 추가; 관련 생성자 및 팩토리 메서드 조정
OAuth Account Entity
src/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.java
appleRefreshToken 필드 추가; 생성자 및 팩토리 메서드 확장; updateAppleRefreshToken(), updateProviderId() 메서드 추가
Repository Layer
src/main/java/gg/agit/konect/domain/user/repository/UserOAuthAccountRepository.java, src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java
UserOAuthAccountRepository에 새로운 쿼리 메서드 추가(findUserByOauthEmailAndProvider, findByProviderAndProviderId, existsByProviderAndProviderId); UserRepository에서 provider 기반 메서드 제거 및 역할 기반 조회 수정
Service Layer
src/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.java, src/main/java/gg/agit/konect/domain/user/service/UserService.java
UserOAuthAccountService에서 appleRefreshToken 처리 로직 추가 및 getPrimaryOAuthAccount() 메서드 추가; UserService에서 OAuth 계정 조회 및 저장 로직 변경
OAuth Handlers
src/main/java/gg/agit/konect/global/auth/oauth/OAuth2LoginSuccessHandler.java, src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java, src/main/java/gg/agit/konect/infrastructure/oauth/*.java
UserOAuthAccountRepository를 사용하여 OAuth 계정 조회 및 업데이트; AppleOAuthServiceImpl, GoogleOAuthServiceImpl, KakaoOAuthServiceImpl, NaverOAuthServiceImpl 모두 OAuth 이메일 기반 사용자 검색 로직 통일
Test Fixtures
src/test/java/gg/agit/konect/support/fixture/UserFixture.java
사용자 픽스처에서 provider, providerId 빌더 호출 제거
Database Migrations
src/main/resources/db/migration/V43__..._sql, V45__make_provider_id_nullable_in_users.sql, V46__add_apple_refresh_token_to_user_oauth_account.sql, V47__remove_oauth_columns_from_users.sql, V48__add_unique_constraint_provider_oauth_email.sql
OAuth 계정 테이블 생성, provider_id 컬럼 널 허용, apple_refresh_token 추가, users 테이블의 OAuth 관련 컬럼 제거, (provider, oauth_email) 고유 제약 추가

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰✨ OAuth 계정들을 정리하며,
사용자 테이블에서 분리하네,
새로운 관계로 한뼘 성장하고,
마이그레이션의 여정을 거쳐,
더욱 깔끔한 도메인이 탄생하도다! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 주요 변경 사항인 OAuth 관련 컬럼 이동과 유저 테이블 구조 리팩토링을 정확히 요약하고 있습니다.
Description check ✅ Passed 설명이 변경 사항(OAuth 컬럼 이동, 유니크 제약 추가)과 목표를 명확히 설명하고 체크리스트를 포함하고 있습니다.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/user-oauth-column

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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 005534a and cf9584a.

📒 Files selected for processing (15)
  • src/main/java/gg/agit/konect/domain/user/model/User.java
  • src/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.java
  • src/main/java/gg/agit/konect/domain/user/repository/UserOAuthAccountRepository.java
  • src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java
  • src/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.java
  • src/main/java/gg/agit/konect/domain/user/service/UserService.java
  • src/main/java/gg/agit/konect/global/auth/oauth/OAuth2LoginSuccessHandler.java
  • src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java
  • src/main/java/gg/agit/konect/infrastructure/oauth/AppleOAuthServiceImpl.java
  • src/main/java/gg/agit/konect/infrastructure/oauth/GoogleOAuthServiceImpl.java
  • src/main/java/gg/agit/konect/infrastructure/oauth/KakaoOAuthServiceImpl.java
  • src/main/java/gg/agit/konect/infrastructure/oauth/NaverOAuthServiceImpl.java
  • src/main/resources/db/migration/V45__add_apple_refresh_token_to_user_oauth_account.sql
  • src/main/resources/db/migration/V46__remove_oauth_columns_from_users.sql
  • src/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.java
  • src/main/java/gg/agit/konect/global/auth/oauth/OAuth2LoginSuccessHandler.java
  • src/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.java
  • src/main/java/gg/agit/konect/domain/user/model/User.java
  • src/main/java/gg/agit/konect/domain/user/repository/UserOAuthAccountRepository.java
  • src/main/java/gg/agit/konect/infrastructure/oauth/GoogleOAuthServiceImpl.java
  • src/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.java
  • src/main/java/gg/agit/konect/infrastructure/oauth/KakaoOAuthServiceImpl.java
  • src/main/java/gg/agit/konect/infrastructure/oauth/NaverOAuthServiceImpl.java
  • src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java
  • src/main/java/gg/agit/konect/infrastructure/oauth/AppleOAuthServiceImpl.java
  • src/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.java
  • src/main/resources/db/migration/V45__add_apple_refresh_token_to_user_oauth_account.sql
  • src/main/java/gg/agit/konect/global/auth/oauth/OAuth2LoginSuccessHandler.java
  • src/main/resources/db/migration/V46__remove_oauth_columns_from_users.sql
  • src/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.java
  • src/main/java/gg/agit/konect/domain/user/model/User.java
  • src/main/java/gg/agit/konect/domain/user/repository/UserOAuthAccountRepository.java
  • src/main/java/gg/agit/konect/infrastructure/oauth/GoogleOAuthServiceImpl.java
  • src/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.java
  • src/main/java/gg/agit/konect/infrastructure/oauth/KakaoOAuthServiceImpl.java
  • src/main/java/gg/agit/konect/infrastructure/oauth/NaverOAuthServiceImpl.java
  • src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java
  • src/main/java/gg/agit/konect/infrastructure/oauth/AppleOAuthServiceImpl.java
  • src/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.sql
  • src/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 관계가 LAZY fetch로 적절히 설정되었고, 서비스 레이어에서 UserOAuthAccountRepository를 통해 명시적으로 조회하는 패턴과 일관성이 있습니다. final 키워드로 컬렉션 재할당 방지도 좋습니다.

src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java (1)

19-25: LGTM!

findById 쿼리가 iddeletedAt IS NULL 조건으로 정확히 구성되어 있습니다. Provider 관련 쿼리 제거는 OAuth 계정 분리 리팩토링 목적에 부합합니다.

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: 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_id NULL 허용 여부와 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

📥 Commits

Reviewing files that changed from the base of the PR and between cf9584a and a6fc9d8.

📒 Files selected for processing (5)
  • src/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.java
  • src/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.java
  • src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java
  • src/main/resources/db/migration/V43__add_user_oauth_account_table.sql
  • src/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.sql
  • src/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.sql
  • src/main/java/gg/agit/konect/domain/user/service/UserOAuthAccountService.java
  • src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java
  • src/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.java
  • src/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.java
  • src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java
  • src/main/java/gg/agit/konect/domain/user/model/UserOAuthAccount.java

Comment on lines +128 to 144
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);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

[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.

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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between a6fc9d8 and df5fab9.

📒 Files selected for processing (6)
  • src/main/java/gg/agit/konect/domain/user/repository/UserOAuthAccountRepository.java
  • src/main/resources/db/migration/V43__add_user_oauth_account_table.sql
  • src/main/resources/db/migration/V45__make_provider_id_nullable_in_users.sql
  • src/main/resources/db/migration/V46__add_apple_refresh_token_to_user_oauth_account.sql
  • src/main/resources/db/migration/V47__remove_oauth_columns_from_users.sql
  • src/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.sql
  • src/main/resources/db/migration/V47__remove_oauth_columns_from_users.sql
  • src/main/resources/db/migration/V48__add_unique_constraint_provider_oauth_email.sql
  • src/main/resources/db/migration/V45__make_provider_id_nullable_in_users.sql
  • src/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.sql
  • src/main/resources/db/migration/V47__remove_oauth_columns_from_users.sql
  • src/main/resources/db/migration/V48__add_unique_constraint_provider_oauth_email.sql
  • src/main/resources/db/migration/V45__make_provider_id_nullable_in_users.sql
  • src/main/resources/db/migration/V46__add_apple_refresh_token_to_user_oauth_account.sql
  • src/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

Comment on lines +7 to +12
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;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

[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.

Comment on lines +14 to +27
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;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

[LEVEL: high] 중복 정리 DELETE가 남길 행이 아니라 다른 행들을 지웁니다.

문제: Line 20-27은 각 행 자신의 idkeep_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.

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