Skip to content

Feat/#139 광고 플랫폼 연동 해제 API 추가#145

Open
ojy0903 wants to merge 27 commits into
developfrom
feat/#139
Open

Feat/#139 광고 플랫폼 연동 해제 API 추가#145
ojy0903 wants to merge 27 commits into
developfrom
feat/#139

Conversation

@ojy0903

@ojy0903 ojy0903 commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

📌 관련 이슈

🚀 개요

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

광고 플랫폼 연동해제와 관련 광고 엔티티 삭제 진행 API 추가

📄 작업 내용

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

  • PlaformConnection, PlatformAccount 삭제 로직 추가
  • 연관된 AdCampaign, MetricFact, ClickLog 삭제 로직 추가

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

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

연동 해제 API 호출 전 상태 (Meta 와 Naver 연동 상태, AdCampaign, AdGroup, AdContent, MetricFact 에 각각 3개 데이터 존재, ClickLog 에는 존재 X)

연동 해제 전 DB 상태 캡쳐 PlatformAccount, PlatformConnection DB 상태 image image AdCampaign, AdGroup, AdContent, MetricFact, ClickLog DB 상태 image image image image image

플랫폼 연동 조회 API 를 통해 네이버, 메타 연동됨을 확인 (네이버는 세부 광고 정보 없음)
image

연동 해제 API 호출 (id 가 3번인 Meta 플랫폼 연동 해제)
image

연동 해제 후 DB 상태 캡쳐 PlaformAccount, PlaformConnection image image

AdCampaign, AdGroup, AdContent, MetricFact, ClickLog DB 상태
image
image
image
image
image

✅ 체크리스트

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

🔍 리뷰 포인트 (Review Points)

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

  • MetricFact 와 ClickLog 와 같이 데이터가 많은 부분은 청크 단위로 삭제하는 것이 좋을 것 같아 PlatformDataCleanupExecutor 클래스에 삭제 로직을 별도로 구현했습니다.
  • AdCampaign, AdGroup, AdContent 가 CascadeType.ALL 로 묶여있어 AdCampaign 만 지워서 하위의 AdGroup, AdContent 가 자동 삭제되도록 했는데 괜찮을까요?
  • AdCampaign, AdGroup, AdContent 가 삭제되어서 하위에 AdCampaign 이 아예 없는 Project 엔티티도 삭제되도록 했는데 삭제되도록 두는게 좋을까요 아니면 남겨두는게 좋을까요...?
  • 그 외에 수정이나 리팩터링 필요한 부분 있으면 알려주세요...!!

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

Summary by CodeRabbit

  • New Features

    • 플랫폼 계정 연동 해제 API 추가
    • 연동 해제된 계정의 자동 데이터 정리 스케줄러 추가
  • Refactor

    • 회원 탈퇴 프로세스 개선으로 플랫폼 연동 의존성 제거
    • 회원 탈퇴 시 연결된 모든 플랫폼 계정 자동 정리 처리
  • Documentation

    • 플랫폼 계정 연동 해제 API 문서 추가

@ojy0903 ojy0903 self-assigned this Jun 2, 2026
@ojy0903 ojy0903 added the ✨ Feature 새로운 기능 추가 label Jun 2, 2026
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

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

워크스루

플랫폼 계정 연동 해제 기능을 완성합니다. 사용자 요청 기반 수동 해제(권한·소속 검증) 및 매일 자동 정리(스케줄러), 그리고 회원 탈퇴 시 플랫폼 연동 자동 해제까지 전체 파이프라인을 구현하여, ClickLog/MetricFact/관계 엔티티의 일관된 정리를 보장합니다.

변경사항

플랫폼 계정 연동 해제 데이터 정리

Layer / File(s) Summary
저장소 계층 - 조회 및 배치 삭제
src/main/java/com/whereyouad/WhereYouAd/domains/advertisement/persistence/repository/AdCampaignRepository.java, src/main/java/com/whereyouad/WhereYouAd/domains/click/persistence/repository/ClickLogRepository.java, src/main/java/com/whereyouad/WhereYouAd/domains/platform/persistence/repository/PlatformConnectionRepository.java, src/main/java/com/whereyouad/WhereYouAd/domains/advertisement/persistence/repository/MetricFactRepository.java
AdCampaign 조회(PlatformAccount 기준, distinct projectId, project별 잔존 수), PlatformConnection 확대(Provider 범위, accountId 역조회, distinct accountId), MetricFact/ClickLog 네이티브 배치 삭제(LIMIT 기반) 메서드가 추가되어 정리 작업의 핵심 수집·삭제 로직을 지원합니다.
정리 오케스트레이션
src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/scheduler/PlatformDataCleanupExecutor.java
projectId 수집, REQUIRES_NEW 트랜잭션으로 ClickLog/MetricFact 청크 단위 반복 삭제, PlatformAccount/PlatformConnection/AdCampaign 관계 삭제, 빈 Project 조건부 삭제를 조율합니다.
엔티티 및 도메인 서비스
src/main/java/com/whereyouad/WhereYouAd/domains/platform/persistence/entity/PlatformAccount.java, src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformService.java, src/main/java/com/whereyouad/WhereYouAd/domains/platform/persistence/repository/PlatformAccountRepository.java
PlatformAccount.softDelete()로 상태를 DISCONNECTED로 변경, 사용자 요청 기반 disconnectPlatform과 시스템 내부용 disconnectAccountBySystem 메서드 계약, DISCONNECTED 상태 계정 조회 메서드가 추가됩니다.
PlatformServiceImpl 구현
src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformServiceImpl.java
disconnectPlatform 구현(userId/orgId/accountId 기반 ADMIN 권한·계정 존재·소속·소유자 검증 후 softDelete), disconnectAccountBySystem 구현(NOT_SUPPORTED 트랜잭션으로 Executor 호출), getPlatformSyncInfos 필터링(DISCONNECTED 상태 제외)이 추가되며, PlatformDataCleanupExecutor 의존성과 Propagation/PlatformStatus import가 확대됩니다.
에러 코드
src/main/java/com/whereyouad/WhereYouAd/domains/platform/exception/code/PlatformErrorCode.java
PLATFORM_ACCOUNT_NOT_BELONG_TO_ORG(403_2), PLATFORM_NOT_ACCOUNT_OWNER(403_3) FORBIDDEN 코드 2개가 추가되어 검증 실패 시나리오에 대응합니다.
API 노출 및 문서화
src/main/java/com/whereyouad/WhereYouAd/domains/platform/presentation/PlatformController.java, src/main/java/com/whereyouad/WhereYouAd/domains/platform/presentation/docs/PlatformControllerDocs.java
@Hidden 어노테이션 제거로 disconnectPlatform 엔드포인트가 Swagger OpenAPI에 노출되고, @Operation/@ApiResponses 메타데이터와 함께 요청/응답 시그니처가 추가되며, 성공 메시지가 명확화됩니다.
스케줄러 - 자동 정리
src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/scheduler/PlatformAccountCleanupScheduler.java
매일 새벽 4시(cron: "0 0 4 * * *")에 DISCONNECTED 상태 계정 목록 조회 후 계정별 disconnectAccountBySystem 호출, 계정 단위 예외 격리 로깅, 최종 대상/성공 카운트 로깅을 수행합니다.
조직 Soft Delete - 회원 탈퇴
src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java, src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java
removeOrganizationSoftForWithdrawal(orgId) 메서드가 추가되어 회원 탈퇴 흐름에서 조직 멤버의 currentOrgId 초기화 후 조직을 softDelete 합니다.
회원 탈퇴 통합
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/exception/code/UserErrorCode.java
UserService에서 플랫폼 연동 검증 제거, 조직 삭제 호출 변경, UserDeleteScheduler에서 userId 처리 전 플랫폼 연동 자동 해제(계정 단위 예외 격리) 추가, USER_HAS_PLATFORM_CONNECTION 오류 코드 삭제로 회원 탈퇴 조건 단순화됩니다.

예상 코드 리뷰 노력

🎯 4 (Complex) | ⏱️ ~60분

이 PR은 배치 삭제 최적화, 트랜잭션 전파 제어, 계정 단위 예외 격리, 다층 검증 로직이 조합된 복합적인 변경입니다. 특히 PlatformDataCleanupExecutor의 REQUIRES_NEW 트랜잭션 격리, ClickLog 네이티브 SQL 조인 경로 검증, 회원 탈퇴 시 플랫폼 정리 선행 흐름, 스케줄러의 계정 단위 예외 처리 등을 꼼꼼히 검토해야 합니다.

관련된 PR

  • WhereYouAd/WhereYouAd-Backend#135: PlatformService/PlatformServiceImpl/PlatformController에서 추가된 플랫폼 연동 해제 스켈레톤 흐름을 바탕으로, 이 PR에서 disconnectAccountBySystem 및 PlatformDataCleanupExecutor의 실제 정리 로직을 완성하여 같은 변경 축의 연속입니다.

제안 리뷰어

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

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive UserDeleteScheduler에 플랫폼 연동 해제 로직이 추가되었는데, 이는 #139에서 명시적으로 언급되지 않은 범위의 변경입니다. PR 설명에서 '회원 탈퇴 기능 추가 과정에서 추가해야 할 것'이라고 언급되어 있으나, 핵심 요구사항 외 파생된 설계 변경인지 확인이 필요합니다. 회원 탈퇴 흐름 통합 변경사항이 의도된 범위인지, 별도 이슈로 분리해야 하는지 검토해주세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 '광고 플랫폼 연동 해제 API 추가'는 핵심 변경사항을 명확하고 간결하게 요약하며, 변경사항들의 주요 목적을 정확히 반영하고 있습니다.
Description check ✅ Passed PR 설명이 제공된 템플릿 구조를 충실히 따르고 있으며, 관련 이슈, 개요, 작업 내용, 테스트 결과 스크린샷, 체크리스트가 모두 포함되어 있습니다.
Linked Issues check ✅ Passed 코드 변경사항들이 #139의 모든 주요 요구사항을 충족하고 있습니다: PlatformConnection/PlatformAccount 삭제 로직, 관련 광고 엔티티(AdCampaign, MetricFact, ClickLog) 삭제, 그리고 청크 단위 최적화된 삭제 구현이 모두 포함되었습니다.

✏️ 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/#139

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

🧹 Nitpick comments (2)
src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformServiceImpl.java (1)

152-183: 부분 실패 시 데이터 정합성에 대한 안내/대비를 권장합니다.

NOT_SUPPORTED로 전체를 하나의 트랜잭션으로 묶지 않기 때문에, 예를 들어 ClickLog·MetricFact 청크는 지워졌는데 deleteAccountAndRelations 단계에서 예외가 나면 "지표는 사라졌지만 계정/캠페인은 남아 있는" 중간 상태가 그대로 커밋된 채로 남습니다.

다행히 청크 삭제와 검증 로직이 멱등에 가까워 동일 요청을 재시도하면 복구되는 구조이긴 합니다. 다만 운영 관점에서:

  • 실패 시 재시도가 안전하다는 점을 로그/문서에 남기고,
  • 알림(모니터링)으로 중간 실패를 잡을 수 있게 해두면

장애 상황에서 데이터가 어중간하게 남는 것을 빠르게 인지·복구할 수 있습니다.

🤖 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/domain/service/PlatformServiceImpl.java`
around lines 152 - 183, The disconnectPlatform flow (method disconnectPlatform)
can leave a partially-cleaned state if later steps like
platformDataCleanupExecutor.deleteAccountAndRelations(...) fail after
click/metric chunk deletions; add a defensive try-catch around the sequence of
chunk deletions + deleteAccountAndRelations + deleteEmptyProject loop to detect
partial failure, emit a structured error log including userId/orgId/accountId
and totals (totalClickLogDeleted, totalMetricFactDeleted), record a
monitoring/alert metric (e.g., PlatformCleanupPartialFailure) with those fields,
and either rethrow after logging or persist a retry marker so operators can
safely retry (document that retries are idempotent in logs). Ensure the
try-catch is placed in disconnectPlatform and that
platformDataCleanupExecutor.deleteAccountAndRelations(...) and the loop over
projectIds are included in the protected section so any exception triggers the
alert/logging path.
src/main/java/com/whereyouad/WhereYouAd/domains/advertisement/persistence/repository/MetricFactRepository.java (1)

233-241: ⚡ Quick win

MySQL 기준에서는 DELETE ... LIMIT 호환성 이슈가 크지 않습니다

MetricFactRepository.deleteByPlatformAccountIdInBatch의 native DELETE FROM metric_fact ... LIMIT :batchSize현재 구성(MySQL Dialect/driver + docker-compose의 mysql:8.0) 기준이면 동작 가능성이 높습니다(src/main/resources/application.ymlcom.mysql.cj.jdbc.Driver / org.hibernate.dialect.MySQLDialect). 또한 테스트 환경에서 H2/PostgreSQL 전환 설정이 보이지 않아(별도 application-test.* / H2 의존성 없음) 기본 실행 경로에서 깨질 가능성은 낮습니다.
다만 로컬/CI에서 DB_URL로 다른 DB(H2/PostgreSQL)로 바꾸면 동일 문법이 실패할 수 있으니, 그 경우에만 DB별 쿼리로 분기하는 쪽이 안전합니다.

🤖 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/advertisement/persistence/repository/MetricFactRepository.java`
around lines 233 - 241, The native query in
MetricFactRepository.deleteByPlatformAccountIdInBatch uses "DELETE ... LIMIT
:batchSize", which can fail on non-MySQL DBs; update the repository to use a
DB-agnostic batch-delete approach: implement a two-step deletion in the
service/repository layer that first selects a batch of primary keys (e.g.,
select id from metric_fact where platform_account_id = :platformAccountId order
by id limit :batchSize) using a portable select, then call deleteAllByIdInBatch
or a JPQL delete using those ids, or provide separate native queries per dialect
and choose by current Hibernate dialect; ensure you modify
MetricFactRepository.deleteByPlatformAccountIdInBatch (or add a new repository
method used by the service) so the delete works across MySQL, H2, and
PostgreSQL.
🤖 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/presentation/docs/PlatformControllerDocs.java`:
- Around line 81-83: The ApiResponse labels in PlatformControllerDocs.java are
inconsistent with the actual error domains; update the `@ApiResponse` entries in
the PlatformControllerDocs class so each responseCode and its description use
the correct domain prefix and actual error identifier (e.g., use PLATFORM_404_1
/ PLATFORM_CONNECTION_NOT_FOUND for platform-related 404s and USER_404_1 /
USER_NOT_FOUND for user-related 404s) to match the thrown errors (like
UserErrorCode.USER_NOT_FOUND and platform error enums).

---

Nitpick comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/advertisement/persistence/repository/MetricFactRepository.java`:
- Around line 233-241: The native query in
MetricFactRepository.deleteByPlatformAccountIdInBatch uses "DELETE ... LIMIT
:batchSize", which can fail on non-MySQL DBs; update the repository to use a
DB-agnostic batch-delete approach: implement a two-step deletion in the
service/repository layer that first selects a batch of primary keys (e.g.,
select id from metric_fact where platform_account_id = :platformAccountId order
by id limit :batchSize) using a portable select, then call deleteAllByIdInBatch
or a JPQL delete using those ids, or provide separate native queries per dialect
and choose by current Hibernate dialect; ensure you modify
MetricFactRepository.deleteByPlatformAccountIdInBatch (or add a new repository
method used by the service) so the delete works across MySQL, H2, and
PostgreSQL.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformServiceImpl.java`:
- Around line 152-183: The disconnectPlatform flow (method disconnectPlatform)
can leave a partially-cleaned state if later steps like
platformDataCleanupExecutor.deleteAccountAndRelations(...) fail after
click/metric chunk deletions; add a defensive try-catch around the sequence of
chunk deletions + deleteAccountAndRelations + deleteEmptyProject loop to detect
partial failure, emit a structured error log including userId/orgId/accountId
and totals (totalClickLogDeleted, totalMetricFactDeleted), record a
monitoring/alert metric (e.g., PlatformCleanupPartialFailure) with those fields,
and either rethrow after logging or persist a retry marker so operators can
safely retry (document that retries are idempotent in logs). Ensure the
try-catch is placed in disconnectPlatform and that
platformDataCleanupExecutor.deleteAccountAndRelations(...) and the loop over
projectIds are included in the protected section so any exception triggers the
alert/logging path.
🪄 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: 7f4e8a49-e8eb-495f-ae48-d1347d554cd6

📥 Commits

Reviewing files that changed from the base of the PR and between 6a59971 and 19f3009.

📒 Files selected for processing (10)
  • src/main/java/com/whereyouad/WhereYouAd/domains/advertisement/persistence/repository/AdCampaignRepository.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/advertisement/persistence/repository/MetricFactRepository.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/click/persistence/repository/ClickLogRepository.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformDataCleanupExecutor.java
  • 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/exception/code/PlatformErrorCode.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/persistence/repository/PlatformConnectionRepository.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/presentation/PlatformController.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/presentation/docs/PlatformControllerDocs.java
💤 Files with no reviewable changes (2)
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformService.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/presentation/PlatformController.java

@kingmingyu kingmingyu left a comment

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: 고생하셨습니다! metric_fact나 clicklog같은 데이터들은 커서 청크 단위로 분리해서 삭제하는 것도 좋은 것 같습니다! 근데 이렇게 트랜잭션을 분리하게 되면 어떤 데이터는 삭제되고 어떤 데이터는 삭제되지 않은 경우 데이터 정합성이 깨지는 문제가 발생할 수 있을 것 같습니다
그래서 제 생각에는 API에서 바로 처리하기보다 유저 soft delete 이후 30일이 지난 시점에 스케줄러에서 청크 단위로 정리하는 방식으로 가져가는 것도 괜찮아 보입니다. 이렇게 하면 한 번에 큰 트랜잭션을 잡지 않아도 되고 중간에 일부 청크가 실패하더라도 다음 스케줄 실행 시 재시도할 수 있을 것 같습니다. 이후 관련 데이터 정리가 모두 끝난 경우에만 유저 hard delete를 진행하면 더 안전할 것 같습니다. 추가로 만약 이렇게 한다면 회원 탈퇴시에 플랫폼 연동 해제를 요청하지 않아도 될 것 같습니다.
근데 이렇게 하면 사용자가 그냥 플랫폼 연동을 해제하고 싶은 경우에는 어떻게 처리할지 고민이 될 것 같습니다...

@ojy0903

ojy0903 commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator Author

P2: 고생하셨습니다! metric_fact나 clicklog같은 데이터들은 커서 청크 단위로 분리해서 삭제하는 것도 좋은 것 같습니다! 근데 이렇게 트랜잭션을 분리하게 되면 어떤 데이터는 삭제되고 어떤 데이터는 삭제되지 않은 경우 데이터 정합성이 깨지는 문제가 발생할 수 있을 것 같습니다 그래서 제 생각에는 API에서 바로 처리하기보다 유저 soft delete 이후 30일이 지난 시점에 스케줄러에서 청크 단위로 정리하는 방식으로 가져가는 것도 괜찮아 보입니다. 이렇게 하면 한 번에 큰 트랜잭션을 잡지 않아도 되고 중간에 일부 청크가 실패하더라도 다음 스케줄 실행 시 재시도할 수 있을 것 같습니다. 이후 관련 데이터 정리가 모두 끝난 경우에만 유저 hard delete를 진행하면 더 안전할 것 같습니다. 추가로 만약 이렇게 한다면 회원 탈퇴시에 플랫폼 연동 해제를 요청하지 않아도 될 것 같습니다. 근데 이렇게 하면 사용자가 그냥 플랫폼 연동을 해제하고 싶은 경우에는 어떻게 처리할지 고민이 될 것 같습니다...

UserDeleteScheduler 쪽에서 플랫폼 연동해제 메서드를 호출하게 하는 방식으로 하면 가능할 것 같습니다! 이 방식으로 하면 회원 탈퇴 시 연동 해제를 요청하지 않아도 되고, 지금 만들어진 플랫폼 연동해제 API 엔드포인트도 그대로 유지해서 사용자가 단순히 수동으로 플랫폼 연동을 해제만하고 싶은 경우를 커버하도록 하면 어떨까요??
다만 이 수정은 시간이 조금 걸릴거 같아서... 시험 준비하면서 천천히 해야할 듯 하긴 합니다...

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

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/com/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformDataCleanupExecutor.java (1)

107-107: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

오타 수정이 필요합니다.

"함꼐" → "함께"로 수정해주세요.

📝 수정 제안
-            //AdCampaign 삭제 시 CascadeType.ALL 로 인해 연관된 AdGroup, AdContent 도 함꼐 제거됨
+            //AdCampaign 삭제 시 CascadeType.ALL 로 인해 연관된 AdGroup, AdContent 도 함께 제거됨
🤖 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/domain/service/PlatformDataCleanupExecutor.java`
at line 107, Fix the typo in the comment inside PlatformDataCleanupExecutor:
change "함꼐" to "함께" in the comment that explains cascade deletion of AdGroup and
AdContent when deleting AdCampaign (the comment near the CascadeType.ALL note in
PlatformDataCleanupExecutor).
🧹 Nitpick comments (1)
src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformDataCleanupExecutor.java (1)

120-127: 빈 프로젝트 삭제 로직이 안전하게 설계되었습니다.

AdCampaign과 MetricFact 카운트를 모두 체크하여 완전히 비어있을 때만 삭제하는 방어적 설계가 좋네요. 특히 Project가 여러 PlatformAccount의 캠페인을 포함할 수 있는 구조에서, count 체크로 다른 계정의 데이터가 남아있는 경우를 올바르게 처리합니다 ✓

참고로, MetricFact 청크 삭제가 중간에 실패할 경우 일부 MetricFact가 남을 수 있고, 이 경우 count > 0이므로 Project는 안전하게 보존됩니다. 다만 AdCampaign은 이미 삭제되어 orphan MetricFact가 발생할 수 있는데, 이는 PR 코멘트에서 ojy0903님이 언급하신 "청크 분리로 인한 부분 삭제 리스크"에 해당합니다.

이 트레이드오프(대용량 처리 vs. 완전한 원자성)는 이미 팀에서 논의하고 선택하신 설계 결정이므로, 향후 운영 중 orphan 데이터가 발견되면 배치 정리 스크립트로 보완하시면 될 것 같습니다.

🤖 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/domain/service/PlatformDataCleanupExecutor.java`
around lines 120 - 127, The current deleteEmptyProject method may delete a
Project if adCampaignRepository.countByProject_Id(...) and
metricFactRepository.countByProject_Id(...) return 0, but partial/async
deletions could leave orphan MetricFact after AdCampaigns were removed; to fix,
re-check both counts immediately before calling
projectRepository.deleteById(projectId) (i.e., perform a final
verification/read-after-delete within deleteEmptyProject) so you only delete
when both repositories still report zero, using the same
adCampaignRepository.countByProject_Id and
metricFactRepository.countByProject_Id calls in the deleteEmptyProject method to
locate the check and delete logic.
🤖 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/UserDeleteScheduler.java`:
- Around line 40-48: The scheduler currently calls
platformService.disconnectAccountBySystem(accountId) for each account then
hardDeleteSingleUser(userId), but
PlatformDataCleanupExecutor.deleteAccountAndRelations(Long accountId) throws
PLATFORM_ACCOUNT_NOT_FOUND via
platformAccountRepository.findById(accountId).orElseThrow(...) which will break
retries if an account was already removed; update deleteAccountAndRelations (or
the disconnect wrapper called by UserDeleteScheduler) to treat
PLATFORM_ACCOUNT_NOT_FOUND as a no-op (log and return/continue) instead of
rethrowing so the operation is idempotent, preserving other error behavior and
keeping deleteClickLogChunk (REQUIRES_NEW) semantics intact.

---

Outside diff comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformDataCleanupExecutor.java`:
- Line 107: Fix the typo in the comment inside PlatformDataCleanupExecutor:
change "함꼐" to "함께" in the comment that explains cascade deletion of AdGroup and
AdContent when deleting AdCampaign (the comment near the CascadeType.ALL note in
PlatformDataCleanupExecutor).

---

Nitpick comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformDataCleanupExecutor.java`:
- Around line 120-127: The current deleteEmptyProject method may delete a
Project if adCampaignRepository.countByProject_Id(...) and
metricFactRepository.countByProject_Id(...) return 0, but partial/async
deletions could leave orphan MetricFact after AdCampaigns were removed; to fix,
re-check both counts immediately before calling
projectRepository.deleteById(projectId) (i.e., perform a final
verification/read-after-delete within deleteEmptyProject) so you only delete
when both repositories still report zero, using the same
adCampaignRepository.countByProject_Id and
metricFactRepository.countByProject_Id calls in the deleteEmptyProject method to
locate the check and delete logic.
🪄 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: 2afd04b8-3a18-49a6-997b-8e9c86bfe5cf

📥 Commits

Reviewing files that changed from the base of the PR and between ac597b0 and a8ce99a.

📒 Files selected for processing (9)
  • 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/platform/domain/service/PlatformDataCleanupExecutor.java
  • 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/persistence/repository/PlatformConnectionRepository.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/exception/code/UserErrorCode.java
💤 Files with no reviewable changes (1)
  • src/main/java/com/whereyouad/WhereYouAd/domains/user/exception/code/UserErrorCode.java
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformServiceImpl.java

@ojy0903

ojy0903 commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator Author

P2: 고생하셨습니다! metric_fact나 clicklog같은 데이터들은 커서 청크 단위로 분리해서 삭제하는 것도 좋은 것 같습니다! 근데 이렇게 트랜잭션을 분리하게 되면 어떤 데이터는 삭제되고 어떤 데이터는 삭제되지 않은 경우 데이터 정합성이 깨지는 문제가 발생할 수 있을 것 같습니다 그래서 제 생각에는 API에서 바로 처리하기보다 유저 soft delete 이후 30일이 지난 시점에 스케줄러에서 청크 단위로 정리하는 방식으로 가져가는 것도 괜찮아 보입니다. 이렇게 하면 한 번에 큰 트랜잭션을 잡지 않아도 되고 중간에 일부 청크가 실패하더라도 다음 스케줄 실행 시 재시도할 수 있을 것 같습니다. 이후 관련 데이터 정리가 모두 끝난 경우에만 유저 hard delete를 진행하면 더 안전할 것 같습니다. 추가로 만약 이렇게 한다면 회원 탈퇴시에 플랫폼 연동 해제를 요청하지 않아도 될 것 같습니다. 근데 이렇게 하면 사용자가 그냥 플랫폼 연동을 해제하고 싶은 경우에는 어떻게 처리할지 고민이 될 것 같습니다...

수정 완료했습니다. 기존에 플랫폼 연동 해제하는 API 는 '사용자가 수동으로 연동 해제 요청하는 경우' 커버를 위해 그대로 두고, 기존 회원 탈퇴 로직에서 UserDeleteScheduler 내부에서 플랫폼 계정 연동 해제도 자동으로 수행하도록 추가했습니다!

@kingmingyu

Copy link
Copy Markdown
Collaborator

확인했습니다! 근데 수동으로 API 해체 요청을 하는 경우에는 여전히 delete메서드 1회만 호출해서 트랜잭션이 부분적으로 실패하는 경우에는 데이터 정합성이 깨지는 문제가 남아있는 것 같습니다..!

  • 삭제 진행 상태를 나타내는 필드를 추가하고 수동 API는 상태만 변경한 뒤 스케줄러가 완료될 때까지 재시도하도록 분리
  • 트랜잭션을 아예 크게 1개로 묶는 방법도 있을 것 같습니다..!

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

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/com/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformServiceImpl.java (1)

219-225: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

프로젝트 정리 전에 계정 행을 지우면 재시도 커서가 사라집니다.

Line 220에서 계정을 hard delete한 뒤 Line 224-225의 deleteEmptyProject()가 실패하면, 다음 스케줄은 DISCONNECTED 계정을 다시 찾지 못해서 이 projectIds를 재처리할 방법이 없어집니다. 그 결과 빈 Project가 영구히 남을 수 있습니다. Project 정리가 끝난 뒤 마지막에 계정을 삭제하거나, 별도의 cleanup 상태를 남겨 재시도 대상을 유지하는 쪽이 안전합니다.

🔧 수정 방향 예시
-        // AdCampaign + PlatformConnection + PlatformAccount 삭제 진행
-        platformDataCleanupExecutor.deleteAccountAndRelations(accountId);
-
-        // 비어있는 Project 엔티티 삭제
-        for (Long projectId : projectIds) {
-            platformDataCleanupExecutor.deleteEmptyProject(projectId);
-        }
+        // 1) AdCampaign / PlatformConnection 등 관계 정리
+        platformDataCleanupExecutor.deleteAccountRelations(accountId);
+
+        // 2) 비어있는 Project 정리
+        for (Long projectId : projectIds) {
+            platformDataCleanupExecutor.deleteEmptyProject(projectId);
+        }
+
+        // 3) 마지막에 PlatformAccount hard delete
+        platformDataCleanupExecutor.deletePlatformAccount(accountId);

또는 cleanup_status/아웃박스 같은 별도 재시도 커서를 두는 방법도 가능합니다.

</details>
As per coding guidelines, `SOLID 원칙, 의존성 주입(DI), 예외 처리(GlobalExceptionHandler)가 적절한지 보라.`

<details>
<summary>🤖 Prompt for AI Agents</summary>

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/domain/service/PlatformServiceImpl.java
around lines 219 - 225, The current flow deletes the account first via
platformDataCleanupExecutor.deleteAccountAndRelations(accountId) which loses the
retry cursor if subsequent deleteEmptyProject(projectId) calls fail; change the
sequence to first run all deleteEmptyProject(projectId) for each projectId and
only after all projects succeed call
platformDataCleanupExecutor.deleteAccountAndRelations(accountId), or
alternatively set/emit a persistent cleanup marker (e.g., set cleanup_status on
the account or write an outbox entry) before deleting the account so failures in
deleteEmptyProject can be retried; update PlatformServiceImpl to use the chosen
approach and ensure exceptions from deleteEmptyProject are handled/propagated so
the final account deletion is skipped on failure.


</details>

<!-- cr-comment:v1:215d01d40908c9cbcc064a8c -->

_Source: Coding guidelines_

</blockquote></details>

</blockquote></details>
🤖 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.

Outside diff comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformServiceImpl.java`:
- Around line 219-225: The current flow deletes the account first via
platformDataCleanupExecutor.deleteAccountAndRelations(accountId) which loses the
retry cursor if subsequent deleteEmptyProject(projectId) calls fail; change the
sequence to first run all deleteEmptyProject(projectId) for each projectId and
only after all projects succeed call
platformDataCleanupExecutor.deleteAccountAndRelations(accountId), or
alternatively set/emit a persistent cleanup marker (e.g., set cleanup_status on
the account or write an outbox entry) before deleting the account so failures in
deleteEmptyProject can be retried; update PlatformServiceImpl to use the chosen
approach and ensure exceptions from deleteEmptyProject are handled/propagated so
the final account deletion is skipped on failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f4c90683-408c-4482-9e02-789dfa759048

📥 Commits

Reviewing files that changed from the base of the PR and between 840fe5d and 6b4a047.

📒 Files selected for processing (7)
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/PlatformServiceImpl.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/scheduler/PlatformAccountCleanupScheduler.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/domain/service/scheduler/PlatformDataCleanupExecutor.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/persistence/entity/PlatformAccount.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/persistence/repository/PlatformAccountRepository.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/presentation/PlatformController.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/presentation/docs/PlatformControllerDocs.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/presentation/docs/PlatformControllerDocs.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/platform/presentation/PlatformController.java

@ojy0903

ojy0903 commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator Author

확인했습니다! 근데 수동으로 API 해체 요청을 하는 경우에는 여전히 delete메서드 1회만 호출해서 트랜잭션이 부분적으로 실패하는 경우에는 데이터 정합성이 깨지는 문제가 남아있는 것 같습니다..!

  • 삭제 진행 상태를 나타내는 필드를 추가하고 수동 API는 상태만 변경한 뒤 스케줄러가 완료될 때까지 재시도하도록 분리
  • 트랜잭션을 아예 크게 1개로 묶는 방법도 있을 것 같습니다..!

이 부분 생각을 못했었네요... 일단 PlatformAccount 내부에 PlatformStatus status 필드를 이용해서

  • 최초 요청 들어올 시 해당하는 PlatformAccount 의 stauts 를 DELETED 로 변경
  • 이후 PlatformAccountCleanupScheduler 에서 새벽 4시에 DELETED 상태인 PlatformAccount 일괄 삭제 & 관련 광고 엔티티 모두 삭제
  • 기존 PlatformAccount 목록 조회에서 status 가 DELETED 인 것은 안보이도록 제외

이렇게 진행했습니다. 스케줄러 시간 조정이나 다른 피드백 있으면 남겨 주세요!!

@kingmingyu

Copy link
Copy Markdown
Collaborator

확인했습니다! 고생하셨습니다!!

@kingmingyu kingmingyu self-requested a review June 14, 2026 10:57
@ojy0903 ojy0903 requested a review from jinnieusLab June 16, 2026 05:43
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: 광고 플랫폼 연동 해제 API - PlatformConnection, PlatformAccount 와 관련 광고 엔티티 삭제

2 participants