Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.back.domain.notification.controller;

import com.back.domain.notification.dto.NotificationCreateRequest;
import com.back.domain.notification.dto.NotificationResponse;
import com.back.domain.notification.dto.NotificationListResponse;
import com.back.domain.notification.entity.Notification;
import com.back.domain.notification.service.NotificationService;
import com.back.domain.studyroom.entity.Room;
import com.back.domain.studyroom.repository.RoomRepository;
import com.back.domain.user.entity.User;
import com.back.domain.user.repository.UserRepository;
import com.back.global.common.dto.RsData;
import com.back.global.exception.CustomException;
import com.back.global.exception.ErrorCode;
import com.back.global.security.user.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/notifications")
@Tag(name = "알림", description = "알림 관련 API")
public class NotificationController {

private final NotificationService notificationService;
private final UserRepository userRepository;
private final RoomRepository roomRepository;

@Operation(summary = "알림 전송", description = "USER/ROOM/COMMUNITY/SYSTEM 타입별 알림 생성 및 전송")
@PostMapping
public ResponseEntity<RsData<NotificationResponse>> createNotification(
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
@RequestBody NotificationCreateRequest request) {

log.info("알림 전송 요청 - 타입: {}, 제목: {}", request.targetType(), request.title());

Notification notification = switch (request.targetType()) {
case "USER" -> {
User targetUser = userRepository.findById(request.targetId())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
yield notificationService.createPersonalNotification(
targetUser,
request.title(),
request.message(),
request.redirectUrl()
);
}
case "ROOM" -> {
Room room = roomRepository.findById(request.targetId())
.orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND));
yield notificationService.createRoomNotification(
room,
request.title(),
request.message(),
request.redirectUrl()
);
}
case "COMMUNITY" -> {
User targetUser = userRepository.findById(request.targetId())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
yield notificationService.createCommunityNotification(
targetUser,
request.title(),
request.message(),
request.redirectUrl()
);
}
case "SYSTEM" -> {
yield notificationService.createSystemNotification(
request.title(),
request.message(),
request.redirectUrl()
);
}
default -> throw new IllegalArgumentException("유효하지 않은 알림 타입입니다: " + request.targetType());
};

NotificationResponse response = NotificationResponse.from(notification);

return ResponseEntity.ok(RsData.success("알림 전송 성공", response));
}

@Operation(summary = "알림 목록 조회", description = "사용자의 알림 목록 조회 (페이징)")
@GetMapping
public ResponseEntity<RsData<NotificationListResponse>> getNotifications(
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
@Parameter(description = "페이지 번호 (0부터 시작)") @RequestParam(defaultValue = "0") int page,
@Parameter(description = "페이지 크기") @RequestParam(defaultValue = "20") int size,
@Parameter(description = "읽지 않은 알림만 조회") @RequestParam(defaultValue = "false") boolean unreadOnly) {

log.info("알림 목록 조회 - 유저 ID: {}, 읽지 않은 것만: {}", userDetails.getUserId(), unreadOnly);

Pageable pageable = PageRequest.of(page, size);
Page<Notification> notifications;

if (unreadOnly) {
notifications = notificationService.getUnreadNotifications(userDetails.getUserId(), pageable);
} else {
notifications = notificationService.getUserNotifications(userDetails.getUserId(), pageable);
}

long unreadCount = notificationService.getUnreadCount(userDetails.getUserId());

NotificationListResponse response = NotificationListResponse.from(
notifications,
userDetails.getUserId(),
unreadCount,
notificationService
);

return ResponseEntity.ok(RsData.success("알림 목록 조회 성공", response));
}

@Operation(summary = "알림 읽음 처리", description = "특정 알림을 읽음 상태로 변경")
@PutMapping("/{notificationId}/read")
public ResponseEntity<RsData<NotificationResponse>> markAsRead(
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
@Parameter(description = "알림 ID") @PathVariable Long notificationId) {

log.info("알림 읽음 처리 - 알림 ID: {}, 유저 ID: {}", notificationId, userDetails.getUserId());

User user = userRepository.findById(userDetails.getUserId())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

notificationService.markAsRead(notificationId, user);

Notification notification = notificationService.getNotification(notificationId);
NotificationResponse response = NotificationResponse.from(notification);

return ResponseEntity.ok(RsData.success("알림 읽음 처리 성공", response));
}

@Operation(summary = "모든 알림 읽음 처리", description = "사용자의 읽지 않은 모든 알림을 읽음 상태로 변경")
@PutMapping("/read-all")
public ResponseEntity<RsData<Void>> markAllAsRead(
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails) {

log.info("전체 알림 읽음 처리 - 유저 ID: {}", userDetails.getUserId());

User user = userRepository.findById(userDetails.getUserId())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

notificationService.markMultipleAsRead(userDetails.getUserId(), user);

return ResponseEntity.ok(RsData.success("전체 알림 읽음 처리 성공"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.back.domain.notification.dto;

public record NotificationCreateRequest(
String targetType, // USER, ROOM, SYSTEM
Long targetId, // nullable (SYSTEM일 때 null)
String title,
String message,
String notificationType, // STUDY_REMINDER, ROOM_JOIN 등
String redirectUrl, // targetUrl
String scheduleType, // ONE_TIME (향후 확장용)
String scheduledAt // 예약 시간 (향후 확장용)
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.back.domain.notification.dto;

import com.back.domain.notification.entity.Notification;
import com.back.domain.notification.entity.NotificationType;

import java.time.LocalDateTime;

/**
* 알림 목록 아이템 DTO
*/
public record NotificationItemDto(
Long notificationId,
String title,
String message,
NotificationType notificationType,
String targetUrl,
boolean isRead,
LocalDateTime createdAt
) {
public static NotificationItemDto from(Notification notification, boolean isRead) {
return new NotificationItemDto(
notification.getId(),
notification.getTitle(),
notification.getContent(),
notification.getType(),
notification.getTargetUrl(),
isRead,
notification.getCreatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.back.domain.notification.dto;

import com.back.domain.notification.entity.Notification;
import com.back.domain.notification.service.NotificationService;
import org.springframework.data.domain.Page;

import java.util.List;

/**
* 알림 목록 응답 DTO
*/
public record NotificationListResponse(
List<NotificationItemDto> content,
PageableDto pageable,
long unreadCount
) {

// 페이지 정보 DTO
public record PageableDto(
int page,
int size,
boolean hasNext
) {}

public static NotificationListResponse from(
Page<Notification> notifications,
Long userId,
long unreadCount,
NotificationService notificationService) {

List<NotificationItemDto> items = notifications.getContent().stream()
.map(notification -> {
boolean isRead = notificationService.isNotificationRead(notification.getId(), userId);
return NotificationItemDto.from(notification, isRead);
})
.toList();

return new NotificationListResponse(
items,
new PageableDto(
notifications.getNumber(),
notifications.getSize(),
notifications.hasNext()
),
unreadCount
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.back.domain.notification.dto;

import com.back.domain.notification.entity.Notification;
import com.back.domain.notification.entity.NotificationType;

import java.time.LocalDateTime;

/**
* 알림 응답 DTO
*/
public record NotificationResponse(
Long notificationId,
String title,
String message,
NotificationType notificationType,
String targetUrl,
boolean isRead,
LocalDateTime createdAt,
LocalDateTime readAt
) {
public static NotificationResponse from(Notification notification) {
return new NotificationResponse(
notification.getId(),
notification.getTitle(),
notification.getContent(),
notification.getType(),
notification.getTargetUrl(),
false, // 읽음 여부는 NotificationListResponse에서 처리
notification.getCreatedAt(),
null // readAt은 NotificationRead에서 가져와야 함
);
}

public static NotificationResponse from(Notification notification, boolean isRead, LocalDateTime readAt) {
return new NotificationResponse(
notification.getId(),
notification.getTitle(),
notification.getContent(),
notification.getType(),
notification.getTargetUrl(),
isRead,
notification.getCreatedAt(),
readAt
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.back.domain.notification.dto;

import com.back.domain.notification.entity.NotificationType;

import java.time.LocalDateTime;

public record NotificationWebSocketDto(
Long notificationId,
String title,
String message,
NotificationType notificationType,
String targetUrl,
LocalDateTime createdAt
) {

public static NotificationWebSocketDto from(
Long notificationId,
String title,
String content,
NotificationType type,
String targetUrl,
LocalDateTime createdAt) {

return new NotificationWebSocketDto(
notificationId,
title,
content,
type,
targetUrl,
createdAt
);
}
}
Loading