From 8d8d5e340fe019cd3f89f7156e387bbac9de9fff Mon Sep 17 00:00:00 2001 From: klaus Date: Thu, 2 Apr 2026 12:12:59 +0900 Subject: [PATCH] =?UTF-8?q?fix(mypage):=20=EA=B8=B0=EB=8A=A5=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EB=B0=B0=EC=B9=98=EC=99=80=20=EC=BF=A0=ED=8F=B0=20?= =?UTF-8?q?=EB=B6=84=EA=B8=B0=EB=A5=BC=20=EC=A0=95=EB=A6=AC=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/mypage/MyPageFragment.kt | 309 +++++++++++------- app/src/main/res/layout/fragment_my.xml | 83 +---- .../main/res/layout/item_function_button.xml | 3 +- ...20260402_쿠폰등록해외사용자본인인증예외.md | 34 +- 4 files changed, 218 insertions(+), 211 deletions(-) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt index 745b08c4..c83a0133 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt @@ -4,11 +4,15 @@ import android.annotation.SuppressLint import android.content.Intent import android.graphics.Rect import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import android.webkit.URLUtil import android.widget.Toast +import androidx.annotation.DrawableRes import androidx.core.net.toUri import androidx.media3.common.util.UnstableApi +import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import coil.load @@ -20,10 +24,12 @@ import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity import kr.co.vividnext.sodalive.base.BaseFragment 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.LoadingDialog import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder 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.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.moneyFormat @@ -52,10 +58,15 @@ import org.koin.android.ext.android.inject @UnstableApi class MyPageFragment : BaseFragment(FragmentMyBinding::inflate) { + companion object { + private const val FUNCTION_BUTTON_SPAN_COUNT = 4 + } + private val viewModel: MyPageViewModel by inject() private val recentContentViewModel: RecentContentViewModel by inject() private lateinit var loadingDialog: LoadingDialog + private val functionButtonAdapter = FunctionButtonAdapter() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -187,6 +198,7 @@ class MyPageFragment : BaseFragment(FragmentMyBinding::inflat binding.llProfileLoginContainer.visibility = View.GONE binding.llFunctionButtonGrid.visibility = View.VISIBLE + setupFunctionButtonGrid() binding.ivSettings.setOnClickListener { startActivity( @@ -239,84 +251,6 @@ class MyPageFragment : BaseFragment(FragmentMyBinding::inflat } } - FunctionButtonHelper.setupFunctionButton( - buttonView = binding.btnStorage.root, - iconRes = R.drawable.ic_my_storage, - title = getString(R.string.screen_my_storage) - ) { - startActivity( - Intent( - requireContext(), - AudioContentBoxActivity::class.java - ) - ) - } - - FunctionButtonHelper.setupFunctionButton( - buttonView = binding.btnBlockList.root, - iconRes = R.drawable.ic_my_block, - title = getString(R.string.screen_my_block_list) - ) { - startActivity( - Intent( - requireContext(), - BlockMemberActivity::class.java - ) - ) - } - - FunctionButtonHelper.setupFunctionButton( - buttonView = binding.btnMorningCall.root, - iconRes = R.drawable.ic_my_alarm, - title = getString(R.string.screen_my_morning_call) - ) { - startActivity( - Intent( - requireActivity(), - AlarmListActivity::class.java - ) - ) - } - - FunctionButtonHelper.setupFunctionButton( - buttonView = binding.btnNotice.root, - iconRes = R.drawable.ic_my_notice, - title = getString(R.string.screen_my_notice) - ) { - startActivity( - Intent( - requireActivity(), - NoticeActivity::class.java - ) - ) - } - - FunctionButtonHelper.setupFunctionButton( - buttonView = binding.btnEvent.root, - iconRes = R.drawable.ic_my_event, - title = getString(R.string.screen_my_event) - ) { - startActivity( - Intent( - requireActivity(), - EventActivity::class.java - ) - ) - } - - FunctionButtonHelper.setupFunctionButton( - buttonView = binding.btnCustomerService.root, - iconRes = R.drawable.ic_my_service_center, - title = getString(R.string.screen_my_customer_service) - ) { - startActivity( - Intent( - requireActivity(), - ServiceCenterActivity::class.java - ) - ) - } - if (SharedPreferenceManager.role == MemberRole.CREATOR.name) { binding.tvMyChannel.visibility = View.VISIBLE binding.tvMyChannel.setOnClickListener { @@ -335,6 +269,8 @@ class MyPageFragment : BaseFragment(FragmentMyBinding::inflat } else { binding.tvMyChannel.visibility = View.GONE } + + updateFunctionButtons() } else { binding.ivSettings.visibility = View.GONE binding.llFunctionButtonGrid.visibility = View.GONE @@ -380,29 +316,6 @@ class MyPageFragment : BaseFragment(FragmentMyBinding::inflat } viewModel.myPageLiveData.observe(viewLifecycleOwner) { - val isKoreanUser = SharedPreferenceManager.countryCode.ifBlank { "KR" } == "KR" - - if (isKoreanUser) { - binding.btnIdentityVerification.root.visibility = View.VISIBLE - if (it.isAuth) { - FunctionButtonHelper.setupFunctionButton( - buttonView = binding.btnIdentityVerification.root, - iconRes = R.drawable.ic_my_auth, - title = getString(R.string.screen_my_identity_verified) - ) - } else { - FunctionButtonHelper.setupFunctionButton( - buttonView = binding.btnIdentityVerification.root, - iconRes = R.drawable.ic_my_auth, - title = getString(R.string.screen_my_identity_verification) - ) { - showAuthDialog() - } - } - } else { - binding.btnIdentityVerification.root.visibility = View.INVISIBLE - } - binding.ivProfile.load(it.profileUrl) { crossfade(true) placeholder(R.drawable.ic_place_holder) @@ -413,41 +326,75 @@ class MyPageFragment : BaseFragment(FragmentMyBinding::inflat binding.tvCanAmount.text = (it.chargeCan + it.rewardCan).moneyFormat() binding.tvPointAmount.text = it.point.moneyFormat() - val shouldShowCouponButton = if (isKoreanUser) { - true - } else { - SharedPreferenceManager.isAdultContentVisible - } + updateFunctionButtons(it.isAuth) + } + } - binding.btnCoupon.root.visibility = if (shouldShowCouponButton) { - View.VISIBLE - } else { - View.GONE - } + private fun setupFunctionButtonGrid() { + binding.rvFunctionButtons.layoutManager = GridLayoutManager( + requireContext(), + FUNCTION_BUTTON_SPAN_COUNT + ) - if (!shouldShowCouponButton) { - return@observe - } + if (binding.rvFunctionButtons.itemDecorationCount == 0) { + binding.rvFunctionButtons.addItemDecoration( + GridSpacingItemDecoration( + spanCount = FUNCTION_BUTTON_SPAN_COUNT, + spacing = 16f.dpToPx().toInt(), + includeEdge = false + ) + ) + } - if (it.isAuth || !isKoreanUser) { - FunctionButtonHelper.setupFunctionButton( - buttonView = binding.btnCoupon.root, - iconRes = R.drawable.ic_my_coupon, - title = getString(R.string.screen_my_coupon_register) - ) { + binding.rvFunctionButtons.adapter = functionButtonAdapter + } + + private fun updateFunctionButtons(isAuth: Boolean? = null) { + val isKoreanUser = SharedPreferenceManager.countryCode.ifBlank { "KR" } == "KR" + val items = mutableListOf( + FunctionButtonItem( + iconRes = R.drawable.ic_my_storage, + title = getString(R.string.screen_my_storage) + ) { + startActivity( + Intent( + requireContext(), + AudioContentBoxActivity::class.java + ) + ) + }, + FunctionButtonItem( + iconRes = R.drawable.ic_my_block, + title = getString(R.string.screen_my_block_list) + ) { + startActivity( + Intent( + requireContext(), + BlockMemberActivity::class.java + ) + ) + } + ) + + val shouldShowCouponButton = if (isKoreanUser) { + isAuth != null + } else { + SharedPreferenceManager.isAdultContentVisible + } + + if (shouldShowCouponButton) { + items += FunctionButtonItem( + iconRes = R.drawable.ic_my_coupon, + title = getString(R.string.screen_my_coupon_register) + ) { + if ((isAuth == true) || !isKoreanUser) { startActivity( Intent( requireActivity(), CanCouponActivity::class.java ) ) - } - } else { - FunctionButtonHelper.setupFunctionButton( - buttonView = binding.btnCoupon.root, - iconRes = R.drawable.ic_my_coupon, - title = getString(R.string.screen_my_coupon_register) - ) { + } else { Toast.makeText( requireContext(), getString(R.string.screen_my_auth_required), @@ -458,6 +405,70 @@ class MyPageFragment : BaseFragment(FragmentMyBinding::inflat } } } + + items += listOf( + FunctionButtonItem( + iconRes = R.drawable.ic_my_alarm, + title = getString(R.string.screen_my_morning_call) + ) { + startActivity( + Intent( + requireActivity(), + AlarmListActivity::class.java + ) + ) + }, + FunctionButtonItem( + iconRes = R.drawable.ic_my_notice, + title = getString(R.string.screen_my_notice) + ) { + startActivity( + Intent( + requireActivity(), + NoticeActivity::class.java + ) + ) + }, + FunctionButtonItem( + iconRes = R.drawable.ic_my_event, + title = getString(R.string.screen_my_event) + ) { + startActivity( + Intent( + requireActivity(), + EventActivity::class.java + ) + ) + }, + FunctionButtonItem( + iconRes = R.drawable.ic_my_service_center, + title = getString(R.string.screen_my_customer_service) + ) { + startActivity( + Intent( + requireActivity(), + ServiceCenterActivity::class.java + ) + ) + } + ) + + if (isKoreanUser && isAuth != null) { + items += FunctionButtonItem( + iconRes = R.drawable.ic_my_auth, + title = if (isAuth) { + getString(R.string.screen_my_identity_verified) + } else { + getString(R.string.screen_my_identity_verification) + } + ) { + if (!isAuth) { + showAuthDialog() + } + } + } + + functionButtonAdapter.submitList(items) } private fun showAuthDialog() { @@ -486,3 +497,47 @@ class MyPageFragment : BaseFragment(FragmentMyBinding::inflat } } } + +private data class FunctionButtonItem( + @field:DrawableRes val iconRes: Int, + val title: String, + val onClick: () -> Unit +) + +private class FunctionButtonAdapter : RecyclerView.Adapter() { + private val items = mutableListOf() + + 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) { + items.clear() + items.addAll(newItems) + notifyDataSetChanged() + } +} diff --git a/app/src/main/res/layout/fragment_my.xml b/app/src/main/res/layout/fragment_my.xml index b4784b79..96f52f52 100644 --- a/app/src/main/res/layout/fragment_my.xml +++ b/app/src/main/res/layout/fragment_my.xml @@ -276,86 +276,13 @@ android:orientation="vertical" android:visibility="gone"> - - - - - - - - - - - - - - - - - - - - - - - + android:nestedScrollingEnabled="false" + tools:itemCount="8" + tools:listitem="@layout/item_function_button" /> diff --git a/docs/20260402_쿠폰등록해외사용자본인인증예외.md b/docs/20260402_쿠폰등록해외사용자본인인증예외.md index 1a53736d..fa1ced48 100644 --- a/docs/20260402_쿠폰등록해외사용자본인인증예외.md +++ b/docs/20260402_쿠폰등록해외사용자본인인증예외.md @@ -3,6 +3,7 @@ ## 작업 목표 - 마이페이지 `btnCoupon` 터치 시 한국 사용자는 기존처럼 본인인증이 필요하고, 한국이 아닌 사용자는 본인인증 없이 쿠폰 등록 화면으로 이동하도록 수정한다. - 마이페이지 `btnCoupon`은 한국이 아닌 사용자에 한해 민감한 콘텐츠 보기 설정이 켜져 있을 때만 화면에 보이도록 수정한다. +- 마이페이지 기능 버튼 영역을 `RecyclerView` 기반 그리드로 전환해 숨겨진 버튼이 있어도 중간 빈 슬롯 없이 왼쪽부터 자연스럽게 재배치되도록 수정한다. ## 체크리스트 - [x] AC1: `countryCode == "KR"` 이고 `isAuth == false`인 경우 기존처럼 인증 필요 토스트와 `showAuthDialog()`가 실행된다. @@ -13,12 +14,16 @@ - QA: 인증 완료 사용자의 쿠폰 등록 진입 동작 유지 코드 확인 - [x] AC4: 변경 파일 진단/검증 명령 결과를 기록한다. - QA: `lsp_diagnostics`, 관련 Gradle 검증 명령 결과 기록 -- [x] AC5: `countryCode != "KR"` 이고 `SharedPreferenceManager.isAdultContentVisible == true`인 경우에만 `btnCoupon`이 화면에 보인다. - - QA: 비한국 사용자에서 `btnCoupon.root.visibility`가 민감한 콘텐츠 보기 설정값에 따라 제어되는지 코드 확인 -- [x] AC6: `countryCode != "KR"` 이고 `SharedPreferenceManager.isAdultContentVisible == false`인 경우 `btnCoupon`이 화면에 보이지 않는다. - - QA: 비한국 사용자에서 쿠폰 버튼이 `View.GONE` 처리되는지 코드 확인 +- [x] AC5: `countryCode != "KR"` 이고 `SharedPreferenceManager.isAdultContentVisible == true`인 경우에만 쿠폰 버튼이 기능 버튼 목록에 포함되어 화면에 보인다. + - QA: 비한국 사용자에서 쿠폰 버튼 아이템이 민감한 콘텐츠 보기 설정값에 따라 리스트에 포함되는지 코드 확인 +- [x] AC6: `countryCode != "KR"` 이고 `SharedPreferenceManager.isAdultContentVisible == false`인 경우 쿠폰 버튼이 기능 버튼 목록에서 제외되어 화면에 보이지 않는다. + - QA: 비한국 사용자에서 쿠폰 버튼 아이템이 리스트에 추가되지 않는지 코드 확인 - [x] AC7: `countryCode == "KR"` 인 경우 쿠폰 버튼 노출과 기존 한국/비한국 클릭 분기는 유지된다. - QA: 한국 사용자에서는 버튼이 계속 보이고, 클릭 시 기존 인증 분기가 유지되는지 코드 확인 +- [x] AC8: 기능 버튼 영역이 `RecyclerView` 기반으로 렌더링되고, 숨겨진 버튼은 데이터 목록에서 제외되어 남은 버튼이 좌→우/상→하로 자연스럽게 압축 배치된다. + - QA: `fragment_my.xml`이 `RecyclerView`를 사용하고, `MyPageFragment`가 쿠폰/본인인증 버튼을 조건에 따라 아이템 리스트에 포함/제외하는지 코드 확인 +- [x] AC9: 쿠폰 버튼이 숨겨지는 비한국 사용자(`isAdultContentVisible == false`)에서도 기능 버튼 간 가로/세로 간격이 기존 4열 그리드와 동일하게 유지된다. + - QA: `GridLayoutManager(4)`와 `GridSpacingItemDecoration(..., 16dp, false)` 적용으로 기존 16dp 간격 패턴을 유지하는지 코드 확인 ## 검증 기록 - 2026-04-02 @@ -45,3 +50,24 @@ - 한국 사용자는 쿠폰 버튼이 계속 노출되고, 기존 인증 기반 클릭 분기도 유지된다. - `.kt` 파일 대상 `lsp_diagnostics`는 현재 환경에 Kotlin LSP가 없어 실행 불가(`No LSP server configured for extension: .kt`)였다. - `:app:testDebugUnitTest`, `:app:assembleDebug` 실행은 `BUILD SUCCESSFUL`로 완료됐다. +- 2026-04-02 + - 무엇/왜/어떻게: 기능 버튼 영역이 고정 2행 x 4열 `include` 구조여서 비한국 사용자에게서 쿠폰 버튼이 숨겨질 때 중간 빈칸이 남았다. `fragment_my.xml`을 `RecyclerView` 기반 4열 그리드로 바꾸고, `MyPageFragment`가 기존 버튼 순서를 유지한 채 조건에 맞는 버튼만 리스트에 담아 렌더링하도록 리팩터링했다. + - 실행 명령/도구: + - `apply_patch(app/src/main/res/layout/fragment_my.xml)` + - `apply_patch(app/src/main/res/layout/item_function_button.xml)` + - `apply_patch(app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt)` + - `apply_patch(docs/20260402_쿠폰등록해외사용자본인인증예외.md)` + - `read(app/src/main/res/layout/fragment_my.xml)` + - `read(app/src/main/res/layout/item_function_button.xml)` + - `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)` + - `lsp_diagnostics(app/src/main/res/layout/fragment_my.xml)` + - `lsp_diagnostics(app/src/main/res/layout/item_function_button.xml)` + - `./gradlew :app:testDebugUnitTest :app:assembleDebug` + - 결과: + - 쿠폰 버튼과 본인인증 버튼은 더 이상 고정 슬롯의 `visibility`로 숨기지 않고, 조건에 맞을 때만 `RecyclerView` 데이터에 포함된다. + - 비한국 사용자이면서 `SharedPreferenceManager.isAdultContentVisible == false`인 경우 쿠폰 버튼이 목록에서 빠져 나머지 기능 버튼이 좌측부터 자연스럽게 압축 배치된다. + - 가로/세로 간격은 `GridLayoutManager(4)` + `GridSpacingItemDecoration(..., 16dp, false)`로 기존 4열 레이아웃의 16dp 간격 패턴을 유지한다. + - 기존 버튼 제목, 아이콘, 클릭 액션, 한국/비한국 및 인증/민감 콘텐츠 조건은 그대로 유지됐다. + - `.kt`/`.xml` 대상 `lsp_diagnostics`는 현재 환경에 Kotlin/XML LSP가 없어 실행 불가(`No LSP server configured for extension: .kt/.xml`)였다. + - `:app:testDebugUnitTest`, `:app:assembleDebug` 실행은 `BUILD SUCCESSFUL`로 완료됐다.