Skip to content

feat: 리마인드 알림 기능 구현 #54

Merged
rootTiket merged 11 commits intodevfrom
feat/#48
Jul 13, 2025
Merged

feat: 리마인드 알림 기능 구현 #54
rootTiket merged 11 commits intodevfrom
feat/#48

Conversation

@rootTiket
Copy link
Contributor

@rootTiket rootTiket commented Jul 13, 2025

🚀 PR 요약

close #48 리마인드 알림기능을 구현합니다.

✨ PR 상세 내용

  1. 사용자가 아티클을 저장합니다
  2. 아티클을 저장하면 리마인드 시간이 있을 경우 해당 시간으로 redis ttl을 설정합니다
  3. 해당 ttl이 만료될 경우 TTL 만료 이벤트가 발생 -> redis에서 data를 가져와 알림을 전송

🚨 주의 사항

원래는 notification: 키 만으로 구성하려 했으나 만료시 redis에서 사라져서 'notification:data:` 를 따로 저장하여 데이터를 가져오도록 구현했습니다. (이중키방식)

✅ 체크 리스트

  • 리뷰어 설정했나요?
  • Label 설정했나요?
  • 제목 양식 맞췄나요? (ex. feat: 기능 추가)
  • 변경 사항에 대한 테스트를 진행했나요?

Summary by CodeRabbit

  • New Features

    • Added push notification scheduling for article reminders, sending notifications at user-specified times.
    • Integrated Firebase Cloud Messaging (FCM) for push notifications.
    • Added onboarding and management for user push notification tokens.
    • Implemented Redis-based scheduling and delivery for notifications.
  • Enhancements

    • User sign-up now collects and saves a push notification token.
    • Improved configuration for Firebase and Redis services.
  • Bug Fixes

    • Minor improvements to test setup and logging.
  • Chores

    • Updated build and configuration files to support new Firebase and notification features.

@rootTiket rootTiket requested a review from ose0221 July 13, 2025 08:45
@rootTiket rootTiket self-assigned this Jul 13, 2025
@rootTiket rootTiket added the FEAT label Jul 13, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jul 13, 2025

Walkthrough

This update introduces a comprehensive reminder notification system. It adds Firebase Cloud Messaging (FCM) integration, Redis-based scheduling for notifications, new DTOs and services for push subscriptions, and modifies article creation and user signup flows to support notification tokens. Additional configuration and test updates support these features.

Changes

File(s) Change Summary
build.gradle, src/main/resources/application.yml Added Firebase Admin SDK dependency and new FCM config property.
src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java Schedules push notifications on article creation; adds push subscription and Redis notification service usage.
src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleSaveService.java Changes save method to return the saved Article object.
src/main/java/com/pinback/pinback_server/domain/auth/application/AuthUsecase.java Adds transactional annotation; saves push subscription with token on sign up.
src/main/java/com/pinback/pinback_server/domain/auth/application/command/SignUpCommand.java Adds token field and updates factory method.
src/main/java/com/pinback/pinback_server/domain/auth/presentation/dto/request/SignUpRequest.java Adds non-nullable token field and passes it to command.
src/main/java/com/pinback/pinback_server/domain/auth/presentation/dto/request/KeyCommand.java Adds new record for push key data.
src/main/java/com/pinback/pinback_server/domain/auth/presentation/dto/request/NotificationInfoCommand.java Adds new record for notification info with static factory.
src/main/java/com/pinback/pinback_server/domain/notification/domain/entity/PushSubscription.java Renames endpoint to token, removes key fields, adds static factory method.
src/main/java/com/pinback/pinback_server/domain/notification/domain/repository/PushSubscriptionRepository.java Adds method to find push subscription by user.
src/main/java/com/pinback/pinback_server/domain/notification/domain/service/PushSubscriptionGetService.java Adds service to retrieve push subscription by user.
src/main/java/com/pinback/pinback_server/domain/notification/domain/service/PushSubscriptionSaveService.java Adds service to save push subscription.
src/main/java/com/pinback/pinback_server/domain/notification/presentation/dto/request/KeyRequest.java Adds new record for notification key data.
src/main/java/com/pinback/pinback_server/domain/notification/presentation/dto/request/NotificationOnboardingRequest.java Adds onboarding request record and conversion to command.
src/main/java/com/pinback/pinback_server/global/common/util/TextUtil.java Removes a logging statement.
src/main/java/com/pinback/pinback_server/global/config/firebase/FirebaseConfig.java Adds configuration for initializing Firebase SDK.
src/main/java/com/pinback/pinback_server/global/config/redis/RedisConfig.java Adds beans for object RedisTemplate and RedisMessageListenerContainer for key expiration events.
src/main/java/com/pinback/pinback_server/infra/firebase/FcmService.java Adds service to send notifications via FCM.
src/main/java/com/pinback/pinback_server/infra/redis/NotificationExpirationListener.java Adds listener for Redis key expiration to trigger notifications.
src/main/java/com/pinback/pinback_server/infra/redis/dto/NotificationData.java Adds DTO for notification scheduling data.
src/main/java/com/pinback/pinback_server/infra/redis/service/RedisNotificationService.java Adds service for scheduling and canceling reminders in Redis.
src/test/java/com/pinback/pinback_server/domain/auth/application/AuthUsecaseTest.java Updates tests to use new SignUpCommand with token.
src/test/resources/application-test.properties Adds test DB, JPA, JWT, and Redis config for testing.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant API
    participant AuthUsecase
    participant PushSubscriptionSaveService
    participant ArticleManagementUsecase
    participant PushSubscriptionGetService
    participant RedisNotificationService
    participant Redis
    participant NotificationExpirationListener
    participant FcmService
    participant Firebase

    User->>API: SignUp (with token)
    API->>AuthUsecase: signUp()
    AuthUsecase->>PushSubscriptionSaveService: save(PushSubscription)
    AuthUsecase-->>User: SignUpResponse

    User->>API: Create Article (with reminder)
    API->>ArticleManagementUsecase: createArticle()
    ArticleManagementUsecase->>PushSubscriptionGetService: find(User)
    ArticleManagementUsecase->>RedisNotificationService: scheduleArticleReminder(Article, User, token)
    RedisNotificationService->>Redis: SET notification keys with TTL

    Redis-->>NotificationExpirationListener: Key Expiration Event
    NotificationExpirationListener->>FcmService: sendNotification(token, url)
    FcmService->>Firebase: Send FCM
Loading

Assessment against linked issues

Objective (Issue #) Addressed Explanation
Add reminder notification functionality (#48)
Implement notification scheduling on article create/update (#48)
Integration with push notification infrastructure (#48)

Assessment against linked issues: Out-of-scope changes

No out-of-scope changes found.

Possibly related PRs

Suggested reviewers

  • ose0221

Poem

A bunny taps code with a hop and a cheer,
Now reminders and pings will soon appear!
With Firebase and Redis, the system’s alive—
Push tokens and schedules, your notes will survive.
So hop on, dear user, and never forget,
Your tasks and your memos are safe—no regret!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🔭 Outside diff range comments (1)
src/main/java/com/pinback/pinback_server/domain/notification/domain/entity/PushSubscription.java (1)

31-32: Remove unnecessary length constraint from ID column

The length attribute in @Column annotation is meaningless for numeric types like Long. It only applies to string-based columns.

Apply this diff:

-	@Column(name = "push_subscription_id", nullable = false, unique = true, length = 512)
+	@Column(name = "push_subscription_id", nullable = false, unique = true)
	private Long id;
🧹 Nitpick comments (12)
src/main/resources/application.yml (1)

32-32: Fix formatting issue: Add newline at end of file.

The static analysis tool correctly identified that the file is missing a newline character at the end.

-fcm: ${FCM_JSON}
+fcm: ${FCM_JSON}
+
src/test/resources/application-test.properties (2)

14-16: Use stronger JWT secret even for tests.

The hardcoded JWT secret "secret" is too weak and predictable, even for testing environments. Consider using a longer, more secure secret to ensure tests reflect realistic security conditions and prevent potential security oversights.

-jwt.secret-key=secret
+jwt.secret-key=test-secret-key-for-testing-purposes-only-do-not-use-in-production
-jwt.issuer=secret
+jwt.issuer=test-issuer

11-12: Consider the impact of show-sql in test environment.

Enabling show-sql=true will create verbose SQL logging during tests. While useful for debugging, it might clutter test output. Consider whether this is necessary for all tests or if it should be conditionally enabled.

src/test/java/com/pinback/pinback_server/domain/auth/application/AuthUsecaseTest.java (2)

39-39: Use more meaningful test data for the token parameter.

The hardcoded "token" string doesn't reflect realistic FCM token formats, which could miss edge cases in testing. Consider using a more realistic FCM token format or different token values for each test method.

-		SignUpCommand command = SignUpCommand.of("testEmail", LocalTime.of(10, 0, 0), "token");
+		SignUpCommand command = SignUpCommand.of("testEmail", LocalTime.of(10, 0, 0), "fcm-test-token-" + System.currentTimeMillis());

55-55: Use different token values for distinct test scenarios.

Both test methods use the same hardcoded token value. Using different tokens would better validate the system's ability to handle multiple unique subscriptions and improve test isolation.

-		SignUpCommand command = SignUpCommand.of("testEmail", LocalTime.of(10, 0, 0), "token");
+		SignUpCommand command = SignUpCommand.of("testEmail", LocalTime.of(10, 0, 0), "fcm-valid-token-test");
src/main/java/com/pinback/pinback_server/domain/auth/presentation/dto/request/NotificationInfoCommand.java (1)

7-9: Consider more descriptive factory method name.

The factory method name from is generic. Consider a more descriptive name like of or create to match Java conventions, or fromEndpointAndKey to be more explicit about the parameters.

-	public static NotificationInfoCommand from(String endpoint, KeyCommand command) {
+	public static NotificationInfoCommand of(String endpoint, KeyCommand key) {
		return new NotificationInfoCommand(endpoint, command);
	}
src/main/java/com/pinback/pinback_server/domain/notification/domain/service/PushSubscriptionSaveService.java (2)

15-17: Consider returning the saved entity and adding validation.

The service method doesn't return the saved entity, which callers might need (e.g., for the generated ID). Additionally, consider adding validation or business logic to justify the service layer, or evaluate if direct repository usage would be more appropriate.

-	public void save(PushSubscription pushSubscription) {
-		repository.save(pushSubscription);
+	public PushSubscription save(PushSubscription pushSubscription) {
+		// Add validation if needed
+		if (pushSubscription == null) {
+			throw new IllegalArgumentException("PushSubscription cannot be null");
+		}
+		return repository.save(pushSubscription);
	}

12-18: Evaluate the necessity of this service layer.

This service currently provides no additional value over direct repository usage - it's a simple delegation. Consider whether business logic, validation, or transaction management will be added here, or if the service layer is needed at all for this simple operation.

src/main/java/com/pinback/pinback_server/domain/auth/presentation/dto/request/SignUpRequest.java (1)

21-22: Consider improving validation message clarity.

The validation message "알림 정보는 비어있을 수 없습니다." (notification information cannot be empty) could be more specific about what type of token is expected for better API usability.

Consider updating the validation message to be more specific:

-	@NotNull(message = "알림 정보는 비어있을 수 없습니다.")
+	@NotNull(message = "푸시 알림 토큰은 비어있을 수 없습니다.")

Also consider adding a @Schema annotation to document the field:

+	@Schema(description = "푸시 알림 토큰", example = "fcm_token_example")
 	@NotNull(message = "푸시 알림 토큰은 비어있을 수 없습니다.")
 	String token
src/main/java/com/pinback/pinback_server/infra/firebase/FcmService.java (1)

14-25: Enhance logging and consider internationalization.

The service could benefit from improved logging and error message considerations.

Consider these improvements:

public void sendNotification(String token, String url) {
+    log.debug("Sending FCM notification to token: {} with URL: {}", token.substring(0, 10) + "...", url);
    try {
        Message.Builder messageBuilder = Message.builder()
            .setToken(token)
            .putData("url", url);

        FirebaseMessaging.getInstance().send(messageBuilder.build());
+        log.info("FCM notification sent successfully");

    } catch (FirebaseMessagingException e) {
-        throw new RuntimeException("익스텐션 알림 전송 실패", e);
+        log.error("Failed to send FCM notification: {}", e.getMessage(), e);
+        throw new RuntimeException("Failed to send extension notification", e);
    }
}

This adds debug logging for sends and uses English error messages for better international team collaboration.

src/main/java/com/pinback/pinback_server/domain/notification/domain/entity/PushSubscription.java (1)

38-39: Consider adding length constraint for token field

The length constraint was removed from the token field. Consider adding a reasonable limit to prevent storage of excessively long tokens.

If FCM tokens have a known maximum length, apply a constraint:

-	@Column(name = "endpoint", nullable = false, unique = true)
+	@Column(name = "token", nullable = false, unique = true, length = 4096)
	private String token;
src/main/java/com/pinback/pinback_server/global/config/redis/RedisConfig.java (1)

77-78: Consider limiting Redis keyspace event pattern to specific database

The current pattern "__keyevent@*__:expired" listens to expiration events from all Redis databases. This might capture unintended events if multiple databases are used.

If you're using a specific database (e.g., database 0), consider:

-			new PatternTopic("__keyevent@*__:expired"));
+			new PatternTopic("__keyevent@0__:expired"));
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23d86b5 and 0fa4ef0.

📒 Files selected for processing (24)
  • build.gradle (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java (2 hunks)
  • src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleSaveService.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/auth/application/AuthUsecase.java (2 hunks)
  • src/main/java/com/pinback/pinback_server/domain/auth/application/command/SignUpCommand.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/auth/presentation/dto/request/KeyCommand.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/auth/presentation/dto/request/NotificationInfoCommand.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/auth/presentation/dto/request/SignUpRequest.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/notification/domain/entity/PushSubscription.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/notification/domain/repository/PushSubscriptionRepository.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/notification/domain/service/PushSubscriptionGetService.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/notification/domain/service/PushSubscriptionSaveService.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/notification/presentation/dto/request/KeyRequest.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/notification/presentation/dto/request/NotificationOnboardingRequest.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/global/common/util/TextUtil.java (0 hunks)
  • src/main/java/com/pinback/pinback_server/global/config/firebase/FirebaseConfig.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/global/config/redis/RedisConfig.java (2 hunks)
  • src/main/java/com/pinback/pinback_server/infra/firebase/FcmService.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/infra/redis/NotificationExpirationListener.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/infra/redis/dto/NotificationData.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/infra/redis/service/RedisNotificationService.java (1 hunks)
  • src/main/resources/application.yml (1 hunks)
  • src/test/java/com/pinback/pinback_server/domain/auth/application/AuthUsecaseTest.java (2 hunks)
  • src/test/resources/application-test.properties (1 hunks)
💤 Files with no reviewable changes (1)
  • src/main/java/com/pinback/pinback_server/global/common/util/TextUtil.java
🧰 Additional context used
🧠 Learnings (2)
src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleSaveService.java (1)
Learnt from: rootTiket
PR: Pinback-Team/pinback-server#18
File: src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:38-41
Timestamp: 2025-07-07T15:33:02.748Z
Learning: In the Pinback server project, article detail retrieval (getArticleDetail method) does not require user authorization checks. Articles can be accessed by ID without ownership verification as per the project's design decision.
src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java (1)
Learnt from: rootTiket
PR: Pinback-Team/pinback-server#18
File: src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:38-41
Timestamp: 2025-07-07T15:33:02.748Z
Learning: In the Pinback server project, article detail retrieval (getArticleDetail method) does not require user authorization checks. Articles can be accessed by ID without ownership verification as per the project's design decision.
🧬 Code Graph Analysis (2)
src/main/java/com/pinback/pinback_server/domain/auth/application/AuthUsecase.java (1)
src/main/java/com/pinback/pinback_server/domain/auth/exception/UserDuplicateException.java (1)
  • UserDuplicateException (6-10)
src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java (3)
src/main/java/com/pinback/pinback_server/domain/notification/domain/service/PushSubscriptionGetService.java (1)
  • Service (11-19)
src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleSaveService.java (1)
  • Service (11-20)
src/main/java/com/pinback/pinback_server/infra/redis/service/RedisNotificationService.java (1)
  • Service (17-88)
🪛 YAMLlint (1.37.1)
src/main/resources/application.yml

[error] 32-32: no new line character at the end of file

(new-line-at-end-of-file)

🔇 Additional comments (14)
src/main/resources/application.yml (1)

32-32: LGTM: Secure FCM configuration approach.

Using environment variables for Firebase configuration is a security best practice that keeps sensitive credentials out of the codebase.

src/main/java/com/pinback/pinback_server/domain/auth/presentation/dto/request/KeyCommand.java (1)

3-7: LGTM: Correct WebPush subscription key structure.

The record correctly represents the standard WebPush subscription key components (p256dh and auth) as immutable data.

src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleSaveService.java (1)

17-18: LGTM: Return saved entity enables notification workflow.

Returning the saved Article is a good change that enables the notification scheduling functionality while following standard repository patterns.

src/main/java/com/pinback/pinback_server/domain/auth/application/AuthUsecase.java (2)

27-27: Good practice: Adding transactional integrity.

The @Transactional annotation ensures that both user creation and push subscription creation are handled atomically, which is important for data consistency.


35-40: Push subscription rollback is covered by the existing transaction

The signUp method in AuthUsecase is annotated with Spring’s @Transactional, and PushSubscriptionSaveService.save(...) simply delegates to the JPA repository.save(...) call without catching exceptions. Since Spring rolls back the transaction on any unchecked exception (and Spring Data repository methods throw runtime exceptions on failure), a failure in pushSubscriptionSaveService.save(...) will correctly trigger a rollback of the entire signUp transaction (including user creation).

No changes required.

src/main/java/com/pinback/pinback_server/domain/auth/application/command/SignUpCommand.java (1)

5-12: LGTM: Clean command object update.

The addition of the token field and corresponding factory method update follows good practices and maintains the immutable nature of the command object.

src/main/java/com/pinback/pinback_server/domain/notification/presentation/dto/request/NotificationOnboardingRequest.java (1)

6-9: KeyRequest record definition verified

The KeyRequest record is properly defined and accessible in the codebase:

  • src/main/java/com/pinback/pinback_server/domain/notification/presentation/dto/request/KeyRequest.java

No further action required.

src/main/java/com/pinback/pinback_server/infra/redis/dto/NotificationData.java (1)

10-21: LGTM! Well-structured DTO implementation.

The DTO is properly designed with appropriate Lombok annotations and field types. The use of LocalDateTime for time fields and clear field naming follows good practices.

src/main/java/com/pinback/pinback_server/infra/redis/NotificationExpirationListener.java (1)

19-21: Dual-Key TTL Strategy Verified

The RedisNotificationService configures the data key to expire after ttlSeconds + 100_000, which is longer than the notification key’s ttlSeconds, preventing race conditions.

• In src/main/java/com/pinback/pinback_server/infra/redis/service/RedisNotificationService.java:
objectRedisTemplate.opsForValue().set(notificationKey, …, Duration.ofSeconds(ttlSeconds));
objectRedisTemplate.opsForValue().set(notificationDataKey, …, Duration.ofSeconds(ttlSeconds + 100_000)); // TODO: 명확한 TTL 정책 필요

No further changes required for the dual-key expiration strategy.

src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java (1)

65-65: Verified: ArticleSaveService.save now returns Article – no action needed

The save method in ArticleSaveService has been updated to return an Article (see src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleSaveService.java:17–19), and the usage in ArticleManagementUsecase correctly captures this return value.

src/main/java/com/pinback/pinback_server/domain/notification/domain/entity/PushSubscription.java (1)

41-46: Factory method implementation looks good

The static factory method follows best practices and properly uses the builder pattern.

src/main/java/com/pinback/pinback_server/global/config/redis/RedisConfig.java (1)

45-67: ObjectMapper configuration for Redis template is appropriate

The configuration properly handles Java time objects and unknown properties, which is suitable for Redis serialization.

src/main/java/com/pinback/pinback_server/infra/redis/service/RedisNotificationService.java (2)

70-79: NotificationData creation is well-structured

The helper method properly encapsulates the DTO creation logic with all necessary fields.


54-56: Avoid blocking Redis with keys() in production
The RedisTemplate#keys() call is a blocking operation and can lead to latency spikes on large datasets. Replace it with a non-blocking SCAN-based approach via a RedisCallback, for example:

String pattern = NOTIFICATION_PREFIX + articleId + ":" + userId + ":*";
Set<String> keys = new HashSet<>();
objectRedisTemplate.execute((RedisCallback<Void>) connection -> {
    try (Cursor<byte[]> cursor = connection.scan(
            ScanOptions.scanOptions()
                       .match(pattern)
                       .count(100)
                       .build())) {
        cursor.forEachRemaining(key -> keys.add(new String(key)));
    }
    return null;
});

• Ensure your Spring Data Redis version supports RedisConnection.scan(...) (upgrade if necessary).
• Test in a staging environment to validate performance.

Comment on lines +62 to +63
//firebase
implementation 'com.google.firebase:firebase-admin:9.2.0'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify Firebase Admin SDK version and security status.

The Firebase Admin SDK version 9.2.0 should be checked to ensure it's current and free from known vulnerabilities.


🌐 Web query:

What is the latest version of Firebase Admin SDK for Java and are there any known security vulnerabilities in version 9.2.0?

💡 Result:

The latest version of the Firebase Admin SDK for Java is 9.5.0, released in May 2025[5][8].

Version 9.2.0 is not the most current version (it was released in June 2023), and there are known security vulnerabilities associated with it, specifically:

These vulnerabilities come from dependencies used by the SDK, not necessarily from the SDK's own codebase[2]. As such, using 9.2.0 exposes your application to publicly disclosed security issues.

To maintain the best security posture, it is strongly advised to upgrade to at least version 9.5.0[5][8].

Citations:


Upgrade Firebase Admin SDK to 9.5.0 to address security vulnerabilities

The currently pinned version 9.2.0 (lines 62–63 in build.gradle) is out of date and exposes your application to CVE-2025-24970 and CVE-2024-29025. The latest Firebase Admin SDK for Java is 9.5.0 (May 2025).

• File: build.gradle
Lines: 62–63
Current:

implementation 'com.google.firebase:firebase-admin:9.2.0'

Recommended:

implementation 'com.google.firebase:firebase-admin:9.5.0'
🤖 Prompt for AI Agents
In build.gradle at lines 62 to 63, update the Firebase Admin SDK dependency
version from 9.2.0 to 9.5.0 to fix known security vulnerabilities. Replace the
line "implementation 'com.google.firebase:firebase-admin:9.2.0'" with
"implementation 'com.google.firebase:firebase-admin:9.5.0'".

Comment on lines +3 to +7
public record KeyRequest(
String p256dh,
String auth
) {
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Eliminate code duplication: Consolidate with KeyCommand.

This record is identical to KeyCommand in the auth domain, violating the DRY principle. Consider creating a shared data structure in a common package.

Create a shared record in a common package (e.g., com.pinback.pinback_server.global.dto) and remove the duplicate:

// In com.pinback.pinback_server.global.dto.WebPushKey
public record WebPushKey(
    String p256dh,
    String auth
) {
}

Then update both domains to use the shared record instead of maintaining separate identical structures.

🤖 Prompt for AI Agents
In
src/main/java/com/pinback/pinback_server/domain/notification/presentation/dto/request/KeyRequest.java
lines 3 to 7, the KeyRequest record duplicates the KeyCommand record from the
auth domain. To fix this, create a new shared record named WebPushKey in a
common package like com.pinback.pinback_server.global.dto with the same fields,
then replace both KeyRequest and KeyCommand usages with this shared WebPushKey
record to eliminate duplication and adhere to the DRY principle.

Comment on lines +3 to +6
public record NotificationInfoCommand(
String endpoint,
KeyCommand key
) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify consistency with simplified PushSubscription entity.

According to the AI summary, the PushSubscription entity was simplified to use only a token field, removing endpoint and cryptographic key fields. However, this command still maintains the endpoint and key structure. Please verify if this command is still needed in its current form or if it should be updated to align with the simplified entity model.


🏁 Script executed:

#!/bin/bash
# Check how NotificationInfoCommand is used and if it aligns with PushSubscription entity changes
ast-grep --pattern 'class PushSubscription {
  $$$
}'

# Check usage of NotificationInfoCommand
rg -A 5 "NotificationInfoCommand"

Length of output: 3100


🏁 Script executed:

#!/bin/bash
# Search for PushSubscription entity or record declaration across the repository
rg -n "PushSubscription"

Length of output: 4412


🏁 Script executed:

#!/bin/bash
# Display PushSubscription entity fields and constructor/builder
rg -n "class PushSubscription" -A20 src/main/java/com/pinback/pinback_server/domain/notification/domain/entity/PushSubscription.java

Length of output: 753


Align DTO with simplified PushSubscription model

The PushSubscription entity now only stores a token (mapped from the endpoint column) and drops any cryptographic key fields. However, NotificationInfoCommand still carries a KeyCommand key that is never used when converting to the domain entity:

  • NotificationInfoCommand.from(endpoint, key) → only passes endpoint into PushSubscription.from(user, token)
  • KeyCommand is parsed in NotificationOnboardingRequest but then discarded

Action items:

  • Remove the unused KeyCommand key field and its factory parameter from:
    • src/main/java/com/pinback/pinback_server/domain/auth/presentation/dto/request/NotificationInfoCommand.java
  • Update NotificationOnboardingRequest.toCommand() in:
    • src/main/java/com/pinback/pinback_server/domain/notification/presentation/dto/request/NotificationOnboardingRequest.java
      to only pass endpoint (or rename to token for consistency)
  • (Optional) If you intend to persist VAPID keys/auth secrets, extend the PushSubscription entity to include those fields instead of dropping them

Example diff for NotificationInfoCommand.java:

-public record NotificationInfoCommand(
-   String endpoint,
-   KeyCommand key
-) {
-   public static NotificationInfoCommand from(String endpoint, KeyCommand command) {
-       return new NotificationInfoCommand(endpoint, command);
-   }
-}
+public record NotificationInfoCommand(String endpoint) {
+   public static NotificationInfoCommand from(String endpoint) {
+       return new NotificationInfoCommand(endpoint);
+   }
+}

And in NotificationOnboardingRequest.java:

-public NotificationInfoCommand toCommand() {
-   return NotificationInfoCommand.from(
-       endpoint,
-       new KeyCommand(keyRequest().p256dh(), keyRequest().auth())
-   );
-}
+public NotificationInfoCommand toCommand() {
+   return NotificationInfoCommand.from(endpoint);
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public record NotificationInfoCommand(
String endpoint,
KeyCommand key
) {
public record NotificationInfoCommand(String endpoint) {
public static NotificationInfoCommand from(String endpoint) {
return new NotificationInfoCommand(endpoint);
}
}
🤖 Prompt for AI Agents
In
src/main/java/com/pinback/pinback_server/domain/auth/presentation/dto/request/NotificationInfoCommand.java
lines 3 to 6, remove the unused KeyCommand key field and its constructor
parameter since the PushSubscription model no longer uses cryptographic keys.
Then, in
src/main/java/com/pinback/pinback_server/domain/notification/presentation/dto/request/NotificationOnboardingRequest.java,
update the toCommand() method to only pass the endpoint (or rename it to token)
without the key parameter to align with the simplified PushSubscription entity.

Comment on lines 16 to 18
public PushSubscription find(User user) {
return pushSubscriptionRepository.findPushSubscriptionByUser(user);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Consider handling null return values.

The find method delegates to the repository without null checking. Verify that the repository method findPushSubscriptionByUser either guarantees a non-null result or that callers are prepared to handle null returns.


🏁 Script executed:

#!/bin/bash
# Description: Check PushSubscriptionRepository method signature and return type
# Expected: Verify if method returns Optional<PushSubscription> or PushSubscription

ast-grep --pattern 'interface PushSubscriptionRepository {
  $$$
  findPushSubscriptionByUser($$$) {
    $$$
  }
  $$$
}'

# Also check for any usage patterns
rg -A 3 "findPushSubscriptionByUser"

Length of output: 843


Handle potential null returns from repository

The repository method findPushSubscriptionByUser(User user) returns a plain PushSubscription, which will be null if no record is found. Update the service to handle that case:

• File: src/main/java/com/pinback/pinback_server/domain/notification/domain/service/PushSubscriptionGetService.java

  • Either change the return type to Optional<PushSubscription> and wrap the call:
    - public PushSubscription find(User user) {
    -     return pushSubscriptionRepository.findPushSubscriptionByUser(user);
    - }
    + public Optional<PushSubscription> find(User user) {
    +     return Optional.ofNullable(pushSubscriptionRepository.findPushSubscriptionByUser(user));
    + }
  • Or throw a specific exception when the result is null:
    - public PushSubscription find(User user) {
    -     return pushSubscriptionRepository.findPushSubscriptionByUser(user);
    - }
    + public PushSubscription find(User user) {
    +     return Optional.ofNullable(pushSubscriptionRepository.findPushSubscriptionByUser(user))
    +         .orElseThrow(() ->
    +             new EntityNotFoundException("No PushSubscription found for user: " + user.getId()));
    + }

Ensure downstream callers are updated accordingly.

🤖 Prompt for AI Agents
In
src/main/java/com/pinback/pinback_server/domain/notification/domain/service/PushSubscriptionGetService.java
around lines 16 to 18, the method find(User user) returns a PushSubscription
that can be null if no record is found. To fix this, either change the return
type to Optional<PushSubscription> and return
Optional.ofNullable(pushSubscriptionRepository.findPushSubscriptionByUser(user)),
or keep the return type and throw a specific exception if the repository returns
null. Also update any callers of this method to handle the new return type or
exception accordingly.

Comment on lines +38 to +39
@Column(name = "endpoint", nullable = false, unique = true)
private String token;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix column name to match field name

The column name is still "endpoint" while the field has been renamed to "token". This mismatch will cause issues with database persistence.

Apply this diff to fix the column name:

-	@Column(name = "endpoint", nullable = false, unique = true)
+	@Column(name = "token", nullable = false, unique = true)
	private String token;
🤖 Prompt for AI Agents
In
src/main/java/com/pinback/pinback_server/domain/notification/domain/entity/PushSubscription.java
at lines 38-39, the @Column annotation specifies the column name as "endpoint"
while the field is named "token". To fix this mismatch, update the column name
in the @Column annotation to "token" so it matches the field name and ensures
correct database persistence.

Comment on lines +55 to +58
objectMapper.activateDefaultTyping(
objectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Security concern: Avoid using activateDefaultTyping

Using activateDefaultTyping with NON_FINAL can lead to deserialization vulnerabilities where malicious actors could execute arbitrary code by crafting specific JSON payloads.

Consider using explicit type information or @JsonTypeInfo annotations on your DTOs instead:

-		objectMapper.activateDefaultTyping(
-			objectMapper.getPolymorphicTypeValidator(),
-			ObjectMapper.DefaultTyping.NON_FINAL
-		);

If polymorphic types are necessary, use a more restrictive approach with explicit type mappings.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
objectMapper.activateDefaultTyping(
objectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL
);
🤖 Prompt for AI Agents
In src/main/java/com/pinback/pinback_server/global/config/redis/RedisConfig.java
around lines 55 to 58, avoid using objectMapper.activateDefaultTyping with
NON_FINAL due to security risks of deserialization vulnerabilities. Instead,
remove this call and implement explicit type information by annotating your DTO
classes with @JsonTypeInfo or configure the ObjectMapper with a more restrictive
polymorphic type validator that only allows known safe types. This prevents
arbitrary code execution from crafted JSON payloads.

Comment on lines +38 to +40
objectRedisTemplate.opsForValue()
.set(notificationDataKey, notificationData,
Duration.ofSeconds(ttlSeconds + 100000)); //TODO: 명확한 TTL 정책 필요
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Address TODO: Define clear TTL retention policy

The hardcoded 100,000 seconds (≈27.8 hours) for data retention needs a clear policy. Consider making this configurable.

Define a configuration property and inject it:

@Value("${notification.data.retention.seconds:172800}") // Default 48 hours
private long dataRetentionSeconds;

Then use it:

-				.set(notificationDataKey, notificationData,
-					Duration.ofSeconds(ttlSeconds + 100000)); //TODO: 명확한 TTL 정책 필요
+				.set(notificationDataKey, notificationData,
+					Duration.ofSeconds(ttlSeconds + dataRetentionSeconds));
🤖 Prompt for AI Agents
In
src/main/java/com/pinback/pinback_server/infra/redis/service/RedisNotificationService.java
around lines 38 to 40, replace the hardcoded additional TTL of 100000 seconds
with a configurable property. Define a new configuration property named
notification.data.retention.seconds with a default value (e.g., 172800 seconds
for 48 hours) using @Value annotation, inject it into the class, and use this
property to set the TTL duration instead of the fixed number. This will make the
TTL retention policy clear and adjustable.

Comment on lines +52 to +68
public void cancelArticleReminder(Long articleId, UUID userId) {
try {
String pattern = NOTIFICATION_PREFIX + articleId + ":" + userId + ":*";
var keys = objectRedisTemplate.keys(pattern);

if (keys != null && !keys.isEmpty()) {
objectRedisTemplate.delete(keys);
log.info("Article 알림 취소 완료: articleId={}, userId={}, 삭제된 키 수={}",
articleId, userId, keys.size());
} else {
log.debug("취소할 알림이 없습니다: articleId={}, userId={}", articleId, userId);
}

} catch (Exception e) {
log.error("Article 알림 취소 실패: articleId={}, userId={}", articleId, userId, e);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider also deleting data keys when canceling reminders

The cancel method only deletes notification keys but not the corresponding data keys with notification:data: prefix.

Update the method to delete both key types:

 public void cancelArticleReminder(Long articleId, UUID userId) {
     try {
         String pattern = NOTIFICATION_PREFIX + articleId + ":" + userId + ":*";
+        String dataPattern = NOTIFICATION_PREFIX_DATA + articleId + ":" + userId + ":*";
         var keys = objectRedisTemplate.keys(pattern);
+        var dataKeys = objectRedisTemplate.keys(dataPattern);
 
-        if (keys != null && !keys.isEmpty()) {
-            objectRedisTemplate.delete(keys);
+        Set<String> allKeys = new HashSet<>();
+        if (keys != null) allKeys.addAll(keys);
+        if (dataKeys != null) allKeys.addAll(dataKeys);
+        
+        if (!allKeys.isEmpty()) {
+            objectRedisTemplate.delete(allKeys);
             log.info("Article 알림 취소 완료: articleId={}, userId={}, 삭제된 키 수={}",
-                articleId, userId, keys.size());
+                articleId, userId, allKeys.size());
         } else {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/main/java/com/pinback/pinback_server/infra/redis/service/RedisNotificationService.java
around lines 52 to 68, the cancelArticleReminder method deletes only
notification keys but not the related data keys with the "notification:data:"
prefix. Modify the method to also construct a pattern for data keys using the
same articleId and userId, retrieve those keys from Redis, and delete them
alongside the notification keys to ensure all related reminder data is removed.

Comment on lines +26 to +50
public void scheduleArticleReminder(Article article, User user, String token) {

try {
String notificationId = generateNotificationId(article.getId(), user.getId());
String notificationKey = NOTIFICATION_PREFIX + notificationId;
String notificationDataKey = NOTIFICATION_PREFIX_DATA + notificationId;

NotificationData notificationData = createNotificationData(article, user, token, article.getRemindAt());

long ttlSeconds = calculateTTLSeconds(article.getRemindAt());

objectRedisTemplate.opsForValue().set(notificationKey, notificationData, Duration.ofSeconds(ttlSeconds));
objectRedisTemplate.opsForValue()
.set(notificationDataKey, notificationData,
Duration.ofSeconds(ttlSeconds + 100000)); //TODO: 명확한 TTL 정책 필요

log.info("Article 알림 예약 완료: articleId={}, userId={}, notificationId={}, remindTime={}, ttlSeconds={}",
article.getId(), user.getId(), notificationId, article.getRemindAt(), ttlSeconds);

} catch (Exception e) {
log.error("Article 알림 예약 실패: articleId={}, userId={}, remindTime={}",
article.getId(), user.getId(), article.getRemindAt(), e);

}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Validate reminder time before scheduling

The method doesn't validate if the reminder time is in the future. This could lead to immediate expiration or negative TTL values.

Add validation at the beginning of the method:

 public void scheduleArticleReminder(Article article, User user, String token) {
+    if (article.getRemindAt() == null || article.getRemindAt().isBefore(LocalDateTime.now())) {
+        log.warn("Invalid reminder time for article: articleId={}, remindAt={}", 
+            article.getId(), article.getRemindAt());
+        return;
+    }
 
     try {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void scheduleArticleReminder(Article article, User user, String token) {
try {
String notificationId = generateNotificationId(article.getId(), user.getId());
String notificationKey = NOTIFICATION_PREFIX + notificationId;
String notificationDataKey = NOTIFICATION_PREFIX_DATA + notificationId;
NotificationData notificationData = createNotificationData(article, user, token, article.getRemindAt());
long ttlSeconds = calculateTTLSeconds(article.getRemindAt());
objectRedisTemplate.opsForValue().set(notificationKey, notificationData, Duration.ofSeconds(ttlSeconds));
objectRedisTemplate.opsForValue()
.set(notificationDataKey, notificationData,
Duration.ofSeconds(ttlSeconds + 100000)); //TODO: 명확한 TTL 정책 필요
log.info("Article 알림 예약 완료: articleId={}, userId={}, notificationId={}, remindTime={}, ttlSeconds={}",
article.getId(), user.getId(), notificationId, article.getRemindAt(), ttlSeconds);
} catch (Exception e) {
log.error("Article 알림 예약 실패: articleId={}, userId={}, remindTime={}",
article.getId(), user.getId(), article.getRemindAt(), e);
}
}
public void scheduleArticleReminder(Article article, User user, String token) {
if (article.getRemindAt() == null || article.getRemindAt().isBefore(LocalDateTime.now())) {
log.warn("Invalid reminder time for article: articleId={}, remindAt={}",
article.getId(), article.getRemindAt());
return;
}
try {
String notificationId = generateNotificationId(article.getId(), user.getId());
String notificationKey = NOTIFICATION_PREFIX + notificationId;
String notificationDataKey = NOTIFICATION_PREFIX_DATA + notificationId;
NotificationData notificationData = createNotificationData(article, user, token, article.getRemindAt());
long ttlSeconds = calculateTTLSeconds(article.getRemindAt());
objectRedisTemplate.opsForValue().set(notificationKey, notificationData, Duration.ofSeconds(ttlSeconds));
objectRedisTemplate.opsForValue()
.set(notificationDataKey, notificationData,
Duration.ofSeconds(ttlSeconds + 100000)); //TODO: 명확한 TTL 정책 필요
log.info("Article 알림 예약 완료: articleId={}, userId={}, notificationId={}, remindTime={}, ttlSeconds={}",
article.getId(), user.getId(), notificationId, article.getRemindAt(), ttlSeconds);
} catch (Exception e) {
log.error("Article 알림 예약 실패: articleId={}, userId={}, remindTime={}",
article.getId(), user.getId(), article.getRemindAt(), e);
}
}
🤖 Prompt for AI Agents
In
src/main/java/com/pinback/pinback_server/infra/redis/service/RedisNotificationService.java
around lines 26 to 50, the scheduleArticleReminder method lacks validation to
ensure the reminder time is in the future, which can cause negative or immediate
expiration TTL values. Add a check at the start of the method to verify that
article.getRemindAt() is after the current time, and if not, log a warning or
skip scheduling to prevent setting invalid TTLs.

Copy link
Contributor

@ose0221 ose0221 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다! 궁금한 점 댓글로 남겨두었습니다 !

@Repository
public interface PushSubscriptionRepository extends JpaRepository<PushSubscription, Long> {

PushSubscription findPushSubscriptionByUser(User user);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 PushSubscription가 null 이 될 가능성은 없나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional로 변경했습니다~!!

Comment on lines 16 to 17
public PushSubscription find(User user) {
return pushSubscriptionRepository.findPushSubscriptionByUser(user);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에서 이야기한 PushSubscription이 null 값을 반환한다면 여기서 예외처리 해주어야 할 것 같습니다 !

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했습니다!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/main/java/com/pinback/pinback_server/domain/notification/exception/SubscriptionNotFoundException.java (1)

6-10: Add JavaDoc documentation for better maintainability.

Consider adding JavaDoc to explain when this exception should be thrown and its role in the notification system.

+/**
+ * Exception thrown when a push subscription is not found for a given user.
+ * This typically results in an HTTP 404 response when attempting to retrieve
+ * subscription data for notification scheduling.
+ */
 public class SubscriptionNotFoundException extends ApplicationException {
+	/**
+	 * Constructs a new SubscriptionNotFoundException with the predefined error code.
+	 */
 	public SubscriptionNotFoundException() {
 		super(ExceptionCode.SUBSCRIPTION_NOT_FOUND);
 	}
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fa4ef0 and 881521e.

📒 Files selected for processing (4)
  • src/main/java/com/pinback/pinback_server/domain/notification/domain/repository/PushSubscriptionRepository.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/notification/domain/service/PushSubscriptionGetService.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/notification/exception/SubscriptionNotFoundException.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/global/exception/constant/ExceptionCode.java (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/com/pinback/pinback_server/global/exception/constant/ExceptionCode.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/pinback/pinback_server/domain/notification/domain/service/PushSubscriptionGetService.java
  • src/main/java/com/pinback/pinback_server/domain/notification/domain/repository/PushSubscriptionRepository.java
🔇 Additional comments (1)
src/main/java/com/pinback/pinback_server/domain/notification/exception/SubscriptionNotFoundException.java (1)

1-10: Well-structured exception implementation.

The exception class follows good Java practices by extending the application's base exception and using a specific error code. The implementation is clean and fits well within the notification domain architecture.

@ose0221 ose0221 self-requested a review July 13, 2025 20:04
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/main/resources/application.yml (2)

33-33: Add missing trailing newline to pass YAML linting

Static analysis (new-line-at-end-of-file) fails because the file ends immediately after the fcm property. Add a single newline to avoid tooling / CI noise.

-fcm: ${FCM_JSON}
+fcm: ${FCM_JSON}
+

31-33: Reconsider placing bare fcm at the root level

Dropping the property at the top level deviates from the existing namespacing style (spring.*, springdoc.*, jwt.*).
Two follow-ups worth weighing:

  1. Namespace it to avoid future collisions:
    notification: or firebase:credentials-json: ${FCM_JSON}

  2. Explicitly quote the value if it ever contains braces/colons (e.g. a JSON string or base64):
    fcm: "${FCM_JSON}"

Neither change is strictly required for runtime correctness today, but both harden the configuration against surprises.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 881521e and 7fcb62a.

📒 Files selected for processing (2)
  • src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java (3 hunks)
  • src/main/resources/application.yml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java
🧰 Additional context used
🪛 YAMLlint (1.37.1)
src/main/resources/application.yml

[error] 33-33: no new line character at the end of file

(new-line-at-end-of-file)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7fcb62a and 49e7594.

📒 Files selected for processing (2)
  • src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java (1 hunks)
  • src/test/resources/application-test.properties (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/test/resources/application-test.properties
🧰 Additional context used
🧠 Learnings (1)
src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java (1)
Learnt from: rootTiket
PR: Pinback-Team/pinback-server#18
File: src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:38-41
Timestamp: 2025-07-07T15:33:02.748Z
Learning: In the Pinback server project, article detail retrieval (getArticleDetail method) does not require user authorization checks. Articles can be accessed by ID without ownership verification as per the project's design decision.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

assertThat(article.getRemindAt()).isEqualTo(command.remindTime());
assertThat(article.getIsRead()).isFalse();
}
//TODO: 나중에 firebase mocking 처리해서 주석 해제 할 것
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add tracking issue for Firebase mocking implementation.

The TODO comment indicates this is a temporary solution, but there's no clear timeline or tracking mechanism for when this will be addressed.

Consider creating a GitHub issue to track the Firebase mocking implementation and link it in the TODO comment:

-//TODO: 나중에 firebase mocking 처리해서 주석 해제 할 것
+//TODO: Implement Firebase mocking and restore test coverage (Track: Issue #XX)

This ensures the technical debt is properly tracked and prioritized.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//TODO: 나중에 firebase mocking 처리해서 주석 해제 할 것
//TODO: Implement Firebase mocking and restore test coverage (Track: Issue #XX)
🤖 Prompt for AI Agents
In
src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java
at line 43, the TODO comment about Firebase mocking lacks a tracking reference.
Create a GitHub issue to track the Firebase mocking implementation and update
the TODO comment to include a link or reference to this issue, ensuring the
technical debt is documented and prioritized.

Comment on lines +43 to +63
//TODO: 나중에 firebase mocking 처리해서 주석 해제 할 것

// @DisplayName("사용자는 아티클을 생성할 수 있다.")
// @Test
// void articleSaveService() {
// User user = userRepository.save(user());
// Category category = categoryRepository.save(category(user));
// ArticleCreateCommand command = new ArticleCreateCommand("testUrl", category.getId()
// , "테스트메모",
// LocalDateTime.of(2025, 8, 6, 0, 0, 0));
// //when
// articleManagementUsecase.createArticle(user, command);
//
// //then
// Article article = articleRepository.findById(1L).get();
// assertThat(article.getUrl()).isEqualTo(command.url());
// assertThat(article.getMemo()).isEqualTo(command.memo());
// assertThat(article.getCategory()).isEqualTo(category);
// assertThat(article.getRemindAt()).isEqualTo(command.remindTime());
// assertThat(article.getIsRead()).isFalse();
// }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical: Restore test coverage with proper Firebase mocking.

Commenting out the core article creation test significantly reduces test coverage for a critical feature. This test was validating essential functionality including reminder time handling, which is central to the new notification feature.

The test should be restored immediately with proper Firebase mocking. Consider using:

  • @MockBean for Firebase services in the test context
  • Test containers or embedded Firebase emulator for integration testing
  • Conditional mocking based on test profiles
+@MockBean
+private FirebaseMessaging firebaseMessaging;
+
+@MockBean 
+private NotificationScheduleService notificationScheduleService;

-//TODO: 나중에 firebase mocking 처리해서 주석 해제 할 것
-
-// @DisplayName("사용자는 아티클을 생성할 수 있다.")
-// @Test
-// void articleSaveService() {
+@DisplayName("사용자는 아티클을 생성할 수 있다.")
+@Test
+void articleSaveService() {
+    // Given
+    when(notificationScheduleService.scheduleNotification(any(), any())).thenReturn(true);
+    
     User user = userRepository.save(user());
     Category category = categoryRepository.save(category(user));
     ArticleCreateCommand command = new ArticleCreateCommand("testUrl", category.getId()
         , "테스트메모",
         LocalDateTime.of(2025, 8, 6, 0, 0, 0));
-//   //when
+    //when
     articleManagementUsecase.createArticle(user, command);
-//
-// 	//then
+
+    //then
     Article article = articleRepository.findById(1L).get();
     assertThat(article.getUrl()).isEqualTo(command.url());
     assertThat(article.getMemo()).isEqualTo(command.memo());
     assertThat(article.getCategory()).isEqualTo(category);
     assertThat(article.getRemindAt()).isEqualTo(command.remindTime());
     assertThat(article.getIsRead()).isFalse();
-// }
+    
+    // Verify notification scheduling was called
+    verify(notificationScheduleService).scheduleNotification(any(), any());
+}

Would you like me to help create a comprehensive mocking strategy for the Firebase integration?

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//TODO: 나중에 firebase mocking 처리해서 주석 해제 할 것
// @DisplayName("사용자는 아티클을 생성할 수 있다.")
// @Test
// void articleSaveService() {
// User user = userRepository.save(user());
// Category category = categoryRepository.save(category(user));
// ArticleCreateCommand command = new ArticleCreateCommand("testUrl", category.getId()
// , "테스트메모",
// LocalDateTime.of(2025, 8, 6, 0, 0, 0));
// //when
// articleManagementUsecase.createArticle(user, command);
//
// //then
// Article article = articleRepository.findById(1L).get();
// assertThat(article.getUrl()).isEqualTo(command.url());
// assertThat(article.getMemo()).isEqualTo(command.memo());
// assertThat(article.getCategory()).isEqualTo(category);
// assertThat(article.getRemindAt()).isEqualTo(command.remindTime());
// assertThat(article.getIsRead()).isFalse();
// }
// add mocks for Firebase integration
@MockBean
private FirebaseMessaging firebaseMessaging;
@MockBean
private NotificationScheduleService notificationScheduleService;
@DisplayName("사용자는 아티클을 생성할 수 있다.")
@Test
void articleSaveService() {
// Given
when(notificationScheduleService.scheduleNotification(any(), any())).thenReturn(true);
User user = userRepository.save(user());
Category category = categoryRepository.save(category(user));
ArticleCreateCommand command = new ArticleCreateCommand(
"testUrl",
category.getId(),
"테스트메모",
LocalDateTime.of(2025, 8, 6, 0, 0, 0)
);
// when
articleManagementUsecase.createArticle(user, command);
// then
Article article = articleRepository.findById(1L).get();
assertThat(article.getUrl()).isEqualTo(command.url());
assertThat(article.getMemo()).isEqualTo(command.memo());
assertThat(article.getCategory()).isEqualTo(category);
assertThat(article.getRemindAt()).isEqualTo(command.remindTime());
assertThat(article.getIsRead()).isFalse();
// Verify notification scheduling was called
verify(notificationScheduleService).scheduleNotification(any(), any());
}
🤖 Prompt for AI Agents
In
src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java
between lines 43 and 63, the core article creation test is commented out due to
missing Firebase mocking, which reduces test coverage. To fix this, restore the
test by implementing proper Firebase mocking using @MockBean annotations for
Firebase services in the test context. Alternatively, set up a Firebase emulator
or test containers for integration tests. Ensure the Firebase dependencies are
mocked or emulated so the test can run successfully and validate the article
creation and reminder time handling.

@rootTiket rootTiket merged commit 05abe5d into dev Jul 13, 2025
5 checks passed
@rootTiket rootTiket deleted the feat/#48 branch July 13, 2025 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 리마인드 기능 추가

2 participants