Skip to content

Commit abb9ddb

Browse files
authored
#323 [Merge] v2.0.16 업데이트
1 parent 4d357ee commit abb9ddb

6 files changed

Lines changed: 90 additions & 3 deletions

File tree

‎docs/api-specs/notification-api.md‎

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,35 @@
158158

159159
---
160160

161-
### 3.2 `GET /api/v1/notifications/{notificationId}`
161+
### 3.2 `GET /api/v1/notifications/unread`
162+
163+
알림함 벨 아이콘 배지 표시용으로, 미읽음 알림 존재 여부만 가볍게 확인합니다. 목록 조회와 달리 페이지네이션 없이 boolean 하나만 반환합니다.
164+
165+
요청 헤더:
166+
167+
- `Authorization: Bearer {access_token}`
168+
169+
쿼리 파라미터:
170+
171+
| 파라미터 | 타입 | 필수 | 설명 |
172+
|---|---|---|---|
173+
| `category` | `string` | N | `ALL` \| `CONTENT` \| `NOTICE` \| `EVENT` (생략 시 전체 기준) |
174+
175+
성공 응답 `200 OK`:
176+
177+
```json
178+
{
179+
"statusCode": 200,
180+
"data": {
181+
"hasUnread": true
182+
},
183+
"error": null
184+
}
185+
```
186+
187+
---
188+
189+
### 3.3 `GET /api/v1/notifications/{notificationId}`
162190

163191
알림 상세를 조회합니다. (목록과 동일한 필드 + `readAt`)
164192

@@ -202,7 +230,7 @@
202230

203231
---
204232

205-
### 3.3 `PATCH /api/v1/notifications/{notificationId}/read`
233+
### 3.4 `PATCH /api/v1/notifications/{notificationId}/read`
206234

207235
알림 1건을 읽음 처리합니다.
208236

@@ -222,7 +250,7 @@
222250

223251
---
224252

225-
### 3.4 `PATCH /api/v1/notifications/read-all`
253+
### 3.5 `PATCH /api/v1/notifications/read-all`
226254

227255
알림함의 모든 알림을 읽음 처리합니다.
228256

‎src/main/java/com/swyp/picke/domain/notification/controller/NotificationController.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.swyp.picke.domain.notification.dto.response.NotificationDetailResponse;
44
import com.swyp.picke.domain.notification.dto.response.NotificationListResponse;
5+
import com.swyp.picke.domain.notification.dto.response.NotificationUnreadResponse;
56
import com.swyp.picke.domain.notification.enums.NotificationCategory;
67
import com.swyp.picke.domain.notification.service.NotificationService;
78
import com.swyp.picke.global.common.response.ApiResponse;
@@ -35,6 +36,15 @@ public ApiResponse<NotificationListResponse> getNotifications(
3536
return ApiResponse.onSuccess(notificationService.getNotifications(userId, category, page, size));
3637
}
3738

39+
@Operation(summary = "알림 미읽음 여부 확인 (배지용)")
40+
@GetMapping("/unread")
41+
public ApiResponse<NotificationUnreadResponse> hasUnread(
42+
@AuthenticationPrincipal Long userId,
43+
@RequestParam(required = false) NotificationCategory category
44+
) {
45+
return ApiResponse.onSuccess(notificationService.hasUnread(userId, category));
46+
}
47+
3848
@Operation(summary = "알림 상세 조회")
3949
@GetMapping("/{notificationId}")
4050
public ApiResponse<NotificationDetailResponse> getNotificationDetail(
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.swyp.picke.domain.notification.dto.response;
2+
3+
public record NotificationUnreadResponse(
4+
boolean hasUnread
5+
) {}

‎src/main/java/com/swyp/picke/domain/notification/repository/NotificationRepository.java‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ AND NOT EXISTS (
3838
""")
3939
boolean hasUnreadBroadcast(@Param("userId") Long userId, @Param("category") NotificationCategory category);
4040

41+
@Query("""
42+
SELECT CASE WHEN COUNT(n) > 0 THEN true ELSE false END
43+
FROM Notification n
44+
WHERE (:category IS NULL OR n.category = :category)
45+
AND (
46+
(n.user.id = :userId AND n.read = false)
47+
OR (n.user IS NULL AND NOT EXISTS (
48+
SELECT 1 FROM NotificationRead nr
49+
WHERE nr.notification = n AND nr.userId = :userId
50+
))
51+
)
52+
""")
53+
boolean existsUnread(@Param("userId") Long userId, @Param("category") NotificationCategory category);
54+
4155
@Modifying
4256
@Query("""
4357
UPDATE Notification n SET n.read = true, n.readAt = CURRENT_TIMESTAMP

‎src/main/java/com/swyp/picke/domain/notification/service/NotificationService.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.swyp.picke.domain.notification.dto.response.NotificationDetailResponse;
44
import com.swyp.picke.domain.notification.dto.response.NotificationListResponse;
55
import com.swyp.picke.domain.notification.dto.response.NotificationSummaryResponse;
6+
import com.swyp.picke.domain.notification.dto.response.NotificationUnreadResponse;
67
import com.swyp.picke.domain.notification.entity.Notification;
78
import com.swyp.picke.domain.notification.entity.NotificationRead;
89
import com.swyp.picke.domain.notification.enums.NotificationCategory;
@@ -146,6 +147,11 @@ public boolean hasNewBroadcast(Long userId, NotificationCategory category) {
146147
return notificationRepository.hasUnreadBroadcast(userId, category);
147148
}
148149

150+
public NotificationUnreadResponse hasUnread(Long userId, NotificationCategory category) {
151+
NotificationCategory filterCategory = (category == null || category == NotificationCategory.ALL) ? null : category;
152+
return new NotificationUnreadResponse(notificationRepository.existsUnread(userId, filterCategory));
153+
}
154+
149155
private Notification getAccessibleNotification(Long userId, Long notificationId) {
150156
Notification notification = notificationRepository.findById(notificationId)
151157
.orElseThrow(() -> new CustomException(ErrorCode.NOTIFICATION_NOT_FOUND));

‎src/test/java/com/swyp/picke/domain/notification/service/NotificationServiceTest.java‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,30 @@ void markAllAsRead_calls_repository() {
283283
verify(notificationReadRepository).markAllBroadcastAsRead(userId);
284284
}
285285

286+
@Test
287+
@DisplayName("ALL 카테고리로 미읽음 여부를 확인하면 카테고리 필터 없이 조회한다")
288+
void hasUnread_withAllCategory_queriesWithoutCategoryFilter() {
289+
Long userId = 1L;
290+
when(notificationRepository.existsUnread(userId, null)).thenReturn(true);
291+
292+
var response = notificationService.hasUnread(userId, NotificationCategory.ALL);
293+
294+
assertThat(response.hasUnread()).isTrue();
295+
verify(notificationRepository).existsUnread(userId, null);
296+
}
297+
298+
@Test
299+
@DisplayName("카테고리를 지정하면 해당 카테고리로 미읽음 여부를 확인한다")
300+
void hasUnread_withSpecificCategory_queriesWithCategoryFilter() {
301+
Long userId = 1L;
302+
when(notificationRepository.existsUnread(userId, NotificationCategory.CONTENT)).thenReturn(false);
303+
304+
var response = notificationService.hasUnread(userId, NotificationCategory.CONTENT);
305+
306+
assertThat(response.hasUnread()).isFalse();
307+
verify(notificationRepository).existsUnread(userId, NotificationCategory.CONTENT);
308+
}
309+
286310
private User createMockUser() {
287311
return User.builder()
288312
.userTag("test-user-tag")

0 commit comments

Comments
 (0)