692 lines
29 KiB
Kotlin
692 lines
29 KiB
Kotlin
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()
|
|
}
|
|
}
|
|
|
|
|
|
}
|