Skip to content

fix: 유저 삭제 시 소프트 딜리트 방식으로 전환 - #329

Merged
dh2906 merged 4 commits into
developfrom
fix/user-soft-delete
Feb 27, 2026
Merged

fix: 유저 삭제 시 소프트 딜리트 방식으로 전환#329
dh2906 merged 4 commits into
developfrom
fix/user-soft-delete

Conversation

@dh2906

@dh2906 dh2906 commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

🔍 개요

  • close #이슈번호

🚀 주요 변경 내용

  • 회원탈퇴를 하드 삭제에서 소프트 삭제로 전환했습니다.

  • OAuth 로그인 시 탈퇴 계정을 7일 유예기간 내 자동 복구하도록 변경했습니다.

  • active_flag 컬럼은 deleted_at 컬럼에 따라 1(존재) 또는 NULL(삭제)가 결정됩니다.

  • 탈퇴한 사용자의 경우 이름 조회 시 탈퇴한 사용자로 표시됩니다.


💬 참고 사항


✅ Checklist (완료 조건)

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

@coderabbitai

coderabbitai Bot commented Feb 27, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

사용자 엔티티에 소프트 삭제 기능을 도입하여 물리적 삭제 대신 논리적 삭제를 지원합니다. deletedAt 컬럼을 추가하고, 탈퇴한 사용자 복구 기능을 구현하며, 저장소 쿼리를 업데이트하여 삭제된 기록을 필터링하고, OAuth 로그인 시 최대 7일 이내에 탈퇴 상태를 복구할 수 있도록 합니다.

Changes

Cohort / File(s) Summary
User 엔티티 모델
src/main/java/gg/agit/konect/domain/user/model/User.java
소프트 삭제 지원을 위해 deletedAt 필드와 생성 열 activeFlag 추가; withdraw(), restore(), canRestore() 생명주기 메서드 구현; 탈퇴한 사용자 표시명 상수 추가; 삭제된 사용자의 경우 getName()에서 "탈퇴한 사용자" 반환.
Repository 쿼리 계층
src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java
모든 조회 메서드에 deletedAt IS NULL 필터 추가; 삭제된 사용자 조회를 위한 새로운 메서드 추가(findFirstByEmailAndProviderAndDeletedAtIsNotNullOrderByDeletedAtDesc 등); 물리적 삭제 메서드 제거; 기존 메서드를 @Query 주석이 있는 버전으로 교체하여 논리적 삭제 필터링 일관성 유지.
서비스 계층
src/main/java/gg/agit/konect/domain/user/service/UserActivityService.java, src/main/java/gg/agit/konect/domain/user/service/UserService.java
UserActivityService에서 Optional 기반 업데이트로 변경; UserService.deleteUser()를 물리적 삭제에서 withdraw() 호출로 변경하여 소프트 삭제 방식 적용.
OAuth 로그인 복구 로직
src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java
7일 복구 윈도우 내에서 탈퇴한 사용자 자동 복구 기능 추가; Apple 및 일반 제공자별로 providerId 또는 이메일 기반 복구 전략 구현; 복구된 사용자를 데이터베이스에 저장.
데이터베이스 마이그레이션
src/main/resources/db/migration/V40__add_user_soft_delete_and_active_unique_constraints.sql
deleted_at 컬럼 추가; active_flag 생성 열 추가 (삭제되지 않은 사용자만 1); 기존 고유 제약 조건 제거 및 active_flag를 포함한 새로운 복합 고유 제약 조건 추가 (phone_number, email_provider, university_id_student_number, provider_provider_id).

Sequence Diagram(s)

sequenceDiagram
    actor User as 사용자
    participant OAuthLoginHelper as OAuthLoginHelper
    participant UserRepository as UserRepository
    participant UserModel as User 모델
    participant Database as 데이터베이스

    User->>OAuthLoginHelper: OAuth 로그인 시도
    OAuthLoginHelper->>UserRepository: findByEmailAndProvider() 호출<br/>(deletedAt IS NULL 필터)
    UserRepository->>Database: 활성 사용자 조회
    
    alt 활성 사용자 존재
        Database-->>UserRepository: 사용자 반환
        UserRepository-->>OAuthLoginHelper: 활성 사용자
        OAuthLoginHelper-->>User: 로그인 완료
    else 활성 사용자 없음 (탈퇴 상태)
        Database-->>UserRepository: 결과 없음
        UserRepository-->>OAuthLoginHelper: Optional.empty()
        
        alt Apple 제공자 + providerId 존재
            OAuthLoginHelper->>UserRepository: findFirstByProviderIdAndProviderAndDeletedAtIsNotNull()
        else 이메일 기반 복구
            OAuthLoginHelper->>UserRepository: findFirstByEmailAndProviderAndDeletedAtIsNotNull()
        end
        
        UserRepository->>Database: 탈퇴한 사용자 조회 (최신순)
        Database-->>UserRepository: 탈퇴한 사용자
        UserRepository-->>OAuthLoginHelper: 탈퇴한 사용자 반환
        
        OAuthLoginHelper->>OAuthLoginHelper: canRestore(now, 7일) 검증<br/>(7일 내 탈퇴 확인)
        
        alt 복구 가능
            OAuthLoginHelper->>UserModel: restore() 호출
            UserModel->>UserModel: deletedAt = null 설정
            OAuthLoginHelper->>UserRepository: save(user) 호출
            UserRepository->>Database: 사용자 업데이트 (active_flag = 1)
            Database-->>UserRepository: 저장 완료
            UserRepository-->>OAuthLoginHelper: 복구된 사용자
            OAuthLoginHelper-->>User: 로그인 완료
        else 복구 기간 초과
            OAuthLoginHelper-->>User: 새 계정 생성 필요
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

버그

Poem

🐰 탈퇴한 사용자도 기억하고,
일곱 날 안엔 돌아올 수 있도록,
소프트 삭제의 자비로운 손길,
데이터는 영원히 남고,
사용자는 언제든 돌아온다! 🌟

🚥 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 PR 제목이 주요 변경 내용인 회원탈퇴 프로세스를 하드 삭제에서 소프트 삭제로 전환하는 것을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 소프트 삭제 전환, OAuth 로그인 시 자동 복구, active_flag 컬럼 추가, 탈퇴 사용자 이름 표시 등 변경사항과 관련된 내용을 포함하고 있습니다.

✏️ 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 fix/user-soft-delete

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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/domain/user/service/UserActivityService.java (1)

18-35: 🧹 Nitpick | 🔵 Trivial

[LEVEL: low] updateLastLoginAtupdateLastActivityAt의 예외 처리 방식 불일치

문제: updateLastLoginAt은 사용자가 없으면 예외를 던지지만, updateLastActivityAt은 조용히 무시함.
영향: 탈퇴 직후 로그인 시간 갱신 호출 시 예외 발생 가능성 존재.
제안: 의도된 동작이라면 주석으로 명시, 아니라면 동일한 방식으로 통일 검토 필요.

🤖 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/UserActivityService.java`
around lines 18 - 35, The two methods updateLastLoginAt and updateLastActivityAt
currently handle missing users differently: updateLastLoginAt calls
userRepository.getById(userId) which will throw if the user is absent, while
updateLastActivityAt uses userRepository.findById(...).ifPresent(...) and
silently skips; make the behavior consistent by either (A) changing
updateLastLoginAt to use userRepository.findById(userId).ifPresent(user ->
user.updateLastLoginAt(...)) to silently ignore missing users like
updateLastActivityAt, or (B) if throwing is intended, update
updateLastActivityAt to call
userRepository.getById(userId).updateLastActivityAt(...) and add a clarifying
comment on the intended semantics; update the code path accordingly and keep the
null check for userId.
🤖 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/User.java`:
- Around line 182-187: The current User.getName() hides withdrawn users' real
names by returning WITHDRAWN_USER_NAME when deletedAt != null; add a new
accessor (e.g., getActualName() or getRawName()) in the User class that always
returns the underlying name field regardless of deletedAt so admin/logging code
can retrieve the real name when needed; update any admin/logging call sites to
use the new method where appropriate and keep getName() behavior for
public-facing usage.
- Around line 31-46: The JPA entity User's uniqueConstraints declares a
constraint named "uq_users_phone_number" that must match the migration; update
the name to "uq_users_phone_number_active" in the `@UniqueConstraint` inside the
uniqueConstraints array on the User class so the annotation (uniqueConstraints /
`@UniqueConstraint`) uses the migration-consistent identifier
"uq_users_phone_number_active".

In `@src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java`:
- Line 66: The repository method findFirstByRoleOrderByIdAsc in UserRepository
lacks a soft-delete filter and can return users with deletedAt set; change this
derived query to an explicit `@Query` that adds "AND u.deletedAt IS NULL" (or the
equivalent column) so it only returns non-deleted users, mirroring other
repository methods; update the method signature (still named
findFirstByRoleOrderByIdAsc) to use the `@Query` annotation and ensure callers
like UserService.sendWelcomeMessage will only receive active users.

In `@src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java`:
- Around line 33-56: Add a transactional boundary to the findUserByProvider
method so the sequence of reads and potential re-create/save operations (calls
to restoreWithdrawnByProviderId, restoreWithdrawnByEmail and subsequent
userRepository.save) run in a single DB transaction; annotate the
findUserByProvider method with `@Transactional` (e.g., Spring's
org.springframework.transaction.annotation.Transactional with default
REQUIRED/read-write) and ensure the import is added so concurrent requests
cannot cause race conditions or unique-constraint violations during
restore/save.

In
`@src/main/resources/db/migration/V40__add_user_soft_delete_and_active_unique_constraints.sql`:
- Around line 10-18: The migration drops four unique indexes
(uq_users_phone_number, uq_users_email_provider,
uq_users_university_id_student_number, uq_users_provider_provider_id) and then
adds new UNIQUE constraints with _active suffix; prepare a rollback and safer
deployment plan by either (1) providing a rollback SQL that re-creates the
original indexes with their original definitions and drops the newly added
constraints (uq_users_phone_number_active, uq_users_email_provider_active,
uq_users_university_id_student_number_active,
uq_users_provider_provider_id_active), or (2) restructure the migration to
create the new UNIQUE constraints first (uq_users_..._active) and only drop the
old indexes after verification, and ensure the migration runs inside a
transaction or is tested in staging before production to guarantee
recoverability.

---

Outside diff comments:
In `@src/main/java/gg/agit/konect/domain/user/service/UserActivityService.java`:
- Around line 18-35: The two methods updateLastLoginAt and updateLastActivityAt
currently handle missing users differently: updateLastLoginAt calls
userRepository.getById(userId) which will throw if the user is absent, while
updateLastActivityAt uses userRepository.findById(...).ifPresent(...) and
silently skips; make the behavior consistent by either (A) changing
updateLastLoginAt to use userRepository.findById(userId).ifPresent(user ->
user.updateLastLoginAt(...)) to silently ignore missing users like
updateLastActivityAt, or (B) if throwing is intended, update
updateLastActivityAt to call
userRepository.getById(userId).updateLastActivityAt(...) and add a clarifying
comment on the intended semantics; update the code path accordingly and keep the
null check for userId.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c1f2b7e and e591baf.

📒 Files selected for processing (6)
  • src/main/java/gg/agit/konect/domain/user/model/User.java
  • src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java
  • src/main/java/gg/agit/konect/domain/user/service/UserActivityService.java
  • src/main/java/gg/agit/konect/domain/user/service/UserService.java
  • src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java
  • src/main/resources/db/migration/V40__add_user_soft_delete_and_active_unique_constraints.sql
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Agent
🧰 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/V40__add_user_soft_delete_and_active_unique_constraints.sql
**/*

⚙️ CodeRabbit configuration file

**/*: 공통 리뷰 톤 가이드:

  • 모든 코멘트는 첫 줄에 [LEVEL: ...] 태그를 포함한다.
  • 과장된 표현 없이 사실 기반으로 작성한다.
  • 한 코멘트에는 하나의 이슈만 다룬다.
  • 코드 예시가 필요하면 최소 수정 예시를 제시한다.
  • 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.

Files:

  • src/main/resources/db/migration/V40__add_user_soft_delete_and_active_unique_constraints.sql
  • src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java
  • src/main/java/gg/agit/konect/domain/user/service/UserActivityService.java
  • src/main/java/gg/agit/konect/domain/user/service/UserService.java
  • src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java
  • src/main/java/gg/agit/konect/domain/user/model/User.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/global/auth/oauth/OAuthLoginHelper.java
  • src/main/java/gg/agit/konect/domain/user/service/UserActivityService.java
  • src/main/java/gg/agit/konect/domain/user/service/UserService.java
  • src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java
  • src/main/java/gg/agit/konect/domain/user/model/User.java
🔇 Additional comments (2)
src/main/java/gg/agit/konect/domain/user/service/UserService.java (1)

213-229: 소프트 삭제 전환 구현이 적절함

하드 삭제에서 소프트 삭제로의 전환이 올바르게 구현됨. Apple 토큰 해지 → withdraw → save → 이벤트 발행 순서가 적절하며, 트랜잭션 범위 내에서 일관성 있게 처리됨.

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

18-146: 소프트 삭제 필터링이 일관되게 적용됨

대부분의 조회 메서드에 deletedAt IS NULL 조건이 추가되어 소프트 삭제된 사용자가 일반 조회에서 제외됨. 복구용 메서드(findFirst...DeletedAtIsNotNull...)도 적절히 분리되어 있음.

Comment thread src/main/java/gg/agit/konect/domain/user/model/User.java
Comment thread src/main/java/gg/agit/konect/domain/user/model/User.java

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

이 PR은 사용자 삭제를 하드 삭제에서 소프트 삭제 방식으로 전환하는 중요한 변경 사항을 포함합니다. 사용자 탈퇴 시 데이터를 완전히 제거하는 대신, deleted_at 타임스탬프를 설정하여 논리적으로 삭제 처리하며, OAuth 로그인 시 7일 이내 탈퇴한 계정을 자동으로 복구하는 기능을 추가했습니다.

Changes:

  • 데이터베이스 마이그레이션을 통해 deleted_at 컬럼과 active_flag 생성 컬럼 추가, 유니크 제약 조건에 active_flag 포함
  • User 엔티티에 소프트 삭제 관련 필드 및 메서드(withdraw, restore, canRestore) 추가, 탈퇴 사용자 이름 마스킹 로직 구현
  • UserRepository의 모든 조회 메서드에 deleted_at IS NULL 필터 추가
  • UserService.deleteUser를 하드 삭제에서 소프트 삭제로 변경
  • OAuthLoginHelper에 7일 유예 기간 내 탈퇴 계정 자동 복구 로직 추가
  • UserActivityService.updateLastActivityAtgetById에서 findById + ifPresent 패턴으로 변경

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
V40__add_user_soft_delete_and_active_unique_constraints.sql 소프트 삭제를 위한 deleted_at 컬럼과 active_flag 생성 컬럼 추가, 유니크 제약 조건에 active_flag 포함하여 재생성
User.java deleted_at, active_flag 필드 추가, getName() 오버라이드로 탈퇴 사용자 마스킹, withdraw/restore/canRestore 메서드 구현, 유니크 제약 조건에 active_flag 추가
UserRepository.java 모든 조회 메서드에 deleted_at IS NULL 필터 추가, 탈퇴 사용자 조회 메서드 추가, deleteByUserId 메서드 제거
UserService.java deleteUser 메서드를 하드 삭제에서 소프트 삭제(withdraw + save)로 변경
UserActivityService.java updateLastActivityAt을 안전한 Optional 패턴(findById + ifPresent)으로 변경
OAuthLoginHelper.java 7일 유예 기간 내 탈퇴 계정 자동 복구 로직 추가 (restoreWithdrawnByProviderId, restoreWithdrawnByEmail)

Comment thread src/main/java/gg/agit/konect/domain/user/model/User.java
Comment thread src/main/java/gg/agit/konect/domain/user/model/User.java
Comment thread src/main/java/gg/agit/konect/domain/user/model/User.java
@dh2906
dh2906 merged commit c173151 into develop Feb 27, 2026
6 checks passed
@dh2906
dh2906 deleted the fix/user-soft-delete branch February 28, 2026 04:53
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.

2 participants