From 8a094adc4fd34bd12c42735b84c5f108ac550dca Mon Sep 17 00:00:00 2001 From: klaus Date: Mon, 31 Jul 2023 17:15:46 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20-=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91,=20=EC=B7=A8=EC=86=8C,=20=EC=9E=85=EC=9E=A5,=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=98=88=EC=95=BD=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 3 + .../co/vividnext/sodalive/common/Constants.kt | 4 + .../java/kr/co/vividnext/sodalive/di/AppDI.kt | 10 + .../vividnext/sodalive/dialog/LiveDialog.kt | 74 ++ .../kr/co/vividnext/sodalive/live/LiveApi.kt | 66 ++ .../vividnext/sodalive/live/LiveFragment.kt | 132 ++- .../vividnext/sodalive/live/LiveRepository.kt | 62 ++ .../vividnext/sodalive/live/LiveViewModel.kt | 179 ++++ .../reservation/MakeLiveReservationRequest.kt | 11 + .../MakeLiveReservationResponse.kt | 17 + .../LiveReservationCompleteActivity.kt | 53 ++ .../sodalive/live/room/CancelLiveRequest.kt | 8 + .../live/room/EnterOrQuitLiveRoomRequest.kt | 9 + .../sodalive/live/room/LiveRoomType.kt | 8 + .../sodalive/live/room/StartLiveRequest.kt | 9 + .../live/room/create/CreateLiveRoomRequest.kt | 18 + .../room/create/CreateLiveRoomResponse.kt | 8 + .../room/create/GetRecentRoomInfoResponse.kt | 11 + .../room/create/LiveRoomCreateActivity.kt | 616 ++++++++++++++ .../room/create/LiveRoomCreateViewModel.kt | 255 ++++++ .../live/room/detail/GetRoomDetailResponse.kt | 2 +- .../room/detail/LiveRoomDetailFragment.kt | 2 +- .../live/room/dialog/LiveCancelDialog.kt | 77 ++ .../live/room/dialog/LivePaymentDialog.kt | 32 + .../room/dialog/LiveRoomPasswordDialog.kt | 84 ++ .../live/room/tag/GetLiveTagResponse.kt | 9 + .../sodalive/live/room/tag/LiveTagAdapter.kt | 90 ++ .../sodalive/live/room/tag/LiveTagFragment.kt | 107 +++ .../live/room/tag/LiveTagRepository.kt | 11 + .../live/room/tag/LiveTagViewModel.kt | 46 + .../room/update/EditLiveRoomInfoRequest.kt | 11 + .../live/room/update/LiveRoomEditActivity.kt | 221 +++++ .../live/room/update/LiveRoomEditViewModel.kt | 180 ++++ .../main/res/drawable-xxhdpi/ic_circle_x.png | Bin 0 -> 453 bytes .../res/drawable-xxhdpi/ic_select_check.png | Bin 0 -> 392 bytes .../main/res/drawable-xxhdpi/ic_tag_check.png | Bin 0 -> 2316 bytes .../drawable-xxhdpi/img_compleate_book.png | Bin 0 -> 21432 bytes .../drawable/bg_live_room_price_select.xml | 5 + .../drawable/bg_round_corner_13_3_3e3358.xml | 8 + .../bg_round_corner_24_3_339970ff_9970ff.xml | 8 + .../drawable/bg_round_corner_24_3_9970ff.xml | 8 + .../drawable/bg_round_corner_6_7_1f1734.xml | 8 + .../bg_round_corner_6_7_232323_777777.xml | 8 + .../bg_round_corner_6_7_333333_9970ff.xml | 8 + .../bg_round_corner_6_7_4d9970ff_9970ff.xml | 8 + .../activity_live_reservation_complete.xml | 339 ++++++++ .../res/layout/activity_live_room_create.xml | 783 ++++++++++++++++++ .../res/layout/activity_live_room_edit.xml | 260 ++++++ app/src/main/res/layout/dialog_live.xml | 79 ++ app/src/main/res/layout/dialog_live_input.xml | 88 ++ .../res/layout/dialog_live_room_password.xml | 139 ++++ app/src/main/res/layout/fragment_live_tag.xml | 72 ++ app/src/main/res/layout/item_live_tag.xml | 44 + .../res/layout/item_live_tag_selected.xml | 30 + app/src/main/res/values/colors.xml | 7 +- app/src/main/res/values/themes.xml | 33 + 56 files changed, 4351 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/dialog/LiveDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationResponse.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/reservation/complete/LiveReservationCompleteActivity.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/CancelLiveRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomType.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/StartLiveRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/create/CreateLiveRoomRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/create/CreateLiveRoomResponse.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/create/GetRecentRoomInfoResponse.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateActivity.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateViewModel.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/dialog/LiveCancelDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/dialog/LivePaymentDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/dialog/LiveRoomPasswordDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/GetLiveTagResponse.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/LiveTagAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/LiveTagFragment.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/LiveTagRepository.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/tag/LiveTagViewModel.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/update/EditLiveRoomInfoRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomEditActivity.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomEditViewModel.kt create mode 100644 app/src/main/res/drawable-xxhdpi/ic_circle_x.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_select_check.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_tag_check.png create mode 100644 app/src/main/res/drawable-xxhdpi/img_compleate_book.png create mode 100644 app/src/main/res/drawable/bg_live_room_price_select.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_13_3_3e3358.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_24_3_339970ff_9970ff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_24_3_9970ff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_1f1734.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_232323_777777.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_333333_9970ff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_4d9970ff_9970ff.xml create mode 100644 app/src/main/res/layout/activity_live_reservation_complete.xml create mode 100644 app/src/main/res/layout/activity_live_room_create.xml create mode 100644 app/src/main/res/layout/activity_live_room_edit.xml create mode 100644 app/src/main/res/layout/dialog_live.xml create mode 100644 app/src/main/res/layout/dialog_live_input.xml create mode 100644 app/src/main/res/layout/dialog_live_room_password.xml create mode 100644 app/src/main/res/layout/fragment_live_tag.xml create mode 100644 app/src/main/res/layout/item_live_tag.xml create mode 100644 app/src/main/res/layout/item_live_tag_selected.xml 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 0000000000000000000000000000000000000000..41cc8d73f99db378b8ab55401e77bf7d6cb5cccd GIT binary patch literal 453 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxKmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIDx0X`wF?gc*oi3T$Z{$D!qf7iVKo2L9fvHAbf#{V-XupI*$#a9yK7tC<8 z=<=$d7Y(1emG_G+J#_n^F3>_QPZ!4!i_=r5zTVcNz|nej*_Jc8+rQUeGd7*>kn`xn z`KQ}e%%qLljy?$5qIhG*odmt)FT5Tmb!(FL@@pvlh%3r%_bSOfSsByvJX3y054-RG z=ITjZdtASXnSWfj;{Ce=>(V=y=cZnNQllboZ>4W}e#&Xf`l<4ns=v?n{K~BV^yaOnAsi&D6+Dr3#n*Yst7Bd@Ga^%<}_(iv+1F=r~fU>@Dq4Yvd6;v zf>++^3EMQDg}&E1dE)8M7fL6Ou-JbUUb0wsiPiKc4n7BeA3WK1E9arr`Xl}AXZIbO z+x1@k`QaVq#ueulR_(a*cvU>Zuc?dTgI;a@-tW39{_L;R`p>_AFgxeE2Hp3)VFe5? N22WQ%mvv4FO#li==NkY3 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b7ba15bb2755fa44a5a32a383f974e210e4a0af1 GIT binary patch literal 392 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw3=&b&bO2H;>5jgR3=A9lx&I`x0{M)^LGDfr z>(0r%1ad3`d_r8^3w-{Q1!`@k`2r0QDhcunX864znD4M8tJ=efmy0$hHQIl4?fke7 zD0|-1#WBR=_}i;@`I;4YSRW{9tyuBmxBVNg;O#n16({tj6~5#7`zMo2XzG+Hf6BJB ztz30&tNYs>MI~)VKcuaeSt=N^u~%AS8OE^3ihz@((_2TeOt|K_|bN%MUraI*Zi>aD$N4=I)D+kMeJ zt-e@Wyt^*8#!PXQc=-FTx!x&r#bUc7uGO7k?~YJPGkA9A23+{X l%N6}@=fV0Vs;d9zvTLsvUQ^1r{y5NU44$rjF6*2UngG=O!d?IX literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0243889f83c94e36256fa093f7fb3a512e4f0cad GIT binary patch literal 2316 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 literal 0 HcmV?d00001 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 @@ + + + + + + + +