[fix] 탈퇴 후 카카오 재로그인 시 계정 복구/완전삭제 후 재가입 옵션 제공#184
Conversation
JPA @Version 필드는 낙관적 잠금을 위해 항상 non-null이어야 하므로 NOT NULL 제약 조건을 추가. DEFAULT 0이 설정되어 있어 기존 행은 안전하게 0으로 초기화됨. CodeRabbit 리뷰 반영.
- POST /api/auth/kakao/login 응답 변경: 탈퇴 계정 발견 시 예외 대신 status=WITHDRAWN_PENDING 페이로드 반환하여 클라이언트가 사용자 선택 안내 가능 - POST /api/auth/kakao/restore 신규: User.deletedAt=null로 복구 (탈퇴 취소). 사업장/계약/근무기록은 다른 사용자 데이터 일관성 보호 위해 자동 복구 안 함 - POST /api/auth/kakao/purge-and-register 신규: 30일 스케줄러와 동일 경로 (UserHardDeleteService.hardDeleteUser)로 영구 삭제 후 신규 가입 - UserWithdrawService.cleanupCommonData에서 UserSettings 삭제 제거 → 복구 시 사용자 알림 설정 그대로 유지. 30일 hard delete 시점에 정리됨
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
Walkthrough카카오 로그인이 탈퇴 계정을 발견하면 200 + 상태형 응답을 반환하고, 복구( Changes카카오 계정 탈퇴 라이프사이클
Sequence Diagram(s)sequenceDiagram
participant Client
participant AuthController
participant AuthService
participant UserHardDeleteService
participant UserService
Client->>AuthController: POST /api/auth/kakao/login\n(kakaoAccessToken)
AuthController->>AuthService: loginWithKakao(kakaoAccessToken)
alt 활성 사용자
AuthService->>AuthController: KakaoLoginResult(LOGGED_IN with tokens)
AuthController->>Client: 200 ApiResponse{data: LOGGED_IN}
else 탈퇴 계정
AuthService->>AuthController: KakaoLoginResult(WITHDRAWN_PENDING withdrawnAccount)
AuthController->>Client: 200 ApiResponse{data: WITHDRAWN_PENDING}
Client->>AuthController: POST /api/auth/kakao/restore\n(kakaoAccessToken)
AuthController->>AuthService: restoreWithKakao(kakaoAccessToken)
AuthService->>AuthService: user.restore()\ngenerateTokens()
AuthService->>AuthController: LoginResponse(tokens)
AuthController->>Client: 200 ApiResponse{data: LoginResponse}
Client->>AuthController: POST /api/auth/kakao/purge-and-register\n(registerRequest)
AuthController->>AuthService: purgeAndRegisterWithKakao(request)
AuthService->>UserHardDeleteService: hardDeleteUser(user)
AuthService->>UserService: register(newUser)
AuthService->>AuthController: LoginResponse(new tokens)
AuthController->>Client: 200 ApiResponse{data: LoginResponse}
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
개요카카오 소셜 로그인을 통해 탈퇴한 계정의 복구 및 재가입 워크플로우를 구현했습니다. 탈퇴 계정 로그인 시 오류 반환 대신 탈퇴 상태를 알리고, 복구 또는 완전 삭제 후 재가입 중 선택할 수 있도록 변경했으며, 탈퇴 시 사용자 설정을 30일간 보존하도록 정책을 수정했습니다. 변경 사항카카오 계정 탈퇴 라이프사이클
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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: 2
🧹 Nitpick comments (3)
src/main/java/com/example/paycheck/domain/auth/service/AuthService.java (1)
262-265: ⚡ Quick win
registerWithKakaoInternal의 가시성/트랜잭션 책임 재정리 권장현재
public+@Transactional이지만 두 호출처(registerWithKakao,purgeAndRegisterWithKakao) 모두 동일 클래스 내부에서this.로 호출하기 때문에 이 메서드의@Transactional은 어느 경로에서도 직접 효력을 발휘하지 못합니다(외곽 메서드의 트랜잭션을 단순히 상속할 뿐). 가시성 또한public일 필요가 없습니다. 위 critical 코멘트와 함께 다음 중 하나로 정리하는 것을 추천합니다:
- 외곽 두 메서드 모두
@Transactional을 갖도록 보장하고, 내부 메서드는private로 강등 + 어노테이션 제거- 또는 이 메서드를 별도 빈으로 분리하여 프록시를 통해 호출
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/example/paycheck/domain/auth/service/AuthService.java` around lines 262 - 265, The registerWithKakaoInternal method is declared public and annotated with `@Transactional` but is only called internally via this., so its transaction proxy never applies; make registerWithKakaoInternal private and remove its `@Transactional`, and instead annotate the two callers registerWithKakao and purgeAndRegisterWithKakao with `@Transactional` (or extract registerWithKakaoInternal into a separate `@Component/`@Service bean so it can be called via proxy) so transactional behavior is actually applied when needed.src/test/java/com/example/paycheck/domain/auth/service/AuthServiceTest.java (1)
587-654: 💤 Low value테스트 보강 권장 —
purgeAndRegisterWithKakao에서 기존 사용자가 없는 경우의 분기production 코드 라인 165-174는
userRepository.findByKakaoId(...)가 비어 있을 때 hard delete를 건너뛰고 등록만 진행하도록 되어 있는데, 이 분기를 검증하는 테스트가 없습니다. 정책상 흔한 호출 케이스는 아니지만(탈퇴 후 재가입 의도가 전제),ifPresent분기 회귀 방지를 위해verify(userHardDeleteService, never()).hardDeleteUser(anyLong())+ 정상 가입 결과 검증 테스트를 한 건 추가해두면 좋습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/test/java/com/example/paycheck/domain/auth/service/AuthServiceTest.java` around lines 587 - 654, Add a unit test for the purgeAndRegisterWithKakao(UserDto.KakaoRegisterRequest) success path when userRepository.findByKakaoId(...) returns Optional.empty(): mock oAuthService.getKakaoUserInfo(...) to return kakaoUserInfo, mock userRepository.findByKakaoId(...) to return Optional.empty(), mock userService.register(...) to return a RegisterResponse and tokenService.generateTokenPair(...) to return tokenPair, then call authService.purgeAndRegisterWithKakao(request) and verify that userHardDeleteService.hardDeleteUser(...) is never called, userService.register(...) is called once, and the returned LoginResponse contains the expected accessToken, refreshToken, and userId; reference methods: purgeAndRegisterWithKakao, userRepository.findByKakaoId, userHardDeleteService.hardDeleteUser, userService.register, tokenService.generateTokenPair.src/main/java/com/example/paycheck/domain/auth/dto/AuthDto.java (1)
107-129: ⚡ Quick win
status값을 enum/상수로 승격 권장현재
status는String이고,loggedIn()/withdrawnPending()팩토리, 컨트롤러 응답, 테스트 단언(AuthServiceTest에서"LOGGED_IN","WITHDRAWN_PENDING"직접 비교)에서 동일한 리터럴이 중복 등장합니다. 향후 신규 상태 추가나 리네이밍 시 누락 위험이 있으니 enum 또는public static final String상수로 단일 출처화하는 것을 권장합니다.♻️ 제안 — enum 도입 예시
public static class KakaoLoginResult { + public enum Status { LOGGED_IN, WITHDRAWN_PENDING } + `@Schema`(description = "로그인 결과 상태", allowableValues = {"LOGGED_IN", "WITHDRAWN_PENDING"}) - private String status; + private Status status; ... public static KakaoLoginResult loggedIn(LoginResponse login) { return KakaoLoginResult.builder() - .status("LOGGED_IN") + .status(Status.LOGGED_IN) .login(login) .build(); }테스트와 직렬화 출력은 enum의
name()이 동일 문자열을 내보내므로 호환됩니다(Jackson 기본 직렬화).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/example/paycheck/domain/auth/dto/AuthDto.java` around lines 107 - 129, Convert the String-typed status in KakaoLoginResult to a single-source enum or constants to avoid repeated literals: introduce a new enum KakaoLoginStatus (values LOGGED_IN, WITHDRAWN_PENDING) or public static final Strings with those names, change the field "status" to use that enum/type, update the factory methods KakaoLoginResult.loggedIn(LoginResponse) and KakaoLoginResult.withdrawnPending(WithdrawnAccountInfo) to set status using the enum/constant, and adjust the `@Schema` allowableValues/description to reference the enum names; then update any callers/tests (e.g., AuthServiceTest and controller responses) to compare against KakaoLoginStatus.LOGGED_IN / .WITHDRAWN_PENDING or the new constants.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/java/com/example/paycheck/api/auth/AuthController.java`:
- Around line 22-32: The kakaoLogin response type was changed from
ApiResponse<LoginResponse> to ApiResponse<KakaoLoginResult>, breaking clients
that expect top-level fields like data.accessToken, data.refreshToken,
data.userId; update AuthController.kakaoLogin and related service/output DTOs to
restore backward compatibility or provide a migration path: either (A) keep the
existing endpoint signature (ApiResponse<LoginResponse>) and map the new
AuthDto.KakaoLoginResult into the old flat AuthDto.LoginResponse shape when
status==LOGGED_IN, or (B) introduce a new versioned endpoint (e.g.,
/api/v2/auth/kakao/login) that returns ApiResponse<KakaoLoginResult> and leave
the current /api/auth/kakao/login unchanged; additionally update API docs
(docs/API_SPECIFICATION.md) to reflect the new response branching (status +
login/withdrawnAccount) and add docs/examples for /api/auth/kakao/restore and
/api/auth/kakao/purge-and-register so clients can migrate safely.
In `@src/main/java/com/example/paycheck/domain/auth/service/AuthService.java`:
- Around line 161-177: Add a transactional boundary to make hard delete and
registration atomic: annotate the public method purgeAndRegisterWithKakao with
`@Transactional` (default propagation REQUIRED) so the call to
userHardDeleteService.hardDeleteUser(...) and the subsequent
registerWithKakaoInternal(...) execute in the same transaction and will roll
back together on failure; ensure the class is a Spring bean and imports javax/
spring transaction annotation as appropriate so the proxy applies.
---
Nitpick comments:
In `@src/main/java/com/example/paycheck/domain/auth/dto/AuthDto.java`:
- Around line 107-129: Convert the String-typed status in KakaoLoginResult to a
single-source enum or constants to avoid repeated literals: introduce a new enum
KakaoLoginStatus (values LOGGED_IN, WITHDRAWN_PENDING) or public static final
Strings with those names, change the field "status" to use that enum/type,
update the factory methods KakaoLoginResult.loggedIn(LoginResponse) and
KakaoLoginResult.withdrawnPending(WithdrawnAccountInfo) to set status using the
enum/constant, and adjust the `@Schema` allowableValues/description to reference
the enum names; then update any callers/tests (e.g., AuthServiceTest and
controller responses) to compare against KakaoLoginStatus.LOGGED_IN /
.WITHDRAWN_PENDING or the new constants.
In `@src/main/java/com/example/paycheck/domain/auth/service/AuthService.java`:
- Around line 262-265: The registerWithKakaoInternal method is declared public
and annotated with `@Transactional` but is only called internally via this., so
its transaction proxy never applies; make registerWithKakaoInternal private and
remove its `@Transactional`, and instead annotate the two callers
registerWithKakao and purgeAndRegisterWithKakao with `@Transactional` (or extract
registerWithKakaoInternal into a separate `@Component/`@Service bean so it can be
called via proxy) so transactional behavior is actually applied when needed.
In `@src/test/java/com/example/paycheck/domain/auth/service/AuthServiceTest.java`:
- Around line 587-654: Add a unit test for the
purgeAndRegisterWithKakao(UserDto.KakaoRegisterRequest) success path when
userRepository.findByKakaoId(...) returns Optional.empty(): mock
oAuthService.getKakaoUserInfo(...) to return kakaoUserInfo, mock
userRepository.findByKakaoId(...) to return Optional.empty(), mock
userService.register(...) to return a RegisterResponse and
tokenService.generateTokenPair(...) to return tokenPair, then call
authService.purgeAndRegisterWithKakao(request) and verify that
userHardDeleteService.hardDeleteUser(...) is never called,
userService.register(...) is called once, and the returned LoginResponse
contains the expected accessToken, refreshToken, and userId; reference methods:
purgeAndRegisterWithKakao, userRepository.findByKakaoId,
userHardDeleteService.hardDeleteUser, userService.register,
tokenService.generateTokenPair.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 151407ac-730e-4cdd-a0a7-9a63d7530b88
📒 Files selected for processing (9)
src/main/java/com/example/paycheck/api/auth/AuthController.javasrc/main/java/com/example/paycheck/common/exception/ErrorCode.javasrc/main/java/com/example/paycheck/domain/auth/dto/AuthDto.javasrc/main/java/com/example/paycheck/domain/auth/service/AuthService.javasrc/main/java/com/example/paycheck/domain/user/entity/User.javasrc/main/java/com/example/paycheck/domain/user/service/UserWithdrawService.javasrc/main/resources/db/migration/V20260505__Add_version_column_to_users.sqlsrc/test/java/com/example/paycheck/domain/auth/service/AuthServiceTest.javasrc/test/java/com/example/paycheck/domain/user/service/UserWithdrawServiceTest.java
- KakaoLoginResult를 평탄 구조로 재설계: status + accessToken/refreshToken/userId/name/userType 를 top-level에 두어 기존 클라이언트(data.accessToken 등)와 호환 유지. 탈퇴 케이스는 status=WITHDRAWN_PENDING + withdrawnAccount만 채움. - purgeAndRegisterWithKakao에 @transactional 추가하여 hardDelete + register를 하나의 트랜잭션으로 묶음. register 실패 시 hardDelete도 함께 롤백되어 데이터 영구 손실 방지. - hardDeleteUser 직후 userRepository.flush() 명시 호출. JPA 기본 flush 순서(INSERT→DELETE) 로 인한 unique kakao_id 제약 충돌을 회피. - AuthServiceTest: 평탄 응답 접근 방식으로 수정, hardDelete→flush→register 호출 순서 검증, register 실패 시 예외 전파 검증 추가. - docs/API_SPECIFICATION.md: 1.1 카카오 로그인 응답을 평탄 구조로 동기화하고 1.5 탈퇴 계정 복구, 1.6 완전삭제 후 재가입 항목 신규 추가.
🔖 관련 GitHub Issue
📝 변경 사항
주요 변경 내용
status=WITHDRAWN_PENDING응답으로 사용자 선택 안내POST /api/auth/kakao/restore추가 (탈퇴 취소)POST /api/auth/kakao/purge-and-register추가 (완전 삭제 후 재가입)UserSettings보존 → 복구 시 이전 알림 설정 그대로 유지상세 설명
배경
기존에는 탈퇴(soft delete) 후 30일 hard-delete 스케줄러가 돌기 전에 동일 카카오 계정으로 재로그인 시
BadRequestException("탈퇴한 계정입니다. 다시 가입해주세요.")로 모든 진입이 차단되었습니다. 사용자에게는 30일을 기다리는 것 외에 선택지가 없었습니다.새로운 흐름
탈퇴 후 30일 이내 재로그인 시 사용자에게 두 가지 선택지를 제공합니다:
계정 복구 (
POST /api/auth/kakao/restore)User.deletedAt = null로 되돌림UserSettings는 탈퇴 시 보존되어 사용자가 이전에 설정한 알림 옵션이 그대로 유지됨완전 삭제 후 재가입 (
POST /api/auth/kakao/purge-and-register)UserHardDeleteService.hardDeleteUser()(30일 스케줄러와 동일 경로) 재활용KakaoRegisterRequest그대로 재사용API 응답 형식
{ "success": true, "data": { "status": "LOGGED_IN", "login": { "accessToken": "...", "refreshToken": "...", "userId": 9, "name": "...", "userType": "EMPLOYER" } } }{ "success": true, "data": { "status": "WITHDRAWN_PENDING", "withdrawnAccount": { "name": "...", "userType": "EMPLOYER", "withdrawnAt": "...", "profileImageUrl": "..." } } }미복구 항목 (정책)
✅ 체크리스트
./gradlew testBUILD SUCCESSFUL)🔗 관련 PR
🤖 Generated with Claude Code
Summary by CodeRabbit
카카오 로그인 및 계정 관리 개선
새 기능
개선사항
문서