diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedAdapter.kt
index f7a936d7..41fadac2 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedAdapter.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedAdapter.kt
@@ -28,8 +28,14 @@ class FeedAdapter(
return when (viewType) {
VIEW_TYPE_RANK -> RankViewHolder(inflater.inflate(R.layout.view_feed_rank, parent, false) as FeedRankView, parent)
VIEW_TYPE_LIVE -> LiveViewHolder(inflater.inflate(R.layout.view_feed_live, parent, false) as FeedLiveView, parent)
- VIEW_TYPE_CONTENT -> ContentViewHolder(inflater.inflate(R.layout.view_feed_content, parent, false) as FeedContentView, parent)
- VIEW_TYPE_COMMUNITY -> CommunityViewHolder(inflater.inflate(R.layout.view_feed_community, parent, false) as FeedCommunityView, parent)
+ VIEW_TYPE_CONTENT -> ContentViewHolder(
+ inflater.inflate(R.layout.view_feed_content, parent, false) as FeedContentView,
+ parent
+ )
+ VIEW_TYPE_COMMUNITY -> CommunityViewHolder(
+ inflater.inflate(R.layout.view_feed_community, parent, false) as FeedCommunityView,
+ parent
+ )
else -> error("Unknown viewType: $viewType")
}
}
@@ -95,7 +101,7 @@ class FeedAdapter(
view.setFeedSize(calculateSize(item.variant, parent))
view.bind(item)
view.setOnFeedClick(onClickItem)
- onBindImages(FeedImageViews(profile = view.profileImageView()), item)
+ onBindImages(FeedImageViews(primary = view.communityImageView(), profile = view.profileImageView()), item)
}
}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt
index 73bea907..863d92dd 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt
@@ -23,6 +23,10 @@ class FeedCommunityView @JvmOverloads constructor(
private var createdAtText: TextView? = null
private var bodyText: TextView? = null
private var keywordText: TextView? = null
+ private var communityImageContainer: View? = null
+ private var communityImage: ImageView? = null
+ private var paidOverlay: View? = null
+ private var priceText: TextView? = null
private var commentCountText: TextView? = null
private var likeCountText: TextView? = null
private var currentItem: FeedItem.Community? = null
@@ -36,10 +40,23 @@ class FeedCommunityView @JvmOverloads constructor(
createdAtText = findViewById(R.id.tv_feed_community_created_at)
bodyText = findViewById(R.id.tv_feed_community_body)
keywordText = findViewById(R.id.tv_feed_community_keyword)
+ communityImageContainer = findViewById(R.id.fl_feed_community_image_container)
+ communityImage = findViewById(R.id.iv_feed_community_image)
+ paidOverlay = findViewById(R.id.ll_feed_community_paid_overlay)
+ priceText = findViewById(R.id.tv_feed_community_price)
commentCountText = findViewById(R.id.tv_feed_community_comment_count)
likeCountText = findViewById(R.id.tv_feed_community_like_count)
+ clipToOutline = true
+ outlineProvider = roundedOutlineProvider(CARD_RADIUS_DP)
profileImageView().clipToOutline = true
profileImageView().outlineProvider = circleOutlineProvider()
+ requireNotNull(communityImageContainer).clipToOutline = true
+ requireNotNull(communityImageContainer).outlineProvider = roundedOutlineProvider(COMMUNITY_IMAGE_RADIUS_DP)
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ updateCommunityImageHeight(MeasureSpec.getSize(widthMeasureSpec))
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
fun bind(item: FeedItem.Community) {
@@ -49,7 +66,10 @@ class FeedCommunityView @JvmOverloads constructor(
requireNotNull(bodyText).text = item.bodyText
requireNotNull(keywordText).text = item.keywordText
requireNotNull(bodyText).visibility = visibilityForText(item.bodyText)
- requireNotNull(keywordText).visibility = visibilityForText(item.keywordText)
+ requireNotNull(keywordText).visibility = if (item.showKeyword) visibilityForText(item.keywordText) else View.GONE
+ requireNotNull(communityImageContainer).visibility = if (item.imageUrl.isNullOrBlank()) View.GONE else View.VISIBLE
+ requireNotNull(paidOverlay).visibility = if (item.price > 0 && !item.existOrdered) View.VISIBLE else View.GONE
+ requireNotNull(priceText).text = item.price.toString()
requireNotNull(commentCountText).text = item.commentCount.toString()
requireNotNull(likeCountText).text = item.likeCount.toString()
applyClickState(item)
@@ -57,6 +77,10 @@ class FeedCommunityView @JvmOverloads constructor(
fun profileImageView(): ImageView = requireNotNull(profileImage)
+ fun communityImageView(): ImageView = requireNotNull(communityImage)
+
+ fun boundItem(): FeedItem.Community? = currentItem
+
fun setFeedSize(size: FeedSize) {
updateRootWidth(size.rootWidthDp.dpToPx())
}
@@ -89,11 +113,38 @@ class FeedCommunityView @JvmOverloads constructor(
}
}
+ private fun updateCommunityImageHeight(rootWidth: Int) {
+ val imageWidth = rootWidth - paddingLeft - paddingRight
+ if (imageWidth <= 0) return
+
+ val container = requireNotNull(communityImageContainer)
+ val currentLayoutParams = container.layoutParams
+ val nextHeight = (imageWidth * COMMUNITY_IMAGE_FIGMA_HEIGHT_DP / COMMUNITY_IMAGE_FIGMA_WIDTH_DP.toFloat())
+ .roundToInt()
+ if (currentLayoutParams.height != nextHeight) {
+ currentLayoutParams.height = nextHeight
+ container.layoutParams = currentLayoutParams
+ }
+ }
+
private fun circleOutlineProvider() = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setOval(0, 0, view.width, view.height)
}
}
+ private fun roundedOutlineProvider(radiusDp: Int) = object : ViewOutlineProvider() {
+ override fun getOutline(view: View, outline: Outline) {
+ outline.setRoundRect(0, 0, view.width, view.height, radiusDp.dpToPx().toFloat())
+ }
+ }
+
private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).roundToInt()
+
+ private companion object {
+ const val CARD_RADIUS_DP = 14
+ const val COMMUNITY_IMAGE_RADIUS_DP = 14
+ const val COMMUNITY_IMAGE_FIGMA_WIDTH_DP = 346
+ const val COMMUNITY_IMAGE_FIGMA_HEIGHT_DP = 236
+ }
}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItem.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItem.kt
index f836e3fe..21cbf5a4 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItem.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItem.kt
@@ -42,6 +42,11 @@ sealed class FeedItem(open val feedId: String, val variant: FeedVariant) {
val keywordText: String,
val createdAtText: String,
val commentCount: Int,
- val likeCount: Int
+ val likeCount: Int,
+ val imageUrl: String? = null,
+ val audioUrl: String? = null,
+ val price: Int = 0,
+ val existOrdered: Boolean = false,
+ val showKeyword: Boolean = true
) : FeedItem(feedId, FeedVariant.Community)
}
diff --git a/app/src/main/res/drawable/bg_feed_community_image.xml b/app/src/main/res/drawable/bg_feed_community_image.xml
new file mode 100644
index 00000000..c38ad032
--- /dev/null
+++ b/app/src/main/res/drawable/bg_feed_community_image.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/view_feed_community.xml b/app/src/main/res/layout/view_feed_community.xml
index 8b9407dd..32c4aec0 100644
--- a/app/src/main/res/layout/view_feed_community.xml
+++ b/app/src/main/res/layout/view_feed_community.xml
@@ -1,10 +1,9 @@
@@ -75,6 +74,51 @@
android:textColor="@color/soda_400"
tools:text="#키워드 #키워드 #키워드" />
+
+
+
+
+
+
+
+
+
+
+
+
(R.id.tv_feed_community_keyword).visibility)
}
+ @Test
+ fun `community keeps keyword visible by default for existing feed usage`() {
+ val view = inflateView(R.layout.view_feed_community)
+
+ view.bind(sampleCommunityItem(bodyText = "본문", keywordText = "#키워드"))
+
+ assertEquals(View.VISIBLE, view.findViewById(R.id.tv_feed_community_keyword).visibility)
+ }
+
+ @Test
+ fun `community recommendation image is hidden when image url is null`() {
+ val view = inflateView(R.layout.view_feed_community)
+
+ view.bind(sampleCommunityItem(bodyText = "본문", keywordText = "#키워드", imageUrl = null))
+
+ assertEquals(View.GONE, view.findViewById(R.id.fl_feed_community_image_container).visibility)
+ }
+
+ @Test
+ fun `community paid image shows lock overlay and price capsule only when not purchased`() {
+ val view = inflateView(R.layout.view_feed_community)
+
+ view.bind(
+ sampleCommunityItem(
+ bodyText = "본문",
+ keywordText = "",
+ imageUrl = "https://example.com/post.png",
+ price = 30,
+ existOrdered = false,
+ showKeyword = false
+ )
+ )
+
+ assertEquals(View.GONE, view.findViewById(R.id.tv_feed_community_keyword).visibility)
+ assertEquals(View.VISIBLE, view.findViewById(R.id.fl_feed_community_image_container).visibility)
+ assertEquals(View.VISIBLE, view.findViewById(R.id.ll_feed_community_paid_overlay).visibility)
+ assertEquals("30", view.findViewById(R.id.tv_feed_community_price).text.toString())
+ }
+
+ @Test
+ fun `community purchased paid image hides lock overlay and price capsule`() {
+ val view = inflateView(R.layout.view_feed_community)
+
+ view.bind(
+ sampleCommunityItem(
+ bodyText = "본문",
+ keywordText = "",
+ imageUrl = "https://example.com/post.png",
+ price = 30,
+ existOrdered = true,
+ showKeyword = false
+ )
+ )
+
+ assertEquals(View.VISIBLE, view.findViewById(R.id.fl_feed_community_image_container).visibility)
+ assertEquals(View.GONE, view.findViewById(R.id.ll_feed_community_paid_overlay).visibility)
+ }
+
+ @Test
+ fun `community image container follows measured card width ratio`() {
+ val view = inflateView(R.layout.view_feed_community)
+ val imageContainer = view.findViewById(R.id.fl_feed_community_image_container)
+
+ view.bind(sampleCommunityItem(bodyText = "본문", keywordText = "", imageUrl = "https://example.com/post.png"))
+
+ view.measure(exactly(402.dpToPx()), View.MeasureSpec.UNSPECIFIED)
+
+ val expectedImageWidth = 402.dpToPx() - view.paddingLeft - view.paddingRight
+ assertEquals(expectedImageWidth, imageContainer.measuredWidth)
+ assertEquals((expectedImageWidth * 236 / 346f).roundToInt(), imageContainer.measuredHeight)
+ assertEquals(true, imageContainer.clipToOutline)
+ assertNotNull(imageContainer.outlineProvider)
+ }
+
@Test
fun `rank overlay uses figma overflow position`() {
val view = inflateView(R.layout.view_feed_rank)
@@ -98,7 +174,12 @@ class FeedViewTest {
private fun sampleCommunityItem(
bodyText: String,
- keywordText: String
+ keywordText: String,
+ imageUrl: String? = null,
+ audioUrl: String? = null,
+ price: Int = 0,
+ existOrdered: Boolean = false,
+ showKeyword: Boolean = true
) = FeedItem.Community(
feedId = "feed-community-1",
creatorId = "creator-1",
@@ -109,7 +190,12 @@ class FeedViewTest {
keywordText = keywordText,
createdAtText = "2분 전",
commentCount = 5,
- likeCount = 6
+ likeCount = 6,
+ imageUrl = imageUrl,
+ audioUrl = audioUrl,
+ price = price,
+ existOrdered = existOrdered,
+ showKeyword = showKeyword
)
private fun Int.dpToPx(): Int {
@@ -117,6 +203,8 @@ class FeedViewTest {
return (this * context.resources.displayMetrics.density).toInt()
}
+ private fun exactly(size: Int): Int = View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY)
+
private fun assertNotEmptyContentDescription(imageView: ImageView) {
assertTrue(!imageView.contentDescription.isNullOrEmpty())
}