Skip to content

Commit a215898

Browse files
authored
Merge pull request #317 from DevKor-github/fix/signupStatus
refactor(): 개별 알림 발송 타입 변경, 어드민에서 사용자 검색에 id 검색 추가
2 parents b37593c + 5be6544 commit a215898

8 files changed

Lines changed: 164 additions & 40 deletions

File tree

src/main/java/org/devkor/apu/saerok_server/domain/admin/notification/application/event/AdminNotificationWorker.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
@RequiredArgsConstructor
1717
public class AdminNotificationWorker {
1818

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

2124
@Async("pushNotificationExecutor")
@@ -42,10 +45,14 @@ public void handle(AdminNotificationEvent.AdminMessageSent event) {
4245
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
4346
public void handle(AdminNotificationEvent.ContentDeletedByReport event) {
4447
try {
45-
Map<String, Object> extras = Map.of("reason", event.reason());
48+
Map<String, Object> extras = Map.of(
49+
"title", CONTENT_DELETED_TITLE,
50+
"body", CONTENT_DELETED_BODY_FORMAT.formatted(event.reason()),
51+
"reason", event.reason()
52+
);
4653
notifySystemService.notifyUser(
4754
event.contentOwnerId(),
48-
NotificationType.SYSTEM_CONTENT_DELETED,
55+
NotificationType.SYSTEM_ADMIN_MESSAGE,
4956
null,
5057
extras
5158
);

src/main/java/org/devkor/apu/saerok_server/domain/admin/user/application/AdminUserQueryService.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ public class AdminUserQueryService {
1818

1919
public AdminUserListResponse listUsers(String query, int page, int size) {
2020
String normalizedQuery = normalizeQuery(query);
21+
Long idQuery = parseIdQuery(normalizedQuery);
2122
int offset = (page - 1) * size;
2223

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

2728
List<AdminUserListResponse.Item> items = users.stream()
@@ -37,4 +38,16 @@ private String normalizeQuery(String query) {
3738
}
3839
return query.trim();
3940
}
41+
42+
private Long parseIdQuery(String query) {
43+
if (query == null) {
44+
return null;
45+
}
46+
try {
47+
long value = Long.parseLong(query);
48+
return value > 0 ? value : null;
49+
} catch (NumberFormatException ignored) {
50+
return null;
51+
}
52+
}
4053
}

src/main/java/org/devkor/apu/saerok_server/domain/notification/core/entity/NotificationType.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,4 @@ public enum NotificationType {
1919
// ---- System Notification Types ----
2020
SYSTEM_PUBLISHED_ANNOUNCEMENT,
2121
SYSTEM_ADMIN_MESSAGE,
22-
SYSTEM_CONTENT_DELETED,
2322
}

src/main/java/org/devkor/apu/saerok_server/domain/user/core/repository/UserRepository.java

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.devkor.apu.saerok_server.domain.user.core.repository;
22

33
import jakarta.persistence.EntityManager;
4+
import jakarta.persistence.Query;
45
import lombok.RequiredArgsConstructor;
56
import org.devkor.apu.saerok_server.domain.user.core.entity.SignupStatusType;
67
import org.devkor.apu.saerok_server.domain.user.core.entity.User;
@@ -62,51 +63,64 @@ public List<Long> findActiveUserIds(int offset, int limit) {
6263
.getResultList();
6364
}
6465

65-
public List<User> findActiveNicknameUsers(String nicknameQuery, int offset, int limit) {
66+
public List<User> searchActiveUsers(String nicknameQuery, Long idQuery, int offset, int limit) {
6667
String jpql = """
6768
SELECT u FROM User u
6869
WHERE u.deletedAt IS NULL
6970
AND u.signupStatus <> :withdrawn
70-
AND u.nickname IS NOT NULL
71-
AND TRIM(u.nickname) <> ''
7271
""";
73-
if (nicknameQuery != null) {
74-
jpql += " AND u.nickname LIKE :nicknameQuery";
75-
}
72+
jpql += buildSearchPredicate(nicknameQuery, idQuery);
7673
jpql += " ORDER BY u.nickname ASC, u.id ASC";
7774

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

83-
if (nicknameQuery != null) {
84-
query.setParameter("nicknameQuery", "%" + nicknameQuery + "%");
85-
}
80+
bindSearchParameters(query, nicknameQuery, idQuery);
8681

8782
return query.getResultList();
8883
}
8984

90-
public long countActiveNicknameUsers(String nicknameQuery) {
85+
public long countSearchActiveUsers(String nicknameQuery, Long idQuery) {
9186
String jpql = """
9287
SELECT COUNT(u) FROM User u
9388
WHERE u.deletedAt IS NULL
9489
AND u.signupStatus <> :withdrawn
95-
AND u.nickname IS NOT NULL
96-
AND TRIM(u.nickname) <> ''
9790
""";
98-
if (nicknameQuery != null) {
99-
jpql += " AND u.nickname LIKE :nicknameQuery";
100-
}
91+
jpql += buildSearchPredicate(nicknameQuery, idQuery);
10192

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

96+
bindSearchParameters(query, nicknameQuery, idQuery);
97+
98+
return query.getSingleResult();
99+
}
100+
101+
private String buildSearchPredicate(String nicknameQuery, Long idQuery) {
102+
boolean hasNickname = nicknameQuery != null;
103+
boolean hasId = idQuery != null;
104+
String nicknamePresent = "(u.nickname IS NOT NULL AND TRIM(u.nickname) <> '')";
105+
if (hasNickname && hasId) {
106+
return " AND ((" + nicknamePresent + " AND u.nickname LIKE :nicknameQuery) OR u.id = :idQuery)";
107+
}
108+
if (hasNickname) {
109+
return " AND " + nicknamePresent + " AND u.nickname LIKE :nicknameQuery";
110+
}
111+
if (hasId) {
112+
return " AND u.id = :idQuery";
113+
}
114+
return " AND " + nicknamePresent;
115+
}
116+
117+
private void bindSearchParameters(Query query, String nicknameQuery, Long idQuery) {
105118
if (nicknameQuery != null) {
106119
query.setParameter("nicknameQuery", "%" + nicknameQuery + "%");
107120
}
108-
109-
return query.getSingleResult();
121+
if (idQuery != null) {
122+
query.setParameter("idQuery", idQuery);
123+
}
110124
}
111125

112126
public List<User> findByIds(List<Long> ids) {

src/main/resources/config/notification-messages.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,3 @@ notification-messages:
2626
SYSTEM_ADMIN_MESSAGE:
2727
push-title: "{title}"
2828
push-body: "{body}"
29-
SYSTEM_CONTENT_DELETED:
30-
push-title: "콘텐츠 삭제 안내"
31-
push-body: "회원님의 콘텐츠가 운영정책 위반으로 삭제되었어요. 사유: {reason}"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
UPDATE notification
2+
SET type = 'SYSTEM_ADMIN_MESSAGE'
3+
WHERE type = 'SYSTEM_CONTENT_DELETED';

src/test/java/org/devkor/apu/saerok_server/domain/admin/notification/application/event/AdminNotificationWorkerTest.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,15 @@ void handle_contentDeletedByReport_callsNotifyUser() {
7171

7272
verify(notifySystemService).notifyUser(
7373
eq(42L),
74-
eq(NotificationType.SYSTEM_CONTENT_DELETED),
74+
eq(NotificationType.SYSTEM_ADMIN_MESSAGE),
7575
isNull(),
7676
extrasCaptor.capture()
7777
);
7878

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

8285
@Test

src/test/java/org/devkor/apu/saerok_server/domain/user/core/repository/UserRepositoryTest.java

Lines changed: 101 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ void findActiveUserIds_empty() {
138138
assertThat(activeIds).isEmpty();
139139
}
140140

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

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

161161
assertThat(users)
162162
.extracting(User::getNickname)
163163
.containsExactly("bravo", "charlie");
164-
assertThat(repo.countActiveNicknameUsers(null)).isEqualTo(2);
164+
assertThat(repo.countSearchActiveUsers(null, null)).isEqualTo(2);
165165
}
166166

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

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

180180
assertThat(firstPage)
181181
.extracting(User::getNickname)
182182
.containsExactly("alpha");
183183
assertThat(secondPage)
184184
.extracting(User::getNickname)
185185
.containsExactly("alpine");
186-
assertThat(repo.countActiveNicknameUsers("alp")).isEqualTo(2);
186+
assertThat(repo.countSearchActiveUsers("alp", null)).isEqualTo(2);
187187
}
188188

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

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

201201
assertThat(users)
202202
.extracting(User::getNickname)
203203
.containsExactly("둘리", "비둘기");
204-
assertThat(repo.countActiveNicknameUsers("둘")).isEqualTo(2);
204+
assertThat(repo.countSearchActiveUsers("둘", null)).isEqualTo(2);
205+
}
206+
207+
@Test @DisplayName("searchActiveUsers - ID 단독 검색")
208+
void searchActiveUsers_byIdOnly() {
209+
User alpha = user("id-alpha@example.com", "id-alpha");
210+
alpha.setSignupStatus(SignupStatusType.COMPLETED);
211+
User bravo = user("id-bravo@example.com", "id-bravo");
212+
bravo.setSignupStatus(SignupStatusType.COMPLETED);
213+
em.flush(); em.clear();
214+
215+
List<User> users = repo.searchActiveUsers(null, alpha.getId(), 0, 20);
216+
217+
assertThat(users)
218+
.extracting(User::getId)
219+
.containsExactly(alpha.getId());
220+
assertThat(repo.countSearchActiveUsers(null, alpha.getId())).isEqualTo(1);
221+
}
222+
223+
@Test @DisplayName("searchActiveUsers - ID와 닉네임 OR 검색")
224+
void searchActiveUsers_byIdOrNickname() {
225+
User alpha = user("or-alpha@example.com", "or-alpha");
226+
alpha.setSignupStatus(SignupStatusType.COMPLETED);
227+
User bravo = user("or-bravo@example.com", "or-bravo");
228+
bravo.setSignupStatus(SignupStatusType.COMPLETED);
229+
User charlie = user("or-charlie@example.com", "charlie-other");
230+
charlie.setSignupStatus(SignupStatusType.COMPLETED);
231+
em.flush(); em.clear();
232+
233+
List<User> users = repo.searchActiveUsers("or-", charlie.getId(), 0, 20);
234+
235+
assertThat(users)
236+
.extracting(User::getId)
237+
.containsExactlyInAnyOrder(alpha.getId(), bravo.getId(), charlie.getId());
238+
assertThat(repo.countSearchActiveUsers("or-", charlie.getId())).isEqualTo(3);
239+
}
240+
241+
@Test @DisplayName("searchActiveUsers - 닉네임 null인 활성 사용자도 ID로 검색됨")
242+
void searchActiveUsers_byIdAllowsNullNickname() {
243+
User profileRequired = user("profile-required@example.com", null);
244+
profileRequired.setSignupStatus(SignupStatusType.PROFILE_REQUIRED);
245+
em.flush(); em.clear();
246+
247+
List<User> users = repo.searchActiveUsers(null, profileRequired.getId(), 0, 20);
248+
249+
assertThat(users).extracting(User::getId).containsExactly(profileRequired.getId());
250+
assertThat(repo.countSearchActiveUsers(null, profileRequired.getId())).isEqualTo(1);
251+
}
252+
253+
@Test @DisplayName("searchActiveUsers - 닉네임 검색에는 null 닉네임 사용자 제외")
254+
void searchActiveUsers_nicknameQueryExcludesNullNickname() {
255+
User withNickname = user("with-nick@example.com", "with-nick-target");
256+
withNickname.setSignupStatus(SignupStatusType.COMPLETED);
257+
User noNickname = user("no-nick@example.com", null);
258+
noNickname.setSignupStatus(SignupStatusType.PROFILE_REQUIRED);
259+
em.flush(); em.clear();
260+
261+
List<User> users = repo.searchActiveUsers("nick-target", null, 0, 20);
262+
263+
assertThat(users).extracting(User::getNickname).containsExactly("with-nick-target");
264+
}
265+
266+
@Test @DisplayName("searchActiveUsers - 쿼리 없을 때 닉네임 null 사용자 제외")
267+
void searchActiveUsers_noQueryExcludesNullNickname() {
268+
User withNickname = user("default-nick@example.com", "default-nick");
269+
withNickname.setSignupStatus(SignupStatusType.COMPLETED);
270+
User noNickname = user("default-no-nick@example.com", null);
271+
noNickname.setSignupStatus(SignupStatusType.PROFILE_REQUIRED);
272+
em.flush(); em.clear();
273+
274+
List<User> users = repo.searchActiveUsers(null, null, 0, 20);
275+
276+
assertThat(users).extracting(User::getNickname).containsExactly("default-nick");
277+
}
278+
279+
@Test @DisplayName("searchActiveUsers - 탈퇴/삭제 사용자는 ID 검색에서도 제외")
280+
void searchActiveUsers_byIdExcludesInactive() {
281+
User withdrawn = user("inactive-withdrawn@example.com", "withdrawn-user");
282+
withdrawn.setSignupStatus(SignupStatusType.WITHDRAWN);
283+
User deleted = user("inactive-deleted@example.com", "deleted-user");
284+
deleted.setSignupStatus(SignupStatusType.COMPLETED);
285+
em.flush();
286+
deleted.softDelete();
287+
em.flush(); em.clear();
288+
289+
assertThat(repo.searchActiveUsers(null, withdrawn.getId(), 0, 20)).isEmpty();
290+
assertThat(repo.countSearchActiveUsers(null, withdrawn.getId())).isZero();
291+
assertThat(repo.searchActiveUsers(null, deleted.getId(), 0, 20)).isEmpty();
292+
assertThat(repo.countSearchActiveUsers(null, deleted.getId())).isZero();
205293
}
206294

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

0 commit comments

Comments
 (0)