test(creator): 커뮤니티 탭 레이아웃 검증을 추가한다

This commit is contained in:
2026-06-22 00:36:49 +09:00
parent 318944fbfe
commit 3e4c00fee8
2 changed files with 300 additions and 5 deletions

View File

@@ -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")
}
}

View File

@@ -315,7 +315,7 @@
### 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`
- 작업:
@@ -326,8 +326,11 @@
- `./gradlew :app:mergeDebugResources`
- 기대 결과:
- 신규 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/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"`
- 기대 결과:
- 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/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"`
- 기대 결과:
- 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`
- 작업:
@@ -378,8 +391,13 @@
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"`
- 기대 결과:
- 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-en/strings.xml`
@@ -392,6 +410,9 @@
- `./gradlew :app:mergeDebugResources`
- 기대 결과:
- 한국어/영어/일본어 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.*"` 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 변경 파일의 실패는 없었다.