Skip to content

Conversation

@char-yb
Copy link
Member

@char-yb char-yb commented Dec 28, 2025

๐ŸŒฑ ๊ด€๋ จ ์ด์Šˆ

๐Ÿ“Œ ์ž‘์—… ๋‚ด์šฉ ๋ฐ ํŠน์ด ์‚ฌํ•ญ

  • FCM APN ์ปค์Šคํ…€ ๋ฐ์ดํ„ฐ key-value๋กœ destination ์ •์˜
  • Notification ๋„๋ฉ”์ธ ๋ฐ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ •์˜
  • ์•Œ๋ฆผํ•จ ์š”๊ตฌ ์‹œ ํ•„์š”ํ•œ Cursor ๊ธฐ๋ฐ˜ ์ธํ”ผ๋‹ˆํ‹ฐ ์Šคํฌ๋กค ์š”๊ตฌ์‚ฌํ•ญ ๋ฐ˜์˜
  • ์ž ์ˆ˜ํ•จ ํŒจ์น˜๋กœ Tx ํ™•์žฅํ•จ์ˆ˜ ์ถ”๊ฐ€

๐Ÿ“ ์ฐธ๊ณ 

๐Ÿ“Œ ์ฒดํฌ ๋ฆฌ์ŠคํŠธ

  • ๋ฆฌ๋ทฐ์–ด๋ฅผ ์ถ”๊ฐ€ํ•˜์…จ๋‚˜์š” ?
  • ๋ณ€๊ฒฝ์‚ฌํ•ญ์— ๋Œ€ํ•ด ์ถฉ๋ถ„ํžˆ ์„ค๋ช…ํ•˜๊ณ  ์žˆ๋‚˜์š” ?

Summary by CodeRabbit

Release Notes

  • New Features

    • Notification management system with storage, retrieval, and read status tracking
    • Firebase Cloud Messaging enhanced with destination field support
    • Cursor-based pagination for notification queries with size validation (1-100)
  • Refactor

    • Consolidated transaction management with unified API
    • Simplified AWS S3 client imports
    • Removed lazy application initialization for faster startup

โœ๏ธ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 28, 2025

๐Ÿ“ Walkthrough

Walkthrough

This PR introduces a notification storage system with cursor-based pagination, enhances FCM push notifications with destination metadata propagation, refactors transaction management into a centralized Tx utility, and performs minor code cleanup including AWS S3 import consolidation and lazy initialization config removal.

Changes

Cohort / File(s) Summary
AWS S3 Client Import Cleanup
pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3Client.kt
Replaced wildcard import software.amazon.awssdk.services.s3.model.* with explicit imports: GetObjectRequest, ListObjectsV2Request, ListObjectsV2Response, PutObjectRequest, S3Object. No behavioral changes.
FCM Notification Destination Field
pida-clients/notification/src/main/kotlin/com/pida/client/notification/FcmSendRequest.kt, FcmClientRepository.kt, FirebaseCloudMessageSender.kt
Added destination: String field to FcmSendRequest data class and propagated through FCM send flows. Updated FirebaseCloudMessageSender.sendEachForMulticastAll() signature to accept destination parameter. Modified APNS alert content from combined title/body to body-only, with destination added as custom APNS data.
Notification Storage Core Domain
pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationStored.kt, CreateNotificationStoredCommand.kt, NotificationType.kt, ReadStatus.kt, FirebaseCloudMessage.kt, NewFirebaseCloudMessage.kt
Introduced sealed class NotificationStored with nested Create, Info, Result data classes. Added enums NotificationType (ADMIN\_PUSH, REGULAR) and ReadStatus (READ, UNREAD) with factory methods. Extended FirebaseCloudMessage and NewFirebaseCloudMessage with destination: String field.
Notification Storage Services
pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationService.kt, NotificationStoredAppender.kt, NotificationStoredReader.kt, NotificationStoredUpdater.kt, NotificationStoredKeyGenerator.kt
Added NotificationService with methods: append(), appendAll(), findAllNotifications(), markAsRead(). Created helper components NotificationStoredAppender, NotificationStoredReader, NotificationStoredUpdater delegating to repository. Implemented NotificationStoredKeyGenerator composing keys as yyyyMMdd_NHK_<UUID>.
Notification Storage Repository Interface
pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationStoredRepository.kt
Defined repository interface with CRUD and query methods: save(), saveAll(), findById(), findByUserIdAndReadStatus(), findAllBy() (cursor-based), countByUserIdAndReadStatus(), markAsRead().
Support Domain Models: Pagination
pida-core/core-domain/src/main/kotlin/com/pida/support/page/Page.kt, PageResponse.kt, OffsetLimit.kt, OffsetPageRequest.kt, Sort.kt, SortType.kt
Introduced pagination models: Page<T> with validation; PageResponse<Data> for API responses; OffsetLimit with constraints (offset โ‰ฅ 0, limit > 0); OffsetPageRequest (page โ‰ฅ 0, size โˆˆ [1,100]); enums Sort (DESC, ASC) and SortType (NEW with displayName). All validation messages in Korean.
Support Domain Models: Cursor
pida-core/core-domain/src/main/kotlin/com/pida/support/cursor/Cursor.kt, CursorRequest.kt
Added Cursor<T> generic data class with content, nextCursor, size and factory of() with validation. Created CursorRequest with lastId (nullable) and size (โˆˆ [1,100]) with Korean validation error messages.
Transaction Management Refactoring
pida-core/core-domain/src/main/kotlin/com/pida/support/tx/Tx.kt (new), TxAdvice.kt (removed)
Replaced standalone TxAdvice component with unified Tx utility providing static-style transactional helpers via companion object: writeable(), readable(), requiresNew(), and coroutine counterparts (coWriteable(), coReadable(), coRequiresNew()). Delegates to injected TxAdvice inner class managing three TransactionTemplate instances (write, requiresNew, readOnly).
Repository Migration to Tx Utility
pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/blooming/BloomingCoreRepository.kt, user/UserCoreRepository.kt
Removed dependencies on individual TxAdvice and TransactionTemplates classes. Replaced all transaction wrapping with Tx.writeable(), Tx.readable(), Tx.coReadable(), Tx.coWriteable() calls. Updated method body control flow labels accordingly. Minor signature update in BloomingCoreRepository: renamed findTopByUserIdAndFlowerSpotIdOrderByCreatedAtDesc() โ†’ findTopByUserIdAndSpotIdDecs().
Notification Storage Database Layer
pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredEntity.kt, NotificationStoredJpaRepository.kt, NotificationStoredCustomRepository.kt, NotificationStoredCoreRepository.kt
Implemented NotificationStoredEntity (JPA-mapped to t_notification_stored table) with enums stored as STRING. Created NotificationStoredJpaRepository extending JpaRepository and KotlinJdslJpqlExecutor with query methods. Implemented NotificationStoredCustomRepository for cursor-based queries excluding NotificationType.REGULAR. Implemented NotificationStoredCoreRepository orchestrating persistence, retrieval, and read-status updates via transaction boundaries.
Configuration
pida-core/core-api/src/main/resources/application.yml
Removed spring.main.lazy-initialization: true property; application now uses eager initialization by default.
Core Domain Integration
pida-core/core-domain/src/main/kotlin/com/pida/notification/FcmSender.kt
Updated FirebaseCloudMessage instantiation to include destination = it.destination when mapping from NewFirebaseCloudMessage.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant NotificationService
    participant NotificationStoredAppender
    participant NotificationStoredRepository
    participant NotificationStoredCoreRepository
    participant Tx
    participant Database as NotificationStoredEntity

    Client->>NotificationService: appendAll(List<CreateNotificationStoredCommand>)
    NotificationService->>NotificationService: Generate key for each command
    NotificationService->>NotificationService: Map to NotificationStored.Create
    NotificationService->>NotificationStoredAppender: appendAll(List<Create>)
    NotificationStoredAppender->>NotificationStoredRepository: saveAll(List<Create>)
    NotificationStoredRepository->>NotificationStoredCoreRepository: saveAll(List<Create>)
    NotificationStoredCoreRepository->>Tx: writeable { ... }
    Tx->>Database: Persist NotificationStoredEntity records
    Database-->>Tx: Entities saved
    Tx-->>NotificationStoredCoreRepository: Success
    NotificationStoredCoreRepository-->>NotificationStoredRepository: List<NotificationStored.Info>
    NotificationStoredRepository-->>NotificationStoredAppender: List<NotificationStored.Info>
    NotificationStoredAppender-->>NotificationService: List<NotificationStored.Info>
    NotificationService-->>Client: List<NotificationStored.Info>

    rect rgb(230, 240, 255)
    Note over Client,Database: Cursor-based retrieval flow
    Client->>NotificationService: findAllNotifications(userId, CursorRequest)
    NotificationService->>NotificationStoredReader: findAllBy(userId, CursorRequest)
    NotificationStoredReader->>NotificationStoredRepository: findAllBy(userId, CursorRequest)
    NotificationStoredRepository->>NotificationStoredCoreRepository: findAllBy(userId, CursorRequest)
    NotificationStoredCoreRepository->>Tx: readable { ... }
    Tx->>Database: Query with cursor pagination (excludes REGULAR type)
    Database-->>Tx: Cursor<NotificationStored.Info>
    Tx-->>NotificationStoredCoreRepository: Cursor<NotificationStored.Info>
    NotificationStoredCoreRepository-->>NotificationStoredRepository: Cursor<NotificationStored.Info>
    NotificationStoredRepository-->>NotificationStoredReader: Cursor<NotificationStored.Info>
    NotificationStoredReader-->>NotificationService: Cursor<NotificationStored.Info>
    NotificationService-->>Client: Cursor<NotificationStored.Info>
    end

    rect rgb(240, 255, 240)
    Note over Client,Database: Mark as read flow
    Client->>NotificationService: markAsRead(notificationId)
    NotificationService->>NotificationStoredUpdater: markAsRead(notificationId)
    NotificationStoredUpdater->>NotificationStoredRepository: markAsRead(notificationId)
    NotificationStoredRepository->>NotificationStoredCoreRepository: markAsRead(notificationId)
    NotificationStoredCoreRepository->>Tx: writeable { ... }
    Tx->>Database: Update readStatus to READ
    Database-->>Tx: Success
    Tx-->>NotificationStoredCoreRepository: void
    NotificationStoredCoreRepository-->>NotificationStoredRepository: void
    NotificationStoredRepository-->>NotificationStoredUpdater: void
    NotificationStoredUpdater-->>NotificationService: void
    NotificationService-->>Client: void
    end
Loading

Estimated code review effort

๐ŸŽฏ 4 (Complex) | โฑ๏ธ ~65 minutes

Possibly related PRs

Poem

๐Ÿฐ A warren of changes, hop by hopโ€”
Notifications stored in the DB crop,
Cursors dance through pages anew,
Transactions align, destinations too!
FCM sends its field-enriched call, ๐Ÿš€

Pre-merge checks and finishing touches

โŒ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
โœ… Passed checks (4 passed)
Check name Status Explanation
Description Check โœ… Passed Check skipped - CodeRabbitโ€™s high-level summary is enabled.
Title check โœ… Passed The title '[Refactor&Feature] FCM Sender & Notification Domain' clearly summarizes the main changes: refactoring FCM sender logic and introducing a new notification domain.
Linked Issues check โœ… Passed The PR implements all objectives from issue #69: improves FCM notification logic by adding destination field [FirebaseCloudMessageSender.kt, FcmSendRequest.kt, NewFirebaseCloudMessage.kt], persists notification data [NotificationService, NotificationStored, repository implementation], and implements cursor-based pagination [Cursor.kt, CursorRequest.kt].
Out of Scope Changes check โœ… Passed Additional changes beyond core notification requirements include: AWS S3 import optimization (narrowing wildcard imports), transaction abstraction refactoring (Tx component), page/cursor support classes, and removal of lazy initialization config. All changes support the notification feature or improve technical implementation.
โœจ Finishing touches
  • ๐Ÿ“ Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

โค๏ธ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 9

Caution

Some comments are outside the diff and canโ€™t be posted inline due to platform limitations.

โš ๏ธ Outside diff range comments (1)
pida-clients/notification/src/main/kotlin/com/pida/client/notification/FcmClientRepository.kt (1)

24-30: Critical: Batch send uses only first message's content.

The multicast send on lines 26-28 uses fcmRequests[0].title, fcmRequests[0].body, and fcmRequests[0].destination for all messages in the batch. If the list contains messages with different content, all recipients will receive the first message's content instead of their intended messages.

๐Ÿ”Ž Recommended fix

If all messages in a batch should have identical content, validate this assumption:

     val tokens = fcmRequests.map { it.fcmToken }
+    // Validate all messages have identical content for multicast
+    require(fcmRequests.all { it.title == fcmRequests[0].title && 
+                               it.body == fcmRequests[0].body && 
+                               it.destination == fcmRequests[0].destination }) {
+        "Multicast requires all messages to have identical content"
+    }
     val batchResult =
         firebaseCloudMessageSender.sendEachForMulticastAll(

Or, if messages can differ, send them individually instead of as a multicast.

๐Ÿงน Nitpick comments (16)
pida-core/core-domain/src/main/kotlin/com/pida/support/tx/Tx.kt (2)

50-73: Design inconsistency: templates unused by synchronous methods.

The writeTemplate, requiresNewTemplate, and readOnlyTemplate are created but only used by the coroutine methods (coWrite, coRequiresNew, coReadOnly). The synchronous methods (write, requiresNew, readOnly) rely solely on @Transactional annotations.

This works, but the two approaches could diverge in behavior (e.g., isolation level, timeout settings). Consider unifying:

  • Either use TransactionTemplate.execute() for synchronous methods too, or
  • Document why the dual approach is intentional.

13-23: The thread safety concern is overstated; however, there is a code duplication issue requiring attention.

The lateinit var txAdvice pattern is not as risky as initially suggested. Spring creates a single Tx bean (singleton by default), so the init block executes once, after which txAdvice is only read. The actual risk is minimal: a thread calling Tx.writeable() before the Spring context fully initializes could encounter UninitializedPropertyAccessException, but since these methods are called from repository beans (which won't execute until the context is ready), this is unlikely in practice.

However, there is a significant design issue: TxAdvice is defined in two placesโ€”both as a nested class in Tx.kt and as a standalone @Component in TxAdvice.kt. These are identical and will both be instantiated by Spring, creating redundancy and potential confusion. Remove one of these definitions to eliminate duplication.

The coExecute() extension is properly defined in TransactionTemplates.kt and the templates are used in the coroutine methods, so those earlier concerns do not apply.

pida-core/core-domain/src/main/kotlin/com/pida/notification/FcmToUserCommand.kt (1)

3-7: Consider consolidating with NewFcmToUser or clarifying the distinction.

FcmToUserCommand appears to be a duplicate of NewFcmToUser (both have userKey, title, body fields). If these serve different purposes in the message flow, consider renaming them to clarify their roles (e.g., FcmUserMessageCommand vs FcmUserMessageDto). Otherwise, consolidate to avoid duplication.

pida-core/core-domain/src/main/kotlin/com/pida/support/cursor/CursorRequest.kt (1)

5-5: Consider using Int for size instead of Long.

Since size is constrained to 1..100, using Long is unnecessary. Int is more appropriate for count-based pagination parameters and aligns with typical JVM collection APIs.

๐Ÿ”Ž Proposed refactor
 data class CursorRequest(
     val lastId: Long?,
-    val size: Long,
+    val size: Int,
 ) {
     init {
         require(size in 1..100) { "ํฌ๊ธฐ๋Š” 1 ~ 100 ์‚ฌ์ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค." }
     }
 }
pida-core/core-domain/src/main/kotlin/com/pida/support/page/PageResponse.kt (1)

8-14: The factory method adds no value over the constructor.

The of() method simply delegates to the constructor without validation or transformation. While this may provide consistency with Page.of() and Cursor.of() (which do validate), direct constructor usage with default parameters is clearer when no validation is needed.

pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationType.kt (1)

8-15: Simplify using Kotlin's built-in enum utilities.

The manual when expression duplicates valueOf() functionality and requires updates when adding new enum values. Consider using valueOf() or entries.find().

๐Ÿ”Ž Proposed refactor options

Option 1: Use valueOf() with try-catch

         fun of(type: String): NotificationType =
-            when (type) {
-                "ADMIN_PUSH" -> ADMIN_PUSH
-                "REGULAR" -> REGULAR
-                else -> throw IllegalArgumentException("Unknown NotificationType: $type")
-            }
+            try {
+                valueOf(type)
+            } catch (e: IllegalArgumentException) {
+                throw IllegalArgumentException("Unknown NotificationType: $type", e)
+            }

Option 2: Use entries.find() for custom error handling

         fun of(type: String): NotificationType =
-            when (type) {
-                "ADMIN_PUSH" -> ADMIN_PUSH
-                "REGULAR" -> REGULAR
-                else -> throw IllegalArgumentException("Unknown NotificationType: $type")
-            }
+            entries.find { it.name == type }
+                ?: throw IllegalArgumentException("Unknown NotificationType: $type")
pida-core/core-domain/src/main/kotlin/com/pida/support/page/SortType.kt (1)

3-7: Clarify sort direction in displayName.

The enum entry NEW("id") doesn't clearly indicate the sort direction. Consider using a more descriptive displayName like "id_desc" or "newest" to clarify that this represents newest-first (descending ID) ordering.

pida-core/core-domain/src/main/kotlin/com/pida/support/page/OffsetLimit.kt (1)

14-21: Consider removing the redundant factory method.

The of() factory method duplicates the primary constructor exactlyโ€”same parameters, same defaults, same behavior. In Kotlin, named/default arguments make such companion factories unnecessary unless they provide additional logic (e.g., caching, validation, or alternative construction paths).

pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationStored.kt (2)

3-39: Nested classes do not extend the sealed class.

The data classes Create, Info, and Result are nested inside NotificationStored but don't extend it. This means NotificationStored as a sealed class provides no polymorphic benefitsโ€”you cannot use when exhaustiveness checks or treat these as subtypes. If polymorphism isn't needed, consider using a simple object or companion object as a namespace instead.

If you intend to leverage sealed class benefits:

 sealed class NotificationStored {
-    data class Create(
+    data class Create(...) : NotificationStored()

Otherwise, a namespace object suffices:

object NotificationStored {
    data class Create(...)
    data class Info(...)
    data class Result(...)
}

23-23: Consider using a temporal type for createdAt.

Using String for createdAt loses type safety and requires parsing at usage sites. If this is a domain model, consider java.time.Instant or LocalDateTime. If it's intentionally a formatted string for API responses, this is acceptable but worth documenting.

Also applies to: 37-37

pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationStoredAppender.kt (1)

11-11: Add explicit return type for consistency.

append() explicitly declares : NotificationStored.Info, but appendAll() relies on type inference. For public API consistency and clarity, consider adding the explicit return type.

๐Ÿ”Ž Proposed fix
-    fun appendAll(createAll: List<NotificationStored.Create>) = notificationStoredRepository.saveAll(createAll)
+    fun appendAll(createAll: List<NotificationStored.Create>): List<NotificationStored.Info> = notificationStoredRepository.saveAll(createAll)
pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationStoredKeyGenerator.kt (1)

18-18: Consider using explicit timezone for consistency.

LocalDate.now() uses the system default timezone, which can vary across servers or environments. For consistent key generation in distributed systems, consider using an explicit timezone:

๐Ÿ”Ž Proposed fix
-    private fun generateDate(): String = FORMAT_YYYYMMDD.format(LocalDate.now())
+    private fun generateDate(): String = FORMAT_YYYYMMDD.format(LocalDate.now(java.time.ZoneOffset.UTC))
pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationStoredRepository.kt (1)

6-29: Consider adding KDoc comments to the repository interface.

The repository interface lacks documentation for its public methods. Adding KDoc comments would clarify:

  • Expected behavior when entities are not found (e.g., findById)
  • The semantics of countByUserIdAndReadStatus when readStatus is null
  • Whether markAsRead is idempotent and what happens if the notification doesn't exist
pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredEntity.kt (2)

16-23: Specify explicit column lengths for String fields.

The String fields notificationStoredKey, parameterValue, topic, and contents lack explicit @Column(length = ...) annotations. This can lead to:

  • Database portability issues (different defaults across DB vendors)
  • Potential VARCHAR(255) truncation for longer content
  • Unclear schema intent for other developers

Consider adding explicit length constraints based on expected data sizes, or use @Lob for large text fields like contents.

๐Ÿ”Ž Example with explicit lengths
+    @Column(length = 100)
     val notificationStoredKey: String,
     val userId: Long,
     @Enumerated(value = EnumType.STRING)
     @Column(length = 30)
     val type: NotificationType,
+    @Column(length = 500)
     val parameterValue: String,
+    @Column(length = 100)
     val topic: String,
+    @Lob
     val contents: String,

Adjust lengths based on actual requirements.


48-48: Consider using a consistent date format for createdAt.

Line 48 converts createdAt using .toString(), which produces JVM-default formatting (e.g., 2025-12-28T14:04:40.123). For API consistency, consider:

  • Using ISO-8601 format explicitly (DateTimeFormatter.ISO_DATE_TIME)
  • Converting to a specific timezone (UTC)
  • Keeping it as a structured type rather than String

This ensures predictable serialization across different JVM locales and timezones.

pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationService.kt (1)

21-34: Consider batch key generation for better performance.

Lines 23-33 call notificationStoredKeyGenerator.generate() inside a map operation, generating keys one-by-one for bulk operations. If the key generator has any I/O, synchronization, or expensive operations, this could be inefficient for large batches.

Consider adding a batch key generation method to NotificationStoredKeyGenerator:

interface NotificationStoredKeyGenerator {
    fun generate(): String
    fun generateBatch(count: Int): List<String> = List(count) { generate() }
}

Then use it:

fun appendAll(createAll: List<CreateNotificationStoredCommand>): List<NotificationStored.Info> {
    val keys = notificationStoredKeyGenerator.generateBatch(createAll.size)
    return notificationStoredAppender.appendAll(
        createAll.mapIndexed { index, notification ->
            NotificationStored.Create(
                notificationStoredKey = keys[index],
                // ... rest of fields
            )
        }
    )
}

This is a minor optimization and may not be necessary if key generation is already lightweight.

๐Ÿ“œ Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 914716e and 4ca15bf.

๐Ÿ“’ Files selected for processing (32)
  • pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3Client.kt
  • pida-clients/notification/src/main/kotlin/com/pida/client/notification/FcmClientRepository.kt
  • pida-clients/notification/src/main/kotlin/com/pida/client/notification/FcmSendRequest.kt
  • pida-clients/notification/src/main/kotlin/com/pida/client/notification/FirebaseCloudMessageSender.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/notification/CreateNotificationStoredCommand.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/notification/FcmSender.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/notification/FcmToUserCommand.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/notification/FirebaseCloudMessage.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/notification/FirebaseCloudMessageCommand.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/notification/NewFirebaseCloudMessage.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationService.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationStored.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationStoredAppender.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationStoredKeyGenerator.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationStoredReader.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationStoredRepository.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationStoredUpdater.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationType.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/notification/ReadStatus.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/cursor/Cursor.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/cursor/CursorRequest.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/page/OffsetLimit.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/page/OffsetPageRequest.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/page/Page.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/page/PageResponse.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/page/Sort.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/page/SortType.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/tx/Tx.kt
  • pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredCoreRepository.kt
  • pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredCustomRepository.kt
  • pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredEntity.kt
  • pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredJpaRepository.kt
๐Ÿงฐ Additional context used
๐Ÿงฌ Code graph analysis (2)
pida-core/core-domain/src/main/kotlin/com/pida/notification/NewFirebaseCloudMessage.kt (4)
pida-clients/notification/src/main/kotlin/com/pida/client/notification/FcmSendResponse.kt (1)
  • fcmToken (3-8)
pida-core/core-domain/src/main/kotlin/com/pida/notification/NewFcmToUser.kt (1)
  • userKey (3-7)
pida-clients/notification/src/main/kotlin/com/pida/client/notification/FcmProperties.kt (1)
  • prefix (5-8)
pida-core/core-domain/src/main/kotlin/com/pida/notification/FcmToUserKey.kt (1)
  • key (3-5)
pida-core/core-domain/src/main/kotlin/com/pida/notification/FirebaseCloudMessage.kt (4)
pida-clients/notification/src/main/kotlin/com/pida/client/notification/FcmProperties.kt (1)
  • prefix (5-8)
pida-core/core-domain/src/main/kotlin/com/pida/notification/NewFcmToUser.kt (1)
  • userKey (3-7)
pida-clients/notification/src/main/kotlin/com/pida/client/notification/FcmSendResponse.kt (1)
  • fcmToken (3-8)
pida-core/core-domain/src/main/kotlin/com/pida/notification/FcmToUserKey.kt (1)
  • key (3-5)
โฐ 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: CI (21, 2.1.0, corretto)
๐Ÿ”‡ Additional comments (26)
pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3Client.kt (1)

5-9: LGTM! Explicit imports improve code clarity.

Replacing the wildcard import with explicit imports is a good practice that makes dependencies clearer and reduces namespace pollution. All imported classes are properly used throughout the file.

Minor note: This change appears unrelated to the PR's FCM notification improvements. If this was an incidental cleanup or IDE auto-fix, consider grouping unrelated refactors separately in future PRs for clearer change tracking.

pida-core/core-domain/src/main/kotlin/com/pida/support/page/OffsetPageRequest.kt (1)

3-11: LGTM! Well-structured pagination request object.

The data class design is clean and follows good practices:

  • Appropriate validation bounds (page >= 0, size 1..100)
  • Fail-fast validation in the init block
  • Consistent with the existing CursorRequest pattern
pida-core/core-domain/src/main/kotlin/com/pida/support/tx/Tx.kt (4)

24-44: LGTM on the companion object API.

The static-style helper methods provide a clean, ergonomic API for transaction management. Defaulting coroutine variants to Dispatchers.IO is appropriate for blocking JDBC calls.


60-64: PROPAGATION_SUPPORTS may not provide transactional guarantees.

With PROPAGATION_SUPPORTS, if no outer transaction exists, the block runs non-transactionally. This means:

  • No snapshot isolation or repeatable reads
  • No connection reuse within the block

If consistent reads are required, consider using PROPAGATION_REQUIRED with isReadOnly = true instead.


66-73: LGTM on @Transactional method design.

The synchronous transactional methods are called through the injected TxAdvice bean reference, ensuring Spring's AOP proxy intercepts the calls correctly.


75-88: The TransactionTemplate.coExecute extension function is properly defined in pida-core/core-domain/src/main/kotlin/com/pida/support/tx/TransactionTemplates.kt (lines 35-41) and is widely used throughout the codebase. The code will compile without issues.

pida-core/core-domain/src/main/kotlin/com/pida/support/page/Sort.kt (1)

3-6: LGTM!

Standard sorting direction enum with clear, conventional naming.

pida-core/core-domain/src/main/kotlin/com/pida/notification/CreateNotificationStoredCommand.kt (1)

3-11: LGTM!

The command object structure is clean and the default value ReadStatus.UNREAD for new notifications is appropriate.

pida-clients/notification/src/main/kotlin/com/pida/client/notification/FcmSendRequest.kt (1)

7-7: LGTM!

The destination field addition enables propagation of destination information through FCM flows.

pida-core/core-domain/src/main/kotlin/com/pida/notification/FcmSender.kt (1)

25-25: LGTM!

The destination field is correctly propagated during the mapping from NewFirebaseCloudMessage to FirebaseCloudMessage.

pida-core/core-domain/src/main/kotlin/com/pida/notification/ReadStatus.kt (1)

3-16: LGTM!

The enum structure and factory method follow best practices with proper error handling for invalid input.

pida-clients/notification/src/main/kotlin/com/pida/client/notification/FcmClientRepository.kt (3)

39-39: LGTM!

The destination field is correctly propagated in the result construction for batch sends.


53-53: LGTM!

The destination field is correctly included in the single-send request construction.


61-61: LGTM!

The destination field is correctly propagated in the result construction for single sends.

pida-core/core-domain/src/main/kotlin/com/pida/notification/NewFirebaseCloudMessage.kt (1)

7-7: LGTM!

The destination field addition aligns with the broader refactoring to propagate destination information throughout the FCM messaging flow.

pida-clients/notification/src/main/kotlin/com/pida/client/notification/FirebaseCloudMessageSender.kt (2)

39-45: Verify intentional platform inconsistency in notification display.

The Notification builder sets both title and body (Lines 32-33), which Android will display. However, the APNS alert is set to only request.body (Line 42), meaning iOS users see only the body text without the title. This creates an inconsistent cross-platform experience.

If this is intentional (e.g., iOS design preference), consider adding a comment. Otherwise, restore the title for iOS:

๐Ÿ”Ž Potential fix to include title in iOS alert
                     .setAps(
                         Aps
                             .builder()
-                            .setAlert(request.body)
+                            .setAlert("${request.title}\n${request.body}")
                             .setBadge(1)

70-76: Same platform inconsistency applies to multicast messages.

The multicast path also sets APNS alert to only body (Line 73) while the Notification includes both title and body. Ensure this aligns with the intended UX for iOS.

pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationStoredUpdater.kt (1)

9-9: Verify authorization is handled upstream.

markAsRead accepts only notificationId without userId, which means any notification can be marked as read if the ID is known. Ensure the calling service layer validates that the requesting user owns the notification before invoking this method.

pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationStoredReader.kt (1)

7-15: LGTM!

Clean delegation to the repository with explicit types. Follows the established pattern of the other storage components.

pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredJpaRepository.kt (1)

15-18: This method appears to be unusedโ€”no callers found in the codebase.

The method countByUserIdAndReadStatus is defined across the repository layers but has no invocations. If this is dead code, consider removing it. If it's intended for future use, document the purpose and expected behavior (particularly regarding how null readStatus should be handled in the derived query).

Note: There's also an inconsistency with findByUserIdAndReadStatus, which uses non-nullable readStatus: ReadStatus, while countByUserIdAndReadStatus accepts ReadStatus?. If both methods are retained, clarify the contractโ€”especially whether null means "count all statuses" or "count only IS NULL rows" in Spring Data's derived query interpretation.

pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationStoredRepository.kt (1)

28-28: LGTM! Idempotent markAsRead design is appropriate.

The markAsRead method returns Unit, which is acceptable given the implementation in NotificationStoredCoreRepository shows it's idempotent (early returns if already read). This design simplifies callers who don't need to handle the already-read case.

pida-core/core-domain/src/main/kotlin/com/pida/notification/NotificationService.kt (2)

14-19: Verify key overwrite behavior in append method.

Line 16-17 unconditionally overwrites the notificationStoredKey field using .copy(), even if the caller already set one. This could surprise callers who expect their key to be preserved.

If this is intentional (enforcing that all keys are server-generated), consider:

  • Documenting this behavior in method KDoc
  • Or, alternatively, only generate a key if not already present

Current design enforces central key generation, which is generally good for consistency. Confirm this aligns with the intended contract.


36-42: LGTM! Clean delegation pattern.

The service properly delegates to specialized reader and updater components, maintaining clear separation of concerns. The delegation pattern is appropriate for this orchestration layer.

pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredCoreRepository.kt (2)

67-78: LGTM! Excellent idempotency optimization.

The markAsRead implementation includes an early-return guard (lines 73-75) that prevents unnecessary write operations when the notification is already marked as read. This optimization:

  • Reduces database writes
  • Makes the operation naturally idempotent
  • Avoids unnecessary entity version increments (if using optimistic locking)

Well done!


17-78: Transaction management is properly implemented.

The Tx.writeable and Tx.readable wrappers delegate to Spring's @Transactional annotation framework, which correctly handles:

  • Transaction lifecycle: @Transactional opens a transaction on method entry and commits on successful return
  • Read-only optimization: @ReadOnlyTransactional marks transactions as read-only with isReadOnly=true, allowing database optimizations
  • Exception handling: Spring's AOP automatically rolls back on unchecked exceptions
  • Entity persistence: The notificationStored.read() mutation at line 77 occurs within the transaction context, so JPA's dirty checking detects and persists the changes on commit
pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredCustomRepository.kt (1)

15-20: The exclusion of REGULAR notification type is intentional and appropriate.

NotificationType.REGULAR represents scheduled/automated notifications (regular push every Tuesday at 9 AM), which are correctly excluded from the user's notification inbox query. The findAllBy method retrieves user-facing notifications (ADMIN_PUSH), while automated scheduled notifications are handled separately.

val fcmToken: String,
val title: String,
val body: String,
val destination: String,
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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
# First, find the FirebaseCloudMessage file and examine its structure
echo "=== FirebaseCloudMessage.kt file structure ==="
fd -t f "FirebaseCloudMessage.kt" --exec wc -l {} \;
echo ""

# Read the file to understand the class definition
echo "=== FirebaseCloudMessage.kt contents ==="
fd -t f "FirebaseCloudMessage.kt" --exec cat -n {} \;
echo ""

# Search for destination field usage in message construction and validation
echo "=== Destination field usage patterns ==="
rg -nP --type=kt -C3 'destination\s*[=:]' -g '!*Test.kt' || echo "No matches found"
echo ""

# Search for empty/blank checks on destination
echo "=== Empty/blank checks on destination ==="
rg -nP --type=kt 'destination\.(isNotBlank|isNotEmpty|isEmpty|isBlank)' -g '!*Test.kt' || echo "No matches found"
echo ""

# Search for where FirebaseCloudMessage is instantiated
echo "=== FirebaseCloudMessage instantiation ==="
rg -nP --type=kt 'FirebaseCloudMessage\s*\(' -C2 -g '!*Test.kt' | head -50

Repository: Team-PIDA/Pida-Server

Length of output: 1132


๐Ÿ Script executed:

#!/bin/bash
# Search for destination field usage without file type restriction
echo "=== Destination field usage in codebase ==="
rg -n 'destination' --type-list | grep -i kotlin || echo "Checking Kotlin type support..."
echo ""

# Try broader search
echo "=== All destination references ==="
rg -n '\.destination' -g '*.kt' -C2 | head -100
echo ""

# Search for FirebaseCloudMessage usage
echo "=== FirebaseCloudMessage instantiation/usage ==="
rg -n 'FirebaseCloudMessage(' -g '*.kt' -C3 | head -100
echo ""

# Search for validation in notification package
echo "=== Validation in notification package ==="
rg -n 'require\|check\|assert' pida-core/core-domain/src/main/kotlin/com/pida/notification/ | head -50
echo ""

# Check if there are any constructor validations
echo "=== Constructor or init blocks ==="
rg -n 'init\s*\{' pida-core/core-domain/src/main/kotlin/com/pida/notification/ -A5

Repository: Team-PIDA/Pida-Server

Length of output: 5667


๐Ÿ Script executed:

#!/bin/bash
# Examine the FirebaseCloudMessageSender implementation
echo "=== FirebaseCloudMessageSender.kt contents ==="
cat -n pida-clients/notification/src/main/kotlin/com/pida/client/notification/FirebaseCloudMessageSender.kt
echo ""

# Look for any custom data validation in related files
echo "=== NewFirebaseCloudMessage request creation ==="
rg -n 'NewFirebaseCloudMessage' pida-clients/notification/src/main/kotlin/com/pida/client/notification/ -A3 -B1
echo ""

# Check if there are any tests that show validation expectations
echo "=== Test files for destination ==="
find . -name '*Test.kt' -o -name '*Tests.kt' | xargs grep -l 'destination' 2>/dev/null | head -10
echo ""

# Examine any validation in request/domain models
echo "=== FcmRequest or related request models ==="
rg -n 'data class.*Request' pida-clients/notification/src/main/kotlin/com/pida/client/notification/ -A5

Repository: Team-PIDA/Pida-Server

Length of output: 4347


Add validation to ensure destination is not empty.

The destination field is sent directly as custom data in Firebase messages but has no validation. An empty string would be silently accepted and sent, causing routing failures on the client side. Add validation such as require(destination.isNotBlank()) in the constructor.

๐Ÿค– Prompt for AI Agents
In
pida-core/core-domain/src/main/kotlin/com/pida/notification/FirebaseCloudMessage.kt
around line 8, the destination property lacks validation and may be empty; add a
constructor check such as require(destination.isNotBlank()) (or
require(destination.isNotEmpty()) depending on style) to enforce
non-empty/non-blank values and throw an IllegalArgumentException if invalid,
ensuring the validation runs during object creation and prevents empty
destination values from being sent to Firebase.

Comment on lines +9 to +11
init {
require(offset >= 0) { "offset์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค. " }
require(limit > 0) { "limit์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค. " }
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸก Minor

Error message inconsistent with validation logic.

Line 10: The condition offset >= 0 allows offset to be 0, but the Korean message "offset์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค" translates to "offset must be greater than 0". The message should reflect that offset can be 0.

๐Ÿ”Ž Proposed fix
     init {
-        require(offset >= 0) { "offset์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค. " }
+        require(offset >= 0) { "offset์€ 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." }
         require(limit > 0) { "limit์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค. " }
     }
๐Ÿค– Prompt for AI Agents
In pida-core/core-domain/src/main/kotlin/com/pida/support/page/OffsetLimit.kt
around lines 9 to 11, the require check uses `require(offset >= 0)` but the
error message says "offset์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค." (must be greater than 0); update the
message to reflect the actual validation (e.g. "offset์€ 0๋ณด๋‹ค ํฌ๊ฑฐ๋‚˜ ๊ฐ™์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค." or
"offset์€ 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") so the text matches the condition, keeping the existing
condition as-is.

val size: Int,
) {
init {
require(page >= 0) { "ํŽ˜์ด์ง€๋Š” 0 ์ด์ƒ์ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." }
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸก Minor

Fix typo in error message.

The Korean error message contains a grammatical error: "์ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" should be "์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค".

๐Ÿ”Ž Proposed fix
-        require(page >= 0) { "ํŽ˜์ด์ง€๋Š” 0 ์ด์ƒ์ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." }
+        require(page >= 0) { "ํŽ˜์ด์ง€๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." }
๐Ÿ“ 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
require(page >= 0) { "ํŽ˜์ด์ง€๋Š” 0 ์ด์ƒ์ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." }
require(page >= 0) { "ํŽ˜์ด์ง€๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." }
๐Ÿค– Prompt for AI Agents
In
pida-core/core-domain/src/main/kotlin/com/pida/support/page/OffsetPageRequest.kt
around line 8, the Korean error message in the require() call contains a typo
("์ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"); update the string to the correct grammar ("์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค") so the message
reads "ํŽ˜์ด์ง€๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." and keep the require condition unchanged.

Comment on lines 12 to 17
require(totalCount >= 0) { "totalCount ($totalCount) cannot be negative" }
require(totalCount >= content.size) {
"totalCount ($totalCount) cannot be smaller than content.size (${content.size})"
}
val count = if (content.isEmpty()) 0 else totalCount
return Page(content, count)
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

Inconsistent validation and count assignment logic.

Lines 12-15 validate that totalCount >= content.size, allowing totalCount > 0 when content.isEmpty(). However, Line 16 forces count = 0 when content is empty, overriding the validated totalCount. This creates inconsistency:

  • If totalCount=10 with empty content is allowed by validation, why override it to 0?
  • If count=0 for empty content is intended, validation should reject totalCount > 0 when content is empty.

Clarify the intended behavior and align validation with the assignment logic.

๐Ÿ”Ž Proposed fix: Align validation with assignment logic

Option 1: Remove the override (if totalCount should be preserved)

-            val count = if (content.isEmpty()) 0 else totalCount
-            return Page(content, count)
+            return Page(content, totalCount)

Option 2: Strengthen validation (if count must be 0 for empty content)

             require(totalCount >= 0) { "totalCount ($totalCount) cannot be negative" }
+            require(content.isNotEmpty() || totalCount == 0L) {
+                "totalCount must be 0 when content is empty, but got $totalCount"
+            }
             require(totalCount >= content.size) {
                 "totalCount ($totalCount) cannot be smaller than content.size (${content.size})"
             }
-            val count = if (content.isEmpty()) 0 else totalCount
-            return Page(content, count)
+            return Page(content, totalCount)
๐Ÿ“ 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
require(totalCount >= 0) { "totalCount ($totalCount) cannot be negative" }
require(totalCount >= content.size) {
"totalCount ($totalCount) cannot be smaller than content.size (${content.size})"
}
val count = if (content.isEmpty()) 0 else totalCount
return Page(content, count)
require(totalCount >= 0) { "totalCount ($totalCount) cannot be negative" }
require(totalCount >= content.size) {
"totalCount ($totalCount) cannot be smaller than content.size (${content.size})"
}
return Page(content, totalCount)
Suggested change
require(totalCount >= 0) { "totalCount ($totalCount) cannot be negative" }
require(totalCount >= content.size) {
"totalCount ($totalCount) cannot be smaller than content.size (${content.size})"
}
val count = if (content.isEmpty()) 0 else totalCount
return Page(content, count)
require(totalCount >= 0) { "totalCount ($totalCount) cannot be negative" }
require(content.isNotEmpty() || totalCount == 0L) {
"totalCount must be 0 when content is empty, but got $totalCount"
}
require(totalCount >= content.size) {
"totalCount ($totalCount) cannot be smaller than content.size (${content.size})"
}
return Page(content, totalCount)
๐Ÿค– Prompt for AI Agents
In pida-core/core-domain/src/main/kotlin/com/pida/support/page/Page.kt around
lines 12 to 17, the code validates totalCount >= content.size but then forces
count = 0 when content.isEmpty(), causing inconsistency; to fix, stop overriding
a validated totalCount: assign count = totalCount unconditionally (remove the
empty-content special-case) so the validated totalCount is preserved, and leave
the existing require checks as-is (or add a brief comment explaining that
totalCount may be >0 even when content is empty).

Comment on lines +22 to +52
fun findAllBy(
userId: Long,
cursorRequest: CursorRequest,
): Cursor<NotificationStored.Info> {
val pageable = PageRequest.ofSize(cursorRequest.size.toInt())
val notifications =
notificationStoredJpaRepository.findPage(JDSLExtensions, pageable) {
select(entity(NotificationStoredEntity::class))
.from(entity(NotificationStoredEntity::class))
.whereAnd(
path(NotificationStoredEntity::userId).equal(userId),
path(NotificationStoredEntity::type).notIn(EXCLUDED_TYPES),
cursorRequest.lastId?.let {
path(NotificationStoredEntity::id).lessThan(cursorRequest.lastId)
},
).orderBy(
path(NotificationStoredEntity::id).desc(),
)
}
val content =
notifications.content
.filterNotNull()
.map { it.toNotificationStoredInfo() }
val nextCursor = if (content.size < cursorRequest.size) null else notifications.lastOrNull()?.id

return Cursor.of(
nextCursor = nextCursor,
size = notifications.totalElements,
content = content,
)
}
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

Fix cursor pagination logic to handle edge case.

The nextCursor calculation at line 45 has a subtle bug: when content.size == cursorRequest.size, the code assumes there are more pages and sets nextCursor = notifications.lastOrNull()?.id. However, if the query returned exactly cursorRequest.size items and there are no more items in the database, the client will make an unnecessary additional request that returns empty results.

Standard cursor pagination patterns typically:

  • Fetch size + 1 items to detect if more pages exist, or
  • Use a separate count query (though less efficient)
๐Ÿ”Ž Proposed fix using size+1 approach
-        val pageable = PageRequest.ofSize(cursorRequest.size.toInt())
+        val pageable = PageRequest.ofSize(cursorRequest.size.toInt() + 1)
         val notifications =
             notificationStoredJpaRepository.findPage(JDSLExtensions, pageable) {
                 select(entity(NotificationStoredEntity::class))
                     .from(entity(NotificationStoredEntity::class))
                     .whereAnd(
                         path(NotificationStoredEntity::userId).equal(userId),
                         path(NotificationStoredEntity::type).notIn(EXCLUDED_TYPES),
                         cursorRequest.lastId?.let {
                             path(NotificationStoredEntity::id).lessThan(cursorRequest.lastId)
                         },
                     ).orderBy(
                         path(NotificationStoredEntity::id).desc(),
                     )
             }
-        val content =
-            notifications.content
+        val allItems = notifications.content
                 .filterNotNull()
                 .map { it.toNotificationStoredInfo() }
+        val hasMore = allItems.size > cursorRequest.size
+        val content = if (hasMore) allItems.take(cursorRequest.size.toInt()) else allItems
-        val nextCursor = if (content.size < cursorRequest.size) null else notifications.lastOrNull()?.id
+        val nextCursor = if (hasMore) content.lastOrNull()?.id else null

         return Cursor.of(
             nextCursor = nextCursor,
-            size = notifications.totalElements,
+            size = content.size.toLong(),
             content = content,
         )

Note: Also consider whether size in the Cursor should represent the current page size or total count across all pages. The current implementation uses totalElements which may be inefficient for large datasets.

๐Ÿ“ 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
fun findAllBy(
userId: Long,
cursorRequest: CursorRequest,
): Cursor<NotificationStored.Info> {
val pageable = PageRequest.ofSize(cursorRequest.size.toInt())
val notifications =
notificationStoredJpaRepository.findPage(JDSLExtensions, pageable) {
select(entity(NotificationStoredEntity::class))
.from(entity(NotificationStoredEntity::class))
.whereAnd(
path(NotificationStoredEntity::userId).equal(userId),
path(NotificationStoredEntity::type).notIn(EXCLUDED_TYPES),
cursorRequest.lastId?.let {
path(NotificationStoredEntity::id).lessThan(cursorRequest.lastId)
},
).orderBy(
path(NotificationStoredEntity::id).desc(),
)
}
val content =
notifications.content
.filterNotNull()
.map { it.toNotificationStoredInfo() }
val nextCursor = if (content.size < cursorRequest.size) null else notifications.lastOrNull()?.id
return Cursor.of(
nextCursor = nextCursor,
size = notifications.totalElements,
content = content,
)
}
fun findAllBy(
userId: Long,
cursorRequest: CursorRequest,
): Cursor<NotificationStored.Info> {
val pageable = PageRequest.ofSize(cursorRequest.size.toInt() + 1)
val notifications =
notificationStoredJpaRepository.findPage(JDSLExtensions, pageable) {
select(entity(NotificationStoredEntity::class))
.from(entity(NotificationStoredEntity::class))
.whereAnd(
path(NotificationStoredEntity::userId).equal(userId),
path(NotificationStoredEntity::type).notIn(EXCLUDED_TYPES),
cursorRequest.lastId?.let {
path(NotificationStoredEntity::id).lessThan(cursorRequest.lastId)
},
).orderBy(
path(NotificationStoredEntity::id).desc(),
)
}
val allItems = notifications.content
.filterNotNull()
.map { it.toNotificationStoredInfo() }
val hasMore = allItems.size > cursorRequest.size
val content = if (hasMore) allItems.take(cursorRequest.size.toInt()) else allItems
val nextCursor = if (hasMore) content.lastOrNull()?.id else null
return Cursor.of(
nextCursor = nextCursor,
size = content.size.toLong(),
content = content,
)
}
๐Ÿค– Prompt for AI Agents
In
pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredCustomRepository.kt
around lines 22 to 52, the nextCursor logic assumes more pages exist when
content.size == cursorRequest.size which can cause an extra empty request;
change the query to fetch cursorRequest.size + 1 items, determine hasMore by
whether more than requested items were returned, set nextCursor only when
hasMore (use the id of the extra item or the last of the requested items as
appropriate), trim the returned content to cursorRequest.size, and set the
Cursor.size to the actual returned page size (or content.size) instead of
notifications.totalElements to avoid expensive total count.

}
val content =
notifications.content
.filterNotNull()
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸก Minor

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

# First, let's read the file to understand the context around line 43
cat -n pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredCustomRepository.kt | sed -n '30,60p'

Repository: Team-PIDA/Pida-Server

Length of output: 1209


๐Ÿ Script executed:

# Search for the complete method/class containing line 43
rg -nP --type=kt -B 20 -A 5 'filterNotNull' pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredCustomRepository.kt

Repository: Team-PIDA/Pida-Server

Length of output: 91


๐Ÿ Script executed:

# Check JDSL query patterns in the codebase to understand if null entities are possible
rg -nP --type=kt 'findPage.*filterNotNull' --max-count=10

Repository: Team-PIDA/Pida-Server

Length of output: 91


๐Ÿ Script executed:

# Look for JDSL documentation or type signatures
rg -nP --type=kt 'import.*jdsl' --max-count=20

Repository: Team-PIDA/Pida-Server

Length of output: 91


๐Ÿ Script executed:

# Search for findPage implementation and return type
rg -n 'fun findPage' pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/ -A 5

Repository: Team-PIDA/Pida-Server

Length of output: 47


๐Ÿ Script executed:

# Look for other filterNotNull usages in the codebase
rg -n 'filterNotNull' pida-storage/db-core/src/main/kotlin/ --max-count=15

Repository: Team-PIDA/Pida-Server

Length of output: 213


๐Ÿ Script executed:

# Check JDSL imports to understand the library
rg -n 'import.*jdsl' pida-storage/db-core/src/main/kotlin/ --max-count=10

Repository: Team-PIDA/Pida-Server

Length of output: 1730


๐Ÿ Script executed:

# Look for the complete method signature to see return type
cat -n pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredCustomRepository.kt | sed -n '20,52p'

Repository: Team-PIDA/Pida-Server

Length of output: 1640


๐Ÿ Script executed:

# Find the JDSLExtensions implementation
cat -n pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/support/JDSLExtensions.kt

Repository: Team-PIDA/Pida-Server

Length of output: 763


๐Ÿ Script executed:

# Find the findPage extension function definition
rg -n 'fun.*findPage' pida-storage/db-core/src/main/kotlin/ -A 10

Repository: Team-PIDA/Pida-Server

Length of output: 47


๐Ÿ Script executed:

# Check if there are other similar repository patterns without filterNotNull
rg -n 'findPage.*\.content' pida-storage/db-core/src/main/kotlin/ -A 3

Repository: Team-PIDA/Pida-Server

Length of output: 47


๐Ÿ Script executed:

# Search for findPage in all files including XML or Gradle configs
find pida-storage -name "*.kt" -type f | xargs grep -l 'fun.*findPage' 2>/dev/null

Repository: Team-PIDA/Pida-Server

Length of output: 49


๐Ÿ Script executed:

# Check the JpaRepository interface or extension
rg -n 'findPage|KotlinJdslJpqlExecutor' pida-storage/db-core/src/main/kotlin/ -B 2 -A 8

Repository: Team-PIDA/Pida-Server

Length of output: 11240


๐Ÿ Script executed:

# Check if there are any null checks or Optional handling in the codebase
rg -n 'notifications\.content' pida-storage/db-core/src/main/kotlin/ -B 2 -A 3

Repository: Team-PIDA/Pida-Server

Length of output: 1023


๐Ÿ Script executed:

# Look at the NotificationStoredJpaRepository to understand its interface
cat pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredJpaRepository.kt

Repository: Team-PIDA/Pida-Server

Length of output: 659


๐Ÿ Script executed:

# Search for how findPage is used in other custom repositories
find pida-storage -name "*CustomRepository.kt" -type f -exec grep -l "findPage" {} \;

Repository: Team-PIDA/Pida-Server

Length of output: 177


๐Ÿ Script executed:

# Look at BloomingCustomRepository to see if it uses similar patterns
cat pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/blooming/BloomingCustomRepository.kt

Repository: Team-PIDA/Pida-Server

Length of output: 2650


๐Ÿ Script executed:

# Check Spring Data Page documentation or similar usage in codebase
rg -n '\.content' pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/ -B 2 -A 2

Repository: Team-PIDA/Pida-Server

Length of output: 1495


๐Ÿ Script executed:

# Search for any type annotations or method signatures that might hint at nullable content
rg -n 'Page<.*\?>' pida-storage/ -A 2

Repository: Team-PIDA/Pida-Server

Length of output: 47


๐Ÿ Script executed:

# Check the complete method signature including imports to understand return type
cat pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredCustomRepository.kt | head -20

Repository: Team-PIDA/Pida-Server

Length of output: 726


๐Ÿ Script executed:

# Verify if JDSL is documented in build files or dependencies
find pida-storage -name "build.gradle*" -o -name "pom.xml" | xargs cat 2>/dev/null | grep -i "jdsl\|kotlin.*jdsl" -A 1 -B 1

Repository: Team-PIDA/Pida-Server

Length of output: 208


๐Ÿ Script executed:

# Check if there are any comments or documentation about why filterNotNull is used
grep -B 5 -A 1 'filterNotNull' pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredCustomRepository.kt

Repository: Team-PIDA/Pida-Server

Length of output: 311


๐Ÿ Script executed:

# Verify the Spring Data Page type signature by checking library usage
rg -n 'Page<' pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/ -A 1

Repository: Team-PIDA/Pida-Server

Length of output: 47


Remove unnecessary filterNotNull() call.

The JDSL query explicitly selects complete entities (select(entity(NotificationStoredEntity::class))), and Spring Data's Page<T>.content property returns List<T> (non-nullable). JPQL entity selections cannot produce null values, making filterNotNull() unnecessary.

๐Ÿค– Prompt for AI Agents
In
pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/notification/NotificationStoredCustomRepository.kt
around line 43, remove the unnecessary .filterNotNull() call from the pipeline:
the JDSL query selects full entities and Page<T>.content is non-nullable so
.filterNotNull() is redundant; simply drop that call and adjust any chained
calls accordingly (no replacement needed).

Comment on lines +38 to +49
fun toNotificationStoredInfo(): NotificationStored.Info =
NotificationStored.Info(
id = id!!,
notificationStoredKey = notificationStoredKey,
userId = userId,
type = type,
parameterValue = parameterValue,
topic = topic,
contents = contents,
readStatus = readStatus,
createdAt = createdAt.toString(),
)
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

Fix potential NPE from force-unwrapping id.

Line 40 uses id!! which will throw a NPE if toNotificationStoredInfo() is called on a non-persisted entity (before save() completes). While the current usage pattern may ensure entities are always persisted first, this creates a fragile contract.

๐Ÿ”Ž Proposed fix with safer null handling
     fun toNotificationStoredInfo(): NotificationStored.Info =
         NotificationStored.Info(
-            id = id!!,
+            id = requireNotNull(id) { "Cannot convert unpersisted entity to Info" },
             notificationStoredKey = notificationStoredKey,
             userId = userId,
             type = type,
             parameterValue = parameterValue,
             topic = topic,
             contents = contents,
             readStatus = readStatus,
             createdAt = createdAt.toString(),
         )

This provides a clearer error message if the contract is violated.

๐Ÿ“ 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
fun toNotificationStoredInfo(): NotificationStored.Info =
NotificationStored.Info(
id = id!!,
notificationStoredKey = notificationStoredKey,
userId = userId,
type = type,
parameterValue = parameterValue,
topic = topic,
contents = contents,
readStatus = readStatus,
createdAt = createdAt.toString(),
)
fun toNotificationStoredInfo(): NotificationStored.Info =
NotificationStored.Info(
id = requireNotNull(id) { "Cannot convert unpersisted entity to Info" },
notificationStoredKey = notificationStoredKey,
userId = userId,
type = type,
parameterValue = parameterValue,
topic = topic,
contents = contents,
readStatus = readStatus,
createdAt = createdAt.toString(),
)

Copy link
Collaborator

@LeeHanEum LeeHanEum left a comment

Choose a reason for hiding this comment

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

์•„์ฃผ ๊น”๋”ํ•˜๊ณ  ์ฑ…์ž„์ด ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌ๋œ ์ข‹์€ ์ฝ”๋“œ ์ž˜๋ดค์Šต๋‹ˆ๋‹ค~
๊ถ๊ธˆํ•œ ์ ๋“ค ๋ช‡๊ฐ€์ง€ ๋‚จ๊ฒจ๋ดค์–ด์š” :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

์š” ํƒ€์ž…์€ ์“ฐ๋‹ด์—์„œ๋„ ์•ˆ์“ฐ์ด๊ณ  ์žˆ๋˜๋ฐ, ์–ด๋–ค ์—ญํ• ์„ ํ•˜๋Š”๊ฑด๊ฐ€์š”?

Copy link
Member Author

Choose a reason for hiding this comment

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

์ € ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค๋Š” FcmSender ๋‹จ์—์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘ธ์‹œ๋ฅผ ๋ณด๋‚ด๊ธฐ ์œ„ํ•œ ๊ฐ์ฒด๋กœ ์ƒ์„ฑ์„ ํ–ˆ์—ˆ๋Š”๋ฐ, ์ด๋ฏธ ์‚ฌ์šฉ์ค‘์ธ๊ฒŒ ์žˆ๋„ค์š”..! ์‚ญ์ œ ์กฐ์น˜ํ• ๊ฒŒ์š”!

Comment on lines +26 to +38
data class Result(
val id: Long,
val userId: Long,
val nickname: String?,
val spotName: String?,
val site: String?,
val type: String,
val parameterValue: String,
val topic: String,
val contents: String,
val readStatus: ReadStatus,
val createdAt: String,
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

์š”๊ธฐ Result๋Š” ์–ด๋–ค ์—ญํ• ์ธ๊ฐ€์š”~

Copy link
Member Author

Choose a reason for hiding this comment

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

์—ฌ๊ธฐ Result๋Š” Facade ๋ ˆ์ด์–ด์—์„œ User ๋„๋ฉ”์ธ๊ณผ Notification ๋„๋ฉ”์ธ์—์„œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ฒฐํ•ฉํ•œ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์˜ ๊ฒฐ๊ณผ๋ผ๊ณ  ๋ด์ฃผ์‹ฌ ๋  ๊ฑฐ ๊ฐ™์Šต๋‹ˆ๋‹ค!

val fcmToken: String,
val title: String,
val body: String,
val destination: String,
Copy link
Collaborator

Choose a reason for hiding this comment

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

์ด๊ฑด ์ œ๊ฐ€ FCM์— ๋Œ€ํ•œ ์ดํ•ด๊ฐ€ ๋ถ€์กฑํ•ด์„œ ์ผ ์ˆ˜๋„ ์žˆ๋Š”๋ฐ, destination๊ณผ topic์€ ์–ด๋–ค ์ฐจ์ด๊ฐ€ ์žˆ๋‚˜์š”?
๋‘˜๋‹ค ์ „์†ก ๊ฒฝ๋กœ๋ผ๋Š” ๋น„์Šทํ•œ ์˜๋ฏธ๋กœ ์•Œ๊ณ  ์žˆ์–ด์„œ ์—ฌ์ญค๋ด…๋‹ˆ๋‹ค!

Copy link
Member Author

Choose a reason for hiding this comment

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

destination์€ FCM ๊ด€๋ จํ•œ ํ•„๋“œ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค!
putCustomData์œผ๋กœ APNS์—์„œ ์ €ํฌ๊ฐ€ ์ž„์˜๋กœ ์ปค์Šคํ…€ ํ•„๋“œ๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๊ณ , ํ‘ธ์‹œ ์•Œ๋ฆผ์—์„œ UI ์ „ํ™˜์„ ์œ„ํ•œ ์•ฑ์Šคํ‚ค๋งˆ ์ปจํ…์ŠคํŠธ๋ผ๊ณ  ๋ด์ฃผ์‹ฌ ์ข‹์„ ๊ฑฐ ๊ฐ™์Šต๋‹ˆ๋‹ค!

Copy link
Collaborator

Choose a reason for hiding this comment

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

์•„ํ•˜ ๊ทธ๋ ‡๊ตฐ์š” ๋‹ต๋ณ€ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!

nextCursor: Long?,
size: Long,
): Cursor<T> {
require(size >= 0) { "size ($size) must be greater than or equal to 0" }
Copy link
Collaborator

Choose a reason for hiding this comment

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

๋‚˜๋จธ์ง€๋Š” ๋‹ค ํ•œ๊ธ€์ธ๊ฑฐ ๊ฐ™์€๋ฐ ์š”๊ฑฐ๋Š” ์˜์–ด๋„ค์š”~

content: List<T>,
totalCount: Long,
): Page<T> {
require(totalCount >= 0) { "totalCount ($totalCount) cannot be negative" }
Copy link
Collaborator

Choose a reason for hiding this comment

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

์š”๊ฒƒ๋„ ์˜์–ด๋„ค์š”~ ์ผ๊ด€์„ฑ์ด ์žˆ์œผ๋ฉด ์ข‹์„๊ฑฐ ๊ฐ™์•„์š”!

Copy link
Member Author

Choose a reason for hiding this comment

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

๋„ต ํ™•์ธ๊ฐ์‚ฌํ•ด์š”!

Copy link
Collaborator

Choose a reason for hiding this comment

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

์ด๊ฑฐ๋Š” ์ œ๊ฐ€ ์ดํ•ด๊ฐ€ ์ข€ ํ•„์š”ํ•  ๊ฑฐ ๊ฐ™์•„์„œ Tx์™€ TXAdvice ๊ฐ๊ฐ ์–ด๋–ค ์—ญํ• ์„ ํ•˜๋Š”์ง€ ๊ฐ„๋‹จํ•˜๊ฒŒ ์„ค๋ช… ๋ถ€ํƒ๋“œ๋ ค๋„ ๋ ๊นŒ์š”?

Copy link
Member Author

Choose a reason for hiding this comment

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

์Œ ์ œ๊ฐ€ ์‹ค์ˆ˜ํ•œ ๋ถ€๋ถ„์ด ์žˆ๋„ค์š”..

์ด์ „ ์“ฐ๋‹ด ๋•Œ ์ž‘์—…์—์„œ๋Š” spring.main.lazy-initialization ์˜ต์…˜์„ ๋นผ๊ณ  ์ง„ํ–‰ํ•˜์—ฌ ๊ตณ์ด txAdvice๋ฅผ ์˜์กด์„ฑ ์ฃผ์ž…ํ•˜์ง€ ์•Š์•„๋„ ํ™•์žฅํ•จ์ˆ˜๋กœ ์ถฉ๋ถ„ํžˆ ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„๋ฅผ ์žก์„ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์ €๋ฒˆ ํ”ผ๋‹ค ๊ฐœ๋ฐœํ•  ๋‹น์‹œ์— lazy-initialization์„ ์‹œ๋ฒ”์ ์œผ๋กœ ์‚ฌ์šฉํ•˜์˜€๊ณ , txAdvice๋ฅผ ์˜์กด์„ฑ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์™œ lazy-initialization์ด true ์˜ต์…˜์—์„œ ๊ตณ์ด TxAdvice ํด๋ž˜์Šค๋ฅผ ์˜์กด๋ฐ›๋А๋ƒ ํ•˜๋ฉด, ์Šคํ”„๋ง ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ ์‹œ ๋นˆ ์ดˆ๊ธฐํ™”๊ฐ€ ์ด๋ค„์ง€๋Š” ๊ณผ์ •์—์„œ Tx๋Š” lazy-initialization ์˜ต์…˜์œผ๋กœ ์ธํ•ด ๋Šฆ์€ ์ดˆ๊ธฐํ™”๊ฐ€ ๋˜๊ธฐ์— ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„๋ฅผ ์žก์ง€ ๋ชปํ•˜๋Š” ํ˜„์ƒ์ด ์žˆ์–ด ์˜์กด์„ฑ ์ฃผ์ž…์„ ๋ฐ›์•„ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ lazy-initialization์ด ๊ตณ์ด ์ง€๊ธˆ์œผ๋กœ์จ ํ•„์š”ํ•˜๋Š”๊ฐ€์— ๋Œ€ํ•ด์„œ ๊ณ ๋ฏผ์„ ํ•ด๋ณด๋ฉด ๊ทธ๋ ‡์ง€๋„ ์•Š๋‹ค๊ณ  ์ƒ๊ฐ๋˜๊ธฐ์— lazy-initialization ์ œ๊ฑฐ ํ›„ Tx ํ™•์žฅํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ชฝ์œผ๋กœ ์ˆ˜์ •ํ•ด๋‘๊ฒ ์Šต๋‹ˆ๋‹ค...!

Copy link
Collaborator

Choose a reason for hiding this comment

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

์•„ ๋„ค๋„ค ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. ๋•๋ถ„์— lazy-initialization์— ๋Œ€ํ•ด ์•Œ์•„๋ณผ ์ˆ˜ ์žˆ์—ˆ์–ด์š”! ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค~

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

Caution

Some comments are outside the diff and canโ€™t be posted inline due to platform limitations.

โš ๏ธ Outside diff range comments (2)
pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/blooming/BloomingCoreRepository.kt (2)

25-31: Inconsistent transaction wrapper for suspend functions.

These suspend functions use Tx.readable instead of Tx.coReadable, which differs from the pattern in UserCoreRepository where all suspend read operations use Tx.coReadable.

Method Current Expected (per UserCoreRepository pattern)
findTopByUserIdAndSpotIdDecs Tx.readable Tx.coReadable
findAllByUserId Tx.readable Tx.coReadable
findAllByFlowerSpotId Tx.readable Tx.coReadable

Compare with findRecentlyBySpotId (line 43-46) which correctly uses Tx.coReadable for a suspend function.

๐Ÿ”Ž Proposed fix for consistency
     override suspend fun findTopByUserIdAndSpotIdDecs(
         userId: Long,
         flowerSpotId: Long,
     ): Blooming? =
-        Tx.readable {
+        Tx.coReadable {
             bloomingJpaRepository.findTopByUserIdAndFlowerSpotIdOrderByCreatedAtDesc(userId, flowerSpotId)?.toBlooming()
         }

     override suspend fun findAllByUserId(userId: Long): List<Blooming> =
-        Tx.readable {
+        Tx.coReadable {
             bloomingJpaRepository.findAllByUserId(userId).map { it.toBlooming() }
         }

     override suspend fun findAllByFlowerSpotId(flowerSpotId: Long): List<Blooming> =
-        Tx.readable {
+        Tx.coReadable {
             bloomingJpaRepository.findAllByFlowerSpotId(flowerSpotId).map { it.toBlooming() }
         }

Also applies to: 33-36, 38-41


25-25: Typo in method name: Decs โ†’ Desc.

The method name findTopByUserIdAndSpotIdDecs appears to have a typo. It should likely be findTopByUserIdAndSpotIdDesc (for "descending").

๐Ÿ“œ Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 4ca15bf and dbb4f3c.

๐Ÿ“’ Files selected for processing (3)
  • pida-core/core-domain/src/main/kotlin/com/pida/support/tx/TxAdvice.kt
  • pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/blooming/BloomingCoreRepository.kt
  • pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/user/UserCoreRepository.kt
๐Ÿ’ค Files with no reviewable changes (1)
  • pida-core/core-domain/src/main/kotlin/com/pida/support/tx/TxAdvice.kt
โฐ 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: CI (21, 2.1.0, corretto)
๐Ÿ”‡ Additional comments (2)
pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/blooming/BloomingCoreRepository.kt (1)

43-59: Correct transaction wrapper usage for remaining methods.

These methods correctly apply the transaction pattern:

  • findRecentlyBySpotId (suspend) โ†’ Tx.coReadable โœ“
  • findRecentBySpotIds (non-suspend) โ†’ Tx.readable โœ“
  • findTodayBloomingByUserId (non-suspend) โ†’ Tx.readable โœ“
pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/user/UserCoreRepository.kt (1)

6-6: Clean migration to the Tx API.

The refactoring consistently applies the new transaction abstraction pattern:

  • Non-suspend operations use Tx.writeable/Tx.readable
  • Suspend operations use Tx.coWriteable/Tx.coReadable

The constructor simplification aligns with the centralized Tx utility approach, which is properly configured as a Spring-managed singleton with access to the transaction manager through its TxAdvice component.

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

โ™ป๏ธ Duplicate comments (1)
pida-core/core-domain/src/main/kotlin/com/pida/support/cursor/Cursor.kt (1)

17-20: Past issue resolved: error message now correctly references "size".

The validation logic is correct, and the error message accurately refers to the size parameter. The past review concern about using "totalCount" instead of "size" has been addressed.

๐Ÿงน Nitpick comments (2)
pida-core/core-domain/src/main/kotlin/com/pida/support/cursor/Cursor.kt (2)

8-10: Consider type consistency for DEFAULT_SIZE.

DEFAULT_SIZE is defined as Int while the size parameter in the factory method is Long. While Kotlin handles the conversion automatically, using consistent types improves clarity.

๐Ÿ”Ž Proposed fix for type consistency
     companion object {
         const val DEFAULT_CURSOR = 0L
-        const val DEFAULT_SIZE = 20
+        const val DEFAULT_SIZE = 20L

3-7: Consider clarifying the semantic purpose of the size property.

The size property in a pagination response is semantically unusual. Typical cursor-based pagination responses contain the data (content) and navigation metadata (nextCursor), but don't typically echo back the requested page size. The property name size is also ambiguousโ€”it could be interpreted as the content size, the page size limit, or even a total count.

If the size property serves a specific purpose in your API design, consider renaming it to clarify intent (e.g., pageSize, limit, or requestedSize). Alternatively, if it's not needed by consumers, consider removing it from the response.

๐Ÿ“œ Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between dbb4f3c and 6aae403.

๐Ÿ“’ Files selected for processing (3)
  • pida-core/core-api/src/main/resources/application.yml
  • pida-core/core-domain/src/main/kotlin/com/pida/support/cursor/Cursor.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/page/Page.kt
๐Ÿ’ค Files with no reviewable changes (1)
  • pida-core/core-api/src/main/resources/application.yml
๐Ÿšง Files skipped from review as they are similar to previous changes (1)
  • pida-core/core-domain/src/main/kotlin/com/pida/support/page/Page.kt
โฐ 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: CI (21, 2.1.0, corretto)

@char-yb char-yb merged commit 3865a46 into develop Dec 30, 2025
2 checks passed
@char-yb char-yb deleted the refactor/fcm-notification branch December 30, 2025 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

โ™ป๏ธ FCM ํ‘ธ์‹œ ์•Œ๋ฆผ ๊ฐœ์„  ๋ฐ ํ…Œ์ด๋ธ” ์ •์˜

3 participants