From 9297cd28f91769be8508b400e47f6e902ffa8c73 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Tue, 18 Aug 2026 13:29:36 +0900 Subject: [PATCH] =?UTF-8?q?docs(main):=20=EB=A9=94=EC=9D=B8=20=ED=83=AD=20?= =?UTF-8?q?=EC=83=88=EB=A1=9C=EA=B3=A0=EC=B9=A8=20=EA=B3=84=ED=9A=8D?= =?UTF-8?q?=EC=9D=84=20=EA=B8=B0=EB=A1=9D=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan-task.md | 421 ++++++++++++++++++ docs/20260818_메인_탭_당겨서_새로고침/prd.md | 253 +++++++++++ 2 files changed, 674 insertions(+) create mode 100644 docs/20260818_메인_탭_당겨서_새로고침/plan-task.md create mode 100644 docs/20260818_메인_탭_당겨서_새로고침/prd.md diff --git a/docs/20260818_메인_탭_당겨서_새로고침/plan-task.md b/docs/20260818_메인_탭_당겨서_새로고침/plan-task.md new file mode 100644 index 00000000..bec50ada --- /dev/null +++ b/docs/20260818_메인_탭_당겨서_새로고침/plan-task.md @@ -0,0 +1,421 @@ +# 메인 탭 당겨서 새로고침 구현 계획 + +| 문서 항목 | 내용 | +|---|---| +| 상태 | 구현 완료 | +| 작성일 | `2026-08-18` | +| 요구사항 기준 | `docs/20260818_메인_탭_당겨서_새로고침/prd.md` | +| API 기준 | 기존 홈/콘텐츠/대화 API 계약 재사용, 신규 API 없음 | +| 현재 Phase | Phase 1~3 완료 | +| 현재 활성 Goal | 없음 | + +## 목표 + +메인 `홈`, `콘텐츠`, `대화` 탭에서 현재 선택된 내부 탭·필터 상태를 유지한 채 사용자가 당겨서 최신 데이터를 다시 조회할 수 있게 한다. + +## 현재 상태 + +| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 | +|---:|---|---:|---|---| +| 1 | 완료 | `1/1` | 없음 | 없음 | +| 2 | 완료 | `1/1` | 없음 | 없음 | +| 3 | 완료 | `1/1` | 없음 | 없음 | + +- 동시에 하나의 미완료 goal만 운용한다. +- 완료된 Task와 검증 기록은 되돌리거나 삭제하지 않는다. 후속 수정은 회귀 수정 Task와 새 goal ID를 추가한다. +- 구현 체크박스는 완료 상태를 반영하고, 사용자 수동 검증 체크박스는 실제 확인 전까지 미완료로 둔다. + +## 범위 + +### 포함 + +- 메인 홈 내부 `추천`, `랭킹`, `팔로잉` 현재 선택 상태 새로고침. +- 메인 콘텐츠 내부 `추천`, `랭킹`, `전체` 현재 선택 상태 새로고침. +- 콘텐츠 `랭킹`의 현재 `AudioRankingType`, 콘텐츠 `전체`의 현재 `MainContentAllType`, `ContentSort`, `SeriesPublishedDaysOfWeek` 유지. +- 대화 탭의 현재 `MainChatFilter` 유지와 첫 페이지 재조회. +- loading, empty, error, content 상태에서 모두 새로고침 가능하도록 상태 화면 구조 점검. + +### 제외 + +- `마이` 탭 새로고침. +- 하단 탭 전체 일괄 새로고침. +- 신규 API endpoint, 신규 dependency, 신규 공통 refresh abstraction. +- 자동 갱신, polling, push 기반 갱신. + +## 기술적 제약 + +- 기술 스택: Swift, SwiftUI, Combine, Moya, CocoaPods 기반 iOS 앱. +- 아키텍처: 기존 `View -> ViewModel -> Repository -> Api(TargetType)` 흐름을 유지한다. +- UI: SwiftUI `.refreshable` 또는 동등한 iOS 기본 refresh affordance를 우선 사용한다. +- 데이터·보안: 기존 인증 header, token 저장, 언어 header 흐름을 변경하지 않는다. +- 의존성: 신규 dependency를 추가하지 않는다. +- 계약: 제공되지 않은 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] 새로고침 후 스크롤 하단에서 다음 페이지 조회가 이어지는지 확인한다. + +## 실행 순서와 의존성 + +| 순서 | 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 생성 | + +```text +P1-T1 -> P1-GATE -> P2-T1 -> P2-GATE -> P3-T1 -> P3-GATE +``` + +## 변경 금지 항목 + +- 확정된 요구사항을 근거 없이 변경하지 않는다. +- 기존 완료 체크박스와 Progress·Decision Log·검증 기록을 삭제하거나 덮어쓰지 않는다. +- 신규 endpoint, DTO, dependency, 공통 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 수용 기준 — 충족 확인. +- 남은 항목: 없음. +- 다음 행동: 없음. + +## 사용자 수동 테스트 목록 + +사전 조건: + +- Debug 앱을 실제 기기 또는 iOS Simulator에서 실행한다. +- 로그인 후 메인 화면에 진입한다. +- 각 테스트에서 화면 상단 목록을 아래로 충분히 당겨 iOS 기본 새로고침 indicator가 보이는지 확인한다. + +### 홈 탭 + +- [x] `홈 > 추천`: 아래로 당기면 indicator가 API 응답까지 유지되고 최신 데이터가 표시된다. 선택 탭은 `추천`으로 유지된다. +- [x] `홈 > 랭킹`: 아래로 당기면 랭킹이 다시 조회되고 선택 탭은 `랭킹`으로 유지된다. +- [x] `홈 > 팔로잉`: 아래로 당기면 팔로잉 데이터가 다시 조회되고 선택 탭은 `팔로잉`으로 유지된다. +- [x] `홈 > 팔로잉` 로그인 필요/빈/오류 상태: 상태 화면에서도 아래로 당겨 재시도할 수 있다. + +### 콘텐츠 탭 + +- [x] `콘텐츠 > 추천`: 아래로 당기면 추천 데이터가 다시 조회되고 선택 탭은 `추천`으로 유지된다. +- [x] `콘텐츠 > 랭킹`: 주간/월간 등 기본값이 아닌 랭킹 타입을 선택한 뒤 새로고침해도 같은 타입이 유지된다. +- [x] `콘텐츠 > 전체 > 오디오`: 타입과 정렬을 변경한 뒤 새로고침해도 선택한 타입/정렬이 유지되고 목록이 첫 페이지 결과로 교체된다. +- [x] `콘텐츠 > 전체 > 시리즈`: 요일과 정렬을 변경한 뒤 새로고침해도 타입/요일/정렬이 유지되고 목록이 첫 페이지 결과로 교체된다. +- [x] 콘텐츠의 빈/오류 상태: 상태 화면에서도 아래로 당겨 재시도할 수 있다. + +### 대화 탭 + +- [x] `대화 > 전체`: 아래로 당기면 `ALL` 첫 페이지가 다시 조회되고 선택 필터는 유지된다. +- [x] `대화 > AI`: 아래로 당기면 `AI` 첫 페이지가 다시 조회되고 선택 필터는 유지된다. +- [x] `대화 > DM`: 아래로 당기면 `DM` 첫 페이지가 다시 조회되고 선택 필터는 유지된다. +- [x] 대화 빈/오류 상태: 상태 화면에서도 아래로 당겨 재시도할 수 있다. +- [x] 대화 목록 새로고침 후 하단까지 스크롤하면 다음 페이지가 중복 없이 이어서 추가된다. + +### 공통 회귀 + +- [x] 새로고침 중 다른 내부 탭/필터로 강제 전환되지 않는다. +- [x] 새로고침 실패 시 기존 토스트 또는 오류/빈 상태가 표시되고 다시 당겨 재시도할 수 있다. +- [x] 하단 `마이` 탭에는 이번 변경으로 새로고침 동작이 추가되지 않았다. + +## 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 | + +## 발견된 문제 + +| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 | +|---|---|---|---|---|---| +| `ISSUE-001` | Low | 해결 | 현재 테스트 번들 타깃이 명확하지 않아 SwiftUI 새로고침 동작을 자동 테스트로 고정하기 어렵다. | 전체 Gate | Debug 빌드와 사용자 수동 검증을 완료 증거로 기록했다. 테스트 타깃이 추가되면 focused test를 후속 보강한다. | + +## 최종 보고 형식 + +```markdown +구현 결과: 메인 홈, 콘텐츠, 대화 탭의 당겨서 새로고침 구현 완료 여부 + +- 변경: 홈/콘텐츠/대화 각 현재 선택 상태의 첫 페이지 또는 단일 조회 새로고침 연결 +- 결정: DEC-001 현재 선택 상태만 새로고침, DEC-002 빈/오류 상태 새로고침 허용, DEC-003 신규 API/dependency/공통 abstraction 제외 +- 검증: + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` — 성공/실패와 exit code + - 수동 검증 — 홈/콘텐츠/대화 내부 탭·필터별 새로고침 성공/실패와 불가 사유 +- 남은 항목: 구현자가 확인한 외부 의존 또는 후속 범위, 없으면 없음 +- 문서: `docs/20260818_메인_탭_당겨서_새로고침/prd.md`, `docs/20260818_메인_탭_당겨서_새로고침/plan-task.md` +``` diff --git a/docs/20260818_메인_탭_당겨서_새로고침/prd.md b/docs/20260818_메인_탭_당겨서_새로고침/prd.md new file mode 100644 index 00000000..d35f3bbb --- /dev/null +++ b/docs/20260818_메인_탭_당겨서_새로고침/prd.md @@ -0,0 +1,253 @@ +# PRD: 메인 탭 당겨서 새로고침 + +## 문서 정보 + +| 항목 | 내용 | +|---|---| +| 문서 상태 | 구현 완료 | +| 작성일 | `2026-08-18` | +| 최종 수정일 | `2026-08-18` | +| 대상 제품 | 메인 홈, 콘텐츠, 대화 탭 당겨서 새로고침 | +| 작성자·결정권자 | 사용자, Sisyphus | +| 관련 API Contract | 기존 각 탭 API 계약 재사용, 신규 endpoint 없음 | +| 관련 구현 계획 | `docs/20260818_메인_탭_당겨서_새로고침/plan-task.md` | +| 관련 review | 없음 | + +### 요구사항 상태 + +| 상태 | 의미 | 구현 처리 | +|---|---|---| +| 확정 | 제품·기술 결정이 완료되어 구현 기준으로 사용 | `plan-task.md`의 Task와 완료 증거로 추적 | +| 미결 | 제품·UX·운영 결정이 더 필요함 | 권고안과 결정 주체를 기록하고 임의 구현 금지 | +| 제외 | 현재 릴리스에서 구현하지 않기로 결정 | 제외 이유와 후속 조건을 Decision Log에 기록 | + +## 1. Overview + +메인 하단 `홈`, `콘텐츠`, `대화` 탭에 iOS 기본 당겨서 새로고침 동작을 추가한다. 사용자가 현재 보고 있는 내부 탭과 필터 상태를 유지한 채 해당 화면의 첫 페이지 또는 단일 조회 API를 다시 호출해 최신 데이터를 표시한다. + +구현 결과와 사용자 수동 검증 대기 상태는 관련 구현 계획에서 추적한다. + +## 2. Problem Statement + +현재 V2 메인 화면의 주요 조회 탭은 최초 진입 또는 내부 필터 변경 시에만 데이터를 불러온다. 사용자가 이미 보고 있는 화면에서 최신 상태를 직접 확인하려면 탭을 이동하거나 앱 흐름을 다시 진입해야 한다. + +문제를 해결했다는 판단은 `홈`, `콘텐츠`, `대화` 탭에서 현재 선택 상태 그대로 당겨서 새로고침을 실행하고, 성공·빈 목록·오류 상태에서도 재시도 가능한 것으로 한다. + +## 3. Goals + +### 3.1 제품 목표 + +- `홈`, `콘텐츠`, `대화` 하단 탭에서 사용자가 수동으로 최신 데이터를 다시 조회할 수 있다. +- 새로고침은 현재 선택된 내부 탭·필터·정렬 상태만 대상으로 한다. +- 새로고침 실패 시 기존 오류 노출 패턴을 유지하고, 사용자는 같은 화면에서 다시 당겨서 재시도할 수 있다. + +### 3.2 UX 목표 + +- iOS 기본 pull-to-refresh 제스처와 표시 방식을 사용한다. +- loading, empty, error, content 상태 어디서든 당겨서 새로고침을 시도할 수 있다. +- 새로고침은 현재 선택된 탭을 바꾸지 않고 스크롤 가능한 화면 문맥 안에서 동작한다. + +## 4. Non-Goals + +- `마이` 탭에는 당겨서 새로고침을 추가하지 않는다. +- 하단 탭 전체 데이터를 한 번에 새로고침하지 않는다. +- background 자동 갱신, 주기적 polling, push 기반 갱신은 추가하지 않는다. +- 신규 API endpoint, DTO, Repository를 만들지 않는다. +- 새 공통 추상화나 refresh protocol을 만들지 않는다. 기존 `ViewModel` fetch 경로를 화면별로 연결한다. + +## 5. Target Users and Permissions + +### 5.1 사용자 + +| 사용자 | 목표 | 주요 작업 | 사용 환경 | +|---|---|---|---| +| 로그인 사용자 | 메인 화면의 최신 홈·콘텐츠·대화 데이터를 직접 갱신 | 당겨서 새로고침 | iOS 앱 | +| 비로그인 또는 로그인 필요 상태 사용자 | 실패/제한 상태에서 재시도 | 당겨서 새로고침 | iOS 앱 | + +### 5.2 권한 + +- 인증 주체: 기존 앱 인증 상태와 각 API의 현재 인증 정책을 그대로 사용한다. +- 허용 역할: 기존 각 탭 접근 정책을 따른다. +- 거부 조건: 기존 API 실패·로그인 필요 상태를 그대로 표시한다. +- 리소스 소유권: 기존 API와 화면 모델의 소유권 검증을 변경하지 않는다. + +## 6. 핵심 사용자 흐름 + +1. 사용자가 메인 하단 `홈`, `콘텐츠`, `대화` 중 하나에 진입한다. +2. 사용자는 내부 탭 또는 필터를 선택해 특정 상태를 보고 있다. +3. 사용자가 화면을 아래로 당긴다. +4. 앱은 현재 선택 상태를 유지하고 해당 상태의 첫 페이지 또는 단일 조회 API를 다시 호출한다. +5. 성공하면 최신 데이터로 교체하고, 실패하면 기존 오류 메시지/토스트 정책을 유지한다. +6. 빈 목록이나 오류 상태에서도 사용자는 다시 당겨서 새로고침할 수 있다. + +## 7. 정보 구조와 라우팅 + +```text +MainView + MainTab.home + MainHomeTab.recommendation + MainHomeTab.ranking + MainHomeTab.following + MainTab.content + MainContentTab.recommendation + MainContentTab.ranking + AudioRankingType + MainContentTab.all + MainContentAllType + ContentSort + SeriesPublishedDaysOfWeek + MainTab.chat + MainChatFilter.all + MainChatFilter.ai + MainChatFilter.dm +``` + +- 새로고침은 라우팅을 변경하지 않는다. +- 현재 하단 탭과 내부 선택 상태는 유지한다. +- 목록 아이템 상세 진입, 검색, 충전, 보관함 라우팅은 변경하지 않는다. + +## 8. 기능 요구사항 + +### 8.1 공통 새로고침 동작 + +| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 | +|---|---|---|---|---| +| `REFRESH-001` | 확정 | `홈`, `콘텐츠`, `대화` 하단 탭에 당겨서 새로고침을 제공한다. | 각 탭에서 아래로 당기면 현재 화면의 조회 API가 다시 호출된다. | `P1-T1`, `P2-T1`, `P3-T1` | +| `REFRESH-002` | 확정 | 현재 선택된 내부 탭·필터·정렬·요일 상태만 새로고침한다. | 선택 상태가 변경되지 않고 해당 조건의 데이터만 첫 페이지부터 다시 조회된다. | `P1-T1`, `P2-T1`, `P3-T1` | +| `REFRESH-003` | 확정 | loading, empty, error, content 상태 모두에서 새로고침을 시도할 수 있다. | 빈/오류 화면에서도 pull-to-refresh 제스처 또는 동등한 재시도 흐름이 가능하다. | `P1-GATE`, `P2-GATE`, `P3-GATE` | +| `REFRESH-004` | 확정 | 새로고침은 기존 loading guard와 요청 경합 방지 정책을 깨지 않는다. | 중복 요청이 화면 상태를 역전시키지 않고, 기존 최신 요청 우선 패턴을 유지한다. | `P2-T1`, `P3-T1` | + +### 8.2 홈 탭 + +| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 | +|---|---|---|---|---| +| `HOME-REFRESH-001` | 확정 | 홈 `추천` 선택 시 `MainHomeRecommendationViewModel.fetchRecommendations()` 경로로 다시 조회한다. | 기존 추천 섹션 데이터가 최신 응답으로 교체된다. | `P1-T1` | +| `HOME-REFRESH-002` | 확정 | 홈 `랭킹` 선택 시 `MainHomeRankingViewModel.fetchRankings()` 경로로 다시 조회한다. | 기존 랭킹 데이터와 rank change 표시가 최신 응답으로 교체된다. | `P1-T1` | +| `HOME-REFRESH-003` | 확정 | 홈 `팔로잉` 선택 시 `MainHomeFollowingViewModel.fetchFollowing()` 경로로 다시 조회한다. | 로그인 필요, 빈 상태, 콘텐츠 상태가 최신 응답 기준으로 갱신된다. | `P1-T1` | + +### 8.3 콘텐츠 탭 + +| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 | +|---|---|---|---|---| +| `CONTENT-REFRESH-001` | 확정 | 콘텐츠 `추천` 선택 시 `MainContentRecommendationViewModel.fetchRecommendations()` 경로로 다시 조회한다. | 추천 콘텐츠 섹션 데이터가 최신 응답으로 교체된다. | `P2-T1` | +| `CONTENT-REFRESH-002` | 확정 | 콘텐츠 `랭킹` 선택 시 현재 `AudioRankingType`을 유지하고 `MainContentRankingViewModel.fetchRankings()` 경로로 다시 조회한다. | 선택된 랭킹 타입이 유지되고 목록이 최신 응답으로 교체된다. | `P2-T1` | +| `CONTENT-REFRESH-003` | 확정 | 콘텐츠 `전체` 선택 시 현재 `MainContentAllType`, `ContentSort`, `SeriesPublishedDaysOfWeek`를 유지하고 첫 페이지를 다시 조회한다. | 기존 목록이 첫 페이지 결과로 교체되고 다음 페이지 상태가 초기화된다. | `P2-T1` | + +### 8.4 대화 탭 + +| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 | +|---|---|---|---|---| +| `CHAT-REFRESH-001` | 확정 | 대화 탭에서 현재 `MainChatFilter`를 유지하고 첫 페이지를 다시 조회한다. | `ALL`, `AI`, `DM` 중 선택된 필터가 유지되고 `rooms`가 첫 페이지 결과로 교체된다. | `P3-T1` | +| `CHAT-REFRESH-002` | 확정 | 새로고침 후 pagination 상태를 첫 페이지 기준으로 초기화한다. | `hasMore`, `nextCursor`가 최신 첫 페이지 응답 기준으로 갱신된다. | `P3-T1` | + +## 9. 반응형 기능 범위 + +| 기능 | iPhone | iPad | 비고 | +|---|---:|---:|---| +| 당겨서 새로고침 | 전체 | 전체 | SwiftUI 기본 동작 기준 | +| 빈/오류 상태 재시도 | 전체 | 전체 | 상태 화면도 스크롤 가능한 컨테이너 안에서 표시 | + +## 10. UI/UX Expectations + +### 10.1 디자인과 component 원칙 + +- SwiftUI `.refreshable` 또는 동등한 iOS 기본 refresh affordance를 사용한다. +- 별도 커스텀 spinner, 신규 refresh button, 신규 공통 컴포넌트는 만들지 않는다. +- 기존 `Color.black`, `ProgressView`, `sodaToast`, empty state 스타일을 유지한다. + +### 10.2 화면 상태 + +- 첫 로딩 상태와 수동 새로고침 상태를 구분하되, 기존 화면 레이아웃을 크게 바꾸지 않는다. +- 빈/오류 상태에서도 새로고침 가능해야 하므로 상태 화면을 refresh 가능한 스크롤 컨테이너로 감싸는 방식을 계획한다. +- 새로고침 실패 시 현재 탭 선택 상태는 유지한다. + +### 10.3 접근성 + +- 기본 iOS refresh control 접근성 동작을 해치지 않는다. +- 기존 버튼, 탭, 리스트 아이템의 focus/touch target을 변경하지 않는다. + +## 11. API 계약 + +### 11.1 공통 규칙 + +- 신규 endpoint 없음. +- 기존 Repository와 API 계약을 그대로 사용한다. +- 콘텐츠 `전체`와 대화 탭은 첫 페이지 재조회 시 기존 pagination cursor/page 상태를 초기화한다. + +### 11.2 Endpoint 추적 + +| 요구사항 | Method | Path | 계약 상태 | API Contract | 소유 Goal | +|---|---|---|---|---|---| +| `HOME-REFRESH-001` | GET | 기존 홈 추천 API | 제공됨 | 기존 `MainHomeRecommendationRepository` | `P1-T1` | +| `HOME-REFRESH-002` | GET | 기존 홈 랭킹 API | 제공됨 | 기존 `MainHomeRankingRepository` | `P1-T1` | +| `HOME-REFRESH-003` | GET | 기존 홈 팔로잉 API | 제공됨 | 기존 `MainHomeFollowingRepository` | `P1-T1` | +| `CONTENT-REFRESH-001` | GET | 기존 콘텐츠 추천 API | 제공됨 | 기존 `MainContentRecommendationRepository` | `P2-T1` | +| `CONTENT-REFRESH-002` | GET | 기존 콘텐츠 랭킹 API | 제공됨 | 기존 `MainContentRankingRepository` | `P2-T1` | +| `CONTENT-REFRESH-003` | GET | 기존 콘텐츠 전체 API | 제공됨 | 기존 `MainContentAllRepository` | `P2-T1` | +| `CHAT-REFRESH-001` | GET | `/api/v2/chat/rooms` | 제공됨 | `docs/20260708_홈_채팅_탭/prd.md` | `P3-T1` | + +## 12. 보안과 데이터 취급 + +- 인증 header, token 저장, 언어 header 흐름은 변경하지 않는다. +- 새로고침 실패 로그에 token, 개인 메시지 본문, signed URL 등 민감정보를 기록하지 않는다. +- 기존 `AuthPlugin`, `UserDefaultsKey.token` 처리 로직은 수정 대상이 아니다. + +## 13. 성능과 품질 요구사항 + +- 이미 로딩 중인 동일 화면에서 중복 새로고침이 과도하게 발생하지 않도록 기존 loading guard를 유지하거나 같은 수준의 guard를 둔다. +- 콘텐츠 `전체`와 대화 탭은 첫 페이지 새로고침 시 기존 다음 페이지 로딩 상태를 초기화한다. +- 새 공통 abstraction을 만들지 않고 화면별 최소 변경으로 구현한다. +- 현재 테스트 번들 타깃이 명확하지 않으므로 구현 검증은 focused 코드 리뷰, `xcodebuild` 빌드, 수동 QA를 필수로 한다. + +## 14. 성공 기준 + +### 14.1 기능 수용 기준 + +- [x] 홈 `추천`, `랭킹`, `팔로잉`에서 각각 현재 선택된 내부 탭만 새로고침된다. (`HOME-REFRESH-001~003`) +- [x] 콘텐츠 `추천`, `랭킹`, `전체`에서 각각 현재 선택된 내부 탭과 필터 상태만 새로고침된다. (`CONTENT-REFRESH-001~003`) +- [x] 대화 `전체`, `AI`, `DM`에서 현재 필터만 유지해 첫 페이지를 새로고침한다. (`CHAT-REFRESH-001~002`) +- [x] 빈/오류 상태에서도 사용자가 당겨서 재시도할 수 있다. (`REFRESH-003`) + +### 14.2 UI/UX 수용 기준 + +- [x] iOS 기본 refresh affordance가 보인다. +- [x] 새로고침 후 선택된 하단 탭과 내부 탭/필터가 변경되지 않는다. +- [x] 새로고침 실패 시 기존 토스트/오류 상태 정책을 유지한다. + +### 14.3 추적성 완료 기준 + +- [x] 모든 `확정` 요구사항이 `plan-task.md`의 Task/Goal 완료 증거로 연결된다. +- [x] 신규 API 또는 신규 dependency가 없다는 결정이 Decision Log에 남아 있다. +- [x] 코드 구현 전 이 문서와 계획 문서가 먼저 작성되어 있다. + +## 15. Open Questions + +| ID | 상태 | 결정 필요 사항 | 현재 권고 | 결정 주체 | 결정 기한/시점 | 영향 Goal | +|---|---|---|---|---|---|---| +| 없음 | 확정 | 남은 제품 결정 없음 | 없음 | 사용자 | 2026-08-18 | 전체 | + +## 16. 요구사항 추적표 + +| 요구사항 범위 | API Contract | 계획 Phase | Goal | 자동 검증 | 수동 검증 | +|---|---|---:|---|---|---| +| `REFRESH-001~004` | 기존 API 재사용 | 1~3 | `P1-T1`, `P2-T1`, `P3-T1` | 빌드 | 홈/콘텐츠/대화 새로고침 | +| `HOME-REFRESH-001~003` | 홈 기존 Repository | 1 | `P1-T1`, `P1-GATE` | 빌드 | 홈 내부 탭별 새로고침 | +| `CONTENT-REFRESH-001~003` | 콘텐츠 기존 Repository | 2 | `P2-T1`, `P2-GATE` | 빌드 | 콘텐츠 내부 탭/필터별 새로고침 | +| `CHAT-REFRESH-001~002` | `/api/v2/chat/rooms` | 3 | `P3-T1`, `P3-GATE` | 빌드 | 대화 필터별 새로고침 | + +## 17. Decision Log + +| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계약·Goal | +|---|---|---|---|---|---| +| 2026-08-18 | `DEC-001` | 확정 | 당겨서 새로고침은 현재 선택된 내부 탭·필터·정렬 상태만 대상으로 한다. | 사용자 인터뷰 A안 선택 | `REFRESH-002`, 전체 Goal | +| 2026-08-18 | `DEC-002` | 확정 | 빈/오류 상태에서도 당겨서 새로고침을 허용한다. | 사용자 인터뷰 A안 선택 | `REFRESH-003`, 전체 Gate | +| 2026-08-18 | `DEC-003` | 확정 | 신규 API, 신규 dependency, 신규 공통 refresh abstraction은 만들지 않는다. | 기존 화면별 fetch 경로 존재, 최소 변경 원칙 | `REFRESH-004`, 전체 Goal | + +## 18. 변경 관리 + +요구사항 변경 시 다음을 확인한다. + +- [x] Decision Log에 변경 이유와 날짜를 기록했다. +- [x] 관련 요구사항 상태·본문·수용 기준을 갱신했다. +- [x] `plan-task.md`의 범위·Files·체크박스·완료 증거를 코드 변경 전에 갱신했다. +- [x] 기존 Progress·review·검증 기록을 삭제하거나 덮어쓰지 않았다.