From 52e4fd71cbad2ff3d86fc8fc77597fa5beef135a Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sun, 28 Dec 2025 17:46:57 +0900 Subject: [PATCH 01/11] =?UTF-8?q?NR-130=20assistedViewModel=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=ED=95=A8=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/extension/Fragment.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/extension/Fragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/extension/Fragment.kt index dd9c91b..6046c28 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/extension/Fragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/extension/Fragment.kt @@ -6,7 +6,10 @@ import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import com.google.android.material.snackbar.Snackbar import com.nextroom.nextroom.presentation.common.NRSnackbar import com.nextroom.nextroom.presentation.util.Logger @@ -111,3 +114,34 @@ fun Fragment.enableFullScreen( fun Fragment.disableFullScreen() { (requireActivity() as? WindowInsetsManager)?.disableFullScreen() } + +/** + * AssistedInject를 사용하는 ViewModel을 쉽게 생성하기 위한 확장 함수 + * + * ## 사용 예시 + * ```kotlin + * @Inject + * lateinit var viewModelFactory: HintViewModel.Factory + * + * private val gameSharedViewModel: GameSharedViewModel by hiltNavGraphViewModels(R.id.game_navigation) + * + * override val viewModel: HintViewModel by assistedViewModel { + * viewModelFactory.create(gameSharedViewModel) + * } + * ``` + * + * @param factory ViewModel을 생성하는 람다 함수 + * @return ViewModel의 Lazy 인스턴스 + */ +inline fun Fragment.assistedViewModel( + crossinline factory: () -> VM +): Lazy { + return viewModels { + object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return factory() as T + } + } + } +} From a2ec3d039b18b92340871b171131debe8cacdc16 Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sun, 28 Dec 2025 18:24:48 +0900 Subject: [PATCH 02/11] =?UTF-8?q?NR-130=20=EB=B3=B8=20=EC=A0=81=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=ED=9E=8C=ED=8A=B8=20id=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=EC=9D=84=20gameSharedViewModel=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/main/GameSharedViewModel.kt | 17 +++++++++ .../presentation/ui/main/TimerFragment.kt | 12 +++++-- .../presentation/ui/main/TimerScreenState.kt | 8 ++--- .../presentation/ui/main/TimerViewModel.kt | 35 ++++++++++--------- 4 files changed, 46 insertions(+), 26 deletions(-) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt index d7ba213..a4aae9b 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt @@ -23,7 +23,24 @@ class GameSharedViewModel @Inject constructor( private val _currentHint = MutableStateFlow(null) val currentHint: StateFlow = _currentHint.asStateFlow() + private val _openedHintIds = MutableStateFlow>(emptySet()) + val openedHintIds: StateFlow> = _openedHintIds.asStateFlow() + fun setCurrentHint(hint: Hint) { _currentHint.value = hint } + + fun addOpenedHintId(hintId: Int) { + _openedHintIds.value += hintId + } + + fun updateOpenedHintIds(hintIds: Set) { + _openedHintIds.value = hintIds + } + + fun hasOpenedHint(hintId: Int): Boolean { + return openedHintIds.value.contains(hintId) + } + + fun getOpenedHintCount() = openedHintIds.value.size } \ No newline at end of file diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerFragment.kt index 1f047e2..2607d41 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerFragment.kt @@ -11,7 +11,6 @@ import androidx.activity.OnBackPressedCallback import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.fragment.app.setFragmentResultListener -import androidx.fragment.app.viewModels import androidx.hilt.navigation.fragment.hiltNavGraphViewModels import androidx.navigation.fragment.findNavController import com.bumptech.glide.Glide @@ -27,6 +26,7 @@ import com.nextroom.nextroom.presentation.common.NRDialog import com.nextroom.nextroom.presentation.common.NROneButtonDialog import com.nextroom.nextroom.presentation.common.NRTwoButtonDialog import com.nextroom.nextroom.presentation.databinding.FragmentTimerBinding +import com.nextroom.nextroom.presentation.extension.assistedViewModel import com.nextroom.nextroom.presentation.extension.disableFullScreen import com.nextroom.nextroom.presentation.extension.enableFullScreen import com.nextroom.nextroom.presentation.extension.getResultData @@ -44,12 +44,18 @@ import com.nextroom.nextroom.presentation.util.Logger import dagger.hilt.android.AndroidEntryPoint import org.orbitmvi.orbit.viewmodel.observe import java.util.Locale +import javax.inject.Inject @AndroidEntryPoint class TimerFragment : BaseFragment(FragmentTimerBinding::inflate) { private lateinit var backCallback: OnBackPressedCallback - private val viewModel: TimerViewModel by viewModels() + @Inject + lateinit var viewModelFactory: TimerViewModel.Factory + + private val viewModel: TimerViewModel by assistedViewModel { + viewModelFactory.create(gameSharedViewModel) + } private val painterViewModel: PainterViewModel by activityViewModels() private val gameSharedViewModel: GameSharedViewModel by hiltNavGraphViewModels(R.id.game_navigation) @@ -238,7 +244,7 @@ class TimerFragment : BaseFragment(FragmentTimerBinding::i tvHintCount.text = String.format( Locale.getDefault(), "%d/%s", - state.usedHintsCount, + state.openedHintCount, if (state.totalHintCount == -1) "∞" else state.totalHintCount.toString(), ) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerScreenState.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerScreenState.kt index 618fb91..38014a4 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerScreenState.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerScreenState.kt @@ -8,14 +8,10 @@ data class TimerScreenState( val lastSeconds: Int = 0, val currentInput: String = "", val inputState: InputState = InputState.Empty, - val usedHints: Set = emptySet(), - val answerOpenedHints: Set = emptySet(), + val openedHintCount: Int = 0, val totalHintCount: Int = -1, val startTime: Long = -1, val themeImageUrl: String? = null, val themeImageCustomInfo: ThemeImageCustomInfo? = null, val themeImageEnabled: Boolean = false -) { - val usedHintsCount: Int - get() = usedHints.size -} +) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt index de3e590..b67f85e 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt @@ -1,20 +1,20 @@ package com.nextroom.nextroom.presentation.ui.main -import androidx.lifecycle.SavedStateHandle import com.nextroom.nextroom.domain.model.GameState import com.nextroom.nextroom.domain.model.ThemeImageCustomInfo import com.nextroom.nextroom.domain.model.ThemeInfo import com.nextroom.nextroom.domain.model.TimerState import com.nextroom.nextroom.domain.repository.GameStateRepository import com.nextroom.nextroom.domain.repository.HintRepository -import com.nextroom.nextroom.domain.repository.StatisticsRepository import com.nextroom.nextroom.domain.repository.ThemeRepository import com.nextroom.nextroom.domain.repository.TimerRepository import com.nextroom.nextroom.presentation.R import com.nextroom.nextroom.presentation.base.BaseViewModel import com.nextroom.nextroom.presentation.model.Hint import com.nextroom.nextroom.presentation.model.InputState -import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first @@ -25,16 +25,13 @@ import org.orbitmvi.orbit.syntax.simple.postSideEffect import org.orbitmvi.orbit.syntax.simple.reduce import org.orbitmvi.orbit.viewmodel.container import timber.log.Timber -import javax.inject.Inject -@HiltViewModel -class TimerViewModel @Inject constructor( - private val savedStateHandle: SavedStateHandle, +class TimerViewModel @AssistedInject constructor( private val themeRepository: ThemeRepository, private val timerRepository: TimerRepository, private val gameStateRepository: GameStateRepository, private val hintRepository: HintRepository, - private val statsRepository: StatisticsRepository, + @Assisted private val gameSharedViewModel: GameSharedViewModel ) : BaseViewModel() { override val container: Container = container(TimerScreenState()) @@ -192,11 +189,9 @@ class TimerViewModel @Inject constructor( private fun validateHintCode() = intent { suspend fun openHint(hint: com.nextroom.nextroom.domain.model.Hint) { + gameSharedViewModel.addOpenedHintId(hint.id) reduce { - state.copy( - usedHints = state.usedHints + hint.id, - inputState = InputState.Ok, - ) + state.copy(inputState = InputState.Ok, openedHintCount = gameSharedViewModel.getOpenedHintCount()) } postSideEffect( TimerEvent.OnOpenHint( @@ -205,7 +200,7 @@ class TimerViewModel @Inject constructor( progress = hint.progress, hint = hint.description, answer = hint.answer, - answerOpened = state.answerOpenedHints.contains(hint.id), + answerOpened = gameSharedViewModel.openedHintIds.value.contains(hint.id), hintImageUrlList = hint.hintImageUrlList.toList(), answerImageUrlList = hint.answerImageUrlList.toList() ) @@ -222,9 +217,9 @@ class TimerViewModel @Inject constructor( } hintRepository.getHint(state.currentInput)?.let { hint -> - if (state.usedHints.contains(hint.id)) { + if (gameSharedViewModel.hasOpenedHint(hint.id)) { openHint(hint) - } else if (state.usedHintsCount < state.totalHintCount) { + } else if (gameSharedViewModel.getOpenedHintCount() < state.totalHintCount) { openHint(hint) } else { postSideEffect(TimerEvent.ShowAvailableHintExceedError) @@ -242,7 +237,7 @@ class TimerViewModel @Inject constructor( gameStateRepository.saveGameState( timeLimitInMinute = state.totalSeconds / 60, hintLimit = state.totalHintCount, - usedHints = state.usedHints, + usedHints = gameSharedViewModel.openedHintIds.value, startTime = state.startTime, useTimerUrl = state.themeImageEnabled, themeImageUrl = state.themeImageUrl, @@ -275,12 +270,13 @@ class TimerViewModel @Inject constructor( themeImageCustomInfo: ThemeImageCustomInfo? = null, themeImageEnabled: Boolean, ) = intent { + gameSharedViewModel.updateOpenedHintIds(usedHints) reduce { state.copy( totalSeconds = seconds, totalHintCount = hintLimit, - usedHints = usedHints, lastSeconds = lastSeconds, + openedHintCount = usedHints.size, startTime = startTime, themeImageUrl = themeImageUrl, themeImageCustomInfo = themeImageCustomInfo, @@ -298,4 +294,9 @@ class TimerViewModel @Inject constructor( super.onCleared() Timber.d("onCleared: GameViewModel") } + + @AssistedFactory + interface Factory { + fun create(gameSharedViewModel: GameSharedViewModel): TimerViewModel + } } From f8e713f1bf525b52415d3e9e4387b36dab0b71bf Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sun, 28 Dec 2025 18:46:19 +0900 Subject: [PATCH 03/11] =?UTF-8?q?NR-130=20=EB=B3=B8=20=EC=A0=81=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=ED=9E=8C=ED=8A=B8=20=EC=B9=B4=EC=9A=B4?= =?UTF-8?q?=ED=8C=85=EC=9D=84=20=EB=8D=94=EC=9D=B4=EC=83=81=20=ED=83=80?= =?UTF-8?q?=EC=9D=B4=EB=A8=B8=20=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 왜? 힌트 화면에서 버튼을 클릭하면 카운팅 하도록 변경할 예정임 그래서 타이머 화면에서 addOpenedHintId 호출하는 로직을 제거했고 gameSharedViewModel의 openedHintIds flow를 combine하여 화면을 그리도록 수정하였음 --- .../presentation/ui/main/TimerFragment.kt | 16 ++++++++++++++-- .../presentation/ui/main/TimerViewModel.kt | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerFragment.kt index 2607d41..dcc9139 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerFragment.kt @@ -31,6 +31,7 @@ import com.nextroom.nextroom.presentation.extension.disableFullScreen import com.nextroom.nextroom.presentation.extension.enableFullScreen import com.nextroom.nextroom.presentation.extension.getResultData import com.nextroom.nextroom.presentation.extension.hasResultData +import com.nextroom.nextroom.presentation.extension.repeatOnStarted import com.nextroom.nextroom.presentation.extension.safeNavigate import com.nextroom.nextroom.presentation.extension.setOnLongClickListener import com.nextroom.nextroom.presentation.extension.snackbar @@ -42,7 +43,7 @@ import com.nextroom.nextroom.presentation.ui.main.ModifyTimeBottomSheet.Companio import com.nextroom.nextroom.presentation.ui.memo.PainterViewModel import com.nextroom.nextroom.presentation.util.Logger import dagger.hilt.android.AndroidEntryPoint -import org.orbitmvi.orbit.viewmodel.observe +import kotlinx.coroutines.launch import java.util.Locale import javax.inject.Inject @@ -78,11 +79,22 @@ class TimerFragment : BaseFragment(FragmentTimerBinding::i initViews() initListener() setFragmentResultListeners() - viewModel.observe(viewLifecycleOwner, state = ::render, sideEffect = ::handleEvent) + initObserve() enableFullScreen() setGameStartConfirmDialog() } + private fun initObserve() { + viewLifecycleOwner.repeatOnStarted { + launch { + viewModel.uiState.collect(::render) + } + launch { + viewModel.container.sideEffectFlow.collect(::handleEvent) + } + } + } + private fun setGameStartConfirmDialog() { gameStartConfirmDialog = NRDialog.Builder(requireContext()) .setTitle(R.string.dialog_title_game_start_confirm) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt index b67f85e..dbf5a46 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt @@ -17,7 +17,10 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.syntax.simple.intent @@ -36,6 +39,17 @@ class TimerViewModel @AssistedInject constructor( override val container: Container = container(TimerScreenState()) + val uiState = combine( + container.stateFlow, + gameSharedViewModel.openedHintIds + ) { state, openedHintIds -> + state.copy(openedHintCount = openedHintIds.size) + }.stateIn( + baseViewModelScope, + SharingStarted.Lazily, + container.stateFlow.value + ) + init { baseViewModelScope.launch { timerRepository.lastSeconds.collect(::tick) @@ -189,7 +203,6 @@ class TimerViewModel @AssistedInject constructor( private fun validateHintCode() = intent { suspend fun openHint(hint: com.nextroom.nextroom.domain.model.Hint) { - gameSharedViewModel.addOpenedHintId(hint.id) reduce { state.copy(inputState = InputState.Ok, openedHintCount = gameSharedViewModel.getOpenedHintCount()) } From 10b8824edee1784262990972955ed71da66cd815 Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sun, 28 Dec 2025 20:33:33 +0900 Subject: [PATCH 04/11] =?UTF-8?q?NR-130=20=EB=B3=B8=20=EC=A0=81=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=ED=9E=8C=ED=8A=B8=EB=A5=BC=20=EB=A1=9C?= =?UTF-8?q?=EC=BB=AC=EC=97=90=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 왜? 본 적 있는 힌트만 업데이트하면 되는데 기존에는 gameState를 전부 업데이트하고 있었음 어떻게? updateUsedHints를 만들었음 그리고 gameSharedViewModel이 앞으로 본 적 있는 힌트를 관리할거라 여기서 업데이트 하도록 수정함 --- .../com/nextroom/nextroom/data/db/GameStateDao.kt | 3 +++ .../data/repository/GameStateRepositoryImpl.kt | 4 ++++ .../domain/repository/GameStateRepository.kt | 2 ++ .../presentation/ui/main/GameSharedViewModel.kt | 8 +++++++- .../presentation/ui/main/TimerViewModel.kt | 15 --------------- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/data/src/main/java/com/nextroom/nextroom/data/db/GameStateDao.kt b/data/src/main/java/com/nextroom/nextroom/data/db/GameStateDao.kt index 578ad88..5372a1e 100644 --- a/data/src/main/java/com/nextroom/nextroom/data/db/GameStateDao.kt +++ b/data/src/main/java/com/nextroom/nextroom/data/db/GameStateDao.kt @@ -17,4 +17,7 @@ interface GameStateDao { @Query("DELETE FROM $GAME_STATE_TABLE") suspend fun deleteGameState() + + @Query("UPDATE $GAME_STATE_TABLE SET usedHints = :usedHints") + suspend fun updateUsedHints(usedHints: Set) } diff --git a/data/src/main/java/com/nextroom/nextroom/data/repository/GameStateRepositoryImpl.kt b/data/src/main/java/com/nextroom/nextroom/data/repository/GameStateRepositoryImpl.kt index 23ee1bd..8eb5168 100644 --- a/data/src/main/java/com/nextroom/nextroom/data/repository/GameStateRepositoryImpl.kt +++ b/data/src/main/java/com/nextroom/nextroom/data/repository/GameStateRepositoryImpl.kt @@ -48,4 +48,8 @@ class GameStateRepositoryImpl @Inject constructor( override suspend fun getGameState(): GameState? { return gameStateDao.getGameState()?.toDomain() } + + override suspend fun updateUsedHints(usedHints: Set) { + gameStateDao.updateUsedHints(usedHints) + } } diff --git a/domain/src/main/java/com/nextroom/nextroom/domain/repository/GameStateRepository.kt b/domain/src/main/java/com/nextroom/nextroom/domain/repository/GameStateRepository.kt index 89e0529..4ebcd0c 100644 --- a/domain/src/main/java/com/nextroom/nextroom/domain/repository/GameStateRepository.kt +++ b/domain/src/main/java/com/nextroom/nextroom/domain/repository/GameStateRepository.kt @@ -18,4 +18,6 @@ interface GameStateRepository { suspend fun finishGame(onFinished: () -> Unit = {}) suspend fun getGameState(): GameState? + + suspend fun updateUsedHints(usedHints: Set) } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt index a4aae9b..0665051 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt @@ -2,17 +2,20 @@ package com.nextroom.nextroom.presentation.ui.main import androidx.lifecycle.SavedStateHandle import com.nextroom.nextroom.domain.model.SubscribeStatus +import com.nextroom.nextroom.domain.repository.GameStateRepository import com.nextroom.nextroom.presentation.base.NewBaseViewModel import com.nextroom.nextroom.presentation.model.Hint import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class GameSharedViewModel @Inject constructor( - savedStateHandle: SavedStateHandle + savedStateHandle: SavedStateHandle, + private val gameStateRepository: GameStateRepository ) : NewBaseViewModel() { private val _subscribeStatus = MutableStateFlow( @@ -32,6 +35,9 @@ class GameSharedViewModel @Inject constructor( fun addOpenedHintId(hintId: Int) { _openedHintIds.value += hintId + baseViewModelScope.launch { + gameStateRepository.updateUsedHints(_openedHintIds.value) + } } fun updateOpenedHintIds(hintIds: Set) { diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt index dbf5a46..b9450ce 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt @@ -219,7 +219,6 @@ class TimerViewModel @AssistedInject constructor( ) ), ) - setGameState() } if (timerRepository.timerState.value is TimerState.Finished) { @@ -245,20 +244,6 @@ class TimerViewModel @AssistedInject constructor( } } - private fun setGameState() = intent { - if (timerRepository.timerState.value !is TimerState.Finished) { - gameStateRepository.saveGameState( - timeLimitInMinute = state.totalSeconds / 60, - hintLimit = state.totalHintCount, - usedHints = gameSharedViewModel.openedHintIds.value, - startTime = state.startTime, - useTimerUrl = state.themeImageEnabled, - themeImageUrl = state.themeImageUrl, - themeImageCustomInfo = state.themeImageCustomInfo - ) - } - } - private fun startGame(endTimeMillis: Long) = intent { timerRepository.startTimerUntil(endTimeMillis) } From 1deb6742b2ef3d442b2529538382147e85e38e07 Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sun, 28 Dec 2025 20:38:30 +0900 Subject: [PATCH 05/11] =?UTF-8?q?NR-130=20=ED=9E=8C=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EB=B8=94=EB=9F=AC=EC=B2=98=EB=A6=AC=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD=ED=95=B4=EC=95=BC=20=ED=9E=8C=ED=8A=B8?= =?UTF-8?q?=EA=B0=80=20=EB=B3=B4=EC=9D=B4=EB=8F=84=EB=A1=9D=20=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 왜? 힌트 차감없이 진행률만 보고 싶어하는 손님들을 위한 로직 개선. --- .../presentation/ui/hint/HintFragment.kt | 15 ++- .../presentation/ui/hint/HintState.kt | 3 +- .../presentation/ui/hint/HintViewModel.kt | 30 ++++- .../ui/hint/compose/HintScreen.kt | 112 ++++++++++++++---- .../src/main/res/drawable/ic_lock.webp | Bin 0 -> 2430 bytes presentation/src/main/res/values/strings.xml | 1 + 6 files changed, 129 insertions(+), 32 deletions(-) create mode 100644 presentation/src/main/res/drawable/ic_lock.webp diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt index 68cb8c5..5e2391c 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.os.bundleOf -import androidx.fragment.app.viewModels import androidx.hilt.navigation.fragment.hiltNavGraphViewModels import androidx.navigation.fragment.findNavController import com.google.firebase.analytics.FirebaseAnalytics @@ -20,6 +19,7 @@ import com.nextroom.nextroom.domain.model.SubscribeStatus import com.nextroom.nextroom.presentation.NavGraphDirections import com.nextroom.nextroom.presentation.R import com.nextroom.nextroom.presentation.base.ComposeBaseViewModelFragment +import com.nextroom.nextroom.presentation.extension.assistedViewModel import com.nextroom.nextroom.presentation.extension.enableFullScreen import com.nextroom.nextroom.presentation.extension.repeatOnStarted import com.nextroom.nextroom.presentation.extension.safeNavigate @@ -30,13 +30,21 @@ import com.nextroom.nextroom.presentation.ui.hint.compose.HintTimerToolbar import com.nextroom.nextroom.presentation.ui.main.GameSharedViewModel import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +import javax.inject.Inject @AndroidEntryPoint class HintFragment : ComposeBaseViewModelFragment() { override val screenName: String = "hint" - override val viewModel: HintViewModel by viewModels() + + @Inject + lateinit var viewModelFactory: HintViewModel.Factory + private val gameSharedViewModel: GameSharedViewModel by hiltNavGraphViewModels(R.id.game_navigation) + override val viewModel: HintViewModel by assistedViewModel { + viewModelFactory.create(gameSharedViewModel) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -60,7 +68,8 @@ class HintFragment : ComposeBaseViewModelFragment() { state = state, onAnswerButtonClick = ::handleAnswerButton, onHintImageClick = ::navigateToHintImageViewer, - onAnswerImageClick = ::navigateToAnswerImageViewer + onAnswerImageClick = ::navigateToAnswerImageViewer, + onHintOpenClick = { gameSharedViewModel.addOpenedHintId(state.hint.id) } ) } } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintState.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintState.kt index 5fe399c..396f248 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintState.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintState.kt @@ -7,5 +7,6 @@ data class HintState( val loading: Boolean = false, val hint: Hint = Hint(), val userSubscribeStatus: SubscribeStatus = SubscribeStatus.Default, - val networkDisconnectedCount: Int = 0 + val networkDisconnectedCount: Int = 0, + val isHintOpened: Boolean = false ) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt index a2268aa..bbac04a 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt @@ -5,22 +5,35 @@ import com.nextroom.nextroom.domain.repository.DataStoreRepository import com.nextroom.nextroom.domain.repository.TimerRepository import com.nextroom.nextroom.presentation.base.NewBaseViewModel import com.nextroom.nextroom.presentation.model.Hint -import dagger.hilt.android.lifecycle.HiltViewModel +import com.nextroom.nextroom.presentation.ui.main.GameSharedViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class HintViewModel @Inject constructor( +class HintViewModel @AssistedInject constructor( private val timerRepository: TimerRepository, private val dataStoreRepository: DataStoreRepository, + @Assisted private val gameSharedViewModel: GameSharedViewModel ) : NewBaseViewModel() { private val _uiState = MutableStateFlow(HintState()) - val uiState = _uiState.asStateFlow() + val uiState = combine( + _uiState, + gameSharedViewModel.openedHintIds + ) { state, openedHintIds -> + state.copy(isHintOpened = state.hint.id in openedHintIds) + }.stateIn( + baseViewModelScope, + SharingStarted.Lazily, + _uiState.value + ) private val _uiEvent = MutableSharedFlow(extraBufferCapacity = 1) val uiEvent = _uiEvent.asSharedFlow() @@ -56,4 +69,9 @@ class HintViewModel @Inject constructor( hint = _uiState.value.hint.copy(answerOpened = true) ) } + + @AssistedFactory + interface Factory { + fun create(gameSharedViewModel: GameSharedViewModel): HintViewModel + } } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/compose/HintScreen.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/compose/HintScreen.kt index df80024..c192af1 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/compose/HintScreen.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/compose/HintScreen.kt @@ -9,10 +9,14 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.HorizontalDivider @@ -28,8 +32,10 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -40,6 +46,7 @@ import com.nextroom.nextroom.presentation.common.compose.NRColor import com.nextroom.nextroom.presentation.common.compose.NRLoading import com.nextroom.nextroom.presentation.common.compose.NRToolbar import com.nextroom.nextroom.presentation.common.compose.NRTypo +import com.nextroom.nextroom.presentation.extension.throttleClick import com.nextroom.nextroom.presentation.extension.toTimerFormat import com.nextroom.nextroom.presentation.model.Hint import com.nextroom.nextroom.presentation.ui.hint.HintState @@ -52,6 +59,7 @@ fun HintScreen( onAnswerButtonClick: () -> Unit, onHintImageClick: (Int) -> Unit, onAnswerImageClick: (Int) -> Unit, + onHintOpenClick: () -> Unit, modifier: Modifier = Modifier ) { val listState = rememberLazyListState() @@ -115,24 +123,78 @@ fun HintScreen( modifier = Modifier.fillMaxWidth() ) - if (state.hint.hintImageUrlList.isNotEmpty()) { - ImagePager( - imageUrls = state.hint.hintImageUrlList, - subscribeStatus = state.userSubscribeStatus, - networkDisconnectedCount = state.networkDisconnectedCount, - onImageClick = onHintImageClick, - modifier = Modifier.padding(top = 12.dp) - ) - } - - Text( - text = state.hint.hint, - style = NRTypo.Pretendard.size20, - color = NRColor.Gray01, + Box( modifier = Modifier .fillMaxWidth() + .wrapContentHeight() + .heightIn(min = if (state.isHintOpened) 0.dp else 200.dp) .padding(top = 12.dp) - ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .blur(if (state.isHintOpened) 0.dp else 10.dp) + ) { + if (state.hint.hintImageUrlList.isNotEmpty() && state.isHintOpened) { + ImagePager( + imageUrls = state.hint.hintImageUrlList, + subscribeStatus = state.userSubscribeStatus, + networkDisconnectedCount = state.networkDisconnectedCount, + onImageClick = onHintImageClick, + modifier = Modifier.padding(bottom = 20.dp) + ) + } + + Text( + text = state.hint.hint, + style = NRTypo.Pretendard.size20, + color = NRColor.Gray01, + modifier = Modifier.fillMaxWidth() + ) + } + + if (!state.isHintOpened) { + Box( + modifier = Modifier + .matchParentSize() + .background(NRColor.Black.copy(alpha = 0.1f)) + .throttleClick { onHintOpenClick() }, + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(horizontal = 20.dp) + ) { + Image( + modifier = modifier.size(20.dp), + painter = painterResource(R.drawable.ic_lock), + colorFilter = ColorFilter.tint(NRColor.White), + contentDescription = null, + ) + Text( + text = stringResource(R.string.text_open_hint_guide_message), + color = NRColor.White, + style = NRTypo.Pretendard.size14SemiBold, + modifier = modifier + .padding(top = 10.dp) + .throttleClick { onHintOpenClick() } + ) + Text( + text = stringResource(R.string.game_view_hint), + color = NRColor.Black, + style = NRTypo.Pretendard.size16Bold, + modifier = modifier + .padding(top = 20.dp) + .background( + color = NRColor.White, + shape = RoundedCornerShape(8.dp) + ) + .padding(vertical = 8.dp, horizontal = 12.dp) + ) + } + } + } + } } } @@ -160,7 +222,7 @@ fun HintScreen( subscribeStatus = state.userSubscribeStatus, networkDisconnectedCount = state.networkDisconnectedCount, onImageClick = onAnswerImageClick, - modifier = Modifier.padding(top = 12.dp) + modifier = Modifier.padding(top = 12.dp, bottom = 20.dp) ) } @@ -241,11 +303,13 @@ private fun HintScreenWithNoImagesPreview() { answerImageUrlList = emptyList() ), userSubscribeStatus = SubscribeStatus.Subscribed, - networkDisconnectedCount = 0 + networkDisconnectedCount = 0, + isHintOpened = true ), onAnswerButtonClick = {}, onHintImageClick = {}, - onAnswerImageClick = {} + onAnswerImageClick = {}, + onHintOpenClick = {} ) } @@ -268,11 +332,13 @@ private fun HintScreenWithImagesPreview() { answerImageUrlList = emptyList() ), userSubscribeStatus = SubscribeStatus.Subscribed, - networkDisconnectedCount = 0 + networkDisconnectedCount = 0, + isHintOpened = false ), onAnswerButtonClick = {}, onHintImageClick = {}, - onAnswerImageClick = {} + onAnswerImageClick = {}, + onHintOpenClick = {} ) } @@ -292,11 +358,13 @@ private fun HintScreenAnswerOpenedPreview() { answerImageUrlList = emptyList(), ), userSubscribeStatus = SubscribeStatus.Subscribed, - networkDisconnectedCount = 0 + networkDisconnectedCount = 0, + isHintOpened = true ), onAnswerButtonClick = {}, onHintImageClick = {}, - onAnswerImageClick = {} + onAnswerImageClick = {}, + onHintOpenClick = {} ) } diff --git a/presentation/src/main/res/drawable/ic_lock.webp b/presentation/src/main/res/drawable/ic_lock.webp new file mode 100644 index 0000000000000000000000000000000000000000..5a02700971f7e97d8706c1d56f15e4bfddaf452b GIT binary patch literal 2430 zcmb8hc{~$}0|0PG7_YfK%bq#&)XFtVt~N49u5uPRTI@Af?sIqu?>Uov$W2jM-Ad%6ce0=Vf=1y)-hIaS=yY|VuWJz&yN3czy zy3O?Q$;r&nIK%|rn#Ttb;6pkso}2{L9XW5V>(M%h_|TcsFJC%>X&u1{9aCE5mXHST z&euHqer6CyI#*xJub0|k%q;h;m>~Zzp zc%eh4FSl8VwV2Js9lCZc&<}7@sWq?c%??D~b3(Tc^MExRRRnh!n;|6M(_2Jr`YQfpK z5LQK#gBPP_sZKVsZkjg?dM+5b8m9!hK1qF&V5$}E8q-S_t7lVd7UG>)COEA-3n+7@ z%-W~xKMPu2PZRc%@&OV+YTI@qJe})#=WX8y3njt^-9SnF`tglIC9%JDtIImmt;}_Tqx5sDMN;2hnxq~x4h&mw_*66(?~&j*KxAiP ztrh?5hm8lJVUSaH5{+Z?L$TuJ%?>d?bzAEQ^YxkiwHvV!5dK?hm8ilWe5_|pEe%U^ z*e)>y<1m+!;NY@vOphD-B&gZ2$z6*Tc+K80Onkm5<&rZSIzhEqi|j1*$gkO{(15!i z?m9hwf#c0{E+*YUR!i&0pNm=up#Y@^^JOBKyi9b&^1i~ogYdpcgGLw0-I6h?x3CXW z&sB*td6i2=)82sQoVB7#dN4SxP#i^G;-KMZtA$!(9{FF3b>dOo9v9ZCj%lH~9qE-; zp>f{dcgGSt*gl*DN8?fUM4@Z5ORyrDr=7Ha-*RN*ylQiBHQvWicDcQ(1$f9Q!d5?A zb(691E2geq)I2n@)oJ8^rhR;Rucwp$1=U_>0nWwP4^}Q!65m4?e2M9NQ-OM7sEnTq zdzS854tH(j${{pXO)EJe&GW+HaQENy#=WrJ%{xJF2-X5d`oTlvtUmBu@g%~-7O|Kk zE@c6{kO>lmKKD8C5D=JEkG~X-{qxD$c@hHc3+YG0w!tM6RAX_s&+!+Z+M62V!doLe zHpQ=JfBy3MC}Xc}3rvGz@=M9zwq#tJTl|pnvXA|p9+QC>-OV$B&;a2tPIcKT^#nx= z_?gGzf@pkxUWs>VQripjddVx`q!4TW8=Z^F7SrPimxvD5!=3Kqd{R(`4!AGmnm@9^ z7n3vGXfR4jd3mq;K@p;&hR5SEx*{ct2nJw}7AXPK6mK!Qs) zQ(+Zy_7z_rs3>Ur@eI9@E9U6M`w~cKQ|aW}G#=}~JY?|iLPKW)w3vWh3e$0e+q_XN zp~zj>8(Kbd0m{hs$1_Fo=Ut?Kb-JCcUPLfm^3?p}FIXE`adaK9rR;(Zz&L6S5|wbj zJS?;f*#P?Qr)@adRgBEnl!r)qJvtB`o7lNnc$?^pP}h71BQ~kFRm;=!2a+`3aje3P z_&LI<>Z6m*@UIjR0jSe}U6|0#28nUebXj97hVWenr7p$cr#9Yy(#@2!(*&+wXAB`` zgc1?uGw}Nz8hHd$U*vC*<39$12K|r{U69Q0uqkLBO>kIe?G_qC$^4VaSE+2r2xxQ% zMX@upNGV>}Fy$YNaFBJ57g28LV^zHlvuvbAr*~H|5~SL`_pr^8bIP@-5lwKE-3xF; zf@T;^?+YH8j=f(jeFYvGsifTC!en}Dx$O+BY;*wgS{vR<`k~aB!&+q<2*e10T+We0 zNgj?>q=?;ADf%H+dur7oURHdSiBvI-DMqTOq4jFH;f#BdW&RT$TgqSQaq{VZm+mSK ze5@!|F8??OPA~qnq$lKOIfDZ%w~X%yBSuH(#A{>G4lc6h?HtRU0Ea$JKsyK72K)UA zAQ<-jGGL?>W(25WbA;RUG6C;-Jy?XY9vUI(#%NCUD8*|4Q0(Z`R*i@b5tS>Uas#8~ zBm}ePyYg=nboun*o(kpO@ou?O3qAA7^wDxO;=w3Qba|}Y5z)_%PGg;m_>tB>N|PV> zSiS)QRS&=h9_Td~2se&5gQC9;-&G#-8iaG$3*_XhCr3Z`|Ci)jzU?vrSxd>q8X0)$ zTjZg7n03S)c zFrLN8o-$E;LM2^wq;(y52tG&xQZj2Z7N2~+5EqY;^md+0UzY@Bafraw1Ykxj`Z!R$ z!_fdlyww~gcG}7cjPeFrK}Y>%3{0o!Q#CylYWLEQXCROt=|z79 zET;CG*r!l(aw0r^V}Kv2e2Md@72_Qn;WX#)M2Eb*%VyJ4VI-NGBzE@1(;eM9wtpgS ztzg9>FfxAV128WRIH?HklWIp%LerU_a}-oUgTCbzXkW@k4)b{1jJ~Rqq~pW?J8q@A z=HprR2Y^SAGOm=W)+LDAy&ZVbImX(UvA#T&`5TI z6%0{?n(5Z942h_n0ka1#OQ8gH!C5ac_Y#|goQH-B#NSNihqVQ$?VXXw%Td1H2t&g} zpXB^yO4IEw$j|DoF1GarNwCd>4dP62Uv9rel8kAALKN#R8BCjt;>sfZO90}j)w?)! z)2%}z;a*c(E~;*Vu}w6@uLu1`Ib;TZm시간 수정 에러 코드: %d + 힌트를 보려면 클릭하세요! \ No newline at end of file From 1b98c58aa1a8982a4b7053460006c10da55f828d Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sun, 28 Dec 2025 20:59:31 +0900 Subject: [PATCH 06/11] =?UTF-8?q?NR-130=20=EC=A0=95=EB=8B=B5=EC=9D=84=20?= =?UTF-8?q?=EB=B8=94=EB=9F=AC=EC=B2=98=EB=A6=AC=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD=ED=95=B4=EC=95=BC=20=EC=A0=95=EB=8B=B5?= =?UTF-8?q?=EC=9D=B4=20=EB=B3=B4=EC=9D=B4=EB=8F=84=EB=A1=9D=20=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 왜? 힌트와 똑같은 로직 반영 --- .../presentation/ui/hint/HintEvent.kt | 1 - .../presentation/ui/hint/HintFragment.kt | 13 +- .../presentation/ui/hint/HintState.kt | 3 +- .../presentation/ui/hint/HintViewModel.kt | 16 +- .../ui/hint/compose/HintScreen.kt | 165 +++++++++--------- .../ui/main/GameSharedViewModel.kt | 7 + presentation/src/main/res/values/strings.xml | 2 + 7 files changed, 101 insertions(+), 106 deletions(-) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintEvent.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintEvent.kt index 7855911..3e9ecb7 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintEvent.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintEvent.kt @@ -1,7 +1,6 @@ package com.nextroom.nextroom.presentation.ui.hint sealed interface HintEvent { - data object OpenAnswer : HintEvent data object NetworkError : HintEvent data object UnknownError : HintEvent data class ClientError(val message: String) : HintEvent diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt index 5e2391c..df069ec 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt @@ -66,10 +66,10 @@ class HintFragment : ComposeBaseViewModelFragment() { HintScreen( state = state, - onAnswerButtonClick = ::handleAnswerButton, onHintImageClick = ::navigateToHintImageViewer, onAnswerImageClick = ::navigateToAnswerImageViewer, - onHintOpenClick = { gameSharedViewModel.addOpenedHintId(state.hint.id) } + onHintOpenClick = { gameSharedViewModel.addOpenedHintId(state.hint.id) }, + onAnswerOpenClick = { gameSharedViewModel.addOpenedAnswerId(state.hint.id) } ) } } @@ -101,14 +101,6 @@ class HintFragment : ComposeBaseViewModelFragment() { } } - private fun handleAnswerButton() { - if (viewModel.uiState.value.hint.answerOpened) { - gotoHome() - } else { - viewModel.openAnswer() - } - } - private fun navigateToHintImageViewer(position: Int) { val state = viewModel.uiState.value if (state.userSubscribeStatus == SubscribeStatus.Subscribed) { @@ -144,7 +136,6 @@ class HintFragment : ComposeBaseViewModelFragment() { private fun handleEvent(event: HintEvent) { when (event) { - HintEvent.OpenAnswer -> viewModel.openAnswer() is HintEvent.NetworkError -> snackbar(R.string.error_network) is HintEvent.UnknownError -> snackbar(R.string.error_something) is HintEvent.ClientError -> snackbar(event.message) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintState.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintState.kt index 396f248..086aa9b 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintState.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintState.kt @@ -8,5 +8,6 @@ data class HintState( val hint: Hint = Hint(), val userSubscribeStatus: SubscribeStatus = SubscribeStatus.Default, val networkDisconnectedCount: Int = 0, - val isHintOpened: Boolean = false + val isHintOpened: Boolean = false, + val isAnswerOpened: Boolean = false ) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt index bbac04a..0279e1d 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt @@ -26,9 +26,13 @@ class HintViewModel @AssistedInject constructor( private val _uiState = MutableStateFlow(HintState()) val uiState = combine( _uiState, - gameSharedViewModel.openedHintIds - ) { state, openedHintIds -> - state.copy(isHintOpened = state.hint.id in openedHintIds) + gameSharedViewModel.openedHintIds, + gameSharedViewModel.openedAnswerIds + ) { state, openedHintIds, openedAnswerIds -> + state.copy( + isHintOpened = state.hint.id in openedHintIds, + isAnswerOpened = state.hint.id in openedAnswerIds + ) }.stateIn( baseViewModelScope, SharingStarted.Lazily, @@ -64,12 +68,6 @@ class HintViewModel @AssistedInject constructor( _uiState.value = _uiState.value.copy(userSubscribeStatus = subscribeStatus) } - fun openAnswer() { - _uiState.value = _uiState.value.copy( - hint = _uiState.value.hint.copy(answerOpened = true) - ) - } - @AssistedFactory interface Factory { fun create(gameSharedViewModel: GameSharedViewModel): HintViewModel diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/compose/HintScreen.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/compose/HintScreen.kt index c192af1..bf07299 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/compose/HintScreen.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/compose/HintScreen.kt @@ -17,24 +17,15 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.blur -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -50,30 +41,18 @@ import com.nextroom.nextroom.presentation.extension.throttleClick import com.nextroom.nextroom.presentation.extension.toTimerFormat import com.nextroom.nextroom.presentation.model.Hint import com.nextroom.nextroom.presentation.ui.hint.HintState -import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable fun HintScreen( state: HintState, - onAnswerButtonClick: () -> Unit, onHintImageClick: (Int) -> Unit, onAnswerImageClick: (Int) -> Unit, onHintOpenClick: () -> Unit, + onAnswerOpenClick: () -> Unit, modifier: Modifier = Modifier ) { val listState = rememberLazyListState() - val coroutineScope = rememberCoroutineScope() - var hasScrolledToAnswer by rememberSaveable { mutableStateOf(false) } - - LaunchedEffect(state.hint.answerOpened) { - if (state.hint.answerOpened && !hasScrolledToAnswer) { - coroutineScope.launch { - listState.animateScrollToItem(index = 1) - hasScrolledToAnswer = true - } - } - } Box(modifier = modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) { @@ -198,7 +177,7 @@ fun HintScreen( } } - if (state.hint.answerOpened) { + if (state.isHintOpened) { item { Column( modifier = Modifier.fillMaxWidth(), @@ -216,73 +195,90 @@ fun HintScreen( modifier = Modifier.fillMaxWidth() ) - if (state.hint.answerImageUrlList.isNotEmpty()) { - ImagePager( - imageUrls = state.hint.answerImageUrlList, - subscribeStatus = state.userSubscribeStatus, - networkDisconnectedCount = state.networkDisconnectedCount, - onImageClick = onAnswerImageClick, - modifier = Modifier.padding(top = 12.dp, bottom = 20.dp) - ) - } - - Text( - text = state.hint.answer, - style = NRTypo.Pretendard.size20, - color = NRColor.Gray01, + Box( modifier = Modifier .fillMaxWidth() + .wrapContentHeight() + .heightIn(min = if (state.isAnswerOpened) 0.dp else 200.dp) .padding(top = 12.dp) - ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .blur(if (state.isAnswerOpened) 0.dp else 10.dp) + ) { + if (state.hint.answerImageUrlList.isNotEmpty() && state.isAnswerOpened) { + ImagePager( + imageUrls = state.hint.answerImageUrlList, + subscribeStatus = state.userSubscribeStatus, + networkDisconnectedCount = state.networkDisconnectedCount, + onImageClick = onAnswerImageClick, + modifier = Modifier.padding(bottom = 20.dp) + ) + } - Spacer(modifier = Modifier.height(200.dp)) + Text( + text = state.hint.answer, + style = NRTypo.Pretendard.size20, + color = NRColor.Gray01, + modifier = Modifier.fillMaxWidth() + ) + } + + if (!state.isAnswerOpened) { + Box( + modifier = Modifier + .matchParentSize() + .background(NRColor.Black.copy(alpha = 0.1f)) + .throttleClick { onAnswerOpenClick() }, + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(horizontal = 20.dp) + ) { + Image( + modifier = modifier.size(20.dp), + painter = painterResource(R.drawable.ic_lock), + colorFilter = ColorFilter.tint(NRColor.White), + contentDescription = null, + ) + Text( + text = stringResource(R.string.text_open_answer_guide_message), + color = NRColor.White, + style = NRTypo.Pretendard.size14SemiBold, + modifier = modifier + .padding(top = 10.dp) + .throttleClick { onAnswerOpenClick() } + ) + Text( + text = stringResource(R.string.game_view_answer), + color = NRColor.Black, + style = NRTypo.Pretendard.size16Bold, + modifier = modifier + .padding(top = 20.dp) + .background( + color = NRColor.White, + shape = RoundedCornerShape(8.dp) + ) + .padding(vertical = 8.dp, horizontal = 12.dp) + ) + } + } + } + } + + Spacer(modifier = Modifier.height(80.dp)) } } } else { item { - Spacer(modifier = Modifier.height(200.dp)) + Spacer(modifier = Modifier.height(80.dp)) } } } } - Box( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .height(200.dp) - .background( - Brush.verticalGradient( - colors = listOf( - Color.Transparent, - NRColor.Black - ) - ) - ), - contentAlignment = Alignment.BottomCenter - ) { - Button( - onClick = onAnswerButtonClick, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 40.dp) - .height(56.dp), - colors = ButtonDefaults.buttonColors( - containerColor = NRColor.White - ) - ) { - Text( - text = if (state.hint.answerOpened) { - stringResource(R.string.game_hint_button_goto_home) - } else { - stringResource(R.string.game_hint_button_show_answer) - }, - color = NRColor.Black, - style = NRTypo.Pretendard.size16Bold - ) - } - } - NRLoading(isVisible = state.loading) } } @@ -306,10 +302,10 @@ private fun HintScreenWithNoImagesPreview() { networkDisconnectedCount = 0, isHintOpened = true ), - onAnswerButtonClick = {}, onHintImageClick = {}, onAnswerImageClick = {}, - onHintOpenClick = {} + onHintOpenClick = {}, + onAnswerOpenClick = {} ) } @@ -335,10 +331,10 @@ private fun HintScreenWithImagesPreview() { networkDisconnectedCount = 0, isHintOpened = false ), - onAnswerButtonClick = {}, onHintImageClick = {}, onAnswerImageClick = {}, - onHintOpenClick = {} + onHintOpenClick = {}, + onAnswerOpenClick = {} ) } @@ -359,12 +355,13 @@ private fun HintScreenAnswerOpenedPreview() { ), userSubscribeStatus = SubscribeStatus.Subscribed, networkDisconnectedCount = 0, - isHintOpened = true + isHintOpened = true, + isAnswerOpened = true ), - onAnswerButtonClick = {}, onHintImageClick = {}, onAnswerImageClick = {}, - onHintOpenClick = {} + onHintOpenClick = {}, + onAnswerOpenClick = {} ) } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt index 0665051..b3df14e 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt @@ -29,6 +29,9 @@ class GameSharedViewModel @Inject constructor( private val _openedHintIds = MutableStateFlow>(emptySet()) val openedHintIds: StateFlow> = _openedHintIds.asStateFlow() + private val _openedAnswerIds = MutableStateFlow>(emptySet()) + val openedAnswerIds: StateFlow> = _openedAnswerIds.asStateFlow() + fun setCurrentHint(hint: Hint) { _currentHint.value = hint } @@ -49,4 +52,8 @@ class GameSharedViewModel @Inject constructor( } fun getOpenedHintCount() = openedHintIds.value.size + + fun addOpenedAnswerId(hintId: Int) { + _openedAnswerIds.value += hintId + } } \ No newline at end of file diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 33ae909..b60f96b 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -83,6 +83,8 @@ 종료하시면 모든 기록은 초기화됩니다. 게임이 종료되었습니다 힌트 보기 + 정답 보기 + 정답을 보려면 클릭하세요! 힌트 코드를 잘못 입력했습니다 사용 가능한 힌트 개수를 초과했습니다. Hint Code From d1d05e0259ea4f19b59832b442c88c06c55f9b92 Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sun, 28 Dec 2025 21:37:17 +0900 Subject: [PATCH 07/11] =?UTF-8?q?NR-130=20=ED=9E=8C=ED=8A=B8=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=ED=9A=9F=EC=88=98=20=EC=A0=9C=ED=95=9C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=84=20=ED=9E=8C=ED=8A=B8=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/hint/HintEvent.kt | 1 + .../presentation/ui/hint/HintFragment.kt | 3 ++- .../presentation/ui/hint/HintState.kt | 3 ++- .../presentation/ui/hint/HintViewModel.kt | 19 ++++++++++++++++--- .../ui/main/GameSharedViewModel.kt | 7 +++++++ .../presentation/ui/main/TimerEvent.kt | 1 - .../presentation/ui/main/TimerFragment.kt | 1 - .../presentation/ui/main/TimerViewModel.kt | 10 ++-------- 8 files changed, 30 insertions(+), 15 deletions(-) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintEvent.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintEvent.kt index 3e9ecb7..4888f2f 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintEvent.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintEvent.kt @@ -4,4 +4,5 @@ sealed interface HintEvent { data object NetworkError : HintEvent data object UnknownError : HintEvent data class ClientError(val message: String) : HintEvent + data object HintLimitExceed : HintEvent } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt index df069ec..11ab96e 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt @@ -68,7 +68,7 @@ class HintFragment : ComposeBaseViewModelFragment() { state = state, onHintImageClick = ::navigateToHintImageViewer, onAnswerImageClick = ::navigateToAnswerImageViewer, - onHintOpenClick = { gameSharedViewModel.addOpenedHintId(state.hint.id) }, + onHintOpenClick = { viewModel.tryOpenHint(state.hint.id) }, onAnswerOpenClick = { gameSharedViewModel.addOpenedAnswerId(state.hint.id) } ) } @@ -139,6 +139,7 @@ class HintFragment : ComposeBaseViewModelFragment() { is HintEvent.NetworkError -> snackbar(R.string.error_network) is HintEvent.UnknownError -> snackbar(R.string.error_something) is HintEvent.ClientError -> snackbar(event.message) + is HintEvent.HintLimitExceed -> snackbar(R.string.game_hint_limit_exceed) } } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintState.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintState.kt index 086aa9b..b3a9863 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintState.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintState.kt @@ -9,5 +9,6 @@ data class HintState( val userSubscribeStatus: SubscribeStatus = SubscribeStatus.Default, val networkDisconnectedCount: Int = 0, val isHintOpened: Boolean = false, - val isAnswerOpened: Boolean = false + val isAnswerOpened: Boolean = false, + val totalHintCount: Int = -1 ) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt index 0279e1d..5933d7e 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt @@ -27,11 +27,13 @@ class HintViewModel @AssistedInject constructor( val uiState = combine( _uiState, gameSharedViewModel.openedHintIds, - gameSharedViewModel.openedAnswerIds - ) { state, openedHintIds, openedAnswerIds -> + gameSharedViewModel.openedAnswerIds, + gameSharedViewModel.totalHintCount + ) { state, openedHintIds, openedAnswerIds, totalHintCount -> state.copy( isHintOpened = state.hint.id in openedHintIds, - isAnswerOpened = state.hint.id in openedAnswerIds + isAnswerOpened = state.hint.id in openedAnswerIds, + totalHintCount = totalHintCount ) }.stateIn( baseViewModelScope, @@ -68,6 +70,17 @@ class HintViewModel @AssistedInject constructor( _uiState.value = _uiState.value.copy(userSubscribeStatus = subscribeStatus) } + fun tryOpenHint(hintId: Int) { + val openedCount = gameSharedViewModel.getOpenedHintCount() + val openableHintCount = uiState.value.totalHintCount + + if (openableHintCount == -1 || openedCount < openableHintCount) { + gameSharedViewModel.addOpenedHintId(hintId) + } else { + _uiEvent.tryEmit(HintEvent.HintLimitExceed) + } + } + @AssistedFactory interface Factory { fun create(gameSharedViewModel: GameSharedViewModel): HintViewModel diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt index b3df14e..ae19902 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt @@ -32,10 +32,17 @@ class GameSharedViewModel @Inject constructor( private val _openedAnswerIds = MutableStateFlow>(emptySet()) val openedAnswerIds: StateFlow> = _openedAnswerIds.asStateFlow() + private val _totalHintCount = MutableStateFlow(-1) + val totalHintCount: StateFlow = _totalHintCount.asStateFlow() + fun setCurrentHint(hint: Hint) { _currentHint.value = hint } + fun setTotalHintCount(count: Int) { + _totalHintCount.value = count + } + fun addOpenedHintId(hintId: Int) { _openedHintIds.value += hintId baseViewModelScope.launch { diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerEvent.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerEvent.kt index 47d8e38..8137413 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerEvent.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerEvent.kt @@ -10,6 +10,5 @@ sealed interface TimerEvent { data object TimerFinish : TimerEvent data object ClearHintCode : TimerEvent - data object ShowAvailableHintExceedError : TimerEvent data object NewTimer : TimerEvent } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerFragment.kt index dcc9139..1d417da 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerFragment.kt @@ -294,7 +294,6 @@ class TimerFragment : BaseFragment(FragmentTimerBinding::i is TimerEvent.TimerFinish -> snackbar(R.string.game_finished) - TimerEvent.ShowAvailableHintExceedError -> snackbar(message = getString(R.string.game_hint_limit_exceed)) TimerEvent.NewTimer -> { gameStartConfirmDialog?.show(parentFragmentManager, "GameStartConfirmDialog") } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt index b9450ce..488c1e6 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt @@ -229,14 +229,7 @@ class TimerViewModel @AssistedInject constructor( } hintRepository.getHint(state.currentInput)?.let { hint -> - if (gameSharedViewModel.hasOpenedHint(hint.id)) { - openHint(hint) - } else if (gameSharedViewModel.getOpenedHintCount() < state.totalHintCount) { - openHint(hint) - } else { - postSideEffect(TimerEvent.ShowAvailableHintExceedError) - reduce { state.copy(inputState = InputState.Typing, currentInput = "") } - } + openHint(hint) } ?: run { reduce { state.copy(inputState = InputState.Error(R.string.game_wrong_hint_code)) } delay(500) @@ -269,6 +262,7 @@ class TimerViewModel @AssistedInject constructor( themeImageEnabled: Boolean, ) = intent { gameSharedViewModel.updateOpenedHintIds(usedHints) + gameSharedViewModel.setTotalHintCount(hintLimit) reduce { state.copy( totalSeconds = seconds, From 9437731b5f6d2156127998d6a6241ce36013fa29 Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sun, 28 Dec 2025 21:51:47 +0900 Subject: [PATCH 08/11] =?UTF-8?q?NR-130=20GameSharedViewModel=EC=9D=98=20f?= =?UTF-8?q?low=EB=A5=BC=20=ED=95=98=EB=82=98=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 왜? 상태 관리를 하는 Flow를 하나로 두어 관리를 용이하게 한다. --- .../presentation/ui/hint/HintFragment.kt | 10 ++-- .../presentation/ui/hint/HintViewModel.kt | 12 ++--- .../ui/main/GameSharedViewModel.kt | 47 +++++++++---------- .../presentation/ui/main/TimerViewModel.kt | 8 ++-- 4 files changed, 35 insertions(+), 42 deletions(-) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt index 11ab96e..539f723 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt @@ -86,13 +86,9 @@ class HintFragment : ComposeBaseViewModelFragment() { override fun initSubscribe() { viewLifecycleOwner.repeatOnStarted { launch { - gameSharedViewModel.currentHint.collect { hint -> - hint?.let { viewModel.setHint(it) } - } - } - launch { - gameSharedViewModel.subscribeStatus.collect { subscribeStatus -> - viewModel.setSubscribeStatus(subscribeStatus) + gameSharedViewModel.state.collect { gameSharedState -> + gameSharedState.currentHint?.let { viewModel.setHint(it) } + viewModel.setSubscribeStatus(gameSharedState.subscribeStatus) } } launch { diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt index 5933d7e..bad6fdc 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt @@ -26,14 +26,12 @@ class HintViewModel @AssistedInject constructor( private val _uiState = MutableStateFlow(HintState()) val uiState = combine( _uiState, - gameSharedViewModel.openedHintIds, - gameSharedViewModel.openedAnswerIds, - gameSharedViewModel.totalHintCount - ) { state, openedHintIds, openedAnswerIds, totalHintCount -> + gameSharedViewModel.state + ) { state, gameSharedState -> state.copy( - isHintOpened = state.hint.id in openedHintIds, - isAnswerOpened = state.hint.id in openedAnswerIds, - totalHintCount = totalHintCount + isHintOpened = state.hint.id in gameSharedState.openedHintIds, + isAnswerOpened = state.hint.id in gameSharedState.openedAnswerIds, + totalHintCount = gameSharedState.totalHintCount ) }.stateIn( baseViewModelScope, diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt index ae19902..8276d83 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt @@ -9,6 +9,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -18,49 +19,47 @@ class GameSharedViewModel @Inject constructor( private val gameStateRepository: GameStateRepository ) : NewBaseViewModel() { - private val _subscribeStatus = MutableStateFlow( - TimerFragmentArgs.fromSavedStateHandle(savedStateHandle).subscribeStatus + private val _state = MutableStateFlow( + GameSharedState( + subscribeStatus = TimerFragmentArgs.fromSavedStateHandle(savedStateHandle).subscribeStatus + ) ) - val subscribeStatus: StateFlow = _subscribeStatus.asStateFlow() - - private val _currentHint = MutableStateFlow(null) - val currentHint: StateFlow = _currentHint.asStateFlow() - - private val _openedHintIds = MutableStateFlow>(emptySet()) - val openedHintIds: StateFlow> = _openedHintIds.asStateFlow() - - private val _openedAnswerIds = MutableStateFlow>(emptySet()) - val openedAnswerIds: StateFlow> = _openedAnswerIds.asStateFlow() - - private val _totalHintCount = MutableStateFlow(-1) - val totalHintCount: StateFlow = _totalHintCount.asStateFlow() + val state: StateFlow = _state.asStateFlow() fun setCurrentHint(hint: Hint) { - _currentHint.value = hint + _state.update { it.copy(currentHint = hint) } } fun setTotalHintCount(count: Int) { - _totalHintCount.value = count + _state.update { it.copy(totalHintCount = count) } } fun addOpenedHintId(hintId: Int) { - _openedHintIds.value += hintId + _state.update { it.copy(openedHintIds = it.openedHintIds + hintId) } baseViewModelScope.launch { - gameStateRepository.updateUsedHints(_openedHintIds.value) + gameStateRepository.updateUsedHints(_state.value.openedHintIds) } } fun updateOpenedHintIds(hintIds: Set) { - _openedHintIds.value = hintIds + _state.update { it.copy(openedHintIds = hintIds) } } fun hasOpenedHint(hintId: Int): Boolean { - return openedHintIds.value.contains(hintId) + return state.value.openedHintIds.contains(hintId) } - fun getOpenedHintCount() = openedHintIds.value.size + fun getOpenedHintCount() = state.value.openedHintIds.size fun addOpenedAnswerId(hintId: Int) { - _openedAnswerIds.value += hintId + _state.update { it.copy(openedAnswerIds = it.openedAnswerIds + hintId) } } -} \ No newline at end of file +} + +data class GameSharedState( + val subscribeStatus: SubscribeStatus, + val currentHint: Hint? = null, + val openedHintIds: Set = emptySet(), + val openedAnswerIds: Set = emptySet(), + val totalHintCount: Int = -1 +) \ No newline at end of file diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt index 488c1e6..b1a91c9 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt @@ -41,9 +41,9 @@ class TimerViewModel @AssistedInject constructor( val uiState = combine( container.stateFlow, - gameSharedViewModel.openedHintIds - ) { state, openedHintIds -> - state.copy(openedHintCount = openedHintIds.size) + gameSharedViewModel.state + ) { state, gameSharedState -> + state.copy(openedHintCount = gameSharedState.openedHintIds.size) }.stateIn( baseViewModelScope, SharingStarted.Lazily, @@ -213,7 +213,7 @@ class TimerViewModel @AssistedInject constructor( progress = hint.progress, hint = hint.description, answer = hint.answer, - answerOpened = gameSharedViewModel.openedHintIds.value.contains(hint.id), + answerOpened = gameSharedViewModel.state.value.openedHintIds.contains(hint.id), hintImageUrlList = hint.hintImageUrlList.toList(), answerImageUrlList = hint.answerImageUrlList.toList() ) From b1925af426c2dc8e21feed58c4c1fa443f49e274 Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sun, 28 Dec 2025 21:59:13 +0900 Subject: [PATCH 09/11] =?UTF-8?q?NR-130=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextroom/presentation/ui/main/GameSharedViewModel.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt index 8276d83..f52e58a 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/GameSharedViewModel.kt @@ -45,10 +45,6 @@ class GameSharedViewModel @Inject constructor( _state.update { it.copy(openedHintIds = hintIds) } } - fun hasOpenedHint(hintId: Int): Boolean { - return state.value.openedHintIds.contains(hintId) - } - fun getOpenedHintCount() = state.value.openedHintIds.size fun addOpenedAnswerId(hintId: Int) { From 42326688d78ec050f715ad1e0dd7404afe09fb36 Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sun, 28 Dec 2025 22:21:37 +0900 Subject: [PATCH 10/11] =?UTF-8?q?NR-130=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=95=84=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 왜? 정답을 본적이 있는지 여부는 gameSharedViewModel에서 따로 관리하기로 했음. 힌트, 타이머 화면에서 들고있을 필요가 없기 때문에 Hint 데이터 클래스에서 제거한다. --- .../main/java/com/nextroom/nextroom/presentation/model/Hint.kt | 1 - .../nextroom/nextroom/presentation/ui/hint/HintViewModel.kt | 2 +- .../nextroom/presentation/ui/hint/compose/HintScreen.kt | 3 --- .../nextroom/nextroom/presentation/ui/main/TimerViewModel.kt | 1 - 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/model/Hint.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/model/Hint.kt index 7a1de7d..436b36e 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/model/Hint.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/model/Hint.kt @@ -7,7 +7,6 @@ data class Hint( val progress: Int = 0, val hint: String = "", val answer: String = "", - val answerOpened: Boolean = false, val hintImageUrlList: List = emptyList(), val answerImageUrlList: List = emptyList() ) : Serializable diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt index bad6fdc..ec99edb 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt @@ -60,7 +60,7 @@ class HintViewModel @AssistedInject constructor( fun setHint(hint: Hint) { _uiState.value = _uiState.value.copy( - hint = hint.copy(answerOpened = _uiState.value.hint.answerOpened) + hint = hint ) } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/compose/HintScreen.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/compose/HintScreen.kt index bf07299..be52fc4 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/compose/HintScreen.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/compose/HintScreen.kt @@ -294,7 +294,6 @@ private fun HintScreenWithNoImagesPreview() { progress = 45, hint = "이 힌트는 미로 출구를 찾는 데 도움이 됩니다. 벽의 패턴을 주의 깊게 살펴보세요.", answer = "미로의 출구는 북쪽 벽의 세 번째 문입니다. 빨간색 표시를 따라가세요.", - answerOpened = false, hintImageUrlList = emptyList(), answerImageUrlList = emptyList() ), @@ -320,7 +319,6 @@ private fun HintScreenWithImagesPreview() { progress = 45, hint = "이 힌트는 미로 출구를 찾는 데 도움이 됩니다. 벽의 패턴을 주의 깊게 살펴보세요.", answer = "미로의 출구는 북쪽 벽의 세 번째 문입니다. 빨간색 표시를 따라가세요.", - answerOpened = false, hintImageUrlList = listOf( "https://example.com/hint1.jpg", "https://example.com/hint2.jpg" @@ -349,7 +347,6 @@ private fun HintScreenAnswerOpenedPreview() { progress = 85, hint = "이 힌트는 미로 출구를 찾는 데 도움이 됩니다. 벽의 패턴을 주의 깊게 살펴보세요.", answer = "미로의 출구는 북쪽 벽의 세 번째 문입니다. 빨간색 표시를 따라가세요.", - answerOpened = true, hintImageUrlList = emptyList(), answerImageUrlList = emptyList(), ), diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt index b1a91c9..48ad8c2 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/main/TimerViewModel.kt @@ -213,7 +213,6 @@ class TimerViewModel @AssistedInject constructor( progress = hint.progress, hint = hint.description, answer = hint.answer, - answerOpened = gameSharedViewModel.state.value.openedHintIds.contains(hint.id), hintImageUrlList = hint.hintImageUrlList.toList(), answerImageUrlList = hint.answerImageUrlList.toList() ) From df78e0b36dc51e3840663678b7cb4da7abd066a3 Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sun, 28 Dec 2025 23:05:07 +0900 Subject: [PATCH 11/11] =?UTF-8?q?NR-130=20HintViewModel=EC=97=90=EC=84=9C?= =?UTF-8?q?=20SharedViewModel=EC=9D=98=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=94=EB=A1=9C=20=EA=BA=BC=EB=82=B4=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 왜? 기존에는 Fragment가 collect해서 세팅해주었으나 assistedInject 받았으므로 바로 값을 꺼내 사용하도록 해준다. --- .../presentation/ui/hint/HintFragment.kt | 6 ------ .../presentation/ui/hint/HintViewModel.kt | 18 ++++-------------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt index 539f723..bc406f7 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintFragment.kt @@ -85,12 +85,6 @@ class HintFragment : ComposeBaseViewModelFragment() { override fun initSubscribe() { viewLifecycleOwner.repeatOnStarted { - launch { - gameSharedViewModel.state.collect { gameSharedState -> - gameSharedState.currentHint?.let { viewModel.setHint(it) } - viewModel.setSubscribeStatus(gameSharedState.subscribeStatus) - } - } launch { viewModel.uiEvent.collect(::handleEvent) } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt index ec99edb..a7d2d8c 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/hint/HintViewModel.kt @@ -1,10 +1,8 @@ package com.nextroom.nextroom.presentation.ui.hint -import com.nextroom.nextroom.domain.model.SubscribeStatus import com.nextroom.nextroom.domain.repository.DataStoreRepository import com.nextroom.nextroom.domain.repository.TimerRepository import com.nextroom.nextroom.presentation.base.NewBaseViewModel -import com.nextroom.nextroom.presentation.model.Hint import com.nextroom.nextroom.presentation.ui.main.GameSharedViewModel import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -29,8 +27,10 @@ class HintViewModel @AssistedInject constructor( gameSharedViewModel.state ) { state, gameSharedState -> state.copy( - isHintOpened = state.hint.id in gameSharedState.openedHintIds, - isAnswerOpened = state.hint.id in gameSharedState.openedAnswerIds, + hint = gameSharedState.currentHint ?: state.hint, + userSubscribeStatus = gameSharedState.subscribeStatus, + isHintOpened = (gameSharedState.currentHint?.id ?: state.hint.id) in gameSharedState.openedHintIds, + isAnswerOpened = (gameSharedState.currentHint?.id ?: state.hint.id) in gameSharedState.openedAnswerIds, totalHintCount = gameSharedState.totalHintCount ) }.stateIn( @@ -58,16 +58,6 @@ class HintViewModel @AssistedInject constructor( _uiState.value = _uiState.value.copy(networkDisconnectedCount = count) } - fun setHint(hint: Hint) { - _uiState.value = _uiState.value.copy( - hint = hint - ) - } - - fun setSubscribeStatus(subscribeStatus: SubscribeStatus) { - _uiState.value = _uiState.value.copy(userSubscribeStatus = subscribeStatus) - } - fun tryOpenHint(hintId: Int) { val openedCount = gameSharedViewModel.getOpenedHintCount() val openableHintCount = uiState.value.totalHintCount