Skip to content

Commit

Permalink
Merge pull request #62 from Project-Catcher/feat-dj-admin-user
Browse files Browse the repository at this point in the history
어드민 유저 API
  • Loading branch information
pingu9 authored Dec 30, 2023
2 parents e4f6630 + 9216895 commit eaf0561
Show file tree
Hide file tree
Showing 24 changed files with 971 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ out/
!**/src/main/**/out/
!**/src/test/**/out/

### QueryDSL ###
/src/main/generated

### NetBeans ###
/nbproject/private/
/nbbuild/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public enum BaseResponseStatus {
FULL_PARTICIPATE_LIMIT(2006, "참여 제한 인원을 초과하였습니다."),
REJECTED_PARTICIPATE(2007, "참여가 거절된 일정입니다."),
INVALID_SCHEDULE_PARTICIPANT_TIME(2008, "참가 일정이 유효하지 않은 신청입니다."),
INVALID_DATE_INPUT(2030, "입력하신 일자를 확인해 주세요."),
THREE_MONTHS_DATE_RANGE_EXCEPTION(2031, "최대 조회 가능 기간은 3개월 입니다."),
TAG_NOT_FOUND(2032, "태그가 존재하지 않습니다."),
ALREAD_BLACKLISTED(2033, "이미 블랙리스트에 등록된 유저입니다."),

/**
* 3000 : Response 오류
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/com/catcher/core/db/UserRepository.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package com.catcher.core.db;

import com.catcher.core.domain.UserSearchFilterType;
import com.catcher.core.domain.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public interface UserRepository {
Expand All @@ -13,4 +18,17 @@ public interface UserRepository {
User save(User user);

void saveAll(List<User> userList);

Long countByDeletedAtIsNotNull();

Long count();

Map<String, Long> countNewUsersPerDay(LocalDate startDate, LocalDate endDate);

Map<String, Long> countDeletedUsersPerDay(LocalDate startDate, LocalDate endDate);

Map<String, Long> countReportedUsersPerDay(LocalDate startDate, LocalDate endDate);

Page<User> searchUsersWithFilter(UserSearchFilterType filterType, LocalDate startDate, LocalDate endDate, String query, Pageable pageable);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.catcher.core.domain;

public interface GeneralSearchFilterType {

String getMatchedField();

GeneralSearchFilterType getDefaultField();
}
16 changes: 16 additions & 0 deletions src/main/java/com/catcher/core/domain/UserSearchFilterType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.catcher.core.domain;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum UserSearchFilterType implements GeneralSearchFilterType {
ID("username"), NICKNAME("nickname"), EMAIL("email"), PHONE_NUMBER("phone"), NONE("");

private final String matchedField;

public GeneralSearchFilterType getDefaultField() {
return NONE;
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/catcher/core/domain/UserStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.catcher.core.domain;

public enum UserStatus {
NORMAL, DELETED, REPORTED, BLACKLISTED

}
30 changes: 30 additions & 0 deletions src/main/java/com/catcher/core/domain/entity/User.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package com.catcher.core.domain.entity;

import com.catcher.common.exception.BaseException;
import com.catcher.common.exception.BaseResponseStatus;
import com.catcher.core.domain.UserStatus;
import com.catcher.core.domain.entity.enums.UserGender;
import com.catcher.core.domain.entity.enums.UserProvider;
import com.catcher.core.domain.entity.enums.UserRole;
import jakarta.persistence.*;
import lombok.*;

import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Entity
@Getter
Expand Down Expand Up @@ -50,6 +55,10 @@ public class User extends BaseTimeEntity {
@Enumerated(value = EnumType.STRING)
private UserRole userRole;

@Enumerated(value = EnumType.STRING)
@Builder.Default
private UserStatus status = UserStatus.NORMAL;

private ZonedDateTime phoneAuthentication;

@Column(nullable = false)
Expand All @@ -66,4 +75,25 @@ public class User extends BaseTimeEntity {
private ZonedDateTime phoneMarketingTerm; // 핸드폰 선택약관

private ZonedDateTime deletedAt;

@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<UserStatusChangeHistory> userStatusChangeHistories = new ArrayList<>();

public boolean changeUserStatus(UserStatus userStatus) {
if (this.status == userStatus && this.status == UserStatus.BLACKLISTED) {
throw new BaseException(BaseResponseStatus.ALREAD_BLACKLISTED);
}

if (this.status == userStatus) {
return false; // 같은 상태로 변경하려고 하면 변경되지 않음
}

if (this.status == UserStatus.BLACKLISTED && userStatus == UserStatus.REPORTED) {
return false; // 블랙리스트 상태에서 신고해도 신고 상태로 변경되지 않음
}

this.status = userStatus;
return true;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.catcher.core.domain.entity;

import com.catcher.core.domain.UserStatus;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
@Table(name = "user_status_change_history")
public class UserStatusChangeHistory extends BaseTimeEntity {

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

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

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "child_id")
private UserStatusChangeHistory child;

private UserStatus action;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "changed_user_id", nullable = false)
private User changedUser;

private String reason;

private boolean affected;

public static UserStatusChangeHistory create(User user,
User changedUser,
UserStatus action,
String reason,
boolean affected) {
return UserStatusChangeHistory.builder()
.user(user)
.changedUser(changedUser)
.action(action)
.reason(reason)
.affected(affected)
.build();
}

public void setChild(final UserStatusChangeHistory history) {
this.child = history;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.catcher.core.port;

import com.catcher.core.domain.UserStatus;
import com.catcher.core.domain.entity.User;
import com.catcher.core.domain.entity.UserStatusChangeHistory;

import java.util.List;
import java.util.Optional;

public interface UserStatusChangeHistoryRepository {

UserStatusChangeHistory save(UserStatusChangeHistory userStatusChangeHistory);

Optional<UserStatusChangeHistory> findFirstByUserAndActionAndAffectedOrderByIdDesc(User user, UserStatus userStatus, boolean affected);

List<UserStatusChangeHistory> findAllByUserId(Long id);

}
101 changes: 101 additions & 0 deletions src/main/java/com/catcher/core/service/AdminUserService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.catcher.core.service;

import com.catcher.common.exception.BaseException;
import com.catcher.common.exception.BaseResponseStatus;
import com.catcher.core.db.UserRepository;
import com.catcher.core.domain.UserSearchFilterType;
import com.catcher.core.domain.UserStatus;
import com.catcher.core.domain.entity.User;
import com.catcher.core.domain.entity.UserStatusChangeHistory;
import com.catcher.core.port.UserStatusChangeHistoryRepository;
import com.catcher.resource.response.AdminUserCountPerDayResponse;
import com.catcher.resource.response.AdminUserDetailResponse;
import com.catcher.resource.response.AdminUserSearchResponse;
import com.catcher.resource.response.AdminUserTotalCountResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;

@Service
@RequiredArgsConstructor
public class AdminUserService {

private final UserRepository userRepository;

private final UserStatusChangeHistoryRepository userStatusChangeHistoryRepository;

@Transactional(readOnly = true)
public AdminUserTotalCountResponse getTotalUserCountInfo() {

Long totalUserCount = userRepository.count();
Long deletedUserCount = userRepository.countByDeletedAtIsNotNull();

return AdminUserTotalCountResponse.create(totalUserCount, deletedUserCount);
}

@Transactional(readOnly = true)
public AdminUserCountPerDayResponse getUserCountPerDay(final LocalDate startDate, final LocalDate endDate) {
validateTimeInput(startDate, endDate);

final var newUsersDateCountMap = userRepository.countNewUsersPerDay(startDate, endDate);
final var deletedUsersDateCountMap = userRepository.countDeletedUsersPerDay(startDate, endDate);
final var reportedUsersDateCountMap = userRepository.countReportedUsersPerDay(startDate, endDate);

return AdminUserCountPerDayResponse.create(newUsersDateCountMap, deletedUsersDateCountMap, reportedUsersDateCountMap);
}

@Transactional(readOnly = true)
public Page<AdminUserSearchResponse> searchUser(UserSearchFilterType filterType,
LocalDate startDate,
LocalDate endDate,
String query,
Pageable pageable) {
final var userPage = userRepository.searchUsersWithFilter(filterType, startDate, endDate, query, pageable);

return userPage.map(AdminUserSearchResponse::create);
}

private void validateTimeInput(final LocalDate startDate, final LocalDate endDate) {
if (startDate.isAfter(endDate) || startDate.isAfter(LocalDate.now()) || endDate.isBefore(LocalDate.now())) {
throw new BaseException(BaseResponseStatus.INVALID_DATE_INPUT);
}
}

@Transactional
public void changeUserStatus(final Long userId, final Long adminUserId, final UserStatus afterStatus, final String reason) {
final User user = userRepository.findById(userId).orElseThrow(() -> new BaseException(BaseResponseStatus.DATA_NOT_FOUND));
final User adminUser = userRepository.findById(adminUserId).orElseThrow(() -> new BaseException(BaseResponseStatus.DATA_NOT_FOUND));

final UserStatus beforeStatus = user.getStatus();
final boolean affected = user.changeUserStatus(afterStatus);

var parent = userStatusChangeHistoryRepository.findFirstByUserAndActionAndAffectedOrderByIdDesc(user, beforeStatus, true).orElse(null);

UserStatusChangeHistory history = UserStatusChangeHistory.create(user, adminUser, afterStatus, reason, affected);
history = userStatusChangeHistoryRepository.save(history);
if (parent != null) {
parent.setChild(history);
}
}

public void validateUserCountDateInput(final LocalDate startDate, final LocalDate endDate) {
if (startDate.isAfter(endDate)) {
throw new BaseException(BaseResponseStatus.INVALID_DATE_INPUT);
}

if (startDate.plusMonths(3).isAfter(endDate)) {
throw new BaseException(BaseResponseStatus.THREE_MONTHS_DATE_RANGE_EXCEPTION);
}
}

public AdminUserDetailResponse searchUserDetail(final Long userId) {
final var user = userRepository.findById(userId).orElseThrow(() -> new BaseException(BaseResponseStatus.DATA_NOT_FOUND));

return AdminUserDetailResponse.create(user);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.catcher.datasource.repository;

import com.catcher.core.domain.UserStatus;
import com.catcher.core.domain.entity.User;
import com.catcher.core.domain.entity.UserStatusChangeHistory;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;

public interface UserStatusChangeHistoryJpaRepository extends JpaRepository<UserStatusChangeHistory, Long> {

Optional<UserStatusChangeHistory> findFirstByUserAndActionAndAffectedOrderByIdDesc(User user, UserStatus userStatus, boolean affected);

List<UserStatusChangeHistory> findByUserId(Long userId);

}
39 changes: 39 additions & 0 deletions src/main/java/com/catcher/datasource/user/UserJpaRepository.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,50 @@
package com.catcher.datasource.user;

import com.catcher.core.domain.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public interface UserJpaRepository extends JpaRepository<User, Long> {

Optional<User> findByUsername(String username);

Long countByDeletedAtIsNotNull();

Page<User> findAll(Specification<User> specification, Pageable pageable);

@Query("SELECT new map(date(u.createdAt) as date, COUNT(u) as count) " +
"FROM User u " +
"WHERE u.createdAt BETWEEN :startDate AND :endDate " +
"GROUP BY date(u.createdAt)")
List<Map<String, Object>> countNewUsersPerDay(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);

@Query("SELECT new map(date(u.createdAt) as date, COUNT(u) as count) " +
"FROM User u " +
"WHERE u.deletedAt BETWEEN :startDate AND :endDate " +
"GROUP BY date(u.createdAt)")
List<Map<String, Object>> countDeletedUsersPerDay(@Param("startDate") ZonedDateTime startDate, @Param("endDate") ZonedDateTime endDate);

default List<Map<String, Object>> countDeletedUsersPerDay(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate) {
return countDeletedUsersPerDay(startDate.atZone(ZoneId.systemDefault()), endDate.atZone(ZoneId.systemDefault()));
}

@Query("SELECT new map(date(ush.createdAt) as date, COUNT(DISTINCT u) as count) " +
"FROM UserStatusChangeHistory ush " +
"JOIN ush.user u " +
"WHERE ush.createdAt BETWEEN :startDate AND :endDate " +
"AND ush.action = com.catcher.core.domain.UserStatus.REPORTED " +
"GROUP BY date(ush.createdAt)")
List<Map<String, Object>> countReportedUsersPerDay(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);

}
Loading

0 comments on commit eaf0561

Please sign in to comment.