refactor(mypage): 기능 버튼 렌더링 책임을 분리한다

This commit is contained in:
2026-04-02 12:22:08 +09:00
parent 8d8d5e340f
commit 336d411627
4 changed files with 89 additions and 50 deletions

View File

@@ -4,12 +4,9 @@ import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.webkit.URLUtil import android.webkit.URLUtil
import android.widget.Toast import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
@@ -23,13 +20,11 @@ import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.base.BaseFragment import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.FunctionButtonHelper
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.databinding.FragmentMyBinding import kr.co.vividnext.sodalive.databinding.FragmentMyBinding
import kr.co.vividnext.sodalive.databinding.ItemFunctionButtonBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.extensions.moneyFormat
@@ -42,6 +37,8 @@ import kr.co.vividnext.sodalive.mypage.block.BlockMemberActivity
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
import kr.co.vividnext.sodalive.mypage.can.coupon.CanCouponActivity import kr.co.vividnext.sodalive.mypage.can.coupon.CanCouponActivity
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusActivity import kr.co.vividnext.sodalive.mypage.can.status.CanStatusActivity
import kr.co.vividnext.sodalive.mypage.function_button.FunctionButtonAdapter
import kr.co.vividnext.sodalive.mypage.function_button.FunctionButtonItem
import kr.co.vividnext.sodalive.mypage.point.PointStatusActivity import kr.co.vividnext.sodalive.mypage.point.PointStatusActivity
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateActivity import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateActivity
import kr.co.vividnext.sodalive.mypage.recent.RecentContentAdapter import kr.co.vividnext.sodalive.mypage.recent.RecentContentAdapter
@@ -350,7 +347,12 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
} }
private fun updateFunctionButtons(isAuth: Boolean? = null) { private fun updateFunctionButtons(isAuth: Boolean? = null) {
functionButtonAdapter.submitList(buildFunctionButtonItems(isAuth))
}
private fun buildFunctionButtonItems(isAuth: Boolean?): List<FunctionButtonItem> {
val isKoreanUser = SharedPreferenceManager.countryCode.ifBlank { "KR" } == "KR" val isKoreanUser = SharedPreferenceManager.countryCode.ifBlank { "KR" } == "KR"
val items = mutableListOf( val items = mutableListOf(
FunctionButtonItem( FunctionButtonItem(
iconRes = R.drawable.ic_my_storage, iconRes = R.drawable.ic_my_storage,
@@ -468,7 +470,7 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
} }
} }
functionButtonAdapter.submitList(items) return items
} }
private fun showAuthDialog() { private fun showAuthDialog() {
@@ -497,47 +499,3 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
} }
} }
} }
private data class FunctionButtonItem(
@field:DrawableRes val iconRes: Int,
val title: String,
val onClick: () -> Unit
)
private class FunctionButtonAdapter : RecyclerView.Adapter<FunctionButtonAdapter.ViewHolder>() {
private val items = mutableListOf<FunctionButtonItem>()
inner class ViewHolder(
private val binding: ItemFunctionButtonBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: FunctionButtonItem) {
FunctionButtonHelper.setupFunctionButton(
buttonView = binding.root,
iconRes = item.iconRes,
title = item.title,
clickListener = View.OnClickListener { item.onClick() }
)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemFunctionButtonBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
@SuppressLint("NotifyDataSetChanged")
fun submitList(newItems: List<FunctionButtonItem>) {
items.clear()
items.addAll(newItems)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,47 @@
package kr.co.vividnext.sodalive.mypage.function_button
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.common.FunctionButtonHelper
import kr.co.vividnext.sodalive.databinding.ItemFunctionButtonBinding
class FunctionButtonAdapter : RecyclerView.Adapter<FunctionButtonAdapter.ViewHolder>() {
private val items = mutableListOf<FunctionButtonItem>()
inner class ViewHolder(
private val binding: ItemFunctionButtonBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: FunctionButtonItem) {
FunctionButtonHelper.setupFunctionButton(
buttonView = binding.root,
iconRes = item.iconRes,
title = item.title,
clickListener = View.OnClickListener { item.onClick() }
)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemFunctionButtonBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
@SuppressLint("NotifyDataSetChanged")
fun submitList(newItems: List<FunctionButtonItem>) {
items.clear()
items.addAll(newItems)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.mypage.function_button
import androidx.annotation.DrawableRes
data class FunctionButtonItem(
@param:DrawableRes val iconRes: Int,
val title: String,
val onClick: () -> Unit
)

View File

@@ -4,6 +4,7 @@
- 마이페이지 `btnCoupon` 터치 시 한국 사용자는 기존처럼 본인인증이 필요하고, 한국이 아닌 사용자는 본인인증 없이 쿠폰 등록 화면으로 이동하도록 수정한다. - 마이페이지 `btnCoupon` 터치 시 한국 사용자는 기존처럼 본인인증이 필요하고, 한국이 아닌 사용자는 본인인증 없이 쿠폰 등록 화면으로 이동하도록 수정한다.
- 마이페이지 `btnCoupon`은 한국이 아닌 사용자에 한해 민감한 콘텐츠 보기 설정이 켜져 있을 때만 화면에 보이도록 수정한다. - 마이페이지 `btnCoupon`은 한국이 아닌 사용자에 한해 민감한 콘텐츠 보기 설정이 켜져 있을 때만 화면에 보이도록 수정한다.
- 마이페이지 기능 버튼 영역을 `RecyclerView` 기반 그리드로 전환해 숨겨진 버튼이 있어도 중간 빈 슬롯 없이 왼쪽부터 자연스럽게 재배치되도록 수정한다. - 마이페이지 기능 버튼 영역을 `RecyclerView` 기반 그리드로 전환해 숨겨진 버튼이 있어도 중간 빈 슬롯 없이 왼쪽부터 자연스럽게 재배치되도록 수정한다.
- `MyPageFragment`에 혼합된 기능 버튼 모델/어댑터 책임을 분리해 화면 제어와 렌더링 관심사를 정리한다.
## 체크리스트 ## 체크리스트
- [x] AC1: `countryCode == "KR"` 이고 `isAuth == false`인 경우 기존처럼 인증 필요 토스트와 `showAuthDialog()`가 실행된다. - [x] AC1: `countryCode == "KR"` 이고 `isAuth == false`인 경우 기존처럼 인증 필요 토스트와 `showAuthDialog()`가 실행된다.
@@ -24,6 +25,10 @@
- QA: `fragment_my.xml``RecyclerView`를 사용하고, `MyPageFragment`가 쿠폰/본인인증 버튼을 조건에 따라 아이템 리스트에 포함/제외하는지 코드 확인 - QA: `fragment_my.xml``RecyclerView`를 사용하고, `MyPageFragment`가 쿠폰/본인인증 버튼을 조건에 따라 아이템 리스트에 포함/제외하는지 코드 확인
- [x] AC9: 쿠폰 버튼이 숨겨지는 비한국 사용자(`isAdultContentVisible == false`)에서도 기능 버튼 간 가로/세로 간격이 기존 4열 그리드와 동일하게 유지된다. - [x] AC9: 쿠폰 버튼이 숨겨지는 비한국 사용자(`isAdultContentVisible == false`)에서도 기능 버튼 간 가로/세로 간격이 기존 4열 그리드와 동일하게 유지된다.
- QA: `GridLayoutManager(4)``GridSpacingItemDecoration(..., 16dp, false)` 적용으로 기존 16dp 간격 패턴을 유지하는지 코드 확인 - QA: `GridLayoutManager(4)``GridSpacingItemDecoration(..., 16dp, false)` 적용으로 기존 16dp 간격 패턴을 유지하는지 코드 확인
- [x] AC10: `FunctionButtonItem``FunctionButtonAdapter``MyPageFragment` 밖 별도 파일로 분리되어 Fragment가 화면 상태/버튼 목록 조립에 집중한다.
- QA: 모델/어댑터 클래스가 별도 파일로 이동하고 `MyPageFragment` 하단 정의가 제거됐는지 코드 확인
- [x] AC11: 기능 버튼 조립 로직은 `buildFunctionButtonItems()`로 분리되며, 기존 버튼 순서/조건/클릭 동작은 유지된다.
- QA: `MyPageFragment``updateFunctionButtons()`에서 조립 함수 결과만 어댑터에 전달하는지, 버튼 분기 로직이 동일한지 코드 확인
## 검증 기록 ## 검증 기록
- 2026-04-02 - 2026-04-02
@@ -71,3 +76,23 @@
- 기존 버튼 제목, 아이콘, 클릭 액션, 한국/비한국 및 인증/민감 콘텐츠 조건은 그대로 유지됐다. - 기존 버튼 제목, 아이콘, 클릭 액션, 한국/비한국 및 인증/민감 콘텐츠 조건은 그대로 유지됐다.
- `.kt`/`.xml` 대상 `lsp_diagnostics`는 현재 환경에 Kotlin/XML LSP가 없어 실행 불가(`No LSP server configured for extension: .kt/.xml`)였다. - `.kt`/`.xml` 대상 `lsp_diagnostics`는 현재 환경에 Kotlin/XML LSP가 없어 실행 불가(`No LSP server configured for extension: .kt/.xml`)였다.
- `:app:testDebugUnitTest`, `:app:assembleDebug` 실행은 `BUILD SUCCESSFUL`로 완료됐다. - `:app:testDebugUnitTest`, `:app:assembleDebug` 실행은 `BUILD SUCCESSFUL`로 완료됐다.
- 2026-04-02
- 무엇/왜/어떻게: `RecyclerView` 전환 이후 `MyPageFragment` 안에 기능 버튼 모델, 어댑터, 버튼 목록 조립이 함께 들어와 관심사가 섞였다. 기존 `mypage` 패키지의 어댑터 분리 패턴에 맞춰 `FunctionButtonItem``FunctionButtonAdapter`를 별도 파일로 분리하고, Fragment에는 `buildFunctionButtonItems()`를 통한 버튼 목록 조립과 화면 제어만 남기도록 정리했다.
- 실행 명령/도구:
- `read(app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt)`
- `read(app/src/main/java/kr/co/vividnext/sodalive/mypage/recent/RecentContentAdapter.kt)`
- `read(app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/tag/MemberTagAdapter.kt)`
- `apply_patch(app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt)`
- `apply_patch(app/src/main/java/kr/co/vividnext/sodalive/mypage/function_button/FunctionButtonItem.kt)`
- `apply_patch(app/src/main/java/kr/co/vividnext/sodalive/mypage/function_button/FunctionButtonAdapter.kt)`
- `read(app/src/main/java/kr/co/vividnext/sodalive/mypage/function_button/FunctionButtonItem.kt)`
- `read(app/src/main/java/kr/co/vividnext/sodalive/mypage/function_button/FunctionButtonAdapter.kt)`
- `read(app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt)`
- `lsp_diagnostics(app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt)`
- `./gradlew :app:testDebugUnitTest :app:assembleDebug`
- 결과:
- `FunctionButtonItem``app/src/main/java/kr/co/vividnext/sodalive/mypage/function_button/FunctionButtonItem.kt`로, `FunctionButtonAdapter``.../FunctionButtonAdapter.kt`로 분리됐다.
- `MyPageFragment` 하단의 내부 모델/어댑터 정의는 제거됐고, `updateFunctionButtons()``buildFunctionButtonItems()` 결과를 어댑터에 전달하는 역할만 수행한다.
- 버튼 순서, 쿠폰 노출 조건, 한국/비한국 인증 분기, 각 버튼 클릭 동작은 유지됐다.
- `.kt` 대상 `lsp_diagnostics`는 현재 환경에 Kotlin LSP가 없어 실행 불가(`No LSP server configured for extension: .kt`)였다.
- `:app:testDebugUnitTest`, `:app:assembleDebug` 실행은 `BUILD SUCCESSFUL`로 완료됐다.