Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions core/common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
plugins {
alias(libs.plugins.metasearch.android.library)
alias(libs.plugins.metasearch.android.library.compose)
alias(libs.plugins.metasearch.android.retrofit)
}

android {
namespace = "com.example.metasearch.core.common"
}

dependencies {
implementation(projects.core.network)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.metasearch.core.common.constants

enum class ErrorScope {
GLOBAL,
IMAGE_ANALYSIS,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.example.metasearch.core.common.utils

import com.example.metasearch.core.common.constants.ErrorScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import retrofit2.HttpException

object EventHandler {
private val _eventFlow = Channel<MetaSearchEvent>(Channel.BUFFERED)
val eventFlow = _eventFlow.receiveAsFlow()

fun sendEvent(event: MetaSearchEvent) {
_eventFlow.trySend(event)
}
}

sealed interface MetaSearchEvent {
data class ShowDialog(
val dialogSpec: MetaSearchDialogSpec,
) : MetaSearchEvent
}

data class MetaSearchDialogSpec(
val title: String? = null,
val description: String,
val confirmText: String,
val dismissText: String? = null,
val onConfirm: () -> Unit,
val onDismiss: () -> Unit = {},
)

fun showErrorDialog(
errorScope: ErrorScope,
exception: Throwable,
confirmText: String = "확인",
onConfirm: () -> Unit = {},
) {
val (title, message) = when {
exception.isNetworkError() -> {
null to "네트워크 연결이 불안정합니다.\n인터넷 연결을 확인해주세요."
}

exception is HttpException -> {
when (errorScope) {
ErrorScope.GLOBAL -> {
null to "알 수 없는 문제가 발생했습니다.\n잠시 후 다시 시도해주세요."
}

ErrorScope.IMAGE_ANALYSIS -> {
null to "이미지 분석 완료 후 다시 시도해주세요."
}
}
}

else -> {
null to "알 수 없는 문제가 발생했습니다.\n잠시 후 다시 시도해주세요."
}
}

val spec = MetaSearchDialogSpec(
title = title,
description = message,
confirmText = confirmText,
onConfirm = onConfirm,
)

EventHandler.sendEvent(event = MetaSearchEvent.ShowDialog(spec))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.example.metasearch.core.common.utils

import com.example.metasearch.core.common.constants.ErrorScope
import retrofit2.HttpException
import java.io.IOException
import java.net.ConnectException
import java.net.SocketTimeoutException
import java.net.UnknownHostException

fun handleException(
exception: Throwable,
onError: (String) -> Unit,
) {
when {
exception is HttpException -> {
when (exception.code()) {
401 -> showErrorDialog(ErrorScope.IMAGE_ANALYSIS, exception)
else -> {
val message = "서버 오류가 발생했습니다. (${exception.code()})"
onError(message)
}
}
}

exception.isNetworkError() -> {
onError("네트워크 연결이 불안정합니다. 잠시 후 다시 시도해주세요.")
}

else -> {
val message = exception.message ?: "문제가 발생했습니다. 잠시 후 다시 시도해주세요."
onError(message)
}
}
}

fun Throwable.isNetworkError(): Boolean {
return this is UnknownHostException ||
this is ConnectException ||
this is SocketTimeoutException ||
this is IOException
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import com.example.metasearch.core.designsystem.theme.MetaSearchTheme
@Composable
fun MetaSearchToast(
modifier: Modifier = Modifier,
message: String,
message: String? = null,
isVisible: Boolean,
) {
Box(
Expand All @@ -40,15 +40,17 @@ fun MetaSearchToast(
shape = RoundedCornerShape(MetaSearchTheme.radius.full),
modifier = modifier.padding(horizontal = MetaSearchTheme.spacing.spacing8),
) {
Text(
text = message,
color = LightPink,
style = MetaSearchTheme.typography.captionSmall,
modifier = Modifier.padding(
horizontal = MetaSearchTheme.spacing.spacing4,
vertical = MetaSearchTheme.spacing.spacing2,
),
)
message?.let {
Text(
text = it,
color = LightPink,
style = MetaSearchTheme.typography.captionSmall,
modifier = Modifier.padding(
horizontal = MetaSearchTheme.spacing.spacing4,
vertical = MetaSearchTheme.spacing.spacing2,
),
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ import com.example.metasearch.core.designsystem.theme.White
@Composable
fun MetaSearchDialog(
modifier: Modifier = Modifier,
onDismissRequest: () -> Unit,
onConfirmRequest: () -> Unit = {},
dismissButtonText: String,
confirmButtonText: String? = null,
onDismissRequest: () -> Unit = {},
onConfirmRequest: () -> Unit,
dismissButtonText: String? = null,
confirmButtonText: String,
title: String? = null,
content: @Composable (() -> Unit)? = null,
properties: DialogProperties = DialogProperties(),
Expand Down Expand Up @@ -90,20 +90,20 @@ fun MetaSearchDialog(
MetaSearchTheme.spacing.spacing3,
),
) {
MetaSearchButton(
modifier = Modifier.weight(1f),
text = dismissButtonText,
onClick = onDismissRequest,
contentColor = Neutral500,
)

confirmButtonText?.let {
dismissButtonText?.let {
MetaSearchButton(
modifier = Modifier.weight(1f),
text = it,
onClick = onConfirmRequest,
onClick = onDismissRequest,
contentColor = Neutral500,
)
}

MetaSearchButton(
modifier = Modifier.weight(1f),
text = confirmButtonText,
onClick = onConfirmRequest,
)
}
}
}
Expand All @@ -120,7 +120,7 @@ private fun MetaSearchDialogPreview() {
text = "앱을 이용하려면 권한 설정이 필요합니다.",
)
},
onDismissRequest = {},
onConfirmRequest = {},
dismissButtonText = "닫기",
confirmButtonText = "설정으로 이동",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ fun GraphDetailUi(
if (state.errorMessage.isNotBlank()) {
MetaSearchDialog(
title = stringResource(R.string.graph_detail_screen_dialog_title),
onDismissRequest = { state.eventSink(GraphDetailUiEvent.OnErrorDialogDismiss) },
onConfirmRequest = { state.eventSink(GraphDetailUiEvent.OnErrorDialogDismiss) },
content = { Text(state.errorMessage) },
dismissButtonText = stringResource(R.string.graph_detail_screen_dialog_close_button),
confirmButtonText = stringResource(R.string.graph_detail_screen_dialog_close_button),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ fun GraphUi(
if (state.errorMessage.isNotBlank()) {
MetaSearchDialog(
title = stringResource(R.string.graph_screen_dialog_title),
onDismissRequest = { state.eventSink(GraphUiEvent.OnErrorDialogDismiss) },
onConfirmRequest = { state.eventSink(GraphUiEvent.OnErrorDialogDismiss) },
content = { Text(state.errorMessage) },
dismissButtonText = stringResource(R.string.graph_screen_dialog_close_button),
confirmButtonText = stringResource(R.string.graph_screen_dialog_close_button),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.example.metasearch.core.common.utils.EventHandler
import com.example.metasearch.core.common.utils.MetaSearchDialogSpec
import com.example.metasearch.core.common.utils.MetaSearchEvent
import com.example.metasearch.core.designsystem.theme.MetaSearchTheme
import com.example.metasearch.core.ui.component.MetaSearchDialog
import com.example.metasearch.feature.screens.SplashScreen
import com.slack.circuit.backstack.rememberSaveableBackStack
import com.slack.circuit.foundation.Circuit
Expand Down Expand Up @@ -41,9 +47,34 @@ class MainActivity : ComponentActivity() {
}

MetaSearchTheme {
val dialogSpec = remember { mutableStateOf<MetaSearchDialogSpec?>(null) }

val backStack = rememberSaveableBackStack(SplashScreen)
val navigator = rememberCircuitNavigator(backStack)

LaunchedEffect(Unit) {
EventHandler.eventFlow.collect { event ->
when (event) {
is MetaSearchEvent.ShowDialog -> dialogSpec.value = event.dialogSpec
}
}
}

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

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


CircuitCompositionLocals(circuit) {
NavigableCircuitContent(
modifier = Modifier.fillMaxSize(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import com.example.metasearch.core.common.utils.handleException
import com.example.metasearch.core.data.api.repository.PersonRepository
import com.example.metasearch.feature.screens.PersonDetailScreen
import com.example.metasearch.feature.screens.PersonScreen
Expand Down Expand Up @@ -38,6 +39,7 @@ class PersonPresenter @AssistedInject constructor(
var deleteJob by remember { mutableStateOf<Job?>(null) }

var showToast by remember { mutableStateOf(false) }
var toastMessage by remember { mutableStateOf("") }
var showDeleteDialog by remember { mutableStateOf(false) }
var pendingDeletePersonId by remember { mutableStateOf<Long?>(null) }
var pendingDeletePersonName by remember { mutableStateOf("") }
Expand Down Expand Up @@ -81,8 +83,14 @@ class PersonPresenter @AssistedInject constructor(
showDeleteDialog = false
pendingDeletePersonId = null
}
.onFailure {
showToast = true
.onFailure { exception ->
handleException(
exception = exception,
onError = { message ->
toastMessage = message
showToast = true
},
)
showDeleteDialog = false
pendingDeletePersonId = null
}
Expand All @@ -103,6 +111,7 @@ class PersonPresenter @AssistedInject constructor(
}

return PersonUiState(
toastMessage = toastMessage,
showToast = showToast,
showDeleteDialog = showDeleteDialog,
pendingDeletePersonName = pendingDeletePersonName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ private fun PersonUiContent(
MetaSearchToast(
modifier = Modifier.align(Alignment.Center),
isVisible = state.showToast,
message = stringResource(R.string.person_delete_failed_toast_message),
message = state.toastMessage,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ data class PersonUiState(
val showDeleteDialog: Boolean = false,
val pendingDeletePersonName: String = "",
val showToast: Boolean = false,
val toastMessage: String? = null,
val people: List<PersonModel> = emptyList(),
val eventSink: (PersonUiEvent) -> Unit,
) : CircuitUiState
Expand Down
1 change: 0 additions & 1 deletion feature/person/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,4 @@
<string name="person_delete_dialog_content">\'%s\'님을 인물 목록에서 삭제할까요?</string>
<string name="person_delete_dialog_confirm_button">삭제</string>
<string name="person_delete_dialog_cancel_button">취소</string>
<string name="person_delete_failed_toast_message">삭제 실패 🚨 네트워크를 확인하거나 잠시후 다시 시도해주세요.</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,10 @@ private fun NLSearchUiContent(
text = state.errorMessage,
)
},
onDismissRequest = {
onConfirmRequest = {
state.eventSink(NLSearchUiEvent.OnDialogCloseButtonClick)
},
dismissButtonText = stringResource(R.string.nl_search_screen_dialog_close_button),
confirmButtonText = stringResource(R.string.nl_search_screen_dialog_close_button),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ fun SplashUi(
),
)
},
onDismissRequest = {
onConfirmRequest = {
state.eventSink(SplashUiEvent.OnConfirmSettings)
},
dismissButtonText = stringResource(R.string.confirm_settings),
confirmButtonText = stringResource(R.string.confirm_settings),
)
}
}
Expand Down