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,17 @@
package com.ntropy.common.client;

import com.ntropy.common.dto.notification.NotificationCreateCommand;
import com.ntropy.common.dto.notification.NotificationSummary;

/**
* 알림 생성/읽음처리/삭제를 담당하는 notification-service 계약.
* create()는 다른 도메인 서비스(defense, payment, work 등)가 이벤트 발생 시 호출한다.
*/
public interface NotificationCommandClient {

NotificationSummary create(NotificationCreateCommand command);

void markAsRead(Long userId, Long notificationId);

void delete(Long userId, Long notificationId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.ntropy.common.client;

import com.ntropy.common.dto.account.PageSummary;
import com.ntropy.common.dto.notification.NotificationSummary;

/** 로그인 사용자의 알림 이력을 조회하는 notification-service 계약. */
public interface NotificationQueryClient {

PageSummary<NotificationSummary> findNotifications(Long userId, int page, int size);

long countUnread(Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.ntropy.common.dto.notification;

/**
* 다른 도메인 서비스(defense, payment, work 등)가 이벤트 발생 시 알림 생성을 요청할 때 사용하는 커맨드.
* eventId는 동일 이벤트로 인한 중복 알림 생성을 막기 위한 멱등성 키로 사용한다.
*/
public record NotificationCreateCommand(
Long userId,
String eventId,
String notificationType,
String title,
String body
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ntropy.common.dto.notification;

import java.time.LocalDateTime;

/** 외부 모듈에 노출하는 알림 정보. */
public record NotificationSummary(
Long notificationId,
String eventId,
String notificationType,
String title,
String body,
LocalDateTime readAt,
LocalDateTime createdAt
) {
}
2 changes: 1 addition & 1 deletion db/init-all.sql
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ SOURCE services/defense-service/src/main/resources/db/defense-service-schema.sql
SOURCE services/payment-service/src/main/resources/db/payment-service-schema.sql;

-- notification-service
-- SOURCE services/notification-service/src/main/resources/db/notification-service-schema.sql;
SOURCE services/notification-service/src/main/resources/db/notification-service-schema.sql;

-- bff-service (해당 없으면 스킵)
-- SOURCE services/bff-service/src/main/resources/db/bff-service-schema.sql;
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.ntropy.bff.controller.notification;

import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.ntropy.bff.dto.common.ApiResponse;
import com.ntropy.bff.dto.notification.response.NotificationsResponse;
import com.ntropy.bff.dto.notification.response.UnreadCountResponse;
import com.ntropy.bff.security.AuthenticatedUserIdResolver;
import com.ntropy.common.client.NotificationCommandClient;
import com.ntropy.common.client.NotificationQueryClient;

import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api/notifications")
@RequiredArgsConstructor
public class NotificationController {

private final NotificationQueryClient notificationQueryClient;
private final NotificationCommandClient notificationCommandClient;
private final AuthenticatedUserIdResolver authenticatedUserIdResolver;

@GetMapping
public ApiResponse<NotificationsResponse> getNotifications(@ApiParam(hidden = true) Authentication authentication,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Long userId = authenticatedUserIdResolver.resolve(authentication);
return ApiResponse.success(
NotificationsResponse.from(notificationQueryClient.findNotifications(userId, page, size)));
}

@GetMapping("/unread-count")
public ApiResponse<UnreadCountResponse> getUnreadCount(@ApiParam(hidden = true) Authentication authentication) {
Long userId = authenticatedUserIdResolver.resolve(authentication);
return ApiResponse.success(new UnreadCountResponse(notificationQueryClient.countUnread(userId)));
}

@PatchMapping("/{notificationId}/read")
public ApiResponse<Void> markAsRead(@ApiParam(hidden = true) Authentication authentication,
@PathVariable Long notificationId) {
Long userId = authenticatedUserIdResolver.resolve(authentication);
notificationCommandClient.markAsRead(userId, notificationId);
return ApiResponse.success(200, "알림을 읽음 처리했습니다.", null);
}

@DeleteMapping("/{notificationId}")
public ApiResponse<Void> delete(@ApiParam(hidden = true) Authentication authentication,
@PathVariable Long notificationId) {
Long userId = authenticatedUserIdResolver.resolve(authentication);
notificationCommandClient.delete(userId, notificationId);
return ApiResponse.success(200, "알림을 삭제했습니다.", null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.ntropy.bff.dto.notification.response;

import java.time.LocalDateTime;

import com.ntropy.common.dto.notification.NotificationSummary;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class NotificationResponse {

private Long notificationId;
private String notificationType;
private String title;
private String body;
private LocalDateTime readAt;
private LocalDateTime createdAt;

public static NotificationResponse from(NotificationSummary summary) {
return new NotificationResponse(
summary.notificationId(),
summary.notificationType(),
summary.title(),
summary.body(),
summary.readAt(),
summary.createdAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.ntropy.bff.dto.notification.response;

import java.util.List;
import java.util.stream.Collectors;

import com.ntropy.common.dto.account.PageSummary;
import com.ntropy.common.dto.notification.NotificationSummary;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class NotificationsResponse {

private List<NotificationResponse> notifications;
private int page;
private int size;
private long totalElements;
private int totalPages;
private boolean hasNext;

public static NotificationsResponse from(PageSummary<NotificationSummary> page) {
List<NotificationResponse> notifications = page.content().stream()
.map(NotificationResponse::from)
.collect(Collectors.toList());
return new NotificationsResponse(
notifications,
page.page(),
page.size(),
page.totalElements(),
page.totalPages(),
page.hasNext()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.ntropy.bff.dto.notification.response;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class UnreadCountResponse {

private long unreadCount;
}
3 changes: 3 additions & 0 deletions services/notification-service/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
dependencies {
implementation project(':common')
implementation "org.springframework:spring-webmvc:${springVersion}"
implementation "org.springframework:spring-tx:${springVersion}"
implementation 'org.mybatis:mybatis-spring:2.1.2'
implementation 'org.mybatis:mybatis:3.5.9'
implementation 'org.slf4j:slf4j-api:2.0.12'

compileOnly "org.projectlombok:lombok:${lombokVersion}"
annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.ntropy.notification.client;

import org.springframework.stereotype.Component;

import com.ntropy.common.client.NotificationCommandClient;
import com.ntropy.common.dto.notification.NotificationCreateCommand;
import com.ntropy.common.dto.notification.NotificationSummary;
import com.ntropy.notification.domain.entity.Notification;
import com.ntropy.notification.service.NotificationService;

import lombok.RequiredArgsConstructor;

/** notification-service가 구현하는 알림 생성/읽음처리/삭제 계약. */
@Component
@RequiredArgsConstructor
public class LocalNotificationCommandClient implements NotificationCommandClient {

private final NotificationService notificationService;

/** 알림 수신 동의가 꺼져 있으면 실제로 생성되지 않으므로 null을 반환할 수 있다. */
@Override
public NotificationSummary create(NotificationCreateCommand command) {
return notificationService.createNotification(
command.userId(),
command.eventId(),
command.notificationType(),
command.title(),
command.body()
)
.map(this::toSummary)
.orElse(null);
}

private NotificationSummary toSummary(Notification notification) {
return new NotificationSummary(
notification.getNotificationId(),
notification.getEventId(),
notification.getNotificationType(),
notification.getTitle(),
notification.getBody(),
notification.getReadAt(),
notification.getCreatedAt()
);
}

@Override
public void markAsRead(Long userId, Long notificationId) {
notificationService.markAsRead(userId, notificationId);
}

@Override
public void delete(Long userId, Long notificationId) {
notificationService.delete(userId, notificationId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.ntropy.notification.client;

import java.util.List;

import org.springframework.stereotype.Component;

import com.ntropy.common.client.NotificationQueryClient;
import com.ntropy.common.dto.account.PageSummary;
import com.ntropy.common.dto.notification.NotificationSummary;
import com.ntropy.notification.domain.entity.Notification;
import com.ntropy.notification.service.NotificationService;

import lombok.RequiredArgsConstructor;

/** notification-service가 구현하는 알림 조회 계약. */
@Component
@RequiredArgsConstructor
public class LocalNotificationQueryClient implements NotificationQueryClient {

private final NotificationService notificationService;

@Override
public PageSummary<NotificationSummary> findNotifications(Long userId, int page, int size) {
List<Notification> notifications = notificationService.getNotifications(userId, page, size);
long totalElements = notificationService.countNotifications(userId);

List<NotificationSummary> content = notifications.stream()
.map(this::toSummary)
.toList();

return PageSummary.of(content, page, size, totalElements);
}

@Override
public long countUnread(Long userId) {
return notificationService.countUnread(userId);
}

private NotificationSummary toSummary(Notification notification) {
return new NotificationSummary(
notification.getNotificationId(),
notification.getEventId(),
notification.getNotificationType(),
notification.getTitle(),
notification.getBody(),
notification.getReadAt(),
notification.getCreatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.ntropy.notification.domain.entity;

import java.time.LocalDateTime;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Notification {

private Long notificationId; // notification_id
private Long userId; // user_id
private String eventId; // event_id - 중복 생성 방지용 멱등성 키
private String notificationType; // notification_type
private String title; // title
private String body; // body
private LocalDateTime readAt; // read_at
private LocalDateTime createdAt; // created_at
private LocalDateTime deletedAt; // deleted_at
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.ntropy.notification.exception;

import com.ntropy.common.exception.ServiceErrorCode;

import lombok.Getter;

@Getter
public enum NotificationErrorCode implements ServiceErrorCode {

NOTIFICATION_NOT_FOUND(404, "알림을 찾을 수 없습니다."),
NOTIFICATION_ACCESS_DENIED(403, "본인의 알림만 처리할 수 있습니다."),
DUPLICATE_EVENT(409, "이미 처리된 이벤트입니다.");

private final int statusCode;
private final String message;

NotificationErrorCode(int statusCode, String message) {
this.statusCode = statusCode;
this.message = message;
}
}
Loading
Loading