Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -1,8 +1,10 @@
package com.devkor.ifive.nadab.domain.auth.application;

import com.devkor.ifive.nadab.domain.auth.core.entity.UserWithdrawalReason;
import com.devkor.ifive.nadab.domain.auth.core.entity.WithdrawalEvent;
import com.devkor.ifive.nadab.domain.auth.core.entity.WithdrawalEventReason;
import com.devkor.ifive.nadab.domain.auth.core.entity.WithdrawalReasonType;
import com.devkor.ifive.nadab.domain.auth.core.repository.UserWithdrawalReasonRepository;
import com.devkor.ifive.nadab.domain.auth.core.repository.WithdrawalEventReasonRepository;
import com.devkor.ifive.nadab.domain.auth.core.repository.WithdrawalEventRepository;
import com.devkor.ifive.nadab.domain.user.core.entity.User;
import com.devkor.ifive.nadab.domain.user.core.repository.UserRepository;
import com.devkor.ifive.nadab.global.core.response.ErrorCode;
Expand All @@ -26,7 +28,8 @@ public class AuthServiceV2 {

private final WithdrawalService withdrawalService;
private final UserRepository userRepository;
private final UserWithdrawalReasonRepository userWithdrawalReasonRepository;
private final WithdrawalEventRepository withdrawalEventRepository;
private final WithdrawalEventReasonRepository withdrawalEventReasonRepository;

public void withdrawUser(Long userId, List<WithdrawalReasonType> reasons, String customReason) {
List<WithdrawalReasonType> validatedReasons = validateReasons(reasons);
Expand All @@ -41,17 +44,15 @@ public void withdrawUser(Long userId, List<WithdrawalReasonType> reasons, String
OffsetDateTime effectiveWithdrawnAt = user.getDeletedAt() != null
? user.getDeletedAt()
: OffsetDateTime.now();
List<UserWithdrawalReason> entities = new ArrayList<>(validatedReasons.size());
WithdrawalEvent event = withdrawalEventRepository.save(
WithdrawalEvent.create(user, effectiveWithdrawnAt)
);
List<WithdrawalEventReason> eventReasons = new ArrayList<>(validatedReasons.size());
for (WithdrawalReasonType reason : validatedReasons) {
String detail = reason == WithdrawalReasonType.OTHER ? normalizedCustomReason : null;
entities.add(UserWithdrawalReason.create(
user,
reason,
detail,
effectiveWithdrawnAt
));
eventReasons.add(WithdrawalEventReason.create(event, reason, detail));
}
userWithdrawalReasonRepository.saveAll(entities);
withdrawalEventReasonRepository.saveAll(eventReasons);
}

private List<WithdrawalReasonType> validateReasons(List<WithdrawalReasonType> reasons) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.devkor.ifive.nadab.domain.auth.core.entity;

import com.devkor.ifive.nadab.domain.user.core.entity.User;
import com.devkor.ifive.nadab.global.shared.entity.CreatableEntity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.OffsetDateTime;

@Entity
@Table(
name = "withdrawal_events",
uniqueConstraints = {
@UniqueConstraint(
name = "uq_withdrawal_events_user_withdrawn_at",
columnNames = {"user_id", "withdrawn_at"}
)
}
)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class WithdrawalEvent extends CreatableEntity {

private static final int RETENTION_YEARS = 1;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;

@Column(name = "withdrawn_at", nullable = false)
private OffsetDateTime withdrawnAt;

@Column(name = "anonymized_at")
private OffsetDateTime anonymizedAt;

@Column(name = "expires_at", nullable = false)
private OffsetDateTime expiresAt;

public static WithdrawalEvent create(User user, OffsetDateTime withdrawnAt) {
WithdrawalEvent event = new WithdrawalEvent();
event.user = user;
event.withdrawnAt = withdrawnAt;
event.expiresAt = withdrawnAt.plusYears(RETENTION_YEARS);
return event;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.devkor.ifive.nadab.domain.auth.core.entity;

import com.devkor.ifive.nadab.global.shared.entity.CreatableEntity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(
name = "withdrawal_event_reasons",
uniqueConstraints = {
@UniqueConstraint(
name = "uq_withdrawal_event_reasons_event_reason",
columnNames = {"event_id", "reason"}
)
}
)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class WithdrawalEventReason extends CreatableEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "event_id", nullable = false)
private WithdrawalEvent event;

@Enumerated(EnumType.STRING)
@Column(name = "reason", nullable = false, length = 50)
private WithdrawalReasonType reason;

@Column(name = "custom_reason", length = 200)
private String customReason;

public static WithdrawalEventReason create(
WithdrawalEvent event,
WithdrawalReasonType reason,
String customReason
) {
WithdrawalEventReason eventReason = new WithdrawalEventReason();
eventReason.event = event;
eventReason.reason = reason;
eventReason.customReason = customReason;
return eventReason;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

import com.devkor.ifive.nadab.domain.auth.core.entity.UserWithdrawalReason;
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;

import java.time.OffsetDateTime;

public interface UserWithdrawalReasonRepository extends JpaRepository<UserWithdrawalReason, Long> {

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
delete from UserWithdrawalReason reason
where reason.withdrawnAt < :anonymizationDate
""")
int deleteReasonsWithdrawnBefore(
@Param("anonymizationDate") OffsetDateTime anonymizationDate
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.devkor.ifive.nadab.domain.auth.core.repository;

import com.devkor.ifive.nadab.domain.auth.core.entity.WithdrawalEventReason;
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;

import java.time.OffsetDateTime;

public interface WithdrawalEventReasonRepository extends JpaRepository<WithdrawalEventReason, Long> {

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(value = """
update withdrawal_event_reasons reason
set custom_reason = null
from withdrawal_events event
where event.id = reason.event_id
and event.withdrawn_at < :anonymizationDate
and reason.custom_reason is not null
""", nativeQuery = true)
int clearCustomReasonsWithdrawnBefore(
@Param("anonymizationDate") OffsetDateTime anonymizationDate
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.devkor.ifive.nadab.domain.auth.core.repository;

import com.devkor.ifive.nadab.domain.auth.core.entity.WithdrawalEvent;
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;

import java.time.OffsetDateTime;

public interface WithdrawalEventRepository extends JpaRepository<WithdrawalEvent, Long> {

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
delete from WithdrawalEvent event
where event.expiresAt <= :expirationDate
""")
int deleteExpiredEvents(@Param("expirationDate") OffsetDateTime expirationDate);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update WithdrawalEvent event
set event.user = null,
event.anonymizedAt = :anonymizedAt
where event.withdrawnAt < :anonymizationDate
and (event.user is not null or event.anonymizedAt is null)
""")
int anonymizeEventsWithdrawnBefore(
@Param("anonymizationDate") OffsetDateTime anonymizationDate,
@Param("anonymizedAt") OffsetDateTime anonymizedAt
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.devkor.ifive.nadab.domain.auth.infra.scheduler;

import com.devkor.ifive.nadab.domain.auth.core.repository.UserWithdrawalReasonRepository;
import com.devkor.ifive.nadab.domain.auth.core.repository.WithdrawalEventReasonRepository;
import com.devkor.ifive.nadab.domain.auth.core.repository.WithdrawalEventRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.OffsetDateTime;

@Slf4j
@Component
@RequiredArgsConstructor
public class WithdrawalEventCleanupScheduler {

private static final int ACCOUNT_RECOVERY_PERIOD_DAYS = 14;

private final WithdrawalEventRepository withdrawalEventRepository;
private final WithdrawalEventReasonRepository withdrawalEventReasonRepository;
private final UserWithdrawalReasonRepository userWithdrawalReasonRepository;

@Scheduled(cron = "0 10 0 * * *", zone = "Asia/Seoul")
@Transactional
public void cleanupWithdrawalEvents() {
OffsetDateTime now = OffsetDateTime.now();
OffsetDateTime anonymizationDate = now.minusDays(ACCOUNT_RECOVERY_PERIOD_DAYS);

int deletedEventCount = withdrawalEventRepository.deleteExpiredEvents(now);
int deletedLegacyReasonCount = userWithdrawalReasonRepository
.deleteReasonsWithdrawnBefore(anonymizationDate);
int clearedCustomReasonCount = withdrawalEventReasonRepository
.clearCustomReasonsWithdrawnBefore(anonymizationDate);
int anonymizedEventCount = withdrawalEventRepository
.anonymizeEventsWithdrawnBefore(anonymizationDate, now);

if (deletedEventCount == 0
&& deletedLegacyReasonCount == 0
&& clearedCustomReasonCount == 0
&& anonymizedEventCount == 0) {
log.debug("정리할 탈퇴 이벤트가 없습니다.");
return;
}

log.info(
"탈퇴 이벤트 정리 완료: 만료 이벤트 {}건 삭제, 레거시 사유 {}건 삭제, "
+ "자유 입력 {}건 제거, 이벤트 {}건 비식별화",
deletedEventCount,
deletedLegacyReasonCount,
clearedCustomReasonCount,
anonymizedEventCount
);
}
}
Loading
Loading