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

28 KiB

유저-크리에이터 채팅방 개편 계획

이 문서는 현재 요청 범위인 문서 작성만 다룬다. 실제 구현 시에는 이 계획의 구현 항목을 기준으로 세부 API, 엔티티, 테스트를 별도 작업 단위로 쪼갠다.

결정 요약

  • 새 기능부터 유저-크리에이터 채팅방을 제공한다.
  • 기존 message 도메인의 과거 메시지는 신규 채팅방으로 마이그레이션하지 않고 별도 유지한다.
  • 기존 AI chat/room 엔티티는 외부 AI 세션, 캐릭터 참여자, 이미지 메시지, 쿼터 관련 의미에 결합되어 있으므로 직접 재사용하지 않는다.
  • 신규 유저-크리에이터 채팅방 도메인을 작성하되, 기존 message의 텍스트/음성 및 FCM 패턴과 AI chat/room의 방/참여자/메시지 구조는 참고한다.
  • 실시간 전송 방식은 SSE(SseEmitter)로 결정한다.
  • 앱 백그라운드 전환, 채팅방 화면 이탈, 연결 종료, 타임아웃 시 실시간 수신 상태를 해제한다.
  • typing indicator는 요구사항에서 제거한다.
  • 상대방이 방에 입장 중이라고 판단된 상태에서 실시간 전송이 실패해도 보완 푸시는 발송하지 않는다.
  • 채팅 리스트 API는 전체, AI 채팅, DM 채팅 필터를 하나의 통합 API에서 제공한다.
  • DM은 유저-크리에이터 채팅방을 의미하는 클라이언트/문서 표기명으로 사용한다.
  • 채팅 리스트에는 내가 참여 중인 방만 노출하고, 최종 대화 시간은 UTC 기준으로 내려 클라이언트가 표시 방식을 결정한다.
  • 채팅 리스트 API는 /api/v2/chat/rooms를 사용하고 최신순 30개씩 cursor 기반으로 페이징한다.
  • 마지막 메시지가 없는 방은 채팅 리스트에 노출하지 않고, 음성 메시지의 마지막 대화 요약은 [음성 메시지]를 사용한다.
  • 상대방 회원 또는 AI 캐릭터의 프로필 이미지가 없으면 기본 이미지를 내려준다.

접근안 비교

  • Option A: 기존 message 도메인 확장 검토

    • 장점: 텍스트/음성 메시지와 FCM 발행 흐름 재사용이 쉽다.
    • 단점: 방, 참여자, presence, typing을 송수신함 모델에 섞어야 한다.
    • 결론: 기존 메시지를 별도 유지하기로 했으므로 채택하지 않는다.
  • Option B: 기존 AI chat/room 엔티티 재활용 검토

    • 장점: 방/참여자/메시지 구조와 cursor 조회 패턴을 참고할 수 있다.
    • 단점: ChatRoom.sessionId는 AI 외부 세션 의미이고, ChatParticipantUSER/CHARACTERChatCharacter 참조를 전제로 하며, ChatMessage는 음성 메시지 필드가 없다.
    • 결론: 엔티티만 재활용하더라도 사람 간 채팅 의미와 맞지 않아 채택하지 않고 구조만 참고한다.
  • Option C: 신규 유저-크리에이터 채팅방 도메인 작성 검토

    • 장점: 신규 요구사항인 실시간 수신, 조건부 푸시, presence를 독립적으로 설계할 수 있다.
    • 단점: 신규 엔티티, API, 실시간 연결 관리 구현이 필요하다.
    • 결론: 이번 개편의 권장안으로 채택한다.
  • Option D: AI/DM 채팅 리스트 API를 각각 작성 검토

    • 장점: 각 도메인의 조회 조건은 단순하게 유지할 수 있다.
    • 단점: 클라이언트가 전체 리스트를 만들기 위해 두 API를 호출하고 병합/정렬해야 한다.
    • 결론: 전체 필터 요구사항과 맞지 않아 채택하지 않는다.
  • Option E: 통합 채팅 리스트 API에서 필터로 구분 검토

    • 장점: 클라이언트는 하나의 API로 전체, AI, DM 탭을 처리할 수 있다.
    • 장점: 참여 중인 방만 노출, 최신 대화순 정렬, 마지막 메시지 요약 정책을 서버에서 일관되게 적용할 수 있다.
    • 단점: 서버에서 AI 채팅방과 DM 채팅방의 응답 모델을 하나로 맞추는 조립 계층이 필요하다.
    • 결론: 이번 채팅 리스트 API의 채택안이다.
  • Option F: 기존 AI 채팅 리스트 API에 DM을 추가 검토

    • 장점: 새 endpoint 수를 줄일 수 있다.
    • 단점: 기존 AI 채팅 API의 의미가 넓어지고, DM 도메인 결합이 생긴다.
    • 결론: 공개 API 의미가 불명확해지므로 채택하지 않는다.

구현 계획 항목

  • 신규 도메인 패키지와 엔티티 설계

    • 신규 패키지: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/
    • 신규 엔티티: UserCreatorChatRoom, UserCreatorChatParticipant, UserCreatorChatMessage
    • 메시지 타입: TEXT, VOICE
    • 채팅방 참여자는 UserCreatorChatParticipant를 기준으로 관리하고, UserCreatorChatRoom에는 특정 참여자 컬럼을 두지 않는다.
    • AI chat/room 엔티티는 직접 재활용하지 않고, 방/참여자/메시지 분리 구조만 참고한다.
  • 채팅방 생성/열기 API 설계

    • 구현 전 신규 채팅방 API의 URL prefix와 응답 DTO 필드명을 추천안으로 제시하고 확정한다.
    • URL prefix는 기존 /api/chat/room과 충돌하지 않도록 /api/v2/user-creator-chat/rooms를 사용한다.
    • 응답 DTO 필드명은 기존 chat/room의 응답 관례를 참고해 roomId, messages, hasMore, nextCursor, messageId, messageType, mine, createdAt, textMessage, voiceMessageUrl, senderId, senderNickname, senderProfileImageUrl을 우선 사용한다.
    • 활성 방 조회 또는 생성 API를 정의한다.
    • 채팅방 화면을 열 때 최신 메시지를 cursor 기반으로 조회한다.
    • 기존 chat/roomgetChatMessages cursor 조회 패턴을 참고한다.
  • 텍스트 메시지 발송 API 설계

    • 기존 MessageService.sendTextMessage의 수신자 활성 여부 검증, 차단 검증, FCM 이벤트 발행 흐름을 참고한다.
    • 메시지 저장 후 상대방 presence를 확인해 실시간 전송 또는 푸시 발송 중 하나만 수행한다.
  • 음성 메시지 발송 API 설계

    • 기존 MessageService.sendVoiceMessage의 S3 업로드 경로와 CloudFront 응답 방식을 참고한다.
    • 파일 업로드와 메시지 저장 정합성 정책을 정한다.
  • 실시간 연결 방식 확정

    • SSE(SseEmitter)를 사용한다.
    • 현재 build.gradle.kts에는 spring-boot-starter-web이 있고 spring-boot-starter-websocket은 없으므로 신규 WebSocket 의존성을 추가하지 않는다.
    • 클라이언트 메시지 발송과 방 화면 열기는 HTTP API로 처리하고 서버의 새 메시지 전달만 SSE로 처리한다.
    • SSE 연결 인증은 기존 API 인증과 동일하게 Authorization: Bearer <accessToken> 헤더를 사용하는 방식을 우선 사용한다.
    • 모바일 클라이언트에서 헤더 설정이 제한되는 라이브러리를 사용할 경우에만 단기 수명 SSE 전용 토큰을 발급해 query parameter로 전달하는 대안을 검토한다.
    • 클라이언트 재연결 간격은 기본 3초로 시작하고, 연속 실패 시 3초, 6초, 12초, 24초, 최대 30초 지수 백오프로 확정한다.
    • 서버는 SSE retry 값을 3초로 내려 클라이언트 기본 재연결 기준을 맞춘다.
  • Presence 정책 설계

    • SSE 연결 시 해당 회원의 활성 연결을 등록한다.
    • 앱 백그라운드 전환, 채팅방 화면 이탈, 연결 종료, 만료 시간 초과 시 활성 연결을 제거한다.
    • 여러 기기에서 같은 방에 입장한 경우 하나 이상의 활성 연결이 있으면 입장 중으로 판단한다.
    • 이미 Redis/Redisson 의존성이 있으므로 Redis TTL 기반 presence 저장을 우선 검토한다.
  • 조건부 푸시 발송 정책 설계

    • 상대방이 같은 방에 입장 중이면 푸시를 발송하지 않는다.
    • 상대방이 같은 방에 입장 중이 아니면 기존 FcmEventType.SEND_MESSAGE 패턴을 재사용하거나 신규 타입을 추가한다.
    • 상대방이 같은 방에 입장 중이라고 판단된 상태에서 SSE 전송이 실패해도 보완 푸시는 발송하지 않는다.
    • 신규 채팅방 딥링크가 필요하면 FcmDeepLinkValue 확장 여부를 검토한다.
  • 테스트 계획 수립

    • 방 생성 중복 방지 테스트를 작성한다.
    • 텍스트/음성 메시지 발송 성공 테스트를 작성한다.
    • 상대방 presence 상태에 따른 실시간 전송/푸시 발송 분기 테스트를 작성한다.
    • 앱 백그라운드 전환, 채팅방 화면 이탈, 연결 종료 시 presence 해제 테스트를 작성한다.
    • SSE 전송 실패 시 보완 푸시를 발송하지 않는 테스트를 작성한다.
  • 기존 도메인 영향도 확인

    • 기존 message API 동작을 변경하지 않는다.
    • 기존 AI chat/room API 동작을 변경하지 않는다.
    • 신규 도메인에서 필요한 공통 로직만 재사용하고, 외부 AI 세션 또는 쿼터 정책을 끌어오지 않는다.
  • 채팅 리스트 API 설계

    • API는 GET /api/v2/chat/rooms를 사용한다.
    • query parameter는 filterlimit, cursor를 둔다.
    • filter 값은 ALL, AI, DM 중 하나이며 기본값은 ALL이다.
    • 인증된 회원이 참여 중인 AI 채팅방과 DM 채팅방만 조회한다.
    • 기본 정렬은 최종 대화 시간 내림차순이다.
    • 최신순 30개씩 조회한다.
    • 페이징은 기존 채팅 메시지 조회 관례와 맞춰 cursor 기반으로 설계한다.
    • 마지막 메시지가 없는 방은 리스트에서 제외한다.
  • 채팅 리스트 응답 DTO 설계

    • 페이지 응답 필드는 rooms, hasMore, nextCursor를 사용한다.
    • 각 방 응답 필드는 roomId, chatType, targetName, targetImageUrl, lastMessage, lastMessageAt을 사용한다.
    • chatTypeAI 또는 DM이다.
    • roomId는 해당 타입의 방 입장 API에 전달할 식별자이다.
    • targetName은 AI 채팅이면 캐릭터명, DM이면 나를 제외한 참여 회원 닉네임이다.
    • targetImageUrl은 AI 채팅이면 캐릭터 대표 이미지, DM이면 나를 제외한 참여 회원 프로필 이미지이다.
    • 상대방 회원 또는 AI 캐릭터의 프로필 이미지가 없으면 targetImageUrl에는 기본 이미지 URL을 내려준다.
    • lastMessage는 서버에서 15글자까지 자르고, 원문이 15글자를 초과하면 말줄임표를 붙인다.
    • 음성 메시지의 lastMessage 문구는 [음성 메시지]로 둔다.
    • lastMessageAt은 UTC 기준 ISO-8601 문자열을 사용한다.
  • 채팅 리스트 조회 정책 설계

    • DM 방은 UserCreatorChatParticipant 기준으로 현재 회원이 활성 참여자인 방만 조회한다.
    • AI 방은 기존 AI 채팅 참여자 모델 기준으로 현재 회원이 참여자인 방만 조회한다.
    • 비활성 메시지는 마지막 메시지 산정에서 제외한다.
    • 상대방 정보가 비활성 또는 삭제 상태일 때의 닉네임 표시 정책은 기존 회원/캐릭터 응답 관례를 따른다.
    • 상대방 회원 또는 AI 캐릭터 프로필 이미지가 없으면 기본 이미지를 사용한다.
    • 통합 조회 시 도메인별 최신 메시지 후보를 조회한 뒤 공통 DTO로 변환하고, lastMessageAt 기준으로 병합 정렬한다.

참고 파일

  • src/main/kotlin/kr/co/vividnext/sodalive/message/Message.kt
  • src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt
  • src/main/kotlin/kr/co/vividnext/sodalive/chat/room/ChatRoom.kt
  • src/main/kotlin/kr/co/vividnext/sodalive/chat/room/service/ChatRoomService.kt
  • src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt
  • src/main/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationService.kt

MySQL 테이블 생성 SQL

CREATE TABLE user_creator_chat_room (
    id BIGINT NOT NULL AUTO_INCREMENT COMMENT '유저-크리에이터 채팅방 ID',
    is_active TINYINT(1) NOT NULL DEFAULT TRUE COMMENT '활성 여부',
    created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 일시',
    updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 일시',
    PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE user_creator_chat_participant (
    id BIGINT NOT NULL AUTO_INCREMENT COMMENT '유저-크리에이터 채팅방 참여자 ID',
    chat_room_id BIGINT NOT NULL COMMENT '유저-크리에이터 채팅방 ID',
    member_id BIGINT NOT NULL COMMENT '참여 회원 ID',
    is_active TINYINT(1) NOT NULL DEFAULT TRUE COMMENT '활성 여부',
    created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 일시',
    updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 일시',
    PRIMARY KEY (id),
    UNIQUE KEY uk_user_creator_chat_participant_room_member (chat_room_id, member_id),
    KEY idx_user_creator_chat_participant_member (member_id),
    CONSTRAINT fk_user_creator_chat_participant_room FOREIGN KEY (chat_room_id) REFERENCES user_creator_chat_room (id),
    CONSTRAINT fk_user_creator_chat_participant_member FOREIGN KEY (member_id) REFERENCES member (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE user_creator_chat_message (
    id BIGINT NOT NULL AUTO_INCREMENT COMMENT '유저-크리에이터 채팅 메시지 ID',
    chat_room_id BIGINT NOT NULL COMMENT '유저-크리에이터 채팅방 ID',
    participant_id BIGINT NOT NULL COMMENT '메시지 발신 참여자 ID',
    message_type VARCHAR(20) NOT NULL COMMENT '메시지 타입(TEXT, VOICE)',
    text_message TEXT NULL COMMENT '텍스트 메시지 본문',
    voice_message VARCHAR(1024) NULL COMMENT '음성 메시지 파일 경로',
    is_active TINYINT(1) NOT NULL DEFAULT TRUE COMMENT '활성 여부',
    created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 일시',
    updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 일시',
    PRIMARY KEY (id),
    KEY idx_user_creator_chat_message_room_id_id (chat_room_id, id),
    KEY idx_user_creator_chat_message_participant (participant_id),
    CONSTRAINT fk_user_creator_chat_message_room FOREIGN KEY (chat_room_id) REFERENCES user_creator_chat_room (id),
    CONSTRAINT fk_user_creator_chat_message_participant FOREIGN KEY (participant_id) REFERENCES user_creator_chat_participant (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

클라이언트 연동 프롬프트

신규 유저-크리에이터 채팅방 API를 연동한다.

공통 조건:
- 모든 요청은 `Authorization: Bearer <accessToken>` 헤더를 포함한다.
- API prefix는 `/api/v2/user-creator-chat/rooms`를 사용한다.
- 메시지 타입은 `TEXT`, `VOICE`만 처리한다.
- 앱이 백그라운드로 전환되거나 채팅방 화면에서 나가면 `POST /{roomId}/events/disconnect`를 호출하고 SSE 연결을 종료한다.
- 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 }`
   - response data: `{ "roomId": number }`

2. 채팅방 화면 열기 및 최신 메시지 조회
   - `GET /api/v2/user-creator-chat/rooms/{roomId}/open?limit=20`
   - response data: `{ "roomId", "messages", "hasMore", "nextCursor" }`

3. 과거 메시지 조회
   - `GET /api/v2/user-creator-chat/rooms/{roomId}/messages?cursor={nextCursor}&limit=20`
   - response data: `{ "messages", "hasMore", "nextCursor" }`

4. SSE 연결
   - `GET /api/v2/user-creator-chat/rooms/{roomId}/events`
   - Accept: `text/event-stream`
   - 이벤트 이름 `message`를 수신하면 payload를 현재 채팅방 메시지 목록에 append한다.
   - 이벤트 이름 `connected`는 연결 확인용으로만 사용한다.

5. 텍스트 메시지 전송
   - `POST /api/v2/user-creator-chat/rooms/{roomId}/messages/text`
   - body: `{ "textMessage": string }`
   - response data: `{ "message", "deliveredRealtime", "pushSent" }`

6. 음성 메시지 전송
   - `POST /api/v2/user-creator-chat/rooms/{roomId}/messages/voice`
   - multipart/form-data
   - part `voiceMessageFile`: 음성 파일
   - part `request`: `{}` JSON 문자열
   - response data: `{ "message", "deliveredRealtime", "pushSent" }`

7. 실시간 연결 해제
   - `POST /api/v2/user-creator-chat/rooms/{roomId}/events/disconnect`
   - DB 참여자를 삭제하거나 비활성화하지 않고 SSE/presence 상태만 해제한다.

메시지 DTO 필드:
- `messageId`: number
- `messageType`: `TEXT` 또는 `VOICE`
- `mine`: boolean
- `createdAt`: epoch milliseconds
- `textMessage`: string 또는 null
- `voiceMessageUrl`: string 또는 null
- `senderId`: number
- `senderNickname`: string
- `senderProfileImageUrl`: string

채팅 리스트 API 응답 예시

{
  "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차 문서 작성

  • 무엇을: 유저-크리에이터 메시지 채팅방 개편 PRD와 계획 TASK 문서를 작성했다.
  • 왜: 구현 전에 PRD와 계획 TASK 문서를 작성하고, 기존 AI 채팅 도메인 재사용 여부를 코드 근거로 결정해야 하기 때문이다.
  • 어떻게: 기존 message, AI chat/room, fcm 도메인 파일과 문서 작성 규칙을 확인했다. 문서에는 기존 메시지를 별도 유지하고 신규 기능부터 채팅방으로 제공한다는 사용자 결정을 반영했다. 문서 내 미완성 표식 검색에서 추가 수정 필요 항목이 없음을 확인했고, ./gradlew tasks --all 실행 결과 BUILD SUCCESSFUL을 확인했다.

2차 피드백 반영

  • 무엇을: 실시간 전송 방식을 SSE로 확정하고, 앱 백그라운드/채팅방 화면 이탈 시 presence 해제, typing indicator 제거, 실시간 전송 실패 시 보완 푸시 미발송, AI chat/room 엔티티 직접 재활용 불가 판단을 문서에 반영했다.
  • 왜: 사용자 피드백에서 신규 요구사항과 도메인 재활용 질문의 의도가 명확해졌고, 현재 코드베이스의 의존성 기준으로 WebSocket보다 SSE가 더 작은 변경이기 때문이다.
  • 어떻게: build.gradle.kts의 의존성과 실시간 관련 구현 검색 결과를 확인했고, ChatRoom, ChatParticipant, ChatMessage 엔티티 필드가 유저-크리에이터 채팅 의미와 맞지 않는 점을 기준으로 문서를 수정했다. 문서 내 미완성 표식과 남은 미결정 표현 검색에서 추가 수정 필요 항목이 없음을 확인했고, Markdown diagnostics는 두 문서 모두 No diagnostics found, ./gradlew tasks --allBUILD SUCCESSFUL이었다.

3차 API/SSE 정책 보강

  • 무엇을: 신규 채팅방 API URL prefix와 응답 DTO 필드명은 구현 직전 추천 후 확정하는 절차를 추가하고, SSE 인증 방식과 클라이언트 재연결 간격 추천안을 확정해 문서에 반영했다.
  • 왜: 구현 전에 API 계약을 한 번 더 확정하되, SSE 운영 정책은 계획 단계에서 명확히 고정해야 하기 때문이다.
  • 어떻게: URL prefix 추천안은 /api/v2/user-creator-chat/rooms, SSE 인증은 Authorization: Bearer <accessToken> 헤더 우선, 헤더 제한 시 단기 수명 SSE 전용 토큰 대안, 재연결은 기본 3초 및 최대 30초 지수 백오프로 정리했다.

4차 구현

  • 무엇을: kr.co.vividnext.sodalive.v2.usercreatorchat 신규 패키지 아래에 신규 엔티티, 리포지토리, 서비스, 컨트롤러, DTO, SSE presence 서비스를 추가했다.
  • 왜: 기존 기능과 분리된 신규 버전 API로 개발하고, 기존 메시지/AI 채팅 도메인은 참고만 하기로 했기 때문이다.
  • 어떻게: 실패 테스트를 먼저 작성해 신규 클래스 부재로 실패하는 것을 확인한 뒤 최소 구현을 추가했다. 이후 ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest', ./gradlew ktlintCheck, ./gradlew build, 단독 ./gradlew test 실행 결과 모두 BUILD SUCCESSFUL을 확인했다. Kotlin LSP는 이 환경에 서버가 없어 diagnostics를 수행할 수 없었고, Markdown diagnostics는 No diagnostics found였다. 신규 엔티티 기준 MySQL 테이블 생성 SQL과 클라이언트 연동 프롬프트를 이 문서에 추가했다.

5차 SQL 보완

  • 무엇을: MySQL 테이블 생성 SQL의 created_at, updated_at 타입을 TIMESTAMP 기본값/자동 갱신 형식으로 변경하고 모든 컬럼에 COMMENT를 추가했다.
  • 왜: 실제 MySQL DDL에서 생성/수정 시간 기본 동작과 컬럼 설명을 명확히 남기기 위해서다.
  • 어떻게: SQL 블록의 세 테이블 컬럼 정의를 직접 수정했다.

6차 SQL 컬럼 순서 및 Boolean 보완

  • 무엇을: MySQL 테이블 생성 SQL에서 created_at, updated_at을 각 테이블 컬럼 목록의 마지막으로 이동하고, is_activeTINYINT(1) NOT NULL DEFAULT TRUE로 변경했다.
  • 왜: 공통 시간 컬럼 위치와 boolean 기본값 표기를 일관되게 맞추기 위해서다.
  • 어떻게: SQL 블록의 세 테이블 컬럼 순서와 is_active 타입/기본값, 활성 방 생성 컬럼 조건식을 수정했다.

7차 이전 메시지 조회 테스트 보강

  • 무엇을: 유저-크리에이터 채팅방의 cursor 기반 이전 메시지 조회 테스트를 추가했다.
  • 왜: 방 내부에서 이전 채팅을 조회할 수 있는지 검증하고, 기본 조회 개수 20개가 repository PageRequest에 적용되는지 확인하기 위해서다.
  • 어떻게: UserCreatorChatServiceTestcursor가 있을 때 findByChatRoomAndIdLessThanAndIsActiveTrueOrderByIdDescPageRequest.of(0, 20)으로 호출되고, 응답의 messages, hasMore, nextCursor가 기대대로 반환되는지 검증하는 테스트를 추가했다. ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest' 실행 결과 BUILD SUCCESSFUL을 확인했다.

8차 참여자 모델 단순화

  • 무엇을: UserCreatorChatRoomuser, creator 고정 참여자 컬럼과 UserCreatorChatParticipantRole을 제거하고, 참여자는 UserCreatorChatParticipant만 기준으로 관리하도록 수정했다.
  • 왜: 현재 1:1 방에서는 별도 참여자 권한이 필요하지 않고, 향후 user-to-user 채팅 확장을 막지 않도록 방 엔티티에서 특정 참여자 역할을 고정하지 않기 위해서다.
  • 어떻게: 테스트를 먼저 새 구조 기준으로 바꿔 컴파일 실패를 확인한 뒤, 방 중복 조회를 participants 기준 쿼리로 변경하고 엔티티/서비스/테스트를 수정했다. 아직 테이블을 생성하지 않았으므로 MySQL CREATE TABLE 문 자체에서 user_id, creator_id, active_room_key, participant role 컬럼을 제거했다.

9차 API 의미 명확화

  • 무엇을: 실제 채팅방 탈퇴로 오해될 수 있는 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 조건도 같은 정렬 기준에 맞췄다. lastMessageAtZoneOffset.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 결함 없음 승인을 받았다.