feat(creator): 채널 홈 시리즈, 커뮤니티, 팬 Talk 섹션을 재구성한다
This commit is contained in:
@@ -82,11 +82,8 @@ data class CreatorChannelSeriesResponse(
|
|||||||
@SerializedName("seriesId") val seriesId: Long,
|
@SerializedName("seriesId") val seriesId: Long,
|
||||||
@SerializedName("title") val title: String,
|
@SerializedName("title") val title: String,
|
||||||
@SerializedName("coverImageUrl") val coverImageUrl: String,
|
@SerializedName("coverImageUrl") val coverImageUrl: String,
|
||||||
@SerializedName("publishedDaysOfWeek") val publishedDaysOfWeek: String,
|
|
||||||
@SerializedName("isComplete") val isComplete: Boolean,
|
|
||||||
@SerializedName("numberOfContent") val numberOfContent: Int,
|
@SerializedName("numberOfContent") val numberOfContent: Int,
|
||||||
@SerializedName("isNew") val isNew: Boolean,
|
@SerializedName("isNew") val isNew: Boolean,
|
||||||
@SerializedName("isPopular") val isPopular: Boolean,
|
|
||||||
@SerializedName("isOriginal") val isOriginal: Boolean
|
@SerializedName("isOriginal") val isOriginal: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ fun CreatorChannelHomeResponse.toUiContent(): CreatorChannelHomeUiState.Content
|
|||||||
audioContents.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.AudioContents(it)) }
|
audioContents.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.AudioContents(it)) }
|
||||||
series.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Series(it)) }
|
series.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Series(it)) }
|
||||||
communities.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Communities(it)) }
|
communities.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Communities(it)) }
|
||||||
fanTalk.takeIf { it.totalCount > 0 || it.latestFanTalk != null }?.let { add(CreatorChannelHomeSection.FanTalk(it)) }
|
add(CreatorChannelHomeSection.FanTalk(fanTalk))
|
||||||
introduce.takeIf { it.isNotBlank() }?.let { add(CreatorChannelHomeSection.Introduce(it)) }
|
introduce.takeIf { it.isNotBlank() }?.let { add(CreatorChannelHomeSection.Introduce(it)) }
|
||||||
add(CreatorChannelHomeSection.Activity(activity))
|
add(CreatorChannelHomeSection.Activity(activity))
|
||||||
sns.toUiItems().takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Sns(it)) }
|
sns.toUiItems().takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Sns(it)) }
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Outline
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewOutlineProvider
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
|
||||||
|
class CreatorChannelFanTalkCardView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
clipToOutline = true
|
||||||
|
outlineProvider = object : ViewOutlineProvider() {
|
||||||
|
override fun getOutline(view: View, outline: Outline) {
|
||||||
|
outline.setRoundRect(0, 0, view.width, view.height, resources.getDimension(R.dimen.radius_14))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import android.view.Gravity
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.HorizontalScrollView
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
@@ -18,11 +17,17 @@ import androidx.recyclerview.widget.GridLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
|
import kr.co.vividnext.sodalive.common.image.BlurTransformation
|
||||||
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.data.CreatorChannelAudioContentResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelCommunityPostResponse
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeSection
|
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeSection
|
||||||
|
import kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityView
|
||||||
|
import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem
|
||||||
|
import kr.co.vividnext.sodalive.v2.widget.feed.FeedSize
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
@@ -30,7 +35,8 @@ import kotlin.math.roundToInt
|
|||||||
|
|
||||||
class CreatorChannelHomeSectionAdapter(
|
class CreatorChannelHomeSectionAdapter(
|
||||||
private val onScheduleClick: (CreatorChannelScheduleResponse) -> Unit = {},
|
private val onScheduleClick: (CreatorChannelScheduleResponse) -> Unit = {},
|
||||||
private val onAudioContentClick: (CreatorChannelAudioContentResponse) -> Unit = {}
|
private val onAudioContentClick: (CreatorChannelAudioContentResponse) -> Unit = {},
|
||||||
|
private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit = {}
|
||||||
) : RecyclerView.Adapter<CreatorChannelHomeSectionAdapter.SectionViewHolder>() {
|
) : RecyclerView.Adapter<CreatorChannelHomeSectionAdapter.SectionViewHolder>() {
|
||||||
|
|
||||||
private var items: List<CreatorChannelHomeSection> = emptyList()
|
private var items: List<CreatorChannelHomeSection> = emptyList()
|
||||||
@@ -44,7 +50,7 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionViewHolder {
|
||||||
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||||
return SectionViewHolder(view, onScheduleClick, onAudioContentClick)
|
return SectionViewHolder(view, onScheduleClick, onAudioContentClick, onSeriesClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: SectionViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: SectionViewHolder, position: Int) {
|
||||||
@@ -56,7 +62,8 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
class SectionViewHolder(
|
class SectionViewHolder(
|
||||||
view: View,
|
view: View,
|
||||||
private val onScheduleClick: (CreatorChannelScheduleResponse) -> Unit,
|
private val onScheduleClick: (CreatorChannelScheduleResponse) -> Unit,
|
||||||
private val onAudioContentClick: (CreatorChannelAudioContentResponse) -> Unit
|
private val onAudioContentClick: (CreatorChannelAudioContentResponse) -> Unit,
|
||||||
|
private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit
|
||||||
) : RecyclerView.ViewHolder(view) {
|
) : RecyclerView.ViewHolder(view) {
|
||||||
private val title: TextView? = view.findViewById(R.id.tv_section_title)
|
private val title: TextView? = view.findViewById(R.id.tv_section_title)
|
||||||
private val sectionItems: LinearLayout? = view.findViewById(R.id.ll_section_items)
|
private val sectionItems: LinearLayout? = view.findViewById(R.id.ll_section_items)
|
||||||
@@ -74,6 +81,15 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
private val scheduleTimeline: LinearLayout? = view.findViewById(R.id.ll_schedule_timeline)
|
private val scheduleTimeline: LinearLayout? = view.findViewById(R.id.ll_schedule_timeline)
|
||||||
private val scheduleItems: LinearLayout? = view.findViewById(R.id.ll_schedule_items)
|
private val scheduleItems: LinearLayout? = view.findViewById(R.id.ll_schedule_items)
|
||||||
private val audioContentsRecyclerView: RecyclerView? = view.findViewById(R.id.rv_audio_contents)
|
private val audioContentsRecyclerView: RecyclerView? = view.findViewById(R.id.rv_audio_contents)
|
||||||
|
private val seriesItems: LinearLayout? = view.findViewById(R.id.ll_series_items)
|
||||||
|
private val communityItems: LinearLayout? = view.findViewById(R.id.ll_community_items)
|
||||||
|
private val fanTalkCard: View? = view.findViewById(R.id.layout_fantalk_card)
|
||||||
|
private val fanTalkTotalRow: View? = view.findViewById(R.id.layout_fantalk_total_row)
|
||||||
|
private val fanTalkLatestRow: View? = view.findViewById(R.id.layout_fantalk_latest_row)
|
||||||
|
private val fanTalkEmpty: View? = view.findViewById(R.id.layout_fantalk_empty)
|
||||||
|
private val fanTalkTotalCount: TextView? = view.findViewById(R.id.tv_fantalk_total_count)
|
||||||
|
private val fanTalkProfile: ImageView? = view.findViewById(R.id.iv_fantalk_profile)
|
||||||
|
private val fanTalkContent: TextView? = view.findViewById(R.id.tv_fantalk_content)
|
||||||
private val audioContentGridAdapter = AudioContentGridAdapter(
|
private val audioContentGridAdapter = AudioContentGridAdapter(
|
||||||
itemWidth = calculateCreatorChannelAudioItemWidthDp(itemView.resources.configuration.screenWidthDp).dp(),
|
itemWidth = calculateCreatorChannelAudioItemWidthDp(itemView.resources.configuration.screenWidthDp).dp(),
|
||||||
onAudioContentClick = onAudioContentClick
|
onAudioContentClick = onAudioContentClick
|
||||||
@@ -90,6 +106,8 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
noticeItems?.removeAllViews()
|
noticeItems?.removeAllViews()
|
||||||
scheduleTimeline?.removeAllViews()
|
scheduleTimeline?.removeAllViews()
|
||||||
scheduleItems?.removeAllViews()
|
scheduleItems?.removeAllViews()
|
||||||
|
seriesItems?.removeAllViews()
|
||||||
|
communityItems?.removeAllViews()
|
||||||
when (item) {
|
when (item) {
|
||||||
is CreatorChannelHomeSection.CurrentLive -> bindCurrentLive(item)
|
is CreatorChannelHomeSection.CurrentLive -> bindCurrentLive(item)
|
||||||
is CreatorChannelHomeSection.LatestAudioContent -> bindLatestAudioContent(item)
|
is CreatorChannelHomeSection.LatestAudioContent -> bindLatestAudioContent(item)
|
||||||
@@ -256,54 +274,118 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun bindSeries(item: CreatorChannelHomeSection.Series) {
|
private fun bindSeries(item: CreatorChannelHomeSection.Series) {
|
||||||
val row = createHorizontalRow()
|
val visibleSeries = item.series.take(MAX_SERIES_ITEM_COUNT)
|
||||||
item.series.forEach { series ->
|
visibleSeries.forEachIndexed { index, series ->
|
||||||
row.addView(
|
val seriesWidthDp = calculateCreatorChannelSeriesCardWidthDp(itemView.resources.configuration.screenWidthDp)
|
||||||
createContentTile(
|
val row = LayoutInflater.from(itemView.context).inflate(
|
||||||
title = series.title,
|
R.layout.item_creator_channel_home_series_content,
|
||||||
body = itemView.context.getString(
|
seriesItems,
|
||||||
R.string.creator_channel_series_summary,
|
false
|
||||||
series.numberOfContent,
|
|
||||||
series.publishedDaysOfWeek
|
|
||||||
),
|
|
||||||
imageUrl = series.coverImageUrl,
|
|
||||||
imageWidth = 163.dp(),
|
|
||||||
imageHeight = 230.dp()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
row.layoutParams = LinearLayout.LayoutParams(
|
||||||
|
seriesWidthDp.dp(),
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
).apply {
|
||||||
|
marginEnd = if (index == visibleSeries.lastIndex) 0 else 4.dp()
|
||||||
|
}
|
||||||
|
(row as CreatorChannelHomeSeriesCardView).apply {
|
||||||
|
setThumbnailSize(seriesWidthDp, calculateCreatorChannelSeriesCardHeightDp(seriesWidthDp))
|
||||||
|
bind(series)
|
||||||
|
}
|
||||||
|
row.setOnClickListener { onSeriesClick(series) }
|
||||||
|
seriesItems?.addView(row)
|
||||||
}
|
}
|
||||||
sectionItems?.addView(createHorizontalScrollRow(row))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindCommunities(item: CreatorChannelHomeSection.Communities) {
|
private fun bindCommunities(item: CreatorChannelHomeSection.Communities) {
|
||||||
item.communities.forEach { community ->
|
val visibleCommunities = item.communities.take(MAX_COMMUNITY_ITEM_COUNT)
|
||||||
addFeedCard(
|
visibleCommunities.forEachIndexed { index, community ->
|
||||||
title = community.content,
|
val communityWidthDp = calculateCreatorChannelCommunityCardWidthDp(
|
||||||
body = itemView.context.getString(
|
itemView.resources.configuration.screenWidthDp
|
||||||
R.string.creator_channel_community_summary,
|
|
||||||
community.creatorNickname,
|
|
||||||
community.likeCount,
|
|
||||||
community.commentCount
|
|
||||||
),
|
|
||||||
imageUrl = community.imageUrl
|
|
||||||
)
|
)
|
||||||
|
val row = LayoutInflater.from(itemView.context).inflate(
|
||||||
|
R.layout.view_feed_community,
|
||||||
|
communityItems,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
(row as FeedCommunityView).apply {
|
||||||
|
setFeedSize(
|
||||||
|
FeedSize(
|
||||||
|
rootWidthDp = communityWidthDp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setHideEmptyTextRows(true)
|
||||||
|
bind(community.toFeedCommunityItem())
|
||||||
|
}
|
||||||
|
bindCommunityImages(row, community)
|
||||||
|
row.layoutParams = LinearLayout.LayoutParams(
|
||||||
|
communityWidthDp.dp(),
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
).apply {
|
||||||
|
bottomMargin = if (index == visibleCommunities.lastIndex) 0 else 8.dp()
|
||||||
|
}
|
||||||
|
communityItems?.addView(row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindFanTalk(item: CreatorChannelHomeSection.FanTalk) {
|
private fun bindFanTalk(item: CreatorChannelHomeSection.FanTalk) {
|
||||||
item.fanTalk.latestFanTalk?.let { fanTalk ->
|
fanTalkCard?.layoutParams = fanTalkCard.layoutParams?.apply {
|
||||||
addCommentCard(
|
width = calculateCreatorChannelFanTalkCardWidthDp(itemView.resources.configuration.screenWidthDp).dp()
|
||||||
title = fanTalk.content,
|
}
|
||||||
body = itemView.context.getString(
|
fanTalkTotalCount?.text = item.fanTalk.totalCount.toString()
|
||||||
R.string.creator_channel_fantalk_summary,
|
val fanTalk = item.fanTalk.latestFanTalk
|
||||||
fanTalk.nickname,
|
fanTalkTotalRow?.isVisible = fanTalk != null && item.fanTalk.totalCount > 0
|
||||||
item.fanTalk.totalCount
|
fanTalkLatestRow?.isVisible = fanTalk != null && item.fanTalk.totalCount > 0
|
||||||
),
|
fanTalkEmpty?.isVisible = fanTalk == null || item.fanTalk.totalCount <= 0
|
||||||
imageUrl = fanTalk.profileImageUrl
|
if (fanTalk != null) {
|
||||||
)
|
fanTalkContent?.text = fanTalk.content
|
||||||
|
fanTalkProfile?.loadUrl(fanTalk.profileImageUrl) {
|
||||||
|
placeholder(R.drawable.ic_placeholder_profile)
|
||||||
|
transformations(CircleCropTransformation())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fanTalkContent?.text = ""
|
||||||
|
fanTalkProfile?.setImageResource(R.drawable.ic_placeholder_profile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun bindCommunityImages(row: View, community: CreatorChannelCommunityPostResponse) {
|
||||||
|
val communityRow = row as FeedCommunityView
|
||||||
|
communityRow.profileImageView().loadUrl(community.creatorProfileUrl) {
|
||||||
|
placeholder(R.drawable.ic_placeholder_profile)
|
||||||
|
transformations(CircleCropTransformation())
|
||||||
|
}
|
||||||
|
val isCommunityLocked = community.price > 0 && !community.existOrdered
|
||||||
|
val communityImageUrl = community.imageUrl.takeIf { !it.isNullOrBlank() }
|
||||||
|
if (communityImageUrl != null) {
|
||||||
|
communityRow.communityImageView().loadUrl(communityImageUrl) {
|
||||||
|
if (isCommunityLocked) {
|
||||||
|
transformations(BlurTransformation(itemView.context, 25f, 2.5f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
communityRow.communityImageView().setImageDrawable(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CreatorChannelCommunityPostResponse.toFeedCommunityItem(): FeedItem.Community = FeedItem.Community(
|
||||||
|
feedId = postId.toString(),
|
||||||
|
creatorId = creatorId.toString(),
|
||||||
|
creatorName = creatorNickname,
|
||||||
|
creatorImageUrl = creatorProfileUrl,
|
||||||
|
postId = postId.toString(),
|
||||||
|
bodyText = content,
|
||||||
|
keywordText = "",
|
||||||
|
createdAtText = dateUtc,
|
||||||
|
commentCount = commentCount,
|
||||||
|
likeCount = likeCount,
|
||||||
|
imageUrl = imageUrl,
|
||||||
|
audioUrl = audioUrl,
|
||||||
|
price = price,
|
||||||
|
existOrdered = existOrdered,
|
||||||
|
showKeyword = false
|
||||||
|
)
|
||||||
|
|
||||||
private fun bindIntroduce(item: CreatorChannelHomeSection.Introduce) {
|
private fun bindIntroduce(item: CreatorChannelHomeSection.Introduce) {
|
||||||
addTextCard(title = item.introduce, body = "", imageUrl = null)
|
addTextCard(title = item.introduce, body = "", imageUrl = null)
|
||||||
}
|
}
|
||||||
@@ -371,26 +453,6 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
sectionItems?.addView(card)
|
sectionItems?.addView(card)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addFeedCard(title: String, body: String, imageUrl: String?) {
|
|
||||||
val card = LinearLayout(itemView.context).apply {
|
|
||||||
orientation = LinearLayout.VERTICAL
|
|
||||||
setPadding(14.dp(), 14.dp(), 14.dp(), 14.dp())
|
|
||||||
setBackgroundResource(R.drawable.bg_round_corner_16_7_222222)
|
|
||||||
layoutParams = defaultBlockLayoutParams()
|
|
||||||
}
|
|
||||||
card.addView(createText(body, R.style.Typography_Caption2, R.color.gray_500, maxLines = 1))
|
|
||||||
card.addView(createText(title, R.style.Typography_Body2, R.color.white, maxLines = 4))
|
|
||||||
card.addView(createImage(imageUrl, LinearLayout.LayoutParams.MATCH_PARENT, 180.dp()))
|
|
||||||
sectionItems?.addView(card)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addCommentCard(title: String, body: String, imageUrl: String?) {
|
|
||||||
val card = createHorizontalCard()
|
|
||||||
card.addView(createImage(imageUrl, 28.dp(), 28.dp()))
|
|
||||||
card.addView(createTextGroup(title, body, bodyMaxLines = 2))
|
|
||||||
sectionItems?.addView(card)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createHorizontalCard(): LinearLayout =
|
private fun createHorizontalCard(): LinearLayout =
|
||||||
LinearLayout(itemView.context).apply {
|
LinearLayout(itemView.context).apply {
|
||||||
orientation = LinearLayout.HORIZONTAL
|
orientation = LinearLayout.HORIZONTAL
|
||||||
@@ -412,22 +474,6 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
return textGroup
|
return textGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createContentTile(
|
|
||||||
title: String,
|
|
||||||
body: String,
|
|
||||||
imageUrl: String?,
|
|
||||||
imageWidth: Int,
|
|
||||||
imageHeight: Int
|
|
||||||
): LinearLayout = LinearLayout(itemView.context).apply {
|
|
||||||
orientation = LinearLayout.VERTICAL
|
|
||||||
layoutParams = LinearLayout.LayoutParams(imageWidth, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
|
||||||
marginEnd = 12.dp()
|
|
||||||
}
|
|
||||||
addView(createImage(imageUrl, imageWidth, imageHeight))
|
|
||||||
addView(createText(title, R.style.Typography_Body2, R.color.white, maxLines = 2))
|
|
||||||
addView(createText(body, R.style.Typography_Caption2, R.color.gray_500, maxLines = 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createImage(imageUrl: String?, width: Int, height: Int): ImageView = ImageView(itemView.context).apply {
|
private fun createImage(imageUrl: String?, width: Int, height: Int): ImageView = ImageView(itemView.context).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(width, height).apply {
|
layoutParams = LinearLayout.LayoutParams(width, height).apply {
|
||||||
bottomMargin = 8.dp()
|
bottomMargin = 8.dp()
|
||||||
@@ -438,19 +484,6 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
imageUrl?.let(::loadUrl)
|
imageUrl?.let(::loadUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createHorizontalRow(): LinearLayout = LinearLayout(itemView.context).apply {
|
|
||||||
orientation = LinearLayout.HORIZONTAL
|
|
||||||
layoutParams = defaultBlockLayoutParams()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createHorizontalScrollRow(row: LinearLayout): HorizontalScrollView =
|
|
||||||
HorizontalScrollView(itemView.context).apply {
|
|
||||||
isHorizontalScrollBarEnabled = false
|
|
||||||
overScrollMode = View.OVER_SCROLL_NEVER
|
|
||||||
layoutParams = defaultBlockLayoutParams()
|
|
||||||
addView(row)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun defaultBlockLayoutParams(): LinearLayout.LayoutParams = LinearLayout.LayoutParams(
|
private fun defaultBlockLayoutParams(): LinearLayout.LayoutParams = LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
@@ -618,6 +651,8 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
private const val MAX_NOTICE_ITEM_COUNT = 3
|
private const val MAX_NOTICE_ITEM_COUNT = 3
|
||||||
private const val MAX_SCHEDULE_ITEM_COUNT = 3
|
private const val MAX_SCHEDULE_ITEM_COUNT = 3
|
||||||
private const val MAX_AUDIO_ITEM_COUNT = 9
|
private const val MAX_AUDIO_ITEM_COUNT = 9
|
||||||
|
private const val MAX_SERIES_ITEM_COUNT = 10
|
||||||
|
private const val MAX_COMMUNITY_ITEM_COUNT = 3
|
||||||
private const val AUDIO_GRID_SPAN_COUNT = 3
|
private const val AUDIO_GRID_SPAN_COUNT = 3
|
||||||
|
|
||||||
fun List<String>.joinToText(): String = filter(String::isNotBlank).joinToString(separator = " · ")
|
fun List<String>.joinToText(): String = filter(String::isNotBlank).joinToString(separator = " · ")
|
||||||
@@ -699,4 +734,33 @@ internal fun calculateCreatorChannelAudioItemWidthDp(screenWidthDp: Int): Int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun calculateCreatorChannelSeriesCardWidthDp(screenWidthDp: Int): Int {
|
||||||
|
val width = screenWidthDp.takeIf { it > 0 } ?: 402
|
||||||
|
return if (width >= 402) {
|
||||||
|
163
|
||||||
|
} else {
|
||||||
|
(163f * width / 402f).roundToInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun calculateCreatorChannelSeriesCardHeightDp(widthDp: Int): Int = (widthDp * 230f / 163f).roundToInt()
|
||||||
|
|
||||||
|
internal fun calculateCreatorChannelCommunityCardWidthDp(screenWidthDp: Int): Int {
|
||||||
|
val width = screenWidthDp.takeIf { it > 0 } ?: 402
|
||||||
|
return if (width >= 402) {
|
||||||
|
374
|
||||||
|
} else {
|
||||||
|
(374f * width / 402f).roundToInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun calculateCreatorChannelFanTalkCardWidthDp(screenWidthDp: Int): Int {
|
||||||
|
val width = screenWidthDp.takeIf { it > 0 } ?: 402
|
||||||
|
return if (width >= 402) {
|
||||||
|
374
|
||||||
|
} else {
|
||||||
|
(374f * width / 402f).roundToInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal fun calculateCreatorChannelScheduleTimelineLineCount(scheduleCount: Int): Int = (scheduleCount - 1).coerceAtLeast(0)
|
internal fun calculateCreatorChannelScheduleTimelineLineCount(scheduleCount: Int): Int = (scheduleCount - 1).coerceAtLeast(0)
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Outline
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewOutlineProvider
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
|
||||||
|
|
||||||
|
class CreatorChannelHomeSeriesCardView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private val thumbnailContainer: View by lazy { findViewById(R.id.layout_series_thumbnail) }
|
||||||
|
private val thumbnail: ImageView by lazy { findViewById(R.id.iv_series_thumbnail) }
|
||||||
|
private val originalTag: View by lazy { findViewById(R.id.layout_series_original_tag) }
|
||||||
|
|
||||||
|
override fun onFinishInflate() {
|
||||||
|
super.onFinishInflate()
|
||||||
|
thumbnailContainer.clipToOutline = true
|
||||||
|
thumbnailContainer.outlineProvider = object : ViewOutlineProvider() {
|
||||||
|
override fun getOutline(view: View, outline: Outline) {
|
||||||
|
outline.setRoundRect(0, 0, view.width, view.height, resources.getDimension(R.dimen.radius_14))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(series: CreatorChannelSeriesResponse) {
|
||||||
|
thumbnail.loadUrl(series.coverImageUrl)
|
||||||
|
originalTag.isVisible = series.isOriginal
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setThumbnailSize(widthDp: Int, heightDp: Int) {
|
||||||
|
thumbnailContainer.layoutParams = thumbnailContainer.layoutParams.apply {
|
||||||
|
width = widthDp.dp()
|
||||||
|
height = heightDp.dp()
|
||||||
|
} ?: ViewGroup.LayoutParams(widthDp.dp(), heightDp.dp())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int.dp(): Int = (this * resources.displayMetrics.density).toInt()
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import android.view.ViewGroup
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.dispose
|
import coil.dispose
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import kr.co.vividnext.sodalive.common.image.BlurTransformation
|
||||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostSection
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostSection
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostUiModel
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostUiModel
|
||||||
@@ -49,15 +50,27 @@ class HomePopularCommunityAdapter(
|
|||||||
view.bind(item.item)
|
view.bind(item.item)
|
||||||
view.setOnFeedClick { feedItem -> onClickItem(feedItem as FeedItem.Community) }
|
view.setOnFeedClick { feedItem -> onClickItem(feedItem as FeedItem.Community) }
|
||||||
bindImage(item.item.creatorImageUrl, view.profileImageView())
|
bindImage(item.item.creatorImageUrl, view.profileImageView())
|
||||||
bindImage(item.item.imageUrl.takeIf { item.item.price <= 0 || item.item.existOrdered }, view.communityImageView())
|
bindImage(
|
||||||
|
imageUrl = item.item.imageUrl,
|
||||||
|
imageView = view.communityImageView(),
|
||||||
|
shouldBlur = item.item.price > 0 && !item.item.existOrdered
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindImage(imageUrl: String?, imageView: android.widget.ImageView) {
|
private fun bindImage(
|
||||||
|
imageUrl: String?,
|
||||||
|
imageView: android.widget.ImageView,
|
||||||
|
shouldBlur: Boolean = false
|
||||||
|
) {
|
||||||
if (imageUrl.isNullOrBlank()) {
|
if (imageUrl.isNullOrBlank()) {
|
||||||
imageView.dispose()
|
imageView.dispose()
|
||||||
imageView.setImageDrawable(null)
|
imageView.setImageDrawable(null)
|
||||||
} else {
|
} else {
|
||||||
imageView.loadUrl(imageUrl)
|
imageView.loadUrl(imageUrl) {
|
||||||
|
if (shouldBlur) {
|
||||||
|
transformations(BlurTransformation(imageView.context, 25f, 2.5f))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import android.view.ViewOutlineProvider
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@@ -22,7 +23,6 @@ class FeedCommunityView @JvmOverloads constructor(
|
|||||||
private var creatorText: TextView? = null
|
private var creatorText: TextView? = null
|
||||||
private var createdAtText: TextView? = null
|
private var createdAtText: TextView? = null
|
||||||
private var bodyText: TextView? = null
|
private var bodyText: TextView? = null
|
||||||
private var keywordText: TextView? = null
|
|
||||||
private var communityImageContainer: View? = null
|
private var communityImageContainer: View? = null
|
||||||
private var communityImage: ImageView? = null
|
private var communityImage: ImageView? = null
|
||||||
private var paidOverlay: View? = null
|
private var paidOverlay: View? = null
|
||||||
@@ -39,7 +39,6 @@ class FeedCommunityView @JvmOverloads constructor(
|
|||||||
creatorText = findViewById(R.id.tv_feed_community_creator)
|
creatorText = findViewById(R.id.tv_feed_community_creator)
|
||||||
createdAtText = findViewById(R.id.tv_feed_community_created_at)
|
createdAtText = findViewById(R.id.tv_feed_community_created_at)
|
||||||
bodyText = findViewById(R.id.tv_feed_community_body)
|
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)
|
communityImageContainer = findViewById(R.id.fl_feed_community_image_container)
|
||||||
communityImage = findViewById(R.id.iv_feed_community_image)
|
communityImage = findViewById(R.id.iv_feed_community_image)
|
||||||
paidOverlay = findViewById(R.id.ll_feed_community_paid_overlay)
|
paidOverlay = findViewById(R.id.ll_feed_community_paid_overlay)
|
||||||
@@ -54,21 +53,20 @@ class FeedCommunityView @JvmOverloads constructor(
|
|||||||
requireNotNull(communityImageContainer).outlineProvider = roundedOutlineProvider(COMMUNITY_IMAGE_RADIUS_DP)
|
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) {
|
fun bind(item: FeedItem.Community) {
|
||||||
currentItem = item
|
currentItem = item
|
||||||
requireNotNull(creatorText).text = item.creatorName
|
requireNotNull(creatorText).text = item.creatorName
|
||||||
requireNotNull(createdAtText).text = item.createdAtText
|
requireNotNull(createdAtText).text = item.createdAtText
|
||||||
requireNotNull(bodyText).text = item.bodyText
|
requireNotNull(bodyText).text = item.bodyText
|
||||||
requireNotNull(keywordText).text = item.keywordText
|
|
||||||
requireNotNull(bodyText).visibility = visibilityForText(item.bodyText)
|
requireNotNull(bodyText).visibility = visibilityForText(item.bodyText)
|
||||||
requireNotNull(keywordText).visibility = if (item.showKeyword) visibilityForText(item.keywordText) else View.GONE
|
val isLocked = item.price > 0 && !item.existOrdered
|
||||||
requireNotNull(communityImageContainer).visibility = if (item.imageUrl.isNullOrBlank()) View.GONE else View.VISIBLE
|
val hasImage = !item.imageUrl.isNullOrBlank()
|
||||||
requireNotNull(paidOverlay).visibility = if (item.price > 0 && !item.existOrdered) View.VISIBLE else View.GONE
|
requireNotNull(communityImageContainer).isVisible = hasImage || isLocked
|
||||||
|
requireNotNull(communityImage).isVisible = hasImage
|
||||||
|
if (!hasImage || isLocked) {
|
||||||
|
requireNotNull(communityImage).setImageDrawable(null)
|
||||||
|
}
|
||||||
|
requireNotNull(paidOverlay).isVisible = isLocked
|
||||||
requireNotNull(priceText).text = item.price.toString()
|
requireNotNull(priceText).text = item.price.toString()
|
||||||
requireNotNull(commentCountText).text = item.commentCount.toString()
|
requireNotNull(commentCountText).text = item.commentCount.toString()
|
||||||
requireNotNull(likeCountText).text = item.likeCount.toString()
|
requireNotNull(likeCountText).text = item.likeCount.toString()
|
||||||
@@ -113,20 +111,6 @@ 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() {
|
private fun circleOutlineProvider() = object : ViewOutlineProvider() {
|
||||||
override fun getOutline(view: View, outline: Outline) {
|
override fun getOutline(view: View, outline: Outline) {
|
||||||
outline.setOval(0, 0, view.width, view.height)
|
outline.setOval(0, 0, view.width, view.height)
|
||||||
@@ -144,7 +128,5 @@ 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 COMMUNITY_IMAGE_FIGMA_WIDTH_DP = 346
|
|
||||||
const val COMMUNITY_IMAGE_FIGMA_HEIGHT_DP = 236
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
app/src/main/res/drawable-mdpi/ic_chevron_down_white.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_chevron_down_white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 225 B |
BIN
app/src/main/res/drawable-mdpi/ic_new_fantalk_plus.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_new_fantalk_plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 245 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@color/white" />
|
||||||
|
<corners android:radius="100dp" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@android:color/transparent" />
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="@color/color_4dffffff" />
|
||||||
|
<corners android:radius="100dp" />
|
||||||
|
</shape>
|
||||||
@@ -1,25 +1,30 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingHorizontal="@dimen/spacing_20"
|
|
||||||
android:paddingTop="@dimen/spacing_20">
|
android:paddingTop="@dimen/spacing_20">
|
||||||
|
|
||||||
<TextView
|
<include layout="@layout/view_section_title" />
|
||||||
android:id="@+id/tv_section_title"
|
|
||||||
style="@style/Typography.Heading3"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
tools:text="섹션" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/ll_section_items"
|
android:id="@+id/ll_community_items"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/spacing_8"
|
android:layout_marginTop="@dimen/spacing_14"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
android:orientation="vertical" />
|
android:orientation="vertical" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/layout_community_more_button"
|
||||||
|
style="@style/Typography.Body3"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:layout_marginHorizontal="@dimen/spacing_14"
|
||||||
|
android:layout_marginTop="@dimen/spacing_8"
|
||||||
|
android:background="@drawable/bg_creator_channel_more_button"
|
||||||
|
android:gravity="center"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:text="@string/view_all"
|
||||||
|
android:textColor="@color/white" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -4,22 +4,131 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingHorizontal="@dimen/spacing_20"
|
|
||||||
android:paddingTop="@dimen/spacing_20">
|
android:paddingTop="@dimen/spacing_20">
|
||||||
|
|
||||||
<TextView
|
<include layout="@layout/view_section_title" />
|
||||||
android:id="@+id/tv_section_title"
|
|
||||||
style="@style/Typography.Heading3"
|
<kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelFanTalkCardView
|
||||||
|
android:id="@+id/layout_fantalk_card"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@color/white"
|
android:layout_marginHorizontal="@dimen/spacing_14"
|
||||||
tools:text="섹션" />
|
android:layout_marginTop="@dimen/spacing_14"
|
||||||
|
android:background="@drawable/bg_feed_card"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="@dimen/spacing_14">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/ll_section_items"
|
android:id="@+id/layout_fantalk_total_row"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/spacing_8"
|
android:gravity="center_vertical"
|
||||||
android:orientation="vertical" />
|
android:orientation="horizontal"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_fantalk_total_label"
|
||||||
|
style="@style/Typography.Body3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:text="@string/screen_chat_filter_all"
|
||||||
|
android:textColor="@color/white" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_fantalk_total_count"
|
||||||
|
style="@style/Typography.Body3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/spacing_4"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:textColor="@color/gray_500"
|
||||||
|
tools:text="23" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_fantalk_latest_row"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/spacing_8"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_fantalk_profile"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:src="@drawable/ic_placeholder_profile" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_fantalk_content"
|
||||||
|
style="@style/Typography.Body6"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginStart="@dimen/spacing_14"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
tools:text="팬이 쓴 응원이 보이는 부분입니다. 두 줄까지만 보여주고 초과 시 말줄임 처리." />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
|
android:layout_marginStart="@dimen/spacing_14"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/ic_chevron_down_white" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/layout_fantalk_empty"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="169dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_fantalk_empty_title"
|
||||||
|
style="@style/Typography.Heading1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="top|start"
|
||||||
|
android:layout_marginStart="@dimen/spacing_6"
|
||||||
|
android:layout_marginTop="@dimen/spacing_6"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:text="@string/creator_channel_fantalk_empty_title"
|
||||||
|
android:textColor="@color/white" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_fantalk_support_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|center_horizontal"
|
||||||
|
android:layout_marginBottom="@dimen/spacing_6"
|
||||||
|
android:background="@drawable/bg_round_corner_999_white"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="@dimen/spacing_12">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/ic_new_fantalk_plus" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/Typography.Body2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/spacing_6"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:text="@string/creator_channel_fantalk_support_action"
|
||||||
|
android:textColor="@color/black" />
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
</kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelFanTalkCardView>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingHorizontal="@dimen/spacing_20"
|
|
||||||
android:paddingTop="@dimen/spacing_20">
|
android:paddingTop="@dimen/spacing_20">
|
||||||
|
|
||||||
<TextView
|
<include layout="@layout/view_section_title" />
|
||||||
android:id="@+id/tv_section_title"
|
|
||||||
style="@style/Typography.Heading3"
|
<HorizontalScrollView
|
||||||
|
android:id="@+id/hsv_series_items"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@color/white"
|
android:layout_marginTop="@dimen/spacing_14"
|
||||||
tools:text="섹션" />
|
android:clipToPadding="false"
|
||||||
|
android:overScrollMode="never"
|
||||||
<LinearLayout
|
android:paddingStart="@dimen/spacing_14"
|
||||||
android:id="@+id/ll_section_items"
|
android:paddingEnd="@dimen/spacing_20"
|
||||||
android:layout_width="match_parent"
|
android:scrollbars="none">
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/spacing_8"
|
|
||||||
android:orientation="vertical" />
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/ll_series_items"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal" />
|
||||||
|
</HorizontalScrollView>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelHomeSeriesCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/layout_series_content_card"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/layout_series_thumbnail"
|
||||||
|
android:layout_width="163dp"
|
||||||
|
android:layout_height="230dp"
|
||||||
|
android:background="@drawable/bg_series_content_thumbnail">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_series_thumbnail"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@drawable/ic_launcher_background" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/layout_series_original_tag"
|
||||||
|
android:layout_width="70dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_gravity="top|start"
|
||||||
|
android:background="@drawable/bg_series_original_tag">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_series_original_icon"
|
||||||
|
android:layout_width="14dp"
|
||||||
|
android:layout_height="14dp"
|
||||||
|
android:layout_gravity="start|center_vertical"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/ic_series_original" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_series_original_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="26dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:fontFamily="@font/phosphate_solid"
|
||||||
|
android:text="Only"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="16sp"
|
||||||
|
tools:ignore="HardcodedText" />
|
||||||
|
</FrameLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
</kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelHomeSeriesCardView>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_feed_community_creator"
|
android:id="@+id/tv_feed_community_creator"
|
||||||
style="@style/Typography.Body5"
|
style="@style/Typography.Body5"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="85dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:includeFontPadding="false"
|
android:includeFontPadding="false"
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_feed_community_created_at"
|
android:id="@+id/tv_feed_community_created_at"
|
||||||
style="@style/Typography.Body6"
|
style="@style/Typography.Body6"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="85dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="2dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
@@ -57,35 +57,26 @@
|
|||||||
style="@style/Typography.Body3"
|
style="@style/Typography.Body3"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/spacing_14"
|
android:layout_marginTop="@dimen/spacing_16"
|
||||||
android:includeFontPadding="false"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
tools:text="크리에이터가 커뮤니티에 올린 글이 보이는 부분" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tv_feed_community_keyword"
|
|
||||||
style="@style/Typography.Body3"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/spacing_4"
|
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:includeFontPadding="false"
|
android:includeFontPadding="false"
|
||||||
android:maxLines="1"
|
android:maxLines="5"
|
||||||
android:textColor="@color/soda_400"
|
android:textColor="@color/white"
|
||||||
tools:text="#키워드 #키워드 #키워드" />
|
tools:text="크리에이터가 커뮤니티에 올린 글이 보이는 부분" />
|
||||||
|
|
||||||
<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="0dp"
|
android:layout_height="236dp"
|
||||||
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">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
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="centerCrop"
|
||||||
tools:src="@drawable/ic_launcher_background" />
|
tools:src="@drawable/ic_launcher_background" />
|
||||||
@@ -94,10 +85,11 @@
|
|||||||
android:id="@+id/ll_feed_community_paid_overlay"
|
android:id="@+id/ll_feed_community_paid_overlay"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@color/color_99525252"
|
android:background="@color/color_26909090"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:visibility="gone">
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
@@ -105,24 +97,37 @@
|
|||||||
android:contentDescription="@null"
|
android:contentDescription="@null"
|
||||||
android:src="@drawable/ic_new_community_lock" />
|
android:src="@drawable/ic_new_community_lock" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/tv_feed_community_price"
|
|
||||||
style="@style/Typography.Body3"
|
|
||||||
android:layout_width="70dp"
|
android:layout_width="70dp"
|
||||||
android:layout_height="36dp"
|
android:layout_height="36dp"
|
||||||
android:layout_marginTop="@dimen/spacing_4"
|
android:layout_marginTop="@dimen/spacing_4"
|
||||||
android:background="@drawable/bg_round_corner_999_white"
|
android:background="@drawable/bg_creator_channel_community_price"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:includeFontPadding="false"
|
android:orientation="horizontal">
|
||||||
android:textColor="@color/black"
|
|
||||||
tools:text="30" />
|
<ImageView
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/ic_bar_cash" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_feed_community_price"
|
||||||
|
style="@style/Typography.Body3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/spacing_6"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
tools:text="30" />
|
||||||
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
android:layout_marginTop="@dimen/spacing_14"
|
android:layout_marginTop="@dimen/spacing_16"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
@@ -139,7 +144,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_4"
|
android:layout_marginStart="@dimen/spacing_4"
|
||||||
android:includeFontPadding="false"
|
android:includeFontPadding="false"
|
||||||
android:textColor="@color/gray_500"
|
android:textColor="@color/gray_400"
|
||||||
tools:text="5" />
|
tools:text="5" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
@@ -156,7 +161,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_4"
|
android:layout_marginStart="@dimen/spacing_4"
|
||||||
android:includeFontPadding="false"
|
android:includeFontPadding="false"
|
||||||
android:textColor="@color/gray_500"
|
android:textColor="@color/gray_400"
|
||||||
tools:text="6" />
|
tools:text="6" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityView>
|
</kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityView>
|
||||||
|
|||||||
@@ -297,6 +297,8 @@
|
|||||||
<string name="creator_channel_series_summary">%1$d episodes · %2$s</string>
|
<string name="creator_channel_series_summary">%1$d episodes · %2$s</string>
|
||||||
<string name="creator_channel_community_summary">%1$s · Likes %2$d · Comments %3$d</string>
|
<string name="creator_channel_community_summary">%1$s · Likes %2$d · Comments %3$d</string>
|
||||||
<string name="creator_channel_fantalk_summary">%1$s · Total %2$d</string>
|
<string name="creator_channel_fantalk_summary">%1$s · Total %2$d</string>
|
||||||
|
<string name="creator_channel_fantalk_empty_title">Be the first\nto cheer them on!</string>
|
||||||
|
<string name="creator_channel_fantalk_support_action">Leave support</string>
|
||||||
<string name="creator_channel_activity_summary">Live %1$d · %2$d hours · %3$d contributors · Audio %4$d · Series %5$d</string>
|
<string name="creator_channel_activity_summary">Live %1$d · %2$d hours · %3$d contributors · Audio %4$d · Series %5$d</string>
|
||||||
<string name="creator_channel_activity_debut">Debut</string>
|
<string name="creator_channel_activity_debut">Debut</string>
|
||||||
<string name="creator_channel_activity_debut_format">%1$s (%2$s)</string>
|
<string name="creator_channel_activity_debut_format">%1$s (%2$s)</string>
|
||||||
|
|||||||
@@ -297,6 +297,8 @@
|
|||||||
<string name="creator_channel_series_summary">%1$d件 · %2$s</string>
|
<string name="creator_channel_series_summary">%1$d件 · %2$s</string>
|
||||||
<string name="creator_channel_community_summary">%1$s · いいね %2$d · コメント %3$d</string>
|
<string name="creator_channel_community_summary">%1$s · いいね %2$d · コメント %3$d</string>
|
||||||
<string name="creator_channel_fantalk_summary">%1$s · 全体 %2$d</string>
|
<string name="creator_channel_fantalk_summary">%1$s · 全体 %2$d</string>
|
||||||
|
<string name="creator_channel_fantalk_empty_title">最初の応援を\n待っています!</string>
|
||||||
|
<string name="creator_channel_fantalk_support_action">応援を残す</string>
|
||||||
<string name="creator_channel_activity_summary">ライブ %1$d回 · 累計 %2$d時間 · 参加者 %3$d人 · オーディオ %4$d件 · シリーズ %5$d件</string>
|
<string name="creator_channel_activity_summary">ライブ %1$d回 · 累計 %2$d時間 · 参加者 %3$d人 · オーディオ %4$d件 · シリーズ %5$d件</string>
|
||||||
<string name="creator_channel_activity_debut">デビュー</string>
|
<string name="creator_channel_activity_debut">デビュー</string>
|
||||||
<string name="creator_channel_activity_debut_format">%1$s(%2$s)</string>
|
<string name="creator_channel_activity_debut_format">%1$s(%2$s)</string>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
<color name="color_ccd85e37">#CCD85E37</color>
|
<color name="color_ccd85e37">#CCD85E37</color>
|
||||||
<color name="color_cc333333">#CC333333</color>
|
<color name="color_cc333333">#CC333333</color>
|
||||||
<color name="color_33ffffff">#33FFFFFF</color>
|
<color name="color_33ffffff">#33FFFFFF</color>
|
||||||
|
<color name="color_4dffffff">#4DFFFFFF</color>
|
||||||
<color name="color_303030">#303030</color>
|
<color name="color_303030">#303030</color>
|
||||||
<color name="color_555555">#555555</color>
|
<color name="color_555555">#555555</color>
|
||||||
<color name="color_3e1b93">#3E1B93</color>
|
<color name="color_3e1b93">#3E1B93</color>
|
||||||
|
|||||||
@@ -296,6 +296,8 @@
|
|||||||
<string name="creator_channel_series_summary">%1$d개 · %2$s</string>
|
<string name="creator_channel_series_summary">%1$d개 · %2$s</string>
|
||||||
<string name="creator_channel_community_summary">%1$s · 좋아요 %2$d · 댓글 %3$d</string>
|
<string name="creator_channel_community_summary">%1$s · 좋아요 %2$d · 댓글 %3$d</string>
|
||||||
<string name="creator_channel_fantalk_summary">%1$s · 전체 %2$d</string>
|
<string name="creator_channel_fantalk_summary">%1$s · 전체 %2$d</string>
|
||||||
|
<string name="creator_channel_fantalk_empty_title">당신의 첫 응원을\n기다리고 있어요!</string>
|
||||||
|
<string name="creator_channel_fantalk_support_action">응원 남기기</string>
|
||||||
<string name="creator_channel_activity_summary">라이브 %1$d회 · 누적 %2$d시간 · 참여자 %3$d명 · 오디오 %4$d개 · 시리즈 %5$d개</string>
|
<string name="creator_channel_activity_summary">라이브 %1$d회 · 누적 %2$d시간 · 참여자 %3$d명 · 오디오 %4$d개 · 시리즈 %5$d개</string>
|
||||||
<string name="creator_channel_activity_debut">데뷔</string>
|
<string name="creator_channel_activity_debut">데뷔</string>
|
||||||
<string name="creator_channel_activity_debut_format">%1$s(%2$s)</string>
|
<string name="creator_channel_activity_debut_format">%1$s(%2$s)</string>
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelDon
|
|||||||
import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelDonationHeaderColorRes
|
import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelDonationHeaderColorRes
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelNoticeCardWidthDp
|
import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelNoticeCardWidthDp
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelAudioItemWidthDp
|
import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelAudioItemWidthDp
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelCommunityCardWidthDp
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelFanTalkCardWidthDp
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelSeriesCardHeightDp
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelSeriesCardWidthDp
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelScheduleTimelineLineCount
|
import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelScheduleTimelineLineCount
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleDate
|
import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleDate
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleDayOfWeek
|
import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleDayOfWeek
|
||||||
@@ -156,11 +160,11 @@ class CreatorChannelHomeActivitySourceTest {
|
|||||||
assertTrue(adapter.contains("ll_section_items"))
|
assertTrue(adapter.contains("ll_section_items"))
|
||||||
assertTrue(adapter.contains("sectionItems?.addView"))
|
assertTrue(adapter.contains("sectionItems?.addView"))
|
||||||
assertTrue(adapter.contains("addActivityRow"))
|
assertTrue(adapter.contains("addActivityRow"))
|
||||||
assertTrue(adapter.contains("createContentTile"))
|
assertFalse(adapter.contains("createContentTile"))
|
||||||
assertTrue(adapter.contains("addFeedCard"))
|
assertFalse(adapter.contains("addFeedCard"))
|
||||||
assertFalse(adapter.contains("addScheduleRow"))
|
assertFalse(adapter.contains("addScheduleRow"))
|
||||||
assertFalse(adapter.contains("addDonationCard"))
|
assertFalse(adapter.contains("addDonationCard"))
|
||||||
assertTrue(adapter.contains("addCommentCard"))
|
assertFalse(adapter.contains("addCommentCard"))
|
||||||
assertTrue(adapter.contains("createSnsButton"))
|
assertTrue(adapter.contains("createSnsButton"))
|
||||||
assertTrue(adapter.contains("activity.debutDateUtc"))
|
assertTrue(adapter.contains("activity.debutDateUtc"))
|
||||||
assertTrue(adapter.contains("activity.liveDurationHours"))
|
assertTrue(adapter.contains("activity.liveDurationHours"))
|
||||||
@@ -382,14 +386,11 @@ class CreatorChannelHomeActivitySourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `section adapter source는 가로 시리즈와 SNS 링크와 일정 타입 label을 보존한다`() {
|
fun `section adapter source는 SNS 링크와 일정 타입 label을 보존한다`() {
|
||||||
val adapter = projectFile(
|
val adapter = projectFile(
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt"
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt"
|
||||||
).readText()
|
).readText()
|
||||||
|
|
||||||
assertTrue(adapter.contains("HorizontalScrollView"))
|
|
||||||
assertTrue(adapter.contains("createHorizontalScrollRow"))
|
|
||||||
assertTrue(adapter.contains("createHorizontalScrollRow(row)"))
|
|
||||||
assertTrue(adapter.contains("iconResId = sns.iconResId"))
|
assertTrue(adapter.contains("iconResId = sns.iconResId"))
|
||||||
assertTrue(adapter.contains("url = sns.url"))
|
assertTrue(adapter.contains("url = sns.url"))
|
||||||
assertTrue(adapter.contains("schedule.type.labelResId"))
|
assertTrue(adapter.contains("schedule.type.labelResId"))
|
||||||
@@ -542,6 +543,217 @@ class CreatorChannelHomeActivitySourceTest {
|
|||||||
assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContent.audioContentId)"))
|
assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContent.audioContentId)"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `시리즈 섹션은 Figma Contents series size m 카드 row로 렌더링한다`() {
|
||||||
|
val adapter = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt"
|
||||||
|
).readText()
|
||||||
|
val seriesLayout = projectFile("app/src/main/res/layout/item_creator_channel_home_series.xml").readText()
|
||||||
|
val seriesItemLayout = projectFile(
|
||||||
|
"app/src/main/res/layout/item_creator_channel_home_series_content.xml"
|
||||||
|
).readText()
|
||||||
|
val seriesCardView = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSeriesCardView.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(seriesLayout.contains("@layout/view_section_title"))
|
||||||
|
assertTrue(seriesLayout.contains("@+id/hsv_series_items"))
|
||||||
|
assertTrue(seriesLayout.contains("@+id/ll_series_items"))
|
||||||
|
assertFalse(seriesLayout.contains("@+id/ll_section_items"))
|
||||||
|
assertTrue(seriesItemLayout.contains("CreatorChannelHomeSeriesCardView"))
|
||||||
|
assertTrue(seriesItemLayout.contains("@+id/layout_series_content_card"))
|
||||||
|
assertTrue(seriesItemLayout.contains("@+id/layout_series_thumbnail"))
|
||||||
|
assertTrue(seriesItemLayout.contains("@+id/iv_series_thumbnail"))
|
||||||
|
assertTrue(seriesItemLayout.contains("@+id/layout_series_original_tag"))
|
||||||
|
assertTrue(seriesItemLayout.contains("@+id/iv_series_original_icon"))
|
||||||
|
assertTrue(seriesItemLayout.contains("@+id/tv_series_original_text"))
|
||||||
|
assertTrue(seriesItemLayout.contains("android:text=\"Only\""))
|
||||||
|
assertTrue(seriesItemLayout.contains("@font/phosphate_solid"))
|
||||||
|
assertTrue(seriesItemLayout.contains("android:layout_width=\"70dp\""))
|
||||||
|
assertTrue(seriesItemLayout.contains("@drawable/bg_series_original_tag"))
|
||||||
|
assertTrue(seriesItemLayout.contains("@drawable/ic_series_original"))
|
||||||
|
assertTrue(seriesItemLayout.contains("android:layout_width=\"163dp\""))
|
||||||
|
assertTrue(seriesItemLayout.contains("android:layout_height=\"230dp\""))
|
||||||
|
assertTrue(seriesItemLayout.contains("android:layout_width=\"14dp\""))
|
||||||
|
assertFalse(seriesItemLayout.contains("android:clipToOutline"))
|
||||||
|
assertTrue(seriesCardView.contains("clipToOutline = true"))
|
||||||
|
assertTrue(seriesCardView.contains("ViewOutlineProvider"))
|
||||||
|
assertTrue(seriesCardView.contains("outline.setRoundRect"))
|
||||||
|
assertTrue(seriesCardView.contains("R.dimen.radius_14"))
|
||||||
|
assertTrue(seriesCardView.contains("originalTag.isVisible = series.isOriginal"))
|
||||||
|
assertTrue(seriesCardView.contains("thumbnail.loadUrl(series.coverImageUrl)"))
|
||||||
|
assertTrue(seriesCardView.contains("fun setThumbnailSize(widthDp: Int, heightDp: Int)"))
|
||||||
|
assertTrue(adapter.contains("private val seriesItems: LinearLayout?"))
|
||||||
|
assertTrue(adapter.contains("val visibleSeries = item.series.take(MAX_SERIES_ITEM_COUNT)"))
|
||||||
|
assertTrue(adapter.contains("R.layout.item_creator_channel_home_series_content"))
|
||||||
|
assertTrue(adapter.contains("(row as CreatorChannelHomeSeriesCardView).apply"))
|
||||||
|
assertTrue(adapter.contains("bind(series)"))
|
||||||
|
assertTrue(adapter.contains("setThumbnailSize(seriesWidthDp, calculateCreatorChannelSeriesCardHeightDp(seriesWidthDp))"))
|
||||||
|
assertTrue(adapter.contains("row.setOnClickListener { onSeriesClick(series) }"))
|
||||||
|
assertTrue(adapter.contains("calculateCreatorChannelSeriesCardWidthDp"))
|
||||||
|
assertTrue(adapter.contains("private const val MAX_SERIES_ITEM_COUNT = 10"))
|
||||||
|
assertFalse(adapter.contains("createContentTile("))
|
||||||
|
assertFalse(adapter.contains("creator_channel_series_summary"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `시리즈 DTO와 bind source는 현재 백엔드 필드만 화면에 사용한다`() {
|
||||||
|
val model = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeModels.kt"
|
||||||
|
).readText()
|
||||||
|
val adapter = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt"
|
||||||
|
).readText()
|
||||||
|
val seriesCardView = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSeriesCardView.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(model.contains("@SerializedName(\"seriesId\") val seriesId: Long"))
|
||||||
|
assertTrue(model.contains("@SerializedName(\"coverImageUrl\") val coverImageUrl: String"))
|
||||||
|
assertTrue(model.contains("@SerializedName(\"isOriginal\") val isOriginal: Boolean"))
|
||||||
|
assertFalse(model.contains("publishedDaysOfWeek"))
|
||||||
|
assertFalse(model.contains("isPopular"))
|
||||||
|
assertFalse(model.contains("isComplete"))
|
||||||
|
assertTrue(adapter.contains("onSeriesClick(series)"))
|
||||||
|
assertTrue(seriesCardView.contains("series.coverImageUrl"))
|
||||||
|
assertTrue(seriesCardView.contains("series.isOriginal"))
|
||||||
|
assertFalse(seriesCardView.contains("series.numberOfContent"))
|
||||||
|
assertFalse(seriesCardView.contains("series.isNew"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `시리즈 카드 width는 402dp 기준 최대 163dp이고 작은 화면에서는 비율 축소한다`() {
|
||||||
|
assertEquals(163, calculateCreatorChannelSeriesCardWidthDp(402))
|
||||||
|
assertEquals(163, calculateCreatorChannelSeriesCardWidthDp(430))
|
||||||
|
assertEquals(146, calculateCreatorChannelSeriesCardWidthDp(360))
|
||||||
|
assertEquals(230, calculateCreatorChannelSeriesCardHeightDp(163))
|
||||||
|
assertEquals(206, calculateCreatorChannelSeriesCardHeightDp(146))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 섹션은 Figma feed card 3개와 전체보기 capsule로 렌더링한다`() {
|
||||||
|
val adapter = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt"
|
||||||
|
).readText()
|
||||||
|
val communityLayout = projectFile("app/src/main/res/layout/item_creator_channel_home_community.xml").readText()
|
||||||
|
val communityViewLayout = projectFile("app/src/main/res/layout/view_feed_community.xml").readText()
|
||||||
|
val feedCommunityView = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(communityLayout.contains("@layout/view_section_title"))
|
||||||
|
assertTrue(communityLayout.contains("@+id/ll_community_items"))
|
||||||
|
assertTrue(communityLayout.contains("@+id/layout_community_more_button"))
|
||||||
|
assertTrue(communityLayout.contains("@string/view_all"))
|
||||||
|
assertFalse(communityLayout.contains("@+id/ll_section_items"))
|
||||||
|
assertTrue(communityViewLayout.contains("FeedCommunityView"))
|
||||||
|
assertTrue(communityViewLayout.contains("android:layout_width=\"match_parent\""))
|
||||||
|
assertTrue(communityViewLayout.contains("@+id/fl_feed_community_image_container"))
|
||||||
|
assertTrue(communityViewLayout.contains("@+id/iv_feed_community_image"))
|
||||||
|
assertTrue(communityViewLayout.contains("@+id/ll_feed_community_paid_overlay"))
|
||||||
|
assertTrue(communityViewLayout.contains("@+id/tv_feed_community_price"))
|
||||||
|
assertTrue(communityViewLayout.contains("@drawable/bg_creator_channel_community_price"))
|
||||||
|
assertTrue(communityViewLayout.contains("@drawable/ic_bar_cash"))
|
||||||
|
assertFalse(communityViewLayout.contains("android:drawableStart=\"@drawable/ic_bar_cash\""))
|
||||||
|
assertTrue(communityViewLayout.contains("android:layout_marginStart=\"@dimen/spacing_6\""))
|
||||||
|
assertFalse(communityViewLayout.contains("android:clipToOutline"))
|
||||||
|
assertFalse(communityViewLayout.contains("android:layout_height=\"0dp\""))
|
||||||
|
assertTrue(feedCommunityView.contains("clipToOutline = true"))
|
||||||
|
assertTrue(feedCommunityView.contains("ViewOutlineProvider"))
|
||||||
|
assertTrue(feedCommunityView.contains("outline.setRoundRect"))
|
||||||
|
assertTrue(adapter.contains("private val communityItems: LinearLayout?"))
|
||||||
|
assertTrue(adapter.contains("val visibleCommunities = item.communities.take(MAX_COMMUNITY_ITEM_COUNT)"))
|
||||||
|
assertTrue(adapter.contains("R.layout.view_feed_community"))
|
||||||
|
assertTrue(adapter.contains("communityItems?.addView(row)"))
|
||||||
|
assertTrue(adapter.contains("(row as FeedCommunityView).apply"))
|
||||||
|
assertTrue(adapter.contains("setFeedSize("))
|
||||||
|
assertTrue(adapter.contains("FeedSize("))
|
||||||
|
assertTrue(adapter.contains("val communityWidthDp = calculateCreatorChannelCommunityCardWidthDp("))
|
||||||
|
assertTrue(adapter.contains("rootWidthDp = communityWidthDp"))
|
||||||
|
assertTrue(adapter.contains("setHideEmptyTextRows(true)"))
|
||||||
|
assertTrue(adapter.contains("bind(community.toFeedCommunityItem())"))
|
||||||
|
assertTrue(adapter.contains("bindCommunityImages(row, community)"))
|
||||||
|
assertTrue(adapter.contains("BlurTransformation(itemView.context, 25f, 2.5f)"))
|
||||||
|
assertTrue(adapter.contains("row.layoutParams = LinearLayout.LayoutParams("))
|
||||||
|
assertTrue(adapter.contains("communityWidthDp.dp()"))
|
||||||
|
assertTrue(adapter.contains("LinearLayout.LayoutParams.WRAP_CONTENT"))
|
||||||
|
assertTrue(adapter.contains("communityImageUrl = community.imageUrl.takeIf { !it.isNullOrBlank() }"))
|
||||||
|
assertTrue(adapter.contains("communityImageView().setImageDrawable(null)"))
|
||||||
|
assertTrue(adapter.contains("private const val MAX_COMMUNITY_ITEM_COUNT = 3"))
|
||||||
|
assertFalse(adapter.contains("CreatorChannelCommunityCardView"))
|
||||||
|
assertFalse(adapter.contains("CreatorChannelCommunityThumbnailView"))
|
||||||
|
assertFalse(adapter.contains("private fun addFeedCard"))
|
||||||
|
assertFalse(adapter.contains("addFeedCard("))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 컴포넌트 width는 402dp 기준 최대 374dp이고 작은 화면에서는 비율 축소한다`() {
|
||||||
|
assertEquals(374, calculateCreatorChannelCommunityCardWidthDp(402))
|
||||||
|
assertEquals(374, calculateCreatorChannelCommunityCardWidthDp(430))
|
||||||
|
assertEquals(335, calculateCreatorChannelCommunityCardWidthDp(360))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `팬Talk 섹션은 Figma ListComment layout과 bind로 렌더링한다`() {
|
||||||
|
val adapter = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt"
|
||||||
|
).readText()
|
||||||
|
val fanTalkLayout = projectFile("app/src/main/res/layout/item_creator_channel_home_fantalk.xml").readText()
|
||||||
|
val fanTalkCardView = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelFanTalkCardView.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(fanTalkLayout.contains("@layout/view_section_title"))
|
||||||
|
assertTrue(fanTalkLayout.contains("CreatorChannelFanTalkCardView"))
|
||||||
|
assertTrue(fanTalkLayout.contains("@+id/layout_fantalk_card"))
|
||||||
|
assertTrue(fanTalkLayout.contains("@+id/layout_fantalk_total_row"))
|
||||||
|
assertTrue(fanTalkLayout.contains("@+id/tv_fantalk_total_label"))
|
||||||
|
assertTrue(fanTalkLayout.contains("@+id/tv_fantalk_total_count"))
|
||||||
|
assertTrue(fanTalkLayout.contains("@+id/layout_fantalk_latest_row"))
|
||||||
|
assertTrue(fanTalkLayout.contains("@+id/iv_fantalk_profile"))
|
||||||
|
assertTrue(fanTalkLayout.contains("@+id/tv_fantalk_content"))
|
||||||
|
assertTrue(fanTalkLayout.contains("@+id/layout_fantalk_empty"))
|
||||||
|
assertTrue(fanTalkLayout.contains("@+id/tv_fantalk_empty_title"))
|
||||||
|
assertTrue(fanTalkLayout.contains("@+id/layout_fantalk_support_button"))
|
||||||
|
assertTrue(fanTalkLayout.contains("@string/creator_channel_fantalk_empty_title"))
|
||||||
|
assertTrue(fanTalkLayout.contains("@string/creator_channel_fantalk_support_action"))
|
||||||
|
assertTrue(fanTalkLayout.contains("@drawable/ic_new_fantalk_plus"))
|
||||||
|
assertTrue(fanTalkLayout.contains("android:layout_width=\"match_parent\""))
|
||||||
|
assertTrue(fanTalkLayout.contains("android:layout_height=\"169dp\""))
|
||||||
|
assertTrue(fanTalkLayout.contains("android:layout_marginBottom=\"@dimen/spacing_6\""))
|
||||||
|
assertTrue(fanTalkLayout.contains("@drawable/ic_chevron_down_white"))
|
||||||
|
assertFalse(fanTalkLayout.contains("@+id/ll_section_items"))
|
||||||
|
assertFalse(fanTalkLayout.contains("android:clipToOutline"))
|
||||||
|
assertTrue(fanTalkCardView.contains("clipToOutline = true"))
|
||||||
|
assertTrue(fanTalkCardView.contains("ViewOutlineProvider"))
|
||||||
|
assertTrue(fanTalkCardView.contains("outline.setRoundRect"))
|
||||||
|
assertTrue(adapter.contains("private val fanTalkTotalCount: TextView?"))
|
||||||
|
assertTrue(adapter.contains("private val fanTalkCard: View?"))
|
||||||
|
assertTrue(adapter.contains("calculateCreatorChannelFanTalkCardWidthDp"))
|
||||||
|
assertTrue(adapter.contains("private val fanTalkProfile: ImageView?"))
|
||||||
|
assertTrue(adapter.contains("private val fanTalkContent: TextView?"))
|
||||||
|
assertTrue(adapter.contains("private val fanTalkTotalRow: View?"))
|
||||||
|
assertTrue(adapter.contains("private val fanTalkLatestRow: View?"))
|
||||||
|
assertTrue(adapter.contains("private val fanTalkEmpty: View?"))
|
||||||
|
assertTrue(adapter.contains("fanTalkTotalCount?.text = item.fanTalk.totalCount.toString()"))
|
||||||
|
assertTrue(adapter.contains("fanTalkTotalRow?.isVisible = fanTalk != null"))
|
||||||
|
assertTrue(adapter.contains("fanTalkLatestRow?.isVisible = fanTalk != null"))
|
||||||
|
assertTrue(adapter.contains("fanTalkEmpty?.isVisible = fanTalk == null"))
|
||||||
|
assertTrue(adapter.contains("fanTalkContent?.text = fanTalk.content"))
|
||||||
|
assertTrue(adapter.contains("fanTalkProfile?.loadUrl(fanTalk.profileImageUrl)"))
|
||||||
|
assertTrue(adapter.contains("fanTalkContent?.text = \"\""))
|
||||||
|
assertTrue(adapter.contains("fanTalkProfile?.setImageResource(R.drawable.ic_placeholder_profile)"))
|
||||||
|
assertFalse(adapter.contains("private fun addCommentCard"))
|
||||||
|
assertFalse(adapter.contains("addCommentCard("))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `팬Talk 카드 width는 402dp 기준 최대 374dp이고 작은 화면에서는 비율 축소한다`() {
|
||||||
|
assertEquals(374, calculateCreatorChannelFanTalkCardWidthDp(402))
|
||||||
|
assertEquals(374, calculateCreatorChannelFanTalkCardWidthDp(430))
|
||||||
|
assertEquals(335, calculateCreatorChannelFanTalkCardWidthDp(360))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `SNS source는 ic_sns 아이콘을 ImageView로 표시한다`() {
|
fun `SNS source는 ic_sns 아이콘을 ImageView로 표시한다`() {
|
||||||
val uiModel = projectFile(
|
val uiModel = projectFile(
|
||||||
@@ -606,11 +818,8 @@ class CreatorChannelHomeActivitySourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `section item layouts는 legacy generic card id를 제거하고 동적 컨테이너만 둔다`() {
|
fun `남은 section item layouts는 legacy generic card id를 제거하고 동적 컨테이너만 둔다`() {
|
||||||
val layoutNames = listOf(
|
val layoutNames = listOf(
|
||||||
"series",
|
|
||||||
"community",
|
|
||||||
"fantalk",
|
|
||||||
"introduce",
|
"introduce",
|
||||||
"activity",
|
"activity",
|
||||||
"sns"
|
"sns"
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class CreatorChannelHomeMapperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `null 단건 콘텐츠와 빈 리스트와 blank SNS는 section을 생성하지 않는다`() {
|
fun `null 단건 콘텐츠와 빈 리스트와 blank SNS는 팬Talk empty section만 생성한다`() {
|
||||||
val content = response(
|
val content = response(
|
||||||
currentLive = null,
|
currentLive = null,
|
||||||
latestAudioContent = null,
|
latestAudioContent = null,
|
||||||
@@ -81,7 +81,7 @@ class CreatorChannelHomeMapperTest {
|
|||||||
assertFalse(content.sections.any { it is CreatorChannelHomeSection.AudioContents })
|
assertFalse(content.sections.any { it is CreatorChannelHomeSection.AudioContents })
|
||||||
assertFalse(content.sections.any { it is CreatorChannelHomeSection.Series })
|
assertFalse(content.sections.any { it is CreatorChannelHomeSection.Series })
|
||||||
assertFalse(content.sections.any { it is CreatorChannelHomeSection.Communities })
|
assertFalse(content.sections.any { it is CreatorChannelHomeSection.Communities })
|
||||||
assertFalse(content.sections.any { it is CreatorChannelHomeSection.FanTalk })
|
assertTrue(content.sections.any { it is CreatorChannelHomeSection.FanTalk })
|
||||||
assertFalse(content.sections.any { it is CreatorChannelHomeSection.Introduce })
|
assertFalse(content.sections.any { it is CreatorChannelHomeSection.Introduce })
|
||||||
assertFalse(content.sections.any { it is CreatorChannelHomeSection.Sns })
|
assertFalse(content.sections.any { it is CreatorChannelHomeSection.Sns })
|
||||||
}
|
}
|
||||||
@@ -239,11 +239,8 @@ class CreatorChannelHomeMapperTest {
|
|||||||
seriesId = 1L,
|
seriesId = 1L,
|
||||||
title = "시리즈",
|
title = "시리즈",
|
||||||
coverImageUrl = "https://example.com/series.png",
|
coverImageUrl = "https://example.com/series.png",
|
||||||
publishedDaysOfWeek = "MON",
|
|
||||||
isComplete = false,
|
|
||||||
numberOfContent = 3,
|
numberOfContent = 3,
|
||||||
isNew = true,
|
isNew = true,
|
||||||
isPopular = false,
|
|
||||||
isOriginal = true
|
isOriginal = true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1088,28 +1088,15 @@ class HomeMainFragmentLayoutTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `home popular community adapter does not load original image for locked paid post`() {
|
fun `home popular community adapter applies blur when locked paid post image is loaded`() {
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
val source = projectFile(
|
||||||
val parent = RecyclerView(context)
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomePopularCommunityAdapter.kt"
|
||||||
parent.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
).readText()
|
||||||
val adapter = HomePopularCommunityAdapter(onClickItem = {})
|
|
||||||
val post = popularCommunityPost(1L).copy(
|
|
||||||
item = popularCommunityPost(1L).item.copy(
|
|
||||||
imageUrl = "https://example.com/paid.png",
|
|
||||||
price = 30,
|
|
||||||
existOrdered = false
|
|
||||||
),
|
|
||||||
paidStatus = HomeRecommendationPaidStatus.Paid(30)
|
|
||||||
)
|
|
||||||
adapter.submitSection(HomeRecommendationPopularCommunityPostSection(listOf(post)))
|
|
||||||
val viewHolder = adapter.onCreateViewHolder(parent, 0)
|
|
||||||
val communityImage = viewHolder.itemView.findViewById<ImageView>(R.id.iv_feed_community_image)
|
|
||||||
communityImage.setImageResource(R.drawable.ic_launcher_background)
|
|
||||||
|
|
||||||
adapter.onBindViewHolder(viewHolder, 0)
|
assertTrue(source.contains("imageUrl = item.item.imageUrl"))
|
||||||
|
assertTrue(source.contains("shouldBlur = item.item.price > 0 && !item.item.existOrdered"))
|
||||||
assertEquals(null, communityImage.drawable)
|
assertTrue(source.contains("BlurTransformation(imageView.context, 25f, 2.5f)"))
|
||||||
assertEquals(View.VISIBLE, viewHolder.itemView.findViewById<View>(R.id.ll_feed_community_paid_overlay).visibility)
|
assertFalse(source.contains("item.item.imageUrl.takeIf { item.item.price <= 0 || item.item.existOrdered }"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -1550,6 +1537,12 @@ class HomeMainFragmentLayoutTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun projectFile(relativePath: String): java.io.File {
|
||||||
|
val candidates = listOf(java.io.File(relativePath), java.io.File("../$relativePath"))
|
||||||
|
return candidates.firstOrNull { it.exists() }
|
||||||
|
?: error("Project file not found: $relativePath")
|
||||||
|
}
|
||||||
|
|
||||||
private fun genreGroup(
|
private fun genreGroup(
|
||||||
genre: String,
|
genre: String,
|
||||||
creators: List<HomeRecommendationCreatorUiModel>
|
creators: List<HomeRecommendationCreatorUiModel>
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ import org.junit.Test
|
|||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
@Config(sdk = [28], application = Application::class)
|
@Config(sdk = [28], application = Application::class)
|
||||||
class FeedViewTest {
|
class FeedViewTest {
|
||||||
@@ -45,23 +43,31 @@ class FeedViewTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `community can hide empty body and keyword rows by caller policy`() {
|
fun `community can hide empty body row by caller policy`() {
|
||||||
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
||||||
|
|
||||||
view.setHideEmptyTextRows(true)
|
view.setHideEmptyTextRows(true)
|
||||||
view.bind(sampleCommunityItem(bodyText = "", keywordText = ""))
|
view.bind(sampleCommunityItem(bodyText = "", keywordText = ""))
|
||||||
|
|
||||||
assertEquals(View.GONE, view.findViewById<TextView>(R.id.tv_feed_community_body).visibility)
|
assertEquals(View.GONE, view.findViewById<TextView>(R.id.tv_feed_community_body).visibility)
|
||||||
assertEquals(View.GONE, view.findViewById<TextView>(R.id.tv_feed_community_keyword).visibility)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `community keeps keyword visible by default for existing feed usage`() {
|
fun `community layout matches creator channel feed row structure`() {
|
||||||
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()
|
||||||
|
|
||||||
view.bind(sampleCommunityItem(bodyText = "본문", keywordText = "#키워드"))
|
assertFalse(layout.contains("@+id/tv_feed_community_keyword"))
|
||||||
|
assertTrue(layout.contains("android:layout_width=\"match_parent\""))
|
||||||
assertEquals(View.VISIBLE, view.findViewById<TextView>(R.id.tv_feed_community_keyword).visibility)
|
assertTrue(layout.contains("android:maxLines=\"5\""))
|
||||||
|
assertTrue(layout.contains("android:layout_height=\"236dp\""))
|
||||||
|
assertTrue(layout.contains("@drawable/bg_creator_channel_community_price"))
|
||||||
|
assertTrue(layout.contains("@drawable/ic_bar_cash"))
|
||||||
|
assertFalse(layout.contains("android:drawableStart=\"@drawable/ic_bar_cash\""))
|
||||||
|
assertTrue(layout.contains("android:layout_marginStart=\"@dimen/spacing_6\""))
|
||||||
|
assertTrue(layout.contains("@color/gray_400"))
|
||||||
|
assertFalse(layout.contains("android:layout_height=\"0dp\""))
|
||||||
|
assertEquals(5, view.findViewById<TextView>(R.id.tv_feed_community_body).maxLines)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -88,8 +94,8 @@ class FeedViewTest {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(View.GONE, view.findViewById<TextView>(R.id.tv_feed_community_keyword).visibility)
|
|
||||||
assertEquals(View.VISIBLE, view.findViewById<FrameLayout>(R.id.fl_feed_community_image_container).visibility)
|
assertEquals(View.VISIBLE, view.findViewById<FrameLayout>(R.id.fl_feed_community_image_container).visibility)
|
||||||
|
assertEquals(View.VISIBLE, view.findViewById<ImageView>(R.id.iv_feed_community_image).visibility)
|
||||||
assertEquals(View.VISIBLE, view.findViewById<View>(R.id.ll_feed_community_paid_overlay).visibility)
|
assertEquals(View.VISIBLE, view.findViewById<View>(R.id.ll_feed_community_paid_overlay).visibility)
|
||||||
assertEquals("30", view.findViewById<TextView>(R.id.tv_feed_community_price).text.toString())
|
assertEquals("30", view.findViewById<TextView>(R.id.tv_feed_community_price).text.toString())
|
||||||
}
|
}
|
||||||
@@ -114,7 +120,7 @@ class FeedViewTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `community image container follows measured card width ratio`() {
|
fun `community image container keeps creator channel fixed height`() {
|
||||||
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)
|
||||||
|
|
||||||
@@ -124,7 +130,7 @@ class FeedViewTest {
|
|||||||
|
|
||||||
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((expectedImageWidth * 236 / 346f).roundToInt(), imageContainer.measuredHeight)
|
assertEquals(236.dpToPx(), imageContainer.measuredHeight)
|
||||||
assertEquals(true, imageContainer.clipToOutline)
|
assertEquals(true, imageContainer.clipToOutline)
|
||||||
assertNotNull(imageContainer.outlineProvider)
|
assertNotNull(imageContainer.outlineProvider)
|
||||||
}
|
}
|
||||||
@@ -205,6 +211,12 @@ class FeedViewTest {
|
|||||||
|
|
||||||
private fun exactly(size: Int): Int = View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY)
|
private fun exactly(size: Int): Int = View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY)
|
||||||
|
|
||||||
|
private fun projectFile(relativePath: String): java.io.File {
|
||||||
|
val candidates = listOf(java.io.File(relativePath), java.io.File("../$relativePath"))
|
||||||
|
return candidates.firstOrNull { it.exists() }
|
||||||
|
?: error("Project file not found: $relativePath")
|
||||||
|
}
|
||||||
|
|
||||||
private fun assertNotEmptyContentDescription(imageView: ImageView) {
|
private fun assertNotEmptyContentDescription(imageView: ImageView) {
|
||||||
assertTrue(!imageView.contentDescription.isNullOrEmpty())
|
assertTrue(!imageView.contentDescription.isNullOrEmpty())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -650,7 +650,7 @@
|
|||||||
- 검증 기록:
|
- 검증 기록:
|
||||||
- 2026-06-15: Figma `296:14958`을 확인해 오디오 컨텐츠 섹션이 `SectionTitle`, 88dp 썸네일, 16sp title, 14sp secondary text를 가진 item row이며, 아이템 최대 width가 402dp 기준 346dp인 구조임을 반영했다. 사용자 요구에 따라 최대 9개만 표시하고, 섹션은 `RecyclerView + GridLayoutManager`로 구성했다. `AudioContentCardView`는 요구사항과 달라 재사용하지 않고 `CreatorChannelHomeAudioContentCardView` 전용 Android View와 `item_creator_channel_home_audio_content.xml`을 추가했다. item 내부는 썸네일 + 텍스트 컬럼의 단순 행 구조라 사용자 피드백에 따라 `GridLayout`이 아니라 `LinearLayout`으로 구성하고, 목록 grid는 `GridLayoutManager`로 유지했다. 아이템 클릭은 `AudioContentDetailActivity`에 `Constants.EXTRA_AUDIO_CONTENT_ID`로 이동하도록 연결했다. RED로 `CreatorChannelHomeActivitySourceTest`에 오디오 전용 layout/View, 최대 9개, 346dp width 비율 계산, `GridLayoutManager`, 상세 이동 계약을 추가했고, 최초 실행에서 `calculateCreatorChannelAudioItemWidthDp` 미구현으로 실패함을 확인했다. 구현 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeActivitySourceTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`가 PASS했다. `compileDebugKotlin` 중 Kotlin daemon cache/파일 경합으로 fallback compile이 발생했으나 최종 빌드는 성공했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation 경고와 Kotlin/Gradle deprecation warning은 기존 경고로 이번 변경과 무관하다.
|
- 2026-06-15: Figma `296:14958`을 확인해 오디오 컨텐츠 섹션이 `SectionTitle`, 88dp 썸네일, 16sp title, 14sp secondary text를 가진 item row이며, 아이템 최대 width가 402dp 기준 346dp인 구조임을 반영했다. 사용자 요구에 따라 최대 9개만 표시하고, 섹션은 `RecyclerView + GridLayoutManager`로 구성했다. `AudioContentCardView`는 요구사항과 달라 재사용하지 않고 `CreatorChannelHomeAudioContentCardView` 전용 Android View와 `item_creator_channel_home_audio_content.xml`을 추가했다. item 내부는 썸네일 + 텍스트 컬럼의 단순 행 구조라 사용자 피드백에 따라 `GridLayout`이 아니라 `LinearLayout`으로 구성하고, 목록 grid는 `GridLayoutManager`로 유지했다. 아이템 클릭은 `AudioContentDetailActivity`에 `Constants.EXTRA_AUDIO_CONTENT_ID`로 이동하도록 연결했다. RED로 `CreatorChannelHomeActivitySourceTest`에 오디오 전용 layout/View, 최대 9개, 346dp width 비율 계산, `GridLayoutManager`, 상세 이동 계약을 추가했고, 최초 실행에서 `calculateCreatorChannelAudioItemWidthDp` 미구현으로 실패함을 확인했다. 구현 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeActivitySourceTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`가 PASS했다. `compileDebugKotlin` 중 Kotlin daemon cache/파일 경합으로 fallback compile이 발생했으나 최종 빌드는 성공했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation 경고와 Kotlin/Gradle deprecation warning은 기존 경고로 이번 변경과 무관하다.
|
||||||
|
|
||||||
- [ ] **Task 5R.7: 시리즈 섹션 재구성**
|
- [x] **Task 5R.7: 시리즈 섹션 재구성**
|
||||||
- 수정 예정:
|
- 수정 예정:
|
||||||
- `CreatorChannelHomeSectionAdapter.kt`
|
- `CreatorChannelHomeSectionAdapter.kt`
|
||||||
- `item_creator_channel_home_series.xml`
|
- `item_creator_channel_home_series.xml`
|
||||||
@@ -661,12 +661,17 @@
|
|||||||
- 작업:
|
- 작업:
|
||||||
- `series`가 empty이면 섹션을 표시하지 않는다.
|
- `series`가 empty이면 섹션을 표시하지 않는다.
|
||||||
- 시리즈 썸네일은 163dp x 230dp 비율과 radius 14dp를 따른다.
|
- 시리즈 썸네일은 163dp x 230dp 비율과 radius 14dp를 따른다.
|
||||||
- `isOriginal`, `isNew`, `isPopular`, `isComplete`, `numberOfContent`, `publishedDaysOfWeek`를 Figma tag/label 정책에 맞춰 표시한다.
|
- 백엔드에서 내려오지 않는 `isPopular`, `isComplete`, `publishedDaysOfWeek`는 표시하지 않고, 현재 응답에서 화면 표시가 가능한 `coverImageUrl`, `isOriginal`만 표시한다.
|
||||||
|
- item click에 필요한 `seriesId`는 row click callback으로 전달 가능한 구조를 둔다.
|
||||||
- 기존 `SeriesContentCardView` 또는 시리즈 카드 layout을 재사용할 수 있는지 먼저 확인한다.
|
- 기존 `SeriesContentCardView` 또는 시리즈 카드 layout을 재사용할 수 있는지 먼저 확인한다.
|
||||||
- 검증 기준:
|
- 검증 기준:
|
||||||
- 시리즈 섹션은 Figma `Contents` 구조에 대응하는 View/layout으로 렌더링된다.
|
- 시리즈 섹션은 Figma `Contents` 구조에 대응하는 View/layout으로 렌더링된다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 2026-06-15: Figma `296:14970`을 확인해 시리즈 섹션이 `SectionTitle`, 163dp x 230dp `Contents(type=series, size=m)` 썸네일 카드 row 구조임을 반영했다. 사용자 추가 설명에 따라 `CreatorChannelSeriesResponse`에서 현재 화면에 쓰는 값은 `coverImageUrl`, `isOriginal`이고 터치 시 필요한 값은 `seriesId`로 범위를 축소했다. RED로 `CreatorChannelHomeActivitySourceTest`에 시리즈 전용 layout/custom view, radius clipping custom view, 402dp 기준 163dp width 비율 계산, 백엔드 미전송 필드 미사용 계약을 추가했고, 최초 실행에서 기존 adapter의 `publishedDaysOfWeek` 참조로 `compileDebugKotlin` 실패함을 확인했다. 구현 후 focused source test와 creator channel 전체 테스트가 PASS했다.
|
||||||
|
- 2026-06-15: reviewer gate에서 작은 화면에서 adapter row width는 146dp로 줄지만 내부 썸네일 XML은 163dp x 230dp 고정이라 클리핑/비율 불일치가 발생할 수 있다는 FAIL을 받았다. RED로 `calculateCreatorChannelSeriesCardHeightDp` 미구현 컴파일 실패를 확인한 뒤, `CreatorChannelHomeSeriesCardView.setThumbnailSize(widthDp, heightDp)`와 높이 비율 계산을 추가해 thumbnail width/height를 함께 조정했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeActivitySourceTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`가 PASS했다.
|
||||||
|
- 2026-06-15: 사용자 후속 확인에서 `item_creator_channel_home_series_content.xml`의 오리지널 태그가 Figma `296:14973`과 다르다는 지적을 반영했다. 기존 단일 `ic_content_tag_original` 이미지를 제거하고 `layout_series_original_tag` 70dp x 24dp 컨테이너 안에 `iv_series_original_icon`과 `tv_series_original_text(Only)`를 배치했다. `CreatorChannelHomeSeriesCardView`는 새 태그 컨테이너 visibility를 `series.isOriginal`로 토글하도록 변경했다.
|
||||||
|
|
||||||
- [ ] **Task 5R.8: 커뮤니티 섹션 재구성**
|
- [x] **Task 5R.8: 커뮤니티 섹션 재구성**
|
||||||
- 수정 예정:
|
- 수정 예정:
|
||||||
- `CreatorChannelHomeSectionAdapter.kt`
|
- `CreatorChannelHomeSectionAdapter.kt`
|
||||||
- `item_creator_channel_home_community.xml`
|
- `item_creator_channel_home_community.xml`
|
||||||
@@ -680,8 +685,14 @@
|
|||||||
- Figma의 `전체보기` capsule 버튼을 표시하되, 이동 대상이 없는 경우 click listener는 후속 범위로 분리한다.
|
- Figma의 `전체보기` capsule 버튼을 표시하되, 이동 대상이 없는 경우 click listener는 후속 범위로 분리한다.
|
||||||
- 검증 기준:
|
- 검증 기준:
|
||||||
- 커뮤니티는 단순 `addFeedCard`가 아니라 Figma `Feed` 구조를 가진 View/layout으로 렌더링된다.
|
- 커뮤니티는 단순 `addFeedCard`가 아니라 Figma `Feed` 구조를 가진 View/layout으로 렌더링된다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 2026-06-15: Figma `296:14982`를 확인해 커뮤니티 섹션이 `SectionTitle(hasRightIcon=false)`, 374dp feed card 최대 3개, 42dp 프로필, 작성자/날짜, 본문, 선택 이미지, 유료 잠금 overlay, 댓글/좋아요 reaction, `전체보기` capsule 버튼 구조임을 반영했다. RED로 `CreatorChannelHomeActivitySourceTest`에 커뮤니티 전용 layout/row/custom view, 최대 3개, 402dp 기준 374dp width 비율 계산, `addFeedCard` 제거 계약을 추가했고, 구현 후 focused source test와 creator channel 전체 테스트가 PASS했다.
|
||||||
|
- 2026-06-15: reviewer gate에서 `imageUrl`이 없는 게시글도 236dp 이미지 컨테이너가 남는 문제와 잠긴 유료 이미지가 그대로 `loadUrl` 되는 privacy leak 가능성으로 FAIL을 받았다. RED로 `layout_community_thumbnail` visibility, `hasCommunityImage`, `isCommunityLocked`, 잠김 이미지 미로드/clear 계약을 추가한 뒤, `bindCommunityRow()`에서 이미지 컨테이너는 이미지가 있거나 잠김 상태일 때만 보이게 하고, 잠긴 상태에서는 썸네일을 숨기고 `setImageDrawable(null)`로 초기화하도록 수정했다. 수정 후 focused source test와 creator channel 전체 테스트, 리소스/컴파일/ktlint/diff check가 PASS했다.
|
||||||
|
- 2026-06-15: 사용자 후속 확인에서 `FeedCommunityView`와 새로 만든 `CreatorChannelCommunityCardView`가 동일 UI이며, 실제로는 `FeedCommunityView`를 `item_creator_channel_home_community_row.xml`와 동일하게 고쳐 재사용해야 한다는 지적을 반영했다. `view_feed_community.xml`을 커뮤니티 row 구조에 맞춰 keyword row 제거, 본문 5줄, 이미지 컨테이너 236dp 고정, `bg_creator_channel_community_price`/`ic_bar_cash`, reaction `gray_400`, root `match_parent` 구조로 수정했다. `CreatorChannelHomeSectionAdapter`는 `R.layout.view_feed_community`를 inflate하고 `FeedItem.Community`로 변환해 `FeedCommunityView`를 바인딩하도록 변경했으며, 더 이상 필요 없는 `CreatorChannelCommunityCardView`, `CreatorChannelCommunityThumbnailView`, `item_creator_channel_home_community_row.xml`는 참조 제거 후 삭제했다.
|
||||||
|
- 2026-06-15: 사용자 후속 확인에서 `tv_feed_community_price`의 `drawableStart` 방식은 Figma처럼 가격과 아이콘 간격을 정확히 표현하기 어렵다는 지적을 반영했다. 가격 capsule을 `LinearLayout` 안의 별도 `ImageView(ic_bar_cash)`와 `TextView(tv_feed_community_price)` 구조로 바꾸고, 텍스트에 `layout_marginStart=@dimen/spacing_6`을 지정해 아이콘/가격 간격을 명시했다. 미구매 유료 커뮤니티 이미지 blur는 원본 보호 관점에서 클라이언트 blur보다 서버가 비가역 preview/blur 이미지를 별도 URL로 내려주는 방식이 더 안전하다고 판단했다.
|
||||||
|
- 2026-06-15: 사용자 후속 요청으로 서버가 미구매 유료 커뮤니티 게시글의 `imageUrl`을 blur 처리하지 않고 내려줄 가능성에 대비해 클라이언트에서도 blur를 한 번 더 적용하도록 변경했다. 기존 커뮤니티 목록 `CreatorCommunityAdapter`에서 사용하던 `BlurTransformation(context, 25f, 2.5f)` 패턴을 확인했고, 크리에이터 채널 홈과 홈 추천의 `FeedCommunityView` 이미지 바인딩에도 같은 transformation을 적용했다. 이제 미구매 유료 게시글도 `imageUrl`을 로드하되 이미지 위에 lock overlay를 유지하고, 구매하지 않은 상태에서는 클라이언트 blur fallback을 적용한다.
|
||||||
|
|
||||||
- [ ] **Task 5R.9: 팬Talk 섹션 재구성**
|
- [x] **Task 5R.9: 팬Talk 섹션 재구성**
|
||||||
- 수정 예정:
|
- 수정 예정:
|
||||||
- `CreatorChannelHomeSectionAdapter.kt`
|
- `CreatorChannelHomeSectionAdapter.kt`
|
||||||
- `item_creator_channel_home_fantalk.xml`
|
- `item_creator_channel_home_fantalk.xml`
|
||||||
@@ -694,6 +705,10 @@
|
|||||||
- 최신 팬Talk의 프로필 28dp 원형 이미지, 댓글 본문 2줄 말줄임, chevron-down 아이콘을 표시한다.
|
- 최신 팬Talk의 프로필 28dp 원형 이미지, 댓글 본문 2줄 말줄임, chevron-down 아이콘을 표시한다.
|
||||||
- 검증 기준:
|
- 검증 기준:
|
||||||
- 팬Talk는 `addCommentCard`가 아니라 `ListComment` 대응 View/layout으로 렌더링된다.
|
- 팬Talk는 `addCommentCard`가 아니라 `ListComment` 대응 View/layout으로 렌더링된다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 2026-06-15: Figma `296:14989`를 확인해 팬Talk 섹션이 `SectionTitle`, 374dp `ListComment` card, `전체` label과 total count, 28dp 프로필, 2줄 말줄임 본문, 18dp chevron-down 구조임을 반영했다. RED로 `CreatorChannelHomeActivitySourceTest`에 팬Talk 전용 layout/custom view, 402dp 기준 374dp width 비율 계산, `addCommentCard` 제거 계약을 추가했고, 구현 후 focused source test와 creator channel 전체 테스트가 PASS했다. `전체` label은 기존 다국어 string `screen_chat_filter_all`을 재사용했다.
|
||||||
|
- 2026-06-15: reviewer gate에서 `latestFanTalk == null` 리바인드 시 이전 ViewHolder의 프로필/본문이 남을 수 있다는 FAIL을 받았다. RED로 `layout_fantalk_latest_row`, null 시 row 숨김, 본문 빈 문자열, placeholder 초기화 계약을 추가한 뒤, `bindFanTalk()`에서 최신 팬Talk 유무에 따라 row visibility와 값을 명시적으로 초기화했다. 수정 후 focused source test와 creator channel 전체 테스트, 리소스/컴파일/ktlint/diff check가 PASS했다.
|
||||||
|
- 2026-06-15: 사용자 후속 요청으로 팬Talk가 없을 때 표시할 empty UI Figma `304:17362`를 반영했다. 기존에는 `totalCount=0`, `latestFanTalk=null`이면 FanTalk section을 생성하지 않았지만, empty card 표시를 위해 mapper가 항상 FanTalk section을 포함하도록 변경했다. `item_creator_channel_home_fantalk.xml`에는 `layout_fantalk_empty`를 추가하고, `당신의 첫 응원을\n기다리고 있어요!` 문구와 `ic_new_fantalk_plus` 아이콘, `응원 남기기` 흰색 capsule 버튼을 배치했다. `bindFanTalk()`는 최신 팬Talk가 있으면 total/latest row를 표시하고 empty UI를 숨기며, 없으면 total/latest row를 숨기고 empty UI를 표시하도록 변경했다.
|
||||||
|
|
||||||
- [ ] **Task 5R.10: 소개 섹션 재구성**
|
- [ ] **Task 5R.10: 소개 섹션 재구성**
|
||||||
- 수정 예정:
|
- 수정 예정:
|
||||||
@@ -934,3 +949,4 @@
|
|||||||
- 2026-06-14: Phase 5 SNS 후속 리뷰 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeMapperTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeActivitySourceTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`를 실행했다. 모두 `BUILD SUCCESSFUL` 또는 출력 없음(`git diff --check`)을 확인했다. 첫 RED 테스트 시도는 sandbox의 `~/.gradle` lock 파일 접근 제한으로 실패해 승인 후 재실행했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation 경고는 기존 설정 경고로 이번 변경과 무관하다.
|
- 2026-06-14: Phase 5 SNS 후속 리뷰 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeMapperTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeActivitySourceTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`를 실행했다. 모두 `BUILD SUCCESSFUL` 또는 출력 없음(`git diff --check`)을 확인했다. 첫 RED 테스트 시도는 sandbox의 `~/.gradle` lock 파일 접근 제한으로 실패해 승인 후 재실행했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation 경고는 기존 설정 경고로 이번 변경과 무관하다.
|
||||||
- 2026-06-15: 사용자 요청에 따라 탭 바 아래 홈 컨텐츠를 전부 삭제 후 Figma `296:14895`의 `SectionTitle`, `Contents`, `Feed`, `ListComment`, `SNS` 컴포넌트 기준으로 다시 구성하는 Phase 5R을 추가했다. 각 홈 섹션을 하나의 Task로 분리했고, 현재 API 응답에 없는 `화보` 섹션은 이번 재구성 구현 범위에서 제외한다고 명시했다. 이번 단계는 문서 수정만 수행했으며 구현/빌드/테스트는 실행하지 않았다.
|
- 2026-06-15: 사용자 요청에 따라 탭 바 아래 홈 컨텐츠를 전부 삭제 후 Figma `296:14895`의 `SectionTitle`, `Contents`, `Feed`, `ListComment`, `SNS` 컴포넌트 기준으로 다시 구성하는 Phase 5R을 추가했다. 각 홈 섹션을 하나의 Task로 분리했고, 현재 API 응답에 없는 `화보` 섹션은 이번 재구성 구현 범위에서 제외한다고 명시했다. 이번 단계는 문서 수정만 수행했으며 구현/빌드/테스트는 실행하지 않았다.
|
||||||
- 2026-06-15: Phase 5R.4 공지 섹션 재구성으로 Figma `296:14915` 기준 `SectionTitle`, `ic_pin` Notice label, 프로필/작성자/날짜, 본문, 선택 썸네일을 가진 공지 전용 가로 feed card를 추가했다. 공지는 최대 3개까지 표시하고, item width는 402dp 기준 최대 346dp 및 작은 화면 비율 축소로 계산한다. RED로 공지 row/custom thumbnail 부재와 기존 374dp width 계산 실패를 확인한 뒤 구현했으며, `CreatorChannelHomeActivitySourceTest` 공지 단일 테스트 2개, `kr.co.vividnext.sodalive.v2.creator.channel.*`, `:app:mergeDebugResources`, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`가 모두 PASS했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation 경고는 기존 설정 경고로 이번 변경과 무관하다.
|
- 2026-06-15: Phase 5R.4 공지 섹션 재구성으로 Figma `296:14915` 기준 `SectionTitle`, `ic_pin` Notice label, 프로필/작성자/날짜, 본문, 선택 썸네일을 가진 공지 전용 가로 feed card를 추가했다. 공지는 최대 3개까지 표시하고, item width는 402dp 기준 최대 346dp 및 작은 화면 비율 축소로 계산한다. RED로 공지 row/custom thumbnail 부재와 기존 374dp width 계산 실패를 확인한 뒤 구현했으며, `CreatorChannelHomeActivitySourceTest` 공지 단일 테스트 2개, `kr.co.vividnext.sodalive.v2.creator.channel.*`, `:app:mergeDebugResources`, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`가 모두 PASS했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation 경고는 기존 설정 경고로 이번 변경과 무관하다.
|
||||||
|
- 2026-06-15: 코드 리뷰에서 커뮤니티 카드가 `setFeedSize(374dp 기준)` 호출 후 `LinearLayout.LayoutParams.MATCH_PARENT`로 덮여 실제 폭 계산이 적용되지 않는다는 지적을 확인했다. `CreatorChannelHomeSectionAdapter.bindCommunities()`에서 `communityWidthDp`를 한 번 계산해 `FeedSize.rootWidthDp`와 row `layoutParams.width`에 동일하게 적용하도록 수정하고, `CreatorChannelHomeActivitySourceTest`가 `communityWidthDp.dp()` 계약을 검증하도록 갱신했다.
|
||||||
|
|||||||
Reference in New Issue
Block a user