diff --git a/app.apk b/app.apk index 8d531cf..6e0e8c3 100644 Binary files a/app.apk and b/app.apk differ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 70a612e..52c1393 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 = 102 - versionName = "1.0.2" + versionCode = 103 + versionName = "1.0.3" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/example/shiftalarm/AlarmSyncManager.kt b/app/src/main/java/com/example/shiftalarm/AlarmSyncManager.kt index 7546b35..c6ff4b0 100644 --- a/app/src/main/java/com/example/shiftalarm/AlarmSyncManager.kt +++ b/app/src/main/java/com/example/shiftalarm/AlarmSyncManager.kt @@ -31,6 +31,19 @@ object AlarmSyncManager { // 1. DB에 알람 추가 val alarmId = repo.addCustomAlarm(alarm) Log.d(TAG, "알람 DB 추가 완료: ID=$alarmId") + + // 시스템 알람 볼륨이 0인 경우 최대치로 자동 보정 + try { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as android.media.AudioManager + val maxVol = audioManager.getStreamMaxVolume(android.media.AudioManager.STREAM_ALARM) + val currVol = audioManager.getStreamVolume(android.media.AudioManager.STREAM_ALARM) + if (currVol == 0) { + audioManager.setStreamVolume(android.media.AudioManager.STREAM_ALARM, maxVol, 0) + Log.d(TAG, "알람 볼륨이 0이어서 최대($maxVol)로 자동 설정") + } + } catch (e: Exception) { + Log.w(TAG, "볼륨 자동 보정 실패", e) + } // 2. AlarmManager에 예약 val today = LocalDate.now(SEOUL_ZONE) diff --git a/app/src/main/java/com/example/shiftalarm/CalendarAdapter.kt b/app/src/main/java/com/example/shiftalarm/CalendarAdapter.kt index 7a3c995..62e1e34 100644 --- a/app/src/main/java/com/example/shiftalarm/CalendarAdapter.kt +++ b/app/src/main/java/com/example/shiftalarm/CalendarAdapter.kt @@ -32,6 +32,7 @@ class CalendarAdapter( class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val root: View = view.findViewById(R.id.dayRoot) val dayNumber: TextView = view.findViewById(R.id.dayNumber) + val tvShiftSmall: TextView = view.findViewById(R.id.tvShiftSmall) val shiftChar: TextView = view.findViewById(R.id.shiftChar) val holidayNameSmall: TextView = view.findViewById(R.id.holidayNameSmall) val memoIndicator: ImageView = view.findViewById(R.id.memoIndicator) @@ -71,6 +72,7 @@ class CalendarAdapter( val isSaturday = item.date.dayOfWeek == java.time.DayOfWeek.SATURDAY val fullHolidayName = HolidayManager.getHolidayName(item.date) val isToday = item.date == LocalDate.now() + val hasMemo = item.hasMemo && !item.memoContent.isNullOrEmpty() // Day Number Color if (fullHolidayName != null || isSunday) { @@ -102,82 +104,97 @@ class CalendarAdapter( // --- Shift & Holiday Display Logic --- holder.shiftChar.background = null holder.shiftChar.text = "" + holder.tvShiftSmall.visibility = View.GONE holder.holidayNameSmall.visibility = View.GONE holder.shiftChar.textSize = 18f - // "반월", "반년" (Half-Monthly, Half-Yearly) Special Logic - // These are overrides or specific shifts that user sets. - // User requested: "월", "년" text. Half-filled background (Red + Transparent). - // Check exact string or "startswith" if logic changed? - // Logic in adapter `getShift` might return "반월", "반년". - if (showHolidays && fullHolidayName != null) { // Holiday Mode (Priority): Show full holiday name, no circle holder.shiftChar.text = fullHolidayName holder.shiftChar.setTextColor(Color.parseColor("#FF5252")) - holder.shiftChar.textSize = 13f + holder.shiftChar.textSize = 13f holder.shiftChar.background = null + holder.shiftChar.visibility = View.VISIBLE } else if (item.shift != null && item.shift != "비번") { // Shift Mode - - // Handle specific "Half" cases first - if (item.shift == "반월" || item.shift == "반년") { - holder.shiftChar.text = if (item.shift == "반월") "월" else "년" - holder.shiftChar.setTextColor(ContextCompat.getColor(context, R.color.black)) // Black for contrast on Half Red/Transparent - holder.shiftChar.textSize = 18f - holder.shiftChar.background = ContextCompat.getDrawable(context, R.drawable.bg_shift_half_red) + val shiftAbbreviation = when (item.shift) { + "주간" -> "주" + "석간" -> "석" + "야간" -> "야" + "주간 맞교대" -> "주맞" + "야간 맞교대" -> "야맞" + "휴무", "휴가" -> "휴" + "월차" -> "월" + "연차" -> "연" + "반월" -> "월" + "반년" -> "년" + "교육" -> "교" + else -> item.shift.take(2) + } + + val shiftColorRes = when (item.shift) { + "주간" -> R.color.shift_lemon + "석간" -> R.color.shift_seok + "야간" -> R.color.shift_ya + "주간 맞교대" -> R.color.shift_jumat + "야간 맞교대" -> R.color.shift_yamat + "휴무", "휴가", "월차", "연차" -> R.color.shift_red + "반월", "반년" -> R.color.shift_red + "교육" -> R.color.primary + else -> R.color.text_secondary + } + val shiftColor = ContextCompat.getColor(context, shiftColorRes) + + if (hasMemo) { + // When day has a memo: show small shift badge next to day number, hide large center shift circle + holder.shiftChar.visibility = View.GONE + holder.tvShiftSmall.visibility = View.VISIBLE + holder.tvShiftSmall.text = shiftAbbreviation + + val smallBg = ContextCompat.getDrawable(context, R.drawable.bg_shift_stroke_v4) as? android.graphics.drawable.GradientDrawable + smallBg?.mutate() + smallBg?.setStroke(dpToPx(context, 1.2f), shiftColor) + smallBg?.setColor(if (isToday) shiftColor else Color.TRANSPARENT) + holder.tvShiftSmall.background = smallBg + holder.tvShiftSmall.setTextColor(if (isToday && (item.shift == "야간" || item.shift == "교육")) Color.WHITE else shiftColor) } else { - // Standard Logic - val shiftAbbreviation = when (item.shift) { - "주간" -> "주" - "석간" -> "석" - "야간" -> "야" - "주간 맞교대" -> "주맞" - "야간 맞교대" -> "야맞" - "휴무", "휴가" -> "휴" - "월차" -> "월" - "연차" -> "연" - "교육" -> "교" - else -> item.shift.take(1) - } - holder.shiftChar.text = shiftAbbreviation - holder.shiftChar.textSize = 20f - holder.shiftChar.setTypeface(null, android.graphics.Typeface.BOLD) + // When day has NO memo: standard large centered shift circle + holder.shiftChar.visibility = View.VISIBLE + holder.tvShiftSmall.visibility = View.GONE - val shiftColorRes = when (item.shift) { - "주간" -> R.color.shift_lemon - "석간" -> R.color.shift_seok - "야간" -> R.color.shift_ya - "주간 맞교대" -> R.color.shift_jumat - "야간 맞교대" -> R.color.shift_yamat - "휴무", "휴가", "월차", "연차" -> R.color.shift_red - "교육" -> R.color.primary - else -> R.color.text_secondary - } - val shiftColor = ContextCompat.getColor(context, shiftColorRes) - - if (isToday) { - // Today: Solid Circle - val background = ContextCompat.getDrawable(context, R.drawable.bg_shift_solid_v4) as? android.graphics.drawable.GradientDrawable - background?.setColor(shiftColor) - holder.shiftChar.background = background - holder.shiftChar.backgroundTintList = null - - if (item.shift == "주간" || item.shift == "석간") { - holder.shiftChar.setTextColor(ContextCompat.getColor(context, R.color.black)) - } else { - holder.shiftChar.setTextColor(Color.WHITE) - } + if (item.shift == "반월" || item.shift == "반년") { + holder.shiftChar.text = if (item.shift == "반월") "월" else "년" + holder.shiftChar.setTextColor(ContextCompat.getColor(context, R.color.black)) + holder.shiftChar.textSize = 18f + holder.shiftChar.background = ContextCompat.getDrawable(context, R.drawable.bg_shift_half_red) } else { - // Not Today: Stroke Circle - val background = ContextCompat.getDrawable(context, R.drawable.bg_shift_stroke_v4) as? android.graphics.drawable.GradientDrawable - background?.setStroke(dpToPx(context, 1.5f), shiftColor) - background?.setColor(Color.TRANSPARENT) - - holder.shiftChar.background = background - holder.shiftChar.backgroundTintList = null - - holder.shiftChar.setTextColor(shiftColor) + holder.shiftChar.text = shiftAbbreviation + holder.shiftChar.textSize = 20f + holder.shiftChar.setTypeface(null, android.graphics.Typeface.BOLD) + + if (isToday) { + // Today: Solid Circle + val background = ContextCompat.getDrawable(context, R.drawable.bg_shift_solid_v4) as? android.graphics.drawable.GradientDrawable + background?.setColor(shiftColor) + holder.shiftChar.background = background + holder.shiftChar.backgroundTintList = null + + if (item.shift == "주간" || item.shift == "석간") { + holder.shiftChar.setTextColor(ContextCompat.getColor(context, R.color.black)) + } else { + holder.shiftChar.setTextColor(Color.WHITE) + } + } else { + // Not Today: Stroke Circle + val background = ContextCompat.getDrawable(context, R.drawable.bg_shift_stroke_v4) as? android.graphics.drawable.GradientDrawable + background?.setStroke(dpToPx(context, 1.5f), shiftColor) + background?.setColor(Color.TRANSPARENT) + + holder.shiftChar.background = background + holder.shiftChar.backgroundTintList = null + + holder.shiftChar.setTextColor(shiftColor) + } } } } @@ -187,41 +204,25 @@ class CalendarAdapter( holder.holidayNameSmall.visibility = View.VISIBLE holder.holidayNameSmall.text = fullHolidayName } else { - // Ensure visibility GONE if not needed (e.g. standard day) - holder.holidayNameSmall.visibility = View.GONE + holder.holidayNameSmall.visibility = View.GONE } - - // Double check: if showHolidays=true (Holiday mode), we handled it at top block. - // But if showHolidays=true and NO holiday, we show lunar date? - // User asked: "Overlap date and holiday text". - // My item_day.xml has holidayNameSmall at bottom now. - // If showHolidays=true, CalendarAdapter usually HIDES shiftChar and shows Holiday Name? - // Wait, standard logic (lines 84-91 above): - // If showHolidays && fullHolidayName != null -> shiftChar shows Name. - // If showHolidays && fullHolidayName == null -> shiftChar shows LUNAR DATE? (Old logic had this). if (showHolidays && fullHolidayName == null) { - // Show Lunar Date in shiftChar instead of empty? - // Or shiftChar is empty, show small text? - // Previous code: - // holder.shiftChar.text = "" - // holder.holidayNameSmall.visibility = View.VISIBLE - // holder.holidayNameSmall.text = HolidayManager.getLunarDateString(item.date) - holder.shiftChar.text = HolidayManager.getLunarDateString(item.date) holder.shiftChar.textSize = 13f holder.shiftChar.setTextColor(ContextCompat.getColor(context, R.color.text_tertiary)) holder.shiftChar.background = null + holder.shiftChar.visibility = View.VISIBLE } - - // Memo Indicator - holder.memoIndicator.visibility = View.GONE // Hide indicator, showing text instead - if (item.hasMemo && !item.memoContent.isNullOrEmpty()) { + // Memo Content Text (Up to 4 lines in center of day cell) + if (hasMemo) { holder.memoContent.visibility = View.VISIBLE holder.memoContent.text = item.memoContent + holder.memoIndicator.visibility = View.VISIBLE } else { holder.memoContent.visibility = View.GONE + holder.memoIndicator.visibility = View.GONE } // Today Border or Highlight diff --git a/app/src/main/java/com/example/shiftalarm/FragmentSettingsAlarm.kt b/app/src/main/java/com/example/shiftalarm/FragmentSettingsAlarm.kt index 1449b4d..ced2e85 100644 --- a/app/src/main/java/com/example/shiftalarm/FragmentSettingsAlarm.kt +++ b/app/src/main/java/com/example/shiftalarm/FragmentSettingsAlarm.kt @@ -476,7 +476,18 @@ class FragmentSettingsAlarm : Fragment(), SharedPreferences.OnSharedPreferenceCh Log.w("ShiftAlarm", "soundUri가 비어있어 기본값으로 재설정: $currentDialogSoundUri") } - Log.d("ShiftAlarm", "알람 ${if (isNew) "생성" else "수정"} - 최종 soundUri: $currentDialogSoundUri") + // 새 알람 추가 시 또는 볼륨이 0인 경우 시스템 알람 스트림 볼륨을 최대치로 설정 + try { + val audioManager = requireContext().getSystemService(Context.AUDIO_SERVICE) as android.media.AudioManager + val maxVol = audioManager.getStreamMaxVolume(android.media.AudioManager.STREAM_ALARM) + val currVol = audioManager.getStreamVolume(android.media.AudioManager.STREAM_ALARM) + if (isNew || currVol == 0) { + audioManager.setStreamVolume(android.media.AudioManager.STREAM_ALARM, maxVol, 0) + Log.d("ShiftAlarm", "알람 스트림 볼륨 최대치($maxVol)로 초기화 완료") + } + } catch (e: Exception) { + Log.w("ShiftAlarm", "알람 볼륨 설정 실패", e) + } fun updateSoundName(uriStr: String?) { if (uriStr.isNullOrEmpty() || uriStr == "null") { diff --git a/app/src/main/res/layout/item_day.xml b/app/src/main/res/layout/item_day.xml index 2b6896d..8648617 100644 --- a/app/src/main/res/layout/item_day.xml +++ b/app/src/main/res/layout/item_day.xml @@ -7,19 +7,43 @@ android:layout_height="wrap_content" android:background="@drawable/bg_grid_cell_v4"> - - + + app:layout_constraintTop_toTopOf="parent"> + + + + + + - + - + + app:layout_constraintEnd_toEndOf="parent"/>