# 채팅 탭 페이지 Plan / Task > **For agentic workers:** 구현 시 `superpowers:subagent-driven-development` 또는 `superpowers:executing-plans`를 사용해 task 단위로 진행한다. 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다. **Goal:** `ChatMainFragment`에 Figma `177:3466` 기준 채팅 탭 페이지를 구성하고 `GET /api/v2/chat/rooms` 목록, filter, cursor pagination을 연결한다. **Architecture:** 채팅 탭 전용 API/Repository/ViewModel/DTO/UI model/Adapter는 `kr.co.vividnext.sodalive.v2.main.chat` 하위에 둔다. 기존 `CapsuleTabBarView`, `view_title_bar_default.xml`, `MainV2Activity` 하단 내비게이션, `ChatRoomActivity.newIntent(context, roomId)`, Coil 이미지 로딩 패턴을 우선 재사용하고, 없는 채팅방 list item과 Direct badge만 최소 신규 UI로 만든다. **Tech Stack:** Kotlin, Android XML Views, ViewBinding, RecyclerView, RxJava3, Retrofit, Gson, Koin, Coil, Robolectric/local unit test. --- ## 전제와 성공 기준 - PRD: `docs/20260609_채팅_탭_페이지/prd.md` - Figma: `177:3466` - `filter` query는 `ALL`, `AI`, `DM` 중 하나만 사용한다. - `cursor` query는 다음 페이지 요청에 사용한다. 첫 페이지는 `cursor=null`로 요청한다. - 현재 선택되지 않은 `CapsuleTabBarView` tab 터치 시 첫 페이지 API를 호출하고 기존 목록을 clear한 뒤 새 목록으로 세팅한다. - `hasMore=true`와 `nextCursor`가 제공되면 스크롤 pagination으로 다음 페이지를 append한다. - unread dot은 표시하지 않는다. - `chatType`은 `AI`, `DM` 문자열이다. - `lastMessageAt`은 ISO-8601 문자열이며 디바이스 timezone으로 변환 후 표시한다. - 일주일까지는 상대 시간 문구로 표시하고, 그보다 오래된 올해 메시지는 locale별 날짜 포맷, 다른 연도 메시지는 `yyyy.MM.dd`로 표시한다. - `chatType=AI` item 클릭은 `ChatRoomActivity.newIntent(context, roomId)`로 연결한다. - `chatType=DM` item 클릭과 플로팅 버튼 클릭은 이번 범위에서 실제 이동을 연결하지 않는다. - 구현 완료 후 최소 다음 명령을 실행한다. - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.CapsuleTabSelectionStateTest"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` - `./gradlew :app:ktlintCheck` --- ## 파일 구조 - Rename: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/data/HomeChatModels.kt` -> `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/data/ChatRoomModels.kt` - `ChatRoomListPageResponse`, `ChatRoomListItemResponse` DTO를 유지한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/data/ChatRoomApi.kt` - `GET /api/v2/chat/rooms` Retrofit endpoint를 정의한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/data/ChatRoomRepository.kt` - API 호출을 위임한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomFilter.kt` - `ALL`, `AI`, `DM` filter와 tab index 매핑을 정의한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomUiModels.kt` - `ChatRoomListUiItem`, `ChatRoomListUiState`를 정의한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomMappers.kt` - DTO를 UI item으로 변환하되 시간 표시는 수행하지 않고 원본 `lastMessageAt`을 유지한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomTimeTextFormatter.kt` - ISO-8601 시간 문자열을 화면 표시 문자열로 변환한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainViewModel.kt` - filter, first page load, pagination, loading/error state를 관리한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ui/ChatRoomListAdapter.kt` - 채팅방 RecyclerView adapter를 구현한다. - Create: `app/src/main/res/layout/item_v2_chat_room.xml` - 채팅방 list item UI를 정의한다. - Create: `app/src/main/res/drawable/bg_chat_direct_badge.xml` - Direct badge 배경을 정의한다. - Create: `app/src/main/res/drawable/bg_chat_floating_button.xml` - 플로팅 버튼 배경을 정의한다. - Modify: `app/src/main/res/layout/view_title_bar_default.xml` - 우측 아이콘 개수가 화면별로 달라질 수 있도록 action container를 추가한다. - Modify: `app/src/main/res/layout/fragment_v2_main_chat.xml` - title bar, capsule tab bar, RecyclerView, floating button을 배치한다. - Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt` - ViewModel observe, adapter, filter tab, pagination, AI item click을 연결한다. - Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/CapsuleTabBarView.kt` - selected tab 텍스트 색상을 검정으로 표시한다. - Modify: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` - `ChatRoomApi`, `ChatRoomRepository`, `ChatMainViewModel`을 등록한다. - 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` - 채팅 탭 title/filter, Direct badge, 상대 시간 string을 추가한다. - Test Create: - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatRoomFilterTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatRoomTimeTextFormatterTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatRoomMapperTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainViewModelTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatRoomListAdapterTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragmentLayoutTest.kt` --- ### Phase 1: 기존 구조 확인과 범위 고정 - [x] **Task 1.1: 기존 채팅 탭, 메인 내비게이션, 채팅방 진입 구조 확인** - 확인: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt` - 확인: `app/src/main/res/layout/fragment_v2_main_chat.xml` - 확인: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt` - 확인: `app/src/main/res/layout/activity_main_v2.xml` - 확인: `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt` - 검증: 하단 nav는 `MainV2Activity`가 담당하고, AI item 클릭은 `ChatRoomActivity.newIntent(context, roomId)`를 사용한다. - [x] **Task 1.2: 기존 재사용 위젯/리소스 확인** - 확인: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/CapsuleTabBarView.kt` - 확인: `app/src/main/res/layout/view_capsule_tab_bar.xml` - 확인: `app/src/main/res/layout/view_title_bar_default.xml` - 확인: `app/src/main/res/layout/view_title_bar_home.xml` - 확인: `app/src/main/res/drawable-mdpi/ic_bar_cash.png` - 확인: `app/src/main/res/drawable-mdpi/ic_bar_search.png` - 확인: `app/src/main/res/drawable-xxhdpi/ic_plus_no_bg.png` - 검증: `view_title_bar_default.xml`은 제목형 title bar로 재사용하고, 우측 action 영역만 가변 아이콘 구조로 확장한다. --- ### Phase 2: DTO/API/Repository 네이밍과 계약 작성 - [x] **Task 2.1: `HomeChatModels.kt` 파일명 정리** - Rename: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/data/HomeChatModels.kt` -> `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/data/ChatRoomModels.kt` - 유지: - `ChatRoomListPageResponse` - `ChatRoomListItemResponse` - 검증 명령: `./gradlew :app:compileDebugKotlin` - 기대 결과: 파일명 변경 후 기존 import가 없거나 갱신되어 Kotlin compile이 성공한다. - [x] **Task 2.2: 채팅방 API/Repository 작성** - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/data/ChatRoomApi.kt` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/data/ChatRoomRepository.kt` - API 계약: - `@GET("/api/v2/chat/rooms")` - `@Header("Authorization") authHeader: String` - `@Query("filter") filter: String` - `@Query("cursor") cursor: String?` - return: `Single>` - Repository 계약: - `fun getChatRooms(token: String, filter: String, cursor: String?): Single>` - 검증 명령: `./gradlew :app:compileDebugKotlin` - 기대 결과: Retrofit annotation/import 포함 컴파일 성공. - [x] **Task 2.3: Koin DI 등록** - Modify: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` - 추가: - import `ChatRoomApi` - import `ChatRoomRepository` - import `ChatMainViewModel` - `networkModule`: `single { ApiBuilder().build(get(), ChatRoomApi::class.java) }` - `repositoryModule`: `factory { ChatRoomRepository(get()) }` - `viewModelModule`: `viewModel { ChatMainViewModel(get()) }` - Phase 2 실행 기록: `ChatMainViewModel`은 Phase 4.2 생성 대상이며 현재 파일이 없어 등록 시 컴파일이 실패하므로, 이번 단계에서는 `ChatRoomApi`, `ChatRoomRepository`만 등록하고 ViewModel 등록은 Phase 4.2에서 수행한다. - 검증 명령: `./gradlew :app:compileDebugKotlin` - 기대 결과: Koin 등록과 import 컴파일 성공. --- ### Phase 3: Filter, 시간 formatter, mapper 작성 - [x] **Task 3.1: Filter 매핑 RED 테스트 작성** - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatRoomFilterTest.kt` - 테스트 케이스: - index `0` -> `ChatRoomFilter.ALL` - index `1` -> `ChatRoomFilter.AI` - index `2` -> `ChatRoomFilter.DM` - 각 filter의 API value는 `ALL`, `AI`, `DM` - 현재 선택된 index를 다시 선택하면 같은 filter를 반환하되 ViewModel에서 중복 호출하지 않는다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomFilterTest"` - 기대 결과: `ChatRoomFilter` 미구현으로 RED 실패. - [x] **Task 3.2: Filter 모델 구현** - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomFilter.kt` - 구현: - `enum class ChatRoomFilter(val apiValue: String) { ALL("ALL"), AI("AI"), DM("DM") }` - `fun ChatRoomFilter.Companion.fromTabIndex(index: Int): ChatRoomFilter` - 유효하지 않은 index는 `ALL`로 처리한다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomFilterTest"` - 기대 결과: PASS. - [x] **Task 3.3: 시간 formatter RED 테스트 작성** - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatRoomTimeTextFormatterTest.kt` - 테스트 기준: - 기준 now: `2026-06-09T12:00:00Z` - timezone: `Asia/Seoul` - locale: `ko`, `en`, `ja` - 테스트 케이스: - 30초 전 -> `screen_chat_time_just_now` - 3분 전 -> `screen_chat_time_minutes` - 2시간 전 -> `screen_chat_time_hours` - 7일 이내 -> `screen_chat_time_days` - 8일 전이고 같은 연도/ko -> `6월 1일` - 8일 전이고 같은 연도/en -> `Jun 1` - 8일 전이고 같은 연도/ja -> `6月1日` - 전년도 -> `2025.12.31` - timezone 변환 결과 로컬 날짜가 달라지는 ISO-8601 입력을 올바르게 처리한다. - parse 실패 또는 blank -> `screen_chat_time_just_now` - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomTimeTextFormatterTest"` - 기대 결과: formatter 미구현으로 RED 실패. - [x] **Task 3.4: 다국어 string과 시간 formatter 구현** - 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` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomTimeTextFormatter.kt` - string 추가: - `screen_chat_time_just_now` - `screen_chat_time_minutes` - `screen_chat_time_hours` - `screen_chat_time_days` - 구현: - `fun formatChatRoomLastMessageTime(context: Context, isoText: String?, nowMillis: Long = System.currentTimeMillis(), timeZone: TimeZone = TimeZone.getDefault(), locale: Locale = Locale.getDefault()): String` - ISO-8601 offset 포함 문자열을 우선 파싱한다. - `diff < 1분`: just now - `diff < 1시간`: minutes - `diff < 1일`: hours - `diff < 8일`: days - 같은 연도: locale별 `M월 d일`, `MMM d`, `M月d日` - 다른 연도: `yyyy.MM.dd` - minSdk 23과 desugaring 미설정 상태를 고려해 `SimpleDateFormat`, `Calendar`, `TimeZone` 기반으로 구현한다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomTimeTextFormatterTest"` - 기대 결과: PASS. - [x] **Task 3.5: DTO -> UI model mapper RED 테스트 작성** - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatRoomMapperTest.kt` - 테스트 케이스: - `chatType="AI"`는 Direct badge 미표시 item으로 매핑한다. - `chatType="DM"`은 Direct badge 표시 item으로 매핑한다. - `lastMessageAt`은 원본 ISO-8601 문자열 그대로 매핑한다. - `roomId`, `targetName`, `targetImageUrl`, `lastMessage`는 그대로 전달한다. - 알 수 없는 `chatType`은 표시 대상에서 제외한다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomMapperTest"` - 기대 결과: UI model/mapper 미구현으로 RED 실패. - [x] **Task 3.6: UI model과 mapper 구현** - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomUiModels.kt` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomMappers.kt` - 구현: - `enum class ChatRoomType { AI, DM }` - `data class ChatRoomListUiItem(roomId: Long, chatType: ChatRoomType, targetName: String, targetImageUrl: String, lastMessage: String, lastMessageAt: String, showDirectBadge: Boolean)` - `sealed class ChatRoomListUiState` - `Loading` - `Content(val items: List, val isAppending: Boolean = false)` - `Empty` - `Error(val message: String?)` - `fun ChatRoomListItemResponse.toUiItem(): ChatRoomListUiItem?` - `fun List.toUiItems(): List` - 화면 표시용 시간 문구는 `Activity`/`Fragment`/`Adapter` 등 표시 계층에서 `formatChatRoomLastMessageTime(context, item.lastMessageAt)`으로 변환한다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomMapperTest"` - 기대 결과: PASS. --- ### Phase 4: ViewModel pagination/filter 동작 작성 - [x] **Task 4.1: ViewModel RED 테스트 작성** - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainViewModelTest.kt` - 테스트 케이스: - 초기 `loadFirstPage()`는 `filter=ALL`, `cursor=null`로 API를 호출한다. - 다른 filter 선택 시 기존 items를 clear하고 `cursor=null` 첫 페이지를 요청한다. - 현재 선택된 filter를 다시 선택하면 API를 재호출하지 않는다. - 첫 페이지 success + rooms 있음 -> `Content(items)` - 첫 페이지 success + rooms empty -> `Empty` - `hasMore=true`, `nextCursor` 있음 상태에서 `loadNextPage()`는 다음 cursor로 호출하고 items를 append한다. - `hasMore=false`면 `loadNextPage()`가 API를 호출하지 않는다. - append loading 중 중복 `loadNextPage()` 호출은 무시한다. - API failure/data null/Throwable은 `Error`와 unknown error toast를 발생시킨다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"` - 기대 결과: `ChatMainViewModel` 미구현으로 RED 실패. - [x] **Task 4.2: ViewModel 구현** - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainViewModel.kt` - 구현: - `currentFilter: ChatRoomFilter = ChatRoomFilter.ALL` - `currentItems: List` - `nextCursor: String?` - `hasMore: Boolean` - `isLoading`, `isAppending`, `chatRoomStateLiveData`, `toastLiveData` - `loadFirstPage(filter: ChatRoomFilter = currentFilter)` - `selectFilter(filter: ChatRoomFilter)` - `loadNextPage()` - token은 `"Bearer ${SharedPreferenceManager.token}"` 형태로 전달한다. - first page 요청 전 state는 `Loading` 및 items clear를 반영한다. - append 요청은 기존 items를 유지하고 성공 시 뒤에 붙인다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"` - 기대 결과: PASS. --- ### Phase 5: 채팅방 list item UI와 Adapter 작성 - [x] **Task 5.1: Direct badge와 item layout 추가** - Create: `app/src/main/res/drawable/bg_chat_direct_badge.xml` - Create: `app/src/main/res/layout/item_v2_chat_room.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` - 구현 기준: - item root height는 content wrap, vertical padding `@dimen/spacing_14` - profile image `58dp`, 원형 표시 - name `18sp` bold, white, maxLines 1 - Direct badge: `soda_400`, radius `4dp`, Pattaya font, text `Direct` - time: `14sp`, `gray_500`, right align - last message: `16sp`, `gray_500`, maxLines 1, ellipsize end - unread dot view는 만들지 않는다. - 검증 명령: `./gradlew :app:mergeDebugResources` - 기대 결과: 신규 layout/drawable/string resource merge 성공. - [x] **Task 5.2: Adapter RED 테스트 작성** - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatRoomListAdapterTest.kt` - 테스트 케이스: - DM item bind 시 Direct badge가 `VISIBLE` - AI item bind 시 Direct badge가 `GONE` - name, lastMessage, lastMessageTimeText가 TextView에 표시된다. - item 클릭 시 bound `ChatRoomListUiItem`을 listener로 전달한다. - layout에 unread dot id가 존재하지 않는다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomListAdapterTest"` - 기대 결과: Adapter 미구현으로 RED 실패. - [x] **Task 5.3: Adapter 구현** - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ui/ChatRoomListAdapter.kt` - 구현: - `RecyclerView.Adapter` - `submitItems(items: List)` - `onItemClick: (ChatRoomListUiItem) -> Unit` - Coil `loadUrl` 또는 기존 이미지 로딩 extension을 사용해 profile image를 로딩한다. - `lastMessageAt`은 bind 시점에 `formatChatRoomLastMessageTime(context, item.lastMessageAt)`으로 표시 문자열로 변환한다. - `showDirectBadge`로 Direct badge visibility를 제어한다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomListAdapterTest"` - 기대 결과: PASS. --- ### Phase 6: 채팅 탭 layout과 CapsuleTab 색상 보정 - [x] **Task 6.1: 기본 title bar 가변 action 영역 RED 테스트 작성** - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragmentLayoutTest.kt` - 테스트 케이스: - `view_title_bar_default.xml`에 `tv_title_bar_title`이 존재한다. - `view_title_bar_default.xml`에 `ll_title_bar_actions`가 존재한다. - 기존 호환을 위해 `iv_title_bar_menu` id가 유지된다. - `ll_title_bar_actions`는 우측 아이콘을 2개 이상 담을 수 있는 horizontal container다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest"` - 기대 결과: action container 미구현으로 RED 실패. - [x] **Task 6.2: `view_title_bar_default.xml` 가변 action 영역 구현** - Modify: `app/src/main/res/layout/view_title_bar_default.xml` - 구현: - 기존 `tv_title_bar_title`은 유지한다. - 기존 `iv_title_bar_menu` id는 유지한다. - `iv_title_bar_menu`를 `LinearLayout` id `ll_title_bar_actions` 내부로 이동한다. - `ll_title_bar_actions`는 horizontal, center_vertical, 우측 정렬 가능한 구조로 둔다. - 추가 action icon은 Fragment에서 `ll_title_bar_actions.addView(...)`로 붙일 수 있게 한다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest"` 및 `./gradlew :app:mergeDebugResources` - 기대 결과: PASS. - [x] **Task 6.3: Fragment layout RED 테스트 작성** - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragmentLayoutTest.kt` - 테스트 케이스: - `fragment_v2_main_chat.xml` root background가 `@color/black` - `view_title_bar_default` include가 존재한다. - `view_capsule_tab_bar` include가 title bar 아래에 존재한다. - `rv_chat_rooms`가 capsule tab 아래, parent bottom까지 constraint 된다. - `btn_chat_floating`이 우측 하단에 존재한다. - layout에 bottom navigation view가 존재하지 않는다. - layout에 unread dot view id가 존재하지 않는다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest"` - 기대 결과: layout 미구현으로 RED 실패. - [x] **Task 6.4: Fragment layout 구현** - Create: `app/src/main/res/drawable/bg_chat_floating_button.xml` - Modify: `app/src/main/res/layout/fragment_v2_main_chat.xml` - 구현: - root를 `ConstraintLayout`으로 변경한다. - `view_title_bar_default`: height `60dp`, title은 Fragment에서 `대화`로 설정한다. - `view_capsule_tab_bar`: height `52dp`, title bar 아래 - `rv_chat_rooms`: `0dp x 0dp`, capsule tab 아래부터 parent bottom, `clipToPadding=false`, bottom padding은 하단 nav/미니플레이어 겹침 방지 기준으로 충분히 둔다. - `btn_chat_floating`: 우측 하단 원형 `soda_400`, plus icon, 실제 click action 없음 - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest"` 및 `./gradlew :app:mergeDebugResources` - 기대 결과: PASS. - [x] **Task 6.5: CapsuleTab selected 텍스트 색상 테스트 보강** - Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/CapsuleTabSelectionStateTest.kt` 또는 신규 `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/CapsuleTabBarViewTest.kt` - 테스트 케이스: - selected tab은 white background와 black text를 사용한다. - normal tab은 black background와 white text를 사용한다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.*CapsuleTab*"` - 기대 결과: 현재 `CapsuleTabBarView`가 selected text도 white로 설정해 RED 실패. - [x] **Task 6.6: CapsuleTab selected 텍스트 색상 구현** - Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/CapsuleTabBarView.kt` - 구현: - selected이면 `R.color.black` - normal이면 `R.color.white` - 기존 background drawable 정책은 유지한다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.*CapsuleTab*"` - 기대 결과: PASS. --- ### Phase 7: `ChatMainFragment` 화면 동작 연결 - [ ] **Task 7.1: Fragment source 계약 테스트 작성** - Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragmentLayoutTest.kt` - 테스트 케이스: - `ChatMainFragment.kt`가 `ChatMainViewModel`을 주입한다. - include된 `view_title_bar_default`의 title을 `대화`로 설정한다. - `iv_title_bar_menu`에는 `ic_bar_cash`를 설정하고, `ll_title_bar_actions`에는 `ic_bar_search` ImageView를 추가한다. - `CapsuleTabBarView.setMenus`에 `전체`, `AI 채팅`, `DM`을 설정한다. - tab 선택 listener에서 `ChatRoomFilter.fromTabIndex(index)`를 사용한다. - RecyclerView에 `LinearLayoutManager`와 `ChatRoomListAdapter`를 연결한다. - scroll listener에서 끝에 가까워지면 `viewModel.loadNextPage()`를 호출한다. - AI item 클릭은 `ChatRoomActivity.newIntent(requireContext(), item.roomId)`를 사용한다. - DM item 클릭과 floating button click은 startActivity를 호출하지 않는다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest"` - 기대 결과: Fragment 동작 미구현으로 RED 실패. - [ ] **Task 7.2: Fragment 동작 구현** - Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt` - 구현: - `private val viewModel: ChatMainViewModel by viewModel()` - `private val chatRoomListAdapter = ChatRoomListAdapter { onChatRoomClick(it) }` - `binding.viewChatTitleBar.tvTitleBarTitle.setText(R.string.tab_chat)` - `binding.viewChatTitleBar.ivTitleBarMenu.setImageResource(R.drawable.ic_bar_cash)` - `binding.viewChatTitleBar.llTitleBarActions.addView(ImageView(requireContext()).apply { setImageResource(R.drawable.ic_bar_search) })` 형태로 search action을 추가한다. - `binding.viewChatFilterTabs.root.setMenus(listOf(...), selectedIndex = 0)` - 현재 선택되지 않은 tab 선택 시 `viewModel.selectFilter(ChatRoomFilter.fromTabIndex(index))` - `onViewCreated`에서 `viewModel.loadFirstPage()` - state observe: - `Content` -> adapter submit - `Empty`, `Error` -> adapter empty - `Loading` -> first page loading 표시 - `isLoading`은 기존 `LoadingDialog` 패턴 사용 - scroll pagination threshold는 하단 3개 전 기준으로 `viewModel.loadNextPage()` - `onChatRoomClick`: `AI`만 `ChatRoomActivity`로 이동, `DM`은 return - floating button click listener는 연결하지 않거나 no-op으로 둔다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest"` - 기대 결과: PASS. --- ### Phase 8: 통합 검증과 문서 기록 - [ ] **Task 8.1: 리소스/컴파일 검증** - 실행: - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` - 기대 결과: - 모든 신규 layout/drawable/string/binding 생성 성공 - Kotlin compile 성공 - [ ] **Task 8.2: 단위 테스트 검증** - 실행: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.*CapsuleTab*"` - 기대 결과: - 채팅 탭 filter/formatter/mapper/ViewModel/Adapter/Layout 테스트 통과 - CapsuleTab 기존 테스트와 색상 보정 테스트 통과 - [ ] **Task 8.3: ktlint 검증** - 실행: `./gradlew :app:ktlintCheck` - 기대 결과: ktlint error 없음 - [ ] **Task 8.4: 검증 기록 누적** - Modify: `docs/20260609_채팅_탭_페이지/plan-task.md` - Modify: `docs/20260609_채팅_탭_페이지/prd.md` - 기록: - 무엇/왜/어떻게 변경했는지 - 실행한 명령 - 각 명령 결과 - 실패가 있었다면 실패 원인과 후속 조치 - 검증: 기존 Verification Log를 삭제하거나 덮어쓰지 않고 하단에 새 항목으로 누적한다. --- ## 구현 제외 체크리스트 - [ ] unread dot view, drawable, binding을 추가하지 않는다. - [ ] `ChatMainFragment` 내부에 하단 `BottomNavigationView`를 추가하지 않는다. - [ ] `chatType=DM` item 클릭 이동을 이번 범위에서 구현하지 않는다. - [ ] 플로팅 버튼 클릭 이동을 이번 범위에서 구현하지 않는다. - [ ] WebSocket/SSE, pull-to-refresh, skeleton/shimmer를 추가하지 않는다. - [ ] API schema 필드명을 임의 변경하지 않는다. --- ## Verification Log - 2026-06-09: `docs/20260609_채팅_탭_페이지/prd.md`, `docs/agent-guides/work-plan-docs.md`, 기존 `ChatMainFragment`, `fragment_v2_main_chat.xml`, `HomeChatModels.kt`, `HomeRecommendationApi/Repository/ViewModel`, `AppDI`, `CapsuleTabBarView`, `ChatRoomActivity.newIntent`, `RelativeTimeFormatter`, locale string 리소스를 확인해 plan-task를 작성했다. - 2026-06-09: 이번 단계는 계획 문서 작성만 수행했으며 구현/빌드/테스트는 실행하지 않았다. - 2026-06-09: 사용자 피드백에 따라 신규 `view_title_bar_chat.xml` 생성 계획을 제거하고, 기존 `view_title_bar_default.xml`에 `ll_title_bar_actions` 가변 action container를 추가해 cash/search icon을 구성하는 계획으로 수정했다. - 2026-06-09: Phase 1.1 확인 완료. `ChatMainFragment.kt`는 현재 `BaseFragment`만 상속하는 빈 구조이고, `fragment_v2_main_chat.xml`은 black `FrameLayout`만 가진 초기 상태다. `MainV2Activity.kt`가 `BottomNavigationView` item 선택과 `MainV2Tab.CHAT -> ChatMainFragment()` 전환을 담당하며, `activity_main_v2.xml`의 `bottom_navigation`은 Activity 레벨에 존재한다. `ChatRoomActivity.newIntent(context, roomId)`는 `extra_room_id`를 담아 채팅방 Activity로 이동하는 기존 진입점임을 확인했다. - 2026-06-09: Phase 1.2 확인 완료. `CapsuleTabBarView.kt`는 `setMenus`, `selectTab`, `setOnTabSelectedListener`를 제공하고 `view_capsule_tab_bar.xml`은 horizontal scroll container를 포함한다. `view_title_bar_default.xml`은 제목형 title bar로 재사용 가능하나 현재 `tv_title_bar_title`, `iv_title_bar_menu`만 있고 가변 action container는 아직 없다. `view_title_bar_home.xml`은 `ic_bar_cash`, `ic_bar_search` 우측 아이콘 배치 예시를 제공하며, `ic_plus_no_bg` 리소스는 기존 리소스로 참조 가능함을 확인했다. Phase 1은 구조 확인/문서 갱신만 수행했으므로 빌드/테스트는 실행하지 않았다. - 2026-06-09: Phase 2.1 완료. `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/data/HomeChatModels.kt`를 `ChatRoomModels.kt`로 정리했고, `ChatRoomListPageResponse`, `ChatRoomListItemResponse` DTO 필드는 변경하지 않았다. - 2026-06-09: Phase 2.2 완료. `ChatRoomApi`에 `GET /api/v2/chat/rooms`, `Authorization`, `filter`, nullable `cursor` query와 `Single>` 반환 계약을 추가했고, `ChatRoomRepository.getChatRooms(token, filter, cursor)`에서 API 호출을 위임하도록 작성했다. - 2026-06-09: Phase 2.3 완료 범위. `AppDI`의 `networkModule`에 `ChatRoomApi`, `repositoryModule`에 `ChatRoomRepository`를 등록했다. `ChatMainViewModel`은 현재 소스 파일이 없고 Phase 4.2 생성 대상이라 이번 단계에서 등록하면 컴파일이 실패하므로, ViewModel 등록은 Phase 4.2 구현 시 수행하도록 이연했다. - 2026-06-09: Phase 2 검증 완료. `rg -n "HomeChatModels|ChatRoomModels|ChatRoomApi|ChatRoomRepository|ChatMainViewModel" "app/src/main/java" "app/src/test/java"`로 `HomeChatModels` 잔여 참조가 없고 `ChatRoomApi`, `ChatRoomRepository`, `AppDI` 등록만 존재함을 확인했다. `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 43s`로 Kotlin 컴파일이 성공했다. 빌드 중 Agora namespace 중복 warning과 Gradle deprecation warning이 출력됐으나 Phase 2 변경으로 인한 컴파일 실패는 없었다. - 2026-06-09: Phase 3.1/3.3/3.5 RED 테스트를 추가했다. `ChatRoomFilterTest`, `ChatRoomTimeTextFormatterTest`, `ChatRoomMapperTest` 실행 시 production model/formatter/mapper 및 string 미구현으로 `Unresolved reference 'ChatRoomFilter'`, `formatChatRoomLastMessageTime`, `ChatRoomType`, `toUiItem`, `screen_chat_time_*` 컴파일 실패가 발생해 RED 상태를 확인했다. 병렬 Gradle 실행 중 Kotlin incremental cache 충돌 메시지가 함께 출력되어 이후 검증은 순차 실행으로 전환했다. - 2026-06-09: Phase 3.2 완료. `ChatRoomFilter` enum을 추가하고 tab index `0/1/2`를 `ALL/AI/DM`으로 매핑했으며, 유효하지 않은 index는 `ALL`로 처리했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomFilterTest"` 실행 결과 `BUILD SUCCESSFUL in 1m 23s`로 통과했다. - 2026-06-09: Phase 3.4 완료. `screen_chat_time_just_now`, `screen_chat_time_minutes`, `screen_chat_time_hours`, `screen_chat_time_days`를 `values`, `values-en`, `values-ja`에 추가하고, `formatChatRoomLastMessageTime(...)`을 `SimpleDateFormat`, `Calendar`, `TimeZone`, `Locale` 기반으로 구현했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomTimeTextFormatterTest"` 실행 결과 `BUILD SUCCESSFUL in 9s`로 통과했다. - 2026-06-09: Phase 3.6 완료. `ChatRoomType`, `ChatRoomListUiItem`, `ChatRoomListUiState`와 `ChatRoomListItemResponse.toUiItem(context)`, `List.toUiItems(context)` mapper를 추가했다. `AI`는 Direct badge 미표시, `DM`은 표시, 알 수 없는 `chatType`은 제외하도록 구현했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomMapperTest"` 실행 결과 `BUILD SUCCESSFUL in 7s`로 통과했다. - 2026-06-09: Phase 3 통합 검증 완료. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"`는 `BUILD SUCCESSFUL in 9s`, `./gradlew :app:mergeDebugResources`는 `BUILD SUCCESSFUL in 1s`, `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL in 1s`로 통과했다. 최초 `./gradlew :app:ktlintCheck`는 `ChatRoomTimeTextFormatterTest.kt` 긴 줄로 실패했으며, 줄바꿈 수정 후 재실행한 `./gradlew :app:ktlintCheck`는 `BUILD SUCCESSFUL in 4s`로 통과했다. ktlint 실행 중 `.editorconfig`의 `disabled_rules` deprecation warning은 기존 설정 경고로 남아 있다. - 2026-06-09: 자체 점검 중 `ChatRoomTimeTextFormatter.kt`의 parse pattern 순회 예외 처리에 `continue`를 명시하도록 보강했다. 보강 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"`는 `BUILD SUCCESSFUL in 19s`, `./gradlew :app:ktlintCheck`는 `BUILD SUCCESSFUL in 4s`로 재통과했다. - 2026-06-09: 사용자 지적에 따라 Phase 3 테스트 메소드명을 저장소 가이드에 맞춰 한글 시나리오 설명으로 수정했다. `rg`로 `ChatRoomFilterTest`, `ChatRoomTimeTextFormatterTest`, `ChatRoomMapperTest`의 테스트명이 모두 한글 설명임을 확인했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"`는 `BUILD SUCCESSFUL in 12s`, `./gradlew :app:ktlintCheck`는 `BUILD SUCCESSFUL in 4s`로 통과했다. - 2026-06-09: 리뷰 게이트에서 `ChatRoomTimeTextFormatter`의 ISO-8601 offset 분 단위 파싱 결함과 trailing garbage 부분 파싱 가능성이 발견되어 보강했다. `분 단위 offset이 포함된 ISO 시간은 offset 전체를 반영한다` 테스트를 추가해 기존 구현에서 `ComparisonFailure` RED를 확인했고, parser를 `ParsePosition` 기반 전체 문자열 소비 검증과 `XXX`, `XX`, `X` 순서의 구체 패턴 우선순위로 수정했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomTimeTextFormatterTest"`는 `BUILD SUCCESSFUL in 9s`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"`는 `BUILD SUCCESSFUL in 6s`, `./gradlew :app:mergeDebugResources`는 `BUILD SUCCESSFUL in 1s`, `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL in 1s`, `./gradlew :app:ktlintCheck`는 `BUILD SUCCESSFUL in 3s`로 통과했다. - 2026-06-09: 최종 컨텍스트 리뷰에서 mapper 테스트가 `lastMessageAt`의 formatter 결과 문자열 매핑을 `isNotBlank()`로만 확인한다는 점과 신규 테스트 추가에 따른 `docs/agent-guides/build-test-style.md` 단일 실행 예시 갱신 누락이 발견되어 보강했다. `ChatRoomMapperTest`는 `formatChatRoomLastMessageTime(context, lastMessageAt)` 결과와 `lastMessageTimeText`를 직접 비교하도록 수정했고, `build-test-style.md`에 `ChatRoomFilterTest`, `ChatRoomTimeTextFormatterTest`, `ChatRoomMapperTest`, `kr.co.vividnext.sodalive.v2.main.chat.*` 실행 예시를 추가했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomMapperTest"`는 `BUILD SUCCESSFUL in 20s`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"`는 `BUILD SUCCESSFUL in 6s`, `./gradlew :app:ktlintCheck`는 `BUILD SUCCESSFUL in 3s`, `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 1s`로 통과했다. - 2026-06-10: Phase 4.1 RED 테스트 완료. `ChatMainViewModelTest`를 추가해 초기 `loadFirstPage()`의 `ALL/null` 요청, filter 변경 시 첫 페이지 재요청과 기존 목록 교체, 동일 filter 중복 선택 무시, Content/Empty/Error 상태, `hasMore`와 `nextCursor` 기반 append pagination, append 중복 요청 방지, failure/data null/Throwable의 unknown error toast를 검증했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"` 최초 실행은 `ChatMainViewModel`, `loadFirstPage`, `selectFilter`, `loadNextPage`, `chatRoomStateLiveData`, `toastLiveData` 미구현으로 `Unresolved reference` 컴파일 실패가 발생해 RED 상태를 확인했다. - 2026-06-10: Phase 4.2 구현 완료. `ChatMainViewModel`을 추가해 `ChatRoomRepository`, `ChatRoomFilter`, `SharedPreferenceManager.token`, `ChatRoomListUiState`, `ToastMessage(R.string.common_error_unknown)`, `toUiItems(context)`를 연결했고, first page loading/clear, filter 선택, cursor pagination append, loading/error/toast 상태를 관리하도록 구현했다. `ChatRoomMappers.toUiItems(context)`가 Android `Context`를 요구하므로 `ChatMainViewModel(repository, context)` 생성자를 사용하고 `AppDI`에는 `viewModel { ChatMainViewModel(get(), androidContext()) }`로 등록했다. GREEN 전환 중 최초 구현은 toast를 `postValue`로 emit해 즉시 관찰 테스트 3건이 실패했으며, 같은 main-thread 흐름에서 `_toastLiveData.value`를 사용하도록 최소 수정한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"`가 `BUILD SUCCESSFUL in 19s`로 통과했다. - 2026-06-10: Phase 4 검증 완료. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"`는 `BUILD SUCCESSFUL in 10s`, `./gradlew :app:mergeDebugResources`는 `BUILD SUCCESSFUL in 1s`, `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL in 1s`, `./gradlew :app:ktlintCheck`는 `BUILD SUCCESSFUL in 5s`로 통과했다. ktlint 실행 중 `.editorconfig`의 `disabled_rules` deprecation warning과 Gradle deprecation warning은 기존 경고로 남아 있다. - 2026-06-10: Phase 4 리뷰 게이트에서 filter 변경 중 늦게 도착한 first page/next page 응답이 현재 filter 상태를 덮거나 append할 수 있는 race 조건이 발견되어 보강했다. `filter 변경 전 첫 페이지 응답이 늦게 도착하면 현재 filter 목록을 덮어쓰지 않는다`, `filter 변경 전 다음 페이지 응답이 늦게 도착하면 현재 filter 목록에 append하지 않는다` 테스트를 추가했고, 기존 구현에서 각각 `AssertionError`로 RED 실패를 확인했다. 이후 `ChatMainViewModel`에 `requestGeneration` guard를 추가하고 first page 시작 시 `_isAppending=false`로 reset해 stale first/append 응답을 무시하도록 수정했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"`는 `BUILD SUCCESSFUL in 18s`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"`는 `BUILD SUCCESSFUL in 8s`, `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL in 1s`, `./gradlew :app:ktlintCheck`는 `BUILD SUCCESSFUL in 3s`, `./gradlew :app:mergeDebugResources`는 `BUILD SUCCESSFUL in 1s`로 재통과했다. - 2026-06-10: `ChatMainViewModel`에서 `Context`를 필드로 보관해 `This field leaks a context object` 경고가 발생할 수 있는 구조를 점검했다. 시간 포맷은 리소스가 필요한 표시 계층 책임으로 두는 편이 적절하다고 판단해 `ChatRoomListUiItem`은 `lastMessageTimeText` 대신 원본 `lastMessageAt`을 보관하도록 바꾸고, `ChatRoomMappers.toUiItem()/toUiItems()`와 `ChatMainViewModel`에서 `Context` 의존을 제거했다. `AppDI` 등록도 `viewModel { ChatMainViewModel(get()) }`로 되돌렸으며, Phase 5 Adapter 구현 시 bind 직전에 `formatChatRoomLastMessageTime(context, item.lastMessageAt)`을 호출하도록 계획 문서의 계약을 갱신했다. 테스트 계약 변경 후 최초 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomMapperTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"`는 기존 production이 아직 `context` 파라미터와 `lastMessageTimeText`를 요구해 `No value passed for parameter 'context'`, `Unresolved reference 'lastMessageAt'`로 RED 실패했다. 구현 수정 후 같은 명령은 `BUILD SUCCESSFUL in 19s`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"`는 `BUILD SUCCESSFUL in 9s`, `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL in 1s`, `./gradlew :app:ktlintCheck`는 `BUILD SUCCESSFUL in 4s`로 통과했다. ktlint의 `.editorconfig disabled_rules` deprecation warning과 Gradle deprecation warning은 기존 경고로 남아 있다. - 2026-06-10: Phase 5 완료. Task 5.1에서 `bg_chat_direct_badge.xml`, `item_v2_chat_room.xml`을 생성하고 `strings.xml`/`values-en`/`values-ja`에 `screen_chat_direct_badge`, `screen_chat_filter_all/ai/dm` 문자열을 추가했다. `./gradlew :app:mergeDebugResources`는 `BUILD SUCCESSFUL in 6s`로 통과했다. Task 5.2에서 `ChatRoomListAdapterTest`를 추가해 DM/AI badge visibility, name/lastMessage 표시, 시간 포맷 변환, item click listener, unread dot 부재를 검증했다. 최초 실행은 `ChatRoomListAdapter` 미구현으로 RED 실패를 확인했다. Task 5.3에서 `ChatRoomListAdapter`를 구현해 `loadUrl`, `formatChatRoomLastMessageTime`, `showDirectBadge` visibility를 연결했다. 최초 테스트 실행 시 `ImageLoaderProvider is not initialized` 예외가 발생해 테스트 `@Before`에서 `ImageLoaderProvider.init(context)`를 추가했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomListAdapterTest"`는 `BUILD SUCCESSFUL in 8s`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"`는 `BUILD SUCCESSFUL in 7s`, `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL in 1m 15s`, `./gradlew :app:ktlintCheck`는 `BUILD SUCCESSFUL in 3s`로 통과했다. - 2026-06-10: Phase 5 리뷰와 Figma `177:3469` 대조를 추가 수행했다. 기존 `dimens.xml`, `typography.xml`, `bg_character_chat_count_badge.xml`, `loadUrl`/Coil 원형 변환 패턴을 확인해 `item_v2_chat_room.xml`의 하드코딩 spacing/typography 일부를 기존 토큰으로 치환하고, `bg_chat_direct_badge.xml`의 `4dp` radius를 `@dimen/radius_4`로 변경했다. Figma 기준에 맞춰 item padding `14dp`, profile/body gap `14dp`, title/message gap `12dp`, Direct badge horizontal padding `4dp`, badge text `14sp`, time/body medium typography를 반영했으며, profile image는 기존 `loadUrl` builder에 `CircleCropTransformation()`을 적용하도록 수정했다. `ChatRoomListAdapterTest`는 시간 텍스트를 formatter 결과와 직접 비교하도록 강화하고, profile image 원형 변환 테스트를 추가했다. RED 확인으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomListAdapterTest"`가 `Unresolved reference 'profileImageTransformations'`로 실패한 뒤 production 구현을 추가해 GREEN 전환했다. 리뷰 게이트에서는 AI row에서 Direct badge가 `GONE`일 때 title/time `24dp` gap이 사라질 수 있다는 차단 의견을 받아 `tv_name`에 `app:layout_goneMarginEnd="@dimen/spacing_24"`를 추가했고, 재리뷰에서 무조건 승인을 받았다. 최종 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomListAdapterTest"`는 `BUILD SUCCESSFUL in 23s`, `./gradlew :app:mergeDebugResources`는 `BUILD SUCCESSFUL in 15s`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"`는 `BUILD SUCCESSFUL in 30s`, `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL in 7s`, `./gradlew :app:ktlintCheck`는 `BUILD SUCCESSFUL in 17s`로 통과했다. Gradle deprecation warning은 기존 경고로 남아 있다. - 2026-06-10: Phase 6 완료. `ChatMainFragmentLayoutTest`를 추가해 기본 title bar의 `tv_title_bar_title`, `ll_title_bar_actions`, `iv_title_bar_menu` 유지와 chat fragment layout의 black root, title bar/capsule tab/RecyclerView/floating button 배치, bottom navigation/unread dot 부재를 검증했다. `CapsuleTabBarViewTest`를 추가해 selected tab은 `bg_capsule_tab_selected`와 black text, normal tab은 `bg_capsule_tab_normal`과 white text를 사용하는지 검증했다. 최초 RED 검증 시 Gradle 병렬/장시간 실행이 timeout 또는 사용자 중단되어 assertion 결과까지 확보하지 못했으나, 테스트 작성 시점의 기존 구현은 `ll_title_bar_actions`, `view_chat_filter_tabs`, floating button layout, selected black text가 없어 실패 조건이 명확했다. 이후 `view_title_bar_default.xml`에 `ll_title_bar_actions` container를 추가하고 기존 `iv_title_bar_menu`를 내부로 이동했으며, `fragment_v2_main_chat.xml`을 `ConstraintLayout` 기반 title bar/capsule tab/list/floating button 구조로 변경하고 `bg_chat_floating_button.xml`을 추가했다. `CapsuleTabBarView`는 selected text `R.color.black`, normal text `R.color.white`로 보정했다. 구현 후 최초 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest"`는 테스트 helper의 XML source 경로 문제로 `FileNotFoundException` 실패했고, repository/module 실행 위치 모두에서 동작하도록 helper를 수정한 뒤 같은 명령은 `BUILD SUCCESSFUL in 14s`로 통과했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.*CapsuleTab*"`는 중복 테스트 정리 후 `BUILD SUCCESSFUL in 10s`, `./gradlew :app:mergeDebugResources`는 `BUILD SUCCESSFUL in 1s`, `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL in 1s`, `./gradlew :app:ktlintCheck`는 `BUILD SUCCESSFUL in 3s`로 통과했다. ktlint의 `.editorconfig disabled_rules` deprecation warning과 Gradle deprecation warning은 기존 경고로 남아 있다. Phase 6 리뷰 게이트는 `UNCONDITIONAL APPROVAL`을 받았다.