fix(home): 팔로잉 탭 후속 검증을 반영한다

This commit is contained in:
2026-07-31 03:22:50 +09:00
parent f600d10e8b
commit 49f6fe592c
37 changed files with 2918 additions and 447 deletions

View File

@@ -20,6 +20,7 @@ import kr.co.vividnext.sodalive.v2.access.AccessRequirement
import kr.co.vividnext.sodalive.v2.access.ensureV2Access
import kr.co.vividnext.sodalive.v2.chat.action.ChatActionCommand
import kr.co.vividnext.sodalive.v2.chat.action.handleChatAction
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
import kr.co.vividnext.sodalive.v2.community.action.CommunityActionCommand
import kr.co.vividnext.sodalive.v2.community.action.handleCommunityAction
import kr.co.vividnext.sodalive.v2.content.action.ContentActionCommand
@@ -30,6 +31,7 @@ import kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinator
import kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveActivity
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiItem
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomType
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingNewsType
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingChatSection
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingCreatorSection
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingLiveSection
@@ -409,7 +411,10 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
bindHomeFollowingEmpty(showEmptyMessage = true)
finishHomePullRefresh(HOME_TAB_FOLLOWING)
}
HomeFollowingUiState.LoginRequired -> bindHomeFollowingEmpty(showEmptyMessage = false)
HomeFollowingUiState.LoginRequired -> {
bindHomeFollowingEmpty(showEmptyMessage = false)
finishHomePullRefresh(HOME_TAB_FOLLOWING)
}
is HomeFollowingUiState.Error -> {
if (shouldPreserveHomeContentOnPullRefreshError(HOME_TAB_FOLLOWING)) return@observe
bindHomeFollowingEmpty(showEmptyMessage = false)
@@ -665,16 +670,39 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
private fun onFollowingSectionMoreClick(section: HomeFollowingSection) = Unit
private fun onFollowingLiveClick(item: HomeFollowingLiveUiItem) = Unit
private fun onFollowingLiveClick(item: HomeFollowingLiveUiItem) {
liveActionCoordinator.enterLiveRoom(item.liveId)
}
private fun onFollowingScheduleClick(item: HomeFollowingScheduleUiItem) = Unit
private fun onFollowingScheduleClick(item: HomeFollowingScheduleUiItem) {
if (item.targetId <= 0L) return
when (item.type) {
CreatorActivityType.Live -> liveActionCoordinator.enterLiveRoom(item.targetId)
CreatorActivityType.LiveReplay,
CreatorActivityType.Audio -> handleContentAction(
ContentActionCommand.AudioDetail(audioContentId = item.targetId)
)
CreatorActivityType.Community -> handleCommunityAction(
CommunityActionCommand.PostDetail(item.targetId)
)
}
}
private fun onFollowingNewsClick(item: HomeFollowingNewsUiItem) {
val community = item as? HomeFollowingNewsUiItem.Community ?: return
val postId = community.postId
when (item) {
is HomeFollowingNewsUiItem.Ranking -> if (item.type == FollowingNewsType.CREATOR_RANKING) {
handleCreatorAction(CreatorActionCommand.Profile(item.targetId))
}
is HomeFollowingNewsUiItem.Content -> if (item.type == FollowingNewsType.AUDIO_CONTENT) {
handleContentAction(ContentActionCommand.AudioDetail(audioContentId = item.contentId))
}
is HomeFollowingNewsUiItem.Community -> {
val postId = item.postId
if (postId <= 0L) return
handleCommunityAction(CommunityActionCommand.PostDetail(postId))
}
}
}
private fun openFollowingCreatorAll() {
ensureV2Access(AccessRequirement.Login) {

View File

@@ -1,7 +1,5 @@
package kr.co.vividnext.sodalive.v2.main.home.model
import androidx.annotation.StringRes
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.main.chat.model.toUiItems
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingCommunityPostNewsResponse
@@ -123,8 +121,7 @@ private fun FollowingContentNewsResponse.toUiItem(
contentImageUrl = contentImageUrl,
title = title,
creatorProfileImageUrl = creatorProfileImageUrl,
creatorNickname = creatorNickname,
labelResId = type.toLabelResId()
creatorNickname = creatorNickname
)
private fun FollowingCommunityPostNewsResponse.toUiItem(
@@ -147,12 +144,3 @@ private fun FollowingCommunityPostNewsResponse.toUiItem(
likeCount = likeCount,
commentCount = commentCount
)
@StringRes
private fun FollowingNewsType.toLabelResId(): Int = when (this) {
FollowingNewsType.PHOTO_CONTENT -> R.string.screen_home_following_photo_content
FollowingNewsType.AUDIO_CONTENT -> R.string.home_recommendation_activity_audio
FollowingNewsType.COMMUNITY_POST -> R.string.home_recommendation_activity_community
FollowingNewsType.CREATOR_RANKING,
FollowingNewsType.CONTENT_RANKING -> R.string.weekly_chart
}

View File

@@ -81,8 +81,7 @@ sealed interface HomeFollowingNewsUiItem {
val contentImageUrl: String?,
val title: String,
val creatorProfileImageUrl: String,
val creatorNickname: String,
@param:StringRes val labelResId: Int
val creatorNickname: String
) : HomeFollowingNewsUiItem
data class Community(

View File

@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.main.home.ui
import android.widget.ImageView
import coil.dispose
import coil.transform.CircleCropTransformation
import coil.transform.Transformation
import kr.co.vividnext.sodalive.extensions.loadUrl
@@ -9,6 +10,7 @@ fun homeCreatorProfileImageTransformations(): List<Transformation> = listOf(Circ
fun ImageView.loadHomeCreatorProfileImage(url: String?) {
if (url.isNullOrBlank()) {
dispose()
setImageDrawable(null)
} else {
loadUrl(url) {

View File

@@ -22,7 +22,9 @@ class HomeFollowingChatAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_home_following_chat, parent, false)
view.layoutParams = recyclerItemLayoutParams(parent)
val layoutParams = view.layoutParams as RecyclerView.LayoutParams
layoutParams.marginEnd = parent.resources.getDimensionPixelSize(R.dimen.spacing_12)
view.layoutParams = layoutParams
return ChatViewHolder(view, onClickItem)
}

View File

@@ -21,14 +21,15 @@ class HomeFollowingLiveAdapter(
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LiveViewHolder {
return LiveViewHolder(
LayoutInflater.from(parent.context).inflate(
val view = LayoutInflater.from(parent.context).inflate(
R.layout.item_home_following_live,
parent,
false
),
onClickItem
)
val layoutParams = view.layoutParams as RecyclerView.LayoutParams
layoutParams.marginEnd = parent.resources.getDimensionPixelSize(R.dimen.spacing_12)
view.layoutParams = layoutParams
return LiveViewHolder(view, onClickItem)
}
override fun onBindViewHolder(holder: LiveViewHolder, position: Int) {

View File

@@ -1,9 +1,11 @@
package kr.co.vividnext.sodalive.v2.main.home.ui
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.dispose
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingNewsType
@@ -19,6 +21,38 @@ import kr.co.vividnext.sodalive.v2.widget.feed.FeedWidthMode
import kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityView
import kotlin.math.roundToInt
internal fun buildHomeFollowingRankingFeedItem(
context: Context,
ranking: HomeFollowingNewsUiItem.Ranking
): FeedItem.Rank {
val rankText = context.getString(R.string.screen_home_following_ranking_rank_format, ranking.rank)
val message = ranking.toRankingMessage(context, rankText)
return FeedItem.Rank(
feedId = ranking.newsId,
imageUrl = ranking.imageUrl.orEmpty(),
rankText = ranking.rank.toString(),
message = message,
highlightRanges = listOf(
FeedRankHighlight(
start = message.lastIndexOf(rankText),
endExclusive = message.lastIndexOf(rankText) + rankText.length
)
)
)
}
private fun HomeFollowingNewsUiItem.Ranking.toRankingMessage(context: Context, rankText: String): String {
val messageResId = when (type) {
FollowingNewsType.CREATOR_RANKING -> R.string.screen_home_following_creator_ranking_news_message
FollowingNewsType.CONTENT_RANKING -> R.string.screen_home_following_content_ranking_news_message
FollowingNewsType.COMMUNITY_POST,
FollowingNewsType.AUDIO_CONTENT,
FollowingNewsType.PHOTO_CONTENT -> R.string.screen_home_following_content_ranking_news_message
}
val subject = if (type == FollowingNewsType.CREATOR_RANKING) creatorNickname else title
return context.getString(messageResId, subject, rankText)
}
class HomeFollowingNewsAdapter(
private val onClickItem: (HomeFollowingNewsUiItem) -> Unit = {}
) : RecyclerView.Adapter<HomeFollowingNewsAdapter.NewsViewHolder>() {
@@ -73,39 +107,12 @@ class HomeFollowingNewsAdapter(
override fun bind(item: HomeFollowingNewsUiItem) {
val ranking = item as HomeFollowingNewsUiItem.Ranking
val context = view.context
val rankText = "${ranking.rank}"
val message = ranking.toRankingMessage(rankText)
val feedItem = FeedItem.Rank(
feedId = ranking.newsId,
imageUrl = ranking.imageUrl.orEmpty(),
rankText = ranking.rank.toString(),
message = message,
highlightRanges = listOf(
FeedRankHighlight(
start = message.indexOf(rankText),
endExclusive = message.indexOf(rankText) + rankText.length
)
)
)
val feedItem = buildHomeFollowingRankingFeedItem(view.context, ranking)
view.setFeedSize(feedSize(FeedVariant.Rank, parent))
view.bind(feedItem)
bindImage(view.imageView(), feedItem.imageUrl)
view.setOnFeedClick { onClickItem(ranking) }
}
private fun HomeFollowingNewsUiItem.Ranking.toRankingMessage(rankText: String): String {
val messageResId = when (type) {
FollowingNewsType.CREATOR_RANKING -> R.string.screen_home_following_creator_ranking_news_message
FollowingNewsType.CONTENT_RANKING -> R.string.screen_home_following_content_ranking_news_message
FollowingNewsType.COMMUNITY_POST,
FollowingNewsType.AUDIO_CONTENT,
FollowingNewsType.PHOTO_CONTENT -> R.string.screen_home_following_content_ranking_news_message
}
val subject = if (type == FollowingNewsType.CREATOR_RANKING) creatorNickname else title
val context = view.context
return context.getString(messageResId, subject, rankText)
}
}
class CommunityViewHolder(
@@ -118,7 +125,7 @@ class HomeFollowingNewsAdapter(
val community = item as HomeFollowingNewsUiItem.Community
val feedItem = FeedItem.Community(
feedId = community.newsId,
creatorId = community.targetId.toString(),
creatorId = "",
creatorName = community.creatorNickname,
creatorImageUrl = community.creatorProfileImageUrl,
postId = community.postId.toString(),
@@ -149,7 +156,7 @@ class HomeFollowingNewsAdapter(
val content = item as HomeFollowingNewsUiItem.Content
val feedItem = FeedItem.Content(
feedId = content.newsId,
creatorId = content.targetId.toString(),
creatorId = "",
creatorName = content.creatorNickname,
creatorImageUrl = content.creatorProfileImageUrl,
contentId = content.contentId.toString(),
@@ -184,6 +191,7 @@ class HomeFollowingNewsAdapter(
onImageLoaded: ((Int, Int) -> Unit)? = null
) {
if (url.isBlank()) {
imageView.dispose()
imageView.setImageDrawable(null)
} else {
imageView.loadUrl(url) {

View File

@@ -1,126 +0,0 @@
package kr.co.vividnext.sodalive.v2.widget.feed
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kotlin.math.roundToInt
class FeedAdapter(
private val widthMode: FeedWidthMode = FeedWidthMode.FigmaFixed,
private val horizontalItemDecorationDp: Int = 0,
private val onClickItem: (FeedItem) -> Unit,
private val onBindImages: (FeedImageViews, FeedItem) -> Unit = { _, _ -> }
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val items = mutableListOf<FeedItem>()
override fun getItemViewType(position: Int): Int = when (items[position].variant) {
FeedVariant.Rank -> VIEW_TYPE_RANK
FeedVariant.Live -> VIEW_TYPE_LIVE
FeedVariant.Content -> VIEW_TYPE_CONTENT
FeedVariant.Community -> VIEW_TYPE_COMMUNITY
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
VIEW_TYPE_RANK -> RankViewHolder(inflater.inflate(R.layout.view_feed_rank, parent, false) as FeedRankView, parent)
VIEW_TYPE_LIVE -> LiveViewHolder(inflater.inflate(R.layout.view_feed_live, parent, false) as FeedLiveView, parent)
VIEW_TYPE_CONTENT -> ContentViewHolder(
inflater.inflate(R.layout.view_feed_content, parent, false) as FeedContentView,
parent
)
VIEW_TYPE_COMMUNITY -> CommunityViewHolder(
inflater.inflate(R.layout.view_feed_community, parent, false) as FeedCommunityView,
parent
)
else -> error("Unknown viewType: $viewType")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = items[position]) {
is FeedItem.Rank -> (holder as RankViewHolder).bind(item)
is FeedItem.Live -> (holder as LiveViewHolder).bind(item)
is FeedItem.Content -> (holder as ContentViewHolder).bind(item)
is FeedItem.Community -> (holder as CommunityViewHolder).bind(item)
}
}
override fun getItemCount(): Int = items.size
fun submitItems(items: List<FeedItem>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
private inner class RankViewHolder(
private val view: FeedRankView,
private val parent: ViewGroup
) : RecyclerView.ViewHolder(view) {
fun bind(item: FeedItem.Rank) {
view.setFeedSize(calculateSize(item.variant, parent))
view.bind(item)
view.setOnFeedClick(onClickItem)
onBindImages(FeedImageViews(primary = view.imageView()), item)
}
}
private inner class LiveViewHolder(
private val view: FeedLiveView,
private val parent: ViewGroup
) : RecyclerView.ViewHolder(view) {
fun bind(item: FeedItem.Live) {
view.setFeedSize(calculateSize(item.variant, parent))
view.bind(item)
view.setOnFeedClick(onClickItem)
onBindImages(FeedImageViews(profile = view.profileImageView()), item)
}
}
private inner class ContentViewHolder(
private val view: FeedContentView,
private val parent: ViewGroup
) : RecyclerView.ViewHolder(view) {
fun bind(item: FeedItem.Content) {
view.setFeedSize(calculateSize(item.variant, parent))
view.bind(item)
view.setOnFeedClick(onClickItem)
onBindImages(FeedImageViews(primary = view.contentImageView(), profile = view.profileImageView()), item)
}
}
private inner class CommunityViewHolder(
private val view: FeedCommunityView,
private val parent: ViewGroup
) : RecyclerView.ViewHolder(view) {
fun bind(item: FeedItem.Community) {
view.setFeedSize(calculateSize(item.variant, parent))
view.bind(item)
view.setOnFeedClick(onClickItem)
onBindImages(FeedImageViews(primary = view.communityImageView(), profile = view.profileImageView()), item)
}
}
private fun calculateSize(variant: FeedVariant, parent: ViewGroup): FeedSize {
val parentWidthPx = parent.width.takeIf { it > 0 } ?: parent.resources.displayMetrics.widthPixels
val availableWidthPx = parentWidthPx - parent.paddingLeft - parent.paddingRight
val availableWidthDp = (availableWidthPx / parent.resources.displayMetrics.density).roundToInt()
return FeedSize.from(variant, widthMode, availableWidthDp, horizontalItemDecorationDp)
}
companion object {
private const val VIEW_TYPE_RANK = 1
private const val VIEW_TYPE_LIVE = 2
private const val VIEW_TYPE_CONTENT = 3
private const val VIEW_TYPE_COMMUNITY = 4
}
}
data class FeedImageViews(
val primary: ImageView? = null,
val profile: ImageView? = null
)

View File

@@ -365,7 +365,7 @@
android:orientation="vertical"
android:paddingHorizontal="@dimen/spacing_14"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_home_following_news_content" />
tools:listitem="@layout/view_feed_content" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -1,118 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_feed_card"
android:orientation="vertical"
android:padding="@dimen/spacing_14">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_home_following_news_creator_profile"
android:layout_width="42dp"
android:layout_height="42dp"
android:background="@drawable/bg_round_corner_999_263238"
android:contentDescription="@null"
android:scaleType="centerCrop"
tools:src="@drawable/ic_placeholder_profile" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_8"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tv_home_following_news_creator_nickname"
style="@style/Typography.Body5"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/white"
tools:text="크리에이터 이름" />
<TextView
android:id="@+id/tv_home_following_news_created_at"
style="@style/Typography.Body6"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/gray_500"
tools:text="2분 전" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/tv_home_following_news_title"
style="@style/Typography.Heading4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_14"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/white"
tools:text="콘텐츠 이름" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_8"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_home_following_news_label"
style="@style/Typography.Caption3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_feed_category_tag"
android:includeFontPadding="false"
android:paddingHorizontal="@dimen/spacing_4"
android:paddingVertical="2dp"
android:textColor="@color/gray_100"
tools:text="오디오" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_12"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_home_following_news_content"
style="@style/Typography.Body3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="4"
android:textColor="@color/white"
tools:text="크리에이터가 남긴 최근 소식이 노출됩니다. 긴 내용은 말줄임 처리합니다." />
<ImageView
android:id="@+id/iv_home_following_news_thumbnail"
android:layout_width="92dp"
android:layout_height="92dp"
android:layout_marginStart="@dimen/spacing_8"
android:background="@drawable/bg_feed_community_image"
android:contentDescription="@null"
android:scaleType="centerCrop"
tools:src="@drawable/ic_launcher_background" />
</LinearLayout>
</LinearLayout>

View File

@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingVertical="@dimen/spacing_8">
<TextView
android:id="@+id/tv_home_following_news_rank"
style="@style/Typography.Heading3"
android:layout_width="32dp"
android:layout_height="wrap_content"
android:gravity="center"
android:includeFontPadding="false"
android:textColor="@color/green_400"
tools:text="1" />
<TextView
android:id="@+id/tv_home_following_news_rank_title"
style="@style/Typography.Body4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_8"
android:layout_weight="1"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/white"
tools:text="최근 소식 제목" />
<TextView
android:id="@+id/tv_home_following_news_rank_count"
style="@style/Typography.Body6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_8"
android:includeFontPadding="false"
android:textColor="@color/gray_500"
tools:text="999" />
</LinearLayout>

View File

@@ -309,17 +309,15 @@
<string name="home_recommendation_section_genre_creator_suffix">\u0020creators</string>
<string name="home_recommendation_section_cheer_creators">Creators with recent cheers</string>
<string name="home_recommendation_section_popular_community_posts">Popular community</string>
<string name="screen_home_following_creators_title">Following creators</string>
<string name="screen_home_following_on_air_title">On Air</string>
<string name="screen_home_following_recent_chats_title">Recent chats</string>
<string name="screen_home_following_monthly_schedules_title">Monthly schedule</string>
<string name="screen_home_following_recent_news_title">Recent news</string>
<string name="screen_home_following_on_air">On Air</string>
<string name="screen_home_following_photo_content">Photo</string>
<string name="screen_home_following_ranking_rank_format">No. %1$d</string>
<string name="screen_home_following_creator_ranking_news_message">%1$s reached %2$s in this week\'s ranking!</string>
<string name="screen_home_following_content_ranking_news_message">%1$s reached %2$s in this week\'s ranking!</string>
<string name="screen_home_following_empty">No following updates yet.\nFollow creators you are interested in.</string>
<string name="screen_home_following_error">Could not load following news.</string>
<string name="home_recommendation_activity_live">Live</string>
<string name="home_recommendation_activity_audio">Audio</string>
<string name="home_recommendation_activity_community">Community</string>

View File

@@ -309,17 +309,15 @@
<string name="home_recommendation_section_genre_creator_suffix">のクリエイター</string>
<string name="home_recommendation_section_cheer_creators">最近応援が多いクリエイター</string>
<string name="home_recommendation_section_popular_community_posts">人気コミュニティ</string>
<string name="screen_home_following_creators_title">フォロー中のクリエイター</string>
<string name="screen_home_following_on_air_title">On Air</string>
<string name="screen_home_following_recent_chats_title">最近のチャット</string>
<string name="screen_home_following_monthly_schedules_title">今月のスケジュール</string>
<string name="screen_home_following_recent_news_title">最近のお知らせ</string>
<string name="screen_home_following_on_air">On Air</string>
<string name="screen_home_following_photo_content">グラビア</string>
<string name="screen_home_following_ranking_rank_format">%1$d位</string>
<string name="screen_home_following_creator_ranking_news_message">%1$sさんが今週のランキング%2$sに入りました</string>
<string name="screen_home_following_content_ranking_news_message">%1$sが今週のランキング%2$sに入りました</string>
<string name="screen_home_following_empty">フォロー中のお知らせはまだありません。\n気になるクリエイターをフォローしてみましょう。</string>
<string name="screen_home_following_error">フォロー中のお知らせを読み込めませんでした。</string>
<string name="home_recommendation_activity_live">ライブ</string>
<string name="home_recommendation_activity_audio">オーディオ</string>
<string name="home_recommendation_activity_community">コミュニティ</string>

View File

@@ -308,17 +308,15 @@
<string name="home_recommendation_section_genre_creator_suffix">\u0020크리에이터</string>
<string name="home_recommendation_section_cheer_creators">최근 응원이 많은 크리에이터</string>
<string name="home_recommendation_section_popular_community_posts">인기 커뮤니티</string>
<string name="screen_home_following_creators_title">팔로잉 크리에이터</string>
<string name="screen_home_following_on_air_title">On Air</string>
<string name="screen_home_following_recent_chats_title">최근 대화</string>
<string name="screen_home_following_monthly_schedules_title">이달의 스케줄</string>
<string name="screen_home_following_recent_news_title">최근 소식</string>
<string name="screen_home_following_on_air">On Air</string>
<string name="screen_home_following_photo_content">화보</string>
<string name="screen_home_following_ranking_rank_format">%1$d위</string>
<string name="screen_home_following_creator_ranking_news_message">%1$s님이 이번 주 랭킹 %2$s에 올랐어요!</string>
<string name="screen_home_following_content_ranking_news_message">%1$s 콘텐츠가 이번 주 랭킹 %2$s에 올랐어요!</string>
<string name="screen_home_following_empty">아직 팔로잉 소식이 없어요.\n관심 있는 크리에이터를 팔로우해 보세요.</string>
<string name="screen_home_following_error">팔로잉 소식을 불러오지 못했습니다.</string>
<string name="home_recommendation_activity_live">라이브</string>
<string name="home_recommendation_activity_audio">오디오</string>
<string name="home_recommendation_activity_community">커뮤니티</string>

View File

@@ -0,0 +1,42 @@
package kr.co.vividnext.sodalive.v2.main.home
import android.app.Application
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.widget.ImageView
import coil.load
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
import kr.co.vividnext.sodalive.v2.main.home.ui.loadHomeCreatorProfileImage
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class)
class HomeCreatorProfileImageLoaderTest {
@Before
fun setUp() {
if (!ImageLoaderProvider.isInitialized) {
ImageLoaderProvider.init(RuntimeEnvironment.getApplication())
}
}
@Test
fun `blank 프로필 이미지는 이전 Coil 요청을 해제하고 drawable을 비운다`() {
val imageView = ImageView(RuntimeEnvironment.getApplication())
val disposable = imageView.load(ColorDrawable(Color.RED), ImageLoaderProvider.imageLoader)
assertFalse(disposable.isDisposed)
imageView.loadHomeCreatorProfileImage(" ")
assertTrue(disposable.isDisposed)
assertNull(imageView.drawable)
}
}

View File

@@ -8,7 +8,7 @@ import java.io.File
class HomeFollowingFragmentSourceTest {
@Test
fun `home layout exposes following content and section recycler ids`() {
fun ` layout은 팔로잉 content section recycler id를 노출한다`() {
val layout = homeMainLayoutSource()
assertTrue(layout.contains("@+id/nsv_home_following_content"))
@@ -20,7 +20,7 @@ class HomeFollowingFragmentSourceTest {
}
@Test
fun `home main fragment injects following viewmodel and wires adapters`() {
fun ` main fragment는 팔로잉 viewmodel과 adapter를 연결한다`() {
val source = homeMainFragmentSource()
assertTrue(source.contains("HomeFollowingViewModel"))
@@ -38,7 +38,7 @@ class HomeFollowingFragmentSourceTest {
}
@Test
fun `following tab branch shows following content and hides other home surfaces`() {
fun `팔로잉 탭 분기는 팔로잉 content만 표시한다`() {
val source = homeMainFragmentSource()
val branch = source.substringAfter("private fun showHomeTab(index: Int)")
.substringAfter("HOME_TAB_FOLLOWING ->")
@@ -50,7 +50,7 @@ class HomeFollowingFragmentSourceTest {
}
@Test
fun `following tab loads following content only once`() {
fun `팔로잉 탭은 팔로잉 content를 최초 한 번만 로드한다`() {
val source = homeMainFragmentSource()
assertTrue(source.contains("private var hasLoadedFollowing = false"))
@@ -60,7 +60,7 @@ class HomeFollowingFragmentSourceTest {
}
@Test
fun `following section chevrons call callback without starting activity`() {
fun `팔로잉 section chevron은 이동 없이 callback만 호출한다`() {
val source = homeMainFragmentSource()
val callback = source.substringAfter("private fun onFollowingSectionMoreClick")
.substringBefore("\n private fun")
@@ -77,7 +77,7 @@ class HomeFollowingFragmentSourceTest {
}
@Test
fun `following state binding clears or binds each following section`() {
fun `팔로잉 상태 binding은 section을 비우거나 바인딩한다`() {
val source = homeMainFragmentSource()
assertTrue(source.contains("followingStateLiveData.observe(viewLifecycleOwner)"))
@@ -85,7 +85,10 @@ class HomeFollowingFragmentSourceTest {
assertTrue(source.contains("bindHomeFollowingContent(state)"))
assertTrue(source.contains("HomeFollowingUiState.Empty ->"))
assertTrue(source.contains("bindHomeFollowingEmpty(showEmptyMessage = true)"))
assertTrue(source.contains("HomeFollowingUiState.LoginRequired -> bindHomeFollowingEmpty(showEmptyMessage = false)"))
val loginRequiredSource = source.substringAfter("HomeFollowingUiState.LoginRequired ->")
.substringBefore("is HomeFollowingUiState.Error ->")
assertTrue(loginRequiredSource.contains("bindHomeFollowingEmpty(showEmptyMessage = false)"))
assertTrue(loginRequiredSource.contains("finishHomePullRefresh(HOME_TAB_FOLLOWING)"))
assertTrue(source.contains("is HomeFollowingUiState.Error ->"))
assertTrue(source.contains("if (shouldPreserveHomeContentOnPullRefreshError(HOME_TAB_FOLLOWING)) return@observe"))
assertTrue(source.contains("bindHomeFollowingEmpty(showEmptyMessage = false)"))
@@ -102,7 +105,7 @@ class HomeFollowingFragmentSourceTest {
}
@Test
fun `following empty state shows centered reusable empty string only for all empty content`() {
fun `팔로잉 empty 상태는 다국어 빈 문구와 상태 분기를 유지한다`() {
val layout = homeMainLayoutSource()
val emptyText = layout.substringAfter("@+id/tv_home_following_empty")
.substringBefore("@+id/ll_home_following_creators_section")
@@ -112,12 +115,7 @@ class HomeFollowingFragmentSourceTest {
val stringsJa = projectFile("app/src/main/res/values-ja/strings.xml").readText()
assertTrue(layout.contains("@+id/tv_home_following_empty"))
assertTrue(emptyText.contains("android:layout_width=\"match_parent\""))
assertTrue(emptyText.contains("android:layout_height=\"match_parent\""))
assertTrue(emptyText.contains("android:gravity=\"center\""))
assertTrue(emptyText.contains("android:textAlignment=\"center\""))
assertTrue(emptyText.contains("android:text=\"@string/screen_home_following_empty\""))
assertTrue(emptyText.contains("android:visibility=\"gone\""))
assertTrue(
strings.contains(
@@ -138,8 +136,6 @@ class HomeFollowingFragmentSourceTest {
)
)
assertTrue(source.contains("binding.tvHomeFollowingEmpty.visibility = View.GONE"))
assertTrue(source.contains("binding.tvHomeFollowingEmpty.visibility = if (showEmptyMessage) View.VISIBLE else View.GONE"))
assertTrue(source.contains("HomeFollowingUiState.Empty ->"))
assertTrue(source.contains("bindHomeFollowingEmpty(showEmptyMessage = true)"))
assertTrue(source.contains("is HomeFollowingUiState.Error ->"))
@@ -147,7 +143,7 @@ class HomeFollowingFragmentSourceTest {
}
@Test
fun `following adapters bind figma required item fields`() {
fun `팔로잉 adapter는 필수 item field를 바인딩한다`() {
val liveAdapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingLiveAdapter.kt"
).readText()
@@ -183,7 +179,7 @@ class HomeFollowingFragmentSourceTest {
}
@Test
fun `following recent news hides chevron and reuses feed widgets by item type`() {
fun `팔로잉 최근 소식은 chevron을 숨기고 feed widget을 재사용한다`() {
val layout = homeMainLayoutSource()
val recentNewsSection = layout.substringAfter("@+id/ll_home_following_recent_news_section")
.substringBefore("</LinearLayout>")
@@ -215,10 +211,13 @@ class HomeFollowingFragmentSourceTest {
assertTrue(adapter.contains("FeedItem.Content"))
assertFalse(adapter.contains("R.layout.item_home_following_news_content"))
assertFalse(adapter.contains("R.layout.item_home_following_news_rank"))
assertTrue(recentNewsSection.contains("tools:listitem=\"@layout/view_feed_content\""))
assertTrue(projectFileOrNull("app/src/main/res/layout/item_home_following_news_content.xml") == null)
assertTrue(projectFileOrNull("app/src/main/res/layout/item_home_following_news_rank.xml") == null)
}
@Test
fun `following ranking news builds sentence message and highlights rank text`() {
fun `팔로잉 ranking 소식은 문장 message와 순위 강조를 만든다`() {
val adapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt"
).readText()
@@ -229,14 +228,15 @@ class HomeFollowingFragmentSourceTest {
assertTrue(adapter.contains("context.getString("))
assertTrue(adapter.contains("highlightRanges = listOf("))
assertTrue(adapter.contains("FeedRankHighlight("))
assertTrue(adapter.contains("start = message.indexOf(rankText)"))
assertTrue(adapter.contains("endExclusive = message.indexOf(rankText) + rankText.length"))
assertTrue(adapter.contains("start = message.lastIndexOf(rankText)"))
assertTrue(adapter.contains("endExclusive = message.lastIndexOf(rankText) + rankText.length"))
assertTrue(adapter.contains("R.string.screen_home_following_ranking_rank_format"))
assertFalse(adapter.contains("message = ranking.body.ifBlank { ranking.title }"))
assertFalse(adapter.contains("highlightRanges = emptyList()"))
}
@Test
fun `following recent news models use nested payload response contract`() {
fun `팔로잉 최근 소식 model nested payload 응답 계약을 사용한다`() {
val models = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingModels.kt"
).readText()
@@ -266,7 +266,7 @@ class HomeFollowingFragmentSourceTest {
}
@Test
fun `following recent news adapter binds nested payload ui variants`() {
fun `팔로잉 최근 소식 adapter nested payload UI variant를 바인딩한다`() {
val adapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt"
).readText()
@@ -289,20 +289,34 @@ class HomeFollowingFragmentSourceTest {
}
@Test
fun `following creators section has no header and uses figma simple profile size`() {
fun `팔로잉 최근 소식 feed model은 미제공 creator id를 합성하지 않는다`() {
val adapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt"
).readText()
assertFalse(adapter.contains("creatorId = community.targetId.toString()"))
assertFalse(adapter.contains("creatorId = content.targetId.toString()"))
assertTrue(adapter.contains("creatorId = \"\""))
assertTrue(adapter.contains("postId = community.postId.toString()"))
assertTrue(adapter.contains("contentId = content.contentId.toString()"))
assertTrue(adapter.contains("view.setOnFeedClick { onClickItem(community) }"))
assertTrue(adapter.contains("view.setOnFeedClick { onClickItem(content) }"))
}
@Test
fun `팔로잉 크리에이터 섹션은 header 없이 item layout을 사용한다`() {
val layout = homeMainLayoutSource()
val section = layout.substringAfter("@+id/ll_home_following_creators_section")
.substringBefore("@+id/ll_home_following_on_air_section")
val itemLayout = projectFile("app/src/main/res/layout/item_home_following_creator.xml").readText()
assertFalse(section.contains("view_home_following_creators_title"))
assertTrue(itemLayout.contains("android:layout_width=\"75dp\""))
assertTrue(itemLayout.contains("android:layout_height=\"75dp\""))
assertTrue(itemLayout.contains("@style/Typography.Body5"))
assertTrue(itemLayout.contains("@+id/iv_home_following_creator_profile"))
assertTrue(itemLayout.contains("@+id/tv_home_following_creator_nickname"))
}
@Test
fun `following creators list appends all button as last item`() {
fun `팔로잉 크리에이터 목록은 전체 item을 마지막에 추가하고 이동을 연결한다`() {
val adapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingCreatorAdapter.kt"
).readText()
@@ -324,48 +338,36 @@ class HomeFollowingFragmentSourceTest {
assertTrue(fragment.contains("onClickAll = { openFollowingCreatorAll() }"))
assertTrue(fragment.contains("private fun openFollowingCreatorAll()"))
assertTrue(fragment.contains("startActivity(Intent(requireContext(), FollowingCreatorActivity::class.java))"))
assertTrue(allLayout.contains("android:layout_width=\"wrap_content\""))
assertTrue(allLayout.contains("android:layout_height=\"wrap_content\""))
assertTrue(allLayout.contains("android:layout_height=\"75dp\""))
assertTrue(allLayout.contains("android:paddingHorizontal=\"16dp\""))
assertTrue(allLayout.contains("android:textColor=\"@color/soda_400\""))
assertTrue(allLayout.contains("android:visibility=\"invisible\""))
assertTrue(allLayout.contains("@string/screen_home_theme_all"))
}
@Test
fun `following recent chats section is horizontal box list`() {
fun `팔로잉 최근 대화 섹션은 RecyclerView와 adapter를 연결한다`() {
val layout = homeMainLayoutSource()
val section = layout.substringAfter("@+id/rv_home_following_recent_chats")
.substringBefore("@+id/ll_home_following_monthly_schedules_section")
val fragment = homeMainFragmentSource()
assertTrue(section.contains("android:orientation=\"horizontal\""))
assertTrue(layout.contains("@+id/rv_home_following_recent_chats"))
assertTrue(fragment.contains("rvHomeFollowingRecentChats.apply"))
assertTrue(fragment.contains("LinearLayoutManager.HORIZONTAL"))
}
@Test
fun `following recent chat item matches figma box fields`() {
fun `팔로잉 최근 대화 item은 필수 field와 클릭 계약을 바인딩한다`() {
val adapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingChatAdapter.kt"
).readText()
val itemLayout = projectFile("app/src/main/res/layout/item_home_following_chat.xml").readText()
assertTrue(itemLayout.contains("android:layout_width=\"284dp\""))
assertTrue(itemLayout.contains("android:layout_width=\"62dp\""))
assertTrue(itemLayout.contains("android:layout_height=\"62dp\""))
assertTrue(itemLayout.contains("@+id/iv_home_following_chat_creator_profile"))
assertTrue(itemLayout.contains("@+id/tv_home_following_chat_creator_nickname"))
assertTrue(itemLayout.contains("@+id/tv_home_following_chat_message"))
assertTrue(itemLayout.contains("@+id/tv_home_following_chat_time"))
assertTrue(itemLayout.contains("@+id/tv_home_following_chat_direct_badge"))
assertTrue(itemLayout.contains("@drawable/bg_chat_direct_badge"))
assertTrue(itemLayout.contains("@string/screen_chat_direct_badge"))
val messageText = itemLayout.substringAfter("@+id/tv_home_following_chat_message")
.substringBefore("@+id/tv_home_following_chat_creator_nickname")
assertTrue(messageText.contains("android:maxLines=\"1\""))
assertTrue(messageText.contains("android:ellipsize=\"end\""))
assertFalse(adapter.contains("ViewGroup.LayoutParams.MATCH_PARENT"))
assertTrue(adapter.contains("recyclerItemLayoutParams(parent)"))
assertTrue(adapter.contains("directBadgeText.visibility = if (item.showDirectBadge)"))
assertTrue(adapter.contains("formatChatRoomLastMessageTime(itemView.context, item.lastMessageAt)"))
assertTrue(adapter.contains("itemView.setOnClickListener { onClickItem(item) }"))
}
private fun homeMainFragmentSource(): String {

View File

@@ -1,6 +1,5 @@
package kr.co.vividnext.sodalive.v2.main.home
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomListItemResponse
@@ -75,16 +74,6 @@ class HomeFollowingMapperTest {
assertEquals(listOf("second", "first"), state.monthlySchedules.items.map { it.scheduleId })
}
@Test
fun `PHOTO_CONTENT는 photo content label로 매핑된다`() {
val state = response(
recentNews = listOf(photoContentNews())
).toUiState(formatter) as HomeFollowingUiState.Content
val item = state.recentNews.items.single() as HomeFollowingNewsUiItem.Content
assertEquals(R.string.screen_home_following_photo_content, item.labelResId)
}
@Test
fun `recentNews nested payload는 type별 UI item으로 매핑된다`() {
val state = response(
@@ -121,7 +110,6 @@ class HomeFollowingMapperTest {
val photoContent = state.recentNews.items[3] as HomeFollowingNewsUiItem.Content
assertEquals(FollowingNewsType.PHOTO_CONTENT, photoContent.type)
assertEquals(3002L, photoContent.contentId)
assertEquals(R.string.screen_home_following_photo_content, photoContent.labelResId)
val community = state.recentNews.items[4] as HomeFollowingNewsUiItem.Community
assertEquals(4001L, community.postId)

View File

@@ -0,0 +1,138 @@
package kr.co.vividnext.sodalive.v2.main.home
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.test.core.app.ApplicationProvider
import coil.load
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingNewsType
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingNewsUiItem
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeFollowingNewsAdapter
import kr.co.vividnext.sodalive.v2.main.home.ui.buildHomeFollowingRankingFeedItem
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import java.util.Locale
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class)
class HomeFollowingNewsAdapterTest {
@Before
fun setUp() {
if (!ImageLoaderProvider.isInitialized) {
ImageLoaderProvider.init(RuntimeEnvironment.getApplication())
}
}
@Test
fun `ranking 문장은 locale별 순위 단위를 사용하고 같은 문자열을 강조한다`() {
assertRankingText(locale = Locale.KOREAN, expectedRankText = "7위")
assertRankingText(locale = Locale.ENGLISH, expectedRankText = "No. 7")
assertRankingText(locale = Locale.JAPANESE, expectedRankText = "7位")
}
@Test
fun `ranking 제목에 같은 순위 문자열이 있어도 삽입된 순위 문자열을 강조한다`() {
val feedItem = buildHomeFollowingRankingFeedItem(
localizedContext(Locale.KOREAN),
ranking(creatorNickname = "7위 크리에이터")
)
val highlight = feedItem.highlightRanges.single()
assertEquals(feedItem.message.lastIndexOf("7위"), highlight.start)
assertEquals("7위", feedItem.message.substring(highlight.start, highlight.endExclusive))
}
@Test
fun `nullable content 이미지는 재사용된 ImageView의 이전 Coil 요청을 해제한다`() {
val adapter = HomeFollowingNewsAdapter()
adapter.submitItems(listOf(content(contentImageUrl = null)))
val parent = FrameLayout(RuntimeEnvironment.getApplication()).apply {
layoutParams = ViewGroup.LayoutParams(360, ViewGroup.LayoutParams.WRAP_CONTENT)
}
val holder = adapter.onCreateViewHolder(parent, adapter.getItemViewType(0))
val imageView = holder.itemView.findViewById<ImageView>(R.id.iv_feed_content_image)
val disposable = imageView.load(ColorDrawable(Color.BLUE), ImageLoaderProvider.imageLoader)
assertFalse(disposable.isDisposed)
adapter.onBindViewHolder(holder, 0)
assertTrue(disposable.isDisposed)
assertNull(imageView.drawable)
}
@Test
fun `PHOTO_CONTENT는 Feed photo category label로 표시된다`() {
val adapter = HomeFollowingNewsAdapter()
adapter.submitItems(listOf(content(type = FollowingNewsType.PHOTO_CONTENT)))
val parent = FrameLayout(RuntimeEnvironment.getApplication()).apply {
layoutParams = ViewGroup.LayoutParams(360, ViewGroup.LayoutParams.WRAP_CONTENT)
}
val holder = adapter.onCreateViewHolder(parent, adapter.getItemViewType(0))
adapter.onBindViewHolder(holder, 0)
val categoryText = holder.itemView.findViewById<TextView>(R.id.tv_feed_content_category)
assertEquals(parent.context.getString(R.string.screen_home_following_photo_content), categoryText.text.toString())
}
private fun assertRankingText(locale: Locale, expectedRankText: String) {
val feedItem = buildHomeFollowingRankingFeedItem(localizedContext(locale), ranking())
assertEquals("7", feedItem.rankText)
assertFalse(feedItem.message.contains("7위") && expectedRankText != "7위")
val highlight = feedItem.highlightRanges.single()
assertEquals(expectedRankText, feedItem.message.substring(highlight.start, highlight.endExclusive))
}
private fun localizedContext(locale: Locale): Context {
val context = ApplicationProvider.getApplicationContext<Context>()
val configuration = Configuration(context.resources.configuration)
configuration.setLocale(locale)
return context.createConfigurationContext(configuration)
}
private fun ranking(creatorNickname: String = "creator") = HomeFollowingNewsUiItem.Ranking(
newsId = "news-1",
type = FollowingNewsType.CREATOR_RANKING,
targetId = 1L,
visibleFromAtUtc = "2026-07-30T00:00:00Z",
visibleFromText = "1분 전",
rank = 7,
imageUrl = "https://example.com/rank.png",
title = "ranking title",
creatorNickname = creatorNickname
)
private fun content(
contentImageUrl: String? = "https://example.com/content.png",
type: FollowingNewsType = FollowingNewsType.AUDIO_CONTENT
) = HomeFollowingNewsUiItem.Content(
newsId = "news-content",
type = type,
targetId = 10L,
visibleFromAtUtc = "2026-07-30T00:00:00Z",
visibleFromText = "1분 전",
contentId = 10L,
contentImageUrl = contentImageUrl,
title = "content title",
creatorProfileImageUrl = "https://example.com/profile.png",
creatorNickname = "creator"
)
}

View File

@@ -56,10 +56,16 @@ class HomeFollowingViewModelTest {
@Test
fun `loadFollowing은 loading 후 content 상태를 발행한다`() {
api.enqueueSuccess(response(followingCreators = listOf(creator())))
val states = mutableListOf<HomeFollowingUiState>()
val observer = Observer<HomeFollowingUiState> { states.add(it) }
viewModel.followingStateLiveData.observeForever(observer)
viewModel.loadFollowing()
viewModel.followingStateLiveData.removeObserver(observer)
val state = viewModel.followingStateLiveData.requireValue() as HomeFollowingUiState.Content
assertTrue(states.first() is HomeFollowingUiState.Loading)
assertTrue(states.last() is HomeFollowingUiState.Content)
assertEquals(1L, state.followingCreators.items.single().creatorId)
assertFalse(viewModel.isLoading.requireValue() ?: true)
}

View File

@@ -57,11 +57,11 @@ class HomeMainFragmentLoginGuardSourceTest {
).readText()
val clickSource = source.substringFrom("private fun onFollowingNewsClick(item: HomeFollowingNewsUiItem)")
assertTrue(clickSource.contains("val community = item as? HomeFollowingNewsUiItem.Community ?: return"))
assertTrue(clickSource.contains("val postId = community.postId"))
assertTrue(clickSource.contains("is HomeFollowingNewsUiItem.Community -> {"))
assertTrue(clickSource.contains("val postId = item.postId"))
assertBefore(
clickSource,
"val community = item as? HomeFollowingNewsUiItem.Community ?: return",
"is HomeFollowingNewsUiItem.Community -> {",
"handleCommunityAction"
)
assertBefore(
@@ -75,6 +75,34 @@ class HomeMainFragmentLoginGuardSourceTest {
assertFalse(clickSource.contains("EXTRA_COMMUNITY_POST_ID"))
}
@Test
fun `HomeMainFragment 팔로잉 최근 소식 CREATOR_RANKING은 크리에이터 채널로 이동한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
).readText()
val clickSource = source.substringFrom("private fun onFollowingNewsClick(item: HomeFollowingNewsUiItem)")
assertTrue(clickSource.contains("is HomeFollowingNewsUiItem.Ranking ->"))
assertTrue(clickSource.contains("item.type == FollowingNewsType.CREATOR_RANKING"))
assertTrue(clickSource.contains("handleCreatorAction(CreatorActionCommand.Profile(item.targetId))"))
}
@Test
fun `HomeMainFragment 팔로잉 최근 소식 AUDIO_CONTENT는 오디오 상세로 이동한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
).readText()
val clickSource = source.substringFrom("private fun onFollowingNewsClick(item: HomeFollowingNewsUiItem)")
assertTrue(clickSource.contains("is HomeFollowingNewsUiItem.Content ->"))
assertTrue(clickSource.contains("item.type == FollowingNewsType.AUDIO_CONTENT"))
assertTrue(
clickSource.contains(
"handleContentAction(ContentActionCommand.AudioDetail(audioContentId = item.contentId))"
)
)
}
@Test
fun `HomeMainFragment 팔로잉 탭 선택은 로그인 가드 통과 후 탭을 전환한다`() {
val source = projectFile(
@@ -125,6 +153,34 @@ class HomeMainFragmentLoginGuardSourceTest {
assertTrue(source.contains("private val liveActionCoordinator: LiveActionCoordinator by lazy"))
}
@Test
fun `HomeMainFragment 팔로잉 On Air 아이템은 공통 Live Action으로 입장한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
).readText()
val clickSource = source.substringFrom("private fun onFollowingLiveClick(item: HomeFollowingLiveUiItem)")
assertTrue(clickSource.contains("liveActionCoordinator.enterLiveRoom(item.liveId)"))
assertFalse(clickSource.contains("= Unit"))
}
@Test
fun `HomeMainFragment 팔로잉 스케줄은 type별 공통 Action으로 이동한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
).readText()
val clickSource = source.substringFrom("private fun onFollowingScheduleClick(item: HomeFollowingScheduleUiItem)")
assertTrue(clickSource.contains("if (item.targetId <= 0L) return"))
assertTrue(clickSource.contains("CreatorActivityType.Live -> liveActionCoordinator.enterLiveRoom(item.targetId)"))
assertTrue(clickSource.contains("CreatorActivityType.LiveReplay"))
assertTrue(clickSource.contains("CreatorActivityType.Audio -> handleContentAction("))
assertTrue(clickSource.contains("ContentActionCommand.AudioDetail(audioContentId = item.targetId)"))
assertTrue(clickSource.contains("CreatorActivityType.Community -> handleCommunityAction("))
assertTrue(clickSource.contains("CommunityActionCommand.PostDetail(item.targetId)"))
assertFalse(clickSource.contains("Intent("))
}
@Test
fun `HomeMainFragment 최근 활동 LIVE route는 공통 Live와 Creator Action을 사용한다`() {
val source = projectFile(

View File

@@ -42,27 +42,6 @@ class FeedViewTest {
assertSame(category.parent, time.parent)
}
@Test
fun `content layout matches figma compact upload notice dimensions`() {
val layout = projectFile("app/src/main/res/layout/view_feed_content.xml").readText()
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedContentView.kt"
).readText()
val imageBlock = layout.substringAfter("@+id/iv_feed_content_image")
.substringBefore("@+id/ll_feed_content_info")
val infoBlock = layout.substringAfter("@+id/ll_feed_content_info")
.substringBefore("</kr.co.vividnext.sodalive.v2.widget.feed.FeedContentView>")
val titleBlock = layout.substringAfter("@+id/tv_feed_content_title")
.substringBefore("@+id/ll_feed_content_meta")
assertTrue(layout.contains("android:padding=\"@dimen/spacing_14\""))
assertTrue(imageBlock.contains("android:layout_width=\"88dp\""))
assertTrue(imageBlock.contains("android:layout_height=\"88dp\""))
assertTrue(infoBlock.contains("android:layout_marginStart=\"@dimen/spacing_14\""))
assertTrue(titleBlock.contains("android:layout_marginTop=\"@dimen/spacing_6\""))
assertTrue(source.contains("const val CONTENT_IMAGE_WIDTH_DP = 88"))
}
@Test
fun `content root clipping is implemented in custom view code`() {
val layout = projectFile("app/src/main/res/layout/view_feed_content.xml").readText()

View File

@@ -89,10 +89,10 @@
- 최근 대화 compact card item이다.
- Create: `app/src/main/res/layout/item_home_following_schedule.xml`
- 이달의 스케줄 item이다.
- Create: `app/src/main/res/layout/item_home_following_news_rank.xml`
- ranking news item이다.
- Create: `app/src/main/res/layout/item_home_following_news_content.xml`
- audio/photo content news item이다.
- Reuse: `app/src/main/res/layout/view_feed_rank.xml`, `app/src/main/res/layout/view_feed_content.xml`, `app/src/main/res/layout/view_feed_community.xml`
- 최근 소식은 기존 Feed 위젯 layout을 재사용한다.
- Delete: `app/src/main/res/layout/item_home_following_news_rank.xml`, `app/src/main/res/layout/item_home_following_news_content.xml`
- Task 6.5에서 Feed 위젯 전환 후 obsolete 팔로잉 전용 최근 소식 layout을 제거했다.
- Modify: `app/src/main/res/values/strings.xml`
- 팔로잉 섹션 title, `On Air`, `화보`, empty/error label을 추가한다.
- Modify: `app/src/main/res/values-en/strings.xml`
@@ -131,7 +131,10 @@
- 확인:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeCreatorProfileImageLoader.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailDetailView.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedAdapter.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItem.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedRankView.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedContentView.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomMappers.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/common/CreatorActivityType.kt`
- 작업:
@@ -262,6 +265,57 @@
- Expected: PASS.
- Result: PASS. login-required, empty, content mapping, invalid chat 제외, schedule 순서 유지, `PHOTO_CONTENT` label, null rank filtering, `visibleFromAtUtc` formatter 전달이 검증됐다.
#### Task 2.7: 미사용 팔로잉 string resource 정리
**Goal 실행 `P2-R1`:** 확정 review 항목 `REV-P2-001`에 따라 후속 UI·오류 처리 변경 후 참조되지 않는 팔로잉 문자열 리소스만 제거한다.
- **시작 조건:** `reviews/phase2-following-data-mapper-review.md``REV-P2-001` 확정.
- **완료 증거:** 3개 미사용 key의 참조 0건 재확인 → 3개 locale 선언 제거 → resource merge·Kotlin compile·팔로잉 회귀·diff 검증 PASS.
- **범위 밖:** 사용 중인 `screen_home_following_on_air`, empty·ranking·section title 문자열 변경, 표시 문구 재기획, UI 구조 변경.
- **TDD 예외 사유:** 실행 로직이 아닌 미사용 Android resource 선언 삭제이며, 이를 위한 신규 source test는 저장소 테스트 정책에 맞지 않는다.
- **대체 검증 방법:** `rg`로 참조 0건을 재확인하고 resource merge, Kotlin compile, 기존 팔로잉 회귀 테스트, `git diff --check`를 실행한다.
**Files:**
- Modify: `app/src/main/res/values/strings.xml`
- Modify: `app/src/main/res/values-en/strings.xml`
- Modify: `app/src/main/res/values-ja/strings.xml`
- [x] `screen_home_following_creators_title`, `screen_home_following_on_air_title`, `screen_home_following_error`의 production/test 참조가 0건인지 재확인한다.
- [x] 3개 locale에서 위 3개 key만 제거하고 사용 중인 팔로잉 문자열은 유지한다.
- [x] `./gradlew --no-daemon :app:mergeDebugResources :app:compileDebugKotlin``./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`를 실행한다.
- [x] `git diff --check`를 실행하고 검증 결과를 이 Task 아래에 누적한다.
검증 기록:
- 2026-07-31: `rg -n "screen_home_following_creators_title|screen_home_following_on_air_title|screen_home_following_error" app/src/main/java app/src/main/res/layout app/src/test` 결과 선언 외 참조 0건을 확인했다. 3개 locale `strings.xml`에서 위 3개 key만 제거하고 `screen_home_following_on_air`, empty, ranking, section title 문자열은 유지했다. `./gradlew --no-daemon :app:mergeDebugResources :app:compileDebugKotlin` PASS. 이후 팔로잉 전체 회귀 첫 실행에서 `R.string` id 변경 증분 캐시로 `HomeFollowingMapperTest` 2건이 실패해 `./gradlew --no-daemon --rerun-tasks :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingMapperTest"`로 재빌드했고 PASS. 최종 `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew --no-daemon :app:ktlintCheck`, `git diff --check` PASS.
#### Task 2.8: 미사용 최근 소식 label UI model 정리
**Goal 실행 `P2-R2`:** 확정 review 항목 `REV-P2-002`에 따라 Feed category로 대체된 후 production에서 사용하지 않는 `HomeFollowingNewsUiItem.Content.labelResId`와 전용 mapper를 제거한다.
- **시작 조건:** `reviews/phase2-following-data-mapper-review.md``REV-P2-002` 확정.
- **완료 증거:** production 참조 0건 재확인 → UI model·mapper·fixture의 미사용 label 경로 제거 → Feed `Photo` category의 locale label 회귀·Kotlin compile·ktlint PASS.
- **범위 밖:** `FeedContentCategory` 구조, `screen_home_following_photo_content` 문구, 최근 소식 API/DTO·표시·라우팅 변경.
- **TDD 예외 사유:** 실행 동작 수정이 아닌 미사용 UI model 필드와 mapper 제거이며, 기존 Feed category 회귀로 동작 무변경을 검증한다.
- **대체 검증 방법:** `rg`로 production/test 참조를 대조하고 mapper·adapter focused test, Kotlin compile, ktlint, `git diff --check`를 실행한다.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingUiModels.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingMappers.kt`
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingMapperTest.kt`
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingNewsAdapterTest.kt`
- [x] `rg -n "labelResId|toLabelResId"` 결과로 최근 소식 `Content.labelResId``FollowingNewsType.toLabelResId()`가 production 표시에서 사용되지 않는지 재확인한다.
- [x] `HomeFollowingNewsUiItem.Content.labelResId`, mapper 대입과 전용 `toLabelResId()`만 제거하고 Feed `Audio`/`Photo` category mapping은 유지한다.
- [x] mapper/adapter test fixture에서 제거된 필드를 정리하고 `PHOTO_CONTENT` 표시가 `FeedContentCategory.Photo`와 locale string을 사용하는 회귀를 유지한다.
- [x] mapper·news adapter focused test, 팔로잉 전체 회귀, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`를 실행하고 결과를 누적한다.
검증 기록:
- 2026-07-31: `rg -n -- "labelResId|toLabelResId" app/src/main/java/kr/co/vividnext/sodalive/v2/main/home app/src/test/java/kr/co/vividnext/sodalive/v2/main/home`로 팔로잉 최근 소식 `Content.labelResId``FollowingNewsType.toLabelResId()`의 production 표시 참조가 없음을 재확인했다. `HomeFollowingNewsUiItem.Content.labelResId`, mapper 대입, 전용 `toLabelResId()`를 제거했고, `HomeFollowingNewsAdapterTest``PHOTO_CONTENT`가 실제 `FeedContentCategory.Photo` locale label로 표시되는 회귀를 추가했다. `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingMapperTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingNewsAdapterTest"`, 팔로잉 전체 회귀, `:app:mergeDebugResources`, `:app:compileDebugKotlin`, `:app:ktlintCheck` PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다.
---
### Phase 3: ViewModel 상태와 API 호출 연결
@@ -313,6 +367,29 @@
- 2026-06-25 코드 리뷰: Phase 1~3 범위의 API/DTO/Repository/mapper/ViewModel/DI/string/test 변경을 검토했으며, 현재 코드 기준으로 blocking finding은 발견하지 못했다.
- 2026-06-25 검증: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` 모두 PASS.
#### Task 3.4: ViewModel Loading → 결과 상태 발행 순서 회귀 검증
**Goal 실행 `P3-R1`:** 확정 review 항목 `REV-P3-001`에 따라 `loadFollowing()``Loading`과 최종 상태 발행 순서를 실행 가능한 테스트로 고정한다.
- **시작 조건:** `reviews/phase3-following-viewmodel-review.md``REV-P3-001` 확정.
- **완료 증거:** 상태 이력 assertion 추가 → Phase 3 focused test와 팔로잉 전체 회귀 PASS.
- **범위 밖:** `HomeFollowingViewModel` 상태 구조 변경, scheduler 교체, repository/API 변경.
- **TDD 예외 사유:** production은 이미 `Loading`과 결과 상태를 순서대로 발행하며, 확정 항목은 동작 결함이 아니라 기존 테스트의 assertion 누락이므로 의도적인 production 변조 없이 RED를 만들지 않는다.
- **대체 검증 방법:** observer 상태 이력으로 `Loading → Content`를 직접 assertion하고 기존 error/login-required 테스트와 전체 팔로잉 회귀를 실행한다.
**Files:**
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingViewModelTest.kt`
- [x] observer가 수집한 상태 이력이 `Loading``Content`를 순서대로 포함하도록 테스트 fixture와 assertion을 최소 보완한다.
- [x] production 파일 변경이 없고 기존 error/login-required 테스트가 그대로 유지되는지 diff로 확인한다.
- [x] **GREEN 확인:** `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingViewModelTest"`를 실행한다.
- [x] **회귀 검증:** `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"``git diff --check`를 실행하고 결과를 이 Task 아래에 누적한다.
검증 기록:
- 2026-07-30: `HomeFollowingViewModelTest.loadFollowing은 loading 후 content 상태를 발행한다`에 observer 상태 이력 assertion을 추가해 `Loading → Content` 순서를 고정했다. production 파일은 변경하지 않았다. focused test와 팔로잉 전체 회귀, `git diff --check`가 PASS했다.
---
### Phase 4: 팔로잉 탭 UI surface와 adapter 연결
@@ -401,6 +478,101 @@
- 2026-06-25 Phase 4 코드 리뷰: Figma `24:5682`와 PRD 기준으로 `On Air` 시작 시간, 이달의 스케줄 프로필/타입 label/On Air 상태, 최근 소식 label/title 바인딩 누락을 확인했다. 누락 항목은 `HomeFollowingFragmentSourceTest` RED로 고정한 뒤 adapter/layout 최소 수정으로 보완했다.
- 2026-06-25 Phase 4 재코드 리뷰: 현재 워킹트리 기준 `HomeMainFragment`, 팔로잉 adapter 5개, 팔로잉 layout, `HomeFollowingFragmentSourceTest`를 Figma `24:5682`/PRD와 대조했으며 blocking finding은 발견하지 못했다.
#### Task 4.6: On Air·스케줄 item 상세 이동 연결
**Goal 실행 `P4-R1`:** 확정 review 항목 `REV-P4-001`에 따라 On Air와 이달의 스케줄 item 터치가 기존 공통 도메인 액션으로 실제 이동하도록 한다.
- **시작 조건:** `reviews/phase4-following-ui-routing-review.md``REV-P4-001` 확정과 `FollowingLiveResponse.liveId`, `FollowingScheduleResponse.type/targetId` 계약 확인.
- **완료 증거:** no-op 재현 RED → 기존 Live/Creator/Content/Community Action을 재사용한 GREEN → focused test와 공통 액션 회귀 PASS.
- **범위 밖:** 더보기 chevron 목적지, 최근 소식 `CONTENT_RANKING`·`PHOTO_CONTENT` 이동, 공통 액션 정책 변경.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLoginGuardSourceTest.kt`
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt`
- [x] **RED:** On Air와 schedule type별 공통 액션 호출을 요구하는 테스트를 추가해 현재 `Unit` 구현에서 실패하는지 확인한다.
- [x] **GREEN:** On Air는 `liveActionCoordinator.enterLiveRoom(item.liveId)`를 사용하고, 스케줄은 `CreatorActivityType``targetId`를 기존 최근 활동 route 정책에 맞춰 Live/Content/Community 공통 액션으로 전달한다.
- [x] **방어 검증:** `targetId <= 0L`과 지원하지 않는 route는 공통 액션 호출 전에 무시하고, 직접 `Intent` 또는 legacy extra를 만들지 않는지 검증한다.
- [x] **회귀 검증:** home focused test, `v2.live.action.*`, `v2.content.action.*`, `v2.community.action.*`, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`를 실행한다.
검증 기록:
- 2026-07-30 RED: `HomeMainFragmentLoginGuardSourceTest`에 팔로잉 On Air와 스케줄 type별 공통 Action 호출 assertion을 추가했고, 구현 전 `onFollowingLiveClick()`/`onFollowingScheduleClick()``Unit`이라 focused test가 실패하는 것을 확인했다.
- 2026-07-30 GREEN: On Air는 `liveActionCoordinator.enterLiveRoom(item.liveId)`, 스케줄은 `targetId > 0L`일 때 `CreatorActivityType.Live`는 live action, `LiveReplay`/`Audio``ContentActionCommand.AudioDetail`, `Community``CommunityActionCommand.PostDetail`로 연결했다. 직접 `Intent` 조립은 추가하지 않았다. focused test, 팔로잉 전체 회귀, 공통 action 테스트, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check` PASS.
#### Task 4.7: LoginRequired 당겨서 새로고침 종료 보장
**Goal 실행 `P4-R2`:** 확정 review 항목 `REV-P4-002`에 따라 팔로잉 새로고침 응답이 `LoginRequired`여도 spinner와 refresh 추적 상태를 종료한다.
- **시작 조건:** `reviews/phase4-following-ui-routing-review.md``REV-P4-002` 확정과 기존 `finishHomePullRefresh(HOME_TAB_FOLLOWING)` 정책 확인.
- **완료 증거:** `LoginRequired` 분기의 refresh 종료 누락 RED → 최소 분기 보완 GREEN → 팔로잉·홈 refresh 회귀 PASS.
- **범위 밖:** 로그인 유도 UI/문구/CTA, `ensureV2Access` 정책, ViewModel API 계약 변경.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt`
- [x] **RED:** pull refresh 중 `LoginRequired` 상태가 `finishHomePullRefresh(HOME_TAB_FOLLOWING)`을 호출하지 않는 현재 분기를 재현한다.
- [x] **GREEN:** `LoginRequired`에서 section 숨김 후 팔로잉 refresh를 종료하고, 일반 진입에서는 불필요한 scroll 변경이 없는 최소 분기를 적용한다.
- [x] **GREEN 확인:** 팔로잉 Fragment focused test와 홈 pull-refresh 관련 focused test를 실행한다.
- [x] **회귀 검증:** 팔로잉 전체 회귀, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`를 실행하고 결과를 누적한다.
검증 기록:
- 2026-07-30 RED: `HomeFollowingFragmentSourceTest``LoginRequired` 분기에서 `finishHomePullRefresh(HOME_TAB_FOLLOWING)` 호출을 요구하도록 보강했고, 구현 전 focused test 실패로 누락을 확인했다.
- 2026-07-30 GREEN: `LoginRequired` 분기에서 섹션을 숨긴 뒤 팔로잉 pull-refresh 종료를 호출하도록 최소 보완했다. focused test, 팔로잉 전체 회귀, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check` PASS.
#### Task 4.8: blank 프로필 이미지의 이전 Coil 요청 해제
**Goal 실행 `P4-R3`:** 확정 review 항목 `REV-P4-003`에 따라 재사용된 프로필 `ImageView`에 blank URL을 바인딩할 때 이전 Coil 요청을 해제해 다른 item의 이미지가 뒤늦게 표시되지 않도록 한다.
- **시작 조건:** `reviews/phase4-following-ui-routing-review.md``REV-P4-003` 확정과 PRD의 blank profile image fallback 정책 확인.
- **완료 증거:** 이전 Coil 요청이 남는 재사용 시나리오 RED → blank 분기에서 요청 해제와 drawable 초기화 GREEN → profile loader focused test와 팔로잉 전체 회귀 PASS.
- **범위 밖:** non-blank 이미지 transformation/placeholder 디자인 변경, Coil 전역 설정 변경, adapter 구조 변경.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeCreatorProfileImageLoader.kt`
- Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeCreatorProfileImageLoaderTest.kt`
- Modify: `docs/agent-guides/build-test-style.md`
- [x] **RED:** Robolectric `ImageView`에 Coil 요청을 연결한 뒤 blank URL을 바인딩하고, 기존 요청이 해제되지 않는 현재 동작을 `coil.result` 또는 동등한 요청 상태 assertion으로 재현한다.
- [x] **GREEN:** `loadHomeCreatorProfileImage()`의 blank 분기에서 `coil.dispose()`로 이전 요청을 해제한 뒤 drawable을 비우는 최소 수정만 적용한다.
- [x] **GREEN 확인:** `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeCreatorProfileImageLoaderTest"`를 실행한다.
- [x] **회귀 검증:** 팔로잉 전체 회귀, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`를 실행하고 신규 테스트 단일 실행 예시를 빌드 가이드에 반영한다.
검증 기록:
- 2026-07-30 RED: `HomeCreatorProfileImageLoaderTest`를 추가해 blank profile URL 바인딩 시 기존 Coil `Disposable`이 해제되어야 함을 검증했고, 구현 전 `assertTrue(disposable.isDisposed)` 실패로 현재 요청이 남는 동작을 확인했다.
- 2026-07-30 GREEN: `loadHomeCreatorProfileImage()` blank 분기에서 `dispose()``setImageDrawable(null)`을 호출하도록 최소 수정했다. focused test, 팔로잉 전체 회귀와 신규 프로필 테스트, `:app:mergeDebugResources`, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check` PASS. `build-test-style.md``HomeCreatorProfileImageLoaderTest` 단일 실행 예시를 추가했다.
#### Task 4.9: 팔로잉 공용 source test 이름 규칙 정합성 보완
**Goal 실행 `P4-R4`:** 확정 review 항목 `REV-P4-004`에 따라 `HomeFollowingFragmentSourceTest`에 남은 영문 문장형 테스트명을 검증 의도가 드러나는 한글 문장으로 변경한다.
- **시작 조건:** `reviews/phase4-following-ui-routing-review.md``REV-P4-004` 확정과 `docs/agent-guides/build-test-style.md`의 신규 Kotlin 테스트명 규칙 확인.
- **완료 증거:** 영문 문장형 테스트명 11개 한글화 → assertion·production 코드 무변경 확인 → Fragment focused test·팔로잉 전체 회귀·ktlint·diff 검증 PASS.
- **범위 밖:** 테스트 assertion 재설계, production 코드·resource 변경, 다른 테스트 클래스의 기존 이름 일괄 수정.
- **TDD 예외 사유:** runtime 동작 결함이 아니라 테스트 이름 규칙 정합성 수정이므로 production RED/GREEN을 만들지 않는다.
- **대체 검증 방법:** 대상 파일의 `@Test` 함수명을 `rg`로 확인하고 변경 전후 assertion diff, focused test, 팔로잉 전체 회귀를 대조한다.
**Files:**
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt`
- [x] 완전한 영문 문장으로 남은 backtick 테스트명 11개를 각 검증 의도가 드러나는 한글 문장으로 변경한다.
- [x] 테스트 본문의 assertion과 helper, production 코드·resource가 변경되지 않았는지 diff로 확인한다.
- [x] `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest"`를 실행한다.
- [x] 팔로잉 전체 회귀, `:app:ktlintCheck`, `git diff --check`를 실행하고 결과를 이 Task 아래에 누적한다.
검증 기록:
- 2026-07-31: `HomeFollowingFragmentSourceTest`의 완전한 영문 문장형 backtick 테스트명 11개를 검증 의도가 드러나는 한글 문장으로 변경했다. Task 9.6의 신규 한글 테스트 추가 외 기존 assertion/helper와 production/resource 변경은 포함하지 않았다. `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingNewsAdapterTest"`, 팔로잉 전체 회귀와 `:app:compileDebugKotlin`, `:app:ktlintCheck` PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다.
---
### Phase 5: 통합 검증과 문서 기록
@@ -443,6 +615,8 @@
- 2026-06-25 Figma `24:5682` 디자인 컨텍스트와 스크린샷 기준 정적 대조를 수행했다. 실제 기기/에뮬레이터에서의 수동 화면 확인은 아직 실행하지 않았다.
- 2026-06-25 Phase 5 진행: Figma `24:5682` 스크린샷과 `fragment_v2_main_home.xml`, `HomeMainFragment.kt`를 대조했다. 팔로잉 탭 전용 `nsv_home_following_content`가 title bar/tab bar 아래 별도 scroll surface로 배치되어 있고, 팔로잉 선택 시 추천/랭킹 surface를 숨기는 분기, 섹션 순서, empty/login-required 섹션 숨김, 더보기 chevron no-op callback 연결을 정적으로 확인했다.
- 2026-06-25 Phase 5 진행: 실제 기기 검증을 위해 `adb devices`에서 `2cec640c34017ece` 연결을 확인한 뒤 `./gradlew :app:installDebug`를 실행했으나, 설치 중 디바이스 연결이 해제되어 `device '2cec640c34017ece' not found`로 실패했다. 재확인 시 `adb devices`에 연결된 디바이스가 없어 실제 화면 수동 확인은 blocked 상태로 남긴다.
- 2026-07-31 Task 2.7 완료 후 재확인: `adb devices`는 정상 실행됐지만 `List of devices attached` 아래 연결 기기가 없어 Figma/API 데이터 기반 실기기 수동 확인은 계속 보류한다.
- 2026-07-31 Task 4.9/9.6 완료 중 재확인: `adb devices`에서 `2cec640c34017ece` 연결을 확인했고 `./gradlew --no-daemon :app:installDebug`, `adb shell monkey -p kr.co.vividnext.sodalive.debug -c android.intent.category.LAUNCHER 1` PASS. 다만 `adb exec-out screencap -p`로 저장한 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sodalive-home-after-launch.png`와 5초 대기 후 재촬영한 `sodalive-home-after-launch-2.png`가 모두 검은 화면으로 분석되어 팔로잉 탭 content와 Figma `24:5682` 상세 대조는 완료하지 못했다.
---
@@ -516,6 +690,104 @@
- 2026-06-26 최근 소식 후속 수정: 먼저 `HomeFollowingFragmentSourceTest`에 최근 소식 chevron 제거와 Feed 위젯 재사용 검증을 추가해 RED를 확인했다. 이후 `HomeMainFragment`에서 최근 소식 `showMore = true`와 chevron listener를 제거했고, `HomeFollowingNewsAdapter``COMMUNITY_POST``FeedCommunityView`, ranking은 `FeedRankView`, audio/photo content는 `FeedContentView`를 inflate/bind하도록 변경했다.
- 2026-06-26 최근 소식 후속 검증: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` 모두 PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다.
#### Task 6.5: Feed 위젯 전환 후 obsolete 최근 소식 layout 제거
**Goal 실행 `P6-R1`:** 확정 review 항목 `REV-P6-001`에 따라 runtime에서 사용하지 않는 팔로잉 전용 news layout과 잘못된 preview 참조를 제거한다.
- **시작 조건:** `reviews/phase6-figma-follow-up-review.md``REV-P6-001` 확정과 `HomeFollowingNewsAdapter`의 Feed 위젯 사용 확인.
- **완료 증거:** resource 참조 검색 → preview를 실제 Feed layout으로 교체 → obsolete layout 2개 삭제 → resource merge와 source test PASS.
- **범위 밖:** Feed 위젯 UI 재설계, 최근 소식 DTO/mapper/routing 변경.
**Files:**
- Delete: `app/src/main/res/layout/item_home_following_news_rank.xml`
- Delete: `app/src/main/res/layout/item_home_following_news_content.xml`
- Modify: `app/src/main/res/layout/fragment_v2_main_home.xml`
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt`
- [x] **RED:** `rg`와 source test로 두 obsolete layout이 runtime adapter에서 사용되지 않고 `tools:listitem`만 과거 layout을 가리키는 상태를 고정한다.
- [x] **GREEN:** 최근 소식 preview를 실제 `view_feed_content`로 교체하고 obsolete layout 2개를 삭제한다.
- [x] **GREEN 확인:** `./gradlew --no-daemon :app:mergeDebugResources``HomeFollowingFragmentSourceTest`를 실행한다.
- [x] **회귀 검증:** 팔로잉 전체 회귀, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`를 실행한다.
검증 기록:
- 2026-07-30 RED: `HomeFollowingFragmentSourceTest`가 최근 소식 preview `tools:listitem`과 obsolete layout 파일 제거를 요구하도록 보강했고, 구현 전 과거 layout 참조/파일 존재로 실패했다.
- 2026-07-30 GREEN: `fragment_v2_main_home.xml`의 최근 소식 preview를 `@layout/view_feed_content`로 교체하고 `item_home_following_news_rank.xml`, `item_home_following_news_content.xml`을 삭제했다. `:app:mergeDebugResources`, focused test, 팔로잉 전체 회귀, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check` PASS.
#### Task 6.6: 삭제된 최근 소식 layout의 파일 구조 문서 정합성 보완
**Goal 실행 `P6-R2`:** 확정 review 항목 `REV-P6-002`에 따라 상단 `파일 구조`가 Feed 위젯 전환 후 현재 resource 구성을 정확히 설명하도록 갱신한다.
- **시작 조건:** `reviews/phase6-figma-follow-up-review.md``REV-P6-002` 확정과 Task 6.5의 obsolete layout 삭제 완료.
- **완료 증거:** 상단 파일 구조에서 삭제된 두 layout의 Create 항목 제거 또는 삭제 이력 명시 → 실제 Feed layout 참조와 문서 대조 → 문서 diff 검증 PASS.
- **범위 밖:** 삭제된 layout 복원, Feed 위젯 UI·adapter·DTO·routing 변경.
- **TDD 예외 사유:** 계획 문서의 현재 파일 목록을 바로잡는 문서 전용 수정이므로 실행 가능한 실패 test를 만들지 않는다.
- **대체 검증 방법:** `rg`로 삭제된 layout의 runtime 참조와 상단 파일 구조 문구를 대조하고 `git diff --check`를 실행한다.
**Files:**
- Modify: `docs/20260625_메인_홈_팔로잉_탭/plan-task.md`
- [x] 상단 `파일 구조`에서 `item_home_following_news_rank.xml`, `item_home_following_news_content.xml`을 현재 생성 파일로 오인하지 않도록 정리한다.
- [x] `HomeFollowingNewsAdapter`가 재사용하는 `view_feed_rank`, `view_feed_content`, `view_feed_community`와 Task 6.5 삭제 이력을 문서에서 확인 가능하게 유지한다.
- [x] `rg -n "item_home_following_news_(rank|content)|view_feed_(rank|content|community)" docs/20260625_메인_홈_팔로잉_탭/plan-task.md app/src/main`으로 문서와 runtime 참조를 대조한다.
- [x] `git diff --check`를 실행하고 결과를 이 Task 아래에 누적한다.
검증 기록:
- 2026-07-30: 상단 `파일 구조`의 obsolete 최근 소식 layout 2개를 Create 항목에서 제거하고, runtime Feed layout 3종 재사용 및 Task 6.5 삭제 이력을 명시했다. `rg -n "item_home_following_news_(rank|content)|view_feed_(rank|content|community)" docs/20260625_메인_홈_팔로잉_탭/plan-task.md app/src/main` 대조 결과 runtime adapter는 `view_feed_rank`, `view_feed_content`, `view_feed_community`를 참조하고 삭제 layout은 문서/이력 참조로만 남는 것을 확인했다. `git diff --check` PASS.
#### Task 6.7: 최근 대화 고정 폭 LayoutParams 보존
**Goal 실행 `P6-R3`:** 확정 review 항목 `REV-P6-003`에 따라 최근 대화 item의 XML `284dp` 폭을 런타임에서도 유지하면서 기존 item 간격을 보존한다.
- **시작 조건:** `reviews/phase6-figma-follow-up-review.md``REV-P6-003` 확정.
- **완료 증거:** inflate 직후 `284dp` width를 `WRAP_CONTENT`로 교체하는 경로 제거 → 기존 layout parameter에 end margin만 적용 → resource merge·Kotlin compile·팔로잉 회귀 PASS → 연결 기기 준비 시 Task 5.3에서 폭/간격 수동 확인.
- **범위 밖:** 최근 대화 카드의 Figma 크기 변경, 공통 helper의 전체 호출부 리팩터링, 다른 팔로잉 adapter 수정.
- **TDD 예외 사유:** `docs/agent-guides/code-style.md`가 View width·margin 같은 UI 표현 속성 테스트 추가를 금지하므로 새 자동 UI 크기 테스트를 작성하지 않는다.
- **대체 검증 방법:** `item_home_following_chat.xml`, `HomeFollowingChatAdapter.kt`, `HomeRecyclerItemLayoutParams.kt`를 정적 대조하고 resource merge·Kotlin compile·기존 팔로잉 로직 회귀 및 가능한 경우 실기기 수동 확인을 수행한다.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingChatAdapter.kt`
- Verify: `app/src/main/res/layout/item_home_following_chat.xml`
- [x] `HomeFollowingChatAdapter.onCreateViewHolder()`가 inflate된 root의 기존 width/height를 보존하고 end margin만 추가하도록 최소 수정한다.
- [x] 공통 helper나 다른 adapter를 불필요하게 변경하지 않았는지 diff로 확인한다.
- [x] `./gradlew --no-daemon :app:mergeDebugResources`, `:app:compileDebugKotlin`, 팔로잉 전체 회귀를 실행한다.
- [x] 연결 기기가 있으면 Task 5.3에서 최근 대화 카드 폭과 item 간격을 확인하고, 없으면 미실행 사유를 기록한다.
검증 기록:
- 2026-07-30: `HomeFollowingChatAdapter.onCreateViewHolder()`에서 `recyclerItemLayoutParams(parent)` 사용을 제거하고 inflate된 `RecyclerView.LayoutParams`의 width/height를 유지한 채 `spacing_12` end margin만 적용했다. 공통 helper와 다른 adapter는 변경하지 않았다. focused test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint가 PASS했다. `adb devices`에 연결 기기가 없어 최근 대화 카드 폭/간격 실기기 확인은 Task 5.3 보류 사유와 함께 유지한다.
#### Task 6.8: creator·최근 대화 UI 표현 속성 source test 정합성 보완
**Goal 실행 `P6-R4`:** 확정 review 항목 `REV-P6-004`에 따라 creator·최근 대화의 크기·방향·말줄임 값을 XML 문자열로 고정하는 source assertion을 제거하고 허용된 구조·바인딩 검증만 유지한다.
- **시작 조건:** `reviews/phase6-figma-follow-up-review.md``REV-P6-004` 확정과 `docs/agent-guides/code-style.md` 테스트 관례 확인.
- **완료 증거:** 금지된 XML 크기·orientation·style·maxLines·ellipsize assertion 제거 → production XML/adapter 무변경 → focused test·resource merge·Kotlin compile·ktlint PASS → 실기기 가능 시 Task 5.3에서 표현 속성 대조.
- **범위 밖:** creator/chat production UI 값 변경, adapter 데이터 바인딩, 다른 기존 Feed 테스트 일괄 정리.
- **TDD 예외 사유:** 동작 결함 수정이 아닌 현행 테스트 정책 위반 assertion 제거이며 production은 변경하지 않는다.
- **대체 검증 방법:** Figma와 production XML/adapter는 정적 대조하고, 자동 검증은 section 존재·adapter view type·필수 field 바인딩·click route와 순수 로직으로 한정한다.
**Files:**
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt`
- Verify: `app/src/main/res/layout/item_home_following_creator.xml`
- Verify: `app/src/main/res/layout/item_home_following_chat.xml`
- Verify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingChatAdapter.kt`
- [x] creator test에서 `75dp` width/height와 typography style source assertion을 제거하고 creator header 제거 구조 검증은 유지한다.
- [x] 최근 대화 test에서 XML orientation, `284dp`/`62dp` width/height, `maxLines`/`ellipsize` source assertion을 제거한다.
- [x] Direct badge 노출 결정, 상대 시간 formatter, 필수 field 바인딩과 adapter click 계약 검증은 유지한다.
- [x] `HomeFollowingFragmentSourceTest`, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint, `git diff --check`를 실행하고 실기기 표현 확인 가능 여부를 누적한다.
검증 기록:
- 2026-07-31: creator source test명을 `팔로잉 크리에이터 섹션은 header 없이 item layout을 사용한다`로, 최근 대화 source test명을 `팔로잉 최근 대화 섹션은 RecyclerView와 adapter를 연결한다`, `팔로잉 최근 대화 item은 필수 field와 클릭 계약을 바인딩한다`로 한글화했다. creator의 `75dp`/typography, 최근 대화의 XML orientation, `284dp`/`62dp`, `maxLines`/`ellipsize` source assertion은 제거하고 creator header 제거, 필수 field id, Direct badge, 상대 시간 formatter, click listener 계약은 유지했다. `rg -n -- "75dp|284dp|62dp|android:orientation|android:maxLines|android:ellipsize|Typography.Body5" HomeFollowingFragmentSourceTest.kt` 결과 제거 대상 참조 0건을 확인했다. `HomeFollowingFragmentSourceTest`, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint PASS. `adb devices`에는 연결 기기가 없어 실기기 표현 확인은 Task 5.3 보류 상태를 유지한다.
---
### Phase 7: 최근 소식 ranking 문장과 오디오 콘텐츠 feed 정합성 후속 수정
@@ -546,7 +818,7 @@
- Expected: PASS.
- Result: PASS. ranking news가 크리에이터명/순위를 포함한 문장형 message와 순위 highlight range를 구성하고, content feed가 `88dp` 이미지 및 Figma compact feed 간격 기준을 갖도록 검증됐다.
- [ ] **Task 7.3: 후속 변경 통합 검증**
- [x] **Task 7.3: 후속 변경 통합 검증**
- 검증:
- Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`
- Expected: 팔로잉 관련 local unit/source test가 모두 PASS.
@@ -564,6 +836,7 @@
- 2026-06-30 후속 변경 검증: `./gradlew :app:compileDebugKotlin` PASS.
- 2026-06-30 후속 변경 검증: `git diff --check` PASS.
- 2026-06-30 후속 변경 검증: `./gradlew :app:ktlintCheck`는 FAIL. 실패 지점은 이번 변경 파일이 아닌 `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingTopCardView.kt:11`의 기존 unused import이며, 저장소 규칙에 따라 unrelated 파일은 수정하지 않았다.
- 2026-07-30 재검증: 현재 working tree 기준 `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*" --tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedViewTest"`, `./gradlew --no-daemon :app:mergeDebugResources`, `./gradlew --no-daemon :app:compileDebugKotlin`, `./gradlew --no-daemon :app:ktlintCheck`, `git diff --check` PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다.
- [x] **Task 7.4: content feed radius clipping 구현 위치 수정**
- 수정:
@@ -582,6 +855,82 @@
- 2026-06-30 clipping 위치 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew :app:compileDebugKotlin`, `git diff --check` PASS.
- 2026-06-30 clipping 위치 수정 후 `./gradlew :app:mergeDebugResources`는 최초 병렬 실행 중 Gradle wrapper lock sandbox 권한 오류로 실패했고, 승인 실행 재시도에서 PASS.
#### Task 7.5: ranking 순위 단위 locale 적용
**Goal 실행 `P7-R1`:** 확정 review 항목 `REV-P7-001`에 따라 ranking 문장의 순위 텍스트를 한국어·영어·일본어 locale에 맞게 표시하고 같은 구간을 highlight한다.
- **시작 조건:** `reviews/phase7-ranking-content-feed-review.md``REV-P7-001` 확정.
- **완료 증거:** 영어·일본어에서도 `"위"`가 노출되는 RED → locale string format GREEN → 세 locale와 highlight range 회귀 PASS.
- **범위 밖:** ranking message 문장 자체 재기획, FeedRankView 스타일·색상 변경, API rank 계약 변경.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt`
- Modify: `app/src/main/res/values/strings.xml`
- Modify: `app/src/main/res/values-en/strings.xml`
- Modify: `app/src/main/res/values-ja/strings.xml`
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingNewsAdapterTest.kt`
- Modify: `docs/agent-guides/build-test-style.md`
- [x] **RED:** locale별 ranking message를 생성해 영어·일본어 결과에 한국어 `"위"`가 포함되는 현재 문제를 재현한다.
- [x] **GREEN:** 순위 단위를 locale string resource로 분리해 한국어 `위`, 영어 locale 표기, 일본어 `位`를 사용한다.
- [x] **highlight 검증:** locale별 완성 문장에서 순위 문자열 index가 유효하고 `FeedRankHighlight` 범위가 해당 문자열과 일치하는지 검증한다.
- [x] **회귀 검증:** focused test, Feed view test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint, `git diff --check`를 실행한다.
검증 기록:
- 2026-07-30 RED: `HomeFollowingNewsAdapterTest`를 추가해 한국어/영어/일본어 locale별 ranking message와 highlight range를 검증했고, 구현 전 `buildHomeFollowingRankingFeedItem` 및 locale rank format 부재로 focused test가 실패했다.
- 2026-07-30 GREEN: `screen_home_following_ranking_rank_format`을 3개 locale에 추가하고 ranking feed item 생성 helper에서 locale rank 문자열을 만든 뒤 같은 문자열 범위를 `FeedRankHighlight`로 전달하도록 변경했다. 한국어 `7위`, 영어 `No. 7`, 일본어 `7位` focused test, Feed view test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint, `git diff --check` PASS.
- 2026-07-30 리뷰 게이트 후 보완: subject에 같은 rank 문자열이 먼저 포함될 때 highlight가 잘못 잡힐 수 있다는 Minor 지적을 받아 `lastIndexOf(rankText)`로 삽입된 순위 문자열을 강조하도록 보완했다. 중복 문자열 fixture RED 확인 후 `HomeFollowingNewsAdapterTest`, focused test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint, `git diff --check` PASS.
#### Task 7.6: 신규 ranking 테스트와 실행 가이드 정합성 보완
**Goal 실행 `P7-R2`:** 확정 review 항목 `REV-P7-002`에 따라 신규 ranking 단위 테스트가 저장소의 한글 테스트명과 단일 실행 가이드 규칙을 충족하도록 한다.
- **시작 조건:** `reviews/phase7-ranking-content-feed-review.md``REV-P7-002` 확정과 `HomeFollowingNewsAdapterTest` 추가 상태.
- **완료 증거:** 신규 test name 한글화 → `build-test-style.md` 단일 실행 예시 추가 → focused test·ktlint·문서 diff PASS.
- **범위 밖:** ranking message·highlight production 로직, locale 문구, 기존 영어 테스트명의 일괄 변경.
- **TDD 예외 사유:** 동작 결함이 아니라 테스트/가이드 규칙 정합성 수정이므로 production RED를 만들지 않는다.
- **대체 검증 방법:** 테스트 메서드명과 가이드 명령을 `rg`로 확인하고 focused test와 ktlint를 실행한다.
**Files:**
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingNewsAdapterTest.kt`
- Modify: `docs/agent-guides/build-test-style.md`
- [x] `HomeFollowingNewsAdapterTest`의 신규 backtick 테스트명 2개를 검증 의도가 드러나는 한글 문장으로 변경한다.
- [x] `docs/agent-guides/build-test-style.md``HomeFollowingNewsAdapterTest` 클래스 단위 실행 예시를 추가한다.
- [x] `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingNewsAdapterTest"`를 실행한다.
- [x] `./gradlew --no-daemon :app:ktlintCheck``git diff --check`를 실행하고 결과를 이 Task 아래에 누적한다.
검증 기록:
- 2026-07-30: `HomeFollowingNewsAdapterTest`의 신규 ranking 테스트명 2개를 한글 문장으로 변경하고 `build-test-style.md`에 클래스 단위 실행 예시를 추가했다. `rg`로 한글 테스트명과 가이드 예시를 확인했다. `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingNewsAdapterTest"`, `./gradlew --no-daemon :app:ktlintCheck`, `git diff --check` PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다.
#### Task 7.7: content feed UI 표현 속성 source test 정책 정합성 보완
**Goal 실행 `P7-R3`:** 확정 review 항목 `REV-P7-003`에 따라 가이드 제정 이후 추가된 content feed 크기·padding·margin source test를 제거하고 허용된 대체 검증으로 전환한다.
- **시작 조건:** `reviews/phase7-ranking-content-feed-review.md``REV-P7-003` 확정.
- **완료 증거:** `content layout matches figma compact upload notice dimensions` 테스트 제거 → production layout/View 코드는 유지 → Feed/팔로잉 회귀·resource merge·Kotlin compile·ktlint PASS.
- **범위 밖:** `view_feed_content.xml`의 88dp/간격 값 변경, `FeedContentView` clipping 정책 변경, 다른 기존 Feed 테스트 일괄 정리.
- **TDD 예외 사유:** 동작 결함 수정이 아니라 금지된 UI 표현 속성 테스트 제거이므로 RED/GREEN production 변경을 만들지 않는다.
- **대체 검증 방법:** Figma 기준과 `view_feed_content.xml`/`FeedContentView.kt`를 정적 대조하고 resource merge·compile 및 Task 5.3 수동 화면 확인을 사용한다.
**Files:**
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedViewTest.kt`
- [x] `content layout matches figma compact upload notice dimensions` 테스트만 제거하고 production 파일을 변경하지 않는다.
- [x] `FeedViewTest`의 로직·접근성·clipping 계약 테스트는 이번 범위에서 유지한다.
- [x] Feed focused test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint를 실행한다.
- [x] Figma 크기/간격 대조 결과와 실기기 확인 가능 여부를 검증 기록에 남긴다.
검증 기록:
- 2026-07-30: `FeedViewTest`에서 content feed 크기·padding·margin 문자열을 고정하던 `content layout matches figma compact upload notice dimensions` 테스트만 제거했다. `view_feed_content.xml``FeedContentView.kt` production 코드는 변경하지 않았고, content meta row·clipping·접근성 테스트는 유지했다. focused test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint가 PASS했다. Figma 크기/간격은 기존 production XML/View 정적 대조로 유지하며, `adb devices`에 연결 기기가 없어 실기기 확인은 Task 5.3 보류 사유와 함께 유지한다.
---
### Phase 8: 팔로잉 크리에이터 전체 버튼 후속 추가
@@ -634,6 +983,29 @@
- 2026-06-30 팔로잉 크리에이터 `전체` 버튼 후속 검증: `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다.
- 2026-06-30 팔로잉 크리에이터 `전체` 버튼 높이 보정 후 재검증: creator item 전체 높이에 맞추기 위해 invisible nickname spacer를 추가했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` PASS. `mergeDebugResources`는 최초 sandbox lock 권한 오류 후 승인 실행으로 PASS했고, `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다.
#### Task 8.4: 팔로잉 크리에이터 전체 item 테스트 정책 정합성 보완
**Goal 실행 `P8-R1`:** 확정 review 항목 `REV-P8-001`에 따라 adapter의 마지막 `전체` item과 이동 로직 검증은 유지하고, 금지된 width·height·padding·visibility source assertion만 제거한다.
- **시작 조건:** `reviews/phase8-following-creators-all-review.md``REV-P8-001` 확정.
- **완료 증거:** UI 표현 속성 assertion 제거 및 남은 테스트명 한글화 → adapter view type/개수/route 검증 유지 → focused test·resource merge·Kotlin compile·ktlint PASS.
- **범위 밖:** `item_home_following_creator_all.xml` 디자인 값 변경, adapter 동작 변경, 다른 기존 영문 테스트명 일괄 변경.
- **TDD 예외 사유:** production 동작 결함이 아니라 테스트 정책 정합성 수정이므로 production RED를 만들지 않는다.
- **대체 검증 방법:** XML 토큰은 review 시 정적 대조하고 실제 높이·padding·색상은 resource merge와 Task 5.3 수동 화면 확인으로 검증한다.
**Files:**
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt`
- [x] `following creators list appends all button as last item`에서 layout width·height·padding·visibility·색상 문자열 assertion만 제거한다.
- [x] adapter의 creator/all view type, item count, 마지막 item, `FollowingCreatorActivity` route assertion은 유지한다.
- [x] 수정하는 테스트명을 검증 의도가 드러나는 한글 문장으로 변경한다.
- [x] focused test, resource merge, Kotlin compile, ktlint를 실행하고 대체 검증 결과를 누적한다.
검증 기록:
- 2026-07-30: `following creators list appends all button as last item` 테스트명을 `팔로잉 크리에이터 목록은 전체 item을 마지막에 추가하고 이동을 연결한다`로 한글화하고, width/height/padding/visibility/color source assertion만 제거했다. adapter view type, item count, 마지막 all item, `FollowingCreatorActivity` route, all label string 검증은 유지했다. focused test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint가 PASS했다.
---
### Phase 9: 최근 소식 Response nested payload 계약 반영
@@ -716,6 +1088,77 @@
- 2026-06-30 Phase 9 검증 참고: 최초 병렬 Gradle 검증은 Kotlin incremental cache `Storage ... is already registered` 충돌과 timeout으로 중단되어, 이후 모든 Gradle 검증은 `--no-daemon` 순차 실행으로 재검증했다.
- 2026-06-30 Phase 9 코드 리뷰 후 재검증: `contentImageUrl` nullable 계약을 보완한 뒤 `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew --no-daemon :app:mergeDebugResources`, `./gradlew --no-daemon :app:compileDebugKotlin`, `./gradlew --no-daemon :app:ktlintCheck`, `git diff --check` PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다.
#### Task 9.5: nullable 최근 소식 이미지의 이전 Coil 요청 해제
**Goal 실행 `P9-R1`:** 확정 review 항목 `REV-P9-001`에 따라 nullable/blank 최근 소식 이미지를 바인딩할 때 재사용된 `ImageView`의 이전 Coil 요청을 해제해 다른 news item 이미지가 노출되지 않도록 한다.
- **시작 조건:** `reviews/phase9-news-nested-payload-review.md``REV-P9-001` 확정과 `contentImageUrl`, `imageUrl` nullable 계약 확인.
- **완료 증거:** 최근 소식 ViewHolder 재사용 시 이전 요청이 남는 RED → 공통 blank image 분기 요청 해제 GREEN → adapter focused test와 Phase 9 회귀 PASS.
- **범위 밖:** nullable image의 placeholder/숨김 디자인 변경, Feed 위젯 레이아웃 변경, DTO·mapper·routing 변경.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt`
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingNewsAdapterTest.kt`
- [x] **RED:** 완료된 Coil 요청이 연결된 최근 소식 image view를 nullable image item에 재사용한 뒤 요청이 남는 현재 동작을 Robolectric adapter binding test로 재현한다.
- [x] **GREEN:** `HomeFollowingNewsAdapter.bindImage()`의 blank 분기에서 `coil.dispose()`로 이전 요청을 해제한 뒤 drawable을 비우는 최소 수정만 적용한다.
- [x] **GREEN 확인:** `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingNewsAdapterTest"`를 실행한다.
- [x] **회귀 검증:** mapper와 팔로잉 전체 회귀, `:app:mergeDebugResources`, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`를 실행하고 결과를 이 Task 아래에 누적한다.
검증 기록:
- 2026-07-30 RED: `HomeFollowingNewsAdapterTest`에 nullable content image ViewHolder 재사용 테스트를 추가했고, 구현 전 `assertTrue(disposable.isDisposed)` 실패로 이전 Coil 요청이 남는 동작을 확인했다.
- 2026-07-30 GREEN: `HomeFollowingNewsAdapter.bindImage()` blank 분기에서 `imageView.dispose()` 후 drawable을 비우도록 최소 수정했다. focused test, 팔로잉 전체 회귀와 신규 프로필 테스트, `:app:mergeDebugResources`, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check` PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다.
#### Task 9.6: 최근 소식 Feed model의 미제공 creator ID 합성 제거
**Goal 실행 `P9-R2`:** 확정 review 항목 `REV-P9-002`에 따라 API payload에 없는 `creatorId``contentId` 또는 `postId`로 대신 채우지 않고 미제공 값으로 유지한다.
- **시작 조건:** `reviews/phase9-news-nested-payload-review.md``REV-P9-002` 확정과 `FollowingContentNewsResponse`, `FollowingCommunityPostNewsResponse``creatorId`가 없음을 확인.
- **완료 증거:** 잘못된 ID 대입을 고정한 RED → 두 `FeedItem``creatorId`를 빈 값으로 전달하는 GREEN → 최근 소식 adapter focused test·팔로잉 전체 회귀·Kotlin compile·ktlint PASS.
- **범위 밖:** 서버 DTO에 `creatorId` 추정 추가, API 스키마 변경, 최근 소식 클릭 routing·Feed 공통 model 구조 변경.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt`
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt`
- [x] **RED:** `Content``Community` binding이 API에 없는 `creatorId`를 각각 `contentId`·`postId`로 합성하지 않도록 요구하는 한글명 회귀 테스트를 추가하고 현재 실패를 확인한다.
- [x] **GREEN:**`FeedItem``creatorId`에 빈 문자열을 전달하고 원본 `contentId`·`postId`, creator 이름·이미지, 기존 click callback은 유지한다.
- [x] **GREEN 확인:** `HomeFollowingFragmentSourceTest``HomeFollowingNewsAdapterTest`를 실행한다.
- [x] **회귀 검증:** 팔로잉 전체 회귀, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`를 실행하고 결과를 이 Task 아래에 누적한다.
검증 기록:
- 2026-07-31 RED: `HomeFollowingFragmentSourceTest``팔로잉 최근 소식 feed model은 미제공 creator id를 합성하지 않는다`를 추가했고, 구현 전 `creatorId = community.targetId.toString()`/`creatorId = content.targetId.toString()` 때문에 focused test가 17개 중 1개 실패했다.
- 2026-07-31 GREEN: `HomeFollowingNewsAdapter``FeedItem.Community``FeedItem.Content`에서 `creatorId = ""`를 전달하도록 변경했다. `postId = community.postId.toString()`, `contentId = content.contentId.toString()`, creator 이름·이미지와 원본 item click callback은 유지했다. `rg`로 잘못된 targetId 대입 0건과 빈 creatorId 2건, content/post ID 보존을 확인했다. focused test, 팔로잉 전체 회귀, `:app:compileDebugKotlin`, `:app:ktlintCheck` PASS. `./gradlew --no-daemon :app:installDebug`와 launcher monkey 실행도 PASS했으나, 화면 캡처가 검은 화면이라 실제 팔로잉 탭 터치 대조는 Task 5.3에 미완료로 유지한다.
#### Task 9.7: 미사용 FeedAdapter 제거와 현재 문서 정리
**Goal 실행 `P9-R3`:** 현재 production/test 호출자가 없는 `FeedAdapter``FeedImageViews`를 제거하고, 팔로잉 문서의 재사용 후보 설명을 실제 사용 중인 Feed view/model 기준으로 정리한다.
- **시작 조건:** `FeedAdapter`/`FeedImageViews` production/test 호출자 0건 확인.
- **완료 증거:** `FeedAdapter.kt` 삭제 → 현재 팔로잉 PRD/계획의 `FeedAdapter` 참조 제거 → Kotlin compile·Feed/팔로잉 회귀·diff 검증 PASS.
- **범위 밖:** 과거 완료 문서의 생성 이력 수정, `FeedRankView`/`FeedContentView`/`FeedCommunityView`/`FeedItem` 구조 변경, 신규 공용 adapter 재작성.
- **TDD 예외 사유:** 실행 동작 추가가 아니라 호출자 0건인 미사용 Kotlin 파일 삭제와 문서 정리이므로 신규 RED 테스트를 만들지 않는다.
- **대체 검증 방법:** `codegraph_callers``rg`로 호출자/참조 0건을 확인하고 Kotlin compile, 관련 unit/source test, `git diff --check`를 실행한다.
**Files:**
- Delete: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedAdapter.kt`
- Modify: `docs/20260625_메인_홈_팔로잉_탭/plan-task.md`
- Modify: `docs/20260625_메인_홈_팔로잉_탭/prd.md`
- [x] `codegraph_callers``rg -n "FeedAdapter|FeedImageViews" app/src/main/java app/src/test/java docs/20260625_메인_홈_팔로잉_탭 docs/agent-guides`로 production/test 호출자 0건과 현재 문서 참조만 남았는지 확인한다.
- [x] `FeedAdapter.kt`를 삭제한다.
- [x] 현재 팔로잉 PRD/계획 문서에서 `FeedAdapter` 재사용 후보 문구를 제거하고 실제 재사용 중인 `FeedItem`, `FeedRankView`, `FeedContentView`, `FeedCommunityView` 기준으로 정리한다.
- [x] `./gradlew --no-daemon :app:compileDebugKotlin`, `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedViewTest" --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew --no-daemon :app:ktlintCheck`, `git diff --check`를 실행하고 결과를 누적한다.
검증 기록:
- 2026-07-31: `codegraph_callers` 결과 `FeedAdapter` 호출자 0건을 확인했고, `rg -n "FeedAdapter|FeedImageViews" app/src/main/java app/src/test/java docs/20260625_메인_홈_팔로잉_탭 docs/agent-guides` 결과 production/test 참조 없이 Task 9.7 문서 참조만 남는 것을 확인했다. `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedAdapter.kt`를 삭제하고 현재 팔로잉 PRD/계획 문서의 재사용 후보를 `FeedItem`, `FeedRankView`, `FeedContentView`, `FeedCommunityView` 기준으로 정리했다. `./gradlew --no-daemon :app:compileDebugKotlin :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedViewTest" --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*" :app:ktlintCheck`, `git diff --check` PASS.
---
### Phase 10: On Air 라이브 item Figma 재구현
@@ -740,6 +1183,76 @@
- 2026-07-03 검증: `./gradlew --no-daemon :app:mergeDebugResources`, `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `git diff --check` PASS. Gradle 실행 중 기존 deprecation warning만 출력됐다.
- 2026-07-03 후속 정리: Figma 캡슐형 item에 필요 없는 `iv_home_following_live_thumbnail` 숨김 View와 `HomeFollowingLiveAdapter`의 obsolete thumbnail binding을 제거했다.
#### Task 10.2: On Air 가로 item 간격 복구
**Goal 실행 `P10-R1`:** 확정 review 항목 `REV-P10-001`에 따라 연속된 On Air 캡슐 item 사이에 기존 홈 가로 리스트와 동일한 간격을 적용한다.
- **시작 조건:** `reviews/phase10-on-air-live-item-review.md``REV-P10-001` 확정.
- **완료 증거:** item layout parameter에 end margin이 없는 RED → 기존 `recyclerItemLayoutParams(parent)` 재사용 GREEN → resource/source/팔로잉 회귀 PASS.
- **범위 밖:** 캡슐 크기·색상·내부 spacing 변경, RecyclerView padding 변경, 신규 ItemDecoration 추상화.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingLiveAdapter.kt`
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt`
- [x] **RED:** `HomeFollowingLiveAdapter.onCreateViewHolder()`가 item end margin을 적용하지 않는 현재 상태를 실패 테스트로 고정한다.
- [x] **GREEN:** creator/chat adapter와 같은 `recyclerItemLayoutParams(parent)`를 재사용해 XML 크기를 유지하면서 `spacing_12` end margin을 적용한다.
- [x] **GREEN 확인:** `HomeFollowingFragmentSourceTest`와 팔로잉 전체 회귀 테스트를 실행한다.
- [x] **회귀 검증:** resource merge, Kotlin compile, ktlint, `git diff --check`를 실행하고 결과를 누적한다.
검증 기록:
- 2026-07-30 RED: `HomeFollowingFragmentSourceTest.following live adapter keeps horizontal item spacing`을 추가해 live adapter의 `recyclerItemLayoutParams(parent)` 재사용을 요구했고, 구현 전 helper 미사용으로 focused test가 실패했다.
- 2026-07-30 GREEN: `HomeFollowingLiveAdapter.onCreateViewHolder()`에서 inflate한 view에 `recyclerItemLayoutParams(parent)`를 적용해 기존 XML 크기와 `spacing_12` end margin을 유지했다. focused test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint, `git diff --check` PASS.
#### Task 10.3: On Air 간격 검증의 테스트 정책 정합성 보완
**Goal 실행 `P10-R2`:** 확정 review 항목 `REV-P10-002`에 따라 UI margin을 source 문자열로 고정한 신규 테스트를 제거하고 가이드가 허용하는 대체 검증 기록으로 전환한다.
- **시작 조건:** `reviews/phase10-on-air-live-item-review.md``REV-P10-002` 확정과 Task 10.2 production 수정 완료.
- **완료 증거:** 금지된 UI 표현 속성 source test만 제거 → adapter helper 사용 정적 대조 → resource merge·Kotlin compile·팔로잉 회귀 PASS.
- **범위 밖:** `HomeFollowingLiveAdapter``recyclerItemLayoutParams(parent)` 적용 제거, item 크기·간격 변경, 기존 source test 일괄 정리.
- **TDD 예외 사유:** `docs/agent-guides/code-style.md`가 View margin 같은 UI 표현 속성 테스트를 금지하므로 실행 명령과 정적 대조를 대체 검증으로 사용한다.
- **대체 검증 방법:** adapter의 기존 helper 재사용을 `rg`로 확인하고 resource merge, Kotlin compile, 기존 팔로잉 로직 회귀를 실행한다.
**Files:**
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt`
- [x] 신규 `following live adapter keeps horizontal item spacing` source test만 제거하고 Task 10.2 production 코드는 유지한다.
- [x] `rg -n "recyclerItemLayoutParams\(parent\)" app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingLiveAdapter.kt`로 기존 helper 재사용을 확인한다.
- [x] `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `:app:mergeDebugResources`, `:app:compileDebugKotlin`을 실행한다.
- [x] `./gradlew --no-daemon :app:ktlintCheck``git diff --check`를 실행하고 결과를 이 Task 아래에 누적한다.
검증 기록:
- 2026-07-30: UI margin을 source 문자열로 고정하던 `following live adapter keeps horizontal item spacing` 테스트만 제거하고 `HomeFollowingLiveAdapter``recyclerItemLayoutParams(parent)` 적용은 유지했다. `rg -n "recyclerItemLayoutParams\(parent\)" app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingLiveAdapter.kt`로 helper 재사용을 확인했다. `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew --no-daemon :app:mergeDebugResources`, `./gradlew --no-daemon :app:compileDebugKotlin`, `./gradlew --no-daemon :app:ktlintCheck`, `git diff --check` PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다.
#### Task 10.4: On Air 고정 폭 LayoutParams 보존
**Goal 실행 `P10-R3`:** 확정 review 항목 `REV-P10-003`에 따라 On Air item의 XML `263dp` 폭을 런타임에서도 유지하면서 `spacing_12` end margin을 적용한다.
- **시작 조건:** `reviews/phase10-on-air-live-item-review.md``REV-P10-003` 확정.
- **완료 증거:** `RecyclerView.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)` 교체 경로 제거 → inflate된 width/height를 유지한 채 end margin만 적용 → resource merge·Kotlin compile·팔로잉 회귀 PASS → 연결 기기 준비 시 Task 5.3 수동 확인.
- **범위 밖:** 263dp × 100dp 디자인 변경, 공통 helper 전체 리팩터링, On Air 내부 필드/라우팅 변경.
- **TDD 예외 사유:** `docs/agent-guides/code-style.md`가 View width·margin 테스트 추가를 금지하므로 새 자동 UI 크기 테스트를 작성하지 않는다.
- **대체 검증 방법:** inflate 후 기존 `layoutParams` 보존 여부를 코드로 대조하고 resource merge·compile·기존 팔로잉 회귀 및 가능한 경우 실기기 수동 확인을 수행한다.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingLiveAdapter.kt`
- Verify: `app/src/main/res/layout/item_home_following_live.xml`
- [x] `HomeFollowingLiveAdapter.onCreateViewHolder()`가 inflate된 root의 기존 263dp × 100dp layout parameter를 보존하고 end margin만 추가하도록 최소 수정한다.
- [x] `HomeFollowingFragmentSourceTest`에 width·margin source assertion을 다시 추가하지 않는다.
- [x] 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint를 실행한다.
- [x] 연결 기기가 있으면 연속 On Air item의 폭과 간격을 수동 확인하고, 없으면 Task 5.3과 함께 사유를 기록한다.
검증 기록:
- 2026-07-30: `HomeFollowingLiveAdapter.onCreateViewHolder()`에서 `recyclerItemLayoutParams(parent)` 사용을 제거하고 inflate된 `RecyclerView.LayoutParams`의 263dp × 100dp 계약을 유지한 채 `spacing_12` end margin만 적용했다. width·margin source assertion은 다시 추가하지 않았다. focused test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint가 PASS했다. `adb devices`에 연결 기기가 없어 On Air 폭/간격 실기기 확인은 Task 5.3 보류 사유와 함께 유지한다.
---
### Phase 11: 최근 소식 COMMUNITY_POST 상세 이동
@@ -829,9 +1342,122 @@
- 검증 기록:
- 2026-07-13: `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew --no-daemon :app:mergeDebugResources`, `./gradlew --no-daemon :app:compileDebugKotlin`, `git diff --check` 모두 PASS. Gradle 실행 중 기존 deprecation warning만 출력됐다.
#### Task 12.4: empty 상태 UI 표현 속성 source test 정책 정합성 보완
**Goal 실행 `P12-R1`:** 확정 review 항목 `REV-P12-001`에 따라 empty 상태의 mapper/문구/분기 검증은 유지하고, 금지된 layout 크기·정렬·visibility 문자열 assertion을 제거한다.
- **시작 조건:** `reviews/phase12-empty-state-review.md``REV-P12-001` 확정.
- **완료 증거:** UI 표현 속성 assertion 제거 및 남은 테스트명 한글화 → mapper Empty 조건과 locale 문구/상태 분기 검증 유지 → focused test·resource merge·Kotlin compile·ktlint PASS.
- **범위 밖:** empty 문구/중앙 배치 production 변경, 로그인 유도 UI, 다른 기존 source test 일괄 정리.
- **TDD 예외 사유:** production 동작이 아니라 테스트 정책 정합성을 수정하므로 production RED를 만들지 않는다.
- **대체 검증 방법:** 중앙 배치는 `fragment_v2_main_home.xml` 정적 대조와 Task 5.3 실기기 확인으로 검증하고, Empty 판정은 `HomeFollowingMapperTest`로 유지한다.
**Files:**
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt`
- [x] empty 테스트에서 width·height·gravity·textAlignment·visibility와 직접 `View.VISIBLE/GONE` 문자열 assertion을 제거한다.
- [x] 3개 locale 문구 계약, `HomeFollowingUiState.Empty` 분기, 기존 mapper Empty 테스트는 유지한다.
- [x] 수정하는 테스트명을 검증 의도가 드러나는 한글 문장으로 변경한다.
- [x] focused test, resource merge, Kotlin compile, ktlint를 실행하고 Task 5.3 수동 확인 가능 여부를 기록한다.
검증 기록:
- 2026-07-30: empty source test명을 `팔로잉 empty 상태는 다국어 빈 문구와 상태 분기를 유지한다`로 한글화하고, width/height/gravity/textAlignment/visibility 및 직접 `View.VISIBLE/GONE` 문자열 assertion을 제거했다. 3개 locale 문구, empty string 재사용, `HomeFollowingUiState.Empty`/Error 상태 분기 검증은 유지했다. focused test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint가 PASS했다. `adb devices`에 연결 기기가 없어 중앙 배치 실기기 확인은 Task 5.3 보류 사유와 함께 유지한다.
---
### Phase 13: 최근 소식 CREATOR_RANKING·AUDIO_CONTENT 터치 이동
**Phase 결과:** 팔로잉 탭 최근 소식에서 크리에이터 순위 item은 해당 크리에이터 채널로, 오디오 콘텐츠 업로드 item은 해당 오디오 콘텐츠 상세로 이동한다.
**선행조건:** Phase 11의 기존 `COMMUNITY_POST` 상세 이동과 공통 Creator/Content Action 경로가 유지되어야 한다.
**Phase 완료 조건:** `CREATOR_RANKING`, `AUDIO_CONTENT`, 기존 `COMMUNITY_POST` 이동 source test와 팔로잉 회귀 테스트, Kotlin 컴파일이 통과한다.
#### Task 13.1 최근 소식 이동 RED 테스트 추가
**Goal 실행 `P13-T1`:** 현재 실행되지 않는 `CREATOR_RANKING``AUDIO_CONTENT` 터치 이동을 실패 source test로 재현한다.
- **시작 조건:** PRD의 `2026-07-30 CREATOR_RANKING·AUDIO_CONTENT 상세 이동 Requirements` 확정.
- **완료 증거:** 구현 전 focused test가 새 이동 호출 부재로 실패한다.
- **범위 밖:** `CONTENT_RANKING`, `PHOTO_CONTENT`, On Air, 스케줄 터치 이동.
**Files:**
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLoginGuardSourceTest.kt`
- [x] **RED:** `onFollowingNewsClick()``CREATOR_RANKING``CreatorActionCommand.Profile`로, `AUDIO_CONTENT``ContentActionCommand.AudioDetail`로 전달하는지 검증하는 source test를 추가한다.
- [x] **RED 확인:** `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest"`를 실행해 두 공통 액션 호출 부재로 인한 assertion 실패를 확인한다.
- [x] **GREEN:** Task 13.2의 최소 구현으로 RED를 통과시킨다.
- [x] **GREEN 확인:** 같은 focused test를 다시 실행해 성공을 확인한다.
- [x] **REFACTOR:** 새 abstraction 없이 기존 공통 액션을 재사용하고 focused test를 다시 실행한다.
검증 기록:
- 2026-07-30 RED: focused test를 실행해 11개 중 `COMMUNITY_POST` 분기 구조, `CREATOR_RANKING` 채널 이동, `AUDIO_CONTENT` 상세 이동 3개가 assertion 실패하는 것을 확인했다. 현재 production이 `HomeFollowingNewsUiItem.Community` 이외의 item을 즉시 반환해 요청 동작이 없는 것이 실패 원인이다.
- 2026-07-30 GREEN: 동일 focused test를 다시 실행해 11개 모두 PASS했다. 새 abstraction이나 직접 `Intent` 조립 없이 기존 Creator/Content/Community Action 호출을 검증했다.
#### Task 13.2 CREATOR_RANKING·AUDIO_CONTENT 이동 구현
**Goal 실행 `P13-T2`:** 요청된 두 최근 소식 타입만 기존 공통 액션 경로로 이동시킨다.
- **시작 조건:** `P13-T1` RED 확인.
- **완료 증거:** focused test와 Kotlin 컴파일 통과.
- **범위 밖:** API/DTO/mapper/UI 변경, 직접 `Intent` 조립, 다른 최근 소식 타입의 신규 이동.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`
- [x] **RED:** Task 13.1의 실패 source test를 기준으로 한다.
- [x] **RED 확인:** Task 13.1 실행 결과를 확인한다.
- [x] **GREEN:** `onFollowingNewsClick()`에서 `CREATOR_RANKING``CreatorActionCommand.Profile(item.targetId)`, `AUDIO_CONTENT``ContentActionCommand.AudioDetail(item.contentId)`를 호출하고 기존 `COMMUNITY_POST` 분기를 유지한다.
- [x] **GREEN 확인:** focused test와 `./gradlew --no-daemon :app:compileDebugKotlin`을 실행해 성공을 확인한다.
- [x] **REFACTOR:** `CONTENT_RANKING``PHOTO_CONTENT`는 no-op으로 유지하고 이번 변경이 만든 중복만 점검한다.
검증 기록:
- 2026-07-30: `HomeMainFragment.onFollowingNewsClick()`을 sealed UI item 분기로 변경했다. `CREATOR_RANKING``CreatorActionCommand.Profile(item.targetId)`, `AUDIO_CONTENT``ContentActionCommand.AudioDetail(item.contentId)`로 전달하며 기존 `COMMUNITY_POST``postId` 검증과 상세 이동을 유지했다. focused test와 `:app:compileDebugKotlin`이 PASS했다.
#### Task 13.3 최근 소식 터치 이동 회귀 검증
**Goal 실행 `P13-GATE`:** 요청된 이동과 기존 팔로잉 동작에 회귀가 없는지 판정한다.
- **시작 조건:** `P13-T1`, `P13-T2` 완료.
- **완료 증거:** 아래 자동 검증 결과와 수동 검증 가능 여부를 기록한다.
- **범위 밖:** 연결 기기 없는 환경에서 수동 검증을 성공으로 추정하는 행위.
- [x] `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest"` PASS.
- [x] `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"` PASS.
- [x] `./gradlew --no-daemon :app:compileDebugKotlin` PASS.
- [x] `./gradlew --no-daemon :app:ktlintCheck` PASS 또는 이번 변경과 무관한 기존 위반만 확인.
- [x] `git diff --check` PASS.
- [x] 연결 기기가 있으면 `CREATOR_RANKING``AUDIO_CONTENT` item 터치 목적지를 수동 확인하고, 없으면 미실행 사유를 기록한다.
검증 기록:
- 2026-07-30: focused test, 팔로잉 전체 회귀 테스트, Kotlin 컴파일, ktlint, `git diff --check`가 모두 PASS했다. ktlint에는 기존 `.editorconfig``disabled_rules` deprecation 경고만 출력됐다. `adb devices` 결과 연결된 기기가 없어 실제 item 터치 수동 검증은 실행하지 못했다.
---
## Verification Log
- 2026-07-31 이번 Phase 1~13 리뷰 재검증: 현재 working tree의 PRD·계획·production/test 코드와 Phase별 리뷰 보고서 13개를 대조했다. 새 확정 발견 사항은 없고 기존 확정 항목은 모두 각 회귀 수정 Task에서 수정 완료 상태다. Phase 9 보고서 범위를 Task 9.7까지 확장해 production/test `FeedAdapter|FeedImageViews` 호출자 0건과 현재 Feed view/model 직접 재사용을 기록했다. `./gradlew --no-daemon :app:testDebugUnitTest ... :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` PASS(`BUILD SUCCESSFUL`, 16 suites·120 tests, failures/errors/skipped 0, 46 actionable tasks), `./gradlew --no-daemon tasks --all`, staged/unstaged `git diff --check` PASS. `adb devices`에서 기기 `2cec640c34017ece` 연결은 확인했지만 테스트 계정·API 데이터 기반 팔로잉 화면 조작과 Figma 대조는 수행하지 않아 `REV-P5-001` / Task 5.3 보류를 유지한다. 이번 리뷰에서는 production/test/resource 코드를 수정하지 않았다.
- 2026-07-31 Phase 1~13 최종 리뷰 검증: 현재 working tree의 PRD·계획·production/test 코드와 Phase별 리뷰 보고서 13개를 다시 대조했다. 신규 확정 항목은 `REV-P4-004`(공용 source test의 영문 문장형 테스트명 11개)와 `REV-P9-002`(API가 제공하지 않는 creator ID 자리에 content/post ID를 합성)이며, 코드 수정 전에 Phase 4 Task 4.9 / `P4-R4`, Phase 9 Task 9.6 / `P9-R2`로 전환했다. 이번 리뷰에서는 production/test/resource 코드를 수정하지 않았다. `./gradlew --no-daemon :app:testDebugUnitTest ... :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` PASS(`BUILD SUCCESSFUL`, 20 suites·306 tests, failures/errors/skipped 0, 46 actionable tasks), `./gradlew --no-daemon tasks --all`, staged/unstaged `git diff --check`도 PASS했다. `adb devices`는 정상 실행됐으나 연결 기기가 없어 `REV-P5-001` / Task 5.3 실기기 수동 확인 보류를 유지한다.
- 2026-07-31 리뷰 보완 Task 2.8, 6.8 완료 검증: `REV-P2-002`에 따라 미사용 `HomeFollowingNewsUiItem.Content.labelResId`와 전용 `FollowingNewsType.toLabelResId()`를 제거하고, 실제 표시 경로인 `FeedContentCategory.Photo` label 회귀를 `HomeFollowingNewsAdapterTest`에 유지했다. `REV-P6-004`에 따라 creator·최근 대화 source test의 UI 표현 속성 문자열 assertion만 제거하고 구조·필수 field·Direct badge·상대 시간 formatter·click 계약 검증은 유지했다. focused test 3개, 팔로잉 전체 회귀, `./gradlew --no-daemon :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다. `adb devices`에는 연결 기기가 없어 Task 5.3 실기기 수동 확인 보류를 유지한다.
- 2026-07-31 Phase 1~13 리뷰 재점검: 현재 working tree의 PRD·계획·production/test 코드와 Phase별 리뷰 보고서 13개를 다시 대조했다. 신규 확정 항목은 `REV-P2-002`(최근 소식 `Content.labelResId`와 전용 mapper가 production 표시에서 미사용)와 `REV-P6-004`(creator·최근 대화 UI 표현 속성을 검증하는 source test가 현재 코드 스타일 정책과 불일치)이며, 구현 전에 Phase 2 Task 2.8 / `P2-R2`, Phase 6 Task 6.8 / `P6-R4`로 전환했다. 이번 리뷰에서는 production/test/resource 코드를 수정하지 않았다. `./gradlew --no-daemon :app:testDebugUnitTest --tests 'kr.co.vividnext.sodalive.v2.main.home.*Following*' --tests 'kr.co.vividnext.sodalive.v2.main.home.HomeCreatorProfileImageLoaderTest' --tests 'kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest' --tests 'kr.co.vividnext.sodalive.v2.widget.feed.FeedViewTest' --tests 'kr.co.vividnext.sodalive.v2.live.action.*' --tests 'kr.co.vividnext.sodalive.v2.content.action.*' --tests 'kr.co.vividnext.sodalive.v2.community.action.*' :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` PASS(`BUILD SUCCESSFUL`, 15 suites·113 tests, failures/errors 0, 46 actionable tasks). `./gradlew --no-daemon tasks --all`, staged/unstaged `git diff --check`도 PASS했다. `adb devices`에는 연결 기기가 없어 Phase 5 Task 5.3 실기기 수동 확인 보류를 유지한다.
- 2026-07-31 리뷰 보완 Task 2.7 완료 검증: `REV-P2-001`에 따라 미사용 팔로잉 string resource `screen_home_following_creators_title`, `screen_home_following_on_air_title`, `screen_home_following_error`를 3개 locale에서 제거했다. 선언 외 참조 0건을 `rg`로 확인했고 사용 중인 팔로잉 문자열은 유지했다. `./gradlew --no-daemon :app:mergeDebugResources :app:compileDebugKotlin`, `./gradlew --no-daemon --rerun-tasks :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingMapperTest"`, `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew --no-daemon :app:ktlintCheck`, `git diff --check` PASS. 첫 팔로잉 전체 회귀는 `R.string` id 변경 증분 캐시로 mapper test 2건이 실패했으나 `--rerun-tasks` 재빌드 후 focused와 전체 회귀가 PASS했다. `adb devices`에 연결된 기기가 없어 Phase 5 Task 5.3 실기기 수동 확인은 계속 보류한다.
- 2026-07-30 Phase 1~13 최종 리뷰 재점검: 현재 working tree의 PRD·계획·production/test 코드·Phase별 리뷰 보고서 13개를 다시 대조했다. 신규 확정 항목은 `REV-P2-001`(후속 UI·오류 처리 변경 후 3개 locale에 미사용 string resource 3개가 남음) 1건이며 Phase 2 Task 2.7 / `P2-R1`로 전환했다. 이번 리뷰에서 production/test/resource 코드는 수정하지 않았다. `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeCreatorProfileImageLoaderTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest" --tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedViewTest" --tests "kr.co.vividnext.sodalive.v2.live.action.*" --tests "kr.co.vividnext.sodalive.v2.content.action.*" --tests "kr.co.vividnext.sodalive.v2.creator.action.*" --tests "kr.co.vividnext.sodalive.v2.community.action.*" :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` PASS(`BUILD SUCCESSFUL`, 119 tests, failures/errors 0, 46 actionable tasks), `./gradlew --no-daemon tasks --all`, staged/unstaged `git diff --check` PASS. `adb devices`는 정상 실행됐으나 연결 기기가 없어 Phase 5 Task 5.3 수동 검증 보류를 유지한다.
- 2026-07-30 리뷰 보완 Task 4.8, 9.5 완료 검증: blank profile image와 nullable 최근 소식 image 바인딩 시 재사용된 `ImageView`의 이전 Coil 요청을 `dispose()`로 해제하도록 보완했다. 각 focused test는 구현 전 `disposable.isDisposed` assertion 실패로 RED를 확인했고, 최소 구현 후 GREEN으로 전환했다. `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeCreatorProfileImageLoaderTest"`, `./gradlew --no-daemon :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck`, `git diff --check` PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다.
- 2026-07-30 Phase 1~13 리뷰 최종 재검증: 현재 working tree의 PRD·계획·production/test 코드와 Phase별 리뷰 보고서 13개를 대조했다. 신규 확정 항목은 `REV-P4-003`(blank profile image binding의 이전 Coil 요청 해제 누락)과 `REV-P9-001`(nullable 최근 소식 image binding의 이전 Coil 요청 해제 누락)이며, 구현 전에 Phase 4 Task 4.8 / `P4-R3`, Phase 9 Task 9.5 / `P9-R1`로 전환했다. 이미 완료된 `REV-P6-003` / Task 6.7은 Phase 6 보고서의 상태와 수정 검증 기록을 현재 코드에 맞게 정정했다. `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest" --tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedViewTest" --tests "kr.co.vividnext.sodalive.v2.live.action.*" --tests "kr.co.vividnext.sodalive.v2.content.action.*" --tests "kr.co.vividnext.sodalive.v2.community.action.*" :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` PASS(`BUILD SUCCESSFUL`, 111 tests, failures/errors 0, 46 actionable tasks), `./gradlew --no-daemon tasks --all` PASS, staged/unstaged `git diff --check` PASS. `adb devices`는 정상 실행됐으나 연결 기기가 없어 Phase 5 Task 5.3 수동 검증 보류를 유지한다. 이번 리뷰에서는 production/test 코드를 수정하지 않았다.
- 2026-07-30 리뷰 보완 Task 6.7, 7.7, 8.4, 10.4, 12.4 완료 검증: 최근 대화와 On Air adapter는 inflate된 root의 기존 `RecyclerView.LayoutParams` width/height를 보존하고 `spacing_12` end margin만 적용하도록 수정했다. 금지된 UI 표현 속성 source assertion은 Feed content, 팔로잉 크리에이터 전체 item, empty 상태 테스트에서 제거하고, 동작/문구/route 계약 검증은 유지했다. `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest" --tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedViewTest"`, `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*" --tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedViewTest" :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다. `adb devices`에 연결 기기가 없어 Task 5.3 실기기 수동 확인 보류를 유지한다.
- 2026-07-30 Phase 1~13 리뷰 최종 검증: `./gradlew --no-daemon :app:testDebugUnitTest --tests 'kr.co.vividnext.sodalive.v2.main.home.*Following*' --tests 'kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest' --tests 'kr.co.vividnext.sodalive.v2.main.home.FeedViewTest' --tests 'kr.co.vividnext.sodalive.v2.live.action.*' --tests 'kr.co.vividnext.sodalive.v2.content.action.*' --tests 'kr.co.vividnext.sodalive.v2.community.action.*' :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` PASS(`BUILD SUCCESSFUL`, 46 actionable tasks), `git diff --check` PASS. `adb devices`는 정상 실행됐으나 연결 기기가 없어 Phase 5 Task 5.3 수동 검증은 미실행 상태를 유지한다. 자동 검증 PASS는 신규 확정 항목 `REV-P6-003`, `REV-P7-003`, `REV-P8-001`, `REV-P10-003`, `REV-P12-001`의 정적 리뷰 결과를 해소하지 않으며, 각 신규 Task에서 후속 조치한다.
- 2026-07-30 Phase별 추가 재점검: 기존 Phase 1~13 리뷰 보고서, 현재 production/test 코드, `docs/agent-guides/code-style.md`를 다시 대조했다. 신규 확정 항목은 `REV-P6-003`(최근 대화 `284dp` 폭을 adapter가 `WRAP_CONTENT`로 교체), `REV-P7-003`(content feed UI 크기·padding·margin source test 정책 위반), `REV-P8-001`(`전체` item UI 표현 속성 source assertion), `REV-P10-003`(On Air `263dp` 폭을 간격 helper가 `WRAP_CONTENT`로 교체), `REV-P12-001`(empty 크기·정렬·visibility source assertion)이다. 구현 전에 해당 Phase에 Task 6.7, 7.7, 8.4, 10.4, 12.4를 추가했으며, 이번 재점검에서는 production/test 코드를 수정하지 않았다. Phase 5 Task 5.3의 실기기 수동 확인은 기존 보류 상태를 유지한다.
- 2026-07-30 리뷰 보완 Task 6.6, 7.6, 10.3 완료 검증: 상단 파일 구조의 obsolete 최근 소식 layout 항목을 Feed layout 재사용/삭제 이력 기준으로 정리하고, `HomeFollowingNewsAdapterTest` 신규 테스트명 2개를 한글화했으며, `build-test-style.md`에 클래스 단위 실행 예시를 추가하고, On Air 간격 source test 정책 위반 항목을 제거했다. `rg` 대체 검증, `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingNewsAdapterTest"`, `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew --no-daemon :app:mergeDebugResources`, `./gradlew --no-daemon :app:compileDebugKotlin`, `./gradlew --no-daemon :app:ktlintCheck`, `git diff --check` PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다. 리뷰어 재확인 결과 blocking finding 없음.
- 2026-07-30 Phase별 리뷰 산출물 재점검: 기존 Phase 1~13 보고서와 현재 working tree를 다시 대조했다. 기존 통합 리뷰 기록의 “이번 리뷰에서는 production 코드를 수정하지 않았다”는 문구와 달리, 같은 리뷰 후속 Task 4.6·4.7·6.5·7.5·10.2에서 production 코드가 수정된 상태임을 정정한다. 이번 재점검 자체에서는 production 코드를 변경하지 않았으며, `REV-P6-002`(삭제된 layout의 상단 파일 구조 잔존), `REV-P7-002`(신규 테스트명과 단일 실행 가이드 미준수), `REV-P10-002`(UI margin source test 정책 위반)를 추가 확정해 Task 6.6·7.6·10.3으로 전환했다. `*Following*` 39개, `HomeMainFragmentLoginGuardSourceTest` 13개, Feed·Live·Content·Community Action 회귀, resource merge, Kotlin compile, ktlint, `./gradlew --no-daemon tasks --all`, staged/unstaged diff whitespace 검증이 PASS했다. `adb devices`에는 연결 기기가 없어 Task 5.3 실기기 수동 확인 보류를 유지한다.
- 2026-07-30 리뷰 보완 Task 3.4, 4.6, 4.7, 6.5, 7.3, 7.5, 10.2 완료 검증: RED 단계에서 focused source/unit test 실패를 확인한 뒤 최소 구현으로 GREEN 전환했다. `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingNewsAdapterTest"`, 팔로잉 전체 회귀 + FeedView + `v2.live.action.*`/`v2.content.action.*`/`v2.community.action.*`, `./gradlew --no-daemon :app:mergeDebugResources`, `./gradlew --no-daemon :app:compileDebugKotlin`, `./gradlew --no-daemon :app:ktlintCheck`, `git diff --check` PASS. 최종 리뷰 게이트에서 Critical/Important finding은 없었고, ranking highlight 중복 문자열 Minor를 추가 RED/GREEN으로 보완했다. 최초 병렬 Gradle 검증은 `mergeDebugResources` incremental missing file로 실패했으나 순차 재실행에서 PASS했다. `adb devices`에 연결 기기가 없어 Task 5.3 실기기 수동 확인은 계속 blocked다.
- 2026-07-30 Phase 1~13 통합 리뷰: `docs/sample/sample-review.md`와 review guide에 따라 Phase별 리뷰 보고서 13개를 `reviews/` 아래의 `phase1-...-review.md`부터 `phase13-...-review.md`까지 작성했다. 확정 항목은 `REV-P3-001`(Loading 상태 발행 순서 테스트 누락), `REV-P4-001`(On Air·스케줄 item 이동 no-op), `REV-P4-002`(LoginRequired pull refresh 종료 누락), `REV-P6-001`(Feed 전환 전 obsolete layout), `REV-P7-001`(영어·일본어 ranking 순위 단위의 한국어 고정), `REV-P10-001`(On Air item 간격 누락)이며, 코드 수정 전에 해당 Phase에 Task 3.4, 4.6, 4.7, 6.5, 7.5, 10.2를 추가했다. Phase 5 실기기 수동 확인은 `adb devices`에 연결 기기가 없어 기존 Task 5.3 보류를 유지했다. 검증으로 Phase 2~3 focused test, Phase 4·6·7·8·9·11·12·13 source/feed focused test, 팔로잉 전체 회귀 36개, `:app:mergeDebugResources`, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`를 실행했고 모두 PASS했다. Gradle deprecation과 Agora namespace는 기존 경고이며, 이번 리뷰에서는 production 코드를 수정하지 않았다.
- 2026-07-30 Phase 13 최근 소식 터치 이동 검증: `onFollowingNewsClick()``CREATOR_RANKING`을 기존 `CreatorActionCommand.Profile`로 전달해 해당 크리에이터 채널로 이동하고, `AUDIO_CONTENT`를 기존 `ContentActionCommand.AudioDetail`로 전달해 해당 오디오 콘텐츠 상세로 이동하도록 보완했다. 기존 `COMMUNITY_POST` 상세 이동은 유지하고 `CONTENT_RANKING`, `PHOTO_CONTENT`는 no-op으로 유지했다. RED에서 focused test 11개 중 요청 동작 관련 3개 실패를 확인했고, GREEN에서 11개 모두 PASS했다. 팔로잉 전체 회귀 테스트, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`가 PASS했다. `adb devices`에 연결된 기기가 없어 실제 터치 수동 검증은 미실행했다.
- 2026-07-13 Phase 12 전체 empty 문구 중앙 표시 검증: 팔로잉 탭 전체 empty 상태에서 기존 `screen_home_following_empty` 리소스를 재사용해 `아직 팔로잉 소식이 없어요.\n관심 있는 크리에이터를 팔로우해 보세요.` 문구를 중앙 표시하도록 구현했다. 영어는 `No following updates yet.\nFollow creators you are interested in.`, 일본어는 `フォロー中のお知らせはまだありません。\n気になるクリエイターをフォローしてみましょう。`로 갱신했다. `HomeFollowingUiState.Empty`에서만 empty 문구가 보이고 `Content`, `LoginRequired`, `Error`에서는 숨김 처리된다. 검증으로 `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew --no-daemon :app:mergeDebugResources`, `./gradlew --no-daemon :app:compileDebugKotlin`, `git diff --check` 모두 PASS. Gradle 실행 중 기존 deprecation warning만 출력됐다.
- 2026-07-12 Phase 11 검증: 최근 소식 `COMMUNITY_POST` 상세 이동 계약을 source test로 추가하고 `HomeMainFragment.onFollowingNewsClick()`에서 `HomeFollowingNewsUiItem.Community``CreatorChannelCommunityDetailActivity.newIntent(requireContext(), postId)`로 이동하도록 구현했다. RED 시도: 구현 전 targeted test `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest"`는 120초/300초 timeout으로 assertion 결과까지 도달하지 못했지만, 당시 production은 no-op 상태였고 새 source test는 `ensureMainV2NavigationAllowed`와 상세 이동 호출 문자열을 요구하도록 추가되어 실패 조건을 고정했다. GREEN: 동일 targeted test PASS. 회귀 검증 `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew --no-daemon :app:compileDebugKotlin`, `git diff --check` PASS. `./gradlew --no-daemon :app:ktlintCheck`는 이번 변경과 무관한 기존 legacy/source 위반(`explorer/profile/creator_community` package underscore, `LiveRoomActivity.kt` formatting, `NicknameUpdateViewModel.kt` indentation 등)으로 FAIL. 실기기 수동 QA는 `adb devices` 결과 연결된 device가 없어 blocked.
- 2026-06-25 Phase 1-3 구현 검증: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` 모두 PASS.

View File

@@ -272,7 +272,7 @@ Figma `24:5682` 기준 상단 title bar와 tab bar는 기존 홈 화면 구조
- `contentImageUrl` 또는 `imageUrl`이 null이면 타입별 기본 이미지 표시/숨김 정책을 구현 계획에서 확정한다.
- item 터치 시 `type`과 payload의 target id(`creatorId`, `contentId`, `postId`)에 맞는 목적지로 이동한다.
- 리스트가 비어 있으면 섹션을 숨긴다.
- 기존 `FeedAdapter`, `FeedItem.Rank`, `FeedItem.Community`, `FeedItem.Content`, `FeedRankView`, `FeedCommunityView`, `FeedContentView` 재사용 가능성을 우선 검토한다.
- 기존 `FeedItem.Rank`, `FeedItem.Community`, `FeedItem.Content`, `FeedRankView`, `FeedCommunityView`, `FeedContentView` 재사용 가능성을 우선 검토한다.
- `PHOTO_CONTENT`는 기존 `FeedItem.Content`의 category 확장 또는 팔로잉 전용 item 추가 중 더 작은 변경을 구현 계획에서 선택한다.
- 각 섹션의 “더보기” chevron은 터치 액션만 연결하고, 실제 이동 목적지는 아직 만들지 않는다.
@@ -298,6 +298,14 @@ Figma `24:5682` 기준 상단 title bar와 tab bar는 기존 홈 화면 구조
- `HomeFollowingUiState.LoginRequired``Error`는 이번 범위에서 empty 문구를 표시하지 않고 기존처럼 섹션을 숨긴다.
- API/DTO/mapper 계약과 팔로잉 크리에이터 `전체` 버튼 노출 정책은 변경하지 않는다.
#### 2026-07-30 CREATOR_RANKING·AUDIO_CONTENT 상세 이동 Requirements
- 최근 소식 섹션의 `CREATOR_RANKING` item 터치 시 `creatorRanking.creatorId`에 해당하는 크리에이터 채널로 이동한다.
- 최근 소식 섹션의 `AUDIO_CONTENT` item 터치 시 `audioContent.contentId`에 해당하는 오디오 콘텐츠 상세 화면으로 이동한다.
- 크리에이터 채널 이동은 기존 `CreatorActionCommand.Profile`, 오디오 콘텐츠 상세 이동은 기존 `ContentActionCommand.AudioDetail` 공통 액션 경로를 재사용한다.
- 공통 액션 경로의 로그인·콘텐츠 접근 정책과 유효 ID 검증을 유지하며 직접 `Intent` 또는 legacy extra를 새로 조립하지 않는다.
- 기존 `COMMUNITY_POST` 상세 이동은 유지하고, `CONTENT_RANKING``PHOTO_CONTENT`의 터치 동작은 이번 범위에서 추가하지 않는다.
- API/DTO/mapper, 최근 소식 표시 UI와 다른 팔로잉 섹션의 동작은 변경하지 않는다.
#### 2026-06-30 후속 수정 Requirements
- `CREATOR_RANKING` 최근 소식 본문은 순위 숫자만 단독 표시하지 않고, Figma `24:5717` 기준으로 크리에이터 이름과 순위를 포함한 문장형 메시지를 표시한다.
- ranking 문장 안의 순위 텍스트는 기존 `FeedRankTextStyler` highlight range로 강조한다.
@@ -326,7 +334,6 @@ PRD 작성 전 확인한 `v2` 패키지 하위 재사용 후보는 다음과 같
- `ChatRoomListUiItem`, `ChatRoomMappers`, `ChatRoomTimeTextFormatter`: 최근 대화 데이터 변환/시간 표시 후보.
#### Feed/News
- `FeedAdapter`: rank/live/content/community variant를 가진 feed list 후보.
- `FeedRankView`, `FeedContentView`, `FeedCommunityView`: 최근 소식 타입별 카드 후보.
- `FeedItem`, `FeedContentCategory`, `FeedRankHighlight`: 최근 소식 UI model 후보.

View File

@@ -0,0 +1,88 @@
# Phase 1 기존 구조와 작업 경계 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 1 / Task 1.1~1.3 |
| 기준 commit 또는 working tree | `f600d10e8b63b6fdc812b18a874b077dfe072ca8` + 2026-07-31 working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | `docs/20260625_메인_홈_팔로잉_탭/prd.md`, `docs/20260625_메인_홈_팔로잉_탭/plan-task.md` |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- 기존 홈 탭 삽입 지점, 재사용 후보, 제외 범위가 이후 구현과 일치하는지 확인한다.
- 레거시 직접 수정이나 추천·랭킹 리팩터링으로 범위가 확장되지 않았는지 확인한다.
### 포함 범위
- `HomeMainFragment`, `fragment_v2_main_home.xml`, v2 홈 widget/mapper와 Phase 1 문서 기록.
### 제외 범위
- Phase 2 이후 구현의 동작 상세와 Figma 실기기 대조.
## 3. 판정 기준
- 심각도와 상태는 `docs/sample/sample-review.md`의 Blocker/High/Medium/Low 및 확정/오탐/보류 기준을 적용했다.
- 완료 체크박스, 실제 코드 위치, 후속 Phase가 유지한 경계를 상호 대조했다.
## 4. 검토한 근거
### 문서와 코드
- `HomeMainFragment.kt:85-168`, `366-395`
- `fragment_v2_main_home.xml:9-34`, `227-393`
- `HomeCreatorProfileImageLoader`, `ChatRoomListItemResponse.toUiItems()`, `CreatorActivityType`
- Phase 1 Task 1.1~1.3과 PRD Non-Goals
### 실행 환경
```text
OS: Darwin 25.0.0 x86_64
JDK: OpenJDK 17.0.15
Gradle wrapper: 8.14.3
환경 변수: 민감정보 확인/출력 없음
```
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `rg -n "HOME_TAB_FOLLOWING|showHomeTab|HomeCreatorProfileImageLoader|CreatorActivityType" ...` | 성공 | 홈 탭 분기와 재사용 경로 확인 |
| Phase 1 문서와 현재 파일 구조 대조 | 성공 | 신규 팔로잉 코드는 `v2.main.home` 하위, 레거시는 호출만 사용 |
## 5. 발견 사항 요약
확정 발견 사항 없음.
## 6. 발견 사항 상세
해당 없음. 이후 별도 작업에서 도입된 로그인 가드와 pull refresh는 각각의 PRD/계획으로 관리되고 있어 Phase 1 범위 위반으로 판정하지 않았다.
## 7. 확정 항목의 plan·goal 전환
전환 항목 없음.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | Task 1.1~1.3과 현재 구조 대조 완료 |
| 후보 항목 판정 완료 | 충족 | 범위 확장 후보를 후속 문서 근거로 오탐 판정 |
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
| 검증 명령과 결과 기록 | 충족 | 검색·정적 대조 기록 |
**최종 결론:** 확정 발견 사항 없음
**남은 항목:** 없음
## 9. 수정 후 검증 기록
- 2026-07-31 재점검: Phase 1 경계와 현재 파일 구조를 다시 대조했고 기존 판정을 유지했다. Phase 1~13 focused 회귀 15개 suite/113개 test, resource merge, Kotlin compile, ktlint PASS.
- 2026-07-31 최종 검증: 현재 경계·파일 구조 판정을 유지했다. 통합 20개 suite·306개 test, resource merge, Kotlin compile, ktlint, Gradle task 목록, staged/unstaged diff 검사가 모두 PASS했다.
- 2026-07-31 이번 리뷰 재검증: Phase 1의 홈 탭 삽입 경계, v2 재사용 경로와 레거시 비수정 원칙을 현재 working tree에서 다시 대조했다. 통합 16개 suite·120개 test, resource merge, Kotlin compile, ktlint, staged/unstaged diff 검사가 PASS해 기존 “확정 발견 사항 없음” 판정을 유지한다.

View File

@@ -0,0 +1,178 @@
# Phase 10 On Air 라이브 item 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 10 / Task 10.1~10.4 |
| 기준 commit 또는 working tree | `f600d10e8b63b6fdc812b18a874b077dfe072ca8` + 2026-07-31 working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | 대상 `prd.md`, `plan-task.md` |
| 리뷰 상태 | 수정 검증 완료 |
## 2. 리뷰 목적과 범위
### 목적
- 263dp × 100dp 캡슐 item의 필드·배경·profile binding과 가로 목록 배치를 확인한다.
### 포함 범위
- `item_home_following_live.xml`, `bg_home_following_live_capsule.xml`
- `HomeFollowingLiveAdapter.kt`, following RecyclerView와 source test
### 제외 범위
- item 터치 이동은 Phase 4 `REV-P4-001`에서 별도 판정.
## 3. 판정 기준
- 캡슐 내부 표현은 Task 10.1, 가로 item 간격은 PRD UX의 item spacing과 기존 홈 helper 관례로 판정했다.
## 4. 검토한 근거
- `item_home_following_live.xml:2-99`
- `bg_home_following_live_capsule.xml:2-8`
- `HomeFollowingLiveAdapter.kt:23-31`
- `HomeRecyclerItemLayoutParams.kt:7-14`
- creator/chat adapter의 동일 horizontal item layout parameter 사용
- `item_home_following_live.xml:5-6`: root `263dp x 100dp`
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| 팔로잉 전체 회귀 | 성공 | 39개 테스트 실패 0 |
| resource merge/compile/ktlint | 성공 | 모두 PASS |
| adapter layout parameter 수동 대조 | 문제 확인 | live adapter만 end margin helper 미사용 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P10-001` | Low | 수정 완료 | 연속된 On Air 캡슐 item 사이 간격이 없음 | Task 10.2 | `P10-R1` 완료 |
| `REV-P10-002` | Low | 수정 완료 | On Air margin을 source 문자열 테스트로 고정해 UI 표현 속성 테스트 금지 규칙을 위반함 | Task 10.3 | `P10-R2` 완료 |
| `REV-P10-003` | Medium | 수정 완료 | 간격 helper가 On Air root의 `263dp` 폭을 `WRAP_CONTENT`로 덮음 | Task 10.4 | `P10-R3` 완료 |
## 6. 발견 사항 상세
### REV-P10-001 — On Air 가로 item end margin 누락
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** PRD 가로 리스트 item 간격, Task 10.1 Figma 간격
- **소유 Task:** Task 10.2
**관찰 내용**
live adapter는 XML을 inflate한 뒤 layout parameter를 그대로 반환한다. RecyclerView에는 ItemDecoration이 없어 두 번째 item부터 캡슐이 바로 붙는다. creator/chat adapter는 공통 helper로 `spacing_12` end margin을 적용한다.
**근거**
- 코드: `HomeFollowingLiveAdapter.kt:23-31`
- 비교: `HomeFollowingCreatorAdapter.kt:28-30`, `HomeFollowingChatAdapter.kt:23-26`
- helper: `HomeRecyclerItemLayoutParams.kt:7-14`
**영향**
On Air item이 2개 이상일 때 Figma/기존 홈 가로 리스트와 다른 비핵심 UI가 노출된다.
**권장 조치**
신규 decoration 없이 기존 `recyclerItemLayoutParams(parent)`를 재사용한다.
**판정 기록**
- 2026-07-30 — RecyclerView/adapter/helper 경로 대조로 확정.
- 2026-07-30 — Task 10.2에서 기존 helper를 적용하고 resource/compile/회귀 검증을 완료.
### REV-P10-002 — On Air 간격 source test가 테스트 정책을 위반함
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** `docs/agent-guides/code-style.md` 테스트 관례
- **소유 Task:** Task 10.3
**관찰 내용**
Task 10.2에서 추가한 `following live adapter keeps horizontal item spacing``recyclerItemLayoutParams(parent)` 문자열을 검사해 item margin이라는 UI 표현 속성을 고정한다. 현재 가이드는 View 크기·margin·padding 같은 UI 표현 속성 테스트를 새로 작성하지 않도록 규정한다.
**근거**
- 테스트: `HomeFollowingFragmentSourceTest.kt:355-363`
- 가이드: `docs/agent-guides/code-style.md`의 “신규 테스트는 로직에 대한 테스트 코드만 작성” 및 UI margin 테스트 금지 규칙
- production: `HomeFollowingLiveAdapter.kt:23-30`의 helper 적용은 현재 유지되어야 함
**영향**
기능에는 영향이 없지만 금지된 테스트 유형이 회귀 Gate에 추가되고, 내부 구현 문자열 변경만으로 불필요한 실패가 발생한다.
**권장 조치**
해당 신규 source test만 제거하고 adapter의 helper 적용은 유지한다. 이 UI 간격은 `rg` 정적 대조, resource merge, compile, 수동 화면 확인으로 검증한다.
**판정 기록**
- 2026-07-30 — 신규 테스트 diff와 현재 코드 스타일 가이드를 대조해 확정.
- 2026-07-30 — Task 10.3에서 금지된 UI margin source test만 제거하고 helper 재사용 대체 검증을 완료.
### REV-P10-003 — 간격 보완이 On Air 고정 폭을 보존하지 않음
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** Task 10.1 `263dp x 100dp`, Task 10.2 “기존 XML 크기 유지”
- **소유 Task:** Task 10.4
**관찰 내용**
Task 10.2는 `recyclerItemLayoutParams(parent)`를 적용해 XML 크기를 유지했다고 기록했지만, helper는 새 `RecyclerView.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)`를 반환한다. 따라서 inflate된 root의 `263dp x 100dp` 중 width 계약이 런타임에 `WRAP_CONTENT`로 교체된다.
**근거**
- layout: `item_home_following_live.xml:5-6`
- adapter: `HomeFollowingLiveAdapter.kt:23-30`
- helper: `HomeRecyclerItemLayoutParams.kt:7-14`
- 문서: Task 10.2 완료 증거와 검증 기록
**영향**
On Air 캡슐의 폭이 자식 content 측정값에 따라 축소되거나 달라져 제목/상태 영역과 연속 item 배치가 Figma와 불일치할 수 있다.
**권장 조치**
inflate된 기존 layout parameter의 width/height는 보존하고 `spacing_12` end margin만 추가한다. UI width/margin 자동 테스트는 다시 추가하지 않고 정적 대조·resource merge·compile·Task 5.3 수동 확인으로 검증한다.
**판정 기록**
- 2026-07-30 — XML root 크기와 adapter/helper의 최종 layout parameter를 대조해 확정.
- 2026-07-30 — 구현 전 Phase 10 Task 10.4 / `P10-R3`로 전환.
- 2026-07-30 — Task 10.4에서 inflate된 root의 기존 layout parameter를 보존하고 end margin만 적용하도록 수정했다.
## 7. 확정 항목의 plan·goal 전환
- Phase 10에 Task 10.2 / `P10-R1`을 추가해 완료했다.
- 재점검에서 Task 10.3 / `P10-R2`를 추가해 완료했다.
- 추가 재점검에서 Task 10.4 / `P10-R3`를 추가해 완료했다.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | layout/drawable/adapter 대조 |
| 후보 항목 판정 완료 | 충족 | REV-P10-001~003 판정 완료 |
| 확정 항목 plan 반영 | 충족 | Task 10.2, Task 10.3, Task 10.4 |
| 검증 명령과 결과 기록 | 충족 | 빌드 Gate PASS |
**최종 결론:** 확정 발견 사항 모두 수정 완료
**남은 항목:** 없음
## 9. 수정 후 검증 기록
- 2026-07-30: `HomeFollowingLiveAdapter.onCreateViewHolder()`에서 기존 horizontal list helper `recyclerItemLayoutParams(parent)`를 재사용해 XML 크기는 유지하고 `spacing_12` end margin을 적용했다. `HomeFollowingFragmentSourceTest`, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint, `git diff --check` PASS.
- 2026-07-30: Task 10.3으로 `following live adapter keeps horizontal item spacing` source test만 제거하고 production helper 적용은 유지했다. `rg``HomeFollowingLiveAdapter``recyclerItemLayoutParams(parent)` 재사용을 대조했고 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint, `git diff --check` PASS.
- 2026-07-30: Task 10.4로 `HomeFollowingLiveAdapter``recyclerItemLayoutParams(parent)` 대신 inflate된 `RecyclerView.LayoutParams`를 보존하고 `spacing_12` end margin만 적용하도록 수정했다. focused test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint PASS. 연결 기기가 없어 실기기 확인은 Task 5.3 보류 상태를 유지했다.
- 2026-07-31 재점검: On Air binding/action을 포함한 Phase 1~13 focused 113개 test, resource merge, Kotlin compile, ktlint PASS. 연결 기기가 없어 실기기 폭/간격 확인은 Task 5.3에 유지했다.
- 2026-07-31 최종 검증: On Air binding/action 회귀를 포함한 통합 20개 suite·306개 test와 resource merge, Kotlin compile, ktlint가 PASS했다. 추가 확정 수정 사항은 없고 실기기 확인은 Phase 5 Task 5.3에 유지한다.
- 2026-07-31 이번 리뷰 재검증: On Air 캡슐의 XML 고정 크기 보존, end margin과 live action 경로를 다시 추적했다. 통합 16개 suite·120개 test, resource merge, Kotlin compile, ktlint가 PASS해 `REV-P10-001~003` 수정 완료 판정을 유지하고 시각 대조는 Phase 5 Task 5.3에 유지한다.

View File

@@ -0,0 +1,77 @@
# Phase 11 COMMUNITY_POST 상세 이동 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 11 / Task 11.1~11.3 |
| 기준 commit 또는 working tree | `f600d10e8b63b6fdc812b18a874b077dfe072ca8` + 2026-07-31 working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | 대상 `prd.md`, `plan-task.md`, 공통 community action 문서 |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- COMMUNITY_POST가 유효한 `postId`로 상세 이동하고 legacy extra 조립을 사용하지 않는지 확인한다.
- 후속 공통 도메인 액션 리팩터링이 Phase 11 의도를 유지하는지 확인한다.
### 포함 범위
- `HomeMainFragment.onFollowingNewsClick()`
- `CommunityActionCommand.PostDetail`, handler와 source test
### 제외 범위
- 상세 Activity 내부 데이터 로딩과 다른 news type 이동.
## 3. 판정 기준
- 유효 ID 검증, Login access, `CreatorChannelCommunityDetailActivity` intent 생성과 legacy 참조 부재를 기준으로 판정했다.
## 4. 검토한 근거
- `HomeMainFragment.kt:673-686`
- `CommunityAction.kt:5-20`
- `CommunityActionHandler.kt:13-42`
- `HomeMainFragmentLoginGuardSourceTest.kt:53-76`
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| `HomeMainFragmentLoginGuardSourceTest` | 성공 | source 계약 PASS |
| 팔로잉 전체 회귀/compile/ktlint/diff | 성공 | 모두 PASS |
## 5. 발견 사항 요약
확정 발견 사항 없음.
## 6. 발견 사항 상세
`postId <= 0L`은 Fragment와 공통 action에서 모두 무시되고, 유효 ID만 공통 Login access 후 `CreatorChannelCommunityDetailActivity`로 이동한다. Phase 11의 직접 intent 표현은 후속 공통 action 리팩터링으로 대체되었지만 사용자 동작과 보안 정책은 유지된다.
## 7. 확정 항목의 plan·goal 전환
전환 항목 없음.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | Fragment→Community Action→Activity 추적 |
| 후보 항목 판정 완료 | 충족 | 추가 후보 없음 |
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
| 검증 명령과 결과 기록 | 충족 | focused/통합 PASS |
**최종 결론:** 확정 발견 사항 없음
**남은 항목:** 없음
## 9. 수정 후 검증 기록
- 2026-07-31 재점검: COMMUNITY_POST 공통 action과 ID guard를 다시 추적했고 Phase 1~13 focused 113개 test, resource merge, Kotlin compile, ktlint PASS. 추가 확정 발견 사항 없음.
- 2026-07-31 최종 검증: COMMUNITY_POST route 회귀를 포함한 통합 20개 suite·306개 test와 Kotlin compile, ktlint가 PASS했다. 추가 확정 발견 사항 없음.
- 2026-07-31 이번 리뷰 재검증: `COMMUNITY_POST`의 양수 `postId` 가드와 공통 Community Action 이동을 다시 추적했다. 통합 16개 suite·120개 test, Kotlin compile, ktlint가 PASS해 기존 “확정 발견 사항 없음” 판정을 유지한다.

View File

@@ -0,0 +1,111 @@
# Phase 12 전체 empty 상태 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 12 / Task 12.1~12.4 |
| 기준 commit 또는 working tree | `f600d10e8b63b6fdc812b18a874b077dfe072ca8` + 2026-07-31 working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | 대상 `prd.md`, `plan-task.md` |
| 리뷰 상태 | 수정 검증 완료 |
## 2. 리뷰 목적과 범위
### 목적
- Empty에서만 중앙 문구를 표시하고 Content/LoginRequired/Error에서는 숨기는지 확인한다.
- 한국어·영어·일본어 문구와 resource merge를 확인한다.
### 포함 범위
- `fragment_v2_main_home.xml`, `HomeMainFragment` empty/content binding, 3개 locale string, mapper Empty 조건과 source test.
### 제외 범위
- 로그인 유도 문구·CTA와 실기기 typography 시각 대조.
## 3. 판정 기준
- PRD의 전체 section empty 조건, fillViewport surface, locale별 문구와 visibility 분기를 기준으로 판정했다.
## 4. 검토한 근거
- `HomeFollowingUiState.kt:8-23`
- `HomeFollowingMappers.kt:18-35`
- `fragment_v2_main_home.xml:237-277`
- `HomeMainFragment.kt:397-419`, `503-524`
- `HomeFollowingFragmentSourceTest.kt:104-147`
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| mapper/source/팔로잉 전체 회귀 | 성공 | 관련 테스트 실패 0 |
| resource merge/compile/ktlint/diff | 성공 | 모두 PASS |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P12-001` | Low | 수정 완료 | empty source test가 금지된 크기·정렬·visibility 표현을 고정함 | Task 12.4 | `P12-R1` 완료 |
## 6. 발견 사항 상세
모든 표시 section이 비었을 때만 mapper가 `Empty`를 만들고, Fragment는 empty 문구를 표시하면서 adapter와 section을 비운다. Content/LoginRequired/Error의 숨김 경로와 세 locale 문구도 요구사항과 일치한다.
### REV-P12-001 — empty source test가 UI 표현 속성 정책을 위반함
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** `docs/agent-guides/code-style.md` 테스트 관례
- **소유 Task:** Task 12.4
**관찰 내용**
`following empty state shows centered reusable empty string only for all empty content` 테스트는 XML width/height/gravity/textAlignment/visibility와 Fragment의 직접 `View.VISIBLE/GONE` 문자열을 assertion한다. 이 테스트는 UI 크기·constraint·visibility 테스트 금지 규칙이 추가된 뒤 생성되었다.
**근거**
- 테스트: `HomeFollowingFragmentSourceTest.kt:107-150`
- 가이드: `docs/agent-guides/code-style.md:42-43`
- 이력: 가이드 규칙 `566f76df` 이후 테스트 `2c609836`, locale 문자열 assertion `dadf3a51` 추가
**영향**
mapper의 Empty 판정과 locale 문구라는 유효한 계약에 UI 배치 구현 문자열이 섞여 있어, 디자인 조정이 기능 회귀처럼 실패하고 현재 테스트 정책과 불일치한다.
**권장 조치**
mapper Empty 조건·locale 문구·상태 분기 검증은 유지하고 width/height/정렬/직접 visibility assertion만 제거한다. 중앙 배치는 정적 대조와 Task 5.3 실기기 확인으로 검증한다.
**판정 기록**
- 2026-07-30 — 테스트 추가 시점과 assertion 범위를 현행 가이드와 대조해 확정.
- 2026-07-30 — 구현 전 Phase 12 Task 12.4 / `P12-R1`로 전환.
- 2026-07-30 — Task 12.4에서 UI 표현 source assertion만 제거하고 locale 문구와 상태 분기 검증을 유지했다.
## 7. 확정 항목의 plan·goal 전환
- Phase 12에 Task 12.4 / `P12-R1`을 추가해 완료했다.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | mapper→layout→binding→resource 추적 |
| 후보 항목 판정 완료 | 충족 | REV-P12-001 확정 |
| 확정 항목 plan 반영 | 충족 | Task 12.4 |
| 검증 명령과 결과 기록 | 충족 | focused/통합 PASS |
**최종 결론:** 확정 발견 사항 모두 수정 완료
**남은 항목:** 실기기 중앙 정렬 대조는 Phase 5 Task 5.3에 포함
## 9. 수정 후 검증 기록
- 2026-07-30: Task 12.4로 empty source test명을 한글화하고 width/height/gravity/textAlignment/visibility 및 직접 `View.VISIBLE/GONE` 문자열 assertion을 제거했다. 3개 locale 문구와 `HomeFollowingUiState.Empty` 상태 분기 검증은 유지했다. focused test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint PASS.
- 2026-07-31 재점검: Empty mapper/state/locale 회귀를 포함한 Phase 1~13 focused 113개 test, resource merge, Kotlin compile, ktlint PASS. 연결 기기가 없어 중앙 정렬 실기기 확인은 Task 5.3에 유지했다.
- 2026-07-31 최종 검증: empty state·locale 회귀를 포함한 통합 20개 suite·306개 test와 resource merge, Kotlin compile, ktlint가 PASS했다. 추가 확정 수정 사항은 없고 실기기 중앙 정렬 확인은 Phase 5 Task 5.3에 유지한다.
- 2026-07-31 이번 리뷰 재검증: 전체 section empty mapper 조건, 세 locale 문구와 상태별 표시 분기를 다시 대조했다. 통합 16개 suite·120개 test, resource merge, Kotlin compile, ktlint가 PASS해 `REV-P12-001` 수정 완료 판정을 유지하고 중앙 정렬 시각 대조는 Phase 5 Task 5.3에 유지한다.

View File

@@ -0,0 +1,87 @@
# Phase 13 CREATOR_RANKING·AUDIO_CONTENT 이동 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 13 / Task 13.1~13.3 |
| 기준 commit 또는 working tree | `f600d10e8b63b6fdc812b18a874b077dfe072ca8` + 2026-07-31 working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | 대상 `prd.md`, `plan-task.md`, 공통 creator/content/community action |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- 미커밋 Phase 13 변경이 요청된 두 news type만 기존 공통 액션으로 이동시키는지 확인한다.
- 기존 COMMUNITY_POST와 명시적 no-op인 CONTENT_RANKING/PHOTO_CONTENT를 회귀시키지 않는지 확인한다.
### 포함 범위
- `HomeMainFragment.onFollowingNewsClick()`
- `HomeMainFragmentLoginGuardSourceTest`
- `CreatorAction`, `ContentAction`, `CommunityAction`의 ID/access 정책
- Phase 13 PRD/Task/검증 기록
### 제외 범위
- CONTENT_RANKING/PHOTO_CONTENT 신규 이동, API/DTO/mapper/UI 변경, 실제 기기 터치.
## 3. 판정 기준
- type 판별, payload target ID, 공통 액션 재사용, invalid ID 무시와 기존 COMMUNITY_POST 유지 여부를 기준으로 판정했다.
## 4. 검토한 근거
- working diff의 `HomeMainFragment.kt:673-687`
- `HomeMainFragmentLoginGuardSourceTest.kt:53-104`
- `CreatorAction.kt`, `ContentAction.kt`, `CommunityAction.kt`의 양수 ID 검증과 access 정책
- Phase 13 Task 13.1~13.3
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| `HomeMainFragmentLoginGuardSourceTest` | 성공 | 13개 테스트 실패 0 |
| 팔로잉 전체 회귀 | 성공 | 39개 테스트 실패 0 |
| resource merge/compile/ktlint/diff | 성공 | 모두 PASS |
| `adb devices` | 기기 없음 | 실제 터치 미실행 |
## 5. 발견 사항 요약
확정 발견 사항 없음.
## 6. 발견 사항 상세
- `CREATOR_RANKING``CreatorActionCommand.Profile(item.targetId)`로 전달된다.
- `AUDIO_CONTENT``ContentActionCommand.AudioDetail(item.contentId)`로 전달된다.
- 두 공통 action은 양수 ID와 access requirement를 자체 검증한다.
- COMMUNITY_POST의 `postId` 가드와 상세 이동은 유지된다.
- CONTENT_RANKING/PHOTO_CONTENT는 조건 불일치로 no-op을 유지한다.
실기기 터치는 연결 기기 부재로 실행하지 못했지만 Task 13.3이 이 경우 사유 기록을 완료 조건으로 허용하므로 보류 finding으로 전환하지 않았다.
## 7. 확정 항목의 plan·goal 전환
전환 항목 없음.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | working diff와 공통 액션 end-to-end 추적 |
| 후보 항목 판정 완료 | 충족 | 추가 후보 없음 |
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
| 검증 명령과 결과 기록 | 충족 | focused/통합/build PASS |
**최종 결론:** 확정 발견 사항 없음
**남은 항목:** 없음
## 9. 수정 후 검증 기록
- 2026-07-31 재점검: CREATOR_RANKING·AUDIO_CONTENT·COMMUNITY_POST 공통 action 경로를 다시 추적했고 Phase 1~13 focused 113개 test, resource merge, Kotlin compile, ktlint PASS. 추가 확정 발견 사항 없음.
- 2026-07-31 최종 검증: 최근 소식 상세 route 회귀를 포함한 통합 20개 suite·306개 test와 Kotlin compile, ktlint가 PASS했다. 추가 확정 발견 사항 없음.
- 2026-07-31 이번 리뷰 재검증: `CREATOR_RANKING`, `AUDIO_CONTENT`, 기존 `COMMUNITY_POST` 공통 액션과 `CONTENT_RANKING`·`PHOTO_CONTENT` no-op 범위를 다시 추적했다. 통합 16개 suite·120개 test, Kotlin compile, ktlint가 PASS해 기존 “확정 발견 사항 없음” 판정을 유지한다.

View File

@@ -0,0 +1,173 @@
# Phase 2 API·DTO·Repository·mapper 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 2 / Task 2.1~2.8 |
| 기준 commit 또는 working tree | `f600d10e8b63b6fdc812b18a874b077dfe072ca8` + 2026-07-31 working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | 대상 `prd.md`, `plan-task.md`, `docs/agent-guides/code-style.md` |
| 리뷰 상태 | 수정 검증 완료 |
## 2. 리뷰 목적과 범위
### 목적
- endpoint, optional auth header, DTO nested payload, mapper 상태 계약과 locale resource가 현재 PRD를 충족하는지 확인한다.
- Phase 9에서 교체된 최근 소식 계약을 Phase 2의 과거 기록과 혼동하지 않고 현재 계약 기준으로 판정한다.
### 포함 범위
- `HomeFollowingApi.kt`, `HomeFollowingModels.kt`, `HomeFollowingRepository.kt`
- `HomeFollowingAuthHeader.kt`, `HomeFollowingUiState.kt`, `HomeFollowingUiModels.kt`, `HomeFollowingMappers.kt`
- `HomeFollowingAuthHeaderTest`, `HomeFollowingMapperTest`, `HomeFollowingNewsAdapterTest`, 관련 string resource와 DI
### 제외 범위
- Fragment 표시·터치 이동과 서버 `permitAll` 실제 설정.
## 3. 판정 기준
- PRD endpoint/response contract, Gson/Kotlin 관례, mapper의 login/empty/content 분기와 `docs/sample/sample-review.md` 심각도 기준을 적용했다.
## 4. 검토한 근거
### 문서와 코드
- `HomeFollowingApi.kt:8-12`: query 없는 `GET /api/v2/home/following`, nullable header
- `HomeFollowingModels.kt:8-111`: DTO와 nested payload
- `HomeFollowingMappers.kt:18-158`: LoginRequired/Empty/Content, matching payload 필터, locale label
- `HomeFollowingNewsAdapter.kt:155-171`: UI model label을 읽지 않고 `FollowingNewsType.toFeedContentCategory()`로 표시 label을 결정
- `AppDI.kt`: API/Repository 등록
### 실행 환경
```text
OS: Darwin 25.0.0 x86_64
JDK: OpenJDK 17.0.15
Gradle wrapper: 8.14.3
```
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| `./gradlew --no-daemon :app:testDebugUnitTest --tests "...HomeFollowingAuthHeaderTest" --tests "...HomeFollowingMapperTest" --tests "...HomeFollowingViewModelTest"` | 성공 | 전체 20개 중 Phase 2 대상 13개(auth 3, mapper 10) 실패 0 |
| `./gradlew --no-daemon :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` | 성공 | resource/compile/style PASS |
| 2026-07-31 Phase 1~13 통합 focused 회귀 | 성공 | 15개 suite, 113개 test, failures/errors 0 |
| `git diff --check` | 성공 | whitespace 오류 없음 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P2-001` | Low | 수정 완료 | 후속 UI·오류 처리 변경 후 미사용 팔로잉 string resource 3개가 남음 | Task 2.7 | `P2-R1` 완료 |
| `REV-P2-002` | Low | 수정 완료 | Feed category 전환 후 미사용 `Content.labelResId`와 mapper가 남음 | Task 2.8 | `P2-R2` 완료 |
## 6. 발견 사항 상세
- Phase 2 기록의 `rank == null` 필터 문구는 Phase 9 nested payload 계약에서 `rank: Int`로 확정된 현재 계약에 의해 대체되었다. 현재 DTO·fixture·mapper가 동일 계약을 사용하므로 결함이 아닌 과거 기록으로 판정했다.
### REV-P2-001 — 후속 UI·오류 처리 변경 후 미사용 팔로잉 string resource가 남음
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** Task 2.4 string resource, Phase 6 팔로잉 크리에이터 header 제거, Task 3.2 공통 오류 Toast 패턴
- **소유 Task:** Task 2.7
**관찰 내용**
`screen_home_following_creators_title`, `screen_home_following_on_air_title`, `screen_home_following_error``values`, `values-en`, `values-ja`에 모두 선언되어 있지만 production/test/layout에서 참조되지 않는다. On Air 섹션과 상태는 `screen_home_following_on_air`를 사용하고, API 오류는 `common_error_unknown` Toast를 사용한다.
**근거**
- resource: `app/src/main/res/values/strings.xml:311-322`, `values-en/strings.xml:312-323`, `values-ja/strings.xml:312-323`
- code: `HomeMainFragment.kt:627-640`, `HomeFollowingViewModel.kt:73-75`
- 검색: 각 대상 key의 `R.string` / `@string` production/test 참조 0건
**재현 또는 검증 절차**
1. 3개 locale string file에서 위 key 선언을 확인한다.
2. `rg``app/src/main`, `app/src/test`의 resource 참조를 검색한다.
3. 실제 결과는 선언 외 참조 0건이다.
**영향**
런타임 오류는 없지만 번역·유지보수 대상을 불필요하게 늘리고, 후속 작업자가 현재 UI에 필요한 계약으로 오인할 수 있다.
**권장 조치**
3개 locale에서 위 3개 미사용 key만 제거하고, 사용 중인 `screen_home_following_on_air`와 나머지 팔로잉 문자열은 유지한다.
**판정 기록**
- 2026-07-30 — 3개 locale 선언과 production/test/layout 참조를 대조해 확정.
- 2026-07-30 — 코드는 수정하지 않고 Phase 2 Task 2.7 / `P2-R1`로 전환.
### REV-P2-002 — Feed category 전환 후 미사용 label UI model이 남음
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** Task 2.6 최근 소식 UI model/mapper, Task 6.4 Feed 위젯 재사용
- **소유 Task:** Task 2.8
**관찰 내용**
`HomeFollowingNewsUiItem.Content.labelResId`는 mapper가 값을 대입하고 test fixture가 검증하지만 production adapter는 이 필드를 읽지 않는다. 실제 label은 `content.type.toFeedContentCategory()``FeedContentCategory.labelResId`에서 결정된다.
**근거**
- UI model: `HomeFollowingUiModels.kt:74-86`
- mapper: `HomeFollowingMappers.kt:111-128`, `151-158`
- runtime: `HomeFollowingNewsAdapter.kt:155-171`, `FeedContentCategory.kt:6-15`
- 검색: 팔로잉 `Content.labelResId`의 production read 0건, test/fixture만 참조
**재현 또는 검증 절차**
1. `rg -n "labelResId|toLabelResId"` 결과에서 팔로잉 최근 소식 model·mapper·test 참조를 확인한다.
2. `HomeFollowingNewsAdapter.ContentViewHolder.bind()`의 category/label 결정 경로를 추적한다.
3. 실제 결과는 `labelResId`를 읽지 않고 `FeedContentCategory.Audio/Photo`를 사용한다.
**영향**
런타임 표시 오류는 없지만 동일 label 계약이 UI model mapper와 Feed category에 이중으로 남아 후속 문구·category 변경 시 test와 production이 엇갈릴 수 있다.
**권장 조치**
미사용 `Content.labelResId``FollowingNewsType.toLabelResId()`만 제거하고, 실제 표시 경로인 `FeedContentCategory.Audio/Photo`와 locale string 회귀를 유지한다.
**판정 기록**
- 2026-07-31 — production/test 참조와 Feed binding 경로를 대조해 확정.
- 2026-07-31 — production은 수정하지 않고 Phase 2 Task 2.8 / `P2-R2`로 전환.
- 2026-07-31 — Task 2.8에서 미사용 UI model 필드와 mapper를 제거하고 Feed category label 회귀 검증을 완료.
## 7. 확정 항목의 plan·goal 전환
- `plan-task.md` Phase 2에 Task 2.7 / `P2-R1`을 추가했다.
- objective: `[P2-R1]의 REV-P2-001을 최소 resource 삭제로 정리하고 팔로잉 resource 회귀를 방지한다.`
- `plan-task.md` Phase 2에 Task 2.8 / `P2-R2`를 추가했다.
- objective: `[P2-R2]의 REV-P2-002를 미사용 UI model/mapper 제거로 정리하고 Feed category label 회귀를 방지한다.`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | API→Repository→mapper와 resource 확인 |
| 후보 항목 판정 완료 | 충족 | 과거 rank 문구는 오탐, `REV-P2-001~002` 판정 완료 |
| 확정 항목 plan 반영 | 충족 | Task 2.7 / `P2-R1`, Task 2.8 / `P2-R2` 추가 |
| 검증 명령과 결과 기록 | 충족 | focused test 및 빌드 품질 검증 PASS |
**최종 결론:** 확정 발견 사항 모두 수정 완료
**남은 항목:** 없음
## 9. 수정 후 검증 기록
- 2026-07-31: `REV-P2-001` 보완으로 `screen_home_following_creators_title`, `screen_home_following_on_air_title`, `screen_home_following_error`를 3개 locale에서 제거했다. `rg`로 production/test/layout 참조 0건을 확인했고, 사용 중인 `screen_home_following_on_air`, empty, ranking, section title 문자열은 유지했다. `./gradlew --no-daemon :app:mergeDebugResources :app:compileDebugKotlin`, `./gradlew --no-daemon --rerun-tasks :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingMapperTest"`, `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew --no-daemon :app:ktlintCheck`, `git diff --check` PASS.
- 2026-07-31 재점검: Phase 1~13 focused 회귀 15개 suite/113개 test, resource merge, Kotlin compile, ktlint는 PASS했다. `REV-P2-002`는 정적 사용처 대조로 확정했고, 이번 리뷰에서 production/test 코드는 수정하지 않았다.
- 2026-07-31: `REV-P2-002` 보완으로 `HomeFollowingNewsUiItem.Content.labelResId`, mapper 대입, `FollowingNewsType.toLabelResId()`를 제거했다. `PHOTO_CONTENT` 표시는 실제 `FeedContentCategory.Photo`와 locale string을 쓰는 adapter 회귀로 유지했다. mapper/news adapter focused test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint PASS.
- 2026-07-31 최종 검증: DTO·Repository·mapper 회귀를 포함한 통합 20개 suite·306개 test와 resource merge, Kotlin compile, ktlint가 PASS했다. 추가 확정 발견 사항 없음.
- 2026-07-31 이번 리뷰 재검증: endpoint·optional auth·nested payload·mapper·Feed category label 경로를 다시 추적했다. 통합 16개 suite·120개 test, resource merge, Kotlin compile, ktlint, staged/unstaged diff 검사가 PASS했으며 추가 확정 발견 사항은 없다.

View File

@@ -0,0 +1,123 @@
# Phase 3 ViewModel 상태와 API 호출 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 3 / Task 3.1~3.4 |
| 기준 commit 또는 working tree | `f600d10e8b63b6fdc812b18a874b077dfe072ca8` + 2026-07-31 working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | 대상 `prd.md`, `plan-task.md` |
| 리뷰 상태 | 수정 검증 완료 |
## 2. 리뷰 목적과 범위
### 목적
- Loading, success/login-required/error와 optional header 흐름이 구현·테스트에서 일치하는지 확인한다.
- 완료 기록이 주장하는 상태 발행 순서가 실제 assertion으로 보호되는지 확인한다.
### 포함 범위
- `HomeFollowingViewModel.kt`, DI 등록, `HomeFollowingViewModelTest.kt`
### 제외 범위
- Fragment rendering과 pull refresh UI.
## 3. 판정 기준
- 확정 상태 전환 요구사항 위반은 High, 기능은 맞지만 완료 증거가 누락된 검증 문제는 Low로 판정했다.
## 4. 검토한 근거
### 문서와 코드
- `HomeFollowingViewModel.kt:38-78`
- `HomeFollowingViewModelTest.kt:56-65`, `67-128`
- Task 3.1의 “loading 후 content 상태를 발행한다” 테스트 케이스와 Task 3.2 완료 기록
### 실행 환경
```text
OS: Darwin 25.0.0 x86_64
JDK: OpenJDK 17.0.15
Gradle wrapper: 8.14.3
```
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| `./gradlew --no-daemon :app:testDebugUnitTest --tests "...HomeFollowingViewModelTest"` | 성공 | 7개 테스트 실패 0 |
| 테스트 assertion 수동 대조 | 문제 확인 | 첫 테스트는 최종 Content와 `isLoading=false`만 확인하고 `Loading` 이력은 수집하지 않음 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P3-001` | Low | 수정 완료 | Loading → 결과 발행 순서가 테스트 이름과 완료 기록과 달리 검증되지 않음 | Task 3.4 | `P3-R1` 완료 |
## 6. 발견 사항 상세
### REV-P3-001 — Loading → 결과 발행 순서가 테스트로 보호되지 않음
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** Task 3.1 loading 후 content 상태 발행
- **소유 Task:** 신규 Task 3.4
**관찰 내용**
production은 `Loading`을 먼저 설정하지만 테스트는 `loadFollowing()` 완료 후 LiveData의 마지막 값만 읽는다. 테스트가 통과해도 `Loading` 발행이 제거되는 회귀를 잡지 못한다.
**근거**
- 코드: `HomeFollowingViewModel.kt:43-57`
- 테스트: `HomeFollowingViewModelTest.kt:56-65`
- 문서: Task 3.1, Task 3.2 검증 기록
**재현 또는 검증 절차**
1. `loadFollowing은 loading 후 content 상태를 발행한다` 테스트를 확인한다.
2. observer 이력 수집이나 `Loading` assertion이 없는지 대조한다.
3. 실제 결과는 최종 `Content``isLoading=false`만 검증된다.
**영향**
로딩 UI 계약이 회귀해도 Phase 3 focused test와 완료 기록은 계속 PASS할 수 있다.
**권장 조치**
observer가 수집한 상태 이력을 이용해 `Loading`, `Content` 순서를 한 테스트에서 검증한다. production 변경은 필요하지 않으며, 이미 존재하는 동작의 coverage 보강이므로 의도적인 RED를 만들지 않는다.
**판정 기록**
- 2026-07-30 — 테스트와 완료 기준을 직접 대조해 확정.
- 2026-07-30 — Task 3.4에서 observer 상태 이력 assertion을 추가하고 focused/전체 회귀 PASS로 수정 완료.
## 7. 확정 항목의 plan·goal 전환
- `plan-task.md` Phase 3에 Task 3.4 / `P3-R1`을 추가했다.
- objective: `[P3-R1]의 REV-P3-001을 테스트로 고정하고 Phase 3 회귀를 방지한다.`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | ViewModel, DI, test 확인 |
| 후보 항목 판정 완료 | 충족 | REV-P3-001 확정 |
| 확정 항목 plan 반영 | 충족 | Task 3.4 추가 |
| 검증 명령과 결과 기록 | 충족 | focused test 7개 PASS |
**최종 결론:** 수정 검증 완료
**남은 항목:** 없음
## 9. 수정 후 검증 기록
- 2026-07-30: `HomeFollowingViewModelTest`에 observer 상태 이력 assertion을 추가해 `Loading → Content` 발행 순서를 검증했다. production 변경 없이 focused test와 팔로잉 전체 회귀, `git diff --check` PASS.
- 2026-07-31 재점검: ViewModel 7개 test와 Phase 1~13 focused 회귀 113개 test, resource merge, Kotlin compile, ktlint PASS. 추가 확정 발견 사항 없음.
- 2026-07-31 최종 검증: ViewModel 상태 전이를 포함한 통합 20개 suite·306개 test와 Kotlin compile, ktlint가 PASS했다. 추가 확정 발견 사항 없음.
- 2026-07-31 이번 리뷰 재검증: `Loading → Content`, optional auth, login-required, 초기 오류와 refresh fallback 상태를 다시 대조했다. 통합 16개 suite·120개 test와 Kotlin compile·ktlint가 PASS해 추가 확정 발견 사항이 없다.

View File

@@ -0,0 +1,247 @@
# Phase 4 팔로잉 UI surface와 routing 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 4 / Task 4.1~4.9 |
| 기준 commit 또는 working tree | `f600d10e8b63b6fdc812b18a874b077dfe072ca8` + 2026-07-31 working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | 대상 `prd.md`, `plan-task.md`, 후속 pull-refresh/access 문서 |
| 리뷰 상태 | 수정 검증 완료 |
## 2. 리뷰 목적과 범위
### 목적
- 팔로잉 surface 전환, section visibility, adapter click 연결과 상태 observer가 요구사항을 충족하는지 확인한다.
- 후속 pull refresh와 공통 액션 변경을 포함한 현재 실행 경로에서 회귀가 없는지 확인한다.
### 포함 범위
- `HomeMainFragment.kt`, `fragment_v2_main_home.xml`
- 팔로잉 adapter 5개, `HomeFollowingFragmentSourceTest`, `HomeMainFragmentLoginGuardSourceTest`
### 제외 범위
- chevron 실제 목적지와 로그인 유도 UI 디자인.
## 3. 판정 기준
- item 상세 이동 누락은 확정 기능 요구사항 위반으로 High, 제한된 로그인 만료/refresh 조건의 복구 문제는 Medium으로 판정했다.
## 4. 검토한 근거
### 문서와 코드
- PRD `On Air` item 터치 요구사항과 `이달의 스케줄` type/target 이동 요구사항
- `HomeMainFragment.kt:115-122`, `397-419`, `657-687`
- `HomeFollowingLiveAdapter.kt:49-54`, `HomeFollowingScheduleAdapter.kt:53-67`
### 실행 환경
```text
OS: Darwin 25.0.0 x86_64
JDK: OpenJDK 17.0.15
Gradle wrapper: 8.14.3
연결 기기: 없음
```
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| `./gradlew --no-daemon :app:testDebugUnitTest --tests "...HomeFollowingFragmentSourceTest" --tests "...HomeMainFragmentLoginGuardSourceTest"` | 성공 | source test 모두 PASS |
| callback/observer 본문 수동 추적 | 문제 확인 | live/schedule callback은 `Unit`, LoginRequired는 refresh 종료 호출 없음 |
| profile helper·Coil 2.7.0 source·저장소 `dispose()` 사용례 대조 | 문제 확인 | blank 분기가 drawable만 비우고 View 요청을 해제하지 않음 |
| `HomeFollowingFragmentSourceTest` 이름과 테스트명 가이드 대조 | 문제 확인 | 완전한 영문 문장형 테스트명 11개 확인 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P4-001` | High | 수정 완료 | On Air와 스케줄 item 터치가 no-op | Task 4.6 | `P4-R1` 완료 |
| `REV-P4-002` | Medium | 수정 완료 | LoginRequired 응답에서 pull refresh가 종료되지 않음 | Task 4.7 | `P4-R2` 완료 |
| `REV-P4-003` | Medium | 수정 완료 | blank 프로필 이미지 바인딩이 이전 Coil 요청을 해제하지 않음 | Task 4.8 | `P4-R3` 완료 |
| `REV-P4-004` | Low | 수정 완료 | 공용 source test의 영문 문장형 테스트명이 한글 테스트명 규칙과 불일치 | Task 4.9 | `P4-R4` 완료 |
## 6. 발견 사항 상세
### REV-P4-001 — On Air와 스케줄 item 터치가 no-op
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** PRD On Air `item 터치 시 해당 live room/detail`, 스케줄 `type/targetId 목적지`
- **소유 Task:** Task 4.6
**관찰 내용**
adapter click listener는 Fragment callback까지 전달되지만 `onFollowingLiveClick()``onFollowingScheduleClick()` 본문이 `Unit`이라 아무 이동도 발생하지 않는다.
**근거**
- 코드: `HomeMainFragment.kt:119-122`, `669-671`
- 테스트: source test는 callback 존재만 확인하고 실제 액션을 요구하지 않음
- 문서: Task 4.5 결과도 route 없는 skeleton임을 기록
**재현 또는 검증 절차**
1. On Air 또는 schedule item을 bind한다.
2. item root를 터치하면 adapter callback이 Fragment로 전달된다.
3. 실제 결과는 `Unit`; 요구 결과는 기존 공통 Live/Content/Community Action 이동이다.
**영향**
팔로잉 핵심 섹션 두 곳에서 사용자가 상세 콘텐츠로 진입할 수 없다.
**권장 조치**
On Air `liveId``LiveActionCoordinator`, schedule `type/targetId`는 기존 최근 활동 route와 공통 액션을 재사용한다.
**판정 기록**
- 2026-07-30 — PRD와 실제 callback 본문을 대조해 확정.
- 2026-07-30 — Task 4.6에서 공통 Live/Content/Community Action 연결과 회귀 검증을 완료.
### REV-P4-002 — LoginRequired 응답에서 pull refresh가 종료되지 않음
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** 로그인 필요 상태 분기, 후속 홈 pull refresh 완료 정책
- **소유 Task:** Task 4.7
**관찰 내용**
`Content``Empty``finishHomePullRefresh()`를 호출하지만 `LoginRequired`는 section만 숨긴다. 유효하지 않은 token 등으로 refresh API가 LoginRequired를 반환하면 SwipeRefresh spinner와 내부 refresh 추적 상태가 남는다.
**근거**
- 코드: `HomeMainFragment.kt:397-419`
- 비교 경로: `Content` 406, `Empty` 411, `LoginRequired` 413
- 테스트: LoginRequired의 section 숨김만 검사하고 refresh 종료는 검사하지 않음
**재현 또는 검증 절차**
1. 팔로잉 탭에서 pull refresh를 시작한다.
2. API가 `isLoginRequired=true`를 반환한다.
3. 실제 결과는 `bindHomeFollowingEmpty(false)`만 실행된다.
4. 요구 결과는 section 숨김과 refresh 종료다.
**영향**
로그인 만료·서버 세션 불일치 조건에서 refresh UI가 계속 동작 중인 것처럼 남는다.
**권장 조치**
LoginRequired 분기에서 팔로잉 refresh 종료를 보장하는 최소 분기와 회귀 테스트를 추가한다.
**판정 기록**
- 2026-07-30 — observer 분기와 refresh 상태 함수를 추적해 확정.
- 2026-07-30 — Task 4.7에서 LoginRequired refresh 종료 분기와 회귀 검증을 완료.
### REV-P4-003 — blank 프로필 이미지 바인딩이 이전 Coil 요청을 해제하지 않음
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD 팔로잉 크리에이터의 blank/실패 profile fallback, Phase 4 adapter image binding
- **소유 Task:** Task 4.8
**관찰 내용**
팔로잉 creator/live/chat/schedule와 community profile이 재사용하는 `loadHomeCreatorProfileImage()`는 URL이 blank이면 drawable만 비운다. Coil 2.7.0은 View에 연결된 기존 요청을 별도로 유지하므로, 이전 item의 요청이 아직 진행 중이면 완료 시점에 재사용된 View에 이전 이미지가 다시 표시될 수 있다.
**근거**
- 코드: `HomeCreatorProfileImageLoader.kt:10-17`의 blank 분기에 `dispose()`가 없음
- 호출부: `HomeFollowingCreatorAdapter`, `HomeFollowingLiveAdapter`, `HomeFollowingChatAdapter`, `HomeFollowingScheduleAdapter`, `HomeFollowingNewsAdapter`
- 의존성: `app/build.gradle`의 Coil `2.7.0`; 로컬 Coil source의 `ViewTargetRequestManager.dispose()`는 현재 요청을 취소·분리함
- 저장소 정상 사용례: `HomePopularCommunityAdapter.kt:67-70`은 blank image에서 `imageView.dispose()` 후 drawable을 비움
**재현 또는 검증 절차**
1. 재사용 가능한 profile `ImageView`에 완료 전 Coil 요청 A를 연결한다.
2. 같은 ViewHolder에 blank profile URL item B를 바인딩한다.
3. 현재 구현은 drawable만 비우고 요청 A를 유지한다.
4. 요청 A가 뒤늦게 완료되면 item B에 A의 프로필이 표시될 수 있다. 요구 결과는 blank 바인딩 시 요청 A를 해제하고 빈 상태를 유지하는 것이다.
**영향**
빠른 스크롤이나 느린 이미지 응답 조건에서 creator/live/chat/schedule/news item에 다른 사용자의 프로필 이미지가 노출될 수 있다.
**권장 조치**
공통 `loadHomeCreatorProfileImage()`의 blank 분기에서 Coil 요청을 해제한 뒤 drawable을 비운다. 각 adapter를 중복 수정하지 않고 공통 helper 한 곳을 수정하며, Robolectric에서 View 요청 연결 해제를 검증한다.
**판정 기록**
- 2026-07-30 — 공통 helper, 모든 팔로잉 호출부, Coil 2.7.0 요청 manager와 저장소 내 `dispose()` 사용례를 대조해 확정.
- 2026-07-30 — 구현 전 Phase 4 Task 4.8 / `P4-R3`로 전환.
- 2026-07-30 — Task 4.8에서 `loadHomeCreatorProfileImage()` blank 분기에 `dispose()`를 추가하고, Robolectric focused test와 팔로잉 회귀 검증을 완료.
### REV-P4-004 — 공용 source test의 영문 문장형 테스트명이 규칙과 불일치함
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** `docs/agent-guides/build-test-style.md`, `docs/agent-guides/code-style.md`의 신규 Kotlin 테스트명 규칙
- **소유 Task:** 신규 Task 4.9
**관찰 내용**
Phase 4에서 생성되고 후속 Phase가 함께 확장한 `HomeFollowingFragmentSourceTest`에 완전한 영문 문장형 backtick 테스트명 11개가 남아 있다. 테스트 작성 전부터 가이드가 신규 Kotlin 테스트명을 한글 문장으로 작성하도록 규정했지만 별도 한글 설명도 없다.
**근거**
- 테스트: `HomeFollowingFragmentSourceTest.kt``home layout exposes...`부터 `following recent news adapter binds...`까지 영문 문장형 테스트명 11개
- 가이드: `docs/agent-guides/build-test-style.md``docs/agent-guides/code-style.md`의 신규 테스트명 한글 규칙
- 이력: 테스트명 규칙은 `2026-05-28`, 대상 테스트는 `2026-06-25` 이후 추가됨
**재현 또는 검증 절차**
1. ``rg -n 'fun `[A-Za-z]' HomeFollowingFragmentSourceTest.kt``로 대상 이름을 확인한다.
2. 클래스의 완전한 영문 문장형 이름 11개와 가이드의 한글 문장 규칙을 대조한다.
3. 테스트 동작은 통과하지만 명명 규칙 불일치가 재현된다.
**영향**
runtime 동작에는 영향이 없지만 동일 기능 테스트의 언어 관례가 섞이고, 완료된 Phase가 저장소 테스트 규칙을 충족한다는 판정과 어긋난다.
**권장 조치**
assertion과 production 코드는 바꾸지 않고 해당 11개 테스트명만 검증 의도가 드러나는 한글 문장으로 변경한다.
**판정 기록**
- 2026-07-31 — 테스트 생성 이력, 현재 이름과 가이드를 대조해 확정하고 Phase 4 Task 4.9 / `P4-R4`로 전환.
- 2026-07-31 — Task 4.9에서 대상 테스트명 11개를 한글 문장으로 변경하고 focused/팔로잉 회귀 검증을 완료.
## 7. 확정 항목의 plan·goal 전환
- Phase 4에 Task 4.6 / `P4-R1`, Task 4.7 / `P4-R2`를 추가해 완료했다.
- 추가 재점검에서 Task 4.8 / `P4-R3`를 추가했으며 구현·검증을 완료했다.
- 최종 재점검에서 Task 4.9 / `P4-R4`를 추가했으며 테스트명 정리를 완료했다.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | UI→adapter→callback→공통 액션 경로 추적 |
| 후보 항목 판정 완료 | 충족 | REV-P4-001~004 판정 완료 |
| 확정 항목 plan 반영 | 충족 | Task 4.6~4.9 추가 |
| 검증 명령과 결과 기록 | 충족 | focused/source test PASS와 수동 코드 추적 |
**최종 결론:** 확정 발견 사항 모두 수정 완료
**남은 항목:** 없음
## 9. 수정 후 검증 기록
- 2026-07-30: `REV-P4-001` 보완으로 팔로잉 On Air item은 `liveActionCoordinator.enterLiveRoom(item.liveId)`, 스케줄 item은 `CreatorActivityType`별 기존 Live/Content/Community Action을 호출하도록 연결했다. `targetId <= 0L`은 공통 action 호출 전 return하며 직접 `Intent` 또는 legacy extra 조립은 추가하지 않았다.
- 2026-07-30: `REV-P4-002` 보완으로 `HomeFollowingUiState.LoginRequired` 분기에서 section 숨김 후 `finishHomePullRefresh(HOME_TAB_FOLLOWING)`을 호출하도록 변경했다. focused source test, 팔로잉 전체 회귀, `v2.live.action.*`, `v2.content.action.*`, `v2.community.action.*`, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check` PASS.
- 2026-07-30: `REV-P4-003` 보완으로 `HomeCreatorProfileImageLoader.loadHomeCreatorProfileImage()`의 blank URL 분기에서 기존 Coil 요청을 `dispose()`로 해제한 뒤 drawable을 비우도록 변경했다. 구현 전 `HomeCreatorProfileImageLoaderTest` RED, 구현 후 focused GREEN, 팔로잉 전체 회귀와 신규 프로필 테스트, `:app:mergeDebugResources`, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check` PASS.
- 2026-07-31 재점검: Fragment/source·공통 action·프로필 loader를 포함한 Phase 1~13 focused 회귀 113개 test, resource merge, Kotlin compile, ktlint PASS. 추가 확정 발견 사항 없음.
- 2026-07-31 최종 재점검: `HomeFollowingFragmentSourceTest`의 영문 문장형 테스트명 11개를 `REV-P4-004`로 확정하고, production/test 동작을 변경하기 전에 Task 4.9 / `P4-R4`로 전환했다.
- 2026-07-31 최종 검증: 통합 20개 suite·306개 test와 resource merge, Kotlin compile, ktlint가 PASS했다. 자동 검증 성공과 별개로 이름 규칙 위반 `REV-P4-004`는 Task 4.9의 수정 항목으로 유지했다.
- 2026-07-31: `REV-P4-004` 보완으로 `HomeFollowingFragmentSourceTest`의 영문 문장형 테스트명 11개를 한글 문장으로 변경했다. `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingNewsAdapterTest"`, 팔로잉 전체 회귀, `:app:compileDebugKotlin`, `:app:ktlintCheck` PASS.
- 2026-07-31 이번 리뷰 재검증: surface 전환, section 상태, live/chat/schedule/news 공통 액션과 blank profile 요청 해제 경로를 다시 추적했다. 통합 16개 suite·120개 test, resource merge, Kotlin compile, ktlint가 PASS해 `REV-P4-001~004` 수정 완료 판정을 유지한다.

View File

@@ -0,0 +1,107 @@
# Phase 5 통합 검증 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 5 / Task 5.1~5.3 |
| 기준 commit 또는 working tree | `f600d10e8b63b6fdc812b18a874b077dfe072ca8` + 2026-07-31 working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | 대상 `prd.md`, `plan-task.md`, `docs/agent-guides/build-test-style.md` |
| 리뷰 상태 | 판정 완료 / 수동 검증 보류 |
## 2. 리뷰 목적과 범위
### 목적
- Phase 5 자동 Gate를 현재 working tree에서 재실행하고 기록과 일치하는지 확인한다.
- Task 5.3의 실기기 수동 확인 가능 여부를 추정 없이 판정한다.
### 포함 범위
- 팔로잉 전체 unit/source test, resource merge, compile, ktlint, diff check, ADB 연결 확인.
### 제외 범위
- 테스트 계정·서버 데이터를 임의 생성하는 행위와 연결 기기 없는 상태의 수동 화면 성공 추정.
## 3. 판정 기준
- 자동 Gate 실패는 High 이상, 환경이 필요한 미실행 검증은 보류로 판정했다.
## 4. 검토한 근거
### 실행 환경
```text
OS: Darwin 25.0.0 x86_64
JDK: OpenJDK 17.0.15
Gradle wrapper: 8.14.3
ADB: 연결 기기 없음
```
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"` | 성공 | 39개 테스트, 실패 0 |
| `./gradlew --no-daemon :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck` | 성공 | 3개 task PASS |
| `git diff --check` | 성공 | 출력 없음 |
| `adb devices` | 성공 / 기기 없음 | `List of devices attached` 아래 항목 없음 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P5-001` | Low | 보류 | Figma/API 데이터 기반 실기기 수동 화면 검증 미완료 | 기존 Task 5.3 | 기기·계정 준비 후 재개 |
## 6. 발견 사항 상세
### REV-P5-001 — Figma/API 데이터 기반 실기기 수동 화면 검증 미완료
- **심각도:** Low
- **상태:** 보류
- **관련 요구사항:** Task 5.3
- **소유 Task:** 기존 Task 5.3
**관찰 내용**
자동 Gate는 모두 통과했으나 현재 ADB 연결 기기가 없어 실제 팔로잉 데이터 화면, scroll, 겹침, item 터치를 직접 확인하지 못했다.
**영향**
실제 렌더링과 상호작용 문제는 source test와 컴파일만으로 완전히 배제할 수 없다.
**권장 조치**
기기/에뮬레이터와 팔로잉 데이터가 있는 테스트 계정이 준비되면 기존 Task 5.3 체크리스트를 그대로 수행한다.
**판정 기록**
- 2026-07-30 — `adb devices` 결과 기기 없음으로 보류 유지.
- 2026-07-31 — Task 2.7 완료 후 `adb devices`를 재실행했으나 `List of devices attached` 아래 연결 기기가 없어 보류 유지.
## 7. 확정 항목의 plan·goal 전환
신규 전환 항목 없음. 기존 Task 5.3이 보류 범위와 재개 조건을 이미 소유한다.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | 자동 Gate와 ADB 확인 완료 |
| 후보 항목 판정 완료 | 충족 | REV-P5-001 보류 |
| 확정 항목 plan 반영 | 해당 없음 | 신규 확정 수정 없음 |
| 보류 항목의 담당·재개 조건 기록 | 충족 | Task 5.3, 기기·계정 준비 후 재개 |
| 검증 명령과 결과 기록 | 충족 | 39개 테스트 및 빌드 품질 PASS |
**최종 결론:** 외부 조건 대기
**남은 항목:** `REV-P5-001` / Task 5.3
## 9. 수정 후 검증 기록
- 2026-07-31 재점검: Phase 1~13 focused 회귀 15개 suite/113개 test, resource merge, Kotlin compile, ktlint PASS. `adb devices`는 정상 실행됐으나 연결 기기가 없어 `REV-P5-001` / Task 5.3 보류 판정을 유지했다.
- 2026-07-31 최종 검증: 통합 20개 suite·306개 test, resource merge, Kotlin compile, ktlint, Gradle task 목록, staged/unstaged diff 검사가 모두 PASS했다. `adb devices`에는 연결 기기가 없어 `REV-P5-001` / Task 5.3 보류 판정을 유지한다.
- 2026-07-31 이번 리뷰 재검증: 통합 16개 suite·120개 test(failures/errors/skipped 0), resource merge, Kotlin compile, ktlint, `./gradlew --no-daemon tasks --all`, staged/unstaged diff 검사가 PASS했다. `adb devices`에서 기기 `2cec640c34017ece` 연결은 확인했지만 테스트 계정·API 데이터 기반 팔로잉 화면 조작과 Figma 대조를 수행하지 않았으므로 `REV-P5-001` / Task 5.3은 보류 유지한다.

View File

@@ -0,0 +1,227 @@
# Phase 6 Figma 후속 수정 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 6 / Task 6.1~6.8 |
| 기준 commit 또는 working tree | `f600d10e8b63b6fdc812b18a874b077dfe072ca8` + 2026-07-31 working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | 대상 `prd.md`, `plan-task.md` |
| 리뷰 상태 | 수정 검증 완료 / 수동 검증 보류 |
## 2. 리뷰 목적과 범위
### 목적
- 크리에이터·최근 대화 UI 보정과 최근 소식 Feed 위젯 전환이 현재 코드에 유지되는지 확인한다.
- 전환 후 사용되지 않는 전용 resource가 남았는지 확인한다.
- 최근 대화 item의 XML 고정 폭이 adapter binding 이후에도 유지되는지 확인한다.
- creator·최근 대화 source test가 현행 UI 표현 속성 테스트 금지 규칙을 준수하는지 확인한다.
### 포함 범위
- `fragment_v2_main_home.xml`, creator/chat/news adapter와 layout, Feed 위젯 참조, source test.
### 제외 범위
- Phase 7 이후 Feed 내부 spacing과 Phase 9 DTO 계약.
## 3. 판정 기준
- runtime 기능에는 영향이 없지만 잘못된 preview와 obsolete resource를 남긴 문제는 Low로 판정했다.
## 4. 검토한 근거
### 문서와 코드
- `HomeFollowingNewsAdapter.kt:32-55`: Feed layout 3종 사용
- `fragment_v2_main_home.xml:359-368`: 과거 content layout preview 참조
- `item_home_following_news_rank.xml`, `item_home_following_news_content.xml`
- `HomeFollowingFragmentSourceTest.kt:185-218`
- `item_home_following_chat.xml:4`: root width `284dp`
- Task 6.7 수정 전 `HomeFollowingChatAdapter``HomeRecyclerItemLayoutParams.kt:7-14`: inflate된 width를 `WRAP_CONTENT`로 교체
- 현재 `HomeFollowingChatAdapter.kt:23-27`: inflate된 `RecyclerView.LayoutParams`의 width/height를 보존하고 end margin만 적용
- `HomeFollowingFragmentSourceTest.kt:292-360`: creator/chat XML 크기·orientation·style·maxLines·ellipsize 문자열 assertion
- `docs/agent-guides/code-style.md`: View 크기·margin·padding·constraint·visibility 등 UI 표현 속성 테스트 금지
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| `rg -n "item_home_following_news_(rank|content)" ...` | 성공 | runtime adapter 참조 없음, preview/doc/test만 존재 |
| `./gradlew --no-daemon :app:testDebugUnitTest --tests "...HomeFollowingFragmentSourceTest"` | 성공 | 16개 테스트 실패 0 |
| resource merge/compile/ktlint | 성공 | BUILD SUCCESSFUL |
| Task 6.7 수정 후 chat layout parameter 정적 대조 | 성공 | XML `284dp` width를 보존하고 end margin만 추가 |
| 2026-07-31 Phase 1~13 통합 focused 회귀 | 성공 | 15개 suite, 113개 test, failures/errors 0 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P6-001` | Low | 수정 완료 | Feed 전환 전 최근 소식 layout 2개와 preview 참조가 남음 | Task 6.5 | `P6-R1` 완료 |
| `REV-P6-002` | Low | 수정 완료 | 삭제된 최근 소식 layout이 상단 파일 구조에 생성 대상으로 남음 | Task 6.6 | `P6-R2` 완료 |
| `REV-P6-003` | Medium | 수정 완료 | 최근 대화 item의 XML `284dp` 폭이 런타임 `WRAP_CONTENT`로 덮임 | Task 6.7 | `P6-R3` 완료 |
| `REV-P6-004` | Low | 수정 완료 | creator·최근 대화 UI 표현 속성을 source 문자열로 검증함 | Task 6.8 | `P6-R4` 완료 |
## 6. 발견 사항 상세
### REV-P6-001 — Feed 전환 전 최근 소식 layout이 남음
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** Task 6.4 Feed 위젯 재사용
- **소유 Task:** Task 6.5
**관찰 내용**
runtime adapter는 `view_feed_rank/content/community`만 inflate하지만 과거 전용 news layout 두 개가 유지되고, RecyclerView preview는 그중 하나를 계속 가리킨다.
**근거**
- 코드: `HomeFollowingNewsAdapter.kt:38-55`
- resource: `fragment_v2_main_home.xml:368`
- 검색: `item_home_following_news_rank`는 runtime 참조 없음
**영향**
Layout Editor preview와 실제 UI가 다르고, 유지보수자가 사용 중인 resource로 오해할 수 있다.
**권장 조치**
preview를 `view_feed_content`로 바꾸고 obsolete layout 2개를 삭제한다.
**판정 기록**
- 2026-07-30 — 전체 resource 참조 검색으로 확정.
- 2026-07-30 — Task 6.5에서 preview 교체와 obsolete layout 삭제 후 resource/회귀 검증을 완료.
### REV-P6-002 — 삭제된 최근 소식 layout이 파일 구조에 남음
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** Task 6.5, 계획 문서 범위 변경 선반영 규칙
- **소유 Task:** Task 6.6
**관찰 내용**
Task 6.5와 현재 resource는 `item_home_following_news_rank.xml`, `item_home_following_news_content.xml`을 삭제했지만, `plan-task.md` 상단 `파일 구조`는 두 파일을 여전히 Create 대상으로 설명한다.
**근거**
- 문서: `plan-task.md` 상단 `파일 구조`와 Task 6.5
- 코드: `HomeFollowingNewsAdapter``view_feed_rank`, `view_feed_content`, `view_feed_community`를 사용
- resource: 두 obsolete layout은 현재 working tree에서 삭제 상태
**영향**
후속 작업자가 삭제된 resource를 현재 필수 파일로 오인하거나 다시 생성할 수 있다.
**권장 조치**
상단 파일 구조를 현재 Feed 위젯 재사용 구조에 맞추되 Task 6.5의 삭제 이력은 유지한다.
**판정 기록**
- 2026-07-30 — 현재 파일 목록, adapter 참조, 계획 문서 상단 목록을 대조해 확정.
- 2026-07-30 — Task 6.6에서 상단 파일 구조를 Feed layout 재사용/obsolete layout 삭제 이력 기준으로 갱신하고 대체 검증을 완료.
### REV-P6-003 — 최근 대화 고정 폭이 adapter에서 보존되지 않음
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** Task 6.2의 Figma `284dp` box card, Task 6.3의 폭 보정 기록
- **소유 Task:** Task 6.7
**관찰 내용**
`item_home_following_chat.xml` root는 `284dp`지만 `HomeFollowingChatAdapter.onCreateViewHolder()`가 inflate 직후 `recyclerItemLayoutParams(parent)`로 전체 layout parameter를 교체한다. 해당 helper는 width와 height를 모두 `WRAP_CONTENT`로 새로 생성하므로 문서에 기록된 “XML 폭 유지”가 실제 코드에서는 성립하지 않는다.
**근거**
- layout: `item_home_following_chat.xml:4-5`
- adapter: `HomeFollowingChatAdapter.kt:23-26`
- helper: `HomeRecyclerItemLayoutParams.kt:7-14`
- 문서: Task 6.3의 최근 대화 item 폭 보정 검증 기록
**영향**
가로 목록에서 카드 폭이 Figma `284dp`가 아니라 자식 content의 wrap-content 측정값으로 결정되어 제목·메시지 영역과 item 간격이 설계와 다르게 보일 수 있다.
**권장 조치**
inflate된 root의 기존 width/height는 유지하고 end margin만 추가한다. 공통 helper 전체 리팩터링 없이 `HomeFollowingChatAdapter` 한 곳을 최소 수정하고, UI 크기 자동 테스트 대신 resource/compile/정적 대조와 Task 5.3 수동 확인을 사용한다.
**판정 기록**
- 2026-07-30 — XML root width, adapter 대입문, helper 반환값을 end-to-end로 대조해 확정.
- 2026-07-30 — 구현 전 Phase 6 Task 6.7 / `P6-R3`로 전환.
- 2026-07-30 — Task 6.7에서 inflate된 `RecyclerView.LayoutParams`의 width/height를 보존하고 end margin만 적용하도록 수정한 뒤 회귀 검증을 완료.
### REV-P6-004 — creator·최근 대화 UI 표현 속성을 source test로 고정함
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** `docs/agent-guides/code-style.md` 테스트 관례, Task 6.1~6.2
- **소유 Task:** Task 6.8
**관찰 내용**
`HomeFollowingFragmentSourceTest`가 creator item의 `75dp` 폭/높이와 typography style, 최근 대화 section의 XML orientation, `284dp`/`62dp` 폭/높이, `maxLines`/`ellipsize`를 파일 문자열로 검증한다. 이는 로직 테스트만 추가하고 UI 레이아웃/표현 속성 테스트를 작성하지 않는 현행 가이드와 맞지 않는다.
**근거**
- test: `HomeFollowingFragmentSourceTest.kt:292-302`, `331-361`
- guide: `docs/agent-guides/code-style.md` “신규 테스트는 로직에 대한 테스트 코드만 작성” 및 UI 표현 속성 검증 금지
- 비교: 동일 리뷰의 Task 7.7, 8.4, 10.3, 12.4는 같은 종류의 assertion을 이미 제거함
**재현 또는 검증 절차**
1. `rg -n 'layout_(width|height)|orientation|maxLines|ellipsize|Typography' HomeFollowingFragmentSourceTest.kt`를 실행한다.
2. Phase 6 creator/chat 테스트 범위에서 XML 표현 값 assertion을 확인한다.
3. production 동작은 정상이지만 테스트 정책 위반은 재현된다.
**영향**
런타임 결함은 아니지만 디자인 값 조정이 로직 회귀 실패로 잘못 표시되고, 가이드에서 정한 수동/Figma 대조 경계를 흐린다.
**권장 조치**
XML 크기·방향·style·말줄임 문자열 assertion만 제거하고, creator header 제거 구조, Direct badge 결정, 상대 시간 formatter, 필수 field 바인딩은 유지한다. production UI 값은 변경하지 않는다.
**판정 기록**
- 2026-07-31 — test source 검색과 현행 가이드를 대조해 확정.
- 2026-07-31 — production/test는 수정하지 않고 Phase 6 Task 6.8 / `P6-R4`로 전환.
- 2026-07-31 — Task 6.8에서 금지된 UI 표현 속성 source assertion만 제거하고 구조·바인딩·클릭 계약 회귀 검증을 완료.
## 7. 확정 항목의 plan·goal 전환
- Phase 6에 Task 6.5 / `P6-R1`을 추가해 완료했다.
- 재점검에서 Task 6.6 / `P6-R2`를 추가해 완료했다.
- 추가 재점검에서 Task 6.7 / `P6-R3`를 추가해 완료했다.
- 2026-07-31 재점검에서 Task 6.8 / `P6-R4`를 추가해 완료했다.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | UI resource와 adapter 참조 검색 |
| 후보 항목 판정 완료 | 충족 | REV-P6-001~004 판정 완료 |
| 확정 항목 plan 반영 | 충족 | Task 6.5~6.8 |
| 검증 명령과 결과 기록 | 충족 | source test 및 build PASS |
**최종 결론:** 확정 발견 사항 모두 수정 완료 / 실기기 검증은 외부 조건 대기
**남은 항목:** Phase 5 Task 5.3 실기기 확인
## 9. 수정 후 검증 기록
- 2026-07-30: 최근 소식 preview `tools:listitem`을 실제 runtime Feed layout인 `@layout/view_feed_content`로 교체하고 obsolete `item_home_following_news_rank.xml`, `item_home_following_news_content.xml`을 삭제했다. `rg` 기준 runtime 참조는 없고 문서/테스트 검증 참조만 남았다. `:app:mergeDebugResources`, `HomeFollowingFragmentSourceTest`, 팔로잉 전체 회귀, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check` PASS.
- 2026-07-30: Task 6.6으로 `plan-task.md` 상단 파일 구조를 현재 resource 구성에 맞춰 갱신했다. obsolete layout 2개는 삭제 이력으로만 남기고, 최근 소식 runtime layout은 `view_feed_rank`, `view_feed_content`, `view_feed_community` 재사용으로 명시했다. `rg` 대조와 `git diff --check` PASS.
- 2026-07-30: Task 6.7로 `HomeFollowingChatAdapter`가 새 `WRAP_CONTENT` layout parameter를 만들지 않고 inflate된 root의 `284dp` width와 height를 보존한 채 `spacing_12` end margin만 적용하도록 수정했다. focused test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint PASS. 연결 기기가 없어 실기기 확인은 Task 5.3 보류 상태를 유지했다.
- 2026-07-31 재점검: Phase 1~13 focused 회귀 15개 suite/113개 test, resource merge, Kotlin compile, ktlint는 PASS했다. `REV-P6-004`는 가이드 대조로 확정했고, 이번 리뷰에서 production/test 코드는 수정하지 않았다.
- 2026-07-31: `REV-P6-004` 보완으로 creator·최근 대화 source test에서 UI 표현 속성 문자열 assertion을 제거했다. creator header 제거, 필수 field id, Direct badge, 상대 시간 formatter, click listener 계약 검증은 유지했다. `rg` 대체 검증, `HomeFollowingFragmentSourceTest`, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint PASS. 연결 기기가 없어 실기기 표현 확인은 Task 5.3 보류 상태를 유지했다.
- 2026-07-31 최종 검증: creator·최근 대화 회귀를 포함한 통합 20개 suite·306개 test와 resource merge, Kotlin compile, ktlint가 PASS했다. 추가 확정 수정 사항은 없고 실기기 확인은 Phase 5 Task 5.3에 유지한다.
- 2026-07-31 이번 리뷰 재검증: creator/chat 고정 폭 보존, Feed 위젯 재사용과 obsolete layout 제거 상태를 다시 대조했다. 통합 16개 suite·120개 test, resource merge, Kotlin compile, ktlint가 PASS해 `REV-P6-001~004` 수정 완료 판정을 유지하고, 시각 대조는 Phase 5 Task 5.3에 유지한다.

View File

@@ -0,0 +1,185 @@
# Phase 7 ranking 문장과 content feed 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 7 / Task 7.1~7.7 |
| 기준 commit 또는 working tree | `f600d10e8b63b6fdc812b18a874b077dfe072ca8` + 2026-07-31 working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | 대상 `prd.md`, `plan-task.md`, code style guide |
| 리뷰 상태 | 수정 검증 완료 |
## 2. 리뷰 목적과 범위
### 목적
- ranking message/highlight와 compact content Feed가 Figma·locale 요구를 충족하는지 확인한다.
- 과거 Task 7.3 ktlint 실패가 현재도 남았는지 재검증한다.
### 포함 범위
- `HomeFollowingNewsAdapter.kt`, `view_feed_content.xml`, `FeedContentView.kt`
- 한국어/영어/일본어 string resource, `FeedViewTest`, following source test
### 제외 범위
- Phase 9 nested payload와 Phase 13 routing.
## 3. 판정 기준
- 특정 locale에서 잘못된 사용자 문자열이 노출되는 문제는 Medium으로 판정했다.
## 4. 검토한 근거
### 문서와 코드
- `HomeFollowingNewsAdapter.kt:74-108`
- `values/strings.xml:318-319`, `values-en/strings.xml:319-320`, `values-ja/strings.xml:319-320`
- `view_feed_content.xml`, `FeedContentView.kt:30-112`
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| following source test + `FeedViewTest` | 성공 | 기존 layout/highlight source 계약 PASS |
| resource/compile/ktlint | 성공 | 현재 working tree에서 Task 7.3 명령 모두 PASS |
| locale message 수동 대입 | 문제 확인 | 영어·일본어 `%2$s``"7위"`가 전달됨 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P7-001` | Medium | 수정 완료 | 영어·일본어 ranking 문장에도 한국어 순위 단위 `"위"`가 노출됨 | Task 7.5 | `P7-R1` 완료 |
| `REV-P7-002` | Low | 수정 완료 | 신규 ranking 테스트명과 단일 실행 가이드가 저장소 규칙을 충족하지 않음 | Task 7.6 | `P7-R2` 완료 |
| `REV-P7-003` | Low | 수정 완료 | 가이드 제정 이후 content feed 크기·padding·margin source test를 추가함 | Task 7.7 | `P7-R3` 완료 |
## 6. 발견 사항 상세
### REV-P7-001 — ranking 순위 단위가 한국어로 고정됨
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** 3개 locale string resource, ranking 문장형 message
- **소유 Task:** Task 7.5
**관찰 내용**
adapter가 locale과 무관하게 `val rankText = "${ranking.rank}위"`를 만든다. 영어와 일본어 문장은 resource로 번역되지만 placeholder에는 한국어 `위`가 그대로 들어간다.
**근거**
- 코드: `HomeFollowingNewsAdapter.kt:77-88`
- resource: 영어 `%1$s reached %2$s...`, 일본어 `ランキング%2$s...`
- 테스트: source 문자열 존재만 검사하고 locale별 결과 문자열은 검증하지 않음
**재현 또는 검증 절차**
1. locale을 English 또는 Japanese로 설정한다.
2. rank 7 item의 message를 생성한다.
3. 실제 결과 placeholder는 `7위`.
4. 요구 결과는 locale별 영어 표기 또는 일본어 `7位`다.
**영향**
영어·일본어 사용자에게 혼합 언어 문장이 노출되고 highlight 대상도 잘못된 단위를 포함한다.
**권장 조치**
rank format을 locale string resource로 분리하고 완성된 rank 문자열의 유효 index/highlight 범위를 테스트한다.
**판정 기록**
- 2026-07-30 — adapter와 세 locale resource를 대입해 확정.
- 2026-07-30 — Task 7.5에서 locale rank format과 highlight 단위 테스트를 추가하고 회귀 검증을 완료.
### REV-P7-002 — 신규 ranking 테스트와 실행 가이드 규칙 미준수
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** `docs/agent-guides/code-style.md` 테스트 관례, `docs/agent-guides/build-test-style.md` 단일 실행 예시 갱신 규칙
- **소유 Task:** Task 7.6
**관찰 내용**
새로 추가된 `HomeFollowingNewsAdapterTest`의 두 backtick 테스트명이 영문 문장이고, 새 테스트 클래스에 대응하는 단일 실행 예시가 `build-test-style.md`에 없다. Task 7.5 Files에는 가이드 수정이 명시되어 있지만 실제 변경에는 포함되지 않았다.
**근거**
- 테스트: `HomeFollowingNewsAdapterTest.kt:23`, `:30`
- 가이드: `code-style.md`의 신규 테스트명 한글 규칙과 테스트 추가 시 실행 예시 갱신 규칙
- 문서: Task 7.5 Files의 `docs/agent-guides/build-test-style.md`
**영향**
기능 동작에는 영향이 없지만 완료 체크리스트와 실제 변경이 불일치하고 신규 테스트 작성 관례가 깨진다.
**권장 조치**
신규 테스트명 2개만 한글화하고 `HomeFollowingNewsAdapterTest` 클래스 단위 실행 예시를 빌드 가이드에 추가한다. 기존 영어 테스트명 일괄 변경은 범위 밖으로 둔다.
**판정 기록**
- 2026-07-30 — 신규 테스트 diff와 현재 가이드 및 Task 7.5 Files를 대조해 확정.
- 2026-07-30 — Task 7.6에서 신규 테스트명 2개를 한글화하고 `HomeFollowingNewsAdapterTest` 실행 예시를 추가했다.
### REV-P7-003 — content feed UI 표현 속성을 source test로 고정함
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** `docs/agent-guides/code-style.md` 테스트 관례
- **소유 Task:** Task 7.7
**관찰 내용**
`FeedViewTest.content layout matches figma compact upload notice dimensions``view_feed_content.xml``padding`, `88dp` width/height, 두 margin 문자열을 직접 assertion한다. 이 테스트는 UI 크기·margin·padding 테스트 금지 규칙이 추가된 뒤 생성되었다.
**근거**
- 테스트: `FeedViewTest.kt:45-64`
- 가이드: `docs/agent-guides/code-style.md:42-43`
- 이력: 가이드 규칙 `566f76df` 이후 테스트 `61fe8a9f` 추가
**영향**
기능 로직이 아닌 Figma 표현값과 XML 구현 문자열이 회귀 Gate에 고정되어, 합법적인 UI 조정이나 구현 방식 변경만으로 테스트가 실패하고 현재 저장소 테스트 정책과 불일치한다.
**권장 조치**
해당 테스트만 제거하고 production layout/View는 유지한다. Figma 크기·간격은 정적 대조, resource merge, compile, Task 5.3 수동 화면 확인으로 검증한다.
**판정 기록**
- 2026-07-30 — test 추가 시점, assertion 내용, 현행 가이드를 대조해 확정.
- 2026-07-30 — 구현 전 Phase 7 Task 7.7 / `P7-R3`로 전환.
## 7. 확정 항목의 plan·goal 전환
- Phase 7에 Task 7.5 / `P7-R1`을 추가해 완료했다.
- 재점검에서 Task 7.6 / `P7-R2`를 추가해 완료했다.
- 추가 재점검에서 Task 7.7 / `P7-R3`를 추가해 완료했다.
- Task 7.3의 과거 ktlint 실패는 현재 재실행에서 PASS했으나 기존 기록과 체크박스는 유지했다.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | adapter, shared Feed, locale resource 확인 |
| 후보 항목 판정 완료 | 충족 | REV-P7-001~003 판정 완료 |
| 확정 항목 plan 반영 | 충족 | Task 7.5, Task 7.6, Task 7.7 |
| 검증 명령과 결과 기록 | 충족 | focused/통합 Gate PASS |
**최종 결론:** 확정 발견 사항 모두 수정 완료
**남은 항목:** 없음
## 9. 수정 후 검증 기록
- 2026-07-30: ranking 순위 단위를 `screen_home_following_ranking_rank_format` locale resource로 분리했다. 한국어는 `7위`, 영어는 `No. 7`, 일본어는 `7位`로 생성하며, 완성 문장에서 같은 문자열 범위를 `FeedRankHighlight`로 전달하도록 `HomeFollowingNewsAdapterTest`로 검증했다. focused test, Feed view test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint, `git diff --check` PASS.
- 2026-07-30 리뷰 게이트 후 보완: subject에 rank 문자열이 먼저 포함되는 경우를 추가 테스트로 고정하고, highlight 시작점을 `lastIndexOf(rankText)`로 변경해 실제 삽입 rank 문자열을 강조하도록 보완했다. RED/GREEN focused test와 최종 회귀 검증 PASS.
- 2026-07-30: Task 7.6으로 `HomeFollowingNewsAdapterTest`의 신규 ranking 테스트명 2개를 한글화하고 `docs/agent-guides/build-test-style.md`에 클래스 단위 실행 예시를 추가했다. focused test, ktlint, `git diff --check` PASS.
- 2026-07-30: Task 7.7로 `FeedViewTest`의 content feed 크기·padding·margin source test만 제거하고 production layout/View 코드는 유지했다. Feed focused test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint PASS.
- 2026-07-31 재점검: ranking locale/highlight와 Feed 회귀를 포함한 Phase 1~13 focused 113개 test, resource merge, Kotlin compile, ktlint PASS. 추가 확정 발견 사항 없음.
- 2026-07-31 최종 검증: ranking·공용 Feed 회귀를 포함한 통합 20개 suite·306개 test와 resource merge, Kotlin compile, ktlint가 PASS했다. 추가 확정 발견 사항 없음.
- 2026-07-31 이번 리뷰 재검증: 세 locale ranking 문장·highlight와 공용 Feed 표시 경로를 다시 대조했다. 통합 16개 suite·120개 test, resource merge, Kotlin compile, ktlint가 PASS해 추가 확정 발견 사항이 없다.

View File

@@ -0,0 +1,110 @@
# Phase 8 팔로잉 크리에이터 전체 버튼 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 8 / Task 8.1~8.4 |
| 기준 commit 또는 working tree | `f600d10e8b63b6fdc812b18a874b077dfe072ca8` + 2026-07-31 working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | 대상 `prd.md`, `plan-task.md` |
| 리뷰 상태 | 수정 검증 완료 |
## 2. 리뷰 목적과 범위
### 목적
- creator가 있을 때 마지막 `전체` item을 추가하고 기존 목록 화면으로 이동하는 계약을 확인한다.
### 포함 범위
- `HomeFollowingCreatorAdapter.kt`, `item_home_following_creator_all.xml`, `HomeMainFragment.openFollowingCreatorAll()`, source test.
### 제외 범위
- `FollowingCreatorActivity` 내부 동작과 creator API pagination.
## 3. 판정 기준
- item count/view type/layout token/공통 로그인 접근 경로를 대조했다.
## 4. 검토한 근거
- `HomeFollowingCreatorAdapter.kt:18-45`, `61-73`
- `item_home_following_creator_all.xml:2-28`
- `HomeMainFragment.kt:115-118`, `689-693`
- `HomeFollowingFragmentSourceTest.kt:304-334`
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| `HomeFollowingFragmentSourceTest` | 성공 | 16개 테스트 실패 0 |
| 팔로잉 전체 회귀 | 성공 | 39개 테스트 실패 0 |
| resource/compile/ktlint/diff | 성공 | 모두 PASS |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P8-001` | Low | 수정 완료 | `전체` item 테스트가 금지된 UI 표현 속성을 source 문자열로 검증함 | Task 8.4 | `P8-R1` 완료 |
## 6. 발견 사항 상세
creator가 비어 있으면 all item도 숨고, 1개 이상이면 마지막 view type으로 추가된다. layout width/높이 spacer/padding/color와 로그인 가드 경로도 요구사항과 일치한다.
### REV-P8-001 — `전체` item 테스트가 UI 표현 속성 정책을 위반함
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** `docs/agent-guides/code-style.md` 테스트 관례
- **소유 Task:** Task 8.4
**관찰 내용**
`following creators list appends all button as last item` 테스트는 adapter의 view type/item count/route 검증과 함께 XML의 width, height, padding, text color, visibility를 직접 assertion한다. 이 테스트는 UI 크기·padding·visibility 테스트 금지 규칙이 추가된 뒤 생성되었다.
**근거**
- 테스트: `HomeFollowingFragmentSourceTest.kt:311-341`
- 가이드: `docs/agent-guides/code-style.md:42-43`
- 이력: 가이드 규칙 `566f76df` 이후 테스트 `658b4f0a` 추가
**영향**
유효한 adapter 로직 검증에 금지된 UI 표현 문자열 검증이 섞여 있어, 단순 디자인/구현 변경도 기능 회귀처럼 실패하고 테스트 정책을 어긴다.
**권장 조치**
adapter view type·item count·route assertion은 유지하고 UI 표현 속성 assertion만 제거한다. 수정하는 테스트명은 한글화하고 XML 값은 정적 대조·resource merge·Task 5.3 수동 확인으로 검증한다.
**판정 기록**
- 2026-07-30 — 테스트 추가 시점과 assertion 범위를 현행 가이드와 대조해 확정.
- 2026-07-30 — 구현 전 Phase 8 Task 8.4 / `P8-R1`로 전환.
- 2026-07-30 — Task 8.4에서 UI 표현 source assertion만 제거하고 adapter item/route 검증을 유지했다.
## 7. 확정 항목의 plan·goal 전환
- Phase 8에 Task 8.4 / `P8-R1`을 추가해 완료했다.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | adapter→layout→route 추적 |
| 후보 항목 판정 완료 | 충족 | REV-P8-001 확정 |
| 확정 항목 plan 반영 | 충족 | Task 8.4 |
| 검증 명령과 결과 기록 | 충족 | focused/통합 PASS |
**최종 결론:** 확정 발견 사항 모두 수정 완료
**남은 항목:** 없음
## 9. 수정 후 검증 기록
- 2026-07-30: Task 8.4로 `전체` item 테스트명을 한글화하고 width/height/padding/visibility/color source assertion만 제거했다. adapter view type, item count, 마지막 item, `FollowingCreatorActivity` route 검증은 유지했다. focused test, 팔로잉 전체 회귀, resource merge, Kotlin compile, ktlint PASS.
- 2026-07-31 재점검: creator all item/route를 포함한 Phase 1~13 focused 113개 test, resource merge, Kotlin compile, ktlint PASS. 추가 확정 발견 사항 없음.
- 2026-07-31 최종 검증: creator 전체 item·route 회귀를 포함한 통합 20개 suite·306개 test와 resource merge, Kotlin compile, ktlint가 PASS했다. 추가 확정 발견 사항 없음.
- 2026-07-31 이번 리뷰 재검증: creator가 있을 때 마지막 `전체` item 추가와 로그인 가드 이동 경로를 다시 추적했다. 통합 16개 suite·120개 test, resource merge, Kotlin compile, ktlint가 PASS해 추가 확정 발견 사항이 없다.

View File

@@ -0,0 +1,168 @@
# Phase 9 최근 소식 nested payload 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 9 / Task 9.1~9.7 |
| 기준 commit 또는 working tree | `f600d10e8b63b6fdc812b18a874b077dfe072ca8` + 2026-07-31 working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | 대상 `prd.md`, `plan-task.md` |
| 리뷰 상태 | 수정 검증 완료 |
## 2. 리뷰 목적과 범위
### 목적
- type별 nested payload DTO, mapper 필터, nullable image, adapter binding과 target ID 보존을 확인한다.
- Task 9.7의 미사용 `FeedAdapter`·`FeedImageViews` 제거가 실제 호출자 0건과 일치하는지 확인한다.
### 포함 범위
- `HomeFollowingModels.kt`, `HomeFollowingUiModels.kt`, `HomeFollowingMappers.kt`, `HomeFollowingNewsAdapter.kt`
- 삭제된 `v2/widget/feed/FeedAdapter.kt`와 현재 재사용 중인 `FeedItem`, `FeedRankView`, `FeedContentView`, `FeedCommunityView`
- mapper/source tests와 content ranking string resource
### 제외 범위
- 실제 서버 응답 샘플과 Phase 11/13 터치 목적지.
## 3. 판정 기준
- PRD의 nested payload 필드, matching payload null 제외, `contentImageUrl: String?`, `visibleFromAtUtc` 정책을 기준으로 판정했다.
## 4. 검토한 근거
- `HomeFollowingModels.kt:47-111`
- `HomeFollowingMappers.kt:64-149`
- `HomeFollowingUiModels.kt:55-103`
- `HomeFollowingNewsAdapter.kt:68-167`
- `HomeFollowingMapperTest.kt:88-182`
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| mapper + Fragment source focused test | 성공 | mapper 10, source 16 테스트 실패 0 |
| 팔로잉 전체 회귀 | 성공 | 39개 테스트 실패 0 |
| resource/compile/ktlint/diff | 성공 | 모두 PASS |
| nullable image binding·Coil 2.7.0 source·저장소 `dispose()` 사용례 대조 | 문제 확인 | blank 분기가 drawable만 비우고 View 요청을 해제하지 않음 |
| DTO→mapper→Feed item의 ID 의미 대조 | 문제 확인 | content/post ID가 creator ID 필드에 대입됨 |
| `rg -n "FeedAdapter|FeedImageViews" app/src/main/java app/src/test/java docs/20260625_메인_홈_팔로잉_탭 docs/agent-guides` | 성공 | production/test 호출자 0건, Task 9.7 이력만 남음 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P9-001` | Medium | 수정 완료 | nullable 최근 소식 이미지 바인딩이 이전 Coil 요청을 해제하지 않음 | Task 9.5 | `P9-R1` 완료 |
| `REV-P9-002` | Low | 수정 완료 | API에 없는 creator ID를 content/post ID로 합성해 Feed model에 전달함 | Task 9.6 | `P9-R2` 완료 |
## 6. 발견 사항 상세
matching payload가 null이면 `mapNotNull`로 제외되고, content image null은 UI model까지 보존된다. `visibleFromAtUtc` 기준 상대 시간과 payload별 target ID도 현재 계약과 일치한다. Task 9.7에서 삭제한 `FeedAdapter``FeedImageViews`는 production/test 호출자가 없고, 최근 소식은 현재 `FeedItem`과 타입별 Feed view를 직접 재사용하므로 삭제에 따른 확정 발견 사항은 없다.
### REV-P9-001 — nullable 최근 소식 이미지 바인딩이 이전 Coil 요청을 해제하지 않음
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD `contentImageUrl`·`imageUrl` nullable 계약, Task 9.3 Feed image fallback
- **소유 Task:** Task 9.5
**관찰 내용**
`HomeFollowingNewsAdapter.bindImage()`는 blank URL에서 `setImageDrawable(null)`만 호출한다. 같은 ViewHolder가 이미지가 있는 item에서 nullable image item으로 재사용될 때 이전 Coil 요청이 진행 중이면, 그 요청이 완료되면서 현재 item에 이전 이미지가 표시될 수 있다.
**근거**
- 코드: `HomeFollowingNewsAdapter.kt:187-203`의 blank 분기에 `dispose()`가 없음
- 계약: `FollowingContentNewsResponse.contentImageUrl`, `FollowingContentRankingNewsResponse.contentImageUrl`, `FollowingCommunityPostNewsResponse.imageUrl`은 nullable
- 테스트: `HomeFollowingMapperTest`는 null 보존만 확인하고 adapter 재사용 시 이미지 요청 상태는 검증하지 않음
- 저장소 정상 사용례: `HomePopularCommunityAdapter.kt:67-70`은 blank image에서 `imageView.dispose()`와 drawable 초기화를 함께 수행함
- Coil 2.7.0 로컬 source: `ImageView.dispose()`는 View의 현재 요청을 취소하고 분리함
**재현 또는 검증 절차**
1. 최근 소식 ViewHolder의 image view에 item A의 완료 전 Coil 요청을 연결한다.
2. 같은 holder에 `contentImageUrl = null` 또는 `imageUrl = null`인 item B를 바인딩한다.
3. 현재 blank 분기는 drawable만 비우므로 요청 A가 남는다.
4. 요청 A가 완료되면 B에 A의 이미지가 표시될 수 있다. 요구 결과는 blank 바인딩 시 요청을 해제하고 빈 이미지 상태를 유지하는 것이다.
**영향**
빠른 스크롤이나 느린 이미지 응답 조건에서 ranking/content/community 최근 소식 카드에 다른 item의 이미지가 노출될 수 있다.
**권장 조치**
공유되는 `bindImage()` blank 분기에서 `imageView.dispose()` 후 drawable을 비운다. 한 대표 nullable content item의 ViewHolder 재사용을 Robolectric test로 고정하고 mapper/Feed 회귀를 실행한다.
**판정 기록**
- 2026-07-30 — nullable DTO·adapter binding·Coil request lifecycle·저장소 내 정상 해제 패턴을 대조해 확정.
- 2026-07-30 — 구현 전 Phase 9 Task 9.5 / `P9-R1`로 전환.
- 2026-07-30 — Task 9.5에서 `HomeFollowingNewsAdapter.bindImage()` blank 분기에 `dispose()`를 추가하고, Robolectric adapter binding test와 팔로잉 회귀 검증을 완료.
### REV-P9-002 — API에 없는 creator ID를 다른 target ID로 합성함
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** Task 6.4의 API 미제공 값 비합성 정책, Phase 9 nested payload 계약
- **소유 Task:** 신규 Task 9.6
**관찰 내용**
`FollowingContentNewsResponse``FollowingCommunityPostNewsResponse`에는 `creatorId`가 없지만 adapter가 `FeedItem.Content.creatorId`에는 `content.targetId`, `FeedItem.Community.creatorId`에는 `community.targetId`를 문자열로 넣는다. 두 target은 각각 `contentId`, `postId`이므로 creator ID가 아니다.
**근거**
- DTO: `HomeFollowingModels.kt`의 content/community nested payload에는 `creatorId` 없음
- mapper: content `targetId = contentId`, community `targetId = postId`
- adapter: `HomeFollowingNewsAdapter.kt`에서 두 `FeedItem.creatorId`에 각각 위 target ID를 대입
- 현재 클릭 경로: adapter callback은 원본 `HomeFollowingNewsUiItem`을 캡처하므로 현행 상세 이동에는 잘못된 `creatorId`가 사용되지 않음
**재현 또는 검증 절차**
1. content/community nested payload와 UI model의 target ID 의미를 대조한다.
2. 두 Feed item binding의 `creatorId` 대입값을 확인한다.
3. 실제 결과는 content/post ID가 creator ID 필드에 들어가며, 요구 결과는 서버가 제공하지 않은 ID를 합성하지 않는 것이다.
**영향**
현재 화면 클릭에는 직접 영향이 없지만 Feed model의 의미가 잘못되고, 공통 Feed 위젯이 추후 creator ID를 사용하면 잘못된 크리에이터 이동이나 분석 값으로 이어질 수 있다.
**권장 조치**
서버 계약을 추정해 확장하지 말고 두 Feed item의 `creatorId`를 빈 값으로 전달한다. 실제 `contentId`, `postId`와 기존 원본 UI model click callback은 유지한다.
**판정 기록**
- 2026-07-31 — DTO→mapper→adapter→click callback을 추적해 확정하고 Phase 9 Task 9.6 / `P9-R2`로 전환.
- 2026-07-31 — Task 9.6에서 `FeedItem.Content``FeedItem.Community``creatorId`를 빈 값으로 전달하도록 수정하고 focused/팔로잉 회귀 검증을 완료.
## 7. 확정 항목의 plan·goal 전환
- Phase 9에 Task 9.5 / `P9-R1`을 추가했으며 구현·검증을 완료했다.
- 최종 재점검에서 Task 9.6 / `P9-R2`를 추가했으며 creator ID 합성 제거를 완료했다.
- Task 9.7 / `P9-R3`의 미사용 `FeedAdapter` 제거와 문서 정리를 현재 호출자 검색·컴파일·회귀 결과로 확인했다.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | DTO→mapper→adapter 추적과 Task 9.7 미사용 호출자 검색 |
| 후보 항목 판정 완료 | 충족 | REV-P9-001~002 판정 완료 |
| 확정 항목 plan 반영 | 충족 | Task 9.5, Task 9.6 추가 |
| 검증 명령과 결과 기록 | 충족 | focused/통합 PASS |
**최종 결론:** 확정 발견 사항 모두 수정 완료
**남은 항목:** 없음
## 9. 수정 후 검증 기록
- 2026-07-30: `REV-P9-001` 보완으로 `HomeFollowingNewsAdapter.bindImage()`의 blank image 분기에서 기존 Coil 요청을 `dispose()`로 해제한 뒤 drawable을 비우도록 변경했다. 구현 전 `HomeFollowingNewsAdapterTest` RED, 구현 후 focused GREEN, 팔로잉 전체 회귀와 신규 프로필 테스트, `:app:mergeDebugResources`, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check` PASS.
- 2026-07-31 재점검: nested payload mapper·nullable image 회귀를 포함한 Phase 1~13 focused 113개 test, resource merge, Kotlin compile, ktlint PASS. 추가 확정 발견 사항 없음.
- 2026-07-31 최종 재점검: API에 없는 creator ID를 content/post ID로 대신 채우는 binding을 `REV-P9-002`로 확정하고, production/test 코드를 변경하기 전에 Task 9.6 / `P9-R2`로 전환했다.
- 2026-07-31 최종 검증: nested payload·adapter 회귀를 포함한 통합 20개 suite·306개 test와 resource merge, Kotlin compile, ktlint가 PASS했다. 자동 검증 성공과 별개로 ID 의미 계약 불일치 `REV-P9-002`는 Task 9.6의 수정 항목으로 유지했다.
- 2026-07-31: `REV-P9-002` 보완으로 `HomeFollowingNewsAdapter`의 content/community Feed model에서 API가 제공하지 않는 `creatorId`를 content/post ID로 합성하지 않고 빈 값으로 전달하도록 변경했다. RED focused test 실패를 확인한 뒤 GREEN으로 전환했고, focused test, 팔로잉 전체 회귀, `:app:compileDebugKotlin`, `:app:ktlintCheck` PASS.
- 2026-07-31 이번 리뷰 재검증: nested payload·nullable image·ID 의미와 Task 9.7의 미사용 `FeedAdapter` 삭제 상태를 다시 대조했다. production/test `FeedAdapter|FeedImageViews` 호출자는 0건이며, 통합 16개 suite·120개 test, resource merge, Kotlin compile, ktlint가 PASS해 추가 확정 발견 사항이 없다.

View File

@@ -101,6 +101,16 @@ Live Action Phase 테스트 예시:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinatorUiGateTest"
```
Home Following 최근 소식 테스트 예시:
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingNewsAdapterTest"
```
Home Following 프로필 이미지 테스트 예시:
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeCreatorProfileImageLoaderTest"
```
Creator/Community Action Phase 테스트 예시:
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.action.*"