-
Notifications
You must be signed in to change notification settings - Fork 0
feat: OCR 인식에 갤러리 이미지 선택 기능 추가 #257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d271be6
96a7fbf
7f95b18
b7c5a33
f304d2a
ef0c300
65b7f39
237de4a
53a4508
7af19b7
d889432
c576319
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| android:width="24dp" | ||
| android:height="24dp" | ||
| android:viewportWidth="24" | ||
| android:viewportHeight="24"> | ||
| <path | ||
| android:pathData="M19,2.25C19.729,2.25 20.429,2.54 20.944,3.056C21.46,3.571 21.75,4.271 21.75,5V19C21.75,19.729 21.46,20.429 20.944,20.944C20.429,21.46 19.729,21.75 19,21.75H5C4.271,21.75 3.571,21.46 3.056,20.944C2.54,20.429 2.25,19.729 2.25,19V5C2.25,4.271 2.54,3.571 3.056,3.056C3.571,2.54 4.271,2.25 5,2.25H19ZM8.395,13.096C8.302,13.022 8.17,13.023 8.079,13.099L3.75,16.706V19C3.75,19.331 3.882,19.649 4.116,19.884C4.351,20.118 4.668,20.25 5,20.25H19C19.331,20.25 19.649,20.118 19.884,19.884C20.118,19.649 20.25,19.331 20.25,19V18.713L16.799,15.492C16.705,15.405 16.559,15.403 16.463,15.488L15.161,16.646C14.524,17.212 13.571,17.237 12.905,16.704L8.395,13.096ZM5,3.75C4.668,3.75 4.351,3.882 4.116,4.116C3.882,4.351 3.75,4.668 3.75,5V14.753L7.118,11.946C7.757,11.414 8.683,11.405 9.332,11.925L13.842,15.532C13.937,15.608 14.073,15.605 14.164,15.524L15.466,14.367C16.141,13.767 17.163,13.779 17.823,14.396L20.25,16.66V5C20.25,4.668 20.118,4.351 19.884,4.116C19.649,3.882 19.331,3.75 19,3.75H5ZM16,7C17.105,7 18,7.895 18,9C18,10.105 17.105,11 16,11C14.895,11 14,10.105 14,9C14,7.895 14.895,7 16,7Z" | ||
| android:fillColor="#ffffff"/> | ||
| </vector> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,14 +2,21 @@ package com.ninecraft.booket.feature.record.ocr | |
|
|
||
| import android.net.Uri | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.LaunchedEffect | ||
| import androidx.compose.runtime.getValue | ||
| import androidx.compose.runtime.mutableIntStateOf | ||
| import androidx.compose.runtime.mutableStateOf | ||
| import androidx.compose.runtime.rememberCoroutineScope | ||
| import androidx.compose.runtime.setValue | ||
| import androidx.core.net.toUri | ||
| import com.ninecraft.booket.core.common.analytics.AnalyticsHelper | ||
| import com.ninecraft.booket.core.common.utils.UiText | ||
| import com.ninecraft.booket.core.common.utils.handleException | ||
| import com.ninecraft.booket.core.ocr.recognizer.CloudOcrRecognizer | ||
| import com.ninecraft.booket.feature.record.R | ||
| import com.ninecraft.booket.feature.record.ocr.OcrSideEffect.ShowToast | ||
| import com.ninecraft.booket.feature.screens.OcrScreen | ||
| import com.ninecraft.booket.feature.screens.OcrScreen.OcrResult | ||
| import com.orhanobut.logger.Logger | ||
| import com.slack.circuit.codegen.annotations.CircuitInject | ||
| import com.slack.circuit.retained.rememberRetained | ||
|
|
@@ -24,6 +31,7 @@ import kotlinx.collections.immutable.persistentListOf | |
| import kotlinx.collections.immutable.persistentSetOf | ||
| import kotlinx.collections.immutable.toPersistentList | ||
| import kotlinx.collections.immutable.toPersistentSet | ||
| import kotlinx.coroutines.delay | ||
| import kotlinx.coroutines.launch | ||
|
|
||
| @AssistedInject | ||
|
|
@@ -41,22 +49,65 @@ class OcrPresenter( | |
|
|
||
| companion object { | ||
| private const val RECORD_OCR_SENTENCE = "record_OCR_sentence" | ||
| private const val CAMERA_MAX_FAILURES = 2 | ||
| } | ||
|
|
||
| @Composable | ||
| override fun present(): OcrUiState { | ||
| val scope = rememberCoroutineScope() | ||
| var isLoading by rememberRetained { mutableStateOf(false) } | ||
| var currentUi by rememberRetained { mutableStateOf(OcrUi.CAMERA) } | ||
| var isPermissionDialogVisible by rememberRetained { mutableStateOf(false) } | ||
| var selectedImage by rememberRetained { mutableStateOf("") } | ||
| var sentenceList by rememberRetained { mutableStateOf(persistentListOf<String>()) } | ||
| var selectedIndices by rememberRetained { mutableStateOf(persistentSetOf<Int>()) } | ||
| var mergedSentence by rememberRetained { mutableStateOf("") } | ||
| var isTextDetectionFailed by rememberRetained { mutableStateOf(false) } | ||
| var isCameraRecognitionFailedDialogVisible by rememberRetained { mutableStateOf(false) } | ||
| var isGalleryRecognitionFailedDialogVisible by rememberRetained { mutableStateOf(false) } | ||
| var isRecaptureDialogVisible by rememberRetained { mutableStateOf(false) } | ||
| var isLoading by rememberRetained { mutableStateOf(false) } | ||
| var cameraFailureCount by rememberRetained { mutableIntStateOf(0) } | ||
| var sideEffect by rememberRetained { mutableStateOf<OcrSideEffect?>(null) } | ||
|
|
||
| fun recognizeText(imageUri: Uri) { | ||
| LaunchedEffect(isTextDetectionFailed) { | ||
| if (isTextDetectionFailed) { | ||
| delay(2000) | ||
| isTextDetectionFailed = false | ||
| } | ||
| } | ||
|
|
||
| fun handleRecognitionSuccess(text: String) { | ||
| isTextDetectionFailed = false | ||
| cameraFailureCount = 0 | ||
|
|
||
| val sentences = text | ||
| .split("\n") | ||
| .map { it.trim() } | ||
| .filter { it.isNotEmpty() } | ||
|
|
||
| sentenceList = sentences.toPersistentList() | ||
| currentUi = OcrUi.RESULT | ||
| analyticsHelper.logScreenView(RECORD_OCR_SENTENCE) | ||
| } | ||
|
|
||
| fun handleRecognitionFailure(source: RecognizeSource) { | ||
| when (source) { | ||
| RecognizeSource.CAMERA -> { | ||
| isTextDetectionFailed = true | ||
| cameraFailureCount += 1 | ||
|
|
||
| if (cameraFailureCount > CAMERA_MAX_FAILURES) { | ||
| isCameraRecognitionFailedDialogVisible = true | ||
| } | ||
| } | ||
|
|
||
| RecognizeSource.GALLERY -> { | ||
| isGalleryRecognitionFailedDialogVisible = true | ||
| } | ||
| } | ||
|
Comment on lines
+93
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -type f -name "OcrPresenter.kt" | head -5Repository: YAPP-Github/Reed-Android Length of output: 156 🏁 Script executed: wc -l feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.ktRepository: YAPP-Github/Reed-Android Length of output: 158 🏁 Script executed: cat -n feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt | sed -n '85,110p'Repository: YAPP-Github/Reed-Android Length of output: 1099 🏁 Script executed: cat -n feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt | sed -n '210,225p'Repository: YAPP-Github/Reed-Android Length of output: 657 🏁 Script executed: rg "cameraFailureCount" feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt -nRepository: YAPP-Github/Reed-Android Length of output: 300 🏁 Script executed: cat -n feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt | sed -n '70,85p'Repository: YAPP-Github/Reed-Android Length of output: 640 🏁 Script executed: rg "OnImageSelected" feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt -n -B 2 -A 5Repository: YAPP-Github/Reed-Android Length of output: 383 🏁 Script executed: rg "CAMERA_MAX_FAILURES" feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/ -nRepository: YAPP-Github/Reed-Android Length of output: 366 🏁 Script executed: cat -n feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt | sed -n '174,180p'Repository: YAPP-Github/Reed-Android Length of output: 437 🏁 Script executed: rg "OcrUiEvent\." feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt | head -20Repository: YAPP-Github/Reed-Android Length of output: 924 카메라 인식 실패 횟수 리셋 누락으로 인한 논리 오류
이는 사용자가 다이얼로그를 닫은 후 새로운 시도를 하려 할 때 예상치 못한 조기 실패 다이얼로그 노출로 이어집니다. 또한 갤러리 모드로 전환 후 다시 카메라로 돌아올 때도 카운트가 유지되어 새 세션에서 1회 실패만으로 다이얼로그가 나타날 수 있습니다. 수정 제안 OcrUiEvent.OnCameraRecognitionFailedDialogDismissed -> {
isCameraRecognitionFailedDialogVisible = false
+ cameraFailureCount = 0
}
is OcrUiEvent.OnImageSelected -> {
currentUi = OcrUi.IMAGE
selectedImage = event.imageUri
isTextDetectionFailed = false
isGalleryRecognitionFailedDialogVisible = false
+ cameraFailureCount = 0
}🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 정책 내용이 아래와 같은데, 연속 2회의 경우에 띄우는건지 2회 이상 실패에 띄우는건지 확인해보고 작업할게 OCR 인식 오류 <다이얼로그> There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
현재 코드는 다만 제가 지적한 핵심 이슈는 카운트 리셋 누락입니다:
정책 확인 후 임계값( 🧠 Learnings used |
||
| } | ||
|
|
||
| fun recognizeText(imageUri: Uri, source: RecognizeSource) { | ||
| scope.launch { | ||
| try { | ||
| isLoading = true | ||
|
|
@@ -65,25 +116,17 @@ class OcrPresenter( | |
| val text = it.responses.firstOrNull()?.fullTextAnnotation?.text.orEmpty() | ||
|
|
||
| if (text.isNotBlank()) { | ||
| isTextDetectionFailed = false | ||
| val sentences = text | ||
| .split("\n") | ||
| .map { it.trim() } | ||
| .filter { it.isNotEmpty() } | ||
|
|
||
| sentenceList = sentences.toPersistentList() | ||
| currentUi = OcrUi.RESULT | ||
| analyticsHelper.logScreenView(RECORD_OCR_SENTENCE) | ||
| handleRecognitionSuccess(text) | ||
| } else { | ||
| isTextDetectionFailed = true | ||
| handleRecognitionFailure(source) | ||
| } | ||
| } | ||
| .onFailure { exception -> | ||
| isTextDetectionFailed = true | ||
| handleRecognitionFailure(source) | ||
|
|
||
| val handleErrorMessage = { message: String -> | ||
| Logger.e("Cloud Vision API Error: ${exception.message}") | ||
| sideEffect = OcrSideEffect.ShowToast(message) | ||
| sideEffect = ShowToast(UiText.DirectString(message)) | ||
| } | ||
|
|
||
| handleException( | ||
|
|
@@ -118,14 +161,24 @@ class OcrPresenter( | |
|
|
||
| is OcrUiEvent.OnCaptureFailed -> { | ||
| isLoading = false | ||
| sideEffect = OcrSideEffect.ShowToast("이미지 캡처에 실패했어요") | ||
| sideEffect = ShowToast(UiText.StringResource(R.string.ocr_capture_failed)) | ||
| Logger.e("ImageCaptureException: ${event.exception.message}") | ||
| } | ||
|
|
||
| is OcrUiEvent.OnImageCaptured -> { | ||
| isTextDetectionFailed = false | ||
|
|
||
| recognizeText(event.imageUri) | ||
| recognizeText(event.imageUri, RecognizeSource.CAMERA) | ||
| } | ||
|
|
||
| is OcrUiEvent.OnImageSelected -> { | ||
| currentUi = OcrUi.IMAGE | ||
| selectedImage = event.imageUri | ||
| isTextDetectionFailed = false | ||
| cameraFailureCount = 0 | ||
|
|
||
| val pareUri = selectedImage.toUri() | ||
| recognizeText(pareUri, RecognizeSource.GALLERY) | ||
| } | ||
|
|
||
| is OcrUiEvent.OnReCaptureButtonClick -> { | ||
|
|
@@ -135,7 +188,7 @@ class OcrPresenter( | |
| is OcrUiEvent.OnSelectionConfirmed -> { | ||
| mergedSentence = selectedIndices | ||
| .sorted().joinToString("") { sentenceList[it] } | ||
| navigator.pop(result = OcrScreen.OcrResult(mergedSentence)) | ||
| navigator.pop(result = OcrResult(mergedSentence)) | ||
| } | ||
|
|
||
| is OcrUiEvent.OnSentenceSelected -> { | ||
|
|
@@ -155,6 +208,19 @@ class OcrPresenter( | |
| is OcrUiEvent.OnRecaptureDialogDismissed -> { | ||
| isRecaptureDialogVisible = false | ||
| } | ||
|
|
||
| OcrUiEvent.OnImageContentClosed -> { | ||
| currentUi = OcrUi.CAMERA | ||
| } | ||
|
|
||
| OcrUiEvent.OnCameraRecognitionFailedDialogDismissed -> { | ||
| isCameraRecognitionFailedDialogVisible = false | ||
| cameraFailureCount = 0 | ||
| } | ||
|
|
||
| OcrUiEvent.OnImageRecognitionFailedDialogDismissed -> { | ||
| isGalleryRecognitionFailedDialogVisible = false | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -165,9 +231,12 @@ class OcrPresenter( | |
| return OcrUiState( | ||
| currentUi = currentUi, | ||
| isPermissionDialogVisible = isPermissionDialogVisible, | ||
| selectedImage = selectedImage, | ||
| sentenceList = sentenceList, | ||
| selectedIndices = selectedIndices, | ||
| isTextDetectionFailed = isTextDetectionFailed, | ||
| isCameraRecognitionFailedDialogVisible = isCameraRecognitionFailedDialogVisible, | ||
| isGalleryRecognitionFailedDialogVisible = isGalleryRecognitionFailedDialogVisible, | ||
| isRecaptureDialogVisible = isRecaptureDialogVisible, | ||
| isLoading = isLoading, | ||
| sideEffect = sideEffect, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
대용량 이미지 방어 로직이 필요합니다.
갤러리 이미지 도입으로 매우 큰 파일이 들어올 수 있어
readBytes()가 OOM 또는 API 제한 초과를 유발할 수 있습니다. 파일/콘텐츠 길이 사전 체크(또는 제한 스트리밍)로 상한을 두는 처리가 필요합니다.🛠️ 제안 수정안 (사전 용량 체크 예시)
📝 Committable suggestion
🤖 Prompt for AI Agents