21 KiB
현재 진행 중인 라이브 리스트 페이지 구현 계획/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이다. - 앱은
sizequery 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:ktlintCheckgit diff --check
파일 구조
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/data/HomeOnAirLiveApi.kt/api/v2/home/on-air-livesRetrofit endpoint를 정의한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/data/HomeOnAirLiveModels.ktHomeOnAirLivePageResponse,HomeOnAirLiveResponseDTO를 정의한다.
- 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으로 변환한다.
- DTO를 UI model로 변환하고
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveAuthHeader.kt- blank token이면
null, 값이 있으면Bearer {token}을 반환한다.
- blank 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.xmlHomeOnAirLiveActivity를 등록한다.
- Modify:
app/src/main/res/values/strings.xmlOn 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, 가격/무료 표시를 검증한다.
- DTO to UI mapping,
Phase 1: 문서와 기존 구조 확인
-
Task 1.1: 기존 홈 라이브 클릭 동작 확인
- 확인:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.ktapp/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: 클릭 콜백과 미구현 지점이 확인된다.
- Run:
- 확인:
-
Task 1.2: 기존 라이브 입장 정책 확인
- 확인:
app/src/main/java/kr/co/vividnext/sodalive/live/now/all/LiveNowAllActivity.ktapp/src/main/java/kr/co/vividnext/sodalive/live/LiveViewModel.ktapp/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 호출 지점이 확인된다.
- Run:
- 확인:
Phase 2: API, DTO, mapper, ViewModel 추가
-
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 실패 확인.
- Run:
- 생성:
-
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.
- Run:
- 생성:
-
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 실패 확인.
- Run:
- 생성:
-
Task 2.4: API/DTO/Repository/model/mapper 구현
- 생성:
app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/data/HomeOnAirLiveApi.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/data/HomeOnAirLiveModels.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/data/HomeOnAirLiveRepository.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveUiModels.ktapp/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패턴을 사용한다.
- Retrofit endpoint는
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveMapperTest" - Expected: PASS.
- Result:
--no-daemon재실행 기준 BUILD SUCCESSFUL.
- Run:
- 생성:
-
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=falseguard를 검증했고 BUILD SUCCESSFUL.
- Run:
- 생성:
Phase 3: 화면 UI와 홈 진입점 연결
-
Task 3.1: string/layout 추가
- 생성:
app/src/main/res/layout/activity_home_on_air_live.xmlapp/src/main/res/layout/item_home_on_air_live.xml
- 수정:
app/src/main/res/values/strings.xmlapp/src/main/res/values-en/strings.xmlapp/src/main/res/values-ja/strings.xml
- 작업:
On Airtitle, 무료 문자열은 기존 리소스가 있으면 재사용하고 부족한 값만 추가한다.item_home_on_air_live.xml은 75dp profile,LIVE HH:mm, title, creator, price/free 영역을 포함한다.
- 검증:
- Run:
./gradlew :app:mergeDebugResources - Expected: 신규 layout/string resource가 merge된다.
- Result: Figma item node
185:4509를Figma_get_design_context,Figma_get_screenshot으로 확인한 뒤75dpprofile,LIVEpill/time 분리, title, creator, price/free 영역을 반영했다.--no-daemon단독 실행 기준 BUILD SUCCESSFUL.
- Run:
- 생성:
-
Task 3.2: adapter 구현
- 생성:
app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/ui/HomeOnAirLiveAdapter.kt
- 작업:
submitItems(items)와onClickcallback을 제공한다.- 유료/무료 표시 모델에 따라 cash icon visibility와 텍스트를 바인딩한다.
- 이미지 로드는 기존
loadUrl확장을 사용한다.
- 검증:
- Run:
./gradlew :app:compileDebugKotlin - Expected: adapter가 컴파일된다.
- Result:
HomeOnAirLiveAdapter가loadUrl, paid/free visibility, item click callback을 바인딩하도록 구현했고--no-daemon실행 기준 BUILD SUCCESSFUL.
- Run:
- 생성:
-
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 등록이 컴파일된다.
- Result:
HomeOnAirLiveActivity.newIntent(context), 첫 페이지 로드, pagination, toast/loading/empty 표시, 기존getRoomDetail(roomId)후 입장/비밀번호/결제 흐름을 연결했고--no-daemon실행 기준 BUILD SUCCESSFUL.
- Run:
- 생성:
-
Task 3.4: 홈 추천 라이브 더보기 진입점 연결
- 수정:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeLiveAdapter.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt
- 작업:
HomeLiveAdapter에setOnMoreClick()을 추가한다.- 더보기 항목 클릭 시
HomeOnAirLiveActivity.newIntent(requireContext())를 실행한다. - 기존 라이브 아이템 클릭은 이번 범위에서 직접 입장 연결하지 않고 기존
onLiveClick()미구현 상태를 유지한다.
- 검증:
- Run:
./gradlew :app:compileDebugKotlin - Expected: 홈 추천 더보기에서 신규 Activity 진입 코드가 컴파일된다.
- Result:
HomeLiveAdapter.setOnMoreClick()과HomeMainFragment.openHomeOnAirLive()를 연결했고 기존onLiveClick()은 미구현 상태로 유지했다.--no-daemon실행 기준 BUILD SUCCESSFUL.
- Run:
- 수정:
-
Task 3.5: Phase 3 코드 리뷰 및 검증
- 리뷰 결과:
HomeOnAirLiveActivity가LiveViewModel.getRoomDetail()/enterRoom()을 호출하지만liveViewModel.isLoading,liveViewModel.toastLiveData를 observe하지 않아 상세 조회/입장 실패 시 기존 loading/toast 패턴이 사용자에게 전달되지 않는다.GetRoomDetailResponse.isAdult기준의 기존ensureLoginAndAdultAuth정책이 신규 화면에 연결되어 있지 않아 성인 라이브에서 본인인증/콘텐츠 설정 가드가 누락된다.- 기존
LiveNowAllActivity는 상세 응답의channelName != null일 때만 입장 분기를 수행하지만, 신규 화면에는 해당 가드가 없어 종료/예약 상태 응답에서 입장을 시도할 수 있다.
- 반영:
- 로그인 확인 후
getRoomDetail(roomId)를 호출하고, 응답의isAdult기준으로 기존 성인 인증/성인 콘텐츠 설정 가드를 수행한다. LiveViewModel.isLoading과LiveViewModel.toastLiveData를 observe하고, 목록 로딩과 입장 로딩 상태를 OR 조건으로 합쳐LoadingDialog를 제어한다.channelName이 blank인 상세 응답은 입장 분기를 중단하도록 정책 함수를 추가한다.
- 로그인 확인 후
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.*" --no-daemon - Result: BUILD SUCCESSFUL.
- Run:
./gradlew :app:mergeDebugResources --no-daemon - Result: BUILD SUCCESSFUL.
- Run:
./gradlew :app:compileDebugKotlin --no-daemon - Result: BUILD SUCCESSFUL.
- Run:
./gradlew :app:ktlintCheck --no-daemon - Result: BUILD SUCCESSFUL.
- Run:
git diff --check - Result: whitespace error 없음.
- 종합: 리뷰 지적사항을 반영했고 빌드/린트/단위 테스트가 모두 통과했다.
- 추가 Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveEntryPolicyTest" - 추가 RED Result:
canEnterHomeOnAirLiveRoom미구현으로 컴파일 실패를 확인했다. - 추가 GREEN Result:
channelName이 blank인 상세 응답에서 입장 분기를 중단하는 정책 함수를 구현한 뒤 BUILD SUCCESSFUL. - 추가 Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.*" --no-daemon - 추가 Result: BUILD SUCCESSFUL.
- 추가 Run:
./gradlew :app:mergeDebugResources --no-daemon - 추가 Result: 최초 sandbox 실행은
~/.gradlewrapper lock 접근 제한으로 실패했고, 승인 실행 기준 BUILD SUCCESSFUL. - 추가 Run:
./gradlew :app:compileDebugKotlin --no-daemon - 추가 Result: BUILD SUCCESSFUL.
- 추가 Run:
./gradlew :app:ktlintCheck --no-daemon - 추가 Result: 기존
.editorconfig의disabled_rulesdeprecation 경고가 출력되었지만 BUILD SUCCESSFUL. - 추가 Run:
git diff --check - 추가 Result: whitespace error 없음.
- Run:
- 리뷰 결과:
Phase 4: 최종 검증
-
Task 4.1: 단위 테스트와 컴파일 검증
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.*" - Expected: on-air live 관련 단위 테스트 PASS.
- Result:
--no-daemon단독 실행 기준 BUILD SUCCESSFUL. - Run:
./gradlew :app:mergeDebugResources - Expected: resource merge PASS.
- Result:
--no-daemon단독 실행 기준 BUILD SUCCESSFUL. - Run:
./gradlew :app:compileDebugKotlin - Expected: Kotlin compile PASS.
- Result:
--no-daemon단독 실행 기준 BUILD SUCCESSFUL.
- Run:
- 검증:
-
Task 4.2: 린트/차이 검증
- 검증:
- Run:
./gradlew :app:ktlintCheck - Expected: ktlint PASS.
- Result:
--no-daemon단독 실행 기준 BUILD SUCCESSFUL. - Run:
git diff --check - Expected: whitespace error 없음.
- Result: 출력 없음. whitespace error 없음.
- Run:
- 검증:
Verification Log
- 2026-06-26 Phase 2 RED: production 구현 전
HomeOnAirLiveAuthHeaderTest,HomeOnAirLiveMapperTest실행 시 신규data/modelsymbol 미존재로 컴파일 실패를 확인했다. - 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모두 통과했다. - 2026-06-27 Phase 3 검증:
./gradlew :app:mergeDebugResources --no-daemon,./gradlew :app:compileDebugKotlin --no-daemon,./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.*" --no-daemon,./gradlew :app:ktlintCheck --no-daemon,git diff --check모두 통과했다. 최초 병렬 실행 중mergeDebugResources가 stripped 중간 파일 경합으로 1회 실패했으나, 순차 단독 재실행에서 BUILD SUCCESSFUL을 확인했다. - 2026-06-27 Phase 3 Figma 재검증: 리뷰 지적에 따라
item_home_on_air_live.xml의 title을Typography.Heading4, LIVE badge를Typography.Caption3기반 pill, time/creator/price를Typography.Body6로 조정했다. 이후./gradlew :app:mergeDebugResources --no-daemon,./gradlew :app:compileDebugKotlin --no-daemon,./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.*" --no-daemon,./gradlew :app:ktlintCheck --no-daemon,git diff --check모두 통과했다. - 2026-06-27 Phase 3 리뷰 반영:
HomeOnAirLiveActivity에 기존 라이브 입장 정책과 동일하게 로그인 확인,getRoomDetail(roomId)응답의isAdult기준 성인 인증/성인 콘텐츠 설정 가드,LiveViewModel.isLoading/toastLiveData관찰을 추가했다. 이후./gradlew :app:mergeDebugResources --no-daemon,./gradlew :app:compileDebugKotlin --no-daemon,./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.*" --no-daemon,./gradlew :app:ktlintCheck --no-daemon,git diff --check모두 통과했다. - 2026-06-27 Phase 3 코드 리뷰 및 재검증:
HomeOnAirLiveActivity에LiveViewModelloading/toast 관찰과GetRoomDetailResponse.isAdult기준 성인 인증/성인 콘텐츠 설정 가드를 추가한 뒤./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를 순차 실행해 모두 통과했다. - 2026-06-27 Phase 4 최종 검증:
./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모두 BUILD SUCCESSFUL을 확인했다.git diff --check는 출력이 없어 whitespace error가 없음을 확인했다.