From 831533b4f65aa4025932897f9e0e8651d5fae1d1 Mon Sep 17 00:00:00 2001 From: klaus Date: Thu, 16 Jul 2026 22:59:07 +0900 Subject: [PATCH] =?UTF-8?q?refactor(creator):=20=EC=A7=84=ED=96=89=20?= =?UTF-8?q?=EC=A4=91=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EB=A5=BC=20=EA=B3=B5=ED=86=B5=ED=99=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/agora/Agora.kt | 25 +- .../co/vividnext/sodalive/app/SodaLiveApp.kt | 2 +- .../AddAllPlaybackTrackingRequest.kt | 2 +- .../audio_content/AudioContentViewModel.kt | 2 +- .../sodalive/audio_content/PurchaseOption.kt | 2 +- .../all/AudioContentNewAllAdapter.kt | 2 +- .../AudioContentCommentReplyAdapter.kt | 6 +- .../detail/GetAudioContentDetailResponse.kt | 2 +- .../AudioContentMainNewContentThemeAdapter.kt | 13 +- .../modify/AudioContentModifyActivity.kt | 3 +- .../order/AudioContentOrderConfirmDialog.kt | 2 +- .../order/AudioContentOrderListActivity.kt | 11 - .../order/GetAudioContentOrderListResponse.kt | 2 +- .../player/AudioContentPlayerFragment.kt | 1 - .../series/SeriesListAllViewModel.kt | 7 +- .../series/detail/SeriesDetailHomeFragment.kt | 1 - .../upload/AudioContentUploadActivity.kt | 2 +- .../upload/CreateAudioContentRequest.kt | 2 +- .../applicant/AuditionApplicantListAdapter.kt | 8 +- .../audition/detail/AuditionDetailActivity.kt | 1 - .../chat/character/CharacterAdapter.kt | 3 +- .../comment/CharacterCommentListFragment.kt | 5 +- .../comment/CharacterCommentReplyFragment.kt | 26 +- .../comment/CharacterCommentsAdapter.kt | 1 - .../detail/detail/CharacterDetailFragment.kt | 1 - .../detail/detail/CharacterDetailResponse.kt | 1 + .../detail/detail/OtherCharacterAdapter.kt | 1 - .../gallery/CharacterGalleryViewModel.kt | 4 +- .../detail/OriginalWorkDetailActivity.kt | 1 - .../ChatBackgroundPickerDialogFragment.kt | 5 +- .../sodalive/chat/talk/room/MessageStatus.kt | 2 + .../chat/talk/room/db/ChatMessageDao.kt | 6 +- .../sodalive/common/ImagePickerCropper.kt | 38 ++- .../vividnext/sodalive/common/RealPathUtil.kt | 18 +- .../kr/co/vividnext/sodalive/common/Utils.kt | 3 - .../vividnext/sodalive/dialog/LiveDialog.kt | 2 +- .../profile/CreatorFollowNotifyFragment.kt | 2 +- .../explorer/profile/UserProfileActivity.kt | 6 +- .../profile/cheers/PutModifyCheersRequest.kt | 2 +- .../all/CreatorCommunityAllViewModel.kt | 80 +++--- .../all/CreatorCommunityReportDialog.kt | 3 +- .../all/PurchaseCommunityPostDialog.kt | 2 +- .../all/PurchasePostRequest.kt | 2 +- .../CreatorCommunityCommentReplyAdapter.kt | 7 +- .../modify/CreatorCommunityModifyActivity.kt | 4 +- .../profile/donation/DonationRankingPeriod.kt | 1 + .../donation/GetDonationAllResponse.kt | 2 +- .../UserProfileFantalkAllViewActivity.kt | 2 +- .../follow/UserFollowerListActivity.kt | 1 - .../following/FollowingCreatorRepository.kt | 2 - .../sodalive/home/CreatorRankingAdapter.kt | 1 - .../sodalive/home/DayOfWeekAdapter.kt | 6 +- .../sodalive/home/HomeContentAdapter.kt | 2 +- .../vividnext/sodalive/live/LiveViewModel.kt | 1 - .../live/event_banner/EventBannerAdapter.kt | 38 --- .../sodalive/live/now/LiveNowAdapter.kt | 2 +- .../LiveRecommendChannelAdapter.kt | 2 +- .../GetLiveReservationResponse.kt | 2 +- .../LiveReservationCancelActivity.kt | 4 +- .../LiveReservationStatusAdapter.kt | 4 +- .../LiveReservationStatusViewModel.kt | 1 - .../sodalive/live/room/GenderRestriction.kt | 11 +- .../sodalive/live/room/LiveRoomActivity.kt | 95 ++++--- .../sodalive/live/room/LiveRoomDialog.kt | 6 +- .../live/room/LiveRoomNoChattingDialog.kt | 6 +- .../sodalive/live/room/LiveRoomRequestType.kt | 23 +- .../sodalive/live/room/LiveRoomStatus.kt | 7 +- .../sodalive/live/room/LiveRoomType.kt | 7 +- .../sodalive/live/room/StartLiveRequest.kt | 2 +- .../room/create/LiveRoomCreateActivity.kt | 8 +- .../room/create/LiveRoomCreateViewModel.kt | 3 +- .../live/room/dialog/LiveCancelDialog.kt | 2 +- .../live/room/dialog/LivePaymentDialog.kt | 2 +- .../room/dialog/LiveRoomPasswordDialog.kt | 2 +- .../room/like/LiveRoomLikeHeartRequest.kt | 2 +- .../live/room/menu/MenuConfigActivity.kt | 1 - .../room/profile/LiveRoomProfileAdapter.kt | 6 +- .../room/profile/LiveRoomUserProfileDialog.kt | 2 +- .../sodalive/live/room/tag/LiveTagFragment.kt | 2 - .../live/room/update/LiveRoomEditViewModel.kt | 4 +- .../sodalive/live/roulette/RouletteView.kt | 9 +- .../roulette/config/RouletteConfigActivity.kt | 16 +- .../sodalive/mypage/MyPageViewModel.kt | 3 +- .../sodalive/mypage/alarm/AddAlarmActivity.kt | 3 +- .../sodalive/mypage/alarm/AlarmActivity.kt | 2 +- .../mypage/alarm/AlarmListActivity.kt | 9 +- .../AlarmSelectAudioContentAdapter.kt | 2 - .../sodalive/mypage/auth/BootpayResponse.kt | 2 +- .../mypage/block/BlockMemberActivity.kt | 10 +- .../can/charge/iap/CanChargeIapViewModel.kt | 10 +- .../mypage/can/coupon/CanCouponActivity.kt | 3 +- .../mypage/can/coupon/CanCouponViewModel.kt | 7 +- .../mypage/can/payment/PaymentGateway.kt | 7 +- .../mypage/profile/ProfileUpdateActivity.kt | 3 +- .../mypage/profile/ProfileUpdateViewModel.kt | 6 +- .../nickname/NicknameUpdateViewModel.kt | 86 +++--- .../mypage/recent/db/RecentContentDao.kt | 4 +- .../sodalive/report/ReportRequest.kt | 9 +- .../sodalive/search/SearchResponse.kt | 11 +- .../settings/signout/SignOutViewModel.kt | 36 +-- .../live/CreatorChannelLiveFragment.kt | 14 +- .../ui/CreatorChannelCurrentLiveView.kt | 74 +++++ .../ui/CreatorChannelHomeSectionAdapter.kt | 25 +- .../bg_creator_channel_live_current.xml | 9 - .../bg_creator_channel_live_price.xml | 6 - .../layout/fragment_creator_channel_live.xml | 112 +------- .../layout/item_creator_channel_home_live.xml | 130 +-------- .../view_creator_channel_current_live.xml | 119 ++++++++ .../CreatorChannelActivitySourceTest.kt | 46 ++-- .../CreatorChannelLiveFragmentLayoutTest.kt | 34 ++- .../CreatorChannelCurrentLiveViewModelTest.kt | 88 ++++++ .../plan-task.md | 260 ++++++++++++++++++ .../prd.md | 184 +++++++++++++ docs/agent-guides/build-test-style.md | 7 + 114 files changed, 1204 insertions(+), 725 deletions(-) delete mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/event_banner/EventBannerAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveView.kt delete mode 100644 app/src/main/res/drawable/bg_creator_channel_live_current.xml delete mode 100644 app/src/main/res/drawable/bg_creator_channel_live_price.xml create mode 100644 app/src/main/res/layout/view_creator_channel_current_live.xml create mode 100644 app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveViewModelTest.kt create mode 100644 docs/20260716_크리에이터_채널_진행_중_라이브_공통_컴포넌트/plan-task.md create mode 100644 docs/20260716_크리에이터_채널_진행_중_라이브_공통_컴포넌트/prd.md diff --git a/app/src/main/java/kr/co/vividnext/sodalive/agora/Agora.kt b/app/src/main/java/kr/co/vividnext/sodalive/agora/Agora.kt index 7dff77dc..998869cb 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/agora/Agora.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/agora/Agora.kt @@ -361,17 +361,20 @@ class Agora( fun deInitRtmClient(rtmEventListener: RtmEventListener) { rtmClient?.removeEventListener(rtmEventListener) - rtmClient?.unsubscribe(roomChannelName, object : ResultCallback { - override fun onSuccess(responseInfo: Void?) { - Logger.e("RTM unsubscribe - $roomChannelName") - roomChannelName = null - } + rtmClient?.unsubscribe( + roomChannelName, + object : ResultCallback { + override fun onSuccess(responseInfo: Void?) { + Logger.e("RTM unsubscribe - $roomChannelName") + roomChannelName = null + } - override fun onFailure(errorInfo: ErrorInfo) { - Logger.e("RTM unsubscribe fail - ${errorInfo.errorCode}") - Logger.e("RTM unsubscribe fail - ${errorInfo.errorReason}") + override fun onFailure(errorInfo: ErrorInfo) { + Logger.e("RTM unsubscribe fail - ${errorInfo.errorCode}") + Logger.e("RTM unsubscribe fail - ${errorInfo.errorReason}") + } } - }) + ) rtmClient?.unsubscribe( "inbox_${SharedPreferenceManager.userId}", object : ResultCallback { @@ -383,7 +386,8 @@ class Agora( Logger.e("RTM unsubscribe fail - ${errorInfo.errorCode}") Logger.e("RTM unsubscribe fail - ${errorInfo.errorReason}") } - }) + } + ) rtmClient?.logout(object : ResultCallback { override fun onSuccess(responseInfo: Void?) { Logger.e("RTM logout") @@ -398,7 +402,6 @@ class Agora( // 상태 리셋 rtmLoggedIn = false rtmLoginInProgress = false - } // endregion } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/app/SodaLiveApp.kt b/app/src/main/java/kr/co/vividnext/sodalive/app/SodaLiveApp.kt index 913957f9..78782160 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/app/SodaLiveApp.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/app/SodaLiveApp.kt @@ -136,7 +136,7 @@ class SodaLiveApp : Application(), DefaultLifecycleObserver { applicationContext, BuildConfig.NOTIFLY_PROJECT_ID, BuildConfig.NOTIFLY_USERNAME, - BuildConfig.NOTIFLY_PASSWORD, + BuildConfig.NOTIFLY_PASSWORD ) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AddAllPlaybackTrackingRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AddAllPlaybackTrackingRequest.kt index 11571c8d..0e7ada13 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AddAllPlaybackTrackingRequest.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AddAllPlaybackTrackingRequest.kt @@ -14,5 +14,5 @@ data class AddAllPlaybackTrackingRequest( data class PlaybackTrackingData( @SerializedName("contentId") val contentId: Long, @SerializedName("playDateTime") val playDateTime: String, - @SerializedName("isPreview") val isPreview: Boolean, + @SerializedName("isPreview") val isPreview: Boolean ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentViewModel.kt index 7ebd406d..97c4a32e 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentViewModel.kt @@ -127,7 +127,7 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba compositeDisposable.add( repository.getCategoryList( creatorId = userId, - token = "Bearer ${SharedPreferenceManager.token}", + token = "Bearer ${SharedPreferenceManager.token}" ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/PurchaseOption.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/PurchaseOption.kt index be11d5c9..912606dc 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/PurchaseOption.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/PurchaseOption.kt @@ -10,5 +10,5 @@ enum class PurchaseOption { BUY_ONLY, @SerializedName("RENT_ONLY") - RENT_ONLY, + RENT_ONLY } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentNewAllAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentNewAllAdapter.kt index e1f13429..66ffdbb9 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentNewAllAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentNewAllAdapter.kt @@ -21,7 +21,7 @@ import kr.co.vividnext.sodalive.extensions.moneyFormat class AudioContentNewAllAdapter( private val itemWidth: Int, private val onClickItem: (Long) -> Unit, - private val onClickCreator: (Long) -> Unit, + private val onClickCreator: (Long) -> Unit ) : RecyclerView.Adapter() { inner class ViewHolder( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentReplyAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentReplyAdapter.kt index e7b60854..4e0fe102 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentReplyAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentReplyAdapter.kt @@ -35,7 +35,7 @@ class AudioContentCommentReplyAdapter( LayoutInflater.from(parent.context), parent, false - ), + ) ) } else { AudioContentCommentReplyItemViewHolder( @@ -172,9 +172,7 @@ class AudioContentCommentReplyItemViewHolder( private val context: Context, private val creatorId: Long, private val binding: ItemAudioContentCommentReplyBinding, - private val showOptionMenu: ( - Context, View, Long, Long, Long, onClickModify: () -> Unit - ) -> Unit, + private val showOptionMenu: (Context, View, Long, Long, Long, onClickModify: () -> Unit) -> Unit, private val modifyComment: (Long, String) -> Unit ) : AudioContentCommentReplyViewHolder(binding) { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/GetAudioContentDetailResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/GetAudioContentDetailResponse.kt index bc4d08a7..7f28b9cb 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/GetAudioContentDetailResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/GetAudioContentDetailResponse.kt @@ -51,7 +51,7 @@ data class GetAudioContentDetailResponse( data class OtherContentResponse( @SerializedName("contentId") val contentId: Long, @SerializedName("title") val title: String, - @SerializedName("coverUrl") val coverUrl: String, + @SerializedName("coverUrl") val coverUrl: String ) @Keep diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/new_content/AudioContentMainNewContentThemeAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/new_content/AudioContentMainNewContentThemeAdapter.kt index d6a1b36b..2fb36120 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/new_content/AudioContentMainNewContentThemeAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/new_content/AudioContentMainNewContentThemeAdapter.kt @@ -25,10 +25,15 @@ class AudioContentMainNewContentThemeAdapter( fun bind(theme: String) { if ( theme == selectedTheme || - (selectedTheme == "" && theme == SodaLiveApplicationHolder.get() - .getString(R.string.audio_content_label_all)) || - (selectedTheme == "" && theme == SodaLiveApplicationHolder.get() - .getString(R.string.screen_home_sort_revenue)) + ( + selectedTheme == "" && theme == SodaLiveApplicationHolder.get() + .getString(R.string.audio_content_label_all)) || + ( + selectedTheme == "" && theme == SodaLiveApplicationHolder.get() + .getString( + R.string.screen_home_sort_revenue + ) + ) ) { binding.tvTheme.setBackgroundResource( R.drawable.bg_round_corner_16_7_transparent_3bb9f1 diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/modify/AudioContentModifyActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/modify/AudioContentModifyActivity.kt index ad8ab230..9ee47858 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/modify/AudioContentModifyActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/modify/AudioContentModifyActivity.kt @@ -76,7 +76,8 @@ class AudioContentModifyActivity : BaseActivity Unit, + confirmButtonClick: () -> Unit ) { private val alertDialog: AlertDialog diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderListActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderListActivity.kt index 7ab67aea..320e47eb 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderListActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderListActivity.kt @@ -1,20 +1,9 @@ package kr.co.vividnext.sodalive.audio_content.order -import android.annotation.SuppressLint -import android.content.Intent -import android.graphics.Rect import android.os.Bundle -import android.view.View -import android.widget.Toast -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import kr.co.vividnext.sodalive.R -import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity 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.ActivityAudioContentOrderListBinding -import kr.co.vividnext.sodalive.extensions.dpToPx class AudioContentOrderListActivity : BaseActivity( ActivityAudioContentOrderListBinding::inflate diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/GetAudioContentOrderListResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/GetAudioContentOrderListResponse.kt index aed272a9..8f401c1c 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/GetAudioContentOrderListResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/GetAudioContentOrderListResponse.kt @@ -20,5 +20,5 @@ data class GetAudioContentOrderListItem( @SerializedName("isAdult") val isAdult: Boolean, @SerializedName("orderType") val orderType: OrderType, @SerializedName("likeCount") val likeCount: Int, - @SerializedName("commentCount") val commentCount: Int, + @SerializedName("commentCount") val commentCount: Int ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/player/AudioContentPlayerFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/player/AudioContentPlayerFragment.kt index f69ae529..f4363261 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/player/AudioContentPlayerFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/player/AudioContentPlayerFragment.kt @@ -429,7 +429,6 @@ class AudioContentPlayerFragment( mediaController?.seekTo(progress.toLong()) } } - }) updateMediaMetadata(mediaController?.currentMediaItem?.mediaMetadata) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/SeriesListAllViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/SeriesListAllViewModel.kt index 73b977d0..c698c401 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/SeriesListAllViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/SeriesListAllViewModel.kt @@ -14,8 +14,11 @@ import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder class SeriesListAllViewModel(private val repository: SeriesRepository) : BaseViewModel() { enum class SeriesSortType { - @SerializedName("NEWEST") NEWEST, - @SerializedName("OLDEST") OLDEST + @SerializedName("NEWEST") + NEWEST, + + @SerializedName("OLDEST") + OLDEST } private val _toastLiveData = MutableLiveData() diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/detail/SeriesDetailHomeFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/detail/SeriesDetailHomeFragment.kt index 9eda0b74..22fbb458 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/detail/SeriesDetailHomeFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/detail/SeriesDetailHomeFragment.kt @@ -1,6 +1,5 @@ package kr.co.vividnext.sodalive.audio_content.series.detail -import android.annotation.SuppressLint import android.content.Intent import android.os.Build import android.os.Bundle diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/AudioContentUploadActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/AudioContentUploadActivity.kt index c1f7e293..36fab8dc 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/AudioContentUploadActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/AudioContentUploadActivity.kt @@ -240,7 +240,7 @@ class AudioContentUploadActivity : BaseActivity { - outRect.top = 24f.dpToPx().toInt(); + outRect.top = 24f.dpToPx().toInt() outRect.bottom = 12f.dpToPx().toInt() } @@ -196,7 +199,6 @@ class CharacterCommentReplyFragment : BaseFragment( binding.tvPersonalityContent.maxLines = 3 binding.tvPersonalityContent.ellipsize = TextUtils.TruncateAt.END } - } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/detail/CharacterDetailResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/detail/CharacterDetailResponse.kt index b53f149c..e0f7c3ef 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/detail/CharacterDetailResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/detail/CharacterDetailResponse.kt @@ -29,6 +29,7 @@ data class CharacterDetailResponse( enum class CharacterType { @SerializedName("Clone") CLONE, + @SerializedName("Character") CHARACTER } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/detail/OtherCharacterAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/detail/OtherCharacterAdapter.kt index 98e2dd95..05cd6eb4 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/detail/OtherCharacterAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/detail/OtherCharacterAdapter.kt @@ -49,7 +49,6 @@ class OtherCharacterAdapter( transformations(RoundedCornersTransformation(16f.dpToPx())) } - binding.root.setOnClickListener { onItemClick?.invoke(item) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryViewModel.kt index b4b0d75d..8724bd88 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryViewModel.kt @@ -118,9 +118,7 @@ class CharacterGalleryViewModel( val token = "Bearer ${SharedPreferenceManager.token}" isPurchasing = true - _uiState.value = _uiState.value?.copy( - isLoading = isRequesting || isPurchasing, - ) + _uiState.value = _uiState.value?.copy(isLoading = isRequesting || isPurchasing) compositeDisposable.add( repository.purchaseCharacterImage(token = token, imageId = imageId) .subscribeOn(Schedulers.io()) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/original/detail/OriginalWorkDetailActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/original/detail/OriginalWorkDetailActivity.kt index c0d62823..e9d5dc4e 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/original/detail/OriginalWorkDetailActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/original/detail/OriginalWorkDetailActivity.kt @@ -85,7 +85,6 @@ class OriginalWorkDetailActivity : BaseActivity // 실패 시에도 현재까지의 목록 표시(없으면 빈 목록) @@ -161,9 +160,7 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() { } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BgVH { - val binding = ItemChatBackgroundImageBinding.inflate( - LayoutInflater.from(parent.context), parent, false - ) + val binding = ItemChatBackgroundImageBinding.inflate(LayoutInflater.from(parent.context), parent, false) return BgVH(binding, onClick) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/MessageStatus.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/MessageStatus.kt index a2134426..e5aae25d 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/MessageStatus.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/MessageStatus.kt @@ -12,8 +12,10 @@ import androidx.annotation.Keep enum class MessageStatus { /** 전송 중 */ SENDING, + /** 전송 완료 */ SENT, + /** 전송 실패 */ FAILED } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/db/ChatMessageDao.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/db/ChatMessageDao.kt index 0241b51a..65e58967 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/db/ChatMessageDao.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/db/ChatMessageDao.kt @@ -4,7 +4,11 @@ package kr.co.vividnext.sodalive.chat.talk.room.db import androidx.annotation.Keep -import androidx.room.* +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update @Keep @Dao diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/ImagePickerCropper.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/ImagePickerCropper.kt index b1167d73..2e694ef7 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/common/ImagePickerCropper.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/ImagePickerCropper.kt @@ -45,15 +45,21 @@ class ImagePickerCropper( // 13+ : 시스템 Photo Picker private val pickPhoto: ActivityResultLauncher = caller.registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> - if (uri == null) onError(CancellationException("이미지 선택을 취소했습니다.")) - else handlePickedUri(uri) + if (uri == null) { + onError(CancellationException("이미지 선택을 취소했습니다.")) + } else { + handlePickedUri(uri) + } } // 12- : SAF GetContent private val pickContent: ActivityResultLauncher = caller.registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> - if (uri == null) onError(CancellationException("이미지 선택을 취소했습니다.")) - else handlePickedUri(uri) + if (uri == null) { + onError(CancellationException("이미지 선택을 취소했습니다.")) + } else { + handlePickedUri(uri) + } } // uCrop 결과 수신 @@ -105,7 +111,9 @@ class ImagePickerCropper( val gifFile = copyUriToCacheAsGif(source) lastCroppedFile = gifFile val fileUri = FileProvider.getUriForFile( - context, "${BuildConfig.APPLICATION_ID}.fileprovider", gifFile + context, + "${BuildConfig.APPLICATION_ID}.fileprovider", + gifFile ) // 2) 바로 반환 (크롭 생략) @@ -137,13 +145,20 @@ class ImagePickerCropper( ) ?.use { c -> val idx = c.getColumnIndex(OpenableColumns.DISPLAY_NAME) - if (idx >= 0 && c.moveToFirst()) c.getString(idx) else null + if (idx >= 0 && c.moveToFirst()) { + c.getString(idx) + } else { + null + } } } private fun copyUriToCacheAsGif(uri: Uri): File { - val base = if (config.useExternalCache) context.externalCacheDir ?: context.cacheDir - else context.cacheDir + val base = if (config.useExternalCache) { + context.externalCacheDir ?: context.cacheDir + } else { + context.cacheDir + } // 원본 이름 유지 시도, 실패하면 타임스탬프 val name = getDisplayName(uri)?.takeIf { it.endsWith(".gif", true) } @@ -201,8 +216,11 @@ class ImagePickerCropper( } private fun createTempCropFile(): File { - val base = if (config.useExternalCache) context.externalCacheDir ?: context.cacheDir - else context.cacheDir + val base = if (config.useExternalCache) { + context.externalCacheDir ?: context.cacheDir + } else { + context.cacheDir + } val ext = when (config.compressFormat) { Bitmap.CompressFormat.PNG -> "png" diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/RealPathUtil.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/RealPathUtil.kt index 9afc7287..cb400315 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/common/RealPathUtil.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/RealPathUtil.kt @@ -89,14 +89,17 @@ object RealPathUtil { } // MediaProvider // DownloadsProvider } else if ("content".equals(uri.scheme!!, ignoreCase = true)) { - // Return the remote address - return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn( - context, - uri, - null, - null - ) + return if (isGooglePhotosUri(uri)) { + uri.lastPathSegment + } else { + getDataColumn( + context, + uri, + null, + null + ) + } } else if ("file".equals(uri.scheme!!, ignoreCase = true)) { return uri.path } // File @@ -122,7 +125,6 @@ object RealPathUtil { selection: String?, selectionArgs: Array? ): String? { - var cursor: Cursor? = null val column = "_data" val projection = arrayOf(column) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/Utils.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/Utils.kt index d7c9fd96..29c7ae38 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/common/Utils.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/Utils.kt @@ -1,10 +1,7 @@ package kr.co.vividnext.sodalive.common -import android.content.Context -import android.os.Build import java.net.URLEncoder import java.nio.charset.StandardCharsets -import kr.co.vividnext.sodalive.settings.language.LanguageManager object Utils { fun convertDurationToString(duration: Int, showHours: Boolean = true): String { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/dialog/LiveDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/dialog/LiveDialog.kt index 37e9f25f..45e78613 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/dialog/LiveDialog.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/dialog/LiveDialog.kt @@ -18,7 +18,7 @@ open class LiveDialog( confirmButtonTitle: String, confirmButtonClick: () -> Unit, cancelButtonTitle: String = "", - cancelButtonClick: (() -> Unit)? = null, + cancelButtonClick: (() -> Unit)? = null ) { private val alertDialog: AlertDialog diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/CreatorFollowNotifyFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/CreatorFollowNotifyFragment.kt index cee1f149..ece943a1 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/CreatorFollowNotifyFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/CreatorFollowNotifyFragment.kt @@ -11,7 +11,7 @@ import kr.co.vividnext.sodalive.R class CreatorFollowNotifyFragment( private val onClickNotifyAll: () -> Unit, private val onClickNotifyNone: () -> Unit, - private val onClickUnFollow: () -> Unit, + private val onClickUnFollow: () -> Unit ) : BottomSheetDialogFragment() { override fun onCreateView( inflater: LayoutInflater, diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileActivity.kt index 21ae2bdd..bf574447 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileActivity.kt @@ -37,12 +37,12 @@ import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllActivity import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity -import kr.co.vividnext.sodalive.common.image.BlurTransformation import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.SodaDialog import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.common.image.BlurTransformation import kr.co.vividnext.sodalive.databinding.ActivityUserProfileBinding import kr.co.vividnext.sodalive.databinding.ItemCreatorCommunityBinding import kr.co.vividnext.sodalive.dialog.MemberProfileDialog @@ -138,7 +138,7 @@ class UserProfileActivity : BaseActivity( binding.ivMenu.setOnClickListener { showOptionMenu( this, - binding.ivMenu, + binding.ivMenu ) } @@ -468,7 +468,7 @@ class UserProfileActivity : BaseActivity( viewModel.modifyCheers( cheersId = cheersId, creatorId = userId, - cheersContent = content, + cheersContent = content ) } }, diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/cheers/PutModifyCheersRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/cheers/PutModifyCheersRequest.kt index 4624bdd1..913b87de 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/cheers/PutModifyCheersRequest.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/cheers/PutModifyCheersRequest.kt @@ -7,5 +7,5 @@ import com.google.gson.annotations.SerializedName data class PutModifyCheersRequest( @SerializedName("cheersId") val cheersId: Long, @SerializedName("content") var content: String? = null, - @SerializedName("isActive") var isActive: Boolean? = null, + @SerializedName("isActive") var isActive: Boolean? = null ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityAllViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityAllViewModel.kt index f5c32d01..407ea3f4 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityAllViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityAllViewModel.kt @@ -149,7 +149,6 @@ class CreatorCommunityAllViewModel( .observeOn(AndroidSchedulers.mainThread()) .subscribe({}, {}) ) - } fun registerComment( @@ -282,49 +281,48 @@ class CreatorCommunityAllViewModel( ) ) } - } fun updateCommunityPostFixed(postId: Long, isFixed: Boolean) { - if (_isLoading.value == true) return - _isLoading.value = true + if (_isLoading.value == true) return + _isLoading.value = true - compositeDisposable.add( - repository.updateCommunityPostFixed( - postId = postId, - isFixed = isFixed, - token = "Bearer ${SharedPreferenceManager.token}" - ) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { - _isLoading.value = false - if (it.success) { - // 목록을 초기화하고 재조회하여 최신 고정 상태를 반영한다. - page = 1 - isLast = false - getCommunityPostList() - } else { - if (it.message != null) { - _toastLiveData.postValue(it.message) - } else { - _toastLiveData.postValue( - SodaLiveApplicationHolder.get() - .getString(R.string.common_error_unknown) - ) - } - } - }, - { - _isLoading.value = false - it.message?.let { message -> Logger.e(message) } - _toastLiveData.postValue( - SodaLiveApplicationHolder.get() - .getString(R.string.common_error_unknown) - ) - } - ) + compositeDisposable.add( + repository.updateCommunityPostFixed( + postId = postId, + isFixed = isFixed, + token = "Bearer ${SharedPreferenceManager.token}" ) - } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success) { + // 목록을 초기화하고 재조회하여 최신 고정 상태를 반영한다. + page = 1 + isLast = false + getCommunityPostList() + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + SodaLiveApplicationHolder.get() + .getString(R.string.common_error_unknown) + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue( + SodaLiveApplicationHolder.get() + .getString(R.string.common_error_unknown) + ) + } + ) + ) + } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityReportDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityReportDialog.kt index 097f7d2c..3530eb68 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityReportDialog.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityReportDialog.kt @@ -39,7 +39,8 @@ class CreatorCommunityReportDialog( alertDialog.dismiss() confirmButtonClick(reason) } else { - Toast.makeText(activity, + Toast.makeText( + activity, SodaLiveApplicationHolder.get() .getString(R.string.screen_audio_content_detail_report_reason_required), Toast.LENGTH_LONG diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/PurchaseCommunityPostDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/PurchaseCommunityPostDialog.kt index 471e32b2..f14d2d0a 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/PurchaseCommunityPostDialog.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/PurchaseCommunityPostDialog.kt @@ -16,7 +16,7 @@ class PurchaseCommunityPostDialog( activity: Activity, layoutInflater: LayoutInflater, can: Int, - confirmButtonClick: () -> Unit, + confirmButtonClick: () -> Unit ) { private val alertDialog: AlertDialog diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/PurchasePostRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/PurchasePostRequest.kt index 60fdcaeb..bc99d2f5 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/PurchasePostRequest.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/PurchasePostRequest.kt @@ -7,5 +7,5 @@ import com.google.gson.annotations.SerializedName data class PurchasePostRequest( @SerializedName("postId") val postId: Long, @SerializedName("timezone") val timezone: String, - @SerializedName("container") val container: String = "aos", + @SerializedName("container") val container: String = "aos" ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/comment/CreatorCommunityCommentReplyAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/comment/CreatorCommunityCommentReplyAdapter.kt index 85b9a563..2b98a676 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/comment/CreatorCommunityCommentReplyAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/comment/CreatorCommunityCommentReplyAdapter.kt @@ -127,9 +127,7 @@ class CreatorCommunityCommentReplyItemViewHolder( private val context: Context, private val creatorId: Long, private val binding: ItemCommunityPostCommentReplyBinding, - private val showOptionMenu: ( - Context, View, Long, Long, Long, onClickModify: () -> Unit - ) -> Unit, + private val showOptionMenu: (Context, View, Long, Long, Long, onClickModify: () -> Unit) -> Unit, private val modifyComment: (Long, String) -> Unit, private val onClickProfile: (Long) -> Unit ) : CreatorCommunityCommentReplyViewHolder(binding) { @@ -139,7 +137,7 @@ class CreatorCommunityCommentReplyItemViewHolder( onClickProfile(item.writerId) } } - + binding.ivCommentProfile.load(item.profileUrl) { crossfade(true) placeholder(R.drawable.ic_place_holder) @@ -178,5 +176,4 @@ class CreatorCommunityCommentReplyItemViewHolder( binding.ivMenu.visibility = View.GONE } } - } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/modify/CreatorCommunityModifyActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/modify/CreatorCommunityModifyActivity.kt index 94b75247..c0bf5ff3 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/modify/CreatorCommunityModifyActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/modify/CreatorCommunityModifyActivity.kt @@ -1,7 +1,6 @@ package kr.co.vividnext.sodalive.explorer.profile.creator_community.modify import android.Manifest -import android.annotation.SuppressLint import android.graphics.Bitmap import android.os.Build import android.os.Bundle @@ -76,7 +75,8 @@ class CreatorCommunityModifyActivity : BaseActivity, + val userDonationRanking: List ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/fantalk/UserProfileFantalkAllViewActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/fantalk/UserProfileFantalkAllViewActivity.kt index 31619c43..ae1067c3 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/fantalk/UserProfileFantalkAllViewActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/fantalk/UserProfileFantalkAllViewActivity.kt @@ -103,7 +103,7 @@ class UserProfileFantalkAllViewActivity : BaseActivity Unit, + context: Context, + private val onClickItem: (SeriesPublishedDaysOfWeek) -> Unit ) : RecyclerView.Adapter() { inner class ViewHolder( @@ -40,7 +40,7 @@ class DayOfWeekAdapter( DayOfWeek(dayOfWeekStr = context.getString(R.string.day_fri_short), dayOfWeek = SeriesPublishedDaysOfWeek.FRI), DayOfWeek(dayOfWeekStr = context.getString(R.string.day_sat_short), dayOfWeek = SeriesPublishedDaysOfWeek.SAT), DayOfWeek(dayOfWeekStr = context.getString(R.string.day_sun_short), dayOfWeek = SeriesPublishedDaysOfWeek.SUN), - DayOfWeek(dayOfWeekStr = context.getString(R.string.day_random), dayOfWeek = SeriesPublishedDaysOfWeek.RANDOM), + DayOfWeek(dayOfWeekStr = context.getString(R.string.day_random), dayOfWeek = SeriesPublishedDaysOfWeek.RANDOM) ) // 요일 숫자에 맞춰 배열 diff --git a/app/src/main/java/kr/co/vividnext/sodalive/home/HomeContentAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/home/HomeContentAdapter.kt index b122d107..f94cc6a3 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/home/HomeContentAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/home/HomeContentAdapter.kt @@ -13,7 +13,7 @@ import kr.co.vividnext.sodalive.extensions.dpToPx class HomeContentAdapter( private val itemSquareSizePx: Int? = null, - private val onClickItem: (Long) -> Unit, + private val onClickItem: (Long) -> Unit ) : RecyclerView.Adapter() { private val items = mutableListOf() 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 727f8b7e..fb394380 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 @@ -149,7 +149,6 @@ class LiveViewModel( fun getSummary() { if (!_isLoading.value!!) { - if (SharedPreferenceManager.token.isNotBlank()) { getFollowedChannelList() getLatestPostListFromCreatorsYouFollow() diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/event_banner/EventBannerAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/event_banner/EventBannerAdapter.kt deleted file mode 100644 index cdaed105..00000000 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/event_banner/EventBannerAdapter.kt +++ /dev/null @@ -1,38 +0,0 @@ -package kr.co.vividnext.sodalive.live.event_banner - -import android.content.Context -import android.widget.ImageView -import coil.load -import com.zhpan.bannerview.BaseBannerAdapter -import com.zhpan.bannerview.BaseViewHolder -import kr.co.vividnext.sodalive.R -import kr.co.vividnext.sodalive.settings.event.EventItem - -class EventBannerAdapter( - private val context: Context, - private val itemClick: (EventItem) -> Unit -) : BaseBannerAdapter() { - - override fun bindData( - holder: BaseViewHolder, - data: EventItem, - position: Int, - pageSize: Int - ) { - val ivThumbnail = holder.findViewById(R.id.iv_thumbnail) - ivThumbnail.load(data.thumbnailImageUrl) { - crossfade(true) - - val layoutParams = ivThumbnail.layoutParams - val screenWidth = context.resources.displayMetrics.widthPixels - layoutParams.width = screenWidth - layoutParams.height = (screenWidth * 300) / 1000 - ivThumbnail.layoutParams = layoutParams - } - ivThumbnail.setOnClickListener { itemClick(data) } - } - - override fun getLayoutId(viewType: Int): Int { - return R.layout.item_event_slider - } -} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/now/LiveNowAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/now/LiveNowAdapter.kt index c69ffe20..5cd54258 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/now/LiveNowAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/now/LiveNowAdapter.kt @@ -14,7 +14,7 @@ import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.live.GetRoomListResponse class LiveNowAdapter( - private val onClick: (GetRoomListResponse) -> Unit, + private val onClick: (GetRoomListResponse) -> Unit ) : RecyclerView.Adapter() { var items = mutableListOf() diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/recommend_channel/LiveRecommendChannelAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/recommend_channel/LiveRecommendChannelAdapter.kt index fc32588c..67e347f7 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/recommend_channel/LiveRecommendChannelAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/recommend_channel/LiveRecommendChannelAdapter.kt @@ -14,7 +14,7 @@ import kr.co.vividnext.sodalive.extensions.dpToPx class LiveRecommendChannelAdapter( private val onClick: (Long) -> Unit, - private val onClickMore: () -> Unit, + private val onClickMore: () -> Unit ) : RecyclerView.Adapter() { private val items = mutableListOf() diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/reservation_status/GetLiveReservationResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/reservation_status/GetLiveReservationResponse.kt index 9c7ca695..0424d7f7 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/reservation_status/GetLiveReservationResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/reservation_status/GetLiveReservationResponse.kt @@ -15,5 +15,5 @@ data class GetLiveReservationResponse( @SerializedName("price") val price: Int, @SerializedName("masterNickname") val masterNickname: String, @SerializedName("beginDateTimeUtc") val beginDateTimeUtc: String, - @SerializedName("cancelable") val cancelable: Boolean, + @SerializedName("cancelable") val cancelable: Boolean ) : Parcelable diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/reservation_status/LiveReservationCancelActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/reservation_status/LiveReservationCancelActivity.kt index 518fb9e9..c1527940 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/reservation_status/LiveReservationCancelActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/reservation_status/LiveReservationCancelActivity.kt @@ -126,9 +126,7 @@ class LiveReservationCancelActivity : BaseActivity() inner class ViewHolder( - private val binding: ItemLiveReservationStatusBinding, + private val binding: ItemLiveReservationStatusBinding ) : RecyclerView.ViewHolder(binding.root) { fun bind(item: GetLiveReservationResponse) { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/reservation_status/LiveReservationStatusViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/reservation_status/LiveReservationStatusViewModel.kt index cb955fd2..3defc50d 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/reservation_status/LiveReservationStatusViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/reservation_status/LiveReservationStatusViewModel.kt @@ -90,7 +90,6 @@ class LiveReservationStatusViewModel( _isLoading.value = false }, { - _isLoading.value = false it.message?.let { message -> Logger.e(message) } _toastLiveData.postValue( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/GenderRestriction.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/GenderRestriction.kt index 9241e491..12c96d40 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/GenderRestriction.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/GenderRestriction.kt @@ -3,7 +3,12 @@ package kr.co.vividnext.sodalive.live.room import com.google.gson.annotations.SerializedName enum class GenderRestriction { - @SerializedName("ALL") ALL, - @SerializedName("MALE_ONLY") MALE_ONLY, - @SerializedName("FEMALE_ONLY") FEMALE_ONLY + @SerializedName("ALL") + ALL, + + @SerializedName("MALE_ONLY") + MALE_ONLY, + + @SerializedName("FEMALE_ONLY") + FEMALE_ONLY } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt index 81715e73..75c4ecfd 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt @@ -481,8 +481,10 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB context = this, excludeGif = true, config = ImagePickerCropper.Config( - aspectX = 2f, aspectY = 3.8f, - maxWidth = 1080, maxHeight = 2052, + aspectX = 2f, + aspectY = 3.8f, + maxWidth = 1080, + maxHeight = 2052, compressFormat = Bitmap.CompressFormat.JPEG, compressQuality = 90 ), @@ -640,7 +642,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB userId, nickname, isBlock, - view, + view ) } ) @@ -1208,7 +1210,8 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB ) roomInfoEditDialog.setCoverImageUrl(response.coverImageUrl) roomInfoEditDialog.setMenuPreset(it) - roomInfoEditDialog.setConfirmAction { newTitle, newContent, newCoverImageFile, isActivateMenu, menuId, menu, isAdult, isEntryMessageEnabled -> + roomInfoEditDialog.setConfirmAction { newTitle, newContent, newCoverImageFile, + isActivateMenu, menuId, menu, isAdult, isEntryMessageEnabled -> if (isEntryMessageEnabled != null) { this.isEntryMessageEnabled = isEntryMessageEnabled } @@ -1337,7 +1340,10 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB hasInvokedJoinChannel = true joinChannel(response) } else { - Logger.e("joinChannel - skip (rtcConnected=$rtcConnected, rtmLoggedIn=$rtmLoggedIn, hasInvokedJoinChannel=$hasInvokedJoinChannel)") + Logger.e( + "joinChannel - skip (rtcConnected=$rtcConnected, " + + "rtmLoggedIn=$rtmLoggedIn, hasInvokedJoinChannel=$hasInvokedJoinChannel)" + ) } } @@ -3027,8 +3033,10 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB val elapsed = System.currentTimeMillis() - pressStartTime // 경과 시간 기준으로 물 채우기(progress) 계산 (0..100) - val progressFraction = (elapsed.coerceAtMost(longPressDurationMs) - .toFloat() / longPressDurationMs.toFloat()) + val progressFraction = ( + elapsed.coerceAtMost(longPressDurationMs) + .toFloat() / longPressDurationMs.toFloat() + ) val progress = (progressFraction * 100f).toInt() longPressCenterHeart?.let { heartView -> try { @@ -3131,8 +3139,11 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB heart.setFrontWaveColor("#ff959a".toColorInt()) heart.setBehindWaveColor("#ff959a".toColorInt()) val elapsed = System.currentTimeMillis() - pressStartTime - val progress = ((elapsed.coerceAtMost(longPressDurationMs) - .toFloat() / longPressDurationMs.toFloat()) * 100f).toInt() + val progress = ( + ( + elapsed.coerceAtMost(longPressDurationMs).toFloat() / longPressDurationMs.toFloat() + ) * 100f + ).toInt() try { heart.progress = progress } catch (_: Throwable) { @@ -3589,14 +3600,20 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB val s = 0.9f moveTo(0f, -0.45f * s) cubicTo( - 0.62f * s, -1.02f * s, - 1.22f * s, -0.04f * s, - 0f, 0.65f * s + 0.62f * s, + -1.02f * s, + 1.22f * s, + -0.04f * s, + 0f, + 0.65f * s ) cubicTo( - -1.22f * s, -0.04f * s, - -0.62f * s, -1.02f * s, - 0f, -0.45f * s + -1.22f * s, + -0.04f * s, + -0.62f * s, + -1.02f * s, + 0f, + -0.45f * s ) close() } @@ -3784,14 +3801,20 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB val s = 0.9f moveTo(0f, -0.45f * s) cubicTo( - 0.62f * s, -1.02f * s, - 1.22f * s, -0.04f * s, - 0f, 0.65f * s + 0.62f * s, + -1.02f * s, + 1.22f * s, + -0.04f * s, + 0f, + 0.65f * s ) cubicTo( - -1.22f * s, -0.04f * s, - -0.62f * s, -1.02f * s, - 0f, -0.45f * s + -1.22f * s, + -0.04f * s, + -0.62f * s, + -1.02f * s, + 0f, + -0.45f * s ) close() } @@ -3899,14 +3922,20 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB val s = 0.9f moveTo(0f, -0.45f * s) cubicTo( - 0.62f * s, -1.02f * s, - 1.22f * s, -0.04f * s, - 0f, 0.65f * s + 0.62f * s, + -1.02f * s, + 1.22f * s, + -0.04f * s, + 0f, + 0.65f * s ) cubicTo( - -1.22f * s, -0.04f * s, - -0.62f * s, -1.02f * s, - 0f, -0.45f * s + -1.22f * s, + -0.04f * s, + -0.62f * s, + -1.02f * s, + 0f, + -0.45f * s ) close() } @@ -3946,7 +3975,6 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB if (rotateEnabled) 300f * Math.random().toFloat() else 0f // deg/s val rotation0 = (-180f + 360f * Math.random().toFloat()) - val fadeStartRatio = 0.80f + 0.15f * Math.random().toFloat() // 0.80..0.95 val fadeStartY = h * fadeStartRatio @@ -3991,7 +4019,9 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB val fadeAlpha = if (y >= d.fadeStartY) { val denom = (h - d.fadeStartY).coerceAtLeast(1f) (1f - (y - d.fadeStartY) / denom).coerceIn(0f, 1f) - } else 1f + } else { + 1f + } val alpha = fadeAlpha.coerceIn(0f, 1f) paint.alpha = (alpha * 255).toInt().coerceIn(0, 255) @@ -4139,7 +4169,6 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB return false } - }) .into(binding.ivSignature) } @@ -4205,7 +4234,6 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } } } - } else { binding.flRoulette.visibility = View.GONE } @@ -4221,7 +4249,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB type = LiveRoomChatRawMessageType.ROULETTE_DONATION, message = randomItem, can = can, - donationMessage = "", + donationMessage = "" ) ) @@ -4269,7 +4297,8 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB .subscribe( { response -> Logger.e("성공: $response") - }, { error -> + }, + { error -> Logger.e("실패: ${error.message}") } ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomDialog.kt index 44b15f09..e4beca24 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomDialog.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomDialog.kt @@ -2,7 +2,6 @@ package kr.co.vividnext.sodalive.live.room import android.app.Activity import android.graphics.Color -import android.graphics.drawable.ColorDrawable import android.os.Handler import android.os.Looper import android.view.LayoutInflater @@ -10,12 +9,13 @@ import android.view.View import android.view.WindowManager import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog +import androidx.core.graphics.drawable.toDrawable import kr.co.vividnext.sodalive.databinding.DialogLiveRoomBinding import kr.co.vividnext.sodalive.extensions.dpToPx class LiveRoomDialog( activity: Activity, - layoutInflater: LayoutInflater, + layoutInflater: LayoutInflater ) { private val alertDialog: AlertDialog @@ -27,7 +27,7 @@ class LiveRoomDialog( dialogBuilder.setView(dialogView.root) alertDialog = dialogBuilder.create() - alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + alertDialog.window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable()) } fun show(width: Int) { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomNoChattingDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomNoChattingDialog.kt index 644bb3f9..ba3b4de9 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomNoChattingDialog.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomNoChattingDialog.kt @@ -2,10 +2,10 @@ package kr.co.vividnext.sodalive.live.room import android.app.Activity import android.graphics.Color -import android.graphics.drawable.ColorDrawable import android.view.LayoutInflater import android.view.WindowManager import androidx.appcompat.app.AlertDialog +import androidx.core.graphics.drawable.toDrawable import coil.load import coil.transform.CircleCropTransformation import kr.co.vividnext.sodalive.R @@ -17,7 +17,7 @@ class LiveRoomNoChattingDialog( layoutInflater: LayoutInflater, nickname: String, profileUrl: String, - confirmButtonClick: () -> Unit, + confirmButtonClick: () -> Unit ) { private val alertDialog: AlertDialog @@ -30,7 +30,7 @@ class LiveRoomNoChattingDialog( alertDialog = dialogBuilder.create() alertDialog.setCancelable(false) - alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + alertDialog.window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable()) dialogView.tvCancel.setOnClickListener { alertDialog.dismiss() diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomRequestType.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomRequestType.kt index 56bf0747..5d9191b8 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomRequestType.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomRequestType.kt @@ -3,10 +3,21 @@ package kr.co.vividnext.sodalive.live.room import com.google.gson.annotations.SerializedName enum class LiveRoomRequestType { - @SerializedName("INVITE_SPEAKER") INVITE_SPEAKER, - @SerializedName("CHANGE_LISTENER") CHANGE_LISTENER, - @SerializedName("KICK_OUT") KICK_OUT, - @SerializedName("SET_MANAGER") SET_MANAGER, - @SerializedName("RELEASE_MANAGER") RELEASE_MANAGER, - @SerializedName("NO_CHATTING") NO_CHATTING, + @SerializedName("INVITE_SPEAKER") + INVITE_SPEAKER, + + @SerializedName("CHANGE_LISTENER") + CHANGE_LISTENER, + + @SerializedName("KICK_OUT") + KICK_OUT, + + @SerializedName("SET_MANAGER") + SET_MANAGER, + + @SerializedName("RELEASE_MANAGER") + RELEASE_MANAGER, + + @SerializedName("NO_CHATTING") + NO_CHATTING } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomStatus.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomStatus.kt index 0acf81d5..031bd6ec 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomStatus.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomStatus.kt @@ -3,6 +3,9 @@ package kr.co.vividnext.sodalive.live.room import com.google.gson.annotations.SerializedName enum class LiveRoomStatus { - @SerializedName("NOW") NOW, - @SerializedName("RESERVATION") RESERVATION + @SerializedName("NOW") + NOW, + + @SerializedName("RESERVATION") + RESERVATION } 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 index 9460caa2..9671409c 100644 --- 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 @@ -3,6 +3,9 @@ package kr.co.vividnext.sodalive.live.room import com.google.gson.annotations.SerializedName enum class LiveRoomType { - @SerializedName("OPEN") OPEN, - @SerializedName("PRIVATE") PRIVATE, + @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 index a8416ba0..b43e5183 100644 --- 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 @@ -7,5 +7,5 @@ import java.util.TimeZone @Keep data class StartLiveRequest( @SerializedName("roomId") val roomId: Long, - @SerializedName("timezone") val timezone: String = TimeZone.getDefault().id, + @SerializedName("timezone") val timezone: String = TimeZone.getDefault().id ) 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 index d33e6530..547aabb5 100644 --- 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 @@ -61,7 +61,7 @@ class LiveRoomCreateActivity : BaseActivity( "%d-%02d-%02d", year, monthOfYear + 1, - dayOfMonth, + dayOfMonth ) viewModel.setReservationDate( String.format( @@ -136,8 +136,10 @@ class LiveRoomCreateActivity : BaseActivity( context = this, excludeGif = true, config = ImagePickerCropper.Config( - aspectX = 2f, aspectY = 3.8f, - maxWidth = 1080, maxHeight = 2052, + aspectX = 2f, + aspectY = 3.8f, + maxWidth = 1080, + maxHeight = 2052, compressFormat = Bitmap.CompressFormat.JPEG, compressQuality = 90 ), 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 index 56158096..14d68904 100644 --- 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 @@ -1,12 +1,12 @@ 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.R import kr.co.vividnext.sodalive.base.BaseViewModel import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.UiText @@ -16,7 +16,6 @@ import kr.co.vividnext.sodalive.live.LiveRepository import kr.co.vividnext.sodalive.live.room.GenderRestriction import kr.co.vividnext.sodalive.live.room.LiveRoomType import kr.co.vividnext.sodalive.live.room.menu.GetMenuPresetResponse -import kr.co.vividnext.sodalive.R import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody 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 index b519e671..d8076dfa 100644 --- 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 @@ -20,7 +20,7 @@ class LiveCancelDialog( confirmButtonTitle: String, confirmButtonClick: (String) -> Unit, cancelButtonTitle: String = "", - cancelButtonClick: (() -> Unit)? = null, + cancelButtonClick: (() -> Unit)? = null ) { private val alertDialog: AlertDialog private val dialogView = DialogLiveInputBinding.inflate(layoutInflater) 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 index 12723ed8..91b2e638 100644 --- 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 @@ -21,7 +21,7 @@ class LivePaymentDialog( confirmButtonTitle: String, confirmButtonClick: () -> Unit, cancelButtonTitle: String = "", - cancelButtonClick: (() -> Unit)? = null, + cancelButtonClick: (() -> Unit)? = null ) { private val alertDialog: AlertDialog private val dialogView = DialogLivePaymentBinding.inflate(layoutInflater) 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 index daef4542..065d6617 100644 --- 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 @@ -20,7 +20,7 @@ class LiveRoomPasswordDialog( activity: Activity, layoutInflater: LayoutInflater, can: Int, - confirmButtonClick: (String) -> Unit, + confirmButtonClick: (String) -> Unit ) { private val alertDialog: AlertDialog diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/like/LiveRoomLikeHeartRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/like/LiveRoomLikeHeartRequest.kt index d8e189d0..ebd38aea 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/like/LiveRoomLikeHeartRequest.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/like/LiveRoomLikeHeartRequest.kt @@ -7,5 +7,5 @@ import com.google.gson.annotations.SerializedName data class LiveRoomLikeHeartRequest( @SerializedName("roomId") val roomId: Long, @SerializedName("container") val container: String, - @SerializedName("heartCount") val heartCount: Int = 1, + @SerializedName("heartCount") val heartCount: Int = 1 ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/menu/MenuConfigActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/menu/MenuConfigActivity.kt index 357ccf4d..e34bce93 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/menu/MenuConfigActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/menu/MenuConfigActivity.kt @@ -15,7 +15,6 @@ 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.base.SodaDialog import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.databinding.ActivityMenuConfigBinding import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateViewModel diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileAdapter.kt index 4dcda8a3..2a1a6a7f 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileAdapter.kt @@ -195,19 +195,19 @@ abstract class LiveRoomProfileViewHolder(binding: ViewBinding) : } class LiveRoomProfileSpeakerTitleViewHolder( - private val binding: ItemLiveRoomProfileHeaderBinding, + private val binding: ItemLiveRoomProfileHeaderBinding ) : LiveRoomProfileViewHolder(binding) { override fun bind(item: LiveRoomProfileItem) = item.bind(binding) } class LiveRoomProfileListenerTitleViewHolder( - private val binding: ItemLiveRoomProfileHeaderBinding, + private val binding: ItemLiveRoomProfileHeaderBinding ) : LiveRoomProfileViewHolder(binding) { override fun bind(item: LiveRoomProfileItem) = item.bind(binding) } class LiveRoomProfileManagerTitleViewHolder( - private val binding: ItemLiveRoomProfileHeaderBinding, + private val binding: ItemLiveRoomProfileHeaderBinding ) : LiveRoomProfileViewHolder(binding) { override fun bind(item: LiveRoomProfileItem) = item.bind(binding) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomUserProfileDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomUserProfileDialog.kt index 13273206..12b9bbb0 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomUserProfileDialog.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomUserProfileDialog.kt @@ -28,7 +28,7 @@ class LiveRoomUserProfileDialog( private val onClickChangeListener: (Long) -> Unit, private val onClickKickOut: (Long) -> Unit, private val onClickNoChatting: (Long, String, String) -> Unit, - private val onClickPopupMenu: (Long, String, Boolean, View) -> Unit, + private val onClickPopupMenu: (Long, String, Boolean, View) -> Unit ) { private val alertDialog: AlertDialog private val dialogView = DialogLiveRoomUserProfileBinding.inflate(layoutInflater) 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 index 3714dde8..6faf9c65 100644 --- 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 @@ -24,8 +24,6 @@ class LiveTagFragment( private val selectedTags: Set, private val onItemClick: (String, Boolean) -> Boolean ) : BottomSheetDialogFragment() { - - private val viewModel: LiveTagViewModel by inject() private lateinit var adapter: LiveTagAdapter 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 index b30f41d2..8f06c29a 100644 --- 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 @@ -190,12 +190,12 @@ class LiveRoomEditViewModel( beginDate = date.convertDateFormat( from = "yyyy.MM.dd", - to = "yyyy-MM-dd", + to = "yyyy-MM-dd" ) beginTime = time.convertDateFormat( from = "a hh:mm", - to = "HH:mm", + to = "HH:mm" ) beginDateTimeStr = "$beginDate $beginTime" diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteView.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteView.kt index 11a78ec9..746d14a6 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteView.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteView.kt @@ -132,9 +132,12 @@ class RouletteView @JvmOverloads constructor( val targetAngle = 0 - (getAngleForOption(option) + 360 * 10) val rotateAnimation = RotateAnimation( - 0f, targetAngle, - RotateAnimation.RELATIVE_TO_SELF, 0.5f, - RotateAnimation.RELATIVE_TO_SELF, 0.5f + 0f, + targetAngle, + RotateAnimation.RELATIVE_TO_SELF, + 0.5f, + RotateAnimation.RELATIVE_TO_SELF, + 0.5f ) rotateAnimation.duration = 2000 rotateAnimation.fillAfter = true diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/config/RouletteConfigActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/config/RouletteConfigActivity.kt index e6d45fb5..32b5bb18 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/config/RouletteConfigActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/config/RouletteConfigActivity.kt @@ -67,15 +67,17 @@ class RouletteConfigActivity : BaseActivity( Service.INPUT_METHOD_SERVICE ) as InputMethodManager - binding.etSetPrice.filters = arrayOf(InputFilter { source, start, end, _, _, _ -> - // Only allow numeric input - for (i in start until end) { - if (!Character.isDigit(source[i])) { - return@InputFilter "" + binding.etSetPrice.filters = arrayOf( + InputFilter { source, start, end, _, _, _ -> + // Only allow numeric input + for (i in start until end) { + if (!Character.isDigit(source[i])) { + return@InputFilter "" + } } + null } - null - }) + ) binding.ivRouletteIsActive.setOnClickListener { viewModel.toggleIsActive() } binding.ivAddOption.setOnClickListener { addOption() } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageViewModel.kt index aa4d430b..8f85f7b0 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageViewModel.kt @@ -5,8 +5,8 @@ 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.R import kr.co.vividnext.sodalive.base.BaseViewModel -import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.ToastMessage import kr.co.vividnext.sodalive.mypage.auth.AuthRepository @@ -15,7 +15,6 @@ import kr.co.vividnext.sodalive.settings.ContentType import kr.co.vividnext.sodalive.settings.notice.NoticeItem import kr.co.vividnext.sodalive.settings.notice.NoticeRepository import kr.co.vividnext.sodalive.user.UserRepository -import kr.co.vividnext.sodalive.R class MyPageViewModel( private val userRepository: UserRepository, diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AddAlarmActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AddAlarmActivity.kt index 203fa5c5..7dd33f94 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AddAlarmActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AddAlarmActivity.kt @@ -11,7 +11,6 @@ import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels -import com.orhanobut.logger.Logger import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.common.Constants @@ -211,7 +210,7 @@ class AddAlarmActivity : BaseActivity( contentTitle = selectedContentTitle, contentCreatorNickname = selectedContentCreatorNickname, volume = binding.sbVolume.progress, - days = selectedDays.toList(), + days = selectedDays.toList() ) if (alarmId > 0) { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmActivity.kt index a2cc46ec..54a2393e 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmActivity.kt @@ -95,7 +95,7 @@ class AlarmActivity : BaseActivity( } private fun getContent(contentId: Long) { - contentViewModel.getAudioContentDetail(contentId, {},) + contentViewModel.getAudioContentDetail(contentId) {} } private fun bindData() { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmListActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmListActivity.kt index 43c43928..d7e8b8a2 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmListActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/AlarmListActivity.kt @@ -5,7 +5,6 @@ import android.annotation.SuppressLint import android.app.AlarmManager import android.content.Context import android.content.Intent -import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings @@ -15,7 +14,7 @@ import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.annotation.RequiresApi -import androidx.core.app.NotificationManagerCompat +import androidx.core.net.toUri import androidx.recyclerview.widget.LinearLayoutManager import com.gun0912.tedpermission.PermissionListener import com.gun0912.tedpermission.normal.TedPermission @@ -28,7 +27,6 @@ import kr.co.vividnext.sodalive.databinding.ActivityAlarmListBinding import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.mypage.alarm.db.Alarm import org.koin.android.ext.android.inject -import androidx.core.net.toUri class AlarmListActivity : BaseActivity( ActivityAlarmListBinding::inflate @@ -156,7 +154,6 @@ class AlarmListActivity : BaseActivity( binding.tvEmptyAlarms.visibility = if (it.isEmpty()) View.VISIBLE else View.GONE - viewModel.getSlotQuantityAndPrice() } } @@ -236,7 +233,7 @@ class AlarmListActivity : BaseActivity( ).show() adapterRefresh() }, - cancelButtonTitle = getString(R.string.cancel), + cancelButtonTitle = getString(R.string.cancel) ).show(screenWidth) } @@ -248,7 +245,7 @@ class AlarmListActivity : BaseActivity( desc = getString(R.string.alarm_list_buy_desc), confirmButtonTitle = getString(R.string.alarm_list_buy_confirm), confirmButtonClick = { viewModel.buyExtraSlot() }, - cancelButtonTitle = getString(R.string.cancel), + cancelButtonTitle = getString(R.string.cancel) ).show(screenWidth) } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentAdapter.kt index 98d9195a..9d29b24d 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/alarm/select_audio_content/AlarmSelectAudioContentAdapter.kt @@ -1,14 +1,12 @@ package kr.co.vividnext.sodalive.mypage.alarm.select_audio_content import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import coil.load import coil.transform.RoundedCornersTransformation import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListItem -import kr.co.vividnext.sodalive.audio_content.order.OrderType import kr.co.vividnext.sodalive.databinding.ItemAudioContentOrderListBinding import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.moneyFormat diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/auth/BootpayResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/auth/BootpayResponse.kt index 43e274ef..ad8d3a68 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/auth/BootpayResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/auth/BootpayResponse.kt @@ -15,6 +15,6 @@ data class BootpayResponse( @SerializedName("receipt_id") val receiptId: String, @SerializedName("order_id") - val orderId: String, + val orderId: String ) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/block/BlockMemberActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/block/BlockMemberActivity.kt index 911dd513..cc1128a9 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/block/BlockMemberActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/block/BlockMemberActivity.kt @@ -98,12 +98,10 @@ class BlockMemberActivity : BaseActivity( } viewModel.blockedMemberTotalCountLiveData.observe(this) { - binding.tvTotalCount.text = " ${ - getString( - R.string.screen_block_member_total_count, - it - ) - } " + binding.tvTotalCount.text = getString( + R.string.screen_block_member_total_count, + it + ) if (it > 0) { binding.tvNone.visibility = View.GONE diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/charge/iap/CanChargeIapViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/charge/iap/CanChargeIapViewModel.kt index 8f887fde..f73c8389 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/charge/iap/CanChargeIapViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/charge/iap/CanChargeIapViewModel.kt @@ -7,10 +7,10 @@ import com.android.billingclient.api.Purchase 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.ToastMessage -import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.common.ToastMessage import kr.co.vividnext.sodalive.mypage.can.CanRepository class CanChargeIapViewModel(private val repository: CanRepository) : BaseViewModel() { @@ -38,8 +38,8 @@ class CanChargeIapViewModel(private val repository: CanRepository) : BaseViewMod title = title, chargeCan = selectedProductDetails.description.toInt(), price = ( - selectedProductDetails.oneTimePurchaseOfferDetails?.priceAmountMicros - ?: 0L).toDouble() / 1000000, + selectedProductDetails.oneTimePurchaseOfferDetails?.priceAmountMicros ?: 0L + ).toDouble() / 1000000, currencyCode = selectedProductDetails.oneTimePurchaseOfferDetails?.priceCurrencyCode ?: "KRW", productId = purchase.products[0], diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/coupon/CanCouponActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/coupon/CanCouponActivity.kt index cda5cb92..4c8bd53b 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/coupon/CanCouponActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/coupon/CanCouponActivity.kt @@ -3,13 +3,12 @@ package kr.co.vividnext.sodalive.mypage.can.coupon import android.os.Bundle import android.text.InputFilter import android.widget.Toast -import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.databinding.ActivityCanCouponBinding import org.koin.android.ext.android.inject - class CanCouponActivity : BaseActivity( ActivityCanCouponBinding::inflate ) { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/coupon/CanCouponViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/coupon/CanCouponViewModel.kt index 062860a9..224ac099 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/coupon/CanCouponViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/coupon/CanCouponViewModel.kt @@ -5,10 +5,10 @@ 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.ToastMessage -import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.common.ToastMessage import kr.co.vividnext.sodalive.mypage.can.CanRepository class CanCouponViewModel(private val repository: CanRepository) : BaseViewModel() { @@ -49,7 +49,6 @@ class CanCouponViewModel(private val repository: CanRepository) : BaseViewModel( _toastLiveData.value = it.message?.let { message -> ToastMessage(message = message) } ?: ToastMessage(resId = R.string.msg_can_coupon_unknown_error) - } ) ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/payment/PaymentGateway.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/payment/PaymentGateway.kt index cd164f5f..c7355071 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/payment/PaymentGateway.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/payment/PaymentGateway.kt @@ -3,6 +3,9 @@ package kr.co.vividnext.sodalive.mypage.can.payment import com.google.gson.annotations.SerializedName enum class PaymentGateway { - @SerializedName("PG") PG, - @SerializedName("GOOGLE_IAP") GOOGLE_IAP + @SerializedName("PG") + PG, + + @SerializedName("GOOGLE_IAP") + GOOGLE_IAP } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateActivity.kt index 81199311..f3d3beac 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateActivity.kt @@ -220,7 +220,8 @@ class ProfileUpdateActivity : BaseActivity( excludeGif = true, isEnabledFreeStyleCrop = false, config = ImagePickerCropper.Config( - aspectX = 1f, aspectY = 1f, + aspectX = 1f, + aspectY = 1f, compressFormat = Bitmap.CompressFormat.JPEG, compressQuality = 90 ), diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateViewModel.kt index 3742d81e..7d6961e6 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateViewModel.kt @@ -212,8 +212,10 @@ class ProfileUpdateViewModel(private val repository: UserRepository) : BaseViewM } ) ) - } else run { - onSuccess() + } else { + run { + onSuccess() + } } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/nickname/NicknameUpdateViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/nickname/NicknameUpdateViewModel.kt index a7b68495..144d519d 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/nickname/NicknameUpdateViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/nickname/NicknameUpdateViewModel.kt @@ -68,28 +68,28 @@ class NicknameUpdateViewModel(private val repository: UserRepository) : BaseView .observeOn(AndroidSchedulers.mainThread()) .subscribe( { - _isLoading.value = false - if (it.success) { - isCheckedNickname = true - _toastLiveData.postValue( - ToastMessage(resId = R.string.msg_nickname_update_available) - ) - } else { - _toastLiveData.postValue( - it.message?.let { message -> - ToastMessage(message = message) - } ?: ToastMessage(resId = R.string.common_error_unknown) - ) + _isLoading.value = false + if (it.success) { + isCheckedNickname = true + _toastLiveData.postValue( + ToastMessage(resId = R.string.msg_nickname_update_available) + ) + } else { + _toastLiveData.postValue( + it.message?.let { message -> + ToastMessage(message = message) + } ?: ToastMessage(resId = R.string.common_error_unknown) + ) + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue(ToastMessage(resId = R.string.common_error_unknown)) } - }, - { - _isLoading.value = false - it.message?.let { message -> Logger.e(message) } - _toastLiveData.postValue(ToastMessage(resId = R.string.common_error_unknown)) - } - ) - ) - } else { + ) + ) + } else { _toastLiveData.postValue( ToastMessage(resId = R.string.msg_nickname_update_input_required) ) @@ -111,29 +111,29 @@ class NicknameUpdateViewModel(private val repository: UserRepository) : BaseView .observeOn(AndroidSchedulers.mainThread()) .subscribe( { - _isLoading.value = false - if (it.success) { - _toastLiveData.postValue( - ToastMessage(resId = R.string.msg_nickname_update_success) - ) - SharedPreferenceManager.nickname = nickname - onSuccess() - } else { - _toastLiveData.postValue( - it.message?.let { message -> - ToastMessage(message = message) - } ?: ToastMessage(resId = R.string.common_error_unknown) - ) + _isLoading.value = false + if (it.success) { + _toastLiveData.postValue( + ToastMessage(resId = R.string.msg_nickname_update_success) + ) + SharedPreferenceManager.nickname = nickname + onSuccess() + } else { + _toastLiveData.postValue( + it.message?.let { message -> + ToastMessage(message = message) + } ?: ToastMessage(resId = R.string.common_error_unknown) + ) + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue(ToastMessage(resId = R.string.common_error_unknown)) } - }, - { - _isLoading.value = false - it.message?.let { message -> Logger.e(message) } - _toastLiveData.postValue(ToastMessage(resId = R.string.common_error_unknown)) - } - ) - ) - } else { + ) + ) + } else { _toastLiveData.postValue( ToastMessage(resId = R.string.msg_nickname_update_check_required) ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/recent/db/RecentContentDao.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/recent/db/RecentContentDao.kt index d01126a2..68cbf54b 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/recent/db/RecentContentDao.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/recent/db/RecentContentDao.kt @@ -20,7 +20,9 @@ interface RecentContentDao { @Query("SELECT COUNT(*) FROM recent_contents") fun getCount(): Flow - @Query("DELETE FROM recent_contents WHERE contentId NOT IN (SELECT contentId FROM recent_contents ORDER BY listenedAt DESC LIMIT :limit)") + @Query( + "DELETE FROM recent_contents WHERE contentId NOT IN (SELECT contentId FROM recent_contents ORDER BY listenedAt DESC LIMIT :limit)" + ) suspend fun keepMostRecent(limit: Int) @Query("DELETE FROM recent_contents") diff --git a/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRequest.kt index 6365aaa6..367496d2 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRequest.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRequest.kt @@ -1,8 +1,7 @@ package kr.co.vividnext.sodalive.report -import com.google.gson.annotations.SerializedName - import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName @Keep data class ReportRequest( @@ -11,18 +10,22 @@ data class ReportRequest( @SerializedName("reportedMemberId") val reportedMemberId: Long? = null, @SerializedName("cheersId") val cheersId: Long? = null, @SerializedName("audioContentId") val contentId: Long? = null, - @SerializedName("communityPostId") val communityPostId: Long? = null, + @SerializedName("communityPostId") val communityPostId: Long? = null ) enum class ReportType { @SerializedName("PROFILE") PROFILE, + @SerializedName("USER") USER, + @SerializedName("CHEERS") CHEERS, + @SerializedName("AUDIO_CONTENT") AUDIO_CONTENT, + @SerializedName("COMMUNITY_POST") COMMUNITY_POST } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/search/SearchResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/search/SearchResponse.kt index b612ceba..71efa851 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/search/SearchResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/search/SearchResponse.kt @@ -27,7 +27,12 @@ data class SearchResponseItem( @Keep enum class SearchResponseType { - @SerializedName("CREATOR") CREATOR, - @SerializedName("CONTENT") CONTENT, - @SerializedName("SERIES") SERIES + @SerializedName("CREATOR") + CREATOR, + + @SerializedName("CONTENT") + CONTENT, + + @SerializedName("SERIES") + SERIES } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/settings/signout/SignOutViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/settings/signout/SignOutViewModel.kt index 743d15fa..5b24d81c 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/settings/signout/SignOutViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/settings/signout/SignOutViewModel.kt @@ -7,8 +7,8 @@ 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.BaseViewModel -import kr.co.vividnext.sodalive.common.ToastMessage import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.common.ToastMessage import kr.co.vividnext.sodalive.user.UserRepository class SignOutViewModel(private val repository: UserRepository) : BaseViewModel() { @@ -42,29 +42,29 @@ class SignOutViewModel(private val repository: UserRepository) : BaseViewModel() .observeOn(AndroidSchedulers.mainThread()) .subscribe( { - _isLoading.value = false + _isLoading.value = false - if (it.message != null) { - _toastLiveData.postValue(ToastMessage(message = it.message)) - } else { - _toastLiveData.postValue( - ToastMessage(resId = R.string.common_error_unknown) - ) - } + if (it.message != null) { + _toastLiveData.postValue(ToastMessage(message = it.message)) + } else { + _toastLiveData.postValue( + ToastMessage(resId = R.string.common_error_unknown) + ) + } if (it.success) { SharedPreferenceManager.clear() onSuccess() } - }, - { - _isLoading.value = false - it.message?.let { message -> Logger.e(message) } - _toastLiveData.postValue( - ToastMessage(resId = R.string.common_error_unknown) - ) - } - ) + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue( + ToastMessage(resId = R.string.common_error_unknown) + ) + } + ) ) } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt index b5ddca74..8095e9dc 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt @@ -15,7 +15,6 @@ import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveRespon import kr.co.vividnext.sodalive.v2.creator.channel.live.model.toReplayUiModel import kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelAudioContentAdapter import kr.co.vividnext.sodalive.v2.creator.channel.model.toLabelResId -import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelLiveDateTime import kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelSortPopup import org.koin.androidx.viewmodel.ext.android.viewModel @@ -173,18 +172,7 @@ class CreatorChannelLiveFragment : BaseFragment 0) { - live.price.moneyFormat() - } else { - getString(R.string.audio_content_tag_free) - } - layoutCreatorChannelLiveCurrentPrice.isVisible = true - ivCreatorChannelLiveCurrentPriceCash.isVisible = live.price > 0 - layoutCreatorChannelLiveCurrentCard.setOnClickListener { - host.onCreatorChannelCurrentLiveClicked(live) - } + layoutCreatorChannelLiveCurrentCard.bind(live, host::onCreatorChannelCurrentLiveClicked) } interface Host { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveView.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveView.kt new file mode 100644 index 00000000..e21d15f9 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveView.kt @@ -0,0 +1,74 @@ +package kr.co.vividnext.sodalive.v2.creator.channel.ui + +import android.content.Context +import android.graphics.Outline +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewOutlineProvider +import android.widget.FrameLayout +import androidx.core.view.isVisible +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ViewCreatorChannelCurrentLiveBinding +import kr.co.vividnext.sodalive.extensions.moneyFormat +import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse + +class CreatorChannelCurrentLiveView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + private val binding = ViewCreatorChannelCurrentLiveBinding.inflate( + LayoutInflater.from(context), + this, + true + ) + + init { + binding.root.clipToOutline = true + binding.root.outlineProvider = object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect(0, 0, view.width, view.height, view.height / 2f) + } + } + } + + fun bind( + live: CreatorChannelLiveResponse, + onClick: (CreatorChannelLiveResponse) -> Unit + ) = with(binding) { + val presentation = createCreatorChannelCurrentLivePresentation( + price = live.price, + isAdult = live.isAdult, + freeText = context.getString(R.string.audio_content_tag_free), + priceFormatter = Int::moneyFormat + ) + tvCurrentLiveTitle.text = live.title + tvCurrentLiveStartTime.text = formatCreatorChannelLiveTime(live.beginDateTimeUtc) + tvCurrentLivePrice.text = presentation.priceText + ivCurrentLivePrice.isVisible = presentation.showPriceIcon + ivCurrentLiveAdult.isVisible = presentation.showAdultBadge + setOnClickListener { onClick(live) } + } +} + +internal data class CreatorChannelCurrentLivePresentation( + val priceText: String, + val showPriceIcon: Boolean, + val showAdultBadge: Boolean +) + +internal fun createCreatorChannelCurrentLivePresentation( + price: Int, + isAdult: Boolean, + freeText: String, + priceFormatter: (Int) -> String +): CreatorChannelCurrentLivePresentation { + val isPaid = price > 0 + return CreatorChannelCurrentLivePresentation( + priceText = if (isPaid) priceFormatter(price) else freeText, + showPriceIcon = isPaid, + showAdultBadge = isAdult + ) +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt index 7ad501c3..c13c3c2c 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt @@ -30,6 +30,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab import kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityView import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem import kr.co.vividnext.sodalive.v2.widget.feed.FeedSize +import java.text.ParsePosition import java.text.SimpleDateFormat import java.util.Locale import java.util.TimeZone @@ -86,12 +87,7 @@ class CreatorChannelHomeSectionAdapter( ) : RecyclerView.ViewHolder(view) { private val title: TextView? = view.findViewById(R.id.tv_section_title) private val sectionTitleChevron: ImageView? = view.findViewById(R.id.iv_section_title_chevron) - private val currentLiveTitle: TextView? = view.findViewById(R.id.tv_current_live_title) - private val currentLiveStartTime: TextView? = view.findViewById(R.id.tv_current_live_start_time) - private val currentLivePrice: TextView? = view.findViewById(R.id.tv_current_live_price) - private val currentLiveAdult: TextView? = view.findViewById(R.id.tv_current_live_adult) - private val currentLivePriceLayout: View? = view.findViewById(R.id.layout_current_live_price) - private val currentLiveCard: View? = view.findViewById(R.id.layout_current_live_card) + private val currentLiveView: CreatorChannelCurrentLiveView? = view.findViewById(R.id.view_current_live) private val latestAudioThumbnail: ImageView? = view.findViewById(R.id.iv_latest_audio_thumbnail) private val latestAudioPointTag: ImageView? = view.findViewById(R.id.iv_latest_audio_point_tag) private val latestAudioTitle: TextView? = view.findViewById(R.id.tv_latest_audio_title) @@ -165,12 +161,7 @@ class CreatorChannelHomeSectionAdapter( } private fun bindCurrentLive(item: CreatorChannelHomeSection.CurrentLive) { - currentLiveTitle?.text = item.live.title - currentLiveStartTime?.text = formatCreatorChannelLiveDateTime(item.live.beginDateTimeUtc) - currentLivePrice?.text = item.live.price.toString() - currentLivePriceLayout?.isVisible = item.live.price > 0 - currentLiveAdult?.isVisible = item.live.isAdult - currentLiveCard?.setOnClickListener { onLiveClick(item.live) } + currentLiveView?.bind(item.live, onLiveClick) } private fun bindLatestAudioContent(item: CreatorChannelHomeSection.LatestAudioContent) { @@ -660,11 +651,11 @@ internal fun formatCreatorChannelScheduleTime( locale: Locale = Locale.getDefault() ): String = formatCreatorChannelScheduleUtc(scheduledAtUtc, "a hh:mm", timeZone, locale) -internal fun formatCreatorChannelLiveDateTime( +internal fun formatCreatorChannelLiveTime( beginDateTimeUtc: String, timeZone: TimeZone = TimeZone.getDefault(), locale: Locale = Locale.getDefault() -): String = formatCreatorChannelUtcOrNull(beginDateTimeUtc, "yyyy.MM.dd HH:mm:ss", timeZone, locale).orEmpty() +): String = formatCreatorChannelUtcOrNull(beginDateTimeUtc, "HH:mm", timeZone, locale).orEmpty() internal fun formatCreatorChannelDebutActivityValue( debutDateUtc: String?, @@ -697,10 +688,12 @@ private fun formatCreatorChannelUtcOrNull( timeZone: TimeZone, locale: Locale ): String? { - val date = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { + val parser = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { this.timeZone = TimeZone.getTimeZone("UTC") isLenient = false - }.runCatching { parse(utc) }.getOrNull() ?: return null + } + val position = ParsePosition(0) + val date = parser.parse(utc, position)?.takeIf { position.index == utc.length } ?: return null return SimpleDateFormat(pattern, locale).apply { this.timeZone = timeZone }.format(date) } diff --git a/app/src/main/res/drawable/bg_creator_channel_live_current.xml b/app/src/main/res/drawable/bg_creator_channel_live_current.xml deleted file mode 100644 index 9b292fe5..00000000 --- a/app/src/main/res/drawable/bg_creator_channel_live_current.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/bg_creator_channel_live_price.xml b/app/src/main/res/drawable/bg_creator_channel_live_price.xml deleted file mode 100644 index ad74df8e..00000000 --- a/app/src/main/res/drawable/bg_creator_channel_live_price.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/app/src/main/res/layout/fragment_creator_channel_live.xml b/app/src/main/res/layout/fragment_creator_channel_live.xml index d1428ebf..aba64070 100644 --- a/app/src/main/res/layout/fragment_creator_channel_live.xml +++ b/app/src/main/res/layout/fragment_creator_channel_live.xml @@ -63,122 +63,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + app:layout_constraintTop_toBottomOf="@id/layout_creator_channel_live_sort_bar" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="78dp" /> diff --git a/app/src/main/res/layout/view_creator_channel_current_live.xml b/app/src/main/res/layout/view_creator_channel_current_live.xml new file mode 100644 index 00000000..e7df3f6c --- /dev/null +++ b/app/src/main/res/layout/view_creator_channel_current_live.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt index cd46d1c3..7dacbb69 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt @@ -11,7 +11,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelSer import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelSeriesCardWidthDp import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelScheduleTimelineLineCount import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelDebutActivityValue -import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelLiveDateTime +import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelLiveTime import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleDate import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleDayOfWeek import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleTime @@ -929,31 +929,22 @@ class CreatorChannelActivitySourceTest { } @Test - fun `현재 라이브 섹션은 전용 layout과 bind로 렌더링한다`() { + fun `현재 라이브 섹션은 공통 View로 렌더링한다`() { val adapter = projectFile( "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" ).readText() val liveLayout = projectFile("app/src/main/res/layout/item_creator_channel_home_live.xml").readText() - assertTrue(liveLayout.contains("@+id/layout_current_live_card")) - assertTrue(liveLayout.contains("@+id/tv_current_live_title")) - assertTrue(liveLayout.contains("@+id/tv_current_live_start_time")) - assertTrue(liveLayout.contains("@+id/tv_current_live_price")) - assertTrue(liveLayout.contains("@+id/tv_current_live_adult")) - assertTrue(liveLayout.contains("@drawable/bg_creator_channel_current_live")) - assertTrue(liveLayout.contains("@drawable/bg_creator_channel_current_live_price")) - assertTrue(liveLayout.contains("@drawable/ic_can")) - assertTrue(liveLayout.contains("android:layout_height=\"78dp\"")) + assertTrue(liveLayout.contains("CreatorChannelCurrentLiveView")) assertFalse(liveLayout.contains("@layout/view_section_title")) assertFalse(liveLayout.contains("@+id/tv_section_title")) assertFalse(liveLayout.contains("@+id/ll_section_items")) - assertTrue(adapter.contains("private val currentLiveTitle: TextView?")) - assertTrue(adapter.contains("private val currentLiveStartTime: TextView?")) - assertTrue(adapter.contains("private val currentLivePrice: TextView?")) - assertTrue(adapter.contains("private val currentLiveAdult: TextView?")) - assertTrue(adapter.contains("currentLiveTitle?.text = item.live.title")) - assertTrue(adapter.contains("currentLivePriceLayout?.isVisible = item.live.price > 0")) - assertTrue(adapter.contains("currentLiveAdult?.isVisible = item.live.isAdult")) + assertTrue(adapter.contains("private val currentLiveView: CreatorChannelCurrentLiveView?")) + assertTrue(adapter.contains("currentLiveView?.bind(item.live, onLiveClick)")) + assertFalse(adapter.contains("private val currentLiveTitle: TextView?")) + assertFalse(adapter.contains("private val currentLiveStartTime: TextView?")) + assertFalse(adapter.contains("private val currentLivePrice: TextView?")) + assertFalse(adapter.contains("private val currentLiveAdult: TextView?")) assertFalse(adapter.contains("private fun addHeroCard")) assertFalse(adapter.contains("addHeroCard(")) } @@ -1367,11 +1358,21 @@ class CreatorChannelActivitySourceTest { } @Test - fun `현재 진행 중인 라이브 시간은 디바이스 timezone 기준 초 단위 날짜 시간으로 표시한다`() { + fun `현재 진행 중인 라이브 시간은 디바이스 timezone 기준 시분으로 표시한다`() { val timeZone = TimeZone.getTimeZone("Asia/Seoul") val locale = Locale.KOREA - assertEquals("2026.06.30 00:00:01", formatCreatorChannelLiveDateTime("2026-06-29T15:00:01Z", timeZone, locale)) + assertEquals("00:00", formatCreatorChannelLiveTime("2026-06-29T15:00:01Z", timeZone, locale)) + } + + @Test + fun `현재 진행 중인 라이브 UTC 시간이 잘못되면 빈 문자열을 표시한다`() { + assertEquals("", formatCreatorChannelLiveTime("invalid")) + } + + @Test + fun `현재 진행 중인 라이브 UTC 시간 뒤에 추가 문자가 있으면 빈 문자열을 표시한다`() { + assertEquals("", formatCreatorChannelLiveTime("2026-06-29T15:00:01Zjunk")) } @Test @@ -1601,9 +1602,8 @@ class CreatorChannelActivitySourceTest { ).readText() assertTrue(adapter.contains("private val onLiveClick: (CreatorChannelLiveResponse) -> Unit = {}")) - assertTrue(adapter.contains("private val currentLiveCard: View?")) - assertTrue(adapter.contains("currentLiveCard?.setOnClickListener")) - assertTrue(adapter.contains("onLiveClick(item.live)")) + assertTrue(adapter.contains("private val currentLiveView: CreatorChannelCurrentLiveView?")) + assertTrue(adapter.contains("currentLiveView?.bind(item.live, onLiveClick)")) assertTrue(fragment.contains("onLiveClick = ::onCurrentLiveClicked")) assertTrue(fragment.contains("private fun onCurrentLiveClicked(live: CreatorChannelLiveResponse)")) assertTrue(fragment.contains("host.onCreatorChannelCurrentLiveClicked(live)")) diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt index cdeeb034..ea36d8d7 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt @@ -42,7 +42,6 @@ class CreatorChannelLiveFragmentLayoutTest { assertSame(emptyContainer, emptyMessage.parent) assertSame(root, errorMessage.parent) assertSame(root, retryButton.parent) - assertNotNull(currentLiveCard.findViewById(R.id.tv_creator_channel_live_current_title)) assertEquals(false, replayList.clipToPadding) assertTrue(layout.contains("android:layout_height=\"wrap_content\"")) assertTrue(layout.contains("android:id=\"@+id/layout_creator_channel_live_empty\"")) @@ -64,6 +63,21 @@ class CreatorChannelLiveFragmentLayoutTest { assertFalse(layout.contains("app:layout_constraintBottom_toTopOf=\"@id/btn_creator_channel_live_retry\"")) } + @Test + fun `라이브 탭 현재 라이브는 공통 View를 사용한다`() { + val layout = projectFile("app/src/main/res/layout/fragment_creator_channel_live.xml").readText() + val fragment = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt" + ).readText() + + assertTrue(layout.contains("CreatorChannelCurrentLiveView")) + assertTrue(fragment.contains("layoutCreatorChannelLiveCurrentCard.bind(live")) + assertFalse(fragment.contains("tvCreatorChannelLiveCurrentTitle.text")) + assertFalse(fragment.contains("tvCreatorChannelLiveCurrentTime.text")) + assertFalse(fragment.contains("tvCreatorChannelLiveCurrentPrice.text")) + assertFalse(fragment.contains("ivCreatorChannelLiveCurrentPriceCash.isVisible")) + } + @Test fun `라이브 empty 상태는 추가 스크롤 영역을 만들지 않도록 minimumHeight를 사용하지 않는다`() { val fragment = projectFile( @@ -165,20 +179,14 @@ class CreatorChannelLiveFragmentLayoutTest { @Test fun `라이브 layout은 가격 영역을 유연한 너비와 bar cash icon으로 제공한다`() { - val fragmentLayout = projectFile("app/src/main/res/layout/fragment_creator_channel_live.xml").readText() val itemLayout = projectFile("app/src/main/res/layout/item_creator_channel_audio_content.xml").readText() - val root = inflateView(R.layout.fragment_creator_channel_live) val item = inflateView(R.layout.item_creator_channel_audio_content) - val currentPrice = requireNotNull(root.findViewById(R.id.layout_creator_channel_live_current_price)) - val currentPriceCash = requireNotNull(root.findViewById(R.id.iv_creator_channel_live_current_price_cash)) val replayAction = requireNotNull(item.findViewById(R.id.layout_creator_channel_audio_content_action)) val replayPlay = requireNotNull(item.findViewById(R.id.iv_creator_channel_audio_content_play)) val actionText = requireNotNull(item.findViewById(R.id.layout_creator_channel_audio_content_action_text)) val adultBadge = requireNotNull(item.findViewById(R.id.iv_creator_channel_audio_content_adult_badge)) val adultBadgeParams = adultBadge.layoutParams as ViewGroup.MarginLayoutParams - assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, currentPrice.layoutParams.width) - assertNotNull(currentPriceCash) assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, replayAction.layoutParams.width) assertEquals(dp(28), replayPlay.layoutParams.width) assertEquals(dp(28), replayPlay.layoutParams.height) @@ -186,18 +194,10 @@ class CreatorChannelLiveFragmentLayoutTest { assertEquals(dp(8), (actionText.layoutParams as ViewGroup.MarginLayoutParams).topMargin) assertEquals(dp(6), adultBadgeParams.topMargin) assertEquals(dp(6), adultBadgeParams.marginEnd) - assertTrue(fragmentLayout.contains("@drawable/ic_bar_cash")) assertTrue(itemLayout.contains("@drawable/ic_bar_cash")) assertTrue(itemLayout.contains("@drawable/bg_creator_channel_live_replay_play")) assertTrue(itemLayout.contains("android:orientation=\"vertical\"")) - assertFalse(fragmentLayout.contains("@drawable/ic_can")) assertFalse(itemLayout.contains("@drawable/ic_can")) - assertFalse( - fragmentLayout.contains( - "android:id=\"@+id/layout_creator_channel_live_current_price\"\n" + - " android:layout_width=\"60dp\"" - ) - ) assertFalse( itemLayout.contains( "android:id=\"@+id/layout_creator_channel_audio_content_action\"\n" + @@ -243,9 +243,7 @@ class CreatorChannelLiveFragmentLayoutTest { assertTrue(fragment.contains("viewModel.consumePaginationErrorMessage()")) assertTrue(fragment.contains("bindEmpty() = with(binding)")) assertTrue(fragment.contains("bindError(state: CreatorChannelLiveUiState.Error) = with(binding)")) - assertTrue(fragment.contains("formatCreatorChannelLiveDateTime(live.beginDateTimeUtc)")) - assertTrue(fragment.contains("ivCreatorChannelLiveCurrentPriceCash.isVisible = live.price > 0")) - assertTrue(fragment.contains("R.string.audio_content_tag_free")) + assertTrue(fragment.contains("layoutCreatorChannelLiveCurrentCard.bind(live")) assertTrue(fragment.contains("onCreatorChannelCurrentLiveClicked")) assertTrue(fragment.contains("onCreatorChannelLiveContentChanged")) assertFalse(fragment.contains("addOnScrollListener(object : RecyclerView.OnScrollListener()")) diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveViewModelTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveViewModelTest.kt new file mode 100644 index 00000000..53d87969 --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveViewModelTest.kt @@ -0,0 +1,88 @@ +package kr.co.vividnext.sodalive.v2.creator.channel.ui + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class CreatorChannelCurrentLiveViewModelTest { + + @Test + fun `양수 가격은 포맷된 금액과 가격 아이콘을 표시한다`() { + val presentation = createCreatorChannelCurrentLivePresentation( + price = 1200, + isAdult = false, + freeText = "무료", + priceFormatter = { "$it CAN" } + ) + + assertEquals("1200 CAN", presentation.priceText) + assertTrue(presentation.showPriceIcon) + } + + @Test + fun `0 이하 가격은 무료 문구를 표시하고 가격 아이콘을 숨긴다`() { + listOf(0, -1).forEach { price -> + val presentation = createCreatorChannelCurrentLivePresentation( + price = price, + isAdult = false, + freeText = "무료", + priceFormatter = Int::toString + ) + + assertEquals("무료", presentation.priceText) + assertFalse(presentation.showPriceIcon) + } + } + + @Test + fun `성인 여부는 성인 배지 표시 상태에 반영한다`() { + val adult = createCreatorChannelCurrentLivePresentation( + price = 0, + isAdult = true, + freeText = "무료", + priceFormatter = Int::toString + ) + val allAges = createCreatorChannelCurrentLivePresentation( + price = 0, + isAdult = false, + freeText = "무료", + priceFormatter = Int::toString + ) + + assertTrue(adult.showAdultBadge) + assertFalse(allAges.showAdultBadge) + } + + @Test + fun `유료와 무료 재바인딩 상태 전환은 표시 모델을 매번 새로 계산한다`() { + val paidAdult = createCreatorChannelCurrentLivePresentation( + price = 300, + isAdult = true, + freeText = "무료", + priceFormatter = { "$it CAN" } + ) + val freeAllAges = createCreatorChannelCurrentLivePresentation( + price = 0, + isAdult = false, + freeText = "무료", + priceFormatter = { "$it CAN" } + ) + val paidAllAges = createCreatorChannelCurrentLivePresentation( + price = 300, + isAdult = false, + freeText = "무료", + priceFormatter = { "$it CAN" } + ) + + assertEquals("300 CAN", paidAdult.priceText) + assertTrue(paidAdult.showPriceIcon) + assertTrue(paidAdult.showAdultBadge) + assertEquals("무료", freeAllAges.priceText) + assertFalse(freeAllAges.showPriceIcon) + assertFalse(freeAllAges.showAdultBadge) + assertEquals("300 CAN", paidAllAges.priceText) + assertTrue(paidAllAges.showPriceIcon) + assertFalse(paidAllAges.showAdultBadge) + } +} diff --git a/docs/20260716_크리에이터_채널_진행_중_라이브_공통_컴포넌트/plan-task.md b/docs/20260716_크리에이터_채널_진행_중_라이브_공통_컴포넌트/plan-task.md new file mode 100644 index 00000000..a678050b --- /dev/null +++ b/docs/20260716_크리에이터_채널_진행_중_라이브_공통_컴포넌트/plan-task.md @@ -0,0 +1,260 @@ +# 크리에이터 채널 진행 중 라이브 공통 컴포넌트 구현 계획 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** CreatorChannel 홈 탭과 라이브 탭의 진행 중인 라이브 UI를 하나의 공통 View로 통합하고 시간, 가격, 제목, 성인 배지 표시를 일관되게 만든다. + +**Architecture:** `CreatorChannelCurrentLiveView`가 카드 레이아웃과 바인딩을 소유하고 두 화면은 `CreatorChannelLiveResponse`와 클릭 콜백만 전달한다. 시간과 가격·성인 표시 결정은 테스트 가능한 순수 함수/표시 모델로 분리하되 단일 컴포넌트 외의 추상화는 추가하지 않는다. + +**Tech Stack:** Android XML View, Kotlin, ViewBinding, ConstraintLayout, JUnit4, Gradle + +## Global Constraints + +- 신규 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다. +- 레거시 코드는 수정하지 않는다. +- 기존 `CreatorChannelLiveResponse`, ViewModel, Repository, API, 라이브 입장 콜백을 유지한다. +- 신규 외부 의존성이나 디자인 토큰을 추가하지 않는다. +- 시간은 UTC 입력을 디바이스 타임존으로 변환해 24시간제 `HH:mm`으로 표시한다. +- `price > 0`은 `ic_bar_cash`와 금액, `price <= 0`은 아이콘 없이 `무료`를 표시한다. +- 제목은 한 줄 끝 말줄임으로 가격 영역을 침범하지 않는다. +- 성인 배지는 `18dp × 18dp`, `bg_creator_channel_live_adult_badge`, `ic_new_shield_large`를 사용하고 시간 우측 `8dp` 간격에 표시한다. +- 성인 배지는 스크린 리더가 성인 제한 상태를 알 수 있는 접근성 설명을 제공한다. +- 홈 탭과 라이브 탭의 기존 바깥 여백과 클릭 동작을 유지한다. + +--- + +## 파일 구조 + +- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveView.kt` + - 공통 카드 바인딩, 시간 변환, 가격 및 성인 표시 상태를 담당한다. +- 생성: `app/src/main/res/layout/view_creator_channel_current_live.xml` + - LIVE 배지, 시간, 성인 배지, 한 줄 제목, 가격 캡슐의 공통 레이아웃이다. +- 수정: `app/src/main/res/layout/item_creator_channel_home_live.xml` + - 홈 섹션의 외부 패딩 안에 공통 View만 배치한다. +- 수정: `app/src/main/res/layout/fragment_creator_channel_live.xml` + - 기존 중복 카드 내부 레이아웃을 공통 View로 교체한다. +- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt` + - 개별 TextView 바인딩을 공통 View의 `bind` 호출로 교체한다. +- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` + - 개별 카드 바인딩을 공통 View의 `bind` 호출로 교체한다. +- 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` + - 홈 탭 공통 View 사용과 `HH:mm` 변환 계약을 검증한다. +- 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` + - 라이브 탭 공통 View 사용과 중복 바인딩 제거를 검증한다. +- 생성: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveViewModelTest.kt` + - 가격·무료·성인 표시 상태를 순수 로직으로 검증한다. +- 수정: `docs/agent-guides/build-test-style.md` + - 신규 단위 테스트의 단일 실행 명령을 추가한다. + +--- + +### Phase 1: 공통 표시 계약을 테스트로 정의 + +- [x] **Task 1.1: 시간 포맷과 가격·성인 표시 상태의 실패 테스트를 작성한다** + - 파일: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveViewModelTest.kt` + - 인터페이스: + - `formatCreatorChannelLiveTime(beginDateTimeUtc, timeZone, locale): String` + - `CreatorChannelCurrentLivePresentation(priceText: String, showPriceIcon: Boolean, showAdultBadge: Boolean)` + - `createCreatorChannelCurrentLivePresentation(price, isAdult, freeText, priceFormatter)` + - 테스트 내용: + - `2026-06-29T15:00:01Z`를 `Asia/Seoul`로 변환하면 `00:00`이다. + - 잘못된 UTC 입력은 빈 문자열이다. + - 양수 가격은 포맷된 금액과 가격 아이콘을 표시한다. + - 0과 음수 가격은 `무료`와 숨겨진 가격 아이콘 상태다. + - `isAdult` 값이 성인 배지 표시 상태에 반영된다. + - 실행: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelCurrentLiveViewModelTest"` + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` + - 기대 결과: 신규 함수와 타입이 없어 컴파일 실패하거나 기존 날짜·초 포함 기대값 때문에 실패한다. + - 검증 기록: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelCurrentLiveViewModelTest"` 실행 시 신규 표시 생성 함수가 없어 컴파일 실패했다. + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 실행 시 신규 시간 포맷 함수가 없어 컴파일 실패했다. + +- [x] **Task 1.2: 최소 순수 로직으로 실패 테스트를 통과시킨다** + - 파일: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveView.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt` + - 구현: + - 기존 UTC 파서를 재사용해 `HH:mm`을 반환하는 `formatCreatorChannelLiveTime`을 추가한다. + - `price > 0`만 유료로 분류하고 나머지는 무료로 분류하는 표시 모델 생성 함수를 추가한다. + - 기존 `formatCreatorChannelLiveDateTime` 호출부는 이후 공통 View 연결과 함께 제거할 수 있도록 테스트 계약부터 새 함수로 전환한다. + - 실행: + - Task 1.1의 두 테스트 명령 + - 기대 결과: 시간과 표시 상태 테스트가 모두 통과한다. + - 검증 기록: + - 가격·성인 표시 테스트는 `BUILD SUCCESSFUL in 32s`로 통과했다. + - 시간 및 기존 CreatorChannel 소스 계약 테스트는 순차 재실행 후 `BUILD SUCCESSFUL in 3s`로 통과했다. + - 최초 병렬 실행 중 한 Gradle 프로세스에서 KSP 캐시 `StreamCorruptedException`이 발생해 이후 Gradle 검증은 순차 실행하기로 했다. + +### Phase 2: 공통 View 구현 및 두 화면 연결 + +- [x] **Task 2.1: 두 화면이 공통 View를 사용해야 하는 실패 소스 계약 테스트를 작성한다** + - 파일: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` + - 테스트 내용: + - `item_creator_channel_home_live.xml`과 `fragment_creator_channel_live.xml`에 `CreatorChannelCurrentLiveView`가 존재한다. + - 공통 레이아웃에 `ic_bar_cash`, `ic_new_shield_large`, `bg_creator_channel_live_adult_badge`가 존재한다. + - 공통 레이아웃의 성인 배지는 `18dp × 18dp`이며 시간 우측 `8dp` 간격이다. + - 제목은 `maxLines="1"`, `ellipsize="end"`이고 가격 영역 시작 전에 제약된다. + - 홈 어댑터와 라이브 Fragment는 공통 View의 `bind`를 호출하고 개별 TextView 바인딩을 보유하지 않는다. + - 실행: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` + - 기대 결과: 공통 View와 공통 레이아웃이 없어 실패한다. + - 검증 기록: + - 홈 소스 계약 테스트는 공통 레이아웃 파일이 없어 실패했다. + - 라이브 탭 계약 테스트는 공통 View 선언과 `bind` 호출이 없어 실패했다. + +- [x] **Task 2.2: 공통 View와 레이아웃을 최소 구현한다** + - 파일: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveView.kt` + - `app/src/main/res/layout/view_creator_channel_current_live.xml` + - 인터페이스: + - `fun bind(live: CreatorChannelLiveResponse, onClick: (CreatorChannelLiveResponse) -> Unit)` + - 구현: + - ViewBinding으로 레이아웃을 inflate한다. + - 제목, `HH:mm` 시간, 가격 표시 모델, 성인 배지 visibility를 매번 갱신한다. + - 카드 자체의 클릭 리스너가 현재 바인딩된 `live`를 전달한다. + - 카드 radius clipping은 Kotlin에서 `clipToOutline`과 `ViewOutlineProvider.setRoundRect`를 함께 설정한다. + - 실행: + - Task 2.1의 두 테스트 명령 + - 기대 결과: 공통 View 자체 계약 테스트는 통과하고 소비 화면 연결 관련 검증만 남는다. + - 검증 기록: + - `CreatorChannelCurrentLiveView`와 `view_creator_channel_current_live.xml`을 추가했다. + - 최초 실행에서 기존 라이브 레이아웃 전용 child ID를 참조하던 테스트가 컴파일 실패해 공통 레이아웃 ID 계약으로 갱신했다. + +- [x] **Task 2.3: 홈 탭과 라이브 탭을 공통 View에 연결한다** + - 파일: + - `app/src/main/res/layout/item_creator_channel_home_live.xml` + - `app/src/main/res/layout/fragment_creator_channel_live.xml` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` + - 구현: + - 홈 item은 기존 `20dp` 수평 패딩과 상단 패딩 안에 공통 View를 배치한다. + - 라이브 Fragment는 기존 `14dp` 수평 margin 위치에 공통 View를 배치한다. + - 두 소비자는 기존 콜백에 공통 View의 `bind`를 연결한다. + - 중복 TextView/ImageView 조회, 가격 분기, 시간 포맷 호출, 성인 배지 바인딩 코드를 제거한다. + - 실행: + - Task 2.1의 두 테스트 명령 + - 기대 결과: 두 화면의 공통 View 연결과 소스 계약 테스트가 통과한다. + - 검증 기록: + - `CreatorChannelActivitySourceTest`는 `BUILD SUCCESSFUL in 4s`로 통과했다. + - `CreatorChannelLiveFragmentLayoutTest`는 `BUILD SUCCESSFUL in 7s`로 통과했다. + - 두 화면의 기존 라이브 클릭 콜백은 공통 View의 `bind`에 그대로 전달한다. + +### Phase 3: 회귀 검증과 문서 기록 + +- [x] **Task 3.1: 신규 테스트 실행 예시를 문서에 추가한다** + - 파일: + - `docs/agent-guides/build-test-style.md` + - 내용: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelCurrentLiveViewModelTest"` + - CreatorChannel 현재 라이브 관련 테스트 묶음 실행 예시 + - 실행: + - `./gradlew tasks --all` + - 기대 결과: Gradle task 목록 조회가 성공한다. + - 검증 기록: + - sandbox 내 Gradle wrapper lock 접근이 거부되어 동일 명령을 승인된 외부 실행으로 재시도했다. + - `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 1s`로 완료되었다. + +- [x] **Task 3.2: 관련 테스트, 컴파일, 린트를 실행하고 결과를 기록한다** + - 실행: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelCurrentLiveViewModelTest"` + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` + - `./gradlew :app:compileDebugKotlin` + - `./gradlew :app:ktlintCheck` + - `git diff --check` + - 기대 결과: + - 모든 명령이 exit code 0으로 종료된다. + - 테스트 실패, Kotlin 컴파일 오류, ktlint 오류, whitespace 오류가 없다. + - 수동 확인: + - 홈 탭과 라이브 탭에서 동일한 카드가 표시된다. + - 유료는 `ic_bar_cash + 금액`, 무료는 아이콘 없이 `무료`다. + - 성인 배지가 LIVE/시간 행의 시간 우측에 표시된다. + - 긴 제목이 한 줄 말줄임되고 가격 영역을 침범하지 않는다. + - 시간은 디바이스 타임존 기준 `HH:mm`이다. + - 같은 View에 다른 라이브 데이터를 다시 바인딩한 뒤 클릭하면 최신 라이브 데이터로 입장한다. + - 검증 기록: + - 세 관련 테스트를 `--rerun-tasks`로 실행해 39개 Gradle task가 실제 재실행되었고 `BUILD SUCCESSFUL in 1m 26s`로 완료되었다. + - `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL in 1s`로 완료되었다. + - 최초 `:app:ktlintCheck`는 신규 테스트의 `=` 뒤 공백 누락 1건으로 실패했고 해당 줄만 수정했다. + - 수정 후 `./gradlew :app:ktlintCheck`는 `BUILD SUCCESSFUL in 3s`로 완료되었다. + - `git diff --check`는 출력 없이 exit code 0으로 완료되었다. + - 에뮬레이터 또는 실기기가 제공되지 않아 화면 수동 확인은 실행하지 못했다. + +### Phase 4: 리뷰 후속 수정 + +- [x] **Task 4.1: 재바인딩 관련 표시 상태 전환을 순수 로직 테스트로 검증한다** + - 파일: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveViewModelTest.kt` + - 테스트 내용: + - 유료·성인, 무료·일반, 유료·일반 상태의 표시 모델을 순수 함수로 검증한다. + - 실제 View의 `TextView`, `visibility`, `performClick()` 직접 검증은 저장소 테스트 정책과 충돌하므로 제거하고 수동 확인 대상으로 기록한다. + - 검증 기록: + - 최초 신규 테스트는 한국어 무료 문구를 직접 기대해 기본 locale 리소스와 맞지 않아 실패했고, View와 동일한 `R.string.audio_content_tag_free` 기준으로 수정했다. + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelCurrentLiveViewModelTest"`는 `BUILD SUCCESSFUL in 32s`로 완료되었다. + - 후속 리뷰 반영으로 실제 View 표현 테스트를 제거하고 순수 표시 모델 전환 테스트로 대체했다. + +- [x] **Task 4.2: 저장소 테스트 규칙과 충돌하는 XML 표현 단언을 제거한다** + - 파일: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` + - 수정 내용: + - 공통 View 연결과 adapter `bind` 호출 계약은 유지한다. + - 공통 View의 크기, margin, drawable, maxLines, ellipsize, constraint 같은 표현 속성 문자열 단언은 제거한다. + - 검증 기록: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 포함 세 관련 테스트 묶음은 `BUILD SUCCESSFUL in 14s`로 완료되었다. + - 후속 리뷰 반영으로 라이브 탭 테스트에 남아 있던 공통 View 가격 영역 너비와 drawable 단언도 제거했다. + +- [x] **Task 4.3: 가격 영역의 불필요한 XML 부모를 제거한다** + - 파일: + - `app/src/main/res/layout/view_creator_channel_current_live.xml` + - 수정 내용: + - `layout_current_live_price`의 `FrameLayout` 부모를 제거하고 가격 캡슐 `LinearLayout`이 직접 constraint와 background를 갖도록 단순화한다. + - 검증 기록: + - `./gradlew :app:lintDebug`는 기존 `AndroidManifest.xml`의 `com.facebook.FacebookActivity` `MissingClass` 등으로 실패했지만, lint 결과에서 `view_creator_channel_current_live.xml` 및 `layout_current_live_price` 관련 `UselessParent` 항목은 검출되지 않았다. + - `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL in 1s`로 완료되었다. + - `./gradlew :app:runKtlintCheckOverTestSourceSet`는 `BUILD SUCCESSFUL in 1s`로 완료되었다. + - `git diff --check`는 출력 없이 exit code 0으로 완료되었다. + +- [x] **Task 4.4: 성인 배지 접근성 요구사항과 검증 기록을 문서화한다** + - 파일: + - `docs/20260716_크리에이터_채널_진행_중_라이브_공통_컴포넌트/prd.md` + - `docs/20260716_크리에이터_채널_진행_중_라이브_공통_컴포넌트/plan-task.md` + - 수정 내용: + - 성인 배지가 스크린 리더용 접근성 설명을 제공해야 한다는 요구사항을 추가한다. + - 접근성 설명 상태는 수동 확인 항목에 포함한다. + - 검증 기록: + - `view_creator_channel_current_live.xml`의 `iv_current_live_adult`는 `@string/screen_live_room_age_limit_adult`를 `contentDescription`으로 사용한다. + +- [x] **Task 4.5: UTC 파서 suffix 거부와 최신 검증 기록을 보완한다** + - 파일: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` + - `docs/20260716_크리에이터_채널_진행_중_라이브_공통_컴포넌트/prd.md` + - `docs/20260716_크리에이터_채널_진행_중_라이브_공통_컴포넌트/plan-task.md` + - 수정 내용: + - UTC 파서가 `ParsePosition`으로 입력 문자열 전체 소비 여부를 확인해 뒤에 추가 문자가 붙은 값을 거부한다. + - `beginDateTimeUtc` 뒤에 suffix가 붙으면 빈 문자열을 표시하는 회귀 테스트를 추가한다. + - 재바인딩 후 클릭 시 최신 라이브 데이터가 전달되어야 한다는 수동 확인 항목을 추가한다. + - 검증 기록: + - RED: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest.현재 진행 중인 라이브 UTC 시간 뒤에 추가 문자가 있으면 빈 문자열을 표시한다"`는 `ComparisonFailure`로 실패해 suffix 입력이 정상 시간으로 표시되는 문제를 재현했다. + - GREEN: 유효 UTC, invalid UTC, suffix UTC focused 테스트 3건은 `BUILD SUCCESSFUL in 18s`로 통과했다. + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`는 `BUILD SUCCESSFUL in 2s`로 통과했다. + - `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL in 1s`로 통과했다. + - `./gradlew :app:ktlintCheck --rerun-tasks`는 기존 main source set ktlint 위반 550건으로 실패했다. 보고서에는 이번 변경 main 파일인 `CreatorChannelHomeSectionAdapter.kt` 항목이 없었다. + - `./gradlew :app:runKtlintCheckOverTestSourceSet --rerun-tasks`는 기존 `.editorconfig disabled_rules` deprecation 경고만 출력하고 `BUILD SUCCESSFUL in 7s`로 통과했다. + - `git diff --check`는 출력 없이 exit code 0으로 완료되었다. + +## Verification Log + +- 구현 전 계획 작성 완료. 검증 결과는 각 Task와 이 로그에 기존 기록을 유지한 채 누적한다. +- 2026-07-16: 홈 탭과 라이브 탭 공통 View 연결, UTC `HH:mm`, 무료/유료, 성인 배지, 긴 제목 제약 관련 자동 검증을 완료했다. +- 2026-07-16: 기존 프로젝트의 Gradle/ktlint deprecation 경고는 남아 있으나 이번 변경과 직접 관련된 오류는 확인되지 않았다. +- 2026-07-16: 리뷰 후속으로 재바인딩 관련 순수 표시 모델 테스트를 추가하고, 공통 View의 UI 표현 속성 문자열 테스트를 제거했으며, 가격 영역의 불필요한 XML 부모를 제거했다. +- 2026-07-16: 성인 배지 접근성 설명 요구사항과 수동 확인 항목을 PRD 및 계획 문서에 반영했다. +- 2026-07-16: UTC 파서 suffix 거부 테스트와 `ParsePosition` 전체 소비 검사를 추가했다. 캐시 없이 재실행한 `:app:ktlintCheck`는 기존 main source set 위반 550건으로 실패했으며, 이번 변경 main/test Kotlin 파일의 신규 ktlint 위반은 확인되지 않았다. diff --git a/docs/20260716_크리에이터_채널_진행_중_라이브_공통_컴포넌트/prd.md b/docs/20260716_크리에이터_채널_진행_중_라이브_공통_컴포넌트/prd.md new file mode 100644 index 00000000..f5f62cc1 --- /dev/null +++ b/docs/20260716_크리에이터_채널_진행_중_라이브_공통_컴포넌트/prd.md @@ -0,0 +1,184 @@ +# PRD: 크리에이터 채널 진행 중 라이브 공통 컴포넌트 + +## 1. Overview + +CreatorChannel 홈 탭과 라이브 탭 최상단의 진행 중인 라이브 UI를 하나의 공통 컴포넌트로 통합한다. + +--- + +## 2. Problem + +- 홈 탭과 라이브 탭이 같은 진행 중인 라이브 정보를 서로 다른 레이아웃과 바인딩 코드로 표시한다. +- 두 화면의 시간, 가격, 무료 상태, 성인 배지 표현이 일치하지 않는다. +- UTC 시작 시간이 날짜와 초까지 포함된 형태로 표시되어 사용자가 현재 디바이스 기준 시작 시간을 빠르게 파악하기 어렵다. +- 홈 탭의 제목 영역이 고정 너비여서 화면 크기나 긴 제목에 안전하게 대응하지 못한다. +- 홈 탭은 가격 아이콘으로 `ic_can`을 사용하고 무료 라이브에서는 가격 영역 전체를 숨긴다. +- 홈 탭의 성인 배지는 텍스트 형태로 카드 우측 하단에 있으며 라이브 탭에는 성인 배지가 없다. + +--- + +## 3. Goals + +- 홈 탭과 라이브 탭이 동일한 진행 중인 라이브 컴포넌트를 사용한다. +- UTC 시작 시간을 디바이스 타임존으로 변환해 `HH:mm` 형식으로 표시한다. +- 제목을 한 줄 말줄임 처리하고 가격 영역을 침범하지 않도록 한다. +- 유료 라이브는 `ic_bar_cash` 아이콘과 가격을 표시한다. +- `price <= 0`인 라이브는 아이콘 없이 `무료`를 표시한다. +- 성인 라이브는 LIVE 배지 및 시간과 같은 행에 지정된 성인 배지를 표시한다. +- 기존 라이브 입장 흐름과 각 탭의 데이터 로딩 동작을 유지한다. + +--- + +## 4. Non-Goals + +- CreatorChannel API 응답 모델 또는 서버 스키마를 변경하지 않는다. +- 진행 중인 라이브 외 다른 콘텐츠 카드 UI를 변경하지 않는다. +- 라이브 입장, 결제, 본인 인증 또는 연령 제한 로직을 변경하지 않는다. +- CreatorChannel 홈/라이브 탭의 전체 구조를 리팩터링하지 않는다. +- 신규 디자인 토큰이나 외부 라이브러리를 추가하지 않는다. + +--- + +## 5. Target Users + +- CreatorChannel 홈 탭에서 현재 라이브를 확인하는 사용자 +- CreatorChannel 라이브 탭에서 현재 라이브와 다시 듣기 콘텐츠를 확인하는 사용자 + +--- + +## 6. User Stories + +- 사용자는 어느 탭에서 보더라도 동일한 형태의 진행 중인 라이브 정보를 확인하고 싶다. +- 사용자는 라이브 시작 시간을 자신의 디바이스 타임존 기준 `HH:mm`으로 확인하고 싶다. +- 사용자는 라이브가 무료인지 유료인지 명확히 확인하고 싶다. +- 사용자는 긴 라이브 제목이 가격 정보를 가리지 않는 상태로 확인하고 싶다. +- 사용자는 성인 라이브 여부를 LIVE 배지 옆 정보 행에서 즉시 확인하고 싶다. +- 사용자는 진행 중인 라이브 카드를 눌러 기존과 동일한 방식으로 라이브에 입장하고 싶다. + +--- + +## 7. Core Features + +### Feature A: 진행 중인 라이브 공통 View + +홈 탭과 라이브 탭이 함께 사용하는 `CreatorChannelCurrentLiveView`를 `kr.co.vividnext.sodalive.v2.creator.channel.ui` 하위에 추가한다. + +#### Requirements + +- 카드 내부 레이아웃과 표시 로직을 공통 View가 소유한다. +- 공통 View는 `CreatorChannelLiveResponse`와 클릭 동작을 입력받아 화면을 갱신한다. +- 홈 탭 RecyclerView item과 라이브 탭 Fragment는 각 화면에 필요한 바깥 여백만 담당한다. +- 기존 홈 탭과 라이브 탭의 라이브 클릭 콜백을 그대로 연결한다. + +#### Edge Cases + +- 동일한 View에 다른 라이브 데이터가 다시 바인딩되어도 이전 성인 배지 및 가격 아이콘 상태가 남지 않아야 한다. +- 클릭 동작이 다시 바인딩될 때 이전 데이터가 전달되지 않아야 한다. + +### Feature B: UTC 시작 시간 표시 + +`beginDateTimeUtc`를 UTC로 해석한 뒤 현재 디바이스 타임존 기준 시간으로 변환한다. + +#### Requirements + +- 출력 형식은 24시간제 `HH:mm`이다. +- 시스템 기본 타임존을 사용한다. +- 시간 포맷 결과는 홈 탭과 라이브 탭에서 동일해야 한다. +- 기존 API 입력 형식인 `yyyy-MM-dd'T'HH:mm:ss'Z'`를 지원한다. + +#### Edge Cases + +- 파싱할 수 없는 값은 기존 동작과 동일하게 빈 문자열로 표시하고 앱이 종료되지 않아야 한다. +- `yyyy-MM-dd'T'HH:mm:ss'Z'` 뒤에 추가 문자가 붙은 값은 파싱할 수 없는 값으로 처리한다. +- 날짜가 타임존 변환으로 전날 또는 다음 날이 되더라도 변환된 현지 시간만 표시한다. + +### Feature C: 제목과 가격 표시 + +제목과 가격 영역이 서로 침범하지 않도록 제약 기반 레이아웃을 사용한다. + +#### Requirements + +- 제목은 한 줄만 표시한다. +- 긴 제목은 끝 말줄임 처리한다. +- 제목 영역의 끝은 가격 영역의 시작에 제약한다. +- `price > 0`이면 `ic_bar_cash`와 포맷된 가격을 표시한다. +- `price <= 0`이면 가격 아이콘을 숨기고 `무료`를 표시한다. +- 무료와 유료 모두 가격 캡슐 영역 자체는 표시한다. + +#### Edge Cases + +- 가격 자릿수가 길어져도 제목이 가격 영역 아래나 위로 겹치지 않아야 한다. +- 유료 상태에서 무료 상태로 다시 바인딩하면 가격 아이콘이 숨겨져야 한다. +- 무료 상태에서 유료 상태로 다시 바인딩하면 가격 아이콘이 다시 표시되어야 한다. + +### Feature D: 성인 배지 + +성인 라이브 여부를 LIVE 배지 및 시간과 같은 정보 행에 아이콘형 배지로 표시한다. + +#### Requirements + +- 정보 행의 순서는 `LIVE 배지 → 시간 → 성인 배지`다. +- LIVE 배지와 시간 사이, 시간과 성인 배지 사이에는 동일한 `8dp` 간격을 사용한다. +- 성인 배지는 LIVE 배지 높이와 같은 `18dp × 18dp` 정사각형으로 표시한다. +- 배경은 기존 `bg_creator_channel_live_adult_badge`를 사용한다. +- 아이콘은 `ic_new_shield_large`를 사용한다. +- `isAdult == true`일 때만 성인 배지를 표시한다. +- 성인 배지는 스크린 리더가 성인 제한 상태를 알 수 있는 접근성 설명을 제공한다. + +#### Edge Cases + +- 성인 라이브에서 일반 라이브로 다시 바인딩하면 배지가 숨겨져야 한다. +- 시간이 비어 있어도 성인 배지는 정보 행의 정해진 순서를 유지한다. + +--- + +## 8. UX / UI Expectations + +- 기준 디자인: + - 진행 중인 라이브 컴포넌트: Figma node `290:8950` + - 성인 배지 형태: Figma node `567:18346` +- 카드 높이는 현재 두 화면에서 사용하는 `78dp`를 유지한다. +- LIVE 배지 높이는 `18dp`를 유지한다. +- 성인 배지는 LIVE 배지와 같은 높이인 `18dp`를 사용한다. +- 성인 배지는 `bg_creator_channel_live_adult_badge`와 `ic_new_shield_large` 조합으로 표시한다. +- 제목은 가격 영역 전까지 가변 너비를 사용하고 한 줄 말줄임 처리한다. +- 화면별 카드 바깥 여백은 기존 홈 탭과 라이브 탭의 배치를 유지한다. + +--- + +## 9. Technical Constraints + +- 신규 공통 View와 연결 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다. +- 레거시 코드는 수정하지 않는다. +- 기존 `CreatorChannelLiveResponse`를 재사용한다. +- 기존 drawable, color, typography, spacing 리소스를 우선 재사용한다. +- ViewBinding을 사용해 공통 View 내부 요소를 참조한다. +- 시간 변환은 현재 프로젝트에서 사용하는 Java 날짜/시간 API 범위 안에서 구현하며 신규 의존성을 추가하지 않는다. +- 기존 홈 탭과 라이브 탭의 ViewModel, Repository, API는 변경하지 않는다. +- 기존 라이브 입장 콜백 계약을 유지한다. + +--- + +## 10. Verification + +- 시간 포맷 단위 테스트: + - UTC 입력이 `Asia/Seoul`에서 다음 날 `HH:mm`로 변환되는지 확인한다. + - 파싱할 수 없는 입력이 빈 문자열을 반환하는지 확인한다. +- 표시 로직 단위 테스트: + - `price > 0`이면 가격 아이콘과 금액을 표시하는 상태인지 확인한다. + - `price <= 0`이면 가격 아이콘 없이 `무료` 상태인지 확인한다. + - `isAdult`에 따라 성인 배지 표시 상태가 결정되는지 확인한다. + - 유료·성인, 무료·일반, 유료·일반 상태 전환에 필요한 표시 모델이 매번 새로 계산되는지 확인한다. +- 소스 계약 테스트: + - 홈 탭과 라이브 탭이 모두 `CreatorChannelCurrentLiveView`를 사용하는지 확인한다. + - 홈 탭과 라이브 탭이 기존 클릭 콜백을 공통 View의 `bind`에 연결하는지 확인한다. +- 회귀 검증: + - 관련 단위 테스트를 실행한다. + - `:app:compileDebugKotlin`과 `:app:ktlintCheck`를 실행한다. + - 가능하면 홈 탭과 라이브 탭에서 유료, 무료, 성인, 긴 제목, 접근성 설명 상태를 수동 확인한다. + +--- + +## 11. Open Questions + +- 없음. 사용자 승인 내용과 기존 리소스 조사 결과로 구현 범위가 확정되었다. diff --git a/docs/agent-guides/build-test-style.md b/docs/agent-guides/build-test-style.md index 1f90ab40..9ad20d42 100644 --- a/docs/agent-guides/build-test-style.md +++ b/docs/agent-guides/build-test-style.md @@ -102,6 +102,13 @@ Creator/Community Action Phase 테스트 예시: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.community.action.*" ``` +CreatorChannel 현재 라이브 공통 컴포넌트 테스트 예시: +```bash +./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelCurrentLiveViewModelTest" +./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" +./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest" +``` + Chat Action Phase 테스트 예시: ```bash ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.chat.action.*"