diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8c90386..f0aa820 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -36,6 +36,9 @@
+
+
+
Unit,
+ cancelButtonTitle: String = "",
+ cancelButtonClick: (() -> Unit)? = null,
+) {
+
+ private val alertDialog: AlertDialog
+
+ val dialogView = DialogLiveBinding.inflate(layoutInflater)
+
+ init {
+ val dialogBuilder = AlertDialog.Builder(activity)
+ dialogBuilder.setView(dialogView.root)
+
+ alertDialog = dialogBuilder.create()
+ alertDialog.setCancelable(false)
+ alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+
+ dialogView.tvTitle.text = title
+ dialogView.tvDesc.text = desc
+
+ dialogView.tvCancel.text = cancelButtonTitle
+ dialogView.tvCancel.setOnClickListener {
+ alertDialog.dismiss()
+ cancelButtonClick?.let { it() }
+ }
+
+ dialogView.tvConfirm.text = confirmButtonTitle
+ dialogView.tvConfirm.setOnClickListener {
+ alertDialog.dismiss()
+ confirmButtonClick()
+ }
+
+ dialogView.tvCancel.visibility = if (cancelButtonTitle.isNotBlank()) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+
+ dialogView.tvConfirm.visibility = if (confirmButtonTitle.isNotBlank()) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+ }
+
+ fun show(width: Int) {
+ alertDialog.show()
+
+ val lp = WindowManager.LayoutParams()
+ lp.copyFrom(alertDialog.window?.attributes)
+ lp.width = width - (26.7f.dpToPx()).toInt()
+ lp.height = WindowManager.LayoutParams.WRAP_CONTENT
+
+ alertDialog.window?.attributes = lp
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt
index 80de8ae..272b69f 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt
@@ -3,14 +3,34 @@ package kr.co.vividnext.sodalive.live
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
+import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationRequest
+import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationResponse
+import kr.co.vividnext.sodalive.live.room.CancelLiveRequest
+import kr.co.vividnext.sodalive.live.room.EnterOrQuitLiveRoomRequest
import kr.co.vividnext.sodalive.live.room.LiveRoomStatus
+import kr.co.vividnext.sodalive.live.room.StartLiveRequest
+import kr.co.vividnext.sodalive.live.room.create.CreateLiveRoomResponse
+import kr.co.vividnext.sodalive.live.room.create.GetRecentRoomInfoResponse
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
+import kr.co.vividnext.sodalive.live.room.tag.GetLiveTagResponse
+import okhttp3.MultipartBody
+import okhttp3.RequestBody
+import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
+import retrofit2.http.Multipart
+import retrofit2.http.POST
+import retrofit2.http.PUT
+import retrofit2.http.Part
import retrofit2.http.Path
import retrofit2.http.Query
interface LiveApi {
+ @GET("/live/tag")
+ fun getTags(
+ @Header("Authorization") authHeader: String
+ ): Single>>
+
@GET("/live/room")
fun roomList(
@Query("timezone") timezone: String,
@@ -27,4 +47,50 @@ interface LiveApi {
@Query("timezone") timezone: String,
@Header("Authorization") authHeader: String
): Single>
+
+ @GET("/live/room/recent-room-info")
+ fun getRecentRoomInfo(
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @POST("/live/room")
+ @Multipart
+ fun createRoom(
+ @Part coverImage: MultipartBody.Part?,
+ @Part("request") request: RequestBody,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @PUT("/live/room/start")
+ fun startLive(
+ @Body request: StartLiveRequest,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @PUT("/live/room/cancel")
+ fun cancelLive(
+ @Body request: CancelLiveRequest,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @POST("/live/room/enter")
+ fun enterRoom(
+ @Body request: EnterOrQuitLiveRoomRequest,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @POST("/live/reservation")
+ fun makeReservation(
+ @Body request: MakeLiveReservationRequest,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @PUT("/live/room/{id}")
+ @Multipart
+ fun editLiveRoomInfo(
+ @Path("id") id: Long,
+ @Part coverImage: MultipartBody.Part?,
+ @Part("request") request: RequestBody?,
+ @Header("Authorization") authHeader: String
+ ): Single>
}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt
index 771aca9..31c8eaa 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt
@@ -1,14 +1,19 @@
package kr.co.vividnext.sodalive.live
import android.annotation.SuppressLint
+import android.app.Activity
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
import android.view.View
import android.webkit.URLUtil
import android.widget.LinearLayout
import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -17,16 +22,25 @@ import com.zhpan.indicator.enums.IndicatorSlideMode
import com.zhpan.indicator.enums.IndicatorStyle
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
+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.FragmentLiveBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
+import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.live.event_banner.EventBannerAdapter
import kr.co.vividnext.sodalive.live.now.LiveNowAdapter
import kr.co.vividnext.sodalive.live.recommend.RecommendLiveAdapter
import kr.co.vividnext.sodalive.live.recommend_channel.LiveRecommendChannelAdapter
import kr.co.vividnext.sodalive.live.reservation.LiveReservationAdapter
+import kr.co.vividnext.sodalive.live.reservation.complete.LiveReservationCompleteActivity
+import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateActivity
+import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailFragment
+import kr.co.vividnext.sodalive.live.room.dialog.LiveCancelDialog
+import kr.co.vividnext.sodalive.live.room.dialog.LivePaymentDialog
+import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog
+import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditActivity
import kr.co.vividnext.sodalive.settings.notification.MemberRole
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
@@ -39,8 +53,23 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl
private lateinit var liveRecommendChannelAdapter: LiveRecommendChannelAdapter
private lateinit var loadingDialog: LoadingDialog
+ private lateinit var activityResultLauncher: ActivityResultLauncher
private var message = ""
+ private val handler = Handler(Looper.getMainLooper())
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ activityResultLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {
+ if (it.resultCode == Activity.RESULT_OK) {
+ refreshSummary()
+ }
+ }
+ }
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -76,7 +105,11 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl
} else {
View.GONE
}
- binding.ivMakeRoom.setOnClickListener {}
+
+ binding.ivMakeRoom.setOnClickListener {
+ val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java)
+ activityResultLauncher.launch(intent)
+ }
binding.swipeRefreshLayout.setOnRefreshListener { refreshSummary() }
@@ -320,10 +353,10 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl
val detailFragment = LiveRoomDetailFragment(
it.roomId,
onClickParticipant = {},
- onClickReservation = {},
- onClickModify = {},
- onClickStart = {},
- onClickCancel = {}
+ onClickReservation = { reservationRoom(it.roomId) },
+ onClickModify = { roomDetailResponse -> modifyLive(roomDetailResponse) },
+ onClickStart = { startLive(it.roomId) },
+ onClickCancel = { cancelLive(it.roomId) }
)
if (detailFragment.isAdded) return@LiveReservationAdapter
@@ -431,4 +464,93 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl
}
}
}
+
+ private fun startLive(roomId: Long) {
+ val onEnterRoomSuccess = {
+ viewModel.getSummary()
+ }
+
+ viewModel.startLive(roomId, onEnterRoomSuccess)
+ }
+
+ private fun cancelLive(roomId: Long) {
+ LiveCancelDialog(
+ activity = requireActivity(),
+ layoutInflater = layoutInflater,
+ title = "예약취소",
+ hint = "취소사유를 입력하세요.",
+ confirmButtonTitle = "예약취소",
+ confirmButtonClick = {
+ viewModel.cancelLive(roomId, it) {
+ Toast.makeText(
+ requireActivity(),
+ "예약이 취소되었습니다.",
+ Toast.LENGTH_LONG
+ ).show()
+ message = "라이브를 불러오고 있습니다."
+ liveNowAdapter.clear()
+ liveReservationAdapter.clear()
+ viewModel.getSummary()
+ }
+ },
+ cancelButtonTitle = "닫기",
+ cancelButtonClick = {}
+ ).show(screenWidth)
+ }
+
+ fun reservationRoom(roomId: Long) {
+ viewModel.getRoomDetail(roomId) {
+ if (it.manager.id == SharedPreferenceManager.userId) {
+ showToast("내가 만든 라이브는 예약할 수 없습니다.")
+ } else {
+ if (it.isPrivateRoom) {
+ LiveRoomPasswordDialog(
+ activity = requireActivity(),
+ layoutInflater = layoutInflater,
+ can = if (it.isPaid) 0 else it.price,
+ confirmButtonClick = { password ->
+ handler.postDelayed({
+ processLiveReservation(roomId, password)
+ }, 300)
+ }
+ ).show(screenWidth)
+ } else {
+ if (it.price == 0 || it.isPaid) {
+ processLiveReservation(roomId)
+ } else {
+ LivePaymentDialog(
+ activity = requireActivity(),
+ layoutInflater = layoutInflater,
+ title = "${it.price.moneyFormat()}코인으로 예약",
+ desc = "'${it.title}' 라이브에 참여하기 위해 결제합니다.",
+ confirmButtonTitle = "예약하기",
+ confirmButtonClick = { processLiveReservation(roomId) },
+ cancelButtonTitle = "취소",
+ cancelButtonClick = {}
+ ).show(screenWidth)
+ }
+ }
+ }
+ }
+ }
+
+ private fun processLiveReservation(roomId: Long, password: String = "") {
+ viewModel.reservationRoom(roomId, password) {
+ refreshSummary()
+ val intent = Intent(
+ requireActivity(),
+ LiveReservationCompleteActivity::class.java
+ )
+ intent.putExtra(Constants.EXTRA_LIVE_RESERVATION_RESPONSE, it)
+ startActivity(intent)
+ }
+ }
+
+ private fun modifyLive(roomDetail: GetRoomDetailResponse) {
+ startActivity(
+ Intent(requireContext(), LiveRoomEditActivity::class.java).apply {
+ putExtra(Constants.EXTRA_ROOM_DETAIL, roomDetail)
+ }
+ )
+ }
}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt
index 2e79523..7167f78 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt
@@ -3,8 +3,15 @@ package kr.co.vividnext.sodalive.live
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
+import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationRequest
+import kr.co.vividnext.sodalive.live.room.CancelLiveRequest
+import kr.co.vividnext.sodalive.live.room.EnterOrQuitLiveRoomRequest
import kr.co.vividnext.sodalive.live.room.LiveRoomStatus
+import kr.co.vividnext.sodalive.live.room.StartLiveRequest
+import kr.co.vividnext.sodalive.live.room.create.CreateLiveRoomResponse
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
+import okhttp3.MultipartBody
+import okhttp3.RequestBody
import java.util.TimeZone
class LiveRepository(private val api: LiveApi) {
@@ -32,4 +39,59 @@ class LiveRepository(private val api: LiveApi) {
authHeader = token
)
}
+
+ fun getRecentRoomInfo(token: String) = api.getRecentRoomInfo(authHeader = token)
+
+ fun createRoom(
+ coverImage: MultipartBody.Part? = null,
+ request: RequestBody,
+ token: String
+ ): Single> {
+ return api.createRoom(
+ coverImage,
+ request,
+ authHeader = token
+ )
+ }
+
+ fun startLive(
+ request: StartLiveRequest,
+ token: String
+ ) = api.startLive(
+ request,
+ authHeader = token
+ )
+
+ fun cancelLive(
+ request: CancelLiveRequest,
+ token: String
+ ) = api.cancelLive(request, authHeader = token)
+
+ fun enterRoom(
+ request: EnterOrQuitLiveRoomRequest,
+ token: String
+ ) = api.enterRoom(
+ request,
+ authHeader = token
+ )
+
+ fun makeReservation(
+ request: MakeLiveReservationRequest,
+ token: String
+ ) = api.makeReservation(
+ request,
+ authHeader = token
+ )
+
+ fun editLiveRoomInfo(
+ roomId: Long,
+ coverImage: MultipartBody.Part? = null,
+ request: RequestBody? = null,
+ token: String
+ ) = api.editLiveRoomInfo(
+ id = roomId,
+ coverImage = coverImage,
+ request = request,
+ authHeader = token
+ )
}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveViewModel.kt
index bba57cb..51c07d7 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveViewModel.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveViewModel.kt
@@ -11,7 +11,13 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.live.recommend.GetRecommendLiveResponse
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendRepository
import kr.co.vividnext.sodalive.live.recommend_channel.GetRecommendChannelResponse
+import kr.co.vividnext.sodalive.live.room.CancelLiveRequest
+import kr.co.vividnext.sodalive.live.room.EnterOrQuitLiveRoomRequest
import kr.co.vividnext.sodalive.live.room.LiveRoomStatus
+import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationRequest
+import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationResponse
+import kr.co.vividnext.sodalive.live.room.StartLiveRequest
+import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
import kr.co.vividnext.sodalive.settings.event.EventItem
import kr.co.vividnext.sodalive.settings.event.EventRepository
@@ -238,4 +244,177 @@ class LiveViewModel(
)
}
}
+
+ fun startLive(roomId: Long, onEnterRoomSuccess: () -> Unit) {
+ _isLoading.value = true
+ compositeDisposable.add(
+ repository.startLive(
+ StartLiveRequest(roomId),
+ "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ _isLoading.value = false
+ if (it.success) {
+ enterRoom(roomId, onEnterRoomSuccess)
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
+ fun cancelLive(roomId: Long, reason: String, onSuccess: () -> Unit) {
+ _isLoading.postValue(true)
+
+ compositeDisposable.add(
+ repository.cancelLive(
+ CancelLiveRequest(roomId, reason),
+ "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success) {
+ _isLoading.value = false
+ onSuccess()
+ } else {
+ _isLoading.value = false
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
+ fun enterRoom(roomId: Long, onSuccess: () -> Unit, password: Int? = null) {
+ _isLoading.value = true
+ val request = EnterOrQuitLiveRoomRequest(roomId, password = password)
+ compositeDisposable.add(
+ repository.enterRoom(request, "Bearer ${SharedPreferenceManager.token}")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success) {
+ _isLoading.value = false
+ onSuccess()
+ } else {
+ _isLoading.value = false
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
+ fun getRoomDetail(roomId: Long, onSuccess: (GetRoomDetailResponse) -> Unit) {
+ _isLoading.value = true
+ compositeDisposable.add(
+ repository.getRoomDetail(
+ roomId = roomId,
+ token = "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ _isLoading.value = false
+ if (it.success && it.data != null) {
+ onSuccess(it.data)
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "라이브 정보를 가져오지 못했습니다.\n다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue(
+ "라이브 정보를 가져오지 못했습니다.\n다시 시도해 주세요."
+ )
+ }
+ )
+ )
+ }
+
+ fun reservationRoom(
+ roomId: Long,
+ password: String? = null,
+ onSuccess: (MakeLiveReservationResponse) -> Unit
+ ) {
+ _isLoading.value = true
+ compositeDisposable.add(
+ repository.makeReservation(
+ MakeLiveReservationRequest(roomId = roomId, password = password),
+ "Bearer ${SharedPreferenceManager.token}"
+ )
+ .retry(3)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ _isLoading.value = false
+ if (it.success && it.data != null) {
+ onSuccess(it.data)
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationRequest.kt
new file mode 100644
index 0000000..2fca0a8
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationRequest.kt
@@ -0,0 +1,11 @@
+package kr.co.vividnext.sodalive.live.reservation
+
+import com.google.gson.annotations.SerializedName
+import java.util.TimeZone
+
+data class MakeLiveReservationRequest(
+ @SerializedName("roomId") val roomId: Long,
+ @SerializedName("container") val container: String = "aos",
+ @SerializedName("timezone") val timezone: String = TimeZone.getDefault().id,
+ @SerializedName("password") val password: String? = null
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationResponse.kt
new file mode 100644
index 0000000..9673145
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationResponse.kt
@@ -0,0 +1,17 @@
+package kr.co.vividnext.sodalive.live.reservation
+
+import android.os.Parcelable
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class MakeLiveReservationResponse(
+ @SerializedName("reservationId") val reservationId: Long,
+ @SerializedName("nickname") val nickname: String,
+ @SerializedName("title") val title: String,
+ @SerializedName("beginDateString") val beginDateString: String,
+ @SerializedName("price") val price: String,
+ @SerializedName("haveCan") val haveCan: Int,
+ @SerializedName("useCan") val useCan: Int,
+ @SerializedName("remainingCoin") val remainingCoin: Int
+) : Parcelable
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/reservation/complete/LiveReservationCompleteActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/reservation/complete/LiveReservationCompleteActivity.kt
new file mode 100644
index 0000000..8d21d71
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/reservation/complete/LiveReservationCompleteActivity.kt
@@ -0,0 +1,53 @@
+package kr.co.vividnext.sodalive.live.reservation.complete
+
+import android.content.Intent
+import android.widget.Toast
+import androidx.core.content.IntentCompat
+import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.base.BaseActivity
+import kr.co.vividnext.sodalive.common.Constants
+import kr.co.vividnext.sodalive.databinding.ActivityLiveReservationCompleteBinding
+import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationResponse
+import kr.co.vividnext.sodalive.main.MainActivity
+
+class LiveReservationCompleteActivity : BaseActivity(
+ ActivityLiveReservationCompleteBinding::inflate
+) {
+ override fun setupView() {
+ val response = IntentCompat.getParcelableExtra(
+ intent,
+ Constants.EXTRA_LIVE_RESERVATION_RESPONSE,
+ MakeLiveReservationResponse::class.java
+ )
+
+ if (response == null) {
+ Toast.makeText(applicationContext, R.string.retry, Toast.LENGTH_LONG).show()
+ finish()
+ return
+ }
+
+ binding.toolbar.tvBack.text = "라이브 예약 완료"
+ binding.toolbar.tvBack.setOnClickListener { finish() }
+
+ binding.tvNickname.text = response.nickname
+ binding.tvTitle.text = response.title
+ binding.tvDate.text = response.beginDateString
+ binding.tvPrice.text = response.price
+
+ binding.tvHaveCoin.text = "${response.haveCan}"
+ binding.tvUseCoin.text = "${response.useCan}"
+ binding.tvRemainingCoin.text = "${response.remainingCoin}"
+
+ binding.tvGoHome.setOnClickListener {
+ val intent = Intent(applicationContext, MainActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
+ startActivity(intent)
+ finish()
+ }
+
+ binding.tvGoReservationList.setOnClickListener {
+ finish()
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/CancelLiveRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/CancelLiveRequest.kt
new file mode 100644
index 0000000..de4dde2
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/CancelLiveRequest.kt
@@ -0,0 +1,8 @@
+package kr.co.vividnext.sodalive.live.room
+
+import com.google.gson.annotations.SerializedName
+
+data class CancelLiveRequest(
+ @SerializedName("roomId") val roomId: Long,
+ @SerializedName("reason") val reason: String
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt
new file mode 100644
index 0000000..c78db54
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt
@@ -0,0 +1,9 @@
+package kr.co.vividnext.sodalive.live.room
+
+import com.google.gson.annotations.SerializedName
+
+data class EnterOrQuitLiveRoomRequest(
+ @SerializedName("roomId") val roomId: Long,
+ @SerializedName("container") val container: String = "aos",
+ @SerializedName("password") val password: Int? = null
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomType.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomType.kt
new file mode 100644
index 0000000..9460caa
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomType.kt
@@ -0,0 +1,8 @@
+package kr.co.vividnext.sodalive.live.room
+
+import com.google.gson.annotations.SerializedName
+
+enum class LiveRoomType {
+ @SerializedName("OPEN") OPEN,
+ @SerializedName("PRIVATE") PRIVATE,
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/StartLiveRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/StartLiveRequest.kt
new file mode 100644
index 0000000..08af7fc
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/StartLiveRequest.kt
@@ -0,0 +1,9 @@
+package kr.co.vividnext.sodalive.live.room
+
+import com.google.gson.annotations.SerializedName
+import java.util.TimeZone
+
+data class StartLiveRequest(
+ @SerializedName("roomId") val roomId: Long,
+ @SerializedName("timezone") val timezone: String = TimeZone.getDefault().id,
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/CreateLiveRoomRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/CreateLiveRoomRequest.kt
new file mode 100644
index 0000000..1a13005
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/CreateLiveRoomRequest.kt
@@ -0,0 +1,18 @@
+package kr.co.vividnext.sodalive.live.room.create
+
+import com.google.gson.annotations.SerializedName
+import kr.co.vividnext.sodalive.live.room.LiveRoomType
+
+data class CreateLiveRoomRequest(
+ @SerializedName("title") val title: String,
+ @SerializedName("price") val price: Int = 0,
+ @SerializedName("content") val content: String,
+ @SerializedName("coverImageUrl") val coverImageUrl: String? = null,
+ @SerializedName("isAdult") val isAdult: Boolean,
+ @SerializedName("tags") val tags: List,
+ @SerializedName("numberOfPeople") val numberOfPeople: Int,
+ @SerializedName("beginDateTimeString") val beginDateTimeString: String? = null,
+ @SerializedName("timezone") val timezone: String,
+ @SerializedName("type") val type: LiveRoomType,
+ @SerializedName("password") val password: String? = null
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/CreateLiveRoomResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/CreateLiveRoomResponse.kt
new file mode 100644
index 0000000..d0028a9
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/CreateLiveRoomResponse.kt
@@ -0,0 +1,8 @@
+package kr.co.vividnext.sodalive.live.room.create
+
+import com.google.gson.annotations.SerializedName
+
+data class CreateLiveRoomResponse(
+ @SerializedName("id") val id: Long?,
+ @SerializedName("channelName") val channelName: String?
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/GetRecentRoomInfoResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/GetRecentRoomInfoResponse.kt
new file mode 100644
index 0000000..80e4565
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/GetRecentRoomInfoResponse.kt
@@ -0,0 +1,11 @@
+package kr.co.vividnext.sodalive.live.room.create
+
+import com.google.gson.annotations.SerializedName
+
+data class GetRecentRoomInfoResponse(
+ @SerializedName("title") val title: String,
+ @SerializedName("notice") val notice: String,
+ @SerializedName("coverImageUrl") val coverImageUrl: String,
+ @SerializedName("coverImagePath") val coverImagePath: String,
+ @SerializedName("numberOfPeople") val numberOfPeople: Int
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateActivity.kt
new file mode 100644
index 0000000..cb80816
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateActivity.kt
@@ -0,0 +1,616 @@
+package kr.co.vividnext.sodalive.live.room.create
+
+import android.annotation.SuppressLint
+import android.app.DatePickerDialog
+import android.app.TimePickerDialog
+import android.content.Intent
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.MotionEvent
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import coil.load
+import coil.transform.RoundedCornersTransformation
+import com.github.dhaval2404.imagepicker.ImagePicker
+import com.jakewharton.rxbinding4.widget.textChanges
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kr.co.vividnext.sodalive.R
+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.common.RealPathUtil
+import kr.co.vividnext.sodalive.common.SharedPreferenceManager
+import kr.co.vividnext.sodalive.databinding.ActivityLiveRoomCreateBinding
+import kr.co.vividnext.sodalive.databinding.ItemLiveTagSelectedBinding
+import kr.co.vividnext.sodalive.extensions.convertDateFormat
+import kr.co.vividnext.sodalive.extensions.dpToPx
+import kr.co.vividnext.sodalive.live.room.LiveRoomType
+import kr.co.vividnext.sodalive.live.room.tag.LiveTagFragment
+import kr.co.vividnext.sodalive.settings.notification.MemberRole
+import org.koin.android.ext.android.inject
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+class LiveRoomCreateActivity : BaseActivity(
+ ActivityLiveRoomCreateBinding::inflate
+) {
+
+ private val viewModel: LiveRoomCreateViewModel by inject()
+
+ private lateinit var loadingDialog: LoadingDialog
+
+ private val handler = Handler(Looper.getMainLooper())
+
+ private val datePickerDialogListener =
+ DatePickerDialog.OnDateSetListener { _, year, monthOfYear, dayOfMonth ->
+ viewModel.beginDate = String.format("%d-%02d-%02d", year, monthOfYear + 1, dayOfMonth)
+ viewModel.setReservationDate(
+ String.format(
+ "%d.%02d.%02d",
+ year,
+ monthOfYear + 1,
+ dayOfMonth
+ )
+ )
+ }
+
+ private val timePickerDialogListener =
+ TimePickerDialog.OnTimeSetListener { _, hourOfDay, minute ->
+ val timeString = String.format("%02d:%02d", hourOfDay, minute)
+ viewModel.beginTime = timeString
+ viewModel.setReservationTime(timeString.convertDateFormat("HH:mm", "a hh:mm"))
+ }
+
+ private val tagFragment: LiveTagFragment by lazy {
+ LiveTagFragment(viewModel.tags) { tag, isChecked ->
+ when {
+ isChecked && viewModel.tags.size < 3 -> {
+ viewModel.addTag(tag)
+ return@LiveTagFragment true
+ }
+
+ !isChecked -> {
+ viewModel.removeTag(tag)
+ return@LiveTagFragment true
+ }
+
+ else -> {
+ Toast.makeText(
+ this,
+ "최대 3개까지 선택 가능합니다.",
+ Toast.LENGTH_SHORT
+ ).show()
+ return@LiveTagFragment false
+ }
+ }
+ }
+ }
+
+ private val imageResult =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ val resultCode = result.resultCode
+ val data = result.data
+
+ if (resultCode == RESULT_OK) {
+ // Image Uri will not be null for RESULT_OK
+ val fileUri = data?.data!!
+ binding.ivCover.background = null
+ binding.ivCover.load(fileUri) {
+ crossfade(true)
+ placeholder(R.drawable.bg_placeholder)
+ transformations(RoundedCornersTransformation(13.3f.dpToPx()))
+ }
+ viewModel.coverImageUri = fileUri
+ viewModel.coverImagePath = null
+ } else if (resultCode == ImagePicker.RESULT_ERROR) {
+ Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ viewModel.getRealPathFromURI = {
+ RealPathUtil.getRealPath(applicationContext, it)
+ }
+
+ bindData()
+
+ viewModel.setTimeNow(
+ intent.getBooleanExtra(Constants.EXTRA_LIVE_TIME_NOW, true)
+ )
+ }
+
+ @SuppressLint("SetTextI18n", "ClickableViewAccessibility")
+ override fun setupView() {
+ loadingDialog = LoadingDialog(this, layoutInflater)
+
+
+ binding.tvBack.setOnClickListener { finish() }
+ binding.ivPhotoPicker.setOnClickListener {
+ ImagePicker.with(this)
+ .crop()
+ .galleryOnly()
+ .galleryMimeTypes( // Exclude gif images
+ mimeTypes = arrayOf(
+ "image/png",
+ "image/jpg",
+ "image/jpeg"
+ )
+ )
+ .createIntent { imageResult.launch(it) }
+ }
+
+ binding.llOpen.setOnClickListener {
+ viewModel.setRoomType(LiveRoomType.OPEN)
+ binding.llConfigTime.visibility = View.VISIBLE
+ binding.tvConfigTime.visibility = View.VISIBLE
+ }
+ binding.llPrivate.setOnClickListener {
+ viewModel.setRoomType(LiveRoomType.PRIVATE)
+ binding.llConfigTime.visibility = View.VISIBLE
+ binding.tvConfigTime.visibility = View.VISIBLE
+ }
+
+ binding.llTimeNow.setOnClickListener { viewModel.setTimeNow(true) }
+ binding.llTimeReservation.setOnClickListener { viewModel.setTimeNow(false) }
+
+ binding.tvReservationDate.setOnClickListener {
+ val reservationDate = viewModel.beginDate.split("-")
+ val datePicker: DatePickerDialog
+
+ if (reservationDate.isNotEmpty() && reservationDate.size == 3) {
+ datePicker = DatePickerDialog(
+ this,
+ R.style.DatePickerStyle,
+ datePickerDialogListener,
+ reservationDate[0].toInt(),
+ reservationDate[1].toInt() - 1,
+ reservationDate[2].toInt()
+ )
+ } else {
+ val dateString = SimpleDateFormat(
+ "yyyy.MM.dd",
+ Locale.getDefault()
+ ).format(Date()).split(".")
+
+ datePicker = DatePickerDialog(
+ this,
+ R.style.DatePickerStyle,
+ datePickerDialogListener,
+ dateString[0].toInt(),
+ dateString[1].toInt() - 1,
+ dateString[2].toInt()
+ )
+ }
+
+ datePicker.show()
+ }
+
+ binding.tvReservationTime.setOnClickListener {
+ val reservationTime = viewModel.beginTime.split(":")
+ val timePicker: TimePickerDialog
+
+ if (reservationTime.isNotEmpty() && reservationTime.size == 2) {
+ timePicker = TimePickerDialog(
+ this,
+ R.style.TimePickerStyle,
+ timePickerDialogListener,
+ reservationTime[0].toInt(),
+ reservationTime[1].toInt(),
+ false
+ )
+ } else {
+ val timeString = SimpleDateFormat(
+ "HH:mm",
+ Locale.getDefault()
+ ).format(Date()).split(":")
+
+ timePicker = TimePickerDialog(
+ this,
+ R.style.TimePickerStyle,
+ timePickerDialogListener,
+ timeString[0].toInt(),
+ timeString[1].toInt(),
+ false
+ )
+ }
+
+ timePicker.show()
+ }
+
+ binding.tvSelectTag.setOnClickListener {
+ if (tagFragment.isAdded) return@setOnClickListener
+
+ tagFragment.show(supportFragmentManager, tagFragment.tag)
+ }
+
+ binding.tvMakeRoom.setOnClickListener {
+ binding.tvMakeRoom.isEnabled = false
+ viewModel.createLiveRoom {
+ val intent = Intent()
+ if (it.id != null) {
+ intent.putExtra(Constants.EXTRA_ROOM_ID, it.id)
+ }
+
+ if (it.channelName != null) {
+ intent.putExtra(Constants.EXTRA_ROOM_CHANNEL_NAME, it.channelName)
+ }
+ setResult(RESULT_OK, intent)
+ finish()
+ }
+
+ handler.postDelayed(
+ { binding.tvMakeRoom.isEnabled = true },
+ 3000
+ )
+ }
+
+ if (SharedPreferenceManager.isAuth) {
+ binding.llSetAdult.visibility = View.VISIBLE
+ } else {
+ binding.llSetAdult.visibility = View.GONE
+ }
+
+ if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
+ binding.llPrice.visibility = View.VISIBLE
+ binding.tvPriceFree.setOnClickListener { binding.etPrice.setText("0") }
+ binding.tvPrice100.setOnClickListener { binding.etPrice.setText("100") }
+ binding.tvPrice300.setOnClickListener { binding.etPrice.setText("300") }
+ binding.tvPrice500.setOnClickListener { binding.etPrice.setText("500") }
+ binding.tvPrice1000.setOnClickListener { binding.etPrice.setText("1000") }
+ binding.tvPrice2000.setOnClickListener { binding.etPrice.setText("2000") }
+ } else {
+ binding.llPrice.visibility = View.GONE
+ }
+
+ binding.tvGetRecentInfo.setOnClickListener {
+ viewModel.getRecentInfo {
+ binding.tvGetRecentInfo.visibility = View.GONE
+
+ binding.etTitle.setText(it.title)
+ binding.etNotice.setText(it.notice)
+ binding.etNumberOfPeople.setText(it.numberOfPeople.toString())
+ binding.ivCover.background = null
+ binding.ivCover.load(it.coverImageUrl) {
+ crossfade(true)
+ placeholder(R.drawable.bg_placeholder)
+ transformations(RoundedCornersTransformation(13.3f.dpToPx()))
+ }
+ }
+ }
+
+ binding.etNotice.setOnTouchListener { view, motionEvent ->
+ view.parent.parent.requestDisallowInterceptTouchEvent(true)
+ if ((motionEvent.action and MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
+ view.parent.parent.requestDisallowInterceptTouchEvent(false)
+ }
+ false
+ }
+ }
+
+ @SuppressLint("SetTextI18n")
+ private fun bindData() {
+ compositeDisposable.add(
+ binding.etTitle.textChanges().skip(1)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ viewModel.title = it.toString()
+ }
+ )
+
+ compositeDisposable.add(
+ binding.etNotice.textChanges().skip(1)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ binding.tvNumberOfCharacters.text = "${it.length}자"
+ viewModel.content = it.toString()
+ }
+ )
+
+ compositeDisposable.add(
+ binding.etNumberOfPeople.textChanges().skip(1)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ if (it.isEmpty()) {
+ viewModel.numberOfPeople = 0
+ } else {
+ viewModel.numberOfPeople = it.toString().toInt()
+ }
+ }
+ )
+
+ compositeDisposable.add(
+ binding.etRoomPassword.textChanges().skip(1)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ if (it.isEmpty()) {
+ viewModel.password = null
+ } else {
+ try {
+ viewModel.password = it.toString()
+ } catch (e: NumberFormatException) {
+ binding.etRoomPassword.setText(it.substring(0, it.length - 1))
+ }
+ }
+ }
+ )
+
+ 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.timeNowLiveData.observe(this) {
+ if (it) {
+ binding.llReservationDatetime.visibility = View.GONE
+ binding.ivTimeReservation.visibility = View.GONE
+ binding.ivTimeNow.visibility = View.VISIBLE
+ binding.llTimeNow.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
+ binding.llTimeReservation.setBackgroundResource(
+ R.drawable.bg_round_corner_6_7_1f1734
+ )
+ binding.tvTimeNow.setTextColor(
+ ContextCompat.getColor(
+ applicationContext,
+ R.color.color_eeeeee
+ )
+ )
+
+ binding.tvTimeReservation.setTextColor(
+ ContextCompat.getColor(
+ applicationContext,
+ R.color.color_9970ff
+ )
+ )
+ } else {
+ binding.llReservationDatetime.visibility = View.VISIBLE
+ binding.ivTimeReservation.visibility = View.VISIBLE
+ binding.ivTimeNow.visibility = View.GONE
+ binding.llTimeNow.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
+ binding.llTimeReservation.setBackgroundResource(
+ R.drawable.bg_round_corner_6_7_9970ff
+ )
+
+ binding.tvTimeNow.setTextColor(
+ ContextCompat.getColor(
+ applicationContext,
+ R.color.color_9970ff
+ )
+ )
+
+ binding.tvTimeReservation.setTextColor(
+ ContextCompat.getColor(
+ applicationContext,
+ R.color.color_eeeeee
+ )
+ )
+ }
+ }
+
+ viewModel.roomTypeLiveData.observe(this) {
+ when (it) {
+ LiveRoomType.PRIVATE -> {
+ binding.ivPrivate.visibility = View.VISIBLE
+ binding.ivOpen.visibility = View.GONE
+
+ binding.llPrivate.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
+ binding.llOpen.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
+
+ binding.tvPrivate.setTextColor(
+ ContextCompat.getColor(
+ applicationContext,
+ R.color.color_eeeeee
+ )
+ )
+
+ binding.tvOpen.setTextColor(
+ ContextCompat.getColor(
+ applicationContext,
+ R.color.color_9970ff
+ )
+ )
+
+ binding.llRoomPassword.visibility = View.VISIBLE
+ }
+
+ else -> {
+ binding.ivOpen.visibility = View.VISIBLE
+ binding.ivPrivate.visibility = View.GONE
+
+ binding.llOpen.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
+ binding.llPrivate.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
+
+ binding.tvPrivate.setTextColor(
+ ContextCompat.getColor(
+ applicationContext,
+ R.color.color_9970ff
+ )
+ )
+
+ binding.tvOpen.setTextColor(
+ ContextCompat.getColor(
+ applicationContext,
+ R.color.color_eeeeee
+ )
+ )
+
+ binding.llRoomPassword.visibility = View.GONE
+ }
+ }
+ }
+
+ viewModel.reservationDateLiveData.observe(this) {
+ binding.tvReservationDate.text = it
+ }
+
+ viewModel.reservationTimeLiveData.observe(this) {
+ binding.tvReservationTime.text = it
+ }
+
+ viewModel.selectedLiveData.observe(this) {
+ binding.llSelectTags.removeAllViews()
+ for (index in it.indices) {
+ val tag = it[index]
+ val itemView = ItemLiveTagSelectedBinding.inflate(layoutInflater)
+ itemView.tvTag.text = tag
+ itemView.ivRemove.setOnClickListener {
+ viewModel.removeTag(tag)
+ }
+ binding.llSelectTags.addView(itemView.root)
+
+ if (index > 0) {
+ val layoutParams = itemView.root.layoutParams as LinearLayout.LayoutParams
+ layoutParams.marginStart = 10.dpToPx().toInt()
+ itemView.root.layoutParams = layoutParams
+ }
+ }
+ }
+
+ if (SharedPreferenceManager.role == MemberRole.CREATOR.name ||
+ SharedPreferenceManager.isAuth
+ ) {
+
+ binding.llAgeAll.setOnClickListener {
+ viewModel.setAdult(false)
+ }
+
+ binding.llAge19.setOnClickListener {
+ viewModel.setAdult(true)
+ }
+
+ viewModel.isAdultLiveData.observe(this) {
+ if (it) {
+ binding.ivAgeAll.visibility = View.GONE
+ binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
+ binding.tvAgeAll.setTextColor(
+ ContextCompat.getColor(
+ applicationContext,
+ R.color.color_9970ff
+ )
+ )
+
+ binding.ivAge19.visibility = View.VISIBLE
+ binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
+ binding.tvAge19.setTextColor(
+ ContextCompat.getColor(
+ applicationContext,
+ R.color.color_eeeeee
+ )
+ )
+ } else {
+ binding.ivAge19.visibility = View.GONE
+ binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
+ binding.tvAge19.setTextColor(
+ ContextCompat.getColor(
+ applicationContext,
+ R.color.color_9970ff
+ )
+ )
+
+ binding.ivAgeAll.visibility = View.VISIBLE
+ binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
+ binding.tvAgeAll.setTextColor(
+ ContextCompat.getColor(
+ applicationContext,
+ R.color.color_eeeeee
+ )
+ )
+ }
+ }
+ }
+
+ if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
+ compositeDisposable.add(
+ binding.etPrice.textChanges()
+ .skip(1)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ if (it.isNotEmpty()) {
+ val price = it.toString().toUIntOrNull()
+ if (price != null) {
+ viewModel.setPrice(price.toInt())
+ } else {
+ binding.etPrice.setText(it.substring(0, it.length - 1))
+ binding.etPrice.setSelection(it.length - 1)
+ }
+ } else {
+ viewModel.setPrice(0)
+ }
+ }
+ )
+
+ viewModel.priceLiveData.observe(this) {
+ allPriceSelectFalse()
+
+ when (it) {
+ 0 -> priceSelect(binding.tvPriceFree)
+ 100 -> priceSelect(binding.tvPrice100)
+ 300 -> priceSelect(binding.tvPrice300)
+ 500 -> priceSelect(binding.tvPrice500)
+ 1000 -> priceSelect(binding.tvPrice1000)
+ 2000 -> priceSelect(binding.tvPrice2000)
+ else -> binding.rlPrice.isSelected = true
+ }
+ }
+ }
+ }
+
+ private fun allPriceSelectFalse() {
+ binding.rlPrice.isSelected = false
+ priceSelectFalse(binding.tvPriceFree)
+ priceSelectFalse(binding.tvPrice100)
+ priceSelectFalse(binding.tvPrice300)
+ priceSelectFalse(binding.tvPrice500)
+ priceSelectFalse(binding.tvPrice1000)
+ priceSelectFalse(binding.tvPrice2000)
+ }
+
+ private fun priceSelectFalse(priceView: TextView) {
+ priceView.isSelected = false
+ priceView.setTextColor(
+ ContextCompat.getColor(
+ applicationContext,
+ R.color.color_eeeeee
+ )
+ )
+ priceView.typeface = ResourcesCompat.getFont(
+ applicationContext,
+ R.font.gmarket_sans_medium
+ )
+ }
+
+ private fun priceSelect(priceView: TextView) {
+ priceView.isSelected = true
+ priceView.setTextColor(
+ ContextCompat.getColor(
+ applicationContext,
+ R.color.color_9970ff
+ )
+ )
+ priceView.typeface = ResourcesCompat.getFont(
+ applicationContext,
+ R.font.gmarket_sans_bold
+ )
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateViewModel.kt
new file mode 100644
index 0000000..2c29da7
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateViewModel.kt
@@ -0,0 +1,255 @@
+package kr.co.vividnext.sodalive.live.room.create
+
+import android.net.Uri
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.google.gson.Gson
+import com.orhanobut.logger.Logger
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kr.co.vividnext.sodalive.base.BaseViewModel
+import kr.co.vividnext.sodalive.common.SharedPreferenceManager
+import kr.co.vividnext.sodalive.live.LiveRepository
+import kr.co.vividnext.sodalive.live.room.LiveRoomType
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.MultipartBody
+import okhttp3.RequestBody.Companion.asRequestBody
+import okhttp3.RequestBody.Companion.toRequestBody
+import java.io.File
+import java.util.TimeZone
+
+class LiveRoomCreateViewModel(
+ private val repository: LiveRepository
+) : BaseViewModel() {
+
+ private val _roomTypeLiveData = MutableLiveData(LiveRoomType.OPEN)
+ val roomTypeLiveData: LiveData
+ get() = _roomTypeLiveData
+
+ private val _timeNowLiveData = MutableLiveData(true)
+ val timeNowLiveData: LiveData
+ get() = _timeNowLiveData
+
+ private val _reservationDateLiveData = MutableLiveData("날짜를 선택해주세요")
+ val reservationDateLiveData: LiveData
+ get() = _reservationDateLiveData
+
+ private val _reservationTimeLiveData = MutableLiveData("시간을 설정해주세요")
+ val reservationTimeLiveData: LiveData
+ get() = _reservationTimeLiveData
+
+ private val _selectedLiveData = MutableLiveData>()
+ val selectedLiveData: LiveData>
+ get() = _selectedLiveData
+
+ private val _toastLiveData = MutableLiveData()
+ val toastLiveData: LiveData
+ get() = _toastLiveData
+
+ private var _isLoading = MutableLiveData(false)
+ val isLoading: LiveData
+ get() = _isLoading
+
+ private val _priceLiveData = MutableLiveData(0)
+ val priceLiveData: LiveData
+ get() = _priceLiveData
+
+ private val _isAdultLiveData = MutableLiveData(false)
+ val isAdultLiveData: LiveData
+ get() = _isAdultLiveData
+
+ lateinit var getRealPathFromURI: (Uri) -> String?
+
+ var title = ""
+ var content = ""
+ var numberOfPeople = 0
+ var tags = mutableSetOf()
+ var beginDate = ""
+ var beginTime = ""
+ var coverImageUri: Uri? = null
+ var coverImagePath: String? = null
+ var password: String? = null
+
+ fun setRoomType(roomType: LiveRoomType) {
+ if (_roomTypeLiveData.value!! != roomType) {
+ _roomTypeLiveData.postValue(roomType)
+ }
+ }
+
+ fun setTimeNow(timeNow: Boolean) {
+ _timeNowLiveData.postValue(timeNow)
+ }
+
+ fun setReservationDate(dateString: String) {
+ _reservationDateLiveData.postValue(dateString)
+ }
+
+ fun setReservationTime(timeString: String) {
+ _reservationTimeLiveData.postValue(timeString)
+ }
+
+ fun createLiveRoom(onSuccess: (CreateLiveRoomResponse) -> Unit) {
+ if (!_isLoading.value!! && validateData()) {
+ _isLoading.postValue(true)
+ val request = CreateLiveRoomRequest(
+ title = title,
+ price = _priceLiveData.value!!,
+ content = content,
+ coverImageUrl = coverImagePath,
+ isAdult = _isAdultLiveData.value!!,
+ tags = tags.toList(),
+ numberOfPeople = numberOfPeople,
+ beginDateTimeString = if (
+ !_timeNowLiveData.value!!
+ ) {
+ "$beginDate $beginTime"
+ } else {
+ null
+ },
+ timezone = TimeZone.getDefault().id,
+ type = _roomTypeLiveData.value!!,
+ password = if (
+ _roomTypeLiveData.value!! == LiveRoomType.PRIVATE &&
+ password != null
+ ) {
+ password
+ } else {
+ null
+ }
+ )
+
+ val requestJson = Gson().toJson(request)
+
+ val coverImage = if (coverImageUri != null) {
+ val file = File(getRealPathFromURI(coverImageUri!!))
+ MultipartBody.Part.createFormData(
+ "coverImage",
+ file.name,
+ file.asRequestBody("image/*".toMediaType())
+ )
+ } else {
+ null
+ }
+ compositeDisposable.add(
+ repository.createRoom(
+ coverImage,
+ requestJson.toRequestBody("text/plain".toMediaType()),
+ "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success && it.data != null) {
+ onSuccess(it.data!!)
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ _isLoading.postValue(false)
+ },
+ {
+ _isLoading.postValue(false)
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+ }
+
+ fun removeTag(tag: String) {
+ tags.remove(tag)
+ _selectedLiveData.postValue(tags.toList())
+ }
+
+ fun addTag(tag: String) {
+ tags.add(tag)
+ _selectedLiveData.postValue(tags.toList())
+ }
+
+ private fun validateData(): Boolean {
+ if (title.isBlank()) {
+ _toastLiveData.postValue("제목을 입력해주세요.")
+ return false
+ }
+
+ if (content.isBlank() || content.length < 5) {
+ _toastLiveData.postValue("내용을 5자 이상 입력해주세요.")
+ return false
+ }
+
+ if (numberOfPeople < 3 || numberOfPeople > 999) {
+ _toastLiveData.postValue("인원을 3~999명 사이로 입력해주세요.")
+ return false
+ }
+
+ if (coverImageUri == null && coverImagePath == null) {
+ _toastLiveData.postValue("커버이미지를 선택해주세요.")
+ return false
+ }
+
+ if (!_timeNowLiveData.value!! && (beginDate.isBlank() || beginTime.isBlank())) {
+ _toastLiveData.postValue("예약날짜와 시간을 선택해주세요.")
+ return false
+ }
+
+ if (
+ _roomTypeLiveData.value!! == LiveRoomType.PRIVATE &&
+ (password == null || password!!.length != 6)
+ ) {
+ _toastLiveData.postValue("방 입장 비밀번호 6자리를 입력해 주세요.")
+ return false
+ }
+
+ return true
+ }
+
+ fun setPrice(price: Int) {
+ _priceLiveData.value = price
+ }
+
+ fun setAdult(isAdult: Boolean) {
+ _isAdultLiveData.value = isAdult
+ }
+
+ fun getRecentInfo(onSuccess: (GetRecentRoomInfoResponse) -> Unit) {
+ _isLoading.value = true
+ compositeDisposable.add(
+ repository.getRecentRoomInfo(token = "Bearer ${SharedPreferenceManager.token}")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success && it.data != null) {
+ coverImageUri = null
+ coverImagePath = it.data.coverImagePath
+ onSuccess(it.data!!)
+
+ _toastLiveData.postValue("최근데이터를 불러왔습니다.")
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "최근데이터를 불러오지 못했습니다.\n다시 시도해 주세요."
+ )
+ }
+ }
+
+ _isLoading.value = false
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("최근데이터를 불러오지 못했습니다.\n다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt
index 37ff507..4161011 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt
@@ -9,7 +9,7 @@ data class GetRoomDetailResponse(
@SerializedName("roomId") val roomId: Long,
@SerializedName("price") val price: Int,
@SerializedName("title") val title: String,
- @SerializedName("content") val content: String,
+ @SerializedName("notice") val notice: String,
@SerializedName("isPaid") val isPaid: Boolean,
@SerializedName("isPrivateRoom") val isPrivateRoom: Boolean,
@SerializedName("password") val password: Int?,
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt
index 6c8c2bf..ba18d11 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt
@@ -173,7 +173,7 @@ class LiveRoomDetailFragment(
setParticipantUserSummary(response.participatingUsers)
binding.tvTags.text = response.tags.joinToString(" ") { "#$it" }
- binding.tvContent.text = response.content
+ binding.tvContent.text = response.notice
if (response.channelName.isNullOrBlank()) {
binding.tvParticipateExpression.text = "예약자"
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/dialog/LiveCancelDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/dialog/LiveCancelDialog.kt
new file mode 100644
index 0000000..c37a350
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/dialog/LiveCancelDialog.kt
@@ -0,0 +1,77 @@
+package kr.co.vividnext.sodalive.live.room.dialog
+
+import android.app.Activity
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.WindowManager
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import kr.co.vividnext.sodalive.databinding.DialogLiveInputBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+
+class LiveCancelDialog(
+ activity: Activity,
+ layoutInflater: LayoutInflater,
+ title: String,
+ hint: String,
+ confirmButtonTitle: String,
+ confirmButtonClick: (String) -> Unit,
+ cancelButtonTitle: String = "",
+ cancelButtonClick: (() -> Unit)? = null,
+) {
+ private val alertDialog: AlertDialog
+ private val dialogView = DialogLiveInputBinding.inflate(layoutInflater)
+
+ init {
+ val dialogBuilder = AlertDialog.Builder(activity)
+ dialogBuilder.setView(dialogView.root)
+
+ alertDialog = dialogBuilder.create()
+ alertDialog.setCancelable(false)
+ alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+
+ dialogView.tvTitle.text = title
+ dialogView.etReason.hint = hint
+
+ dialogView.tvCancel.text = cancelButtonTitle
+ dialogView.tvCancel.setOnClickListener {
+ alertDialog.dismiss()
+ cancelButtonClick?.let { it() }
+ }
+
+ dialogView.tvConfirm.text = confirmButtonTitle
+ dialogView.tvConfirm.setOnClickListener {
+ if (dialogView.etReason.text.isNotBlank()) {
+ alertDialog.dismiss()
+ confirmButtonClick(dialogView.etReason.text.toString())
+ } else {
+ Toast.makeText(activity, "취소사유를 입력하세요.", Toast.LENGTH_LONG).show()
+ }
+ }
+
+ dialogView.tvCancel.visibility = if (cancelButtonTitle.isNotBlank()) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+
+ dialogView.tvConfirm.visibility = if (confirmButtonTitle.isNotBlank()) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+ }
+
+ fun show(width: Int) {
+ alertDialog.show()
+
+ val lp = WindowManager.LayoutParams()
+ lp.copyFrom(alertDialog.window?.attributes)
+ lp.width = width - (26.7f.dpToPx()).toInt()
+ lp.height = WindowManager.LayoutParams.WRAP_CONTENT
+
+ alertDialog.window?.attributes = lp
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/dialog/LivePaymentDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/dialog/LivePaymentDialog.kt
new file mode 100644
index 0000000..c7ca33b
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/dialog/LivePaymentDialog.kt
@@ -0,0 +1,32 @@
+package kr.co.vividnext.sodalive.live.room.dialog
+
+import android.app.Activity
+import android.view.LayoutInflater
+import android.widget.LinearLayout
+import kr.co.vividnext.sodalive.dialog.LiveDialog
+
+class LivePaymentDialog(
+ activity: Activity,
+ layoutInflater: LayoutInflater,
+ title: String,
+ desc: String,
+ confirmButtonTitle: String,
+ confirmButtonClick: () -> Unit,
+ cancelButtonTitle: String = "",
+ cancelButtonClick: (() -> Unit)? = null,
+) : LiveDialog(
+ activity,
+ layoutInflater,
+ title,
+ desc,
+ confirmButtonTitle,
+ confirmButtonClick,
+ cancelButtonTitle,
+ cancelButtonClick
+) {
+ init {
+ val lp = dialogView.tvConfirm.layoutParams as LinearLayout.LayoutParams
+ lp.weight = 2F
+ dialogView.tvConfirm.layoutParams = lp
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/dialog/LiveRoomPasswordDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/dialog/LiveRoomPasswordDialog.kt
new file mode 100644
index 0000000..e3166ca
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/dialog/LiveRoomPasswordDialog.kt
@@ -0,0 +1,84 @@
+package kr.co.vividnext.sodalive.live.room.dialog
+
+import android.app.Activity
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.WindowManager
+import androidx.appcompat.app.AlertDialog
+import com.jakewharton.rxbinding4.widget.textChanges
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.disposables.CompositeDisposable
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kr.co.vividnext.sodalive.databinding.DialogLiveRoomPasswordBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+import kr.co.vividnext.sodalive.extensions.moneyFormat
+
+class LiveRoomPasswordDialog(
+ activity: Activity,
+ layoutInflater: LayoutInflater,
+ can: Int,
+ confirmButtonClick: (String) -> Unit,
+) {
+
+ private val alertDialog: AlertDialog
+ val dialogView = DialogLiveRoomPasswordBinding.inflate(layoutInflater)
+
+ private val compositeDisposable = CompositeDisposable()
+
+ init {
+ val dialogBuilder = AlertDialog.Builder(activity)
+ dialogBuilder.setView(dialogView.root)
+
+ alertDialog = dialogBuilder.create()
+ alertDialog.setCancelable(false)
+ alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+
+ if (can > 0) {
+ dialogView.tvCoin.visibility = View.VISIBLE
+ dialogView.tvCoin.text = can.moneyFormat()
+ dialogView.tvConfirm.text = "으로 입장"
+ } else {
+ dialogView.tvCoin.visibility = View.GONE
+ dialogView.tvConfirm.text = "입장하기"
+ }
+
+ compositeDisposable.add(
+ dialogView.etPassword.textChanges().skip(1)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ if (it.isNotEmpty()) {
+ try {
+ it.toString().toInt()
+ } catch (e: NumberFormatException) {
+ dialogView.etPassword.setText(it.substring(0, it.length - 1))
+ }
+ }
+ }
+ )
+ dialogView.tvCancel.setOnClickListener { alertDialog.dismiss() }
+ dialogView.llConfirm.setOnClickListener {
+ alertDialog.dismiss()
+ if (dialogView.etPassword.text.isNotBlank()) {
+ confirmButtonClick(dialogView.etPassword.text.toString())
+ } else {
+ confirmButtonClick("")
+ }
+ }
+
+ alertDialog.setOnDismissListener { compositeDisposable.clear() }
+ }
+
+ fun show(width: Int) {
+ alertDialog.show()
+
+ val lp = WindowManager.LayoutParams()
+ lp.copyFrom(alertDialog.window?.attributes)
+ lp.width = width - (26.7f.dpToPx()).toInt()
+ lp.height = WindowManager.LayoutParams.WRAP_CONTENT
+
+ alertDialog.window?.attributes = lp
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/GetLiveTagResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/GetLiveTagResponse.kt
new file mode 100644
index 0000000..6cc7187
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/GetLiveTagResponse.kt
@@ -0,0 +1,9 @@
+package kr.co.vividnext.sodalive.live.room.tag
+
+import com.google.gson.annotations.SerializedName
+
+data class GetLiveTagResponse(
+ @SerializedName("id") val id: Long,
+ @SerializedName("tag") val tag: String,
+ @SerializedName("image") val image: String
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/LiveTagAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/LiveTagAdapter.kt
new file mode 100644
index 0000000..504be6d
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/LiveTagAdapter.kt
@@ -0,0 +1,90 @@
+package kr.co.vividnext.sodalive.live.room.tag
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import coil.load
+import coil.transform.RoundedCornersTransformation
+import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.databinding.ItemLiveTagBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+
+class LiveTagAdapter(
+ private val selectedTags: Set,
+ private val onItemClick: (String, Boolean) -> Boolean
+) : RecyclerView.Adapter() {
+ inner class ViewHolder(
+ private val context: Context,
+ private val binding: ItemLiveTagBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+
+ private var isChecked = false
+
+ fun bind(item: GetLiveTagResponse) {
+ if (selectedTags.contains(item.tag)) {
+ binding.ivTagChecked.visibility = View.VISIBLE
+ binding.tvTag.setTextColor(ContextCompat.getColor(context, R.color.color_9970ff))
+ isChecked = true
+ } else {
+ binding.ivTagChecked.visibility = View.GONE
+ binding.tvTag.setTextColor(ContextCompat.getColor(context, R.color.color_bbbbbb))
+ isChecked = false
+ }
+
+ binding.ivTag.load(item.image) {
+ crossfade(true)
+ placeholder(R.drawable.bg_placeholder)
+ transformations(RoundedCornersTransformation(30f.dpToPx()))
+ }
+ binding.tvTag.text = item.tag
+
+ binding.root.setOnClickListener {
+ isChecked = !isChecked
+
+ if (onItemClick(item.tag, isChecked)) {
+ if (isChecked) {
+ binding.ivTagChecked.visibility = View.VISIBLE
+ binding.tvTag.setTextColor(
+ ContextCompat.getColor(
+ context,
+ R.color.color_9970ff
+ )
+ )
+ } else {
+ binding.ivTagChecked.visibility = View.GONE
+ binding.tvTag.setTextColor(
+ ContextCompat.getColor(
+ context,
+ R.color.color_bbbbbb
+ )
+ )
+ }
+ } else {
+ isChecked = !isChecked
+ }
+ }
+ }
+ }
+
+ val items = mutableSetOf()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(
+ parent.context,
+ ItemLiveTagBinding.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/live/room/tag/LiveTagFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/LiveTagFragment.kt
new file mode 100644
index 0000000..3714dde
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/LiveTagFragment.kt
@@ -0,0 +1,107 @@
+package kr.co.vividnext.sodalive.live.room.tag
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.graphics.Rect
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import android.widget.Toast
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.extensions.dpToPx
+import org.koin.android.ext.android.inject
+
+class LiveTagFragment(
+ private val selectedTags: Set,
+ private val onItemClick: (String, Boolean) -> Boolean
+) : BottomSheetDialogFragment() {
+
+
+ private val viewModel: LiveTagViewModel by inject()
+
+ private lateinit var adapter: LiveTagAdapter
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val dialog = super.onCreateDialog(savedInstanceState)
+
+ dialog.setOnShowListener {
+ val d = it as BottomSheetDialog
+ val bottomSheet = d.findViewById(
+ com.google.android.material.R.id.design_bottom_sheet
+ )
+ if (bottomSheet != null) {
+ BottomSheetBehavior.from(bottomSheet).state = BottomSheetBehavior.STATE_EXPANDED
+ }
+ }
+
+ return dialog
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = inflater.inflate(R.layout.fragment_live_tag, container, false)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ view.findViewById(R.id.iv_close).setOnClickListener {
+ dialog?.dismiss()
+ }
+
+ view.findViewById(R.id.tv_select).setOnClickListener {
+ dialog?.dismiss()
+ }
+
+ setupAdapter(view)
+ bindData()
+
+ viewModel.getTags()
+ }
+
+ private fun setupAdapter(view: View) {
+ val recyclerView = view.findViewById(R.id.rv_tags)
+ adapter = LiveTagAdapter(selectedTags) { tag, isChecked ->
+ return@LiveTagAdapter onItemClick(tag, isChecked)
+ }
+
+ recyclerView.setHasFixedSize(true)
+ recyclerView.layoutManager = GridLayoutManager(requireContext(), 4)
+ recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
+ override fun getItemOffsets(
+ outRect: Rect,
+ view: View,
+ parent: RecyclerView,
+ state: RecyclerView.State
+ ) {
+ super.getItemOffsets(outRect, view, parent, state)
+
+ outRect.top = 13.3f.dpToPx().toInt()
+ outRect.bottom = 13.3f.dpToPx().toInt()
+ outRect.left = 13.3f.dpToPx().toInt()
+ outRect.right = 13.3f.dpToPx().toInt()
+ }
+ })
+ recyclerView.adapter = adapter
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ private fun bindData() {
+ viewModel.toastLiveData.observe(viewLifecycleOwner) {
+ it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
+ }
+
+ viewModel.tagLiveData.observe(viewLifecycleOwner) {
+ adapter.items.addAll(it)
+ adapter.notifyDataSetChanged()
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/LiveTagRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/LiveTagRepository.kt
new file mode 100644
index 0000000..721b15c
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/LiveTagRepository.kt
@@ -0,0 +1,11 @@
+package kr.co.vividnext.sodalive.live.room.tag
+
+import io.reactivex.rxjava3.core.Single
+import kr.co.vividnext.sodalive.common.ApiResponse
+import kr.co.vividnext.sodalive.live.LiveApi
+
+class LiveTagRepository(private val api: LiveApi) {
+ fun getTags(token: String): Single>> {
+ return api.getTags(token)
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/LiveTagViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/LiveTagViewModel.kt
new file mode 100644
index 0000000..691b7fe
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/LiveTagViewModel.kt
@@ -0,0 +1,46 @@
+package kr.co.vividnext.sodalive.live.room.tag
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.orhanobut.logger.Logger
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kr.co.vividnext.sodalive.base.BaseViewModel
+import kr.co.vividnext.sodalive.common.SharedPreferenceManager
+
+class LiveTagViewModel(private val repository: LiveTagRepository) : BaseViewModel() {
+ private val _toastLiveData = MutableLiveData()
+ val toastLiveData: LiveData
+ get() = _toastLiveData
+
+ private val _tagLiveData = MutableLiveData>()
+ val tagLiveData: LiveData>
+ get() = _tagLiveData
+
+ fun getTags() {
+ compositeDisposable.add(
+ repository.getTags("Bearer ${SharedPreferenceManager.token}")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success && it.data != null) {
+ _tagLiveData.postValue(it.data!!)
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/EditLiveRoomInfoRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/EditLiveRoomInfoRequest.kt
new file mode 100644
index 0000000..00f6568
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/EditLiveRoomInfoRequest.kt
@@ -0,0 +1,11 @@
+package kr.co.vividnext.sodalive.live.room.update
+
+import com.google.gson.annotations.SerializedName
+
+data class EditLiveRoomInfoRequest(
+ @SerializedName("title") val title: String?,
+ @SerializedName("content") val notice: String?,
+ @SerializedName("numberOfPeople") val numberOfPeople: Int?,
+ @SerializedName("beginDateTimeString") val beginDateTimeString: String?,
+ @SerializedName("timezone") val timezone: String?
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomEditActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomEditActivity.kt
new file mode 100644
index 0000000..39f003f
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomEditActivity.kt
@@ -0,0 +1,221 @@
+package kr.co.vividnext.sodalive.live.room.update
+
+import android.annotation.SuppressLint
+import android.app.DatePickerDialog
+import android.app.TimePickerDialog
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.MotionEvent
+import android.widget.Toast
+import androidx.core.content.IntentCompat
+import com.jakewharton.rxbinding4.widget.textChanges
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kr.co.vividnext.sodalive.R
+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.ActivityLiveRoomEditBinding
+import kr.co.vividnext.sodalive.extensions.convertDateFormat
+import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
+import org.koin.android.ext.android.inject
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+class LiveRoomEditActivity : BaseActivity(
+ ActivityLiveRoomEditBinding::inflate
+) {
+
+ private val viewModel: LiveRoomEditViewModel by inject()
+
+ private lateinit var loadingDialog: LoadingDialog
+
+ private val handler = Handler(Looper.getMainLooper())
+
+ private val datePickerDialogListener =
+ DatePickerDialog.OnDateSetListener { _, year, monthOfYear, dayOfMonth ->
+ viewModel.beginDate = String.format("%d-%02d-%02d", year, monthOfYear + 1, dayOfMonth)
+ viewModel.setReservationDate(
+ String.format(
+ "%d.%02d.%02d",
+ year,
+ monthOfYear + 1,
+ dayOfMonth
+ )
+ )
+ }
+
+ private val timePickerDialogListener =
+ TimePickerDialog.OnTimeSetListener { _, hourOfDay, minute ->
+ val timeString = String.format("%02d:%02d", hourOfDay, minute)
+ viewModel.beginTime = timeString
+ viewModel.setReservationTime(timeString.convertDateFormat("HH:mm", "a hh:mm"))
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ val roomDetail = IntentCompat.getParcelableExtra(
+ intent,
+ Constants.EXTRA_ROOM_DETAIL,
+ GetRoomDetailResponse::class.java
+ )
+
+ super.onCreate(savedInstanceState)
+
+ if (roomDetail == null) {
+ Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
+ finish()
+ }
+
+ bindData()
+ binding.etTitle.setText(roomDetail!!.title)
+ binding.etContent.setText(roomDetail.notice)
+ binding.etNumberOfPeople.setText(roomDetail.numberOfParticipantsTotal.toString())
+ viewModel.setRoomDetail(roomDetail = roomDetail)
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun setupView() {
+ loadingDialog = LoadingDialog(this, layoutInflater)
+
+ binding.toolbar.tvBack.text = "라이브 수정"
+ binding.toolbar.tvBack.setOnClickListener { finish() }
+
+ binding.tvReservationDate.setOnClickListener {
+ val reservationDate = viewModel.beginDate.split("-")
+ val datePicker: DatePickerDialog
+
+ if (reservationDate.isNotEmpty() && reservationDate.size == 3) {
+ datePicker = DatePickerDialog(
+ this,
+ R.style.DatePickerStyle,
+ datePickerDialogListener,
+ reservationDate[0].toInt(),
+ reservationDate[1].toInt() - 1,
+ reservationDate[2].toInt()
+ )
+ } else {
+ val dateString = SimpleDateFormat(
+ "yyyy.MM.dd",
+ Locale.getDefault()
+ ).format(Date()).split(".")
+
+ datePicker = DatePickerDialog(
+ this,
+ R.style.DatePickerStyle,
+ datePickerDialogListener,
+ dateString[0].toInt(),
+ dateString[1].toInt() - 1,
+ dateString[2].toInt()
+ )
+ }
+
+ datePicker.show()
+ }
+ binding.tvReservationTime.setOnClickListener {
+ val reservationTime = viewModel.beginTime.split(":")
+ val timePicker: TimePickerDialog
+
+ if (reservationTime.isNotEmpty() && reservationTime.size == 2) {
+ timePicker = TimePickerDialog(
+ this,
+ R.style.TimePickerStyle,
+ timePickerDialogListener,
+ reservationTime[0].toInt(),
+ reservationTime[1].toInt(),
+ false
+ )
+ } else {
+ val timeString = SimpleDateFormat(
+ "HH:mm",
+ Locale.getDefault()
+ ).format(Date()).split(":")
+
+ timePicker = TimePickerDialog(
+ this,
+ R.style.TimePickerStyle,
+ timePickerDialogListener,
+ timeString[0].toInt(),
+ timeString[1].toInt(),
+ false
+ )
+ }
+
+ timePicker.show()
+ }
+ binding.tvUpdate.setOnClickListener {
+ binding.tvUpdate.isEnabled = false
+
+ viewModel.updateLiveRoom { finish() }
+
+ handler.postDelayed(
+ { binding.tvUpdate.isEnabled = true },
+ 3000
+ )
+ }
+
+ binding.etContent.setOnTouchListener { view, motionEvent ->
+ view.parent.parent.requestDisallowInterceptTouchEvent(true)
+ if ((motionEvent.action and MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
+ view.parent.parent.requestDisallowInterceptTouchEvent(false)
+ }
+ false
+ }
+ }
+
+ @SuppressLint("SetTextI18n")
+ private fun bindData() {
+ compositeDisposable.add(
+ binding.etTitle.textChanges().skip(1)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ viewModel.title = it.toString()
+ }
+ )
+
+ compositeDisposable.add(
+ binding.etContent.textChanges().skip(1)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ binding.tvNumberOfCharacters.text = "${it.length}자"
+ viewModel.content = it.toString()
+ }
+ )
+
+ compositeDisposable.add(
+ binding.etNumberOfPeople.textChanges().skip(1)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ if (it.isEmpty()) {
+ viewModel.numberOfPeople = 0
+ } else {
+ viewModel.numberOfPeople = it.toString().toInt()
+ }
+ }
+ )
+
+ 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.reservationDateLiveData.observe(this) {
+ binding.tvReservationDate.text = it
+ }
+
+ viewModel.reservationTimeLiveData.observe(this) {
+ binding.tvReservationTime.text = it
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomEditViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomEditViewModel.kt
new file mode 100644
index 0000000..78697e1
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomEditViewModel.kt
@@ -0,0 +1,180 @@
+package kr.co.vividnext.sodalive.live.room.update
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.google.gson.Gson
+import com.orhanobut.logger.Logger
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kr.co.vividnext.sodalive.base.BaseViewModel
+import kr.co.vividnext.sodalive.common.SharedPreferenceManager
+import kr.co.vividnext.sodalive.extensions.convertDateFormat
+import kr.co.vividnext.sodalive.live.LiveRepository
+import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.RequestBody.Companion.toRequestBody
+import java.util.Locale
+import java.util.TimeZone
+
+class LiveRoomEditViewModel(
+ private val repository: LiveRepository
+) : BaseViewModel() {
+
+ private val _reservationDateLiveData = MutableLiveData("날짜를 선택해주세요")
+ val reservationDateLiveData: LiveData
+ get() = _reservationDateLiveData
+
+ private val _reservationTimeLiveData = MutableLiveData("시간을 설정해주세요")
+ val reservationTimeLiveData: LiveData
+ get() = _reservationTimeLiveData
+
+ private val _toastLiveData = MutableLiveData()
+ val toastLiveData: LiveData
+ get() = _toastLiveData
+
+ private var _isLoading = MutableLiveData(false)
+ val isLoading: LiveData
+ get() = _isLoading
+
+ private lateinit var roomDetail: GetRoomDetailResponse
+
+ var title = ""
+ var content = ""
+ var numberOfPeople = 0
+ var beginDate = ""
+ var beginTime = ""
+ var beginDateTimeStr = ""
+
+ fun setReservationDate(dateString: String) {
+ _reservationDateLiveData.postValue(dateString)
+ }
+
+ fun setReservationTime(timeString: String) {
+ _reservationTimeLiveData.postValue(timeString)
+ }
+
+ fun updateLiveRoom(onSuccess: () -> Unit) {
+ if (!_isLoading.value!! && validateData()) {
+ _isLoading.value = true
+ val request = EditLiveRoomInfoRequest(
+ title = if (title != roomDetail.title) {
+ title
+ } else {
+ null
+ },
+ notice = if (content != roomDetail.notice) {
+ content
+ } else {
+ null
+ },
+ numberOfPeople = if (numberOfPeople != roomDetail.numberOfParticipantsTotal) {
+ numberOfPeople
+ } else {
+ null
+ },
+ beginDateTimeString = if (beginDateTimeStr != "$beginDate $beginTime") {
+ "$beginDate $beginTime"
+ } else {
+ null
+ },
+ timezone = TimeZone.getDefault().id
+ )
+
+ if (
+ request.title == null &&
+ request.notice == null &&
+ request.numberOfPeople == null &&
+ request.beginDateTimeString == null
+ ) {
+ _toastLiveData.value = "변경사항이 없습니다."
+ _isLoading.value = false
+ return
+ }
+
+ val requestJson = Gson().toJson(request)
+
+ compositeDisposable.add(
+ repository.editLiveRoomInfo(
+ roomId = roomDetail.roomId,
+ request = requestJson.toRequestBody("text/plain".toMediaType()),
+ token = "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success && it.data != null) {
+ _toastLiveData.value = "라이브 정보가 수정 되었습니다."
+ onSuccess()
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "라이브 정보를 수정 하지 못했습니다.\n다시 시도해 주세요."
+ )
+ }
+ }
+ _isLoading.postValue(false)
+ },
+ {
+ _isLoading.postValue(false)
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue(
+ "라이브 정보를 수정 하지 못했습니다.\n다시 시도해 주세요."
+ )
+ }
+ )
+ )
+ }
+ }
+
+ fun setRoomDetail(roomDetail: GetRoomDetailResponse) {
+ this.roomDetail = roomDetail
+ val date = roomDetail.beginDateTime.convertDateFormat(
+ from = "yyyy.MM.dd EEE hh:mm a",
+ to = "yyyy.MM.dd",
+ inputLocale = Locale.ENGLISH
+ )
+
+ val time = roomDetail.beginDateTime.convertDateFormat(
+ from = "yyyy.MM.dd EEE hh:mm a",
+ to = "a hh:mm",
+ inputLocale = Locale.ENGLISH
+ )
+
+ _reservationDateLiveData.value = date
+ _reservationTimeLiveData.value = time
+
+ beginDate = date.convertDateFormat(
+ from = "yyyy.MM.dd",
+ to = "yyyy-MM-dd",
+ )
+
+ beginTime = time.convertDateFormat(
+ from = "a hh:mm",
+ to = "HH:mm",
+ )
+
+ beginDateTimeStr = "$beginDate $beginTime"
+ }
+
+ private fun validateData(): Boolean {
+ if (title.isBlank()) {
+ _toastLiveData.postValue("제목을 입력해주세요.")
+ return false
+ }
+
+ if (content.isBlank() || content.length < 5) {
+ _toastLiveData.postValue("내용을 5자 이상 입력해주세요.")
+ return false
+ }
+
+ if (numberOfPeople < 3 || numberOfPeople > 999) {
+ _toastLiveData.postValue("인원을 3~999명 사이로 입력해주세요.")
+ return false
+ }
+
+ return true
+ }
+}
diff --git a/app/src/main/res/drawable-xxhdpi/ic_circle_x.png b/app/src/main/res/drawable-xxhdpi/ic_circle_x.png
new file mode 100644
index 0000000..41cc8d7
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_circle_x.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_select_check.png b/app/src/main/res/drawable-xxhdpi/ic_select_check.png
new file mode 100644
index 0000000..b7ba15b
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_select_check.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_tag_check.png b/app/src/main/res/drawable-xxhdpi/ic_tag_check.png
new file mode 100644
index 0000000..0243889
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_tag_check.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/img_compleate_book.png b/app/src/main/res/drawable-xxhdpi/img_compleate_book.png
new file mode 100644
index 0000000..a9ec0af
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/img_compleate_book.png differ
diff --git a/app/src/main/res/drawable/bg_live_room_price_select.xml b/app/src/main/res/drawable/bg_live_room_price_select.xml
new file mode 100644
index 0000000..7526c8e
--- /dev/null
+++ b/app/src/main/res/drawable/bg_live_room_price_select.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_13_3_3e3358.xml b/app/src/main/res/drawable/bg_round_corner_13_3_3e3358.xml
new file mode 100644
index 0000000..f79d25f
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_13_3_3e3358.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_24_3_339970ff_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_24_3_339970ff_9970ff.xml
new file mode 100644
index 0000000..69ab8aa
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_24_3_339970ff_9970ff.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_24_3_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_24_3_9970ff.xml
new file mode 100644
index 0000000..9eb276c
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_24_3_9970ff.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_1f1734.xml b/app/src/main/res/drawable/bg_round_corner_6_7_1f1734.xml
new file mode 100644
index 0000000..3459572
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_6_7_1f1734.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_232323_777777.xml b/app/src/main/res/drawable/bg_round_corner_6_7_232323_777777.xml
new file mode 100644
index 0000000..18b009e
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_6_7_232323_777777.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_333333_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_6_7_333333_9970ff.xml
new file mode 100644
index 0000000..9c6d1ad
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_6_7_333333_9970ff.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_4d9970ff_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_6_7_4d9970ff_9970ff.xml
new file mode 100644
index 0000000..65d78c1
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_6_7_4d9970ff_9970ff.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_live_reservation_complete.xml b/app/src/main/res/layout/activity_live_reservation_complete.xml
new file mode 100644
index 0000000..cd6bfda
--- /dev/null
+++ b/app/src/main/res/layout/activity_live_reservation_complete.xml
@@ -0,0 +1,339 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_live_room_create.xml b/app/src/main/res/layout/activity_live_room_create.xml
new file mode 100644
index 0000000..24f95c3
--- /dev/null
+++ b/app/src/main/res/layout/activity_live_room_create.xml
@@ -0,0 +1,783 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_live_room_edit.xml b/app/src/main/res/layout/activity_live_room_edit.xml
new file mode 100644
index 0000000..99f0cfc
--- /dev/null
+++ b/app/src/main/res/layout/activity_live_room_edit.xml
@@ -0,0 +1,260 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_live.xml b/app/src/main/res/layout/dialog_live.xml
new file mode 100644
index 0000000..8eda466
--- /dev/null
+++ b/app/src/main/res/layout/dialog_live.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_live_input.xml b/app/src/main/res/layout/dialog_live_input.xml
new file mode 100644
index 0000000..690d101
--- /dev/null
+++ b/app/src/main/res/layout/dialog_live_input.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_live_room_password.xml b/app/src/main/res/layout/dialog_live_room_password.xml
new file mode 100644
index 0000000..d07dfd0
--- /dev/null
+++ b/app/src/main/res/layout/dialog_live_room_password.xml
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_live_tag.xml b/app/src/main/res/layout/fragment_live_tag.xml
new file mode 100644
index 0000000..b81d7e6
--- /dev/null
+++ b/app/src/main/res/layout/fragment_live_tag.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_live_tag.xml b/app/src/main/res/layout/item_live_tag.xml
new file mode 100644
index 0000000..6c29764
--- /dev/null
+++ b/app/src/main/res/layout/item_live_tag.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_live_tag_selected.xml b/app/src/main/res/layout/item_live_tag_selected.xml
new file mode 100644
index 0000000..31622c2
--- /dev/null
+++ b/app/src/main/res/layout/item_live_tag_selected.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 3f10932..1d8a96e 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -27,12 +27,15 @@
#352953
#664aab
#232323
+ #525252
+ #DD4500
+ #1F1734
+ #333333
#B3909090
#88909090
#339970FF
#7FE2E2E2
#4D9970FF
- #525252
- #DD4500
+ #A285EB
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index c742e17..6785574 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -26,4 +26,37 @@
+
+
+
+
+
+
+
+