diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/Character.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/Character.kt index ce8a0727..3874152f 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/Character.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/Character.kt @@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName @Keep data class Character( - @SerializedName("id") val id: String, + @SerializedName("characterId") val characterId: Long, @SerializedName("name") val name: String, @SerializedName("description") val description: String, @SerializedName("imageUrl") val imageUrl: String diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterAdapter.kt index 869e9e6b..0d41fc55 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterAdapter.kt @@ -15,7 +15,7 @@ import kr.co.vividnext.sodalive.extensions.dpToPx class CharacterAdapter( private var characters: List = emptyList(), private val showRanking: Boolean = false, - private val onCharacterClick: (Character) -> Unit = {} + private val onCharacterClick: (Long) -> Unit = {} ) : RecyclerView.Adapter() { inner class ViewHolder( @@ -46,7 +46,7 @@ class CharacterAdapter( ) .into(binding.ivCharacter) - binding.root.setOnClickListener { onCharacterClick(character) } + binding.root.setOnClickListener { onCharacterClick(character.characterId) } } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterApi.kt index efdada04..9bd7975e 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterApi.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterApi.kt @@ -1,13 +1,21 @@ package kr.co.vividnext.sodalive.chat.character import io.reactivex.rxjava3.core.Single +import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailResponse import kr.co.vividnext.sodalive.common.ApiResponse import retrofit2.http.GET import retrofit2.http.Header +import retrofit2.http.Path interface CharacterApi { @GET("/api/chat/character/main") fun getCharacterMain( @Header("Authorization") authHeader: String ): Single> + + @GET("/api/chat/character/{characterId}") + fun getCharacterDetail( + @Header("Authorization") authHeader: String, + @Path("characterId") characterId: Long + ): Single> } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterBannerAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterBannerAdapter.kt index 3b5b700f..cfadfaa3 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterBannerAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterBannerAdapter.kt @@ -11,7 +11,6 @@ import com.bumptech.glide.request.transition.Transition import com.zhpan.bannerview.BaseBannerAdapter import com.zhpan.bannerview.BaseViewHolder import kr.co.vividnext.sodalive.R -import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse class CharacterBannerAdapter( private val context: Context, diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterTabFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterTabFragment.kt index 0aa06cfd..98efe6b6 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterTabFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterTabFragment.kt @@ -371,11 +371,11 @@ class CharacterTabFragment : BaseFragment( // TODO: 최근 대화한 캐릭터 클릭 처리 } - private fun onCharacterClick(character: Character) { + private fun onCharacterClick(characterId: Long) { if (SharedPreferenceManager.token.isNotBlank()) { startActivity( Intent(requireContext(), CharacterDetailActivity::class.java).apply { - putExtra(EXTRA_CHARACTER_ID, character.id) + putExtra(EXTRA_CHARACTER_ID, characterId) } ) } else { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterTabViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterTabViewModel.kt index 52c22286..4c2de503 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterTabViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterTabViewModel.kt @@ -5,7 +5,6 @@ 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.audio_content.main.GetAudioContentBannerResponse import kr.co.vividnext.sodalive.base.BaseViewModel import kr.co.vividnext.sodalive.chat.character.curation.CurationSection import kr.co.vividnext.sodalive.chat.character.recent.RecentCharacter diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/curation/CurationSectionAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/curation/CurationSectionAdapter.kt index 66f8925b..4fff15c2 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/curation/CurationSectionAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/curation/CurationSectionAdapter.kt @@ -8,14 +8,13 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import kr.co.vividnext.sodalive.chat.character.Character import kr.co.vividnext.sodalive.chat.character.CharacterAdapter import kr.co.vividnext.sodalive.databinding.ItemCurationSectionBinding import kr.co.vividnext.sodalive.extensions.dpToPx class CurationSectionAdapter( private var sections: List = emptyList(), - private val onCharacterClick: (Character) -> Unit = {} + private val onCharacterClick: (Long) -> Unit = {} ) : RecyclerView.Adapter() { inner class ViewHolder( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/CharacterDetailActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/CharacterDetailActivity.kt index 2496561a..26a48128 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/CharacterDetailActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/CharacterDetailActivity.kt @@ -5,20 +5,23 @@ import android.graphics.Rect import android.os.Bundle import android.text.TextUtils import android.view.View -import androidx.activity.viewModels import androidx.core.net.toUri import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import coil.load 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.ActivityCharacterDetailBinding import kr.co.vividnext.sodalive.extensions.dpToPx +import org.koin.androidx.viewmodel.ext.android.viewModel class CharacterDetailActivity : BaseActivity( ActivityCharacterDetailBinding::inflate ) { - private val viewModel: CharacterDetailViewModel by viewModels() + private val viewModel: CharacterDetailViewModel by viewModel() + + private lateinit var loadingDialog: LoadingDialog private val adapter by lazy { OtherCharacterAdapter( @@ -43,13 +46,15 @@ class CharacterDetailActivity : BaseActivity( if (characterId <= 0) { showToast("잘못된 접근 입니다.") finish() + } else { + bindObservers() + viewModel.load(characterId) } - - viewModel.loadMock(characterId) - bindObservers() } override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + // 뒤로 가기 binding.detailToolbar.tvBack.setOnClickListener { finish() } @@ -103,6 +108,21 @@ class CharacterDetailActivity : BaseActivity( private fun bindObservers() { viewModel.uiState.observe(this) { state -> + // 1) 로딩 상태 처리 + if (state.isLoading) { + loadingDialog.show(screenWidth) + } else { + loadingDialog.dismiss() + } + + // 2) 에러 토스트 처리 + state.error?.let { errorMsg -> + if (errorMsg.isNotBlank()) { + showToast(errorMsg) + } + } + + // 3) 상세 데이터가 있을 경우에만 기존 UI 바인딩 수행 val detail = state.detail ?: return@observe // 배경 이미지 @@ -157,7 +177,12 @@ class CharacterDetailActivity : BaseActivity( } // 다른 캐릭터 리스트 - adapter.submitList(detail.others) + if (detail.others.isEmpty()) { + binding.llOtherCharactersSection.visibility = View.GONE + } else { + binding.llOtherCharactersSection.visibility = View.VISIBLE + adapter.submitList(detail.others) + } } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/CharacterDetailRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/CharacterDetailRepository.kt new file mode 100644 index 00000000..6ebe11de --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/CharacterDetailRepository.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.chat.character.detail + +import kr.co.vividnext.sodalive.chat.character.CharacterApi + +class CharacterDetailRepository(private val api: CharacterApi) { + fun getCharacterDetail(token: String, characterId: Long) = + api.getCharacterDetail(authHeader = token, characterId = characterId) +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/CharacterDetailResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/CharacterDetailResponse.kt index 6c146602..71fb9804 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/CharacterDetailResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/CharacterDetailResponse.kt @@ -21,9 +21,9 @@ data class CharacterDetailResponse( @Keep enum class CharacterType { - @SerializedName("CLONE") + @SerializedName("Clone") CLONE, - @SerializedName("CHARACTER") + @SerializedName("Character") CHARACTER } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/CharacterDetailViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/CharacterDetailViewModel.kt index c2b3ed7e..4ecc4c93 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/CharacterDetailViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/CharacterDetailViewModel.kt @@ -2,7 +2,11 @@ package kr.co.vividnext.sodalive.chat.character.detail import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager /** * 캐릭터 상세 화면에서 사용하는 ViewModel. @@ -13,61 +17,41 @@ import kr.co.vividnext.sodalive.base.BaseViewModel * - 원작 섹션 (빈 값이면 UI에서 숨김) * - 다른 캐릭터 목록 (이미지, 캐릭터 명, 태그) */ -class CharacterDetailViewModel : BaseViewModel() { - // UiState를 CharacterDetailResponse 구조에 맞게 변경 +class CharacterDetailViewModel( + private val repository: CharacterDetailRepository +) : BaseViewModel() { data class UiState( - val detail: CharacterDetailResponse? = null + val detail: CharacterDetailResponse? = null, + val isLoading: Boolean = false, + val error: String? = null ) private val _uiState = MutableLiveData(UiState()) val uiState: LiveData get() = _uiState - fun loadMock(characterId: Long) { - // TODO: Repository 연동 예정. 현재는 더미 데이터로 표시 - val demoWorldview = CharacterBackgroundResponse( - topic = "세계관", - description = "특별한 꽃을 길러낼 수 있는 능력을 가진 리엘라.\n그녀는 호손 공작의 상속자가 되고 말아버리는데...\n\n뜻하지 않은 만남과 비밀이 펼쳐진다." - ) - val demoPersonality = CharacterPersonalityResponse( - trait = "밝음, 고집", - description = "밝고 쾌활하지만 고집이 있으며, 친구를 소중히 여기고 어려움 앞에서도 물러서지 않습니다.\n상황에 따라 유연하게 대처하지만 신념은 확고합니다.\n\n상황에 따라 유연하게 대처하지만 신념은 확고합니다.\n\n\n상황에 따라 유연하게 대처하지만 신념은 확고합니다." - ) - val others = listOf( - OtherCharacter( - characterId = 1, - name = "엘리시아", - imageUrl = "https://picsum.photos/seed/char1/300/300", - tags = "#마법 #학생 #쾌활" - ), - OtherCharacter( - characterId = 2, - name = "루카", - imageUrl = "https://picsum.photos/seed/char2/300/300", - tags = "#기사 #충직 #과묵" - ), - OtherCharacter( - characterId = 3, - name = "세라", - imageUrl = "https://picsum.photos/seed/char3/300/300", - tags = "#암살자 #쿨 #시크" - ) - ) + fun load(characterId: Long) { + _uiState.value = _uiState.value?.copy(isLoading = true, error = null) + val token = "Bearer ${SharedPreferenceManager.token}" - val response = CharacterDetailResponse( - characterId = characterId, - name = "리엘라", - description = "꽃을 키우는 공작가의 상속자", - mbti = "ENFP", - imageUrl = "https://picsum.photos/seed/bg1/1000/1000", - personalities = demoPersonality, - backgrounds = demoWorldview, - tags = "#커버곡 #라이브 #연애 #썸 #채팅 #라방", - originalTitle = "네이버 시리즈 독 안에 든 선생님", - originalLink = "https://series.naver.com/", - characterType = CharacterType.CLONE, - others = others + compositeDisposable.add( + repository.getCharacterDetail(token = token, characterId = characterId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { response -> + val success = response.success + val data = response.data + if (success && data != null) { + _uiState.value = UiState(detail = data, isLoading = false, error = null) + } else { + _uiState.value = UiState(detail = null, isLoading = false, error = response.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + }, + { throwable -> + Logger.e(throwable, throwable.message ?: "") + _uiState.value = UiState(detail = null, isLoading = false, error = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) ) - - _uiState.value = UiState(detail = response) } } 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 15d76b50..c62f1bb4 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 @@ -67,6 +67,8 @@ import kr.co.vividnext.sodalive.audition.role.AuditionRoleDetailViewModel import kr.co.vividnext.sodalive.chat.character.CharacterApi import kr.co.vividnext.sodalive.chat.character.CharacterTabRepository import kr.co.vividnext.sodalive.chat.character.CharacterTabViewModel +import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailRepository +import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailViewModel import kr.co.vividnext.sodalive.chat.talk.TalkApi import kr.co.vividnext.sodalive.chat.talk.TalkTabRepository import kr.co.vividnext.sodalive.chat.talk.TalkTabViewModel @@ -351,6 +353,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { viewModel { PointStatusViewModel(get()) } viewModel { HomeViewModel(get(), get()) } viewModel { CharacterTabViewModel(get()) } + viewModel { CharacterDetailViewModel(get()) } viewModel { TalkTabViewModel(get()) } } @@ -397,6 +400,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { factory { PointStatusRepository(get()) } factory { HomeRepository(get()) } factory { CharacterTabRepository(get()) } + factory { CharacterDetailRepository(get()) } factory { TalkTabRepository(get()) } }