feat(character): 캐릭터 탭 UI 및 기본 기능 구현

This commit is contained in:
2025-08-04 20:27:33 +09:00
parent e90222e8db
commit b919691689
12 changed files with 665 additions and 14 deletions

View File

@@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.chat.character
data class Character(
val id: String,
val name: String,
val description: String,
val imageUrl: String,
val ranking: Int? = null // 인기 캐릭터에서만 사용
)

View File

@@ -0,0 +1,63 @@
package kr.co.vividnext.sodalive.chat.character
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.chat.character.Character
class CharacterAdapter(
private var characters: List<Character> = emptyList(),
private val showRanking: Boolean = false,
private val onCharacterClick: (Character) -> Unit = {}
) : RecyclerView.Adapter<CharacterAdapter.ViewHolder>() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val ivCharacter: ImageView = itemView.findViewById(R.id.iv_character)
val tvCharacterName: TextView = itemView.findViewById(R.id.tv_character_name)
val tvCharacterDescription: TextView = itemView.findViewById(R.id.tv_character_description)
val llRanking: LinearLayout = itemView.findViewById(R.id.ll_ranking)
val tvRanking: TextView = itemView.findViewById(R.id.tv_ranking)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_character, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val character = characters[position]
holder.tvCharacterName.text = character.name
holder.tvCharacterDescription.text = character.description
// 순위 표시 여부 결정
if (showRanking && character.ranking != null) {
holder.llRanking.visibility = View.VISIBLE
holder.tvRanking.text = character.ranking.toString()
} else {
holder.llRanking.visibility = View.GONE
}
// TODO: 이미지 로딩 라이브러리 사용 (Glide, Picasso 등)
// Glide.with(holder.itemView.context)
// .load(character.imageUrl)
// .into(holder.ivCharacter)
holder.itemView.setOnClickListener {
onCharacterClick(character)
}
}
override fun getItemCount(): Int = characters.size
fun updateCharacters(newCharacters: List<Character>) {
characters = newCharacters
notifyDataSetChanged()
}
}

View File

@@ -2,15 +2,162 @@ package kr.co.vividnext.sodalive.chat.character
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import kr.co.vividnext.sodalive.base.BaseFragment import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.chat.character.curation.CurationSection
import kr.co.vividnext.sodalive.chat.character.curation.CurationSectionAdapter
import kr.co.vividnext.sodalive.chat.character.recent.RecentCharacter
import kr.co.vividnext.sodalive.chat.character.recent.RecentCharacterAdapter
import kr.co.vividnext.sodalive.databinding.FragmentCharacterTabBinding import kr.co.vividnext.sodalive.databinding.FragmentCharacterTabBinding
// 캐릭터 탭 프래그먼트 // 캐릭터 탭 프래그먼트
class CharacterTabFragment : BaseFragment<FragmentCharacterTabBinding>( class CharacterTabFragment : BaseFragment<FragmentCharacterTabBinding>(
FragmentCharacterTabBinding::inflate FragmentCharacterTabBinding::inflate
) { ) {
private lateinit var recentCharacterAdapter: RecentCharacterAdapter
private lateinit var popularCharacterAdapter: CharacterAdapter
private lateinit var newCharacterAdapter: CharacterAdapter
private lateinit var curationSectionAdapter: CurationSectionAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// 캐릭터 탭 초기화 로직 setupRecyclerViews()
loadData()
}
private fun setupRecyclerViews() {
// 최근 대화한 캐릭터 RecyclerView 설정
recentCharacterAdapter = RecentCharacterAdapter { character ->
// 캐릭터 클릭 처리
onRecentCharacterClick(character)
}
binding.rvRecentCharacters.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
adapter = recentCharacterAdapter
}
// 인기 캐릭터 RecyclerView 설정 (순위 표시)
popularCharacterAdapter = CharacterAdapter(
showRanking = true
) { character ->
// 캐릭터 클릭 처리
onCharacterClick(character)
}
binding.rvPopularCharacters.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
adapter = popularCharacterAdapter
}
// 신규 캐릭터 RecyclerView 설정
newCharacterAdapter = CharacterAdapter(
showRanking = false
) { character ->
// 캐릭터 클릭 처리
onCharacterClick(character)
}
binding.rvNewCharacters.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
adapter = newCharacterAdapter
}
// 큐레이션 섹션 RecyclerView 설정
curationSectionAdapter = CurationSectionAdapter { character ->
// 캐릭터 클릭 처리
onCharacterClick(character)
}
binding.rvCurationSections.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = curationSectionAdapter
}
// 전체보기 버튼 클릭 리스너
binding.tvFamousCharacterAll.setOnClickListener {
// 인기 캐릭터 전체보기 처리
}
binding.tvNewCharacterAll.setOnClickListener {
// 신규 캐릭터 전체보기 처리
}
}
private fun loadData() {
// TODO: 실제 데이터 로딩 로직 구현
loadRecentCharacters()
loadPopularCharacters()
loadNewCharacters()
loadCurationSections()
}
private fun loadRecentCharacters() {
// TODO: 서버에서 최근 대화한 캐릭터 데이터 로드
val recentCharacters = listOf(
RecentCharacter("1", "Yubin...", ""),
RecentCharacter("2", "Yubin...", ""),
RecentCharacter("3", "Yubin...", ""),
RecentCharacter("4", "Yubin...", ""),
RecentCharacter("5", "Yubin...", "")
)
recentCharacterAdapter.updateCharacters(recentCharacters)
binding.tvLatestCharacterCount.text = recentCharacters.size.toString()
}
private fun loadPopularCharacters() {
// TODO: 서버에서 인기 캐릭터 데이터 로드
val popularCharacters = listOf(
Character("1", "캐릭터 이름", "캐릭터 한줄 소개인데 2줄까", "", 1),
Character("2", "캐릭터 이름", "캐릭터 한줄 소개인데 2줄까", "", 2),
Character("3", "캐릭터 이름", "#태그#태그#태그", "", 3),
Character("4", "캐릭터 이름", "캐릭터 한줄 소개인데 2줄까", "", 4),
Character("5", "캐릭터 이름", "캐릭터 한줄 소개인데 2줄까", "", 5)
)
popularCharacterAdapter.updateCharacters(popularCharacters)
}
private fun loadNewCharacters() {
// TODO: 서버에서 신규 캐릭터 데이터 로드
val newCharacters = listOf(
Character("1", "캐릭터 이름", "캐릭터 한줄 소개인데 2줄까", ""),
Character("2", "하이퍼나이프", "캐릭터 한줄 소개인데 2줄까", ""),
Character("3", "내일", "#태그#태그", "")
)
newCharacterAdapter.updateCharacters(newCharacters)
}
private fun loadCurationSections() {
// TODO: 서버에서 큐레이션 섹션 데이터 로드
val curationSections = listOf(
CurationSection(
"1",
"큐레이션",
listOf(
Character("1", "캐릭터 이름", "캐릭터 한줄 소개인데 2줄까", ""),
Character("2", "하이퍼나이프", "캐릭터 한줄 소개인데 2줄까", ""),
Character("3", "내일", "#태그#태그", "")
)
),
CurationSection(
"2",
"큐레이션",
listOf(
Character("4", "캐릭터 이름", "캐릭터 한줄 소개인데 2줄까", ""),
Character("5", "하이퍼나이프", "캐릭터 한줄 소개인데 2줄까", ""),
Character("6", "내일", "#태그#태그", "")
)
)
)
curationSectionAdapter.updateSections(curationSections)
}
private fun onRecentCharacterClick(character: RecentCharacter) {
// TODO: 최근 대화한 캐릭터 클릭 처리
}
private fun onCharacterClick(character: Character) {
// TODO: 캐릭터 클릭 처리
} }
} }

View File

@@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.chat.character.curation
import kr.co.vividnext.sodalive.chat.character.Character
data class CurationSection(
val id: String,
val title: String,
val characters: List<Character>
)

View File

@@ -0,0 +1,54 @@
package kr.co.vividnext.sodalive.chat.character.curation
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.chat.character.CharacterAdapter
import kr.co.vividnext.sodalive.chat.character.Character
import kr.co.vividnext.sodalive.chat.character.curation.CurationSection
class CurationSectionAdapter(
private var sections: List<CurationSection> = emptyList(),
private val onCharacterClick: (Character) -> Unit = {}
) : RecyclerView.Adapter<CurationSectionAdapter.ViewHolder>() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val tvSectionTitle: TextView = itemView.findViewById(R.id.tv_section_title)
val rvCharacters: RecyclerView = itemView.findViewById(R.id.rv_characters)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_curation_section, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val section = sections[position]
holder.tvSectionTitle.text = section.title
// 캐릭터 리스트 설정
val characterAdapter = CharacterAdapter(
characters = section.characters,
showRanking = false,
onCharacterClick = onCharacterClick
)
holder.rvCharacters.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
adapter = characterAdapter
}
}
override fun getItemCount(): Int = sections.size
fun updateSections(newSections: List<CurationSection>) {
sections = newSections
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,7 @@
package kr.co.vividnext.sodalive.chat.character.recent
data class RecentCharacter(
val id: String,
val name: String,
val profileImageUrl: String
)

View File

@@ -0,0 +1,50 @@
package kr.co.vividnext.sodalive.chat.character.recent
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
class RecentCharacterAdapter(
private var characters: List<RecentCharacter> = emptyList(),
private val onCharacterClick: (RecentCharacter) -> Unit = {}
) : RecyclerView.Adapter<RecentCharacterAdapter.ViewHolder>() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val ivProfile: ImageView = itemView.findViewById(R.id.iv_profile)
val tvName: TextView = itemView.findViewById(R.id.tv_name)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_recent_character, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val character = characters[position]
holder.tvName.text = character.name
// TODO: 이미지 로딩 라이브러리 사용 (Glide, Picasso 등)
// Glide.with(holder.itemView.context)
// .load(character.profileImageUrl)
// .into(holder.ivProfile)
holder.itemView.setOnClickListener {
onCharacterClick(character)
}
}
override fun getItemCount(): Int = characters.size
@SuppressLint("NotifyDataSetChanged")
fun updateCharacters(newCharacters: List<RecentCharacter>) {
characters = newCharacters
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="135"
android:startColor="#80000000"
android:centerColor="#99000000"
android:endColor="#CC000000"
android:type="linear" />
<corners
android:bottomLeftRadius="16dp"
android:bottomRightRadius="0dp"
android:topLeftRadius="0dp"
android:topRightRadius="0dp" />
</shape>

View File

@@ -1,19 +1,183 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.core.widget.NestedScrollView 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/black"> android:background="@color/color_131313"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 배너 섹션 -->
<LinearLayout
android:id="@+id/ll_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
android:orientation="vertical">
<com.zhpan.bannerview.BannerViewPager
android:id="@+id/event_banner_slider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false" />
<com.zhpan.indicator.IndicatorView
android:id="@+id/indicator_event_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="28dp" />
</LinearLayout>
<!-- 최근 대화한 캐릭터 섹션 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
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 <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="캐릭터 탭" android:fontFamily="@font/pretendard_bold"
android:text="최근 대화한 캐릭터"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="20sp" android:textSize="20sp" />
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> <TextView
android:id="@+id/tv_latest_character_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:fontFamily="@font/pretendard_bold"
android:textColor="@color/color_fdca2f"
android:textSize="20sp"
tools:text="14" />
</LinearLayout>
<!-- 프로필 리스트 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_recent_characters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
</LinearLayout>
<!-- 인기 캐릭터 섹션 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
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: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="24sp" />
<TextView
android:id="@+id/tv_famous_character_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_regular"
android:text="전체보기"
android:textColor="#90A4AE"
android:textSize="14sp" />
</LinearLayout>
<!-- 캐릭터 카드 리스트 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_popular_characters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingStart="24dp" />
</LinearLayout>
<!-- 신규 캐릭터 섹션 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
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: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="24sp" />
<TextView
android:id="@+id/tv_new_character_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_regular"
android:text="전체보기"
android:textColor="#90A4AE"
android:textSize="14sp" />
</LinearLayout>
<!-- 캐릭터 카드 리스트 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_new_characters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingStart="24dp" />
</LinearLayout>
<!-- 큐레이션 섹션들 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_curation_sections"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="168dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_character"
android:layout_width="168dp"
android:layout_height="168dp"
android:background="@color/color_777777"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="4dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_character_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_regular"
android:textColor="@color/color_b0bec5"
android:textSize="18sp" />
<TextView
android:id="@+id/tv_character_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="4dp"
android:fontFamily="@font/pretendard_regular"
android:textColor="#78909C"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<!-- 순위 표시 (인기 캐릭터에서만 보임) -->
<LinearLayout
android:id="@+id/ll_ranking"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@drawable/gradient_ranking_bg"
android:gravity="center"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
android:visibility="gone">
<TextView
android:id="@+id/tv_ranking"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"
android:textColor="@color/white"
android:textSize="72sp" />
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
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_section_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:fontFamily="@font/pretendard_bold"
android:textColor="@color/white"
android:textSize="24sp" />
</LinearLayout>
<!-- 캐릭터 카드 리스트 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_characters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingStart="24dp" />
</LinearLayout>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="76dp"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_profile"
android:layout_width="76dp"
android:layout_height="76dp"
android:background="@color/color_777777"
android:contentDescription="@null"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:ellipsize="end"
android:fontFamily="@font/pretendard_regular"
android:gravity="center"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="18sp" />
</LinearLayout>