47 KiB
채팅 탭 페이지 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 filterquery는ALL,AI,DM중 하나만 사용한다.cursorquery는 다음 페이지 요청에 사용한다. 첫 페이지는cursor=null로 요청한다.- 현재 선택되지 않은
CapsuleTabBarViewtab 터치 시 첫 페이지 API를 호출하고 기존 목록을 clear한 뒤 새 목록으로 세팅한다. hasMore=true와nextCursor가 제공되면 스크롤 pagination으로 다음 페이지를 append한다.- unread dot은 표시하지 않는다.
chatType은AI,DM문자열이다.lastMessageAt은 ISO-8601 문자열이며 디바이스 timezone으로 변환 후 표시한다.- 일주일까지는 상대 시간 문구로 표시하고, 그보다 오래된 올해 메시지는 locale별 날짜 포맷, 다른 연도 메시지는
yyyy.MM.dd로 표시한다. chatType=AIitem 클릭은ChatRoomActivity.newIntent(context, roomId)로 연결한다.chatType=DMitem 클릭과 플로팅 버튼 클릭은 이번 범위에서 실제 이동을 연결하지 않는다.- 구현 완료 후 최소 다음 명령을 실행한다.
./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.ktChatRoomListPageResponse,ChatRoomListItemResponseDTO를 유지한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/data/ChatRoomApi.ktGET /api/v2/chat/roomsRetrofit 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.ktALL,AI,DMfilter와 tab index 매핑을 정의한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomUiModels.ktChatRoomListUiItem,ChatRoomListUiState를 정의한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomMappers.kt- DTO를 UI item으로 변환하되 시간 표시는 수행하지 않고 원본
lastMessageAt을 유지한다.
- DTO를 UI item으로 변환하되 시간 표시는 수행하지 않고 원본
- 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.ktChatRoomApi,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.ktapp/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatRoomTimeTextFormatterTest.ktapp/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatRoomMapperTest.ktapp/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainViewModelTest.ktapp/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatRoomListAdapterTest.ktapp/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragmentLayoutTest.kt
Phase 1: 기존 구조 확인과 범위 고정
-
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)를 사용한다.
- 확인:
-
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 네이밍과 계약 작성
-
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 - 유지:
ChatRoomListPageResponseChatRoomListItemResponse
- 검증 명령:
./gradlew :app:compileDebugKotlin - 기대 결과: 파일명 변경 후 기존 import가 없거나 갱신되어 Kotlin compile이 성공한다.
- Rename:
-
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<ApiResponse<ChatRoomListPageResponse>>
- Repository 계약:
fun getChatRooms(token: String, filter: String, cursor: String?): Single<ApiResponse<ChatRoomListPageResponse>>
- 검증 명령:
./gradlew :app:compileDebugKotlin - 기대 결과: Retrofit annotation/import 포함 컴파일 성공.
- Create:
-
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()) }
- import
- Phase 2 실행 기록:
ChatMainViewModel은 Phase 4.2 생성 대상이며 현재 파일이 없어 등록 시 컴파일이 실패하므로, 이번 단계에서는ChatRoomApi,ChatRoomRepository만 등록하고 ViewModel 등록은 Phase 4.2에서 수행한다. - 검증 명령:
./gradlew :app:compileDebugKotlin - 기대 결과: Koin 등록과 import 컴파일 성공.
- Modify:
Phase 3: Filter, 시간 formatter, mapper 작성
-
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에서 중복 호출하지 않는다.
- index
- 검증 명령:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomFilterTest" - 기대 결과:
ChatRoomFilter미구현으로 RED 실패.
- Create:
-
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.
- Create:
-
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
- 기준 now:
- 테스트 케이스:
- 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
- 30초 전 ->
- 검증 명령:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomTimeTextFormatterTest" - 기대 결과: formatter 미구현으로 RED 실패.
- Create:
-
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_nowscreen_chat_time_minutesscreen_chat_time_hoursscreen_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 nowdiff < 1시간: minutesdiff < 1일: hoursdiff < 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.
- Modify:
-
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 실패.
- Create:
-
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 ChatRoomListUiStateLoadingContent(val items: List<ChatRoomListUiItem>, val isAppending: Boolean = false)EmptyError(val message: String?)
fun ChatRoomListItemResponse.toUiItem(): ChatRoomListUiItem?fun List<ChatRoomListItemResponse>.toUiItems(): List<ChatRoomListUiItem>- 화면 표시용 시간 문구는
Activity/Fragment/Adapter등 표시 계층에서formatChatRoomLastMessageTime(context, item.lastMessageAt)으로 변환한다.
- 검증 명령:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomMapperTest" - 기대 결과: PASS.
- Create:
Phase 4: ViewModel pagination/filter 동작 작성
-
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 실패.
- Create:
-
Task 4.2: ViewModel 구현
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainViewModel.kt - 구현:
currentFilter: ChatRoomFilter = ChatRoomFilter.ALLcurrentItems: List<ChatRoomListUiItem>nextCursor: String?hasMore: BooleanisLoading,isAppending,chatRoomStateLiveData,toastLiveDataloadFirstPage(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.
- Create:
Phase 5: 채팅방 list item UI와 Adapter 작성
-
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
18spbold, white, maxLines 1 - Direct badge:
soda_400, radius4dp, Pattaya font, textDirect - time:
14sp,gray_500, right align - last message:
16sp,gray_500, maxLines 1, ellipsize end - unread dot view는 만들지 않는다.
- item root height는 content wrap, vertical padding
- 검증 명령:
./gradlew :app:mergeDebugResources - 기대 결과: 신규 layout/drawable/string resource merge 성공.
- Create:
-
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가 존재하지 않는다.
- DM item bind 시 Direct badge가
- 검증 명령:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomListAdapterTest" - 기대 결과: Adapter 미구현으로 RED 실패.
- Create:
-
Task 5.3: Adapter 구현
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ui/ChatRoomListAdapter.kt - 구현:
RecyclerView.AdaptersubmitItems(items: List<ChatRoomListUiItem>)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.
- Create:
Phase 6: 채팅 탭 layout과 CapsuleTab 색상 보정
-
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_menuid가 유지된다. ll_title_bar_actions는 우측 아이콘을 2개 이상 담을 수 있는 horizontal container다.
- 검증 명령:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest" - 기대 결과: action container 미구현으로 RED 실패.
- Create:
-
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_menuid는 유지한다. iv_title_bar_menu를LinearLayoutidll_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.
- Modify:
-
Task 6.3: Fragment layout RED 테스트 작성
- Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragmentLayoutTest.kt - 테스트 케이스:
fragment_v2_main_chat.xmlroot background가@color/blackview_title_bar_defaultinclude가 존재한다.view_capsule_tab_barinclude가 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 실패.
- Create:
-
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: height60dp, title은 Fragment에서대화로 설정한다.view_capsule_tab_bar: height52dp, 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 없음
- root를
- 검증 명령:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest"및./gradlew :app:mergeDebugResources - 기대 결과: PASS.
- Create:
-
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 실패.
- Modify:
-
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 정책은 유지한다.
- selected이면
- 검증 명령:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.*CapsuleTab*" - 기대 결과: PASS.
- Modify:
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_searchImageView를 추가한다.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 실패.
- Modify:
-
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 submitEmpty,Error-> adapter emptyLoading-> 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.
- Modify:
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를 삭제하거나 덮어쓰지 않고 하단에 새 항목으로 누적한다.
- Modify:
구현 제외 체크리스트
- unread dot view, drawable, binding을 추가하지 않는다.
ChatMainFragment내부에 하단BottomNavigationView를 추가하지 않는다.chatType=DMitem 클릭 이동을 이번 범위에서 구현하지 않는다.- 플로팅 버튼 클릭 이동을 이번 범위에서 구현하지 않는다.
- 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<FragmentV2MainChatBinding>만 상속하는 빈 구조이고,fragment_v2_main_chat.xml은 blackFrameLayout만 가진 초기 상태다.MainV2Activity.kt가BottomNavigationViewitem 선택과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,ChatRoomListItemResponseDTO 필드는 변경하지 않았다. - 2026-06-09: Phase 2.2 완료.
ChatRoomApi에GET /api/v2/chat/rooms,Authorization,filter, nullablecursorquery와Single<ApiResponse<ChatRoomListPageResponse>>반환 계약을 추가했고,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 완료.
ChatRoomFilterenum을 추가하고 tab index0/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<ChatRoomListItemResponse>.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_rulesdeprecation 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 전체를 반영한다테스트를 추가해 기존 구현에서ComparisonFailureRED를 확인했고, 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)가 AndroidContext를 요구하므로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_rulesdeprecation 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에requestGenerationguard를 추가하고 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_rulesdeprecation 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,showDirectBadgevisibility를 연결했다. 최초 테스트 실행 시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의4dpradius를@dimen/radius_4로 변경했다. Figma 기준에 맞춰 item padding14dp, profile/body gap14dp, title/message gap12dp, Direct badge horizontal padding4dp, badge text14sp, time/body medium typography를 반영했으며, profile image는 기존loadUrlbuilder에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/time24dpgap이 사라질 수 있다는 차단 의견을 받아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_actionscontainer를 추가하고 기존iv_title_bar_menu를 내부로 이동했으며,fragment_v2_main_chat.xml을ConstraintLayout기반 title bar/capsule tab/list/floating button 구조로 변경하고bg_chat_floating_button.xml을 추가했다.CapsuleTabBarView는 selected textR.color.black, normal textR.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_rulesdeprecation warning과 Gradle deprecation warning은 기존 경고로 남아 있다. Phase 6 리뷰 게이트는UNCONDITIONAL APPROVAL을 받았다. - 2026-06-10: Phase 6 코드리뷰 경미 개선안 반영. (1)
btn_chat_floating의contentDescription을@null에서 신규 stringscreen_chat_floating_button(values/values-en/values-ja)으로 교체해 접근성/의도를 명시했다. (2)ChatMainFragmentLayoutTest의 기본 title bar 테스트가tv_title_bar_title이 title bar 직속이며ll_title_bar_actions보다 앞 index임을 검증하도록 강화했다. (3) unread 부재 검증을 XML 파일 텍스트 파싱(chatMainLayoutSource())에서 inflated view-tree id 순회(containsViewIdContaining("unread"))로 교체하고 미사용assertNotNullimport를 제거했다. 환경에 동작 가능한 JDK(/usr/bin/java는 스텁)가 없어 Gradle 테스트/빌드 실행은 불가했고, 대신 IDE lint로ChatMainFragmentLayoutTest.kt,fragment_v2_main_chat.xml,values/strings.xml에서 오류 없음을 확인했다. (후속: JDK 사용 가능 환경에서./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest",:app:mergeDebugResources,:app:ktlintCheck재실행 필요.)