Skip to content

Commit ef0c300

Browse files
committed
[BOOK-491] feat: 텍스트 인식 실패 케이스 처리
1 parent f304d2a commit ef0c300

5 files changed

Lines changed: 96 additions & 21 deletions

File tree

feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.ninecraft.booket.feature.record.ocr
22

33
import android.net.Uri
44
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.LaunchedEffect
56
import androidx.compose.runtime.getValue
67
import androidx.compose.runtime.mutableStateOf
78
import androidx.compose.runtime.rememberCoroutineScope
@@ -27,6 +28,7 @@ import kotlinx.collections.immutable.persistentListOf
2728
import kotlinx.collections.immutable.persistentSetOf
2829
import kotlinx.collections.immutable.toPersistentList
2930
import kotlinx.collections.immutable.toPersistentSet
31+
import kotlinx.coroutines.delay
3032
import kotlinx.coroutines.launch
3133

3234
@AssistedInject
@@ -44,6 +46,7 @@ class OcrPresenter(
4446

4547
companion object {
4648
private const val RECORD_OCR_SENTENCE = "record_OCR_sentence"
49+
private const val CAMERA_MAX_FAILURES = 2
4750
}
4851

4952
@Composable
@@ -56,11 +59,53 @@ class OcrPresenter(
5659
var selectedIndices by rememberRetained { mutableStateOf(persistentSetOf<Int>()) }
5760
var mergedSentence by rememberRetained { mutableStateOf("") }
5861
var isTextDetectionFailed by rememberRetained { mutableStateOf(false) }
62+
var isCameraRecognitionFailedDialogVisible by rememberRetained { mutableStateOf(false) }
63+
var isGalleryRecognitionFailedDialogVisible by rememberRetained { mutableStateOf(false) }
5964
var isRecaptureDialogVisible by rememberRetained { mutableStateOf(false) }
6065
var isLoading by rememberRetained { mutableStateOf(false) }
6166
var sideEffect by rememberRetained { mutableStateOf<OcrSideEffect?>(null) }
6267

63-
fun recognizeText(imageUri: Uri) {
68+
var cameraFailureCount by rememberRetained { mutableStateOf(0) }
69+
70+
LaunchedEffect(isTextDetectionFailed) {
71+
if (isTextDetectionFailed) {
72+
delay(2000)
73+
isTextDetectionFailed = false
74+
}
75+
}
76+
77+
fun handleRecognitionSuccess(text: String) {
78+
isTextDetectionFailed = false
79+
cameraFailureCount = 0
80+
81+
val sentences = text
82+
.split("\n")
83+
.map { it.trim() }
84+
.filter { it.isNotEmpty() }
85+
86+
sentenceList = sentences.toPersistentList()
87+
currentUi = OcrUi.RESULT
88+
analyticsHelper.logScreenView(RECORD_OCR_SENTENCE)
89+
}
90+
91+
fun handleRecognitionFailure(source: RecognizeSource) {
92+
when (source) {
93+
RecognizeSource.CAMERA -> {
94+
isTextDetectionFailed = true
95+
cameraFailureCount += 1
96+
97+
if (cameraFailureCount >= CAMERA_MAX_FAILURES) {
98+
isCameraRecognitionFailedDialogVisible = true
99+
}
100+
}
101+
102+
RecognizeSource.GALLERY -> {
103+
isGalleryRecognitionFailedDialogVisible = true
104+
}
105+
}
106+
}
107+
108+
fun recognizeText(imageUri: Uri, source: RecognizeSource) {
64109
scope.launch {
65110
try {
66111
isLoading = true
@@ -69,21 +114,13 @@ class OcrPresenter(
69114
val text = it.responses.firstOrNull()?.fullTextAnnotation?.text.orEmpty()
70115

71116
if (text.isNotBlank()) {
72-
isTextDetectionFailed = false
73-
val sentences = text
74-
.split("\n")
75-
.map { it.trim() }
76-
.filter { it.isNotEmpty() }
77-
78-
sentenceList = sentences.toPersistentList()
79-
currentUi = OcrUi.RESULT
80-
analyticsHelper.logScreenView(RECORD_OCR_SENTENCE)
117+
handleRecognitionSuccess(text)
81118
} else {
82-
isTextDetectionFailed = true
119+
handleRecognitionFailure(source)
83120
}
84121
}
85122
.onFailure { exception ->
86-
isTextDetectionFailed = true
123+
handleRecognitionFailure(source)
87124

88125
val handleErrorMessage = { message: String ->
89126
Logger.e("Cloud Vision API Error: ${exception.message}")
@@ -128,16 +165,20 @@ class OcrPresenter(
128165

129166
is OcrUiEvent.OnImageCaptured -> {
130167
isTextDetectionFailed = false
168+
isCameraRecognitionFailedDialogVisible = false
169+
isGalleryRecognitionFailedDialogVisible = false
131170

132-
recognizeText(event.imageUri)
171+
recognizeText(event.imageUri, RecognizeSource.CAMERA)
133172
}
134173

135174
is OcrUiEvent.OnImageSelected -> {
136175
currentUi = OcrUi.IMAGE
137176
selectedImage = event.imageUri
177+
isTextDetectionFailed = false
178+
isGalleryRecognitionFailedDialogVisible = false
138179

139180
val pareUri = selectedImage.toUri()
140-
recognizeText(pareUri)
181+
recognizeText(pareUri, RecognizeSource.GALLERY)
141182
}
142183

143184
is OcrUiEvent.OnReCaptureButtonClick -> {
@@ -168,9 +209,17 @@ class OcrPresenter(
168209
isRecaptureDialogVisible = false
169210
}
170211

171-
OcrUiEvent.OnImageViewClosed -> {
212+
OcrUiEvent.OnImageContentClosed -> {
172213
currentUi = OcrUi.CAMERA
173214
}
215+
216+
OcrUiEvent.OnCameraRecognitionFailedDialogDismissed -> {
217+
isCameraRecognitionFailedDialogVisible = false
218+
}
219+
220+
OcrUiEvent.OnImageRecognitionFailedDialogDismissed -> {
221+
isGalleryRecognitionFailedDialogVisible = false
222+
}
174223
}
175224
}
176225

@@ -185,6 +234,8 @@ class OcrPresenter(
185234
sentenceList = sentenceList,
186235
selectedIndices = selectedIndices,
187236
isTextDetectionFailed = isTextDetectionFailed,
237+
isCameraRecognitionFailedDialogVisible = isCameraRecognitionFailedDialogVisible,
238+
isGalleryRecognitionFailedDialogVisible = isGalleryRecognitionFailedDialogVisible,
188239
isRecaptureDialogVisible = isRecaptureDialogVisible,
189240
isLoading = isLoading,
190241
sideEffect = sideEffect,

feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUiState.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ sealed interface OcrSideEffect {
3535

3636
sealed interface OcrUiEvent : CircuitUiEvent {
3737
data object OnCloseClick : OcrUiEvent
38-
data object OnImageViewClosed : OcrUiEvent
38+
data object OnImageContentClosed : OcrUiEvent
3939
data object OnShowPermissionDialog : OcrUiEvent
4040
data object OnHidePermissionDialog : OcrUiEvent
4141
data object OnCaptureStart : OcrUiEvent
@@ -46,6 +46,8 @@ sealed interface OcrUiEvent : CircuitUiEvent {
4646
data object OnSelectionConfirmed : OcrUiEvent
4747
data object OnRecaptureDialogConfirmed : OcrUiEvent
4848
data object OnRecaptureDialogDismissed : OcrUiEvent
49+
data object OnCameraRecognitionFailedDialogDismissed : OcrUiEvent
50+
data object OnImageRecognitionFailedDialogDismissed : OcrUiEvent
4951
data class OnSentenceSelected(val index: Int) : OcrUiEvent
5052
}
5153

@@ -54,3 +56,8 @@ enum class OcrUi {
5456
IMAGE,
5557
RESULT,
5658
}
59+
60+
enum class RecognizeSource {
61+
CAMERA,
62+
GALLERY,
63+
}

feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/content/OcrCameraContent.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,9 @@ internal fun OcrCameraContent(
301301
state.eventSink(OcrUiEvent.OnCloseClick)
302302
},
303303
dismissButtonText = stringResource(R.string.ocr_recognition_failed_dialog_camera),
304-
onDismissRequest = {},
304+
onDismissRequest = {
305+
state.eventSink(OcrUiEvent.OnCameraRecognitionFailedDialogDismissed)
306+
},
305307
)
306308
}
307309
}

feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/content/OcrImageContent.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.ninecraft.booket.feature.record.ocr.content
22

3+
import androidx.activity.compose.rememberLauncherForActivityResult
4+
import androidx.activity.result.PickVisualMediaRequest
5+
import androidx.activity.result.contract.ActivityResultContracts
36
import androidx.compose.foundation.background
47
import androidx.compose.foundation.layout.Box
58
import androidx.compose.foundation.layout.Column
@@ -33,6 +36,15 @@ internal fun OcrImageContent(
3336
state: OcrUiState,
3437
modifier: Modifier = Modifier,
3538
) {
39+
val photoPickerLauncher = rememberLauncherForActivityResult(
40+
contract = ActivityResultContracts.PickVisualMedia(),
41+
onResult = { uri ->
42+
if (uri != null) {
43+
state.eventSink(OcrUiEvent.OnImageSelected(uri.toString()))
44+
}
45+
},
46+
)
47+
3648
ReedScaffold(
3749
modifier = modifier.fillMaxSize(),
3850
containerColor = Neutral950,
@@ -47,7 +59,7 @@ internal fun OcrImageContent(
4759
.background(color = Color.Black),
4860
isDark = true,
4961
onClose = {
50-
state.eventSink(OcrUiEvent.OnImageViewClosed)
62+
state.eventSink(OcrUiEvent.OnImageContentClosed)
5163
},
5264
)
5365
Box(
@@ -89,8 +101,11 @@ internal fun OcrImageContent(
89101
},
90102
dismissButtonText = stringResource(R.string.ocr_recognition_failed_dialog_image),
91103
onDismissRequest = {
92-
// 갤러리 열기
93-
}
104+
state.eventSink(OcrUiEvent.OnImageRecognitionFailedDialogDismissed)
105+
photoPickerLauncher.launch(
106+
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly),
107+
)
108+
},
94109
)
95110
}
96111
}

feature/record/src/main/res/values/strings.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
<string name="emotion_edit_dialog_description">기록된 감정이 삭제됩니다.</string>
5454
<string name="ocr_recognition_failed_dialog_title">문장을 인식하지 못했어요</string>
5555
<string name="ocr_recognition_failed_dialog_description">직접 문장을 입력하시겠어요?</string>
56-
<string name="ocr_recognition_failed_dialog_direct_input">직접 촬영하기</string>
56+
<string name="ocr_recognition_failed_dialog_direct_input">직접 입력하기</string>
5757
<string name="ocr_recognition_failed_dialog_camera">다시 촬영하기</string>
5858
<string name="ocr_recognition_failed_dialog_image">이미지 선택하기</string>
5959
</resources>

0 commit comments

Comments
 (0)