-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathNotificationController.java
More file actions
206 lines (182 loc) · 9.31 KB
/
Copy pathNotificationController.java
File metadata and controls
206 lines (182 loc) · 9.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
package apu.saerok_admin.web;
import apu.saerok_admin.infra.notification.AdminNotificationClient;
import apu.saerok_admin.infra.notification.dto.AdminSendMessageRequest;
import apu.saerok_admin.infra.user.AdminUserClient;
import apu.saerok_admin.infra.user.dto.AdminUserListResponse;
import apu.saerok_admin.web.view.Breadcrumb;
import apu.saerok_admin.web.view.CurrentAdminProfile;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Slf4j
@Controller
@RequiredArgsConstructor
@RequestMapping("/notifications")
public class NotificationController {
private static final String PERMISSION_ADMIN_ANNOUNCEMENT_WRITE = "ADMIN_ANNOUNCEMENT_WRITE";
private static final int MAX_TITLE_LENGTH = 100;
private static final int MAX_BODY_LENGTH = 500;
private final AdminNotificationClient adminNotificationClient;
private final AdminUserClient adminUserClient;
@GetMapping("/compose")
public String compose(@ModelAttribute("currentAdminProfile") CurrentAdminProfile currentAdminProfile,
Model model) {
prepareComposeModel(currentAdminProfile, model);
if (!model.containsAttribute("form")) {
model.addAttribute("form", NotificationMessageForm.empty());
}
return "notifications/compose";
}
@GetMapping("/users/search")
@ResponseBody
public ResponseEntity<?> searchUsers(@ModelAttribute("currentAdminProfile") CurrentAdminProfile currentAdminProfile,
@RequestParam(name = "q", required = false) String query) {
if (currentAdminProfile == null || !currentAdminProfile.hasPermission(PERMISSION_ADMIN_ANNOUNCEMENT_WRITE)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("message", "사용자 목록을 조회할 권한이 없습니다."));
}
try {
AdminUserListResponse response = adminUserClient.listUsers(query, 1, 10);
return ResponseEntity.ok(response);
} catch (RestClientResponseException exception) {
log.warn("Failed to search admin users. status={}, body={}",
exception.getStatusCode(), exception.getResponseBodyAsString(), exception);
return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
.body(Map.of("message", "사용자 목록을 불러오지 못했습니다."));
} catch (RestClientException | IllegalStateException exception) {
log.warn("Failed to search admin users.", exception);
return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
.body(Map.of("message", "사용자 목록을 불러오지 못했습니다."));
}
}
@PostMapping("/send")
public String send(@ModelAttribute("currentAdminProfile") CurrentAdminProfile currentAdminProfile,
@RequestParam(name = "userIds", required = false) String userIdsRaw,
@RequestParam(name = "title", required = false) String title,
@RequestParam(name = "body", required = false) String body,
RedirectAttributes redirectAttributes) {
NotificationMessageForm form = new NotificationMessageForm(
nullToEmpty(userIdsRaw),
nullToEmpty(title),
nullToEmpty(body)
);
if (currentAdminProfile == null || !currentAdminProfile.hasPermission(PERMISSION_ADMIN_ANNOUNCEMENT_WRITE)) {
return redirectWithError("대상 공지를 발송할 권한이 없습니다.", form, redirectAttributes);
}
String trimmedTitle = title != null ? title.trim() : "";
String trimmedBody = body != null ? body.trim() : "";
if (!StringUtils.hasText(trimmedTitle) || !StringUtils.hasText(trimmedBody)) {
return redirectWithError("제목과 본문을 모두 입력해 주세요.", form, redirectAttributes);
}
if (trimmedTitle.length() > MAX_TITLE_LENGTH) {
return redirectWithError("제목은 100자 이내로 입력해 주세요.", form, redirectAttributes);
}
if (trimmedBody.length() > MAX_BODY_LENGTH) {
return redirectWithError("본문은 500자 이내로 입력해 주세요.", form, redirectAttributes);
}
List<Long> userIds;
try {
userIds = parseUserIds(userIdsRaw);
} catch (IllegalArgumentException exception) {
return redirectWithError(exception.getMessage(), form, redirectAttributes);
}
if (userIds.isEmpty()) {
return redirectWithError("수신자 사용자 ID를 1개 이상 입력해 주세요.", form, redirectAttributes);
}
try {
adminNotificationClient.sendMessage(new AdminSendMessageRequest(userIds, trimmedTitle, trimmedBody));
redirectAttributes.addFlashAttribute("flashStatus", "success");
redirectAttributes.addFlashAttribute("flashMessage",
userIds.size() + "명에게 대상 공지 발송 요청이 접수되었습니다.");
} catch (RestClientResponseException exception) {
log.warn("Failed to send admin notification. status={}, body={}",
exception.getStatusCode(), exception.getResponseBodyAsString(), exception);
return redirectWithError(resolveFailureMessage(exception), form, redirectAttributes);
} catch (RestClientException | IllegalStateException exception) {
log.warn("Failed to send admin notification.", exception);
return redirectWithError("대상 공지 발송에 실패했습니다. 잠시 후 다시 시도해주세요.", form, redirectAttributes);
}
return "redirect:/notifications/compose";
}
private void prepareComposeModel(CurrentAdminProfile currentAdminProfile, Model model) {
model.addAttribute("pageTitle", "시스템 알림 발송");
model.addAttribute("activeMenu", "notifications");
model.addAttribute("breadcrumbs", List.of(
Breadcrumb.of("대시보드", "/"),
Breadcrumb.active("대상 공지 발송")
));
model.addAttribute("toastMessages", List.of());
model.addAttribute("canSend", currentAdminProfile != null
&& currentAdminProfile.hasPermission(PERMISSION_ADMIN_ANNOUNCEMENT_WRITE));
}
private String redirectWithError(String message,
NotificationMessageForm form,
RedirectAttributes redirectAttributes) {
redirectAttributes.addFlashAttribute("flashStatus", "error");
redirectAttributes.addFlashAttribute("flashMessage", message);
redirectAttributes.addFlashAttribute("form", form);
return "redirect:/notifications/compose";
}
private List<Long> parseUserIds(String raw) {
if (!StringUtils.hasText(raw)) {
return List.of();
}
String[] tokens = raw.trim().split("[,\\s]+");
Set<Long> userIds = new LinkedHashSet<>();
for (String token : tokens) {
if (!StringUtils.hasText(token)) {
continue;
}
long userId;
try {
userId = Long.parseLong(token);
} catch (NumberFormatException exception) {
throw new IllegalArgumentException("사용자 ID는 숫자로만 입력해 주세요.");
}
if (userId <= 0) {
throw new IllegalArgumentException("사용자 ID는 1 이상의 숫자로 입력해 주세요.");
}
userIds.add(userId);
}
return List.copyOf(userIds);
}
private String resolveFailureMessage(RestClientResponseException exception) {
int statusCode = exception.getStatusCode().value();
if (statusCode == 400) {
return "발송 요청이 거절되었습니다. 사용자 ID와 메시지 길이를 확인해 주세요.";
}
if (statusCode == 403) {
return "대상 공지를 발송할 권한이 없습니다.";
}
return "대상 공지 발송에 실패했습니다. 잠시 후 다시 시도해주세요.";
}
private static String nullToEmpty(String value) {
return value == null ? "" : value;
}
public record NotificationMessageForm(
String userIdsRaw,
String title,
String body
) {
static NotificationMessageForm empty() {
return new NotificationMessageForm("", "", "");
}
}
}