Skip to content

[Feat] 회원탈퇴기능 추가#77

Open
gtmong0077 wants to merge 1 commit into
mainfrom
feat/withdrawal
Open

[Feat] 회원탈퇴기능 추가#77
gtmong0077 wants to merge 1 commit into
mainfrom
feat/withdrawal

Conversation

@gtmong0077

@gtmong0077 gtmong0077 commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

관련 이슈번호

Key Changes

  1. 내용
    • user rows를 다 삭제하였습니다.
    • Owner가 회원탈퇴시 프로젝트를 삭제하였고 Admin이 회원탈퇴시 관리자목록에서 삭제하였습니다.
    • 그 후 google oauth 토큰을 삭제, 탈퇴한 회원에게 갔던 알림 삭제, 그 회원이 썼던 댓글도 삭제하였습니다.

To Reviewers

  • 프로필 이미지는 삭제하려했으나 테스트하기 어려울 것 같아 주석처리해두었습니다.

Summary by CodeRabbit

릴리스 노트

  • New Features
    • 사용자가 자신의 계정을 삭제할 수 있는 기능 추가
    • 계정 삭제 시 관련 데이터(알림 설정, 프로젝트 정보, 댓글) 자동 정리

@gtmong0077

Copy link
Copy Markdown
Contributor Author

@code rabbit

@coderabbitai

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown

개요

사용자 계정 삭제 기능이 도메인 이벤트 기반으로 구현되었습니다. 새로운 UserDeletedEvent가 발행되면 알림, 프로젝트, 모집 모듈의 이벤트 리스너들이 각각의 데이터를 정리하여 계단식 삭제를 수행합니다.

변경사항

집합 / 파일(s) 요약
도메인 이벤트
src/main/java/com/campusform/server/global/event/UserDeletedEvent.java
사용자 삭제 이벤트를 나타내는 새로운 레코드 도입. userId 필드 포함.
사용자 서비스 및 컨트롤러
src/main/java/com/campusform/server/identity/application/service/UserService.java, src/main/java/com/campusform/server/identity/presentation/UserAuthController.java
deleteUser(Long userId) 트랜잭션 메서드 추가. ApplicationEventPublisherUserDeletedEvent 발행. DELETE /api/users/me 엔드포인트 신설.
사용자 저장소
src/main/java/com/campusform/server/identity/domain/repository/UserRepository.java, src/main/java/com/campusform/server/identity/infrastructure/persistence/UserRepositoryImpl.java
delete(User user) 메서드 추가하여 삭제 작업 지원.
알림 모듈
src/main/java/com/campusform/server/notification/application/service/NotificationUserEventListener.java, src/main/java/com/campusform/server/notification/domain/repository/UserNotificationSettingsRepository.java, src/main/java/com/campusform/server/notification/infrastructure/persistence/NotificationJpaRepository.java, src/main/java/com/campusform/server/notification/infrastructure/persistence/UserNotificationSettingsRepositoryImpl.java
UserDeletedEvent 수신 시 사용자의 알림 설정 및 모든 알림 삭제. deleteByReceiverId(Long receiverId) 쿼리 메서드 추가.
프로젝트 모듈
src/main/java/com/campusform/server/project/application/service/ProjectUserEventListener.java
UserDeletedEvent 수신 시 사용자가 소유한 프로젝트는 삭제, 관리자인 프로젝트에서만 제거. OAuth 토큰도 정리.
모집 모듈
src/main/java/com/campusform/server/recruiting/application/service/RecruitingUserEventListener.java, src/main/java/com/campusform/server/recruiting/infrastructure/persistence/CommentRepository.java
UserDeletedEvent 수신 시 사용자의 모든 댓글 삭제. deleteByAuthorId(Long authorId) 메서드 추가.

순서도

sequenceDiagram
    participant User as 사용자
    participant Controller as UserAuthController
    participant Service as UserService
    participant Repo as UserRepository
    participant Publisher as ApplicationEventPublisher
    participant NotifListener as NotificationUserEventListener
    participant ProjListener as ProjectUserEventListener
    participant RecListener as RecruitingUserEventListener
    
    User->>Controller: DELETE /api/users/me
    Controller->>Service: deleteUser(userId)
    Service->>Repo: delete(user)
    Repo->>Repo: 사용자 삭제 완료
    Service->>Publisher: publish(UserDeletedEvent)
    
    par 병렬 이벤트 처리
        Publisher->>NotifListener: handleUserDeletedEvent
        NotifListener->>NotifListener: 알림 설정 삭제
        NotifListener->>NotifListener: 알림 삭제
    and
        Publisher->>ProjListener: handleUserDeletedEvent
        ProjListener->>ProjListener: 프로젝트 정리
        ProjListener->>ProjListener: OAuth 토큰 삭제
    and
        Publisher->>RecListener: handleUserDeletedEvent
        RecListener->>RecListener: 댓글 삭제
    end
Loading

예상 코드 리뷰 소요 시간

🎯 3 (보통) | ⏱️ ~20분

토끼의 축시

🐰 사용자가 떠날 때도 우아하게,
이벤트는 흘러 각 방에 전해지고,
알림, 프로젝트, 댓글 모두 깔끔이 정리되네!
데이터는 순환하고 시스템은 건강해지는 날 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 변경 사항의 핵심을 명확하게 반영하고 있으며, 회원탈퇴 기능 추가라는 주요 변경 사항을 잘 설명합니다.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/withdrawal

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.

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

🤖 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/com/campusform/server/identity/application/service/UserService.java`:
- Around line 126-136: Re-enable and harden the S3 profile-image cleanup in the
user-deletion flow: in UserService (the method that removes the User row),
restore the TransactionSynchronizationManager.registerSynchronization block that
reads user.getProfileImageUrl(), and in afterCommit call
s3Service.deleteFile(profileImageUrl) so file deletion happens only after DB
commit; add a null-check for profileImageUrl, wrap the s3Service.deleteFile call
in a try/catch to log failures (use log.info/log.error) and avoid failing the
transaction, and ensure the block references TransactionSynchronization
(afterCommit) so cleanup runs asynchronously post-commit.

In
`@src/main/java/com/campusform/server/recruiting/infrastructure/persistence/CommentRepository.java`:
- Around line 51-54: The derived repository method deleteByAuthorId in
CommentRepository triggers entity loading and per-entity removes; replace it
with a true bulk DELETE by annotating a custom repository method (in
CommentRepository) with `@Modifying` and `@Query`("DELETE FROM Comment c WHERE
c.authorId = :authorId") and ensure the call runs in a `@Transactional` context
(either annotate the repository method or call from a `@Transactional` service)
and add imports for org.springframework.data.jpa.repository.Modifying and Query;
this makes the delete a JPQL bulk delete and avoids loading all Comment entities
into memory.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 449fd742-f2c6-403a-83c5-bdb3e73cc952

📥 Commits

Reviewing files that changed from the base of the PR and between 60af589 and 1810017.

📒 Files selected for processing (12)
  • src/main/java/com/campusform/server/global/event/UserDeletedEvent.java
  • src/main/java/com/campusform/server/identity/application/service/UserService.java
  • src/main/java/com/campusform/server/identity/domain/repository/UserRepository.java
  • src/main/java/com/campusform/server/identity/infrastructure/persistence/UserRepositoryImpl.java
  • src/main/java/com/campusform/server/identity/presentation/UserAuthController.java
  • src/main/java/com/campusform/server/notification/application/service/NotificationUserEventListener.java
  • src/main/java/com/campusform/server/notification/domain/repository/UserNotificationSettingsRepository.java
  • src/main/java/com/campusform/server/notification/infrastructure/persistence/NotificationJpaRepository.java
  • src/main/java/com/campusform/server/notification/infrastructure/persistence/UserNotificationSettingsRepositoryImpl.java
  • src/main/java/com/campusform/server/project/application/service/ProjectUserEventListener.java
  • src/main/java/com/campusform/server/recruiting/application/service/RecruitingUserEventListener.java
  • src/main/java/com/campusform/server/recruiting/infrastructure/persistence/CommentRepository.java

Comment on lines +126 to +136
// // 프로필 이미지가 있다면 S3에서도 삭제
// String profileImageUrl = user.getProfileImageUrl();
// if (profileImageUrl != null) {
// TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
// @Override
// public void afterCommit() {
// s3Service.deleteFile(profileImageUrl);
// log.info("S3 프로필 이미지 삭제 완료 (회원탈퇴): {}", profileImageUrl);
// }
// });
// }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

회원탈퇴 시 프로필 이미지 삭제 로직 주석 처리 상태는 개인정보 잔존 리스크입니다.

Line 126-136이 비활성화되어 탈퇴 후에도 S3 프로필 이미지가 남을 수 있습니다. 계정 삭제 흐름에서는 DB row뿐 아니라 외부 저장소의 개인정보도 함께 정리되어야 합니다.

🔧 제안 수정안
-        // // 프로필 이미지가 있다면 S3에서도 삭제
-        // String profileImageUrl = user.getProfileImageUrl();
-        // if (profileImageUrl != null) {
-        //     TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
-        //         `@Override`
-        //         public void afterCommit() {
-        //             s3Service.deleteFile(profileImageUrl);
-        //             log.info("S3 프로필 이미지 삭제 완료 (회원탈퇴): {}", profileImageUrl);
-        //         }
-        //     });
-        // }
+        // 프로필 이미지가 있다면 트랜잭션 커밋 후 S3에서도 삭제
+        String profileImageUrl = user.getProfileImageUrl();
+        if (profileImageUrl != null) {
+            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+                `@Override`
+                public void afterCommit() {
+                    try {
+                        s3Service.deleteFile(profileImageUrl);
+                        log.info("S3 프로필 이미지 삭제 완료 (회원탈퇴): {}", profileImageUrl);
+                    } catch (Exception e) {
+                        log.error("S3 프로필 이미지 삭제 실패 (회원탈퇴): {}", profileImageUrl, e);
+                    }
+                }
+            });
+        }

원하시면 이 부분을 테스트 가능한 구조(예: 삭제 전담 컴포넌트 분리 + 이벤트/모킹 테스트)로 정리해 새 이슈 템플릿까지 같이 제안드릴 수 있습니다.

📝 Committable suggestion

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

Suggested change
// // 프로필 이미지가 있다면 S3에서도 삭제
// String profileImageUrl = user.getProfileImageUrl();
// if (profileImageUrl != null) {
// TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
// @Override
// public void afterCommit() {
// s3Service.deleteFile(profileImageUrl);
// log.info("S3 프로필 이미지 삭제 완료 (회원탈퇴): {}", profileImageUrl);
// }
// });
// }
// 프로필 이미지가 있다면 트랜잭션 커밋 후 S3에서도 삭제
String profileImageUrl = user.getProfileImageUrl();
if (profileImageUrl != null) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
`@Override`
public void afterCommit() {
try {
s3Service.deleteFile(profileImageUrl);
log.info("S3 프로필 이미지 삭제 완료 (회원탈퇴): {}", profileImageUrl);
} catch (Exception e) {
log.error("S3 프로필 이미지 삭제 실패 (회원탈퇴): {}", profileImageUrl, e);
}
}
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/campusform/server/identity/application/service/UserService.java`
around lines 126 - 136, Re-enable and harden the S3 profile-image cleanup in the
user-deletion flow: in UserService (the method that removes the User row),
restore the TransactionSynchronizationManager.registerSynchronization block that
reads user.getProfileImageUrl(), and in afterCommit call
s3Service.deleteFile(profileImageUrl) so file deletion happens only after DB
commit; add a null-check for profileImageUrl, wrap the s3Service.deleteFile call
in a try/catch to log failures (use log.info/log.error) and avoid failing the
transaction, and ensure the block references TransactionSynchronization
(afterCommit) so cleanup runs asynchronously post-commit.

Comment on lines +51 to +54
/**
* 특정 작성자의 모든 댓글 삭제
*/
void deleteByAuthorId(Long authorId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Spring Data JPA에서 derived deleteBy... 메서드가 벌크 DELETE로 실행되는지, 아니면 엔티티 단위 삭제 경로를 타는지 공식 문서 기준으로 확인해줘.

💡 Result:

Spring Data JPA에서 derived deleteBy... 메서드(예: deleteByRoleId)는 벌크 DELETE로 실행되지 않고, 엔티티 단위 삭제 경로를 타며, lifecycle 이벤트(@PreRemove 등)를 보장하기 위해 SELECT 쿼리로 엔티티를 먼저 로드한 후 CrudRepository.delete(Iterable)를 호출하여 하나씩 삭제합니다. 공식 문서에 명시되어 있습니다.

Citations:


탈퇴 정리 경로는 벌크 DELETE로 명시하는 편이 필수입니다.

Line 54의 파생 메서드 deleteByAuthorId()는 내부적으로 모든 해당 엔티티를 먼저 SELECT로 로드한 후, 라이프사이클 이벤트(@PreRemove 등)를 보장하기 위해 엔티티를 하나씩 삭제합니다. 탈퇴 일괄정리처럼 대량의 댓글을 삭제해야 하는 경우 메모리 오버헤드와 성능 저하가 발생하므로, @Modifying + JPQL DELETE로 진정한 벌크 DELETE를 명시하는 것이 필수적입니다.

변경 제안
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
@@
-    void deleteByAuthorId(Long authorId);
+    `@Modifying`(clearAutomatically = true, flushAutomatically = true)
+    `@Query`("DELETE FROM Comment c WHERE c.authorId = :authorId")
+    int deleteByAuthorId(`@Param`("authorId") Long authorId);
📝 Committable suggestion

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

Suggested change
/**
* 특정 작성자의 모든 댓글 삭제
*/
void deleteByAuthorId(Long authorId);
/**
* 특정 작성자의 모든 댓글 삭제
*/
`@Modifying`(clearAutomatically = true, flushAutomatically = true)
`@Query`("DELETE FROM Comment c WHERE c.authorId = :authorId")
int deleteByAuthorId(`@Param`("authorId") Long authorId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/campusform/server/recruiting/infrastructure/persistence/CommentRepository.java`
around lines 51 - 54, The derived repository method deleteByAuthorId in
CommentRepository triggers entity loading and per-entity removes; replace it
with a true bulk DELETE by annotating a custom repository method (in
CommentRepository) with `@Modifying` and `@Query`("DELETE FROM Comment c WHERE
c.authorId = :authorId") and ensure the call runs in a `@Transactional` context
(either annotate the repository method or call from a `@Transactional` service)
and add imports for org.springframework.data.jpa.repository.Modifying and Query;
this makes the delete a JPQL bulk delete and avoids loading all Comment entities
into memory.

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