diff --git a/app.apk b/app.apk index 7127eae..b1f6803 100644 Binary files a/app.apk and b/app.apk differ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5691207..694d8bc 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 = 105 - versionName = "1.0.5" + versionCode = 106 + versionName = "1.0.6" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/example/shiftalarm/MainActivity.kt b/app/src/main/java/com/example/shiftalarm/MainActivity.kt index fa1df9c..960da08 100644 --- a/app/src/main/java/com/example/shiftalarm/MainActivity.kt +++ b/app/src/main/java/com/example/shiftalarm/MainActivity.kt @@ -270,118 +270,121 @@ class MainActivity : AppCompatActivity() { updateCalendar() } - private fun updateCalendar() { + private suspend fun loadCalendarDataForMonth(targetMonth: YearMonth) { val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) val selectedTeam = prefs.getString(KEY_TEAM, "A") ?: "A" val factory = prefs.getString("selected_factory", "Jeonju") ?: "Jeonju" - - // Today's Shift val today = LocalDate.now(ShiftCalculator.SEOUL_ZONE) - lifecycleScope.launch { - val db = AppDatabase.getDatabase(this@MainActivity) - val dao = db.shiftDao() - val repo = ShiftRepository(this@MainActivity) - - // 전체 사용자 알람 로드 (일원화된 Room DB 사용) - val allCustomAlarms = repo.getAllCustomAlarms() + val db = AppDatabase.getDatabase(this@MainActivity) + val dao = db.shiftDao() + val repo = ShiftRepository(this@MainActivity) + + // 전체 사용자 알람 로드 (일원화된 Room DB 사용) + val allCustomAlarms = repo.getAllCustomAlarms() - // 디스플레이 업데이트 - if (currentViewTeam == selectedTeam) { - val shiftForMyTeam = withContext(Dispatchers.IO) { repo.getShift(today, selectedTeam, factory) } - updateAlarmTimeDisplay(today, shiftForMyTeam, factory, allCustomAlarms) - binding.alarmTimeText.visibility = android.view.View.VISIBLE - } else { - binding.alarmTimeText.visibility = android.view.View.GONE + // 디스플레이 업데이트 + if (currentViewTeam == selectedTeam) { + val shiftForMyTeam = withContext(Dispatchers.IO) { repo.getShift(today, selectedTeam, factory) } + updateAlarmTimeDisplay(today, shiftForMyTeam, factory, allCustomAlarms) + binding.alarmTimeText.visibility = android.view.View.VISIBLE + } else { + binding.alarmTimeText.visibility = android.view.View.GONE + } + + // Load overrides and memos for the month + val monthStr = targetMonth.toString() + val overrides = withContext(Dispatchers.IO) { + dao.getOverridesForMonth(factory, currentViewTeam, monthStr).associateBy { overrideItem -> overrideItem.date } + } + val memos = withContext(Dispatchers.IO) { + dao.getMemosForMonth(monthStr).associateBy { memoItem -> memoItem.date } + } + + val days = generateDaysForMonthWithData(targetMonth, currentViewTeam, factory, overrides, memos) + + val availableHeight = binding.calendarGrid.height + val calcRowHeight = if (availableHeight > 0) availableHeight / 5 else 0 + + if (calendarAdapter == null || binding.calendarGrid.adapter == null) { + val newAdapter = CalendarAdapter(days, object : CalendarAdapter.OnDayClickListener { + override fun onDayClick(date: LocalDate, currentShift: String) { + showDaySettingsDialog(date, currentShift) + } + }, binding.cbShowHolidays.isChecked) + if (calcRowHeight > 0) { + newAdapter.rowHeight = calcRowHeight } - - // Load overrides and memos for the month - val monthStr = currentViewMonth.toString() - val overrides = withContext(Dispatchers.IO) { - dao.getOverridesForMonth(factory, currentViewTeam, monthStr).associateBy { overrideItem -> overrideItem.date } - } - val memos = withContext(Dispatchers.IO) { - dao.getMemosForMonth(monthStr).associateBy { memoItem -> memoItem.date } - } - - val days = generateDaysForMonthWithData(currentViewMonth, currentViewTeam, factory, overrides, memos) - - val availableHeight = binding.calendarGrid.height - val calcRowHeight = if (availableHeight > 0) availableHeight / 5 else 0 - - if (calendarAdapter == null || binding.calendarGrid.adapter == null) { - val newAdapter = CalendarAdapter(days, object : CalendarAdapter.OnDayClickListener { - override fun onDayClick(date: LocalDate, currentShift: String) { - showDaySettingsDialog(date, currentShift) - } - }, binding.cbShowHolidays.isChecked) + calendarAdapter = newAdapter + binding.calendarGrid.adapter = newAdapter + } else { + calendarAdapter?.let { ad -> + ad.days = days + ad.showHolidays = binding.cbShowHolidays.isChecked if (calcRowHeight > 0) { - newAdapter.rowHeight = calcRowHeight - } - calendarAdapter = newAdapter - binding.calendarGrid.adapter = newAdapter - } else { - calendarAdapter?.let { ad -> - ad.days = days - ad.showHolidays = binding.cbShowHolidays.isChecked - if (calcRowHeight > 0) { - ad.rowHeight = calcRowHeight - } - ad.notifyDataSetChanged() + ad.rowHeight = calcRowHeight } + ad.notifyDataSetChanged() } + } - binding.monthTitle.text = currentViewMonth.format(DateTimeFormatter.ofPattern("yyyy년 MM월")) + binding.monthTitle.text = targetMonth.format(DateTimeFormatter.ofPattern("yyyy년 MM월")) - if (calcRowHeight == 0) { - binding.calendarGrid.post { - val h = binding.calendarGrid.height - if (h > 0) { - val rH = h / 5 - calendarAdapter?.let { - if (it.rowHeight != rH) { - it.rowHeight = rH - it.notifyDataSetChanged() - } + if (calcRowHeight == 0) { + binding.calendarGrid.post { + val h = binding.calendarGrid.height + if (h > 0) { + val rH = h / 5 + calendarAdapter?.let { + if (it.rowHeight != rH) { + it.rowHeight = rH + it.notifyDataSetChanged() } } } } - - // Update Header Status Text with Permission Warning if needed - val shiftForViewingTeam = withContext(Dispatchers.IO) { repo.getShift(today, currentViewTeam, factory) } - val teamSuffix = if (currentViewTeam == selectedTeam) " (내 반)" else " (${currentViewTeam}반)" - - if (currentViewTeam == selectedTeam && !AlarmPermissionUtil.getExactAlarmStatus(this@MainActivity)) { - binding.todayStatusText.text = "⚠️ 정확한 알람 권한이 필요합니다 (설정 필요)" - binding.todayStatusText.setTextColor(androidx.core.content.ContextCompat.getColor(this@MainActivity, R.color.warning_red)) - } else { - binding.todayStatusText.text = "오늘의 근무: $shiftForViewingTeam$teamSuffix" - binding.todayStatusText.setTextColor(androidx.core.content.ContextCompat.getColor(this@MainActivity, R.color.text_secondary)) - } - - // Update Annual Leave display - val annualLeave = withContext(Dispatchers.IO) { repo.getAnnualLeave() } - annualLeave?.let { - binding.tvAnnualLeave.text = "연차: ${formatRemainingDays(it.remainingDays)}" - } ?: run { - binding.tvAnnualLeave.text = "연차: --" - } - - // Update Shift Summary Chips (v2.2.0) - val juCount = days.count { it.date != null && (it.shift == "주간" || it.shift == "주") } - val seokCount = days.count { it.date != null && (it.shift == "석간" || it.shift == "석") } - val yaCount = days.count { it.date != null && (it.shift == "야간" || it.shift == "야") } - val monthlyOvertime = withContext(Dispatchers.IO) { repo.getMonthlyOvertimeHours(currentViewMonth) } - - binding.chipJu.text = "주간 ${juCount}일" - binding.chipSeok.text = "석간 ${seokCount}일" - binding.chipYa.text = "야간 ${yaCount}일" - binding.chipOvertime.text = "연장 ${monthlyOvertime}h" } + + // Update Header Status Text with Permission Warning if needed + val shiftForViewingTeam = withContext(Dispatchers.IO) { repo.getShift(today, currentViewTeam, factory) } + val teamSuffix = if (currentViewTeam == selectedTeam) " (내 반)" else " (${currentViewTeam}반)" + + if (currentViewTeam == selectedTeam && !AlarmPermissionUtil.getExactAlarmStatus(this@MainActivity)) { + binding.todayStatusText.text = "⚠️ 정확한 알람 권한이 필요합니다 (설정 필요)" + binding.todayStatusText.setTextColor(androidx.core.content.ContextCompat.getColor(this@MainActivity, R.color.warning_red)) + } else { + binding.todayStatusText.text = "오늘의 근무: $shiftForViewingTeam$teamSuffix" + binding.todayStatusText.setTextColor(androidx.core.content.ContextCompat.getColor(this@MainActivity, R.color.text_secondary)) + } + + // Update Annual Leave display + val annualLeave = withContext(Dispatchers.IO) { repo.getAnnualLeave() } + annualLeave?.let { + binding.tvAnnualLeave.text = "연차: ${formatRemainingDays(it.remainingDays)}" + } ?: run { + binding.tvAnnualLeave.text = "연차: --" + } + + // Update Shift Summary Chips (v2.2.0) + val juCount = days.count { it.date != null && (it.shift == "주간" || it.shift == "주") } + val seokCount = days.count { it.date != null && (it.shift == "석간" || it.shift == "석") } + val yaCount = days.count { it.date != null && (it.shift == "야간" || it.shift == "야") } + val monthlyOvertime = withContext(Dispatchers.IO) { repo.getMonthlyOvertimeHours(targetMonth) } + + binding.chipJu.text = "주간 ${juCount}일" + binding.chipSeok.text = "석간 ${seokCount}일" + binding.chipYa.text = "야간 ${yaCount}일" + binding.chipOvertime.text = "연장 ${monthlyOvertime}h" + updateOtherTeamsLayout(today, factory, prefs) } + private fun updateCalendar() { + lifecycleScope.launch { + loadCalendarDataForMonth(currentViewMonth) + } + } + private fun updateOtherTeamsLayout(today: LocalDate, factory: String, prefs: android.content.SharedPreferences) { val teamColors = mapOf( "A" to R.color.team_a_color, @@ -751,12 +754,14 @@ class MainActivity : AppCompatActivity() { val snapshot = binding.calendarOverlaySnapshot val width = card.width.toFloat() + val nextMonth = if (direction > 0) { + currentViewMonth.plusMonths(1) + } else { + currentViewMonth.minusMonths(1) + } + if (width == 0f) { - currentViewMonth = if (direction > 0) { - currentViewMonth.plusMonths(1) - } else { - currentViewMonth.minusMonths(1) - } + currentViewMonth = nextMonth updateCalendar() return } @@ -778,44 +783,37 @@ class MainActivity : AppCompatActivity() { Log.w("MainActivity", "Failed to capture snapshot for month transition", e) } - // 2. Update to new month immediately - currentViewMonth = if (direction > 0) { - currentViewMonth.plusMonths(1) - } else { - currentViewMonth.minusMonths(1) - } - updateCalendar() - - // 3. Prepare incoming new month card on top + // 2. Position top card offscreen before loading card.translationX = if (direction > 0) width else -width - card.alpha = 1.0f - card.cardElevation = dpToPx(8f).toFloat() + currentViewMonth = nextMonth - val fluidInterpolator = androidx.core.view.animation.PathInterpolatorCompat.create(0.22f, 1.0f, 0.36f, 1.0f) - val duration = 280L + // 3. Load next month's data BEFORE starting slide animation to eliminate any flicker + lifecycleScope.launch { + loadCalendarDataForMonth(currentViewMonth) - // 4. Slide top new month card smoothly into position - card.animate() - .translationX(0f) - .setDuration(duration) - .setInterpolator(fluidInterpolator) - .withEndAction { - card.cardElevation = 0f - snapshot.visibility = android.view.View.GONE - snapshot.setImageDrawable(null) - isAnimatingMonth = false - } - .start() + val fluidInterpolator = androidx.core.view.animation.PathInterpolatorCompat.create(0.22f, 1.0f, 0.36f, 1.0f) + val duration = 280L - // 5. Parallax fade & scale down bottom snapshot card - snapshot.animate() - .scaleX(0.94f) - .scaleY(0.94f) - .alpha(0.35f) - .translationX(if (direction > 0) -width * 0.22f else width * 0.22f) - .setDuration(duration) - .setInterpolator(fluidInterpolator) - .start() + card.animate() + .translationX(0f) + .setDuration(duration) + .setInterpolator(fluidInterpolator) + .withEndAction { + snapshot.visibility = android.view.View.GONE + snapshot.setImageDrawable(null) + isAnimatingMonth = false + } + .start() + + snapshot.animate() + .scaleX(0.94f) + .scaleY(0.94f) + .alpha(0.35f) + .translationX(if (direction > 0) -width * 0.22f else width * 0.22f) + .setDuration(duration) + .setInterpolator(fluidInterpolator) + .start() + } } private fun formatRemainingDays(days: Float): String { diff --git a/app/src/main/res/drawable/ic_factory.xml b/app/src/main/res/drawable/ic_factory.xml new file mode 100755 index 0000000..f0a23c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_factory.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_layers.xml b/app/src/main/res/drawable/ic_layers.xml new file mode 100755 index 0000000..732c59d --- /dev/null +++ b/app/src/main/res/drawable/ic_layers.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_palette.xml b/app/src/main/res/drawable/ic_palette.xml new file mode 100755 index 0000000..197d5c2 --- /dev/null +++ b/app/src/main/res/drawable/ic_palette.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100755 index 0000000..29ffbb1 --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_tide.xml b/app/src/main/res/drawable/ic_tide.xml new file mode 100755 index 0000000..7cf58ff --- /dev/null +++ b/app/src/main/res/drawable/ic_tide.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_settings_additional.xml b/app/src/main/res/layout/fragment_settings_additional.xml index 01eac28..daa8198 100644 --- a/app/src/main/res/layout/fragment_settings_additional.xml +++ b/app/src/main/res/layout/fragment_settings_additional.xml @@ -49,8 +49,8 @@ android:layout_width="20dp" android:layout_height="20dp" android:layout_gravity="center" - android:src="@drawable/ic_plus_circle" - app:tint="@color/primary"/> + android:src="@drawable/ic_tide" + app:tint="#0EA5E9"/> + android:src="@drawable/ic_palette" + app:tint="#AF52DE"/> + app:tint="#0284C7"/> + app:tint="#FF9500"/> + android:src="@drawable/ic_share" + app:tint="#34C759"/> + android:src="@drawable/ic_factory" + app:tint="#007AFF"/> + android:src="@drawable/ic_user" + app:tint="#34C759"/> + android:src="@drawable/ic_layers" + app:tint="#00C7BE"/>