Files
sodalive-android/docs/plan-task/20260527_배너컴포넌트.md

49 KiB

배너 컴포넌트 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Figma 24:5525와 PRD docs/prd/20260527_배너컴포넌트_prd.md 기준으로 자동 전환, 무한 순환, 수동 스와이프, 카운터 오버레이, XML 미리보기를 제공하는 재사용 가능한 Android XML Views 배너 컴포넌트를 추가한다.

Architecture: 기존 BannerViewPager 래핑 없이 kr.co.vividnext.sodalive.v2.widget.banner 하위에 순수 Kotlin 상태/계산 contract와 Android custom view를 분리한다. 배너 UI는 RecyclerView + PagerSnapHelper 기반으로 구현하고, 이미지 로딩과 터치 후 이동은 컴포넌트 내부에서 결정하지 않고 호출부 callback 또는 노출된 ImageView 바인딩으로 위임한다.

Tech Stack: Android XML Views, Kotlin custom View, RecyclerView, PagerSnapHelper, Handler/Runnable 또는 lifecycle-aware timer, ViewBinding/resource merge, JUnit4 local unit test, Robolectric view test.


작업 목표

  • 배너 item은 1:1 비율이며 width/height는 screenWidth - 40dp로 계산한다.
  • BannerView는 XML에서 layout_width="match_parent", layout_height="wrap_content"로 배치해도 화면 폭에 맞는 높이를 자체 계산한다.
  • 현재 배너는 가운데 정렬하고 화면 좌우 20dp 기준 여백을 유지한다.
  • 배너가 2개 이상이면 item 간격 8dp와 좌우 이전/다음 배너 일부 노출을 제공한다.
  • 배너 목록 0개는 숨김, 1개는 단일 이미지, 2개 이상은 카운터/스와이프/자동 전환을 적용한다.
  • 자동 전환 주기는 5초, 전환 애니메이션 시간은 350ms다.
  • 마지막 배너 다음은 첫 번째 배너, 첫 번째 배너 이전은 마지막 배너로 이어지는 무한 순환을 제공한다.
  • 사용자가 직접 스와이프하면 자동 전환 타이머를 초기화한다.
  • 터치 액션은 호출부 콜백으로 위임한다.
  • XML layout editor에서 샘플 이미지/placeholder와 샘플 카운터를 미리 볼 수 있어야 한다.

파일 구조

  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerItem.kt
    • 배너 컴포넌트 전용 UI 모델을 정의한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerLayoutCalculator.kt
    • 화면 폭, 좌우 여백 20dp, item 간격 8dp 기준으로 item size와 peek/padding 값을 계산한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerCounterFormatter.kt
    • 01 / 20 형식의 현재 index/전체 count 문자열을 만든다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerState.kt
    • item count별 표시 모드, 현재 index 보정, 다음/이전 index 계산을 담당한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerAdapter.kt
    • RecyclerView item을 생성하고 이미지 view와 클릭 callback을 연결한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt
    • XML custom view 루트로 RecyclerView, counter, timer, lifecycle start/stop을 조합한다.
  • Create: app/src/main/res/layout/view_banner.xml
    • BannerView 루트, 내부 RecyclerView, 우상단 counter overlay를 정의한다.
  • Create: app/src/main/res/layout/item_banner.xml
    • 1:1 이미지 item layout을 정의한다.
  • Create: app/src/main/res/drawable/bg_banner_counter.xml
    • 반투명 검정 capsule counter 배경을 정의한다.
  • Create if needed: app/src/main/res/drawable/bg_banner_preview_placeholder.xml
    • XML 미리보기용 placeholder 배경을 정의한다. 기존 적절한 placeholder drawable이 있으면 새 파일 대신 재사용한다.
  • Modify: app/src/main/res/values/attrs.xml
    • 없으면 생성하고, XML preview용 bannerPreviewItemCount, bannerPreviewCurrentIndex, bannerPreviewImage 속성을 정의한다.
  • Create: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerLayoutCalculatorTest.kt
    • screenWidth - 40dp, 1:1, 8dp 간격 계산을 검증한다.
  • Create: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerCounterFormatterTest.kt
    • 두 자리 counter 형식을 검증한다.
  • Create: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerStateTest.kt
    • 0/1/2개 이상 표시 모드와 무한 순환 index 계산을 검증한다.
  • Create: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerViewTest.kt
    • Robolectric으로 visibility, counter, adapter click, preview 속성 반영을 검증한다.
  • Modify: docs/agent-guides/build-test-style.md
    • 신규 배너 테스트 단일 실행 예시를 추가한다.
  • Modify: docs/plan-task/20260527_배너컴포넌트.md
    • 구현 중 체크박스와 검증 기록을 누적한다.

구현 계획

Phase 1: 기준 확인 및 구현 경계 확정

  • Task 1.1: PRD와 Figma 기준 재확인

    • 확인 파일: docs/prd/20260527_배너컴포넌트_prd.md
    • Figma 기준: 24:5525
    • 확인 항목: 1:1 item 비율, screenWidth - 40dp, 좌우 20dp, item 간격 8dp, 좌우 peek, radius 14dp, counter 위치 top/right 14dp, counter text 01 / 20.
    • 검증: PRD의 Goals, Visual Requirements, Behavior Requirements, Metrics가 계획의 phase/task에 모두 매핑되는지 확인한다.
  • Task 1.2: 기존 v2 custom view와 테스트 패턴 확인

    • 확인 파일:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedAdapter.kt
      • app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedViewTest.kt
      • app/src/main/res/values/colors.xml
      • app/src/main/res/values/dimens.xml
      • app/src/main/res/values/typography.xml
    • 검증: @JvmOverloads custom view, ImageView 노출, Robolectric inflate test, 기존 token 이름을 확인한다.
  • Task 1.3: RecyclerView 기반 배너 구현 경계 확정

    • 구현 원칙: 기존 BannerViewPager는 래핑하거나 확장하지 않는다.
    • 구현 방식: RecyclerView horizontal layout + PagerSnapHelper + adapter virtual position 또는 index remap으로 무한 순환을 구현한다.
    • 검증: app/build.gradleandroidx.recyclerview:recyclerview 의존성이 있으므로 신규 pager 의존성을 추가하지 않는다.

Phase 2: 순수 Kotlin contract와 단위 테스트

  • Task 2.1: BannerItem 모델 추가

    • 생성 파일: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerItem.kt
    • 요구사항: bannerId: String?, imageUrl: String만 포함한다.
    • 검증: 서버 DTO, 딥링크 타입, 외부 URL 타입을 모델에 포함하지 않는다.
  • Task 2.2: BannerLayoutCalculatorTest 작성

    • 생성 파일: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerLayoutCalculatorTest.kt
    • 테스트 항목:
      • 화면 폭 402dp이면 item width/height는 362dp.
      • 화면 폭 360dp이면 item width/height는 320dp.
      • item spacing은 항상 8dp.
      • horizontal side inset 기준값은 20dp.
    • 실행: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerLayoutCalculatorTest"
    • 기대: 구현 전 실패, 구현 후 성공.
  • Task 2.3: BannerLayoutCalculator 구현

    • 생성 파일: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerLayoutCalculator.kt
    • 요구사항:
      • screenWidthPx, density 또는 dp 변환 가능한 입력을 받아 item width/height를 계산한다.
      • side inset은 20dp, item spacing은 8dp로 고정한다.
      • item height는 item width와 동일하다.
    • 검증: BannerLayoutCalculatorTest가 통과한다.
  • Task 2.4: BannerCounterFormatterTest 작성

    • 생성 파일: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerCounterFormatterTest.kt
    • 테스트 항목:
      • current index 0, count 20이면 01 / 20.
      • current index 9, count 20이면 10 / 20.
      • count 1이어도 formatter 자체는 01 / 01을 만들 수 있지만 view에서는 1개일 때 counter를 숨긴다.
    • 실행: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerCounterFormatterTest"
    • 기대: 구현 전 실패, 구현 후 성공.
  • Task 2.5: BannerCounterFormatter 구현

    • 생성 파일: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerCounterFormatter.kt
    • 요구사항: 현재 index는 0-based 입력을 받고 표시값은 1-based 두 자리 문자열로 변환한다.
    • 검증: BannerCounterFormatterTest가 통과한다.
  • Task 2.6: BannerStateTest 작성

    • 생성 파일: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerStateTest.kt
    • 테스트 항목:
      • count 0은 Hidden 모드다.
      • count 1은 Single 모드이며 swipe/auto/counter 대상이 아니다.
      • count 2 이상은 Carousel 모드다.
      • 마지막 index의 next는 0이다.
      • 첫 번째 index의 previous는 마지막 index다.
      • 목록 갱신으로 current index가 범위를 벗어나면 0으로 보정한다.
    • 실행: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerStateTest"
    • 기대: 구현 전 실패, 구현 후 성공.
  • Task 2.7: BannerState 구현

    • 생성 파일: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerState.kt
    • 요구사항: 표시 모드, 현재 index 보정, next/previous 계산을 Android view 의존성 없이 구현한다.
    • 검증: BannerStateTest가 통과한다.

Phase 3: XML 리소스와 디자인 타임 미리보기

  • Task 3.1: counter 배경 drawable 추가

    • 생성 파일: app/src/main/res/drawable/bg_banner_counter.xml
    • 요구사항:
      • solid color는 #B3000000 또는 동등한 rgba(0,0,0,0.7) 표현을 사용한다.
      • corners radius는 capsule 형태로 충분히 큰 값을 사용한다.
    • 검증: resource merge 시 drawable이 정상 참조된다.
  • Task 3.2: preview placeholder drawable 추가 또는 재사용 결정

    • 생성 후보: app/src/main/res/drawable/bg_banner_preview_placeholder.xml
    • 요구사항:
      • XML layout editor에서 이미지 데이터 없이도 배너 영역이 보인다.
      • 기존 적절한 placeholder drawable이 있으면 새 drawable을 만들지 않고 해당 리소스를 사용한다.
    • 검증: item_banner.xmltools:src 또는 android:background에서 참조 가능하다.
  • Task 3.3: preview attrs 추가

    • 생성 또는 수정 파일: app/src/main/res/values/attrs.xml
    • 추가 속성:
      • bannerPreviewItemCount format integer
      • bannerPreviewCurrentIndex format integer
      • bannerPreviewImage format reference
    • 검증: BannerView에서 context.obtainStyledAttributes(attrs, R.styleable.BannerView)로 읽을 수 있다.
  • Task 3.4: item_banner.xml 추가

    • 생성 파일: app/src/main/res/layout/item_banner.xml
    • 요구사항:
      • root는 FrameLayout.
      • 내부 ImageViewmatch_parent, centerCrop, contentDescription="@null".
      • root와 image는 14dp radius clip 대상이 될 수 있어야 한다.
    • 검증: XML preview에서 정사각형 item 내부 이미지 영역이 확인된다.
  • Task 3.5: view_banner.xml 추가

    • 생성 파일: app/src/main/res/layout/view_banner.xml
    • 요구사항:
      • root는 kr.co.vividnext.sodalive.v2.widget.banner.BannerView.
      • 내부 RecyclerView는 horizontal carousel 용도로 배치하고 clipToPadding=false, clipChildren=false를 적용한다.
      • counter container는 우상단 14dp margin, bg_banner_counter, padding horizontal 6dp, vertical 4dp.
      • counter는 현재 index, /, total count를 분리된 TextView 또는 span으로 표현해 현재 index white, 나머지 gray_400을 적용한다.
      • tools: 속성으로 샘플 counter와 preview image가 보이게 한다.
    • 검증: XML preview에서 배너 형태, radius, counter 위치가 확인된다.

Phase 4: Adapter와 View 구현

  • Task 4.1: BannerAdapter 구현

    • 생성 파일: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerAdapter.kt
    • 요구사항:
      • RecyclerView.Adapter로 구현한다.
      • item count 2개 이상에서는 virtual position을 실제 item index로 remap해 무한 순환에 필요한 충분한 scroll range를 제공한다.
      • item count 0/1개에서는 실제 count만 반환한다.
      • ImageView는 호출부가 이미지 로딩을 연결할 수 있도록 bind callback으로 전달한다.
      • item click은 현재 BannerItem을 호출부 callback으로 전달한다.
    • 검증: adapter 단위 또는 view test에서 click callback이 현재 item을 전달한다.
  • Task 4.2: BannerView 기본 구조 구현

    • 생성 파일: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt
    • 요구사항:
      • @JvmOverloads constructor(context, attrs, defStyleAttr) 패턴을 사용한다.
      • view_banner.xml을 inflate하고 내부 RecyclerView/counter view를 찾는다.
      • setItems(items: List<BannerItem>) API를 제공한다.
      • setOnBannerClickListener(listener: ((BannerItem) -> Unit)?) API를 제공한다.
      • setOnBindBannerImage(listener: ((ImageView, BannerItem) -> Unit)?) 또는 동등한 이미지 바인딩 API를 제공한다.
    • 검증: 빈 목록, 단일 목록, 복수 목록을 설정해 visibility와 counter 상태가 바뀐다.
  • Task 4.3: layout size와 peek 적용

    • 수정 파일: BannerView.kt, BannerAdapter.kt
    • 요구사항:
      • view width를 기준으로 item size를 width - 40dp로 계산한다.
      • layout_height="wrap_content"이면 view 자체 measured height도 width - 40dp 기준으로 결정한다.
      • item layout params는 width/height 동일 값으로 설정한다.
      • RecyclerView horizontal padding 또는 item decoration을 조합해 현재 item 가운데 정렬, item 간격 8dp, 좌우 peek 노출을 적용한다.
      • 단일 item은 peek 없이 가운데 정렬한다.
    • 검증: BannerLayoutCalculatorTestBannerViewTest에서 item size, spacing, counter visibility를 확인한다.
  • Task 4.4: counter 표시 구현

    • 수정 파일: BannerView.kt
    • 요구사항:
      • 0개/1개에서는 counter를 숨긴다.
      • 2개 이상에서는 BannerCounterFormatter 결과를 표시한다.
      • 현재 index는 white, slash와 total count는 gray_400을 적용한다.
      • scroll settle 후 실제 item index에 맞춰 counter를 갱신한다.
    • 검증: 빠른 scroll 후에도 counter index가 현재 item과 일치한다.
  • Task 4.5: radius clipping 구현

    • 수정 파일: BannerView.kt 또는 item view holder
    • 요구사항:
      • 배너 image/root에 radius_14 outline provider를 적용한다.
      • Android XML preview와 runtime 모두에서 corner radius가 적용된다.
    • 검증: Robolectric에서는 outline provider 설정 여부를 확인하고, 수동 확인에서는 radius clipping을 확인한다.

Phase 5: 자동 전환, 수동 스와이프, lifecycle

  • Task 5.1: 자동 전환 timer 구현

    • 수정 파일: BannerView.kt
    • 요구사항:
      • 배너 2개 이상일 때만 5초 timer를 시작한다.
      • timer tick 시 다음 배너 방향으로 이동한다.
      • 전환 애니메이션 시간은 350ms를 기준으로 한다.
    • 검증: 테스트 가능한 timer wrapper를 두거나 Robolectric scheduler로 자동 전환 호출을 검증한다.
  • Task 5.2: 무한 순환 이동 구현

    • 수정 파일: BannerView.kt, BannerAdapter.kt, BannerState.kt
    • 요구사항:
      • 마지막 배너 다음 방향은 첫 번째 배너로 이어진다.
      • 첫 번째 배너 이전 방향은 마지막 배너로 이어진다.
      • virtual position을 사용하는 경우 실제 index와 counter index가 항상 일치한다.
    • 검증: BannerStateTestBannerViewTest에서 next/previous remap을 확인한다.
  • Task 5.3: 수동 스와이프 timer reset 구현

    • 수정 파일: BannerView.kt
    • 요구사항:
      • RecyclerView scroll state가 사용자 drag로 시작되면 자동 전환 예약을 취소한다.
      • scroll settle 후 현재 시점부터 5초 timer를 다시 예약한다.
    • 검증: 수동 scroll 이벤트 시 timer reset 메서드가 호출되는지 view test에서 확인한다.
  • Task 5.4: attach/detach lifecycle 정리

    • 수정 파일: BannerView.kt
    • 요구사항:
      • onAttachedToWindow()에서 조건에 맞으면 timer를 시작한다.
      • onDetachedFromWindow()에서 timer callback을 제거한다.
      • item count가 0/1개로 바뀌면 timer를 중지한다.
    • 검증: detach 후 pending callback이 남지 않도록 테스트하거나 코드 리뷰 체크리스트에 명시한다.

Phase 6: 상태별 view test와 회귀 방지

  • Task 6.1: BannerViewTest 기본 상태 테스트 작성

    • 생성 파일: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerViewTest.kt
    • 테스트 항목:
      • empty items 설정 시 root visibility는 GONE.
      • one item 설정 시 root visibility는 VISIBLE, counter는 GONE.
      • two items 설정 시 counter는 VISIBLE.
    • 실행: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"
  • Task 6.2: BannerViewTest preview 속성 테스트 작성

    • 수정 파일: BannerViewTest.kt
    • 테스트 항목:
      • edit mode 또는 preview 속성 처리 경로에서 sample count/current index가 counter에 반영된다.
      • preview image 속성이 있으면 placeholder/image view에 적용된다.
    • 실행: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"
  • Task 6.3: BannerViewTest click callback 테스트 작성

    • 수정 파일: BannerViewTest.kt
    • 테스트 항목:
      • 첫 번째 배너 클릭 시 첫 번째 BannerItem이 callback으로 전달된다.
      • click listener가 null이어도 클릭으로 crash가 발생하지 않는다.
    • 실행: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"
  • Task 6.4: BannerViewTest layout 계산 연결 테스트 작성

    • 수정 파일: BannerViewTest.kt
    • 테스트 항목:
      • 측정된 width 기준 item layout params가 정사각형으로 설정된다.
      • width=match_parent, height=wrap_content 조합으로 측정하면 root measured height가 width - 40dp와 일치한다.
      • 복수 item일 때 item decoration 또는 padding이 8dp spacing과 좌우 peek 조건을 반영한다.
    • 실행: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"
  • Task 6.5: BannerView wrap_content 높이 측정 보완

    • 수정 파일: BannerView.kt, BannerViewTest.kt
    • 요구사항:
      • 호출부 XML에서 layout_width="match_parent", layout_height="wrap_content"를 사용해도 화면 폭별로 배너 높이가 자동 계산된다.
      • root measured height는 measured width 기준 width - 40dp와 일치한다.
      • 고정 362dp 같은 화면별 상수 height를 호출부에 요구하지 않는다.
      • 기존 item size, 좌우 20dp inset, 8dp spacing, counter 위치 동작은 유지한다.
    • 테스트 항목:
      • BannerViewTest에서 width 402dp, height wrap_content 측정 시 measured height가 362dp인지 확인한다.
      • width 360dp, height wrap_content 측정 시 measured height가 320dp인지 확인한다.
      • 명시 height가 들어온 경우 기존 부모 측정 제약을 깨지 않는지 확인한다.
    • 실행: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"

Phase 7: 문서, 검증, 마무리

  • Task 7.1: 테스트 실행 예시 문서 갱신

    • 수정 파일: docs/agent-guides/build-test-style.md
    • 추가 내용: 배너 컴포넌트 단일 테스트 실행 예시.
    • 예시 명령: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.*"
    • 검증: 기존 문서 형식과 중복되지 않게 최소 변경한다.
  • Task 7.2: 단위 테스트 실행

    • 실행:
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerLayoutCalculatorTest"
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerCounterFormatterTest"
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerStateTest"
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"
    • 기대 결과: 모두 BUILD SUCCESSFUL.
  • Task 7.3: 리소스/빌드 검증

    • 실행:
      • ./gradlew :app:assembleDebug
      • 필요 시 ./gradlew :app:ktlintCheck
    • 기대 결과: resource merge, Kotlin compile, ktlint가 성공한다.
  • Task 7.4: 수동 확인 항목 기록

    • 확인 항목:
      • XML layout editor에서 배너 preview, radius, counter 위치가 보인다.
      • 1개 배너는 정사각형 단일 이미지로 가운데 정렬된다.
      • 2개 이상 배너는 좌우 이전/다음 배너 일부가 보인다.
      • 자동 전환은 5초 주기로 동작한다.
      • 수동 스와이프 후 자동 전환 timer가 다시 5초부터 시작한다.
      • 마지막/첫 번째 경계에서 무한 순환이 자연스럽다.
    • 기록 위치: 이 문서 하단 검증 기록 섹션.

Phase 8: PRD 불일치 및 검증 갭 보완

  • Task 8.1: counter 시각 형식 회귀 테스트 작성

    • 수정 파일: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerViewTest.kt
    • 대상 리스크: counter가 PRD의 01 / 20 형식, 14sp, line-height 1.45 기준과 다르게 표시될 수 있다.
    • 테스트 항목:
      • 복수 배너 counter가 실제 조합 기준으로 01 / 02처럼 slash 앞뒤 공백을 포함해 보이는지 검증한다.
      • 현재 index는 white, separator/total count는 gray_400을 유지하는지 검증한다.
      • counter text style이 PRD 기준 14sp token 또는 동등한 typography로 적용되는지 검증한다.
    • 실행: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"
    • 기대: 구현 전에는 counter 공백 또는 typography mismatch로 실패하고, 구현 후 성공한다.
  • Task 8.2: counter 시각 형식 구현 보완

    • 수정 파일: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt, app/src/main/res/layout/view_banner.xml
    • 요구사항:
      • counter가 실제 렌더링에서 01 / 20 형태로 표시되도록 separator 주변 공백 또는 동등한 간격을 명시한다.
      • 현재 index는 white, separator와 total count는 gray_400을 유지한다.
      • PRD 기준 14sp, line-height 1.45에 맞는 기존 typography token을 우선 사용한다.
    • 검증: Task 8.1 테스트가 통과한다.
  • Task 8.3: lifecycle stop timer 중지 테스트 작성

    • 수정 파일: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerViewTest.kt
    • 대상 리스크: view가 detach되지 않고 lifecycle만 stop되는 경우 자동 전환 timer가 계속 동작할 수 있다.
    • 테스트 항목:
      • BannerView가 attached 상태에서 lifecycle ON_STOP을 받으면 pending auto scroll callback이 제거된다.
      • lifecycle ON_START 또는 재개 조건에서 배너가 2개 이상이면 auto scroll timer가 다시 예약된다.
    • 실행: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"
    • 기대: 구현 전에는 lifecycle stop 시나리오가 실패하고, 구현 후 성공한다.
  • Task 8.4: lifecycle stop timer 중지 구현

    • 수정 파일: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt
    • 요구사항:
      • ViewTreeLifecycleOwner 또는 기존 프로젝트 관례에 맞는 lifecycle 관찰 방식으로 stop 시 timer를 중지한다.
      • detach cleanup은 기존대로 유지한다.
      • lifecycle owner가 없는 preview/test 경로에서 crash가 발생하지 않아야 한다.
    • 검증: Task 8.3 테스트와 기존 attach/detach cleanup 테스트가 모두 통과한다.
  • Task 8.5: 목록 갱신 시 첫 번째 배너 재시작 테스트 작성

    • 수정 파일: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerViewTest.kt
    • 대상 리스크: setItems()가 기존 currentIndex를 유지해 PRD의 "목록 갱신 후 2개 이상이면 첫 번째 배너 기준으로 다시 시작" 요구와 다를 수 있다.
    • 테스트 항목:
      • 현재 counter가 두 번째 이상인 상태에서 새 복수 배너 목록을 setItems()로 설정하면 counter가 01로 초기화된다.
      • 목록 갱신 후 auto scroll timer도 첫 번째 배너 기준으로 다시 시작된다.
    • 실행: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"
    • 기대: 구현 전에는 기존 index 유지로 실패하고, 구현 후 성공한다.
  • Task 8.6: 목록 갱신 시 첫 번째 배너 재시작 구현

    • 수정 파일: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt
    • 요구사항:
      • 새 목록이 2개 이상이면 currentIndex와 adapter position을 첫 번째 배너 기준으로 초기화한다.
      • 새 목록이 0개이면 숨김과 timer 중지를 유지한다.
      • 새 목록이 1개이면 첫 번째 이미지만 표시하고 counter/auto scroll을 비활성화한다.
    • 검증: Task 8.5 테스트와 기존 0/1/2개 상태 테스트가 모두 통과한다.
  • Task 8.7: 수동 경계 스와이프 및 빠른 연속 스와이프 View 테스트 보강

    • 수정 파일: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerViewTest.kt
    • 대상 리스크: 순수 index 계산은 검증됐지만 실제 RecyclerView snap/counter가 수동 경계 스와이프와 빠른 연속 스와이프에서 일치하는지 직접 검증하지 않는다.
    • 테스트 항목:
      • 첫 번째 배너에서 이전 방향으로 이동한 뒤 idle 상태가 되면 마지막 배너 counter가 표시된다.
      • 마지막 배너에서 다음 방향으로 이동한 뒤 idle 상태가 되면 첫 번째 배너 counter가 표시된다.
      • 빠른 연속 position 변경 후 idle 상태가 되면 snap된 adapter position의 실제 index와 counter가 일치한다.
    • 실행: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"
    • 기대: 구현이 이미 충족하면 GREEN을 확인하고, 실패하면 Task 8.8에서 최소 수정한다.
  • Task 8.8: 수동 스와이프 counter 동기화 보완

    • 수정 파일: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt, 필요 시 app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerAdapter.kt
    • 요구사항:
      • SCROLL_STATE_IDLE 시 snap된 adapter position을 기준으로 실제 item index와 counter를 항상 동기화한다.
      • 첫 번째/마지막 경계에서 수동 이전/다음 이동이 끊김 없이 순환한다.
      • 빠른 연속 스와이프 후에도 counter가 실제 표시 item과 일치한다.
    • 검증: Task 8.7 테스트와 기존 자동 전환, drag reset, 무한 순환 테스트가 모두 통과한다.
  • Task 8.9: Phase 8 최종 검증 실행 및 기록

    • 실행:
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.*"
      • ./gradlew :app:assembleDebug
      • ./gradlew :app:ktlintCheck
    • 기록 위치: 이 문서 하단 검증 기록 섹션.
    • 기대 결과: 모두 BUILD SUCCESSFUL이며, 실제 기기 또는 Android Studio XML layout editor 확인이 가능하면 counter 공백/typography, preview, peek, radius를 수동 확인한다.

Phase 9: 실제 기기 시각 보정

  • Task 9.1: carousel item 중심 쏠림 회귀 테스트 작성

    • 수정 파일: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerViewTest.kt
    • 대상 리스크: item 간격을 오른쪽 offset으로만 적용하면 PagerSnapHelper의 decorated center 기준과 실제 이미지 중심이 달라져 현재 banner가 왼쪽으로 치우쳐 보일 수 있다.
    • 테스트 항목:
      • 복수 banner에서 item 사이 실제 간격은 8dp를 유지한다.
      • spacing decoration은 좌우 대칭이거나, snap 기준 중심과 item image 중심이 일치하는 값을 반환한다.
      • 단일 banner에서는 기존처럼 spacing을 적용하지 않는다.
    • 실행: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"
    • 기대: 현재 구현이 오른쪽 offset만 사용하면 RED가 되고, 대칭 offset 또는 동등한 중심 보정 구현 후 GREEN이 된다.
  • Task 9.2: carousel item 중심 쏠림 보정 구현

    • 수정 파일: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt
    • 요구사항:
      • 복수 banner item spacing 8dp는 유지한다.
      • BannerSpacingDecoration은 item 오른쪽에만 spacing을 몰아주지 않고 좌우 4dp씩 나누거나, PagerSnapHelper 기준 중심과 실제 이미지 중심이 일치하는 방식으로 구현한다.
      • 단일 item은 spacing 0을 유지한다.
    • 검증: Task 9.1 테스트와 기존 layout size, wrap content, counter, auto scroll 테스트가 모두 통과한다.
  • Task 9.3: counter item 기준 위치 회귀 테스트 작성

    • 수정 파일: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerViewTest.kt
    • 대상 리스크: counter가 BannerView root 우상단 14dp에 붙으면 Figma의 "중앙 banner item 내부 우상단 14dp" 위치보다 오른쪽으로 치우친다.
    • 테스트 항목:
      • width 402dp, side inset 20dp 기준 counter root end margin은 34dp다.
      • top margin은 14dp를 유지한다.
      • width가 바뀌어도 counter는 현재 banner item의 우측 경계에서 14dp 안쪽에 위치한다.
    • 실행: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"
    • 기대: 현재 구현이 root end 14dp이면 RED가 되고, item 기준 end 34dp 적용 후 GREEN이 된다.
  • Task 9.4: counter item 기준 위치 보정 구현

    • 수정 파일: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt, 필요 시 app/src/main/res/layout/view_banner.xml
    • 요구사항:
      • counter top margin은 Figma 기준 14dp를 유지한다.
      • counter end margin은 sideInsetDp + 14dp로 계산해 현재 banner item 내부 우상단 기준에 맞춘다.
      • view_banner.xml에는 기본값을 유지하되, runtime layout size 적용 시 동적으로 보정한다.
    • 검증: Task 9.3 테스트와 기존 counter 형식/색상/typography 테스트가 모두 통과한다.
  • Task 9.5: 실제 기기 수동 확인 기록

    • 확인 항목:
      • 현재 banner image가 화면 중앙에 정렬되어 보인다.
      • 좌우 이전/다음 banner 일부가 대칭적으로 노출된다.
      • counter가 중앙 banner item 내부 우상단 14dp 위치에 보인다.
    • 기록 위치: 이 문서 하단 검증 기록 섹션.

Phase 10: 초기 표시 상태 중심 정렬 보완

  • Task 10.1: 최초 렌더링 중심 정렬 회귀 테스트 작성

    • 수정 파일: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerViewTest.kt
    • 대상 리스크: Phase 9 보정 후 자동 또는 수동 슬라이드가 1회 이상 발생하면 중심이 맞지만, 최초 표시 상태에서는 초기 adapter position 적용 시점과 item size/padding/decoration 적용 시점 차이로 현재 banner가 약간 우측으로 치우쳐 보일 수 있다.
    • 테스트 항목:
      • 복수 banner를 setItems()로 설정하고 최초 measure/layout이 끝난 직후, 별도 scroll 이벤트 없이 현재 banner item 중심이 BannerView 중심과 일치한다.
      • 최초 표시 상태의 좌우 peek 노출량이 대칭이다.
      • 자동/수동 슬라이드 이후 중심 정렬 테스트는 기존 Phase 9 결과를 유지한다.
    • 실행: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"
    • 기대: 현재 구현이 최초 layout 직후 우측 쏠림을 재현하면 RED가 되고, 초기 scroll position 재적용 또는 layout 완료 후 snap 보정 구현 후 GREEN이 된다.
  • Task 10.2: 최초 렌더링 중심 정렬 보정 구현

    • 수정 파일: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt
    • 요구사항:
      • setItems() 시점에 계산한 currentAdapterPosition이 item size, RecyclerView padding, spacing decoration 적용 이후에도 유지되도록 보정한다.
      • onSizeChanged() 또는 layout size 적용 이후 복수 banner이면 최초 표시 position을 다시 적용하거나, RecyclerView.post { ... } 등 기존 구조에 맞는 최소 보정으로 첫 frame의 중심 정렬을 맞춘다.
      • 보정은 최초 표시 또는 size 변경 시점에만 적용하고, 사용자가 수동으로 이동한 이후의 현재 위치를 불필요하게 되돌리지 않는다.
      • 기존 자동 전환, 수동 스와이프, 무한 순환, counter 동기화 동작을 변경하지 않는다.
    • 검증: Task 10.1 테스트와 Phase 9 중심/peek/counter 테스트, 기존 banner 전체 테스트가 모두 통과한다.
  • Task 10.3: 실제 기기 수동 확인 기록

    • 확인 항목:
      • 앱 진입 후 슬라이드 전 최초 banner가 화면 중앙에 보인다.
      • 자동 또는 수동 슬라이드 후에도 중앙 정렬이 유지된다.
      • 좌우 peek 노출량이 최초 표시와 슬라이드 이후 모두 대칭이다.
    • 기록 위치: 이 문서 하단 검증 기록 섹션.

검증 기록

  • 2026-05-27 Phase 1 완료: PRD docs/prd/20260527_배너컴포넌트_prd.md의 Goals, Visual Requirements, Behavior Requirements, Metrics가 계획의 Phase 2~7 task로 매핑됨을 확인했다. Figma 기준 24:5525에서 계획에 명시된 1:1, screenWidth - 40dp, 좌우 20dp, item 간격 8dp, 좌우 peek, radius_14, counter top/right 14dp, 01 / 20 형식과 불일치하는 항목은 발견하지 못했다.
  • 2026-05-27 Phase 1 패턴 확인: AudioContentCardView@JvmOverloads custom view, thumbnailView(): ImageView 노출, radius_14 outline clipping 패턴을 확인했다. FeedAdapterRecyclerView.Adapter, click callback, image binding callback 패턴과 FeedViewTest의 Robolectric XML inflate test 패턴을 확인했다.
  • 2026-05-27 Phase 1 리소스/의존성 확인: colors.xmlwhite, gray_400, dimens.xmlspacing_8, spacing_14, spacing_20, radius_14, typography.xmlTypography.Body5 medium 14sp 토큰이 있음을 확인했다. attrs.xml은 현재 없어 Phase 3에서 생성 대상이다.
  • 2026-05-27 Phase 1 구현 경계 확정: app/build.gradleandroidx.recyclerview:recyclerview:1.4.0, JUnit4, Robolectric 의존성이 이미 있으므로 신규 pager 의존성은 추가하지 않는다. 기존 com.github.zhpanvip:bannerviewpager:3.5.7 의존성은 존재하지만 이번 컴포넌트는 PRD/계획대로 래핑하거나 확장하지 않는다.
  • 2026-05-27 Phase 2 진행: BannerItem, BannerLayoutCalculator, BannerCounterFormatter, BannerState 순수 Kotlin contract 파일과 BannerLayoutCalculatorTest, BannerCounterFormatterTest, BannerStateTest 단위 테스트 파일이 존재함을 확인했다.
  • 2026-05-27 Phase 2 검증 준비: 순수 Kotlin contract 테스트는 BannerLayoutCalculatorTest, BannerCounterFormatterTest, BannerStateTest 범위로 실행했다.
  • 2026-05-27 Phase 2 검증 완료: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerLayoutCalculatorTest" --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerCounterFormatterTest" --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerStateTest" 실행 결과 BUILD SUCCESSFUL을 확인했다.
  • 2026-05-27 RED: BannerLayoutCalculatorTest, BannerCounterFormatterTest, BannerStateTest는 최초 실행 시 compileDebugUnitTestKotlin에서 BannerLayoutCalculator, BannerCounterFormatter, BannerState, BannerDisplayMode 미해결 참조로 실패했다.
  • 2026-05-27 GREEN: 순수 Kotlin contract 구현 후 대상 순수 테스트가 통과했다.
  • 2026-05-27 LSP 진단: kotlin-lsp가 설치되어 있지 않아 Phase 2 Kotlin 파일의 LSP diagnostics는 사용할 수 없었다.
  • 2026-05-27 Phase 3 리소스 추가: bg_banner_counter.xml, bg_banner_preview_placeholder.xml, attrs.xml, item_banner.xml, view_banner.xml을 추가했다. 기존 bg_placeholder.xml은 큰 vector icon이라 배너 영역 배경으로 재사용하지 않고, radius_14 기반 preview placeholder를 별도 생성했다.
  • 2026-05-27 Phase 3 counter/view 구성: counter 배경은 #B3000000 capsule로 만들고, view_banner.xmlBannerView root, horizontal RecyclerView, 우상단 counter overlay를 포함하도록 구성했다. counter text는 현재 index white, separator/total gray_400 TextView로 분리했다.
  • 2026-05-27 Phase 3 warning 조치: android:clipToOutline은 minSdk 23 기준 API 31 warning 대상이라 item_banner.xml에서 사용하지 않았다. 실제 radius clipping은 계획된 Phase 4.5 Kotlin outline provider 구현에서 처리한다.
  • 2026-05-27 Phase 4 RED: BannerViewTest 추가 후 최초 실행 시 view_banner.xml이 참조하는 BannerView production class가 없어 compileDebugJavaWithJavac에서 실패함을 확인했다.
  • 2026-05-27 Phase 4 구현: BannerAdapter는 0/1개 실제 count, 2개 이상 virtual position remap, image bind callback, item click callback, image radius clipping을 구현했다. BannerView는 XML child 연결, RecyclerView + PagerSnapHelper, setItems, click/image bind API, visibility, layout size/padding/spacing, counter 표시를 구현했다.
  • 2026-05-27 Phase 4 RED/GREEN: 단일 item spacing 회귀 테스트 배너 view는 단일 item이면 item 간격을 적용하지 않는다를 추가해 RED를 확인한 뒤, 단일 item에서는 spacing을 0, 복수 item에서는 8dp로 적용하도록 수정해 GREEN을 확인했다.
  • 2026-05-27 Phase 4 검증: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.*"./gradlew :app:assembleDebug를 순차 실행해 모두 BUILD SUCCESSFUL을 확인했다. Kotlin LSP는 kotlin-lsp가 설치되어 있지 않아 diagnostics를 실행할 수 없었다.
  • 2026-05-27 Phase 4 리뷰 수정: 리뷰에서 BannerView(context) 실제 사용 경로가 내부 layout을 inflate하지 않는 문제가 지적되어, view_banner.xml<merge>로 변경하고 BannerView 생성 시 내부 layout을 inflate하도록 수정했다. 배너 view는 코드로 생성해도 내부 layout을 포함한다 테스트로 RED를 확인한 뒤 GREEN을 확인했다.
  • 2026-05-27 Phase 4 리뷰 수정 후 검증: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.*"./gradlew :app:assembleDebug를 다시 실행해 모두 BUILD SUCCESSFUL을 확인했다.
  • 2026-05-28 Phase 5 RED: BannerViewTest에 자동 전환, 수동 drag timer reset, detach cleanup 테스트를 추가한 뒤 production 변경 전 실행해 자동 전환/drag reset 시나리오 실패를 확인했다.
  • 2026-05-28 Phase 5 구현: BannerView에 main looper Handler/Runnable 기반 5초 자동 전환, SCROLL_STATE_DRAGGING 중 timer 취소, SCROLL_STATE_IDLE 후 재예약, onAttachedToWindow 시작, onDetachedFromWindow callback 제거를 추가했다. BannerAdapter.startPosition()으로 carousel 시작 위치를 virtual range 중앙에 맞췄다.
  • 2026-05-28 Phase 5 검증: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest", ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.*", ./gradlew :app:assembleDebug 실행 결과 모두 BUILD SUCCESSFUL을 확인했다.
  • 2026-05-28 Phase 4.3 보완 RED: BannerViewTestlayout_width="match_parent", layout_height="wrap_content" 측정 시 width 402dp는 height 362dp, width 360dp는 height 320dp가 되어야 한다는 회귀 테스트를 추가했고, production 변경 전 ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest" 실행에서 두 테스트 실패를 확인했다.
  • 2026-05-28 Phase 4.3 보완 구현: BannerView.onMeasure()에서 height measure spec이 EXACTLY가 아닐 때 measured width 기준 BannerLayoutCalculator 결과로 root measured height를 계산하도록 수정했다. 명시 height가 들어온 경우에는 부모 측정 제약을 유지한다.
  • 2026-05-28 Phase 4.3 보완 검증: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest", ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.*", ./gradlew :app:assembleDebug 실행 결과 모두 BUILD SUCCESSFUL을 확인했다.
  • 2026-05-28 Phase 6 RED: BannerViewTest에 preview count/current index, preview image, null click listener 테스트를 추가했다. Production 변경 전 ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest" 실행에서 preview counter assertion 실패와 preview item holder 미생성 실패를 확인했다.
  • 2026-05-28 Phase 6 구현: BannerView에서 bannerPreviewItemCount, bannerPreviewCurrentIndex, bannerPreviewImage attrs를 읽어 preview sample item, counter, image resource를 반영하도록 보완했다. TypedArray.use는 Robolectric SDK 28에서 AutoCloseable 캐스팅 문제가 있어 try/finally recycle()로 처리했다. BannerAdapter는 preview image resource가 지정된 경우 bind 시 ImageView에 적용하도록 보완했다.
  • 2026-05-28 Phase 6 GREEN: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest" 실행 결과 BUILD SUCCESSFUL을 확인했다.
  • 2026-05-28 Phase 6 최종 검증: click callback 보강 테스트 추가 후 ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest", ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.*", ./gradlew :app:assembleDebug 실행 결과 모두 BUILD SUCCESSFUL을 확인했다.
  • 2026-05-28 Phase 7 문서 갱신: docs/agent-guides/build-test-style.md의 단일 테스트 실행 섹션에 배너 컴포넌트 테스트 예시 ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.*"를 추가했다.
  • 2026-05-28 Phase 7 단위 테스트 검증: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerLayoutCalculatorTest" --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerCounterFormatterTest" --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerStateTest" --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.*" 실행 결과 모두 BUILD SUCCESSFUL을 확인했다.
  • 2026-05-28 Phase 7 리소스/빌드/스타일 검증: ./gradlew :app:assembleDebug./gradlew :app:ktlintCheck 실행 결과 모두 BUILD SUCCESSFUL을 확인했다. ktlintCheck 과정에서 배너 파일의 wrapping/blank line 지적과 테스트 소스의 unused import 지적을 최소 수정했다.
  • 2026-05-28 Phase 7 수동 확인 기록: 실제 기기/Android Studio XML layout editor는 이 환경에서 열 수 없어 직접 시각 확인은 수행하지 못했다. 대신 Robolectric BannerViewTestassembleDebug로 preview attrs 반영, counter 표시, radius outline provider, 0/1/2개 상태, 좌우 peek/spacing 계산, 자동 전환 5초, 수동 drag timer reset, attach/detach cleanup, 무한 순환 index 동작을 검증했다.
  • 2026-05-28 Phase 8 RED: BannerViewTest에 counter 01 / 02 공백/색상/14sp, lifecycle ON_STOP/ON_START, setItems() 목록 갱신 첫 번째 배너 재시작, 수동 경계/빠른 위치 변경 idle counter 동기화 테스트를 추가했다. Production 변경 전 ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest" 실행 결과 의도한 4개 시나리오가 실패함을 확인했다.
  • 2026-05-28 Phase 8 구현: view_banner.xml counter typography를 Typography.Body5로 맞추고, BannerView에서 separator를 /로 표시하도록 보완했다. findViewTreeLifecycleOwner()DefaultLifecycleObserver로 lifecycle stop/start timer 제어를 추가하고, detach 시 observer를 제거한다. setItems()는 새 목록을 첫 번째 배너 기준으로 초기화하며, idle 시 snap view가 없으면 현재 adapter position 기준으로 counter를 동기화한다.
  • 2026-05-28 Phase 8 GREEN: 구현 후 ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest" 실행 결과 BUILD SUCCESSFUL을 확인했다.
  • 2026-05-28 Phase 8 최종 검증: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.*", ./gradlew :app:assembleDebug, ./gradlew :app:ktlintCheck 실행 결과 모두 BUILD SUCCESSFUL을 확인했다. 실제 기기/Android Studio XML layout editor는 이 환경에서 열 수 없어 직접 시각 확인은 수행하지 못했고, Robolectric/빌드 검증으로 counter 공백/typography, lifecycle stop/start, 목록 갱신 초기화, 수동 경계/연속 위치 변경 counter 동기화를 확인했다.
  • 2026-05-28 Phase 9 RED: BannerViewTest에 carousel item spacing 좌우 대칭 검증과 counter item 내부 우상단 기준 margin 검증을 추가했다. Production 변경 전 ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest" 실행 결과 배너 view는 정사각형 item 크기와 좌우 padding 및 간격을 적용한다, 배너 view는 carousel item 간격을 좌우 대칭으로 적용한다, 배너 view counter는 현재 item 내부 우상단 기준 margin을 적용한다 3개 테스트 실패를 확인했다.
  • 2026-05-28 Phase 9 구현: BannerSpacingDecoration이 복수 banner spacing 8dp를 좌우 4dp씩 대칭 적용하도록 수정했다. BannerView.applyLayoutSize()에서 counter end margin을 sideInset 20dp + item 내부 margin 14dp = 34dp로 동적 보정해 root 우측이 아닌 현재 banner item 내부 우상단 기준에 맞췄다.
  • 2026-05-28 Phase 9 GREEN: 구현 후 ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest" 실행 결과 BUILD SUCCESSFUL을 확인했다.
  • 2026-05-28 Phase 9 수동 확인 기록: 실제 기기/Android Studio XML layout editor는 이 환경에서 열 수 없어 직접 시각 확인은 수행하지 못했다. 대신 Robolectric 검증으로 현재 banner 중심 기준 spacing 좌우 대칭, 단일 item spacing 0 유지, counter top 14dp, counter end 34dp 적용을 확인했다.
  • 2026-05-28 Phase 9 최종 검증: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.*", ./gradlew :app:assembleDebug, ./gradlew :app:ktlintCheck 실행 결과 모두 BUILD SUCCESSFUL을 확인했다.
  • 2026-05-28 Phase 10 RED: BannerViewTest에 최초 measure/layout 직후 현재 adapter position의 item 중심이 BannerView 중심과 일치하는지 검증하는 회귀 테스트를 추가했다. Production 보정 전 ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest" 실행 결과 배너 view는 최초 layout 직후 현재 item 중심을 view 중심에 맞춘다 테스트 실패를 확인했다.
  • 2026-05-28 Phase 10 구현: setItems() 이후 최초 layout size 적용 시점에만 현재 adapter position을 다시 정렬하도록 shouldAlignCurrentPositionAfterLayout 플래그를 추가했다. 복수 banner에서는 LinearLayoutManager.scrollToPositionWithOffset()에 decoration 절반 offset을 적용해 초기 child content 중심을 view 중심에 맞추고, 수동 이동 이후에는 해당 보정을 반복하지 않도록 했다.
  • 2026-05-28 Phase 10 GREEN: 구현 후 ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest" 실행 결과 BUILD SUCCESSFUL을 확인했다.
  • 2026-05-28 Phase 10 수동 확인 기록: 실제 기기/Android Studio XML layout editor는 이 환경에서 열 수 없어 직접 시각 확인은 수행하지 못했다. 대신 Robolectric 검증으로 최초 layout 직후 현재 item 중심과 view 중심 일치, 좌우 20dp 기준 위치, 기존 자동/수동 슬라이드 테스트 유지를 확인했다.
  • 2026-05-28 Phase 10 최종 검증: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.*", ./gradlew :app:assembleDebug, ./gradlew :app:ktlintCheck 실행 결과 모두 BUILD SUCCESSFUL을 확인했다.