Compare commits
10 Commits
3a62ed8463
...
f508d847ea
| Author | SHA1 | Date | |
|---|---|---|---|
| f508d847ea | |||
| e86e5a1c3d | |||
| 26eae18edc | |||
| b5cfdc83e3 | |||
| dd4a5167a9 | |||
| d592038edd | |||
| a106548f87 | |||
| 49f6fe592c | |||
| f600d10e8b | |||
| a1aa9c2243 |
11
AGENTS.md
11
AGENTS.md
@@ -114,12 +114,19 @@ Strong success criteria let you loop independently. Weak criteria ("make it work
|
|||||||
- 예: `docs/20260601_메인_홈_추천_UI와_API_연동/`
|
- 예: `docs/20260601_메인_홈_추천_UI와_API_연동/`
|
||||||
- 기존에 생성된 `docs/prd/`, `docs/plan-task/` 문서는 유지하고, 신규 생성 문서부터 위 구조를 적용한다.
|
- 기존에 생성된 `docs/prd/`, `docs/plan-task/` 문서는 유지하고, 신규 생성 문서부터 위 구조를 적용한다.
|
||||||
- 날짜는 `YYYYMMDD` 8자리 숫자를 사용한다.
|
- 날짜는 `YYYYMMDD` 8자리 숫자를 사용한다.
|
||||||
- PRD 문서는 `sample-prd.md` 파일에서 작업에 필요한 부분만 발췌해 작성한다. `sample-prd.md`가 없거나 위치가 불명확하면 추측하지 말고 사용자에게 확인한다.
|
- 문서 샘플의 기준 위치는 `docs/sample/`이다.
|
||||||
|
- PRD 문서는 `docs/sample/sample-prd.md`, 구현 계획/TASK 문서는 `docs/sample/sample-plan-task.md`를 참조해 작업에 필요한 항목을 구체화한다.
|
||||||
|
- 샘플의 `<...>` placeholder와 예시 값은 실제 작업 값으로 교체한다. 필요한 샘플이 없거나 위치가 불명확하면 추측하지 말고 사용자에게 확인한다.
|
||||||
- 연속된 하나의 작업이라면 별도 새 문서를 만들지 말고 기존 PRD와 계획/TASK 문서에 추가 작업으로 이어서 기록한다.
|
- 연속된 하나의 작업이라면 별도 새 문서를 만들지 말고 기존 PRD와 계획/TASK 문서에 추가 작업으로 이어서 기록한다.
|
||||||
- 작업 도중 범위가 변경되면 계획/TASK 문서 체크리스트를 먼저 업데이트한 뒤 구현한다.
|
- 작업 도중 범위가 변경되면 계획/TASK 문서 체크리스트를 먼저 업데이트한 뒤 구현한다.
|
||||||
- 특정 Phase 또는 Task에 직접 대응되는 검증 기록은 해당 Phase 또는 Task 아래에 한국어로 남긴다.
|
- 특정 Phase 또는 Task에 직접 대응되는 검증 기록은 해당 Phase 또는 Task 아래에 한국어로 남긴다.
|
||||||
- 여러 Phase에 걸치거나 문서 전체에 해당하는 통합 검증, 회귀 검증, 최종 수동 확인 기록은 문서 최하단 `Verification Log`에 한국어로 남긴다.
|
- 여러 Phase에 걸치거나 문서 전체에 해당하는 통합 검증, 회귀 검증, 최종 수동 확인 기록은 문서 최하단 `Verification Log`에 한국어로 남긴다.
|
||||||
- 후속 수정이 발생해도 기존 검증 기록은 삭제하거나 덮어쓰지 않고 위치별로 누적한다.
|
- 코드 리뷰, 코드 품질 점검, 완료 Phase 재검토를 수행하면 `docs/sample/sample-review.md`를 참조해 `docs/[날짜]_구현할내용한글/reviews/[리뷰범위]-review.md`에 결과를 기록한다.
|
||||||
|
- 리뷰 보고서명의 `[리뷰범위]`는 `phase<번호>-<구현 내용을 나타내는 영문 kebab-case>` 형식으로 작성한다. 예: `phase2-main-home-recommendation-review.md`
|
||||||
|
- 일반 빌드, 테스트, 린트 실행 결과는 리뷰 보고서를 만들지 않고 계획/TASK 문서의 Task별 검증 기록 또는 `Verification Log`에 남긴다.
|
||||||
|
- 리뷰에서 수정 항목이 확정되면 코드를 수정하기 전에 review ID, 대상 파일, 수정 범위, 회귀 테스트, 완료 증거를 포함한 신규 회귀 수정 Task를 `plan-task.md`의 해당 Phase에 추가하고 그 Task에 따라 바로 수정한다.
|
||||||
|
- 리뷰 후속 수정 시 기존 완료 Task를 다시 열지 않으며, 기존 완료 체크박스와 검증·리뷰 기록은 삭제하거나 덮어쓰지 않고 위치별로 누적한다.
|
||||||
|
- 샘플의 위치, 문서 구조, 작성·운용 규칙이 변경되면 같은 작업에서 `AGENTS.md`와 관련 가이드 문서를 함께 갱신한다.
|
||||||
|
|
||||||
## 상세 참조 문서
|
## 상세 참조 문서
|
||||||
- 빌드/린트/테스트는 `docs/agent-guides/build-test-style.md`를 참고한다.
|
- 빌드/린트/테스트는 `docs/agent-guides/build-test-style.md`를 참고한다.
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ android {
|
|||||||
applicationId "kr.co.vividnext.sodalive"
|
applicationId "kr.co.vividnext.sodalive"
|
||||||
minSdk 23
|
minSdk 23
|
||||||
targetSdk 35
|
targetSdk 35
|
||||||
versionCode 243
|
versionCode 244
|
||||||
versionName "1.55.1"
|
versionName "1.56.0"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("ktlint:package-name", "ktlint:standard:package-name")
|
||||||
|
|
||||||
package kr.co.vividnext.sodalive.audio_content.detail
|
package kr.co.vividnext.sodalive.audio_content.detail
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
@@ -10,6 +12,7 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.text.TextUtils
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
@@ -463,6 +466,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewModel.isExpandDetail.observe(this) {
|
viewModel.isExpandDetail.observe(this) {
|
||||||
|
binding.tvDetail.ellipsize = if (it) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
TextUtils.TruncateAt.END
|
||||||
|
}
|
||||||
binding.tvDetail.maxLines = if (it) {
|
binding.tvDetail.maxLines = if (it) {
|
||||||
Int.MAX_VALUE
|
Int.MAX_VALUE
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.Creato
|
|||||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.ModifyCommunityPostRequest
|
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.ModifyCommunityPostRequest
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity
|
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAllViewActivity
|
import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAllViewActivity
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.follow.UserFollowerListActivity
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||||
@@ -224,6 +225,7 @@ class CreatorChannelActivity :
|
|||||||
private fun setupClickListeners() {
|
private fun setupClickListeners() {
|
||||||
binding.ivBack.setOnClickListener { finish() }
|
binding.ivBack.setOnClickListener { finish() }
|
||||||
binding.ivMore.setOnClickListener { onMoreClicked() }
|
binding.ivMore.setOnClickListener { onMoreClicked() }
|
||||||
|
binding.tvFollowerCount.setOnClickListener { onFollowerCountClicked() }
|
||||||
binding.layoutFollowCapsule.setOnClickListener { onFollowCapsuleClicked() }
|
binding.layoutFollowCapsule.setOnClickListener { onFollowCapsuleClicked() }
|
||||||
binding.ivBell.setOnClickListener { onBellClicked() }
|
binding.ivBell.setOnClickListener { onBellClicked() }
|
||||||
binding.ownerFabButton.setOnClickListener { expandOwnerFab() }
|
binding.ownerFabButton.setOnClickListener { expandOwnerFab() }
|
||||||
@@ -251,6 +253,14 @@ class CreatorChannelActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onFollowerCountClicked() {
|
||||||
|
if (currentHeader?.isOwner != true) return
|
||||||
|
startActivity(
|
||||||
|
Intent(this, UserFollowerListActivity::class.java)
|
||||||
|
.putExtra(Constants.EXTRA_USER_ID, creatorId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun bindHeader(header: CreatorChannelHeaderUiModel) {
|
private fun bindHeader(header: CreatorChannelHeaderUiModel) {
|
||||||
binding.tvNickname.text = header.nickname
|
binding.tvNickname.text = header.nickname
|
||||||
binding.tvTitleNickname.text = header.nickname
|
binding.tvTitleNickname.text = header.nickname
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import kr.co.vividnext.sodalive.v2.access.AccessRequirement
|
|||||||
import kr.co.vividnext.sodalive.v2.access.ensureV2Access
|
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.ChatActionCommand
|
||||||
import kr.co.vividnext.sodalive.v2.chat.action.handleChatAction
|
import kr.co.vividnext.sodalive.v2.chat.action.handleChatAction
|
||||||
|
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
||||||
|
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.CommunityActionCommand
|
||||||
import kr.co.vividnext.sodalive.v2.community.action.handleCommunityAction
|
import kr.co.vividnext.sodalive.v2.community.action.handleCommunityAction
|
||||||
import kr.co.vividnext.sodalive.v2.content.action.ContentActionCommand
|
import kr.co.vividnext.sodalive.v2.content.action.ContentActionCommand
|
||||||
@@ -30,6 +32,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.live.onair.HomeOnAirLiveActivity
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiItem
|
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.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.HomeFollowingChatSection
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingCreatorSection
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingCreatorSection
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingLiveSection
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingLiveSection
|
||||||
@@ -230,10 +233,26 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
adapter = popularCommunityAdapter
|
adapter = popularCommunityAdapter
|
||||||
}
|
}
|
||||||
onCheerFollowAllClick = { creatorIds ->
|
onCheerFollowAllClick = { creatorIds ->
|
||||||
homeRecommendationViewModel.followCreators(SECTION_KEY_CHEER_CREATORS, creatorIds)
|
ensureV2Access(AccessRequirement.Login) {
|
||||||
|
showCheerCreatorsFollowAllConfirmDialog(creatorIds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showCheerCreatorsFollowAllConfirmDialog(creatorIds: List<Long>) {
|
||||||
|
V2ModalDialog(
|
||||||
|
activity = requireActivity(),
|
||||||
|
layoutInflater = layoutInflater,
|
||||||
|
title = getString(R.string.home_recommendation_follow_all_confirm_title),
|
||||||
|
desc = getString(R.string.home_recommendation_follow_all_confirm_description),
|
||||||
|
confirmButtonTitle = getString(R.string.home_recommendation_follow_all_confirm_button),
|
||||||
|
confirmButtonClick = {
|
||||||
|
homeRecommendationViewModel.followCreators(SECTION_KEY_CHEER_CREATORS, creatorIds)
|
||||||
|
},
|
||||||
|
cancelButtonTitle = getString(R.string.cancel)
|
||||||
|
).show(screenWidth)
|
||||||
|
}
|
||||||
|
|
||||||
private fun setUpCreatorRankingAdapter() {
|
private fun setUpCreatorRankingAdapter() {
|
||||||
binding.rvHomeCreatorRankings.apply {
|
binding.rvHomeCreatorRankings.apply {
|
||||||
layoutManager = CreatorRankingAdapter.createGridLayoutManager(requireContext())
|
layoutManager = CreatorRankingAdapter.createGridLayoutManager(requireContext())
|
||||||
@@ -409,7 +428,10 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
bindHomeFollowingEmpty(showEmptyMessage = true)
|
bindHomeFollowingEmpty(showEmptyMessage = true)
|
||||||
finishHomePullRefresh(HOME_TAB_FOLLOWING)
|
finishHomePullRefresh(HOME_TAB_FOLLOWING)
|
||||||
}
|
}
|
||||||
HomeFollowingUiState.LoginRequired -> bindHomeFollowingEmpty(showEmptyMessage = false)
|
HomeFollowingUiState.LoginRequired -> {
|
||||||
|
bindHomeFollowingEmpty(showEmptyMessage = false)
|
||||||
|
finishHomePullRefresh(HOME_TAB_FOLLOWING)
|
||||||
|
}
|
||||||
is HomeFollowingUiState.Error -> {
|
is HomeFollowingUiState.Error -> {
|
||||||
if (shouldPreserveHomeContentOnPullRefreshError(HOME_TAB_FOLLOWING)) return@observe
|
if (shouldPreserveHomeContentOnPullRefreshError(HOME_TAB_FOLLOWING)) return@observe
|
||||||
bindHomeFollowingEmpty(showEmptyMessage = false)
|
bindHomeFollowingEmpty(showEmptyMessage = false)
|
||||||
@@ -665,15 +687,38 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
|
|
||||||
private fun onFollowingSectionMoreClick(section: HomeFollowingSection) = Unit
|
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) {
|
private fun onFollowingNewsClick(item: HomeFollowingNewsUiItem) {
|
||||||
val community = item as? HomeFollowingNewsUiItem.Community ?: return
|
when (item) {
|
||||||
val postId = community.postId
|
is HomeFollowingNewsUiItem.Ranking -> if (item.type == FollowingNewsType.CREATOR_RANKING) {
|
||||||
if (postId <= 0L) return
|
handleCreatorAction(CreatorActionCommand.Profile(item.targetId))
|
||||||
handleCommunityAction(CommunityActionCommand.PostDetail(postId))
|
}
|
||||||
|
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() {
|
private fun openFollowingCreatorAll() {
|
||||||
@@ -707,6 +752,10 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
private fun onRecentActivityClick(item: HomeRecommendationRecentlyActiveCreatorUiModel) {
|
private fun onRecentActivityClick(item: HomeRecommendationRecentlyActiveCreatorUiModel) {
|
||||||
val route = item.toHomeRecommendationRecentlyActiveCreatorRoute() ?: return
|
val route = item.toHomeRecommendationRecentlyActiveCreatorRoute() ?: return
|
||||||
when (route) {
|
when (route) {
|
||||||
|
is HomeRecommendationRecentlyActiveCreatorRoute.LiveRoom -> liveActionCoordinator.enterLiveRoom(route.roomId)
|
||||||
|
is HomeRecommendationRecentlyActiveCreatorRoute.CreatorChannel -> handleCreatorAction(
|
||||||
|
CreatorActionCommand.Profile(route.creatorId)
|
||||||
|
)
|
||||||
is HomeRecommendationRecentlyActiveCreatorRoute.AudioContent -> handleContentAction(
|
is HomeRecommendationRecentlyActiveCreatorRoute.AudioContent -> handleContentAction(
|
||||||
ContentActionCommand.AudioDetail(audioContentId = route.contentId)
|
ContentActionCommand.AudioDetail(audioContentId = route.contentId)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ data class HomeBannerItem(
|
|||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
data class HomeActiveCreatorItem(
|
data class HomeActiveCreatorItem(
|
||||||
|
@SerializedName("creatorId") val creatorId: Long,
|
||||||
@SerializedName("creatorNickname") val creatorNickname: String,
|
@SerializedName("creatorNickname") val creatorNickname: String,
|
||||||
@SerializedName("creatorProfileImage") val creatorProfileImage: String,
|
@SerializedName("creatorProfileImage") val creatorProfileImage: String,
|
||||||
@SerializedName("activityType") val activityType: String,
|
@SerializedName("activityType") val activityType: String,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.main.home.model
|
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.common.UtcRelativeTimeTextFormatter
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.model.toUiItems
|
import kr.co.vividnext.sodalive.v2.main.chat.model.toUiItems
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingCommunityPostNewsResponse
|
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingCommunityPostNewsResponse
|
||||||
@@ -123,8 +121,7 @@ private fun FollowingContentNewsResponse.toUiItem(
|
|||||||
contentImageUrl = contentImageUrl,
|
contentImageUrl = contentImageUrl,
|
||||||
title = title,
|
title = title,
|
||||||
creatorProfileImageUrl = creatorProfileImageUrl,
|
creatorProfileImageUrl = creatorProfileImageUrl,
|
||||||
creatorNickname = creatorNickname,
|
creatorNickname = creatorNickname
|
||||||
labelResId = type.toLabelResId()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun FollowingCommunityPostNewsResponse.toUiItem(
|
private fun FollowingCommunityPostNewsResponse.toUiItem(
|
||||||
@@ -147,12 +144,3 @@ private fun FollowingCommunityPostNewsResponse.toUiItem(
|
|||||||
likeCount = likeCount,
|
likeCount = likeCount,
|
||||||
commentCount = commentCount
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -81,8 +81,7 @@ sealed interface HomeFollowingNewsUiItem {
|
|||||||
val contentImageUrl: String?,
|
val contentImageUrl: String?,
|
||||||
val title: String,
|
val title: String,
|
||||||
val creatorProfileImageUrl: String,
|
val creatorProfileImageUrl: String,
|
||||||
val creatorNickname: String,
|
val creatorNickname: String
|
||||||
@param:StringRes val labelResId: Int
|
|
||||||
) : HomeFollowingNewsUiItem
|
) : HomeFollowingNewsUiItem
|
||||||
|
|
||||||
data class Community(
|
data class Community(
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ fun HomeBannerItem.toUiModel(): HomeRecommendationBannerUiModel = HomeRecommenda
|
|||||||
fun HomeActiveCreatorItem.toUiModel(): HomeRecommendationRecentlyActiveCreatorUiModel {
|
fun HomeActiveCreatorItem.toUiModel(): HomeRecommendationRecentlyActiveCreatorUiModel {
|
||||||
val creatorActivityType = CreatorActivityType.from(activityType)
|
val creatorActivityType = CreatorActivityType.from(activityType)
|
||||||
return HomeRecommendationRecentlyActiveCreatorUiModel(
|
return HomeRecommendationRecentlyActiveCreatorUiModel(
|
||||||
|
creatorId = creatorId,
|
||||||
nickname = creatorNickname,
|
nickname = creatorNickname,
|
||||||
profileImage = creatorProfileImage,
|
profileImage = creatorProfileImage,
|
||||||
activityType = creatorActivityType,
|
activityType = creatorActivityType,
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ fun HomeRecommendationBannerRoute.toHomeRecommendationBannerIntent(context: Cont
|
|||||||
private fun homeRecommendationAppLinkHost(): String = "${BuildConfig.APPSCHEME}.onelink.me"
|
private fun homeRecommendationAppLinkHost(): String = "${BuildConfig.APPSCHEME}.onelink.me"
|
||||||
|
|
||||||
data class HomeRecommendationRecentlyActiveCreatorUiModel(
|
data class HomeRecommendationRecentlyActiveCreatorUiModel(
|
||||||
|
val creatorId: Long,
|
||||||
val nickname: String,
|
val nickname: String,
|
||||||
val profileImage: String,
|
val profileImage: String,
|
||||||
val activityType: CreatorActivityType?,
|
val activityType: CreatorActivityType?,
|
||||||
@@ -125,6 +126,10 @@ data class HomeRecommendationRecentlyActiveCreatorUiModel(
|
|||||||
)
|
)
|
||||||
|
|
||||||
sealed interface HomeRecommendationRecentlyActiveCreatorRoute {
|
sealed interface HomeRecommendationRecentlyActiveCreatorRoute {
|
||||||
|
data class LiveRoom(val roomId: Long) : HomeRecommendationRecentlyActiveCreatorRoute
|
||||||
|
|
||||||
|
data class CreatorChannel(val creatorId: Long) : HomeRecommendationRecentlyActiveCreatorRoute
|
||||||
|
|
||||||
data class AudioContent(val contentId: Long) : HomeRecommendationRecentlyActiveCreatorRoute
|
data class AudioContent(val contentId: Long) : HomeRecommendationRecentlyActiveCreatorRoute
|
||||||
|
|
||||||
data class Community(val postId: Long) : HomeRecommendationRecentlyActiveCreatorRoute
|
data class Community(val postId: Long) : HomeRecommendationRecentlyActiveCreatorRoute
|
||||||
@@ -132,12 +137,18 @@ sealed interface HomeRecommendationRecentlyActiveCreatorRoute {
|
|||||||
|
|
||||||
fun HomeRecommendationRecentlyActiveCreatorUiModel.toHomeRecommendationRecentlyActiveCreatorRoute():
|
fun HomeRecommendationRecentlyActiveCreatorUiModel.toHomeRecommendationRecentlyActiveCreatorRoute():
|
||||||
HomeRecommendationRecentlyActiveCreatorRoute? {
|
HomeRecommendationRecentlyActiveCreatorRoute? {
|
||||||
val routeTargetId = targetId?.takeIf { it > 0 } ?: return null
|
|
||||||
return when (activityType) {
|
return when (activityType) {
|
||||||
CreatorActivityType.Live -> null
|
CreatorActivityType.Live -> when {
|
||||||
|
targetId == null -> creatorId.takeIf { it > 0L }
|
||||||
|
?.let(HomeRecommendationRecentlyActiveCreatorRoute::CreatorChannel)
|
||||||
|
targetId > 0L -> HomeRecommendationRecentlyActiveCreatorRoute.LiveRoom(targetId)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
CreatorActivityType.LiveReplay,
|
CreatorActivityType.LiveReplay,
|
||||||
CreatorActivityType.Audio -> HomeRecommendationRecentlyActiveCreatorRoute.AudioContent(routeTargetId)
|
CreatorActivityType.Audio -> targetId?.takeIf { it > 0L }
|
||||||
CreatorActivityType.Community -> HomeRecommendationRecentlyActiveCreatorRoute.Community(routeTargetId)
|
?.let(HomeRecommendationRecentlyActiveCreatorRoute::AudioContent)
|
||||||
|
CreatorActivityType.Community -> targetId?.takeIf { it > 0L }
|
||||||
|
?.let(HomeRecommendationRecentlyActiveCreatorRoute::Community)
|
||||||
null -> null
|
null -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.main.home.ui
|
package kr.co.vividnext.sodalive.v2.main.home.ui
|
||||||
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import coil.dispose
|
||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
import coil.transform.Transformation
|
import coil.transform.Transformation
|
||||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
@@ -9,6 +10,7 @@ fun homeCreatorProfileImageTransformations(): List<Transformation> = listOf(Circ
|
|||||||
|
|
||||||
fun ImageView.loadHomeCreatorProfileImage(url: String?) {
|
fun ImageView.loadHomeCreatorProfileImage(url: String?) {
|
||||||
if (url.isNullOrBlank()) {
|
if (url.isNullOrBlank()) {
|
||||||
|
dispose()
|
||||||
setImageDrawable(null)
|
setImageDrawable(null)
|
||||||
} else {
|
} else {
|
||||||
loadUrl(url) {
|
loadUrl(url) {
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ class HomeFollowingChatAdapter(
|
|||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatViewHolder {
|
||||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_home_following_chat, parent, false)
|
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)
|
return ChatViewHolder(view, onClickItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,15 @@ class HomeFollowingLiveAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LiveViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LiveViewHolder {
|
||||||
return LiveViewHolder(
|
val view = LayoutInflater.from(parent.context).inflate(
|
||||||
LayoutInflater.from(parent.context).inflate(
|
R.layout.item_home_following_live,
|
||||||
R.layout.item_home_following_live,
|
parent,
|
||||||
parent,
|
false
|
||||||
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) {
|
override fun onBindViewHolder(holder: LiveViewHolder, position: Int) {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.main.home.ui
|
package kr.co.vividnext.sodalive.v2.main.home.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import coil.dispose
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingNewsType
|
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 kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityView
|
||||||
import kotlin.math.roundToInt
|
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(
|
class HomeFollowingNewsAdapter(
|
||||||
private val onClickItem: (HomeFollowingNewsUiItem) -> Unit = {}
|
private val onClickItem: (HomeFollowingNewsUiItem) -> Unit = {}
|
||||||
) : RecyclerView.Adapter<HomeFollowingNewsAdapter.NewsViewHolder>() {
|
) : RecyclerView.Adapter<HomeFollowingNewsAdapter.NewsViewHolder>() {
|
||||||
@@ -73,39 +107,12 @@ class HomeFollowingNewsAdapter(
|
|||||||
|
|
||||||
override fun bind(item: HomeFollowingNewsUiItem) {
|
override fun bind(item: HomeFollowingNewsUiItem) {
|
||||||
val ranking = item as HomeFollowingNewsUiItem.Ranking
|
val ranking = item as HomeFollowingNewsUiItem.Ranking
|
||||||
val context = view.context
|
val feedItem = buildHomeFollowingRankingFeedItem(view.context, ranking)
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
view.setFeedSize(feedSize(FeedVariant.Rank, parent))
|
view.setFeedSize(feedSize(FeedVariant.Rank, parent))
|
||||||
view.bind(feedItem)
|
view.bind(feedItem)
|
||||||
bindImage(view.imageView(), feedItem.imageUrl)
|
bindImage(view.imageView(), feedItem.imageUrl)
|
||||||
view.setOnFeedClick { onClickItem(ranking) }
|
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(
|
class CommunityViewHolder(
|
||||||
@@ -118,7 +125,7 @@ class HomeFollowingNewsAdapter(
|
|||||||
val community = item as HomeFollowingNewsUiItem.Community
|
val community = item as HomeFollowingNewsUiItem.Community
|
||||||
val feedItem = FeedItem.Community(
|
val feedItem = FeedItem.Community(
|
||||||
feedId = community.newsId,
|
feedId = community.newsId,
|
||||||
creatorId = community.targetId.toString(),
|
creatorId = "",
|
||||||
creatorName = community.creatorNickname,
|
creatorName = community.creatorNickname,
|
||||||
creatorImageUrl = community.creatorProfileImageUrl,
|
creatorImageUrl = community.creatorProfileImageUrl,
|
||||||
postId = community.postId.toString(),
|
postId = community.postId.toString(),
|
||||||
@@ -149,7 +156,7 @@ class HomeFollowingNewsAdapter(
|
|||||||
val content = item as HomeFollowingNewsUiItem.Content
|
val content = item as HomeFollowingNewsUiItem.Content
|
||||||
val feedItem = FeedItem.Content(
|
val feedItem = FeedItem.Content(
|
||||||
feedId = content.newsId,
|
feedId = content.newsId,
|
||||||
creatorId = content.targetId.toString(),
|
creatorId = "",
|
||||||
creatorName = content.creatorNickname,
|
creatorName = content.creatorNickname,
|
||||||
creatorImageUrl = content.creatorProfileImageUrl,
|
creatorImageUrl = content.creatorProfileImageUrl,
|
||||||
contentId = content.contentId.toString(),
|
contentId = content.contentId.toString(),
|
||||||
@@ -184,6 +191,7 @@ class HomeFollowingNewsAdapter(
|
|||||||
onImageLoaded: ((Int, Int) -> Unit)? = null
|
onImageLoaded: ((Int, Int) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
if (url.isBlank()) {
|
if (url.isBlank()) {
|
||||||
|
imageView.dispose()
|
||||||
imageView.setImageDrawable(null)
|
imageView.setImageDrawable(null)
|
||||||
} else {
|
} else {
|
||||||
imageView.loadUrl(url) {
|
imageView.loadUrl(url) {
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ import kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailSimpleView
|
|||||||
|
|
||||||
class HomeLiveAdapter : RecyclerView.Adapter<HomeLiveAdapter.LiveViewHolder>() {
|
class HomeLiveAdapter : RecyclerView.Adapter<HomeLiveAdapter.LiveViewHolder>() {
|
||||||
private var items: List<HomeRecommendationLiveUiModel> = emptyList()
|
private var items: List<HomeRecommendationLiveUiModel> = emptyList()
|
||||||
private var hasMore: Boolean = false
|
private var showAllItem: Boolean = false
|
||||||
private var onClick: ((HomeRecommendationLiveUiModel) -> Unit)? = null
|
private var onClick: ((HomeRecommendationLiveUiModel) -> Unit)? = null
|
||||||
private var onMoreClick: (() -> Unit)? = null
|
private var onMoreClick: (() -> Unit)? = null
|
||||||
|
|
||||||
fun submitItems(items: List<HomeRecommendationLiveUiModel>) {
|
fun submitItems(items: List<HomeRecommendationLiveUiModel>) {
|
||||||
this.items = items.take(MAX_VISIBLE_LIVE_COUNT)
|
this.items = items.take(MAX_VISIBLE_LIVE_COUNT)
|
||||||
hasMore = items.size > MAX_VISIBLE_LIVE_COUNT
|
showAllItem = items.isNotEmpty()
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,10 +49,10 @@ class HomeLiveAdapter : RecyclerView.Adapter<HomeLiveAdapter.LiveViewHolder>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = items.size + if (hasMore) 1 else 0
|
override fun getItemCount(): Int = items.size + if (showAllItem) 1 else 0
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
return if (hasMore && position == itemCount - 1) VIEW_TYPE_MORE else VIEW_TYPE_LIVE
|
return if (showAllItem && position == itemCount - 1) VIEW_TYPE_MORE else VIEW_TYPE_LIVE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun liveItemLayoutParams(parent: ViewGroup): RecyclerView.LayoutParams {
|
private fun liveItemLayoutParams(parent: ViewGroup): RecyclerView.LayoutParams {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.view.ViewGroup
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.dispose
|
import coil.dispose
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.common.image.BlurTransformation
|
|
||||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostSection
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostSection
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostUiModel
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostUiModel
|
||||||
@@ -51,9 +50,8 @@ class HomePopularCommunityAdapter(
|
|||||||
view.setOnFeedClick { feedItem -> onClickItem(feedItem as FeedItem.Community) }
|
view.setOnFeedClick { feedItem -> onClickItem(feedItem as FeedItem.Community) }
|
||||||
bindImage(item.item.creatorImageUrl, view.profileImageView())
|
bindImage(item.item.creatorImageUrl, view.profileImageView())
|
||||||
bindImage(
|
bindImage(
|
||||||
imageUrl = item.item.imageUrl,
|
imageUrl = item.item.imageUrl.takeIf { !item.item.isLockedPaidPost() },
|
||||||
imageView = view.communityImageView(),
|
imageView = view.communityImageView(),
|
||||||
shouldBlur = item.item.price > 0 && !item.item.existOrdered,
|
|
||||||
onImageLoaded = { width, height -> view.applyCommunityImageSize(width, height) }
|
onImageLoaded = { width, height -> view.applyCommunityImageSize(width, height) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -61,7 +59,6 @@ class HomePopularCommunityAdapter(
|
|||||||
private fun bindImage(
|
private fun bindImage(
|
||||||
imageUrl: String?,
|
imageUrl: String?,
|
||||||
imageView: android.widget.ImageView,
|
imageView: android.widget.ImageView,
|
||||||
shouldBlur: Boolean = false,
|
|
||||||
onImageLoaded: ((Int, Int) -> Unit)? = null
|
onImageLoaded: ((Int, Int) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
if (imageUrl.isNullOrBlank()) {
|
if (imageUrl.isNullOrBlank()) {
|
||||||
@@ -74,11 +71,12 @@ class HomePopularCommunityAdapter(
|
|||||||
onImageLoaded?.invoke(result.drawable.intrinsicWidth, result.drawable.intrinsicHeight)
|
onImageLoaded?.invoke(result.drawable.intrinsicWidth, result.drawable.intrinsicHeight)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (shouldBlur) {
|
|
||||||
transformations(BlurTransformation(imageView.context, 25f, 2.5f))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun FeedItem.Community.isLockedPaidPost(): Boolean {
|
||||||
|
return price > 0 && !existOrdered
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -365,7 +365,7 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingHorizontal="@dimen/spacing_14"
|
android:paddingHorizontal="@dimen/spacing_14"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
tools:listitem="@layout/item_home_following_news_content" />
|
tools:listitem="@layout/view_feed_content" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -309,22 +309,23 @@
|
|||||||
<string name="home_recommendation_section_genre_creator_suffix">\u0020creators</string>
|
<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_cheer_creators">Creators with recent cheers</string>
|
||||||
<string name="home_recommendation_section_popular_community_posts">Popular community</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_recent_chats_title">Recent chats</string>
|
||||||
<string name="screen_home_following_monthly_schedules_title">Monthly schedule</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_recent_news_title">Recent news</string>
|
||||||
<string name="screen_home_following_on_air">On Air</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_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_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_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_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_live">Live</string>
|
||||||
<string name="home_recommendation_activity_audio">Audio</string>
|
<string name="home_recommendation_activity_audio">Audio</string>
|
||||||
<string name="home_recommendation_activity_community">Community</string>
|
<string name="home_recommendation_activity_community">Community</string>
|
||||||
<string name="home_recommendation_follow_all">Follow all</string>
|
<string name="home_recommendation_follow_all">Follow all</string>
|
||||||
<string name="home_recommendation_follow_all_done">Following all</string>
|
<string name="home_recommendation_follow_all_done">Following all</string>
|
||||||
|
<string name="home_recommendation_follow_all_confirm_title">Follow all</string>
|
||||||
|
<string name="home_recommendation_follow_all_confirm_description">Would you like to follow all creators in this list?</string>
|
||||||
|
<string name="home_recommendation_follow_all_confirm_button">Follow</string>
|
||||||
<string name="home_recommendation_more">More</string>
|
<string name="home_recommendation_more">More</string>
|
||||||
<string name="home_recommendation_collapse">Collapse</string>
|
<string name="home_recommendation_collapse">Collapse</string>
|
||||||
<string name="common_error_unknown">An unknown error occurred. try again.</string>
|
<string name="common_error_unknown">An unknown error occurred. try again.</string>
|
||||||
@@ -1391,7 +1392,7 @@ The upload will continue even if you leave this page.</string>
|
|||||||
<string name="screen_audio_content_detail_donation_minimum">You can donate 1 can or more.</string>
|
<string name="screen_audio_content_detail_donation_minimum">You can donate 1 can or more.</string>
|
||||||
<string name="screen_audio_content_detail_donation_empty_message">Enter a message to send.</string>
|
<string name="screen_audio_content_detail_donation_empty_message">Enter a message to send.</string>
|
||||||
<string name="screen_audio_content_detail_pin_limit_title">Pin limit reached</string>
|
<string name="screen_audio_content_detail_pin_limit_title">Pin limit reached</string>
|
||||||
<string name="screen_audio_content_detail_pin_limit_message">Pin this content?\nYou can pin up to 3 items on your channel.\nPinning this will replace the oldest item.</string>
|
<string name="screen_audio_content_detail_pin_limit_message">Pin this content?\nYou can pin up to 9 items on your channel.\nPinning this will replace the oldest item.</string>
|
||||||
<string name="screen_audio_content_detail_time_default">00:00:00</string>
|
<string name="screen_audio_content_detail_time_default">00:00:00</string>
|
||||||
<string name="screen_audio_content_detail_time_total_default"> / 00:00:00</string>
|
<string name="screen_audio_content_detail_time_total_default"> / 00:00:00</string>
|
||||||
<string name="screen_audio_content_detail_total_duration_format"> / %1$s</string>
|
<string name="screen_audio_content_detail_total_duration_format"> / %1$s</string>
|
||||||
|
|||||||
@@ -162,7 +162,7 @@
|
|||||||
<string name="screen_chat_empty_ai">まだ会話がありません。\nAIクリエイターと話してみましょう。</string>
|
<string name="screen_chat_empty_ai">まだ会話がありません。\nAIクリエイターと話してみましょう。</string>
|
||||||
<string name="screen_chat_empty_dm">まだ会話がありません。\n好きなクリエイターにメッセージを送ってみましょう。</string>
|
<string name="screen_chat_empty_dm">まだ会話がありません。\n好きなクリエイターにメッセージを送ってみましょう。</string>
|
||||||
<string name="screen_dm_chat_room_enter_failed">チャットルームに接続できませんでした</string>
|
<string name="screen_dm_chat_room_enter_failed">チャットルームに接続できませんでした</string>
|
||||||
<string name="live_now">ただいま配信中</string>
|
<string name="live_now">On Air</string>
|
||||||
<string name="screen_live_now_all_title">配信中のライブをすべて見る</string>
|
<string name="screen_live_now_all_title">配信中のライブをすべて見る</string>
|
||||||
<string name="screen_live_now_all_empty_message">参加可能なライブがないか、年齢制限で入室できません。\n本人確認を行うか、チャンネルをフォローして\n配信通知を受け取ってみましょう。</string>
|
<string name="screen_live_now_all_empty_message">参加可能なライブがないか、年齢制限で入室できません。\n本人確認を行うか、チャンネルをフォローして\n配信通知を受け取ってみましょう。</string>
|
||||||
<string name="screen_live_now_all_free">無料</string>
|
<string name="screen_live_now_all_free">無料</string>
|
||||||
@@ -309,22 +309,23 @@
|
|||||||
<string name="home_recommendation_section_genre_creator_suffix">のクリエイター</string>
|
<string name="home_recommendation_section_genre_creator_suffix">のクリエイター</string>
|
||||||
<string name="home_recommendation_section_cheer_creators">最近応援が多いクリエイター</string>
|
<string name="home_recommendation_section_cheer_creators">最近応援が多いクリエイター</string>
|
||||||
<string name="home_recommendation_section_popular_community_posts">人気コミュニティ</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_recent_chats_title">最近のチャット</string>
|
||||||
<string name="screen_home_following_monthly_schedules_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_recent_news_title">最近のお知らせ</string>
|
||||||
<string name="screen_home_following_on_air">On Air</string>
|
<string name="screen_home_following_on_air">On Air</string>
|
||||||
<string name="screen_home_following_photo_content">グラビア</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_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_content_ranking_news_message">%1$sが今週のランキング%2$sに入りました!</string>
|
||||||
<string name="screen_home_following_empty">フォロー中のお知らせはまだありません。\n気になるクリエイターをフォローしてみましょう。</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_live">ライブ</string>
|
||||||
<string name="home_recommendation_activity_audio">オーディオ</string>
|
<string name="home_recommendation_activity_audio">オーディオ</string>
|
||||||
<string name="home_recommendation_activity_community">コミュニティ</string>
|
<string name="home_recommendation_activity_community">コミュニティ</string>
|
||||||
<string name="home_recommendation_follow_all">すべてフォロー</string>
|
<string name="home_recommendation_follow_all">すべてフォロー</string>
|
||||||
<string name="home_recommendation_follow_all_done">すべてフォロー中</string>
|
<string name="home_recommendation_follow_all_done">すべてフォロー中</string>
|
||||||
|
<string name="home_recommendation_follow_all_confirm_title">すべてフォロー</string>
|
||||||
|
<string name="home_recommendation_follow_all_confirm_description">このリストのクリエイターをすべてフォローしますか?</string>
|
||||||
|
<string name="home_recommendation_follow_all_confirm_button">フォローする</string>
|
||||||
<string name="home_recommendation_more">もっと見る</string>
|
<string name="home_recommendation_more">もっと見る</string>
|
||||||
<string name="home_recommendation_collapse">折りたたむ</string>
|
<string name="home_recommendation_collapse">折りたたむ</string>
|
||||||
<string name="common_error_unknown">不明なエラーが発生しました。恐れ入りますが、もう一度お試しください。</string>
|
<string name="common_error_unknown">不明なエラーが発生しました。恐れ入りますが、もう一度お試しください。</string>
|
||||||
@@ -1389,7 +1390,7 @@
|
|||||||
<string name="screen_audio_content_detail_donation_minimum">1CAN以上からギフト可能です。</string>
|
<string name="screen_audio_content_detail_donation_minimum">1CAN以上からギフト可能です。</string>
|
||||||
<string name="screen_audio_content_detail_donation_empty_message">一緒に送るメッセージを入力してください。</string>
|
<string name="screen_audio_content_detail_donation_empty_message">一緒に送るメッセージを入力してください。</string>
|
||||||
<string name="screen_audio_content_detail_pin_limit_title">固定上限到達</string>
|
<string name="screen_audio_content_detail_pin_limit_title">固定上限到達</string>
|
||||||
<string name="screen_audio_content_detail_pin_limit_message">このコンテンツを固定しますか?\nチャンネルに最大3つまで固定できます。\n固定すると一番古いコンテンツが入れ替わります。</string>
|
<string name="screen_audio_content_detail_pin_limit_message">このコンテンツを固定しますか?\nチャンネルに最大9つまで固定できます。\n固定すると一番古いコンテンツが入れ替わります。</string>
|
||||||
<string name="screen_audio_content_detail_time_default">00:00:00</string>
|
<string name="screen_audio_content_detail_time_default">00:00:00</string>
|
||||||
<string name="screen_audio_content_detail_time_total_default"> / 00:00:00</string>
|
<string name="screen_audio_content_detail_time_total_default"> / 00:00:00</string>
|
||||||
<string name="screen_audio_content_detail_total_duration_format"> / %1$s</string>
|
<string name="screen_audio_content_detail_total_duration_format"> / %1$s</string>
|
||||||
|
|||||||
@@ -161,7 +161,7 @@
|
|||||||
<string name="screen_chat_empty_ai">아직 나눈 대화가 없어요.\nAI 크리에이터와 대화를 시작해 보세요.</string>
|
<string name="screen_chat_empty_ai">아직 나눈 대화가 없어요.\nAI 크리에이터와 대화를 시작해 보세요.</string>
|
||||||
<string name="screen_chat_empty_dm">아직 나눈 대화가 없어요.\n좋아하는 크리에이터에게 메세지를 보내보세요.</string>
|
<string name="screen_chat_empty_dm">아직 나눈 대화가 없어요.\n좋아하는 크리에이터에게 메세지를 보내보세요.</string>
|
||||||
<string name="screen_dm_chat_room_enter_failed">대화방에 접속하지 못했습니다</string>
|
<string name="screen_dm_chat_room_enter_failed">대화방에 접속하지 못했습니다</string>
|
||||||
<string name="live_now">지금 라이브 중</string>
|
<string name="live_now">On Air</string>
|
||||||
<string name="screen_live_now_all_title">지금 라이브 중 전체보기</string>
|
<string name="screen_live_now_all_title">지금 라이브 중 전체보기</string>
|
||||||
<string name="screen_live_now_all_empty_message">현재 참여 가능한 라이브 방송이 없거나\n연령제한으로 입장이 불가능합니다.\n본인인증을 해보거나 채널을 팔로잉하고\n라이브 방송 알림을 받아보세요.</string>
|
<string name="screen_live_now_all_empty_message">현재 참여 가능한 라이브 방송이 없거나\n연령제한으로 입장이 불가능합니다.\n본인인증을 해보거나 채널을 팔로잉하고\n라이브 방송 알림을 받아보세요.</string>
|
||||||
<string name="screen_live_now_all_free">무료</string>
|
<string name="screen_live_now_all_free">무료</string>
|
||||||
@@ -308,22 +308,23 @@
|
|||||||
<string name="home_recommendation_section_genre_creator_suffix">\u0020크리에이터</string>
|
<string name="home_recommendation_section_genre_creator_suffix">\u0020크리에이터</string>
|
||||||
<string name="home_recommendation_section_cheer_creators">최근 응원이 많은 크리에이터</string>
|
<string name="home_recommendation_section_cheer_creators">최근 응원이 많은 크리에이터</string>
|
||||||
<string name="home_recommendation_section_popular_community_posts">인기 커뮤니티</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_recent_chats_title">최근 대화</string>
|
||||||
<string name="screen_home_following_monthly_schedules_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_recent_news_title">최근 소식</string>
|
||||||
<string name="screen_home_following_on_air">On Air</string>
|
<string name="screen_home_following_on_air">On Air</string>
|
||||||
<string name="screen_home_following_photo_content">화보</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_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_content_ranking_news_message">%1$s 콘텐츠가 이번 주 랭킹 %2$s에 올랐어요!</string>
|
||||||
<string name="screen_home_following_empty">아직 팔로잉 소식이 없어요.\n관심 있는 크리에이터를 팔로우해 보세요.</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_live">라이브</string>
|
||||||
<string name="home_recommendation_activity_audio">오디오</string>
|
<string name="home_recommendation_activity_audio">오디오</string>
|
||||||
<string name="home_recommendation_activity_community">커뮤니티</string>
|
<string name="home_recommendation_activity_community">커뮤니티</string>
|
||||||
<string name="home_recommendation_follow_all">모두 팔로우 하기</string>
|
<string name="home_recommendation_follow_all">모두 팔로우 하기</string>
|
||||||
<string name="home_recommendation_follow_all_done">모두 팔로우 완료</string>
|
<string name="home_recommendation_follow_all_done">모두 팔로우 완료</string>
|
||||||
|
<string name="home_recommendation_follow_all_confirm_title">모두 팔로우</string>
|
||||||
|
<string name="home_recommendation_follow_all_confirm_description">목록의 크리에이터를 모두 팔로우하시겠어요?</string>
|
||||||
|
<string name="home_recommendation_follow_all_confirm_button">팔로우 하기</string>
|
||||||
<string name="home_recommendation_more">더보기</string>
|
<string name="home_recommendation_more">더보기</string>
|
||||||
<string name="home_recommendation_collapse">접기</string>
|
<string name="home_recommendation_collapse">접기</string>
|
||||||
<string name="common_error_unknown">알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.</string>
|
<string name="common_error_unknown">알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.</string>
|
||||||
@@ -1388,7 +1389,7 @@
|
|||||||
<string name="screen_audio_content_detail_donation_minimum">1캔 이상 후원하실 수 있습니다.</string>
|
<string name="screen_audio_content_detail_donation_minimum">1캔 이상 후원하실 수 있습니다.</string>
|
||||||
<string name="screen_audio_content_detail_donation_empty_message">함께 보낼 메시지를 입력하세요.</string>
|
<string name="screen_audio_content_detail_donation_empty_message">함께 보낼 메시지를 입력하세요.</string>
|
||||||
<string name="screen_audio_content_detail_pin_limit_title">고정 한도 도달</string>
|
<string name="screen_audio_content_detail_pin_limit_title">고정 한도 도달</string>
|
||||||
<string name="screen_audio_content_detail_pin_limit_message">이 콘텐츠를 고정하시겠어요?\n채널에 콘텐츠를 최대 3개까지 고정할 수 있습니다.\n이 콘텐츠를 고정하면 가장 오래된 콘텐츠가 대체됩니다.</string>
|
<string name="screen_audio_content_detail_pin_limit_message">이 콘텐츠를 고정하시겠어요?\n채널에 콘텐츠를 최대 9개까지 고정할 수 있습니다.\n이 콘텐츠를 고정하면 가장 오래된 콘텐츠가 대체됩니다.</string>
|
||||||
<string name="screen_audio_content_detail_time_default">00:00:00</string>
|
<string name="screen_audio_content_detail_time_default">00:00:00</string>
|
||||||
<string name="screen_audio_content_detail_time_total_default"> / 00:00:00</string>
|
<string name="screen_audio_content_detail_time_total_default"> / 00:00:00</string>
|
||||||
<string name="screen_audio_content_detail_total_duration_format"> / %1$s</string>
|
<string name="screen_audio_content_detail_total_duration_format"> / %1$s</string>
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
@file:Suppress("ktlint:package-name", "ktlint:standard:package-name")
|
||||||
|
|
||||||
|
package kr.co.vividnext.sodalive.audio_content.detail
|
||||||
|
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class AudioContentDetailActivitySourceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `긴 설명을 펼치면 말줄임을 제거한다`() {
|
||||||
|
val observer = expandDetailObserver()
|
||||||
|
|
||||||
|
assertTrue(observer.contains("binding.tvDetail.ellipsize = if (it)"))
|
||||||
|
assertTrue(observer.contains("null"))
|
||||||
|
assertTrue(observer.contains("Int.MAX_VALUE"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `긴 설명을 접으면 두 줄 말줄임을 복원한다`() {
|
||||||
|
val observer = expandDetailObserver()
|
||||||
|
|
||||||
|
assertTrue(observer.contains("TextUtils.TruncateAt.END"))
|
||||||
|
assertTrue(observer.contains("2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `긴 설명의 반복 토글 상태를 한 observer에서 처리한다`() {
|
||||||
|
val observer = expandDetailObserver()
|
||||||
|
|
||||||
|
assertTrue(observer.contains("binding.tvDetail.ellipsize = if (it)"))
|
||||||
|
assertTrue(observer.contains("binding.tvDetail.maxLines = if (it)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expandDetailObserver(): String {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
return source
|
||||||
|
.substringAfter("viewModel.isExpandDetail.observe(this) {")
|
||||||
|
.substringBefore("viewModel.isShowPreviewAlert.observe(this) {")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun projectFile(relativePath: String): File {
|
||||||
|
val candidates = listOf(File(relativePath), File("../$relativePath"))
|
||||||
|
return candidates.firstOrNull { it.exists() }
|
||||||
|
?: error("Project file not found: $relativePath")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -372,6 +372,26 @@ class CreatorChannelActivitySourceTest {
|
|||||||
assertFalse(layout.contains("@+id/tv_follow\""))
|
assertFalse(layout.contains("@+id/tv_follow\""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `내 채널 팔로워 수 클릭 source는 레거시 팔로워 목록으로 이동한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||||
|
).readText()
|
||||||
|
val clickSource = sourceSection(
|
||||||
|
source = source,
|
||||||
|
startMarker = "private fun onFollowerCountClicked()",
|
||||||
|
endMarker = "private fun bindHeader"
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(source.contains("import kr.co.vividnext.sodalive.explorer.profile.follow.UserFollowerListActivity"))
|
||||||
|
assertTrue(source.contains("binding.tvFollowerCount.setOnClickListener"))
|
||||||
|
assertTrue(clickSource.contains("if (currentHeader?.isOwner != true) return"))
|
||||||
|
assertTrue(clickSource.contains("Intent(this, UserFollowerListActivity::class.java)"))
|
||||||
|
assertTrue(clickSource.contains("putExtra(Constants.EXTRA_USER_ID, creatorId)"))
|
||||||
|
assertFalse(clickSource.contains("followerCount > 0"))
|
||||||
|
assertFalse(source.contains("FollowingCreatorActivity::class.java"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `creator channel home은 status bar 뒤까지 header를 그리고 자체 inset을 처리한다`() {
|
fun `creator channel home은 status bar 뒤까지 header를 그리고 자체 inset을 처리한다`() {
|
||||||
val baseActivity = projectFile("app/src/main/java/kr/co/vividnext/sodalive/base/BaseActivity.kt").readText()
|
val baseActivity = projectFile("app/src/main/java/kr/co/vividnext/sodalive/base/BaseActivity.kt").readText()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import java.io.File
|
|||||||
class HomeFollowingFragmentSourceTest {
|
class HomeFollowingFragmentSourceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `home layout exposes following content and section recycler ids`() {
|
fun `홈 layout은 팔로잉 content와 section recycler id를 노출한다`() {
|
||||||
val layout = homeMainLayoutSource()
|
val layout = homeMainLayoutSource()
|
||||||
|
|
||||||
assertTrue(layout.contains("@+id/nsv_home_following_content"))
|
assertTrue(layout.contains("@+id/nsv_home_following_content"))
|
||||||
@@ -20,7 +20,7 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `home main fragment injects following viewmodel and wires adapters`() {
|
fun `홈 main fragment는 팔로잉 viewmodel과 adapter를 연결한다`() {
|
||||||
val source = homeMainFragmentSource()
|
val source = homeMainFragmentSource()
|
||||||
|
|
||||||
assertTrue(source.contains("HomeFollowingViewModel"))
|
assertTrue(source.contains("HomeFollowingViewModel"))
|
||||||
@@ -38,7 +38,7 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `following tab branch shows following content and hides other home surfaces`() {
|
fun `팔로잉 탭 분기는 팔로잉 content만 표시한다`() {
|
||||||
val source = homeMainFragmentSource()
|
val source = homeMainFragmentSource()
|
||||||
val branch = source.substringAfter("private fun showHomeTab(index: Int)")
|
val branch = source.substringAfter("private fun showHomeTab(index: Int)")
|
||||||
.substringAfter("HOME_TAB_FOLLOWING ->")
|
.substringAfter("HOME_TAB_FOLLOWING ->")
|
||||||
@@ -50,7 +50,7 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `following tab loads following content only once`() {
|
fun `팔로잉 탭은 팔로잉 content를 최초 한 번만 로드한다`() {
|
||||||
val source = homeMainFragmentSource()
|
val source = homeMainFragmentSource()
|
||||||
|
|
||||||
assertTrue(source.contains("private var hasLoadedFollowing = false"))
|
assertTrue(source.contains("private var hasLoadedFollowing = false"))
|
||||||
@@ -60,7 +60,7 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `following section chevrons call callback without starting activity`() {
|
fun `팔로잉 section chevron은 이동 없이 callback만 호출한다`() {
|
||||||
val source = homeMainFragmentSource()
|
val source = homeMainFragmentSource()
|
||||||
val callback = source.substringAfter("private fun onFollowingSectionMoreClick")
|
val callback = source.substringAfter("private fun onFollowingSectionMoreClick")
|
||||||
.substringBefore("\n private fun")
|
.substringBefore("\n private fun")
|
||||||
@@ -77,7 +77,7 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `following state binding clears or binds each following section`() {
|
fun `팔로잉 상태 binding은 section을 비우거나 바인딩한다`() {
|
||||||
val source = homeMainFragmentSource()
|
val source = homeMainFragmentSource()
|
||||||
|
|
||||||
assertTrue(source.contains("followingStateLiveData.observe(viewLifecycleOwner)"))
|
assertTrue(source.contains("followingStateLiveData.observe(viewLifecycleOwner)"))
|
||||||
@@ -85,7 +85,10 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
assertTrue(source.contains("bindHomeFollowingContent(state)"))
|
assertTrue(source.contains("bindHomeFollowingContent(state)"))
|
||||||
assertTrue(source.contains("HomeFollowingUiState.Empty ->"))
|
assertTrue(source.contains("HomeFollowingUiState.Empty ->"))
|
||||||
assertTrue(source.contains("bindHomeFollowingEmpty(showEmptyMessage = true)"))
|
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("is HomeFollowingUiState.Error ->"))
|
||||||
assertTrue(source.contains("if (shouldPreserveHomeContentOnPullRefreshError(HOME_TAB_FOLLOWING)) return@observe"))
|
assertTrue(source.contains("if (shouldPreserveHomeContentOnPullRefreshError(HOME_TAB_FOLLOWING)) return@observe"))
|
||||||
assertTrue(source.contains("bindHomeFollowingEmpty(showEmptyMessage = false)"))
|
assertTrue(source.contains("bindHomeFollowingEmpty(showEmptyMessage = false)"))
|
||||||
@@ -102,7 +105,7 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `following empty state shows centered reusable empty string only for all empty content`() {
|
fun `팔로잉 empty 상태는 다국어 빈 문구와 상태 분기를 유지한다`() {
|
||||||
val layout = homeMainLayoutSource()
|
val layout = homeMainLayoutSource()
|
||||||
val emptyText = layout.substringAfter("@+id/tv_home_following_empty")
|
val emptyText = layout.substringAfter("@+id/tv_home_following_empty")
|
||||||
.substringBefore("@+id/ll_home_following_creators_section")
|
.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()
|
val stringsJa = projectFile("app/src/main/res/values-ja/strings.xml").readText()
|
||||||
|
|
||||||
assertTrue(layout.contains("@+id/tv_home_following_empty"))
|
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:text=\"@string/screen_home_following_empty\""))
|
||||||
assertTrue(emptyText.contains("android:visibility=\"gone\""))
|
|
||||||
|
|
||||||
assertTrue(
|
assertTrue(
|
||||||
strings.contains(
|
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("HomeFollowingUiState.Empty ->"))
|
||||||
assertTrue(source.contains("bindHomeFollowingEmpty(showEmptyMessage = true)"))
|
assertTrue(source.contains("bindHomeFollowingEmpty(showEmptyMessage = true)"))
|
||||||
assertTrue(source.contains("is HomeFollowingUiState.Error ->"))
|
assertTrue(source.contains("is HomeFollowingUiState.Error ->"))
|
||||||
@@ -147,7 +143,7 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `following adapters bind figma required item fields`() {
|
fun `팔로잉 adapter는 필수 item field를 바인딩한다`() {
|
||||||
val liveAdapter = projectFile(
|
val liveAdapter = projectFile(
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingLiveAdapter.kt"
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingLiveAdapter.kt"
|
||||||
).readText()
|
).readText()
|
||||||
@@ -183,7 +179,7 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `following recent news hides chevron and reuses feed widgets by item type`() {
|
fun `팔로잉 최근 소식은 chevron을 숨기고 feed widget을 재사용한다`() {
|
||||||
val layout = homeMainLayoutSource()
|
val layout = homeMainLayoutSource()
|
||||||
val recentNewsSection = layout.substringAfter("@+id/ll_home_following_recent_news_section")
|
val recentNewsSection = layout.substringAfter("@+id/ll_home_following_recent_news_section")
|
||||||
.substringBefore("</LinearLayout>")
|
.substringBefore("</LinearLayout>")
|
||||||
@@ -215,10 +211,13 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
assertTrue(adapter.contains("FeedItem.Content"))
|
assertTrue(adapter.contains("FeedItem.Content"))
|
||||||
assertFalse(adapter.contains("R.layout.item_home_following_news_content"))
|
assertFalse(adapter.contains("R.layout.item_home_following_news_content"))
|
||||||
assertFalse(adapter.contains("R.layout.item_home_following_news_rank"))
|
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
|
@Test
|
||||||
fun `following ranking news builds sentence message and highlights rank text`() {
|
fun `팔로잉 ranking 소식은 문장 message와 순위 강조를 만든다`() {
|
||||||
val adapter = projectFile(
|
val adapter = projectFile(
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt"
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt"
|
||||||
).readText()
|
).readText()
|
||||||
@@ -229,14 +228,15 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
assertTrue(adapter.contains("context.getString("))
|
assertTrue(adapter.contains("context.getString("))
|
||||||
assertTrue(adapter.contains("highlightRanges = listOf("))
|
assertTrue(adapter.contains("highlightRanges = listOf("))
|
||||||
assertTrue(adapter.contains("FeedRankHighlight("))
|
assertTrue(adapter.contains("FeedRankHighlight("))
|
||||||
assertTrue(adapter.contains("start = message.indexOf(rankText)"))
|
assertTrue(adapter.contains("start = message.lastIndexOf(rankText)"))
|
||||||
assertTrue(adapter.contains("endExclusive = message.indexOf(rankText) + rankText.length"))
|
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("message = ranking.body.ifBlank { ranking.title }"))
|
||||||
assertFalse(adapter.contains("highlightRanges = emptyList()"))
|
assertFalse(adapter.contains("highlightRanges = emptyList()"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `following recent news models use nested payload response contract`() {
|
fun `팔로잉 최근 소식 model은 nested payload 응답 계약을 사용한다`() {
|
||||||
val models = projectFile(
|
val models = projectFile(
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingModels.kt"
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingModels.kt"
|
||||||
).readText()
|
).readText()
|
||||||
@@ -266,7 +266,7 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `following recent news adapter binds nested payload ui variants`() {
|
fun `팔로잉 최근 소식 adapter는 nested payload UI variant를 바인딩한다`() {
|
||||||
val adapter = projectFile(
|
val adapter = projectFile(
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt"
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt"
|
||||||
).readText()
|
).readText()
|
||||||
@@ -289,20 +289,34 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 layout = homeMainLayoutSource()
|
||||||
val section = layout.substringAfter("@+id/ll_home_following_creators_section")
|
val section = layout.substringAfter("@+id/ll_home_following_creators_section")
|
||||||
.substringBefore("@+id/ll_home_following_on_air_section")
|
.substringBefore("@+id/ll_home_following_on_air_section")
|
||||||
val itemLayout = projectFile("app/src/main/res/layout/item_home_following_creator.xml").readText()
|
val itemLayout = projectFile("app/src/main/res/layout/item_home_following_creator.xml").readText()
|
||||||
|
|
||||||
assertFalse(section.contains("view_home_following_creators_title"))
|
assertFalse(section.contains("view_home_following_creators_title"))
|
||||||
assertTrue(itemLayout.contains("android:layout_width=\"75dp\""))
|
assertTrue(itemLayout.contains("@+id/iv_home_following_creator_profile"))
|
||||||
assertTrue(itemLayout.contains("android:layout_height=\"75dp\""))
|
assertTrue(itemLayout.contains("@+id/tv_home_following_creator_nickname"))
|
||||||
assertTrue(itemLayout.contains("@style/Typography.Body5"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `following creators list appends all button as last item`() {
|
fun `팔로잉 크리에이터 목록은 전체 item을 마지막에 추가하고 이동을 연결한다`() {
|
||||||
val adapter = projectFile(
|
val adapter = projectFile(
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingCreatorAdapter.kt"
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingCreatorAdapter.kt"
|
||||||
).readText()
|
).readText()
|
||||||
@@ -324,48 +338,36 @@ class HomeFollowingFragmentSourceTest {
|
|||||||
assertTrue(fragment.contains("onClickAll = { openFollowingCreatorAll() }"))
|
assertTrue(fragment.contains("onClickAll = { openFollowingCreatorAll() }"))
|
||||||
assertTrue(fragment.contains("private fun openFollowingCreatorAll()"))
|
assertTrue(fragment.contains("private fun openFollowingCreatorAll()"))
|
||||||
assertTrue(fragment.contains("startActivity(Intent(requireContext(), FollowingCreatorActivity::class.java))"))
|
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"))
|
assertTrue(allLayout.contains("@string/screen_home_theme_all"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `following recent chats section is horizontal box list`() {
|
fun `팔로잉 최근 대화 섹션은 RecyclerView와 adapter를 연결한다`() {
|
||||||
val layout = homeMainLayoutSource()
|
val layout = homeMainLayoutSource()
|
||||||
val section = layout.substringAfter("@+id/rv_home_following_recent_chats")
|
|
||||||
.substringBefore("@+id/ll_home_following_monthly_schedules_section")
|
|
||||||
val fragment = homeMainFragmentSource()
|
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("rvHomeFollowingRecentChats.apply"))
|
||||||
assertTrue(fragment.contains("LinearLayoutManager.HORIZONTAL"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `following recent chat item matches figma box fields`() {
|
fun `팔로잉 최근 대화 item은 필수 field와 클릭 계약을 바인딩한다`() {
|
||||||
val adapter = projectFile(
|
val adapter = projectFile(
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingChatAdapter.kt"
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingChatAdapter.kt"
|
||||||
).readText()
|
).readText()
|
||||||
val itemLayout = projectFile("app/src/main/res/layout/item_home_following_chat.xml").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("@+id/iv_home_following_chat_creator_profile"))
|
||||||
assertTrue(itemLayout.contains("android:layout_width=\"62dp\""))
|
assertTrue(itemLayout.contains("@+id/tv_home_following_chat_creator_nickname"))
|
||||||
assertTrue(itemLayout.contains("android:layout_height=\"62dp\""))
|
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("@+id/tv_home_following_chat_direct_badge"))
|
||||||
assertTrue(itemLayout.contains("@drawable/bg_chat_direct_badge"))
|
assertTrue(itemLayout.contains("@drawable/bg_chat_direct_badge"))
|
||||||
assertTrue(itemLayout.contains("@string/screen_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"))
|
assertFalse(adapter.contains("ViewGroup.LayoutParams.MATCH_PARENT"))
|
||||||
assertTrue(adapter.contains("recyclerItemLayoutParams(parent)"))
|
|
||||||
assertTrue(adapter.contains("directBadgeText.visibility = if (item.showDirectBadge)"))
|
assertTrue(adapter.contains("directBadgeText.visibility = if (item.showDirectBadge)"))
|
||||||
assertTrue(adapter.contains("formatChatRoomLastMessageTime(itemView.context, item.lastMessageAt)"))
|
assertTrue(adapter.contains("formatChatRoomLastMessageTime(itemView.context, item.lastMessageAt)"))
|
||||||
|
assertTrue(adapter.contains("itemView.setOnClickListener { onClickItem(item) }"))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun homeMainFragmentSource(): String {
|
private fun homeMainFragmentSource(): String {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.main.home
|
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.common.UtcRelativeTimeTextFormatter
|
||||||
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomListItemResponse
|
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 })
|
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
|
@Test
|
||||||
fun `recentNews nested payload는 type별 UI item으로 매핑된다`() {
|
fun `recentNews nested payload는 type별 UI item으로 매핑된다`() {
|
||||||
val state = response(
|
val state = response(
|
||||||
@@ -121,7 +110,6 @@ class HomeFollowingMapperTest {
|
|||||||
val photoContent = state.recentNews.items[3] as HomeFollowingNewsUiItem.Content
|
val photoContent = state.recentNews.items[3] as HomeFollowingNewsUiItem.Content
|
||||||
assertEquals(FollowingNewsType.PHOTO_CONTENT, photoContent.type)
|
assertEquals(FollowingNewsType.PHOTO_CONTENT, photoContent.type)
|
||||||
assertEquals(3002L, photoContent.contentId)
|
assertEquals(3002L, photoContent.contentId)
|
||||||
assertEquals(R.string.screen_home_following_photo_content, photoContent.labelResId)
|
|
||||||
|
|
||||||
val community = state.recentNews.items[4] as HomeFollowingNewsUiItem.Community
|
val community = state.recentNews.items[4] as HomeFollowingNewsUiItem.Community
|
||||||
assertEquals(4001L, community.postId)
|
assertEquals(4001L, community.postId)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -56,10 +56,16 @@ class HomeFollowingViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `loadFollowing은 loading 후 content 상태를 발행한다`() {
|
fun `loadFollowing은 loading 후 content 상태를 발행한다`() {
|
||||||
api.enqueueSuccess(response(followingCreators = listOf(creator())))
|
api.enqueueSuccess(response(followingCreators = listOf(creator())))
|
||||||
|
val states = mutableListOf<HomeFollowingUiState>()
|
||||||
|
val observer = Observer<HomeFollowingUiState> { states.add(it) }
|
||||||
|
|
||||||
|
viewModel.followingStateLiveData.observeForever(observer)
|
||||||
viewModel.loadFollowing()
|
viewModel.loadFollowing()
|
||||||
|
viewModel.followingStateLiveData.removeObserver(observer)
|
||||||
|
|
||||||
val state = viewModel.followingStateLiveData.requireValue() as HomeFollowingUiState.Content
|
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)
|
assertEquals(1L, state.followingCreators.items.single().creatorId)
|
||||||
assertFalse(viewModel.isLoading.requireValue() ?: true)
|
assertFalse(viewModel.isLoading.requireValue() ?: true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -720,6 +720,7 @@ class HomeMainFragmentLayoutTest {
|
|||||||
link = "https://example.com"
|
link = "https://example.com"
|
||||||
).toUiModel()
|
).toUiModel()
|
||||||
val activeCreator = HomeActiveCreatorItem(
|
val activeCreator = HomeActiveCreatorItem(
|
||||||
|
creatorId = 45L,
|
||||||
creatorNickname = "활동 크리에이터",
|
creatorNickname = "활동 크리에이터",
|
||||||
creatorProfileImage = "https://example.com/active.png",
|
creatorProfileImage = "https://example.com/active.png",
|
||||||
activityType = "AUDIO",
|
activityType = "AUDIO",
|
||||||
@@ -749,6 +750,7 @@ class HomeMainFragmentLayoutTest {
|
|||||||
assertEquals(30L, banner.creatorId)
|
assertEquals(30L, banner.creatorId)
|
||||||
assertEquals(40L, banner.seriesId)
|
assertEquals(40L, banner.seriesId)
|
||||||
assertEquals("https://example.com", banner.link)
|
assertEquals("https://example.com", banner.link)
|
||||||
|
assertEquals(45L, activeCreator.creatorId)
|
||||||
assertEquals("활동 크리에이터", activeCreator.nickname)
|
assertEquals("활동 크리에이터", activeCreator.nickname)
|
||||||
assertEquals("https://example.com/active.png", activeCreator.profileImage)
|
assertEquals("https://example.com/active.png", activeCreator.profileImage)
|
||||||
assertEquals("방금 전", activeCreator.activityAt)
|
assertEquals("방금 전", activeCreator.activityAt)
|
||||||
@@ -904,6 +906,11 @@ class HomeMainFragmentLayoutTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `home recent activity route maps activity type to target destination`() {
|
fun `home recent activity route maps activity type to target destination`() {
|
||||||
|
assertEquals(
|
||||||
|
HomeRecommendationRecentlyActiveCreatorRoute.LiveRoom(10L),
|
||||||
|
recentActivityCreator(CreatorActivityType.Live, targetId = 10L)
|
||||||
|
.toHomeRecommendationRecentlyActiveCreatorRoute()
|
||||||
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
HomeRecommendationRecentlyActiveCreatorRoute.AudioContent(11L),
|
HomeRecommendationRecentlyActiveCreatorRoute.AudioContent(11L),
|
||||||
recentActivityCreator(CreatorActivityType.LiveReplay, targetId = 11L)
|
recentActivityCreator(CreatorActivityType.LiveReplay, targetId = 11L)
|
||||||
@@ -922,12 +929,41 @@ class HomeMainFragmentLayoutTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `home recent activity route ignores live unknown and missing target`() {
|
fun `홈 최근 활동 LIVE는 target 유무에 따라 라이브 룸 또는 크리에이터 채널로 이동한다`() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
null,
|
HomeRecommendationRecentlyActiveCreatorRoute.LiveRoom(10L),
|
||||||
recentActivityCreator(CreatorActivityType.Live, targetId = 10L)
|
recentActivityCreator(CreatorActivityType.Live, targetId = 10L, creatorId = 20L)
|
||||||
.toHomeRecommendationRecentlyActiveCreatorRoute()
|
.toHomeRecommendationRecentlyActiveCreatorRoute()
|
||||||
)
|
)
|
||||||
|
assertEquals(
|
||||||
|
HomeRecommendationRecentlyActiveCreatorRoute.CreatorChannel(20L),
|
||||||
|
recentActivityCreator(CreatorActivityType.Live, targetId = null, creatorId = 20L)
|
||||||
|
.toHomeRecommendationRecentlyActiveCreatorRoute()
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
null,
|
||||||
|
recentActivityCreator(CreatorActivityType.Live, targetId = 0L, creatorId = 20L)
|
||||||
|
.toHomeRecommendationRecentlyActiveCreatorRoute()
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
null,
|
||||||
|
recentActivityCreator(CreatorActivityType.Live, targetId = -1L, creatorId = 20L)
|
||||||
|
.toHomeRecommendationRecentlyActiveCreatorRoute()
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
null,
|
||||||
|
recentActivityCreator(CreatorActivityType.Live, targetId = null, creatorId = 0L)
|
||||||
|
.toHomeRecommendationRecentlyActiveCreatorRoute()
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
null,
|
||||||
|
recentActivityCreator(CreatorActivityType.Live, targetId = null, creatorId = -1L)
|
||||||
|
.toHomeRecommendationRecentlyActiveCreatorRoute()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `홈 최근 활동 route는 알 수 없는 activity와 유효하지 않은 target을 무시한다`() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
null,
|
null,
|
||||||
recentActivityCreator(activityType = null, targetId = 10L).toHomeRecommendationRecentlyActiveCreatorRoute()
|
recentActivityCreator(activityType = null, targetId = 10L).toHomeRecommendationRecentlyActiveCreatorRoute()
|
||||||
@@ -986,18 +1022,6 @@ class HomeMainFragmentLayoutTest {
|
|||||||
assertEquals(null, aiCharacter(creatorId = -1L).toHomeRecommendationAiCharacterRoute())
|
assertEquals(null, aiCharacter(creatorId = -1L).toHomeRecommendationAiCharacterRoute())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `home popular community adapter applies blur when locked paid post image is loaded`() {
|
|
||||||
val source = projectFile(
|
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomePopularCommunityAdapter.kt"
|
|
||||||
).readText()
|
|
||||||
|
|
||||||
assertTrue(source.contains("imageUrl = item.item.imageUrl"))
|
|
||||||
assertTrue(source.contains("shouldBlur = item.item.price > 0 && !item.item.existOrdered"))
|
|
||||||
assertTrue(source.contains("BlurTransformation(imageView.context, 25f, 2.5f)"))
|
|
||||||
assertFalse(source.contains("item.item.imageUrl.takeIf { item.item.price <= 0 || item.item.existOrdered }"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `home live section height follows its items`() {
|
fun `home live section height follows its items`() {
|
||||||
val root = inflateView(R.layout.fragment_v2_main_home)
|
val root = inflateView(R.layout.fragment_v2_main_home)
|
||||||
@@ -1019,6 +1043,28 @@ class HomeMainFragmentLayoutTest {
|
|||||||
assertEquals(14.dpToPx(), layoutParams.marginEnd)
|
assertEquals(14.dpToPx(), layoutParams.marginEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `홈 라이브 어댑터는 라이브가 있으면 전체 항목을 추가한다`() {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
val parent = RecyclerView(context)
|
||||||
|
parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
|
||||||
|
val adapter = HomeLiveAdapter()
|
||||||
|
|
||||||
|
adapter.submitItems(emptyList())
|
||||||
|
assertEquals(0, adapter.itemCount)
|
||||||
|
|
||||||
|
adapter.submitItems(listOf(liveItem(1L)))
|
||||||
|
assertEquals(2, adapter.itemCount)
|
||||||
|
val viewHolder = adapter.onCreateViewHolder(parent, adapter.getItemViewType(1))
|
||||||
|
assertEquals(
|
||||||
|
context.getString(R.string.screen_home_theme_all),
|
||||||
|
(viewHolder.itemView as TextView).text.toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
adapter.submitItems((1L..20L).map(::liveItem))
|
||||||
|
assertEquals(21, adapter.itemCount)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `home live adapter appends all item after twenty lives`() {
|
fun `home live adapter appends all item after twenty lives`() {
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
@@ -1262,6 +1308,73 @@ class HomeMainFragmentLayoutTest {
|
|||||||
assertFalse(source.contains("followCreators(genreSectionKey"))
|
assertFalse(source.contains("followCreators(genreSectionKey"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `응원 크리에이터 모두 팔로우는 API 요청 전에 확인창을 연다`() {
|
||||||
|
val source = homeMainFragmentSource()
|
||||||
|
|
||||||
|
assertTrue(source.contains("import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog"))
|
||||||
|
assertTrue(source.contains("private fun showCheerCreatorsFollowAllConfirmDialog(creatorIds: List<Long>)"))
|
||||||
|
assertTrue(
|
||||||
|
source.contains(
|
||||||
|
"onCheerFollowAllClick = { creatorIds ->\n" +
|
||||||
|
" ensureV2Access(AccessRequirement.Login) {\n" +
|
||||||
|
" showCheerCreatorsFollowAllConfirmDialog(creatorIds)\n" +
|
||||||
|
" }\n" +
|
||||||
|
" }"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertTrue(source.contains("V2ModalDialog("))
|
||||||
|
assertTrue(source.contains("title = getString(R.string.home_recommendation_follow_all_confirm_title)"))
|
||||||
|
assertTrue(source.contains("desc = getString(R.string.home_recommendation_follow_all_confirm_description)"))
|
||||||
|
assertTrue(
|
||||||
|
source.contains(
|
||||||
|
"confirmButtonTitle = getString(R.string.home_recommendation_follow_all_confirm_button)"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertTrue(source.contains("cancelButtonTitle = getString(R.string.cancel)"))
|
||||||
|
assertTrue(
|
||||||
|
source.contains(
|
||||||
|
"confirmButtonClick = {\n" +
|
||||||
|
" homeRecommendationViewModel.followCreators(SECTION_KEY_CHEER_CREATORS, creatorIds)\n" +
|
||||||
|
" }"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertTrue(source.contains(".show(screenWidth)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `모두 팔로우 확인창 문자열은 다국어로 제공한다`() {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
val koreanContext = context.createConfigurationContext(
|
||||||
|
Configuration(context.resources.configuration).apply { setLocale(Locale.KOREAN) }
|
||||||
|
)
|
||||||
|
val englishContext = context.createConfigurationContext(
|
||||||
|
Configuration(context.resources.configuration).apply { setLocale(Locale.ENGLISH) }
|
||||||
|
)
|
||||||
|
val japaneseContext = context.createConfigurationContext(
|
||||||
|
Configuration(context.resources.configuration).apply { setLocale(Locale.JAPANESE) }
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("모두 팔로우", koreanContext.getString(R.string.home_recommendation_follow_all_confirm_title))
|
||||||
|
assertEquals(
|
||||||
|
"목록의 크리에이터를 모두 팔로우하시겠어요?",
|
||||||
|
koreanContext.getString(R.string.home_recommendation_follow_all_confirm_description)
|
||||||
|
)
|
||||||
|
assertEquals("팔로우 하기", koreanContext.getString(R.string.home_recommendation_follow_all_confirm_button))
|
||||||
|
assertEquals("Follow all", englishContext.getString(R.string.home_recommendation_follow_all_confirm_title))
|
||||||
|
assertEquals(
|
||||||
|
"Would you like to follow all creators in this list?",
|
||||||
|
englishContext.getString(R.string.home_recommendation_follow_all_confirm_description)
|
||||||
|
)
|
||||||
|
assertEquals("Follow", englishContext.getString(R.string.home_recommendation_follow_all_confirm_button))
|
||||||
|
assertEquals("すべてフォロー", japaneseContext.getString(R.string.home_recommendation_follow_all_confirm_title))
|
||||||
|
assertEquals(
|
||||||
|
"このリストのクリエイターをすべてフォローしますか?",
|
||||||
|
japaneseContext.getString(R.string.home_recommendation_follow_all_confirm_description)
|
||||||
|
)
|
||||||
|
assertEquals("フォローする", japaneseContext.getString(R.string.home_recommendation_follow_all_confirm_button))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `home recommendation viewmodel does not keep android context`() {
|
fun `home recommendation viewmodel does not keep android context`() {
|
||||||
val viewModelSource = homeRecommendationViewModelSource()
|
val viewModelSource = homeRecommendationViewModelSource()
|
||||||
@@ -1393,9 +1506,11 @@ class HomeMainFragmentLayoutTest {
|
|||||||
|
|
||||||
private fun recentActivityCreator(
|
private fun recentActivityCreator(
|
||||||
activityType: CreatorActivityType?,
|
activityType: CreatorActivityType?,
|
||||||
targetId: Long?
|
targetId: Long?,
|
||||||
|
creatorId: Long = 20L
|
||||||
): HomeRecommendationRecentlyActiveCreatorUiModel {
|
): HomeRecommendationRecentlyActiveCreatorUiModel {
|
||||||
return HomeRecommendationRecentlyActiveCreatorUiModel(
|
return HomeRecommendationRecentlyActiveCreatorUiModel(
|
||||||
|
creatorId = creatorId,
|
||||||
nickname = "크리에이터",
|
nickname = "크리에이터",
|
||||||
profileImage = "",
|
profileImage = "",
|
||||||
activityType = activityType,
|
activityType = activityType,
|
||||||
|
|||||||
@@ -57,11 +57,11 @@ class HomeMainFragmentLoginGuardSourceTest {
|
|||||||
).readText()
|
).readText()
|
||||||
|
|
||||||
val clickSource = source.substringFrom("private fun onFollowingNewsClick(item: HomeFollowingNewsUiItem)")
|
val clickSource = source.substringFrom("private fun onFollowingNewsClick(item: HomeFollowingNewsUiItem)")
|
||||||
assertTrue(clickSource.contains("val community = item as? HomeFollowingNewsUiItem.Community ?: return"))
|
assertTrue(clickSource.contains("is HomeFollowingNewsUiItem.Community -> {"))
|
||||||
assertTrue(clickSource.contains("val postId = community.postId"))
|
assertTrue(clickSource.contains("val postId = item.postId"))
|
||||||
assertBefore(
|
assertBefore(
|
||||||
clickSource,
|
clickSource,
|
||||||
"val community = item as? HomeFollowingNewsUiItem.Community ?: return",
|
"is HomeFollowingNewsUiItem.Community -> {",
|
||||||
"handleCommunityAction"
|
"handleCommunityAction"
|
||||||
)
|
)
|
||||||
assertBefore(
|
assertBefore(
|
||||||
@@ -75,6 +75,34 @@ class HomeMainFragmentLoginGuardSourceTest {
|
|||||||
assertFalse(clickSource.contains("EXTRA_COMMUNITY_POST_ID"))
|
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
|
@Test
|
||||||
fun `HomeMainFragment 팔로잉 탭 선택은 로그인 가드 통과 후 탭을 전환한다`() {
|
fun `HomeMainFragment 팔로잉 탭 선택은 로그인 가드 통과 후 탭을 전환한다`() {
|
||||||
val source = projectFile(
|
val source = projectFile(
|
||||||
@@ -125,6 +153,63 @@ class HomeMainFragmentLoginGuardSourceTest {
|
|||||||
assertTrue(source.contains("private val liveActionCoordinator: LiveActionCoordinator by lazy"))
|
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(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
val clickSource = source.substringFrom(
|
||||||
|
"private fun onRecentActivityClick(item: HomeRecommendationRecentlyActiveCreatorUiModel)"
|
||||||
|
)
|
||||||
|
assertTrue(clickSource.contains("liveActionCoordinator.enterLiveRoom(route.roomId)"))
|
||||||
|
assertTrue(clickSource.contains("handleCreatorAction("))
|
||||||
|
assertTrue(clickSource.contains("CreatorActionCommand.Profile(route.creatorId)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `HomeMainFragment 응원 크리에이터 모두 팔로우는 확인창보다 로그인 가드를 먼저 실행한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
val setupSource = source.substringFrom("private fun setUpRecommendationAdapters()")
|
||||||
|
assertTrue(setupSource.contains("onCheerFollowAllClick = { creatorIds ->"))
|
||||||
|
assertBefore(
|
||||||
|
setupSource,
|
||||||
|
"ensureV2Access(AccessRequirement.Login)",
|
||||||
|
"showCheerCreatorsFollowAllConfirmDialog(creatorIds)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `HomeMainFragment loading dialog는 통합 loading state로 제어한다`() {
|
fun `HomeMainFragment loading dialog는 통합 loading state로 제어한다`() {
|
||||||
val source = projectFile(
|
val source = projectFile(
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main.home
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import coil.load
|
||||||
|
import io.mockk.Runs
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import io.mockk.verify
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
|
||||||
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPaidStatus
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostSection
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostUiModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.home.ui.HomePopularCommunityAdapter
|
||||||
|
import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem
|
||||||
|
import org.junit.After
|
||||||
|
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
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(sdk = [28], application = Application::class)
|
||||||
|
class HomePopularCommunityAdapterTest {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
if (!ImageLoaderProvider.isInitialized) {
|
||||||
|
ImageLoaderProvider.init(RuntimeEnvironment.getApplication())
|
||||||
|
}
|
||||||
|
mockkStatic("kr.co.vividnext.sodalive.extensions.ImageExtensionsKt")
|
||||||
|
every { any<ImageView>().loadUrl(any(), any()) } just Runs
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkStatic("kr.co.vividnext.sodalive.extensions.ImageExtensionsKt")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `유료 미구매 bind는 원본을 로드하지 않고 이전 요청과 drawable을 제거한다`() {
|
||||||
|
val adapter = adapterWith(
|
||||||
|
communityPost(
|
||||||
|
imageUrl = "https://example.com/locked.png",
|
||||||
|
price = 30,
|
||||||
|
existOrdered = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val holder = createViewHolder(adapter)
|
||||||
|
val imageView = holder.itemView.findViewById<ImageView>(R.id.iv_feed_community_image)
|
||||||
|
val disposable = imageView.load(ColorDrawable(Color.BLUE), ImageLoaderProvider.imageLoader)
|
||||||
|
assertFalse(disposable.isDisposed)
|
||||||
|
|
||||||
|
adapter.onBindViewHolder(holder, 0)
|
||||||
|
|
||||||
|
verify(exactly = 0) { imageView.loadUrl(any(), any()) }
|
||||||
|
assertTrue(disposable.isDisposed)
|
||||||
|
assertNull(imageView.drawable)
|
||||||
|
assertEquals(
|
||||||
|
View.VISIBLE,
|
||||||
|
holder.itemView.findViewById<View>(R.id.ll_feed_community_paid_overlay).visibility
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"30",
|
||||||
|
holder.itemView.findViewById<TextView>(R.id.tv_feed_community_price).text.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `무료와 구매 완료 bind는 원본 이미지 URL을 로더에 전달한다`() {
|
||||||
|
val freeImageUrl = "https://example.com/free.png"
|
||||||
|
val purchasedImageUrl = "https://example.com/purchased.png"
|
||||||
|
val adapter = adapterWith(
|
||||||
|
communityPost(imageUrl = freeImageUrl, price = 0, existOrdered = false),
|
||||||
|
communityPost(imageUrl = purchasedImageUrl, price = 30, existOrdered = true)
|
||||||
|
)
|
||||||
|
val holder = createViewHolder(adapter)
|
||||||
|
val imageView = holder.itemView.findViewById<ImageView>(R.id.iv_feed_community_image)
|
||||||
|
|
||||||
|
adapter.onBindViewHolder(holder, 0)
|
||||||
|
adapter.onBindViewHolder(holder, 1)
|
||||||
|
|
||||||
|
verify(exactly = 1) { imageView.loadUrl(freeImageUrl, any()) }
|
||||||
|
verify(exactly = 1) { imageView.loadUrl(purchasedImageUrl, any()) }
|
||||||
|
assertEquals(View.GONE, holder.itemView.findViewById<View>(R.id.ll_feed_community_paid_overlay).visibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adapterWith(
|
||||||
|
vararg items: HomeRecommendationPopularCommunityPostUiModel
|
||||||
|
): HomePopularCommunityAdapter {
|
||||||
|
return HomePopularCommunityAdapter(onClickItem = {}).apply {
|
||||||
|
submitSection(HomeRecommendationPopularCommunityPostSection(items.toList()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createViewHolder(
|
||||||
|
adapter: HomePopularCommunityAdapter
|
||||||
|
): HomePopularCommunityAdapter.CommunityViewHolder {
|
||||||
|
val context = RuntimeEnvironment.getApplication()
|
||||||
|
val parent = RecyclerView(context).apply {
|
||||||
|
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||||
|
}
|
||||||
|
return adapter.onCreateViewHolder(parent, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun communityPost(
|
||||||
|
imageUrl: String,
|
||||||
|
price: Int,
|
||||||
|
existOrdered: Boolean
|
||||||
|
): HomeRecommendationPopularCommunityPostUiModel {
|
||||||
|
val paidStatus = when {
|
||||||
|
price == 0 -> HomeRecommendationPaidStatus.Free
|
||||||
|
existOrdered -> HomeRecommendationPaidStatus.Purchased
|
||||||
|
else -> HomeRecommendationPaidStatus.Paid(price)
|
||||||
|
}
|
||||||
|
return HomeRecommendationPopularCommunityPostUiModel(
|
||||||
|
item = FeedItem.Community(
|
||||||
|
feedId = "feed-1",
|
||||||
|
creatorId = "creator-1",
|
||||||
|
creatorName = "크리에이터",
|
||||||
|
creatorImageUrl = "",
|
||||||
|
postId = "post-1",
|
||||||
|
bodyText = "본문",
|
||||||
|
keywordText = "",
|
||||||
|
createdAtText = "방금 전",
|
||||||
|
commentCount = 1,
|
||||||
|
likeCount = 2,
|
||||||
|
imageUrl = imageUrl,
|
||||||
|
audioUrl = null,
|
||||||
|
price = price,
|
||||||
|
existOrdered = existOrdered,
|
||||||
|
showKeyword = false
|
||||||
|
),
|
||||||
|
paidStatus = paidStatus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,27 +42,6 @@ class FeedViewTest {
|
|||||||
assertSame(category.parent, time.parent)
|
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
|
@Test
|
||||||
fun `content root clipping is implemented in custom view code`() {
|
fun `content root clipping is implemented in custom view code`() {
|
||||||
val layout = projectFile("app/src/main/res/layout/view_feed_content.xml").readText()
|
val layout = projectFile("app/src/main/res/layout/view_feed_content.xml").readText()
|
||||||
|
|||||||
@@ -42,6 +42,192 @@
|
|||||||
- 파일 경로: `docs/20260601_계획문서규칙수정/plan-task.md`
|
- 파일 경로: `docs/20260601_계획문서규칙수정/plan-task.md`
|
||||||
- 검증 기준: 문서 하단에 무엇/왜/어떻게, 실행 명령, 결과를 한국어로 누적 기록한다.
|
- 검증 기준: 문서 하단에 무엇/왜/어떻게, 실행 명령, 결과를 한국어로 누적 기록한다.
|
||||||
|
|
||||||
|
## 2026-07-30 후속 실행 정보
|
||||||
|
|
||||||
|
| 문서 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 상태 | 구현 완료 |
|
||||||
|
| 요구사항 기준 | `docs/20260601_계획문서규칙수정/prd.md`의 `12. 2026-07-30 후속 변경` |
|
||||||
|
| 샘플 기준 | `docs/sample/sample-plan-task.md` |
|
||||||
|
| 현재 Phase | Phase 4: 샘플 문서와 리뷰 보고서 규칙 |
|
||||||
|
| 현재 활성 Goal | 없음(별도 goal 생성 요청 없음) |
|
||||||
|
|
||||||
|
### 목표
|
||||||
|
|
||||||
|
PRD·구현 계획/TASK·리뷰 보고서의 기준 샘플과 유지보수 절차를 명확히 하고, 리뷰에서 확정된 수정 항목을 해당 Phase의 신규 Task로 바로 전환할 수 있게 한다.
|
||||||
|
|
||||||
|
### 범위
|
||||||
|
|
||||||
|
#### 포함
|
||||||
|
|
||||||
|
- `AGENTS.md`의 작업 절차 핵심 규칙
|
||||||
|
- `docs/agent-guides/work-plan-docs.md`의 상세 작성·유지보수·리뷰 후속 규칙
|
||||||
|
- 기존 PRD와 계획/TASK 문서의 후속 요구사항·Task·검증 기록 누적
|
||||||
|
|
||||||
|
#### 제외
|
||||||
|
|
||||||
|
- 과거 작업 문서의 일괄 변환
|
||||||
|
- Android 앱 소스와 빌드 설정 변경
|
||||||
|
- 일반 빌드·테스트·린트 결과를 위한 리뷰 보고서 생성
|
||||||
|
- 실제 코드 리뷰를 수행하지 않는 이번 문서 정비 작업의 `reviews/` 폴더 생성
|
||||||
|
|
||||||
|
### 기술적 제약
|
||||||
|
|
||||||
|
- Markdown 문서만 수정한다.
|
||||||
|
- 사용자 작업인 `docs/prd/sample-prd.md` 삭제와 `docs/sample/`의 신규 샘플 3종을 변경하지 않는다.
|
||||||
|
- 기존 완료 Task와 검증 기록을 삭제하거나 미완료로 되돌리지 않는다.
|
||||||
|
- 제공되지 않은 리뷰 파일 이름이나 범위는 미리 만들지 않는다.
|
||||||
|
- 문서 Task는 production 동작을 변경하지 않아 TDD를 적용하지 않고 경로·문구 대조와 명령 검증으로 대체한다.
|
||||||
|
|
||||||
|
### Phase 4: 샘플 문서와 리뷰 보고서 규칙
|
||||||
|
|
||||||
|
**Phase 결과:** 에이전트가 세 샘플의 정확한 위치를 따라 문서를 작성하고, 코드 리뷰 결과와 확정 수정 항목을 일관된 경로와 Task로 추적할 수 있다.
|
||||||
|
|
||||||
|
**선행조건:** Phase 3 완료와 `DEC-001~003` 확정.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P4-T1`~`P4-T3`, `P4-T5`, `P4-GATE`, `P4-GATE-2`의 체크리스트와 완료 증거가 모두 충족되고 검증 기록이 누적된다.
|
||||||
|
|
||||||
|
#### Task 4.1 후속 요구사항과 실행 계획 확정
|
||||||
|
|
||||||
|
**Goal 실행 `P4-T1`:** 새 샘플과 사용자 결정을 기존 PRD·계획/TASK 문서에 추적 가능한 후속 범위로 기록한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `docs/sample/`의 샘플 3종과 사용자 결정 확인.
|
||||||
|
- **완료 증거:** PRD의 `DOC-001~004`, `REV-001~005`, `DEC-001~003`과 이 Phase의 Task·완료 조건.
|
||||||
|
- **범위 밖:** 규칙 문서와 샘플 원문 수정.
|
||||||
|
- **TDD 예외 사유:** production 동작을 변경하지 않는 문서 계획 Task다.
|
||||||
|
- **대체 검증 방법:** 샘플 경로, 요구사항 ID, Task 연결을 `rg`와 `git diff --check`로 대조한다.
|
||||||
|
|
||||||
|
- [x] `docs/sample/sample-prd.md`, `docs/sample/sample-plan-task.md`, `docs/sample/sample-review.md`의 구조와 경로를 확인한다.
|
||||||
|
- [x] 사용자 결정과 리뷰 후속 절차를 `prd.md`에 누적한다.
|
||||||
|
- [x] 기존 완료 Task를 유지하고 Phase 4 실행 계획을 추가한다.
|
||||||
|
|
||||||
|
#### Task 4.2 `AGENTS.md` 핵심 규칙 갱신
|
||||||
|
|
||||||
|
**Goal 실행 `P4-T2`:** 작업 시작 시 샘플 위치와 리뷰 보고서·후속 수정의 필수 순서를 바로 확인할 수 있게 한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P4-T1` 완료.
|
||||||
|
- **완료 증거:** `DOC-001~004`, `REV-001~005`의 핵심 원칙이 `AGENTS.md`에 요약되고 상세 가이드 경로가 유지된다.
|
||||||
|
- **범위 밖:** 상세 템플릿 전체 복제와 다른 실행 원칙 수정.
|
||||||
|
- **TDD 예외 사유:** 에이전트 안내 문구만 변경하는 문서 Task다.
|
||||||
|
- **대체 검증 방법:** 세 샘플 경로, `reviews/` 경로, 해당 Phase 신규 Task 선행 규칙을 검색해 확인한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `AGENTS.md`
|
||||||
|
|
||||||
|
- [x] 기존 `sample-prd.md` 안내를 `docs/sample/sample-prd.md`의 정확한 경로로 바꾼다.
|
||||||
|
- [x] 구현 계획/TASK 문서가 `docs/sample/sample-plan-task.md`를 참조하도록 명시한다.
|
||||||
|
- [x] 리뷰 보고서 대상·샘플·저장 경로와 일반 검증 제외 규칙을 요약한다.
|
||||||
|
- [x] 확정 리뷰 항목은 코드 수정 전에 해당 Phase의 신규 회귀 수정 Task로 추가하도록 명시한다.
|
||||||
|
- [x] 기존 완료·검증·리뷰 기록 보존 규칙을 유지한다.
|
||||||
|
|
||||||
|
검증 기록(2026-07-30):
|
||||||
|
|
||||||
|
- `rg -n "docs/sample/sample-(prd|plan-task|review)\\.md|reviews/|신규 회귀 수정 Task|기존 완료 Task|빌드.*테스트.*린트|샘플.*변경" AGENTS.md` 실행 결과, 세 샘플 경로와 리뷰 보고서·후속 Task·기록 보존 규칙을 확인했다.
|
||||||
|
- `git diff --check -- AGENTS.md`는 출력 없이 exit code 0으로 통과했다.
|
||||||
|
|
||||||
|
#### Task 4.3 `work-plan-docs.md` 상세 규칙 갱신
|
||||||
|
|
||||||
|
**Goal 실행 `P4-T3`:** 문서 작성, 샘플 동기화, 리뷰 보고서 작성, 확정 항목의 수정 전환 절차를 하나의 상세 가이드에서 확인할 수 있게 한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P4-T2` 완료.
|
||||||
|
- **완료 증거:** PRD·계획/TASK·리뷰 보고서별 샘플과 작성 절차, 문서 유지보수 규칙, 리뷰 후속 Task 전환 규칙이 구체적으로 기록된다.
|
||||||
|
- **범위 밖:** 샘플 문서 원문 변경과 코드 리뷰 수행.
|
||||||
|
- **TDD 예외 사유:** 작업 절차 문구만 변경하는 문서 Task다.
|
||||||
|
- **대체 검증 방법:** 요구사항별 문구 대조표와 경로 검색, Markdown diff 검증을 사용한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `docs/agent-guides/work-plan-docs.md`
|
||||||
|
|
||||||
|
- [x] PRD와 계획/TASK 문서의 기준 샘플 경로와 placeholder 구체화 규칙을 명시한다.
|
||||||
|
- [x] 리뷰 보고서 생성 대상, `sample-review.md`, `reviews/[리뷰범위]-review.md` 경로를 명시한다.
|
||||||
|
- [x] 일반 빌드·테스트·린트 결과는 Task별 검증 기록 또는 `Verification Log`에 남기도록 구분한다.
|
||||||
|
- [x] 확정 리뷰 항목을 review ID와 연결한 해당 Phase 신규 회귀 수정 Task로 먼저 추가하고 바로 수정하는 순서를 명시한다.
|
||||||
|
- [x] 샘플 위치·구조·작성 규칙 변경 시 `AGENTS.md`와 관련 가이드를 함께 동기화하도록 유지보수 규칙을 추가한다.
|
||||||
|
|
||||||
|
검증 기록(2026-07-30):
|
||||||
|
|
||||||
|
- `rg -n "docs/sample/sample-(prd|plan-task|review)\\.md|reviews/|신규 회귀 수정 Task|기존 완료 Task|빌드.*테스트.*린트|샘플.*변경" docs/agent-guides/work-plan-docs.md` 실행 결과, 기준 샘플·리뷰 보고서·확정 항목 전환·유지보수 규칙을 확인했다.
|
||||||
|
- `rg -n '### Phase [0-9]|docs/prd/sample-prd\\.md|체크박스.*Task N\\.N' AGENTS.md docs/agent-guides/work-plan-docs.md`는 결과가 없어 폐기된 경로와 이전 Phase/Task 형식 안내가 남지 않았음을 확인했다.
|
||||||
|
- `git diff --check -- docs/agent-guides/work-plan-docs.md`는 출력 없이 exit code 0으로 통과했다.
|
||||||
|
|
||||||
|
#### Phase 4 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P4-GATE`:** 샘플 경로, 리뷰 보고서 범위, 확정 항목 전환 순서, 기존 기록 보존 규칙의 문서 간 정합성을 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P4-T1`~`P4-T3` 완료.
|
||||||
|
- **완료 증거:** 아래 명령이 성공하고 실제 결과가 `검증 기록`에 누적된다.
|
||||||
|
- **범위 밖:** 앱 빌드·테스트와 과거 문서 일괄 정비.
|
||||||
|
|
||||||
|
- [x] **Task 4.4: 문서 정합성 및 명령 유효성 검증**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "docs/sample/sample-(prd|plan-task|review)\\.md|reviews/|신규 회귀 수정 Task|빌드.*테스트.*린트" AGENTS.md docs/agent-guides/work-plan-docs.md docs/20260601_계획문서규칙수정
|
||||||
|
git diff --check
|
||||||
|
./gradlew tasks --all
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** 세 샘플의 정확한 경로와 리뷰 후속 절차가 문서 간 일치하고, whitespace 오류가 없으며 Gradle task 목록 조회가 exit code 0으로 끝난다.
|
||||||
|
|
||||||
|
검증 기록(2026-07-30):
|
||||||
|
|
||||||
|
- 문서 검색은 세 샘플 경로, 리뷰 보고서 대상, 일반 검증 제외, 확정 항목의 신규 회귀 수정 Task 전환 규칙을 모두 찾고 exit code 0으로 끝났다.
|
||||||
|
- `git diff --check`는 출력 없이 exit code 0으로 통과했다.
|
||||||
|
- 최초 `./gradlew tasks --all`은 sandbox가 `~/.gradle` lock 파일 접근을 차단해 실패했다. 같은 명령을 승인된 권한 범위에서 재실행해 `BUILD SUCCESSFUL in 24s`, `1 actionable task: 1 executed`를 확인했다.
|
||||||
|
|
||||||
|
#### Task 4.5 리뷰 범위 파일명 형식 추가
|
||||||
|
|
||||||
|
**Goal 실행 `P4-T5`:** 리뷰 보고서명만으로 대상 Phase와 구현 내용을 식별할 수 있게 `[리뷰범위]` 형식을 고정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `DEC-004`, `REV-006` 확정.
|
||||||
|
- **완료 증거:** `AGENTS.md`와 `work-plan-docs.md`에 `phase<번호>-<구현 내용을 나타내는 영문 kebab-case>` 형식과 실제 파일명 예시가 일치하게 기록된다.
|
||||||
|
- **범위 밖:** 기존 리뷰 보고서 파일의 일괄 이름 변경과 샘플 문서 원문 수정.
|
||||||
|
- **TDD 예외 사유:** 파일명 작성 규칙만 변경하는 문서 Task다.
|
||||||
|
- **대체 검증 방법:** 형식·예시 검색, 전체 diff whitespace 검사, Gradle task 목록 조회로 검증한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `AGENTS.md`
|
||||||
|
- Modify: `docs/agent-guides/work-plan-docs.md`
|
||||||
|
- Modify: `docs/20260601_계획문서규칙수정/prd.md`
|
||||||
|
- Modify: `docs/20260601_계획문서규칙수정/plan-task.md`
|
||||||
|
|
||||||
|
- [x] PRD에 `REV-006`, `DEC-004`, 파일명 수용 기준을 추가한다.
|
||||||
|
- [x] `AGENTS.md`에 `[리뷰범위]` 형식과 예시를 추가한다.
|
||||||
|
- [x] `work-plan-docs.md`에 `[리뷰범위]` 구성 요소와 예시를 추가한다.
|
||||||
|
- [x] 문서 간 형식·예시가 일치하는지 검증한다.
|
||||||
|
|
||||||
|
검증 기록(2026-07-30):
|
||||||
|
|
||||||
|
- 형식·예시 검색으로 네 문서가 같은 신규 규칙을 안내함을 확인했다.
|
||||||
|
- `AGENTS.md`, `work-plan-docs.md`, `prd.md`를 대상으로 `phase-2-review.md`를 검색한 결과가 없어 실제 규칙에는 이전 예시가 남지 않았음을 확인했다. `plan-task.md`에는 검증에 사용한 검색어가 과거 실행 기록으로만 남아 있다.
|
||||||
|
- `git diff HEAD --check`는 출력 없이 exit code 0으로 통과했다.
|
||||||
|
|
||||||
|
#### Phase 4 재검증 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P4-GATE-2`:** 후속 파일명 규칙이 기존 리뷰 보고서 경로·후속 수정 절차와 충돌하지 않는지 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P4-T5` 완료.
|
||||||
|
- **완료 증거:** 아래 명령이 성공하고 실제 결과가 `검증 기록`에 누적된다.
|
||||||
|
- **범위 밖:** 기존 리뷰 보고서 파일 이름 변경.
|
||||||
|
|
||||||
|
- [x] **Task 4.6: 리뷰 보고서명 규칙 재검증**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "phase<번호>-<구현 내용을 나타내는 영문 kebab-case>|phase2-main-home-recommendation-review\\.md" AGENTS.md docs/agent-guides/work-plan-docs.md docs/20260601_계획문서규칙수정
|
||||||
|
git diff HEAD --check
|
||||||
|
./gradlew tasks --all
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** 네 문서가 같은 `[리뷰범위]` 형식과 예시를 안내하고, whitespace 오류가 없으며 Gradle task 목록 조회가 exit code 0으로 끝난다.
|
||||||
|
|
||||||
|
검증 기록(2026-07-30):
|
||||||
|
|
||||||
|
- 형식·예시 검색은 네 문서에서 `phase<번호>-<구현 내용을 나타내는 영문 kebab-case>`와 `phase2-main-home-recommendation-review.md`를 확인하고 exit code 0으로 끝났다.
|
||||||
|
- `git diff HEAD --check`는 출력 없이 exit code 0으로 통과했다.
|
||||||
|
- `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 4s`, `1 actionable task: 1 executed`로 통과했다.
|
||||||
|
|
||||||
## 검증 기록
|
## 검증 기록
|
||||||
- 2026-06-01
|
- 2026-06-01
|
||||||
- 무엇/왜/어떻게: 계획 문서 규칙 수정 요청에 따라 기존 작업 절차 문서와 `sample-prd.md` 위치를 확인하고, 신규 작업 문서부터 `docs/[날짜]_구현할내용한글/prd.md`, `docs/[날짜]_구현할내용한글/plan-task.md` 구조를 적용하도록 규칙을 수정했다.
|
- 무엇/왜/어떻게: 계획 문서 규칙 수정 요청에 따라 기존 작업 절차 문서와 `sample-prd.md` 위치를 확인하고, 신규 작업 문서부터 `docs/[날짜]_구현할내용한글/prd.md`, `docs/[날짜]_구현할내용한글/plan-task.md` 구조를 적용하도록 규칙을 수정했다.
|
||||||
@@ -64,3 +250,26 @@
|
|||||||
- 결과:
|
- 결과:
|
||||||
- `AGENTS.md`와 `docs/agent-guides/work-plan-docs.md`에 신규 문서 경로와 기존 문서 유지 조건이 반영됐음을 확인했다.
|
- `AGENTS.md`와 `docs/agent-guides/work-plan-docs.md`에 신규 문서 경로와 기존 문서 유지 조건이 반영됐음을 확인했다.
|
||||||
- 변경 범위가 `AGENTS.md`, `docs/agent-guides/work-plan-docs.md`, `docs/20260601_계획문서규칙수정/prd.md`, `docs/20260601_계획문서규칙수정/plan-task.md`로 제한됐음을 확인했다.
|
- 변경 범위가 `AGENTS.md`, `docs/agent-guides/work-plan-docs.md`, `docs/20260601_계획문서규칙수정/prd.md`, `docs/20260601_계획문서규칙수정/plan-task.md`로 제한됐음을 확인했다.
|
||||||
|
|
||||||
|
- 2026-07-30
|
||||||
|
- 무엇/왜/어떻게: 새 PRD·계획/TASK·리뷰 보고서 샘플과 코드 리뷰 후속 수정 절차를 작업 규칙에 반영했다. 기존 작업의 연속 범위이므로 새 작업 폴더를 만들지 않고 Phase 4와 후속 요구사항을 기존 문서에 누적했다.
|
||||||
|
- 실행 명령:
|
||||||
|
- `rg -n "docs/sample/sample-(prd|plan-task|review)\\.md|reviews/|신규 회귀 수정 Task|빌드.*테스트.*린트" AGENTS.md docs/agent-guides/work-plan-docs.md docs/20260601_계획문서규칙수정`
|
||||||
|
- `git diff --check`
|
||||||
|
- `./gradlew tasks --all`
|
||||||
|
- 결과:
|
||||||
|
- `AGENTS.md`와 `docs/agent-guides/work-plan-docs.md`에서 세 샘플의 정확한 경로, 리뷰 보고서 저장 위치, 일반 검증 제외, 확정 항목의 해당 Phase 신규 Task 전환 규칙을 확인했다.
|
||||||
|
- `git diff --check`는 출력 없이 exit code 0으로 통과했다.
|
||||||
|
- `./gradlew tasks --all`은 최초 sandbox 권한 제한을 확인한 뒤 승인 범위에서 재실행해 `BUILD SUCCESSFUL in 24s`로 통과했다.
|
||||||
|
- 이번 작업은 코드 리뷰를 수행하지 않은 문서 정비이므로 `reviews/` 폴더와 리뷰 보고서를 생성하지 않았다.
|
||||||
|
|
||||||
|
- 2026-07-30
|
||||||
|
- 무엇/왜/어떻게: 사용자 후속 요청에 따라 리뷰 보고서명의 `[리뷰범위]`를 `phase<번호>-<구현 내용을 나타내는 영문 kebab-case>`로 고정하고, `phase2-main-home-recommendation-review.md` 예시를 핵심·상세 규칙과 PRD에 일치하게 반영했다.
|
||||||
|
- 실행 명령:
|
||||||
|
- `rg -n "phase<번호>-<구현 내용을 나타내는 영문 kebab-case>|phase2-main-home-recommendation-review\\.md" AGENTS.md docs/agent-guides/work-plan-docs.md docs/20260601_계획문서규칙수정`
|
||||||
|
- `git diff HEAD --check`
|
||||||
|
- `./gradlew tasks --all`
|
||||||
|
- 결과:
|
||||||
|
- 네 문서가 같은 `[리뷰범위]` 형식과 실제 파일명 예시를 안내함을 확인했다.
|
||||||
|
- `git diff HEAD --check`는 출력 없이 exit code 0으로 통과했다.
|
||||||
|
- `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 4s`로 통과했다.
|
||||||
|
|||||||
@@ -86,3 +86,80 @@
|
|||||||
|
|
||||||
## 11. Open Questions
|
## 11. Open Questions
|
||||||
- 없음.
|
- 없음.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 2026-07-30 후속 변경: 샘플 문서와 리뷰 보고서 규칙
|
||||||
|
|
||||||
|
### 12.1 변경 배경
|
||||||
|
|
||||||
|
- PRD와 구현 계획/TASK 문서의 샘플이 `docs/sample/` 아래의 새 문서로 교체됐다.
|
||||||
|
- 기존 작업 절차에는 구현 계획/TASK 문서가 참조해야 할 샘플의 정확한 위치와 문서 유지보수 규칙이 없다.
|
||||||
|
- 코드 리뷰, 코드 품질 점검, 완료 Phase 재검토 결과를 별도 산출물로 추적할 리뷰 보고서 규칙이 필요하다.
|
||||||
|
- 리뷰에서 확정된 수정 항목을 기존 완료 기록을 훼손하지 않고 구현 흐름으로 전환해야 한다.
|
||||||
|
|
||||||
|
### 12.2 목표
|
||||||
|
|
||||||
|
- `docs/sample/sample-prd.md`, `docs/sample/sample-plan-task.md`, `docs/sample/sample-review.md`를 각 문서 유형의 기준 샘플로 명시한다.
|
||||||
|
- PRD와 구현 계획/TASK 문서를 만들 때 해당 샘플을 참조하고 작업에 필요한 항목을 구체화하도록 규정한다.
|
||||||
|
- 코드 리뷰, 코드 품질 점검, 완료 Phase 재검토 결과를 대상 작업 디렉터리의 `reviews/` 아래에 리뷰 보고서로 작성하도록 규정한다.
|
||||||
|
- 리뷰에서 수정 항목이 확정되면 `plan-task.md`의 해당 Phase에 신규 회귀 수정 Task를 먼저 추가한 뒤 즉시 수정할 수 있도록 후속 절차를 명시한다.
|
||||||
|
- 샘플과 안내 문서가 서로 다른 경로 또는 작성 규칙을 안내하지 않도록 유지보수 규칙을 보강한다.
|
||||||
|
|
||||||
|
### 12.3 제외 범위
|
||||||
|
|
||||||
|
- 기존 PRD, 계획/TASK 문서, 과거 리뷰 기록을 새 샘플 형식으로 일괄 변환하지 않는다.
|
||||||
|
- 일반 빌드, 테스트, 린트 실행마다 리뷰 보고서를 만들지 않는다.
|
||||||
|
- Android 앱 소스 코드와 빌드 설정은 수정하지 않는다.
|
||||||
|
- 이번 문서 규칙 정비 자체를 코드 리뷰로 간주해 리뷰 보고서를 만들지 않는다.
|
||||||
|
|
||||||
|
### 12.4 기능 요구사항
|
||||||
|
|
||||||
|
| ID | 상태 | 요구사항 | 수용 기준 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `DOC-001` | 확정 | 문서 샘플의 기준 위치를 `docs/sample/`로 통일한다. | `AGENTS.md`와 `work-plan-docs.md`가 세 샘플의 정확한 경로를 안내한다. |
|
||||||
|
| `DOC-002` | 확정 | PRD 작성 시 `docs/sample/sample-prd.md`를 참조한다. | 필요한 항목만 사용하고 placeholder와 예시 값은 실제 작업 값으로 교체하도록 명시한다. |
|
||||||
|
| `DOC-003` | 확정 | 구현 계획/TASK 문서 작성 시 `docs/sample/sample-plan-task.md`를 참조한다. | 작업에 필요한 구조와 실행 규칙을 실제 경로, Task, 검증 기준으로 구체화하도록 명시한다. |
|
||||||
|
| `DOC-004` | 확정 | 샘플 변경 시 관련 안내 문서를 함께 유지보수한다. | 샘플 위치·문서 구조·작성 규칙이 바뀌면 `AGENTS.md`와 관련 가이드를 같은 작업에서 동기화하도록 명시한다. |
|
||||||
|
| `REV-001` | 확정 | 코드 리뷰, 코드 품질 점검, 완료 Phase 재검토 결과는 리뷰 보고서로 작성한다. | `docs/sample/sample-review.md`를 참조해 `docs/[날짜]_구현할내용한글/reviews/[리뷰범위]-review.md`에 저장한다. |
|
||||||
|
| `REV-002` | 확정 | 일반 빌드, 테스트, 린트는 리뷰 보고서 대상에서 제외한다. | 해당 결과는 기존처럼 `plan-task.md`의 Task별 검증 기록 또는 `Verification Log`에 남긴다. |
|
||||||
|
| `REV-003` | 확정 | 리뷰에서 수정 항목이 확정되면 해당 Phase에 신규 회귀 수정 Task를 추가한다. | 코드 수정 전에 review ID, 대상 파일, 수정 범위, 회귀 테스트, 완료 증거가 포함된 Task가 `plan-task.md`에 추가된다. |
|
||||||
|
| `REV-004` | 확정 | 확정된 리뷰 Task는 계획 반영 후 바로 수정할 수 있다. | 별도 PRD를 만들거나 기존 완료 Task를 다시 열지 않고 신규 Task의 체크리스트에 따라 수정한다. |
|
||||||
|
| `REV-005` | 확정 | 기존 완료·검증·리뷰 기록을 보존한다. | 완료 체크박스를 미완료로 되돌리거나 기존 기록을 삭제·덮어쓰지 않고 후속 Task와 기록을 누적한다. |
|
||||||
|
| `REV-006` | 확정 | 리뷰 보고서명의 `[리뷰범위]`는 `phase<번호>-<구현 내용을 나타내는 영문 kebab-case>` 형식으로 작성한다. | Phase 2 메인 홈 추천 구현 리뷰는 `phase2-main-home-recommendation-review.md`로 저장한다. |
|
||||||
|
|
||||||
|
### 12.5 문서 배치
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/sample/
|
||||||
|
├── sample-prd.md
|
||||||
|
├── sample-plan-task.md
|
||||||
|
└── sample-review.md
|
||||||
|
|
||||||
|
docs/[날짜]_구현할내용한글/
|
||||||
|
├── prd.md
|
||||||
|
├── plan-task.md
|
||||||
|
└── reviews/
|
||||||
|
└── phase<번호>-<implementation-content-kebab-case>-review.md
|
||||||
|
```
|
||||||
|
|
||||||
|
`reviews/`는 실제 리뷰 보고서가 생길 때 만들며, 리뷰가 없는 작업에는 빈 폴더를 만들지 않는다.
|
||||||
|
`[리뷰범위]`는 `phase<번호>-<구현 내용을 나타내는 영문 kebab-case>`로 작성하며, `phase`와 번호 사이에는 하이픈을 넣지 않는다.
|
||||||
|
|
||||||
|
### 12.6 성공 기준
|
||||||
|
|
||||||
|
- [x] `AGENTS.md`가 세 샘플의 정확한 위치와 용도를 안내한다.
|
||||||
|
- [x] `docs/agent-guides/work-plan-docs.md`가 PRD·계획/TASK·리뷰 보고서의 작성 및 유지보수 절차를 안내한다.
|
||||||
|
- [x] 리뷰 보고서 대상과 일반 빌드·테스트·린트 검증 기록이 명확히 구분된다.
|
||||||
|
- [x] 확정된 리뷰 항목을 해당 Phase의 신규 회귀 수정 Task로 전환하는 순서가 명시된다.
|
||||||
|
- [x] 기존 문서와 기록을 일괄 변경하지 않는 범위가 유지된다.
|
||||||
|
- [x] 리뷰 보고서명의 `[리뷰범위]` 형식과 실제 파일명 예시가 규칙 문서에 일치하게 반영된다.
|
||||||
|
|
||||||
|
### 12.7 Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·문서 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 2026-07-30 | `DEC-001` | 확정 | 핵심 규칙은 `AGENTS.md`에 요약하고 상세 절차는 `docs/agent-guides/work-plan-docs.md`에 기록한다. | 사용자 승인 및 중복 유지보수 최소화 | `DOC-001~004`, `REV-001~005` |
|
||||||
|
| 2026-07-30 | `DEC-002` | 확정 | 리뷰 보고서는 코드 리뷰, 코드 품질 점검, 완료 Phase 재검토에만 필수로 만들고 일반 빌드·테스트·린트에는 만들지 않는다. | 사용자 답변 | `REV-001`, `REV-002` |
|
||||||
|
| 2026-07-30 | `DEC-003` | 확정 | 리뷰에서 확정된 수정 항목은 해당 Phase의 신규 회귀 수정 Task로 먼저 등록한 뒤 바로 수정한다. | 사용자 추가 요구사항 | `REV-003~005` |
|
||||||
|
| 2026-07-30 | `DEC-004` | 확정 | 리뷰 보고서명의 `[리뷰범위]`는 `phase<번호>-<구현 내용을 나타내는 영문 kebab-case>` 형식으로 고정한다. | 사용자 추가 요구사항 | `REV-006` |
|
||||||
|
|||||||
@@ -41,6 +41,28 @@
|
|||||||
- 신규 필요 후보: 최근 활동 카드, 최근 데뷔 크리에이터 카드, 장르별/응원 크리에이터 profile grid, 모두 팔로우 버튼, 사업자 정보 접기 영역.
|
- 신규 필요 후보: 최근 활동 카드, 최근 데뷔 크리에이터 카드, 장르별/응원 크리에이터 profile grid, 모두 팔로우 버튼, 사업자 정보 접기 영역.
|
||||||
- 검증: PRD의 Section Mapping 표와 구현 대상/제외 대상이 일치하는지 문서 체크한다.
|
- 검증: PRD의 Section Mapping 표와 구현 대상/제외 대상이 일치하는지 문서 체크한다.
|
||||||
|
|
||||||
|
#### Task R1.1: HomeMainFragment 현재 경로 추적성 복구
|
||||||
|
|
||||||
|
**Goal 실행 `P1-R1`:** 현재 `HomeMainFragment` 경로와 PRD·계획의 활성 경로 안내를 일치시켜 후속 작업이 존재하지 않는 과거 경로를 기준으로 삼지 않게 한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `REV-P1-001` 확정 및 `reviews/phase1-existing-boundary-review.md` 판정 완료.
|
||||||
|
- **완료 증거:** 현재 파일 존재/과거 파일 미존재 재현, PRD 활성 제약과 Phase 1 현재 경로 안내 정정, 문서 검색과 `git diff --check` 성공.
|
||||||
|
- **범위 밖:** 완료된 과거 Task의 파일 이력 삭제, production 패키지 이동, 관련 없는 홈 구조 리팩터링.
|
||||||
|
- **TDD 예외 사유:** production 동작을 바꾸지 않는 문서 경로 추적성 정정이므로 실패 test 대신 파일 존재와 문서 검색을 대체 검증으로 사용한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/prd.md`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/reviews/phase1-existing-boundary-review.md`
|
||||||
|
|
||||||
|
- [x] `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` 존재와 과거 `v2/main/HomeMainFragment.kt` 미존재를 재현한다.
|
||||||
|
- [x] PRD Technical Constraints와 Phase 1에 현재 canonical 경로를 명시하되 기존 완료 이력은 삭제하지 않는다.
|
||||||
|
- [x] `rg` 경로 검색과 `git diff --check`를 실행하고 결과를 Task 검증 기록에 누적한다.
|
||||||
|
|
||||||
|
- 검증 기록: 2026-07-31 `test -f "app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"`와 `test ! -f "app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt"`를 실행해 현재 파일 존재와 과거 파일 미존재를 확인했다. PRD Technical Constraints에는 현재 canonical 경로인 `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`와 package `kr.co.vividnext.sodalive.v2.main.home.HomeMainFragment`를 명시하고 과거 경로 미존재를 덧붙였다.
|
||||||
|
- 검증 기록: 2026-07-31 `rg -n "v2\.main\.HomeMainFragment|v2/main/HomeMainFragment|v2/main/home/HomeMainFragment|kr\.co\.vividnext\.sodalive\.v2\.main\.home\.HomeMainFragment|kr\.co\.vividnext\.sodalive\.v2\.main\.HomeMainFragment"`로 PRD, 계획, Phase 1 리뷰의 현재/과거 경로 참조를 확인했다. `git diff --check`는 출력 없이 성공했다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Phase 2: API DTO, Repository, DI 추가
|
### Phase 2: API DTO, Repository, DI 추가
|
||||||
@@ -409,6 +431,79 @@
|
|||||||
- Kotlin `clipToOutline`/`ViewOutlineProvider.setRoundRect(...)` clipping 계약 유지
|
- Kotlin `clipToOutline`/`ViewOutlineProvider.setRoundRect(...)` clipping 계약 유지
|
||||||
- 검증: 인기 커뮤니티 이미지 영역이 화면 폭에 맞춰 계산되어 고정 폭 때문에 잘리지 않는다.
|
- 검증: 인기 커뮤니티 이미지 영역이 화면 폭에 맞춰 계산되어 고정 폭 때문에 잘리지 않는다.
|
||||||
|
|
||||||
|
#### Task R7.1: 유료 미구매 원본 이미지 로드 회귀 수정
|
||||||
|
|
||||||
|
**Goal 실행 `P7-R1`:** 유료 미구매 게시물에서 원본 `imageUrl`을 다시 로드하는 회귀를 막고, 원본 미로드 상태에서도 기존 lock overlay와 가격 표시를 유지하며 요구사항·코드·테스트·검증 기록을 일치시킨다.
|
||||||
|
|
||||||
|
- **시작 조건:** `REV-P7-001` 확정 및 `reviews/phase7-popular-community-review.md` 판정 완료.
|
||||||
|
- **완료 증거:** 현재 코드에서 실패하는 원본 미로드 회귀 테스트, 최소 수정 후 focused test와 home 회귀·compile·ktlint 성공, PRD/plan 정합성 정정, 리뷰 보고서 수정 후 검증 기록 누적.
|
||||||
|
- **범위 밖:** API·DTO에 서버 preview 필드 추정 추가, 구매 권한 판정 변경, `FeedCommunityView` 공통 레이아웃 리팩터링, 기존 완료 기록 삭제 또는 덮어쓰기.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomePopularCommunityAdapter.kt`
|
||||||
|
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/prd.md`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/reviews/phase7-popular-community-review.md`
|
||||||
|
|
||||||
|
- [x] **RED:** 유료 미구매 item의 원본 URL을 image loader에 전달하지 않고 기존 요청·drawable을 비우며 lock overlay/가격은 유지하는 회귀 테스트를 먼저 추가해 현재 구현에서 의도한 실패를 확인한다.
|
||||||
|
- [x] **GREEN:** `HomePopularCommunityAdapter`만 최소 수정해 무료·구매 완료 이미지는 기존대로 로드하고 유료 미구매 원본 이미지는 로드하지 않도록 복구한다.
|
||||||
|
- [x] 현재 PRD의 blur 문구를 원본 미로드 보안 결정과 일치시키고, commit `2d58a876`에서 발생한 회귀 및 이번 복구 이력을 기존 기록을 삭제하지 않은 채 PRD와 `Verification Log`에 누적한다.
|
||||||
|
- [x] focused test, `HomeMainFragmentLayoutTest` 전체, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`를 실행한다.
|
||||||
|
- [x] `REV-P7-001` 보고서의 `수정 후 검증 기록`에 변경 파일, 검증 명령, 결과와 남은 항목을 누적한다.
|
||||||
|
|
||||||
|
- 검증 기록: 2026-07-31 RED는 `HomeMainFragmentLayoutTest.home popular community adapter does not load original image for locked paid post`를 먼저 추가하고 focused test를 실행해 `HomeMainFragmentLayoutTest.kt:1032` assertion 실패로 확인했다. GREEN은 `HomePopularCommunityAdapter`에서 잠금 유료 게시물일 때 `imageUrl`을 전달하지 않고 `BlurTransformation` 경로를 제거하는 최소 변경으로 동일 focused test BUILD SUCCESSFUL을 확인했다. `FeedCommunityView.bind(item.item)` 호출은 유지해 lock overlay와 가격 capsule은 기존 View 계약을 따른다.
|
||||||
|
- 검증 기록: 2026-07-31 후속 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`를 순차 실행해 모두 성공했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation warning과 Gradle deprecation warning은 기존 경고다. `REV-P7-001` 보고서의 `수정 후 검증 기록`에 변경 파일, 검증 명령, 결과, 남은 항목 없음 상태를 누적했다.
|
||||||
|
|
||||||
|
#### Task R7.2: 인기 커뮤니티 회귀 테스트 이름 규칙 정정
|
||||||
|
|
||||||
|
**Goal 실행 `P7-R2`:** `REV-P7-002`의 현재 영문 테스트명을 한글 문장으로 정정하고 원본 미로드 회귀 assertion을 그대로 유지한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `REV-P7-002` 확정 및 `P7-R1` 수정 검증 완료.
|
||||||
|
- **완료 증거:** 테스트명을 `유료 미구매 인기 커뮤니티는 원본 이미지를 로드하지 않는다`로 정정, 동일 focused test·`ktlintCheck`·`git diff --check` 성공, 리뷰 보고서 검증 기록 누적.
|
||||||
|
- **범위 밖:** production adapter 동작, assertion 완화, 다른 기존 영문 테스트명 일괄 정리.
|
||||||
|
- **TDD 예외 사유:** 기존 회귀 테스트의 표현만 정정하는 코드 품질 Task이므로 RED/GREEN 대신 정정 전·후 동일 assertion 성공을 검증한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/reviews/phase7-popular-community-review.md`
|
||||||
|
|
||||||
|
- [x] 현재 테스트명과 `code-style.md` 규칙 위반을 검색으로 재현한다.
|
||||||
|
- [x] assertion과 production code는 바꾸지 않고 대상 테스트명만 한글로 정정한다.
|
||||||
|
- [x] 변경된 메서드 focused test, `./gradlew :app:ktlintCheck`, `git diff --check`를 실행하고 결과를 누적한다.
|
||||||
|
|
||||||
|
- 검증 기록: 2026-07-31 `rg -n "home popular community adapter does not load original image for locked paid post|cheer creator follow all opens confirm dialog before api request|follow all confirm strings are localized" app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt docs/agent-guides/code-style.md`로 신규 영문 테스트명과 `code-style.md` 한글 테스트명 규칙 위반을 재현했다. Production code와 assertion은 바꾸지 않고 대상 테스트명을 `유료 미구매 인기 커뮤니티는 원본 이미지를 로드하지 않는다`로 정정했다.
|
||||||
|
- 검증 기록: 2026-07-31 변경된 메서드 focused test `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.유료 미구매 인기 커뮤니티는 원본 이미지를 로드하지 않는다" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.응원 크리에이터 모두 팔로우는 API 요청 전에 확인창을 연다" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.모두 팔로우 확인창 문자열은 다국어로 제공한다"`, 직접 영향 회귀 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialogTest"`, `./gradlew :app:ktlintCheck`, `git diff --check`를 실행했고 모두 성공했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation warning과 Gradle deprecation warning은 기존 경고다.
|
||||||
|
|
||||||
|
#### Task R7.3: 유료 미구매 이미지 미로드 실행형 회귀 테스트 보강
|
||||||
|
|
||||||
|
**Goal 실행 `P7-R3`:** `REV-P7-003`의 소스 문자열 검사를 실제 adapter bind 동작 회귀 테스트로 대체해, 유료 미구매 이미지의 원본 미로드·기존 요청 해제·drawable 제거·lock overlay·가격 표시 계약을 실행 결과로 보호한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `REV-P7-003` 확정 및 `reviews/phase7-popular-community-review.md` 판정 완료.
|
||||||
|
- **완료 증거:** 잠금 유료 item을 `HomePopularCommunityAdapter` ViewHolder에 bind하는 실행형 테스트, 기존 image request/drawable 제거와 overlay/가격 유지 assertion, 무료·구매 완료 이미지 경로 회귀, focused·home·compile·ktlint·diff 검증 성공, 리뷰 보고서 결과 누적.
|
||||||
|
- **범위 밖:** production 보안 정책 변경, 서버 preview 필드 추정, `FeedCommunityView` 공통 레이아웃 리팩터링, 기존 테스트 전체 재구성.
|
||||||
|
- **TDD 예외 사유:** production 보안 회귀는 `P7-R1`에서 이미 RED/GREEN으로 수정됐고 이번 항목은 완료 증거의 실행성 보강이다. 현재 정상 동작에서 새 테스트가 통과하는 것을 기준으로 하며, 실행형 테스트가 추가 결함을 드러낸 경우에만 production 수정용 RED/GREEN으로 전환한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt` 또는 동일 패키지의 전용 adapter test
|
||||||
|
- Modify only if RED 재현이 production 결함을 확정할 때: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomePopularCommunityAdapter.kt`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/reviews/phase7-popular-community-review.md`
|
||||||
|
|
||||||
|
- [x] **TEST:** 잠금 유료 item bind 전 image view에 기존 drawable/request가 있는 상태를 만들고 bind 후 원본 로드 미실행, 요청 해제, drawable 제거, lock overlay 표시, 가격 유지를 실행형 assertion으로 검증한다.
|
||||||
|
- [x] **FIX IF NEEDED:** 실행형 테스트가 production 결함을 재현한 경우에만 별도 RED/GREEN 기록 후 adapter를 최소 수정하고, 현재 동작이 이미 계약을 충족하면 production은 변경하지 않는다.
|
||||||
|
- [x] 무료·구매 완료 item의 이미지 로드 경로와 잠금 유료 item의 overlay/가격 표시 회귀를 함께 검증한다.
|
||||||
|
- [x] focused test, `HomeMainFragmentLayoutTest` 또는 전용 adapter test 전체, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`를 실행한다.
|
||||||
|
- [x] `REV-P7-003` 보고서에 변경 파일, 실행형 테스트 결과, 필요 시 RED/GREEN 결과, 검증 명령, 남은 항목을 누적한다.
|
||||||
|
|
||||||
|
- 검증 기록: 2026-07-31 기존 `HomeMainFragmentLayoutTest`의 production source 문자열 검사를 제거하고 `HomePopularCommunityAdapterTest`를 추가했다. 잠금 유료 item은 실제 adapter/ViewHolder bind에서 원본 URL `loadUrl` 호출 0회, 재사용 image view의 기존 Coil `Disposable` 해제, drawable 제거, lock overlay `VISIBLE`, 가격 `30`을 검증한다. 무료와 구매 완료 item은 각각의 원본 URL이 loader 경계에 정확히 1회 전달되는지 검증한다. 외부 이미지 로딩만 MockK static 경계로 차단하고 adapter, ViewHolder, `FeedCommunityView`, Coil 요청 해제는 실제 구현을 실행했다.
|
||||||
|
- 검증 기록: 2026-07-31 테스트 효과 확인을 위해 `HomePopularCommunityAdapter`의 보안 분기를 일시적으로 `item.item.imageUrl` 직접 전달로 변경한 뒤 잠금 focused test를 실행했고, `HomePopularCommunityAdapterTest.kt:72`의 원본 `loadUrl` 호출 0회 assertion에서 1건 실패했다. 즉시 `takeIf { !item.item.isLockedPaidPost() }` 분기를 복구한 뒤 전용 test class 2건이 BUILD SUCCESSFUL로 다시 통과했다. 최종 production 동작은 `P7-R1` 상태와 같아 이번 Task에서 추가 production 수정은 남기지 않았다.
|
||||||
|
- 검증 기록: 2026-07-31 `HomePopularCommunityAdapterTest`와 `HomeMainFragmentLayoutTest`, `:app:compileDebugKotlin`, `:app:ktlintCheck` 결합 명령은 BUILD SUCCESSFUL이었다. 전체 `./gradlew :app:testDebugUnitTest --no-parallel --console=plain`도 162 suites, 1,198 tests, 실패 0, error 0으로 BUILD SUCCESSFUL이었다. ktlint의 `.editorconfig disabled_rules`와 Gradle deprecation warning은 기존 경고다. 코드 재검토에서 추가 production 결함이나 신규 회귀 Task 전환 항목은 확인되지 않았다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Phase 8: 사업자 정보 접기/더보기 구현
|
### Phase 8: 사업자 정보 접기/더보기 구현
|
||||||
@@ -582,7 +677,7 @@
|
|||||||
- 실행: `./gradlew :app:ktlintCheck`
|
- 실행: `./gradlew :app:ktlintCheck`
|
||||||
- 기대 결과: 모두 성공.
|
- 기대 결과: 모두 성공.
|
||||||
|
|
||||||
- [ ] **Task 12.2: 수동 확인** `[blocked: 연결된 Android 기기 없음]`
|
- [x] **Task 12.2: 수동 확인**
|
||||||
- 확인 항목:
|
- 확인 항목:
|
||||||
- 추천 tab 선택 상태
|
- 추천 tab 선택 상태
|
||||||
- tab 전환은 글자 터치로만 동작하고 swipe 전환은 없음
|
- tab 전환은 글자 터치로만 동작하고 swipe 전환은 없음
|
||||||
@@ -590,7 +685,7 @@
|
|||||||
- 섹션 순서가 PRD 도식과 일치
|
- 섹션 순서가 PRD 도식과 일치
|
||||||
- 제외 섹션 미노출
|
- 제외 섹션 미노출
|
||||||
- 빈 리스트 섹션 숨김
|
- 빈 리스트 섹션 숨김
|
||||||
- 첫 오디오 태그 조건
|
- Phase 12B에서 제거된 첫 오디오 섹션과 관련 태그 미노출
|
||||||
- activity type 다국어 문자열
|
- activity type 다국어 문자열
|
||||||
- 방금 활동한 크리에이터 목적지별 클릭 이동
|
- 방금 활동한 크리에이터 목적지별 클릭 이동
|
||||||
- AI 캐릭터 item 크리에이터 채널 클릭 이동
|
- AI 캐릭터 item 크리에이터 채널 클릭 이동
|
||||||
@@ -606,6 +701,48 @@
|
|||||||
- 구현 내용: 실행 명령, 결과, 실패 시 원인과 후속 조치를 문서 하단 `검증 기록`에 누적한다.
|
- 구현 내용: 실행 명령, 결과, 실패 시 원인과 후속 조치를 문서 하단 `검증 기록`에 누적한다.
|
||||||
- 주의: 기존 검증 기록은 삭제하거나 덮어쓰지 않는다.
|
- 주의: 기존 검증 기록은 삭제하거나 덮어쓰지 않는다.
|
||||||
|
|
||||||
|
#### Task R12.1: 연결 기기 확인 후 수동 검증 재개
|
||||||
|
|
||||||
|
**Goal 실행 `P12-R1`:** `REV-P12-001`의 오래된 `연결된 Android 기기 없음` blocker를 현재 환경에 맞게 정정하고 Task 12.2 수동 확인을 재개한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `REV-P12-001` 확정, `adb devices`에서 사용 가능한 기기 1대 확인, Task 12.1 자동 검증 성공.
|
||||||
|
- **완료 증거:** Task 12.2의 홈 추천 항목을 실제 기기에서 확인, 항목별 결과·불가 사유·캡처 또는 로그 등 근거 누적, Phase 12 리뷰 판정 갱신.
|
||||||
|
- **범위 밖:** 수동 Gate 통과를 위한 production 변경, 서버 추천 데이터 임의 조작, 다른 홈 tab 기능 수정.
|
||||||
|
- **TDD 예외 사유:** 실기기 UI·상호작용 확인 Task이므로 기존 자동 검증을 선행 조건으로 사용하고 항목별 수동 증거로 완료를 판정한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/reviews/phase12-final-verification-review.md`
|
||||||
|
|
||||||
|
- [x] `adb devices`를 재실행해 기기 연결 상태를 기록하고 Task 12.2의 기존 blocker 문구를 현재 상태로 정정한다.
|
||||||
|
- [x] Task 12.2 추천 tab·스크롤·섹션·라우팅·사업자 정보 항목을 실기기에서 확인하고 각 항목의 결과를 누적한다.
|
||||||
|
- [x] 수동 확인 결과에 따라 Task 12.2와 Phase 12 리뷰 종료 판정을 갱신하되 기존 기록은 삭제하지 않는다.
|
||||||
|
|
||||||
|
- 검증 기록: 2026-07-31 `adb devices`에서 `2cec640c34017ece device`를 확인해 기존 `연결된 Android 기기 없음` blocker는 현재 상태와 다름을 확인했다. `./gradlew :app:installDebug`는 SM-G960N 기기에 설치 성공했고, `adb shell monkey -p kr.co.vividnext.sodalive.debug -c android.intent.category.LAUNCHER 1`도 성공했다. 그러나 `adb shell dumpsys window`는 foreground가 `kr.co.vividnext.sodalive.debug/kr.co.vividnext.sodalive.splash.SplashActivity`임을 반환했고, `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sodalive_r12_manual.png` 및 retry `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sodalive_r12_manual_retry.png`는 모두 검은 화면으로 판독됐다. 따라서 Task 12.2 추천 tab·스크롤·섹션·라우팅·사업자 정보 실기기 항목은 홈 추천 화면 접근 불가로 계속 blocked 처리한다.
|
||||||
|
- 검증 기록: 2026-07-31 재검증에서 `adb devices`는 `2cec640c34017ece device`를 반환했고 `./gradlew :app:installDebug`, `adb shell monkey -p kr.co.vividnext.sodalive.debug -c android.intent.category.LAUNCHER 1`는 성공했다. `adb shell dumpsys window`는 foreground가 `kr.co.vividnext.sodalive.debug/kr.co.vividnext.sodalive.v2.main.MainV2Activity`임을 반환했지만, `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sodalive_manual_gate_20260731/screen.png`는 검은 배경의 splash/voice on 화면으로 판독됐다. 홈 추천 화면과 Phase 17 확인창까지 접근하지 못해 Task 12.2 및 P17-GATE 수동 항목은 계속 blocked로 유지한다.
|
||||||
|
- 검증 기록: 2026-07-31 사용자 수동 확인으로 추천 tab 선택, tab 터치 전환과 swipe 미전환, title/tab bar 고정과 content 스크롤, 제외 섹션 미노출, 빈 리스트 섹션 숨김, first audio 섹션/태그 미노출, 최근 활동/AI 캐릭터/배너 라우팅, 인기 커뮤니티 이미지 상태, 사업자 정보 더보기/접기가 모두 동작함을 확인했다.
|
||||||
|
|
||||||
|
#### Task R12.2: 수동 확인의 first audio 제거 계약 동기화
|
||||||
|
|
||||||
|
**Goal 실행 `P12-R2`:** `REV-P12-002`의 미완료 Task 12.2가 제거된 first audio 표시 계약을 요구하지 않고 Phase 12B의 미노출 계약을 검증하도록 정정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `REV-P12-002` 확정, `P12B-T1`·`P12B-T2` production 제거 완료 유지, Task 12.2 수동 확인 blocked 상태.
|
||||||
|
- **완료 증거:** Task 12.2의 `첫 오디오 태그 조건`을 `첫 오디오 섹션과 관련 태그 미노출`로 정정, production 금지 참조 및 문서 활성 항목 검색, `git diff --check` 성공, 리뷰 보고서 검증 기록 누적.
|
||||||
|
- **범위 밖:** Task 12.2 수동 항목 실행, production·resource·test 변경, 과거 Phase 6·9 이력 삭제.
|
||||||
|
- **TDD 예외 사유:** 이미 완료된 production 제거에 대한 미완료 수동 체크리스트만 정정하므로 `rg` 대조와 문서 diff를 대체 검증으로 사용한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/reviews/phase12-final-verification-review.md`
|
||||||
|
|
||||||
|
- [x] Task 12.2의 활성 first audio 표시 확인 항목과 Phase 12B 제거 계약의 불일치를 `rg`로 재현한다.
|
||||||
|
- [x] 과거 검증 이력은 보존하고 Task 12.2 활성 항목만 first audio 미노출 검증으로 정정한다.
|
||||||
|
- [x] production 금지 참조 및 문서 활성 항목 검색과 `git diff --check`를 실행하고 결과를 누적한다.
|
||||||
|
|
||||||
|
- 검증 기록: 2026-07-31 `rg -n "firstAudioContents|HomeFirstAudioContentItem|releaseDate|첫 오디오|처음부터 함께 성장" docs/20260601_메인_홈_추천_UI와_API_연동/prd.md docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md app/src/main/java/kr/co/vividnext/sodalive/v2/main/home app/src/main/res/values app/src/main/res/layout`로 Task 12.2의 활성 `첫 오디오 태그 조건`과 Phase 12B 제거 계약이 추돌함을 재현했다. 해당 항목을 `Phase 12B에서 제거된 첫 오디오 섹션과 관련 태그 미노출`로 정정했고 production/resource 제거 계약은 유지됨을 확인했다. `git diff --check`는 출력 없이 성공했다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 확인 완료 사항
|
## 확인 완료 사항
|
||||||
@@ -721,6 +858,16 @@
|
|||||||
|
|
||||||
### Phase 12: `처음부터 함께 성장!` 섹션 제거
|
### Phase 12: `처음부터 함께 성장!` 섹션 제거
|
||||||
|
|
||||||
|
#### Phase 12B 식별자 별칭
|
||||||
|
|
||||||
|
기존 완료 이력의 제목과 체크박스는 보존한다. 이후 리뷰·회귀 수정·검증 기록에서 이 두 번째 Phase 12를 참조할 때는 아래 고유 별칭을 사용한다.
|
||||||
|
|
||||||
|
| 역사적 식별자 | 고유 별칭 | 이후 참조 규칙 |
|
||||||
|
|---|---|---|
|
||||||
|
| 두 번째 `Phase 12: 처음부터 함께 성장! 섹션 제거` | `P12B` | first audio 제거 Phase는 `P12B`로 표기한다. |
|
||||||
|
| 두 번째 Phase 12의 `Task 10.1` | `P12B-T1` | 홈 추천 Response/UI first audio 제거 항목은 `P12B-T1`로 표기한다. |
|
||||||
|
| 두 번째 Phase 12의 `Task 10.2` | `P12B-T2` | first audio 전체보기와 리소스 제거 항목은 `P12B-T2`로 표기한다. |
|
||||||
|
|
||||||
- [x] **Task 10.1: 홈 추천 Response와 UI에서 firstAudio 제거**
|
- [x] **Task 10.1: 홈 추천 Response와 UI에서 firstAudio 제거**
|
||||||
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeRecommendationModels.kt`
|
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeRecommendationModels.kt`
|
||||||
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationMappers.kt`
|
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationMappers.kt`
|
||||||
@@ -737,6 +884,49 @@
|
|||||||
- 수정: `app/src/main/res/values/strings.xml`, `values-en/strings.xml`, `values-ja/strings.xml`
|
- 수정: `app/src/main/res/values/strings.xml`, `values-en/strings.xml`, `values-ja/strings.xml`
|
||||||
- 검증 기록: 2026-06-29 `ContentOverviewType.FIRST_AUDIO_CONTENT`와 `home_recommendation_section_first_audio_contents` production 참조를 제거하고, `NEW_AND_HOT_AUDIO` 경로는 유지했다.
|
- 검증 기록: 2026-06-29 `ContentOverviewType.FIRST_AUDIO_CONTENT`와 `home_recommendation_section_first_audio_contents` production 참조를 제거하고, `NEW_AND_HOT_AUDIO` 경로는 유지했다.
|
||||||
|
|
||||||
|
#### Task R12B.1: 중복 Phase·Task 식별자 추적성 복구
|
||||||
|
|
||||||
|
**Goal 실행 `P12B-R1`:** 두 번째 `Phase 12`와 그 안의 `Task 10.1`·`Task 10.2`를 기존 완료 이력을 보존한 채 고유 별칭으로 식별할 수 있게 해 리뷰·회귀 Task 연결의 모호성을 제거한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `REV-P12B-001` 확정 및 `reviews/phase12-first-audio-removal-review.md` 판정 완료.
|
||||||
|
- **완료 증거:** 역사적 식별자와 신규 고유 별칭 대응표 추가, 이후 참조 규칙 명시, 중복 식별자 검색 결과와 `git diff --check` 성공, 리뷰 보고서 수정 후 검증 기록 누적.
|
||||||
|
- **범위 밖:** 기존 완료 Phase·Task 체크박스 재개방, 기존 식별자 삭제, first audio production 코드 또는 리소스 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/reviews/phase12-first-audio-removal-review.md`
|
||||||
|
|
||||||
|
- [x] **문서 검증 예외:** production 동작 변경이 없는 추적성 정정이므로 실패 재현 테스트 대신 `Phase 12` 및 `Task 10.*` 중복 검색 결과를 재현 근거로 사용한다.
|
||||||
|
- [x] 기존 제목과 완료 기록은 유지하고, 두 번째 Phase 12 및 하위 Task에 `P12B`, `P12B-T1`, `P12B-T2` 고유 별칭 대응표와 이후 참조 규칙을 추가한다.
|
||||||
|
- [x] Phase별 리뷰·회귀 문서가 고유 별칭으로 연결되는지 `rg`로 확인하고 `git diff --check`를 실행한다.
|
||||||
|
- [x] `REV-P12B-001` 보고서의 `수정 후 검증 기록`에 변경 파일, 검증 명령, 결과와 남은 항목을 누적한다.
|
||||||
|
|
||||||
|
- 검증 기록: 2026-07-31 문서 검증 예외로 `rg -n "^(### Phase 12:|## Phase 12:|#### Task 10\.|- \[x\] \*\*Task 10\.)" "docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md"`를 실행해 첫 번째 `Phase 12`, 두 번째 `Phase 12`, Phase 10 `Task 10.1`, 두 번째 Phase 12의 `Task 10.1`·`Task 10.2` 충돌을 재현했다. 기존 완료 제목과 체크박스는 유지하고 `P12B`, `P12B-T1`, `P12B-T2` 별칭 표와 이후 참조 규칙만 추가했다.
|
||||||
|
- 검증 기록: 2026-07-31 `rg -n "P12B|P12B-T1|P12B-T2|P12B-R1|REV-P12B-001" "docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md" "docs/20260601_메인_홈_추천_UI와_API_연동/reviews/phase12-first-audio-removal-review.md"`로 별칭과 리뷰 연결을 확인했고, `git diff --check`는 출력 없이 성공했다. `REV-P12B-001` 보고서의 `수정 후 검증 기록`에 변경 파일, 검증 명령, 결과, 남은 항목 없음 상태를 누적했다.
|
||||||
|
|
||||||
|
#### Task R12B.2: PRD 활성 영역의 first audio 제거 계약 동기화
|
||||||
|
|
||||||
|
**Goal 실행 `P12B-R2`:** `REV-P12B-002`에서 확인한 PRD 활성 섹션 도식·Metrics·Open Questions의 first audio 잔존 문구를 Phase 12B 제거 계약과 일치시킨다.
|
||||||
|
|
||||||
|
- **시작 조건:** `REV-P12B-002` 확정, `P12B-T1`·`P12B-T2` production 제거 완료 유지.
|
||||||
|
- **완료 증거:** PRD의 현재 Problem·섹션 도식·Metrics·Open Questions에서 제거된 `firstAudioContents`·`HomeFirstAudioContentItem`·`releaseDate` 활성 계약 제거, 2026-06-01 구현 이력과 2026-06-29 제거 기록 보존, production 금지 참조 검색과 `git diff --check` 성공.
|
||||||
|
- **범위 밖:** 과거 검증 이력 삭제, `NEW_AND_HOT_AUDIO` 기능, production·resource·test 변경.
|
||||||
|
- **TDD 예외 사유:** production 제거는 이미 완료되었고 현재 PRD 정합성만 정정하므로 `rg` 금지 참조 검색과 문서 재독을 대체 검증으로 사용한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/prd.md`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/reviews/phase12-first-audio-removal-review.md`
|
||||||
|
|
||||||
|
- [x] PRD 활성 영역에 남은 first audio 참조와 production 미존재를 `rg`로 재현한다.
|
||||||
|
- [x] Problem·섹션 도식·Metrics·Open Questions를 제거 계약에 맞게 정정하고 과거 이력은 그대로 보존한다.
|
||||||
|
- [x] `rg -n "firstAudioContents|HomeFirstAudioContentItem|releaseDate"` 결과를 과거 기록과 활성 계약으로 분류하고 `git diff --check` 결과를 누적한다.
|
||||||
|
|
||||||
|
- 검증 기록: 2026-07-31 `rg -n "firstAudioContents|HomeFirstAudio|FIRST_AUDIO_CONTENT|AudioContentCardView|releaseDate|첫 오디오|처음부터 함께 성장" "docs/20260601_메인_홈_추천_UI와_API_연동/prd.md" app/src/main`로 PRD 활성 영역의 first audio 잔존 참조와 production 미존재를 재현했다. 활성 Problem, 섹션 도식, Metrics, Open Questions에서는 first audio 완료 조건을 제거했고, 2026-06-01 과거 기록 및 2026-06-29 제거 기록은 보존했다.
|
||||||
|
- 검증 기록: 2026-07-31 `rg -n "firstAudioContents|HomeFirstAudioContentItem|releaseDate|첫 오디오|처음부터 함께 성장" "docs/20260601_메인_홈_추천_UI와_API_연동/prd.md" app/src/main/java/kr/co/vividnext/sodalive/v2/main/home app/src/main/res/values app/src/main/res/layout` 결과는 PRD 과거 기록 및 2026-06-29 제거 기록만 반환하고, v2 home production/resource 제거 대상 참조는 반환하지 않았다. `git diff --check`는 출력 없이 성공했다.
|
||||||
|
|
||||||
### Phase 13: 커뮤니티 상세 라우팅 변경
|
### Phase 13: 커뮤니티 상세 라우팅 변경
|
||||||
|
|
||||||
- [x] **Task 13.1: 방금 활동한 크리에이터 COMMUNITY 라우팅 변경**
|
- [x] **Task 13.1: 방금 활동한 크리에이터 COMMUNITY 라우팅 변경**
|
||||||
@@ -793,7 +983,411 @@
|
|||||||
- 검증 기록:
|
- 검증 기록:
|
||||||
- 2026-07-12: `HomeMainFragmentSourceTest`에 `홈 추천 최근 데뷔와 AI 캐릭터 섹션 타이틀은 chevron을 숨긴다`를 먼저 추가해 기존 구현에서 `HomeMainFragmentSourceTest.kt:40` assertion 실패로 RED를 확인했다. 이후 `HomeMainFragment.setUpSectionTitles()`에서 `viewHomeRecentDebutTitle`, `viewHomeAiCharacterTitle`의 `showMore = true`를 제거해 기본 `false` 정책을 적용했다.
|
- 2026-07-12: `HomeMainFragmentSourceTest`에 `홈 추천 최근 데뷔와 AI 캐릭터 섹션 타이틀은 chevron을 숨긴다`를 먼저 추가해 기존 구현에서 `HomeMainFragmentSourceTest.kt:40` assertion 실패로 RED를 확인했다. 이후 `HomeMainFragment.setUpSectionTitles()`에서 `viewHomeRecentDebutTitle`, `viewHomeAiCharacterTitle`의 `showMore = true`를 제거해 기본 `false` 정책을 적용했다.
|
||||||
|
|
||||||
|
### Phase 16: 방금 활동한 크리에이터 LIVE 라우팅
|
||||||
|
|
||||||
|
**Phase 결과:** `방금 활동한 크리에이터`의 `LIVE` item을 터치하면 `targetId` 유무에 따라 기존 공통 라이브 룸 진입 또는 크리에이터 채널 이동이 실행된다.
|
||||||
|
|
||||||
|
**선행 조건:** `prd.md`의 `2026-07-30 변경: 최근 활동 LIVE 라우팅` 요구사항 승인과 서버의 필수 양수 `HomeActiveCreatorItem.creatorId: Long` 계약 확정.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P16-T1`과 `P16-GATE` 완료, Task와 `Verification Log`에 실제 검증 결과 누적.
|
||||||
|
|
||||||
|
#### Task 16.1: LIVE route와 공통 액션 연결
|
||||||
|
|
||||||
|
**Goal 실행 `P16-T1`:** 최근 활동 `LIVE` item이 유효한 `targetId > 0L`가 있으면 라이브 룸으로 진입하고, `targetId == null && creatorId > 0L`이면 크리에이터 채널로 이동하며, 잘못된 `targetId` 또는 `creatorId`에서는 이동하지 않는다.
|
||||||
|
|
||||||
|
- **시작 조건:** Phase 16 선행 조건 충족.
|
||||||
|
- **완료 증거:** RED/GREEN 체크박스 완료, focused test와 직접 영향 회귀 성공, 문서 검증 기록 누적.
|
||||||
|
- **범위 밖:** `LiveActionCoordinator`, `CreatorAction`, `V2AccessGuard`의 공통 정책 변경과 다른 activity type 라우팅 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeRecommendationModels.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationUiModels.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationMappers.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLoginGuardSourceTest.kt`
|
||||||
|
- Modify: `docs/agent-guides/build-test-style.md`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `HomeActiveCreatorItem(activityType, targetId, creatorId)`, `LiveActionCoordinator.enterLiveRoom(roomId: Long)`, `CreatorActionCommand.Profile(creatorId: Long)`.
|
||||||
|
- Produces: `HomeRecommendationRecentlyActiveCreatorUiModel.creatorId: Long`, `HomeRecommendationRecentlyActiveCreatorRoute.LiveRoom(roomId: Long)`, `HomeRecommendationRecentlyActiveCreatorRoute.CreatorChannel(creatorId: Long)`.
|
||||||
|
|
||||||
|
- [x] **RED:** `HomeMainFragmentLayoutTest`와 `HomeMainFragmentLoginGuardSourceTest`에 다음 실패 계약을 추가한다.
|
||||||
|
- `HomeActiveCreatorItem.creatorId`가 `HomeRecommendationRecentlyActiveCreatorUiModel.creatorId`로 보존된다.
|
||||||
|
- `LIVE + targetId > 0L`은 `HomeRecommendationRecentlyActiveCreatorRoute.LiveRoom(targetId)`를 반환한다.
|
||||||
|
- `LIVE + targetId == null + creatorId > 0L`은 `HomeRecommendationRecentlyActiveCreatorRoute.CreatorChannel(creatorId)`을 반환한다.
|
||||||
|
- `LIVE + targetId == null + creatorId <= 0L`은 `null`을 반환한다.
|
||||||
|
- `HomeMainFragment.onRecentActivityClick()`은 `LiveRoom`을 `liveActionCoordinator.enterLiveRoom(route.roomId)`으로, `CreatorChannel`을 `handleCreatorAction(CreatorActionCommand.Profile(route.creatorId))`로 전달한다.
|
||||||
|
- 기존 `LIVE_REPLAY`, `AUDIO`, `COMMUNITY`, unknown, invalid `targetId` 기대값은 유지한다.
|
||||||
|
- [x] **RED 확인:** 아래 focused test를 실행해 `creatorId`, `LiveRoom`, `CreatorChannel` 미정의 또는 기존 `LIVE -> null` 기대값 불일치로 실패하는지 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** RED를 통과시키는 최소 구현을 작성한다.
|
||||||
|
- DTO와 최근 활동 UI model에 필수 `creatorId: Long`을 추가하고 mapper에서 그대로 전달한다.
|
||||||
|
- 최근 활동 route에 `LiveRoom(roomId)`과 `CreatorChannel(creatorId)`을 추가한다.
|
||||||
|
- `CreatorActivityType.Live`는 `targetId > 0L`이면 `LiveRoom(targetId)`, `targetId == null`이면 `creatorId.takeIf { it > 0L }`에 대해서만 `CreatorChannel(creatorId)`을 반환하고, `targetId <= 0L`이면 fallback 없이 `null`을 반환한다.
|
||||||
|
- Fragment는 `LiveRoom`에 기존 `LiveActionCoordinator.enterLiveRoom()`을 사용하고 `CreatorChannel`에 기존 `CreatorActionCommand.Profile`을 사용한다.
|
||||||
|
- 홈 전용 로그인·성인 콘텐츠 분기를 새로 만들지 않는다.
|
||||||
|
- `build-test-style.md`에 위 두 home test class를 함께 실행하는 Phase 16 focused test 예시를 추가한다.
|
||||||
|
- [x] **GREEN 확인:** RED 확인과 같은 focused test를 다시 실행해 route, mapper, Fragment 공통 액션 연결이 모두 성공하는지 확인한다.
|
||||||
|
- [x] **REFACTOR:** 이번 Task가 만든 중복만 정리하고 아래 직접 영향 회귀와 품질 검증 결과를 Task 검증 기록 및 `Verification Log`에 누적한다. 전체 unit test는 공통 access/action 구현을 변경하지 않고 home route 소비만 추가하므로 생략하며, 대신 home focused test와 기존 live/creator action 회귀를 실행한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.live.action.*" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.action.*"
|
||||||
|
./gradlew :app:compileDebugKotlin
|
||||||
|
./gradlew :app:ktlintCheck
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
### 완료 조건
|
||||||
|
|
||||||
|
- [x] `HomeActiveCreatorItem.creatorId`가 DTO에서 UI model까지 필수 `Long`으로 보존된다.
|
||||||
|
- [x] `LIVE`의 두 route와 invalid `creatorId` no-op이 자동 테스트로 검증된다.
|
||||||
|
- [x] 라이브 룸 route가 `LiveActionCoordinator`를 사용해 기존 로그인·본인인증·콘텐츠 보기 설정 정책을 우회하지 않는다.
|
||||||
|
- [x] 다른 activity type의 route 회귀 테스트가 성공한다.
|
||||||
|
- [x] 사용자 직접 지시가 없으므로 커밋하지 않는다.
|
||||||
|
|
||||||
|
### 검증 방법
|
||||||
|
|
||||||
|
#### Phase 16 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P16-GATE`:** Phase 16 route 분기, 공통 접근 흐름 재사용, 직접 영향 회귀를 최종 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P16-T1` 체크박스와 검증 기록 완료.
|
||||||
|
- **완료 증거:** Task 16.1 REFACTOR 명령이 모두 성공하고 아래 수동 대조를 완료한다.
|
||||||
|
- **범위 밖:** Gate 통과를 위한 test 삭제·완화, 공통 access/action 로직 수정, 관련 없는 홈 코드 정리.
|
||||||
|
|
||||||
|
수동 대조:
|
||||||
|
|
||||||
|
- [x] `targetId > 0L`인 `LIVE` route가 `LiveActionCoordinator.enterLiveRoom(targetId)`에 연결된다.
|
||||||
|
- [x] `targetId == null && creatorId > 0L`인 `LIVE` route가 `CreatorActionCommand.Profile(creatorId)`에 연결된다.
|
||||||
|
- [x] `targetId == null && creatorId <= 0L`에서 액션이 실행되지 않는다.
|
||||||
|
- [x] Git diff가 Phase 16 Files와 승인된 PRD·test guide 범위 안에 있고 커밋이 생성되지 않았다.
|
||||||
|
|
||||||
|
검증 기록:
|
||||||
|
|
||||||
|
- 2026-07-30: 사용자의 `진행`은 production 구현이 아니라 `plan-task.md` 반영을 의미한다는 설명에 따라 Phase 16 구현을 중단했다. 설명을 받기 전에 `HomeMainFragmentLayoutTest`에 `creatorId` DTO/UI model 보존, `LIVE + targetId`의 `LiveRoom`, `LIVE + targetId == null`의 `CreatorChannel`, `creatorId <= 0L` no-op RED 계약을 추가했고, `HomeMainFragmentLoginGuardSourceTest`에 최근 활동 route가 기존 Live/Creator Action을 사용하는 계약을 추가했다. 사용자가 이미 작성된 코드는 삭제하지 않도록 요청해 두 테스트 변경은 유지한다. focused test 명령은 실행 중 사용자 중단으로 종료되어 성공·실패 출력이 없으므로 `RED 확인`은 미완료 상태로 남긴다. `git diff --name-only` 기준 변경은 PRD, 계획 문서, 위 두 테스트 파일뿐이며 `app/src/main` production 코드는 변경되지 않았다. 커밋은 생성하지 않았다.
|
||||||
|
- 2026-07-30: Phase 16 구현을 재개해 기존 RED 계약을 실행했다. focused RED는 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest"`에서 `HomeActiveCreatorItem.creatorId`, `HomeRecommendationRecentlyActiveCreatorUiModel.creatorId`, `LiveRoom`, `CreatorChannel` 미정의 컴파일 실패로 확인했다. GREEN 구현으로 `HomeActiveCreatorItem.creatorId`를 DTO에 추가하고 최근 활동 UI model까지 보존했으며, `CreatorActivityType.Live` route를 `targetId > 0L`이면 `LiveRoom(roomId)`, `targetId == null && creatorId > 0L`이면 `CreatorChannel(creatorId)`, 그 외에는 `null`로 분기했다. 리뷰에서 `targetId <= 0L`일 때 creator fallback이 실행될 수 있는 차단 이슈가 확인되어 `targetId = 0L/-1L + creatorId > 0L` RED 테스트를 추가했고, `CreatorActivityType.Live`는 `targetId == null`일 때만 creator fallback을 허용하도록 보완했다. `HomeMainFragment.onRecentActivityClick()`은 `LiveRoom`을 `liveActionCoordinator.enterLiveRoom(route.roomId)`로, `CreatorChannel`을 `handleCreatorAction(CreatorActionCommand.Profile(route.creatorId))`로 전달해 공통 접근 정책을 재사용한다. focused GREEN, 직접 영향 회귀(`HomeMainFragmentLayoutTest`, `HomeMainFragmentLoginGuardSourceTest`, `v2.live.action.*`, `v2.creator.action.*`), `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`가 모두 성공했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation warning과 Gradle deprecation warning은 기존 경고다. `./gradlew :app:installDebug`는 SM-G960N 기기에 설치 성공했고, `adb shell monkey -p kr.co.vividnext.sodalive.debug -c android.intent.category.LAUNCHER 1`로 debug 앱 런처 실행을 확인했다. 실제 추천 LIVE item 터치는 서버 데이터/로그인 상태가 필요한 경로라 이번 검증에서는 자동 route test와 source 계약으로 대체했다. 커밋은 생성하지 않았다.
|
||||||
|
|
||||||
|
## Phase 17: 모두 팔로우 확인 절차
|
||||||
|
|
||||||
|
**Phase 결과:** 여러 크리에이터를 한 번에 팔로우하는 공통 동작은 API 호출 전에 기존 `V2ModalDialog` 확인창을 거치고, 현재 노출 중인 `최근 응원이 많은 크리에이터` 섹션에서는 사용자가 `팔로우 하기`를 선택한 경우에만 기존 모두 팔로우 API가 실행된다.
|
||||||
|
|
||||||
|
**선행 조건:** `prd.md`의 `2026-07-31 변경: 모두 팔로우 확인 절차` 승인, 기존 `V2ModalDialog`의 취소·확인 2버튼 및 중복 클릭 방지 계약 유지.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P17-T1`과 `P17-GATE` 완료, Task 검증 기록과 `Verification Log`에 실제 결과 누적.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 17.1: 응원 크리에이터 모두 팔로우 확인창 연결
|
||||||
|
|
||||||
|
**Goal 실행 `P17-T1`:** `최근 응원이 많은 크리에이터`의 기본 상태 모두 팔로우 버튼은 확인창만 열고, 취소에서는 요청하지 않으며, 확인에서만 기존 `creatorIds`로 팔로우 API를 정확히 한 번 요청한다.
|
||||||
|
|
||||||
|
- **시작 조건:** Phase 17 선행 조건 충족.
|
||||||
|
- **완료 증거:** RED/GREEN 체크박스 완료, 한국어·영어·일본어 resource 및 확인/취소/API 호출 시점 자동 검증, focused 회귀와 수동 확인 결과 누적.
|
||||||
|
- **범위 밖:** 모두 팔로우 API·request/response·ViewModel 로딩/성공/실패 처리 변경, `V2ModalDialog` 공통 구현·layout 변경, 숨겨진 `genreCreators` UI 복원, 신규 범용 wrapper 또는 dependency 추가, 로그인 정책 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.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`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt`
|
||||||
|
- Verify without modification: `app/src/test/java/kr/co/vividnext/sodalive/v2/components/modal/V2ModalDialogTest.kt`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `HomeCheerCreatorAdapter(onFollowAllClick: (List<Long>) -> Unit)`, `V2ModalDialog`, `HomeRecommendationViewModel.followCreators(sectionKey: String, creatorIds: List<Long>)`, `SECTION_KEY_CHEER_CREATORS`, `screenWidth`.
|
||||||
|
- Produces: `HomeMainFragment.showCheerCreatorsFollowAllConfirmDialog(creatorIds: List<Long>)`과 한국어·영어·일본어 `home_recommendation_follow_all_confirm_title`, `home_recommendation_follow_all_confirm_description`, `home_recommendation_follow_all_confirm_button` string resource.
|
||||||
|
|
||||||
|
- [x] **RED:** `HomeMainFragmentLayoutTest`에 다음 실패 계약을 추가한다.
|
||||||
|
- 응원 크리에이터 adapter callback은 `followCreators()`를 직접 호출하지 않고 `showCheerCreatorsFollowAllConfirmDialog(creatorIds)`를 호출한다.
|
||||||
|
- 확인창은 `V2ModalDialog`를 사용하고 title/desc/confirm에 신규 resource, cancel에 기존 `R.string.cancel`을 전달한다.
|
||||||
|
- `confirmButtonClick`만 `homeRecommendationViewModel.followCreators(SECTION_KEY_CHEER_CREATORS, creatorIds)`를 호출하며 cancel callback은 API를 호출하지 않는다.
|
||||||
|
- 한국어·영어·일본어 `strings.xml`에 PRD의 resource key와 확정 번역이 모두 존재한다.
|
||||||
|
- 기존 완료 상태와 빈 `creatorIds` no-op은 `HomeFollowAllButtonBinder` 및 adapter 회귀 계약으로 유지된다.
|
||||||
|
- [x] **RED 확인:** 아래 focused test를 실행해 신규 resource와 `showCheerCreatorsFollowAllConfirmDialog()`가 없고 adapter callback이 `followCreators()`를 직접 호출하므로 의도한 실패를 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** RED를 통과시키는 최소 구현을 작성한다.
|
||||||
|
- 세 locale에 PRD에서 확정한 title/description/confirm string resource만 추가하고 취소는 기존 `cancel`을 재사용한다.
|
||||||
|
- `HomeMainFragment`의 응원 크리에이터 adapter callback을 `showCheerCreatorsFollowAllConfirmDialog(creatorIds)` 호출로 변경한다.
|
||||||
|
- `showCheerCreatorsFollowAllConfirmDialog()`는 기존 `V2ModalDialog`의 2버튼 구성을 표시하며 `confirmButtonClick`에서만 `followCreators(SECTION_KEY_CHEER_CREATORS, creatorIds)`를 호출한다.
|
||||||
|
- Dialog는 `show(screenWidth)`로 표시하고, 취소·바깥 영역·시스템 뒤로가기 및 빠른 중복 확인 동작은 `V2ModalDialog`의 기존 계약을 그대로 사용한다.
|
||||||
|
- 기존 `HomeFollowAllButtonBinder`의 완료 상태·빈 목록 no-op과 `HomeRecommendationViewModel`의 로딩·성공·실패 처리는 변경하지 않는다.
|
||||||
|
- [x] **GREEN 확인:** RED 확인과 같은 focused test를 다시 실행해 직접 API 호출 제거, Dialog 연결과 세 locale resource가 모두 성공하는지 확인한다.
|
||||||
|
- [x] **REFACTOR:** 이번 Task가 만든 중복만 정리하고 아래 직접 영향 회귀와 품질 검증 결과를 Task 검증 기록 및 `Verification Log`에 누적한다. 공통 모달 구현과 API/ViewModel 계약을 변경하지 않으므로 전체 unit test는 생략하고 home focused test, 기존 modal test, resource merge와 compile로 영향 범위를 검증한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialogTest"
|
||||||
|
./gradlew :app:mergeDebugResources
|
||||||
|
./gradlew :app:compileDebugKotlin
|
||||||
|
./gradlew :app:ktlintCheck
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Task R17.1: 모두 팔로우 회귀 테스트 이름 규칙 정정
|
||||||
|
|
||||||
|
**Goal 실행 `P17-R1`:** `REV-P17-001`의 신규 영문 테스트명 2건을 한글 문장으로 정정하고 Dialog 선행·다국어 assertion을 그대로 유지한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `REV-P17-001` 확정 및 `P17-T1` 자동 검증 완료.
|
||||||
|
- **완료 증거:** 대상 테스트명을 한글 문장으로 정정, `HomeMainFragmentLayoutTest`·`V2ModalDialogTest`·`ktlintCheck`·`git diff --check` 성공, 리뷰 보고서 검증 기록 누적.
|
||||||
|
- **범위 밖:** production Dialog·ViewModel·API 동작, assertion 완화, 다른 기존 영문 테스트명 일괄 정리.
|
||||||
|
- **TDD 예외 사유:** 신규 회귀 테스트의 표현 규칙만 정정하므로 RED/GREEN 대신 정정 전·후 동일 assertion 성공을 검증한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/reviews/phase17-follow-all-confirmation-review.md`
|
||||||
|
|
||||||
|
- [x] `cheer creator follow all opens confirm dialog before api request`, `follow all confirm strings are localized` 테스트명을 규칙 위반 근거로 기록한다.
|
||||||
|
- [x] 각 테스트명을 `응원 크리에이터 모두 팔로우는 API 요청 전에 확인창을 연다`, `모두 팔로우 확인창 문자열은 다국어로 제공한다`로 정정한다.
|
||||||
|
- [x] `HomeMainFragmentLayoutTest`·`V2ModalDialogTest`, `./gradlew :app:ktlintCheck`, `git diff --check`를 실행하고 결과를 누적한다.
|
||||||
|
|
||||||
|
- 검증 기록: 2026-07-31 `rg -n "home popular community adapter does not load original image for locked paid post|cheer creator follow all opens confirm dialog before api request|follow all confirm strings are localized|유료 미구매 인기 커뮤니티는 원본 이미지를 로드하지 않는다|응원 크리에이터 모두 팔로우는 API 요청 전에 확인창을 연다|모두 팔로우 확인창 문자열은 다국어로 제공한다" app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt`로 영문 테스트명 제거와 한글 테스트명 반영을 확인했다. Production Dialog·ViewModel·API 동작과 assertion은 변경하지 않았다.
|
||||||
|
- 검증 기록: 2026-07-31 직접 영향 회귀 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialogTest"`, `./gradlew :app:ktlintCheck`, `git diff --check`를 실행했고 모두 성공했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation warning과 Gradle deprecation warning은 기존 경고다.
|
||||||
|
|
||||||
|
#### Task R17.2: 모두 팔로우 확인창 로그인 가드 순서 수정
|
||||||
|
|
||||||
|
**Goal 실행 `P17-R2`:** `최근 응원이 많은 크리에이터` 모두 팔로우 터치 시 미로그인 상태에서는 확인 Dialog보다 기존 로그인 가드가 먼저 동작하도록 순서를 복구한다.
|
||||||
|
|
||||||
|
- **시작 조건:** 수동 확인에서 `모두 팔로우` 터치 시 로그인 가드보다 확인 Dialog가 먼저 표시되는 버그 확인.
|
||||||
|
- **완료 증거:** 로그인 가드가 Dialog 표시보다 먼저 호출되는 RED/GREEN source regression, 기존 Dialog 선행/API confirm-only 계약 회귀, compile·ktlint·`git diff --check` 성공.
|
||||||
|
- **범위 밖:** `V2ModalDialog` 공통 구현 변경, 모두 팔로우 API/ViewModel 변경, 신규 로그인 정책·공통 abstraction 추가.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLoginGuardSourceTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/reviews/phase17-follow-all-confirmation-review.md`
|
||||||
|
|
||||||
|
- [x] **RED:** `onCheerFollowAllClick` source에서 `ensureV2Access(AccessRequirement.Login)`이 `showCheerCreatorsFollowAllConfirmDialog(creatorIds)`보다 먼저 호출되어야 함을 테스트로 추가하고 현재 구현 실패를 확인한다.
|
||||||
|
- [x] **GREEN:** `onCheerFollowAllClick`만 최소 수정해 로그인 가드 통과 후 확인 Dialog를 표시한다.
|
||||||
|
- [x] 기존 확인 Dialog resource, 취소 no-op, confirm-only API 호출 계약 테스트와 `compileDebugKotlin`, `ktlintCheck`, `git diff --check`를 실행하고 결과를 누적한다.
|
||||||
|
|
||||||
|
- 검증 기록: 2026-07-31 RED는 `HomeMainFragmentLoginGuardSourceTest.HomeMainFragment 응원 크리에이터 모두 팔로우는 확인창보다 로그인 가드를 먼저 실행한다`와 `HomeMainFragmentLayoutTest.응원 크리에이터 모두 팔로우는 API 요청 전에 확인창을 연다`를 추가·수정한 뒤 focused test에서 각각 `HomeMainFragmentLoginGuardSourceTest.kt:283`, `HomeMainFragmentLayoutTest.kt:1309` assertion 실패로 확인했다. GREEN은 `onCheerFollowAllClick`에서 `ensureV2Access(AccessRequirement.Login) { showCheerCreatorsFollowAllConfirmDialog(creatorIds) }`로 감싸는 최소 변경 후 동일 focused test BUILD SUCCESSFUL로 확인했다.
|
||||||
|
- 검증 기록: 2026-07-31 직접 영향 회귀 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialogTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`를 실행했고 모두 성공했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation warning과 Gradle deprecation warning은 기존 경고다.
|
||||||
|
|
||||||
|
### 완료 조건
|
||||||
|
|
||||||
|
- [x] 기본 상태 모두 팔로우 버튼 터치 직후 API 호출 없이 `V2ModalDialog`가 표시된다.
|
||||||
|
- [x] `취소`는 Dialog만 닫고 API 호출과 버튼 상태 변경을 발생시키지 않는다.
|
||||||
|
- [x] `팔로우 하기`는 Dialog를 닫은 뒤 기존 `cheerCreators.creatorId` 목록으로 API를 정확히 한 번 호출한다.
|
||||||
|
- [x] API 성공·실패·로딩과 완료 버튼 상태는 기존 동작을 유지한다.
|
||||||
|
- [x] 확인창 문구가 한국어·영어·일본어 resource로 제공되고 UI 문자열이 Kotlin에 하드코딩되지 않는다.
|
||||||
|
- [x] 완료 상태·빈 목록에서는 Dialog와 API가 실행되지 않는다.
|
||||||
|
- [x] `genreCreators` 섹션은 계속 숨김 상태이고 신규 공통 abstraction을 추가하지 않는다.
|
||||||
|
|
||||||
|
### 검증 방법
|
||||||
|
|
||||||
|
#### Phase 17 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P17-GATE`:** Phase 17의 확인 전 요청 금지, 취소 no-op, 확인 후 단일 요청, 다국어와 기존 회귀를 최종 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P17-T1` 체크박스와 검증 기록 완료.
|
||||||
|
- **완료 증거:** Task 17.1 REFACTOR 명령이 모두 성공하고 아래 수동 확인을 완료한다.
|
||||||
|
- **범위 밖:** Gate 통과를 위한 test 삭제·완화, API/ViewModel/공통 모달 계약 변경, 관련 없는 홈 코드 정리.
|
||||||
|
|
||||||
|
수동 확인:
|
||||||
|
|
||||||
|
- [x] 한국어·영어·일본어 각각에서 확인창 title/description/confirm/cancel 문구가 잘림이나 하드코딩 없이 표시된다.
|
||||||
|
- [x] 기본 상태 버튼 터치 시 확인창만 표시되고 loading 및 API 요청이 시작되지 않는다.
|
||||||
|
- [x] `취소` 선택 시 Dialog가 닫히고 버튼이 기본 상태로 유지된다.
|
||||||
|
- [x] `팔로우 하기` 선택 시 Dialog가 닫히고 loading 후 기존 성공 또는 실패 feedback이 표시된다.
|
||||||
|
- [x] 완료 상태 버튼과 빈 creator 목록에서는 확인창이 표시되지 않는다.
|
||||||
|
- [x] Git diff가 Phase 17 Files와 승인된 PRD 범위 안에 있고 커밋이 생성되지 않았다.
|
||||||
|
|
||||||
|
계획 기록:
|
||||||
|
|
||||||
|
- 2026-07-31: 사용자 인터뷰에서 모든 `모두 팔로우` 동작에 확인 절차를 적용하되 현재 구현은 `최근 응원이 많은 크리에이터` 섹션만 대상으로 하는 B안을 확정했다. 기존 `HomeMainFragment`의 직접 API 호출 경로와 `V2ModalDialog`의 취소·확인·중복 클릭 방지 계약을 확인해 Phase 17을 작성했다. 이번 요청 범위에 따라 production·test·resource 구현은 시작하지 않았으며 모든 구현 및 Gate 체크박스는 미완료로 유지한다.
|
||||||
|
- 2026-07-31: Phase 17 구현으로 `HomeMainFragment`의 응원 크리에이터 모두 팔로우 callback을 기존 즉시 `followCreators()` 호출에서 `V2ModalDialog` 확인창 표시로 변경했다. 확인 버튼에서만 기존 `homeRecommendationViewModel.followCreators(SECTION_KEY_CHEER_CREATORS, creatorIds)`를 호출하고, 취소 버튼은 기존 `R.string.cancel`과 `V2ModalDialog` 기본 닫기 동작을 재사용한다. 한국어·영어·일본어 `home_recommendation_follow_all_confirm_title`, `home_recommendation_follow_all_confirm_description`, `home_recommendation_follow_all_confirm_button` resource를 추가했다.
|
||||||
|
|
||||||
|
## Phase 18: 현재 진행 중인 라이브 `전체` 항목 상시 표시
|
||||||
|
|
||||||
|
**Phase 결과:** 메인 홈 추천 탭의 현재 진행 중인 라이브가 1개 이상이면 최대 20개의 라이브 뒤에 `전체` 항목이 항상 표시되고, 0개이면 기존처럼 라이브 섹션이 숨겨진다.
|
||||||
|
|
||||||
|
**선행 조건:** `prd.md`의 `2026-07-31 변경: 현재 진행 중인 라이브 전체 항목 상시 표시` 승인, 기존 `HomeLiveAdapter`의 20개 상한·`MoreViewHolder` 디자인·`HomeOnAirLiveActivity` 이동 계약 유지.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P18-T1`과 `P18-GATE` 완료, Task 검증 기록과 `Verification Log`에 실제 결과 누적.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 18.1: 라이브가 1개 이상이면 `전체` 항목 추가
|
||||||
|
|
||||||
|
**Goal 실행 `P18-T1`:** `HomeLiveAdapter`가 빈 목록에는 항목을 만들지 않고, 라이브가 1개 이상이면 라이브 개수와 관계없이 마지막에 기존 `전체` 항목을 추가한다.
|
||||||
|
|
||||||
|
- **시작 조건:** Phase 18 선행 조건 충족.
|
||||||
|
- **완료 증거:** RED/GREEN 체크박스 완료, 0개·1개·20개·21개 이상 경계 자동 검증, focused 회귀·compile·lint·diff 결과 누적.
|
||||||
|
- **범위 밖:** 라이브 최대 표시 개수 변경, `전체` 항목 디자인·문구·클릭 목적지·로그인 가드 변경, 라이브 item 입장 흐름, 추천/전체 라이브 API 계약, 신규 abstraction·dependency 추가.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeLiveAdapter.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `HomeLiveAdapter.submitItems(items: List<HomeRecommendationLiveUiModel>)`, `MAX_VISIBLE_LIVE_COUNT = 20`, 기존 `MoreViewHolder`와 `setOnMoreClick(listener: (() -> Unit)?)`.
|
||||||
|
- Produces: `showAllItem: Boolean` 내부 상태와 `0개 -> itemCount 0`, `1~20개 -> lives + 전체`, `21개 이상 -> live 20개 + 전체` adapter 계약.
|
||||||
|
|
||||||
|
- [x] **RED:** `HomeMainFragmentLayoutTest`에 아래 경계 테스트를 추가한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@Test
|
||||||
|
fun `홈 라이브 어댑터는 라이브가 있으면 전체 항목을 추가한다`() {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
val parent = RecyclerView(context)
|
||||||
|
parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
|
||||||
|
val adapter = HomeLiveAdapter()
|
||||||
|
|
||||||
|
adapter.submitItems(emptyList())
|
||||||
|
assertEquals(0, adapter.itemCount)
|
||||||
|
|
||||||
|
adapter.submitItems(listOf(liveItem(1L)))
|
||||||
|
assertEquals(2, adapter.itemCount)
|
||||||
|
val viewHolder = adapter.onCreateViewHolder(parent, adapter.getItemViewType(1))
|
||||||
|
assertEquals(
|
||||||
|
context.getString(R.string.screen_home_theme_all),
|
||||||
|
(viewHolder.itemView as TextView).text.toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
adapter.submitItems((1L..20L).map(::liveItem))
|
||||||
|
assertEquals(21, adapter.itemCount)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **RED 확인:** 아래 focused test를 실행해 기존 `hasMore = items.size > MAX_VISIBLE_LIVE_COUNT` 조건에서는 라이브 1개의 `itemCount`가 `1`이라 `assertEquals(2, adapter.itemCount)`에서 의도대로 실패하는지 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** `HomeLiveAdapter`의 `hasMore` 이름과 조건만 아래처럼 최소 수정한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private var showAllItem: Boolean = false
|
||||||
|
|
||||||
|
fun submitItems(items: List<HomeRecommendationLiveUiModel>) {
|
||||||
|
this.items = items.take(MAX_VISIBLE_LIVE_COUNT)
|
||||||
|
showAllItem = items.isNotEmpty()
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size + if (showAllItem) 1 else 0
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return if (showAllItem && position == itemCount - 1) VIEW_TYPE_MORE else VIEW_TYPE_LIVE
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN 확인:** RED 확인과 같은 focused test를 다시 실행해 신규 0개·1개 계약과 기존 21개 표시·22개 입력 cap 테스트가 모두 성공하는지 확인한다.
|
||||||
|
- [x] **REFACTOR:** 새 abstraction 없이 변수명과 조건 변경만 유지하고 아래 직접 영향 회귀와 품질 검증 결과를 Task 검증 기록 및 `Verification Log`에 누적한다. 공용 계약·API·Fragment 흐름을 변경하지 않는 단일 adapter 분기는 `HomeMainFragmentLayoutTest`의 0개·1개·20개·21개·22개 경계로 직접 검증하고, 최종 branch 완료 검증에서 전체 unit test도 실행한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"
|
||||||
|
./gradlew :app:compileDebugKotlin
|
||||||
|
./gradlew :app:ktlintCheck
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
검증 기록:
|
||||||
|
|
||||||
|
- 2026-07-31: 구현 전 baseline `HomeMainFragmentLayoutTest` 73개가 성공했다. 신규 `홈 라이브 어댑터는 라이브가 있으면 전체 항목을 추가한다` 테스트를 추가한 RED 실행은 총 74개 중 신규 1개만 `expected:<2> but was:<1>`로 실패해 기존 `items.size > MAX_VISIBLE_LIVE_COUNT` 조건을 재현했다.
|
||||||
|
- 2026-07-31: `HomeLiveAdapter`의 `hasMore`를 `showAllItem`으로 변경하고 `showAllItem = items.isNotEmpty()`를 적용했다. 동일 focused test 74개, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`가 모두 `BUILD SUCCESSFUL`이었고 `git diff --check`는 출력 없이 성공했다. ktlint의 `.editorconfig disabled_rules` 및 Gradle deprecation warning은 기존 경고다.
|
||||||
|
- 2026-07-31: REFACTOR에서는 신규 abstraction 없이 테스트명을 한글 의도에 맞게 정리하고 focused test와 ktlint를 재실행해 성공을 확인했다. 최종 완료 검증으로 전체 `:app:testDebugUnitTest --no-parallel --console=plain`도 실행해 162 suites, 1,199 tests, 실패 0·오류 0·skip 0을 확인했다.
|
||||||
|
- 2026-07-31: Phase 18 코드 리뷰에서 PRD·Task와 `HomeLiveAdapter` 및 `HomeMainFragment` 호출 흐름을 대조해 확정 발견 사항이 없음을 확인했다. focused 74 tests, debug Kotlin compile, ktlint는 성공했고, 전체 unit test를 `--rerun-tasks`로 재실행해 162 suites·1,199 tests·실패 0·오류 0·skip 0을 확인했다. `lintDebug`는 변경되지 않은 `AndroidManifest.xml:316`의 `FacebookActivity` `MissingClass`를 포함한 기존 28 errors·767 warnings로 실패했으며, 이번 production 변경 파일에는 기존 `notifyDataSetChanged()` warning 외 신규 lint error가 없었다. 문서 기록 후 `./gradlew tasks --all --console=plain`도 성공했고 결과는 `reviews/phase18-live-all-item-review.md`에 기록했다.
|
||||||
|
|
||||||
|
### 완료 조건
|
||||||
|
|
||||||
|
- [x] 빈 `lives` 입력에서 adapter item이 0개이고 기존 Fragment의 라이브 섹션 숨김 정책이 유지된다.
|
||||||
|
- [x] 라이브가 1개 이상 20개 이하이면 모든 라이브 뒤에 `전체` 항목이 표시된다.
|
||||||
|
- [x] 라이브가 21개 이상이면 라이브 최대 20개와 `전체` 항목 1개만 표시된다.
|
||||||
|
- [x] 기존 `전체` 디자인·문구·클릭 목적지와 라이브 item 입장 흐름이 변경되지 않는다.
|
||||||
|
- [x] 변경 파일이 Phase 18 Files 범위 안이고 신규 파일·dependency·commit이 생성되지 않는다.
|
||||||
|
|
||||||
|
### 검증 방법
|
||||||
|
|
||||||
|
#### Phase 18 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P18-GATE`:** Phase 18의 0개·1개·20개·21개 이상 경계와 기존 `전체` 항목 회귀를 최종 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P18-T1` 체크박스와 검증 기록 완료.
|
||||||
|
- **완료 증거:** Task 18.1 REFACTOR 명령이 모두 성공하고 아래 대조를 완료한다.
|
||||||
|
- **범위 밖:** Gate 통과를 위한 test 삭제·완화, `HomeMainFragment`·전체 라이브 화면·API 수정, 관련 없는 홈 코드 정리.
|
||||||
|
|
||||||
|
대조:
|
||||||
|
|
||||||
|
- [x] `HomeLiveAdapter.submitItems()`가 표시 라이브를 `take(20)`으로 제한한다.
|
||||||
|
- [x] `showAllItem`은 원본 `items.isNotEmpty()`로만 결정된다.
|
||||||
|
- [x] 기존 `MoreViewHolder`, `createMoreView()`, `onMoreClick` 연결은 변경되지 않는다.
|
||||||
|
- [x] Git diff가 Phase 18 Files와 승인된 PRD 범위 안에 있고 커밋이 생성되지 않았다.
|
||||||
|
|
||||||
|
계획 기록:
|
||||||
|
|
||||||
|
- 2026-07-31: 사용자 인터뷰에서 라이브 0개는 기존처럼 섹션을 숨기고 1개 이상이면 최대 20개 뒤에 `전체` 항목을 항상 표시하는 정책을 확정했다. 기존 20개 상한·`전체` 디자인·클릭 목적지·로그인 가드·API 계약을 유지하고, `HomeLiveAdapter` 내부 상태명과 조건 및 `HomeMainFragmentLayoutTest` 경계 테스트만 변경하는 Phase 18을 작성했다. production·test 구현은 시작하지 않았다.
|
||||||
|
- 2026-07-31: Phase 18을 TDD로 구현했다. RED는 라이브 1개에서 `itemCount`가 기대 2, 실제 1로 실패했고, GREEN은 `showAllItem = items.isNotEmpty()` 최소 변경 후 focused 74개 테스트 성공으로 확인했다. 전체 1,199 tests와 compile·ktlint·diff 검증도 성공했으며 기존 `MoreViewHolder`·클릭 연결·20개 cap은 변경하지 않았다.
|
||||||
|
|
||||||
|
## Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 2026-07-31 | `DEC-017` | 확정 | 모든 `모두 팔로우` 동작은 공통 확인 절차를 사용하고, 현재 구현은 노출 중인 응원 크리에이터 섹션으로 제한한다. 신규 공통 abstraction 없이 기존 `V2ModalDialog`를 재사용한다. | 사용자 `deep-interview`의 B안 선택과 현재 `genreCreators` 숨김 정책 | `P17-T1`, `P17-GATE`, `prd.md`의 `2026-07-31 변경` |
|
||||||
|
| 2026-07-31 | `DEC-018` | 확정 | Phase 7의 2026-06-05 보안 재리뷰에서 승인된 유료 미구매 원본 이미지 미로드 정책을 기준으로 삼고, commit `2d58a876`에서 재도입된 client-side blur 원본 로드를 회귀로 수정한다. | 보안 재리뷰·검증 기록, 현재 `HomePopularCommunityAdapter`와 반대 방향의 test 변경 이력 교차 검토 | `REV-P7-001`, `P7-R1` |
|
||||||
|
| 2026-07-31 | `DEC-019` | 확정 | 두 번째 `Phase 12`와 그 하위 `Task 10.*`는 기존 기록을 보존하면서 `P12B` 계열 고유 별칭을 추가해 이후 리뷰와 회귀 작업에서 사용한다. | `work-plan-docs.md`의 Goal/Task 추적성 규칙과 현재 문서의 중복 식별자 | `REV-P12B-001`, `P12B-R1` |
|
||||||
|
| 2026-07-31 | `DEC-020` | 확정 | 현재 진행 중인 라이브가 0개이면 섹션을 숨기고, 1개 이상이면 최대 20개 라이브 뒤에 `전체` 항목을 항상 표시한다. | 사용자 인터뷰 승인과 기존 `HomeLiveAdapter`·전체 라이브 이동 계약 확인 | `P18-T1`, `P18-GATE`, `prd.md`의 `2026-07-31 변경: 현재 진행 중인 라이브 전체 항목 상시 표시` |
|
||||||
|
|
||||||
## Verification Log
|
## Verification Log
|
||||||
|
- 2026-07-31: Phase 18 변경 코드 리뷰 및 최신 검증을 수행했다. `HomeLiveAdapter`의 0개·1~20개·21개 이상 경계와 기존 `MoreViewHolder`, 로그인 가드, `HomeOnAirLiveActivity` 이동 경로를 대조해 확정 발견 사항이 없음을 판정했다. focused 74 tests·compile·ktlint와 `--rerun-tasks` 전체 162 suites·1,199 tests가 실패 0·오류 0으로 성공했고 staged·unstaged diff check와 문서 변경 후 `./gradlew tasks --all --console=plain`도 성공했다. 프로젝트 전체 `lintDebug`는 이번 diff 밖의 기존 28 errors·767 warnings로 실패했으며 첫 오류는 `AndroidManifest.xml:316`의 `FacebookActivity` `MissingClass`다. 상세 결과는 `reviews/phase18-live-all-item-review.md`에 기록했다.
|
||||||
|
- 2026-07-31: Phase 18 TDD와 Gate 검증을 완료했다. 구현 전 focused baseline 73개 성공 후 신규 경계 테스트가 라이브 1개에서 `expected:<2> but was:<1>`로 RED 실패했고, `HomeLiveAdapter`의 `hasMore`를 `showAllItem`으로 바꾸고 `items.isNotEmpty()` 조건을 적용한 뒤 focused 74개가 성공했다. `:app:testDebugUnitTest --no-parallel --console=plain` 전체 실행은 162 suites, 1,199 tests, 실패 0·오류 0·skip 0으로 성공했고 `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`도 성공했다. 기존 deprecation warning 외 실패는 없었으며 신규 파일·dependency·commit은 생성하지 않았다.
|
||||||
|
- 2026-07-31: Phase 18 문서 준비 검증으로 PRD의 0개·1~20개·21개 이상 표시 계약과 Non-Goal을 `P18-T1` 및 `P18-GATE`에 대조했고 누락된 확정 요구사항이 없음을 확인했다. `P18`, `Task 18`, `showAllItem` 추적 검색과 placeholder/TODO 검색, `git diff --check`가 성공했으며 변경 파일은 `prd.md`, `plan-task.md` 두 문서뿐이다. `./gradlew tasks --all --console=plain` 최초 sandbox 실행은 사용자 Gradle cache lock 접근 제한으로 실패했고, 승인된 동일 명령 재실행은 `BUILD SUCCESSFUL`로 완료됐다. production·test 구현은 시작하지 않았고 커밋은 생성하지 않았다.
|
||||||
|
- 2026-07-31: `REV-P7-003` 후속 `Task R7.3` / `P7-R3`를 실행했다. `HomeMainFragmentLayoutTest`의 source 문자열 검사를 제거하고 `HomePopularCommunityAdapterTest`에 실제 adapter/ViewHolder bind 회귀 2건을 추가해 잠금 유료 원본 미로드·기존 Coil 요청 해제·drawable 제거·overlay/가격 유지와 무료·구매 완료 URL 전달을 검증했다. 보안 분기 mutation에서는 잠금 test가 `HomePopularCommunityAdapterTest.kt:72`에서 의도대로 실패했고 원상 복구 후 다시 통과했다. 직접 영향 test·compile·ktlint와 전체 162 suites/1,198 tests가 실패 0·error 0으로 성공했고, 문서 반영 후 `./gradlew tasks --all --console=plain`도 BUILD SUCCESSFUL이었다. 최종 production 코드는 이번 Task에서 추가 변경하지 않았다. Phase별 리뷰를 재검색한 결과 새 확정 항목은 확인되지 않았다.
|
||||||
|
- 2026-07-31: Phase별 리뷰 리포트 최종 문서 검증으로 `git diff --check`를 실행해 출력 없이 성공했고, `reviews/` 아래 Phase 1~17 및 별도 이력 Phase 12B 보고서 18개 존재와 `REV-P7-003` ↔ `Task R7.3` ↔ `P7-R3` 연결을 확인했다. `./gradlew tasks --all --console=plain` 최초 sandbox 실행은 사용자 Gradle cache lock 접근 제한으로 실패했으며, 승인된 동일 명령 재실행은 BUILD SUCCESSFUL로 완료되어 프로젝트 task 인식 문제와 분리했다.
|
||||||
|
- 2026-07-31: 현재 working tree를 Phase별 리뷰 보고서와 다시 대조했다. 신규 확정 항목은 Phase 7의 유료 미구매 원본 이미지 미로드 테스트가 실제 adapter bind, 기존 request/drawable 제거, lock overlay와 가격 유지를 실행하지 않고 production source 문자열만 검사하는 회귀 보호 공백(`REV-P7-003`) 1건이다. 이를 `Task R7.3` / `P7-R3`의 미완료 실행형 회귀 테스트 보강 Task로 Phase 7에 추가했으며 production 코드는 변경하지 않았다. Task 12.2는 사용자 수동 확인 완료 상태로 Phase 5·6·8·9·10·11·12·13 리포트의 오래된 blocker/남은 항목을 현재 상태와 동기화했다.
|
||||||
|
- 2026-07-31: 최신 자동 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*" --tests "kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialogTest" :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck --no-parallel --console=plain`을 실행해 BUILD SUCCESSFUL을 확인했다. 이어 `./gradlew :app:testDebugUnitTest --no-parallel --console=plain`을 실행했고 test result XML 161 suites의 1,197 tests에서 실패 0·error 0으로 BUILD SUCCESSFUL을 확인했다. 과거 `REV-P12-003`의 비결정적 creator channel/preferences 실패 이력은 삭제하지 않고 동일 실패 재현 시 별도 격리 범위로 재개한다.
|
||||||
|
- 2026-07-31: 사용자 수동 확인 결과 Task 12.2 홈 추천 화면 항목과 Phase 17 모두 팔로우 확인창 항목은 모두 동작함을 확인했다. 단, `모두 팔로우` 터치 시 로그인 가드보다 확인 Dialog가 먼저 표시되는 버그가 발견되어 `Task R17.2` / `P17-R2`로 전환했다.
|
||||||
|
- 2026-07-31: `Task R17.2`로 모두 팔로우 확인창 로그인 가드 순서를 복구했다. RED는 source regression 2건 실패로 확인했고, GREEN은 `onCheerFollowAllClick`에서 기존 `ensureV2Access(AccessRequirement.Login)` 통과 후 확인 Dialog를 표시하도록 최소 수정했다. 직접 영향 회귀, compile, ktlint, `git diff --check`는 모두 성공했다.
|
||||||
|
- 2026-07-31: 수동 Gate blocker를 재검증했다. `adb devices`는 `2cec640c34017ece device`를 반환했고 `./gradlew :app:installDebug`, `adb shell monkey -p kr.co.vividnext.sodalive.debug -c android.intent.category.LAUNCHER 1`는 성공했다. `adb shell dumpsys window`는 `MainV2Activity` foreground를 반환했지만 캡처 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sodalive_manual_gate_20260731/screen.png`는 검은 배경의 splash/voice on 화면으로 판독되어 홈 추천 화면과 Phase 17 확인창 수동 검증은 계속 blocked다.
|
||||||
|
- 2026-07-31: Phase별 리뷰 보고서와 `Task R12.2` 문서 변경 후 `./gradlew tasks --all --console=plain`을 실행해 BUILD SUCCESSFUL을 확인했다. `reviews/`에는 Phase 1~17 각각의 보고서와 별도 이력 Phase 12B 보고서가 존재하며, 확정 항목의 review ID·신규 Task·검증 기록 연결 및 `git diff --check` 성공을 확인했다.
|
||||||
|
- 2026-07-31: 현재 working tree 최종 회귀로 `./gradlew :app:testDebugUnitTest --no-parallel`을 두 차례 실행했고, 각각 1,196개 중 `CreatorChannelHomeViewModelTest`의 `SharedPreferenceManager.can` assertion 2건과 1건이 실패했다. 실패 class 19개 test를 격리 재실행하면 모두 통과했고, 실패 지점은 비동기 DataStore observer/write를 공유하는 `SharedPreferenceManager.can`이 차감 후 이전 값으로 돌아온 assertion으로 좁혀졌다. 현재 diff에 `v2.creator.channel`, `SharedPreferenceManager`, `AppPreferencesDataStoreProvider` 변경은 없고 홈 추천·모달 focused 159 tests와 resource merge·compile·ktlint는 성공했다. 이 항목은 `REV-P12-003` 보류로 기록하고, 해당 공유 저장소/테스트 격리 범위가 별도로 승인되면 재현 test부터 재개한다.
|
||||||
|
- 2026-07-31: `REV-P12-002` 후속 `Task R12.2`로 미완료 Task 12.2에 남은 `첫 오디오 태그 조건` 표시 계약을 Phase 12B의 제거 결과와 대조했다. 과거 이력은 보존하고 활성 수동 항목만 `제거된 첫 오디오 섹션과 관련 태그 미노출`로 정정했다. production/resource 금지 참조 검색과 `git diff --check`는 성공했으며 production·test·resource는 변경하지 않았다.
|
||||||
|
- 2026-07-31: Task R1.1, R7.2, R12.1, R12B.2, R17.1 후속 보완을 진행했다. R1.1은 현재 `HomeMainFragment` canonical 경로를 PRD에 반영하고 파일 존재/미존재 및 문서 경로 검색을 확인했다. R7.2와 R17.1은 신규 영문 테스트명 3건을 한글 문장으로 정정하고 focused/direct 영향 테스트를 통과시켰다. R12B.2는 PRD 활성 영역의 first audio 완료 조건을 제거하고 과거 기록/제거 기록은 보존했다. R12.1은 연결 기기와 설치·런처 실행은 확인했지만 `SplashActivity` 검은 화면 캡처만 재현되어 Task 12.2 수동 확인은 홈 추천 화면 접근 불가로 blocked 상태를 갱신했다. 검증으로 focused 3개 테스트, `HomeMainFragmentLayoutTest`·`V2ModalDialogTest`, `./gradlew :app:ktlintCheck`, `git diff --check`가 성공했다.
|
||||||
|
- 2026-07-31: Phase 재리뷰 최종 자동 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*" --tests "kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialogTest" :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck --no-parallel`을 실행해 BUILD SUCCESSFUL, 159 tests, 실패 0, error 0을 확인했다. 최초 sandbox 실행은 사용자 Gradle cache lock 접근 제한으로 실패했고, 승인된 동일 명령 재실행으로 코드 결함과 분리했다. `adb devices`에서 사용 가능한 기기 1대를 확인했지만, 이번 리뷰 범위에서 앱 설치나 수동 UI 검증은 실행하지 않았다.
|
||||||
|
- 2026-07-31: Phase별 리뷰 초안을 코드·test·PRD·현재 기기 상태와 재대조했다. 추가 확정 항목은 현재 `HomeMainFragment` 경로와 활성 문서 경로의 불일치(`REV-P1-001`), Phase 7 회귀 테스트의 신규 영문 테스트명(`REV-P7-002`), 연결 기기가 현재 존재하지만 `연결된 Android 기기 없음` 상태로 남은 Phase 12 수동 검증 blocker(`REV-P12-001`), 제거된 first audio가 PRD 활성 영역에 남은 정합성 문제(`REV-P12B-002`), Phase 17 신규 영문 테스트명 2건(`REV-P17-001`)이다. 각각 `Task R1.1`·`Task R7.2`·`Task R12.1`·`Task R12B.2`·`Task R17.1`로 전환했고, 이번 리뷰 요청의 코드 수정 범위가 아니므로 미완료 체크박스로 남겼다. 기존 `REV-P7-001`과 `REV-P12B-001`은 현재 working tree에서 수정·검증까지 완료된 상태로 재판정했다.
|
||||||
|
- 2026-07-31: `plan-task.md` 전체 Phase를 현재 working tree와 대조해 독립 리뷰했다. Phase 7에서 2026-06-05 보안 재리뷰로 제거했던 유료 미구매 원본 이미지 로드가 commit `2d58a876`에서 client-side blur 방식으로 재도입된 회귀를 `REV-P7-001`로, 두 번째 `Phase 12` 및 그 안의 `Task 10.1`·`Task 10.2` 중복 식별자를 `REV-P12B-001`로 확정해 각각 `Task R7.1`/`P7-R1`, `Task R12B.1`/`P12B-R1`로 전환했다. 그 밖의 Phase에는 확정 코드 결함이 없었고, Phase 12 최종 수동 확인과 Phase 17 수동 Gate는 기존 blocker를 유지한다. 직접 영향 검증 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*" --tests "kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialogTest" :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck --no-parallel`은 159 tests, 실패 0으로 성공했다. 전체 `./gradlew :app:testDebugUnitTest --no-parallel` 최초 실행은 범위 밖 `CreatorChannelHomeViewModelTest`의 비동기 `SharedPreferenceManager.can` assertion 1건이 실패했으나 해당 클래스 격리 재실행과 전체 1,196 tests 재실행은 모두 성공해 이번 Phase 변경 회귀와 분리했다. Phase별 결과는 `reviews/phase1-*.md`부터 `reviews/phase17-*.md`까지 기록했으며 기존 Phase 16 보고서는 재검토 기록을 누적했다.
|
||||||
|
- 2026-07-31: Phase 17 독립 리뷰 게이트를 수행했다. 리뷰 결과는 PASS이며 blocking finding은 없었다. 리뷰에서 `onCheerFollowAllClick`이 확인창 helper만 호출하는 점, 확인 버튼에서만 `followCreators(SECTION_KEY_CHEER_CREATORS, creatorIds)`가 실행되는 점, 취소는 `V2ModalDialog`의 null cancel callback으로 dismiss만 수행하는 점, `HomeFollowAllButtonBinder`의 완료/빈 목록 no-op이 유지되는 점, 한국어·영어·일본어 문자열이 PRD와 일치하는 점, 수동 시각 검증 blocker가 문서에 정직하게 남은 점을 확인했다.
|
||||||
|
- 2026-07-31: Phase 17 RED/GREEN 검증을 수행했다. RED는 `HomeMainFragmentLayoutTest`에 확인창 연결과 다국어 문자열 계약을 먼저 추가한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`에서 신규 `home_recommendation_follow_all_confirm_*` resource 미정의 컴파일 실패로 확인했다. GREEN 구현 후 동일 focused test는 BUILD SUCCESSFUL을 확인했다. 이후 직접 영향 회귀로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialogTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`를 실행했고 모두 성공했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation warning과 Gradle deprecation warning은 기존 경고다. 표면 확인으로 `adb devices`에서 `2cec640c34017ece device`를 확인했고 `./gradlew :app:installDebug`, `adb shell monkey -p kr.co.vividnext.sodalive.debug -c android.intent.category.LAUNCHER 1`, `adb shell screencap`/`adb pull`을 실행했다. 설치와 런처 실행은 성공했지만 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sodalive_phase17_home.png` 캡처는 완전 검은 화면으로 확인되어 실제 확인창 육안 확인은 blocked로 남긴다. 커밋은 생성하지 않았다.
|
||||||
|
- 2026-07-31: Task R7.1 RED/GREEN을 진행했다. RED는 `HomeMainFragmentLayoutTest.home popular community adapter does not load original image for locked paid post`를 추가한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.home popular community adapter does not load original image for locked paid post"`에서 `HomeMainFragmentLayoutTest.kt:1032` assertion 실패로 확인했다. GREEN은 `HomePopularCommunityAdapter`에서 유료 미구매 item의 `imageUrl`을 image loader에 전달하지 않도록 바꾸고 `BlurTransformation` client-side blur 경로를 제거한 뒤 동일 focused test BUILD SUCCESSFUL을 확인했다. PRD의 blur 문구는 원본 미로드 보안 결정과 일치하도록 갱신했다. 남은 검증은 `HomeMainFragmentLayoutTest` 전체, compile, ktlint, `git diff --check`, 리뷰 보고서 후속 기록이다.
|
||||||
|
- 2026-07-31: Task R7.1 최종 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`를 순차 실행했고 모두 성공했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation warning과 Gradle deprecation warning은 기존 경고다. `reviews/phase7-popular-community-review.md` 수정 후 검증 기록까지 누적해 `REV-P7-001` 후속 항목을 완료했다.
|
||||||
|
- 2026-07-31: Task R12B.1 문서 추적성 복구를 완료했다. 중복 재현 명령 `rg -n "^(### Phase 12:|## Phase 12:|#### Task 10\.|- \[x\] \*\*Task 10\.)" "docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md"`로 첫 번째 `Phase 12`, 두 번째 `Phase 12`, Phase 10 `Task 10.1`, 두 번째 Phase 12의 `Task 10.1`·`Task 10.2` 충돌을 확인했다. 기존 완료 제목과 체크박스는 보존하고 두 번째 Phase 12에 `P12B`, `P12B-T1`, `P12B-T2` 별칭 표와 이후 참조 규칙을 추가했다. `rg -n "P12B|P12B-T1|P12B-T2|P12B-R1|REV-P12B-001" "docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md" "docs/20260601_메인_홈_추천_UI와_API_연동/reviews/phase12-first-audio-removal-review.md"`와 `git diff --check`는 성공했고, 리뷰 보고서 후속 기록까지 누적해 `REV-P12B-001` 후속 항목을 완료했다.
|
||||||
|
- 2026-07-31: 최종 전체 회귀로 `./gradlew :app:testDebugUnitTest`를 실행했고 BUILD SUCCESSFUL을 확인했다. Gradle deprecation warning은 기존 경고다.
|
||||||
|
- 2026-07-31: Phase 17 문서 준비 검증으로 PRD의 공통 정책·현재 적용 범위·다국어 문구·Non-Goal을 `P17-T1` 및 `P17-GATE`에 대조했고 누락된 확정 요구사항이 없음을 확인했다. 신규 Phase의 구현·Gate 체크박스는 모두 미완료이며 production·test·resource 구현을 시작하지 않았다. `git status --short`는 변경 파일이 `prd.md`, `plan-task.md` 두 문서뿐임을 표시했고, placeholder 검색과 `git diff --check`는 출력 없이 성공했다. `./gradlew tasks --all`은 최초 sandbox 실행에서 사용자 Gradle cache lock 접근 제한으로 실패했으나 승인된 동일 명령 재실행은 `BUILD SUCCESSFUL`로 완료됐다.
|
||||||
|
- 2026-07-30: Phase 16 방금 활동한 크리에이터 LIVE 라우팅 검증으로 focused RED/GREEN과 직접 영향 회귀를 수행했다. RED는 `creatorId`, `LiveRoom`, `CreatorChannel` 미정의 컴파일 실패로 확인했고, GREEN 후 focused home tests와 `v2.live.action.*`, `v2.creator.action.*` 회귀, `compileDebugKotlin`, `ktlintCheck`, `git diff --check`가 모두 통과했다. 리뷰에서 `targetId <= 0L`일 때 creator fallback이 실행될 수 있는 차단 이슈가 확인되어 `0L/-1L` 회귀 테스트와 null 전용 fallback 분기를 추가했고, 재검증과 재리뷰에서 blocking finding 없음으로 승인받았다. 수동 표면 확인으로 `adb devices`에서 `2cec640c34017ece device`를 확인하고 `installDebug` 및 launcher monkey 실행을 성공시켰다. 실제 LIVE item 터치는 추천 데이터 조건이 필요해 자동 route/source 테스트로 대체했다.
|
||||||
- 2026-07-12: Phase 15 추천 섹션 타이틀 chevron 숨김 변경 검증으로 `HomeMainFragmentSourceTest`에 `홈 추천 최근 데뷔와 AI 캐릭터 섹션 타이틀은 chevron을 숨긴다`를 추가했고, 구현 전 `HomeMainFragmentSourceTest.kt:40` assertion 실패로 RED를 확인했다. `HomeMainFragment.setUpSectionTitles()`에서 `viewHomeRecentDebutTitle`, `viewHomeAiCharacterTitle`의 `showMore = true`를 제거한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest"`, `./gradlew :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck`, `git diff --check`가 통과했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation warning은 기존 설정 경고다. `./gradlew :app:testDebugUnitTest` 전체 실행은 `CreatorChannelActivitySourceTest > 팬Talk 섹션은 Figma ListComment layout과 bind로 렌더링한다`, `CreatorChannelHomeViewModelTest > 채널 후원 성공은 기존 후원 API를 호출하고 홈을 다시 로드한다` 실패 후 타임아웃됐으며, 실패 영역은 이번 변경 파일과 무관한 `v2.creator.channel` 범위다. `adb devices`는 처음 1대를 표시했으나 `:app:installDebug` 시점에는 `No connected devices!`로 실패했고 이후 `adb devices`가 빈 목록을 반환해 실기기 수동 확인은 수행하지 못했다.
|
- 2026-07-12: Phase 15 추천 섹션 타이틀 chevron 숨김 변경 검증으로 `HomeMainFragmentSourceTest`에 `홈 추천 최근 데뷔와 AI 캐릭터 섹션 타이틀은 chevron을 숨긴다`를 추가했고, 구현 전 `HomeMainFragmentSourceTest.kt:40` assertion 실패로 RED를 확인했다. `HomeMainFragment.setUpSectionTitles()`에서 `viewHomeRecentDebutTitle`, `viewHomeAiCharacterTitle`의 `showMore = true`를 제거한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest"`, `./gradlew :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck`, `git diff --check`가 통과했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation warning은 기존 설정 경고다. `./gradlew :app:testDebugUnitTest` 전체 실행은 `CreatorChannelActivitySourceTest > 팬Talk 섹션은 Figma ListComment layout과 bind로 렌더링한다`, `CreatorChannelHomeViewModelTest > 채널 후원 성공은 기존 후원 API를 호출하고 홈을 다시 로드한다` 실패 후 타임아웃됐으며, 실패 영역은 이번 변경 파일과 무관한 `v2.creator.channel` 범위다. `adb devices`는 처음 1대를 표시했으나 `:app:installDebug` 시점에는 `No connected devices!`로 실패했고 이후 `adb devices`가 빈 목록을 반환해 실기기 수동 확인은 수행하지 못했다.
|
||||||
- 2026-07-12: `genreCreators` UI 미표시 변경 검증으로 targeted RED/GREEN 테스트를 수행했다. 리뷰에서 PRD 앞부분의 장르별 크리에이터 표시/모두 팔로우 문구와 `HomeRecommendationUiState.Content.isEmpty`의 `genreCreators.groups` 포함이 차단 이슈로 확인되어, PRD 문구를 미표시 정책으로 정리하고 `isEmpty`는 실제 표시 섹션만 기준으로 판단하도록 수정했다. `home recommendation empty state ignores hidden genre creators` 테스트는 수정 전 `HomeMainFragmentLayoutTest.kt:180` assertion 실패로 RED를 확인했고, 수정 후 GREEN 전환을 확인했다. 추가 재검토에서 plan-task 과거 Task 6.4/6.12와 코드/테스트에 남은 `genreCreators` follow-all 및 pager UI 활성 계약이 차단 이슈로 확인되어, 과거 Task 문구는 현재 미사용 정책으로 정리하고 `HomeMainFragment`의 genre follow-all 연결, `HomeRecommendationViewModel`의 genre follow 완료 상태 갱신, 활성 UI/follow-all 테스트 계약을 제거했다. 이후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck`를 실행해 BUILD SUCCESSFUL을 확인했다. `ktlintCheck` 최초 재실행은 테스트 파일 불필요한 빈 줄로 실패했고 해당 줄만 제거한 뒤 재실행해 성공했다. `git diff --check` 출력 없음도 확인했다. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐고 lint 실패는 없었다. `adb devices` 결과 연결된 기기/에뮬레이터가 없어 실기기 수동 확인은 수행하지 못했다.
|
- 2026-07-12: `genreCreators` UI 미표시 변경 검증으로 targeted RED/GREEN 테스트를 수행했다. 리뷰에서 PRD 앞부분의 장르별 크리에이터 표시/모두 팔로우 문구와 `HomeRecommendationUiState.Content.isEmpty`의 `genreCreators.groups` 포함이 차단 이슈로 확인되어, PRD 문구를 미표시 정책으로 정리하고 `isEmpty`는 실제 표시 섹션만 기준으로 판단하도록 수정했다. `home recommendation empty state ignores hidden genre creators` 테스트는 수정 전 `HomeMainFragmentLayoutTest.kt:180` assertion 실패로 RED를 확인했고, 수정 후 GREEN 전환을 확인했다. 추가 재검토에서 plan-task 과거 Task 6.4/6.12와 코드/테스트에 남은 `genreCreators` follow-all 및 pager UI 활성 계약이 차단 이슈로 확인되어, 과거 Task 문구는 현재 미사용 정책으로 정리하고 `HomeMainFragment`의 genre follow-all 연결, `HomeRecommendationViewModel`의 genre follow 완료 상태 갱신, 활성 UI/follow-all 테스트 계약을 제거했다. 이후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck`를 실행해 BUILD SUCCESSFUL을 확인했다. `ktlintCheck` 최초 재실행은 테스트 파일 불필요한 빈 줄로 실패했고 해당 줄만 제거한 뒤 재실행해 성공했다. `git diff --check` 출력 없음도 확인했다. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐고 lint 실패는 없었다. `adb devices` 결과 연결된 기기/에뮬레이터가 없어 실기기 수동 확인은 수행하지 못했다.
|
||||||
- 2026-07-10: Phase 13 코드 리뷰 및 검증을 재수행했다. 리뷰 범위는 최근 활동 COMMUNITY route가 `targetId`를 `postId`로 해석해 `CreatorChannelCommunityDetailActivity.newIntent(context, postId)`를 생성하는지, 인기 커뮤니티 item이 `FeedItem.Community.postId` 기반으로 상세 Activity를 여는지, 기존 `CreatorCommunityAllActivity`/커뮤니티 목록 extra 제거 여부, 로그인 가드 전 invalid return 유지 여부다. 코드 리뷰 결과 blocking finding은 없었다. 검증으로 Phase 13 targeted test, `HomeMainFragmentLayoutTest` 전체, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --cached --check`가 모두 통과했고, `rg`로 v2 home production 코드에 기존 커뮤니티 목록 라우팅 참조가 남지 않았음을 확인했다.
|
- 2026-07-10: Phase 13 코드 리뷰 및 검증을 재수행했다. 리뷰 범위는 최근 활동 COMMUNITY route가 `targetId`를 `postId`로 해석해 `CreatorChannelCommunityDetailActivity.newIntent(context, postId)`를 생성하는지, 인기 커뮤니티 item이 `FeedItem.Community.postId` 기반으로 상세 Activity를 여는지, 기존 `CreatorCommunityAllActivity`/커뮤니티 목록 extra 제거 여부, 로그인 가드 전 invalid return 유지 여부다. 코드 리뷰 결과 blocking finding은 없었다. 검증으로 Phase 13 targeted test, `HomeMainFragmentLayoutTest` 전체, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --cached --check`가 모두 통과했고, `rg`로 v2 home production 코드에 기존 커뮤니티 목록 라우팅 참조가 남지 않았음을 확인했다.
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ Figma `home_001` 화면(`24:5514`)을 기준으로 메인 홈 추천 영역을
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 2. Problem
|
## 2. Problem
|
||||||
- 메인 홈 추천 화면은 라이브, 배너, 최근 활동, 신규 데뷔, 첫 오디오 콘텐츠, AI 캐릭터, 응원 크리에이터, 인기 커뮤니티 등 여러 데이터 섹션을 한 화면에 표시해야 한다. `genreCreators` 응답 계약은 유지하지만 장르별 크리에이터 UI는 추천 탭에 표시하지 않는다.
|
- 메인 홈 추천 화면은 라이브, 배너, 최근 활동, 신규 데뷔, AI 캐릭터, 응원 크리에이터, 인기 커뮤니티 등 여러 데이터 섹션을 한 화면에 표시해야 한다. `genreCreators` 응답 계약은 유지하지만 장르별 크리에이터 UI는 추천 탭에 표시하지 않는다.
|
||||||
- Figma에는 이미 `live`, `banner`, `section-title`, `contents`, `chat-thumbnail`, `profile`, `feed` 등 재사용 가능한 형태가 다수 포함되어 있으므로, 기존에 생성된 widget을 최대한 활용해야 한다.
|
- Figma에는 이미 `live`, `banner`, `section-title`, `contents`, `chat-thumbnail`, `profile`, `feed` 등 재사용 가능한 형태가 다수 포함되어 있으므로, 기존에 생성된 widget을 최대한 활용해야 한다.
|
||||||
- `HomeActiveCreatorItem.activityType`은 백엔드 enum code 그대로 표시하면 사용자에게 부자연스러우므로 `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY`를 다국어 처리 가능한 표시 문자열로 변환해야 한다.
|
- `HomeActiveCreatorItem.activityType`은 백엔드 enum code 그대로 표시하면 사용자에게 부자연스러우므로 `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY`를 다국어 처리 가능한 표시 문자열로 변환해야 한다.
|
||||||
|
- `모두 팔로우 하기` 버튼은 현재 터치 즉시 여러 크리에이터를 팔로우하는 API를 호출하므로, 사용자가 대상과 영향을 다시 확인하지 못한 채 의도하지 않은 크리에이터까지 팔로우할 수 있다.
|
||||||
- 사업자 정보 섹션은 별도 외부 라이브러리 없이 최대 3줄 말줄임과 더보기/접기 토글을 제공해야 한다.
|
- 사업자 정보 섹션은 별도 외부 라이브러리 없이 최대 3줄 말줄임과 더보기/접기 토글을 제공해야 한다.
|
||||||
- Figma에 포함된 일부 섹션은 이번 범위에서 제외되어야 하므로 구현 범위를 명확히 분리해야 한다.
|
- Figma에 포함된 일부 섹션은 이번 범위에서 제외되어야 하므로 구현 범위를 명확히 분리해야 한다.
|
||||||
|
|
||||||
@@ -20,9 +21,11 @@ Figma `home_001` 화면(`24:5514`)을 기준으로 메인 홈 추천 영역을
|
|||||||
- 추천 탭이 선택된 `HomeMainFragment`에 메인 홈 추천 UI를 구현한다.
|
- 추천 탭이 선택된 `HomeMainFragment`에 메인 홈 추천 UI를 구현한다.
|
||||||
- Figma에서 기존 widget으로 대응 가능한 항목은 기존 v2 widget을 우선 재사용한다.
|
- Figma에서 기존 widget으로 대응 가능한 항목은 기존 v2 widget을 우선 재사용한다.
|
||||||
- 재사용이 어려운 영역만 최소 범위 신규 UI 또는 adapter item으로 정의한다.
|
- 재사용이 어려운 영역만 최소 범위 신규 UI 또는 adapter item으로 정의한다.
|
||||||
|
- 현재 진행 중인 라이브가 1개 이상이면 라이브 가로 목록 마지막에 `전체` 항목을 항상 표시해 전체 라이브 목록으로 이동할 수 있게 한다.
|
||||||
- `추천 필모그래피`, `또 다른 모습` 섹션은 만들지 않는다.
|
- `추천 필모그래피`, `또 다른 모습` 섹션은 만들지 않는다.
|
||||||
- 최하단 사업자 정보 섹션은 표시하되, 기본 최대 3줄 말줄임과 `더보기`/`접기` 토글을 제공한다.
|
- 최하단 사업자 정보 섹션은 표시하되, 기본 최대 3줄 말줄임과 `더보기`/`접기` 토글을 제공한다.
|
||||||
- `HomeActiveCreatorItem.activityType`은 백엔드 code를 앱 내부 enum 또는 mapper로 변환하고, 표시 문구는 string resource 기반 다국어 처리를 적용한다.
|
- `HomeActiveCreatorItem.activityType`은 백엔드 code를 앱 내부 enum 또는 mapper로 변환하고, 표시 문구는 string resource 기반 다국어 처리를 적용한다.
|
||||||
|
- 여러 크리에이터를 한 번에 팔로우하는 모든 `모두 팔로우` 동작은 API 호출 전에 공통 확인창을 표시하고, 사용자가 명시적으로 확인한 경우에만 기존 팔로우 API를 호출한다.
|
||||||
- API DTO, 화면 상태, empty/error/loading 정책, click routing은 구현 계획에서 검증 가능하도록 정리한다.
|
- API DTO, 화면 상태, empty/error/loading 정책, click routing은 구현 계획에서 검증 가능하도록 정리한다.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -38,6 +41,10 @@ Figma `home_001` 화면(`24:5514`)을 기준으로 메인 홈 추천 영역을
|
|||||||
- 이번 범위에서 `랭킹`, `팔로잉`용 별도 Fragment 또는 숨김 View를 미리 만들지 않는다.
|
- 이번 범위에서 `랭킹`, `팔로잉`용 별도 Fragment 또는 숨김 View를 미리 만들지 않는다.
|
||||||
- 기존 레거시 홈 화면 전체 리팩터링은 포함하지 않는다.
|
- 기존 레거시 홈 화면 전체 리팩터링은 포함하지 않는다.
|
||||||
- Figma에 없는 skeleton loading, shimmer, 임의 애니메이션, 추가 badge는 만들지 않는다.
|
- Figma에 없는 skeleton loading, shimmer, 임의 애니메이션, 추가 badge는 만들지 않는다.
|
||||||
|
- 모두 팔로우 확인 절차를 위해 신규 Dialog component나 layout을 만들지 않고 기존 `V2ModalDialog`를 재사용한다.
|
||||||
|
- 모두 팔로우 API endpoint, request body, 성공·실패 처리와 완료 버튼 UI는 변경하지 않는다.
|
||||||
|
- 현재 숨김 상태인 `genreCreators` 섹션이나 모두 팔로우 버튼을 다시 노출하지 않는다.
|
||||||
|
- 라이브 `전체` 항목의 디자인, 클릭 목적지, 로그인 가드와 전체 라이브 목록 화면 동작은 변경하지 않는다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -49,8 +56,10 @@ Figma `home_001` 화면(`24:5514`)을 기준으로 메인 홈 추천 영역을
|
|||||||
|
|
||||||
## 6. User Stories
|
## 6. User Stories
|
||||||
- 사용자는 홈에서 현재 라이브 중인 크리에이터와 추천 콘텐츠를 빠르게 탐색하고 싶다.
|
- 사용자는 홈에서 현재 라이브 중인 크리에이터와 추천 콘텐츠를 빠르게 탐색하고 싶다.
|
||||||
|
- 사용자는 현재 진행 중인 라이브가 적더라도 `전체` 항목을 통해 전체 라이브 목록 화면으로 이동하고 싶다.
|
||||||
- 사용자는 배너를 스와이프하거나 터치해 관련 이벤트, 크리에이터, 시리즈, 외부 링크 또는 앱 내부 딥링크로 이동하고 싶다.
|
- 사용자는 배너를 스와이프하거나 터치해 관련 이벤트, 크리에이터, 시리즈, 외부 링크 또는 앱 내부 딥링크로 이동하고 싶다.
|
||||||
- 사용자는 최근 활동한 크리에이터의 활동 유형을 `라이브`, `오디오`, `커뮤니티`처럼 이해 가능한 문구로 보고 싶다.
|
- 사용자는 최근 활동한 크리에이터의 활동 유형을 `라이브`, `오디오`, `커뮤니티`처럼 이해 가능한 문구로 보고 싶다.
|
||||||
|
- 사용자는 여러 크리에이터를 한 번에 팔로우하기 전에 확인창에서 동작을 검토하고, 확인하거나 취소할 수 있기를 원한다.
|
||||||
- 사용자는 인기 커뮤니티 글을 읽다가 사업자 정보가 길어도 홈 화면 탐색이 과도하게 방해받지 않기를 기대한다.
|
- 사용자는 인기 커뮤니티 글을 읽다가 사업자 정보가 길어도 홈 화면 탐색이 과도하게 방해받지 않기를 기대한다.
|
||||||
- 개발자는 기존 v2 widget을 최대한 재사용해 화면별 UI 중복과 스타일 차이를 줄이고 싶다.
|
- 개발자는 기존 v2 widget을 최대한 재사용해 화면별 UI 중복과 스타일 차이를 줄이고 싶다.
|
||||||
|
|
||||||
@@ -88,6 +97,15 @@ data class HomeRecommendationResponse(
|
|||||||
#### Changed Item Contracts
|
#### Changed Item Contracts
|
||||||
```kotlin
|
```kotlin
|
||||||
|
|
||||||
|
data class HomeActiveCreatorItem(
|
||||||
|
val creatorId: Long,
|
||||||
|
val creatorNickname: String,
|
||||||
|
val creatorProfileImage: String,
|
||||||
|
val activityType: String,
|
||||||
|
val activityAt: String,
|
||||||
|
val targetId: Long?
|
||||||
|
)
|
||||||
|
|
||||||
data class HomeAiCharacterItem(
|
data class HomeAiCharacterItem(
|
||||||
val characterId: Long,
|
val characterId: Long,
|
||||||
val creatorId: Long,
|
val creatorId: Long,
|
||||||
@@ -115,6 +133,9 @@ data class HomePopularCommunityPostItem(
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Changed Item Requirements
|
#### Changed Item Requirements
|
||||||
|
- `HomeActiveCreatorItem.creatorId`는 서버가 항상 양수로 제공하는 필수 `Long` 필드이며, 최근 활동 UI model까지 보존한다.
|
||||||
|
- API 계약과 별개로 클라이언트는 크리에이터 채널 route 생성 전에 `creatorId > 0L`을 확인한다.
|
||||||
|
- `HomeActiveCreatorItem.activityType == LIVE`일 때 유효한 `targetId > 0L`는 라이브 `roomId`로 해석하고, `targetId == null && creatorId > 0L`이면 `creatorId`를 크리에이터 채널 이동 값으로 사용한다.
|
||||||
- `HomeAiCharacterItem.creatorId`는 `크리에이터와 이야기를 나눠요!` 섹션 item 터치 시 크리에이터 채널 이동 값으로 사용한다.
|
- `HomeAiCharacterItem.creatorId`는 `크리에이터와 이야기를 나눠요!` 섹션 item 터치 시 크리에이터 채널 이동 값으로 사용한다.
|
||||||
- `HomeAiCharacterItem.characterId`는 `CharacterChatThumbnailView` 표시 모델의 캐릭터 식별 값으로 유지하되, 추천 홈 item 터치 목적지로는 사용하지 않는다.
|
- `HomeAiCharacterItem.characterId`는 `CharacterChatThumbnailView` 표시 모델의 캐릭터 식별 값으로 유지하되, 추천 홈 item 터치 목적지로는 사용하지 않는다.
|
||||||
- `HomeAiCharacterItem.profileImage`는 `CharacterChatThumbnailView`의 실제 이미지 URL로 사용한다.
|
- `HomeAiCharacterItem.profileImage`는 `CharacterChatThumbnailView`의 실제 이미지 URL로 사용한다.
|
||||||
@@ -123,6 +144,15 @@ data class HomePopularCommunityPostItem(
|
|||||||
- `HomePopularCommunityPostItem.price`, `existOrdered`는 추천 페이지용 `FeedCommunityView`의 유료 미구매 UI 판정에 사용한다.
|
- `HomePopularCommunityPostItem.price`, `existOrdered`는 추천 페이지용 `FeedCommunityView`의 유료 미구매 UI 판정에 사용한다.
|
||||||
- `HomePopularCommunityPostItem.content`, `price`, `createdAt`, `likeCount`, `commentCount`는 인기 커뮤니티 feed 표시 데이터로 사용한다.
|
- `HomePopularCommunityPostItem.content`, `price`, `createdAt`, `likeCount`, `commentCount`는 인기 커뮤니티 feed 표시 데이터로 사용한다.
|
||||||
|
|
||||||
|
### 현재 진행 중인 라이브 `전체` 항목
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- `lives`가 비어 있으면 기존 정책대로 라이브 섹션 전체를 숨기고 `전체` 항목도 표시하지 않는다.
|
||||||
|
- `lives`가 1개 이상 20개 이하이면 모든 라이브 항목 뒤에 `전체` 항목을 표시한다.
|
||||||
|
- `lives`가 21개 이상이면 기존 정책대로 라이브를 최대 20개까지 표시하고 마지막에 `전체` 항목을 표시한다.
|
||||||
|
- `전체` 항목의 기존 크기, 색상, typography와 클릭 시 `HomeOnAirLiveActivity`로 이동하는 동작을 유지한다.
|
||||||
|
- 라이브 item 표시 순서, item 클릭 입장 흐름, 추천 API와 전체 라이브 목록 API 계약은 변경하지 않는다.
|
||||||
|
|
||||||
### Activity Type 다국어 표시
|
### Activity Type 다국어 표시
|
||||||
`HomeActiveCreatorItem.activityType`은 백엔드 enum code를 앱 표시용 enum으로 변환한다.
|
`HomeActiveCreatorItem.activityType`은 백엔드 enum code를 앱 표시용 enum으로 변환한다.
|
||||||
|
|
||||||
@@ -144,6 +174,18 @@ enum class RecommendedActivityType(val code: String) {
|
|||||||
- 알 수 없는 code는 크래시시키지 않고 기본값 또는 미표시 정책을 구현 계획에서 확정한다.
|
- 알 수 없는 code는 크래시시키지 않고 기본값 또는 미표시 정책을 구현 계획에서 확정한다.
|
||||||
- mapper 또는 enum 변환 로직은 local unit test로 검증한다.
|
- mapper 또는 enum 변환 로직은 local unit test로 검증한다.
|
||||||
|
|
||||||
|
### 방금 활동한 크리에이터 LIVE 라우팅
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- `activityType == LIVE && targetId > 0L`인 item을 터치하면 `targetId`를 `roomId`로 사용해 라이브 룸 진입을 요청한다.
|
||||||
|
- `activityType == LIVE && targetId == null`인 item을 터치하면 `creatorId > 0L`을 확인한 뒤 크리에이터 채널로 이동한다.
|
||||||
|
- 서버 버그 등으로 `creatorId <= 0L`이면 route를 만들지 않고 이동하지 않는다.
|
||||||
|
- 라이브 룸 진입은 홈 전용 인증 분기를 추가하지 않고 기존 `LiveActionCoordinator.enterLiveRoom(roomId)`을 사용한다.
|
||||||
|
- `LiveActionCoordinator`의 기존 흐름에 따라 비로그인 사용자는 로그인 절차로 이동하고, 방 상세 조회 후 성인 방이면 사용자 상태에 맞춰 본인인증 또는 콘텐츠 보기 옵션 설정 절차를 적용한다.
|
||||||
|
- 크리에이터 채널 이동은 기존 `CreatorActionCommand.Profile(creatorId)` 공통 액션을 사용한다.
|
||||||
|
- `LIVE_REPLAY`, `AUDIO`, `COMMUNITY`, 알 수 없는 activity type의 기존 라우팅 정책은 변경하지 않는다.
|
||||||
|
- 이번 작업에서는 공통 로그인·본인인증·성인 콘텐츠 보기 정책 자체를 변경하지 않는다.
|
||||||
|
|
||||||
### 사업자 정보 접기/더보기
|
### 사업자 정보 접기/더보기
|
||||||
Figma 최하단 사업자 정보 섹션(`218:2058`)은 외부 라이브러리 없이 TextView와 클릭 가능한 컨트롤로 구현한다.
|
Figma 최하단 사업자 정보 섹션(`218:2058`)은 외부 라이브러리 없이 TextView와 클릭 가능한 컨트롤로 구현한다.
|
||||||
|
|
||||||
@@ -172,7 +214,7 @@ Figma 최하단 사업자 정보 섹션(`218:2058`)은 외부 라이브러리
|
|||||||
- 세로 스크롤 시 title-bar와 `TextTabBarView`는 화면에 유지하고, `TextTabBarView` 아래 추천 content 영역만 스크롤되도록 구성한다.
|
- 세로 스크롤 시 title-bar와 `TextTabBarView`는 화면에 유지하고, `TextTabBarView` 아래 추천 content 영역만 스크롤되도록 구성한다.
|
||||||
|
|
||||||
### 모두 팔로우 하기
|
### 모두 팔로우 하기
|
||||||
`최근 응원이 많은 크리에이터` 섹션에는 모두 팔로우 버튼을 제공한다. `장르별 크리에이터`는 추천 탭 UI에 표시하지 않으므로 모두 팔로우 버튼도 제공하지 않는다.
|
여러 크리에이터를 한 번에 팔로우하는 모든 `모두 팔로우` 동작은 API 호출 전에 공통 확인 절차를 거친다. 현재 구현 대상은 실제 노출 중인 `최근 응원이 많은 크리에이터` 섹션으로 제한한다. `장르별 크리에이터`는 추천 탭 UI에 표시하지 않으므로 모두 팔로우 버튼과 확인창도 제공하지 않는다.
|
||||||
|
|
||||||
#### API Requirements
|
#### API Requirements
|
||||||
- API는 `POST /api/v2/home/recommendations/creators/follow`를 사용한다.
|
- API는 `POST /api/v2/home/recommendations/creators/follow`를 사용한다.
|
||||||
@@ -204,6 +246,23 @@ data class FollowRecommendedCreatorsRequest(
|
|||||||
- 문구는 string resource로 관리한다.
|
- 문구는 string resource로 관리한다.
|
||||||
- 버튼 상태는 API success 후 중복 호출되지 않도록 disabled 또는 click 무시 상태로 전환한다.
|
- 버튼 상태는 API success 후 중복 호출되지 않도록 disabled 또는 click 무시 상태로 전환한다.
|
||||||
|
|
||||||
|
#### Confirm Dialog Requirements
|
||||||
|
- 기본 상태의 `모두 팔로우 하기` 버튼을 터치하면 기존 `V2ModalDialog`의 취소·확인 2버튼 구성을 표시한다.
|
||||||
|
- 최초 버튼 터치만으로는 `POST /api/v2/home/recommendations/creators/follow`를 호출하지 않는다.
|
||||||
|
- Dialog 제목은 `모두 팔로우`, 설명은 `목록의 크리에이터를 모두 팔로우하시겠어요?`, 취소 버튼은 `취소`, 확인 버튼은 `팔로우 하기`로 표시한다.
|
||||||
|
- `팔로우 하기`를 터치하면 Dialog를 닫은 뒤 해당 표시 섹션의 `creatorIds`로 기존 모두 팔로우 API를 한 번 호출한다.
|
||||||
|
- `취소`를 터치하면 Dialog만 닫고 API를 호출하거나 버튼 상태를 변경하지 않는다.
|
||||||
|
- 완료 상태 버튼 또는 빈 `creatorIds`에서는 Dialog와 API를 모두 실행하지 않는다.
|
||||||
|
- Dialog 바깥 영역과 시스템 뒤로가기에 대한 닫기 정책은 기존 `V2ModalDialog`의 non-cancelable 동작을 유지하며, 취소는 명시적인 `취소` 버튼으로 수행한다.
|
||||||
|
- 공통 정책은 이후 다른 화면이나 섹션에 `모두 팔로우` 동작이 추가될 때도 동일하게 적용하지만, 이번 구현에서는 범용 wrapper나 신규 추상화를 미리 만들지 않는다.
|
||||||
|
- 표시 문자열은 아래 resource key로 한국어·영어·일본어에 모두 제공하고, 기존 `cancel` resource를 재사용한다.
|
||||||
|
|
||||||
|
| Resource key | 한국어 | English | 日本語 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `home_recommendation_follow_all_confirm_title` | `모두 팔로우` | `Follow all` | `すべてフォロー` |
|
||||||
|
| `home_recommendation_follow_all_confirm_description` | `목록의 크리에이터를 모두 팔로우하시겠어요?` | `Would you like to follow all creators in this list?` | `このリストのクリエイターをすべてフォローしますか?` |
|
||||||
|
| `home_recommendation_follow_all_confirm_button` | `팔로우 하기` | `Follow` | `フォローする` |
|
||||||
|
|
||||||
### 추천 페이지 위젯 수정
|
### 추천 페이지 위젯 수정
|
||||||
추천 페이지에 표시되는 기존 widget은 화면 요구사항에 맞춰 최소 범위로 확장한다. 별도 PRD로 분리하지 않고, 메인 홈 추천 UI의 표시 계약으로 이 문서에 함께 기록한다.
|
추천 페이지에 표시되는 기존 widget은 화면 요구사항에 맞춰 최소 범위로 확장한다. 별도 PRD로 분리하지 않고, 메인 홈 추천 UI의 표시 계약으로 이 문서에 함께 기록한다.
|
||||||
|
|
||||||
@@ -253,10 +312,6 @@ HomeRecommendation 화면
|
|||||||
│ ├─ Figma instance: section-title, creater
|
│ ├─ Figma instance: section-title, creater
|
||||||
│ ├─ 재사용: view_section_title
|
│ ├─ 재사용: view_section_title
|
||||||
│ └─ 신규: 최근 데뷔 크리에이터 카드 UI, item root `185dp x 234dp`, radius `14dp` 전체 clip
|
│ └─ 신규: 최근 데뷔 크리에이터 카드 UI, item root `185dp x 234dp`, radius `14dp` 전체 clip
|
||||||
├─ 첫 오디오 콘텐츠: firstAudioContents
|
|
||||||
│ ├─ Figma instance: contents, profile
|
|
||||||
│ ├─ 재사용: v2.widget.AudioContentCardView
|
|
||||||
│ └─ 신규: 카드 하단 profile 결합 adapter item 필요 여부 확인
|
|
||||||
├─ AI 캐릭터: aiCharacters
|
├─ AI 캐릭터: aiCharacters
|
||||||
│ ├─ Figma instance: section-title, chat-thumbnail
|
│ ├─ Figma instance: section-title, chat-thumbnail
|
||||||
│ ├─ 재사용: view_section_title
|
│ ├─ 재사용: view_section_title
|
||||||
@@ -310,6 +365,7 @@ HomeRecommendation 화면
|
|||||||
- `link`가 웹 URL이면 `Intent.ACTION_VIEW`를 사용해 외부 웹 URL로 이동한다.
|
- `link`가 웹 URL이면 `Intent.ACTION_VIEW`를 사용해 외부 웹 URL로 이동한다.
|
||||||
- `link`가 내부 딥링크이면 기존 딥링크 실행 정책에 따라 앱 내부 딥링크를 실행한다.
|
- `link`가 내부 딥링크이면 기존 딥링크 실행 정책에 따라 앱 내부 딥링크를 실행한다.
|
||||||
- 웹 URL도 내부 딥링크도 아닌 `link`는 이동하지 않는다.
|
- 웹 URL도 내부 딥링크도 아닌 `link`는 이동하지 않는다.
|
||||||
|
- 모두 팔로우 확인창의 확인 버튼을 빠르게 반복 터치해도 기존 `V2ModalDialog`의 중복 클릭 방지 계약에 따라 API 요청은 한 번만 시작한다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -322,8 +378,9 @@ HomeRecommendation 화면
|
|||||||
- 각 섹션 제목은 기존 `view_section_title`을 사용한다.
|
- 각 섹션 제목은 기존 `view_section_title`을 사용한다.
|
||||||
- `최근 데뷔한 크리에이터` item은 기존 `205dp x 259dp` 비율을 유지해 `185dp x 234dp`로 축소하고, 이미지뿐 아니라 dim gradient와 닉네임을 포함한 item root 전체가 `14dp` rounded corner로 잘려야 한다.
|
- `최근 데뷔한 크리에이터` item은 기존 `205dp x 259dp` 비율을 유지해 `185dp x 234dp`로 축소하고, 이미지뿐 아니라 dim gradient와 닉네임을 포함한 item root 전체가 `14dp` rounded corner로 잘려야 한다.
|
||||||
- `최근 응원이 많은 크리에이터`의 모두 팔로우 버튼은 success 전/후 상태가 명확히 구분되어야 한다. `장르별 크리에이터`는 추천 탭 UI에 표시하지 않으므로 버튼도 노출하지 않는다.
|
- `최근 응원이 많은 크리에이터`의 모두 팔로우 버튼은 success 전/후 상태가 명확히 구분되어야 한다. `장르별 크리에이터`는 추천 탭 UI에 표시하지 않으므로 버튼도 노출하지 않는다.
|
||||||
|
- `최근 응원이 많은 크리에이터`의 기본 상태 모두 팔로우 버튼을 터치하면 기존 V2 공통 디자인의 확인창이 먼저 표시되고, 사용자의 확인 전에는 네트워크 요청이 시작되지 않아야 한다.
|
||||||
- 인기 커뮤니티는 keyword 없이 본문, 선택적 이미지, 유료 잠금 상태, reaction row를 보여야 한다.
|
- 인기 커뮤니티는 keyword 없이 본문, 선택적 이미지, 유료 잠금 상태, reaction row를 보여야 한다.
|
||||||
- 유료 미구매 커뮤니티 이미지는 내용을 바로 읽을 수 없도록 blur/lock overlay가 적용되어야 한다.
|
- 유료 미구매 커뮤니티는 원본 `imageUrl`을 로드하지 않고 lock overlay와 가격 capsule만 표시한다. 별도 안전 preview 필드가 제공되기 전까지 client-side blur를 위해 원본 이미지를 요청하지 않는다.
|
||||||
- 유료 구매 완료 또는 무료 커뮤니티 이미지는 overlay 없이 표시하고, `구매완료` 태그는 표시하지 않는다.
|
- 유료 구매 완료 또는 무료 커뮤니티 이미지는 overlay 없이 표시하고, `구매완료` 태그는 표시하지 않는다.
|
||||||
- 가로 목록은 화면 밖 다음 item이 일부 보이도록 Figma의 peek 느낌을 유지한다.
|
- 가로 목록은 화면 밖 다음 item이 일부 보이도록 Figma의 peek 느낌을 유지한다.
|
||||||
- 커뮤니티 본문과 사업자 정보는 긴 텍스트가 화면 폭을 밀어내지 않아야 한다.
|
- 커뮤니티 본문과 사업자 정보는 긴 텍스트가 화면 폭을 밀어내지 않아야 한다.
|
||||||
@@ -335,11 +392,12 @@ HomeRecommendation 화면
|
|||||||
|
|
||||||
## 9. Technical Constraints
|
## 9. Technical Constraints
|
||||||
- 현재 프로젝트는 Android XML Views + Kotlin + ViewBinding 기반이므로 XML layout, RecyclerView/adapter, custom view 패턴을 우선한다.
|
- 현재 프로젝트는 Android XML Views + Kotlin + ViewBinding 기반이므로 XML layout, RecyclerView/adapter, custom view 패턴을 우선한다.
|
||||||
- UI는 기존 `kr.co.vividnext.sodalive.v2.main.HomeMainFragment`에 구현한다.
|
- UI는 현재 canonical 경로인 `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`의 `kr.co.vividnext.sodalive.v2.main.home.HomeMainFragment`에 구현한다. 과거 `kr.co.vividnext.sodalive.v2.main.HomeMainFragment` 경로는 현재 존재하지 않는다.
|
||||||
- `HomeMainFragment`는 이번 범위에서 추천 content만 직접 구성하며, `ViewPager2`, `FragmentStateAdapter`, tab별 신규 Fragment 3개 구조는 도입하지 않는다.
|
- `HomeMainFragment`는 이번 범위에서 추천 content만 직접 구성하며, `ViewPager2`, `FragmentStateAdapter`, tab별 신규 Fragment 3개 구조는 도입하지 않는다.
|
||||||
- `TextTabBarView` 아래 추천 content 영역만 `NestedScrollView` 또는 동등한 단일 세로 스크롤 컨테이너로 구성한다.
|
- `TextTabBarView` 아래 추천 content 영역만 `NestedScrollView` 또는 동등한 단일 세로 스크롤 컨테이너로 구성한다.
|
||||||
- 신규 API/Repository/ViewModel 및 그와 연결된 하위 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
|
- 신규 API/Repository/ViewModel 및 그와 연결된 하위 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
|
||||||
- 기존 `kr.co.vividnext.sodalive.v2.widget.*` 컴포넌트를 우선 재사용한다.
|
- 기존 `kr.co.vividnext.sodalive.v2.widget.*` 컴포넌트를 우선 재사용한다.
|
||||||
|
- 모두 팔로우 확인 UI는 기존 `kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog`를 재사용하고 공통 모달 component나 layout을 수정하지 않는다.
|
||||||
- 기존 API 인터페이스에 메서드를 추가하지 않고, 홈 추천 전용 신규 API 인터페이스를 만든다.
|
- 기존 API 인터페이스에 메서드를 추가하지 않고, 홈 추천 전용 신규 API 인터페이스를 만든다.
|
||||||
- 이미지 로딩 라이브러리를 새로 추가하지 않고 기존 호출부의 이미지 로딩 방식을 따른다.
|
- 이미지 로딩 라이브러리를 새로 추가하지 않고 기존 호출부의 이미지 로딩 방식을 따른다.
|
||||||
- 외부 라이브러리를 추가하지 않는다.
|
- 외부 라이브러리를 추가하지 않는다.
|
||||||
@@ -356,10 +414,16 @@ HomeRecommendation 화면
|
|||||||
- `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY` 표시 문자열이 string resource 기반으로 매핑된다.
|
- `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY` 표시 문자열이 string resource 기반으로 매핑된다.
|
||||||
- `HomeMainFragment`에서 `view_title_bar_home`, `TextTabBarView`, `view_section_title` 재사용이 확인된다.
|
- `HomeMainFragment`에서 `view_title_bar_home`, `TextTabBarView`, `view_section_title` 재사용이 확인된다.
|
||||||
- `FeedCommunityView`에서 keyword 영역이 추천 페이지에 노출되지 않고, 이미지와 유료 미구매 overlay가 표시된다.
|
- `FeedCommunityView`에서 keyword 영역이 추천 페이지에 노출되지 않고, 이미지와 유료 미구매 overlay가 표시된다.
|
||||||
- `AudioContentCardView`에서 `ic_content_tag_point`, `무료`, `FIRST` 태그가 조건에 맞게 표시되고, `ic_content_tag_original`은 표시되지 않는다.
|
|
||||||
- `HomeAiCharacterItem.creatorId`가 UI model까지 보존되고, AI 캐릭터 item 터치 시 `CreatorChannelActivity`에 `creatorId`가 전달된다.
|
- `HomeAiCharacterItem.creatorId`가 UI model까지 보존되고, AI 캐릭터 item 터치 시 `CreatorChannelActivity`에 `creatorId`가 전달된다.
|
||||||
|
- `HomeActiveCreatorItem.creatorId`가 UI model까지 보존되고, `LIVE` item은 `targetId` 유무에 따라 라이브 룸 또는 크리에이터 채널 route로 분기된다.
|
||||||
|
- 최근 활동 `LIVE`의 `targetId == null && creatorId <= 0L` 입력에서 route가 생성되지 않는다.
|
||||||
|
- 최근 활동 `LIVE`의 라이브 룸 진입이 `LiveActionCoordinator`를 사용해 로그인·본인인증·성인 콘텐츠 보기 설정 정책을 우회하지 않는다.
|
||||||
|
- 현재 진행 중인 라이브가 0개이면 라이브 섹션과 `전체` 항목이 표시되지 않고, 1개 이상이면 최대 20개 라이브 뒤에 `전체` 항목이 표시된다.
|
||||||
- 제외 대상인 `추천 필모그래피`, `또 다른 모습` 섹션이 화면에 생성되지 않는다.
|
- 제외 대상인 `추천 필모그래피`, `또 다른 모습` 섹션이 화면에 생성되지 않는다.
|
||||||
- 최근 응원이 많은 크리에이터 모두 팔로우 API success 후 버튼이 `모두 팔로우 완료`와 `ic_new_following` 상태로 변경된다.
|
- 최근 응원이 많은 크리에이터 모두 팔로우 API success 후 버튼이 `모두 팔로우 완료`와 `ic_new_following` 상태로 변경된다.
|
||||||
|
- 최근 응원이 많은 크리에이터의 기본 상태 모두 팔로우 버튼 터치 직후에는 확인창만 표시되고 API 호출 횟수는 `0`이다.
|
||||||
|
- 확인창에서 `취소`를 선택하면 API 호출 횟수는 계속 `0`이고 버튼 상태가 유지되며, `팔로우 하기`를 선택하면 기존 API가 정확히 한 번 호출된다.
|
||||||
|
- 모두 팔로우 확인창의 제목·설명·확인 버튼 문구가 한국어·영어·일본어 string resource로 제공된다.
|
||||||
- 사업자 정보는 기본 3줄 말줄임, 더보기 후 전체 표시, 접기 후 3줄 복귀가 동작한다.
|
- 사업자 정보는 기본 3줄 말줄임, 더보기 후 전체 표시, 접기 후 3줄 복귀가 동작한다.
|
||||||
- 기존 v2 widget 재사용 범위가 구현 diff에서 확인된다.
|
- 기존 v2 widget 재사용 범위가 구현 diff에서 확인된다.
|
||||||
- 관련 unit test, resource merge, `./gradlew :app:testDebugUnitTest` 또는 구현 범위에 맞는 단일 테스트가 성공한다.
|
- 관련 unit test, resource merge, `./gradlew :app:testDebugUnitTest` 또는 구현 범위에 맞는 단일 테스트가 성공한다.
|
||||||
@@ -372,11 +436,18 @@ HomeRecommendation 화면
|
|||||||
- 인증 token 처리 방식은 아직 제공되지 않았다. 구현 전 기존 `HomeApi` 확장 지점과 공통 인증 interceptor 사용 여부를 확인해야 한다.
|
- 인증 token 처리 방식은 아직 제공되지 않았다. 구현 전 기존 `HomeApi` 확장 지점과 공통 인증 interceptor 사용 여부를 확인해야 한다.
|
||||||
- `cheerCreators`의 profile grid는 기존 profile widget이 저장소에 있는지 추가 확인 후 재사용/신규를 결정한다. `genreCreators`는 응답/매핑 계약만 유지하고 추천 탭 UI에는 표시하지 않는다.
|
- `cheerCreators`의 profile grid는 기존 profile widget이 저장소에 있는지 추가 확인 후 재사용/신규를 결정한다. `genreCreators`는 응답/매핑 계약만 유지하고 추천 탭 UI에는 표시하지 않는다.
|
||||||
- 배너 이동 정책은 `type` 없이 `eventItem`, `creatorId`, `seriesId`, `link` 값 기준으로 처리하는 것으로 확정됐다. `link`가 내부 딥링크이면 기존 딥링크 실행 정책을 사용한다.
|
- 배너 이동 정책은 `type` 없이 `eventItem`, `creatorId`, `seriesId`, `link` 값 기준으로 처리하는 것으로 확정됐다. `link`가 내부 딥링크이면 기존 딥링크 실행 정책을 사용한다.
|
||||||
- `activityAt`, `releaseDate`, `createdAt`, `beginDateTime`의 표시 포맷은 기존 시간 formatter 재사용 여부를 구현 계획에서 확인한다.
|
- `activityAt`, `createdAt`, `beginDateTime`의 표시 포맷은 기존 시간 formatter 재사용 여부를 구현 계획에서 확인한다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. 검증 기록
|
## 12. 검증 기록
|
||||||
|
- 2026-07-31: 현재 진행 중인 라이브 `전체` 항목 상시 표시 요구사항을 TDD로 구현했다. 라이브 1개 입력의 adapter item 수가 기대 2, 실제 1로 실패하는 RED를 확인한 뒤 `HomeLiveAdapter`가 비어 있지 않은 입력에서 `전체` 항목을 추가하도록 최소 수정했다. focused `HomeMainFragmentLayoutTest` 74개와 전체 162 suites·1,199 tests, debug Kotlin compile, ktlint, diff 검증이 성공했으며 0개 숨김·20개 cap·기존 `전체` 디자인과 클릭 경로는 유지했다.
|
||||||
|
- 2026-07-31: 사용자 인터뷰에서 현재 진행 중인 라이브가 0개이면 기존처럼 섹션을 숨기고, 1개 이상이면 최대 20개 라이브 뒤에 `전체` 항목을 항상 표시하는 정책을 확정했다. `HomeLiveAdapter.submitItems()`의 기존 `items.size > MAX_VISIBLE_LIVE_COUNT` 조건과 `HomeMainFragmentLayoutTest`의 21개 초과 회귀 테스트를 확인했으며, 기존 디자인·클릭 목적지·로그인 가드·API 계약은 변경하지 않는 최소 범위를 PRD에 반영했다.
|
||||||
|
- 2026-07-31: PRD와 계획/TASK 보완 후 신규 범위의 placeholder·모순·요구사항 추적성을 자체 검토했다. `git status --short`로 변경 파일이 두 문서뿐임을 확인했고, placeholder 검색과 `git diff --check`는 출력 없이 성공했다. `./gradlew tasks --all`은 최초 sandbox 실행에서 사용자 Gradle cache lock 접근 제한으로 실패했으나 승인된 동일 명령 재실행은 `BUILD SUCCESSFUL`로 완료되어 계획에 기재한 Gradle task 이름의 유효성을 확인했다. production·test·resource 파일은 변경하지 않았다.
|
||||||
|
- 2026-07-31: `deep-interview` 결과, 모든 `모두 팔로우` 동작에 API 호출 전 확인 절차를 적용하는 공통 정책을 확정했다. 현재 구현 대상은 실제 노출 중인 `최근 응원이 많은 크리에이터` 섹션으로 제한하고, 숨겨진 `genreCreators` UI는 다시 노출하지 않는다. 저장소의 `HomeMainFragment`가 `cheerCreators` 전체 id를 `HomeRecommendationViewModel.followCreators()`에 즉시 전달하는 현재 흐름과, 취소·확인 2버튼 및 중복 클릭 방지를 지원하는 기존 `V2ModalDialog`를 확인했다. 확인창은 제목 `모두 팔로우`, 설명 `목록의 크리에이터를 모두 팔로우하시겠어요?`, 확인 `팔로우 하기`, 취소 `취소`로 확정하고 한국어·영어·일본어 resource를 계획했다. 이번 요청은 PRD와 계획/TASK 보완까지만 수행하며 구현 코드는 변경하지 않는다.
|
||||||
|
- 2026-07-31: `REV-P7-001` 후속으로 유료 미구매 인기 커뮤니티의 원본 이미지 미로드 보안 결정을 PRD에 재정렬했다. commit `2d58a876`에서 client-side blur 원본 로드가 재도입된 회귀를 복구해, 무료·구매 완료 이미지는 기존대로 로드하고 유료 미구매는 원본 `imageUrl` 요청 없이 lock overlay와 가격만 유지한다.
|
||||||
|
- 2026-07-31: R7.1 RED/GREEN 검증으로 `HomeMainFragmentLayoutTest.home popular community adapter does not load original image for locked paid post`를 먼저 추가해 focused test가 `HomeMainFragmentLayoutTest.kt:1032` assertion 실패로 RED가 됨을 확인했다. `HomePopularCommunityAdapter` 최소 수정 후 동일 focused test는 BUILD SUCCESSFUL로 GREEN 전환됐고, 후속으로 `HomeMainFragmentLayoutTest` 전체, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`가 모두 성공했다.
|
||||||
|
- 2026-07-30: 사용자 인터뷰에서 `HomeActiveCreatorItem.creatorId`는 서버가 항상 양수로 제공하는 필수 `Long` 필드로 확정했다. 현재 `CreatorActivityType.Live` route가 명시적으로 `null`을 반환해 터치 액션이 종료되는 것을 코드로 확인했으며, `targetId != null`이면 기존 `LiveActionCoordinator.enterLiveRoom(targetId)`, `targetId == null && creatorId > 0L`이면 기존 `CreatorActionCommand.Profile(creatorId)`를 사용하도록 요구사항을 확정했다. 서버 버그 등으로 `creatorId <= 0L`이면 이동하지 않는 방어 검증을 포함한다. 공통 라이브 진입을 재사용해 비로그인·본인인증·성인 콘텐츠 보기 설정 정책을 그대로 적용하고, 이번 범위에서는 해당 공통 정책 자체를 변경하지 않는다. PRD 변경 후 placeholder·모순·모호성·범위를 자체 검토하고 `git diff --check`와 `./gradlew tasks --all`을 실행했으며 모두 성공했다.
|
||||||
- 2026-07-10: 사용자 요청에 따라 `방금 활동한 크리에이터` 섹션의 `activityType == COMMUNITY` 클릭과 `인기 커뮤니티` 섹션 item 클릭을 커뮤니티 전체 목록이 아니라 `CreatorChannelCommunityDetailActivity`로 이동하도록 요구사항과 구현을 반영했다. 최근 활동 COMMUNITY의 `targetId`는 커뮤니티 게시글 `postId`로 해석하고, 인기 커뮤니티는 `HomePopularCommunityPostItem.postId`를 상세 화면 식별 값으로 사용한다.
|
- 2026-07-10: 사용자 요청에 따라 `방금 활동한 크리에이터` 섹션의 `activityType == COMMUNITY` 클릭과 `인기 커뮤니티` 섹션 item 클릭을 커뮤니티 전체 목록이 아니라 `CreatorChannelCommunityDetailActivity`로 이동하도록 요구사항과 구현을 반영했다. 최근 활동 COMMUNITY의 `targetId`는 커뮤니티 게시글 `postId`로 해석하고, 인기 커뮤니티는 `HomePopularCommunityPostItem.postId`를 상세 화면 식별 값으로 사용한다.
|
||||||
- 2026-06-29: 사용자 추가 요청에 따라 `최근 데뷔한 크리에이터` item을 기존 `205dp x 259dp` 비율로 축소한 `185dp x 234dp`로 표시하고, 이미지 단독이 아니라 item root 전체에 `14dp` rounded clipping을 적용해야 한다는 요구사항을 PRD에 반영했다. Figma `24:5534`, `24:5537` design context와 screenshot을 확인했다.
|
- 2026-06-29: 사용자 추가 요청에 따라 `최근 데뷔한 크리에이터` item을 기존 `205dp x 259dp` 비율로 축소한 `185dp x 234dp`로 표시하고, 이미지 단독이 아니라 item root 전체에 `14dp` rounded clipping을 적용해야 한다는 요구사항을 PRD에 반영했다. Figma `24:5534`, `24:5537` design context와 screenshot을 확인했다.
|
||||||
- 2026-06-23: 기존 `CreatorChannelFanTalkFragmentLayoutTest.kt:78` ktlint 줄바꿈 위반을 동작 변경 없이 수정한 뒤 `./gradlew :app:testDebugUnitTest`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:ktlintCheck`가 모두 BUILD SUCCESSFUL임을 재확인했다. `adb devices` 결과 연결된 Android 기기가 없어 실제 기기 수동 확인은 수행하지 못했고, 해당 수동 확인은 환경 차단으로 계획 문서에 남겼다.
|
- 2026-06-23: 기존 `CreatorChannelFanTalkFragmentLayoutTest.kt:78` ktlint 줄바꿈 위반을 동작 변경 없이 수정한 뒤 `./gradlew :app:testDebugUnitTest`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:ktlintCheck`가 모두 BUILD SUCCESSFUL임을 재확인했다. `adb devices` 결과 연결된 Android 기기가 없어 실제 기기 수동 확인은 수행하지 못했고, 해당 수동 확인은 환경 차단으로 계획 문서에 남겼다.
|
||||||
@@ -431,3 +502,29 @@ HomeRecommendation 화면
|
|||||||
- `최근 데뷔한 크리에이터` 섹션 타이틀 우측 chevron을 표시하지 않는다.
|
- `최근 데뷔한 크리에이터` 섹션 타이틀 우측 chevron을 표시하지 않는다.
|
||||||
- `크리에이터와 이야기를 나눠요!` 섹션 타이틀 우측 chevron을 표시하지 않는다.
|
- `크리에이터와 이야기를 나눠요!` 섹션 타이틀 우측 chevron을 표시하지 않는다.
|
||||||
- 섹션 목록 표시, item 터치 동작, 다른 섹션의 chevron 정책은 변경하지 않는다.
|
- 섹션 목록 표시, item 터치 동작, 다른 섹션의 chevron 정책은 변경하지 않는다.
|
||||||
|
|
||||||
|
### 2026-07-30 변경: 최근 활동 LIVE 라우팅
|
||||||
|
- 홈 추천 API의 `HomeActiveCreatorItem`에 서버가 항상 양수로 제공하는 필수 `creatorId: Long`을 추가한다.
|
||||||
|
- `activityType == LIVE && targetId > 0L`이면 `targetId`를 `roomId`로 사용해 기존 공통 라이브 진입 흐름을 실행한다.
|
||||||
|
- `activityType == LIVE && targetId == null`이면 `creatorId > 0L`을 확인한 뒤 크리에이터 채널로 이동한다.
|
||||||
|
- 서버 버그 등으로 `creatorId <= 0L`이면 route를 만들지 않고 이동하지 않는다.
|
||||||
|
- 공통 라이브 진입 흐름을 재사용해 비로그인, 본인인증 필요, 성인 콘텐츠 보기 설정 필요 상태를 기존 접근 정책으로 처리한다.
|
||||||
|
- 다른 activity type의 기존 라우팅과 공통 접근 정책 자체는 변경하지 않는다.
|
||||||
|
|
||||||
|
### 2026-07-31 변경: 모두 팔로우 확인 절차
|
||||||
|
- 여러 크리에이터를 한 번에 팔로우하는 모든 `모두 팔로우` 동작은 API 호출 전에 기존 `V2ModalDialog` 확인창을 표시한다.
|
||||||
|
- 공통 정책의 현재 구현 대상은 실제 노출 중인 `최근 응원이 많은 크리에이터` 섹션이며, 숨겨진 `genreCreators` 섹션은 구현 범위에 포함하지 않는다.
|
||||||
|
- 최초 버튼 터치와 취소에서는 API를 호출하지 않고, 확인창의 `팔로우 하기`를 터치한 경우에만 기존 `creatorIds`와 API 계약으로 요청한다.
|
||||||
|
- Dialog 문구는 한국어·영어·일본어 string resource로 제공하며, API·로딩·성공·실패·완료 버튼 상태 계약은 변경하지 않는다.
|
||||||
|
- 이번 요청에서는 PRD와 구현 계획/TASK 문서만 보완하고 production·test·resource 구현은 진행하지 않는다.
|
||||||
|
|
||||||
|
### 2026-07-31 변경: 인기 커뮤니티 유료 미구매 원본 이미지 미로드 복구
|
||||||
|
- 유료 미구매 인기 커뮤니티 item은 원본 `imageUrl`을 image loader에 전달하지 않는다.
|
||||||
|
- 기존 Coil 요청과 drawable을 비우는 방식으로 원본 네트워크 요청/cache 노출을 막고, `FeedCommunityView.bind()`의 lock overlay와 가격 capsule 표시는 유지한다.
|
||||||
|
- 무료 또는 구매 완료 item의 이미지 로딩, 커뮤니티 상세 이동, 구매 권한 판정, API/DTO 계약은 변경하지 않는다.
|
||||||
|
|
||||||
|
### 2026-07-31 변경: 현재 진행 중인 라이브 `전체` 항목 상시 표시
|
||||||
|
- 현재 진행 중인 라이브가 0개이면 기존처럼 라이브 섹션 전체를 숨긴다.
|
||||||
|
- 현재 진행 중인 라이브가 1개 이상이면 라이브 개수와 관계없이 목록 마지막에 `전체` 항목을 표시한다.
|
||||||
|
- 라이브는 기존처럼 최대 20개까지 표시하므로 21개 이상일 때도 `라이브 20개 + 전체` 구성을 유지한다.
|
||||||
|
- `전체` 항목의 디자인·클릭 목적지·로그인 가드와 라이브 item의 입장 흐름은 변경하지 않는다.
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Phase 1 기존 구조·구현 경계 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 1 / 기존 구조 확인과 구현 경계 고정 |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` Phase 1 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: Phase 1에서 확정한 v2 화면·공용 위젯·DI 경계가 후속 구현에서도 유지됐는지 확인한다.
|
||||||
|
- 포함: `v2/main`, `v2/widget`, `AppDI.kt`, 관련 레이아웃과 현재 파일 배치.
|
||||||
|
- 제외: 후속 Phase의 상세 UI 동작과 실기기 시각 검증.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
`sample-review.md`의 심각도와 상태 기준을 적용하고, 현재 경로가 후속 리팩터링으로 이동한 경우에는 기능·참조가 함께 보존됐는지로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 계획: Phase 1의 기존 `HomeMainFragment`, 재사용 widget, DI 경계 확인 항목.
|
||||||
|
- 코드: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`, `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`.
|
||||||
|
- 검증: v2 home focused test, resource merge, compile, ktlint 성공. 최신 전체 1,198 tests 중 실패 0·error 0.
|
||||||
|
- 환경: macOS, Android Gradle 프로젝트, 연결 기기 기반 수동 확인 제외.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `REV-P1-001` | Low | 수정 완료 | 활성 문서의 HomeMainFragment 경로가 현재 파일과 다르다 | `Task R1.1` | `P1-R1` |
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
### REV-P1-001 — 활성 문서의 HomeMainFragment 경로가 현재 파일과 다르다
|
||||||
|
|
||||||
|
- **심각도:** Low
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **관련 요구사항:** v2 홈 구현 경계·파일 추적성
|
||||||
|
- **소유 Task:** `Task R1.1` / `P1-R1`
|
||||||
|
|
||||||
|
**관찰 내용**
|
||||||
|
|
||||||
|
현재 Fragment는 `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`에 있고 계획의 초기 `app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt`는 존재하지 않는다. 과거 Task 이력은 보존 대상이지만 PRD Technical Constraints도 과거 패키지를 현재 제약으로 안내하고 있어 활성 경로 안내는 정정이 필요하다.
|
||||||
|
|
||||||
|
**근거·재현**
|
||||||
|
|
||||||
|
1. `test -f app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`는 성공한다.
|
||||||
|
2. `test -f app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt`는 실패한다.
|
||||||
|
3. `prd.md:387`과 `plan-task.md` Phase 1·5의 활성 안내/경로가 과거 위치를 사용한다.
|
||||||
|
|
||||||
|
**영향**
|
||||||
|
|
||||||
|
런타임 회귀는 없지만 후속 작업이 미존재 파일을 근거로 범위를 잡거나 리뷰 근거 경로를 잘못 인용할 수 있다.
|
||||||
|
|
||||||
|
**권장 조치**
|
||||||
|
|
||||||
|
기존 완료 이력은 삭제하지 않고 PRD와 Phase 1에 현재 canonical 경로를 명시한다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-07-31 — 파일 존재 검사와 문서 검색으로 활성 경로 불일치를 확정했다.
|
||||||
|
- 2026-07-31 — `P1-R1` 문서 정정과 경로 검색, `git diff --check` 성공으로 수정 완료 판정했다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
`plan-task.md` Phase 1에 `Task R1.1` / `P1-R1`을 추가했다.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | 구조·의존성 경계 대조 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | `REV-P1-001` 수정 완료 |
|
||||||
|
| 확정 항목 plan 반영 | 충족 | `Task R1.1` / `P1-R1` 추가 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | `plan-task.md` Verification Log 연결 |
|
||||||
|
|
||||||
|
**최종 결론:** 승인
|
||||||
|
|
||||||
|
**남은 항목:** 없음
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
- 2026-07-31 — `Task R1.1` 실행으로 PRD Technical Constraints에 현재 canonical 경로 `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`와 package `kr.co.vividnext.sodalive.v2.main.home.HomeMainFragment`를 명시하고 과거 경로 미존재를 덧붙였다. `test -f "app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"`와 `test ! -f "app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt"`로 파일 상태를 확인했고, `rg` 경로 검색 및 `git diff --check`는 성공했다. 남은 항목은 없다.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Phase 10 배너 라우팅 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 10 / 배너 이동 정책 연결 |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` Phase 10 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: event→creator→series→link 우선순위와 web/internal link 안전 처리를 확인한다.
|
||||||
|
- 포함: 배너 route model/intent 생성, Fragment callback, 관련 tests.
|
||||||
|
- 제외: 외부 브라우저·딥링크 목적 앱의 실제 실행 결과.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
우선순위 보존, blank/malformed/허용하지 않은 scheme no-op, 기존 extra 계약과 로그인 정책 유지로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 코드: `HomeRecommendationUiModels.kt` 배너 route, `HomeMainFragment` 배너 callback.
|
||||||
|
- 테스트: `HomeMainFragmentLayoutTest` 배너 route/intent 계약.
|
||||||
|
- 검증: home/modal focused test·compile·ktlint 성공, 최신 전체 1,198 tests 중 실패 0·error 0.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
없음. 여러 payload 우선순위와 링크 분류 guard가 현재 테스트로 고정되어 있다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | route·intent·callback 검토 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | 확정 후보 없음 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | focused 검증 성공 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음
|
||||||
|
|
||||||
|
**남은 항목:** 없음. Phase 12의 배너 목적지별 클릭 이동은 사용자 확인으로 완료됐다.
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
수정 항목 없음.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Phase 11 크리에이터 item 라우팅 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 11 / 최근 활동·AI 캐릭터 터치 액션 |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` Phase 11 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: activity type별 route와 AI 캐릭터의 creator channel 이동이 유효 ID만 사용하도록 구성됐는지 확인한다.
|
||||||
|
- 포함: recent activity route, AI creatorId mapper/route, adapter callback과 tests.
|
||||||
|
- 제외: COMMUNITY와 LIVE의 후속 목적지 변경은 Phase 13·16에서 별도 판정.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
표시 ID와 route ID 분리, invalid ID no-op, 목적지 extra, 후속 승인 변경 반영으로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 코드: `HomeRecommendationUiModels.kt`, `HomeAiCharacterAdapter.kt`, `HomeRecentActivityCreatorAdapter.kt`, `HomeMainFragment.kt`.
|
||||||
|
- 테스트: `HomeMainFragmentLayoutTest`의 recent activity/AI mapper·route·intent 계약.
|
||||||
|
- 검증: home/modal focused test 성공, 최신 전체 1,198 tests 중 실패 0·error 0.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
없음. 초기 COMMUNITY/LIVE 동작은 Phase 13·16에서 승인된 목적지로 대체됐으며 AI characterId 표시와 creatorId 이동 계약은 분리되어 있다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | mapper·route·intent 검토 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | 후속 Phase 변경 분리 완료 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | 자동 검증 성공 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음
|
||||||
|
|
||||||
|
**남은 항목:** 없음. Phase 12의 최근 활동·AI 캐릭터 이동 항목은 사용자 확인으로 완료됐다.
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
수정 항목 없음.
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# Phase 12 최종 검증·문서 기록 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | 첫 번째 Phase 12 / Task 12.1~12.3 |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` 첫 번째 Phase 12 |
|
||||||
|
| 리뷰 상태 | 수정·수동 검증 완료, 최신 전체 1,198 tests 성공, 과거 test isolation 관찰 보류 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: 자동 검증 완료 기록과 수동 확인 blocker가 실제 상태와 일치하는지 확인한다.
|
||||||
|
- 포함: Gradle test/compile/resource/ktlint, 문서 Verification Log, Task 12.2 상태.
|
||||||
|
- 제외: Codex가 직접 수행하지 않은 실제 UI 조작. Task 12.2 수동 항목은 사용자 확인 결과를 증거로 판정한다.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
실행 결과의 재현 가능성, 실패 원인 분리, 미완료 수동 항목의 명시와 기존 기록 보존으로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 계획: Task 12.1~12.3 완료. Task 12.2는 2026-07-31 사용자 수동 확인 결과를 누적해 완료 상태다.
|
||||||
|
- 실행: home/modal focused test·resource merge·compile·ktlint 성공. 최신 `./gradlew :app:testDebugUnitTest --no-parallel --console=plain`은 162 suites, 1,198 tests 중 실패 0·error 0으로 성공했다. 과거 전체 suite 2회의 `CreatorChannelHomeViewModelTest` 2건/1건 실패와 해당 class 격리 19 tests 성공 기록은 `REV-P12-003` 관찰 이력으로 보존한다.
|
||||||
|
- 수동 검증: Codex 직접 기기 조작은 홈 추천 화면 접근 제약으로 완료하지 못했지만, 사용자가 Task 12.2의 홈 추천 화면 항목이 모두 동작함을 확인했다.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `REV-P12-001` | Low | 수정 완료 | Task 12.2의 기기 미연결 blocker가 현재 환경과 다르다 | `Task R12.1` | `P12-R1` |
|
||||||
|
| `REV-P12-002` | Low | 수정 완료 | Task 12.2가 제거된 first audio 표시 계약을 요구한다 | `Task R12.2` | `P12-R2` |
|
||||||
|
| `REV-P12-003` | Low | 보류 | 전체 unit suite에서 공유 `can` 상태 assertion이 비결정적으로 실패한다 | 별도 creator channel/preferences 테스트 격리 작업 | 범위 승인 후 정의 |
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
### REV-P12-001 — Task 12.2의 기기 미연결 blocker가 현재 환경과 다르다
|
||||||
|
|
||||||
|
- **심각도:** Low
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **관련 요구사항:** Task 12.2 수동 확인·리뷰 검증 상태 정확성
|
||||||
|
- **소유 Task:** `Task R12.1` / `P12-R1`
|
||||||
|
|
||||||
|
**관찰 내용·근거**
|
||||||
|
|
||||||
|
`plan-task.md` Task 12.2는 `[blocked: 연결된 Android 기기 없음]`으로 남아 있지만 2026-07-31 재검사한 `adb devices`는 `device` 상태 기기 1대를 반환했다. 수동 항목은 아직 미실행이므로 Task 12.2를 완료로 판정할 수는 없으나 blocker 사유와 재개 조건은 갱신해야 한다.
|
||||||
|
|
||||||
|
**영향·권장 조치**
|
||||||
|
|
||||||
|
기능 결함은 확인되지 않았으나 현재 실행 가능한 검증을 외부 조건 대기로 남겨 Phase 종료 판정을 지연시킨다. 기기 연결 상태를 다시 기록하고 Task 12.2 수동 확인을 재개한다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-07-31 — escalated `adb devices` 결과로 기존 미연결 blocker가 현재 상태와 다름을 확정했다.
|
||||||
|
- 2026-07-31 — `P12-R1` 실행으로 기기 연결, 설치, 런처 실행, 스크린샷을 재확인했고 blocker를 `SplashActivity` 검은 화면으로 갱신해 당시 상태를 정정했다.
|
||||||
|
- 2026-07-31 — 이후 사용자 수동 확인으로 Task 12.2 홈 추천 화면 항목이 모두 동작함을 확인해 현재 blocker는 해소됐다.
|
||||||
|
|
||||||
|
전체 테스트 최초 실패는 이번 Phase 대상이 아닌 `CreatorChannelHomeViewModelTest`의 기존 비동기 preference 경쟁 조건으로 좁혀졌고 격리·전체 재실행이 성공해 현재 변경 회귀로 판정하지 않았다.
|
||||||
|
|
||||||
|
### REV-P12-002 — Task 12.2가 제거된 first audio 표시 계약을 요구한다
|
||||||
|
|
||||||
|
- **심각도:** Low
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **관련 요구사항:** `P12B-T1`·`P12B-T2` first audio production/UI/resource 제거
|
||||||
|
- **소유 Task:** `Task R12.2` / `P12-R2`
|
||||||
|
|
||||||
|
**관찰 내용·근거**
|
||||||
|
|
||||||
|
두 번째 Phase 12(`P12B`)에서 first audio 섹션·DTO·adapter·resource를 제거했지만, 아직 미완료인 첫 번째 Phase 12의 Task 12.2 수동 확인 목록은 `첫 오디오 태그 조건`을 현재 표시 계약으로 유지했다. `rg`로 v2 home production/resource에 제거 대상 참조가 없고 해당 수동 항목만 활성 계약으로 남은 상태를 확인했다.
|
||||||
|
|
||||||
|
**영향·권장 조치**
|
||||||
|
|
||||||
|
실행하려는 수동 검증이 이미 제거된 UI를 요구해 항목 완료가 불가능하고 Phase 12B 제거 계약과도 모순된다. 과거 이력은 보존하고 Task 12.2의 활성 항목만 first audio 섹션과 관련 태그 미노출 확인으로 정정한다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-07-31 — `plan-task.md` Task 12.2의 활성 항목과 Phase 12B 제거 Task·production 검색을 대조해 문서 정합성 문제로 확정했다.
|
||||||
|
- 2026-07-31 — `P12-R2`로 Task 12.2 활성 항목을 미노출 검증으로 정정하고 production 금지 참조 검색과 `git diff --check`를 통과해 수정 완료로 판정했다.
|
||||||
|
|
||||||
|
### REV-P12-003 — 전체 unit suite에서 공유 `can` 상태 assertion이 비결정적으로 실패한다
|
||||||
|
|
||||||
|
- **심각도:** Low
|
||||||
|
- **상태:** 보류
|
||||||
|
- **관련 요구사항:** Task 12.1 전체 `:app:testDebugUnitTest` 성공 검증
|
||||||
|
- **소유 Task:** 현재 계획 범위 밖 `CreatorChannelHomeViewModelTest` / `SharedPreferenceManager` 테스트 격리
|
||||||
|
|
||||||
|
**관찰 내용**
|
||||||
|
|
||||||
|
`./gradlew :app:testDebugUnitTest --no-parallel`을 두 차례 실행했을 때 첫 실행은 1,196개 중 `CreatorChannelHomeViewModelTest` 2건, 두 번째 실행은 동일 class 1건이 실패했다. 실패 테스트는 채널 후원 성공 후 `SharedPreferenceManager.can`이 `200 -> 150`, `30 -> 0`으로 차감되어야 하지만 각각 `200`, `30`으로 남은 assertion이다. 해당 class 19개를 격리 재실행하면 모두 통과했다.
|
||||||
|
|
||||||
|
**근거**
|
||||||
|
|
||||||
|
- 현재 diff에 `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel`, `CreatorChannelHomeViewModelTest.kt`, `SharedPreferenceManager.kt`, `AppPreferencesDataStoreProvider.kt` 변경은 없다.
|
||||||
|
- `채널 후원 성공은 기존 후원 API를 호출하고 홈을 다시 로드한다` 테스트는 repository success callback 후 두 번째 `getHome()` 검증까지 통과했지만, 직후 `can` 값은 설정 전 값으로 관찰됐다.
|
||||||
|
- `SharedPreferenceManager` reset은 observer job을 cancel하지만 전역 `appScope(Dispatchers.IO)`와 DataStore write/collect를 사용하고, 전체 suite의 여러 test class가 같은 저장소를 init/reset한다. 이 구조는 격리 성공·전체 실패 패턴과 일치하지만, 결정적 재현 test와 수정 범위는 이번 홈 추천 리뷰에서 추가하지 않았다.
|
||||||
|
|
||||||
|
**영향·권장 조치**
|
||||||
|
|
||||||
|
홈 추천 focused 회귀는 green이지만 Task 12.1의 전체 suite를 현재 시점에서 안정적인 green gate로 사용할 수 없다. `CreatorChannelHomeViewModelTest`와 공유 preferences 테스트 격리를 별도 PRD/계획 범위로 승인하면, 순서와 관계없이 재현되는 실패 test를 먼저 고정한 뒤 최소 수정한다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-07-31 — 전체 suite 2회 실패, 실패 class 격리 성공, 변경 범위 대조로 홈 추천 회귀와 분리했다.
|
||||||
|
- 2026-07-31 — 수정은 현재 문서의 Phase 범위를 넘고 공유 preferences 테스트 구조를 변경할 수 있어 보류했다. 사용자가 별도 범위를 승인하면 재현 test부터 재개한다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
`plan-task.md` 첫 번째 Phase 12의 `Task R12.1` / `P12-R1`은 기기 상태·blocker 정정과 이후 사용자 수동 확인까지, `Task R12.2` / `P12-R2`는 first audio 제거 후 수동 확인 기준 동기화까지 완료했다. `REV-P12-003`은 현재 Phase 범위 밖이므로 plan Task로 전환하지 않고, 비결정적 실패가 다시 재현될 때 별도 creator channel/preferences 테스트 격리 범위로 재개한다.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | 자동·문서·수동 상태 확인 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | `REV-P12-001`, `REV-P12-002` 수정 완료, `REV-P12-003` 보류 조건 기록 |
|
||||||
|
| 확정 항목 plan 반영 | 충족 | `Task R12.1` / `P12-R1`, `Task R12.2` / `P12-R2` 완료 |
|
||||||
|
| 보류 항목의 담당·재개 조건 기록 | 충족 | `REV-P12-003`은 비결정적 실패 재현 시 creator channel/preferences 테스트 격리 범위로 재개 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | focused 성공·최신 전체 1,198 tests 성공·과거 격리 관찰을 Verification Log에 누적 |
|
||||||
|
|
||||||
|
**최종 결론:** 자동·사용자 수동 검증 승인, 과거 test isolation 관찰 보류
|
||||||
|
|
||||||
|
**남은 항목:** 현재 Phase 수정 항목은 없음. `REV-P12-003`은 전체 suite에서 동일 실패가 다시 재현될 때 별도 테스트 격리 범위로 재개한다.
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
- 2026-07-31 — `P7-R3` 실행형 회귀 테스트 보강 후 전체 `:app:testDebugUnitTest`를 재실행했고 162 suites, 1,198 tests, 실패 0, error 0으로 BUILD SUCCESSFUL을 확인했다. `REV-P12-003`의 과거 비결정적 관찰은 재현되지 않았으며 기존 재개 조건을 유지한다.
|
||||||
|
- 2026-07-31 — 현재 working tree에서 home/modal focused test·`:app:mergeDebugResources`·`:app:compileDebugKotlin`·`:app:ktlintCheck`를 함께 실행해 BUILD SUCCESSFUL을 확인했다. 이어 `./gradlew :app:testDebugUnitTest --no-parallel --console=plain`을 실행했고, test result XML 161 suites의 1,197 tests에서 실패 0·error 0으로 BUILD SUCCESSFUL을 확인했다. 이 결과는 `REV-P12-003`의 과거 비결정적 관찰을 삭제하지 않으며, 동일 실패 재현 시 재개 조건을 유지한다.
|
||||||
|
- 2026-07-31 — 사용자 수동 확인 결과 Task 12.2의 추천 tab·스크롤·섹션·라우팅·인기 커뮤니티 상태·사업자 정보 항목이 모두 동작함을 확인해 이전 화면 접근 blocker를 해소된 이력으로 갱신했다.
|
||||||
|
- 2026-07-31 — Phase별 리뷰 보고서와 계획 문서 반영 후 `./gradlew tasks --all --console=plain`을 실행해 BUILD SUCCESSFUL을 확인했다. `reviews/`의 Phase 1~17 및 별도 이력 Phase 12B 보고서 존재 여부, 확정 지적과 신규 회귀 Task의 연결, `git diff --check`를 다시 확인했다.
|
||||||
|
- 2026-07-31 — `Task R12.1` 실행으로 `adb devices`에서 `2cec640c34017ece device`를 확인해 기존 `연결된 Android 기기 없음` blocker가 현재 상태와 다름을 재확인했다. `./gradlew :app:installDebug`는 SM-G960N 기기에 설치 성공했고, `adb shell monkey -p kr.co.vividnext.sodalive.debug -c android.intent.category.LAUNCHER 1`도 성공했다. 그러나 `adb shell dumpsys window`는 foreground가 `kr.co.vividnext.sodalive.debug/kr.co.vividnext.sodalive.splash.SplashActivity`임을 반환했고, `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sodalive_r12_manual.png`와 retry `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sodalive_r12_manual_retry.png`는 모두 검은 화면으로 판독됐다. Task 12.2 수동 항목은 홈 추천 화면 접근 불가로 계속 blocked다.
|
||||||
|
- 2026-07-31 — 수동 Gate 재검증에서 `adb devices`는 `2cec640c34017ece device`를 반환했고 `./gradlew :app:installDebug`, `adb shell monkey -p kr.co.vividnext.sodalive.debug -c android.intent.category.LAUNCHER 1`는 성공했다. `adb shell dumpsys window`는 foreground가 `kr.co.vividnext.sodalive.debug/kr.co.vividnext.sodalive.v2.main.MainV2Activity`임을 반환했지만, `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sodalive_manual_gate_20260731/screen.png`는 검은 배경의 splash/voice on 화면으로 판독됐다. Task 12.2 수동 항목은 홈 추천 화면 접근 불가로 계속 blocked다.
|
||||||
|
- 2026-07-31 — `Task R12.2` 실행으로 Task 12.2의 `첫 오디오 태그 조건`을 `Phase 12B에서 제거된 첫 오디오 섹션과 관련 태그 미노출`로 정정했다. 과거 검증 이력은 보존했고 v2 home production/resource의 제거 대상 참조가 없음을 `rg`로 확인했으며 `git diff --check`는 출력 없이 성공했다. 수동 확인은 기존 `SplashActivity` 검은 화면 blocker로 인해 계속 미완료다.
|
||||||
|
- 2026-07-31 — 현재 working tree에서 `./gradlew :app:testDebugUnitTest --no-parallel`을 두 차례 실행했고 1,196개 중 `CreatorChannelHomeViewModelTest` 2건/1건 실패로 모두 `BUILD FAILED`를 확인했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModelTest" --no-parallel`은 19개 모두 통과했다. 실패는 현재 diff 범위 밖의 공유 preferences 비동기 상태와 일치해 `REV-P12-003` 보류로 기록했고 해당 코드는 변경하지 않았다.
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# Phase 12B first audio 제거 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | 두 번째 Phase 12 / 역사적 Task 10.1~10.2 |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` 두 번째 Phase 12 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: first audio 제거 완료 여부와 Phase/Task 식별자의 추적 가능성을 확인한다.
|
||||||
|
- 포함: recommendation DTO/model/state/Fragment, content overview, string/layout/adapter 참조와 계획 문서 식별자.
|
||||||
|
- 제외: `NEW_AND_HOT_AUDIO` 등 유지 대상 오디오 기능.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
production 참조 완전 제거, 유지 대상 비회귀, Phase·Task·Goal의 고유 추적 가능성으로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 코드 검색: `app/src/main`에서 `firstAudioContents`, `HomeFirstAudio`, `FIRST_AUDIO_CONTENT`, 제거 string/layout production 참조 확인.
|
||||||
|
- 문서: `plan-task.md`의 두 `Phase 12` 제목과 두 번째 Phase의 `Task 10.1`·`Task 10.2`.
|
||||||
|
- 검증: home/content overview focused 검증 기록, 현재 home/modal focused test 성공 및 최신 전체 1,198 tests 중 실패 0·error 0.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `REV-P12B-001` | Low | 수정 완료 | Phase와 Task 식별자가 다른 완료 항목과 중복된다 | `Task R12B.1` | `P12B-R1` |
|
||||||
|
| `REV-P12B-002` | Low | 수정 완료 | 제거된 first audio가 PRD 활성 계약에 남아 있다 | `Task R12B.2` | `P12B-R2` |
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
### REV-P12B-001 — Phase와 Task 식별자가 다른 완료 항목과 중복된다
|
||||||
|
|
||||||
|
- **심각도:** Low
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **관련 요구사항:** first audio 제거 자체에는 위반 없음
|
||||||
|
- **관련 계약:** `work-plan-docs.md` Goal/Task 추적성 규칙
|
||||||
|
- **소유 Task:** `Task R12B.1` / `P12B-R1`
|
||||||
|
|
||||||
|
**관찰 내용**
|
||||||
|
|
||||||
|
문서에 `Phase 12`가 두 번 있고, second Phase 12의 완료 항목은 `Task 10.1`·`Task 10.2`로 기록되어 Phase 10 및 첫 번째 Phase 12와 참조가 충돌한다. 실제 first audio 제거는 완료됐지만 리뷰 보고서와 후속 회귀 Task에서 소유 Phase를 고유하게 지정할 수 없다.
|
||||||
|
|
||||||
|
**근거**
|
||||||
|
|
||||||
|
- 문서: `plan-task.md`의 `Phase 12: 최종 검증과 문서 기록`과 `Phase 12: 처음부터 함께 성장! 섹션 제거` 제목.
|
||||||
|
- 문서: 두 번째 Phase 12 안의 `Task 10.1`·`Task 10.2`.
|
||||||
|
- 코드: 제거 대상 production 참조가 없어 기능 결함은 확인되지 않았다.
|
||||||
|
|
||||||
|
**재현 또는 검증 절차**
|
||||||
|
|
||||||
|
1. `rg -n '^### Phase 12|Task 10\\.' plan-task.md`를 실행한다.
|
||||||
|
2. 서로 다른 결과가 같은 Phase/Task 식별자를 공유함을 확인한다.
|
||||||
|
3. first audio production 참조 제거 여부를 별도 검색해 기능 완료와 문서 추적성 문제를 분리한다.
|
||||||
|
|
||||||
|
**영향**
|
||||||
|
|
||||||
|
런타임 영향은 없지만 향후 리뷰, 회귀 수정, goal 완료 증거가 잘못된 Phase에 연결될 수 있다.
|
||||||
|
|
||||||
|
**권장 조치**
|
||||||
|
|
||||||
|
기존 완료 기록을 재작성하지 않고 두 번째 Phase 12와 하위 Task에 `P12B` 계열 고유 별칭 대응표를 추가하고 이후 참조에 사용한다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-07-31 — 문서 검색과 current tree 대조로 추적성 문제를 확정했다.
|
||||||
|
- 2026-07-31 — `P12B-R1` 별칭 표·이후 참조 규칙 추가와 `rg`·`git diff --check` 성공으로 수정 완료 판정했다.
|
||||||
|
|
||||||
|
### REV-P12B-002 — 제거된 first audio가 PRD 활성 계약에 남아 있다
|
||||||
|
|
||||||
|
- **심각도:** Low
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **관련 요구사항:** 2026-06-29 `firstAudioContents`·`HomeFirstAudioContentItem` 제거
|
||||||
|
- **소유 Task:** `Task R12B.2` / `P12B-R2`
|
||||||
|
|
||||||
|
**관찰 내용**
|
||||||
|
|
||||||
|
production에서 first audio 섹션·adapter·resource는 제거됐지만 PRD의 현재 Problem, 섹션 구성 도식, Metrics, Open Questions에 `첫 오디오 콘텐츠`, `firstAudioContents`, `AudioContentCardView`, `releaseDate` 계약이 아직 남아 있다.
|
||||||
|
|
||||||
|
**근거·재현**
|
||||||
|
|
||||||
|
1. `rg -n "firstAudioContents|HomeFirstAudio|FIRST_AUDIO_CONTENT|AudioContentCardView|releaseDate" prd.md`는 PRD 활성 영역의 잔존 참조를 반환한다.
|
||||||
|
2. 동일 패턴을 `app/src/main`에서 검색하면 제거 대상 production 참조가 없다.
|
||||||
|
3. PRD의 `2026-06-29 변경`은 해당 필드·UI·resource 제거를 명시한다.
|
||||||
|
|
||||||
|
**영향·권장 조치**
|
||||||
|
|
||||||
|
런타임 결함은 없지만 요구사항과 Metrics가 이미 삭제된 기능을 현재 완료 조건으로 제시한다. 과거 이력은 보존하고 PRD 활성 영역만 Phase 12B 계약에 맞게 정정한다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-07-31 — PRD 활성 영역과 production 금지 참조 검색으로 문서 정합성 문제를 확정했다.
|
||||||
|
- 2026-07-31 — `P12B-R2` PRD 활성 계약 정정과 production/resource 참조 검색, `git diff --check` 성공으로 수정 완료 판정했다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
`Task R12B.1` / `P12B-R1`과 `Task R12B.2` / `P12B-R2`는 수정 검증까지 완료했다.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | 제거 코드·문서 식별자 검토 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | `REV-P12B-001`, `REV-P12B-002` 수정 완료 |
|
||||||
|
| 확정 항목 plan 반영 | 충족 | `Task R12B.2` / `P12B-R2` 완료 |
|
||||||
|
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | Verification Log 연결 |
|
||||||
|
|
||||||
|
**최종 결론:** 승인
|
||||||
|
|
||||||
|
**남은 항목:** 없음
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
- 2026-07-31 — `P12B-R1` 실행으로 두 번째 `Phase 12`와 하위 `Task 10.1`·`Task 10.2`에 고유 별칭을 추가했다. 변경 파일은 `plan-task.md`와 이 리뷰 보고서다. 중복 재현은 `rg -n "^(### Phase 12:|## Phase 12:|#### Task 10\.|- \[x\] \*\*Task 10\.)" "docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md"`로 확인했고, 기존 완료 제목과 체크박스는 보존한 채 `P12B`, `P12B-T1`, `P12B-T2` 별칭 표와 이후 참조 규칙만 추가했다. 연결 확인은 `rg -n "P12B|P12B-T1|P12B-T2|P12B-R1|REV-P12B-001" "docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md" "docs/20260601_메인_홈_추천_UI와_API_연동/reviews/phase12-first-audio-removal-review.md"`로 수행했고, `git diff --check`는 출력 없이 성공했다. 남은 항목은 없다.
|
||||||
|
- 2026-07-31 — `P12B-R2` 실행으로 PRD 활성 Problem, 섹션 도식, Metrics, Open Questions에서 제거된 first audio 완료 조건을 정리했다. 2026-06-01 과거 기록과 2026-06-29 제거 기록은 보존했다. `rg -n "firstAudioContents|HomeFirstAudioContentItem|releaseDate|첫 오디오|처음부터 함께 성장" "docs/20260601_메인_홈_추천_UI와_API_연동/prd.md" app/src/main/java/kr/co/vividnext/sodalive/v2/main/home app/src/main/res/values app/src/main/res/layout` 결과는 PRD 과거 기록 및 제거 기록만 반환하고 v2 home production/resource 제거 대상 참조는 반환하지 않았다. `git diff --check`는 출력 없이 성공했다. 남은 항목은 없다.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Phase 13 커뮤니티 상세 라우팅 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 13 / Task 13.1~13.2 |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` Phase 13 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: 최근 활동 COMMUNITY와 인기 커뮤니티 item이 `postId` 기반 상세 화면으로 이동하는지 확인한다.
|
||||||
|
- 포함: route·공통 `CommunityAction` 연결, Fragment login guard, 기존 목록 route 제거와 tests.
|
||||||
|
- 제외: 상세 화면 내부 기능과 API 조회.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
유효 `postId` 전달, invalid 값 no-op, 로그인 가드 순서, 기존 `CreatorCommunityAllActivity` extra 제거로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 코드: `HomeRecommendationUiModels.kt`, `HomeMainFragment.kt` 커뮤니티 callback.
|
||||||
|
- 테스트: `HomeMainFragmentLayoutTest`, `HomeMainFragmentLoginGuardSourceTest`.
|
||||||
|
- 검색: v2 home production의 이전 커뮤니티 전체 목록 route 잔존 여부.
|
||||||
|
- 검증: home/modal focused test·compile·ktlint 성공, 최신 전체 1,198 tests 중 실패 0·error 0.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
없음. 두 진입점 모두 현재 `postId`를 `CommunityActionCommand.PostDetail`로 전달하고 공통 handler가 `CreatorChannelCommunityDetailActivity`로 이동하며 invalid 값은 이동하지 않는다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | route·guard·금지 참조 검토 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | 확정 후보 없음 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | focused 검증 성공 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음
|
||||||
|
|
||||||
|
**남은 항목:** 없음. Phase 12 관련 수동 항목은 사용자 확인으로 완료됐다.
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
수정 항목 없음.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Phase 14 장르 크리에이터 미표시 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 14 / genreCreators UI 미표시 |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` Phase 14 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: 응답·mapper 계약은 유지하면서 추천 탭에서 genre section과 follow-all 동작이 노출되지 않는지 확인한다.
|
||||||
|
- 포함: Fragment visibility/binding, `HomeRecommendationUiState.isEmpty`, ViewModel follow 상태와 tests.
|
||||||
|
- 제외: 장르 섹션 재도입 설계.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
항상 `GONE`, hidden section 기준 empty 오판 방지, genre follow callback 미연결, 응답 파싱 유지로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 코드: `HomeMainFragment.bindGenreCreatorSection()`, `HomeRecommendationUiState.kt`, recommendation mapper.
|
||||||
|
- 테스트: `HomeMainFragmentLayoutTest`의 hidden genre/empty 계약.
|
||||||
|
- 검증: home/modal focused test 성공, 최신 전체 1,198 tests 중 실패 0·error 0.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
없음. 숨겨진 genre data만 존재하는 경우도 Empty로 판정하며 Fragment가 해당 섹션을 노출하거나 모두 팔로우를 연결하지 않는다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | UI·state·mapper 대조 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | 확정 후보 없음 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | 자동 검증 성공 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음
|
||||||
|
|
||||||
|
**남은 항목:** 없음
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
수정 항목 없음.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Phase 15 추천 섹션 chevron 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 15 / 추천 섹션 타이틀 chevron 숨김 |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` Phase 15 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: 홈 추천의 최근 데뷔와 AI 캐릭터 섹션 제목에 더보기 chevron이 노출되지 않는지 확인한다.
|
||||||
|
- 포함: `HomeMainFragment.setUpSectionTitles()`와 source/layout tests.
|
||||||
|
- 제외: 다른 홈 tab과 섹션 전체보기 기능 설계.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
대상 두 제목의 `showMore` 미지정과 다른 tab/section에 대한 비의도 변경 없음으로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 코드: `HomeMainFragment.setUpSectionTitles()`.
|
||||||
|
- 테스트: `HomeMainFragmentSourceTest`, `HomeMainFragmentLayoutTest`, `HomeFollowingFragmentSourceTest`.
|
||||||
|
- 검증: home/modal focused test·compile/resource/ktlint 성공, 최신 전체 1,198 tests 중 실패 0·error 0.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
없음. 대상 제목에 chevron 활성 계약이 남아 있지 않고 다른 홈 tab source test도 통과한다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | source와 test 대조 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | 확정 후보 없음 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | focused 검증 성공 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음
|
||||||
|
|
||||||
|
**남은 항목:** 없음
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
수정 항목 없음.
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# Phase 16 방금 활동한 크리에이터 LIVE 라우팅 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 16 / Task 16.1 / Phase 16 Gate |
|
||||||
|
| 기준 commit 또는 working tree | `a1aa9c224326e4f730f715a4eba20add33329aed` + Phase 16 미커밋 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-30 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `docs/20260601_메인_홈_추천_UI와_API_연동/prd.md`, `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md`, `docs/agent-guides/build-test-style.md` |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
### 목적
|
||||||
|
|
||||||
|
- Phase 16의 DTO, mapper, route, Fragment 액션 연결이 PRD의 최근 활동 `LIVE` 라우팅 요구사항을 충족하는지 확인한다.
|
||||||
|
- 완료 체크박스와 실제 코드, 테스트, 검증 기록이 일치하는지 확인한다.
|
||||||
|
- 기존 공통 라이브·크리에이터 액션을 우회하거나 다른 activity type을 회귀시키지 않는지 확인한다.
|
||||||
|
|
||||||
|
### 포함 범위
|
||||||
|
|
||||||
|
- 코드:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeRecommendationModels.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationMappers.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationUiModels.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`
|
||||||
|
- 테스트:
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLoginGuardSourceTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/live/action/`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/action/`
|
||||||
|
- 문서: PRD의 `방금 활동한 크리에이터 LIVE 라우팅`, `plan-task.md`의 Phase 16, Phase 16 focused test guide.
|
||||||
|
- 수동 대조: DTO → UI model → route → 공통 액션 호출 경로와 working tree 변경 범위.
|
||||||
|
|
||||||
|
### 제외 범위
|
||||||
|
|
||||||
|
- `LiveActionCoordinator`, `CreatorAction`, `V2AccessGuard` 자체 정책의 변경 또는 재설계.
|
||||||
|
- 실제 서버 추천 데이터와 로그인·본인인증 상태를 이용한 기기 터치 검증.
|
||||||
|
- Phase 16 변경 경로 밖에서 관찰된 `v2.creator.channel` 테스트 안정성 수정.
|
||||||
|
- 다른 Phase 기능의 재리뷰.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
### 심각도
|
||||||
|
|
||||||
|
| 심각도 | 기준 |
|
||||||
|
|---|---|
|
||||||
|
| Blocker | 보안·데이터 손실 위험, 핵심 흐름 불능, 완료 판정을 무효화하는 문제 |
|
||||||
|
| High | 확정 요구사항·API Contract 위반 또는 주요 회귀 |
|
||||||
|
| Medium | 제한된 조건에서 발생하는 기능·접근성·복구 문제 |
|
||||||
|
| Low | 유지보수성, 문서 정합성 또는 비핵심 UX 문제 |
|
||||||
|
|
||||||
|
### 상태
|
||||||
|
|
||||||
|
| 상태 | 의미 | 후속 처리 |
|
||||||
|
|---|---|---|
|
||||||
|
| 후보 | 근거를 발견했지만 아직 재현·판정하지 않음 | 검증 후 상태 변경 |
|
||||||
|
| 확정 | 코드·test·문서 근거로 문제가 확인됨 | `plan-task.md` 회귀 수정 Task 후보 |
|
||||||
|
| 오탐 | 요구사항이나 실행 결과상 문제가 아님 | 근거를 남기고 종료 |
|
||||||
|
| 보류 | 외부 계약·환경·제품 결정이 필요함 | 담당 주체와 재개 조건 기록 |
|
||||||
|
| 수정 완료 | 수정과 관련 검증이 완료됨 | 실행 명령과 결과 연결 |
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
### 문서와 코드
|
||||||
|
|
||||||
|
- 요구사항:
|
||||||
|
- `prd.md:159`의 `방금 활동한 크리에이터 LIVE 라우팅`
|
||||||
|
- `plan-task.md:796`의 Phase 16
|
||||||
|
- DTO·mapper:
|
||||||
|
- `HomeRecommendationModels.kt:36`에서 필수 `creatorId: Long` 계약 확인.
|
||||||
|
- `HomeRecommendationMappers.kt:44`에서 DTO의 `creatorId`가 UI model로 보존됨을 확인.
|
||||||
|
- route:
|
||||||
|
- `HomeRecommendationUiModels.kt:128`에서 `LiveRoom`, `CreatorChannel` route 정의 확인.
|
||||||
|
- `HomeRecommendationUiModels.kt:138`에서 `targetId > 0L`, `targetId == null && creatorId > 0L`, invalid ID no-op 분기 확인.
|
||||||
|
- Fragment:
|
||||||
|
- `HomeMainFragment.kt:707`에서 `LiveRoom`은 `LiveActionCoordinator.enterLiveRoom()`, `CreatorChannel`은 `CreatorActionCommand.Profile`로 전달됨을 확인.
|
||||||
|
- 테스트:
|
||||||
|
- `HomeMainFragmentLayoutTest.kt:907`에서 다른 activity type route 회귀 확인.
|
||||||
|
- `HomeMainFragmentLayoutTest.kt:931`에서 `LIVE` 정상·fallback·invalid ID 분기 확인.
|
||||||
|
- `HomeMainFragmentLoginGuardSourceTest.kt:128`에서 기존 공통 Live/Creator Action 사용 계약 확인.
|
||||||
|
|
||||||
|
### 실행 환경
|
||||||
|
|
||||||
|
```text
|
||||||
|
OS: macOS 26.0 (25A354), x86_64
|
||||||
|
Java: OpenJDK 17.0.15
|
||||||
|
Gradle: 8.14.3
|
||||||
|
Kotlin: 2.0.21
|
||||||
|
기준 commit: a1aa9c224326e4f730f715a4eba20add33329aed
|
||||||
|
환경 변수: 별도 민감정보 확인 또는 출력 없음
|
||||||
|
```
|
||||||
|
|
||||||
|
### 실행한 검증
|
||||||
|
|
||||||
|
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|
||||||
|
|---|---|---|
|
||||||
|
| Phase 16 focused test와 live/creator action 회귀 `--rerun-tasks` | 성공 | `BUILD SUCCESSFUL in 7m 19s`, 39 tasks executed |
|
||||||
|
| Phase 16 focused test와 live/creator action 회귀 최종 재실행 | 성공 | 116 tests, failures 0, errors 0, `BUILD SUCCESSFUL in 24s` |
|
||||||
|
| `./gradlew :app:compileDebugKotlin :app:ktlintCheck` | 성공 | `BUILD SUCCESSFUL in 5s` |
|
||||||
|
| `./gradlew tasks --all --console=plain` | 성공 | 문서 변경 후 Gradle task 유효성 확인, `BUILD SUCCESSFUL in 3s` |
|
||||||
|
| `git diff --check` | 성공 | 출력 없음 |
|
||||||
|
| `git diff --name-only`와 Phase 16 Files 대조 | 성공 | 이 리뷰 보고서를 제외하면 production 4개, test 2개, 승인된 PRD·plan·test guide만 변경됨 |
|
||||||
|
| `./gradlew :app:testDebugUnitTest` | 실패 | 1186 tests 중 Phase 16 변경 경로 밖 `v2.creator.channel` 2건 실패 |
|
||||||
|
| 전체 suite 실패 3개 test method 개별 격리 실행 | 성공 | 각 `BUILD SUCCESSFUL`; 전체 실행의 실패가 개별 실행에서는 재현되지 않음 |
|
||||||
|
| 실제 추천 `LIVE` item 기기 터치 | 불가 | 서버 추천 데이터와 사용자 상태가 필요한 경로이므로 route 단위 테스트와 공통 액션 source 계약으로 대체 |
|
||||||
|
|
||||||
|
전체 suite에서 처음 실패한 테스트는 아래 2건이다.
|
||||||
|
|
||||||
|
- `CreatorChannelHomeViewModelTest > 채널 후원 성공 시 보유 can보다 큰 금액 차감은 0으로 보정한다`
|
||||||
|
- `CreatorChannelCommunityDetailViewModelTest > 채널 작성자는 유료 미구매 게시물도 이미지와 오디오 URL을 볼 수 있다`
|
||||||
|
|
||||||
|
두 class만 재실행했을 때는 위 2건이 통과하고 같은 class의 다른 후원 테스트 1건이 실패했으며, 관련 3개 method를 각각 격리 실행하면 모두 통과했다. Phase 16 working tree에는 `v2.creator.channel` production/test 변경이 없다. 따라서 이 결과는 Phase 16 회귀 근거로 사용하지 않고, 범위 밖의 비결정적 전체 suite 관찰로 남긴다.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
Phase 16 요구사항 위반, 공통 접근 정책 우회, 다른 activity type route 회귀, 변경 범위 이탈은 확인되지 않았다.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음. Phase 16에 추가할 신규 회귀 수정 Task와 create_goal objective가 없다.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | Phase 16 문서, production 4개, test 2개, 공통 action 호출 경로 대조 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | Phase 16 확정 발견 사항 없음 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 전환할 확정 review 항목 없음 |
|
||||||
|
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | Phase 16 보류 항목 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | focused 116 tests, compile, ktlint, diff 검증과 전체 suite 관찰 기록 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음.
|
||||||
|
|
||||||
|
**남은 항목:** Phase 16 수정 항목은 없다. 이전 `v2.creator.channel` 비결정적 실패는 관찰 이력으로 보존하고, 동일 실패 재현 시 별도 테스트 격리 범위로 재개한다.
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
수정 항목이 없어 해당 없음.
|
||||||
|
|
||||||
|
## 10. 2026-07-31 전체 계획 기준 재검토
|
||||||
|
|
||||||
|
- 기준: `a106548f` 기반 현재 working tree와 `plan-task.md` 전체 Phase를 다시 대조했다.
|
||||||
|
- 범위: LIVE route의 `targetId` 우선, null일 때만 `creatorId` fallback, invalid ID no-op, 공통 Live/Creator action 경로와 기존 activity type 회귀.
|
||||||
|
- 검증: v2 home 및 `V2ModalDialog` focused test·resource merge·compile·ktlint가 성공했고, 최신 전체 1,198 tests는 실패 0·error 0으로 성공했다.
|
||||||
|
- 판정: Phase 16 확정 발견 사항 없음. 신규 회귀 Task 전환 항목 없음.
|
||||||
|
- 참고: 전체 테스트 최초 실행의 범위 밖 `CreatorChannelHomeViewModelTest` 1건 실패는 격리 및 전체 재실행에서 통과해 기존 보고서의 비결정적 suite 관찰과 같은 범주로 유지한다.
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Phase 17 모두 팔로우 확인 절차 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 17 / Task 17.1, P17-GATE |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 Phase 17 미커밋 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` Phase 17 |
|
||||||
|
| 리뷰 상태 | 수정 검증 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: 응원 크리에이터 모두 팔로우가 확인 전 요청하지 않고 취소 no-op, 확인 후 단일 요청과 다국어 계약을 지키는지 확인한다.
|
||||||
|
- 포함: `HomeMainFragment`, `HomeCheerCreatorAdapter`, `HomeFollowAllButtonBinder`, `V2ModalDialog`, 세 locale resource와 tests.
|
||||||
|
- 제외: 모두 팔로우 API/ViewModel 계약 변경, 숨겨진 genre UI, 공통 Dialog 리팩터링.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
Dialog 선행, confirm callback 단독 API 호출, cancel dismiss-only, 중복 confirm 방지, 완료·빈 목록 no-op, locale resource 분리로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 코드: `HomeMainFragment.showCheerCreatorsFollowAllConfirmDialog()`, `HomeFollowAllButtonBinder`, `V2ModalDialog`.
|
||||||
|
- 리소스: `values`, `values-en`, `values-ja`의 `home_recommendation_follow_all_confirm_*`.
|
||||||
|
- 테스트: `HomeMainFragmentLayoutTest`, `V2ModalDialogTest`.
|
||||||
|
- 검증: home/modal focused test·resource merge·compile·ktlint 성공. 최신 전체 1,198 tests 중 실패 0·error 0.
|
||||||
|
- 수동 검증: 사용자 수동 확인으로 P17-GATE의 시각·상호작용 항목은 동작 확인. 추가 발견된 미로그인 로그인 가드 순서 회귀는 `P17-R2`로 수정.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `REV-P17-001` | Low | 수정 완료 | 신규 모두 팔로우 테스트명 2건이 영문이다 | `Task R17.1` | `P17-R1` |
|
||||||
|
| `REV-P17-002` | Medium | 수정 완료 | 모두 팔로우 미로그인 터치에서 확인 Dialog가 로그인 가드보다 먼저 표시된다 | `Task R17.2` | `P17-R2` |
|
||||||
|
|
||||||
|
사용자 수동 확인과 `P17-R2` 회귀 수정 후 Phase 17 기능 계약은 충족한다.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
### REV-P17-001 — 신규 모두 팔로우 테스트명 2건이 영문이다
|
||||||
|
|
||||||
|
- **심각도:** Low
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **관련 요구사항:** `code-style.md` 신규 Kotlin 테스트명 한글 문장 규칙
|
||||||
|
- **소유 Task:** `Task R17.1` / `P17-R1`
|
||||||
|
|
||||||
|
**관찰 내용·근거**
|
||||||
|
|
||||||
|
Phase 17에서 신규 추가한 `cheer creator follow all opens confirm dialog before api request`, `follow all confirm strings are localized` 테스트명이 영문이고 한글 검증 의도 주석도 없다. Dialog 선행·다국어 assertion 자체는 focused test에서 성공했다.
|
||||||
|
|
||||||
|
**재현 절차**
|
||||||
|
|
||||||
|
1. current diff에서 위 두 신규 `fun \`...\`` 메서드를 확인한다.
|
||||||
|
2. `code-style.md` §5 테스트 관례와 대조한다.
|
||||||
|
|
||||||
|
**영향·권장 조치**
|
||||||
|
|
||||||
|
런타임 영향은 없으나 신규 테스트가 저장소 표현 규칙과 다르다. assertion은 바꾸지 않고 두 테스트명만 한글 문장으로 정정한다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-07-31 — current diff와 테스트 네이밍 규칙 대조로 확정했다.
|
||||||
|
- 2026-07-31 — `P17-R1` 테스트명 정정과 direct 영향 회귀, `ktlintCheck`, `git diff --check` 성공으로 수정 완료 판정했다.
|
||||||
|
|
||||||
|
기능 판정은 변경 없음. `onCheerFollowAllClick`은 Dialog helper만 호출하고 `confirmButtonClick`에서만 기존 section key와 creator ID 목록으로 ViewModel을 호출한다. 취소 callback은 전달하지 않아 공통 Dialog의 dismiss-only 동작을 사용한다.
|
||||||
|
|
||||||
|
### REV-P17-002 — 모두 팔로우 미로그인 터치에서 확인 Dialog가 로그인 가드보다 먼저 표시된다
|
||||||
|
|
||||||
|
- **심각도:** Medium
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **관련 요구사항:** 기존 MainV2 로그인 가드 우선 정책, `P17-T1` 확인 전 요청 금지
|
||||||
|
- **소유 Task:** `Task R17.2` / `P17-R2`
|
||||||
|
|
||||||
|
**관찰 내용·근거**
|
||||||
|
|
||||||
|
사용자 수동 확인에서 `모두 팔로우` 터치 시 미로그인 로그인 가드보다 `V2ModalDialog` 확인창이 먼저 표시되는 버그가 확인됐다. 현재 `onCheerFollowAllClick`은 `showCheerCreatorsFollowAllConfirmDialog(creatorIds)`를 직접 호출한다.
|
||||||
|
|
||||||
|
**영향·권장 조치**
|
||||||
|
|
||||||
|
미로그인 사용자에게 follow 확인 UI를 먼저 보여 기존 접근 정책과 맞지 않는다. 신규 공통 abstraction 없이 기존 `ensureV2Access(AccessRequirement.Login)`를 Dialog 표시 전에 적용한다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-07-31 — 사용자 수동 확인으로 로그인 가드 순서 회귀를 확정했다.
|
||||||
|
- 2026-07-31 — `P17-R2` RED/GREEN과 직접 영향 회귀, compile, ktlint, `git diff --check` 성공으로 수정 완료 판정했다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
`plan-task.md` Phase 17의 `Task R17.1` / `P17-R1`과 `Task R17.2` / `P17-R2`는 수정 검증까지 완료했다.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | 코드·resource·test·Gate 상태 대조 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | `REV-P17-001`, `REV-P17-002` 수정 완료 |
|
||||||
|
| 확정 항목 plan 반영 | 충족 | `Task R17.1` / `P17-R1`, `Task R17.2` / `P17-R2` 완료 |
|
||||||
|
| 보류 항목의 담당·재개 조건 기록 | 충족 | 보류 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | focused/full 검증 기록 연결 |
|
||||||
|
|
||||||
|
**최종 결론:** 승인
|
||||||
|
|
||||||
|
**남은 항목:** 없음
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
- 2026-07-31 — `Task R17.1` 실행으로 신규 영문 테스트명 2건을 `응원 크리에이터 모두 팔로우는 API 요청 전에 확인창을 연다`, `모두 팔로우 확인창 문자열은 다국어로 제공한다`로 정정했다. Production Dialog·ViewModel·API 동작과 assertion은 변경하지 않았다. `HomeMainFragmentLayoutTest`·`V2ModalDialogTest`, `./gradlew :app:ktlintCheck`, `git diff --check`가 모두 성공했다. 당시 `P17-GATE`의 실제 확인창 시각·상호작용 4항목은 정상 화면 캡처·조작이 가능한 기기 환경에서 재개하는 상태였다.
|
||||||
|
- 2026-07-31 — 수동 Gate 재검증에서 `adb devices`는 `2cec640c34017ece device`를 반환했고 `./gradlew :app:installDebug`, `adb shell monkey -p kr.co.vividnext.sodalive.debug -c android.intent.category.LAUNCHER 1`는 성공했다. `adb shell dumpsys window`는 foreground가 `kr.co.vividnext.sodalive.debug/kr.co.vividnext.sodalive.v2.main.MainV2Activity`임을 반환했지만, `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sodalive_manual_gate_20260731/screen.png`는 검은 배경의 splash/voice on 화면으로 판독됐다. 당시 실제 확인창 시각·상호작용 4항목은 홈 추천 화면 접근 불가로 blocked였다.
|
||||||
|
- 2026-07-31 — 사용자 수동 확인에서 Task 12.2와 P17-GATE 수동 항목은 모두 동작함을 확인했지만, 모두 팔로우 미로그인 터치 시 로그인 가드보다 확인 Dialog가 먼저 표시되는 회귀가 발견되어 `REV-P17-002` / `P17-R2`로 전환했다.
|
||||||
|
- 2026-07-31 — `Task R17.2` 실행으로 `onCheerFollowAllClick`에서 기존 `ensureV2Access(AccessRequirement.Login)` 통과 후 `showCheerCreatorsFollowAllConfirmDialog(creatorIds)`를 호출하도록 수정했다. RED는 `HomeMainFragmentLoginGuardSourceTest.HomeMainFragment 응원 크리에이터 모두 팔로우는 확인창보다 로그인 가드를 먼저 실행한다`와 `HomeMainFragmentLayoutTest.응원 크리에이터 모두 팔로우는 API 요청 전에 확인창을 연다` focused test 실패로 확인했고, GREEN 후 동일 focused test는 성공했다. 직접 영향 회귀 `HomeMainFragmentLoginGuardSourceTest`, `HomeMainFragmentLayoutTest`, `V2ModalDialogTest`, `compileDebugKotlin`, `ktlintCheck`, `git diff --check`도 모두 성공했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation warning과 Gradle deprecation warning은 기존 경고다.
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Phase 18 현재 진행 중인 라이브 `전체` 항목 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 18 / Task 18.1, P18-GATE |
|
||||||
|
| 기준 commit 또는 working tree | `d592038e` 기반 Phase 18 staged working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` Phase 18 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: 라이브가 비어 있지 않으면 기존 `전체` 항목을 항상 추가하고, 빈 목록·20개 cap·클릭 경로를 유지하는지 확인한다.
|
||||||
|
- 포함: `HomeLiveAdapter`, `HomeMainFragment`의 섹션 표시와 `전체` 클릭 연결, `HomeMainFragmentLayoutTest`, Phase 18 문서.
|
||||||
|
- 제외: 전체 라이브 API·화면, 라이브 item 입장 로직, 기존 프로젝트 전체 lint 부채 수정, 수동 UI 시각 확인.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
빈 목록 0개, 라이브 1~20개는 원본 개수와 `전체` 1개, 21개 이상은 라이브 20개와 `전체` 1개인지 판정했다. 기존 `MoreViewHolder`, 로그인 가드, `HomeOnAirLiveActivity` 이동 경로가 변경되지 않았는지도 대조했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
### 문서와 코드
|
||||||
|
|
||||||
|
- 요구사항: `prd.md:147-154`, `prd.md:526-530`
|
||||||
|
- 계획: `plan-task.md`의 `P18-T1`, `P18-GATE`
|
||||||
|
- 코드: `HomeLiveAdapter.kt:16-55`, `HomeMainFragment.kt:213-219`, `HomeMainFragment.kt:572-575`, `HomeMainFragment.kt:682-685`
|
||||||
|
- 테스트: `HomeMainFragmentLayoutTest.kt:1046-1095`, `HomeMainFragmentLoginGuardSourceTest`
|
||||||
|
|
||||||
|
### 실행 환경
|
||||||
|
|
||||||
|
```text
|
||||||
|
OS: macOS 26.0 (25A354)
|
||||||
|
Java: OpenJDK 17.0.15
|
||||||
|
Gradle wrapper: 8.14.3
|
||||||
|
기준 HEAD: d592038eddce8245e197aef48dbae7c7eb48585a
|
||||||
|
```
|
||||||
|
|
||||||
|
### 실행한 검증
|
||||||
|
|
||||||
|
| 명령 또는 대조 | 결과 | 핵심 증거 |
|
||||||
|
|---|---|---|
|
||||||
|
| `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" :app:compileDebugKotlin :app:ktlintCheck :app:lintDebug --no-parallel --console=plain` | 일부 성공 / lint 실패 | focused 74 tests 실패·오류 0, compile·ktlint 성공. `lintDebug`는 기존 범위에서 28 errors·767 warnings로 실패 |
|
||||||
|
| `./gradlew :app:testDebugUnitTest --no-parallel --console=plain --rerun-tasks` | 성공 | 162 suites, 1,199 tests, 실패 0·오류 0·skip 0, exit code 0 |
|
||||||
|
| `./gradlew tasks --all --console=plain` | 성공 | `BUILD SUCCESSFUL`, 문서 변경 후 task 인식 확인 |
|
||||||
|
| `git diff --cached --check` 및 `git diff --check` | 성공 | 출력 없음, exit code 0 |
|
||||||
|
| PRD·계획·호출 흐름 정적 대조 | 성공 | 0개 숨김, 20개 cap, 기존 `전체` ViewHolder·로그인 가드·이동 경로 유지 |
|
||||||
|
|
||||||
|
`lintDebug` 첫 오류는 변경되지 않은 `AndroidManifest.xml:316`의 `com.facebook.FacebookActivity` `MissingClass`다. 전체 lint 보고서의 이번 production 변경 파일에는 기존 `notifyDataSetChanged()` warning 1건만 있고, 변경된 줄에 신규 error는 없다.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
프로젝트 전체 `lintDebug` 실패는 staged diff에서 추가된 문제가 아니므로 Phase 18 수정 항목으로 전환하지 않는다.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
없음.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | 코드·test·PRD·Task와 호출 흐름 대조 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | 후보 및 확정 발견 사항 없음 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 전환할 review ID 없음 |
|
||||||
|
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | focused·전체 unit test·compile·ktlint·lint·diff 결과 기록 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음
|
||||||
|
|
||||||
|
**남은 항목:** Phase 18 범위에는 없음. 프로젝트 전체 `lintDebug` 기존 오류는 별도 작업 범위가 승인될 때 처리한다.
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
코드 수정 항목 없음.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Phase 2 추천 데이터 계층 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 2 / API DTO, Repository, DI 추가 |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` Phase 2 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: 추천 조회·모두 팔로우 계약과 DI 연결이 PRD 범위에 맞는지 확인한다.
|
||||||
|
- 포함: `HomeRecommendationApi`, DTO, Repository, `AppDI` 등록.
|
||||||
|
- 제외: 서버 실제 응답과 네트워크 E2E.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
API path/method, request 필드, auth header 전달, repository 책임, v2 패키지 경계를 기준으로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 코드: `v2/main/home/data/HomeRecommendationApi.kt`, `HomeRecommendationModels.kt`, `HomeRecommendationRepository.kt`, `di/AppDI.kt`.
|
||||||
|
- 테스트: `HomeRecommendationViewModelTest`, home source/layout tests.
|
||||||
|
- 검증: home/modal focused test 및 compile/resource/ktlint 성공, 최신 전체 1,198 tests 중 실패 0·error 0.
|
||||||
|
- 환경: 실제 서버 호출 없음.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
없음. 후속 응답 스키마 변경과 first audio 제거가 현재 DTO·mapper에 반영되어 초기 Phase 계약과의 역사적 차이는 후속 승인 변경으로 확인했다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | API·DTO·Repository·DI 대조 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | 후속 계약 변경 이력 확인 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | `plan-task.md` Verification Log 연결 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음
|
||||||
|
|
||||||
|
**남은 항목:** 실제 서버 E2E는 이번 리뷰 범위 밖이다.
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
수정 항목 없음.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Phase 3 활동 타입·mapper 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 3 / 순수 mapper와 unit test 작성 |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` Phase 3 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: 추천 응답의 UI model 변환과 활동 타입 다국어 fallback 계약을 확인한다.
|
||||||
|
- 포함: `CreatorActivityType`, 추천 mapper/UI model, 문자열 리소스와 mapper 테스트.
|
||||||
|
- 제외: 카드 레이아웃과 클릭 라우팅.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
null/unknown 안전성, 필드 보존, locale resource 분리, 후속 공용 타입 전환 시 회귀 여부로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 코드: `v2/common/CreatorActivityType.kt`, `HomeRecommendationMappers.kt`, `HomeRecommendationUiModels.kt`.
|
||||||
|
- 테스트: `HomeCreatorActivityTypeLabelResTest`, `HomeMainFragmentLayoutTest` mapper 계약.
|
||||||
|
- 검증: home/modal focused test 성공, 최신 전체 1,198 tests 중 실패 0·error 0.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
없음. 계획의 초기 `RecommendedActivityType` 명칭은 후속 commit에서 공용 `CreatorActivityType`으로 전환됐고 production과 테스트가 함께 갱신되어 현재 결함이 아니다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | mapper·공용 타입·다국어 대조 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | 명칭 변경의 후속 이력 확인 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | 자동 테스트 성공 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음
|
||||||
|
|
||||||
|
**남은 항목:** 없음
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
수정 항목 없음.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Phase 4 추천 상태·ViewModel 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 4 / ViewModel과 화면 상태 구성 |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` Phase 4 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: loading/content/empty/error와 모두 팔로우 상태 갱신이 계획과 일치하는지 확인한다.
|
||||||
|
- 포함: `HomeRecommendationUiState`, `HomeRecommendationViewModel`, DI와 unit test.
|
||||||
|
- 제외: Fragment 표시와 실제 네트워크 E2E.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
상태 전이, 성공 여부 판정, 표시하지 않는 genre 제외 empty 정책, 오류·토스트 처리와 기존 API 계약 유지 여부로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 코드: `HomeRecommendationUiState.kt`, `HomeRecommendationViewModel.kt`, `AppDI.kt`.
|
||||||
|
- 테스트: `HomeRecommendationViewModelTest`, `HomeMainFragmentLayoutTest`의 empty/mapper 계약.
|
||||||
|
- 검증: 관련 focused test 성공, 최신 전체 1,198 tests 중 실패 0·error 0.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
없음. 숨겨진 genre를 empty 판정에서 제외하는 후속 Phase 14 정책과 Phase 17의 Dialog 선행 절차가 현재 상태 계층과 충돌하지 않는다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | 상태·ViewModel·DI 검토 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | 후속 정책 포함 판정 완료 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | focused/full test 성공 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음
|
||||||
|
|
||||||
|
**남은 항목:** 없음
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
수정 항목 없음.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Phase 5 홈 shell·상단 UI 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 5 / HomeMainFragment 레이아웃과 상단 UI 구현 |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` Phase 5 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: title bar, text tab bar, 추천 content scroll shell과 섹션 container 구성이 유지되는지 확인한다.
|
||||||
|
- 포함: `fragment_v2_main_home.xml`, `HomeMainFragment`, title/tab/section source tests.
|
||||||
|
- 제외: Figma 실기기 pixel 비교와 후속 섹션 내부 상세.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
추천 tab 선택, swipe 미사용, 고정 상단과 하단 content 스크롤 분리, 섹션 기본 visibility를 기준으로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 코드: `fragment_v2_main_home.xml`, `HomeMainFragment.onViewCreated()`의 tab bar 설정, `setUpSectionTitles()`.
|
||||||
|
- 테스트: `HomeMainFragmentLayoutTest`, `HomeMainFragmentSourceTest`, `HomeMainTitleBarActionSourceTest`.
|
||||||
|
- 검증: home/modal focused test·resource merge·compile·ktlint 성공, 최신 전체 1,198 tests 중 실패 0·error 0.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
없음. 현재 Fragment 경로는 후속 패키지 정리 결과이며 레이아웃 binding과 source test가 현 경로를 기준으로 통과한다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | XML·Fragment·source test 대조 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | 확정 후보 없음 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | merge/compile/test/ktlint 성공 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음
|
||||||
|
|
||||||
|
**남은 항목:** 없음. Phase 12 관련 수동 항목은 사용자 확인으로 완료됐다.
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
수정 항목 없음.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Phase 6 추천 섹션 adapter·widget 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 6 / Task 6.1~6.13 |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` Phase 6 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: 각 추천 섹션 adapter/binder, 빈 목록 숨김, Figma 후속 조정과 공용 widget 재사용을 확인한다.
|
||||||
|
- 포함: `v2/main/home/ui`, 홈 레이아웃, `HomeMainFragment` section binding.
|
||||||
|
- 제외: 인기 커뮤니티 상세는 Phase 7, first audio 제거와 genre 숨김은 후속 Phase에서 별도 판정.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
섹션별 단일 책임, 기존 widget 재사용, 빈 목록 `GONE`, 표시 개수·간격·클릭 callback, 후속 승인 변경 반영으로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 코드: `HomeLiveAdapter`, `HomeRecentActivityCreatorAdapter`, `HomeRecentDebutCreatorAdapter`, `HomeAiCharacterAdapter`, `HomeCheerCreatorAdapter`, 관련 binder.
|
||||||
|
- 테스트: `HomeMainFragmentLayoutTest`, `HomeCreatorProfileImageLoaderTest`.
|
||||||
|
- 검증: home/modal focused test·resource merge·compile·ktlint 성공, 최신 전체 1,198 tests 중 실패 0·error 0.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
없음. first audio와 genre의 초기 구현 기록은 Phase 12B와 Phase 14의 승인된 후속 변경으로 현재 UI에서 제거·숨김 처리되어 있다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | 섹션 adapter/binder와 후속 변경 대조 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | 후속 Phase 소유 항목 분리 완료 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | focused 검증 성공 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음
|
||||||
|
|
||||||
|
**남은 항목:** 없음. Phase 12 관련 수동 항목은 사용자 확인으로 완료됐다.
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
수정 항목 없음.
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# Phase 7 인기 커뮤니티 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 7 / Task 7.1~7.5 |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` Phase 7 |
|
||||||
|
| 리뷰 상태 | 수정 검증 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: 추천 커뮤니티의 데이터 보존, 이미지·유료 상태, 동적 비율과 검증 기록 정합성을 확인한다.
|
||||||
|
- 포함: `FeedItem.Community`, `FeedCommunityView`, `HomePopularCommunityAdapter`, mapper, 관련 XML/test/문서.
|
||||||
|
- 제외: 커뮤니티 상세 화면 자체와 서버 구매 권한 판정.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
PRD의 시각 계약과 이후 보안 재리뷰 결정, 현재 코드·회귀 테스트, 변경 이력을 함께 대조하고 유료 원본의 네트워크·cache 노출 가능성을 기준으로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 요구사항: `prd.md` FeedCommunityView Requirements와 UX의 유료 미구매 blur/lock 항목.
|
||||||
|
- 코드: `HomePopularCommunityAdapter.kt:53-79`, `FeedCommunityView.kt`, `FeedItem.kt`.
|
||||||
|
- 테스트: `HomePopularCommunityAdapterTest`, `HomeMainFragmentLayoutTest`, `FeedViewTest`.
|
||||||
|
- 이력: commit `2d58a876`에서 원본 미로드 방식을 blur 변환 로드 방식으로 변경.
|
||||||
|
- 후속 승인 계약: `docs/20260712_v2_크리에이터_커뮤니티_게시글_이미지_원본비율/` 문서와 commit `9244a007`이 홈 추천을 포함한 커뮤니티 이미지를 원본 비율·`fitCenter`로 변경했다. 따라서 Phase 7의 초기 `346:236` 비율과 다른 현재 이미지 크기 계산은 결함으로 판정하지 않는다.
|
||||||
|
- 검증: `HomePopularCommunityAdapterTest`·home focused test·compile·ktlint 성공, 최신 전체 1,198 tests 중 실패 0·error 0.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `REV-P7-001` | High | 수정 완료 | 유료 미구매 원본 이미지 로드가 다시 도입됐다 | `Task R7.1` | `P7-R1` |
|
||||||
|
| `REV-P7-002` | Low | 수정 완료 | 회귀 수정에서 변경한 테스트명이 영문이다 | `Task R7.2` | `P7-R2` |
|
||||||
|
| `REV-P7-003` | Medium | 수정 완료 | 원본 미로드 회귀 테스트가 adapter bind 동작을 검증하지 않는다 | `Task R7.3` | `P7-R3` |
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
### REV-P7-001 — 유료 미구매 원본 이미지 로드가 다시 도입됐다
|
||||||
|
|
||||||
|
- **심각도:** High
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **관련 요구사항:** 유료 미구매 커뮤니티 이미지 blur/lock 표시
|
||||||
|
- **관련 계약:** 별도 API Contract 없음
|
||||||
|
- **소유 Task:** `Task R7.1` / `P7-R1`
|
||||||
|
|
||||||
|
**관찰 내용**
|
||||||
|
|
||||||
|
2026-06-05 보안 재리뷰에서 유료 미구매 item의 원본 `imageUrl`을 로드하지 않고 기존 Coil 요청과 drawable을 비우도록 수정했다. commit `2d58a876`에서 이 원본 URL을 다시 로드한 뒤 client-side `BlurTransformation`을 적용하도록 바뀌었다. 현재 working tree에서는 `P7-R1` 수정으로 원본 미로드 계약이 복구됐다.
|
||||||
|
|
||||||
|
**근거**
|
||||||
|
|
||||||
|
- 수정 전 코드: commit `2d58a876`의 `HomePopularCommunityAdapter.kt`는 `imageUrl`을 로드하고 잠금 상태에서 blur를 적용한다.
|
||||||
|
- 현재 코드: `HomePopularCommunityAdapter.kt`는 잠금 유료 게시물의 `imageUrl`을 image loader에 전달하지 않는다.
|
||||||
|
- 문서: `prd.md` 2026-06-05 이력과 `plan-task.md`의 보안 재리뷰 기록은 원본 미로드와 기존 Coil 요청 취소를 성공 결과로 적는다.
|
||||||
|
- 변경 이력: commit `2d58a876`이 원본 미로드 구현과 회귀 테스트를 client-side blur 로드 방식으로 함께 변경했다.
|
||||||
|
|
||||||
|
**재현 또는 검증 절차**
|
||||||
|
|
||||||
|
1. PRD 현재 요구사항과 2026-06-05 변경 이력을 대조한다.
|
||||||
|
2. adapter의 `imageUrl`/`BlurTransformation`과 회귀 테스트를 확인한다.
|
||||||
|
3. `git show 2d58a876 -- HomePopularCommunityAdapter.kt HomeMainFragmentLayoutTest.kt`로 후속 정책 변경을 확인한다.
|
||||||
|
4. 보안 재리뷰에서 제거한 원본 로드가 후속 승인 기록 없이 다시 도입됐음을 확인한다.
|
||||||
|
|
||||||
|
**영향**
|
||||||
|
|
||||||
|
유료 미구매 원본 URL이 client network 요청과 image cache에 전달되므로 화면 blur와 무관하게 원본 콘텐츠 접근 가능성이 생긴다. 또한 현재 PRD의 시각 blur 문구와 보안 결정이 충돌해 후속 구현 판단도 불안정하다.
|
||||||
|
|
||||||
|
**권장 조치**
|
||||||
|
|
||||||
|
원본 미로드를 검증하는 실패 테스트를 먼저 복구하고 adapter를 최소 수정한다. 무료·구매 완료 이미지는 기존대로 유지하고, 유료 미구매 item은 원본 요청을 취소·비우되 lock overlay와 가격을 유지한다. PRD도 이 보안 결정과 일치시키며 서버가 별도 안전 preview를 제공한다는 계약은 추정하지 않는다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-07-31 — 현재 코드·테스트·git 이력과 2026-06-05 보안 재리뷰 기록을 교차 확인해 보안 정책 회귀로 확정했다.
|
||||||
|
- 2026-07-31 — `P7-R1` 수정과 focused·home·compile·ktlint·diff 검증 성공으로 수정 완료 판정했다.
|
||||||
|
|
||||||
|
### REV-P7-002 — 회귀 수정에서 변경한 테스트명이 영문이다
|
||||||
|
|
||||||
|
- **심각도:** Low
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **관련 요구사항:** `code-style.md` 신규 Kotlin 테스트명 한글 문장 규칙
|
||||||
|
- **소유 Task:** `Task R7.2` / `P7-R2`
|
||||||
|
|
||||||
|
**관찰 내용·근거**
|
||||||
|
|
||||||
|
`P7-R1`에서 기존 영문 테스트명을 `home popular community adapter does not load original image for locked paid post`로 변경했다. 신규/변경 Kotlin 테스트명은 한글 문장을 사용해야 하며 외부 API명 등으로 영문을 유지할 경우에는 한글 주석이 필요하지만 두 조건 모두 충족하지 않는다.
|
||||||
|
|
||||||
|
**재현 절차**
|
||||||
|
|
||||||
|
1. `git diff -- HomeMainFragmentLayoutTest.kt`로 테스트명 변경을 확인한다.
|
||||||
|
2. `code-style.md` §5 테스트 관례와 대조한다.
|
||||||
|
|
||||||
|
**영향·권장 조치**
|
||||||
|
|
||||||
|
런타임 영향은 없으나 신규 테스트 규칙과 리뷰 완료 증거가 불일치한다. assertion과 production code는 유지하고 테스트명만 한글 문장으로 정정한다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-07-31 — current diff와 코드 스타일 규칙 대조로 확정했다.
|
||||||
|
- 2026-07-31 — `P7-R2` 테스트명 정정과 focused/direct 영향 테스트, `ktlintCheck`, `git diff --check` 성공으로 수정 완료 판정했다.
|
||||||
|
|
||||||
|
### REV-P7-003 — 원본 미로드 회귀 테스트가 adapter bind 동작을 검증하지 않는다
|
||||||
|
|
||||||
|
- **심각도:** Medium
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **관련 요구사항:** 유료 미구매 원본 미로드·기존 Coil 요청 해제·drawable 제거·lock overlay/가격 유지
|
||||||
|
- **소유 Task:** `Task R7.3` / `P7-R3`
|
||||||
|
|
||||||
|
**관찰 내용·근거**
|
||||||
|
|
||||||
|
`P7-R3` 수정 전 `HomeMainFragmentLayoutTest.유료 미구매 인기 커뮤니티는 원본 이미지를 로드하지 않는다`는 `HomePopularCommunityAdapter.kt`를 문자열로 읽어 특정 코드 조각과 `BlurTransformation` 미존재만 확인했다. ViewHolder를 실제로 bind하지 않아 기존 image request가 해제되는지, 재활용된 view의 drawable가 비워지는지, lock overlay와 가격이 유지되는지를 증명하지 못했다. 특히 `P7-R1` 완료 증거가 이 동작들을 명시했고, 같은 보안 계약이 commit `2d58a876`에서 이미 한 차례 회귀했으므로 소스 문자열 검사만으로는 회귀 방지 강도가 부족했다.
|
||||||
|
|
||||||
|
**영향·권장 조치**
|
||||||
|
|
||||||
|
현재 production 코드에서는 잠금 유료 item의 URL이 loader로 전달되지 않고 blank 경로에서 `dispose()`와 `setImageDrawable(null)`을 호출하므로 즉시 production 결함은 확인되지 않았다. 다만 재활용된 image view 상태를 포함한 adapter bind 실행형 테스트로 보안 계약을 고정하고, 무료·구매 완료 경로가 기존대로 이미지를 로드하는지도 함께 검증한다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-07-31 — production bind 코드와 현재 테스트 assertion, `P7-R1` 완료 증거를 대조해 실행형 회귀 검증 부재를 확정했다.
|
||||||
|
- 2026-07-31 — `P7-R3`에서 source 문자열 검사를 제거하고 실제 adapter/ViewHolder bind 회귀 테스트 2건으로 교체했다. 잠금 유료 원본 미로드·이전 요청 해제·drawable 제거·overlay/가격 유지와 무료·구매 완료 URL 전달을 검증했고, 보안 분기 mutation 실패와 원상 복구 후 성공을 확인해 수정 완료로 판정했다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
`Task R7.1` / `P7-R1`, `Task R7.2` / `P7-R2`, `Task R7.3` / `P7-R3`은 모두 수정 검증까지 완료했다.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | 코드·test·문서·git 이력 대조 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | `REV-P7-001`, `REV-P7-002`, `REV-P7-003` 수정 완료 |
|
||||||
|
| 확정 항목 plan 반영 | 충족 | `Task R7.3` / `P7-R3` 실행·검증 완료 |
|
||||||
|
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | focused/full 검증 기록 연결 |
|
||||||
|
|
||||||
|
**최종 결론:** 승인
|
||||||
|
|
||||||
|
**남은 항목:** 없음
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
- 2026-07-31 — `P7-R1` 실행으로 `HomePopularCommunityAdapter`의 유료 미구매 원본 이미지 로드 회귀를 복구했다. 변경 파일은 `HomePopularCommunityAdapter.kt`, `HomeMainFragmentLayoutTest.kt`, `prd.md`, `plan-task.md`, 이 리뷰 보고서다. RED는 `HomeMainFragmentLayoutTest.home popular community adapter does not load original image for locked paid post`를 추가한 뒤 focused test에서 `HomeMainFragmentLayoutTest.kt:1032` assertion 실패로 확인했다. GREEN은 유료 미구매 item의 `imageUrl`을 image loader에 전달하지 않고 `BlurTransformation` 경로를 제거하는 최소 수정 후 동일 focused test BUILD SUCCESSFUL로 확인했다. `FeedCommunityView.bind()` 호출은 유지하므로 lock overlay와 가격 표시는 기존 계약을 따른다. 후속 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`를 순차 실행했고 모두 성공했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation warning과 Gradle deprecation warning은 기존 경고다.
|
||||||
|
- 2026-07-31 — `P7-R2` 실행으로 `HomeMainFragmentLayoutTest`의 테스트명을 `유료 미구매 인기 커뮤니티는 원본 이미지를 로드하지 않는다`로 정정했다. Production code와 assertion은 변경하지 않았다. focused test, `HomeMainFragmentLayoutTest`·`V2ModalDialogTest` 직접 영향 회귀, `./gradlew :app:ktlintCheck`, `git diff --check`가 모두 성공했다. 남은 항목은 없다.
|
||||||
|
- 2026-07-31 — `P7-R3` 실행으로 `HomeMainFragmentLayoutTest`의 source 문자열 검사를 제거하고 `HomePopularCommunityAdapterTest.kt`를 추가했다. 실제 adapter/ViewHolder bind와 외부 이미지 loader 경계만 차단하는 테스트 2건으로 잠금 유료 원본 미로드·기존 Coil 요청 해제·drawable 제거·lock overlay/가격 유지, 무료·구매 완료 원본 URL 전달을 검증했다. 보안 분기를 일시적으로 직접 URL 전달로 바꾼 mutation 실행은 잠금 test 1건이 `HomePopularCommunityAdapterTest.kt:72`에서 실패했고, 원상 복구 후 전용 class 2건이 성공했다. 직접 영향 test·compile·ktlint가 성공했으며 전체 unit suite는 162 suites, 1,198 tests, 실패 0, error 0으로 성공했다. 최종 production 코드는 이번 Task에서 추가 변경하지 않았고 남은 항목은 없다.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Phase 8 사업자 정보 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 8 / 사업자 정보 접기·더보기 |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` Phase 8 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: 3줄 접힘, inline 더보기·접기, 짧은 본문 처리와 레이아웃 안정성을 확인한다.
|
||||||
|
- 포함: `HomeBusinessInfoBinder`, 홈 Fragment/XML, 관련 Robolectric test.
|
||||||
|
- 제외: 사업자 정보 문구의 제품·법무 검토.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
짧은 문구 no-action, 긴 문구 접힘/펼침 복귀, clickable span 범위와 별도 toggle View 미사용으로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 코드: `HomeBusinessInfoBinder.kt`, `HomeMainFragment.setUpBusinessInfo()`, `fragment_v2_main_home.xml`.
|
||||||
|
- 테스트: `HomeMainFragmentLayoutTest`의 사업자 정보 inline action 계약.
|
||||||
|
- 검증: home/modal focused test·resource merge·compile·ktlint 성공, 최신 전체 1,198 tests 중 실패 0·error 0.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
없음. 초기 별도 toggle 계획은 승인된 후속 inline action 변경으로 대체됐고 현재 테스트가 해당 계약을 검증한다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | binder·XML·test 대조 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | 확정 후보 없음 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | focused 검증 성공 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음
|
||||||
|
|
||||||
|
**남은 항목:** 없음. Phase 12의 사업자 정보 수동 항목은 사용자 확인으로 완료됐다.
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
수정 항목 없음.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Phase 9 상태 바인딩·기본 라우팅 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 9 / 상태 observe, schema, 라우팅·안정성 후속 Task |
|
||||||
|
| 기준 commit 또는 working tree | `a106548f` 기반 working tree |
|
||||||
|
| 리뷰 일자 | 2026-07-31 |
|
||||||
|
| 리뷰어 | Codex |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` Phase 9 |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- 목적: sample 제거, 실제 ViewModel 상태 바인딩, loading/toast, 최신 schema, 이미지·배너 안정성 후속 조치를 확인한다.
|
||||||
|
- 포함: `HomeMainFragment`, recommendation data/model/state, `HomeBannerBinder`, image loader와 관련 tests.
|
||||||
|
- 제외: 목적지별 확정 라우팅은 Phase 10·11·13·16에서 별도 판정.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
임시 데이터 미사용, 표시 상태 일관성, invalid data no-op, 중복 배너 식별, 캐시·대량 notify 회귀 방지로 판정했다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 코드: `HomeMainFragment.kt`, `HomeRecommendationModels.kt`, `HomeRecommendationMappers.kt`, `HomeBannerBinder.kt`, `ImageLoaderProvider`.
|
||||||
|
- 테스트: `HomeMainFragmentLayoutTest`, `BannerViewTest`, `ImageLoaderProviderTest`.
|
||||||
|
- 검증: home/modal focused test·merge·compile·ktlint 성공, 최신 전체 1,198 tests 중 실패 0·error 0.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
없음. 계획에 누적된 schema·cache·banner 후속 수정이 현재 코드와 테스트에 남아 있으며 sample content 경로는 production에서 제거됐다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | 상태·schema·안정성 코드 대조 완료 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | 확정 후보 없음 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | 자동 검증 성공 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음
|
||||||
|
|
||||||
|
**남은 항목:** 없음. Phase 12 화면 수동 항목은 사용자 확인으로 완료됐다.
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
수정 항목 없음.
|
||||||
@@ -89,10 +89,10 @@
|
|||||||
- 최근 대화 compact card item이다.
|
- 최근 대화 compact card item이다.
|
||||||
- Create: `app/src/main/res/layout/item_home_following_schedule.xml`
|
- Create: `app/src/main/res/layout/item_home_following_schedule.xml`
|
||||||
- 이달의 스케줄 item이다.
|
- 이달의 스케줄 item이다.
|
||||||
- Create: `app/src/main/res/layout/item_home_following_news_rank.xml`
|
- 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`
|
||||||
- ranking news item이다.
|
- 최근 소식은 기존 Feed 위젯 layout을 재사용한다.
|
||||||
- Create: `app/src/main/res/layout/item_home_following_news_content.xml`
|
- Delete: `app/src/main/res/layout/item_home_following_news_rank.xml`, `app/src/main/res/layout/item_home_following_news_content.xml`
|
||||||
- audio/photo content news item이다.
|
- Task 6.5에서 Feed 위젯 전환 후 obsolete 팔로잉 전용 최근 소식 layout을 제거했다.
|
||||||
- Modify: `app/src/main/res/values/strings.xml`
|
- Modify: `app/src/main/res/values/strings.xml`
|
||||||
- 팔로잉 섹션 title, `On Air`, `화보`, empty/error label을 추가한다.
|
- 팔로잉 섹션 title, `On Air`, `화보`, empty/error label을 추가한다.
|
||||||
- Modify: `app/src/main/res/values-en/strings.xml`
|
- 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/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/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/main/chat/model/ChatRoomMappers.kt`
|
||||||
- `app/src/main/java/kr/co/vividnext/sodalive/v2/common/CreatorActivityType.kt`
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/common/CreatorActivityType.kt`
|
||||||
- 작업:
|
- 작업:
|
||||||
@@ -262,6 +265,57 @@
|
|||||||
- Expected: PASS.
|
- Expected: PASS.
|
||||||
- Result: PASS. login-required, empty, content mapping, invalid chat 제외, schedule 순서 유지, `PHOTO_CONTENT` label, null rank filtering, `visibleFromAtUtc` formatter 전달이 검증됐다.
|
- 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 호출 연결
|
### 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 코드 리뷰: 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.
|
- 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 연결
|
### 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 코드 리뷰: 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은 발견하지 못했다.
|
- 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: 통합 검증과 문서 기록
|
### Phase 5: 통합 검증과 문서 기록
|
||||||
@@ -443,6 +615,8 @@
|
|||||||
- 2026-06-25 Figma `24:5682` 디자인 컨텍스트와 스크린샷 기준 정적 대조를 수행했다. 실제 기기/에뮬레이터에서의 수동 화면 확인은 아직 실행하지 않았다.
|
- 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 진행: 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-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 최근 소식 후속 수정: 먼저 `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만 출력됐다.
|
- 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 정합성 후속 수정
|
### Phase 7: 최근 소식 ranking 문장과 오디오 콘텐츠 feed 정합성 후속 수정
|
||||||
@@ -546,7 +818,7 @@
|
|||||||
- Expected: PASS.
|
- Expected: PASS.
|
||||||
- Result: PASS. ranking news가 크리에이터명/순위를 포함한 문장형 message와 순위 highlight range를 구성하고, content feed가 `88dp` 이미지 및 Figma compact feed 간격 기준을 갖도록 검증됐다.
|
- 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*"`
|
- Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`
|
||||||
- Expected: 팔로잉 관련 local unit/source test가 모두 PASS.
|
- Expected: 팔로잉 관련 local unit/source test가 모두 PASS.
|
||||||
@@ -564,6 +836,7 @@
|
|||||||
- 2026-06-30 후속 변경 검증: `./gradlew :app:compileDebugKotlin` PASS.
|
- 2026-06-30 후속 변경 검증: `./gradlew :app:compileDebugKotlin` PASS.
|
||||||
- 2026-06-30 후속 변경 검증: `git diff --check` 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-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 구현 위치 수정**
|
- [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: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.
|
- 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: 팔로잉 크리에이터 전체 버튼 후속 추가
|
### 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 팔로잉 크리에이터 `전체` 버튼 후속 검증: `./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만 출력됐다.
|
- 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 계약 반영
|
### 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 검증 참고: 최초 병렬 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만 출력됐다.
|
- 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 재구현
|
### 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 검증: `./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을 제거했다.
|
- 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 상세 이동
|
### 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만 출력됐다.
|
- 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
|
## 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-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-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.
|
- 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.
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ Figma `24:5682` 기준 상단 title bar와 tab bar는 기존 홈 화면 구조
|
|||||||
- `contentImageUrl` 또는 `imageUrl`이 null이면 타입별 기본 이미지 표시/숨김 정책을 구현 계획에서 확정한다.
|
- `contentImageUrl` 또는 `imageUrl`이 null이면 타입별 기본 이미지 표시/숨김 정책을 구현 계획에서 확정한다.
|
||||||
- item 터치 시 `type`과 payload의 target id(`creatorId`, `contentId`, `postId`)에 맞는 목적지로 이동한다.
|
- 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 추가 중 더 작은 변경을 구현 계획에서 선택한다.
|
- `PHOTO_CONTENT`는 기존 `FeedItem.Content`의 category 확장 또는 팔로잉 전용 item 추가 중 더 작은 변경을 구현 계획에서 선택한다.
|
||||||
- 각 섹션의 “더보기” chevron은 터치 액션만 연결하고, 실제 이동 목적지는 아직 만들지 않는다.
|
- 각 섹션의 “더보기” chevron은 터치 액션만 연결하고, 실제 이동 목적지는 아직 만들지 않는다.
|
||||||
|
|
||||||
@@ -298,6 +298,14 @@ Figma `24:5682` 기준 상단 title bar와 tab bar는 기존 홈 화면 구조
|
|||||||
- `HomeFollowingUiState.LoginRequired`와 `Error`는 이번 범위에서 empty 문구를 표시하지 않고 기존처럼 섹션을 숨긴다.
|
- `HomeFollowingUiState.LoginRequired`와 `Error`는 이번 범위에서 empty 문구를 표시하지 않고 기존처럼 섹션을 숨긴다.
|
||||||
- API/DTO/mapper 계약과 팔로잉 크리에이터 `전체` 버튼 노출 정책은 변경하지 않는다.
|
- 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
|
#### 2026-06-30 후속 수정 Requirements
|
||||||
- `CREATOR_RANKING` 최근 소식 본문은 순위 숫자만 단독 표시하지 않고, Figma `24:5717` 기준으로 크리에이터 이름과 순위를 포함한 문장형 메시지를 표시한다.
|
- `CREATOR_RANKING` 최근 소식 본문은 순위 숫자만 단독 표시하지 않고, Figma `24:5717` 기준으로 크리에이터 이름과 순위를 포함한 문장형 메시지를 표시한다.
|
||||||
- ranking 문장 안의 순위 텍스트는 기존 `FeedRankTextStyler` highlight range로 강조한다.
|
- ranking 문장 안의 순위 텍스트는 기존 `FeedRankTextStyler` highlight range로 강조한다.
|
||||||
@@ -326,7 +334,6 @@ PRD 작성 전 확인한 `v2` 패키지 하위 재사용 후보는 다음과 같
|
|||||||
- `ChatRoomListUiItem`, `ChatRoomMappers`, `ChatRoomTimeTextFormatter`: 최근 대화 데이터 변환/시간 표시 후보.
|
- `ChatRoomListUiItem`, `ChatRoomMappers`, `ChatRoomTimeTextFormatter`: 최근 대화 데이터 변환/시간 표시 후보.
|
||||||
|
|
||||||
#### Feed/News
|
#### Feed/News
|
||||||
- `FeedAdapter`: rank/live/content/community variant를 가진 feed list 후보.
|
|
||||||
- `FeedRankView`, `FeedContentView`, `FeedCommunityView`: 최근 소식 타입별 카드 후보.
|
- `FeedRankView`, `FeedContentView`, `FeedCommunityView`: 최근 소식 타입별 카드 후보.
|
||||||
- `FeedItem`, `FeedContentCategory`, `FeedRankHighlight`: 최근 소식 UI model 후보.
|
- `FeedItem`, `FeedContentCategory`, `FeedRankHighlight`: 최근 소식 UI model 후보.
|
||||||
|
|
||||||
|
|||||||
@@ -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해 기존 “확정 발견 사항 없음” 판정을 유지한다.
|
||||||
@@ -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에 유지한다.
|
||||||
@@ -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해 기존 “확정 발견 사항 없음” 판정을 유지한다.
|
||||||
111
docs/20260625_메인_홈_팔로잉_탭/reviews/phase12-empty-state-review.md
Normal file
111
docs/20260625_메인_홈_팔로잉_탭/reviews/phase12-empty-state-review.md
Normal 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에 유지한다.
|
||||||
@@ -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해 기존 “확정 발견 사항 없음” 판정을 유지한다.
|
||||||
@@ -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했으며 추가 확정 발견 사항은 없다.
|
||||||
@@ -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해 추가 확정 발견 사항이 없다.
|
||||||
@@ -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` 수정 완료 판정을 유지한다.
|
||||||
@@ -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은 보류 유지한다.
|
||||||
@@ -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에 유지한다.
|
||||||
@@ -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해 추가 확정 발견 사항이 없다.
|
||||||
@@ -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해 추가 확정 발견 사항이 없다.
|
||||||
@@ -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해 추가 확정 발견 사항이 없다.
|
||||||
@@ -339,3 +339,45 @@
|
|||||||
|
|
||||||
- 2026-07-06 Phase 5 검증: 홈 추천 라이브 클릭 source test를 RED 상태로 먼저 확인한 뒤, `HomeMainFragment`에서 로그인 guard, 상세 조회, `roomDetail.isAdult` 기반 성인 guard, 기존 v2 `CreatorChannelLiveCoordinator` 입장 위임을 연결했다. 이후 `HomeMainFragmentLoginGuardSourceTest`, `HomeOnAirLiveEntryPolicyTest`, `compileDebugKotlin`, `git diff --check`가 모두 통과했다.
|
- 2026-07-06 Phase 5 검증: 홈 추천 라이브 클릭 source test를 RED 상태로 먼저 확인한 뒤, `HomeMainFragment`에서 로그인 guard, 상세 조회, `roomDetail.isAdult` 기반 성인 guard, 기존 v2 `CreatorChannelLiveCoordinator` 입장 위임을 연결했다. 이후 `HomeMainFragmentLoginGuardSourceTest`, `HomeOnAirLiveEntryPolicyTest`, `compileDebugKotlin`, `git diff --check`가 모두 통과했다.
|
||||||
- 2026-07-06 Phase 5 리뷰 반영: 코드 품질 리뷰에서 `getRoomDetail` 콜백 lifecycle guard 부재와 여러 ViewModel의 `LoadingDialog` 직접 제어 race가 지적되어, 콜백 초입에 `if (!isAdded || view == null) return@getRoomDetail`를 추가하고 recommendation/ranking/following/live entry loading state를 `updateLoadingDialog()`로 통합했다. 이후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest" --no-daemon`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveEntryPolicyTest" --no-daemon`, `./gradlew :app:compileDebugKotlin --no-daemon`, `git diff --check`가 모두 통과했다.
|
- 2026-07-06 Phase 5 리뷰 반영: 코드 품질 리뷰에서 `getRoomDetail` 콜백 lifecycle guard 부재와 여러 ViewModel의 `LoadingDialog` 직접 제어 race가 지적되어, 콜백 초입에 `if (!isAdded || view == null) return@getRoomDetail`를 추가하고 recommendation/ranking/following/live entry loading state를 `updateLoadingDialog()`로 통합했다. 이후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest" --no-daemon`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveEntryPolicyTest" --no-daemon`, `./gradlew :app:compileDebugKotlin --no-daemon`, `git diff --check`가 모두 통과했다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 6: 전체 라이브 리스트 상단 제목 정정
|
||||||
|
|
||||||
|
**Phase 결과:** 메인 홈 추천 라이브 섹션의 `전체`에서 진입한 화면 상단에 locale과 관계없이 `On Air`가 표시된다.
|
||||||
|
|
||||||
|
#### Task 6.1: `live_now` locale 리소스 정정
|
||||||
|
|
||||||
|
**Goal 실행 `P6-T1`:** 기존 화면 연결을 유지하면서 기본·영어·일본어 `live_now` 값을 `On Air`로 통일한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `prd.md` §13 승인과 `HomeOnAirLiveActivity`의 `R.string.live_now` 소비 확인.
|
||||||
|
- **완료 증거:** 세 locale 리소스 값 대조, debug resource merge, focused 회귀 테스트, `git diff --check` 결과.
|
||||||
|
- **범위 밖:** Activity/layout/navigation 변경, 홈 추천 섹션 제목 변경, 레거시 `screen_live_now_all_title` 변경.
|
||||||
|
- **TDD 예외 사유:** 실행 로직이나 분기가 없는 UI 카피 전용 변경이며 영어 리소스는 이미 목표값이다. 신규 source test는 XML 구현 문자열을 중복 고정하므로 추가하지 않는다.
|
||||||
|
- **대체 검증 방법:** 변경 전 기본·일본어 값이 각각 `지금 라이브 중`, `ただいま配信中`임을 확인하고, 변경 후 기본·영어·일본어 값을 직접 대조한 뒤 resource merge와 기존 진입 경로 회귀 테스트를 실행한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `app/src/main/res/values/strings.xml`
|
||||||
|
- Modify: `app/src/main/res/values-ja/strings.xml`
|
||||||
|
- Verify unchanged: `app/src/main/res/values-en/strings.xml`
|
||||||
|
- Verify unchanged: `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt`
|
||||||
|
|
||||||
|
- [x] 기본 locale의 `live_now` 값을 `On Air`로 변경한다.
|
||||||
|
- [x] 일본어 locale의 `live_now` 값을 `On Air`로 변경한다.
|
||||||
|
- [x] 영어 locale과 세 locale의 최종값이 모두 `On Air`인지 확인한다.
|
||||||
|
- [x] `./gradlew --no-daemon :app:mergeDebugResources`를 실행한다.
|
||||||
|
- [x] `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest" --tests "kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveEntryPolicyTest"`를 실행한다.
|
||||||
|
- [x] 가능한 연결 기기/에뮬레이터에서 진입 화면 상단 제목을 확인하고, 불가능하면 사유를 Verification Log에 기록한다.
|
||||||
|
- [x] `git diff --check`와 변경 파일 범위를 확인한다.
|
||||||
|
|
||||||
|
#### Phase 6 검증 시나리오
|
||||||
|
- Happy path: 기본 locale에서 `HomeOnAirLiveActivity`가 소비하는 `live_now`의 병합값이 정확히 `On Air`다.
|
||||||
|
- Edge: 영어·일본어 locale에서도 `live_now`가 정확히 `On Air`이며 빈 목록/오류 상태와 독립적인 toolbar 설정은 유지된다.
|
||||||
|
- Adjacent regression: 홈 추천 `전체` 진입 경로와 `screen_live_now_all_title` 값은 변경되지 않는다.
|
||||||
|
|
||||||
|
## Verification Log 추가 기록
|
||||||
|
- 2026-08-03 Phase 6 계획: 기존 PRD §7의 `On Air` 제목 요구사항과 구현의 기본·일본어 `live_now` 값 불일치를 확인했다. 사용자 승인에 따라 리소스 값만 정정하고 화면 코드와 레거시 제목은 유지한다.
|
||||||
|
- 2026-08-03 Phase 6 구현: 기본·일본어 `live_now` 값을 `On Air`로 정정했고 영어값이 이미 `On Air`임을 확인했다. `screen_live_now_all_title`은 세 locale 모두 기존값을 유지했다.
|
||||||
|
- 2026-08-03 Phase 6 자동 검증: `xmllint --noout` 성공, `./gradlew --no-daemon :app:mergeDebugResources` BUILD SUCCESSFUL, focused 2개 테스트 클래스 BUILD SUCCESSFUL, 전체 `:app:testDebugUnitTest` BUILD SUCCESSFUL, `:app:assembleDebug` BUILD SUCCESSFUL을 확인했다. 병합 산출물의 기본·일본어 `live_now`도 `On Air`였다.
|
||||||
|
- 2026-08-03 Phase 6 실제 화면 검증: `Pixel_9_Pro` AVD에 debug APK를 설치하고 정상 홈 화면을 열었으나 현재 API 응답에 라이브 항목이 없어 `전체` 진입점이 렌더링되지 않았다. 내부 Activity 직접 실행은 `android:exported=false`로 거부됐고, 일회성 계측 QA는 첫 시도에서 DEX 공백 메서드명 제약, 두 번째 시도에서 로그인 화면 전환으로 Activity가 종료돼 assertion 전에 실패했다. 임시 계측 테스트는 제거했으며 대상 화면 스크린샷 검증은 미완료로 기록한다.
|
||||||
|
- 2026-08-03 Phase 6 리뷰 반영: Oracle 리뷰 `REV-P6-001`에서 실제 `git diff --check` 성공과 미완료 체크박스의 문서 불일치를 확인했다. 체크박스를 완료로 정정했고 프로덕션 변경이 리소스 2개 각 1줄뿐이며 기본·영어·일본어 `live_now`가 모두 `On Air`임을 재확인했다.
|
||||||
|
|||||||
@@ -156,3 +156,11 @@ data class HomeOnAirLiveResponse(
|
|||||||
- 로그인 통과 후 `LiveViewModel.getRoomDetail(roomId)`를 호출하고, 상세 응답의 `isAdult == true`이면 `requiresAdultContentAccess = true`로 성인 인증/성인 콘텐츠 설정 guard를 한 번 더 통과한 뒤 입장한다.
|
- 로그인 통과 후 `LiveViewModel.getRoomDetail(roomId)`를 호출하고, 상세 응답의 `isAdult == true`이면 `requiresAdultContentAccess = true`로 성인 인증/성인 콘텐츠 설정 guard를 한 번 더 통과한 뒤 입장한다.
|
||||||
- 실제 입장 분기는 기존 `LiveFragment.enterLiveRoom`과 동일한 `manager.id`, `price`, `isPaid`, `isPrivateRoom`, `beginDateTimeUtc` 기준을 따른다.
|
- 실제 입장 분기는 기존 `LiveFragment.enterLiveRoom`과 동일한 `manager.id`, `price`, `isPaid`, `isPrivateRoom`, `beginDateTimeUtc` 기준을 따른다.
|
||||||
- 기존 레거시 `LiveFragment`, `LiveViewModel`, `LiveRepository`는 직접 수정하지 않는다.
|
- 기존 레거시 `LiveFragment`, `LiveViewModel`, `LiveRepository`는 직접 수정하지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Follow-up: 전체 라이브 리스트 상단 제목 정정
|
||||||
|
- 메인 홈 추천 탭 최상단 라이브 섹션의 `전체`를 터치해 여는 `HomeOnAirLiveActivity` 상단 제목은 locale과 관계없이 정확히 `On Air`로 표시한다.
|
||||||
|
- 기존 `R.string.live_now` 연결, 화면 구조, 뒤로가기, 목록 상태와 라이브 입장 동작은 유지한다.
|
||||||
|
- 홈 추천 섹션 제목과 레거시 `screen_live_now_all_title`은 변경하지 않는다.
|
||||||
|
- 성공 기준은 기본·영어·일본어 리소스의 `live_now` 값이 모두 `On Air`이고 debug resource merge가 성공하는 것이다.
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Phase 6 On Air 제목 변경 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 6 / Task 6.1 |
|
||||||
|
| 기준 commit 또는 working tree | `dd4a5167a9e9d6e55fd0629534342e8df66fe3f5` 기준 working tree |
|
||||||
|
| 리뷰 일자 | 2026-08-03 |
|
||||||
|
| 리뷰어 | Oracle |
|
||||||
|
| 기준 문서 | `prd.md` §13, `plan-task.md` Phase 6 |
|
||||||
|
| 리뷰 상태 | 수정 검증 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
- `HomeOnAirLiveActivity` 상단 제목이 locale과 관계없이 정확히 `On Air`인지 확인한다.
|
||||||
|
- 기본·영어·일본어 리소스, 기존 Activity 바인딩, 인접 레거시 제목과 문서 추적성을 검토한다.
|
||||||
|
- 화면 구조, 내비게이션, API와 라이브 입장 로직은 변경 대상에서 제외한다.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
- 확정 PRD와 구현이 일치해야 한다.
|
||||||
|
- 프로덕션 변경은 필요한 locale 리소스에만 한정해야 한다.
|
||||||
|
- 자동 검증 결과와 `plan-task.md` 체크박스가 일치해야 한다.
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- `HomeOnAirLiveActivity.kt:51`의 `R.string.live_now` 바인딩.
|
||||||
|
- `values/strings.xml:164`, `values-en/strings.xml:165`, `values-ja/strings.xml:165`의 `On Air` 값.
|
||||||
|
- 세 locale의 `screen_live_now_all_title` 기존값 유지.
|
||||||
|
- `mergeDebugResources`, 전체 `testDebugUnitTest`, `assembleDebug`, `git diff --check` 성공.
|
||||||
|
- `Pixel_9_Pro` AVD 실제 화면 검증 제한과 제거된 일회성 계측 QA 기록.
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `REV-P6-001` | Low | 수정 완료 | 성공한 diff 검증 체크박스가 미완료로 남음 | `P6-T1` | 없음 |
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
### REV-P6-001 — 검증 기록과 체크박스 불일치
|
||||||
|
|
||||||
|
- **관찰:** `git diff --check`는 성공했지만 `plan-task.md`의 해당 체크박스가 미완료였다.
|
||||||
|
- **영향:** 제품 동작 영향은 없으나 Phase 6 완료 추적성이 실제 검증과 불일치했다.
|
||||||
|
- **조치:** 체크박스를 완료로 정정하고 Oracle 재검토 대상으로 등록했다.
|
||||||
|
|
||||||
|
## 7. plan·goal 전환
|
||||||
|
|
||||||
|
- 코드 수정이나 회귀 테스트가 필요한 확정 항목이 아니므로 신규 회귀 수정 Task와 goal은 만들지 않는다.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | 리소스, 소비처, 문서, 검증 결과 확인 |
|
||||||
|
| 확정 항목 반영 | 충족 | `REV-P6-001` 체크박스 정정 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | `plan-task.md` Verification Log |
|
||||||
|
|
||||||
|
**최종 결론:** 수정 검증 완료
|
||||||
|
|
||||||
|
**남은 항목:** 대상 화면 스크린샷은 라이브 데이터 및 로그인 상태 제약으로 확보하지 못했으며 `plan-task.md`에 사유를 기록했다.
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
- 2026-08-03: `git diff --check` 성공과 변경 범위가 리소스 2개 및 작업 문서로 제한됨을 재확인했다.
|
||||||
|
- 2026-08-03: Oracle 재검토 결과 `VERDICT: APPROVE`, 차단 항목 없음으로 판정됐다.
|
||||||
213
docs/20260803_내_채널_팔로워_목록_연결/plan-task.md
Normal file
213
docs/20260803_내_채널_팔로워_목록_연결/plan-task.md
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
# 내 채널 팔로워 목록 연결 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:test-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 내 채널 상단 `팔로워 n명` 터치 시 기존 레거시 팔로워 목록 화면으로 이동한다.
|
||||||
|
|
||||||
|
**Architecture:** `CreatorChannelActivity`가 이미 헤더와 현재 `creatorId`를 소유하므로 Activity 내부에서 클릭 리스너를 추가한다. 목적지는 레거시 `UserFollowerListActivity`이며 기존 호출 방식처럼 `Constants.EXTRA_USER_ID`만 전달한다.
|
||||||
|
|
||||||
|
**Tech Stack:** Android Kotlin, ViewBinding, JUnit source test, Gradle `:app:testDebugUnitTest`, `:app:assembleDebug`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| 문서 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 상태 | 구현 완료 |
|
||||||
|
| 작성일 | `2026-08-03` |
|
||||||
|
| 요구사항 기준 | `docs/20260803_내_채널_팔로워_목록_연결/prd.md` |
|
||||||
|
| API 기준 | 기존 레거시 `UserFollowerListActivity` / `ExplorerApi.getFollowerList` |
|
||||||
|
| 현재 Phase | Phase 1 |
|
||||||
|
| 현재 활성 Goal | 없음 |
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
내 채널 소유자가 v2 크리에이터 채널의 `팔로워 n명`을 눌러 기존 레거시 팔로워 목록을 확인한다.
|
||||||
|
|
||||||
|
## 현재 상태
|
||||||
|
|
||||||
|
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|
||||||
|
|---:|---|---:|---|---|
|
||||||
|
| 1 | 완료 | `1/1` | 없음 | 직접 Activity 기동은 `not exported` 보안 정책상 adb에서 차단됨 |
|
||||||
|
|
||||||
|
## 범위
|
||||||
|
|
||||||
|
### 포함
|
||||||
|
|
||||||
|
- `CreatorChannelActivity`의 `tvFollowerCount` 클릭 연결.
|
||||||
|
- 기존 `UserFollowerListActivity`에 `Constants.EXTRA_USER_ID = creatorId` 전달.
|
||||||
|
- owner 전용 동작과 0명 허용 계약을 source test로 고정.
|
||||||
|
|
||||||
|
### 제외
|
||||||
|
|
||||||
|
- 신규 v2 팔로워/팔로잉 목록 화면.
|
||||||
|
- 레거시 `UserFollowerListActivity` 및 API 수정.
|
||||||
|
- 타인 채널의 팔로워 수 클릭 동작.
|
||||||
|
- 팔로워/팔로잉 문구 변경.
|
||||||
|
|
||||||
|
## 기술적 제약
|
||||||
|
|
||||||
|
- 레거시 코드는 직접 수정하지 않는다.
|
||||||
|
- 새 dependency, router, helper abstraction을 추가하지 않는다.
|
||||||
|
- 기존 `CreatorChannelActivitySourceTest`의 source-test 패턴을 따른다.
|
||||||
|
- production 구현은 RED 확인 후 진행한다.
|
||||||
|
|
||||||
|
## Phase 1
|
||||||
|
|
||||||
|
**Phase 결과:** 내 채널 헤더의 `팔로워 n명`이 레거시 팔로워 목록으로 이동한다.
|
||||||
|
|
||||||
|
**선행조건:** PRD 승인 완료.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P1-T1`과 `P1-GATE` 완료, 검증 기록 누적.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 1.1 내 채널 팔로워 수 클릭 연결
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T1`:** `CreatorChannelActivity`에서 owner의 `tvFollowerCount` 클릭을 `UserFollowerListActivity`로 연결한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `FOLLOWER-LINK-001~003` 확정.
|
||||||
|
- **완료 증거:** RED/GREEN source test, debug build, 수동 검증 기록.
|
||||||
|
- **범위 밖:** 레거시 화면 수정, 신규 v2 화면 작성, 타인 채널 동작 추가.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `creatorId: Long`, `currentHeader?.isOwner`, `Constants.EXTRA_USER_ID`.
|
||||||
|
- Produces: `Intent(this, UserFollowerListActivity::class.java).putExtra(Constants.EXTRA_USER_ID, creatorId)` 실행 경로.
|
||||||
|
|
||||||
|
- [x] **RED:** `CreatorChannelActivitySourceTest`에 `내 채널 팔로워 수 클릭 source는 레거시 팔로워 목록으로 이동한다` 테스트를 추가한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@Test
|
||||||
|
fun `내 채널 팔로워 수 클릭 source는 레거시 팔로워 목록으로 이동한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||||
|
).readText()
|
||||||
|
val clickSource = sourceSection(
|
||||||
|
source = source,
|
||||||
|
startMarker = "private fun onFollowerCountClicked()",
|
||||||
|
endMarker = "private fun bindHeader"
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(source.contains("import kr.co.vividnext.sodalive.explorer.profile.follow.UserFollowerListActivity"))
|
||||||
|
assertTrue(source.contains("binding.tvFollowerCount.setOnClickListener"))
|
||||||
|
assertTrue(clickSource.contains("if (currentHeader?.isOwner != true) return"))
|
||||||
|
assertTrue(clickSource.contains("Intent(this, UserFollowerListActivity::class.java)"))
|
||||||
|
assertTrue(clickSource.contains("putExtra(Constants.EXTRA_USER_ID, creatorId)"))
|
||||||
|
assertFalse(clickSource.contains("followerCount > 0"))
|
||||||
|
assertFalse(source.contains("FollowingCreatorActivity::class.java"))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **RED 확인:** focused test를 실행해 `start marker not found: private fun onFollowerCountClicked()` 실패를 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest.내 채널 팔로워 수 클릭 source는 레거시 팔로워 목록으로 이동한다"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** `CreatorChannelActivity`에 import, click listener, 최소 handler를 추가한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.follow.UserFollowerListActivity
|
||||||
|
|
||||||
|
binding.tvFollowerCount.setOnClickListener { onFollowerCountClicked() }
|
||||||
|
|
||||||
|
private fun onFollowerCountClicked() {
|
||||||
|
if (currentHeader?.isOwner != true) return
|
||||||
|
startActivity(
|
||||||
|
Intent(this, UserFollowerListActivity::class.java)
|
||||||
|
.putExtra(Constants.EXTRA_USER_ID, creatorId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN 확인:** 같은 focused test가 성공하는지 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest.내 채널 팔로워 수 클릭 source는 레거시 팔로워 목록으로 이동한다"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **REFACTOR:** 불필요한 변경이 없는지 확인하고 아래 직접 영향 회귀를 실행한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"
|
||||||
|
./gradlew :app:assembleDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
### 검증 방법
|
||||||
|
|
||||||
|
#### Phase 1 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P1-GATE`:** 문서와 코드가 요구사항을 충족하는지 최종 판정한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"
|
||||||
|
./gradlew :app:assembleDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** test/build 모두 exit code 0. `CreatorChannelActivity`는 `UserFollowerListActivity`만 참조하고 `FollowingCreatorActivity`를 참조하지 않는다.
|
||||||
|
|
||||||
|
수동 검증:
|
||||||
|
|
||||||
|
- [ ] Android 실행 환경에서 내 채널 진입 후 `팔로워 n명` 터치 시 `UserFollowerListActivity`가 열리는지 확인한다.
|
||||||
|
- [x] 실행 환경이 없으면 사용 가능한 명령과 부재 사유를 Progress에 기록한다.
|
||||||
|
|
||||||
|
## 실행 순서와 의존성
|
||||||
|
|
||||||
|
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|
||||||
|
|---:|---|---|---|---|
|
||||||
|
| 1 | `P1-T1` | PRD 승인 | 아니요 | 테스트 실패 원인을 해당 Task에서 수정 |
|
||||||
|
| 2 | `P1-GATE` | `P1-T1` 완료 | 아니요 | 실패 소유 Task에 회귀 수정 기록 추가 |
|
||||||
|
|
||||||
|
```text
|
||||||
|
P1-T1 -> P1-GATE
|
||||||
|
```
|
||||||
|
|
||||||
|
## 변경 금지 항목
|
||||||
|
|
||||||
|
- 레거시 `UserFollowerListActivity`를 수정하지 않는다.
|
||||||
|
- 새 화면, dependency, router abstraction을 만들지 않는다.
|
||||||
|
- test를 삭제, skip, 완화하지 않는다.
|
||||||
|
- 민감정보를 로그, Toast, 문서에 기록하지 않는다.
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
기존 기록을 삭제하거나 덮어쓰지 않고 실제 실행 결과를 누적한다.
|
||||||
|
|
||||||
|
### P1-T1 1차 실행 — 2026-08-03
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: `CreatorChannelActivity`의 `tvFollowerCount` 클릭을 owner 전용 `UserFollowerListActivity` 진입으로 연결하고 source test를 추가했다.
|
||||||
|
- 왜: `FOLLOWER-LINK-001~003` 구현 기준 충족.
|
||||||
|
- 어떻게:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest.내 채널 팔로워 수 클릭 source는 레거시 팔로워 목록으로 이동한다"` — RED 실패, `CreatorChannelActivitySourceTest.kt:2378`에서 `onFollowerCountClicked()` source marker 부재로 실패.
|
||||||
|
- 같은 focused 명령 재실행 — GREEN 성공, exit code 0.
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` — 성공, exit code 0.
|
||||||
|
- `./gradlew :app:testDebugUnitTest` — 성공, exit code 0.
|
||||||
|
- `./gradlew :app:assembleDebug` — 성공, exit code 0.
|
||||||
|
- `adb install -r "app/build/outputs/apk/debug/app-debug.apk"` — 성공.
|
||||||
|
- `adb shell am start -W -n kr.co.vividnext.sodalive.debug/kr.co.vividnext.sodalive.explorer.profile.follow.UserFollowerListActivity --el extra_user_id 1` — `not exported` 보안 정책으로 직접 Activity 기동 차단.
|
||||||
|
- `adb shell monkey -p kr.co.vividnext.sodalive.debug 1` — debug 앱 실행 이벤트 주입 성공.
|
||||||
|
- 남은 항목: 로그인된 내 채널 실제 클릭 검증은 인증 상태가 있는 수동 QA 환경에서 확인 필요.
|
||||||
|
- 다음 행동: 없음.
|
||||||
|
|
||||||
|
## Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `2026-08-03` | `DEC-001` | 확정 | `UserFollowerListActivity`를 목적지로 사용한다. | 내 채널을 팔로우하는 사용자 목록 요구. | `P1-T1`, `prd.md#15-decision-log` |
|
||||||
|
|
||||||
|
## 발견된 문제
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 없음 | Low | 보류 | 현재 발견된 범위 내 문제 없음 | 없음 | 없음 |
|
||||||
|
|
||||||
|
## Verification Log
|
||||||
|
|
||||||
|
- `2026-08-03`: RED/GREEN source test, 직접 영향 source test, 전체 debug unit test, debug build가 모두 성공했다. 연결된 Android 기기에는 APK 설치와 앱 실행 이벤트 주입까지 확인했으나, 레거시 Activity 직접 기동은 `not exported` 보안 정책으로 차단되어 로그인된 내 채널에서의 실제 클릭은 별도 QA 환경에서 확인해야 한다.
|
||||||
|
- `2026-08-03`: 리뷰 게이트를 수행했고 `reviews/phase1-follower-list-link-review.md`에 확정 발견 사항 없음으로 기록했다.
|
||||||
119
docs/20260803_내_채널_팔로워_목록_연결/prd.md
Normal file
119
docs/20260803_내_채널_팔로워_목록_연결/prd.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# 내 채널 팔로워 목록 연결 PRD
|
||||||
|
|
||||||
|
## 문서 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 문서 상태 | 구현 완료 |
|
||||||
|
| 작성일 | `2026-08-03` |
|
||||||
|
| 최종 수정일 | `2026-08-03` |
|
||||||
|
| 대상 제품 | v2 크리에이터 채널 내 채널 팔로워 목록 연결 |
|
||||||
|
| 작성자·결정권자 | 사용자, Sisyphus |
|
||||||
|
| 관련 API Contract | 기존 레거시 `UserFollowerListActivity` / `ExplorerApi.getFollowerList` 재사용 |
|
||||||
|
| 관련 구현 계획 | `docs/20260803_내_채널_팔로워_목록_연결/plan-task.md` |
|
||||||
|
| 관련 review | `docs/20260803_내_채널_팔로워_목록_연결/reviews/phase1-follower-list-link-review.md` |
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
내 채널 소유자는 v2 크리에이터 채널 상단의 `팔로워 n명` 영역을 눌러 자신을 팔로우하는 사용자 목록을 확인할 수 있어야 한다. 신규 v2 화면을 만들지 않고 기존 레거시 `UserFollowerListActivity`를 그대로 연결한다.
|
||||||
|
|
||||||
|
## 2. Problem Statement
|
||||||
|
|
||||||
|
현재 v2 크리에이터 채널은 `팔로워 n명`을 표시하지만 클릭 동작이 없다. 사용자는 내 채널을 팔로우하는 사용자 목록으로 이동하려면 레거시 프로필 경로를 우회해야 한다.
|
||||||
|
|
||||||
|
문제를 해결했다는 판단은 내 채널에서 `팔로워 n명`을 터치했을 때 `Constants.EXTRA_USER_ID`에 현재 `creatorId`가 담긴 `UserFollowerListActivity`가 실행되는 것으로 한다.
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
|
||||||
|
### 3.1 제품 목표
|
||||||
|
|
||||||
|
- 내 채널 소유자는 채널 상단 `팔로워 n명`에서 팔로워 목록 화면으로 이동할 수 있다.
|
||||||
|
- 기존 레거시 팔로워 목록 화면과 API 계약을 재사용한다.
|
||||||
|
- 타인 채널에서는 이번 연결을 노출하지 않는다.
|
||||||
|
|
||||||
|
### 3.2 UX 목표
|
||||||
|
|
||||||
|
- 기존 문구, 배치, 수치 표시를 변경하지 않는다.
|
||||||
|
- 팔로워 수가 0명이어도 같은 목록 화면으로 진입한다.
|
||||||
|
- 별도 안내, 다이얼로그, 신규 화면을 추가하지 않는다.
|
||||||
|
|
||||||
|
## 4. Non-Goals
|
||||||
|
|
||||||
|
- `FollowingCreatorActivity` 연결은 구현하지 않는다. 이 화면은 현재 사용자가 팔로잉한 크리에이터 목록이다.
|
||||||
|
- 신규 v2 팔로워 목록 화면을 만들지 않는다.
|
||||||
|
- 레거시 `UserFollowerListActivity`, 레거시 API, 레거시 레이아웃은 수정하지 않는다.
|
||||||
|
- `팔로워 n명` 문구를 `팔로잉 n명`으로 변경하지 않는다.
|
||||||
|
- 타인 채널의 팔로워 수 클릭 동작은 추가하지 않는다.
|
||||||
|
|
||||||
|
## 5. Target Users and Permissions
|
||||||
|
|
||||||
|
| 사용자 | 목표 | 주요 작업 | 사용 환경 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 내 채널 소유자 | 내 채널을 팔로우하는 사용자 확인 | `팔로워 n명` 터치 후 목록 조회 | Android 앱 |
|
||||||
|
|
||||||
|
권한 기준은 기존 `CreatorChannelActivity` 진입의 로그인 가드와 `UserFollowerListActivity`의 기존 API 인증을 따른다.
|
||||||
|
|
||||||
|
## 6. 핵심 사용자 흐름
|
||||||
|
|
||||||
|
1. 사용자가 본인 크리에이터 채널에 진입한다.
|
||||||
|
2. 채널 상단 헤더의 `팔로워 n명`을 터치한다.
|
||||||
|
3. 앱은 기존 `UserFollowerListActivity`를 실행하며 `Constants.EXTRA_USER_ID`에 현재 `creatorId`를 전달한다.
|
||||||
|
4. 레거시 화면이 기존 방식대로 팔로워 목록을 조회하고 표시한다.
|
||||||
|
|
||||||
|
## 7. 정보 구조와 라우팅
|
||||||
|
|
||||||
|
```text
|
||||||
|
CreatorChannelActivity
|
||||||
|
tvFollowerCount click
|
||||||
|
-> UserFollowerListActivity
|
||||||
|
extra: Constants.EXTRA_USER_ID = creatorId
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 기능 요구사항
|
||||||
|
|
||||||
|
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `FOLLOWER-LINK-001` | 확정 | 내 채널 소유자가 `팔로워 n명`을 누르면 레거시 팔로워 목록으로 이동한다. | `UserFollowerListActivity` Intent가 생성되고 `Constants.EXTRA_USER_ID`에 현재 `creatorId`가 전달된다. | `P1-T1` |
|
||||||
|
| `FOLLOWER-LINK-002` | 확정 | 타인 채널에서는 이번 클릭 연결이 동작하지 않는다. | `currentHeader.isOwner != true`이면 목록 Activity를 시작하지 않는다. | `P1-T1` |
|
||||||
|
| `FOLLOWER-LINK-003` | 확정 | 팔로워 수 0명도 동일하게 목록 화면으로 진입한다. | 클릭 조건에 `followerCount > 0` 제한이 없다. | `P1-T1` |
|
||||||
|
|
||||||
|
## 9. UI/UX Expectations
|
||||||
|
|
||||||
|
- 기존 `tv_follower_count` 표시를 그대로 사용한다.
|
||||||
|
- 레이아웃, 색상, 문구, 접근성 리소스는 변경하지 않는다.
|
||||||
|
- 클릭 가능성 외 시각적 embellishment는 추가하지 않는다.
|
||||||
|
|
||||||
|
## 10. API 계약
|
||||||
|
|
||||||
|
신규 API 계약은 없다. `UserFollowerListActivity`가 기존 `ExplorerApi.getFollowerList(userId, page, size, Authorization)` 흐름을 재사용한다.
|
||||||
|
|
||||||
|
## 11. 보안과 데이터 취급
|
||||||
|
|
||||||
|
- 전달 값은 기존 내부 식별자 `creatorId`뿐이다.
|
||||||
|
- 토큰, API URL, 민감정보를 로그나 Toast에 추가로 노출하지 않는다.
|
||||||
|
|
||||||
|
## 12. 성공 기준
|
||||||
|
|
||||||
|
- [x] `CreatorChannelActivitySourceTest`가 클릭 연결, owner 제한, extra 전달 계약을 검증한다.
|
||||||
|
- [x] focused unit test가 RED 후 GREEN으로 통과한다.
|
||||||
|
- [x] `:app:assembleDebug`가 성공한다.
|
||||||
|
- [x] 실제 Android 표면 검증 또는 실행 환경 부재 사유가 계획 문서에 기록된다.
|
||||||
|
|
||||||
|
## 13. Open Questions
|
||||||
|
|
||||||
|
| ID | 상태 | 결정 필요 사항 | 현재 권고 | 결정 주체 | 결정 기한/시점 | 영향 Goal |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 없음 | 확정 | 추가 결정 없음 | 권장 설계 승인됨 | 사용자 | 완료 | `P1-T1` |
|
||||||
|
|
||||||
|
## 14. 요구사항 추적표
|
||||||
|
|
||||||
|
| 요구사항 범위 | API Contract | 계획 Phase | Goal | 자동 검증 | 수동 검증 |
|
||||||
|
|---|---|---:|---|---|---|
|
||||||
|
| `FOLLOWER-LINK-001~003` | 기존 레거시 API 재사용 | 1 | `P1-T1`, `P1-GATE` | `CreatorChannelActivitySourceTest` | 내 채널 `팔로워 n명` 터치 |
|
||||||
|
|
||||||
|
## 15. Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계약·Goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `2026-08-03` | `DEC-001` | 확정 | `FollowingCreatorActivity`가 아닌 `UserFollowerListActivity`로 연결한다. | 전자는 내가 팔로잉한 크리에이터 목록이고, 후자는 채널 팔로워 목록이다. | `FOLLOWER-LINK-001`, `P1-T1` |
|
||||||
|
| `2026-08-03` | `DEC-002` | 확정 | v2 신규 화면을 만들지 않고 레거시 화면을 재사용한다. | 사용자 명시 요청. | `FOLLOWER-LINK-001`, `P1-T1` |
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Phase 1 팔로워 목록 연결 코드 리뷰 보고서
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 1 / `P1-T1` |
|
||||||
|
| 기준 commit 또는 working tree | working tree 변경분 |
|
||||||
|
| 리뷰 일자 | `2026-08-03` |
|
||||||
|
| 리뷰어 | Sisyphus-Junior |
|
||||||
|
| 기준 문서 | `docs/20260803_내_채널_팔로워_목록_연결/prd.md`, `docs/20260803_내_채널_팔로워_목록_연결/plan-task.md` |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
### 목적
|
||||||
|
|
||||||
|
- v2 내 채널 `팔로워 n명` 클릭 연결이 확정 요구사항을 충족하는지 확인한다.
|
||||||
|
- 테스트, 구현, 문서, 검증 기록이 같은 목적지를 가리키는지 확인한다.
|
||||||
|
|
||||||
|
### 포함 범위
|
||||||
|
|
||||||
|
- 코드: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
- 테스트: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
|
||||||
|
- 문서: `prd.md`, `plan-task.md`
|
||||||
|
- 수동 검증: debug APK 설치, 앱 실행 이벤트, 직접 Activity 기동 가능 여부
|
||||||
|
|
||||||
|
### 제외 범위
|
||||||
|
|
||||||
|
- 레거시 `UserFollowerListActivity` 내부 동작 수정.
|
||||||
|
- 신규 v2 팔로워 목록 화면.
|
||||||
|
- 타인 채널 팔로워 수 클릭 동작.
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
| 심각도 | 기준 |
|
||||||
|
|---|---|
|
||||||
|
| Blocker | 요구사항 미충족, 잘못된 목적지 연결, build/test 실패 |
|
||||||
|
| High | owner 제한 또는 extra 전달 누락 |
|
||||||
|
| Medium | 문서와 구현 불일치 |
|
||||||
|
| Low | 완료 판정을 막지 않는 잔여 QA 제약 |
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
### 문서와 코드
|
||||||
|
|
||||||
|
- 요구사항: `FOLLOWER-LINK-001~003`
|
||||||
|
- 계획: `P1-T1`, `P1-GATE`
|
||||||
|
- 코드: `CreatorChannelActivity.kt`의 `tvFollowerCount` listener와 `onFollowerCountClicked()`
|
||||||
|
- 테스트: `CreatorChannelActivitySourceTest`의 `내 채널 팔로워 수 클릭 source는 레거시 팔로워 목록으로 이동한다`
|
||||||
|
|
||||||
|
### 실행 환경
|
||||||
|
|
||||||
|
```text
|
||||||
|
OS: macOS / Android device via adb
|
||||||
|
Build tool: Gradle wrapper
|
||||||
|
Android package: kr.co.vividnext.sodalive.debug
|
||||||
|
```
|
||||||
|
|
||||||
|
### 실행한 검증
|
||||||
|
|
||||||
|
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|
||||||
|
|---|---|---|
|
||||||
|
| `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest.내 채널 팔로워 수 클릭 source는 레거시 팔로워 목록으로 이동한다"` | 실패 후 성공 | RED는 marker 부재로 실패, GREEN은 exit code 0 |
|
||||||
|
| `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` | 성공 | exit code 0 |
|
||||||
|
| `./gradlew :app:testDebugUnitTest` | 성공 | exit code 0 |
|
||||||
|
| `./gradlew :app:assembleDebug` | 성공 | exit code 0 |
|
||||||
|
| `adb install -r "app/build/outputs/apk/debug/app-debug.apk"` | 성공 | `Success` |
|
||||||
|
| `adb shell am start -W -n kr.co.vividnext.sodalive.debug/kr.co.vividnext.sodalive.explorer.profile.follow.UserFollowerListActivity --el extra_user_id 1` | 불가 | `not exported` 보안 정책으로 직접 기동 차단 |
|
||||||
|
| `adb shell monkey -p kr.co.vividnext.sodalive.debug 1` | 성공 | `Events injected: 1` |
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 없음 | Low | 오탐 | 차단 이슈 없음 | `P1-T1` | 전환 항목 없음 |
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
확정 발견 사항 없음. 잔여 위험은 로그인된 owner 채널에서의 실제 탭 동작이 별도 QA 환경에서 확인되어야 한다는 점이며, 이는 `plan-task.md`에 기록되어 있다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | 구현, 테스트, 문서 diff 검토 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | Blocker 없음 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 전환 항목 없음 |
|
||||||
|
| 보류 항목의 담당·재개 조건 기록 | 충족 | 로그인된 내 채널 수동 QA 필요 사항을 `plan-task.md`에 기록 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | `plan-task.md`와 본 문서에 기록 |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음.
|
||||||
|
|
||||||
|
**남은 항목:** 로그인된 owner 채널 실제 클릭은 별도 QA 환경에서 확인 필요.
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
수정 항목 없음.
|
||||||
167
docs/20260803_콘텐츠_상세_긴_내용_펼침_성능_개선/plan-task.md
Normal file
167
docs/20260803_콘텐츠_상세_긴_내용_펼침_성능_개선/plan-task.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# 콘텐츠 상세 긴 내용 펼침 성능 개선 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 콘텐츠 상세의 긴 작품 설명을 펼칠 때 불필요한 말줄임 처리를 제거하고 기존 접힘 동작을 유지한다.
|
||||||
|
|
||||||
|
**Architecture:** 기존 `AudioContentDetailActivity`의 `isExpandDetail` observer 안에서 `TextView.maxLines`와 `TextView.ellipsize`를 함께 갱신한다. ViewModel, XML, API와 화면 구조는 변경하지 않는다.
|
||||||
|
|
||||||
|
**Tech Stack:** Kotlin, Android TextView, LiveData, JUnit 4, Gradle
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| 문서 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 상태 | 구현 완료 |
|
||||||
|
| 작성일 | 2026-08-03 |
|
||||||
|
| 요구사항 기준 | `docs/20260803_콘텐츠_상세_긴_내용_펼침_성능_개선/prd.md` |
|
||||||
|
| API 기준 | 해당 없음: API 변경 없음 |
|
||||||
|
| 현재 Phase | Phase 1 긴 내용 펼침 처리 |
|
||||||
|
| 현재 활성 Goal | 없음 |
|
||||||
|
|
||||||
|
## 범위
|
||||||
|
|
||||||
|
### 포함
|
||||||
|
|
||||||
|
- `AudioContentDetailActivity`의 설명 접기/펼치기 상태 처리
|
||||||
|
- 접힘, 펼침, 반복 토글 회귀 source test
|
||||||
|
- focused test, debug build, 연결 기기 수동 검증
|
||||||
|
|
||||||
|
### 제외
|
||||||
|
|
||||||
|
- XML 및 스크롤/추천 목록 구조 변경
|
||||||
|
- 신규 helper, dependency, benchmark framework
|
||||||
|
- 관련 없는 레거시 코드 정리
|
||||||
|
|
||||||
|
## 기술적 제약
|
||||||
|
|
||||||
|
- 사용자 승인에 따라 해당 레거시 Activity의 observer만 최소 수정한다.
|
||||||
|
- 기존 `HomeBusinessInfoBinder`와 `CharacterDetailFragment`의 `ellipsize = null`/`END` 패턴을 따른다.
|
||||||
|
- 테스트를 먼저 추가하고 RED 확인 후 production 코드를 수정한다.
|
||||||
|
- 커밋은 사용자가 명시적으로 요청하지 않았으므로 수행하지 않는다.
|
||||||
|
|
||||||
|
## 시나리오 계약
|
||||||
|
|
||||||
|
| 시나리오 | 이진 통과 조건 | 자동 검증 | 실제 표면 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 펼침 | observer가 `Int.MAX_VALUE`와 `ellipsize = null`을 함께 설정한다. | `AudioContentDetailActivitySourceTest.긴 설명을 펼치면 말줄임을 제거한다` | 연결 기기 콘텐츠 상세 화면 터치 |
|
||||||
|
| 접힘 | observer가 `2`와 `TextUtils.TruncateAt.END`를 함께 설정한다. | `AudioContentDetailActivitySourceTest.긴 설명을 접으면 두 줄 말줄임을 복원한다` | 연결 기기 콘텐츠 상세 화면 재터치 |
|
||||||
|
| 반복 토글 회귀 | 하나의 observer 분기에 펼침/접힘 값이 모두 존재한다. | `AudioContentDetailActivitySourceTest.긴 설명의 반복 토글 상태를 한 observer에서 처리한다` | 연결 기기에서 펼침 후 접힘 |
|
||||||
|
|
||||||
|
중단 조건: 세 시나리오, focused test, debug build, 진단과 리뷰가 통과하고 가능한 실제 기기 검증 및 정리 기록이 남으면 즉시 중단한다.
|
||||||
|
|
||||||
|
## Phase 1
|
||||||
|
|
||||||
|
**Phase 결과:** 긴 작품 설명이 펼침 상태에서 말줄임 없이 표시되고 접힘 상태가 보존된다.
|
||||||
|
|
||||||
|
**선행조건:** `DETAIL-001~003`, `DEC-001` 확정.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P1-T1`과 `P1-GATE` 완료 및 검증 기록 누적.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 1.1 설명 접기/펼치기 상태 최적화
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T1`:** 펼침 상태의 불필요한 ellipsize를 제거하고 접힘 상태에서 복원한다.
|
||||||
|
|
||||||
|
- **시작 조건:** PRD와 이 계획 문서 작성 완료.
|
||||||
|
- **완료 증거:** source test RED/GREEN, debug build, 변경 파일 진단.
|
||||||
|
- **범위 밖:** 레이아웃 구조, ViewModel, API 수정.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `app/src/test/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivitySourceTest.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `AudioContentDetailViewModel.isExpandDetail: LiveData<Boolean>`
|
||||||
|
- Produces: 펼침 시 `ellipsize == null`, 접힘 시 `ellipsize == TextUtils.TruncateAt.END`
|
||||||
|
|
||||||
|
- [x] **RED:** 세 시나리오를 검증하는 source test를 작성한다.
|
||||||
|
- [x] **RED 확인:** `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivitySourceTest"` 실행 시 ellipsize 상태 미구현 assertion 실패를 확인한다.
|
||||||
|
- [x] **GREEN:** `isExpandDetail` observer에서 `ellipsize`를 펼침/접힘 상태에 맞게 설정한다.
|
||||||
|
- [x] **GREEN 확인:** 같은 focused test가 모두 통과하는지 확인한다.
|
||||||
|
- [x] **REFACTOR:** 추가 abstraction 없이 import와 변경 블록만 정리하고 focused test 및 build를 다시 확인한다.
|
||||||
|
|
||||||
|
계획된 최소 구현:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
binding.tvDetail.ellipsize = if (it) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
TextUtils.TruncateAt.END
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 완료 조건
|
||||||
|
|
||||||
|
- [x] `DETAIL-001~003`이 source test와 구현으로 추적된다.
|
||||||
|
- [x] 요청 밖 파일과 동작을 변경하지 않았다.
|
||||||
|
|
||||||
|
### 검증 방법
|
||||||
|
|
||||||
|
#### Phase 1 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P1-GATE`:** 접기/펼치기 동작과 회귀 여부를 최종 판정한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivitySourceTest"
|
||||||
|
./gradlew :app:assembleDebug
|
||||||
|
adb devices
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** focused test 3건 통과, debug build exit code 0, 연결 기기에서 접힘/펼침 상태 확인 또는 접근 불가 사유 기록.
|
||||||
|
|
||||||
|
- [x] focused test 3건이 통과했다.
|
||||||
|
- [x] ktlint main/test와 debug build가 성공했다.
|
||||||
|
- [x] 연결 기기에서 접힘, 펼침, 재접힘 상태를 확인했다.
|
||||||
|
- [x] 독립 코드 및 화면 리뷰가 PASS로 종료됐다.
|
||||||
|
|
||||||
|
## 실행 순서와 의존성
|
||||||
|
|
||||||
|
| 순서 | Goal | 선행조건 | 병행 가능 |
|
||||||
|
|---:|---|---|---|
|
||||||
|
| 1 | `P1-T1` RED | 문서 작성 완료 | 아니요 |
|
||||||
|
| 2 | `P1-T1` GREEN | RED 확인 | 아니요 |
|
||||||
|
| 3 | `P1-GATE` | GREEN 확인 | 아니요 |
|
||||||
|
|
||||||
|
## 변경 금지 항목
|
||||||
|
|
||||||
|
- 테스트 삭제, skip, assertion 완화
|
||||||
|
- 신규 dependency 또는 공통 abstraction 추가
|
||||||
|
- 레이아웃, ViewModel, API와 관련 없는 코드 수정
|
||||||
|
- 사용자 요청 없는 커밋 및 Git 이력 변경
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
### `P1-T1` 1차 실행 — 2026-08-03
|
||||||
|
|
||||||
|
- 상태: 구현 및 Gate 검증 완료, 리뷰 진행 중
|
||||||
|
- 무엇을: PRD와 구현 계획을 작성하고 변경 범위를 고정했다.
|
||||||
|
- 왜: 코드 변경 전 문서 준비와 최소 변경 범위를 확정하기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- `codegraph` 및 `rg` 조사 — 기존 동일 패턴과 source test 관례 확인.
|
||||||
|
- `adb devices` — 기기 `2cec640c34017ece` 연결 확인.
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivitySourceTest"` — 의도한 assertion 실패 3건으로 RED 확인.
|
||||||
|
- 같은 focused test 재실행 — 3건 통과, `BUILD SUCCESSFUL`.
|
||||||
|
- `./gradlew :app:ktlintMainSourceSetCheck :app:ktlintTestSourceSetCheck :app:assembleDebug` — `BUILD SUCCESSFUL`.
|
||||||
|
- 연결 기기 debug APK 설치 및 콘텐츠 상세 토글 — 접힘 2줄 말줄임, 펼침 전체 표시, 재접힘 복원 확인.
|
||||||
|
- `adb shell dumpsys gfxinfo kr.co.vividnext.sodalive.debug` — 펼침 렌더링 프레임 150ms, 접힘 렌더링 프레임 117ms.
|
||||||
|
- 남은 항목: 독립 리뷰와 QA 임시 파일 정리.
|
||||||
|
- 다음 행동: 변경 diff와 최신 화면 캡처를 독립 검토한다.
|
||||||
|
|
||||||
|
## Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 2026-08-03 | `DEC-001` | 확정 | observer에 ellipsize 상태 복원만 추가한다. | 사용자 최소 변경 요청 | `P1-T1`, PRD `DETAIL-001~003` |
|
||||||
|
|
||||||
|
## Verification Log
|
||||||
|
|
||||||
|
- 2026-08-03: 구현 전 `git status --short` 출력 없음으로 clean worktree를 확인했다.
|
||||||
|
- 2026-08-03: Kotlin LSP는 사용자가 이전에 설치를 거부해 실행할 수 없었으며, Kotlin compiler, focused test와 ktlint로 대체 검증했다.
|
||||||
|
- 2026-08-03: 실기기에서 접힘/펼침/재접힘 화면과 UI hierarchy를 캡처해 세 시나리오를 확인했다.
|
||||||
|
- 2026-08-03: 독립 코드 리뷰와 CJK 표시 리뷰가 PASS였다. 기능 QA의 초기 프레임 예산 후보는 명시된 수용 기준에 없는 조건으로 재판정되어 철회됐고 최종 PASS를 받았다.
|
||||||
|
- 2026-08-03: `./gradlew :app:testDebugUnitTest :app:ktlintMainSourceSetCheck :app:ktlintTestSourceSetCheck :app:assembleDebug` 최종 실행이 `BUILD SUCCESSFUL`로 완료됐다.
|
||||||
|
- 2026-08-03: 기기와 로컬의 QA 임시 XML, screenshot 및 notepad 파일을 삭제했다.
|
||||||
73
docs/20260803_콘텐츠_상세_긴_내용_펼침_성능_개선/prd.md
Normal file
73
docs/20260803_콘텐츠_상세_긴_내용_펼침_성능_개선/prd.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# 콘텐츠 상세 긴 내용 펼침 성능 개선 PRD
|
||||||
|
|
||||||
|
## 문서 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 문서 상태 | 구현 완료 |
|
||||||
|
| 작성일 | 2026-08-03 |
|
||||||
|
| 최종 수정일 | 2026-08-03 |
|
||||||
|
| 대상 제품 | SodaLive Android 콘텐츠 상세 |
|
||||||
|
| 관련 API Contract | 해당 없음: API 변경 없음 |
|
||||||
|
| 관련 구현 계획 | `docs/20260803_콘텐츠_상세_긴_내용_펼침_성능_개선/plan-task.md` |
|
||||||
|
| 관련 review | 구현 후 `reviews/phase1-content-detail-expansion-review.md` |
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
콘텐츠 상세 화면에서 긴 작품 설명을 터치해 전체 내용을 펼칠 때 발생하는 지연을 최소 변경으로 줄인다. 기존 접기/펼치기 UX와 데이터 흐름은 유지한다.
|
||||||
|
|
||||||
|
## 2. Problem Statement
|
||||||
|
|
||||||
|
- 약 1,000자의 작품 설명을 펼칠 때 3초 이상 지연되는 사례가 있다.
|
||||||
|
- 현재 `TextView`는 접힘용 `ellipsize=end`를 유지한 채 `maxLines`만 `Int.MAX_VALUE`로 변경한다.
|
||||||
|
- 긴 본문을 펼친 상태에서는 말줄임 계산이 필요하지 않다.
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
|
||||||
|
- 펼친 상태에서 `TextView`의 말줄임 처리를 제거한다.
|
||||||
|
- 접힌 상태에서는 기존 2줄 및 끝 말줄임 표시를 유지한다.
|
||||||
|
- 반복해서 접고 펼쳐도 각 상태가 정확히 복원된다.
|
||||||
|
- 기존 상세 화면의 API, ViewModel, 레이아웃 구조는 변경하지 않는다.
|
||||||
|
|
||||||
|
## 4. Non-Goals
|
||||||
|
|
||||||
|
- `NestedScrollView` 또는 하위 `RecyclerView` 구조 변경
|
||||||
|
- 애니메이션, 별도 더보기 버튼 또는 신규 UI 추가
|
||||||
|
- API, 번역 데이터, 콘텐츠 본문 가공 변경
|
||||||
|
- 정량 성능 기준을 검증하기 위한 benchmark framework 추가
|
||||||
|
|
||||||
|
## 5. 핵심 사용자 흐름
|
||||||
|
|
||||||
|
1. 사용자가 콘텐츠 상세 화면에서 2줄로 접힌 작품 설명을 본다.
|
||||||
|
2. 작품 설명을 터치하면 전체 본문이 말줄임 없이 표시된다.
|
||||||
|
3. 다시 터치하면 2줄 끝 말줄임 상태로 복원된다.
|
||||||
|
|
||||||
|
## 6. 기능 요구사항
|
||||||
|
|
||||||
|
| ID | 상태 | 요구사항 | 수용 기준 | Goal 연결 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `DETAIL-001` | 확정 | 펼침 상태에서는 말줄임을 사용하지 않는다. | `maxLines == Int.MAX_VALUE`, `ellipsize == null` | `P1-T1` |
|
||||||
|
| `DETAIL-002` | 확정 | 접힘 상태에서는 기존 표시를 유지한다. | `maxLines == 2`, `ellipsize == TextUtils.TruncateAt.END` | `P1-T1` |
|
||||||
|
| `DETAIL-003` | 확정 | 반복 토글 시 상태가 복원된다. | 펼침 후 접힘 처리에 두 상태가 모두 명시된다. | `P1-T1`, `P1-GATE` |
|
||||||
|
|
||||||
|
## 7. 성능과 품질 요구사항
|
||||||
|
|
||||||
|
- 신규 dependency와 abstraction을 추가하지 않는다.
|
||||||
|
- 기존 저장소의 `TextView` 접기/펼치기 패턴을 재사용한다.
|
||||||
|
- source regression test, debug unit test, debug build를 통과해야 한다.
|
||||||
|
- 연결된 Android 기기에서 가능한 범위의 실제 화면 검증을 수행한다.
|
||||||
|
|
||||||
|
## 8. 성공 기준
|
||||||
|
|
||||||
|
- [x] 펼침 시 `ellipsize`가 `null`로 설정된다.
|
||||||
|
- [x] 접힘 시 `ellipsize`가 `END`로 복원된다.
|
||||||
|
- [x] 접힘/펼침의 기존 `maxLines` 값이 유지된다.
|
||||||
|
- [x] focused test와 debug build가 성공한다.
|
||||||
|
- [x] 실제 기기 검증 결과가 기록된다.
|
||||||
|
|
||||||
|
## 9. Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·Goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 2026-08-03 | `DEC-001` | 확정 | 펼침 상태의 `ellipsize`만 제거하고 접힘 시 복원한다. | 사용자 최소 변경 요청과 저장소 내 동일 패턴 | `DETAIL-001~003`, `P1-T1` |
|
||||||
|
| 2026-08-03 | `DEC-002` | 확정 | 레이아웃 구조와 추천 목록 측정 최적화는 제외한다. | 검증되지 않은 범위 확장을 피한다. | Non-Goals, `P1-GATE` |
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Phase 1 콘텐츠 상세 긴 내용 펼침 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 1 / Task 1.1 |
|
||||||
|
| 기준 commit 또는 working tree | working tree |
|
||||||
|
| 리뷰 일자 | 2026-08-03 |
|
||||||
|
| 리뷰어 | Oracle 코드 리뷰, 기능 QA, CJK 표시 QA |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` |
|
||||||
|
| 리뷰 상태 | 판정 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
### 목적
|
||||||
|
|
||||||
|
- `DETAIL-001~003`과 최소 변경 요구사항 충족 여부를 확인한다.
|
||||||
|
- source test, 빌드 및 실기기 검증 기록과 실제 구현이 일치하는지 확인한다.
|
||||||
|
|
||||||
|
### 포함 범위
|
||||||
|
|
||||||
|
- 코드: `app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt`
|
||||||
|
- 테스트: `app/src/test/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivitySourceTest.kt`
|
||||||
|
- 수동 검증: 연결 기기의 접힘, 펼침, 재접힘 상태와 `gfxinfo`
|
||||||
|
|
||||||
|
### 제외 범위
|
||||||
|
|
||||||
|
- 스크롤 및 추천 목록 레이아웃 구조
|
||||||
|
- 신규 benchmark framework와 60Hz 단일 프레임 목표
|
||||||
|
|
||||||
|
## 3. 검토한 근거
|
||||||
|
|
||||||
|
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|
||||||
|
|---|---|---|
|
||||||
|
| focused test RED | 성공 | 구현 전 assertion 실패 3건 |
|
||||||
|
| focused test GREEN | 성공 | 테스트 3건 통과 |
|
||||||
|
| 전체 debug unit test | 성공 | `:app:testDebugUnitTest` 통과 |
|
||||||
|
| ktlint main/test | 성공 | 두 source set 검사 통과 |
|
||||||
|
| `:app:assembleDebug` | 성공 | debug APK 생성 |
|
||||||
|
| 실기기 접힘 → 펼침 → 재접힘 | 성공 | 2줄 말줄임 → 전체 표시 → 2줄 말줄임 복원 |
|
||||||
|
| 실기기 `gfxinfo` | 성공 | 펼침 150ms, 접힘 117ms 렌더링 프레임 |
|
||||||
|
|
||||||
|
## 4. 발견 사항 요약
|
||||||
|
|
||||||
|
확정 발견 사항 없음.
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 제목 | 후속 goal |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `REV-P1-001` | Low | 오탐 | 16ms 프레임 예산 미충족 후보 | 없음 |
|
||||||
|
|
||||||
|
## 5. 발견 사항 상세
|
||||||
|
|
||||||
|
### REV-P1-001 — 16ms 프레임 예산 미충족 후보
|
||||||
|
|
||||||
|
- **심각도:** Low
|
||||||
|
- **상태:** 오탐
|
||||||
|
- **관련 요구사항:** `DETAIL-001~003`
|
||||||
|
- **소유 Task:** `P1-T1`
|
||||||
|
|
||||||
|
**관찰 내용**
|
||||||
|
|
||||||
|
초기 기능 QA에서 펼침 150ms와 접힘 117ms가 60Hz 프레임 예산을 초과한다는 후보가 제기됐다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-08-03 — PRD에는 16ms 목표가 없고 사용자 요청은 기존 3초 이상 지연을 최소 변경으로 줄이는 것이다. 현재 결과는 세 기능 요구사항을 충족하고 기존 관찰값보다 크게 개선됐으므로 차단 기준이 아니며 리뷰어 재판정에서 PASS로 철회됐다.
|
||||||
|
|
||||||
|
## 6. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
전환 항목 없음.
|
||||||
|
|
||||||
|
## 7. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | 코드, 테스트, 문서 및 최신 실기기 캡처 확인 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | `REV-P1-001` 오탐 판정 |
|
||||||
|
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | `plan-task.md` Progress와 Verification Log |
|
||||||
|
|
||||||
|
**최종 결론:** 확정 발견 사항 없음, 독립 리뷰 PASS.
|
||||||
|
|
||||||
|
**남은 항목:** 없음.
|
||||||
@@ -101,12 +101,29 @@ Live Action Phase 테스트 예시:
|
|||||||
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinatorUiGateTest"
|
./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 테스트 예시:
|
Creator/Community Action Phase 테스트 예시:
|
||||||
```bash
|
```bash
|
||||||
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.action.*"
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.action.*"
|
||||||
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.community.action.*"
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.community.action.*"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Home Recommendation Phase 16 테스트 예시:
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest"
|
||||||
|
```
|
||||||
|
|
||||||
CreatorChannel 현재 라이브 공통 컴포넌트 테스트 예시:
|
CreatorChannel 현재 라이브 공통 컴포넌트 테스트 예시:
|
||||||
```bash
|
```bash
|
||||||
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelCurrentLiveViewModelTest"
|
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelCurrentLiveViewModelTest"
|
||||||
|
|||||||
@@ -13,25 +13,57 @@
|
|||||||
- PRD 작성 중 애매하거나 더 필요한 내용, 결정해야 하는 사항이 있으면 애매한 사항이 없어질 때까지 사용자와 인터뷰한다.
|
- PRD 작성 중 애매하거나 더 필요한 내용, 결정해야 하는 사항이 있으면 애매한 사항이 없어질 때까지 사용자와 인터뷰한다.
|
||||||
- 인터뷰로 확정한 내용을 PRD 문서에 보강한 뒤, 해당 PRD를 기준으로 구현 계획/TASK 문서를 작성한다.
|
- 인터뷰로 확정한 내용을 PRD 문서에 보강한 뒤, 해당 PRD를 기준으로 구현 계획/TASK 문서를 작성한다.
|
||||||
- 구현은 계획/TASK 문서를 기준으로 필요한 내용만 최소 범위로 진행한다.
|
- 구현은 계획/TASK 문서를 기준으로 필요한 내용만 최소 범위로 진행한다.
|
||||||
|
|
||||||
|
### 기준 샘플
|
||||||
|
- 문서 샘플의 기준 위치는 `docs/sample/`이다.
|
||||||
|
- PRD는 `docs/sample/sample-prd.md`를 참조해 작업에 필요한 항목만 사용한다.
|
||||||
|
- 구현 계획/TASK 문서는 `docs/sample/sample-plan-task.md`를 참조해 작업에 필요한 구조와 실행 규칙을 구체화한다.
|
||||||
|
- 리뷰 보고서는 `docs/sample/sample-review.md`를 참조한다.
|
||||||
|
- 실제 문서를 만들 때 샘플의 `<...>` placeholder와 예시 ID·경로·명령·기대 결과를 해당 작업의 구체적인 값으로 교체한다.
|
||||||
|
- 필요한 샘플이 없거나 경로가 불명확하면 다른 위치를 추정하거나 임의 형식을 만들지 말고 사용자에게 확인한다.
|
||||||
|
|
||||||
|
### 문서 배치
|
||||||
- 문서는 `docs/[날짜]_구현할내용한글/` 아래에 `prd.md`, `plan-task.md`로 만든다.
|
- 문서는 `docs/[날짜]_구현할내용한글/` 아래에 `prd.md`, `plan-task.md`로 만든다.
|
||||||
- `docs/[날짜]_구현할내용한글/prd.md`
|
- `docs/[날짜]_구현할내용한글/prd.md`
|
||||||
- `docs/[날짜]_구현할내용한글/plan-task.md`
|
- `docs/[날짜]_구현할내용한글/plan-task.md`
|
||||||
- 문서 폴더명에서 원래 띄어쓰기가 들어갈 위치는 공백 대신 `_`를 사용한다.
|
- 문서 폴더명에서 원래 띄어쓰기가 들어갈 위치는 공백 대신 `_`를 사용한다.
|
||||||
- 예: `docs/20260601_메인_홈_추천_UI와_API_연동/`
|
- 예: `docs/20260601_메인_홈_추천_UI와_API_연동/`
|
||||||
- 기존에 생성된 `docs/prd/`, `docs/plan-task/` 문서는 유지하고, 신규 생성 문서부터 위 구조를 적용한다.
|
- 기존에 생성된 `docs/prd/`, `docs/plan-task/` 문서는 유지하고, 신규 생성 문서부터 위 구조를 적용한다.
|
||||||
- PRD 문서는 `sample-prd.md` 파일에서 작업에 필요한 부분만 발췌해 작성한다. `sample-prd.md`가 없거나 위치가 불명확하면 추측하지 말고 사용자에게 확인한다.
|
|
||||||
- 날짜는 `YYYYMMDD` 8자리 숫자를 사용한다.
|
- 날짜는 `YYYYMMDD` 8자리 숫자를 사용한다.
|
||||||
- 연속된 하나의 작업이라면 별도 새 문서를 만들지 말고 기존 PRD와 계획/TASK 문서에 추가 작업으로 이어서 기록한다.
|
- 연속된 하나의 작업이라면 별도 새 문서를 만들지 말고 기존 PRD와 계획/TASK 문서에 추가 작업으로 이어서 기록한다.
|
||||||
- 계획/TASK 문서는 의미 단위 phase로 나누고 `### Phase 1: ...`, `### Phase 2: ...` 형식의 heading을 사용한다.
|
|
||||||
- 각 phase 아래에는 단계별 task를 체크박스(`- [ ] **Task N.N: ...**`) 형태로 작성하고 완료 즉시 `- [x]`로 갱신한다.
|
### 계획/TASK 문서 작성과 유지
|
||||||
- 각 task에는 구현 시 생성/수정/확인할 파일 경로를 명시한다.
|
- 계획/TASK 문서는 `docs/sample/sample-plan-task.md`의 상태·목표·범위·기술적 제약·Phase·Task·Gate·Progress·Decision Log 구조 중 작업에 필요한 항목을 사용한다.
|
||||||
- 각 phase 또는 task에는 실행 명령, 기대 결과, 수동 확인 항목 등 검증 기준을 함께 작성한다.
|
- 의미 단위 Phase는 `## Phase N`, 구현 항목의 Task는 `#### Task N.N` 형식으로 작성한다.
|
||||||
|
- 각 Task 또는 Phase Gate에는 독립적으로 실행·판정할 수 있는 목표, 시작 조건, 완료 증거, 범위 밖 항목을 명시한다.
|
||||||
|
- 각 Task에는 구현 시 생성·수정·확인할 파일 경로와 필요한 인터페이스를 명시한다.
|
||||||
|
- 구현 Task의 실행 단계는 `RED → RED 확인 → GREEN → GREEN 확인 → REFACTOR` 순서의 체크박스로 작성하고 완료 즉시 `- [x]`로 갱신한다.
|
||||||
|
- 테스트 작성이 현실적으로 불가능한 read-only 리뷰·문서·외부 확인 Task에는 `TDD 예외 사유`와 실행 명령·대조표·수동 확인을 포함한 `대체 검증 방법`을 명시한다.
|
||||||
|
- 각 Phase 또는 Task에는 실행 명령, 기대 결과, 수동 확인 항목 등 검증 기준을 함께 작성한다.
|
||||||
- 작업 도중 범위가 변경되면 계획 문서 체크리스트를 먼저 업데이트한 뒤 구현한다.
|
- 작업 도중 범위가 변경되면 계획 문서 체크리스트를 먼저 업데이트한 뒤 구현한다.
|
||||||
|
- 실제 실행 결과는 샘플의 `Progress` 형식에 맞춰 무엇을/왜/어떻게 수행했는지와 남은 항목을 한국어로 누적한다.
|
||||||
- 특정 phase 또는 task에 직접 대응되는 검증 기록(무엇/왜/어떻게, 실행 명령, 결과)은 해당 phase 또는 task 아래에 `검증 기록`으로 한국어로 남긴다.
|
- 특정 phase 또는 task에 직접 대응되는 검증 기록(무엇/왜/어떻게, 실행 명령, 결과)은 해당 phase 또는 task 아래에 `검증 기록`으로 한국어로 남긴다.
|
||||||
- 여러 phase에 걸치거나 문서 전체에 해당하는 통합 검증, 회귀 검증, 최종 수동 확인 기록은 문서 최하단 `Verification Log`에 한국어로 남긴다.
|
- 여러 phase에 걸치거나 문서 전체에 해당하는 통합 검증, 회귀 검증, 최종 수동 확인 기록은 문서 최하단 `Verification Log`에 한국어로 남긴다.
|
||||||
- 후속 수정이 발생해도 기존 검증 기록은 삭제/덮어쓰기 없이 위치별로 누적한다.
|
- 후속 수정이 발생해도 기존 검증 기록은 삭제/덮어쓰기 없이 위치별로 누적한다.
|
||||||
|
|
||||||
|
### 코드 리뷰 보고서와 후속 수정
|
||||||
|
- 코드 리뷰, 코드 품질 점검, 완료 Phase 재검토를 수행하면 결과를 별도 리뷰 보고서로 작성한다.
|
||||||
|
- 리뷰 보고서는 `docs/sample/sample-review.md`를 참조해 대상 PRD·`plan-task.md`와 같은 작업 디렉터리의 `reviews/[리뷰범위]-review.md`에 저장한다.
|
||||||
|
- `[리뷰범위]`는 `phase<번호>-<구현 내용을 나타내는 영문 kebab-case>` 형식으로 작성한다.
|
||||||
|
- `phase`와 번호 사이에는 하이픈을 넣지 않는다.
|
||||||
|
- 구현 내용은 소문자 영문 단어를 하이픈으로 연결한다.
|
||||||
|
- 예: `docs/20260601_메인_홈_추천_UI와_API_연동/reviews/phase2-main-home-recommendation-review.md`
|
||||||
|
- `reviews/`는 실제 리뷰 보고서가 생길 때 만들며, 리뷰가 없는 작업에는 빈 폴더를 만들지 않는다.
|
||||||
|
- 확정 발견 사항이 없더라도 리뷰 범위, 검토 근거, 실행한 검증, “확정 발견 사항 없음” 판정을 보고서에 남긴다.
|
||||||
|
- 일반 빌드, 테스트, 린트 실행은 리뷰 보고서 생성 대상이 아니며 해당 결과는 Task별 검증 기록 또는 `Verification Log`에 남긴다.
|
||||||
|
- 리뷰에서 발견한 후보는 재현·근거 확인 후 `확정`, `오탐`, `보류`로 판정하고, 확정된 수정 항목만 계획/TASK 문서로 전환한다.
|
||||||
|
- 수정 항목이 확정되면 코드를 변경하기 전에 `plan-task.md`의 해당 Phase에 review ID, 대상 파일, 수정 범위, 회귀 테스트, 완료 증거를 포함한 신규 회귀 수정 Task를 추가한다.
|
||||||
|
- 계획/TASK 문서 갱신이 끝나면 신규 회귀 수정 Task의 체크리스트에 따라 바로 수정하며, 기존 완료 Task나 Phase의 체크박스를 미완료로 되돌리지 않는다.
|
||||||
|
- 수정 후 실행한 검증과 판정은 리뷰 보고서와 계획/TASK 문서의 검증 기록에 삭제·덮어쓰기 없이 누적한다.
|
||||||
|
|
||||||
## 문서 유지보수 규칙
|
## 문서 유지보수 규칙
|
||||||
|
- `docs/sample/` 샘플의 위치, 문서 구조, 작성·운용 규칙이 변경되면 같은 작업에서 `AGENTS.md`와 관련 `docs/agent-guides/` 문서를 함께 갱신한다.
|
||||||
|
- 샘플과 안내 문서가 서로 다른 경로 또는 작성 규칙을 안내하지 않는지 문서 변경 후 검색해 확인한다.
|
||||||
- `build.gradle`/`app/build.gradle`/`settings.gradle` 변경 시 실행 명령 섹션을 함께 갱신한다.
|
- `build.gradle`/`app/build.gradle`/`settings.gradle` 변경 시 실행 명령 섹션을 함께 갱신한다.
|
||||||
- 테스트 클래스 추가/이동 시 단일 테스트 실행 예시를 최신 상태로 유지한다.
|
- 테스트 클래스 추가/이동 시 단일 테스트 실행 예시를 최신 상태로 유지한다.
|
||||||
- `.editorconfig` 변경 시 `docs/agent-guides/code-style.md`의 포맷 규칙 섹션을 동기화한다.
|
- `.editorconfig` 변경 시 `docs/agent-guides/code-style.md`의 포맷 규칙 섹션을 동기화한다.
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
# PRD: [제품명]
|
|
||||||
|
|
||||||
## 1. Overview
|
|
||||||
이 제품이 무엇인지 한 줄 설명
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Problem
|
|
||||||
어떤 문제를 해결하는가?
|
|
||||||
|
|
||||||
- 현재 사용자의 불편
|
|
||||||
- 기존 방식의 한계
|
|
||||||
- 왜 지금 필요한가
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Goals
|
|
||||||
성공 기준
|
|
||||||
|
|
||||||
예:
|
|
||||||
- 가입 전환율 20%
|
|
||||||
- 작업 시간 50% 감소
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Non-Goals
|
|
||||||
이번에 하지 않을 것
|
|
||||||
|
|
||||||
매우 중요함.
|
|
||||||
|
|
||||||
예:
|
|
||||||
- 모바일 앱 지원 안 함
|
|
||||||
- 실시간 협업 제외
|
|
||||||
- 다국어 제외
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Target Users
|
|
||||||
누가 사용하는가?
|
|
||||||
|
|
||||||
- 초보 개발자
|
|
||||||
- PM
|
|
||||||
- 디자이너
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. User Stories
|
|
||||||
사용자 행동 시나리오
|
|
||||||
|
|
||||||
예:
|
|
||||||
- 사용자는 버튼 하나로 요약하고 싶다
|
|
||||||
- 사용자는 로그인 없이 체험하고 싶다
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Core Features
|
|
||||||
|
|
||||||
### Feature A
|
|
||||||
설명
|
|
||||||
|
|
||||||
#### Requirements
|
|
||||||
- must
|
|
||||||
- should
|
|
||||||
- constraints
|
|
||||||
|
|
||||||
#### Edge Cases
|
|
||||||
- 빈 입력
|
|
||||||
- timeout
|
|
||||||
- 중복 요청
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. UX / UI Expectations
|
|
||||||
|
|
||||||
- 반응속도
|
|
||||||
- 클릭 수
|
|
||||||
- 모바일 대응
|
|
||||||
- 접근성
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Technical Constraints
|
|
||||||
|
|
||||||
- Next.js 사용
|
|
||||||
- PostgreSQL 사용
|
|
||||||
- API latency 2초 이하
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Metrics
|
|
||||||
|
|
||||||
무엇을 측정할 것인가?
|
|
||||||
|
|
||||||
- retention
|
|
||||||
- DAU
|
|
||||||
- conversion
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Open Questions
|
|
||||||
|
|
||||||
아직 결정 안 된 것
|
|
||||||
|
|
||||||
- OAuth 제공?
|
|
||||||
- pricing?
|
|
||||||
- offline mode?
|
|
||||||
317
docs/sample/sample-plan-task.md
Normal file
317
docs/sample/sample-plan-task.md
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
# Goal 실행형 구현 계획 샘플
|
||||||
|
|
||||||
|
> 이 문서는 goal 기능으로 구현 계획을 실행하기 위한 템플릿이다. 실제 `plan-task.md`를 만들 때 `<...>` placeholder를 모두 구체적인 값으로 교체한다. Phase는 결과와 의존성을 묶고, `create_goal`에는 Task 또는 Phase Gate 하나만 등록한다.
|
||||||
|
|
||||||
|
| 문서 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 상태 | 계획 작성 중 / 구현 중 / 외부 조건 대기 / 구현 완료 |
|
||||||
|
| 작성일 | `YYYY-MM-DD` |
|
||||||
|
| 요구사항 기준 | `<대상 prd.md 경로>` |
|
||||||
|
| API 기준 | `<대상 api-contract.md 경로>` |
|
||||||
|
| 현재 Phase | `<Phase 번호와 이름>` |
|
||||||
|
| 현재 활성 Goal | `<없음 또는 Goal ID>` |
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
`<사용자가 얻는 최종 결과를 한 문장으로 작성한다. 구현 수단보다 완결된 사용자 흐름을 먼저 쓴다.>`
|
||||||
|
|
||||||
|
## 현재 상태
|
||||||
|
|
||||||
|
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|
||||||
|
|---:|---|---:|---|---|
|
||||||
|
| 1 | 대기 / 진행 중 / 완료 / 제외 | `0/N` | `P1-T1` | `<없음 또는 조건>` |
|
||||||
|
| 2 | 대기 / 진행 중 / 완료 / 제외 | `0/N` | `P2-T1` | `<없음 또는 조건>` |
|
||||||
|
|
||||||
|
- 동시에 하나의 미완료 goal만 운용한다.
|
||||||
|
- 완료된 Task와 검증 기록은 되돌리거나 삭제하지 않는다. 후속 수정은 회귀 수정 Task와 새 goal ID를 추가한다.
|
||||||
|
- 사용자가 명시적으로 요청하지 않으면 goal에 token budget을 설정하지 않는다.
|
||||||
|
|
||||||
|
## 범위
|
||||||
|
|
||||||
|
### 포함
|
||||||
|
|
||||||
|
- `<구현할 사용자 흐름>`
|
||||||
|
- `<구현할 route/API/UI 범위>`
|
||||||
|
- `<필수 오류·반응형·접근성·test 범위>`
|
||||||
|
|
||||||
|
### 제외
|
||||||
|
|
||||||
|
- `<이번 릴리스에서 만들지 않을 기능>`
|
||||||
|
- `<외부 시스템 또는 다음 Phase 소유 범위>`
|
||||||
|
- `<복원, hard delete처럼 명시적으로 금지한 기능>`
|
||||||
|
|
||||||
|
## 기술적 제약
|
||||||
|
|
||||||
|
- 기술 스택: `<언어, framework, 주요 library와 version 기준>`
|
||||||
|
- 아키텍처: `<상태·API·UI·파일 책임 경계>`
|
||||||
|
- 데이터·보안: `<저장 위치, 인증, 민감정보 비기록 규칙>`
|
||||||
|
- 호환성: `<지원 browser, viewport, runtime>`
|
||||||
|
- 의존성: 실제 소비 Task에서 필요한 최소 dependency만 추가한다.
|
||||||
|
- 계약: 제공되지 않은 endpoint, DTO, enum, 오류 status/key와 validation 상한을 추정하지 않는다.
|
||||||
|
- backend 구현 전 UI 확인이 필요하면 제공 계약 기반 explicit mock mode를 사용하고 실제 404 자동 fallback·production mock을 금지하며 mock/server 완료 증거를 분리한다.
|
||||||
|
- 구현: 모든 구현 Task는 아래 `RED → GREEN → REFACTOR` 순서를 체크박스에 명시하고, 가장 작은 실패 test에서 시작해 최소 구현으로 통과시킨다.
|
||||||
|
- 검증: focused test에서 시작해 영향받는 package/feature 회귀로 확장한다. 전체 회귀는 공통 경계 변경, 여러 domain/phase 변경,
|
||||||
|
release/final Gate에서 전체 상태 증거를 별도로 요구하는 경우, targeted test만으로 영향 범위를 판단할 수 없는 실패 또는 사용자
|
||||||
|
명시 요청이 있을 때만 실행한다.
|
||||||
|
- 전체 회귀를 생략하면 생략 사실·근거와 대신 실행한 focused/영향 범위 회귀 명령을 Progress에 기록한다.
|
||||||
|
|
||||||
|
## Task TDD 작성 규칙
|
||||||
|
|
||||||
|
구현 Task의 실행 체크박스는 아래 순서와 표제어를 그대로 사용한다. 각 단계에는 대상 test, 실행 명령과 관찰할 실패·성공 결과를
|
||||||
|
구체적으로 적고, `RED` 확인 없이 production 구현을 시작하거나 `GREEN` 확인 없이 `REFACTOR`로 넘어가지 않는다.
|
||||||
|
|
||||||
|
- [ ] **RED:** 가장 작은 실패 test를 작성한다.
|
||||||
|
- [ ] **RED 확인:** focused test를 실행해 요구 동작이 없어서 발생한 의도한 assertion 실패를 확인한다.
|
||||||
|
- [ ] **GREEN:** RED를 통과시키는 최소 구현을 작성한다.
|
||||||
|
- [ ] **GREEN 확인:** 같은 focused test를 다시 실행해 성공을 확인한다.
|
||||||
|
- [ ] **REFACTOR:** 새 동작을 바꾸지 않는 범위에서 이번 Task가 만든 중복만 정리하고, focused test·직접 영향 회귀·lint를 다시 실행해 실제 결과를 Progress에 기록한다.
|
||||||
|
|
||||||
|
test 작성이 현실적으로 불가능한 read-only 리뷰·문서·외부 확인 Task는 TDD 단계를 형식적으로 만들지 않는다. 대신 Task 본문에
|
||||||
|
`TDD 예외 사유`와 실행 명령·대조표·수동 확인을 포함한 `대체 검증 방법`을 명시한다.
|
||||||
|
|
||||||
|
## Phase 1
|
||||||
|
|
||||||
|
**Phase 결과:** `<이 Phase가 끝나면 사용자가 완료할 수 있는 흐름>`
|
||||||
|
|
||||||
|
**선행조건:** `<없음 또는 선행 Goal/Gate ID>`
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P1-T1`~`P1-TN`과 `P1-GATE` 완료, 검증 기록 누적.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 1.1 `<독립적으로 검토 가능한 결과>`
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T1`:** `<이 Task가 만드는 결과를 한 문장으로 작성한다.>`
|
||||||
|
|
||||||
|
- **시작 조건:** `<선행 Goal, 요구사항 ID, API Contract section>`
|
||||||
|
- **완료 증거:** `<체크박스 전체, focused test, 산출물, 문서 기록>`
|
||||||
|
- **범위 밖:** `<다음 Task 또는 다른 Phase가 소유하는 항목>`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `<정확한 파일 경로>`
|
||||||
|
- Modify: `<정확한 파일 경로>`
|
||||||
|
- Test: `<정확한 test 파일 경로>`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `<선행 Task가 제공하는 type/function/component contract>`
|
||||||
|
- Produces: `<후속 Task가 사용할 정확한 type/function/component contract>`
|
||||||
|
|
||||||
|
- [ ] **RED:** `<요구 동작>`을 재현하는 가장 작은 실패 test를 작성한다.
|
||||||
|
- [ ] **RED 확인:** `<focused test 명령>`을 실행해 `<요구 동작 미구현 때문에 발생할 assertion 실패>`를 확인한다.
|
||||||
|
- [ ] **GREEN:** RED를 통과시키는 최소 구현을 작성한다.
|
||||||
|
- [ ] **GREEN 확인:** `<focused test 명령>`을 다시 실행해 성공을 확인한다.
|
||||||
|
- [ ] **REFACTOR:** 이번 Task가 만든 중복만 정리하고 focused test·직접 영향 회귀·typecheck·lint 결과를 Progress에 기록한다.
|
||||||
|
|
||||||
|
#### Task 1.2 `<두 번째 독립 결과>`
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T2`:** `<이 Task가 만드는 결과를 한 문장으로 작성한다.>`
|
||||||
|
|
||||||
|
- **시작 조건:** `P1-T1` 완료와 `<필요한 계약>`.
|
||||||
|
- **완료 증거:** `<체크박스 전체, test와 문서 기록>`
|
||||||
|
- **범위 밖:** `<이 Task에서 다루지 않는 항목>`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `<정확한 파일 경로>`
|
||||||
|
- Modify: `<정확한 파일 경로>`
|
||||||
|
- Test: `<정확한 test 파일 경로>`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `<P1-T1이 제공한 정확한 contract>`
|
||||||
|
- Produces: `<Phase 2 또는 Gate가 사용할 정확한 contract>`
|
||||||
|
|
||||||
|
- [ ] **RED:** `<두 번째 요구 동작>`의 실패 test를 작성한다.
|
||||||
|
- [ ] **RED 확인:** `<focused test 명령>`으로 의도한 assertion 실패를 확인한다.
|
||||||
|
- [ ] **GREEN:** 최소 구현으로 focused test를 통과시킨다.
|
||||||
|
- [ ] **GREEN 확인:** 오류·loading·empty·success와 접근성 상태를 포함한 focused test 성공을 확인한다.
|
||||||
|
- [ ] **REFACTOR:** 이번 Task가 만든 중복만 정리하고 관련 test·typecheck·lint 결과를 Progress에 기록한다.
|
||||||
|
|
||||||
|
### 완료 조건
|
||||||
|
|
||||||
|
- [ ] `P1-T1`, `P1-T2`의 체크박스와 완료 증거가 모두 충족됐다.
|
||||||
|
- [ ] Phase 1의 확정 요구사항이 구현·명시적 제외·후속 결정 중 하나로 추적된다.
|
||||||
|
- [ ] 알려진 문서와 구현의 차이가 없다.
|
||||||
|
|
||||||
|
### 검증 방법
|
||||||
|
|
||||||
|
#### Phase 1 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P1-GATE`:** Phase 1의 사용자 흐름과 공통 품질 기준을 최종 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** Phase 1의 모든 활성 Task goal 완료.
|
||||||
|
- **완료 증거:** 아래 자동·수동 검증 통과와 Progress 기록.
|
||||||
|
- **범위 밖:** Gate 통과를 위한 test 삭제·완화와 관련 없는 기능 수정.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
<focused unit/integration test 명령>
|
||||||
|
<Phase 전용 E2E 명령>
|
||||||
|
<typecheck 명령>
|
||||||
|
<lint 명령>
|
||||||
|
<build 명령>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** `<0 exit code, test 수, 사용자가 완료할 흐름, 금지 요청 0회 등 관찰 가능한 결과>`
|
||||||
|
|
||||||
|
수동 검증:
|
||||||
|
|
||||||
|
- [ ] `<viewport와 사용자 흐름>`
|
||||||
|
- [ ] `<keyboard·focus·zoom·접근성 검사>`
|
||||||
|
- [ ] `<민감정보·network request 검증>`
|
||||||
|
|
||||||
|
## Phase 2
|
||||||
|
|
||||||
|
**Phase 결과:** `<Phase 1 결과를 소비해 완성하는 다음 사용자 흐름>`
|
||||||
|
|
||||||
|
**선행조건:** `P1-GATE` 또는 `<Phase 1의 부분 contract Goal ID>` 완료.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P2-T1`~`P2-TN`과 `P2-GATE` 완료, 검증 기록 누적.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 2.1 계약·상태 확인
|
||||||
|
|
||||||
|
**Goal 실행 `P2-T1`:** Phase 2 구현에 필요한 계약, 상태/action inventory와 component/file map을 확정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `<선행 Goal>` 완료, 관련 PRD/API Contract 확인.
|
||||||
|
- **완료 증거:** 계약 제공 또는 제외 결정이 기준 문서에 일치하고 구현 map이 기록됨.
|
||||||
|
- **범위 밖:** 계약을 추정한 production adapter와 실제 기능 구현.
|
||||||
|
- **TDD 예외 사유:** production 동작을 변경하지 않는 read-only 계약·상태 확인 Task다.
|
||||||
|
- **대체 검증 방법:** PRD/API Contract/현재 구현의 field·상태·파일 대조표와 누락·결정 기록을 작성하고 문서 검증 명령 결과를 Progress에 남긴다.
|
||||||
|
|
||||||
|
- [ ] 필요한 endpoint·DTO·오류·pagination 계약을 확인한다.
|
||||||
|
- [ ] loading·empty·error·success·read-only·viewport 상태와 action을 inventory한다.
|
||||||
|
- [ ] 계약이 없으면 담당 주체·영향·재개 조건과 제외/후속 결정을 문서화한다.
|
||||||
|
- [ ] Page·feature·shared component와 test file 책임을 확정한다.
|
||||||
|
|
||||||
|
#### Task 2.2 `<Phase 2의 독립 구현 결과>`
|
||||||
|
|
||||||
|
**Goal 실행 `P2-T2`:** `<사용자가 직접 확인할 수 있는 흐름을 한 문장으로 작성한다.>`
|
||||||
|
|
||||||
|
- **시작 조건:** `P2-T1` 완료.
|
||||||
|
- **완료 증거:** `<contract/UI/E2E test와 문서 기록>`
|
||||||
|
- **범위 밖:** `<다음 Task 또는 후속 Phase>`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `<정확한 파일 경로>`
|
||||||
|
- Modify: `<정확한 파일 경로>`
|
||||||
|
- Test: `<정확한 test 파일 경로>`
|
||||||
|
|
||||||
|
- [ ] **RED:** contract·serializer와 사용자 action의 가장 작은 실패 test를 작성한다.
|
||||||
|
- [ ] **RED 확인:** `<focused test 명령>`을 실행해 각 요구 동작이 없어 발생한 assertion 실패를 확인한다.
|
||||||
|
- [ ] **GREEN:** RED를 통과시키는 최소 구현을 작성한다.
|
||||||
|
- [ ] **GREEN 확인:** 같은 focused test를 다시 실행해 contract와 사용자 흐름의 성공을 확인한다.
|
||||||
|
- [ ] **REFACTOR:** 이번 Task가 만든 중복만 정리하고 관련 integration/E2E·공통 품질 명령의 실제 결과와 남은 항목을 Progress에 기록한다.
|
||||||
|
|
||||||
|
### 완료 조건
|
||||||
|
|
||||||
|
- [ ] `P2-T1`, `P2-T2`의 체크박스와 완료 증거가 모두 충족됐다.
|
||||||
|
- [ ] 외부 의존은 제공 계약 구현 또는 명시적 제외/후속 결정으로 종결됐다.
|
||||||
|
- [ ] Phase 2의 사용자 흐름과 오류·반응형·접근성 상태가 검증됐다.
|
||||||
|
|
||||||
|
### 검증 방법
|
||||||
|
|
||||||
|
#### Phase 2 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P2-GATE`:** Phase 2의 contract, 사용자 흐름과 회귀 방지를 최종 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** Phase 2의 모든 활성 Task goal 완료.
|
||||||
|
- **완료 증거:** 아래 명령과 Expected 통과, Progress에 실제 결과 누적.
|
||||||
|
- **범위 밖:** 실패와 무관한 다음 Phase 구현.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
<Phase 2 focused test 명령>
|
||||||
|
<Phase 2 E2E 명령>
|
||||||
|
<typecheck·lint·build 명령>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** `<사용자 journey, 오류 처리, request payload와 금지 동작을 포함한 최종 결과>`
|
||||||
|
|
||||||
|
## 실행 순서와 의존성
|
||||||
|
|
||||||
|
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|
||||||
|
|---:|---|---|---|---|
|
||||||
|
| 1 | `P1-T1` | 없음 | 아니요 | 근거 문서 보정 |
|
||||||
|
| 2 | `P1-T2` | `P1-T1` | 아니요 | 독립 검증 계속 |
|
||||||
|
| 3 | `P1-GATE` | Phase 1 Task 전체 | 아니요 | 실패 소유 Task의 회귀 수정 goal 생성 |
|
||||||
|
| 4 | `P2-T1` | `P1-GATE` | 아니요 | 외부 계약 담당·재개 조건 기록 |
|
||||||
|
| 5 | `P2-T2` | `P2-T1` | `<예/아니요>` | 독립 범위만 계속 수행 |
|
||||||
|
| 6 | `P2-GATE` | Phase 2 Task 전체 | 아니요 | 실패 소유 Task로 되돌림 |
|
||||||
|
|
||||||
|
```text
|
||||||
|
P1-T1 → P1-T2 → P1-GATE → P2-T1 → P2-T2 → P2-GATE
|
||||||
|
```
|
||||||
|
|
||||||
|
## 변경 금지 항목
|
||||||
|
|
||||||
|
- 확정된 요구사항·API Contract를 근거 없이 변경하지 않는다.
|
||||||
|
- 기존 완료 체크박스와 Progress·Decision Log·검증 기록을 삭제하거나 덮어쓰지 않는다.
|
||||||
|
- 제공되지 않은 endpoint·DTO·enum·오류 값과 validation 상한을 만들지 않는다.
|
||||||
|
- 요청 범위 밖의 리팩터링, dependency 추가와 공통 abstraction을 확장하지 않는다.
|
||||||
|
- test를 삭제·skip·완화하거나 타입 오류를 우회해 Gate를 통과시키지 않는다.
|
||||||
|
- JWT, password, signed URL, 파일 본문 같은 민감정보를 log·fixture·문서에 기록하지 않는다.
|
||||||
|
|
||||||
|
## 의사결정 및 중단 규칙
|
||||||
|
|
||||||
|
- PRD와 API Contract가 충돌하면 프로젝트가 정한 우선순위에 따라 결정하고 Decision Log에 근거를 남긴다.
|
||||||
|
- 구현 범위가 바뀌면 `plan-task.md` 체크박스와 범위를 먼저 갱신한 뒤 코드를 수정한다.
|
||||||
|
- 안전한 최소 기본값이 문서에 있으면 그 값만 구현한다. 안전하게 구현할 수 없으면 추정하지 않는다.
|
||||||
|
- 계약 미제공 기능을 제외할 때는 PRD 결정 기록 → API Contract → 이 계획 순서로 갱신한다.
|
||||||
|
- 같은 차단 사유가 최초 시도와 자동 후속을 포함해 3회 연속 반복되고, 문서화나 독립 작업도 불가능할 때만 goal을 `blocked`로 갱신한다.
|
||||||
|
- 체크박스 일부, test 일부 또는 코드 작성만 끝난 상태에서는 goal을 `complete`로 갱신하지 않는다.
|
||||||
|
- 완료 증거와 Progress 기록까지 충족한 뒤에만 goal을 `complete`로 갱신한다.
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
기존 기록을 삭제하거나 덮어쓰지 않고 실제 실행 결과를 차수별로 누적한다.
|
||||||
|
|
||||||
|
### `<Goal ID>` N차 실행 — YYYY-MM-DD
|
||||||
|
|
||||||
|
- 상태: 진행 중 / 완료 / 차단 감사 중 / 차단
|
||||||
|
- 무엇을: `<이번 실행에서 완료한 체크박스와 산출물>`
|
||||||
|
- 왜: `<Task objective와 요구사항 근거>`
|
||||||
|
- 어떻게:
|
||||||
|
- `<실행 명령>` — `<성공/실패, exit code, test 수와 핵심 결과>`
|
||||||
|
- `<수동 검증>` — `<성공/실패/불가 사유>`
|
||||||
|
- 남은 항목: `<체크박스, 외부 조건 또는 없음>`
|
||||||
|
- 다음 행동: `<같은 goal에서 이어서 할 가장 작은 단계>`
|
||||||
|
|
||||||
|
## Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `YYYY-MM-DD` | `DEC-001` | 제안 / 확정 / 정정 | `<결정 내용>` | `<요구사항, 계약, 검증 근거>` | `<Goal ID와 문서>` |
|
||||||
|
|
||||||
|
- 기존 결정을 정정할 때 원문을 지우지 않고 새 `정정` 행을 추가한다.
|
||||||
|
- 외부 의존 제외, 새로운 dependency, 범위 변경, 안전한 기본값 선택은 반드시 기록한다.
|
||||||
|
|
||||||
|
## 발견된 문제
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `ISSUE-001` | Blocker / High / Medium / Low | 후보 / 확정 / 보류 / 해결 | `<관찰 사실>` | `<Goal ID>` | `<수정 Task, 외부 담당 또는 없음>` |
|
||||||
|
|
||||||
|
- 구현 중 발견한 범위 내 문제는 근거와 재현 방법을 기록하고 해당 Task에서 처리한다.
|
||||||
|
- 완료된 범위의 회귀는 기존 Task를 다시 열지 않고 별도 회귀 수정 Task와 goal을 만든다.
|
||||||
|
- 범위 밖 문제는 임의로 수정하지 않고 사용자에게 보고하거나 후속 Task로 결정한다.
|
||||||
|
- 상세 코드 리뷰 결과가 필요하면 `docs/sample/sample-review.md` 형식으로
|
||||||
|
`docs/[날짜]_구현할내용한글/reviews/[리뷰범위]-review.md`에 별도 문서를 만든다.
|
||||||
|
|
||||||
|
## 최종 보고 형식
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
구현 결과: <완료한 Phase와 사용자 흐름>
|
||||||
|
|
||||||
|
- 변경: <주요 파일과 동작>
|
||||||
|
- 결정: <중요한 Decision Log ID와 내용>
|
||||||
|
- 검증:
|
||||||
|
- `<실행 명령>` — <성공/실패와 핵심 수치>
|
||||||
|
- `<수동 검증>` — <성공/실패/불가 사유>
|
||||||
|
- 남은 항목: <외부 의존, 후속 범위 또는 없음>
|
||||||
|
- 문서: <갱신한 PRD/API Contract/plan/review 링크>
|
||||||
|
```
|
||||||
|
|
||||||
|
최종 보고는 성공을 추정하지 않는다. 실제 실행한 최신 검증 결과와 완료되지 않은 범위를 함께 전달한다.
|
||||||
271
docs/sample/sample-prd.md
Normal file
271
docs/sample/sample-prd.md
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
# 제품 요구사항 문서(PRD) 샘플
|
||||||
|
|
||||||
|
> 이 문서는 요구사항을 API Contract와 goal 실행형 `plan-task.md`로 연결하기 위한 템플릿이다. 실제 `prd.md`를 만들 때 `<...>` placeholder와 예시 ID를 모두 제품의 구체적인 값으로 교체한다.
|
||||||
|
|
||||||
|
## 문서 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 문서 상태 | 초안 / 검토 중 / 구현 기준 확정 / 구현 완료 |
|
||||||
|
| 작성일 | `YYYY-MM-DD` |
|
||||||
|
| 최종 수정일 | `YYYY-MM-DD` |
|
||||||
|
| 대상 제품 | `<제품 또는 기능 이름>` |
|
||||||
|
| 작성자·결정권자 | `<이름 또는 역할>` |
|
||||||
|
| 관련 API Contract | `<대상 api-contract.md 경로>` |
|
||||||
|
| 관련 구현 계획 | `<대상 plan-task.md 경로>` |
|
||||||
|
| 관련 review | `<없음 또는 같은 작업 디렉터리의 reviews/ 아래 문서 링크>` |
|
||||||
|
|
||||||
|
### 요구사항 상태
|
||||||
|
|
||||||
|
| 상태 | 의미 | 구현 처리 |
|
||||||
|
|---|---|---|
|
||||||
|
| 확정 | 제품·기술 결정이 완료되어 구현 기준으로 사용 | `plan-task.md`의 Task와 완료 증거로 추적 |
|
||||||
|
| 미결 | 제품·UX·운영 결정이 더 필요함 | 권고안과 결정 주체·기한을 기록하고 임의 구현 금지 |
|
||||||
|
| 외부 의존 | 프론트엔드 밖의 계약·권한·환경 제공이 필요함 | 담당 주체·영향·재개 조건을 기록하고 추정 구현 금지 |
|
||||||
|
| 권고 | 미결 항목에 대한 현재 추천안 | 확정되기 전 계약이나 수용 기준으로 사용하지 않음 |
|
||||||
|
| 제외 | 현재 릴리스에서 구현하지 않기로 결정 | 제외 이유와 후속 조건을 Decision Log에 기록 |
|
||||||
|
|
||||||
|
### 문서 우선순위와 갱신 순서
|
||||||
|
|
||||||
|
1. 사용자·제품 결정은 이 PRD에 기록한다.
|
||||||
|
2. request/response/error 계약은 `api-contract.md`에 정규화한다.
|
||||||
|
3. 구현 범위·순서·완료 증거는 `plan-task.md`에 반영한다.
|
||||||
|
4. 요구사항이 바뀌면 Decision Log → 관련 요구사항·수용 기준 → API Contract → plan 순서로 갱신한다.
|
||||||
|
5. 기존 결정과 검증 기록은 삭제하거나 덮어쓰지 않고 정정 기록을 누적한다.
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
`<누가 어떤 상황에서 어떤 가치를 얻는 제품인지 2~4문장으로 설명한다.>`
|
||||||
|
|
||||||
|
## 2. Problem Statement
|
||||||
|
|
||||||
|
현재 사용자는 다음 문제를 겪는다.
|
||||||
|
|
||||||
|
- `<관찰 가능한 현재 문제>`
|
||||||
|
- `<기존 방식의 비용·위험·제약>`
|
||||||
|
- `<해결하지 않을 때의 사용자 또는 사업 영향>`
|
||||||
|
|
||||||
|
문제를 해결했다는 판단은 `<측정하거나 직접 확인할 결과>`로 한다.
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
|
||||||
|
### 3.1 제품 목표
|
||||||
|
|
||||||
|
- `<사용자가 완료할 수 있어야 하는 핵심 결과>`
|
||||||
|
- `<안전성·운영 효율·데이터 품질 목표>`
|
||||||
|
- `<릴리스 후 측정 가능한 성공 목표>`
|
||||||
|
|
||||||
|
### 3.2 UX 목표
|
||||||
|
|
||||||
|
- `<핵심 흐름의 명확성·속도 목표>`
|
||||||
|
- `<오류·loading·empty·success feedback 목표>`
|
||||||
|
- `<반응형·keyboard·접근성 목표>`
|
||||||
|
|
||||||
|
## 4. Non-Goals
|
||||||
|
|
||||||
|
- `<명시적으로 구현하지 않을 기능>`
|
||||||
|
- `<다음 릴리스 또는 외부 시스템 소유 범위>`
|
||||||
|
- `<복원, hard delete, 자동 갱신처럼 금지할 동작>`
|
||||||
|
|
||||||
|
Non-Goal을 변경하려면 Decision Log와 `plan-task.md` 범위를 먼저 갱신한다.
|
||||||
|
|
||||||
|
## 5. Target Users and Permissions
|
||||||
|
|
||||||
|
### 5.1 사용자
|
||||||
|
|
||||||
|
| 사용자 | 목표 | 주요 작업 | 사용 환경 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `<역할>` | `<달성 목표>` | `<조회·생성·수정 등>` | `<desktop/mobile 등>` |
|
||||||
|
|
||||||
|
### 5.2 권한
|
||||||
|
|
||||||
|
- 인증 주체: `<사용자 또는 시스템>`
|
||||||
|
- 허용 역할: `<role 목록>`
|
||||||
|
- 거부 조건: `<401/403 또는 제품 정책>`
|
||||||
|
- 리소스 소유권: `<tenant, characterId, ownerId 등 격리 기준>`
|
||||||
|
- read-only 조건: `<비활성·권한 부족·외부 상태 등>`
|
||||||
|
|
||||||
|
## 6. 핵심 사용자 흐름
|
||||||
|
|
||||||
|
1. `<시작 조건과 진입점>`
|
||||||
|
2. `<대상 탐색·선택>`
|
||||||
|
3. `<핵심 생성·조회·수정 동작>`
|
||||||
|
4. `<성공 feedback과 다음 화면>`
|
||||||
|
5. `<오류·권한·session 만료 복구>`
|
||||||
|
|
||||||
|
각 흐름은 `plan-task.md`의 최소 하나의 Phase 결과와 E2E 완료 증거로 연결한다.
|
||||||
|
|
||||||
|
## 7. 정보 구조와 라우팅
|
||||||
|
|
||||||
|
```text
|
||||||
|
/<entry>
|
||||||
|
/<resource>
|
||||||
|
/<resource>/:resourceId
|
||||||
|
/<child-resource>
|
||||||
|
```
|
||||||
|
|
||||||
|
- 전역 화면과 선택된 리소스 문맥의 경계를 명시한다.
|
||||||
|
- URL path와 query에 보존할 식별자·검색·filter·page 상태를 명시한다.
|
||||||
|
- 직접 링크·새로고침이 가능한 화면과 collection modal/Sheet처럼 별도 route가 없는 화면을 구분한다.
|
||||||
|
- 존재하지 않음, 다른 소유자, 비활성 리소스의 처리는 서버 계약과 공통 오류 정책을 따른다.
|
||||||
|
|
||||||
|
## 8. 기능 요구사항
|
||||||
|
|
||||||
|
요구사항 ID는 `<DOMAIN>-NNN` 형식을 사용한다. 하나의 행에는 독립적으로 판정 가능한 요구사항 하나만 작성한다.
|
||||||
|
|
||||||
|
### 8.1 `<도메인 A>`
|
||||||
|
|
||||||
|
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `DOMAINA-001` | 확정 | `<사용자가 할 수 있어야 하는 동작>` | `<관찰 가능한 성공·오류 결과>` | `api-contract.md §<번호>`, `P1-T1` |
|
||||||
|
| `DOMAINA-002` | 확정 | `<payload·상태·권한 불변식>` | `<보내야/보내지 말아야 할 값과 test>` | `P1-T2` |
|
||||||
|
| `DOMAINA-003` | 외부 의존 | `<외부 제공이 필요한 계약>` | `<제공 전 network integration 0건>` | `EXT-001`, `P1-T1` |
|
||||||
|
|
||||||
|
### 8.2 `<도메인 B>`
|
||||||
|
|
||||||
|
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `DOMAINB-001` | 확정 | `<목록·상세·mutation 흐름>` | `<loading·empty·error·success 포함>` | `api-contract.md §<번호>`, `P2-T2` |
|
||||||
|
| `DOMAINB-002` | 미결 | `<제품 결정이 필요한 항목>` | `<확정 전 최대값·동작 추정 금지>` | `OQ-001` |
|
||||||
|
|
||||||
|
### 8.3 공통 파일·데이터 정책
|
||||||
|
|
||||||
|
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `FILE-001` | 확정 | `<허용 확장자·MIME·크기>` | `<정확한 byte 경계 test>` | `api-contract.md §<번호>`, `<Goal ID>` |
|
||||||
|
| `DATA-001` | 확정 | `<날짜·가격·enum·pagination 규칙>` | `<formatter/schema/contract test>` | `<Goal ID>` |
|
||||||
|
|
||||||
|
## 9. 반응형 기능 범위
|
||||||
|
|
||||||
|
| 기능 | Desktop | Tablet | Mobile | 비고 |
|
||||||
|
|---|---:|---:|---:|---|
|
||||||
|
| 조회 | 전체 | 전체 | 전체 | `<예외>` |
|
||||||
|
| 생성·수정 | 허용 | 허용 | 허용 / 조회 전용 | `<직접 route 차단 정책>` |
|
||||||
|
| 파일 upload | 허용 | 허용 | 허용 / 미지원 | `<이유>` |
|
||||||
|
|
||||||
|
- 화면에서 action을 숨기는 것뿐 아니라 직접 route와 mutation capability도 같은 정책으로 차단한다.
|
||||||
|
- 최소 viewport, zoom, touch target과 virtual keyboard 조건을 수용 기준에 연결한다.
|
||||||
|
|
||||||
|
## 10. UI/UX Expectations
|
||||||
|
|
||||||
|
### 10.1 디자인과 component 원칙
|
||||||
|
|
||||||
|
- `<브랜드 token, theme 범위, typography>`
|
||||||
|
- Page는 route·query·permission·component 조합을 담당한다.
|
||||||
|
- 도메인 표시·입력·상호작용은 feature component가 담당한다.
|
||||||
|
- 두 개 이상 Phase에서 같은 의미·동작으로 재사용할 때만 shared component로 올린다.
|
||||||
|
|
||||||
|
### 10.2 화면 상태
|
||||||
|
|
||||||
|
- 모든 비동기 화면에 loading·empty·error·success 상태를 정의한다.
|
||||||
|
- mutation에는 진행 중·성공·실패·재시도·중복 제출 정책을 정의한다.
|
||||||
|
- destructive action에는 대상·영향·복구 여부를 알리는 확인 절차를 둔다.
|
||||||
|
|
||||||
|
### 10.3 접근성
|
||||||
|
|
||||||
|
- 모든 input은 visible label과 연결된 오류를 가진다.
|
||||||
|
- keyboard-only 흐름, focus 표시·복귀, skip link와 live region 기준을 명시한다.
|
||||||
|
- 색상만으로 상태를 전달하지 않고 목표 대비와 touch target을 명시한다.
|
||||||
|
- `<지원 viewport>`, 200% zoom과 axe critical·serious 0건을 수용 기준으로 사용한다.
|
||||||
|
|
||||||
|
## 11. API 계약
|
||||||
|
|
||||||
|
### 11.1 공통 규칙
|
||||||
|
|
||||||
|
- base URL과 인증 header: `<값>`
|
||||||
|
- locale: `<header와 값>`
|
||||||
|
- 성공 envelope: `<type 또는 예시>`
|
||||||
|
- 오류 envelope와 status: `<type 또는 예시>`
|
||||||
|
- pagination: `<page/size/total/hasNext 규칙>`
|
||||||
|
- multipart JSON part: `<이름>`
|
||||||
|
|
||||||
|
### 11.2 Endpoint 추적
|
||||||
|
|
||||||
|
| 요구사항 | Method | Path | 계약 상태 | API Contract | 소유 Goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `DOMAINA-001` | `<METHOD>` | `<path>` | 제공됨 / 보정 필요 / 제공 대기 | `§<번호>` | `<Goal ID>` |
|
||||||
|
|
||||||
|
### 11.3 외부 제공 대기 계약
|
||||||
|
|
||||||
|
| ID | 우선순위 | 제공 필요 계약 | 담당 주체 | 구현 영향 | 재개 조건 |
|
||||||
|
|---|---:|---|---|---|---|
|
||||||
|
| `EXT-001` | P0 / P1 | `<endpoint·DTO·오류 규칙>` | `<팀/역할>` | `<차단되는 흐름>` | `<문서와 fixture 제공>` |
|
||||||
|
|
||||||
|
- P0 계약이 없으면 영향을 받는 network flow를 완료로 표시하지 않는다.
|
||||||
|
- 계약에 의존하지 않는 UI shell·상태 inventory·문서화는 독립적으로 진행할 수 있다.
|
||||||
|
|
||||||
|
## 12. 보안과 데이터 취급
|
||||||
|
|
||||||
|
- 인증 정보 저장 위치와 lifecycle: `<정확한 정책>`
|
||||||
|
- log·분석·오류 리포트 금지 값: `<token, password, signed URL 등>`
|
||||||
|
- 업로드 파일명·MIME·본문 취급: `<정책>`
|
||||||
|
- 리소스 격리와 ownership 검증: `<정책>`
|
||||||
|
- 401·403·동시 실패 처리: `<정책>`
|
||||||
|
- 감사 로그: `<포함, 외부 의존 또는 후속 범위>`
|
||||||
|
|
||||||
|
## 13. 성능과 품질 요구사항
|
||||||
|
|
||||||
|
- 목록 pagination과 전체 로드 예외: `<정책>`
|
||||||
|
- 검색 debounce와 기존 데이터 유지: `<정책>`
|
||||||
|
- image layout shift와 lazy loading: `<정책>`
|
||||||
|
- mutation 중복 제출·upload 취소/재시도: `<정책>`
|
||||||
|
- 지원 runtime·browser: `<정확한 범위>`
|
||||||
|
- test stack과 필수 Gate: `<unit/integration/E2E/typecheck/lint/build>`
|
||||||
|
- backend 구현 전 UI 확인: `<불필요 또는 explicit mock mode, production 금지, no-auto-fallback, mock/server 완료 상태 분리>`
|
||||||
|
|
||||||
|
## 14. 성공 기준
|
||||||
|
|
||||||
|
### 14.1 기능 수용 기준
|
||||||
|
|
||||||
|
- [ ] `<핵심 사용자 journey가 성공한다.>` (`DOMAINA-001`, `P1-GATE`)
|
||||||
|
- [ ] `<권한·오류·payload 불변식이 검증된다.>` (`DOMAINA-002`, `<test>`)
|
||||||
|
- [ ] `<외부 의존의 구현 또는 제외 결정이 문서화된다.>` (`EXT-001`)
|
||||||
|
|
||||||
|
### 14.2 UI/UX 수용 기준
|
||||||
|
|
||||||
|
- [ ] loading·empty·error·success 상태가 있다.
|
||||||
|
- [ ] keyboard-only로 핵심 흐름을 완료한다.
|
||||||
|
- [ ] `<최소 viewport>`와 200% zoom에서 핵심 control이 가려지지 않는다.
|
||||||
|
- [ ] axe critical·serious 위반이 0건이다.
|
||||||
|
|
||||||
|
### 14.3 추적성 완료 기준
|
||||||
|
|
||||||
|
- [ ] 모든 `확정` 요구사항이 API Contract와 하나 이상의 Task/Goal 완료 증거로 연결된다.
|
||||||
|
- [ ] 모든 `미결` 항목에 결정 주체와 다음 행동이 있다.
|
||||||
|
- [ ] 모든 `외부 의존` 항목에 담당 주체·영향·재개 조건이 있다.
|
||||||
|
- [ ] `제외` 항목에 Decision Log와 후속 조건이 있다.
|
||||||
|
|
||||||
|
## 15. Open Questions
|
||||||
|
|
||||||
|
| ID | 상태 | 결정 필요 사항 | 현재 권고 | 결정 주체 | 결정 기한/시점 | 영향 Goal |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| `OQ-001` | 미결 | `<질문>` | `<권고안 또는 없음>` | `<역할>` | `<날짜 또는 UI 작성 후>` | `<Goal ID>` |
|
||||||
|
|
||||||
|
- Open Question과 외부 의존을 혼합하지 않는다. 제품이 결정할 수 없는 backend 계약은 `EXT-*`로 관리한다.
|
||||||
|
- 미결 값을 임의의 상수·enum·endpoint로 구현하지 않는다.
|
||||||
|
|
||||||
|
## 16. 요구사항 추적표
|
||||||
|
|
||||||
|
| 요구사항 범위 | API Contract | 계획 Phase | Goal | 자동 검증 | 수동 검증 |
|
||||||
|
|---|---|---:|---|---|---|
|
||||||
|
| `DOMAINA-001~003` | `§<번호>` | 1 | `P1-T1`, `P1-T2`, `P1-GATE` | `<test 경로>` | `<흐름>` |
|
||||||
|
| `DOMAINB-001~002` | `§<번호 또는 제공 대기>` | 2 | `P2-T1`, `P2-T2`, `P2-GATE` | `<test 경로>` | `<흐름>` |
|
||||||
|
|
||||||
|
## 17. Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계약·Goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `YYYY-MM-DD` | `DEC-001` | 확정 / 정정 / 폐기 | `<결정 내용>` | `<인터뷰·계약·검증 근거>` | `<ID와 문서 section>` |
|
||||||
|
|
||||||
|
- 기존 결정을 수정할 때 원문을 지우지 않고 `정정` 행을 추가한다.
|
||||||
|
- 범위 변경, Non-Goal 변경, 외부 의존 제외, 안전한 기본값과 주요 기술 선택을 기록한다.
|
||||||
|
|
||||||
|
## 18. 변경 관리
|
||||||
|
|
||||||
|
요구사항 변경 시 다음을 확인한다.
|
||||||
|
|
||||||
|
- [ ] Decision Log에 변경 이유와 날짜를 기록했다.
|
||||||
|
- [ ] 관련 요구사항 상태·본문·수용 기준을 갱신했다.
|
||||||
|
- [ ] API Contract의 request/response/error와 fixture를 갱신했다.
|
||||||
|
- [ ] `plan-task.md`의 범위·Files·Interfaces·체크박스·완료 증거를 코드 변경 전에 갱신했다.
|
||||||
|
- [ ] 기존 Progress·review·검증 기록을 삭제하거나 덮어쓰지 않았다.
|
||||||
187
docs/sample/sample-review.md
Normal file
187
docs/sample/sample-review.md
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# 코드 리뷰 보고서 샘플
|
||||||
|
|
||||||
|
> 이 문서는 완료된 Phase를 다시 검토할 때 사용하는 템플릿이다. 리뷰에서 발견한 후보를 먼저 검증하고, **확정**된 항목만 `plan-task.md`의 회귀 수정 Task와 goal로 전환한다. 기존 완료 체크박스와 검증 기록은 삭제하거나 되돌리지 않는다.
|
||||||
|
> 실제 리뷰 문서는 대상 PRD·`plan-task.md`와 같은 작업 디렉터리의 `reviews/` 아래에
|
||||||
|
> `docs/[날짜]_구현할내용한글/reviews/[리뷰범위]-review.md` 형식으로 저장한다.
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase `<번호>` / Task `<번호 또는 범위>` |
|
||||||
|
| 기준 commit 또는 working tree | `<commit SHA 또는 변경 상태>` |
|
||||||
|
| 리뷰 일자 | `YYYY-MM-DD` |
|
||||||
|
| 리뷰어 | `<이름 또는 agent>` |
|
||||||
|
| 기준 문서 | `<대상 prd.md, api-contract.md, plan-task.md 경로>` |
|
||||||
|
| 리뷰 상태 | 진행 중 / 판정 완료 / 수정 검증 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
### 목적
|
||||||
|
|
||||||
|
- `<예: Phase 0~1 구현이 요구사항과 API Contract를 충족하는지 확인한다.>`
|
||||||
|
- `<예: 완료 체크박스와 실제 코드·test·검증 기록이 일치하는지 확인한다.>`
|
||||||
|
|
||||||
|
### 포함 범위
|
||||||
|
|
||||||
|
- 코드: `<검토할 경로>`
|
||||||
|
- 테스트: `<검토할 unit/integration/E2E 경로>`
|
||||||
|
- 문서: `<검토할 요구사항·계약·Task 범위>`
|
||||||
|
- 수동 검증: `<브라우저, viewport, keyboard, 접근성 등>`
|
||||||
|
|
||||||
|
### 제외 범위
|
||||||
|
|
||||||
|
- `<이번 리뷰에서 다루지 않는 Phase, 기능 또는 외부 계약>`
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
### 심각도
|
||||||
|
|
||||||
|
| 심각도 | 기준 |
|
||||||
|
|---|---|
|
||||||
|
| Blocker | 보안·데이터 손실 위험, 핵심 흐름 불능, 완료 판정을 무효화하는 문제 |
|
||||||
|
| High | 확정 요구사항·API Contract 위반 또는 주요 회귀 |
|
||||||
|
| Medium | 제한된 조건에서 발생하는 기능·접근성·복구 문제 |
|
||||||
|
| Low | 유지보수성, 문서 정합성 또는 비핵심 UX 문제 |
|
||||||
|
|
||||||
|
### 상태
|
||||||
|
|
||||||
|
| 상태 | 의미 | 후속 처리 |
|
||||||
|
|---|---|---|
|
||||||
|
| 후보 | 근거를 발견했지만 아직 재현·판정하지 않음 | 검증 후 상태 변경 |
|
||||||
|
| 확정 | 코드·test·문서 근거로 문제가 확인됨 | `plan-task.md` 회귀 수정 Task 후보 |
|
||||||
|
| 오탐 | 요구사항이나 실행 결과상 문제가 아님 | 근거를 남기고 종료 |
|
||||||
|
| 보류 | 외부 계약·환경·제품 결정이 필요함 | 담당 주체와 재개 조건 기록 |
|
||||||
|
| 수정 완료 | 수정과 관련 검증이 완료됨 | 실행 명령과 결과 연결 |
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
### 문서와 코드
|
||||||
|
|
||||||
|
- 요구사항: `<예: AUTH-001~013>`
|
||||||
|
- API Contract: `<예: §3 인증>`
|
||||||
|
- 계획: `<예: P1-T2, P1-T3, P1-GATE>`
|
||||||
|
- 코드: `<파일 경로와 line>`
|
||||||
|
- 테스트: `<테스트 파일과 test name>`
|
||||||
|
|
||||||
|
### 실행 환경
|
||||||
|
|
||||||
|
```text
|
||||||
|
OS: <값>
|
||||||
|
Node: <값>
|
||||||
|
npm: <값>
|
||||||
|
Browser/viewport: <값>
|
||||||
|
환경 변수: 민감정보를 제외한 이름과 사용 mode만 기록
|
||||||
|
```
|
||||||
|
|
||||||
|
### 실행한 검증
|
||||||
|
|
||||||
|
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|
||||||
|
|---|---|---|
|
||||||
|
| `<실제 실행 명령>` | 성공 / 실패 / 불가 | `<exit code, test 수, 오류 또는 불가 사유>` |
|
||||||
|
| `<수동 검증 절차>` | 성공 / 실패 / 불가 | `<관찰 결과>` |
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `REV-P1-001` | `<심각도>` | 후보 | `<한 문장 제목>` | `<예: P1-T3>` | 판정 전 |
|
||||||
|
|
||||||
|
발견 사항이 없으면 “확정 발견 사항 없음”이라고 명시하고, 검토 범위와 실행 증거는 그대로 남긴다.
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
### REV-P1-001 — `<한 문장 제목>`
|
||||||
|
|
||||||
|
- **심각도:** `<Blocker | High | Medium | Low>`
|
||||||
|
- **상태:** `<후보 | 확정 | 오탐 | 보류 | 수정 완료>`
|
||||||
|
- **관련 요구사항:** `<요구사항 ID 또는 없음>`
|
||||||
|
- **관련 계약:** `<api-contract.md section 또는 없음>`
|
||||||
|
- **소유 Task:** `<기존 Goal ID 또는 신규 회귀 Task>`
|
||||||
|
|
||||||
|
**관찰 내용**
|
||||||
|
|
||||||
|
`<실제로 관찰한 동작을 추정 없이 작성한다.>`
|
||||||
|
|
||||||
|
**근거**
|
||||||
|
|
||||||
|
- 코드: `<파일 경로:line과 관련 동작>`
|
||||||
|
- 테스트: `<테스트 파일:test name 또는 누락 사실>`
|
||||||
|
- 문서: `<문서 경로와 요구사항/계약/계획 항목>`
|
||||||
|
|
||||||
|
**재현 또는 검증 절차**
|
||||||
|
|
||||||
|
1. `<사전 조건>`
|
||||||
|
2. `<실행 명령 또는 사용자 동작>`
|
||||||
|
3. `<실제 결과>`
|
||||||
|
4. `<요구되는 결과>`
|
||||||
|
|
||||||
|
**영향**
|
||||||
|
|
||||||
|
`<사용자, 데이터, 보안, 접근성, 회귀 범위를 구체적으로 작성한다.>`
|
||||||
|
|
||||||
|
**권장 조치**
|
||||||
|
|
||||||
|
`<최소 수정 방향과 추가해야 할 회귀 test를 작성한다. 구현 전 확정되지 않은 endpoint·DTO·오류 값은 추정하지 않는다.>`
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- `YYYY-MM-DD` — `<확정/오탐/보류 판정과 근거>`
|
||||||
|
- `YYYY-MM-DD` — `<후속 정정이 있으면 기존 기록을 지우지 않고 추가>`
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
확정 발견 사항이 없으면 이 절에 “전환 항목 없음”을 기록한다. 확정 항목이 있으면 구현 전에 `plan-task.md`에 아래 내용을 반영한다.
|
||||||
|
|
||||||
|
### 신규 회귀 수정 Task 초안
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### Task R<번호>.<번호> <수정할 결과>
|
||||||
|
|
||||||
|
**Goal 실행 `P<Phase>-R<번호>`:** <확정된 문제를 수정하고 회귀를 방지하는 한 문장 objective>
|
||||||
|
|
||||||
|
- **시작 조건:** <관련 review ID, 기존 Task/Gate, 필요한 계약>
|
||||||
|
- **완료 증거:** <실패 재현 test → 수정 후 focused test → Phase Gate → 검증 기록>
|
||||||
|
- **범위 밖:** <이번 수정에서 건드리지 않을 기능>
|
||||||
|
|
||||||
|
- [ ] `<재현 가능한 실패 test를 먼저 추가하고 의도한 assertion 실패를 확인한다.>`
|
||||||
|
- [ ] `<최소 수정으로 test를 통과시킨다.>`
|
||||||
|
- [ ] `<관련 focused test와 Phase Gate를 실행한다.>`
|
||||||
|
- [ ] `<plan-task.md 하단에 무엇을/왜/어떻게를 누적한다.>`
|
||||||
|
```
|
||||||
|
|
||||||
|
### create_goal objective 초안
|
||||||
|
|
||||||
|
```text
|
||||||
|
[P<Phase>-R<번호>]의 확정 review 항목 <REV-ID 목록>을 수정하고 회귀를 방지한다.
|
||||||
|
plan-task.md에 추가된 회귀 수정 Task만 수행한다.
|
||||||
|
실패 재현, 최소 수정, focused test, Phase Gate와 검증 기록이 모두 끝나기 전에는 complete로 표시하지 않는다.
|
||||||
|
관련 없는 리팩터링과 계약 추정은 범위 밖이다.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 / 미충족 | `<근거>` |
|
||||||
|
| 후보 항목 판정 완료 | 충족 / 미충족 | `<근거>` |
|
||||||
|
| 확정 항목 plan 반영 | 충족 / 해당 없음 / 미충족 | `<Task 또는 사유>` |
|
||||||
|
| 보류 항목의 담당·재개 조건 기록 | 충족 / 해당 없음 / 미충족 | `<근거>` |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 / 미충족 | `<근거>` |
|
||||||
|
|
||||||
|
**최종 결론:** `<확정 발견 사항 없음 | 수정 goal 필요 | 외부 조건 대기 | 수정 검증 완료>`
|
||||||
|
|
||||||
|
**남은 항목:** `<없음 또는 review ID와 다음 행동>`
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
기존 기록을 삭제하거나 덮어쓰지 않고 차수별로 누적한다.
|
||||||
|
|
||||||
|
### N차 수정 검증 — YYYY-MM-DD
|
||||||
|
|
||||||
|
- 무엇을: `<수정한 review ID와 결과>`
|
||||||
|
- 왜: `<요구사항·계약 위반 또는 회귀 위험>`
|
||||||
|
- 어떻게:
|
||||||
|
- `<실행 명령>` — `<성공/실패와 핵심 수치>`
|
||||||
|
- `<수동 검증>` — `<성공/실패/불가 사유>`
|
||||||
|
- 남은 항목: `<없음, 보류 또는 후속 review ID>`
|
||||||
Reference in New Issue
Block a user