From ca3a2c664b0e6d4bb83f05769a77bb998a15a9d1 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sat, 4 Apr 2026 03:29:26 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Refactor:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=EC=A1=B0=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/application/FcmSender.java | 10 ++++ .../{service => }/FcmTokenService.java | 19 +++---- .../NotificationCommandService.java | 56 +++++-------------- .../NotificationQueryService.java | 25 ++++----- .../dto/command/NotificationListCommand.java | 16 ++++++ .../dto/request/NotificationListRequest.java | 24 -------- .../application/dto/result/FcmSendResult.java | 9 +++ .../NotificationResult.java} | 12 ++-- .../application/dto/result/PagedResult.java | 14 +++++ .../application/port/FcmSender.java | 15 ----- .../common/response/CursorPagingResponse.java | 21 ------- .../common}/BizException.java | 2 +- .../common}/CommonErrorCode.java | 2 +- .../common}/ErrorCode.java | 2 +- .../domain/fcmtoken/FcmTokenRepository.java | 7 ++- .../domain/notification/Notification.java | 4 +- .../notification}/NotificationErrorCode.java | 3 +- .../infrastructure/config/AppConfig.java | 25 +++++++++ .../infrastructure/config/SwaggerConfig.java | 2 +- .../infrastructure/fcm/FirebaseFcmSender.java | 45 ++++++++++++--- .../messaging}/GroupInviteMessage.java | 2 +- .../messaging/GroupInviteMessageListener.java | 3 +- .../messaging}/GroupJoinRequestMessage.java | 2 +- .../GroupJoinRequestMessageListener.java | 3 +- .../JpaAuditingConfig.java | 2 +- .../NotificationController.java | 28 +++++----- .../NotificationControllerDocs.java | 12 ++-- .../http/common}/ApiResponse.java | 4 +- .../http/common}/ApiResponseAdvice.java | 2 +- .../http/common}/CursorPagingRequest.java | 2 +- .../http/common/CursorPagingResponse.java | 24 ++++++++ .../http/common}/GlobalExceptionHandler.java | 16 ++++-- .../http}/common/HttpHeaders.java | 2 +- .../dto/request/NotificationListRequest.java | 21 +++++++ .../dto/request/TokenRegisterRequest.java | 2 +- .../interfaces/http/dto/response/.gitkeep | 0 36 files changed, 251 insertions(+), 187 deletions(-) create mode 100644 src/main/java/flipnote/notification/application/FcmSender.java rename src/main/java/flipnote/notification/application/{service => }/FcmTokenService.java (63%) rename src/main/java/flipnote/notification/application/{service => }/NotificationCommandService.java (63%) rename src/main/java/flipnote/notification/application/{service => }/NotificationQueryService.java (58%) create mode 100644 src/main/java/flipnote/notification/application/dto/command/NotificationListCommand.java delete mode 100644 src/main/java/flipnote/notification/application/dto/request/NotificationListRequest.java create mode 100644 src/main/java/flipnote/notification/application/dto/result/FcmSendResult.java rename src/main/java/flipnote/notification/application/dto/{response/NotificationResponse.java => result/NotificationResult.java} (63%) create mode 100644 src/main/java/flipnote/notification/application/dto/result/PagedResult.java delete mode 100644 src/main/java/flipnote/notification/application/port/FcmSender.java delete mode 100644 src/main/java/flipnote/notification/common/response/CursorPagingResponse.java rename src/main/java/flipnote/notification/{common/exception => domain/common}/BizException.java (78%) rename src/main/java/flipnote/notification/{common/exception => domain/common}/CommonErrorCode.java (91%) rename src/main/java/flipnote/notification/{common/exception => domain/common}/ErrorCode.java (66%) rename src/main/java/flipnote/notification/{common/exception => domain/notification}/NotificationErrorCode.java (87%) create mode 100644 src/main/java/flipnote/notification/infrastructure/config/AppConfig.java rename src/main/java/flipnote/notification/{application/dto/message => infrastructure/messaging}/GroupInviteMessage.java (61%) rename src/main/java/flipnote/notification/{application/dto/message => infrastructure/messaging}/GroupJoinRequestMessage.java (73%) rename src/main/java/flipnote/notification/infrastructure/{config => persistence}/JpaAuditingConfig.java (77%) rename src/main/java/flipnote/notification/interfaces/{rest => http}/NotificationController.java (68%) rename src/main/java/flipnote/notification/interfaces/{rest/docs => http}/NotificationControllerDocs.java (62%) rename src/main/java/flipnote/notification/{common/response => interfaces/http/common}/ApiResponse.java (93%) rename src/main/java/flipnote/notification/{common/response => interfaces/http/common}/ApiResponseAdvice.java (96%) rename src/main/java/flipnote/notification/{common/response => interfaces/http/common}/CursorPagingRequest.java (95%) create mode 100644 src/main/java/flipnote/notification/interfaces/http/common/CursorPagingResponse.java rename src/main/java/flipnote/notification/{common/exception => interfaces/http/common}/GlobalExceptionHandler.java (79%) rename src/main/java/flipnote/notification/{ => interfaces/http}/common/HttpHeaders.java (78%) create mode 100644 src/main/java/flipnote/notification/interfaces/http/dto/request/NotificationListRequest.java rename src/main/java/flipnote/notification/{application => interfaces/http}/dto/request/TokenRegisterRequest.java (66%) create mode 100644 src/main/java/flipnote/notification/interfaces/http/dto/response/.gitkeep diff --git a/src/main/java/flipnote/notification/application/FcmSender.java b/src/main/java/flipnote/notification/application/FcmSender.java new file mode 100644 index 0000000..ace35a4 --- /dev/null +++ b/src/main/java/flipnote/notification/application/FcmSender.java @@ -0,0 +1,10 @@ +package flipnote.notification.application; + +import java.util.List; + +import flipnote.notification.application.dto.result.FcmSendResult; + +public interface FcmSender { + + FcmSendResult sendEachForMulticast(List tokens, String title, String body); +} diff --git a/src/main/java/flipnote/notification/application/service/FcmTokenService.java b/src/main/java/flipnote/notification/application/FcmTokenService.java similarity index 63% rename from src/main/java/flipnote/notification/application/service/FcmTokenService.java rename to src/main/java/flipnote/notification/application/FcmTokenService.java index e3ba37b..de48eb4 100644 --- a/src/main/java/flipnote/notification/application/service/FcmTokenService.java +++ b/src/main/java/flipnote/notification/application/FcmTokenService.java @@ -1,4 +1,4 @@ -package flipnote.notification.application.service; +package flipnote.notification.application; import java.util.Objects; import java.util.Optional; @@ -6,7 +6,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import flipnote.notification.application.dto.request.TokenRegisterRequest; import flipnote.notification.domain.fcmtoken.FcmToken; import flipnote.notification.domain.fcmtoken.FcmTokenRepository; import lombok.RequiredArgsConstructor; @@ -19,20 +18,20 @@ public class FcmTokenService { private final FcmTokenRepository fcmTokenRepository; @Transactional - public void registerFcmToken(Long userId, TokenRegisterRequest req) { - Optional existingToken = fcmTokenRepository.findByToken(req.token()); + public void registerFcmToken(Long userId, String token) { + Optional existingToken = fcmTokenRepository.findByToken(token); if (existingToken.isPresent()) { - FcmToken token = existingToken.get(); + FcmToken fcmToken = existingToken.get(); - if (Objects.equals(token.getUserId(), userId)) { - token.updateLastUsedAt(); + if (Objects.equals(fcmToken.getUserId(), userId)) { + fcmToken.updateLastUsedAt(); } else { - fcmTokenRepository.deleteById(token.getId()); - saveFcmToken(userId, req.token()); + fcmTokenRepository.deleteById(fcmToken.getId()); + saveFcmToken(userId, token); } } else { - saveFcmToken(userId, req.token()); + saveFcmToken(userId, token); } } diff --git a/src/main/java/flipnote/notification/application/service/NotificationCommandService.java b/src/main/java/flipnote/notification/application/NotificationCommandService.java similarity index 63% rename from src/main/java/flipnote/notification/application/service/NotificationCommandService.java rename to src/main/java/flipnote/notification/application/NotificationCommandService.java index f987e2d..c27dc6b 100644 --- a/src/main/java/flipnote/notification/application/service/NotificationCommandService.java +++ b/src/main/java/flipnote/notification/application/NotificationCommandService.java @@ -1,7 +1,6 @@ -package flipnote.notification.application.service; +package flipnote.notification.application; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; @@ -11,19 +10,14 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.google.firebase.messaging.BatchResponse; -import com.google.firebase.messaging.FirebaseMessagingException; -import com.google.firebase.messaging.SendResponse; - -import flipnote.notification.application.port.FcmSender; -import flipnote.notification.common.exception.BizException; -import flipnote.notification.common.exception.NotificationErrorCode; +import flipnote.notification.application.dto.result.FcmSendResult; +import flipnote.notification.domain.common.BizException; import flipnote.notification.domain.fcmtoken.FcmToken; import flipnote.notification.domain.fcmtoken.FcmTokenRepository; import flipnote.notification.domain.notification.Notification; +import flipnote.notification.domain.notification.NotificationErrorCode; import flipnote.notification.domain.notification.NotificationRepository; import flipnote.notification.domain.notification.NotificationType; -import flipnote.notification.infrastructure.fcm.FcmErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -101,44 +95,20 @@ public void markAllNotificationsAsRead(Long userId) { } private void sendFcmNotification(Long userId, String body) { - List infos = fcmTokenRepository.findByUserId(userId); - if (infos.isEmpty()) { + List fcmTokens = fcmTokenRepository.findByUserId(userId); + if (fcmTokens.isEmpty()) { log.warn("No FCM tokens for user {}", userId); return; } - List tokens = infos.stream().map(FcmToken::getToken).toList(); - try { - BatchResponse response = fcmSender.sendEachForMulticast(tokens, "알림", body); - - List validTokens = new ArrayList<>(); - List invalidTokens = new ArrayList<>(); - for (int i = 0; i < response.getResponses().size(); i++) { - SendResponse res = response.getResponses().get(i); - if (res.isSuccessful()) { - validTokens.add(tokens.get(i)); - } else { - String errorName = res.getException().getMessagingErrorCode().name(); - FcmErrorCode code = FcmErrorCode.from(errorName); - if (code == FcmErrorCode.UNREGISTERED || code == FcmErrorCode.INVALID_ARGUMENT) { - invalidTokens.add(tokens.get(i)); - } - } - } + List tokens = fcmTokens.stream().map(FcmToken::getToken).toList(); + FcmSendResult result = fcmSender.sendEachForMulticast(tokens, "알림", body); - if (!invalidTokens.isEmpty()) { - fcmTokenRepository.deleteByUserIdAndTokenIn(userId, invalidTokens); - } - if (!validTokens.isEmpty()) { - fcmTokenRepository.bulkUpdateLastUsedAt(validTokens, LocalDateTime.now()); - } - } catch (FirebaseMessagingException e) { - String errorName = e.getMessagingErrorCode() != null ? e.getMessagingErrorCode().name() : "INTERNAL"; - FcmErrorCode code = FcmErrorCode.from(errorName); - if (code == FcmErrorCode.UNAVAILABLE) { - throw new BizException(NotificationErrorCode.FCM_SERVER_UNAVAILABLE); - } - throw new BizException(NotificationErrorCode.FCM_INTERNAL_ERROR); + if (!result.invalidTokens().isEmpty()) { + fcmTokenRepository.deleteByUserIdAndTokenIn(userId, result.invalidTokens()); + } + if (!result.validTokens().isEmpty()) { + fcmTokenRepository.bulkUpdateLastUsedAt(result.validTokens(), LocalDateTime.now()); } } diff --git a/src/main/java/flipnote/notification/application/service/NotificationQueryService.java b/src/main/java/flipnote/notification/application/NotificationQueryService.java similarity index 58% rename from src/main/java/flipnote/notification/application/service/NotificationQueryService.java rename to src/main/java/flipnote/notification/application/NotificationQueryService.java index 971a284..6d2a71b 100644 --- a/src/main/java/flipnote/notification/application/service/NotificationQueryService.java +++ b/src/main/java/flipnote/notification/application/NotificationQueryService.java @@ -1,4 +1,4 @@ -package flipnote.notification.application.service; +package flipnote.notification.application; import java.util.List; import java.util.Locale; @@ -8,9 +8,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import flipnote.notification.application.dto.request.NotificationListRequest; -import flipnote.notification.application.dto.response.NotificationResponse; -import flipnote.notification.common.response.CursorPagingResponse; +import flipnote.notification.application.dto.command.NotificationListCommand; +import flipnote.notification.application.dto.result.NotificationResult; +import flipnote.notification.application.dto.result.PagedResult; import flipnote.notification.domain.notification.Notification; import flipnote.notification.domain.notification.NotificationRepository; import lombok.RequiredArgsConstructor; @@ -23,26 +23,23 @@ public class NotificationQueryService { private final NotificationRepository notificationRepository; private final MessageSource messageSource; - public CursorPagingResponse getNotifications(Long userId, NotificationListRequest req) { + public PagedResult getNotifications(Long userId, NotificationListCommand command) { List notifications = notificationRepository.findNotificationsByReceiverIdAndCursor( - userId, req.getCursorId(), req.getGroupId(), req.getRead(), req.getPageRequest() + userId, command.cursorId(), command.groupId(), command.read(), command.getPageRequest() ); - boolean hasNext = notifications.size() > req.getSize(); + boolean hasNext = notifications.size() > command.size(); Long nextCursor = null; if (hasNext) { - notifications = notifications.subList(0, req.getSize()); + notifications = notifications.subList(0, command.size()); nextCursor = notifications.get(notifications.size() - 1).getId(); } - List content = notifications.stream() - .map(notification -> { - String message = buildMessage(notification); - return NotificationResponse.of(notification, message); - }) + List content = notifications.stream() + .map(notification -> NotificationResult.of(notification, buildMessage(notification))) .toList(); - return CursorPagingResponse.of(content, hasNext, nextCursor); + return PagedResult.of(content, hasNext, nextCursor); } private String buildMessage(Notification notification) { diff --git a/src/main/java/flipnote/notification/application/dto/command/NotificationListCommand.java b/src/main/java/flipnote/notification/application/dto/command/NotificationListCommand.java new file mode 100644 index 0000000..08997ae --- /dev/null +++ b/src/main/java/flipnote/notification/application/dto/command/NotificationListCommand.java @@ -0,0 +1,16 @@ +package flipnote.notification.application.dto.command; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; + +public record NotificationListCommand( + Long cursorId, + Long groupId, + Boolean read, + int size +) { + + public PageRequest getPageRequest() { + return PageRequest.of(0, size + 1, Sort.by(Sort.Direction.DESC, "id")); + } +} diff --git a/src/main/java/flipnote/notification/application/dto/request/NotificationListRequest.java b/src/main/java/flipnote/notification/application/dto/request/NotificationListRequest.java deleted file mode 100644 index b00f01e..0000000 --- a/src/main/java/flipnote/notification/application/dto/request/NotificationListRequest.java +++ /dev/null @@ -1,24 +0,0 @@ -package flipnote.notification.application.dto.request; - -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; - -import flipnote.notification.common.response.CursorPagingRequest; -import jakarta.validation.constraints.Min; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class NotificationListRequest extends CursorPagingRequest { - - @Min(1) - private Long groupId; - - private Boolean read; - - @Override - public PageRequest getPageRequest() { - return PageRequest.of(0, getSize(), Sort.by(Sort.Direction.DESC, "id")); - } -} diff --git a/src/main/java/flipnote/notification/application/dto/result/FcmSendResult.java b/src/main/java/flipnote/notification/application/dto/result/FcmSendResult.java new file mode 100644 index 0000000..7c70821 --- /dev/null +++ b/src/main/java/flipnote/notification/application/dto/result/FcmSendResult.java @@ -0,0 +1,9 @@ +package flipnote.notification.application.dto.result; + +import java.util.List; + +public record FcmSendResult( + List validTokens, + List invalidTokens +) { +} diff --git a/src/main/java/flipnote/notification/application/dto/response/NotificationResponse.java b/src/main/java/flipnote/notification/application/dto/result/NotificationResult.java similarity index 63% rename from src/main/java/flipnote/notification/application/dto/response/NotificationResponse.java rename to src/main/java/flipnote/notification/application/dto/result/NotificationResult.java index 3aa9ce2..6595f0a 100644 --- a/src/main/java/flipnote/notification/application/dto/response/NotificationResponse.java +++ b/src/main/java/flipnote/notification/application/dto/result/NotificationResult.java @@ -1,4 +1,4 @@ -package flipnote.notification.application.dto.response; +package flipnote.notification.application.dto.result; import java.time.LocalDateTime; import java.util.Map; @@ -7,22 +7,18 @@ import flipnote.notification.domain.notification.Notification; -public record NotificationResponse( +public record NotificationResult( Long notificationId, Long groupId, String message, Map metadata, boolean isRead, - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime readAt, - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt ) { - public static NotificationResponse of(Notification notification, String message) { - return new NotificationResponse( + public static NotificationResult of(Notification notification, String message) { + return new NotificationResult( notification.getId(), notification.getGroupId(), message, diff --git a/src/main/java/flipnote/notification/application/dto/result/PagedResult.java b/src/main/java/flipnote/notification/application/dto/result/PagedResult.java new file mode 100644 index 0000000..cbab9bf --- /dev/null +++ b/src/main/java/flipnote/notification/application/dto/result/PagedResult.java @@ -0,0 +1,14 @@ +package flipnote.notification.application.dto.result; + +import java.util.List; + +public record PagedResult( + List content, + boolean hasNext, + Long nextCursor +) { + + public static PagedResult of(List content, boolean hasNext, Long nextCursor) { + return new PagedResult<>(content, hasNext, hasNext ? nextCursor : null); + } +} diff --git a/src/main/java/flipnote/notification/application/port/FcmSender.java b/src/main/java/flipnote/notification/application/port/FcmSender.java deleted file mode 100644 index f7e8558..0000000 --- a/src/main/java/flipnote/notification/application/port/FcmSender.java +++ /dev/null @@ -1,15 +0,0 @@ -package flipnote.notification.application.port; - -import java.util.List; - -import com.google.firebase.messaging.BatchResponse; -import com.google.firebase.messaging.FirebaseMessagingException; - -public interface FcmSender { - - BatchResponse sendEachForMulticast( - List tokens, - String title, - String body - ) throws FirebaseMessagingException; -} diff --git a/src/main/java/flipnote/notification/common/response/CursorPagingResponse.java b/src/main/java/flipnote/notification/common/response/CursorPagingResponse.java deleted file mode 100644 index 2bdbf94..0000000 --- a/src/main/java/flipnote/notification/common/response/CursorPagingResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package flipnote.notification.common.response; - -import java.util.List; -import java.util.Objects; - -public record CursorPagingResponse( - List content, - boolean hasNext, - String nextCursor, - int size -) { - - public static CursorPagingResponse of(List content, boolean hasNext, String nextCursor) { - return new CursorPagingResponse<>(content, hasNext, hasNext ? nextCursor : null, content.size()); - } - - public static CursorPagingResponse of(List content, boolean hasNext, Long nextCursorId) { - String nextCursor = Objects.toString(nextCursorId, null); - return of(content, hasNext, nextCursor); - } -} diff --git a/src/main/java/flipnote/notification/common/exception/BizException.java b/src/main/java/flipnote/notification/domain/common/BizException.java similarity index 78% rename from src/main/java/flipnote/notification/common/exception/BizException.java rename to src/main/java/flipnote/notification/domain/common/BizException.java index 5231a70..ccca01f 100644 --- a/src/main/java/flipnote/notification/common/exception/BizException.java +++ b/src/main/java/flipnote/notification/domain/common/BizException.java @@ -1,4 +1,4 @@ -package flipnote.notification.common.exception; +package flipnote.notification.domain.common; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/flipnote/notification/common/exception/CommonErrorCode.java b/src/main/java/flipnote/notification/domain/common/CommonErrorCode.java similarity index 91% rename from src/main/java/flipnote/notification/common/exception/CommonErrorCode.java rename to src/main/java/flipnote/notification/domain/common/CommonErrorCode.java index bad856f..d23da8c 100644 --- a/src/main/java/flipnote/notification/common/exception/CommonErrorCode.java +++ b/src/main/java/flipnote/notification/domain/common/CommonErrorCode.java @@ -1,4 +1,4 @@ -package flipnote.notification.common.exception; +package flipnote.notification.domain.common; import org.springframework.http.HttpStatus; diff --git a/src/main/java/flipnote/notification/common/exception/ErrorCode.java b/src/main/java/flipnote/notification/domain/common/ErrorCode.java similarity index 66% rename from src/main/java/flipnote/notification/common/exception/ErrorCode.java rename to src/main/java/flipnote/notification/domain/common/ErrorCode.java index ab86736..6ac44e0 100644 --- a/src/main/java/flipnote/notification/common/exception/ErrorCode.java +++ b/src/main/java/flipnote/notification/domain/common/ErrorCode.java @@ -1,4 +1,4 @@ -package flipnote.notification.common.exception; +package flipnote.notification.domain.common; public interface ErrorCode { diff --git a/src/main/java/flipnote/notification/domain/fcmtoken/FcmTokenRepository.java b/src/main/java/flipnote/notification/domain/fcmtoken/FcmTokenRepository.java index 1ffe0f2..6f694c7 100644 --- a/src/main/java/flipnote/notification/domain/fcmtoken/FcmTokenRepository.java +++ b/src/main/java/flipnote/notification/domain/fcmtoken/FcmTokenRepository.java @@ -8,6 +8,7 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; public interface FcmTokenRepository extends JpaRepository { @@ -15,8 +16,12 @@ public interface FcmTokenRepository extends JpaRepository { Optional findByToken(String token); - void deleteByUserIdAndTokenIn(Long userId, List tokens); + @Transactional + @Modifying(clearAutomatically = true) + @Query("DELETE FROM FcmToken f WHERE f.userId = :userId AND f.token IN :tokens") + void deleteByUserIdAndTokenIn(@Param("userId") Long userId, @Param("tokens") List tokens); + @Transactional @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("UPDATE FcmToken f SET f.lastUsedAt = :now WHERE f.token IN :tokens") int bulkUpdateLastUsedAt(@Param("tokens") List tokens, @Param("now") LocalDateTime now); diff --git a/src/main/java/flipnote/notification/domain/notification/Notification.java b/src/main/java/flipnote/notification/domain/notification/Notification.java index 33acc69..2855c9f 100644 --- a/src/main/java/flipnote/notification/domain/notification/Notification.java +++ b/src/main/java/flipnote/notification/domain/notification/Notification.java @@ -46,9 +46,9 @@ public class Notification extends BaseEntity { private Map metadata; @Column(name = "is_read", nullable = false) - boolean read; + private boolean read; - LocalDateTime readAt; + private LocalDateTime readAt; @Builder public Notification( diff --git a/src/main/java/flipnote/notification/common/exception/NotificationErrorCode.java b/src/main/java/flipnote/notification/domain/notification/NotificationErrorCode.java similarity index 87% rename from src/main/java/flipnote/notification/common/exception/NotificationErrorCode.java rename to src/main/java/flipnote/notification/domain/notification/NotificationErrorCode.java index 185e33e..89cfc34 100644 --- a/src/main/java/flipnote/notification/common/exception/NotificationErrorCode.java +++ b/src/main/java/flipnote/notification/domain/notification/NotificationErrorCode.java @@ -1,7 +1,8 @@ -package flipnote.notification.common.exception; +package flipnote.notification.domain.notification; import org.springframework.http.HttpStatus; +import flipnote.notification.domain.common.ErrorCode; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/flipnote/notification/infrastructure/config/AppConfig.java b/src/main/java/flipnote/notification/infrastructure/config/AppConfig.java new file mode 100644 index 0000000..03ca193 --- /dev/null +++ b/src/main/java/flipnote/notification/infrastructure/config/AppConfig.java @@ -0,0 +1,25 @@ +package flipnote.notification.infrastructure.config; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import tools.jackson.databind.ext.javatime.ser.LocalDateTimeSerializer; +import tools.jackson.databind.module.SimpleModule; + +@Configuration +public class AppConfig { + + @Bean + public JsonMapperBuilderCustomizer jacksonCustomizer() { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + return builder -> { + SimpleModule module = new SimpleModule(); + module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter)); + builder.addModule(module); + }; + } +} diff --git a/src/main/java/flipnote/notification/infrastructure/config/SwaggerConfig.java b/src/main/java/flipnote/notification/infrastructure/config/SwaggerConfig.java index 7cff309..7ea0ab7 100644 --- a/src/main/java/flipnote/notification/infrastructure/config/SwaggerConfig.java +++ b/src/main/java/flipnote/notification/infrastructure/config/SwaggerConfig.java @@ -14,7 +14,7 @@ @Configuration public class SwaggerConfig { - @Value("${springdoc.server.url:http://localhost:8081}") + @Value("${springdoc.server.url:http://localhost:8086}") private String serverUrl; @Bean diff --git a/src/main/java/flipnote/notification/infrastructure/fcm/FirebaseFcmSender.java b/src/main/java/flipnote/notification/infrastructure/fcm/FirebaseFcmSender.java index fbcc7d0..0c546f4 100644 --- a/src/main/java/flipnote/notification/infrastructure/fcm/FirebaseFcmSender.java +++ b/src/main/java/flipnote/notification/infrastructure/fcm/FirebaseFcmSender.java @@ -1,5 +1,6 @@ package flipnote.notification.infrastructure.fcm; +import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Component; @@ -9,18 +10,18 @@ import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.MulticastMessage; import com.google.firebase.messaging.Notification; +import com.google.firebase.messaging.SendResponse; -import flipnote.notification.application.port.FcmSender; +import flipnote.notification.application.FcmSender; +import flipnote.notification.application.dto.result.FcmSendResult; +import flipnote.notification.domain.common.BizException; +import flipnote.notification.domain.notification.NotificationErrorCode; @Component public class FirebaseFcmSender implements FcmSender { @Override - public BatchResponse sendEachForMulticast( - List tokens, - String title, - String body - ) throws FirebaseMessagingException { + public FcmSendResult sendEachForMulticast(List tokens, String title, String body) { Notification notification = Notification.builder() .setTitle(title) .setBody(body) @@ -30,6 +31,36 @@ public BatchResponse sendEachForMulticast( .setNotification(notification) .build(); - return FirebaseMessaging.getInstance().sendEachForMulticast(message); + try { + BatchResponse response = FirebaseMessaging.getInstance().sendEachForMulticast(message); + return toSendResult(tokens, response); + } catch (FirebaseMessagingException e) { + String errorName = e.getMessagingErrorCode() != null ? e.getMessagingErrorCode().name() : "INTERNAL"; + FcmErrorCode code = FcmErrorCode.from(errorName); + if (code == FcmErrorCode.UNAVAILABLE) { + throw new BizException(NotificationErrorCode.FCM_SERVER_UNAVAILABLE); + } + throw new BizException(NotificationErrorCode.FCM_INTERNAL_ERROR); + } + } + + private FcmSendResult toSendResult(List tokens, BatchResponse response) { + List validTokens = new ArrayList<>(); + List invalidTokens = new ArrayList<>(); + + for (int i = 0; i < response.getResponses().size(); i++) { + SendResponse res = response.getResponses().get(i); + if (res.isSuccessful()) { + validTokens.add(tokens.get(i)); + } else { + String errorName = res.getException().getMessagingErrorCode().name(); + FcmErrorCode code = FcmErrorCode.from(errorName); + if (code == FcmErrorCode.UNREGISTERED || code == FcmErrorCode.INVALID_ARGUMENT) { + invalidTokens.add(tokens.get(i)); + } + } + } + + return new FcmSendResult(validTokens, invalidTokens); } } diff --git a/src/main/java/flipnote/notification/application/dto/message/GroupInviteMessage.java b/src/main/java/flipnote/notification/infrastructure/messaging/GroupInviteMessage.java similarity index 61% rename from src/main/java/flipnote/notification/application/dto/message/GroupInviteMessage.java rename to src/main/java/flipnote/notification/infrastructure/messaging/GroupInviteMessage.java index d02a356..6b52292 100644 --- a/src/main/java/flipnote/notification/application/dto/message/GroupInviteMessage.java +++ b/src/main/java/flipnote/notification/infrastructure/messaging/GroupInviteMessage.java @@ -1,4 +1,4 @@ -package flipnote.notification.application.dto.message; +package flipnote.notification.infrastructure.messaging; public record GroupInviteMessage( Long groupId, diff --git a/src/main/java/flipnote/notification/infrastructure/messaging/GroupInviteMessageListener.java b/src/main/java/flipnote/notification/infrastructure/messaging/GroupInviteMessageListener.java index 8c03e6c..15683cd 100644 --- a/src/main/java/flipnote/notification/infrastructure/messaging/GroupInviteMessageListener.java +++ b/src/main/java/flipnote/notification/infrastructure/messaging/GroupInviteMessageListener.java @@ -3,8 +3,7 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; -import flipnote.notification.application.dto.message.GroupInviteMessage; -import flipnote.notification.application.service.NotificationCommandService; +import flipnote.notification.application.NotificationCommandService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/flipnote/notification/application/dto/message/GroupJoinRequestMessage.java b/src/main/java/flipnote/notification/infrastructure/messaging/GroupJoinRequestMessage.java similarity index 73% rename from src/main/java/flipnote/notification/application/dto/message/GroupJoinRequestMessage.java rename to src/main/java/flipnote/notification/infrastructure/messaging/GroupJoinRequestMessage.java index f6f8837..332597d 100644 --- a/src/main/java/flipnote/notification/application/dto/message/GroupJoinRequestMessage.java +++ b/src/main/java/flipnote/notification/infrastructure/messaging/GroupJoinRequestMessage.java @@ -1,4 +1,4 @@ -package flipnote.notification.application.dto.message; +package flipnote.notification.infrastructure.messaging; import java.util.List; diff --git a/src/main/java/flipnote/notification/infrastructure/messaging/GroupJoinRequestMessageListener.java b/src/main/java/flipnote/notification/infrastructure/messaging/GroupJoinRequestMessageListener.java index 6aa3587..adf4101 100644 --- a/src/main/java/flipnote/notification/infrastructure/messaging/GroupJoinRequestMessageListener.java +++ b/src/main/java/flipnote/notification/infrastructure/messaging/GroupJoinRequestMessageListener.java @@ -3,8 +3,7 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; -import flipnote.notification.application.dto.message.GroupJoinRequestMessage; -import flipnote.notification.application.service.NotificationCommandService; +import flipnote.notification.application.NotificationCommandService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/flipnote/notification/infrastructure/config/JpaAuditingConfig.java b/src/main/java/flipnote/notification/infrastructure/persistence/JpaAuditingConfig.java similarity index 77% rename from src/main/java/flipnote/notification/infrastructure/config/JpaAuditingConfig.java rename to src/main/java/flipnote/notification/infrastructure/persistence/JpaAuditingConfig.java index d5382b4..52f84d3 100644 --- a/src/main/java/flipnote/notification/infrastructure/config/JpaAuditingConfig.java +++ b/src/main/java/flipnote/notification/infrastructure/persistence/JpaAuditingConfig.java @@ -1,4 +1,4 @@ -package flipnote.notification.infrastructure.config; +package flipnote.notification.infrastructure.persistence; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; diff --git a/src/main/java/flipnote/notification/interfaces/rest/NotificationController.java b/src/main/java/flipnote/notification/interfaces/http/NotificationController.java similarity index 68% rename from src/main/java/flipnote/notification/interfaces/rest/NotificationController.java rename to src/main/java/flipnote/notification/interfaces/http/NotificationController.java index b8d0904..b2e7f07 100644 --- a/src/main/java/flipnote/notification/interfaces/rest/NotificationController.java +++ b/src/main/java/flipnote/notification/interfaces/http/NotificationController.java @@ -1,4 +1,4 @@ -package flipnote.notification.interfaces.rest; +package flipnote.notification.interfaces.http; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -11,15 +11,14 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import flipnote.notification.application.dto.request.NotificationListRequest; -import flipnote.notification.application.dto.request.TokenRegisterRequest; -import flipnote.notification.application.dto.response.NotificationResponse; -import flipnote.notification.application.service.FcmTokenService; -import flipnote.notification.application.service.NotificationCommandService; -import flipnote.notification.application.service.NotificationQueryService; -import flipnote.notification.common.response.CursorPagingResponse; -import flipnote.notification.common.HttpHeaders; -import flipnote.notification.interfaces.rest.docs.NotificationControllerDocs; +import flipnote.notification.application.FcmTokenService; +import flipnote.notification.application.NotificationCommandService; +import flipnote.notification.application.NotificationQueryService; +import flipnote.notification.application.dto.result.NotificationResult; +import flipnote.notification.interfaces.http.common.CursorPagingResponse; +import flipnote.notification.interfaces.http.common.HttpHeaders; +import flipnote.notification.interfaces.http.dto.request.NotificationListRequest; +import flipnote.notification.interfaces.http.dto.request.TokenRegisterRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -34,12 +33,13 @@ public class NotificationController implements NotificationControllerDocs { @Override @GetMapping - public ResponseEntity> getNotifications( + public ResponseEntity> getNotifications( @Valid @ModelAttribute NotificationListRequest req, @RequestHeader(HttpHeaders.USER_ID) Long userId ) { - CursorPagingResponse res - = notificationQueryService.getNotifications(userId, req); + CursorPagingResponse res = CursorPagingResponse.from( + notificationQueryService.getNotifications(userId, req.toCommand()) + ); return ResponseEntity.ok(res); } @@ -50,7 +50,7 @@ public ResponseEntity registerFcmToken( @Valid @RequestBody TokenRegisterRequest req, @RequestHeader(HttpHeaders.USER_ID) Long userId ) { - fcmTokenService.registerFcmToken(userId, req); + fcmTokenService.registerFcmToken(userId, req.token()); return ResponseEntity.status(HttpStatus.CREATED).build(); } diff --git a/src/main/java/flipnote/notification/interfaces/rest/docs/NotificationControllerDocs.java b/src/main/java/flipnote/notification/interfaces/http/NotificationControllerDocs.java similarity index 62% rename from src/main/java/flipnote/notification/interfaces/rest/docs/NotificationControllerDocs.java rename to src/main/java/flipnote/notification/interfaces/http/NotificationControllerDocs.java index 805a96b..7e2a9d6 100644 --- a/src/main/java/flipnote/notification/interfaces/rest/docs/NotificationControllerDocs.java +++ b/src/main/java/flipnote/notification/interfaces/http/NotificationControllerDocs.java @@ -1,11 +1,11 @@ -package flipnote.notification.interfaces.rest.docs; +package flipnote.notification.interfaces.http; import org.springframework.http.ResponseEntity; -import flipnote.notification.application.dto.request.NotificationListRequest; -import flipnote.notification.application.dto.request.TokenRegisterRequest; -import flipnote.notification.application.dto.response.NotificationResponse; -import flipnote.notification.common.response.CursorPagingResponse; +import flipnote.notification.application.dto.result.NotificationResult; +import flipnote.notification.interfaces.http.common.CursorPagingResponse; +import flipnote.notification.interfaces.http.dto.request.NotificationListRequest; +import flipnote.notification.interfaces.http.dto.request.TokenRegisterRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -13,7 +13,7 @@ public interface NotificationControllerDocs { @Operation(summary = "알림 목록 조회") - ResponseEntity> getNotifications( + ResponseEntity> getNotifications( NotificationListRequest req, Long userId ); diff --git a/src/main/java/flipnote/notification/common/response/ApiResponse.java b/src/main/java/flipnote/notification/interfaces/http/common/ApiResponse.java similarity index 93% rename from src/main/java/flipnote/notification/common/response/ApiResponse.java rename to src/main/java/flipnote/notification/interfaces/http/common/ApiResponse.java index c38be2f..73ec9cd 100644 --- a/src/main/java/flipnote/notification/common/response/ApiResponse.java +++ b/src/main/java/flipnote/notification/interfaces/http/common/ApiResponse.java @@ -1,11 +1,11 @@ -package flipnote.notification.common.response; +package flipnote.notification.interfaces.http.common; import java.util.List; import java.util.stream.Collectors; import org.springframework.validation.BindingResult; -import flipnote.notification.common.exception.ErrorCode; +import flipnote.notification.domain.common.ErrorCode; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/flipnote/notification/common/response/ApiResponseAdvice.java b/src/main/java/flipnote/notification/interfaces/http/common/ApiResponseAdvice.java similarity index 96% rename from src/main/java/flipnote/notification/common/response/ApiResponseAdvice.java rename to src/main/java/flipnote/notification/interfaces/http/common/ApiResponseAdvice.java index 8f348bb..5fcd4c0 100644 --- a/src/main/java/flipnote/notification/common/response/ApiResponseAdvice.java +++ b/src/main/java/flipnote/notification/interfaces/http/common/ApiResponseAdvice.java @@ -1,4 +1,4 @@ -package flipnote.notification.common.response; +package flipnote.notification.interfaces.http.common; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; diff --git a/src/main/java/flipnote/notification/common/response/CursorPagingRequest.java b/src/main/java/flipnote/notification/interfaces/http/common/CursorPagingRequest.java similarity index 95% rename from src/main/java/flipnote/notification/common/response/CursorPagingRequest.java rename to src/main/java/flipnote/notification/interfaces/http/common/CursorPagingRequest.java index 92b3ecd..60b9f36 100644 --- a/src/main/java/flipnote/notification/common/response/CursorPagingRequest.java +++ b/src/main/java/flipnote/notification/interfaces/http/common/CursorPagingRequest.java @@ -1,4 +1,4 @@ -package flipnote.notification.common.response; +package flipnote.notification.interfaces.http.common; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; diff --git a/src/main/java/flipnote/notification/interfaces/http/common/CursorPagingResponse.java b/src/main/java/flipnote/notification/interfaces/http/common/CursorPagingResponse.java new file mode 100644 index 0000000..3e79a80 --- /dev/null +++ b/src/main/java/flipnote/notification/interfaces/http/common/CursorPagingResponse.java @@ -0,0 +1,24 @@ +package flipnote.notification.interfaces.http.common; + +import java.util.List; +import java.util.Objects; + +import flipnote.notification.application.dto.result.PagedResult; + +public record CursorPagingResponse( + List content, + boolean hasNext, + String nextCursor, + int size +) { + + public static CursorPagingResponse from(PagedResult pagedResult) { + String nextCursor = Objects.toString(pagedResult.nextCursor(), null); + return new CursorPagingResponse<>( + pagedResult.content(), + pagedResult.hasNext(), + nextCursor, + pagedResult.content().size() + ); + } +} diff --git a/src/main/java/flipnote/notification/common/exception/GlobalExceptionHandler.java b/src/main/java/flipnote/notification/interfaces/http/common/GlobalExceptionHandler.java similarity index 79% rename from src/main/java/flipnote/notification/common/exception/GlobalExceptionHandler.java rename to src/main/java/flipnote/notification/interfaces/http/common/GlobalExceptionHandler.java index 6108fdc..2651114 100644 --- a/src/main/java/flipnote/notification/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/flipnote/notification/interfaces/http/common/GlobalExceptionHandler.java @@ -1,4 +1,4 @@ -package flipnote.notification.common.exception; +package flipnote.notification.interfaces.http.common; import java.util.List; @@ -8,7 +8,9 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import flipnote.notification.common.response.ApiResponse; +import flipnote.notification.domain.common.BizException; +import flipnote.notification.domain.common.CommonErrorCode; +import flipnote.notification.domain.common.ErrorCode; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -48,11 +50,17 @@ public ResponseEntity>> handleValidatio } @ExceptionHandler(MissingServletRequestParameterException.class) - public ResponseEntity handleMissingServletRequestParameter( + public ResponseEntity> handleMissingServletRequestParameter( MissingServletRequestParameterException exception ) { String missingParam = exception.getParameterName(); String message = String.format("필수 파라미터 '%s'가 없습니다.", missingParam); - return ResponseEntity.badRequest().body(message); + return ResponseEntity + .badRequest() + .body(ApiResponse.builder() + .status(400) + .code(CommonErrorCode.INVALID_INPUT_VALUE.getCode()) + .message(message) + .build()); } } diff --git a/src/main/java/flipnote/notification/common/HttpHeaders.java b/src/main/java/flipnote/notification/interfaces/http/common/HttpHeaders.java similarity index 78% rename from src/main/java/flipnote/notification/common/HttpHeaders.java rename to src/main/java/flipnote/notification/interfaces/http/common/HttpHeaders.java index c92d2c9..d0084de 100644 --- a/src/main/java/flipnote/notification/common/HttpHeaders.java +++ b/src/main/java/flipnote/notification/interfaces/http/common/HttpHeaders.java @@ -1,4 +1,4 @@ -package flipnote.notification.common; +package flipnote.notification.interfaces.http.common; import lombok.AccessLevel; import lombok.NoArgsConstructor; diff --git a/src/main/java/flipnote/notification/interfaces/http/dto/request/NotificationListRequest.java b/src/main/java/flipnote/notification/interfaces/http/dto/request/NotificationListRequest.java new file mode 100644 index 0000000..0054d73 --- /dev/null +++ b/src/main/java/flipnote/notification/interfaces/http/dto/request/NotificationListRequest.java @@ -0,0 +1,21 @@ +package flipnote.notification.interfaces.http.dto.request; + +import flipnote.notification.application.dto.command.NotificationListCommand; +import flipnote.notification.interfaces.http.common.CursorPagingRequest; +import jakarta.validation.constraints.Min; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class NotificationListRequest extends CursorPagingRequest { + + @Min(1) + private Long groupId; + + private Boolean read; + + public NotificationListCommand toCommand() { + return new NotificationListCommand(getCursorId(), groupId, read, getSize()); + } +} diff --git a/src/main/java/flipnote/notification/application/dto/request/TokenRegisterRequest.java b/src/main/java/flipnote/notification/interfaces/http/dto/request/TokenRegisterRequest.java similarity index 66% rename from src/main/java/flipnote/notification/application/dto/request/TokenRegisterRequest.java rename to src/main/java/flipnote/notification/interfaces/http/dto/request/TokenRegisterRequest.java index 909fe0a..b279180 100644 --- a/src/main/java/flipnote/notification/application/dto/request/TokenRegisterRequest.java +++ b/src/main/java/flipnote/notification/interfaces/http/dto/request/TokenRegisterRequest.java @@ -1,4 +1,4 @@ -package flipnote.notification.application.dto.request; +package flipnote.notification.interfaces.http.dto.request; import jakarta.validation.constraints.NotEmpty; diff --git a/src/main/java/flipnote/notification/interfaces/http/dto/response/.gitkeep b/src/main/java/flipnote/notification/interfaces/http/dto/response/.gitkeep new file mode 100644 index 0000000..e69de29 From fc781eb4f8c594765cc2839d8c1efa426e84634a Mon Sep 17 00:00:00 2001 From: dungbik Date: Sat, 4 Apr 2026 03:30:11 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Chore:=20README=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 239 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ac5882 --- /dev/null +++ b/README.md @@ -0,0 +1,239 @@ +# 📔 FlipNote — Notification Service + +**FlipNote 서비스의 알림 도메인 백엔드 레포지토리입니다.** + +![Spring Boot](https://img.shields.io/badge/Spring_Boot-6DB33F?logo=springboot&logoColor=white) +![Java](https://img.shields.io/badge/Java_21-007396?logo=openjdk&logoColor=white) +![MySQL](https://img.shields.io/badge/MySQL-4479A1?logo=mysql&logoColor=white) +![RabbitMQ](https://img.shields.io/badge/RabbitMQ-FF6600?logo=rabbitmq&logoColor=white) +![Firebase](https://img.shields.io/badge/Firebase-FFCA28?logo=firebase&logoColor=black) +![Deploy](https://img.shields.io/badge/Deploy-GHCR%20%2B%20Docker-2496ED?logo=docker&logoColor=white) + +--- + +## 📑 목차 + +- [시작하기](#시작하기) +- [환경 변수](#환경-변수) +- [실행 및 배포](#실행-및-배포) +- [프로젝트 구조](#프로젝트-구조) + +--- + + + +## 🚀 시작하기 + +### 사전 요구사항 + +- **Java** 21 이상 +- **Gradle** 8 이상 +- **MySQL** 8 이상 +- **RabbitMQ** 3 이상 +- Firebase 프로젝트 생성 및 서비스 계정 JSON 발급 + +### 설치 + +```bash +# 의존성 설치 및 빌드 +./gradlew build -x test +``` + +--- + + + +## 🔐 환경 변수 + +`application.yaml`에서 참조하는 환경 변수 목록입니다. + +```text +# ─── Database ─────────────────────────────────────────── +DB_URL=jdbc:mysql://localhost:3306/flipnote_notification +DB_USERNAME= +DB_PASSWORD= + +# ─── JPA ──────────────────────────────────────────────── +# create | create-drop | update | validate | none +JPA_DDL_AUTO=validate + +# ─── RabbitMQ ─────────────────────────────────────────── +RABBITMQ_HOST=localhost +RABBITMQ_PORT=5672 +RABBITMQ_USERNAME=guest +RABBITMQ_PASSWORD=guest + +# ─── Firebase ─────────────────────────────────────────── +# 서비스 계정 JSON 전체 내용 (문자열) +FIREBASE_SERVICE_ACCOUNT_JSON= + +# ─── Server ───────────────────────────────────────────── +SERVER_PORT=8086 + +# ─── Async Thread Pool ────────────────────────────────── +ASYNC_CORE_POOL_SIZE=4 +ASYNC_MAX_POOL_SIZE=10 +ASYNC_QUEUE_CAPACITY=100 + +# ─── Swagger ──────────────────────────────────────────── +SPRINGDOC_SERVER_URL=http://localhost:8086 +``` + +> **⚠️ 주의**: 환경 변수 파일은 절대 git에 커밋하지 마세요. `.gitignore`에 포함되어 있는지 반드시 확인하세요. + +--- + + + +## 🖥️ 실행 및 배포 + +### 로컬 개발 서버 실행 + +```bash +./gradlew bootRun +``` + +기본적으로 `http://localhost:8086`에서 실행됩니다. +Swagger UI는 `http://localhost:8086/notifications/swagger-ui.html`에서 확인할 수 있습니다. + +### 프로덕션 빌드 + +```bash +./gradlew bootJar +``` + +`build/libs/notification-0.0.1-SNAPSHOT.jar` 파일이 생성됩니다. + +### 테스트 실행 + +```bash +./gradlew test +``` + +### Docker 이미지 빌드 및 실행 + +```bash +# 이미지 빌드 +docker build -t flipnote-notification . + +# 컨테이너 실행 +docker run -p 8086:8086 \ + -e DB_URL=... \ + -e FIREBASE_SERVICE_ACCOUNT_JSON=... \ + flipnote-notification +``` + +### 배포 (GitHub Actions) + +`main` 브랜치에 push 시 GitHub Actions가 자동으로 아래 과정을 실행합니다. + +**CI** (`push` / `pull_request` → `main`) +1. JDK 21 설치 +2. `./gradlew build -x test` — 빌드 검증 +3. `./gradlew test` — 테스트 실행 +4. Dependency-Check — 취약점 분석 리포트 생성 + +**CD** (`push` → `main`) +1. GitHub Container Registry(GHCR) 로그인 +2. Docker 이미지 빌드 +3. `ghcr.io/dungbik/flipnote-notification` 이미지 Push + +> 배포에 필요한 시크릿(`ORG_PAT`)은 GitHub Repository → Settings → Secrets and variables → Actions에 등록해야 합니다. + +--- + + + +## 📁 프로젝트 구조 + +- 간략화 버전 + + ```text + src/main/java/flipnote/notification/ + ├── domain/ # 도메인 레이어 (엔티티, 레포지토리, 에러코드) + ├── application/ # 애플리케이션 레이어 (서비스, 커맨드, 결과 객체) + ├── infrastructure/ # 인프라 레이어 (Firebase, RabbitMQ, 설정) + └── interfaces/ # 인터페이스 레이어 (HTTP 진입점) + ``` + +```text +FlipNote-Notification/ +├── src/ +│ ├── main/ +│ │ ├── java/flipnote/notification/ +│ │ │ ├── FlipNoteNotificationApplication.java +│ │ │ │ +│ │ │ ├── domain/ # 도메인 레이어 +│ │ │ │ ├── common/ # 도메인 공통 +│ │ │ │ │ ├── ErrorCode.java +│ │ │ │ │ ├── BizException.java +│ │ │ │ │ ├── CommonErrorCode.java +│ │ │ │ │ └── BaseEntity.java +│ │ │ │ ├── notification/ # 알림 도메인 +│ │ │ │ │ ├── Notification.java +│ │ │ │ │ ├── NotificationType.java +│ │ │ │ │ ├── NotificationRepository.java +│ │ │ │ │ └── NotificationErrorCode.java +│ │ │ │ └── fcmtoken/ # FCM 토큰 도메인 +│ │ │ │ ├── FcmToken.java +│ │ │ │ └── FcmTokenRepository.java +│ │ │ │ +│ │ │ ├── application/ # 애플리케이션 레이어 +│ │ │ │ ├── dto/ +│ │ │ │ │ ├── command/ # 서비스 입력 커맨드 (검증 어노테이션 없음) +│ │ │ │ │ │ └── NotificationListCommand.java +│ │ │ │ │ └── result/ # 서비스 출력 결과 객체 (프로토콜 무관) +│ │ │ │ │ ├── FcmSendResult.java +│ │ │ │ │ ├── NotificationResult.java +│ │ │ │ │ └── PagedResult.java +│ │ │ │ ├── FcmSender.java # FCM 아웃바운드 포트 인터페이스 +│ │ │ │ ├── FcmTokenService.java +│ │ │ │ ├── NotificationCommandService.java +│ │ │ │ └── NotificationQueryService.java +│ │ │ │ +│ │ │ ├── infrastructure/ # 인프라 레이어 +│ │ │ │ ├── config/ # 범용 설정 +│ │ │ │ │ ├── AppConfig.java +│ │ │ │ │ ├── AsyncConfig.java +│ │ │ │ │ ├── AsyncProperties.java +│ │ │ │ │ └── SwaggerConfig.java +│ │ │ │ ├── persistence/ # JPA 설정 및 변환기 +│ │ │ │ │ ├── JpaAuditingConfig.java +│ │ │ │ │ └── MapToJsonConverter.java +│ │ │ │ ├── fcm/ # Firebase Cloud Messaging 어댑터 +│ │ │ │ │ ├── FirebaseConfig.java +│ │ │ │ │ ├── FirebaseFcmSender.java # FcmSender 구현체 +│ │ │ │ │ └── FcmErrorCode.java +│ │ │ │ └── messaging/ # RabbitMQ 메시지 처리 +│ │ │ │ ├── RabbitMQConfig.java +│ │ │ │ ├── GroupInviteMessage.java +│ │ │ │ ├── GroupInviteMessageListener.java +│ │ │ │ ├── GroupJoinRequestMessage.java +│ │ │ │ └── GroupJoinRequestMessageListener.java +│ │ │ │ +│ │ │ └── interfaces/ # 인터페이스 레이어 +│ │ │ └── http/ # HTTP 진입점 +│ │ │ ├── NotificationController.java +│ │ │ ├── NotificationControllerDocs.java +│ │ │ ├── dto/request/ # HTTP Request DTO (@Valid 포함) +│ │ │ │ ├── NotificationListRequest.java +│ │ │ │ └── TokenRegisterRequest.java +│ │ │ └── common/ # HTTP 공통 유틸 +│ │ │ ├── ApiResponse.java +│ │ │ ├── ApiResponseAdvice.java +│ │ │ ├── CursorPagingRequest.java +│ │ │ ├── CursorPagingResponse.java +│ │ │ ├── GlobalExceptionHandler.java +│ │ │ └── HttpHeaders.java +│ │ │ +│ │ └── resources/ +│ │ ├── application.yaml +│ │ └── messages.properties # 알림 메시지 템플릿 +│ │ +│ └── test/ +│ └── java/flipnote/notification/ +│ +├── Dockerfile +├── build.gradle.kts +└── settings.gradle.kts +```