diff --git a/docs/20260610_DM_채팅화면/plan-task.md b/docs/20260610_DM_채팅화면/plan-task.md index 2e5a22ce..6797d94f 100644 --- a/docs/20260610_DM_채팅화면/plan-task.md +++ b/docs/20260610_DM_채팅화면/plan-task.md @@ -443,6 +443,60 @@ - 화면 이탈 또는 앱 background 전환 시 disconnect API가 호출된다. - SSE 연결 실패가 앱 crash로 이어지지 않는다. +### Phase 8: 크리에이터 채널 DM 진입 crash 수정 + +- [x] **Task 8.1: creatorId 기반 진입 thread crash 재현 테스트 추가** + - Files: + - Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt` + - 작업: + - `DmChatRoomActivity.newIntentByCreatorId()`로 들어오는 흐름에 대응해 `enter(roomId = 0L, creatorId > 0L)` 테스트를 보강한다. + - `CreateOrGetRoom` 이후 `OpenRoom` 결과가 background scheduler에서 전달되어도 `LiveData` 상태 갱신이 main thread에서 처리되어야 함을 고정한다. + - 기존 roomId 기반 진입 테스트는 유지하고, creatorId 기반 진입만의 Rx chain thread 전환 문제를 분리해 검증한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1` + - Expected: 수정 전에는 background thread `MutableLiveData.setValue()` 예외 또는 main thread 보장 assertion으로 RED를 확인하고, 수정 후 PASS한다. + - 검증 기록: + - 2026-06-17: `DmChatRoomViewModelTest`에 `creatorId 진입은 openRoom 결과 처리 전에 main thread로 다시 전환한다` 테스트를 추가했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1` 실행 결과 해당 테스트가 `DmChatRoomViewModelTest.kt:126` assertion failure로 RED가 되었고, 현재 `createRoomAndOpen()`에는 `flatMap` 이후 `OpenRoom` 결과 처리 전 main thread 재전환이 없음을 확인했다. + +- [x] **Task 8.2: createRoomAndOpen main thread 전환 보장** + - Files: + - Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt` + - 작업: + - `createRoomAndOpen()`의 `CreateOrGetRoom` 후 `OpenRoom` 연속 호출 흐름에서 최종 결과 처리 전에 main thread 전환이 보장되는지 점검한다. + - `handleOpenRoomResult()`, `handleError()`, `_roomOpenedEventLiveData` 갱신, `emitContent()` 호출이 main thread에서 실행되도록 최소 변경을 적용한다. + - `openRoom(roomId)` 단독 진입, pagination, send/retry, SSE callback scheduling, reconnect/disconnect 정리 정책은 변경하지 않는다. + - `postValue()`로 증상을 숨기기보다 기존 ViewModel의 `setValue` 기반 동기 상태 갱신 의미를 유지할 수 있는 scheduler 위치를 우선 검토한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1` + - Expected: creatorId 기반 진입에서 `Cannot invoke setValue on a background thread` 예외 없이 Content 상태와 room opened event가 발행된다. + - 검증 기록: + - 2026-06-17: `DmChatRoomViewModel.createRoomAndOpen()`의 `flatMap` 뒤에 `observeOn(AndroidSchedulers.mainThread())`를 추가해 `OpenRoom` 결과 처리 전 main thread 전환을 보장했다. 이후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1`을 실행해 Task 8.1에서 RED였던 테스트를 포함한 `DmChatRoomViewModelTest` 전체 PASS를 확인했다. + +- [x] **Task 8.3: DM 채팅 회귀 테스트와 빌드 확인** + - Files: + - Check: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/*Test.kt` + - Check: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt` + - 작업: + - DM 채팅 ViewModel 변경이 mapper, repository, SSE parser/client, pagination 상태 테스트를 깨지 않는지 확인한다. + - 가능하면 debug 빌드와 ktlint를 순차 실행해 Gradle cache 경합을 피한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*" --max-workers=1` + - Run: `./gradlew :app:compileDebugKotlin --max-workers=1` + - Run: `./gradlew :app:ktlintCheck --max-workers=1` + - Expected: 모두 PASS. 기존 `.editorconfig` `disabled_rules` deprecation warning은 실패로 보지 않는다. + - 검증 기록: + - 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*" --max-workers=1`, `./gradlew :app:compileDebugKotlin --max-workers=1`, `./gradlew :app:ktlintCheck --max-workers=1`를 순차 실행해 모두 PASS를 확인했다. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` deprecation warning이 출력됐지만 실패는 없었다. + +- [x] **Task 8.4: 수동 확인 항목 갱신** + - Files: + - Check: `DmChatRoomActivity` + - Check: `CreatorChannelActivity` + - 확인 항목: + - 크리에이터 채널에서 `DM 보내기`를 터치하면 `DmChatRoomActivity`로 이동한다. + - `creatorId` 기반 진입 후 방 생성/조회와 OpenRoom 결과 반영 중 앱이 crash 되지 않는다. + - header 상대 정보와 초기 메시지 목록이 표시된다. + - 채팅 탭의 기존 `roomId` 기반 DM 진입은 기존처럼 동작한다. + ## 5. 검증 기록 - 2026-06-10: `docs/20260610_DM_채팅화면/prd.md`를 확인해 DM 채팅방 진입, UI 제거 대상, REST API, SSE 이벤트, pagination, 전송 실패/재시도, lifecycle disconnect 요구사항을 계획에 반영했다. - 2026-06-10: `docs/agent-guides/work-plan-docs.md`, `docs/agent-guides/build-test-style.md`, `docs/agent-guides/code-style.md`를 확인해 신규 계획 문서 위치, phase/task 체크박스 형식, 테스트 명령 작성 방식을 확인했다. diff --git a/docs/20260610_DM_채팅화면/prd.md b/docs/20260610_DM_채팅화면/prd.md index ed6fffe5..5fee434e 100644 --- a/docs/20260610_DM_채팅화면/prd.md +++ b/docs/20260610_DM_채팅화면/prd.md @@ -11,6 +11,7 @@ - DM 채팅은 AI 채팅과 다르게 크리에이터와 사용자 간 메시지 송수신, SSE 실시간 이벤트 연결/해제, 커서 기반 과거 메시지 조회가 핵심이다. - 화면 이탈 또는 앱 백그라운드 전환 시 실시간 연결 해제 API를 항상 호출해야 하므로 생명주기 요구사항을 명확히 문서화해야 한다. - REST pagination과 SSE 실시간 수신 결과가 겹칠 수 있으므로 메시지 병합/중복 제거 기준이 필요하다. +- 크리에이터 채널에서 `DM 보내기`를 눌러 `creatorId` 기반으로 `DmChatRoomActivity`에 진입하면 `DmChatRoomViewModel.emitContent()`가 background thread에서 `MutableLiveData.setValue()`를 호출해 앱이 crash 된다. --- @@ -22,6 +23,7 @@ - 텍스트 메시지 전송 후 서버 응답 메시지를 화면에 반영한다. - 채팅방 화면 진입/이탈, 앱 foreground/background 전환에 따른 SSE 연결/해제 정책을 정의한다. - SSE 이벤트 이름/응답 payload, 재연결 가이드, UI thread 비차단, 최신 메시지 동기화 같은 실시간 연결 운영 기준을 정의한다. +- 크리에이터 채널 `DM 보내기` 진입에서 방 생성/열기 완료 후 모든 `LiveData` 상태 갱신이 main thread에서 수행되어 background thread `setValue()` 예외가 발생하지 않도록 한다. --- @@ -133,6 +135,20 @@ data class UserCreatorChatRoomOpenResponse( ) ``` +### Creator Channel DM Entry Crash Fix +크리에이터 채널의 `DM 보내기` 버튼에서 DM 채팅방으로 이동할 때 앱이 종료되지 않도록 한다. + +#### Requirements +- `DmChatRoomActivity.newIntentByCreatorId(context, creatorId)`로 진입한 경우 `CreateOrGetRoom` 성공 후 `OpenRoom` 결과를 안전하게 처리한다. +- `DmChatRoomViewModel.emitContent()`에서 `MutableLiveData.setValue()`를 호출하는 시점은 main thread여야 한다. +- RxJava chain에서 `flatMap` 이후 upstream/downstream scheduler가 달라져도 `handleOpenRoomResult()`, `handleError()`, `_roomOpenedEventLiveData` 갱신은 main thread에서 실행되어야 한다. +- 기존 `roomId` 기반 채팅 탭 DM 진입 동작은 변경하지 않는다. +- 수정은 DM 채팅 ViewModel의 thread 전환 문제에 한정하고, 크리에이터 채널 layout이나 다른 UI 동작은 이번 범위에서 변경하지 않는다. + +#### Edge Cases +- `CreateOrGetRoom`은 성공했지만 `OpenRoom`이 실패해도 앱 crash 없이 기존 오류 처리 정책을 따른다. +- 빠르게 화면을 이탈하거나 background 전환이 발생해도 예약된 realtime callback 정리 정책을 훼손하지 않는다. + ### SSE Realtime Events 채팅방이 열려 있는 동안 서버 이벤트를 연결해 새 메시지를 실시간으로 반영한다. @@ -302,6 +318,7 @@ data class UserCreatorChatMessageItemDto( - 앱 foreground/background 감지는 Activity lifecycle과 앱 전체 `ProcessLifecycleOwner` 중 어떤 기준을 사용할지 구현 계획에서 확정한다. - 구현 전 `docs/20260610_DM_채팅화면/plan-task.md`를 작성하고, 그 문서에 따라 최소 구현한다. - 테스트는 DTO mapper, 메시지 정렬/중복 제거, pagination state, disconnect lifecycle을 우선 검증한다. +- 크리에이터 채널 `DM 보내기` crash 수정은 기존 DM 채팅 문서의 후속 범위로 누적하며, 구현 전 `plan-task.md`에 대응 task와 검증 기록을 추가한다. --- @@ -331,6 +348,7 @@ data class UserCreatorChatMessageItemDto( - SSE 재연결 후 필요 시 GetMessages API로 누락 메시지를 동기화한다. - disconnect 처리는 UI thread를 블로킹하지 않는다. - 제거 대상 UI(`character_type_badge`, `ll_can_badge`, `iv_more`, `notice_container`)가 DM 화면에 나타나지 않는다. +- 크리에이터 채널 `DM 보내기`로 `creatorId` 기반 진입 시 `Cannot invoke setValue on a background thread` 예외 없이 DM 채팅방 Content 상태가 표시된다. --- @@ -365,3 +383,4 @@ data class UserCreatorChatMessageItemDto( - 2026-06-10: 사용자 제공 백엔드 계약을 반영해 SSE 이벤트 이름과 payload를 확정했다. `connected` 이벤트는 `"connected"` 문자열 handshake로, `message` 이벤트는 `UserCreatorChatMessageItemDto` JSON으로 문서화했다. 서버 `reconnectTime=3000`ms, `Last-Event-ID` 기반 replay 미지원, 재연결 후 GetMessages API를 통한 누락 메시지 보정 요구사항도 반영했다. - 2026-06-10: 백그라운드 조사 결과와 대조해 `ConnectEvents` 응답 표기를 `ApiResponse`에서 `text/event-stream`으로 보정하고, API Summary와 기술 제약도 SSE stream 수신/파싱 기준으로 갱신했다. - 2026-06-10: 후속 Repository 구현 시 `Authorization` 헤더 오입력 방지를 위해 단일 bearer helper로 헤더 문자열을 생성하도록 기술 제약에 기록했다. +- 2026-06-17: 사용자 제보 스택트레이스(`DmChatRoomViewModel.emitContent()`의 `MutableLiveData.setValue()` background thread 예외)와 `DmChatRoomViewModel.kt`의 `createRoomAndOpen()` Rx chain을 확인했다. 크리에이터 채널 `DM 보내기`의 `creatorId` 기반 진입에서 `CreateOrGetRoom` 후 `OpenRoom` 결과 처리 thread를 main thread로 보장해야 하는 요구사항을 기존 DM 채팅화면 PRD에 후속 범위로 누적했다.