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 +``` 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