Skip to content

Commit bed04c8

Browse files
authored
feat: 알림 읽음 처리 (#170)
* feat(ExceptionMessage): 알림 예외 메시지 추가 * refactor(NotificationQueryController): 컨트롤러 클래스 rename * feat(NotificationQueryService): transaction readonly 설정 * feat(NotificationIdsRequestDto): 읽음 처리할 알림 아이디 리스트 요청 DTO 추가 * feat(NotificationRepository): 알림 아이디 기준 알림 조회 추가 - 1건, N건 * feat(NotificationCommandService): 읽음 처리 추가 - 1건, N건. - 쪽지 수신자와 읽음 요청 유저 아이디 검증 추가. * feat(NotificationCommandController): 읽음 처리 엔드포인트 - 1건, N건 * test(NotificationCommandService): 읽음 처리 테스트 * test(NotificationRepository): 알림 조회 테스트 추가 * style(개행): 잠재적 git 이슈 방지 * refactor: 코드 리뷰 테스트 수정 * fix: import 문제 해결
1 parent fc09163 commit bed04c8

File tree

12 files changed

+333
-26
lines changed

12 files changed

+333
-26
lines changed

src/main/java/com/somemore/auth/controller/UserInfoQueryController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import org.springframework.web.bind.annotation.RequestMapping;
1212
import org.springframework.web.bind.annotation.RestController;
1313

14-
import static com.somemore.global.exception.ExceptionMessage.INVALID_TOKEN;
14+
import static com.somemore.auth.jwt.exception.JwtErrorType.INVALID_TOKEN;
1515

1616
@RestController
1717
@RequiredArgsConstructor
@@ -26,7 +26,7 @@ public ApiResponse<UserInfoResponseDto> getUserInfoBySCH() {
2626
String role = authentication.getAuthorities().stream()
2727
.findFirst()
2828
.map(GrantedAuthority::getAuthority)
29-
.orElseThrow(() -> new BadRequestException(INVALID_TOKEN));
29+
.orElseThrow(() -> new BadRequestException(INVALID_TOKEN.getMessage()));
3030

3131
return ApiResponse.ok(200,
3232
new UserInfoResponseDto(userId, role),

src/main/java/com/somemore/global/exception/ExceptionMessage.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
@Getter
99
public enum ExceptionMessage {
1010

11-
INVALID_TOKEN("잘못된 엑세스 토큰입니다"),
1211
NOT_EXISTS_CENTER("존재하지 않는 기관입니다."),
1312
NOT_EXISTS_COMMUNITY_BOARD("존재하지 않는 게시글입니다."),
1413
UNAUTHORIZED_COMMUNITY_BOARD("해당 게시글에 권한이 없습니다."),
@@ -34,6 +33,8 @@ public enum ExceptionMessage {
3433
DUPLICATE_APPLICATION("이미 신청한 봉사 모집 공고입니다."),
3534
UNAUTHORIZED_VOLUNTEER_APPLY("해당 지원에 권한이 없습니다."),
3635
RECRUIT_BOARD_ALREADY_COMPLETED("이미 종료된 봉사 활동입니다."),
36+
NOT_EXISTS_NOTIFICATION("존재하지 않는 알림입니다."),
37+
UNAUTHORIZED_NOTIFICATION("해당 알림에 권한이 없습니다."),
3738
VOLUNTEER_APPLY_LIST_MISMATCH("봉사 지원 목록과 요청된 봉사 지원 목록이 일치하지 않습니다."),
3839
RECRUIT_BOARD_ID_MISMATCH("모든 봉사 신청이 동일한 모집글 ID를 가져야 합니다."),
3940

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.somemore.notification.controller;
2+
3+
import com.somemore.auth.annotation.CurrentUser;
4+
import com.somemore.global.common.response.ApiResponse;
5+
import com.somemore.notification.dto.NotificationIdsRequestDto;
6+
import com.somemore.notification.usecase.NotificationCommandUseCase;
7+
import io.swagger.v3.oas.annotations.Operation;
8+
import io.swagger.v3.oas.annotations.tags.Tag;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.security.access.annotation.Secured;
11+
import org.springframework.web.bind.annotation.PatchMapping;
12+
import org.springframework.web.bind.annotation.PathVariable;
13+
import org.springframework.web.bind.annotation.PostMapping;
14+
import org.springframework.web.bind.annotation.RequestBody;
15+
import org.springframework.web.bind.annotation.RequestMapping;
16+
import org.springframework.web.bind.annotation.RestController;
17+
18+
import java.util.UUID;
19+
20+
@Tag(name = "Notification Command API", description = "알림 읽음 처리 API")
21+
@RequiredArgsConstructor
22+
@RequestMapping("/api/notification")
23+
@RestController
24+
public class NotificationCommandController {
25+
26+
private final NotificationCommandUseCase notificationCommandUseCase;
27+
28+
@Secured({"ROLE_VOLUNTEER", "ROLE_CENTER"})
29+
@Operation(summary = "알림(1개) 읽음 처리", description = "알림 1개를 읽음 처리합니다.")
30+
@PatchMapping("/read/{notificationId}")
31+
public ApiResponse<String> markSingleNotification(
32+
@CurrentUser UUID userId,
33+
@PathVariable Long notificationId
34+
) {
35+
notificationCommandUseCase.markSingleNotificationAsRead(userId, notificationId);
36+
return ApiResponse.ok("알림 1개 읽음 처리 성공");
37+
}
38+
39+
@Secured({"ROLE_VOLUNTEER", "ROLE_CENTER"})
40+
@Operation(summary = "알림(N개) 읽음 처리", description = "알림 N개를 읽음 처리합니다.")
41+
@PostMapping("/read/multiple")
42+
public ApiResponse<String> markMultipleNotifications(
43+
@CurrentUser UUID userId,
44+
@RequestBody NotificationIdsRequestDto notificationIds
45+
) {
46+
notificationCommandUseCase.markMultipleNotificationsAsRead(userId, notificationIds);
47+
return ApiResponse.ok("알림 N개 읽음 처리 성공");
48+
}
49+
}

src/main/java/com/somemore/notification/controller/NotificationController.java renamed to src/main/java/com/somemore/notification/controller/NotificationQueryController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
import java.util.List;
1616
import java.util.UUID;
1717

18-
@Tag(name = "Notification API", description = "알림 API")
18+
@Tag(name = "Notification Query API", description = "알림 조회 API")
1919
@RequiredArgsConstructor
2020
@RequestMapping("/api/notification")
2121
@RestController
22-
public class NotificationController {
22+
public class NotificationQueryController {
2323

2424
private final NotificationQueryUseCase notificationQueryUseCase;
2525

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.somemore.notification.dto;
2+
3+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
4+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import lombok.Builder;
7+
8+
import java.util.List;
9+
10+
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
11+
@Builder
12+
public record NotificationIdsRequestDto(
13+
14+
@Schema(description = "읽음 처리할 알림 아이디 리스트",
15+
example = "[1, 2, 3, 4, 5]")
16+
List<Long> ids
17+
) {
18+
}

src/main/java/com/somemore/notification/repository/NotificationRepository.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@
44
import org.springframework.stereotype.Repository;
55

66
import java.util.List;
7+
import java.util.Optional;
78
import java.util.UUID;
89

910
@Repository
1011
public interface NotificationRepository {
1112

1213
Notification save(Notification notification);
1314

15+
Optional<Notification> findById(Long id);
16+
17+
List<Notification> findAllByIds(List<Long> ids);
18+
1419
List<Notification> findByReceiverIdAndUnread(UUID userId);
1520
List<Notification> findByReceiverIdAndRead(UUID userId);
21+
22+
void deleteAllInBatch();
1623
}

src/main/java/com/somemore/notification/repository/NotificationRepositoryImpl.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.springframework.stereotype.Repository;
99

1010
import java.util.List;
11+
import java.util.Optional;
1112
import java.util.UUID;
1213

1314
@Repository
@@ -24,6 +25,16 @@ public Notification save(Notification notification) {
2425
return notificationJpaRepository.save(notification);
2526
}
2627

28+
@Override
29+
public Optional<Notification> findById(Long id) {
30+
return notificationJpaRepository.findById(id);
31+
}
32+
33+
@Override
34+
public List<Notification> findAllByIds(List<Long> ids) {
35+
return notificationJpaRepository.findAllById(ids);
36+
}
37+
2738
@Override
2839
public List<Notification> findByReceiverIdAndUnread(UUID receiverId) {
2940
return queryFactory.selectFrom(notification)
@@ -40,6 +51,11 @@ public List<Notification> findByReceiverIdAndRead(UUID receiverId) {
4051
.fetch();
4152
}
4253

54+
@Override
55+
public void deleteAllInBatch() {
56+
notificationJpaRepository.deleteAllInBatch();
57+
}
58+
4359
private static BooleanExpression eqReceiverId(UUID userId) {
4460
return notification.receiverId.eq(userId);
4561
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.somemore.notification.service;
2+
3+
import com.somemore.global.exception.BadRequestException;
4+
import com.somemore.notification.domain.Notification;
5+
import com.somemore.notification.dto.NotificationIdsRequestDto;
6+
import com.somemore.notification.repository.NotificationRepository;
7+
import com.somemore.notification.usecase.NotificationCommandUseCase;
8+
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.stereotype.Service;
11+
import org.springframework.transaction.annotation.Transactional;
12+
13+
import java.util.List;
14+
import java.util.UUID;
15+
16+
import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_NOTIFICATION;
17+
import static com.somemore.global.exception.ExceptionMessage.UNAUTHORIZED_NOTIFICATION;
18+
19+
@Slf4j
20+
@Service
21+
@RequiredArgsConstructor
22+
@Transactional
23+
public class NotificationCommandService implements NotificationCommandUseCase {
24+
25+
private final NotificationRepository notificationRepository;
26+
27+
@Override
28+
public void markSingleNotificationAsRead(UUID userId, Long notificationId) {
29+
Notification notification = notificationRepository.findById(notificationId)
30+
.orElseThrow(() -> new BadRequestException(NOT_EXISTS_NOTIFICATION));
31+
32+
validateNotificationOwnership(userId, notification.getReceiverId());
33+
34+
notification.markAsRead();
35+
}
36+
37+
@Override
38+
public void markMultipleNotificationsAsRead(UUID userId, NotificationIdsRequestDto notificationIds) {
39+
List<Notification> notifications = notificationRepository.findAllByIds(notificationIds.ids());
40+
41+
notifications.forEach(notification ->
42+
validateNotificationOwnership(userId, notification.getReceiverId()));
43+
44+
notifications.forEach(Notification::markAsRead);
45+
}
46+
47+
private void validateNotificationOwnership(UUID userId, UUID receiverId) {
48+
if (!receiverId.equals(userId)) {
49+
throw new BadRequestException(UNAUTHORIZED_NOTIFICATION);
50+
}
51+
}
52+
53+
}

src/main/java/com/somemore/notification/service/NotificationQueryService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
import lombok.RequiredArgsConstructor;
88
import lombok.extern.slf4j.Slf4j;
99
import org.springframework.stereotype.Service;
10+
import org.springframework.transaction.annotation.Transactional;
1011

1112
import java.util.List;
1213
import java.util.UUID;
1314

1415
@Slf4j
1516
@Service
1617
@RequiredArgsConstructor
18+
@Transactional(readOnly = true)
1719
public class NotificationQueryService implements NotificationQueryUseCase {
1820

1921
private final NotificationRepository notificationRepository;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.somemore.notification.usecase;
2+
3+
import com.somemore.notification.dto.NotificationIdsRequestDto;
4+
5+
import java.util.UUID;
6+
7+
public interface NotificationCommandUseCase {
8+
9+
void markSingleNotificationAsRead(UUID userId, Long notificationId);
10+
void markMultipleNotificationsAsRead(UUID userId, NotificationIdsRequestDto notificationIds);
11+
}

0 commit comments

Comments
 (0)