From 896935e19a86374e3ae9aeee41e426b6449208ec Mon Sep 17 00:00:00 2001 From: klaus Date: Wed, 10 Jun 2026 12:00:34 +0900 Subject: [PATCH] =?UTF-8?q?docs(chat):=20=EC=B1=84=ED=8C=85=EB=B0=A9=20Vie?= =?UTF-8?q?wModel=20=EA=B2=80=EC=A6=9D=EC=9D=84=20=EA=B8=B0=EB=A1=9D?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/20260609_채팅_탭_페이지/plan-task.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/20260609_채팅_탭_페이지/plan-task.md b/docs/20260609_채팅_탭_페이지/plan-task.md index b4c3521d..24d8f0d9 100644 --- a/docs/20260609_채팅_탭_페이지/plan-task.md +++ b/docs/20260609_채팅_탭_페이지/plan-task.md @@ -44,7 +44,7 @@ - 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으로 변환한다. + - DTO를 UI item으로 변환하되 시간 표시는 수행하지 않고 원본 `lastMessageAt`을 유지한다. - 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` @@ -212,7 +212,7 @@ - 테스트 케이스: - `chatType="AI"`는 Direct badge 미표시 item으로 매핑한다. - `chatType="DM"`은 Direct badge 표시 item으로 매핑한다. - - `lastMessageAt`은 formatter 결과 문자열로 매핑한다. + - `lastMessageAt`은 원본 ISO-8601 문자열 그대로 매핑한다. - `roomId`, `targetName`, `targetImageUrl`, `lastMessage`는 그대로 전달한다. - 알 수 없는 `chatType`은 표시 대상에서 제외한다. - 검증 명령: `./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` - 구현: - `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` - `Loading` - `Content(val items: List, val isAppending: Boolean = false)` - `Empty` - `Error(val message: String?)` - - `fun ChatRoomListItemResponse.toUiItem(context: Context): ChatRoomListUiItem?` - - `fun List.toUiItems(context: Context): List` + - `fun ChatRoomListItemResponse.toUiItem(): ChatRoomListUiItem?` + - `fun List.toUiItems(): List` + - 화면 표시용 시간 문구는 `Activity`/`Fragment`/`Adapter` 등 표시 계층에서 `formatChatRoomLastMessageTime(context, item.lastMessageAt)`으로 변환한다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatRoomMapperTest"` - 기대 결과: PASS. @@ -238,7 +239,7 @@ ### 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` - 테스트 케이스: - 초기 `loadFirstPage()`는 `filter=ALL`, `cursor=null`로 API를 호출한다. @@ -253,7 +254,7 @@ - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"` - 기대 결과: `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` - 구현: - `currentFilter: ChatRoomFilter = ChatRoomFilter.ALL` @@ -309,6 +310,7 @@ - `submitItems(items: List)` - `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. @@ -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: 리뷰 게이트에서 `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-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은 기존 경고로 남아 있다.