refactor: 알람 시스템 단순화 - 기본 알람 제거, ID 체계 개선

- 기본 알람(legacy) 로직 완전 제거
- 알람 ID 체계 단순화: alarmDbId * 100 + dayOffset
- 예약 범위 축소: 오늘/내일만 예약 (복잡한 30일/365일 예약 제거)
- AlarmSyncManager 단순화: 즉시 반영 로직
- AlarmReceiver 강화: 삭제된 알람 필터링 개선
- suspend 함수로 변경하여 비동기 처리 개선

Fixes: 삭제된 알람이 울리는 문제 해결

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-02-28 18:10:46 +09:00
parent fdcbb615ab
commit 380b2c5a6c
5 changed files with 502 additions and 429 deletions

View File

@@ -13,7 +13,7 @@ class AlarmReceiver : BroadcastReceiver() {
private val TAG = "AlarmReceiver"
override fun onReceive(context: Context, intent: Intent?) {
Log.d(TAG, "===== 알람 수신 (Receiver) =====")
Log.d(TAG, "===== 알람 수신 =====")
// 마스터 알람이 꺼져있으면 알람 무시
val prefs = context.getSharedPreferences("ShiftAlarmPrefs", Context.MODE_PRIVATE)
@@ -22,36 +22,69 @@ class AlarmReceiver : BroadcastReceiver() {
return
}
val alarmId = intent?.getIntExtra("EXTRA_ALARM_ID", -1) ?: -1
val isCustom = intent?.getBooleanExtra("EXTRA_IS_CUSTOM", false) ?: false
val action = intent?.action
// 커스텀 알람인 경우 DB에서 여전히 유효한지 확인 (삭제된 알람이 울리는 문제 해결)
if (isCustom && alarmId != -1) {
val customAlarmId = intent.getIntExtra("EXTRA_UNIQUE_ID", -1)
if (customAlarmId != -1) {
// 비동기로 DB 확인
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
val repo = ShiftRepository(context)
val alarms = repo.getAllCustomAlarms()
val alarmExists = alarms.any { it.id == customAlarmId && it.isEnabled }
if (!alarmExists) {
Log.w(TAG, "삭제된 또는 비활성화된 알람입니다. 무시합니다. (ID: $customAlarmId)")
scope.cancel()
return@launch
when (action) {
"com.example.shiftalarm.SNOOZE" -> {
// 스누즈 알람
startAlarm(context, intent)
}
else -> {
// 일반 알람 - DB에서 여전히 유효한지 확인
val alarmDbId = intent?.getIntExtra("EXTRA_ALARM_DB_ID", -1) ?: -1
if (alarmDbId != -1) {
// 비동기로 DB 확인
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
try {
val repo = ShiftRepository(context)
val alarms = repo.getAllCustomAlarms()
val alarm = alarms.find { it.id == alarmDbId }
// 알람이 존재하지 않거나 비활성화되었으면 무시
if (alarm == null) {
Log.w(TAG, "DB에서 알람을 찾을 수 없음 (삭제됨): ID=$alarmDbId")
scope.cancel()
return@launch
}
if (!alarm.isEnabled) {
Log.w(TAG, "알람이 비활성화됨: ID=$alarmDbId")
scope.cancel()
return@launch
}
// 근무 조건 확인
val dateStr = intent?.getStringExtra("EXTRA_DATE")
if (dateStr != null) {
val date = java.time.LocalDate.parse(dateStr)
val team = prefs.getString("selected_team", "A") ?: "A"
val factory = prefs.getString("selected_factory", "Jeonju") ?: "Jeonju"
val shift = repo.getShift(date, team, factory)
if (alarm.shiftType != "기타" && alarm.shiftType != shift) {
Log.w(TAG, "근무 조건이 맞지 않음: 알람=${alarm.shiftType}, 실제=$shift")
scope.cancel()
return@launch
}
}
// 모든 검증 통과 - 알람 실행
startAlarm(context, intent)
scope.cancel()
} catch (e: Exception) {
Log.e(TAG, "알람 검증 중 오류", e)
scope.cancel()
}
}
// 알람이 유효하면 직접 AlarmActivity 실행 + Foreground Service 시작
} else {
// DB ID가 없는 경우 (테스트 알람 등) - 바로 실행
startAlarm(context, intent)
scope.cancel()
}
return
}
}
// 일반 알람은 바로 직접 실행
startAlarm(context, intent)
}
private fun startAlarm(context: Context, intent: Intent?) {
@@ -62,15 +95,20 @@ class AlarmReceiver : BroadcastReceiver() {
PowerManager.PARTIAL_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
"ShiftAlarm::AlarmWakeLock"
)
wakeLock.acquire(30 * 1000L) // 30초 - Activity 실행 및 초기화에 충분한 시간
wakeLock.acquire(30 * 1000L) // 30초
try {
// 1. Foreground Service 시작 (알림 표시 및 시스템에 알람 실행 중 알림)
val shiftType = intent?.getStringExtra("EXTRA_SHIFT_TYPE") ?: "근무"
val soundUri = intent?.getStringExtra("EXTRA_SOUND")
val snoozeMin = intent?.getIntExtra("EXTRA_SNOOZE", 5) ?: 5
val snoozeRepeat = intent?.getIntExtra("EXTRA_SNOOZE_REPEAT", 3) ?: 3
// 1. Foreground Service 시작
val serviceIntent = Intent(context, AlarmForegroundService::class.java).apply {
putExtra("EXTRA_SHIFT", intent?.getStringExtra("EXTRA_SHIFT") ?: "근무")
putExtra("EXTRA_SOUND", intent?.getStringExtra("EXTRA_SOUND"))
putExtra("EXTRA_SNOOZE", intent?.getIntExtra("EXTRA_SNOOZE", 5) ?: 5)
putExtra("EXTRA_SNOOZE_REPEAT", intent?.getIntExtra("EXTRA_SNOOZE_REPEAT", 3) ?: 3)
putExtra("EXTRA_SHIFT", shiftType)
putExtra("EXTRA_SOUND", soundUri)
putExtra("EXTRA_SNOOZE", snoozeMin)
putExtra("EXTRA_SNOOZE_REPEAT", snoozeRepeat)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -81,28 +119,24 @@ class AlarmReceiver : BroadcastReceiver() {
Log.d(TAG, "ForegroundService 시작 완료")
// 2. AlarmActivity 직접 실행 (알람 화면 표시)
// 2. AlarmActivity 직접 실행
val activityIntent = Intent(context, AlarmActivity::class.java).apply {
putExtra("EXTRA_SHIFT", intent?.getStringExtra("EXTRA_SHIFT") ?: "근무")
putExtra("EXTRA_SOUND", intent?.getStringExtra("EXTRA_SOUND"))
putExtra("EXTRA_SNOOZE", intent?.getIntExtra("EXTRA_SNOOZE", 5) ?: 5)
putExtra("EXTRA_SNOOZE_REPEAT", intent?.getIntExtra("EXTRA_SNOOZE_REPEAT", 3) ?: 3)
// 중요: 새 태스크로 실행 (FLAG_ACTIVITY_NEW_TASK)
putExtra("EXTRA_SHIFT", shiftType)
putExtra("EXTRA_SOUND", soundUri)
putExtra("EXTRA_SNOOZE", snoozeMin)
putExtra("EXTRA_SNOOZE_REPEAT", snoozeRepeat)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// 기존 인스턴스 재사용 및 최상위로 가져오기
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
// 잠금 화면 위에 표시
addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT)
// 화면 켜기
addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
}
// 지연 후 Activity 시작 (ForegroundService가 알림을 먼저 표시하도록)
// 지연 후 Activity 시작 (ForegroundService가 먼저 시작되도록)
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
try {
context.startActivity(activityIntent)
Log.d(TAG, "AlarmActivity 실행 완료")
Log.d(TAG, "AlarmActivity 실행 완료: $shiftType")
} catch (e: Exception) {
Log.e(TAG, "AlarmActivity 실행 실패", e)
}
@@ -111,7 +145,7 @@ class AlarmReceiver : BroadcastReceiver() {
} catch (e: Exception) {
Log.e(TAG, "알람 실행 실패", e)
} finally {
// WakeLock은 Activity가 화면을 켜고 나서 해제
// WakeLock 해제
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
if (wakeLock.isHeld) wakeLock.release()
}, 5000)