[BE 테크스펙] 채팅 도메인 - 100-hours-a-week/KTB4-3rd-wiki GitHub Wiki

채팅·신고 도메인 테크 스펙

목차

  1. 배경
  2. 목표가 아닌 것
  3. 설계 및 기술 자료

배경 (Background)

프로젝트 목표 (Objective)

매칭 성사 후 참여자 간 원활한 소통을 지원하고, 문제가 되는 메시지나 사용자를 신고할 수 있는 안전장치를 제공한다.

핵심 결과 (Key Results)

  • 네트워크 재시도로 인한 메시지 중복 전송이 발생하지 않는다.
  • 채팅방 참여자가 아닌 사용자에게 채팅방의 존재 여부나 내용이 노출되지 않는다.
  • 신고 접수 시 중복 신고 없이 정확히 한 건으로 기록된다.

문제 정의 (Problem)

  • 모바일 네트워크 특성상 메시지 전송 요청이 재시도될 수 있다. 멱등성 처리가 없으면 같은 메시지가 중복으로 저장될 수 있다.
  • 채팅방은 매칭 참여자만 접근해야 하는 민감한 공간이다. 권한 검증이 허술하면 비참여자에게 대화 내용이나 참여자 정보가 노출될 위험이 있다.

가설 (Hypothesis)

메시지 전송에 멱등키를 도입하고, 채팅방 접근 권한을 존재 자체를 숨기는 404 Not Found 방식으로 엄격히 검증한다.

이를 통해 중복 전송과 정보 노출 문제를 구조적으로 방지할 수 있을 것이다.

관련 자료


목표가 아닌 것 (Non-goals)

이번 프로젝트에서 다루지 않는 내용:

  • 현재 명시된 항목 없음

설계 및 기술 자료 (Architecture and Technical Documentation)

데이터베이스 스키마 (ERD)

테이블 주요 컬럼 및 역할
chat_rooms id, companion_id, closed_at, created_at, last_message_id
messages id, room_id, sender_id, client_message_id, type, content, created_at
companion_participants last_read_message_id를 통해 참여자별 읽음 위치 추적
reports id, reporter_id, reported_user_id, reported_message_id, reason, reason_text, created_at
  • 메시지 타입: TEXT, SYSTEM_JOIN, SYSTEM_LEAVE
  • reported_message_id: nullable
  • 신고 사유: ABUSE, UNSETTLED, NO_SHOW, ETC

API 명세 (API Specifications)

  • 목차:
    • 내 채팅방 목록 조회 API
    • 채팅방 상세 조회 API
    • 채팅 메시지 목록 조회 API
    • 채팅 메시지 전송 API
    • 메시지 읽음 처리 API
    • 메시지 신고하기 API

내 채팅방 목록 조회 API

  • API 명세:
    • GET /chat-rooms?cursor={cursor}
    • 쿼리 파라미터: cursor (페이지네이션 커서, 선택)
    • API 문서
  • 권한:
    • 로그인한 사용자(본인이 참여 중인 방만)
  • 구현 상세:
    • 응답 예시:

      {
        "message": "조회에 성공했습니다",
        "data": {
          "items": [
            {
              "id": 501,
              "companion_id": 10,
              "kind": "TAXI_POT",
              "title": "8시 판교역",
              "host": {
                "profile_image_url": null
              },
              "current_count": 3,
              "capacity": 4,
              "has_unread": true
            }
          ],
          "next_cursor": "v1.eyJsYXN0X21lc3NhZ2VfaWQiOjE0NTJ9"
        }
      }
      
    • companion_participants에서 내가 참여 중인 방을 chat_rooms와 조인해 목록을 뽑고, companions/users/messages는 화면에 보여줄 세부 내용을 채우기 위해 추가로 조인한다.

    • 정렬: chat_rooms.last_message_id가 가리키는 메시지의 created_at 기준 최신순 (id 크기가 아니라 실제 시각 기준)

    • 커서 페이지네이션: Base64로 인코딩한 JSON을 사용한다. created_at을 기준으로 {"last_message_at": ..., "room_id": ...} 형태의 튜플을 비교하며, 동점 처리를 위해 room_id를 포함한다. 첫 페이지에서는 cursor를 생략한다.

    • 현재 응답에는 읽지 않은 메시지의 존재 여부만 필요하므로 has_unread 비교로 처리한다. 읽지 않은 메시지 개수는 후속 개선 사항이다.

    • 확인 필요: 가장 최근 메시지를 목록의 미리보기 데이터로 포함할지 결정 필요

채팅방 상세 조회 API

  • API 명세:
  • 권한: 로그인한 사용자(해당 방의 참여자만)
  • 구현 상세:
    • 응답 예시:

      {
        "message": "조회에 성공했습니다",
        "data": {
          "id": 501,
          "companion_id": 10,
          "kind": "TAXI_POT",
          "title": "8시 판교역",
          "host_id": 7,
          "origin_name": "판교역",
          "dest_name": "강남역",
          "departure_at": "2026-09-05T08:30:00.000Z",
          "current_count": 3,
          "capacity": 4,
          "companion_status": "IN_PROGRESS",
          "closed_at": null,
          "last_read_message_id": 1440
        }
      }
      
    • 참여 여부 우선 확인: companion_participants에서 (room_id, 인증 사용자) 조합의 존재 여부를 먼저 확인한다. 없으면 방의 실제 존재 여부를 노출하지 않도록 즉시 404 Not Found를 반환한다.

    • 참여자로 확인되면 chat_rooms, companions, companion_participants의 본인 행을 JOIN한 DTO Projection으로 응답 데이터를 조회한다.

      • title은 DB 컬럼이 아니라 origin_name + departure_at(시각)을 조합해 생성하는 파생 필드

채팅 메시지 목록 조회 API

  • API 명세:
    • GET /chat-rooms/{room_id}/messages
    • 쿼리 파라미터: cursor (페이지네이션 커서, 선택)
    • API 문서
  • 권한: 로그인한 사용자(해당 방의 참여자만)
  • 구현 상세:
    • 응답 예시:

      {
        "message": "조회에 성공했습니다",
        "data": {
          "items": [
            {
              "id": 1452,
              "type": "SYSTEM_RIDE_START_REQUESTED",
              "content": null,
              "created_at": "2026-09-05T07:58:12.000Z"
            },
            {
              "id": 1441,
              "type": "TEXT",
              "sender": {
                "id": 7,
                "nickname": "우림",
                "profile_image_url": null
              },
              "content": "3분 뒤 도착합니다",
              "created_at": "2026-09-05T07:41:12.000Z"
            },
            {
              "id": 1440,
              "type": "SYSTEM_JOIN",
              "sender": null,
              "content": "",
              "created_at": "2026-09-05T07:40:00.000Z"
            }
          ],
          "next_cursor": "v1.eyJsYXN0X2lkIjoxNDQwfQ"
        }
      }
      
    • 채팅방 참여자만 메시지를 조회할 수 있다. 비참여자에게는 403 Forbidden이 아니라 404 Not Found를 반환하여 채팅방의 존재를 숨긴다.

    • sender.nicknamesender.profile_image_url은 조회 시점의 사용자 정보를 JOIN하여 가져온다. 따라서 닉네임 변경이나 탈퇴 익명화가 과거 메시지 표시에도 반영된다.

    • 정렬: id 내림차순

    • 페이지네이션: 커서 기반

    • 메시지 타입에 따라 시스템 메시지와 일반 메시지를 구분하여 DTO에 담는다.

  • 전송 채널: 현재 REST API 설계와 WebSocket 전송은 분리하여 다룬다.

채팅 메시지 전송 API

  • API 명세:
  • 권한: 로그인한 사용자(해당 방의 참여자만)
  • 구현 상세:
    • 요청 DTO: content, clientMessageId
      • content: @NotBlank 및 길이 제한
      • clientMessageId: @NotBlank 및 UUID 형식 @Pattern
    • 종료된 채팅방은 chat_rooms.closed_at IS NOT NULL 조건으로 판별하고 409 ROOM_CLOSED를 반환한다.
    • client_message_id로 기존 메시지를 조회한다.
      • UUID가 존재하고 content가 같으면 새 메시지를 만들지 않고 기존 메시지를 200 OK로 반환한다.
      • UUID가 존재하지만 content가 다르면 409 CLIENT_MESSAGE_ID_REUSED를 반환한다.
      • UUID가 없으면 메시지를 추가하고 201 Created를 반환한다. 이후 chat_rooms.last_message_id를 갱신한다.
  • WebSocket 전송: 트랜잭션 커밋 후 저장된 메시지를 해당 채팅방 구독자 전체에 Push한다.
  • 트랜잭션 관리: messages Insert와 chat_rooms.last_message_id 갱신을 같은 트랜잭션으로 묶는다.
  • 멱등성: client_message_id에 DB Unique 제약을 적용하여 동시 재시도로 인한 중복 Insert를 DB 수준에서 방지한다.

메시지 읽음 처리 API

  • API 명세:
    • PUT /chat-rooms/{room_id}/read-marker
    • API 문서
  • 권한: 로그인한 사용자(해당 방의 참여자만)
  • 구현 상세:
    • 요청 DTO: last_read_message_id@NotNull 적용
    • companion_participants.last_read_message_id를 요청값으로 갱신한다.
    • 저장된 값보다 작은 과거 시점의 값이 들어오면 역행 요청으로 판단하여 갱신하지 않고 기존 값을 반환한다.

[신고] 메시지 신고하기 API

  • API 명세:
  • 권한: 로그인한 사용자(해당 방의 참여자만)
  • 구현 상세:
    • 요청 예시:

      {
        "reported_user_id": 7,
        "reported_message_id": 1441,
        "reason": "ABUSE",
        "reason_text": null
      }
      
    • 요청 DTO: ReportCreateRequest(reportedUserId, reportedMessageId, reason, reasonText)

      • reportedUserId, reason: @NotNull
      • reportedMessageId: 이번 메시지 신고 범위에서 필수
      • reason=ETC이면 reasonText 필수. 교차 필드 검증은 서비스 계층에서 처리한다.
    • reporter_id는 Access Token에서 서버가 추출하며 클라이언트로부터 받지 않는다.

    • 신고 대상 메시지의 존재 여부와 신고자가 해당 채팅방의 참여자인지 확인한다.

    • 동일 메시지를 중복 신고하면 409 DUPLICATE_REPORT를 반환한다. (reporter_id, reported_message_id)에는 Unique 제약을 적용한다.