Files
sodalive-android/docs/prd/20260520_콘텐츠랭킹위젯컴포넌트_prd.md

196 lines
15 KiB
Markdown

# PRD: 콘텐츠 랭킹 위젯 컴포넌트
## 1. Overview
Figma `20:3715`, `20:3718`, `20:3721`, `20:3724` 디자인을 기준으로 콘텐츠 랭킹을 순위 구간별 카드 형태로 표현하는 Android XML Views 기반 위젯 컴포넌트를 개발한다.
---
## 2. Problem
- 콘텐츠 랭킹은 순위 구간에 따라 카드 UI와 한 줄 배치 개수가 달라져야 한다.
- 기기 폭이 하나로 고정되지 않으므로 Figma metadata size를 실제 이미지 크기로 고정하면 다양한 화면 폭에서 재사용하기 어렵다.
- 2위~7위와 8위~10위는 카드 UI와 텍스트 크기가 달라질 수 있어 순위 구간별 표시 contract를 명확히 해야 한다.
- 순위 변동, 신규 진입, 차단 관계, 터치 가능 여부가 함께 표시되어야 하므로 데이터와 UI 상태 계약을 분리해야 한다.
---
## 3. Goals
- Figma 4개 노드 기준의 콘텐츠 랭킹 카드 variant와 row 배치 정책을 제공한다.
- 이미지 크기는 컴포넌트 내부에서 고정하지 않고 실제 사용하는 row container 폭과 row count에 맞춰 계산한다.
- 순위 구간별 한 줄 배치 규칙을 제공한다.
- 1위: 한 줄에 1개, Figma `20:3715`, 큰 콘텐츠 카드.
- 2위~7위: 한 줄에 2개, Figma `20:3718`, 2열 정사각형 카드.
- 8위~10위: 한 줄에 3개, Figma `20:3721`, 3열 정사각형 카드.
- 11위 이후: 가로형으로 한 줄에 1개, Figma `20:3724`, 가로형 카드.
- 콘텐츠명은 순위 구간별 글자 수 기준을 초과하면 한 줄 말줄임 처리한다.
- `rank-num` 영역은 이전 순위와 비교한 변동 상태를 표시한다.
- 차단 관계인 크리에이터의 콘텐츠는 이미지 블러, 정보 비노출 또는 대체문구, 터치 불가 상태로 표시한다.
- 기존 화면 일괄 적용은 구현 계획에서 별도 task로 제한하고, 컴포넌트 계약을 우선 고정한다.
---
## 4. Non-Goals
- 이번 범위에서는 서버 API 설계나 응답 필드명을 확정하지 않는다. 필요한 클라이언트 데이터 계약만 문서화한다.
- 크리에이터 랭킹 위젯, 오디오 콘텐츠 카드, 콘텐츠 상세 화면, 검색/필터 UI는 변경하지 않는다.
- Compose 컴포넌트 또는 Compose Theme를 추가하지 않는다.
- Figma에 없는 skeleton loading, shimmer, pressed animation, 별도 badge, 광고 영역, 페이지네이션 UI를 추가하지 않는다.
- 차단/차단 해제 기능 자체를 새로 만들지 않는다.
- 내가 차단했는지, 나를 차단했는지를 UI에서 구분해 표시하지 않는다.
- 이미지 로딩 라이브러리 교체를 수행하지 않는다.
---
## 5. Target Users
- 콘텐츠 랭킹 화면을 보는 앱 사용자.
- XML 레이아웃과 RecyclerView 기반 랭킹 UI를 구현/유지보수하는 Android 개발자.
---
## 6. User Stories
- 사용자는 인기 콘텐츠의 상위 순위를 구간별 강조도 차이로 빠르게 확인하고 싶다.
- 사용자는 콘텐츠의 순위가 올랐는지, 내려갔는지, 유지됐는지, 신규 진입했는지 알고 싶다.
- 사용자는 차단 관계에 있는 크리에이터의 콘텐츠 정보가 노출되지 않기를 기대한다.
- 개발자는 순위 구간별 UI를 하나의 명확한 계약으로 바인딩하고 싶다.
---
## 7. Core Features
### Content Ranking Widget
콘텐츠 랭킹 목록을 순위 구간별 카드 variant와 행 배치 규칙으로 표시한다.
#### Figma References
- Rank 1 large content card: 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=20-3715&m=dev
- Rank 2~7 two-column content card: 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=20-3718&m=dev
- Rank 8~10 three-column content card: 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=20-3721&m=dev
- Rank 11+ horizontal content card: 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=20-3724&m=dev
#### Variant and Row Requirements
| Rank range | Figma node | Row count | UI variant | Size policy |
| --- | --- | --- | --- | --- |
| 1 | `20:3715` | 한 줄에 1개 | `Large` | 실제 사용 영역 폭을 1등분하고 Figma 큰 카드 비율로 표시 |
| 2~7 | `20:3718` | 한 줄에 2개 | `MediumGrid` | 실제 사용 영역 폭을 2등분해 정사각형으로 표시 |
| 8~10 | `20:3721` | 한 줄에 3개 | `SmallGrid` | 실제 사용 영역 폭을 3등분해 정사각형으로 표시 |
| 11+ | `20:3724` | 한 줄에 1개 | `Horizontal` | 실제 사용 영역 폭을 1등분하고 Figma 가로형 비율로 표시 |
#### Variant Details
- `Large`: 1위 전용 카드다. 배경 영역, 중앙 콘텐츠 이미지, 하단 콘텐츠명/크리에이터명, 순위 숫자, 순위 변동 표시를 포함한다.
- `MediumGrid`: 2위~7위 전용 정사각형 카드다. 2열 배치를 기준으로 콘텐츠명은 `22sp` bold 스타일을 사용한다.
- `SmallGrid`: 8위~10위 전용 정사각형 카드다. 3열 배치를 기준으로 콘텐츠명은 `14sp` bold 스타일을 사용한다.
- `Horizontal`: 11위 이후 전용 가로형 카드다. 좌측 순위/변동, 중앙 이미지, 우측 콘텐츠명/크리에이터명 영역을 가진다.
- Figma metadata size는 참고용 비율 확인에만 사용하고, 구현에서 고정 dp 크기로 사용하지 않는다.
#### Text Requirements
- 모든 텍스트는 `maxLines=1`, `ellipsize=end`로 한 줄 말줄임 처리한다.
- 1위 콘텐츠명은 16자를 초과하면 말줄임 처리한다.
- 2위~10위 콘텐츠명은 8자를 초과하면 말줄임 처리한다.
- 11위 이후 콘텐츠명은 12자를 초과하면 말줄임 처리한다.
- 크리에이터명도 한 줄 제한을 유지하고, 실제 잘림은 레이아웃 폭과 `ellipsize=end`에 따른다.
#### Figma Token Requirements
- 공통 카드 이미지 radius는 `radius_14` 또는 `14dp`를 사용한다.
- 공통 dim gradient는 위쪽 `rgba(0,0,0,0)`, 아래쪽 black, opacity `50%`, 전환 시작점 `64.423%` 기준으로 구현한다.
- 1위 카드에는 Figma 기준 배경 blur/dim 영역과 중앙 콘텐츠 이미지 영역을 함께 둔다.
- 공통 `rank-num` 배경은 `gray_900` (`#202020`), radius `4dp`, horizontal padding `4dp`, gap `2dp`를 사용한다.
- 공통 `rank-num` 숫자는 Pretendard Variable Medium, `16sp`, line-height `1.45`, color white를 사용한다.
- 공통 caret icon 크기는 `14dp x 14dp`를 기준으로 한다.
- 순위 숫자는 Pattaya Regular를 사용하고 white~`#EEEEEE` vertical gradient와 `0px 0px 4px rgba(0,0,0,0.48)` shadow를 적용한다.
- `Large` 콘텐츠명은 Pretendard Variable Bold, `22sp`, line-height `1.45`, color white를 기준으로 한다.
- `Large` 크리에이터명은 Pretendard Variable Regular, `12sp`, line-height normal, color white를 기준으로 한다.
- `MediumGrid` 콘텐츠명은 Pretendard Variable Bold, `22sp`, line-height `1.45`, color white를 기준으로 한다.
- `MediumGrid` 크리에이터명은 Pretendard Variable Regular, `12sp`, line-height normal, color white를 기준으로 한다.
- `SmallGrid` 콘텐츠명은 Pretendard Variable Bold, `14sp`, line-height normal, color white를 기준으로 한다.
- `SmallGrid` 크리에이터명은 Pretendard Variable Regular, `12sp`, line-height normal, color white를 기준으로 한다.
- `Horizontal` 콘텐츠명은 Pretendard Variable Bold, `18sp`, line-height `1.45`, color white를 기준으로 한다.
- `Horizontal` 크리에이터명은 Pretendard Variable Regular, `14sp`, line-height `1.45`, color white를 기준으로 한다.
#### Rank Change Requirements
- 모든 variant는 현재 순위 숫자를 표시한다.
- `rank-num` 영역은 순위 변동 상태를 표시한다.
- 순위 상승: `ic_rank_caret_increase`와 변동 숫자를 표시한다.
- 순위 하락: `ic_rank_caret_decrease`와 변동 숫자를 표시한다.
- 순위 동일: 숫자 없이 `ic_rank_caret_stay` 아이콘만 표시한다.
- 신규 진입: `rank-num` 대신 `ic_rank_new` 이미지를 표시한다.
- 신규 진입이 아니고 순위 동일이 아닌 경우 `rank-num`에는 이전 순위 대비 변동 숫자를 표시한다.
#### Blocked Creator Requirements
- 내가 차단했거나 나를 차단한 크리에이터는 하나의 차단 관계 상태로만 전달받는다.
- 차단 관계 상태에서는 콘텐츠 이미지를 블러 처리한다.
- 차단 관계 상태의 1위~10위 카드는 콘텐츠명과 크리에이터 이름을 표시하지 않는다.
- 차단 관계 상태의 1위~10위 카드는 이름 영역을 숨겨도 하단 dim gradient 영역은 유지한다.
- 차단 관계 상태의 11위 이후 가로형 카드는 콘텐츠명과 크리에이터 이름 대신 `접근할 수 없는 정보입니다.` 한 줄만 표시한다.
- 차단 관계 상태의 카드는 터치할 수 없다.
- 접근 가능 상태의 카드는 터치할 수 있고, 터치 시 호출부가 콘텐츠 상세 이동 등 후속 동작을 처리한다.
#### Data Contract Requirements
- 최소 데이터 계약은 다음 정보를 포함해야 한다.
- `contentId`: 콘텐츠 식별자.
- `creatorId`: 크리에이터 식별자.
- `rank`: 현재 순위. 1부터 시작한다.
- `previousRank`: 이전 순위. 신규 진입이면 null 허용.
- `rankChangeType`: `increase`, `decrease`, `stay`, `new` 중 하나.
- `rankChangeAmount`: 신규 진입이 아닌 경우 표시할 변동 숫자.
- `contentName`: 콘텐츠명.
- `creatorName`: 크리에이터 이름.
- `imageUrl`: 콘텐츠 이미지 URL.
- `isBlocked`: 내가 차단했거나 나를 차단한 차단 관계 여부.
- UI는 `isBlocked`만 사용하고 차단 방향은 구분하지 않는다.
- 순위 변동 타입은 콘텐츠 랭킹 전용 타입을 새로 만들지 않고, 크리에이터 랭킹에서 사용하는 변동 타입을 공용 이름으로 변경해 함께 사용한다.
#### Edge Cases
- 랭킹 데이터가 0개이면 위젯 영역은 표시하지 않거나 호출부의 empty 정책을 따른다.
- 랭킹 데이터가 1~10개이면 존재하는 순위까지만 구간 규칙을 적용한다.
- `rank`가 누락되거나 1보다 작으면 호출부 데이터 오류로 간주하고 해당 항목을 표시하지 않는다.
- `rankChangeType``new`이면 `previousRank``rankChangeAmount`가 없어도 된다.
- `rankChangeType``stay`이면 `rankChangeAmount`가 있어도 숫자를 표시하지 않는다.
- `rankChangeType``increase` 또는 `decrease`인데 `rankChangeAmount`가 없으면 구현 단계에서 데이터 검증 정책을 정한다.
- 이미지 로딩 실패 시 placeholder 정책은 기존 이미지 로딩 계층 또는 호출 화면 정책을 따른다.
---
## 8. UX / UI Expectations
- 전체 위젯은 어두운 배경 위에서 사용하는 것을 전제로 한다.
- 카드 이미지는 rounded corner를 가진다.
- 카드 이미지는 공통 dim gradient를 가진다. 차단 관계 상태에서 이름을 숨기더라도 1위~10위의 gradient overlay는 유지한다.
- 1위 카드는 배경 영역과 중앙 콘텐츠 이미지가 분리된 형태를 유지한다.
- 2위~7위와 8위~10위는 각각 2열/3열 배치에 맞춰 같은 데이터 계약을 다른 variant로 표시한다.
- 11위 이후 카드는 좌측 순위, 중앙 이미지, 우측 텍스트 영역을 가진다.
- 이미지 크기는 고정 dp로 박지 않고 row container 폭에서 계산한다.
- 정사각형 variant는 계산된 카드 폭과 동일한 높이로 표시한다.
- 가로형 variant는 부모 폭을 채우고 Figma 가로형 비율에 맞는 높이를 유지한다.
- 차단 관계 상태는 사용자가 상세로 들어갈 수 없음을 시각적으로 알 수 있어야 한다.
---
## 9. Technical Constraints
- 현재 프로젝트는 Android XML Views + ViewBinding + RecyclerView 기반이므로 XML 레이아웃과 Kotlin custom view/adapter 패턴을 우선한다.
- 신규 Kotlin 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
- 재사용 가능한 위젯은 `kr.co.vividnext.sodalive.v2.widget.contentranking` 또는 기능 범위에 맞는 하위 패키지에 둔다.
- 기존 크리에이터 랭킹 위젯 문서는 참고 대상으로만 사용하고, 콘텐츠 랭킹 contract는 별도 파일로 분리한다.
- 크리에이터 랭킹에서 사용 중인 순위 변동 타입은 `RankingChangeType`처럼 랭킹 공용 이름으로 변경하고, 크리에이터 랭킹과 콘텐츠 랭킹이 동일 타입을 참조하도록 한다.
- 기존 프로젝트의 이미지 로딩 방식이 화면별로 Glide/Coil을 함께 사용하므로, 컴포넌트 내부에 특정 이미지 로더를 강제하지 않는 API를 우선한다.
- 차단 관계 이미지 블러는 기존 `kr.co.vividnext.sodalive.common.image.BlurTransformation` 등 기존 blur 구현의 재사용 가능성을 먼저 검토한다.
- `ic_rank_caret_increase`, `ic_rank_caret_decrease`, `ic_rank_caret_stay`, `ic_rank_new` 리소스가 없으면 구현 단계에서 디자인 에셋 추가가 필요하다.
---
## 10. Metrics
- 순위 구간별 row count가 요구사항과 일치한다.
- 1위는 `Large`, 2위~7위는 `MediumGrid`, 8위~10위는 `SmallGrid`, 11위 이후는 `Horizontal` variant로 바인딩된다.
- 콘텐츠명은 순위 구간별 글자 수 기준과 한 줄 말줄임 계약을 만족한다.
- `rankChangeType`별 아이콘/숫자 표시가 문서와 일치한다.
- `stay` 상태에서는 숫자 없이 `ic_rank_caret_stay`만 표시된다.
- `new` 상태에서는 `rank-num` 대신 `ic_rank_new`가 표시된다.
- 차단 관계 상태에서 이미지 블러, 이름 비노출/대체문구, 터치 불가가 모두 적용된다.
- 차단 관계 상태에서 1위~10위 카드의 gradient overlay가 유지된다.
- 이미지 크기가 고정 dp가 아닌 부모 폭과 row count 기반으로 계산된다.
- 관련 unit test와 Android resource merge/build가 성공한다.
---
## 11. Open Questions
- 서버 응답에 이전 순위, 신규 진입 여부, 차단 관계 여부가 이미 포함되는지 확인이 필요하다. 없으면 API/DTO 확장이 별도 백엔드 협의 항목이다.
- Figma `get_design_context` 확인 결과 typography/color/radius 토큰은 본 문서의 `Figma Token Requirements`에 반영했다.
- 콘텐츠명 글자 수 제한은 사용자 요구사항에 따라 1위 16자, 2위~10위 8자, 11위 이후 12자로 확정한다.
- 차단 관계 상태에서 1위~10위 카드의 이름 영역을 숨길 때 gradient 영역은 유지하는 것으로 확정한다.
- 순위 변동 타입은 크리에이터 랭킹과 콘텐츠 랭킹이 같은 데이터이므로, 구현 시 기존 크리에이터 랭킹 타입을 공용 `RankingChangeType`으로 rename해 재사용하는 것으로 확정한다.