Skip to content

Feat/#127 회원 탈퇴 - User 엔티티 삭제 기능 추가#135

Merged
ojy0903 merged 45 commits into
developfrom
feat/#127
May 26, 2026
Merged

Feat/#127 회원 탈퇴 - User 엔티티 삭제 기능 추가#135
ojy0903 merged 45 commits into
developfrom
feat/#127

Conversation

@ojy0903
Copy link
Copy Markdown
Collaborator

@ojy0903 ojy0903 commented May 17, 2026

📌 관련 이슈

🚀 개요

이번 PR에서 변경된 핵심 내용을 요약해주세요.

회원 탈퇴 기능과 함께 연관된 엔티티 제거 로직 추가

📄 작업 내용

구체적인 작업 내용을 설명해주세요.

  • 회원 탈퇴 API 추가
  • 관련 엔티티(Organization, AuthProviderAccount, 등) 제거 or 예외 처리 로직 추가

📸 스크린샷 / 테스트 결과 (선택)

결과물 확인을 위한 사진이나 테스트 로그를 첨부해주세요.

탈퇴 API 테스팅을 위한 회원 가입 진행 (userId=14)
최초 회원가입


userId=14 인 회원이 조직 생성(owner 가 userId=14 이고 다른 멤버 없는 상태)
삭제용 조직 생성
조직 DB


userId=14 의 조직 가입 상태 (org_id = 10 은 생성자, org_id = 1, 2, 3 은 MEMBER)
회원 조직 가입 상태(org_id=10 은 생성자, 1,2,3 은 MEMBER)


회원 탈퇴 진행 결과 -> 회원 삭제, 조직 Soft Delete, OrgMember 관련 데이터들 모두 삭제 회원 탈퇴 진행 결과 삭제 후 회원 테이블 삭제 후 조직 테이블 삭제 후 org_member 테이블




===오류 테스트 (다른 회원이 조직에 속한 경우)===

오류 테스트를 위해 회원, 조직 복구
오류테스트 - 회원 복구
오류테스트 - 조직 복구


user_id = 3 이 탈퇴하려는 회원이 owner 인 조직에 MEMBER 로 속해있는 상태 오류테스트 - orgMember userId=3 이 조직의 멤버
탈퇴 시도 시 오류 다른멤버속한조직오류

✅ 체크리스트

  • [✅] 브랜치 전략(GitHub Flow)을 준수했나요?
  • [✅] 메서드 단위로 코드가 잘 쪼개져 있나요?
  • [✅] 테스트 통과 확인
  • [✅] 서버 실행 확인
  • [✅] API 동작 확인

🔍 리뷰 포인트 (Review Points)

리뷰어가 중점적으로 확인했으면 하는 부분을 적어주세요. (P1~P4 적용 가이드)

  • 지금 로직에서는 회원이 속한 조직들 중에서 "회원이 owner 인 조직이 있는데 그 조직에 다른 멤버가 속해있을 경우" 오류가 발생하고, "회원이 연결한 PlatformConnection" 이 존재할 경우 오류가 발생합니다.
  • 일단 구현을 하긴 했는데... 회원 탈퇴 외적으로 추가해야할 로직이 몇가지 더 생겨서 그것도 추가 작업으로 진행해야 할 것 같습니다.
    1. 지금 회원탈퇴 로직에서는 탈퇴하려는 회원과 연관된 PlatformConnection 이 존재할 시 오류를 발생합니다. -> 조직 내에서 PlatformConnection 과 PlatformAccount 를 삭제하는 API 를 추가해야 할 듯 합니다...
    1. 위 1번에서 PlatformConnection 과 PlatformAccount 을 삭제하면서 연관된 광고 관련 엔티티(Project ,AdCampaign, AdGroup, AdContent, MetricFact) 도 함께 삭제 진행하도록 해야할 것 같습니다.
    1. 기존 OrgServiceImpl 에서 조직 삭제 진행 시 연관된 광고 엔티티와 PlatformConnection, PlatformAccount 를 같이 제거해주도록 수정해야 할 것 같습니다...

===의논점===

  • 지금 회원 탈퇴를 진행하게 되면 본인 혼자 속한 Organization 에 대해서는 Soft Delete, 나머지 연관된 Timeline, OrgMember, AIInsightReport, OrgInvitation 은 Hard Delete 되는 방식으로 진행했습니다. 추후 위에 1,2 번 API 가 개발되면 PlatformConnection 과 PlatformAccount Hard Delete 유도 -> 사용자가 해당 API 호출해 제거하고 나면 Organization Timeline, OrgMember, AIInsightReport, OrgInvitation 를 Hard Delete 하는 방식을 고려중입니다...
  • 생각보다 User 엔티티 관련 연관관계 다뤄야 하는게 많아서 다른 API 개발이랑 엮으려면 오래 걸릴 수 있을 듯 합니다... 일단 이 PR 을 Draft 로 바꾸고 다른 API 완성한뒤에 다시 전환하는 것도 고려중입니다.

💬 리뷰어 가이드 (P-Rules)
P1: 필수 반영 (Critical) - 버그 가능성, 컨벤션 위반. 해결 전 머지 불가.
P2: 적극 권장 (Recommended) - 더 나은 대안 제시. 가급적 반영 권장.
P3: 제안 (Suggestion) - 아이디어 공유. 반영 여부는 드라이버 자율.
P4: 단순 확인/칭찬 (Nit) - 사소한 오타, 칭찬 등 피드백.

Summary by CodeRabbit

  • New Features

    • 조직 소유자 양도 API 및 소유자 변경 기능 추가
    • 로그인 사용자의 계정 탈퇴(회원 탈퇴) 엔드포인트 추가
    • 광고 플랫폼 연동 해제(계정 삭제) 엔드포인트 추가
    • 소프트 삭제 후 일정 기간 경과 계정 자동 완전 삭제 스케줄러 추가
  • Behavior Changes

    • 조직/사용자 삭제 시 초대, 타임라인, AI 인사이트 리포트 등 연관 데이터 일괄 정리 강화
    • 플랫폼 연동 존재 시 삭제 제한 검사 추가
    • 소프트 삭제 처리 흐름 간소화
  • Errors / Messages

    • 소유자 변경·탈퇴 관련 신규 오류 코드 및 상세 메시지 추가

Review Change Stack

@ojy0903 ojy0903 self-assigned this May 17, 2026
@ojy0903 ojy0903 added the ✨ Feature 새로운 기능 추가 label May 17, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

조직 소유자 변경 API와 관련 계약·엔티티·서비스·문서를 추가하고, 조직 삭제 시 연관 데이터(AIInsightReport/Timeline/OrgInvitation 등) 정리용 리포지토리 메서드를 도입하며, 회원 탈퇴(soft→soft+scheduled hard) 흐름과 스케줄러/하드삭제 실행기를 추가합니다.

Changes

조직 소유자 양도 및 회원 탈퇴 기능 추가

Layer / File(s) Summary
소유자 양도 — DTO·엔티티·오류 코드
src/main/java/.../OrgRequest.java, src/main/java/.../Organization.java, src/main/java/.../OrgErrorCode.java
ChangeOwner 요청 DTO, Organization.changeOwner(Long) 메서드, ORG_OWNER_SAME_AS_BEFORE 오류 코드가 추가됩니다.
소유자 양도 — 서비스·컨트롤러·문서
src/main/java/.../OrgService.java, src/main/java/.../OrgServiceImpl.java, src/main/java/.../OrgController.java, src/main/java/.../OrgControllerDocs.java
OrgService에 changeOwner 계약을 추가하고 구현에서 요청자/기존 오너/새 오너 검증 후 소유자 변경을 수행합니다. PATCH /api/org/{orgId}/changeOwner 엔드포인트와 Swagger 문서가 추가됩니다.
조직 정리 — 리포지토리 메서드 추가
src/main/java/.../AIInsightReportRepository.java, src/main/java/.../TimelineRepository.java, src/main/java/.../OrgInvitationRepository.java, src/main/java/.../OrgMemberRepository.java, src/main/java/.../OrgRepository.java
조직 ID 또는 이메일/유저 ID 기준의 JPQL 일괄 삭제 메서드(deleteByOrganizationId, deleteByEmail, deleteByUserId)와 조직 조회용 쿼리(findAllByOwnerUserIdAndStatus)가 추가됩니다.
조직 정리 — 서비스 호출 지점
src/main/java/.../OrgServiceImpl.java
조직 제거(하드/소프트) 흐름에 플랫폼 연동 검사 추가 및 관련 리포지토리의 deleteByOrganizationId 호출로 정리 순서를 변경하고, soft delete 경로를 단순화합니다.
회원 탈퇴 — 계약·리포지토리 준비
src/main/java/.../PlatformConnectionRepository.java, src/main/java/.../UserErrorCode.java, src/main/java/.../UserRepository.java, src/main/java/.../AuthProviderAccountRepository.java
플랫폼 연동 조회용 findByUser_Id, 탈퇴 관련 오류 코드(USER_400_9, USER_400_10), 소프트 삭제 임계 조회 findAllByStatusAndDeletedAtBefore, AuthProviderAccount 하드 삭제 시그니처 등이 추가됩니다.
회원 탈퇴 — 서비스·스케줄러·실행기·API
src/main/java/.../UserService.java, src/main/java/.../UserController.java, src/main/java/.../UserControllerDocs.java, src/main/java/.../UserDeleteScheduler.java, src/main/java/.../UserDeleteExecutor.java, src/main/java/.../User.java
deleteUser(Long userId)를 추가해 플랫폼 연동 검사, 소유 조직 처리(조건부 soft-delete 호출), 관련 초대/토큰 삭제, 사용자 soft-delete를 수행합니다. 또한 주기적 하드 삭제 스케줄러와 REQUIRES_NEW 기반 UserDeleteExecutor 및 User.deletedAt/softDeleteUser()가 추가됩니다.
플랫폼 연동 해제 — API·서비스
src/main/java/.../PlatformService.java, src/main/java/.../PlatformServiceImpl.java, src/main/java/.../PlatformController.java
플랫폼 연결 해제용 disconnectPlatform(userId, orgId, accountId) 계약과 컨트롤러 엔드포인트가 추가되었고, 구현체에 빈 TODO 메서드가 추가됩니다.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant OrgController
  participant OrgService
  participant OrgMemberRepository
  participant Organization
  Client->>OrgController: PATCH /api/org/{orgId}/changeOwner
  OrgController->>OrgService: changeOwner(userId, orgId, request)
  OrgService->>OrgMemberRepository: find org member for newOwnerUserId
  OrgMemberRepository-->>OrgService: OrgMember
  OrgService->>Organization: changeOwner(newOwnerUserId)
  Organization-->>OrgService: done
  OrgService-->>OrgController: void
  OrgController-->>Client: 200 OK
Loading
sequenceDiagram
  participant Client
  participant UserController
  participant UserService
  participant PlatformConnectionRepository
  participant OrgService
  participant OrgMemberRepository
  participant AuthProviderAccountRepository
  participant UserRepository
  participant S3UploadService
  Client->>UserController: DELETE /my
  UserController->>UserService: deleteUser(userId)
  UserService->>PlatformConnectionRepository: findByUser_Id(userId)
  alt platform connections exist
    UserService-->>UserController: USER_HAS_PLATFORM_CONNECTION (error)
  else
    UserService->>OrgService: removeOrganizationSoft(...) when owner and only member
    UserService->>OrgMemberRepository: deleteByUserId(userId)
    UserService->>AuthProviderAccountRepository: deleteByUserId(userId)
    UserService->>UserRepository: softDeleteUser()
    UserService->>S3UploadService: deleteImageFromUrl(profileImageUrl) (warn on failure)
    UserService-->>UserController: 200 OK
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

📡 API, 🗄️ DB

Suggested reviewers

  • jinnieusLab
  • kingmingyu
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning PR에 포함된 변경사항들이 회원 탈퇴 기능 구현 범위 내에 있습니다. 다만 PlatformController의 disconnectPlatform 메서드는 TODO 상태로 미완성이며, 조직 소유권 이전(changeOwner)은 추가 기능으로 구현되었습니다. PlatformController.disconnectPlatform 메서드가 빈 구현(TODO) 상태이므로, 이 메서드를 제거하거나 별도 PR로 분리하는 것을 검토해주세요. 조직 소유권 이전은 핵심 목표와 직결되지 않으면 별도 PR로 분리할 수 있습니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 핵심 기능을 명확히 반영하고 있습니다. '회원 탈퇴' 기능과 'User 엔티티 삭제'를 구체적으로 표현했습니다.
Description check ✅ Passed PR 설명이 필수 항목들(관련 이슈, 개요, 작업 내용, 테스트 결과, 체크리스트, 리뷰 포인트)을 모두 포함하고 있으며, 상세한 테스트 스크린샷과 함께 제공됩니다.
Linked Issues check ✅ Passed 구현 범위가 #127 요구사항을 기본적으로 충족합니다: User 엔티티 삭제 진행, 관련 Organization 처리(owner인 경우 soft delete), 회원 탈퇴 API 추가. 다만 PlatformConnection 관련 처리는 별도 작업으로 분리되었습니다.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#127

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
Copy Markdown

@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

🧹 Nitpick comments (2)
src/main/java/com/whereyouad/WhereYouAd/domains/platform/persistence/repository/PlatformConnectionRepository.java (1)

27-28: ⚡ Quick win

탈퇴 검증 용도라면 목록 조회 대신 존재 여부 조회로 바꾸는 게 좋습니다.

Line 27의 메서드는 존재 여부 확인만 필요한 경로에서 엔티티 리스트를 모두 로딩하게 되어 불필요한 DB/메모리 비용이 생깁니다. existsByUser_Id로 바꾸면 검증 쿼리가 훨씬 가벼워집니다.

♻️ 제안 diff
--- a/src/main/java/com/whereyouad/WhereYouAd/domains/platform/persistence/repository/PlatformConnectionRepository.java
+++ b/src/main/java/com/whereyouad/WhereYouAd/domains/platform/persistence/repository/PlatformConnectionRepository.java
@@
-    List<PlatformConnection> findByUser_Id(Long userId);
+    boolean existsByUser_Id(Long userId);
--- a/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java
+++ b/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java
@@
-        List<PlatformConnection> platformConnections = platformConnectionRepository.findByUser_Id(userId);
-        if (!platformConnections.isEmpty()) {
+        if (platformConnectionRepository.existsByUser_Id(userId)) {
             throw new UserHandler(UserErrorCode.USER_HAS_PLATFORM_CONNECTION);
         }

As per coding guidelines "JPA 사용 시 N+1 문제나 불필요한 쿼리가 발생하지 않는지 ... 체크하라."

🤖 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/whereyouad/WhereYouAd/domains/platform/persistence/repository/PlatformConnectionRepository.java`
around lines 27 - 28, The repository method findByUser_Id in
PlatformConnectionRepository loads full entity lists when only existence is
needed; replace it with a boolean existsByUser_Id(Long userId) to avoid fetching
entities and reduce DB/memory cost—update the repository interface to declare
existsByUser_Id and change any callers that expect List<PlatformConnection>
(e.g., user deletion validation paths) to use the boolean result accordingly.
src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/docs/UserControllerDocs.java (1)

127-137: ⚡ Quick win

Swagger 설명 문자열을 Java 17 Text Block으로 정리해 주세요.

현재 프로젝트는 Java 17로 설정되어 있어 Text Block을 충분히 활용할 수 있습니다. 긴 문자열을 +로 연결하면 수정할 때 \n 누락이나 오타가 쉽게 발생하는데, Text Block을 사용하면 실제 모양 그대로 작성할 수 있어 훨씬 안정적입니다.

♻️ 제안 diff
-            description = "AccessToken 을 헤더로 받아 현재 로그인한 회원을 탈퇴 처리합니다.\n\n" +
-                    "### 1. 사전 검증 (통과해야 탈퇴 가능)\n" +
-                    "- **광고 플랫폼 연동(PlatformConnection) 존재 여부**: 연동된 광고 플랫폼이 하나라도 남아 있으면 탈퇴가 차단됩니다. 먼저 모든 광고 플랫폼 연동을 해제한 뒤 다시 시도해야 합니다. -> 추후 삭제 API 추가 예정\n\n" +
-                    "- **본인이 소유자인 조직에 다른 멤버 존재**: 본인이 생성자(owner)인 조직에 본인 외 다른 멤버가 남아 있으면 탈퇴가 차단됩니다. `PATCH /api/org/{orgId}/changeOwner` API 로 소유권을 위임한 뒤 재시도해야 합니다.\n\n" +
-                    "### 2. 회원이 속한 워크스페이스(Organization) 처리\n" +
-                    "- **단순 ADMIN / MEMBER 로만 속해있는 조직** → 해당 가입 정보(OrgMember)만 제거됩니다. 조직 자체는 유지됩니다.\n\n" +
-                    "- **회원이 생성자(owner)이고 본인만 속한 조직** → 조직이 Soft Delete 처리되며, 해당 조직의 부수 데이터(보낸 초대장 / 활동 타임라인 / AI 인사이트 리포트)도 함께 정리됩니다. 조직 자체는 복구 가능한 상태(status = DELETED) 로 남습니다.\n\n" +
-                    "### 3. 회원 본인 데이터 정리\n" +
-                    "- 소유자가 아닌채로 속한 조직에서 탈퇴하는 회원 정보가 모두 제거됩니다.\n\n" +
-                    "- 소셜 로그인 사용자의 경우 연결된 소셜계정이 함께 제거됩니다.\n\n" +
-                    "- 회원 본인이 삭제된 후, 프로필 이미지가 S3 에서 삭제됩니다. S3 이미지 삭제 실패는 서버 로그로만 기록됩니다."
+            description = """
+                    AccessToken 을 헤더로 받아 현재 로그인한 회원을 탈퇴 처리합니다.
+
+                    ### 1. 사전 검증 (통과해야 탈퇴 가능)
+                    - **광고 플랫폼 연동(PlatformConnection) 존재 여부**: 연동된 광고 플랫폼이 하나라도 남아 있으면 탈퇴가 차단됩니다. 먼저 모든 광고 플랫폼 연동을 해제한 뒤 다시 시도해야 합니다. -> 추후 삭제 API 추가 예정
+
+                    - **본인이 소유자인 조직에 다른 멤버 존재**: 본인이 생성자(owner)인 조직에 본인 외 다른 멤버가 남아 있으면 탈퇴가 차단됩니다. `PATCH /api/org/{orgId}/changeOwner` API 로 소유권을 위임한 뒤 재시도해야 합니다.
+
+                    ### 2. 회원이 속한 워크스페이스(Organization) 처리
+                    - **단순 ADMIN / MEMBER 로만 속해있는 조직** → 해당 가입 정보(OrgMember)만 제거됩니다. 조직 자체는 유지됩니다.
+
+                    - **회원이 생성자(owner)이고 본인만 속한 조직** → 조직이 Soft Delete 처리되며, 해당 조직의 부수 데이터(보낸 초대장 / 활동 타임라인 / AI 인사이트 리포트)도 함께 정리됩니다. 조직 자체는 복구 가능한 상태(status = DELETED) 로 남습니다.
+
+                    ### 3. 회원 본인 데이터 정리
+                    - 소유자가 아닌채로 속한 조직에서 탈퇴하는 회원 정보가 모두 제거됩니다.
+
+                    - 소셜 로그인 사용자의 경우 연결된 소셜계정이 함께 제거됩니다.
+
+                    - 회원 본인이 삭제된 후, 프로필 이미지가 S3 에서 삭제됩니다. S3 이미지 삭제 실패는 서버 로그로만 기록됩니다.
+                    """
🤖 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/whereyouad/WhereYouAd/domains/user/presentation/docs/UserControllerDocs.java`
around lines 127 - 137, Replace the long concatenated description string in
UserControllerDocs (the assignment to description) with a Java 17 text block
("""...""") so the multi-line Swagger description is written verbatim; locate
the line containing description = "..." and swap the concatenated pieces for a
single text block, preserving the existing newlines and content and ensuring
indentation is trimmed appropriately for Java text blocks.
🤖 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/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java`:
- Around line 39-43: The ChangeOwner.record's newOwnerUserId currently only has
`@NotNull`; add a positive-number constraint (e.g., `@Positive` or `@Min`(1)) to
reject 0/negative IDs at validation time so invalid IDs don't reach the service
layer; update the annotation on newOwnerUserId in the ChangeOwner record to
include the chosen positive constraint and an appropriate message.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java`:
- Around line 312-314: The soft-delete path in OrgServiceImpl currently calls
cleanupOrganizationRelatedData(orgId) which hard-deletes related entities and
breaks restoreOrganization; remove that call from the soft-delete flow and
instead create a dedicated hard-cleanup method (e.g.,
performHardCleanupForWithdrawal or cleanupForWithdrawal) and call it only from
the explicit withdrawal/terminate flow. Update OrgServiceImpl to: 1) stop
invoking cleanupOrganizationRelatedData during soft deletes, 2) add a new public
method for permanent cleanup used by the withdrawal endpoint/process, and 3)
ensure restoreOrganization only toggles status and does not depend on removed
relations.
- Around line 501-512: The self-transfer check (Objects.equals(userId,
request.newOwnerUserId())) is performed before verifying the requester is the
current owner, causing non-owners to get ORG_OWNER_SAME_AS_BEFORE; move and
re-order the logic so you first load the organization via
orgRepository.findById(...) and validate ownership with
Objects.equals(organization.getOwnerUserId(), userId) (throw
OrgHandler(OrgErrorCode.ORG_FORBIDDEN) if false), and only after that perform
the self-transfer comparison against organization.getOwnerUserId() /
request.newOwnerUserId() and throw
OrgHandler(OrgErrorCode.ORG_OWNER_SAME_AS_BEFORE) when equal.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java`:
- Around line 231-233: The code calls deleteProfileImage(profileImageUrl)
immediately after userRepository.deleteById(userId) which may delete S3 content
before the DB transaction commits, causing data inconsistency on rollback;
change this so S3 deletion runs only after a successful transaction commit by
registering a post-commit hook (e.g.,
TransactionSynchronizationManager.registerSynchronization(...) with afterCommit)
or publish a domain event and handle it with an
`@TransactionalEventListener`(phase = TransactionPhase.AFTER_COMMIT) that invokes
deleteProfileImage; apply the same change for the similar block around lines
280-288 to ensure all external S3 deletes happen only after the DB transaction
commits.
- Around line 255-257: The current check in UserService uses
orgMemberRepository.countByOrganizationId(organization.getId()), which counts
all OrgMember records including inactive/cleanup users and can wrongly block a
user’s leave; update the logic to call
orgMemberRepository.countByOrganizationIdAndUserStatus(organization.getId(),
<activeStatus>) (using your existing active/normal UserStatus enum constant) so
only active members are counted before deciding on ownership transfer; ensure
the conditional still checks > 1 and adjust any import/usages if necessary.

---

Nitpick comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/platform/persistence/repository/PlatformConnectionRepository.java`:
- Around line 27-28: The repository method findByUser_Id in
PlatformConnectionRepository loads full entity lists when only existence is
needed; replace it with a boolean existsByUser_Id(Long userId) to avoid fetching
entities and reduce DB/memory cost—update the repository interface to declare
existsByUser_Id and change any callers that expect List<PlatformConnection>
(e.g., user deletion validation paths) to use the boolean result accordingly.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/docs/UserControllerDocs.java`:
- Around line 127-137: Replace the long concatenated description string in
UserControllerDocs (the assignment to description) with a Java 17 text block
("""...""") so the multi-line Swagger description is written verbatim; locate
the line containing description = "..." and swap the concatenated pieces for a
single text block, preserving the existing newlines and content and ensuring
indentation is trimmed appropriately for Java text blocks.
🪄 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: 656340f3-9458-4f72-a5d2-817c6922c131

📥 Commits

Reviewing files that changed from the base of the PR and between a7eb466 and f793b2f.

📒 Files selected for processing (16)
  • src/main/java/com/whereyouad/WhereYouAd/domains/ai/persistence/repository/AIInsightReportRepository.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/entity/Organization.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgInvitationRepository.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/persistence/repository/PlatformConnectionRepository.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/timeline/persistence/repository/TimelineRepository.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/user/exception/code/UserErrorCode.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/UserController.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/docs/UserControllerDocs.java

Comment on lines +39 to +43
public record ChangeOwner(
@Schema(description = "워크스페이스 새 소유자로 지정할 사용자의 DB Id", example = "1")
@NotNull(message = "새 소유자 Id 는 필수입니다.")
Long newOwnerUserId
) {}
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 | 🟡 Minor | ⚡ Quick win

newOwnerUserId는 양수 제약까지 추가하는 게 안전합니다.

Line 41의 @NotNull만으로는 0/음수 ID가 통과되어 서비스 계층에서 의미가 덜 맞는 에러로 떨어질 수 있습니다. 요청 검증 단계에서 차단해 주세요.

제안 수정안
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
@@
     public record ChangeOwner(
             `@Schema`(description = "워크스페이스 새 소유자로 지정할 사용자의 DB Id", example = "1")
             `@NotNull`(message = "새 소유자 Id 는 필수입니다.")
+            `@Positive`(message = "새 소유자 Id 는 1 이상이어야 합니다.")
             Long newOwnerUserId
     ) {}
📝 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
public record ChangeOwner(
@Schema(description = "워크스페이스 새 소유자로 지정할 사용자의 DB Id", example = "1")
@NotNull(message = "새 소유자 Id 는 필수입니다.")
Long newOwnerUserId
) {}
public record ChangeOwner(
`@Schema`(description = "워크스페이스 새 소유자로 지정할 사용자의 DB Id", example = "1")
`@NotNull`(message = "새 소유자 Id 는 필수입니다.")
`@Positive`(message = "새 소유자 Id 는 1 이상이어야 합니다.")
Long newOwnerUserId
) {}
🤖 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/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java`
around lines 39 - 43, The ChangeOwner.record's newOwnerUserId currently only has
`@NotNull`; add a positive-number constraint (e.g., `@Positive` or `@Min`(1)) to
reject 0/negative IDs at validation time so invalid IDs don't reach the service
layer; update the annotation on newOwnerUserId in the ChangeOwner record to
include the chosen positive constraint and an appropriate message.

Copy link
Copy Markdown
Collaborator

@kingmingyu kingmingyu left a comment

Choose a reason for hiding this comment

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

P1: 고생하셨습니다! 생각보다 유저와 연관된 엔티티가 많아서 조금 까다로운 것 같습니다. 다만.. 지금 로직이 소프트 딜리트와 하드 딜리트가 섞여있는 것 같습니다..!

제 생각에는..

  • Hard Delete를 구현 (DB FK에 맞춰 유저 관련 데이터 전부 삭제)
  • Soft Delete를 구현(User status만 Deleted로 변경)
  • Soft Delete된 상태가 1달이상 정도가 되면 스케줄러를 통해서 자동으로 HardDelete 메서드를 호출하는 방식
    으로 구현하는 것은 어떤가요..?

지금 Hard Delete하려면 유저가 먼저 삭제해야 하는 것들이 너무 많은 것 같아서 프론트 쪽에서 유저를 삭제하면 관련된 조직(소유자인 경우)등 모든 데이터가 삭제된다고 안내하고 관련된 모든 데이터를 DB FK 제약조건을 지켜서 순서대로 관련 데이터를 전부 삭제하면 괜찮을 것 같습니다..! 혹시 Metric_Fact부터 순차적으로 조직까지 삭제하고 PlatformConnection -> PlatformAccount 순서로 삭제하는 방식으로 FK 제약조건 맞추어서 삭제를 진행해도 오류가 날까요...? 이쪽에 뭔가 연관된 엔티티가 많아서 삭제하는 순서가 좀 어려운 것 같습니다..

추가로 만약 User에도 SoftDelete가 생긴다면 기존 쿼리문도 수정해야할 수도 있을 것 같습니다..! (WHERE 절에 USER.Status = ACTIVE 추가)

continue;
}

int memberCount = orgMemberRepository.countByOrganizationIdAndUserStatus(organization.getId(), UserStatus.ACTIVE);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P2: 아마 지금 구현하신 것이 Hard Delete 같은데 Soft Delete랑 좀 섞여서 구현된 것 같습니다..!

}

// 본인만 속한 조직은 OrgService 의 Soft Delete 로직 호출 (조직 연관된 엔티티 정리 포함)
orgService.removeOrganizationSoft(userId, organization.getId());
Copy link
Copy Markdown
Collaborator

@kingmingyu kingmingyu May 18, 2026

Choose a reason for hiding this comment

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

P2: 아마 조직 soft delete된 조직을 한달 이상된 조직에 대해서 삭제와 같이 주기적으로 삭제하는 코드가 없는 걸로 알고 있습니다..! 그래서 유저를 하드 딜리트하고 조직을 소프트 딜리트하게 된다면 해당 조직은 계속 DB에 남게 될 것 같습니다..! 또한 회원에 대해서는 Hard Delete이기 때문에 다시 복구할 방법도 없는 것 같습니다..!

Copy link
Copy Markdown
Collaborator

@jinnieusLab jinnieusLab left a comment

Choose a reason for hiding this comment

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

P3: 확인했습니다! 음 생각보다 회원 탈퇴가 복잡한 것 같아서 제 의견을 정리해봤습니다..! 백엔드 내부에서 1차 지연 삭제(Soft) -> 2차 물리 삭제(Batch)하는 식으로 가면 될 듯한데, 말씀해주신 대로 작업량이 꽤 될 듯 하네요..

1. 회원 탈퇴 요청
단, 탈퇴하는 유저가 소유자(Owner)라면, 다른 멤버에게 소유권을 이전 후 탈퇴하도록 함.

엔티티 삭제 여부 설명
User Soft Delete (status = DELETED) 한 달 유예 기간
PlatformConnection Hard Delete 유저 개인 인증 정보, 즉시 삭제
OrgMember Hard Delete 중간 테이블이니 삭제
RefreshToken Hard Delete (Redis에서 제거) 탈퇴 시 기존 JWT 토큰 즉시 무효화하기
OrgInvitation Hard Delete  탈퇴 이메일로 pending 중인 조직들 초대 정리 필요

만약 유저가 조직 내 유일한 멤버였다면, 조직 관련도 삭제 처리 (Soft Delete)

엔티티 삭제 여부 설명
Organization Soft Delete status = DELETED
PlatformAccount Soft Delete
AdCampaign ~ AdContent Soft Delete  
ClickLog / MetricFact 상태 변경 없이 유지 상위 엔티티(Project/Campaign) 활성 상태 여부만 체크

2. 유저 재가입 한 달 유예 기간 지난 후
스케줄러나 스프링 배치 도입하여 새벽에 작업 진행

  • ClickLog/MetricFact: chuck 단위만큼 쪼개서 안전하게 Hard Delete
  • Organization ~ AdContent: 연관 관계 역순으로 Hard Delete
  • User: 개인정보(이메일, 이름, 비밀번호 등) 마스킹/익명화 처리. (과거 소속되었던 조직의 데이터 무결성을 위해 Hard Delete하지 않음)

ClickLog나 MetricFact의 경우 양이 매우 많다면 일일이 삭제 시 DB 병목이나 락이 생길 수도 있을 듯하여, 삭제 시 스케줄러나 배치를 도입해보는 건 어떨까요?


  • UserWithdrawalFacade 등 파사드 패턴 도입
    여러 군데에서 로직이 흩어져 있는 걸 모아서 한 번에 관장하는 곳이 필요해보이는데, 파사드 클래스에서 하위에 UserService, OrganizationService, PlatformAuthService 등 주입받아 각자의 도메인 로직만 수행하도록 위임하는 것도 좋을 것 같습니다! (도메인간 결합도 낮추기)

정리해보니 수정이 꽤 오래 걸려서 작업량이 많아질 것 같아서 탈퇴 관련 작업 분배를 한 번 해야 할 듯합니다!

Copy link
Copy Markdown

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

🧹 Nitpick comments (2)
src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java (1)

23-26: ⚡ Quick win

@ModifyingclearAutomatically = true 추가를 권장합니다.

벌크 DELETE 쿼리 실행 후 영속성 컨텍스트에 캐시된 OrgMember 엔티티가 자동으로 제거되지 않습니다. 같은 트랜잭션 내에서 삭제된 엔티티를 조회하면 여전히 존재하는 것처럼 보일 수 있어요.

예를 들어:

// 1. OrgMember 조회 (영속성 컨텍스트에 캐시됨)
List<OrgMember> members = orgMemberRepository.findOrgMemberByUserId(userId);

// 2. 벌크 삭제 실행
orgMemberRepository.deleteByUserId(userId);

// 3. 다시 조회하면? → clearAutomatically 없으면 캐시된 데이터 반환 가능!
💡 제안 수정안
 // userId 기준 OrgMember 일괄 Hard Delete (User Hard Delete 정리용)
-@Modifying
+@Modifying(clearAutomatically = true)
 `@Query`("DELETE FROM OrgMember om WHERE om.user.id = :userId")
 void deleteByUserId(`@Param`("userId") Long userId);
🤖 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/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java`
around lines 23 - 26, The bulk DELETE in OrgMemberRepository.deleteByUserId uses
`@Modifying` but doesn't set clearAutomatically, so deleted OrgMember entities can
remain in the persistence context and appear to still exist within the same
transaction; update the `@Modifying` annotation on the deleteByUserId method to
include clearAutomatically = true so the persistence context is cleared after
the bulk delete and stale OrgMember cache entries are removed.
src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/entity/User.java (1)

48-49: ⚡ Quick win

LocalDate 대신 LocalDateTime 사용을 고려해 주세요.

탈퇴 시각을 LocalDate로 저장하면 날짜만 기록되고 시/분/초 정보가 유실됩니다. 운영 중 문제 추적이나 감사 로그 분석 시 정확한 탈퇴 시각이 필요할 수 있어요.

예를 들어, 같은 날 여러 사용자가 탈퇴했을 때 순서를 파악하거나, 탈퇴 후 일정 시간 내 복구 요청이 들어왔을 때 시간 단위 검증이 필요할 수 있습니다.

💡 제안 수정안
-import java.time.LocalDate;
+import java.time.LocalDateTime;

 `@Column`(nullable = true, name = "deleted_at")
-private LocalDate deletedAt;
+private LocalDateTime deletedAt;

그리고 softDeleteUser() 메서드도:

 public void softDeleteUser() {
     this.status = UserStatus.DELETED;
-    this.deletedAt = LocalDate.now();
+    this.deletedAt = LocalDateTime.now();
 }
🤖 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/whereyouad/WhereYouAd/domains/user/persistence/entity/User.java`
around lines 48 - 49, User 엔티티의 deletedAt 필드가 LocalDate로 선언되어 있어 시/분/초 정보가
손실됩니다; User 클래스의 private LocalDate deletedAt를 LocalDateTime으로 변경하고, 관련
getter/setter와 매핑 어노테이션(`@Column` name="deleted_at")은 그대로 유지하되 타입을 LocalDateTime으로
업데이트하세요; 또한 softDeleteUser() 메서드(혹은 관련 서비스/레포지토리에서 삭제 시각을 설정하는 메서드)를 찾아 현재
LocalDate로 할당하는 부분을 LocalDateTime.now() 또는 적절한 시점의 LocalDateTime 값으로 변경해 일관된 타입을
사용하도록 수정하세요.
🤖 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/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java`:
- Around line 329-358: The removeOrganizationsOwnedBySoftDeletedUser method
omits cleaning up OrgInvitation records and should mirror removeOrganization;
before deleting the Organization, call
orgInvitationRepository.deleteByOrganizationId(orgId) (using the same orgId
variable) to remove related invitations so no orphaned OrgInvitation rows or FK
violations remain; add this call alongside
timelineRepository.deleteByOrganizationId(orgId) and
aiInsightReportRepository.deleteByOrganizationId(orgId) prior to
orgRepository.delete(organization).

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/scheduler/UserDeleteScheduler.java`:
- Around line 40-56: The loop in hardDeleteUsers is held in a single transaction
so failures can rollback prior successful deletes; fix by executing each user
deletion in its own transaction: move the deletion logic currently in
hardDeleteSingleUser into a Spring-managed bean (e.g., UserDeletionService) and
annotate that method with `@Transactional`(propagation = Propagation.REQUIRES_NEW)
(or call hardDeleteSingleUser via the class's proxy/self-injection) and invoke
that per-user from UserDeleteScheduler.hardDeleteUsers so each deletion is
isolated and failure on one user does not roll back others.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java`:
- Around line 225-233: The current flow only soft-deletes the User
(user.softDeleteUser()) and defers cleaning OrgMember and AuthProviderAccount to
the scheduler; instead, immediately remove organization memberships and social
auth links before or as part of the soft-delete. Add calls to the appropriate
repositories (e.g., orgMemberRepository.deleteByUserId(user.getId()) or
deleteByEmail(user.getEmail()) and
authProviderAccountRepository.deleteByUserId(user.getId()) or
deleteByEmail(user.getEmail())) right before
refreshTokenRepository.deleteById(...) / user.softDeleteUser(), ensuring
OrgMember and AuthProviderAccount entries are removed immediately on withdrawal.

---

Nitpick comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java`:
- Around line 23-26: The bulk DELETE in OrgMemberRepository.deleteByUserId uses
`@Modifying` but doesn't set clearAutomatically, so deleted OrgMember entities can
remain in the persistence context and appear to still exist within the same
transaction; update the `@Modifying` annotation on the deleteByUserId method to
include clearAutomatically = true so the persistence context is cleared after
the bulk delete and stale OrgMember cache entries are removed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/entity/User.java`:
- Around line 48-49: User 엔티티의 deletedAt 필드가 LocalDate로 선언되어 있어 시/분/초 정보가 손실됩니다;
User 클래스의 private LocalDate deletedAt를 LocalDateTime으로 변경하고, 관련 getter/setter와
매핑 어노테이션(`@Column` name="deleted_at")은 그대로 유지하되 타입을 LocalDateTime으로 업데이트하세요; 또한
softDeleteUser() 메서드(혹은 관련 서비스/레포지토리에서 삭제 시각을 설정하는 메서드)를 찾아 현재 LocalDate로 할당하는
부분을 LocalDateTime.now() 또는 적절한 시점의 LocalDateTime 값으로 변경해 일관된 타입을 사용하도록 수정하세요.
🪄 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: 417ef569-f9fa-4670-ba2b-6b50346bd089

📥 Commits

Reviewing files that changed from the base of the PR and between 011b115 and cdafc59.

📒 Files selected for processing (11)
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgInvitationRepository.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgRepository.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/scheduler/UserDeleteScheduler.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/entity/User.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/repository/AuthProviderAccountRepository.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/repository/UserRepository.java

Comment on lines +225 to +233
// 2) 즉시 정리 단계
// 해당 회원 이메일로 발송된 pending OrgInvitation 정리
orgInvitationRepository.deleteByEmail(user.getEmail());
// JWT RefreshToken 삭제
refreshTokenRepository.deleteById(user.getEmail());

// 3) 회원 삭제 -> Soft Delete 상태로 변경, 30일 지날 시 스케줄러에서 Hard Delete
user.softDeleteUser();
}
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 | 🏗️ Heavy lift

회원 탈퇴 직후에도 조직 멤버십/소셜 연동 계정이 남아 요구사항과 어긋납니다.

Line 225~233은 사용자만 Soft Delete 처리하고, OrgMember/AuthProviderAccount 정리는 스케줄러(30일 후)로 미뤄집니다. 현재 PR 목표(탈퇴 시 멤버 관계 정리) 기준이면 탈퇴 즉시 정리되어야 합니다.

제안 diff
         // JWT RefreshToken 삭제
         refreshTokenRepository.deleteById(user.getEmail());
 
+        // 탈퇴 즉시 관계 정리
+        orgMemberRepository.deleteByUserId(userId);
+        authProviderAccountRepository.deleteByUserId(userId);
+
         // 3) 회원 삭제 -> Soft Delete 상태로 변경, 30일 지날 시 스케줄러에서 Hard Delete
         user.softDeleteUser();
🤖 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/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java`
around lines 225 - 233, The current flow only soft-deletes the User
(user.softDeleteUser()) and defers cleaning OrgMember and AuthProviderAccount to
the scheduler; instead, immediately remove organization memberships and social
auth links before or as part of the soft-delete. Add calls to the appropriate
repositories (e.g., orgMemberRepository.deleteByUserId(user.getId()) or
deleteByEmail(user.getEmail()) and
authProviderAccountRepository.deleteByUserId(user.getId()) or
deleteByEmail(user.getEmail())) right before
refreshTokenRepository.deleteById(...) / user.softDeleteUser(), ensuring
OrgMember and AuthProviderAccount entries are removed immediately on withdrawal.

@ojy0903
Copy link
Copy Markdown
Collaborator Author

ojy0903 commented May 20, 2026

@kingmingyu @jinnieusLab 어느정도 수정 진행해봤습니다... 이전 커밋내역이 섞여서 좀 복잡할 수 있을거 같아서 깃허브로 확인하기보다 feat/#127 브랜치로 체크아웃해서 직접 IDE 로 보시는걸 추천합니다ㅠㅠ

  • 우선 회원탈퇴 요청이 들어오면, 해당 User 의 status 를 업데이트하여 Soft Delete 처리하되, 해당 User 와 연관된 "owner 이면서 다른 회원이 존재하는 Organization" 이나 PlatformConnection 이 존재할경우 오류 발생하도록 했으며, Organization owner 위임은 기존 PATCH /api/org/{orgId}/changeOwner 경로로 진행하도록 하는 걸로 진행했습니다.
  • PlatformConnection, PlatformAccount, 그리고 이와 연관된 AdCampaign, AdGroup, AdContent, MetricFact, ClickLog 제거는 별도 API 추가가 필요합니다. 이 부분을 따로 작업분배에서 맡아야 할 것 같습니다.
  • 기존 OrgServiceImpl 에서 조직 Soft / Hard Delete 시 해당 조직에 PlatformConnection 존재 시 요청 불가하도록 제약을 추가했고, Hard Delete 쪽에 조직과 연관된 Timeline, AiInsightReport, OrgInvitation 을 제거하도록 추가했습니다.
  • 스케줄러를 추가하여 매주 토요일에서 일요일로 넘어가는 새벽 2시에 User 에 대한 Hard Delete 와 각 User 에 연관된 "본인이 owner 이지만 혼자 쓰는 Organization" 에 대한 삭제, 다른 OrgMember 삭제를 추가했습니다. 이 스케줄러를 통해 제거되는 User 는 모두 기존 회원 탈퇴 요청을 처리하면서 PlatformConnection 과 연관된 모든 광고 관련 엔티티를 이미 제거하도록 제약이 걸려있기 때문에, 내부에서 Timeline, AIInsightReport, OrgInvitation 만 제거하도록 추가했습니다.

저는 "PlatformConnection, PlatformAccount, 와 광고 관련 (AdCampaign, AdGroup, AdContent, MetricFact, ClickLog)" 를 하나로 묶어서 보고, "Organization 과 OrgInvitation, AIInsightReport, Timeline" 을 하나로 묶어서 보는걸로 생각했습니다.
그래서 Organization 과 OrgInvitation, AIInsightReport, Timeline 을 기존 조직 삭제 메서드와 회원 탈퇴시에 처리해주고, 위에 광고관련과 플랫폼 관련을 PlatformController 에서 일괄삭제 해주는 API 를 추후에 추가하는걸로 생각했는데 어떨까요....? 이런식으로 가면 남는 작업은 플랫폼 관련과 광고관련을 일괄삭제하는 API 추가랑 회원 가입 요청에서 만약 Soft Delete 상태였던 회원 요청이라면 다시 복구하는 로직 설계 정도가 될 것 같습니다...

하다보니 많이 복잡해졌네요...ㅠㅠ 시간 걸려도 괜찮으니 천천히 봐주셔도 됩니다ㅠㅠㅠㅠㅠ

+++
추가 작업 중에서 광고관련과 플랫폼 관련을 PlatformController 에서 일괄삭제 해주는 API 는 제가 별도 브랜치에서 작업중입니다.

Copy link
Copy Markdown

@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

🤖 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/whereyouad/WhereYouAd/domains/user/domain/service/scheduler/UserDeleteExecutor.java`:
- Around line 32-47: The hardDeleteSingleUser method in UserDeleteExecutor is
missing cleanup of pending OrgInvitation entries sent to the user's email; add a
call to orgInvitationRepository.deleteByEmail(user.getEmail()) (or equivalent)
before deleting the user and/or after removing org memberships, and if the
repository method doesn't exist implement deleteByEmail(String email) in
OrgInvitationRepository to remove invitations by email; ensure you reference the
orgInvitationRepository field in UserDeleteExecutor and handle null/empty email
defensively (skip call if email is null/blank) and keep the S3 image deletion
behavior unchanged.
🪄 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: 50a92fcb-e298-4d9b-833e-436157c3a66b

📥 Commits

Reviewing files that changed from the base of the PR and between cdafc59 and 2f004ae.

📒 Files selected for processing (3)
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/scheduler/UserDeleteExecutor.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/scheduler/UserDeleteScheduler.java

Copy link
Copy Markdown

@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

🤖 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/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformServiceImpl.java`:
- Around line 147-151: The disconnectPlatform method in PlatformServiceImpl
currently no-ops and falsely signals success; change it to fail-fast until real
deletion logic is implemented by throwing a clear runtime exception (e.g.,
UnsupportedOperationException or a custom NotImplemented/OperationNotSupported
exception) from disconnectPlatform(Long userId, Long orgId, Long accountId) so
callers receive an explicit failure instead of a silent success; include a
message referencing the missing removal of PlatformConnection/PlatformAccount
and related ad-domain entities to aid future implementation and debugging.
🪄 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: 7abcfd9b-9e02-4969-b28e-8566487fd2d1

📥 Commits

Reviewing files that changed from the base of the PR and between 2f004ae and 0c92f8c.

📒 Files selected for processing (4)
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformService.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformServiceImpl.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/presentation/PlatformController.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java
💤 Files with no reviewable changes (1)
  • src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java

Copy link
Copy Markdown
Collaborator

@jinnieusLab jinnieusLab left a comment

Choose a reason for hiding this comment

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

P4: 고생하셨습니다! 최근에 계속 일이 많았어서 이제 코드 리뷰 남기네요 ㅠㅠ..
유저와 조직 쪽 Soft/Delete 로직이 알맞은 여러 클래스에 잘 맞게 분리해서 코드 작성해주신 것 같아 좋습니다!

말씀해주신대로 작업 분배 해보자면

  • 광고 관련 MetricFact, ClickLog Hard Delete (배치나 chunk)
  • 광고 관련 (Project ~ AdContent) Hard Delete
  • PlatformConnection, PlatformAccount Hard Delete

이렇게 나눠서 맡아보면 괜찮을 것 같습니다!

@ojy0903
Copy link
Copy Markdown
Collaborator Author

ojy0903 commented May 24, 2026

P4: 고생하셨습니다! 최근에 계속 일이 많았어서 이제 코드 리뷰 남기네요 ㅠㅠ.. 유저와 조직 쪽 Soft/Delete 로직이 알맞은 여러 클래스에 잘 맞게 분리해서 코드 작성해주신 것 같아 좋습니다!

말씀해주신대로 작업 분배 해보자면

  • 광고 관련 MetricFact, ClickLog Hard Delete (배치나 chunk)
  • 광고 관련 (Project ~ AdContent) Hard Delete
  • PlatformConnection, PlatformAccount Hard Delete

이렇게 나눠서 맡아보면 괜찮을 것 같습니다!

괜찮습니다! 작업한 내용도 좀 복잡해서 제가 보기에도 복잡했어서 오래 걸릴 수 있었을 것 같아요....
다만 작업 분배에서, 해당 작업들을 각각 나누기 보다는 PlatformConnection & PlatformAccount Hard Delete 진행 시에 내부적으로 해당 객체에 연관된 광고 관련 (Project ~ AdContent, MetricFact, ClickLog) 까지 한번에 제거하는 하나의 작업으로 보는게 좋을 것 같습니다! 실제 사용자는 저희 처럼 Mock Data 가 아니여서 모든 광고 관련 객체가 특정 PlatformConnection & PlatformAccount 에 연관되어 있을 거 같아서... 이게 더 맞는거 같아요. 그리고 이 작업을 feat/#139 에 이미 진행중이여서, 분배할 작업이 있다면

  • Soft Delete 처리된 회원이 30일 이전(Hard Delete 되기 전)에 같은 이메일로 회원가입 시도할 경우 기존 User 를 복구할 것인지 혹은 아예 가입을 막을 지 논의 & 복구할거라면 복구 로직 추가
  • 기존 로그인에서 Soft Delete 된 계정으로 로그인 시도할 경우 예외 처리 로직 추가

이정도 일 것 같습니다!

Copy link
Copy Markdown
Collaborator

@kingmingyu kingmingyu left a comment

Choose a reason for hiding this comment

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

P4: 고생하셨습니다!! 저도 리뷰가 좀 늦어져서 죄송합니다.. 😢 회원 탈퇴가 생각보다 복잡한 것 같은데 이것저것 많이 고려해서 잘 구현해주신 것 같습니다! 플랫폼 연동 해제 쪽만 잘 구현되면 괜찮을 것 같습니다!
궁금한 점이 있는데 현재 스케줄러가 싱글 스레드로 알고 있는데 모든 작업을 새벽 2시에만 하면 유저나 조직등에 만약 문제가 생기거나 작업이 많아지면 광고 연동 작업이 지연될 것 같습니다. 혹은 반대로 광고 연동이 느려져도 마찬가지로 회원 삭제가 지연될 것 같아서 혹시 조직이나 회원, 초대장에 관련된 스케줄러 작업은 새벽 3시 쯤으로 바꾸는 건 어떨까요??
그리고 유저 삭제 스케줄러를 일주일 중 하루로 정하신 이유가 궁금합니다!

@ojy0903
Copy link
Copy Markdown
Collaborator Author

ojy0903 commented May 25, 2026

P4: 고생하셨습니다!! 저도 리뷰가 좀 늦어져서 죄송합니다.. 😢 회원 탈퇴가 생각보다 복잡한 것 같은데 이것저것 많이 고려해서 잘 구현해주신 것 같습니다! 플랫폼 연동 해제 쪽만 잘 구현되면 괜찮을 것 같습니다! 궁금한 점이 있는데 현재 스케줄러가 싱글 스레드로 알고 있는데 모든 작업을 새벽 2시에만 하면 유저나 조직등에 만약 문제가 생기거나 작업이 많아지면 광고 연동 작업이 지연될 것 같습니다. 혹은 반대로 광고 연동이 느려져도 마찬가지로 회원 삭제가 지연될 것 같아서 혹시 조직이나 회원, 초대장에 관련된 스케줄러 작업은 새벽 3시 쯤으로 바꾸는 건 어떨까요?? 그리고 유저 삭제 스케줄러를 일주일 중 하루로 정하신 이유가 궁금합니다!

코드 구조가 복잡해서 시간 충분히 걸릴 수 있었을 거 같습니다. 괜찮습니다!!

  • 스케줄러가 싱글 스레드로 동작하는건 생각을 못했었네요... 이부분은 일단 회원 탈퇴 스케줄러를 새벽 3시에 동작하도록 수정하겠습니다!
  • 유저 삭제 스케줄러를 일주일 중 하루로 정했던건 특별한 이유가 있다기 보다는 회원 Soft Delete 된 시점부터 30일이 지나서도 그대로 삭제된 상태일때 Hard Delete 를 하는 로직이다보니 매일 스케줄러로 확인하는건 비효율적이라고 생각해서 가장 서비스 사용이 적을 것 같은 주말 새벽 시간에 주중 1번으로 하는 것이 부하를 줄일 수 있을 것 같다고 생각해서였습니다. 빈도를 바꾸는게 좋을까요...? 추천하는 수정안이 있다면 말해주시면 반영해보겠습니다!

@kingmingyu
Copy link
Copy Markdown
Collaborator

코드 구조가 복잡해서 시간 충분히 걸릴 수 있었을 거 같습니다. 괜찮습니다!!

  • 스케줄러가 싱글 스레드로 동작하는건 생각을 못했었네요... 이부분은 일단 회원 탈퇴 스케줄러를 새벽 3시에 동작하도록 수정하겠습니다!
  • 유저 삭제 스케줄러를 일주일 중 하루로 정했던건 특별한 이유가 있다기 보다는 회원 Soft Delete 된 시점부터 30일이 지나서도 그대로 삭제된 상태일때 Hard Delete 를 하는 로직이다보니 매일 스케줄러로 확인하는건 비효율적이라고 생각해서 가장 서비스 사용이 적을 것 같은 주말 새벽 시간에 주중 1번으로 하는 것이 부하를 줄일 수 있을 것 같다고 생각해서였습니다. 빈도를 바꾸는게 좋을까요...? 추천하는 수정안이 있다면 말해주시면 반영해보겠습니다!

30일 이후 회원은 삭제 처리 의도라면 매일 새벽 3시도 괜찮은 것 같습니다! 하지만 일주일에 한 번 처리하는게 모아서 처리하는 방식이다 보니 효율성은 더 좋을 것 같습니다! 좀 더 괜찮다고 생각하시는 방향으로 해도 좋을 것 같습니다..!
제 생각은 아직 삭제할 것도 많지 않고 크게 문제될 것 같지 않아서 매일 하는 방식으로 했다가 성능 개선이 필요한 것 같으면 일주일에 한번 처리하는 방식으로 바꿔도 괜찮을 것 같습니다!

@ojy0903
Copy link
Copy Markdown
Collaborator Author

ojy0903 commented May 26, 2026

30일 이후 회원은 삭제 처리 의도라면 매일 새벽 3시도 괜찮은 것 같습니다! 하지만 일주일에 한 번 처리하는게 모아서 처리하는 방식이다 보니 효율성은 더 좋을 것 같습니다! 좀 더 괜찮다고 생각하시는 방향으로 해도 좋을 것 같습니다..! 제 생각은 아직 삭제할 것도 많지 않고 크게 문제될 것 같지 않아서 매일 하는 방식으로 했다가 성능 개선이 필요한 것 같으면 일주일에 한번 처리하는 방식으로 바꿔도 괜찮을 것 같습니다!

피드백 반영해서 매일 새벽 3시에 Hard Delete 수정으로 변경했습니다! 머지 진행하겠습니다.

@ojy0903 ojy0903 merged commit 6a59971 into develop May 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 회원 탈퇴 - User 엔티티 삭제 기능 추가

3 participants