캐릭터 상세 문자열 리소스화

CharacterDetail/갤러리 탭 다국어 리소스 추가

UiText로 오류 메시지 지역화 처리
This commit is contained in:
2025-12-01 17:00:26 +09:00
parent 4e0e6708e6
commit 3cf24c2ab6
12 changed files with 189 additions and 54 deletions

View File

@@ -17,7 +17,7 @@ class CharacterDetailActivity : BaseActivity<ActivityCharacterDetailBinding>(
val characterId = intent.getLongExtra(EXTRA_CHARACTER_ID, 0)
if (characterId <= 0) {
showToast("잘못된 접근 입니다.")
showToast(getString(R.string.character_detail_error_invalid_access))
finish()
return
}
@@ -28,11 +28,15 @@ class CharacterDetailActivity : BaseActivity<ActivityCharacterDetailBinding>(
override fun setupView() {
// 뒤로 가기
binding.detailToolbar.tvBack.setOnClickListener { finish() }
binding.detailToolbar.tvBack.text = "캐릭터 정보"
binding.detailToolbar.tvBack.text = getString(R.string.screen_character_detail_title)
// 탭 구성: 상세, 갤러리
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("상세"))
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("갤러리"))
binding.tabLayout.addTab(
binding.tabLayout.newTab().setText(R.string.screen_character_detail_tab_info)
)
binding.tabLayout.addTab(
binding.tabLayout.newTab().setText(R.string.screen_character_detail_tab_gallery)
)
val characterId = intent.getLongExtra(EXTRA_CHARACTER_ID, 0)

View File

@@ -92,10 +92,9 @@ class CharacterDetailFragment : BaseFragment<FragmentCharacterDetailBinding>(
}
// 2) 에러 토스트 처리
state.error?.let { errorMsg ->
if (errorMsg.isNotBlank()) {
showToast(errorMsg)
}
state.error?.let { error ->
val message = error.asString(requireContext())
if (message.isNotBlank()) showToast(message)
}
// 2-1) 채팅방 생성 성공 처리 (이벤트)
@@ -143,7 +142,7 @@ class CharacterDetailFragment : BaseFragment<FragmentCharacterDetailBinding>(
if (detail.age != null) {
binding.tvAge.visibility = View.VISIBLE
binding.tvAge.text = "${detail.age}"
binding.tvAge.text = getString(R.string.character_detail_age, detail.age)
} else {
binding.tvAge.visibility = View.GONE
}
@@ -167,8 +166,8 @@ class CharacterDetailFragment : BaseFragment<FragmentCharacterDetailBinding>(
binding.tvCharacterName.text = detail.name
binding.tvCharacterStatus.text = when (detail.characterType) {
CharacterType.CLONE -> "Clone"
CharacterType.CHARACTER -> "Character"
CharacterType.CLONE -> getString(R.string.chat_character_type_clone)
CharacterType.CHARACTER -> getString(R.string.chat_character_type_character)
}
// 캐릭터 타입에 따른 배경 설정
binding.tvCharacterStatus.setBackgroundResource(
@@ -192,7 +191,7 @@ class CharacterDetailFragment : BaseFragment<FragmentCharacterDetailBinding>(
// 표시 상태는 항상 접힘 상태로 시작
applyWorldviewCollapsedLayout()
isWorldviewExpanded = false
binding.tvWorldviewExpand.text = "더보기"
binding.tvWorldviewExpand.text = getString(R.string.read_more)
binding.ivWorldviewExpand.setImageResource(R.drawable.ic_chevron_down)
}
@@ -207,7 +206,7 @@ class CharacterDetailFragment : BaseFragment<FragmentCharacterDetailBinding>(
binding.llPersonalityExpand.visibility = if (needExpand) View.VISIBLE else View.GONE
applyPersonalityCollapsedLayout()
isPersonalityExpanded = false
binding.tvPersonalityExpand.text = "더보기"
binding.tvPersonalityExpand.text = getString(R.string.read_more)
binding.ivPersonalityExpand.setImageResource(R.drawable.ic_chevron_down)
}
@@ -300,7 +299,7 @@ class CharacterDetailFragment : BaseFragment<FragmentCharacterDetailBinding>(
val idFromState = viewModel.uiState.value?.detail?.characterId ?: 0L
val targetCharacterId = if (idFromState > 0) idFromState else characterId
if (targetCharacterId <= 0) {
showToast("잘못된 접근 입니다.")
showToast(getString(R.string.character_detail_error_invalid_access))
return@setOnClickListener
}
@@ -313,13 +312,15 @@ class CharacterDetailFragment : BaseFragment<FragmentCharacterDetailBinding>(
.subscribe({ resp ->
if (resp.success) {
binding.etCommentInput.setText("")
showToast("등록되었습니다.")
showToast(getString(R.string.character_detail_comment_register_success))
viewModel.load(targetCharacterId)
} else {
showToast(resp.message ?: "요청 중 오류가 발생했습니다")
showToast(
resp.message ?: getString(R.string.common_error_request)
)
}
}, { e ->
showToast(e.message ?: "요청 중 오류가 발생했습니다")
showToast(e.message ?: getString(R.string.common_error_request))
})
compositeDisposable.add(d)
}
@@ -384,7 +385,7 @@ class CharacterDetailFragment : BaseFragment<FragmentCharacterDetailBinding>(
if (targetId > 0) {
viewModel.createChatRoom(targetId)
} else {
showToast("잘못된 접근 입니다.")
showToast(getString(R.string.character_detail_error_invalid_access))
}
}
}
@@ -395,12 +396,12 @@ class CharacterDetailFragment : BaseFragment<FragmentCharacterDetailBinding>(
// 확장 상태
binding.tvWorldviewContent.maxLines = Integer.MAX_VALUE
binding.tvWorldviewContent.ellipsize = null
binding.tvWorldviewExpand.text = "간략히"
binding.tvWorldviewExpand.text = getString(R.string.read_less)
binding.ivWorldviewExpand.setImageResource(R.drawable.ic_chevron_up)
} else {
// 접힘 상태 (3줄)
applyWorldviewCollapsedLayout()
binding.tvWorldviewExpand.text = "더보기"
binding.tvWorldviewExpand.text = getString(R.string.read_more)
binding.ivWorldviewExpand.setImageResource(R.drawable.ic_chevron_down)
}
}
@@ -415,11 +416,11 @@ class CharacterDetailFragment : BaseFragment<FragmentCharacterDetailBinding>(
if (isPersonalityExpanded) {
binding.tvPersonalityContent.maxLines = Integer.MAX_VALUE
binding.tvPersonalityContent.ellipsize = null
binding.tvPersonalityExpand.text = "간략히"
binding.tvPersonalityExpand.text = getString(R.string.read_less)
binding.ivPersonalityExpand.setImageResource(R.drawable.ic_chevron_up)
} else {
applyPersonalityCollapsedLayout()
binding.tvPersonalityExpand.text = "더보기"
binding.tvPersonalityExpand.text = getString(R.string.read_more)
binding.ivPersonalityExpand.setImageResource(R.drawable.ic_chevron_down)
}
}

View File

@@ -5,8 +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.R
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.chat.talk.room.CreateChatRoomRequest
import kr.co.vividnext.sodalive.common.UiText
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
/**
@@ -24,7 +26,7 @@ class CharacterDetailViewModel(
data class UiState(
val detail: CharacterDetailResponse? = null,
val isLoading: Boolean = false,
val error: String? = null,
val error: UiText? = null,
val chatRoomId: Long? = null
)
@@ -49,7 +51,9 @@ class CharacterDetailViewModel(
_uiState.value = UiState(
detail = null,
isLoading = false,
error = response.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
error = response.message?.takeIf { it.isNotBlank() }
?.let { UiText.DynamicString(it) }
?: UiText.StringResource(R.string.common_error_unknown)
)
}
},
@@ -58,7 +62,7 @@ class CharacterDetailViewModel(
_uiState.value = UiState(
detail = null,
isLoading = false,
error = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
error = UiText.StringResource(R.string.common_error_unknown)
)
}
)
@@ -89,7 +93,9 @@ class CharacterDetailViewModel(
} else {
_uiState.value = _uiState.value?.copy(
isLoading = false,
error = response.message ?: "채팅방 생성에 실패했습니다. 다시 시도해 주세요."
error = response.message?.takeIf { it.isNotBlank() }
?.let { UiText.DynamicString(it) }
?: UiText.StringResource(R.string.character_detail_chat_room_create_failed)
)
}
},
@@ -97,7 +103,7 @@ class CharacterDetailViewModel(
Logger.e(throwable, throwable.message ?: "")
_uiState.value = _uiState.value?.copy(
isLoading = false,
error = "채팅방 생성 중 오류가 발생했습니다. 다시 시도해 주세요."
error = UiText.StringResource(R.string.character_detail_chat_room_create_failed)
)
}
)

View File

@@ -7,6 +7,7 @@ import android.view.View
import androidx.core.graphics.toColorInt
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.base.SodaDialog
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity.Companion.EXTRA_CHARACTER_ID
@@ -106,11 +107,16 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
binding.clRatio.visibility = View.VISIBLE
val percent = (state.ratio * 100).toInt()
binding.tvRatioLeft.text = "$percent% 보유중"
binding.tvRatioLeft.text =
getString(R.string.character_gallery_ratio_owned, percent)
val ownedStr = state.ownedCount.toString()
val totalStr = state.totalCount.toString()
val fullText = "$ownedStr / ${totalStr}"
val fullText = getString(
R.string.character_gallery_ratio_count,
state.ownedCount,
state.totalCount
)
val spannable = android.text.SpannableString(fullText)
val ownedColor = "#FDD453".toColorInt()
spannable.setSpan(
@@ -132,7 +138,10 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
binding.clRatio.visibility = View.GONE
}
state.error?.let { showToast(it) }
state.error?.let { error ->
val message = error.asString(requireContext())
if (message.isNotBlank()) showToast(message)
}
}
}
@@ -140,13 +149,16 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
SodaDialog(
activity = requireActivity(),
layoutInflater = this.layoutInflater,
title = "구매 확인",
desc = "선택한 이미지를 구매하시겠습니까?",
confirmButtonTitle = "${item.imagePriceCan}캔으로 구매",
title = getString(R.string.character_gallery_purchase_title),
desc = getString(R.string.character_gallery_purchase_desc),
confirmButtonTitle = getString(
R.string.character_gallery_purchase_confirm,
item.imagePriceCan
),
confirmButtonClick = {
viewModel.purchaseImage(item.id, position)
},
cancelButtonTitle = "취소"
cancelButtonTitle = getString(R.string.cancel)
).show(screenWidth)
}

View File

@@ -5,8 +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.R
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.UiText
class CharacterGalleryViewModel(
private val repository: CharacterGalleryRepository
@@ -18,7 +20,7 @@ class CharacterGalleryViewModel(
val ratio: Float = 0f, // 0.0 ~ 1.0
val items: List<CharacterImageListItemResponse> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
val error: UiText? = null
)
private val _uiState = MutableLiveData(UiState())
@@ -92,7 +94,9 @@ class CharacterGalleryViewModel(
} else {
_uiState.value = _uiState.value?.copy(
isLoading = isRequesting || isPurchasing,
error = response.message ?: "갤러리 정보를 불러오지 못했습니다."
error = response.message?.takeIf { it.isNotBlank() }
?.let { UiText.DynamicString(it) }
?: UiText.StringResource(R.string.character_gallery_load_error)
)
}
}, { throwable ->
@@ -100,7 +104,7 @@ class CharacterGalleryViewModel(
Logger.e(throwable, throwable.message ?: "")
_uiState.value = _uiState.value?.copy(
isLoading = isRequesting || isPurchasing,
error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
error = UiText.StringResource(R.string.common_error_network_retry)
)
})
)
@@ -153,7 +157,9 @@ class CharacterGalleryViewModel(
)
} else {
_uiState.value = _uiState.value?.copy(
error = response.message ?: "구매에 실패했습니다."
error = response.message?.takeIf { it.isNotBlank() }
?.let { UiText.DynamicString(it) }
?: UiText.StringResource(R.string.character_gallery_purchase_failed)
)
}
},
@@ -162,7 +168,7 @@ class CharacterGalleryViewModel(
Logger.e(throwable, throwable.message ?: "")
_uiState.value = _uiState.value?.copy(
isLoading = isRequesting || isPurchasing,
error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
error = UiText.StringResource(R.string.common_error_network_retry)
)
}
)

View File

@@ -0,0 +1,16 @@
package kr.co.vividnext.sodalive.common
import android.content.Context
import androidx.annotation.StringRes
sealed class UiText {
data class DynamicString(val value: String) : UiText()
class StringResource(@StringRes val resId: Int, vararg val args: Any) : UiText() {
val formatArgs: Array<out Any> = args
}
fun asString(context: Context): String = when (this) {
is DynamicString -> value
is StringResource -> context.getString(resId, *formatArgs)
}
}