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
Expand Up @@ -164,14 +164,8 @@ public void changePassword(String password) {
public void withdraw() {
this.status = UserStatus.DELETED;
this.deletedAt = LocalDateTime.now();
/// 랜덤 6자리 숫자 생성
String randomNum = String.format("%06d", (int) (Math.random() * 1000000));
this.nickname = "D_" + randomNum;
/// 개인정보 마스킹
this.profileImageUrl = null;
this.homeShortcut = null;
this.fcmToken = null;
this.updatedAt = LocalDateTime.now();
this.fcmToken = null;
}

/// 현재 deletedAt 값을 previousDeletedAt에 저장
Expand Down Expand Up @@ -251,4 +245,16 @@ public void updateStatus(UserStatus status) {
// TODO : 활성화 시 특별한 처리가 필요하다면 여기에 추가
}
}

public void withdrawAfter30Days() {
this.nickname = "D_" + this.id;
this.profileImageUrl = null;
this.homeShortcut = null;
this.email = "deleted_" + this.id + "@deleted.user";
this.kakaoProviderId = null;
this.naverProviderId = null;
this.appleProviderId = null;
this.career = null;
this.status = UserStatus.ANONYMOUS;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
public enum UserStatus {
ACTIVE,
SUSPENDED,
DELETED
DELETED,
ANONYMOUS
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.onebyone.kindergarten.domain.user.repository;

import com.onebyone.kindergarten.domain.user.entity.User;
import com.onebyone.kindergarten.domain.user.enums.UserStatus;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
Expand All @@ -14,14 +16,12 @@
public interface UserRepository extends JpaRepository<User, Long> {
Boolean existsByEmail(String email);

Optional<User> findByEmailAndDeletedAtIsNull(String email);
Optional<User> findByEmailAndStatus(String email, UserStatus status);

@Query(
"SELECT u FROM user u LEFT JOIN FETCH u.Kindergarten WHERE u.id = :userId AND u.deletedAt IS NULL")
"SELECT u FROM user u LEFT JOIN FETCH u.Kindergarten WHERE u.id = :userId AND u.status = 'ACTIVE'")
Optional<User> findIdWithKindergarten(@Param("userId") Long userId);

Optional<User> findByEmailAndDeletedAtIsNotNull(String email);

@Query("SELECT u FROM user u LEFT JOIN FETCH u.Kindergarten WHERE u.id = :id")
Optional<User> findByIdWithKindergarten(@Param("id") Long id);

Expand Down Expand Up @@ -73,8 +73,17 @@ Page<User> findUsersWithFilters(
Pageable pageable);

/// 모든 활성 사용자 조회
@Query("SELECT u FROM user u WHERE u.deletedAt IS NULL")
@Query("SELECT u FROM user u WHERE u.status = 'ACTIVE'")
List<User> findAllActiveUsers();

Optional<User> findByIdAndDeletedAtIsNull(Long userId);
Optional<User> findByIdAndStatus(Long userId, UserStatus status);

@Query(
"SELECT u "
+ "FROM user u "
+ "WHERE u.deletedAt <= :before30Days "
+ "AND u.status = 'DELETED' ")
List<User> findAllByWithdrawAfter30Days(@Param("before30Days") LocalDateTime before30Days);

Optional<Object> findByEmail(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.onebyone.kindergarten.domain.user.entity.User;
import com.onebyone.kindergarten.domain.user.enums.UserRole;
import com.onebyone.kindergarten.domain.user.enums.UserStatus;
import com.onebyone.kindergarten.domain.user.repository.UserRepository;
import com.onebyone.kindergarten.global.exception.BusinessException;
import com.onebyone.kindergarten.global.exception.ErrorCodes;
Expand All @@ -20,7 +21,7 @@ public class CustomUserDetailService implements UserDetailsService {
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user =
userRepository
.findByEmailAndDeletedAtIsNull(email)
.findByEmailAndStatus(email, UserStatus.ACTIVE)
.orElseThrow(() -> new BusinessException(ErrorCodes.NOT_FOUND_EMAIL));

return org.springframework.security.core.userdetails.User.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,21 @@
import com.onebyone.kindergarten.domain.user.enums.EmailCertificationType;
import com.onebyone.kindergarten.domain.user.enums.NotificationSetting;
import com.onebyone.kindergarten.domain.user.enums.UserRole;
import com.onebyone.kindergarten.domain.user.enums.UserStatus;
import com.onebyone.kindergarten.domain.user.repository.EmailCertificationRepository;
import com.onebyone.kindergarten.domain.user.repository.UserRepository;
import com.onebyone.kindergarten.global.exception.BusinessException;
import com.onebyone.kindergarten.global.exception.ErrorCodes;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
Expand All @@ -34,6 +39,7 @@ public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
private final EmailCertificationRepository emailCertificationRepository;
private static Logger logger = LoggerFactory.getLogger(UserService.class);

@Transactional
public JwtUserInfoDto signUp(SignUpRequestDTO request) {
Expand All @@ -55,7 +61,7 @@ public JwtUserInfoDto signUp(SignUpRequestDTO request) {

@Transactional(readOnly = true)
public boolean isExistedEmail(String email) {
return userRepository.findByEmailAndDeletedAtIsNull(email).isPresent();
return userRepository.findByEmail(email).isPresent();
}

private String encodePassword(String password) {
Expand All @@ -65,7 +71,8 @@ private String encodePassword(String password) {
@Transactional
public JwtUserInfoDto signIn(SignInRequestDTO request) {
// 먼저 활성 사용자 확인
Optional<User> activeUser = userRepository.findByEmailAndDeletedAtIsNull(request.getEmail());
Optional<User> activeUser =
userRepository.findByEmailAndStatus(request.getEmail(), UserStatus.ACTIVE);
if (activeUser.isPresent()) {
User user = activeUser.get();
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
Expand All @@ -81,7 +88,7 @@ public JwtUserInfoDto signIn(SignInRequestDTO request) {

// 탈퇴된 사용자 확인 및 복구
Optional<User> deletedUser =
userRepository.findByEmailAndDeletedAtIsNotNull(request.getEmail());
userRepository.findByEmailAndStatus(request.getEmail(), UserStatus.DELETED);
if (deletedUser.isPresent()) {
User user = deletedUser.get();
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
Expand Down Expand Up @@ -132,7 +139,7 @@ public void withdraw(Long userId) {

public User getUserByEmail(String email) {
return userRepository
.findByEmailAndDeletedAtIsNull(email)
.findByEmailAndStatus(email, UserStatus.ACTIVE)
.orElseThrow(() -> new BusinessException(ErrorCodes.NOT_FOUND_EMAIL));
}

Expand All @@ -148,7 +155,7 @@ public void removeCareer(User user, LocalDate startDate, LocalDate endDate) {

public User getUserById(Long userId) {
return userRepository
.findByIdAndDeletedAtIsNull(userId)
.findByIdAndStatus(userId, UserStatus.ACTIVE)
.orElseThrow(() -> new BusinessException(ErrorCodes.NOT_FOUND_EMAIL));
}

Expand Down Expand Up @@ -180,13 +187,13 @@ public User signUpByKakao(KakaoUserResponse userResponse) {
}

// 활성 사용자 확인
Optional<User> activeUser = userRepository.findByEmailAndDeletedAtIsNull(email);
Optional<User> activeUser = userRepository.findByEmailAndStatus(email, UserStatus.ACTIVE);
if (activeUser.isPresent()) {
return activeUser.get();
}

// 탈퇴된 사용자 확인 및 복구
Optional<User> deletedUser = userRepository.findByEmailAndDeletedAtIsNotNull(email);
Optional<User> deletedUser = userRepository.findByEmailAndStatus(email, UserStatus.DELETED);
if (deletedUser.isPresent()) {
User user = deletedUser.get();
user.restore();
Expand Down Expand Up @@ -227,13 +234,13 @@ public User signUpByNaver(NaverUserResponse userResponse) {
String email = userResponse.getResponse().getEmail();

// 활성 사용자 확인
Optional<User> activeUser = userRepository.findByEmailAndDeletedAtIsNull(email);
Optional<User> activeUser = userRepository.findByEmailAndStatus(email, UserStatus.ACTIVE);
if (activeUser.isPresent()) {
return activeUser.get();
}

// 탈퇴된 사용자 확인 및 복구
Optional<User> deletedUser = userRepository.findByEmailAndDeletedAtIsNotNull(email);
Optional<User> deletedUser = userRepository.findByEmailAndStatus(email, UserStatus.DELETED);
if (deletedUser.isPresent()) {
User user = deletedUser.get();
user.restore();
Expand Down Expand Up @@ -304,13 +311,14 @@ public User signUpByApple(AppleUserResponse userResponse) {
}

// 활성 사용자 확인
Optional<User> activeUser = userRepository.findByEmailAndDeletedAtIsNull(systemEmail);
Optional<User> activeUser = userRepository.findByEmailAndStatus(systemEmail, UserStatus.ACTIVE);
if (activeUser.isPresent()) {
return activeUser.get();
}

// 탈퇴된 사용자 확인 및 복구
Optional<User> deletedUser = userRepository.findByEmailAndDeletedAtIsNotNull(systemEmail);
Optional<User> deletedUser =
userRepository.findByEmailAndStatus(systemEmail, UserStatus.DELETED);
if (deletedUser.isPresent()) {
User user = deletedUser.get();
user.restore();
Expand Down Expand Up @@ -532,6 +540,15 @@ public void updateUserStatus(Long userId, UpdateUserStatusRequestDTO request) {
targetUser.updateStatus(request.getStatus());
}

@Transactional
public void withdrawAfter30Days(LocalDateTime before30Days) {
List<User> users = userRepository.findAllByWithdrawAfter30Days(before30Days);

logger.debug("withdrawAfter30Days 대상 사용자 수: {}", users.size());

users.forEach(User::withdrawAfter30Days);
}

/// 경력 개월 수 계산
private int calculateCareerMonths(
User user, LocalDate startDate, LocalDate endDate, boolean isAdding) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.onebyone.kindergarten.global.batch.job.PushNotificationJob;
import com.onebyone.kindergarten.global.batch.job.TopPostsCacheRefreshJob;
import com.onebyone.kindergarten.global.batch.job.WithdrawAfter30DaysJob;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand Down Expand Up @@ -83,4 +84,28 @@ public Trigger topPostsCacheRefreshTrigger() {
public CronScheduleBuilder topPostsCacheRefreshCronScheduler() {
return CronScheduleBuilder.cronSchedule("0 0 6 * * ?");
}

@Bean
public JobDetail withdrawAfter30DaysJobDetail() {
return JobBuilder.newJob(WithdrawAfter30DaysJob.class)
.storeDurably()
.withIdentity("withdrawAfter30DaysJob")
.withDescription("매일 새벽 3시에 30일 지난 사용자 탈퇴 처리 Job Detail")
.build();
}

@Bean
public Trigger withdrawAfter30DaysTrigger() {
return TriggerBuilder.newTrigger()
.forJob(withdrawAfter30DaysJobDetail())
.withIdentity("withdrawAfter30DaysTrigger")
.withDescription("매일 새벽 3시에 30일 지난 사용자 탈퇴 처리 트리거")
.startNow()
.withSchedule(withdrawAfter30DaysCronScheduler())
.build();
}

public CronScheduleBuilder withdrawAfter30DaysCronScheduler() {
return CronScheduleBuilder.cronSchedule("0 0 3 * * ?");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.onebyone.kindergarten.global.batch.job;

import com.onebyone.kindergarten.domain.user.service.UserService;
import com.onebyone.kindergarten.global.exception.BusinessException;
import com.onebyone.kindergarten.global.exception.ErrorCodes;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.quartz.JobExecutionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class WithdrawAfter30DaysJob extends QuartzJobBean {
private final UserService userService;

private static Logger log = LoggerFactory.getLogger(WithdrawAfter30DaysJob.class);

@Override
protected void executeInternal(JobExecutionContext context) {
try {
log.info("===== 탈퇴 Job 실행 시작: {} =====", LocalDateTime.now());

LocalDateTime minus30Days = LocalDateTime.now().minusDays(30);
userService.withdrawAfter30Days(minus30Days);
} catch (Exception e) {
log.error("탈퇴 Job 실행 중 오류 발생: {}", e.getMessage(), e);
throw new BusinessException(ErrorCodes.FAILED_WITHDRAW_EXCEPTION);
}

log.info("===== 탈퇴 Job 실행 종료: {} =====", LocalDateTime.now());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public enum ErrorCodes {
REGION_NOT_MATCHED_WITH_SUB_REGION("E0055", "해당 행정구역에 연결될 시군구를 찾을 수 없습니다."),
NOT_FOUND_CERTIFICATION("E0056", "인증 내역이 존재하지 않습니다."),
CERTIFICATION_CODE_MISMATCH("E0057", "인증 코드가 일치하지 않습니다."),
FAILED_WITHDRAW_EXCEPTION("E0058", "30일 지난 사용자 익명 처리에 실패했습니다."),
INTERNAL_SERVER_ERROR("E9999", "알 수 없는 에러 발생");

private final String code;
Expand Down