Files
sodalive-android/docs/20260609_채팅_탭_페이지/plan-task.md

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
  • filter query는 ALL, AI, DM 중 하나만 사용한다.
  • cursor query는 다음 페이지 요청에 사용한다. 첫 페이지는 cursor=null로 요청한다.
  • 현재 선택되지 않은 CapsuleTabBarView tab 터치 시 첫 페이지 API를 호출하고 기존 목록을 clear한 뒤 새 목록으로 세팅한다.
  • hasMore=truenextCursor가 제공되면 스크롤 pagination으로 다음 페이지를 append한다.
  • unread dot은 표시하지 않는다.
  • chatTypeAI, 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으로 변환한다.
  • 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: 기존 구조 확인과 범위 고정

  • 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
    • 유지:
      • ChatRoomListPageResponse
      • ChatRoomListItemResponse
    • 검증 명령: ./gradlew :app:compileDebugKotlin
    • 기대 결과: 파일명 변경 후 기존 import가 없거나 갱신되어 Kotlin compile이 성공한다.
  • 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 포함 컴파일 성공.
  • 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 작성

  • 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 실패.
  • 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.
  • 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 실패.
  • 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.
  • 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 실패.
  • 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 ChatRoomListUiState
        • Loading
        • Content(val items: List<ChatRoomListUiItem>, val isAppending: Boolean = false)
        • Empty
        • Error(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.

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=falseloadNextPage()가 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 실패.
  • Task 4.2: ViewModel 구현

    • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainViewModel.kt
    • 구현:
      • currentFilter: ChatRoomFilter = ChatRoomFilter.ALL
      • currentItems: List<ChatRoomListUiItem>
      • 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 작성

  • 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 성공.
  • 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 실패.
  • Task 5.3: Adapter 구현

    • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ui/ChatRoomListAdapter.kt
    • 구현:
      • RecyclerView.Adapter
      • submitItems(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.

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.xmltv_title_bar_title이 존재한다.
      • view_title_bar_default.xmlll_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 실패.
  • 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_menuLinearLayout 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.
  • 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 실패.
  • 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.
  • 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 실패.
  • 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.ktChatMainViewModel을 주입한다.
      • 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에 LinearLayoutManagerChatRoomListAdapter를 연결한다.
      • 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: AIChatRoomActivity로 이동, 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.xmlll_title_bar_actions 가변 action container를 추가해 cash/search icon을 구성하는 계획으로 수정했다.
  • 2026-06-09: Phase 1.1 확인 완료. ChatMainFragment.kt는 현재 BaseFragment<FragmentV2MainChatBinding>만 상속하는 빈 구조이고, fragment_v2_main_chat.xml은 black FrameLayout만 가진 초기 상태다. MainV2Activity.ktBottomNavigationView item 선택과 MainV2Tab.CHAT -> ChatMainFragment() 전환을 담당하며, activity_main_v2.xmlbottom_navigation은 Activity 레벨에 존재한다. ChatRoomActivity.newIntent(context, roomId)extra_room_id를 담아 채팅방 Activity로 이동하는 기존 진입점임을 확인했다.
  • 2026-06-09: Phase 1.2 확인 완료. CapsuleTabBarView.ktsetMenus, 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.xmlic_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.ktChatRoomModels.kt로 정리했고, ChatRoomListPageResponse, ChatRoomListItemResponse DTO 필드는 변경하지 않았다.
  • 2026-06-09: Phase 2.2 완료. ChatRoomApiGET /api/v2/chat/rooms, Authorization, filter, nullable cursor query와 Single<ApiResponse<ChatRoomListPageResponse>> 반환 계약을 추가했고, ChatRoomRepository.getChatRooms(token, filter, cursor)에서 API 호출을 위임하도록 작성했다.
  • 2026-06-09: Phase 2.3 완료 범위. AppDInetworkModuleChatRoomApi, repositoryModuleChatRoomRepository를 등록했다. 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 변경으로 인한 컴파일 실패는 없었다.