docs(chat): DM 채팅화면 계획을 기록한다
This commit is contained in:
367
docs/20260610_DM_채팅화면/prd.md
Normal file
367
docs/20260610_DM_채팅화면/prd.md
Normal file
@@ -0,0 +1,367 @@
|
||||
# PRD: DM 채팅화면
|
||||
|
||||
## 1. Overview
|
||||
`ChatRoomActivity`와 유사한 DM 채팅방 상세 화면을 제공하고, 사용자와 크리에이터 간 DM 메시지를 `user-creator-chat` API와 SSE 기반 실시간 이벤트로 송수신한다.
|
||||
|
||||
---
|
||||
|
||||
## 2. Problem
|
||||
- 기존 `ChatRoomActivity`는 AI 캐릭터 채팅방 기준 화면으로, 캐릭터 타입 배지, CAN 배지, 더보기, 안내 메시지, 쿼터/유료 메시지 흐름이 포함되어 있다.
|
||||
- 채팅 탭의 DM item 클릭 시 이동할 DM 상세 화면이 아직 별도 범위로 구현되어 있지 않다.
|
||||
- DM 채팅은 AI 채팅과 다르게 크리에이터와 사용자 간 메시지 송수신, SSE 실시간 이벤트 연결/해제, 커서 기반 과거 메시지 조회가 핵심이다.
|
||||
- 화면 이탈 또는 앱 백그라운드 전환 시 실시간 연결 해제 API를 항상 호출해야 하므로 생명주기 요구사항을 명확히 문서화해야 한다.
|
||||
- REST pagination과 SSE 실시간 수신 결과가 겹칠 수 있으므로 메시지 병합/중복 제거 기준이 필요하다.
|
||||
|
||||
---
|
||||
|
||||
## 3. Goals
|
||||
- 기존 `ChatRoomActivity`의 채팅방 상세 UI 구조를 최대한 재사용하되, DM에 맞지 않는 UI를 제거한 화면을 정의한다.
|
||||
- `CreateOrGetRoom`, `OpenRoom`, `ConnectEvents`, `GetMessages`, `SendTextMessage`, `DisconnectRealtime` API 계약을 Android 프로젝트 네이밍에 맞게 정리한다.
|
||||
- DM 채팅방 진입 시 방 생성/조회 후 생성된 `roomId`로 채팅방을 열고 초기 메시지를 표시한다.
|
||||
- 사용자가 상단으로 스크롤하면 과거 메시지를 커서 기반으로 추가 조회한다.
|
||||
- 텍스트 메시지 전송 후 서버 응답 메시지를 화면에 반영한다.
|
||||
- 채팅방 화면 진입/이탈, 앱 foreground/background 전환에 따른 SSE 연결/해제 정책을 정의한다.
|
||||
- SSE 이벤트 이름/응답 payload, 재연결 가이드, UI thread 비차단, 최신 메시지 동기화 같은 실시간 연결 운영 기준을 정의한다.
|
||||
|
||||
---
|
||||
|
||||
## 4. Non-Goals
|
||||
- AI 캐릭터 채팅방의 쿼터 구매, 광고 보상, 유료 메시지 구매, 채팅 리셋 기능은 DM 채팅화면에 포함하지 않는다.
|
||||
- `character_type_badge`, `ll_can_badge`, `iv_more`, `notice_container`는 DM 화면에 표시하지 않는다.
|
||||
- 음성 메시지 전송/재생 UI는 이번 범위에서 구현하지 않는다. 단, 서버 DTO의 `voiceMessageUrl` 필드는 모델에 보존한다.
|
||||
- 메시지 삭제, 신고, 차단, 알림 설정, 읽음 처리, unread count 실시간 갱신은 이번 범위에 포함하지 않는다.
|
||||
- 백엔드 API 스키마와 필드명을 Android에서 임의 변경하지 않는다. Kotlin class 이름만 프로젝트 가이드에 맞게 조정한다.
|
||||
- 기존 AI `ChatRoomActivity` 동작을 변경하거나 리팩터링하지 않는다.
|
||||
- 음성 메시지 UI가 아직 정해지지 않았으므로 `messageType=VOICE` 메시지는 DTO에는 보존하되 이번 구현 대상 UI에서 제외한다.
|
||||
|
||||
---
|
||||
|
||||
## 5. Target Users
|
||||
- 채팅 탭의 DM 채팅방에서 크리에이터와 1:1 메시지를 주고받으려는 앱 사용자.
|
||||
- `kr.co.vividnext.sodalive.v2.main.chat` 및 신규 DM 채팅 화면을 구현/유지보수하는 Android 개발자.
|
||||
|
||||
---
|
||||
|
||||
## 6. User Stories
|
||||
- 사용자는 크리에이터 프로필 또는 채팅 탭에서 DM 채팅방을 열고 싶다.
|
||||
- 사용자는 DM 채팅방에 들어왔을 때 상대 프로필, 상대 이름, 최근 메시지를 바로 보고 싶다.
|
||||
- 사용자는 텍스트를 입력해 크리에이터에게 메시지를 보낼 수 있어야 한다.
|
||||
- 사용자는 채팅 목록 상단으로 스크롤해 이전 대화를 이어서 확인하고 싶다.
|
||||
- 사용자는 화면을 벗어나거나 앱이 백그라운드로 전환될 때 실시간 연결이 안전하게 종료되기를 기대한다.
|
||||
|
||||
---
|
||||
|
||||
## 7. Core Features
|
||||
|
||||
### DM Chat Room Entry
|
||||
DM 채팅방을 생성하거나 기존 방을 조회한 뒤 상세 화면을 연다.
|
||||
|
||||
#### Requirements
|
||||
- 크리에이터와의 DM 시작 진입점에서는 `creatorId`를 전달받아 `POST /api/v2/user-creator-chat/rooms/create`를 호출한다.
|
||||
- API 응답의 `roomId`를 기준으로 DM 채팅방 상세 화면을 연다.
|
||||
- 채팅 탭 DM item처럼 이미 `roomId`를 알고 있는 진입점은 방 생성/조회 API를 생략하고 바로 OpenRoom 흐름으로 진입할 수 있다.
|
||||
- `roomId <= 0`인 상태에서는 채팅방을 열지 않고 기존 Activity 종료/오류 처리 정책을 따른다.
|
||||
|
||||
#### API Contract
|
||||
- Operation: `CreateOrGetRoom`
|
||||
- Method: `POST`
|
||||
- Path: `/api/v2/user-creator-chat/rooms/create`
|
||||
- Request: `CreateUserCreatorChatRoomRequest`
|
||||
- Response: `CreateUserCreatorChatRoomResponse`
|
||||
|
||||
```kotlin
|
||||
data class CreateUserCreatorChatRoomRequest(
|
||||
val creatorId: Long
|
||||
)
|
||||
|
||||
data class CreateUserCreatorChatRoomResponse(
|
||||
val roomId: Long
|
||||
)
|
||||
```
|
||||
|
||||
#### Naming Requirements
|
||||
- Android DTO 이름은 기능 도메인과 현재 프로젝트 가이드를 고려해 `CreateDmChatRoomRequest`, `CreateDmChatRoomResponse` 또는 동등하게 명확한 이름으로 변경한다.
|
||||
- 파일/패키지는 신규 v2 화면 원칙에 따라 `kr.co.vividnext.sodalive.v2` 하위에 둔다.
|
||||
|
||||
### DM Chat Room UI
|
||||
기존 `activity_chat_room.xml`과 유사한 전체 구조를 유지하되 DM에 맞지 않는 요소를 제거한다.
|
||||
|
||||
#### Requirements
|
||||
- 전체 화면은 기존 채팅방처럼 배경 이미지, dim, header, message RecyclerView, input 영역으로 구성한다.
|
||||
- header에는 뒤로가기, 상대 프로필 이미지, 상대 닉네임을 표시한다.
|
||||
- 아래 UI는 표시하지 않는다.
|
||||
- `character_type_badge`
|
||||
- `ll_can_badge`
|
||||
- 더보기 버튼 `iv_more`
|
||||
- 안내 메시지 영역 `notice_container`
|
||||
- `rv_messages`의 top constraint는 `notice_container`가 아닌 `header_container` 하단을 기준으로 조정한다.
|
||||
- 입력 영역은 기존 `et_message`, `iv_send`와 유사하게 텍스트 입력 및 전송 버튼을 제공한다.
|
||||
- 전송 버튼은 입력값이 blank이면 비활성화하고, 입력값이 있으면 활성화한다.
|
||||
|
||||
#### Edge Cases
|
||||
- 상대 프로필 이미지 URL이 비어 있거나 로딩 실패하면 기존 placeholder 정책을 따른다.
|
||||
- 상대 닉네임이 비어 있으면 빈 문자열 그대로 표시하고 임의 대체 문구를 추가하지 않는다.
|
||||
- 키보드 표시, IME send, 화면 하단 padding/inset은 기존 `ChatRoomActivity`의 사용자 경험을 우선 참고한다.
|
||||
|
||||
### Open Room And Initial Messages
|
||||
생성되었거나 기존에 존재하는 DM 채팅방을 열고 최신 메시지를 표시한다.
|
||||
|
||||
#### Requirements
|
||||
- 화면 진입 시 `GET /api/v2/user-creator-chat/rooms/{roomId}/open`을 호출한다.
|
||||
- `limit` query parameter 기본값은 `20`으로 설정한다.
|
||||
- 응답의 `messages`를 오래된 메시지부터 최신 메시지 순으로 정렬해 표시한다.
|
||||
- `hasMore`, `nextCursor`를 저장해 과거 메시지 조회 상태로 사용한다.
|
||||
- 응답의 `opponentNickname`, `opponentProfileImageUrl`을 header 상대 정보로 사용한다.
|
||||
- `mine=true`인 메시지는 사용자 발신 말풍선, `mine=false`인 메시지는 상대 발신 말풍선으로 표시한다.
|
||||
- `senderNickname`, `senderProfileImageUrl`은 상대 메시지 item 표시와 메시지별 발신자 정보가 필요한 경우에 활용한다.
|
||||
|
||||
#### API Contract
|
||||
- Operation: `OpenRoom`
|
||||
- Method: `GET`
|
||||
- Path: `/api/v2/user-creator-chat/rooms/{roomId}/open`
|
||||
- Query: `limit=20`
|
||||
- Response: `UserCreatorChatRoomOpenResponse`
|
||||
|
||||
```kotlin
|
||||
data class UserCreatorChatRoomOpenResponse(
|
||||
val roomId: Long,
|
||||
val opponentNickname: String,
|
||||
val opponentProfileImageUrl: String,
|
||||
val messages: List<UserCreatorChatMessageItemDto>,
|
||||
val hasMore: Boolean,
|
||||
val nextCursor: Long?
|
||||
)
|
||||
```
|
||||
|
||||
### SSE Realtime Events
|
||||
채팅방이 열려 있는 동안 서버 이벤트를 연결해 새 메시지를 실시간으로 반영한다.
|
||||
|
||||
#### Requirements
|
||||
- OpenRoom 성공 후 `GET /api/v2/user-creator-chat/rooms/{roomId}/events`를 호출해 SSE 연결을 시작한다.
|
||||
- SSE 연결은 화면이 foreground에 있고 채팅방이 활성 상태일 때만 유지한다.
|
||||
- 화면을 벗어나거나 앱이 background로 전환되면 `DisconnectRealtime` API를 항상 호출한다.
|
||||
- 재진입 또는 foreground 복귀 시 현재 `roomId`로 다시 SSE 연결을 시도한다.
|
||||
- 재연결 성공 후 필요한 경우 GetMessages API로 누락 가능성이 있는 메시지를 동기화한 뒤 SSE 연결 상태를 갱신한다.
|
||||
- SSE로 수신한 메시지는 기존 목록에 중복 추가하지 않는다. 중복 판단 기준은 `messageId`다.
|
||||
- SSE 연결 객체는 더 이상 사용하지 않을 때 cancel/close 처리하고 listener 참조를 해제한다.
|
||||
- 서버는 SSE `reconnectTime`을 `3000`ms로 내려주므로 클라이언트 재연결 기본 간격은 3초를 따른다.
|
||||
- 서버가 각 이벤트에 `id`를 부여하더라도 현재 백엔드는 재연결 시 `Last-Event-ID`를 해석해 유실 이벤트를 replay하지 않는다.
|
||||
- 네트워크 오류로 인한 재연결은 서버의 3초 가이드를 따르되, 구현 라이브러리가 추가 backoff/jitter를 제공하는 경우 즉시 무한 재시도가 발생하지 않도록 적용한다.
|
||||
- disconnect/cancel 처리는 UI thread를 블로킹하지 않아야 한다.
|
||||
- 현재 저장소에는 SSE/EventSource 구현 패턴이 없으므로 구현 계획에서 OkHttp 기반 EventSource, Retrofit streaming, 별도 라이브러리 사용 여부를 확정한다.
|
||||
|
||||
#### Event Payloads
|
||||
- `connected` 이벤트
|
||||
- 이벤트 이름: `connected`
|
||||
- 데이터 형식: `"connected"` 단순 문자열
|
||||
- 용도: SSE 연결 성공 시 최초 1회 발송되는 handshake 이벤트다.
|
||||
- `message` 이벤트
|
||||
- 이벤트 이름: `message`
|
||||
- 데이터 형식: `UserCreatorChatMessageItemDto` JSON 객체
|
||||
- 용도: 채팅방에 새로운 메시지가 수신되었을 때 실시간으로 전송된다.
|
||||
- 주요 필드: `messageId`, `messageType`, `mine`, `createdAt`, `textMessage`, `voiceMessageUrl`, `senderId`, `senderNickname`, `senderProfileImageUrl`
|
||||
|
||||
#### API Contract
|
||||
- Operation: `ConnectEvents`
|
||||
- Method: `GET`
|
||||
- Path: `/api/v2/user-creator-chat/rooms/{roomId}/events`
|
||||
- Response: `text/event-stream`
|
||||
- Events: `connected`, `message`
|
||||
- Event id: 서버가 이벤트 `id`를 내려주지만 현재 서버는 재연결 시 `Last-Event-ID`를 replay 기준으로 처리하지 않는다.
|
||||
|
||||
#### Edge Cases
|
||||
- 연결 실패 시 앱이 crash 되지 않아야 하며, 기존 네트워크 오류 처리 정책에 맞춰 사용자에게 최소한으로 안내한다.
|
||||
- SSE 종료/실패 콜백 수신 시 연결 상태를 disconnected로 갱신한다.
|
||||
- 이미 연결 중이면 중복 연결을 만들지 않는다.
|
||||
- 연결 해제 API 호출 중 화면 종료가 진행되어도 종료 흐름을 막지 않는다.
|
||||
- `connected` 이벤트는 UI 메시지 목록에 추가하지 않고 연결 상태 갱신에만 사용한다.
|
||||
- `message` 이벤트 수신 시 `UserCreatorChatMessageItemDto`로 파싱하고, `messageType=TEXT`인 메시지만 이번 UI 대상으로 표시한다.
|
||||
- 재연결 후 `Last-Event-ID` 기반 자동 replay를 기대하지 않고, 필요한 경우 GetMessages API로 누락 메시지를 보정한다.
|
||||
|
||||
### Load Older Messages
|
||||
사용자가 메시지 목록 상단으로 스크롤하면 과거 메시지를 추가 조회한다.
|
||||
|
||||
#### Requirements
|
||||
- `hasMore=true`이고 `isLoading=false`인 상태에서 상단에 도달하면 과거 메시지 API를 호출한다.
|
||||
- `cursor`는 OpenRoom 또는 직전 GetMessages 응답의 `nextCursor`를 사용한다.
|
||||
- `cursor`는 현재 화면에 로드된 가장 오래된 메시지보다 이전 페이지를 요청하기 위한 keyset cursor로 취급한다.
|
||||
- `limit` query parameter 기본값은 `20`으로 설정한다.
|
||||
- 응답 메시지는 기존 목록 상단에 prepend하고, 스크롤 위치를 유지한다.
|
||||
- 중복 메시지는 `messageId` 기준으로 제거한다.
|
||||
|
||||
#### API Contract
|
||||
- Operation: `GetMessages`
|
||||
- Method: `GET`
|
||||
- Path: `/api/v2/user-creator-chat/rooms/{roomId}/messages`
|
||||
- Query: `cursor: Long?`, `limit=20`
|
||||
- Response: `UserCreatorChatMessagesPageResponse`
|
||||
|
||||
```kotlin
|
||||
data class UserCreatorChatMessagesPageResponse(
|
||||
val messages: List<UserCreatorChatMessageItemDto>,
|
||||
val hasMore: Boolean,
|
||||
val nextCursor: Long?
|
||||
)
|
||||
```
|
||||
|
||||
### Send Text Message
|
||||
사용자가 입력한 텍스트 메시지를 서버로 전송하고 결과를 UI에 반영한다.
|
||||
|
||||
#### Requirements
|
||||
- 전송 버튼 또는 IME send 액션 시 trim된 입력값이 blank이면 전송하지 않는다.
|
||||
- 전송 요청이 진행 중인 동안 같은 입력에 대한 연타/중복 전송을 방지한다.
|
||||
- 텍스트 메시지는 `POST /api/v2/user-creator-chat/rooms/{roomId}/messages/text`로 전송한다.
|
||||
- 요청 body는 `textMessage`를 사용한다.
|
||||
- 성공 응답의 `message`를 화면에 반영한다.
|
||||
- `deliveredRealtime`, `pushSent` 값은 응답 모델에 보존하되, 이번 PRD에서는 별도 UI를 추가하지 않는다.
|
||||
- 전송 시 낙관적 UI를 적용해 사용자 메시지를 즉시 목록에 추가하고 전송 중 상태로 표시한다.
|
||||
- 전송 실패 시 해당 메시지를 실패 상태로 전환하고 재시도 버튼을 표시한다.
|
||||
- 재시도 버튼을 누르면 같은 텍스트 메시지를 다시 전송하고, 성공 시 실패 상태를 정상 메시지 상태로 갱신한다.
|
||||
- 사용자가 과거 메시지를 보고 있는 상태에서 메시지를 전송하면 최신 메시지 위치로 이동하거나, 최신 페이지 동기화 후 전송 메시지를 반영하는 방식 중 하나를 구현 계획에서 확정한다.
|
||||
|
||||
#### API Contract
|
||||
- Operation: `SendTextMessage`
|
||||
- Method: `POST`
|
||||
- Path: `/api/v2/user-creator-chat/rooms/{roomId}/messages/text`
|
||||
- Request: `SendUserCreatorTextMessageRequest`
|
||||
- Response: `SendUserCreatorChatMessageResponse`
|
||||
|
||||
```kotlin
|
||||
data class SendUserCreatorTextMessageRequest(
|
||||
val textMessage: String
|
||||
)
|
||||
|
||||
data class SendUserCreatorChatMessageResponse(
|
||||
val message: UserCreatorChatMessageItemDto,
|
||||
val deliveredRealtime: Boolean,
|
||||
val pushSent: Boolean
|
||||
)
|
||||
```
|
||||
|
||||
### Disconnect Realtime
|
||||
채팅방 화면 종료 또는 앱 background 전환 시 실시간 연결을 서버에 명시적으로 해제한다.
|
||||
|
||||
#### Requirements
|
||||
- 화면을 벗어날 때 항상 `POST /api/v2/user-creator-chat/rooms/{roomId}/events/disconnect`를 호출한다.
|
||||
- 앱이 background로 전환될 때도 같은 API를 호출한다.
|
||||
- 이미 disconnect 호출 중이면 중복 요청을 만들지 않는다.
|
||||
- disconnect 성공 여부와 관계없이 화면 종료 자체는 막지 않는다.
|
||||
- 화면 이탈 시 disconnect는 실시간 연결 해제 범위로 한정하고, 로그아웃처럼 로컬 캐시 삭제가 필요한 흐름과 구분한다.
|
||||
|
||||
#### API Contract
|
||||
- Operation: `DisconnectRealtime`
|
||||
- Method: `POST`
|
||||
- Path: `/api/v2/user-creator-chat/rooms/{roomId}/events/disconnect`
|
||||
- Response: `ApiResponse<Boolean>`
|
||||
|
||||
### Message DTO
|
||||
서버 메시지 DTO는 텍스트/음성 필드를 모두 보존하되, 이번 화면의 구현 대상은 텍스트 메시지 UI와 텍스트 메시지 전송만으로 한정한다.
|
||||
|
||||
#### Response Model
|
||||
```kotlin
|
||||
data class UserCreatorChatMessageItemDto(
|
||||
val messageId: Long,
|
||||
val messageType: String,
|
||||
val mine: Boolean,
|
||||
val createdAt: Long,
|
||||
val textMessage: String?,
|
||||
val voiceMessageUrl: String?,
|
||||
val senderId: Long,
|
||||
val senderNickname: String,
|
||||
val senderProfileImageUrl: String
|
||||
)
|
||||
```
|
||||
|
||||
#### Naming Requirements
|
||||
- Android DTO 이름은 `DmChatMessageResponse`, `DmChatRoomOpenResponse`, `DmChatMessagesPageResponse`, `SendDmTextMessageRequest`, `SendDmChatMessageResponse` 등 기능 중심 이름으로 조정한다.
|
||||
- `messageType`은 서버 문자열을 그대로 보존한다.
|
||||
- 가능한 `messageType` 값은 `TEXT`, `VOICE`다.
|
||||
- `TEXT`는 텍스트 메시지이며 `textMessage`를 사용해 말풍선 UI로 표시한다.
|
||||
- `VOICE`는 음성 메시지이며 `voiceMessageUrl`을 보존한다. 음성 메시지 UI가 아직 정해지지 않았으므로 이번 구현에서는 별도 재생/전송 UI를 만들지 않는다.
|
||||
- `createdAt`은 epoch millis로 보고 기존 시간 표시 유틸 또는 신규 formatter에서 변환한다.
|
||||
|
||||
---
|
||||
|
||||
## 8. UX / UI Expectations
|
||||
- 기존 AI 채팅방과 동일한 채팅 화면 감각을 유지하되 DM에서 불필요한 캐릭터/쿼터 요소는 제거한다.
|
||||
- header는 뒤로가기, 상대 프로필, 상대 닉네임만으로 간결하게 구성한다.
|
||||
- 메시지 영역은 header 바로 아래부터 시작한다.
|
||||
- 내 메시지와 상대 메시지의 시각적 구분은 기존 `ChatMessageAdapter`/item 스타일을 우선 재사용한다.
|
||||
- 긴 텍스트 메시지는 기존 채팅 말풍선의 줄바꿈/최대 폭 정책을 따른다.
|
||||
- 키보드가 올라와도 입력 영역과 최신 메시지가 가려지지 않아야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 9. Technical Constraints
|
||||
- Android XML Views, ViewBinding, RecyclerView, RxJava3, Retrofit, Gson, Koin 구조를 따른다.
|
||||
- 신규 `Activity`, `Fragment`, `ViewModel` 및 연결 하위 코드는 저장소 규칙에 따라 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
|
||||
- DM 화면은 신규 Activity로 생성한다.
|
||||
- 기존 `ChatRoomActivity`는 직접 변형하지 않는다. 필요한 경우 메시지 item/adapter/model 등 공용화 가능한 최소 컴포넌트만 구현 계획에서 검토한다.
|
||||
- REST API 응답은 기존 패턴처럼 `Single<ApiResponse<...>>`를 우선 사용한다. SSE 연결은 `text/event-stream` 수신으로 처리하며, `connected`/`message` event 파싱 방식을 구현 계획에서 확정한다.
|
||||
- token 전달은 기존 v2 API 패턴과 `SharedPreferenceManager.token` 사용 방식을 따른다. Repository 구현 시 `Authorization` 헤더 문자열은 단일 bearer helper로 생성해 오입력을 방지한다.
|
||||
- 앱 foreground/background 감지는 Activity lifecycle과 앱 전체 `ProcessLifecycleOwner` 중 어떤 기준을 사용할지 구현 계획에서 확정한다.
|
||||
- 구현 전 `docs/20260610_DM_채팅화면/plan-task.md`를 작성하고, 그 문서에 따라 최소 구현한다.
|
||||
- 테스트는 DTO mapper, 메시지 정렬/중복 제거, pagination state, disconnect lifecycle을 우선 검증한다.
|
||||
|
||||
---
|
||||
|
||||
## 10. API Summary
|
||||
|
||||
| 기능 | Method | Path | Request | Response |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 방 생성 또는 조회 | `POST` | `/api/v2/user-creator-chat/rooms/create` | `CreateUserCreatorChatRoomRequest` | `CreateUserCreatorChatRoomResponse` |
|
||||
| 생성된 채팅방 열기 | `GET` | `/api/v2/user-creator-chat/rooms/{roomId}/open?limit=20` | 없음 | `UserCreatorChatRoomOpenResponse` |
|
||||
| SSE 연결 | `GET` | `/api/v2/user-creator-chat/rooms/{roomId}/events` | 없음 | `text/event-stream`: `connected`(`String`), `message`(`UserCreatorChatMessageItemDto` JSON) |
|
||||
| 과거 메시지 조회 | `GET` | `/api/v2/user-creator-chat/rooms/{roomId}/messages?cursor={cursor}&limit=20` | 없음 | `UserCreatorChatMessagesPageResponse` |
|
||||
| 텍스트 메시지 보내기 | `POST` | `/api/v2/user-creator-chat/rooms/{roomId}/messages/text` | `SendUserCreatorTextMessageRequest` | `SendUserCreatorChatMessageResponse` |
|
||||
| SSE 연결 끊기 | `POST` | `/api/v2/user-creator-chat/rooms/{roomId}/events/disconnect` | 없음 | `ApiResponse<Boolean>` |
|
||||
|
||||
---
|
||||
|
||||
## 11. Metrics
|
||||
- `creatorId` 기반 진입 시 CreateOrGetRoom 성공 후 반환된 `roomId`로 DM 채팅방이 열린다.
|
||||
- `roomId` 기반 진입 시 OpenRoom이 `limit=20`으로 호출된다.
|
||||
- OpenRoom 응답 메시지가 시간순으로 `RecyclerView`에 표시된다.
|
||||
- OpenRoom 응답의 `opponentNickname`, `opponentProfileImageUrl`이 header 상대 정보로 표시된다.
|
||||
- 상단 스크롤 시 `hasMore=true`와 `nextCursor` 조건에 따라 GetMessages가 호출된다.
|
||||
- 텍스트 메시지 전송 성공 시 응답의 `message`가 화면에 반영된다.
|
||||
- 텍스트 메시지 전송 실패 시 낙관적으로 추가된 메시지가 실패 상태와 재시도 버튼을 표시한다.
|
||||
- 화면 이탈 또는 앱 background 전환 시 DisconnectRealtime 호출이 발생한다.
|
||||
- SSE 재연결 기본 간격은 서버 `reconnectTime=3000`ms를 따른다.
|
||||
- SSE 재연결 후 필요 시 GetMessages API로 누락 메시지를 동기화한다.
|
||||
- disconnect 처리는 UI thread를 블로킹하지 않는다.
|
||||
- 제거 대상 UI(`character_type_badge`, `ll_can_badge`, `iv_more`, `notice_container`)가 DM 화면에 나타나지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 12. Open Questions
|
||||
- DM 채팅방 진입점별 intent extra 이름은 구현 계획에서 확정한다.
|
||||
- 음성 메시지 UI/UX와 `VOICE` 메시지 표시 방식은 후속 범위에서 확정한다.
|
||||
|
||||
---
|
||||
|
||||
## 13. References
|
||||
- 기존 AI 채팅방 Activity: `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt`
|
||||
- 기존 AI 채팅방 layout: `app/src/main/res/layout/activity_chat_room.xml`
|
||||
- 기존 AI 채팅 API: `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkApi.kt`
|
||||
- 기존 AI 채팅 Repository: `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRepository.kt`
|
||||
- v2 채팅 탭 API: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/data/ChatRoomApi.kt`
|
||||
- v2 채팅 탭 모델: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/data/ChatRoomModels.kt`
|
||||
- 관련 PRD: `docs/20260609_채팅_탭_페이지/prd.md`
|
||||
- PRD 템플릿: `docs/prd/sample-prd.md`
|
||||
- 작업 문서 규칙: `docs/agent-guides/work-plan-docs.md`
|
||||
|
||||
---
|
||||
|
||||
## 14. Verification Log
|
||||
- 2026-06-10: `docs/prd/sample-prd.md`, `docs/agent-guides/work-plan-docs.md`, `docs/20260609_채팅_탭_페이지/prd.md`를 확인해 PRD 구조, 신규 문서 경로 규칙, 검증 기록 누적 방식을 확인했다.
|
||||
- 2026-06-10: `activity_chat_room.xml`을 확인해 제거 대상 UI인 `tv_character_type_badge`, `ll_can_badge`, `iv_more`, `notice_container`의 실제 위치와 메시지 영역 constraint를 확인했다.
|
||||
- 2026-06-10: `ChatRoomActivity.kt`, `TalkApi.kt`, `ChatRepository.kt`를 확인해 기존 채팅방의 초기 로드, 상단 pagination, 텍스트 전송, 입력 UI, header/notice/쿼터 구조를 분석했다.
|
||||
- 2026-06-10: `ChatRoomApi.kt`, `ChatRoomModels.kt` 및 `docs/20260609_채팅_탭_페이지/prd.md`를 확인해 v2 채팅 탭의 패키지/DTO/Retrofit 패턴과 DM 상세 화면 연결 배경을 확인했다.
|
||||
- 2026-06-10: `rg`로 `EventSource`, `SSE`, `text/event-stream`, `ResponseBody`, `events/disconnect` 등을 검색했으며 현재 저장소에는 재사용 가능한 SSE 구현 패턴이 없는 것으로 확인했다. 따라서 SSE 구현 방식은 후속 `plan-task.md`에서 별도 검증/결정할 항목으로 남겼다.
|
||||
- 2026-06-10: 이번 단계는 PRD 문서 생성만 수행했으며 Android 구현, 빌드, 테스트는 실행하지 않았다.
|
||||
- 2026-06-10: 백그라운드 조사 결과를 반영해 REST pagination과 realtime 수신의 `messageId` 기준 중복 제거, keyset cursor 의미, 전송 중 연타 방지, 과거 페이지를 보고 있을 때 전송 처리 기준, SSE cancel/close, failure 상태 전환, backoff/jitter 재연결, UI thread 비차단, disconnect와 로그아웃 캐시 삭제 범위 구분을 보강했다.
|
||||
- 2026-06-10: 사용자 확정 사항을 반영해 텍스트 전송 실패 UX를 낙관적 UI와 재시도 버튼으로 확정하고, DM 화면은 신규 Activity로 생성하도록 명시했다. OpenRoom 응답에 `opponentNickname`, `opponentProfileImageUrl`을 추가했으며, `messageType` 가능한 값은 `TEXT`, `VOICE`로 확정하고 이번 구현은 텍스트 메시지만 대상으로 제한했다.
|
||||
- 2026-06-10: 사용자 제공 백엔드 계약을 반영해 SSE 이벤트 이름과 payload를 확정했다. `connected` 이벤트는 `"connected"` 문자열 handshake로, `message` 이벤트는 `UserCreatorChatMessageItemDto` JSON으로 문서화했다. 서버 `reconnectTime=3000`ms, `Last-Event-ID` 기반 replay 미지원, 재연결 후 GetMessages API를 통한 누락 메시지 보정 요구사항도 반영했다.
|
||||
- 2026-06-10: 백그라운드 조사 결과와 대조해 `ConnectEvents` 응답 표기를 `ApiResponse<Unit>`에서 `text/event-stream`으로 보정하고, API Summary와 기술 제약도 SSE stream 수신/파싱 기준으로 갱신했다.
|
||||
- 2026-06-10: 후속 Repository 구현 시 `Authorization` 헤더 오입력 방지를 위해 단일 bearer helper로 헤더 문자열을 생성하도록 기술 제약에 기록했다.
|
||||
Reference in New Issue
Block a user