475 lines
29 KiB
Markdown
475 lines
29 KiB
Markdown
# 채팅 탭 페이지 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으로 변환한다.
|
|
- 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 네이밍과 계약 작성
|
|
|
|
- [ ] **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()) }`
|
|
- 검증 명령: `./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=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 실패.
|
|
|
|
- [ ] **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.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 실패.
|
|
|
|
- [ ] **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.
|
|
|
|
- [ ] **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.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<FragmentV2MainChatBinding>`만 상속하는 빈 구조이고, `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은 구조 확인/문서 갱신만 수행했으므로 빌드/테스트는 실행하지 않았다.
|