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
@@ -1,8 +1,12 @@
package com.ongil.backend.domain.notification.repository;

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

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 com.ongil.backend.domain.notification.entity.Notification;

Expand All @@ -13,4 +17,9 @@ public interface NotificationRepository extends JpaRepository<Notification, Long

// 해당 사용자의 읽지 않은 알림 개수 조회 (알림 아이콘 뱅지용)
long countByUserIdAndIsReadFalse(Long userId);

// 해당 사용자의 읽지 않은 알림 전체 읽음 처리 (벌크 UPDATE)
@Modifying
@Query("UPDATE Notification n SET n.isRead = true, n.readAt = :now WHERE n.user.id = :userId AND n.isRead = false")
void markAllAsRead(@Param("userId") Long userId, @Param("now") LocalDateTime now);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ongil.backend.domain.notification.service;

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

import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -51,12 +52,9 @@ public void markAsRead(Long userId, Long notificationId) {
notification.markAsRead();
}

// 모든 알림 읽음 처리
// 모든 알림 읽음 처리 (벌크 UPDATE로 N+1 방지)
@Transactional
public void markAllAsRead(Long userId) {
List<Notification> notifications = notificationRepository
.findByUserIdAndIsReadFalseOrderByNotifiedAtDesc(userId);

notifications.forEach(Notification::markAsRead);
notificationRepository.markAllAsRead(userId, LocalDateTime.now());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,19 @@ public SseEmitter subscribe(Long userId) {
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT);
emitters.put(userId, emitter);

// 연결 종료 시 제거
// 연결 종료 시 제거 (value 지정으로 새로 등록된 emitter를 잘못 제거하는 race condition 방지)
emitter.onCompletion(() -> {
emitters.remove(userId);
emitters.remove(userId, emitter);
log.info("SSE 연결 종료 - userId: {}", userId);
});

emitter.onTimeout(() -> {
emitters.remove(userId);
emitters.remove(userId, emitter);
log.info("SSE 타임아웃 - userId: {}", userId);
});

emitter.onError(e -> {
emitters.remove(userId);
emitters.remove(userId, emitter);
log.error("SSE 에러 - userId: {}", userId, e);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

Expand All @@ -53,7 +53,7 @@ public SseEmitter subscribe(Long userId) {
.name("connect")
.data("SSE 연결 성공"));
} catch (IOException e) {
emitters.remove(userId);
emitters.remove(userId, emitter);
log.error("SSE 초기 이벤트 전송 실패 - userId: {}", userId, e);
}

Expand All @@ -76,7 +76,7 @@ public void sendNotification(Long userId, NotificationResponse notification) {
.data(notification));
log.info("SSE 알림 전송 성공 - userId: {}, notificationId: {}", userId, notification.getNotificationId());
} catch (IOException e) {
emitters.remove(userId);
emitters.remove(userId, emitter);
log.error("SSE 알림 전송 실패 - userId: {}", userId, e);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.ongil.backend.domain.pricealert.service;

import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -14,6 +16,7 @@
import com.ongil.backend.domain.user.repository.UserRepository;
import com.ongil.backend.global.common.exception.EntityNotFoundException;
import com.ongil.backend.global.common.exception.ErrorCode;
import com.ongil.backend.global.common.exception.ValidationException;

import lombok.RequiredArgsConstructor;

Expand All @@ -30,9 +33,15 @@ public class PriceAlertService {
* 사용자가 상품 상세 화면에서 원하는 할인가를 선택하여 DB에 저장
* 실제 알림 발송은 PriceAlertScheduler가 주기적으로 가격을 확인하여 처리
*/
private static final List<Integer> ALLOWED_DISCOUNT_RATES = List.of(10, 20, 30, 40);

@Transactional
public PriceAlert createOrUpdatePriceAlert(Long userId, PriceAlertRequest request) {

if (!ALLOWED_DISCOUNT_RATES.contains(request.getDiscountRate())) {
throw new ValidationException(ErrorCode.INVALID_DISCOUNT_RATE);
}

User user = userRepository.findById(userId)
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.USER_NOT_FOUND));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public enum ErrorCode {

// PRICE_ALERT
PRICE_ALERT_NOT_FOUND(HttpStatus.NOT_FOUND, "설정된 가격 알림이 없습니다.", "ALERT-001"),
INVALID_DISCOUNT_RATE(HttpStatus.BAD_REQUEST, "할인율은 10, 20, 30, 40 중 하나여야 합니다.", "ALERT-002"),

// FILE / S3
FILE_IS_EMPTY(HttpStatus.BAD_REQUEST, "파일이 비어 있습니다.", "FILE-001"),
Expand Down