From 45b866b6a30f1c6c9a61357c0b0d4eeb0bd03890 Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 22 Aug 2025 17:38:08 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[BOOK-291]=20fix:=20initialLoad=EC=97=90?= =?UTF-8?q?=EC=84=9C=20CancellationException=EC=9D=BC=20=EA=B2=BD=EC=9A=B0?= =?UTF-8?q?=20throw?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../booket/feature/detail/book/BookDetailPresenter.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt index 1525a760..4d00094f 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt @@ -38,6 +38,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch @@ -120,6 +121,8 @@ class BookDetailPresenter @AssistedInject constructor( uiState = UiState.Success } + } catch (ce: CancellationException) { + throw ce } catch (e: Throwable) { uiState = UiState.Error(e) From 7c6044d027c7810453b952818afda024828985af Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 22 Aug 2025 18:04:49 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[BOOK-291]=20fix:=20preventMultiTouch=20Mod?= =?UTF-8?q?ifier=20=ED=99=95=EC=9E=A5=ED=95=A8=EC=88=98=20=EB=B6=80?= =?UTF-8?q?=EB=AA=A8=20=EC=BB=B4=ED=8F=AC=EC=A0=80=EB=B8=94=EC=97=90=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../booket/core/common/extensions/Modifier.kt | 22 +++++++++++++++++++ .../ninecraft/booket/core/ui/ReedScaffold.kt | 5 ++++- .../record/register/RecordRegisterUi.kt | 5 ++++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Modifier.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Modifier.kt index 019befb7..d8b076d4 100644 --- a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Modifier.kt +++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Modifier.kt @@ -1,6 +1,8 @@ package com.ninecraft.booket.core.common.extensions import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.material3.ripple import androidx.compose.runtime.remember @@ -9,6 +11,8 @@ import androidx.compose.ui.composed import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.semantics.Role import com.ninecraft.booket.core.common.utils.MultipleEventsCutter @@ -54,3 +58,21 @@ fun Modifier.captureToGraphicsLayer(graphicsLayer: GraphicsLayer) = graphicsLayer.record { this@drawWithContent.drawContent() } drawLayer(graphicsLayer) } + +/** + * 부모 영역에서 동시 터치(두 손가락 이상)를 차단하는 Modifier + */ +fun Modifier.preventMultiTouch() = pointerInput(Unit) { + // awaitEachGesture: 한 번의 제스쳐 세션을 추상화 + awaitEachGesture { + val first = awaitFirstDown(requireUnconsumed = false) + do { + // 이벤트 전파 초기 단계(PointerEventPass.Initial)에서 하위 컴포저블로 이벤트가 내려가기 전에 가로채 소비한다 + val event = awaitPointerEvent(pass = PointerEventPass.Initial) + event.changes.forEach { change -> + if (change.id != first.id && change.pressed) change.consume() + } + // 루프 조건: 첫 포인터가 pressed 상태일 동안만 유지한다 (up이거나 cancel되면 pressed=false로 루프 종료) + } while (event.changes.any { it.id == first.id && it.pressed }) + } +} diff --git a/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/ReedScaffold.kt b/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/ReedScaffold.kt index be8a59c2..92270ebb 100644 --- a/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/ReedScaffold.kt +++ b/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/ReedScaffold.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.ScaffoldDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import com.ninecraft.booket.core.common.extensions.preventMultiTouch import com.ninecraft.booket.core.designsystem.theme.White import tech.thdev.compose.extensions.keyboard.state.foundation.keyboardHide @@ -22,7 +23,9 @@ fun ReedScaffold( content: @Composable (PaddingValues) -> Unit, ) { Scaffold( - modifier = modifier.keyboardHide(), + modifier = modifier + .keyboardHide() + .preventMultiTouch(), topBar = topBar, bottomBar = bottomBar, snackbarHost = snackbarHost, diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt index 4f676ad6..3ae920d7 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.ninecraft.booket.core.common.extensions.preventMultiTouch import com.ninecraft.booket.core.designsystem.DevicePreview import com.ninecraft.booket.core.designsystem.RecordStep import com.ninecraft.booket.core.designsystem.component.RecordProgressBar @@ -46,7 +47,9 @@ internal fun RecordRegisterUi( } Scaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .preventMultiTouch(), containerColor = White, contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.ime), ) { innerPadding -> From 644a8c86219128cd5fcc142c62b2e9847c74f7d2 Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 22 Aug 2025 18:21:29 +0900 Subject: [PATCH 3/4] [BOOK-291] chore: app version update 1.1.1 --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 12530d9b..484f649a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -79,8 +79,8 @@ firebase-crashlytics = "3.0.4" minSdk = "28" targetSdk = "35" compileSdk = "35" -versionName = "1.1.0" -versionCode = "4" +versionName = "1.1.1" +versionCode = "5" packageName = "com.ninecraft.booket" [libraries] From 6f3f8a3f5d6cee07faaa39dc387c2cfbf5110fce Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 22 Aug 2025 18:31:37 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[BOOK-291]=20refactor:=20=ED=86=A0=EB=81=BC?= =?UTF-8?q?=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../booket/core/common/extensions/Modifier.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Modifier.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Modifier.kt index d8b076d4..4c5695cb 100644 --- a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Modifier.kt +++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Modifier.kt @@ -66,13 +66,17 @@ fun Modifier.preventMultiTouch() = pointerInput(Unit) { // awaitEachGesture: 한 번의 제스쳐 세션을 추상화 awaitEachGesture { val first = awaitFirstDown(requireUnconsumed = false) - do { + + while (true) { // 이벤트 전파 초기 단계(PointerEventPass.Initial)에서 하위 컴포저블로 이벤트가 내려가기 전에 가로채 소비한다 val event = awaitPointerEvent(pass = PointerEventPass.Initial) event.changes.forEach { change -> - if (change.id != first.id && change.pressed) change.consume() + if (change.id != first.id && change.pressed) { + change.consume() + } } - // 루프 조건: 첫 포인터가 pressed 상태일 동안만 유지한다 (up이거나 cancel되면 pressed=false로 루프 종료) - } while (event.changes.any { it.id == first.id && it.pressed }) + // 첫 포인터가 pressed 상태일 동안만 유지한다 (up이거나 cancel되면 pressed=false로 루프 종료) + if (event.changes.none { it.id == first.id && it.pressed }) break + } } }