Skip to content

Commit 3a7888f

Browse files
authored
Merge pull request #304 from Team-WSS/feat/#303
[FEAT] 알림 생성 API 구현
2 parents 1159b20 + 401a8aa commit 3a7888f

File tree

9 files changed

+176
-22
lines changed

9 files changed

+176
-22
lines changed

src/main/java/org/websoso/WSSServer/controller/NotificationController.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@
33
import static org.springframework.http.HttpStatus.CREATED;
44
import static org.springframework.http.HttpStatus.OK;
55

6+
import jakarta.validation.Valid;
67
import java.security.Principal;
78
import lombok.RequiredArgsConstructor;
89
import org.springframework.http.ResponseEntity;
910
import org.springframework.web.bind.annotation.GetMapping;
1011
import org.springframework.web.bind.annotation.PathVariable;
1112
import org.springframework.web.bind.annotation.PostMapping;
13+
import org.springframework.web.bind.annotation.RequestBody;
1214
import org.springframework.web.bind.annotation.RequestMapping;
1315
import org.springframework.web.bind.annotation.RequestParam;
1416
import org.springframework.web.bind.annotation.RestController;
1517
import org.websoso.WSSServer.domain.User;
18+
import org.websoso.WSSServer.dto.notification.NotificationCreateRequest;
1619
import org.websoso.WSSServer.dto.notification.NotificationGetResponse;
1720
import org.websoso.WSSServer.dto.notification.NotificationsGetResponse;
1821
import org.websoso.WSSServer.dto.notification.NotificationsReadStatusGetResponse;
@@ -64,4 +67,14 @@ public ResponseEntity<Void> createNotificationAsRead(Principal principal,
6467
.status(CREATED)
6568
.build();
6669
}
70+
71+
@PostMapping
72+
public ResponseEntity<Void> createNoticeNotification(Principal principal,
73+
@Valid @RequestBody NotificationCreateRequest notificationCreateRequest) {
74+
User user = userService.getUserOrException(Long.valueOf(principal.getName()));
75+
notificationService.createNoticeNotification(user, notificationCreateRequest);
76+
return ResponseEntity
77+
.status(CREATED)
78+
.build();
79+
}
6780
}

src/main/java/org/websoso/WSSServer/domain/Notification.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@ public class Notification extends BaseEntity {
2424
@Column(nullable = false)
2525
private Long notificationId;
2626

27-
@Column(nullable = false)
27+
@Column(columnDefinition = "varchar(200)", nullable = false)
2828
private String notificationTitle;
2929

30-
@Column(nullable = false)
30+
@Column(columnDefinition = "varchar(200)", nullable = false)
3131
private String notificationBody;
3232

33+
@Column(columnDefinition = "varchar(2000)")
3334
private String notificationDetail;
3435

3536
@Column(nullable = false)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.websoso.WSSServer.dto.notification;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.NotNull;
5+
import jakarta.validation.constraints.Size;
6+
import org.websoso.WSSServer.validation.ZeroAllowedUserIdConstraint;
7+
8+
public record NotificationCreateRequest(
9+
10+
@NotBlank(message = "알림 제목은 비어 있거나, 공백일 수 없습니다.")
11+
@Size(max = 200, message = "알림 제목은 200자를 초과할 수 없습니다.")
12+
String notificationTitle,
13+
14+
@NotBlank(message = "알림 개요는 비어 있거나, 공백일 수 없습니다.")
15+
@Size(max = 200, message = "알림 개요는 200자를 초과할 수 없습니다.")
16+
String notificationBody,
17+
18+
@NotBlank(message = "알림 내용은 비어 있거나, 공백일 수 없습니다.")
19+
@Size(max = 2000, message = "알림 내용은 2000자를 초과할 수 없습니다.")
20+
String notificationDetail,
21+
22+
@NotNull(message = "유저 ID는 필수입니다.")
23+
@ZeroAllowedUserIdConstraint
24+
Long userId,
25+
26+
@NotBlank(message = "알림 타입 이름은 필수입니다.")
27+
String notificationTypeName
28+
) {
29+
}

src/main/java/org/websoso/WSSServer/exception/error/CustomNotificationError.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
@Getter
1515
public enum CustomNotificationError implements ICustomError {
1616

17-
NOTIFICATION_NOT_FOUND("NOTIFICATION-001", "해당 ID를 가진 공지사항을 찾을 수 없습니다.", NOT_FOUND),
17+
NOTIFICATION_NOT_FOUND("NOTIFICATION-001", "해당 ID를 가진 알림을 찾을 수 없습니다.", NOT_FOUND),
1818
NOTIFICATION_READ_FORBIDDEN("NOTIFICATION-002", "해당 알림의 대상 유저가 아닙니다.", FORBIDDEN),
1919
NOTIFICATION_TYPE_INVALID("NOTIFICATION-003", "해당 알림은 요구된 알림 타입에 적합하지 않습니다.", BAD_REQUEST),
20-
NOTIFICATION_ALREADY_READ("NOTIFICATION-004", "해당 알림은 유저가 이미 읽은 알림입니다.", CONFLICT);
20+
NOTIFICATION_ALREADY_READ("NOTIFICATION-004", "해당 알림은 유저가 이미 읽은 알림입니다.", CONFLICT),
21+
NOTIFICATION_ADMIN_ONLY("NOTIFICATION-005", "관리자가 아닌 계정은 알림을 작성 혹은 수정 혹은 삭제할 수 없습니다.", FORBIDDEN),
22+
NOTIFICATION_NOT_NOTICE_TYPE("NOTIFICATION-006", "해당 알림 타입은 NOTICE에 속하지 않습니다", BAD_REQUEST);
2123

2224
private final String code;
2325
private final String description;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.websoso.WSSServer.exception.error;
2+
3+
import static org.springframework.http.HttpStatus.NOT_FOUND;
4+
5+
import lombok.AllArgsConstructor;
6+
import lombok.Getter;
7+
import org.springframework.http.HttpStatus;
8+
import org.websoso.WSSServer.exception.common.ICustomError;
9+
10+
@AllArgsConstructor
11+
@Getter
12+
public enum CustomNotificationTypeError implements ICustomError {
13+
14+
NOTIFICATION_TYPE_NOT_FOUND("NOTIFICATION-TYPE-001", "해당 이름을 가진 알림 타입을 찾을 수 없습니다.", NOT_FOUND);
15+
16+
private final String code;
17+
private final String description;
18+
private final HttpStatus statusCode;
19+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.websoso.WSSServer.exception.exception;
2+
3+
import lombok.Getter;
4+
import org.websoso.WSSServer.exception.common.AbstractCustomException;
5+
import org.websoso.WSSServer.exception.error.CustomNotificationTypeError;
6+
7+
@Getter
8+
public class CustomNotificationTypeException extends AbstractCustomException {
9+
10+
public CustomNotificationTypeException(CustomNotificationTypeError customNotificationTypeError, String message) {
11+
super(customNotificationTypeError, message);
12+
}
13+
}

src/main/java/org/websoso/WSSServer/repository/NotificationTypeRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.websoso.WSSServer.repository;
22

3+
import java.util.Optional;
34
import org.springframework.data.jpa.repository.JpaRepository;
45
import org.springframework.stereotype.Repository;
56
import org.websoso.WSSServer.domain.NotificationType;
@@ -8,4 +9,6 @@
89
public interface NotificationTypeRepository extends JpaRepository<NotificationType, Long> {
910

1011
NotificationType findByNotificationTypeName(String notificationTypeName);
12+
13+
Optional<NotificationType> findOptionalByNotificationTypeName(String notificationTypeName);
1114
}

src/main/java/org/websoso/WSSServer/repository/UserRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.websoso.WSSServer.repository;
22

3+
import java.util.List;
34
import org.springframework.data.jpa.repository.JpaRepository;
45
import org.springframework.stereotype.Repository;
56
import org.websoso.WSSServer.domain.User;
@@ -10,4 +11,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
1011
boolean existsByNickname(String nickname);
1112

1213
User findBySocialId(String socialId);
14+
15+
List<User> findAllByIsPushEnabledTrue();
1316
}

src/main/java/org/websoso/WSSServer/service/NotificationService.java

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,31 @@
11
package org.websoso.WSSServer.service;
22

3-
import static org.websoso.WSSServer.domain.common.NotificationTypeGroup.FEED;
4-
import static org.websoso.WSSServer.domain.common.NotificationTypeGroup.NOTICE;
5-
import static org.websoso.WSSServer.exception.error.CustomNotificationError.NOTIFICATION_ALREADY_READ;
6-
import static org.websoso.WSSServer.exception.error.CustomNotificationError.NOTIFICATION_NOT_FOUND;
7-
import static org.websoso.WSSServer.exception.error.CustomNotificationError.NOTIFICATION_READ_FORBIDDEN;
8-
import static org.websoso.WSSServer.exception.error.CustomNotificationError.NOTIFICATION_TYPE_INVALID;
9-
10-
import java.util.List;
11-
import java.util.Set;
12-
import java.util.stream.Collectors;
133
import lombok.RequiredArgsConstructor;
144
import org.springframework.data.domain.PageRequest;
155
import org.springframework.data.domain.Slice;
166
import org.springframework.stereotype.Service;
177
import org.springframework.transaction.annotation.Transactional;
18-
import org.websoso.WSSServer.domain.Notification;
19-
import org.websoso.WSSServer.domain.NotificationType;
20-
import org.websoso.WSSServer.domain.ReadNotification;
21-
import org.websoso.WSSServer.domain.User;
8+
import org.websoso.WSSServer.domain.*;
229
import org.websoso.WSSServer.domain.common.NotificationTypeGroup;
23-
import org.websoso.WSSServer.dto.notification.NotificationGetResponse;
24-
import org.websoso.WSSServer.dto.notification.NotificationInfo;
25-
import org.websoso.WSSServer.dto.notification.NotificationsGetResponse;
26-
import org.websoso.WSSServer.dto.notification.NotificationsReadStatusGetResponse;
10+
import org.websoso.WSSServer.dto.notification.*;
2711
import org.websoso.WSSServer.exception.exception.CustomNotificationException;
12+
import org.websoso.WSSServer.exception.exception.CustomNotificationTypeException;
13+
import org.websoso.WSSServer.notification.FCMService;
14+
import org.websoso.WSSServer.notification.dto.FCMMessageRequest;
2815
import org.websoso.WSSServer.repository.NotificationRepository;
16+
import org.websoso.WSSServer.repository.NotificationTypeRepository;
2917
import org.websoso.WSSServer.repository.ReadNotificationRepository;
18+
import org.websoso.WSSServer.repository.UserRepository;
19+
20+
import java.util.List;
21+
import java.util.Set;
22+
import java.util.stream.Collectors;
23+
24+
import static org.websoso.WSSServer.domain.common.NotificationTypeGroup.FEED;
25+
import static org.websoso.WSSServer.domain.common.NotificationTypeGroup.NOTICE;
26+
import static org.websoso.WSSServer.domain.common.Role.ADMIN;
27+
import static org.websoso.WSSServer.exception.error.CustomNotificationError.*;
28+
import static org.websoso.WSSServer.exception.error.CustomNotificationTypeError.NOTIFICATION_TYPE_NOT_FOUND;
3029

3130
@Service
3231
@RequiredArgsConstructor
@@ -36,6 +35,10 @@ public class NotificationService {
3635
private static final int DEFAULT_PAGE_NUMBER = 0;
3736
private final NotificationRepository notificationRepository;
3837
private final ReadNotificationRepository readNotificationRepository;
38+
private final NotificationTypeRepository notificationTypeRepository;
39+
private final UserRepository userRepository;
40+
private final FCMService fcmService;
41+
private final UserService userService;
3942

4043
@Transactional(readOnly = true)
4144
public NotificationsReadStatusGetResponse checkNotificationsReadStatus(User user) {
@@ -109,4 +112,72 @@ private void checkIfNotificationAlreadyRead(User user, Notification notification
109112
"notifications that the user has already read");
110113
}
111114
}
115+
116+
public void createNoticeNotification(User user, NotificationCreateRequest request) {
117+
validateAdminPrivilege(user);
118+
validateNoticeType(request.notificationTypeName());
119+
120+
Notification notification = notificationRepository.save(Notification.create(
121+
request.notificationTitle(),
122+
request.notificationBody(),
123+
request.notificationDetail(),
124+
request.userId(),
125+
null,
126+
getNotificationTypeOrException(request.notificationTypeName()))
127+
);
128+
129+
sendNoticePushMessage(request.userId(), notification);
130+
}
131+
132+
private void validateAdminPrivilege(User user) {
133+
if (user.getRole() != ADMIN) {
134+
throw new CustomNotificationException(NOTIFICATION_ADMIN_ONLY,
135+
"User who tried to create, modify, or delete the notice is not an ADMIN.");
136+
}
137+
}
138+
139+
private void validateNoticeType(String notificationTypeName) {
140+
if (!NotificationTypeGroup.isTypeInGroup(notificationTypeName, NOTICE)) {
141+
throw new CustomNotificationException(NOTIFICATION_NOT_NOTICE_TYPE,
142+
"given notification type does not belong to the NOTICE category");
143+
}
144+
}
145+
146+
private NotificationType getNotificationTypeOrException(String notificationTypeName) {
147+
return notificationTypeRepository
148+
.findOptionalByNotificationTypeName(notificationTypeName)
149+
.orElseThrow(() -> new CustomNotificationTypeException(NOTIFICATION_TYPE_NOT_FOUND,
150+
"notification type with the given name is not found"));
151+
}
152+
153+
private void sendNoticePushMessage(Long userId, Notification notification) {
154+
FCMMessageRequest fcmMessageRequest = FCMMessageRequest.of(
155+
notification.getNotificationTitle(),
156+
notification.getNotificationBody(),
157+
"",
158+
"notificationDetail",
159+
String.valueOf(notification.getNotificationId())
160+
);
161+
162+
List<String> targetFCMTokens = getTargetFCMTokens(userId);
163+
164+
fcmService.sendMulticastPushMessage(targetFCMTokens, fcmMessageRequest);
165+
}
166+
167+
private List<String> getTargetFCMTokens(Long userId) {
168+
if (userId.equals(0L)) {
169+
return userRepository.findAllByIsPushEnabledTrue()
170+
.stream()
171+
.flatMap(user -> user.getUserDevices().stream())
172+
.map(UserDevice::getFcmToken)
173+
.distinct()
174+
.toList();
175+
}
176+
return userService.getUserOrException(userId)
177+
.getUserDevices()
178+
.stream()
179+
.map(UserDevice::getFcmToken)
180+
.distinct()
181+
.toList();
182+
}
112183
}

0 commit comments

Comments
 (0)