Initial commit - v1.1.9
This commit is contained in:
691
app/src/main/java/com/example/shiftalarm/MainActivity.kt
Normal file
691
app/src/main/java/com/example/shiftalarm/MainActivity.kt
Normal file
@@ -0,0 +1,691 @@
|
||||
package com.example.shiftalarm
|
||||
|
||||
import android.Manifest
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import com.example.shiftalarm.databinding.ActivityMainBinding
|
||||
import java.time.LocalDate
|
||||
import java.time.YearMonth
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.concurrent.TimeUnit
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import kotlin.math.abs
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private val PREFS_NAME = "ShiftAlarmPrefs"
|
||||
private val KEY_TEAM = "selected_team"
|
||||
private var currentViewMonth: YearMonth = YearMonth.now(ShiftCalculator.SEOUL_ZONE)
|
||||
private var currentViewTeam: String = "A"
|
||||
private lateinit var gestureDetector: GestureDetector
|
||||
|
||||
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
// Smooth transition for theme change
|
||||
finish()
|
||||
startActivity(Intent(this, javaClass))
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val density = resources.displayMetrics.density
|
||||
val p = (8 * density).toInt()
|
||||
v.setPadding(systemBars.left + p, systemBars.top + p, systemBars.right + p, systemBars.bottom + p)
|
||||
insets
|
||||
}
|
||||
|
||||
setupCalendar()
|
||||
setupWorker()
|
||||
setupSwipeGesture()
|
||||
AppUpdateManager.checkUpdate(this, silent = true)
|
||||
checkRoot()
|
||||
|
||||
val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
currentViewTeam = prefs.getString(KEY_TEAM, "A") ?: "A"
|
||||
|
||||
// Default to Shift Calendar mode (checkbox unchecked)
|
||||
binding.cbShowHolidays.isChecked = false
|
||||
|
||||
binding.btnSettings.setOnClickListener {
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
}
|
||||
|
||||
binding.prevMonth.setOnClickListener {
|
||||
currentViewMonth = currentViewMonth.minusMonths(1)
|
||||
updateCalendar()
|
||||
}
|
||||
|
||||
binding.monthTitle.setOnClickListener {
|
||||
showMonthYearPicker()
|
||||
}
|
||||
|
||||
binding.nextMonth.setOnClickListener {
|
||||
currentViewMonth = currentViewMonth.plusMonths(1)
|
||||
updateCalendar()
|
||||
}
|
||||
|
||||
binding.btnToday.setOnClickListener {
|
||||
val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
currentViewTeam = prefs.getString(KEY_TEAM, "A") ?: "A"
|
||||
currentViewMonth = YearMonth.now(ShiftCalculator.SEOUL_ZONE)
|
||||
updateCalendar()
|
||||
}
|
||||
|
||||
binding.cbShowHolidays.setOnCheckedChangeListener { _, _ ->
|
||||
updateCalendar()
|
||||
}
|
||||
|
||||
binding.alarmInfoBar.setOnClickListener {
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
intent.putExtra("TARGET_TAB", 1) // 1 is Alarm tab
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
// Tide Location Cycle Logic
|
||||
val tideLocations = listOf("군산", "변산", "여수", "태안")
|
||||
binding.btnTideLocation.setOnClickListener {
|
||||
val currentLoc = prefs.getString("selected_tide_location", "군산") ?: "군산"
|
||||
val nextIndex = (tideLocations.indexOf(currentLoc) + 1) % tideLocations.size
|
||||
val nextLoc = tideLocations[nextIndex]
|
||||
|
||||
prefs.edit().putString("selected_tide_location", nextLoc).apply()
|
||||
binding.btnTideLocation.text = nextLoc
|
||||
updateCalendar()
|
||||
}
|
||||
|
||||
// setupWorker(), checkRoot() 등은 이미 호출됨
|
||||
}
|
||||
|
||||
private fun updateTideButtonVisibility() {
|
||||
val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val showTide = prefs.getBoolean("show_tide", false)
|
||||
val currentLoc = prefs.getString("selected_tide_location", "군산") ?: "군산"
|
||||
|
||||
if (showTide) {
|
||||
binding.btnTideLocation.visibility = android.view.View.VISIBLE
|
||||
binding.btnTideLocation.text = currentLoc
|
||||
} else {
|
||||
binding.btnTideLocation.visibility = android.view.View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSwipeGesture() {
|
||||
gestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
|
||||
private val SWIPE_THRESHOLD = 100
|
||||
private val SWIPE_VELOCITY_THRESHOLD = 100
|
||||
|
||||
override fun onFling(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent,
|
||||
velocityX: Float,
|
||||
velocityY: Float
|
||||
): Boolean {
|
||||
if (e1 == null) return false
|
||||
|
||||
val diffX = e2.x - e1.x
|
||||
val diffY = e2.y - e1.y
|
||||
|
||||
if (abs(diffX) > abs(diffY)) {
|
||||
if (abs(diffX) > SWIPE_THRESHOLD && abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
|
||||
if (diffX > 0) {
|
||||
// Swipe Right -> Previous Month
|
||||
currentViewMonth = currentViewMonth.minusMonths(1)
|
||||
updateCalendar()
|
||||
} else {
|
||||
// Swipe Left -> Next Month
|
||||
currentViewMonth = currentViewMonth.plusMonths(1)
|
||||
updateCalendar()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
binding.calendarGrid.addOnItemTouchListener(object : androidx.recyclerview.widget.RecyclerView.OnItemTouchListener {
|
||||
override fun onInterceptTouchEvent(rv: androidx.recyclerview.widget.RecyclerView, e: MotionEvent): Boolean {
|
||||
gestureDetector.onTouchEvent(e)
|
||||
return false
|
||||
}
|
||||
override fun onTouchEvent(rv: androidx.recyclerview.widget.RecyclerView, e: MotionEvent) {}
|
||||
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {}
|
||||
})
|
||||
|
||||
binding.calendarContainer.setOnTouchListener { _, event ->
|
||||
gestureDetector.onTouchEvent(event)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
currentViewTeam = prefs.getString(KEY_TEAM, "A") ?: "A"
|
||||
|
||||
updateTideButtonVisibility()
|
||||
updateCalendar()
|
||||
|
||||
// 일원화된 통합 권한 체크 실행 (신뢰도 100% 보장)
|
||||
AlarmPermissionUtil.checkAndRequestAllPermissions(this)
|
||||
|
||||
// 설정 변경 시 즉시 반영을 위한 강제 동기화 (30일 스케줄링)
|
||||
lifecycleScope.launch {
|
||||
syncAllAlarms(this@MainActivity)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMonthYearPicker() {
|
||||
val dialogView = layoutInflater.inflate(R.layout.dialog_month_year_picker, null)
|
||||
val yearPicker = dialogView.findViewById<android.widget.NumberPicker>(R.id.yearPicker)
|
||||
val monthPicker = dialogView.findViewById<android.widget.NumberPicker>(R.id.monthPicker)
|
||||
|
||||
val currentYear = currentViewMonth.year
|
||||
val currentMonth = currentViewMonth.monthValue
|
||||
|
||||
yearPicker.minValue = 2010
|
||||
yearPicker.maxValue = 2050
|
||||
yearPicker.value = currentYear
|
||||
yearPicker.wrapSelectorWheel = false
|
||||
|
||||
monthPicker.minValue = 1
|
||||
monthPicker.maxValue = 12
|
||||
monthPicker.value = currentMonth
|
||||
monthPicker.displayedValues = arrayOf("1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월")
|
||||
|
||||
val dialog = androidx.appcompat.app.AlertDialog.Builder(this, R.style.OneUI8_Dialog)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("이동") { _, _ ->
|
||||
currentViewMonth = YearMonth.of(yearPicker.value, monthPicker.value)
|
||||
updateCalendar()
|
||||
}
|
||||
.setNegativeButton("취소", null)
|
||||
.create()
|
||||
|
||||
dialog.show()
|
||||
|
||||
// 90% Screen Width for One UI 8.0 feel
|
||||
val width = (resources.displayMetrics.widthPixels * 0.9).toInt()
|
||||
dialog.window?.setLayout(width, android.view.ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
dialog.window?.setDimAmount(0.6f) // Darker dim to focus on popup
|
||||
|
||||
// Style buttons to look like One UI 8.0
|
||||
dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).apply {
|
||||
setTextColor(ContextCompat.getColor(this@MainActivity, R.color.primary))
|
||||
textSize = 17f
|
||||
setPadding(dpToPx(32f), dpToPx(16f), dpToPx(32f), dpToPx(16f))
|
||||
}
|
||||
dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEGATIVE).apply {
|
||||
setTextColor(ContextCompat.getColor(this@MainActivity, R.color.text_secondary))
|
||||
textSize = 17f
|
||||
setPadding(dpToPx(32f), dpToPx(16f), dpToPx(32f), dpToPx(16f))
|
||||
}
|
||||
}
|
||||
|
||||
private fun dpToPx(dp: Float): Int {
|
||||
return (dp * resources.displayMetrics.density).toInt()
|
||||
}
|
||||
|
||||
private fun setupCalendar() {
|
||||
binding.calendarGrid.layoutManager = GridLayoutManager(this, 7)
|
||||
updateCalendar()
|
||||
}
|
||||
|
||||
private fun updateCalendar() {
|
||||
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()
|
||||
|
||||
// 디스플레이 업데이트
|
||||
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 = 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 adapter = CalendarAdapter(days, object : CalendarAdapter.OnDayClickListener {
|
||||
override fun onDayClick(date: LocalDate, currentShift: String) {
|
||||
showDaySettingsDialog(date, currentShift)
|
||||
}
|
||||
}, binding.cbShowHolidays.isChecked)
|
||||
|
||||
binding.calendarGrid.adapter = adapter
|
||||
binding.monthTitle.text = currentViewMonth.format(DateTimeFormatter.ofPattern("yyyy년 MM월"))
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
updateOtherTeamsLayout(today, factory, prefs)
|
||||
}
|
||||
|
||||
private fun updateOtherTeamsLayout(today: LocalDate, factory: String, prefs: android.content.SharedPreferences) {
|
||||
val teamColors = mapOf(
|
||||
"A" to R.color.team_a_color,
|
||||
"B" to R.color.team_b_color,
|
||||
"C" to R.color.team_c_color,
|
||||
"D" to R.color.team_d_color
|
||||
)
|
||||
val container = binding.otherTeamsContainer
|
||||
container.removeAllViews()
|
||||
|
||||
val rowLayout = android.widget.LinearLayout(this).apply {
|
||||
orientation = android.widget.LinearLayout.HORIZONTAL
|
||||
layoutParams = android.widget.LinearLayout.LayoutParams(
|
||||
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
val allTeams = if (factory == "Nonsan") listOf("A", "B", "C") else listOf("A", "B", "C", "D")
|
||||
|
||||
lifecycleScope.launch {
|
||||
for (t in allTeams) {
|
||||
val shift = ShiftCalculator.getShift(today, t, factory)
|
||||
val shortShift = when(shift) {
|
||||
"주간" -> "주"
|
||||
"석간" -> "석"
|
||||
"야간" -> "야"
|
||||
"주간 맞교대" -> "주맞"
|
||||
"야간 맞교대" -> "야맞"
|
||||
"휴무", "휴가" -> "휴"
|
||||
else -> shift.take(1)
|
||||
}
|
||||
|
||||
val textView = android.widget.TextView(this@MainActivity).apply {
|
||||
text = "${t}반 ($shortShift)"
|
||||
setPadding(0, 24, 0, 24)
|
||||
textSize = 12f
|
||||
gravity = android.view.Gravity.CENTER
|
||||
setTypeface(null, android.graphics.Typeface.BOLD)
|
||||
|
||||
if (currentViewTeam == t) {
|
||||
setTextColor(android.graphics.Color.WHITE)
|
||||
setBackgroundResource(R.drawable.bg_pill_rect_selected)
|
||||
} else {
|
||||
setTextColor(androidx.core.content.ContextCompat.getColor(context, teamColors[t]!!))
|
||||
setBackgroundResource(R.drawable.bg_pill_rect_unselected)
|
||||
}
|
||||
|
||||
layoutParams = android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1f).apply {
|
||||
setMargins(4, 0, 4, 0)
|
||||
}
|
||||
|
||||
setOnClickListener {
|
||||
if (currentViewTeam != t) {
|
||||
currentViewTeam = t
|
||||
updateCalendar()
|
||||
Toast.makeText(context, "${t}반 근무표를 표시합니다.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
rowLayout.addView(textView)
|
||||
}
|
||||
container.addView(rowLayout)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateDaysForMonthWithData(
|
||||
month: YearMonth,
|
||||
team: String,
|
||||
factory: String,
|
||||
overrides: Map<String, ShiftOverride>,
|
||||
memos: Map<String, DailyMemo>
|
||||
): List<DayShift> {
|
||||
val daysInMonth = month.lengthOfMonth()
|
||||
val firstDayOfMonth = month.atDay(1).dayOfWeek.value % 7
|
||||
val actualDayCount = firstDayOfMonth + daysInMonth
|
||||
val targetCells = if (actualDayCount <= 35) 35 else 42
|
||||
|
||||
val dayList = mutableListOf<DayShift>()
|
||||
|
||||
for (i in 0 until firstDayOfMonth) {
|
||||
dayList.add(DayShift(null, null))
|
||||
}
|
||||
|
||||
for (day in 1..daysInMonth) {
|
||||
val date = month.atDay(day)
|
||||
val dateStr = date.toString()
|
||||
|
||||
val shift = overrides[dateStr]?.shift ?: ShiftCalculator.getShift(date, team, factory)
|
||||
val memo = memos[dateStr]
|
||||
val hasMemo = memo != null
|
||||
val memoContent = memo?.content
|
||||
|
||||
dayList.add(DayShift(date, shift, hasMemo, memoContent))
|
||||
}
|
||||
|
||||
while (dayList.size < targetCells) {
|
||||
dayList.add(DayShift(null, null))
|
||||
}
|
||||
return dayList
|
||||
}
|
||||
|
||||
private fun getAlarmsStrForDate(date: LocalDate, shift: String, allAlarms: List<CustomAlarm>): List<String> {
|
||||
val alarmTimes = mutableListOf<String>()
|
||||
val isOff = shift == "휴무" || shift == "휴가"
|
||||
|
||||
for (alarm in allAlarms) {
|
||||
if (alarm.isEnabled && (alarm.shiftType == "기타" || (!isOff && alarm.shiftType == shift))) {
|
||||
if (!alarmTimes.contains(alarm.time)) {
|
||||
alarmTimes.add(alarm.time)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
alarmTimes.sort()
|
||||
return alarmTimes
|
||||
}
|
||||
|
||||
private fun updateAlarmTimeDisplay(date: LocalDate, shift: String, factory: String, allAlarms: List<CustomAlarm>) {
|
||||
val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
// 0. Check Master Switch
|
||||
if (!ShiftAlarmDefaults.isMasterAlarmEnabled(prefs)) {
|
||||
binding.alarmTimeText.text = "전체 알람 꺼짐"
|
||||
binding.alarmTimeText.visibility = android.view.View.VISIBLE
|
||||
binding.alarmTimeText.setTextColor(androidx.core.content.ContextCompat.getColor(this, R.color.shift_red))
|
||||
binding.alarmTimeText.setTypeface(null, android.graphics.Typeface.BOLD)
|
||||
return
|
||||
}
|
||||
|
||||
val todayStr = getAlarmsStrForDate(date, shift, allAlarms).firstOrNull() ?: "없음"
|
||||
val tomorrowDate = date.plusDays(1)
|
||||
val repo = ShiftRepository(this)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val tomorrowShiftFull = repo.getShift(tomorrowDate, currentViewTeam, factory)
|
||||
val tomorrowAlarms = getAlarmsStrForDate(tomorrowDate, tomorrowShiftFull, allAlarms)
|
||||
val tomorrowStr = tomorrowAlarms.firstOrNull() ?: "없음"
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (!ShiftAlarmDefaults.isMasterAlarmEnabled(prefs)) {
|
||||
binding.alarmTimeText.text = "전체 알람 꺼짐"
|
||||
binding.alarmTimeText.setTextColor(androidx.core.content.ContextCompat.getColor(this@MainActivity, R.color.shift_red))
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val todayLabel = if (todayStr == "없음") "오늘: 없음" else "오늘: $todayStr"
|
||||
val tomorrowLabel = if (tomorrowStr == "없음") "내일: 없음" else "내일: $tomorrowStr"
|
||||
|
||||
binding.alarmTimeText.text = "$todayLabel | $tomorrowLabel"
|
||||
binding.alarmTimeText.visibility = android.view.View.VISIBLE
|
||||
binding.alarmTimeText.setTypeface(null, android.graphics.Typeface.BOLD)
|
||||
binding.alarmTimeText.setTextColor(androidx.core.content.ContextCompat.getColor(this@MainActivity, R.color.text_primary))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDaySettingsDialog(date: LocalDate, currentShift: String) {
|
||||
val dialogView = layoutInflater.inflate(R.layout.dialog_day_settings, null)
|
||||
val dialog = androidx.appcompat.app.AlertDialog.Builder(this)
|
||||
.setView(dialogView)
|
||||
.create()
|
||||
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
|
||||
|
||||
val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val factory = prefs.getString("selected_factory", "Jeonju") ?: "Jeonju"
|
||||
val team = prefs.getString(KEY_TEAM, "A") ?: "A"
|
||||
|
||||
// Set Title - One UI Style Header
|
||||
val titleText = dialogView.findViewById<android.widget.TextView>(R.id.dialogTitle)
|
||||
val subtitleText = dialogView.findViewById<android.widget.TextView>(R.id.dialogSubtitle)
|
||||
|
||||
subtitleText.text = if (date == LocalDate.now()) "오늘의 근무" else "${date.monthValue}월 ${date.dayOfMonth}일 근무"
|
||||
titleText.text = "$currentShift"
|
||||
|
||||
// Button Handlers
|
||||
val actionMap = mapOf(
|
||||
R.id.btnJu to "주간",
|
||||
R.id.btnJuMat to "주간 맞교대",
|
||||
R.id.btnSeok to "석간",
|
||||
R.id.btnYa to "야간",
|
||||
R.id.btnYaMat to "야간 맞교대",
|
||||
R.id.btnOff to "휴무",
|
||||
R.id.btnWolcha to "월차",
|
||||
R.id.btnYeoncha to "연차",
|
||||
R.id.btnBanwol to "반월",
|
||||
R.id.btnBannyeon to "반년",
|
||||
R.id.btnEdu to "교육",
|
||||
R.id.btnManual to "직접 입력",
|
||||
R.id.btnReset to "원래대로"
|
||||
)
|
||||
|
||||
fun applyStrokeStyle(viewId: Int, shiftType: String) {
|
||||
val view = dialogView.findViewById<android.widget.TextView>(viewId)
|
||||
val colorRes = when(shiftType) {
|
||||
"주간" -> R.color.shift_ju
|
||||
"석간" -> R.color.shift_seok
|
||||
"야간" -> R.color.shift_ya
|
||||
"주간 맞교대" -> R.color.shift_jumat
|
||||
"야간 맞교대" -> R.color.shift_yamat
|
||||
"휴무", "휴가" -> R.color.shift_off
|
||||
"월차", "연차" -> R.color.secondary
|
||||
"반월", "반년" -> R.color.shift_red
|
||||
"교육" -> R.color.primary
|
||||
"원래대로" -> R.color.text_secondary
|
||||
else -> R.color.shift_gray
|
||||
}
|
||||
val color = androidx.core.content.ContextCompat.getColor(this, colorRes)
|
||||
|
||||
view.setTextColor(color)
|
||||
|
||||
// Create Stroke Drawable Programmatically
|
||||
val drawable = android.graphics.drawable.GradientDrawable()
|
||||
drawable.shape = android.graphics.drawable.GradientDrawable.OVAL
|
||||
drawable.setColor(android.graphics.Color.TRANSPARENT)
|
||||
val density = resources.displayMetrics.density
|
||||
drawable.setStroke((1.5 * density).toInt(), color)
|
||||
|
||||
view.background = drawable
|
||||
}
|
||||
|
||||
actionMap.forEach { (id, type) -> applyStrokeStyle(id, type) }
|
||||
|
||||
// Memo Handling
|
||||
val etMemo = dialogView.findViewById<android.widget.EditText>(R.id.etMemo)
|
||||
val repo = ShiftRepository(this)
|
||||
lifecycleScope.launch {
|
||||
val existingMemo = repo.getMemo(date)
|
||||
etMemo.setText(existingMemo ?: "")
|
||||
}
|
||||
|
||||
for ((id, action) in actionMap) {
|
||||
dialogView.findViewById<android.view.View>(id).setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
val content = etMemo.text.toString().trim()
|
||||
repo.setMemo(date, content) // Save memo even when shift button clicked
|
||||
handleDaySettingAction(date, action, team, factory)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialogView.findViewById<android.view.View>(R.id.btnClearMemo).setOnClickListener {
|
||||
etMemo.setText("")
|
||||
lifecycleScope.launch {
|
||||
repo.setMemo(date, "")
|
||||
updateCalendar()
|
||||
Toast.makeText(this@MainActivity, "메모가 삭제되었습니다.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
dialogView.findViewById<android.view.View>(R.id.btnClose).setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
val content = etMemo.text.toString().trim()
|
||||
repo.setMemo(date, content)
|
||||
updateCalendar()
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private suspend fun handleDaySettingAction(date: LocalDate, selected: String, team: String, factory: String) {
|
||||
val repo = ShiftRepository(this)
|
||||
val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
|
||||
when (selected) {
|
||||
"원래대로" -> {
|
||||
repo.clearOverride(date, team, factory)
|
||||
repo.setMemo(date, "") // Clear memo too on reset
|
||||
android.widget.Toast.makeText(this, "원래 근무로 복구되었습니다.", android.widget.Toast.LENGTH_SHORT).show()
|
||||
syncAllAlarms(this)
|
||||
updateCalendar()
|
||||
}
|
||||
"직접 입력" -> {
|
||||
showCustomInputDialog(date, repo, team, factory)
|
||||
}
|
||||
"주간", "석간", "야간", "주간 맞교대", "야간 맞교대" -> {
|
||||
// Standard Shifts -> Override Shift WITHOUT manual time
|
||||
repo.setOverride(date, selected, team, factory)
|
||||
updateCalendar()
|
||||
// Alarms are handled by syncAllAlarms/CustomAlarms during updateCalendar
|
||||
syncAllAlarms(this)
|
||||
android.widget.Toast.makeText(this, "${date} [$selected]로 설정되었습니다.", android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
"휴무" -> {
|
||||
// Standard Off
|
||||
repo.setOverride(date, selected, team, factory)
|
||||
updateCalendar()
|
||||
syncAllAlarms(this)
|
||||
android.widget.Toast.makeText(this, "${selected}로 설정되었습니다. 알람이 해제됩니다.", android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
else -> {
|
||||
// New Types: 월차, 연차, 반월, 반년, 교육 -> Saved as Override with no time
|
||||
repo.setOverride(date, selected, team, factory)
|
||||
updateCalendar()
|
||||
syncAllAlarms(this)
|
||||
android.widget.Toast.makeText(this, "${selected}(으)로 기록되었습니다. 알람이 해제됩니다.", android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCustomInputDialog(date: LocalDate, repo: ShiftRepository, team: String, factory: String) {
|
||||
val layout = android.widget.LinearLayout(this).apply {
|
||||
orientation = android.widget.LinearLayout.VERTICAL
|
||||
setPadding(50, 40, 50, 10)
|
||||
}
|
||||
|
||||
val etMemoInput = android.widget.EditText(this).apply {
|
||||
hint = "메모 내용 (예: 회식, 교육, 출장)"
|
||||
maxLines = 1
|
||||
filters = arrayOf(android.text.InputFilter.LengthFilter(20))
|
||||
}
|
||||
|
||||
layout.addView(etMemoInput)
|
||||
|
||||
androidx.appcompat.app.AlertDialog.Builder(this)
|
||||
.setTitle("${date.monthValue}월 ${date.dayOfMonth}일 메모 입력")
|
||||
.setView(layout)
|
||||
.setPositiveButton("확인") { _, _ ->
|
||||
val content = etMemoInput.text.toString().trim()
|
||||
|
||||
if (content.isNotEmpty()) {
|
||||
lifecycleScope.launch {
|
||||
// Direct Input -> Set as Memo.
|
||||
// We do NOT change the shift (keep default or existing override).
|
||||
// If user wants to change shift, they should use the specific buttons.
|
||||
repo.setMemo(date, content)
|
||||
updateCalendar()
|
||||
android.widget.Toast.makeText(this@MainActivity, "메모가 저장되었습니다.", android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
android.widget.Toast.makeText(this, "입력이 취소되었습니다.", android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
.setNegativeButton("취소", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
|
||||
private fun setupWorker() {
|
||||
val workRequest = PeriodicWorkRequestBuilder<AlarmWorker>(24, TimeUnit.HOURS)
|
||||
.setInitialDelay(calculateDelayToMidnight(), TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
|
||||
"DailyShiftCheck",
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
workRequest
|
||||
)
|
||||
}
|
||||
|
||||
private fun calculateDelayToMidnight(): Long {
|
||||
val seoulZone = java.time.ZoneId.of("Asia/Seoul")
|
||||
val now = java.time.LocalDateTime.now(seoulZone)
|
||||
val midnight = now.plusDays(1).withHour(0).withMinute(0).withSecond(1)
|
||||
return java.time.Duration.between(now, midnight).toMillis()
|
||||
}
|
||||
|
||||
private fun checkRoot() {
|
||||
if (RootUtil.isDeviceRooted()) {
|
||||
Toast.makeText(this, "⚠️ 루팅된 기기에서 시각적 오류나 알람 불안정이 발생할 수 있습니다.", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user