-
Notifications
You must be signed in to change notification settings - Fork 0
[Feature/#71] Quiz에 TTS 기능을 추가합니다. #81
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
Walkthrough음성 합성(TTS) 기능을 구현하여 퀴즈 화면에 통합합니다. TTS 인터페이스, Android 구현체, 텍스트 정규화기, 의존성 주입을 추가하고, 모든 퀴즈 뷰모델에 TtsManager를 주입한 후 로딩 완료 시 자동 음성 출력과 이미지 클릭 콜백을 연결합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Screen
participant ViewModel
participant LaunchedEffect
participant TtsManager
participant AndroidTTS
Screen->>Screen: Render Quiz Content
Screen->>LaunchedEffect: Subscribe to currentQuizId + isLoading
Note over LaunchedEffect: Wait for isLoading → false
LaunchedEffect->>ViewModel: speakCurrentQuestion()
ViewModel->>ViewModel: Get current quiz
ViewModel->>ViewModel: Normalize text
ViewModel->>TtsManager: speak(normalizedText)
TtsManager->>TtsManager: Initialize TextToSpeech if needed
TtsManager->>AndroidTTS: speak(text)
AndroidTTS->>AndroidTTS: Queue audio playback
sequenceDiagram
participant User
participant Screen
participant ViewModel
participant TtsManager
User->>Screen: Click Image
Screen->>ViewModel: onImageClick() callback
alt TTS currently speaking
ViewModel->>TtsManager: stop()
TtsManager->>TtsManager: Halt playback
end
ViewModel->>TtsManager: speak(question)
TtsManager->>TtsManager: Play audio
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/daily/DailyQuizViewModel.kt (1)
261-264:onCleared()에서ttsManager.destroy()호출이 누락되었습니다.
MemoryQuizViewModel과 달리DailyQuizViewModel의onCleared()에서ttsManager.destroy()가 호출되지 않아 TTS 리소스가 해제되지 않을 수 있습니다.🔧 수정 제안
override fun onCleared() { super.onCleared() sttManager.destroy() + ttsManager.destroy() }
🤖 Fix all issues with AI agents
In
@feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/TtsManager.kt:
- Around line 3-8: The current TtsManager.destroy() design lets multiple
ViewModels call AndroidTtsManager.destroy()/tts?.shutdown(), causing premature
shutdowns and repeated init/shutdown cycles; change the lifecycle management so
singleton TTS is only shutdown at app level: either remove destroy() from the
TtsManager interface and make AndroidTtsManager.shutdown()/tts?.shutdown()
private and invoked from an Application-level cleanup (e.g.,
Application.onTerminate or a LifecycleObserver via ProcessLifecycleOwner), or
replace destroy() with explicit acquire()/release() reference-counting methods
on TtsManager and have each ViewModel call release() (not direct shutdown);
update all ViewModels (SpaceTimeQuizViewModel, PersistenceQuizViewModel,
MemoryQuizViewModel, LinguisticQuizViewModel, AttentionQuizViewModel,
DailyQuizViewModel) to follow the chosen pattern so cleanup is centralized and
consistent, and remove direct calls to AndroidTtsManager.initEngine()/shutdown()
from ViewModels.
In
@feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/TtsModule.kt:
- Around line 9-15: AndroidTtsManager currently stores only a single pendingTask
so multiple speak() calls during initialization (isReady == false) overwrite
pendingTask and drop earlier requests; change pendingTask into a queue (e.g., a
FIFO list) or buffer all incoming speak() requests when isReady is false,
enqueue each call from speak(), and on initialization completion (or after
destroy() reinitializes) drain the queue and invoke speak for each entry in
order; update references to pendingTask in AndroidTtsManager, ensure destroy()
resets/clears the queue appropriately, and preserve existing behavior when
isReady == true.
🧹 Nitpick comments (5)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/QuizTextNormalizer.kt (1)
4-12: 음수 및 공백 처리 개선 고려현재 구현은 기본적인 산술 표현식에는 잘 동작하지만, 몇 가지 엣지 케이스가 있습니다:
- 음수 표현:
"-5"→" 빼기 5"(의도치 않은 결과)- 기존 공백:
"5 - 3"→"5 빼기 3"(이중 공백)퀴즈 문맥에서 이런 케이스가 드물다면 현재 구현도 괜찮지만, 필요시 정규식으로 개선할 수 있습니다.
♻️ 선택적 개선안
object QuizTextNormalizer { fun normalizeExpression(expression: String): String { return expression - .replace("-", " 빼기 ") - .replace("+", " 더하기 ") - .replace("*", " 곱하기 ") - .replace("/", " 나누기 ") - .replace("=", " 은(는) ") - .trim() + .replace(Regex("(?<=\\d)\\s*-\\s*(?=\\d)"), " 빼기 ") + .replace(Regex("(?<=\\d)\\s*\\+\\s*(?=\\d)"), " 더하기 ") + .replace(Regex("(?<=\\d)\\s*\\*\\s*(?=\\d)"), " 곱하기 ") + .replace(Regex("(?<=\\d)\\s*/\\s*(?=\\d)"), " 나누기 ") + .replace(Regex("\\s*=\\s*"), " 은(는) ") + .trim() } }feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizViewModel.kt (1)
42-45: 하드코딩된 TTS 텍스트를 문자열 리소스로 추출하는 것을 고려해 보세요.현재 "아래의 그림은 무엇일까요?"가 코드에 직접 작성되어 있습니다. 다국어 지원이나 텍스트 변경 시 유지보수가 어려울 수 있습니다.
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/AndroidTtsManager.kt (1)
25-36: onInit에서 언어 설정 실패 시 사용자에게 피드백이 없습니다.
LANG_MISSING_DATA또는LANG_NOT_SUPPORTED상태일 때isReady가false로 유지되어 TTS가 작동하지 않지만, 사용자나 호출자에게 이 상태를 알리는 방법이 없습니다. 로깅을 추가하거나 콜백/상태를 통해 초기화 실패를 알리는 것을 고려해 보세요.🔧 로깅 추가 제안
override fun onInit(status: Int) { if (status == TextToSpeech.SUCCESS) { val result = tts?.setLanguage(Locale.KOREAN) if (result != TextToSpeech.LANG_MISSING_DATA && result != TextToSpeech.LANG_NOT_SUPPORTED) { isReady = true pendingTask?.invoke() pendingTask = null + } else { + Timber.w("TTS 한국어 언어팩이 지원되지 않거나 누락되었습니다: result=$result") } } else { + Timber.e("TTS 초기화 실패: status=$status") tts = null } }feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizDescription.kt (1)
29-29: 포매팅:=앞에 공백이 누락되었습니다.코드 스타일 일관성을 위해
=앞에 공백을 추가해 주세요.🔧 수정 제안
- onImageClick: () -> Unit= {}, + onImageClick: () -> Unit = {},feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/component/MemoryQuizVoiceModeContent.kt (1)
57-62: 일관성 문제:onImageClick을TopQuizDescription에 직접 전달하세요.
TopQuizDescription은 이미onImageClick파라미터를 받아 내부 이미지에 클릭 핸들러를 적용합니다. 현재 구현은modifier에clickableWithoutRipple을 적용하여 전체 영역을 클릭 가능하게 만들었지만, 이는 다른 화면들과 일관성이 없습니다.♻️ 수정 제안
TopQuizDescription( quizDescription = "방금 나온 단어를\n순서대로 말씀해주세요!", - modifier = Modifier - .padding(horizontal = 20.dp) - .clickableWithoutRipple(onClick = onImageClick, role = Role.Button) + onImageClick = onImageClick, + modifier = Modifier.padding(horizontal = 20.dp) )
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (24)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/attention/AttentionQuizScreen.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/attention/AttentionQuizViewModel.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizDescription.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/LinguisticQuizForm.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/MemoryQuizForm.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/PersistenceQuizForm.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/SpaceTimeQuizForm.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/daily/DailyQuizScreen.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/daily/DailyQuizViewModel.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizScreen.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizViewModel.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/model/LinguisticQuizUiState.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/MemoryQuizScreen.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/MemoryQuizViewModel.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/component/MemoryQuizTextModeContent.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/component/MemoryQuizVoiceModeContent.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizScreen.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizViewModel.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/spacetime/SpaceTimeQuizScreen.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/spacetime/SpaceTimeQuizViewModel.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/AndroidTtsManager.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/QuizTextNormalizer.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/TtsManager.ktfeature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/TtsModule.kt
🧰 Additional context used
🧬 Code graph analysis (8)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/AndroidTtsManager.kt (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/TtsManager.kt (1)
speak(5-5)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/attention/AttentionQuizScreen.kt (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizLoadContent.kt (1)
QuizLoadContent(33-87)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/LinguisticQuizForm.kt (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizDescription.kt (1)
CenterQuizDescription(62-96)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/PersistenceQuizForm.kt (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizDescription.kt (1)
TopQuizDescription(26-60)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/MemoryQuizScreen.kt (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizLoadContent.kt (1)
QuizLoadContent(33-87)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/SpaceTimeQuizForm.kt (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizDescription.kt (1)
CenterQuizDescription(62-96)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizScreen.kt (2)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizLoadContent.kt (1)
QuizLoadContent(33-87)core/designsystem/src/main/kotlin/com/moa/app/designsystem/component/core/button/MaButton.kt (1)
MaButton(30-68)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/MemoryQuizForm.kt (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/component/MemoryQuizTextModeContent.kt (1)
MemoryQuizTextModeContent(26-91)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run Unit Tests
🔇 Additional comments (49)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/component/MemoryQuizTextModeContent.kt (4)
30-31: LGTM!TTS 기능 지원을 위한
isContinueEnabled와onImageClick파라미터 추가가 적절합니다. 파라미터 명명이 명확하고 용도가 잘 드러납니다.
43-46: LGTM!
onImageClick콜백을CenterQuizDescription에 전달하여 이미지 클릭 시 TTS가 실행될 수 있도록 올바르게 연결되었습니다.
77-79: LGTM!
MaButton의enabled상태를 외부에서 제어할 수 있도록 변경하여 입력 유효성에 따른 버튼 활성화/비활성화가 가능해졌습니다.
96-102: LGTM!Preview 함수가 새로운 파라미터들로 올바르게 업데이트되었습니다.
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/MemoryQuizForm.kt (4)
20-23: LGTM!
isTextContinueButtonEnabled와onImageClick파라미터가 추가되어 TTS 기능과 텍스트 모드 버튼 상태 제어가 가능해졌습니다. 파라미터 위치도 관련 콜백들과 논리적으로 그룹화되어 있습니다.
44-52: LGTM!VOICE 모드에서
onImageClick콜백이 올바르게 전달되어 음성 입력 모드에서도 TTS 재생이 가능합니다.
55-62: LGTM!TEXT 모드에서
MemoryQuizTextModeContent에 모든 필요한 파라미터가 올바르게 전달되었습니다.isTextContinueButtonEnabled→isContinueEnabled매핑이 명확합니다.
71-88: LGTM!Preview 함수가 새로운 파라미터들을 포함하여 올바르게 업데이트되었습니다.
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/attention/AttentionQuizViewModel.kt (5)
86-86: TTS 중지 로직 적절함답변 확인 전 진행 중인 TTS를 중지하는 것은 올바른 UX 처리입니다.
109-125: 다음 문제 이동 시 TTS 자동 재생 누락 가능성
goToNextQuestion()에서currentQuestionIndex가 업데이트된 후 새 문제에 대한 TTS가 자동으로 시작되지 않습니다. UI 레이어에서 상태 변화를 감지하여 처리한다면 문제없으나, 그렇지 않다면 사용자가 수동으로 재생해야 합니다.
161-164: TtsManager 리소스 정리 적절함
onCleared()에서ttsManager.destroy()를 호출하여 ViewModel 소멸 시 TTS 리소스를 올바르게 해제합니다.
30-35: TtsManager DI 주입 적절함Hilt를 통한
TtsManager생성자 주입이 올바르게 구현되었습니다.
44-48: 자동 재생 로직이 올바르게 구현되어 있습니다
speakCurrentQuestion()함수는 UI 레이어(AttentionQuizScreen)에서 자동으로 호출됩니다.LaunchedEffect를 통해currentQuizId변경을 감지하고, 로딩이 완료될 때까지 대기한 후(isLoading == false) 자동으로 발화를 재생하므로 PR 목표인 "퀴즈 진입 시 TTS 발화(자동 재생)"가 올바르게 구현되었습니다.Likely an incorrect or invalid review comment.
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/MemoryQuizScreen.kt (3)
44-51:inputMode변경 시 TTS가 재실행될 수 있습니다.
snapshotFlow에서Triple에inputMode를 포함하고 있지만,filter조건에서는 사용하지 않습니다.distinctUntilChanged()는Triple전체를 비교하므로, 사용자가 음성 모드에서 텍스트 모드로 전환할 때 TTS가 다시 실행됩니다.의도된 동작이라면 무시해도 됩니다. 그렇지 않다면
inputMode를 제외하는 것을 고려해주세요:♻️ 제안된 수정
snapshotFlow { - Triple(uiState.quizState, uiState.inputMode, uiState.isLoading) + uiState.quizState to uiState.isLoading } - .filter { (quizState, _, isLoading) -> + .filter { (quizState, isLoading) -> quizState == MemoryQuizSetState.ANSWERING && !isLoading }
60-66: LGTM!
onImagesFinished와onImageClick콜백이 ViewModel 메서드에 올바르게 연결되었습니다. 이미지 클릭 시 수동 TTS 재생 기능이 적절히 구현되었습니다.
140-158: LGTM!
ANSWERING상태에서InputMode에 따른 분기 처리가 명확하고,onImageClick과isContinueEnabled가 각각의 모드 컴포넌트에 올바르게 전달되고 있습니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/model/LinguisticQuizUiState.kt (1)
30-31: LGTM!
selectedAnswerIndex의 null 여부를 기반으로 버튼 활성화 상태를 반환하는 깔끔한 파생 상태입니다. 기존 computed property 패턴과 일관성 있게 구현되었습니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/SpaceTimeQuizForm.kt (1)
35-42: LGTM!
onImageClick콜백이CenterQuizDescription에 올바르게 전달되고 있습니다. 다른 퀴즈 폼 컴포넌트들과 일관된 패턴입니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/LinguisticQuizForm.kt (1)
36-42: LGTM!
onImageClick파라미터가 올바르게 추가되고CenterQuizDescription에 전달되었습니다. TTS 트리거를 위한 일관된 패턴입니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/PersistenceQuizForm.kt (1)
25-31: LGTM!
onImageClick파라미터가TopQuizDescription에 올바르게 전달되고 있습니다. 다른 퀴즈 폼들과 일관된 TTS 트리거 패턴을 따르고 있습니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizViewModel.kt (3)
28-33: TtsManager 의존성 주입이 적절합니다.Hilt를 통한 TtsManager 주입과 생성자 매개변수 추가가 올바르게 구현되었습니다.
78-79: TTS 중지 로직이 적절합니다.사용자가 답변을 확인할 때 진행 중인 TTS를 중지하는 것은 좋은 UX입니다.
147-150: onCleared()에서 TtsManager 리소스 정리가 올바르게 구현되었습니다.ViewModel이 소멸될 때 TTS 리소스를 적절히 해제하여 메모리 누수를 방지합니다.
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/spacetime/SpaceTimeQuizViewModel.kt (2)
27-33: TTS 통합 패턴이 다른 ViewModel과 일관성 있게 구현되었습니다.LinguisticQuizViewModel과 동일한 패턴으로 TtsManager가 주입되어 코드베이스 전반에 걸쳐 일관된 구조를 유지합니다.
42-45: TTS 발화 및 생명주기 관리가 적절합니다.speakCurrentQuestion()과 onCleared()에서의 리소스 정리가 올바르게 구현되었습니다.
Also applies to: 147-150
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/attention/AttentionQuizScreen.kt (2)
45-54: LaunchedEffect를 사용한 TTS 자동 재생 구현이 적절합니다.
snapshotFlow와first()를 조합하여 로딩 완료 시점을 감지하고 TTS를 트리거하는 패턴이 올바르게 구현되었습니다.currentQuizId를 키로 사용하여 퀴즈가 변경될 때마다 새로운 발화를 시작합니다.
59-66: onImageClick 콜백 연결이 올바릅니다.이미지 클릭 시
speakCurrentQuestion을 호출하여 사용자가 질문을 다시 들을 수 있도록 구현되었습니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizScreen.kt (2)
52-61: 다른 퀴즈 화면과 일관된 TTS 자동 재생 패턴입니다.AttentionQuizScreen과 동일한
LaunchedEffect+snapshotFlow패턴을 사용하여 코드베이스 전반에 걸쳐 일관성을 유지합니다.
134-137: 버튼 활성화 로직 개선이 적절합니다.
uiState.isContinueButtonEnabled를 사용하여 버튼 상태를 관리하는 것은 상태 관리를 UI 상태 클래스에 위임하는 좋은 패턴입니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/AndroidTtsManager.kt (3)
9-15: AndroidTtsManager 클래스 구조가 적절합니다.
TtsManager인터페이스와TextToSpeech.OnInitListener를 구현하여 Android TTS 기능을 캡슐화했습니다. 지연 초기화 패턴을 사용하여 필요할 때만 TTS 엔진을 초기화합니다.
38-50: speak() 메서드의 지연 실행 패턴이 적절합니다.TTS가 준비되지 않았을 때
pendingTask에 저장하여 초기화 완료 후 실행하는 패턴이 올바르게 구현되었습니다.isFlush파라미터를 통해 큐 모드를 제어할 수 있어 유연합니다.
57-63: destroy() 메서드에서 리소스 정리가 완전합니다.
stop(),shutdown()호출 후 모든 상태를 초기화하여 메모리 누수와 리소스 누수를 방지합니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/spacetime/SpaceTimeQuizScreen.kt (2)
52-61: 일관된 TTS 자동 재생 패턴이 적용되었습니다.다른 퀴즈 화면(Attention, Linguistic)과 동일한
LaunchedEffect+snapshotFlow패턴을 사용합니다.
94-101: SpaceTimeQuizContent에 onImageClick이 올바르게 전파됩니다.컴포저블 계층 구조를 통해 이미지 클릭 콜백이 적절히 전달됩니다.
Also applies to: 126-132
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizViewModel.kt (4)
44-47: 동적 질문 텍스트를 사용하는 좋은 패턴입니다.다른 ViewModel(Linguistic, SpaceTime)에서 하드코딩된 텍스트를 사용하는 것과 달리,
currentQuiz.questionContent를 직접 사용하여 각 퀴즈의 실제 질문을 읽어줍니다. 이 패턴이 다른 ViewModel에도 적용되면 좋겠습니다.
80-81: checkAnswer()에서 TTS 중지 로직이 일관되게 구현되었습니다.다른 ViewModel과 동일한 패턴으로 답변 확인 시 진행 중인 TTS를 중지합니다.
130-134: onSuccess 핸들러에서 isLoading 상태 업데이트가 추가되었습니다.성공 시에도
isLoading = false를 설정하여 상태 관리가 더 명확해졌습니다.
153-156: onCleared()에서 TtsManager 정리가 올바르게 구현되었습니다.ViewModel 생명주기 종료 시 TTS 리소스를 적절히 해제합니다.
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizDescription.kt (1)
51-58: LGTM!이미지에
clickableWithoutRipple을 적용하고Role.Button을 설정하여 접근성을 고려한 구현이 적절합니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizScreen.kt (2)
44-53: LGTM!
LaunchedEffect와snapshotFlow를 사용하여 로딩 완료 후 TTS를 트리거하는 패턴이 적절합니다.currentQuizId가 변경될 때마다 새로운 effect가 시작되고,.first()를 사용하여 로딩이 완료된 후 한 번만 음성을 출력합니다.
86-122: LGTM!
onImageClick콜백이PersistenceQuizContent에서PersistenceQuizForm까지 올바르게 전달되고 있습니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/MemoryQuizViewModel.kt (3)
60-68: LGTM!
speakCurrentQuestion()구현이 입력 모드에 따라 적절한 안내 문구를 선택하여 TTS로 출력합니다.
100-103: LGTM!STT 시작 전에 TTS를 중지하여 음성 입력과 출력 간의 충돌을 방지하는 좋은 패턴입니다.
234-238: LGTM!
onCleared()에서ttsManager.destroy()를 호출하여 리소스를 적절히 정리합니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/component/MemoryQuizVoiceModeContent.kt (1)
194-205: LGTM!Preview가 새로운
onImageClick파라미터를 포함하도록 업데이트되었습니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/daily/DailyQuizScreen.kt (2)
51-65: 모드 변경 시 TTS가 다시 트리거될 수 있습니다.
snapshotFlow가memoryQuizInputMode를 포함하고.collect를 사용하므로, MemoryQuiz에서 음성 모드와 텍스트 모드 간 전환 시speakCurrentQuestion()이 다시 호출됩니다. 이것이 의도된 동작인지 확인해 주세요.만약 의도된 동작이 아니라면, 모드 전환 시에는 TTS를 트리거하지 않도록 수정이 필요합니다.
106-212: LGTM!모든 퀴즈 폼 컴포넌트(
PersistenceQuizForm,LinguisticQuizForm,MemoryQuizForm,AttentionQuizForm,SpaceTimeQuizForm)에onImageClick콜백이 일관되게 전달되고 있습니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/daily/DailyQuizViewModel.kt (2)
65-85: LGTM!
speakCurrentQuestion()이 모든 퀴즈 유형을 적절히 처리하고, 로딩 중이거나 결과 다이얼로그 표시 중에는 TTS를 실행하지 않도록 가드 조건이 잘 설정되어 있습니다.
137-140: LGTM!STT 시작 전 TTS 중지 패턴이
MemoryQuizViewModel과 일관되게 적용되었습니다.
| interface TtsManager { | ||
| val isSpeaking: Boolean | ||
| fun speak(text: String, isFlush: Boolean = true) | ||
| fun stop() | ||
| fun destroy() | ||
| } |
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:
#!/bin/bash
# Check how destroy() is called across ViewModels
rg -n "ttsManager\.destroy\(\)" --type kotlinRepository: team-memory-care/MoA-Android
Length of output: 731
🏁 Script executed:
#!/bin/bash
# Also search for TtsModule binding configuration
rg -n "TtsManager" --type kotlin | head -30Repository: team-memory-care/MoA-Android
Length of output: 2373
🏁 Script executed:
#!/bin/bash
# Check for any @HiltViewModel annotations and onCleared implementations
rg -n "onCleared\|@HiltViewModel\|SingletonComponent" --type kotlin | grep -E "(onCleared|HiltViewModel|SingletonComponent)" | head -20Repository: team-memory-care/MoA-Android
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Check TtsModule scope annotation
cat feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/TtsModule.ktRepository: team-memory-care/MoA-Android
Length of output: 437
🏁 Script executed:
#!/bin/bash
# Check one ViewModel's onCleared implementation to see context
sed -n '145,155p' feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/spacetime/SpaceTimeQuizViewModel.ktRepository: team-memory-care/MoA-Android
Length of output: 213
🏁 Script executed:
#!/bin/bash
# Check AndroidTtsManager destroy() implementation
cat feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/AndroidTtsManager.kt | head -50Repository: team-memory-care/MoA-Android
Length of output: 1571
🏁 Script executed:
#!/bin/bash
# Get the full AndroidTtsManager including destroy() method
cat feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/AndroidTtsManager.ktRepository: team-memory-care/MoA-Android
Length of output: 1803
🏁 Script executed:
#!/bin/bash
# Check if all ViewModels using TtsManager call destroy() - particularly DailyQuizViewModel
for vm in feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/*/\*ViewModel.kt; do
if [ -f "$vm" ]; then
if grep -q "private val ttsManager: TtsManager" "$vm"; then
echo "=== $(basename $vm) ==="
if grep -q "ttsManager.destroy()" "$vm"; then
echo "✓ Calls destroy()"
else
echo "✗ Does NOT call destroy()"
fi
fi
fi
doneRepository: team-memory-care/MoA-Android
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Find all ViewModels that use TtsManager
rg -l "private val ttsManager: TtsManager" --type kotlinRepository: team-memory-care/MoA-Android
Length of output: 657
🏁 Script executed:
#!/bin/bash
# For each ViewModel, check if it calls destroy()
rg -A 10 "override fun onCleared" feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz --type kotlinRepository: team-memory-care/MoA-Android
Length of output: 5856
Singleton 스코프의 destroy() 호출 구조에 실제 문제가 있습니다.
이 인터페이스 설계 자체는 깔끔하지만, 구현 레벨에서 심각한 생명주기 문제가 발생합니다. AndroidTtsManager.destroy()가 tts?.shutdown()을 호출하는데, 현재 5개 ViewModel이 모두 onCleared()에서 이를 호출하고 있습니다:
- SpaceTimeQuizViewModel, PersistenceQuizViewModel, MemoryQuizViewModel, LinguisticQuizViewModel, AttentionQuizViewModel에서 destroy() 호출
문제점:
- ViewModel1이 종료되면 destroy()가 TextToSpeech 엔진을 완전히 shutdown
- 다른 화면에서 TTS가 필요하면 다시 initEngine()으로 생성 → 반복되는 init/shutdown 사이클
- TextToSpeech 자원 누수 및 성능 저하
또한 DailyQuizViewModel은 ttsManager를 주입받았는데 destroy()를 호출하지 않으므로 일관성이 없습니다.
해결안: Singleton 자원은 destroy()를 여러 ViewModel에서 호출하지 않도록 설계를 변경하거나, Application 레벨에서만 cleanup하도록 관리해야 합니다.
🤖 Prompt for AI Agents
In
@feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/TtsManager.kt
around lines 3 - 8, The current TtsManager.destroy() design lets multiple
ViewModels call AndroidTtsManager.destroy()/tts?.shutdown(), causing premature
shutdowns and repeated init/shutdown cycles; change the lifecycle management so
singleton TTS is only shutdown at app level: either remove destroy() from the
TtsManager interface and make AndroidTtsManager.shutdown()/tts?.shutdown()
private and invoked from an Application-level cleanup (e.g.,
Application.onTerminate or a LifecycleObserver via ProcessLifecycleOwner), or
replace destroy() with explicit acquire()/release() reference-counting methods
on TtsManager and have each ViewModel call release() (not direct shutdown);
update all ViewModels (SpaceTimeQuizViewModel, PersistenceQuizViewModel,
MemoryQuizViewModel, LinguisticQuizViewModel, AttentionQuizViewModel,
DailyQuizViewModel) to follow the chosen pattern so cleanup is centralized and
consistent, and remove direct calls to AndroidTtsManager.initEngine()/shutdown()
from ViewModels.
| @Module | ||
| @InstallIn(SingletonComponent::class) | ||
| abstract class TtsModule { | ||
|
|
||
| @Binds | ||
| @Singleton | ||
| abstract fun bindTtsManager(impl: AndroidTtsManager): TtsManager |
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:
#!/bin/bash
# Find and examine AndroidTtsManager implementation
fd "AndroidTtsManager" --type fRepository: team-memory-care/MoA-Android
Length of output: 159
🏁 Script executed:
cat feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/AndroidTtsManager.ktRepository: team-memory-care/MoA-Android
Length of output: 1803
🏁 Script executed:
# Check for speak() call patterns and usage of TtsManager
rg "\.speak\(" feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/ -A 2 -B 2Repository: team-memory-care/MoA-Android
Length of output: 679
🏁 Script executed:
# Search for TtsManager usage across the entire repository
rg "TtsManager" --type kt -B 2 -A 5 | head -100Repository: team-memory-care/MoA-Android
Length of output: 98
🏁 Script executed:
# Search for TtsManager usage - using pattern matching without file type
rg "TtsManager" --include="*.kt" | head -50Repository: team-memory-care/MoA-Android
Length of output: 513
🏁 Script executed:
# Search for TtsManager usage with correct rg syntax
rg "TtsManager" -g "*.kt"Repository: team-memory-care/MoA-Android
Length of output: 2327
🏁 Script executed:
# Check how ttsManager.speak() is actually called in one of the ViewModels
rg "ttsManager\.speak" feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/ -B 3 -A 1Repository: team-memory-care/MoA-Android
Length of output: 3842
pendingTask 덮어쓰기로 인한 음성 출력 손실 버그 있음
destroy() 후 speak() 재호출 시 적절히 재초기화되는 점은 확인되었습니다. 다만 AndroidTtsManager에서 초기화 중(isReady=false)에 여러 speak() 호출이 연속으로 들어올 경우, 각 호출이 pendingTask를 덮어써서 이전 요청들이 손실되는 버그가 있습니다.
예: speak("A") → speak("B") → speak("C") 순서로 호출되면, "A"와 "B"는 무시되고 "C"만 실행됩니다.
pendingTask를 큐 형태로 관리하거나, 초기화 중 들어오는 요청들을 모두 처리할 수 있도록 수정이 필요합니다.
🤖 Prompt for AI Agents
In
@feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/tts/TtsModule.kt
around lines 9 - 15, AndroidTtsManager currently stores only a single
pendingTask so multiple speak() calls during initialization (isReady == false)
overwrite pendingTask and drop earlier requests; change pendingTask into a queue
(e.g., a FIFO list) or buffer all incoming speak() requests when isReady is
false, enqueue each call from speak(), and on initialization completion (or
after destroy() reinitializes) drain the queue and invoke speak for each entry
in order; update references to pendingTask in AndroidTtsManager, ensure
destroy() resets/clears the queue appropriately, and preserve existing behavior
when isReady == true.
Related issue 🛠
Work Description ✏️
Screenshot 📸
Uncompleted Tasks 😅
Summary by CodeRabbit
새로운 기능
음성 읽기 기능 - 퀴즈 질문을 자동으로 음성으로 들을 수 있습니다. 화면 로드 완료 후 질문이 자동 재생되며, 필요시 이미지를 탭하여 다시 청취할 수 있습니다.
스마트 버튼 제어 - 사용자 입력 상태에 따라 진행 버튼이 자동으로 활성화/비활성화되어 더 나은 사용 경험을 제공합니다.
전체 퀴즈 지원 - 모든 퀴즈 유형(주의력, 언어, 기억, 공간-시간, 지속성)에서 음성 기능을 이용할 수 있습니다.
✏️ Tip: You can customize this high-level summary in your review settings.