Skip to content

Conversation

@kkh725
Copy link
Collaborator

@kkh725 kkh725 commented Dec 18, 2025

1. ⭐️ 변경된 내용

  • 매칭 -> 매칭디테일로 스낵바와 함께 넘어가야 하는 상황에, fullScreen 및 dismiss 되는 이슈 수정
    • 스낵바 위치를 scaffold 밖으로 변경.
    • navigate 시 스낵바 dismiss 되는 로직 주석
  • 카드 디자인 이슈 수정
    • 부자연스럽던 애니메이션 수정
    • 카드 background Color가 full로 반영되지 않던 이슈 수정

스낵바 추가 이슈로 로직 수정해야할 듯 합니다 !!!! 리뷰 x..

2. 🖼️ 스크린샷(선택)

Screen_recording_20251218_221943.mp4
Screen_recording_20251218_222035.mp4

3. 💡 알게된 부분

4. 📌 이 부분은 꼭 봐주세요!

  • navigate 시 스낵바 dismiss 되는 로직을 주석처리했는데 어떤 의도를 가지고 작성하셨는지 궁금합니다! 꼭 필요한 기능일까요??

Summary by CodeRabbit

  • New Features

    • Pull-to-refresh added to matching lists with visible refresh indicator.
    • Expanded snackbar side-effect types and improved message formatting.
  • Bug Fixes

    • Improved dialog dismissal and callback flows across match, block, and report screens.
    • More consistent snackbar layout, positioning, and display timing.
    • Matching screens now surface refresh and snackbar events reliably.
  • Chores

    • App locked to portrait orientation.
    • Removed an unused animation API.

✏️ Tip: You can customize this high-level summary in your review settings.

@kkh725 kkh725 requested review from comst19 and tgyuuAn December 18, 2025 13:23
@kkh725 kkh725 added 리뷰 원해요🔥 피어의 리뷰를 기다리는 ing.. 🔥 ㄱㅎ근형 🌙 labels Dec 18, 2025
private suspend fun getInstantNewMatch() = suspendRunCatching {
matchingRepository.getNewInstantMatch()
}.onSuccess { matchId ->
eventHelper.sendEvent(PieceEvent.ShowSnackBar(SnackBarState.Matching("퍼즐을 2개 사용했어요")))
Copy link
Collaborator

Choose a reason for hiding this comment

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

PUZZLE_COUNT_NEW_MATCH(2) 이걸로 바꾸는게 좋을 거 같아요

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

e40568c 수정했습니다 ~!

Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(bottom = snackBarPosition, start = 20.dp, end = 20.dp)
Copy link
Collaborator

Choose a reason for hiding this comment

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

bottom padding 없어지면서 스낵바가 Info,TextOnly일 때 좀 낮게 뜨는 거 같은데 확인 부탁드립니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

최종적으로 a199db5 이렇게 정리 했어요

private suspend fun postOpponentContact(matchId: Int) = suspendRunCatching {
matchingRepository.postOpponentContacts(matchId)
}.onSuccess {
eventHelper.sendEvent(PieceEvent.ShowSnackBar(SnackBarState.Matching("퍼즐을 3개 사용했어요")))
Copy link
Collaborator

Choose a reason for hiding this comment

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

PUZZLE_COUNT_ACCEPT_MATCHING(3) 이걸로 바꾸는게 좋을 거 같아요

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

얘도 똑같이 e40568c 수정했습니다 ~


Box(
modifier = Modifier
.animateContentSize()
Copy link
Collaborator

Choose a reason for hiding this comment

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

줄어들 때 확 줄어들지 않고 각각 애니메이션 주면 좀 더 부드럽게 될 거 같아요

@kkh725 kkh725 removed the 리뷰 원해요🔥 피어의 리뷰를 기다리는 ing.. 🔥 label Dec 19, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 20, 2025

📝 Walkthrough

Walkthrough

Refactors snackbar side-effect handling to a new SnackBarSideEffect API, removes PieceCardTransitionAnimation, adds pull-to-refresh (PiecePullRefreshBox), introduces dialog dismissal callbacks and OnRefresh intents, adjusts matching UI/state, and moves snackbar host out of the Scaffold.

Changes

Cohort / File(s) Summary
SnackBar side-effect & viewmodel updates
feature/matching/.../MatchingDetailViewModel.kt, feature/matching/.../MatchingDetailScreen.kt, feature/matching/.../MatchingViewModel.kt, feature/matching/.../MatchingScreen.kt, feature/matching/.../detail/common/contract/effect/SnackBarSideEffect.kt, feature/matching/.../detail/contract/MatchingDetailSideEffect.kt
Introduced SnackBarSideEffect (sealed interface + helpers); viewmodels now expose/emit SnackBarSideEffect via Channel/Flow; removed legacy MatchingDetailSideEffect file; consumers use getMessage/handle helpers.
Pull-to-refresh integration
core/designsystem/.../component/Box.kt, feature/matching/.../main/page/MatchingUserScreen.kt, feature/matching/.../main/contract/MatchingIntent.kt, feature/matching/.../main/contract/MatchingState.kt, feature/matching/.../main/MatchingViewModel.kt
Added PiecePullRefreshBox; wrapped MatchingUserScreen content with pull-to-refresh; added OnRefresh intent and isRefreshing state; MatchingViewModel supports refresh flow and emits related side effects.
Matching card & animation changes
core/common-ui/.../Animation.kt, feature/matching/.../component/card/MatchingCard.kt
Removed public PieceCardTransitionAnimation; simplified MatchingCard to state-driven Column layout with animateContentSize and static image scaling.
SnackBar UI & host relocation
core/designsystem/.../component/SnackBar.kt, presentation/.../ui/PieceApp.kt, presentation/src/.../MainActivity.kt
Reworked SnackBar layout to a Box with inner Row; moved PieceSnackBarHost outside the Scaffold; disabled automatic snackbar dismissal on navigation.
Dialog dismissal callbacks & intents
feature/matching/.../block/BlockScreen.kt, feature/matching/.../block/BlockViewModel.kt, feature/matching/.../block/dialog/BlockDoneDialog.kt, feature/matching/.../block/contract/BlockIntent.kt, feature/matching/.../report/..., feature/matching/.../detail/dialog/*.kt
Added onDismissClick/onDismissRequest params and OnDismissClick intents; wired dismiss callbacks through routes/dialogs; adjusted some button handlers to call dismiss before done actions (others removed explicit dismiss).
Report screen & previews
feature/matching/.../report/ReportScreen.kt, feature/matching/.../report/ReportViewModel.kt, feature/matching/.../report/dialog/ReportDoneDialog.kt
Propagated onDismissClick through route/screen to ReportDoneDialog; ReportViewModel handles OnDismissClick to reset state; previews updated.
Tests & previews adjustments
feature/matching/.../detail/MatchingDetailScreenTest.kt, feature/matching/.../main/page/MatchingUserScreen.kt, previews
Tests updated (removed imageUrl, added onAcceptMatchingClick); previews updated to pass new params (isRefreshing, onRefresh, onDismissClick).
Design system component added
core/designsystem/.../component/Box.kt
Added PiecePullRefreshBox composable exposing pull-to-refresh behavior and indicator.
Miscellaneous base / manifest
core/common-ui/.../BaseViewModel.kt, core/common-ui/.../BaseContract.kt, app/.../AndroidManifest.xml
Minor setState refactor, formatting-only contract change, and locked MainActivity to portrait orientation.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as MatchingUserScreen
    participant Pull as PiecePullRefreshBox
    participant VM as MatchingViewModel
    participant Side as SideEffectFlow
    participant Snack as PieceSnackBarHost

    User->>Pull: Pull to refresh
    activate Pull
    Pull->>UI: onRefresh()
    UI->>VM: dispatch(OnRefresh)
    activate VM
    VM->>VM: refreshMatchingList() (concurrent tasks)
    VM-->>Side: emit SnackBarSideEffect (optional)
    VM-->>UI: update state (isRefreshing)
    deactivate VM
    Side->>Snack: ShowSnackBar(ShowSnackBar(SnackBarState.*))
    Snack->>User: display snackbar
    deactivate Pull
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Suggested labels

UI/UX 🎨, 픽스 👨‍🔧

Suggested reviewers

  • tgyuuAn

Poem

🐰 A twitch, a pull, a new refresh spring,
Snackbars whisper messages on a string.
Dialogs bow out with a polite click,
Cards ease their motion and settle quick—
Rabbit hops, delighted by tidy spring.

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 (2 passed)
Check name Status Explanation
Title check ✅ Passed Title directly references PC-1485 issue and mentions design-related fixes for snackbar and card, which aligns with the main changes in the changeset.
Description check ✅ Passed Description covers main changes (snackbar positioning, dismiss logic, card animation/background fixes), includes screenshots, and raises reviewer concerns. Follows the template structure with all major sections populated.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/PC-1485

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b4857d and 5ecee57.

📒 Files selected for processing (1)
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt
🧰 Additional context used
🧬 Code graph analysis (1)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt (1)
feature/matching/src/main/java/com/puzzle/matching/graph/detail/dialog/PurchaseMatchingDialog.kt (2)
  • PurchaseDialog (18-75)
  • InsufficientPuzzleDialog (77-104)
⏰ 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
🔇 Additional comments (5)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt (5)

29-29: LGTM! Imports support the new features.

The new imports are all necessary for the analytics tracking, snackbar handling, lifecycle-aware collection, and concurrent coroutine execution introduced in this PR.

Also applies to: 32-32, 36-36, 46-47, 64-64


100-111: LGTM! BackHandler logic is correct.

The double back press to exit pattern is implemented correctly with appropriate timing and user feedback via snackbar.


123-123: LGTM! Refresh callback properly wired.

The onRefresh callback is correctly passed through to enable pull-to-refresh functionality.


138-138: LGTM! Refresh functionality properly integrated.

The isRefreshing state and onRefresh callback are correctly threaded through to MatchingUserScreen to enable pull-to-refresh behavior.

Also applies to: 248-254


315-316: LGTM! Preview properly updated.

The preview includes sensible defaults for the new refresh parameters, maintaining a working preview.


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

♻️ Duplicate comments (3)
core/designsystem/src/main/java/com/puzzle/designsystem/component/SnackBar.kt (1)

46-52: Address past review concern about snackbar positioning.

A previous reviewer noted that removing bottom padding caused Info and TextOnly snackbars to appear too low. The new top-padding approach calculates position as screenHeight - 36.dp - 82.dp for these states, which should place them 82dp from the bottom. Please verify this positioning in the UI, especially on different screen sizes.

feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt (2)

205-205: Use string resources and constants for puzzle counts.

The hardcoded message contains a magic number (2) and isn't localized. As mentioned in previous reviews, consider using a constant like PUZZLE_COUNT_NEW_MATCH and a string resource with a format parameter.

🔎 Suggested approach

Define the constant (if not already present):

private const val PUZZLE_COUNT_NEW_MATCH = 2

Add a string resource in strings.xml:

<string name="puzzle_used_message">퍼즐을 %d개 사용했어요</string>

Then use:

-eventHelper.sendEvent(PieceEvent.ShowSnackBar(SnackBarState.Matching("퍼즐을 2개 사용했어요")))
+val message = context.getString(R.string.puzzle_used_message, PUZZLE_COUNT_NEW_MATCH)
+eventHelper.sendEvent(PieceEvent.ShowSnackBar(SnackBarState.Matching(message)))

219-219: Use string resources and constants for puzzle counts.

Similar to Line 205, this hardcoded message should use a constant (e.g., PUZZLE_COUNT_ACCEPT_MATCHING = 3) and a string resource for proper localization and maintainability.

🧹 Nitpick comments (1)
core/designsystem/src/main/java/com/puzzle/designsystem/component/SnackBar.kt (1)

152-165: Consider edge cases in padding calculation.

The current calculation screenHeight - snackBarHeight - snackBarBottomOffset may not account for:

  • System insets (navigation bar, status bar)
  • Different screen sizes and aspect ratios
  • Scaffold content that might overlap

Consider extracting the constants to a companion object for better testability and verifying the positioning accounts for safe areas on devices with notches or gesture navigation.

🔎 Optional refactor: Extract constants
+private object SnackBarConstants {
+    val SNACK_BAR_HEIGHT = 36.dp
+    val SNACK_BAR_TOP_OFFSET = 112.dp
+    val SNACK_BAR_BOTTOM_OFFSET = 82.dp
+}
+
 @Composable
 private fun calculateSnackBarTopPadding(state: SnackBarState): Dp {
     val configuration = LocalConfiguration.current
 
-    val snackBarHeight = 36.dp
-    val snackBarTopOffset = 112.dp
-    val snackBarBottomOffset = 82.dp
-
     val screenHeight = configuration.screenHeightDp.dp
     val isMatching = state is SnackBarState.Matching
 
-    return if (isMatching) snackBarTopOffset
-    else screenHeight - snackBarHeight - snackBarBottomOffset
+    return if (isMatching) SnackBarConstants.SNACK_BAR_TOP_OFFSET
+    else screenHeight - SnackBarConstants.SNACK_BAR_HEIGHT - SnackBarConstants.SNACK_BAR_BOTTOM_OFFSET
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a76d053 and e52afa0.

📒 Files selected for processing (7)
  • core/common-ui/src/main/java/com/puzzle/common/ui/Animation.kt (0 hunks)
  • core/designsystem/src/main/java/com/puzzle/designsystem/component/SnackBar.kt (4 hunks)
  • feature/matching/src/androidTest/java/com/puzzle/matching/graph/detail/MatchingDetailScreenTest.kt (1 hunks)
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt (3 hunks)
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/component/card/MatchingCard.kt (6 hunks)
  • presentation/src/main/java/com/puzzle/presentation/MainActivity.kt (1 hunks)
  • presentation/src/main/java/com/puzzle/presentation/ui/PieceApp.kt (3 hunks)
💤 Files with no reviewable changes (1)
  • core/common-ui/src/main/java/com/puzzle/common/ui/Animation.kt
🧰 Additional context used
🧬 Code graph analysis (1)
presentation/src/main/java/com/puzzle/presentation/ui/PieceApp.kt (1)
core/designsystem/src/main/java/com/puzzle/designsystem/component/SnackBar.kt (2)
  • PieceSnackBarHost (77-97)
  • PieceSnackBar (36-75)
⏰ 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
🔇 Additional comments (9)
feature/matching/src/androidTest/java/com/puzzle/matching/graph/detail/MatchingDetailScreenTest.kt (1)

35-52: LGTM — new callback parameter properly stubbed.

The addition of onAcceptMatchingClick = {} correctly accommodates the updated MatchingDetailScreen API. Consider adding a dedicated test case to verify the acceptance flow behavior (e.g., that clicking the accept button triggers this callback and that the snackbar message is emitted as described in the PR objectives).

presentation/src/main/java/com/puzzle/presentation/MainActivity.kt (1)

177-178: Snackbar persistence across navigation needs clarification on scope and impact.

Commenting out line 178 removes snackbar dismissal during all navigation events, causing messages from previous screens to persist in the navigation stack. While this enables the intended pattern of showing "puzzle consumed" messages during transitions like Matching → MatchingDetail, the global nature of this change affects all navigation flows.

Current behavior:

  • Snackbars only clear when a new ShowSnackBar event is triggered (line 248) or if they auto-dismiss
  • Without an explicit ShowSnackBar event preceding navigation, stale snackbars from previous screens will remain visible

Recommend:

  • Add a comment documenting why snackbar dismissal is intentionally skipped during navigation (is this for the Matching flow specifically, or a deliberate app-wide change?)
  • Consider whether selective dismissal would be more appropriate (e.g., only preserve snackbars for specific navigation routes)
  • Test the impact on navigation flows that don't explicitly trigger ShowSnackBar events
presentation/src/main/java/com/puzzle/presentation/ui/PieceApp.kt (4)

49-49: LGTM: Visibility restriction improves encapsulation.

Changing PieceApp to internal appropriately reduces the public API surface, preventing external modules from depending on this implementation detail.


108-111: Verify snackbar overlay behavior and z-index ordering.

Moving PieceSnackBarHost outside the Scaffold changes its positioning and layering:

  1. Overlay behavior: The snackbar will now overlay the bottom navigation bar and FAB, which may or may not be intentional.
  2. Z-index: NetworkScreen and ForceUpdateDialog (declared after) will render on top of the snackbar, potentially obscuring it.

Please confirm:

  • Is overlaying the bottom bar the intended fix for the snackbar positioning issue mentioned in the PR?
  • Should NetworkScreen and ForceUpdateDialog take precedence over the snackbar in the rendering order?

121-121: LGTM: Proper encapsulation of internal helper.

Changing EdgeToEdgeContent to private correctly limits its scope to this file, as it's an implementation detail of PieceApp.


53-53: Navigation callback is properly implemented and consistently wired.

The navigateToBottomNaviDestination callback correctly extracts navigation logic and is properly threaded through the app. All bottom navigation destinations (Matching, Profile, Setting) are routed through this callback in AppBottomBar and the FAB, with no inconsistencies or missing integrations.

feature/matching/src/main/java/com/puzzle/matching/graph/main/component/card/MatchingCard.kt (3)

103-103: Good semantic improvement with parameter rename.

Renaming matchValueCountTextColor to nicknameTextColor better reflects the primary usage of this color, though it's also applied to the match count. This improves code readability.

Also applies to: 175-175, 197-197, 203-203


380-383: Animation implementation looks good.

The animateContentSize modifier with 300ms duration and FastOutSlowInEasing should provide smooth transitions during expand/collapse, addressing the previous concern about jerky animations. The approach is cleaner than managing separate animations for different content states.


389-389: Verify that ContentScale.FillBounds is appropriate for the card background.

ContentScale.FillBounds scales the image vertically and horizontally non-uniformly to fill the space, and this type of scaling distorts the image if the container doesn't match the exact ratio of the image. If bg_matched_card is a decorative background with patterns or gradients that don't depend on aspect ratio, FillBounds is acceptable. However, if the background has directional or proportional visual elements, consider alternatives:

  • ContentScale.Crop: Maintains aspect ratio, fills container by cropping edges
  • ContentScale.Fit: Maintains aspect ratio, may leave letterboxing
  • Apply Modifier.aspectRatio() to lock the card's aspect ratio, making FillBounds predictable

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

♻️ Duplicate comments (1)
core/designsystem/src/main/java/com/puzzle/designsystem/component/SnackBar.kt (1)

51-84: Verify snackbar positioning for Info and TextOnly states.

The layout restructure from Row to Box with top padding is well-structured. However, per the past review comment, please verify that the snackbar position is correct for Info and TextOnly states, as it was reported to appear lower after bottom padding removal.

Run the following visual test to confirm positioning:

#!/bin/bash
# Search for usages of SnackBarState.Info and SnackBarState.TextOnly to identify test/preview locations
rg -n -C3 --type=kotlin 'SnackBarState\.(Info|TextOnly)' -g '!**/test/**'
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e52afa0 and 8409ec3.

📒 Files selected for processing (1)
  • core/designsystem/src/main/java/com/puzzle/designsystem/component/SnackBar.kt (4 hunks)
⏰ 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

Comment on lines 162 to 180
@Composable
private fun calculateSnackBarTopPadding(state: SnackBarState): Dp {
val density = LocalDensity.current
val configuration = LocalConfiguration.current

val systemBarsInsets = WindowInsets.systemBars

val statusBarHeight = with(density) { systemBarsInsets.getTop(this).toDp() }
val navigationBarHeight = with(density) { systemBarsInsets.getBottom(this).toDp() }

val snackBarHeight = 48.dp
val isMatching = state is SnackBarState.Matching

return if (isMatching) {
statusBarHeight + 112.dp
} else {
configuration.screenHeightDp.dp - navigationBarHeight - 82.dp - snackBarHeight
}
}
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

🧩 Analysis chain

🏁 Script executed:

find . -name "SnackBar.kt" -type f

Repository: Piece-Puzzly/Piece-Android

Length of output: 149


🏁 Script executed:

find . -path "*/core/designsystem/src/main/java/com/puzzle/designsystem/component/SnackBar.kt" -o -path "*/SnackBar.kt" | head -1

Repository: Piece-Puzzly/Piece-Android

Length of output: 149


🏁 Script executed:

#!/bin/bash
find . -type f -name "*.kt" | xargs grep -l "calculateSnackBarTopPadding" 2>/dev/null | head -5

Repository: Piece-Puzzly/Piece-Android

Length of output: 149


🏁 Script executed:

#!/bin/bash
fd -e kt "SnackBar" | head -20

Repository: Piece-Puzzly/Piece-Android

Length of output: 214


🏁 Script executed:

#!/bin/bash
find . -type f -name "*.kt" -path "*/designsystem/*" 2>/dev/null | head -20

Repository: Piece-Puzzly/Piece-Android

Length of output: 1332


🏁 Script executed:

#!/bin/bash
grep -r "112\.dp\|82\.dp\|48\.dp" . --include="*.kt" 2>/dev/null | head -20

Repository: Piece-Puzzly/Piece-Android

Length of output: 704


🏁 Script executed:

cat -n ./core/designsystem/src/main/java/com/puzzle/designsystem/component/SnackBar.kt | head -200

Repository: Piece-Puzzly/Piece-Android

Length of output: 7953


Replace hard-coded dimensions with measured values or named constants.

The padding calculation relies on inflexible hard-coded values that don't account for actual content:

  1. Line 172: snackBarHeight = 48.dp assumes a fixed height, but the Text component on line 78 has maxLines = 2, meaning content can wrap to multiple lines. Material Design specifies single-line snackbars at 48dp and multi-line at 80dp or greater, so this hard-coded value won't accommodate wrapped text.

  2. Lines 176, 178: Magic numbers 112.dp and 82.dp lack any documentation explaining their purpose or origin, making maintenance difficult.

  3. Line 178: The calculation screenHeightDp - navigationBarHeight - 82.dp - snackBarHeight simulates bottom positioning using top padding, which is brittle when dealing with different screen sizes, orientations, and edge-to-edge display modes.

Consider these improvements:

  • Extract magic numbers as named constants with documentation explaining their purpose (e.g., MATCHING_TOP_OFFSET, NON_MATCHING_BOTTOM_OFFSET)
  • Use onGloballyPositioned modifier to measure the actual size and position of the snackbar composable after it is measured and laid out on the UI
  • Validate positioning logic across different device configurations, orientations, and content sizes (especially with multi-line text)
  • Document the positioning strategy for each state
🤖 Prompt for AI Agents
In core/designsystem/src/main/java/com/puzzle/designsystem/component/SnackBar.kt
around lines 162-180, the top-padding calculation uses hard-coded sizes
(snackBarHeight = 48.dp, 112.dp and 82.dp) that don't account for multi-line
snackbars, different device configurations or edge-to-edge insets; replace these
magic numbers with named constants (e.g., MATCHING_TOP_OFFSET,
NON_MATCHING_BOTTOM_OFFSET, SINGLE_LINE_HEIGHT, MULTI_LINE_HEIGHT) and document
them, measure the actual snackbar height using onGloballyPositioned to decide
between single- and multi-line heights (or compute dynamic height), and compute
padding using measured values and WindowInsets instead of subtracting
screenHeightDp to ensure correct positioning across orientations and screen
sizes.

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

♻️ Duplicate comments (1)
core/designsystem/src/main/java/com/puzzle/designsystem/component/SnackBar.kt (1)

43-49: Magic number 64.35.dp is unusual and fragile.

The value 64.35.dp appears to be a pixel-perfect calculation for a specific device/design spec. This precise decimal is fragile and unclear. Consider:

  1. Defining named constants with documentation explaining the origin (e.g., status bar + some offset)
  2. Using WindowInsets APIs to compute dynamic values based on actual system bar heights

The 82.dp bottom padding also lacks documentation. As noted in previous review, these hard-coded values should be extracted as named constants with clear explanations.

🔎 Suggested improvement
private object SnackBarPadding {
    /** Bottom offset for Info/TextOnly states - accounts for bottom navigation bar height */
    val BOTTOM_OFFSET = 82.dp
    /** Top offset for Matching state - accounts for status bar + design margin */
    val MATCHING_TOP_OFFSET = 64.dp // Consider using WindowInsets.statusBars instead
}
🧹 Nitpick comments (2)
core/designsystem/src/main/java/com/puzzle/designsystem/component/SnackBar.kt (1)

110-138: Preview renders two overlapping snackbars - consider splitting for clarity.

This preview renders two PieceSnackBar components without a container. Since each uses fillMaxSize() internally, they overlay each other (one at TopCenter, one at BottomCenter). While this happens to work visually to show both states, it's unconventional and could be confusing.

Consider either:

  1. Splitting into separate preview functions for each state (like PieceInfoSnackBarPreview)
  2. Adding a comment explaining the intentional overlay behavior
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt (1)

78-96: LGTM! Side-effect collection properly implemented.

The lifecycle-aware collection using repeatOnStarted ensures snackbar side effects are only processed when the screen is active, preventing leaks and stale UI updates.

Optional: The side-effect handling logic (lines 82-91) is duplicated in MatchingDetailScreen.kt (lines 87-97). Consider extracting this into a common composable function like CollectMatchingSnackBarSideEffect to reduce duplication.

🔎 Example extraction

Create a shared composable in a common utilities file:

@Composable
internal fun CollectMatchingSnackBarSideEffect(
    sideEffects: Flow<MatchingSideEffect>,
    eventHelper: EventHelper
) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    
    LaunchedEffect(Unit) {
        lifecycleOwner.repeatOnStarted {
            sideEffects.collect { sideEffect ->
                when (sideEffect) {
                    is MatchingSideEffect.ShowMatchingSnackBar -> {
                        val message = sideEffect.count?.let { count ->
                            context.getString(sideEffect.resId, count)
                        } ?: context.getString(sideEffect.resId)
                        
                        eventHelper.sendEvent(
                            PieceEvent.ShowSnackBar(SnackBarState.Matching(message))
                        )
                    }
                }
            }
        }
    }
}

Then use it in both screens:

CollectMatchingSnackBarSideEffect(
    sideEffects = viewModel.sideEffects,
    eventHelper = viewModel.eventHelper
)
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8409ec3 and a199db5.

📒 Files selected for processing (7)
  • core/designsystem/src/main/java/com/puzzle/designsystem/component/SnackBar.kt (3 hunks)
  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailScreen.kt (2 hunks)
  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailViewModel.kt (5 hunks)
  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/common/contract/MatchingSideEffect.kt (1 hunks)
  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/contract/MatchingDetailSideEffect.kt (0 hunks)
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt (5 hunks)
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt (7 hunks)
💤 Files with no reviewable changes (1)
  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/contract/MatchingDetailSideEffect.kt
🧰 Additional context used
🧬 Code graph analysis (1)
core/designsystem/src/main/java/com/puzzle/designsystem/component/SnackBar.kt (1)
core/designsystem/src/main/java/com/puzzle/designsystem/foundation/Theme.kt (1)
  • PieceTheme (15-18)
⏰ 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
🔇 Additional comments (11)
core/designsystem/src/main/java/com/puzzle/designsystem/component/SnackBar.kt (1)

51-83: Improved layout approach using alignment-based positioning.

The refactored layout using Box with contentAlignment and systemBarsPadding() is cleaner than the previous calculation-heavy approach. The structure properly:

  • Uses fillMaxSize() on the container for positioning flexibility
  • Applies systemBarsPadding() for proper edge-to-edge support
  • Wraps content width on the inner Row so the snackbar sizes to its content
feature/matching/src/main/java/com/puzzle/matching/graph/detail/common/contract/MatchingSideEffect.kt (1)

1-7: LGTM! Clean side-effect contract.

The sealed class structure is well-designed with proper resource ID annotation and flexible count parameter for formatted strings.

feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailScreen.kt (2)

60-60: LGTM! Import updated correctly.

The migration from MatchingDetailSideEffect to MatchingSideEffect is properly reflected in the import.


82-102: LGTM! Side-effect handling migrated correctly.

The LaunchedEffect correctly collects and handles the new MatchingSideEffect.ShowMatchingSnackBar type with proper string formatting logic.

feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt (3)

65-66: LGTM! Side-effect channel properly initialized.

The Channel-based side-effect mechanism is correctly set up with BUFFERED capacity and exposed as a Flow for UI consumption.


215-215: LGTM! Snackbar side effect emitted correctly.

The side effect properly notifies the UI about puzzle usage when acquiring a new instant match.


260-263: LGTM! Formatting improved for readability.

The multi-line formatting of the map update enhances code clarity without changing behavior.

feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailViewModel.kt (4)

34-34: LGTM! Side-effect type successfully migrated.

The Channel and import have been correctly updated from MatchingDetailSideEffect to MatchingSideEffect, maintaining consistency with the new contract.

Also applies to: 60-61


118-122: LGTM! Refuse matching side effect updated correctly.

The snackbar emission for match refusal now uses the unified MatchingSideEffect.ShowMatchingSnackBar type.


190-194: LGTM! Accept matching side effect updated correctly.

The snackbar emission for match acceptance now uses the unified MatchingSideEffect.ShowMatchingSnackBar type.


247-252: LGTM! Profile image purchase side effect updated correctly.

The snackbar emission for puzzle usage now uses the unified MatchingSideEffect.ShowMatchingSnackBar type with the correct puzzle count.

Comment on lines 229 to 237
// 2개를 쓴건지, free로 들어간건지 처리 필요함.
_sideEffects.send(
ShowMatchingSnackBar(
R.string.puzzle_used,
PUZZLE_COUNT_ACCEPT_MATCHING
)
)
navigationHelper.navigate(To(ContactRoute(matchId)))
}.onFailure { handlePurchaseError(it) }
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

Address the TODO comment about free vs. paid match handling.

The TODO comment indicates uncertainty about whether 2 puzzles were used or if the user accessed the contact via free match. This ambiguity could lead to incorrect snackbar messages shown to users.

Consider checking currentState.canFreeMatch or the match type to determine the correct puzzle count (or omit the snackbar entirely for free matches).

Do you want me to help implement logic to differentiate between free and paid contact access?

🤖 Prompt for AI Agents
In
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt
around lines 229 to 237, implement the TODO: determine whether the contact
access was a free match or consumed puzzles and show the snackbar accordingly.
Inspect the current state (e.g., currentState?.canFreeMatch or the match
object/type) and if the access was paid, send
ShowMatchingSnackBar(R.string.puzzle_used, PUZZLE_COUNT_ACCEPT_MATCHING),
otherwise either omit the snackbar or send a different message/count for free
matches; ensure you handle null currentState safely and remove the TODO comment;
keep the existing navigationHelper.navigate(To(ContactRoute(matchId))) and the
onFailure { handlePurchaseError(it) } unchanged.

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

♻️ Duplicate comments (1)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt (1)

231-237: Duplicate: TODO for free vs. paid match handling remains unaddressed.

This TODO comment and the unconditional snackbar emission were already flagged in a previous review. The issue is identical to the concern at lines 215-217: the code doesn't differentiate between free contact access and paid contact access, potentially showing incorrect puzzle consumption messages to users.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a199db5 and b5fe0bc.

📒 Files selected for processing (6)
  • feature/matching/src/main/java/com/puzzle/matching/graph/block/BlockScreen.kt (4 hunks)
  • feature/matching/src/main/java/com/puzzle/matching/graph/block/BlockViewModel.kt (1 hunks)
  • feature/matching/src/main/java/com/puzzle/matching/graph/block/contract/BlockIntent.kt (1 hunks)
  • feature/matching/src/main/java/com/puzzle/matching/graph/block/dialog/BlockDoneDialog.kt (3 hunks)
  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailViewModel.kt (5 hunks)
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailViewModel.kt
🧰 Additional context used
🧬 Code graph analysis (2)
feature/matching/src/main/java/com/puzzle/matching/graph/block/dialog/BlockDoneDialog.kt (1)
core/designsystem/src/main/java/com/puzzle/designsystem/foundation/Theme.kt (1)
  • PieceTheme (15-18)
feature/matching/src/main/java/com/puzzle/matching/graph/block/BlockViewModel.kt (1)
core/common-ui/src/main/java/com/puzzle/common/base/BaseViewModel.kt (1)
  • setState (39-41)
⏰ 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
🔇 Additional comments (8)
feature/matching/src/main/java/com/puzzle/matching/graph/block/contract/BlockIntent.kt (1)

9-9: LGTM!

The new OnDismissClick intent follows the established pattern and integrates cleanly with the existing sealed class hierarchy.

feature/matching/src/main/java/com/puzzle/matching/graph/block/BlockViewModel.kt (1)

32-32: LGTM!

The dismiss handling correctly resets the isBlockDone state, which will remove the dialog from the composition.

feature/matching/src/main/java/com/puzzle/matching/graph/block/dialog/BlockDoneDialog.kt (2)

34-37: Verify the sequential callback pattern.

The onClick handler calls onDismissRequest() followed immediately by onBlockDoneClick(). Both trigger asynchronous operations in the ViewModel:

  • onDismissRequest()OnDismissClick → async setState { copy(isBlockDone = false) }
  • onBlockDoneClick()OnBlockDoneClick → async navigation

Since both operations execute nearly simultaneously, the state update might not complete before navigation begins. While this may work fine in practice due to Compose's recomposition behavior, please verify that:

  1. The dialog dismisses cleanly before navigation
  2. No visual glitches occur during the transition
  3. The timing behaves as intended across different devices

48-56: LGTM!

The preview function is well-structured and provides a useful visualization of the dialog with localized content.

feature/matching/src/main/java/com/puzzle/matching/graph/block/BlockScreen.kt (3)

43-43: LGTM!

The onDismissClick callback is properly wired to the ViewModel intent, following the established pattern for other click handlers.


54-54: LGTM!

The onDismissClick parameter is properly added to the BlockScreen signature and correctly forwarded to BlockDoneDialog as onDismissRequest. The callback threading is clean and follows the established pattern.

Also applies to: 73-73


181-181: LGTM!

The preview is correctly updated with the new onDismissClick parameter.

feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt (1)

65-67: LGTM!

The side-effect channel pattern is correctly implemented using buffered capacity and exposed as a Flow for UI consumption. The internal visibility is appropriate for a feature module ViewModel.

Copy link
Member

@tgyuuAn tgyuuAn left a comment

Choose a reason for hiding this comment

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

영상이 있엇내요 하하.
네비게이션 될 때 스낵바가 dismiss되는건 자연스러운 현상이지 않나요(?)

대부분 각 feature 모듈에 scaffold랑 snackbarHost를 두니까 네비게이션되면 당연히 스낵바가 제거되어요.

하지만 피스는 싱글톤으로 스낵바를 관리하고 있어서 네비게이션될때 명시적으로 숨겨주고있어요.

만약 한 화면보다 생명주기가 긴 스낵바가 필요하다면 navigationEvent에 파라미터가 추가되면 될 것 같아요.

이와 관련해서는 민수님께 이전에 이야기 드렸어요~! @comst19

Comment on lines 80 to 81
lifecycleOwner.repeatOnStarted {
viewModel.sideEffects.collect { sideEffect ->
Copy link
Member

Choose a reason for hiding this comment

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

이거 민수님 pr에도 있었는데, common 모듈에 따로 없나용? 없으면 정의하는 것도 괜찮을 것 같아요

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

관련 로직 common 모듈에 공통화하여 푸시헀습니다~

deff465

Copy link
Member

Choose a reason for hiding this comment

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

헉 죄송해요. sideEffect 가 아니라, lifecycleOwner.repeatOnStrated 였어요.
대부분의 feature에서 현재는 sideEffect가 불필요한 상황이라 sideEffect를 Base로 추출하는 것은 아직 이른 것 같아요.
제가 리뷰를 명확하게 하지 못했나봐요 죄송해요 ㅠㅠㅠㅠㅠ

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

저도 고민하다가 분리한거긴한데,

   PieceEvent.ShowSnackBar(SnackBarState.TextOnly("필수 권한을 허용해주세요"))

지금 어느 뷰모델에서는 위와같이 하드코딩으로 사용하고,
특정 뷰모델에서는 아래와 같이

postSideEffect(SnackBarSideEffect.Matching(R.string.puzzle_used, PUZZLE_COUNT_CHECK_PICTURE))

R.String으로 사용하고있는데, 다 통일해서 사용하면 어떨까 해서 아예 base 까지 추출한 것 같아요.

추가의견 주시면 감사하겠습니다! @tgyuuAn

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

일단 matching 한정으로 분리해놓았습니다 !

추후 리팩토링하면서 더 고민해볼게요. 900bf01

Comment on lines 107 to +112

PieceSnackBarHost(
hostState = snackBarHostState,
snackbar = { snackBarData -> PieceSnackBar(snackBarData) },
)

Copy link
Member

Choose a reason for hiding this comment

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

Scaffold 밖으로 꺼낸 이유가 있나요?
Scaffold 내부에서는 snackbarHost 슬롯에 어떤 처리가 되고있나욥?.?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

스캐폴드 내부코드를 찾아보면 간단히

val snackbarOffsetFromBottom =
            if (snackbarHeight != 0) {
                snackbarHeight +
                    (fabOffsetFromBottom
                        ?: bottomBarHeight
                        ?: contentWindowInsets.getBottom(this@SubcomposeLayout))
            } else {
                0
            }

요런식으로 fab 여부, bottombar 여부, contentWindowInsets 여부로 위치를 판단해주고 있네요.

퍼즐 소비 플로우 추가 이후 MatchMain ↔ MatchDetail 전환이 Snackbar 와 함께 자주 발생하면서,
화면별 BottomBar 유무 차이로 Snackbar 위치가 통일되지 않는 문제가 생겼습니다. (디테일은 바텀바 안씀)

기존 코드에 BottomBar 여부의 분기를 추가하는 대신, Snackbar 를 Scaffold 외부로 분리하는 선택을 했습니다!

Comment on lines +34 to +37
onClick = {
onDismissRequest()
onBlockDoneClick()
},
Copy link
Member

Choose a reason for hiding this comment

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

이거 외부에서 처리하는게 더 깔끔할 것 같아요.
사용자가 실제론 onBlockDone만 클릭한거니까요.
상위 레벨에서 처리해주면 좋을 것 같아요

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

넵 처리완료했습니다.

사실 저 로직이 추가된게, ViewModel에서 BlockDone이 클릭됨과 동시에 dialog가 닫히고, navigate 가 순차적으로 이루어져야 하는데, setState가 비동기적으로 이루어져서 나오는 타이밍 이슈 때문이었어요.

관련 이슈 대응 커밋 - ed67d68

Copy link
Member

Choose a reason for hiding this comment

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

@kkh725 그렇다면 정석적으로 sideEffect로 대응하는 것도 방법일듯해요.
근데 BaseViewModel에 api 추가하는 것 보다는 기존 코드가 더 나은 것 같네요. 그대로 롤백 가능할까요 ?!
주석도 추가해주시면 좋을 것 같아요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

롤백하고 사이드이펙트로 대응하겠습니다 ~!

Copy link
Collaborator 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

♻️ Duplicate comments (1)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt (1)

245-251: TODO comment about free vs. paid match handling remains unaddressed.

This issue was already flagged in a previous review. The snackbar currently always shows PUZZLE_COUNT_ACCEPT_MATCHING regardless of whether the user accessed the contact via a free match or paid with puzzles.

🧹 Nitpick comments (1)
feature/matching/src/main/java/com/puzzle/matching/graph/main/page/MatchingUserScreen.kt (1)

44-47: Duplicate contentDescription on both parent and child containers.

Both PiecePullRefreshBox and the inner LazyColumn have the same contentDescription = "MatchingUserScreen". This can cause confusion in accessibility services and UI tests, as both elements will be announced/identified with the same description.

Consider keeping the description only on one element (preferably the outer container) or using distinct descriptions.

🔎 Proposed fix
     PiecePullRefreshBox(
         isRefreshing = isRefreshing,
         onRefresh = onRefresh,
         modifier = Modifier.semantics { contentDescription = "MatchingUserScreen" }
     ) {
         LazyColumn(
-            modifier = Modifier.semantics { contentDescription = "MatchingUserScreen" },
+            modifier = Modifier,
             verticalArrangement = Arrangement.spacedBy(12.dp)
         ) {
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1226c08 and 7f53b8e.

📒 Files selected for processing (6)
  • core/designsystem/src/main/java/com/puzzle/designsystem/component/Box.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/contract/MatchingIntent.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/contract/MatchingState.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/page/MatchingUserScreen.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt
🧰 Additional context used
🧬 Code graph analysis (1)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt (1)
core/common-ui/src/main/java/com/puzzle/common/base/BaseViewModel.kt (1)
  • setState (39-41)
⏰ 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
🔇 Additional comments (8)
feature/matching/src/main/java/com/puzzle/matching/graph/main/contract/MatchingState.kt (1)

20-20: LGTM!

The isRefreshing property is correctly added with a sensible default value and follows the existing state pattern.

feature/matching/src/main/java/com/puzzle/matching/graph/main/contract/MatchingIntent.kt (1)

14-14: LGTM!

The OnRefresh intent follows the established pattern for other intents in this sealed class.

feature/matching/src/main/java/com/puzzle/matching/graph/main/page/MatchingUserScreen.kt (1)

31-68: LGTM on the pull-to-refresh integration.

The PiecePullRefreshBox wrapper is correctly wired with isRefreshing and onRefresh, and the content structure is properly maintained.

feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt (4)

66-67: LGTM on the side effects Channel setup.

Using Channel<MatchingSideEffect>(BUFFERED) with receiveAsFlow() is a standard pattern for emitting one-time UI events from ViewModels in Jetpack Compose.


160-170: Well-structured refresh implementation.

The guard at line 161 prevents concurrent refresh operations, and the try-finally block ensures isRefreshing is always reset even if an error occurs. Using joinAll to wait for both initMatchInfo() and initFreeMatch() is appropriate since both functions handle their own errors internally.


276-279: LGTM on the formatting adjustment.

The multi-line formatting improves readability of the map update operation.


226-233: Snackbar condition logic requires verification for consistency.

In the getInstantNewMatch() function (lines 226-233), the snackbar is sent only when !currentState.canFreeMatch evaluates to true. However, this function is called directly from processNewMatchingCardOpen() (line 222) only when canFreeMatch is true, meaning the condition would evaluate to false and the snackbar would never show for that call path.

The snackbar logic should either:

  1. Check if puzzles were actually consumed (perhaps by comparing state before/after the network call)
  2. Be inverted if the intent is to show a snackbar on successful free match retrieval
  3. Include a clarifying comment explaining the intent if this behavior is intentional
core/designsystem/src/main/java/com/puzzle/designsystem/component/Box.kt (1)

11-36: Clean implementation of the pull-to-refresh wrapper, but note API stability concerns.

The composable correctly uses rememberPullToRefreshState and properly wires the state to both the PullToRefreshBox and Indicator. The @OptIn(ExperimentalMaterial3Api::class) annotation appropriately marks the use of the experimental API.

The PullToRefreshBox API remains experimental, which means it is subject to breaking changes in future releases. Previous versions introduced breaking changes to the API, and current versions still require the @ExperimentalMaterial3Api annotation. Consider the stability implications for your production roadmap, particularly if you plan to upgrade Material3 frequently.

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

🧹 Nitpick comments (2)
feature/matching/src/main/java/com/puzzle/matching/graph/block/dialog/BlockDoneDialog.kt (1)

33-33: Optional: Remove redundant lambda wrapper.

Since onBlockDoneClick is already of type () -> Unit, wrapping it in another lambda is unnecessary. The original direct reference was cleaner:

🔎 Simplify by removing the wrapper
-                onClick = { onBlockDoneClick() },
+                onClick = onBlockDoneClick,
core/common-ui/src/main/java/com/puzzle/common/effect/CollectSideEffect.kt (1)

17-21: Include sideEffectFlow as a LaunchedEffect key to handle flow instance changes.

If sideEffectFlow changes (e.g., different ViewModel), the effect won't restart and will continue collecting from the stale flow.

🔎 Proposed fix
-    LaunchedEffect(lifecycleOwner) {
+    LaunchedEffect(lifecycleOwner, sideEffectFlow) {
         lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
             sideEffectFlow.collect { effect -> onSideEffect(effect) }
         }
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f53b8e and ed67d68.

📒 Files selected for processing (13)
  • core/common-ui/src/main/java/com/puzzle/common/base/BaseContract.kt
  • core/common-ui/src/main/java/com/puzzle/common/base/BaseViewModel.kt
  • core/common-ui/src/main/java/com/puzzle/common/effect/CollectSideEffect.kt
  • core/common-ui/src/main/java/com/puzzle/common/effect/SnackBarSideEffect.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/block/BlockScreen.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/block/BlockViewModel.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/block/dialog/BlockDoneDialog.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailScreen.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailViewModel.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/report/ReportScreen.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/report/ReportViewModel.kt
✅ Files skipped from review due to trivial changes (1)
  • feature/matching/src/main/java/com/puzzle/matching/graph/report/ReportScreen.kt
🚧 Files skipped from review as they are similar to previous changes (5)
  • feature/matching/src/main/java/com/puzzle/matching/graph/block/BlockViewModel.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/block/BlockScreen.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailScreen.kt
🧰 Additional context used
🧬 Code graph analysis (3)
feature/matching/src/main/java/com/puzzle/matching/graph/block/dialog/BlockDoneDialog.kt (1)
core/designsystem/src/main/java/com/puzzle/designsystem/foundation/Theme.kt (1)
  • PieceTheme (15-18)
feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailViewModel.kt (1)
core/common-ui/src/main/java/com/puzzle/common/base/BaseViewModel.kt (2)
  • setState (43-44)
  • postSideEffect (48-49)
feature/matching/src/main/java/com/puzzle/matching/graph/report/ReportViewModel.kt (1)
core/common-ui/src/main/java/com/puzzle/common/base/BaseViewModel.kt (1)
  • setStateSync (46-46)
⏰ 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
🔇 Additional comments (9)
feature/matching/src/main/java/com/puzzle/matching/graph/block/dialog/BlockDoneDialog.kt (2)

8-8: LGTM! Imports support the preview function.

The Preview and PieceTheme imports are necessary for the new preview composable added at the end of the file.

Also applies to: 15-15


44-51: LGTM! Well-structured preview function.

The preview composable follows Compose best practices with proper theming and sample data, making it easy to visualize the dialog during development.

core/common-ui/src/main/java/com/puzzle/common/effect/SnackBarSideEffect.kt (1)

6-25: LGTM!

Clean sealed interface design with proper @StringRes annotations. The handle() extension provides a nice pattern for exhaustive handling of snackbar types.

core/common-ui/src/main/java/com/puzzle/common/base/BaseContract.kt (1)

6-7: LGTM!

Clean addition of the UiSideEffect marker interface to complete the unidirectional data flow contract.

core/common-ui/src/main/java/com/puzzle/common/base/BaseViewModel.kt (1)

26-27: LGTM!

The buffered channel with receiveAsFlow() is the correct pattern for one-shot side effects, ensuring events are not lost and are consumed by a single collector.

feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailViewModel.kt (3)

95-105: LGTM!

The refactored refuseMatching correctly sequences state update → side effect emission → navigation, aligning with the new SnackBarSideEffect-based architecture.


152-178: LGTM!

The acceptMatching flow properly sets isShowDialog = false before emitting the snackbar side effect and navigating, preventing UI state inconsistencies.


214-231: LGTM!

The purchaseProfileImage flow correctly uses postSideEffect with the PUZZLE_COUNT_CHECK_PICTURE constant, consistent with the new side effect architecture.

feature/matching/src/main/java/com/puzzle/matching/graph/report/ReportViewModel.kt (1)

34-43: State reset is redundant but harmless.

The ViewModel is automatically cleared when the Report destination is popped from the back stack, so the synchronous reset of isReportDone to false before navigating is unnecessary. However, this defensive practice causes no harm and the code is acceptable as-is.

protected fun setState(reduce: S.() -> S) =
viewModelScope.launch { _reducer.send(reduce) }

protected fun setStateSync(reduce: S.() -> S) { _state.update { currentState.reduce() }}
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

Race condition: use the lambda parameter instead of currentState.

currentState is read before the atomic compare-and-swap, so concurrent updates can cause lost state modifications. The update lambda receives the current value atomically—use that instead.

🔎 Proposed fix
-    protected fun setStateSync(reduce: S.() -> S) { _state.update { currentState.reduce() }}
+    protected fun setStateSync(reduce: S.() -> S) { _state.update { it.reduce() } }
📝 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
protected fun setStateSync(reduce: S.() -> S) { _state.update { currentState.reduce() }}
protected fun setStateSync(reduce: S.() -> S) { _state.update { it.reduce() } }
🤖 Prompt for AI Agents
In core/common-ui/src/main/java/com/puzzle/common/base/BaseViewModel.kt around
line 46, the state update uses currentState inside _state.update which reads the
value outside the atomic lambda and can lose concurrent updates; change the code
to use the lambda parameter provided by update (e.g., use it as the receiver or
named parameter) so the reducer is applied to the current value passed into the
lambda, ensuring the update is performed atomically and avoiding race
conditions.

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)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt (1)

78-86: Consider simplifying the when expression.

The when block with a single branch checking effect is SnackBarSideEffect can be simplified to an if statement for better readability.

🔎 Proposed simplification
-    CollectSideEffect(viewModel.sideEffect) { effect ->
-        when {
-            effect is SnackBarSideEffect -> effect.handle { _, count ->
-                val message = effect.getMessage(context, count)
-
-                viewModel.eventHelper.sendEvent(PieceEvent.ShowSnackBar(SnackBarState.Matching(message)))
-            }
-        }
-    }
+    CollectSideEffect(viewModel.sideEffect) { effect ->
+        if (effect is SnackBarSideEffect) {
+            effect.handle { _, count ->
+                val message = effect.getMessage(context, count)
+                viewModel.eventHelper.sendEvent(PieceEvent.ShowSnackBar(SnackBarState.Matching(message)))
+            }
+        }
+    }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e7f243 and 2beed7a.

📒 Files selected for processing (3)
  • core/common-ui/src/main/java/com/puzzle/common/effect/SnackBarSideEffect.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailScreen.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailScreen.kt
  • core/common-ui/src/main/java/com/puzzle/common/effect/SnackBarSideEffect.kt
🔇 Additional comments (6)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt (6)

26-31: Good refactoring: Common module integration.

The addition of getMessage to the common module aligns with the previous review feedback. The new imports support analytics tracking and side-effect handling, which are properly utilized in this file.

Based on learnings, this addresses the previous concern about common module organization.


111-111: LGTM!

The onRefresh callback is properly wired to the ViewModel through MatchingIntent.OnRefresh, following the established pattern for other user interactions.


126-128: LGTM!

The function signature properly extends the public API with onRefresh, and the analytics helper is correctly retrieved for event tracking.


303-304: LGTM!

The preview function is properly updated with sensible default values for the new refresh parameters.


236-242: Refresh state management is correctly implemented.

The state.isRefreshing is properly managed in the ViewModel: it's set to true when refresh starts (line 153) and false when complete (line 158 in finally block), with a guard clause preventing concurrent refreshes. The parameters are correctly wired to MatchingUserScreen.


135-141: Track analytics before calling the parent accept handler to ensure events are captured.

Analytics events should be tracked before calling onAcceptClick(NewMatching) to guarantee they are recorded before any dialog dismissal or navigation. In Jetpack Compose, when callbacks modify composition state, subsequent operations may not execute if the composable is immediately removed from the composition.

Also applies to: 148-154, 159-165

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

🧹 Nitpick comments (2)
core/common-ui/src/main/java/com/puzzle/common/effect/SnackBarSideEffect.kt (2)

7-8: Consider adding @StringRes annotation to the interface property.

While the implementations already use @param:StringRes, adding @StringRes to the interface property would improve type safety documentation and provide better IDE support.

🔎 Proposed addition
 sealed interface SnackBarSideEffect : UiSideEffect {
-    val resId: Int
+    @get:StringRes
+    val resId: Int

38-40: Remove trailing empty lines.

The file has multiple trailing blank lines that can be removed for consistency.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2beed7a and f60bc98.

📒 Files selected for processing (1)
  • core/common-ui/src/main/java/com/puzzle/common/effect/SnackBarSideEffect.kt

Comment on lines +28 to +36
fun SnackBarSideEffect.getMessage(
context: Context,
): String = when(this){
is SnackBarSideEffect.Matching -> {
if (count != null) context.getString(resId,count)
else context.getString(resId)
}
else -> context.getString(resId)
}
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:

find . -type f -name "SnackBarSideEffect.kt" 2>/dev/null

Repository: Piece-Puzzly/Piece-Android

Length of output: 147


🏁 Script executed:

cat -n ./core/common-ui/src/main/java/com/puzzle/common/effect/SnackBarSideEffect.kt

Repository: Piece-Puzzly/Piece-Android

Length of output: 1513


🏁 Script executed:

rg -n 'SnackBarSideEffect\.Matching|getString.*count' --type kotlin

Repository: Piece-Puzzly/Piece-Android

Length of output: 1280


🏁 Script executed:

find . -type f -name "strings.xml" -path "*/values*" | head -20

Repository: Piece-Puzzly/Piece-Android

Length of output: 159


🏁 Script executed:

rg -n 'match_refuse|match_accept|puzzle_used' ./app/src/main/res/values/strings.xml ./core/designsystem/src/main/res/values/strings.xml

Repository: Piece-Puzzly/Piece-Android

Length of output: 389


Fix formatting issues in getMessage function

Two minor spacing violations need correction:

  • Line 30: Add space after when keyword: when (this) instead of when(this)
  • Line 32: Add space after comma in function call: getString(resId, count) instead of getString(resId,count)

String resources are properly formatted with placeholders matching their usage—no action needed.

 fun SnackBarSideEffect.getMessage(
     context: Context,
-): String = when(this){
+): String = when (this) {
     is SnackBarSideEffect.Matching -> {
-        if (count != null) context.getString(resId,count)
+        if (count != null) context.getString(resId, count)
         else context.getString(resId)
     }
     else -> context.getString(resId)
 }
📝 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 SnackBarSideEffect.getMessage(
context: Context,
): String = when(this){
is SnackBarSideEffect.Matching -> {
if (count != null) context.getString(resId,count)
else context.getString(resId)
}
else -> context.getString(resId)
}
fun SnackBarSideEffect.getMessage(
context: Context,
): String = when (this) {
is SnackBarSideEffect.Matching -> {
if (count != null) context.getString(resId, count)
else context.getString(resId)
}
else -> context.getString(resId)
}
🤖 Prompt for AI Agents
In core/common-ui/src/main/java/com/puzzle/common/effect/SnackBarSideEffect.kt
around lines 28 to 36, fix two spacing style issues: change `when(this)` to
`when (this)` and change `getString(resId,count)` to `getString(resId, count)`
so the code follows the project's spacing conventions.

CollectSideEffect(viewModel.sideEffect) { effect ->
when {
effect is SnackBarSideEffect -> effect.handle { _, count ->
val message = effect.getMessage(context, count)
Copy link
Collaborator

Choose a reason for hiding this comment

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

fun SnackBarSideEffect.getMessage(
    context: Context,
): String = when(this){
    is SnackBarSideEffect.Matching -> {
        if (count != null) context.getString(resId,count)
        else context.getString(resId)
    }
    else -> context.getString(resId)
}

위와 같이 바뀌었으면 count는 빠져야 할 거 같아요

최소한 빌드는 성공 확인하고 커밋하는게 좋을 거 같아요~

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

수면코딩 issue 였던것.. 수정했어욥 b88ffd9

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

🧹 Nitpick comments (1)
feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailScreen.kt (1)

81-89: Review comment is accurate; refactoring is good with optional improvements suggested.

The lifecycle-aware side-effect collection using CollectSideEffect is properly abstracted, and the SnackBarSideEffect handling correctly resolves messages and emits ShowSnackBar events.

Consider these optional improvements:

  • Replace the single-branch when with a simpler if (effect is SnackBarSideEffect) for improved clarity.
  • The effect.handle { _, _ -> } lambda ignores both parameters. Verify this is intentional—this pattern only executes the onMatching callback while Info and TextOnly effects silently use empty defaults. If all SnackBarSideEffect types should be handled uniformly, a more direct approach may improve readability.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f60bc98 and b88ffd9.

📒 Files selected for processing (2)
  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailScreen.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt
🧰 Additional context used
🧬 Code graph analysis (2)
feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailScreen.kt (1)
core/common-ui/src/main/java/com/puzzle/common/effect/CollectSideEffect.kt (1)
  • CollectSideEffect (10-22)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt (2)
core/common-ui/src/main/java/com/puzzle/common/effect/CollectSideEffect.kt (1)
  • CollectSideEffect (10-22)
feature/matching/src/main/java/com/puzzle/matching/graph/detail/dialog/PurchaseMatchingDialog.kt (2)
  • PurchaseDialog (18-75)
  • InsufficientPuzzleDialog (77-104)
🔇 Additional comments (6)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt (5)

26-31: LGTM! Imports support new side effect handling and analytics.

The new imports for CollectSideEffect, SnackBarSideEffect, analytics helpers, and message utilities properly support the lifecycle-aware side effect collection and analytics tracking added in this PR.


78-86: LGTM! Lifecycle-aware side effect handling implemented correctly.

The CollectSideEffect pattern properly collects the viewModel's side effect flow with lifecycle awareness (per past review feedback) and correctly handles SnackBarSideEffect by extracting the message and dispatching it via PieceEvent.ShowSnackBar.


111-111: LGTM! Pull-to-refresh parameter properly propagated.

The onRefresh parameter is correctly added to both MatchingRoute and MatchingScreen signatures, enabling pull-to-refresh functionality as described in the PR objectives.

Also applies to: 126-126


236-237: LGTM! Refresh state and callback properly wired to MatchingUserScreen.

The isRefreshing state and onRefresh callback are correctly propagated to MatchingUserScreen, enabling pull-to-refresh UI indication and action handling.

Also applies to: 242-242


303-304: LGTM! Preview updated with new refresh parameters.

The preview correctly includes the new isRefreshing and onRefresh parameters with appropriate default values for preview rendering.

feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailScreen.kt (1)

31-34: LGTM! Clean import additions for the new side effect pattern.

The new imports support the refactored side-effect handling mechanism and are all actively used.

Comment on lines 135 to 141
onAcceptClick = {
onAcceptClick(NewMatching)
analyticsHelper.trackClickEvent(
screenName = "new_matching_dialog",
buttonName = "new_matching",
)
}
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

Analytics screen name inconsistency across dialog tracking.

The analytics screenName values used here differ from those in the dialog components themselves:

Dialog Dialog TrackScreenViewEvent MatchingScreen trackClickEvent
NewMatching "match_main_new_match_dialog" "new_matching_dialog" (Line 138)
Contact "match_main_contact_dialog" "contact_matching_dialog" (Line 151)
InSufficient "match_main_accept_popup" "insufficient_puzzle_dialog" (Line 162)

This inconsistency can fragment analytics data and make tracking user flows difficult. Consider using consistent naming conventions across screen view events and click events for the same UI component.

Based on relevant code snippets from:

  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/dialog/PurchaseMatchingDialog.kt (lines 17-74, 76-103)
🔎 Suggested fix: align with dialog screen names
                    onAcceptClick = {
                        onAcceptClick(NewMatching)
                        analyticsHelper.trackClickEvent(
-                           screenName = "new_matching_dialog",
+                           screenName = "match_main_new_match_dialog",
                            buttonName = "new_matching",
                        )
                    }
                onAcceptClick = {
                    onAcceptClick(Contact(state.dialogType.match))
                    analyticsHelper.trackClickEvent(
-                       screenName = "contact_matching_dialog",
+                       screenName = "match_main_contact_dialog",
                        buttonName = "contact_matching",
                    )
                }
                onAcceptClick = {
                    onAcceptClick(InSufficient)
                    analyticsHelper.trackClickEvent(
-                       screenName = "insufficient_puzzle_dialog",
+                       screenName = "match_main_accept_popup",
                        buttonName = "purchase_puzzle",
                    )
                }

Also applies to: 148-154, 159-165

🤖 Prompt for AI Agents
In
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt
around lines 135-141 (and similarly adjust lines 148-154 and 159-165), the
analytics trackClickEvent screenName values are inconsistent with the dialog
components; update the screenName strings to match the dialog
TrackScreenViewEvent names: replace "new_matching_dialog" with
"match_main_new_match_dialog", "contact_matching_dialog" with
"match_main_contact_dialog", and "insufficient_puzzle_dialog" with
"match_main_accept_popup" so click events and dialog view events use the same
identifiers across the codebase.

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 (2)
feature/matching/src/main/java/com/puzzle/matching/graph/detail/common/contract/effect/SnackBarSideEffect.kt (1)

29-31: Fix spacing style issues.

There are spacing violations that were previously flagged:

  • Line 29: Add space after when and before {: when (this) {
  • Line 31: Add space after comma: getString(resId, count)
🔎 Proposed fix
 fun SnackBarSideEffect.getMessage(
     context: Context,
-): String = when(this){
+): String = when (this) {
     is SnackBarSideEffect.Matching -> {
-        if (count != null) context.getString(resId,count)
+        if (count != null) context.getString(resId, count)
         else context.getString(resId)
     }
     else -> context.getString(resId)
 }
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt (1)

139-145: Analytics screen name inconsistency still present.

The analytics screenName values used in the click tracking here remain inconsistent with the dialog components' TrackScreenViewEvent names, as previously flagged:

Dialog Dialog screen name MatchingScreen screen name
NewMatching "match_main_new_match_dialog" "new_matching_dialog" (Line 142)
Contact "match_main_contact_dialog" "contact_matching_dialog" (Line 155)
InSufficient "match_main_accept_popup" "insufficient_puzzle_dialog" (Line 166)

This inconsistency fragments analytics data and makes tracking user flows difficult. Please align the screenName values to match the dialog component names for consistent tracking.

Also applies to: 152-158, 163-169

🧹 Nitpick comments (2)
feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailScreen.kt (1)

85-90: Consider simplifying side-effect handling.

The handle() function provides type-specific dispatch, but here you're calling getMessage() uniformly for all types and sending the same SnackBarState.Matching regardless of the effect type. This could be simplified:

viewModel.sideEffect.collect { effect ->
    val message = effect.getMessage(context)
    viewModel.eventHelper.sendEvent(ShowSnackBar(SnackBarState.Matching(message)))
}

Alternatively, if different effect types should produce different SnackBarState variants (e.g., SnackBarState.Info for SnackBarSideEffect.Info), consider utilizing handle() properly for type-specific behavior.

feature/matching/src/main/java/com/puzzle/matching/graph/detail/common/contract/effect/SnackBarSideEffect.kt (1)

37-39: Remove trailing blank lines.

There are unnecessary trailing blank lines at the end of the file.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b88ffd9 and 900bf01.

📒 Files selected for processing (15)
  • core/common-ui/src/main/java/com/puzzle/common/base/BaseContract.kt
  • core/common-ui/src/main/java/com/puzzle/common/base/BaseViewModel.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/block/BlockScreen.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/block/BlockViewModel.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/block/contract/BlockIntent.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/block/dialog/BlockDoneDialog.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailScreen.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailViewModel.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/detail/common/contract/effect/SnackBarSideEffect.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/report/ReportScreen.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/report/ReportViewModel.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/report/contract/ReportIntent.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/report/dialog/ReportDoneDialog.kt
✅ Files skipped from review due to trivial changes (1)
  • core/common-ui/src/main/java/com/puzzle/common/base/BaseContract.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • feature/matching/src/main/java/com/puzzle/matching/graph/block/BlockViewModel.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/report/ReportViewModel.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt
🧰 Additional context used
🧬 Code graph analysis (3)
feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailViewModel.kt (1)
core/common-ui/src/main/java/com/puzzle/common/base/BaseViewModel.kt (1)
  • setState (39-40)
feature/matching/src/main/java/com/puzzle/matching/graph/block/dialog/BlockDoneDialog.kt (1)
core/designsystem/src/main/java/com/puzzle/designsystem/foundation/Theme.kt (1)
  • PieceTheme (15-18)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt (1)
feature/matching/src/main/java/com/puzzle/matching/graph/detail/dialog/PurchaseMatchingDialog.kt (2)
  • PurchaseDialog (18-75)
  • InsufficientPuzzleDialog (77-104)
🔇 Additional comments (20)
feature/matching/src/main/java/com/puzzle/matching/graph/block/contract/BlockIntent.kt (1)

9-9: LGTM!

The new OnDismissClick intent follows the existing pattern and integrates well with the dismiss handling flow described in the PR objectives.

feature/matching/src/main/java/com/puzzle/matching/graph/block/dialog/BlockDoneDialog.kt (2)

23-24: Consider wiring onDismissRequest to PieceDialog.

The new onDismissRequest parameter is added to the function signature but PieceDialog still receives an empty lambda. If users can dismiss the dialog by tapping outside (depending on PieceDialog implementation), the dismiss callback won't fire.

If this is intentional (forcing users to use the button), consider adding a brief comment to clarify.


48-56: LGTM!

Preview composable is well-structured and follows the project's pattern for using PieceTheme.

feature/matching/src/main/java/com/puzzle/matching/graph/block/BlockScreen.kt (2)

43-44: LGTM!

The onDismissClick callback is properly wired through the component hierarchy from BlockRoute to BlockDoneDialog. This enables the ViewModel to handle the dismiss action via BlockIntent.OnDismissClick, addressing the timing issues mentioned in the PR discussion.

Also applies to: 54-54, 73-73


171-183: LGTM!

Preview updated correctly to include the new onDismissClick parameter.

core/common-ui/src/main/java/com/puzzle/common/base/BaseViewModel.kt (1)

39-40: LGTM! Clean refactor to expression body syntax.

The refactor from block body to expression body syntax is a clean style improvement with no behavioral change. The function still correctly launches a coroutine and sends the reduce function to the _reducer channel.

feature/matching/src/main/java/com/puzzle/matching/graph/report/contract/ReportIntent.kt (1)

9-9: LGTM!

The new OnDismissClick intent follows the existing pattern and provides a clear, consistent way to handle dialog dismissal in the report flow.

feature/matching/src/main/java/com/puzzle/matching/graph/report/dialog/ReportDoneDialog.kt (2)

19-22: LGTM!

The addition of onDismissRequest parameter provides proper separation between dialog dismissal and navigation logic. The empty lambda for PieceDialog.onDismissRequest enforces that users must interact with the button, which is appropriate for a confirmation dialog.


32-35: Verify callback order is intentional.

The onDismissRequest() is invoked before onReportDoneClick(). Ensure this order is intentional—typically dismiss/cleanup happens before navigation triggers.

feature/matching/src/main/java/com/puzzle/matching/graph/report/ReportScreen.kt (1)

53-53: LGTM!

The onDismissClick callback is properly wired through the component hierarchy from ReportRouteReportScreenReportDoneDialog, providing a clean separation of concerns for dialog dismissal handling.

Also applies to: 65-65, 91-91

feature/matching/src/main/java/com/puzzle/matching/graph/detail/common/contract/effect/SnackBarSideEffect.kt (1)

6-13: LGTM on the sealed interface design.

The SnackBarSideEffect hierarchy provides a clean, type-safe way to represent different snackbar notification types with the shared resId property. The Matching variant appropriately includes an optional count for parameterized string formatting.

feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailViewModel.kt (4)

61-62: LGTM on the side-effect channel setup.

Using Channel<SnackBarSideEffect>(BUFFERED) with receiveAsFlow() is the appropriate pattern for one-shot UI events that should survive configuration changes until consumed.


105-107: Verify state update ordering with side-effect emission.

setState internally launches a coroutine (see BaseViewModel.setState), while _sideEffect.send() suspends. Since these are in the same launch block and send() is awaited, the ordering should be correct, but be aware that the state update is dispatched asynchronously.

If strict ordering is required (state updated before snackbar appears), consider using a synchronous state update or ensuring the UI observes state changes before processing side effects.


53-53: Verify internal visibility for eventHelper.

eventHelper is exposed as internal, which allows access from test code in the same module. Confirm this is intentional and not an oversight.


224-224: LGTM on parameterized snackbar message.

The SnackBarSideEffect.Matching with PUZZLE_COUNT_CHECK_PICTURE count properly utilizes the formatted string resource for displaying puzzle usage information.

feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt (5)

12-12: LGTM: Import additions support new functionality.

The new imports appropriately support side effect handling, lifecycle-aware collection, analytics tracking, and the ShowSnackBar wrapper pattern.

Also applies to: 26-26, 28-28, 30-30, 34-34, 44-45


70-70: LGTM: Lifecycle-aware side effect handling is correctly implemented.

The pattern uses repeatOnStarted to collect side effects only when the lifecycle is in STARTED state or higher, which prevents collecting flows when the screen is not visible. The conversion from side effects to ShowSnackBar events through eventHelper maintains separation between ViewModel side effects and UI event handling.

Also applies to: 80-90


97-97: LGTM: Consistent use of ShowSnackBar wrapper.

The back-button snackbar correctly uses the ShowSnackBar wrapper pattern, consistent with the new side effect handling system introduced in this PR.


115-115: LGTM: onRefresh callback properly wired for pull-to-refresh.

The onRefresh callback is correctly added to the function signatures and properly propagated from MatchingRoute through MatchingScreen to MatchingUserScreen, enabling the pull-to-refresh functionality mentioned in the PR objectives.

Also applies to: 130-130


240-240: LGTM: Refresh state and callback properly propagated.

The isRefreshing state and onRefresh callback are correctly passed to MatchingUserScreen, and the preview is updated with appropriate default values. This completes the pull-to-refresh wiring introduced in earlier segments.

Also applies to: 246-246, 307-308

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

♻️ Duplicate comments (1)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt (1)

143-149: Analytics screen names are inconsistent with dialog TrackScreenViewEvent values.

As flagged in a previous review, the screenName values here don't match those in the dialog components:

Dialog Dialog TrackScreenViewEvent MatchingScreen trackClickEvent
NewMatching "match_main_new_match_dialog" "new_matching_dialog"
Contact "match_main_contact_dialog" "contact_matching_dialog"
InSufficient "match_main_accept_popup" "insufficient_puzzle_dialog"

Also applies to: 156-162, 167-173

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 900bf01 and 1b3e95f.

📒 Files selected for processing (2)
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt
🧰 Additional context used
🧬 Code graph analysis (1)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt (2)
core/common/src/main/java/com/puzzle/common/ResultUtil.kt (1)
  • suspendRunCatching (5-13)
core/common-ui/src/main/java/com/puzzle/common/base/BaseViewModel.kt (1)
  • setState (39-40)
⏰ 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
🔇 Additional comments (6)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt (3)

69-71: Side effect channel setup looks good.

Using Channel.BUFFERED with receiveAsFlow() is appropriate for emitting side effects that should be collected by the UI layer.


135-139: Return type change: getRejectReason now returns Result<*>.

The function now returns the result of suspendRunCatching, but the return value isn't used at the call site (line 114). This is fine but consider if the return value adds value or if the previous void-like behavior was intentional.


259-265: Snackbar always emitted on contact purchase success.

Unlike getInstantNewMatch, this path always emits the snackbar on success, which is correct since postOpponentContact is only called from the dialog confirmation (paid path).

feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingScreen.kt (3)

119-119: onRefresh callback wiring looks correct.

The callback is properly passed from MatchingRoute to MatchingScreen and then to MatchingUserScreen.

Also applies to: 134-134


243-251: MatchingUserScreen now receives isRefreshing and onRefresh props.

The pull-to-refresh state and callback are correctly passed to enable the refresh functionality.


311-312: Preview updated with new parameters.

Preview composable correctly includes the new isRefreshing and onRefresh parameters with sensible defaults.

Comment on lines +238 to +245
if (!currentState.canFreeMatch)
_sideEffect.send(
SnackBarSideEffect.Matching(
R.string.puzzle_used,
PUZZLE_COUNT_NEW_MATCH
)
)

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

Unreachable code: snackbar emission will never execute.

getInstantNewMatch() is only called from processNewMatchingCardOpen() when currentState.canFreeMatch is true (line 231), or from processConfirmDialog() for NewMatching dialog (which appears after the free match check fails). However, the condition !currentState.canFreeMatch at line 238 will be false when called directly from line 231, making this snackbar emission unreachable in that path.

For the dialog confirmation path, the state may still reflect the pre-purchase value since it hasn't been refreshed after the dialog confirmation.

Consider:

  1. Passing a parameter indicating whether this is a paid match
  2. Always showing the snackbar when called from the dialog confirmation path
  3. Refreshing canFreeMatch state before this check
🔎 Proposed approach - pass explicit parameter
-private suspend fun getInstantNewMatch() = suspendRunCatching {
+private suspend fun getInstantNewMatch(isPaidMatch: Boolean = false) = suspendRunCatching {
     matchingRepository.getNewInstantMatch()
 }.onSuccess { matchId ->
-    if (!currentState.canFreeMatch)
+    if (isPaidMatch)
         _sideEffect.send(
             SnackBarSideEffect.Matching(
                 R.string.puzzle_used,
                 PUZZLE_COUNT_NEW_MATCH
             )
         )

     navigationHelper.navigate(To(MatchingDetailRoute(matchId)))
 }.onFailure { handlePurchaseError(it) }

Then update the call sites:

// In processNewMatchingCardOpen - free match, no snackbar
if (currentState.canFreeMatch) getInstantNewMatch(isPaidMatch = false)

// In processConfirmDialog - paid match from dialog
is MatchingDialogType.NewMatching -> getInstantNewMatch(isPaidMatch = true)

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

🤖 Prompt for AI Agents
In
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt
around lines 238 to 245, the snackbar emission guarded by
"!currentState.canFreeMatch" is unreachable from the free-match call path and
uses stale state from the dialog confirmation path; change getInstantNewMatch to
accept a boolean parameter (e.g., isPaidMatch) and use that explicit flag to
decide whether to emit the snackbar, update all call sites
(processNewMatchingCardOpen -> pass false, processConfirmDialog for NewMatching
-> pass true), and remove the reliance on currentState.canFreeMatch inside
getInstantNewMatch (or refresh state before checking) so the snackbar behavior
is driven by the passed parameter.

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)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt (1)

238-244: Unreachable/unreliable snackbar emission due to state timing.

The condition !currentState.canFreeMatch is unreliable:

  • When called from the free match path (line 231), canFreeMatch is true, so the snackbar correctly doesn't emit
  • When called from dialog confirmation (line 252), currentState.canFreeMatch may still be true (not yet refreshed after purchase), causing the snackbar to be skipped even though puzzles were consumed

The previous review suggested passing an explicit isPaidMatch boolean parameter to make the logic deterministic. Consider implementing that approach:

🔎 Recommended fix - add explicit parameter
-private suspend fun getInstantNewMatch() = suspendRunCatching {
+private suspend fun getInstantNewMatch(isPaidMatch: Boolean = false) = suspendRunCatching {
     matchingRepository.getNewInstantMatch()
 }.onSuccess { matchId ->
-    if (!currentState.canFreeMatch)
+    if (isPaidMatch)
         _sideEffect.send(
             SnackBarSideEffect.Matching(
                 R.string.puzzle_used,
                 PUZZLE_COUNT_NEW_MATCH
             )
         )

     navigationHelper.navigate(To(MatchingDetailRoute(matchId)))
 }.onFailure { handlePurchaseError(it) }

Then update call sites:

// Line 231: Free match, no snackbar
if (currentState.canFreeMatch) getInstantNewMatch(isPaidMatch = false)

// Line 252: Paid match from dialog
is MatchingDialogType.NewMatching -> getInstantNewMatch(isPaidMatch = true)
🧹 Nitpick comments (1)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt (1)

135-139: Consider explicit return type or result handling.

The function now returns Result<T> but the call site at line 114 ignores the return value, relying solely on side effects in onSuccess/onFailure. While this works, it may be clearer to either:

  1. Explicitly declare the return type if it's intended for future use
  2. Keep it as a fire-and-forget suspend function if the result won't be consumed
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b3e95f and 5b4857d.

📒 Files selected for processing (1)
  • feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt
🧰 Additional context used
🧬 Code graph analysis (1)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt (2)
core/common/src/main/java/com/puzzle/common/ResultUtil.kt (1)
  • suspendRunCatching (5-13)
core/common-ui/src/main/java/com/puzzle/common/base/BaseViewModel.kt (1)
  • setState (39-40)
⏰ 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
🔇 Additional comments (5)
feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt (5)

69-71: LGTM! Side effect mechanism properly implemented.

The Channel-to-Flow pattern for side effects is correctly set up with buffered capacity, allowing non-blocking emission of snackbar events to the UI layer.


87-87: LGTM! OnRefresh intent properly handled.

The intent handler correctly delegates to the new refresh function, supporting the pull-to-refresh functionality mentioned in the PR objectives.


169-179: LGTM! Refresh logic correctly implemented.

The function properly guards against concurrent refreshes, uses joinAll for concurrent data fetching, and ensures isRefreshing state is reset in the finally block. The previous issue with non-existent method names has been resolved.


259-264: LGTM! Snackbar correctly emitted after contact access.

The snackbar emission is unconditional and appropriate here, as accessing an opponent's contact via the dialog confirmation always consumes puzzles.


290-293: LGTM! Consistent use of currentState property.

Using the currentState convenience property is appropriate and consistent with the rest of the codebase.

@comst19 comst19 merged commit f68f228 into develop Dec 25, 2025
2 checks passed
@comst19 comst19 deleted the fix/PC-1485 branch December 25, 2025 12:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ㄱㅎ근형 🌙 리뷰 원해요🔥 피어의 리뷰를 기다리는 ing.. 🔥

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants