feat(chat): 채팅방 리스트 조회 API를 추가한다
This commit is contained in:
@@ -12,6 +12,12 @@
|
||||
- 앱 백그라운드 전환, 채팅방 화면 이탈, 연결 종료, 타임아웃 시 실시간 수신 상태를 해제한다.
|
||||
- typing indicator는 요구사항에서 제거한다.
|
||||
- 상대방이 방에 입장 중이라고 판단된 상태에서 실시간 전송이 실패해도 보완 푸시는 발송하지 않는다.
|
||||
- 채팅 리스트 API는 전체, AI 채팅, DM 채팅 필터를 하나의 통합 API에서 제공한다.
|
||||
- DM은 유저-크리에이터 채팅방을 의미하는 클라이언트/문서 표기명으로 사용한다.
|
||||
- 채팅 리스트에는 내가 참여 중인 방만 노출하고, 최종 대화 시간은 UTC 기준으로 내려 클라이언트가 표시 방식을 결정한다.
|
||||
- 채팅 리스트 API는 `/api/v2/chat/rooms`를 사용하고 최신순 30개씩 cursor 기반으로 페이징한다.
|
||||
- 마지막 메시지가 없는 방은 채팅 리스트에 노출하지 않고, 음성 메시지의 마지막 대화 요약은 `[음성 메시지]`를 사용한다.
|
||||
- 상대방 회원 또는 AI 캐릭터의 프로필 이미지가 없으면 기본 이미지를 내려준다.
|
||||
|
||||
## 접근안 비교
|
||||
|
||||
@@ -30,6 +36,22 @@
|
||||
- 단점: 신규 엔티티, API, 실시간 연결 관리 구현이 필요하다.
|
||||
- 결론: 이번 개편의 권장안으로 채택한다.
|
||||
|
||||
- [x] Option D: AI/DM 채팅 리스트 API를 각각 작성 검토
|
||||
- 장점: 각 도메인의 조회 조건은 단순하게 유지할 수 있다.
|
||||
- 단점: 클라이언트가 전체 리스트를 만들기 위해 두 API를 호출하고 병합/정렬해야 한다.
|
||||
- 결론: 전체 필터 요구사항과 맞지 않아 채택하지 않는다.
|
||||
|
||||
- [x] Option E: 통합 채팅 리스트 API에서 필터로 구분 검토
|
||||
- 장점: 클라이언트는 하나의 API로 전체, AI, DM 탭을 처리할 수 있다.
|
||||
- 장점: 참여 중인 방만 노출, 최신 대화순 정렬, 마지막 메시지 요약 정책을 서버에서 일관되게 적용할 수 있다.
|
||||
- 단점: 서버에서 AI 채팅방과 DM 채팅방의 응답 모델을 하나로 맞추는 조립 계층이 필요하다.
|
||||
- 결론: 이번 채팅 리스트 API의 채택안이다.
|
||||
|
||||
- [x] Option F: 기존 AI 채팅 리스트 API에 DM을 추가 검토
|
||||
- 장점: 새 endpoint 수를 줄일 수 있다.
|
||||
- 단점: 기존 AI 채팅 API의 의미가 넓어지고, DM 도메인 결합이 생긴다.
|
||||
- 결론: 공개 API 의미가 불명확해지므로 채택하지 않는다.
|
||||
|
||||
## 구현 계획 항목
|
||||
|
||||
- [x] 신규 도메인 패키지와 엔티티 설계
|
||||
@@ -88,6 +110,36 @@
|
||||
- 기존 AI `chat/room` API 동작을 변경하지 않는다.
|
||||
- 신규 도메인에서 필요한 공통 로직만 재사용하고, 외부 AI 세션 또는 쿼터 정책을 끌어오지 않는다.
|
||||
|
||||
- [x] 채팅 리스트 API 설계
|
||||
- API는 `GET /api/v2/chat/rooms`를 사용한다.
|
||||
- query parameter는 `filter`와 `limit`, `cursor`를 둔다.
|
||||
- `filter` 값은 `ALL`, `AI`, `DM` 중 하나이며 기본값은 `ALL`이다.
|
||||
- 인증된 회원이 참여 중인 AI 채팅방과 DM 채팅방만 조회한다.
|
||||
- 기본 정렬은 최종 대화 시간 내림차순이다.
|
||||
- 최신순 30개씩 조회한다.
|
||||
- 페이징은 기존 채팅 메시지 조회 관례와 맞춰 cursor 기반으로 설계한다.
|
||||
- 마지막 메시지가 없는 방은 리스트에서 제외한다.
|
||||
|
||||
- [x] 채팅 리스트 응답 DTO 설계
|
||||
- 페이지 응답 필드는 `rooms`, `hasMore`, `nextCursor`를 사용한다.
|
||||
- 각 방 응답 필드는 `roomId`, `chatType`, `targetName`, `targetImageUrl`, `lastMessage`, `lastMessageAt`을 사용한다.
|
||||
- `chatType`은 `AI` 또는 `DM`이다.
|
||||
- `roomId`는 해당 타입의 방 입장 API에 전달할 식별자이다.
|
||||
- `targetName`은 AI 채팅이면 캐릭터명, DM이면 나를 제외한 참여 회원 닉네임이다.
|
||||
- `targetImageUrl`은 AI 채팅이면 캐릭터 대표 이미지, DM이면 나를 제외한 참여 회원 프로필 이미지이다.
|
||||
- 상대방 회원 또는 AI 캐릭터의 프로필 이미지가 없으면 `targetImageUrl`에는 기본 이미지 URL을 내려준다.
|
||||
- `lastMessage`는 서버에서 15글자까지 자르고, 원문이 15글자를 초과하면 말줄임표를 붙인다.
|
||||
- 음성 메시지의 `lastMessage` 문구는 `[음성 메시지]`로 둔다.
|
||||
- `lastMessageAt`은 UTC 기준 ISO-8601 문자열을 사용한다.
|
||||
|
||||
- [x] 채팅 리스트 조회 정책 설계
|
||||
- DM 방은 `UserCreatorChatParticipant` 기준으로 현재 회원이 활성 참여자인 방만 조회한다.
|
||||
- AI 방은 기존 AI 채팅 참여자 모델 기준으로 현재 회원이 참여자인 방만 조회한다.
|
||||
- 비활성 메시지는 마지막 메시지 산정에서 제외한다.
|
||||
- 상대방 정보가 비활성 또는 삭제 상태일 때의 닉네임 표시 정책은 기존 회원/캐릭터 응답 관례를 따른다.
|
||||
- 상대방 회원 또는 AI 캐릭터 프로필 이미지가 없으면 기본 이미지를 사용한다.
|
||||
- 통합 조회 시 도메인별 최신 메시지 후보를 조회한 뒤 공통 DTO로 변환하고, `lastMessageAt` 기준으로 병합 정렬한다.
|
||||
|
||||
## 참고 파일
|
||||
|
||||
- `src/main/kotlin/kr/co/vividnext/sodalive/message/Message.kt`
|
||||
@@ -153,6 +205,13 @@ CREATE TABLE user_creator_chat_message (
|
||||
- SSE 연결이 끊기면 3초부터 재연결하고, 연속 실패 시 3초, 6초, 12초, 24초, 최대 30초까지 지수 백오프한다.
|
||||
|
||||
연동할 API:
|
||||
0. 채팅방 리스트 조회
|
||||
- `GET /api/v2/chat/rooms?filter=ALL&limit=30`
|
||||
- `filter`: `ALL`, `AI`, `DM`
|
||||
- 최신순 30개씩 cursor 기반으로 조회한다.
|
||||
- response data: `{ "rooms", "hasMore", "nextCursor" }`
|
||||
- room item: `{ "roomId", "chatType", "targetName", "targetImageUrl", "lastMessage", "lastMessageAt" }`
|
||||
|
||||
1. 방 생성/조회
|
||||
- `POST /api/v2/user-creator-chat/rooms/create`
|
||||
- body: `{ "creatorId": number }`
|
||||
@@ -200,6 +259,33 @@ CREATE TABLE user_creator_chat_message (
|
||||
- `senderProfileImageUrl`: string
|
||||
```
|
||||
|
||||
## 채팅 리스트 API 응답 예시
|
||||
|
||||
```json
|
||||
{
|
||||
"rooms": [
|
||||
{
|
||||
"roomId": 123,
|
||||
"chatType": "DM",
|
||||
"targetName": "creator_nick",
|
||||
"targetImageUrl": "https://cdn.example.com/profile/creator.png",
|
||||
"lastMessage": "안녕하세요. 문의드...",
|
||||
"lastMessageAt": "2026-05-14T03:12:30Z"
|
||||
},
|
||||
{
|
||||
"roomId": 456,
|
||||
"chatType": "AI",
|
||||
"targetName": "AI 캐릭터",
|
||||
"targetImageUrl": "https://cdn.example.com/default/profile.png",
|
||||
"lastMessage": "[음성 메시지]",
|
||||
"lastMessageAt": "2026-05-14T02:40:10Z"
|
||||
}
|
||||
],
|
||||
"hasMore": true,
|
||||
"nextCursor": "2026-05-14T02:40:10Z:456:AI"
|
||||
}
|
||||
```
|
||||
|
||||
## 검증 기록
|
||||
|
||||
### 1차 문서 작성
|
||||
@@ -246,3 +332,23 @@ CREATE TABLE user_creator_chat_message (
|
||||
- 무엇을: 실제 채팅방 탈퇴로 오해될 수 있는 `enter`, `leave` 표현을 제거하고, 방 화면 열기는 `open`, 실시간 수신 해제는 `events/disconnect`로 변경했다.
|
||||
- 왜: 현재 기능은 DB 참여자 삭제/비활성화가 아니라 최신 메시지 조회와 SSE/presence 해제이므로, 일반적인 채팅방 입장/탈퇴 의미와 혼동되지 않게 하기 위해서다.
|
||||
- 어떻게: 테스트에서 `disconnectRealtime` 메서드가 필요하도록 먼저 변경해 컴파일 실패를 확인한 뒤, 컨트롤러 URL과 서비스/DTO 함수명을 수정했다. 클라이언트 연동 문서도 새 API 의미와 URL로 갱신했다.
|
||||
|
||||
### 10차 채팅 리스트 API 문서화
|
||||
- 무엇을: 전체, AI 채팅, DM 채팅 필터를 지원하는 통합 채팅 리스트 API 요구사항과 응답 DTO 초안을 문서에 추가했다.
|
||||
- 왜: 클라이언트가 내가 참여 중인 채팅방만 최신 대화순으로 표시하고, 방 입장에 필요한 `roomId`와 상대방 정보, 마지막 대화 요약, UTC 기준 최종 대화 시간을 받아야 하기 때문이다.
|
||||
- 어떻게: 기존 유저-크리에이터 채팅방 개편 문서를 후속 요구사항으로 재사용하고, 통합 API 접근안을 채택했다. 응답 필드는 `roomId`, `chatType`, `targetName`, `targetImageUrl`, `lastMessage`, `lastMessageAt`으로 정리했으며, 마지막 대화는 서버에서 15글자 초과 시 말줄임표를 붙이고 최종 대화 시간은 UTC ISO-8601 문자열로 내려주도록 기록했다.
|
||||
|
||||
### 11차 채팅 리스트 API 정책 확정
|
||||
- 무엇을: 채팅 리스트 API 접근안 Option B 채택, `/api/v2/chat/rooms` URL, 30개 단위 최신순 페이징, 마지막 메시지 없는 방 제외, 음성 메시지 요약 문구, 기본 이미지 정책, 짧은 DTO 필드명을 문서에 반영했다.
|
||||
- 왜: 사용자 결정사항을 구현 전 계약으로 고정하고, 상대방 표시 정보의 응답 필드명을 더 짧게 만들기 위해서다.
|
||||
- 어떻게: 응답 필드명을 `targetName`, `targetImageUrl`로 변경하고, 음성 메시지 요약은 `[음성 메시지]`, 이미지가 없는 경우 기본 이미지 URL 사용, 기본 조회 개수는 30개로 갱신했다.
|
||||
|
||||
### 12차 채팅 리스트 API 구현
|
||||
- 무엇을: `/api/v2/chat/rooms` 통합 채팅 리스트 API, 응답 DTO, 서비스, AI/DM 조회 쿼리, 단위 테스트를 추가했다.
|
||||
- 왜: 문서에서 확정한 전체/AI/DM 필터, 내가 참여 중인 방만 조회, 최신순 30개 cursor 페이징, 마지막 메시지 요약, 기본 이미지, UTC ISO-8601 시간 응답을 구현하기 위해서다.
|
||||
- 어떻게: 실패 테스트를 먼저 작성해 신규 서비스/DTO/쿼리 부재로 컴파일 실패를 확인한 뒤 최소 구현을 추가했다. 이후 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.chat.ChatRoomListServiceTest' --rerun-tasks`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest'`, `./gradlew ktlintCheck`, `./gradlew build` 실행 결과 `BUILD SUCCESSFUL`을 확인했다. Kotlin LSP는 이 환경에 서버가 없어 diagnostics를 수행할 수 없었다.
|
||||
|
||||
### 13차 코드 리뷰 반영
|
||||
- 무엇을: 채팅 리스트 cursor 조건, UTC ISO 시간 변환, 빈 이미지 경로 기본 이미지 처리를 보완했다.
|
||||
- 왜: 같은 `lastMessageAt`을 가진 방이 여러 개일 때 cursor 이후 항목이 누락되지 않아야 하고, 문서 기준 UTC 시간과 기본 이미지 정책을 정확히 지켜야 하기 때문이다.
|
||||
- 어떻게: cursor를 `lastMessageAt`, `chatType`, `roomId` tuple 기준으로 적용하고 repository seek 조건도 같은 정렬 기준에 맞췄다. `lastMessageAt`은 `ZoneOffset.UTC` 기준 ISO 문자열로 변환하고, 빈 이미지 경로도 기본 이미지로 대체했다. 동일 timestamp cursor 테스트를 추가한 뒤 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.chat.ChatRoomListServiceTest' --rerun-tasks`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest'`, `./gradlew ktlintCheck`, `./gradlew build` 실행 결과 `BUILD SUCCESSFUL`을 확인했고, 재리뷰에서 남은 Critical/Important 결함 없음 승인을 받았다.
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
- 메시지 발송 시 상대방이 해당 방에 입장해 있으면 푸시를 보내지 않고 실시간으로 메시지를 표시한다.
|
||||
- 메시지 발송 시 상대방이 해당 방에 입장해 있지 않으면 푸시 알림을 발송한다.
|
||||
- 기존 AI 채팅 도메인의 엔티티라도 재활용할 수 있는지 코드 근거에 따라 결정한다.
|
||||
- 사용자가 참여 중인 AI 채팅방과 DM 채팅방을 하나의 채팅 리스트 API에서 조회할 수 있게 한다.
|
||||
- 채팅 리스트는 전체, AI 채팅, DM 채팅 필터를 지원한다.
|
||||
- 채팅 리스트 응답은 방 입장에 필요한 `roomId`, 상대방 표시 정보, 마지막 메시지 요약, 최종 대화 시간을 제공한다.
|
||||
|
||||
---
|
||||
|
||||
@@ -30,6 +33,7 @@
|
||||
- 텍스트/음성 외 이미지, 파일, 이모티콘, 읽음 확인, 메시지 수정 기능은 이번 범위에 포함하지 않는다.
|
||||
- typing indicator는 이번 범위에 포함하지 않는다.
|
||||
- 관리자 화면 개편은 이번 범위에 포함하지 않는다.
|
||||
- 채팅 리스트 API에서 메시지 본문 전체, 읽지 않은 메시지 수, 고정/숨김/삭제 상태, 검색 기능은 이번 범위에 포함하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
@@ -46,6 +50,9 @@
|
||||
- 크리에이터는 방에 들어와 있지 않을 때 새 메시지가 오면 푸시로 알림을 받고 싶다.
|
||||
- 크리에이터는 방에 들어와 있을 때 새 메시지가 오면 푸시 없이 화면에 바로 표시되기를 원한다.
|
||||
- 양쪽 사용자는 상대방이 방에 들어와 있을 때 새 메시지를 즉시 확인하고 싶다.
|
||||
- 사용자는 내가 참여 중인 모든 채팅방을 최신 대화순으로 보고 싶다.
|
||||
- 사용자는 AI 채팅방만 또는 DM 채팅방만 필터링해서 보고 싶다.
|
||||
- 사용자는 리스트에서 상대방 닉네임, 프로필 이미지, 마지막 대화 요약, 최종 대화 시간을 확인한 뒤 방에 입장하고 싶다.
|
||||
|
||||
---
|
||||
|
||||
@@ -111,6 +118,26 @@
|
||||
- 한 사용자가 여러 기기에서 같은 방에 입장할 수 있다면, 하나 이상의 활성 연결이 있을 때 입장 중으로 판단한다.
|
||||
- 앱 백그라운드 전환 또는 화면 이탈 이벤트가 서버에 도달하지 못해도 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
|
||||
@@ -147,6 +174,24 @@
|
||||
- 단점: 신규 엔티티, 저장소, 실시간 연결 관리, 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
|
||||
@@ -158,5 +203,12 @@
|
||||
|
||||
---
|
||||
|
||||
## 11. Open Questions
|
||||
- 없음. URL prefix와 DTO 필드명은 구현 직전 추천안을 제시해 확정하고, SSE 인증과 재연결 간격은 본 문서의 Technical Constraints 기준을 따른다.
|
||||
## 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 기준을 따른다.
|
||||
|
||||
Reference in New Issue
Block a user