From a631aa1b650211c7fd9aed3cea36d5fb9c7a1f12 Mon Sep 17 00:00:00 2001 From: klaus Date: Mon, 15 Jun 2026 13:20:11 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator):=20=EC=B1=84=EB=84=90=20=ED=99=88?= =?UTF-8?q?=20Activity=20=EC=A7=84=EC=9E=85=EC=A0=90=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 1 + .../vividnext/sodalive/base/BaseActivity.kt | 4 +- .../channel/CreatorChannelHomeActivity.kt | 261 ++++++++++++++++++ .../layout/activity_creator_channel_home.xml | 225 +++++++++++++++ 4 files changed, 490 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeActivity.kt create mode 100644 app/src/main/res/layout/activity_creator_channel_home.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d73d05a7..1c44d6f3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -112,6 +112,7 @@ + diff --git a/app/src/main/java/kr/co/vividnext/sodalive/base/BaseActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/base/BaseActivity.kt index 5874a849..0372fa28 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/base/BaseActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/base/BaseActivity.kt @@ -31,6 +31,8 @@ abstract class BaseActivity( lateinit var binding: T private set + protected open val shouldApplySystemBarTopInset: Boolean = true + val screenWidth: Int by lazy { resources.displayMetrics.widthPixels } @@ -81,7 +83,7 @@ abstract class BaseActivity( // 루트는 좌/우/하만 처리(상단은 Toolbar에 위임). IME가 등장하면 하단 패딩을 IME 높이까지 확장 val left = max(systemBars.left, ime.left) - val top = systemBars.top + val top = if (shouldApplySystemBarTopInset) systemBars.top else 0 val right = max(systemBars.right, ime.right) val bottom = max(systemBars.bottom, ime.bottom) v.setPadding(left, top, right, bottom) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeActivity.kt new file mode 100644 index 00000000..3342340d --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeActivity.kt @@ -0,0 +1,261 @@ +package kr.co.vividnext.sodalive.v2.creator.channel + +import android.content.Context +import android.content.Intent +import android.graphics.Typeface +import android.view.Gravity +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast +import androidx.core.view.ViewCompat +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowCompat +import androidx.recyclerview.widget.LinearLayoutManager +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelHomeBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.extensions.loadUrl +import kr.co.vividnext.sodalive.extensions.moneyFormat +import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailFragment +import kr.co.vividnext.sodalive.v2.common.CreatorActivityType +import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse +import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel +import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeUiState +import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab +import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTitleBarState +import kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelHomeSectionAdapter +import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivity +import org.koin.androidx.viewmodel.ext.android.viewModel + +class CreatorChannelHomeActivity : BaseActivity( + ActivityCreatorChannelHomeBinding::inflate +) { + + private val viewModel: CreatorChannelHomeViewModel by viewModel() + private val sectionAdapter = CreatorChannelHomeSectionAdapter(::onScheduleClicked) + private var creatorId: Long = 0L + private var currentHeader: CreatorChannelHeaderUiModel? = null + + override val shouldApplySystemBarTopInset: Boolean = false + + override fun setupView() { + creatorId = intent.getLongExtra(EXTRA_CREATOR_ID, 0L) + if (creatorId <= 0L) { + finish() + return + } + + setupRecyclerView() + setStatusBarIconAppearance() + setTitleBarTopInset() + setupClickListeners() + observeViewModel() + viewModel.loadHome(creatorId) + } + + private fun setupRecyclerView() { + binding.rvHomeSections.layoutManager = LinearLayoutManager(this) + binding.rvHomeSections.adapter = sectionAdapter + } + + private fun setupClickListeners() { + binding.ivBack.setOnClickListener { finish() } + binding.ivMore.setOnClickListener { onMoreClicked() } + binding.tvChatButton.setOnClickListener { + currentHeader?.characterId?.let { characterId -> viewModel.createChatRoom(characterId) } + } + binding.tvDmButton.setOnClickListener { + startActivity(DmChatRoomActivity.newIntentByCreatorId(this, creatorId)) + } + } + + private fun observeViewModel() { + viewModel.homeStateLiveData.observe(this) { state -> + when (state) { + is CreatorChannelHomeUiState.Content -> bindContent(state) + is CreatorChannelHomeUiState.Error -> Unit + CreatorChannelHomeUiState.Empty -> Unit + CreatorChannelHomeUiState.Loading -> Unit + } + } + viewModel.chatRoomIdLiveData.observe(this) { event -> + event.consume()?.let { chatRoomId -> + startActivity(ChatRoomActivity.newIntent(this, chatRoomId)) + } + } + viewModel.toastLiveData.observe(this) { event -> + event.consume()?.let { + val message = it.message ?: it.resId?.let(::getString) + message?.let { text -> Toast.makeText(applicationContext, text, Toast.LENGTH_LONG).show() } + } + } + } + + private fun bindContent(content: CreatorChannelHomeUiState.Content) { + currentHeader = content.header + bindHeader(content.header) + bindTitleBar(content.header) + bindTabs(content.tabs) + sectionAdapter.submitItems(content.sections) + } + + private fun bindHeader(header: CreatorChannelHeaderUiModel) { + binding.tvNickname.text = header.nickname + binding.tvFollowerCount.text = getString( + R.string.creator_channel_follower_count, + header.followerCount.moneyFormat() + ) + binding.ivHeaderImage.loadUrl(header.profileImageUrl) { + placeholder(R.drawable.ic_placeholder_profile) + error(R.drawable.ic_placeholder_profile) + } + updateActionButtonLayout( + isChatVisible = header.isAiChatAvailable && header.characterId != null, + isDmVisible = header.isDmAvailable + ) + } + + private fun bindTitleBar(header: CreatorChannelHeaderUiModel) { + val titleBarState = CreatorChannelTitleBarState.from( + isFollow = header.isFollow, + isNotify = header.isNotify, + isInProgress = false + ) + binding.ivFollow.setImageResource(titleBarState.followIconResId) + binding.layoutFollowCapsule.isEnabled = titleBarState.isActionEnabled + binding.layoutFollowCapsule.setBackgroundResource( + if (header.isFollow) { + R.drawable.bg_creator_channel_following_capsule + } else { + R.drawable.bg_creator_channel_follow_capsule + } + ) + binding.layoutFollowCapsule.updateLayoutParams { + width = if (header.isFollow) 36.dpToPx().toInt() else LinearLayout.LayoutParams.WRAP_CONTENT + } + binding.tvFollowLabel.isVisible = !header.isFollow + titleBarState.bellIconResId?.let { + binding.ivBell.setImageResource(it) + binding.ivBell.visibility = View.VISIBLE + } ?: run { + binding.ivBell.visibility = View.GONE + } + } + + private fun bindTabs(tabs: List) { + binding.tabContainer.removeAllViews() + tabs.forEachIndexed { index, tab -> + binding.tabContainer.addView(createTabView(tab, isSelected = index == 0)) + } + } + + private fun createTabView(tab: CreatorChannelTab, isSelected: Boolean): LinearLayout { + val tabText = TextView(this).apply { + text = getString(tab.labelResId) + gravity = Gravity.CENTER + setTextColor(getColor(if (isSelected) R.color.white else R.color.gray_500)) + setTypeface(null, Typeface.NORMAL) + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + 0, + 1f + ) + } + tabText.textSize = 16f + val indicator = View(this).apply { + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + 3.dpToPx().toInt() + ) + } + indicator.setBackgroundColor(getColor(R.color.soda_400)) + indicator.isVisible = isSelected + return LinearLayout(this).apply { + orientation = LinearLayout.VERTICAL + gravity = Gravity.CENTER + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.MATCH_PARENT + ).apply { + width = 110.dpToPx().toInt() + } + addView(tabText) + addView(indicator) + } + } + + private fun setTitleBarTopInset() { + val baseTitleBarHeight = 60.dpToPx().toInt() + ViewCompat.setOnApplyWindowInsetsListener(binding.titleBarContainer) { view, insets -> + val topInset = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top + view.updatePadding(top = topInset) + view.updateLayoutParams { + height = baseTitleBarHeight + topInset + } + insets + } + } + + private fun setStatusBarIconAppearance() { + WindowCompat.getInsetsController(window, binding.root).isAppearanceLightStatusBars = false + } + + private fun updateActionButtonLayout(isChatVisible: Boolean, isDmVisible: Boolean) { + binding.tvChatButton.isVisible = isChatVisible + binding.tvDmButton.isVisible = isDmVisible + (binding.tvDmButton.layoutParams as LinearLayout.LayoutParams).apply { + marginStart = if (isChatVisible && isDmVisible) 6.dpToPx().toInt() else 0 + }.also(binding.tvDmButton::setLayoutParams) + } + + private fun onMoreClicked() { + Toast.makeText(applicationContext, getString(R.string.creator_channel_more_ready), Toast.LENGTH_SHORT).show() + } + + private fun onScheduleClicked(schedule: CreatorChannelScheduleResponse) { + when (schedule.type) { + CreatorActivityType.Audio, + CreatorActivityType.LiveReplay -> startActivity( + Intent(this, AudioContentDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, schedule.targetId) + } + ) + + CreatorActivityType.Live -> showLiveRoomDetail(schedule.targetId) + + CreatorActivityType.Community -> Unit + } + } + + private fun showLiveRoomDetail(roomId: Long) { + val detailFragment = LiveRoomDetailFragment( + roomId, + onClickParticipant = {}, + onClickReservation = {}, + onClickModify = {}, + onClickStart = {}, + onClickCancel = {} + ) + if (detailFragment.isAdded) return + + detailFragment.show(supportFragmentManager, detailFragment.tag) + } + + companion object { + const val EXTRA_CREATOR_ID: String = "extra_creator_id" + + fun newIntent(context: Context, creatorId: Long): Intent { + return Intent(context, CreatorChannelHomeActivity::class.java).apply { + putExtra(EXTRA_CREATOR_ID, creatorId) + } + } + } +} diff --git a/app/src/main/res/layout/activity_creator_channel_home.xml b/app/src/main/res/layout/activity_creator_channel_home.xml new file mode 100644 index 00000000..d88438c0 --- /dev/null +++ b/app/src/main/res/layout/activity_creator_channel_home.xml @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +