240 lines
14 KiB
Markdown
240 lines
14 KiB
Markdown
# 오디오 콘텐츠 위젯 태그 배지 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<AudioContentTag>.audioContentTopTags(): List<AudioContentTag>`는 `[Original, First]` 순서를 적용한다.
|
|
- `fun Collection<AudioContentTag>.audioContentBottomTags(): List<AudioContentTag>`는 `[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<AudioContentTag>)` 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<AudioContentTag>)` 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로 대체 검증했다.
|