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 @@ -16,6 +16,9 @@
@RequiredArgsConstructor
public class AdminNotificationWorker {

private static final String CONTENT_DELETED_TITLE = "콘텐츠 삭제 안내";
private static final String CONTENT_DELETED_BODY_FORMAT = "회원님의 콘텐츠가 운영정책 위반으로 삭제되었어요. 사유: %s";

private final NotifySystemService notifySystemService;

@Async("pushNotificationExecutor")
Expand All @@ -42,10 +45,14 @@ public void handle(AdminNotificationEvent.AdminMessageSent event) {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handle(AdminNotificationEvent.ContentDeletedByReport event) {
try {
Map<String, Object> extras = Map.of("reason", event.reason());
Map<String, Object> extras = Map.of(
"title", CONTENT_DELETED_TITLE,
"body", CONTENT_DELETED_BODY_FORMAT.formatted(event.reason()),
"reason", event.reason()
);
notifySystemService.notifyUser(
event.contentOwnerId(),
NotificationType.SYSTEM_CONTENT_DELETED,
NotificationType.SYSTEM_ADMIN_MESSAGE,
null,
extras
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ public class AdminUserQueryService {

public AdminUserListResponse listUsers(String query, int page, int size) {
String normalizedQuery = normalizeQuery(query);
Long idQuery = parseIdQuery(normalizedQuery);
int offset = (page - 1) * size;

List<User> users = userRepository.findActiveNicknameUsers(normalizedQuery, offset, size);
long totalElements = userRepository.countActiveNicknameUsers(normalizedQuery);
List<User> users = userRepository.searchActiveUsers(normalizedQuery, idQuery, offset, size);
long totalElements = userRepository.countSearchActiveUsers(normalizedQuery, idQuery);
int totalPages = totalElements == 0 ? 0 : (int) Math.ceil((double) totalElements / size);

List<AdminUserListResponse.Item> items = users.stream()
Expand All @@ -37,4 +38,16 @@ private String normalizeQuery(String query) {
}
return query.trim();
}

private Long parseIdQuery(String query) {
if (query == null) {
return null;
}
try {
long value = Long.parseLong(query);
return value > 0 ? value : null;
} catch (NumberFormatException ignored) {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,4 @@ public enum NotificationType {
// ---- System Notification Types ----
SYSTEM_PUBLISHED_ANNOUNCEMENT,
SYSTEM_ADMIN_MESSAGE,
SYSTEM_CONTENT_DELETED,
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.devkor.apu.saerok_server.domain.user.core.repository;

import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import lombok.RequiredArgsConstructor;
import org.devkor.apu.saerok_server.domain.user.core.entity.SignupStatusType;
import org.devkor.apu.saerok_server.domain.user.core.entity.User;
Expand Down Expand Up @@ -62,51 +63,64 @@ public List<Long> findActiveUserIds(int offset, int limit) {
.getResultList();
}

public List<User> findActiveNicknameUsers(String nicknameQuery, int offset, int limit) {
public List<User> searchActiveUsers(String nicknameQuery, Long idQuery, int offset, int limit) {
String jpql = """
SELECT u FROM User u
WHERE u.deletedAt IS NULL
AND u.signupStatus <> :withdrawn
AND u.nickname IS NOT NULL
AND TRIM(u.nickname) <> ''
""";
if (nicknameQuery != null) {
jpql += " AND u.nickname LIKE :nicknameQuery";
}
jpql += buildSearchPredicate(nicknameQuery, idQuery);
jpql += " ORDER BY u.nickname ASC, u.id ASC";

var query = em.createQuery(jpql, User.class)
.setParameter("withdrawn", SignupStatusType.WITHDRAWN)
.setFirstResult(offset)
.setMaxResults(limit);

if (nicknameQuery != null) {
query.setParameter("nicknameQuery", "%" + nicknameQuery + "%");
}
bindSearchParameters(query, nicknameQuery, idQuery);

return query.getResultList();
}

public long countActiveNicknameUsers(String nicknameQuery) {
public long countSearchActiveUsers(String nicknameQuery, Long idQuery) {
String jpql = """
SELECT COUNT(u) FROM User u
WHERE u.deletedAt IS NULL
AND u.signupStatus <> :withdrawn
AND u.nickname IS NOT NULL
AND TRIM(u.nickname) <> ''
""";
if (nicknameQuery != null) {
jpql += " AND u.nickname LIKE :nicknameQuery";
}
jpql += buildSearchPredicate(nicknameQuery, idQuery);

var query = em.createQuery(jpql, Long.class)
.setParameter("withdrawn", SignupStatusType.WITHDRAWN);

bindSearchParameters(query, nicknameQuery, idQuery);

return query.getSingleResult();
}

private String buildSearchPredicate(String nicknameQuery, Long idQuery) {
boolean hasNickname = nicknameQuery != null;
boolean hasId = idQuery != null;
String nicknamePresent = "(u.nickname IS NOT NULL AND TRIM(u.nickname) <> '')";
if (hasNickname && hasId) {
return " AND ((" + nicknamePresent + " AND u.nickname LIKE :nicknameQuery) OR u.id = :idQuery)";
}
if (hasNickname) {
return " AND " + nicknamePresent + " AND u.nickname LIKE :nicknameQuery";
}
if (hasId) {
return " AND u.id = :idQuery";
}
return " AND " + nicknamePresent;
}

private void bindSearchParameters(Query query, String nicknameQuery, Long idQuery) {
if (nicknameQuery != null) {
query.setParameter("nicknameQuery", "%" + nicknameQuery + "%");
}

return query.getSingleResult();
if (idQuery != null) {
query.setParameter("idQuery", idQuery);
}
}

public List<User> findByIds(List<Long> ids) {
Expand Down
3 changes: 0 additions & 3 deletions src/main/resources/config/notification-messages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,3 @@ notification-messages:
SYSTEM_ADMIN_MESSAGE:
push-title: "{title}"
push-body: "{body}"
SYSTEM_CONTENT_DELETED:
push-title: "콘텐츠 삭제 안내"
push-body: "회원님의 콘텐츠가 운영정책 위반으로 삭제되었어요. 사유: {reason}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
UPDATE notification
SET type = 'SYSTEM_ADMIN_MESSAGE'
WHERE type = 'SYSTEM_CONTENT_DELETED';
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,15 @@ void handle_contentDeletedByReport_callsNotifyUser() {

verify(notifySystemService).notifyUser(
eq(42L),
eq(NotificationType.SYSTEM_CONTENT_DELETED),
eq(NotificationType.SYSTEM_ADMIN_MESSAGE),
isNull(),
extrasCaptor.capture()
);

assertThat(extrasCaptor.getValue()).containsEntry("reason", "커뮤니티 가이드라인 위반");
assertThat(extrasCaptor.getValue())
.containsEntry("title", "콘텐츠 삭제 안내")
.containsEntry("body", "회원님의 콘텐츠가 운영정책 위반으로 삭제되었어요. 사유: 커뮤니티 가이드라인 위반")
.containsEntry("reason", "커뮤니티 가이드라인 위반");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ void findActiveUserIds_empty() {
assertThat(activeIds).isEmpty();
}

@Test @DisplayName("findActiveNicknameUsers - 활성 닉네임 사용자만 닉네임순으로 조회")
void findActiveNicknameUsers() {
@Test @DisplayName("searchActiveUsers - 활성 닉네임 사용자만 닉네임순으로 조회")
void searchActiveUsers() {
User charlie = user("charlie@example.com", "charlie");
charlie.setSignupStatus(SignupStatusType.COMPLETED);
User bravo = user("bravo@example.com", "bravo");
Expand All @@ -156,16 +156,16 @@ void findActiveNicknameUsers() {
deleted.softDelete();
em.flush(); em.clear();

List<User> users = repo.findActiveNicknameUsers(null, 0, 20);
List<User> users = repo.searchActiveUsers(null, null, 0, 20);

assertThat(users)
.extracting(User::getNickname)
.containsExactly("bravo", "charlie");
assertThat(repo.countActiveNicknameUsers(null)).isEqualTo(2);
assertThat(repo.countSearchActiveUsers(null, null)).isEqualTo(2);
}

@Test @DisplayName("findActiveNicknameUsers - 닉네임 검색과 페이징")
void findActiveNicknameUsers_queryAndPagination() {
@Test @DisplayName("searchActiveUsers - 닉네임 검색과 페이징")
void searchActiveUsers_queryAndPagination() {
User alpha = user("alpha@example.com", "alpha");
alpha.setSignupStatus(SignupStatusType.COMPLETED);
User alpine = user("alpine@example.com", "alpine");
Expand All @@ -174,20 +174,20 @@ void findActiveNicknameUsers_queryAndPagination() {
bravo.setSignupStatus(SignupStatusType.COMPLETED);
em.flush(); em.clear();

List<User> firstPage = repo.findActiveNicknameUsers("alp", 0, 1);
List<User> secondPage = repo.findActiveNicknameUsers("alp", 1, 1);
List<User> firstPage = repo.searchActiveUsers("alp", null, 0, 1);
List<User> secondPage = repo.searchActiveUsers("alp", null, 1, 1);

assertThat(firstPage)
.extracting(User::getNickname)
.containsExactly("alpha");
assertThat(secondPage)
.extracting(User::getNickname)
.containsExactly("alpine");
assertThat(repo.countActiveNicknameUsers("alp")).isEqualTo(2);
assertThat(repo.countSearchActiveUsers("alp", null)).isEqualTo(2);
}

@Test @DisplayName("findActiveNicknameUsers - 닉네임 중간 문자열 검색")
void findActiveNicknameUsers_containsQuery() {
@Test @DisplayName("searchActiveUsers - 닉네임 중간 문자열 검색")
void searchActiveUsers_containsQuery() {
User duli = user("duli@example.com", "둘리");
duli.setSignupStatus(SignupStatusType.COMPLETED);
User pigeon = user("pigeon@example.com", "비둘기");
Expand All @@ -196,12 +196,100 @@ void findActiveNicknameUsers_containsQuery() {
magpie.setSignupStatus(SignupStatusType.COMPLETED);
em.flush(); em.clear();

List<User> users = repo.findActiveNicknameUsers("둘", 0, 20);
List<User> users = repo.searchActiveUsers("둘", null, 0, 20);

assertThat(users)
.extracting(User::getNickname)
.containsExactly("둘리", "비둘기");
assertThat(repo.countActiveNicknameUsers("둘")).isEqualTo(2);
assertThat(repo.countSearchActiveUsers("둘", null)).isEqualTo(2);
}

@Test @DisplayName("searchActiveUsers - ID 단독 검색")
void searchActiveUsers_byIdOnly() {
User alpha = user("id-alpha@example.com", "id-alpha");
alpha.setSignupStatus(SignupStatusType.COMPLETED);
User bravo = user("id-bravo@example.com", "id-bravo");
bravo.setSignupStatus(SignupStatusType.COMPLETED);
em.flush(); em.clear();

List<User> users = repo.searchActiveUsers(null, alpha.getId(), 0, 20);

assertThat(users)
.extracting(User::getId)
.containsExactly(alpha.getId());
assertThat(repo.countSearchActiveUsers(null, alpha.getId())).isEqualTo(1);
}

@Test @DisplayName("searchActiveUsers - ID와 닉네임 OR 검색")
void searchActiveUsers_byIdOrNickname() {
User alpha = user("or-alpha@example.com", "or-alpha");
alpha.setSignupStatus(SignupStatusType.COMPLETED);
User bravo = user("or-bravo@example.com", "or-bravo");
bravo.setSignupStatus(SignupStatusType.COMPLETED);
User charlie = user("or-charlie@example.com", "charlie-other");
charlie.setSignupStatus(SignupStatusType.COMPLETED);
em.flush(); em.clear();

List<User> users = repo.searchActiveUsers("or-", charlie.getId(), 0, 20);

assertThat(users)
.extracting(User::getId)
.containsExactlyInAnyOrder(alpha.getId(), bravo.getId(), charlie.getId());
assertThat(repo.countSearchActiveUsers("or-", charlie.getId())).isEqualTo(3);
}

@Test @DisplayName("searchActiveUsers - 닉네임 null인 활성 사용자도 ID로 검색됨")
void searchActiveUsers_byIdAllowsNullNickname() {
User profileRequired = user("profile-required@example.com", null);
profileRequired.setSignupStatus(SignupStatusType.PROFILE_REQUIRED);
em.flush(); em.clear();

List<User> users = repo.searchActiveUsers(null, profileRequired.getId(), 0, 20);

assertThat(users).extracting(User::getId).containsExactly(profileRequired.getId());
assertThat(repo.countSearchActiveUsers(null, profileRequired.getId())).isEqualTo(1);
}

@Test @DisplayName("searchActiveUsers - 닉네임 검색에는 null 닉네임 사용자 제외")
void searchActiveUsers_nicknameQueryExcludesNullNickname() {
User withNickname = user("with-nick@example.com", "with-nick-target");
withNickname.setSignupStatus(SignupStatusType.COMPLETED);
User noNickname = user("no-nick@example.com", null);
noNickname.setSignupStatus(SignupStatusType.PROFILE_REQUIRED);
em.flush(); em.clear();

List<User> users = repo.searchActiveUsers("nick-target", null, 0, 20);

assertThat(users).extracting(User::getNickname).containsExactly("with-nick-target");
}

@Test @DisplayName("searchActiveUsers - 쿼리 없을 때 닉네임 null 사용자 제외")
void searchActiveUsers_noQueryExcludesNullNickname() {
User withNickname = user("default-nick@example.com", "default-nick");
withNickname.setSignupStatus(SignupStatusType.COMPLETED);
User noNickname = user("default-no-nick@example.com", null);
noNickname.setSignupStatus(SignupStatusType.PROFILE_REQUIRED);
em.flush(); em.clear();

List<User> users = repo.searchActiveUsers(null, null, 0, 20);

assertThat(users).extracting(User::getNickname).containsExactly("default-nick");
}

@Test @DisplayName("searchActiveUsers - 탈퇴/삭제 사용자는 ID 검색에서도 제외")
void searchActiveUsers_byIdExcludesInactive() {
User withdrawn = user("inactive-withdrawn@example.com", "withdrawn-user");
withdrawn.setSignupStatus(SignupStatusType.WITHDRAWN);
User deleted = user("inactive-deleted@example.com", "deleted-user");
deleted.setSignupStatus(SignupStatusType.COMPLETED);
em.flush();
deleted.softDelete();
em.flush(); em.clear();

assertThat(repo.searchActiveUsers(null, withdrawn.getId(), 0, 20)).isEmpty();
assertThat(repo.countSearchActiveUsers(null, withdrawn.getId())).isZero();
assertThat(repo.searchActiveUsers(null, deleted.getId(), 0, 20)).isEmpty();
assertThat(repo.countSearchActiveUsers(null, deleted.getId())).isZero();
}

@Test @DisplayName("save - 중복 닉네임은 제약조건 위반")
Expand Down
Loading