47 KiB
DM 채팅화면 구현 계획/TASK
1. 목표
docs/20260610_DM_채팅화면/prd.md를 기준으로 신규 DM 채팅방 상세 화면을 v2 패키지 하위에 구현한다. 기존 AI ChatRoomActivity는 직접 수정하지 않고, DM 전용 Activity, ViewModel, Repository, DTO, SSE 클라이언트, 메시지 UI 모델을 최소 범위로 추가한다.
2. 구현 결정 사항
- 신규 화면은
kr.co.vividnext.sodalive.v2.main.chat.dm하위에 둔다. - DM 채팅방 Activity 이름은
DmChatRoomActivity로 한다. - intent extra는
EXTRA_ROOM_ID,EXTRA_CREATOR_ID를 사용한다.roomId > 0:OpenRoom부터 시작한다.roomId <= 0 && creatorId > 0:CreateOrGetRoom호출 후 반환된roomId로OpenRoom을 호출한다.- 둘 다 유효하지 않으면 Activity를 종료한다.
- REST API는 기존 v2 채팅 탭과 동일하게 Retrofit + RxJava3 +
ApiResponse<T>패턴을 사용한다. - SSE는 현재 저장소에 재사용 패턴이 없으므로 별도 라이브러리 추가 없이 기존
OkHttpClient의newCall()과 streamingResponseBody를 사용하는DmChatEventClient를 추가한다. - SSE 연결/해제는 Activity foreground 범위에서 처리한다.
onStart:OpenRoom완료 후 연결 가능 상태면 SSE 연결을 시작한다.onStop: SSE call을 cancel하고DisconnectRealtimeAPI를 비동기로 호출한다.onDestroy: listener 참조와 disposable을 정리한다.
Last-Event-IDreplay는 기대하지 않고, SSE 재연결 후GetMessages로 최신 누락 가능 메시지를 동기화한다.- 네트워크 오류로 SSE가 실패하면 화면이 foreground에 있고 채팅방이 활성 상태인 경우 서버
reconnectTime=3000ms 기준으로 재연결을 시도한다.- 재연결 성공 후
Last-Event-IDreplay는 기대하지 않고GetMessages로 누락 가능 메시지를 보정한다. - 화면 이탈 또는 background 전환 시 예약된 재연결 시도는 취소한다.
- 재연결 성공 후
VOICE메시지는 DTO에 보존하되 이번 UI 목록에는 표시하지 않는다.- 전송은 낙관적 UI를 적용한다.
- 전송 직후 local pending 메시지를 추가한다.
- 성공 시 서버 응답 메시지로 교체한다.
- 실패 시 실패 상태와 재시도 버튼을 표시한다.
- Phase 3 ViewModel 전송 정책은 단일
isSendingguard로 한 번에 하나의 전송만 허용한다.- 이번 범위에서는 “전송 중 중복 요청 방지” 요구사항을 우선 충족한다.
- 서로 다른 메시지의 연속 병렬 전송 허용 여부는 Phase 5 Activity/input UX 연결 시 필요하면 재검토한다.
- Phase 3 ViewModel의 pagination/reconnect 동기화 실패는 화면 종료나 Error 화면 전환 없이 기존 메시지 상태를 유지하고 내부 loading 상태만 복구한다.
- 사용자 노출 toast/retry UI는 Phase 5 Activity 연결 시 필요하면 별도 처리한다.
3. 파일 구조
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt- DM 채팅방 화면, intent 진입, RecyclerView/input/header/lifecycle/SSE 연결 제어를 담당한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt- 방 생성/열기, pagination, 메시지 전송, SSE 이벤트 반영, disconnect 상태를 관리한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatApi.ktuser-creator-chatREST endpoint를 정의한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatModels.kt- REST DTO와 서버 메시지 DTO를 정의한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatRepository.kt- API 호출 래핑, token 전달, SSE 클라이언트 위임을 담당한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatEventClient.kt- OkHttp 기반 SSE 연결,
connected/message이벤트 파싱, cancel 처리를 담당한다.
- OkHttp 기반 SSE 연결,
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/model/DmChatUiModels.kt- UI 메시지 모델, 전송 상태, 화면 상태를 정의한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/model/DmChatMappers.kt- 서버 DTO를 UI 모델로 변환하고 정렬/중복 제거 helper를 제공한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/ui/DmChatMessageAdapter.kt- 내 메시지/상대 메시지 ViewHolder, 실패 재시도 callback을 담당한다.
- Create:
app/src/main/res/layout/activity_dm_chat_room.xml- AI 채팅방 layout을 기준으로 DM 전용 header, message list, input 영역을 구성한다.
- Create:
app/src/main/res/layout/item_dm_chat_my_message.xml- 내 텍스트 메시지, 전송 중/실패 상태, 재시도 버튼을 표시한다.
- Create:
app/src/main/res/layout/item_dm_chat_opponent_message.xml- 상대 텍스트 메시지와 프로필/닉네임을 표시한다.
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt- DM item 클릭 시
DmChatRoomActivity.newIntentByRoomId()로 이동한다.
- DM item 클릭 시
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.ktDmChatApi,DmChatRepository,DmChatEventClient,DmChatRoomViewModelDI를 추가한다.
- Modify:
app/src/main/AndroidManifest.xmlDmChatRoomActivity를 등록한다.
- Modify:
docs/agent-guides/build-test-style.md- 신규 DM 채팅 테스트 단일 실행 예시를 추가한다.
- Test Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatMapperTest.kt - Test Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatPaginationStateTest.kt - Test Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatEventParserTest.kt - Test Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt
4. 성공 기준
roomId기반 진입은 create API 없이OpenRoom을 호출한다.creatorId기반 진입은CreateOrGetRoom성공 후 반환된roomId로OpenRoom을 호출한다.- OpenRoom 메시지는 오래된 순서에서 최신 순서로 표시된다.
- 메시지 병합은
messageId기준으로 중복을 제거한다. - 상단 스크롤 시
hasMore=true,nextCursor != null,isLoading=false조건에서만 과거 메시지를 조회한다. - 텍스트 전송은 blank 입력을 무시하고, 전송 중 중복 요청을 막는다.
- 전송 실패 메시지는 재시도 버튼을 표시하고, 재시도 성공 시 정상 메시지로 교체된다.
- 화면 stop/destroy 흐름에서 SSE cancel과 disconnect API 호출이 화면 종료를 막지 않는다.
- DM 화면에는
character_type_badge,ll_can_badge,iv_more,notice_container가 없다. ChatRoomActivity기존 동작은 변경하지 않는다.
Phase 1: API/모델/매퍼 기반 추가
-
Task 1.1: REST DTO와 API 정의
- Files:
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatApi.kt - Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatModels.kt
- Create:
- 작업:
CreateDmChatRoomRequest,CreateDmChatRoomResponse를 추가한다.DmChatRoomOpenResponse,DmChatMessagesPageResponse,DmChatMessageResponse,SendDmTextMessageRequest,SendDmChatMessageResponse를 추가한다.- Retrofit endpoint를 아래 계약으로 추가한다.
POST /api/v2/user-creator-chat/rooms/createGET /api/v2/user-creator-chat/rooms/{roomId}/openGET /api/v2/user-creator-chat/rooms/{roomId}/messagesPOST /api/v2/user-creator-chat/rooms/{roomId}/messages/textPOST /api/v2/user-creator-chat/rooms/{roomId}/events/disconnect
- 검증:
- DTO 필드명은 PRD의 서버 필드명을
@SerializedName으로 그대로 보존한다. - REST 반환 타입은
Single<ApiResponse<...>>를 사용한다.
- DTO 필드명은 PRD의 서버 필드명을
- Files:
-
Task 1.2: UI 모델과 메시지 병합 helper 추가
- Files:
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/model/DmChatUiModels.kt - Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/model/DmChatMappers.kt - Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatMapperTest.kt
- Create:
- 작업:
DmChatMessageUiItem에messageId,localId,mine,textMessage,senderNickname,senderProfileImageUrl,createdAt,status를 둔다.DmChatMessageStatus는SENDING,SENT,FAILED로 정의한다.messageType은 서버 계약상TEXT/VOICE대문자이나, UI 매핑에서는 오입력 방지를 위해 대소문자를 무시해TEXT를 판정한다.messageType이TEXT가 아니거나textMessage == null이면 UI item으로 매핑하지 않는다.sortByCreatedAtAndMessageId()와mergeByMessageId()helper를 추가한다.- 정렬은
createdAt오름차순 후 같은 시각에서는messageId오름차순을 따른다. - 중복
messageId병합은 선도착한 기존 item을 유지하고 후도착 중복 item을 버린다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatMapperTest" - Expected:
TEXT대소문자 무시 매핑,TEXT외 타입과textMessage == null제외,createdAt/messageId오름차순 정렬, 중복messageId선도착 우선 테스트가 PASS.
- Run:
- Files:
Phase 2: Repository와 SSE 클라이언트 추가
-
Task 2.1: Repository 추가
- Files:
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatRepository.kt
- Create:
- 작업:
createOrGetRoom(token, creatorId),openRoom(token, roomId, limit),getMessages(token, roomId, cursor, limit),sendTextMessage(token, roomId, textMessage),disconnectRealtime(token, roomId)를 추가한다.limit기본값은20으로 둔다.Authorization헤더 문자열은 Repository 내부의 단일 helper로 만든다. 예:private fun bearer(token: String) = "Bearer $token".
- 검증:
- Repository는 API 호출을 얇게 위임하고 별도 비즈니스 로직을 넣지 않는다.
- 모든 REST API 호출은 동일한 bearer helper를 통해 생성된 auth header를 사용한다.
- Files:
-
Task 2.2: OkHttp 기반 SSE 클라이언트 추가
- Files:
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatEventClient.kt - Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatEventParserTest.kt
- Create:
- 작업:
connect(token, roomId, listener)는GET /api/v2/user-creator-chat/rooms/{roomId}/events요청을 만든다.- header는 REST와 동일하게
Authorization: Bearer ...를 전달한다. connected이벤트는 메시지 목록에 전달하지 않고 연결 상태 callback만 호출한다.message이벤트 data는 Gson으로DmChatMessageResponse로 파싱한다.cancel()은 진행 중인Call을 cancel하고 listener 참조를 해제한다.- stream read와 cancel은 UI thread를 블로킹하지 않도록 OkHttp callback thread에서 처리한다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatEventParserTest" - Expected:
connected이벤트, 단일message이벤트, 여러 줄 SSE frame 파싱, 잘못된 JSON 무시 또는 failure callback 테스트가 PASS.
- Run:
- Files:
Phase 3: ViewModel 상태와 단위 테스트 추가
-
Task 3.1: ViewModel 초기 진입 흐름 구현
- Files:
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt - Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt
- Create:
- 작업:
enter(roomId, creatorId)를 추가한다.roomId > 0이면openRoom을 호출한다.roomId <= 0 && creatorId > 0이면createOrGetRoom후openRoom을 호출한다.roomId <= 0 && creatorId <= 0이면 종료 이벤트를 발행한다.- OpenRoom 성공 시 header 정보와 메시지 목록,
hasMore,nextCursor를 상태에 반영한다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" - Expected: roomId 진입, creatorId 진입, invalid 진입, OpenRoom 정렬 반영 테스트가 PASS.
- Run:
- Files:
-
Task 3.2: pagination 상태 구현
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt - Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatPaginationStateTest.kt
- Modify:
- 작업:
loadOlderMessages()를 추가한다.hasMore=false,isLoadingOlder=true,nextCursor=null이면 요청하지 않는다.- 성공 응답 메시지는 기존 목록 상단에 prepend하고
messageId기준 중복을 제거한다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatPaginationStateTest" - Expected: 요청 조건, cursor 전달, prepend, 스크롤 보정용 추가 개수 반환 테스트가 PASS.
- Run:
- Files:
-
Task 3.3: 전송/재시도/SSE 반영 상태 구현
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt
- Modify:
- 작업:
sendText(text)는 trim 후 blank면 종료한다.- 전송 중 같은 local message에 대한 중복 요청을 막는다.
- 전송 직후
SENDINGlocal item을 추가한다. - 성공 시 local item을 서버 메시지로 교체한다.
- 실패 시 local item status를
FAILED로 변경한다. retry(localId)는 실패 item의 text를 다시 전송한다.onRealtimeMessage(message)는messageId중복을 제거하고 최신 메시지로 append한다.- 재연결 후 동기화는
syncLatestMessagesAfterReconnect()로 분리해GetMessages호출과 병합을 수행한다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" - Expected: blank 무시, pending 추가, 성공 교체, 실패 상태, retry 성공, SSE 중복 제거 테스트가 PASS.
- Run:
- Files:
Phase 4: UI 레이아웃과 Adapter 구현
-
Task 4.1: DM 채팅방 layout 생성
- Files:
- Create:
app/src/main/res/layout/activity_dm_chat_room.xml
- Create:
- 작업:
activity_chat_room.xml구조를 참고해 배경 이미지, dim, header,rv_messages,input_container를 구성한다.- header에는
iv_back,iv_profile,tv_name만 둔다. rv_messagestop constraint는header_container하단으로 연결한다.character_type_badge,ll_can_badge,iv_more,notice_container는 추가하지 않는다.
- 검증:
- XML에서 제거 대상 id가 검색되지 않아야 한다.
- Run:
rg "character_type_badge|ll_can_badge|iv_more|notice_container" app/src/main/res/layout/activity_dm_chat_room.xml - Expected: 결과 없음.
- Files:
-
Task 4.2: 메시지 item layout과 Adapter 구현
- Files:
- Create:
app/src/main/res/layout/item_dm_chat_my_message.xml - Create:
app/src/main/res/layout/item_dm_chat_opponent_message.xml - Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/ui/DmChatMessageAdapter.kt
- Create:
- 작업:
- 내 메시지는 오른쪽 정렬, 상대 메시지는 왼쪽 정렬로 구성한다.
- 내 실패 메시지에는 재시도 버튼 또는 클릭 가능한 실패 상태 view를 둔다.
DiffUtil과 stable id는messageId우선, local pending 메시지는localId기준으로 처리한다.- 상대 프로필 이미지는 기존 placeholder 정책에 맞춰
ic_placeholder_profile을 사용한다.
- 검증:
- 긴 텍스트가 item 너비 안에서 줄바꿈되는지 XML
maxWidth또는 constraint를 확인한다.
- 긴 텍스트가 item 너비 안에서 줄바꿈되는지 XML
- Files:
Phase 5: Activity 구현과 화면 연결
-
Task 5.1: DmChatRoomActivity 생성
- Files:
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt
- Create:
- 작업:
newIntentByRoomId(context, roomId)와newIntentByCreatorId(context, creatorId)를 제공한다.- ViewBinding으로
activity_dm_chat_room.xml을 연결한다. - header, RecyclerView, input, IME send, send button enable/disable을 설정한다.
- 상단 도달 시
viewModel.loadOlderMessages()를 호출한다. - prepend 후 기존 스크롤 위치를 유지한다.
- 사용자가 하단 근처에 있을 때만 새 메시지 수신 후 하단으로 스크롤한다.
- 검증:
ChatRoomActivity의 쿼터/광고/더보기/notice 관련 로직을 가져오지 않는다.
- Files:
-
Task 5.2: SSE lifecycle과 disconnect 연결
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt - Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt
- Modify:
- 작업:
onStart에서 OpenRoom 완료 상태이면viewModel.connectRealtime()를 호출한다.onStop에서viewModel.disconnectRealtime()를 호출한다.- disconnect 요청 중복을 막는
isDisconnecting상태를 둔다. - disconnect 실패는 화면 종료를 막지 않고 toast를 과하게 노출하지 않는다.
- 검증:
- ViewModel 테스트에서 disconnect 중복 방지와 cancel 호출 여부를 검증한다.
- Files:
-
Task 5.3: 채팅 탭 DM item 클릭 연결
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt
- Modify:
- 작업:
ChatRoomType.AI는 기존ChatRoomActivity로 이동한다.ChatRoomType.DM은DmChatRoomActivity.newIntentByRoomId(requireContext(), item.roomId)로 이동한다.
- 검증:
- AI item 클릭 동작은 기존과 동일하게 유지한다.
- Files:
-
Task 5.4: Phase 5 리뷰 관찰 항목 정리
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt - Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivitySourceTest.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt
- Modify:
- 작업:
bindContent의 모든 Content emit마다connectRealtime()를 호출하는 흐름을 점검한다.- 기능 변경이 과도하지 않으면 OpenRoom 완료 후 연결 가능 상태 진입 시점에만 realtime connect를 트리거하는 별도 신호로 분리한다.
- SSE 실패 후 자동 재연결은 PRD 범위에 포함되므로,
onFailure이후 foreground/활성 채팅방 상태일 때 서버reconnectTime=3000ms 기준으로 재연결을 예약한다. - 재연결 성공 후
Last-Event-ID기반 replay는 기대하지 않고GetMessages로 누락 가능 메시지를 보정한다. - 화면 이탈 또는 background 전환 시 예약된 재연결을 취소해 종료 후 재연결이 발생하지 않도록 한다.
disconnectRealtime()진행 중 빠른onStart재진입 시 crash 위험이 없음을 guard 조합과 테스트로 확인한다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" - Expected: realtime connect 호출 의도가 테스트 또는 source test로 확인되고, SSE 실패 후 3초 지연 재연결, 재연결 후 최신 메시지 동기화, 화면 이탈 시 예약 재연결 취소, disconnect 중 재진입이 crash로 이어지지 않는 정책이 확인된다.
- Run:
- Files:
-
Task 5.5: 자동 재연결 실행 스레드 race 제거
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt
- Modify:
- 작업:
reconnectScheduler.scheduleDirect { connectRealtime(token) }처럼 io scheduler에서 직접connectRealtime()를 실행하는 흐름을 제거한다.- 지연 예약은 기존 scheduler를 사용하되, 실제
connectRealtime(token)호출과 realtime mutable flag 변경은 main thread에서 수행되도록scheduleRealtimeCallback { connectRealtime(token) }또는 main scheduler 관찰로 옮긴다. isRealtimeConnected,shouldReconnectRealtime,reconnectDisposable,currentRealtimeToken변경 스레드가 main thread 기준으로 일관되는지 확인한다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" - Expected: SSE failure 후 예약된 재연결이 main thread에서
connectRealtime()를 실행하고, background/io thread에서 realtime mutable flag를 직접 변경하지 않는다.
- Run:
- Files:
-
Task 5.6: disconnect와 예약 재연결 경합 방지
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt
- Modify:
- 작업:
- 예약된 재연결 람다가 실행을 시작한 직후
disconnectRealtime()가 호출되는 경우를 점검한다. connectRealtime()진입부에서shouldReconnectRealtime또는 foreground/활성 채팅방 상태를 재확인해 disconnect 이후 재연결이 살아남지 않도록 한다.disconnectRealtime()의 예약 취소와 local realtime 정리 순서가 기존 중복 disconnect API guard와 충돌하지 않는지 확인한다.
- 예약된 재연결 람다가 실행을 시작한 직후
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" - Expected: 예약 재연결 실행 직전/직후 disconnect가 호출되어도 새 SSE 연결이 남지 않고, disconnect API 중복 방지 동작은 유지된다.
- Run:
- Files:
-
Task 5.7: SSE 재연결 backoff 또는 시도 제한 검토
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt - Modify:
docs/20260610_DM_채팅화면/plan-task.md
- Modify:
- 작업:
- foreground 한정 3초 무한 재시도가 PRD의 서버
reconnectTime=3000ms 기준과 충돌하지 않는지 검토한다. - 지속 실패 상황의 네트워크 부담을 줄이기 위해 지수 backoff 또는 최대 시도 횟수 제한 중 최소 변경안을 선택한다.
- backoff/시도 제한을 적용하는 경우, 재연결 성공 또는 수동 재진입 시 재시도 상태가 초기화되도록 한다.
- PRD 범위와 충돌하거나 정책 결정이 필요하면 구현하지 않고 결정 필요 사항을 문서에 남긴다.
- foreground 한정 3초 무한 재시도가 PRD의 서버
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" - Expected: 선택한 재연결 정책이 테스트로 고정되고, PRD의 3초 기본 간격 및 foreground 한정 조건을 깨지 않는다.
- Run:
- Files:
-
Task 5.8: roomOpenedEventLiveData 스티키 재전달 방지
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt - Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivitySourceTest.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt
- Modify:
- 작업:
roomOpenedEventLiveData가 일반MutableLiveData<Boolean>로 마지막true를 재구독자에게 재전달하는지 확인한다.- 단발성 이벤트에는 기존 프로젝트 패턴에 맞는 SingleLiveEvent, Event wrapper, consume flag 중 최소 변경 방식을 적용한다.
- 화면 회전 또는 observer 재등록 시
connectRealtimeIfStarted()가 이벤트 재전달만으로 다시 호출되지 않도록 한다. - 기존
connectRealtime()idempotent guard는 유지하되, 단발성 이벤트 자체의 의미를 명확히 한다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" - Expected: OpenRoom 완료 이벤트는 한 번만 소비되고, observer 재등록만으로 realtime connect 트리거가 반복되지 않는다.
- Run:
- Files:
Phase 6: DI, Manifest, 문서 갱신
-
Task 6.1: Koin DI 등록
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt
- Modify:
- 작업:
DmChatApiAPI builder 등록을 추가한다.DmChatEventClient는 기존OkHttpClient,Gson,BuildConfig.BASE_URL기반으로 생성되도록 등록한다.DmChatRepository,DmChatRoomViewModel등록을 추가한다.
- 검증:
- import 추가 외 기존 DI 등록 순서를 불필요하게 재정렬하지 않는다.
- Files:
-
Task 6.2: Manifest 등록
- Files:
- Modify:
app/src/main/AndroidManifest.xml
- Modify:
- 작업:
.v2.main.chat.dm.DmChatRoomActivity를 application 하위 activity 목록에 추가한다.- 키보드 UX는 기존 채팅방과 유사하게
android:windowSoftInputMode="stateAlwaysHidden|adjustResize"를 우선 적용한다.
- 검증:
- activity는 exported를 명시하지 않는 기존 내부 Activity 패턴을 따른다.
- Files:
-
Task 6.3: 테스트 실행 가이드 갱신
- Files:
- Modify:
docs/agent-guides/build-test-style.md
- Modify:
- 작업:
- DM 채팅 테스트 단일 실행 예시를 추가한다.
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*"
- DM 채팅 테스트 단일 실행 예시를 추가한다.
- 검증:
- 문서 변경은 신규 테스트 명령 예시 추가로만 제한한다.
- Files:
-
Task 6.4: SSE 전용 read timeout 제거
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt - Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatEventClient.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatEventClientTest.kt
- Modify:
- 작업:
- Phase 6 DI 등록 시
DmChatEventClient에 주입되는OkHttpClient인스턴스를 확인한다. - 공유
OkHttpClient의 일반readTimeout이 idle SSE stream을 조기 종료하지 않도록 SSE 전용 client를okHttpClient.newBuilder().readTimeout(0, TimeUnit.MILLISECONDS).build()로 생성한다. - REST API용 공유 client timeout 정책은 변경하지 않는다.
- Phase 6 DI 등록 시
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatEventClientTest" - Expected: SSE 전용 client의 read timeout이 0으로 설정되고, 기존 SSE parsing/cancel/failure 동작은 유지된다.
- Run:
- Files:
-
Task 6.5: realtime callback scheduling Disposable 누적 방지
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt
- Modify:
- 작업:
scheduleRealtimeCallback()이 SSE message마다 완료된Disposable을CompositeDisposable에 계속 누적하는 패턴을 제거한다.- 권장 우선순위는
Handler(Looper.getMainLooper()).post { }로 main thread에 전달하는 방식이다. - Rx scheduler를 유지해야 한다면 완료 후 자기 자신을
CompositeDisposable에서 제거하는 방식으로 누적을 방지한다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" - Expected: SSE callback은 main thread에서 처리되고, 메시지 수신 횟수만큼 완료된
Disposable이 누적되지 않는다.
- Run:
- Files:
Phase 7: 최종 검증과 기록
-
Task 7.1: 단위 테스트 실행
- Files:
- Check:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/*Test.kt
- Check:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*"
- Expected:
- 신규 DM 채팅 단위 테스트가 모두 PASS.
- Files:
-
Task 7.2: 앱 빌드 확인
- Files:
- Check: Gradle project
- Run:
./gradlew :app:assembleDebug
- Expected:
- Debug APK 빌드 PASS.
- Files:
-
Task 7.3: 린트/스타일 확인
- Files:
- Check: Kotlin/XML 변경 파일
- Run:
./gradlew :app:ktlintCheck
- Expected:
- ktlint PASS.
- Files:
-
Task 7.4: 수동 확인
- Files:
- Check:
DmChatRoomActivity
- Check:
- 확인 항목:
- DM item 클릭 시 DM 채팅방 화면으로 이동한다.
- header에 뒤로가기, 상대 프로필, 상대 닉네임만 표시된다.
- 메시지 목록은 header 바로 아래에서 시작한다.
- blank 입력은 전송되지 않는다.
- 텍스트 전송 실패 시 실패 상태와 재시도 버튼이 표시된다.
- 화면 이탈 또는 앱 background 전환 시 disconnect API가 호출된다.
- SSE 연결 실패가 앱 crash로 이어지지 않는다.
- Files:
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 체크박스 형식, 테스트 명령 작성 방식을 확인했다. -
2026-06-10:
ChatRoomActivity.kt,activity_chat_room.xml,ChatRoomApi.kt,ChatRoomModels.kt,ChatRoomRepository.kt,ChatMainFragment.kt,AppDI.kt,AndroidManifest.xml을 확인해 기존 채팅 화면 구조, v2 API/Repository 패턴, DI/Manifest 등록 위치, DM item 클릭 연결 지점을 확인했다. -
2026-06-10:
rg로EventSource,text/event-stream,ResponseBody,OkHttpClient사용 현황을 확인했으며 저장소에 기존 SSE 구현 패턴은 없고 OkHttp 의존성은 이미 존재함을 확인했다. 이에 따라 별도 라이브러리 추가 없이 OkHttp streaming 기반DmChatEventClient를 계획에 반영했다. -
2026-06-10: 이번 단계는 계획 문서 생성만 수행했으며 Android 구현, 빌드, 테스트는 실행하지 않았다.
-
2026-06-10: Phase 1 구현 전
DmChatMapperTest를 먼저 추가하고./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatMapperTest"를 실행해DmChatMessageResponse및dm.model심볼 부재로 실패하는 RED 상태를 확인했다. -
2026-06-10: Phase 1 범위로
DmChatApi.kt,DmChatModels.kt,DmChatUiModels.kt,DmChatMappers.kt,DmChatMapperTest.kt를 추가했다. DTO/API는 PRD 서버 필드를@SerializedName으로 보존하고Single<ApiResponse<...>>반환 타입을 사용하도록 정의했다. -
2026-06-10:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatMapperTest",./gradlew :app:compileDebugKotlin,./gradlew :app:ktlintCheck를 실행해 모두 PASS를 확인했다.ktlintCheck에서는 기존.editorconfig의disabled_rulesdeprecation warning이 출력됐지만 실패는 없었다. -
2026-06-10:
DmChatMappers.kt와DmChatMapperTest.kt를 보강해messageType대소문자 무시TEXT매핑,TEXT외 타입 및textMessage == null미매핑,createdAt동일 시messageId오름차순, 중복messageId선도착 우선 정책을 명시했다../gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatMapperTest"와./gradlew :app:ktlintCheckPASS를 확인했다. -
2026-06-10: Phase 2 구현 전
DmChatEventParserTest를 먼저 추가하고./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatEventParserTest"를 실행해DmChatEventParser심볼 부재로 실패하는 RED 상태를 확인했다. -
2026-06-10: Phase 2 범위로
DmChatRepository.kt,DmChatEventClient.kt,DmChatEventParserTest.kt,DmChatRepositoryTest.kt를 추가했다. Repository는bearer(token)helper를 통해 모든 REST API auth header를 생성하고 API 호출만 얇게 위임하도록 구현했다. SSE 클라이언트는 OkHttpCall.enqueue()와 streamingResponseBody를 사용하고,connected/messageevent frame parser를 분리했다. -
2026-06-10:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatEventParserTest",./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*",./gradlew :app:compileDebugKotlin,./gradlew :app:ktlintCheck를 실행해 모두 PASS를 확인했다.ktlintCheck에서는 기존.editorconfig의disabled_rulesdeprecation warning이 출력됐지만 실패는 없었다. -
2026-06-10: Phase 2 리뷰에서 SSE stream read 중
IOException이 failure callback으로 전달되지 않는 문제가 발견되어, 취소되지 않은 call의 read 실패만listener.onFailure(e)로 전달하도록DmChatEventClient를 수정했다. 이후./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*",./gradlew :app:compileDebugKotlin,./gradlew :app:ktlintCheckPASS를 재확인했다. -
2026-06-10: SSE 클라이언트 보강으로 비정상 HTTP 응답 failure callback 전달, trailing blank line 없는 마지막 frame dispatch,
data:뒤 단일 공백 제거 정책, listener@Volatile가시성 보완을 반영했다. 보강 전DmChatEventClientTest에서 HTTP 500 failure 누락과 마지막 frame 누락 RED를 확인했고, 이후./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatEventClientTest",./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*",./gradlew :app:compileDebugKotlin,./gradlew :app:ktlintCheck,git diff --checkPASS를 확인했다. -
2026-06-10: Phase 3 구현 전
DmChatRoomViewModelTest,DmChatPaginationStateTest를 먼저 추가하고./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest"를 실행해DmChatRoomViewModel및DmChatRoomUiState심볼 부재로 실패하는 RED 상태를 확인했다. -
2026-06-10: Phase 3 범위로
DmChatRoomViewModel.kt,DmChatUiModels.kt,DmChatRoomViewModelTest.kt,DmChatPaginationStateTest.kt를 추가/수정했다. roomId/creatorId 진입, invalid 종료 이벤트, OpenRoom 정렬, pagination guard/prepend/중복 제거, blank 전송 무시, pending 추가, 성공 교체, 실패 상태, retry 성공, SSE 중복 제거, 재연결 후 최신 메시지 동기화를 ViewModel 상태로 구현했다. -
2026-06-10:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatPaginationStateTest",./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*"PASS를 확인했다. -
2026-06-10: Phase 3 최종 확인으로
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*",./gradlew :app:compileDebugKotlin,./gradlew :app:ktlintCheck,git diff --checkPASS를 확인했다.ktlintCheck에서는 기존.editorconfig의disabled_rulesdeprecation warning이 출력됐지만 실패는 없었다. -
2026-06-10: Phase 3 리뷰에서 전송 성공 전 SSE echo가 먼저 도착하면 같은
messageId가 중복될 수 있는 문제가 발견되어DmChatRoomViewModelTest에 재현 테스트를 추가했다. 보강 전 해당 테스트는 중복 메시지 assertion으로 RED를 확인했고, 전송 성공 local 교체 후 동일messageId를 한 개로 정리하도록DmChatRoomViewModel을 수정했다. 이후 해당 단일 테스트와./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatPaginationStateTest"PASS를 확인했다. -
2026-06-10: Phase 3 추가 리뷰 보강으로 retry 중 SSE echo가 먼저 도착해도 성공 교체 후 동일
messageId가 한 개만 남는 테스트, 과거 메시지 요청 실패 시isLoadingOlder=false로 복구하고 기존 목록을 유지하는 테스트, 재연결 최신 메시지 동기화 실패 시 기존 메시지를 유지하는 테스트를 추가했다.isSending단일 전송 guard와 pagination/reconnect 실패의 silent 유지 정책은 Phase 3 ViewModel 범위의 의도된 제약으로 문서화했다. 이후./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatPaginationStateTest"PASS를 확인했다. -
2026-06-10: Phase 4 범위로
activity_dm_chat_room.xml,item_dm_chat_my_message.xml,item_dm_chat_opponent_message.xml,DmChatMessageAdapter.kt를 추가했다. DM 레이아웃은 기존 채팅방의 배경/딤/header/메시지 목록/input 구조를 따르되 header에는iv_back,iv_profile,tv_name만 두고rv_messages를header_container하단에 연결했다. 메시지 Adapter는DiffUtil, stable id(messageId우선,localIdfallback), 내 메시지 실패 재시도 callback, 상대 프로필ic_placeholder_profile로딩을 구현했다. -
2026-06-10: Phase 4 검증으로
rg "character_type_badge|ll_can_badge|iv_more|notice_container" app/src/main/res/layout/activity_dm_chat_room.xml결과 없음,./gradlew :app:compileDebugKotlin,./gradlew :app:ktlintCheck,./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*",./gradlew :app:assembleDebug,git diff --checkPASS를 확인했다.ktlintCheck에서는 기존.editorconfig의disabled_rulesdeprecation warning이 출력됐지만 실패는 없었다. Phase 4 변경 파일 대상 리뷰어 검토에서도 blocking issue 없음 PASS를 받았다. -
2026-06-10: Phase 4 리뷰 개선 권장 사항 반영으로 DM 메시지 item XML의 고정
layout_constraintWidth_max를 제거해 말풍선 폭 제어를 Adapter 비율 기준으로 통일했다. 내 메시지는 기존 사용자 메시지 관례처럼 65%, 상대 메시지는 기존 AI/상대 메시지 관례와guideline_90에 맞춰 90%를 적용했다. 또한DmChatMessageAdapter의 local/fallback stable id를 64-bit 문자열 해시 기반 음수 namespace로 분리해 서버messageId와의 충돌 가능성을 낮췄다. -
2026-06-10: Phase 4 리뷰 개선 반영 후
rg "layout_constraintWidth_max" app/src/main/res/layout/item_dm_chat_my_message.xml app/src/main/res/layout/item_dm_chat_opponent_message.xml결과 없음,./gradlew :app:ktlintCheck,./gradlew :app:compileDebugKotlin,./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*"PASS를 확인했다. 최초 병렬 Gradle 실행에서는kspCaches/debug증분 캐시 동시 접근으로:app:kspDebugKotlin이 실패했으나, 동일 명령을 순차 재실행해 PASS를 확인했다. -
2026-06-10: Phase 4 재리뷰 후속 보강으로 남은 개선 권장 사항을 반영했다. (4) 재시도 아이콘을 시스템 리소스
@android:drawable/ic_popup_sync에서 프로젝트 전용 vectoric_dm_retry로 교체했다. (5) 내/상대 말풍선 폭 기준 불일치(65% vs 90%)를 단일 상수MESSAGE_MAX_WIDTH_RATIO=0.68f로 통일하고,item_dm_chat_opponent_message.xml의guideline_90의존을 제거해 폭 제어를 Adapter 비율 단일 소스로 일원화했다. 권장 1(폭 이중 제어)·2(stableId namespace)는 직전 보강에서 이미 반영된 상태를 확인했다. -
2026-06-10: 위 보강 검증으로
./gradlew :app:ktlintCheck(ktlintMainSourceSetCheck) PASS를 확인했다. 단, 본 작업 환경에는 JDK 17이 없고 Android Studio JBR 21만 존재해jvmToolchain(17)을 요구하는:app:assembleDebug/testDebugUnitTest는 toolchain 미탐지로 실행하지 못했다. 변경은 XML/소량 Kotlin 수정으로 surgical하며 JDK 17 환경에서 빌드/테스트 재확인이 필요하다. -
2026-06-10: Phase 5 구현 전
DmChatRoomActivitySourceTest,DmChatRoomViewModelTest,ChatMainFragmentLayoutTest를 추가/수정하고./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest",./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest",./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest"를 실행해DmChatRealtimeClient,connectRealtime(),disconnectRealtime(),DmChatRoomActivity/DM routing 부재로 RED 상태를 확인했다. -
2026-06-10: Phase 5 범위로
DmChatRoomActivity.kt를 추가하고 Activity intent helper, ViewBinding, header, RecyclerView, input/IME send, 상단 pagination, prepend scroll 보정, 하단 근처 auto-scroll을 연결했다.DmChatRoomViewModel에는 SSE connect/disconnect lifecycle, connected callback 최신 동기화, message callback 병합, duplicate connect/disconnect guard, disconnect 실패 silent 처리 상태를 추가했다.DmChatRepository에는DmChatRealtimeClientseam을 추가하고DmChatEventClient가 이를 구현하도록 정리했다.ChatMainFragment는 AI item은 기존ChatRoomActivity, DM item은DmChatRoomActivity.newIntentByRoomId()로 이동하도록 분기했다. -
2026-06-10: Phase 5 검증으로 targeted 테스트 3종,
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*",./gradlew :app:compileDebugKotlin,./gradlew :app:ktlintCheck,git diff --check,./gradlew :app:assembleDebugPASS를 확인했다.ktlintCheck에서는 기존.editorconfig의disabled_rulesdeprecation warning이 출력됐지만 실패는 없었다. -
2026-06-10: Phase 5 리뷰 게이트에서 SSE listener callback이 OkHttp background thread에서 호출되어
LiveData.setValue()crash 가능성이 있다는 blocking issue가 발견됐다.DmChatRoomViewModelTest에 main thread scheduler 사용을 고정하는 RED 테스트를 추가해 실패를 확인한 뒤,connectRealtime()의onConnected/onMessage/onFailure상태 갱신을AndroidSchedulers.mainThread().scheduleDirect로 marshal하도록 수정했다. 이후./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest",./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*",./gradlew :app:compileDebugKotlin,./gradlew :app:ktlintCheck,git diff --check,./gradlew :app:assembleDebugPASS를 재확인했다. -
2026-06-11: Phase 5 코드리뷰 권장/관찰 항목을 계획 문서에 후속 Task로 반영했다. Phase 5에는
bindContent의 반복connectRealtime()호출 정리, SSE 자동 재연결 미구현 정책 인지, disconnect 중 빠른 재진입 확인을Task 5.4로 추가했다. Phase 6에는 SSE 전용 read timeout 제거를Task 6.4, realtime callback schedulingDisposable누적 방지를Task 6.5로 추가했다. 이번 변경은 문서 갱신만 수행했으며 Android 빌드/테스트는 실행하지 않았다. -
2026-06-11:
prd.md의 SSE Realtime Events 요구사항과 성공 기준을 재확인한 결과, 네트워크 오류 후 SSE 자동 재연결은 PRD 범위에 포함되는 것으로 판단했다. 이에 따라Task 5.4의 “자동 재연결 미구현” 문구를 정정하고, foreground/활성 채팅방 상태에서 서버reconnectTime=3000ms 기준 재연결 예약, 재연결 성공 후GetMessages누락 메시지 보정, 화면 이탈/background 전환 시 예약 재연결 취소를 후속 작업으로 명시했다. 이번 변경은 문서 갱신만 수행했으며 Android 빌드/테스트는 실행하지 않았다. -
2026-06-11: Task 5.4 리뷰 게이트 후속 보강으로
disconnectRealtime()의 local realtime 정리 순서를isDisconnectingAPI 중복 guard보다 앞에 두어 disconnect API 진행 중 다시 background로 가는 경우에도 새 SSE 연결과 예약 재연결이 cancel되도록 수정했다. 또한DmChatEventClient가 취소되지 않은 SSE stream EOF 종료를SSE stream closedfailure callback으로 전달하도록 보강해 조용한 stream 종료도 ViewModel의 3초 재연결 경로로 들어가게 했다. 기존 403번째 stale 완료 기록은 제거 상태를 유지했다. 회귀 테스트로disconnect API 진행 중 다시 background로 가면 새 SSE 연결도 cancel하고 API 중복 호출은 하지 않는다,취소되지 않은 SSE stream이 EOF로 종료되면 failure callback으로 전달된다를 추가했고,./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatEventClientTest",./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*",./gradlew :app:compileDebugKotlin,./gradlew :app:ktlintCheck,git diff --check,./gradlew :app:assembleDebugPASS를 확인했다. -
2026-06-11: Phase 5 코드리뷰 권장 변경사항 A-D를 각각 후속 Task로 추가했다.
Task 5.5는 자동 재연결 실행 스레드 race 제거,Task 5.6은 disconnect와 예약 재연결 경합 방지,Task 5.7은 SSE 재연결 backoff 또는 시도 제한 검토,Task 5.8은roomOpenedEventLiveData스티키 재전달 방지를 다룬다. 이번 변경은 문서 갱신만 수행했으며 Android 빌드/테스트는 실행하지 않았다. -
2026-06-11: Phase 5.5~5.8 범위로 예약 재연결 실행 시
connectRealtime()를 scheduler thread에서 직접 호출하지 않고 main callback 경로로 전달하도록 수정했다. disconnect 이후 예약 재연결이 실행되어도shouldReconnectRealtime재확인으로 새 SSE 연결이 남지 않도록 했고,roomOpenedEventLiveData는DmChatEvent<Boolean>소비형 이벤트로 바꿔 observer 재등록만으로 realtime connect가 반복 트리거되지 않도록 했다. SSE 재연결 정책은 PRD의 서버reconnectTime=3000ms 및 foreground 한정 조건을 우선해 backoff/최대 횟수 제한을 추가하지 않고 3초 반복 재시도 유지로 테스트 고정했다. -
2026-06-11: Phase 5.5~5.8 검증으로 RED 단계에서
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest"가consumeAPI 부재로 실패함을 확인했고, 구현 후./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest",./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*",./gradlew :app:compileDebugKotlin,./gradlew :app:ktlintCheck,git diff --check,./gradlew :app:assembleDebugPASS를 확인했다.ktlintCheck에서는 기존.editorconfig의disabled_rulesdeprecation warning이 출력됐지만 실패는 없었다.