# 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·검증 기록을 삭제하거나 덮어쓰지 않았다.