Compare commits
10 Commits
2d8f8cab81
...
7ec4e09fb4
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ec4e09fb4 | |||
| 862cec8375 | |||
| 2d4956ae25 | |||
| beadc7d4c4 | |||
| 2633912e93 | |||
| 6cbc751f0c | |||
| 2a499eaf41 | |||
| 9244a00713 | |||
| 79eadbd39d | |||
| c389d1945d |
@@ -86,7 +86,7 @@ android {
|
|||||||
applicationId "kr.co.vividnext.sodalive"
|
applicationId "kr.co.vividnext.sodalive"
|
||||||
minSdk 23
|
minSdk 23
|
||||||
targetSdk 35
|
targetSdk 35
|
||||||
versionCode 239
|
versionCode 242
|
||||||
versionName "1.55.0"
|
versionName "1.55.0"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -655,14 +655,11 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
/** 쿼터 안내 메시지 뷰홀더: 광고 보기 + 캔 구매 버튼 */
|
|
||||||
class QuotaNoticeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
class QuotaNoticeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
private val btnRewardedAd: View = itemView.findViewById(R.id.ll_rewarded_ad)
|
|
||||||
private val btnPurchase10Can: View = itemView.findViewById(R.id.ll_purchase_10_can)
|
private val btnPurchase10Can: View = itemView.findViewById(R.id.ll_purchase_10_can)
|
||||||
private val btnPurchase20Can: View = itemView.findViewById(R.id.ll_purchase_20_can)
|
private val btnPurchase20Can: View = itemView.findViewById(R.id.ll_purchase_20_can)
|
||||||
|
|
||||||
fun bind(onAction: (ChatQuotaNoticeAction) -> Unit) {
|
fun bind(onAction: (ChatQuotaNoticeAction) -> Unit) {
|
||||||
btnRewardedAd.setOnClickListener { onAction(ChatQuotaNoticeAction.REWARDED_AD) }
|
|
||||||
btnPurchase10Can.setOnClickListener { onAction(ChatQuotaNoticeAction.PURCHASE_10_CAN) }
|
btnPurchase10Can.setOnClickListener { onAction(ChatQuotaNoticeAction.PURCHASE_10_CAN) }
|
||||||
btnPurchase20Can.setOnClickListener { onAction(ChatQuotaNoticeAction.PURCHASE_20_CAN) }
|
btnPurchase20Can.setOnClickListener { onAction(ChatQuotaNoticeAction.PURCHASE_20_CAN) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ package kr.co.vividnext.sodalive.v2.creator.channel.community.detail
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Outline
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.method.ScrollingMovementMethod
|
import android.text.method.ScrollingMovementMethod
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewOutlineProvider
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -13,6 +17,10 @@ import androidx.core.widget.doAfterTextChanged
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.DataSource
|
||||||
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
|
import com.bumptech.glide.request.RequestListener
|
||||||
|
import com.bumptech.glide.request.target.Target
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityDetailBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityDetailBinding
|
||||||
@@ -24,6 +32,7 @@ import kr.co.vividnext.sodalive.extensions.moneyFormat
|
|||||||
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.CreatorChannelCommunityReplyActivity
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.CreatorChannelCommunityReplyActivity
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.ui.CreatorChannelCommunityCommentAdapter
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.ui.CreatorChannelCommunityCommentAdapter
|
||||||
|
import kr.co.vividnext.sodalive.v2.widget.feed.calculateFeedCommunityImageHeight
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChannelCommunityDetailBinding>(
|
class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChannelCommunityDetailBinding>(
|
||||||
@@ -56,6 +65,8 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.btnCreatorChannelCommunityDetailBack.setOnClickListener { finish() }
|
binding.btnCreatorChannelCommunityDetailBack.setOnClickListener { finish() }
|
||||||
|
binding.layoutCreatorChannelCommunityDetailImageContainer.clipToOutline = true
|
||||||
|
binding.layoutCreatorChannelCommunityDetailImageContainer.outlineProvider = roundedOutlineProvider()
|
||||||
binding.rvCreatorChannelCommunityDetailComments.layoutManager = LinearLayoutManager(this)
|
binding.rvCreatorChannelCommunityDetailComments.layoutManager = LinearLayoutManager(this)
|
||||||
binding.rvCreatorChannelCommunityDetailComments.adapter = commentAdapter
|
binding.rvCreatorChannelCommunityDetailComments.adapter = commentAdapter
|
||||||
binding.scrollCreatorChannelCommunityDetail.setOnScrollChangeListener { scrollView, _, scrollY, _, oldScrollY ->
|
binding.scrollCreatorChannelCommunityDetail.setOnScrollChangeListener { scrollView, _, scrollY, _, oldScrollY ->
|
||||||
@@ -156,6 +167,10 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
|||||||
tvCreatorChannelCommunityDetailPrice.text = post.price.moneyFormat()
|
tvCreatorChannelCommunityDetailPrice.text = post.price.moneyFormat()
|
||||||
tvCreatorChannelCommunityDetailPurchased.isVisible = post.existOrdered && post.price > 0
|
tvCreatorChannelCommunityDetailPurchased.isVisible = post.existOrdered && post.price > 0
|
||||||
layoutCreatorChannelCommunityDetailImageContainer.isVisible = post.imageUrl != null || post.showPaywall
|
layoutCreatorChannelCommunityDetailImageContainer.isVisible = post.imageUrl != null || post.showPaywall
|
||||||
|
layoutCreatorChannelCommunityDetailImageContainer.layoutParams =
|
||||||
|
layoutCreatorChannelCommunityDetailImageContainer.layoutParams.apply {
|
||||||
|
height = if (post.imageUrl != null) DEFAULT_IMAGE_HEIGHT_DP.dpToPx() else DEFAULT_PAYWALL_HEIGHT_DP.dpToPx()
|
||||||
|
}
|
||||||
layoutCreatorChannelCommunityDetailPaywall.isVisible = post.showPaywall
|
layoutCreatorChannelCommunityDetailPaywall.isVisible = post.showPaywall
|
||||||
tvCreatorChannelCommunityDetailPaywallPrice.text = post.price.moneyFormat()
|
tvCreatorChannelCommunityDetailPaywallPrice.text = post.price.moneyFormat()
|
||||||
layoutCreatorChannelCommunityDetailPaywall.setOnClickListener {
|
layoutCreatorChannelCommunityDetailPaywall.setOnClickListener {
|
||||||
@@ -165,6 +180,25 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
|||||||
Glide.with(ivCreatorChannelCommunityDetailImage)
|
Glide.with(ivCreatorChannelCommunityDetailImage)
|
||||||
.load(post.imageUrl)
|
.load(post.imageUrl)
|
||||||
.placeholder(R.drawable.ic_place_holder)
|
.placeholder(R.drawable.ic_place_holder)
|
||||||
|
.listener(object : RequestListener<Drawable> {
|
||||||
|
override fun onLoadFailed(
|
||||||
|
e: GlideException?,
|
||||||
|
model: Any?,
|
||||||
|
target: Target<Drawable>,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean = false
|
||||||
|
|
||||||
|
override fun onResourceReady(
|
||||||
|
resource: Drawable,
|
||||||
|
model: Any,
|
||||||
|
target: Target<Drawable>?,
|
||||||
|
dataSource: DataSource,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean {
|
||||||
|
applyDetailImageSize(resource.intrinsicWidth, resource.intrinsicHeight)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
.into(ivCreatorChannelCommunityDetailImage)
|
.into(ivCreatorChannelCommunityDetailImage)
|
||||||
} else {
|
} else {
|
||||||
Glide.with(ivCreatorChannelCommunityDetailImage).clear(ivCreatorChannelCommunityDetailImage)
|
Glide.with(ivCreatorChannelCommunityDetailImage).clear(ivCreatorChannelCommunityDetailImage)
|
||||||
@@ -196,6 +230,29 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
|||||||
updateSendButton(content.commentInput.trim().isNotEmpty() && !content.isSendingComment)
|
updateSendButton(content.commentInput.trim().isNotEmpty() && !content.isSendingComment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun applyDetailImageSize(intrinsicWidthPx: Int, intrinsicHeightPx: Int): Unit = with(binding) {
|
||||||
|
val container = layoutCreatorChannelCommunityDetailImageContainer
|
||||||
|
val containerWidth = container.width.takeIf { it > 0 }
|
||||||
|
?: container.measuredWidth.takeIf { it > 0 }
|
||||||
|
?: ivCreatorChannelCommunityDetailImage.width.takeIf { it > 0 }
|
||||||
|
?: run {
|
||||||
|
container.post { applyDetailImageSize(intrinsicWidthPx, intrinsicHeightPx) }
|
||||||
|
return@with
|
||||||
|
}
|
||||||
|
val imageHeight = calculateFeedCommunityImageHeight(
|
||||||
|
containerWidthPx = containerWidth,
|
||||||
|
intrinsicWidthPx = intrinsicWidthPx,
|
||||||
|
intrinsicHeightPx = intrinsicHeightPx
|
||||||
|
) ?: return@with
|
||||||
|
container.layoutParams = container.layoutParams.apply { height = imageHeight }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun roundedOutlineProvider() = object : ViewOutlineProvider() {
|
||||||
|
override fun getOutline(view: View, outline: Outline) {
|
||||||
|
outline.setRoundRect(0, 0, view.width, view.height, COMMUNITY_IMAGE_RADIUS_DP.dpToPx().toFloat())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateSendButton(enabled: Boolean) = with(binding.btnCreatorChannelCommunityDetailSend) {
|
private fun updateSendButton(enabled: Boolean) = with(binding.btnCreatorChannelCommunityDetailSend) {
|
||||||
isEnabled = enabled
|
isEnabled = enabled
|
||||||
setBackgroundResource(
|
setBackgroundResource(
|
||||||
@@ -269,10 +326,15 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val EXTRA_POST_ID = "extra_post_id"
|
private const val EXTRA_POST_ID = "extra_post_id"
|
||||||
|
private const val DEFAULT_IMAGE_HEIGHT_DP = 236
|
||||||
|
private const val DEFAULT_PAYWALL_HEIGHT_DP = 236
|
||||||
|
private const val COMMUNITY_IMAGE_RADIUS_DP = 14
|
||||||
|
|
||||||
fun newIntent(context: Context, postId: Long): Intent {
|
fun newIntent(context: Context, postId: Long): Intent {
|
||||||
return Intent(context, CreatorChannelCommunityDetailActivity::class.java)
|
return Intent(context, CreatorChannelCommunityDetailActivity::class.java)
|
||||||
.putExtra(EXTRA_POST_ID, postId)
|
.putExtra(EXTRA_POST_ID, postId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).toInt()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.creator.channel.community.ui
|
package kr.co.vividnext.sodalive.v2.creator.channel.community.ui
|
||||||
|
|
||||||
|
import android.graphics.Outline
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewOutlineProvider
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
import com.bumptech.glide.load.DataSource
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestListener
|
||||||
|
import com.bumptech.glide.request.target.Target
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityListBinding
|
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityListBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.widget.feed.calculateFeedCommunityImageHeight
|
||||||
|
|
||||||
class CreatorChannelCommunityListAdapter(
|
class CreatorChannelCommunityListAdapter(
|
||||||
private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {},
|
private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {},
|
||||||
@@ -54,6 +60,11 @@ class CreatorChannelCommunityListAdapter(
|
|||||||
private val isPlayingContent: (Long) -> Boolean
|
private val isPlayingContent: (Long) -> Boolean
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
binding.layoutCreatorChannelCommunityListImageContainer.clipToOutline = true
|
||||||
|
binding.layoutCreatorChannelCommunityListImageContainer.outlineProvider = roundedOutlineProvider()
|
||||||
|
}
|
||||||
|
|
||||||
fun bind(item: CreatorChannelCommunityPostUiModel) = with(binding) {
|
fun bind(item: CreatorChannelCommunityPostUiModel) = with(binding) {
|
||||||
root.setOnClickListener { onPostClick(item) }
|
root.setOnClickListener { onPostClick(item) }
|
||||||
ivCreatorChannelCommunityListProfile.loadUrl(item.creatorProfileUrl) {
|
ivCreatorChannelCommunityListProfile.loadUrl(item.creatorProfileUrl) {
|
||||||
@@ -77,11 +88,30 @@ class CreatorChannelCommunityListAdapter(
|
|||||||
layoutCreatorChannelCommunityListImageContainer.isVisible =
|
layoutCreatorChannelCommunityListImageContainer.isVisible =
|
||||||
visibleImageUrl != null || item.isLocked || item.showPlayButton
|
visibleImageUrl != null || item.isLocked || item.showPlayButton
|
||||||
ivCreatorChannelCommunityListImage.isVisible = visibleImageUrl != null
|
ivCreatorChannelCommunityListImage.isVisible = visibleImageUrl != null
|
||||||
|
resetCommunityImageHeight()
|
||||||
if (visibleImageUrl != null) {
|
if (visibleImageUrl != null) {
|
||||||
Glide.with(ivCreatorChannelCommunityListImage)
|
Glide.with(ivCreatorChannelCommunityListImage)
|
||||||
.load(visibleImageUrl)
|
.load(visibleImageUrl)
|
||||||
.placeholder(R.drawable.ic_place_holder)
|
.placeholder(R.drawable.ic_place_holder)
|
||||||
.apply(communityImageRequestOptions())
|
.listener(object : RequestListener<Drawable> {
|
||||||
|
override fun onLoadFailed(
|
||||||
|
e: GlideException?,
|
||||||
|
model: Any?,
|
||||||
|
target: Target<Drawable>,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean = false
|
||||||
|
|
||||||
|
override fun onResourceReady(
|
||||||
|
resource: Drawable,
|
||||||
|
model: Any,
|
||||||
|
target: Target<Drawable>?,
|
||||||
|
dataSource: DataSource,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean {
|
||||||
|
applyCommunityImageSize(resource.intrinsicWidth, resource.intrinsicHeight)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
.into(ivCreatorChannelCommunityListImage)
|
.into(ivCreatorChannelCommunityListImage)
|
||||||
} else {
|
} else {
|
||||||
Glide.with(ivCreatorChannelCommunityListImage).clear(ivCreatorChannelCommunityListImage)
|
Glide.with(ivCreatorChannelCommunityListImage).clear(ivCreatorChannelCommunityListImage)
|
||||||
@@ -104,11 +134,45 @@ class CreatorChannelCommunityListAdapter(
|
|||||||
ivCreatorChannelCommunityListOwnerMore.setOnClickListener { onOwnerMoreClick(item) }
|
ivCreatorChannelCommunityListOwnerMore.setOnClickListener { onOwnerMoreClick(item) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun communityImageRequestOptions(): RequestOptions {
|
private fun resetCommunityImageHeight() {
|
||||||
return RequestOptions().transform(
|
binding.layoutCreatorChannelCommunityListImageContainer.layoutParams =
|
||||||
CenterCrop(),
|
binding.layoutCreatorChannelCommunityListImageContainer.layoutParams.apply {
|
||||||
RoundedCorners(14f.dpToPx().toInt())
|
height = DEFAULT_IMAGE_HEIGHT_DP.dpToPx().toInt()
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyCommunityImageSize(intrinsicWidthPx: Int, intrinsicHeightPx: Int): Unit = with(binding) {
|
||||||
|
val container = layoutCreatorChannelCommunityListImageContainer
|
||||||
|
val containerWidth = container.width.takeIf { it > 0 }
|
||||||
|
?: container.measuredWidth.takeIf { it > 0 }
|
||||||
|
?: root.width.takeIf { it > 0 }?.let { it - root.paddingLeft - root.paddingRight }
|
||||||
|
?: run {
|
||||||
|
container.post { applyCommunityImageSize(intrinsicWidthPx, intrinsicHeightPx) }
|
||||||
|
return@with
|
||||||
|
}
|
||||||
|
val imageHeight = calculateFeedCommunityImageHeight(
|
||||||
|
containerWidthPx = containerWidth,
|
||||||
|
intrinsicWidthPx = intrinsicWidthPx,
|
||||||
|
intrinsicHeightPx = intrinsicHeightPx
|
||||||
|
) ?: return@with
|
||||||
|
container.layoutParams = container.layoutParams.apply { height = imageHeight }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun roundedOutlineProvider() = object : ViewOutlineProvider() {
|
||||||
|
override fun getOutline(view: View, outline: Outline) {
|
||||||
|
outline.setRoundRect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
view.width,
|
||||||
|
view.height,
|
||||||
|
COMMUNITY_IMAGE_RADIUS_DP.dpToPx()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val DEFAULT_IMAGE_HEIGHT_DP = 236f
|
||||||
|
const val COMMUNITY_IMAGE_RADIUS_DP = 14f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -435,6 +435,14 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
val communityImageUrl = community.imageUrl.takeIf { !it.isNullOrBlank() }
|
val communityImageUrl = community.imageUrl.takeIf { !it.isNullOrBlank() }
|
||||||
if (communityImageUrl != null) {
|
if (communityImageUrl != null) {
|
||||||
communityRow.communityImageView().loadUrl(communityImageUrl) {
|
communityRow.communityImageView().loadUrl(communityImageUrl) {
|
||||||
|
listener(
|
||||||
|
onSuccess = { _, result ->
|
||||||
|
communityRow.applyCommunityImageSize(
|
||||||
|
result.drawable.intrinsicWidth,
|
||||||
|
result.drawable.intrinsicHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
if (isCommunityLocked) {
|
if (isCommunityLocked) {
|
||||||
transformations(BlurTransformation(itemView.context, 25f, 2.5f))
|
transformations(BlurTransformation(itemView.context, 25f, 2.5f))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ class ChatMainFragment : BaseFragment<FragmentV2MainChatBinding>(
|
|||||||
setupChatRooms()
|
setupChatRooms()
|
||||||
bindViewModel()
|
bindViewModel()
|
||||||
|
|
||||||
binding.btnChatFloating.setOnClickListener { }
|
|
||||||
viewModel.loadFirstPage(initialFilter)
|
viewModel.loadFirstPage(initialFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,9 @@ class HomeFollowingNewsAdapter(
|
|||||||
view.setFeedSize(feedSize(FeedVariant.Community, parent))
|
view.setFeedSize(feedSize(FeedVariant.Community, parent))
|
||||||
view.bind(feedItem)
|
view.bind(feedItem)
|
||||||
view.profileImageView().loadHomeCreatorProfileImage(feedItem.creatorImageUrl)
|
view.profileImageView().loadHomeCreatorProfileImage(feedItem.creatorImageUrl)
|
||||||
bindImage(view.communityImageView(), feedItem.imageUrl.orEmpty())
|
bindImage(view.communityImageView(), feedItem.imageUrl.orEmpty()) { width, height ->
|
||||||
|
view.applyCommunityImageSize(width, height)
|
||||||
|
}
|
||||||
view.setOnFeedClick { onClickItem(community) }
|
view.setOnFeedClick { onClickItem(community) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,11 +178,21 @@ class HomeFollowingNewsAdapter(
|
|||||||
return FeedSize.from(variant, FeedWidthMode.ParentAvailable, availableWidthDp)
|
return FeedSize.from(variant, FeedWidthMode.ParentAvailable, availableWidthDp)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindImage(imageView: android.widget.ImageView, url: String) {
|
private fun bindImage(
|
||||||
|
imageView: android.widget.ImageView,
|
||||||
|
url: String,
|
||||||
|
onImageLoaded: ((Int, Int) -> Unit)? = null
|
||||||
|
) {
|
||||||
if (url.isBlank()) {
|
if (url.isBlank()) {
|
||||||
imageView.setImageDrawable(null)
|
imageView.setImageDrawable(null)
|
||||||
} else {
|
} else {
|
||||||
imageView.loadUrl(url)
|
imageView.loadUrl(url) {
|
||||||
|
listener(
|
||||||
|
onSuccess = { _, result ->
|
||||||
|
onImageLoaded?.invoke(result.drawable.intrinsicWidth, result.drawable.intrinsicHeight)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,20 +53,27 @@ class HomePopularCommunityAdapter(
|
|||||||
bindImage(
|
bindImage(
|
||||||
imageUrl = item.item.imageUrl,
|
imageUrl = item.item.imageUrl,
|
||||||
imageView = view.communityImageView(),
|
imageView = view.communityImageView(),
|
||||||
shouldBlur = item.item.price > 0 && !item.item.existOrdered
|
shouldBlur = item.item.price > 0 && !item.item.existOrdered,
|
||||||
|
onImageLoaded = { width, height -> view.applyCommunityImageSize(width, height) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindImage(
|
private fun bindImage(
|
||||||
imageUrl: String?,
|
imageUrl: String?,
|
||||||
imageView: android.widget.ImageView,
|
imageView: android.widget.ImageView,
|
||||||
shouldBlur: Boolean = false
|
shouldBlur: Boolean = false,
|
||||||
|
onImageLoaded: ((Int, Int) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
if (imageUrl.isNullOrBlank()) {
|
if (imageUrl.isNullOrBlank()) {
|
||||||
imageView.dispose()
|
imageView.dispose()
|
||||||
imageView.setImageDrawable(null)
|
imageView.setImageDrawable(null)
|
||||||
} else {
|
} else {
|
||||||
imageView.loadUrl(imageUrl) {
|
imageView.loadUrl(imageUrl) {
|
||||||
|
listener(
|
||||||
|
onSuccess = { _, result ->
|
||||||
|
onImageLoaded?.invoke(result.drawable.intrinsicWidth, result.drawable.intrinsicHeight)
|
||||||
|
}
|
||||||
|
)
|
||||||
if (shouldBlur) {
|
if (shouldBlur) {
|
||||||
transformations(BlurTransformation(imageView.context, 25f, 2.5f))
|
transformations(BlurTransformation(imageView.context, 25f, 2.5f))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.widget.feed
|
||||||
|
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
fun calculateFeedCommunityImageHeight(
|
||||||
|
containerWidthPx: Int,
|
||||||
|
intrinsicWidthPx: Int,
|
||||||
|
intrinsicHeightPx: Int
|
||||||
|
): Int? {
|
||||||
|
if (containerWidthPx <= 0 || intrinsicWidthPx <= 0 || intrinsicHeightPx <= 0) return null
|
||||||
|
return (containerWidthPx * intrinsicHeightPx.toFloat() / intrinsicWidthPx).roundToInt()
|
||||||
|
}
|
||||||
@@ -62,6 +62,7 @@ class FeedCommunityView @JvmOverloads constructor(
|
|||||||
val isLocked = item.price > 0 && !item.existOrdered
|
val isLocked = item.price > 0 && !item.existOrdered
|
||||||
val hasImage = !item.imageUrl.isNullOrBlank()
|
val hasImage = !item.imageUrl.isNullOrBlank()
|
||||||
requireNotNull(communityImageContainer).isVisible = hasImage || isLocked
|
requireNotNull(communityImageContainer).isVisible = hasImage || isLocked
|
||||||
|
resetCommunityImageHeight()
|
||||||
requireNotNull(communityImage).isVisible = hasImage
|
requireNotNull(communityImage).isVisible = hasImage
|
||||||
if (!hasImage || isLocked) {
|
if (!hasImage || isLocked) {
|
||||||
requireNotNull(communityImage).setImageDrawable(null)
|
requireNotNull(communityImage).setImageDrawable(null)
|
||||||
@@ -79,6 +80,33 @@ class FeedCommunityView @JvmOverloads constructor(
|
|||||||
|
|
||||||
fun boundItem(): FeedItem.Community? = currentItem
|
fun boundItem(): FeedItem.Community? = currentItem
|
||||||
|
|
||||||
|
fun applyCommunityImageSize(
|
||||||
|
intrinsicWidthPx: Int,
|
||||||
|
intrinsicHeightPx: Int
|
||||||
|
) {
|
||||||
|
val container = requireNotNull(communityImageContainer)
|
||||||
|
val width = container.width.takeIf { it > 0 }
|
||||||
|
?: container.measuredWidth.takeIf { it > 0 }
|
||||||
|
?: (width - paddingLeft - paddingRight).takeIf { it > 0 }
|
||||||
|
?: run {
|
||||||
|
post { applyCommunityImageSize(intrinsicWidthPx, intrinsicHeightPx) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val imageHeight = calculateFeedCommunityImageHeight(
|
||||||
|
containerWidthPx = width,
|
||||||
|
intrinsicWidthPx = intrinsicWidthPx,
|
||||||
|
intrinsicHeightPx = intrinsicHeightPx
|
||||||
|
) ?: return
|
||||||
|
container.layoutParams = container.layoutParams.apply { height = imageHeight }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetCommunityImageHeight() {
|
||||||
|
val container = requireNotNull(communityImageContainer)
|
||||||
|
container.layoutParams = container.layoutParams.apply {
|
||||||
|
height = DEFAULT_IMAGE_HEIGHT_DP.dpToPx()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setFeedSize(size: FeedSize) {
|
fun setFeedSize(size: FeedSize) {
|
||||||
updateRootWidth(size.rootWidthDp.dpToPx())
|
updateRootWidth(size.rootWidthDp.dpToPx())
|
||||||
}
|
}
|
||||||
@@ -128,5 +156,6 @@ class FeedCommunityView @JvmOverloads constructor(
|
|||||||
private companion object {
|
private companion object {
|
||||||
const val CARD_RADIUS_DP = 14
|
const val CARD_RADIUS_DP = 14
|
||||||
const val COMMUNITY_IMAGE_RADIUS_DP = 14
|
const val COMMUNITY_IMAGE_RADIUS_DP = 14
|
||||||
|
const val DEFAULT_IMAGE_HEIGHT_DP = 236
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/layout_creator_channel_community_detail_image_container"
|
android:id="@+id/layout_creator_channel_community_detail_image_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="236dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/spacing_14"
|
android:layout_marginTop="@dimen/spacing_14"
|
||||||
android:background="@drawable/bg_feed_community_image"
|
android:background="@drawable/bg_feed_community_image"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
@@ -147,7 +147,8 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:contentDescription="@string/a11y_feed_content_image"
|
android:contentDescription="@string/a11y_feed_content_image"
|
||||||
android:scaleType="centerCrop" />
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="fitCenter" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/iv_creator_channel_community_detail_play_or_pause"
|
android:id="@+id/iv_creator_channel_community_detail_play_or_pause"
|
||||||
|
|||||||
@@ -50,17 +50,4 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/view_chat_filter_tabs"
|
app:layout_constraintTop_toBottomOf="@id/view_chat_filter_tabs"
|
||||||
tools:text="아직 나눈 대화가 없어요.\n크리에이터와 이야기를 시작해 보세요." />
|
tools:text="아직 나눈 대화가 없어요.\n크리에이터와 이야기를 시작해 보세요." />
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/btn_chat_floating"
|
|
||||||
android:layout_width="@dimen/spacing_48"
|
|
||||||
android:layout_height="@dimen/spacing_48"
|
|
||||||
android:layout_marginEnd="@dimen/spacing_20"
|
|
||||||
android:layout_marginBottom="@dimen/spacing_20"
|
|
||||||
android:background="@drawable/bg_chat_floating_button"
|
|
||||||
android:contentDescription="@string/screen_chat_floating_button"
|
|
||||||
android:scaleType="center"
|
|
||||||
android:src="@drawable/ic_plus_no_bg"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@@ -6,47 +6,8 @@
|
|||||||
android:paddingVertical="20dp">
|
android:paddingVertical="20dp">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/ll_rewarded_ad"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/bg_chat_quota_rewarded_ad_button"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:paddingVertical="12dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:fontFamily="@font/bold"
|
|
||||||
android:includeFontPadding="false"
|
|
||||||
android:text="@string/chat_quota_rewarded_ad_label"
|
|
||||||
android:textColor="@color/color_37474f"
|
|
||||||
android:textSize="24sp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="4dp"
|
|
||||||
android:fontFamily="@font/medium"
|
|
||||||
android:includeFontPadding="false"
|
|
||||||
android:text="@string/chat_quota_separator"
|
|
||||||
android:textColor="@color/color_37474f"
|
|
||||||
android:textSize="18sp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:fontFamily="@font/medium"
|
|
||||||
android:includeFontPadding="false"
|
|
||||||
android:text="@string/chat_quota_rewarded_ad_chat_count"
|
|
||||||
android:textColor="@color/color_37474f"
|
|
||||||
android:textSize="18sp" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:baselineAligned="false"
|
android:baselineAligned="false"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|||||||
@@ -160,7 +160,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:contentDescription="@string/a11y_feed_content_image"
|
android:contentDescription="@string/a11y_feed_content_image"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="fitCenter"
|
||||||
tools:src="@drawable/ic_launcher_background" />
|
tools:src="@drawable/ic_launcher_background" />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -67,8 +67,9 @@
|
|||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/fl_feed_community_image_container"
|
android:id="@+id/fl_feed_community_image_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="236dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/spacing_14"
|
android:layout_marginTop="@dimen/spacing_14"
|
||||||
|
android:background="@drawable/bg_feed_community_image"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
@@ -76,9 +77,8 @@
|
|||||||
android:id="@+id/iv_feed_community_image"
|
android:id="@+id/iv_feed_community_image"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@drawable/bg_feed_community_image"
|
|
||||||
android:contentDescription="@string/a11y_feed_content_image"
|
android:contentDescription="@string/a11y_feed_content_image"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="fitCenter"
|
||||||
tools:src="@drawable/ic_launcher_background" />
|
tools:src="@drawable/ic_launcher_background" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
<string name="chat_quota_20_can">20</string>
|
<string name="chat_quota_20_can">20</string>
|
||||||
<string name="chat_quota_20_can_chat_count">40 chats</string>
|
<string name="chat_quota_20_can_chat_count">40 chats</string>
|
||||||
<string name="chat_quota_separator">/</string>
|
<string name="chat_quota_separator">/</string>
|
||||||
<string name="chat_quota_rewarded_ad_unavailable">Unable to load the ad.</string>
|
<string name="chat_quota_rewarded_ad_unavailable">Unable to load the ad.\nPlease try again in a moment.</string>
|
||||||
<string name="chat_more_title">Chat settings</string>
|
<string name="chat_more_title">Chat settings</string>
|
||||||
<string name="chat_more_background_label">Background image</string>
|
<string name="chat_more_background_label">Background image</string>
|
||||||
<string name="chat_more_background_change">Change background image</string>
|
<string name="chat_more_background_change">Change background image</string>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
<string name="chat_quota_20_can">20</string>
|
<string name="chat_quota_20_can">20</string>
|
||||||
<string name="chat_quota_20_can_chat_count">40チャット</string>
|
<string name="chat_quota_20_can_chat_count">40チャット</string>
|
||||||
<string name="chat_quota_separator">/</string>
|
<string name="chat_quota_separator">/</string>
|
||||||
<string name="chat_quota_rewarded_ad_unavailable">広告を読み込めませんでした。</string>
|
<string name="chat_quota_rewarded_ad_unavailable">広告を読み込めませんでした。\nしばらくしてからもう一度お試しください。</string>
|
||||||
<string name="chat_more_title">トーク設定</string>
|
<string name="chat_more_title">トーク設定</string>
|
||||||
<string name="chat_more_background_label">背景画像</string>
|
<string name="chat_more_background_label">背景画像</string>
|
||||||
<string name="chat_more_background_change">背景画像の変更</string>
|
<string name="chat_more_background_change">背景画像の変更</string>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
<string name="chat_quota_20_can">20</string>
|
<string name="chat_quota_20_can">20</string>
|
||||||
<string name="chat_quota_20_can_chat_count">40채팅</string>
|
<string name="chat_quota_20_can_chat_count">40채팅</string>
|
||||||
<string name="chat_quota_separator">/</string>
|
<string name="chat_quota_separator">/</string>
|
||||||
<string name="chat_quota_rewarded_ad_unavailable">광고를 불러오지 못했어요.</string>
|
<string name="chat_quota_rewarded_ad_unavailable">광고를 불러오지 못했어요\n잠시 후 다시 시도해 주세요.</string>
|
||||||
<string name="chat_more_title">대화 설정</string>
|
<string name="chat_more_title">대화 설정</string>
|
||||||
<string name="chat_more_background_label">배경 이미지</string>
|
<string name="chat_more_background_label">배경 이미지</string>
|
||||||
<string name="chat_more_background_change">배경 이미지 변경</string>
|
<string name="chat_more_background_change">배경 이미지 변경</string>
|
||||||
|
|||||||
@@ -1847,6 +1847,7 @@ class CreatorChannelActivitySourceTest {
|
|||||||
assertTrue(adapter.contains("communityWidthDp.dp()"))
|
assertTrue(adapter.contains("communityWidthDp.dp()"))
|
||||||
assertTrue(adapter.contains("LinearLayout.LayoutParams.WRAP_CONTENT"))
|
assertTrue(adapter.contains("LinearLayout.LayoutParams.WRAP_CONTENT"))
|
||||||
assertTrue(adapter.contains("communityImageUrl = community.imageUrl.takeIf { !it.isNullOrBlank() }"))
|
assertTrue(adapter.contains("communityImageUrl = community.imageUrl.takeIf { !it.isNullOrBlank() }"))
|
||||||
|
assertTrue(adapter.contains("communityRow.applyCommunityImageSize("))
|
||||||
assertTrue(adapter.contains("communityImageView().setImageDrawable(null)"))
|
assertTrue(adapter.contains("communityImageView().setImageDrawable(null)"))
|
||||||
assertTrue(adapter.contains("private const val MAX_COMMUNITY_ITEM_COUNT = 3"))
|
assertTrue(adapter.contains("private const val MAX_COMMUNITY_ITEM_COUNT = 3"))
|
||||||
assertFalse(adapter.contains("CreatorChannelCommunityCardView"))
|
assertFalse(adapter.contains("CreatorChannelCommunityCardView"))
|
||||||
|
|||||||
@@ -46,19 +46,17 @@ class ChatMainFragmentLayoutTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `채팅 fragment layout은 title bar tab list floating button만 포함한다`() {
|
fun `채팅 fragment layout은 title bar tab list만 포함한다`() {
|
||||||
val root = inflateView(R.layout.fragment_v2_main_chat) as ConstraintLayout
|
val root = inflateView(R.layout.fragment_v2_main_chat) as ConstraintLayout
|
||||||
val titleBar = requireNotNull(root.findViewById<View>(R.id.view_chat_title_bar))
|
val titleBar = requireNotNull(root.findViewById<View>(R.id.view_chat_title_bar))
|
||||||
val tabBar = requireNotNull(root.findViewById<CapsuleTabBarView>(R.id.view_chat_filter_tabs))
|
val tabBar = requireNotNull(root.findViewById<CapsuleTabBarView>(R.id.view_chat_filter_tabs))
|
||||||
val recyclerView = requireNotNull(root.findViewById<RecyclerView>(R.id.rv_chat_rooms))
|
val recyclerView = requireNotNull(root.findViewById<RecyclerView>(R.id.rv_chat_rooms))
|
||||||
val floatingButton = requireNotNull(root.findViewById<ImageView>(R.id.btn_chat_floating))
|
|
||||||
val emptyMessage = requireNotNull(root.findViewById<TextView>(R.id.tv_chat_empty_message))
|
val emptyMessage = requireNotNull(root.findViewById<TextView>(R.id.tv_chat_empty_message))
|
||||||
|
|
||||||
assertEquals(Color.BLACK, (root.background as ColorDrawable).color)
|
assertEquals(Color.BLACK, (root.background as ColorDrawable).color)
|
||||||
assertSame(root, titleBar.parent)
|
assertSame(root, titleBar.parent)
|
||||||
assertSame(root, tabBar.parent)
|
assertSame(root, tabBar.parent)
|
||||||
assertSame(root, recyclerView.parent)
|
assertSame(root, recyclerView.parent)
|
||||||
assertSame(root, floatingButton.parent)
|
|
||||||
assertSame(root, emptyMessage.parent)
|
assertSame(root, emptyMessage.parent)
|
||||||
assertEquals(View.GONE, emptyMessage.visibility)
|
assertEquals(View.GONE, emptyMessage.visibility)
|
||||||
assertFalse(root.containsClassName("com.google.android.material.bottomnavigation.BottomNavigationView"))
|
assertFalse(root.containsClassName("com.google.android.material.bottomnavigation.BottomNavigationView"))
|
||||||
@@ -66,17 +64,15 @@ class ChatMainFragmentLayoutTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `채팅 fragment list와 floating button은 계획된 constraint를 사용한다`() {
|
fun `채팅 fragment list는 계획된 constraint를 사용한다`() {
|
||||||
val root = inflateView(R.layout.fragment_v2_main_chat)
|
val root = inflateView(R.layout.fragment_v2_main_chat)
|
||||||
val titleBar = requireNotNull(root.findViewById<View>(R.id.view_chat_title_bar))
|
val titleBar = requireNotNull(root.findViewById<View>(R.id.view_chat_title_bar))
|
||||||
val tabBar = requireNotNull(root.findViewById<CapsuleTabBarView>(R.id.view_chat_filter_tabs))
|
val tabBar = requireNotNull(root.findViewById<CapsuleTabBarView>(R.id.view_chat_filter_tabs))
|
||||||
val recyclerView = requireNotNull(root.findViewById<RecyclerView>(R.id.rv_chat_rooms))
|
val recyclerView = requireNotNull(root.findViewById<RecyclerView>(R.id.rv_chat_rooms))
|
||||||
val floatingButton = requireNotNull(root.findViewById<ImageView>(R.id.btn_chat_floating))
|
|
||||||
val emptyMessage = requireNotNull(root.findViewById<TextView>(R.id.tv_chat_empty_message))
|
val emptyMessage = requireNotNull(root.findViewById<TextView>(R.id.tv_chat_empty_message))
|
||||||
val tabParams = tabBar.layoutParams as ConstraintLayout.LayoutParams
|
val tabParams = tabBar.layoutParams as ConstraintLayout.LayoutParams
|
||||||
val listParams = recyclerView.layoutParams as ConstraintLayout.LayoutParams
|
val listParams = recyclerView.layoutParams as ConstraintLayout.LayoutParams
|
||||||
val emptyParams = emptyMessage.layoutParams as ConstraintLayout.LayoutParams
|
val emptyParams = emptyMessage.layoutParams as ConstraintLayout.LayoutParams
|
||||||
val buttonParams = floatingButton.layoutParams as ConstraintLayout.LayoutParams
|
|
||||||
|
|
||||||
assertEquals(60.dpToPx(), titleBar.layoutParams.height)
|
assertEquals(60.dpToPx(), titleBar.layoutParams.height)
|
||||||
assertEquals(52.dpToPx(), tabBar.layoutParams.height)
|
assertEquals(52.dpToPx(), tabBar.layoutParams.height)
|
||||||
@@ -87,8 +83,6 @@ class ChatMainFragmentLayoutTest {
|
|||||||
assertEquals(ConstraintLayout.LayoutParams.PARENT_ID, emptyParams.bottomToBottom)
|
assertEquals(ConstraintLayout.LayoutParams.PARENT_ID, emptyParams.bottomToBottom)
|
||||||
assertEquals(false, recyclerView.clipToPadding)
|
assertEquals(false, recyclerView.clipToPadding)
|
||||||
assertTrue(recyclerView.paddingBottom >= 28.dpToPx())
|
assertTrue(recyclerView.paddingBottom >= 28.dpToPx())
|
||||||
assertEquals(ConstraintLayout.LayoutParams.PARENT_ID, buttonParams.endToEnd)
|
|
||||||
assertEquals(ConstraintLayout.LayoutParams.PARENT_ID, buttonParams.bottomToBottom)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -141,7 +135,6 @@ class ChatMainFragmentLayoutTest {
|
|||||||
assertTrue(source.contains("ChatRoomActivity.newIntent(requireContext(), item.roomId)"))
|
assertTrue(source.contains("ChatRoomActivity.newIntent(requireContext(), item.roomId)"))
|
||||||
assertTrue(source.contains("ChatRoomType.DM ->"))
|
assertTrue(source.contains("ChatRoomType.DM ->"))
|
||||||
assertTrue(source.contains("DmChatRoomActivity.newIntentByRoomId(requireContext(), item.roomId)"))
|
assertTrue(source.contains("DmChatRoomActivity.newIntentByRoomId(requireContext(), item.roomId)"))
|
||||||
assertTrue(source.contains("btnChatFloating.setOnClickListener { }"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.widget.feed
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class FeedCommunityImageSizeTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 이미지는 원본 비율로 높이를 계산한다`() {
|
||||||
|
val height = calculateFeedCommunityImageHeight(
|
||||||
|
containerWidthPx = 300,
|
||||||
|
intrinsicWidthPx = 1200,
|
||||||
|
intrinsicHeightPx = 800
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(200, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 이미지는 특정 최대 높이로 제한하지 않는다`() {
|
||||||
|
val height = calculateFeedCommunityImageHeight(
|
||||||
|
containerWidthPx = 300,
|
||||||
|
intrinsicWidthPx = 600,
|
||||||
|
intrinsicHeightPx = 1800
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(900, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 이미지는 크기 정보가 없으면 높이를 계산하지 않는다`() {
|
||||||
|
val height = calculateFeedCommunityImageHeight(
|
||||||
|
containerWidthPx = 300,
|
||||||
|
intrinsicWidthPx = 0,
|
||||||
|
intrinsicHeightPx = 800
|
||||||
|
)
|
||||||
|
|
||||||
|
assertNull(height)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,14 +85,15 @@ class FeedViewTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `community layout matches creator channel feed row structure`() {
|
fun `community layout uses runtime image height instead of fixed crop height`() {
|
||||||
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
||||||
val layout = projectFile("app/src/main/res/layout/view_feed_community.xml").readText()
|
val layout = projectFile("app/src/main/res/layout/view_feed_community.xml").readText()
|
||||||
|
|
||||||
assertFalse(layout.contains("@+id/tv_feed_community_keyword"))
|
assertFalse(layout.contains("@+id/tv_feed_community_keyword"))
|
||||||
assertTrue(layout.contains("android:layout_width=\"match_parent\""))
|
assertTrue(layout.contains("android:layout_width=\"match_parent\""))
|
||||||
assertTrue(layout.contains("android:maxLines=\"5\""))
|
assertTrue(layout.contains("android:maxLines=\"5\""))
|
||||||
assertTrue(layout.contains("android:layout_height=\"236dp\""))
|
assertFalse(layout.contains("android:layout_height=\"236dp\""))
|
||||||
|
assertTrue(layout.contains("android:scaleType=\"fitCenter\""))
|
||||||
assertTrue(layout.contains("@drawable/bg_creator_channel_community_price"))
|
assertTrue(layout.contains("@drawable/bg_creator_channel_community_price"))
|
||||||
assertTrue(layout.contains("@drawable/ic_bar_cash"))
|
assertTrue(layout.contains("@drawable/ic_bar_cash"))
|
||||||
assertFalse(layout.contains("android:drawableStart=\"@drawable/ic_bar_cash\""))
|
assertFalse(layout.contains("android:drawableStart=\"@drawable/ic_bar_cash\""))
|
||||||
@@ -152,17 +153,21 @@ class FeedViewTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `community image container keeps creator channel fixed height`() {
|
fun `community image container keeps fallback height until original image ratio is applied`() {
|
||||||
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
||||||
val imageContainer = view.findViewById<FrameLayout>(R.id.fl_feed_community_image_container)
|
val imageContainer = view.findViewById<FrameLayout>(R.id.fl_feed_community_image_container)
|
||||||
|
|
||||||
view.bind(sampleCommunityItem(bodyText = "본문", keywordText = "", imageUrl = "https://example.com/post.png"))
|
view.bind(sampleCommunityItem(bodyText = "본문", keywordText = "", imageUrl = "https://example.com/post.png"))
|
||||||
|
view.measure(exactly(402.dpToPx()), View.MeasureSpec.UNSPECIFIED)
|
||||||
|
|
||||||
|
assertEquals(236.dpToPx(), imageContainer.measuredHeight)
|
||||||
|
|
||||||
|
view.applyCommunityImageSize(intrinsicWidthPx = 600, intrinsicHeightPx = 1800)
|
||||||
view.measure(exactly(402.dpToPx()), View.MeasureSpec.UNSPECIFIED)
|
view.measure(exactly(402.dpToPx()), View.MeasureSpec.UNSPECIFIED)
|
||||||
|
|
||||||
val expectedImageWidth = 402.dpToPx() - view.paddingLeft - view.paddingRight
|
val expectedImageWidth = 402.dpToPx() - view.paddingLeft - view.paddingRight
|
||||||
assertEquals(expectedImageWidth, imageContainer.measuredWidth)
|
assertEquals(expectedImageWidth, imageContainer.measuredWidth)
|
||||||
assertEquals(236.dpToPx(), imageContainer.measuredHeight)
|
assertEquals(expectedImageWidth * 3, imageContainer.measuredHeight)
|
||||||
assertEquals(true, imageContainer.clipToOutline)
|
assertEquals(true, imageContainer.clipToOutline)
|
||||||
assertNotNull(imageContainer.outlineProvider)
|
assertNotNull(imageContainer.outlineProvider)
|
||||||
}
|
}
|
||||||
|
|||||||
78
docs/20260712_v2_크리에이터_커뮤니티_게시글_이미지_원본비율/plan-task.md
Normal file
78
docs/20260712_v2_크리에이터_커뮤니티_게시글_이미지_원본비율/plan-task.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# v2 크리에이터 커뮤니티 게시글 이미지 원본비율 구현 계획/TASK
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
크리에이터 게시물 상세, 메인 홈 추천 탭 인기 커뮤니티 게시물, 메인 홈 팔로잉 탭 최근 소식 커뮤니티 게시물, 크리에이터 채널 커뮤니티 리스트형 아이템의 이미지를 특정 최대 높이 제한 없이 원본 비율로 표시한다.
|
||||||
|
|
||||||
|
## References
|
||||||
|
- PRD: `docs/20260712_v2_크리에이터_커뮤니티_게시글_이미지_원본비율/prd.md`
|
||||||
|
- 상세 layout: `app/src/main/res/layout/activity_creator_channel_community_detail.xml`
|
||||||
|
- 홈 공통 layout: `app/src/main/res/layout/view_feed_community.xml`
|
||||||
|
- 홈 공통 view: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt`
|
||||||
|
- 상세 activity: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/CreatorChannelCommunityDetailActivity.kt`
|
||||||
|
|
||||||
|
## Scenario Contract
|
||||||
|
|
||||||
|
### Scenario 1: 일반 이미지 원본 비율 표시
|
||||||
|
- Pass 조건: 컨테이너 폭 300px, 원본 1200x800 이미지는 높이 200px로 계산된다.
|
||||||
|
- Real surface: 홈/상세 커뮤니티 이미지 영역이 고정 `236dp`가 아니라 로드된 이미지 비율 기반 높이를 사용한다.
|
||||||
|
- Test: `FeedCommunityImageSizeTest#커뮤니티 이미지는 원본 비율로 높이를 계산한다`
|
||||||
|
|
||||||
|
### Scenario 2: 세로 이미지 최대 높이 미제한
|
||||||
|
- Pass 조건: 컨테이너 폭 300px, 원본 600x1800 이미지는 높이 900px로 계산된다.
|
||||||
|
- Real surface: 세로로 긴 커뮤니티 이미지도 특정 최대 높이로 자르지 않고 원본 비율대로 표시한다.
|
||||||
|
- Test: `FeedCommunityImageSizeTest#커뮤니티 이미지는 특정 최대 높이로 제한하지 않는다`
|
||||||
|
|
||||||
|
### Scenario 3: 인접 동작 회귀 방지
|
||||||
|
- Pass 조건: 이미지가 없는 커뮤니티 게시글은 이미지 컨테이너가 기존처럼 `GONE`이다.
|
||||||
|
- Real surface: 이미지 없는 홈/상세 게시글의 텍스트/반응 영역 표시가 유지된다.
|
||||||
|
- Test: `FeedViewTest#community recommendation image is hidden when image url is null`
|
||||||
|
|
||||||
|
### Scenario 4: 로드 전 높이와 둥근 모서리 유지
|
||||||
|
- Pass 조건: 이미지 URL이 있는 커뮤니티 게시글은 로드 성공 전 기본 높이를 유지하고, 로드 성공 후 원본 비율 높이를 적용한다.
|
||||||
|
- Real surface: 홈 추천 인기 커뮤니티 이미지가 사라지지 않고, 홈 팔로잉/상세/리스트 이미지 둥근 모서리가 유지된다.
|
||||||
|
- Test: `FeedViewTest#community image container keeps fallback height until original image ratio is applied`
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
- [x] **Task 1: RED 테스트 작성**
|
||||||
|
- 순수 높이 계산 테스트를 추가한다.
|
||||||
|
- 기존 `236dp` 고정 높이 테스트를 원본 비율 기반 런타임 높이 테스트로 변경한다.
|
||||||
|
- 검증: 구현 전 테스트가 실패한다.
|
||||||
|
|
||||||
|
- [x] **Task 2: 공통 사이징 helper 구현**
|
||||||
|
- `FeedCommunityImageSize.kt`에 특정 최대 높이 제한 없는 원본 비율 높이 계산 로직을 추가한다.
|
||||||
|
- 검증: helper 테스트가 통과한다.
|
||||||
|
|
||||||
|
- [x] **Task 3: 홈 공통 커뮤니티 카드 적용**
|
||||||
|
- `view_feed_community.xml`의 `236dp` 고정 높이와 `centerCrop`을 제거한다.
|
||||||
|
- `FeedCommunityView`에 로드된 이미지 크기를 적용하는 메서드를 추가한다.
|
||||||
|
- 홈 추천/팔로잉 어댑터의 이미지 로드 성공 시 원본 크기로 높이를 적용한다.
|
||||||
|
- 검증: `FeedViewTest`가 통과한다.
|
||||||
|
|
||||||
|
- [x] **Task 4: 상세 화면 적용**
|
||||||
|
- 상세 layout의 `236dp` 고정 높이와 `centerCrop`을 제거한다.
|
||||||
|
- Glide 로드 성공 시 원본 크기로 높이를 적용한다.
|
||||||
|
- 검증: 컴파일 통과.
|
||||||
|
|
||||||
|
- [x] **Task 5: 최종 검증과 문서 기록**
|
||||||
|
- 관련 단위 테스트, Kotlin 컴파일, ktlint/build를 실행한다.
|
||||||
|
- 가능한 경우 실제 표면을 실행 확인하고, 불가하면 환경 제약을 기록한다.
|
||||||
|
|
||||||
|
- [x] **Task 6: 후속 버그 수정 반영**
|
||||||
|
- 이미지 높이 특정 최대 제한을 제거한다.
|
||||||
|
- 홈 공통 커뮤니티 이미지 컨테이너의 기본 높이와 clipping 배경을 복구한다.
|
||||||
|
- 상세/리스트 이미지 컨테이너에 clipping 기반 둥근 모서리를 적용한다.
|
||||||
|
- 크리에이터 채널 커뮤니티 리스트형 아이템에 원본 비율 sizing을 적용한다.
|
||||||
|
- 검증: 관련 단위 테스트와 Gradle 검증 명령으로 확인한다.
|
||||||
|
|
||||||
|
## Verification Log
|
||||||
|
- 2026-07-12: 사용자 요청, 기존 코드 조사 결과, Plan Agent 결과를 기준으로 구현 계획/TASK를 작성했다. 구현 전 문서 작성 단계이므로 빌드/테스트는 아직 실행하지 않았다.
|
||||||
|
- 2026-07-12: RED 확인. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityImageSizeTest" --tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedViewTest"` 실행 결과 신규 `calculateFeedCommunityImageHeight`, `applyCommunityImageSize` 미구현 컴파일 오류가 발생했다. 동일 실행에서 기존 `ChatMainFragmentLayoutTest.kt`의 `btn_chat_floating` 미해결 참조도 함께 노출됐다.
|
||||||
|
- 2026-07-12: 구현 후 `./gradlew --no-daemon :app:compileDebugKotlin` 실행 결과 BUILD SUCCESSFUL을 확인했다.
|
||||||
|
- 2026-07-12: 구현 후 `./gradlew --no-daemon :app:assembleDebug` 실행 결과 BUILD SUCCESSFUL을 확인했다.
|
||||||
|
- 2026-07-12: 구현 후 `./gradlew --no-daemon :app:ktlintCheck` 실행 결과 BUILD SUCCESSFUL을 확인했다. 기존 `.editorconfig disabled_rules` deprecation warning은 출력됐으나 lint 실패는 없었다.
|
||||||
|
- 2026-07-12: 대상 단위 테스트 재실행은 `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragmentLayoutTest.kt:54`, `:74`의 기존 `btn_chat_floating` 미해결 참조로 `compileDebugUnitTestKotlin` 단계에서 차단됐다. 신규 테스트 자체 실행 단계에는 도달하지 못했다.
|
||||||
|
- 2026-07-12: `adb devices` 실행 결과 연결된 기기/에뮬레이터가 없어 홈 추천/팔로잉/상세 화면 실기기 수동 확인은 수행하지 못했다.
|
||||||
|
- 2026-07-12: 후속 요청의 4개 버그 원인을 확인했다. 홈 추천 이미지는 `wrap_content` 컨테이너와 `match_parent` 이미지 조합으로 로드 전 높이가 0이 될 수 있었고, 둥근 모서리는 이미지가 아닌 컨테이너 clipping 책임이 불완전했으며, 크리에이터 채널 커뮤니티 리스트형 아이템은 별도 경로라 기존 `236dp`/`centerCrop` 정책이 남아 있었다.
|
||||||
|
- 2026-07-12: 후속 버그 수정 후 `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityImageSizeTest" --tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedViewTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest"` 실행 결과 BUILD SUCCESSFUL을 확인했다.
|
||||||
|
- 2026-07-12: 후속 버그 수정 후 `./gradlew --no-daemon :app:compileDebugKotlin`, `./gradlew --no-daemon :app:ktlintCheck`, `./gradlew --no-daemon :app:assembleDebug` 실행 결과 모두 BUILD SUCCESSFUL을 확인했다. ktlint 실행 중 기존 `.editorconfig disabled_rules` deprecation warning은 출력됐으나 실패는 없었다.
|
||||||
91
docs/20260712_v2_크리에이터_커뮤니티_게시글_이미지_원본비율/prd.md
Normal file
91
docs/20260712_v2_크리에이터_커뮤니티_게시글_이미지_원본비율/prd.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# PRD: v2 크리에이터 커뮤니티 게시글 이미지 원본비율
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
v2 크리에이터 커뮤니티 게시글 이미지가 고정 높이로 잘리지 않고 원본 이미지 비율대로 표시되도록 조정한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Problem
|
||||||
|
- 현재 크리에이터 커뮤니티 게시물 상세, 메인 홈 추천 탭 인기 커뮤니티 게시물, 메인 홈 팔로잉 탭 최근 소식 커뮤니티 게시물의 이미지는 `236dp` 고정 높이와 `centerCrop`으로 표시된다.
|
||||||
|
- 크리에이터 채널 커뮤니티 리스트형 아이템도 별도 레이아웃/어댑터를 사용해 동일한 고정 높이와 crop 정책이 남아 있다.
|
||||||
|
- 원본 이미지 비율과 다른 이미지가 잘려 보일 수 있다.
|
||||||
|
- 이전 최대 높이 제한 구현 후 일부 화면에서 이미지가 사라지거나 둥근 모서리가 사라지는 회귀가 발생했다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
- 요청된 커뮤니티 게시글 이미지를 특정 최대 높이로 제한하지 않고 원본 이미지 비율대로 표시한다.
|
||||||
|
- 이미지 로드 전에는 기존 기본 높이를 임시로 유지해 이미지 영역이 사라지지 않도록 한다.
|
||||||
|
- 기존 유료 잠금 overlay, 오디오 재생 버튼, 댓글/좋아요 영역 동작은 유지한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Non-Goals
|
||||||
|
- 레거시 커뮤니티 목록/상세 화면은 변경하지 않는다.
|
||||||
|
- API/DTO/model 필드는 변경하지 않는다.
|
||||||
|
- 이미지 업로드/크롭 정책은 변경하지 않는다.
|
||||||
|
- 별도 최소 높이, placeholder 비율 정책은 추가하지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Target Users
|
||||||
|
- 크리에이터 커뮤니티 게시글을 보는 사용자
|
||||||
|
- 메인 홈 추천/팔로잉 탭에서 커뮤니티 게시글을 탐색하는 사용자
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. User Stories
|
||||||
|
- 사용자는 커뮤니티 게시글 이미지를 원본 비율대로 확인하고 싶다.
|
||||||
|
- 사용자는 이미지 로드 전후에도 커뮤니티 이미지 영역과 둥근 모서리가 안정적으로 유지되기를 원한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Core Features
|
||||||
|
|
||||||
|
### Feature A: 커뮤니티 이미지 원본비율 표시
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- 크리에이터 게시물 상세 이미지는 원본 이미지 비율로 표시한다.
|
||||||
|
- 메인 홈 추천 탭 인기 커뮤니티 게시물 이미지는 원본 이미지 비율로 표시한다.
|
||||||
|
- 메인 홈 팔로잉 탭 최근 소식 커뮤니티 게시물 이미지는 원본 이미지 비율로 표시한다.
|
||||||
|
- 크리에이터 채널 커뮤니티 리스트형 아이템 이미지는 원본 이미지 비율로 표시한다.
|
||||||
|
- 계산된 이미지 높이를 특정 최대 높이로 제한하지 않는다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 원본 이미지 크기 정보가 없으면 기존 화면이 깨지지 않도록 높이를 강제로 계산하지 않는다.
|
||||||
|
- 유료 미구매 overlay는 계산된 이미지 영역 전체를 덮는다.
|
||||||
|
- 이미지가 없는 게시글은 기존처럼 이미지 영역을 숨긴다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. UX / UI Expectations
|
||||||
|
- 이미지는 잘리지 않고 원본 비율 기준으로 표시된다.
|
||||||
|
- `236dp` 고정 높이로 인한 crop은 제거된다.
|
||||||
|
- 로드 성공 전 임시 기본 높이는 유지하되, 로드 성공 후 원본 비율 높이로 교체된다.
|
||||||
|
- 이미지 컨테이너의 둥근 모서리는 홈/상세/리스트 화면에서 유지된다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Technical Constraints
|
||||||
|
- 신규 코드는 `kr.co.vividnext.sodalive.v2` 하위에 둔다.
|
||||||
|
- 레거시 파일은 직접 수정하지 않는다.
|
||||||
|
- 홈 추천/팔로잉 커뮤니티 카드는 공통 `FeedCommunityView`와 `view_feed_community.xml` 변경으로 처리한다.
|
||||||
|
- 상세 화면은 `CreatorChannelCommunityDetailActivity`와 해당 layout에서 최소 변경한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Metrics
|
||||||
|
- 순수 로직 테스트에서 특정 최대 높이 제한 없는 원본 비율 계산이 통과한다.
|
||||||
|
- 관련 Gradle 테스트/빌드가 통과한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Open Questions
|
||||||
|
- 현재 추가 확인이 필요한 사항은 없다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Log
|
||||||
|
- 2026-07-12: 사용자 요청과 기존 조사 결과를 기준으로 PRD를 작성했다. 구현 전 문서 작성 단계이므로 빌드/테스트는 아직 실행하지 않았다.
|
||||||
|
- 2026-07-12: 구현 결과로 요청된 3개 위치의 커뮤니티 게시글 이미지 고정 `236dp`/`centerCrop` 표시를 제거하고, 이미지 로드 성공 시 원본 비율 기반 높이를 적용하되 기기 높이의 1/2로 제한하도록 반영했다. 이미지 URL이 없는 유료 잠금/paywall-only 영역은 원본 비율 계산 대상이 아니므로 기존 기본 높이를 유지한다.
|
||||||
|
- 2026-07-12: 후속 버그 수정 요청에 따라 특정 최대 높이 제한 요구사항을 제거하고, 이미지 로드 전 기본 높이 유지, 컨테이너 clipping 기반 둥근 모서리 유지, 크리에이터 채널 커뮤니티 리스트형 아이템 원본 비율 표시를 추가 범위로 반영했다.
|
||||||
Reference in New Issue
Block a user