From bd80be2201ea949a1acea2c68f636f63a307959c Mon Sep 17 00:00:00 2001 From: gagip Date: Thu, 26 Mar 2026 12:56:08 +0900 Subject: [PATCH 1/5] =?UTF-8?q?refactor:=20PlaylistDetailViewModel=20onAct?= =?UTF-8?q?ion()=20=EB=8B=A8=EC=9D=BC=20=EC=A7=84=EC=9E=85=EC=A0=90=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit selectPlaylist/removeTrackFromPlaylist/playPlaylist를 private으로 변경하고 onAction()을 통해서만 접근하도록 통일. PlaylistDetailAction에 SelectPlaylist 액션 추가, NavGraph와 테스트 코드도 onAction() 방식으로 업데이트. --- .../zenplayer/core/navigation/NavGraph.kt | 5 ++- .../playlist/ui/PlaylistDetailAction.kt | 2 ++ .../playlist/ui/PlaylistDetailViewModel.kt | 15 ++++++-- .../ui/PlaylistDetailViewModelTest.kt | 34 +++++++++---------- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/happyseal/zenplayer/core/navigation/NavGraph.kt b/app/src/main/java/com/happyseal/zenplayer/core/navigation/NavGraph.kt index 9c33f7e8..d77390d9 100644 --- a/app/src/main/java/com/happyseal/zenplayer/core/navigation/NavGraph.kt +++ b/app/src/main/java/com/happyseal/zenplayer/core/navigation/NavGraph.kt @@ -75,7 +75,7 @@ fun NavGraph( val miniPlayerViewModel = hiltViewModel() LaunchedEffect(playlistId) { - viewModel.selectPlaylist(playlistId) + viewModel.onAction(PlaylistDetailAction.SelectPlaylist(playlistId)) } val uiState by viewModel.detailUiState.collectAsStateWithLifecycle() @@ -89,9 +89,8 @@ fun NavGraph( uiEvent = viewModel.uiEvent, onAction = { action -> when (action) { - is PlaylistDetailAction.PlayPlaylist -> viewModel.playPlaylist(action.playlistId, action.startIndex) - is PlaylistDetailAction.RemoveTrack -> viewModel.removeTrackFromPlaylist(action.trackRowId) is PlaylistDetailAction.MiniPlayer -> miniPlayerViewModel.onAction(action.action) + else -> viewModel.onAction(action) } }, onNavigateBack = { navController.navigateUp() }, diff --git a/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailAction.kt b/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailAction.kt index a566e59c..a695a591 100644 --- a/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailAction.kt +++ b/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailAction.kt @@ -3,6 +3,8 @@ package com.happyseal.zenplayer.features.playlist.ui import com.happyseal.zenplayer.core.model.MiniPlayerAction sealed interface PlaylistDetailAction { + data class SelectPlaylist(val playlistId: Long) : PlaylistDetailAction + data class PlayPlaylist(val playlistId: Long, val startIndex: Int = 0) : PlaylistDetailAction data class RemoveTrack(val trackRowId: Long) : PlaylistDetailAction diff --git a/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailViewModel.kt b/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailViewModel.kt index 486a1723..5b8d8055 100644 --- a/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailViewModel.kt +++ b/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailViewModel.kt @@ -33,7 +33,16 @@ constructor( private var trackCollectionJob: Job? = null private val binder = serviceBinder.binderFlow - fun selectPlaylist(playlistId: Long) { + fun onAction(action: PlaylistDetailAction) { + when (action) { + is PlaylistDetailAction.SelectPlaylist -> selectPlaylist(action.playlistId) + is PlaylistDetailAction.PlayPlaylist -> playPlaylist(action.playlistId, action.startIndex) + is PlaylistDetailAction.RemoveTrack -> removeTrackFromPlaylist(action.trackRowId) + is PlaylistDetailAction.MiniPlayer -> { /* MiniPlayer 액션은 MiniPlayerViewModel에서 처리 */ } + } + } + + private fun selectPlaylist(playlistId: Long) { require(playlistId > 0) { "유효하지 않은 playlistId: $playlistId" } trackCollectionJob?.cancel() _detailUiState.value = PlaylistDetailUiState.Loading @@ -58,7 +67,7 @@ constructor( } } - fun removeTrackFromPlaylist(trackRowId: Long) { + private fun removeTrackFromPlaylist(trackRowId: Long) { require(trackRowId > 0) { "유효하지 않은 trackRowId: $trackRowId" } viewModelScope.launch { playlistRepository @@ -70,7 +79,7 @@ constructor( } } - fun playPlaylist( + private fun playPlaylist( playlistId: Long, startIndex: Int = 0, ) { diff --git a/app/src/test/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailViewModelTest.kt b/app/src/test/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailViewModelTest.kt index 822dc3b0..3fc5c19d 100644 --- a/app/src/test/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailViewModelTest.kt +++ b/app/src/test/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailViewModelTest.kt @@ -48,8 +48,8 @@ class PlaylistDetailViewModelTest : BehaviorSpec({ repo.addTrackToPlaylist(playlistId, makeTrack(2)) val viewModel = makeViewModel(repo) - When("selectPlaylist(playlistId)를 호출하면") { - viewModel.selectPlaylist(playlistId) + When("onAction(SelectPlaylist(playlistId))를 호출하면") { + viewModel.onAction(PlaylistDetailAction.SelectPlaylist(playlistId)) testScheduler.advanceUntilIdle() Then("detailUiState가 Success로 전환되고 플레이리스트 이름과 트랙 목록이 포함된다") { @@ -64,8 +64,8 @@ class PlaylistDetailViewModelTest : BehaviorSpec({ Dispatchers.setMain(testDispatcher) val viewModel = makeViewModel() - When("selectPlaylist(999L)를 호출하면") { - viewModel.selectPlaylist(999L) + When("onAction(SelectPlaylist(999L))를 호출하면") { + viewModel.onAction(PlaylistDetailAction.SelectPlaylist(999L)) testScheduler.advanceUntilIdle() Then("detailUiState가 Error로 전환된다") { @@ -82,11 +82,11 @@ class PlaylistDetailViewModelTest : BehaviorSpec({ repo.addTrackToPlaylist(playlistId, makeTrack(2)) val binder = FakeZenPlayerBinder() val viewModel = makeViewModel(repo = repo, binder = binder) - viewModel.selectPlaylist(playlistId) + viewModel.onAction(PlaylistDetailAction.SelectPlaylist(playlistId)) testScheduler.advanceUntilIdle() - When("playPlaylist()를 호출하면") { - viewModel.playPlaylist(playlistId) + When("onAction(PlayPlaylist(playlistId))를 호출하면") { + viewModel.onAction(PlaylistDetailAction.PlayPlaylist(playlistId)) testScheduler.advanceUntilIdle() Then("binder의 setPlayQueueAndPlay가 트랙 목록과 함께 호출된다") { @@ -102,10 +102,10 @@ class PlaylistDetailViewModelTest : BehaviorSpec({ val repo = FakePlaylistRepository(throwOnRemoveTrack = RuntimeException("DB 오류")) val viewModel = makeViewModel(repo) - When("removeTrackFromPlaylist()를 호출하면") { + When("onAction(RemoveTrack(1L))를 호출하면") { Then("uiEvent로 RemoveTrackFailed가 발행된다") { viewModel.uiEvent.test { - viewModel.removeTrackFromPlaylist(1L) + viewModel.onAction(PlaylistDetailAction.RemoveTrack(1L)) testScheduler.advanceUntilIdle() val event = awaitItem() @@ -120,29 +120,29 @@ class PlaylistDetailViewModelTest : BehaviorSpec({ Dispatchers.setMain(testDispatcher) val viewModel = makeViewModel() - When("selectPlaylist(0)을 호출하면") { - val exception = runCatching { viewModel.selectPlaylist(0L) }.exceptionOrNull() + When("onAction(SelectPlaylist(0))을 호출하면") { + val exception = runCatching { viewModel.onAction(PlaylistDetailAction.SelectPlaylist(0L)) }.exceptionOrNull() Then("IllegalArgumentException이 발생한다") { exception shouldBe instanceOf() } } - When("removeTrackFromPlaylist(0)을 호출하면") { - val exception = runCatching { viewModel.removeTrackFromPlaylist(0L) }.exceptionOrNull() + When("onAction(RemoveTrack(0))을 호출하면") { + val exception = runCatching { viewModel.onAction(PlaylistDetailAction.RemoveTrack(0L)) }.exceptionOrNull() Then("IllegalArgumentException이 발생한다") { exception shouldBe instanceOf() } } - When("playPlaylist(0, 0)을 호출하면") { - val exception = runCatching { viewModel.playPlaylist(0L, 0) }.exceptionOrNull() + When("onAction(PlayPlaylist(0, 0))을 호출하면") { + val exception = runCatching { viewModel.onAction(PlaylistDetailAction.PlayPlaylist(0L, 0)) }.exceptionOrNull() Then("IllegalArgumentException이 발생한다") { exception shouldBe instanceOf() } } - When("playPlaylist(1, -1)을 호출하면") { - val exception = runCatching { viewModel.playPlaylist(1L, -1) }.exceptionOrNull() + When("onAction(PlayPlaylist(1, -1))을 호출하면") { + val exception = runCatching { viewModel.onAction(PlaylistDetailAction.PlayPlaylist(1L, -1)) }.exceptionOrNull() Then("IllegalArgumentException이 발생한다") { exception shouldBe instanceOf() } From 670642ac86c72e20a8c0f66502e01205e65f69aa Mon Sep 17 00:00:00 2001 From: gagip Date: Thu, 26 Mar 2026 12:56:58 +0900 Subject: [PATCH 2/5] =?UTF-8?q?docs:=20stateIn(WhileSubscribed5s)=20?= =?UTF-8?q?=EB=AF=B8=EC=82=AC=EC=9A=A9=20=EC=9D=98=EB=8F=84=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PlaylistDetailViewModel과 AudioPermissionHandler는 외부 Flow 구독 없이 직접 값을 push하거나 ViewModel 생명주기 내내 상태를 유지해야 하므로 MutableStateFlow.asStateFlow()가 적합함을 주석으로 명시. --- .../zenplayer/features/music/ui/AudioPermissionHandler.kt | 2 ++ .../zenplayer/features/playlist/ui/PlaylistDetailViewModel.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/src/main/java/com/happyseal/zenplayer/features/music/ui/AudioPermissionHandler.kt b/app/src/main/java/com/happyseal/zenplayer/features/music/ui/AudioPermissionHandler.kt index 1cb86f5e..ab50ac3c 100644 --- a/app/src/main/java/com/happyseal/zenplayer/features/music/ui/AudioPermissionHandler.kt +++ b/app/src/main/java/com/happyseal/zenplayer/features/music/ui/AudioPermissionHandler.kt @@ -11,6 +11,8 @@ import javax.inject.Inject class AudioPermissionHandler @Inject constructor() { + // WhileSubscribed5s 미사용: 권한 상태는 ViewModel 생명주기 동안 유지되어야 하므로 + // stateIn() 대신 MutableStateFlow.asStateFlow()로 항상 활성 상태를 유지 private val _status = MutableStateFlow(AudioPermissionStatus.UNDETERMINED) val status: StateFlow = _status.asStateFlow() diff --git a/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailViewModel.kt b/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailViewModel.kt index 5b8d8055..09ccbf34 100644 --- a/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailViewModel.kt +++ b/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailViewModel.kt @@ -23,6 +23,8 @@ constructor( private val playlistRepository: PlaylistRepository, serviceBinder: ZenPlayerServiceBinder, ) : ViewModel() { + // WhileSubscribed5s 미사용: 외부 Flow를 구독하지 않고 viewModelScope에서 + // 직접 값을 push하는 구조이므로 MutableStateFlow.asStateFlow()가 적합 private val _detailUiState = MutableStateFlow(PlaylistDetailUiState.Loading) val detailUiState: StateFlow = _detailUiState.asStateFlow() From 095df6b5f76e140faa270f4a9e217091498345e9 Mon Sep 17 00:00:00 2001 From: gagip Date: Thu, 26 Mar 2026 12:59:46 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20MiniPlayerUiState/progress=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ZenPlayerServiceBinder 확장 함수(miniPlayerUiState, miniPlayerProgress)로 공통 로직을 core/service에 추출. MiniPlayerViewModel과 ZenMusicSelectViewModel의 중복 구현을 제거하고 공통 함수를 재사용. --- .../ZenPlayerServiceBinderExtensions.kt | 47 +++++++++++++++++++ .../zen/miniplayer/MiniPlayerViewModel.kt | 43 ++--------------- .../musicselect/ZenMusicSelectViewModel.kt | 32 ++----------- 3 files changed, 56 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/com/happyseal/zenplayer/core/service/ZenPlayerServiceBinderExtensions.kt diff --git a/app/src/main/java/com/happyseal/zenplayer/core/service/ZenPlayerServiceBinderExtensions.kt b/app/src/main/java/com/happyseal/zenplayer/core/service/ZenPlayerServiceBinderExtensions.kt new file mode 100644 index 00000000..69fb3b3a --- /dev/null +++ b/app/src/main/java/com/happyseal/zenplayer/core/service/ZenPlayerServiceBinderExtensions.kt @@ -0,0 +1,47 @@ +package com.happyseal.zenplayer.core.service + +import com.happyseal.zenplayer.core.ui.component.MiniPlayerUiState +import com.happyseal.zenplayer.core.util.WhileSubscribed5s +import com.happyseal.zenplayer.core.util.whenNonNull +import com.happyseal.zenplayer.features.player.domain.PlaybackState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn + +/** + * binderFlow로부터 MiniPlayerUiState를 생성하는 공통 확장 함수. + * MiniPlayerViewModel과 ZenMusicSelectViewModel 등 MiniPlayer 상태가 필요한 + * 모든 ViewModel에서 재사용한다. + */ +fun ZenPlayerServiceBinder.miniPlayerUiState(scope: CoroutineScope): StateFlow = binderFlow + .whenNonNull(MiniPlayerUiState()) { binder -> + combine( + binder.playQueue, + binder.playbackState, + ) { playQueue, playbackState -> + MiniPlayerUiState( + currentTrackTitle = playQueue?.currentTrack?.title, + currentTrackArtist = playQueue?.currentTrack?.artist, + albumArtUri = playQueue?.currentTrack?.albumArtUri, + trackId = playQueue?.currentTrack?.id ?: 0L, + isPlaying = playbackState == PlaybackState.PLAYING, + ) + } + }.stateIn(scope, SharingStarted.WhileSubscribed5s, MiniPlayerUiState()) + +/** + * binderFlow로부터 재생 진행률(0f~1f)을 생성하는 공통 확장 함수. + * MiniPlayerViewModel과 ZenMusicSelectViewModel 등 progress가 필요한 + * 모든 ViewModel에서 재사용한다. + */ +fun ZenPlayerServiceBinder.miniPlayerProgress(scope: CoroutineScope): StateFlow = binderFlow + .whenNonNull(0f) { binder -> + combine( + binder.currentPosition, + binder.duration, + ) { position, duration -> + if (duration > 0) (position.toFloat() / duration).coerceIn(0f, 1f) else 0f + } + }.stateIn(scope, SharingStarted.WhileSubscribed5s, 0f) diff --git a/app/src/main/java/com/happyseal/zenplayer/zen/miniplayer/MiniPlayerViewModel.kt b/app/src/main/java/com/happyseal/zenplayer/zen/miniplayer/MiniPlayerViewModel.kt index ce854458..cc9832ab 100644 --- a/app/src/main/java/com/happyseal/zenplayer/zen/miniplayer/MiniPlayerViewModel.kt +++ b/app/src/main/java/com/happyseal/zenplayer/zen/miniplayer/MiniPlayerViewModel.kt @@ -4,15 +4,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.happyseal.zenplayer.core.model.MiniPlayerAction import com.happyseal.zenplayer.core.service.ZenPlayerServiceBinder +import com.happyseal.zenplayer.core.service.miniPlayerProgress +import com.happyseal.zenplayer.core.service.miniPlayerUiState import com.happyseal.zenplayer.core.ui.component.MiniPlayerUiState -import com.happyseal.zenplayer.core.util.WhileSubscribed5s -import com.happyseal.zenplayer.core.util.whenNonNull import com.happyseal.zenplayer.features.player.domain.PlaybackState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @HiltViewModel @@ -23,41 +20,9 @@ constructor( ) : ViewModel() { private val _binder = serviceBinder.binderFlow - val uiState: StateFlow = - _binder - .whenNonNull(MiniPlayerUiState()) { binder -> - combine( - binder.playQueue, - binder.playbackState, - ) { playQueue, playbackState -> - MiniPlayerUiState( - currentTrackTitle = playQueue?.currentTrack?.title, - currentTrackArtist = playQueue?.currentTrack?.artist, - albumArtUri = playQueue?.currentTrack?.albumArtUri, - trackId = playQueue?.currentTrack?.id ?: 0L, - isPlaying = playbackState == PlaybackState.PLAYING, - ) - } - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed5s, - initialValue = MiniPlayerUiState(), - ) + val uiState: StateFlow = serviceBinder.miniPlayerUiState(viewModelScope) - val progress: StateFlow = - _binder - .whenNonNull(0f) { binder -> - combine( - binder.currentPosition, - binder.duration, - ) { position, duration -> - if (duration > 0) (position.toFloat() / duration).coerceIn(0f, 1f) else 0f - } - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed5s, - initialValue = 0f, - ) + val progress: StateFlow = serviceBinder.miniPlayerProgress(viewModelScope) fun onAction(action: MiniPlayerAction) { val binder = _binder.value ?: return diff --git a/app/src/main/java/com/happyseal/zenplayer/zen/musicselect/ZenMusicSelectViewModel.kt b/app/src/main/java/com/happyseal/zenplayer/zen/musicselect/ZenMusicSelectViewModel.kt index 2c7cacca..2c6242e3 100644 --- a/app/src/main/java/com/happyseal/zenplayer/zen/musicselect/ZenMusicSelectViewModel.kt +++ b/app/src/main/java/com/happyseal/zenplayer/zen/musicselect/ZenMusicSelectViewModel.kt @@ -6,6 +6,8 @@ import com.happyseal.zenplayer.LogMusic import com.happyseal.zenplayer.core.model.MiniPlayerAction import com.happyseal.zenplayer.core.model.Track import com.happyseal.zenplayer.core.service.ZenPlayerServiceBinder +import com.happyseal.zenplayer.core.service.miniPlayerProgress +import com.happyseal.zenplayer.core.service.miniPlayerUiState import com.happyseal.zenplayer.core.ui.component.MiniPlayerUiState import com.happyseal.zenplayer.core.util.WhileSubscribed5s import com.happyseal.zenplayer.core.util.whenNonNull @@ -42,7 +44,7 @@ class ZenMusicSelectViewModel @Inject constructor( private val musicRepository: MusicRepository, - serviceBinder: ZenPlayerServiceBinder, + private val serviceBinder: ZenPlayerServiceBinder, private val permissionHandler: AudioPermissionHandler, private val playlistRepository: PlaylistRepository, ) : ViewModel() { @@ -63,33 +65,9 @@ constructor( private val _playlistListState = MutableStateFlow(PlaylistListUiState.Loading) // --- MiniPlayer 로직 --- - private val miniPlayerUiState: StateFlow = - _binder - .whenNonNull(MiniPlayerUiState()) { binder -> - combine( - binder.playQueue, - binder.playbackState, - ) { playQueue, playbackState -> - MiniPlayerUiState( - currentTrackTitle = playQueue?.currentTrack?.title, - currentTrackArtist = playQueue?.currentTrack?.artist, - albumArtUri = playQueue?.currentTrack?.albumArtUri, - trackId = playQueue?.currentTrack?.id ?: 0L, - isPlaying = playbackState == PlaybackState.PLAYING, - ) - } - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed5s, MiniPlayerUiState()) + private val miniPlayerUiState: StateFlow = serviceBinder.miniPlayerUiState(viewModelScope) - private val miniPlayerProgress: StateFlow = - _binder - .whenNonNull(0f) { binder -> - combine( - binder.currentPosition, - binder.duration, - ) { position, duration -> - if (duration > 0) (position.toFloat() / duration).coerceIn(0f, 1f) else 0f - } - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed5s, 0f) + private val miniPlayerProgress: StateFlow = serviceBinder.miniPlayerProgress(viewModelScope) // --- 통합 UiState --- val uiState: StateFlow = From 5a59505901b6b59f0715f0651f48580d2f58a241 Mon Sep 17 00:00:00 2001 From: gagip Date: Thu, 26 Mar 2026 13:00:56 +0900 Subject: [PATCH 4/5] =?UTF-8?q?docs:=20UPDATE=5FINTERVAL=5FMS=20=EC=83=81?= =?UTF-8?q?=EC=88=98=20=EB=B6=84=EB=A6=AC=20=EC=9D=98=EB=8F=84=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TimerEngine.UPDATE_INTERVAL_MS와 ZenAudioController.POSITION_UPDATE_INTERVAL_MS는 값(100L)이 같지만 각각 타이머 UI 갱신, 재생 위치 추적이라는 독립적인 용도를 가지므로 공통 상수로 통합하지 않고 의도를 주석으로 명시. --- .../zenplayer/features/player/data/ZenAudioController.kt | 2 ++ .../happyseal/zenplayer/features/timer/domain/TimerEngine.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/src/main/java/com/happyseal/zenplayer/features/player/data/ZenAudioController.kt b/app/src/main/java/com/happyseal/zenplayer/features/player/data/ZenAudioController.kt index 0ee00c38..71934c50 100644 --- a/app/src/main/java/com/happyseal/zenplayer/features/player/data/ZenAudioController.kt +++ b/app/src/main/java/com/happyseal/zenplayer/features/player/data/ZenAudioController.kt @@ -248,6 +248,8 @@ class ZenAudioController( } companion object { + // TimerEngine.UPDATE_INTERVAL_MS와 값이 같지만 의도가 다름. + // 이 값은 재생 위치(currentPosition) 추적 갱신 주기이며, 타이머 UI와는 독립적으로 조정될 수 있음. private const val POSITION_UPDATE_INTERVAL_MS = 100L private const val DUCK_VOLUME = 0.3f } diff --git a/app/src/main/java/com/happyseal/zenplayer/features/timer/domain/TimerEngine.kt b/app/src/main/java/com/happyseal/zenplayer/features/timer/domain/TimerEngine.kt index 9883778b..57eb9b7d 100644 --- a/app/src/main/java/com/happyseal/zenplayer/features/timer/domain/TimerEngine.kt +++ b/app/src/main/java/com/happyseal/zenplayer/features/timer/domain/TimerEngine.kt @@ -379,6 +379,8 @@ class TimerEngine( } companion object { + // ZenAudioController.POSITION_UPDATE_INTERVAL_MS와 값이 같지만 의도가 다름. + // 이 값은 타이머 진행률 UI 갱신 주기이며, 오디오 재생 위치 추적과는 독립적으로 조정될 수 있음. internal const val UPDATE_INTERVAL_MS = 100L } } From 3baff2d6d93984aa8172edf251b253855bbbf758 Mon Sep 17 00:00:00 2001 From: gagip Date: Thu, 26 Mar 2026 13:05:31 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EA=B0=9C=EC=84=A0=EC=82=AC=ED=95=AD=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PlaylistDetailAction에서 MiniPlayer 케이스 제거 → Screen에 onMiniPlayerAction 파라미터 분리, NavGraph에서 exhaustive when으로 컴파일러 안전망 복원 - MiniPlayerViewModel, ZenMusicSelectViewModel의 _binder 필드 제거 → serviceBinder.binderFlow에 직접 접근하여 불필요한 중간 필드 삭제 --- .../com/happyseal/zenplayer/core/navigation/NavGraph.kt | 7 +++++-- .../zenplayer/features/playlist/ui/PlaylistDetailAction.kt | 4 ---- .../zenplayer/features/playlist/ui/PlaylistDetailScreen.kt | 7 ++++--- .../features/playlist/ui/PlaylistDetailViewModel.kt | 1 - .../zenplayer/zen/miniplayer/MiniPlayerViewModel.kt | 6 ++---- .../zenplayer/zen/musicselect/ZenMusicSelectViewModel.kt | 7 +++---- 6 files changed, 14 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/happyseal/zenplayer/core/navigation/NavGraph.kt b/app/src/main/java/com/happyseal/zenplayer/core/navigation/NavGraph.kt index d77390d9..0f42cb49 100644 --- a/app/src/main/java/com/happyseal/zenplayer/core/navigation/NavGraph.kt +++ b/app/src/main/java/com/happyseal/zenplayer/core/navigation/NavGraph.kt @@ -89,10 +89,13 @@ fun NavGraph( uiEvent = viewModel.uiEvent, onAction = { action -> when (action) { - is PlaylistDetailAction.MiniPlayer -> miniPlayerViewModel.onAction(action.action) - else -> viewModel.onAction(action) + is PlaylistDetailAction.SelectPlaylist, + is PlaylistDetailAction.PlayPlaylist, + is PlaylistDetailAction.RemoveTrack, + -> viewModel.onAction(action) } }, + onMiniPlayerAction = { miniPlayerViewModel.onAction(it) }, onNavigateBack = { navController.navigateUp() }, onNavigateToPlayer = { navController.navigate(Screen.Zen.route) { diff --git a/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailAction.kt b/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailAction.kt index a695a591..64cdd374 100644 --- a/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailAction.kt +++ b/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailAction.kt @@ -1,13 +1,9 @@ package com.happyseal.zenplayer.features.playlist.ui -import com.happyseal.zenplayer.core.model.MiniPlayerAction - sealed interface PlaylistDetailAction { data class SelectPlaylist(val playlistId: Long) : PlaylistDetailAction data class PlayPlaylist(val playlistId: Long, val startIndex: Int = 0) : PlaylistDetailAction data class RemoveTrack(val trackRowId: Long) : PlaylistDetailAction - - data class MiniPlayer(val action: MiniPlayerAction) : PlaylistDetailAction } diff --git a/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailScreen.kt b/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailScreen.kt index 394a5cd2..e873aea1 100644 --- a/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailScreen.kt +++ b/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailScreen.kt @@ -71,6 +71,7 @@ fun PlaylistDetailScreen( miniPlayerProgress: Float, uiEvent: Flow, onAction: (PlaylistDetailAction) -> Unit, + onMiniPlayerAction: (MiniPlayerAction) -> Unit, onNavigateBack: () -> Unit, onNavigateToPlayer: () -> Unit, modifier: Modifier = Modifier, @@ -194,9 +195,9 @@ fun PlaylistDetailScreen( MiniPlayerBar( uiState = miniPlayerUiState, progressProvider = { miniPlayerProgress }, - onTogglePlayPause = { onAction(PlaylistDetailAction.MiniPlayer(MiniPlayerAction.TogglePlayPause)) }, - onSkipNext = { onAction(PlaylistDetailAction.MiniPlayer(MiniPlayerAction.SkipNext)) }, - onSkipPrevious = { onAction(PlaylistDetailAction.MiniPlayer(MiniPlayerAction.SkipPrevious)) }, + onTogglePlayPause = { onMiniPlayerAction(MiniPlayerAction.TogglePlayPause) }, + onSkipNext = { onMiniPlayerAction(MiniPlayerAction.SkipNext) }, + onSkipPrevious = { onMiniPlayerAction(MiniPlayerAction.SkipPrevious) }, ) } } diff --git a/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailViewModel.kt b/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailViewModel.kt index 09ccbf34..baa86c1d 100644 --- a/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailViewModel.kt +++ b/app/src/main/java/com/happyseal/zenplayer/features/playlist/ui/PlaylistDetailViewModel.kt @@ -40,7 +40,6 @@ constructor( is PlaylistDetailAction.SelectPlaylist -> selectPlaylist(action.playlistId) is PlaylistDetailAction.PlayPlaylist -> playPlaylist(action.playlistId, action.startIndex) is PlaylistDetailAction.RemoveTrack -> removeTrackFromPlaylist(action.trackRowId) - is PlaylistDetailAction.MiniPlayer -> { /* MiniPlayer 액션은 MiniPlayerViewModel에서 처리 */ } } } diff --git a/app/src/main/java/com/happyseal/zenplayer/zen/miniplayer/MiniPlayerViewModel.kt b/app/src/main/java/com/happyseal/zenplayer/zen/miniplayer/MiniPlayerViewModel.kt index cc9832ab..1002d71d 100644 --- a/app/src/main/java/com/happyseal/zenplayer/zen/miniplayer/MiniPlayerViewModel.kt +++ b/app/src/main/java/com/happyseal/zenplayer/zen/miniplayer/MiniPlayerViewModel.kt @@ -16,16 +16,14 @@ import javax.inject.Inject class MiniPlayerViewModel @Inject constructor( - serviceBinder: ZenPlayerServiceBinder, + private val serviceBinder: ZenPlayerServiceBinder, ) : ViewModel() { - private val _binder = serviceBinder.binderFlow - val uiState: StateFlow = serviceBinder.miniPlayerUiState(viewModelScope) val progress: StateFlow = serviceBinder.miniPlayerProgress(viewModelScope) fun onAction(action: MiniPlayerAction) { - val binder = _binder.value ?: return + val binder = serviceBinder.binderFlow.value ?: return when (action) { MiniPlayerAction.TogglePlayPause -> when (binder.playbackState.value) { PlaybackState.PLAYING -> binder.pauseAudio() diff --git a/app/src/main/java/com/happyseal/zenplayer/zen/musicselect/ZenMusicSelectViewModel.kt b/app/src/main/java/com/happyseal/zenplayer/zen/musicselect/ZenMusicSelectViewModel.kt index 2c6242e3..ca0ef5e8 100644 --- a/app/src/main/java/com/happyseal/zenplayer/zen/musicselect/ZenMusicSelectViewModel.kt +++ b/app/src/main/java/com/happyseal/zenplayer/zen/musicselect/ZenMusicSelectViewModel.kt @@ -50,11 +50,10 @@ constructor( ) : ViewModel() { // --- MusicList 로직 --- - private val _binder = serviceBinder.binderFlow private val _musicListState = MutableStateFlow(MusicListUiState()) private val musicListUiState: StateFlow = - _binder + serviceBinder.binderFlow .whenNonNull(_musicListState) { binder -> combine(_musicListState, binder.currentTrack) { state, currentTrack -> state.copy(currentTrack = currentTrack) @@ -172,7 +171,7 @@ constructor( val tracks = musicListUiState.value.tracks check(tracks.isNotEmpty()) { "tracks is empty" } require(startIndex in tracks.indices) { "startIndex out of range: $startIndex" } - _binder.value?.setPlayQueueAndPlay(tracks, startIndex, playlistName = null) + serviceBinder.binderFlow.value?.setPlayQueueAndPlay(tracks, startIndex, playlistName = null) } private fun createPlaylist(name: String) { @@ -228,7 +227,7 @@ constructor( } private fun handleMiniPlayerAction(action: MiniPlayerAction) { - val binder = _binder.value ?: return + val binder = serviceBinder.binderFlow.value ?: return when (action) { MiniPlayerAction.TogglePlayPause -> when (binder.playbackState.value) { PlaybackState.PLAYING -> binder.pauseAudio()