Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c35256c
feat: 연습 녹음 기능 구현 및 홈 화면 연동
HamBeomJoon Apr 30, 2026
ce7bbbd
feat: 연습 녹음 분석 결과 및 상태(Loading/Error/Success) 화면 구현
HamBeomJoon Apr 30, 2026
0c901bb
feat: 연습 녹음 기능의 예외 처리 강화 및 상태 관리 리팩터링
HamBeomJoon May 1, 2026
eb8b122
refactor: 연습 녹음 기능 구조 개선 및 코드 리팩터링
HamBeomJoon May 1, 2026
24c2b47
refactor: 연습 녹음 분석 결과 화면 패키지 구조 개선 및 컴포넌트 정리
HamBeomJoon May 1, 2026
11f3854
feat: 연습 녹음 분석 기능 구현 및 관련 도메인/데이터 레이어 추가
HamBeomJoon May 1, 2026
9f90412
feat: 연습 녹음 화면 권한 처리 로직 개선 및 오류 처리 강화
HamBeomJoon May 2, 2026
cf07025
feat: 연습(Practice) 관련 UseCase KDoc 주석 추가
HamBeomJoon May 2, 2026
52331c6
refactor: 녹음 권한 요청 로직 간소화 및 오디오 컨트롤러 상태 초기화 로직 개선
HamBeomJoon May 2, 2026
97b3d74
feat: 공통 권한 요청 유틸리티 구현 및 오디오 녹음 로직 개선
HamBeomJoon May 3, 2026
3fc3663
feat: 연습 녹음 분석 기능 구현 및 오디오 로직 리팩터링
HamBeomJoon May 4, 2026
d8e4d9c
refactor: 오디오 녹음 로직 core 모듈 분리 및 권한 요청 로직 개선
HamBeomJoon May 4, 2026
aa8c634
feat: 음성 녹음 및 외부 오디오 파일 선택 기능 고도화
HamBeomJoon May 6, 2026
582aed0
Merge remote-tracking branch 'origin/develop' into feat/#101-practice…
HamBeomJoon May 8, 2026
7b06176
refactor: 홈 화면 코드 구조 개선 및 녹음 로직 간소화
HamBeomJoon May 8, 2026
ac2b6aa
refactor: 연습 녹음(Practice Recording) 상태 관리 및 결과 화면 로직 개선
HamBeomJoon May 8, 2026
92fc811
refactor: `RecordingAudioController` 내 `Immutable` 어노테이션 교체 및 의존성 정리
HamBeomJoon May 10, 2026
1ce3b6e
Merge remote-tracking branch 'origin/develop' into feat/#101-practice…
HamBeomJoon May 10, 2026
f2a0fa2
refactor: 오디오 관련 도메인 모델 및 상태 인터페이스 분리
HamBeomJoon May 10, 2026
a83f59a
refactor: 연습 결과 페이지의 칩(Chip) 컴포넌트 사용 로직 간소화 및 코드 포맷팅 개선
HamBeomJoon May 11, 2026
853d1d2
feat: 연습 녹음 분석 기능 구현 및 관련 API 연동
HamBeomJoon May 13, 2026
cb1713d
feat: 연습 녹음 분석 결과에 종합 평가(Overall Evaluation) 추가 및 도메인 모델 통합
HamBeomJoon May 13, 2026
18ac526
Merge remote-tracking branch 'origin/develop' into feat/#101-practice…
HamBeomJoon May 13, 2026
167fc1d
refactor: 연습(Practice) 기능을 별도 모듈로 분리 및 분석 로직 개선
HamBeomJoon May 13, 2026
1ca917a
refactor: `PracticeRecordingUiState` 구조 개선 및 오디오 재생 로직 리팩터링
HamBeomJoon May 13, 2026
594c28a
refactor: `MediaRecordingAudioController` 리팩터링 및 오디오 세션 로직 분리
HamBeomJoon May 13, 2026
ac9949d
refactor: PracticeRecordingUiIntent 정리 및 스크립트 로드 로직 개선
HamBeomJoon May 14, 2026
ee8b7aa
build: app 모듈 내 불필요한 feature API 의존성 제거
moondev03 May 14, 2026
6e176e0
feat: 연습 녹음 분석 기능 분리 및 아키텍처 개선
HamBeomJoon May 14, 2026
bd8b829
feat: Audio 관련 세션 인터페이스 도입 및 의존성 주입 구조 개선
HamBeomJoon May 14, 2026
67f3082
Merge remote-tracking branch 'origin/feat/#101-practice-record' into …
HamBeomJoon May 14, 2026
76105e2
build: practice feature implementation 모듈에 serialization 플러그인 추가
moondev03 May 16, 2026
318eb99
refactor: 연습 기록 및 분석 기능 구조 개선 및 패키지 정리
moondev03 May 16, 2026
117d3b7
refactor: 홈 화면 연습 녹음 네비게이션 경로 수정
moondev03 May 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Prezel/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ dependencies {
implementation(projects.featureSplashImpl)
implementation(projects.featureLoginApi)
implementation(projects.featureLoginImpl)
implementation(projects.featureTermsApi)
implementation(projects.featureTermsImpl)
implementation(projects.featureHomeApi)
implementation(projects.featureHomeImpl)
implementation(projects.featureHistoryApi)
implementation(projects.featureHistoryImpl)
implementation(projects.featureMyApi)
implementation(projects.featureMyImpl)
implementation(projects.featureSettingApi)

implementation(projects.featureTermsImpl)
implementation(projects.featurePracticeImpl)
implementation(projects.featureSettingImpl)
implementation(projects.featureProfileImpl)

Expand Down
1 change: 1 addition & 0 deletions Prezel/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

<application
android:name=".PrezelApplication"
Expand Down
14 changes: 14 additions & 0 deletions Prezel/core/audio/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
plugins {
alias(libs.plugins.prezel.android.library)
alias(libs.plugins.prezel.hilt)
}

android {
namespace = "com.team.prezel.core.audio"
}

dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.runtime)
Comment thread
HamBeomJoon marked this conversation as resolved.
implementation(libs.kotlinx.coroutines.core)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.team.prezel.core.audio

internal interface AudioPlayerSession {
fun start(
source: AudioSource,
startPositionSeconds: Int,
onCompleted: () -> Unit,
): Result<Int>

fun currentPositionSeconds(): Int

fun release()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.team.prezel.core.audio

internal interface AudioRecorderSession {
fun start(): Result<Unit>

fun stop(elapsedSeconds: Int): Result<RecordedAudio>

fun reset()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.team.prezel.core.audio

sealed interface AudioSessionEffect {
data object RecordingStartFailed : AudioSessionEffect

data object RecordingStopFailed : AudioSessionEffect

data object PlaybackStartFailed : AudioSessionEffect
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.team.prezel.core.audio

import androidx.compose.runtime.Immutable

@Immutable
sealed interface AudioSessionState {
data object Idle : AudioSessionState

data class Recording(
val elapsedSeconds: Int,
) : AudioSessionState

data class ReadyToPlay(
val source: AudioSource,
val positionSeconds: Int = 0,
val durationSeconds: Int,
) : AudioSessionState

data class Playing(
val source: AudioSource,
val positionSeconds: Int,
val durationSeconds: Int,
) : AudioSessionState
}

@Immutable
sealed interface AudioSource {
val filePath: String

data class RecordedFile(
override val filePath: String,
) : AudioSource
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.team.prezel.core.audio

import android.media.MediaPlayer
import javax.inject.Inject

internal class MediaPlayerSession @Inject constructor() : AudioPlayerSession {
private var player: MediaPlayer? = null

override fun start(
source: AudioSource,
startPositionSeconds: Int,
onCompleted: () -> Unit,
): Result<Int> =
runCatching {
release()

var pendingPlayer: MediaPlayer? = null
val newPlayer = runCatching {
MediaPlayer().also { pendingPlayer = it }.apply {
setDataSource(source.filePath)
prepare()
seekToStartPosition(startPositionSeconds)
setOnCompletionListener { onCompleted() }
start()
}
}.getOrElse { throwable ->
pendingPlayer?.release()
throw throwable
}

player = newPlayer
newPlayer.duration
}.onFailure {
release()
}

override fun currentPositionSeconds(): Int = player?.currentPosition?.toSeconds() ?: 0

override fun release() {
player?.runCatching { stop() }
player?.release()
player = null
}

private fun MediaPlayer.seekToStartPosition(startPositionSeconds: Int) {
if (startPositionSeconds > 0) {
seekTo(startPositionSeconds * MILLIS_PER_SECOND)
}
}

private companion object {
const val MILLIS_PER_SECOND = 1_000
}
}

internal fun Int.toSeconds(): Int = this / 1_000
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.team.prezel.core.audio

import android.content.Context
import android.media.MediaRecorder
import android.os.Build
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import javax.inject.Inject
import kotlin.math.max

internal class MediaRecorderSession @Inject constructor(
@param:ApplicationContext private val context: Context,
) : AudioRecorderSession {
private var recorder: MediaRecorder? = null
private var currentAudioFile: File? = null

override fun start(): Result<Unit> =
runCatching {
reset()

val file = File.createTempFile("recording_", ".m4a", context.cacheDir)
var pendingRecorder: MediaRecorder? = null
val newRecorder = runCatching {
createMediaRecorder(context = context).also { pendingRecorder = it }.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
currentAudioFile = file
}.onFailure {
reset()
}

override fun stop(elapsedSeconds: Int): Result<RecordedAudio> =
runCatching {
val file = currentAudioFile!!
recorder!!.stop()
RecordedAudio(
source = AudioSource.RecordedFile(filePath = file.absolutePath),
durationSeconds = max(elapsedSeconds, 0),
)
}.onSuccess {
releaseRecorder()
}.onFailure {
reset()
}

override fun reset() {
releaseRecorder()
currentAudioFile?.delete()
currentAudioFile = null
}

private fun releaseRecorder() {
recorder?.release()
recorder = null
}
}

internal data class RecordedAudio(
val source: AudioSource.RecordedFile,
val durationSeconds: Int,
)

@Suppress("DEPRECATION")
private fun createMediaRecorder(context: Context): MediaRecorder =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(context)
} else {
MediaRecorder()
}
Loading