117 KiB
DM 채팅화면 구현 계획/TASK
1. 목표
docs/20260610_DM_채팅화면/prd.md를 기준으로 신규 DM 채팅방 상세 화면을 v2 패키지 하위에 구현한다. 기존 AI ChatRoomActivity는 직접 수정하지 않고, DM 전용 Activity, ViewModel, Repository, DTO, WebSocket 클라이언트, 메시지 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>패턴을 사용한다. - Phase 1~8은 기존 SSE 기반 구현 이력으로 보존한다. WebSocket 전환은 Phase 9부터 기존 SSE 구현을 교체하는 후속 범위로 진행한다.
- WebSocket은
OkHttpClient.newWebSocket()기반 전용DmChatSocketClient를 추가하고, 기존DmChatEventClient/SSE parser는 WebSocket 전환 완료 후 사용하지 않는다. - WebSocket 연결/해제는 Activity foreground 범위와 로그아웃 흐름에서 처리한다.
onStart:OpenRoom완료 후 연결 가능 상태면 WebSocket 연결을 시작하고JOIN_ROOM을 보낸다.JOINED: 실시간 수신 가능 상태로 판단한다.onStop/화면 이탈/앱 background/로그아웃:LEAVE_ROOM전송 후 socket close를 수행한다.onDestroy/onCleared: listener 참조, heartbeat, reconnect 예약, socket을 정리한다.
- WebSocket 재연결 후에는
JOIN_ROOM을 다시 보내고, 필요하면GetMessages로 최신 누락 가능 메시지를 동기화한다. - 네트워크 오류로 WebSocket이 실패하면 화면이 foreground에 있고 채팅방이 활성 상태인 경우에만 재연결을 시도한다.
- 재연결 성공 후
JOIN_ROOM을 다시 보내고GetMessages로 누락 가능 메시지를 보정한다. - 화면 이탈 또는 background 전환 시 예약된 재연결 시도는 취소한다.
- 재연결 성공 후
VOICE메시지는 DTO에 보존하되 이번 UI 목록에는 표시하지 않는다.- 전송은 낙관적 UI를 적용한다.
- 전송 직후 local pending 메시지를 추가한다.
- WebSocket
SEND_ACK수신 시requestId로 pending 메시지를 찾아 서버messageId,createdAt, 프로필 정보로 확정한다. ERROR또는 timeout 시 실패 상태와 재시도 버튼을 표시한다.
- Phase 9 이후 텍스트 전송 정책은
requestId단위 pending map으로 관리한다.- 서로 다른 텍스트 메시지는 각각의
requestId로 독립 pending 상태를 가질 수 있다. - 같은 UI item 재시도는 새
requestId를 발급하되 기존 local item을 유지한다.
- 서로 다른 텍스트 메시지는 각각의
- 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/WebSocket 연결 제어를 담당한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt- 방 생성/열기, pagination, WebSocket 메시지 전송/수신, leave/close 상태를 관리한다.
- 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- REST API 호출 래핑, token 전달, WebSocket 클라이언트 위임을 담당한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatEventClient.kt- 기존 SSE 연결 구현이다. Phase 9 이후 WebSocket 전환 완료 시 신규 경로에서 사용하지 않으며 제거 또는 미사용 상태로 둔다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatSocketClient.kt- OkHttp WebSocket 연결, handshake header,
JOIN_ROOM/LEAVE_ROOM/SEND_TEXT/PING송신, 수신 callback, close 처리를 담당한다.
- OkHttp WebSocket 연결, handshake header,
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatSocketModels.kt- WebSocket envelope, payload,
requestId기반SEND_ACK,ERROR,MESSAGE,PONG모델을 정의한다.
- WebSocket envelope, payload,
- Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatSocketClientTest.kt- WebSocket handshake header, endpoint, send/receive envelope, close 동작을 검증한다.
- Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatSocketParserTest.ktJOINED,MESSAGE,SEND_ACK,ERROR,PONG, 알 수 없는 type 파싱을 검증한다.
- 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,DmChatSocketClient,DmChatRoomViewModelDI를 등록한다.
- Modify:
app/src/main/AndroidManifest.xmlDmChatRoomActivity를 등록한다.
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt- 푸시 payload의
chat_type,room_id를DeepLinkActivity로 전달한다.
- 푸시 payload의
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.ktchat_type == "USER_CREATOR"와room_id기준으로 DM 채팅방 진입 intent를 만든다.
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt- 로그인/메인 진입 후 전달된 DM 푸시 extras를 DM 채팅방 진입으로 연결한다.
- 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 입력을 무시하고, WebSocket
SEND_TEXT와requestIdpending 매칭을 사용한다. - 전송 실패 메시지는
ERROR또는 timeout 기준으로 실패 상태와 재시도 버튼을 표시하고, 재시도 성공 시 정상 메시지로 교체된다. - 화면 stop/destroy/background/logout 흐름에서
LEAVE_ROOM전송과 socket close가 화면 종료를 막지 않는다. - 제거된
GET /api/v2/user-creator-chat/rooms/{roomId}/events,POST /api/v2/user-creator-chat/rooms/{roomId}/events/disconnect, 텍스트 전송용POST /api/v2/user-creator-chat/rooms/{roomId}/messages/text는 신규 텍스트 실시간 송수신 경로에서 호출하지 않는다. - 음성 메시지는 기존 multipart REST 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:
-
Task 6.6: ViewModel onCleared 정리 보장
- 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:
- 작업:
DmChatRoomViewModel에onCleared()오버라이드를 추가한다.- ViewModel 소멸 후
mainHandler.post { action() }로 예약된 realtime callback이 실행되지 않도록mainHandler.removeCallbacksAndMessages(null)를 호출한다. compositeDisposable.clear()또는dispose(),reconnectDisposable.dispose(),realtimeClient.cancel()정리가onCleared()에서도 보장되는지 확인하고 누락된 정리를 추가한다.- Activity
onStop/onDestroy경로에만 의존하지 않고 ViewModel lifecycle 종료만으로 realtime 연결과 예약 작업이 정리되도록 한다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" - Expected:
onCleared()이후 예약된 main callback이 상태를 갱신하지 않고, composite/reconnect disposable 및 realtime client cancel 정리가 호출된다.
- Run:
- Files:
-
Task 6.7: DmChatEventClientTest reflection 의존 제거 검토
- Files:
- Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatEventClientTest.kt
- Modify:
- 작업:
getDeclaredField("okHttpClient")로 내부 필드를 읽는 검증을 동작 기반 검증으로 대체할 수 있는지 확인한다.- 최소 변경으로 가능하면
MockWebServer또는 실제 request/stream 동작을 통해 SSE 전용 read timeout 정책과 기존 parsing/cancel/failure 동작을 검증한다. - reflection 제거가 과도한 테스트 구조 변경을 요구하면 현재 검증은 유지하되, 필드명 변경에 취약한 낮은 우선순위 테스트 부채로 문서에 남긴다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatEventClientTest" - Expected: 내부 필드명에 직접 의존하지 않거나, 유지 사유가 명확히 기록되고 기존 SSE 전용 client 동작 검증은 유지된다.
- 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 입력은 전송되지 않는다.
- 텍스트 전송 실패 시 실패 상태와 재시도 버튼이 표시된다.
- Phase 9~13 WebSocket 전환 후 화면 이탈 또는 앱 background 전환 시
LEAVE_ROOM전송 후 socket close가 호출된다. - Phase 9~13 WebSocket 전환 후 WebSocket 연결 실패가 앱 crash로 이어지지 않는다.
- Files:
Phase 8: 크리에이터 채널 DM 진입 crash 수정
-
Task 8.1: creatorId 기반 진입 thread crash 재현 테스트 추가
- Files:
- Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt
- Modify:
- 작업:
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한다.
- Run:
- 검증 기록:
- 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:126assertion failure로 RED가 되었고, 현재createRoomAndOpen()에는flatMap이후OpenRoom결과 처리 전 main thread 재전환이 없음을 확인했다.
- 2026-06-17:
- Files:
-
Task 8.2: createRoomAndOpen main thread 전환 보장
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt
- Modify:
- 작업:
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가 발행된다.
- Run:
- 검증 기록:
- 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를 확인했다.
- 2026-06-17:
- Files:
-
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
- Check:
- 작업:
- 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. 기존
.editorconfigdisabled_rulesdeprecation warning은 실패로 보지 않는다.
- Run:
- 검증 기록:
- 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_rulesdeprecation warning이 출력됐지만 실패는 없었다.
- 2026-06-17:
- Files:
-
Task 8.4: 수동 확인 항목 갱신
- Files:
- Check:
DmChatRoomActivity - Check:
CreatorChannelActivity
- Check:
- 확인 항목:
- 크리에이터 채널에서
DM 보내기를 터치하면DmChatRoomActivity로 이동한다. creatorId기반 진입 후 방 생성/조회와 OpenRoom 결과 반영 중 앱이 crash 되지 않는다.- header 상대 정보와 초기 메시지 목록이 표시된다.
- 채팅 탭의 기존
roomId기반 DM 진입은 기존처럼 동작한다.
- 크리에이터 채널에서
- Files:
Phase 9: WebSocket 계약/클라이언트 기반 추가
-
Task 9.1: WebSocket envelope와 payload 모델 정의
- Files:
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatSocketModels.kt - Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatSocketParserTest.kt
- Create:
- 작업:
- WebSocket 공통 envelope는
type과payload를 분리해 파싱할 수 있게 정의한다. - Client send type은
JOIN_ROOM,LEAVE_ROOM,SEND_TEXT,PING을 지원한다. - Server receive type은
JOINED,MESSAGE,SEND_ACK,ERROR,PONG을 지원한다. JOIN_ROOM/LEAVE_ROOMpayload는 현재roomId를 포함한다.SEND_TEXTpayload는roomId,requestId,textMessage를 포함한다.SEND_ACKpayload는requestId와 서버 확정 메시지(DmChatMessageResponse)를 포함한다.MESSAGEpayload는 서버 메시지(DmChatMessageResponse)를 포함한다.ERRORpayload는requestId가 있을 수도 있고 없을 수도 있으므로 nullable로 모델링하고, message/code 필드를 보존한다.- 서버의 정확한 JSON field name은 백엔드 계약과 대조해 확정하되, Android 모델은 서버 field name을
@SerializedName으로 보존한다.
- WebSocket 공통 envelope는
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketParserTest" --max-workers=1 - Expected:
JOINED,MESSAGE,SEND_ACK,ERROR,PONG, 알 수 없는 type, 잘못된 JSON 파싱 테스트가 PASS.
- Run:
- 검증 기록:
- 2026-06-18:
DmChatSocketParserTest를 먼저 추가하고./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketParserTest" --max-workers=1실행 시DmChatSocketEvent/DmChatSocketParser미정의 컴파일 오류로 RED를 확인했다. 이후DmChatSocketModels.kt에type/payloadenvelope,JOIN_ROOM/LEAVE_ROOM/SEND_TEXT/PINGclient type,JOINED/MESSAGE/SEND_ACK/ERROR/PONGparser 모델을 추가했다. 재실행 결과DmChatSocketParserTestPASS를 확인했고,./gradlew :app:compileDebugKotlin --max-workers=1,./gradlew :app:ktlintCheck --max-workers=1도 PASS했다.ktlintCheck에서는 기존.editorconfig의disabled_rulesdeprecation warning이 출력됐지만 실패는 없었다. - 2026-06-18: 리뷰에서
ERROR의code/messagenullable 보존 테스트가 부족하다는 지적을 받아code=null,message=null케이스를 추가했다. 수정 전./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketParserTest" --max-workers=1실행 결과ERROR type은 nullable code와 message를 보존한다테스트가 RED로 실패함을 확인했고,DmChatSocketErrorPayload와DmChatSocketEvent.Error의code/message를 nullable로 변경한 뒤 같은 테스트가 PASS했다. 이후./gradlew :app:compileDebugKotlin --max-workers=1,./gradlew :app:ktlintCheck --max-workers=1,./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*" --max-workers=1모두 PASS를 확인했다. - 2026-06-18: 코드 리뷰로
DmChatSocketModels.kt와DmChatSocketParserTest.kt를 재검토했다.type/payloadenvelope,JOINED/MESSAGE/SEND_ACK/ERROR/PONG파싱, nullableERROR필드 보존, 알 수 없는 type/잘못된 JSON 무시 동작이 Task 9.1 범위와 일치함을 확인했고 추가 결함은 발견하지 못했다. 재검증으로./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketParserTest" --max-workers=1PASS를 확인했다.
- 2026-06-18:
- Files:
-
Task 9.2: OkHttp WebSocket 클라이언트 추가
- Files:
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatSocketClient.kt - Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatSocketClientTest.kt
- Create:
- 작업:
DmChatSocketClient는OkHttpClient.newWebSocket()으로${BuildConfig.BASE_URL}의 scheme을 WebSocket scheme으로 변환해/ws/v2/user-creator-chat에 연결한다.https://는wss://,http://는ws://로 변환한다.
- handshake request에
Authorization: Bearer <accessToken>header를 추가한다. connect(token, listener)는 socket 연결만 수행하고, 방 참여는 ViewModel이sendJoinRoom(roomId)로 명시 호출한다.sendJoinRoom(roomId),sendLeaveRoom(roomId),sendText(roomId, requestId, textMessage),sendPing()를 제공한다.close()는 진행 중 socket을 close하고 listener 참조를 해제한다.onMessage는DmChatSocketModelsparser로 envelope를 파싱해 listener callback으로 전달한다.- 알 수 없는 type 또는 파싱 실패는 앱 crash 없이 listener failure 또는 ignored event로 처리한다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketClientTest" --max-workers=1 - Expected: endpoint URL 변환, Authorization header,
JOIN_ROOM/LEAVE_ROOM/SEND_TEXT/PINGJSON 송신, 수신 callback, close 정리 테스트가 PASS.
- Run:
- 검증 기록:
- 2026-06-18:
DmChatSocketClientTest를 먼저 추가하고./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketClientTest" --max-workers=1실행 시DmChatSocketClient미정의 컴파일 오류로 RED를 확인했다. 이후DmChatSocketClient.kt를 추가해https/httpbase URL의 WebSocket scheme 변환,Authorization: Bearer ...handshake header, 명시적JOIN_ROOM/LEAVE_ROOM/SEND_TEXT/PING송신, parser 기반 수신 event callback, 알 수 없는 type/잘못된 JSON 무시, close 정리를 구현했다. 구현 직후 URL scheme 검증 2건이 RED로 남아 OkHttpRequest.url정규화 특성을 반영해 원본 WebSocket URL을 request tag로 보존하도록 조정했고, 재실행 결과DmChatSocketClientTestPASS를 확인했다../gradlew :app:compileDebugKotlin --max-workers=1,./gradlew :app:ktlintCheck --max-workers=1,./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*" --max-workers=1도 PASS했다.ktlintCheck에서는 기존.editorconfig의disabled_rulesdeprecation warning이 출력됐지만 실패는 없었다. - 2026-06-18: 코드 리뷰로
DmChatSocketClient.kt와DmChatSocketClientTest.kt를 재검토했다. WebSocket endpoint scheme 변환, bearer header, 명시적JOIN_ROOM/LEAVE_ROOM/SEND_TEXT/PING송신, 현재 socket guard, close 정리, parser event 전달이 Task 9.2 범위와 일치함을 확인했고 추가 결함은 발견하지 못했다. 재검증으로./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketClientTest" --max-workers=1,./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=1PASS를 확인했다.
- 2026-06-18:
- Files:
-
Task 9.3: Repository와 DI를 WebSocket 클라이언트 기준으로 전환
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatRepository.kt - Modify:
app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRepositoryTest.kt
- Modify:
- 작업:
DmChatRepository는connectRealtime()/cancelRealtime()seam을 WebSocket 클라이언트 위임으로 교체하거나connectSocket()/closeSocket()처럼 의미가 분명한 이름으로 변경한다.- REST
createOrGetRoom,openRoom,getMessages는 유지한다. - 텍스트 전송용 REST
sendTextMessage()와disconnectRealtime()repository method는 신규 전송/해제 경로에서 사용하지 않도록 제거하거나 deprecated 없이 삭제한다. AppDI.kt는DmChatEventClient대신DmChatSocketClient를 등록한다.Authorizationheader 생성은 기존bearer(token)helper를 재사용해 REST와 WebSocket의 bearer 문자열이 일관되도록 한다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRepositoryTest" --max-workers=1 - Expected: REST create/open/messages는 기존처럼 동작하고, WebSocket connect/send/close 위임과 bearer header 생성 테스트가 PASS.
- Run:
- 검증 기록:
- 2026-06-18:
DmChatRepository와AppDI를DmChatSocketClient기준으로 전환하고 RESTcreateOrGetRoom/openRoom/getMessages경로는 유지됨을 확인했다. 대상 실행으로DmChatRepositoryTest,DmChatSocketClientTest,DmChatRoomActivitySourceTest,DmChatRoomViewModelTest가 PASS했고,./gradlew :app:compileDebugKotlin --max-workers=1및 style 정리 후./gradlew :app:ktlintCheck --max-workers=1도 PASS했다.ktlintCheck에는 기존.editorconfig의disabled_rulesdeprecation warning만 남았다. - 2026-06-18: Phase 9 코드 리뷰로
DmChatRepository,AppDI,DmChatRoomViewModel,DmChatRoomActivity의 WebSocket 전환 diff를 재검토했다.DmChatRepositoryTest,DmChatSocketClientTest,DmChatRoomActivitySourceTest,DmChatRoomViewModelTest,compileDebugKotlin,ktlintCheck,git diff --check는 PASS했다. 다만DmChatRoomViewModel이JOINED수신 전isRealtimeConnected=true로 처리하고, 전송 제어가 아직 단일isSending기준이라 Phase 9 이후 정책인requestId단위 독립 pending과 완전히 맞지 않는 보완 필요 사항을 발견했다. 해당 보완은 Phase 10의 ViewModel WebSocket 세션/전송 전환 범위에서 처리한다.
- 2026-06-18:
- Files:
-
Task 9.4: 제거 endpoint API 정의 삭제
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatApi.kt - Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatModels.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRepositoryTest.kt
- Modify:
- 작업:
DmChatApi.sendDmTextMessage()endpoint 정의를 삭제한다.DmChatApi.disconnectRealtime()endpoint 정의를 삭제한다.SendDmTextMessageRequest,SendDmChatMessageResponse모델은 REST 텍스트 전송 전용이면 삭제한다.- 음성 메시지 multipart REST API가 현재 DM 채팅 화면에 필요하면 별도
sendDmVoiceMessage()로 명시하고, 텍스트 전송 경로와 섞지 않는다.
- 검증:
- Run:
rg "messages/text|events/disconnect|SendDmTextMessageRequest|SendDmChatMessageResponse|disconnectRealtime\\(" app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm - Expected: WebSocket 전환 후 유지가 필요한 과거 검증 로그를 제외하고 main/test 코드의 신규 경로에서 결과 없음.
- Run:
- 검증 기록:
- 2026-06-18:
messages/text|events/disconnect|SendDmTextMessageRequest|SendDmChatMessageResponse|disconnectRealtime\(검색 결과 출력이 없음을 확인했다. 대상 실행으로DmChatRepositoryTest,DmChatSocketClientTest,DmChatRoomActivitySourceTest,DmChatRoomViewModelTest가 PASS했고,./gradlew :app:compileDebugKotlin --max-workers=1및 style 정리 후./gradlew :app:ktlintCheck --max-workers=1도 PASS했다.ktlintCheck에는 기존.editorconfig의disabled_rulesdeprecation warning만 남았다. - 2026-06-18: Phase 9 재검증으로
rg "messages/text|events/disconnect|SendDmTextMessageRequest|SendDmChatMessageResponse|disconnectRealtime\\(" app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm결과 없음,git diff --checkPASS를 확인했다. 최초 Gradle 실행은 sandbox의~/.gradlelock 파일 접근 제한으로 실패했으나, 승인된 Gradle 실행에서DmChatRepositoryTest,DmChatSocketClientTest,DmChatRoomActivitySourceTest,DmChatRoomViewModelTest,compileDebugKotlin,ktlintCheck가 PASS했다.
- 2026-06-18:
- Files:
Phase 10: ViewModel WebSocket 세션/수신/전송 전환
-
Task 10.1: OpenRoom 성공 후 JOIN_ROOM 흐름으로 연결 기준 변경
- 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:
- 작업:
OpenRoom성공 전에는 WebSocket 연결을 시작하지 않는다.roomOpenedEventLiveData또는 동등한 단발 이벤트는 Activity가 WebSocket 연결 시작을 트리거하는 용도로 유지한다.- 기존 SSE
connectedcallback 기준 상태 갱신을 제거하고, WebSocketJOINED수신 시점에만 실시간 수신 가능 상태로 판단한다. - Phase 9 코드 리뷰 반영: socket 연결 시도 직후
isRealtimeConnected=true로 확정하지 않고, 연결 시도 상태와JOINED완료 상태를 분리한다. - 같은
roomId로 이미 연결 중이면 중복 socket 연결과 중복JOIN_ROOM전송을 막는다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1 - Expected: OpenRoom 전 미연결, OpenRoom 후 connect +
JOIN_ROOM,JOINED전 connected 미확정,JOINED후 connected 상태, 중복 connect 방지 테스트가 PASS.
- Run:
- 검증 기록:
- 2026-06-18:
DmChatRoomViewModelTest에JOINED전isRealtimeConnected=false,JOINED후true, 중복connectRealtime()시 socket connect와JOIN_ROOM이 1회만 수행되는 검증을 추가했다. 수정 전./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1실행 결과DmChatRoomViewModelTest.kt:326assertion failure로 RED를 확인했다. 이후DmChatRoomViewModel에isRealtimeJoining/currentRealtimeRoomId를 추가하고JOINED수신 시점에만 connected로 전환하도록 변경했다. 재실행 결과 같은 ViewModel 테스트가 PASS했고,./gradlew :app:compileDebugKotlin --max-workers=1,./gradlew :app:ktlintCheck --max-workers=1,git diff --check도 PASS했다.ktlintCheck에서는 기존.editorconfig의disabled_rulesdeprecation warning만 출력됐다.
- 2026-06-18:
- Files:
-
Task 10.2: MESSAGE 수신 반영으로 교체
- 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:
- 작업:
- 기존 SSE
onMessage()callback 연결을 WebSocketMESSAGEevent callback으로 교체한다. MESSAGEpayload의DmChatMessageResponse를 기존 mapper로 UI item에 변환한다.- 현재 채팅방 메시지는
messageId기준 중복 제거 후 append/merge한다. - 현재 room이 아닌 메시지가 payload로 구분 가능하면 현재 목록에 반영하지 않는다.
- 잘못된 payload 또는 알 수 없는 type은 앱 crash 없이 무시한다.
- 기존 SSE
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1 - Expected:
MESSAGEappend, 중복messageId제거, 잘못된 payload 무시 테스트가 PASS.
- Run:
- 검증 기록:
- 2026-06-18: 기존
socketFactory.emitMessage()기반 WebSocketMESSAGEcallback이handleSocketEvent()에서onRealtimeMessage()로 연결되고,DmChatMessageResponse.toUiItem()및mergeByMessageId()로 UI 목록에 반영되는 흐름을 확인했다.DmChatRoomViewModelTest의realtime message callback은 SSE 메시지를 화면 상태에 병합한다,SSE 메시지는 messageId 중복을 제거하고 최신 메시지를 추가한다테스트를 유지해MESSAGEappend와messageId중복 제거 회귀를 고정했다../gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1PASS를 확인했다.
- 2026-06-18: 기존
- Files:
-
Task 10.3: SEND_TEXT/requestId pending 전송으로 교체
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt - Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/model/DmChatUiModels.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt
- Modify:
- 작업:
sendText(text)는 trim 후 blank면 종료한다.- 전송 시
requestId를 생성하고 local pending item에 보존한다. - Phase 9 코드 리뷰 반영: 단일
isSending전역 제한을 제거하고, 서로 다른 텍스트 메시지는 각각의requestId로 독립 pending 상태를 가질 수 있게 한다. - pending 상태는
requestId기준 map 또는 동등하게 검증 가능한 구조로 관리한다. - REST
repository.sendTextMessage()호출을 제거하고 WebSocketSEND_TEXT를 전송한다. SEND_ACK수신 시requestId로 pending item을 찾아 서버messageId,createdAt,senderNickname,senderProfileImageUrl기준으로 확정한다.ERROR또는 timeout 시 해당 pending item을FAILED로 전환한다.retry(localId)는 기존 failed item을 유지하고 새requestId를 발급해SEND_TEXT를 다시 전송한다.- 같은
requestId의SEND_ACK가 중복 수신되면 첫 번째 확정 결과만 반영한다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1 - Expected: blank 무시, pending 추가, 서로 다른 텍스트의 독립 pending,
SEND_TEXT송신,SEND_ACKrequestId 매칭,ERROR실패, timeout 실패, retry 새 requestId, 중복 ack 무시 테스트가 PASS.
- Run:
- 검증 기록:
- 2026-06-18:
DmChatRoomViewModelTest에 requestId 기반 독립 pending,SEND_ACKrequestId 매칭,ERROR실패, timeout 실패, retry 새 requestId, 중복 ACK 무시 테스트를 먼저 추가했다. 수정 전./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1실행 시DmChatMessageUiItem.requestId미정의 컴파일 오류로 RED를 확인했고, timeout 테스트는 구현 전DmChatRoomViewModelTest.kt:270assertion failure로 RED를 확인했다. - 2026-06-18:
DmChatMessageUiItem에requestId를 추가하고,DmChatRoomViewModel의 단일isSending제한을 제거해 각 텍스트 전송이 새requestId와 pending map으로 관리되도록 변경했다.SEND_TEXT는 WebSocket으로 전송하고,SEND_ACK/ERROR/send false/10초 timeout은 해당 requestId의 local item만 확정 또는 실패 처리한다. retry는 기존localIditem을 유지하면서 새requestId를 발급하도록 변경했다. 이후./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1PASS를 확인했다.
- 2026-06-18:
- Files:
-
Task 10.4: SEND_ACK보다 MESSAGE가 먼저 도착하는 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:
- 작업:
- pending 텍스트와 동일한 서버 메시지가
SEND_ACK보다 먼저MESSAGE로 도착할 수 있는 케이스를 테스트로 고정한다. MESSAGEpayload에requestId가 포함되면 requestId 기준으로 pending item을 확정한다.MESSAGEpayload에requestId가 없으면messageId중복 제거를 우선 적용하고, 이후SEND_ACK도착 시 같은messageId가 중복되지 않게 병합한다.- timeout으로 실패 처리된 뒤 늦은
SEND_ACK가 도착하면 같은 local item이 아직 존재하는 경우 정상 메시지로 복구한다.
- pending 텍스트와 동일한 서버 메시지가
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1 - Expected: MESSAGE 선도착, ACK 후도착, timeout 후 ACK 복구, 중복
messageId방지 테스트가 PASS.
- Run:
- 검증 기록:
- 2026-06-18:
DmChatSocketParserTest에MESSAGEpayload의 nullablerequestId보존 테스트를 추가하고,DmChatRoomViewModelTest에MESSAGE(requestId)선도착 시 pending local item 확정,MESSAGE에requestId가 없는 경우 후도착SEND_ACK와messageId중복 방지, timeout 실패 후 늦은SEND_ACK의 같은 local item 복구 테스트를 추가했다. 수정 전./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketParserTest" --max-workers=1및 ViewModel 대상 테스트 실행에서DmChatSocketParserTest.kt:61의DmChatSocketEvent.Message.requestId미정의 컴파일 오류로 RED를 확인했다. 이후DmChatSocketEvent.Message와 parser가 nullablerequestId를 보존하도록 변경하고, ViewModel이MESSAGE(requestId)를SEND_ACK와 동일한 pending 확정 경로로 처리하며 timeout 후 실패 requestId도 늦은 ACK로 복구할 수 있도록 변경했다. 재실행 결과DmChatSocketParserTest와DmChatRoomViewModelTest가 PASS했다. 병렬 Gradle 실행 중 1회kspDebugUnitTestKotlin의StreamCorruptedException이 발생했으나, 같은 ViewModel 테스트 단독 재실행에서 PASS를 확인했다. - 2026-06-18: 리뷰에서 timeout된
request-1이 실패 기록에 남은 상태로 같은 local item을request-2로 retry하고request-2ACK 성공 후 늦은request-1ACK가 도착하면 최신 성공 메시지를 오래된 ACK로 덮을 수 있다는 blocker를 확인했다.DmChatRoomViewModelTest에retry 성공 후 이전 timeout request ACK는 같은 local item을 덮어쓰지 않는다테스트를 추가했고, 수정 전./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1실행 결과DmChatRoomViewModelTest.kt:466assertion failure로 RED를 확인했다. 이후 retry 시작 및 ACK 성공 확정 시 같은localId에 묶인 stale failed request 기록을 제거하도록 변경했고, 같은 ViewModel 테스트 재실행 결과 PASS를 확인했다.
- 2026-06-18:
- Files:
-
Task 10.5: Phase 9 코드 리뷰 결과 회귀 테스트 반영
- Files:
- Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt - Modify:
docs/20260610_DM_채팅화면/plan-task.md
- Modify:
- 작업:
- Task 10.1 검증에
JOINED전 connected 미확정 테스트를 추가했는지 확인한다. - Task 10.3 검증에 서로 다른 텍스트 메시지의 독립 pending 테스트를 추가했는지 확인한다.
- Phase 9 코드 리뷰에서 지적된 부정확한 leave 중복 테스트는 실제 연결 없는
closeCount == 0검증으로 유지하지 않는다. LEAVE_ROOM + close의 상세 lifecycle 동작 검증은 기존 범위대로 Task 11.1에서 수행하되, Phase 10 완료 시 해당 테스트 gap이 남아 있음을 문서에 명확히 남긴다.
- Task 10.1 검증에
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1 - Expected: Phase 9 리뷰 3개 항목 중 ViewModel 세션/전송 항목은 Phase 10 테스트로 고정되고, leave/close 항목은 부정확한 테스트 제거 또는 Task 11.1 이관 기록으로 남는다.
- Run:
- 검증 기록:
- 2026-06-18: Phase 9 리뷰 항목 중
JOINED전 connected 미확정은 Task 10.1의roomId가 있으면 realtime 연결 후 connected callback에서 최신 메시지를 동기화한다테스트에서JOINED전isRealtimeConnected=false,JOINED후true로 고정되어 있음을 확인했다. requestId 단위 독립 pending은 Task 10.3의서로 다른 텍스트는 각각 requestId로 독립 pending 전송한다테스트로 고정되어 있음을 확인했다.LEAVE_ROOM + closelifecycle 상세 검증은 Task 11.1 범위로 남기고, 실제 연결 없이closeCount == 0만 확인하던realtime leave 중 중복 요청은 close를 반복할 수 있다테스트는 부정확한 회귀 테스트로 제거했다. Task 10.4까지 포함한DmChatRoomViewModelTest는 이후 재실행해 PASS를 확인한다. - 2026-06-18: Phase 10 전체 단위 코드 리뷰로
DmChatRoomViewModel,DmChatSocketModels,DmChatRoomViewModelTest,DmChatSocketParserTest의 diff를 재검토했다.MESSAGE(requestId)선도착 시 pending local item 확정, requestId 없는MESSAGE와 후도착SEND_ACK의messageId중복 제거, timeout 후 늦은SEND_ACK복구, retry 성공 후 이전 timeout request ACK 무시, Phase 9 리뷰 회귀 테스트 반영 상태를 확인했고 blocking issue는 발견하지 못했다. fresh 검증으로./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1,./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketParserTest" --max-workers=1,./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,git diff --checkPASS를 확인했다. 최초 샌드박스 Gradle 실행은~/.gradlewrapper lock 파일 접근 제한으로 실패해 승인된 Gradle 실행으로 재검증했다. Gradle deprecation warning은 출력됐지만 실패는 없었다.
- 2026-06-18: Phase 9 리뷰 항목 중
- Files:
Phase 11: Lifecycle, reconnect, heartbeat, token 갱신 처리
-
Task 11.1: LEAVE_ROOM 후 socket close로 lifecycle 해제 전환
- 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:
- 작업:
onStop/화면 이탈 시 기존disconnectRealtime()REST 호출 대신LEAVE_ROOM전송 후 socket close를 수행한다.- 앱 background 진입과 로그아웃 흐름에서 같은 leave/close API를 호출할 수 있도록 ViewModel method를 분리한다.
- 이미 leave/close 중이면 중복
LEAVE_ROOM전송과 중복 close를 만들지 않는다. - close 실패 또는 네트워크 단절은 화면 종료를 막지 않는다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1 - Expected: lifecycle stop에서
LEAVE_ROOM+ close, 중복 방지, REST disconnect 미호출 테스트가 PASS.
- Run:
- 검증 기록:
- 2026-06-18:
DmChatRoomViewModelTest에leave는 LEAVE_ROOM 전송 후 socket을 close하고 중복 호출은 무시한다테스트를 추가했다. 수정 전./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1실행에서 중복 leave/close 및 이후 heartbeat/token 관련 RED 실패를 확인했다. 이후leaveRealtime()에 활성 socket guard와 heartbeat/reconnect 정리를 추가해LEAVE_ROOM전송 후 socket close가 1회만 수행되도록 했다.
- 2026-06-18:
- Files:
-
Task 11.2: WebSocket reconnect 정책 구현
- 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:
- 작업:
- 네트워크 오류 또는 비정상 close가 발생하면 현재 채팅방 화면에 남아 있는 동안에만 재연결을 예약한다.
- 재연결 성공 후
JOIN_ROOM을 다시 보낸다. JOINED수신 후 필요하면GetMessages(cursor = null, limit = 20)로 최신 누락 메시지를 동기화한다.- 화면 이탈/background/logout 이후 예약된 reconnect는 실행하지 않는다.
- backoff 간격과 최대 재시도 횟수는 PRD Open Questions에 남긴 상태이므로, 구현 전 서버 권장값이 없으면 기존 3초 반복 재시도 정책을 WebSocket에도 우선 적용한다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1 - Expected: 화면 내부 오류 재연결, 재연결 후
JOIN_ROOM, 최신 메시지 동기화, 화면 밖 reconnect 취소 테스트가 PASS.
- Run:
- 검증 기록:
- 2026-06-18: 기존 WebSocket failure 후 3초 재연결, 재연결 후
JOIN_ROOM,JOINED후 최신 메시지 동기화, leave 이후 reconnect 취소 테스트를 유지하면서 reconnect 예약 실행 시 최신 token을 다시 읽도록 보강했다../gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1재실행 결과 PASS를 확인했다.
- 2026-06-18: 기존 WebSocket failure 후 3초 재연결, 재연결 후
- Files:
-
Task 11.3: PING/PONG heartbeat 구현
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt - Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatSocketClient.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatSocketClientTest.kt
- Modify:
- 작업:
JOINED이후 heartbeat timer를 시작한다.- 주기적으로
PING을 전송하고PONG수신 시 마지막 heartbeat 시간을 갱신한다. PONGtimeout이면 연결 상태를 disconnected로 바꾸고 WebSocket을 close한 뒤 화면 내부 조건에서 reconnect를 예약한다.- leave/close/onCleared 시 heartbeat timer를 취소한다.
- 서버 권장값이 없으면 heartbeat 주기와 timeout 값은 상수로 분리하고 구현 계획 검증 기록에 남긴다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketClientTest" --max-workers=1 - Expected:
PING주기 송신,PONG수신 상태 유지, timeout reconnect, leave 시 timer 취소 테스트가 PASS.
- Run:
- 검증 기록:
- 2026-06-18:
DmChatRoomViewModelTest에JOINED 이후 heartbeat는 PING을 보내고 PONG 수신 시 연결을 유지한다,heartbeat PONG timeout은 socket close 후 foreground 조건에서 reconnect를 예약한다,leave는 heartbeat timeout과 reconnect 예약을 취소한다테스트를 추가했다. RED 단계에서PING미전송과 timeout 미처리 assertion 실패를 확인했고,JOINED이후 30초 주기PING, 10초PONGtimeout, timeout 시 socket close 후 foreground 조건 재연결 예약, leave/onCleared heartbeat 취소를 구현했다. 서버 권장값이 없어 heartbeat 주기 30초, timeout 10초를 상수로 분리했다. - 2026-06-18: 리뷰 게이트에서
PONG수신 시 timeout을 재예약하면 다음PING전에 정상 연결도 닫힐 수 있다는 blocker가 발견됐다.JOINED 이후 heartbeat는 PING을 보내고 PONG 수신 시 연결을 유지한다테스트를PONG수신 후 10초 초과, 다음 30초PING전까지 연결 유지 검증으로 강화했고, 수정 전 해당 단일 테스트가 RED로 실패함을 확인했다. 이후PONG수신 시heartbeatTimeoutDisposable을 해제만 하도록 변경했고 같은 테스트가 GREEN으로 전환됨을 확인했다.
- 2026-06-18:
- Files:
-
Task 11.4: access token refresh 시 WebSocket 재연결
- 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:
- 작업:
- token provider가 이전 handshake token과 다른 token을 반환하면 기존 WebSocket을 close한다.
- 새 token으로 WebSocket handshake를 다시 수행한다.
- 재연결 후 현재
roomId로JOIN_ROOM을 다시 보낸다. - token 갱신 중 화면이 종료되면 reconnect를 진행하지 않는다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1 - Expected: token 변경 감지, 기존 socket close, 새 Authorization header 연결,
JOIN_ROOM재전송, 화면 종료 시 중단 테스트가 PASS.
- Run:
- 검증 기록:
- 2026-06-18:
DmChatRoomViewModelTest에 mutable token provider 기반token이 변경되면 기존 socket을 close하고 새 token으로 다시 JOIN_ROOM을 보낸다,leave 이후 token이 변경되어도 socket reconnect를 진행하지 않는다테스트를 추가했다. RED 단계에서 기존 connected guard가 token 변경을 무시하는 assertion 실패를 확인했고,connectRealtime()에서 같은 room의 token 변경을 connected/joining 조기 return보다 먼저 감지해 기존 socket close 후 새 token으로 WebSocket handshake와JOIN_ROOM을 수행하도록 수정했다. 화면 종료 후에는leaveRealtime()guard로 reconnect가 진행되지 않음을 확인했다. - 2026-06-18: Phase 11 최종 검증으로
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketClientTest" --max-workers=1,./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,git diff --checkPASS를 확인했다. blocker 수정 후에도./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,git diff --checkPASS를 재확인했다.ktlintCheck에서는 기존.editorconfig의disabled_rulesdeprecation warning만 출력됐다. - 2026-06-18: Phase 11 코드 리뷰 및 재검증을 수행했다.
DmChatRoomViewModel의LEAVE_ROOM후 close, 중복 leave 방지, reconnect 예약 시 최신 token 사용,JOINED이후 heartbeat 시작,PONG수신 시 timeout 해제, heartbeat timeout 후 close/reconnect, token 변경 시 기존 socket close 후 새 token으로JOIN_ROOM재전송 흐름을 재검토했고 blocking issue는 발견하지 못했다. 재검증으로./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketClientTest" --max-workers=1,./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,git diff --checkPASS를 확인했다. 최초 샌드박스 Gradle 실행은~/.gradlewrapper lock 파일 접근 제한으로 실패해 승인된 Gradle 실행으로 재검증했다. Gradle deprecation warning은 출력됐지만 실패는 없었다.
- 2026-06-18:
- Files:
Phase 12: 푸시 진입과 제거 endpoint 회귀 검증
-
Task 12.1: FCM payload에 chat_type 전달 추가
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt - Test:
app/src/test/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingServiceSourceTest.kt
- Modify:
- 작업:
sendNotification()에서messageData["chat_type"]이 있으면Constants.EXTRA_DATAbundle에chat_type을 보존한다.- 기존
room_id,message_id,deep_link_value전달은 유지한다. - deep link URL이 있는 경우에도
chat_type/room_id가 필요한지 서버 payload 정책을 확인하고, URL 우선 정책을 유지한다면 이 동작을 테스트에 명시한다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.fcm.SodaFirebaseMessagingServiceSourceTest" --max-workers=1 - Expected:
chat_type과room_id가 notification intent extras에 포함되는 source test가 PASS.
- Run:
- 검증 기록:
- 2026-06-18:
SodaFirebaseMessagingServiceSourceTest를 추가해chat_type,room_id,message_id,deep_link_value가Constants.EXTRA_DATAbundle에 보존되는지 source test로 고정했다. 수정 전./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.fcm.SodaFirebaseMessagingServiceSourceTest" --max-workers=1실행 결과SodaFirebaseMessagingServiceSourceTest.kt:15assertion failure로 RED를 확인했다. 이후SodaFirebaseMessagingService.sendNotification()의 non-URL extras bundle에messageData["chat_type"]복사를 추가했고 같은 테스트 PASS를 확인했다. - 2026-06-18: Phase 12 코드 리뷰에서 FCM payload에
deepLink가 함께 있는 경우 기존 구현이deep_link만 extra bundle에 보존하고chat_type/room_id를 누락해 USER_CREATOR DM 라우팅이 일반 딥링크 흐름으로 빠질 수 있음을 확인했다.SodaFirebaseMessagingServiceSourceTest에 deepLink payload에서도chat_type/room_id를 보존하는 회귀 테스트를 추가했고, 수정 전SodaFirebaseMessagingServiceSourceTest.kt:32assertion failure RED를 확인했다. 이후 deepLink URL 분기에도messageData["chat_type"],messageData["room_id"]복사를 추가했고 같은 테스트 PASS를 확인했다.
- 2026-06-18:
- Files:
-
Task 12.2: USER_CREATOR 푸시를 DM 채팅방으로 라우팅
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt - Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt - Test:
app/src/test/java/kr/co/vividnext/sodalive/main/DeepLinkActivitySourceTest.kt - Test:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2ActivitySourceTest.kt
- Modify:
- 작업:
chat_type == "USER_CREATOR"이고room_id가 Long으로 변환 가능하면DmChatRoomActivity.newIntentByRoomId(context, roomId)로 이동한다.- 푸시로 진입한 채팅방도 일반 진입과 동일하게
OpenRoom후 WebSocket 연결/JOIN_ROOM을 수행한다. room_id가 없거나 잘못된 경우 DM 채팅방을 열지 않고 기존 fallback 흐름을 따른다.- 기존 live/content/message deep link 처리는 변경하지 않는다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.main.DeepLinkActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.main.MainV2ActivitySourceTest" --max-workers=1 - Expected: USER_CREATOR push routing, invalid room fallback, 기존 deep link 유지 테스트가 PASS.
- Run:
- 검증 기록:
- 2026-06-18:
DeepLinkActivitySourceTest와MainV2ActivitySourceTest를 추가해chat_type == "USER_CREATOR"와 validroom_id조합이DmChatRoomActivity.newIntentByRoomId()로 라우팅되는지 source test로 고정했다. 수정 전 실행 결과 두 테스트가 각각DeepLinkActivitySourceTest.kt:13,MainV2ActivitySourceTest.kt:13assertion failure로 RED가 됨을 확인했다. 이후DeepLinkActivity는Constants.EXTRA_DATA에서chat_type을 복사하고 foreground 라우팅에서 USER_CREATOR DM 분기를 기존 live room fallback보다 먼저 처리하도록 변경했다.MainV2Activity도 Splash/Login 이후 전달된 bundle에서 같은 USER_CREATOR DM 분기를 기존 channel/content/message routing보다 먼저 처리하도록 변경했고, 동일 테스트 PASS를 확인했다. - 2026-06-18: 리뷰 게이트에서 LiveRoom foreground 상태일 때
ACTION_LIVE_ROOM_DEEPLINK_CONFIRM브로드캐스트가 USER_CREATOR DM push를 먼저 가로챌 수 있다는 blocker를 확인했다.DeepLinkActivitySourceTest에routeForegroundDeepLink(deepLinkExtras)가 LiveRoom foreground confirm broadcast보다 먼저 실행되는 순서 회귀 테스트를 추가했고, 수정 전DeepLinkActivitySourceTest.kt:28assertion failure로 RED를 확인했다. 이후DeepLinkActivity.onCreate()에서 foreground USER_CREATOR extras는 LiveRoom 브로드캐스트 전에routeForegroundDeepLink(deepLinkExtras)를 먼저 시도하도록 변경했고,./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.main.DeepLinkActivitySourceTest" --max-workers=1PASS를 확인했다. - 2026-06-18: Phase 12 코드 리뷰에서 URL query에
chat_type=USER_CREATOR가 포함된 deepLink 진입은DeepLinkActivity.buildDeepLinkExtras()가room_id는 복사하지만chat_type은 복사하지 않아 DM 분기를 탈 수 없음을 확인했다.DeepLinkActivitySourceTest에 URL querychat_type보존 테스트를 추가했고, 수정 전DeepLinkActivitySourceTest.kt:28assertion failure RED를 확인했다. 이후 data query 수집 목록에putQuery("chat_type")을 추가했고 같은 테스트 PASS를 확인했다.
- 2026-06-18:
- Files:
-
Task 12.3: 제거 endpoint 호출 회귀 방지 테스트 추가
- Files:
- Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRepositoryTest.kt - Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRemovedEndpointSourceTest.kt
- Modify:
- 작업:
- source test로 main DM 채팅 코드에서 아래 문자열이 남아 있지 않은지 확인한다.
/api/v2/user-creator-chat/rooms/{roomId}/events/api/v2/user-creator-chat/rooms/{roomId}/events/disconnect/api/v2/user-creator-chat/rooms/{roomId}/messages/textAccept: text/event-streamEventSource
- ViewModel 테스트에서 텍스트 전송이 REST repository method를 호출하지 않고 WebSocket
SEND_TEXT를 호출함을 검증한다. - lifecycle close 테스트에서 REST disconnect method가 호출되지 않고
LEAVE_ROOM+ close만 호출됨을 검증한다.
- source test로 main DM 채팅 코드에서 아래 문자열이 남아 있지 않은지 확인한다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRemovedEndpointSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRepositoryTest" --max-workers=1 - Expected: 제거 endpoint 문자열 없음, 텍스트 전송 WebSocket 사용, leave/close WebSocket 사용 테스트가 PASS.
- Run:
- 검증 기록:
- 2026-06-18:
DmChatRemovedEndpointSourceTest를 추가해 active DM 채팅 경로(DmChatRoomActivity,DmChatRoomViewModel,DmChatApi,DmChatRepository,DmChatSocketClient)에/events,events/disconnect,messages/text,text/event-stream,EventSource문자열이 남지 않는지 source test로 고정했다. 또한DmChatRoomViewModel의 텍스트 전송은repository.sendSocketText(, lifecycle 종료는repository.sendLeaveRoom(roomId)와repository.closeSocket()을 사용하는지 확인했다../gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRemovedEndpointSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRepositoryTest" --max-workers=1PASS를 확인했다.
- 2026-06-18:
- Files:
-
Task 12.4: 음성 메시지 REST 유지 범위 확인
- Files:
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatApi.kt - Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatRepository.kt - Modify:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRepositoryTest.kt
- Modify:
- 작업:
- 현재 DM 채팅 화면에서 음성 메시지 전송 UI가 없으면 API 추가 없이 DTO의
voiceMessageUrl보존만 유지한다. - DM 음성 전송 UI가 이미 연결되어 있거나 서버 배포 범위에서 필요하면
POST /api/v2/user-creator-chat/rooms/{roomId}/messages/voicemultipart API만 추가한다. - 음성 전송 경로는 WebSocket
SEND_TEXT/SEND_ACKpending 정책과 섞지 않는다.
- 현재 DM 채팅 화면에서 음성 메시지 전송 UI가 없으면 API 추가 없이 DTO의
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRepositoryTest" --max-workers=1 - Expected: 음성 API를 추가한 경우 multipart endpoint만 검증되고, 추가하지 않은 경우 기존 DTO 보존 테스트가 PASS.
- Run:
- 검증 기록:
- 2026-06-18: 현재 DM 채팅 화면에는 음성 전송 UI가 연결되어 있지 않아 신규 multipart API를 추가하지 않았다.
DmChatRemovedEndpointSourceTest에voiceMessageUrlDTO 필드는 보존하되DmChatApi에messages/voice및Multipart가 없고DmChatRepository에sendDmVoiceMessage가 없음을 확인하는 source test를 추가했다../gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRemovedEndpointSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRepositoryTest" --max-workers=1PASS를 확인했다. - 2026-06-18: Phase 12 코드 리뷰 및 통합 검증으로 FCM
chat_type전달, foreground/LiveRoom foreground/Splash/Login 이후 USER_CREATOR DM 라우팅, 제거 endpoint 회귀 방지, 음성 DTO 보존 범위를 재검토했다. deepLink payload/query의chat_type누락 blocker 2건은 위 회귀 테스트로 고정 후 수정했다. 재검증으로./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.fcm.SodaFirebaseMessagingServiceSourceTest" --tests "kr.co.vividnext.sodalive.main.DeepLinkActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.main.MainV2ActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRemovedEndpointSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRepositoryTest" --max-workers=1,./gradlew :app:compileDebugKotlin --max-workers=1,./gradlew :app:ktlintCheck --max-workers=1,git diff --checkPASS를 확인했다. 최초 샌드박스 Gradle 실행은~/.gradlewrapper lock 파일 접근 제한으로 실패해 승인된 Gradle 실행으로 재검증했다.ktlintCheck에서는 기존.editorconfig의disabled_rulesdeprecation warning만 출력됐다. - 2026-06-19: Phase 12 최신 코드 리뷰로 FCM deepLink/non-deepLink payload의
chat_type/room_id보존,DeepLinkActivityforeground USER_CREATOR DM 선분기, URL querychat_type보존,MainV2ActivitySplash/Login 이후 USER_CREATOR DM 라우팅, 제거 endpoint 문자열 회귀 방지, 음성 DTO 보존 범위를 재확인했다. 추가 blocking issue는 발견하지 못했다. 재검증으로./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.fcm.SodaFirebaseMessagingServiceSourceTest" --tests "kr.co.vividnext.sodalive.main.DeepLinkActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.main.MainV2ActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRemovedEndpointSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRepositoryTest" --max-workers=1,./gradlew :app:compileDebugKotlin --max-workers=1,./gradlew :app:ktlintCheck --max-workers=1,git diff --checkPASS를 확인했다. 최초 샌드박스 Gradle 실행은~/.gradlewrapper lock 파일 접근 제한으로 실패해 승인된 Gradle 실행으로 재검증했다. Gradle deprecation warning은 출력됐지만 실패는 없었다.
- 2026-06-18: 현재 DM 채팅 화면에는 음성 전송 UI가 연결되어 있지 않아 신규 multipart API를 추가하지 않았다.
- Files:
Phase 13: WebSocket 전환 최종 검증과 수동 확인
-
Task 13.1: DM 채팅 WebSocket 단위 테스트 실행
- 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.*" --max-workers=1
- Expected:
- DM 채팅 WebSocket 전환 관련 단위 테스트가 모두 PASS.
- 검증 기록:
- 2026-06-19:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*" --max-workers=1실행 결과BUILD SUCCESSFUL을 확인했다.
- 2026-06-19:
- Files:
-
Task 13.2: 푸시/딥링크 라우팅 테스트 실행
- Files:
- Check:
app/src/test/java/kr/co/vividnext/sodalive/fcm/*Test.kt - Check:
app/src/test/java/kr/co/vividnext/sodalive/main/*Test.kt - Check:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/*Test.kt
- Check:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.fcm.*" --tests "kr.co.vividnext.sodalive.main.*" --tests "kr.co.vividnext.sodalive.v2.main.*" --max-workers=1
- Expected:
- USER_CREATOR push routing과 기존 deep link 회귀 테스트가 PASS.
- 검증 기록:
- 2026-06-19:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.fcm.*" --tests "kr.co.vividnext.sodalive.main.*" --tests "kr.co.vividnext.sodalive.v2.main.*" --max-workers=1실행 결과BUILD SUCCESSFUL을 확인했다.
- 2026-06-19:
- Files:
-
Task 13.3: 빌드/스타일/문자열 회귀 확인
- Files:
- Check: Gradle project
- Check:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm
- Run:
./gradlew :app:compileDebugKotlin --max-workers=1./gradlew :app:ktlintCheck --max-workers=1git diff --checkrg "messages/text|events/disconnect|text/event-stream|EventSource" app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm
- Expected:
- Kotlin compile, ktlint, whitespace check PASS.
- 제거 endpoint 문자열은 삭제 검증 테스트나 과거 이력 문서를 제외한 신규 DM main/test 코드에 남지 않는다.
- 검증 기록:
- 2026-06-19:
./gradlew :app:compileDebugKotlin --max-workers=1,./gradlew :app:ktlintCheck --max-workers=1,git diff --check를 실행해 모두 PASS를 확인했다. Gradle 실행에서는 기존 deprecation warning이 출력됐지만 실패는 없었다. - 2026-06-19:
rg "messages/text|events/disconnect|text/event-stream|EventSource" app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm결과 없음으로 active DM main 코드에 제거 endpoint 문자열이 남지 않음을 확인했다. 동일 패턴을 DM test 경로까지 넓히면DmChatRemovedEndpointSourceTest의 금지 문자열 assertion과 과거 SSE 이력 테스트인DmChatEventClientTest의text/event-streamfixture만 매칭됨을 확인했다.
- 2026-06-19:
- Files:
-
Task 13.4: WebSocket 전환 수동 확인
- Files:
- Check:
DmChatRoomActivity - Check:
SodaFirebaseMessagingService - Check:
DeepLinkActivity - Check:
MainV2Activity
- Check:
- 확인 항목:
- 채팅 탭 DM item 클릭 시
OpenRoom호출 후 WebSocket/ws/v2/user-creator-chat연결과JOIN_ROOM전송이 발생한다. JOINED수신 전에는 실시간 수신 상태로 판단하지 않는다.- 상대방 메시지
MESSAGE수신 시 현재 채팅방 메시지 목록에 append된다. - 텍스트 전송 시 REST
/messages/text가 호출되지 않고SEND_TEXT가 전송된다. SEND_ACK수신 시 pending 메시지가 서버messageId,createdAt, 프로필 정보로 확정된다.ERROR또는 timeout 시 pending 메시지가 실패 상태와 재시도 버튼을 표시한다.- 화면 이탈, 앱 background, 로그아웃 시
LEAVE_ROOM전송 후 socket close가 발생하고/events/disconnect는 호출되지 않는다. - 네트워크 오류 후 같은 채팅방 화면에 남아 있으면 reconnect 후
JOIN_ROOM과 누락 메시지 동기화가 수행된다. - 화면 밖에서는 reconnect가 예약/실행되지 않는다.
- heartbeat
PING/PONGtimeout 시 연결 상태가 disconnected로 전환된다. - USER_CREATOR push 터치 시
room_id기준 DM 채팅방에 진입하고 일반 진입과 동일하게OpenRoom후 WebSocket join을 수행한다.
- 채팅 탭 DM item 클릭 시
- 검증 기록:
- 2026-06-19:
DmChatRoomViewModelTest,DmChatSocketClientTest,DmChatRemovedEndpointSourceTest,SodaFirebaseMessagingServiceSourceTest,DeepLinkActivitySourceTest,MainV2ActivitySourceTest로 WebSocket join/send/ack/error/timeout/retry/reconnect/heartbeat, 제거 endpoint 미사용, USER_CREATOR push/deep link 라우팅의 자동 검증 PASS를 확인했다. 단,adb devices결과 연결된 Android 기기가 없어 실제 앱 화면과 실제 서버/WebSocket을 통한 수동 확인은 수행하지 못했으므로 Task 13.4는 미완료로 유지한다. - 2026-06-19: Phase 13 코드 리뷰로
DmChatRoomActivity,DmChatRoomViewModel,DmChatSocketClient,DmChatSocketModels,DmChatRepository,DmChatApi의 WebSocket 연결/해제,JOINED기준 연결 확정,SEND_TEXT/SEND_ACKpending 처리, timeout/retry/reconnect/heartbeat, 제거 REST endpoint 미사용 경로를 재검토했고 blocking issue는 발견하지 못했다. 재검증으로./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*" --max-workers=1,./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.fcm.*" --tests "kr.co.vividnext.sodalive.main.*" --tests "kr.co.vividnext.sodalive.v2.main.*" --max-workers=1,./gradlew :app:compileDebugKotlin --max-workers=1,./gradlew :app:ktlintCheck --max-workers=1,git diff --checkPASS를 확인했다. active DM main 코드의 제거 endpoint 문자열 검색 결과는 없음이며, DM test 경로까지 확장하면 삭제 검증 테스트와 과거 SSE 이력 테스트 fixture만 매칭된다. 최초 Gradle 실행은~/.gradlewrapper lock 파일 접근 제한으로 실패해 승인된 Gradle 실행으로 재검증했고,adb devices는 승인된 실행에서도 연결된 기기가 없어 실제 앱/서버 WebSocket 수동 확인은 미완료로 유지한다.
- 2026-06-19:
- 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이 출력됐지만 실패는 없었다. -
2026-06-11: Phase 6.1~6.5 범위로
AppDI.kt에DmChatApi,DmChatRealtimeClient/DmChatEventClient,DmChatRepository,DmChatRoomViewModelDI 등록을 추가하고,AndroidManifest.xml에.v2.main.chat.dm.DmChatRoomActivity를stateAlwaysHidden|adjustResize로 등록했다.docs/agent-guides/build-test-style.md에는 DM 채팅 단위 테스트 wildcard 실행 예시를 추가했다. -
2026-06-11: Phase 6.4 RED 단계에서
DmChatEventClientTest에 공유OkHttpClient의 60초 read timeout이 SSE 전용 client에서 0으로 제거되어야 한다는 테스트를 추가했고, 기존 구현에서assertEquals(0, sseClient.readTimeoutMillis)실패를 확인했다. 이후DmChatEventClient가 전달받은 client에서readTimeout(0, TimeUnit.MILLISECONDS)를 적용한 전용 client를 생성하도록 수정했고,./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatEventClientTest"PASS를 확인했다. -
2026-06-11: Phase 6.5 RED 단계에서
DmChatRoomViewModelTest에 반복 realtime message callback 후compositeDisposable.size()가 증가하지 않아야 한다는 테스트를 추가했고, 기존AndroidSchedulers.mainThread().scheduleDirectdisposable 누적으로 실패를 확인했다. 이후 realtime callback 전달을 main thread에서는 즉시 실행, background thread에서는Handler(Looper.getMainLooper()).post { ... }로 전달하도록 수정해 완료된 callbackDisposable누적을 제거했고,./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest"PASS를 확인했다. -
2026-06-11: Phase 7.1~7.3 자동 검증으로
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*",./gradlew :app:assembleDebug,./gradlew :app:ktlintCheck를 순차 실행해 모두 PASS를 확인했다.ktlintCheck에서는 기존.editorconfig의disabled_rulesdeprecation warning이 출력됐지만 실패는 없었다. Phase 7.4 수동 화면 확인은 현재 연결된 기기/에뮬레이터 실기 실행 증거가 없어 완료 처리하지 않았다. -
2026-06-11: Phase 6, 7 코드리뷰 개선사항을 후속 Task로 추가했다.
Task 6.6은DmChatRoomViewModel.onCleared()에서 main handler callback, disposable, realtime client 정리를 보장하는 작업이고,Task 6.7은DmChatEventClientTest의getDeclaredField("okHttpClient")reflection 의존을 동작 기반 검증으로 대체하거나 유지 사유를 명확히 남기는 작업이다. 이번 변경은 문서 갱신만 수행했으며 Android 빌드/테스트는 실행하지 않았다. -
2026-06-11: Phase 6.6 RED 단계에서
DmChatRoomViewModelTest에onCleared()정리 테스트를 추가했고, 기존 구현에서onCleared()오버라이드 및mainHandler.removeCallbacksAndMessages(null)부재로 실패를 확인했다. 이후DmChatRoomViewModel.onCleared()에서 예약된 main handler callback 제거,reconnectDisposabledispose, realtime client cancel을 수행하고super.onCleared()로 기존compositeDisposable.dispose()정리를 유지하도록 수정했다. -
2026-06-11: Phase 6.7에서
DmChatEventClientTest의getDeclaredField("okHttpClient")reflection 검증을 제거하고, OkHttp interceptor의chain.readTimeoutMillis()를 실제 SSE 요청 경로에서 관찰하는 동작 기반 검증으로 대체했다. 비동기 요청 실행을 기다리기 위해CountDownLatch를 사용했고, 공유 client의 60초 read timeout은 유지되며 SSE 요청 경로에서는 0ms read timeout이 적용됨을 확인했다. -
2026-06-11: Phase 6.6~6.7 검증으로
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatEventClientTest" --max-workers=1,./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,git diff --checkPASS를 확인했다. 최초 병렬 Gradle 실행에서는 Kotlin incremental cache 동시 접근으로 timeout/daemon fallback이 발생해 순차 실행으로 재검증했다.ktlintCheck에서는 기존.editorconfig의disabled_rulesdeprecation warning이 출력됐지만 실패는 없었다. -
2026-06-11: Phase 6.6 리뷰 후속으로
onCleared()테스트를 source 문자열 검증에서 런타임 동작 검증으로 보강했다. 예약된 realtime 재연결이onCleared()후 실행되지 않는지, background thread에서 예약된mainHandler.post { action() }callback이onCleared()후 상태를 갱신하지 않는지 확인하도록 수정했다. Phase 6.7 테스트에는requestLatch.await(2, TimeUnit.SECONDS)반환값 assertion을 추가해 timeout 검증 의도를 명확히 했다. 이후./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1,./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatEventClientTest" --max-workers=1,./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*" --max-workers=1,./gradlew :app:ktlintCheck --max-workers=1PASS를 확인했다. -
2026-06-18:
docs/20260610_DM_채팅화면/prd.md의 WebSocket 전환 요구사항을 기준으로plan-task.md를 보강했다. 기존 Phase 18의 SSE 구현 이력은 보존하고, Phase 913에 WebSocket 모델/클라이언트, Repository/DI 전환,JOIN_ROOM/JOINED,MESSAGE,SEND_TEXT/SEND_ACK,LEAVE_ROOM/close, reconnect/heartbeat/token refresh, USER_CREATOR push routing, 제거 endpoint 회귀 검증, 최종 수동 확인 작업을 추가했다. -
2026-06-18: 현재 코드 기준으로
DmChatApi의/messages/text,/events/disconnect,DmChatEventClient,DmChatRoomViewModel.sendText()REST 전송,disconnectRealtime()SSE 해제 경로가 남아 있음을 확인했고, 이를 Phase 9~13에서 제거/교체할 대상으로 문서화했다. 이번 단계는 계획 문서 수정만 수행했으며 Android 구현, 빌드, 테스트는 실행하지 않았다. -
2026-06-18: Phase 10의 Task 10.1~10.3 코드 리뷰 및 검증을 수행했다.
DmChatRoomViewModel의JOINED기준 연결 확정, WebSocketMESSAGE병합,requestId단위 pending/SEND_ACK/ERROR/timeout/retry 처리를 확인했고 blocking issue는 발견하지 못했다../gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1,./gradlew :app:compileDebugKotlin --max-workers=1,./gradlew :app:ktlintCheck --max-workers=1,git diff --checkPASS를 확인했다.ktlintCheck와 Gradle 실행에서는 기존 Gradle deprecation warning만 출력됐고 실패는 없었다. Task 10.4의 MESSAGE/SEND_ACK race와 Task 10.5 회귀 테스트 정리는 후속 미완료 범위로 유지한다.