Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
bdbf213
[UI/#226] OrbitPicker 사이즈 축소
DongChyeon Jul 14, 2025
3705c85
[UI/#226] 미션 선택 섹션 추가
DongChyeon Jul 14, 2025
0d281bd
[REFACTOR/#226] Type 안정성과 재사용성을 위해 PickeState를 generic으로 처리
DongChyeon Jul 14, 2025
1b3a951
[FEAT/#226] 미션 종류에 없음 추가
DongChyeon Jul 15, 2025
e6b9872
[REMOVE/#226] JUnit4를 사용하므로 useJUnitPlatform 제거
DongChyeon Jul 15, 2025
7474871
[FEAT/#226] AlarmEntity isAm 컬럼 제거
DongChyeon Jul 15, 2025
153cca5
[FEAT/#226] OrbitPicker가 java.time.LocalTime을 사용하도록 수정
DongChyeon Jul 15, 2025
aa761ec
[FEAT/#226] 알람 시간 표현을 LocalTime으로 통일
DongChyeon Jul 15, 2025
1826d1e
[FEAT/#226] 미션 타입이 NONE일 때 처리 추가
DongChyeon Jul 15, 2025
5c58962
[REMOVE/#226] 사용하지 않는 DB 연산 제거
DongChyeon Jul 15, 2025
ec9ab8b
[FEAT/#226] AlarmDateTimeFormatter에 알람 시간 계산 로직 위임
DongChyeon Jul 16, 2025
790faf6
[TEST/#226] AlarmDateTimeFormatter 테스트 코드 추가
DongChyeon Jul 16, 2025
2fcbf39
[REFACTOR/#226] AlarmScheduler를 UseCase를 통해 사용하도록 변경
DongChyeon Jul 16, 2025
b258571
[REFACTOR/#226] 알람 시간 계산 로직을 AlarmTimeCalculator로 분리
DongChyeon Jul 16, 2025
a68c962
[FIX/#226] 알람 스케줄링 시 공휴일 건너뛰기 여부에 따라 건너뛰도록 수정
DongChyeon Jul 16, 2025
d28a2ef
[TEST/#226] AlarmTimeCalculator 테스트 코드 추가
DongChyeon Jul 16, 2025
5587818
[FEAT/#226] AlarmEntity 관련 매핑 함수에 missionType과 missionCount 필드 추가
DongChyeon Jul 16, 2025
e3eea36
[FEAT/#226] MIGRATION_1_2 로직에 isAm 컬럼에 따른 시간 변환 로직 추가
DongChyeon Jul 16, 2025
955f467
[REFACTOR/#226] AlarmDateTimeFormatter 상수 정의 및 로그 제거
DongChyeon Jul 16, 2025
28d7e10
[FEAT/#226] 데이터베이스 마이그레이션 로직에 롤백 보장을 위한 트랜잭션 적용
DongChyeon Jul 16, 2025
05f5150
[TEST/#226] 12시간제에서 24시간제로 시간 변환 검증 테스트 추가
DongChyeon Jul 16, 2025
152b993
[REFACTOR/#226] 반복 알람 요일이 비어있을 경우 예외 발생하도록 검증 로직 변경
DongChyeon Jul 16, 2025
f392798
[REFACTOR/#226] 반복 알람 로직에서 잘못된 fallback 제거 및 명시적 예외 처리
DongChyeon Jul 16, 2025
bb9f921
[TEST/#226] AlarmDateTimeFormatterTest에 Clock 주입 적용하여 CI 환경 시간 오류 방지
DongChyeon Jul 16, 2025
a72dfae
[FIX/#226] ClockModule 생성 및 의존성 주입
DongChyeon Jul 16, 2025
8bbaea0
[REFACTOR/#226] AlarmDateTimeFormatter :feature:home 모듈로 이전
DongChyeon Jul 17, 2025
b114a1d
[MOVE/#226] 홈 모듈 내 알람 관련 파일 com.yapp.alarm 패키지로 이동
DongChyeon Jul 17, 2025
ecb5d2a
[MOVE/#226] ClockModule을 common 모듈로 이동
DongChyeon Jul 17, 2025
acf0c82
[TEST/#226] AlarmDateTimeFormatter 테스트용 Locale 사용
DongChyeon Jul 17, 2025
fc1702b
[FEAT/#226] AlarmDateTimeFormatter 예외 발생 시 로그 추가
DongChyeon Jul 17, 2025
06107dd
[REFACTOR/#226] formatTimeDifference 중복 검사 로직 제거
DongChyeon Jul 17, 2025
36bcb48
[FEAT/#226] AlarmDateTimeFormatter 테스트 용이성을 위한 Locale 주입
DongChyeon Jul 17, 2025
57d2da9
[FEAT/#226] 의존성 그래프 모듈 유형별 색상 구분
DongChyeon Jul 17, 2025
c5f3202
[REFACTOR/#226] given/when/then 분리
DongChyeon Jul 17, 2025
8ce6247
[CHORE/#227] Jacoco 리포트를 Codecov에 업로드하도록 수정
MoonsuKang Jul 20, 2025
d9cb6aa
[ADD/#227] Jacoco XML 리포트 생성 활성화
MoonsuKang Jul 20, 2025
1b958ab
[MOD/#227] CI 워크플로우에 Codecov 토큰 추가
MoonsuKang Jul 20, 2025
6a56222
[ADD/#227] Codecov 설정 파일 추가
MoonsuKang Jul 20, 2025
1f10398
[MOD/#227] 커버리지 리포트 PR 자동 코멘트 기능 삭제
MoonsuKang Jul 20, 2025
f63bce9
[FEAT/#226] MigrationTest에서 db를 닫기 위해 close 호출
DongChyeon Jul 21, 2025
d38d230
[REFACTOR/#226] AlarmDateTimeFormatter 코드 가독성 개선
DongChyeon Jul 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ internal fun Project.configureComposeUiTest() {
@Suppress("UnstableApiUsage")
internal fun Project.configureJUnitAndroid() {
androidExtension.apply {
testOptions { unitTests.all { it.useJUnitPlatform() } }
defaultConfig { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" }

val libs = extensions.libs
Expand Down
1 change: 1 addition & 0 deletions core/alarm/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ android {

dependencies {
implementation(projects.core.analytics)
implementation(projects.core.common)
implementation(projects.core.designsystem)
implementation(projects.core.media)
implementation(projects.domain)
Expand Down
2 changes: 0 additions & 2 deletions core/alarm/src/main/java/com/yapp/alarm/AlarmConstants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ object AlarmConstants {

const val SNOOZE_ID_OFFSET = 10000

const val WEEK_INTERVAL_MILLIS: Long = 7 * 24 * 60 * 60 * 1000

val HOLIDAYS_2025 = setOf(
"2025-01-01", "2025-01-27", "2025-01-28", "2025-01-29", "2025-01-30",
"2025-03-01", "2025-03-03", "2025-05-05", "2025-05-06", "2025-06-06",
Expand Down
98 changes: 98 additions & 0 deletions core/alarm/src/main/java/com/yapp/alarm/AlarmTimeCalculator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.yapp.alarm

import com.yapp.domain.model.Alarm
import com.yapp.domain.model.AlarmDay
import com.yapp.domain.model.toDayOfWeek
import java.time.Clock
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import javax.inject.Inject

class AlarmTimeCalculator @Inject constructor(
private val clock: Clock,
) {
private val holidayDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")

private fun isHoliday(dateToCheck: LocalDateTime): Boolean {
if (dateToCheck.year == 2025) {
val dateString = dateToCheck.format(holidayDateFormatter)
return AlarmConstants.HOLIDAYS_2025.contains(dateString)
}
return false
}

private fun skipHolidaysIfEnabled(initialDateTime: LocalDateTime, alarm: Alarm): LocalDateTime {
if (!alarm.isHolidayAlarmOff) return initialDateTime

var adjustedDateTime = initialDateTime
while (isHoliday(adjustedDateTime)) {
adjustedDateTime = adjustedDateTime.plusWeeks(1)
}

return adjustedDateTime
}

private fun getAlarmDateTimeOnDate(alarm: Alarm, now: LocalDateTime): LocalDateTime {
return now
.withHour(alarm.hour)
.withMinute(alarm.minute)
.withSecond(alarm.second)
.withNano(0)
}

fun calculateNextRepeatingTimeMillis(
alarm: Alarm,
alarmDay: AlarmDay,
zoneId: ZoneId = clock.zone,
): Long {
val now = LocalDateTime.now(clock)
val targetDayOfWeek = alarmDay.toDayOfWeek()

val alarmDateTimeToday = getAlarmDateTimeOnDate(alarm, now)

var nextAlarmDateTimeCandidate = alarmDateTimeToday

while (nextAlarmDateTimeCandidate.dayOfWeek != targetDayOfWeek || nextAlarmDateTimeCandidate.isBefore(now)) {
nextAlarmDateTimeCandidate = nextAlarmDateTimeCandidate.plusDays(1)
}

nextAlarmDateTimeCandidate = skipHolidaysIfEnabled(nextAlarmDateTimeCandidate, alarm)

return nextAlarmDateTimeCandidate.atZone(zoneId).toInstant().toEpochMilli()
}

fun calculateNonRepeatingTimeMillis(
alarm: Alarm,
zoneId: ZoneId = clock.zone,
): Long {
val now = LocalDateTime.now(clock)
var alarmDateTime = getAlarmDateTimeOnDate(alarm, now)

if (alarmDateTime.isBefore(now)) {
alarmDateTime = alarmDateTime.plusDays(1)
}

return alarmDateTime.atZone(zoneId).toInstant().toEpochMilli()
}

fun calculateNextWeeklyRescheduledTimeMillis(
alarm: Alarm,
alarmTargetDay: AlarmDay,
zoneId: ZoneId = clock.zone,
): Long {
val now = LocalDateTime.now(clock)
val targetDayOfWeek = alarmTargetDay.toDayOfWeek()

var initialAlarmDateTimeCandidate = getAlarmDateTimeOnDate(alarm, now)

while (initialAlarmDateTimeCandidate.dayOfWeek != targetDayOfWeek || initialAlarmDateTimeCandidate.isBefore(now)) {
initialAlarmDateTimeCandidate = initialAlarmDateTimeCandidate.plusDays(1)
}

val nextWeeklyAlarmDateTimeCandidate = initialAlarmDateTimeCandidate.plusWeeks(1)
val nextWeeklyAlarmDateTime = skipHolidaysIfEnabled(nextWeeklyAlarmDateTimeCandidate, alarm)

return nextWeeklyAlarmDateTime.atZone(zoneId).toInstant().toEpochMilli()
}
}
116 changes: 25 additions & 91 deletions core/alarm/src/main/java/com/yapp/alarm/AndroidAlarmScheduler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,20 @@ package com.yapp.alarm

import android.app.AlarmManager
import android.app.Application
import android.util.Log
import com.yapp.alarm.pendingIntent.schedule.createAlarmReceiverPendingIntentForSchedule
import com.yapp.alarm.pendingIntent.schedule.createAlarmReceiverPendingIntentForUnSchedule
import com.yapp.domain.model.Alarm
import com.yapp.domain.model.AlarmDay
import com.yapp.domain.model.toAlarmDays
import com.yapp.domain.model.toDayOfWeek
import com.yapp.domain.scheduler.AlarmScheduler
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import javax.inject.Inject

class AndroidAlarmScheduler @Inject constructor(
private val app: Application,
private val alarmManager: AlarmManager,
private val alarmTimeCalculator: AlarmTimeCalculator,
) : AlarmScheduler {

override fun scheduleAlarm(alarm: Alarm) {
val selectedDays = alarm.repeatDays.toAlarmDays()

Expand All @@ -32,19 +28,37 @@ class AndroidAlarmScheduler @Inject constructor(
}
}

fun scheduleWeeklyAlarm(alarm: Alarm, day: AlarmDay) {
val initialTriggerMillis = getNextAlarmTimeMillis(alarm, day) + AlarmConstants.WEEK_INTERVAL_MILLIS
val triggerMillis = findNextNonHolidayDate(initialTriggerMillis)

private fun setRepeatingAlarm(day: AlarmDay, alarm: Alarm) {
val triggerMillis = alarmTimeCalculator.calculateNextRepeatingTimeMillis(alarm, day)
val pendingIntent = createAlarmReceiverPendingIntentForSchedule(app, alarm, day)

alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerMillis,
pendingIntent,
)
}

private fun setNonRepeatingAlarm(alarm: Alarm) {
val triggerMillis = alarmTimeCalculator.calculateNonRepeatingTimeMillis(alarm)
val pendingIntent = createAlarmReceiverPendingIntentForSchedule(app, alarm)

alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerMillis,
pendingIntent,
)
}

fun rescheduleUpcomingWeeklyAlarm(alarm: Alarm, day: AlarmDay) {
val triggerMillis = alarmTimeCalculator.calculateNextWeeklyRescheduledTimeMillis(alarm, day)
val pendingIntent = createAlarmReceiverPendingIntentForSchedule(app, alarm, day)

Log.d("AlarmHelper", "Scheduled weekly alarm for $day at: $triggerMillis")
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerMillis,
pendingIntent,
)
}

override fun unScheduleAlarm(alarm: Alarm) {
Expand Down Expand Up @@ -73,85 +87,5 @@ class AndroidAlarmScheduler @Inject constructor(
val snoozedAlarmId = alarmId + AlarmConstants.SNOOZE_ID_OFFSET
val pendingIntent = createAlarmReceiverPendingIntentForUnSchedule(app, Alarm(id = snoozedAlarmId))
alarmManager.cancel(pendingIntent)
Log.d("AlarmHelper", "Canceled snoozed alarm with id: $snoozedAlarmId")
}

private fun setRepeatingAlarm(day: AlarmDay, alarm: Alarm) {
val alarmReceiverPendingIntent =
createAlarmReceiverPendingIntentForSchedule(app, alarm, day)
val firstAlarmTriggerMillis = getNextAlarmTimeMillis(alarm, day)

Log.d("AlarmHelper", "Setting repeating alarm id: ${alarm.id} at: $firstAlarmTriggerMillis")

alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
firstAlarmTriggerMillis,
alarmReceiverPendingIntent,
)
}

private fun setNonRepeatingAlarm(alarm: Alarm) {
val alarmReceiverPendingIntent =
createAlarmReceiverPendingIntentForSchedule(app, alarm)

val triggerMillis = getNextAlarmTimeMillis(alarm, null)

Log.d("AlarmHelper", "Setting one-time alarm at: $triggerMillis")

alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerMillis,
alarmReceiverPendingIntent,
)
}

private fun getNextAlarmTimeMillis(alarm: Alarm, day: AlarmDay?): Long {
val now = LocalDateTime.now().withNano(0) // 밀리초 제거하여 정확한 초 기준 설정

val alarmHour = when {
alarm.isAm && alarm.hour == 12 -> 0
!alarm.isAm && alarm.hour != 12 -> alarm.hour + 12
else -> alarm.hour
}

var alarmDateTime = now.withHour(alarmHour).withMinute(alarm.minute).withSecond(alarm.second)

if (day != null) {
val targetDayOfWeek = day.toDayOfWeek()
while (alarmDateTime.dayOfWeek != targetDayOfWeek || alarmDateTime.isBefore(now)) {
alarmDateTime = alarmDateTime.plusDays(1)
}
} else {
if (alarmDateTime.isBefore(now)) {
alarmDateTime = alarmDateTime.plusDays(1)
}
}

val epochMillis = alarmDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()

Log.d("AlarmHelper", "Alarm scheduled at: $alarmDateTime (epochMillis=$epochMillis)")

return epochMillis
}

private fun findNextNonHolidayDate(initialMillis: Long): Long {
val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")

var adjustedMillis = initialMillis

while (true) {
val localDate = Instant.ofEpochMilli(adjustedMillis)
.atZone(ZoneId.systemDefault())
.toLocalDate()

val dateString = localDate.format(dateFormatter)

if (!AlarmConstants.HOLIDAYS_2025.contains(dateString)) {
return adjustedMillis // 공휴일이 아니라면 해당 날짜 반환
}

// 공휴일이라면 다음 1주 뒤로 이동
adjustedMillis += AlarmConstants.WEEK_INTERVAL_MILLIS
}
}
}
8 changes: 8 additions & 0 deletions core/alarm/src/main/java/com/yapp/alarm/di/AlarmModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.yapp.alarm.di

import android.app.AlarmManager
import android.content.Context
import com.yapp.alarm.AlarmTimeCalculator
import com.yapp.alarm.AndroidAlarmScheduler
import com.yapp.domain.scheduler.AlarmScheduler
import dagger.Binds
Expand All @@ -10,6 +11,7 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import java.time.Clock
import javax.inject.Singleton

@Module
Expand All @@ -22,6 +24,12 @@ abstract class AlarmModule {
): AlarmScheduler

companion object {
@Provides
@Singleton
fun provideAlarmTimeCalculator(clock: Clock): AlarmTimeCalculator {
return AlarmTimeCalculator(clock)
}

@Provides
@Singleton
fun provideAlarmManager(@ApplicationContext context: Context): AlarmManager {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,7 @@ class AlarmReceiver : BroadcastReceiver() {
.plusMinutes(alarm.snoozeInterval.toLong())

val updatedAlarm = alarm.copy(
isAm = snoozeDateTime.hour < 12,
hour = if (snoozeDateTime.hour == 0) 12 else if (snoozeDateTime.hour > 12) snoozeDateTime.hour - 12 else snoozeDateTime.hour,
hour = snoozeDateTime.hour,
minute = snoozeDateTime.minute,
second = snoozeDateTime.second,
repeatDays = 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ class AlarmService : Service() {
// 반복 요일 알람 시, 다음 주 동일 요일 알람 예약
if (!isOneTimeAlarm) {
intent.getStringExtra(AlarmConstants.EXTRA_ALARM_DAY)?.let {
androidAlarmScheduler.scheduleWeeklyAlarm(alarm, AlarmDay.valueOf(it))
androidAlarmScheduler.rescheduleUpcomingWeeklyAlarm(alarm, AlarmDay.valueOf(it))
}
}

Expand Down
Loading