Files
sodalive-backend-spring-boot/docs/prd/20260513_유저크리에이터채팅방개편_prd.md

215 lines
16 KiB
Markdown

# PRD: 유저-크리에이터 채팅방 개편
## 1. Overview
유저와 크리에이터가 주고받는 신규 메시지를 채팅방 형태로 제공하고, 텍스트/음성 메시지, 실시간 수신, 조건부 푸시 알림을 지원한다.
---
## 2. Problem
- 기존 `message` 도메인은 유저 간 1:1 텍스트/음성 메시지를 송수신함으로 구분해 조회하는 구조이며, 채팅방 입장 상태나 실시간 수신 상태를 표현하지 않는다.
- 기존 `chat/room` 도메인은 AI 캐릭터 채팅방으로, 외부 AI 세션 생성, 캐릭터 응답 저장, 이미지 메시지, 쿼터 차감이 핵심 책임이다.
- 유저-크리에이터 채팅방은 사람 간 대화이며, 상대방이 방에 들어와 있으면 푸시가 아닌 실시간 메시지 표시가 필요하다.
- 상대방이 현재 채팅방 화면에 있는지 판단하려면 방 단위 presence가 필요하지만, 현재 코드에서 WebSocket/SSE/STOMP 또는 presence 구현은 확인되지 않았다.
---
## 3. Goals
- 새 기능부터 유저-크리에이터 메시지를 채팅방 형태로 제공한다.
- 기존 `message` 도메인의 과거 메시지는 별도 유지하고, 신규 채팅방 메시지로 자동 마이그레이션하지 않는다.
- 신규 채팅방 메시지는 기존 메시지와 동일하게 텍스트 또는 음성 메시지만 허용한다.
- 메시지 발송 시 상대방이 해당 방에 입장해 있으면 푸시를 보내지 않고 실시간으로 메시지를 표시한다.
- 메시지 발송 시 상대방이 해당 방에 입장해 있지 않으면 푸시 알림을 발송한다.
- 기존 AI 채팅 도메인의 엔티티라도 재활용할 수 있는지 코드 근거에 따라 결정한다.
- 사용자가 참여 중인 AI 채팅방과 DM 채팅방을 하나의 채팅 리스트 API에서 조회할 수 있게 한다.
- 채팅 리스트는 전체, AI 채팅, DM 채팅 필터를 지원한다.
- 채팅 리스트 응답은 방 입장에 필요한 `roomId`, 상대방 표시 정보, 마지막 메시지 요약, 최종 대화 시간을 제공한다.
---
## 4. Non-Goals
- 기존 `message` 데이터의 신규 채팅방 메시지 마이그레이션은 이번 범위에 포함하지 않는다.
- 기존 `message` API와 조회 화면을 제거하지 않는다.
- AI 캐릭터 채팅의 외부 세션, 캐릭터 응답, 이미지 메시지, 쿼터 정책을 유저-크리에이터 채팅에 적용하지 않는다.
- 텍스트/음성 외 이미지, 파일, 이모티콘, 읽음 확인, 메시지 수정 기능은 이번 범위에 포함하지 않는다.
- typing indicator는 이번 범위에 포함하지 않는다.
- 관리자 화면 개편은 이번 범위에 포함하지 않는다.
- 채팅 리스트 API에서 메시지 본문 전체, 읽지 않은 메시지 수, 고정/숨김/삭제 상태, 검색 기능은 이번 범위에 포함하지 않는다.
---
## 5. Target Users
- 유저: 크리에이터에게 텍스트 또는 음성 메시지를 보내고 채팅방에서 대화를 이어가려는 회원
- 크리에이터: 유저와의 대화를 채팅방 단위로 확인하고 실시간으로 응답하려는 회원
---
## 6. User Stories
- 유저는 크리에이터와의 대화방에 들어가 이전 신규 채팅 메시지를 시간순으로 보고 싶다.
- 유저는 채팅방에서 텍스트 메시지를 보내고 상대방이 방에 있으면 바로 표시되기를 원한다.
- 유저는 채팅방에서 음성 메시지를 보내고 기존 메시지와 동일한 방식으로 재생 가능한 URL을 받기를 원한다.
- 크리에이터는 방에 들어와 있지 않을 때 새 메시지가 오면 푸시로 알림을 받고 싶다.
- 크리에이터는 방에 들어와 있을 때 새 메시지가 오면 푸시 없이 화면에 바로 표시되기를 원한다.
- 양쪽 사용자는 상대방이 방에 들어와 있을 때 새 메시지를 즉시 확인하고 싶다.
- 사용자는 내가 참여 중인 모든 채팅방을 최신 대화순으로 보고 싶다.
- 사용자는 AI 채팅방만 또는 DM 채팅방만 필터링해서 보고 싶다.
- 사용자는 리스트에서 상대방 닉네임, 프로필 이미지, 마지막 대화 요약, 최종 대화 시간을 확인한 뒤 방에 입장하고 싶다.
---
## 7. Core Features
### 도메인 결정
#### Requirements
- 신규 유저-크리에이터 채팅방 도메인과 엔티티를 작성한다.
- 기존 `message` 도메인은 과거 1:1 메시지와 기존 API 유지 용도로 둔다.
- 기존 `chat/room` 도메인은 AI 캐릭터 채팅 용도로 유지하고, 유저-크리에이터 채팅방의 엔티티로 직접 재활용하지 않는다.
- `message` 도메인의 텍스트/음성 저장 방식, 차단 검증, 음성 파일 업로드 방식, `FcmEventType.SEND_MESSAGE` 푸시 발행 패턴은 참고한다.
- `chat/room` 도메인의 방/참여자/메시지 분리 구조와 cursor 기반 메시지 조회 방식은 참고할 수 있다.
#### Rationale
- `src/main/kotlin/kr/co/vividnext/sodalive/chat/room/service/ChatRoomService.kt`는 외부 `weraser` AI 세션 생성과 `/api/chat` 호출, 캐릭터 응답 저장, 이미지 메시지, 쿼터 차감에 강하게 결합되어 있다.
- `src/main/kotlin/kr/co/vividnext/sodalive/chat/room/ChatRoom.kt``sessionId``title`이 필수이고, AI 외부 세션 식별자 의미가 들어간다.
- `src/main/kotlin/kr/co/vividnext/sodalive/chat/room/ChatParticipant.kt``USER/CHARACTER` 참여자와 `ChatCharacter` 참조를 전제로 하므로 유저-크리에이터의 회원 간 참여자 모델로 그대로 쓰기 어렵다.
- `src/main/kotlin/kr/co/vividnext/sodalive/chat/room/ChatMessage.kt`는 텍스트/이미지 중심이고 `CharacterImage`, `imagePath`, `price`를 포함하지만 음성 메시지 경로는 없다.
- `src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt`는 텍스트/음성 메시지와 푸시 발행 흐름이 유사하지만 방, 참여자, presence 개념이 없다.
- 따라서 AI 채팅 엔티티를 직접 재활용하기보다 신규 엔티티를 만들고, 엔티티 분리 구조와 조회 패턴만 참고하는 것이 책임 경계가 명확하다.
### 채팅방 생성 및 입장
#### Requirements
- 유저와 크리에이터 조합당 활성 채팅방은 하나만 사용한다.
- 신규 메시지를 처음 보내거나 채팅방 진입을 요청할 때 활성 방이 없으면 생성한다.
- 채팅방 입장 시 최신 메시지 목록을 cursor 기반으로 조회한다.
- 입장 상태는 실시간 연결 상태와 연동해 관리한다.
#### Edge Cases
- 상대방이 비활성 회원이면 메시지 발송을 거부한다.
- 기존 차단 관계가 있으면 메시지 발송을 거부한다.
- 동시에 같은 유저-크리에이터 조합으로 방 생성 요청이 들어오면 하나의 활성 방만 남도록 유니크 제약 또는 트랜잭션 정책을 둔다.
### 메시지 발송
#### Requirements
- 메시지 타입은 `TEXT`, `VOICE`만 허용한다.
- 텍스트 메시지는 본문을 저장한다.
- 음성 메시지는 기존 `message` 도메인처럼 S3 업로드 후 저장 경로를 메시지에 연결한다.
- 메시지 저장 후 상대방의 현재 방 입장 여부를 확인한다.
- 상대방이 같은 방에 입장 중이면 실시간 채널로 메시지를 전송하고 푸시는 발송하지 않는다.
- 상대방이 같은 방에 입장 중이 아니면 기존 FCM 발송 패턴을 사용해 푸시 알림을 발송한다.
#### Edge Cases
- 음성 파일 업로드 실패 시 메시지 저장과 파일 저장의 정합성을 보장한다.
- 실시간 전송 실패 시 메시지는 저장되어야 하지만, 상대방이 방에 있는 것으로 판단된 경우 보완 푸시는 발송하지 않는다.
- 같은 사용자의 중복 전송 요청은 클라이언트 재시도 정책과 서버 idempotency 필요 여부를 별도 결정한다.
### 실시간 수신 및 presence
#### Requirements
- 실시간 전송 방식은 SSE(`SseEmitter`)를 우선 사용한다.
- 메시지 발송은 기존 HTTP API로 처리하고, 서버에서 수신자에게 전달해야 하는 새 메시지만 SSE로 전송한다.
- 방 입장 시 사용자를 해당 방의 온라인 참여자로 등록하고 SSE 연결을 생성한다.
- 앱이 백그라운드로 전환되거나, 사용자가 채팅방 화면에서 나가거나, 연결이 종료되거나, 타임아웃되면 방 입장 상태를 해제한다.
- 메시지 발송 시 presence 상태를 기준으로 푸시 발송 여부를 결정한다.
- 서버 재시작 또는 비정상 연결 종료에도 오래된 presence가 남지 않도록 만료 시간을 둔다.
- Redis/Redisson 의존성이 이미 있으므로, 다중 서버 가능성을 고려해 presence는 Redis TTL 기반 저장을 우선 검토한다.
#### Edge Cases
- 한 사용자가 여러 기기에서 같은 방에 입장할 수 있다면, 하나 이상의 활성 연결이 있을 때 입장 중으로 판단한다.
- 앱 백그라운드 전환 또는 화면 이탈 이벤트가 서버에 도달하지 못해도 SSE 연결 종료와 Redis TTL 만료로 방 입장 상태가 해제되어야 한다.
### 채팅 리스트 API
#### Requirements
- 인증된 회원이 참여 중인 채팅방만 조회한다.
- 필터는 `ALL`, `AI`, `DM` 3가지를 지원한다.
- `AI`는 기존 AI 캐릭터 채팅방을 의미한다.
- `DM`은 유저-크리에이터 채팅방을 의미하며, API 문서와 클라이언트 표시 용어에서 User-Creator 채팅 대신 DM으로 명명한다.
- 기본 정렬은 최종 대화 시간 내림차순이다.
- 채팅 리스트는 최신순 30개씩 조회하고 cursor 기반으로 다음 페이지를 조회한다.
- 응답 항목은 방 입장을 위한 `roomId`, `chatType`, 상대방 닉네임, 상대방 프로필 이미지, 마지막 대화 요약, 최종 대화 시간을 포함한다.
- 마지막 대화 요약은 서버에서 15글자까지 내려주고, 15글자를 초과하면 말줄임표를 붙인다.
- 최종 대화 시간은 UTC 기준 값을 내려주고, 클라이언트가 표시 방식과 로컬 타임존 변환을 처리한다.
- 마지막 메시지가 없는 방은 채팅 리스트에 노출하지 않는다.
#### Edge Cases
- 상대방 회원 또는 AI 캐릭터의 프로필 이미지가 없으면 기본 이미지를 사용한다.
- 마지막 메시지가 음성 메시지이면 본문 요약 대신 `[음성 메시지]`를 내려준다.
- 마지막 메시지가 비활성화되었거나 표시할 수 없는 상태라면 해당 메시지를 제외하고 다음 최신 표시 가능 메시지를 기준으로 요약한다.
- DM 채팅방에서 현재 회원이 유저인지 크리에이터인지와 관계없이 상대방은 나를 제외한 참여자로 계산한다.
---
## 8. Technical Constraints
- 기존 Spring Boot 2.7.14, Kotlin, Java 17, Gradle Wrapper 구조를 유지한다.
- 문서 기준 신규 도메인 후보 패키지는 `user/creator/chat` 또는 기존 패키지 관례에 맞춘 별도 패키지로 둔다.
- 실시간 전송 방식은 SSE(`SseEmitter`)를 사용한다. 현재 코드베이스에 `spring-boot-starter-web`은 있으나 `spring-boot-starter-websocket`은 없고, typing indicator 요구사항이 제거되어 양방향 실시간 프로토콜 필요성이 낮기 때문이다.
- 서버는 새 메시지 이벤트만 SSE로 전송하고, 클라이언트의 메시지 발송/입장/퇴장 이벤트는 HTTP API로 처리한다.
- 신규 채팅방 API의 URL prefix와 응답 DTO 필드명은 구현 직전 추천안을 제시한 뒤 확정한다.
- 계획 문서 기준 URL prefix 추천안은 기존 `/api/chat/room`과 충돌하지 않는 `/api/user-creator-chat/rooms`이다.
- SSE 연결 인증은 기존 API 인증과 동일하게 `Authorization: Bearer <accessToken>` 헤더 방식을 우선 사용한다.
- 모바일 클라이언트에서 `Authorization` 헤더 기반 SSE 연결을 사용할 수 없는 라이브러리를 선택하는 경우에만 단기 수명 SSE 전용 토큰을 발급해 query parameter로 전달한다.
- 클라이언트 재연결 간격은 기본 3초, 연속 실패 시 3초, 6초, 12초, 24초, 최대 30초 지수 백오프로 확정한다.
- 푸시 발송은 기존 `FcmEvent`, `FcmSendListener`, `PushNotificationService` 패턴을 우선 재사용한다.
- 음성 파일 업로드는 기존 `S3Uploader`와 CloudFront URL 조합 방식을 우선 재사용한다.
- 공개 API 스키마는 신규 API로 분리하고 기존 `message` 및 AI `chat/room` API를 깨지 않는다.
---
## 9. Domain Approach Options
### Option A: 기존 `message` 도메인 확장
- 장점: 텍스트/음성 메시지, 차단 검증, FCM 발행 흐름을 가장 많이 재사용할 수 있다.
- 단점: 방, 참여자, presence를 기존 송수신함 모델에 추가해야 하므로 기존 API와 데이터 의미가 혼재된다.
- 판단: 기존 메시지를 별도 유지하기로 했으므로 권장하지 않는다.
### Option B: 기존 AI `chat/room` 엔티티 재활용
- 장점: `ChatRoom`, `ChatParticipant`, `ChatMessage`의 방/참여자/메시지 분리 구조와 cursor 조회 패턴이 이미 있다.
- 단점: `ChatRoom.sessionId`는 AI 외부 세션 의미이고, `ChatParticipant``USER/CHARACTER``ChatCharacter` 참조를 전제로 하며, `ChatMessage`는 이미지/가격 필드는 있지만 음성 메시지 필드가 없다.
- 판단: 엔티티만 재활용하더라도 유저-크리에이터 채팅 의미와 맞지 않는 필드와 제약이 많아 권장하지 않는다. 구조만 참고한다.
### Option C: 신규 유저-크리에이터 채팅방 도메인 작성
- 장점: 사람 간 채팅, presence, 조건부 푸시의 책임을 명확히 분리할 수 있다.
- 장점: 기존 `message`와 AI `chat/room`을 깨지 않고 필요한 패턴만 참고할 수 있다.
- 단점: 신규 엔티티, 저장소, 실시간 연결 관리, API가 필요해 초기 구현량이 늘어난다.
- 판단: 이번 요구사항의 권장안이다.
### 채팅 리스트 API 접근안
#### Option A: AI/DM 리스트 API를 각각 작성
- 장점: 각 도메인의 조회 조건과 응답 조립을 단순하게 유지할 수 있다.
- 단점: 클라이언트가 전체 리스트를 만들기 위해 두 API를 호출하고 병합/정렬해야 한다.
- 판단: 전체 필터 요구사항과 맞지 않아 권장하지 않는다.
#### Option B: 통합 리스트 API에서 필터로 구분
- 장점: 클라이언트는 하나의 API로 전체, AI, DM 탭을 처리할 수 있다.
- 장점: 참여 중인 방만 노출, 최신 대화순 정렬, 마지막 메시지 요약 정책을 서버에서 일관되게 적용할 수 있다.
- 단점: 서버에서 AI 채팅방과 DM 채팅방의 응답 모델을 하나로 맞추는 조립 계층이 필요하다.
- 판단: 이번 채팅 리스트 API의 채택안이다.
#### Option C: 기존 AI 채팅 리스트 API에 DM을 추가
- 장점: 새 endpoint 수를 줄일 수 있다.
- 단점: 기존 AI 채팅 API의 의미가 넓어지고, DM 도메인 결합이 생긴다.
- 판단: 공개 API 의미가 불명확해지므로 권장하지 않는다.
---
## 10. Metrics
- 신규 채팅방 메시지 발송 성공률
- 실시간 수신 성공률
- 방 입장 중 메시지에 대한 푸시 미발송 비율
- 방 미입장 상태 메시지에 대한 푸시 발송 성공률
- 방 입장 상태 해제 지연 시간
---
## 11. Confirmed Defaults
- 채팅 리스트에서 마지막 메시지가 없는 방은 노출하지 않는다.
- 음성 메시지의 마지막 대화 요약 문구는 `[음성 메시지]`를 사용한다.
- 채팅 리스트 API URL prefix는 `/api/v2/chat/rooms`를 사용한다.
- 채팅 리스트 DTO 필드명은 `roomId`, `chatType`, `targetName`, `targetImageUrl`, `lastMessage`, `lastMessageAt`을 사용한다.
- `targetName`은 AI 채팅이면 캐릭터명, DM이면 나를 제외한 참여 회원 닉네임이다.
- `targetImageUrl`은 AI 채팅이면 캐릭터 대표 이미지, DM이면 나를 제외한 참여 회원 프로필 이미지이며, 이미지가 없으면 기본 이미지 URL을 내려준다.
- 채팅 리스트는 최신순 30개씩 cursor 기반으로 페이징한다.
- SSE 인증과 재연결 간격은 본 문서의 Technical Constraints 기준을 따른다.