refactor(creator): 진행 중 라이브 컴포넌트를 공통화한다

This commit is contained in:
2026-07-16 22:59:07 +09:00
parent de90f771c1
commit 831533b4f6
114 changed files with 1204 additions and 725 deletions

View File

@@ -0,0 +1,260 @@
# 크리에이터 채널 진행 중 라이브 공통 컴포넌트 구현 계획
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** CreatorChannel 홈 탭과 라이브 탭의 진행 중인 라이브 UI를 하나의 공통 View로 통합하고 시간, 가격, 제목, 성인 배지 표시를 일관되게 만든다.
**Architecture:** `CreatorChannelCurrentLiveView`가 카드 레이아웃과 바인딩을 소유하고 두 화면은 `CreatorChannelLiveResponse`와 클릭 콜백만 전달한다. 시간과 가격·성인 표시 결정은 테스트 가능한 순수 함수/표시 모델로 분리하되 단일 컴포넌트 외의 추상화는 추가하지 않는다.
**Tech Stack:** Android XML View, Kotlin, ViewBinding, ConstraintLayout, JUnit4, Gradle
## Global Constraints
- 신규 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
- 레거시 코드는 수정하지 않는다.
- 기존 `CreatorChannelLiveResponse`, ViewModel, Repository, API, 라이브 입장 콜백을 유지한다.
- 신규 외부 의존성이나 디자인 토큰을 추가하지 않는다.
- 시간은 UTC 입력을 디바이스 타임존으로 변환해 24시간제 `HH:mm`으로 표시한다.
- `price > 0``ic_bar_cash`와 금액, `price <= 0`은 아이콘 없이 `무료`를 표시한다.
- 제목은 한 줄 끝 말줄임으로 가격 영역을 침범하지 않는다.
- 성인 배지는 `18dp × 18dp`, `bg_creator_channel_live_adult_badge`, `ic_new_shield_large`를 사용하고 시간 우측 `8dp` 간격에 표시한다.
- 성인 배지는 스크린 리더가 성인 제한 상태를 알 수 있는 접근성 설명을 제공한다.
- 홈 탭과 라이브 탭의 기존 바깥 여백과 클릭 동작을 유지한다.
---
## 파일 구조
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveView.kt`
- 공통 카드 바인딩, 시간 변환, 가격 및 성인 표시 상태를 담당한다.
- 생성: `app/src/main/res/layout/view_creator_channel_current_live.xml`
- LIVE 배지, 시간, 성인 배지, 한 줄 제목, 가격 캡슐의 공통 레이아웃이다.
- 수정: `app/src/main/res/layout/item_creator_channel_home_live.xml`
- 홈 섹션의 외부 패딩 안에 공통 View만 배치한다.
- 수정: `app/src/main/res/layout/fragment_creator_channel_live.xml`
- 기존 중복 카드 내부 레이아웃을 공통 View로 교체한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt`
- 개별 TextView 바인딩을 공통 View의 `bind` 호출로 교체한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt`
- 개별 카드 바인딩을 공통 View의 `bind` 호출로 교체한다.
- 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
- 홈 탭 공통 View 사용과 `HH:mm` 변환 계약을 검증한다.
- 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt`
- 라이브 탭 공통 View 사용과 중복 바인딩 제거를 검증한다.
- 생성: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveViewModelTest.kt`
- 가격·무료·성인 표시 상태를 순수 로직으로 검증한다.
- 수정: `docs/agent-guides/build-test-style.md`
- 신규 단위 테스트의 단일 실행 명령을 추가한다.
---
### Phase 1: 공통 표시 계약을 테스트로 정의
- [x] **Task 1.1: 시간 포맷과 가격·성인 표시 상태의 실패 테스트를 작성한다**
- 파일:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveViewModelTest.kt`
- 인터페이스:
- `formatCreatorChannelLiveTime(beginDateTimeUtc, timeZone, locale): String`
- `CreatorChannelCurrentLivePresentation(priceText: String, showPriceIcon: Boolean, showAdultBadge: Boolean)`
- `createCreatorChannelCurrentLivePresentation(price, isAdult, freeText, priceFormatter)`
- 테스트 내용:
- `2026-06-29T15:00:01Z``Asia/Seoul`로 변환하면 `00:00`이다.
- 잘못된 UTC 입력은 빈 문자열이다.
- 양수 가격은 포맷된 금액과 가격 아이콘을 표시한다.
- 0과 음수 가격은 `무료`와 숨겨진 가격 아이콘 상태다.
- `isAdult` 값이 성인 배지 표시 상태에 반영된다.
- 실행:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelCurrentLiveViewModelTest"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`
- 기대 결과: 신규 함수와 타입이 없어 컴파일 실패하거나 기존 날짜·초 포함 기대값 때문에 실패한다.
- 검증 기록:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelCurrentLiveViewModelTest"` 실행 시 신규 표시 생성 함수가 없어 컴파일 실패했다.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 실행 시 신규 시간 포맷 함수가 없어 컴파일 실패했다.
- [x] **Task 1.2: 최소 순수 로직으로 실패 테스트를 통과시킨다**
- 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveView.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt`
- 구현:
- 기존 UTC 파서를 재사용해 `HH:mm`을 반환하는 `formatCreatorChannelLiveTime`을 추가한다.
- `price > 0`만 유료로 분류하고 나머지는 무료로 분류하는 표시 모델 생성 함수를 추가한다.
- 기존 `formatCreatorChannelLiveDateTime` 호출부는 이후 공통 View 연결과 함께 제거할 수 있도록 테스트 계약부터 새 함수로 전환한다.
- 실행:
- Task 1.1의 두 테스트 명령
- 기대 결과: 시간과 표시 상태 테스트가 모두 통과한다.
- 검증 기록:
- 가격·성인 표시 테스트는 `BUILD SUCCESSFUL in 32s`로 통과했다.
- 시간 및 기존 CreatorChannel 소스 계약 테스트는 순차 재실행 후 `BUILD SUCCESSFUL in 3s`로 통과했다.
- 최초 병렬 실행 중 한 Gradle 프로세스에서 KSP 캐시 `StreamCorruptedException`이 발생해 이후 Gradle 검증은 순차 실행하기로 했다.
### Phase 2: 공통 View 구현 및 두 화면 연결
- [x] **Task 2.1: 두 화면이 공통 View를 사용해야 하는 실패 소스 계약 테스트를 작성한다**
- 파일:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt`
- 테스트 내용:
- `item_creator_channel_home_live.xml``fragment_creator_channel_live.xml``CreatorChannelCurrentLiveView`가 존재한다.
- 공통 레이아웃에 `ic_bar_cash`, `ic_new_shield_large`, `bg_creator_channel_live_adult_badge`가 존재한다.
- 공통 레이아웃의 성인 배지는 `18dp × 18dp`이며 시간 우측 `8dp` 간격이다.
- 제목은 `maxLines="1"`, `ellipsize="end"`이고 가격 영역 시작 전에 제약된다.
- 홈 어댑터와 라이브 Fragment는 공통 View의 `bind`를 호출하고 개별 TextView 바인딩을 보유하지 않는다.
- 실행:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`
- 기대 결과: 공통 View와 공통 레이아웃이 없어 실패한다.
- 검증 기록:
- 홈 소스 계약 테스트는 공통 레이아웃 파일이 없어 실패했다.
- 라이브 탭 계약 테스트는 공통 View 선언과 `bind` 호출이 없어 실패했다.
- [x] **Task 2.2: 공통 View와 레이아웃을 최소 구현한다**
- 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveView.kt`
- `app/src/main/res/layout/view_creator_channel_current_live.xml`
- 인터페이스:
- `fun bind(live: CreatorChannelLiveResponse, onClick: (CreatorChannelLiveResponse) -> Unit)`
- 구현:
- ViewBinding으로 레이아웃을 inflate한다.
- 제목, `HH:mm` 시간, 가격 표시 모델, 성인 배지 visibility를 매번 갱신한다.
- 카드 자체의 클릭 리스너가 현재 바인딩된 `live`를 전달한다.
- 카드 radius clipping은 Kotlin에서 `clipToOutline``ViewOutlineProvider.setRoundRect`를 함께 설정한다.
- 실행:
- Task 2.1의 두 테스트 명령
- 기대 결과: 공통 View 자체 계약 테스트는 통과하고 소비 화면 연결 관련 검증만 남는다.
- 검증 기록:
- `CreatorChannelCurrentLiveView``view_creator_channel_current_live.xml`을 추가했다.
- 최초 실행에서 기존 라이브 레이아웃 전용 child ID를 참조하던 테스트가 컴파일 실패해 공통 레이아웃 ID 계약으로 갱신했다.
- [x] **Task 2.3: 홈 탭과 라이브 탭을 공통 View에 연결한다**
- 파일:
- `app/src/main/res/layout/item_creator_channel_home_live.xml`
- `app/src/main/res/layout/fragment_creator_channel_live.xml`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt`
- 구현:
- 홈 item은 기존 `20dp` 수평 패딩과 상단 패딩 안에 공통 View를 배치한다.
- 라이브 Fragment는 기존 `14dp` 수평 margin 위치에 공통 View를 배치한다.
- 두 소비자는 기존 콜백에 공통 View의 `bind`를 연결한다.
- 중복 TextView/ImageView 조회, 가격 분기, 시간 포맷 호출, 성인 배지 바인딩 코드를 제거한다.
- 실행:
- Task 2.1의 두 테스트 명령
- 기대 결과: 두 화면의 공통 View 연결과 소스 계약 테스트가 통과한다.
- 검증 기록:
- `CreatorChannelActivitySourceTest``BUILD SUCCESSFUL in 4s`로 통과했다.
- `CreatorChannelLiveFragmentLayoutTest``BUILD SUCCESSFUL in 7s`로 통과했다.
- 두 화면의 기존 라이브 클릭 콜백은 공통 View의 `bind`에 그대로 전달한다.
### Phase 3: 회귀 검증과 문서 기록
- [x] **Task 3.1: 신규 테스트 실행 예시를 문서에 추가한다**
- 파일:
- `docs/agent-guides/build-test-style.md`
- 내용:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelCurrentLiveViewModelTest"`
- CreatorChannel 현재 라이브 관련 테스트 묶음 실행 예시
- 실행:
- `./gradlew tasks --all`
- 기대 결과: Gradle task 목록 조회가 성공한다.
- 검증 기록:
- sandbox 내 Gradle wrapper lock 접근이 거부되어 동일 명령을 승인된 외부 실행으로 재시도했다.
- `./gradlew tasks --all``BUILD SUCCESSFUL in 1s`로 완료되었다.
- [x] **Task 3.2: 관련 테스트, 컴파일, 린트를 실행하고 결과를 기록한다**
- 실행:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelCurrentLiveViewModelTest"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`
- `./gradlew :app:compileDebugKotlin`
- `./gradlew :app:ktlintCheck`
- `git diff --check`
- 기대 결과:
- 모든 명령이 exit code 0으로 종료된다.
- 테스트 실패, Kotlin 컴파일 오류, ktlint 오류, whitespace 오류가 없다.
- 수동 확인:
- 홈 탭과 라이브 탭에서 동일한 카드가 표시된다.
- 유료는 `ic_bar_cash + 금액`, 무료는 아이콘 없이 `무료`다.
- 성인 배지가 LIVE/시간 행의 시간 우측에 표시된다.
- 긴 제목이 한 줄 말줄임되고 가격 영역을 침범하지 않는다.
- 시간은 디바이스 타임존 기준 `HH:mm`이다.
- 같은 View에 다른 라이브 데이터를 다시 바인딩한 뒤 클릭하면 최신 라이브 데이터로 입장한다.
- 검증 기록:
- 세 관련 테스트를 `--rerun-tasks`로 실행해 39개 Gradle task가 실제 재실행되었고 `BUILD SUCCESSFUL in 1m 26s`로 완료되었다.
- `./gradlew :app:compileDebugKotlin``BUILD SUCCESSFUL in 1s`로 완료되었다.
- 최초 `:app:ktlintCheck`는 신규 테스트의 `=` 뒤 공백 누락 1건으로 실패했고 해당 줄만 수정했다.
- 수정 후 `./gradlew :app:ktlintCheck``BUILD SUCCESSFUL in 3s`로 완료되었다.
- `git diff --check`는 출력 없이 exit code 0으로 완료되었다.
- 에뮬레이터 또는 실기기가 제공되지 않아 화면 수동 확인은 실행하지 못했다.
### Phase 4: 리뷰 후속 수정
- [x] **Task 4.1: 재바인딩 관련 표시 상태 전환을 순수 로직 테스트로 검증한다**
- 파일:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelCurrentLiveViewModelTest.kt`
- 테스트 내용:
- 유료·성인, 무료·일반, 유료·일반 상태의 표시 모델을 순수 함수로 검증한다.
- 실제 View의 `TextView`, `visibility`, `performClick()` 직접 검증은 저장소 테스트 정책과 충돌하므로 제거하고 수동 확인 대상으로 기록한다.
- 검증 기록:
- 최초 신규 테스트는 한국어 무료 문구를 직접 기대해 기본 locale 리소스와 맞지 않아 실패했고, View와 동일한 `R.string.audio_content_tag_free` 기준으로 수정했다.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelCurrentLiveViewModelTest"``BUILD SUCCESSFUL in 32s`로 완료되었다.
- 후속 리뷰 반영으로 실제 View 표현 테스트를 제거하고 순수 표시 모델 전환 테스트로 대체했다.
- [x] **Task 4.2: 저장소 테스트 규칙과 충돌하는 XML 표현 단언을 제거한다**
- 파일:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt`
- 수정 내용:
- 공통 View 연결과 adapter `bind` 호출 계약은 유지한다.
- 공통 View의 크기, margin, drawable, maxLines, ellipsize, constraint 같은 표현 속성 문자열 단언은 제거한다.
- 검증 기록:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 포함 세 관련 테스트 묶음은 `BUILD SUCCESSFUL in 14s`로 완료되었다.
- 후속 리뷰 반영으로 라이브 탭 테스트에 남아 있던 공통 View 가격 영역 너비와 drawable 단언도 제거했다.
- [x] **Task 4.3: 가격 영역의 불필요한 XML 부모를 제거한다**
- 파일:
- `app/src/main/res/layout/view_creator_channel_current_live.xml`
- 수정 내용:
- `layout_current_live_price``FrameLayout` 부모를 제거하고 가격 캡슐 `LinearLayout`이 직접 constraint와 background를 갖도록 단순화한다.
- 검증 기록:
- `./gradlew :app:lintDebug`는 기존 `AndroidManifest.xml``com.facebook.FacebookActivity` `MissingClass` 등으로 실패했지만, lint 결과에서 `view_creator_channel_current_live.xml``layout_current_live_price` 관련 `UselessParent` 항목은 검출되지 않았다.
- `./gradlew :app:compileDebugKotlin``BUILD SUCCESSFUL in 1s`로 완료되었다.
- `./gradlew :app:runKtlintCheckOverTestSourceSet``BUILD SUCCESSFUL in 1s`로 완료되었다.
- `git diff --check`는 출력 없이 exit code 0으로 완료되었다.
- [x] **Task 4.4: 성인 배지 접근성 요구사항과 검증 기록을 문서화한다**
- 파일:
- `docs/20260716_크리에이터_채널_진행_중_라이브_공통_컴포넌트/prd.md`
- `docs/20260716_크리에이터_채널_진행_중_라이브_공통_컴포넌트/plan-task.md`
- 수정 내용:
- 성인 배지가 스크린 리더용 접근성 설명을 제공해야 한다는 요구사항을 추가한다.
- 접근성 설명 상태는 수동 확인 항목에 포함한다.
- 검증 기록:
- `view_creator_channel_current_live.xml``iv_current_live_adult``@string/screen_live_room_age_limit_adult``contentDescription`으로 사용한다.
- [x] **Task 4.5: UTC 파서 suffix 거부와 최신 검증 기록을 보완한다**
- 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
- `docs/20260716_크리에이터_채널_진행_중_라이브_공통_컴포넌트/prd.md`
- `docs/20260716_크리에이터_채널_진행_중_라이브_공통_컴포넌트/plan-task.md`
- 수정 내용:
- UTC 파서가 `ParsePosition`으로 입력 문자열 전체 소비 여부를 확인해 뒤에 추가 문자가 붙은 값을 거부한다.
- `beginDateTimeUtc` 뒤에 suffix가 붙으면 빈 문자열을 표시하는 회귀 테스트를 추가한다.
- 재바인딩 후 클릭 시 최신 라이브 데이터가 전달되어야 한다는 수동 확인 항목을 추가한다.
- 검증 기록:
- RED: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest.현재 진행 중인 라이브 UTC 시간 뒤에 추가 문자가 있으면 빈 문자열을 표시한다"``ComparisonFailure`로 실패해 suffix 입력이 정상 시간으로 표시되는 문제를 재현했다.
- GREEN: 유효 UTC, invalid UTC, suffix UTC focused 테스트 3건은 `BUILD SUCCESSFUL in 18s`로 통과했다.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"``BUILD SUCCESSFUL in 2s`로 통과했다.
- `./gradlew :app:compileDebugKotlin``BUILD SUCCESSFUL in 1s`로 통과했다.
- `./gradlew :app:ktlintCheck --rerun-tasks`는 기존 main source set ktlint 위반 550건으로 실패했다. 보고서에는 이번 변경 main 파일인 `CreatorChannelHomeSectionAdapter.kt` 항목이 없었다.
- `./gradlew :app:runKtlintCheckOverTestSourceSet --rerun-tasks`는 기존 `.editorconfig disabled_rules` deprecation 경고만 출력하고 `BUILD SUCCESSFUL in 7s`로 통과했다.
- `git diff --check`는 출력 없이 exit code 0으로 완료되었다.
## Verification Log
- 구현 전 계획 작성 완료. 검증 결과는 각 Task와 이 로그에 기존 기록을 유지한 채 누적한다.
- 2026-07-16: 홈 탭과 라이브 탭 공통 View 연결, UTC `HH:mm`, 무료/유료, 성인 배지, 긴 제목 제약 관련 자동 검증을 완료했다.
- 2026-07-16: 기존 프로젝트의 Gradle/ktlint deprecation 경고는 남아 있으나 이번 변경과 직접 관련된 오류는 확인되지 않았다.
- 2026-07-16: 리뷰 후속으로 재바인딩 관련 순수 표시 모델 테스트를 추가하고, 공통 View의 UI 표현 속성 문자열 테스트를 제거했으며, 가격 영역의 불필요한 XML 부모를 제거했다.
- 2026-07-16: 성인 배지 접근성 설명 요구사항과 수동 확인 항목을 PRD 및 계획 문서에 반영했다.
- 2026-07-16: UTC 파서 suffix 거부 테스트와 `ParsePosition` 전체 소비 검사를 추가했다. 캐시 없이 재실행한 `:app:ktlintCheck`는 기존 main source set 위반 550건으로 실패했으며, 이번 변경 main/test Kotlin 파일의 신규 ktlint 위반은 확인되지 않았다.

View File

@@ -0,0 +1,184 @@
# PRD: 크리에이터 채널 진행 중 라이브 공통 컴포넌트
## 1. Overview
CreatorChannel 홈 탭과 라이브 탭 최상단의 진행 중인 라이브 UI를 하나의 공통 컴포넌트로 통합한다.
---
## 2. Problem
- 홈 탭과 라이브 탭이 같은 진행 중인 라이브 정보를 서로 다른 레이아웃과 바인딩 코드로 표시한다.
- 두 화면의 시간, 가격, 무료 상태, 성인 배지 표현이 일치하지 않는다.
- UTC 시작 시간이 날짜와 초까지 포함된 형태로 표시되어 사용자가 현재 디바이스 기준 시작 시간을 빠르게 파악하기 어렵다.
- 홈 탭의 제목 영역이 고정 너비여서 화면 크기나 긴 제목에 안전하게 대응하지 못한다.
- 홈 탭은 가격 아이콘으로 `ic_can`을 사용하고 무료 라이브에서는 가격 영역 전체를 숨긴다.
- 홈 탭의 성인 배지는 텍스트 형태로 카드 우측 하단에 있으며 라이브 탭에는 성인 배지가 없다.
---
## 3. Goals
- 홈 탭과 라이브 탭이 동일한 진행 중인 라이브 컴포넌트를 사용한다.
- UTC 시작 시간을 디바이스 타임존으로 변환해 `HH:mm` 형식으로 표시한다.
- 제목을 한 줄 말줄임 처리하고 가격 영역을 침범하지 않도록 한다.
- 유료 라이브는 `ic_bar_cash` 아이콘과 가격을 표시한다.
- `price <= 0`인 라이브는 아이콘 없이 `무료`를 표시한다.
- 성인 라이브는 LIVE 배지 및 시간과 같은 행에 지정된 성인 배지를 표시한다.
- 기존 라이브 입장 흐름과 각 탭의 데이터 로딩 동작을 유지한다.
---
## 4. Non-Goals
- CreatorChannel API 응답 모델 또는 서버 스키마를 변경하지 않는다.
- 진행 중인 라이브 외 다른 콘텐츠 카드 UI를 변경하지 않는다.
- 라이브 입장, 결제, 본인 인증 또는 연령 제한 로직을 변경하지 않는다.
- CreatorChannel 홈/라이브 탭의 전체 구조를 리팩터링하지 않는다.
- 신규 디자인 토큰이나 외부 라이브러리를 추가하지 않는다.
---
## 5. Target Users
- CreatorChannel 홈 탭에서 현재 라이브를 확인하는 사용자
- CreatorChannel 라이브 탭에서 현재 라이브와 다시 듣기 콘텐츠를 확인하는 사용자
---
## 6. User Stories
- 사용자는 어느 탭에서 보더라도 동일한 형태의 진행 중인 라이브 정보를 확인하고 싶다.
- 사용자는 라이브 시작 시간을 자신의 디바이스 타임존 기준 `HH:mm`으로 확인하고 싶다.
- 사용자는 라이브가 무료인지 유료인지 명확히 확인하고 싶다.
- 사용자는 긴 라이브 제목이 가격 정보를 가리지 않는 상태로 확인하고 싶다.
- 사용자는 성인 라이브 여부를 LIVE 배지 옆 정보 행에서 즉시 확인하고 싶다.
- 사용자는 진행 중인 라이브 카드를 눌러 기존과 동일한 방식으로 라이브에 입장하고 싶다.
---
## 7. Core Features
### Feature A: 진행 중인 라이브 공통 View
홈 탭과 라이브 탭이 함께 사용하는 `CreatorChannelCurrentLiveView``kr.co.vividnext.sodalive.v2.creator.channel.ui` 하위에 추가한다.
#### Requirements
- 카드 내부 레이아웃과 표시 로직을 공통 View가 소유한다.
- 공통 View는 `CreatorChannelLiveResponse`와 클릭 동작을 입력받아 화면을 갱신한다.
- 홈 탭 RecyclerView item과 라이브 탭 Fragment는 각 화면에 필요한 바깥 여백만 담당한다.
- 기존 홈 탭과 라이브 탭의 라이브 클릭 콜백을 그대로 연결한다.
#### Edge Cases
- 동일한 View에 다른 라이브 데이터가 다시 바인딩되어도 이전 성인 배지 및 가격 아이콘 상태가 남지 않아야 한다.
- 클릭 동작이 다시 바인딩될 때 이전 데이터가 전달되지 않아야 한다.
### Feature B: UTC 시작 시간 표시
`beginDateTimeUtc`를 UTC로 해석한 뒤 현재 디바이스 타임존 기준 시간으로 변환한다.
#### Requirements
- 출력 형식은 24시간제 `HH:mm`이다.
- 시스템 기본 타임존을 사용한다.
- 시간 포맷 결과는 홈 탭과 라이브 탭에서 동일해야 한다.
- 기존 API 입력 형식인 `yyyy-MM-dd'T'HH:mm:ss'Z'`를 지원한다.
#### Edge Cases
- 파싱할 수 없는 값은 기존 동작과 동일하게 빈 문자열로 표시하고 앱이 종료되지 않아야 한다.
- `yyyy-MM-dd'T'HH:mm:ss'Z'` 뒤에 추가 문자가 붙은 값은 파싱할 수 없는 값으로 처리한다.
- 날짜가 타임존 변환으로 전날 또는 다음 날이 되더라도 변환된 현지 시간만 표시한다.
### Feature C: 제목과 가격 표시
제목과 가격 영역이 서로 침범하지 않도록 제약 기반 레이아웃을 사용한다.
#### Requirements
- 제목은 한 줄만 표시한다.
- 긴 제목은 끝 말줄임 처리한다.
- 제목 영역의 끝은 가격 영역의 시작에 제약한다.
- `price > 0`이면 `ic_bar_cash`와 포맷된 가격을 표시한다.
- `price <= 0`이면 가격 아이콘을 숨기고 `무료`를 표시한다.
- 무료와 유료 모두 가격 캡슐 영역 자체는 표시한다.
#### Edge Cases
- 가격 자릿수가 길어져도 제목이 가격 영역 아래나 위로 겹치지 않아야 한다.
- 유료 상태에서 무료 상태로 다시 바인딩하면 가격 아이콘이 숨겨져야 한다.
- 무료 상태에서 유료 상태로 다시 바인딩하면 가격 아이콘이 다시 표시되어야 한다.
### Feature D: 성인 배지
성인 라이브 여부를 LIVE 배지 및 시간과 같은 정보 행에 아이콘형 배지로 표시한다.
#### Requirements
- 정보 행의 순서는 `LIVE 배지 → 시간 → 성인 배지`다.
- LIVE 배지와 시간 사이, 시간과 성인 배지 사이에는 동일한 `8dp` 간격을 사용한다.
- 성인 배지는 LIVE 배지 높이와 같은 `18dp × 18dp` 정사각형으로 표시한다.
- 배경은 기존 `bg_creator_channel_live_adult_badge`를 사용한다.
- 아이콘은 `ic_new_shield_large`를 사용한다.
- `isAdult == true`일 때만 성인 배지를 표시한다.
- 성인 배지는 스크린 리더가 성인 제한 상태를 알 수 있는 접근성 설명을 제공한다.
#### Edge Cases
- 성인 라이브에서 일반 라이브로 다시 바인딩하면 배지가 숨겨져야 한다.
- 시간이 비어 있어도 성인 배지는 정보 행의 정해진 순서를 유지한다.
---
## 8. UX / UI Expectations
- 기준 디자인:
- 진행 중인 라이브 컴포넌트: Figma node `290:8950`
- 성인 배지 형태: Figma node `567:18346`
- 카드 높이는 현재 두 화면에서 사용하는 `78dp`를 유지한다.
- LIVE 배지 높이는 `18dp`를 유지한다.
- 성인 배지는 LIVE 배지와 같은 높이인 `18dp`를 사용한다.
- 성인 배지는 `bg_creator_channel_live_adult_badge``ic_new_shield_large` 조합으로 표시한다.
- 제목은 가격 영역 전까지 가변 너비를 사용하고 한 줄 말줄임 처리한다.
- 화면별 카드 바깥 여백은 기존 홈 탭과 라이브 탭의 배치를 유지한다.
---
## 9. Technical Constraints
- 신규 공통 View와 연결 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
- 레거시 코드는 수정하지 않는다.
- 기존 `CreatorChannelLiveResponse`를 재사용한다.
- 기존 drawable, color, typography, spacing 리소스를 우선 재사용한다.
- ViewBinding을 사용해 공통 View 내부 요소를 참조한다.
- 시간 변환은 현재 프로젝트에서 사용하는 Java 날짜/시간 API 범위 안에서 구현하며 신규 의존성을 추가하지 않는다.
- 기존 홈 탭과 라이브 탭의 ViewModel, Repository, API는 변경하지 않는다.
- 기존 라이브 입장 콜백 계약을 유지한다.
---
## 10. Verification
- 시간 포맷 단위 테스트:
- UTC 입력이 `Asia/Seoul`에서 다음 날 `HH:mm`로 변환되는지 확인한다.
- 파싱할 수 없는 입력이 빈 문자열을 반환하는지 확인한다.
- 표시 로직 단위 테스트:
- `price > 0`이면 가격 아이콘과 금액을 표시하는 상태인지 확인한다.
- `price <= 0`이면 가격 아이콘 없이 `무료` 상태인지 확인한다.
- `isAdult`에 따라 성인 배지 표시 상태가 결정되는지 확인한다.
- 유료·성인, 무료·일반, 유료·일반 상태 전환에 필요한 표시 모델이 매번 새로 계산되는지 확인한다.
- 소스 계약 테스트:
- 홈 탭과 라이브 탭이 모두 `CreatorChannelCurrentLiveView`를 사용하는지 확인한다.
- 홈 탭과 라이브 탭이 기존 클릭 콜백을 공통 View의 `bind`에 연결하는지 확인한다.
- 회귀 검증:
- 관련 단위 테스트를 실행한다.
- `:app:compileDebugKotlin``:app:ktlintCheck`를 실행한다.
- 가능하면 홈 탭과 라이브 탭에서 유료, 무료, 성인, 긴 제목, 접근성 설명 상태를 수동 확인한다.
---
## 11. Open Questions
- 없음. 사용자 승인 내용과 기존 리소스 조사 결과로 구현 범위가 확정되었다.

View File

@@ -102,6 +102,13 @@ Creator/Community Action Phase 테스트 예시:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.community.action.*"
```
CreatorChannel 현재 라이브 공통 컴포넌트 테스트 예시:
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelCurrentLiveViewModelTest"
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"
```
Chat Action Phase 테스트 예시:
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.chat.action.*"