# PRD: 배너 컴포넌트 ## 1. Overview Figma `24:5525` 디자인을 기준으로 이미지 배너를 자동 전환, 수동 스와이프, 카운터 오버레이, 터치 콜백과 함께 제공하는 Android XML Views 기반 재사용 컴포넌트를 개발한다. --- ## 2. Problem - 여러 화면에서 이미지 배너를 사용할 때 자동 전환 주기, 전환 시간, 카운터 표기, 무한 순환 동작이 화면별로 달라질 수 있다. - 배너 터치 동작은 화면마다 달라질 수 있으므로 UI 컴포넌트가 이동 목적지나 액션 타입을 직접 알면 재사용성이 낮아진다. - XML 레이아웃에서 배너를 배치할 때 런타임 데이터가 없어도 레이아웃 에디터에서 크기와 기본 형태를 미리 확인할 수 있어야 한다. - 기존 배너 구현은 특정 화면과 외부 라이브러리 사용 방식에 묶여 있어 신규 `v2` 공용 컴포넌트 계약으로 바로 재사용하기 어렵다. --- ## 3. Goals - `kr.co.vividnext.sodalive.v2.widget.banner` 하위에 신규 공용 배너 컴포넌트를 정의한다. - Figma `24:5525` 기준의 이미지 카드, `14dp` radius, 우상단 `01 / 20` 형태의 카운터 오버레이를 제공한다. - 배너 아이템은 `1:1` 비율로 표시하고, 아이템 width는 화면 width에서 좌우 여백 `20dp`씩을 제외한 값으로 계산한다. - 배너가 여러 개일 때 아이템 사이 간격은 `8dp`이며, 현재 배너 좌우 빈 공간에는 이전/다음 배너가 약간 보여야 한다. - 배너 목록이 2개 이상이면 5초마다 자동으로 다음 배너로 전환한다. - 배너 전환 애니메이션 시간은 350ms를 기준으로 한다. - 마지막 배너에서 동일한 방향으로 스와이프하면 첫 번째 배너가 보이는 무한 순환 동작을 제공한다. - 사용자가 직접 스와이프하면 자동 전환 타이머를 초기화한다. - 호출부는 배너 아이템별 터치 액션을 콜백으로 연결할 수 있어야 한다. - XML에서 배치할 때 디자인 타임 미리보기가 가능해야 한다. - 개발자는 화면별 고정 높이 상수를 계산하지 않고 `match_parent` width와 `wrap_content` height 조합으로 배너를 배치할 수 있어야 한다. --- ## 4. Non-Goals - 이번 범위에서는 서버 API, DTO, 딥링크 스키마, 이벤트 추적 스키마를 정의하지 않는다. - 기존 화면의 배너를 신규 컴포넌트로 일괄 교체하지 않는다. - Compose 컴포넌트 또는 Compose Theme를 추가하지 않는다. - Yandex 광고 배너와 같은 광고 SDK 배너는 이번 컴포넌트 범위에 포함하지 않는다. - 기존 `com.zhpan.bannerview.BannerViewPager` 래핑 가능성은 검토하지 않는다. - Figma에 없는 제목, 설명, CTA 버튼, 페이지 dot indicator, skeleton loading, shimmer, 별도 pressed animation은 추가하지 않는다. --- ## 5. Target Users - 앱 내 배너를 확인하고 터치하는 사용자. - XML 레이아웃에서 재사용 가능한 배너 UI를 배치하고 데이터와 클릭 동작을 연결하는 Android 개발자. --- ## 6. User Stories - 사용자는 여러 개의 배너를 자동 전환과 스와이프로 자연스럽게 탐색하고 싶다. - 사용자는 마지막 배너 이후에도 같은 방향 스와이프로 첫 번째 배너를 이어서 볼 수 있기를 기대한다. - 사용자는 현재 보고 있는 배너가 전체 중 몇 번째인지 우상단 카운터로 확인하고 싶다. - 개발자는 화면별 이동 정책을 컴포넌트 밖에서 콜백으로 연결하고 싶다. - 개발자는 XML 작성 시 실제 데이터가 없어도 배너의 크기와 형태를 미리 확인하고 싶다. --- ## 7. Core Features ### Banner Component Figma `24:5525` 기준의 이미지 배너를 XML + Kotlin custom view 기반 공용 컴포넌트로 제공한다. #### Figma Reference - Banner: 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-5525&m=dev #### Visual Requirements - 기본 배너 비율은 `1:1` 정사각형이다. - 기본 배너 item width는 `screenWidth - 40dp`로 계산한다. - 배너 item height는 계산된 item width와 동일하게 적용한다. - `BannerView` root height가 `wrap_content`이면 measured width를 기준으로 자체 높이를 `screenWidth - 40dp`에 맞춰 측정한다. - 현재 배너 item은 부모 영역의 가로 가운데에 정렬한다. - 현재 배너 item은 화면 왼쪽에서 `20dp`, 오른쪽에서 `20dp` 떨어진 위치를 기준으로 표시한다. - 배너가 여러 개이면 item 사이 간격은 `8dp`다. - 배너가 여러 개일 때 item 간격은 시각 중심이 밀리지 않도록 좌우 대칭 offset으로 적용한다. 예: 실제 item 사이 간격 `8dp`를 유지하되 각 item의 좌우 offset을 `4dp`씩 나누거나, 동등하게 `PagerSnapHelper` 기준 중심과 이미지 중심이 일치하는 방식을 사용한다. - 배너가 여러 개일 때 최초 렌더링 직후, 자동/수동 슬라이드가 한 번도 발생하기 전에도 현재 banner item은 화면 중앙에 정렬되어야 한다. - 배너가 여러 개이면 현재 배너 좌우 빈 공간에 이전/다음 배너 일부가 보여야 한다. - 배너 이미지는 전체 영역을 채우며 `centerCrop` 기준으로 표시한다. - 배너 root와 이미지 영역은 `radius_14` 또는 `14dp` radius를 적용한다. - 카운터는 배너 우상단에 배치한다. - 카운터 위치는 상단 `14dp`, 오른쪽 `14dp`를 기준으로 한다. - 카운터의 오른쪽 `14dp` 기준은 `BannerView` root가 아니라 현재 표시 중인 중앙 banner item의 우측 경계 기준이다. `BannerView`가 `match_parent`이고 item이 좌우 `20dp` inset 안쪽에 있으면 root 기준 end margin은 `20dp + 14dp`가 되어야 한다. - 카운터 배경은 `rgba(0,0,0,0.7)`에 가까운 반투명 검정, capsule radius를 적용한다. - 카운터 내부 padding은 horizontal `6dp`, vertical `4dp`를 기준으로 한다. - 카운터 텍스트는 Pretendard Medium, `14sp`, line-height `1.45`를 기준으로 한다. - 현재 index는 white, 구분자 `/`와 전체 count는 `gray_400` 계열 색상을 사용한다. - 현재 index와 전체 count는 두 자리 형식으로 표시한다. 예: `01 / 20`. #### Behavior Requirements - 배너 목록이 0개이면 컴포넌트 영역은 표시하지 않는다. - 배너 목록이 1개이면 자동 전환, 수동 스와이프, 카운터를 사용하지 않고 단일 이미지만 표시한다. - 배너 목록이 1개이면 좌우 peek 영역 없이 단일 배너를 가운데 정렬한다. - 배너 목록이 2개 이상이면 카운터를 표시하고 자동 전환을 시작한다. - 배너 목록이 2개 이상이면 이전/다음 배너가 좌우에 일부 보이는 상태를 유지한다. - 자동 전환 주기는 5초다. - 전환 애니메이션 시간은 350ms다. - 자동 전환 방향은 다음 배너 방향으로 고정한다. - 마지막 배너 자동 전환 후에는 첫 번째 배너가 표시되어야 한다. - 마지막 배너에서 사용자가 다음 방향으로 스와이프하면 첫 번째 배너가 표시되어야 한다. - 첫 번째 배너에서 사용자가 이전 방향으로 스와이프하면 마지막 배너가 표시되어야 한다. - 사용자가 직접 스와이프하면 현재 시점부터 자동 전환 타이머를 다시 5초로 초기화한다. - 컴포넌트가 화면에서 detach되거나 lifecycle이 정지되면 자동 전환 타이머가 중지되어야 한다. - 컴포넌트가 다시 표시되고 배너 목록이 2개 이상이면 자동 전환 타이머를 재개할 수 있어야 한다. #### Data Contract Requirements - 배너 컴포넌트 전용 UI 모델을 정의한다. - 최소 데이터는 다음 정보를 포함한다. - `bannerId`: 호출부에서 식별에 사용할 수 있는 선택적 id. - `imageUrl`: 배너 이미지 URL. - 서버 응답 DTO, 딥링크 타입, 콘텐츠 상세 타입, 외부 URL 타입은 배너 컴포넌트 모델에 포함하지 않는다. - 배너 터치 시 컴포넌트는 현재 배너 UI 모델을 호출부 콜백으로 전달한다. - 이미지 로딩 실패, placeholder, retry 정책은 호출 화면 또는 이미지 로딩 계층 정책을 따른다. #### XML Preview Requirements - XML layout editor에서 배너 컴포넌트의 기본 형태를 미리 볼 수 있어야 한다. - XML layout에서 `layout_width="match_parent"`, `layout_height="wrap_content"`로 선언해도 배너가 화면 폭에 맞는 정사각형 높이로 표시되어야 한다. - 디자인 타임에는 샘플 이미지 또는 placeholder 배경과 샘플 카운터가 표시되어야 한다. - `tools:` 속성 또는 커스텀 preview 속성으로 샘플 count와 current index를 확인할 수 있어야 한다. - 런타임 데이터가 없어도 XML 미리보기에서 배너 높이, radius, 카운터 위치가 확인되어야 한다. #### Edge Cases - `imageUrl`이 비어 있으면 호출부 이미지 로딩 정책에 따라 placeholder 또는 빈 이미지로 표시한다. - 배너 목록 갱신 시 현재 index가 새 목록 범위를 벗어나면 첫 번째 배너로 보정한다. - 배너 목록 갱신 후 0개가 되면 컴포넌트 영역을 숨기고 자동 전환 타이머를 중지한다. - 배너 목록 갱신 후 1개가 되면 첫 번째 이미지만 표시하고 자동 전환 타이머를 중지한다. - 배너 목록 갱신 후 2개 이상이 되면 첫 번째 배너 기준으로 카운터와 자동 전환을 다시 시작한다. - 빠른 연속 스와이프 중에도 카운터는 실제 표시 중인 배너 index와 일치해야 한다. - 터치 콜백이 등록되지 않은 경우 배너 터치가 앱을 크래시시키지 않아야 한다. --- ## 8. UX / UI Expectations - 배너는 이미지 중심 컴포넌트이며 텍스트 정보는 카운터만 표시한다. - 배너 radius와 카운터 위치는 Figma와 일관되게 유지한다. - 배너는 화면 좌우 `20dp` 여백을 기준으로 가운데 정렬되고, 정사각형 비율을 유지해야 한다. - 배너 높이는 호출부가 화면별 상수 dp 값을 지정하지 않아도 컴포넌트가 width 기반으로 결정해야 한다. - 여러 배너를 표시할 때는 `8dp` 간격과 좌우 peek 노출로 다음/이전 콘텐츠가 있음을 사용자가 알 수 있어야 한다. - 카운터는 이미지 위에서 읽히되 배너 콘텐츠를 과도하게 가리지 않아야 한다. - 자동 전환은 사용자의 수동 스와이프보다 우선하지 않아야 하며, 사용자가 조작한 직후에는 타이머가 초기화되어야 한다. - 무한 순환은 사용자가 끝에 도달했다는 끊김 없이 자연스럽게 동작해야 한다. - 배너 터치 가능 여부는 호출부 콜백 등록 여부와 화면 정책을 따른다. --- ## 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/widget/banner` 하위 패키지에 작성한다. - 색상, spacing, radius, typography는 기존 `colors.xml`, `dimens.xml`, `typography.xml` token을 우선 재사용한다. - 기존 `BannerViewPager` 래핑 없이 신규 컴포넌트 자체 구현으로 진행한다. - 이미지 로딩 라이브러리를 컴포넌트 내부에 고정하지 않고, 호출부가 이미지 로딩을 연결할 수 있는 `ImageView` 접근 또는 바인딩 계약을 제공한다. - 자동 전환 타이머는 view lifecycle에 맞춰 누수 없이 시작/중지되어야 한다. - 기존 화면 파일은 요청 없이 변경하지 않는다. --- ## 10. Metrics - Figma `24:5525` 기준의 `14dp` radius와 우상단 카운터가 구현된다. - 배너 item은 `1:1` 비율이며 width와 height가 `screenWidth - 40dp`로 계산된다. - `BannerView`는 `layout_width="match_parent"`, `layout_height="wrap_content"` 선언에서 기기 폭에 따라 적절한 높이로 측정된다. - 현재 배너는 가운데 정렬되고 화면 좌우에 각각 `20dp` 기준 여백을 가진다. - 배너 목록 2개 이상에서는 item 간격 `8dp`와 좌우 이전/다음 배너 일부 노출이 적용된다. - 배너 목록 2개 이상에서도 현재 banner item의 이미지 중심과 snap 기준 중심이 일치하며, item spacing 때문에 이미지가 좌우 어느 한쪽으로 치우쳐 보이지 않는다. - 배너 목록 2개 이상에서 최초 표시 상태와 슬라이드 이후 상태 모두 현재 banner item이 동일한 중심 정렬 기준을 만족한다. - 카운터는 중앙 banner item 내부 우상단 `14dp` 위치에 표시되며, root 기준 우상단이 아니라 item 기준 우상단과 일치한다. - 배너 목록 0개에서는 컴포넌트 영역이 숨겨진다. - 배너 목록 1개에서는 단일 이미지만 표시되고 자동 전환, 스와이프, 카운터가 비활성화된다. - 배너 목록 2개 이상에서는 5초 자동 전환과 350ms 전환 애니메이션이 동작한다. - 마지막 배너 다음에는 첫 번째 배너가 표시되고, 첫 번째 배너 이전에는 마지막 배너가 표시된다. - 수동 스와이프 후 자동 전환 타이머가 5초로 초기화된다. - 배너 터치 시 호출부 콜백으로 현재 배너 아이템이 전달된다. - XML layout editor에서 배너 기본 형태와 샘플 카운터를 미리 볼 수 있다. - 관련 unit test 또는 Robolectric/view 로직 테스트와 Android resource merge/build가 성공한다. --- ## 11. Open Questions - 사용자 요청이 “PRD 문서 작성”이므로 이번 작업에서는 구현 파일과 계획/TASK 문서를 만들지 않는다. - 실제 이미지 placeholder 리소스는 구현 계획 단계에서 기존 리소스 재사용 여부를 확인해 결정한다.