diff --git a/src/main/java/com/jiwon/mylog/domain/comment/service/CommentService.java b/src/main/java/com/jiwon/mylog/domain/comment/service/CommentService.java index 0db5955..e7cf953 100644 --- a/src/main/java/com/jiwon/mylog/domain/comment/service/CommentService.java +++ b/src/main/java/com/jiwon/mylog/domain/comment/service/CommentService.java @@ -6,6 +6,8 @@ import com.jiwon.mylog.domain.comment.entity.Comment; import com.jiwon.mylog.domain.comment.repository.CommentRepository; import com.jiwon.mylog.domain.event.dto.CommentCreatedEvent; +import com.jiwon.mylog.domain.notification.repository.NotificationRepository; +import com.jiwon.mylog.domain.notification.service.NotificationService; import com.jiwon.mylog.domain.post.entity.Post; import com.jiwon.mylog.domain.user.entity.User; import com.jiwon.mylog.global.common.error.ErrorCode; @@ -14,11 +16,13 @@ import com.jiwon.mylog.domain.post.repository.PostRepository; import com.jiwon.mylog.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Slf4j @RequiredArgsConstructor @Service public class CommentService { @@ -45,7 +49,18 @@ public CommentResponse createComment(Long userId, Long postId, CommentCreateRequ Comment comment = Comment.create(request, parent, user, post); Comment savedComment = commentRepository.save(comment); - eventPublisher.publishEvent(new CommentCreatedEvent(userId, savedComment.getId())); + Long receiverId = post.getUser().getId(); + + if (!receiverId.equals(userId)) { + eventPublisher.publishEvent( + new CommentCreatedEvent( + postId, + post.getUser().getId(), + comment.getId(), + userId, + user.getUsername()) + ); + } return CommentResponse.fromComment(savedComment); } diff --git a/src/main/java/com/jiwon/mylog/domain/event/NotificationEventListener.java b/src/main/java/com/jiwon/mylog/domain/event/NotificationEventListener.java new file mode 100644 index 0000000..7ebcbb3 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/event/NotificationEventListener.java @@ -0,0 +1,48 @@ +package com.jiwon.mylog.domain.event; + +import com.jiwon.mylog.domain.event.dto.CommentCreatedEvent; +import com.jiwon.mylog.domain.notification.entity.Notification; +import com.jiwon.mylog.domain.notification.repository.NotificationRepository; +import com.jiwon.mylog.domain.notification.service.NotificationService; +import com.jiwon.mylog.domain.notification.entity.NotificationType; +import com.jiwon.mylog.domain.user.entity.User; +import com.jiwon.mylog.domain.user.repository.UserRepository; +import com.jiwon.mylog.global.common.error.ErrorCode; +import com.jiwon.mylog.global.common.error.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Component +public class NotificationEventListener { + + private final NotificationService notificationService; + private final NotificationRepository notificationRepository; + private final UserRepository userRepository; + + @Transactional + @EventListener + public void handleCommentCreated(CommentCreatedEvent event) { + Long receiverId = event.getPostWriterId(); + User receiver = userRepository.findById(receiverId) + .orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_USER)); + + Notification notification = Notification.create( + receiver, + event.getCommentWriterName() + "왹이 외계 수집물에 발사를 남겼습니다.", + "/posts/" + event.getPostId(), + NotificationType.COMMENT + ); + notificationRepository.save(notification); + + try { + notificationService.sendNotification(receiverId, notification); + } catch(Exception e) { + log.warn("SSE 전송 실패"); + } + } +} diff --git a/src/main/java/com/jiwon/mylog/domain/event/PointEventListener.java b/src/main/java/com/jiwon/mylog/domain/event/PointEventListener.java index 5acbc5d..a8b2886 100644 --- a/src/main/java/com/jiwon/mylog/domain/event/PointEventListener.java +++ b/src/main/java/com/jiwon/mylog/domain/event/PointEventListener.java @@ -50,10 +50,10 @@ public void handlePostCreated(PostCreatedEvent event) { @EventListener public void handleCommentCreated(CommentCreatedEvent event) { if (historyRepository.countDailyPointByDescription( - event.getUserId(), + event.getCommentWriterId(), COMMENT_EARN_DESCRIPTION) >= COMMENT_POINT_LIMIT) { return; } - pointService.earnPoint(event.getUserId(), 44, COMMENT_EARN_DESCRIPTION); + pointService.earnPoint(event.getCommentWriterId(), 44, COMMENT_EARN_DESCRIPTION); } } diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/CommentCreatedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/CommentCreatedEvent.java index 3fcfe96..fa87b3b 100644 --- a/src/main/java/com/jiwon/mylog/domain/event/dto/CommentCreatedEvent.java +++ b/src/main/java/com/jiwon/mylog/domain/event/dto/CommentCreatedEvent.java @@ -6,6 +6,9 @@ @Getter @RequiredArgsConstructor public class CommentCreatedEvent { - private final Long userId; + private final Long postId; + private final Long postWriterId; private final Long commentId; + private final Long commentWriterId; + private final String commentWriterName; } diff --git a/src/main/java/com/jiwon/mylog/domain/notification/controller/NotificationController.java b/src/main/java/com/jiwon/mylog/domain/notification/controller/NotificationController.java new file mode 100644 index 0000000..5efeab1 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/notification/controller/NotificationController.java @@ -0,0 +1,71 @@ +package com.jiwon.mylog.domain.notification.controller; + +import com.jiwon.mylog.domain.notification.dto.NotificationCountResponse; +import com.jiwon.mylog.domain.notification.service.NotificationService; +import com.jiwon.mylog.global.common.entity.PageResponse; +import com.jiwon.mylog.global.common.error.ErrorCode; +import com.jiwon.mylog.global.common.error.exception.UnauthorizedException; +import com.jiwon.mylog.global.security.auth.annotation.LoginUser; +import com.jiwon.mylog.global.security.jwt.JwtService; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@Slf4j +@RequiredArgsConstructor +@RequestMapping("/api") +@RestController +public class NotificationController { + + private final NotificationService notificationService; + private final JwtService jwtService; + + @PatchMapping("/notifications") + public ResponseEntity updateNotificationRead(@LoginUser Long userId) { + notificationService.updateNotificationRead(userId); + return new ResponseEntity<>(HttpStatus.OK); + } + + @GetMapping("/notifications") + public ResponseEntity getNotifications( + @LoginUser Long userId, + @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { + PageResponse response = notificationService.getAllNotifications(userId, pageable); + return ResponseEntity.ok(response); + } + + @GetMapping("/notifications/count") + public ResponseEntity countUnreadNotification(@LoginUser Long userId) { + NotificationCountResponse response = notificationService.countUnreadNotifications(userId); + return ResponseEntity.ok(response); + } + + @GetMapping(value = "/sse/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter subscribe( + @CookieValue(value = "accessToken", required = false) String accessToken, + HttpServletResponse response) { + + if (accessToken == null || !jwtService.validateToken(accessToken)) { + throw new UnauthorizedException(ErrorCode.UNAUTHORIZED); + } + + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Connection", "keep-alive"); + response.setHeader("X-Accel-Buffering", "no"); + + Long userId = jwtService.getUserId(accessToken); + return notificationService.subscribe(userId); + } +} diff --git a/src/main/java/com/jiwon/mylog/domain/notification/dto/NotificationCountResponse.java b/src/main/java/com/jiwon/mylog/domain/notification/dto/NotificationCountResponse.java new file mode 100644 index 0000000..ddfa419 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/notification/dto/NotificationCountResponse.java @@ -0,0 +1,10 @@ +package com.jiwon.mylog.domain.notification.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class NotificationCountResponse { + private long unreadCount; +} diff --git a/src/main/java/com/jiwon/mylog/domain/notification/dto/NotificationResponse.java b/src/main/java/com/jiwon/mylog/domain/notification/dto/NotificationResponse.java new file mode 100644 index 0000000..2642b5c --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/notification/dto/NotificationResponse.java @@ -0,0 +1,27 @@ +package com.jiwon.mylog.domain.notification.dto; + +import com.jiwon.mylog.domain.notification.entity.Notification; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@AllArgsConstructor +@Builder +@Getter +public class NotificationResponse { + private Long notificationId; + private String content; + private String url; + private LocalDateTime createdAt; + + public static NotificationResponse from(Notification notification) { + return NotificationResponse.builder() + .notificationId(notification.getId()) + .content(notification.getContent()) + .url(notification.getUrl()) + .createdAt(notification.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/com/jiwon/mylog/domain/notification/entity/Notification.java b/src/main/java/com/jiwon/mylog/domain/notification/entity/Notification.java new file mode 100644 index 0000000..01b7c6e --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/notification/entity/Notification.java @@ -0,0 +1,64 @@ +package com.jiwon.mylog.domain.notification.entity; + +import com.jiwon.mylog.domain.user.entity.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@EntityListeners(AuditingEntityListener.class) +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Getter +@Entity +public class Notification { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String content; + + @Column(nullable = false) + private String url; + + @ManyToOne(fetch = FetchType.LAZY) + private User receiver; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private NotificationType type; + + @Column(nullable = false) + private boolean isRead = false; + + @CreatedDate + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + + public static Notification create(User user, String content, String url, NotificationType type) { + return Notification.builder() + .receiver(user) + .content(content) + .url(url) + .type(type) + .isRead(false) + .build(); + } +} diff --git a/src/main/java/com/jiwon/mylog/domain/notification/entity/NotificationType.java b/src/main/java/com/jiwon/mylog/domain/notification/entity/NotificationType.java new file mode 100644 index 0000000..faea851 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/notification/entity/NotificationType.java @@ -0,0 +1,10 @@ +package com.jiwon.mylog.domain.notification.entity; + +public enum NotificationType { + COMMENT, + REPLY, + FOLLOW, + UNFOLLOW, + SERVER, + ETC +} diff --git a/src/main/java/com/jiwon/mylog/domain/notification/repository/NotificationRepository.java b/src/main/java/com/jiwon/mylog/domain/notification/repository/NotificationRepository.java new file mode 100644 index 0000000..aded4c6 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/notification/repository/NotificationRepository.java @@ -0,0 +1,25 @@ +package com.jiwon.mylog.domain.notification.repository; + +import com.jiwon.mylog.domain.notification.entity.Notification; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface NotificationRepository extends JpaRepository { + + @Modifying(clearAutomatically = true) + @Query("update Notification n set n.isRead = true where n.receiver.id = :receiverId and n.isRead = false") + void updateReadStateByReceiverId(@Param("receiverId") Long receiverId); + + @Query("select count(n.id) from Notification n where n.receiver.id = :receiverId and n.isRead = false") + long countByReceiverIdAndReadIsFalse(@Param("receiverId") Long receiverId); + + Page findAllByReceiverId(Long receiverId, Pageable pageable); +} diff --git a/src/main/java/com/jiwon/mylog/domain/notification/repository/SseEmitterRepository.java b/src/main/java/com/jiwon/mylog/domain/notification/repository/SseEmitterRepository.java new file mode 100644 index 0000000..87e2231 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/notification/repository/SseEmitterRepository.java @@ -0,0 +1,61 @@ +package com.jiwon.mylog.domain.notification.repository; + +import org.springframework.stereotype.Repository; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Repository +public class SseEmitterRepository { + + private final Map emitters = new ConcurrentHashMap<>(); + private final Map connectionsByUserId = new ConcurrentHashMap<>(); + private final Map emitterTimestamps = new ConcurrentHashMap<>(); + + public String save(Long userId, SseEmitter emitter) { + String emitterId = generateConnectionId(userId); + emitters.put(emitterId, emitter); + connectionsByUserId.put(emitterId, userId); + emitterTimestamps.put(emitterId, System.currentTimeMillis()); + return emitterId; + } + + public Optional get(String emitterId) { + return Optional.ofNullable(emitters.get(emitterId)); + } + + public void delete(String emitterId) { + emitters.remove(emitterId); + connectionsByUserId.remove(emitterId); + emitterTimestamps.remove(emitterId); + } + + public List findEmitterIdsByUserId(Long userId) { + return connectionsByUserId.entrySet().stream() + .filter(entry -> entry.getValue().equals(userId)) + .map(Map.Entry::getKey) + .toList(); + } + + public List findEmittersByUserId(Long userId) { + return connectionsByUserId.entrySet().stream() + .filter(entry -> entry.getValue().equals(userId)) + .map(entry -> emitters.get(entry.getKey())) + .filter(Objects::nonNull) + .toList(); + } + + public Map findAllEmitterTimestamps() { + return new HashMap<>(emitterTimestamps); + } + + private String generateConnectionId(Long userId) { + return userId + "_" + UUID.randomUUID().toString().substring(0, 8); + } +} diff --git a/src/main/java/com/jiwon/mylog/domain/notification/service/NotificationService.java b/src/main/java/com/jiwon/mylog/domain/notification/service/NotificationService.java new file mode 100644 index 0000000..2fd1172 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/notification/service/NotificationService.java @@ -0,0 +1,98 @@ +package com.jiwon.mylog.domain.notification.service; + +import com.jiwon.mylog.domain.notification.dto.NotificationCountResponse; +import com.jiwon.mylog.domain.notification.dto.NotificationResponse; +import com.jiwon.mylog.domain.notification.entity.Notification; +import com.jiwon.mylog.domain.notification.repository.NotificationRepository; +import com.jiwon.mylog.domain.notification.repository.SseEmitterRepository; +import com.jiwon.mylog.global.common.entity.PageResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +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; + +@Slf4j +@RequiredArgsConstructor +@Service +public class NotificationService { + + private static final Long DEFAULT_TIMEOUT = 30 * 60 * 1000L; + + private final SseEmitterRepository emitterRepository; + private final NotificationRepository notificationRepository; + + @Transactional + public void updateNotificationRead(Long userId) { + notificationRepository.updateReadStateByReceiverId(userId); + } + + @Transactional(readOnly = true) + public PageResponse getAllNotifications(Long userId, Pageable pageable) { + Page notificationPage = notificationRepository.findAllByReceiverId(userId, pageable); + List notifications = notificationPage.stream() + .map(NotificationResponse::from) + .toList(); + + return PageResponse.from( + notifications, + notificationPage.getNumber(), + notificationPage.getSize(), + notificationPage.getTotalPages(), + notificationPage.getTotalElements() + ); + } + + @Transactional(readOnly = true) + public NotificationCountResponse countUnreadNotifications(Long userId) { + long countedNotification = notificationRepository.countByReceiverIdAndReadIsFalse(userId); + return new NotificationCountResponse(countedNotification); + } + + public SseEmitter subscribe(Long userId) { + SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); + String emitterId = emitterRepository.save(userId, emitter); + + emitter.onCompletion(() -> emitterRepository.delete(emitterId)); + emitter.onTimeout(() -> emitterRepository.delete(emitterId)); + emitter.onError((e) -> emitterRepository.delete(emitterId)); + + send(emitter, emitterId, "connect", "connect completed"); + + return emitter; + } + + public void sendNotification(Long userId, Notification notification) { + List emitterIds = emitterRepository.findEmitterIdsByUserId(userId); + + for (String emitterId : emitterIds) { + SseEmitter emitter = emitterRepository.get(emitterId).orElse(null); + if (emitter == null) { + emitterRepository.delete(emitterId); + continue; + } + + log.info("notification: {}", emitterId); + send(emitter, emitterId, "notification", notification.getContent()); + log.info("notification: {} 성공", emitterId); + } + } + + private void send(SseEmitter emitter, String emitterId, String name, String content) { + try { + emitter.send(SseEmitter.event() + .id(emitterId) + .name(name) + .data(content)); + } catch (IOException e) { + emitter.completeWithError(e); + emitterRepository.delete(emitterId); + throw new RuntimeException(); + } + } +} diff --git a/src/main/java/com/jiwon/mylog/global/common/config/SecurityConfig.java b/src/main/java/com/jiwon/mylog/global/common/config/SecurityConfig.java index 597bc38..78324be 100644 --- a/src/main/java/com/jiwon/mylog/global/common/config/SecurityConfig.java +++ b/src/main/java/com/jiwon/mylog/global/common/config/SecurityConfig.java @@ -86,9 +86,9 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtService jwtService) .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 단순 조회 (권한X) - .requestMatchers(HttpMethod.GET, "/api/users/**", "/api/posts/**", "/api/categories/**", "/api/images/**", "/api/points/**", "/api/items/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/users/**", "/api/posts/**", "/api/categories/**", "/api/images/**", "/api/points/**", "/api/items/**", "/api/sse/**").permitAll() // 블로그 사용자 - .requestMatchers("/api/users/**", "/api/posts/**", "/api/categories/**", "/api/comments/**", "/api/images/**").authenticated() + .requestMatchers("/api/users/**", "/api/posts/**", "/api/categories/**", "/api/comments/**", "/api/images/**", "/api/notifications/**").authenticated() // 관리자 전용 .requestMatchers("/api/admin/**").hasRole("ADMIN")); diff --git a/src/main/java/com/jiwon/mylog/global/common/error/ErrorCode.java b/src/main/java/com/jiwon/mylog/global/common/error/ErrorCode.java index ef918d2..27ea8f8 100644 --- a/src/main/java/com/jiwon/mylog/global/common/error/ErrorCode.java +++ b/src/main/java/com/jiwon/mylog/global/common/error/ErrorCode.java @@ -32,7 +32,8 @@ public enum ErrorCode { NOT_FOUND_CATEGORY(HttpStatus.NOT_FOUND, "존재하지 않는 카테고리입니다."), NOT_FOUND_COMMENT(HttpStatus.NOT_FOUND, "존재하지 않는 댓글입니다."), - FORBIDDEN(HttpStatus.FORBIDDEN, "게시글 작성자만 접근할 수 있습니다."); + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증되지 않은 사용자입니다."), + FORBIDDEN(HttpStatus.FORBIDDEN, "권한이 없습니다."); private final HttpStatus status; private final String message; diff --git a/src/main/java/com/jiwon/mylog/global/common/error/exception/UnauthorizedException.java b/src/main/java/com/jiwon/mylog/global/common/error/exception/UnauthorizedException.java new file mode 100644 index 0000000..d81d80d --- /dev/null +++ b/src/main/java/com/jiwon/mylog/global/common/error/exception/UnauthorizedException.java @@ -0,0 +1,9 @@ +package com.jiwon.mylog.global.common.error.exception; + +import com.jiwon.mylog.global.common.error.ErrorCode; + +public class UnauthorizedException extends CustomException { + public UnauthorizedException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/jiwon/mylog/global/schedular/EmitterCleanUpScheduler.java b/src/main/java/com/jiwon/mylog/global/schedular/EmitterCleanUpScheduler.java new file mode 100644 index 0000000..540af4a --- /dev/null +++ b/src/main/java/com/jiwon/mylog/global/schedular/EmitterCleanUpScheduler.java @@ -0,0 +1,29 @@ +package com.jiwon.mylog.global.schedular; + +import com.jiwon.mylog.domain.notification.repository.SseEmitterRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +public class EmitterCleanUpScheduler { + + private static final long TTL = 30 * 60 * 1000L; + + private final SseEmitterRepository emitterRepository; + + @Scheduled(fixedDelay = 10 * 60 * 1000L) + public void cleanUpExpiredEmitters() { + long now = System.currentTimeMillis(); + + emitterRepository.findAllEmitterTimestamps().forEach((emitterId, time) -> { + if (now - time > TTL) { + emitterRepository.delete(emitterId); + log.info("emitter time out: {}", emitterId); + } + }); + } +} \ No newline at end of file diff --git a/src/test/java/com/jiwon/mylog/TestDataFactory.java b/src/test/java/com/jiwon/mylog/TestDataFactory.java index dcef7d6..161db10 100644 --- a/src/test/java/com/jiwon/mylog/TestDataFactory.java +++ b/src/test/java/com/jiwon/mylog/TestDataFactory.java @@ -3,6 +3,8 @@ import com.jiwon.mylog.domain.category.entity.Category; import com.jiwon.mylog.domain.image.entity.ProfileImage; import com.jiwon.mylog.domain.item.entity.Item; +import com.jiwon.mylog.domain.notification.entity.Notification; +import com.jiwon.mylog.domain.notification.entity.NotificationType; import com.jiwon.mylog.domain.post.entity.Post; import com.jiwon.mylog.domain.user.entity.User; import com.jiwon.mylog.domain.user.entity.UserStatus; @@ -49,4 +51,14 @@ public static Item createItem() { .price(0) .build(); } + + public static Notification createNotification(User receiver, boolean isRead) { + return Notification.builder() + .receiver(receiver) + .content("테스트") + .url("/test") + .isRead(isRead) + .type(NotificationType.SERVER) + .build(); + } }