From b29a30b888bf863eb0e92b4889a4de0c7f04417a Mon Sep 17 00:00:00 2001 From: komodgn Date: Thu, 8 Jan 2026 16:45:17 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EC=A0=84=EC=97=AD=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=ED=95=B8=EB=93=A4=EB=A7=81=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#68)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 공통 예외 핸들링 유틸리티 handleException 추가 - 예외 상황에 따라 토스트 또는 다이얼로그를 선택적으로 적용 --- .../core/common/constants/ErrorScope.kt | 6 ++ .../core/common/utils/EventHandler.kt | 68 +++++++++++++++++++ .../metasearch/core/common/utils/Exception.kt | 41 +++++++++++ .../metasearch/feature/main/MainActivity.kt | 31 +++++++++ 4 files changed, 146 insertions(+) create mode 100644 core/common/src/main/java/com/example/metasearch/core/common/constants/ErrorScope.kt create mode 100644 core/common/src/main/java/com/example/metasearch/core/common/utils/EventHandler.kt create mode 100644 core/common/src/main/java/com/example/metasearch/core/common/utils/Exception.kt diff --git a/core/common/src/main/java/com/example/metasearch/core/common/constants/ErrorScope.kt b/core/common/src/main/java/com/example/metasearch/core/common/constants/ErrorScope.kt new file mode 100644 index 00000000..618a57b1 --- /dev/null +++ b/core/common/src/main/java/com/example/metasearch/core/common/constants/ErrorScope.kt @@ -0,0 +1,6 @@ +package com.example.metasearch.core.common.constants + +enum class ErrorScope { + GLOBAL, + IMAGE_ANALYSIS, +} diff --git a/core/common/src/main/java/com/example/metasearch/core/common/utils/EventHandler.kt b/core/common/src/main/java/com/example/metasearch/core/common/utils/EventHandler.kt new file mode 100644 index 00000000..0250d617 --- /dev/null +++ b/core/common/src/main/java/com/example/metasearch/core/common/utils/EventHandler.kt @@ -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(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)) +} diff --git a/core/common/src/main/java/com/example/metasearch/core/common/utils/Exception.kt b/core/common/src/main/java/com/example/metasearch/core/common/utils/Exception.kt new file mode 100644 index 00000000..83cdf108 --- /dev/null +++ b/core/common/src/main/java/com/example/metasearch/core/common/utils/Exception.kt @@ -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 +} diff --git a/feature/main/src/main/java/com/example/metasearch/feature/main/MainActivity.kt b/feature/main/src/main/java/com/example/metasearch/feature/main/MainActivity.kt index 710c5454..1eab0681 100644 --- a/feature/main/src/main/java/com/example/metasearch/feature/main/MainActivity.kt +++ b/feature/main/src/main/java/com/example/metasearch/feature/main/MainActivity.kt @@ -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 @@ -41,9 +47,34 @@ class MainActivity : ComponentActivity() { } MetaSearchTheme { + val dialogSpec = remember { mutableStateOf(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 + }, + onConfirmRequest = { + spec.onConfirm() + dialogSpec.value = null + }, + dismissButtonText = spec.dismissText, + confirmButtonText = spec.confirmText, + title = spec.title, + ) + } + CircuitCompositionLocals(circuit) { NavigableCircuitContent( modifier = Modifier.fillMaxSize(), From ee50b73dbe766c1c16b44f3e5fbb73432e7b5a73 Mon Sep 17 00:00:00 2001 From: komodgn Date: Thu, 8 Jan 2026 16:52:44 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=EA=B0=80=EB=8F=85=EC=84=B1?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EA=B3=B5=ED=86=B5=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=86=8D=EC=84=B1=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 필수 속성을 confirm 기준으로 변경 (dismiss -> confirm) --- .../core/ui/component/MetaSearchDialog.kt | 28 +++++++++---------- .../feature/detail/graph/GraphDetailUi.kt | 4 +-- .../metasearch/feature/graph/GraphUi.kt | 4 +-- .../feature/search/nls/NLSearchUi.kt | 4 +-- .../metasearch/feature/splash/SplashUi.kt | 4 +-- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/core/ui/src/main/java/com/example/metasearch/core/ui/component/MetaSearchDialog.kt b/core/ui/src/main/java/com/example/metasearch/core/ui/component/MetaSearchDialog.kt index cfb1e0b0..7783613c 100644 --- a/core/ui/src/main/java/com/example/metasearch/core/ui/component/MetaSearchDialog.kt +++ b/core/ui/src/main/java/com/example/metasearch/core/ui/component/MetaSearchDialog.kt @@ -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(), @@ -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, + ) } } } @@ -120,7 +120,7 @@ private fun MetaSearchDialogPreview() { text = "앱을 이용하려면 권한 설정이 필요합니다.", ) }, - onDismissRequest = {}, + onConfirmRequest = {}, dismissButtonText = "닫기", confirmButtonText = "설정으로 이동", ) diff --git a/feature/detail/src/main/java/com/example/metasearch/feature/detail/graph/GraphDetailUi.kt b/feature/detail/src/main/java/com/example/metasearch/feature/detail/graph/GraphDetailUi.kt index 8650f13e..0601766b 100644 --- a/feature/detail/src/main/java/com/example/metasearch/feature/detail/graph/GraphDetailUi.kt +++ b/feature/detail/src/main/java/com/example/metasearch/feature/detail/graph/GraphDetailUi.kt @@ -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), ) } } diff --git a/feature/graph/src/main/java/com/example/metasearch/feature/graph/GraphUi.kt b/feature/graph/src/main/java/com/example/metasearch/feature/graph/GraphUi.kt index 9619a48c..f8a7c7cd 100644 --- a/feature/graph/src/main/java/com/example/metasearch/feature/graph/GraphUi.kt +++ b/feature/graph/src/main/java/com/example/metasearch/feature/graph/GraphUi.kt @@ -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), ) } } diff --git a/feature/search/src/main/java/com/example/metasearch/feature/search/nls/NLSearchUi.kt b/feature/search/src/main/java/com/example/metasearch/feature/search/nls/NLSearchUi.kt index 9c954b82..dedf2bfb 100644 --- a/feature/search/src/main/java/com/example/metasearch/feature/search/nls/NLSearchUi.kt +++ b/feature/search/src/main/java/com/example/metasearch/feature/search/nls/NLSearchUi.kt @@ -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), ) } } diff --git a/feature/splash/src/main/java/com/example/metasearch/feature/splash/SplashUi.kt b/feature/splash/src/main/java/com/example/metasearch/feature/splash/SplashUi.kt index b26d3814..695d4e6c 100644 --- a/feature/splash/src/main/java/com/example/metasearch/feature/splash/SplashUi.kt +++ b/feature/splash/src/main/java/com/example/metasearch/feature/splash/SplashUi.kt @@ -62,10 +62,10 @@ fun SplashUi( ), ) }, - onDismissRequest = { + onConfirmRequest = { state.eventSink(SplashUiEvent.OnConfirmSettings) }, - dismissButtonText = stringResource(R.string.confirm_settings), + confirmButtonText = stringResource(R.string.confirm_settings), ) } } From 14323f07bc8606a8c317ad676b64daa27d2d12da Mon Sep 17 00:00:00 2001 From: komodgn Date: Thu, 8 Jan 2026 17:04:02 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EC=9D=B8=EB=AC=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=EC=97=90=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=A7=81=20=EC=A0=81=EC=9A=A9=20(#68)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용하지 않는 문자열 리소스 제거 --- .../designsystem/component/MetaSearchToast.kt | 22 ++++++++++--------- .../feature/person/PersonPresenter.kt | 13 +++++++++-- .../metasearch/feature/person/PersonUi.kt | 2 +- .../feature/person/PersonUiState.kt | 1 + .../person/src/main/res/values/strings.xml | 1 - 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/core/designsystem/src/main/java/com/example/metasearch/core/designsystem/component/MetaSearchToast.kt b/core/designsystem/src/main/java/com/example/metasearch/core/designsystem/component/MetaSearchToast.kt index 231f0c7e..8276dd49 100644 --- a/core/designsystem/src/main/java/com/example/metasearch/core/designsystem/component/MetaSearchToast.kt +++ b/core/designsystem/src/main/java/com/example/metasearch/core/designsystem/component/MetaSearchToast.kt @@ -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( @@ -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, + ), + ) + } } } } diff --git a/feature/person/src/main/java/com/example/metasearch/feature/person/PersonPresenter.kt b/feature/person/src/main/java/com/example/metasearch/feature/person/PersonPresenter.kt index fea851c4..c2b16ed2 100644 --- a/feature/person/src/main/java/com/example/metasearch/feature/person/PersonPresenter.kt +++ b/feature/person/src/main/java/com/example/metasearch/feature/person/PersonPresenter.kt @@ -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 @@ -38,6 +39,7 @@ class PersonPresenter @AssistedInject constructor( var deleteJob by remember { mutableStateOf(null) } var showToast by remember { mutableStateOf(false) } + var toastMessage by remember { mutableStateOf("") } var showDeleteDialog by remember { mutableStateOf(false) } var pendingDeletePersonId by remember { mutableStateOf(null) } var pendingDeletePersonName by remember { mutableStateOf("") } @@ -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 } @@ -103,6 +111,7 @@ class PersonPresenter @AssistedInject constructor( } return PersonUiState( + toastMessage = toastMessage, showToast = showToast, showDeleteDialog = showDeleteDialog, pendingDeletePersonName = pendingDeletePersonName, diff --git a/feature/person/src/main/java/com/example/metasearch/feature/person/PersonUi.kt b/feature/person/src/main/java/com/example/metasearch/feature/person/PersonUi.kt index 4364d3eb..bb35c5b7 100644 --- a/feature/person/src/main/java/com/example/metasearch/feature/person/PersonUi.kt +++ b/feature/person/src/main/java/com/example/metasearch/feature/person/PersonUi.kt @@ -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, ) } } diff --git a/feature/person/src/main/java/com/example/metasearch/feature/person/PersonUiState.kt b/feature/person/src/main/java/com/example/metasearch/feature/person/PersonUiState.kt index 959cbcd0..f77adbca 100644 --- a/feature/person/src/main/java/com/example/metasearch/feature/person/PersonUiState.kt +++ b/feature/person/src/main/java/com/example/metasearch/feature/person/PersonUiState.kt @@ -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 = emptyList(), val eventSink: (PersonUiEvent) -> Unit, ) : CircuitUiState diff --git a/feature/person/src/main/res/values/strings.xml b/feature/person/src/main/res/values/strings.xml index 64b72854..22f320dd 100644 --- a/feature/person/src/main/res/values/strings.xml +++ b/feature/person/src/main/res/values/strings.xml @@ -6,5 +6,4 @@ \'%s\'님을 인물 목록에서 삭제할까요? 삭제 취소 - 삭제 실패 🚨 네트워크를 확인하거나 잠시후 다시 시도해주세요. From 0cf077171a3ce22f2e4f556a49bb7c777a2d8a7c Mon Sep 17 00:00:00 2001 From: komodgn Date: Thu, 8 Jan 2026 17:19:22 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chore(common):=20Retrofit=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80=20(#68)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/common/build.gradle.kts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 27f9a0a0..af93dc06 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -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) +}