diff --git a/app.apk b/app.apk index b37149e..9cbf5db 100644 Binary files a/app.apk and b/app.apk differ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1201a01..4bb7c99 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "com.example.shiftalarm" minSdk = 26 targetSdk = 35 - versionCode = 110 - versionName = "1.1.0" + versionCode = 111 + versionName = "1.1.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/example/shiftalarm/AlarmPermissionUtil.kt b/app/src/main/java/com/example/shiftalarm/AlarmPermissionUtil.kt index 680d345..ccd96b8 100644 --- a/app/src/main/java/com/example/shiftalarm/AlarmPermissionUtil.kt +++ b/app/src/main/java/com/example/shiftalarm/AlarmPermissionUtil.kt @@ -29,9 +29,10 @@ object AlarmPermissionUtil { fun monitorAndAutoReturn(activity: Activity, isGrantedCheck: () -> Boolean) { monitorJob?.cancel() monitorJob = CoroutineScope(Dispatchers.Main).launch { + delay(1000) // 설정 화면 전환 시간 대기 var count = 0 - while (count < 150 && isActive) { // 최대 60초간 400ms 주기로 체크 - delay(400) + while (count < 150 && isActive) { // 최대 60초간 500ms 주기로 체크 + delay(500) count++ if (isGrantedCheck()) { try { diff --git a/app/src/main/java/com/example/shiftalarm/FragmentSettingsAdditional.kt b/app/src/main/java/com/example/shiftalarm/FragmentSettingsAdditional.kt index b9924e7..ba1e30e 100644 --- a/app/src/main/java/com/example/shiftalarm/FragmentSettingsAdditional.kt +++ b/app/src/main/java/com/example/shiftalarm/FragmentSettingsAdditional.kt @@ -187,21 +187,19 @@ class FragmentSettingsAdditional : Fragment() { } } + // 1. 개별 근무/알람 설정 전체 삭제 binding.btnResetOverrides.setOnClickListener { androidx.appcompat.app.AlertDialog.Builder(requireContext()) - .setTitle("데이터 초기화") - .setMessage("달력에서 개별적으로 바꾼 모든 근무와 알람 설정이 삭제됩니다. 계속하시겠습니까?") - .setPositiveButton("초기화") { _, _ -> + .setTitle("개별 근무/알람 설정 전체 삭제") + .setMessage("달력에서 개별 변경한 모든 근무와 알람 일정이 삭제되고 기본값으로 복원됩니다. 계속하시겠습니까?") + .setPositiveButton("삭제") { _, _ -> lifecycleScope.launch { try { val db = AppDatabase.getDatabase(requireContext()) val dao = db.shiftDao() dao.clearOverrides() - - // Immediately re-sync all alarms syncAllAlarms(requireContext()) - - Toast.makeText(requireContext(), "모든 개별 설정이 삭제되고 알람이 재설정되었습니다.", Toast.LENGTH_SHORT).show() + Toast.makeText(requireContext(), "개별 근무 및 알람 설정이 초기화되었습니다.", Toast.LENGTH_SHORT).show() } catch (e: Exception) { Toast.makeText(requireContext(), "초기화 실패: ${e.message}", Toast.LENGTH_LONG).show() } @@ -210,6 +208,75 @@ class FragmentSettingsAdditional : Fragment() { .setNegativeButton("취소", null) .show() } + + // 2. 개별 근무 삭제 + binding.btnResetShifts.setOnClickListener { + androidx.appcompat.app.AlertDialog.Builder(requireContext()) + .setTitle("개별 근무 삭제") + .setMessage("달력에서 개별 변경한 교대근무 일정만 삭제되고 기본 근무표로 복원됩니다. 계속하시겠습니까?") + .setPositiveButton("삭제") { _, _ -> + lifecycleScope.launch { + try { + val db = AppDatabase.getDatabase(requireContext()) + val dao = db.shiftDao() + dao.clearOverrides() + syncAllAlarms(requireContext()) + Toast.makeText(requireContext(), "개별 근무 변경 내역이 초기화되었습니다.", Toast.LENGTH_SHORT).show() + } catch (e: Exception) { + Toast.makeText(requireContext(), "초기화 실패: ${e.message}", Toast.LENGTH_LONG).show() + } + } + } + .setNegativeButton("취소", null) + .show() + } + + // 3. 알람 삭제 + binding.btnResetAlarms.setOnClickListener { + androidx.appcompat.app.AlertDialog.Builder(requireContext()) + .setTitle("알람 삭제") + .setMessage("등록된 모든 사용자 지정 알람이 삭제되고 기본 알람으로 초기화됩니다. 계속하시겠습니까?") + .setPositiveButton("삭제") { _, _ -> + lifecycleScope.launch { + try { + val db = AppDatabase.getDatabase(requireContext()) + val dao = db.shiftDao() + dao.clearCustomAlarms() + syncAllAlarms(requireContext()) + Toast.makeText(requireContext(), "등록된 알람이 모두 삭제되었습니다.", Toast.LENGTH_SHORT).show() + } catch (e: Exception) { + Toast.makeText(requireContext(), "삭제 실패: ${e.message}", Toast.LENGTH_LONG).show() + } + } + } + .setNegativeButton("취소", null) + .show() + } + + // 4. 전체 삭제 (완전 초기화) + binding.btnResetAll.setOnClickListener { + androidx.appcompat.app.AlertDialog.Builder(requireContext()) + .setTitle("전체 데이터 초기화") + .setMessage("⚠️ 주의: 메모, 연차, 개별 근무, 알람을 포함한 모든 앱 데이터가 완전히 초기화됩니다.\n\n정말 전체 데이터를 초기화하시겠습니까?") + .setPositiveButton("전체 초기화") { _, _ -> + lifecycleScope.launch { + try { + val db = AppDatabase.getDatabase(requireContext()) + val dao = db.shiftDao() + dao.clearOverrides() + dao.clearMemos() + dao.clearCustomAlarms() + dao.clearAnnualLeave() + syncAllAlarms(requireContext()) + Toast.makeText(requireContext(), "모든 데이터가 완전히 초기화되었습니다.", Toast.LENGTH_LONG).show() + } catch (e: Exception) { + Toast.makeText(requireContext(), "전체 초기화 실패: ${e.message}", Toast.LENGTH_LONG).show() + } + } + } + .setNegativeButton("취소", null) + .show() + } } diff --git a/app/src/main/java/com/example/shiftalarm/FragmentSettingsAlarm.kt b/app/src/main/java/com/example/shiftalarm/FragmentSettingsAlarm.kt index ced2e85..09fe6ff 100644 --- a/app/src/main/java/com/example/shiftalarm/FragmentSettingsAlarm.kt +++ b/app/src/main/java/com/example/shiftalarm/FragmentSettingsAlarm.kt @@ -134,32 +134,72 @@ class FragmentSettingsAlarm : Fragment(), SharedPreferences.OnSharedPreferenceCh } private val soundTitleCache = mutableMapOf() + private var isUpdatingMasterSwitch = false private fun updateMasterToggleUI(isEnabled: Boolean) { + if (_binding == null) return + isUpdatingMasterSwitch = true + binding.masterSwitch.isChecked = isEnabled + isUpdatingMasterSwitch = false + if (isEnabled) { binding.tvMasterStatus.text = "전체 알람 켜짐" - binding.tvMasterStatus.setTextColor(ContextCompat.getColor(requireContext(), R.color.primary)) - binding.tvMasterStatus.backgroundTintList = android.content.res.ColorStateList.valueOf(Color.parseColor("#E3F2FD")) + binding.tvMasterStatus.setTextColor(ContextCompat.getColor(requireContext(), R.color.text_primary)) + binding.ivMasterBell.imageTintList = android.content.res.ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.primary)) } else { binding.tvMasterStatus.text = "전체 알람 꺼짐" - binding.tvMasterStatus.setTextColor(ContextCompat.getColor(requireContext(), R.color.shift_red)) - binding.tvMasterStatus.backgroundTintList = android.content.res.ColorStateList.valueOf(Color.parseColor("#FFEBEE")) + binding.tvMasterStatus.setTextColor(ContextCompat.getColor(requireContext(), R.color.text_secondary)) + binding.ivMasterBell.imageTintList = android.content.res.ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.text_secondary)) } } + private fun showMasterAlarmOffConfirmationDialog() { + val prefs = requireContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + androidx.appcompat.app.AlertDialog.Builder(requireContext()) + .setTitle("전체 알람 끄기") + .setMessage("전체 알람을 끄시겠습니까?\n모든 교대근무 알람 및 개별 알람이 정시에 울리지 않게 됩니다.") + .setPositiveButton("끄기") { _, _ -> + prefs.edit().putBoolean("master_alarm_enabled", false).apply() + updateMasterToggleUI(false) + Toast.makeText(requireContext(), "전체 알람이 꺼졌습니다.", Toast.LENGTH_SHORT).show() + lifecycleScope.launch { syncAllAlarms(requireContext()) } + } + .setNegativeButton("취소") { dialog, _ -> + // 스위치 원래대로 ON 복귀 + isUpdatingMasterSwitch = true + binding.masterSwitch.isChecked = true + isUpdatingMasterSwitch = false + dialog.dismiss() + } + .setOnCancelListener { + // 바깥 터치 시에도 원래대로 ON 복귀 + isUpdatingMasterSwitch = true + binding.masterSwitch.isChecked = true + isUpdatingMasterSwitch = false + } + .show() + } + private fun setupListeners() { val prefs = requireContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - binding.tvMasterStatus.setOnClickListener { - val isEnabled = !ShiftAlarmDefaults.isMasterAlarmEnabled(prefs) - prefs.edit().putBoolean("master_alarm_enabled", isEnabled).apply() - updateMasterToggleUI(isEnabled) - - val message = if (isEnabled) "전체 알람이 켜졌습니다." else "전체 알람이 꺼졌습니다." - Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() - - // Resync immediately - lifecycleScope.launch { syncAllAlarms(requireContext()) } + binding.masterAlarmCard.setOnClickListener { + binding.masterSwitch.performClick() + } + + binding.masterSwitch.setOnCheckedChangeListener { _, isChecked -> + if (isUpdatingMasterSwitch) return@setOnCheckedChangeListener + + if (isChecked) { + // 바로 켜기 + prefs.edit().putBoolean("master_alarm_enabled", true).apply() + updateMasterToggleUI(true) + Toast.makeText(requireContext(), "전체 알람이 켜졌습니다.", Toast.LENGTH_SHORT).show() + lifecycleScope.launch { syncAllAlarms(requireContext()) } + } else { + // 끌 때는 모달 확인 팝업 + showMasterAlarmOffConfirmationDialog() + } } binding.btnAddCustomAlarm.setOnClickListener { diff --git a/app/src/main/java/com/example/shiftalarm/FragmentSettingsBasic.kt b/app/src/main/java/com/example/shiftalarm/FragmentSettingsBasic.kt index da8c33d..272c61b 100644 --- a/app/src/main/java/com/example/shiftalarm/FragmentSettingsBasic.kt +++ b/app/src/main/java/com/example/shiftalarm/FragmentSettingsBasic.kt @@ -233,46 +233,56 @@ class FragmentSettingsBasic : Fragment() { val prefs = requireContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) binding.btnBatteryOptimize.setOnClickListener { + val isAlreadyGranted = AlarmPermissionUtil.getBatteryOptimizationStatus(requireContext()) AlarmPermissionUtil.requestBatteryOptimization(requireActivity()) - AlarmPermissionUtil.monitorAndAutoReturn(requireActivity()) { - AlarmPermissionUtil.getBatteryOptimizationStatus(requireContext()) + if (!isAlreadyGranted) { + AlarmPermissionUtil.monitorAndAutoReturn(requireActivity()) { + AlarmPermissionUtil.getBatteryOptimizationStatus(requireContext()) + } } } binding.btnExactAlarm.setOnClickListener { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val isAlreadyGranted = AlarmPermissionUtil.getExactAlarmStatus(requireContext()) val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply { data = Uri.parse("package:${requireContext().packageName}") } requireActivity().startActivity(intent) - AlarmPermissionUtil.monitorAndAutoReturn(requireActivity()) { - AlarmPermissionUtil.getExactAlarmStatus(requireContext()) + if (!isAlreadyGranted) { + AlarmPermissionUtil.monitorAndAutoReturn(requireActivity()) { + AlarmPermissionUtil.getExactAlarmStatus(requireContext()) + } } } } binding.btnOverlayPermission.setOnClickListener { + val isAlreadyGranted = AlarmPermissionUtil.getOverlayStatus(requireContext()) AlarmPermissionUtil.requestOverlayPermission(requireActivity()) - AlarmPermissionUtil.monitorAndAutoReturn(requireActivity()) { - AlarmPermissionUtil.getOverlayStatus(requireContext()) + if (!isAlreadyGranted) { + AlarmPermissionUtil.monitorAndAutoReturn(requireActivity()) { + AlarmPermissionUtil.getOverlayStatus(requireContext()) + } } } binding.btnFullScreenIntent.setOnClickListener { + val isAlreadyGranted = AlarmPermissionUtil.getFullScreenIntentStatus(requireContext()) AlarmPermissionUtil.requestFullScreenIntentPermission(requireActivity()) - AlarmPermissionUtil.monitorAndAutoReturn(requireActivity()) { - AlarmPermissionUtil.getFullScreenIntentStatus(requireContext()) + if (!isAlreadyGranted) { + AlarmPermissionUtil.monitorAndAutoReturn(requireActivity()) { + AlarmPermissionUtil.getFullScreenIntentStatus(requireContext()) + } } } binding.btnPermissionSettings.setOnClickListener { + // 앱 정보 상세 설정 화면은 일반 열람용이므로 자동 복귀를 걸지 않음 val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { data = Uri.parse("package:${requireContext().packageName}") } requireActivity().startActivity(intent) - AlarmPermissionUtil.monitorAndAutoReturn(requireActivity()) { - AlarmPermissionUtil.isAllPermissionsGranted(requireContext()) - } } binding.btnCheckUpdate.setOnClickListener { diff --git a/app/src/main/res/layout/fragment_settings_additional.xml b/app/src/main/res/layout/fragment_settings_additional.xml index 4f75f33..d464a8a 100644 --- a/app/src/main/res/layout/fragment_settings_additional.xml +++ b/app/src/main/res/layout/fragment_settings_additional.xml @@ -397,32 +397,116 @@ app:cardCornerRadius="24dp" app:cardElevation="0dp" app:cardBackgroundColor="@android:color/transparent" - android:layout_marginBottom="40dp"> + android:layout_marginBottom="32dp"> + android:padding="16dp"> + + android:layout_marginTop="4dp" + android:layout_marginBottom="12dp" + android:layout_marginStart="4dp"/> + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_settings_alarm.xml b/app/src/main/res/layout/fragment_settings_alarm.xml index 045f936..d1b998a 100644 --- a/app/src/main/res/layout/fragment_settings_alarm.xml +++ b/app/src/main/res/layout/fragment_settings_alarm.xml @@ -12,33 +12,44 @@ android:background="@color/surface" android:padding="16dp"> + - + android:background="@drawable/bg_group_header" + android:paddingHorizontal="16dp" + android:paddingVertical="12dp" + android:layout_marginBottom="12dp" + android:clickable="true" + android:focusable="true"> + + + + android:textColor="@color/text_primary"/> - + diff --git a/scratch/release.py b/scratch/release.py index 98285bb..cae5608 100755 --- a/scratch/release.py +++ b/scratch/release.py @@ -6,17 +6,21 @@ import shutil token = "e3b515eaa0a6683c921ca3bf718e281ed30a6075" owner_repo = "sanjeok77/ShiftRing" -tag = "v1.1.0" -title = "v1.1.0 - 오늘의 근무 폰트 확대, 설정 드롭박스 텍스트 중앙 정렬 및 전체 알람 켜짐 타이포 크기 일원화" -body = """## 🚀 ShiftRing v1.1.0 릴리즈 +tag = "v1.1.1" +title = "v1.1.1 - 전체 알람 온오프 스위치 및 해제 시 경고 팝업 적용, 앱 정보 화면 바운스 수정, 데이터 초기화 4단계 세분화" +body = """## 🚀 ShiftRing v1.1.1 릴리즈 ### 🌟 주요 변경 및 개선 사항 -1. **🔍 메인 화면 '오늘의 근무' 폰트 크기 확대 (`activity_main.xml`)**: - - 기존의 작은 폰트(13sp)에서 교대링 로고(24sp)와 조화를 이루는 18sp 볼드 폰트로 시원하게 확대하여 한눈에 오늘의 근무를 확인할 수 있도록 개선 -2. **🎯 설정 탭 드롭박스(근무 회사, 나의 근무 반, 테마, 총 연차) 텍스트 중앙 정렬 (`spinner_item_oneui.xml`, 각 설정 레이아웃)**: - - 우측으로 치우쳐 표시되던 스피너 선택 텍스트를 완벽한 중앙 정렬(`gravity="center"`, `textAlignment="center"`)로 정돈하여 깔끔한 One UI 룩앤필 완성 -3. **⏰ 알람 설정 '전체 알람 켜짐' 폰트 크기 일원화 (`fragment_settings_alarm.xml`)**: - - 기존 13sp에서 그룹 타이틀('주간 알람' 16sp)과 완벽히 동일한 16sp 볼드 타이포그래피로 통일하여 시각적 위계와 터치 편의성 극대화""" +1. **🔘 전체 알람 온/오프 스위치 UI 도입 및 안전 끄기 모달 팝업 (`fragment_settings_alarm.xml`, `FragmentSettingsAlarm.kt`)**: + - 기존 텍스트 터치 방식에서 그룹 알람과 동일한 원터치 머티리얼 스위치(`MaterialSwitch`) 카드로 개편 + - 알람을 끌 때는 "전체 알람을 끄시겠습니까? 모든 교대근무 알람이 울리지 않게 됩니다." 경고 다이얼로그(확인/취소)를 띄워 오동작을 방지 +2. **🛡️ 시스템 전체 권한 설정 진입 시 즉시 튕겨 돌아오던 현상 수정 (`FragmentSettingsBasic.kt`, `AlarmPermissionUtil.kt`)**: + - 앱 상세 정보 열람 화면에는 자동 복귀 타이머를 배제하고, 개별 권한 설정 시에도 미허용 상태일 때만 지능적으로 동작하도록 개선 +3. **🧹 데이터 초기화 4단계 정밀 세분화 (`fragment_settings_additional.xml`, `FragmentSettingsAdditional.kt`)**: + - **개별 근무/알람 설정 전체 삭제**: 달력 개별 변경 내역 초기화 + - **개별 근무 삭제**: 변경된 교대근무 일정만 복원 + - **알람 삭제**: 추가된 커스텀 알람 삭제 및 기본 복원 + - **전체 삭제**: 메모, 연차, 근무, 알람 등 전체 앱 데이터 완전 초기화""" apk_path = "app/build/outputs/apk/release/app-release.apk" if not os.path.exists(apk_path): @@ -54,10 +58,10 @@ print("Updated app.apk at project root.") # Update version.json v_info = { - "versionCode": 110, - "versionName": "1.1.0", + "versionCode": 111, + "versionName": "1.1.1", "apkUrl": direct_url, - "changelog": "v1.1.0: 오늘의 근무 폰트 확대, 설정 드롭박스 텍스트 중앙 정렬, 전체 알람 타이포 통일", + "changelog": "v1.1.1: 전체 알람 스위치 및 경고 팝업, 앱 상세설정 바운스 수정, 데이터 초기화 4단계 세분화", "forceUpdate": False } with open("version.json", "w", encoding="utf-8") as f: @@ -69,7 +73,7 @@ print(f"Updated version.json with apkUrl: {direct_url}") git_path = r"C:\Users\work\AppData\Roaming\MobaXterm\slash\mx86_64b\bin" os.environ["PATH"] = git_path + os.pathsep + os.environ.get("PATH", "") -commit_msg = "v1.1.0 - 오늘의 근무 폰트 확대, 설정 드롭박스 텍스트 중앙 정렬 및 전체 알람 켜짐 타이포 크기 일원화" +commit_msg = "v1.1.1 - 전체 알람 온오프 스위치 및 해제 시 경고 팝업 적용, 앱 정보 화면 바운스 수정, 데이터 초기화 4단계 세분화" subprocess.run(["git", "add", "."], check=True) subprocess.run(["git", "commit", "-m", commit_msg], check=True) subprocess.run(["git", "push", "origin", "main"], check=True) diff --git a/version.json b/version.json index 7534d2b..90a1c23 100644 --- a/version.json +++ b/version.json @@ -1,7 +1,7 @@ { - "versionCode": 110, - "versionName": "1.1.0", - "apkUrl": "https://git.webpluss.net/attachments/a7d302ab-914c-4152-9aa5-b6efaec7c0a6", - "changelog": "v1.1.0: 오늘의 근무 폰트 확대, 설정 드롭박스 텍스트 중앙 정렬, 전체 알람 타이포 통일", + "versionCode": 111, + "versionName": "1.1.1", + "apkUrl": "https://git.webpluss.net/attachments/338d4f1c-3253-4de6-8632-0232bc1ce7a9", + "changelog": "v1.1.1: 전체 알람 스위치 및 경고 팝업, 앱 상세설정 바운스 수정, 데이터 초기화 4단계 세분화", "forceUpdate": false } \ No newline at end of file