Files
sodalive-android/docs/20260610_DM_채팅화면/plan-task.md

24 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 호출 후 반환된 roomIdOpenRoom을 호출한다.
    • 둘 다 유효하지 않으면 Activity를 종료한다.
  • REST API는 기존 v2 채팅 탭과 동일하게 Retrofit + RxJava3 + ApiResponse<T> 패턴을 사용한다.
  • SSE는 현재 저장소에 재사용 패턴이 없으므로 별도 라이브러리 추가 없이 기존 OkHttpClientnewCall()과 streaming ResponseBody를 사용하는 DmChatEventClient를 추가한다.
  • SSE 연결/해제는 Activity foreground 범위에서 처리한다.
    • onStart: OpenRoom 완료 후 연결 가능 상태면 SSE 연결을 시작한다.
    • onStop: SSE call을 cancel하고 DisconnectRealtime API를 비동기로 호출한다.
    • onDestroy: listener 참조와 disposable을 정리한다.
  • Last-Event-ID replay는 기대하지 않고, SSE 재연결 후 GetMessages로 최신 누락 가능 메시지를 동기화한다.
  • VOICE 메시지는 DTO에 보존하되 이번 UI 목록에는 표시하지 않는다.
  • 전송은 낙관적 UI를 적용한다.
    • 전송 직후 local pending 메시지를 추가한다.
    • 성공 시 서버 응답 메시지로 교체한다.
    • 실패 시 실패 상태와 재시도 버튼을 표시한다.

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.kt
    • user-creator-chat REST 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 처리를 담당한다.
  • 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()로 이동한다.
  • Modify: app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt
    • DmChatApi, DmChatRepository, DmChatEventClient, DmChatRoomViewModel DI를 추가한다.
  • Modify: app/src/main/AndroidManifest.xml
    • DmChatRoomActivity를 등록한다.
  • 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 성공 후 반환된 roomIdOpenRoom을 호출한다.
  • 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
    • 작업:
      • CreateDmChatRoomRequest, CreateDmChatRoomResponse를 추가한다.
      • DmChatRoomOpenResponse, DmChatMessagesPageResponse, DmChatMessageResponse, SendDmTextMessageRequest, SendDmChatMessageResponse를 추가한다.
      • Retrofit endpoint를 아래 계약으로 추가한다.
        • POST /api/v2/user-creator-chat/rooms/create
        • GET /api/v2/user-creator-chat/rooms/{roomId}/open
        • GET /api/v2/user-creator-chat/rooms/{roomId}/messages
        • POST /api/v2/user-creator-chat/rooms/{roomId}/messages/text
        • POST /api/v2/user-creator-chat/rooms/{roomId}/events/disconnect
    • 검증:
      • DTO 필드명은 PRD의 서버 필드명을 @SerializedName으로 그대로 보존한다.
      • REST 반환 타입은 Single<ApiResponse<...>>를 사용한다.
  • 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
    • 작업:
      • DmChatMessageUiItemmessageId, localId, mine, textMessage, senderNickname, senderProfileImageUrl, createdAt, status를 둔다.
      • DmChatMessageStatusSENDING, SENT, FAILED로 정의한다.
      • messageType은 서버 계약상 TEXT/VOICE 대문자이나, UI 매핑에서는 오입력 방지를 위해 대소문자를 무시해 TEXT를 판정한다.
      • messageTypeTEXT가 아니거나 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.

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
    • 작업:
      • 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를 사용한다.
  • 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
    • 작업:
      • 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.

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
    • 작업:
      • enter(roomId, creatorId)를 추가한다.
      • roomId > 0이면 openRoom을 호출한다.
      • roomId <= 0 && creatorId > 0이면 createOrGetRoomopenRoom을 호출한다.
      • 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.
  • 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
    • 작업:
      • 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.
  • 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
    • 작업:
      • sendText(text)는 trim 후 blank면 종료한다.
      • 전송 중 같은 local message에 대한 중복 요청을 막는다.
      • 전송 직후 SENDING local 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.

Phase 4: UI 레이아웃과 Adapter 구현

  • Task 4.1: DM 채팅방 layout 생성

    • Files:
      • Create: app/src/main/res/layout/activity_dm_chat_room.xml
    • 작업:
      • activity_chat_room.xml 구조를 참고해 배경 이미지, dim, header, rv_messages, input_container를 구성한다.
      • header에는 iv_back, iv_profile, tv_name만 둔다.
      • rv_messages top 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: 결과 없음.
  • 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
    • 작업:
      • 내 메시지는 오른쪽 정렬, 상대 메시지는 왼쪽 정렬로 구성한다.
      • 내 실패 메시지에는 재시도 버튼 또는 클릭 가능한 실패 상태 view를 둔다.
      • DiffUtil과 stable id는 messageId 우선, local pending 메시지는 localId 기준으로 처리한다.
      • 상대 프로필 이미지는 기존 placeholder 정책에 맞춰 ic_placeholder_profile을 사용한다.
    • 검증:
      • 긴 텍스트가 item 너비 안에서 줄바꿈되는지 XML maxWidth 또는 constraint를 확인한다.

Phase 5: Activity 구현과 화면 연결

  • Task 5.1: DmChatRoomActivity 생성

    • Files:
      • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt
    • 작업:
      • 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 관련 로직을 가져오지 않는다.
  • 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
    • 작업:
      • onStart에서 OpenRoom 완료 상태이면 viewModel.connectRealtime()를 호출한다.
      • onStop에서 viewModel.disconnectRealtime()를 호출한다.
      • disconnect 요청 중복을 막는 isDisconnecting 상태를 둔다.
      • disconnect 실패는 화면 종료를 막지 않고 toast를 과하게 노출하지 않는다.
    • 검증:
      • ViewModel 테스트에서 disconnect 중복 방지와 cancel 호출 여부를 검증한다.
  • Task 5.3: 채팅 탭 DM item 클릭 연결

    • Files:
      • Modify: app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt
    • 작업:
      • ChatRoomType.AI는 기존 ChatRoomActivity로 이동한다.
      • ChatRoomType.DMDmChatRoomActivity.newIntentByRoomId(requireContext(), item.roomId)로 이동한다.
    • 검증:
      • AI item 클릭 동작은 기존과 동일하게 유지한다.

Phase 6: DI, Manifest, 문서 갱신

  • Task 6.1: Koin DI 등록

    • Files:
      • Modify: app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt
    • 작업:
      • DmChatApi API builder 등록을 추가한다.
      • DmChatEventClient는 기존 OkHttpClient, Gson, BuildConfig.BASE_URL 기반으로 생성되도록 등록한다.
      • DmChatRepository, DmChatRoomViewModel 등록을 추가한다.
    • 검증:
      • import 추가 외 기존 DI 등록 순서를 불필요하게 재정렬하지 않는다.
  • Task 6.2: Manifest 등록

    • Files:
      • Modify: app/src/main/AndroidManifest.xml
    • 작업:
      • .v2.main.chat.dm.DmChatRoomActivity를 application 하위 activity 목록에 추가한다.
      • 키보드 UX는 기존 채팅방과 유사하게 android:windowSoftInputMode="stateAlwaysHidden|adjustResize"를 우선 적용한다.
    • 검증:
      • activity는 exported를 명시하지 않는 기존 내부 Activity 패턴을 따른다.
  • Task 6.3: 테스트 실행 가이드 갱신

    • Files:
      • Modify: docs/agent-guides/build-test-style.md
    • 작업:
      • DM 채팅 테스트 단일 실행 예시를 추가한다.
        • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*"
    • 검증:
      • 문서 변경은 신규 테스트 명령 예시 추가로만 제한한다.

Phase 7: 최종 검증과 기록

  • Task 7.1: 단위 테스트 실행

    • Files:
      • Check: app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/*Test.kt
    • Run:
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.*"
    • Expected:
      • 신규 DM 채팅 단위 테스트가 모두 PASS.
  • Task 7.2: 앱 빌드 확인

    • Files:
      • Check: Gradle project
    • Run:
      • ./gradlew :app:assembleDebug
    • Expected:
      • Debug APK 빌드 PASS.
  • Task 7.3: 린트/스타일 확인

    • Files:
      • Check: Kotlin/XML 변경 파일
    • Run:
      • ./gradlew :app:ktlintCheck
    • Expected:
      • ktlint PASS.
  • Task 7.4: 수동 확인

    • Files:
      • Check: DmChatRoomActivity
    • 확인 항목:
      • DM item 클릭 시 DM 채팅방 화면으로 이동한다.
      • header에 뒤로가기, 상대 프로필, 상대 닉네임만 표시된다.
      • 메시지 목록은 header 바로 아래에서 시작한다.
      • blank 입력은 전송되지 않는다.
      • 텍스트 전송 실패 시 실패 상태와 재시도 버튼이 표시된다.
      • 화면 이탈 또는 앱 background 전환 시 disconnect API가 호출된다.
      • SSE 연결 실패가 앱 crash로 이어지지 않는다.

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: rgEventSource, 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"를 실행해 DmChatMessageResponsedm.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에서는 기존 .editorconfigdisabled_rules deprecation warning이 출력됐지만 실패는 없었다.
  • 2026-06-10: DmChatMappers.ktDmChatMapperTest.kt를 보강해 messageType 대소문자 무시 TEXT 매핑, TEXT 외 타입 및 textMessage == null 미매핑, createdAt 동일 시 messageId 오름차순, 중복 messageId 선도착 우선 정책을 명시했다. ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatMapperTest"./gradlew :app:ktlintCheck PASS를 확인했다.
  • 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 클라이언트는 OkHttp Call.enqueue()와 streaming ResponseBody를 사용하고, connected/message event 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에서는 기존 .editorconfigdisabled_rules deprecation 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:ktlintCheck PASS를 재확인했다.
  • 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 --check PASS를 확인했다.