docs(chat): 채팅방 ViewModel 검증을 기록한다

This commit is contained in:
2026-06-10 12:00:34 +09:00
parent bb17f0014a
commit 896935e19a

View File

@@ -44,7 +44,7 @@
- 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/ChatRoomUiModels.kt`
- `ChatRoomListUiItem`, `ChatRoomListUiState`를 정의한다. - `ChatRoomListUiItem`, `ChatRoomListUiState`를 정의한다.
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomMappers.kt` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomMappers.kt`
- DTO를 UI item으로 변환한다. - DTO를 UI item으로 변환하되 시간 표시는 수행하지 않고 원본 `lastMessageAt`을 유지한다.
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomTimeTextFormatter.kt` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomTimeTextFormatter.kt`
- ISO-8601 시간 문자열을 화면 표시 문자열로 변환한다. - ISO-8601 시간 문자열을 화면 표시 문자열로 변환한다.
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainViewModel.kt` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainViewModel.kt`
@@ -212,7 +212,7 @@
- 테스트 케이스: - 테스트 케이스:
- `chatType="AI"`는 Direct badge 미표시 item으로 매핑한다. - `chatType="AI"`는 Direct badge 미표시 item으로 매핑한다.
- `chatType="DM"`은 Direct badge 표시 item으로 매핑한다. - `chatType="DM"`은 Direct badge 표시 item으로 매핑한다.
- `lastMessageAt`formatter 결과 문자열로 매핑한다. - `lastMessageAt`원본 ISO-8601 문자열 그대로 매핑한다.
- `roomId`, `targetName`, `targetImageUrl`, `lastMessage`는 그대로 전달한다. - `roomId`, `targetName`, `targetImageUrl`, `lastMessage`는 그대로 전달한다.
- 알 수 없는 `chatType`은 표시 대상에서 제외한다. - 알 수 없는 `chatType`은 표시 대상에서 제외한다.
- 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomMapperTest"` - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomMapperTest"`
@@ -223,14 +223,15 @@
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomMappers.kt` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomMappers.kt`
- 구현: - 구현:
- `enum class ChatRoomType { AI, DM }` - `enum class ChatRoomType { AI, DM }`
- `data class ChatRoomListUiItem(roomId: Long, chatType: ChatRoomType, targetName: String, targetImageUrl: String, lastMessage: String, lastMessageTimeText: String, showDirectBadge: Boolean)` - `data class ChatRoomListUiItem(roomId: Long, chatType: ChatRoomType, targetName: String, targetImageUrl: String, lastMessage: String, lastMessageAt: String, showDirectBadge: Boolean)`
- `sealed class ChatRoomListUiState` - `sealed class ChatRoomListUiState`
- `Loading` - `Loading`
- `Content(val items: List<ChatRoomListUiItem>, val isAppending: Boolean = false)` - `Content(val items: List<ChatRoomListUiItem>, val isAppending: Boolean = false)`
- `Empty` - `Empty`
- `Error(val message: String?)` - `Error(val message: String?)`
- `fun ChatRoomListItemResponse.toUiItem(context: Context): ChatRoomListUiItem?` - `fun ChatRoomListItemResponse.toUiItem(): ChatRoomListUiItem?`
- `fun List<ChatRoomListItemResponse>.toUiItems(context: Context): List<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"` - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomMapperTest"`
- 기대 결과: PASS. - 기대 결과: PASS.
@@ -238,7 +239,7 @@
### Phase 4: ViewModel pagination/filter 동작 작성 ### Phase 4: ViewModel pagination/filter 동작 작성
- [ ] **Task 4.1: ViewModel RED 테스트 작성** - [x] **Task 4.1: ViewModel RED 테스트 작성**
- Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainViewModelTest.kt` - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainViewModelTest.kt`
- 테스트 케이스: - 테스트 케이스:
- 초기 `loadFirstPage()``filter=ALL`, `cursor=null`로 API를 호출한다. - 초기 `loadFirstPage()``filter=ALL`, `cursor=null`로 API를 호출한다.
@@ -253,7 +254,7 @@
- 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"` - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"`
- 기대 결과: `ChatMainViewModel` 미구현으로 RED 실패. - 기대 결과: `ChatMainViewModel` 미구현으로 RED 실패.
- [ ] **Task 4.2: ViewModel 구현** - [x] **Task 4.2: ViewModel 구현**
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainViewModel.kt` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainViewModel.kt`
- 구현: - 구현:
- `currentFilter: ChatRoomFilter = ChatRoomFilter.ALL` - `currentFilter: ChatRoomFilter = ChatRoomFilter.ALL`
@@ -309,6 +310,7 @@
- `submitItems(items: List<ChatRoomListUiItem>)` - `submitItems(items: List<ChatRoomListUiItem>)`
- `onItemClick: (ChatRoomListUiItem) -> Unit` - `onItemClick: (ChatRoomListUiItem) -> Unit`
- Coil `loadUrl` 또는 기존 이미지 로딩 extension을 사용해 profile image를 로딩한다. - Coil `loadUrl` 또는 기존 이미지 로딩 extension을 사용해 profile image를 로딩한다.
- `lastMessageAt`은 bind 시점에 `formatChatRoomLastMessageTime(context, item.lastMessageAt)`으로 표시 문자열로 변환한다.
- `showDirectBadge`로 Direct badge visibility를 제어한다. - `showDirectBadge`로 Direct badge visibility를 제어한다.
- 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomListAdapterTest"` - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomListAdapterTest"`
- 기대 결과: PASS. - 기대 결과: PASS.
@@ -486,3 +488,8 @@
- 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: 사용자 지적에 따라 Phase 3 테스트 메소드명을 저장소 가이드에 맞춰 한글 시나리오 설명으로 수정했다. `rg``ChatRoomFilterTest`, `ChatRoomTimeTextFormatterTest`, `ChatRoomMapperTest`의 테스트명이 모두 한글 설명임을 확인했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"``BUILD SUCCESSFUL in 12s`, `./gradlew :app:ktlintCheck``BUILD SUCCESSFUL in 4s`로 통과했다.
- 2026-06-09: 리뷰 게이트에서 `ChatRoomTimeTextFormatter`의 ISO-8601 offset 분 단위 파싱 결함과 trailing garbage 부분 파싱 가능성이 발견되어 보강했다. `분 단위 offset이 포함된 ISO 시간은 offset 전체를 반영한다` 테스트를 추가해 기존 구현에서 `ComparisonFailure` RED를 확인했고, parser를 `ParsePosition` 기반 전체 문자열 소비 검증과 `XXX`, `XX`, `X` 순서의 구체 패턴 우선순위로 수정했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomTimeTextFormatterTest"``BUILD SUCCESSFUL in 9s`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"``BUILD SUCCESSFUL in 6s`, `./gradlew :app:mergeDebugResources``BUILD SUCCESSFUL in 1s`, `./gradlew :app:compileDebugKotlin``BUILD SUCCESSFUL in 1s`, `./gradlew :app:ktlintCheck``BUILD SUCCESSFUL in 3s`로 통과했다. - 2026-06-09: 리뷰 게이트에서 `ChatRoomTimeTextFormatter`의 ISO-8601 offset 분 단위 파싱 결함과 trailing garbage 부분 파싱 가능성이 발견되어 보강했다. `분 단위 offset이 포함된 ISO 시간은 offset 전체를 반영한다` 테스트를 추가해 기존 구현에서 `ComparisonFailure` RED를 확인했고, parser를 `ParsePosition` 기반 전체 문자열 소비 검증과 `XXX`, `XX`, `X` 순서의 구체 패턴 우선순위로 수정했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomTimeTextFormatterTest"``BUILD SUCCESSFUL in 9s`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"``BUILD SUCCESSFUL in 6s`, `./gradlew :app:mergeDebugResources``BUILD SUCCESSFUL in 1s`, `./gradlew :app:compileDebugKotlin``BUILD SUCCESSFUL in 1s`, `./gradlew :app:ktlintCheck``BUILD SUCCESSFUL in 3s`로 통과했다.
- 2026-06-09: 최종 컨텍스트 리뷰에서 mapper 테스트가 `lastMessageAt`의 formatter 결과 문자열 매핑을 `isNotBlank()`로만 확인한다는 점과 신규 테스트 추가에 따른 `docs/agent-guides/build-test-style.md` 단일 실행 예시 갱신 누락이 발견되어 보강했다. `ChatRoomMapperTest``formatChatRoomLastMessageTime(context, lastMessageAt)` 결과와 `lastMessageTimeText`를 직접 비교하도록 수정했고, `build-test-style.md``ChatRoomFilterTest`, `ChatRoomTimeTextFormatterTest`, `ChatRoomMapperTest`, `kr.co.vividnext.sodalive.v2.main.chat.*` 실행 예시를 추가했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomMapperTest"``BUILD SUCCESSFUL in 20s`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"``BUILD SUCCESSFUL in 6s`, `./gradlew :app:ktlintCheck``BUILD SUCCESSFUL in 3s`, `./gradlew tasks --all``BUILD SUCCESSFUL in 1s`로 통과했다. - 2026-06-09: 최종 컨텍스트 리뷰에서 mapper 테스트가 `lastMessageAt`의 formatter 결과 문자열 매핑을 `isNotBlank()`로만 확인한다는 점과 신규 테스트 추가에 따른 `docs/agent-guides/build-test-style.md` 단일 실행 예시 갱신 누락이 발견되어 보강했다. `ChatRoomMapperTest``formatChatRoomLastMessageTime(context, lastMessageAt)` 결과와 `lastMessageTimeText`를 직접 비교하도록 수정했고, `build-test-style.md``ChatRoomFilterTest`, `ChatRoomTimeTextFormatterTest`, `ChatRoomMapperTest`, `kr.co.vividnext.sodalive.v2.main.chat.*` 실행 예시를 추가했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomMapperTest"``BUILD SUCCESSFUL in 20s`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"``BUILD SUCCESSFUL in 6s`, `./gradlew :app:ktlintCheck``BUILD SUCCESSFUL in 3s`, `./gradlew tasks --all``BUILD SUCCESSFUL in 1s`로 통과했다.
- 2026-06-10: Phase 4.1 RED 테스트 완료. `ChatMainViewModelTest`를 추가해 초기 `loadFirstPage()``ALL/null` 요청, filter 변경 시 첫 페이지 재요청과 기존 목록 교체, 동일 filter 중복 선택 무시, Content/Empty/Error 상태, `hasMore``nextCursor` 기반 append pagination, append 중복 요청 방지, failure/data null/Throwable의 unknown error toast를 검증했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"` 최초 실행은 `ChatMainViewModel`, `loadFirstPage`, `selectFilter`, `loadNextPage`, `chatRoomStateLiveData`, `toastLiveData` 미구현으로 `Unresolved reference` 컴파일 실패가 발생해 RED 상태를 확인했다.
- 2026-06-10: Phase 4.2 구현 완료. `ChatMainViewModel`을 추가해 `ChatRoomRepository`, `ChatRoomFilter`, `SharedPreferenceManager.token`, `ChatRoomListUiState`, `ToastMessage(R.string.common_error_unknown)`, `toUiItems(context)`를 연결했고, first page loading/clear, filter 선택, cursor pagination append, loading/error/toast 상태를 관리하도록 구현했다. `ChatRoomMappers.toUiItems(context)`가 Android `Context`를 요구하므로 `ChatMainViewModel(repository, context)` 생성자를 사용하고 `AppDI`에는 `viewModel { ChatMainViewModel(get(), androidContext()) }`로 등록했다. GREEN 전환 중 최초 구현은 toast를 `postValue`로 emit해 즉시 관찰 테스트 3건이 실패했으며, 같은 main-thread 흐름에서 `_toastLiveData.value`를 사용하도록 최소 수정한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"``BUILD SUCCESSFUL in 19s`로 통과했다.
- 2026-06-10: Phase 4 검증 완료. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"``BUILD SUCCESSFUL in 10s`, `./gradlew :app:mergeDebugResources``BUILD SUCCESSFUL in 1s`, `./gradlew :app:compileDebugKotlin``BUILD SUCCESSFUL in 1s`, `./gradlew :app:ktlintCheck``BUILD SUCCESSFUL in 5s`로 통과했다. ktlint 실행 중 `.editorconfig``disabled_rules` deprecation warning과 Gradle deprecation warning은 기존 경고로 남아 있다.
- 2026-06-10: Phase 4 리뷰 게이트에서 filter 변경 중 늦게 도착한 first page/next page 응답이 현재 filter 상태를 덮거나 append할 수 있는 race 조건이 발견되어 보강했다. `filter 변경 전 첫 페이지 응답이 늦게 도착하면 현재 filter 목록을 덮어쓰지 않는다`, `filter 변경 전 다음 페이지 응답이 늦게 도착하면 현재 filter 목록에 append하지 않는다` 테스트를 추가했고, 기존 구현에서 각각 `AssertionError`로 RED 실패를 확인했다. 이후 `ChatMainViewModel``requestGeneration` guard를 추가하고 first page 시작 시 `_isAppending=false`로 reset해 stale first/append 응답을 무시하도록 수정했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"``BUILD SUCCESSFUL in 18s`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"``BUILD SUCCESSFUL in 8s`, `./gradlew :app:compileDebugKotlin``BUILD SUCCESSFUL in 1s`, `./gradlew :app:ktlintCheck``BUILD SUCCESSFUL in 3s`, `./gradlew :app:mergeDebugResources``BUILD SUCCESSFUL in 1s`로 재통과했다.
- 2026-06-10: `ChatMainViewModel`에서 `Context`를 필드로 보관해 `This field leaks a context object` 경고가 발생할 수 있는 구조를 점검했다. 시간 포맷은 리소스가 필요한 표시 계층 책임으로 두는 편이 적절하다고 판단해 `ChatRoomListUiItem``lastMessageTimeText` 대신 원본 `lastMessageAt`을 보관하도록 바꾸고, `ChatRoomMappers.toUiItem()/toUiItems()``ChatMainViewModel`에서 `Context` 의존을 제거했다. `AppDI` 등록도 `viewModel { ChatMainViewModel(get()) }`로 되돌렸으며, Phase 5 Adapter 구현 시 bind 직전에 `formatChatRoomLastMessageTime(context, item.lastMessageAt)`을 호출하도록 계획 문서의 계약을 갱신했다. 테스트 계약 변경 후 최초 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomMapperTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"`는 기존 production이 아직 `context` 파라미터와 `lastMessageTimeText`를 요구해 `No value passed for parameter 'context'`, `Unresolved reference 'lastMessageAt'`로 RED 실패했다. 구현 수정 후 같은 명령은 `BUILD SUCCESSFUL in 19s`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*"``BUILD SUCCESSFUL in 9s`, `./gradlew :app:compileDebugKotlin``BUILD SUCCESSFUL in 1s`, `./gradlew :app:ktlintCheck``BUILD SUCCESSFUL in 4s`로 통과했다. ktlint의 `.editorconfig disabled_rules` deprecation warning과 Gradle deprecation warning은 기존 경고로 남아 있다.