Skip to content

에러 처리 중앙화를 위한 코드를 작성한다#69

Merged
komodgn merged 4 commits intodevelopfrom
feature/#68
Jan 8, 2026
Merged

에러 처리 중앙화를 위한 코드를 작성한다#69
komodgn merged 4 commits intodevelopfrom
feature/#68

Conversation

@komodgn
Copy link
Owner

@komodgn komodgn commented Jan 8, 2026

Summary by CodeRabbit

  • New Features

    • Added centralized error dialog system with scope-specific error handling (global and image analysis).
    • Implemented event-driven dialog flow for streamlined error notifications.
  • Bug Fixes

    • Made toast messages optional for improved null-safety and rendering control.
    • Enhanced error messaging for delete operations with dynamic feedback.
  • Refactor

    • Restructured dialog confirm/dismiss parameter handling for clarity.
    • Improved error routing and exception categorization across features.

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

- 공통 예외 핸들링 유틸리티 handleException 추가
- 예외 상황에 따라 토스트 또는 다이얼로그를 선택적으로 적용
- 필수 속성을 confirm 기준으로 변경 (dismiss -> confirm)
- 사용하지 않는 문자열 리소스 제거
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

🤖 Android CI Summary

Step Results:

  • Unit Test: ❌ Failure (0s)
  • Debug Build: ⏭️ Skipped (0s)
  • Code Style Check: ✅ Success (3m 57s)

Total Time: 3m 57s

⚠️ Warning: The following steps failed: Unit Test, Debug Build
See the Actions Log for details.

@coderabbitai
Copy link

coderabbitai bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

This PR introduces a centralized event-driven dialog system with standardized error handling. It adds EventHandler with Channel-based event dispatch, creates ErrorScope enum and error utilities, updates MetaSearchDialog and MetaSearchToast component APIs, and integrates error handling across features including MainActivity and the person feature module.

Changes

Cohort / File(s) Summary
Core Event System
core/common/src/main/java/com/example/metasearch/core/common/utils/EventHandler.kt
Introduces EventHandler object with Channel-based eventFlow, MetaSearchEvent sealed interface with ShowDialog variant, MetaSearchDialogSpec data class for dialog configuration, and showErrorDialog utility for mapping exceptions to dialogs with title/message resolution based on ErrorScope and exception type.
Error Handling Utilities
core/common/src/main/java/com/example/metasearch/core/common/utils/Exception.kt, core/common/src/main/java/com/example/metasearch/core/common/constants/ErrorScope.kt
Adds ErrorScope enum (GLOBAL, IMAGE_ANALYSIS), handleException function to route errors to UI or callbacks with network/HTTP error detection, and Throwable.isNetworkError extension predicate.
UI Component Updates
core/designsystem/src/main/java/com/example/metasearch/core/designsystem/component/MetaSearchToast.kt, core/ui/src/main/java/com/example/metasearch/core/ui/component/MetaSearchDialog.kt
MetaSearchToast message becomes optional (String?) with default null and conditional rendering. MetaSearchDialog API changes: onDismissRequest default {}, onConfirmRequest required; dismissButtonText optional (null), confirmButtonText required; dismiss button conditionally rendered, confirm always rendered.
Build Configuration
core/common/build.gradle.kts
Adds Retrofit plugin alias and core.network project dependency.
MainActivity Integration
feature/main/src/main/java/com/example/metasearch/feature/main/MainActivity.kt
Wires EventHandler.eventFlow via LaunchedEffect to observe and render MetaSearchDialog on ShowDialog events; maintains state with remembered dialogSpec and manages dismiss/confirm callbacks.
Feature Dialog Updates
feature/detail/src/main/java/com/example/metasearch/feature/detail/graph/GraphDetailUi.kt, feature/graph/src/main/java/com/example/metasearch/feature/graph/GraphUi.kt, feature/search/src/main/java/com/example/metasearch/feature/search/nls/NLSearchUi.kt, feature/splash/src/main/java/com/example/metasearch/feature/splash/SplashUi.kt
Update MetaSearchDialog call sites: replace onDismissRequest with onConfirmRequest, replace dismissButtonText with confirmButtonText.
Person Feature Error Handling
feature/person/src/main/java/com/example/metasearch/feature/person/PersonPresenter.kt, feature/person/src/main/java/com/example/metasearch/feature/person/PersonUiState.kt, feature/person/src/main/java/com/example/metasearch/feature/person/PersonUi.kt, feature/person/src/main/res/values/strings.xml
Adds toastMessage: String? field to PersonUiState, integrates handleException in delete error flow to populate toastMessage, updates UI to source toast message from state instead of hardcoded string resource, removes person_delete_failed_toast_message resource.

Sequence Diagram

sequenceDiagram
    actor User
    participant Feature as Feature Module
    participant ErrorHandler as Exception Handler
    participant EventHandler as EventHandler
    participant MainActivity as MainActivity
    participant Dialog as MetaSearchDialog
    
    User->>Feature: Trigger error (e.g., delete, load)
    Feature->>ErrorHandler: handleException(exception)
    ErrorHandler->>ErrorHandler: Classify error type<br/>(network/HTTP/unknown)
    ErrorHandler->>EventHandler: showErrorDialog(scope, exception)
    EventHandler->>EventHandler: Build MetaSearchDialogSpec<br/>with title & message
    EventHandler->>EventHandler: Send ShowDialog event
    MainActivity->>EventHandler: Observe eventFlow
    EventHandler->>MainActivity: Emit ShowDialog(spec)
    MainActivity->>MainActivity: Update dialogSpec state
    MainActivity->>Dialog: Render MetaSearchDialog
    User->>Dialog: Click confirm button
    Dialog->>MainActivity: onConfirm callback
    MainActivity->>MainActivity: Clear dialogSpec
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 A grand event to celebrate today,
With dialogs flowing in a Channel's way!
From errors caught to messages so clear,
The UI speaks with ErrorScope's cheer,
One hop forward, more patterns near! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the pull request - centralizing error handling across the codebase through new utilities, events, and dialog flows.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
core/designsystem/src/main/java/com/example/metasearch/core/designsystem/component/MetaSearchToast.kt (1)

23-56: Consider making the Surface rendering conditional on message being non-null.

Currently, when isVisible is true but message is null, the Surface will still render as an empty black rounded rectangle. This could create a confusing visual glitch where users see an empty toast.

Consider wrapping the Surface inside the message?.let block or adding an early return condition.

🎨 Proposed fix to prevent empty toast rendering
 @Composable
 fun MetaSearchToast(
     modifier: Modifier = Modifier,
     message: String? = null,
     isVisible: Boolean,
 ) {
     Box(
         modifier = Modifier
             .fillMaxSize(),
         contentAlignment = Alignment.Center,
     ) {
-        AnimatedVisibility(
-            visible = isVisible,
+        AnimatedVisibility(
+            visible = isVisible && message != null,
             enter = fadeIn() + slideInVertically(initialOffsetY = { it / 2 }),
             exit = fadeOut() + slideOutVertically(targetOffsetY = { it / 2 }),
         ) {
             Surface(
                 color = Color.Black.copy(alpha = 0.6f),
                 shape = RoundedCornerShape(MetaSearchTheme.radius.full),
                 modifier = modifier.padding(horizontal = MetaSearchTheme.spacing.spacing8),
             ) {
-                message?.let {
-                    Text(
-                        text = it,
-                        color = LightPink,
-                        style = MetaSearchTheme.typography.captionSmall,
-                        modifier = Modifier.padding(
-                            horizontal = MetaSearchTheme.spacing.spacing4,
-                            vertical = MetaSearchTheme.spacing.spacing2,
-                        ),
-                    )
-                }
+                Text(
+                    text = message ?: "",
+                    color = LightPink,
+                    style = MetaSearchTheme.typography.captionSmall,
+                    modifier = Modifier.padding(
+                        horizontal = MetaSearchTheme.spacing.spacing4,
+                        vertical = MetaSearchTheme.spacing.spacing2,
+                    ),
+                )
             }
         }
     }
 }
🤖 Fix all issues with AI agents
In
@feature/main/src/main/java/com/example/metasearch/feature/main/MainActivity.kt:
- Around line 65-67: The dialog dismissal handler currently only clears
dialogSpec.value and never triggers the spec's onDismiss callback; update the
onDismissRequest lambda that sets dialogSpec.value = null to also invoke the
MetaSearchDialogSpec's onDismiss (e.g., call the selected spec's onDismiss
function or safe-invoke it) so any cleanup or state updates in the spec run when
the dialog is dismissed.
- Around line 63-76: The dialog is missing its message body because
MetaSearchDialog isn't receiving the MetaSearchDialogSpec.description; update
the MetaSearchDialog call inside the dialogSpec.value?.let { spec -> ... } block
to pass the description as the composable content (e.g., supply a content = {
Text(spec.description) } or the appropriate content parameter on
MetaSearchDialog) and add the import androidx.compose.material3.Text so the
description is rendered.
🧹 Nitpick comments (5)
core/common/build.gradle.kts (1)

4-4: Remove the unnecessary Retrofit plugin from core.common.

The Retrofit plugin is not needed in this module. While core.common imports retrofit2.HttpException for error handling, it doesn't use any Retrofit annotations (@get, @post, etc.) or configure Retrofit instances. The HttpException class is available through the core.network dependency, making the plugin redundant. Remove line 4 to keep the build configuration clean.

feature/person/src/main/java/com/example/metasearch/feature/person/PersonPresenter.kt (1)

109-109: Consider clearing toastMessage when hiding the toast.

This prevents stale message content from potentially being visible if showToast is set to true before a new message is assigned.

♻️ Suggested change
-                PersonUiEvent.HideToast -> showToast = false
+                PersonUiEvent.HideToast -> {
+                    showToast = false
+                    toastMessage = ""
+                }
core/common/src/main/java/com/example/metasearch/core/common/utils/Exception.kt (1)

36-41: Consider simplifying isNetworkError check.

UnknownHostException, ConnectException, and SocketTimeoutException all extend IOException, making the individual checks redundant. However, keeping them explicit documents the specific network errors you're targeting.

♻️ Option A: Simplified (covers all network I/O)
 fun Throwable.isNetworkError(): Boolean {
-    return this is UnknownHostException ||
-        this is ConnectException ||
-        this is SocketTimeoutException ||
-        this is IOException
+    return this is IOException
 }
♻️ Option B: Explicit only (more targeted)
 fun Throwable.isNetworkError(): Boolean {
     return this is UnknownHostException ||
         this is ConnectException ||
-        this is SocketTimeoutException ||
-        this is IOException
+        this is SocketTimeoutException
 }

The current implementation works correctly; this is purely a readability consideration.

core/common/src/main/java/com/example/metasearch/core/common/utils/EventHandler.kt (2)

12-14: trySend result is not checked.

trySend returns a ChannelResult indicating success or failure. If the channel buffer is full, the event will be silently dropped.

♻️ Option A: Log failures for debugging
 fun sendEvent(event: MetaSearchEvent) {
-    _eventFlow.trySend(event)
+    val result = _eventFlow.trySend(event)
+    if (result.isFailure) {
+        // Consider logging: event dropped
+    }
 }
♻️ Option B: Use suspending send (requires coroutine scope)

If event delivery is critical, consider making this a suspend function or providing a coroutine scope to ensure events are never dropped.


8-15: Consider lifecycle awareness for the event flow.

The singleton EventHandler with a buffered channel has some caveats:

  • Events sent when no collector exists are buffered but may be lost if buffer overflows
  • If multiple components collect from eventFlow, each event goes to only one collector (fan-out behavior from receiveAsFlow)

For the current use case (single collector in MainActivity), this works, but document these constraints for future maintainers.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d29a32e and 0cf0771.

📒 Files selected for processing (15)
  • core/common/build.gradle.kts
  • core/common/src/main/java/com/example/metasearch/core/common/constants/ErrorScope.kt
  • core/common/src/main/java/com/example/metasearch/core/common/utils/EventHandler.kt
  • core/common/src/main/java/com/example/metasearch/core/common/utils/Exception.kt
  • core/designsystem/src/main/java/com/example/metasearch/core/designsystem/component/MetaSearchToast.kt
  • core/ui/src/main/java/com/example/metasearch/core/ui/component/MetaSearchDialog.kt
  • feature/detail/src/main/java/com/example/metasearch/feature/detail/graph/GraphDetailUi.kt
  • feature/graph/src/main/java/com/example/metasearch/feature/graph/GraphUi.kt
  • feature/main/src/main/java/com/example/metasearch/feature/main/MainActivity.kt
  • feature/person/src/main/java/com/example/metasearch/feature/person/PersonPresenter.kt
  • feature/person/src/main/java/com/example/metasearch/feature/person/PersonUi.kt
  • feature/person/src/main/java/com/example/metasearch/feature/person/PersonUiState.kt
  • feature/person/src/main/res/values/strings.xml
  • feature/search/src/main/java/com/example/metasearch/feature/search/nls/NLSearchUi.kt
  • feature/splash/src/main/java/com/example/metasearch/feature/splash/SplashUi.kt
💤 Files with no reviewable changes (1)
  • feature/person/src/main/res/values/strings.xml
🧰 Additional context used
🧬 Code graph analysis (5)
core/common/build.gradle.kts (1)
build-logic/src/main/java/com/example/metasearch/convention/Dependencies.kt (1)
  • implementation (7-9)
feature/main/src/main/java/com/example/metasearch/feature/main/MainActivity.kt (1)
core/ui/src/main/java/com/example/metasearch/core/ui/component/MetaSearchDialog.kt (1)
  • MetaSearchDialog (29-110)
feature/person/src/main/java/com/example/metasearch/feature/person/PersonPresenter.kt (1)
core/common/src/main/java/com/example/metasearch/core/common/utils/Exception.kt (1)
  • handleException (10-34)
core/common/src/main/java/com/example/metasearch/core/common/utils/Exception.kt (1)
core/common/src/main/java/com/example/metasearch/core/common/utils/EventHandler.kt (1)
  • showErrorDialog (32-68)
core/ui/src/main/java/com/example/metasearch/core/ui/component/MetaSearchDialog.kt (1)
core/designsystem/src/main/java/com/example/metasearch/core/designsystem/component/MetaSearchButton.kt (1)
  • MetaSearchButton (21-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: ci-build
🔇 Additional comments (14)
core/common/build.gradle.kts (1)

11-13: LGTM! Core.network dependency supports centralized error handling.

Adding the core.network dependency makes sense for implementing centralized error handling that needs to reference network-related error types like HttpException and ErrorScope.

feature/graph/src/main/java/com/example/metasearch/feature/graph/GraphUi.kt (1)

65-72: LGTM!

The dialog API updates are consistent with the centralized error handling approach. The shift from onDismissRequest to onConfirmRequest and dismissButtonText to confirmButtonText provides clearer semantics for the user action.

feature/splash/src/main/java/com/example/metasearch/feature/splash/SplashUi.kt (1)

53-70: LGTM!

The dialog API updates align with the centralized error handling pattern introduced in this PR. The changes are consistent with other features.

feature/search/src/main/java/com/example/metasearch/feature/search/nls/NLSearchUi.kt (1)

111-124: LGTM!

The dialog updates are consistent with the new centralized error handling approach. The API changes improve clarity by distinguishing confirm actions from dismiss actions.

feature/person/src/main/java/com/example/metasearch/feature/person/PersonUi.kt (1)

126-131: LGTM!

The shift from a hard-coded toast message to dynamic state-driven content (state.toastMessage) provides flexibility for displaying different messages. This aligns well with the optional message parameter introduced in MetaSearchToast.

core/common/src/main/java/com/example/metasearch/core/common/constants/ErrorScope.kt (1)

1-6: LGTM!

Clean enum definition for error scoping. The two scopes (GLOBAL and IMAGE_ANALYSIS) provide clear categorization for error dialog routing.

feature/person/src/main/java/com/example/metasearch/feature/person/PersonUiState.kt (1)

13-14: LGTM!

Good addition of the nullable toastMessage property to support dynamic toast content. The pairing with showToast boolean allows for clean separation of visibility control and message content.

feature/detail/src/main/java/com/example/metasearch/feature/detail/graph/GraphDetailUi.kt (1)

53-60: LGTM!

The dialog API update is correctly applied. The OnErrorDialogDismiss event name still makes sense semantically since confirming an error dialog effectively dismisses it.

feature/person/src/main/java/com/example/metasearch/feature/person/PersonPresenter.kt (1)

86-96: LGTM!

Good integration with handleException. The error handling correctly routes 401 errors to the global dialog system while displaying other errors as toasts.

core/common/src/main/java/com/example/metasearch/core/common/utils/Exception.kt (1)

16-17: The onError callback is not invoked for 401 errors.

When a 401 HttpException occurs, showErrorDialog is called but onError is never invoked. Callers using handleException won't receive any callback to update their local state (e.g., dismiss loading indicators, reset UI state). Verify if this is the intended behavior.

core/common/src/main/java/com/example/metasearch/core/common/utils/EventHandler.kt (1)

32-68: LGTM!

The showErrorDialog function provides clean error-to-dialog mapping with appropriate user-friendly messages for different error types. The use of ErrorScope for context-specific messaging is a good pattern.

core/ui/src/main/java/com/example/metasearch/core/ui/component/MetaSearchDialog.kt (3)

32-35: Verify default onDismissRequest behavior for system-initiated dismissals.

Defaulting onDismissRequest to an empty lambda may cause the dialog to appear stuck when dismissed via back press or outside tap (if properties allows these). The Dialog composable will invoke this callback, but with an empty default, nothing happens—no state change to close the dialog.

Ensure that callers either:

  1. Always provide a proper onDismissRequest handler, or
  2. Use DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) when relying on the default.

93-106: LGTM!

Button rendering logic correctly handles the optional dismiss button. Using weight(1f) on both buttons ensures proper space distribution in both single-button and dual-button layouts.


112-127: LGTM!

Preview is correctly updated to use the new required onConfirmRequest parameter, and demonstrates the typical two-button dialog configuration.

Comment on lines +63 to +76
dialogSpec.value?.let { spec ->
MetaSearchDialog(
onDismissRequest = {
dialogSpec.value = null
},
onConfirmRequest = {
spec.onConfirm()
dialogSpec.value = null
},
dismissButtonText = spec.dismissText,
confirmButtonText = spec.confirmText,
title = spec.title,
)
}
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

Missing dialog description/content.

The MetaSearchDialogSpec contains a description field, but it's not being passed to MetaSearchDialog. The dialog will display without any message body.

🐛 Proposed fix to add content
 dialogSpec.value?.let { spec ->
     MetaSearchDialog(
         onDismissRequest = {
             dialogSpec.value = null
         },
         onConfirmRequest = {
             spec.onConfirm()
             dialogSpec.value = null
         },
         dismissButtonText = spec.dismissText,
         confirmButtonText = spec.confirmText,
         title = spec.title,
+        content = { Text(spec.description) },
     )
 }

Note: You'll need to import androidx.compose.material3.Text.

🤖 Prompt for AI Agents
In
@feature/main/src/main/java/com/example/metasearch/feature/main/MainActivity.kt
around lines 63 - 76, The dialog is missing its message body because
MetaSearchDialog isn't receiving the MetaSearchDialogSpec.description; update
the MetaSearchDialog call inside the dialogSpec.value?.let { spec -> ... } block
to pass the description as the composable content (e.g., supply a content = {
Text(spec.description) } or the appropriate content parameter on
MetaSearchDialog) and add the import androidx.compose.material3.Text so the
description is rendered.

Comment on lines +65 to +67
onDismissRequest = {
dialogSpec.value = null
},
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

spec.onDismiss callback is not invoked.

The MetaSearchDialogSpec includes an onDismiss callback, but it's never called when the dialog is dismissed. This could lead to missed cleanup or state updates.

🐛 Proposed fix
         onDismissRequest = {
+            spec.onDismiss()
             dialogSpec.value = null
         },
📝 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
onDismissRequest = {
dialogSpec.value = null
},
onDismissRequest = {
spec.onDismiss()
dialogSpec.value = null
},
🤖 Prompt for AI Agents
In
@feature/main/src/main/java/com/example/metasearch/feature/main/MainActivity.kt
around lines 65 - 67, The dialog dismissal handler currently only clears
dialogSpec.value and never triggers the spec's onDismiss callback; update the
onDismissRequest lambda that sets dialogSpec.value = null to also invoke the
MetaSearchDialogSpec's onDismiss (e.g., call the selected spec's onDismiss
function or safe-invoke it) so any cleanup or state updates in the spec run when
the dialog is dismissed.

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

🤖 Android CI Summary

Step Results:

  • Unit Test: ✅ Success (3m 1s)
  • Debug Build: ✅ Success (1m 6s)
  • Code Style Check: ✅ Success (4m 6s)

Total Time: 8m 13s

🎉 All steps completed successfully!

@komodgn komodgn merged commit 0a45bdc into develop Jan 8, 2026
2 checks passed
@komodgn komodgn deleted the feature/#68 branch January 8, 2026 08:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant