Skip to content

Commit 17cd5df

Browse files
committed
feat: 어드민 유저 관리 기능
- 코드리뷰 반영
1 parent adf4c9b commit 17cd5df

15 files changed

Lines changed: 252 additions & 203 deletions

File tree

src/main/java/com/example/solidconnection/admin/controller/AdminUserBanController.java

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,27 @@
1414

1515
import jakarta.validation.Valid;
1616
import lombok.RequiredArgsConstructor;
17-
import lombok.extern.slf4j.Slf4j;
1817

1918
@RequiredArgsConstructor
2019
@RequestMapping("/admin/users")
2120
@RestController
22-
@Slf4j
2321
public class AdminUserBanController {
2422
private final AdminUserBanService adminUserBanService;
2523

26-
@PostMapping("/{userId}/ban")
24+
@PostMapping("/{user-id}/ban")
2725
public ResponseEntity<Void> banUser(
28-
@PathVariable long userId,
29-
@Valid @RequestBody UserBanRequest request
26+
@AuthorizedUser long adminId,
27+
@PathVariable(name = "user-id") long userId,
28+
@Valid @RequestBody UserBanRequest request
3029
) {
31-
adminUserBanService.banUser(userId, request);
30+
adminUserBanService.banUser(userId, adminId, request);
3231
return ResponseEntity.ok().build();
3332
}
3433

35-
@PatchMapping("/{userId}/unban")
34+
@PatchMapping("/{user-id}/unban")
3635
public ResponseEntity<Void> unbanUser(
3736
@AuthorizedUser long adminId,
38-
@PathVariable long userId
37+
@PathVariable(name = "user-id") long userId
3938
) {
4039
adminUserBanService.unbanUser(userId, adminId);
4140
return ResponseEntity.ok().build();

src/main/java/com/example/solidconnection/admin/dto/UserBanRequest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.example.solidconnection.admin.dto;
22

3-
43
import com.example.solidconnection.siteuser.domain.UserBanDuration;
54

65
import jakarta.validation.constraints.NotNull;
Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.example.solidconnection.admin.service;
22

3+
import static java.time.ZoneOffset.UTC;
4+
35
import com.example.solidconnection.admin.dto.UserBanRequest;
46
import com.example.solidconnection.chat.repository.ChatMessageRepository;
57
import com.example.solidconnection.common.exception.CustomException;
@@ -11,14 +13,10 @@
1113
import com.example.solidconnection.siteuser.domain.UserStatus;
1214
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
1315
import com.example.solidconnection.siteuser.repository.UserBanRepository;
14-
import static java.time.ZoneOffset.UTC;
15-
1616
import java.time.ZonedDateTime;
1717
import java.util.List;
18-
1918
import lombok.RequiredArgsConstructor;
2019
import lombok.extern.slf4j.Slf4j;
21-
2220
import org.springframework.scheduling.annotation.Scheduled;
2321
import org.springframework.stereotype.Service;
2422
import org.springframework.transaction.annotation.Transactional;
@@ -35,18 +33,19 @@ public class AdminUserBanService {
3533
private final ChatMessageRepository chatMessageRepository;
3634

3735
@Transactional
38-
public void banUser(long userId, UserBanRequest request) {
39-
ZonedDateTime now = ZonedDateTime.now(UTC);
40-
validateNotAlreadyBanned(userId, now);
36+
public void banUser(long userId, long adminId, UserBanRequest request) {
37+
SiteUser user = siteUserRepository.findById(userId)
38+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
39+
validateNotAlreadyBanned(userId);
4140
validateReportExists(userId);
4241

42+
user.updateUserStatus(UserStatus.BANNED);
4343
updateReportedContentIsDeleted(userId, true);
44-
createUserBan(userId, request, now);
45-
updateUserStatus(userId, UserStatus.BANNED);
44+
createUserBan(userId, adminId, request);
4645
}
4746

48-
private void validateNotAlreadyBanned(long userId, ZonedDateTime now) {
49-
if (userBanRepository.existsByBannedUserIdAndIsUnbannedFalseAndExpiredAtAfter(userId, now)) {
47+
private void validateNotAlreadyBanned(long userId) {
48+
if (userBanRepository.existsByBannedUserIdAndIsExpiredFalseAndExpiredAtAfter(userId, ZonedDateTime.now(UTC))) {
5049
throw new CustomException(ErrorCode.ALREADY_BANNED_USER);
5150
}
5251
}
@@ -62,42 +61,53 @@ private void updateReportedContentIsDeleted(long userId, boolean isDeleted) {
6261
chatMessageRepository.updateReportedChatMessagesIsDeleted(userId, isDeleted);
6362
}
6463

65-
private void createUserBan(long userId, UserBanRequest request, ZonedDateTime now) {
64+
private void createUserBan(long userId, long adminId, UserBanRequest request) {
65+
ZonedDateTime now = ZonedDateTime.now(UTC);
6666
ZonedDateTime expiredAt = now.plusDays(request.duration().getDays());
67-
UserBan userBan = new UserBan(userId, request.duration(), expiredAt);
67+
UserBan userBan = new UserBan(userId, adminId, request.duration(), expiredAt);
6868
userBanRepository.save(userBan);
6969
}
7070

71-
private void updateUserStatus(long userId, UserStatus status) {
72-
SiteUser user = siteUserRepository.findById(userId)
73-
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
74-
user.updateUserStatus(status);
75-
}
76-
7771
@Transactional
7872
public void unbanUser(long userId, long adminId) {
79-
UserBan userBan = findBannedUser(userId, ZonedDateTime.now(UTC));
73+
SiteUser user = siteUserRepository.findById(userId)
74+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
75+
UserBan userBan = findActiveBan(userId);
8076
userBan.manuallyUnban(adminId);
81-
processUnban(userId);
77+
78+
user.updateUserStatus(UserStatus.REPORTED);
79+
updateReportedContentIsDeleted(userId, false);
8280
}
8381

84-
private UserBan findBannedUser(long userId, ZonedDateTime now) {
82+
private UserBan findActiveBan(long userId) {
8583
return userBanRepository
86-
.findTopByBannedUserIdAndIsUnbannedFalseAndExpiredAtAfterOrderByCreatedAtDesc(userId, now)
84+
.findByBannedUserIdAndIsExpiredFalseAndExpiredAtAfter(userId, ZonedDateTime.now(UTC))
8785
.orElseThrow(() -> new CustomException(ErrorCode.NOT_BANNED_USER));
8886
}
8987

9088
@Transactional
9189
@Scheduled(cron = "0 0 0 * * *")
9290
public void expireUserBans() {
93-
List<UserBan> expiredBans = userBanRepository.findAllByIsUnbannedFalseAndExpiredAtBefore(ZonedDateTime.now(UTC));
94-
for (UserBan userBan : expiredBans) {
95-
processUnban(userBan.getBannedUserId());
91+
try {
92+
ZonedDateTime now = ZonedDateTime.now(UTC);
93+
List<Long> expiredUserIds = userBanRepository.findExpiredBannedUserIds(now);
94+
95+
if (expiredUserIds.isEmpty()) {
96+
return;
97+
}
98+
99+
userBanRepository.bulkExpireUserBans(now);
100+
siteUserRepository.bulkUpdateUserStatus(expiredUserIds, UserStatus.REPORTED);
101+
bulkUpdateReportedContentIsDeleted(expiredUserIds);
102+
log.info("Finished processing expired blocks:: userIds={}", expiredUserIds);
103+
} catch (Exception e) {
104+
log.error("Failed to process expired blocks", e);
96105
}
97106
}
98107

99-
private void processUnban(long userId) {
100-
updateReportedContentIsDeleted(userId, false);
101-
updateUserStatus(userId, UserStatus.REPORTED);
108+
private void bulkUpdateReportedContentIsDeleted(List<Long> expiredUserIds) {
109+
postRepository.bulkUpdateReportedPostsIsDeleted(expiredUserIds, false);
110+
chatMessageRepository.bulkUpdateReportedChatMessagesIsDeleted(expiredUserIds, false);
102111
}
112+
103113
}

src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,12 @@ WHERE cm.id IN (SELECT r.target_id FROM report r WHERE r.target_type = 'CHAT')
5757
AND cm.sender_id IN (SELECT cp.id FROM chat_participant cp WHERE cp.site_user_id = :siteUserId)
5858
""", nativeQuery = true)
5959
void updateReportedChatMessagesIsDeleted(@Param("siteUserId") long siteUserId, @Param("isDeleted") boolean isDeleted);
60+
61+
@Modifying(clearAutomatically = true, flushAutomatically = true)
62+
@Query(value = """
63+
UPDATE chat_message cm SET cm.is_deleted = :isDeleted
64+
WHERE cm.id IN (SELECT r.target_id FROM report r WHERE r.target_type = 'CHAT')
65+
AND cm.sender_id IN (SELECT cp.id FROM chat_participant cp WHERE cp.site_user_id IN :siteUserIds)
66+
""", nativeQuery = true)
67+
void bulkUpdateReportedChatMessagesIsDeleted(@Param("siteUserIds") List<Long> siteUserIds, @Param("isDeleted") boolean isDeleted);
6068
}

src/main/java/com/example/solidconnection/common/interceptor/BannedUserInterceptor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@ public boolean preHandle(
3434
}
3535
return true;
3636
}
37-
}
37+
}

src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ AND p.id IN (SELECT r.target_id FROM report r WHERE r.target_type = 'POST')
5858
""", nativeQuery = true)
5959
void updateReportedPostsIsDeleted(@Param("siteUserId") long siteUserId, @Param("isDeleted") boolean isDeleted);
6060

61+
@Modifying(clearAutomatically = true, flushAutomatically = true)
62+
@Query(value = """
63+
UPDATE post p SET p.is_deleted = :isDeleted
64+
WHERE p.site_user_id IN :siteUserIds
65+
AND p.id IN (SELECT r.target_id FROM report r WHERE r.target_type = 'POST')
66+
""", nativeQuery = true)
67+
void bulkUpdateReportedPostsIsDeleted(@Param("siteUserIds") List<Long> siteUserIds, @Param("isDeleted") boolean isDeleted);
68+
6169
default Post getByIdUsingEntityGraph(Long id) {
6270
return findPostById(id)
6371
.orElseThrow(() -> new CustomException(INVALID_POST_ID));

src/main/java/com/example/solidconnection/report/repository/ReportRepository.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import com.example.solidconnection.report.domain.Report;
44
import com.example.solidconnection.report.domain.TargetType;
55
import org.springframework.data.jpa.repository.JpaRepository;
6-
import org.springframework.data.jpa.repository.Query;
7-
import org.springframework.data.repository.query.Param;
86

97
public interface ReportRepository extends JpaRepository<Report, Long> {
108

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package com.example.solidconnection.siteuser.domain;
22

3-
import java.time.ZonedDateTime;
3+
import static java.time.ZoneOffset.UTC;
44

5+
import java.time.ZonedDateTime;
56
import com.example.solidconnection.common.BaseEntity;
6-
77
import jakarta.persistence.Column;
88
import jakarta.persistence.Entity;
99
import jakarta.persistence.EnumType;
@@ -12,14 +12,12 @@
1212
import jakarta.persistence.GenerationType;
1313
import jakarta.persistence.Id;
1414
import lombok.AccessLevel;
15-
import lombok.AllArgsConstructor;
1615
import lombok.Getter;
1716
import lombok.NoArgsConstructor;
1817

1918
@Getter
2019
@NoArgsConstructor(access = AccessLevel.PROTECTED)
2120
@Entity
22-
@AllArgsConstructor
2321
public class UserBan extends BaseEntity {
2422

2523
@Id
@@ -29,31 +27,35 @@ public class UserBan extends BaseEntity {
2927
@Column(name = "banned_user_id", nullable = false)
3028
private Long bannedUserId;
3129

30+
@Column(name = "banned_by", nullable = false)
31+
private Long bannedBy;
32+
3233
@Column(name = "duration", nullable = false)
3334
@Enumerated(EnumType.STRING)
3435
private UserBanDuration duration;
3536

3637
@Column(name = "expired_at", nullable = false)
3738
private ZonedDateTime expiredAt;
3839

39-
@Column(name = "is_unbanned", nullable = false)
40-
private boolean isUnbanned = false;
40+
@Column(name = "is_expired", nullable = false)
41+
private boolean isExpired = false;
4142

4243
@Column(name = "unbanned_by")
4344
private Long unbannedBy;
4445

4546
@Column(name = "unbanned_at")
4647
private ZonedDateTime unbannedAt;
4748

48-
public UserBan(Long bannedUserId, UserBanDuration duration, ZonedDateTime expiredAt) {
49+
public UserBan(Long bannedUserId, Long bannedBy, UserBanDuration duration, ZonedDateTime expiredAt) {
4950
this.bannedUserId = bannedUserId;
51+
this.bannedBy = bannedBy;
5052
this.duration = duration;
5153
this.expiredAt = expiredAt;
5254
}
5355

5456
public void manuallyUnban(Long adminId) {
55-
this.isUnbanned = true;
57+
this.isExpired = true;
5658
this.unbannedBy = adminId;
57-
this.unbannedAt = ZonedDateTime.now();
59+
this.unbannedAt = ZonedDateTime.now(UTC);
5860
}
5961
}

src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import com.example.solidconnection.siteuser.domain.AuthType;
44
import com.example.solidconnection.siteuser.domain.SiteUser;
5+
import com.example.solidconnection.siteuser.domain.UserStatus;
56
import java.time.LocalDate;
67
import java.util.List;
78
import java.util.Optional;
89
import org.springframework.data.jpa.repository.JpaRepository;
10+
import org.springframework.data.jpa.repository.Modifying;
911
import org.springframework.data.jpa.repository.Query;
1012
import org.springframework.data.repository.query.Param;
1113

@@ -21,4 +23,8 @@ public interface SiteUserRepository extends JpaRepository<SiteUser, Long> {
2123
List<SiteUser> findUsersToBeRemoved(@Param("cutoffDate") LocalDate cutoffDate);
2224

2325
List<SiteUser> findAllByIdIn(List<Long> ids);
26+
27+
@Modifying
28+
@Query("UPDATE SiteUser u SET u.userStatus = :status WHERE u.id IN :userIds")
29+
void bulkUpdateUserStatus(@Param("userIds") List<Long> userIds, @Param("status") UserStatus status);
2430
}
Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
package com.example.solidconnection.siteuser.repository;
22

3+
import com.example.solidconnection.siteuser.domain.UserBan;
34
import java.time.ZonedDateTime;
45
import java.util.List;
56
import java.util.Optional;
67
import org.springframework.data.jpa.repository.JpaRepository;
7-
8-
import com.example.solidconnection.siteuser.domain.UserBan;
8+
import org.springframework.data.jpa.repository.Modifying;
9+
import org.springframework.data.jpa.repository.Query;
10+
import org.springframework.data.repository.query.Param;
911

1012
public interface UserBanRepository extends JpaRepository<UserBan, Long> {
1113

12-
boolean existsByBannedUserIdAndIsUnbannedFalseAndExpiredAtAfter(long bannedUserId, ZonedDateTime current);
14+
boolean existsByBannedUserIdAndIsExpiredFalseAndExpiredAtAfter(long bannedUserId, ZonedDateTime now);
15+
16+
Optional<UserBan> findByBannedUserIdAndIsExpiredFalseAndExpiredAtAfter(long bannedUserId, ZonedDateTime now);
1317

14-
List<UserBan> findAllByIsUnbannedFalseAndExpiredAtBefore(ZonedDateTime current);
18+
@Query("SELECT ub.bannedUserId FROM UserBan ub WHERE ub.isExpired = false AND ub.expiredAt < :current")
19+
List<Long> findExpiredBannedUserIds(@Param("current") ZonedDateTime current);
1520

16-
Optional<UserBan> findTopByBannedUserIdAndIsUnbannedFalseAndExpiredAtAfterOrderByCreatedAtDesc(long bannedUserId, ZonedDateTime current);
21+
@Modifying
22+
@Query("UPDATE UserBan ub SET ub.isExpired = true WHERE ub.isExpired = false AND ub.expiredAt < :current")
23+
void bulkExpireUserBans(@Param("current") ZonedDateTime current);
1724
}

0 commit comments

Comments
 (0)