Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.back.domain.notification.controller;

import com.back.domain.notification.dto.NotificationGoResponseDto;
import com.back.domain.notification.dto.NotificationListResponseDto;
import com.back.domain.notification.service.NotificationService;
import com.back.global.rsData.RsData;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;

@RestController
@RequestMapping("/api/me")
@RequiredArgsConstructor
@Validated
public class NotificationController {

private final NotificationService notificationService;

@GetMapping("/notifications")
public RsData<NotificationListResponseDto> getNotifications(
@AuthenticationPrincipal(expression = "id") Long userId,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime lastCreatedAt,
@RequestParam(required = false) Long lastId,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int limit
) {
NotificationListResponseDto body = notificationService.getNotifications(userId, lastCreatedAt, lastId, limit);
return RsData.successOf(body);
}

@PostMapping("/notifications/{id}")
public RsData<NotificationGoResponseDto> goPostLink(
@AuthenticationPrincipal(expression = "id") Long userId,
@PathVariable("id") Long notificationId
) {
var body = notificationService.markAsReadAndGetPostLink(userId, notificationId);
return RsData.successOf(body);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.back.domain.notification.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class NotificationGoResponseDto {
private Long postId;
private String postApiUrl;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.back.domain.notification.dto;

import com.back.domain.notification.entity.Notification;
import com.back.domain.notification.enums.NotificationType;
import java.time.LocalDateTime;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class NotificationItemDto {
private Long id;
private NotificationType type;
private Long postId;
private String postTitle;
private boolean read;
private LocalDateTime createdAt;

public static NotificationItemDto from(Notification n) {
return NotificationItemDto.builder()
.id(n.getId())
.type(n.getType())
.postId(n.getPost().getId())
.postTitle(n.getPost().getTitle())
.read(n.isRead())
.createdAt(n.getCreatedAt())
.build();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.back.domain.notification.dto;

import java.time.LocalDateTime;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class NotificationListResponseDto {
private List<NotificationItemDto> items;
private boolean hasNext;
private LocalDateTime nextCreatedAt;
private Long nextId;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.back.domain.notification.entity;

import com.back.domain.notification.enums.NotificationType;
import com.back.domain.post.post.entity.Post;
import com.back.domain.user.entity.User;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@EntityListeners(AuditingEntityListener.class)
public class Notification {

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

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;

@Enumerated(EnumType.STRING)
@Column(name = "type", nullable = false, length = 20)
private NotificationType type;

@Builder.Default
@Column(name = "is_read", nullable = false) //read는 DB 예약어 충돌 가능성 있어 @Column(name = "is_read") 설정
private boolean read = false;

@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;

public void markRead() {
this.read = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.back.domain.notification.enums;

public enum NotificationType {
COMMENT,
LIKE
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.back.domain.notification.repository;

import com.back.domain.notification.entity.Notification;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;

@Repository
public interface NotificationRepository extends JpaRepository<Notification, Long> {

@Query("""
select n from Notification n
where n.user.id = :userId
order by n.createdAt desc, n.id desc
""")
List<Notification> findMyNotificationsFirstPage(@Param("userId") Long userId,
Pageable pageable);

@Query("""
select n from Notification n
where n.user.id = :userId
and (n.createdAt < :lastCreatedAt or (n.createdAt = :lastCreatedAt and n.id < :lastId))
order by n.createdAt desc, n.id desc
""")
List<Notification> findMyNotificationsAfter(@Param("userId") Long userId,
@Param("lastCreatedAt") LocalDateTime lastCreatedAt,
@Param("lastId") Long lastId,
Pageable pageable);

@Query("""
select n from Notification n
where n.id = :id and n.user.id = :userId
""")
Notification findByIdAndUserId(@Param("id") Long id, @Param("userId") Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.back.domain.notification.service;

import com.back.domain.notification.dto.NotificationGoResponseDto;
import com.back.domain.notification.dto.NotificationItemDto;
import com.back.domain.notification.dto.NotificationListResponseDto;
import com.back.domain.notification.entity.Notification;
import com.back.domain.notification.repository.NotificationRepository;
import com.back.global.exception.ServiceException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class NotificationService {

private final NotificationRepository notificationRepository;

@Transactional(readOnly = true)
public NotificationListResponseDto getNotifications(Long userId, LocalDateTime lastCreatedAt, Long lastId, int limit) {
int safeLimit = Math.max(1, Math.min(limit, 100));
int fetchSize = safeLimit + 1;

List<Notification> rows;
if (lastCreatedAt == null || lastId == null) {
rows = notificationRepository.findMyNotificationsFirstPage(userId, PageRequest.of(0, fetchSize));
} else {
rows = notificationRepository.findMyNotificationsAfter(userId, lastCreatedAt, lastId, PageRequest.of(0, fetchSize));
}

boolean hasNext = rows.size() > safeLimit;
if (hasNext) rows = rows.subList(0, safeLimit);

List<NotificationItemDto> items = new ArrayList<>();
for (Notification n : rows) items.add(NotificationItemDto.from(n));

LocalDateTime nextCreatedAt = null;
Long nextId = null;
if (hasNext && !rows.isEmpty()) {
Notification last = rows.get(rows.size() - 1);
nextCreatedAt = last.getCreatedAt();
nextId = last.getId();
}

return new NotificationListResponseDto(items, hasNext, nextCreatedAt, nextId);
}

@Transactional
public NotificationGoResponseDto markAsReadAndGetPostLink(Long userId, Long notificationId) {
Notification notification = notificationRepository.findByIdAndUserId(notificationId, userId);
if (notification == null) {
throw new ServiceException(404, "알림을 찾을 수 없습니다.");
}
if (!notification.isRead()) {
notification.markRead();
}
Long postId = notification.getPost().getId();
String apiUrl = "/api/posts/" + postId;
return new NotificationGoResponseDto(postId, apiUrl);
}
}