# PRD: 피드 컴포넌트 ## 1. Overview Figma `63:4133`, `63:4140`, `63:4142`, `63:4155` 디자인을 기준으로 Rank, Live, Content, Community 4가지 Feed UI를 Android XML Views 기반 재사용 컴포넌트로 문서화한다. --- ## 2. Problem - Feed UI는 같은 카드 배경과 radius를 공유하지만 variant별 내부 구조가 달라 화면별 개별 구현 시 간격, typography, 말줄임 정책이 달라질 수 있다. - Rank Feed는 문장 안의 순위 텍스트만 강조색을 적용해야 하므로 일반 텍스트와 강조 범위를 분리한 바인딩 계약이 필요하다. - Live Feed는 라이브 방송 이름과 상대 시간이 같은 줄에 있어 긴 제목이 시간을 침범하지 않도록 레이아웃 제약이 필요하다. - Content와 Community Feed는 Figma 기준의 표시 요소가 다르므로 하나의 카드 컴포넌트 안에서 variant별 데이터 계약을 명확히 분리해야 한다. --- ## 3. Goals - Figma 4개 노드 기준의 Feed variant를 제공한다. - Rank: Figma `63:4133` - Live: Figma `63:4140` - Content: Figma `63:4142` - Community: Figma `63:4155` - 공통 Feed 카드 배경, padding, radius, typography token을 Android resource 기준으로 정리한다. - Rank Feed의 왼쪽 정사각형 영역은 실제 이미지를 표시하는 `ImageView`로 제공한다. - Rank Feed의 오른쪽 문구는 호출부가 변경 가능해야 하며, 순위에 해당하는 텍스트 범위만 `soda_400` 강조색으로 표시한다. - Rank Feed 전체 UI는 세로 가운데 정렬을 유지한다. - Live Feed의 라이브 방송 이름은 한 줄 제한과 끝 말줄임을 적용한다. - Live Feed의 라이브 방송 이름은 상대 시간 텍스트와 겹치지 않도록 제목 영역을 시간 영역 앞에서 제약한다. - Content와 Community Feed는 Figma에 표시된 이미지, 프로필, 제목/본문, 태그, 반응 수, 시간 정보를 variant별로 바인딩할 수 있게 한다. - Feed를 가로 스크롤 목록에 배치하거나 한 줄에 여러 개 표시할 때는 Figma에 표시된 variant별 가로 사이즈를 사용한다. - Feed를 한 줄에 1개만 표시할 때는 Figma 고정 폭이 아니라 부모 view의 가용 영역 폭을 채운다. --- ## 4. Non-Goals - 이번 범위에서는 서버 API, DTO 필드명, pagination, 정렬, 추천 알고리즘을 변경하지 않는다. - Feed 목록 화면 전체 개편이나 기존 화면 일괄 적용은 포함하지 않는다. - Compose 컴포넌트 또는 Compose Theme를 추가하지 않는다. - Figma에 없는 skeleton loading, shimmer, pressed animation, 추가 badge, 광고 영역은 추가하지 않는다. - 이미지 로딩 라이브러리를 컴포넌트 내부에서 Coil/Glide 중 하나로 고정하지 않는다. - Rank 순위 문구 생성 규칙은 컴포넌트 내부에서 계산하지 않고 호출부가 완성된 문장과 강조 범위를 전달한다. --- ## 5. Target Users - 메인/알림/홈 화면에서 Feed 카드를 확인하는 앱 사용자. - XML Views와 RecyclerView 또는 ViewBinding 기반으로 Feed UI를 구현/유지보수하는 Android 개발자. --- ## 6. User Stories - 사용자는 랭킹, 라이브 종료, 콘텐츠 업로드, 커뮤니티 글 알림을 같은 Feed 카드 스타일 안에서 빠르게 구분하고 싶다. - 사용자는 긴 라이브 제목이나 콘텐츠 제목이 있어도 카드 레이아웃이 깨지지 않기를 기대한다. - 개발자는 4가지 Feed variant를 같은 컴포넌트 계약으로 재사용하고 싶다. - 개발자는 Rank Feed 문구를 상황별로 바꾸면서 순위 텍스트만 강조하고 싶다. --- ## 7. Core Features ### Feed Component Feed 카드는 `Rank`, `Live`, `Content`, `Community` 4가지 variant를 가진다. #### Figma References - Rank Feed: 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=63-4133&m=dev - Live Feed: 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=63-4140&m=dev - Content Feed: 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=63-4142&m=dev - Community Feed: 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=63-4155&m=dev #### Common Token Requirements - Root background는 `gray_900` (`#202020`)을 사용한다. - Root padding은 `spacing_14` (`14dp`)를 기준으로 한다. - Root radius는 `radius_14` (`14dp`)를 기준으로 한다. - 카드 내부 gap은 variant별 Figma 값에 맞춰 `spacing_4`, `spacing_6`, `spacing_8`, `spacing_14`, `spacing_20`을 우선 사용한다. - 모든 이미지 영역은 실제 이미지 URL을 바인딩할 수 있는 `ImageView`로 제공하고, 이미지 로딩 실패/placeholder 정책은 호출부가 결정한다. - 장식 아이콘은 접근성 노출이 필요하지 않으면 `contentDescription=@null`로 둔다. #### Size Policy Requirements - Feed root width는 사용 맥락에 따라 결정한다. - 가로 스크롤 목록에서 사용하는 Feed item은 Figma에 표시된 variant별 root width를 사용한다. - Grid 또는 flex row처럼 한 줄에 여러 Feed item을 표시하는 경우에도 각 item은 Figma에 표시된 variant별 root width를 사용한다. - 한 줄에 Feed item을 1개만 표시하는 경우 root width는 부모 view의 가용 영역을 채운다. - 부모 view의 가용 영역은 부모 width에서 부모 padding과 item decoration 간격을 제외한 폭이다. - 한 줄 1개 모드에서도 내부 image size, padding, radius, typography, icon size는 Figma 기준값을 유지한다. - 한 줄 1개 모드에서 늘어나는 영역은 텍스트 column, 본문 영역, 제목 영역처럼 남은 폭을 사용하는 content 영역이다. - 구현에서는 `WrapContent` 또는 Figma fixed width 모드와 `MatchParent` 또는 parent available width 모드를 명시적으로 구분해야 한다. #### Variant Requirements | Variant | Figma node | Figma root width | Layout | Main elements | | --- | --- | --- | --- | --- | | `Rank` | `63:4133` | `374dp` | 가로 배치, 세로 가운데 정렬 | 80dp 정사각형 이미지, 순위 overlay, 변경 가능한 문장 | | `Live` | `63:4140` | `374dp` | 세로 배치 | 프로필/종료 문구 row, 라이브 제목/상대 시간 row | | `Content` | `63:4142` | `374dp` | 가로 배치 | 122dp 콘텐츠 이미지, 프로필, 콘텐츠명, 카테고리 태그, 상대 시간 | | `Community` | `63:4155` | `374dp` | 세로 배치 | 프로필 row, 커뮤니티 본문, 키워드, 댓글/좋아요 반응 수 | ### Rank Feed Requirements - 왼쪽 정사각형은 실제 이미지를 표시하는 `ImageView`다. - 이미지 영역은 `80dp x 80dp`, `radius_14`, `centerCrop`을 기준으로 한다. - 이미지 위에는 Figma `Rank(type=Default, rank1=all)` 기준의 순위 숫자 overlay를 표시한다. - 순위 숫자 overlay는 `Pattaya Regular`, `40sp`, white-to-`#EEEEEE` 계열 표현에 가장 가깝게 표시하고, 그림자는 `0dp 0dp 4dp rgba(0,0,0,0.48)` 기준으로 둔다. - 순위 숫자 overlay는 이미지 오른쪽 하단에 배치하되 Figma처럼 이미지 영역을 약간 벗어나 보일 수 있도록 image container와 root는 해당 overlay를 자르지 않는다. - 오른쪽 텍스트는 호출부가 변경 가능한 문자열이어야 한다. - 오른쪽 텍스트의 기본 style은 `Pretendard Variable Medium`, `16sp`, line-height `1.45`, white다. - `1위`, `10위`, `30위`처럼 순위를 나타내는 텍스트 범위만 `soda_400` (`#00BDF7`)로 표시한다. - 순위 강조 범위는 컴포넌트가 문자열에서 추론하지 않고 호출부가 `highlightRanges` 또는 동등한 계약으로 전달한다. - 전체 root는 `items_center`에 해당하는 세로 가운데 정렬을 유지한다. ### Live Feed Requirements - 첫 번째 row는 `20dp` 원형 프로필 이미지와 서버/호출부가 전달한 종료 안내 문구를 하나의 `12sp` TextView로 표시한다. - 종료 안내 문구는 creator name과 suffix를 컴포넌트 내부에서 조합하지 않고, 서버/호출부가 내려준 완성 문자열을 그대로 표시한다. - 종료 안내 문구 TextView는 폭 제약을 받아 한 줄 제한과 끝 말줄임을 적용하며 카드 바깥으로 넘치지 않아야 한다. - 두 번째 row는 라이브 방송 이름과 상대 시간을 표시한다. - 라이브 방송 이름은 `Typography.Heading4` 또는 `18sp bold`, white, `maxLines=1`, `ellipsize=end`를 적용한다. - 상대 시간은 `Typography.Body6` 또는 `14sp regular`, `gray_500`, 오른쪽 정렬을 기준으로 한다. - 라이브 방송 이름과 상대 시간 사이에는 최소 `20dp` gap을 둔다. - 라이브 방송 이름 영역은 상대 시간 영역 앞에서 폭이 제한되어야 하며, 긴 제목이 `2분 전`, `10분 전` 같은 시간 텍스트와 겹치면 안 된다. ### Content Feed Requirements - 왼쪽 콘텐츠 이미지는 category tag에 따라 높이가 달라진다. - 왼쪽 콘텐츠 이미지의 가로 크기는 현재 설정된 값을 유지하고, category별 비율에 따라 높이만 계산한다. - category tag에 들어갈 수 있는 값은 `콘텐츠`, `시리즈`, `매거진`이다. - `콘텐츠` category는 `1:1` 비율로 표시한다. - `시리즈` category는 `163:230` 비율로 표시한다. - `매거진` category는 `163:218` 비율로 표시한다. - category별 이미지 높이 계산은 `height = imageWidth * ratioHeight / ratioWidth`를 기준으로 한다. - 이미지 영역은 모든 category에서 `radius_14`, `centerCrop`을 기준으로 한다. - 오른쪽 column은 프로필 row, 콘텐츠명, 하단 메타 row를 가진다. - 프로필 row는 `20dp` 원형 프로필 이미지와 `12sp` creator name을 표시한다. - 콘텐츠명은 `Typography.Heading4` 또는 `18sp bold`, white, `maxLines=1`, `ellipsize=end`를 적용한다. - 하단 메타 row는 왼쪽 category tag와 오른쪽 상대 시간을 표시한다. - category tag는 `gray_700` background, `radius_4`, horizontal padding `4dp`, vertical padding `2dp`, text `14sp regular`, `gray_100`을 기준으로 한다. - 기본 category label은 `콘텐츠`이며, 표시 문자열은 `Content`, `Series`, `Magazine` 별 string resource를 사용한다. - 상대 시간은 `14sp regular`, `gray_500`, 오른쪽 정렬을 기준으로 한다. ### Community Feed Requirements - 상단 profile row는 `42dp` 원형 프로필 이미지, creator name, 상대 시간을 표시한다. - creator name은 `Typography.Body5` 또는 `14sp medium`, white를 기준으로 한다. - 상대 시간은 `Typography.Body6` 또는 `14sp regular`, `gray_500`을 기준으로 한다. - 본문은 `Typography.Body3` 또는 `16sp regular`, white, line-height `1.45`를 기준으로 한다. - 본문 영역 폭은 카드 내부 폭을 채우며, Figma 기준 `346dp`를 기준값으로 삼되 부모 폭이 달라지면 내부 padding을 제외한 가용 폭을 사용한다. - 키워드 영역은 `Typography.Body3` 또는 `16sp regular`, `soda_400`을 기준으로 한다. - 반응 row는 댓글 수와 좋아요 수를 표시하고, icon size는 `18dp`, 텍스트는 `16sp regular`, `gray_500`을 기준으로 한다. - 댓글 아이콘은 `ic_feed_community_reply`, 좋아요 아이콘은 `ic_feed_community_heart` 리소스를 사용한다. - 본문과 키워드가 비어 있으면 전달된 값 그대로 표시하되, 호출부 정책에 따라 빈 row를 숨길 수 있는 API를 제공한다. #### Data Contract Requirements - 모든 Feed item은 `feedId`와 `variant`를 포함해야 한다. - 시간 표시가 있는 `Live`, `Content`, `Community` item은 `createdAtText`를 포함해야 한다. - Rank 데이터는 다음 정보를 포함해야 한다. - `imageUrl`: 왼쪽 정사각형 이미지 URL. - `rankText`: 이미지 overlay 또는 접근성에 사용할 순위 문자열. - `message`: 오른쪽 전체 문장. - `highlightRanges`: 순위 텍스트만 강조할 문자열 범위 목록. - Live 데이터는 다음 정보를 포함해야 한다. - `creatorId`, `creatorName`, `creatorImageUrl`. - `liveId`, `liveTitle`. - `endedMessage`: 서버/호출부가 내려준 완성된 라이브 종료 안내 문구. - Content 데이터는 다음 정보를 포함해야 한다. - `creatorId`, `creatorName`, `creatorImageUrl`. - `contentId`, `contentTitle`, `contentImageUrl`. - `category`: `Content`, `Series`, `Magazine` 중 하나. 표시 label은 각각 `콘텐츠`, `시리즈`, `매거진`이다. - Community 데이터는 다음 정보를 포함해야 한다. - `creatorId`, `creatorName`, `creatorImageUrl`. - `postId`, `bodyText`, `keywordText`. - `commentCount`, `likeCount`. #### Edge Cases - 이미지 URL이 비어 있으면 호출부 이미지 로딩 정책에 따른 placeholder 또는 빈 이미지를 표시한다. - Rank `message`가 비어 있으면 오른쪽 텍스트 영역은 빈 문자열로 유지한다. - Rank `highlightRanges`가 문자열 범위를 벗어나면 구현 단계에서 안전하게 무시하거나 clamp하는 정책을 단위 테스트로 고정한다. - Live `liveTitle`이 매우 길면 한 줄 말줄임 처리하고 상대 시간은 항상 보존한다. - 상대 시간 문자열이 길어도 한 줄로 표시하며, 제목/본문 영역이 해당 영역을 침범하지 않는다. - Content `contentTitle`이 길면 한 줄 말줄임 처리한다. - Content `category`가 서버/호출부에서 누락되면 기본값은 `Content`로 간주해 `콘텐츠` label과 `1:1` 이미지 비율을 적용한다. - Community 본문이 길면 Figma처럼 여러 줄 표시를 허용하되, 최대 줄 수 제한은 호출 화면 정책에 따른다. - 댓글 수 또는 좋아요 수가 없으면 0으로 표시하거나 호출부가 전달한 문자열을 그대로 표시하는 정책 중 하나를 구현 계획에서 고정한다. --- ## 8. UX / UI Expectations - 4가지 Feed 모두 어두운 배경 위에서 사용하는 것을 전제로 한다. - Root 카드의 배경, radius, padding은 variant 간 일관성을 유지한다. - 가로 스크롤 또는 한 줄 다중 표시에서는 Figma `374dp` root width를 유지한다. - 한 줄 1개 표시에서는 부모 view의 가용 폭을 채우되 내부 고정 요소 크기는 유지한다. - Rank Feed는 이미지와 텍스트가 한 줄 카드 안에서 세로 가운데 정렬되어야 한다. - Live Feed의 제목은 시간과 겹치지 않고, 시간이 항상 오른쪽에서 읽혀야 한다. - Content Feed는 왼쪽 큰 썸네일과 오른쪽 정보 column의 수직 배치가 Figma와 일치해야 한다. - Content Feed의 왼쪽 이미지는 category가 바뀌어도 가로 크기를 유지하고, `콘텐츠`, `시리즈`, `매거진` 비율에 따라 높이만 변경되어야 한다. - Community Feed는 프로필, 본문, 키워드, 반응 row의 세로 흐름이 유지되어야 한다. - 모든 터치 동작은 컴포넌트 내부에서 목적지를 결정하지 않고 호출부 callback으로 위임한다. --- ## 9. Technical Constraints - 현재 프로젝트는 Android XML Views + Kotlin custom View + ViewBinding/resource 기반이므로 XML layout과 Kotlin custom view 패턴을 우선한다. - 신규 Kotlin 코드는 `app/src/main/java/kr/co/vividnext/sodalive/v2` 하위 패키지에 작성한다. - 재사용 가능한 위젯은 `kr.co.vividnext.sodalive.v2.widget.feed` 하위에 둔다. - 색상, spacing, radius, typography는 기존 `colors.xml`, `dimens.xml`, `typography.xml` token을 우선 재사용한다. - 기존 `AudioContentCardView`, `LiveThumbnail*View`, `CreatorRanking*View`, `ContentRanking*View` 패턴처럼 표시 데이터 contract와 custom view 바인딩을 분리한다. - 이미지 로딩은 컴포넌트 내부에 고정하지 않고 `ImageView`를 노출하거나 adapter/caller가 수행한다. - 기존 화면 파일은 요청 없이 변경하지 않는다. --- ## 10. Metrics - Rank, Live, Content, Community 4개 variant가 Figma 노드와 주요 배치, 색상, typography, spacing 계약을 만족한다. - Rank Feed의 왼쪽 영역은 실제 이미지로 바인딩 가능하다. - Rank Feed의 오른쪽 문구는 호출부에서 변경 가능하고, 순위 텍스트 범위만 `soda_400`으로 표시된다. - Rank Feed root는 세로 가운데 정렬을 유지한다. - Live Feed의 라이브 방송 이름은 `maxLines=1`, `ellipsize=end`이며 상대 시간과 겹치지 않는다. - Content Feed의 콘텐츠명은 한 줄 말줄임 처리된다. - Content Feed의 왼쪽 이미지는 `콘텐츠=1:1`, `시리즈=163:230`, `매거진=163:218` 비율을 만족한다. - Community Feed의 댓글/좋아요 반응 수가 Figma 순서와 스타일로 표시된다. - 가로 스크롤 및 한 줄 다중 표시에서는 Feed root width가 Figma 기준 `374dp`로 적용된다. - 한 줄 1개 표시에서는 Feed root width가 부모 view의 가용 영역 폭으로 적용된다. - 관련 unit test와 Android resource merge/build가 성공한다. --- ## 11. Open Questions - 사용자 요청이 “문서만 작성”이므로 이번 작업에서는 구현 파일을 만들지 않는다. - Content와 Community variant는 별도 추가 조건이 없으므로 Figma `get_design_context`와 screenshot 기준으로 요구사항을 확정한다. - 실제 적용 화면과 API/DTO 매핑은 구현 단계 또는 호출부 작업에서 결정한다. - Community 본문 최대 줄 수는 Figma에서 긴 본문이 여러 줄로 표시되어 있으므로 컴포넌트 기본값은 제한하지 않고, 호출 화면 정책으로 제한할 수 있게 둔다.