UI 변경, 광고 나오는 타임 변경 -> 30초 이후 클리어시 에서 -> 5분 타이머가 만료되는 스테이지 클리어 시.

This commit is contained in:
2026-09-24 03:31:06 +09:00
parent 4de74a75e4
commit ff9e386b0e
410 changed files with 17688 additions and 17648 deletions
@@ -29,6 +29,7 @@ class UserPreferencesManager(private val context: Context) {
private val KEY_SOUND_ENABLED = booleanPreferencesKey("sound_enabled")
private val KEY_VIBRATION_ENABLED = booleanPreferencesKey("vibration_enabled")
private val KEY_PRE_LEARNED_WORDS = stringSetPreferencesKey("pre_learned_words")
private val KEY_ACCUMULATED_PUZZLE_TIME_SEC = longPreferencesKey("accumulated_puzzle_time_sec")
}
private fun getTodayDateString(): String {
@@ -158,4 +159,30 @@ class UserPreferencesManager(private val context: Context) {
preferences[KEY_PRE_LEARNED_WORDS] = currentSet
}
}
/**
* 활성 퍼즐 풀이 시간을 1초 추가합니다.
*/
suspend fun addPuzzleTime(addedSeconds: Long = 1L) {
context.dataStore.edit { preferences ->
val current = preferences[KEY_ACCUMULATED_PUZZLE_TIME_SEC] ?: 0L
preferences[KEY_ACCUMULATED_PUZZLE_TIME_SEC] = current + addedSeconds
}
}
/**
* 누적 퍼즐 풀이 시간이 5분(300초) 이상인지 확인합니다.
* 300초 이상일 경우 true를 반환하고, 새 카운트를 위해 누적 시간을 0초로 초기화합니다.
*/
suspend fun checkAndResetAdPuzzleTime(): Boolean {
var shouldShowAd = false
context.dataStore.edit { preferences ->
val current = preferences[KEY_ACCUMULATED_PUZZLE_TIME_SEC] ?: 0L
if (current >= 300L) { // 5분 = 300초
preferences[KEY_ACCUMULATED_PUZZLE_TIME_SEC] = 0L
shouldShowAd = true
}
}
return shouldShowAd
}
}
@@ -64,24 +64,14 @@ fun GameScreen(
}
}
// 클리어 이벤트 수신: 상단 메시지 토스트 출력 후 다음 스테이지 전환
// 클리어 이벤트 수신: 누적 퍼즐 풀이 시간이 5분(300초) 이상이면 다음 스테이지 진입 전 전체화면 광고 노출 후 5분 카운트 리셋
LaunchedEffect(Unit) {
viewModel.stageClearEvent.collect { event ->
if (event.isLastStage) {
ToastUtils.showTopToast(context, "🎉 모든 스테이지를 클리어하셨습니다!")
onNavigateBack()
} else if (event.isFastClear) {
ToastUtils.showTopToast(
context,
"⚡ ${event.elapsedSeconds}초 만에 빠른 클리어! (광고 없이 통과)"
)
onNavigateToNextStage(event.nextStageNumber)
} else {
ToastUtils.showTopToast(
context,
"클리어! (소요 시간: ${event.elapsedSeconds}초)"
)
} else if (event.shouldShowAd) {
ToastUtils.showTopToast(context, "🎉 클리어! (소요 시간: ${event.elapsedSeconds}초)")
if (activity != null) {
AdMobManager.showInterstitialAd(
activity = activity,
@@ -91,6 +81,9 @@ fun GameScreen(
} else {
onNavigateToNextStage(event.nextStageNumber)
}
} else {
ToastUtils.showTopToast(context, "🎉 클리어! (소요 시간: ${event.elapsedSeconds}초)")
onNavigateToNextStage(event.nextStageNumber)
}
}
}
@@ -11,7 +11,9 @@ import com.wordpuzzle.hangul.data.repository.StageRepository
import com.wordpuzzle.hangul.data.repository.WordRepository
import com.wordpuzzle.hangul.domain.engine.TilePoolGenerator
import com.wordpuzzle.hangul.domain.usecase.SolveTileUseCase
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
data class GameUiState(
@@ -33,7 +35,7 @@ data class StageClearEvent(
val nextStageNumber: Int,
val isLastStage: Boolean,
val elapsedSeconds: Long,
val isFastClear: Boolean
val shouldShowAd: Boolean = false
)
class GameViewModel(application: Application) : AndroidViewModel(application) {
@@ -73,6 +75,26 @@ class GameViewModel(application: Application) : AndroidViewModel(application) {
}
}
}
// 5분(300초) 순수 퍼즐 풀이 시간 초 단위 누적 타이머 (게임 도중 팝업 없이 누적만 수행)
viewModelScope.launch {
while (isActive) {
delay(1000L)
if (isActivelyPlayingPuzzle()) {
prefs.addPuzzleTime(1L)
}
}
}
}
private fun isActivelyPlayingPuzzle(): Boolean {
val state = _uiState.value
return !isTimerPaused &&
state.stage != null &&
!state.isLoading &&
!state.isStageClear &&
!state.isStageFailed &&
!state.showHintDialog
}
fun loadStage(stageNumber: Int) {
@@ -322,7 +344,6 @@ class GameViewModel(application: Application) : AndroidViewModel(application) {
private fun handleStageClear(stage: CrosswordStage) {
viewModelScope.launch {
val elapsedSeconds = getEffectiveElapsedSeconds()
val isFastClear = elapsedSeconds <= 30L
prefs.setStageCleared(stage.stageNumber)
for (clue in stage.clues) {
@@ -332,13 +353,16 @@ class GameViewModel(application: Application) : AndroidViewModel(application) {
val totalStages = stageRepo.getTotalStageCount()
val isLastStage = stage.stageNumber >= totalStages
// 누적 퍼즐 풀이 시간이 5분(300초) 이상인지 검증하고, 이상일 경우 카운트 0초 초기화 후 광고 노출 플래그 설정
val shouldShowAd = prefs.checkAndResetAdPuzzleTime()
_stageClearEvent.emit(
StageClearEvent(
stageNumber = stage.stageNumber,
nextStageNumber = stage.stageNumber + 1,
isLastStage = isLastStage,
elapsedSeconds = elapsedSeconds,
isFastClear = isFastClear
shouldShowAd = shouldShowAd
)
)
}