fix(character-detail): characterId 전달 및 상세 탭 전환 로직 수정

fix(character-detail): 탭 전환 시 프래그먼트 캐싱하여 재로딩 방지

CharacterDetailFragment에 newInstance(characterId) 도입 및 ARG 전달 구조 추가.
Fragment에서 잘못된 intent 참조 제거하고 arguments → activity.intent 순으로 안전하게 조회.
Activity 초기 진입 시 상세 탭 로딩 경로 정리 및 characterId 유효성 검사 시 종료 처리 보강.

replace 기반 교체를 add/show/hide 구조로 전환.
TAG_DETAIL/TAG_GALLERY로 인스턴스를 식별하여 FragmentManager 복원/재사용.
탭 이동 시 기존 인스턴스 표시만 수행하여 onViewCreated 재호출/네트워크 재요청 방지.
This commit is contained in:
2025-08-22 15:18:28 +09:00
parent 989a0f361b
commit f917eb8c93
14 changed files with 962 additions and 881 deletions

View File

@@ -1,7 +1,7 @@
package kr.co.vividnext.sodalive.chat.character package kr.co.vividnext.sodalive.chat.character
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailResponse import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterDetailResponse
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header

View File

@@ -1,394 +1,96 @@
package kr.co.vividnext.sodalive.chat.character.detail package kr.co.vividnext.sodalive.chat.character.detail
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import androidx.fragment.app.Fragment
import android.view.View import com.google.android.material.tabs.TabLayout
import androidx.core.net.toUri
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentListBottomSheet import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterDetailFragment
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentRepository import kr.co.vividnext.sodalive.chat.character.detail.gallery.CharacterGalleryFragment
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityCharacterDetailBinding import kr.co.vividnext.sodalive.databinding.ActivityCharacterDetailBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
class CharacterDetailActivity : BaseActivity<ActivityCharacterDetailBinding>( class CharacterDetailActivity : BaseActivity<ActivityCharacterDetailBinding>(
ActivityCharacterDetailBinding::inflate ActivityCharacterDetailBinding::inflate
) { ) {
private val viewModel: CharacterDetailViewModel by viewModel()
private val commentRepository: CharacterCommentRepository by inject()
private lateinit var loadingDialog: LoadingDialog
private val adapter by lazy {
OtherCharacterAdapter(
onItemClick = { item ->
startActivity(
Intent(this, CharacterDetailActivity::class.java).apply {
putExtra(EXTRA_CHARACTER_ID, item.characterId)
}
)
}
)
}
private var isWorldviewExpanded = false
private var isPersonalityExpanded = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 더미 데이터 로드 (추후 Intent/Repository 연동)
val characterId = intent.getLongExtra(EXTRA_CHARACTER_ID, 0) val characterId = intent.getLongExtra(EXTRA_CHARACTER_ID, 0)
if (characterId <= 0) { if (characterId <= 0) {
showToast("잘못된 접근 입니다.") showToast("잘못된 접근 입니다.")
finish() finish()
} else { return
bindObservers()
viewModel.load(characterId)
} }
super.onCreate(savedInstanceState)
} }
override fun setupView() { override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
// 뒤로 가기 // 뒤로 가기
binding.detailToolbar.tvBack.setOnClickListener { finish() } binding.detailToolbar.tvBack.setOnClickListener { finish() }
// 탭 구성: 상세, 갤러리 // 탭 구성: 상세, 갤러리
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("상세")) binding.tabLayout.addTab(binding.tabLayout.newTab().setText("상세"))
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("갤러리")) binding.tabLayout.addTab(binding.tabLayout.newTab().setText("갤러리"))
binding.tabLayout.addOnTabSelectedListener(object :
com.google.android.material.tabs.TabLayout.OnTabSelectedListener { val characterId = intent.getLongExtra(EXTRA_CHARACTER_ID, 0)
override fun onTabSelected(tab: com.google.android.material.tabs.TabLayout.Tab) {
if (tab.position == 0) { // 기존 프래그먼트 복원/재사용
// 상세 탭: 기존 스크롤 화면 표시 var detail = supportFragmentManager.findFragmentByTag(TAG_DETAIL)
binding.scrollViewCharacterDetail.visibility = View.VISIBLE var gallery = supportFragmentManager.findFragmentByTag(TAG_GALLERY)
binding.flContainer.visibility = View.GONE
} else { val transaction = supportFragmentManager.beginTransaction()
// 갤러리 탭: 컨테이너 표시 및 갤러리 프래그먼트 로드 if (detail == null) {
binding.scrollViewCharacterDetail.visibility = View.GONE detail = CharacterDetailFragment.newInstance(characterId)
binding.flContainer.visibility = View.VISIBLE transaction.add(R.id.fl_container, detail, TAG_DETAIL)
val fragment = }
kr.co.vividnext.sodalive.chat.character.detail.gallery.CharacterGalleryFragment() if (gallery == null) {
supportFragmentManager.beginTransaction() gallery = CharacterGalleryFragment()
.replace(R.id.fl_container, fragment) transaction.add(R.id.fl_container, gallery, TAG_GALLERY)
.commit() transaction.hide(gallery)
}
transaction.show(detail).commit()
binding.tabLayout
.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
showTab(tab.position)
} }
}
override fun onTabUnselected(tab: com.google.android.material.tabs.TabLayout.Tab) {} override fun onTabUnselected(tab: TabLayout.Tab) {}
override fun onTabReselected(tab: com.google.android.material.tabs.TabLayout.Tab) {} override fun onTabReselected(tab: TabLayout.Tab) {}
}) })
// 다른 캐릭터 리스트: 가로 스크롤
val recyclerView = binding.rvOtherCharacters
recyclerView.layoutManager = LinearLayoutManager(
this,
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 8f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.left = 8f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 8f.dpToPx().toInt()
outRect.right = 8f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = adapter
// 세계관 전체보기 토글 클릭 리스너
binding.llWorldviewExpand.setOnClickListener {
toggleWorldviewExpand()
}
// 성격 전체보기 토글 클릭 리스너
binding.llPersonalityExpand.setOnClickListener {
togglePersonalityExpand()
}
// 대화하기 버튼 클릭: 채팅방 생성 API 호출
binding.btnChat.setOnClickListener {
val idFromState = viewModel.uiState.value?.detail?.characterId ?: 0L
val idFromIntent = intent.getLongExtra(EXTRA_CHARACTER_ID, 0L)
val targetId = if (idFromState > 0) idFromState else idFromIntent
if (targetId > 0) {
viewModel.createChatRoom(targetId)
} else {
showToast("잘못된 접근 입니다.")
}
}
} }
@SuppressLint("SetTextI18n") private fun showTab(position: Int) {
private fun bindObservers() { val detail = supportFragmentManager.findFragmentByTag(TAG_DETAIL)
viewModel.uiState.observe(this) { state -> val gallery = supportFragmentManager.findFragmentByTag(TAG_GALLERY)
// 1) 로딩 상태 처리 val transaction = supportFragmentManager.beginTransaction()
if (state.isLoading) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
// 2) 에러 토스트 처리 fun Fragment?.hideIfExists() {
state.error?.let { errorMsg -> if (this != null && !this.isHidden) transaction.hide(this)
if (errorMsg.isNotBlank()) {
showToast(errorMsg)
}
}
// 2-1) 채팅방 생성 성공 처리 (이벤트)
state.chatRoomId?.let { roomId ->
startActivity(ChatRoomActivity.newIntent(this, roomId))
viewModel.consumeChatRoomCreated()
}
// 3) 상세 데이터가 있을 경우에만 기존 UI 바인딩 수행
val detail = state.detail ?: return@observe
// 배경 이미지
binding.ivCharacterBackground.load(detail.imageUrl) { crossfade(true) }
// 기본 정보
binding.detailToolbar.tvBack.text = detail.name
binding.tvCharacterName.text = detail.name
binding.tvCharacterStatus.text = when (detail.characterType) {
CharacterType.CLONE -> "Clone"
CharacterType.CHARACTER -> "Character"
}
// 캐릭터 타입에 따른 배경 설정
binding.tvCharacterStatus.setBackgroundResource(
when (detail.characterType) {
CharacterType.CLONE -> R.drawable.bg_character_status_clone
CharacterType.CHARACTER -> R.drawable.bg_character_status_character
}
)
binding.tvCharacterDescription.text = detail.description
binding.tvCharacterTags.text = detail.tags
// 세계관 내용과 버튼 가시성 초기화
val worldviewText = detail.backgrounds?.description.orEmpty()
binding.tvWorldviewContent.text = worldviewText
// 먼저 전체 줄 수를 측정한 뒤 접힘 레이아웃 적용
binding.tvWorldviewContent.post {
val totalLines = binding.tvWorldviewContent.layout?.lineCount
?: binding.tvWorldviewContent.lineCount
val needExpand = totalLines > 3
binding.llWorldviewExpand.visibility = if (needExpand) View.VISIBLE else View.GONE
// 표시 상태는 항상 접힘 상태로 시작
applyWorldviewCollapsedLayout()
isWorldviewExpanded = false
binding.tvWorldviewExpand.text = "더보기"
binding.ivWorldviewExpand.setImageResource(R.drawable.ic_chevron_down)
}
// 성격 내용과 버튼 가시성 초기화
val personalityText = detail.personalities?.description.orEmpty()
binding.tvPersonalityContent.text = personalityText
// 먼저 전체 줄 수를 측정한 뒤 접힘 레이아웃 적용
binding.tvPersonalityContent.post {
val totalLines = binding.tvPersonalityContent.layout?.lineCount
?: binding.tvPersonalityContent.lineCount
val needExpand = totalLines > 3
binding.llPersonalityExpand.visibility = if (needExpand) View.VISIBLE else View.GONE
applyPersonalityCollapsedLayout()
isPersonalityExpanded = false
binding.tvPersonalityExpand.text = "더보기"
binding.ivPersonalityExpand.setImageResource(R.drawable.ic_chevron_down)
}
// 원작 섹션 표시/숨김
if (detail.originalTitle.isNullOrBlank() || detail.originalLink.isNullOrBlank()) {
binding.llOriginalSection.visibility = View.GONE
} else {
binding.llOriginalSection.visibility = View.VISIBLE
binding.tvOriginalContent.text = detail.originalTitle
binding.tvOriginalLink.setOnClickListener {
runCatching {
startActivity(Intent(Intent.ACTION_VIEW, detail.originalLink.toUri()))
}
}
}
// 다른 캐릭터 리스트
if (detail.others.isEmpty()) {
binding.llOtherCharactersSection.visibility = View.GONE
} else {
binding.llOtherCharactersSection.visibility = View.VISIBLE
adapter.submitList(detail.others)
}
// 댓글 섹션 바인딩
binding.tvCommentsCount.text = "${detail.totalComments}"
// 댓글 섹션 터치 시 리스트 BottomSheet 열기 (댓글 1개 이상일 때)
binding.llCommentsSection.setOnClickListener(null)
if (detail.totalComments > 0) {
binding.llCommentsSection.setOnClickListener {
val sheet = CharacterCommentListBottomSheet(detail.characterId)
sheet.show(supportFragmentManager, "character_comments")
}
}
if (
detail.totalComments > 0 &&
detail.latestComment != null &&
detail.latestComment.comment.isNotBlank()
) {
binding.llLatestComment.visibility = View.VISIBLE
binding.llNoComment.visibility = View.GONE
val latest = detail.latestComment
val profileUrl = latest.memberProfileImage
if (profileUrl.isNotBlank()) {
binding.ivCommentProfile.load(profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_placeholder_profile)
error(R.drawable.ic_placeholder_profile)
transformations(CircleCropTransformation())
}
} else {
binding.ivMyProfile.load(R.drawable.ic_placeholder_profile) {
crossfade(true)
placeholder(R.drawable.ic_placeholder_profile)
error(R.drawable.ic_placeholder_profile)
transformations(CircleCropTransformation())
}
}
binding.tvLatestComment.text = latest.comment.ifBlank {
latest.memberNickname
}
} else {
binding.llLatestComment.visibility = View.GONE
binding.llNoComment.visibility = View.VISIBLE
// 내 프로필 이미지는 SharedPreference의 profileImage 사용 (fallback: placeholder)
val myProfileUrl = SharedPreferenceManager.profileImage
if (myProfileUrl.isNotBlank()) {
binding.ivMyProfile.load(myProfileUrl) {
crossfade(true)
placeholder(R.drawable.ic_placeholder_profile)
error(R.drawable.ic_placeholder_profile)
transformations(CircleCropTransformation())
}
} else {
binding.ivMyProfile.load(R.drawable.ic_placeholder_profile) {
crossfade(true)
placeholder(R.drawable.ic_placeholder_profile)
error(R.drawable.ic_placeholder_profile)
transformations(CircleCropTransformation())
}
}
binding.ivSendComment.setOnClickListener {
val text = binding.etCommentInput.text?.toString()?.trim().orEmpty()
if (text.isBlank()) return@setOnClickListener
val idFromState = viewModel.uiState.value?.detail?.characterId ?: 0L
val idFromIntent = intent.getLongExtra(EXTRA_CHARACTER_ID, 0L)
val characterId = if (idFromState > 0) idFromState else idFromIntent
if (characterId <= 0) {
showToast("잘못된 접근 입니다.")
return@setOnClickListener
}
val token = "Bearer ${SharedPreferenceManager.token}"
loadingDialog.show(screenWidth)
val d = commentRepository.createComment(characterId, text, token)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally { loadingDialog.dismiss() }
.subscribe({ resp ->
if (resp.success) {
binding.etCommentInput.setText("")
showToast("등록되었습니다.")
viewModel.load(characterId)
} else {
showToast(resp.message ?: "요청 중 오류가 발생했습니다")
}
}, { e ->
showToast(e.message ?: "요청 중 오류가 발생했습니다")
})
compositeDisposable.add(d)
}
}
} }
}
private fun toggleWorldviewExpand() { // 모두 숨김
isWorldviewExpanded = !isWorldviewExpanded detail.hideIfExists()
if (isWorldviewExpanded) { gallery.hideIfExists()
// 확장 상태
binding.tvWorldviewContent.maxLines = Integer.MAX_VALUE // 포지션에 맞게 표시
binding.tvWorldviewContent.ellipsize = null val toShow: Fragment? = when (position) {
binding.tvWorldviewExpand.text = "간략히" 0 -> detail
binding.ivWorldviewExpand.setImageResource(R.drawable.ic_chevron_up) else -> gallery
} else {
// 접힘 상태 (3줄)
applyWorldviewCollapsedLayout()
binding.tvWorldviewExpand.text = "더보기"
binding.ivWorldviewExpand.setImageResource(R.drawable.ic_chevron_down)
} }
if (toShow != null) transaction.show(toShow)
transaction.commit()
} }
private fun applyWorldviewCollapsedLayout() { fun setTitle(title: String) {
binding.tvWorldviewContent.maxLines = 3 binding.detailToolbar.tvBack.text = title
binding.tvWorldviewContent.ellipsize = TextUtils.TruncateAt.END
}
private fun togglePersonalityExpand() {
isPersonalityExpanded = !isPersonalityExpanded
if (isPersonalityExpanded) {
binding.tvPersonalityContent.maxLines = Integer.MAX_VALUE
binding.tvPersonalityContent.ellipsize = null
binding.tvPersonalityExpand.text = "간략히"
binding.ivPersonalityExpand.setImageResource(R.drawable.ic_chevron_up)
} else {
applyPersonalityCollapsedLayout()
binding.tvPersonalityExpand.text = "더보기"
binding.ivPersonalityExpand.setImageResource(R.drawable.ic_chevron_down)
}
}
private fun applyPersonalityCollapsedLayout() {
binding.tvPersonalityContent.maxLines = 3
binding.tvPersonalityContent.ellipsize = TextUtils.TruncateAt.END
} }
companion object { companion object {
const val EXTRA_CHARACTER_ID = "extra_character_id" const val EXTRA_CHARACTER_ID = "extra_character_id"
private const val TAG_DETAIL = "tag_character_detail"
private const val TAG_GALLERY = "tag_character_gallery"
} }
} }

View File

@@ -1,19 +1,383 @@
package kr.co.vividnext.sodalive.chat.character.detail.detail package kr.co.vividnext.sodalive.chat.character.detail.detail
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils
import android.view.View import android.view.View
import androidx.core.net.toUri
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
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.BaseFragment import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentListBottomSheet
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentRepository
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity.Companion.EXTRA_CHARACTER_ID
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentCharacterDetailBinding import kr.co.vividnext.sodalive.databinding.FragmentCharacterDetailBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
/** /**
* 캐릭터 상세 - 상세 탭 * 캐릭터 상세 - 상세 탭
* TODO: 기존 CharacterDetailActivity UI 바인딩 로직을 이 Fragment로 점진적으로 이전합니다.
*/ */
class CharacterDetailFragment : BaseFragment<FragmentCharacterDetailBinding>( class CharacterDetailFragment : BaseFragment<FragmentCharacterDetailBinding>(
FragmentCharacterDetailBinding::inflate FragmentCharacterDetailBinding::inflate
) { ) {
companion object {
private const val ARG_CHARACTER_ID = "arg_character_id"
fun newInstance(characterId: Long): CharacterDetailFragment =
CharacterDetailFragment().apply {
arguments = Bundle().apply { putLong(ARG_CHARACTER_ID, characterId) }
}
}
private val viewModel: CharacterDetailViewModel by viewModel()
private val commentRepository: CharacterCommentRepository by inject()
private lateinit var loadingDialog: LoadingDialog
private val characterId: Long by lazy {
arguments?.getLong(ARG_CHARACTER_ID)
?: requireActivity().intent.getLongExtra(EXTRA_CHARACTER_ID, 0L)
}
private val adapter by lazy {
OtherCharacterAdapter(
onItemClick = { item ->
startActivity(
Intent(
requireActivity(),
CharacterDetailActivity::class.java
).apply {
putExtra(EXTRA_CHARACTER_ID, item.characterId)
}
)
}
)
}
private var isWorldviewExpanded = false
private var isPersonalityExpanded = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// 추후 상세 UI/로직 반영 예정
setupView()
bindObservers()
viewModel.load(characterId)
} }
@SuppressLint("SetTextI18n")
private fun bindObservers() {
viewModel.uiState.observe(viewLifecycleOwner) { state ->
// 1) 로딩 상태 처리
if (state.isLoading) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
// 2) 에러 토스트 처리
state.error?.let { errorMsg ->
if (errorMsg.isNotBlank()) {
showToast(errorMsg)
}
}
// 2-1) 채팅방 생성 성공 처리 (이벤트)
state.chatRoomId?.let { roomId ->
startActivity(
ChatRoomActivity.newIntent(
requireActivity(),
roomId
)
)
viewModel.consumeChatRoomCreated()
}
// 3) 상세 데이터가 있을 경우에만 기존 UI 바인딩 수행
val detail = state.detail ?: return@observe
// 배경 이미지
binding.ivCharacterBackground.load(detail.imageUrl) { crossfade(true) }
// 기본 정보
(requireActivity() as CharacterDetailActivity).setTitle(detail.name)
binding.tvCharacterName.text = detail.name
binding.tvCharacterStatus.text = when (detail.characterType) {
CharacterType.CLONE -> "Clone"
CharacterType.CHARACTER -> "Character"
}
// 캐릭터 타입에 따른 배경 설정
binding.tvCharacterStatus.setBackgroundResource(
when (detail.characterType) {
CharacterType.CLONE -> R.drawable.bg_character_status_clone
CharacterType.CHARACTER -> R.drawable.bg_character_status_character
}
)
binding.tvCharacterDescription.text = detail.description
binding.tvCharacterTags.text = detail.tags
// 세계관 내용과 버튼 가시성 초기화
val worldviewText = detail.backgrounds?.description.orEmpty()
binding.tvWorldviewContent.text = worldviewText
// 먼저 전체 줄 수를 측정한 뒤 접힘 레이아웃 적용
binding.tvWorldviewContent.post {
val totalLines = binding.tvWorldviewContent.layout?.lineCount
?: binding.tvWorldviewContent.lineCount
val needExpand = totalLines > 3
binding.llWorldviewExpand.visibility = if (needExpand) View.VISIBLE else View.GONE
// 표시 상태는 항상 접힘 상태로 시작
applyWorldviewCollapsedLayout()
isWorldviewExpanded = false
binding.tvWorldviewExpand.text = "더보기"
binding.ivWorldviewExpand.setImageResource(R.drawable.ic_chevron_down)
}
// 성격 내용과 버튼 가시성 초기화
val personalityText = detail.personalities?.description.orEmpty()
binding.tvPersonalityContent.text = personalityText
// 먼저 전체 줄 수를 측정한 뒤 접힘 레이아웃 적용
binding.tvPersonalityContent.post {
val totalLines = binding.tvPersonalityContent.layout?.lineCount
?: binding.tvPersonalityContent.lineCount
val needExpand = totalLines > 3
binding.llPersonalityExpand.visibility = if (needExpand) View.VISIBLE else View.GONE
applyPersonalityCollapsedLayout()
isPersonalityExpanded = false
binding.tvPersonalityExpand.text = "더보기"
binding.ivPersonalityExpand.setImageResource(R.drawable.ic_chevron_down)
}
// 원작 섹션 표시/숨김
if (detail.originalTitle.isNullOrBlank() || detail.originalLink.isNullOrBlank()) {
binding.llOriginalSection.visibility = View.GONE
} else {
binding.llOriginalSection.visibility = View.VISIBLE
binding.tvOriginalContent.text = detail.originalTitle
binding.tvOriginalLink.setOnClickListener {
runCatching {
startActivity(Intent(Intent.ACTION_VIEW, detail.originalLink.toUri()))
}
}
}
// 다른 캐릭터 리스트
if (detail.others.isEmpty()) {
binding.llOtherCharactersSection.visibility = View.GONE
} else {
binding.llOtherCharactersSection.visibility = View.VISIBLE
adapter.submitList(detail.others)
}
// 댓글 섹션 바인딩
binding.tvCommentsCount.text = "${detail.totalComments}"
// 댓글 섹션 터치 시 리스트 BottomSheet 열기 (댓글 1개 이상일 때)
binding.llCommentsSection.setOnClickListener(null)
if (detail.totalComments > 0) {
binding.llCommentsSection.setOnClickListener {
val sheet = CharacterCommentListBottomSheet(detail.characterId)
sheet.show(requireActivity().supportFragmentManager, "character_comments")
}
}
if (
detail.totalComments > 0 &&
detail.latestComment != null &&
detail.latestComment.comment.isNotBlank()
) {
binding.llLatestComment.visibility = View.VISIBLE
binding.llNoComment.visibility = View.GONE
val latest = detail.latestComment
val profileUrl = latest.memberProfileImage
if (profileUrl.isNotBlank()) {
binding.ivCommentProfile.load(profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_placeholder_profile)
error(R.drawable.ic_placeholder_profile)
transformations(CircleCropTransformation())
}
} else {
binding.ivMyProfile.load(R.drawable.ic_placeholder_profile) {
crossfade(true)
placeholder(R.drawable.ic_placeholder_profile)
error(R.drawable.ic_placeholder_profile)
transformations(CircleCropTransformation())
}
}
binding.tvLatestComment.text = latest.comment.ifBlank {
latest.memberNickname
}
} else {
binding.llLatestComment.visibility = View.GONE
binding.llNoComment.visibility = View.VISIBLE
// 내 프로필 이미지는 SharedPreference의 profileImage 사용 (fallback: placeholder)
val myProfileUrl = SharedPreferenceManager.profileImage
if (myProfileUrl.isNotBlank()) {
binding.ivMyProfile.load(myProfileUrl) {
crossfade(true)
placeholder(R.drawable.ic_placeholder_profile)
error(R.drawable.ic_placeholder_profile)
transformations(CircleCropTransformation())
}
} else {
binding.ivMyProfile.load(R.drawable.ic_placeholder_profile) {
crossfade(true)
placeholder(R.drawable.ic_placeholder_profile)
error(R.drawable.ic_placeholder_profile)
transformations(CircleCropTransformation())
}
}
binding.ivSendComment.setOnClickListener {
val text = binding.etCommentInput.text?.toString()?.trim().orEmpty()
if (text.isBlank()) return@setOnClickListener
val idFromState = viewModel.uiState.value?.detail?.characterId ?: 0L
val targetCharacterId = if (idFromState > 0) idFromState else characterId
if (targetCharacterId <= 0) {
showToast("잘못된 접근 입니다.")
return@setOnClickListener
}
val token = "Bearer ${SharedPreferenceManager.token}"
loadingDialog.show(screenWidth)
val d = commentRepository.createComment(targetCharacterId, text, token)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally { loadingDialog.dismiss() }
.subscribe({ resp ->
if (resp.success) {
binding.etCommentInput.setText("")
showToast("등록되었습니다.")
viewModel.load(targetCharacterId)
} else {
showToast(resp.message ?: "요청 중 오류가 발생했습니다")
}
}, { e ->
showToast(e.message ?: "요청 중 오류가 발생했습니다")
})
compositeDisposable.add(d)
}
}
}
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
// 다른 캐릭터 리스트: 가로 스크롤
val recyclerView = binding.rvOtherCharacters
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 8f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.left = 8f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 8f.dpToPx().toInt()
outRect.right = 8f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = adapter
// 세계관 전체보기 토글 클릭 리스너
binding.llWorldviewExpand.setOnClickListener {
toggleWorldviewExpand()
}
// 성격 전체보기 토글 클릭 리스너
binding.llPersonalityExpand.setOnClickListener {
togglePersonalityExpand()
}
// 대화하기 버튼 클릭: 채팅방 생성 API 호출
binding.btnChat.setOnClickListener {
val idFromState = viewModel.uiState.value?.detail?.characterId ?: 0L
val targetId = if (idFromState > 0) idFromState else characterId
if (targetId > 0) {
viewModel.createChatRoom(targetId)
} else {
showToast("잘못된 접근 입니다.")
}
}
}
private fun toggleWorldviewExpand() {
isWorldviewExpanded = !isWorldviewExpanded
if (isWorldviewExpanded) {
// 확장 상태
binding.tvWorldviewContent.maxLines = Integer.MAX_VALUE
binding.tvWorldviewContent.ellipsize = null
binding.tvWorldviewExpand.text = "간략히"
binding.ivWorldviewExpand.setImageResource(R.drawable.ic_chevron_up)
} else {
// 접힘 상태 (3줄)
applyWorldviewCollapsedLayout()
binding.tvWorldviewExpand.text = "더보기"
binding.ivWorldviewExpand.setImageResource(R.drawable.ic_chevron_down)
}
}
private fun applyWorldviewCollapsedLayout() {
binding.tvWorldviewContent.maxLines = 3
binding.tvWorldviewContent.ellipsize = TextUtils.TruncateAt.END
}
private fun togglePersonalityExpand() {
isPersonalityExpanded = !isPersonalityExpanded
if (isPersonalityExpanded) {
binding.tvPersonalityContent.maxLines = Integer.MAX_VALUE
binding.tvPersonalityContent.ellipsize = null
binding.tvPersonalityExpand.text = "간략히"
binding.ivPersonalityExpand.setImageResource(R.drawable.ic_chevron_up)
} else {
applyPersonalityCollapsedLayout()
binding.tvPersonalityExpand.text = "더보기"
binding.ivPersonalityExpand.setImageResource(R.drawable.ic_chevron_down)
}
}
private fun applyPersonalityCollapsedLayout() {
binding.tvPersonalityContent.maxLines = 3
binding.tvPersonalityContent.ellipsize = TextUtils.TruncateAt.END
}
} }

View File

@@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.chat.character.detail package kr.co.vividnext.sodalive.chat.character.detail.detail
import kr.co.vividnext.sodalive.chat.character.CharacterApi import kr.co.vividnext.sodalive.chat.character.CharacterApi
import kr.co.vividnext.sodalive.chat.talk.TalkApi import kr.co.vividnext.sodalive.chat.talk.TalkApi

View File

@@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.chat.character.detail package kr.co.vividnext.sodalive.chat.character.detail.detail
import androidx.annotation.Keep import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName

View File

@@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.chat.character.detail package kr.co.vividnext.sodalive.chat.character.detail.detail
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData

View File

@@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.chat.character.detail package kr.co.vividnext.sodalive.chat.character.detail.detail
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater

View File

@@ -5,7 +5,7 @@ package kr.co.vividnext.sodalive.chat.talk.room
import androidx.annotation.Keep import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.chat.character.detail.CharacterType import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterType
@Keep @Keep
data class CharacterInfo( data class CharacterInfo(

View File

@@ -16,7 +16,7 @@ import androidx.recyclerview.widget.RecyclerView
import coil.load import coil.load
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.chat.character.detail.CharacterType import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterType
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityChatRoomBinding import kr.co.vividnext.sodalive.databinding.ActivityChatRoomBinding
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx

View File

@@ -69,8 +69,8 @@ import kr.co.vividnext.sodalive.chat.character.CharacterTabRepository
import kr.co.vividnext.sodalive.chat.character.CharacterTabViewModel import kr.co.vividnext.sodalive.chat.character.CharacterTabViewModel
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentApi import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentApi
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentRepository import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentRepository
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailRepository import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterDetailRepository
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailViewModel import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterDetailViewModel
import kr.co.vividnext.sodalive.chat.talk.TalkApi import kr.co.vividnext.sodalive.chat.talk.TalkApi
import kr.co.vividnext.sodalive.chat.talk.TalkTabRepository import kr.co.vividnext.sodalive.chat.talk.TalkTabRepository
import kr.co.vividnext.sodalive.chat.talk.TalkTabViewModel import kr.co.vividnext.sodalive.chat.talk.TalkTabViewModel

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/color_131313"> android:background="@color/color_131313">
@@ -9,14 +8,21 @@
<!-- 상단 툴바 --> <!-- 상단 툴바 -->
<include <include
android:id="@+id/detail_toolbar" android:id="@+id/detail_toolbar"
layout="@layout/detail_toolbar" /> layout="@layout/detail_toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout" android:id="@+id/tab_layout"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/detail_toolbar"
android:background="@color/color_131313" android:background="@color/color_131313"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/detail_toolbar"
app:tabIndicatorColor="@color/color_3bb9f1" app:tabIndicatorColor="@color/color_3bb9f1"
app:tabIndicatorFullWidth="true" app:tabIndicatorFullWidth="true"
app:tabIndicatorHeight="4dp" app:tabIndicatorHeight="4dp"
@@ -24,515 +30,14 @@
app:tabTextAppearance="@style/tabText" app:tabTextAppearance="@style/tabText"
app:tabTextColor="@color/color_b0bec5" /> app:tabTextColor="@color/color_b0bec5" />
<!-- 메인 스크롤 영역 (상세 탭에서만 표시) -->
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_view_character_detail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/btn_chat"
android:layout_below="@+id/tab_layout"
android:clipToPadding="false"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 캐릭터 이미지 및 프로필 영역 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/rl_character_profile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<!-- 캐릭터 배경 이미지 -->
<ImageView
android:id="@+id/iv_character_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@null"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 캐릭터 정보 -->
<LinearLayout
android:id="@+id/ll_character_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="16dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<!-- 캐릭터명과 상태 -->
<LinearLayout
android:id="@+id/ll_character_name_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_character_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"
android:textColor="@color/white"
android:textSize="26sp"
tools:text="캐릭터명" />
<TextView
android:id="@+id/tv_character_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="@drawable/bg_character_status_clone"
android:fontFamily="@font/pretendard_regular"
android:paddingHorizontal="5dp"
android:paddingVertical="1dp"
android:textColor="@color/white"
android:textSize="12sp"
tools:text="Clone" />
</LinearLayout>
<!-- 캐릭터 소개 -->
<TextView
android:id="@+id/tv_character_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/pretendard_regular"
android:textColor="@color/color_b0bec5"
android:textSize="18sp"
tools:text="캐릭터 한줄 소개" />
<!-- 태그 -->
<TextView
android:id="@+id/tv_character_tags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/pretendard_regular"
android:textColor="@color/color_3bb9f1"
android:textSize="14sp"
tools:text="#커버곡 #라이브 #연애 #썸 #채팅 #라방" />
</LinearLayout>
<!-- 세계관 섹션 -->
<LinearLayout
android:id="@+id/ll_worldview_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="16dp"
android:orientation="vertical">
<!-- 섹션 제목 -->
<TextView
android:id="@+id/tv_worldview_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"
android:text="[세계관 및 작품 소개]"
android:textColor="@color/white"
android:textSize="16sp" />
<!-- 세계관 내용 -->
<TextView
android:id="@+id/tv_worldview_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/pretendard_regular"
android:lineSpacingExtra="4dp"
android:textColor="@color/color_b0bec5"
android:textSize="16sp"
tools:text="특별한 꽃을 길러낼 수 있는 능력을 가진 리엘라.\n\n그녀는 호손 공작의 상속자가 되고 말아버리는데..." />
<!-- 더보기 버튼 -->
<LinearLayout
android:id="@+id/ll_worldview_expand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/iv_worldview_expand"
android:layout_width="20dp"
android:layout_height="20dp"
android:contentDescription="@null"
android:src="@drawable/ic_chevron_down" />
<TextView
android:id="@+id/tv_worldview_expand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_regular"
android:text="더보기"
android:textColor="@color/color_607d8b"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<!-- 원작 섹션 -->
<LinearLayout
android:id="@+id/ll_original_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="36dp"
android:layout_marginBottom="16dp"
android:orientation="vertical">
<!-- 섹션 제목 -->
<TextView
android:id="@+id/tv_original_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"
android:text="[원작]"
android:textColor="@color/white"
android:textSize="16sp" />
<!-- 원작 내용 -->
<TextView
android:id="@+id/tv_original_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:fontFamily="@font/pretendard_regular"
android:maxLines="3"
android:textColor="@color/color_b0bec5"
android:textSize="16sp"
tools:text="네이버 시리즈 독 안에 든 선생님" />
<!-- 원작 보러가기 버튼 -->
<TextView
android:id="@+id/tv_original_link"
android:layout_width="match_parent"
android:layout_height="54dp"
android:layout_marginTop="8dp"
android:background="@drawable/bg_round_corner_16_stroke_3bb9f1"
android:fontFamily="@font/pretendard_bold"
android:gravity="center"
android:text="원작 보러가기"
android:textColor="@color/color_3bb9f1"
android:textSize="16sp" />
</LinearLayout>
<!-- 성격 섹션 (세계관과 동일 UI) -->
<LinearLayout
android:id="@+id/ll_personality_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="16dp"
android:orientation="vertical">
<!-- 섹션 제목 -->
<TextView
android:id="@+id/tv_personality_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"
android:text="[성격 및 특징]"
android:textColor="@color/white"
android:textSize="16sp" />
<!-- 성격 내용 -->
<TextView
android:id="@+id/tv_personality_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/pretendard_regular"
android:lineSpacingExtra="4dp"
android:textColor="@color/color_b0bec5"
android:textSize="16sp"
tools:text="밝고 쾌활하지만 때로는 고집이 센 면모도 있습니다.\n\n친구를 소중히 여기며, 어려움 앞에서도 물러서지 않습니다." />
<!-- 더보기 버튼 -->
<LinearLayout
android:id="@+id/ll_personality_expand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/iv_personality_expand"
android:layout_width="20dp"
android:layout_height="20dp"
android:contentDescription="@null"
android:src="@drawable/ic_chevron_down" />
<TextView
android:id="@+id/tv_personality_expand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_regular"
android:text="더보기"
android:textColor="@color/color_607d8b"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<!-- 캐릭터톡 대화 가이드 섹션 -->
<LinearLayout
android:id="@+id/ll_chat_guide_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="24dp"
android:background="@drawable/bg_round_corner_16_stroke_37474f"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp">
<!-- 가이드 제목 -->
<TextView
android:id="@+id/tv_chat_guide_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"
android:text="⚠️ 캐릭터톡 대화 가이드"
android:textColor="@color/color_b0bec5"
android:textSize="16sp" />
<!-- 가이드 내용 -->
<TextView
android:id="@+id/tv_chat_guide_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/pretendard_regular"
android:lineSpacingExtra="4dp"
android:text="보이스온 AI캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다.\n세계관 속 캐릭터로 대화를 하거나 새로운 인물로 캐릭터와 당신만의 스토리를 만들어보세요."
android:textColor="@color/color_7c7c80"
android:textSize="16sp" />
<!-- 주의사항 -->
<TextView
android:id="@+id/tv_chat_guide_notice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/pretendard_regular"
android:lineSpacingExtra="4dp"
android:text="※ AI캐릭터톡은 오픈베타 서비스 중이며, 캐릭터의 대화가 어색하거나 불완전할 수 있습니다. 대화 초반에 캐릭터 붕괴가 느껴진다면 대화를 리셋하고 다시 시도해보세요."
android:textColor="@color/color_7c7c80"
android:textSize="14sp" />
</LinearLayout>
<!-- 댓글 섹션 -->
<LinearLayout
android:id="@+id/ll_comments_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="24dp"
android:background="@drawable/bg_round_corner_10_263238"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp">
<!-- 헤더: 댓글 (댓글 수) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_comments_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_regular"
android:text="댓글"
android:textColor="@color/color_b0bec5"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_comments_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:fontFamily="@font/pretendard_bold"
android:textColor="@color/color_b0bec5"
android:textSize="16sp"
tools:text="0" />
</LinearLayout>
<!-- 내용 컨테이너 -->
<LinearLayout
android:id="@+id/ll_comments_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="vertical">
<!-- 댓글 있을 때 -->
<LinearLayout
android:id="@+id/ll_latest_comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/iv_comment_profile"
android:layout_width="36dp"
android:layout_height="36dp"
android:contentDescription="@null"
android:src="@drawable/ic_placeholder_profile" />
<TextView
android:id="@+id/tv_latest_comment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:fontFamily="@font/pretendard_regular"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="가장 최근 댓글 내용이 여기에 표시됩니다." />
</LinearLayout>
<!-- 댓글 없을 때 -->
<LinearLayout
android:id="@+id/ll_no_comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_my_profile"
android:layout_width="36dp"
android:layout_height="36dp"
android:contentDescription="@null"
android:src="@drawable/ic_placeholder_profile" />
<LinearLayout
android:id="@+id/ll_comment_input_box"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:background="@drawable/bg_round_corner_5_stroke_white"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<EditText
android:id="@+id/et_comment_input"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:background="@android:color/transparent"
android:fontFamily="@font/pretendard_regular"
android:hint="댓글을 입력해보세요"
android:imeOptions="actionSend"
android:importantForAutofill="no"
android:inputType="textCapSentences|textMultiLine"
android:maxLines="3"
android:padding="0dp"
android:textColor="@color/white"
android:textColorHint="@color/color_7c7c80"
android:textSize="14sp"
tools:ignore="NestedWeights" />
<ImageView
android:id="@+id/iv_send_comment"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@null"
android:src="@drawable/ic_message_send" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- 장르의 다른 캐릭터 섹션 -->
<LinearLayout
android:id="@+id/ll_other_characters_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:orientation="vertical">
<!-- 섹션 제목 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="24dp">
<TextView
android:id="@+id/tv_other_characters_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:fontFamily="@font/pretendard_bold"
android:text="장르의 다른 캐릭터"
android:textColor="@color/white"
android:textSize="26sp" />
</LinearLayout>
<!-- 캐릭터 리스트 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_other_characters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<!-- 갤러리 탭 컨테이너 --> <!-- 갤러리 탭 컨테이너 -->
<FrameLayout <FrameLayout
android:id="@+id/fl_container" android:id="@+id/fl_container"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_above="@+id/btn_chat" app:layout_constraintBottom_toBottomOf="parent"
android:layout_below="@+id/tab_layout" app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone" /> app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tab_layout" />
<!-- 하단 고정 대화하기 버튼 --> </androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/btn_chat"
android:layout_width="match_parent"
android:layout_height="54dp"
android:layout_alignParentBottom="true"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="10dp"
android:background="@drawable/bg_round_corner_16_solid_3bb9f1"
android:fontFamily="@font/pretendard_bold"
android:gravity="center"
android:text="대화하기"
android:textColor="@color/white"
android:textSize="16sp" />
</RelativeLayout>

View File

@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="51.7dp" android:layout_height="51.7dp"
android:background="@color/black" android:background="@color/color_131313"
android:paddingHorizontal="13.3dp"> android:paddingHorizontal="13.3dp">
<TextView <TextView
@@ -13,9 +13,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:drawablePadding="6.7dp" android:drawablePadding="6.7dp"
android:ellipsize="end"
android:fontFamily="@font/gmarket_sans_bold" android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center" android:gravity="center"
android:ellipsize="end"
android:minHeight="48dp" android:minHeight="48dp"
android:textColor="@color/color_eeeeee" android:textColor="@color/color_eeeeee"
android:textSize="18.3sp" android:textSize="18.3sp"

View File

@@ -1,9 +1,518 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/color_131313"> android:background="@color/color_131313">
<!-- TODO: 기존 상세 화면 UI를 이 레이아웃으로 이전 예정 --> <!-- 메인 스크롤 영역 (상세 탭에서만 표시) -->
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_view_character_detail"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_above="@+id/btn_chat"
android:clipToPadding="false"
android:fillViewport="true"
app:layout_constraintBottom_toTopOf="@+id/btn_chat"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</FrameLayout> <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:orientation="vertical">
<!-- 캐릭터 이미지 및 프로필 영역 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/rl_character_profile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<!-- 캐릭터 배경 이미지 -->
<ImageView
android:id="@+id/iv_character_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@null"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 캐릭터 정보 -->
<LinearLayout
android:id="@+id/ll_character_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="16dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<!-- 캐릭터명과 상태 -->
<LinearLayout
android:id="@+id/ll_character_name_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_character_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"
android:textColor="@color/white"
android:textSize="26sp"
tools:text="캐릭터명" />
<TextView
android:id="@+id/tv_character_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="@drawable/bg_character_status_clone"
android:fontFamily="@font/pretendard_regular"
android:paddingHorizontal="5dp"
android:paddingVertical="1dp"
android:textColor="@color/white"
android:textSize="12sp"
tools:text="Clone" />
</LinearLayout>
<!-- 캐릭터 소개 -->
<TextView
android:id="@+id/tv_character_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/pretendard_regular"
android:textColor="@color/color_b0bec5"
android:textSize="18sp"
tools:text="캐릭터 한줄 소개" />
<!-- 태그 -->
<TextView
android:id="@+id/tv_character_tags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/pretendard_regular"
android:textColor="@color/color_3bb9f1"
android:textSize="14sp"
tools:text="#커버곡 #라이브 #연애 #썸 #채팅 #라방" />
</LinearLayout>
<!-- 세계관 섹션 -->
<LinearLayout
android:id="@+id/ll_worldview_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="16dp"
android:orientation="vertical">
<!-- 섹션 제목 -->
<TextView
android:id="@+id/tv_worldview_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"
android:text="[세계관 및 작품 소개]"
android:textColor="@color/white"
android:textSize="16sp" />
<!-- 세계관 내용 -->
<TextView
android:id="@+id/tv_worldview_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/pretendard_regular"
android:lineSpacingExtra="4dp"
android:textColor="@color/color_b0bec5"
android:textSize="16sp"
tools:text="특별한 꽃을 길러낼 수 있는 능력을 가진 리엘라.\n\n그녀는 호손 공작의 상속자가 되고 말아버리는데..." />
<!-- 더보기 버튼 -->
<LinearLayout
android:id="@+id/ll_worldview_expand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/iv_worldview_expand"
android:layout_width="20dp"
android:layout_height="20dp"
android:contentDescription="@null"
android:src="@drawable/ic_chevron_down" />
<TextView
android:id="@+id/tv_worldview_expand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_regular"
android:text="더보기"
android:textColor="@color/color_607d8b"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<!-- 원작 섹션 -->
<LinearLayout
android:id="@+id/ll_original_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="36dp"
android:layout_marginBottom="16dp"
android:orientation="vertical">
<!-- 섹션 제목 -->
<TextView
android:id="@+id/tv_original_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"
android:text="[원작]"
android:textColor="@color/white"
android:textSize="16sp" />
<!-- 원작 내용 -->
<TextView
android:id="@+id/tv_original_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:fontFamily="@font/pretendard_regular"
android:maxLines="3"
android:textColor="@color/color_b0bec5"
android:textSize="16sp"
tools:text="네이버 시리즈 독 안에 든 선생님" />
<!-- 원작 보러가기 버튼 -->
<TextView
android:id="@+id/tv_original_link"
android:layout_width="match_parent"
android:layout_height="54dp"
android:layout_marginTop="8dp"
android:background="@drawable/bg_round_corner_16_stroke_3bb9f1"
android:fontFamily="@font/pretendard_bold"
android:gravity="center"
android:text="원작 보러가기"
android:textColor="@color/color_3bb9f1"
android:textSize="16sp" />
</LinearLayout>
<!-- 성격 섹션 (세계관과 동일 UI) -->
<LinearLayout
android:id="@+id/ll_personality_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="16dp"
android:orientation="vertical">
<!-- 섹션 제목 -->
<TextView
android:id="@+id/tv_personality_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"
android:text="[성격 및 특징]"
android:textColor="@color/white"
android:textSize="16sp" />
<!-- 성격 내용 -->
<TextView
android:id="@+id/tv_personality_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/pretendard_regular"
android:lineSpacingExtra="4dp"
android:textColor="@color/color_b0bec5"
android:textSize="16sp"
tools:text="밝고 쾌활하지만 때로는 고집이 센 면모도 있습니다.\n\n친구를 소중히 여기며, 어려움 앞에서도 물러서지 않습니다." />
<!-- 더보기 버튼 -->
<LinearLayout
android:id="@+id/ll_personality_expand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/iv_personality_expand"
android:layout_width="20dp"
android:layout_height="20dp"
android:contentDescription="@null"
android:src="@drawable/ic_chevron_down" />
<TextView
android:id="@+id/tv_personality_expand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_regular"
android:text="더보기"
android:textColor="@color/color_607d8b"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<!-- 캐릭터톡 대화 가이드 섹션 -->
<LinearLayout
android:id="@+id/ll_chat_guide_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="24dp"
android:background="@drawable/bg_round_corner_16_stroke_37474f"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp">
<!-- 가이드 제목 -->
<TextView
android:id="@+id/tv_chat_guide_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"
android:text="⚠️ 캐릭터톡 대화 가이드"
android:textColor="@color/color_b0bec5"
android:textSize="16sp" />
<!-- 가이드 내용 -->
<TextView
android:id="@+id/tv_chat_guide_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/pretendard_regular"
android:lineSpacingExtra="4dp"
android:text="보이스온 AI캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다.\n세계관 속 캐릭터로 대화를 하거나 새로운 인물로 캐릭터와 당신만의 스토리를 만들어보세요."
android:textColor="@color/color_7c7c80"
android:textSize="16sp" />
<!-- 주의사항 -->
<TextView
android:id="@+id/tv_chat_guide_notice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/pretendard_regular"
android:lineSpacingExtra="4dp"
android:text="※ AI캐릭터톡은 오픈베타 서비스 중이며, 캐릭터의 대화가 어색하거나 불완전할 수 있습니다. 대화 초반에 캐릭터 붕괴가 느껴진다면 대화를 리셋하고 다시 시도해보세요."
android:textColor="@color/color_7c7c80"
android:textSize="14sp" />
</LinearLayout>
<!-- 댓글 섹션 -->
<LinearLayout
android:id="@+id/ll_comments_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="24dp"
android:background="@drawable/bg_round_corner_10_263238"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp">
<!-- 헤더: 댓글 (댓글 수) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_comments_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_regular"
android:text="댓글"
android:textColor="@color/color_b0bec5"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_comments_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:fontFamily="@font/pretendard_bold"
android:textColor="@color/color_b0bec5"
android:textSize="16sp"
tools:text="0" />
</LinearLayout>
<!-- 내용 컨테이너 -->
<LinearLayout
android:id="@+id/ll_comments_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="vertical">
<!-- 댓글 있을 때 -->
<LinearLayout
android:id="@+id/ll_latest_comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/iv_comment_profile"
android:layout_width="36dp"
android:layout_height="36dp"
android:contentDescription="@null"
android:src="@drawable/ic_placeholder_profile" />
<TextView
android:id="@+id/tv_latest_comment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:fontFamily="@font/pretendard_regular"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="가장 최근 댓글 내용이 여기에 표시됩니다." />
</LinearLayout>
<!-- 댓글 없을 때 -->
<LinearLayout
android:id="@+id/ll_no_comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_my_profile"
android:layout_width="36dp"
android:layout_height="36dp"
android:contentDescription="@null"
android:src="@drawable/ic_placeholder_profile" />
<LinearLayout
android:id="@+id/ll_comment_input_box"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:background="@drawable/bg_round_corner_5_stroke_white"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<EditText
android:id="@+id/et_comment_input"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:background="@android:color/transparent"
android:fontFamily="@font/pretendard_regular"
android:hint="댓글을 입력해보세요"
android:imeOptions="actionSend"
android:importantForAutofill="no"
android:inputType="textCapSentences|textMultiLine"
android:maxLines="3"
android:padding="0dp"
android:textColor="@color/white"
android:textColorHint="@color/color_7c7c80"
android:textSize="14sp"
tools:ignore="NestedWeights" />
<ImageView
android:id="@+id/iv_send_comment"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@null"
android:src="@drawable/ic_message_send" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- 장르의 다른 캐릭터 섹션 -->
<LinearLayout
android:id="@+id/ll_other_characters_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:orientation="vertical">
<!-- 섹션 제목 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="24dp">
<TextView
android:id="@+id/tv_other_characters_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:fontFamily="@font/pretendard_bold"
android:text="장르의 다른 캐릭터"
android:textColor="@color/white"
android:textSize="26sp" />
</LinearLayout>
<!-- 캐릭터 리스트 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_other_characters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<!-- 하단 고정 대화하기 버튼 -->
<TextView
android:id="@+id/btn_chat"
android:layout_width="0dp"
android:layout_height="54dp"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="10dp"
android:background="@drawable/bg_round_corner_16_solid_3bb9f1"
android:fontFamily="@font/pretendard_bold"
android:gravity="center"
android:text="대화하기"
android:textColor="@color/white"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -5,6 +5,7 @@ import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterType
import kr.co.vividnext.sodalive.chat.talk.TalkApi import kr.co.vividnext.sodalive.chat.talk.TalkApi
import kr.co.vividnext.sodalive.chat.talk.room.db.ChatMessageDao import kr.co.vividnext.sodalive.chat.talk.room.db.ChatMessageDao
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
@@ -23,7 +24,7 @@ class ChatRepositoryTest {
ServerChatMessage(1, "a1", "", mine = false, createdAt = 1000L), ServerChatMessage(1, "a1", "", mine = false, createdAt = 1000L),
ServerChatMessage(2, "u1", "", mine = true, createdAt = 2000L) ServerChatMessage(2, "u1", "", mine = true, createdAt = 2000L)
) )
val character = CharacterInfo(10, "name", "", kr.co.vividnext.sodalive.chat.character.detail.CharacterType.CLONE) val character = CharacterInfo(10, "name", "", CharacterType.CLONE)
val resp = ChatRoomEnterResponse(99, character, serverMessages, hasMoreMessages = false) val resp = ChatRoomEnterResponse(99, character, serverMessages, hasMoreMessages = false)
every { api.enterChatRoom(any(), any()) } returns Single.just(ApiResponse(true, resp, null)) every { api.enterChatRoom(any(), any()) } returns Single.just(ApiResponse(true, resp, null))