Files
sodalive-android/docs/plan-task/20260519_오디오콘텐츠카드컴포넌트.md

18 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:3800, 20:3818, 20:3829 기준으로 동일 형태와 3개 크기 변형을 갖는 재사용 가능한 Audio Content Card Component를 추가한다.

Architecture: XML 레이아웃 view_audio_content_card.xml은 썸네일과 label contents의 공통 구조만 제공한다. Kotlin custom view AudioContentCardViewAudioContentCardSize size contract를 적용해 카드 폭, 썸네일 크기, label 폭, typography, gap을 변경한다.

Tech Stack: Android XML Views, Kotlin custom View, ViewBinding/resource merge, JUnit4 local unit test.


작업 목표

  • Figma 3개 오디오 콘텐츠 카드 디자인을 Android XML/Kotlin 재사용 컴포넌트로 구현한다.
  • 세 컴포넌트는 구조는 동일하고 크기만 다르므로 하나의 컴포넌트와 size contract로 관리한다.
  • 기존 화면 일괄 적용은 제외하고 컴포넌트 추가로 범위를 제한한다.

파일 구조

  • Create: app/src/main/res/layout/view_audio_content_card.xml
    • AudioContentCardView 루트, thumbnail ImageView, label container, title TextView, creator TextView를 정의한다.
  • Create: app/src/main/res/drawable/bg_audio_content_card_thumbnail.xml
    • 14dp corner radius thumbnail clipping 배경 또는 outline 기준 리소스를 정의한다. 실제 outline clipping은 custom view에서 처리할 수 있다.
  • Modify: app/src/main/res/values/dimens.xml
    • 필요한 경우 spacing_2, spacing_11 및 카드 전용 size dimen을 최소 추가한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardSize.kt
    • large, medium, small size별 dimension/style contract를 정의한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt
    • size 적용, 텍스트 바인딩, 썸네일 view 접근 API를 제공한다.
  • Create: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardSizeTest.kt
    • size별 card width, thumbnail size, label width, typography contract를 검증한다.
  • Modify: docs/plan-task/20260519_오디오콘텐츠카드컴포넌트.md
    • 구현 중 체크박스와 검증 기록을 누적한다.

구현 계획

Task 1: 기존 리소스 및 유사 UI 확인

Files:

  • Read: app/src/main/res/values/colors.xml

  • Read: app/src/main/res/values/dimens.xml

  • Read: app/src/main/res/values/typography.xml

  • Read: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/CapsuleTabBarView.kt

  • Step 1: 기존 v2 custom view 패턴 확인

Run: rg -n "class .*View @JvmOverloads|setTextAppearance|resources.getDimensionPixelSize" app/src/main/java/kr/co/vividnext/sodalive/v2/widget

Expected: CapsuleTabBarView 등 기존 custom view 구현 스타일을 확인한다.

  • Step 2: 필수 디자인 토큰 확인

Run: rg -n "radius_14|spacing_8|gray_500|white|Typography\.Heading4|Typography\.Body1|Typography\.Body5|Typography\.Caption2" app/src/main/res/values

Expected: radius, color, typography 토큰이 존재함을 확인한다.

  • Step 3: 추가 dimen 필요 여부 확인

Run: rg -n "spacing_2|spacing_11|185dp|163dp|151dp|122dp|114dp" app/src/main/res/values app/src/main/res/layout

Expected: 없는 값은 카드 전용 dimen 또는 직접값 중 하나로 최소 추가한다.

Task 2: AudioContentCardSize TDD

Files:

  • Create: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardSizeTest.kt

  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardSize.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 AudioContentCardSizeTest {

    @Test
    fun `large size matches figma contract`() {
        assertEquals(185, AudioContentCardSize.Large.cardWidthDp)
        assertEquals(185, AudioContentCardSize.Large.thumbnailSizeDp)
        assertEquals(185, AudioContentCardSize.Large.labelWidthDp)
        assertEquals(11, AudioContentCardSize.Large.thumbnailLabelGapDp)
        assertEquals(R.style.Typography_Heading4, AudioContentCardSize.Large.titleStyleRes)
        assertEquals(R.style.Typography_Body5, AudioContentCardSize.Large.creatorStyleRes)
    }

    @Test
    fun `medium size matches figma contract`() {
        assertEquals(163, AudioContentCardSize.Medium.cardWidthDp)
        assertEquals(163, AudioContentCardSize.Medium.thumbnailSizeDp)
        assertEquals(151, AudioContentCardSize.Medium.labelWidthDp)
        assertEquals(8, AudioContentCardSize.Medium.thumbnailLabelGapDp)
        assertEquals(R.style.Typography_Heading4, AudioContentCardSize.Medium.titleStyleRes)
        assertEquals(R.style.Typography_Body5, AudioContentCardSize.Medium.creatorStyleRes)
    }

    @Test
    fun `small size matches figma contract`() {
        assertEquals(122, AudioContentCardSize.Small.cardWidthDp)
        assertEquals(122, AudioContentCardSize.Small.thumbnailSizeDp)
        assertEquals(114, AudioContentCardSize.Small.labelWidthDp)
        assertEquals(8, AudioContentCardSize.Small.thumbnailLabelGapDp)
        assertEquals(R.style.Typography_Body1, AudioContentCardSize.Small.titleStyleRes)
        assertEquals(R.style.Typography_Caption2, AudioContentCardSize.Small.creatorStyleRes)
    }
}
  • Step 2: RED 실행

Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.AudioContentCardSizeTest"

Expected: Unresolved reference 'AudioContentCardSize'로 실패한다.

  • Step 3: GREEN - 최소 size contract 추가
package kr.co.vividnext.sodalive.v2.widget

import androidx.annotation.StyleRes
import kr.co.vividnext.sodalive.R

sealed class AudioContentCardSize(
    val cardWidthDp: Int,
    val thumbnailSizeDp: Int,
    val labelWidthDp: Int,
    val thumbnailLabelGapDp: Int,
    @StyleRes val titleStyleRes: Int,
    @StyleRes val creatorStyleRes: Int
) {
    data object Large : AudioContentCardSize(
        cardWidthDp = 185,
        thumbnailSizeDp = 185,
        labelWidthDp = 185,
        thumbnailLabelGapDp = 11,
        titleStyleRes = R.style.Typography_Heading4,
        creatorStyleRes = R.style.Typography_Body5
    )

    data object Medium : AudioContentCardSize(
        cardWidthDp = 163,
        thumbnailSizeDp = 163,
        labelWidthDp = 151,
        thumbnailLabelGapDp = 8,
        titleStyleRes = R.style.Typography_Heading4,
        creatorStyleRes = R.style.Typography_Body5
    )

    data object Small : AudioContentCardSize(
        cardWidthDp = 122,
        thumbnailSizeDp = 122,
        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.AudioContentCardSizeTest"

Expected: BUILD SUCCESSFUL

Task 3: Audio content card XML 리소스 추가

Files:

  • Create: app/src/main/res/layout/view_audio_content_card.xml

  • Create: app/src/main/res/drawable/bg_audio_content_card_thumbnail.xml

  • Modify: app/src/main/res/values/dimens.xml if needed

  • Step 1: thumbnail radius drawable 추가

app/src/main/res/drawable/bg_audio_content_card_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: audio content card layout 추가

app/src/main/res/layout/view_audio_content_card.xml

<?xml version="1.0" encoding="utf-8"?>
<kr.co.vividnext.sodalive.v2.widget.AudioContentCardView 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">

    <ImageView
        android:id="@+id/iv_audio_content_thumbnail"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@drawable/bg_audio_content_card_thumbnail"
        android:clipToOutline="true"
        android:contentDescription="@null"
        android:scaleType="centerCrop" />

    <LinearLayout
        android:id="@+id/ll_audio_content_label"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_audio_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_audio_content_creator"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:maxLines="1"
            android:textColor="@color/gray_500"
            tools:text="크리에이터 이름" />
    </LinearLayout>
</kr.co.vividnext.sodalive.v2.widget.AudioContentCardView>
  • Step 3: 추가 dimen 최소화

필요한 dimension은 AudioContentCardSize의 dp contract에서 변환해 적용한다. XML에서 직접 필요한 값이 생길 때만 dimens.xml에 최소 추가한다.

Task 4: AudioContentCardView 구현

Files:

  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt

  • Step 1: custom view 추가

AudioContentCardViewLinearLayout을 상속하고 @JvmOverloads constructor 패턴을 따른다.

Required API:

  • fun setSize(size: AudioContentCardSize)
  • fun setContent(title: String, creatorName: String)
  • fun thumbnailView(): ImageView

Implementation requirements:

  • 기본 size는 AudioContentCardSize.Medium을 사용한다.

  • orientation = VERTICAL을 보장한다.

  • root layout width를 size별 cardWidthDp로 적용한다.

  • thumbnail width/height를 size별 thumbnailSizeDp로 적용한다.

  • label container width를 size별 labelWidthDp로 적용한다.

  • label top margin을 size별 thumbnailLabelGapDp로 적용한다.

  • title/creator typography는 size별 style resource를 적용한다.

  • title과 creator 사이 gap은 2dp로 적용한다.

  • thumbnail radius clipping은 ViewOutlineProvider 또는 기존 프로젝트에서 사용하는 방식으로 14dp radius를 보장한다.

  • Step 2: 텍스트 바인딩 구현

setContent(title, creatorName)은 title TextView와 creator TextView에 값을 그대로 바인딩한다. 빈 문자열 보정은 호출부 책임으로 둔다.

  • Step 3: 썸네일 바인딩 확장 지점 제공

이미지 로딩 라이브러리를 컴포넌트 내부에 고정하지 않도록 thumbnailView()ImageView를 노출한다.

Task 5: 검증 및 문서 기록

Files:

  • Modify: docs/plan-task/20260519_오디오콘텐츠카드컴포넌트.md

  • Step 1: 단일 테스트 실행

Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.AudioContentCardSizeTest"

Expected: BUILD SUCCESSFUL

  • Step 2: LSP 진단 실행

Run: lsp_diagnostics on modified Kotlin/XML files

Expected: 새 오류가 없다. XML LSP가 환경에 없으면 그 사실을 검증 기록에 남긴다.

  • Step 3: 디버그 빌드 실행

Run: ./gradlew :app:assembleDebug

Expected: BUILD SUCCESSFUL

  • Step 4: ViewBinding 생성 확인

Run: rg --files app/build/generated/data_binding_base_class_source_out/debug/out | rg "ViewAudioContentCardBinding"

Expected: ViewAudioContentCardBinding 생성 파일이 출력된다.

  • Step 5: 검증 기록 누적

문서 하단 검증 기록에 실행한 명령, 결과, 빌드 성공 여부를 한국어로 기록한다.

체크리스트

  • AC1: large 카드는 width 185dp, thumbnail 185dp x 185dp, label width 185dp를 사용한다.
    • QA: AudioContentCardSizeTest, custom view size 적용 확인
  • AC2: medium 카드는 width 163dp, thumbnail 163dp x 163dp, label width 151dp를 사용한다.
    • QA: AudioContentCardSizeTest, custom view size 적용 확인
  • AC3: small 카드는 width 122dp, thumbnail 122dp x 122dp, label width 114dp를 사용한다.
    • QA: AudioContentCardSizeTest, custom view size 적용 확인
  • AC4: 모든 thumbnail은 radius 14dp, centerCrop을 사용한다.
    • QA: drawable/custom view clipping, XML scaleType 확인
  • AC5: title은 white, creator name은 gray_500이며 둘 다 한 줄 말줄임 처리된다.
    • QA: XML textColor, maxLines, ellipsize 확인
  • AC6: size별 typography는 large/medium title Typography.Heading4, creator Typography.Body5, small title Typography.Body1, creator Typography.Caption2를 사용한다.
    • QA: AudioContentCardSizeTest, custom view style 적용 확인
  • AC7: 이미지 로딩 라이브러리를 컴포넌트 내부에 고정하지 않는다.
    • QA: thumbnailView() API 및 의존성 변경 없음 확인
  • AC8: 기존 화면 파일은 변경하지 않는다.
    • QA: git status --short 변경 파일 확인
  • AC9: 리소스 병합 및 디버그 빌드가 성공한다.
    • QA: ./gradlew :app:assembleDebug

검증 기록

  • 2026-05-19
    • 무엇/왜/어떻게: 사용자 요청에 따라 구현 전 PRD와 구현 계획/TASK 문서만 작성했다. Figma 20:3800, 20:3818, 20:3829는 동일 구조의 audio 콘텐츠 카드 크기 변형으로 문서화했다.
    • 실행 명령/도구:
      • Figma_get_design_context(20:3800)
      • Figma_get_design_context(20:3818)
      • Figma_get_design_context(20:3829)
      • Figma_get_screenshot(20:3800)
      • Figma_get_screenshot(20:3818)
      • Figma_get_screenshot(20:3829)
      • 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(docs/prd/20260519_캡슐탭바컴포넌트_prd.md)
      • read(docs/plan-task/20260519_캡슐탭바컴포넌트.md)
      • read(app/src/main/res/values/colors.xml)
      • read(app/src/main/res/values/dimens.xml)
      • read(app/src/main/res/values/typography.xml)
      • read(app/src/main/java/kr/co/vividnext/sodalive/v2/widget/CapsuleTabBarView.kt)
      • read(app/src/main/java/kr/co/vividnext/sodalive/v2/widget/CapsuleTabSelectionState.kt)
    • 결과:
      • PRD 문서는 docs/prd/20260519_오디오콘텐츠카드컴포넌트_prd.md에 작성했다.
      • 계획/TASK 문서는 docs/plan-task/20260519_오디오콘텐츠카드컴포넌트.md에 작성했다.
      • Figma 3개 노드는 large, medium, small size variant로 정리했다.
      • 코드, 리소스, 레이아웃 구현 파일은 변경하지 않았다.
      • 실제 구현과 빌드 검증은 사용자 승인 후 계획 문서 체크리스트에 따라 진행한다.
  • 2026-05-19
    • 무엇/왜/어떻게: 계획 문서에 따라 오디오 콘텐츠 카드 컴포넌트를 구현했다. AudioContentCardSize로 size contract를 분리하고, AudioContentCardView에서 카드 폭/썸네일/label/typography/gap을 size별로 적용하도록 했다.
    • 실행 명령/도구:
      • rg -n "class .*View @JvmOverloads|setTextAppearance|resources.getDimensionPixelSize" app/src/main/java/kr/co/vividnext/sodalive/v2/widget
      • rg -n "radius_14|spacing_8|gray_500|white|Typography\.Heading4|Typography\.Body1|Typography\.Body5|Typography\.Caption2" app/src/main/res/values
      • rg -n "spacing_2|spacing_11|185dp|163dp|151dp|122dp|114dp" app/src/main/res/values app/src/main/res/layout
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.AudioContentCardSizeTest" (RED)
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.AudioContentCardSizeTest" (GREEN)
      • lsp_diagnostics on modified Kotlin/XML files
      • rg -n "AudioContentCardView|iv_audio_content_thumbnail|clipToOutline=\"true\"|scaleType=\"centerCrop\"|tv_audio_content_title|tv_audio_content_creator|gray_500|bg_audio_content_card_thumbnail" app/src/main/res/layout/view_audio_content_card.xml app/src/main/res/drawable/bg_audio_content_card_thumbnail.xml
      • rg -n "fun setSize|fun setContent|fun thumbnailView|AudioContentCardSize\.Medium|TITLE_CREATOR_GAP_DP|setTextAppearance" app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardSize.kt
      • ./gradlew :app:assembleDebug
      • rg --files app/build/generated/data_binding_base_class_source_out/debug/out | rg "ViewAudioContentCardBinding"
      • git status --short
    • 결과:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardSize.kt를 추가했다.
      • app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt를 추가했다.
      • app/src/main/res/layout/view_audio_content_card.xml을 추가했다.
      • app/src/main/res/drawable/bg_audio_content_card_thumbnail.xml을 추가했다.
      • app/src/test/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardSizeTest.kt를 추가했다.
      • RED 실행은 Unresolved reference 'AudioContentCardSize'로 실패해 테스트가 신규 contract 부재를 검증함을 확인했다.
      • GREEN 실행은 BUILD SUCCESSFUL로 완료됐다.
      • 현재 환경에는 Kotlin/XML LSP 서버가 설정되어 있지 않아 lsp_diagnostics는 실행 불가했다.
      • XML 속성 확인에서 clipToOutline, centerCrop, title/creator id, gray_500, thumbnail drawable 참조를 확인했다.
      • :app:assembleDebugBUILD SUCCESSFUL로 완료됐다.
      • ViewAudioContentCardBinding.java 생성 파일을 확인해 ViewBinding 생성 가능성을 확인했다.