From de351d700c450593497ae181b42ed40496562144 Mon Sep 17 00:00:00 2001 From: klaus Date: Tue, 16 Jun 2026 19:22:21 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator):=20=EC=B1=84=EB=84=90=20=ED=9B=84?= =?UTF-8?q?=EC=9B=90=20=EC=9A=94=EC=B2=AD=EC=9D=84=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/co/vividnext/sodalive/di/AppDI.kt | 10 ++- .../channel/CreatorChannelHomeViewModel.kt | 35 ++++++++++ .../data/CreatorChannelHomeRepository.kt | 21 +++++- .../CreatorChannelHomeViewModelTest.kt | 67 +++++++++++++++++++ 4 files changed, 131 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt index 78b542b4..4c31ff24 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt @@ -463,7 +463,15 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { factory { DmChatRepository(api = get(), realtimeClient = get()) } factory { HomeCreatorRankingRepository(get()) } factory { HomeRecommendationRepository(get()) } - factory { CreatorChannelHomeRepository(api = get(), userRepository = get(), talkApi = get(), reportRepository = get()) } + factory { + CreatorChannelHomeRepository( + api = get(), + userRepository = get(), + talkApi = get(), + reportRepository = get(), + explorerRepository = get() + ) + } factory { CharacterTabRepository(get()) } factory { CharacterDetailRepository(get(), get()) } factory { CharacterGalleryRepository(get()) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModel.kt index 3f0d7736..0a46ded9 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModel.kt @@ -36,6 +36,7 @@ class CreatorChannelHomeViewModel( private var isFollowInProgress = false private var isCreateChatRoomInProgress = false + private var isPostChannelDonationInProgress = false fun loadHome(creatorId: Long) { if (creatorId <= 0) return @@ -131,6 +132,40 @@ class CreatorChannelHomeViewModel( ) } + fun postChannelDonation(can: Int, isSecret: Boolean, message: String) { + val content = _homeStateLiveData.value as? CreatorChannelHomeUiState.Content ?: return + if (isPostChannelDonationInProgress) return + + isPostChannelDonationInProgress = true + compositeDisposable.add( + repository.postChannelDonation( + creatorId = content.header.creatorId, + can = can, + isSecret = isSecret, + message = message, + token = authToken() + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + isPostChannelDonationInProgress = false + if (it.success) { + SharedPreferenceManager.can = (SharedPreferenceManager.can - can).coerceAtLeast(0) + loadHome(content.header.creatorId) + } else { + showUnknownErrorToast() + } + }, + { + isPostChannelDonationInProgress = false + it.message?.let { message -> Logger.e(message) } + showUnknownErrorToast() + } + ) + ) + } + fun blockUser() { val content = _homeStateLiveData.value as? CreatorChannelHomeUiState.Content ?: return diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeRepository.kt index 106e2a97..d4a08259 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeRepository.kt @@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.v2.creator.channel.data import kr.co.vividnext.sodalive.chat.talk.TalkApi import kr.co.vividnext.sodalive.chat.talk.room.CreateChatRoomRequest +import kr.co.vividnext.sodalive.explorer.ExplorerRepository +import kr.co.vividnext.sodalive.explorer.profile.channel_donation.PostChannelDonationRequest import kr.co.vividnext.sodalive.report.ReportRepository import kr.co.vividnext.sodalive.report.ReportRequest import kr.co.vividnext.sodalive.report.ReportType @@ -11,7 +13,8 @@ class CreatorChannelHomeRepository( private val api: CreatorChannelHomeApi, private val userRepository: UserRepository, private val talkApi: TalkApi, - private val reportRepository: ReportRepository + private val reportRepository: ReportRepository, + private val explorerRepository: ExplorerRepository ) { fun getHome(creatorId: Long, token: String) = api.getHome( creatorId = creatorId, @@ -35,6 +38,22 @@ class CreatorChannelHomeRepository( request = CreateChatRoomRequest(characterId) ) + fun postChannelDonation( + creatorId: Long, + can: Int, + isSecret: Boolean, + message: String, + token: String + ) = explorerRepository.postChannelDonation( + request = PostChannelDonationRequest( + creatorId = creatorId, + can = can, + isSecret = isSecret, + message = message + ), + token = token + ) + fun blockUser(userId: Long, token: String) = userRepository.memberBlock( userId = userId, token = token diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModelTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModelTest.kt index b654eec4..b3e49def 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModelTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModelTest.kt @@ -269,6 +269,73 @@ class CreatorChannelHomeViewModelTest { assertEquals(null, toastEvent?.consume()) } + @Test + fun `채널 후원 성공은 기존 후원 API를 호출하고 홈을 다시 로드한다`() { + whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null))) + whenever(repository.postChannelDonation(100L, 50, true, "응원", "Bearer test-token")).thenReturn( + Single.just(ApiResponse(true, Any(), null)) + ) + SharedPreferenceManager.can = 200 + viewModel.loadHome(100L) + + viewModel.postChannelDonation(can = 50, isSecret = true, message = "응원") + + verify(repository).postChannelDonation(100L, 50, true, "응원", "Bearer test-token") + verify(repository, times(2)).getHome(100L, "Bearer test-token") + assertEquals(150, SharedPreferenceManager.can) + } + + @Test + fun `채널 후원 성공 시 보유 can보다 큰 금액 차감은 0으로 보정한다`() { + whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null))) + whenever(repository.postChannelDonation(100L, 50, true, "응원", "Bearer test-token")).thenReturn( + Single.just(ApiResponse(true, Any(), null)) + ) + SharedPreferenceManager.can = 30 + viewModel.loadHome(100L) + + viewModel.postChannelDonation(can = 50, isSecret = true, message = "응원") + + assertEquals(0, SharedPreferenceManager.can) + } + + @Test + fun `채널 후원 진행 중 중복 요청은 무시한다`() { + val pending = SingleSubject.create>() + whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null))) + whenever(repository.postChannelDonation(100L, 50, true, "응원", "Bearer test-token")).thenReturn(pending) + viewModel.loadHome(100L) + + viewModel.postChannelDonation(can = 50, isSecret = true, message = "응원") + viewModel.postChannelDonation(can = 50, isSecret = true, message = "응원") + + verify(repository, times(1)).postChannelDonation(100L, 50, true, "응원", "Bearer test-token") + pending.onSuccess(ApiResponse(true, Any(), null)) + } + + @Test + fun `채널 후원은 홈 content가 없으면 API를 호출하지 않는다`() { + viewModel.postChannelDonation(can = 50, isSecret = false, message = "응원") + + verify(repository, never()).postChannelDonation(any(), any(), any(), any(), any()) + } + + @Test + fun `채널 후원 실패는 홈을 다시 로드하지 않고 unknown toast를 emit한다`() { + whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null))) + whenever(repository.postChannelDonation(100L, 50, false, "응원", "Bearer test-token")).thenReturn( + Single.just(ApiResponse(false, null, "failed")) + ) + viewModel.loadHome(100L) + + viewModel.postChannelDonation(can = 50, isSecret = false, message = "응원") + + verify(repository).postChannelDonation(100L, 50, false, "응원", "Bearer test-token") + verify(repository, times(1)).getHome(100L, "Bearer test-token") + val toastEvent = viewModel.toastLiveData.requireValue() + assertEquals(R.string.common_error_unknown, toastEvent?.consume()?.resId) + } + @Test fun `차단 성공은 block API를 호출하고 차단 완료 토스트를 emit한다`() { whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))