Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
30 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.kt01 / 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.xmlBannerView루트, 내부 RecyclerView, 우상단 counter overlay를 정의한다.
- Create:
app/src/main/res/layout/item_banner.xml1: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속성을 정의한다.
- 없으면 생성하고, XML preview용
- Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerLayoutCalculatorTest.ktscreenWidth - 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:1item 비율,screenWidth - 40dp, 좌우20dp, item 간격8dp, 좌우 peek, radius14dp, counter 위치top/right 14dp, counter text01 / 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.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedAdapter.ktapp/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedViewTest.ktapp/src/main/res/values/colors.xmlapp/src/main/res/values/dimens.xmlapp/src/main/res/values/typography.xml
- 검증:
@JvmOverloadscustom view,ImageView노출, Robolectric inflate test, 기존 token 이름을 확인한다.
- 확인 파일:
-
Task 1.3: RecyclerView 기반 배너 구현 경계 확정
- 구현 원칙: 기존
BannerViewPager는 래핑하거나 확장하지 않는다. - 구현 방식:
RecyclerViewhorizontal layout +PagerSnapHelper+ adapter virtual position 또는 index remap으로 무한 순환을 구현한다. - 검증:
app/build.gradle에androidx.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, count20이면01 / 20. - current index
9, count20이면10 / 20. - count
1이어도 formatter 자체는01 / 01을 만들 수 있지만 view에서는 1개일 때 counter를 숨긴다.
- current index
- 실행:
./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으로 보정한다.
- count 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 형태로 충분히 큰 값을 사용한다.
- solid color는
- 검증: 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.xml의tools:src또는android:background에서 참조 가능하다.
- 생성 후보:
-
Task 3.3: preview attrs 추가
- 생성 또는 수정 파일:
app/src/main/res/values/attrs.xml - 추가 속성:
bannerPreviewItemCountformatintegerbannerPreviewCurrentIndexformatintegerbannerPreviewImageformatreference
- 검증:
BannerView에서context.obtainStyledAttributes(attrs, R.styleable.BannerView)로 읽을 수 있다.
- 생성 또는 수정 파일:
-
Task 3.4:
item_banner.xml추가- 생성 파일:
app/src/main/res/layout/item_banner.xml - 요구사항:
- root는
FrameLayout. - 내부
ImageView는match_parent,centerCrop,contentDescription="@null". - root와 image는
14dpradius clip 대상이 될 수 있어야 한다.
- root는
- 검증: 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는 우상단
14dpmargin,bg_banner_counter, padding horizontal6dp, vertical4dp. - counter는 현재 index,
/, total count를 분리된 TextView 또는 span으로 표현해 현재 index white, 나머지gray_400을 적용한다. tools:속성으로 샘플 counter와 preview image가 보이게 한다.
- root는
- 검증: 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 없이 가운데 정렬한다.
- view width를 기준으로 item size를
- 검증:
BannerLayoutCalculatorTest와BannerViewTest에서 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_14outline provider를 적용한다. - Android XML preview와 runtime 모두에서 corner radius가 적용된다.
- 배너 image/root에
- 검증: 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가 항상 일치한다.
- 검증:
BannerStateTest와BannerViewTest에서 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.
- empty items 설정 시 root visibility는
- 실행:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"
- 생성 파일:
-
Task 6.2:
BannerViewTestpreview 속성 테스트 작성- 수정 파일:
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:
BannerViewTestclick callback 테스트 작성- 수정 파일:
BannerViewTest.kt - 테스트 항목:
- 첫 번째 배너 클릭 시 첫 번째
BannerItem이 callback으로 전달된다. - click listener가 null이어도 클릭으로 crash가 발생하지 않는다.
- 첫 번째 배너 클릭 시 첫 번째
- 실행:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"
- 수정 파일:
-
Task 6.4:
BannerViewTestlayout 계산 연결 테스트 작성- 수정 파일:
BannerViewTest.kt - 테스트 항목:
- 측정된 width 기준 item layout params가 정사각형으로 설정된다.
width=match_parent,height=wrap_content조합으로 측정하면 root measured height가width - 40dp와 일치한다.- 복수 item일 때 item decoration 또는 padding이
8dpspacing과 좌우 peek 조건을 반영한다.
- 실행:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"
- 수정 파일:
-
Task 6.5:
BannerViewwrap_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, 좌우
20dpinset,8dpspacing, counter 위치 동작은 유지한다.
- 호출부 XML에서
- 테스트 항목:
BannerViewTest에서 width402dp, heightwrap_content측정 시 measured height가362dp인지 확인한다.- width
360dp, heightwrap_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초부터 시작한다.
- 마지막/첫 번째 경계에서 무한 순환이 자연스럽다.
- 기록 위치: 이 문서 하단
검증 기록섹션.
- 확인 항목:
검증 기록
- 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, countertop/right 14dp,01 / 20형식과 불일치하는 항목은 발견하지 못했다. - 2026-05-27 Phase 1 패턴 확인:
AudioContentCardView의@JvmOverloadscustom view,thumbnailView(): ImageView노출,radius_14outline clipping 패턴을 확인했다.FeedAdapter의RecyclerView.Adapter, click callback, image binding callback 패턴과FeedViewTest의 Robolectric XML inflate test 패턴을 확인했다. - 2026-05-27 Phase 1 리소스/의존성 확인:
colors.xml에white,gray_400,dimens.xml에spacing_8,spacing_14,spacing_20,radius_14,typography.xml에Typography.Body5medium 14sp 토큰이 있음을 확인했다.attrs.xml은 현재 없어 Phase 3에서 생성 대상이다. - 2026-05-27 Phase 1 구현 경계 확정:
app/build.gradle에androidx.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 배경은
#B3000000capsule로 만들고,view_banner.xml은BannerViewroot, horizontalRecyclerView, 우상단 counter overlay를 포함하도록 구성했다. counter text는 현재 indexwhite, separator/totalgray_400TextView로 분리했다. - 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이 참조하는BannerViewproduction 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 looperHandler/Runnable기반 5초 자동 전환,SCROLL_STATE_DRAGGING중 timer 취소,SCROLL_STATE_IDLE후 재예약,onAttachedToWindow시작,onDetachedFromWindowcallback 제거를 추가했다.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:
BannerViewTest에layout_width="match_parent",layout_height="wrap_content"측정 시 width402dp는 height362dp, width360dp는 height320dp가 되어야 한다는 회귀 테스트를 추가했고, 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,bannerPreviewImageattrs를 읽어 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을 확인했다.