# 현재 진행 중인 라이브 리스트 페이지 구현 계획/TASK > **For agentic workers:** REQUIRED SUB-SKILL: 구현 시 `superpowers:executing-plans`를 사용해 task 단위로 진행한다. 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다. **Goal:** `GET /api/v2/home/on-air-lives` 응답을 기반으로 현재 진행 중인 라이브 리스트 화면을 만들고, 아이템 터치 시 기존 라이브 상세 조회 후 입장/비밀번호/결제 흐름을 재사용한다. **Architecture:** 신규 화면/API/Repository/ViewModel/adapter/model은 `kr.co.vividnext.sodalive.v2.live.onair` 하위에 둔다. 기존 홈 추천 라이브 섹션의 더보기 진입점은 새 Activity를 여는 역할만 담당하고, 실제 라이브 입장 정책은 기존 `LiveViewModel.getRoomDetail()`과 `enterRoom()`을 호출해 중복 구현을 최소화한다. **Tech Stack:** Kotlin, Android XML Views, ViewBinding, RecyclerView, Retrofit, Gson, RxJava3, Koin, JUnit4 local unit test. --- ## 전제와 성공 기준 - PRD: `docs/20260626_현재_진행_중인_라이브_리스트_페이지/prd.md` - Figma 전체: `185:4506` - Figma 아이템: `185:4509` - 신규 기능 본체의 Kotlin package는 `kr.co.vividnext.sodalive.v2.live.onair`로 고정한다. - 메인 홈 추천 탭은 `HomeOnAirLiveActivity` 진입 연결만 추가하고, on-air live API/data/model 구현을 홈 패키지에 두지 않는다. - API endpoint는 `GET /api/v2/home/on-air-lives`이다. - 앱은 `size` query parameter를 보내지 않는다. - `HomeOnAirLiveResponse`에는 `beginDateTimeUtc: String`이 포함된다. - 목록 아이템은 `LIVE HH:mm`을 표시하며, `HH:mm`은 `beginDateTimeUtc`를 디바이스 Timezone으로 변환한 라이브 시작 시각이다. - 유료 라이브는 `ic_bar_cash`, 가격, `입장 캔`을 표시한다. - 무료 라이브는 cash 아이콘 없이 `무료`를 표시한다. - 아이템 터치 시 `getRoomDetail(roomId)` 호출 후 기존 입장 정책을 따른다. - 구현 완료 후 최소 다음 명령을 실행한다. - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.*"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` - `./gradlew :app:ktlintCheck` - `git diff --check` --- ## 파일 구조 - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/data/HomeOnAirLiveApi.kt` - `/api/v2/home/on-air-lives` Retrofit endpoint를 정의한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/data/HomeOnAirLiveModels.kt` - `HomeOnAirLivePageResponse`, `HomeOnAirLiveResponse` DTO를 정의한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/data/HomeOnAirLiveRepository.kt` - API 호출을 repository method로 감싼다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveUiModels.kt` - 리스트 item UI model, page state, 시간/가격 표시 모델을 정의한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveMappers.kt` - DTO를 UI model로 변환하고 `beginDateTimeUtc`를 `HH:mm`으로 변환한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveAuthHeader.kt` - blank token이면 `null`, 값이 있으면 `Bearer {token}`을 반환한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveViewModel.kt` - 첫 페이지/추가 페이지 로딩, loading/error/page 상태를 관리한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt` - 화면 구성, pagination, 상세 조회 후 입장 흐름을 연결한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/ui/HomeOnAirLiveAdapter.kt` - 리스트 아이템을 바인딩한다. - Create: `app/src/main/res/layout/activity_home_on_air_live.xml` - title bar와 RecyclerView 컨테이너를 정의한다. - Create: `app/src/main/res/layout/item_home_on_air_live.xml` - Figma 기준 라이브 리스트 아이템을 정의한다. - Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeLiveAdapter.kt` - 더보기 항목 클릭 콜백을 받을 수 있게 한다. - Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` - 홈 추천 라이브 더보기에서 `HomeOnAirLiveActivity`를 연다. - Modify: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` - 신규 API/Repository/ViewModel을 Koin에 등록한다. - Modify: `app/src/main/AndroidManifest.xml` - `HomeOnAirLiveActivity`를 등록한다. - Modify: `app/src/main/res/values/strings.xml` - `On Air`, `입장 캔`, `무료` 등 필요한 문자열을 추가하거나 기존 문자열을 재사용한다. - Modify: `app/src/main/res/values-en/strings.xml` - 신규 문자열 번역을 추가한다. - Modify: `app/src/main/res/values-ja/strings.xml` - 신규 문자열 번역을 추가한다. - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveAuthHeaderTest.kt` - optional auth header 생성 규칙을 검증한다. - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveMapperTest.kt` - DTO to UI mapping, `LIVE HH:mm`, 가격/무료 표시를 검증한다. --- ### Phase 1: 문서와 기존 구조 확인 - [x] **Task 1.1: 기존 홈 라이브 클릭 동작 확인** - 확인: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeLiveAdapter.kt` - 결과: - 홈 추천 최상단 라이브 아이템 클릭은 `onLiveClick()`으로 연결되어 있으나 현재 구현은 `Unit`이다. - 더보기 항목은 `setOnClickListener(null)`로 클릭 동작이 없다. - 검증: - Run: `rg -n "onLiveClick|setOnLiveClick|MoreViewHolder|setOnClickListener\\(null\\)" app/src/main/java/kr/co/vividnext/sodalive/v2/main/home` - Expected: 클릭 콜백과 미구현 지점이 확인된다. - [x] **Task 1.2: 기존 라이브 입장 정책 확인** - 확인: - `app/src/main/java/kr/co/vividnext/sodalive/live/now/all/LiveNowAllActivity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/live/LiveViewModel.kt` - `app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt` - 결과: - 기존 입장은 `getRoomDetail(roomId)` 후 `manager.id`, `price`, `isPaid`, `isPrivateRoom`, `beginDateTimeUtc`를 기준으로 분기한다. - `LiveViewModel.enterRoom(roomId, onSuccess, password)`를 재사용할 수 있다. - 검증: - Run: `rg -n "fun enterLiveRoom|fun getRoomDetail|fun enterRoom|data class GetRoomDetailResponse" app/src/main/java/kr/co/vividnext/sodalive/live` - Expected: 상세 조회와 입장 API 호출 지점이 확인된다. --- ### Phase 2: API, DTO, mapper, ViewModel 추가 - [x] **Task 2.1: optional auth header 테스트 작성** - 생성: - `app/src/test/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveAuthHeaderTest.kt` - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveAuthHeaderTest"` - Expected: helper 구현 전 RED 실패. - Result: helper 구현 전 `Unresolved reference 'model'`, `Unresolved reference 'homeOnAirLiveAuthHeader'`로 RED 실패 확인. - [x] **Task 2.2: optional auth header helper 구현** - 생성: - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveAuthHeader.kt` - 작업: - `fun homeOnAirLiveAuthHeader(token: String): String?`를 추가한다. - `token.trim().takeIf { it.isNotEmpty() }?.let { "Bearer $it" }` 규칙을 적용한다. - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveAuthHeaderTest"` - Expected: PASS. - Result: `--no-daemon` 재실행 기준 BUILD SUCCESSFUL. - [x] **Task 2.3: mapper 테스트 작성** - 생성: - `app/src/test/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveMapperTest.kt` - 테스트 케이스: - `beginDateTimeUtc`를 디바이스 Timezone 기준 `HH:mm`으로 변환한다. - `price > 0`이면 유료 가격 표시 모델로 매핑한다. - `price == 0`이면 무료 표시 모델로 매핑한다. - page 응답의 `page`, `hasNext`, `items`를 UI state로 유지한다. - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveMapperTest"` - Expected: mapper 구현 전 RED 실패. - Result: mapper 구현 전 `Unresolved reference 'data'`, `Unresolved reference 'model'`, `Unresolved reference 'HomeOnAirLivePageResponse'`로 RED 실패 확인. - [x] **Task 2.4: API/DTO/Repository/model/mapper 구현** - 생성: - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/data/HomeOnAirLiveApi.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/data/HomeOnAirLiveModels.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/data/HomeOnAirLiveRepository.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveUiModels.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveMappers.kt` - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` - 작업: - Retrofit endpoint는 `@GET("/api/v2/home/on-air-lives")`로 정의한다. - `@Header("Authorization") authHeader: String?`, `@Query("page") page: Int`만 사용한다. - DTO는 `@Keep`, `@SerializedName`을 사용한다. - mapper는 `java.time` 사용 가능성을 확인하고, 프로젝트 minSdk 제약상 문제가 있으면 기존 `SimpleDateFormat` 패턴을 사용한다. - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveMapperTest"` - Expected: PASS. - Result: `--no-daemon` 재실행 기준 BUILD SUCCESSFUL. - [x] **Task 2.5: ViewModel 구현** - 생성: - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveViewModel.kt` - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` - 작업: - `loadFirstPage()`는 page 0부터 호출하고 기존 items를 교체한다. - `loadNextPage()`는 `hasNext = true`이고 loading 중이 아닐 때만 호출한다. - success이면 mapper 결과를 state로 발행한다. - failure이면 기존 unknown error toast 패턴을 따른다. - 검증: - Run: `./gradlew :app:compileDebugKotlin` - Expected: 신규 data/model/ViewModel/DI 코드가 컴파일된다. - Result: auth header 테스트 GREEN 실행 중 `:app:compileDebugKotlin` 성공. 추가로 `HomeOnAirLiveViewModelTest`를 작성해 최초 page 0 로드, 다음 page append, `hasNext=false` guard를 검증했고 BUILD SUCCESSFUL. --- ### Phase 3: 화면 UI와 홈 진입점 연결 - [ ] **Task 3.1: string/layout 추가** - 생성: - `app/src/main/res/layout/activity_home_on_air_live.xml` - `app/src/main/res/layout/item_home_on_air_live.xml` - 수정: - `app/src/main/res/values/strings.xml` - `app/src/main/res/values-en/strings.xml` - `app/src/main/res/values-ja/strings.xml` - 작업: - `On Air` title, `입장 캔`, 무료 문자열은 기존 리소스가 있으면 재사용하고 부족한 값만 추가한다. - `item_home_on_air_live.xml`은 75dp profile, `LIVE HH:mm`, title, creator, price/free 영역을 포함한다. - 검증: - Run: `./gradlew :app:mergeDebugResources` - Expected: 신규 layout/string resource가 merge된다. - [ ] **Task 3.2: adapter 구현** - 생성: - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/ui/HomeOnAirLiveAdapter.kt` - 작업: - `submitItems(items)`와 `onClick` callback을 제공한다. - 유료/무료 표시 모델에 따라 cash icon visibility와 텍스트를 바인딩한다. - 이미지 로드는 기존 `loadUrl` 확장을 사용한다. - 검증: - Run: `./gradlew :app:compileDebugKotlin` - Expected: adapter가 컴파일된다. - [ ] **Task 3.3: Activity 구현과 Manifest 등록** - 생성: - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt` - 수정: - `app/src/main/AndroidManifest.xml` - 작업: - `newIntent(context)`를 제공한다. - `RecyclerView`와 pagination scroll listener를 연결한다. - item click 시 `getRoomDetail(roomId)` 후 기존 입장 정책을 적용한다. - 오디오 재생 서비스 중지 후 `LiveRoomActivity`를 실행한다. - 검증: - Run: `./gradlew :app:compileDebugKotlin` - Expected: Activity와 Manifest 등록이 컴파일된다. - [ ] **Task 3.4: 홈 추천 라이브 더보기 진입점 연결** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeLiveAdapter.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` - 작업: - `HomeLiveAdapter`에 `setOnMoreClick()`을 추가한다. - 더보기 항목 클릭 시 `HomeOnAirLiveActivity.newIntent(requireContext())`를 실행한다. - 기존 라이브 아이템 클릭은 이번 범위에서 직접 입장 연결하지 않고 기존 `onLiveClick()` 미구현 상태를 유지한다. - 검증: - Run: `./gradlew :app:compileDebugKotlin` - Expected: 홈 추천 더보기에서 신규 Activity 진입 코드가 컴파일된다. --- ### Phase 4: 최종 검증 - [ ] **Task 4.1: 단위 테스트와 컴파일 검증** - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.*"` - Expected: on-air live 관련 단위 테스트 PASS. - Run: `./gradlew :app:mergeDebugResources` - Expected: resource merge PASS. - Run: `./gradlew :app:compileDebugKotlin` - Expected: Kotlin compile PASS. - [ ] **Task 4.2: 린트/차이 검증** - 검증: - Run: `./gradlew :app:ktlintCheck` - Expected: ktlint PASS. - Run: `git diff --check` - Expected: whitespace error 없음. --- ## Verification Log - 2026-06-26 Phase 2 RED: production 구현 전 `HomeOnAirLiveAuthHeaderTest`, `HomeOnAirLiveMapperTest` 실행 시 신규 `data/model` symbol 미존재로 컴파일 실패를 확인했다. - 2026-06-26 Phase 2 GREEN: `HomeOnAirLiveAuthHeaderTest`, `HomeOnAirLiveMapperTest`, `HomeOnAirLiveViewModelTest` 각각 `--no-daemon` 실행 기준 BUILD SUCCESSFUL을 확인했다. - 2026-06-26 Phase 2 최종 검증: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.*" --no-daemon`, `./gradlew :app:mergeDebugResources --no-daemon`, `./gradlew :app:compileDebugKotlin --no-daemon`, `./gradlew :app:ktlintCheck --no-daemon`, `git diff --check` 모두 통과했다.