diff --git a/src/main/java/com/ongil/backend/domain/notification/repository/NotificationRepository.java b/src/main/java/com/ongil/backend/domain/notification/repository/NotificationRepository.java index 0e8ff55..ed20f50 100644 --- a/src/main/java/com/ongil/backend/domain/notification/repository/NotificationRepository.java +++ b/src/main/java/com/ongil/backend/domain/notification/repository/NotificationRepository.java @@ -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; @@ -13,4 +17,9 @@ public interface NotificationRepository extends JpaRepository notifications = notificationRepository - .findByUserIdAndIsReadFalseOrderByNotifiedAtDesc(userId); - - notifications.forEach(Notification::markAsRead); + notificationRepository.markAllAsRead(userId, LocalDateTime.now()); } } diff --git a/src/main/java/com/ongil/backend/domain/notification/service/NotificationSseService.java b/src/main/java/com/ongil/backend/domain/notification/service/NotificationSseService.java index 5cb4a94..38f427a 100644 --- a/src/main/java/com/ongil/backend/domain/notification/service/NotificationSseService.java +++ b/src/main/java/com/ongil/backend/domain/notification/service/NotificationSseService.java @@ -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); }); @@ -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); } @@ -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); } } diff --git a/src/main/java/com/ongil/backend/domain/pricealert/service/PriceAlertService.java b/src/main/java/com/ongil/backend/domain/pricealert/service/PriceAlertService.java index a24fc13..75d8ff8 100644 --- a/src/main/java/com/ongil/backend/domain/pricealert/service/PriceAlertService.java +++ b/src/main/java/com/ongil/backend/domain/pricealert/service/PriceAlertService.java @@ -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; @@ -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; @@ -30,9 +33,15 @@ public class PriceAlertService { * 사용자가 상품 상세 화면에서 원하는 할인가를 선택하여 DB에 저장 * 실제 알림 발송은 PriceAlertScheduler가 주기적으로 가격을 확인하여 처리 */ + private static final List 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)); diff --git a/src/main/java/com/ongil/backend/global/common/exception/ErrorCode.java b/src/main/java/com/ongil/backend/global/common/exception/ErrorCode.java index a73010c..d1dc070 100644 --- a/src/main/java/com/ongil/backend/global/common/exception/ErrorCode.java +++ b/src/main/java/com/ongil/backend/global/common/exception/ErrorCode.java @@ -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"),