Skip to content

연습 녹음 화면 구현#124

Open
HamBeomJoon wants to merge 19 commits intodevelopfrom
feat/#101-practice-record
Open

연습 녹음 화면 구현#124
HamBeomJoon wants to merge 19 commits intodevelopfrom
feat/#101-practice-record

Conversation

@HamBeomJoon
Copy link
Copy Markdown
Contributor

@HamBeomJoon HamBeomJoon commented May 2, 2026

📌 작업 내용

  • 연습 녹음 화면을 추가하고 홈 화면에서 진입할 수 있도록 네비게이션을 연결했습니다.

  • 마이크 권한 요청, 녹음 시작/중지, 녹음 파일 재생/정지, 녹음 시간 표시 흐름을 구현했습니다.

  • 녹음 분석 요청 흐름을 추가했습니다.

    • 연습 대본 조회
    • 녹음 파일 업로드
    • 분석 결과 조회
    • Loading / Success / Error 상태 화면 처리
  • 분석 결과 화면을 추가했습니다.

  • 녹음/재생 로직을 core-audio 모듈로 분리했습니다.

    • RecordingAudioController
    • MediaRecordingAudioController
  • 공통 권한 요청 유틸 rememberPermissionRequest를 추가했습니다.


🧩 관련 이슈


📸 스크린샷

2026-05-02.18.58.38.mov
  • 마이크 권한 거절후 녹음 버튼 클릭시 한번 더 요청 > 2번 거절 하면 설정에서 권한 설정 하도록 스낵바 표시
스크린샷 2026-05-02 18 59 52

📢 논의하고 싶은 내용

Summary by CodeRabbit

  • New Features

    • 연습용 음성 녹음 및 재생 UI 추가
    • 녹음 업로드 기반 음성 분석 기능 추가(발음 점수, 속도 평가)
    • 연습 스크립트 불러오기 및 분석 결과 화면 추가
    • 마이크 권한 요청 흐름 도입
  • Refactor

    • 홈 화면 네비게이션 및 모듈 구조 정리, 연습 녹음 진입 경로 추가

* **feat: 연습 녹음(Practice Recording) 핵심 로직 구현**
    * `PracticeRecordingAudioController`: `MediaRecorder`와 `MediaPlayer`를 사용한 음성 녹음 및 재생 제어 클래스 추가.
    * `PracticeRecordingViewModel`: 녹음 상태(IDLE, RECORDING, RECORDED, PLAYING) 관리 및 타이머 로직 구현.
    * `PracticeRecordingUiState` 및 `Contract`: MVI 패턴 기반의 상태, 의향(Intent), 효과(Effect) 정의.

* **feat: 연습 녹음 UI 컴포넌트 및 화면 추가**
    * `PracticeRecordingScreen`: 권한 체크 로직을 포함한 연습 녹음 메인 화면 구현.
    * `PracticeRecordingControl`: 녹음/정지/재생 상태에 따른 아이콘 및 타이머 텍스트 표시 컴포넌트 추가.
    * `PracticeScriptCard`: 화면 중앙에 연습용 스크립트를 표시하는 카드 컴포넌트 추가.
    * `strings.xml`: 연습 녹음 관련 타이틀, 안내 문구 및 랜덤 연습용 스크립트 배열 추가.

* **feat: 홈 화면 연동 및 네비게이션 설정**
    * `HomeScreen`: 바텀 시트 내 '연습하기' 버튼을 추가하여 연습 녹음 화면으로의 네비게이션 연동.
    * `PresentationSheet` & `EmptyPresentationSheet`: '연습하기' 버튼 UI 및 클릭 이벤트 리스너 추가.
    * `HomeEntryBuilder`: `PracticeRecordingNavKey`를 통한 화면 진입점 정의.
    * `PrezelAppState`: 앱 시작 화면을 `SplashNavKey`에서 `HomeNavKey`로 임시 변경.

* **build: 권한 설정 추가**
    * `AndroidManifest.xml`: 오디오 녹음을 위한 `RECORD_AUDIO` 권한 추가.
* feat: 연습 녹음 분석 단계별 UI 컴포넌트 추가
    * `PracticeRecordingAnalysisLoadingPage`: 분석 중임을 나타내는 Lottie 애니메이션 및 문구 표시
    * `PracticeRecordingAnalysisErrorPage`: 분석 실패 또는 음성 인식 오류 시 재시도 안내 화면 구현
    * `PracticeRecordingAnalysisSuccessPage`: 발음 점수 및 속도 분석 결과를 카드 형태의 시각적 요소와 함께 표시

* feat: `PracticeRecordingViewModel` 및 `UiState` 내 분석 로직 연동
    * `PracticeRecordingAnalysisStatus`: 분석 상태(Ready, Loading, Success, Error)를 관리하는 Sealed Interface 추가
    * `ClickAnalyze` 인텐트 처리 및 가상의 로딩 지연(3초) 후 결과 화면 전환 로직 구현

* refactor: `PracticeRecordingScreen` 구조 개선
    * 분석 상태에 따라 상단 바와 메인 컨텐츠 영역이 전환되도록 화면 흐름 분리
    * 분석 완료 시 홈으로 이동할 수 있도록 `navigateToHome` 내비게이션 콜백 추가

* docs: 분석 결과 표시를 위한 리소스 추가
    * 성공 결과 등급별 카드 이미지(`perfect`, `good`, `try`) 및 오류 아이콘 벡터 추가
    * 분석 관련 다국어 문자열(발화 점수, 속도 상태 등) 추가
* **feat: 녹음 및 재생 시 예외 처리 로직 추가**
    * `PracticeRecordingAudioController`의 주요 메서드(`startRecording`, `stopRecording`, `startPlayback`) 반환 타입을 `Result`로 변경하여 오류 상황을 캡처하도록 개선했습니다.
    * 녹음 시작 실패 시 사용자에게 알림을 주기 위한 `PracticeRecordingUiMessage` 및 `PracticeRecordingUiEffect.ShowMessage`를 추가했습니다.
    * `LaunchedEffect`를 통해 스낵바(`showPrezelSnackbar`)로 녹음 실패 메시지를 출력하는 로직을 구현했습니다.

* **refactor: `PracticeRecordingUiState` 구조 개선**
    * 기존 `PracticeRecordingPhase` 열거형을 제거하고, 상태별 데이터를 포함할 수 있는 `PracticeRecordingState` sealed interface로 개편했습니다. (`Idle`, `Recording`, `Recorded`, `Playing`)
    * 상태 전이에 따른 `currentSeconds` 및 `totalSeconds` 계산 로직을 도메인 모델 내부로 캡슐화했습니다.

* **refactor: `PracticeRecordingViewModel` 로직 고도화**
    * `PracticeRecordingState` 개편에 맞춰 녹음/재생 제어 및 타이머 로직을 리팩터링했습니다.
    * 오디오 컨트롤러의 `Result` 반환값에 따라 성공/실패 처리를 명시적으로 수행하며, 실패 시 에러 상태(`PracticeRecordingAnalysisStatus.Error`)로 전환하도록 보완했습니다.
    * 상수를 사용하여 지연 시간(`TIMER_DELAY_MILLIS` 등)을 체계적으로 관리합니다.

* **etc: 리소스 및 컴포넌트 업데이트**
    * 녹음 실패 관련 문자열 리소스를 추가했습니다.
    * `PracticeRecordingAnalysisErrorType`의 명칭을 보다 명확하게 변경했습니다 (`ANALYZE` -> `ANALYSIS_FAILED`, `VOICE` -> `VOICE_RECOGNITION_FAILED`).
*   **refactor: 연습 녹음 관련 컴포넌트 및 로직 패키지 구조 재구성**
    *   분석 관련 컴포넌트(`PracticeRecordingAnalysisPages`, `PracticeRecordingAnalysisSuccessPage`)를 `practice.analysis.component` 패키지로 이동했습니다.
    *   오디오 제어 로직(`PracticeRecordingAudioController`)을 `practice.audio` 패키지로 이동했습니다.
    *   패키지 이동에 따른 `PracticeRecordingViewModel` 및 관련 클래스의 임포트 경로를 수정했습니다.

*   **feat: PracticeRecordingAnalysisScreen 분리 및 UI 로직 개선**
    *   `PracticeRecordingScreen`에 포함되어 있던 분석 상태별 UI 분기 로직을 별도의 `PracticeRecordingAnalysisScreen` 컴포넌트로 추출했습니다.
    *   `PracticeRecordingScreen`에서 권한 요청 및 녹음 제어 클릭 핸들러를 `remember` API를 사용해 최적화하고 가독성을 높였습니다.

*   **refactor: 도메인 모델 및 확장 함수 정리**
    *   `PracticeRecordingState`를 `PracticeRecordingControlState`로 변환하는 `toControlState` 확장 함수를 `PracticeRecordingControl.kt`로 이동하여 응집도를 높였습니다.
    *   `PracticeRecordingScreen` 내부에서 사용하던 스크립트 선택 및 권한 상태 관리 로직을 전용 `remember` 함수(`rememberPracticeScript`, `rememberRecordAudioPermissionState`)로 분리했습니다.
* **refactor: 분석 결과 관련 화면 패키지 이동 및 명칭 변경**
    * `practice.analysis` 패키지를 `practice.result`로 변경하고 관련 컴포넌트들을 이동했습니다.
    * `PracticeRecordingAnalysisScreen`을 `PracticeRecordingResultScreen`으로 변경했습니다.
    * `PracticeRecordingAnalysisSuccessPage`를 `PracticeRecordingResultPage`로 변경했습니다.
    * `PracticeRecordingAnalysisErrorPage`를 `PracticeRecordingAnalysisFailurePage`로 변경했습니다.

* **refactor: 연습 녹음 화면(PracticeRecordingScreen) UI 구조 개선**
    * 별도 파일로 분리되어 있던 `PracticeRecordingButtonArea`를 제거하고 `PracticeRecordingScreen` 내부에 `PrezelButtonArea`를 직접 구현하여 구조를 단순화했습니다.
    * `PracticeScriptCard` 컴포넌트를 제거하고 `PracticeRecordingContent` 내부에 인라인 `Box` 형태로 통합했습니다.

* **refactor: 분석 로딩 화면 및 컴포넌트 정리**
    * 사용되지 않는 `PracticeRecordingAnalysisPages.kt` 파일을 삭제하고 필요한 로딩 페이지 로직을 `practice.result` 패키지로 재배치했습니다.
* **feat: 연습(Practice) 관련 도메인 모델 및 Repository 추가**
    * 연습 스크립트(`PracticeScript`), 녹음 업로드(`PracticeRecordingUpload`), 분석 결과(`PracticeRecordingAnalysisResult`)를 정의하는 도메인 모델을 추가했습니다.
    * `PracticeRepository` 인터페이스를 정의하고, 가짜 데이터(Fake Data)를 반환하는 `PracticeRepositoryImpl`을 구현했습니다.

* **feat: 연습 관련 UseCase 구현 및 DI 설정**
    * `FetchPracticeScriptUseCase`, `UploadPracticeRecordingUseCase`, `FetchPracticeRecordingAnalysisResultUseCase`를 추가하여 비즈니스 로직을 분리했습니다.
    * `RepositoryModule`에 `PracticeRepository` 의존성 주입 설정을 추가했습니다.

* **feat: 연습 녹음 화면(PracticeRecordingScreen) 기능 확장**
    * 기존에 로컬 리소스에서 랜덤하게 가져오던 연습 스크립트를 서버(UseCase)에서 불러오도록 변경했습니다.
    * 녹음 완료 후 '분석하기' 클릭 시 실제 녹음 파일을 업로드하고 분석 결과를 조회하는 흐름을 구현했습니다.
    * 녹음 중단 실패 및 재생 실패에 대한 예외 처리와 스낵바 메시지(`PracticeRecordingUiMessage`)를 추가했습니다.

* **refactor: UI 상태 및 결과 화면 연동**
    * `PracticeRecordingUiState`에 `practiceScript`를 추가하고, `Success` 상태에 분석 결과 모델을 포함하도록 수정했습니다.
    * `PracticeRecordingResultScreen`에서 실제 분석 데이터(발음 점수, 속도)를 표시하도록 연동했습니다.
    * `PracticeRecordingContent` 레이아웃이 화면 전체 높이를 적절히 활용하도록 수정했습니다.

* **build: feature:home:impl 모듈 의존성 추가**
    * `core:domain` 모듈 의존성을 추가하여 UseCase를 사용할 수 있도록 설정했습니다.
* **feat: 마이크 권한 관리 로직 분리 및 고도화**
    * 권한 요청 및 상태 관리를 위한 `RecordAudioPermission.kt`를 추가하여 관련 로직을 모듈화했습니다.
    * `rememberRecordAudioPermissionControlClickHandler`를 통해 녹음 버튼 클릭 시 권한 상태(허용, 거부, 영구 거부)에 따른 분기 처리를 구현했습니다.
    * 일반 거부와 영구 거부(`shouldShowRequestPermissionRationale`)를 구분하여 각각 다른 UI 피드백을 제공하도록 개선했습니다.

* **feat: 녹음 화면 UI 컴포넌트 정리 및 기능 추가**
    * `PracticeRecordingTopAppBar`를 별도 파일로 분리하여 코드 가독성을 높였습니다.
    * 대본 로드 실패, 권한 거부 등에 대한 새로운 에러 메시지(`PracticeRecordingUiMessage`)와 관련 문자열 리소스를 추가했습니다.
    * 스낵바 표시 시 `collectLatest`와 `dismiss()`를 사용하여 이전 메시지를 정리하고 최신 상태를 즉시 반영하도록 수정했습니다.

* **refactor: PracticeRecordingViewModel 비즈니스 로직 보강**
    * `fetchPracticeScript` 로직을 개선하여 데이터 로드 실패 시 에러 효과(Effect)를 발생시키도록 수정했습니다.
    * 권한 거부 상황을 처리하기 위한 `DenyRecordAudioPermission`, `DenyRecordAudioPermissionPermanently` Intent를 추가했습니다.
    * 내부 메서드 명칭 변경 (`sendMessage` -> `showMessage`) 및 전반적인 예외 처리 로직을 강화했습니다.

* **build: 관련 리소스 추가**
    * 마이크 권한 거부 및 대본 로드 실패 안내를 위한 다국어 문자열을 `strings.xml`에 추가했습니다.
* docs: 연습 관련 UseCase 클래스에 KDoc 주석 추가

`core:domain` 모듈 내 연습(Practice) 기능과 관련된 UseCase 클래스들에 동작 흐름과 역할을 설명하는 한글 주석을 추가했습니다.

*   `FetchPracticeRecordingAnalysisResultUseCase`: 녹음본 ID를 기반으로 분석 결과를 조회하는 로직 설명 추가
*   `UploadPracticeRecordingUseCase`: 녹음본 파일 업로드 요청 처리 로직 설명 추가
*   `FetchPracticeScriptUseCase`: 연습용 대본 조회 요청 처리 로직 설명 추가
@HamBeomJoon HamBeomJoon self-assigned this May 2, 2026
@HamBeomJoon HamBeomJoon requested a review from moondev03 as a code owner May 2, 2026 10:02
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

연습 녹음 도메인·오디오 컨트롤러·리포지토리·유스케이스·Compose UI·권한 유틸을 추가하고 홈 화면 패키지를 리팩토링하며 RECORD_AUDIO 권한과 core:audio 모듈을 등록합니다.

변경 사항

연습 녹음 기능 전체 구현

Layer / File(s) Summary
데이터 모델 / 계약
core/model/practice/*, core/domain/repository/practice/*
PracticeScript, PracticeRecordingUpload, PracticeRecordingAnalysisResult, PracticeRecordingSpeed 추가 및 PracticeRepository 인터페이스 선언.
유스케이스
core/domain/usecase/practice/*
스크립트 조회/녹음 업로드/분석 흐름용 UseCase들 추가(Fetch/Upload/Analyze/etc.).
저장소 구현 / DI
core/data/repository/practice/*, core/data/di/RepositoryModule.kt
PracticeRepositoryImpl 페이크 구현과 Dagger 바인딩 추가.
오디오 API / 구현
core/audio/*
AudioSessionState, AudioSessionEffect, RecordingAudioController 인터페이스와 MediaRecordingAudioController 구현, Hilt 모듈 추가.
권한 유틸
core/ui/util/PermissionRequest.kt
런타임 권한 요청을 위한 rememberPermissionRequestPermissionRequest 타입 추가.
UI 계약 / 모델
feature/home/impl/practice/contract/*, .../model/*
Compose UI 상태·의도·효과·분석 상태·UI 모델 정의.
ViewModel
feature/home/impl/practice/PracticeRecordingViewModel.kt
오디오 컨트롤러와 유스케이스를 연결해 스크립트 로드 및 분석 흐름을 구현.
Screen / Components
feature/home/impl/practice/*
PracticeRecordingScreen, Ready/Result 하위 컴포넌트(TopAppBar, Content, Control, Loading/Result/Failure) 및 프리뷰 추가.
리소스
core/ui/res/drawable/*, feature/home/impl/res/drawable/*, feature/home/impl/res/values/strings.xml
오류 아이콘, 결과 카드 벡터, 문자열 리소스 추가.
네비게이션
feature/home/impl/navigation/*, feature/home/impl/navigation/HomeEntryBuilder.kt
PracticeRecordingNavKey 추가 및 HomeEntryBuilder에 엔트리 등록.

홈 화면 리팩토링

Layer / File(s) Summary
패키지 이동
feature/home/impl/main/*
HomeScreen, HomeViewModel, 계약·모델을 impl.main 패키지로 이동.
시트 버튼 추가
feature/home/impl/main/component/body/*
EmptyPresentationSheet와 PresentationSheet에 연습 시작 버튼 추가 및 콜백 전달.

앱 설정 / 빌드

Layer / File(s) Summary
매니페스트 및 앱 상태
app/src/main/AndroidManifest.xml, app/src/main/java/com/team/prezel/ui/PrezelAppState.kt
android.permission.RECORD_AUDIO 추가, 초기 네비게이션을 HomeNavKey로 변경.
모듈/의존성
settings.gradle.kts, core/audio/build.gradle.kts, feature/home/impl/build.gradle.kts, gradle/libs.versions.toml
:core:audio 모듈 포함, feature/home/impl에 coreDomain 의존성 추가, compose runtime 엔트리 추가.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/PracticeRepositoryImpl.kt`:
- Around line 12-31: The three suspend methods fetchPracticeScript,
uploadPracticeRecording, and fetchPracticeRecordingAnalysisResult currently wrap
delay(...) inside runCatching which swallows CancellationException; update each
to rethrow CancellationException instead of returning a failure Result — either
append .onFailure { if (it is CancellationException) throw it } to the
runCatching chain for each method or replace runCatching with a try/catch that
rethrows CancellationException and converts other exceptions into
Result.failure(...), ensuring proper cancellation propagation.

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/audio/PracticeRecordingAudioController.kt`:
- Around line 111-114: releaseRecorder()가 recorder만 해제하고 recordingStartedAt을
초기화하지 않아 재시도나 중복 stop 시 이전 세션 타이머가 남을 수 있습니다; releaseRecorder()에서
recorder?.release(); recorder = null과 함께 recordingStartedAt도 null/초기값으로 재설정(예:
recordingStartedAt = 0L 또는 null)하여 타이머 기준값을 공통 정리 경로에서 함께 비우도록 수정하세요.

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/component/PracticeRecordingContent.kt`:
- Around line 43-58: The script Box (in PracticeRecordingContent.kt) should be
made vertically scrollable so long scripts don't get clipped; add a scroll state
and verticalScroll to the Box's modifier chain (use rememberScrollState() and
Modifier.verticalScroll(...)) and adjust content alignment to top center if
needed so text starts at the top; keep the existing styling (fillMaxWidth,
weight(1f), clip, background, padding) and apply the verticalScroll to that same
Modifier before calling Text(practiceScript).

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/PracticeRecordingScreen.kt`:
- Around line 52-67: The two LaunchedEffect(Unit) blocks in
PracticeRecordingScreen are racing: the intent is sent via
viewModel.onIntent(PracticeRecordingUiIntent.LoadPracticeScript) before
viewModel.uiEffect.collectLatest is guaranteed to be collecting, risking lost
channel-based effects; consolidate into a single LaunchedEffect or reorder so
collectLatest on viewModel.uiEffect is started first, then call
viewModel.onIntent(PracticeRecordingUiIntent.LoadPracticeScript) afterwards,
ensuring snackbarHostState.currentSnackbarData?.dismiss() and
snackbarHostState.showPrezelSnackbar(...) are handled inside the collector
(viewModel.uiEffect.collectLatest) and not run prior to collection.

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/RecordAudioPermission.kt`:
- Around line 33-100: The returned RecordAudioPermissionRequest instance is
being memoized with remember in rememberRecordAudioPermissionRequest, which
freezes currentOnPermissionPermanentlyDenied to its initial value; remove the
outer remember so a fresh RecordAudioPermissionRequest is created on each
recomposition (still use rememberUpdatedState for
currentOnPermissionPermanentlyDenied and keep the launcher local), ensuring the
onPermanentlyDenied field of the returned RecordAudioPermissionRequest
references the latest currentOnPermissionPermanentlyDenied rather than a stale
captured value.

In `@Prezel/feature/home/impl/src/main/res/values/strings.xml`:
- Around line 44-46: Update the three English string resources used for image
content descriptions to Korean/descriptive phrases: change
feature_home_impl_practice_recording_analysis_card_perfect,
feature_home_impl_practice_recording_analysis_card_good, and
feature_home_impl_practice_recording_analysis_card_try to appropriate Korean
accessibility texts (e.g., “완벽해요”, “잘했어요”, “다음에 또 도전해 보세요” or similar), ensuring
they remain concise and suitable for contentDescription usage in the UI.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 380f4233-c833-48cf-b0dd-6e3865f3d063

📥 Commits

Reviewing files that changed from the base of the PR and between 258373c and cf07025.

📒 Files selected for processing (38)
  • Prezel/app/src/main/AndroidManifest.xml
  • Prezel/app/src/main/java/com/team/prezel/ui/PrezelAppState.kt
  • Prezel/core/data/src/main/java/com/team/prezel/core/data/di/RepositoryModule.kt
  • Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/PracticeRepositoryImpl.kt
  • Prezel/core/domain/src/main/java/com/team/prezel/core/domain/repository/practice/PracticeRepository.kt
  • Prezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/practice/FetchPracticeRecordingAnalysisResultUseCase.kt
  • Prezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/practice/FetchPracticeScriptUseCase.kt
  • Prezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/practice/UploadPracticeRecordingUseCase.kt
  • Prezel/core/model/src/main/java/com/team/prezel/core/model/practice/PracticeRecordingAnalysisResult.kt
  • Prezel/core/model/src/main/java/com/team/prezel/core/model/practice/PracticeRecordingUpload.kt
  • Prezel/core/model/src/main/java/com/team/prezel/core/model/practice/PracticeScript.kt
  • Prezel/core/ui/src/main/res/drawable/core_ui_error_analyze.xml
  • Prezel/core/ui/src/main/res/drawable/core_ui_error_voice.xml
  • Prezel/feature/home/impl/build.gradle.kts
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/HomeScreen.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/component/body/EmptyPresentationSheet.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/component/body/PresentationSheet.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/navigation/HomeEntryBuilder.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/navigation/PracticeRecordingNavKey.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/PracticeRecordingScreen.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/PracticeRecordingViewModel.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/RecordAudioPermission.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/audio/PracticeRecordingAudioController.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/component/PracticeRecordingContent.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/component/PracticeRecordingControl.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/component/PracticeRecordingTopAppBar.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/contract/PracticeRecordingUiEffect.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/contract/PracticeRecordingUiIntent.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/contract/PracticeRecordingUiState.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/model/PracticeRecordingUiMessage.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/result/PracticeRecordingResultScreen.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/result/component/PracticeRecordingAnalysisFailurePage.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/result/component/PracticeRecordingAnalysisLoadingPage.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/result/component/PracticeRecordingResultPage.kt
  • Prezel/feature/home/impl/src/main/res/drawable/feature_home_impl_card_good.xml
  • Prezel/feature/home/impl/src/main/res/drawable/feature_home_impl_card_perfect.xml
  • Prezel/feature/home/impl/src/main/res/drawable/feature_home_impl_card_try.xml
  • Prezel/feature/home/impl/src/main/res/values/strings.xml

Comment thread Prezel/feature/home/impl/src/main/res/values/strings.xml
* **refactor: `RecordAudioPermission` 관련 Compose 코드 최적화**
    * 불필요한 `remember` 및 `rememberUpdatedState`를 제거하여 권한 요청 람다와 `RecordAudioPermissionRequest` 생성 로직을 단순화했습니다.
    * 상태 변경에 따른 불필요한 리컴포지션 오버헤드를 줄였습니다.

* **fix: `PracticeRecordingAudioController` 리소스 해제 로직 보완**
    * `releaseRecorder()` 호출 시 `recordingStartedAt` 시간을 0으로 초기화하도록 수정하여, 녹음기 해제 후 발생할 수 있는 시간 계산 오류를 방지했습니다.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/audio/PracticeRecordingAudioController.kt`:
- Line 18: recordingFile 변수에 할당된 이전 temp .m4a 파일이 제거되지 않아 cacheDir에 고아 파일이 쌓입니다;
새 녹음 파일을 생성하는 코드(예: startRecording / createTempRecordingFile 같은 곳)에서 새 파일을 확정하거나
대체할 때 이전 recordingFile이 가리키던 파일을 삭제하도록 추가하고, 녹음 취소/확정/release() 경로 모두에서
recordingFile을 삭제(파일.delete())하고 recordingFile = null로 초기화하도록 구현하세요; 참조할 심볼:
recordingFile, 녹음 시작/종료 메서드(예: startRecording/stopRecording/confirmRecording)와
release()를 찾아 이전파일 정리 로직을 삽입하면 됩니다.
- Around line 50-58: stopRecording() currently computes duration from
recordingStartedAt and returns a success even when there is no active recording;
guard the method by first checking that recorder is non-null and
recordingStartedAt is a valid start timestamp (e.g., > 0) and if not return a
Result.failure(IllegalStateException("not recording")) instead of proceeding;
when valid, compute durationSeconds from recordingStartedAt, call
recorder.stop() inside runCatching, return Result.success(max(durationSeconds,
0)), and ensure releaseRecorder() still runs (e.g., keep the existing
finally/also behavior) so cleanup always happens; reference symbols:
stopRecording(), recorder, recordingStartedAt, releaseRecorder(),
recorder.stop().

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/RecordAudioPermission.kt`:
- Around line 56-66: The composable's hasRecordAudioPermission and
isPermanentlyDenied are only set once and can become stale when the user changes
permission in system Settings; add a lifecycle observer (e.g., DisposableEffect
+ LifecycleEventObserver on LocalActivity.current.lifecycle) to re-check
ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) and
ActivityCompat.shouldShowRequestPermissionRationale(activity,
Manifest.permission.RECORD_AUDIO) on ON_RESUME and assign the results back to
hasRecordAudioPermission and isPermanentlyDenied so both states stay
synchronized with the system permission when the activity resumes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7ba79f57-a036-4386-8c21-68fd0b35ea9a

📥 Commits

Reviewing files that changed from the base of the PR and between cf07025 and 52331c6.

📒 Files selected for processing (2)
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/RecordAudioPermission.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/audio/PracticeRecordingAudioController.kt

* **feat: 공통 권한 요청 `rememberPermissionRequest` 구현**
    * 기존 `RecordAudioPermission` 내부에 중복되어 있던 권한 요청 로직을 `core:ui` 모듈의 공통 유틸리티 클래스로 추출했습니다.
    * `LifecycleEventObserver`를 추가하여 앱이 다시 활성화(`ON_RESUME`)될 때 권한 상태를 자동으로 동기화하도록 개선했습니다.
    * 권한 허용 여부, 영구 거부 상태를 추적하고 콜백을 관리하는 `PermissionRequest` 데이터 클래스를 정의했습니다.

* **refactor: `RecordAudioPermission` 리팩터링**
    * 새로 구현한 공통 유틸리티 `rememberPermissionRequest`를 사용하도록 기존 코드를 제거하고 구조를 단순화했습니다.

* **fix: `PracticeRecordingAudioController` 리소스 관리 및 예외 처리 강화**
    * 새로운 녹음을 시작할 때 이전 임시 파일을 삭제하도록 수정했습니다.
    * `stopRecording` 호출 시 녹음기가 비활성 상태인 경우에 대한 예외 처리를 추가했습니다.
    * `release` 호출 시 현재 녹음 중인 파일을 명시적으로 삭제하도록 리소스 관리 로직을 보완했습니다.
@HamBeomJoon HamBeomJoon added ✨ feat 새로운 기능 추가 또는 기존 기능 확장 labels May 4, 2026
* **feat: `AnalyzePracticeRecordingUseCase` 및 관련 모델 추가**
    * 녹음본 업로드와 분석 결과 조회를 통합 수행하는 `AnalyzePracticeRecordingUseCase`를 추가했습니다.
    * UI 레이어에서 사용할 분석 상태(`PracticeRecordingAnalysisStatus`) 및 모델(`PracticeRecordingAnalysisUiModel`)을 정의했습니다.
    * `PracticeRecordingState` 및 분석 관련 모델들을 별도 패키지(`model`)로 분리하여 구조를 개선했습니다.

* **feat: `RecordingAudioController` 추상화 및 구현**
    * 오디오 녹음 및 재생 로직을 인터페이스(`RecordingAudioController`)로 추상화했습니다.
    * `MediaRecorder`와 `MediaPlayer`를 사용하는 `MediaRecordingAudioController` 구현체를 추가하고, Hilt를 이용한 의존성 주입(`RecordingAudioModule`)을 설정했습니다.

* **refactor: `PracticeRecordingViewModel` 로직 개선**
    * 녹음 분석 시 `AnalyzePracticeRecordingUseCase`를 사용하도록 변경했습니다.
    * Intent 명칭을 보다 명확하게 변경했습니다. (`ClickControl` -> `ToggleRecordingControl`, `ClickAnalyze` -> `AnalyzeClicked` 등)
    * 분석 시작 시 재생 중인 오디오를 중지하는 로직을 추가했습니다.

* **refactor: `PracticeRepositoryImpl` 위치 변경 및 패키징 정리**
    * `PracticeRepositoryImpl`을 `repository.practice` 패키지로 이동하여 일관성을 높였습니다.

* **ui: 분석 결과 화면 및 권한 처리 연동**
    * 도메인 모델(`PracticeRecordingSpeed`) 대신 UI 모델(`PracticeRecordingAnalysisSpeed`)을 사용하도록 결과 화면을 수정했습니다.
    * 오디오 권한 거부 관련 Intent 처리 로직을 갱신된 명칭에 맞게 수정했습니다.
* **feat: `core:audio` 모듈 신설 및 오디오 제어 로직 이동**
    * 기존 `feature:home:impl` 내에 위치하던 오디오 녹음 및 재생 관련 로직을 공통 모듈인 `core:audio`로 추출했습니다.
    * `RecordingAudioController`: 인터페이스를 `core:audio`로 이동하고 외부 접근이 가능하도록 `public`으로 변경했습니다.
    * `MediaRecordingAudioController`: `MediaRecorder`와 `MediaPlayer`를 사용하는 구현체를 이동했습니다.
    * `RecordingAudioModule`: Hilt를 이용한 의존성 주입 설정을 이동했습니다.
    * `feature:home:impl`에서 `core:audio` 모듈을 의존성에 추가하고 관련 import를 수정했습니다.

* **refactor: PermissionRequest 유틸리티 코드 정리**
    * `core:ui` 모듈의 `PermissionRequest` 내 중복되는 권한 확인 로직을 확장 함수(`isPermissionGranted`, `isPermissionPermanentlyDenied`)로 추출하여 가독성을 높였습니다.
    * 권한 결과 처리 로직(when 문 사용) 및 상태 동기화 로직을 간결하게 개선했습니다.

* **build: 프로젝트 설정 업데이트**
    * `settings.gradle.kts`에 `:core:audio` 모듈을 추가했습니다.
    * `core:audio` 모듈의 `build.gradle.kts`를 정의하고 필요한 플러그인을 설정했습니다.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@Prezel/core/audio/src/main/java/com/team/prezel/core/audio/MediaRecordingAudioController.kt`:
- Around line 75-93: There is a race between assigning the shared field player
and calling start() in startPlayback(): assign the new MediaPlayer to the player
field before calling start(), and ensure you can roll back on failure by storing
the previous player reference, setting pendingPlayer/player to the new instance
first, and if any exception occurs release the new instance and restore the
previous player; also make the OnCompletionListener capture the specific player
instance (not rely on a mutable field) so stopPlayback() operates on the correct
player.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 140cdf19-b3be-4bc1-bc3c-ed1239fd9e7b

📥 Commits

Reviewing files that changed from the base of the PR and between 52331c6 and d8e4d9c.

📒 Files selected for processing (21)
  • Prezel/core/audio/build.gradle.kts
  • Prezel/core/audio/src/main/java/com/team/prezel/core/audio/MediaRecordingAudioController.kt
  • Prezel/core/audio/src/main/java/com/team/prezel/core/audio/RecordingAudioController.kt
  • Prezel/core/audio/src/main/java/com/team/prezel/core/audio/RecordingAudioModule.kt
  • Prezel/core/data/src/main/java/com/team/prezel/core/data/di/RepositoryModule.kt
  • Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/practice/PracticeRepositoryImpl.kt
  • Prezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/practice/AnalyzePracticeRecordingUseCase.kt
  • Prezel/core/ui/src/main/java/com/team/prezel/core/ui/util/PermissionRequest.kt
  • Prezel/feature/home/impl/build.gradle.kts
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/PracticeRecordingScreen.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/PracticeRecordingViewModel.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/RecordAudioPermission.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/component/PracticeRecordingControl.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/contract/PracticeRecordingUiIntent.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/contract/PracticeRecordingUiState.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/model/PracticeRecordingAnalysisStatus.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/model/PracticeRecordingAnalysisUiModel.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/model/PracticeRecordingState.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/result/PracticeRecordingResultScreen.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/result/component/PracticeRecordingAnalysisFailurePage.kt
  • Prezel/settings.gradle.kts
✅ Files skipped from review due to trivial changes (6)
  • Prezel/feature/home/impl/build.gradle.kts
  • Prezel/core/audio/build.gradle.kts
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/model/PracticeRecordingAnalysisUiModel.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/model/PracticeRecordingAnalysisStatus.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/component/PracticeRecordingControl.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/result/PracticeRecordingResultScreen.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/contract/PracticeRecordingUiIntent.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/PracticeRecordingScreen.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/result/component/PracticeRecordingAnalysisFailurePage.kt

* **feat: `RecordingAudioController` 상태 기반 아키텍처 도입 및 기능 확장**
    * `RecordingAudioController`를 `StateFlow`(`AudioSessionState`) 및 `SharedFlow`(`AudioSessionEvent`) 기반의 반응형 구조로 개편했습니다.
    * 녹음 일시정지(`pause`), 재개(`resume`), 초기화(`reset`) 기능을 추가했습니다.
    * 외부 오디오 파일 불러오기(`loadAudioFile`) 기능을 추가하여 `Uri`를 통한 오디오 소스 선택을 지원합니다.
    * 내부적으로 `CoroutineScope`를 사용하여 녹음 및 재생 타이머 로직을 관리하도록 개선했습니다.

* **feat: 연습 녹음 화면(PracticeRecordingScreen) UI 및 기능 개선**
    * **외부 파일 선택:** `ActivityResultContracts.GetContent`를 이용해 기기 내 오디오 파일을 선택할 수 있는 기능을 추가했습니다.
    * **세분화된 제어 기능:** 기존 단일 제어 버튼을 상황에 맞는 다중 액션 버튼(일시정지, 중지, 초기화, 파일 선택 등)으로 확장했습니다.
    * **상태 매핑:** `PracticeRecordingState`에 일시정지 상태(`RecordingPaused`, `PlaybackPaused`) 및 오디오 소스 타입(`RECORDED_FILE`, `EXTERNAL_FILE`)을 추가했습니다.

* **refactor: `PracticeRecordingViewModel` 로직 최적화**
    * 기존의 수동 타이머 관리 로직을 제거하고, `RecordingAudioController`의 상태 흐름을 구독하여 UI를 갱신하도록 변경했습니다.
    * `PracticeRecordingUiIntent`를 세분화하여 각 녹음/재생 제어 동작을 명확히 정의했습니다.

* **build: 의존성 및 리소스 추가**
    * `core:audio` 모듈에 `kotlinx-coroutines-core` 의존성을 추가했습니다.
    * 오디오 파일 로드 실패 관련 에러 메시지 리소스를 추가했습니다.
…-record

# Conflicts:
#	Prezel/core/data/src/main/java/com/team/prezel/core/data/di/RepositoryModule.kt
* **refactor: 홈 화면 관련 패키지 구조 재구성**
    * 홈 화면 구성 컴포넌트들을 `feature:home:impl` 내 `main` 패키지로 이동하여 관리 구조를 개선했습니다.
    * `HomeScreen`, `HomeViewModel`, `HomeUiState` 등 핵심 UI 로직을 `main` 패키지로 그룹화했습니다.

* **refactor: 발표 연습 녹음(Practice Recording) 로직 간소화**
    * `RecordingAudioController`에서 복잡한 일시정지(`pause`), 재개(`resume`), 외부 파일 로드(`loadAudioFile`) 기능을 제거하고 핵심적인 녹음 및 재생 로직에 집중하도록 리팩터링했습니다.
    * `AudioSessionState` 및 `AudioSessionEffect` 모델을 단순화하여 녹음 상태 관리 효율성을 높였습니다.
    * 녹음 화면 UI 컴포넌트(`PracticeRecordingControl` 등)가 단순화된 오디오 상태 모델을 사용하도록 업데이트했습니다.

* **refactor: 도메인 모델 직접 참조 및 불필요한 모델 제거**
    * UI 레이어에서 별도로 정의하여 사용하던 `PracticeRecordingAnalysisSpeed` 및 `PracticeAnalysisSpeed`를 제거하고, `core:model`의 `PracticeRecordingSpeed`를 직접 사용하도록 수정했습니다.
    * `PracticeRecordingState` 인터페이스를 삭제하고 `core:audio`의 `AudioSessionState`로 통합했습니다.

* **feat: UI 컴포넌트 미리보기(Preview) 추가**
    * `PracticeRecordingContent`, `PracticeRecordingControl` 등 주요 연습 관련 컴포넌트에 다양한 상태별 `Preview`를 추가하여 개발 편의성을 향상했습니다.

* **refactor: 버튼 영역(ButtonArea) 호출 방식 개선**
    * `PrezelButtonArea` 사용 시 내부 버튼을 슬롯 방식(`mainButton`)으로 전달하도록 변경하여 확장성을 개선했습니다.
* **feat: 녹음 초기화 및 재시도 기능 구현**
    * `RecordingAudioController` 인터페이스 및 `MediaRecordingAudioController` 구현체에 `reset()` 메서드를 추가했습니다.
    * `PracticeRecordingUiIntent.RetryRecordingClicked` 인텐트를 추가하여 녹음 결과 화면에서 다시 시도할 수 있는 로직을 구현했습니다.
    * `PracticeRecordingViewModel`에서 `resetPracticeRecording`을 호출하여 오디오 컨트롤러를 초기화하고 분석 상태를 `Ready`로 되돌리도록 처리했습니다.

* **refactor: 오디오 컨트롤러 생명주기 및 리소스 관리 개선**
    * `MediaRecordingAudioController`에서 `release()`와 `reset()` 로직을 분리하여 리소스 해제 후에도 Scope가 유지될 수 있도록 수정했습니다.
    * `createMediaRecorder` 함수를 클래스 외부 유틸리티 함수로 분리하고 Context 주입 방식을 개선했습니다.

* **ui: 연습 결과 화면 및 네비게이션 로직 수정**
    * `PracticeRecordingResultPage`에서 불필요한 상단 앱바(`PrezelTopAppBar`)와 뒤로 가기(`onBack`) 파라미터를 제거했습니다.
    * `PracticeRecordingScreen`에서 분석이 완료된 후에는 뒤로 가기 동작(`BackHandler`)을 제한하여 상태 불일치를 방지했습니다.
    * 결과 화면에서 '다시 시도' 클릭 시 분석 로직을 다시 실행하는 대신 녹음 준비 단계로 돌아가도록 `onRetry` 콜백을 `onRetryRecording` 인텐트와 연결했습니다.

* **chore: 불필요한 의존성 및 코드 정리**
    * `PracticeRecordingResultPage`에서 사용하지 않는 Material3 컴포넌트 및 아이콘 관련 import를 삭제했습니다.
    * Preview 코드 내 불필요해진 `onBack` 인자 전달 로직을 정리했습니다.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (7)
Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/body/EmptyPresentationSheet.kt (1)

27-27: ⚡ Quick win

디자인 토큰 미사용 — PrezelTheme.spacing.V12 로 교체 권장

동일한 컨텍스트의 PresentationSheet.kt(line 36)에서는 PrezelTheme.spacing.V12를 사용하는 반면, 이 파일에서는 12.dp를 하드코딩하고 있어 일관성이 없습니다.

♻️ 수정 제안
-        Spacer(modifier = Modifier.height(12.dp))
+        Spacer(modifier = Modifier.height(PrezelTheme.spacing.V12))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/body/EmptyPresentationSheet.kt`
at line 27, EmptyPresentationSheet.kt uses a hardcoded Spacer height
(Modifier.height(12.dp)) causing inconsistency with PresentationSheet.kt;
replace the hardcoded 12.dp with the design token PrezelTheme.spacing.V12.
Locate the Spacer invocation inside EmptyPresentationSheet.kt (the one calling
Modifier.height) and update it to use PrezelTheme.spacing.V12 so spacing is
consistent with PresentationSheet.kt and the app’s design tokens.
Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/model/PresentationUiModel.kt (1)

22-26: 💤 Low value

in Int.MIN_VALUE..-1 범위 표현을 dDay < 0 조건으로 단순화할 수 있습니다.

Int.MIN_VALUE..-1은 음수 전체를 나타내는 올바른 표현이지만, Kotlin에서는 dDay < 0 조건 분기가 더 간결하고 가독성이 좋습니다.

♻️ 제안 수정
     val dDayLabel: String = when (dDay) {
         0 -> "D-Day"
-        in Int.MIN_VALUE..-1 -> "D+${-dDay}"
+        dDay < 0 -> "D+${-dDay}"
         else -> "D-$dDay"
     }

또는 when 대신 조건문으로 표현:

     val dDayLabel: String = when {
         dDay == 0 -> "D-Day"
         dDay < 0 -> "D+${-dDay}"
         else -> "D-$dDay"
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/model/PresentationUiModel.kt`
around lines 22 - 26, The when-expression in PresentationUiModel that computes
dDayLabel uses the verbose range "in Int.MIN_VALUE..-1"; replace that branch
with a simple negative check (dDay < 0) to improve readability: locate the
dDayLabel property in PresentationUiModel (uses dDay) and change the second when
branch to use dDay < 0 (or convert the whole when to an if/else if you prefer)
while keeping the existing outputs ("D-Day", "D+${-dDay}", "D-$dDay") unchanged.
Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/result/component/PracticeRecordingResultPage.kt (2)

134-144: ⚡ Quick win

rememberOverallResult 네이밍/시그니처 정리 + 분기 단순화 제안.

remember로 시작하는 헬퍼는 Compose 관례상 @Composable이며 내부에서 remember(...)로 메모이즈하는 것을 기대하게 됩니다. 이 함수는 일반 순수 함수이므로 이름이 오해를 부릅니다. 또한 pronunciationScore <= 60 -> TRYelse -> TRY가 동일한 결과를 반환해 한 줄이 잉여이고, 가독성을 위해 분기 순서를 결과 카드 기준으로 정리하면 의도가 더 분명해집니다.

추가로 의도 확인 부탁드립니다: 현재 로직상 pronunciationScore가 7094이면서 speed != ADEQUATE인 경우, 그리고 6169이면서 speed == ADEQUATE인 경우 모두 TRY로 분류됩니다. 의도된 등급 기준이라면 그대로 두셔도 되지만, "발음은 양호하지만 속도만 부적절"한 사용자가 가장 낮은 등급을 받는 경험이 되므로 등급 기준표와 한 번 더 대조해 보시면 좋겠습니다.

♻️ 제안 리팩터
-private fun rememberOverallResult(
-    pronunciationScore: Int,
-    speed: PracticeRecordingSpeed,
-): PracticeAnalysisOverallResult =
-    when {
-        pronunciationScore >= 95 && speed == PracticeRecordingSpeed.ADEQUATE -> PracticeAnalysisOverallResult.PERFECT
-        pronunciationScore >= 70 && speed == PracticeRecordingSpeed.ADEQUATE -> PracticeAnalysisOverallResult.GOOD
-        pronunciationScore >= 95 && speed != PracticeRecordingSpeed.ADEQUATE -> PracticeAnalysisOverallResult.GOOD
-        pronunciationScore <= 60 -> PracticeAnalysisOverallResult.TRY
-        else -> PracticeAnalysisOverallResult.TRY
-    }
+private fun overallResultOf(
+    pronunciationScore: Int,
+    speed: PracticeRecordingSpeed,
+): PracticeAnalysisOverallResult {
+    val isAdequateSpeed = speed == PracticeRecordingSpeed.ADEQUATE
+    return when {
+        pronunciationScore >= 95 && isAdequateSpeed -> PracticeAnalysisOverallResult.PERFECT
+        pronunciationScore >= 95 -> PracticeAnalysisOverallResult.GOOD
+        pronunciationScore >= 70 && isAdequateSpeed -> PracticeAnalysisOverallResult.GOOD
+        else -> PracticeAnalysisOverallResult.TRY
+    }
+}

호출부도 함께 수정해야 합니다:

-    val overallResult = rememberOverallResult(
-        pronunciationScore = pronunciationScore,
-        speed = speed,
-    )
+    val overallResult = overallResultOf(
+        pronunciationScore = pronunciationScore,
+        speed = speed,
+    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/result/component/PracticeRecordingResultPage.kt`
around lines 134 - 144, The function rememberOverallResult is not a Compose
memoized helper despite its name and has redundant branches; rename it to
computeOverallResult (or getOverallResult) and simplify its when-expression:
order branches by intended card priority (PERFECT, GOOD, TRY), remove the
duplicate "pronunciationScore <= 60 -> TRY" vs "else -> TRY" redundancy, and
make the conditions explicit for the GOOD/PERFECT cases (use
PracticeRecordingSpeed.ADEQUATE and speed != ADEQUATE checks as needed). Also
verify the edge cases you mentioned (70–94 with speed != ADEQUATE and 61–69 with
speed == ADEQUATE) match the desired grading policy and adjust conditions
accordingly in computeOverallResult.

36-52: 💤 Low value

@param: 사이트 타깃 대신 @get: 또는 @property: 권장.

@param:StringRes / @param:DrawableRes는 생성자 파라미터에만 어노테이션이 붙고, 프로퍼티 게터(예: overallResult.cardResId 호출 지점)에는 전파되지 않아 lint가 리소스 ID 검증을 놓칠 수 있습니다. 같은 패턴이 PracticeRecordingControl.ktPracticeRecordingControlAction.iconResId에도 있으니 함께 정리해 두면 좋겠습니다.

♻️ 제안 수정
-private enum class PracticeAnalysisOverallResult(
-    `@param`:StringRes val contentDescriptionResId: Int,
-    `@param`:DrawableRes val cardResId: Int,
-) {
+private enum class PracticeAnalysisOverallResult(
+    `@get`:StringRes val contentDescriptionResId: Int,
+    `@get`:DrawableRes val cardResId: Int,
+) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/result/component/PracticeRecordingResultPage.kt`
around lines 36 - 52, Replace the constructor parameter annotations on
PracticeAnalysisOverallResult (contentDescriptionResId and cardResId) from
`@param`:StringRes/@param:DrawableRes to a getter or property annotation so
resource type checks apply at call sites; e.g., use `@get`:StringRes or
`@get`:DrawableRes (or `@property`:) on the properties backing
PracticeAnalysisOverallResult and do the same for
PracticeRecordingControlAction.iconResId to ensure lint/resource validation
propagates to usages.
Prezel/core/audio/src/main/java/com/team/prezel/core/audio/RecordingAudioController.kt (1)

55-61: 💤 Low value

AudioSessionEffect에도 @Immutable 부여 권장.

AudioSessionState, AudioSource에는 @Immutable을 명시했지만 AudioSessionEffect만 빠져 있어 일관성이 깨집니다. 모두 data object로만 구성되어 사실상 불변이지만, 추후 페이로드를 가지는 variant가 추가될 때를 대비해 동일하게 표시해 두는 편이 안전합니다.

♻️ 제안 수정
+@Immutable
 sealed interface AudioSessionEffect {
     data object RecordingStartFailed : AudioSessionEffect
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Prezel/core/audio/src/main/java/com/team/prezel/core/audio/RecordingAudioController.kt`
around lines 55 - 61, AudioSessionEffect에 `@Immutable` 어노테이션이 빠져 일관성이 깨지므로 sealed
interface AudioSessionEffect 선언 위에 `@Immutable을` 추가하세요; RecordingStartFailed,
RecordingStopFailed, PlaybackStartFailed 같은 data object로만 구성되어 현재는 불변이지만 향후
페이로드가 추가될 수 있으니 AudioSessionState, AudioSource와 동일하게 `@Immutable을` 명시해 안전성을
확보하십시오.
Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/component/PracticeRecordingControl.kt (2)

117-163: ⚡ Quick win

actions()가 항상 1개짜리 리스트를 반환 — 단일 객체로 단순화 권장.

AudioSessionState 분기에서 반환하는 List<PracticeRecordingControlAction>은 항상 원소가 한 개이고, UI에서도 그 한 개만 그립니다. 리스트 컨테이너가 과한 추상화이며, 단일 액션을 직접 반환하면 forEach 루프와 spacing 처리도 사라져 가독성이 좋아집니다. 추후 정말 다중 액션이 필요해질 때 다시 도입해도 늦지 않습니다.

♻️ 제안 리팩터
-private fun AudioSessionState.actions(
-    onStartRecording: () -> Unit,
-    onStopRecording: () -> Unit,
-    onStartPlayback: () -> Unit,
-    onStopPlayback: () -> Unit,
-): List<PracticeRecordingControlAction> =
-    when (this) {
-        AudioSessionState.Idle -> listOf(
-            PracticeRecordingControlAction(
-                iconResId = PrezelIcons.Recording,
-                colorType = PracticeRecordingControlActionColorType.RECORD,
-                onClick = onStartRecording,
-            ),
-        )
-
-        is AudioSessionState.Recording -> stopAction(onStop = onStopRecording)
-
-        is AudioSessionState.ReadyToPlay -> listOf(
-            PracticeRecordingControlAction(
-                iconResId = PrezelIcons.Play,
-                colorType = PracticeRecordingControlActionColorType.REGULAR,
-                onClick = onStartPlayback,
-            ),
-        )
-
-        is AudioSessionState.Playing -> stopAction(onStop = onStopPlayback)
-    }
-
-private fun stopAction(onStop: () -> Unit): List<PracticeRecordingControlAction> =
-    listOf(
-        PracticeRecordingControlAction(
-            iconResId = PrezelIcons.Stop,
-            colorType = PracticeRecordingControlActionColorType.REGULAR,
-            onClick = onStop,
-        ),
-    )
+private fun AudioSessionState.action(
+    onStartRecording: () -> Unit,
+    onStopRecording: () -> Unit,
+    onStartPlayback: () -> Unit,
+    onStopPlayback: () -> Unit,
+): PracticeRecordingControlAction = when (this) {
+    AudioSessionState.Idle -> PracticeRecordingControlAction(
+        iconResId = PrezelIcons.Recording,
+        colorType = PracticeRecordingControlActionColorType.RECORD,
+        onClick = onStartRecording,
+    )
+    is AudioSessionState.Recording -> PracticeRecordingControlAction(
+        iconResId = PrezelIcons.Stop,
+        colorType = PracticeRecordingControlActionColorType.REGULAR,
+        onClick = onStopRecording,
+    )
+    is AudioSessionState.ReadyToPlay -> PracticeRecordingControlAction(
+        iconResId = PrezelIcons.Play,
+        colorType = PracticeRecordingControlActionColorType.REGULAR,
+        onClick = onStartPlayback,
+    )
+    is AudioSessionState.Playing -> PracticeRecordingControlAction(
+        iconResId = PrezelIcons.Stop,
+        colorType = PracticeRecordingControlActionColorType.REGULAR,
+        onClick = onStopPlayback,
+    )
+}

호출부도 단일 액션을 직접 사용하도록 수정합니다:

-    val actions = audioSessionState.actions(
+    val action = audioSessionState.action(
         onStartRecording = onStartRecording,
         onStopRecording = onStopRecording,
         onStartPlayback = onStartPlayback,
         onStopPlayback = onStopPlayback,
     )
@@
-        Row(
-            horizontalArrangement = Arrangement.spacedBy(PrezelTheme.spacing.V8),
-            verticalAlignment = Alignment.CenterVertically,
-        ) {
-            actions.forEach { action ->
-                PrezelIconButton(
-                    iconResId = action.iconResId,
-                    modifier = Modifier.size(48.dp),
-                    isRounded = true,
-                    buttonDefault = PrezelButtonDefaults.getDefault(
-                        ...
-                    ),
-                    onClick = action.onClick,
-                )
-            }
-        }
+        PrezelIconButton(
+            iconResId = action.iconResId,
+            modifier = Modifier.size(48.dp),
+            isRounded = true,
+            buttonDefault = PrezelButtonDefaults.getDefault(
+                ...
+            ),
+            onClick = action.onClick,
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/component/PracticeRecordingControl.kt`
around lines 117 - 163, The actions() function and stopAction() currently return
a List<PracticeRecordingControlAction> even though every branch only ever
produces a single PracticeRecordingControlAction; change
AudioSessionState.actions(...) to return a single PracticeRecordingControlAction
(not a List) and update stopAction(onStop: () -> Unit) to return
PracticeRecordingControlAction; adjust all callers that iterate or expect a list
(remove forEach/spacing logic and use the single PracticeRecordingControlAction
directly, e.g. replace list usages of PracticeRecordingControlAction returned
from actions() and stopAction() with the direct action and call its onClick or
read its iconResId/colorType as needed).

91-91: 💤 Low value

조건식 스타일 통일 — is AudioSessionState.Idle 사용 권장.

AudioSessionState.Idledata object==도 정상 동작하지만, 같은 줄에서 Recordingis로 검사하고 있어 가독성을 위해 두 분기 모두 is로 통일하는 편이 일관적입니다.

♻️ 제안 수정
-    if (audioSessionState == AudioSessionState.Idle || audioSessionState is AudioSessionState.Recording) {
+    if (audioSessionState is AudioSessionState.Idle || audioSessionState is AudioSessionState.Recording) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/component/PracticeRecordingControl.kt`
at line 91, The conditional mixes equality and type-check styles; update the
check in PracticeRecordingControl (variable audioSessionState) to use the same
'is' style for both branches—replace the left side `audioSessionState ==
AudioSessionState.Idle` with `audioSessionState is AudioSessionState.Idle` so
the if uses `is AudioSessionState.Idle || audioSessionState is
AudioSessionState.Recording` for consistent, idiomatic style.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Prezel/core/audio/src/main/java/com/team/prezel/core/audio/MediaRecordingAudioController.kt`:
- Around line 43-77: startRecording() currently calls deleteCurrentAudioFile()
before the new MediaRecorder is successfully prepared/started, which can
permanently remove a previous ReadyToPlay file on failure; change the flow so
you do not deleteCurrentAudioFile() or clear currentAudioFile/_audioSessionState
until newRecorder is fully created and started (i.e., after runCatching that
prepares/starts the recorder returns successfully), only then assign recorder =
newRecorder, set currentAudioFile = file, update _audioSessionState (Recording)
and call startRecordingTimer(); on failures inside the recorder creation block
ensure pendingRecorder?.release(), delete the temp file, and in the outer
onFailure restore the previous ReadyToPlay state and currentAudioFile instead of
wiping them, then emitEffect(AudioSessionEffect.RecordingStartFailed) as before.

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/component/PracticeRecordingContent.kt`:
- Around line 3-9: The file uses Modifier.weight(1f) but is missing the required
import; add the import androidx.compose.foundation.layout.weight to
PracticeRecordingContent.kt so the Modifier.weight call (used in the composable
where Modifier.weight(1f) appears) can compile; locate the composable/function
that references Modifier.weight(1f) and add that import alongside the other
androidx.compose.foundation.layout imports.

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/PracticeRecordingViewModel.kt`:
- Around line 41-50: The analysis coroutine launched by startAnalysis() must be
tracked and cancelled before starting a new session so stale results don’t
overwrite new state; store the returned Job (or CoroutineScope/Deferred) as a
field on PracticeRecordingViewModel (e.g., analysisJob), and before calling
audioController.startRecording(), before
resetPracticeRecording()/PracticeRecordingUiIntent.RetryRecordingClicked, and at
the start of startAnalysis(), check and cancel (and join if appropriate) any
existing analysisJob and clear it; then assign the new Job when launching the
analysis so subsequent resets/recordings can reliably cancel the previous work.

---

Nitpick comments:
In
`@Prezel/core/audio/src/main/java/com/team/prezel/core/audio/RecordingAudioController.kt`:
- Around line 55-61: AudioSessionEffect에 `@Immutable` 어노테이션이 빠져 일관성이 깨지므로 sealed
interface AudioSessionEffect 선언 위에 `@Immutable을` 추가하세요; RecordingStartFailed,
RecordingStopFailed, PlaybackStartFailed 같은 data object로만 구성되어 현재는 불변이지만 향후
페이로드가 추가될 수 있으니 AudioSessionState, AudioSource와 동일하게 `@Immutable을` 명시해 안전성을
확보하십시오.

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/body/EmptyPresentationSheet.kt`:
- Line 27: EmptyPresentationSheet.kt uses a hardcoded Spacer height
(Modifier.height(12.dp)) causing inconsistency with PresentationSheet.kt;
replace the hardcoded 12.dp with the design token PrezelTheme.spacing.V12.
Locate the Spacer invocation inside EmptyPresentationSheet.kt (the one calling
Modifier.height) and update it to use PrezelTheme.spacing.V12 so spacing is
consistent with PresentationSheet.kt and the app’s design tokens.

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/model/PresentationUiModel.kt`:
- Around line 22-26: The when-expression in PresentationUiModel that computes
dDayLabel uses the verbose range "in Int.MIN_VALUE..-1"; replace that branch
with a simple negative check (dDay < 0) to improve readability: locate the
dDayLabel property in PresentationUiModel (uses dDay) and change the second when
branch to use dDay < 0 (or convert the whole when to an if/else if you prefer)
while keeping the existing outputs ("D-Day", "D+${-dDay}", "D-$dDay") unchanged.

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/component/PracticeRecordingControl.kt`:
- Around line 117-163: The actions() function and stopAction() currently return
a List<PracticeRecordingControlAction> even though every branch only ever
produces a single PracticeRecordingControlAction; change
AudioSessionState.actions(...) to return a single PracticeRecordingControlAction
(not a List) and update stopAction(onStop: () -> Unit) to return
PracticeRecordingControlAction; adjust all callers that iterate or expect a list
(remove forEach/spacing logic and use the single PracticeRecordingControlAction
directly, e.g. replace list usages of PracticeRecordingControlAction returned
from actions() and stopAction() with the direct action and call its onClick or
read its iconResId/colorType as needed).
- Line 91: The conditional mixes equality and type-check styles; update the
check in PracticeRecordingControl (variable audioSessionState) to use the same
'is' style for both branches—replace the left side `audioSessionState ==
AudioSessionState.Idle` with `audioSessionState is AudioSessionState.Idle` so
the if uses `is AudioSessionState.Idle || audioSessionState is
AudioSessionState.Recording` for consistent, idiomatic style.

In
`@Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/result/component/PracticeRecordingResultPage.kt`:
- Around line 134-144: The function rememberOverallResult is not a Compose
memoized helper despite its name and has redundant branches; rename it to
computeOverallResult (or getOverallResult) and simplify its when-expression:
order branches by intended card priority (PERFECT, GOOD, TRY), remove the
duplicate "pronunciationScore <= 60 -> TRY" vs "else -> TRY" redundancy, and
make the conditions explicit for the GOOD/PERFECT cases (use
PracticeRecordingSpeed.ADEQUATE and speed != ADEQUATE checks as needed). Also
verify the edge cases you mentioned (70–94 with speed != ADEQUATE and 61–69 with
speed == ADEQUATE) match the desired grading policy and adjust conditions
accordingly in computeOverallResult.
- Around line 36-52: Replace the constructor parameter annotations on
PracticeAnalysisOverallResult (contentDescriptionResId and cardResId) from
`@param`:StringRes/@param:DrawableRes to a getter or property annotation so
resource type checks apply at call sites; e.g., use `@get`:StringRes or
`@get`:DrawableRes (or `@property`:) on the properties backing
PracticeAnalysisOverallResult and do the same for
PracticeRecordingControlAction.iconResId to ensure lint/resource validation
propagates to usages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 709498a6-4766-468c-8db7-015631700203

📥 Commits

Reviewing files that changed from the base of the PR and between d8e4d9c and ac2b6aa.

📒 Files selected for processing (34)
  • Prezel/core/audio/build.gradle.kts
  • Prezel/core/audio/src/main/java/com/team/prezel/core/audio/MediaRecordingAudioController.kt
  • Prezel/core/audio/src/main/java/com/team/prezel/core/audio/RecordingAudioController.kt
  • Prezel/core/data/src/main/java/com/team/prezel/core/data/di/RepositoryModule.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/HomeScreen.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/HomeViewModel.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/HomePageLayout.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/body/EmptyPresentationSheet.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/body/HomeBottomSheetContent.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/body/HomeBottomSheetTitle.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/body/PresentationSheet.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/head/HomeHeadSection.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/title/EmptyPresentationHero.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/title/HomeHeroLayout.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/title/PracticeActionCard.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/title/PresentationHero.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/contract/HomeUiEffect.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/contract/HomeUiIntent.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/contract/HomeUiState.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/model/HomeUiMessage.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/model/PresentationUiModel.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/navigation/HomeEntryBuilder.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/PracticeRecordingScreen.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/PracticeRecordingViewModel.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/RecordAudioPermission.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/component/PracticeRecordingContent.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/component/PracticeRecordingControl.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/component/PracticeRecordingTopAppBar.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/contract/PracticeRecordingUiIntent.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/contract/PracticeRecordingUiState.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/model/PracticeRecordingAnalysisUiModel.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/result/PracticeRecordingResultScreen.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/result/component/PracticeRecordingResultPage.kt
  • Prezel/settings.gradle.kts
✅ Files skipped from review due to trivial changes (10)
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/body/HomeBottomSheetContent.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/contract/HomeUiEffect.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/head/HomeHeadSection.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/title/EmptyPresentationHero.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/title/PresentationHero.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/HomePageLayout.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/body/HomeBottomSheetTitle.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/model/HomeUiMessage.kt
  • Prezel/core/audio/build.gradle.kts
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/title/PracticeActionCard.kt
🚧 Files skipped from review as they are similar to previous changes (4)
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/component/PracticeRecordingTopAppBar.kt
  • Prezel/settings.gradle.kts
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/navigation/HomeEntryBuilder.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/practice/PracticeRecordingScreen.kt

* **refactor: `Immutable` 어노테이션 패키지 변경**
    * `RecordingAudioController`에서 사용하던 `javax.annotation.concurrent.Immutable`을 Compose Runtime의 `@Immutable` 어노테이션(`androidx.compose.runtime.Immutable`)으로 변경했습니다.

* **build: `core:audio` 모듈 의존성 추가**
    * Compose Runtime 어노테이션 사용을 위해 `core:audio` 모듈의 `build.gradle.kts`에 `androidx-compose-bom` 및 `androidx-compose-runtime` 의존성을 추가했습니다.
    * `libs.versions.toml`에 `androidx-compose-runtime` 라이브러리 정의를 추가했습니다.
* **refactor: `RecordingAudioController.kt` 내 데이터 모델 파일 분리**
    * 하나의 파일에 정의되어 있던 `AudioSessionState`, `AudioSource`, `AudioSessionEffect`를 각각 별도의 파일로 추출하여 모듈 구조를 개선했습니다.
    * `AudioSessionState.kt`: `AudioSessionState` 및 `AudioSource` 인터페이스 이동
    * `AudioSessionEffect.kt`: `AudioSessionEffect` 인터페이스 이동

* **style: `MediaRecordingAudioController` 로직 가독성 개선**
    * `stopRecording` 메서드 내에서 `audioSessionState` 값을 확인하고 처리하는 `when` 식의 변수 할당 방식을 개선했습니다.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
Prezel/core/audio/src/main/java/com/team/prezel/core/audio/MediaRecordingAudioController.kt (2)

156-196: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

startPlayback()에서 완료 콜백과 player 필드 할당 순서의 레이스 조건이 있습니다.

매우 짧은 오디오는 start() 직후 OnCompletionListener가 즉시 호출될 수 있습니다. Line 174에서 start()를 호출하고 Line 181에서 player 필드에 할당하는 사이에 재생이 완료되면, Line 167의 완료 콜백이 실행될 때 releasePlayer()가 아직 할당되지 않은 이전 player 값을 참조하게 되어 리소스 누수 또는 상태 불일치가 발생할 수 있습니다.

player 필드를 start() 호출 전에 할당하고, 실패 시 롤백하도록 수정하세요.

🔧 제안 수정
 private fun startPlayback(
     source: AudioSource,
     durationSeconds: Int,
     startPositionSeconds: Int,
 ) {
     runCatching {
         releasePlayer()
 
         var pendingPlayer: MediaPlayer? = null
         val newPlayer = runCatching {
             val mediaPlayer = MediaPlayer()
             pendingPlayer = mediaPlayer
             mediaPlayer.apply {
                 setDataSource(source.filePath)
                 prepare()
                 if (startPositionSeconds > 0) {
                     seekTo(startPositionSeconds * MILLIS_PER_SECOND)
                 }
                 setOnCompletionListener {
                     releasePlayer()
                     _audioSessionState.value = AudioSessionState.ReadyToPlay(
                         source = source,
                         positionSeconds = durationSeconds,
                         durationSeconds = durationSeconds,
                     )
                 }
-                start()
             }
+            player = mediaPlayer
+            mediaPlayer.start()
+            mediaPlayer
         }.getOrElse { throwable ->
+            if (player === pendingPlayer) player = null
             pendingPlayer?.release()
             throw throwable
         }
 
-        player = newPlayer
         _audioSessionState.value = AudioSessionState.Playing(
             source = source,
             positionSeconds = startPositionSeconds,
             durationSeconds = durationSeconds.coerceAtLeast(newPlayer.duration.toSeconds()),
         )
         startPlaybackTimer()
     }.onFailure {
         releasePlayer()
         _audioSessionState.value = AudioSessionState.ReadyToPlay(
             source = source,
             durationSeconds = durationSeconds,
         )
         emitEffect(AudioSessionEffect.PlaybackStartFailed)
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Prezel/core/audio/src/main/java/com/team/prezel/core/audio/MediaRecordingAudioController.kt`
around lines 156 - 196, The startPlayback() method has a race between starting
the MediaPlayer and assigning the controller's player field; assign the
controller field player (the instance referenced by pendingPlayer/newPlayer)
before calling start() so the OnCompletionListener and releasePlayer() always
see the current player, and if any subsequent step in the runCatching block
fails (e.g., prepare()/seekTo()/start()), roll back by releasing the assigned
player and setting player = null; update the code around
MediaRecordingAudioController.startPlayback(), the pendingPlayer/newPlayer
creation block, and the releasePlayer() usage to perform the pre-start
assignment and proper cleanup on errors.

43-78: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

재녹음 시작 실패 시 이전 녹음본이 유실됩니다.

startRecording()이 새 recorder가 실제로 시작되기 전에 deleteCurrentAudioFile()을 호출합니다(Line 47). ReadyToPlay 상태에서 재녹음을 시도했다가 prepare()/start()가 실패하면, 이전의 성공한 녹음 파일은 이미 삭제되었지만 상태는 Idle로 떨어져 재생/분석을 모두 할 수 없게 됩니다.

기존 파일 삭제는 새 녹음이 성공한 이후로 미루고, 실패 시에는 이전 ReadyToPlay 상태를 복원하는 것이 안전합니다.

🔧 수정 방향 예시
 override fun startRecording() {
+    stopPlayback()
+    val previousReadyState = audioSessionState.value as? AudioSessionState.ReadyToPlay
+    val previousAudioFile = currentAudioFile
+
     runCatching {
-        stopPlayback()
         releaseRecorder()
-        deleteCurrentAudioFile()
 
         val file = File.createTempFile("recording_", ".m4a", context.cacheDir)
         var pendingRecorder: MediaRecorder? = null
         val newRecorder = runCatching {
             val recorder = createMediaRecorder(context = context)
             pendingRecorder = recorder
             recorder.apply {
                 setAudioSource(MediaRecorder.AudioSource.MIC)
                 setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
                 setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
                 setOutputFile(file.absolutePath)
                 prepare()
                 start()
             }
         }.getOrElse { throwable ->
             pendingRecorder?.release()
             file.delete()
             throw throwable
         }
 
         recorder = newRecorder
+        previousAudioFile
+            ?.takeUnless { it.absolutePath == file.absolutePath }
+            ?.delete()
         currentAudioFile = file
         _audioSessionState.value = AudioSessionState.Recording(elapsedSeconds = 0)
         startRecordingTimer()
     }.onFailure {
         releaseRecorder()
-        deleteCurrentAudioFile()
-        _audioSessionState.value = AudioSessionState.Idle
+        currentAudioFile = previousAudioFile
+        _audioSessionState.value = previousReadyState ?: AudioSessionState.Idle
         emitEffect(AudioSessionEffect.RecordingStartFailed)
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Prezel/core/audio/src/main/java/com/team/prezel/core/audio/MediaRecordingAudioController.kt`
around lines 43 - 78, startRecording() currently deletes the existing recording
(deleteCurrentAudioFile()) before the new MediaRecorder is confirmed started,
causing loss if prepare()/start() fails; change the flow so you do NOT call
deleteCurrentAudioFile() up-front — create the temp file and attempt to
create/prepare/start the new recorder (use the existing
pendingRecorder/newRecorder logic around createMediaRecorder and prepare/start),
and only after newRecorder is successfully started assign recorder =
newRecorder, set currentAudioFile = file and then delete the old file; on any
failure ensure pendingRecorder is released, the temp file is deleted, and
restore the previous currentAudioFile and _audioSessionState (e.g. ReadyToPlay)
instead of dropping to Idle; keep releaseRecorder() usage to clear any
partially-initialized recorder.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Prezel/core/audio/build.gradle.kts`:
- Around line 11-12: The Compose runtime dependency is declared as
implementation so consumers cannot see Compose annotations used by your public
API (AudioSessionState, AudioSource); change the dependency declaration for
libs.androidx.compose.runtime from implementation(...) to api(...) so the
`@Immutable` annotation and stability metadata are exposed to downstream modules
and Compose stability inference works correctly.

---

Duplicate comments:
In
`@Prezel/core/audio/src/main/java/com/team/prezel/core/audio/MediaRecordingAudioController.kt`:
- Around line 156-196: The startPlayback() method has a race between starting
the MediaPlayer and assigning the controller's player field; assign the
controller field player (the instance referenced by pendingPlayer/newPlayer)
before calling start() so the OnCompletionListener and releasePlayer() always
see the current player, and if any subsequent step in the runCatching block
fails (e.g., prepare()/seekTo()/start()), roll back by releasing the assigned
player and setting player = null; update the code around
MediaRecordingAudioController.startPlayback(), the pendingPlayer/newPlayer
creation block, and the releasePlayer() usage to perform the pre-start
assignment and proper cleanup on errors.
- Around line 43-78: startRecording() currently deletes the existing recording
(deleteCurrentAudioFile()) before the new MediaRecorder is confirmed started,
causing loss if prepare()/start() fails; change the flow so you do NOT call
deleteCurrentAudioFile() up-front — create the temp file and attempt to
create/prepare/start the new recorder (use the existing
pendingRecorder/newRecorder logic around createMediaRecorder and prepare/start),
and only after newRecorder is successfully started assign recorder =
newRecorder, set currentAudioFile = file and then delete the old file; on any
failure ensure pendingRecorder is released, the temp file is deleted, and
restore the previous currentAudioFile and _audioSessionState (e.g. ReadyToPlay)
instead of dropping to Idle; keep releaseRecorder() usage to clear any
partially-initialized recorder.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 90ba0e75-c781-4533-a303-b80800fb249c

📥 Commits

Reviewing files that changed from the base of the PR and between ac2b6aa and f2a0fa2.

📒 Files selected for processing (7)
  • Prezel/core/audio/build.gradle.kts
  • Prezel/core/audio/src/main/java/com/team/prezel/core/audio/AudioSessionEffect.kt
  • Prezel/core/audio/src/main/java/com/team/prezel/core/audio/AudioSessionState.kt
  • Prezel/core/audio/src/main/java/com/team/prezel/core/audio/MediaRecordingAudioController.kt
  • Prezel/core/audio/src/main/java/com/team/prezel/core/audio/RecordingAudioController.kt
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/title/PresentationHero.kt
  • Prezel/gradle/libs.versions.toml
✅ Files skipped from review due to trivial changes (2)
  • Prezel/gradle/libs.versions.toml
  • Prezel/core/audio/src/main/java/com/team/prezel/core/audio/AudioSessionState.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/title/PresentationHero.kt

Comment on lines +11 to +12
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.runtime)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: audio 모듈의 public API 클래스 사용 패턴 확인

# RecordingAudioController와 MediaRecordingAudioController가 `@Immutable을` 사용하는지 확인
echo "=== Checking `@Immutable` usage in core:audio ==="
fd -e kt . Prezel/core/audio/src/main/java -x rg -l "@Immutable"

# 다른 모듈에서 audio 모듈의 클래스를 import하는 위치 찾기
echo -e "\n=== Finding usages of audio module classes in other modules ==="
rg -n --type kotlin "import com\.team\.prezel\.core\.audio\." \
  -g '!Prezel/core/audio/**' \
  -C 2

Repository: Team-Prezel/Prezel-Android

Length of output: 6018


🏁 Script executed:

# Find all Kotlin files in audio module and search for `@Immutable`
fd -e kt . Prezel/core/audio/src -x cat -n {} | grep -i "@immutable" -B 2 -A 2

Repository: Team-Prezel/Prezel-Android

Length of output: 345


🏁 Script executed:

# Check if RecordingAudioController uses `@Immutable`
fd -e kt . Prezel/core/audio/src -x rg -l "class RecordingAudioController|class MediaRecordingAudioController"

Repository: Team-Prezel/Prezel-Android

Length of output: 162


🏁 Script executed:

# Look at current build.gradle.kts to see actual dependency declarations
cat Prezel/core/audio/build.gradle.kts

Repository: Team-Prezel/Prezel-Android

Length of output: 396


🏁 Script executed:

# Read RecordingAudioController to check for `@Immutable` annotation
cat -n Prezel/core/audio/src/main/java/com/team/prezel/core/audio/RecordingAudioController.kt | head -30

Repository: Team-Prezel/Prezel-Android

Length of output: 623


🏁 Script executed:

# Read MediaRecordingAudioController to check for `@Immutable` annotation
cat -n Prezel/core/audio/src/main/java/com/team/prezel/core/audio/MediaRecordingAudioController.kt | head -30

Repository: Team-Prezel/Prezel-Android

Length of output: 1354


🏁 Script executed:

# Check what files are in the audio module to see all public exports
fd -e kt . Prezel/core/audio/src/main/java -type f

Repository: Team-Prezel/Prezel-Android

Length of output: 241


Compose Runtime 의존성을 api로 노출하는 것을 권장합니다.

이 모듈의 public API 클래스인 AudioSessionStateAudioSource가 Compose의 @Immutable 어노테이션을 사용하므로, 소비 모듈(feature/home/impl의 Compose 화면들)에서 Compose stability inference가 올바르게 작동하려면 어노테이션이 노출되어야 합니다. 현재 implementation으로 선언되어 있으면 소비 모듈에서 어노테이션 메타데이터를 볼 수 없어 불필요한 recomposition이 발생할 수 있습니다.

✨ 권장 수정사항
 dependencies {
     implementation(platform(libs.androidx.compose.bom))
-    implementation(libs.androidx.compose.runtime)
+    api(libs.androidx.compose.runtime)
     implementation(libs.kotlinx.coroutines.core)
 }
📝 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.

Suggested change
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.runtime)
implementation(platform(libs.androidx.compose.bom))
api(libs.androidx.compose.runtime)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Prezel/core/audio/build.gradle.kts` around lines 11 - 12, The Compose runtime
dependency is declared as implementation so consumers cannot see Compose
annotations used by your public API (AudioSessionState, AudioSource); change the
dependency declaration for libs.androidx.compose.runtime from
implementation(...) to api(...) so the `@Immutable` annotation and stability
metadata are exposed to downstream modules and Compose stability inference works
correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 새로운 기능 추가 또는 기존 기능 확장

Projects

None yet

Development

Successfully merging this pull request may close these issues.

연습 녹음 화면 구현

1 participant