31 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으로 변환한다.
- 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은 formatter 결과 문자열로 매핑한다.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, lastMessageTimeText: String, showDirectBadge: Boolean)sealed class ChatRoomListUiStateLoadingContent(val items: List<ChatRoomListUiItem>, val isAppending: Boolean = false)EmptyError(val message: String?)
fun ChatRoomListItemResponse.toUiItem(context: Context): ChatRoomListUiItem?fun List<ChatRoomListItemResponse>.toUiItems(context: Context): List<ChatRoomListUiItem>
- 검증 명령:
./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를 로딩한다. 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 변경으로 인한 컴파일 실패는 없었다.