test(creator): 커뮤니티 탭 레이아웃 검증을 추가한다
This commit is contained in:
@@ -0,0 +1,232 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.community
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertSame
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(sdk = [28], application = Application::class)
|
||||||
|
class CreatorChannelCommunityFragmentLayoutTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 fragment layout은 sort list empty error retry를 제공한다`() {
|
||||||
|
val root = inflateView(R.layout.fragment_creator_channel_community)
|
||||||
|
val layout = projectFile("app/src/main/res/layout/fragment_creator_channel_community.xml").readText()
|
||||||
|
|
||||||
|
val sortBar = requireNotNull(root.findViewById<View>(R.id.layout_creator_channel_community_sort_bar))
|
||||||
|
val communityList = requireNotNull(root.findViewById<RecyclerView>(R.id.rv_creator_channel_community))
|
||||||
|
val emptyContainer = requireNotNull(root.findViewById<View>(R.id.layout_creator_channel_community_empty))
|
||||||
|
val emptyMessage = requireNotNull(root.findViewById<TextView>(R.id.tv_creator_channel_community_empty_message))
|
||||||
|
val errorMessage = requireNotNull(root.findViewById<TextView>(R.id.tv_creator_channel_community_error_message))
|
||||||
|
val retryButton = requireNotNull(root.findViewById<TextView>(R.id.btn_creator_channel_community_retry))
|
||||||
|
|
||||||
|
assertSame(root, sortBar.parent)
|
||||||
|
assertSame(root, communityList.parent)
|
||||||
|
assertSame(root, emptyContainer.parent)
|
||||||
|
assertSame(emptyContainer, emptyMessage.parent)
|
||||||
|
assertSame(root, errorMessage.parent)
|
||||||
|
assertSame(root, retryButton.parent)
|
||||||
|
assertEquals(false, communityList.clipToPadding)
|
||||||
|
assertTrue(layout.contains("android:background=\"@color/black\""))
|
||||||
|
assertTrue(layout.contains("android:text=\"@string/creator_channel_community_empty_message\""))
|
||||||
|
assertTrue(layout.contains("android:text=\"@string/creator_channel_community_error_message\""))
|
||||||
|
assertTrue(layout.contains("tools:listitem=\"@layout/item_creator_channel_community_list\""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 sort bar는 전체 count 보기 방식 label icon을 제공한다`() {
|
||||||
|
val root = inflateView(R.layout.fragment_creator_channel_community)
|
||||||
|
val sortBar = requireNotNull(root.findViewById<View>(R.id.layout_creator_channel_community_sort_bar))
|
||||||
|
|
||||||
|
assertNotNull(sortBar.findViewById<TextView>(R.id.tv_creator_channel_community_total_label))
|
||||||
|
assertNotNull(sortBar.findViewById<TextView>(R.id.tv_creator_channel_community_total_count))
|
||||||
|
assertNotNull(sortBar.findViewById<View>(R.id.layout_creator_channel_community_view_mode_button))
|
||||||
|
assertNotNull(sortBar.findViewById<TextView>(R.id.tv_creator_channel_community_view_mode_label))
|
||||||
|
assertNotNull(sortBar.findViewById<ImageView>(R.id.iv_creator_channel_community_view_mode))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 list item layout은 프로필 본문 이미지 잠금 재생 반응 owner 영역을 제공한다`() {
|
||||||
|
val item = inflateView(R.layout.item_creator_channel_community_list)
|
||||||
|
val itemLayout = projectFile("app/src/main/res/layout/item_creator_channel_community_list.xml").readText()
|
||||||
|
val imageContainer = requireNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_image_container))
|
||||||
|
val lockIcon = requireNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_lock))
|
||||||
|
val lockedPrice = requireNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_locked_price))
|
||||||
|
|
||||||
|
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_profile))
|
||||||
|
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_nickname))
|
||||||
|
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_time))
|
||||||
|
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_notice))
|
||||||
|
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_body))
|
||||||
|
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_image))
|
||||||
|
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_locked_overlay))
|
||||||
|
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_locked_price))
|
||||||
|
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_play))
|
||||||
|
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_comment_count))
|
||||||
|
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_like_count))
|
||||||
|
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions))
|
||||||
|
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_owner_more))
|
||||||
|
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_top_price))
|
||||||
|
assertSame(imageContainer, (lockIcon.parent as View).parent)
|
||||||
|
assertSame(imageContainer, (lockedPrice.parent as View).parent)
|
||||||
|
assertSame(
|
||||||
|
item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions),
|
||||||
|
item.findViewById<TextView>(R.id.tv_creator_channel_community_list_top_price).parent
|
||||||
|
)
|
||||||
|
assertTrue(itemLayout.contains("android:id=\"@+id/layout_creator_channel_community_list_locked_overlay\""))
|
||||||
|
assertTrue(itemLayout.contains("android:id=\"@+id/tv_creator_channel_community_list_locked_price\""))
|
||||||
|
assertTrue(itemLayout.contains("android:background=\"@drawable/bg_creator_channel_community_price\""))
|
||||||
|
assertTrue(itemLayout.contains("android:drawableStart=\"@drawable/ic_bar_cash\""))
|
||||||
|
assertTrue(itemLayout.contains("android:id=\"@+id/iv_creator_channel_community_list_play\""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 grid item layout은 이미지 본문 잠금 가격 공지와 정사각 root 계약을 제공한다`() {
|
||||||
|
val item = inflateView(R.layout.item_creator_channel_community_grid)
|
||||||
|
val itemLayout = projectFile("app/src/main/res/layout/item_creator_channel_community_grid.xml").readText()
|
||||||
|
|
||||||
|
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_grid_image))
|
||||||
|
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_grid_text_preview))
|
||||||
|
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_grid_lock_price))
|
||||||
|
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_grid_notice))
|
||||||
|
assertTrue(itemLayout.contains("android:id=\"@+id/layout_creator_channel_community_grid_root\""))
|
||||||
|
assertTrue(itemLayout.contains("app:layout_constraintDimensionRatio=\"1:1\"") || itemLayout.contains("Square"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 문자열은 한국어 영어 일본어와 기존 보기 방식 label에 존재한다`() {
|
||||||
|
val ko = projectFile("app/src/main/res/values/strings.xml").readText()
|
||||||
|
val en = projectFile("app/src/main/res/values-en/strings.xml").readText()
|
||||||
|
val ja = projectFile("app/src/main/res/values-ja/strings.xml").readText()
|
||||||
|
|
||||||
|
listOf(ko, en, ja).forEach { strings ->
|
||||||
|
assertTrue(strings.contains("name=\"creator_channel_community_empty_message\""))
|
||||||
|
assertTrue(strings.contains("name=\"creator_channel_community_error_message\""))
|
||||||
|
assertTrue(strings.contains("name=\"creator_channel_community_retry_button\""))
|
||||||
|
assertTrue(strings.contains("name=\"creator_channel_community_notice\""))
|
||||||
|
assertTrue(strings.contains("name=\"creator_channel_community_view_mode_list\""))
|
||||||
|
assertTrue(strings.contains("name=\"creator_channel_community_view_mode_grid\""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 fragment source는 pagination view mode owner padding stopContent 계약을 사용한다`() {
|
||||||
|
val fragment = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(fragment.contains("BaseFragment<FragmentCreatorChannelCommunityBinding>"))
|
||||||
|
assertTrue(fragment.contains("private val viewModel: CreatorChannelCommunityViewModel by viewModel()"))
|
||||||
|
assertTrue(fragment.contains("viewModel.communityStateLiveData.observe(viewLifecycleOwner)"))
|
||||||
|
assertTrue(fragment.contains("viewModel.loadCommunity(creatorId, isOwner = host.isCreatorChannelOwner())"))
|
||||||
|
assertTrue(fragment.contains("viewModel.loadMore()"))
|
||||||
|
assertTrue(fragment.contains("viewModel.retryCommunity()"))
|
||||||
|
assertTrue(fragment.contains("viewModel.toggleViewMode()"))
|
||||||
|
assertTrue(fragment.contains("LinearLayoutManager"))
|
||||||
|
assertTrue(
|
||||||
|
fragment.contains("GridLayoutManager(requireContext(), 3") ||
|
||||||
|
fragment.contains("GridLayoutManager(context, 3")
|
||||||
|
)
|
||||||
|
assertTrue(fragment.contains("viewModel.consumePaginationErrorMessage()"))
|
||||||
|
assertTrue(fragment.contains("applyOwnerCtaPadding"))
|
||||||
|
assertTrue(fragment.contains("pauseContent"))
|
||||||
|
assertTrue(fragment.contains("stopContent"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 adapters source는 binding 상태 표시와 root click navigation 배제를 보장한다`() {
|
||||||
|
val listAdapter = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityListAdapter.kt"
|
||||||
|
).readText()
|
||||||
|
val gridAdapter = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityGridAdapter.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(listAdapter.contains("ItemCreatorChannelCommunityListBinding"))
|
||||||
|
assertTrue(gridAdapter.contains("ItemCreatorChannelCommunityGridBinding"))
|
||||||
|
assertTrue(listAdapter.contains("submitItems"))
|
||||||
|
assertTrue(gridAdapter.contains("submitItems"))
|
||||||
|
assertTrue(listAdapter.contains("showComment") && listAdapter.contains("isVisible = item.showComment"))
|
||||||
|
assertTrue(listAdapter.contains("isLocked"))
|
||||||
|
assertTrue(gridAdapter.contains("isLocked"))
|
||||||
|
assertTrue(listAdapter.contains("showPlayButton"))
|
||||||
|
assertTrue(listAdapter.contains("showOwnerMore"))
|
||||||
|
assertTrue(listAdapter.contains("isPlayingContent"))
|
||||||
|
assertTrue(listAdapter.contains("R.drawable.ic_player_pause"))
|
||||||
|
assertTrue(listAdapter.contains("R.drawable.ic_new_player_play"))
|
||||||
|
assertTrue(listAdapter.contains("onOwnerMoreClick(item)"))
|
||||||
|
assertTrue(listAdapter.contains("tvCreatorChannelCommunityListLockedPrice.isVisible = item.isLocked"))
|
||||||
|
assertTrue(listAdapter.contains("tvCreatorChannelCommunityListTopPrice.isVisible = item.showOwnerTopPrice"))
|
||||||
|
assertTrue(!listAdapter.contains("item.isLocked || item.showOwnerTopPrice"))
|
||||||
|
assertTrue(!listAdapter.contains("root.setOnClickListener"))
|
||||||
|
assertTrue(!gridAdapter.contains("root.setOnClickListener"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 media player source는 prepare 완료 전 pause stop 호출을 보호한다`() {
|
||||||
|
val playerManager = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/player/" +
|
||||||
|
"CreatorCommunityMediaPlayerManager.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(playerManager.contains("private var isPrepared: Boolean = false"))
|
||||||
|
assertTrue(playerManager.contains("if (isPrepared)"))
|
||||||
|
assertTrue(playerManager.contains("isPrepared = true"))
|
||||||
|
assertTrue(playerManager.contains("isPrepared = false"))
|
||||||
|
assertTrue(playerManager.contains("mediaPlayer?.pause()"))
|
||||||
|
assertTrue(playerManager.contains("it.stop()"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 adapters source는 잠금 항목과 text preview에서 imageUrl load를 호출하지 않는다`() {
|
||||||
|
val listAdapter = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityListAdapter.kt"
|
||||||
|
).readText()
|
||||||
|
val gridAdapter = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityGridAdapter.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(listAdapter.contains("val visibleImageUrl = item.imageUrl.takeUnless { item.isLocked }"))
|
||||||
|
assertTrue(listAdapter.contains("ivCreatorChannelCommunityListImage.setImageDrawable(null)"))
|
||||||
|
assertTrue(gridAdapter.contains("val visibleImageUrl = item.imageUrl.takeIf"))
|
||||||
|
assertTrue(gridAdapter.contains("item.imageMode == CreatorChannelCommunityImageMode.Image"))
|
||||||
|
assertTrue(gridAdapter.contains("ivCreatorChannelCommunityGridImage.setImageDrawable(null)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 grid adapter source는 margin을 제외한 3열 정사각 크기를 계산한다`() {
|
||||||
|
val gridAdapter = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityGridAdapter.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(gridAdapter.contains("leftMargin + rightMargin"))
|
||||||
|
assertTrue(gridAdapter.contains("availableWidth - totalHorizontalMargins"))
|
||||||
|
assertTrue(gridAdapter.contains("coerceAtLeast(0)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inflateView(layoutResId: Int): View {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
return LayoutInflater.from(context).inflate(layoutResId, null, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun projectFile(relativePath: String): File {
|
||||||
|
val candidates = listOf(File(relativePath), File("../$relativePath"))
|
||||||
|
return candidates.firstOrNull { it.exists() }
|
||||||
|
?: error("Project file not found: $relativePath")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -315,7 +315,7 @@
|
|||||||
|
|
||||||
### Phase 4: Fragment, Adapter, XML UI 구현
|
### Phase 4: Fragment, Adapter, XML UI 구현
|
||||||
|
|
||||||
- [ ] **Task 4.1: Fragment layout 추가**
|
- [x] **Task 4.1: Fragment layout 추가**
|
||||||
- 생성:
|
- 생성:
|
||||||
- `app/src/main/res/layout/fragment_creator_channel_community.xml`
|
- `app/src/main/res/layout/fragment_creator_channel_community.xml`
|
||||||
- 작업:
|
- 작업:
|
||||||
@@ -326,8 +326,11 @@
|
|||||||
- `./gradlew :app:mergeDebugResources`
|
- `./gradlew :app:mergeDebugResources`
|
||||||
- 기대 결과:
|
- 기대 결과:
|
||||||
- 신규 layout binding 생성이 PASS한다.
|
- 신규 layout binding 생성이 PASS한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 2026-06-21: RED 단계에서 `CreatorChannelCommunityFragmentLayoutTest`를 먼저 추가했고, production 파일 추가 전 `compileDebugUnitTestKotlin`이 fragment/list/grid layout ID, resource, source file 미구현으로 실패함을 확인했다.
|
||||||
|
- 2026-06-21: `fragment_creator_channel_community.xml`을 추가해 Sort-bar, 단일 `RecyclerView`, empty/error/retry 영역을 구성했다. `./gradlew :app:mergeDebugResources` PASS로 신규 layout binding 생성을 확인했다.
|
||||||
|
|
||||||
- [ ] **Task 4.2: 리스트형 item layout/adapter 추가**
|
- [x] **Task 4.2: 리스트형 item layout/adapter 추가**
|
||||||
- 생성:
|
- 생성:
|
||||||
- `app/src/main/res/layout/item_creator_channel_community_list.xml`
|
- `app/src/main/res/layout/item_creator_channel_community_list.xml`
|
||||||
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityListAdapter.kt`
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityListAdapter.kt`
|
||||||
@@ -344,8 +347,14 @@
|
|||||||
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragmentLayoutTest"`
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragmentLayoutTest"`
|
||||||
- 기대 결과:
|
- 기대 결과:
|
||||||
- layout resource와 adapter bind source 검증이 PASS한다.
|
- layout resource와 adapter bind source 검증이 PASS한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 2026-06-21: `item_creator_channel_community_list.xml`과 `CreatorChannelCommunityListAdapter.kt`를 추가해 리스트형 card, 댓글 숨김, 유료 미구매 잠금, play button, owner more/price 표시 정책을 binding 경로에 반영했다.
|
||||||
|
- 2026-06-21: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragmentLayoutTest"` PASS로 리스트 layout/resource와 adapter bind source 검증을 확인했다.
|
||||||
|
- 2026-06-21: Phase 4 reviewer gate에서 유료 미구매 locked item의 `imageUrl` 유지와 이미지 로드 가능성, locked price pill 표시 검증 부족, play/pause icon이 실제 재생 상태와 분리된 점, owner more callback에 item 정보와 고정 상태가 부족한 점으로 초기 FAIL을 확인했다. 보정으로 locked item은 `imageUrl`을 비우고 이미지 로드를 막았으며, 가격 pill 표시를 테스트로 고정하고, `isPlayingContent(postId)` 기반 play/pause icon bind와 owner more item callback에 `isPinned` 보존 정보를 반영했다. 보정 후 Phase 4 관련 검증은 PASS했다.
|
||||||
|
- 2026-06-21: 최종 Phase 4 review fix로 리스트형 locked price capsule을 `tv_creator_channel_community_list_locked_price`가 이미지 잠금 영역 안에 표시되도록 이동했다. `ListAdapter`의 locked price와 top price 표시 조건도 분리해, 상단 가격은 본인 채널 owner-only 조건에서만 다시 보이도록 복구했다.
|
||||||
|
- 2026-06-22: Phase 4 코드 리뷰에서 Figma `665:19021` 본인 채널 리스트형 유료 게시글의 가격 태그와 더보기 버튼이 feed 카드 우측 상단 `etc` 영역에 함께 배치되는 것을 확인했다. 기존 XML은 `tv_creator_channel_community_list_top_price`가 reaction row 우측에 있어 요구사항과 어긋났으므로 RED 테스트를 추가한 뒤, `layout_creator_channel_community_list_top_actions` 컨테이너 안으로 가격 태그와 더보기 버튼을 이동했다. `CreatorChannelCommunityListAdapter`는 상단 액션 컨테이너 visibility를 `showOwnerMore || showOwnerTopPrice`로 bind하도록 보정했다.
|
||||||
|
|
||||||
- [ ] **Task 4.3: 썸네일형 grid item layout/adapter 추가**
|
- [x] **Task 4.3: 썸네일형 grid item layout/adapter 추가**
|
||||||
- 생성:
|
- 생성:
|
||||||
- `app/src/main/res/layout/item_creator_channel_community_grid.xml`
|
- `app/src/main/res/layout/item_creator_channel_community_grid.xml`
|
||||||
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityGridAdapter.kt`
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityGridAdapter.kt`
|
||||||
@@ -361,8 +370,12 @@
|
|||||||
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragmentLayoutTest"`
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragmentLayoutTest"`
|
||||||
- 기대 결과:
|
- 기대 결과:
|
||||||
- grid layout/resource 검증이 PASS한다.
|
- grid layout/resource 검증이 PASS한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 2026-06-21: `item_creator_channel_community_grid.xml`과 `CreatorChannelCommunityGridAdapter.kt`를 추가해 3열 정사각형 grid, 이미지/텍스트 preview, 잠금/가격, notice 표시 정책을 구현했다.
|
||||||
|
- 2026-06-21: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragmentLayoutTest"` PASS와 `./gradlew :app:mergeDebugResources` PASS로 grid layout/resource 검증을 확인했다.
|
||||||
|
- 2026-06-21: Phase 4 reviewer gate에서 grid item 크기가 좌우 margin을 반영하지 않아 3열 정사각형 sizing이 과대 계산될 수 있다는 FAIL을 확인했다. 보정으로 grid adapter의 item 크기 계산에 RecyclerView padding과 item margin을 반영했고, margin-aware sizing 테스트를 추가했다. 보정 후 grid layout 검증은 PASS했다.
|
||||||
|
|
||||||
- [ ] **Task 4.4: `CreatorChannelCommunityFragment` 구현**
|
- [x] **Task 4.4: `CreatorChannelCommunityFragment` 구현**
|
||||||
- 생성:
|
- 생성:
|
||||||
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt`
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt`
|
||||||
- 작업:
|
- 작업:
|
||||||
@@ -378,8 +391,13 @@
|
|||||||
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"`
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"`
|
||||||
- 기대 결과:
|
- 기대 결과:
|
||||||
- Fragment와 adapter 테스트가 GREEN이다.
|
- Fragment와 adapter 테스트가 GREEN이다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 2026-06-21: `CreatorChannelCommunityFragment.kt`를 추가해 ViewModel 상태 observe, Loading/Empty/Error/Content bind, 보기 방식 toggle, List/Grid `LayoutManager` 전환, pagination error consume, tab/scroll/owner CTA entry, media player 정리 경로를 구현했다.
|
||||||
|
- 2026-06-21: 병렬 Gradle 실행 1건에서 Kotlin incremental cache/daemon 충돌과 timeout이 있었고, 영향받은 community test를 단독 재실행해 PASS를 확인했다. 이후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"` PASS와 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Community*"` PASS를 확인했다.
|
||||||
|
- 2026-06-21: Phase 4 reviewer gate에서 Fragment가 화면 이탈 `onPause()` 시 오디오를 멈추지 않아 백그라운드 재생이 남을 수 있다는 FAIL을 확인했다. 보정으로 `onPause()`에서 `pauseContent()`를 호출하고, 기존 정리 경로의 `stopContent()`는 유지했다. 보정 후 Fragment 생명주기 검증은 PASS했다.
|
||||||
|
- 2026-06-21: 최종 Phase 4 review fix로 `CreatorCommunityMediaPlayerManager`에 `prepareAsync()` 완료 전 `pauseContent()`와 `stopContent()`가 호출될 수 있는 경로를 막는 prepared-state guard를 추가했다. prepare 전 pause/stop 호출은 MediaPlayer invalid state를 만들지 않도록 보호하고, Fragment 생명주기 정리 경로는 유지했다.
|
||||||
|
|
||||||
- [ ] **Task 4.5: 문자열/리소스 정리**
|
- [x] **Task 4.5: 문자열/리소스 정리**
|
||||||
- 수정:
|
- 수정:
|
||||||
- `app/src/main/res/values/strings.xml`
|
- `app/src/main/res/values/strings.xml`
|
||||||
- `app/src/main/res/values-en/strings.xml`
|
- `app/src/main/res/values-en/strings.xml`
|
||||||
@@ -392,6 +410,9 @@
|
|||||||
- `./gradlew :app:mergeDebugResources`
|
- `./gradlew :app:mergeDebugResources`
|
||||||
- 기대 결과:
|
- 기대 결과:
|
||||||
- 한국어/영어/일본어 string 참조가 모두 해소된다.
|
- 한국어/영어/일본어 string 참조가 모두 해소된다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 2026-06-21: community 관련 문자열을 `strings.xml`, `values-en/strings.xml`, `values-ja/strings.xml`에 추가하고 기존 재사용 가능한 문구는 중복하지 않았다.
|
||||||
|
- 2026-06-21: `./gradlew :app:mergeDebugResources` PASS, `./gradlew :app:compileDebugKotlin` PASS, `./gradlew :app:ktlintCheck` PASS, `git diff --check` PASS를 확인했다. `ktlintCheck`는 신규 layout test와 list adapter의 formatting-only 수정 후 PASS했다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -513,3 +534,45 @@
|
|||||||
- 회귀: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Community*"` PASS.
|
- 회귀: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Community*"` PASS.
|
||||||
- 확장 회귀: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"` PASS.
|
- 확장 회귀: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"` PASS.
|
||||||
- 빌드/리소스/린트: `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` PASS.
|
- 빌드/리소스/린트: `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` PASS.
|
||||||
|
|
||||||
|
- 2026-06-21 Phase 4 검증:
|
||||||
|
- RED: `CreatorChannelCommunityFragmentLayoutTest`를 먼저 추가했고 production 구현 전 `compileDebugUnitTestKotlin`이 fragment/list/grid layout ID, resource, source file 미구현으로 실패함을 확인했다.
|
||||||
|
- Production: `fragment_creator_channel_community.xml`, `item_creator_channel_community_list.xml`, `item_creator_channel_community_grid.xml`, `CreatorChannelCommunityFragment.kt`, `CreatorChannelCommunityListAdapter.kt`, `CreatorChannelCommunityGridAdapter.kt`, ko/en/ja community strings를 추가했다.
|
||||||
|
- GREEN: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragmentLayoutTest"` PASS.
|
||||||
|
- 회귀: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Community*"` PASS.
|
||||||
|
- 빌드/리소스/린트: `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` PASS. `ktlintCheck`는 신규 layout test와 list adapter의 formatting-only 수정 후 PASS했다.
|
||||||
|
- 참고: 병렬 Gradle 실행 1건에서 Kotlin incremental cache/daemon 충돌과 timeout이 발생했고, 영향받은 community test를 단독 재실행해 PASS를 확인했다.
|
||||||
|
|
||||||
|
- 2026-06-21 Phase 4 reviewer gate 수정 및 재검증:
|
||||||
|
- 초기 결과: reviewer gate가 locked image와 price pill, play/pause 상태, owner more item 정보, `onPause()` media pause, grid margin sizing 문제로 FAIL했다.
|
||||||
|
- 수정 기록: `isPinned` 보존, locked item `imageUrl` clearing과 이미지 load 차단, locked price pill 검증, `isPlayingContent(postId)` 기반 play/pause icon, owner more item callback, Fragment `onPause()`의 `pauseContent()`, grid margin-aware sizing, 관련 테스트 갱신을 반영했다.
|
||||||
|
- GREEN: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"` PASS.
|
||||||
|
- 회귀: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Community*"` PASS.
|
||||||
|
- 빌드/리소스/린트: `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` PASS.
|
||||||
|
- 최종 결과: post-fix reviewer gate PASS로 Phase 4 review-gate fixes 검증을 완료했다.
|
||||||
|
|
||||||
|
- 2026-06-21 Phase 4 최종 리뷰 수정 및 검증:
|
||||||
|
- 최종 수정 기록: 리스트형 locked list price capsule은 `tv_creator_channel_community_list_locked_price`를 이미지 잠금 영역 안으로 이동해 locked card 내부에서 표시되게 했다. `ListAdapter`는 locked price와 top price 조건을 분리했고, top price는 본인 채널 owner-only 게시글에서만 보이도록 복구했다. `CreatorCommunityMediaPlayerManager`에는 `prepareAsync()` 완료 전 `pauseContent()`/`stopContent()` 호출을 막는 prepared-state guard를 추가했다.
|
||||||
|
- GREEN: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"` PASS.
|
||||||
|
- 회귀: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Community*"` PASS.
|
||||||
|
- 리소스: `./gradlew :app:mergeDebugResources` PASS.
|
||||||
|
- 컴파일: `./gradlew :app:compileDebugKotlin` PASS.
|
||||||
|
- 린트: `./gradlew :app:ktlintCheck` PASS.
|
||||||
|
- 공백 검증: `git diff --check` PASS.
|
||||||
|
|
||||||
|
- 2026-06-22 Phase 4 보안 로그 제거 후 최종 검증:
|
||||||
|
- 보안 수정 기록: `CreatorChannelCommunityViewModel.kt`의 `Logger.e(message)`와 `Logger` import를 제거했고, `CreatorCommunityMediaPlayerManager.kt`의 `e.printStackTrace()`를 제거했다. `authToken()`/`SharedPreferenceManager.token`은 repository 호출용 bearer 생성 경로로만 남아 있으며 로그로 노출하지 않는다.
|
||||||
|
- GREEN: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"` PASS.
|
||||||
|
- 컴파일: `./gradlew :app:compileDebugKotlin` PASS.
|
||||||
|
- 린트: `./gradlew :app:ktlintCheck` PASS.
|
||||||
|
- 공백 검증: `git diff --check` PASS.
|
||||||
|
- 최종 보안 재리뷰: Oracle verdict PASS, severity none, blocking_issues 없음.
|
||||||
|
|
||||||
|
- 2026-06-22 Phase 4 코드 리뷰 및 검증:
|
||||||
|
- 코드 리뷰: Figma `665:19021`과 Phase 4 요구사항을 기준으로 리스트형 owner 유료 가격 태그 위치, 잠금 이미지 처리, play/pause 상태, owner more callback, grid sizing, media player 생명주기를 대조했다. 가격 태그가 reaction row에 배치된 결함 1건을 발견했고, 우측 상단 액션 컨테이너로 이동해 수정했다.
|
||||||
|
- RED: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragmentLayoutTest"`는 `layout_creator_channel_community_list_top_actions` 미구현으로 실패함을 확인했다.
|
||||||
|
- GREEN: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragmentLayoutTest"` PASS.
|
||||||
|
- 회귀: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Community*"` PASS.
|
||||||
|
- 리소스/컴파일/린트: `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck` PASS.
|
||||||
|
- 공백 검증: `git diff --check` PASS.
|
||||||
|
- 참고: Gradle 실행 중 기존 `WeekCalendarAdapter.kt` Kotlin annotation target 경고, 기존 테스트 deprecation 경고, 기존 `.editorconfig`의 `disabled_rules` deprecation 경고가 출력됐으나 이번 Phase 4 변경 파일의 실패는 없었다.
|
||||||
|
|||||||
Reference in New Issue
Block a user