diff --git a/docs/plan-task/20260513_유저크리에이터채팅방개편.md b/docs/plan-task/20260513_유저크리에이터채팅방개편.md index fddc4ab9..b1218433 100644 --- a/docs/plan-task/20260513_유저크리에이터채팅방개편.md +++ b/docs/plan-task/20260513_유저크리에이터채팅방개편.md @@ -213,6 +213,7 @@ CREATE TABLE user_creator_chat_message ( - `GET /api/v2/chat/rooms?filter=ALL&limit=30` - `filter`: `ALL`, `AI`, `DM` - 최신순 30개씩 cursor 기반으로 조회한다. + - 비로그인 요청은 200 OK와 빈 목록 페이지를 반환한다. - response data: `{ "rooms", "hasMore", "nextCursor" }` - room item: `{ "roomId", "chatType", "targetName", "targetImageUrl", "lastMessage", "lastMessageAt" }` @@ -271,6 +272,18 @@ CREATE TABLE user_creator_chat_message ( ## 채팅 리스트 API 응답 예시 +비로그인 요청: + +```json +{ + "rooms": [], + "hasMore": false, + "nextCursor": null +} +``` + +로그인 요청: + ```json { "rooms": [ @@ -362,3 +375,21 @@ CREATE TABLE user_creator_chat_message ( - 무엇을: 채팅 리스트 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 결함 없음 승인을 받았다. + +### 14차 채팅 리스트 비로그인 응답 정책 반영 +- [x] **Task 14.1: 비로그인 채팅 리스트 빈 목록 반환** + - 수정 파일: `src/main/kotlin/kr/co/vividnext/sodalive/v2/chat/controller/ChatRoomListController.kt` + - 테스트 파일: `src/test/kotlin/kr/co/vividnext/sodalive/v2/chat/ChatRoomListControllerTest.kt` + - 문서 파일: `docs/prd/20260513_유저크리에이터채팅방개편_prd.md`, `docs/plan-task/20260513_유저크리에이터채팅방개편.md` + - RED: `ChatRoomListController.getRooms(member = null, ...)`가 예외 없이 `rooms = []`, `hasMore = false`, `nextCursor = null`을 반환하고 `ChatRoomListService`를 호출하지 않는 테스트를 먼저 작성한다. + - RED 확인 명령: `./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process --tests kr.co.vividnext.sodalive.v2.chat.ChatRoomListControllerTest` + - GREEN: `member == null`이면 `ChatRoomListPageResponse(emptyList(), false, null)`를 `ApiResponse.ok`로 감싸 반환하고, 로그인 사용자는 기존처럼 service에 위임한다. + - GREEN 확인 명령: `./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process --tests kr.co.vividnext.sodalive.v2.chat.ChatRoomListControllerTest --tests kr.co.vividnext.sodalive.v2.chat.ChatRoomListServiceTest` + - REFACTOR/회귀 확인 명령: `./gradlew --no-daemon ktlintCheck` + - 검증 기록: + - RED: `./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process --tests kr.co.vividnext.sodalive.v2.chat.ChatRoomListControllerTest` 실행 결과 비로그인 테스트가 `SodaException`으로 실패해 기존 예외 동작을 재현했다. + - GREEN: `ChatRoomListController.getRooms`의 `member == null` 분기에서 빈 `ChatRoomListPageResponse`를 반환하도록 최소 수정했다. + - GREEN 확인: `./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process --tests kr.co.vividnext.sodalive.v2.chat.ChatRoomListControllerTest --tests kr.co.vividnext.sodalive.v2.chat.ChatRoomListServiceTest` 실행 결과 `BUILD SUCCESSFUL in 5m`을 확인했다. + - 회귀 확인: `./gradlew --no-daemon ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 33s`를 확인했다. + - 문서 확인: PRD와 plan-task 문서에서 미완성 표식을 검색한 결과 매칭이 없었다. + - 문서 명령 확인: 최초 `./gradlew --no-daemon tasks --all`은 Gradle wrapper lock 파일 샌드박스 접근 오류로 실패했고, 승인 실행한 동일 명령은 `BUILD SUCCESSFUL in 6s`로 통과했다. diff --git a/docs/prd/20260513_유저크리에이터채팅방개편_prd.md b/docs/prd/20260513_유저크리에이터채팅방개편_prd.md index a70108d0..3dbe0ea7 100644 --- a/docs/prd/20260513_유저크리에이터채팅방개편_prd.md +++ b/docs/prd/20260513_유저크리에이터채팅방개편_prd.md @@ -124,6 +124,7 @@ #### Requirements - 인증된 회원이 참여 중인 채팅방만 조회한다. +- 비로그인 사용자가 호출하면 예외를 발생시키지 않고 빈 목록을 내려준다. - 필터는 `ALL`, `AI`, `DM` 3가지를 지원한다. - `AI`는 기존 AI 캐릭터 채팅방을 의미한다. - `DM`은 유저-크리에이터 채팅방을 의미하며, API 문서와 클라이언트 표시 용어에서 User-Creator 채팅 대신 DM으로 명명한다. @@ -135,6 +136,7 @@ - 마지막 메시지가 없는 방은 채팅 리스트에 노출하지 않는다. #### Edge Cases +- 비로그인 요청은 `rooms = []`, `hasMore = false`, `nextCursor = null`로 응답한다. - 상대방 회원 또는 AI 캐릭터의 프로필 이미지가 없으면 기본 이미지를 사용한다. - 마지막 메시지가 음성 메시지이면 본문 요약 대신 `[음성 메시지]`를 내려준다. - 마지막 메시지가 비활성화되었거나 표시할 수 없는 상태라면 해당 메시지를 제외하고 다음 최신 표시 가능 메시지를 기준으로 요약한다. @@ -209,6 +211,7 @@ - 채팅 리스트에서 마지막 메시지가 없는 방은 노출하지 않는다. - 음성 메시지의 마지막 대화 요약 문구는 `[음성 메시지]`를 사용한다. - 채팅 리스트 API URL prefix는 `/api/v2/chat/rooms`를 사용한다. +- 채팅 리스트 API는 비로그인 요청에도 200 OK를 반환하며, 빈 목록 페이지를 내려준다. - 채팅 리스트 DTO 필드명은 `roomId`, `chatType`, `targetName`, `targetImageUrl`, `lastMessage`, `lastMessageAt`을 사용한다. - `targetName`은 AI 채팅이면 캐릭터명, DM이면 나를 제외한 참여 회원 닉네임이다. - `targetImageUrl`은 AI 채팅이면 캐릭터 대표 이미지, DM이면 나를 제외한 참여 회원 프로필 이미지이며, 이미지가 없으면 기본 이미지 URL을 내려준다.