docs(dm): WebSocket 전환 요구를 보강한다
This commit is contained in:
@@ -11,8 +11,10 @@
|
||||
- `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 전환 완료 후 사용하지 않는다.
|
||||
- Phase 1~13은 과거 구현/검증 이력으로만 남긴다. 최신 raw WebSocket 계약 보정의 active 작업 범위는 Phase 14부터 진행한다.
|
||||
- 과거 이력에 남은 SSE/EventSource 내용은 이번 구현에서 유지하거나 재사용하지 않는다.
|
||||
- WebSocket은 `OkHttpClient.newWebSocket()` 기반 전용 `DmChatSocketClient`를 사용한다.
|
||||
- 기존 `DmChatEventClient`와 제거된 `/events` 계열 구현은 active main source에서 삭제한다.
|
||||
- WebSocket 연결/해제는 Activity foreground 범위와 로그아웃 흐름에서 처리한다.
|
||||
- `onStart`: `OpenRoom` 완료 후 연결 가능 상태면 WebSocket 연결을 시작하고 `JOIN_ROOM`을 보낸다.
|
||||
- `JOINED`: 실시간 수신 가능 상태로 판단한다.
|
||||
@@ -22,7 +24,9 @@
|
||||
- 네트워크 오류로 WebSocket이 실패하면 화면이 foreground에 있고 채팅방이 활성 상태인 경우에만 재연결을 시도한다.
|
||||
- 재연결 성공 후 `JOIN_ROOM`을 다시 보내고 `GetMessages`로 누락 가능 메시지를 보정한다.
|
||||
- 화면 이탈 또는 background 전환 시 예약된 재연결 시도는 취소한다.
|
||||
- `VOICE` 메시지는 DTO에 보존하되 이번 UI 목록에는 표시하지 않는다.
|
||||
- `VOICE` 메시지는 DTO/API 계약에 보존하되 이번 UI 목록에는 표시하지 않고, 화면에서는 텍스트 메시지만 보낼 수 있게 한다.
|
||||
- 상대방 메시지는 왼쪽 정렬, 내가 보낸 메시지는 오른쪽 정렬로 고정한다.
|
||||
- 채팅 버블은 현재 구현된 DM 채팅 버블을 그대로 사용한다.
|
||||
- 전송은 낙관적 UI를 적용한다.
|
||||
- 전송 직후 local pending 메시지를 추가한다.
|
||||
- WebSocket `SEND_ACK` 수신 시 `requestId`로 pending 메시지를 찾아 서버 `messageId`, `createdAt`, 프로필 정보로 확정한다.
|
||||
@@ -32,6 +36,18 @@
|
||||
- 같은 UI item 재시도는 새 `requestId`를 발급하되 기존 local item을 유지한다.
|
||||
- Phase 3 ViewModel의 pagination/reconnect 동기화 실패는 화면 종료나 Error 화면 전환 없이 기존 메시지 상태를 유지하고 내부 loading 상태만 복구한다.
|
||||
- 사용자 노출 toast/retry UI는 Phase 5 Activity 연결 시 필요하면 별도 처리한다.
|
||||
- 2026-07-10 최신 서버 계약은 Phase 9~13에서 구현한 WebSocket envelope 일부를 대체한다.
|
||||
- 기존 완료 이력과 검증 기록은 보존한다.
|
||||
- 최신 계약 보정은 Phase 14에서 미완료 작업으로 진행한다.
|
||||
- client send JSON은 root `type`, `requestId`, `roomId`, `payload`를 사용한다.
|
||||
- `JOIN_ROOM`, `SEND_TEXT`, `PING`, `LEAVE_ROOM`은 모두 UUID `requestId`와 실제 `roomId > 0`을 root에 포함한다.
|
||||
- `SEND_TEXT` payload에는 `textMessage`만 둔다.
|
||||
- server `SEND_ACK`/`MESSAGE` payload는 `{message: ...}` wrapper 없는 `MessageItem`이다.
|
||||
- `ERROR`는 root `code`/`message`가 아니라 `payload.messageKey`를 파싱한다.
|
||||
- `JOINED` 이후에만 `SEND_TEXT`/`PING`/`LEAVE_ROOM`을 허용한다.
|
||||
- `PING`은 `JOINED` 상태에서 30초 주기로 보낸다.
|
||||
- `SEND_ACK` timeout 기준은 15초로 보정한다.
|
||||
- 음성 메시지는 구현하더라도 UI에 연결하지 않는다.
|
||||
|
||||
## 3. 파일 구조
|
||||
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt`
|
||||
@@ -44,12 +60,18 @@
|
||||
- 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 전환 완료 시 신규 경로에서 사용하지 않으며 제거 또는 미사용 상태로 둔다.
|
||||
- Delete: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatEventClient.kt`
|
||||
- 제거된 `/events` 기반 legacy realtime 구현은 active main source에서 제거한다.
|
||||
- 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 처리를 담당한다.
|
||||
- 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 root envelope, payload, `requestId` 기반 `SEND_ACK`, `ERROR`, `MESSAGE`, `PONG` 모델을 정의한다.
|
||||
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatSocketModels.kt`
|
||||
- Phase 14에서 최신 raw WebSocket 계약에 맞게 root `requestId`/`roomId`, direct `MessageItem` payload, `payload.messageKey`를 보정한다.
|
||||
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatSocketClient.kt`
|
||||
- Phase 14에서 client send raw JSON, UUID `requestId`, root `roomId`, raw JSON logging, `onOpen`/`JOINED` 상태 분리를 보정한다.
|
||||
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatApi.kt`
|
||||
- Phase 14에서 음성 메시지 multipart endpoint가 필요한 경우 `voiceMessageFile` file part와 `request` string part를 추가한다.
|
||||
- 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.kt`
|
||||
@@ -90,12 +112,22 @@
|
||||
- `creatorId` 기반 진입은 `CreateOrGetRoom` 성공 후 반환된 `roomId`로 `OpenRoom`을 호출한다.
|
||||
- OpenRoom 메시지는 오래된 순서에서 최신 순서로 표시된다.
|
||||
- 메시지 병합은 `messageId` 기준으로 중복을 제거한다.
|
||||
- 상대방 메시지는 왼쪽 정렬, 내가 보낸 메시지는 오른쪽 정렬로 표시한다.
|
||||
- 채팅 버블은 현재 구현된 DM 채팅 버블을 그대로 사용한다.
|
||||
- 상단 스크롤 시 `hasMore=true`, `nextCursor != null`, `isLoading=false` 조건에서만 과거 메시지를 조회한다.
|
||||
- 텍스트 전송은 blank 입력을 무시하고, WebSocket `SEND_TEXT`와 `requestId` pending 매칭을 사용한다.
|
||||
- DM 채팅방 입력 UI는 텍스트 전송만 제공하고 음성 녹음/전송/재생 컨트롤을 노출하지 않는다.
|
||||
- WebSocket client send raw JSON은 최신 계약의 root `type`/`requestId`/`roomId`/`payload` 구조와 일치한다.
|
||||
- WebSocket server receive parser는 `SEND_ACK`/`MESSAGE`의 direct `MessageItem` payload와 `ERROR payload.messageKey`를 처리한다.
|
||||
- `roomId`가 0/null이면 `JOIN_ROOM`을 보내지 않는다.
|
||||
- `JOINED` 전에는 `SEND_TEXT`, `PING`, `LEAVE_ROOM`을 보내지 않는다.
|
||||
- `PING`은 30초 주기로 UUID `requestId`와 실제 `roomId`를 포함해 보낸다.
|
||||
- `SEND_ACK` timeout은 15초 기준으로 실패 처리한다.
|
||||
- WebSocket send/receive raw JSON은 토큰 없이 로그로 남긴다.
|
||||
- 전송 실패 메시지는 `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 경로를 유지한다.
|
||||
- 음성 메시지 multipart REST 계약은 보존하되 이번 화면 UI에는 연결하지 않는다.
|
||||
- DM 화면에는 `character_type_badge`, `ll_can_badge`, `iv_more`, `notice_container`가 없다.
|
||||
- `ChatRoomActivity` 기존 동작은 변경하지 않는다.
|
||||
|
||||
@@ -921,6 +953,303 @@
|
||||
- 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_ACK` pending 처리, 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 --check` PASS를 확인했다. active DM main 코드의 제거 endpoint 문자열 검색 결과는 없음이며, DM test 경로까지 확장하면 삭제 검증 테스트와 과거 SSE 이력 테스트 fixture만 매칭된다. 최초 Gradle 실행은 `~/.gradle` wrapper lock 파일 접근 제한으로 실패해 승인된 Gradle 실행으로 재검증했고, `adb devices`는 승인된 실행에서도 연결된 기기가 없어 실제 앱/서버 WebSocket 수동 확인은 미완료로 유지한다.
|
||||
|
||||
### Phase 14: 최신 raw WebSocket 계약 보정
|
||||
|
||||
> 2026-07-10 추가 계약 변경: 서버는 STOMP/SockJS가 아니라 raw WebSocket만 사용하며, WebSocket JSON은 root `type`, `requestId`, `roomId`, `payload` 구조를 따른다. Phase 9~13의 완료 이력은 보존하되, 실제 구현 전 이 Phase의 테스트를 RED로 먼저 고정한 뒤 최소 수정한다.
|
||||
|
||||
- [x] **Task 14.1: 현재 구현과 최신 계약 차이 문서화**
|
||||
- Files:
|
||||
- Check: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatSocketModels.kt`
|
||||
- Check: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatSocketClient.kt`
|
||||
- Check: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatRepository.kt`
|
||||
- Check: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModel.kt`
|
||||
- Modify: `docs/20260610_DM_채팅화면/prd.md`
|
||||
- Modify: `docs/20260610_DM_채팅화면/plan-task.md`
|
||||
- 확인 결과:
|
||||
- `DmChatSocketClient`의 outbound envelope는 root `requestId`/`roomId`가 없고, `JOIN_ROOM`/`LEAVE_ROOM`은 payload에 `roomId`를 넣는다.
|
||||
- `SEND_TEXT`는 payload에 `roomId`/`requestId`/`textMessage`를 함께 넣지만 최신 계약은 root `requestId`/`roomId`, payload `{textMessage}`다.
|
||||
- `PING`은 empty payload만 보내며 root `requestId`/`roomId`가 없다.
|
||||
- `DmChatSocketParser`는 `MESSAGE`/`SEND_ACK` payload를 `{requestId, message}` wrapper로 기대하지만 최신 계약은 root `requestId`, payload `MessageItem`이다.
|
||||
- `ERROR` parser는 `code`/`message`를 보존하지만 최신 계약은 `payload.messageKey`를 읽어야 한다.
|
||||
- `SEND_ACK_TIMEOUT_MILLIS`는 10초이며 최신 예시는 15초다.
|
||||
- `nextRequestId()`는 `request-1` 형식이고 최신 계약은 UUID다.
|
||||
- `AppDI`/Repository 경로는 `DmChatSocketClient`를 사용하지만, legacy realtime `DmChatEventClient.kt`와 `/api/v2/user-creator-chat/rooms/{roomId}/events` URL 정의가 main source에 남아 있다.
|
||||
- REST create/open/messages와 제거된 `/events/disconnect`, `/messages/text` 미사용 상태는 최신 계약과 큰 충돌이 없다.
|
||||
- 검증 기록:
|
||||
- 2026-07-10: 위 파일을 확인해 최신 계약 차이를 문서화했다. 이번 단계는 문서 보완만 수행했으며 Android 구현, 빌드, 테스트는 실행하지 않았다.
|
||||
|
||||
- [x] **Task 14.2: WebSocket raw envelope 모델/파서 RED 테스트 추가**
|
||||
- Files:
|
||||
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatSocketParserTest.kt`
|
||||
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatSocketClientTest.kt`
|
||||
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatSocketModels.kt`
|
||||
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatSocketClient.kt`
|
||||
- 작업:
|
||||
- client send JSON이 아래 형태와 정확히 일치하는지 테스트한다.
|
||||
- `{"type":"JOIN_ROOM","requestId":"uuid","roomId":10,"payload":{}}`
|
||||
- `{"type":"SEND_TEXT","requestId":"uuid","roomId":10,"payload":{"textMessage":"hello"}}`
|
||||
- `{"type":"PING","requestId":"uuid","roomId":10,"payload":{}}`
|
||||
- `{"type":"LEAVE_ROOM","requestId":"uuid","roomId":10,"payload":{}}`
|
||||
- server receive JSON이 아래 형태로 파싱되는지 테스트한다.
|
||||
- `JOINED`: root `requestId`, root `roomId`, empty payload
|
||||
- `SEND_ACK`: root `requestId`, root `roomId`, payload `MessageItem`
|
||||
- `MESSAGE`: root `requestId=null`, root `roomId`, payload `MessageItem`
|
||||
- `PONG`: root `requestId`, root `roomId`, empty payload
|
||||
- `ERROR`: root `roomId`, payload `messageKey`
|
||||
- `{message: ...}` wrapper 전제와 payload 내부 `requestId`/`roomId` 전제를 제거한다.
|
||||
- 검증:
|
||||
- Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketParserTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketClientTest" --max-workers=1`
|
||||
- Expected: 구현 전에는 기존 envelope 차이로 RED, 구현 후 최신 raw JSON 계약 PASS.
|
||||
- 검증 기록:
|
||||
- 2026-07-10: `DmChatSocketParserTest`와 `DmChatSocketClientTest`를 최신 raw envelope 기준으로 갱신하고, `DmChatSocketModels.kt`/`DmChatSocketClient.kt`를 root `type`/`requestId`/`roomId`/`payload` 구조로 보정했다. `SEND_ACK`/`MESSAGE`는 direct `MessageItem` payload로 파싱하고 `ERROR`는 `payload.messageKey`를 읽도록 변경했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketParserTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketClientTest" --max-workers=1` PASS를 확인했다.
|
||||
|
||||
- [x] **Task 14.3: ViewModel WebSocket 상태와 전송 gate 보정**
|
||||
- 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/DmChatRepository.kt`
|
||||
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt`
|
||||
- 작업:
|
||||
- 상태를 `DISCONNECTED`, `CONNECTING`, `SOCKET_OPEN`, `JOINING`, `JOINED` 또는 동등하게 테스트 가능한 구조로 분리한다.
|
||||
- WebSocket `onOpen`은 `SOCKET_OPEN`으로만 보고 UI 실시간 연결 상태로 확정하지 않는다.
|
||||
- `OpenRoom` 성공 전 또는 `roomId <= 0`에서는 `JOIN_ROOM`을 보내지 않는다.
|
||||
- `JOINED` 이후에만 `SEND_TEXT`, `PING`, `LEAVE_ROOM`을 허용한다.
|
||||
- `requestId`는 UUID로 생성한다.
|
||||
- `SEND_ACK` timeout은 15초로 보정한다.
|
||||
- `ERROR payload.messageKey`가 pending `requestId`와 연결 가능한 경우 해당 pending 메시지를 실패 처리하고, 세션 오류면 화면 오류/재연결 정책으로 넘긴다.
|
||||
- `MESSAGE`/`SEND_ACK` root `roomId`가 현재 채팅방과 다르면 무시한다.
|
||||
- 검증:
|
||||
- Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1`
|
||||
- Expected: 0 room join 금지, `JOINED` 전 전송 금지, UUID requestId, 15초 timeout, root room filtering, `payload.messageKey` error 처리 테스트가 PASS.
|
||||
- 검증 기록:
|
||||
- 2026-07-10: `DmChatRoomViewModel.kt`와 `DmChatRepository.kt`를 보정해 `JOINED` 이후에만 `SEND_TEXT`/`PING`/`LEAVE_ROOM`을 허용하고, `requestId`를 UUID로 생성하며, `SEND_ACK` timeout을 15초로 변경했다. 다른 `roomId`의 `MESSAGE`/`SEND_ACK` 무시와 `payload.messageKey` 오류 처리를 테스트로 고정했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1` PASS를 확인했다.
|
||||
|
||||
- [x] **Task 14.4: WebSocket raw JSON logging 추가**
|
||||
- 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/DmChatSocketClient.kt`
|
||||
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatSocketClientTest.kt`
|
||||
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivitySourceTest.kt`
|
||||
- 작업:
|
||||
- WebSocket send 직전 raw JSON을 로그로 남긴다.
|
||||
- `onMessage` 수신 raw JSON을 parser 적용 전에 로그로 남긴다.
|
||||
- Authorization token과 bearer header는 로그에 남기지 않는다.
|
||||
- logging은 테스트 가능한 logger seam 또는 최소 source test로 검증한다.
|
||||
- 검증:
|
||||
- Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketClientTest" --max-workers=1`
|
||||
- Expected: send/receive raw JSON logging 경로가 존재하고 token 값은 로그에 포함되지 않는다.
|
||||
- 검증 기록:
|
||||
- 2026-07-10: `DmChatSocketClient`에 테스트 가능한 `rawJsonLogger` seam을 추가해 send 직전 JSON과 `onMessage` 수신 raw JSON을 기록하도록 했다. handshake token/header 문자열은 raw JSON logger에 전달하지 않도록 테스트로 확인했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketClientTest" --max-workers=1` PASS를 확인했다.
|
||||
- 2026-07-10: review-work에서 production `AppDI` 생성 경로가 `rawJsonLogger` 기본 no-op을 사용해 실제 앱 raw JSON logging 요구가 미충족이라는 blocker를 발견했다. `AppDI.kt`에서 `DmChatSocketClient` 생성 시 `Logger.d("DM WebSocket raw JSON: $rawJson")` logger를 명시 주입하고, `DmChatRoomActivitySourceTest`에 기본 no-op 생성 회귀 방지 검증을 추가했다. Authorization/Bearer header 문자열은 logger 주입 문자열에 포함하지 않는다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --max-workers=1` PASS를 확인했다.
|
||||
|
||||
- [x] **Task 14.5: 음성 메시지 multipart 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/DmChatModels.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`
|
||||
- 작업:
|
||||
- `POST /api/v2/user-creator-chat/rooms/{roomId}/messages/voice` multipart endpoint를 추가한다.
|
||||
- file part 이름은 `voiceMessageFile`로 둔다.
|
||||
- string part 이름은 `request`로 두고 값은 JSON 문자열 `{"recipientId": null}` 형식을 사용한다.
|
||||
- response data 모델은 `message: MessageItem`, `deliveredRealtime: Boolean`, `pushSent: Boolean`을 보존한다.
|
||||
- API/Repository 계약까지만 추가하고 화면 UI에는 연결하지 않는다.
|
||||
- DM 채팅방 입력 영역에는 텍스트 입력/전송만 남긴다.
|
||||
- 검증:
|
||||
- Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRepositoryTest" --max-workers=1`
|
||||
- Expected: multipart part 이름, `request` JSON 문자열, response DTO 필드 보존 테스트가 PASS하고, 음성 UI 연결은 없다.
|
||||
- 검증 기록:
|
||||
- 2026-07-10: `DmChatApi.kt`, `DmChatModels.kt`, `DmChatRepository.kt`에 `POST /api/v2/user-creator-chat/rooms/{roomId}/messages/voice` multipart 계약을 추가했다. file part는 `voiceMessageFile`, string part는 `request` JSON으로 전송하고, 응답 DTO는 `message`/`deliveredRealtime`/`pushSent`를 보존한다. UI에는 연결하지 않았다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRepositoryTest" --max-workers=1` PASS를 확인했다.
|
||||
|
||||
- [x] **Task 14.6: 제거 endpoint와 STOMP/SockJS 회귀 방지 갱신**
|
||||
- Files:
|
||||
- Delete: `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/DmChatRemovedEndpointSourceTest.kt`
|
||||
- 작업:
|
||||
- legacy realtime `DmChatEventClient.kt`는 active main source에서 제거한다.
|
||||
- legacy realtime parser/client 이력 테스트를 유지해야 하면 production source가 아닌 test fixture로 옮긴다.
|
||||
- active DM main 코드에 아래 문자열이 남지 않는지 source test를 갱신한다.
|
||||
- `/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/text`
|
||||
- `CONNECT\n`, `SUBSCRIBE`, STOMP `SEND`, `SockJS`
|
||||
- 테스트 fixture나 문서 이력에 있는 문자열은 active source 검사 범위에서 제외한다.
|
||||
- 검증:
|
||||
- Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRemovedEndpointSourceTest" --max-workers=1`
|
||||
- Expected: 제거 endpoint와 STOMP/SockJS frame 문자열이 active DM main 코드에 없음.
|
||||
- 검증 기록:
|
||||
- 2026-07-10: active main source의 legacy realtime `DmChatEventClient.kt`를 제거하고 관련 legacy test를 삭제했다. `DmChatRemovedEndpointSourceTest`를 갱신해 active DM main 코드에 `/events`, `/events/disconnect`, `/messages/text`, STOMP/SockJS frame 문자열이 남지 않음을 확인했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRemovedEndpointSourceTest" --max-workers=1` PASS를 확인했다.
|
||||
|
||||
- [x] **Task 14.7: 채팅 메시지 UI 정렬과 텍스트 전송 UI 보정**
|
||||
- Files:
|
||||
- Modify: `app/src/main/res/layout/item_dm_chat_my_message.xml`
|
||||
- Modify: `app/src/main/res/layout/item_dm_chat_opponent_message.xml`
|
||||
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/ui/DmChatMessageAdapter.kt`
|
||||
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt`
|
||||
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivitySourceTest.kt`
|
||||
- 작업:
|
||||
- 상대방 메시지는 왼쪽 정렬로 고정한다.
|
||||
- 내가 보낸 메시지는 오른쪽 정렬로 고정한다.
|
||||
- 현재 구현된 DM 채팅 버블 drawable/style을 유지하고 신규 버블 디자인을 만들지 않는다.
|
||||
- DM 채팅방 입력 UI는 텍스트 메시지 전송만 제공한다.
|
||||
- 음성 녹음, 음성 전송, 음성 재생 관련 view/callback이 DM 채팅방 UI에 노출되지 않도록 확인한다.
|
||||
- 긴 텍스트는 현재 버블 폭 정책 안에서 줄바꿈되고, 말풍선이 화면 밖으로 밀리지 않도록 확인한다.
|
||||
- 검증:
|
||||
- Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --max-workers=1`
|
||||
- Expected: 상대방 왼쪽/내 메시지 오른쪽 정렬, 기존 버블 사용, 텍스트 전송 UI만 노출, 음성 UI 미노출 source/layout 검증이 PASS.
|
||||
- 검증 기록:
|
||||
- 2026-07-10: 기존 DM 채팅 버블 drawable/style은 유지하고, source/layout 검증으로 상대방 메시지 왼쪽 정렬, 내 메시지 오른쪽 정렬, 텍스트 전송 UI만 노출, 음성 UI 미노출을 확인했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --max-workers=1` PASS를 확인했다.
|
||||
|
||||
- [ ] **Task 14.8: 최신 계약 통합 검증과 실제 서버 수동 확인**
|
||||
- Files:
|
||||
- Check: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm`
|
||||
- Check: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm`
|
||||
- Run:
|
||||
- `./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 --check`
|
||||
- 수동 확인:
|
||||
- 실제 서버에서 WebSocket handshake `101` 후 response body unreadable 표시가 정상으로 간주되는지 확인한다.
|
||||
- `JOIN_ROOM` raw JSON의 root `roomId`가 실제 room id이고 0이 아님을 확인한다.
|
||||
- 서버가 `JOINED`, `SEND_ACK`, `MESSAGE`, `PONG`, `ERROR payload.messageKey`를 최신 계약대로 내려주는지 log로 확인한다.
|
||||
- `SEND_TEXT` 전송 시 REST `/messages/text`가 호출되지 않음을 proxy/log로 확인한다.
|
||||
- 화면에서 텍스트 메시지만 전송할 수 있고 음성 UI가 노출되지 않는지 확인한다.
|
||||
- 상대방 메시지는 왼쪽, 내 메시지는 오른쪽에 현재 구현된 버블로 표시되는지 확인한다.
|
||||
- `voiceon://chat/{roomId}` 또는 `voiceon-test://chat/{roomId}` push/deep link가 DM 채팅방으로 진입하고 일반 진입과 동일하게 `OpenRoom` 후 `JOIN_ROOM`을 수행하는지 확인한다.
|
||||
- Expected:
|
||||
- 자동 검증 PASS.
|
||||
- 실제 서버에서 raw WebSocket 송수신 계약이 일치한다.
|
||||
- 검증 기록:
|
||||
- 2026-07-10: 자동 검증으로 `./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 --check` PASS를 확인했다. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` deprecation warning만 출력됐다. 실제 서버/WebSocket 및 앱 화면 수동 확인은 연결된 기기와 서버 세션 증거가 없어 미완료로 유지한다.
|
||||
- 2026-07-10: review-work blocker 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --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 --check` PASS를 재확인했다. 실제 서버/WebSocket 및 앱 화면 수동 확인은 여전히 환경 증거가 없어 미완료로 유지한다.
|
||||
|
||||
- [x] **Task 14.9: 전송 후 키보드 유지와 내 메시지 오른쪽 정렬 보정**
|
||||
- Files:
|
||||
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt`
|
||||
- Modify: `app/src/main/res/layout/item_dm_chat_my_message.xml`
|
||||
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivitySourceTest.kt`
|
||||
- 작업:
|
||||
- 텍스트 메시지 전송 후 `EditText` focus와 soft keyboard를 유지한다.
|
||||
- 내 메시지 `message_container`와 상태 텍스트 수평 체인을 오른쪽 packed 정렬로 고정한다.
|
||||
- 검증:
|
||||
- Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --max-workers=1`
|
||||
- Expected: 전송 후 keyboard hide 호출이 없고, 내 메시지 bubble chain이 오른쪽으로 packed 정렬되는 source/layout 검증이 PASS.
|
||||
|
||||
- [x] **Task 14.10: WebSocket 연결 중 LoadingDialog 표시**
|
||||
- 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/DmChatRoomActivity.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/DmChatRoomActivitySourceTest.kt`
|
||||
- 작업:
|
||||
- WebSocket `connectRealtime()` 시작 후 `JOINED` 수신 전까지 연결 중 상태를 노출한다.
|
||||
- DM 채팅방 Activity는 연결 중 상태가 true이면 `LoadingDialog`를 표시하고, `JOINED`/failure/leave 시 dismiss한다.
|
||||
- 검증:
|
||||
- Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --max-workers=1`
|
||||
- Expected: 느린 WebSocket 연결 중 LoadingDialog 표시 경로와 상태 해제 회귀 테스트가 PASS.
|
||||
|
||||
- [x] **Task 14.11: 프로필 배경에 내 프로필 이미지 표시**
|
||||
- Files:
|
||||
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt`
|
||||
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivitySourceTest.kt`
|
||||
- 작업:
|
||||
- DM 채팅방 header foreground profile은 기존처럼 상대방 프로필 이미지를 유지한다.
|
||||
- DM 채팅방 background profile은 현재 요구사항에 맞춰 내 프로필 이미지(`SharedPreferenceManager.profileImage`)를 표시한다.
|
||||
- 검증:
|
||||
- Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --max-workers=1`
|
||||
- Expected: foreground profile은 상대방 프로필을 유지하고 background profile은 내 프로필 이미지를 사용하는 source 검증이 PASS.
|
||||
|
||||
### Phase 15: 채팅 empty state와 WebSocket ERROR UX 보정
|
||||
|
||||
- [x] **Task 15.1: 채팅 탭 empty state 문구 표시**
|
||||
- Files:
|
||||
- Modify: `docs/20260609_채팅_탭_페이지/prd.md`
|
||||
- Modify: `docs/20260609_채팅_탭_페이지/plan-task.md`
|
||||
- Modify: `app/src/main/res/layout/fragment_v2_main_chat.xml`
|
||||
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt`
|
||||
- Modify: `app/src/main/res/values/strings.xml`
|
||||
- Modify: `app/src/main/res/values-en/strings.xml`
|
||||
- Modify: `app/src/main/res/values-ja/strings.xml`
|
||||
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragmentLayoutTest.kt`
|
||||
- 작업:
|
||||
- 전체/AI 채팅/DM 탭의 빈 목록 응답에서 지정된 empty state 문구를 표시한다.
|
||||
- Content/Loading/Error 상태에서는 empty state 문구를 숨긴다.
|
||||
- 검증:
|
||||
- Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest" --max-workers=1`
|
||||
- Expected: 탭별 empty state 문구와 visibility source/layout 검증이 PASS.
|
||||
|
||||
- [x] **Task 15.2: JOINED 전 WebSocket ERROR 재시도와 null roomId 필터링**
|
||||
- Files:
|
||||
- Modify: `docs/20260610_DM_채팅화면/prd.md`
|
||||
- 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`
|
||||
- 작업:
|
||||
- `JOINED` 전 `ERROR` 수신 시 최대 3번 재시도한다.
|
||||
- 재시도 후에도 실패하면 LoadingDialog 상태를 해제하고 `대화방에 접속하지 못했습니다` 오류를 표시한 뒤 socket을 닫고 화면을 종료한다.
|
||||
- root `roomId`가 null인 `MESSAGE`/`SEND_ACK`는 malformed 이벤트로 보고 무시한다.
|
||||
- 검증:
|
||||
- Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1`
|
||||
- Expected: pre-JOIN ERROR retry/terminal failure와 null roomId filtering 테스트가 PASS.
|
||||
- 검증 기록:
|
||||
- 2026-07-11: `JOINED` 전 `ERROR` 수신 시 최대 3회 재시도 후 `screen_dm_chat_room_enter_failed` 오류 상태, LoadingDialog 해제 상태, socket close, 화면 종료 이벤트를 발행하도록 보정했다. 또한 root `roomId == null`인 `MESSAGE`/`SEND_ACK`는 현재 방 이벤트로 허용하지 않도록 strict filtering으로 변경했다. 회귀 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest" --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 --check` PASS를 확인했다. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` deprecation warning이 출력됐지만 실패는 없었다.
|
||||
|
||||
### Phase 16: DM 메시지 시간 표시 후속 보정
|
||||
|
||||
- [x] **Task 16.1: DM 메시지 item createdAt 시간 표시**
|
||||
- Files:
|
||||
- Modify: `docs/20260610_DM_채팅화면/prd.md`
|
||||
- Modify: `docs/20260610_DM_채팅화면/plan-task.md`
|
||||
- Modify: `app/src/main/res/layout/item_dm_chat_my_message.xml`
|
||||
- Modify: `app/src/main/res/layout/item_dm_chat_opponent_message.xml`
|
||||
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/ui/DmChatMessageAdapter.kt`
|
||||
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivitySourceTest.kt`
|
||||
- 작업:
|
||||
- 내 메시지와 상대 메시지 item layout에 `tv_time` TextView를 추가한다.
|
||||
- 기존 `bg_chat_user_message`, `bg_chat_ai_message` 말풍선과 내 메시지 오른쪽/상대 메시지 왼쪽 정렬을 유지한다.
|
||||
- `DmChatMessageAdapter`에서 `DmChatMessageUiItem.createdAt`을 기존 `kr.co.vividnext.sodalive.chat.talk.room.formatMessageTime`으로 변환해 `tv_time`에 바인딩한다.
|
||||
- source/layout 테스트에 `tv_time` 존재와 formatter import/call 검증을 추가한다.
|
||||
- 검증:
|
||||
- Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --max-workers=1`
|
||||
- Run: `./gradlew :app:compileDebugKotlin --max-workers=1`
|
||||
- Run: `./gradlew :app:ktlintCheck --max-workers=1`
|
||||
- Run: `git diff --check`
|
||||
- Expected: DM 메시지 source/layout 검증, Kotlin compile, ktlint, diff whitespace 검증이 PASS.
|
||||
- 검증 기록:
|
||||
- 2026-07-11: `item_dm_chat_my_message.xml`과 `item_dm_chat_opponent_message.xml`에 `tv_time`을 추가하고, `DmChatMessageAdapter`에서 `formatMessageTime(item.createdAt)`을 내 메시지/상대 메시지 모두에 바인딩했다. source/layout 회귀 테스트에 시간 TextView와 formatter import/call 검증을 추가했다. 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --max-workers=1`, `./gradlew :app:compileDebugKotlin --max-workers=1`, `./gradlew :app:ktlintCheck --max-workers=1`, `git diff --check` PASS를 확인했다. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` deprecation warning만 출력됐고 실패는 없었다.
|
||||
- 2026-07-11: 시간 표시 데이터는 서버 DTO `DmChatMessageResponse.createdAt`과 UI 모델 `DmChatMessageUiItem.createdAt`에 이미 존재하고 매퍼에서 보존됨을 확인했다. UI는 해당 값을 기존 `formatMessageTime(createdAt)`으로 변환해 `tv_time`에 표시하므로, 별도 서버 필드 추가 없이 메시지 송수신 시간이 표시된다.
|
||||
|
||||
### Phase 17: WebSocket close 재연결과 전송 수락 여부 보정
|
||||
|
||||
- [x] **Task 17.1: socket close와 heartbeat PING 실패를 연결 끊김으로 처리**
|
||||
- Files:
|
||||
- Modify: `docs/20260610_DM_채팅화면/prd.md`
|
||||
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatSocketClient.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/DmChatSocketClientTest.kt`
|
||||
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomViewModelTest.kt`
|
||||
- 작업:
|
||||
- WebSocket `onClosing`/`onClosed`를 현재 listener의 연결 끊김 신호로 한 번만 전달한다.
|
||||
- ViewModel은 socket failure/close callback을 동일한 disconnect 처리로 모아 heartbeat 정리, socket close, foreground 조건 재연결 예약을 수행한다.
|
||||
- heartbeat `PING` 전송이 false를 반환하면 `PONG` timeout을 기다리지 않고 연결 끊김으로 처리한다.
|
||||
- 검증:
|
||||
- Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketClientTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --max-workers=1`
|
||||
- Expected: socket close callback, heartbeat PING send false 재연결 테스트가 PASS.
|
||||
|
||||
- [x] **Task 17.2: 전송 미수락 시 입력값 보존**
|
||||
- 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/DmChatRoomActivity.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/DmChatRoomActivitySourceTest.kt`
|
||||
- 작업:
|
||||
- `sendText(text)`는 blank, invalid room, `JOINED` 전/재연결 중이면 `false`를 반환한다.
|
||||
- 메시지가 pending/failed local item으로 ViewModel 상태에 수락된 경우에만 `true`를 반환한다.
|
||||
- Activity는 `sendText(text) == true`일 때만 입력창을 비우고 전송 버튼을 비활성화한다.
|
||||
- 검증:
|
||||
- Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --max-workers=1`
|
||||
- Expected: JOINED 전 전송 false와 Activity conditional clear source 검증이 PASS.
|
||||
|
||||
|
||||
## 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 체크박스 형식, 테스트 명령 작성 방식을 확인했다.
|
||||
@@ -973,3 +1302,13 @@
|
||||
- 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` 기준 연결 확정, WebSocket `MESSAGE` 병합, `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 --check` PASS를 확인했다. `ktlintCheck`와 Gradle 실행에서는 기존 Gradle deprecation warning만 출력됐고 실패는 없었다. Task 10.4의 MESSAGE/SEND_ACK race와 Task 10.5 회귀 테스트 정리는 후속 미완료 범위로 유지한다.
|
||||
- 2026-06-19: 사용자 제공 최신 FCM payload 계약을 반영해 파일 구조와 Phase 13 수동 확인 항목의 푸시 진입 기준을 `deep_link=${URISCHEME}://chat/{roomId}`로 갱신했다. Phase 12에는 기존 `chat_type`/`room_id` 기준 완료 이력을 보존한다는 주석을 추가하고, 새 계약 반영 범위를 미완료 `Task 12.5`로 추가했다. 이번 단계는 계획 문서 수정만 수행했으며 Android 구현, 빌드, 테스트는 실행하지 않았다.
|
||||
- 2026-07-10: 사용자 제공 최신 raw WebSocket 계약을 기준으로 `DmChatSocketModels.kt`, `DmChatSocketClient.kt`, `DmChatRepository.kt`, `DmChatRoomViewModel.kt`, `DmChatApi.kt`, `DmChatModels.kt`, `DmChatEventClient.kt`, `AppDI.kt`를 확인했다. 현재 구현은 REST create/open/messages와 제거된 `/events/disconnect`, `/messages/text` 미사용은 대체로 맞지만, 기존 SSE `DmChatEventClient.kt`와 `/events` URL 정의가 main source에 남아 있고 WebSocket root envelope와 server payload 파싱이 최신 계약과 다르다. 이에 따라 Phase 14에 envelope/parser/client/ViewModel/logging/voice multipart/SSE 제거/회귀 검증 작업을 추가했다. 이번 단계는 문서 보완만 수행했으며 Android 구현, 빌드, 테스트는 실행하지 않았다.
|
||||
- 2026-07-10: 사용자 추가 요청을 반영해 Phase 14 active 범위를 raw WebSocket 보정, legacy realtime 제거, 텍스트 전송 UI, 상대방 왼쪽/내 메시지 오른쪽 정렬, 기존 DM 채팅 버블 유지로 재정리했다. 음성 메시지는 DTO/API 계약만 보존하고 이번 UI에는 연결하지 않도록 Task 14.5, 14.7, 14.8에 반영했다. 이번 단계는 문서 보완만 수행했으며 Android 구현, 빌드, 테스트는 실행하지 않았다.
|
||||
- 2026-07-10: Phase 14 구현으로 WebSocket client/server envelope를 root `type`/`requestId`/`roomId`/`payload` 계약에 맞췄고, `SEND_ACK`/`MESSAGE` direct payload, `ERROR payload.messageKey`, `JOINED` 이후 전송 gate, UUID requestId, 15초 SEND_ACK timeout, raw JSON logging, voice multipart API/DTO, legacy realtime production source 제거, DM 채팅 UI 회귀 검증을 반영했다. 자동 검증으로 `./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 --check` PASS를 확인했다. 실제 서버/WebSocket 및 앱 화면 수동 확인은 연결된 기기와 서버 세션 증거가 없어 Task 14.8 미완료로 유지한다.
|
||||
- 2026-07-10: review-work에서 `DmChatSocketClient`의 `rawJsonLogger` seam은 있으나 `AppDI` production 생성 경로가 no-op 기본값을 사용한다는 blocker를 발견했다. `AppDI.kt`에 `Logger.d("DM WebSocket raw JSON: $rawJson")` 주입을 추가하고 `DmChatRoomActivitySourceTest`에 회귀 검증을 추가했다. 재검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --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 --check` PASS를 확인했다.
|
||||
- 2026-07-10: Task 14.10으로 WebSocket `connectRealtime()` 시작 후 `JOINED` 전까지 `LoadingDialog`를 표시하고, `JOINED`/failure/leave/onCleared 시 dismiss되도록 `DmChatRoomViewModel`과 `DmChatRoomActivity`를 보강했다. 회귀 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --max-workers=1`, `./gradlew :app:compileDebugKotlin --max-workers=1`, `./gradlew :app:ktlintCheck --max-workers=1`, `git diff --check` PASS를 확인했다. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` deprecation warning이 출력됐지만 실패는 없었다.
|
||||
- 2026-07-11: DM 채팅방 header foreground profile은 `state.opponentProfileImageUrl`과 `CircleCropTransformation()`을 유지하고, background profile은 `SharedPreferenceManager.profileImage`를 사용하도록 `DmChatRoomActivity`를 보정했다. `DmChatRoomActivitySourceTest`에 background binding 블록이 `SharedPreferenceManager.profileImage`를 포함하고 `state.opponentProfileImageUrl`를 포함하지 않는 회귀 검증을 추가했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --max-workers=1`, `./gradlew :app:compileDebugKotlin --max-workers=1`, `./gradlew :app:ktlintCheck --max-workers=1`, `git diff --check` PASS를 확인했다. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` deprecation warning이 출력됐지만 실패는 없었다.
|
||||
- 2026-07-11: Phase 15 보정 완료. 채팅 탭 empty state 문구 표시, `JOINED` 전 WebSocket `ERROR` 3회 재시도 후 실패 UX, null root `roomId` `MESSAGE`/`SEND_ACK` 무시를 구현하고 source/unit 테스트로 고정했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest" --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 --check` PASS를 확인했다. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` deprecation warning이 출력됐지만 실패는 없었다. 실제 서버/WebSocket 및 앱 화면 수동 확인은 연결된 기기와 서버 세션 증거가 없어 수행하지 못했다.
|
||||
- 2026-07-11: Phase 16 작업 전 `prd.md`, `plan-task.md`, DM 메시지 item layout, `DmChatMessageAdapter.kt`, `DmChatRoomActivitySourceTest.kt`, 기존 AI 채팅 메시지 layout/adapter의 시간 표시 패턴을 확인했다. 기존 AI 채팅은 `tv_time`과 `formatMessageTime(createdAt)`을 사용하고 있어 DM에도 같은 formatter를 사용하도록 계획했다.
|
||||
- 2026-07-11: Phase 16 완료. PRD에 DM 메시지 item createdAt 시간 표시 요구사항과 성공 기준을 추가했고, 계획 문서에 Phase 16/Task 16.1을 완료 상태로 누적했다. DM 내 메시지/상대 메시지 layout에는 `tv_time`을 추가하되 기존 말풍선 drawable과 좌우 정렬을 유지했다. `DmChatMessageAdapter`는 기존 `formatMessageTime(item.createdAt)`로 시간을 바인딩한다. 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --max-workers=1`, `./gradlew :app:compileDebugKotlin --max-workers=1`, `./gradlew :app:ktlintCheck --max-workers=1`, `git diff --check` PASS를 확인했다. 실제 기기/에뮬레이터 화면 수동 확인은 현재 실행 가능한 디바이스 세션 증거가 없어 source/layout/compile 검증으로 대체했다.
|
||||
- 2026-07-11: Phase 17 리뷰 보정으로 WebSocket close frame/closing 상태를 current listener의 disconnect signal로 전달하고, ViewModel에서 socket failure/close 및 heartbeat `PING` 전송 실패를 동일한 reconnect 경로로 처리하도록 보강했다. 또한 `sendText(text)`가 수락 여부를 `Boolean`으로 반환하고, Activity는 수락된 경우에만 입력창을 비우도록 수정했다. 회귀 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatSocketClientTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivitySourceTest" --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 --check` PASS를 확인했다. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` deprecation warning만 출력됐고 실패는 없었다.
|
||||
|
||||
Reference in New Issue
Block a user