22 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 20:3875, 20:3887, 20:3906, 20:3914 기준으로 세로형 poster ratio와 ORIGINAL 태그를 지원하는 재사용 가능한 Series Content Card Component를 추가한다.
Architecture: 기존 AudioContentCardView는 정사각형 오디오 카드 전용으로 유지하고, 시리즈 카드는 별도 SeriesContentCardView와 SeriesContentCardSize로 분리한다. XML 레이아웃은 thumbnail overlay 영역, ORIGINAL tag include, label contents의 공통 구조만 제공하고 Kotlin custom view가 size, tag visibility, 텍스트 바인딩, thumbnail 접근 API를 적용한다.
Tech Stack: Android XML Views, Kotlin custom View, ViewBinding/resource merge, JUnit4 local unit test.
작업 목표
large시리즈 카드는 width163dp, thumbnail163dp x 230dp, label width151dp를 사용한다.small시리즈 카드는 width122dp, thumbnail122dp x 172dp, label width114dp를 사용한다.- thumbnail은
radius_14,centerCrop을 사용한다. - title/creator는 한 줄 말줄임 처리하고 size별 typography를 적용한다.
- ORIGINAL 태그는
ic_series_original아이콘과ORIGINAL텍스트를 포함하고, thumbnail 좌상단 overlay로 표시된다. - 이미지 로딩은 컴포넌트 내부에 고정하지 않고 호출부가 처리한다.
- 기존 오디오 콘텐츠 카드와 기존 화면 파일은 변경하지 않는다.
파일 구조
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardSize.ktlarge,smallsize별 card width, thumbnail width/height, label width, typography contract를 정의한다.
- Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardSizeTest.kt- size별 dimension/style contract를 검증한다.
- Create:
app/src/main/res/drawable/bg_series_content_thumbnail.xml- 14dp corner radius thumbnail 배경을 정의한다.
- Create:
app/src/main/res/drawable/bg_series_original_tag.xmlgray_900배경과 bottom end8dpradius를 가진 ORIGINAL tag 배경을 정의한다.
- Create:
app/src/main/res/layout/view_series_original_tag.xmlic_series_originalicon과ORIGINAL텍스트를 포함하는 101dp x 24dp 태그 layout을 정의한다.
- Create:
app/src/main/res/layout/view_series_content_card.xmlSeriesContentCardView루트, thumbnailImageView, ORIGINAL tag include, label container, titleTextView, creatorTextView를 정의한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardView.kt- size 적용, ORIGINAL tag 표시 여부, 텍스트 바인딩, 썸네일 view 접근 API를 제공한다.
- Modify if missing:
app/src/main/res/drawable-mdpi/ic_series_original.png- 현재 작업트리에 미추적 파일로 존재하므로 구현 전 상태를 확인하고, 없을 때만 디자인 에셋을 추가한다.
- Modify:
docs/plan-task/20260520_시리즈컴포넌트.md- 구현 중 체크박스와 검증 기록을 누적한다.
구현 계획
Task 1: 기존 리소스 및 유사 UI 확인
Files:
-
Read:
docs/prd/20260520_시리즈컴포넌트_prd.md -
Read:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardSize.kt -
Read:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt -
Read:
app/src/main/res/layout/view_audio_content_card.xml -
Read:
app/src/main/res/values/colors.xml -
Read:
app/src/main/res/values/dimens.xml -
Read:
app/src/main/res/values/typography.xml -
Step 1: 기존 custom view와 리소스 패턴 확인
Run: rg -n "AudioContentCard|CapsuleTabBar|setTextAppearance|resources.displayMetrics|radius_14|gray_500|gray_900|Typography_Heading4|Typography_Body1|Typography_Body5|Typography_Caption2" app/src/main docs
Expected: 기존 AudioContentCardView/AudioContentCardSize 패턴, 디자인 토큰, typography 리소스를 확인한다.
- Step 2: ORIGINAL tag icon 상태 확인
Run: rg --files app/src/main/res | rg "ic_series_original"
Expected: app/src/main/res/drawable-mdpi/ic_series_original.png가 출력된다. 출력되지 않으면 Figma asset 또는 제공된 디자인 에셋에서 ic_series_original을 추가한다.
- Step 3: Phosphate font 보유 여부 확인
Run: rg --files app/src/main/res/font | rg -i "phosphate|bold|medium|regular"
Expected: phosphate font가 있으면 ORIGINAL 텍스트에 사용한다. 없으면 기존 @font/bold를 fallback으로 사용하고 검증 기록에 남긴다.
Task 2: SeriesContentCardSize TDD
Files:
-
Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardSizeTest.kt -
Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardSize.kt -
Step 1: RED - size contract 테스트 추가
package kr.co.vividnext.sodalive.v2.widget
import kr.co.vividnext.sodalive.R
import org.junit.Assert.assertEquals
import org.junit.Test
class SeriesContentCardSizeTest {
@Test
fun `large size matches figma contract`() {
assertEquals(163, SeriesContentCardSize.Large.cardWidthDp)
assertEquals(163, SeriesContentCardSize.Large.thumbnailWidthDp)
assertEquals(230, SeriesContentCardSize.Large.thumbnailHeightDp)
assertEquals(151, SeriesContentCardSize.Large.labelWidthDp)
assertEquals(8, SeriesContentCardSize.Large.thumbnailLabelGapDp)
assertEquals(R.style.Typography_Heading4, SeriesContentCardSize.Large.titleStyleRes)
assertEquals(R.style.Typography_Body5, SeriesContentCardSize.Large.creatorStyleRes)
}
@Test
fun `small size matches figma contract`() {
assertEquals(122, SeriesContentCardSize.Small.cardWidthDp)
assertEquals(122, SeriesContentCardSize.Small.thumbnailWidthDp)
assertEquals(172, SeriesContentCardSize.Small.thumbnailHeightDp)
assertEquals(114, SeriesContentCardSize.Small.labelWidthDp)
assertEquals(8, SeriesContentCardSize.Small.thumbnailLabelGapDp)
assertEquals(R.style.Typography_Body1, SeriesContentCardSize.Small.titleStyleRes)
assertEquals(R.style.Typography_Caption2, SeriesContentCardSize.Small.creatorStyleRes)
}
}
- Step 2: RED 실행
Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardSizeTest"
Expected: Unresolved reference 'SeriesContentCardSize'로 실패한다.
- Step 3: GREEN - 최소 size contract 추가
package kr.co.vividnext.sodalive.v2.widget
import androidx.annotation.StyleRes
import kr.co.vividnext.sodalive.R
sealed class SeriesContentCardSize(
val cardWidthDp: Int,
val thumbnailWidthDp: Int,
val thumbnailHeightDp: Int,
val labelWidthDp: Int,
val thumbnailLabelGapDp: Int,
@get:StyleRes val titleStyleRes: Int,
@get:StyleRes val creatorStyleRes: Int
) {
data object Large : SeriesContentCardSize(
cardWidthDp = 163,
thumbnailWidthDp = 163,
thumbnailHeightDp = 230,
labelWidthDp = 151,
thumbnailLabelGapDp = 8,
titleStyleRes = R.style.Typography_Heading4,
creatorStyleRes = R.style.Typography_Body5
)
data object Small : SeriesContentCardSize(
cardWidthDp = 122,
thumbnailWidthDp = 122,
thumbnailHeightDp = 172,
labelWidthDp = 114,
thumbnailLabelGapDp = 8,
titleStyleRes = R.style.Typography_Body1,
creatorStyleRes = R.style.Typography_Caption2
)
}
- Step 4: GREEN 실행
Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardSizeTest"
Expected: BUILD SUCCESSFUL
Task 3: Series content card XML 리소스 추가
Files:
-
Create:
app/src/main/res/drawable/bg_series_content_thumbnail.xml -
Create:
app/src/main/res/drawable/bg_series_original_tag.xml -
Create:
app/src/main/res/layout/view_series_original_tag.xml -
Create:
app/src/main/res/layout/view_series_content_card.xml -
Step 1: thumbnail radius drawable 추가
app/src/main/res/drawable/bg_series_content_thumbnail.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/gray_900" />
<corners android:radius="@dimen/radius_14" />
</shape>
- Step 2: ORIGINAL tag background 추가
app/src/main/res/drawable/bg_series_original_tag.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/gray_900" />
<corners android:bottomRightRadius="@dimen/radius_8" />
</shape>
- Step 3: ORIGINAL tag layout 추가
app/src/main/res/layout/view_series_original_tag.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fl_series_original_tag"
android:layout_width="101dp"
android:layout_height="24dp"
android:background="@drawable/bg_series_original_tag">
<ImageView
android:id="@+id/iv_series_original_icon"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_gravity="start|center_vertical"
android:layout_marginStart="8dp"
android:contentDescription="@null"
android:src="@drawable/ic_series_original" />
<TextView
android:id="@+id/tv_series_original_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="26dp"
android:layout_marginTop="2dp"
android:fontFamily="@font/bold"
android:text="ORIGINAL"
android:textColor="@color/white"
android:textSize="16sp"
tools:ignore="HardcodedText" />
</FrameLayout>
Task 1에서 Phosphate font 리소스가 확인되면 android:fontFamily="@font/bold"를 해당 리소스로 교체한다.
- Step 4: series content card layout 추가
app/src/main/res/layout/view_series_content_card.xml
<?xml version="1.0" encoding="utf-8"?>
<kr.co.vividnext.sodalive.v2.widget.SeriesContentCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:id="@+id/fl_series_thumbnail_container"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/bg_series_content_thumbnail"
android:clipToOutline="true">
<ImageView
android:id="@+id/iv_series_content_thumbnail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@null"
android:scaleType="centerCrop" />
<include
android:id="@+id/include_series_original_tag"
layout="@layout/view_series_original_tag"
android:layout_width="101dp"
android:layout_height="24dp"
android:layout_gravity="top|start" />
</FrameLayout>
<LinearLayout
android:id="@+id/ll_series_content_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tv_series_content_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
tools:text="콘텐츠 제목" />
<TextView
android:id="@+id/tv_series_content_creator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/gray_500"
tools:text="크리에이터 이름" />
</LinearLayout>
</kr.co.vividnext.sodalive.v2.widget.SeriesContentCardView>
Task 4: SeriesContentCardView 구현
Files:
-
Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardView.kt -
Step 1: custom view 추가
SeriesContentCardView는 LinearLayout을 상속하고 @JvmOverloads constructor 패턴을 따른다.
Required API:
fun setSize(size: SeriesContentCardSize)fun setContent(title: String, creatorName: String)fun setOriginalVisible(isVisible: Boolean)fun thumbnailView(): ImageView
Implementation requirements:
-
기본 size는
SeriesContentCardSize.Large를 사용한다. -
orientation = VERTICAL을 보장한다. -
root layout width를 size별
cardWidthDp로 적용한다. -
thumbnail container width/height를 size별
thumbnailWidthDp/thumbnailHeightDp로 적용한다. -
label container width를 size별
labelWidthDp로 적용한다. -
label top margin을 size별
thumbnailLabelGapDp로 적용한다. -
title/creator typography는 size별 style resource를 적용한다.
-
title과 creator 사이 gap은 2dp로 적용한다.
-
ORIGINAL tag는
setOriginalVisible(false)일 때GONE, true일 때VISIBLE로 표시한다. -
thumbnail radius clipping은
clipToOutline또는 기존 프로젝트에서 사용하는 방식으로 14dp radius를 보장한다. -
Step 2: 텍스트 바인딩 구현
setContent(title, creatorName)은 title TextView와 creator TextView에 값을 그대로 바인딩한다. 빈 문자열 보정은 호출부 책임으로 둔다.
- Step 3: 썸네일 바인딩 확장 지점 제공
이미지 로딩 라이브러리를 컴포넌트 내부에 고정하지 않도록 thumbnailView()로 ImageView를 노출한다.
Task 5: 검증 및 문서 기록
Files:
-
Modify:
docs/plan-task/20260520_시리즈컴포넌트.md -
Step 1: 단일 테스트 실행
Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardSizeTest"
Expected: BUILD SUCCESSFUL
- Step 2: LSP 진단 실행
Run: lsp_diagnostics on modified Kotlin/XML files
Expected: 새 오류가 없다. Kotlin/XML LSP가 환경에 없으면 그 사실을 검증 기록에 남긴다.
- Step 3: 리소스/레이아웃 참조 확인
Run: rg -n "SeriesContentCardView|iv_series_content_thumbnail|include_series_original_tag|ic_series_original|clipToOutline=\"true\"|scaleType=\"centerCrop\"|tv_series_content_title|tv_series_content_creator|gray_500|bg_series_content_thumbnail|bg_series_original_tag" app/src/main/res app/src/main/java/kr/co/vividnext/sodalive/v2/widget
Expected: 신규 view, layout id, drawable, icon, text color, thumbnail 속성이 출력된다.
- Step 4: 디버그 빌드 실행
Run: ./gradlew :app:assembleDebug
Expected: BUILD SUCCESSFUL
- Step 5: ViewBinding 생성 확인
Run: rg --files app/build/generated/data_binding_base_class_source_out/debug/out | rg "ViewSeriesContentCardBinding|ViewSeriesOriginalTagBinding"
Expected: ViewSeriesContentCardBinding과 ViewSeriesOriginalTagBinding 생성 파일이 출력된다.
- Step 6: 검증 기록 누적
문서 하단 검증 기록에 실행한 명령, 결과, 빌드 성공 여부를 한국어로 기록한다.
체크리스트
- AC1:
large카드는 width163dp, thumbnail163dp x 230dp, label width151dp를 사용한다.- QA:
SeriesContentCardSizeTest, custom view size 적용 확인
- QA:
- AC2:
small카드는 width122dp, thumbnail122dp x 172dp, label width114dp를 사용한다.- QA:
SeriesContentCardSizeTest, custom view size 적용 확인
- QA:
- AC3: 모든 thumbnail은 radius
14dp,centerCrop을 사용한다.- QA: drawable/custom view clipping, XML
scaleType확인
- QA: drawable/custom view clipping, XML
- AC4: title은 white, creator name은
gray_500이며 둘 다 한 줄 말줄임 처리된다.- QA: XML
textColor,maxLines,ellipsize확인
- QA: XML
- AC5: size별 typography는 large title
Typography.Heading4, creatorTypography.Body5, small titleTypography.Body1, creatorTypography.Caption2를 사용한다.- QA:
SeriesContentCardSizeTest, custom view style 적용 확인
- QA:
- AC6: ORIGINAL 태그는
ic_series_original,ORIGINAL텍스트,gray_900배경, 101dp x 24dp 크기를 사용한다.- QA:
view_series_original_tag.xml, resource reference 확인
- QA:
- AC7: ORIGINAL 태그 표시 여부를 API로 제어할 수 있다.
- QA:
setOriginalVisible(Boolean)구현 확인
- QA:
- AC8: 이미지 로딩 라이브러리를 컴포넌트 내부에 고정하지 않는다.
- QA:
thumbnailView()API 및 의존성 변경 없음 확인
- QA:
- AC9: 기존 오디오 콘텐츠 카드와 기존 화면 파일은 변경하지 않는다.
- QA:
git status --short변경 파일 확인
- QA:
- AC10: 리소스 병합 및 디버그 빌드가 성공한다.
- QA:
./gradlew :app:assembleDebug
- QA:
검증 기록
- 2026-05-20
- 무엇/왜/어떻게: 사용자 요청에 따라 구현 전 PRD와 구현 계획/TASK 문서만 작성했다. Figma
20:3875,20:3887,20:3906,20:3914는 시리즈 콘텐츠 카드와 ORIGINAL 태그 기준으로 문서화했다. - 실행 명령/도구:
Figma_get_design_context(20:3875)Figma_get_design_context(20:3887)Figma_get_design_context(20:3906)Figma_get_design_context(20:3914)Figma_get_screenshot(20:3875)Figma_get_screenshot(20:3887)Figma_get_screenshot(20:3906)Figma_get_screenshot(20:3914)read(docs/agent-guides/workflow-docs-commits.md)read(docs/prd/sample-prd.md)read(docs/prd/20260519_오디오콘텐츠카드컴포넌트_prd.md)read(docs/plan-task/20260519_오디오콘텐츠카드컴포넌트.md)read(app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardSize.kt)read(app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt)read(app/src/main/res/layout/view_audio_content_card.xml)read(app/src/main/res/values/typography.xml)read(app/src/main/res/values/dimens.xml)rg -n "AudioContentCard|CapsuleTabBar|radius_14|gray_500|soda_900|spacing_8|ic_series_original|Typography_Heading|Typography_Body|Typography_Caption" "app/src/main" "docs"git status --short
- 결과:
- PRD 문서는
docs/prd/20260520_시리즈컴포넌트_prd.md에 작성했다. - 계획/TASK 문서는
docs/plan-task/20260520_시리즈컴포넌트.md에 작성했다. - Figma
20:3875는 large 시리즈 카드,20:3887은 small 시리즈 카드,20:3906은 ORIGINAL 태그,20:3914는 ORIGINAL 태그 사용 예시로 정리했다. - 사용자가
20:3096은 오타이고20:3906이 맞다고 정정했으므로, 구현 기준은20:3906으로 확정했다. app/src/main/res/drawable-mdpi/ic_series_original.png는 작업트리에 미추적 파일로 존재함을 확인했으며, 이번 문서 작성 작업에서는 수정하지 않았다.- 코드, 리소스, 레이아웃 구현 파일은 변경하지 않았다.
- 실제 구현과 빌드 검증은 사용자 승인 후 계획 문서 체크리스트에 따라 진행한다.
- PRD 문서는
- 무엇/왜/어떻게: 사용자 요청에 따라 구현 전 PRD와 구현 계획/TASK 문서만 작성했다. Figma
- 2026-05-20
- 무엇/왜/어떻게: 사용자 정정에 따라
20:3096은 오타로 보고20:3906을 ORIGINAL 태그 기준으로 확정한 뒤, 계획 문서에 따라 시리즈 컴포넌트를 구현했다.SeriesContentCardSize로 size contract를 분리하고,SeriesContentCardView에서 카드 폭/썸네일/label/typography/tag visibility를 size별로 적용하도록 했다. - 실행 명령/도구:
rg -n "AudioContentCard|CapsuleTabBar|setTextAppearance|resources.displayMetrics|radius_14|gray_500|gray_900|Typography_Heading4|Typography_Body1|Typography_Body5|Typography_Caption2" app/src/main docsrg --files app/src/main/res | rg "ic_series_original"rg --files app/src/main/res/font | rg -i "phosphate|bold|medium|regular"./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardSizeTest"(RED)./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardSizeTest"(GREEN)lsp_diagnosticson modified Kotlin/XML filesrg -n "SeriesContentCardView|iv_series_content_thumbnail|include_series_original_tag|ic_series_original|clipToOutline=\"true\"|scaleType=\"centerCrop\"|tv_series_content_title|tv_series_content_creator|gray_500|bg_series_content_thumbnail|bg_series_original_tag|setOriginalVisible|thumbnailView|phosphate_solid" app/src/main/res app/src/main/java/kr/co/vividnext/sodalive/v2/widget./gradlew :app:assembleDebugrg --files app/build/generated/data_binding_base_class_source_out/debug/out | rg "ViewSeriesContentCardBinding|ViewSeriesOriginalTagBinding"git status --short
- 결과:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardSize.kt를 추가했다.app/src/main/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardView.kt를 추가했다.app/src/main/res/drawable/bg_series_content_thumbnail.xml을 추가했다.app/src/main/res/drawable/bg_series_original_tag.xml을 추가했다.app/src/main/res/layout/view_series_content_card.xml을 추가했다.app/src/main/res/layout/view_series_original_tag.xml을 추가했다.app/src/test/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardSizeTest.kt를 추가했다.app/src/main/res/drawable-mdpi/ic_series_original.png를 ORIGINAL 태그 아이콘으로 사용했다.- RED 실행은
Unresolved reference 'SeriesContentCardSize'로 실패해 테스트가 신규 contract 부재를 검증함을 확인했다. - GREEN 실행은
BUILD SUCCESSFUL로 완료됐다. - 현재 환경에는 Kotlin/XML LSP 서버가 설정되어 있지 않아
lsp_diagnostics는 실행 불가했다. - 리소스 참조 확인에서
SeriesContentCardView, thumbnail id, ORIGINAL tag include,ic_series_original,clipToOutline,centerCrop, title/creator id,gray_500, series drawable,setOriginalVisible,thumbnailView,phosphate_solid참조를 확인했다. :app:assembleDebug는BUILD SUCCESSFUL로 완료됐다.ViewSeriesContentCardBinding.java와ViewSeriesOriginalTagBinding.java생성 파일을 확인했다.
- 무엇/왜/어떻게: 사용자 정정에 따라