From c22cfafe880c7299352a89ba4821eeb8fb590b59 Mon Sep 17 00:00:00 2001 From: sanjeok77 Date: Wed, 4 Mar 2026 01:45:16 +0900 Subject: [PATCH] Add version management and update check system - Add version.json for remote version check - Add VersionManager utility - Show update dialog on app start - Add version info in settings screen - Version format: x.y.z (1.0.0) - versionCode increments by 1 --- .../alarm/presentation/main/MainActivity.kt | 123 ++++- .../presentation/settings/SettingsScreen.kt | 429 +++++------------- .../com/hotdeal/alarm/util/VersionManager.kt | 114 +++++ version.json | 14 + 4 files changed, 357 insertions(+), 323 deletions(-) create mode 100644 app/src/main/java/com/hotdeal/alarm/util/VersionManager.kt create mode 100644 version.json diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/main/MainActivity.kt b/app/src/main/java/com/hotdeal/alarm/presentation/main/MainActivity.kt index 25ebe28..6d91405 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/main/MainActivity.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/main/MainActivity.kt @@ -1,17 +1,37 @@ package com.hotdeal.alarm.presentation.main +import android.content.Intent +import android.net.Uri import android.os.Bundle +import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.ui.unit.dp +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.lifecycleScope import com.hotdeal.alarm.ui.theme.HotDealTheme +import com.hotdeal.alarm.util.UpdateInfo +import com.hotdeal.alarm.util.VersionManager import com.hotdeal.alarm.worker.WorkerScheduler import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint @@ -24,7 +44,8 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) enableEdgeToEdge() - workerScheduler.schedulePeriodicPolling(15) + // 2분 간격으로 폴� 시작 + workerScheduler.schedulePeriodicPolling(2) setContent { HotDealTheme { @@ -33,9 +54,109 @@ class MainActivity : ComponentActivity() { color = MaterialTheme.colorScheme.background ) { val viewModel: MainViewModel = hiltViewModel() + + // 업데이트 체크 + var updateInfo by remember { mutableStateOf(null) } + var showUpdateDialog by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + checkForUpdate { info -> + updateInfo = info + showUpdateDialog = true + } + } + MainScreen(viewModel = viewModel) + + // 업데이트 다이얼로그 + if (showUpdateDialog && updateInfo != null) { + UpdateDialog( + updateInfo = updateInfo!!, + onDismiss = { showUpdateDialog = false }, + onUpdate = { + openUpdateUrl(updateInfo!!.updateUrl) + showUpdateDialog = false + } + ) + } } } } } + + /** + * 업데이트 체크 + */ + private fun checkForUpdate(onUpdateAvailable: (UpdateInfo) -> Unit) { + lifecycleScope.launch { + try { + val currentVersionCode = VersionManager.getCurrentVersionCode(this@MainActivity) + val remoteInfo = VersionManager.checkForUpdate() + + if (remoteInfo != null && VersionManager.isUpdateAvailable(currentVersionCode, remoteInfo.versionCode)) { + // 토스트로 알림 + Toast.makeText( + this@MainActivity, + "새로운 버전 ${remoteInfo.version}이(가) 있습니다", + Toast.LENGTH_LONG + ).show() + + // 다이얼로그 표시 + onUpdateAvailable(remoteInfo) + } + } catch (e: Exception) { + // 업데이트 체크 실패 시 무시 + } + } + } + + /** + * 업데이트 URL 열기 + */ + private fun openUpdateUrl(url: String) { + try { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + startActivity(intent) + } catch (e: Exception) { + Toast.makeText(this, "브라우저를 열 수 없습니다", Toast.LENGTH_SHORT).show() + } + } +} + +/** + * 업데이트 다이얼로그 + */ +@Composable +fun UpdateDialog( + updateInfo: UpdateInfo, + onDismiss: () -> Unit, + onUpdate: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("업데이트 가능") }, + text = { + androidx.compose.foundation.layout.Column { + Text("새로운 버전 ${updateInfo.version}이(가) 출시되었습니다.") + androidx.compose.foundation.layout.Spacer(modifier = androidx.compose.ui.Modifier.height(8.dp)) + Text( + "변경사항:", + style = MaterialTheme.typography.labelMedium + ) + updateInfo.changelog.take(3).forEach { change -> + Text("• $change", style = MaterialTheme.typography.bodySmall) + } + } + }, + confirmButton = { + Button(onClick = onUpdate) { + Text("업데이트") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("나중에") + } + } + ) } diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt b/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt index 22dbd66..41a93cf 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt @@ -1,11 +1,10 @@ package com.hotdeal.alarm.presentation.settings import android.Manifest -import android.content.pm.PackageManager import android.os.Build +import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -15,11 +14,9 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.hotdeal.alarm.domain.model.Keyword import com.hotdeal.alarm.domain.model.SiteConfig @@ -28,13 +25,16 @@ import com.hotdeal.alarm.presentation.components.PermissionDialog import com.hotdeal.alarm.presentation.main.MainUiState import com.hotdeal.alarm.presentation.main.MainViewModel import com.hotdeal.alarm.util.PermissionHelper +import com.hotdeal.alarm.util.VersionManager import com.hotdeal.alarm.worker.WorkerScheduler +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen(viewModel: MainViewModel) { val context = LocalContext.current val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val scope = rememberCoroutineScope() var showPermissionDialog by remember { mutableStateOf(false) } var permissionDialogTitle by remember { mutableStateOf("") } @@ -46,7 +46,7 @@ fun SettingsScreen(viewModel: MainViewModel) { ) { isGranted -> if (!isGranted) { permissionDialogTitle = "알림 권한 필요" - permissionDialogMessage = "핫딜 알림을 받으려면 알림 권한이 필요합니다.\n설정에서 권한을 허용해주세요." + permissionDialogMessage = "핫딜 알림을 받으려면 알림 권한이 필요합니다." permissionDialogAction = { PermissionHelper.openNotificationSettings(context) } @@ -54,12 +54,9 @@ fun SettingsScreen(viewModel: MainViewModel) { } } - // 권한 상태 확인 val permissionStatus = PermissionHelper.checkAllPermissions(context) val hasEnabledSites = (uiState as? MainUiState.Success) ?.siteConfigs?.any { it.isEnabled } ?: false - val hasKeywords = (uiState as? MainUiState.Success) - ?.keywords?.isNotEmpty() ?: false LazyColumn( modifier = Modifier @@ -67,30 +64,21 @@ fun SettingsScreen(viewModel: MainViewModel) { .padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - // 권한 상태 섹션 item { PermissionStatusSection( permissionStatus = permissionStatus, hasEnabledSites = hasEnabledSites, - hasKeywords = hasKeywords, onRequestNotificationPermission = { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) } - }, - onRequestExactAlarmPermission = { - PermissionHelper.openExactAlarmSettings(context) - }, - onOpenAppSettings = { - PermissionHelper.openAppSettings(context) } ) } - // 폴� 주기 설정 item { PollingIntervalSection( - currentInterval = 2, // 기본 2분 + currentInterval = 2, onIntervalChange = { minutes -> viewModel.stopPolling() viewModel.startPolling(minutes) @@ -98,12 +86,8 @@ fun SettingsScreen(viewModel: MainViewModel) { ) } - // 사이트 선택 섹션 item { - Text( - text = "사이트 선택", - style = MaterialTheme.typography.titleMedium - ) + Text("사이트 선택", style = MaterialTheme.typography.titleMedium) } when (val state = uiState) { @@ -122,20 +106,11 @@ fun SettingsScreen(viewModel: MainViewModel) { item { Spacer(modifier = Modifier.height(16.dp)) - Text( - text = "키워드 설정", - style = MaterialTheme.typography.titleMedium - ) - Text( - text = "특정 키워드가 포함된 핫딜만 알림받으려면 키워드를 추가하세요", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + Text("키워드 설정", style = MaterialTheme.typography.titleMedium) } item { var keywordText by remember { mutableStateOf("") } - Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically @@ -169,20 +144,28 @@ fun SettingsScreen(viewModel: MainViewModel) { ) } - if (state.keywords.isEmpty()) { - item { - Text( - text = "등록된 키워드가 없습니다", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } + // 버전 정보 섹션 + item { + Spacer(modifier = Modifier.height(24.dp)) + VersionSection( + currentVersion = VersionManager.getCurrentVersion(context), + onCheckUpdate = { + scope.launch { + val currentCode = VersionManager.getCurrentVersionCode(context) + val remoteInfo = VersionManager.checkForUpdate() + + if (remoteInfo != null && VersionManager.isUpdateAvailable(currentCode, remoteInfo.versionCode)) { + Toast.makeText(context, "새 버전 ${remoteInfo.version} 사용 가능", Toast.LENGTH_LONG).show() + } else { + Toast.makeText(context, "최신 버전입니다", Toast.LENGTH_SHORT).show() + } + } + } + ) } } else -> { - item { - CircularProgressIndicator() - } + item { CircularProgressIndicator() } } } } @@ -192,82 +175,58 @@ fun SettingsScreen(viewModel: MainViewModel) { title = permissionDialogTitle, message = permissionDialogMessage, onDismiss = { showPermissionDialog = false }, - onOpenSettings = { - showPermissionDialog = false - permissionDialogAction() - } + onOpenSettings = { showPermissionDialog = false; permissionDialogAction() } ) } } @Composable -fun PollingIntervalSection( - currentInterval: Int, - onIntervalChange: (Long) -> Unit -) { - var selectedInterval by remember { mutableStateOf(currentInterval) } - - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f) - ) - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { +fun VersionSection(currentVersion: String, onCheckUpdate: () -> Unit) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Icon( - imageVector = Icons.Default.Schedule, - contentDescription = null, - tint = MaterialTheme.colorScheme.secondary - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = "폴� 주기 설정", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) + Column { + Text("앱 버전", style = MaterialTheme.typography.labelMedium) + Text("v$currentVersion", style = MaterialTheme.typography.titleMedium) + } + Button(onClick = onCheckUpdate) { + Text("업데이트 확인") + } } - - Text( - text = "핫딜을 확인하는 간격을 설정합니다.\n짧을수록 배터리와 데이터 소모가 증가합니다.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - // 간격 선택 - Column { - listOf(1, 2, 5, 10, 15, 30).forEach { minutes -> - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedInterval == minutes, - onClick = { - selectedInterval = minutes - onIntervalChange(minutes.toLong()) - } - ) - Text( - text = when (minutes) { - 1 -> "1분 (빠름 - 배터리 소모 큼)" - 2 -> "2분 (권장)" - 5 -> "5분 (보통)" - 10 -> "10분 (느림)" - 15 -> "15분 (매우 느림)" - 30 -> "30분 (절전)" - else -> "${minutes}분" - }, - style = MaterialTheme.typography.bodyMedium - ) - } + } + } +} + +@Composable +fun PollingIntervalSection(currentInterval: Int, onIntervalChange: (Long) -> Unit) { + var selected by remember { mutableStateOf(currentInterval) } + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Text("폴� 주기", style = MaterialTheme.typography.titleMedium) + Text("2분 권장 (배터리/데이터 절약)", style = MaterialTheme.typography.bodySmall) + Spacer(modifier = Modifier.height(8.dp)) + listOf(1, 2, 5, 10, 15, 30).forEach { minutes -> + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = selected == minutes, + onClick = { + selected = minutes + onIntervalChange(minutes.toLong()) + } + ) + Text(when(minutes) { + 1 -> "1분 (빠름)" + 2 -> "2분 (권장)" + 5 -> "5분 (보통)" + 10 -> "10분 (느림)" + 15 -> "15분 (매우 느림)" + 30 -> "30분 (절전)" + else -> "${minutes}분" + }) } } } @@ -278,217 +237,50 @@ fun PollingIntervalSection( fun PermissionStatusSection( permissionStatus: PermissionHelper.PermissionStatus, hasEnabledSites: Boolean, - hasKeywords: Boolean, - onRequestNotificationPermission: () -> Unit, - onRequestExactAlarmPermission: () -> Unit, - onOpenAppSettings: () -> Unit -) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = if (permissionStatus.isAllGranted && hasEnabledSites) { - MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) - } else { - MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f) - } - ) - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - text = "알림 설정 상태", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - - // 알림 권한 상태 - PermissionItem( - icon = Icons.Default.Notifications, - title = "알림 권한", - isGranted = permissionStatus.hasNotificationPermission, - requiredVersion = "Android 13+", - onClick = onRequestNotificationPermission - ) - - // 정확한 알람 권한 상태 - PermissionItem( - icon = Icons.Default.Alarm, - title = "정확한 알람 권한", - isGranted = permissionStatus.hasExactAlarmPermission, - requiredVersion = "Android 12+", - onClick = onRequestExactAlarmPermission - ) - - // 사이트 활성화 상태 - PermissionItem( - icon = Icons.Default.Language, - title = "사이트 선택", - isGranted = hasEnabledSites, - description = if (!hasEnabledSites) "최소 1개 사이트 활성화 필요" else null, - onClick = null - ) - - // 키워드 상태 (선택사항) - if (!hasKeywords) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Info, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = "키워드를 추가하면 해당 키워드가 포함된 핫딜만 알림받습니다", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - // 전체 상태 요약 - if (!permissionStatus.isAllGranted || !hasEnabledSites) { - HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .background( - MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.5f), - shape = MaterialTheme.shapes.small - ) - .padding(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Warning, - contentDescription = null, - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "알림을 받으려면 모든 필수 권한을 허용하고 사이트를 선택하세요", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error - ) - } - } else { - HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "모든 설정이 완료되었습니다", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.primary - ) - } - } - } - } -} - -@Composable -fun PermissionItem( - icon: androidx.compose.ui.graphics.vector.ImageVector, - title: String, - isGranted: Boolean, - requiredVersion: String? = null, - description: String? = null, - onClick: (() -> Unit)? -) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = if (isGranted) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Column(modifier = Modifier.weight(1f)) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = title, - style = MaterialTheme.typography.bodyMedium - ) - if (requiredVersion != null) { - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = "($requiredVersion)", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - if (description != null) { - Text( - text = description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - if (onClick != null && !isGranted) { - FilledTonalButton( - onClick = onClick, - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp) - ) { - Text("설정", style = MaterialTheme.typography.labelMedium) - } - } else { - Icon( - imageVector = if (isGranted) Icons.Default.Check else Icons.Default.Close, - contentDescription = if (isGranted) "허용됨" else "거부됨", - tint = if (isGranted) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error, - modifier = Modifier.size(24.dp) - ) - } - } -} - -@Composable -fun SiteSection( - siteType: SiteType, - configs: List, - onToggle: (String, Boolean) -> Unit + onRequestNotificationPermission: () -> Unit ) { Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(16.dp)) { - Text( - text = siteType.displayName, - style = MaterialTheme.typography.titleSmall - ) - + Text("알림 설정", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(8.dp)) + + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = if (permissionStatus.hasNotificationPermission) Icons.Default.Check else Icons.Default.Close, + contentDescription = null, + tint = if (permissionStatus.hasNotificationPermission) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error + ) + Spacer(modifier = Modifier.width(12.dp)) + Text("알림 권한", modifier = Modifier.weight(1f)) + if (!permissionStatus.hasNotificationPermission) { + Button(onClick = onRequestNotificationPermission) { Text("설정") } + } + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = if (hasEnabledSites) Icons.Default.Check else Icons.Default.Close, + contentDescription = null, + tint = if (hasEnabledSites) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error + ) + Spacer(modifier = Modifier.width(12.dp)) + Text("사이트 선택", modifier = Modifier.weight(1f)) + } + } + } +} +@Composable +fun SiteSection(siteType: SiteType, configs: List, onToggle: (String, Boolean) -> Unit) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Text(siteType.displayName, style = MaterialTheme.typography.titleSmall) configs.forEach { config -> Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - Text( - text = config.displayName.substringAfter(" - "), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.weight(1f) - ) + Text(config.displayName.substringAfter(" - "), modifier = Modifier.weight(1f)) Switch( checked = config.isEnabled, onCheckedChange = { onToggle(config.siteBoardKey, it) } @@ -500,19 +292,12 @@ fun SiteSection( } @Composable -fun KeywordItem( - keyword: Keyword, - onToggle: () -> Unit, - onDelete: () -> Unit -) { +fun KeywordItem(keyword: Keyword, onToggle: () -> Unit, onDelete: () -> Unit) { ListItem( headlineContent = { Text(keyword.keyword) }, trailingContent = { Row { - Switch( - checked = keyword.isEnabled, - onCheckedChange = { onToggle() } - ) + Switch(checked = keyword.isEnabled, onCheckedChange = { onToggle() }) IconButton(onClick = onDelete) { Icon(Icons.Default.Delete, contentDescription = "삭제") } diff --git a/app/src/main/java/com/hotdeal/alarm/util/VersionManager.kt b/app/src/main/java/com/hotdeal/alarm/util/VersionManager.kt new file mode 100644 index 0000000..0d39635 --- /dev/null +++ b/app/src/main/java/com/hotdeal/alarm/util/VersionManager.kt @@ -0,0 +1,114 @@ +package com.hotdeal.alarm.util + +import android.content.Context +import android.content.pm.PackageManager +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject +import java.net.URL + +/** + * 버전 관리 유틸리티 + * + * 버전 규칙: x.y.z + * - x: 메이저 (API 변경, 호환성 깨짐) + * - y: 마이너 (기능 추가, 하위 호환) + * - z: 패치 (버그 수정) + * + * versionCode: 1씩 증가 (낮은 버전 = 이전 버전) + */ +object VersionManager { + + private const val TAG = "VersionManager" + private const val VERSION_URL = "https://git.webpluss.net/sanjeok77/hotdeal_alarm/raw/branch/main/version.json" + + /** + * 현재 앱 버전 가져오기 + */ + fun getCurrentVersion(context: Context): String { + return try { + val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) + packageInfo.versionName ?: "1.0.0" + } catch (e: Exception) { + Log.e(TAG, "버전 가져오기 실패: ${e.message}") + "1.0.0" + } + } + + /** + * 현재 버전 코드 가져오기 + */ + fun getCurrentVersionCode(context: Context): Int { + return try { + val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) + packageInfo.longVersionCode.toInt() + } catch (e: Exception) { + Log.e(TAG, "버전 코드 가져오기 실패: ${e.message}") + 1 + } + } + + /** + * 원격 버전 정보 가져오기 + */ + suspend fun checkForUpdate(): UpdateInfo? = withContext(Dispatchers.IO) { + try { + val json = URL(VERSION_URL).readText() + val jsonObject = JSONObject(json) + + UpdateInfo( + version = jsonObject.getString("version"), + versionCode = jsonObject.getInt("versionCode"), + forceUpdate = jsonObject.optBoolean("forceUpdate", false), + updateUrl = jsonObject.getString("updateUrl"), + changelog = jsonObject.optJSONArray("changelog")?.let { array -> + List(array.length()) { array.getString(it) } + } ?: emptyList() + ) + } catch (e: Exception) { + Log.e(TAG, "버전 체크 실패: ${e.message}") + null + } + } + + /** + * 업데이트 필요 여부 확인 + */ + fun isUpdateAvailable(currentVersionCode: Int, remoteVersionCode: Int): Boolean { + return remoteVersionCode > currentVersionCode + } + + /** + * 버전 문자열 비교 + * 1.0.0 < 1.0.1 < 1.1.0 < 2.0.0 + */ + fun compareVersions(v1: String, v2: String): Int { + val parts1 = v1.split(".").map { it.toIntOrNull() ?: 0 } + val parts2 = v2.split(".").map { it.toIntOrNull() ?: 0 } + + val maxLength = maxOf(parts1.size, parts2.size) + + for (i in 0 until maxLength) { + val p1 = parts1.getOrElse(i) { 0 } + val p2 = parts2.getOrElse(i) { 0 } + + when { + p1 > p2 -> return 1 + p1 < p2 -> return -1 + } + } + return 0 + } +} + +/** + * 업데이트 정보 + */ +data class UpdateInfo( + val version: String, + val versionCode: Int, + val forceUpdate: Boolean, + val updateUrl: String, + val changelog: List +) diff --git a/version.json b/version.json new file mode 100644 index 0000000..28dae7f --- /dev/null +++ b/version.json @@ -0,0 +1,14 @@ +{ + "version": "1.0.0", + "versionCode": 1, + "minSdk": 31, + "targetSdk": 35, + "forceUpdate": false, + "updateUrl": "https://git.webpluss.net/sanjeok77/hotdeal_alarm/releases", + "changelog": [ + "초기 릴리즈", + "뽐뿌, 클리앙, 루리웹, 쿨엔조이 지원", + "키워드 알림 기능", + "사이트 필터 기능" + ] +}