diff --git a/src/main/java/com/example/RealMatch/notification/application/dto/CreateNotificationCommand.java b/src/main/java/com/example/RealMatch/notification/application/dto/CreateNotificationCommand.java new file mode 100644 index 00000000..6a7f71a2 --- /dev/null +++ b/src/main/java/com/example/RealMatch/notification/application/dto/CreateNotificationCommand.java @@ -0,0 +1,21 @@ +package com.example.RealMatch.notification.application.dto; + +import com.example.RealMatch.notification.domain.entity.enums.NotificationKind; +import com.example.RealMatch.notification.domain.entity.enums.ReferenceType; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class CreateNotificationCommand { + + private final Long userId; + private final NotificationKind kind; + private final String title; + private final String body; + private final ReferenceType referenceType; + private final String referenceId; + private final Long campaignId; + private final Long proposalId; +} diff --git a/src/main/java/com/example/RealMatch/notification/application/service/NotificationQueryService.java b/src/main/java/com/example/RealMatch/notification/application/service/NotificationQueryService.java new file mode 100644 index 00000000..cce9087d --- /dev/null +++ b/src/main/java/com/example/RealMatch/notification/application/service/NotificationQueryService.java @@ -0,0 +1,99 @@ +package com.example.RealMatch.notification.application.service; + +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.RealMatch.global.exception.CustomException; +import com.example.RealMatch.notification.domain.entity.Notification; +import com.example.RealMatch.notification.domain.entity.enums.NotificationCategory; +import com.example.RealMatch.notification.domain.entity.enums.NotificationKind; +import com.example.RealMatch.notification.domain.repository.NotificationRepository; +import com.example.RealMatch.notification.exception.NotificationErrorCode; +import com.example.RealMatch.notification.presentation.dto.response.NotificationDateGroup; +import com.example.RealMatch.notification.presentation.dto.response.NotificationListResponse; +import com.example.RealMatch.notification.presentation.dto.response.NotificationResponse; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class NotificationQueryService { + + private final NotificationRepository notificationRepository; + + private static final DateTimeFormatter DATE_LABEL_FORMATTER = + DateTimeFormatter.ofPattern("yy.MM.dd (E)", Locale.KOREAN); + + public NotificationListResponse getNotifications(Long userId, String filter, int page, int size) { + PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + + List kinds = resolveKinds(filter); + + Page notificationPage; + if (kinds == null) { + notificationPage = notificationRepository.findByUserId(userId, pageRequest); + } else { + notificationPage = notificationRepository.findByUserIdAndKindIn(userId, kinds, pageRequest); + } + + List items = notificationPage.getContent().stream() + .map(NotificationResponse::from) + .toList(); + + List groups = buildDateGroups(notificationPage.getContent()); + + long unreadCount = notificationRepository.countUnreadByUserId(userId); + + return new NotificationListResponse( + items, + groups, + unreadCount, + notificationPage.getTotalElements(), + notificationPage.getTotalPages(), + notificationPage.getNumber(), + notificationPage.getSize() + ); + } + + public long getUnreadCount(Long userId) { + return notificationRepository.countUnreadByUserId(userId); + } + + private List resolveKinds(String filter) { + if (filter == null || "ALL".equalsIgnoreCase(filter)) { + return null; + } + + try { + NotificationCategory category = NotificationCategory.valueOf(filter.toUpperCase()); + return category.getKinds(); + } catch (IllegalArgumentException e) { + throw new CustomException(NotificationErrorCode.NOTIFICATION_INVALID_FILTER); + } + } + + private List buildDateGroups(List notifications) { + return notifications.stream() + .collect(Collectors.groupingBy( + n -> n.getCreatedAt().toLocalDate(), + LinkedHashMap::new, + Collectors.counting() + )) + .entrySet().stream() + .map(entry -> NotificationDateGroup.of( + entry.getKey(), + entry.getValue().intValue(), + DATE_LABEL_FORMATTER)) + .toList(); + } +} diff --git a/src/main/java/com/example/RealMatch/notification/application/service/NotificationService.java b/src/main/java/com/example/RealMatch/notification/application/service/NotificationService.java new file mode 100644 index 00000000..57f3c8bb --- /dev/null +++ b/src/main/java/com/example/RealMatch/notification/application/service/NotificationService.java @@ -0,0 +1,62 @@ +package com.example.RealMatch.notification.application.service; + +import java.util.UUID; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.RealMatch.global.exception.CustomException; +import com.example.RealMatch.notification.application.dto.CreateNotificationCommand; +import com.example.RealMatch.notification.domain.entity.Notification; +import com.example.RealMatch.notification.domain.repository.NotificationRepository; +import com.example.RealMatch.notification.exception.NotificationErrorCode; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional +public class NotificationService { + + private final NotificationRepository notificationRepository; + + public Notification create(CreateNotificationCommand command) { + Notification notification = Notification.builder() + .userId(command.getUserId()) + .kind(command.getKind()) + .title(command.getTitle()) + .body(command.getBody()) + .referenceType(command.getReferenceType()) + .referenceId(command.getReferenceId()) + .campaignId(command.getCampaignId()) + .proposalId(command.getProposalId()) + .build(); + + return notificationRepository.save(notification); + } + + public void markAsRead(Long userId, UUID notificationId) { + Notification notification = findNotificationForUser(userId, notificationId); + notification.markAsRead(); + } + + public int markAllAsRead(Long userId) { + return notificationRepository.markAllAsRead(userId); + } + + public void softDelete(Long userId, UUID notificationId) { + Notification notification = findNotificationForUser(userId, notificationId); + notification.softDelete(); + } + + private Notification findNotificationForUser(Long userId, UUID notificationId) { + Notification notification = notificationRepository.findById(notificationId) + .orElseThrow(() -> new CustomException(NotificationErrorCode.NOTIFICATION_NOT_FOUND)); + + if (!notification.getUserId().equals(userId)) { + throw new CustomException(NotificationErrorCode.NOTIFICATION_FORBIDDEN); + } + + return notification; + } +} diff --git a/src/main/java/com/example/RealMatch/notification/domain/entity/Notification.java b/src/main/java/com/example/RealMatch/notification/domain/entity/Notification.java new file mode 100644 index 00000000..57b073f7 --- /dev/null +++ b/src/main/java/com/example/RealMatch/notification/domain/entity/Notification.java @@ -0,0 +1,84 @@ +package com.example.RealMatch.notification.domain.entity; + +import java.util.UUID; + +import com.example.RealMatch.global.common.DeleteBaseEntity; +import com.example.RealMatch.notification.domain.entity.enums.NotificationKind; +import com.example.RealMatch.notification.domain.entity.enums.ReferenceType; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "notification", indexes = { + @Index(name = "idx_notification_user_read_created", columnList = "user_id, is_read, created_at"), + @Index(name = "idx_notification_user_created", columnList = "user_id, created_at"), + @Index(name = "idx_notification_user_kind", columnList = "user_id, kind") +}) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Notification extends DeleteBaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @Column(columnDefinition = "BINARY(16)") + private UUID id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Enumerated(EnumType.STRING) + @Column(name = "kind", nullable = false, length = 30) + private NotificationKind kind; + + @Column(name = "title", nullable = false) + private String title; + + @Column(name = "body", nullable = false, length = 1000) + private String body; + + @Enumerated(EnumType.STRING) + @Column(name = "reference_type", length = 30) + private ReferenceType referenceType; + + @Column(name = "reference_id", length = 36) + private String referenceId; + + @Column(name = "campaign_id") + private Long campaignId; + + @Column(name = "proposal_id") + private Long proposalId; + + @Column(name = "is_read", nullable = false) + private boolean isRead = false; + + @Builder + protected Notification(Long userId, NotificationKind kind, String title, String body, + ReferenceType referenceType, String referenceId, + Long campaignId, Long proposalId) { + this.userId = userId; + this.kind = kind; + this.title = title; + this.body = body; + this.referenceType = referenceType; + this.referenceId = referenceId; + this.campaignId = campaignId; + this.proposalId = proposalId; + } + + public void markAsRead() { + this.isRead = true; + } +} diff --git a/src/main/java/com/example/RealMatch/notification/domain/entity/enums/NotificationCategory.java b/src/main/java/com/example/RealMatch/notification/domain/entity/enums/NotificationCategory.java new file mode 100644 index 00000000..b0dce3a8 --- /dev/null +++ b/src/main/java/com/example/RealMatch/notification/domain/entity/enums/NotificationCategory.java @@ -0,0 +1,18 @@ +package com.example.RealMatch.notification.domain.entity.enums; + +import java.util.Arrays; +import java.util.List; + +public enum NotificationCategory { + + PROPOSAL, + MATCHING, + SETTLEMENT, + CHAT; + + public List getKinds() { + return Arrays.stream(NotificationKind.values()) + .filter(kind -> kind.getCategory() == this) + .toList(); + } +} diff --git a/src/main/java/com/example/RealMatch/notification/domain/entity/enums/NotificationKind.java b/src/main/java/com/example/RealMatch/notification/domain/entity/enums/NotificationKind.java new file mode 100644 index 00000000..fdc5f94a --- /dev/null +++ b/src/main/java/com/example/RealMatch/notification/domain/entity/enums/NotificationKind.java @@ -0,0 +1,30 @@ +package com.example.RealMatch.notification.domain.entity.enums; + +public enum NotificationKind { + + // 제안 관련 (PROPOSAL 카테고리) + PROPOSAL_RECEIVED(NotificationCategory.PROPOSAL), + PROPOSAL_SENT(NotificationCategory.PROPOSAL), + CAMPAIGN_APPLIED(NotificationCategory.PROPOSAL), + + // 매칭 관련 (MATCHING 카테고리) + CAMPAIGN_MATCHED(NotificationCategory.MATCHING), + AUTO_CONFIRMED(NotificationCategory.MATCHING), + + // 정산 관련 (SETTLEMENT 카테고리) + CAMPAIGN_COMPLETED(NotificationCategory.SETTLEMENT), + SETTLEMENT_READY(NotificationCategory.SETTLEMENT), + + // 채팅 (CHAT 카테고리) + CHAT_MESSAGE(NotificationCategory.CHAT); + + private final NotificationCategory category; + + NotificationKind(NotificationCategory category) { + this.category = category; + } + + public NotificationCategory getCategory() { + return category; + } +} diff --git a/src/main/java/com/example/RealMatch/notification/domain/entity/enums/ReferenceType.java b/src/main/java/com/example/RealMatch/notification/domain/entity/enums/ReferenceType.java new file mode 100644 index 00000000..fa6616b2 --- /dev/null +++ b/src/main/java/com/example/RealMatch/notification/domain/entity/enums/ReferenceType.java @@ -0,0 +1,8 @@ +package com.example.RealMatch.notification.domain.entity.enums; + +public enum ReferenceType { + + CAMPAIGN_PROPOSAL, + CAMPAIGN_APPLY, + CAMPAIGN +} diff --git a/src/main/java/com/example/RealMatch/notification/domain/repository/NotificationRepository.java b/src/main/java/com/example/RealMatch/notification/domain/repository/NotificationRepository.java new file mode 100644 index 00000000..2800446b --- /dev/null +++ b/src/main/java/com/example/RealMatch/notification/domain/repository/NotificationRepository.java @@ -0,0 +1,31 @@ +package com.example.RealMatch.notification.domain.repository; + +import java.util.Collection; +import java.util.UUID; + +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 com.example.RealMatch.notification.domain.entity.Notification; +import com.example.RealMatch.notification.domain.entity.enums.NotificationKind; + +public interface NotificationRepository extends JpaRepository { + + Page findByUserId(Long userId, Pageable pageable); + + Page findByUserIdAndKindIn(Long userId, Collection kinds, Pageable pageable); + + @Query("SELECT COUNT(n) FROM Notification n WHERE n.userId = :userId AND n.isRead = false AND n.isDeleted = false") + long countUnreadByUserId(@Param("userId") Long userId); + + /** + * 해당 유저의 미읽음 알림을 모두 읽음 처리한다 (벌크 UPDATE) + */ + @Modifying(clearAutomatically = true) + @Query("UPDATE Notification n SET n.isRead = true WHERE n.userId = :userId AND n.isRead = false AND n.isDeleted = false") + int markAllAsRead(@Param("userId") Long userId); +} diff --git a/src/main/java/com/example/RealMatch/notification/exception/NotificationErrorCode.java b/src/main/java/com/example/RealMatch/notification/exception/NotificationErrorCode.java new file mode 100644 index 00000000..95830cf5 --- /dev/null +++ b/src/main/java/com/example/RealMatch/notification/exception/NotificationErrorCode.java @@ -0,0 +1,21 @@ +package com.example.RealMatch.notification.exception; + +import org.springframework.http.HttpStatus; + +import com.example.RealMatch.global.presentation.code.BaseErrorCode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NotificationErrorCode implements BaseErrorCode { + + NOTIFICATION_INVALID_FILTER(HttpStatus.BAD_REQUEST, "NOTIFICATION_400_1", "유효하지 않은 필터 값입니다."), + NOTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "NOTIFICATION_404_1", "알림을 찾을 수 없습니다."), + NOTIFICATION_FORBIDDEN(HttpStatus.FORBIDDEN, "NOTIFICATION_403_1", "해당 알림에 대한 권한이 없습니다."); + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/example/RealMatch/notification/presentation/controller/NotificationController.java b/src/main/java/com/example/RealMatch/notification/presentation/controller/NotificationController.java new file mode 100644 index 00000000..e088c816 --- /dev/null +++ b/src/main/java/com/example/RealMatch/notification/presentation/controller/NotificationController.java @@ -0,0 +1,74 @@ +package com.example.RealMatch.notification.presentation.controller; + +import java.util.UUID; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.example.RealMatch.global.config.jwt.CustomUserDetails; +import com.example.RealMatch.global.presentation.CustomResponse; +import com.example.RealMatch.notification.application.service.NotificationQueryService; +import com.example.RealMatch.notification.application.service.NotificationService; +import com.example.RealMatch.notification.presentation.dto.response.NotificationListResponse; +import com.example.RealMatch.notification.presentation.dto.response.ReadAllResponse; +import com.example.RealMatch.notification.presentation.dto.response.UnreadCountResponse; +import com.example.RealMatch.notification.presentation.swagger.NotificationSwagger; + +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@Tag(name = "Notification", description = "알림 API") +@RestController +@RequestMapping("/api/v1/notifications") +@RequiredArgsConstructor +public class NotificationController implements NotificationSwagger { + + private final NotificationService notificationService; + private final NotificationQueryService notificationQueryService; + + @Override + @GetMapping + public CustomResponse getNotifications( + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestParam(defaultValue = "ALL") String filter, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size + ) { + NotificationListResponse response = notificationQueryService.getNotifications( + userDetails.getUserId(), filter, page, size); + return CustomResponse.ok(response); + } + + @Override + @PatchMapping("/{id}/read") + public CustomResponse markAsRead( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable UUID id + ) { + notificationService.markAsRead(userDetails.getUserId(), id); + return CustomResponse.ok(null); + } + + @Override + @PatchMapping("/read-all") + public CustomResponse markAllAsRead( + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + int updatedCount = notificationService.markAllAsRead(userDetails.getUserId()); + return CustomResponse.ok(new ReadAllResponse(updatedCount)); + } + + @Override + @GetMapping("/unread-count") + public CustomResponse getUnreadCount( + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + long count = notificationQueryService.getUnreadCount(userDetails.getUserId()); + return CustomResponse.ok(new UnreadCountResponse(count)); + } +} diff --git a/src/main/java/com/example/RealMatch/notification/presentation/dto/response/NotificationDateGroup.java b/src/main/java/com/example/RealMatch/notification/presentation/dto/response/NotificationDateGroup.java new file mode 100644 index 00000000..d7fcb553 --- /dev/null +++ b/src/main/java/com/example/RealMatch/notification/presentation/dto/response/NotificationDateGroup.java @@ -0,0 +1,18 @@ +package com.example.RealMatch.notification.presentation.dto.response; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +public record NotificationDateGroup( + LocalDate date, + String label, + int count +) { + public static NotificationDateGroup of(LocalDate date, int count, DateTimeFormatter formatter) { + return new NotificationDateGroup( + date, + date.format(formatter), + count + ); + } +} diff --git a/src/main/java/com/example/RealMatch/notification/presentation/dto/response/NotificationListResponse.java b/src/main/java/com/example/RealMatch/notification/presentation/dto/response/NotificationListResponse.java new file mode 100644 index 00000000..6552f75e --- /dev/null +++ b/src/main/java/com/example/RealMatch/notification/presentation/dto/response/NotificationListResponse.java @@ -0,0 +1,14 @@ +package com.example.RealMatch.notification.presentation.dto.response; + +import java.util.List; + +public record NotificationListResponse( + List items, + List groups, + long unreadCount, + long totalElements, + int totalPages, + int number, + int size +) { +} diff --git a/src/main/java/com/example/RealMatch/notification/presentation/dto/response/NotificationResponse.java b/src/main/java/com/example/RealMatch/notification/presentation/dto/response/NotificationResponse.java new file mode 100644 index 00000000..5559457c --- /dev/null +++ b/src/main/java/com/example/RealMatch/notification/presentation/dto/response/NotificationResponse.java @@ -0,0 +1,35 @@ +package com.example.RealMatch.notification.presentation.dto.response; + +import java.time.LocalDateTime; +import java.util.UUID; + +import com.example.RealMatch.notification.domain.entity.Notification; +import com.fasterxml.jackson.annotation.JsonProperty; + +public record NotificationResponse( + UUID id, + String kind, + String category, + String title, + String body, + Long campaignId, + Long proposalId, + @JsonProperty("isRead") Boolean isRead, + LocalDateTime createdAt, + String iconType +) { + public static NotificationResponse from(Notification notification) { + return new NotificationResponse( + notification.getId(), + notification.getKind().name(), + notification.getKind().getCategory().name(), + notification.getTitle(), + notification.getBody(), + notification.getCampaignId(), + notification.getProposalId(), + notification.isRead(), + notification.getCreatedAt(), + notification.getKind().name() + ); + } +} diff --git a/src/main/java/com/example/RealMatch/notification/presentation/dto/response/ReadAllResponse.java b/src/main/java/com/example/RealMatch/notification/presentation/dto/response/ReadAllResponse.java new file mode 100644 index 00000000..aec6bc83 --- /dev/null +++ b/src/main/java/com/example/RealMatch/notification/presentation/dto/response/ReadAllResponse.java @@ -0,0 +1,6 @@ +package com.example.RealMatch.notification.presentation.dto.response; + +public record ReadAllResponse( + int updatedCount +) { +} diff --git a/src/main/java/com/example/RealMatch/notification/presentation/dto/response/UnreadCountResponse.java b/src/main/java/com/example/RealMatch/notification/presentation/dto/response/UnreadCountResponse.java new file mode 100644 index 00000000..d9e3c8ed --- /dev/null +++ b/src/main/java/com/example/RealMatch/notification/presentation/dto/response/UnreadCountResponse.java @@ -0,0 +1,6 @@ +package com.example.RealMatch.notification.presentation.dto.response; + +public record UnreadCountResponse( + long count +) { +} diff --git a/src/main/java/com/example/RealMatch/notification/presentation/swagger/NotificationSwagger.java b/src/main/java/com/example/RealMatch/notification/presentation/swagger/NotificationSwagger.java new file mode 100644 index 00000000..cf82767a --- /dev/null +++ b/src/main/java/com/example/RealMatch/notification/presentation/swagger/NotificationSwagger.java @@ -0,0 +1,85 @@ +package com.example.RealMatch.notification.presentation.swagger; + +import java.util.UUID; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; + +import com.example.RealMatch.global.config.jwt.CustomUserDetails; +import com.example.RealMatch.global.presentation.CustomResponse; +import com.example.RealMatch.notification.presentation.dto.response.NotificationListResponse; +import com.example.RealMatch.notification.presentation.dto.response.ReadAllResponse; +import com.example.RealMatch.notification.presentation.dto.response.UnreadCountResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +public interface NotificationSwagger { + + @Operation( + summary = "알림 목록 조회 API by 여채현", + description = """ + 로그인한 유저의 알림 목록을 조회합니다. + + - filter: ALL(전체), PROPOSAL(받은 제안), MATCHING(캠페인 매칭) 중 선택. 기본값 ALL. + - 날짜별 그룹(groups)과 미읽음 개수(unreadCount)가 함께 반환됩니다. + - 최신순(createdAt DESC) 정렬, offset 기반 페이징. + """ + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "알림 목록 조회 성공") + }) + CustomResponse getNotifications( + @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestParam(defaultValue = "ALL") String filter, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size + ); + + @Operation( + summary = "알림 단건 읽음 처리 API by 여채현", + description = """ + 특정 알림을 읽음 처리합니다. + + - 본인의 알림만 읽음 처리 가능합니다. + - 이미 읽은 경우에도 멱등하게 200을 반환합니다. + """ + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "읽음 처리 성공"), + @ApiResponse(responseCode = "403", description = "NOTIFICATION_403_1 - 해당 알림에 대한 권한 없음"), + @ApiResponse(responseCode = "404", description = "NOTIFICATION_404_1 - 알림을 찾을 수 없음") + }) + CustomResponse markAsRead( + @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable UUID id + ); + + @Operation( + summary = "알림 전체 읽기 API by 여채현", + description = """ + 로그인한 유저의 미읽음 알림을 모두 읽음 처리합니다. + 벌크 UPDATE로 처리되며, 업데이트된 건수가 반환됩니다. + """ + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "전체 읽기 처리 성공") + }) + CustomResponse markAllAsRead( + @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails + ); + + @Operation( + summary = "미읽음 알림 개수 조회 API by 여채현", + description = "로그인한 유저의 미읽음 알림 총 개수를 반환합니다." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "미읽음 개수 조회 성공") + }) + CustomResponse getUnreadCount( + @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails + ); +}