From 3e1cc835a7201ecbb447de810fde6abcd36d47e1 Mon Sep 17 00:00:00 2001 From: kchaeeun Date: Sat, 17 Aug 2024 18:22:59 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8=20[FEAT]=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B8=B0=EB=B3=B8=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/NotificationController.java | 27 ++++++ .../majorLink/domain/NotificationContent.java | 15 +++ .../example/majorLink/domain/RelatedUrl.java | 16 ++++ .../domain/enums/NotificationType.java | 5 + .../dto/response/NotificationResponse.java | 14 +++ .../repository/EmitterRepository.java | 18 ++++ .../repository/EmitterRepositoryImpl.java | 47 ++++++++++ .../repository/NotificationRepository.java | 7 ++ .../service/NotificationService.java | 92 +++++++++++++++++++ .../src/main/resources/static/index.html | 66 +++++++++++++ 10 files changed, 307 insertions(+) create mode 100644 majorLink/src/main/java/com/example/majorLink/controller/NotificationController.java create mode 100644 majorLink/src/main/java/com/example/majorLink/domain/NotificationContent.java create mode 100644 majorLink/src/main/java/com/example/majorLink/domain/RelatedUrl.java create mode 100644 majorLink/src/main/java/com/example/majorLink/domain/enums/NotificationType.java create mode 100644 majorLink/src/main/java/com/example/majorLink/dto/response/NotificationResponse.java create mode 100644 majorLink/src/main/java/com/example/majorLink/repository/EmitterRepository.java create mode 100644 majorLink/src/main/java/com/example/majorLink/repository/EmitterRepositoryImpl.java create mode 100644 majorLink/src/main/java/com/example/majorLink/repository/NotificationRepository.java create mode 100644 majorLink/src/main/java/com/example/majorLink/service/NotificationService.java create mode 100644 majorLink/src/main/resources/static/index.html diff --git a/majorLink/src/main/java/com/example/majorLink/controller/NotificationController.java b/majorLink/src/main/java/com/example/majorLink/controller/NotificationController.java new file mode 100644 index 0000000..5b0b8f7 --- /dev/null +++ b/majorLink/src/main/java/com/example/majorLink/controller/NotificationController.java @@ -0,0 +1,27 @@ +package com.example.majorLink.controller; + +import com.example.majorLink.global.auth.AuthUser; +import com.example.majorLink.service.NotificationService; +import lombok.RequiredArgsConstructor; +import org.hibernate.annotations.Parameter; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.UUID; + +@RestController +@RequestMapping("/notification") +@RequiredArgsConstructor +public class NotificationController { + private final NotificationService notificationService; + + // Last-Event-ID는 SSE 연결이 끊어졌을 경우, 클라이언트가 수신한 마지막 데이터 ID 값을 의미, 항상 존재 X -> false + @GetMapping(value = "/subscribe/{id}", produces = "text/event-stream") + public SseEmitter subscribe(@PathVariable UUID id, + @RequestHeader(value = "Last-Event-ID", required = false, defaultValue = "") String lastEventId) { + return notificationService.subscribe(id, lastEventId); + } +} diff --git a/majorLink/src/main/java/com/example/majorLink/domain/NotificationContent.java b/majorLink/src/main/java/com/example/majorLink/domain/NotificationContent.java new file mode 100644 index 0000000..28de87d --- /dev/null +++ b/majorLink/src/main/java/com/example/majorLink/domain/NotificationContent.java @@ -0,0 +1,15 @@ +package com.example.majorLink.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Embeddable +@NoArgsConstructor +public class NotificationContent { + @Column(nullable = false) + private String content; +} diff --git a/majorLink/src/main/java/com/example/majorLink/domain/RelatedUrl.java b/majorLink/src/main/java/com/example/majorLink/domain/RelatedUrl.java new file mode 100644 index 0000000..304d3f0 --- /dev/null +++ b/majorLink/src/main/java/com/example/majorLink/domain/RelatedUrl.java @@ -0,0 +1,16 @@ +package com.example.majorLink.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Embeddable +@NoArgsConstructor +public class RelatedUrl { + @Column(nullable = false) + private String url; +} diff --git a/majorLink/src/main/java/com/example/majorLink/domain/enums/NotificationType.java b/majorLink/src/main/java/com/example/majorLink/domain/enums/NotificationType.java new file mode 100644 index 0000000..355a833 --- /dev/null +++ b/majorLink/src/main/java/com/example/majorLink/domain/enums/NotificationType.java @@ -0,0 +1,5 @@ +package com.example.majorLink.domain.enums; + +public enum NotificationType { + APPLICATION +} diff --git a/majorLink/src/main/java/com/example/majorLink/dto/response/NotificationResponse.java b/majorLink/src/main/java/com/example/majorLink/dto/response/NotificationResponse.java new file mode 100644 index 0000000..5d8ba6c --- /dev/null +++ b/majorLink/src/main/java/com/example/majorLink/dto/response/NotificationResponse.java @@ -0,0 +1,14 @@ +package com.example.majorLink.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class NotificationResponse { + private Long id; + private String content; + private String url; + private Boolean isRead; + private String createdAt; +} diff --git a/majorLink/src/main/java/com/example/majorLink/repository/EmitterRepository.java b/majorLink/src/main/java/com/example/majorLink/repository/EmitterRepository.java new file mode 100644 index 0000000..0ab0778 --- /dev/null +++ b/majorLink/src/main/java/com/example/majorLink/repository/EmitterRepository.java @@ -0,0 +1,18 @@ +package com.example.majorLink.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Map; +import java.util.UUID; + +public interface EmitterRepository { + SseEmitter save(String emitterId, SseEmitter sseEmitter); + void saveEventCache(String emitterId, Object event); + // 해당 유저와 관련된 모든 emitter를 찾는다 + Map findAllEmitterStartWithByUserId(UUID userId); + // 해당 유저와 관련된 모든 이벤트를 찾는다. + Map findAllEventCacheStartWithByUserId(UUID userId); + + void deleteById(String emitterId); +} diff --git a/majorLink/src/main/java/com/example/majorLink/repository/EmitterRepositoryImpl.java b/majorLink/src/main/java/com/example/majorLink/repository/EmitterRepositoryImpl.java new file mode 100644 index 0000000..5fc761c --- /dev/null +++ b/majorLink/src/main/java/com/example/majorLink/repository/EmitterRepositoryImpl.java @@ -0,0 +1,47 @@ +package com.example.majorLink.repository; + +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class EmitterRepositoryImpl implements EmitterRepository{ + // 동시성 고려를 위해 ConcurrentHashMap 사용 + private final Map emitters = new ConcurrentHashMap<>(); + private final Map eventCache = new ConcurrentHashMap<>(); + + @Override + public SseEmitter save(String emitterId, SseEmitter sseEmitter) { + emitters.put(emitterId, sseEmitter); + return sseEmitter; + } + + @Override + public void saveEventCache(String emitterId, Object event) { + eventCache.put(emitterId, event); + } + + @Override + public Map findAllEmitterStartWithByUserId(UUID userId) { + return emitters.entrySet().stream() + .filter(entry -> entry.getKey().startsWith(String.valueOf(userId))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Override + public Map findAllEventCacheStartWithByUserId(UUID userId) { + return eventCache.entrySet().stream() + .filter(entry -> entry.getKey().startsWith(String.valueOf(userId))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Override + public void deleteById(String emitterId) { + emitters.remove(emitterId); + } + + +} diff --git a/majorLink/src/main/java/com/example/majorLink/repository/NotificationRepository.java b/majorLink/src/main/java/com/example/majorLink/repository/NotificationRepository.java new file mode 100644 index 0000000..3b9ccbe --- /dev/null +++ b/majorLink/src/main/java/com/example/majorLink/repository/NotificationRepository.java @@ -0,0 +1,7 @@ +package com.example.majorLink.repository; + +import com.example.majorLink.domain.Notification; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NotificationRepository extends JpaRepository { +} diff --git a/majorLink/src/main/java/com/example/majorLink/service/NotificationService.java b/majorLink/src/main/java/com/example/majorLink/service/NotificationService.java new file mode 100644 index 0000000..8150bad --- /dev/null +++ b/majorLink/src/main/java/com/example/majorLink/service/NotificationService.java @@ -0,0 +1,92 @@ +package com.example.majorLink.service; + +import com.example.majorLink.domain.*; +import com.example.majorLink.dto.response.NotificationResponse; +import com.example.majorLink.repository.EmitterRepository; +import com.example.majorLink.repository.EmitterRepositoryImpl; +import com.example.majorLink.repository.NotificationRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.Map; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class NotificationService { + private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60; + private final EmitterRepository emitterRepository = new EmitterRepositoryImpl(); + private final NotificationRepository notificationRepository; + + public SseEmitter subscribe(UUID userId, String lastEventId) { + String emitterId = userId + "_" + System.currentTimeMillis(); + SseEmitter emitter = emitterRepository.save(emitterId, new SseEmitter(DEFAULT_TIMEOUT)); + + emitter.onCompletion(() -> { + System.out.println("SSE Connection Completed, emitterId: " + emitterId); + emitterRepository.deleteById(emitterId); + }); + emitter.onTimeout(() -> { + System.out.println("SSE Connection Timed Out, emitterId: " + emitterId); + emitterRepository.deleteById(emitterId); + }); + + sendToClient(emitter, emitterId, "EventStream Create. [userId = " + userId + "]"); + + if (!lastEventId.isEmpty()) { + Map events = emitterRepository.findAllEventCacheStartWithByUserId(userId); + events.entrySet().stream() + .filter(entry -> lastEventId.compareTo(entry.getKey()) < 0) + .forEach(entry -> sendToClient(emitter, entry.getKey(), entry.getValue())); + } + return emitter; + } + + public void send(User receiver, ProfileCard lecture, String content) { + Notification notification = notificationRepository.save(createNotification(receiver, lecture, content)); + String userId = String.valueOf(receiver.getId()); + + Map sseEmitters = emitterRepository.findAllEmitterStartWithByUserId(UUID.fromString(userId)); + sseEmitters.forEach( + (key, emitter) -> { + emitterRepository.saveEventCache(key, notification); + sendToClient(emitter, key, NotificationResponse.builder() + .id(notification.getId()) + .content(String.valueOf(notification.getContent())) + .url(String.valueOf(notification.getUrl())) + .isRead(notification.getIsRead()) + .build()); + } + ); + } + + private Notification createNotification(User receiver, ProfileCard lecture, String content) { + RelatedUrl relatedUrl = new RelatedUrl(); + relatedUrl.setUrl("/lecture/" + lecture.getId()); + + return Notification.builder() + .receiver(receiver) + .content(content) + .lecture(lecture) + .url(relatedUrl) + .isRead(false) + .build(); + } + + private void sendToClient(SseEmitter emitter, String emitterId, Object data) { + try { + String jsonData = new ObjectMapper().writeValueAsString(data); + System.out.println("Sending Data to Client, emitterId: " + emitterId + ", data: " + jsonData); + emitter.send(SseEmitter.event() + .id(emitterId) + .data(jsonData)); + } catch (IOException exception) { + System.err.println("Failed to send data, emitterId: " + emitterId + ", exception: " + exception.getMessage()); + emitterRepository.deleteById(emitterId); + throw new RuntimeException("Failed to send data to client.", exception); + } + } +} diff --git a/majorLink/src/main/resources/static/index.html b/majorLink/src/main/resources/static/index.html new file mode 100644 index 0000000..9697dcc --- /dev/null +++ b/majorLink/src/main/resources/static/index.html @@ -0,0 +1,66 @@ + + + + + Notification Test Page + + + + + + + From df92952849abfffa6d9a0fdc4902f0f27cd4d742 Mon Sep 17 00:00:00 2001 From: kchaeeun Date: Mon, 19 Aug 2024 13:35:23 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=93=9D=20[DOC]=20.idea=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/compiler.xml | 2 +- .idea/modules.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index c6160d9..c82393e 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -7,7 +7,7 @@ - + diff --git a/.idea/modules.xml b/.idea/modules.xml index 995068b..c74ba53 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + From 639a7958560aebf2ed28b75998e9efc2a5f09746 Mon Sep 17 00:00:00 2001 From: kchaeeun Date: Mon, 19 Aug 2024 14:21:11 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=E2=9C=A8=20[FEAT]=20=EC=88=98=EA=B0=95=20?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD=20=EC=8B=9C=20=ED=8A=9C=ED=84=B0=EC=97=90?= =?UTF-8?q?=EA=B2=8C=20=EC=95=8C=EB=A6=BC=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/NotificationController.java | 11 ++++++---- .../com/example/majorLink/domain/Lecture.java | 7 +++++++ .../majorLink/domain/Notification.java | 20 ++++++++++++++++--- .../majorLink/service/LectureServiceImpl.java | 10 +++++++++- .../service/NotificationService.java | 16 +++++++++------ 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/majorLink/src/main/java/com/example/majorLink/controller/NotificationController.java b/majorLink/src/main/java/com/example/majorLink/controller/NotificationController.java index 5b0b8f7..9587384 100644 --- a/majorLink/src/main/java/com/example/majorLink/controller/NotificationController.java +++ b/majorLink/src/main/java/com/example/majorLink/controller/NotificationController.java @@ -1,5 +1,6 @@ package com.example.majorLink.controller; +import com.example.majorLink.domain.User; import com.example.majorLink.global.auth.AuthUser; import com.example.majorLink.service.NotificationService; import lombok.RequiredArgsConstructor; @@ -19,9 +20,11 @@ public class NotificationController { private final NotificationService notificationService; // Last-Event-ID는 SSE 연결이 끊어졌을 경우, 클라이언트가 수신한 마지막 데이터 ID 값을 의미, 항상 존재 X -> false - @GetMapping(value = "/subscribe/{id}", produces = "text/event-stream") - public SseEmitter subscribe(@PathVariable UUID id, - @RequestHeader(value = "Last-Event-ID", required = false, defaultValue = "") String lastEventId) { - return notificationService.subscribe(id, lastEventId); + @GetMapping(value = "/subscribe", produces = "text/event-stream") + public SseEmitter subscribe( + @AuthenticationPrincipal AuthUser authUser, + @RequestHeader(value = "Last-Event-ID", required = false, defaultValue = "") String lastEventId) { + User user = authUser.getUser(); + return notificationService.subscribe(user, lastEventId); } } diff --git a/majorLink/src/main/java/com/example/majorLink/domain/Lecture.java b/majorLink/src/main/java/com/example/majorLink/domain/Lecture.java index ec0d14d..0453b21 100644 --- a/majorLink/src/main/java/com/example/majorLink/domain/Lecture.java +++ b/majorLink/src/main/java/com/example/majorLink/domain/Lecture.java @@ -5,6 +5,8 @@ import com.example.majorLink.domain.enums.Level; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import java.time.LocalTime; import java.util.Date; @@ -67,6 +69,11 @@ public class Lecture extends BaseEntity{ @Column(nullable = false, length = 100) private String tutor; + @OnDelete(action = OnDeleteAction.CASCADE) + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + public void updateLecture(String name, String body, int curri, String info, Level level, int pNum, LocalTime time, Day day, Date startDate, Exam exam, Category category, String tag ){ this.name = name; this.body = body; diff --git a/majorLink/src/main/java/com/example/majorLink/domain/Notification.java b/majorLink/src/main/java/com/example/majorLink/domain/Notification.java index 9f4cada..908d7a4 100644 --- a/majorLink/src/main/java/com/example/majorLink/domain/Notification.java +++ b/majorLink/src/main/java/com/example/majorLink/domain/Notification.java @@ -4,6 +4,8 @@ import com.example.majorLink.domain.mapping.UserNotification; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import java.util.ArrayList; import java.util.List; @@ -25,8 +27,20 @@ public class Notification extends BaseEntity{ @Column(nullable = false, length = 1000) private String content; - @Enumerated(EnumType.STRING) - @Column(columnDefinition = "VARCHAR(10) DEFAULT 'UNCHECK'") - private CheckStatus status; + @Column(nullable = false) + private RelatedUrl url; + + @Column(nullable = false) + private Boolean isRead; + + // receiver 삭제 시 연관관계 동시 삭제 + @OnDelete(action = OnDeleteAction.CASCADE) + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User receiver; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "lecture_id") + private Lecture lecture; } \ No newline at end of file diff --git a/majorLink/src/main/java/com/example/majorLink/service/LectureServiceImpl.java b/majorLink/src/main/java/com/example/majorLink/service/LectureServiceImpl.java index 93bf963..63b2fbc 100644 --- a/majorLink/src/main/java/com/example/majorLink/service/LectureServiceImpl.java +++ b/majorLink/src/main/java/com/example/majorLink/service/LectureServiceImpl.java @@ -30,6 +30,7 @@ public class LectureServiceImpl implements LectureService { private final TutorLectureRepository tutorLectureRepository; private final TuteeLectureRepository tuteeLectureRepository; private final LikedRepository likedRepository; + private final NotificationService notificationService; // 강의 생성 @Override @@ -174,7 +175,14 @@ public TuteeLecture addLecture(UUID userId, Long lectureId) { lecture.addCurPNum(); - return tuteeLectureRepository.save(tuteeLecture); + TuteeLecture savedTuteeLecture = tuteeLectureRepository.save(tuteeLecture); + + // 수강 신청 시 튜터에게 알림 전달 + String msg = user.getNickname() + " 님으로 부터 수업 신청이 왔습니다."; + notificationService.send(lecture.getUser(), lecture, msg); + + return savedTuteeLecture; + } // 강의 좋아요 diff --git a/majorLink/src/main/java/com/example/majorLink/service/NotificationService.java b/majorLink/src/main/java/com/example/majorLink/service/NotificationService.java index 8150bad..d880961 100644 --- a/majorLink/src/main/java/com/example/majorLink/service/NotificationService.java +++ b/majorLink/src/main/java/com/example/majorLink/service/NotificationService.java @@ -6,8 +6,11 @@ import com.example.majorLink.repository.EmitterRepositoryImpl; import com.example.majorLink.repository.NotificationRepository; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; @@ -21,8 +24,9 @@ public class NotificationService { private final EmitterRepository emitterRepository = new EmitterRepositoryImpl(); private final NotificationRepository notificationRepository; - public SseEmitter subscribe(UUID userId, String lastEventId) { - String emitterId = userId + "_" + System.currentTimeMillis(); + public SseEmitter subscribe(User user, String lastEventId) { + // 토큰을 고유 식별자로 사용하여 emitterId를 생성합니다. + String emitterId = user.getId() + "_" + System.currentTimeMillis(); SseEmitter emitter = emitterRepository.save(emitterId, new SseEmitter(DEFAULT_TIMEOUT)); emitter.onCompletion(() -> { @@ -34,10 +38,10 @@ public SseEmitter subscribe(UUID userId, String lastEventId) { emitterRepository.deleteById(emitterId); }); - sendToClient(emitter, emitterId, "EventStream Create. [userId = " + userId + "]"); + sendToClient(emitter, emitterId, "EventStream Create. [userId = " + user.getId() + "]"); if (!lastEventId.isEmpty()) { - Map events = emitterRepository.findAllEventCacheStartWithByUserId(userId); + Map events = emitterRepository.findAllEventCacheStartWithByUserId(user.getId()); events.entrySet().stream() .filter(entry -> lastEventId.compareTo(entry.getKey()) < 0) .forEach(entry -> sendToClient(emitter, entry.getKey(), entry.getValue())); @@ -45,7 +49,7 @@ public SseEmitter subscribe(UUID userId, String lastEventId) { return emitter; } - public void send(User receiver, ProfileCard lecture, String content) { + public void send(User receiver, Lecture lecture, String content) { Notification notification = notificationRepository.save(createNotification(receiver, lecture, content)); String userId = String.valueOf(receiver.getId()); @@ -63,7 +67,7 @@ public void send(User receiver, ProfileCard lecture, String content) { ); } - private Notification createNotification(User receiver, ProfileCard lecture, String content) { + private Notification createNotification(User receiver, Lecture lecture, String content) { RelatedUrl relatedUrl = new RelatedUrl(); relatedUrl.setUrl("/lecture/" + lecture.getId()); From d2a4908585192f78e70890c2993e8f317b60a3e8 Mon Sep 17 00:00:00 2001 From: kchaeeun Date: Mon, 19 Aug 2024 16:21:42 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=E2=9C=A8=20[FEAT]=20=EC=88=98=EA=B0=95=20?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD=20=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20sender,?= =?UTF-8?q?=20receiver=20=ED=99=95=EC=9D=B8=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../majorLink/domain/Notification.java | 15 ++- .../com/example/majorLink/domain/User.java | 2 +- .../dto/response/NotificationResponse.java | 3 +- .../majorLink/service/LectureServiceImpl.java | 3 +- .../service/NotificationService.java | 21 +++-- .../src/main/resources/static/index.html | 91 +++++++++++-------- 6 files changed, 74 insertions(+), 61 deletions(-) diff --git a/majorLink/src/main/java/com/example/majorLink/domain/Notification.java b/majorLink/src/main/java/com/example/majorLink/domain/Notification.java index 908d7a4..4e33c14 100644 --- a/majorLink/src/main/java/com/example/majorLink/domain/Notification.java +++ b/majorLink/src/main/java/com/example/majorLink/domain/Notification.java @@ -21,24 +21,23 @@ public class Notification extends BaseEntity{ @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false, length = 100) - private String title; - @Column(nullable = false, length = 1000) private String content; @Column(nullable = false) - private RelatedUrl url; - - @Column(nullable = false) - private Boolean isRead; + private String url; // receiver 삭제 시 연관관계 동시 삭제 @OnDelete(action = OnDeleteAction.CASCADE) @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") + @JoinColumn(name = "receiver_id") private User receiver; + @OnDelete(action = OnDeleteAction.CASCADE) + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "sender_id") + private User sender; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "lecture_id") private Lecture lecture; diff --git a/majorLink/src/main/java/com/example/majorLink/domain/User.java b/majorLink/src/main/java/com/example/majorLink/domain/User.java index 953e858..86c98fc 100644 --- a/majorLink/src/main/java/com/example/majorLink/domain/User.java +++ b/majorLink/src/main/java/com/example/majorLink/domain/User.java @@ -59,7 +59,7 @@ public class User extends BaseEntity{ @Column(length = 40) private String favorite; - @Column(name = "learnPart", nullable = false, columnDefinition = "VARCHAR(20)") + @Column(name = "learnPart", columnDefinition = "VARCHAR(20)") private String learnPart; @Column(nullable = false, columnDefinition = "INT DEFAULT 0") diff --git a/majorLink/src/main/java/com/example/majorLink/dto/response/NotificationResponse.java b/majorLink/src/main/java/com/example/majorLink/dto/response/NotificationResponse.java index 5d8ba6c..60e0ddb 100644 --- a/majorLink/src/main/java/com/example/majorLink/dto/response/NotificationResponse.java +++ b/majorLink/src/main/java/com/example/majorLink/dto/response/NotificationResponse.java @@ -7,8 +7,9 @@ @Builder public class NotificationResponse { private Long id; + private String receiver; + private String sender; private String content; private String url; - private Boolean isRead; private String createdAt; } diff --git a/majorLink/src/main/java/com/example/majorLink/service/LectureServiceImpl.java b/majorLink/src/main/java/com/example/majorLink/service/LectureServiceImpl.java index 63b2fbc..d742483 100644 --- a/majorLink/src/main/java/com/example/majorLink/service/LectureServiceImpl.java +++ b/majorLink/src/main/java/com/example/majorLink/service/LectureServiceImpl.java @@ -57,6 +57,7 @@ public Lecture createLecture(UUID userId, LectureRequestDTO request) { .tag(request.getTag()) .tutor(user.getNickname()) .cNum(0) + .user(user) .build(); Lecture saveLecture = lectureRepository.save(lecture); @@ -179,7 +180,7 @@ public TuteeLecture addLecture(UUID userId, Long lectureId) { // 수강 신청 시 튜터에게 알림 전달 String msg = user.getNickname() + " 님으로 부터 수업 신청이 왔습니다."; - notificationService.send(lecture.getUser(), lecture, msg); + notificationService.send(user, lecture, msg); return savedTuteeLecture; diff --git a/majorLink/src/main/java/com/example/majorLink/service/NotificationService.java b/majorLink/src/main/java/com/example/majorLink/service/NotificationService.java index d880961..f11bdac 100644 --- a/majorLink/src/main/java/com/example/majorLink/service/NotificationService.java +++ b/majorLink/src/main/java/com/example/majorLink/service/NotificationService.java @@ -49,9 +49,9 @@ public SseEmitter subscribe(User user, String lastEventId) { return emitter; } - public void send(User receiver, Lecture lecture, String content) { - Notification notification = notificationRepository.save(createNotification(receiver, lecture, content)); - String userId = String.valueOf(receiver.getId()); + public void send(User sender, Lecture lecture, String content) { + Notification notification = notificationRepository.save(createNotification(sender, lecture, content)); + String userId = String.valueOf(sender.getId()); Map sseEmitters = emitterRepository.findAllEmitterStartWithByUserId(UUID.fromString(userId)); sseEmitters.forEach( @@ -61,22 +61,23 @@ public void send(User receiver, Lecture lecture, String content) { .id(notification.getId()) .content(String.valueOf(notification.getContent())) .url(String.valueOf(notification.getUrl())) - .isRead(notification.getIsRead()) + .sender(sender.getNickname()) + .receiver(lecture.getUser().getNickname()) + .createdAt(String.valueOf(notification.getCreatedAt())) .build()); } ); } - private Notification createNotification(User receiver, Lecture lecture, String content) { - RelatedUrl relatedUrl = new RelatedUrl(); - relatedUrl.setUrl("/lecture/" + lecture.getId()); + private Notification createNotification(User sender, Lecture lecture, String content) { + String url = "/lecture/" + lecture.getId() + "/details"; return Notification.builder() - .receiver(receiver) + .sender(sender) + .receiver(lecture.getUser()) .content(content) .lecture(lecture) - .url(relatedUrl) - .isRead(false) + .url(url) .build(); } diff --git a/majorLink/src/main/resources/static/index.html b/majorLink/src/main/resources/static/index.html index 9697dcc..c743bdb 100644 --- a/majorLink/src/main/resources/static/index.html +++ b/majorLink/src/main/resources/static/index.html @@ -5,62 +5,73 @@ Notification Test Page - + From e63a18c0b70b1ff7b211a8b7868c4a20cae6972c Mon Sep 17 00:00:00 2001 From: kchaeeun Date: Mon, 19 Aug 2024 16:46:21 +0900 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=94=A5=20[DELET]=20TEST=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=ED=94=84=EB=A1=A0=ED=8A=B8=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/static/index.html | 77 ------------------- 1 file changed, 77 deletions(-) delete mode 100644 majorLink/src/main/resources/static/index.html diff --git a/majorLink/src/main/resources/static/index.html b/majorLink/src/main/resources/static/index.html deleted file mode 100644 index c743bdb..0000000 --- a/majorLink/src/main/resources/static/index.html +++ /dev/null @@ -1,77 +0,0 @@ - - - - - Notification Test Page - - - - - - - From 5c74110a7b5414bd096ff8f3d85bb8fd552e736e Mon Sep 17 00:00:00 2001 From: kchaeeun Date: Mon, 19 Aug 2024 17:39:34 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=E2=9C=A8=20[FEAT]=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/NotificationController.java | 16 ++- .../dto/response/NotificationResponse.java | 4 + .../repository/NotificationRepository.java | 6 + .../service/NotificationService.java | 94 +------------- .../service/NotificationServiceImpl.java | 120 ++++++++++++++++++ 5 files changed, 149 insertions(+), 91 deletions(-) create mode 100644 majorLink/src/main/java/com/example/majorLink/service/NotificationServiceImpl.java diff --git a/majorLink/src/main/java/com/example/majorLink/controller/NotificationController.java b/majorLink/src/main/java/com/example/majorLink/controller/NotificationController.java index 9587384..b7eb15e 100644 --- a/majorLink/src/main/java/com/example/majorLink/controller/NotificationController.java +++ b/majorLink/src/main/java/com/example/majorLink/controller/NotificationController.java @@ -1,17 +1,17 @@ package com.example.majorLink.controller; import com.example.majorLink.domain.User; +import com.example.majorLink.dto.response.NotificationResponse; import com.example.majorLink.global.auth.AuthUser; import com.example.majorLink.service.NotificationService; import lombok.RequiredArgsConstructor; -import org.hibernate.annotations.Parameter; -import org.springframework.http.MediaType; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import java.util.UUID; +import java.util.List; @RestController @RequestMapping("/notification") @@ -27,4 +27,14 @@ public SseEmitter subscribe( User user = authUser.getUser(); return notificationService.subscribe(user, lastEventId); } + + @GetMapping + public ResponseEntity> getNotificationList( + @AuthenticationPrincipal AuthUser authUser) { + User user = authUser.getUser(); + + List notificationResponse = notificationService.getNotificationList(user); + + return ResponseEntity.status(HttpStatus.OK).body(notificationResponse); + } } diff --git a/majorLink/src/main/java/com/example/majorLink/dto/response/NotificationResponse.java b/majorLink/src/main/java/com/example/majorLink/dto/response/NotificationResponse.java index 60e0ddb..ed31b0a 100644 --- a/majorLink/src/main/java/com/example/majorLink/dto/response/NotificationResponse.java +++ b/majorLink/src/main/java/com/example/majorLink/dto/response/NotificationResponse.java @@ -1,10 +1,14 @@ package com.example.majorLink.dto.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @Builder +@NoArgsConstructor +@AllArgsConstructor public class NotificationResponse { private Long id; private String receiver; diff --git a/majorLink/src/main/java/com/example/majorLink/repository/NotificationRepository.java b/majorLink/src/main/java/com/example/majorLink/repository/NotificationRepository.java index 3b9ccbe..9100cae 100644 --- a/majorLink/src/main/java/com/example/majorLink/repository/NotificationRepository.java +++ b/majorLink/src/main/java/com/example/majorLink/repository/NotificationRepository.java @@ -1,7 +1,13 @@ package com.example.majorLink.repository; import com.example.majorLink.domain.Notification; +import com.example.majorLink.domain.User; +import com.example.majorLink.dto.response.NotificationResponse; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface NotificationRepository extends JpaRepository { + + List findAllByReceiver(User user); } diff --git a/majorLink/src/main/java/com/example/majorLink/service/NotificationService.java b/majorLink/src/main/java/com/example/majorLink/service/NotificationService.java index f11bdac..30f3d23 100644 --- a/majorLink/src/main/java/com/example/majorLink/service/NotificationService.java +++ b/majorLink/src/main/java/com/example/majorLink/service/NotificationService.java @@ -2,96 +2,14 @@ import com.example.majorLink.domain.*; import com.example.majorLink.dto.response.NotificationResponse; -import com.example.majorLink.repository.EmitterRepository; -import com.example.majorLink.repository.EmitterRepositoryImpl; -import com.example.majorLink.repository.NotificationRepository; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import java.io.IOException; -import java.util.Map; -import java.util.UUID; +import java.util.List; -@Service -@RequiredArgsConstructor -public class NotificationService { - private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60; - private final EmitterRepository emitterRepository = new EmitterRepositoryImpl(); - private final NotificationRepository notificationRepository; +public interface NotificationService { + SseEmitter subscribe(User user, String lastEventId); + void send(User sender, Lecture lecture, String content); - public SseEmitter subscribe(User user, String lastEventId) { - // 토큰을 고유 식별자로 사용하여 emitterId를 생성합니다. - String emitterId = user.getId() + "_" + System.currentTimeMillis(); - SseEmitter emitter = emitterRepository.save(emitterId, new SseEmitter(DEFAULT_TIMEOUT)); - - emitter.onCompletion(() -> { - System.out.println("SSE Connection Completed, emitterId: " + emitterId); - emitterRepository.deleteById(emitterId); - }); - emitter.onTimeout(() -> { - System.out.println("SSE Connection Timed Out, emitterId: " + emitterId); - emitterRepository.deleteById(emitterId); - }); - - sendToClient(emitter, emitterId, "EventStream Create. [userId = " + user.getId() + "]"); - - if (!lastEventId.isEmpty()) { - Map events = emitterRepository.findAllEventCacheStartWithByUserId(user.getId()); - events.entrySet().stream() - .filter(entry -> lastEventId.compareTo(entry.getKey()) < 0) - .forEach(entry -> sendToClient(emitter, entry.getKey(), entry.getValue())); - } - return emitter; - } - - public void send(User sender, Lecture lecture, String content) { - Notification notification = notificationRepository.save(createNotification(sender, lecture, content)); - String userId = String.valueOf(sender.getId()); - - Map sseEmitters = emitterRepository.findAllEmitterStartWithByUserId(UUID.fromString(userId)); - sseEmitters.forEach( - (key, emitter) -> { - emitterRepository.saveEventCache(key, notification); - sendToClient(emitter, key, NotificationResponse.builder() - .id(notification.getId()) - .content(String.valueOf(notification.getContent())) - .url(String.valueOf(notification.getUrl())) - .sender(sender.getNickname()) - .receiver(lecture.getUser().getNickname()) - .createdAt(String.valueOf(notification.getCreatedAt())) - .build()); - } - ); - } - - private Notification createNotification(User sender, Lecture lecture, String content) { - String url = "/lecture/" + lecture.getId() + "/details"; - - return Notification.builder() - .sender(sender) - .receiver(lecture.getUser()) - .content(content) - .lecture(lecture) - .url(url) - .build(); - } - - private void sendToClient(SseEmitter emitter, String emitterId, Object data) { - try { - String jsonData = new ObjectMapper().writeValueAsString(data); - System.out.println("Sending Data to Client, emitterId: " + emitterId + ", data: " + jsonData); - emitter.send(SseEmitter.event() - .id(emitterId) - .data(jsonData)); - } catch (IOException exception) { - System.err.println("Failed to send data, emitterId: " + emitterId + ", exception: " + exception.getMessage()); - emitterRepository.deleteById(emitterId); - throw new RuntimeException("Failed to send data to client.", exception); - } - } + // 전체 알림 조회 + List getNotificationList(User user); } diff --git a/majorLink/src/main/java/com/example/majorLink/service/NotificationServiceImpl.java b/majorLink/src/main/java/com/example/majorLink/service/NotificationServiceImpl.java new file mode 100644 index 0000000..a810cb2 --- /dev/null +++ b/majorLink/src/main/java/com/example/majorLink/service/NotificationServiceImpl.java @@ -0,0 +1,120 @@ +package com.example.majorLink.service; + +import com.example.majorLink.domain.*; +import com.example.majorLink.dto.response.NotificationResponse; +import com.example.majorLink.repository.EmitterRepository; +import com.example.majorLink.repository.EmitterRepositoryImpl; +import com.example.majorLink.repository.NotificationRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class NotificationServiceImpl implements NotificationService { + private static final Long DEFAULT_TIMEOUT = 5L * 1000 * 60 * 60; // 5시간 + private final EmitterRepository emitterRepository = new EmitterRepositoryImpl(); + private final NotificationRepository notificationRepository; + + @Override + public SseEmitter subscribe(User user, String lastEventId) { + // 토큰을 고유 식별자로 사용하여 emitterId를 생성합니다. + String emitterId = user.getId() + "_" + System.currentTimeMillis(); + SseEmitter emitter = emitterRepository.save(emitterId, new SseEmitter(DEFAULT_TIMEOUT)); + + emitter.onCompletion(() -> { + System.out.println("SSE Connection Completed, emitterId: " + emitterId); + emitterRepository.deleteById(emitterId); + }); + emitter.onTimeout(() -> { + System.out.println("SSE Connection Timed Out, emitterId: " + emitterId); + emitterRepository.deleteById(emitterId); + }); + + sendToClient(emitter, emitterId, "EventStream Create. [userId = " + user.getId() + "]"); + + if (!lastEventId.isEmpty()) { + Map events = emitterRepository.findAllEventCacheStartWithByUserId(user.getId()); + events.entrySet().stream() + .filter(entry -> lastEventId.compareTo(entry.getKey()) < 0) + .forEach(entry -> sendToClient(emitter, entry.getKey(), entry.getValue())); + } + return emitter; + } + + @Override + public void send(User sender, Lecture lecture, String content) { + Notification notification = notificationRepository.save(createNotification(sender, lecture, content)); + String userId = String.valueOf(sender.getId()); + + Map sseEmitters = emitterRepository.findAllEmitterStartWithByUserId(UUID.fromString(userId)); + sseEmitters.forEach( + (key, emitter) -> { + emitterRepository.saveEventCache(key, notification); + sendToClient(emitter, key, NotificationResponse.builder() + .id(notification.getId()) + .content(String.valueOf(notification.getContent())) + .url(String.valueOf(notification.getUrl())) + .sender(sender.getNickname()) + .receiver(lecture.getUser().getNickname()) + .createdAt(String.valueOf(notification.getCreatedAt())) + .build()); + } + ); + } + + private Notification createNotification(User sender, Lecture lecture, String content) { + String url = "/lecture/" + lecture.getId() + "/details"; + + return Notification.builder() + .sender(sender) + .receiver(lecture.getUser()) + .content(content) + .lecture(lecture) + .url(url) + .build(); + } + + private void sendToClient(SseEmitter emitter, String emitterId, Object data) { + try { + String jsonData = new ObjectMapper().writeValueAsString(data); + System.out.println("Sending Data to Client, emitterId: " + emitterId + ", data: " + jsonData); + emitter.send(SseEmitter.event() + .id(emitterId) + .data(jsonData)); + } catch (IOException exception) { + System.err.println("Failed to send data, emitterId: " + emitterId + ", exception: " + exception.getMessage()); + emitterRepository.deleteById(emitterId); + throw new RuntimeException("Failed to send data to client.", exception); + } + } + + // 전체 알림 조회 + @Override + @Transactional(readOnly = true) + public List getNotificationList(User user) { + List notifications = notificationRepository.findAllByReceiver(user); + return notifications.stream() + .map(notification -> NotificationResponse.builder() + .id(notification.getId()) + .sender(notification.getSender().getNickname()) + .receiver(notification.getReceiver().getNickname()) + .content(notification.getContent()) + .url(notification.getUrl()) + .createdAt(String.valueOf(notification.getCreatedAt())) + .build()) + .collect(Collectors.toList()); + + + } +} From d98da0d3421741b4f60b2b4b4ce5430a21512a0b Mon Sep 17 00:00:00 2001 From: kchaeeun Date: Mon, 19 Aug 2024 17:47:24 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=92=A1=20[ADD]=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EA=B8=B0=EB=8A=A5=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/NotificationController.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/majorLink/src/main/java/com/example/majorLink/controller/NotificationController.java b/majorLink/src/main/java/com/example/majorLink/controller/NotificationController.java index b7eb15e..074ec14 100644 --- a/majorLink/src/main/java/com/example/majorLink/controller/NotificationController.java +++ b/majorLink/src/main/java/com/example/majorLink/controller/NotificationController.java @@ -20,6 +20,14 @@ public class NotificationController { private final NotificationService notificationService; // Last-Event-ID는 SSE 연결이 끊어졌을 경우, 클라이언트가 수신한 마지막 데이터 ID 값을 의미, 항상 존재 X -> false + + /** + * 알림을 위한 구독 API + * [GET] /notification/subscribe + * @param authUser + * @param lastEventId + * @return + */ @GetMapping(value = "/subscribe", produces = "text/event-stream") public SseEmitter subscribe( @AuthenticationPrincipal AuthUser authUser, @@ -28,6 +36,12 @@ public SseEmitter subscribe( return notificationService.subscribe(user, lastEventId); } + /** + * 알림 전체 조회 + * [GET] /notification + * @param authUser + * @return + */ @GetMapping public ResponseEntity> getNotificationList( @AuthenticationPrincipal AuthUser authUser) {