Skip to content

Commit

Permalink
Merge pull request #192 from 100-hours-a-week/feat-notification
Browse files Browse the repository at this point in the history
댓글 알림 구현(알림 구독, 알림 전송만)
  • Loading branch information
Namgyu11 authored Sep 22, 2024
2 parents 72743af + 04731c7 commit 5ef1c27
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import connectripbe.connectrip_be.global.util.bucket4j.annotation.RateLimit;
import connectripbe.connectrip_be.member.entity.MemberEntity;
import connectripbe.connectrip_be.member.repository.MemberJpaRepository;
import connectripbe.connectrip_be.notification.dto.NotificationCommentResponse;
import connectripbe.connectrip_be.notification.service.NotificationService;
import connectripbe.connectrip_be.post.entity.AccompanyPostEntity;
import connectripbe.connectrip_be.post.repository.AccompanyPostRepository;
import java.util.List;
Expand All @@ -24,10 +26,12 @@ public class AccompanyCommentServiceImpl implements AccompanyCommentService {
private final AccompanyCommentRepository accompanyCommentRepository;
private final MemberJpaRepository memberRepository;
private final AccompanyPostRepository accompanyPostRepository;
private final NotificationService notificationService; // 주입된 notificationService


/**
* 댓글을 생성하는 메서드. 사용자 이메일을 통해 MemberEntity 를 조회하고, 게시물 ID를 통해 AccompanyPostEntity 를 조회한 후 AccompanyCommentEntity 를
* 생성하여 데이터베이스에 저장
* 댓글을 생성하는 메서드. 사용자 이메일을 통해 MemberEntity를 조회하고, 게시물 ID를 통해 AccompanyPostEntity를 조회한 후 AccompanyCommentEntity를 생성하여
* 데이터베이스에 저장
*
* @param memberId 댓글 작성자의 아이디
* @param request 댓글 생성 요청 정보 (게시물 ID, 댓글 내용 포함)
Expand All @@ -39,6 +43,7 @@ public AccompanyCommentResponse createComment(Long memberId, AccompanyCommentReq
MemberEntity member = getMember(memberId);
AccompanyPostEntity post = getPost(request.getPostId());

// 댓글 생성
AccompanyCommentEntity comment = AccompanyCommentEntity.builder()
.memberEntity(member)
.accompanyPostEntity(post)
Expand All @@ -47,9 +52,30 @@ public AccompanyCommentResponse createComment(Long memberId, AccompanyCommentReq

accompanyCommentRepository.save(comment);

// 댓글 내용에서 첫 20자를 추출하는 로직 추가
String limitedContent = limitContentTo20Characters(request.getContent());

// NotificationCommentResponse 생성 (댓글 내용은 첫 20자만)
NotificationCommentResponse notificationResponse = NotificationCommentResponse.fromEntity(comment,
limitedContent);

// 게시글 작성자에게 실시간 알림 전송
notificationService.sendNotification(post.getMemberEntity().getId(), notificationResponse);

return AccompanyCommentResponse.fromEntity(comment);
}

/**
* 댓글 내용을 20자로 제한하는 메서드
*
* @param content 댓글 내용
* @return 20자 이하로 잘린 댓글 내용
*/
private String limitContentTo20Characters(String content) {
return content.length() > 20 ? content.substring(0, 20) : content;
}


/**
* 댓글을 수정하는 메서드. 주어진 댓글 ID를 통해 AccompanyCommentEntity를 조회하고, 수정 권한이 있는지 확인한 후 댓글 내용을 업데이트
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,8 @@ public void profileUpdate(String nickname, String description) {
this.description = description;
}

public MemberEntity(Long id) {
this.id = id;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package connectripbe.connectrip_be.notification.dto;

import connectripbe.connectrip_be.comment.entity.AccompanyCommentEntity;
import java.time.LocalDateTime;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class NotificationCommentResponse {

private Long userId; // 댓글을 남긴 사용자 ID
private String userNickname; // 댓글을 남긴 사용자 닉네임
private String userProfilePath; // 댓글을 남긴 사용자 프로필 이미지 경로
private Long postId; // 댓글이 달린 게시물 ID
private String content; // 댓글 내용 (글자 제한 적용)
private LocalDateTime notificationTime; // 알림 생성 시간
private boolean isRead; // 읽음 여부


public static NotificationCommentResponse fromEntity(AccompanyCommentEntity comment, String content) {
return NotificationCommentResponse.builder()
.userId(comment.getMemberEntity().getId())
.userNickname(comment.getMemberEntity().getNickname())
.userProfilePath(comment.getMemberEntity().getProfileImagePath())
.postId(comment.getAccompanyPostEntity().getId())
.content(content)
.notificationTime(LocalDateTime.now())
.isRead(false)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package connectripbe.connectrip_be.notification.entity;

import connectripbe.connectrip_be.global.entity.BaseEntity;
import connectripbe.connectrip_be.member.entity.MemberEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "notifications")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class NotificationEntity extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private MemberEntity member; // 알림을 받을 사용자

@Column(nullable = false, length = 256)
private String message; // 알림 내용

@Column(name = "read_at")
private LocalDateTime readAt; // 읽은 시간

public void markAsRead() {
this.readAt = ZonedDateTime.now(ZoneId.of("UTC")).toLocalDateTime();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package connectripbe.connectrip_be.notification.repository;

import connectripbe.connectrip_be.notification.entity.NotificationEntity;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface NotificationRepository extends JpaRepository<NotificationEntity, Long> {
List<NotificationEntity> findByMemberIdAndReadAtIsNull(Long memberId);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package connectripbe.connectrip_be.notification.service;

import connectripbe.connectrip_be.notification.dto.NotificationCommentResponse;
import connectripbe.connectrip_be.notification.entity.NotificationEntity;
import java.util.List;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

public interface NotificationService {

SseEmitter subscribe(Long memberId); // SSE 연결 구독

void sendNotification(Long memberId, NotificationCommentResponse notificationResponse); // 알림 전송

List<NotificationEntity> getUnreadNotifications(Long memberId); // 읽지 않은 알림 조회
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package connectripbe.connectrip_be.notification.service.impl;

import connectripbe.connectrip_be.global.exception.GlobalException;
import connectripbe.connectrip_be.global.exception.type.ErrorCode;
import connectripbe.connectrip_be.member.entity.MemberEntity;
import connectripbe.connectrip_be.member.repository.MemberJpaRepository;
import connectripbe.connectrip_be.notification.dto.NotificationCommentResponse;
import connectripbe.connectrip_be.notification.entity.NotificationEntity;
import connectripbe.connectrip_be.notification.repository.NotificationRepository;
import connectripbe.connectrip_be.notification.service.NotificationService;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@Service
@RequiredArgsConstructor
public class NotificationServiceImpl implements NotificationService {

private final NotificationRepository notificationRepository;
private final MemberJpaRepository memberJpaRepository;
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();

/**
* 사용자가 알림을 구독할 수 있도록 SSE 연결을 설정하는 메서드. SSE 연결을 통해 실시간 알림을 구독하며, 연결이 완료되면 "알림 연결 완료" 메시지를 전송합니다.
*
* @param memberId 알림을 구독할 사용자의 ID
* @return SSE Emitter 객체 (알림 구독을 위한 연결)
*/
@Override
public SseEmitter subscribe(Long memberId) {
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
emitters.put(memberId, emitter);

try {
emitter.send(SseEmitter.event().name("CONNECTED").data("알림 연결 완료"));
} catch (IOException e) {
emitters.remove(memberId);
}

emitter.onCompletion(() -> emitters.remove(memberId));
emitter.onTimeout(() -> emitters.remove(memberId));

return emitter;
}

@Override
public void sendNotification(Long memberId, NotificationCommentResponse notificationResponse) {

// 사용자 ID로 MemberEntity 조회
MemberEntity member = memberJpaRepository.findById(memberId)
.orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND));

// NotificationEntity를 빌더 패턴으로 생성
NotificationEntity notification = NotificationEntity.builder()
.member(member)
.message(notificationResponse.getContent())
.build();

notificationRepository.save(notification);

// 알림을 받을 사용자에게 실시간 알림 전송
SseEmitter emitter = emitters.get(memberId);
if (emitter != null) {
try {
emitter.send(SseEmitter.event()
.name("COMMENT_ADDED")
.data(notificationResponse)); // NotificationCommentResponse 객체 전송
} catch (IOException e) {
emitters.remove(memberId);
}
}
}


/**
* 특정 사용자의 읽지 않은 알림 목록을 조회하는 메서드. 읽지 않은 알림(읽음 시간이 설정되지 않은 알림)을 데이터베이스에서 조회하여 반환합니다.
*
* @param memberId 알림을 조회할 사용자의 ID
* @return 읽지 않은 알림 목록
*/
@Override
public List<NotificationEntity> getUnreadNotifications(Long memberId) {
return notificationRepository.findByMemberIdAndReadAtIsNull(memberId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package connectripbe.connectrip_be.notification.web;

import connectripbe.connectrip_be.notification.entity.NotificationEntity;
import connectripbe.connectrip_be.notification.service.NotificationService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@RestController
@RequestMapping("/api/v1/comment-notifications")
@RequiredArgsConstructor
public class NotificationController {

private final NotificationService notificationService;

@GetMapping("/subscribe/{memberId}")
public SseEmitter subscribe(@PathVariable Long memberId) {
return notificationService.subscribe(memberId);
}

@GetMapping("/unread/{memberId}")
public ResponseEntity<List<NotificationEntity>> getUnreadNotifications(@PathVariable Long memberId) {
List<NotificationEntity> notifications = notificationService.getUnreadNotifications(memberId);
return ResponseEntity.ok(notifications);
}
}

0 comments on commit 5ef1c27

Please sign in to comment.