31 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로 최신 누락 가능 메시지를 동기화한다.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:
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:
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 환경에서 빌드/테스트 재확인이 필요하다.