feat(chat): 채팅방 리스트 조회 API를 추가한다

This commit is contained in:
2026-05-14 16:12:14 +09:00
parent 3a2c21c896
commit acd0393a0e
8 changed files with 650 additions and 2 deletions

View File

@@ -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 결함 없음 승인을 받았다.