Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
23f19a7
:sparkles: feat: 플랫폼 연동 해제를 위한 PlatformErrorCode 오류 코드 추가
ojy0903 May 22, 2026
597823b
:sparkles: feat: 플랫폼 연동 해제를 위한 Repository 메서드 추가
ojy0903 May 22, 2026
973f2b3
:sparkles: feat: 플랫폼 연동 해제를 별도 Executor 클래스 추가
ojy0903 May 22, 2026
db323f2
:sparkles: feat: 플랫폼 연동 해제를 위한 Service 메서드 추가
ojy0903 May 22, 2026
b25cdda
:sparkles: feat: 플랫폼 연동 해제 API 추가
ojy0903 May 22, 2026
8e640ae
:sparkles: feat: PlatformConnection 삭제 시 FK 위반 보완
ojy0903 May 23, 2026
f5201e7
:sparkles: feat: PlatformConnection 삭제 시 FK 위반 보완 & DB 풀 고갈 방지를 위한 로직 분리
ojy0903 May 23, 2026
e4a3abf
Merge remote-tracking branch 'origin/develop' into feat/#139
ojy0903 May 26, 2026
19f3009
:art: style: 소스코드 주석 추가
ojy0903 Jun 1, 2026
17edad5
Merge remote-tracking branch 'origin/develop' into feat/#139
ojy0903 Jun 3, 2026
ac597b0
:sparkles: feat: 빈 Project 엔티티 삭제 시 연관된 MetricFact 가 0인지 검증하는 로직 추가
ojy0903 Jun 3, 2026
2a2ca5f
:sparkles: feat: 시스템 내부 호출용 플랫폼 계정 단위 연동 해제 로직 추가 (회원탈퇴 시 활용)
ojy0903 Jun 5, 2026
6bb9a49
:sparkles: feat: 회원 탈퇴 전용 Organization Soft Delete 메서드 추가
ojy0903 Jun 5, 2026
f20f1cd
:sparkles: feat: 회원 탈퇴 시 플랫폼 연동 여부 검증 제거 및 Hard Delete 시 자동 연동 해제 로직 추가
ojy0903 Jun 5, 2026
a8ce99a
:recycle: refactor: PlatformConnectionRepository 미사용 메서드 제거
ojy0903 Jun 5, 2026
840fe5d
:recycle: refactor: 플랫폼 연동 해제 멱등성 보강 (코드래빗)
ojy0903 Jun 11, 2026
8f2a6fb
:sparkles: feat: PlatformAccount 상태 변경 비즈니스 메서드 추가
ojy0903 Jun 12, 2026
c4a0de5
:sparkles: feat: PlatformAccount 상태 변경을 위한 Repository 조회 메서드 추가
ojy0903 Jun 12, 2026
061daef
:sparkles: feat: PlatformServiceImpl 내부 플랫폼 수동 연동 해제 Soft Delete 로 변경…
ojy0903 Jun 12, 2026
ead7796
:sparkles: feat: PlatformDataCleanupExecutor 내부 검증 로직 삭제
ojy0903 Jun 12, 2026
32bb102
:sparkles: feat: 플랫폼 연동 수동 해제 Hard Delete 처리용 스케줄러 추가
ojy0903 Jun 12, 2026
3a509ea
:recycle: refactor: 파일 디렉터리 구조 및 위치 정리
ojy0903 Jun 12, 2026
cd18ddc
:sparkles: feat: 플랫폼 수동 연동해제 API 수정
ojy0903 Jun 12, 2026
6b4a047
:recycle: refactor: 플랫폼 수동 연동 해제 스케줄러 주기 조정
ojy0903 Jun 12, 2026
64e91c0
:art: style: 플랫폼 수동 연동 해제 스케줄러 로그 문구 수정
ojy0903 Jun 12, 2026
62ab0cf
:recycle: refactor: 빈 Project 삭제 메서드 통합하여 원자성 확보 (코드래빗)
ojy0903 Jun 12, 2026
112a93d
:art: style: 회원 탈퇴 API Swagger Docs 수정
ojy0903 Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,15 @@ Long sumBudgetsByUserIdAndOrgIdAndProvider(@Param("userId") Long userId, @Param(
Optional<AdCampaign> findByExternalCampaignIdAndPlatformAccount(String externalCampaignId, PlatformAccount platformAccount);

Optional<AdCampaign> findByPlatformAccountAndExternalCampaignId(PlatformAccount platformAccount, String externalCampaignId);

// PlatformAccount 연동 해제 시 사용
List<AdCampaign> findByPlatformAccount(PlatformAccount platformAccount);

// 연동 해제 후 빈 Project 정리 대상 식별용 (project null 인 AdCampaign 은 제외)
@Query("SELECT DISTINCT c.project.id FROM AdCampaign c " +
"WHERE c.platformAccount.id = :platformAccountId AND c.project IS NOT NULL")
List<Long> findDistinctProjectIdsByPlatformAccountId(@Param("platformAccountId") Long platformAccountId);

// 특정 Project 에 남아있는 AdCampaign 수 (빈 Project 판단용)
long countByProject_Id(Long projectId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.repository.projection.RoasProjection;
import com.whereyouad.WhereYouAd.domains.project.application.dto.ProjectQueryDto;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

Expand Down Expand Up @@ -227,4 +228,17 @@ Optional<MetricFact> findByAdContentAndTimeBucketAndGrain(
);

Optional<MetricFact> findByPlatformAccount_IdAndAdContent_IdAndTimeBucket(Long platformAccountId, Long adContentId, LocalDateTime timeBucket);

// PlatformAccount 연동 해제 시 청크 단위 정리용
@Modifying
@Query(value = "DELETE FROM metric_fact " +
"WHERE platform_account_id = :platformAccountId " +
"LIMIT :batchSize",
nativeQuery = true)
int deleteByPlatformAccountIdInBatch(
@Param("platformAccountId") Long platformAccountId,
@Param("batchSize") int batchSize
);

long countByProject_Id(Long projectId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

import com.whereyouad.WhereYouAd.domains.click.persistence.entity.ClickLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ClickLogRepository extends JpaRepository<ClickLog, Long> {
}

// PlatformAccount 연동 해제 시 청크 단위 정리용
// ad_content_id → ad_group_id → ad_campaign_id → platform_account_id 경유
@Modifying
@Query(value = "DELETE FROM click_log " +
"WHERE ad_content_id IN (" +
" SELECT ac.ad_content_id FROM ad_content ac " +
" JOIN ad_group ag ON ac.ad_group_id = ag.ad_group_id " +
" JOIN ad_campaign camp ON ag.ad_campaign_id = camp.ad_campaign_id " +
" WHERE camp.platform_account_id = :platformAccountId" +
") " +
"LIMIT :batchSize",
nativeQuery = true)
int deleteByPlatformAccountIdInBatch(
@Param("platformAccountId") Long platformAccountId,
@Param("batchSize") int batchSize
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public interface OrgService {

void removeOrganizationSoft(Long userId, Long orgId);

// 회원 탈퇴 전용 Soft Delete - 플랫폼 연동 검증 생략 (연동 해제는 UserDeleteScheduler 가 Hard Delete 시점에 처리)
void removeOrganizationSoftForWithdrawal(Long orgId);

// User Hard Delete 정리용 - 특정 User 가 owner 인 Soft Deleted Organization 들을 관련 엔티티와 함께 Hard Delete
void removeOrganizationsOwnedBySoftDeletedUser(Long userId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,26 @@ public void removeOrganizationSoft(Long userId, Long orgId) {
organization.softDelete();
}

// 회원 탈퇴 흐름 전용 Soft Delete
// UserService.deleteUser 내부에서 본인 단독 소유 조직을 정리할 때 사용
// 호출 시점에 소유자 검증은 호출부(handleOrganizationsOwnedByUser)에서 이미 완료
@Override
public void removeOrganizationSoftForWithdrawal(Long orgId) {
Organization organization = orgRepository.findById(orgId)
.orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND));

// 현재 워크스페이스가 삭제되는 조직인 멤버들의 currentOrgId를 null로 초기화
List<OrgMember> orgMembers = orgMemberRepository.findOrgMemberByOrg(organization);
for (OrgMember member : orgMembers) {
if (Objects.equals(member.getUser().getCurrentOrgId(), orgId)) {
member.getUser().setCurrentOrgId(null);
}
}

// 조직 status 만 DELETED 로 변경
organization.softDelete();
}

// User Hard Delete 정리용 - 해당 User 가 owner 인 Soft Deleted Organization 들을 Hard Delete
// (Soft Delete 시 'owner + 다른 멤버 존재' 케이스는 차단되므로, 여기서는 본인 1명만 속한 조직만 존재)
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public interface PlatformService {

void disconnectPlatform(Long userId, Long orgId, Long accountId);

// 회원 탈퇴 스케줄러 등 시스템 내부 호출용 - 요청자 권한 검증 없이 계정 단위 연동 해제
void disconnectAccountBySystem(Long accountId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import com.whereyouad.WhereYouAd.domains.platform.application.dto.request.PlatformRequest;
import com.whereyouad.WhereYouAd.domains.platform.application.dto.response.PlatformResponse;
import com.whereyouad.WhereYouAd.domains.platform.application.mapper.PlatformConverter;
import com.whereyouad.WhereYouAd.domains.platform.domain.constant.PlatformStatus;
import com.whereyouad.WhereYouAd.domains.platform.domain.service.scheduler.PlatformDataCleanupExecutor;
import com.whereyouad.WhereYouAd.domains.platform.exception.PlatformHandler;
import com.whereyouad.WhereYouAd.domains.platform.exception.code.PlatformErrorCode;
import com.whereyouad.WhereYouAd.domains.platform.persistence.entity.PlatformAccount;
Expand All @@ -27,6 +29,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
Expand All @@ -42,6 +45,7 @@ public class PlatformServiceImpl implements PlatformService {
private final OrgMemberRepository orgMemberRepository;
private final PlatformAccountRepository platformAccountRepository;
private final PlatformConnectionRepository platformConnectionRepository;
private final PlatformDataCleanupExecutor platformDataCleanupExecutor;
private final NaverClient naverClient;
private final NaverAdAuthStrategy naverAdAuthStrategy;

Expand Down Expand Up @@ -102,8 +106,10 @@ public PlatformResponse.PlatformAccountListResponse getPlatformSyncInfos(Long us
throw new PlatformHandler(PlatformErrorCode.PLATFORM_FORBIDDEN);
}

// userId, orgId 기반 PlatformConnection 모두 조회
List<PlatformConnection> connections = platformConnectionRepository.findByUserIdAndOrgId(userId, orgId);
// userId, orgId 기반 PlatformConnection 조회 (삭제 대기(DISCONNECTED) 계정은 이미 해제된 것으로 보고 제외)
List<PlatformConnection> connections = platformConnectionRepository.findByUserIdAndOrgId(userId, orgId).stream()
.filter(connection -> connection.getPlatformAccount().getStatus() != PlatformStatus.DISCONNECTED)
.toList();

// DTO 로 변환 및 반환
return PlatformConverter.toPlatformAccountListResponse(connections);
Expand Down Expand Up @@ -144,10 +150,75 @@ public PlatformResponse.PlatformAccount updateNaverAdAccount(Long userId, Long o
return PlatformConverter.toPlatformAccountResponse(platformAccount);
}

// 광고 플랫폼 연동 해제 (수동 요청)
// 권한/소유자 검증 후 상태만 DISCONNECTED 로 변경하고 즉시 반환, 실제 데이터 삭제는 PlatformAccountCleanupScheduler 에서 비동기 진행
@Override
public void disconnectPlatform(Long userId, Long orgId, Long accountId) {
// TODO: 관련된 PlatformConnection, PlatformAccount, 광고 도메인 엔티티 제거 로직 개발
return;
// 검증 로직
userRepository.findById(userId)
.orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND));

OrgMember orgMember = orgMemberRepository.findByUserIdAndOrgId(userId, orgId)
.orElseThrow(() -> new PlatformHandler(PlatformErrorCode.PLATFORM_ORG_MEMBER_NOT_FOUND));

if (orgMember.getRole() != OrgRole.ADMIN) {
throw new PlatformHandler(PlatformErrorCode.PLATFORM_FORBIDDEN);
}

PlatformAccount platformAccount = platformAccountRepository.findById(accountId)
.orElseThrow(() -> new PlatformHandler(PlatformErrorCode.PLATFORM_ACCOUNT_NOT_FOUND));

if (!platformAccount.getOrganization().getId().equals(orgId)) {
throw new PlatformHandler(PlatformErrorCode.PLATFORM_ACCOUNT_NOT_BELONG_TO_ORG);
}

// 계정 소유자(연동 주인) 검증
platformConnectionRepository.findByUserIdAndPlatformAccountId(userId, accountId)
.orElseThrow(() -> new PlatformHandler(PlatformErrorCode.PLATFORM_NOT_ACCOUNT_OWNER));

// 멱등성: 이미 삭제 대기 상태면 그대로 반환
if (platformAccount.getStatus() == PlatformStatus.DISCONNECTED) {
return;
}

// 상태만 DISCONNECTED 로 변경
platformAccount.softDelete();
}

// 시스템 내부 호출용 플랫폼 연동 해제 — 권한 검증 없이 계정 단위로 실제 데이터를 정리
// 호출처: 회원 탈퇴 스케줄러(UserDeleteScheduler), 수동 연동 해제 정리 스케줄러(PlatformAccountCleanupScheduler)
@Override
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void disconnectAccountBySystem(Long accountId) {
List<Long> projectIds = platformDataCleanupExecutor.collectProjectIds(accountId);
cleanupAccount(accountId, projectIds);
}

// 계정 단위 데이터 정리 메서드화
// ClickLog / MetricFact 청크 삭제 → AdCampaign + PlatformConnection + PlatformAccount 삭제 → 빈 Project 삭제
// 대규모 엔티티 삭제를 위해 별도 처리 클래스 (PlatformDataCleanupExecutor) 에서 Chunk 단위 삭제 처리
private void cleanupAccount(Long accountId, List<Long> projectIds) {
int chunkDeleted; // 하나의 청크 당 삭제 갯수
long totalClickLogDeleted = 0L; // ClickLog 전체 삭제 갯수
long totalMetricFactDeleted = 0L; // MetricFact 전체 삭제 갯수

// ClickLog 청크 정리 (REQUIRES_NEW)
do {
chunkDeleted = platformDataCleanupExecutor.deleteClickLogChunk(accountId);
totalClickLogDeleted += chunkDeleted;
} while (chunkDeleted > 0);
log.info("ClickLog 삭제 완료 - platformAccountId={}, totalCount={}", accountId, totalClickLogDeleted);

// MetricFact 청크 정리 (REQUIRES_NEW) — Project 삭제 단계에서 FK 위반 방지
do {
chunkDeleted = platformDataCleanupExecutor.deleteMetricFactChunk(accountId);
totalMetricFactDeleted += chunkDeleted;
} while (chunkDeleted > 0);
log.info("MetricFact 삭제 완료 - platformAccountId={}, totalCount={}", accountId, totalMetricFactDeleted);

// AdCampaign + PlatformConnection + PlatformAccount + 비어있는 Project 삭제 진행
// PlatformAccount 가 가장 마지막에 삭제됨 & 삭제 실패 시 전체 롤백되어 다음 회차에 재시도
platformDataCleanupExecutor.deleteAccountAndRelations(accountId, projectIds);
}

private void validateNaverCredentials(String customerId, String encryptedApiKey, String encryptedSecretKey) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.whereyouad.WhereYouAd.domains.platform.domain.service.scheduler;

import com.whereyouad.WhereYouAd.domains.platform.domain.constant.PlatformStatus;
import com.whereyouad.WhereYouAd.domains.platform.domain.service.PlatformService;
import com.whereyouad.WhereYouAd.domains.platform.persistence.repository.PlatformAccountRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.List;

@Slf4j
@Component
@RequiredArgsConstructor
public class PlatformAccountCleanupScheduler {

private final PlatformAccountRepository platformAccountRepository;
private final PlatformService platformService;

// 수동 연동 해제(DISCONNECTED 마킹)된 PlatformAccount 의 실제 데이터 정리
// cleanupAccount 는 ClickLog/MetricFact/AdCampaign/Connection 먼저 삭제, PlatformAccount는 맨 마지막에 삭제
// TODO : 스케줄러 주기를 어떻게 할지? (현재는 새벽 4시에 실행)
@Scheduled(cron = "0 0 4 * * *", zone = "Asia/Seoul")
public void cleanupDeletedAccounts() {
List<Long> accountIds = platformAccountRepository.findIdsByStatus(PlatformStatus.DISCONNECTED);
if (accountIds.isEmpty()) {
return;
}
log.info("연동 해제(DISCONNECTED) 계정 정리 스케줄러 실행 - 대상 {}건", accountIds.size());

int successCount = 0;
for (Long accountId : accountIds) {
try {
platformService.disconnectAccountBySystem(accountId);
successCount++;
} catch (Exception e) {
// 한 계정의 실패가 나머지 계정 정리를 막지 않도록 격리 → 다음 회차에 재시도
log.error("연동 해제 계정 정리 실패 - accountId={}", accountId, e);
}
}

log.info("연동 해제(DISCONNECTED) 계정 정리 완료 - 대상 갯수: {}, 성공 갯수: {}", accountIds.size(), successCount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.whereyouad.WhereYouAd.domains.platform.domain.service.scheduler;

import com.whereyouad.WhereYouAd.domains.advertisement.persistence.entity.AdCampaign;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.repository.AdCampaignRepository;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.repository.MetricFactRepository;
import com.whereyouad.WhereYouAd.domains.click.persistence.repository.ClickLogRepository;
import com.whereyouad.WhereYouAd.domains.platform.persistence.entity.PlatformAccount;
import com.whereyouad.WhereYouAd.domains.platform.persistence.entity.PlatformConnection;
import com.whereyouad.WhereYouAd.domains.platform.persistence.repository.PlatformAccountRepository;
import com.whereyouad.WhereYouAd.domains.platform.persistence.repository.PlatformConnectionRepository;
import com.whereyouad.WhereYouAd.domains.project.persistence.repository.ProjectRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Slf4j
@Component
@RequiredArgsConstructor
public class PlatformDataCleanupExecutor {

private static final int BATCH_SIZE = 1000;

private final PlatformAccountRepository platformAccountRepository;
private final PlatformConnectionRepository platformConnectionRepository;
private final AdCampaignRepository adCampaignRepository;
private final ProjectRepository projectRepository;
private final ClickLogRepository clickLogRepository;
private final MetricFactRepository metricFactRepository;

// 삭제에 영향받는 projectId 수집 (수동 연동 해제 정리 / 회원 탈퇴 스케줄러 등 시스템 내부 호출용)
@Transactional(readOnly = true)
public List<Long> collectProjectIds(Long accountId) {
return adCampaignRepository.findDistinctProjectIdsByPlatformAccountId(accountId);
}

// 청크 단위로 ClickLog 삭제 — 메인 트랜잭션과 분리
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int deleteClickLogChunk(Long platformAccountId) {
int deleted = clickLogRepository.deleteByPlatformAccountIdInBatch(platformAccountId, BATCH_SIZE);
if (deleted > 0) {
log.debug("ClickLog 청크 삭제 - platformAccountId={}, deleted={}", platformAccountId, deleted);
}
return deleted;
}

// 청크 단위로 MetricFact 삭제
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int deleteMetricFactChunk(Long platformAccountId) {
int deleted = metricFactRepository.deleteByPlatformAccountIdInBatch(platformAccountId, BATCH_SIZE);
if (deleted > 0) {
log.debug("MetricFact 청크 삭제 - platformAccountId={}, deleted={}", platformAccountId, deleted);
}
return deleted;
}

// AdCampaign + PlatformConnection + PlatformAccount + 비어있는 Project 원자적 삭제
@Transactional
public void deleteAccountAndRelations(Long accountId, List<Long> projectIds) {
PlatformAccount platformAccount = platformAccountRepository.findById(accountId)
.orElse(null);

if (platformAccount == null) {
log.info("이미 삭제된 PlatformAccount - accountId={}, 정리 skip", accountId);
return;
}

// AdCampaign 제거
List<AdCampaign> campaigns = adCampaignRepository.findByPlatformAccount(platformAccount);
if (!campaigns.isEmpty()) {
adCampaignRepository.deleteAll(campaigns);
adCampaignRepository.flush();
// AdCampaign 삭제 시 CascadeType.ALL 로 인해 연관된 AdGroup, AdContent 도 함께 제거됨
}

// PlatformConnection 제거
List<PlatformConnection> connections = platformConnectionRepository.findAllByPlatformAccount_Id(accountId);
if (!connections.isEmpty()) {
platformConnectionRepository.deleteAll(connections);
platformConnectionRepository.flush();
}

// 빈 Project 제거
for (Long projectId : projectIds) {
if (adCampaignRepository.countByProject_Id(projectId) == 0 &&
metricFactRepository.countByProject_Id(projectId) == 0)
{
projectRepository.deleteById(projectId);
}
}

// PlatformAccount 제거
platformAccountRepository.delete(platformAccount);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public enum PlatformErrorCode implements BaseErrorCode {

// 403
PLATFORM_FORBIDDEN(HttpStatus.FORBIDDEN, "PLATFORM_403_1", "API키 등록은 ADMIN 권한이 필요합니다."),
PLATFORM_ACCOUNT_NOT_BELONG_TO_ORG(HttpStatus.FORBIDDEN, "PLATFORM_403_2", "해당 광고 계정은 요청한 조직 소속이 아닙니다."),
PLATFORM_NOT_ACCOUNT_OWNER(HttpStatus.FORBIDDEN, "PLATFORM_403_3", "본인이 등록한 광고 계정만 연동 해제할 수 있습니다."),

// 404
PLATFORM_CONNECTION_NOT_FOUND(HttpStatus.NOT_FOUND, "PLATFORM_404_1", "조직과 연결된 인증 정보를 찾을 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,9 @@ public class PlatformAccount extends BaseEntity {
@JoinColumn(name = "org_id")
private Organization organization;

// 수동 연동 해제 요청 시 상태만 DISCONNECTED 로 변경 (실제 데이터 정리는 스케줄러가 수행)
public void softDelete() {
this.status = PlatformStatus.DISCONNECTED;
}

}
Loading
Loading