Skip to content

Commit 5135031

Browse files
committed
Feat: 알림 시스템 Controller + Service 구현
1 parent 1758f93 commit 5135031

File tree

7 files changed

+483
-0
lines changed

7 files changed

+483
-0
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package com.back.domain.notification.controller;
2+
3+
import com.back.domain.notification.dto.NotificationCreateRequest;
4+
import com.back.domain.notification.dto.NotificationResponse;
5+
import com.back.domain.notification.dto.NotificationListResponse;
6+
import com.back.domain.notification.entity.Notification;
7+
import com.back.domain.notification.service.NotificationService;
8+
import com.back.domain.studyroom.entity.Room;
9+
import com.back.domain.studyroom.repository.RoomRepository;
10+
import com.back.domain.user.entity.User;
11+
import com.back.domain.user.repository.UserRepository;
12+
import com.back.global.common.dto.RsData;
13+
import com.back.global.exception.CustomException;
14+
import com.back.global.exception.ErrorCode;
15+
import com.back.global.security.user.CustomUserDetails;
16+
import io.swagger.v3.oas.annotations.Operation;
17+
import io.swagger.v3.oas.annotations.Parameter;
18+
import io.swagger.v3.oas.annotations.tags.Tag;
19+
import lombok.RequiredArgsConstructor;
20+
import lombok.extern.slf4j.Slf4j;
21+
import org.springframework.data.domain.Page;
22+
import org.springframework.data.domain.PageRequest;
23+
import org.springframework.data.domain.Pageable;
24+
import org.springframework.http.ResponseEntity;
25+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
26+
import org.springframework.web.bind.annotation.*;
27+
28+
@Slf4j
29+
@RestController
30+
@RequiredArgsConstructor
31+
@RequestMapping("/api/notifications")
32+
@Tag(name = "알림", description = "알림 관련 API")
33+
public class NotificationController {
34+
35+
private final NotificationService notificationService;
36+
private final UserRepository userRepository;
37+
private final RoomRepository roomRepository;
38+
39+
@Operation(summary = "알림 전송", description = "USER/ROOM/COMMUNITY/SYSTEM 타입별 알림 생성 및 전송")
40+
@PostMapping
41+
public ResponseEntity<RsData<NotificationResponse>> createNotification(
42+
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
43+
@RequestBody NotificationCreateRequest request) {
44+
45+
log.info("알림 전송 요청 - 타입: {}, 제목: {}", request.targetType(), request.title());
46+
47+
Notification notification = switch (request.targetType()) {
48+
case "USER" -> {
49+
User targetUser = userRepository.findById(request.targetId())
50+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
51+
yield notificationService.createPersonalNotification(
52+
targetUser,
53+
request.title(),
54+
request.message(),
55+
request.redirectUrl()
56+
);
57+
}
58+
case "ROOM" -> {
59+
Room room = roomRepository.findById(request.targetId())
60+
.orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND));
61+
yield notificationService.createRoomNotification(
62+
room,
63+
request.title(),
64+
request.message(),
65+
request.redirectUrl()
66+
);
67+
}
68+
case "COMMUNITY" -> {
69+
User targetUser = userRepository.findById(request.targetId())
70+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
71+
yield notificationService.createCommunityNotification(
72+
targetUser,
73+
request.title(),
74+
request.message(),
75+
request.redirectUrl()
76+
);
77+
}
78+
case "SYSTEM" -> {
79+
yield notificationService.createSystemNotification(
80+
request.title(),
81+
request.message(),
82+
request.redirectUrl()
83+
);
84+
}
85+
default -> throw new IllegalArgumentException("유효하지 않은 알림 타입입니다: " + request.targetType());
86+
};
87+
88+
NotificationResponse response = NotificationResponse.from(notification);
89+
90+
return ResponseEntity.ok(RsData.success("알림 전송 성공", response));
91+
}
92+
93+
@Operation(summary = "알림 목록 조회", description = "사용자의 알림 목록 조회 (페이징)")
94+
@GetMapping
95+
public ResponseEntity<RsData<NotificationListResponse>> getNotifications(
96+
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
97+
@Parameter(description = "페이지 번호 (0부터 시작)") @RequestParam(defaultValue = "0") int page,
98+
@Parameter(description = "페이지 크기") @RequestParam(defaultValue = "20") int size,
99+
@Parameter(description = "읽지 않은 알림만 조회") @RequestParam(defaultValue = "false") boolean unreadOnly) {
100+
101+
log.info("알림 목록 조회 - 유저 ID: {}, 읽지 않은 것만: {}", userDetails.getUserId(), unreadOnly);
102+
103+
Pageable pageable = PageRequest.of(page, size);
104+
Page<Notification> notifications;
105+
106+
if (unreadOnly) {
107+
notifications = notificationService.getUnreadNotifications(userDetails.getUserId(), pageable);
108+
} else {
109+
notifications = notificationService.getUserNotifications(userDetails.getUserId(), pageable);
110+
}
111+
112+
long unreadCount = notificationService.getUnreadCount(userDetails.getUserId());
113+
114+
NotificationListResponse response = NotificationListResponse.from(
115+
notifications,
116+
userDetails.getUserId(),
117+
unreadCount,
118+
notificationService
119+
);
120+
121+
return ResponseEntity.ok(RsData.success("알림 목록 조회 성공", response));
122+
}
123+
124+
@Operation(summary = "알림 읽음 처리", description = "특정 알림을 읽음 상태로 변경")
125+
@PutMapping("/{notificationId}/read")
126+
public ResponseEntity<RsData<NotificationResponse>> markAsRead(
127+
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
128+
@Parameter(description = "알림 ID") @PathVariable Long notificationId) {
129+
130+
log.info("알림 읽음 처리 - 알림 ID: {}, 유저 ID: {}", notificationId, userDetails.getUserId());
131+
132+
User user = userRepository.findById(userDetails.getUserId())
133+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
134+
135+
notificationService.markAsRead(notificationId, user);
136+
137+
Notification notification = notificationService.getNotification(notificationId);
138+
NotificationResponse response = NotificationResponse.from(notification);
139+
140+
return ResponseEntity.ok(RsData.success("알림 읽음 처리 성공", response));
141+
}
142+
143+
@Operation(summary = "모든 알림 읽음 처리", description = "사용자의 읽지 않은 모든 알림을 읽음 상태로 변경")
144+
@PutMapping("/read-all")
145+
public ResponseEntity<RsData<Void>> markAllAsRead(
146+
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails) {
147+
148+
log.info("전체 알림 읽음 처리 - 유저 ID: {}", userDetails.getUserId());
149+
150+
User user = userRepository.findById(userDetails.getUserId())
151+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
152+
153+
notificationService.markMultipleAsRead(userDetails.getUserId(), user);
154+
155+
return ResponseEntity.ok(RsData.success("전체 알림 읽음 처리 성공"));
156+
}
157+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.back.domain.notification.dto;
2+
3+
public record NotificationCreateRequest(
4+
String targetType, // USER, ROOM, SYSTEM
5+
Long targetId, // nullable (SYSTEM일 때 null)
6+
String title,
7+
String message,
8+
String notificationType, // STUDY_REMINDER, ROOM_JOIN 등
9+
String redirectUrl, // targetUrl
10+
String scheduleType, // ONE_TIME (향후 확장용)
11+
String scheduledAt // 예약 시간 (향후 확장용)
12+
) {
13+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.back.domain.notification.dto;
2+
3+
import com.back.domain.notification.entity.Notification;
4+
import com.back.domain.notification.entity.NotificationType;
5+
6+
import java.time.LocalDateTime;
7+
8+
/**
9+
* 알림 목록 아이템 DTO
10+
*/
11+
public record NotificationItemDto(
12+
Long notificationId,
13+
String title,
14+
String message,
15+
NotificationType notificationType,
16+
String targetUrl,
17+
boolean isRead,
18+
LocalDateTime createdAt
19+
) {
20+
public static NotificationItemDto from(Notification notification, boolean isRead) {
21+
return new NotificationItemDto(
22+
notification.getId(),
23+
notification.getTitle(),
24+
notification.getContent(),
25+
notification.getType(),
26+
notification.getTargetUrl(),
27+
isRead,
28+
notification.getCreatedAt()
29+
);
30+
}
31+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.back.domain.notification.dto;
2+
3+
import com.back.domain.notification.entity.Notification;
4+
import com.back.domain.notification.service.NotificationService;
5+
import org.springframework.data.domain.Page;
6+
7+
import java.util.List;
8+
9+
/**
10+
* 알림 목록 응답 DTO
11+
*/
12+
public record NotificationListResponse(
13+
List<NotificationItemDto> content,
14+
PageableDto pageable,
15+
long unreadCount
16+
) {
17+
18+
// 페이지 정보 DTO
19+
public record PageableDto(
20+
int page,
21+
int size,
22+
boolean hasNext
23+
) {}
24+
25+
public static NotificationListResponse from(
26+
Page<Notification> notifications,
27+
Long userId,
28+
long unreadCount,
29+
NotificationService notificationService) {
30+
31+
List<NotificationItemDto> items = notifications.getContent().stream()
32+
.map(notification -> {
33+
boolean isRead = notificationService.isNotificationRead(notification.getId(), userId);
34+
return NotificationItemDto.from(notification, isRead);
35+
})
36+
.toList();
37+
38+
return new NotificationListResponse(
39+
items,
40+
new PageableDto(
41+
notifications.getNumber(),
42+
notifications.getSize(),
43+
notifications.hasNext()
44+
),
45+
unreadCount
46+
);
47+
}
48+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.back.domain.notification.dto;
2+
3+
import com.back.domain.notification.entity.Notification;
4+
import com.back.domain.notification.entity.NotificationType;
5+
6+
import java.time.LocalDateTime;
7+
8+
/**
9+
* 알림 응답 DTO
10+
*/
11+
public record NotificationResponse(
12+
Long notificationId,
13+
String title,
14+
String message,
15+
NotificationType notificationType,
16+
String targetUrl,
17+
boolean isRead,
18+
LocalDateTime createdAt,
19+
LocalDateTime readAt
20+
) {
21+
public static NotificationResponse from(Notification notification) {
22+
return new NotificationResponse(
23+
notification.getId(),
24+
notification.getTitle(),
25+
notification.getContent(),
26+
notification.getType(),
27+
notification.getTargetUrl(),
28+
false, // 읽음 여부는 NotificationListResponse에서 처리
29+
notification.getCreatedAt(),
30+
null // readAt은 NotificationRead에서 가져와야 함
31+
);
32+
}
33+
34+
public static NotificationResponse from(Notification notification, boolean isRead, LocalDateTime readAt) {
35+
return new NotificationResponse(
36+
notification.getId(),
37+
notification.getTitle(),
38+
notification.getContent(),
39+
notification.getType(),
40+
notification.getTargetUrl(),
41+
isRead,
42+
notification.getCreatedAt(),
43+
readAt
44+
);
45+
}
46+
}

0 commit comments

Comments
 (0)