-
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
Conversation
- OCR 화면의 UI 상태에 따라 `OcrCameraView`, `OcrImageView`, `OcrResultView`로 뷰를 분리하여 구조를 개선
|
Caution Review failedAn error occurred during the review process. Please try again later. Walkthrough갤러리 이미지 선택을 OCR 인식 플로우에 추가하고, 카메라/갤러리별 UI·오류 흐름을 분리·관리하며 CloudOcrRecognizer에 Context 기반 URI 바이트 읽기 로직을 도입했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CameraUI as OcrCameraContent
participant Gallery as PhotoPicker
participant Presenter as OcrPresenter
participant OCR as CloudOcrRecognizer
participant ImageUI as OcrImageContent
participant Result as OcrResultContent
User->>CameraUI: 화면 진입 / 권한 승인
CameraUI->>User: 갤러리 버튼, 캡처 버튼 표시
alt 카메라 캡처
User->>CameraUI: 캡처
CameraUI->>CameraUI: 임시 파일에 저장
CameraUI->>Presenter: OnImageCaptured(fileUri, CAMERA)
Presenter->>OCR: recognizeText(fileUri, CAMERA)
else 갤러리 선택
User->>CameraUI: 갤러리 버튼 클릭
CameraUI->>Gallery: 사진 선택기 실행
Gallery->>User: 이미지 선택
Gallery->>Presenter: OnImageSelected(contentUri)
Presenter->>OCR: recognizeText(contentUri, GALLERY)
end
alt 인식 성공
OCR->>Presenter: 텍스트 결과
Presenter->>Presenter: 문장 분할, 상태 업데이트
Presenter->>Result: 상태 전파 (문장 목록)
Result->>User: 문장 선택 UI 제공
else 인식 실패
OCR->>Presenter: 오류
alt 출처 == CAMERA
Presenter->>CameraUI: 카메라 실패 다이얼로그 표시
else 출처 == GALLERY
Presenter->>ImageUI: 갤러리 실패 다이얼로그 표시
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt (1)
108-138: 동시 인식 요청 시 결과 덮어쓰기 가능
연속 캡처/재선택이 발생하면 늦게 완료된 이전 요청이 최신 결과를 덮어쓸 수 있습니다. 요청 ID 가드 또는 이전 Job 취소를 권장합니다.수정 제안
+ var recognitionRequestId by rememberRetained { mutableStateOf(0) } fun recognizeText(imageUri: Uri, source: RecognizeSource) { + recognitionRequestId += 1 + val requestId = recognitionRequestId scope.launch { try { isLoading = true recognizer.recognizeText(imageUri) .onSuccess { + if (requestId != recognitionRequestId) return@onSuccess val text = it.responses.firstOrNull()?.fullTextAnnotation?.text.orEmpty() if (text.isNotBlank()) { handleRecognitionSuccess(text) } else { handleRecognitionFailure(source) } } .onFailure { exception -> + if (requestId != recognitionRequestId) return@onFailure handleRecognitionFailure(source) val handleErrorMessage = { message: String -> Logger.e("Cloud Vision API Error: ${exception.message}") sideEffect = OcrSideEffect.ShowToast(message) } handleException( exception = exception, onError = handleErrorMessage, onLoginRequired = {}, ) } } finally { - isLoading = false + if (requestId == recognitionRequestId) { + isLoading = false + } } } }
🤖 Fix all issues with AI agents
In
`@core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/recognizer/CloudOcrRecognizer.kt`:
- Around line 31-41: 현재 CloudOcrRecognizer의 이미지 로드(변수 byte 생성)에서 readBytes()를 바로
호출해 대용량 이미지로 인해 OOM/제한 초과가 발생할 수 있으므로, 최대 허용 바이트(MAX_IMAGE_BYTES) 상수를 도입해 파일/콘텐트
길이를 사전 확인하거나 제한 스트리밍으로 읽도록 변경하세요: 파일 스킴일 때는 File.length()로 크기를 확인하고 초과 시
IllegalArgumentException을 던지며, content 스킴일 때는
ContentResolver.query(Images.Media.SIZE) 또는 openAssetFileDescriptor?.length를 시도해
크기를 확인하되 길이를 얻을 수 없으면 InputStream을 읽을 때는 고정 버퍼로 누적 읽기하며 누적 크기가 MAX_IMAGE_BYTES를
넘으면 즉시 중단해 에러를 던지도록 구현(관련 식별자: CloudOcrRecognizer, imageUri,
context.contentResolver, readBytes()).
In
`@feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/component/CameraBottomBar.kt`:
- Around line 51-55: In CameraBottomBar replace hardcoded English
contentDescription values on the Icon composables (the one using
ImageVector.vectorResource(designR.drawable.ic_gallery) and the scan Icon) with
localized string resources; add ocr_gallery_icon and ocr_scan_icon to
strings.xml and use the Compose stringResource(R.string.ocr_gallery_icon) and
stringResource(R.string.ocr_scan_icon) for the contentDescription in the
CameraBottomBar composable so accessibility strings are localizable.
In
`@feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/content/OcrCameraContent.kt`:
- Around line 238-265: Before calling cameraController.takePicture inside the
CameraBottomBar onCaptureClick lambda, check camera permission and camera
binding state and short-circuit if not available: verify permission (e.g., using
ContextCompat.checkSelfPermission or your existing permission state) and whether
the cameraController is ready, and if permission is missing call the permission
request flow or show a dialog instead of proceeding; keep existing eventSink
calls for success/failure (OcrUiEvent.OnCaptureStart, OnImageCaptured,
OnCaptureFailed) but do not invoke takePicture when permission is denied or
camera isn't bound.
In
`@feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/content/OcrImageContent.kt`:
- Around line 94-108: The ReedDialog currently allows outside taps/back button
to trigger its onDismissRequest which reopens the gallery; update the ReedDialog
invocation to set DialogProperties(dismissOnClickOutside = false) (or otherwise
disable outside dismiss) so only explicit dismiss button clicks call the
onDismissRequest that runs
state.eventSink(OcrUiEvent.OnImageRecognitionFailedDialogDismissed) and
photoPickerLauncher.launch(PickVisualMediaRequest(...)); ensure ReedDialog
retains its confirm button handler (OcrUiEvent.OnCloseClick) and that the
gallery reopen logic remains only in the explicit dismiss button handler.
In
`@feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt`:
- Around line 91-105: The bug is that cameraFailureCount is never reset, so the
"2 consecutive failures" policy is broken; update the logic to reset
cameraFailureCount to 0 whenever the camera-failure dialog is dismissed (the
handler that sets isCameraRecognitionFailedDialogVisible to false) and whenever
the app switches away from camera to gallery (the code that sets
isGalleryRecognitionFailedDialogVisible / toggles camera/gallery mode), and also
reset cameraFailureCount when switching back to camera to start a fresh session;
locate references to cameraFailureCount, handleRecognitionFailure,
isCameraRecognitionFailedDialogVisible and
isGalleryRecognitionFailedDialogVisible and add cameraFailureCount = 0 in those
transition/close handlers.
🧹 Nitpick comments (3)
feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUiState.kt (1)
13-21: 이미지 URI 타입을 일관되게 통일하세요.
OnImageCaptured는Uri인데selectedImage/OnImageSelected는String이라 타입 혼용으로 변환/파싱 오류가 생기기 쉽습니다. UI 상태와 이벤트를Uri(또는 모두String)로 통일하는 편이 안전합니다.♻️ 제안 변경 (Uri로 통일)
- val selectedImage: String = "", + val selectedImage: Uri? = null, ... - data class OnImageSelected(val imageUri: String) : OcrUiEvent + data class OnImageSelected(val imageUri: Uri) : OcrUiEventAlso applies to: 42-45
core/designsystem/src/main/res/drawable/ic_gallery.xml (1)
1-8: 라이트/다크 모드 아이콘 분리 여부 확인 필요
기본 drawable에#ffffff고정 색상이라 라이트 배경에서 사용 시 보이지 않을 수 있습니다. 이 아이콘이 다크 배경 전용이 아니라면drawable-night등 모드별 리소스 분리 적용 여부를 확인해 주세요. Based on learnings, 모드별 별도 아이콘 파일 방식을 선호합니다.feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt (1)
180-181: 변수명 오타 정리 제안
pareUri는 오타로 보입니다. 의미 전달을 위해parsedUri같은 이름이 더 명확합니다.수정 제안
- val pareUri = selectedImage.toUri() - recognizeText(pareUri, RecognizeSource.GALLERY) + val parsedUri = selectedImage.toUri() + recognizeText(parsedUri, RecognizeSource.GALLERY)
| val byte = when (imageUri.scheme) { | ||
| null, "file" -> { | ||
| val filePath = imageUri.path ?: throw IllegalArgumentException("URI does not have a valid path.") | ||
| val file = File(filePath) | ||
| file.readBytes() | ||
| } | ||
| else -> { | ||
| context.contentResolver.openInputStream(imageUri)?.use { it.readBytes() } | ||
| ?: throw IllegalArgumentException("Unable to open image input stream.") | ||
| } | ||
| } |
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 제한 초과를 유발할 수 있습니다. 파일/콘텐츠 길이 사전 체크(또는 제한 스트리밍)로 상한을 두는 처리가 필요합니다.
🛠️ 제안 수정안 (사전 용량 체크 예시)
+import android.content.res.AssetFileDescriptor
+
+private companion object {
+ private const val MAX_IMAGE_BYTES = 4 * 1024 * 1024 // 예: 4MB
+}
...
- val byte = when (imageUri.scheme) {
+ val byte = when (imageUri.scheme) {
null, "file" -> {
val filePath = imageUri.path ?: throw IllegalArgumentException("URI does not have a valid path.")
val file = File(filePath)
+ if (file.length() > MAX_IMAGE_BYTES) {
+ throw IllegalArgumentException("Image is too large.")
+ }
file.readBytes()
}
else -> {
+ context.contentResolver.openAssetFileDescriptor(imageUri, "r")?.use { afd ->
+ if (afd.length != AssetFileDescriptor.UNKNOWN_LENGTH && afd.length > MAX_IMAGE_BYTES) {
+ throw IllegalArgumentException("Image is too large.")
+ }
+ } ?: throw IllegalArgumentException("Unable to open image descriptor.")
context.contentResolver.openInputStream(imageUri)?.use { it.readBytes() }
?: throw IllegalArgumentException("Unable to open image input stream.")
}
}📝 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.
| val byte = when (imageUri.scheme) { | |
| null, "file" -> { | |
| val filePath = imageUri.path ?: throw IllegalArgumentException("URI does not have a valid path.") | |
| val file = File(filePath) | |
| file.readBytes() | |
| } | |
| else -> { | |
| context.contentResolver.openInputStream(imageUri)?.use { it.readBytes() } | |
| ?: throw IllegalArgumentException("Unable to open image input stream.") | |
| } | |
| } | |
| val byte = when (imageUri.scheme) { | |
| null, "file" -> { | |
| val filePath = imageUri.path ?: throw IllegalArgumentException("URI does not have a valid path.") | |
| val file = File(filePath) | |
| if (file.length() > MAX_IMAGE_BYTES) { | |
| throw IllegalArgumentException("Image is too large.") | |
| } | |
| file.readBytes() | |
| } | |
| else -> { | |
| context.contentResolver.openAssetFileDescriptor(imageUri, "r")?.use { afd -> | |
| if (afd.length != AssetFileDescriptor.UNKNOWN_LENGTH && afd.length > MAX_IMAGE_BYTES) { | |
| throw IllegalArgumentException("Image is too large.") | |
| } | |
| } ?: throw IllegalArgumentException("Unable to open image descriptor.") | |
| context.contentResolver.openInputStream(imageUri)?.use { it.readBytes() } | |
| ?: throw IllegalArgumentException("Unable to open image input stream.") | |
| } | |
| } |
🤖 Prompt for AI Agents
In
`@core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/recognizer/CloudOcrRecognizer.kt`
around lines 31 - 41, 현재 CloudOcrRecognizer의 이미지 로드(변수 byte 생성)에서 readBytes()를
바로 호출해 대용량 이미지로 인해 OOM/제한 초과가 발생할 수 있으므로, 최대 허용 바이트(MAX_IMAGE_BYTES) 상수를 도입해
파일/콘텐트 길이를 사전 확인하거나 제한 스트리밍으로 읽도록 변경하세요: 파일 스킴일 때는 File.length()로 크기를 확인하고 초과 시
IllegalArgumentException을 던지며, content 스킴일 때는
ContentResolver.query(Images.Media.SIZE) 또는 openAssetFileDescriptor?.length를 시도해
크기를 확인하되 길이를 얻을 수 없으면 InputStream을 읽을 때는 고정 버퍼로 누적 읽기하며 누적 크기가 MAX_IMAGE_BYTES를
넘으면 즉시 중단해 에러를 던지도록 구현(관련 식별자: CloudOcrRecognizer, imageUri,
context.contentResolver, readBytes()).
| Icon( | ||
| imageVector = ImageVector.vectorResource(designR.drawable.ic_gallery), | ||
| contentDescription = "Gallery Icon", | ||
| modifier = Modifier | ||
| .size(ReedTheme.spacing.spacing6), |
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.
접근성 설명 문자열을 리소스로 분리하세요.
contentDescription이 하드코딩 영문이라 로컬라이징이 불가합니다. 문자열 리소스로 분리해 접근성/다국어 대응을 해주세요.
🔧 제안 수정안
@@
-import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
@@
-import com.ninecraft.booket.core.designsystem.R as designR
+import com.ninecraft.booket.core.designsystem.R as designR
+import com.ninecraft.booket.feature.record.R
@@
- Icon(
+ Icon(
imageVector = ImageVector.vectorResource(designR.drawable.ic_gallery),
- contentDescription = "Gallery Icon",
+ contentDescription = stringResource(R.string.ocr_gallery_icon),
modifier = Modifier
.size(ReedTheme.spacing.spacing6),
)
@@
- Icon(
+ Icon(
imageVector = ImageVector.vectorResource(designR.drawable.ic_maximize),
- contentDescription = "Scan Icon",
+ contentDescription = stringResource(R.string.ocr_scan_icon),
modifier = Modifier.size(ReedTheme.spacing.spacing8),
)strings.xml에 ocr_gallery_icon, ocr_scan_icon 추가가 필요합니다.
Also applies to: 71-74
🤖 Prompt for AI Agents
In
`@feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/component/CameraBottomBar.kt`
around lines 51 - 55, In CameraBottomBar replace hardcoded English
contentDescription values on the Icon composables (the one using
ImageVector.vectorResource(designR.drawable.ic_gallery) and the scan Icon) with
localized string resources; add ocr_gallery_icon and ocr_scan_icon to
strings.xml and use the Compose stringResource(R.string.ocr_gallery_icon) and
stringResource(R.string.ocr_scan_icon) for the contentDescription in the
CameraBottomBar composable so accessibility strings are localizable.
| CameraBottomBar( | ||
| onGalleryClick = { | ||
| photoPickerLauncher.launch( | ||
| PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly), | ||
| ) | ||
| }, | ||
| onCaptureClick = { | ||
| state.eventSink(OcrUiEvent.OnCaptureStart) | ||
|
|
||
| val executor = ContextCompat.getMainExecutor(context) | ||
| val photoFile = File.createTempFile("ocr_", ".jpg", context.cacheDir) | ||
| val output = ImageCapture.OutputFileOptions.Builder(photoFile).build() | ||
|
|
||
| cameraController.takePicture( | ||
| output, | ||
| executor, | ||
| object : ImageCapture.OnImageSavedCallback { | ||
| override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { | ||
| state.eventSink(OcrUiEvent.OnImageCaptured(photoFile.toUri())) | ||
| } | ||
|
|
||
| override fun onError(exception: ImageCaptureException) { | ||
| state.eventSink(OcrUiEvent.OnCaptureFailed(exception)) | ||
| } | ||
| }, | ||
| ) | ||
| }, | ||
| buttonEnabled = !state.isLoading, |
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.
권한 미허용 상태에서 촬영 시도를 차단하세요.
현재는 권한이 없는 상태에서도 촬영 버튼 클릭 시 takePicture가 호출됩니다. 카메라가 바인딩되지 않은 상태라 실패하거나 예외가 발생할 수 있습니다. 권한을 먼저 확인하고, 미허용 시 다이얼로그/권한 요청으로 분기하세요.
🔧 제안 수정안
@@
CameraBottomBar(
@@
- onCaptureClick = {
- state.eventSink(OcrUiEvent.OnCaptureStart)
-
- val executor = ContextCompat.getMainExecutor(context)
- val photoFile = File.createTempFile("ocr_", ".jpg", context.cacheDir)
- val output = ImageCapture.OutputFileOptions.Builder(photoFile).build()
-
- cameraController.takePicture(
- output,
- executor,
- object : ImageCapture.OnImageSavedCallback {
- override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
- state.eventSink(OcrUiEvent.OnImageCaptured(photoFile.toUri()))
- }
-
- override fun onError(exception: ImageCaptureException) {
- state.eventSink(OcrUiEvent.OnCaptureFailed(exception))
- }
- },
- )
- },
+ onCaptureClick = {
+ if (!isGranted) {
+ state.eventSink(OcrUiEvent.OnShowPermissionDialog)
+ permissionLauncher.launch(permission)
+ } else {
+ state.eventSink(OcrUiEvent.OnCaptureStart)
+
+ val executor = ContextCompat.getMainExecutor(context)
+ val photoFile = File.createTempFile("ocr_", ".jpg", context.cacheDir)
+ val output = ImageCapture.OutputFileOptions.Builder(photoFile).build()
+
+ cameraController.takePicture(
+ output,
+ executor,
+ object : ImageCapture.OnImageSavedCallback {
+ override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
+ state.eventSink(OcrUiEvent.OnImageCaptured(photoFile.toUri()))
+ }
+
+ override fun onError(exception: ImageCaptureException) {
+ state.eventSink(OcrUiEvent.OnCaptureFailed(exception))
+ }
+ },
+ )
+ }
+ },🤖 Prompt for AI Agents
In
`@feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/content/OcrCameraContent.kt`
around lines 238 - 265, Before calling cameraController.takePicture inside the
CameraBottomBar onCaptureClick lambda, check camera permission and camera
binding state and short-circuit if not available: verify permission (e.g., using
ContextCompat.checkSelfPermission or your existing permission state) and whether
the cameraController is ready, and if permission is missing call the permission
request flow or show a dialog instead of proceeding; keep existing eventSink
calls for success/failure (OcrUiEvent.OnCaptureStart, OnImageCaptured,
OnCaptureFailed) but do not invoke takePicture when permission is denied or
camera isn't bound.
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.
카메라 권한이 없는 경우엔, '카메라 권한이 필요해요' 다이얼로그가 올라오고, 이게 바깥 영역의 터치를 막기 때문에, onCaptureClick Event가 발생할 경우는 없을 것 같아
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.
@easyhooon, 설명 감사합니다! 말씀하신 대로 권한 다이얼로그가 모달로 표시되어 하위 콘텐츠의 터치를 차단한다면, onCaptureClick이 권한 미허용 상태에서 발생하지 않는 것이 맞네요. UI 레이어에서 이미 방어되고 있다는 점 확인했습니다. 👍
🧠 Learnings used
Learnt from: seoyoon513
Repo: YAPP-Github/Reed-Android PR: 175
File: feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt:103-124
Timestamp: 2025-09-01T11:05:37.684Z
Learning: seoyoon513이 확인한 바로는 produceState 기반의 권한 상태 관리가 올바르게 동작한다. permissionLauncher 콜백과 ON_RESUME lifecycle 이벤트 조합으로 모든 권한 변경 시나리오가 적절히 처리됨
...re/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/content/OcrImageContent.kt
Show resolved
Hide resolved
| fun handleRecognitionFailure(source: RecognizeSource) { | ||
| when (source) { | ||
| RecognizeSource.CAMERA -> { | ||
| isTextDetectionFailed = true | ||
| cameraFailureCount += 1 | ||
|
|
||
| if (cameraFailureCount >= CAMERA_MAX_FAILURES) { | ||
| isCameraRecognitionFailedDialogVisible = true | ||
| } | ||
| } | ||
|
|
||
| RecognizeSource.GALLERY -> { | ||
| isGalleryRecognitionFailedDialogVisible = true | ||
| } | ||
| } |
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.
🧩 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
카메라 인식 실패 횟수 리셋 누락으로 인한 논리 오류
cameraFailureCount가 다이얼로그 닫기(라인 216-218)와 갤러리 모드로 전환(라인 174-178) 시에 리셋되지 않아 의도된 "연속 2회 실패" 정책이 깨집니다. 현재 로직에서는:
- 카메라에서 2회 연속 실패 → 다이얼로그 표시 (count=2)
- 다이얼로그 닫음 → count는 여전히 2
- 1회 더 실패 → 다이얼로그 즉시 표시 (count=3 ≥ 2)
이는 사용자가 다이얼로그를 닫은 후 새로운 시도를 하려 할 때 예상치 못한 조기 실패 다이얼로그 노출로 이어집니다.
또한 갤러리 모드로 전환 후 다시 카메라로 돌아올 때도 카운트가 유지되어 새 세션에서 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
In
`@feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt`
around lines 91 - 105, The bug is that cameraFailureCount is never reset, so the
"2 consecutive failures" policy is broken; update the logic to reset
cameraFailureCount to 0 whenever the camera-failure dialog is dismissed (the
handler that sets isCameraRecognitionFailedDialogVisible to false) and whenever
the app switches away from camera to gallery (the code that sets
isGalleryRecognitionFailedDialogVisible / toggles camera/gallery mode), and also
reset cameraFailureCount when switching back to camera to start a fresh session;
locate references to cameraFailureCount, handleRecognitionFailure,
isCameraRecognitionFailedDialogVisible and
isGalleryRecognitionFailedDialogVisible and add cameraFailureCount = 0 in those
transition/close handlers.
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.
정책 내용이 아래와 같은데, 연속 2회의 경우에 띄우는건지 2회 이상 실패에 띄우는건지 확인해보고 작업할게
OCR 인식 오류 <다이얼로그>
OCR 인식 지연 메시지 2번 띄운 후에도 인식 안되는 경우 → 직접 입력 유도
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.
@seoyoon513, 정책 확인하시는 것 좋습니다!
현재 코드는 cameraFailureCount >= CAMERA_MAX_FAILURES (Line 98)를 사용하고 있어서, 2회차 실패부터 다이얼로그가 표시됩니다. 만약 정책이 "2번 띄운 후" (즉, 3회차에 다이얼로그)라면 > 조건으로 변경하시면 됩니다.
다만 제가 지적한 핵심 이슈는 카운트 리셋 누락입니다:
- 다이얼로그를 닫아도
cameraFailureCount가 유지되어, 다음 실패 시 즉시 다이얼로그가 재노출됨 - 갤러리 모드로 전환했다가 카메라로 돌아와도 이전 카운트가 남아있음
정책 확인 후 임계값(>= vs >)과 함께 카운트 리셋 로직도 추가해 주시면 좋을 것 같습니다. 확인 후 작업 진행하시면 됩니다! 👍
🧠 Learnings used
Learnt from: seoyoon513
Repo: YAPP-Github/Reed-Android PR: 72
File: feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt:70-70
Timestamp: 2025-07-28T18:22:00.618Z
Learning: LiveTextAnalyzer는 ML Kit 에러를 addOnFailureListener로 처리하지만 로그만 남기고 presenter에 실패를 알리지 않아서, UI에서 텍스트 인식 실패 상태를 표시할 수 없는 문제가 있음. StillTextAnalyzer처럼 실패 콜백을 추가하는 것이 더 나은 해결책임
Learnt from: seoyoon513
Repo: YAPP-Github/Reed-Android PR: 46
File: feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/component/InfiniteLazyColumn.kt:83-95
Timestamp: 2025-07-14T00:46:03.843Z
Learning: seoyoon513과 팀은 한국어 주석을 선호하며, 한국어 주석을 영어로 번역하라는 제안을 하지 않아야 함
|
깔끔하네요 |
|
도서 상세화면 진입시 앱 크래시 발생하는건 제 작업 브랜치 merge하면 해결될것으로 예상됩니다. |
| import com.ninecraft.booket.core.designsystem.theme.ReedTheme | ||
|
|
||
| @Composable | ||
| internal fun ImageProcessingLoader( |
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.
로띠로 처리할것을 예상했는데 직접 구현했군요ㄷ 좋슴다.
- mutableStateOf<Int>: 제네릭이라 int → Integer 박싱 필요 - mutableIntStateOf: int를 직접 저장하여 박싱 불필요
easyhooon
left a comment
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.
수정 필요 부분 간단한 작업이라 제가 처리했구, 동작에도 문제 없어서 merge 하면 될 것 같습니다~ 수고하셨습니다!
리팩토링 굿굿
|
@easyhooon 수정 감사합니다~!! OCR 인식 실패 정책 반영했고 테스트 후 문제 없어서 머지 진행할게요! |
🔗 관련 이슈
📙 작업 설명
화면 UI 구조 리팩토링
OcrUi에서 각 content를 별도 패키지 및 파일로 분리
갤러리에서 선택한 이미지 인식 기능 추가
텍스트 인식 실패 케이스 추가
https://www.figma.com/design/Y77gFoPNlSRCPOHDR1FZtw/UI--YAPP-?node-id=976-7149&t=fOM62LsQrusfEgDM-1
https://www.figma.com/design/fDTbvFj8OXTlvKY0gNJF6w/UI-v1.1.4~-?node-id=1272-13405&t=RihYxTV2aShV2FID-1
📸 스크린샷 또는 시연 영상
갤러리 이미지 인식
Screen_recording_20260121_223114.mp4
카메라 촬영 이미지 인식 실패
Screen_recording_20260121_223140.mp4
갤러리 이미지 인식 실패
Screen_recording_20260121_223219.mp4
💬 추가 설명 or 리뷰 포인트
Summary by CodeRabbit
새로운 기능
버그 수정
개선사항
✏️ Tip: You can customize this high-level summary in your review settings.