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
Expand Up @@ -100,7 +100,7 @@ public enum ErrorStatus implements BaseErrorCode {
_NOTIFICATION_PAYLOAD_MISSING_STUDY_ID(400, "NOTIFICATION4002", "알림 페이로드에 studyId가 필요합니다."),
_NOTIFICATION_TEMPLATE_NOT_FOUND(500, "NOTIFICATION5000", "알림 템플릿을 찾을 수 없습니다."),
_PUSH_NOTIFICATION_FAILED(500, "NOTIFICATION5001", "푸시 알림 발송에 실패했습니다."),
;
_NOTIFICATION_ACCESS_DENIED(403, "NOTIFICATION403", "해당 알림에 접근할 권한이 없습니다.");

private final int httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package kr.spot.application.command;

import kr.spot.domain.Notification;
import kr.spot.infrastructure.jpa.NotificationRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class NotificationCommandService {

private final NotificationRepository notificationRepository;

public void markAllAsRead(long memberId, long notificationId) {
Notification notification = notificationRepository.getById(notificationId);
notification.markAsRead(memberId);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import kr.spot.domain.Notification;
import kr.spot.infrastructure.jpa.NotificationRepository;
import kr.spot.presentation.query.dto.GetNotificationListResponse;
import kr.spot.presentation.query.dto.GetUnreadNotificationResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -20,4 +21,9 @@ public GetNotificationListResponse getMyNotifications(long memberId) {
.findByMemberIdOrderByCreatedAtDesc(memberId);
return GetNotificationListResponse.from(notifications);
}

public GetUnreadNotificationResponse hasUnreadNotifications(long memberId) {
boolean hasUnread = notificationRepository.existsByMemberIdAndIsReadFalse(memberId);
return new GetUnreadNotificationResponse(hasUnread);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import kr.spot.code.status.ErrorStatus;
import kr.spot.domain.enums.NotificationStatus;
import kr.spot.domain.enums.NotificationType;
import kr.spot.exception.GeneralException;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand Down Expand Up @@ -161,7 +163,10 @@ public boolean canRetry() {
return retryCount < maxRetry;
}

public void markAsRead() {
public void markAsRead(long memberId) {
if (memberId != this.memberId) {
throw new GeneralException(ErrorStatus._NOTIFICATION_ACCESS_DENIED);
}
this.isRead = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import java.time.LocalDateTime;
import java.util.List;
import kr.spot.code.status.ErrorStatus;
import kr.spot.domain.Notification;
import kr.spot.domain.enums.NotificationStatus;
import kr.spot.exception.GeneralException;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
Expand All @@ -15,6 +17,8 @@ public interface NotificationRepository extends JpaRepository<Notification, Long

List<Notification> findByMemberIdOrderByCreatedAtDesc(long memberId);

boolean existsByMemberIdAndIsReadFalse(long memberId);

/**
* 발송 대상 알림 선점 (FOR UPDATE SKIP LOCKED 사용) MySQL 8.0+에서 지원
*/
Expand Down Expand Up @@ -46,4 +50,9 @@ int pickPendingNotifications(
* 특정 서버가 선점한 알림 목록 조회
*/
List<Notification> findByPickedByAndDispatchStatus(String serverId, NotificationStatus status);

default Notification getById(long id) {
return findById(id).orElseThrow(
() -> new GeneralException(ErrorStatus._NOTIFICATION_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package kr.spot.presentation.command;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import kr.spot.ApiResponse;
import kr.spot.annotations.CurrentMember;
import kr.spot.application.command.NotificationCommandService;
import kr.spot.code.status.SuccessStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "알림")
@RestController
@RequestMapping("/api/notifications")
@RequiredArgsConstructor
public class NotificationCommandController {

private final NotificationCommandService notificationCommandService;

@Operation(summary = "알림 읽음 처리", description = "특정 알림을 읽음 처리합니다.")
@PostMapping("/{notificationId}/read")
public ResponseEntity<ApiResponse<Void>> markAsRead(
@PathVariable Long notificationId,
@CurrentMember @Parameter(hidden = true) Long memberId
) {
notificationCommandService.markAllAsRead(memberId, notificationId);
return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import kr.spot.application.query.GetNotificationService;
import kr.spot.code.status.SuccessStatus;
import kr.spot.presentation.query.dto.GetNotificationListResponse;
import kr.spot.presentation.query.dto.GetUnreadNotificationResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -37,4 +38,13 @@ public ResponseEntity<ApiResponse<GetNotificationListResponse>> getMyNotificatio
GetNotificationListResponse response = getNotificationService.getMyNotifications(memberId);
return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, response));
}

@Operation(summary = "읽지 않은 알림 존재 여부 조회", description = "로그인한 사용자의 읽지 않은 알림이 있는지 여부를 조회합니다.")
@GetMapping("/me/unread")
public ResponseEntity<ApiResponse<GetUnreadNotificationResponse>> hasUnreadNotifications(
@CurrentMember @Parameter(hidden = true) Long memberId) {
GetUnreadNotificationResponse response = getNotificationService.hasUnreadNotifications(
memberId);
return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, response));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kr.spot.presentation.query.dto;

public record GetUnreadNotificationResponse(
boolean hasUnreadNotifications
) {

public static GetUnreadNotificationResponse of(boolean hasUnreadNotifications) {
return new GetUnreadNotificationResponse(hasUnreadNotifications);
}
}