diff --git a/docs/20260722_메인_홈_콘텐츠_대화_탭_당겨서_새로고침/plan-task.md b/docs/20260722_메인_홈_콘텐츠_대화_탭_당겨서_새로고침/plan-task.md
new file mode 100644
index 00000000..6400c1b4
--- /dev/null
+++ b/docs/20260722_메인_홈_콘텐츠_대화_탭_당겨서_새로고침/plan-task.md
@@ -0,0 +1,784 @@
+# 메인 홈, 콘텐츠, 대화 탭 당겨서 새로고침 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use `subagent-driven-development` or `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 메인 하단 `홈`, `콘텐츠`, `대화` 탭에서 현재 선택된 내부 탭/필터 기준으로 첫 페이지 API를 다시 호출하는 당겨서 새로고침을 추가한다.
+
+**Architecture:** 신규 공통 abstraction 없이 기존 `androidx.swiperefreshlayout.widget.SwipeRefreshLayout` 의존성을 사용한다. 각 화면의 고정 title/tab 영역은 그대로 두고, 실제 콘텐츠 표면만 `SwipeRefreshLayout`으로 감싸며 Fragment에서 현재 선택 상태를 기준으로 기존 ViewModel 첫 페이지 로드 메서드를 호출한다. ViewModel은 이미 있는 `requestGeneration`, `force`, `loadFirstPage` 패턴을 재사용해 오래된 응답과 pagination append 충돌을 막는다.
+
+**Tech Stack:** Android XML Views, ViewBinding, `SwipeRefreshLayout 1.1.0`, RecyclerView/NestedScrollView, Kotlin, LiveData, RxJava3, Retrofit, Koin, JUnit/Robolectric/source tests.
+
+---
+
+## 파일 구조
+
+- Modify: `app/src/main/res/layout/fragment_v2_main_home.xml`
+ - 홈 `추천`, `랭킹`, `팔로잉` 콘텐츠 표면을 각각 `SwipeRefreshLayout`으로 감싼다.
+- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`
+ - 홈 현재 선택 탭 기준 refresh listener를 연결하고, loading 종료 시 indicator를 닫는다.
+- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentSourceTest.kt`
+ - 현재 탭별 refresh 분기와 `SwipeRefreshLayout` 연결을 source test로 고정한다.
+- Modify: `app/src/main/res/layout/fragment_v2_main_content.xml`
+ - 콘텐츠 `추천`, `랭킹`, `전체` 콘텐츠 표면을 `SwipeRefreshLayout`으로 감싼다.
+- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt`
+ - 콘텐츠 현재 선택 탭/조건 기준 refresh listener를 연결한다.
+- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt`
+ - `loadRecommendations()`, `loadRankings(..., force = true)`, `loadContents()` 분기를 고정한다.
+- Modify: `app/src/main/res/layout/fragment_v2_main_chat.xml`
+ - 채팅방 목록과 empty message를 `SwipeRefreshLayout` 안의 `FrameLayout`으로 감싼다.
+- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt`
+ - 현재 `selectedFilter`로 `viewModel.loadFirstPage(selectedFilter)`를 호출한다.
+- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragmentLayoutTest.kt`
+ - 채팅 layout에 `swipe_chat_rooms`가 있고 empty 상태에서도 refresh container가 유지됨을 검증한다.
+- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainViewModelTest.kt`
+ - 같은 filter의 `loadFirstPage(filter)` 재호출이 cursor 없이 첫 페이지를 재호출하고 기존 목록을 교체하는 테스트를 추가한다.
+- Modify: `docs/20260722_메인_홈_콘텐츠_대화_탭_당겨서_새로고침/prd.md`
+ - 구현 중 확정한 Open Questions 결과와 검증 기록을 누적한다.
+- Modify: `docs/20260722_메인_홈_콘텐츠_대화_탭_당겨서_새로고침/plan-task.md`
+ - 각 Task 완료 직후 `검증 기록`을 누적한다.
+
+---
+
+### Phase 1: 홈 탭 새로고침 연결
+
+**Files:**
+- Modify: `app/src/main/res/layout/fragment_v2_main_home.xml`
+- 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/HomeMainFragmentSourceTest.kt`
+
+- [ ] **Task 1.1: 홈 layout source test를 먼저 추가한다**
+
+ `HomeMainFragmentSourceTest.kt`에 아래 검증을 추가한다.
+
+ ```kotlin
+ @Test
+ fun `home layout has refresh containers for each tab surface`() {
+ val source = File("app/src/main/res/layout/fragment_v2_main_home.xml").readText()
+
+ assertTrue(source.contains("androidx.swiperefreshlayout.widget.SwipeRefreshLayout"))
+ assertTrue(source.contains("@+id/swipe_home_recommendation"))
+ assertTrue(source.contains("@+id/swipe_home_ranking"))
+ assertTrue(source.contains("@+id/swipe_home_following"))
+ assertTrue(source.contains("@+id/nsv_home_recommendation_content"))
+ assertTrue(source.contains("@+id/rv_home_creator_rankings"))
+ assertTrue(source.contains("@+id/nsv_home_following_content"))
+ }
+ ```
+
+ Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentSourceTest"`
+
+ Expected: FAIL because `swipe_home_recommendation`, `swipe_home_ranking`, `swipe_home_following` do not exist yet.
+
+- [ ] **Task 1.2: 홈 layout에 탭별 SwipeRefreshLayout을 추가한다**
+
+ `fragment_v2_main_home.xml`에서 `text_tab_bar_home` 아래 콘텐츠 3개를 각각 아래 구조로 감싼다. 기존 child ID는 유지한다.
+
+ ```xml
+
+
+
+
+ ```
+
+ 랭킹은 `rv_home_creator_rankings`를 직접 감싼다.
+
+ ```xml
+
+
+
+
+ ```
+
+ 팔로잉은 `nsv_home_following_content`를 감싼다.
+
+ ```xml
+
+
+
+
+ ```
+
+ 기존 `nsv_home_*`와 `rv_home_creator_rankings`의 `ConstraintLayout` constraint 속성은 wrapper로 옮기고 child에서는 제거한다.
+
+ Run: `./gradlew :app:mergeDebugResources`
+
+ Expected: BUILD SUCCESSFUL.
+
+- [ ] **Task 1.3: 홈 Fragment source test를 추가한다**
+
+ `HomeMainFragmentSourceTest.kt`에 아래 검증을 추가한다.
+
+ ```kotlin
+ @Test
+ fun `home refresh dispatches by current selected tab`() {
+ val source = File(
+ "app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
+ ).readText()
+
+ assertTrue(source.contains("setUpRefreshLayouts()"))
+ assertTrue(source.contains("private fun refreshCurrentHomeTab()"))
+ assertTrue(source.contains("HOME_TAB_RECOMMENDATION -> homeRecommendationViewModel.loadRecommendations()"))
+ assertTrue(source.contains("HOME_TAB_RANKING -> homeCreatorRankingViewModel.loadCreatorRankings()"))
+ assertTrue(source.contains("HOME_TAB_FOLLOWING -> homeFollowingViewModel.loadFollowing()"))
+ assertTrue(source.contains("binding.swipeHomeRecommendation.isRefreshing = false"))
+ assertTrue(source.contains("binding.swipeHomeRanking.isRefreshing = false"))
+ assertTrue(source.contains("binding.swipeHomeFollowing.isRefreshing = false"))
+ }
+ ```
+
+ Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentSourceTest"`
+
+ Expected: FAIL because refresh dispatch code does not exist yet.
+
+- [ ] **Task 1.4: 홈 Fragment에 refresh listener와 visibility 전환을 연결한다**
+
+ `HomeMainFragment.kt`의 `onViewCreated()`에서 `setUpFollowingAdapters()` 다음에 `setUpRefreshLayouts()`를 호출한다.
+
+ ```kotlin
+ setUpFollowingAdapters()
+ setUpRefreshLayouts()
+ setUpBusinessInfo()
+ ```
+
+ 아래 메서드를 추가한다.
+
+ ```kotlin
+ private fun setUpRefreshLayouts() {
+ binding.swipeHomeRecommendation.setOnRefreshListener { refreshCurrentHomeTab() }
+ binding.swipeHomeRanking.setOnRefreshListener { refreshCurrentHomeTab() }
+ binding.swipeHomeFollowing.setOnRefreshListener { refreshCurrentHomeTab() }
+ }
+
+ private fun refreshCurrentHomeTab() {
+ when (currentHomeTabIndex) {
+ HOME_TAB_RECOMMENDATION -> homeRecommendationViewModel.loadRecommendations()
+ HOME_TAB_RANKING -> homeCreatorRankingViewModel.loadCreatorRankings()
+ HOME_TAB_FOLLOWING -> homeFollowingViewModel.loadFollowing()
+ }
+ }
+
+ private fun stopHomeRefreshIndicators() {
+ binding.swipeHomeRecommendation.isRefreshing = false
+ binding.swipeHomeRanking.isRefreshing = false
+ binding.swipeHomeFollowing.isRefreshing = false
+ }
+ ```
+
+ `showHomeTab()`의 visibility 대상은 콘텐츠 child가 아니라 wrapper로 바꾼다.
+
+ ```kotlin
+ HOME_TAB_RECOMMENDATION -> {
+ binding.swipeHomeRecommendation.visibility = View.VISIBLE
+ binding.swipeHomeRanking.visibility = View.GONE
+ binding.swipeHomeFollowing.visibility = View.GONE
+ }
+
+ HOME_TAB_RANKING -> {
+ binding.swipeHomeRecommendation.visibility = View.GONE
+ binding.swipeHomeRanking.visibility = View.VISIBLE
+ binding.swipeHomeFollowing.visibility = View.GONE
+ if (!hasLoadedCreatorRankings) {
+ hasLoadedCreatorRankings = true
+ homeCreatorRankingViewModel.loadCreatorRankings()
+ }
+ }
+
+ HOME_TAB_FOLLOWING -> {
+ binding.swipeHomeFollowing.visibility = View.VISIBLE
+ binding.swipeHomeRecommendation.visibility = View.GONE
+ binding.swipeHomeRanking.visibility = View.GONE
+ if (!hasLoadedFollowing) {
+ hasLoadedFollowing = true
+ homeFollowingViewModel.loadFollowing()
+ }
+ }
+ ```
+
+ `updateLoadingDialog()` 끝에서 indicator를 닫는다.
+
+ ```kotlin
+ private fun updateLoadingDialog() {
+ if (isRecommendationLoading || isCreatorRankingLoading || isFollowingLoading || isLiveEntryLoading) {
+ loadingDialog.show(screenWidth)
+ } else {
+ loadingDialog.dismiss()
+ stopHomeRefreshIndicators()
+ }
+ }
+ ```
+
+ Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentSourceTest"`
+
+ Expected: PASS.
+
+- [ ] **Task 1.5: 홈 Phase 검증 기록을 남긴다**
+
+ Run:
+ - `./gradlew :app:mergeDebugResources`
+ - `./gradlew :app:compileDebugKotlin`
+ - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentSourceTest"`
+
+ Expected: all BUILD SUCCESSFUL.
+
+ 검증 성공 후 이 Phase 아래에 `검증 기록`을 한국어로 누적한다.
+
+### Phase 2: 콘텐츠 탭 새로고침 연결
+
+**Files:**
+- Modify: `app/src/main/res/layout/fragment_v2_main_content.xml`
+- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt`
+- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt`
+- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentRankingViewModelTest.kt`
+
+- [ ] **Task 2.1: 콘텐츠 layout source test를 먼저 추가한다**
+
+ `ContentMainFragmentSourceTest.kt`에 아래 검증을 추가한다.
+
+ ```kotlin
+ @Test
+ fun `content layout has refresh containers for recommendation ranking and all tabs`() {
+ val source = File("app/src/main/res/layout/fragment_v2_main_content.xml").readText()
+
+ assertTrue(source.contains("androidx.swiperefreshlayout.widget.SwipeRefreshLayout"))
+ assertTrue(source.contains("@+id/swipe_content_recommendation"))
+ assertTrue(source.contains("@+id/swipe_content_ranking"))
+ assertTrue(source.contains("@+id/swipe_content_all"))
+ assertTrue(source.contains("@+id/nsv_content_recommendation_content"))
+ assertTrue(source.contains("@+id/rv_content_rankings"))
+ assertTrue(source.contains("@+id/rv_content_all_items"))
+ assertTrue(source.contains("@+id/layout_content_all_empty_error"))
+ }
+ ```
+
+ Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"`
+
+ Expected: FAIL because content refresh wrappers do not exist yet.
+
+- [ ] **Task 2.2: 콘텐츠 layout에 SwipeRefreshLayout을 추가한다**
+
+ 추천 `NestedScrollView`를 `swipe_content_recommendation`으로 감싼다.
+
+ ```xml
+
+
+
+
+ ```
+
+ 랭킹은 `view_content_ranking_type_tabs` 아래 `rv_content_rankings`만 감싼다. ranking type tab은 wrapper 밖에 둔다.
+
+ ```xml
+
+
+
+
+ ```
+
+ `layout_content_all_surface` 내부에서는 sort bar 아래 영역을 `swipe_content_all`로 감싸고, `FrameLayout` 안에 기존 `rv_content_all_items`와 `layout_content_all_empty_error`를 유지한다.
+
+ ```xml
+
+
+
+
+
+
+
+
+
+ ```
+
+ Run: `./gradlew :app:mergeDebugResources`
+
+ Expected: BUILD SUCCESSFUL.
+
+- [ ] **Task 2.3: 콘텐츠 refresh 분기 source test를 추가한다**
+
+ `ContentMainFragmentSourceTest.kt`에 아래 검증을 추가한다.
+
+ ```kotlin
+ @Test
+ fun `content refresh dispatches by selected content tab`() {
+ val source = File(
+ "app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
+ ).readText()
+
+ assertTrue(source.contains("setUpRefreshLayouts()"))
+ assertTrue(source.contains("private fun refreshCurrentContentTab()"))
+ assertTrue(source.contains("CONTENT_TAB_RECOMMENDATION -> contentMainViewModel.loadRecommendations()"))
+ assertTrue(source.contains("CONTENT_TAB_RANKING -> refreshCurrentRankingTab()"))
+ assertTrue(source.contains("CONTENT_TAB_ALL -> contentAllTabViewModel.loadContents()"))
+ assertTrue(source.contains("contentRankingViewModel.loadRankings(type, force = true)"))
+ assertTrue(source.contains("binding.swipeContentRecommendation.isRefreshing = false"))
+ assertTrue(source.contains("binding.swipeContentRanking.isRefreshing = false"))
+ assertTrue(source.contains("binding.swipeContentAll.isRefreshing = false"))
+ }
+ ```
+
+ Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"`
+
+ Expected: FAIL because refresh dispatch code does not exist yet.
+
+- [ ] **Task 2.4: 콘텐츠 Fragment에 refresh listener를 연결한다**
+
+ `ContentMainFragment.kt`에서 `setUpAdapters()` 다음에 `setUpRefreshLayouts()`를 호출한다.
+
+ ```kotlin
+ setUpAdapters()
+ setUpRefreshLayouts()
+ bindObservers()
+ ```
+
+ 아래 메서드를 추가한다.
+
+ ```kotlin
+ private fun setUpRefreshLayouts() {
+ binding.swipeContentRecommendation.setOnRefreshListener { refreshCurrentContentTab() }
+ binding.swipeContentRanking.setOnRefreshListener { refreshCurrentContentTab() }
+ binding.swipeContentAll.setOnRefreshListener { refreshCurrentContentTab() }
+ }
+
+ private fun refreshCurrentContentTab() {
+ when (currentContentTab) {
+ CONTENT_TAB_RECOMMENDATION -> contentMainViewModel.loadRecommendations()
+ CONTENT_TAB_RANKING -> refreshCurrentRankingTab()
+ CONTENT_TAB_ALL -> contentAllTabViewModel.loadContents()
+ }
+ }
+
+ private fun refreshCurrentRankingTab() {
+ val type = contentRankingViewModel.selectedTypeLiveData.value ?: AudioRankingType.WEEKLY_POPULAR
+ contentRankingViewModel.loadRankings(type, force = true)
+ }
+
+ private fun stopContentRefreshIndicators() {
+ binding.swipeContentRecommendation.isRefreshing = false
+ binding.swipeContentRanking.isRefreshing = false
+ binding.swipeContentAll.isRefreshing = false
+ }
+ ```
+
+ `showRecommendationContent()`, `showRankingContent()`, `showAllContent()`, `hideContentSurfaces()`의 visibility 대상은 wrapper로 바꾼다.
+
+ ```kotlin
+ private fun showRecommendationContent() {
+ binding.swipeContentRecommendation.visibility = View.VISIBLE
+ binding.viewContentRankingTypeTabs.root.visibility = View.GONE
+ binding.swipeContentRanking.visibility = View.GONE
+ binding.layoutContentAllSurface.visibility = View.GONE
+ }
+
+ private fun showRankingContent() {
+ binding.swipeContentRecommendation.visibility = View.GONE
+ binding.viewContentRankingTypeTabs.root.visibility = View.VISIBLE
+ binding.swipeContentRanking.visibility = View.VISIBLE
+ binding.layoutContentAllSurface.visibility = View.GONE
+ if (!hasSelectedRankingTab) {
+ hasSelectedRankingTab = true
+ contentRankingViewModel.loadRankings(AudioRankingType.WEEKLY_POPULAR)
+ }
+ }
+
+ private fun showAllContent() {
+ binding.swipeContentRecommendation.visibility = View.GONE
+ binding.viewContentRankingTypeTabs.root.visibility = View.GONE
+ binding.swipeContentRanking.visibility = View.GONE
+ binding.layoutContentAllSurface.visibility = View.VISIBLE
+ currentAllTabState?.let(::renderAllTabState)
+ if (currentAllTabState == null && !hasSelectedAllTab) {
+ hasSelectedAllTab = true
+ contentAllTabViewModel.loadInitial()
+ }
+ }
+ ```
+
+ `updateLoadingDialog()`에서 loading이 모두 끝나면 indicator를 닫는다.
+
+ ```kotlin
+ private fun updateLoadingDialog() {
+ if (isRecommendationLoading || isRankingLoading || isAllTabLoading) {
+ loadingDialog.show(screenWidth)
+ } else {
+ loadingDialog.dismiss()
+ stopContentRefreshIndicators()
+ }
+ }
+ ```
+
+ Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"`
+
+ Expected: PASS.
+
+- [ ] **Task 2.5: 콘텐츠 랭킹 force refresh 테스트를 실행한다**
+
+ `ContentRankingViewModelTest.kt`에는 이미 아래 테스트가 있으므로 새 테스트를 추가하지 않는다.
+
+ ```kotlin
+ @Test
+ fun `force true이면 같은 type도 다시 호출한다`() {
+ api.enqueueSuccess(response(type = AudioRankingType.WEEKLY_POPULAR, items = listOf(item(contentId = 1L))))
+ api.enqueueSuccess(response(type = AudioRankingType.WEEKLY_POPULAR, items = listOf(item(contentId = 2L))))
+ viewModel.loadRankings(AudioRankingType.WEEKLY_POPULAR)
+
+ viewModel.loadRankings(AudioRankingType.WEEKLY_POPULAR, force = true)
+
+ assertEquals(2, api.calls.size)
+ val state = viewModel.rankingStateLiveData.requireValue() as AudioRankingsUiState.Content
+ assertEquals(listOf("2"), state.items.map { it.contentId })
+ }
+ ```
+
+ Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentRankingViewModelTest"`
+
+ Expected: PASS.
+
+- [ ] **Task 2.6: 콘텐츠 Phase 검증 기록을 남긴다**
+
+ Run:
+ - `./gradlew :app:mergeDebugResources`
+ - `./gradlew :app:compileDebugKotlin`
+ - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"`
+ - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentRankingViewModelTest"`
+ - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentAllTabViewModelTest"`
+
+ Expected: all BUILD SUCCESSFUL.
+
+ 검증 성공 후 이 Phase 아래에 `검증 기록`을 한국어로 누적한다.
+
+### Phase 3: 대화 탭 새로고침 연결
+
+**Files:**
+- Modify: `app/src/main/res/layout/fragment_v2_main_chat.xml`
+- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt`
+- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragmentLayoutTest.kt`
+- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainViewModelTest.kt`
+
+- [ ] **Task 3.1: 대화 layout test를 먼저 추가한다**
+
+ `ChatMainFragmentLayoutTest.kt`에 아래 검증을 추가하고, 기존 `채팅 fragment layout은 title bar tab list만 포함한다`, `채팅 fragment list는 계획된 constraint를 사용한다` 테스트는 `rv_chat_rooms`와 `tv_chat_empty_message`의 부모가 root가 아니라 `swipe_chat_rooms` 내부 `FrameLayout`임을 기준으로 갱신한다.
+
+ ```kotlin
+ @Test
+ fun `chat layout wraps room list and empty message in swipe refresh`() {
+ val source = File("app/src/main/res/layout/fragment_v2_main_chat.xml").readText()
+
+ assertTrue(source.contains("androidx.swiperefreshlayout.widget.SwipeRefreshLayout"))
+ assertTrue(source.contains("@+id/swipe_chat_rooms"))
+ assertTrue(source.contains("@+id/rv_chat_rooms"))
+ assertTrue(source.contains("@+id/tv_chat_empty_message"))
+ assertTrue(source.indexOf("@+id/swipe_chat_rooms") < source.indexOf("@+id/rv_chat_rooms"))
+ assertTrue(source.indexOf("@+id/swipe_chat_rooms") < source.indexOf("@+id/tv_chat_empty_message"))
+ }
+ ```
+
+ Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest"`
+
+ Expected: FAIL because `swipe_chat_rooms` does not exist yet.
+
+- [ ] **Task 3.2: 대화 layout에 SwipeRefreshLayout을 추가한다**
+
+ `fragment_v2_main_chat.xml`에서 `rv_chat_rooms`와 `tv_chat_empty_message`를 하나의 `SwipeRefreshLayout` 안 `FrameLayout`으로 감싼다.
+
+ ```xml
+
+
+
+
+
+
+
+
+
+ ```
+
+ Run: `./gradlew :app:mergeDebugResources`
+
+ Expected: BUILD SUCCESSFUL.
+
+- [ ] **Task 3.3: 대화 Fragment source test를 추가한다**
+
+ `ChatMainFragmentLayoutTest.kt`에 아래 검증을 추가한다.
+
+ ```kotlin
+ @Test
+ fun `chat refresh reloads first page with selected filter`() {
+ val source = File(
+ "app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt"
+ ).readText()
+
+ assertTrue(source.contains("binding.swipeChatRooms.setOnRefreshListener"))
+ assertTrue(source.contains("viewModel.loadFirstPage(selectedFilter)"))
+ assertTrue(source.contains("binding.swipeChatRooms.isRefreshing = false"))
+ }
+ ```
+
+ Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest"`
+
+ Expected: FAIL because refresh listener code does not exist yet.
+
+- [ ] **Task 3.4: 대화 Fragment에 refresh listener를 연결한다**
+
+ `ChatMainFragment.kt`의 `onViewCreated()`에서 `setupChatRooms()` 다음에 `setupRefresh()`를 호출한다.
+
+ ```kotlin
+ setupChatRooms()
+ setupRefresh()
+ bindViewModel()
+ ```
+
+ 아래 메서드를 추가한다.
+
+ ```kotlin
+ private fun setupRefresh() {
+ binding.swipeChatRooms.setOnRefreshListener {
+ viewModel.loadFirstPage(selectedFilter)
+ }
+ }
+ ```
+
+ `isLoading` observer에서 loading 종료 시 indicator를 닫는다.
+
+ ```kotlin
+ viewModel.isLoading.observe(viewLifecycleOwner) {
+ if (it) {
+ loadingDialog.show(screenWidth)
+ } else {
+ loadingDialog.dismiss()
+ binding.swipeChatRooms.isRefreshing = false
+ }
+ }
+ ```
+
+ Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest"`
+
+ Expected: PASS.
+
+- [ ] **Task 3.5: 대화 ViewModel 첫 페이지 교체 테스트를 추가한다**
+
+ `ChatMainViewModelTest.kt`에 같은 filter 첫 페이지 재호출이 기존 목록을 교체하고 cursor 없이 요청하는 테스트를 추가한다.
+
+ ```kotlin
+ @Test
+ fun `loadFirstPage with current filter requests first page and replaces existing rooms`() {
+ api.enqueueSuccess(page(rooms = listOf(room(roomId = 1L)), hasMore = true, nextCursor = "next"))
+ viewModel.loadFirstPage(ChatRoomFilter.AI)
+ api.enqueueSuccess(page(rooms = listOf(room(roomId = 2L)), hasMore = false, nextCursor = null))
+
+ viewModel.loadFirstPage(ChatRoomFilter.AI)
+
+ assertEquals(
+ listOf(
+ ApiCall("Bearer test-token", "AI", null),
+ ApiCall("Bearer test-token", "AI", null)
+ ),
+ api.calls
+ )
+ val state = viewModel.chatRoomStateLiveData.value as ChatRoomListUiState.Content
+ assertEquals(listOf(2L), state.items.map { it.roomId })
+ }
+ ```
+
+ Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"`
+
+ Expected: PASS.
+
+- [ ] **Task 3.6: 대화 Phase 검증 기록을 남긴다**
+
+ Run:
+ - `./gradlew :app:mergeDebugResources`
+ - `./gradlew :app:compileDebugKotlin`
+ - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest"`
+ - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"`
+
+ Expected: all BUILD SUCCESSFUL.
+
+ 검증 성공 후 이 Phase 아래에 `검증 기록`을 한국어로 누적한다.
+
+### Phase 4: 통합 검증과 문서 갱신
+
+**Files:**
+- Modify: `docs/20260722_메인_홈_콘텐츠_대화_탭_당겨서_새로고침/prd.md`
+- Modify: `docs/20260722_메인_홈_콘텐츠_대화_탭_당겨서_새로고침/plan-task.md`
+
+- [ ] **Task 4.1: PRD Open Questions 결과를 문서에 반영한다**
+
+ `prd.md`의 `Open Questions` 아래에 구현 계획 확정 내용을 추가한다.
+
+ ```markdown
+ ### 2026-07-22 구현 계획 확정
+ - 홈 `랭킹` 탭은 `HomeCreatorRankingViewModel.loadCreatorRankings()`와 `HomeCreatorRankingRepository.getCreatorRankings()` 흐름을 새로고침 대상으로 사용한다.
+ - 홈 `팔로잉` 탭은 기존 `ensureV2Access(AccessRequirement.Login)` 진입 가드를 유지한다. 로그인되지 않은 상태에서는 팔로잉 탭으로 전환되지 않으므로 새로고침도 노출하지 않는다.
+ - 콘텐츠 `랭킹` 탭은 `ContentRankingViewModel.selectedTypeLiveData`의 현재 `AudioRankingType`을 `force = true`로 다시 호출한다.
+ - refresh indicator 색상은 별도 디자인 요구가 없으므로 `SwipeRefreshLayout` 기본 theme 색상을 사용한다.
+ ```
+
+ Run: `rg "구현 계획 확정|HomeCreatorRankingViewModel|force = true" docs/20260722_메인_홈_콘텐츠_대화_탭_당겨서_새로고침/prd.md`
+
+ Expected: added lines are found.
+
+- [ ] **Task 4.2: 전체 변경 검증을 실행한다**
+
+ Run:
+ - `./gradlew :app:mergeDebugResources`
+ - `./gradlew :app:compileDebugKotlin`
+ - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*"`
+ - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"`
+ - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"`
+ - `./gradlew :app:ktlintCheck`
+
+ Expected: all BUILD SUCCESSFUL. 기존 경고가 있으면 경고 문구와 변경 관련 여부를 `Verification Log`에 기록한다.
+
+- [ ] **Task 4.3: 수동 QA를 수행한다**
+
+ 연결 기기가 있으면 실행한다.
+
+ Run:
+ - `adb devices`
+ - `./gradlew :app:installDebug`
+
+ 수동 확인:
+ - 홈 `추천`에서 당겨서 새로고침 후 추천 섹션이 첫 페이지/단일 응답 기준으로 교체된다.
+ - 홈 `랭킹`에서 당겨서 새로고침 후 랭킹 목록이 다시 로드되고 `추천`으로 이동하지 않는다.
+ - 로그인 상태에서 홈 `팔로잉` 당겨서 새로고침 후 팔로잉 섹션이 다시 로드된다.
+ - 콘텐츠 `추천`에서 당겨서 새로고침 후 추천 섹션이 다시 로드된다.
+ - 콘텐츠 `랭킹`에서 ranking type을 바꾼 뒤 당겨서 새로고침하면 같은 ranking type이 다시 로드된다.
+ - 콘텐츠 `전체`에서 type/sort/dayOfWeek를 바꾼 뒤 당겨서 새로고침하면 같은 조건으로 `page=0`이 호출된다.
+ - 대화 `전체`, `AI 채팅`, `DM` 각각에서 당겨서 새로고침하면 같은 filter로 첫 페이지가 다시 로드된다.
+ - empty 상태에서도 indicator가 멈추고 재시도가 가능하다.
+
+ 기기가 없으면 `adb devices` 결과를 `Verification Log`에 남기고 수동 QA는 환경 차단으로 표시한다.
+
+- [ ] **Task 4.4: 최종 문서 검증 기록을 누적한다**
+
+ `plan-task.md` 최하단 `Verification Log`와 `prd.md`의 `Verification Log`에 아래 형식으로 실제 실행 결과를 누적한다.
+
+ ```markdown
+ - 2026-07-22: 당겨서 새로고침 구현 후 `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, 홈/콘텐츠/대화 관련 targeted unit test, `./gradlew :app:ktlintCheck`를 실행했고 결과는 BUILD SUCCESSFUL이다.
+ - 2026-07-22: 연결 기기 유무에 따라 수동 QA 수행 결과 또는 환경 차단 사유를 기록했다.
+ ```
+
+## 구현 순서 요약
+
+1. 홈 layout/source test를 RED로 만든 뒤 홈 refresh를 연결한다.
+2. 콘텐츠 layout/source test를 RED로 만든 뒤 콘텐츠 refresh를 연결한다.
+3. 대화 layout/source test를 RED로 만든 뒤 대화 refresh를 연결한다.
+4. 각 ViewModel의 기존 첫 페이지 재조회/force/cache 동작을 테스트로 고정한다.
+5. PRD Open Questions 확정 내용을 문서화하고 통합 검증을 수행한다.
+
+## Verification Log
+
+- 2026-07-22: `docs/20260722_메인_홈_콘텐츠_대화_탭_당겨서_새로고침/prd.md`를 기준으로 계획을 작성했다.
+- 2026-07-22: `app/build.gradle`에서 `androidx.swiperefreshlayout:swiperefreshlayout:1.1.0`이 이미 추가되어 있음을 확인해 신규 의존성 추가 없이 구현하도록 계획했다.
+- 2026-07-22: `HomeMainFragment`, `ContentMainFragment`, `ChatMainFragment`와 각 ViewModel을 확인해 현재 선택 탭/필터 기준 첫 페이지 재호출 경로를 확정했다.
+- 2026-07-22: 이번 단계는 plan-task 문서 작성만 수행했으며 구현/빌드/테스트는 실행하지 않는다.
diff --git a/docs/20260722_메인_홈_콘텐츠_대화_탭_당겨서_새로고침/prd.md b/docs/20260722_메인_홈_콘텐츠_대화_탭_당겨서_새로고침/prd.md
new file mode 100644
index 00000000..d95294ec
--- /dev/null
+++ b/docs/20260722_메인_홈_콘텐츠_대화_탭_당겨서_새로고침/prd.md
@@ -0,0 +1,190 @@
+# PRD: 메인 홈, 콘텐츠, 대화 탭 당겨서 새로고침
+
+## 1. Overview
+메인 하단 `홈`, `콘텐츠`, `대화` 탭의 리스트 화면에 당겨서 새로고침 동작을 추가하고, 각 탭 내부에서 현재 선택된 탭/필터 상태에 맞는 첫 페이지 API를 다시 호출한다.
+
+---
+
+## 2. Problem
+- 메인 하단 탭의 주요 리스트는 API 응답으로 구성되지만, 사용자가 같은 화면에 머무른 상태에서 최신 데이터를 수동으로 다시 불러올 수 있는 동작이 없다.
+- 홈, 콘텐츠, 대화 탭은 각각 내부 탭 또는 필터에 따라 호출해야 하는 API와 query 값이 달라진다.
+- 새로고침이 단순 초기 진입 API를 호출하는 수준으로 구현되면 현재 선택 탭이 아닌 기본 탭 데이터가 다시 로드될 위험이 있다.
+- pagination 중인 리스트에서는 새로고침과 추가 페이지 요청이 충돌하지 않도록 첫 페이지 재조회 기준을 명확히 해야 한다.
+
+---
+
+## 3. Goals
+- 메인 하단 `홈`, `콘텐츠`, `대화` 탭의 리스트 영역에서 Android 표준 당겨서 새로고침 UX를 제공한다.
+- 새로고침 시 현재 선택된 내부 탭/필터 상태를 유지한 채 해당 상태의 첫 페이지 API를 다시 호출한다.
+- 첫 페이지 새로고침 성공 시 기존 목록을 새 응답 데이터로 교체한다.
+- 첫 페이지 새로고침 실패 시 앱이 crash 나지 않고 기존 error/toast 정책을 따른다.
+- 기존 tab 전환, pagination, click routing, 로그인 가드 동작은 변경하지 않는다.
+- 신규 외부 라이브러리 없이 기존 AndroidX/프로젝트 의존성 안에서 구현 가능한 구조로 요구사항을 정의한다.
+
+---
+
+## 4. Non-Goals
+- 이번 PRD 작성 단계에서는 코드, 리소스, 레이아웃 파일을 구현하지 않는다.
+- 새 API endpoint를 추가하거나 서버 스키마를 변경하지 않는다.
+- 자동 주기 새로고침, 실시간 갱신, WebSocket/SSE 연동은 포함하지 않는다.
+- pull-to-refresh 전용 skeleton/shimmer UI를 새로 만들지 않는다.
+- 당겨서 새로고침으로 다음 페이지를 append하지 않는다. 새로고침은 항상 현재 조건의 첫 페이지 재조회로 처리한다.
+- 하단 메인 내비게이션 구조와 내부 탭 구성을 변경하지 않는다.
+- Compose 전환 또는 화면 전체 리팩터링은 포함하지 않는다.
+
+---
+
+## 5. Target Users
+- 메인 홈에서 추천/랭킹/팔로잉 데이터를 최신 상태로 다시 보고 싶은 앱 사용자.
+- 메인 콘텐츠 탭에서 추천/랭킹/전체 콘텐츠 목록을 최신 상태로 다시 불러오고 싶은 앱 사용자.
+- 대화 탭에서 전체/AI 채팅/DM 대화 목록을 최신 순서로 다시 확인하고 싶은 앱 사용자.
+- `kr.co.vividnext.sodalive.v2` 하위 메인 탭 화면을 유지보수하는 Android 개발자.
+
+---
+
+## 6. User Stories
+- 사용자는 홈 추천 탭을 보고 있다가 아래로 당겨 최신 추천 데이터를 다시 받고 싶다.
+- 사용자는 홈 랭킹 또는 팔로잉 탭을 선택한 상태에서 새로고침해 해당 탭의 최신 목록을 유지하고 싶다.
+- 사용자는 콘텐츠 추천, 랭킹, 전체 탭 중 현재 보고 있는 탭만 새로고침하고 싶다.
+- 사용자는 대화 탭의 `전체`, `AI 채팅`, `DM` 필터 중 현재 선택한 필터의 대화 목록만 다시 불러오고 싶다.
+- 사용자는 새로고침 후 스크롤 위치가 새 목록의 시작 지점으로 돌아가 현재 데이터가 갱신됐음을 인지하고 싶다.
+
+---
+
+## 7. Core Features
+
+### Pull-to-Refresh 공통 동작
+각 대상 화면은 리스트 또는 세로 스크롤 콘텐츠의 최상단에서 아래로 당기는 제스처를 새로고침 트리거로 처리한다.
+
+#### Requirements
+- Android XML Views 기반 구조를 유지하고, 신규 외부 라이브러리를 추가하지 않는다.
+- 구현 시 기존 의존성에 `SwipeRefreshLayout` 또는 동등한 AndroidX 컴포넌트가 사용 가능한지 먼저 확인한다.
+- 새로고침 indicator는 API 요청 시작 시 표시하고, success/failure와 관계없이 요청 종료 시 숨긴다.
+- 새로고침 중 같은 화면에서 중복 새로고침 요청이 발생하지 않도록 한다.
+- 새로고침은 현재 선택 조건의 첫 페이지를 재조회한다.
+- 첫 페이지 응답 성공 시 기존 목록을 clear 후 새 응답 목록으로 교체한다.
+- 새로고침 성공 후 리스트/스크롤 위치는 최상단으로 이동한다.
+- 새로고침 실패 시 기존 목록을 가능한 한 유지하고, 기존 화면별 error/toast 정책을 따른다.
+- pagination의 `loadMore`가 진행 중이면 새로고침과 동시에 추가 페이지가 append되지 않도록 구현 계획에서 처리 순서를 확정한다.
+
+#### Edge Cases
+- 빈 응답이면 기존 화면별 empty 상태를 표시한다.
+- 네트워크 오류가 발생해도 새로고침 indicator가 멈춰야 한다.
+- 탭 전환 직후 자동 로딩 중이면 새로고침 제스처로 중복 첫 페이지 요청을 만들지 않는다.
+- 사용자가 새로고침 요청 중 내부 탭을 바꾸면 최신 선택 탭의 응답만 화면에 반영되어야 한다.
+
+### 메인 홈 탭 새로고침
+하단 `홈` 탭은 내부 선택 탭에 따라 서로 다른 데이터 흐름을 새로고침한다.
+
+#### Selected Tab Mapping
+| 현재 선택 탭 | 새로고침 대상 | 기대 동작 |
+| --- | --- | --- |
+| `추천` | 홈 추천 API | 현재 홈 추천 섹션 첫 페이지/단일 응답을 다시 호출하고 섹션 데이터를 교체한다. |
+| `랭킹` | 홈 랭킹 API | 현재 랭킹 조건의 첫 페이지를 다시 호출하고 목록을 교체한다. |
+| `팔로잉` | 홈 팔로잉 API | 현재 팔로잉 목록 첫 페이지를 다시 호출하고 목록을 교체한다. |
+
+#### Requirements
+- 내부 탭 선택 상태를 새로고침 전후로 유지한다.
+- 새로고침 때문에 `추천` 탭으로 강제 이동하지 않는다.
+- 홈 추천 탭의 섹션별 숨김/empty 정책은 기존 홈 추천 요구사항을 따른다.
+- 홈 랭킹/팔로잉 탭의 pagination 조건은 기존 ViewModel 상태를 기준으로 첫 페이지부터 다시 계산한다.
+- 로그인 가드가 필요한 탭은 기존 접근 가드 정책을 변경하지 않는다.
+
+### 메인 콘텐츠 탭 새로고침
+하단 `콘텐츠` 탭은 내부 선택 탭에 따라 추천, 랭킹, 전체 목록의 API를 새로고침한다.
+
+#### Selected Tab Mapping
+| 현재 선택 탭 | 새로고침 대상 | 기대 동작 |
+| --- | --- | --- |
+| `추천` | `GET /api/v2/audio/recommendations` | 콘텐츠 추천 섹션 데이터를 다시 호출하고 기존 섹션 데이터를 교체한다. |
+| `랭킹` | 콘텐츠 랭킹 API | 현재 선택된 랭킹 유형의 데이터를 다시 호출한다. |
+| `전체` | `GET /api/v2/audio/contents` | 현재 선택된 type/sort/dayOfWeek 조건으로 첫 페이지를 다시 호출한다. |
+
+#### Requirements
+- `전체` 탭에서는 `selectedType`, `selectedSort`, `selectedDayOfWeek`를 유지한 채 `page=0`으로 재조회한다.
+- `전체` 탭에서 `SERIES` 선택 시 기존 `dayOfWeek` query 정책을 유지한다.
+- `랭킹` 탭에서는 현재 선택된 랭킹 type을 유지한다.
+- 새로고침은 기존 `loadMore` 결과를 보존해 append하지 않고 첫 페이지 결과로 대체한다.
+- 새로고침 실패 시 기존 목록과 선택 조건을 유지한다.
+
+### 대화 탭 새로고침
+하단 `대화` 탭은 현재 선택된 필터 탭 값으로 채팅방 목록 첫 페이지를 새로고침한다.
+
+#### Selected Filter Mapping
+| 현재 선택 필터 | API filter | 기대 동작 |
+| --- | --- | --- |
+| `전체` | `ALL` | 전체 채팅방 첫 페이지를 다시 호출한다. |
+| `AI 채팅` | `AI` | AI 채팅방 첫 페이지를 다시 호출한다. |
+| `DM` | `DM` | DM 채팅방 첫 페이지를 다시 호출한다. |
+
+#### Requirements
+- API endpoint는 기존 `GET /api/v2/chat/rooms`를 사용한다.
+- 새로고침 시 현재 선택된 `filter` 값을 유지한다.
+- 첫 페이지 요청이므로 `cursor`는 전달하지 않거나 기존 첫 페이지 요청 정책을 따른다.
+- 성공 시 기존 채팅방 목록을 clear하고 응답 `rooms`로 교체한다.
+- `hasMore`, `nextCursor`는 새 응답 기준으로 갱신한다.
+- 빈 응답이면 필터별 기존 empty placeholder 문구를 표시한다.
+- `chatType=AI`, `chatType=DM` item 클릭 정책은 변경하지 않는다.
+
+---
+
+## 8. UX / UI Expectations
+- 당겨서 새로고침은 각 탭의 실제 스크롤 가능한 콘텐츠 영역에서 동작해야 한다.
+- 고정 title-bar, 내부 tab-bar, 하단 navigation bar는 당겨서 새로고침 대상 스크롤 콘텐츠에 포함하지 않는다.
+- refresh indicator는 앱의 기존 색상/테마와 충돌하지 않아야 한다.
+- 새로고침 중에도 내부 탭 선택 상태가 시각적으로 유지되어야 한다.
+- 새로고침 성공 후 사용자는 갱신된 목록의 시작 지점을 볼 수 있어야 한다.
+- 새로고침 실패 시 기존 데이터가 갑자기 사라져 빈 화면으로 보이지 않도록 한다.
+- 리스트가 비어 있는 empty 상태에서도 당겨서 새로고침을 다시 시도할 수 있어야 한다.
+
+---
+
+## 9. Technical Constraints
+- Android Gradle 단일 `:app` 모듈 내에서 작업한다.
+- Android XML Views, ViewBinding, RecyclerView/NestedScrollView, RxJava3, Retrofit, Koin 구조를 따른다.
+- 신규 코드가 필요하면 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
+- 기존 레거시 파일은 직접 수정하지 않는다. 레거시 변경이 불가피해 보이면 구현 전에 사용자 확인을 받는다.
+- 새로고침용 API는 기존 Repository/ViewModel의 첫 페이지 로딩 함수를 재사용하거나, 의미가 더 명확한 `refresh()` wrapper를 최소 범위로 추가한다.
+- `requestGeneration` 또는 동등한 최신 요청 판별 로직이 있는 ViewModel은 기존 패턴을 유지해 오래된 응답이 화면을 덮어쓰지 않게 한다.
+- 공개 API schema, request/response DTO 필드명, endpoint는 변경하지 않는다.
+- 구현 계획 작성 시 홈/콘텐츠/대화 탭을 Phase로 나누고 각 Phase별 테스트와 수동 확인 기준을 명시한다.
+
+---
+
+## 10. Metrics
+- 홈 탭 `추천`, `랭킹`, `팔로잉` 각각에서 당겨서 새로고침 시 현재 선택 탭의 API만 첫 페이지 기준으로 다시 호출된다.
+- 콘텐츠 탭 `추천`, `랭킹`, `전체` 각각에서 당겨서 새로고침 시 현재 선택 탭/조건의 API만 첫 페이지 기준으로 다시 호출된다.
+- 콘텐츠 `전체` 탭은 현재 `type`, `sort`, `dayOfWeek` 조건을 유지한다.
+- 대화 탭은 현재 선택 필터에 따라 `ALL`, `AI`, `DM` 중 올바른 filter 값으로 `GET /api/v2/chat/rooms` 첫 페이지를 다시 호출한다.
+- 새로고침 성공 시 기존 목록이 중복 append되지 않고 새 응답으로 교체된다.
+- 새로고침 실패 시 indicator가 종료되고 crash 없이 기존 error/toast 정책이 적용된다.
+- empty 상태에서도 새로고침 재시도가 가능하다.
+- 관련 ViewModel 단위 테스트 또는 Fragment/adapter 테스트와 구현 범위에 맞는 Gradle 검증이 성공한다.
+
+---
+
+## 11. Open Questions
+- 홈 `랭킹` 탭의 현재 API/상태 이름은 구현 계획 단계에서 실제 파일 기준으로 최종 확인한다.
+- 홈 `팔로잉` 탭의 로그인 필요 상태에서 당겨서 새로고침을 허용할지, 로그인 가드 화면에서는 막을지는 기존 로그인 가드 정책을 확인해 결정한다.
+- 콘텐츠 `랭킹` 탭에서 내부 ranking type이 여러 개일 경우 현재 선택 type 유지 방식은 구현 계획에서 실제 ViewModel 상태를 기준으로 확정한다.
+- refresh indicator 색상은 별도 디자인 요구가 없으므로 기존 theme 기본값을 우선 사용하고, 필요 시 구현 계획에서 기존 앱 색상과 맞춘다.
+
+---
+
+## 12. References
+- PRD sample: `docs/prd/sample-prd.md`
+- 문서 규칙: `docs/agent-guides/work-plan-docs.md`
+- 홈 추천 PRD: `docs/20260601_메인_홈_추천_UI와_API_연동/prd.md`
+- 콘텐츠 추천 PRD: `docs/20260623_메인_콘텐츠_추천_탭/prd.md`
+- 콘텐츠 전체 탭 API: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/MainContentAllTabApi.kt`
+- 콘텐츠 전체 탭 ViewModel: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentAllTabViewModel.kt`
+- 대화 탭 PRD: `docs/20260609_채팅_탭_페이지/prd.md`
+
+---
+
+## 13. Verification Log
+- 2026-07-22: `docs/prd/sample-prd.md`, `docs/agent-guides/work-plan-docs.md`를 확인해 신규 PRD 문서 구조와 저장 위치 규칙을 확인했다.
+- 2026-07-22: 기존 홈 추천, 콘텐츠 추천, 대화 탭 PRD를 확인해 하단 탭 및 내부 탭/필터별 API 호출 정책을 대조했다.
+- 2026-07-22: `ContentAllTabViewModel`, `MainContentAllTabApi`, `ContentOverviewViewModel`, 관련 UI state 구조를 codegraph로 확인해 첫 페이지 재조회와 pagination 분리 요구사항을 문서화했다.
+- 2026-07-22: 사용자 확인에 따라 적용 범위를 메인 홈 `추천/랭킹/팔로잉`, 콘텐츠 `추천/랭킹/전체`, 대화 `전체/AI 채팅/DM` 현재 선택 상태별 새로고침으로 확정했다.
+- 2026-07-22: 이번 단계는 PRD 작성만 수행했으며 구현/빌드/테스트는 실행하지 않는다.