Files
sodalive-ios/docs/20260818_메인_탭_당겨서_새로고침/plan-task.md

694 lines
47 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 메인 탭 당겨서 새로고침 구현 계획
| 문서 항목 | 내용 |
|---|---|
| 상태 | Phase 4 layout 보존 motion 자동 검증 완료 / 사용자 확인 대기 |
| 작성일 | `2026-08-18` |
| 요구사항 기준 | `docs/20260818_메인_탭_당겨서_새로고침/prd.md` |
| API 기준 | 기존 홈/콘텐츠/대화 API 계약 재사용, 신규 API 없음 |
| 현재 Phase | Phase 1~3 완료, Phase 4 Gate 수동 검증 대기 |
| 현재 활성 Goal | `P4-R3` |
## 목표
메인 `홈`, `콘텐츠`, `대화` 탭에서 현재 선택된 내부 탭·필터 상태를 유지한 채 사용자가 당겨서 최신 데이터를 다시 조회할 수 있게 한다.
## 현재 상태
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|---:|---|---:|---|---|
| 1 | 완료 | `1/1` | 없음 | 없음 |
| 2 | 완료 | `1/1` | 없음 | 없음 |
| 3 | 완료 | `1/1` | 없음 | 없음 |
| 4 | 회귀 수정 대기 | `4/5` | `P4-R3` | layout 보존 motion 구현·검증 |
- 동시에 하나의 미완료 goal만 운용한다.
- 완료된 Task와 검증 기록은 되돌리거나 삭제하지 않는다. 후속 수정은 회귀 수정 Task와 새 goal ID를 추가한다.
- 구현 체크박스는 완료 상태를 반영하고, 사용자 수동 검증 체크박스는 실제 확인 전까지 미완료로 둔다.
## 범위
### 포함
- 메인 홈 내부 `추천`, `랭킹`, `팔로잉` 현재 선택 상태 새로고침.
- 메인 콘텐츠 내부 `추천`, `랭킹`, `전체` 현재 선택 상태 새로고침.
- 콘텐츠 `랭킹`의 현재 `AudioRankingType`, 콘텐츠 `전체`의 현재 `MainContentAllType`, `ContentSort`, `SeriesPublishedDaysOfWeek` 유지.
- 대화 탭의 현재 `MainChatFilter` 유지와 첫 페이지 재조회.
- loading, empty, error, content 상태에서 모두 새로고침 가능하도록 상태 화면 구조 점검.
- 메인 홈·콘텐츠·대화 7개 View의 20개 refresh 가능 상태에서 기본 indicator를 `pull-to-refresh.json` Lottie로 교체.
- Lottie를 최대 `48×48pt`, 원본 비율 유지, 중앙 정렬하고 상하 `8pt`를 포함한 총 `64pt` refresh 영역으로 표시.
- Reduce Motion 활성화 시 animation을 재생하지 않고 첫 번째 유효 frame `1`을 표시.
### 제외
- `마이` 탭 새로고침.
- 하단 탭 전체 일괄 새로고침.
- 신규 API endpoint, 신규 dependency, 신규 공통 refresh abstraction.
- 자동 갱신, polling, push 기반 갱신.
- `LiveNowAllView`, `ContentDetailView`와 그 밖의 legacy 화면 pull-to-refresh 변경.
## 기술적 제약
- 기술 스택: Swift, SwiftUI, Combine, Moya, CocoaPods 기반 iOS 앱.
- 아키텍처: 기존 `View -> ViewModel -> Repository -> Api(TargetType)` 흐름을 유지한다.
- UI: SwiftUI `.refreshable` 또는 동등한 iOS 기본 refresh affordance를 우선 사용한다.
- 후속 UI: SwiftUI `.refreshable`은 custom indicator API를 제공하지 않으므로 Phase 4에서는 V2 공용 `LottieRefreshableScrollView` 한 개로만 대체한다.
- Animation: Lottie `4.6.1`, `SodaLive/Resources/pull-to-refresh.json`만 사용한다. asset은 `150×150`, `30fps`, frame `0..<45`, marker 없음이다.
- 접근성: `accessibilityReduceMotion == true`이면 frame `1`을 정지 표시하고, Lottie visual은 접근성 트리에서 숨긴다. refresh action 이름은 기존 `I18n.LiveNow.refreshButton`을 재사용한다.
- 데이터·보안: 기존 인증 header, token 저장, 언어 header 흐름을 변경하지 않는다.
- 의존성: Lottie `4.6.1` 외 신규 dependency를 추가하지 않고, 기존 `RefreshableScrollView` package source는 수정하지 않는다.
- 계약: 제공되지 않은 endpoint, DTO, enum, 오류 status/key를 추정하지 않는다.
- 구현: 모든 구현 Task는 `RED → GREEN → REFACTOR` 순서를 체크박스에 명시한다. 현재 저장소는 테스트 번들 타깃이 명확하지 않으므로 테스트 작성이 불가능한 Task는 대체 검증 방법을 사용한다.
- 검증: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`와 수동 QA를 필수 Gate로 둔다.
## Task TDD 작성 규칙
테스트 작성이 현실적으로 가능한 경우 아래 순서를 따른다.
- [ ] **RED:** 가장 작은 실패 test를 작성한다.
- [ ] **RED 확인:** focused test를 실행해 요구 동작이 없어서 발생한 의도한 assertion 실패를 확인한다.
- [ ] **GREEN:** RED를 통과시키는 최소 구현을 작성한다.
- [ ] **GREEN 확인:** 같은 focused test를 다시 실행해 성공을 확인한다.
- [ ] **REFACTOR:** 새 동작을 바꾸지 않는 범위에서 이번 Task가 만든 중복만 정리하고, focused test·직접 영향 회귀·lint를 다시 실행해 실제 결과를 Progress에 기록한다.
현재 `docs/agent-guides/build-test-verification.md` 기준으로 테스트 번들 타깃이 확인되지 않는다. 테스트 타깃이 없는 상태에서는 각 구현 Task에 `TDD 예외 사유``대체 검증 방법`을 기록하고 빌드와 수동 QA로 Gate를 통과시킨다.
## Phase 1 홈 탭 새로고침
**Phase 결과:** 홈 탭의 현재 내부 선택 상태에서 당겨서 새로고침을 실행할 수 있다.
**선행조건:** PRD `REFRESH-001~004`, `HOME-REFRESH-001~003` 확정.
**Phase 완료 조건:** `P1-T1``P1-GATE` 완료, 검증 기록 누적.
### 구현 항목
#### Task 1.1 홈 내부 탭별 refresh 연결
**Goal 실행 `P1-T1`:**`추천`, `랭킹`, `팔로잉`에서 현재 선택된 내부 탭의 기존 fetch 경로를 당겨서 새로고침에 연결한다.
- **시작 조건:** PRD `HOME-REFRESH-001~003`, `REFRESH-003` 확정.
- **완료 증거:** 홈 내부 3개 탭의 새로고침 동작과 빈/오류 상태 재시도 확인.
- **범위 밖:** 콘텐츠/대화 탭 변경, 공통 refresh abstraction 생성.
- **TDD 예외 사유:** 현재 테스트 번들 타깃이 확인되지 않는 SwiftUI 화면 동작 변경이다.
- **대체 검증 방법:** 구현 diff 검토, `xcodebuild` 빌드, 시뮬레이터 또는 기기 수동 QA.
**Files:**
- Modify: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift`
- Modify: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift`
- Modify: `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift`
- Modify: `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingViewModel.swift`
- Modify: `SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift`
- Modify: `SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingViewModel.swift`
**Interfaces:**
- Consumes: 기존 `fetchRecommendations()`, `fetchRankings()`, `fetchFollowing()` 메서드.
- Produces: 각 홈 내부 View의 pull-to-refresh 사용자 동작.
- [x] **상태 확인:** 각 View의 현재 loading, empty, error, content 분기를 확인하고 새로고침 가능한 ScrollView 배치가 필요한 지점을 표시한다.
- [x] **GREEN:** 최소 구현으로 각 View에 새로고침 동작을 연결한다. 필요 시 ViewModel에 `refresh()`처럼 기존 fetch를 감싸는 얇은 메서드만 추가한다.
- [x] **GREEN 확인:**`추천`, `랭킹`, `팔로잉`에서 당겨서 새로고침 후 선택된 내부 탭이 유지되는지 수동 확인한다.
- [x] **REFACTOR:** 이번 Task에서 만든 중복만 정리하고 신규 공통 abstraction은 만들지 않는다.
### 완료 조건
- [x] `P1-T1`의 체크박스와 완료 증거가 모두 충족됐다.
- [x] 홈 내부 탭 3개가 PRD 요구사항과 추적된다.
- [x] 알려진 문서와 구현의 차이가 없다.
### 검증 방법
#### Phase 1 Gate
**Goal 실행 `P1-GATE`:** 홈 탭 새로고침 사용자 흐름과 공통 품질 기준을 최종 판정한다.
- **시작 조건:** `P1-T1` 완료.
- **완료 증거:** 아래 자동·수동 검증 통과와 Progress 기록.
- **범위 밖:** 콘텐츠/대화 탭 구현.
```bash
xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build
```
**Expected:** exit code 0. 홈 탭 진입 후 `추천`, `랭킹`, `팔로잉` 각각에서 당겨서 새로고침이 동작하고 내부 선택 상태가 유지된다.
수동 검증:
- [x]`추천` 콘텐츠 상태에서 새로고침한다.
- [x]`랭킹` 콘텐츠 상태에서 새로고침한다.
- [x]`팔로잉` 콘텐츠 또는 로그인 필요/빈 상태에서 새로고침한다.
- [x] 새로고침 실패 시 기존 오류 표시 정책을 유지한다.
## Phase 2 콘텐츠 탭 새로고침
**Phase 결과:** 콘텐츠 탭의 현재 내부 선택 상태와 필터 조건에서 당겨서 새로고침을 실행할 수 있다.
**선행조건:** `P1-GATE` 완료.
**Phase 완료 조건:** `P2-T1``P2-GATE` 완료, 검증 기록 누적.
### 구현 항목
#### Task 2.1 콘텐츠 내부 탭별 refresh 연결
**Goal 실행 `P2-T1`:** 콘텐츠 `추천`, `랭킹`, `전체`에서 현재 선택된 내부 탭과 필터 상태의 기존 fetch 경로를 당겨서 새로고침에 연결한다.
- **시작 조건:** PRD `CONTENT-REFRESH-001~003`, `REFRESH-003~004` 확정.
- **완료 증거:** 콘텐츠 내부 3개 탭과 `전체` 하위 필터 상태의 새로고침 확인.
- **범위 밖:** 홈/대화 탭 변경, 전체 데이터 일괄 refresh.
- **TDD 예외 사유:** 현재 테스트 번들 타깃이 확인되지 않는 SwiftUI 화면 동작 변경이다.
- **대체 검증 방법:** 구현 diff 검토, `xcodebuild` 빌드, 시뮬레이터 또는 기기 수동 QA.
**Files:**
- Modify: `SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationView.swift`
- Modify: `SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationViewModel.swift`
- Modify: `SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingView.swift`
- Modify: `SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingViewModel.swift`
- Modify: `SodaLive/Sources/V2/Main/Content/All/MainContentAllView.swift`
- Modify: `SodaLive/Sources/V2/Main/Content/All/MainContentAllViewModel.swift`
**Interfaces:**
- Consumes: 기존 `fetchRecommendations()`, `fetchRankings()`, `fetchFirstPage()` 메서드와 선택 상태 `selectedType`, `selectedSort`, `selectedDayOfWeek`, `selectedType`.
- Produces: 콘텐츠 내부 View의 pull-to-refresh 사용자 동작.
- [x] **상태 확인:** 콘텐츠 `추천`, `랭킹`, `전체`의 loading, empty, error, content 분기를 확인한다.
- [x] **GREEN:** 최소 구현으로 각 View에 새로고침 동작을 연결한다. 콘텐츠 `전체`는 현재 type/sort/dayOfWeek를 유지하고 첫 페이지를 다시 조회한다.
- [x] **GREEN 확인:** 콘텐츠 `추천`, `랭킹`, `전체`에서 당겨서 새로고침 후 선택된 내부 탭과 필터가 유지되는지 확인한다.
- [x] **REFACTOR:** 새로 만든 중복만 정리하고 신규 공통 abstraction은 만들지 않는다.
### 완료 조건
- [x] `P2-T1`의 체크박스와 완료 증거가 모두 충족됐다.
- [x] 콘텐츠 내부 탭 3개와 `전체` 하위 필터 상태가 PRD 요구사항과 추적된다.
- [x] 알려진 문서와 구현의 차이가 없다.
### 검증 방법
#### Phase 2 Gate
**Goal 실행 `P2-GATE`:** 콘텐츠 탭 새로고침 사용자 흐름과 공통 품질 기준을 최종 판정한다.
- **시작 조건:** `P2-T1` 완료.
- **완료 증거:** 아래 자동·수동 검증 통과와 Progress 기록.
- **범위 밖:** 대화 탭 구현.
```bash
xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build
```
**Expected:** exit code 0. 콘텐츠 탭 진입 후 `추천`, `랭킹`, `전체` 각각에서 당겨서 새로고침이 동작하고 현재 선택 상태가 유지된다.
수동 검증:
- [x] 콘텐츠 `추천`에서 새로고침한다.
- [x] 콘텐츠 `랭킹`에서 랭킹 타입을 바꾼 뒤 새로고침하고 선택 타입이 유지되는지 확인한다.
- [x] 콘텐츠 `전체 > 오디오`에서 sort를 바꾼 뒤 새로고침하고 type/sort가 유지되는지 확인한다.
- [x] 콘텐츠 `전체 > 시리즈`에서 요일과 sort를 바꾼 뒤 새로고침하고 type/dayOfWeek/sort가 유지되는지 확인한다.
- [x] 빈/오류 상태에서도 새로고침을 재시도할 수 있는지 확인한다.
## Phase 3 대화 탭 새로고침
**Phase 결과:** 대화 탭의 현재 필터에서 당겨서 첫 페이지를 다시 조회할 수 있다.
**선행조건:** `P2-GATE` 완료.
**Phase 완료 조건:** `P3-T1``P3-GATE` 완료, 검증 기록 누적.
### 구현 항목
#### Task 3.1 대화 필터별 refresh 연결
**Goal 실행 `P3-T1`:** 대화 `전체`, `AI`, `DM` 현재 필터의 기존 첫 페이지 fetch 경로를 당겨서 새로고침에 연결한다.
- **시작 조건:** PRD `CHAT-REFRESH-001~002`, `REFRESH-003~004` 확정.
- **완료 증거:** 대화 3개 필터의 새로고침과 pagination 초기화 확인.
- **범위 밖:** 채팅방 상세 라우팅 변경, 신규 DM 기능.
- **TDD 예외 사유:** 현재 테스트 번들 타깃이 확인되지 않는 SwiftUI 화면 동작 변경이다.
- **대체 검증 방법:** 구현 diff 검토, `xcodebuild` 빌드, 시뮬레이터 또는 기기 수동 QA.
**Files:**
- Modify: `SodaLive/Sources/V2/Main/Chat/MainChatView.swift`
- Modify: `SodaLive/Sources/V2/Main/Chat/MainChatViewModel.swift`
**Interfaces:**
- Consumes: 기존 `selectedFilter`, `fetchFirstPage(filter:)`, `fetchNextPageIfNeeded(currentItem:)`.
- Produces: 대화 View의 pull-to-refresh 사용자 동작.
- [x] **상태 확인:** 대화 탭의 loading, empty, error, content 분기와 `ScrollView` 배치를 확인한다.
- [x] **GREEN:** 최소 구현으로 현재 `selectedFilter`를 유지한 첫 페이지 새로고침을 연결한다.
- [x] **GREEN 확인:** `전체`, `AI`, `DM` 각 필터에서 새로고침 후 필터가 유지되고 `rooms`, `hasMore`, `nextCursor`가 첫 페이지 응답 기준으로 갱신되는지 확인한다.
- [x] **REFACTOR:** 새로 만든 중복만 정리하고 신규 공통 abstraction은 만들지 않는다.
### 완료 조건
- [x] `P3-T1`의 체크박스와 완료 증거가 모두 충족됐다.
- [x] 대화 필터 3개와 pagination 초기화가 PRD 요구사항과 추적된다.
- [x] 알려진 문서와 구현의 차이가 없다.
### 검증 방법
#### Phase 3 Gate
**Goal 실행 `P3-GATE`:** 대화 탭 새로고침 사용자 흐름과 최종 회귀 범위를 판정한다.
- **시작 조건:** `P3-T1` 완료.
- **완료 증거:** 아래 자동·수동 검증 통과와 Progress 기록.
- **범위 밖:** 실패와 무관한 다음 기능 구현.
```bash
xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build
```
**Expected:** exit code 0. 대화 `전체`, `AI`, `DM` 필터에서 당겨서 새로고침이 동작하고 필터와 pagination 상태가 올바르게 유지·초기화된다.
수동 검증:
- [x] 대화 `전체`에서 새로고침한다.
- [x] 대화 `AI`에서 새로고침하고 필터가 유지되는지 확인한다.
- [x] 대화 `DM`에서 새로고침하고 필터가 유지되는지 확인한다.
- [x] 빈/오류 상태에서도 새로고침을 재시도할 수 있는지 확인한다.
- [x] 새로고침 후 스크롤 하단에서 다음 페이지 조회가 이어지는지 확인한다.
## Phase 4 Lottie 새로고침 표시
**Phase 결과:** 메인 홈·콘텐츠·대화 7개 View의 모든 refresh 가능 상태에서 기본 indicator 대신 지정된 Lottie가 표시되고 기존 새로고침 동작은 유지된다.
**선행조건:** Phase 1~3 Gate 완료, PRD `REFRESH-ANIMATION-001~005``DEC-004~011` 확정, 사용자 코드 구현 승인.
**Phase 완료 조건:** `P4-T1`, `P4-T2`, `P4-GATE` 완료와 검증 기록 누적.
### 구현 항목
#### Task 4.1 공용 Lottie refresh component
**Goal 실행 `P4-T1`:** Lottie 표시와 pull-to-refresh 상태를 한 곳에서 관리하는 V2 공용 vertical scroll component를 만든다.
- **시작 조건:** `SodaLive/Resources/pull-to-refresh.json`과 Lottie `4.6.1``SodaLive`, `SodaLive-dev` target에 포함되어 있다.
- **완료 증거:** component source 2개가 두 target에서 빌드되고, pull offset·단일 refresh·완료 초기화·Reduce Motion 상태가 아래 contract를 따른다.
- **범위 밖:** 메인 7개 View 연결, legacy `RefreshableScrollView` 변경, pull progress와 animation progress 연동.
- **TDD 예외 사유:** 현재 저장소에 앱 test bundle target이 없고, ScrollView bounce와 Lottie playback은 runtime UI 동작이다.
- **대체 검증 방법:** asset metadata 검사, 두 scheme Debug 빌드, Phase 4 수동 상태 전이 확인.
**Files:**
- Create: `SodaLive/Sources/V2/Component/Refresh/LottieRefreshIndicator.swift`
- Create: `SodaLive/Sources/V2/Component/Refresh/LottieRefreshableScrollView.swift`
- Modify: `SodaLive.xcodeproj/project.pbxproj` — 두 source를 `SodaLive`, `SodaLive-dev` Sources build phase에 추가
- Verify: `SodaLive/Resources/pull-to-refresh.json`
**Interfaces:**
```swift
struct LottieRefreshIndicator: View {
let isAnimating: Bool
}
struct LottieRefreshableScrollView<Content: View>: View {
init(
action: @escaping () async -> Void,
@ViewBuilder content: @escaping () -> Content
)
}
```
- Consumes: Lottie `LottieView`, `pull-to-refresh.json`, `SodaSpacing.s8`, `SodaSpacing.s48`, `EnvironmentValues.accessibilityReduceMotion`, `I18n.LiveNow.refreshButton`.
- Produces: indicator가 없는 vertical `ScrollView`, `64pt` trigger/표시 영역, 중복 실행을 막는 단일 async refresh action, accessibility refresh action.
- [x] **Asset 확인:** JSON이 `150×150`, `30fps`, frame `0..<45`이고 frame `0`은 비어 있으며 frame `1`부터 stroke가 표시되는지 확인한다.
- [x] **GREEN:** `LottieRefreshIndicator`는 일반 설정에서 영역 노출 중 frame `0..<45`를 반복 재생하고, Reduce Motion에서는 frame `1`에 정지한다. 크기는 aspect fit 최대 `48×48pt`, 중앙 정렬, Lottie 자체는 accessibility tree에서 숨긴다.
- [x] **GREEN:** `LottieRefreshableScrollView`는 양수 pull offset에서 indicator를 보이고, `64pt` 기준으로 action을 한 번만 실행하며, action이 끝날 때까지 영역을 유지한 뒤 offset·animation을 처음 상태로 되돌린다.
- [x] **GREEN:** 기준 거리 미만에서 손을 놓으면 API를 호출하지 않고 indicator를 숨겨 frame `0`으로 초기화하며, refresh 중 재당김은 두 번째 action을 만들지 않는다.
- [x] **GREEN 확인:** 두 scheme을 빌드해 Lottie API, async closure, project source membership 오류가 없음을 확인한다.
- [x] **REFACTOR:** offset 측정용 `PreferenceKey`와 refresh 상태는 `LottieRefreshableScrollView.swift` 내부 private 구현으로 두고 추가 protocol·ViewModel·config type을 만들지 않는다.
#### Task 4.2 메인 7개 View 연결
**Goal 실행 `P4-T2`:** 기존 7개 View의 20개 `.refreshable` 경로를 공용 Lottie refresh component로 교체한다.
- **시작 조건:** `P4-T1` 완료.
- **완료 증거:** 대상 파일의 `.refreshable` 0건, 기존 `refresh()` 호출·상태 분기·레이아웃 유지, 두 scheme 빌드와 수동 QA 통과.
- **범위 밖:** 7개 ViewModel 변경, `LiveNowAllView`, `ContentDetailView`, 최초 loading용 본문 `ProgressView` 디자인 변경.
- **TDD 예외 사유:** 현재 앱 test bundle target이 없는 SwiftUI 화면 연결 변경이다.
- **대체 검증 방법:** 대상 modifier 정적 검색, ViewModel diff 부재 확인, 두 scheme Debug 빌드, 화면별 수동 QA.
**Files:**
- Modify: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift`
- Modify: `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift`
- Modify: `SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift`
- Modify: `SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationView.swift`
- Modify: `SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingView.swift`
- Modify: `SodaLive/Sources/V2/Main/Content/All/MainContentAllView.swift`
- Modify: `SodaLive/Sources/V2/Main/Chat/MainChatView.swift`
- Test: 별도 test file 없음 — Phase 4 Gate의 정적 검사·빌드·수동 검증 사용
**Interfaces:**
- Consumes: `LottieRefreshableScrollView(action:content:)`와 기존 7개 ViewModel의 `refresh() async`.
- Produces: content·loading·empty/error 분기 전체에서 동일한 Lottie pull-to-refresh 사용자 동작.
- [x] **상태 확인:** 현재 7개 View의 `.refreshable` 20곳과 각 content·loading·empty/error 분기를 대조한다.
- [x] **GREEN:**`ScrollView``.refreshable` 조합을 `LottieRefreshableScrollView`로 교체하고 action에는 기존 `await viewModel.refresh()`만 연결한다.
- [x] **GREEN:** 기존 `frame(minHeight:)`, background, padding, pagination `onAppear`와 최초 loading `ProgressView`를 그대로 유지한다.
- [x] **GREEN 확인:** 대상 7개 파일의 `.refreshable`이 0건이고 `LottieRefreshableScrollView` 적용 수가 기존 상태 분기 수와 일치하는지 정적 검사한다.
- [ ] **GREEN 확인:** 홈·콘텐츠·대화의 선택 탭·필터·pagination·실패 재시도 동작이 Phase 1~3 결과와 동일한지 수동 확인한다.
- [x] **REFACTOR:** 7개 ViewModel, legacy pull-to-refresh 화면과 요청 범위 밖 UI에 diff가 없는지 확인한다.
#### Task 4.3 pull trigger 회귀 수정
**Goal 실행 `P4-R1`:** Phase 4 첫 수동 검증에서 Lottie 표시와 refresh가 모두 실행되지 않은 회귀를 수정한다.
- **원인:** viewport와 같은 짧은 content의 bounce 부재, 신뢰할 수 없는 `DragGesture.onEnded`, 현재 hierarchy에서 양수 pull offset을 만들지 못한 named coordinate 측정이 겹쳤다.
- **TDD 예외 사유:** 앱 test bundle target이 없고 실제 `ScrollView` rubber-band와 gesture arbitration이 필요한 runtime UI 회귀다.
- **대체 검증 방법:** 사용자 실패 재현, Apple·OSS·설치 package source 비교, 정적 검사, 두 scheme Debug 빌드, 사용자 재확인.
- **Files:**
- Modify: `SodaLive/Sources/V2/Component/Refresh/LottieRefreshableScrollView.swift`
- [x] **RED:** 사용자가 당김 시 Lottie 표시와 refresh가 모두 실행되지 않음을 확인했다.
- [x] **원인 확인:** 짧은 content의 bounce 부재, `DragGesture.onEnded` 트리거와 offset 측정을 기존 동작 package·Apple·OSS 패턴과 비교했다.
- [x] **GREEN:** vertical bounce를 항상 허용하고 global moving-minus-fixed offset이 `64pt`를 넘겼다가 돌아오는 전이에서 action을 한 번만 실행하도록 수정했다.
- [x] **GREEN 확인:** named coordinate·`DragGesture` 0건, global moving/fixed 측정과 bounce·armed 전이 존재, 두 scheme Debug build 성공을 확인했다.
- [x] **수동 확인:** 기존 실패 화면에서 Lottie가 표시되고 refresh action이 한 번 실행되는지 사용자가 재확인한다.
#### Task 4.4 refresh motion 회귀 수정 (롤백)
**Goal 실행 `P4-R2`:** 기능 복구 후 refresh 시작·완료 시 발생하는 UI 덜컹임을 제거한다.
- **원인:** ScrollView bounce-back 중 refresh row 높이 `0→64pt`와 indicator offset `-64→0`이 동시에 즉시 변경되고, 완료 시 row가 `64→0pt`로 즉시 축소됐다.
- **TDD 예외 사유:** 앱 test bundle target이 없고 실제 ScrollView rubber-band와 layout handoff가 필요한 runtime motion 회귀다.
- **대체 검증 방법:** 사용자 체감 RED, 기존 package freeze 패턴 비교, 정적 검사, 두 scheme Debug 빌드, 사용자 재확인.
- **Files:**
- Modify: `SodaLive/Sources/V2/Component/Refresh/LottieRefreshableScrollView.swift`
- [x] **RED:** 사용자가 Lottie 표시와 refresh는 동작하지만 진행이 덜컹거림을 확인했다.
- [x] **원인 확인:** 시작·완료의 구조적 row 삽입/축소와 indicator 위치 전환이 bounce-back과 충돌함을 확인했다.
- [x] **GREEN:** 구조적 refresh row를 제거하고 content·indicator가 같은 frozen alignment handoff를 사용하도록 수정했다.
- [x] **GREEN 확인:** frozen alignment, Reduce Motion 분기, 완료 animation과 두 scheme Debug build 성공을 확인했다.
- [x] **수동 확인:** motion은 부드러워졌지만 기존 UI 간격이 변경되는 회귀를 사용자가 확인했다.
- [x] **ROLLBACK:** 사용자 결정에 따라 frozen motion handoff와 후속 layout 보정을 모두 제거하고 `P4-R1` 기능 복구 상태로 되돌렸다.
#### Task 4.5 layout 보존 refresh motion 수정
**Goal 실행 `P4-R3`:** 기존 `VStack` layout과 UI 간격을 유지하면서 refresh 시작·완료의 목록 위치 전환을 부드럽게 한다.
- **원인:** `isRefreshing` 전환 때 refresh 영역 높이와 indicator offset이 animation 없이 즉시 `0↔64pt`로 변경되어 목록이 위아래로 순간 이동한다.
- **선택 설계:** 현재 `VStack``refreshRegion` 구조는 유지하고 `isRefreshing` 시작·완료 상태만 동일한 `.easeOut(duration: 0.2)` transaction으로 처리한다.
- **대안 제외:** 고정 64pt 영역은 scroll layout을 변경하고, `UIRefreshControl` 재작성은 현재 회귀에 비해 범위가 크므로 제외한다.
- **접근성:** Reduce Motion에서는 animation 없이 즉시 상태를 전환한다.
- **TDD 예외 사유:** 앱 test bundle target이 없고 실제 ScrollView rubber-band와 layout animation이 필요한 runtime motion 회귀다.
- **대체 검증 방법:** 사용자 체감 RED, 구조·상태 전이 정적 검사, 두 scheme Debug 빌드, 사용자 재확인.
- **Files:**
- Modify: `SodaLive/Sources/V2/Component/Refresh/LottieRefreshableScrollView.swift`
- [x] **RED:** Lottie와 refresh는 동작하지만 시작·완료 때 목록이 위아래로 끊겨 이동함을 사용자가 확인했다.
- [x] **설계 승인:** 기존 구조의 높이·offset 전환만 animate하는 최소안을 사용자가 선택했다.
- [x] **GREEN:** refresh 시작·완료를 같은 Reduce Motion-aware animation transaction으로 처리한다.
- [x] **GREEN 확인:** 기존 `VStack`, global offset 측정, bounce, armed trigger와 두 scheme build 성공을 확인한다.
- [ ] **수동 확인:** 기존 UI 간격이 유지되고 시작·완료 목록 이동이 부드러운지 사용자가 확인한다.
### 완료 조건
- [ ] `P4-T1`, `P4-T2`의 체크박스와 완료 증거가 모두 충족됐다.
- [x] `REFRESH-ANIMATION-001~005`가 구현 또는 명시적 제외로 추적된다.
- [x] Lottie와 기본 indicator가 동시에 보이는 대상 상태가 없다.
- [ ] Phase 1~3에서 검증한 데이터 refresh 동작에 회귀가 없다.
### 검증 방법
#### Phase 4 Gate
**Goal 실행 `P4-GATE`:** Lottie 표시, 접근성 설정, 기존 데이터 refresh와 범위 제외를 최종 판정한다.
- **시작 조건:** `P4-T1`, `P4-T2` 완료.
- **완료 증거:** 아래 자동·수동 검증 통과와 Progress 기록.
- **범위 밖:** Gate 실패와 무관한 UI 변경, legacy refresh 화면 변경, test 삭제·완화.
```bash
jq -e '.w == 150 and .h == 150 and .fr == 30 and .ip == 0 and .op == 45' SodaLive/Resources/pull-to-refresh.json
! rg -n '\.refreshable' SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationView.swift SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingView.swift SodaLive/Sources/V2/Main/Content/All/MainContentAllView.swift SodaLive/Sources/V2/Main/Chat/MainChatView.swift
xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug -sdk iphonesimulator -destination "generic/platform=iOS Simulator" build CODE_SIGNING_ALLOWED=NO
xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -sdk iphonesimulator -destination "generic/platform=iOS Simulator" build CODE_SIGNING_ALLOWED=NO
git diff --check
```
**Expected:** 모든 명령 exit code 0. 대상 7개 파일에 `.refreshable`이 없고 두 scheme이 빌드되며, asset metadata와 target membership이 일치한다.
수동 검증:
- [ ] 기준 거리 미만으로 당기는 동안 Lottie가 반복 재생되고 손을 놓으면 API 호출 없이 사라진 뒤 다음 당김에서 처음부터 재생된다.
- [ ] `64pt` 기준을 넘겨 새로고침하면 Lottie가 API 완료까지 유지되고 기본 indicator는 보이지 않으며 요청은 한 번만 실행된다.
- [ ] Reduce Motion을 켜면 frame `1`이 정지 표시되고, accessibility refresh action으로도 동일한 기존 `refresh()`가 한 번 실행된다.
- [ ] 홈 3개·콘텐츠 3개·대화 View의 content·loading·empty/error 상태에서 최대 `48×48pt` Lottie와 기존 데이터 갱신 동작을 확인한다.
- [ ] iPhone과 iPad에서 영역이 총 `64pt`이고 content가 겹치거나 잘리지 않으며, `LiveNowAllView``ContentDetailView`는 기존 표시를 유지한다.
## 실행 순서와 의존성
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|---:|---|---|---|---|
| 1 | `P1-T1` | PRD 확정 | 아니요 | 홈 탭 상태 분기 재확인 |
| 2 | `P1-GATE` | `P1-T1` | 아니요 | 실패 소유 Task의 회귀 수정 goal 생성 |
| 3 | `P2-T1` | `P1-GATE` | 아니요 | 콘텐츠 탭 상태 분기 재확인 |
| 4 | `P2-GATE` | `P2-T1` | 아니요 | 실패 소유 Task의 회귀 수정 goal 생성 |
| 5 | `P3-T1` | `P2-GATE` | 아니요 | 대화 탭 상태 분기 재확인 |
| 6 | `P3-GATE` | `P3-T1` | 아니요 | 실패 소유 Task의 회귀 수정 goal 생성 |
| 7 | `P4-T1` | `P3-GATE`, 구현 승인 | 아니요 | asset·Lottie target membership 재확인 |
| 8 | `P4-T2` | `P4-T1` | 아니요 | 대상 View의 상태 분기와 wrapper contract 재확인 |
| 9 | `P4-GATE` | `P4-T2` | 아니요 | 실패 소유 Task의 회귀 수정 goal 생성 |
```text
P1-T1 -> P1-GATE -> P2-T1 -> P2-GATE -> P3-T1 -> P3-GATE -> P4-T1 -> P4-T2 -> P4-GATE
```
## 변경 금지 항목
- 확정된 요구사항을 근거 없이 변경하지 않는다.
- 기존 완료 체크박스와 Progress·Decision Log·검증 기록을 삭제하거나 덮어쓰지 않는다.
- 신규 endpoint, DTO, dependency와 `P4-T1`의 component 2개 외 추가 refresh abstraction을 만들지 않는다.
- 요청 범위 밖의 리팩터링과 UI 재설계를 하지 않는다.
- test를 삭제·skip·완화하거나 타입 오류를 우회해 Gate를 통과시키지 않는다.
- JWT, password, signed URL, 개인 메시지 본문 같은 민감정보를 log·fixture·문서에 기록하지 않는다.
## 의사결정 및 중단 규칙
- PRD와 구현 계획이 충돌하면 PRD를 우선하고 Decision Log에 정정 기록을 남긴다.
- 구현 범위가 바뀌면 `prd.md`를 먼저 보강하고 `plan-task.md` 체크리스트를 업데이트한 뒤 코드를 수정한다.
- 안전한 최소 기본값이 문서에 있으면 그 값만 구현한다. 안전하게 구현할 수 없으면 추정하지 않는다.
- 체크박스 일부, 빌드 일부 또는 코드 작성만 끝난 상태에서는 goal을 `complete`로 갱신하지 않는다.
- 완료 증거와 Progress 기록까지 충족한 뒤에만 goal을 `complete`로 갱신한다.
## Progress
기존 기록을 삭제하거나 덮어쓰지 않고 실제 실행 결과를 차수별로 누적한다.
### 문서 작성 1차 실행 — 2026-08-18
- 상태: 완료
- 무엇을: `prd.md``plan-task.md` 신규 작성.
- 왜: 코드 구현 전 요구사항과 구현 계획을 확정하기 위해 작성.
- 어떻게:
- `docs/sample/sample-prd.md` 확인 — 샘플 구조 확인.
- `docs/sample/sample-plan-task.md` 확인 — 계획 문서 구조 확인.
- `docs/agent-guides/documentation-policy.md` 확인 — 신규 문서 경로 규칙 확인.
- 관련 V2 메인 홈/콘텐츠/대화 View와 ViewModel 확인 — 기존 fetch 경로와 선택 상태 확인.
- 남은 항목: 코드 구현 전체.
- 다음 행동: 사용자가 구현을 승인하면 `P1-T1`부터 진행.
### `P1-T1`~`P3-T1` 1차 실행 — 2026-08-18
- 상태: 구현 완료 / 사용자 수동 검증 대기
- 무엇을: 홈, 콘텐츠, 대화 탭의 현재 선택 상태별 당겨서 새로고침과 Combine 완료 대기 연결.
- 왜: 사용자가 현재 화면을 벗어나지 않고 최신 첫 페이지 또는 단일 조회 결과를 받을 수 있게 하기 위해 구현.
- 어떻게:
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug -sdk iphonesimulator -destination "generic/platform=iOS Simulator" build CODE_SIGNING_ALLOWED=NO` — 성공, exit code 0, `BUILD SUCCEEDED`.
- `git diff --check` — 성공, 오류 없음.
- 코드 검토 — 홈/콘텐츠/대화 현재 선택 상태 유지, 빈/오류 상태 ScrollView 제공, 대화/콘텐츠 전체 첫 페이지 응답 기준 pagination 교체 확인.
- visual-qa — 사용자 요청에 따라 미실행.
- 남은 항목: 아래 사용자 수동 테스트 목록과 각 Phase Gate 체크박스 확인.
- 다음 행동: 사용자가 수동 테스트를 수행하고 성공/실패 결과를 전달한다.
### `P1-T1`~`P3-T1` 리뷰 보정 — 2026-08-18
- 상태: 구현 완료 / 사용자 수동 검증 대기
- 무엇을: Combine 취소 시 refresh 대기 종료, 최초 loading 상태 새로고침, 콘텐츠 전체 최초 요청 중복 방지 보정.
- 왜: checked continuation은 Combine 구독 취소 시 completion을 받지 못해 대기할 수 있고, 기존 최초 loading 화면에는 refresh 가능한 ScrollView가 없었기 때문이다.
- 어떻게:
- 7개 ViewModel — checked continuation 대신 `$isLoading.values`를 기다리도록 변경하고 이미 로딩 중이면 중복 요청 없이 기존 요청 완료를 기다림.
- 7개 View — 최초 loading을 full-height ScrollView로 구성해 기본 `.refreshable` 동작 제공.
- `MainContentAllViewModel.fetchFirstPageIfNeeded()``isLoading == false` guard를 추가해 탭 재진입 중 page 0 중복 요청 차단.
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug -sdk iphonesimulator -destination "generic/platform=iOS Simulator" build CODE_SIGNING_ALLOWED=NO` — 성공, exit code 0, `BUILD SUCCEEDED`.
- `git diff --check` — 성공, 오류 없음.
- Oracle 최종 코드 리뷰 — criterion-blocking finding 없음, 무조건 승인.
- 남은 항목: 아래 사용자 수동 테스트 목록과 각 Phase Gate 체크박스 확인.
- 다음 행동: 사용자가 수동 테스트를 수행하고 성공/실패 결과를 전달한다.
### `P1-GATE`~`P3-GATE` 사용자 수동 검증 — 2026-08-18
- 상태: 완료
- 무엇을: 홈, 콘텐츠, 대화 탭의 당겨서 새로고침과 선택 상태·pagination·오류 재시도 회귀를 사용자 수동 확인.
- 왜: 실제 로그인 데이터와 네트워크 환경에서 사용자 동작 기준을 최종 판정하기 위해 수행.
- 어떻게:
- 사용자 수동 테스트 목록 전체 — 성공 확인.
- PRD 기능·UI/UX 수용 기준 — 충족 확인.
- 남은 항목: 없음.
- 다음 행동: 없음.
### Phase 4 요구사항 인터뷰와 계획 작성 — 2026-08-25
- 상태: 계획 완료 / 구현 미착수
- 무엇을: Lottie playback·크기·Reduce Motion·적용 범위를 확정하고 `P4-T1`, `P4-T2`, `P4-GATE`를 작성했다.
- 왜: 기존 7개 View·20개 상태에 같은 표시 규칙을 적용하되 legacy 화면과 데이터 refresh 로직을 변경하지 않기 위해 작성했다.
- 어떻게:
- `rg``.refreshable``RefreshableScrollView` 실제 사용 화면 확인 — 메인 7개 View와 legacy 2개 View 확인.
- `jq``pull-to-refresh.json` 확인 — `150×150`, `30fps`, frame `0..<45`, frame `0` 비어 있음 확인.
- Xcode project와 `Package.resolved` 확인 — Lottie `4.6.1` 및 asset이 `SodaLive`, `SodaLive-dev` 두 target에 포함됨 확인.
- Lottie·Apple 공식 문서와 현재 `RefreshableScrollView` `1.1.1` source 확인 — 공용 V2 component 계획 근거 확인.
- `git diff --check -- docs/20260818_메인_탭_당겨서_새로고침/prd.md docs/20260818_메인_탭_당겨서_새로고침/plan-task.md` — 성공, exit code 0.
- placeholder·미결정 검색 — 남은 placeholder와 미결 요구사항 없음.
- `git diff --name-only -- SodaLive/Sources` — 출력 없음, 앱 source 코드 미수정 확인.
- 남은 항목: `P4-T1`, `P4-T2`, `P4-GATE` 전체. 이번 요청에서는 코드 구현하지 않음.
- 다음 행동: 사용자가 별도로 코드 구현을 요청하면 `P4-T1`부터 시작.
### `P4-T1`~`P4-T2` 구현과 자동 Gate — 2026-08-25
- 상태: 구현·자동 검증 완료 / 사용자 수동 검증 대기
- 무엇을: V2 공용 Lottie refresh component 2개를 만들고 메인 7개 View의 `.refreshable` 20곳을 교체했다.
- 왜: 기존 데이터 새로고침 동작을 유지하면서 지정된 Lottie 표시·접근성·중복 요청 방지 규칙을 한 곳에서 적용하기 위해 구현했다.
- 어떻게:
- `jq` asset metadata 검사 — `150×150`, `30fps`, frame `0..<45` 확인, exit code 0.
- 정적 검사 — 대상 7개 View의 `.refreshable` 0건, `LottieRefreshableScrollView` 20건(`2/3/3/3/3/3/3`) 확인.
- 범위 검사 — 7개 ViewModel, `LiveNowAllView`, `ContentDetailView` diff 없음 확인.
- `xcodebuild` `SodaLive` Debug simulator build — 성공, exit code 0, `BUILD SUCCEEDED`.
- `xcodebuild` `SodaLive-dev` Debug simulator build — 성공, exit code 0, `BUILD SUCCEEDED`.
- `git diff --check` — 성공, 오류 없음.
- P4-T1·P4-T2 spec review와 code-quality review — 차단 finding 없이 승인.
- visual-qa — 사용자 요청에 따라 미실행.
- 남은 항목: Phase 4 수동 검증 5개와 `P4-GATE` 최종 완료 처리.
- 다음 행동: 사용자가 아래 Phase 4 Lottie 후속 검증 5개를 확인하고 결과를 전달한다.
### `P4-R1` pull trigger 회귀 수정 — 2026-08-25
- 상태: 수정·자동 검증 완료 / 사용자 재확인 대기
- 무엇을: `LottieRefreshableScrollView`의 짧은 content bounce와 refresh release 판정을 수정했다.
- 왜: Phase 4 첫 수동 검증에서 Lottie 표시와 데이터 refresh가 모두 실행되지 않았기 때문이다.
- 어떻게:
- 사용자 RED — 당김 시 Lottie 표시 없음, refresh 실행 없음 확인.
- 원인 조사 — `ScrollView` content-size 기반 bounce와 custom `DragGesture.onEnded` 취소 가능성을 Apple·OSS·기존 package source로 교차 확인.
- 1차 수정 — `.scrollBounceBehavior(.always, axes: .vertical)` 추가, `DragGesture` 제거, preference armed→release 전이 적용. 사용자 재확인에서 동일 증상 확인.
- 2차 수정 — 기존 동작 package와 같은 global moving-minus-fixed frame 차이로 pull offset 측정 교체.
- 정적 검사 — named coordinate·`DragGesture` 0건, global moving/fixed 측정과 bounce·armed 상태 전이 확인.
- `xcodebuild` `SodaLive`, `SodaLive-dev` Debug simulator build — 성공, `BUILD SUCCEEDED`.
- 남은 항목: 기존 실패 화면 사용자 재확인과 Phase 4 수동 검증 5개.
- 다음 행동: 사용자가 기존 실패 화면에서 다시 당겨 Lottie 표시와 refresh 실행 여부를 확인한다.
### `P4-R2` motion 보정과 롤백 — 2026-08-25
- 상태: 사용자 결정으로 롤백
- 무엇을: refresh 시작·완료 덜컹임을 줄이기 위해 frozen alignment handoff를 적용했으나 기존 UI 간격이 변경되어 해당 보정과 후속 layout 수정을 모두 제거했다.
- 왜: 사용자 우선순위에 따라 기존 UI layout을 motion 개선보다 우선 보존하기 위해 롤백했다.
- 어떻게:
- motion 보정 후 새로고침 진행이 부드러워짐을 사용자 확인.
- 기존 UI 간격 변경 회귀를 사용자 확인.
- `LottieRefreshableScrollView`를 global moving-minus-fixed offset 기반 `P4-R1` 기능 복구 상태로 복원.
- 남은 항목: Phase 4 수동 검증 잔여 항목.
- 다음 행동: 기존 UI layout과 Lottie refresh 기능 유지 여부를 확인한다.
### `P4-R3` layout 보존 motion 수정 — 2026-08-26
- 상태: 구현·자동 검증 완료 / 사용자 확인 대기
- 무엇을: 기존 `VStack`과 refresh hierarchy를 유지한 채 시작·완료의 `isRefreshing` 상태 전환만 `.easeOut(duration: 0.2)`로 처리했다.
- 왜: Lottie와 refresh 기능은 정상이지만 refresh 영역 높이가 즉시 `0↔64pt`로 바뀌어 목록이 위아래로 순간 이동했기 때문이다.
- 어떻게:
- 사용자 RED — Lottie와 refresh는 동작하지만 목록 위치가 시작·완료 때 끊겨 이동함을 확인.
- 정적 검사 — `VStack(spacing: 0)`, global moving-minus-fixed 측정, bounce, armed trigger, refresh 영역 높이·offset 식 보존 확인.
- 정적 검사 — Reduce Motion-aware `withAnimation` 2건, `ZStack`, `alignmentGuide`, `UIRefreshControl`, `frozen` 0건 확인.
- `git diff --check` — 성공, 오류 없음.
- `xcodebuild` `SodaLive`, `SodaLive-dev` Debug simulator build — 각각 성공, `BUILD SUCCEEDED` 2건 확인.
- spec compliance review와 code-quality review — 승인, runtime motion과 layout은 사용자 확인 대상으로 유지.
- TDD 예외: 앱 test bundle target이 없고 ScrollView rubber-band와 layout animation의 runtime 상호작용이 필요한 UI 회귀이므로 사용자 RED와 정적 검사·두 scheme build를 대체 증거로 사용했다.
- 남은 항목: 기존 UI 간격 유지, 시작·완료 목록 이동, Reduce Motion, 단일 refresh 실행 사용자 확인.
- 다음 행동: 사용자가 기존 화면에서 refresh를 다시 실행해 layout과 motion을 확인한다.
## 사용자 수동 테스트 목록
사전 조건:
- Debug 앱을 실제 기기 또는 iOS Simulator에서 실행한다.
- 로그인 후 메인 화면에 진입한다.
- 각 테스트에서 화면 상단 목록을 아래로 충분히 당겨 Lottie 새로고침 표시가 보이는지 확인한다.
### 홈 탭
- [x] `홈 > 추천`: 아래로 당기면 indicator가 API 응답까지 유지되고 최신 데이터가 표시된다. 선택 탭은 `추천`으로 유지된다.
- [x] `홈 > 랭킹`: 아래로 당기면 랭킹이 다시 조회되고 선택 탭은 `랭킹`으로 유지된다.
- [x] `홈 > 팔로잉`: 아래로 당기면 팔로잉 데이터가 다시 조회되고 선택 탭은 `팔로잉`으로 유지된다.
- [x] `홈 > 팔로잉` 로그인 필요/빈/오류 상태: 상태 화면에서도 아래로 당겨 재시도할 수 있다.
### 콘텐츠 탭
- [x] `콘텐츠 > 추천`: 아래로 당기면 추천 데이터가 다시 조회되고 선택 탭은 `추천`으로 유지된다.
- [x] `콘텐츠 > 랭킹`: 주간/월간 등 기본값이 아닌 랭킹 타입을 선택한 뒤 새로고침해도 같은 타입이 유지된다.
- [x] `콘텐츠 > 전체 > 오디오`: 타입과 정렬을 변경한 뒤 새로고침해도 선택한 타입/정렬이 유지되고 목록이 첫 페이지 결과로 교체된다.
- [x] `콘텐츠 > 전체 > 시리즈`: 요일과 정렬을 변경한 뒤 새로고침해도 타입/요일/정렬이 유지되고 목록이 첫 페이지 결과로 교체된다.
- [x] 콘텐츠의 빈/오류 상태: 상태 화면에서도 아래로 당겨 재시도할 수 있다.
### 대화 탭
- [x] `대화 > 전체`: 아래로 당기면 `ALL` 첫 페이지가 다시 조회되고 선택 필터는 유지된다.
- [x] `대화 > AI`: 아래로 당기면 `AI` 첫 페이지가 다시 조회되고 선택 필터는 유지된다.
- [x] `대화 > DM`: 아래로 당기면 `DM` 첫 페이지가 다시 조회되고 선택 필터는 유지된다.
- [x] 대화 빈/오류 상태: 상태 화면에서도 아래로 당겨 재시도할 수 있다.
- [x] 대화 목록 새로고침 후 하단까지 스크롤하면 다음 페이지가 중복 없이 이어서 추가된다.
### 공통 회귀
- [x] 새로고침 중 다른 내부 탭/필터로 강제 전환되지 않는다.
- [x] 새로고침 실패 시 기존 토스트 또는 오류/빈 상태가 표시되고 다시 당겨 재시도할 수 있다.
- [x] 하단 `마이` 탭에는 이번 변경으로 새로고침 동작이 추가되지 않았다.
### Phase 4 Lottie 후속 검증
- [ ] 당김 영역 노출 중 반복 재생, 기준 미만 취소 시 숨김·초기화를 확인한다.
- [ ] refresh 시작 후 API 완료까지 Lottie가 유지되고 요청이 한 번만 실행되는지 확인한다.
- [ ] Reduce Motion에서 frame `1` 정지 표시와 accessibility refresh action을 확인한다.
- [ ] 홈·콘텐츠·대화 7개 View의 content·loading·empty/error 상태와 iPhone·iPad 레이아웃을 확인한다.
- [ ] legacy `LiveNowAllView`, `ContentDetailView`가 변경되지 않았는지 확인한다.
## Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|---|---|---|---|---|---|
| 2026-08-18 | `DEC-001` | 확정 | 당겨서 새로고침은 현재 선택된 내부 탭·필터·정렬 상태만 대상으로 한다. | 사용자 A안 선택 | `P1-T1`, `P2-T1`, `P3-T1`, `prd.md` |
| 2026-08-18 | `DEC-002` | 확정 | 빈/오류 상태에서도 당겨서 새로고침을 허용한다. | 사용자 A안 선택 | `P1-GATE`, `P2-GATE`, `P3-GATE`, `prd.md` |
| 2026-08-18 | `DEC-003` | 확정 | 신규 API, dependency, 공통 abstraction 없이 기존 fetch 경로를 연결한다. | 최소 변경 원칙과 기존 ViewModel 구조 | 전체 Goal |
| 2026-08-25 | `DEC-004~010` | 확정 | Lottie asset·재생·크기·Reduce Motion·적용 화면을 PRD 인터뷰 결과대로 적용한다. | 사용자 인터뷰와 asset 확인 | `P4-T1`, `P4-T2`, `P4-GATE`, `prd.md` |
| 2026-08-25 | `DEC-011` | 정정 | `DEC-003`의 공통 abstraction 제외는 Phase 1~3에 유지하고, Phase 4에서는 V2 공용 component 한 개만 허용한다. | 7개 View·20개 상태의 중복 방지 | `P4-T1`, `P4-T2`, `prd.md` |
| 2026-08-26 | `DEC-012` | 확정 | 기존 `VStack` layout을 유지하고 refresh 상태의 `0↔64pt` 높이·offset 전환만 `0.2초 easeOut`으로 보간한다. | 기존 UI 간격 보존과 최소 변경 우선 | `P4-R3`, `P4-GATE` |
## 발견된 문제
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|---|---|---|---|---|---|
| `ISSUE-001` | Low | 해결 | 현재 테스트 번들 타깃이 명확하지 않아 SwiftUI 새로고침 동작을 자동 테스트로 고정하기 어렵다. | 전체 Gate | Debug 빌드와 사용자 수동 검증을 완료 증거로 기록했다. 테스트 타깃이 추가되면 focused test를 후속 보강한다. |
| `ISSUE-002` | High | 해결 | custom `ScrollView`의 bounce·gesture 종료·offset 측정 경로 때문에 Lottie와 refresh가 실행되지 않았다. | `P4-R1`, `P4-GATE` | bounce 강제, preference armed→release 전이, global moving-minus-fixed offset 측정으로 수정하고 사용자 동작 확인을 통과했다. |
| `ISSUE-003` | Medium | 롤백 | frozen motion handoff는 덜컹임을 줄였지만 기존 UI 간격을 변경했다. | `P4-R2` | 사용자 결정에 따라 motion 보정과 후속 layout 수정을 모두 롤백했다. |
## 최종 보고 형식
```markdown
구현 결과: 메인 홈, 콘텐츠, 대화 탭의 Lottie 당겨서 새로고침 구현 완료 여부
- 변경: 기존 데이터 새로고침을 유지한 채 메인 7개 View의 기본 indicator를 공용 Lottie refresh component로 교체
- 결정: DEC-006 영역 노출 중 반복, DEC-007 최대 48×48pt·64pt 영역, DEC-008~009 Reduce Motion frame 1, DEC-010 메인 7개 View 한정
- 검증:
- `xcodebuild` `SodaLive`, `SodaLive-dev` Debug simulator build — 성공/실패와 exit code
- 수동 검증 — Lottie 재생·크기·Reduce Motion·기존 refresh 회귀·legacy 제외 결과
- 남은 항목: 미완료 `P4-*` 또는 없음
- 문서: `docs/20260818_메인_탭_당겨서_새로고침/prd.md`, `docs/20260818_메인_탭_당겨서_새로고침/plan-task.md`
```