diff --git a/app/build.gradle b/app/build.gradle
index 83185be..cdecfaf 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -40,8 +40,8 @@ android {
applicationId "kr.co.vividnext.sodalive"
minSdk 23
targetSdk 33
- versionCode 83
- versionName "1.13.1"
+ versionCode 84
+ versionName "1.13.2"
}
buildTypes {
@@ -154,4 +154,9 @@ dependencies {
// google in-app-purchase
implementation "com.android.billingclient:billing-ktx:6.2.0"
+
+ // ROOM
+ kapt "androidx.room:room-compiler:2.5.0"
+ implementation "androidx.room:room-ktx:2.5.0"
+ implementation "androidx.room:room-runtime:2.5.0"
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f03caba..9a64656 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -39,6 +39,14 @@
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
+
+
+
+
+
+
+
+
@@ -138,6 +146,20 @@
+
+
+
+
+
+
+
+
+
+
@@ -165,6 +187,21 @@
+
+
+
+
+
+
+
+
+
{
+ val listType = object : TypeToken>() {}.type
+ return Gson().fromJson(value, listType)
+ }
+
+ @TypeConverter
+ fun fromList(list: List): String {
+ return Gson().toJson(list)
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt
index 2e00b87..088fcfc 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt
@@ -27,6 +27,7 @@ import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.live.reservation_status.LiveReservationStatusActivity
+import kr.co.vividnext.sodalive.mypage.alarm.AlarmListActivity
import kr.co.vividnext.sodalive.mypage.auth.Auth
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
@@ -146,6 +147,15 @@ class MyPageFragment : BaseFragment(FragmentMyBinding::inflat
)
}
+ binding.rlAlarm.setOnClickListener {
+ startActivity(
+ Intent(
+ requireActivity(),
+ AlarmListActivity::class.java
+ )
+ )
+ }
+
binding.llReservationLive.setOnClickListener {
startActivity(
Intent(
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AddAlarmActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AddAlarmActivity.kt
new file mode 100644
index 0000000..2940cc7
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AddAlarmActivity.kt
@@ -0,0 +1,177 @@
+package kr.co.vividnext.sodalive.mypage.alarm
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import android.widget.CheckBox
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import kr.co.vividnext.sodalive.base.BaseActivity
+import kr.co.vividnext.sodalive.common.Constants
+import kr.co.vividnext.sodalive.common.LoadingDialog
+import kr.co.vividnext.sodalive.databinding.ActivityAddAlarmBinding
+import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm
+import kr.co.vividnext.sodalive.mypage.alarm.select_audio_content.AlarmSelectAudioContentActivity
+import java.util.Calendar
+
+class AddAlarmActivity : BaseActivity(
+ ActivityAddAlarmBinding::inflate
+) {
+ private val alarmViewModel: AlarmViewModel by viewModels()
+
+ private lateinit var loadingDialog: LoadingDialog
+ private lateinit var activityResultLauncher: ActivityResultLauncher
+ private lateinit var dayCheckBoxes: List
+
+ private var alarmId: Int = 0
+ private var selectedContentId: Long = 0
+ private var selectedContentTitle = ""
+ private var selectedContentCreatorNickname = ""
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ activityResultLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {
+ if (it.resultCode == Activity.RESULT_OK) {
+ this.selectedContentId = it.data?.getLongExtra(
+ Constants.EXTRA_AUDIO_CONTENT_ID,
+ 0
+ ) ?: 0
+
+ this.selectedContentTitle = it.data?.getStringExtra(
+ Constants.EXTRA_AUDIO_CONTENT_TITLE
+ ) ?: ""
+
+ this.selectedContentCreatorNickname = it.data?.getStringExtra(
+ Constants.EXTRA_AUDIO_CONTENT_CREATOR_NICKNAME
+ ) ?: ""
+
+ binding.tvContentTitle.text = selectedContentTitle
+ }
+ }
+
+ alarmId = intent.getIntExtra(Constants.EXTRA_ALARM_ID, -1)
+ super.onCreate(savedInstanceState)
+
+ bindData()
+ }
+
+ override fun setupView() {
+ loadingDialog = LoadingDialog(this, layoutInflater)
+
+ dayCheckBoxes = listOf(
+ binding.chkSun,
+ binding.chkMon,
+ binding.chkTue,
+ binding.chkWed,
+ binding.chkThu,
+ binding.chkFri,
+ binding.chkSat
+ )
+
+ if (alarmId != -1) {
+ alarmViewModel.getAlarmById(alarmId).observe(this) { alarm ->
+ alarm?.let {
+ binding.etAlarmTitle.setText(it.title)
+
+ val calendar = Calendar.getInstance().apply { timeInMillis = it.time }
+ binding.timePicker.hour = calendar.get(Calendar.HOUR_OF_DAY)
+ binding.timePicker.minute = calendar.get(Calendar.MINUTE)
+ dayCheckBoxes.forEach { checkBox ->
+ checkBox.isChecked = it.days.contains(checkBox.text.toString())
+ }
+ binding.tvContentTitle.text = it.contentTitle
+
+ selectedContentId = it.contentId
+ selectedContentTitle = it.contentTitle
+ selectedContentCreatorNickname = it.contentCreatorNickname
+ }
+ }
+ }
+
+ binding.rlSelectAlarmContent.setOnClickListener {
+ activityResultLauncher.launch(
+ Intent(
+ applicationContext,
+ AlarmSelectAudioContentActivity::class.java
+ )
+ )
+ }
+
+ binding.tvSave.setOnClickListener { saveAlarm() }
+
+ binding.toolbar.tvBack.text = "알람 설정"
+ binding.toolbar.tvBack.setOnClickListener { finish() }
+ binding.tvCancel.setOnClickListener { finish() }
+ }
+
+ private fun saveAlarm() {
+ if (!validate()) return
+
+ val hour = binding.timePicker.hour
+ val minute = binding.timePicker.minute
+ val alarmTitle = binding.etAlarmTitle.text.toString()
+ val selectedDays = dayCheckBoxes.filter { it.isChecked }.map { it.text.toString() }
+
+ val alarmTime = getAdjustedTimeInMillis(hour, minute, selectedDays)
+ val alarm = Alarm(
+ id = if (alarmId == -1) 0 else alarmId,
+ title = alarmTitle,
+ time = alarmTime,
+ contentId = selectedContentId,
+ contentTitle = selectedContentTitle,
+ contentCreatorNickname = selectedContentCreatorNickname,
+ days = selectedDays.toList(),
+ )
+
+ if (alarmId > 0) {
+ alarmViewModel.update(alarm)
+ } else {
+ alarmViewModel.insert(alarm)
+ }
+ finish()
+ }
+
+ private fun getAdjustedTimeInMillis(hour: Int, minute: Int, selectedDays: List): Long {
+ val alarmTimeInMillis = getTimeInMillis(hour, minute)
+ return if (selectedDays.isEmpty() && alarmTimeInMillis <= System.currentTimeMillis()) {
+ getTimeInMillis(hour + 24, minute) // 다음 날로 설정
+ } else {
+ alarmTimeInMillis
+ }
+ }
+
+ private fun getTimeInMillis(hour: Int, minute: Int): Long {
+ val calendar = Calendar.getInstance()
+ calendar.set(Calendar.HOUR_OF_DAY, hour)
+ calendar.set(Calendar.MINUTE, minute)
+ calendar.set(Calendar.SECOND, 0)
+ calendar.set(Calendar.MILLISECOND, 0)
+ return calendar.timeInMillis
+ }
+
+ private fun bindData() {
+ alarmViewModel.isLoading.observe(this) {
+ if (it) {
+ loadingDialog.show(screenWidth, "")
+ } else {
+ loadingDialog.dismiss()
+ }
+ }
+ }
+
+ private fun validate(): Boolean {
+ if (
+ selectedContentId <= 0 ||
+ selectedContentTitle.isBlank() ||
+ selectedContentCreatorNickname.isBlank()
+ ) {
+ Toast.makeText(applicationContext, "알람 콘텐츠를 선택하세요", Toast.LENGTH_LONG).show()
+ return false
+ }
+
+ return true
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmActivity.kt
new file mode 100644
index 0000000..7e65973
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmActivity.kt
@@ -0,0 +1,144 @@
+package kr.co.vividnext.sodalive.mypage.alarm
+
+import android.content.Context
+import android.media.AudioManager
+import android.media.MediaPlayer
+import android.os.Build
+import android.os.Bundle
+import android.view.WindowManager
+import androidx.activity.OnBackPressedCallback
+import androidx.lifecycle.Observer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailViewModel
+import kr.co.vividnext.sodalive.base.BaseActivity
+import kr.co.vividnext.sodalive.common.Constants
+import kr.co.vividnext.sodalive.common.LoadingDialog
+import kr.co.vividnext.sodalive.databinding.ActivityAlarmBinding
+import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm
+import kr.co.vividnext.sodalive.mypage.alarm.db.AlarmDatabase
+import org.koin.android.ext.android.inject
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class AlarmActivity : BaseActivity(
+ ActivityAlarmBinding::inflate
+) {
+
+ private val contentViewModel: AudioContentDetailViewModel by inject()
+
+ private lateinit var mediaPlayer: MediaPlayer
+ private lateinit var loadingDialog: LoadingDialog
+
+ private lateinit var audioManager: AudioManager
+ private var originalVolume: Int = 0
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ setTurnScreenOn(true)
+ setShowWhenLocked(true)
+ } else {
+ window.addFlags(
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+ or WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ )
+ }
+
+ super.onCreate(savedInstanceState)
+
+ initAudioManagerAndSaveOriginalVolume()
+ bindData()
+ getAlarm()
+ }
+
+ private fun getAlarm() {
+ val alarmId = intent.getIntExtra(Constants.EXTRA_ALARM_ID, -1)
+ val alarmDao = AlarmDatabase.getDatabase(applicationContext).alarmDao()
+ val alarmLiveData = alarmDao.getAlarmById(alarmId)
+
+ val observer = object : Observer {
+ override fun onChanged(value: Alarm) {
+ getContent(value.contentId)
+ setAlarmData(value)
+ alarmLiveData.removeObserver(this)
+ }
+ }
+
+ CoroutineScope(Dispatchers.Main).launch {
+ withContext(Dispatchers.Main) {
+ alarmLiveData.observeForever(observer)
+ }
+ }
+ }
+
+ private fun initAudioManagerAndSaveOriginalVolume() {
+ audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ originalVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
+ }
+
+ private fun setVolume(volume: Int) {
+ audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0)
+ }
+
+ private fun setAlarmData(alarm: Alarm) {
+ binding.tvTitle.text = alarm.title
+ binding.tvTime.text = SimpleDateFormat("hh:mm", Locale.getDefault())
+ .format(alarm.time)
+ binding.tvDate.text = SimpleDateFormat("MM월 dd일", Locale.getDefault())
+ .format(alarm.time)
+ }
+
+ private fun getContent(contentId: Long) {
+ contentViewModel.getAudioContentDetail(contentId) {}
+ }
+
+ private fun bindData() {
+ contentViewModel.isLoading.observe(this) {
+ if (it) {
+ loadingDialog.show(screenWidth, "")
+ } else {
+ loadingDialog.dismiss()
+ }
+ }
+
+ contentViewModel.audioContentLiveData.observe(this) {
+ initMediaPlayer(it.contentUrl)
+ }
+ }
+
+ override fun setupView() {
+ loadingDialog = LoadingDialog(this, layoutInflater)
+ binding.ivStopAlarm.setOnClickListener { finish() }
+
+ onBackPressedDispatcher.addCallback(
+ this,
+ object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ }
+ }
+ )
+ }
+
+ private fun initMediaPlayer(alarmUrl: String) {
+ setVolume(audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
+ mediaPlayer = MediaPlayer()
+ mediaPlayer.isLooping = true
+ mediaPlayer.setDataSource(alarmUrl)
+ mediaPlayer.prepareAsync()
+ mediaPlayer.setOnPreparedListener { mp -> mp.start() }
+ }
+
+ override fun onDestroy() {
+ setVolume(originalVolume)
+ if (mediaPlayer.isPlaying) {
+ mediaPlayer.stop()
+ }
+ mediaPlayer.release()
+
+ super.onDestroy()
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmAdapter.kt
new file mode 100644
index 0000000..c5e0540
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmAdapter.kt
@@ -0,0 +1,84 @@
+package kr.co.vividnext.sodalive.mypage.alarm
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.databinding.ItemAlarmBinding
+import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class AlarmAdapter(
+ private val updateAlarm: (Alarm) -> Unit,
+ private val deleteAlarm: (Alarm) -> Unit,
+ private val onClick: (Int) -> Unit
+) : ListAdapter(AlarmDiffCallback()) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AlarmViewHolder {
+ return AlarmViewHolder(
+ ItemAlarmBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ )
+ }
+
+ override fun onBindViewHolder(holder: AlarmViewHolder, position: Int) {
+ val alarm = getItem(position)
+ holder.bind(alarm)
+ }
+
+ inner class AlarmViewHolder(
+ private val binding: ItemAlarmBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+ fun bind(alarm: Alarm) {
+ binding.tvTitle.text = alarm.title
+ binding.tvAmpm.text = SimpleDateFormat("a", Locale.getDefault())
+ .format(alarm.time)
+ binding.tvTime.text = SimpleDateFormat("hh:mm", Locale.getDefault())
+ .format(alarm.time)
+ binding.tvDays.text = if (alarm.days.isNotEmpty()) {
+ alarm.getDaysText()
+ } else {
+ SimpleDateFormat("MM월 dd일 (E)", Locale.getDefault())
+ .format(alarm.time)
+ }
+
+ binding.tvContentTitle.text = alarm.contentTitle
+ binding.tvCreatorNickname.text = alarm.contentCreatorNickname
+
+ binding.ivEnable.setImageResource(
+ if (alarm.isEnabled) {
+ R.drawable.btn_toggle_on_big
+ } else {
+ R.drawable.btn_toggle_off_big
+ }
+ )
+
+ binding.ivEnable.setOnClickListener {
+ alarm.isEnabled = !alarm.isEnabled
+ updateAlarm(alarm)
+ }
+
+ binding.root.setOnClickListener { onClick(alarm.id) }
+ binding.root.setOnLongClickListener {
+ deleteAlarm(alarm)
+ true
+ }
+ }
+ }
+
+ class AlarmDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: Alarm, newItem: Alarm): Boolean {
+ return oldItem.id == newItem.id
+ }
+
+ override fun areContentsTheSame(oldItem: Alarm, newItem: Alarm): Boolean {
+ return oldItem == newItem
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmListActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmListActivity.kt
new file mode 100644
index 0000000..2b2fe9d
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmListActivity.kt
@@ -0,0 +1,143 @@
+package kr.co.vividnext.sodalive.mypage.alarm
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.provider.Settings
+import android.view.View
+import android.widget.Toast
+import androidx.activity.viewModels
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.gun0912.tedpermission.PermissionListener
+import com.gun0912.tedpermission.normal.TedPermission
+import kr.co.vividnext.sodalive.base.BaseActivity
+import kr.co.vividnext.sodalive.base.SodaDialog
+import kr.co.vividnext.sodalive.common.Constants
+import kr.co.vividnext.sodalive.common.LoadingDialog
+import kr.co.vividnext.sodalive.databinding.ActivityAlarmListBinding
+import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm
+
+class AlarmListActivity : BaseActivity(
+ ActivityAlarmListBinding::inflate
+) {
+
+ private val alarmViewModel: AlarmViewModel by viewModels()
+
+ private lateinit var alarmAdapter: AlarmAdapter
+ private lateinit var loadingDialog: LoadingDialog
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ checkPermissions()
+ bindData()
+ }
+
+ override fun setupView() {
+ loadingDialog = LoadingDialog(this, layoutInflater)
+
+ binding.tvBack.text = "알람"
+ binding.tvBack.setOnClickListener { finish() }
+ binding.ivPlus.setOnClickListener {
+ startActivity(
+ Intent(
+ applicationContext,
+ AddAlarmActivity::class.java
+ )
+ )
+ }
+
+ alarmAdapter = AlarmAdapter(
+ updateAlarm = {
+ alarmViewModel.update(it)
+ adapterRefresh()
+ },
+ deleteAlarm = { showDeleteConfirm(it) },
+ onClick = {
+ startActivity(
+ Intent(applicationContext, AddAlarmActivity::class.java).apply {
+ putExtra(Constants.EXTRA_ALARM_ID, it)
+ }
+ )
+ }
+ )
+ binding.rvAlarm.layoutManager = LinearLayoutManager(this)
+ binding.rvAlarm.adapter = alarmAdapter
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ private fun adapterRefresh() {
+ alarmViewModel.refresh()
+ alarmAdapter.notifyDataSetChanged()
+ }
+
+ private fun bindData() {
+ alarmViewModel.allAlarms.observe(this) { alarms ->
+ alarms?.let {
+ alarmAdapter.submitList(it)
+ binding.rvAlarm.visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
+ binding.tvEmptyAlarms.visibility = if (it.isEmpty()) View.VISIBLE else View.GONE
+ }
+ }
+ }
+
+ private fun checkPermissions() {
+ val permissions = mutableListOf()
+
+ if (!Settings.canDrawOverlays(this)) {
+ Toast.makeText(
+ applicationContext,
+ "알람서비스를 이용하시려면 다른 앱 위에 표시 권한을 허용하셔야 합니다.",
+ Toast.LENGTH_LONG
+ ).show()
+ permissions.add(Manifest.permission.SYSTEM_ALERT_WINDOW)
+ }
+
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S) {
+ permissions.add(Manifest.permission.SCHEDULE_EXACT_ALARM)
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ permissions.add(Manifest.permission.USE_FULL_SCREEN_INTENT)
+ }
+
+ if (permissions.isNotEmpty()) {
+ TedPermission.create()
+ .setPermissionListener(object : PermissionListener {
+ override fun onPermissionGranted() {
+ }
+
+ override fun onPermissionDenied(deniedPermissions: MutableList?) {
+ finish()
+ }
+ })
+ .setDeniedMessage(
+ "권한을 거부하시면 알람서비스를 이용하실 수 없습니다."
+ )
+ .setPermissions(*permissions.toTypedArray())
+ .check()
+ }
+ }
+
+ private fun showDeleteConfirm(alarm: Alarm) {
+ SodaDialog(
+ this,
+ layoutInflater,
+ title = "알림",
+ desc = "알람을 삭제하시겠습니까?",
+ confirmButtonTitle = "삭제",
+ confirmButtonClick = {
+ alarmViewModel.delete(alarm)
+ adapterRefresh()
+ Toast.makeText(
+ applicationContext,
+ "알람이 삭제되었습니다.",
+ Toast.LENGTH_SHORT
+ ).show()
+ },
+ cancelButtonTitle = "취소",
+ ).show(screenWidth)
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmRepository.kt
new file mode 100644
index 0000000..62ee4a7
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmRepository.kt
@@ -0,0 +1,41 @@
+package kr.co.vividnext.sodalive.mypage.alarm
+
+import android.content.Context
+import androidx.lifecycle.LiveData
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm
+import kr.co.vividnext.sodalive.mypage.alarm.db.AlarmDao
+import kr.co.vividnext.sodalive.mypage.alarm.db.AlarmDatabase
+
+class AlarmRepository(private val alarmDao: AlarmDao) {
+ val allAlarms = alarmDao.getAllAlarms()
+
+ suspend fun insert(alarm: Alarm) {
+ withContext(Dispatchers.IO) {
+ alarmDao.insertAlarm(alarm)
+ }
+ }
+
+ suspend fun update(alarm: Alarm) {
+ withContext(Dispatchers.IO) {
+ alarmDao.updateAlarm(alarm)
+ }
+ }
+
+ suspend fun delete(alarm: Alarm) {
+ withContext(Dispatchers.IO) {
+ alarmDao.deleteAlarm(alarm)
+ }
+ }
+
+ suspend fun truncate() {
+ withContext(Dispatchers.IO) {
+ alarmDao.truncateTable()
+ }
+ }
+
+ fun getAlarmById(id: Int): LiveData {
+ return alarmDao.getAlarmById(id)
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmViewModel.kt
new file mode 100644
index 0000000..370642a
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmViewModel.kt
@@ -0,0 +1,82 @@
+package kr.co.vividnext.sodalive.mypage.alarm
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.asLiveData
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.launch
+import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm
+import kr.co.vividnext.sodalive.mypage.alarm.db.AlarmDatabase
+import kr.co.vividnext.sodalive.mypage.alarm.scheduler.AlarmScheduler
+
+class AlarmViewModel(application: Application) : AndroidViewModel(application) {
+ private val repository: AlarmRepository
+ private val scheduler: AlarmScheduler
+
+ private var _isLoading = MutableLiveData(false)
+ val isLoading: LiveData
+ get() = _isLoading
+
+ init {
+ val alarmDao = AlarmDatabase.getDatabase(application).alarmDao()
+ repository = AlarmRepository(alarmDao)
+ scheduler = AlarmScheduler(application)
+ }
+
+ var allAlarms = repository.allAlarms.asLiveData()
+
+ fun refresh() = viewModelScope.launch {
+ _isLoading.value = true
+ repository.allAlarms.asLiveData()
+ _isLoading.value = false
+ }
+
+ fun insert(alarm: Alarm) = viewModelScope.launch {
+ _isLoading.value = true
+
+ repository.insert(alarm)
+ if (alarm.isEnabled) {
+ scheduler.setAlarm(alarm)
+ }
+
+ _isLoading.value = false
+ }
+
+ fun update(alarm: Alarm) = viewModelScope.launch {
+ _isLoading.value = true
+
+ repository.update(alarm)
+ scheduler.cancelAlarm(alarm)
+
+ if (alarm.isEnabled) {
+ scheduler.setAlarm(alarm)
+ }
+
+ _isLoading.value = false
+ }
+
+ fun delete(alarm: Alarm) = viewModelScope.launch {
+ _isLoading.value = true
+
+ repository.delete(alarm)
+ scheduler.cancelAlarm(alarm)
+
+ _isLoading.value = false
+ }
+
+ fun truncate() = viewModelScope.launch {
+ repository.truncate()
+ }
+
+ fun getAlarmById(id: Int): LiveData {
+ _isLoading.value = true
+
+ val alarm = repository.getAlarmById(id)
+
+ _isLoading.value = false
+
+ return alarm
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/Alarm.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/Alarm.kt
new file mode 100644
index 0000000..335e24b
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/Alarm.kt
@@ -0,0 +1,20 @@
+package kr.co.vividnext.sodalive.mypage.alarm.db
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "alarms")
+data class Alarm(
+ @PrimaryKey(autoGenerate = true) val id: Int = 0,
+ val title: String,
+ val time: Long,
+ val days: List,
+ val contentId: Long,
+ val contentTitle: String,
+ val contentCreatorNickname: String,
+ var isEnabled: Boolean = true
+) {
+ fun getDaysText(): String {
+ return if (days.size == 7) "매일" else days.joinToString(", ")
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/AlarmDao.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/AlarmDao.kt
new file mode 100644
index 0000000..ebf57d0
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/AlarmDao.kt
@@ -0,0 +1,41 @@
+package kr.co.vividnext.sodalive.mypage.alarm.db
+
+import androidx.lifecycle.LiveData
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Transaction
+import androidx.room.Update
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface AlarmDao {
+ @Query("SELECT * FROM alarms WHERE id = :id")
+ fun getAlarmById(id: Int): LiveData
+
+ @Query("SELECT * FROM alarms")
+ fun getAllAlarms(): Flow>
+
+ @Query("DELETE FROM alarms")
+ fun deleteAllAlarms()
+
+ @Query("DELETE FROM sqlite_sequence WHERE name = 'alarms'")
+ fun resetAutoIncrement()
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertAlarm(alarm: Alarm)
+
+ @Update
+ fun updateAlarm(alarm: Alarm)
+
+ @Delete
+ fun deleteAlarm(alarm: Alarm)
+
+ @Transaction
+ fun truncateTable() {
+ deleteAllAlarms()
+ resetAutoIncrement()
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/AlarmDatabase.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/AlarmDatabase.kt
new file mode 100644
index 0000000..c885c5b
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/db/AlarmDatabase.kt
@@ -0,0 +1,31 @@
+package kr.co.vividnext.sodalive.mypage.alarm.db
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import kr.co.vividnext.sodalive.common.Converter
+
+@Database(entities = [Alarm::class], version = 1)
+@TypeConverters(Converter::class)
+abstract class AlarmDatabase : RoomDatabase() {
+ abstract fun alarmDao(): AlarmDao
+
+ companion object {
+ @Volatile
+ private var INSTANCE: AlarmDatabase? = null
+
+ fun getDatabase(context: Context): AlarmDatabase {
+ return INSTANCE ?: synchronized(this) {
+ val instance = Room.databaseBuilder(
+ context.applicationContext,
+ AlarmDatabase::class.java,
+ "alarm_database"
+ ).build()
+ INSTANCE = instance
+ instance
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/receiver/AlarmBootReceiver.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/receiver/AlarmBootReceiver.kt
new file mode 100644
index 0000000..f13a612
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/receiver/AlarmBootReceiver.kt
@@ -0,0 +1,27 @@
+package kr.co.vividnext.sodalive.mypage.alarm.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kr.co.vividnext.sodalive.mypage.alarm.db.AlarmDatabase
+import kr.co.vividnext.sodalive.mypage.alarm.scheduler.AlarmScheduler
+
+class AlarmBootReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
+ val alarmDao = AlarmDatabase.getDatabase(context).alarmDao()
+ val scheduler = AlarmScheduler(context)
+
+ CoroutineScope(Dispatchers.IO).launch {
+ alarmDao.getAllAlarms().collect { alarms ->
+ alarms.forEach { alarm ->
+ scheduler.setAlarm(alarm)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/receiver/AlarmReceiver.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/receiver/AlarmReceiver.kt
new file mode 100644
index 0000000..ed4c3e6
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/receiver/AlarmReceiver.kt
@@ -0,0 +1,53 @@
+package kr.co.vividnext.sodalive.mypage.alarm.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import androidx.lifecycle.Observer
+import com.orhanobut.logger.Logger
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kr.co.vividnext.sodalive.common.Constants
+import kr.co.vividnext.sodalive.mypage.alarm.AlarmActivity
+import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm
+import kr.co.vividnext.sodalive.mypage.alarm.db.AlarmDatabase
+import kr.co.vividnext.sodalive.mypage.alarm.scheduler.AlarmScheduler
+
+class AlarmReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+
+ val alarmId = intent.getIntExtra(Constants.EXTRA_ALARM_ID, -1)
+
+ if (alarmId > 0) {
+ val alarmIntent = Intent(context, AlarmActivity::class.java).apply {
+ putExtra(Constants.EXTRA_ALARM_ID, alarmId)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or
+ Intent.FLAG_ACTIVITY_CLEAR_TOP or
+ Intent.FLAG_ACTIVITY_SINGLE_TOP
+ }
+
+ val alarmDao = AlarmDatabase.getDatabase(context).alarmDao()
+ val alarmLiveData = alarmDao.getAlarmById(alarmId)
+
+ val observer = object : Observer {
+ override fun onChanged(value: Alarm) {
+ val scheduler = AlarmScheduler(context)
+ if (value.days.isNotEmpty()) {
+ scheduler.setAlarm(value)
+ }
+ alarmLiveData.removeObserver(this)
+ }
+ }
+
+ CoroutineScope(Dispatchers.Main).launch {
+ withContext(Dispatchers.Main) {
+ alarmLiveData.observeForever(observer)
+ }
+ }
+
+ context.startActivity(alarmIntent)
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/scheduler/AlarmScheduler.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/scheduler/AlarmScheduler.kt
new file mode 100644
index 0000000..2c42a13
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/scheduler/AlarmScheduler.kt
@@ -0,0 +1,123 @@
+package kr.co.vividnext.sodalive.mypage.alarm.scheduler
+
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import kr.co.vividnext.sodalive.common.Constants
+import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm
+import kr.co.vividnext.sodalive.mypage.alarm.receiver.AlarmReceiver
+import java.util.Calendar
+
+class AlarmScheduler(private val context: Context) {
+ fun setAlarm(alarm: Alarm) {
+ if (alarm.days.isEmpty()) {
+ setOneTimeAlarm(alarm)
+ } else {
+ setRepeatingAlarm(alarm)
+ }
+ }
+
+ private fun setOneTimeAlarm(alarm: Alarm) {
+ val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+
+ val alarmIntent = Intent(context, AlarmReceiver::class.java).let { intent ->
+ intent.putExtra(Constants.EXTRA_ALARM_ID, alarm.id)
+ PendingIntent.getBroadcast(context, alarm.id, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ val calendar = Calendar.getInstance()
+ calendar.timeInMillis = alarm.time
+
+ // 현재 시간보다 이전이면 내일 해당 시간으로 설정
+ if (calendar.timeInMillis <= System.currentTimeMillis()) {
+ calendar.add(Calendar.DAY_OF_YEAR, 1)
+ }
+
+ alarmManager.setExactAndAllowWhileIdle(
+ AlarmManager.RTC_WAKEUP,
+ calendar.timeInMillis,
+ alarmIntent
+ )
+ }
+
+ private fun setRepeatingAlarm(alarm: Alarm) {
+ val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+
+ alarm.days.forEach { day ->
+ val requestCode = alarm.id * 10 + getDayOfWeek(day)
+ val alarmIntent = Intent(context, AlarmReceiver::class.java).let { intent ->
+ intent.putExtra(Constants.EXTRA_ALARM_ID, alarm.id)
+ PendingIntent.getBroadcast(
+ context,
+ requestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ }
+
+ val calendar = getNextAlarmTime(day, alarm.time)
+
+ // 현재 시간이 설정된 알람 시간보다 늦으면 다음 주에 울리도록 설정
+ if (calendar.timeInMillis <= System.currentTimeMillis()) {
+ calendar.add(Calendar.WEEK_OF_YEAR, 1)
+ }
+
+ alarmManager.setExactAndAllowWhileIdle(
+ AlarmManager.RTC_WAKEUP,
+ calendar.timeInMillis,
+ alarmIntent
+ )
+
+ }
+ }
+
+ fun cancelAlarm(alarm: Alarm) {
+ val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+
+ if (alarm.days.isEmpty()) {
+ val alarmIntent = Intent(context, AlarmReceiver::class.java).let { intent ->
+ PendingIntent.getBroadcast(
+ context,
+ alarm.id,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ }
+ alarmManager.cancel(alarmIntent)
+ } else {
+ alarm.days.forEach { day ->
+ val requestCode = alarm.id * 10 + getDayOfWeek(day)
+ val alarmIntent = Intent(context, AlarmReceiver::class.java).let { intent ->
+ PendingIntent.getBroadcast(
+ context,
+ requestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ }
+ alarmManager.cancel(alarmIntent)
+ }
+ }
+ }
+
+ private fun getNextAlarmTime(day: String, timeInMillis: Long): Calendar {
+ val calendar = Calendar.getInstance()
+ calendar.timeInMillis = timeInMillis
+ calendar.set(Calendar.DAY_OF_WEEK, getDayOfWeek(day))
+ return calendar
+ }
+
+ private fun getDayOfWeek(day: String): Int {
+ return when (day) {
+ "일" -> Calendar.SUNDAY
+ "월" -> Calendar.MONDAY
+ "화" -> Calendar.TUESDAY
+ "수" -> Calendar.WEDNESDAY
+ "목" -> Calendar.THURSDAY
+ "금" -> Calendar.FRIDAY
+ "토" -> Calendar.SATURDAY
+ else -> Calendar.MONDAY
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentActivity.kt
new file mode 100644
index 0000000..c44e194
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentActivity.kt
@@ -0,0 +1,131 @@
+package kr.co.vividnext.sodalive.mypage.alarm.select_audio_content
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.graphics.Rect
+import android.os.Bundle
+import android.view.View
+import android.widget.Toast
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListViewModel
+import kr.co.vividnext.sodalive.audio_content.order.OrderType
+import kr.co.vividnext.sodalive.base.BaseActivity
+import kr.co.vividnext.sodalive.common.Constants
+import kr.co.vividnext.sodalive.common.LoadingDialog
+import kr.co.vividnext.sodalive.databinding.ActivityAlarmSelectAudioContentBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+import org.koin.android.ext.android.inject
+
+class AlarmSelectAudioContentActivity : BaseActivity(
+ ActivityAlarmSelectAudioContentBinding::inflate
+) {
+ private val viewModel: AudioContentOrderListViewModel by inject()
+
+ private lateinit var loadingDialog: LoadingDialog
+ private lateinit var adapter: AlarmSelectAudioContentAdapter
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ bindData()
+ viewModel.getAudioContentOrderList { finish() }
+ }
+
+ override fun setupView() {
+ loadingDialog = LoadingDialog(this, layoutInflater)
+ binding.toolbar.tvBack.text = "콘텐츠 선택"
+ binding.toolbar.tvBack.setOnClickListener { finish() }
+
+ adapter = AlarmSelectAudioContentAdapter {
+ val resultIntent = Intent().apply {
+ putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it.contentId)
+ putExtra(Constants.EXTRA_AUDIO_CONTENT_TITLE, it.title)
+ putExtra(Constants.EXTRA_AUDIO_CONTENT_CREATOR_NICKNAME, it.creatorNickname)
+ }
+ setResult(RESULT_OK, resultIntent)
+ finish()
+ }
+
+ binding.rvOrderList.layoutManager = LinearLayoutManager(
+ applicationContext,
+ LinearLayoutManager.VERTICAL,
+ false
+ )
+
+ binding.rvOrderList.addItemDecoration(object : RecyclerView.ItemDecoration() {
+ override fun getItemOffsets(
+ outRect: Rect,
+ view: View,
+ parent: RecyclerView,
+ state: RecyclerView.State
+ ) {
+ super.getItemOffsets(outRect, view, parent, state)
+
+ outRect.left = 13.3f.dpToPx().toInt()
+ outRect.right = 13.3f.dpToPx().toInt()
+
+ when (parent.getChildAdapterPosition(view)) {
+ 0 -> {
+ outRect.top = 13.3f.dpToPx().toInt()
+ outRect.bottom = 6.7f.dpToPx().toInt()
+ }
+
+ adapter.itemCount - 1 -> {
+ outRect.top = 6.7f.dpToPx().toInt()
+ outRect.bottom = 13.3f.dpToPx().toInt()
+ }
+
+ else -> {
+ outRect.top = 6.7f.dpToPx().toInt()
+ outRect.bottom = 6.7f.dpToPx().toInt()
+ }
+ }
+ }
+ })
+
+ binding.rvOrderList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ super.onScrolled(recyclerView, dx, dy)
+
+ val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
+ .findLastCompletelyVisibleItemPosition()
+ val itemTotalCount = recyclerView.adapter!!.itemCount - 1
+
+ // 스크롤이 끝에 도달했는지 확인
+ if (!recyclerView.canScrollVertically(1) &&
+ lastVisibleItemPosition == itemTotalCount
+ ) {
+ viewModel.getAudioContentOrderList {}
+ }
+ }
+ })
+
+ binding.rvOrderList.adapter = adapter
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ private fun bindData() {
+ viewModel.toastLiveData.observe(this) {
+ it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
+ }
+
+ viewModel.isLoading.observe(this) {
+ if (it) {
+ loadingDialog.show(screenWidth, "")
+ } else {
+ loadingDialog.dismiss()
+ }
+ }
+
+ viewModel.orderList.observe(this) { items ->
+ if (viewModel.page == 2) {
+ adapter.items.clear()
+ }
+
+ adapter.items.addAll(
+ items.filter { it.orderType == OrderType.KEEP }
+ )
+ adapter.notifyDataSetChanged()
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentAdapter.kt
new file mode 100644
index 0000000..98d9195
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentAdapter.kt
@@ -0,0 +1,56 @@
+package kr.co.vividnext.sodalive.mypage.alarm.select_audio_content
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import coil.load
+import coil.transform.RoundedCornersTransformation
+import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListItem
+import kr.co.vividnext.sodalive.audio_content.order.OrderType
+import kr.co.vividnext.sodalive.databinding.ItemAudioContentOrderListBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+import kr.co.vividnext.sodalive.extensions.moneyFormat
+
+class AlarmSelectAudioContentAdapter(
+ private val onItemClick: (GetAudioContentOrderListItem) -> Unit
+) : RecyclerView.Adapter() {
+
+ var items = mutableSetOf()
+
+ inner class ViewHolder(
+ private val binding: ItemAudioContentOrderListBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+ fun bind(item: GetAudioContentOrderListItem) {
+ binding.ivCover.load(item.coverImageUrl) {
+ crossfade(true)
+ placeholder(R.drawable.ic_place_holder)
+ transformations(RoundedCornersTransformation(5.3f.dpToPx()))
+ }
+
+ binding.tvTitle.text = item.title
+ binding.tvTheme.text = item.themeStr
+ binding.tvDuration.text = item.duration
+ binding.tvCreatorNickname.text = item.creatorNickname
+ binding.tvLikeCount.text = item.likeCount.moneyFormat()
+ binding.tvCommentCount.text = item.commentCount.moneyFormat()
+
+ binding.root.setOnClickListener { onItemClick(item) }
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
+ ItemAudioContentOrderListBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ )
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(items.toList()[position])
+ }
+
+ override fun getItemCount() = items.size
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/settings/SettingsActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/settings/SettingsActivity.kt
index d27ae11..28ccdc6 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/settings/SettingsActivity.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/settings/SettingsActivity.kt
@@ -14,6 +14,7 @@ import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivitySettingsBinding
+import kr.co.vividnext.sodalive.mypage.alarm.AlarmViewModel
import kr.co.vividnext.sodalive.settings.event.EventActivity
import kr.co.vividnext.sodalive.settings.notice.NoticeActivity
import kr.co.vividnext.sodalive.settings.notification.NotificationSettingsActivity
@@ -50,6 +51,7 @@ class SettingsActivity : BaseActivity(ActivitySettingsB
}
private val viewModel: SettingsViewModel by inject()
+ private val alarmViewModel: AlarmViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
@@ -148,6 +150,7 @@ class SettingsActivity : BaseActivity(ActivitySettingsB
viewModel.logout {
SharedPreferenceManager.clear()
+ alarmViewModel.truncate()
finishAffinity()
startActivity(Intent(applicationContext, SplashActivity::class.java))
}
diff --git a/app/src/main/res/color/alarm_day_checkbox_text_selector.xml b/app/src/main/res/color/alarm_day_checkbox_text_selector.xml
new file mode 100644
index 0000000..f9837d2
--- /dev/null
+++ b/app/src/main/res/color/alarm_day_checkbox_text_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable-xxhdpi/ic_alarm_clock.png b/app/src/main/res/drawable-xxhdpi/ic_alarm_clock.png
new file mode 100644
index 0000000..8a3defa
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_alarm_clock.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_alarm_stop.png b/app/src/main/res/drawable-xxhdpi/ic_alarm_stop.png
new file mode 100644
index 0000000..7e5676d
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_alarm_stop.png differ
diff --git a/app/src/main/res/drawable/alarm_day_checkbox_selector.xml b/app/src/main/res/drawable/alarm_day_checkbox_selector.xml
new file mode 100644
index 0000000..005cac9
--- /dev/null
+++ b/app/src/main/res/drawable/alarm_day_checkbox_selector.xml
@@ -0,0 +1,13 @@
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_transparent_3bb9f1.xml b/app/src/main/res/drawable/bg_round_corner_6_7_transparent_3bb9f1.xml
new file mode 100644
index 0000000..6d09470
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_6_7_transparent_3bb9f1.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_7_transparent_555555.xml b/app/src/main/res/drawable/bg_round_corner_7_transparent_555555.xml
new file mode 100644
index 0000000..9ef4c50
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_7_transparent_555555.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_top_round_corner_13_3_222222.xml b/app/src/main/res/drawable/bg_top_round_corner_13_3_222222.xml
new file mode 100644
index 0000000..b6a0047
--- /dev/null
+++ b/app/src/main/res/drawable/bg_top_round_corner_13_3_222222.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/gradient_alarm.xml b/app/src/main/res/drawable/gradient_alarm.xml
new file mode 100644
index 0000000..f86a4aa
--- /dev/null
+++ b/app/src/main/res/drawable/gradient_alarm.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/app/src/main/res/layout/activity_add_alarm.xml b/app/src/main/res/layout/activity_add_alarm.xml
new file mode 100644
index 0000000..d6a4a91
--- /dev/null
+++ b/app/src/main/res/layout/activity_add_alarm.xml
@@ -0,0 +1,192 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_alarm.xml b/app/src/main/res/layout/activity_alarm.xml
new file mode 100644
index 0000000..802cbcf
--- /dev/null
+++ b/app/src/main/res/layout/activity_alarm.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_alarm_list.xml b/app/src/main/res/layout/activity_alarm_list.xml
new file mode 100644
index 0000000..b9da9e6
--- /dev/null
+++ b/app/src/main/res/layout/activity_alarm_list.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_alarm_select_audio_content.xml b/app/src/main/res/layout/activity_alarm_select_audio_content.xml
new file mode 100644
index 0000000..aa53654
--- /dev/null
+++ b/app/src/main/res/layout/activity_alarm_select_audio_content.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_my.xml b/app/src/main/res/layout/fragment_my.xml
index 2865098..d06a13b 100644
--- a/app/src/main/res/layout/fragment_my.xml
+++ b/app/src/main/res/layout/fragment_my.xml
@@ -257,6 +257,43 @@
android:src="@drawable/ic_forward" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 9733802..5c1b58a 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -34,6 +34,8 @@
#3D2A6C
#FFDC00
#4999E3
+ #35C2FF
+ #97AEFF
#CCC25264
#B3909090
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 1360d88..1e088cd 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -59,4 +59,16 @@
+
+