From b9196916897639d3ba1d785e4d7edfc096b768c8 Mon Sep 17 00:00:00 2001 From: klaus Date: Mon, 4 Aug 2025 20:27:33 +0900 Subject: [PATCH] =?UTF-8?q?feat(character):=20=EC=BA=90=EB=A6=AD=ED=84=B0?= =?UTF-8?q?=20=ED=83=AD=20UI=20=EB=B0=8F=20=EA=B8=B0=EB=B3=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/chat/character/Character.kt | 9 + .../chat/character/CharacterAdapter.kt | 63 ++++++ .../chat/character/CharacterTabFragment.kt | 149 +++++++++++++- .../character/curation/CurationSection.kt | 9 + .../curation/CurationSectionAdapter.kt | 54 +++++ .../chat/character/recent/RecentCharacter.kt | 7 + .../recent/RecentCharacterAdapter.kt | 50 +++++ .../main/res/drawable/gradient_ranking_bg.xml | 14 ++ .../res/layout/fragment_character_tab.xml | 190 ++++++++++++++++-- app/src/main/res/layout/item_character.xml | 71 +++++++ .../main/res/layout/item_curation_section.xml | 36 ++++ .../main/res/layout/item_recent_character.xml | 27 +++ 12 files changed, 665 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/chat/character/Character.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/chat/character/curation/CurationSection.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/chat/character/curation/CurationSectionAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/chat/character/recent/RecentCharacter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/chat/character/recent/RecentCharacterAdapter.kt create mode 100644 app/src/main/res/drawable/gradient_ranking_bg.xml create mode 100644 app/src/main/res/layout/item_character.xml create mode 100644 app/src/main/res/layout/item_curation_section.xml create mode 100644 app/src/main/res/layout/item_recent_character.xml diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/Character.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/Character.kt new file mode 100644 index 00000000..70bdaaaa --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/Character.kt @@ -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 // 인기 캐릭터에서만 사용 +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterAdapter.kt new file mode 100644 index 00000000..0ecb0fb0 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterAdapter.kt @@ -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 = emptyList(), + private val showRanking: Boolean = false, + private val onCharacterClick: (Character) -> Unit = {} +) : RecyclerView.Adapter() { + + 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) { + characters = newCharacters + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterTabFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterTabFragment.kt index afd279bc..8e430e82 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterTabFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterTabFragment.kt @@ -2,15 +2,162 @@ package kr.co.vividnext.sodalive.chat.character import android.os.Bundle import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager 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 // 캐릭터 탭 프래그먼트 class CharacterTabFragment : BaseFragment( 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?) { 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: 캐릭터 클릭 처리 } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/curation/CurationSection.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/curation/CurationSection.kt new file mode 100644 index 00000000..56caf2c7 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/curation/CurationSection.kt @@ -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 +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/curation/CurationSectionAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/curation/CurationSectionAdapter.kt new file mode 100644 index 00000000..22596049 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/curation/CurationSectionAdapter.kt @@ -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 = emptyList(), + private val onCharacterClick: (Character) -> Unit = {} +) : RecyclerView.Adapter() { + + 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) { + sections = newSections + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/recent/RecentCharacter.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/recent/RecentCharacter.kt new file mode 100644 index 00000000..fc572b36 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/recent/RecentCharacter.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.chat.character.recent + +data class RecentCharacter( + val id: String, + val name: String, + val profileImageUrl: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/recent/RecentCharacterAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/recent/RecentCharacterAdapter.kt new file mode 100644 index 00000000..3770ce14 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/recent/RecentCharacterAdapter.kt @@ -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 = emptyList(), + private val onCharacterClick: (RecentCharacter) -> Unit = {} +) : RecyclerView.Adapter() { + + 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) { + characters = newCharacters + notifyDataSetChanged() + } +} diff --git a/app/src/main/res/drawable/gradient_ranking_bg.xml b/app/src/main/res/drawable/gradient_ranking_bg.xml new file mode 100644 index 00000000..1c8a1573 --- /dev/null +++ b/app/src/main/res/drawable/gradient_ranking_bg.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_character_tab.xml b/app/src/main/res/layout/fragment_character_tab.xml index 81b23f62..3f4492e0 100644 --- a/app/src/main/res/layout/fragment_character_tab.xml +++ b/app/src/main/res/layout/fragment_character_tab.xml @@ -1,19 +1,183 @@ - + android:background="@color/color_131313" + android:fillViewport="true"> - + android:orientation="vertical"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_character.xml b/app/src/main/res/layout/item_character.xml new file mode 100644 index 00000000..180436de --- /dev/null +++ b/app/src/main/res/layout/item_character.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_curation_section.xml b/app/src/main/res/layout/item_curation_section.xml new file mode 100644 index 00000000..7f4a21c0 --- /dev/null +++ b/app/src/main/res/layout/item_curation_section.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_recent_character.xml b/app/src/main/res/layout/item_recent_character.xml new file mode 100644 index 00000000..63eaa677 --- /dev/null +++ b/app/src/main/res/layout/item_recent_character.xml @@ -0,0 +1,27 @@ + + + + + + +