29 KiB
메인 콘텐츠 탭 내부 랭킹 탭 구현 계획
기준 문서
- PRD:
docs/20260706_메인_콘텐츠_내부_랭킹_탭/prd.md - 사용자 요청: 2026-07-06 메인 콘텐츠 탭 내부 랭킹 탭 문서 작성 요청
- 문서 정책:
docs/agent-guides/documentation-policy.md - 코드 스타일:
docs/agent-guides/code-style.md - 빌드/검증:
docs/agent-guides/build-test-verification.md - 참고 구현 문서:
docs/20260630_메인_홈_랭킹_탭/plan-task.md - 참고 구현 문서:
docs/20260705_메인_콘텐츠_내부_추천_탭/plan-task.md - 참고 구현 문서:
docs/20260706_메인_콘텐츠_내부_전체_탭/plan-task.md
구현할 UI의 Figma URL
- 전체:
https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=24-6857&m=dev - 1위:
https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=24-6861&m=dev - 2~10위:
https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=24-6867&m=dev - 11~20위:
https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=24-6872&m=dev
요구사항 요약
- 메인 하단
콘텐츠탭 안의 내부랭킹탭을 구현한다. - API endpoint는
GET /api/v2/audio/rankings를 사용한다. - response는
AudioRankingResponse와AudioRankingItemResponse를 SwiftDecodable모델로 대응한다. AudioRankingType은 서버 enum 값WEEKLY_POPULAR,RISING,REVENUE,SALES_COUNT,COMMENT_COUNT,LIKE_COUNT를 그대로 사용한다.- 서버가 내려준
items[].rank를 화면 순위로 사용하고 클라이언트에서 index 기반 순위를 재계산하지 않는다. - item은 최대 20개로 취급한다.
- 1위 UI는 Figma 기준 별도 형태로 구현한다.
- 2
10위 UI는 썸네일 카드 형태로 표시하고, 27위는 2열, 8~10위는 3열로 구성한다. - 11~20위 UI는 row 형태로 표시하고, 텍스트 영역은 위에 콘텐츠 타이틀, 아래에 크리에이터 닉네임을 표시한다.
showRankChange == false이면 순위 변화 표시는 숨긴다.isNew == true이면 신규 진입 표시를 우선한다.- 루트 layout과 반복 item은 고정 숫자 width/height로 제한하지 않는다.
- 외부 라이브러리는 추가하지 않는다.
접근안 검토
- 선택안:
SodaLive/Sources/V2/Main/Content/Ranking/**하위에 API, 모델, ViewModel, 화면, 랭킹 전용 컴포넌트를 둔다.- 장점: 기존 콘텐츠
추천/전체탭 구조와 동일하게 내부 탭별 책임을 분리할 수 있다. - 장점: 메인 홈 랭킹 탭의 크리에이터 전용 모델과 결합하지 않고 오디오 랭킹 응답 필드를 그대로 표현할 수 있다.
- 단점: 홈 랭킹 row/card와 유사한 UI 코드가 일부 생길 수 있다.
- 장점: 기존 콘텐츠
- 대안 1:
SodaLive/Sources/V2/Main/Home/Ranking/Components/**컴포넌트를 직접 재사용한다.- 장점: 순위 숫자와 rank change badge 스타일을 빠르게 맞출 수 있다.
- 단점: 기존 컴포넌트가
MainHomeCreatorRankingItem,creatorId,nickname,profileImageUrl에 묶여 있어 오디오 타이틀/크리에이터 닉네임 표시 요구와 맞지 않는다.
- 대안 2: 홈 랭킹 컴포넌트를 공용
V2/Component/Ranking/**으로 먼저 추출한다.- 장점: 순위 변화 badge와 row 기초 레이아웃을 장기적으로 공유할 수 있다.
- 단점: 기존 홈 랭킹 구현까지 수정하므로 이번 랭킹 탭 범위보다 변경 범위가 커진다.
선택안은 신규 오디오 랭킹 탭 전용 구현을 기본으로 한다. 단, 1120위 전용 row는 새 파일로 만들더라도 10위는 별도 썸네일 카드 컴포넌트를 하나 만들고 2열/3열 크기 차이만 조정한다. 순위 변화 badge는 UI/조건이 동일하면 작은 공용 컴포넌트로 추출할 수 있는지 구현 직전에 다시 판단한다.MainHomeRankingRow의 레이아웃 구조를 거의 그대로 복사하고, 모델/탭 callback/텍스트 영역만 오디오 랭킹에 맞게 치환하는 방식을 우선한다. 2
API 응답 모델 기준
data class AudioRankingResponse(
val showRankChange: Boolean,
val type: AudioRankingType,
val items: List<AudioRankingItemResponse>
)
enum class AudioRankingType {
WEEKLY_POPULAR,
RISING,
REVENUE,
SALES_COUNT,
COMMENT_COUNT,
LIKE_COUNT
}
data class AudioRankingItemResponse(
val contentId: Long,
val title: String,
val creatorNickname: String,
val rank: Int,
val rankChange: Int?,
@JsonProperty("isNew")
val isNew: Boolean,
val coverImageUrl: String?
)
Swift 모델은 기존 V2 관례에 맞춰 Decodable 구조체로 선언한다. Long 대응 id는 프로젝트 기존 모델 관례에 맞춰 Swift Int를 우선 사용하고, AudioRankingItemResponse 대응 모델은 Identifiable을 채택해 id == contentId로 둔다.
구현 원칙
- 신규 코드는
SodaLive/Sources/V2/Main/Content/Ranking/**아래에 둔다. MainContentView의.rankingplaceholder만 신규MainContentRankingView로 교체한다.MainContentView는 기존 title bar와 내부추천/랭킹/전체tab bar 고정 구조를 유지한다.- 랭킹 탭 본문만 자체
ScrollView로 구성한다. - API 실패, 디코딩 실패, 빈 응답은 랭킹 탭 내부 empty/error 상태로 처리한다.
- 오디오 item 탭은 기존
MainContentView.onTapContentcallback을 사용해MainView.handleRecommendationContentTap흐름으로 연결한다. coverImageUrl이 없거나 비어 있으면 기존 V2 이미지 fallback 관례처럼DownsampledKFImage의 gray background가 보이게 한다.- 1위는 별도
MainContentAudioRankingTopCard로 만들고, Figma24:6861형태를 따른다. - 2
10위는7위는 2열, 8~10위는 3열로 parent width 기반 크기만 조정한다.MainContentAudioRankingThumbnailCard하나를 사용하고, 2 - 11~20위는
MainContentAudioRankingRow로 처리하고, 데이터 모델과 텍스트 영역을title/creatorNickname2줄 구조로 구성한다. - 루트 container에는 고정 숫자
frame(width:),frame(height:)를 사용하지 않는다. - 필요한 숫자 치수는 아이콘, 내부 padding, 최소 글자 크기처럼 컴포넌트 내부 요소에만 제한적으로 사용한다.
- row/card 전체 크기는 parent width,
GeometryReader,aspectRatio,GridItem(.flexible()), padding 기반으로 정한다.
재사용 가능한 V2 위젯 후보
SodaLive/Sources/V2/Main/Content/MainContentView.swift- 내부
rankingplaceholder 교체 지점. title bar와 내부TextTabBar고정 shell은 그대로 재사용한다.
- 내부
SodaLive/Sources/V2/Component/TextTabBar.swift- 내부
추천/랭킹/전체tab bar로 이미 사용 중이다.
- 내부
SodaLive/Sources/V2/Component/HomeTitleBar.swift- 콘텐츠 탭 title bar로 이미 사용 중이다.
SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankChangeBadge.swift- 순위 변화 표시 조건과 asset 사용 방식의 가장 직접적인 후보. 현재 홈 랭킹 전용 경로에 있으므로 직접 재사용보다 공용 추출 또는 콘텐츠 랭킹 전용 badge 작성 중 선택한다.
SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankingRow.swift- 11~20위 row 크리에이터 순위 layout의 핵심 복사 후보. 모델과 텍스트 구조가 달라 직접 재사용은 어렵지만, 오디오 콘텐츠 순위 전용 row를 만들 때 순위 컬럼, 이미지, padding,
aspectRatio, 터치 영역 구조는 거의 그대로 가져오는 것을 우선한다.
- 11~20위 row 크리에이터 순위 layout의 핵심 복사 후보. 모델과 텍스트 구조가 달라 직접 재사용은 어렵지만, 오디오 콘텐츠 순위 전용 row를 만들 때 순위 컬럼, 이미지, padding,
SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankingCard.swift- 순위 숫자와 badge overlay 처리 참고 후보. 1위는 Figma가 완전히 다르므로 구조 참고만 한다.
SodaLive/Sources/V2/Component/AudioContent/AudioContentThumbnailCard.swift- 오디오 썸네일 이미지, 제목, 크리에이터 닉네임 표시 후보. 기존 카드가 명시적
width를 받으므로 이번 랭킹 row/top card에 직접 사용하기보다 adapter와 태그 표시 관례를 참고한다.
- 오디오 썸네일 이미지, 제목, 크리에이터 닉네임 표시 후보. 기존 카드가 명시적
SodaLive/Sources/V2/Component/AudioContent/AudioContentListRow.swift- 오디오 title/subtitle 2줄 텍스트 구성 참고 후보. 현재 purchase/compact 스타일과 고정 썸네일 크기가 있어 랭킹 row에는 직접 재사용하지 않는다.
SodaLive/Sources/V2/Component/SectionTitle.swift- 랭킹 타입별 섹션 제목이 필요해질 경우 후보. 현재 제공 API 응답에는 단일
type만 있으므로 필수는 아니다.
- 랭킹 타입별 섹션 제목이 필요해질 경우 후보. 현재 제공 API 응답에는 단일
SodaLive/Sources/V2/Main/Content/All/Models/MainContentAllTabResponse.swift- 콘텐츠 오디오 모델의
contentId대응 방식과creatorNickname필드명 참고 후보.
- 콘텐츠 오디오 모델의
SodaLive/Sources/V2/Main/Content/Recommendation/Repository/MainContentRecommendationApi.swift- 콘텐츠 탭 V2 API 인증 헤더와 Moya
TargetType패턴 참고 후보.
- 콘텐츠 탭 V2 API 인증 헤더와 Moya
SodaLive/Sources/V2/Main/Content/All/Repository/MainContentAllApi.swift- 콘텐츠 탭 V2 API endpoint 구성과 query 없는 GET/있는 GET 비교 참고 후보.
주요 대상 파일
생성
SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingView.swiftSodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingViewModel.swiftSodaLive/Sources/V2/Main/Content/Ranking/Models/AudioRankingResponse.swiftSodaLive/Sources/V2/Main/Content/Ranking/Repository/MainContentRankingApi.swiftSodaLive/Sources/V2/Main/Content/Ranking/Repository/MainContentRankingRepository.swiftSodaLive/Sources/V2/Main/Content/Ranking/Components/MainContentAudioRankingTopCard.swiftSodaLive/Sources/V2/Main/Content/Ranking/Components/MainContentAudioRankingRow.swiftSodaLive/Sources/V2/Main/Content/Ranking/Components/MainContentAudioRankingEmptyStateView.swift- 필요 시
SodaLive/Sources/V2/Main/Content/Ranking/Components/MainContentRankChangeBadge.swift
수정
SodaLive/Sources/V2/Main/Content/MainContentView.swiftSodaLive/Sources/I18n/I18n.swift- 필요 시
SodaLive.xcodeproj/project.pbxproj
확인
SodaLive/Sources/V2/Main/Content/MainContentTab.swiftSodaLive/Sources/V2/Main/Content/Recommendation/Repository/MainContentRecommendationApi.swiftSodaLive/Sources/V2/Main/Content/All/Repository/MainContentAllApi.swiftSodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankChangeBadge.swiftSodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankingRow.swiftSodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankingCard.swiftSodaLive/Sources/V2/Component/AudioContent/AudioContentThumbnailCard.swiftSodaLive/Sources/V2/Component/AudioContent/AudioContentListRow.swiftSodaLive/Sources/V2/Main/MainView.swiftSodaLive/Resources/Assets.xcassets/v2/ic_rank_caret_decrease.imagesetSodaLive/Resources/Assets.xcassets/v2/ic_rank_caret_increase.imagesetSodaLive/Resources/Assets.xcassets/v2/ic_rank_caret_stay.imagesetSodaLive/Resources/Assets.xcassets/v2/ic_rank_new.imageset
TASK 체크리스트
Phase 1: 기준점 점검과 재사용 후보 확정
-
Task 1.1: 콘텐츠 랭킹 placeholder 확인
- 대상 파일:
- 확인:
SodaLive/Sources/V2/Main/Content/MainContentView.swift - 확인:
SodaLive/Sources/V2/Main/Content/MainContentTab.swift
- 확인:
- 작업 내용:
MainContentView의.ranking분기가MainPlaceholderTabView인지 확인한다.MainContentTab.ranking.title이I18n.MainContentRanking.tabTitle을 사용하는지 확인한다.
- 검증 기준:
- 실행 명령:
rg "case \\.ranking|MainPlaceholderTabView\\(title: MainContentTab.ranking.title\\)|MainContentRanking.tabTitle" SodaLive/Sources/V2/Main/Content - 기대 결과: 랭킹 탭은 아직 placeholder이고 내부 tab title은 이미 I18n으로 연결되어 있다.
- 실행 명령:
- 대상 파일:
-
Task 1.2: 순위 변화 asset과 홈 랭킹 복사 후보 컴포넌트 확인
- 대상 파일:
- 확인:
SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankChangeBadge.swift - 확인:
SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankingRow.swift - 확인:
SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankingCard.swift - 확인:
SodaLive/Resources/Assets.xcassets/v2/ic_rank_caret_decrease.imageset - 확인:
SodaLive/Resources/Assets.xcassets/v2/ic_rank_caret_increase.imageset - 확인:
SodaLive/Resources/Assets.xcassets/v2/ic_rank_caret_stay.imageset - 확인:
SodaLive/Resources/Assets.xcassets/v2/ic_rank_new.imageset
- 확인:
- 작업 내용:
- 홈 랭킹의 badge 조건과 asset 이름을 확인한다.
- 11~20위 row가 거의 그대로 복사할 수 있는
MainHomeRankingRow의 순위 컬럼, 이미지, padding,aspectRatio, 터치 영역 구조를 확인한다.
- 검증 기준:
- 실행 명령:
find SodaLive/Resources/Assets.xcassets/v2 -maxdepth 2 -name 'ic_rank_*' | sort && rg "MainHomeRankChangeBadge|MainHomeRankingRow|aspectRatio|ic_rank_new|ic_rank_caret" SodaLive/Sources/V2/Main/Home/Ranking/Components - 기대 결과: 순위 변화 asset과 홈 랭킹 row의 구조 복사 가능 지점이 확인된다.
- 실행 명령:
- 대상 파일:
Phase 2: API와 응답 모델
-
Task 2.1: 오디오 랭킹 응답 모델 생성
- 대상 파일:
- 생성:
SodaLive/Sources/V2/Main/Content/Ranking/Models/AudioRankingResponse.swift
- 생성:
- 작업 내용:
AudioRankingResponse,AudioRankingType,AudioRankingItemResponse를 Swift 모델로 만든다.AudioRankingType은String,Decodable,CaseIterable,Hashable을 채택한다.AudioRankingItemResponse는Identifiable을 채택하고id는contentId를 사용한다.- Kotlin
Long은 기존 V2 관례에 맞춰 SwiftInt로 둔다.
- 검증 기준:
- 실행 명령:
rg "struct AudioRankingResponse|enum AudioRankingType|WEEKLY_POPULAR|RISING|REVENUE|SALES_COUNT|COMMENT_COUNT|LIKE_COUNT|struct AudioRankingItemResponse|contentId|creatorNickname|coverImageUrl|var id" SodaLive/Sources/V2/Main/Content/Ranking/Models/AudioRankingResponse.swift - 기대 결과: 사용자 제공 response data class와 enum 값이 모두 Swift 모델에 반영된다.
- 실행 명령:
- 대상 파일:
-
Task 2.2: 오디오 랭킹 API endpoint와 repository 생성
- 대상 파일:
- 생성:
SodaLive/Sources/V2/Main/Content/Ranking/Repository/MainContentRankingApi.swift - 생성:
SodaLive/Sources/V2/Main/Content/Ranking/Repository/MainContentRankingRepository.swift - 확인:
SodaLive/Sources/V2/Main/Content/Recommendation/Repository/MainContentRecommendationApi.swift
- 생성:
- 작업 내용:
MainContentRankingApi를TargetType으로 만든다.- case는
getAudioRankings하나만 둔다. - path는
/api/v2/audio/rankings, method는.get, task는.requestPlain로 둔다. - headers는 기존 콘텐츠 V2 API와 동일하게
Authorization: Bearer {token}패턴을 사용한다. - repository는
getAudioRankings() -> AnyPublisher<Response, MoyaError>를 제공한다.
- 검증 기준:
- 실행 명령:
rg "/api/v2/audio/rankings|getAudioRankings|Authorization|requestPlain|AnyPublisher<Response, MoyaError>" SodaLive/Sources/V2/Main/Content/Ranking/Repository - 기대 결과: 신규 endpoint와 repository 메서드가 콘텐츠 랭킹 전용 경로에만 존재한다.
- 실행 명령:
- 대상 파일:
Phase 3: ViewModel 상태와 데이터 로딩
- Task 3.1: 랭킹 ViewModel 생성
- 대상 파일:
- 생성:
SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingViewModel.swift - 수정:
SodaLive/Sources/I18n/I18n.swift
- 생성:
- 작업 내용:
MainContentRankingViewModel을ObservableObject로 만든다.isLoading,isShowPopup,errorMessage,ranking,hasLoaded상태를 둔다.fetchRankings()에서ApiResponse<AudioRankingResponse>를 디코딩한다.- 성공 시
ranking.items는 서버 순서를 유지하되 최대 20개만 표시 대상으로 보관한다. - API 실패/디코딩 실패 시
I18n.MainContentRanking.loadFailedMessage를 표시한다. - 빈 배열이면
I18n.MainContentRanking.emptyStateMessage를 표시한다.
- 검증 기준:
- 실행 명령:
rg "final class MainContentRankingViewModel|fetchRankings|ApiResponse<AudioRankingResponse>|prefix\\(20\\)|emptyStateMessage|loadFailedMessage|hasLoaded" SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingViewModel.swift SodaLive/Sources/I18n/I18n.swift - 기대 결과: 최대 20개 처리, 로딩/실패/empty 상태, I18n 문구가 확인된다.
- 실행 명령:
- 대상 파일:
Phase 4: 순위 변화 표시 컴포넌트
- Task 4.1: rank change badge 구현 방식 확정
- 대상 파일:
- 확인:
SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankChangeBadge.swift - 생성 또는 수정:
SodaLive/Sources/V2/Main/Content/Ranking/Components/MainContentRankChangeBadge.swift
- 확인:
- 작업 내용:
- 홈 랭킹 badge를 공용으로 옮기는 변경이 과도하면 콘텐츠 랭킹 전용
MainContentRankChangeBadge를 만든다. - 입력값은
showRankChange: Bool,rankChange: Int?,isNew: Bool로 둔다. showRankChange == false이면 아무것도 표시하지 않는다.isNew == true이면Image("ic_rank_new")를 우선 표시한다.rankChange > 0이면 절댓값 숫자와Image("ic_rank_caret_increase")를 표시한다.rankChange < 0이면 절댓값 숫자와Image("ic_rank_caret_decrease")를 표시한다.rankChange == 0이면 숫자 없이Image("ic_rank_caret_stay")를 표시한다.rankChange == nil && isNew == false이면 아무것도 표시하지 않는다.
- 홈 랭킹 badge를 공용으로 옮기는 변경이 과도하면 콘텐츠 랭킹 전용
- 검증 기준:
- 실행 명령:
rg "struct MainContentRankChangeBadge|ic_rank_new|ic_rank_caret_increase|ic_rank_caret_decrease|ic_rank_caret_stay|showRankChange|rankChange|isNew" SodaLive/Sources/V2/Main/Content/Ranking/Components - 기대 결과: 콘텐츠 랭킹에서 사용할 순위 변화 표시 조건과 asset 이름이 확인된다.
- 실행 명령:
- 대상 파일:
Phase 5: 1위, 210위 썸네일 카드, 1120위 row UI 컴포넌트
-
Task 5.1: 1위 전용 카드 생성
- 대상 파일:
- 생성:
SodaLive/Sources/V2/Main/Content/Ranking/Components/MainContentAudioRankingTopCard.swift
- 생성:
- 작업 내용:
- Figma
24:6861기준 1위 전용 UI를 구성한다. coverImageUrl,rank,rankChange,isNew,title,creatorNickname을 표시한다.- tap 시
onTapContent(item.contentId)를 호출한다. - 루트 container에 고정 숫자
frame(width:),frame(height:)를 사용하지 않는다. - 전체 크기는 parent width와
aspectRatio또는 container-relative 제약으로 잡는다. - Preview에는 rank change 상승, 신규 진입, 순위 변화 숨김 상태를 포함한다.
- Figma
- 검증 기준:
- 실행 명령:
rg "struct MainContentAudioRankingTopCard|DownsampledKFImage|item.contentId|item.title|item.creatorNickname|MainContentRankChangeBadge|onTapContent\\(item.contentId\\)|aspectRatio|PreviewProvider|showRankChange: false|isNew: true" SodaLive/Sources/V2/Main/Content/Ranking/Components/MainContentAudioRankingTopCard.swift - 기대 결과: 1위 전용 UI가 콘텐츠 응답 필드를 사용하고, 전체 크기는 반응형 제약으로 구성된다.
- 실행 명령:
- 대상 파일:
-
Task 5.2: 2
10위 썸네일 카드와 1120위 row 생성- 대상 파일:
- 생성:
SodaLive/Sources/V2/Main/Content/Ranking/Components/MainContentAudioRankingRow.swift - 확인:
SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankingRow.swift
- 생성:
- 작업 내용:
- 2~10위 item은 썸네일 카드 방식으로 표시한다.
- 2
7위는 한 줄에 2개, 810위는 한 줄에 3개로 배치한다. - 2
7위와 810위는 동일한MainContentAudioRankingThumbnailCard를 사용하고,columnsPerRow에 따라 크기와 내부 요소 크기만 조정한다. - 11~20위 item은 row 방식으로 표시한다.
- 11~20위 row는 메인 홈 랭킹 탭의 크리에이터 순위 row 크기와 반응형 layout을 거의 그대로 복사한다.
- 단, 모델 입력은
AudioRankingItemResponse대응 타입으로 바꾸고 tap callback은onTapContent(item.contentId)로 바꾼다. - 텍스트 영역은 위에
item.title, 아래에item.creatorNickname을 표시한다. - rank 숫자는
item.rank를 사용한다. - 순위 변화 표시는
MainContentRankChangeBadge를 사용한다. - tap 시
onTapContent(item.contentId)를 호출한다. - 루트 container에 고정 숫자
frame(width:),frame(height:)를 사용하지 않는다. - card/row 전체 높이는 고정 숫자로 박지 않고 내부 padding, image aspect ratio, parent width 기반 제약으로 구성한다.
- Preview에는 2위, 10위, 11위, 20위와 상승, 하락, 변동 없음, 신규 진입, 순위 변화 숨김 상태를 배치한다.
- 검증 기준:
- 실행 명령:
rg "struct MainContentAudioRankingRow|DownsampledKFImage|item.rank|item.title|item.creatorNickname|MainContentRankChangeBadge|onTapContent\\(item.contentId\\)|aspectRatio|PreviewProvider|rank: 2|rank: 20|showRankChange: false|isNew: true" SodaLive/Sources/V2/Main/Content/Ranking/Components/MainContentAudioRankingRow.swift - 기대 결과: 2
10위는 동일 썸네일 카드 컴포넌트를 2열/3열 크기로 사용하고, 1120위 row는 홈 랭킹 row 구조를 거의 유지하면서 오디오 title/creatorNickname 2줄 텍스트와 content tap callback만 바꾼다.
- 실행 명령:
- 대상 파일:
-
Task 5.3: empty state 컴포넌트 생성
- 대상 파일:
- 생성:
SodaLive/Sources/V2/Main/Content/Ranking/Components/MainContentAudioRankingEmptyStateView.swift - 수정:
SodaLive/Sources/I18n/I18n.swift
- 생성:
- 작업 내용:
I18n.MainContentRanking.emptyStateMessage를 랭킹 콘텐츠 영역 안에 표시한다.- 배경과 텍스트 스타일은 기존 V2 콘텐츠 추천/전체 empty state와 맞춘다.
- 검증 기준:
- 실행 명령:
rg "struct MainContentAudioRankingEmptyStateView|I18n.MainContentRanking.emptyStateMessage|Color.black" SodaLive/Sources/V2/Main/Content/Ranking/Components/MainContentAudioRankingEmptyStateView.swift SodaLive/Sources/I18n/I18n.swift - 기대 결과: empty/API 실패 공통 표시 컴포넌트와 I18n 문구가 확인된다.
- 실행 명령:
- 대상 파일:
Phase 6: 랭킹 탭 화면 조립과 상세 진입 연결
-
Task 6.1: MainContentRankingView 구현
- 대상 파일:
- 생성:
SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingView.swift
- 생성:
- 작업 내용:
@StateObject private var viewModel = MainContentRankingViewModel()을 사용한다.- 최초
onAppear에서hasLoaded == false일 때만fetchRankings()를 호출한다. - loading, empty, content 상태를 분기한다.
- content 상태에서는 1위가 있으면
MainContentAudioRankingTopCard로 표시한다. - 2~7위는
MainContentAudioRankingThumbnailCard를 2열 grid로 표시한다. - 8~10위는 같은
MainContentAudioRankingThumbnailCard를 3열 grid로 표시한다. - 11~20위는
MainContentAudioRankingRow로 표시한다. showRankChange와onTapContent를 top card/thumbnail card/row에 전달한다.- 내부 tab bar와 title bar는 만들지 않고
MainContentViewshell에 맡긴다.
- 검증 기준:
- 실행 명령:
rg "struct MainContentRankingView|MainContentRankingViewModel|fetchRankings|MainContentAudioRankingTopCard|MainContentAudioRankingRow|MainContentAudioRankingEmptyStateView|onTapContent|MainPlaceholderTabView|TextTabBar|HomeTitleBar" SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingView.swift - 기대 결과:
MainPlaceholderTabView,TextTabBar,HomeTitleBar는 검색되지 않고, 랭킹 ViewModel과 top card/row 조립이 확인된다.
- 실행 명령:
- 대상 파일:
-
Task 6.2: MainContentView에서 랭킹 탭 연결
- 대상 파일:
- 수정:
SodaLive/Sources/V2/Main/Content/MainContentView.swift - 확인:
SodaLive/Sources/V2/Main/MainView.swift
- 수정:
- 작업 내용:
.ranking분기의MainPlaceholderTabView를MainContentRankingView(onTapContent: onTapContent)로 교체한다.- 기존
onTapContentcallback을 그대로 재사용해 오디오 상세 진입 guard 흐름을 유지한다.
- 검증 기준:
- 실행 명령:
rg "MainContentRankingView\\(onTapContent: onTapContent\\)|MainPlaceholderTabView\\(title: MainContentTab.ranking.title\\)|handleRecommendationContentTap" SodaLive/Sources/V2/Main/Content/MainContentView.swift SodaLive/Sources/V2/Main/MainView.swift - 기대 결과: 랭킹 placeholder는 검색되지 않고, 랭킹 item tap은 기존 콘텐츠 상세 callback으로 연결된다.
- 실행 명령:
- 대상 파일:
Phase 7: 프로젝트 연결과 전체 검증
-
Task 7.1: Xcode 프로젝트 포함 여부 확인
- 대상 파일:
- 확인/수정:
SodaLive.xcodeproj/project.pbxproj
- 확인/수정:
- 작업 내용:
- 신규 Swift 파일이 Xcode project에 자동 포함되지 않는 구조이면 project file에 포함한다.
- XcodeGen/SPM 자동 discovery 구조가 아니면 누락 파일 없이 target membership을 맞춘다.
- 검증 기준:
- 실행 명령:
rg "MainContentRankingView|AudioRankingResponse|MainContentRankingApi|MainContentAudioRankingRow" SodaLive.xcodeproj/project.pbxproj - 기대 결과: 프로젝트 파일 관리 방식에 따라 신규 Swift 파일 참조가 확인되거나, 자동 포함 구조라면 별도 수정이 필요 없다는 근거가 확인된다.
- 실행 명령:
- 대상 파일:
-
Task 7.2: 정적 검색 검증
- 대상 파일:
- 확인:
SodaLive/Sources/V2/Main/Content/Ranking/** - 확인:
SodaLive/Sources/V2/Main/Content/MainContentView.swift - 확인:
SodaLive/Sources/I18n/I18n.swift
- 확인:
- 작업 내용:
- endpoint, model, ViewModel, UI 연결, I18n 문구를 정적 검색으로 확인한다.
- 전체 루트 UI에서 고정 width/height를 강제한 지점을 검색한다.
- 검증 기준:
- 실행 명령:
rg "/api/v2/audio/rankings|AudioRankingResponse|MainContentRankingView|MainContentAudioRankingTopCard|MainContentAudioRankingRow|emptyStateMessage|loadFailedMessage" SodaLive/Sources/V2/Main/Content SodaLive/Sources/I18n/I18n.swift - 기대 결과: API, 모델, 화면 연결, I18n 문구가 확인된다.
- 실행 명령:
rg "frame\\(width:|frame\\(height:" SodaLive/Sources/V2/Main/Content/Ranking - 기대 결과: 루트 container의 고정 width/height 강제가 없고, 아이콘/이미지/내부 요소에 필요한 제한만 존재한다.
- 실행 명령:
- 대상 파일:
-
Task 7.3: 빌드 검증
- 대상 파일:
- 확인:
docs/agent-guides/build-test-verification.md
- 확인:
- 작업 내용:
- 프로젝트에서 사용하는 최신 빌드 검증 명령을 문서에서 확인한 뒤 실행한다.
- 검증 기준:
- 실행 명령:
xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -sdk iphonesimulator build - 기대 결과:
** BUILD SUCCEEDED **
- 실행 명령:
- 대상 파일:
검증 기록
- 2026-07-06: 사용자 요청에 따라 구현 대상 Figma URL 4개를 본 문서에 기록했다.
- 2026-07-06: 누락된 PRD를
docs/20260706_메인_콘텐츠_내부_랭킹_탭/prd.md로 추가하고 본 문서의 기준 문서에 연결했다. - 2026-07-06: 사용자 피드백을 반영해 2
10위는 썸네일 카드, 1120위는 row 형태로 구현하도록 문서를 보정했다. - 2026-07-06:
SodaLive/Sources/V2/Main/Content/MainContentView.swift에서 내부ranking탭이 아직MainPlaceholderTabView임을 확인했다. - 2026-07-06: V2 재사용 후보로
TextTabBar,HomeTitleBar, 홈 랭킹 badge/row/card,AudioContentThumbnailCard,AudioContentListRow, 콘텐츠 API 패턴 파일을 확인해 본 문서에 기록했다. - 2026-07-06: 사용자 요청 범위가 “문서만 작성”이므로 Swift 소스, Xcode 프로젝트, asset은 수정하지 않았다.
- 2026-07-06:
SodaLive/Sources/V2/Main/Content/Ranking/**하위에 오디오 랭킹 모델, API, Repository, ViewModel, 전용 badge/top card/row/empty state/view를 추가했다. - 2026-07-06:
MainContentView의.rankingplaceholder를MainContentRankingView(onTapContent: onTapContent)로 교체해 기존 콘텐츠 상세 진입 callback을 재사용하도록 연결했다. - 2026-07-06:
SodaLive.xcodeproj/project.pbxproj에 신규 랭킹 Swift 파일 9개를 3개 앱 타깃 Sources에 포함했다. - 2026-07-06: 테스트 번들 타깃이 확인되지 않아 신규 테스트 인프라는 추가하지 않고, 정적 검색 검증과
xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -sdk iphonesimulator build로 검증했다. 최초 빌드에서SodaSpacing.s2미존재 오류를 확인해SodaSpacing.s4로 수정했고, 재실행 결과** BUILD SUCCEEDED **를 확인했다. - 2026-07-06: Figma
24:6867,24:6872를 재확인해 210위는 썸네일 카드, 1120위는 row 형태임을 반영했다.MainContentAudioRankingThumbnailCard를 추가하고MainContentRankingView에서 27위는 2열, 810위는 3열, 11~20위는 row로 분기하도록 수정했다.