# 오디오 콘텐츠 위젯 태그 배지 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:3840`, `20:3843`, `20:3815`, `20:3814` 기준으로 v2 패키지 아래 신규 위젯인 `AudioContentCardView` 썸네일 영역에 Original, First, Point, Free 태그 배지를 추가한다. **Architecture:** v2 신규 위젯인 `AudioContentCardView`와 `view_audio_content_card.xml`만 수정해 썸네일 `ImageView`를 overlay 가능한 container 안으로 이동하고 상단/하단 tag row를 추가한다. 태그 타입과 정렬 순서는 순수 Kotlin contract로 분리해 단위 테스트하고, 실제 이미지 로딩은 기존 `thumbnailView()` API로 계속 호출부에 위임한다. 레거시 화면, 레거시 adapter, 레거시 XML에는 포함하지 않는다. **Tech Stack:** Android XML Views, Kotlin custom View, Android resources, JUnit4 local unit test. --- ## 작업 목표 - 대상은 v2 신규 위젯 파일인 `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt`와 `app/src/main/res/layout/view_audio_content_card.xml`로 한정한다. - 레거시 화면과 기존 화면 적용 작업은 제외한다. - Original, First는 썸네일 왼쪽 상단에 표시한다. - Original과 First가 함께 있으면 Original, First 순서로 표시한다. - Point, Free는 썸네일 왼쪽 하단에 표시한다. - Free 태그는 string resource 기반 다국어 텍스트로 생성한다. - 기존 `setContent`, `thumbnailView`, `setSize` API는 유지한다. - 구현 중 체크박스와 검증 기록은 이 문서에 누적한다. ## 파일 구조 - Read: `docs/prd/20260522_오디오콘텐츠위젯태그배지_prd.md` - Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt` - tag view 참조, `setTags(...)`, 상/하단 정렬 적용, size 적용 시 thumbnail container 크기 갱신을 처리한다. - Modify: `app/src/main/res/layout/view_audio_content_card.xml` - thumbnail overlay container와 top/bottom tag row를 추가한다. - Do not modify: 레거시 화면 XML, 레거시 adapter, 기존 API/DTO, 기존 화면 바인딩 코드 - 신규 위젯 자체 기능만 추가한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentTag.kt` - `Original`, `First`, `Point`, `Free` 태그 타입과 위치/정렬 계약을 정의한다. - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/AudioContentTagTest.kt` - 태그 중복 제거, 상단/하단 분류, 고정 정렬 순서를 검증한다. - Modify: `app/src/main/res/values/strings.xml` - `audio_content_tag_free` 문자열을 추가한다. - Modify: `app/src/main/res/values-en/strings.xml` - `audio_content_tag_free` 영어 문자열을 추가한다. - Modify: `app/src/main/res/values-ja/strings.xml` - `audio_content_tag_free` 일본어 문자열을 추가한다. - Add if missing: `app/src/main/res/drawable/ic_content_tag_original.png` - Add if missing: `app/src/main/res/drawable/ic_content_tag_point.png` - Add if missing: `app/src/main/res/drawable/ic_content_tag_first_star.png` - Create if missing: `app/src/main/res/drawable/bg_audio_content_tag_first.xml` - `#FF34B8` solid background for the `62dp x 24dp` First badge. - Create if missing: `app/src/main/res/drawable/bg_audio_content_tag_free.xml` - `#052742` solid background for the Free badge. The view uses height `24dp`, min width `34dp`, and `wrap_content` width. - Modify: `docs/plan-task/20260522_오디오콘텐츠위젯태그배지.md` - 구현 중 체크박스와 검증 기록을 누적한다. ## 구현 계획 ### Task 1: 기존 위젯과 Figma 기준 확인 **Files:** - 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/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardSize.kt` - Read: `docs/prd/20260519_오디오콘텐츠카드컴포넌트_prd.md` - Read: `docs/prd/20260522_오디오콘텐츠위젯태그배지_prd.md` - [x] **Step 1: 대상 위젯 확인** Run: `rg -n "class AudioContentCardView|iv_audio_content_thumbnail|ll_audio_content_label|AudioContentCardSize|thumbnailView" app/src/main/java/kr/co/vividnext/sodalive/v2/widget app/src/main/res/layout/view_audio_content_card.xml` Expected: 변경 대상이 v2 신규 위젯인 `AudioContentCardView.kt`와 `view_audio_content_card.xml`이며, 기존 size/content/thumbnail API를 유지해야 함을 확인한다. 레거시 화면 파일은 변경 대상에서 제외한다. - [ ] **Step 2: Figma tag 기준 확인** Run tools: - `Figma_get_design_context(20:3840)` - `Figma_get_screenshot(20:3840)` - `Figma_get_design_context(20:3843)` - `Figma_get_screenshot(20:3843)` - `Figma_get_design_context(20:3815)` - `Figma_get_screenshot(20:3815)` - `Figma_get_design_context(20:3814)` - `Figma_get_screenshot(20:3814)` Expected: First, Free 단일 tag와 Free/Point + Original/First 조합의 위치, 크기, 색상, 순서를 확인한다. - [x] **Step 3: 기존 리소스 확인** Run: `rg -n "ic_content_tag_original|ic_content_tag_point|ic_content_tag_first_star|audio_content_tag_free|#ff34b8|#052742" app/src/main/res` Expected: 재사용 가능한 리소스가 있으면 재사용하고, 없으면 최소 신규 리소스를 추가한다. ### Task 2: AudioContentTag contract TDD **Files:** - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/AudioContentTagTest.kt` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentTag.kt` - [x] **Step 1: RED - 태그 정렬 테스트 추가** Test cases: - 전달 순서가 `First`, `Original`이어도 top row는 `Original`, `First`가 된다. - 전달 순서가 `Free`, `Point`이어도 bottom row는 `Point`, `Free`가 된다. - 중복 태그는 한 번만 표시된다. - 빈 set은 top/bottom 모두 비어 있다. - [x] **Step 2: RED 실행** Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.AudioContentTagTest"` Expected: `Unresolved reference 'AudioContentTag'` 또는 `Unresolved reference 'audioContentTopTags'` / `Unresolved reference 'audioContentBottomTags'`로 실패한다. - [x] **Step 3: GREEN - 최소 contract 추가** Implementation notes: - `enum class AudioContentTag { Original, First, Point, Free }`를 추가한다. - `fun Collection.audioContentTopTags(): List`는 `[Original, First]` 순서를 적용한다. - `fun Collection.audioContentBottomTags(): List`는 `[Point, Free]` 순서를 적용한다. - [x] **Step 4: GREEN 실행** Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.AudioContentTagTest"` Expected: `BUILD SUCCESSFUL` ### Task 3: XML overlay 구조와 리소스 추가 **Files:** - Modify: `app/src/main/res/layout/view_audio_content_card.xml` - Modify: `app/src/main/res/values/strings.xml` - Modify: `app/src/main/res/values-en/strings.xml` - Modify: `app/src/main/res/values-ja/strings.xml` - Add if missing: drawable resources for tags/backgrounds - [x] **Step 1: thumbnail overlay container 적용** Implementation notes: - 기존 `iv_audio_content_thumbnail`는 `FrameLayout` overlay container 내부로 이동한다. - container id는 예: `fl_audio_content_thumbnail_container`로 둔다. - `AudioContentCardView.setSize`는 thumbnail `ImageView`와 container 모두 `thumbnailSizeDp` 크기로 맞춘다. - 기존 `thumbnailView()`는 동일한 `ImageView`를 반환한다. - [x] **Step 2: top/bottom tag row 추가** Implementation notes: - top row id 예: `ll_audio_content_tag_top` - bottom row id 예: `ll_audio_content_tag_bottom` - top row: parent start/top 정렬, horizontal orientation - bottom row: parent start/bottom 정렬, horizontal orientation - row는 태그가 없을 때 `gone` 처리한다. - [x] **Step 3: Free 다국어 문자열 추가** Suggested strings: - `values/strings.xml`: `무료` - `values-en/strings.xml`: `Free` - `values-ja/strings.xml`: `無料` - [x] **Step 4: resource merge 확인** Run: `./gradlew :app:assembleDebug` Expected: Android resource merge와 debug assemble이 성공한다. ### Task 4: AudioContentCardView tag binding 구현 **Files:** - Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt` - [x] **Step 1: view 참조 추가** Implementation notes: - thumbnail container, top tag row, bottom tag row를 `onFinishInflate()`에서 찾는다. - `setSize()`에서 thumbnail container와 thumbnail image 크기를 함께 갱신한다. - [x] **Step 2: public tag API 추가** Implementation notes: - `fun setTags(tags: Set)` API를 추가한다. - top row는 `tags.audioContentTopTags()` 결과를 렌더링한다. - bottom row는 `tags.audioContentBottomTags()` 결과를 렌더링한다. - 빈 row는 `View.GONE`, 표시 row는 `View.VISIBLE`로 처리한다. - 기존 public API는 제거하거나 시그니처 변경하지 않는다. - [x] **Step 3: tag view 생성 로직 추가** Implementation notes: - Original: `ImageView` + `ic_content_tag_original`, `24dp x 24dp` - Point: `ImageView` + `ic_content_tag_point`, `24dp x 24dp` - First: `LinearLayout` + `bg_audio_content_tag_first` + `ic_content_tag_first_star` + `FIRST` 텍스트, `62dp x 24dp` - star icon: `17dp x 17dp`, `marginStart=2dp`, `top=4dp`, `contentDescription=null` - text: `Phosphate Solid`, `16sp`, white, `singleLine=true`, `includeFontPadding=false`, `marginStart=1dp`, `top=2dp` - Free: `TextView` + `bg_audio_content_tag_free` + `@string/audio_content_tag_free`, height `24dp`, min width `34dp`, width `wrap_content` - 장식 아이콘의 `contentDescription`은 `null`로 둔다. ### Task 5: 검증 **Files:** - All changed implementation/resources/tests - Modify: `docs/plan-task/20260522_오디오콘텐츠위젯태그배지.md` - [x] **Step 1: 단위 테스트 실행** Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.AudioContentTagTest"` Expected: `BUILD SUCCESSFUL` - [x] **Step 2: 리소스/빌드 검증 실행** Run: `./gradlew :app:assembleDebug` Expected: `BUILD SUCCESSFUL` - [x] **Step 3: LSP diagnostics 확인** Run tools: - `lsp_diagnostics(app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt)` - `lsp_diagnostics(app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentTag.kt)` - `lsp_diagnostics(app/src/test/java/kr/co/vividnext/sodalive/v2/widget/AudioContentTagTest.kt)` Expected: 변경 파일에 신규 error가 없다. - [x] **Step 4: 검증 기록 누적** 이 문서 하단에 무엇/왜/어떻게, 실행 명령, 결과, 남은 이슈를 한국어로 누적한다. - [x] **Step 5: 레거시 변경 없음 확인** Run: `git diff --name-only | rg -v "^(docs/prd/20260522_오디오콘텐츠위젯태그배지_prd.md|docs/plan-task/20260522_오디오콘텐츠위젯태그배지.md|app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt|app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentTag.kt|app/src/test/java/kr/co/vividnext/sodalive/v2/widget/AudioContentTagTest.kt|app/src/main/res/layout/view_audio_content_card.xml|app/src/main/res/values/strings.xml|app/src/main/res/values-en/strings.xml|app/src/main/res/values-ja/strings.xml|app/src/main/res/drawable/)"` Expected: 출력이 없어야 한다. 출력이 있으면 레거시 화면 또는 범위 밖 파일을 변경한 것이므로 되돌리거나 계획을 갱신하기 전에 사용자 확인을 받는다. ## 검증 기록 ### 2026-05-22 문서 생성 - 무엇/왜/어떻게: 사용자 요청에 따라 오디오 콘텐츠 위젯 태그 배지 추가 작업의 PRD와 구현 계획/TASK 문서를 작성했다. 대상 위젯은 v2 패키지 아래 신규 위젯인 `AudioContentCardView.kt`와 `view_audio_content_card.xml`로 한정했고, Figma `20:3840`, `20:3843`, `20:3815`, `20:3814` 기준 태그 종류/위치/정렬/다국어 요구사항을 반영했다. 레거시 화면에는 포함하지 않는다는 제약도 문서에 반영했다. - 실행 명령: 문서 작성만 수행했으므로 Gradle 빌드는 실행하지 않았다. - 결과: 구현 전 기준 문서가 준비되었다. ### 2026-05-27 구현 및 검증 - 무엇/왜/어떻게: `AudioContentCardView` 썸네일을 `FrameLayout` overlay container로 감싸고 top/bottom tag row를 추가했다. `AudioContentTag` enum과 `audioContentTopTags()` / `audioContentBottomTags()` 순수 Kotlin contract를 추가해 Original/First, Point/Free 정렬과 중복 제거를 테스트했다. `setTags(tags: Set)` API를 추가해 Original, First, Point, Free 태그를 썸네일 내부에 렌더링하도록 구현했으며, 기존 `setContent`, `setSize`, `thumbnailView` API는 유지했다. Free 태그 문자열과 First/Free 배경 drawable도 추가했다. - 실행 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.AudioContentTagTest"`를 RED/GREEN으로 실행했고, `./gradlew :app:assembleDebug`를 실행했다. `git diff --name-only`와 계획 문서의 범위 확인 명령으로 레거시 변경 여부를 확인했다. - 결과: RED 단계는 `Unresolved reference 'AudioContentTag'` 및 `Unresolved reference 'audioContentTopTags'` / `audioContentBottomTags` 컴파일 실패로 확인했다. GREEN 단계의 targeted unit test와 `assembleDebug`는 모두 `BUILD SUCCESSFUL`로 통과했다. debug APK 산출물 `app/build/outputs/apk/debug/app-debug.apk`도 생성되었다. - 남은 이슈: Figma MCP `Figma_get_design_context` 호출은 timeout으로 완료하지 못해, 태그 크기/위치/색상은 PRD와 이 계획 문서에 이미 정리된 Figma 기준 및 기존 `SeriesContentCardView` 패턴을 근거로 구현했다. Kotlin LSP는 `kotlin-lsp` 미설치로 diagnostics를 실행할 수 없었고, Gradle compile/test/build로 대체 검증했다.