채팅(1:1) 도메인 테크 스펙 - 100-hours-a-week/9-team-Devths-WIKI GitHub Wiki

1️⃣ 배경

프로젝트 목표

  • Devths의 채팅 도메인(V1)은 사용자가 팔로잉 관계의 상대와 1:1 대화를 빠르게 시작하고, 대화 내역을 안정적으로 조회/전송하며, 읽지 않은 메시지를 놓치지 않도록 하는 것을 목표로 함
  • 핵심 결과 (Key Result) 1:
    • 사용자가 채팅방 생성 → 상세 진입 → 메시지 전송까지 끊김 없이 완료(에러/무한 로딩/라우팅 실패 0% 지향)
  • 핵심 결과 (Key Result) 2:
    • 채팅방 목록에서 최신 메시지/시간/안읽음 표시가 정확히 동작(정렬 오류/표기 오류 최소화)
  • 핵심 결과 (Key Result) 3:
    • 실시간(WebSocket/STOMP)과 히스토리(REST) 결합 구조를 표준화해서 유지보수 가능한 구조 확보
    • 구독/해제 누락, 중복 연결, 스크롤 점프 등 채팅 특유 버그 방지

문제 정의 (Problem)

  • 채팅은 목록/상세/설정/생성 화면이 분리되어 있고, REST(과거 조회) + WebSocket(실시간)을 같이 다뤄야 해서 상태가 복잡해지기 쉬움
  • 무한 스크롤(목록/메시지) + 정렬
  • 방 진입 시점에 히스토리 로드 + 실시간 구독 순서
  • 전송 실패 시 UI(재전송/삭제)처리

가설 (Hypothesis)

  • 서버 상태(채팅방 목록/메시지 히스토리)는 TanStack Query로 관리함
    • 캐싱/무한스크롤/리패치 같은 서버 데이터 관리를 라이브러리가 표준 방식으로 처리해줘서, 목록 정렬/페이지네이션 버그를 줄일 수 있음
  • 실시간 연결/구독 상태는 WebSocket(STOMP) 매니저를 별도 모듈로 분리해서 관리함
    • 채팅방 이동 시 구독 해제 누락, 중복 연결로 인한 메시지 중복 수신 같은 실시간 특유 버그를 방지할 수 있음
  • UI 상태(모달 열림/닫힘, 선택된 유저/첨부 파일, 입력값)는 Zustand 또는 로컬 상태(usState/userReducer)로 관리함
    • 서버 데이터와 섞이지 않아서 화면 로직이 단순해지고, 모달/입력 상태가 리렌더링이나 캐시 업데이트에 흔들리지 않음

⇒ 채팅 기능을 안정적으로 구현하고 버그를 줄일 수 있음

관련 자료 (References)

  • 백엔드 API 명세
  • 화면 설계서

2️⃣ 목표가 아닌 것 (Non-goals)

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

  • 그룹 채팅
  • 음성/영상 통화
  • 파일 첨부 확장(docx, zip 등) / 대용량 업로드 최적화

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

아키텍처 개요 (Architecture Overview)

채팅 도메인은 REST(과거 데이터)와 WebSocket(실시간 데이터)를 함께 사용하기 때문에, 데이터 흐름을 아래 3가지로 분리함

(1) 채팅방 목록 (REST)

  • 채팅방 목록은 서버가 가진 채팅방 리스트이므로 TanStack Query(React Query)로 조회/캐싱함
  • 무한 스크롤(10개 단위)과 최신 메시지 기준 정렬을 React Query의 useInfiniteQuery로 안정적으로 관리

(2) 채팅방 메시지 히스토리 (REST)

  • 채팅방 진입 시, 서버에서 최근 메시지 10개를 먼저 조회해 화면을 구성함
  • 사용자가 스크롤을 위로 올릴 때마다 lastId(또는 cursor)를 이용해 이전 메시지 페이지를 추가 로드함
  • 이때 새로 받은 메시지는 기존 목록의 앞쪽(prepend)에 붙여서 과거로 확장되는 무한 스크롤을 구현함

(3) 실시간 메시지 (WebSocket / STOMP)

  • 채팅방에 들어가면 해당 roomId에 대해 subscribe를 수행하고, 서버가 푸시하는 새 메시지를 수신함

  • 수신된 메시지는 화면의 메시지 리스트에 append하여 즉시 반영함

  • 연결/구독/해제(재연결 포함)는 페이지에서 직접 다루지 않고, WebSocket(STOMP) 매니저(싱글톤)에서 통합 관리한다.

    → 방 이동 시 중복 구독, 재진입 시 중복 연결 같은 실시간 채팅의 대표 버그를 예방하기 위함

4️⃣ 주요 페이지/컴포넌트 구조

페이지

(1) ChatRoomListPage : 채팅 생성 페이지

  • 주요 기능
    • 1:1 채팅방 목록 조회 (10개 단위 무한 스크롤)
    • 최신 메시지 기준 정렬 (최근 메시지 시간이 최신인 방이 상단)
    • 안 읽음 표시 (읽지 않은 메시지가 있으면 빨간 점 표시)
    • 채팅방 생성 페이지 이동
      • FloatingCreateChatButton(+) 클릭 → /chat/new 이동
    • 빈 상태 처리
      • 채팅방이 없으면 "새롭게 채팅을 시작해보세요" 문구 노출
  • 사용 컴포넌트
    • ChatRoomCategoryToggle
      • V1은 개인(PRIVATE) 고정이지만, 추후 확장 대비 UI 유지
    • ChatRoomList
    • ChatRoomListItemCard
    • FloatingCreateChatButton
    • EmptyState
  • 데이터 로딩 시점
    • 페이지 진입 즉시 GET /chatrooms 첫 페이지 요청
    • 스크롤 하단 도달 시 다음 페이지 요청 (cursor/nextCursor 기반)
  • 라우팅: /chat

(2) ChatCreatePage : 채팅 생성 페이지 (1:1 전용)

  • 주요 기능
    • 팔로잉 유저 검색(닉네임 기반)
      • 검색어 입력 후 엔터 또는 검색 버튼 클릭 시에만 검색 실행(입력 중 자동 검색 X)
    • 팔로잉 유저 목록 조회(무한 스크롤)
      • 기본 목록은 10명 단위로 로드
      • 정렬: 이름 순
    • 초대 유저 선택(1명 고정)
      • 1:1 채팅이므로 동시에 1명만 선택 가능
      • 선택한 유저가 1명일 때만 완료 버튼 활성화
    • 완료 → 채팅방 생성 또는 기존 방으로 이동
      • 완료 클릭 시 POST /chatrooms (type=PRIVATE, userIds=[상대])
      • 이미 존재하는 1:1 채팅방이면 서버가 반환한 roomId로 기존 방 이동
      • 성공 토스트/완료 모달 노출 후 1초 뒤 /chat/{roomId}로 이동
  • 사용 컴포넌트
    • UserSearchBar
    • FollowingUserList
    • UserSelectRow
    • PrimaryButton (완료)
    • Toast / AlertModal
  • 입력값 검증 및 헬퍼 텍스트(검색)
    • 검색어 미입력(빈 값/공백만) → "검색어를 입력해 주세요." (검색 실행 X)
    • 2자 미만 → "검색어는 2자 이상 입력해 주세요." (검색 실행 X)
    • 10자 초과 → 추가 입력 제한 또는 "검색어는 최대 10자까지 입력할 수 있습니다."
  • 검색 결과 표시
    • 결과가 없으면: "해당하는 유저가 없습니다."
    • 입력 중 검색어는 화면 전환 없이 유지
  • 예외 처리(팔로잉 0명)
    • "아직 팔로우 하고 있는 유저가 없습니다" 문구 노출
  • 유효성 검사 실패 토스트
    • 1:1에서 1명 초과 선택 시: "개인 채팅방은 1인 필수 선택입니다."
    • (그룹 관련 문구는 V1 범위에서 제거)
  • 데이터 로딩 시점
    • 페이지 진입 즉시 팔로잉 목록 첫 페이지 로드
    • 스크롤 하단 도달 시 다음 페이지 로드
    • 검색은 엔터/버튼 클릭 시 실행
  • 라우팅: /chat/new

(3) ChatRoomDetailPage : 채팅방 상세 페이지 (1:1)

  • 주요 기능

    • 과거 메시지 로드(REST) + 무한 스크롤
      • 진입 시 최근 메시지 10개를 먼저 조회
      • 사용자가 스크롤을 위로 올리면 lastId(cursor)를 이용해 이전 메시지 페이지를 추가 로드
      • 추가 로드된 메시지는 리스트 앞쪽(prepend)에 붙여서 과거로 확장되는 구조
    • 실시간 송수신(WebSocket/STOMP)
      • 방 진입 후 해당 roomId를 subscribe
      • 새 메시지 수신 시 메시지 리스트에 즉시 append
    • 메시지 전송(TEXT / IMAGE)
      • TEXT: 입력값 전송
      • IMAGE: 이미지 첨부 후 전송(썸네일 표시 → 클릭 시 원본 뷰어)
    • 메시지 삭제
      • 메시지 2초 롱프레스 → 삭제 확인 모달 → 삭제 시 메시지 내용이 "삭제된 메시지"로 변경
    • 읽지 않은 메시지 구분선
      • 읽지 않은 메시지가 있는 상태로 입장하면 마지막 읽은 지점에 "여기까지 읽었습니다" 구분선 표시 + 해당 지점으로 스크롤 이동
      • 채팅방에 들어갈 때 모든 메시지를 읽음 처리
    • 실패 메시지 처리
      • 네트워크/서버 문제로 전송 실패 시 메시지를 실패 상태로 보관
      • 사용자는 해당 메시지에 대해 재전송 또는 삭제 가능
      • 서버 오류는 최대 3회 재시도 후 실패로 확정
  • 사용 컴포넌트

    • ChatHeader (뒤로가기 / 타이틀 / 설정)
    • StickyDateLabel (상단 고정 날짜 라벨: 오늘/날짜 포맷)
    • MessageList
    • MessageBubble / ImageMessageBubble
    • ExpandToggle (장문 300자 초과 시 펼치기/접기)
    • UnreadDivider (“여기까지 읽었습니다”)
    • ChatComposer (입력 / 첨부 / 전송)
    • AttachmentSheet (이미지/파일 선택 모달)
    • ConfirmModal (삭제 확인)
  • 데이터 로딩 시점

    • 진입 즉시 히스토리 1페이지(최근 10개) 로드(REST)
    • 히스토리 로드 성공 후 WebSocket 연결 + 해당 room subscribe
    • 이후 수신되는 실시간 메시지는 append
  • 첨부 유효성 검증

    • 이미지:
      • 한 번에 최대 9장
      • 형식: JPG/JPEG/PNG(요구사항에 webp가 있으면 프로젝트 정책으로 포함 가능)
      • 용량: 5MB 제한
    • 파일:
      • 한 번에 최대 1개
      • 형식: PDF
      • 용량: 5MB 제한
    • 실패 시: 유효성 검사 모달 또는 Alert 노출
  • 스크롤 UX 구현

    • 이전 메시지를 prepend 하면 화면이 점프할 수 있음
    • 따라서:
      • prepend 전 prevScrollHeight 저장

      • prepend 후 newScrollHeight 계산

      • scrollTop += (newScrollHeight - prevScrollHeight)로 보정

        → 사용자가 읽던 위치가 유지돼서 UX가 안정적임

  • 라우팅: /chat/[roomId]

(4) ChatRoomSettingsPage : 채팅 상세 설정 페이지 (1:1)

  • 주요 기능
    • 알림 토글 ON/OFF 저장
      • 기본값: ON
      • 토글 변경 시 즉시 서버에 저장하여 재접속 시 유지
      • OFF: 해당 채팅방 알림 미수신 / ON: 다시 수신
    • 채팅방 이름 개인화 수정(모달)
      • 사용자가 나만 보이는 채팅방 이름으로 변경 가능
      • 수정 완료 시 채팅방 상단 타이틀/목록 카드에도 반영
    • 최근 이미지 미리보기 4개
      • 채팅방에서 공유된 이미지 중 최근 4개 썸네일 표시
      • 썸네일 클릭 시 원본 이미지 뷰어로 확대
    • 참여자 목록
      • 1:1 전용: 본인 + 상대 2명만 노출
      • 유저 클릭 시 유저 조회 모달/프로필 모달로 연결 가능(요구사항에 맞춰 선택)
    • 나가기(모달 확인)
      • 나가기 버튼 클릭 → 확인 모달 → 확인 시 채팅방 퇴장 처리
      • 내 채팅 목록에서만 제거되고 상대방은 유지(요구사항 정책)
  • 사용 컴포넌트
    • AlarmToggleRow
    • EditTitleRow + EditTitleModal
    • RecentImagesPreview
    • ParticipantList
    • DangerButton (나가기) + ConfirmModal
  • 입력값 검증 및 예외 처리
    • 채팅방 이름 수정(모달)
      • 최대 10자
      • 공백만 입력 불가
      • 10자 초과는 추가 입력 제한 또는 helper text 처리
    • 최근 이미지 미리보기
      • 이미지가 1개도 없으면 해당 섹션 숨김 또는 “공유된 이미지가 없습니다” 표시(UX 정책 중 택1)
    • 상대가 나간/탈퇴 등으로 정보가 없을 때
      • 상대 닉네임/프로필은 (알 수 없음) 또는 기본 UI로 표시
  • API 연동
    • 알림 토글 저장
      • PATCH /api/chatrooms/{roomId}
      • Body: { "isAlarmOn": true | false }
    • 채팅방 이름 개인화 수정
      • PUT /api/chatrooms/{roomId}
      • Body: { "roomName": "이직 준비방" }
    • 이미지 조회 (최근 이미지 미리보기 4개)
      • 백엔드 문서에 최근 4개 전용 필드는 보이지 않고, 대신 이미지 목록 조회 API가 제공됨
        • GET /api/chatrooms/{roomId}/images?size=n&lastId=k
        • 여기서 프론트는 size=4로 호출해서 최근 4개 썸네일로 쓰면 됨
      • GET /api/chatrooms/{roomId}/images?size=4
    • 채팅방 나가기(퇴장)
      • DELETE /api/chatrooms/{roomId}
      • Header: Authorization: Bearer {accessToken}
  • 데이터 로딩 시점
    • 진입 시 채팅방 설정 정보 로드
      • 서버에서 설정 조회 API가 없으면:
        • 기존 room 정보 + 히스토리 일부를 기반으로 화면 구성(대체 플로우)
  • 라우팅: /chat/[roomId]/settings

컴포넌트 핵심 흐름

주요 컴포넌트

  • [Page]: 라우트에 매핑되는 페이지 컴포넌트
  • [Container]: 데이터/이벤트를 묶는 상위 컴포넌트(페이지 안에서 분리 가능)
  • [UI]: 순수 UI 표시 컴포넌트
  • [Modal/Sheet]: 모달/바텀시트/뷰어
  • [Common]: 채팅 도메인이 “사용”하지만 공통 도메인에 있는 재사용 컴포넌트(이름 예시)

채팅방 목록 (/chat) 주요 컴포넌트

(1) ChatRoomListPage [Page]

  • 역할
    • /chat 라우트에 매핑되는 페이지 엔트리
    • 목록 화면의 큰 UI 구조 조립(토글/리스트/플로팅 버튼)
    • “상세 이동” 및 “생성 페이지 이동” 라우팅 연결
  • 디자인/동작 핵심
    • 상단: ChatRoomCategoryToggle (V1에서는 개인 고정)
    • 본문: ChatRoomList로 목록 렌더
    • 우하단: FloatingCreateChatButton으로 /chat/new 진입
  • Props & Interface
    • 보통 Page는 외부 props 없음(라우트 params도 없음)
type ChatRoomListPageProps = {};
  • Props 설명
    • 없음(데이터는 Container/Query 훅에서 가져옴)
  • 내부 상태 & 이벤트
    • 내부 상태는 최소화(상태는 Container/React Query로 위임)
    • 이벤트 흐름
      • 페이지 진입 → 목록 첫 페이지 요청 트리거(컨테이너/훅)
      • 카드 클릭 → router.push(/chat/${roomId})
        • 버튼 클릭 → router.push('/chat/new')
  • 에러 처리 / 엣지 케이스
    • 목록 조회 실패/로딩/빈 상태는 ChatRoomList가 담당(페이지는 조립만)
    • 라우팅 실패는 거의 없지만, roomId가 잘못되면 상세 페이지에서 처리
  • 스타일링
    • 스크롤 영역은 리스트에 위임
    • 플로팅 버튼은 Safe Area/BottomNav 영역을 피해서 배치
  • Storybook
    • Page는 스토리 생략

(2) ChatRoomListContainer [Container]

  • 역할
    • 채팅방 목록을 서버 상태로 관리(React Query)
    • GET /chatrooms 기반 무한 스크롤(10개씩) 로딩 상태를 캡슐화
    • UI 컴포넌트에는 데이터/핸들러만 내려준다
  • 디자인/동작 핵심
    • 최초 진입 시 1페이지 로드
    • 하단 도달 시 다음 페이지 로드
    • 최신 메시지 기준 정렬은 서버 응답을 신뢰
      • 서버가 정렬 보장 안 하면 프론트에서 정렬
  • props & Interface
type ChatRoomSummary = {
  roomId: number;
  title: string;
  recentMessage: string | null;
  recentMessageAt: string; // ISO (권장) 또는 서버 포맷 문자열
  hasUnread: boolean;
  thumbnailUrl: string | null;
};

type ChatRoomListVM = {
  items: ChatRoomSummary[];
  isLoading: boolean;
  isError: boolean;
  error: unknown;

  hasNext: boolean;
  isFetchingNext: boolean;
  fetchNext: () => void;

  refetch: () => void;
};

type ChatRoomListContainerProps = {
  children: (vm: ChatRoomListVM) => React.ReactNode;
};
  • Props 설명
    • children(vm)
      • UI에 필요한 상태/데이터를 한 번에 전달해서, 페이지는 뷰만 구성
  • 내부 상태 & 이벤트
    • 내부 상태는 React Query가 대부분 담당
      • data.pages(infinite)
      • isLoading, isError, isFetchingNextPage, hasNextPage
    • 이벤트 흐름
      • 최초 마운트 → 첫 페이지 요청
      • fetchNext() 호출 → 다음 페이지 요청
      • refetch() 호출 → 전체 다시 로드
  • 에러 처리 / 엣지 케이스
    • 중복 데이터 방지
      • 서버가 중복 아이템을 내려줄 가능성 대비해서 roomId 기준 dedupe 가능
    • 다음 페이지 로딩 중 연속 트리거 방지
      • isFetchingNext일 때 fetchNext() 중복 호출 방지
    • 캐시 갱신 포인트
      • 채팅방 나가기/생성/최근 메시지 업데이트 발생 시 invalidateQueries(['chatrooms'])
  • 스타일링
    • 없음(로직 전용)
  • Storybook
    • 스토리보다는 테스트/MSW로 검증 권장

(3) ChatRoomList [UI]

  • 역할
    • ChatRoomSummary[]를 받아서 목록 UI로 렌더
    • 로딩/에러/빈/추가로딩 상태를 분기해서 안정적인 UX 제공
    • 하단 도달 시 onEndReached로 다음 페이지 로드 트리거
  • 디자인/동작 핵심
    • 초기 로딩: SkeletonList
    • 에러: ErrorState + RetryButton
    • 빈 상태: EmptyState
    • 다음 페이지 로딩: 리스트 하단 Spinner
  • props & Interface
type ChatRoomListProps = {
  items: ChatRoomSummary[];

  isLoading: boolean;        // 첫 로딩
  isError: boolean;
  error?: unknown;

  hasNext: boolean;
  isFetchingNext: boolean;   // 다음 페이지 로딩

  onEndReached: () => void;  // 하단 도달 시 호출
  onRetry: () => void;       // 에러 시 재시도
  onClickItem: (roomId: number) => void;

  emptyTitle?: string;       // (선택) 빈 상태 문구 커스터마이즈
};
  • Props 설명
    • items: 렌더할 채팅방 카드 목록
    • isLoading: 첫 페이지 로딩 여부(스켈레톤 노출 기준)
    • hasNext/isFetchingNext: 무한스크롤 상태
    • onEndReached: 하단 센티넬 감지 시 호출
  • 내부 상태 & 이벤트
    • 내부 상태
      • observerAttached(센티넬 연결 여부)
    • 이벤트 흐름
      • 리스트 하단 센티넬이 뷰포트에 들어옴
      • hasNext && !isFetchingNext이면 onEndReached() 호출
      • 카드 클릭 → onClickItem(roomId)
  • 에러 처리 / 엣지 케이스
    • isLoading && items.length === 0 → SkeletonList
    • isError → ErrorState + RetryButton
    • !isLoading && items.length === 0 → EmptyState
    • hasNext=false면 센티넬 비활성화(추가 호출 방지)
  • 스타일링
    • 카드 간격/패딩 통일
    • 스크롤 바/모바일 safe-area 고려
  • Storybook
    • LoadingInitial
    • Loaded
    • Empty
    • Error
    • FetchingNext

(4) ChatRoomListItemCard [UI]

  • 역할
    • 목록에서 “채팅방 1개”를 한 줄 카드로 표시
    • 사용자가 클릭하면 상세로 들어가는 진입 포인트
  • 디자인/동작 핵심
    • 썸네일(상대 프로필)
    • 제목(최대 6자, 초과 시 …)
    • 최신 메시지
    • 시간(오늘/어제/날짜 규칙)
    • 안읽음 빨간 점
  • props & Interface
type ChatRoomListItemCardProps = {
  roomId: number;
  title: string;            // 6자 초과 ... 처리
  recentMessage: string;    // 없으면 " " 처리
  recentMessageAt: string;  // 포맷 규칙 적용(또는 ISO를 받아 내부에서 포맷)
  hasUnread: boolean;
  thumbnailUrl?: string | null;

  onClick: (roomId: number) => void;
};
  • Props 설명
    • recentMessageAt
      • (권장) ISO를 받고 내부에서 ChatRoomTimeLabel로 포맷
      • (현재 설계) 상위에서 이미 포맷된 string을 내려줘도 됨
    • hasUnread
      • true면 빨간 점 표시
  • 내부 상태 & 이벤트
    • 상태 없음(표시용)
    • 이벤트 흐름
      • 카드 클릭 → onClick(roomId)
        • 상위에서 router.push(/chat/${roomId})
  • 에러 처리 / 엣지 케이스
    • recentMessage 없으면 " "로 대체(레이아웃 유지)
    • thumbnailUrl null이면 기본 아바타(회색 원)
    • title 6자 초과 시 … 처리(공백 포함 기준)
  • 스타일링
    • 제목/메시지는 한 줄 ellipsis
    • 빨간 점은 시간 왼쪽
  • Storybook
    • Default
    • NoThumbnail
    • HasUnread
    • LongTitleEllipsis
    • NoRecentMessage

(5) FloatingCreateChatButton [UI]

  • 역할
    • 채팅 생성(/chat/new)으로 진입하는 플로팅 + 버튼
    • “새 채팅 시작” 흐름의 시작점
  • 디자인/동작 핵심
    • 화면 우하단 고정
    • BottomNav/세이프에어리어 위로 떠야 함
  • props & Interface
type FloatingCreateChatButtonProps = {
  onClick: () => void;
  disabled?: boolean;
};
  • Props 설명
    • disabled
      • 목록 로딩 중/권한 문제 등으로 생성 진입을 잠깐 막고 싶을 때 사용
  • 내부 상태 & 이벤트
    • 상태 없음
    • 클릭 → onClick()
      • 상위에서 router.push('/chat/new')
  • 에러 처리 / 엣지 케이스
    • disabled면 클릭 불가
    • 모바일에서 키보드/바텀바와 겹치지 않게 위치 조정
  • 스타일링
    • 원형 버튼 + 그림자
    • 접근성: 최소 터치 영역
  • Storybook
    • Default
    • Disabled

/chat 화면 데이터 흐름 시퀀스 (V1, 1:1)

  • 사용자가 /chat 진입 → ChatRoomListPage 마운트
  • ChatRoomListContainer가 채팅방 목록 1페이지 요청
    • 예: GET /api/chatrooms?size=10
  • 응답 도착
    • 성공: itemsChatRoomList에 전달 → ChatRoomListItemCard 목록 렌더
    • 실패: ChatRoomListErrorState + RetryButton 노출
  • 사용자가 스크롤 하단 도달 → ChatRoomListonEndReached() 호출
  • ChatRoomListContainer.fetchNext() 실행 → 다음 페이지 요청
    • 로딩 중: 리스트 하단에 Spinner 표시
  • 다음 페이지 응답 성공 시 items append → 리스트 갱신(무한 스크롤 반복)
  • 사용자가 카드 클릭(ChatRoomListItemCard) → router.push(/chat/${roomId})로 상세 진입
  • 사용자가 FloatingCreateChatButton 클릭 → router.push('/chat/new')로 생성 페이지 진입

/chat/[roomId] (채팅방 상세) 주요 컴포넌트

(1) ChatCreatePage [Page]

  • 역할
    • /chat/new 라우트 엔트리(Page)
    • 검색/목록/선택/완료 버튼 UI를 조립
    • 생성 성공 시 /chat/{roomId}로 라우팅
  • 디자인/동작 핵심
    • 상단: 검색 입력(UserSearchBar)
    • 본문: 팔로잉/검색 결과 목록(FollowingUserList)
    • 하단: 완료 버튼 고정(CreateChatButton)
  • props & Interface
type ChatCreatePageProps = {};
  • Props 설명
    • 페이지 컴포넌트는 보통 외부 props 없음
  • 내부 상태 & 이벤트
    • 페이지 내부 상태 최소화(상태는 Container로 위임)
    • 이벤트 흐름
      • 페이지 진입 → 팔로잉 목록 1페이지 로드(컨테이너)
      • 검색 실행 → 목록 갱신
      • 완료 클릭 → 생성 mutation → 성공 시 상세로 이동
  • 에러 처리 / 엣지 케이스
    • 로잉 0명 → NoFollowingEmptyState
    • 검색 결과 0명 → NoSearchResultState
    • 생성 실패/네트워크 오류 → 공통 에러 정책(Toast/AlertModal)
  • 스타일링
    • 하단 버튼은 키보드 올라올 때 가려지지 않게 처리

(2) ChatCreateContainer [Container]

  • 역할
    • 팔로잉 목록 조회 + 무한스크롤(10명씩)
    • 검색 실행 시 검색 API 호출(엔터/버튼 기반)
    • “1명 선택” 상태 + submit 가능 여부(유효성) 관리
    • 생성 mutation 수행 후 roomId 반환(상위 라우팅에서 사용)
  • 디자인/동작 핵심
    • 검색 입력 중에는 요청하지 않고, 엔터/버튼에서만 요청
    • 선택은 1명만 유지(기존 선택 해제/교체)
  • props & Interface
type FollowingUser = {
  userId: number;
  nickname: string;
  profileImageUrl?: string | null;
};

type ChatCreateVM = {
  keyword: string;
  setKeyword: (v: string) => void;

  users: FollowingUser[];
  isLoading: boolean;
  isError: boolean;
  error?: unknown;

  hasNext: boolean;
  isFetchingNext: boolean;
  fetchNext: () => void;

  selectedUserId: number | null;
  selectUser: (userId: number) => void;

  canSubmit: boolean;
  submit: () => Promise<{ roomId: number }>;
  refetch: () => void;

  helperText?: string | null; // 검색어 검증 안내(선택)
};

type ChatCreateContainerProps = {
  children: (vm: ChatCreateVM) => React.ReactNode;
};
  • Props 설명
    • children(vm) 형태로 UI에 필요한 값/핸들러를 내려줌
  • 내부 상태 & 이벤트
    • 내부 상태
      • keyword, selectedUserId
    • 이벤트 흐름
      • fetchNext() → 다음 페이지 로드
      • selectUser(userId) → 1명 선택 유지
      • submit() → 생성 mutation 실행(기존 방 있으면 기존 roomId 반환)
  • 에러 처리 / 엣지 케이스
    • 중복 클릭으로 submit 연타 방지(loading 동안 비활성)
    • 선택 없이 submit 시도 → AlertModal 또는 canSubmit=false로 막기
    • 검색어 검증 실패(2자 미만/공백/10자 초과) → 요청 안 보내고 helperText 세팅
  • 스타일링
    • 없음 (로직 전용)

(3) UserSearchBar [UI]

  • 역할
    • 검색어 입력 + 엔터/버튼으로 검색 실행 트리거 제공
    • 검증 실패 시 helper text 표시
  • 디자인/동작 핵심
    • 입력 중에는 검색 요청 X
    • 엔터/검색 버튼에서만 실행
  • props & Interface
type UserSearchBarProps = {
  value: string;
  onChange: (v: string) => void;

  onSearch: () => void;           // 엔터/버튼에서만 호출
  disabled?: boolean;

  helperText?: string | null;     // "검색어는 2자 이상..." 등
};
  • Props 설명
    • helperText: 검증 실패 안내 문구(없으면 미노출)
  • 내부 상태 & 이벤트
    • 내부 상태 없음(컨트롤드)
    • 이벤트
      • Enter → onSearch()
      • 검색 버튼 클릭 → onSearch()
      • 입력 변화 → onChange(v)
  • 에러 처리 / 엣지 케이스
    • 공백만 입력, 2자 미만, 10자 초과 → onSearch에서 요청 막고 helperText 표시(상위/컨테이너)
    • disabled면 입력/검색 실행 불가
  • 스타일링
    • 입력창 + 검색 버튼(또는 아이콘)
    • helperText는 입력 하단에 작게 표시
  • Storybook
    • Default
    • WithHelperText
    • Disabled

(4) FollowingUserList [UI]

  • 역할
    • 팔로잉/검색 결과 목록 렌더
    • 하단 도달 시 onEndReached로 다음 페이지 로드 트리거
  • 디자인/동작 핵심
    • 10명씩 append
    • 추가 로딩 시 하단 스피너 표시 가능
  • props & Interface
type FollowingUserListProps = {
  items: FollowingUser[];

  isLoading: boolean;        // 초기 로딩
  isFetchingNext: boolean;   // 다음 페이지 로딩
  hasNext: boolean;

  onEndReached: () => void;
  onRetry?: () => void;

  renderItem: (user: FollowingUser) => React.ReactNode;

  emptyState?: React.ReactNode;  // NoFollowingEmptyState / NoSearchResultState
  errorState?: React.ReactNode;  // ErrorState
};
  • Props 설명
    • emptyState: 리스트가 비었을 때 보여줄 UI를 외부에서 주입(상황별 분기 가능)
  • 내부 상태 & 이벤트
    • 내부 상태: IntersectionObserver 센티넬
    • 이벤트
      • 하단 도달 + hasNext && !isFetchingNextonEndReached()
  • 에러 처리 / 엣지 케이스
    • 초기 로딩이면 스켈레톤/로딩 표시
    • 에러면 errorState 렌더(+ 재시도)
    • hasNext=false면 추가 로드 트리거 중단
  • 스타일링
    • Row 간격 고정
    • 스크롤 영역 확보(하단 고정 버튼과 겹치지 않게 padding-bottom)
  • Storybook
    • LoadingInitial
    • Loaded
    • Empty
    • Error
    • FetchingNext

(5) UserSelectRow [UI]

  • 역할
    • 유저 한 줄(아바타/닉네임/체크) 표시
    • 클릭으로 선택/해제 트리거 제공(선택 진입 포인트)
  • 디자인/동작 핵심
    • Row 전체 클릭 가능
    • 선택 상태가 눈에 띄게 표시(체크 + 하이라이트)
  • props & Interface
type UserSelectRowProps = {
  user: FollowingUser;
  selected: boolean;
  onClick: (userId: number) => void;

  disabled?: boolean;
};
  • Props 설명
    • selected: 현재 선택된 유저인지 여부(체크 표시)
    • disabled: 로딩/제한 상황에서 클릭 막기
  • 내부 상태 & 이벤트
    • 상태 없음
    • 클릭 → onClick(user.userId)
  • 에러 처리 / 엣지 케이스
    • profileImageUrl 없으면 기본 아바타(회색 원)
    • disabled면 클릭 불가
  • 스타일링
    • 선택 시 Row 배경/테두리 강조
    • 닉네임은 한 줄 ellipsis
  • Storybook
    • Selected
    • Unselected
    • NoAvatar
    • Disabled

(6) SingleSelectCheckbox [UI]

  • 역할
    • 1명만 선택 가능 제약을 UI로 명확히 보여주는 체크 요소
    • 선택/해제 상태 표시(실제 선택 상태는 상위가 관리)
  • 디자인/동작 핵심
    • selected=true면 체크 표시
    • Row 클릭과 동일한 행동을 하도록 연결
  • props & Interface
type SingleSelectCheckboxProps = {
  checked: boolean;
  onChange?: (next: boolean) => void; // Row 클릭으로만 제어하면 optional
  disabled?: boolean;
};
  • Props 설명
    • onChange는 Row 클릭으로 통일하면 생략 가능(표시 전용으로 사용)
  • 내부 상태 & 이벤트
    • 상태 없음
    • 클릭 시 onChange?.(!checked) (사용하는 경우)
  • 에러 처리 / 엣지 케이스
    • disabled면 변경 불가
  • 스타일링
    • 체크 영역 터치 범위 확보
  • Storybook
    • Checked
    • Unchecked
    • Disabled

(7) CreateChatButton [UI]

  • 역할
    • “완료” 버튼
    • 1명 선택 시 활성화 → 클릭 시 채팅방 생성 mutation 실행 → 성공 시 /chat/{roomId} 이동
  • 디자인/동작 핵심
    • 하단 고정 영역에서 항상 보이게
    • 로딩 중에는 버튼 로딩/비활성
  • props & Interface
type CreateChatButtonProps = {
  disabled: boolean;
  loading?: boolean;
  onClick: () => void;
};
  • Props 설명
    • disabled: 선택 1명 미충족 시 true
    • loading: 생성 요청 중 true
  • 내부 상태 & 이벤트
    • 상태 없음
    • 클릭 → onClick() (상위에서 submit 실행)
  • 에러 처리 / 엣지 케이스
    • 연타 방지: loading=true면 disabled 처리
    • 실패 시: Toast/AlertModal로 안내(상위에서)
  • 스타일링
    • Primary 스타일
    • 키보드 올라오는 환경에서 가려지지 않게 처리
  • Storybook
    • Disabled
    • Enabled
    • Loading

/chat/new 화면 데이터 흐름 시퀀스 (V1, 1:1)

(1) 페이지 진입 → 팔로잉 목록 1페이지 로드

  1. 사용자가 /chat/new 진입 → ChatCreatePage 마운트
  2. ChatCreateContainer에서 팔로잉 목록 요청
  • GET /api/users/me/followings?size=n&lastId=k&nickname={name}
  • Header: Authorization: Bearer {accessToken}
  • Query
    • size: int (default = 10)
    • lastId: Long (default = null)
    • nickname: String (default = null)

(2) 응답 처리 → 목록 렌더

  1. 응답 성공 → FollowingUserList가 목록 렌더 (UserSelectRow로 그려짐)
  2. 응답 실패(401/500 등) → 공통 정책대로 ErrorState/Toast 등 노출

(3) 무한 스크롤 → 다음 페이지 로드

  1. 사용자가 스크롤 하단 도달 → FollowingUserList.onEndReached()
  2. 다음 페이지 요청
  • GET /api/users/me/followings?size=n&lastId=k&nickname={name}

(4) 검색(닉네임 기반) → 엔터/버튼 시에만 요청

  1. 사용자가 검색어 입력 (UserSearchBar.onChange)
    • 입력 중에는 요청 안 함(상태만 저장)
  2. 사용자가 Enter/검색 버튼 클릭 → 검색 실행
  • GET /api/users/me/followings?size=n&lastId=k&nickname={name}

(5) 1명 선택 → 완료 버튼 활성화

  1. 사용자가 UserSelectRow 클릭 → selectedUserId 갱신
  2. 1명 선택 상태가 되면 CreateChatButton 활성화
    • 체크 표시는 SingleSelectCheckboxchecked=true로 표시

(6) 완료 클릭 → 채팅방 생성(또는 기존 방 반환) → 상세로 이동

  1. 사용자가 완료 클릭 → 채팅방 생성 요청
  • POST /api/chatrooms
  • Header: Authorization: Bearer {accessToken}
  1. 생성 성공(201) → 응답의 roomId 확보 → router.push(/chat/${roomId})
  2. 생성 실패 → 공통 에러 정책(Toast/AlertModal 등) 노출하고 이동 없음

/chat/[roomId] (채팅방 상세) 주요 컴포넌트

(1) ChatRoomDetailPage [Page]

  • 역할
    • /chat/[roomId] 라우트 엔트리(Page)
    • 히스토리(REST) 로드 → 실시간(WebSocket/STOMP) 연결/구독 → 전송/삭제/읽음 처리를 조립
    • 설정 페이지(/chat/[roomId]/settings) 이동, 뒤로가기(/chat) 이동의 상위 흐름 제공
  • 디자인/동작 핵심
    • 상단: ChatHeader (뒤로/타이틀/설정)
    • 본문: MessageList (히스토리 + 무한 스크롤 + 새 메시지 append)
    • 하단: ChatComposer (입력/전송/첨부)
    • 모달: AttachmentSheet, ConfirmModal 등 필요 시 띄움
  • props & Interface
type ChatRoomDetailPageProps = {
  params: { roomId: string };
};
  • Props 설명
    • params.roomId
      • 문자열이므로 내부에서 Number(roomId)로 변환해 사용
  • 내부 상태 & 이벤트
    • 내부 상태: Container로 위임
      • selectedMessageIdForDelete (삭제 대상)
      • attachmentSheetOpen (첨부 시트 열림 여부)
    • 이벤트 흐름(핵심)
      1. 페이지 마운트 → 히스토리 1페이지 로드 시작
      2. 히스토리 로드 성공 → WebSocket 연결 및 room subscribe 시작
      3. 새 메시지 수신 → 메시지 리스트에 append
      4. 전송(텍스트/첨부) → optimistic 추가(선택) → 실패 시 Failed 처리
      5. 롱프레스 삭제 → ConfirmModal → 삭제 요청 → UI에 “삭제된 메시지” 반영
      6. 미읽음 존재 시 → UnreadDivider를 특정 message 위치에 렌더
  • 에러 처리 / 엣지 케이스
    • roomId가 숫자가 아니거나 비정상 → 안내 후 /chat로 이동(정책 선택)
    • 히스토리 로드 실패 → ErrorState + RetryButton 또는 공통 토스트
    • WS 연결 실패 → 입력 disabled + 토스트 안내(재연결 시도 정책)
    • 방 권한 없음(403) → 접근 불가 안내 후 목록 이동
  • 스타일링
    • 상단 고정 헤더 + 하단 고정 입력창 구조
    • 키보드 오픈 시 입력 영역이 가려지지 않도록 safe-area/viewport 대응

(2) ChatRoomDetailContainer [Container]

  • 역할
    • REST 히스토리(infinite) + WS 연결/구독 + 전송/삭제 mutation을 한곳에서 관리
    • UI는 데이터/핸들러만 받아서 렌더링하도록 분리
  • 디자인/동작 핵심
    • 히스토리 1페이지 성공 이후에 WS를 연결/구독
    • 방 이동 시 구독 해제(unsubscribe) 후 새 방 subscribe(중복수신 방지)
    • 실패 메시지는 local state로 상태를 들고 있다가 재전송/삭제 가능
  • props & Interface
type ChatMessage = {
  messageId: number;
  type: "TEXT" | "IMAGE" | "FILE";
  content: string | null;
  fileUrl?: string | null;     // s3Key 또는 url
  createdAt: string;           // ISO 권장
  sender: { userId: number; nickname: string; avatarUrl?: string | null };
  isMe: boolean;
  isDeleted: boolean;
  status?: "SENT" | "SENDING" | "FAILED"; // 프론트 상태(선택)
};

type ChatRoomDetailVM = {
  roomId: number;

  // 히스토리
  messages: ChatMessage[];
  hasPrev: boolean;
  isFetchingPrev: boolean;
  fetchPrev: () => void;

  // WS 연결 상태
  wsConnected: boolean;

  // unread divider
  unreadMessageId: number | null;

  // actions
  sendText: (text: string) => Promise<void>;
  sendImages: (files: File[]) => Promise<void>;
  sendPdf: (file: File) => Promise<void>;

  requestDeleteMessage: (messageId: number) => void; // 모달 오픈 트리거
  confirmDeleteMessage: () => Promise<void>;          // 실제 삭제 실행
  cancelDelete: () => void;

  resendFailed: (localIdOrMessageId: number) => Promise<void>;
  removeFailed: (localIdOrMessageId: number) => void;
};

type ChatRoomDetailContainerProps = {
  roomId: number;
  children: (vm: ChatRoomDetailVM) => React.ReactNode;
};
  • Props 설명
    • roomId: number로 정제된 값만 받는 걸 권장
  • 내부 상태 & 이벤트
    • 상태
      • selectedMessageIdForDelete
      • failedQueue(실패 메시지 목록)
      • wsConnected
    • 이벤트 흐름
      • fetchPrev() → 이전 메시지 로드
      • sendText/sendImages/sendPdf() → 전송 요청 + 성공 시 상태 업데이트
      • resendFailed/removeFailed() → 실패 처리
      • delete 흐름은 requestDeleteMessage → 모달 → confirmDeleteMessage
  • 에러 처리 / 엣지 케이스
    • REST/WS 중복 메시지: messageId로 dedupe
    • WS 끊김: 재연결(backoff) 정책 + 입력 disabled
    • 전송 실패: 최대 재시도 횟수(예: 3회) 초과 시 FAILED로 확정
  • 스타일링
    • 없음(로직 전용)

(3) ChatHeader [UI: Header]

  • 역할
    • 뒤로가기(/chat) + 채팅방 타이틀 표시 + 설정 이동(/chat/[roomId]/settings) 진입 포인트
  • 디자인/동작 핵심
    • 좌측: Back
    • 중앙: 채팅방 타이틀
    • 우측: 설정 버튼(햄버거)
  • props & Interface
type ChatHeaderProps = {
  title: string;
  onBack: () => void;
  onOpenSettings: () => void;
};
  • 내부 상태 & 이벤트
    • 상태 없음
    • back 클릭 → onBack()
    • settings 클릭 → onOpenSettings()
  • 에러 처리 / 엣지 케이스
    • title이 비어있으면 채팅 등 fallback 텍스트
  • 스타일링
    • 상단 고정(sticky)
    • 터치 영역 충분히 확보
  • Storybook
    • Default
    • LongTitle
    • EmptyTitleFallback

(4) MessageList [UI]

  • 역할
    • 메시지 목록 렌더 + 위로 스크롤 시 이전 메시지 로드(infinite)
    • prepend 시 스크롤 점프를 보정해서 UX 유지
    • 미읽음 구분선(UnreadDivider) 위치에 맞게 렌더링
  • 디자인/동작 핵심
    • 스크롤 컨테이너 내부에서
      • 상단 근처 도달 → fetchPrev() 호출
      • 새 메시지 append 시 필요하면 하단 유지
    • 롱프레스 이벤트를 상위로 전달해 삭제 모달 트리거
  • props & Interface
type MessageListProps = {
  messages: ChatMessage[];

  hasPrev: boolean;
  isFetchingPrev: boolean;
  onFetchPrev: () => void;

  unreadMessageId?: number | null; // 여기까지 읽었습니다 위치 기준
  onLongPressMessage: (messageId: number) => void;

  onClickImage: (fileUrlOrKey: string) => void;
};
  • Props 설명
    • unreadMessageId: 이 messageId 앞(또는 해당 지점)에 UnreadDivider를 끼워 넣음
  • 내부 상태 & 이벤트
    • 내부 상태
      • isAtBottom(하단에 붙어있는지)
      • prevScrollHeightRef(prepend 보정용)
    • 이벤트
      • 스크롤 상단 근처 + hasPrev && !isFetchingPrevonFetchPrev()
      • 롱프레스 → onLongPressMessage(messageId)
  • 에러 처리 / 엣지 케이스
    • prepend 보정 실패하면 “스크롤 튐” 버그 발생 → 반드시 보정 로직 포함
    • 메시지 0개일 때 빈 화면 처리(또는 안내 문구)
  • 스타일링
    • 메시지 간격/그룹핑(연속 메시지) 정책이 있으면 여기서 처리
  • Storybook
    • Loaded
    • WithUnreadDivider
    • FetchingPrev
    • Empty

(5) ChatComposer [UI]

  • 역할
    • 메시지 입력/전송/첨부 시트 열기(AttachmentSheet 진입)
    • WS 끊김/로딩 중일 때 disabled 처리
  • 디자인/동작 핵심
    • 입력창 + 전송 버튼 + 첨부 버튼
    • 모바일에서 “엔터 전송”보다 “버튼 전송” 우선(UX)
  • props & Interface
type ChatComposerProps = {
  onSendText: (text: string) => void;
  onOpenAttachment: () => void;
  disabled?: boolean;
};
  • Props 설명
    • disabled: 연결 끊김, 전송 로딩 중 등 입력 막아야 할 때 사용
  • 내부 상태 & 이벤트
    • 내부 상태: inputValue
    • 이벤트
      • 전송 버튼 클릭 → onSendText(inputValue) (공백 검증 후)
      • 첨부 클릭 → onOpenAttachment()
  • 에러 처리 / 엣지 케이스
    • 공백만 전송 방지
    • 길이 제한 정책 있으면(예: 1000자) 여기서 제한
  • 스타일링
    • 하단 고정 + safe-area padding
  • Storybook
    • Default
    • Disabled
    • WithLongText

(6) AttachmentSheet [Modal/Sheet]

  • 역할
    • 이미지/파일(PDF) 첨부 선택 UI 제공
    • 선택된 파일 검증(개수/용량/형식)
    • 검증 통과한 첨부만 상위에 전달
  • 디자인/동작 핵심
    • 이미지: 최대 9장, jpg/jpeg/png, 용량 제한
    • PDF: 최대 1개, pdf, 용량 제한
    • 실패 시 ValidationFailModal 또는 Alert/Toast 트리거
  • props & Interface
type AttachmentSheetProps = {
  open: boolean;
  onClose: () => void;

  onConfirmImages: (files: File[]) => void; // 최대 9
  onConfirmPdf: (file: File) => void;       // 최대 1

  onValidationFail: (message: string) => void;
};
  • Props 설명
    • onValidationFail: 공통 모달/토스트를 띄우기 위한 트리거
  • 내부 상태 & 이벤트
    • 내부 상태
      • pickedImages: File[]
      • pickedPdf: File | null
    • 이벤트 흐름
      • 파일 선택 → 검증 → 통과 시 state 반영
      • 확인 클릭 → onConfirmImages 또는 onConfirmPdf
  • 에러 처리 / 엣지 케이스
    • 9장 초과 선택 → 일부만 반영하거나 실패 처리(정책 선택, 요구사항이면 실패 토스트)
    • 같은 파일 재선택 가능하도록 input value 초기화
    • preview URL 생성 시 revoke 처리(메모리 누수 방지)
  • 스타일링
    • 바텀시트 형태 + 선택된 파일 미리보기 그리드
  • Storybook
    • Closed
    • ImagePickValid
    • ImagePickTooMany
    • PdfPickValid
    • InvalidType

(7) UnreadDivider [UI]

  • 역할
    • 미읽음 메시지 존재 시, “마지막 읽은 메시지”와 “새 메시지” 사이에 구분선 표시
    • 입장 시 해당 위치로 스크롤 이동 UX(스크롤 이동은 상위에서)
  • 디자인/동작 핵심
    • 문구: “여기까지 읽었습니다.”
    • 리스트에서 특정 messageId 기준으로 삽입됨
  • props & Interface
type UnreadDividerProps = {
  visible: boolean;
  label?: string; // default: "여기까지 읽었습니다."
};
  • Props 설명
    • visible: 해당 방에 미읽음이 있을 때 true
  • 내부 상태 & 이벤트
    • 상태 없음
  • 에러 처리 / 엣지 케이스
    • unread 기준 messageId가 로드 범위 밖이면 표시 못 할 수 있음
      • 이 경우 가장 가까운 로드된 지점에 표시하거나, 로드 전략을 조정
  • 스타일링
    • 양쪽 라인 + 가운데 라벨
  • Storybook
    • Visible
    • Hidden

(8) FailedMessageRow [UI]

  • 역할
    • 전송 실패한 메시지를 “실패 상태”로 표시
    • 재전송/삭제 액션을 제공하여 사용자가 복구 가능하도록 함
  • 디자인/동작 핵심
    • 실패 메시지 버블 + “재전송 / 삭제” 버튼
    • 실패 원인은 토스트로 안내(네트워크/서버 등)
  • props & Interface
type FailedMessageRowProps = {
  message: ChatMessage; // status === "FAILED" 인 메시지
  onResend: (messageId: number) => void;
  onRemove: (messageId: number) => void;
};
  • Props 설명
    • message.status가 FAILED인 경우에만 렌더링(상위에서 분기 권장)
  • 내부 상태 & 이벤트
    • 상태 없음
    • 재전송 클릭 → onResend(message.messageId)
    • 삭제 클릭 → onRemove(message.messageId)
  • 에러 처리 / 엣지 케이스
    • 재전송도 실패할 수 있음 → 재시도 횟수 제한(예: 3회) 후 완전 실패 확정
    • 오프라인 상태일 때는 “연결 후 재시도” 안내
  • 스타일링
    • 실패임을 나타내는 보조 텍스트/아이콘(선택)
    • 버튼 터치 영역 확보
  • Storybook
    • Failed
    • FailedWithLongText

/chat/[roomId] 화면 데이터 흐름 시퀀스 (V1, 1:1)

(0) 라우팅 진입

  1. 사용자가 /chat/{roomId} 진입 → ChatRoomDetailPage 마운트
  2. roomIdnumber로 파싱해서 이후 API에 사용

(1) (REST) 채팅방 기본 정보 로드 (타이틀/알림설정/최근이미지 등)

  1. 채팅방 상세 정보 요청
  • GET /api/chatrooms/{roomId}
  • Header: Authorization: Bearer {accessToken}
  • Path Param: roomId: Long (필수)
  1. 성공(200) → ChatHeadertitle(roomName/title) 세팅, 화면 기본 구성 완료
  2. 실패(401/403/404/500) → 공통 에러 정책(토스트/모달) + 필요 시 /chat로 이동

(2) (REST) 메시지 히스토리 1페이지 로드

  1. 대화 내용(최근) 요청
  • GET /api/chatrooms/{roomId}/messages?size=n&lastId=k
  • Header: Authorization: Bearer {accessToken}
  • Query
    • size: Int (default 20)
    • lastId: Long (선택) ← “이전 메시지 더 불러오기” 커서
  • Path Param: roomId: Long (필수)
  1. 성공(200) → MessageList에 메시지 렌더
  2. 실패(401/403/404/500) → 공통 에러 정책(토스트/모달) + 재시도 UI 제공

(3) (REST) 위로 스크롤 무한 로딩

  1. 사용자가 위로 스크롤(상단 근접) → 이전 메시지 요청
  • GET /api/chatrooms/{roomId}/messages?size=20&lastId={현재_가장_오래된_messageId}
    • 응답의 nextCursor/hasNext를 기반으로 계속 paging
  1. 성공 시 prepend + 스크롤 보정(점프 방지)

(4) (WS/STOMP) 실시간 연결 + 구독

  1. 히스토리 1페이지 로드 성공 이후, WebSocket 연결
  • STOMP URL: ws://{domain}/ws-chat
  • Header: Authorization: Bearer {accessToken}
  • 응답
    • 101: 연결 성공
    • 401: accessToken 만료
    • 404: 엔드포인트 경로 오류
  1. 연결 성공 후 해당 room 구독
  • SUBSCRIBE: /sub/chat/room/{roomId}
  1. 새 메시지 수신 시(payload) → MessageList에 append
  • payload에 isDeleted가 true면 “삭제된 메시지”로 렌더

(4) (전송: TEXT) 입력 → WS 송신

  1. 사용자가 ChatComposer에서 텍스트 전송
  2. STOMP로 송신
  • PUBLISH: /pub/chat/message
  • Payload
{
"roomId":123,
"type":"TEXT",
"content":"안녕하세요!",
"s3Key":null
}
  1. 서버가 브로드캐스트 → 구독(/sub/chat/room/{roomId})으로 수신 → 리스트 append

(5) (전송: IMAGE) 첨부 → Presigned 발급 → S3 업로드 → (메타 등록) → WS 송신

  1. 사용자가 AttachmentSheet에서 이미지 선택
  2. 업로드용 Presigned URL 발급
  • POST /api/files/presigned
  • Header: Authorization: Bearer {accessToken}
  • Body
{
"fileName":"photo.jpg",
"mimeType":"image/jpeg"
}
  1. FE가 presignedUrl로 S3에 직접 업로드
  2. (권장) 파일 메타 등록
  • POST /api/files
  • Header: Authorization: Bearer {accessToken}
  • Body 예시
{
"originalName":"photo.jpg",
"s3Key":"chat/123/photo.jpg",
"mimeType":"image/jpeg",
"category":"CHAT",
"fileSize":1048576,
"refType":"CHATROOM",
"refId":123,
"sortOrder":1
}
  1. WS로 이미지 메시지 송신
  • PUBLISH /pub/chat/message
{
"roomId":123,
"type":"IMAGE",
"content":null,
"s3Key":"chat/123/photo.jpg"
}
  1. 구독으로 수신 → ImageMessageBubble 렌더(썸네일)

(7) (삭제) 롱프레스 → ConfirmModal → REST 삭제

  1. 메시지 롱프레스 → ConfirmModal 오픈
  2. 삭제 확정 클릭 → 삭제 요청
  • DELETE /api/chatrooms/{roomId}/messages/{messageId}
  • Header: Authorization: Bearer {accessToken}
  • Path Param
    • roomId: Long
    • messageId: Long
  • 성공: 204 No Content
  1. 성공 시 UI에서 해당 메시지를 “삭제된 메시지”로 표시(또는 서버 이벤트 수신으로 반영)

(8) (UnreadDivider) “여기까지 읽었습니다” + 읽음 커서 처리

  1. 메시지 히스토리 조회 응답에 lastReadMsgId가 포함된다.
  • GET /api/chatrooms/{roomId}/messages?size=n&lastId=k
  • Header: Authorization: Bearer {accessToken}
  • Query
    • size: Int (default 20)
    • lastId: Long (선택)
  • Path Param
    • roomId: Long (필수)
  • Response(data)
    • messages: Message[]
    • lastReadMsgId: Long
    • nextCursor: Long
    • hasNext: Boolean
  1. 성공(200) 시 MessageList 렌더 단계에서,
  • lastReadMsgId를 기준으로 “여기까지 읽었습니다” 구분선을 표시한다.
    • 예: messageId === lastReadMsgId인 메시지 다음 위치UnreadDivider 삽입
  • lastReadMsgId가 없거나(첫 입장 등) 메시지 범위에 없으면 구분선은 생략하거나 기본 위치로 처리한다.
  1. 사용자가 채팅방에 들어오면(정책에 따라) 읽음 커서를 갱신한다.
  • PATCH /api/chatrooms/{roomId}
  • Header: Authorization: Bearer {accessToken}
  • Body
{
"lastReadMsgId":12345
}
  1. 갱신 타이밍(정책 택1)
  • (A) 입장 즉시: 현재 로드된 메시지 중 최신 messageId로 갱신
  • (B) 스크롤이 최신 도달 시: 사용자가 실제로 최신까지 확인했을 때 갱신
  1. 성공(200) 시 다음 재진입에서 lastReadMsgId가 갱신된 값으로 내려와 구분선 위치가 업데이트된다.
  2. 실패(400/401/403/404/500) 시 공통 에러 정책(토스트/모달)에 따라 안내한다.

/chat/[roomId]/settings (채팅 상세 설정) 주요 컴포넌트

(1) ChatRoomSettingsPage [Page]

  • 역할
    • /chat/[roomId]/settings 라우트 엔트리(Page)
    • 설정 화면의 전체 UI 조립(알림 토글/이름 수정/최근 이미지/나가기)
    • 성공 시 토스트 노출, 필요 시 라우팅(/chat 이동) 처리
  • 디자인/동작 핵심
    • 상단: 설정 타이틀 + 뒤로가기(선택)
    • 섹션 1: 알림 토글 (AlarmToggleRow)
    • 섹션 2: 채팅방 이름 수정 진입 (EditTitleRowEditTitleModal)
    • 섹션 3: 최근 이미지 4개 (RecentImagesPreview)
    • 섹션 4: 나가기 (DangerButtonConfirmModal)
  • props & Interface
type ChatRoomSettingsPageProps = {
  params: { roomId: string };
};
  • Props 설명
    • params.roomId: string → 내부에서 number로 파싱해 사용
  • 내부 상태 & 이벤트
    • 상태(권장: Container로 위임)
      • isEditTitleOpen
      • isLeaveConfirmOpen
    • 이벤트 흐름
      • 페이지 마운트 → 설정 정보 로드(없으면 room 기본값으로 구성)
      • 토글 변경 → PATCH
      • 이름 수정 → 모달 열기 → PUT
      • 나가기 → ConfirmModal → DELETE → /chat 이동
  • 에러 처리 / 엣지 케이스
    • roomId 파싱 실패 → 안내 후 /chat 이동(정책)
    • 401/403 → 공통 인증/인가 처리(토스트/로그인 이동 등 공통 정책)
    • 404 → 채팅방 없음 안내 후 목록 이동
  • 스타일링
    • 섹션 간 간격, 하단 danger 영역 강조

(2) ChatRoomSettingsContainer [Container]

  • 역할
    • 설정 데이터 로드 + PATCH/PUT/DELETE mutation 로직을 한 곳에 모음
    • 성공/실패 처리(Toast), 캐시 갱신(invalidate), 라우팅 이동을 책임
  • 디자인/동작 핵심
    • 알림/이름 수정은 낙관적 업데이트(선택) 가능하지만, 실패 시 롤백 필요
    • 나가기 성공 시 /chat 이동 + 채팅방 목록 캐시 invalidate
  • props & Interface
type ChatRoomSettingsVM = {
  roomId: number;

  // UI data
  isAlarmOn: boolean;
  roomName: string;

  // images
  recentImages: { id: number; imageUrl: string }[];
  isImagesLoading: boolean;

  // loading flags
  isUpdatingAlarm: boolean;
  isUpdatingTitle: boolean;
  isLeaving: boolean;

  // actions
  toggleAlarm: (next: boolean) => Promise<void>;
  updateTitle: (nextName: string) => Promise<void>;
  fetchRecentImages: () => Promise<void>;
  leaveRoom: () => Promise<void>;
};

type ChatRoomSettingsContainerProps = {
  roomId: number;
  children: (vm: ChatRoomSettingsVM) => React.ReactNode;
};
  • Props 설명
    • children(vm)로 UI에 필요한 값/핸들러 전달
  • 내부 상태 & 이벤트
    • 상태
      • isAlarmOn, roomName
      • recentImages
    • 이벤트 흐름(핵심 API)
      • 알림 토글 저장
        • PATCH /api/chatrooms/{roomId}
        • Body: { "isAlarmOn": true | false }
      • 이름 수정
        • PUT /api/chatrooms/{roomId}
        • Body: { "roomName": "..." }
      • 최근 이미지 4개
        • GET /api/chatrooms/{roomId}/images?size=4
      • 나가기
        • DELETE /api/chatrooms/{roomId}
        • 성공(204) → /chat 이동 + 목록 캐시 invalidate
  • 에러 처리 / 엣지 케이스
    • PATCH/PUT 실패 시 토스트 + (선택) 이전 값 롤백
    • 나가기 실패 시 모달 유지/닫기 정책 선택 + 토스트
    • 연타 방지: mutation 중 버튼/토글 disabled 처리

(3)AlarmToggleRow [UI]

  • 역할
    • 해당 채팅방 알림 수신 ON/OFF를 변경하는 진입 포인트
    • 클릭 시 상위에서 toggleAlarm(next) 호출
  • 디자인/동작 핵심
    • 기본값: ON
    • 토글 상태가 서버에 저장되어 재접속 시 유지
  • props & Interface
type AlarmToggleRowProps = {
  value: boolean;                // isAlarmOn
  onChange: (next: boolean) => void;
  disabled?: boolean;            // 저장 중 등
};
  • Props 설명
    • disabled: PATCH 진행 중 토글 중복 변경 방지용
  • 내부 상태 & 이벤트
    • 상태 없음(컨트롤드)
    • 토글 클릭 → onChange(!value)
  • 에러 처리 / 엣지 케이스
    • 서버 저장 실패 시
      • 토스트 안내
      • (권장) value를 이전 값으로 되돌림(상위에서)
  • 스타일링
    • Row 전체 클릭 가능하게 하면 UX 좋음
    • 접근성: 스위치 레이블 연결
  • Storybook
    • On
    • Off
    • Disabled

(4) EditTitleRow [UI]

  • 역할
    • “채팅방 이름 수정” 모달을 여는 진입 포인트
  • 디자인/동작 핵심
    • 현재 표시되는 이름(roomName)을 함께 보여주면 사용자가 맥락 파악 쉬움
  • props & Interface
type EditTitleRowProps = {
  currentTitle: string;
  onOpen: () => void;
  disabled?: boolean;
};
  • 내부 상태 & 이벤트
    • 상태 없음
    • 클릭 → onOpen() → 상위에서 EditTitleModal open
  • 에러 처리 / 엣지 케이스
    • currentTitle이 비어있으면 placeholder 처리(예: “채팅방”)
  • 스타일링
    • 오른쪽 chevron 아이콘 등으로 “수정 가능” 표현(선택)
  • Storybook
    • Default
    • Disabled

(5) EditTitleModal [Modal]

  • 역할
    • 채팅방 이름을 개인화로 수정하는 모달
    • 확인 클릭 시 PUT /api/chatrooms/{roomId} 호출 트리거
  • 디자인/동작 핵심
    • placeholder에 기존 이름 표시
    • 입력 제한: 최대 10자, 공백만 입력 금지
    • 저장 성공 시 모달 닫힘 + 화면에 즉시 반영
  • props & Interface
type EditTitleModalProps = {
  open: boolean;
  initialValue: string;               // 기존 roomName
  onClose: () => void;

  onConfirm: (nextTitle: string) => void; // 상위에서 PUT 실행
  loading?: boolean;

  maxLength?: number;                 // default 10
};
  • Props 설명
    • initialValue: 모달 열릴 때 input 초기값으로 사용
  • 내부 상태 & 이벤트
    • 내부 상태: inputValue
    • 이벤트 흐름
      • input 변경 → 길이 제한 적용
      • 확인 클릭 → 검증
        • 공백만이면 실패 처리
        • 통과 시 onConfirm(inputValue.trim())
  • 에러 처리 / 엣지 케이스
    • 10자 초과 입력 방지(추가 입력 제한)
    • 저장 실패(4xx/5xx) 시
      • 모달 유지(권장) + 토스트 안내
    • 로딩 중에는 확인 버튼 disabled
  • 스타일링
    • 모달 하단 버튼 2개(취소/확인)
    • 에러 시 input 하단 helper text(선택)
  • Storybook
    • OpenDefault
    • InvalidBlank
    • MaxLength
    • Loading

(6) RecentImagesPreview [UI]

  • 역할
    • 채팅방에서 공유된 최근 이미지 4개 썸네일을 표시
    • 데이터는 GET /api/chatrooms/{roomId}/images?size=4 결과 사용
  • 디자인/동작 핵심
    • 4개 그리드(또는 가로 스크롤)로 간단히
    • 썸네일 클릭 시 원본 뷰어로 이동(요구사항에 있으면, 없으면 단순 미리보기만)
  • props & Interface
type RecentImagesPreviewProps = {
  items: { id: number; imageUrl: string }[];
  loading?: boolean;
  onClickItem?: (id: number) => void; // 선택: 이미지 뷰어 열기
};
  • 내부 상태 & 이벤트
    • 상태 없음(표시 위주)
    • 클릭 이벤트가 필요하면 onClickItem 호출
  • 에러 처리 / 엣지 케이스
    • 이미지가 0개면 섹션 숨김 또는 “최근 이미지 없음” 표시(정책)
    • 로딩 중에는 스켈레톤 처리(선택)
  • 스타일링
    • 동일 비율 썸네일(정사각) + overflow hidden
  • Storybook
    • Loading
    • With4Items
    • WithLessItems
    • Empty

(7) DangerButton [UI]

  • 역할
    • “채팅방 나가기(퇴장)”을 시작하는 버튼(진입 포인트)
    • 클릭 시 ConfirmModal을 열도록 상위에 이벤트 전달
  • 디자인/동작 핵심
    • 위험 액션임을 색/라벨로 강조
    • 버튼 클릭 → 확인 모달 필수
  • props & Interface
type DangerButtonProps = {
  onClick: () => void;
  disabled?: boolean; // 나가기 요청 중 등
};
  • 내부 상태 & 이벤트
    • 상태 없음
    • 클릭 → onClick() (상위에서 confirm modal open)
  • 에러 처리 / 엣지 케이스
    • disabled면 클릭 불가(연타 방지)
  • 스타일링
    • Danger 스타일(빨강 계열)
    • 터치 영역 확보
  • Storybook
    • Default
    • Disabled

/chat/[roomId]/settings 화면 데이터 흐름 시퀀스 (V1, 1:1)

(0) 라우팅 진입

  1. 사용자가 /chat/{roomId}/settings 진입 → ChatRoomSettingsPage 마운트
  2. roomId를 number로 파싱해서 이후 API에 사용

(1) (REST) 설정 기본 정보 로드

  1. 채팅방 상세(설정에 필요한 값 포함) 요청
  • GET /api/chatrooms/{roomId}
  • Header: Authorization: Bearer {accessToken}
  • Path Param: roomId: Long (필수)
  1. 성공(200) → 화면 초기값 세팅
  • 알림 토글: isAlarmOn
  • 채팅방 이름: roomName
  • 참여자 정보(있다면): 본인/상대 정보
  1. 실패(401/403/404/500) → 공통 에러 정책(토스트/모달) + 필요 시 /chat 이동

(2) (REST) 최근 이미지 4개 로드

  1. 최근 이미지 썸네일 요청
  • GET /api/chatrooms/{roomId}/images?size=4
  • Header: Authorization: Bearer {accessToken}
  • Path Param: roomId: Long (필수)
  1. 성공(200) → RecentImagesPreview에 최대 4개 렌더
  2. 실패 → 이미지 섹션만 비우고(또는 “로드 실패”) 토스트 안내

(3) (Mutation) 알림 토글 저장

  1. 사용자가 AlarmToggleRow에서 토글 변경 → 저장 요청
  • PATCH /api/chatrooms/{roomId}
  • Header: Authorization: Bearer {accessToken}
  • Body: { "isAlarmOn": true | false }
  1. 성공 → 토글 상태 유지 + Toast로 “저장됨” 안내
  2. 실패(400/401/403/404/500) → 토스트/모달 안내 + (권장) 토글 값 롤백

(4) (Mutation) 채팅방 이름 수정(모달 → 저장)

  1. 사용자가 EditTitleRow 클릭 → EditTitleModal 오픈
  2. 사용자가 이름 입력 후 확인 → 저장 요청
  • PUT /api/chatrooms/{roomId}
  • Header: Authorization: Bearer {accessToken}
  • Body: { "roomName": "이직 준비방" }
  1. 성공 → 모달 닫힘 + 화면의 roomName 즉시 갱신 + Toast
  2. 실패 → 모달 유지(권장) + 토스트로 실패 안내

(5) (Mutation) 채팅방 나가기(퇴장)

  1. 사용자가 DangerButton 클릭 → ConfirmModal 오픈
  2. 확인 클릭 → 나가기 요청
  • DELETE /api/chatrooms/{roomId}
  • Header: Authorization: Bearer {accessToken}
  • Path Param: roomId: Long (필수)
  • 성공: 204 No Content
  1. 성공 시 처리
  • /chat로 이동
  • 채팅방 목록 캐시 invalidate → 목록에서 해당 방 제거
  1. 실패 처리
  • 400 요청 파라미터 오류

  • 401 인증 실패

  • 403 인가 실패

  • 404 채팅방 없음

  • 500 서버 내부 오류

    → 공통 에러 정책에 따라 토스트/모달 노출 (필요 시 목록으로 이동)

5️⃣ 컴포넌트 간 관계

(1) /chat 채팅방 목록 컴포넌트 간 관계

ChatRoomListPage
 └─(ChatRoomListContainer)//ReactQuery+ 무한스크롤 로직
     ├─ChatRoomCategoryToggle
     ├─ChatRoomList
     │   └─ChatRoomListItemCard(반복)
     ├─FloatingCreateChatButton
     └─EmptyState/Skeleton/ErrorState
  • 핵심 흐름
    • Container가 GET /api/chatrooms...로 목록 가져옴
    • ChatRoomList가 렌더 + 바닥 닿으면 다음 페이지 요청
    • ChatRoomListItemCard 클릭 → /chat/{roomId}
    • FloatingCreateChatButton 클릭 → /chat/new

(2) /chat/new 채팅 생성(1:1) 컴포넌트 간 관계

ChatCreatePage
 └─ (ChatCreateContainer)// 팔로잉 조회 + 검색 + 선택 + 생성 mutation
     ├─ UserSearchBar
     ├─ FollowingUserList
     │   └─UserSelectRow (반복)
     │       └─ SingleSelectCheckbox
     ├─ CreateChatButton
     └─ NoFollowingEmptyState / NoSearchResultState / Toast / AlertModal

  • 핵심 흐름
    • Container가 팔로잉 목록 로드(+ 무한스크롤)
    • UserSearchBar가 검색 트리거
    • UserSelectRow에서 1명 선택
    • CreateChatButtonPOST /api/chatrooms → 성공 시 /chat/{roomId} 이동

(3) /chat/[roomId] 채팅방 상세 컴포넌트 간 관계

ChatRoomDetailPage
 └─ (ChatRoomDetailContainer)// 히스토리 + ws + 전송/삭제 + 읽음 갱신
     ├─ ChatHeader
     ├─ MessageList
     │   ├─ (MessageItem 반복)
     │   └─ UnreadDivider (조건부)
     ├─ ChatComposer
     ├─ AttachmentSheet (열렸을 때)
     └─ ConfirmModal (삭제 확인)

그리고 페이지 밖(공통 유틸)에서:

WebSocketChatManager (singleton)
  ├─connect()
  ├─subscribe(roomId)
  ├─publish(message)
  └─unsubscribe(roomId)

  • 핵심 흐름
    • GET /api/chatrooms/{roomId} → 헤더 정보
    • GET /api/chatrooms/{roomId}/messages... → 리스트 + lastReadMsgId 기준 UnreadDivider
    • 필요 시 PATCH /api/chatrooms/{roomId} { lastReadMsgId } 갱신
    • WS 연결/구독 → 새 메시지 오면 append
    • 삭제는 ConfirmModalDELETE /api/chatrooms/{roomId}/messages/{messageId}

(4) /chat/[roomId]/settings 채팅 설정 컴포넌트 간 관계

ChatRoomSettingsPage
 └─ (ChatRoomSettingsContainer)// 상세 조회 + PATCH/PUT/DELETE
     ├─ AlarmToggleRow
     ├─ EditTitleRow
     │   └─ EditTitleModal (열렸을 때)
     ├─ RecentImagesPreview
     ├─ DangerButton (나가기)
     │   └─ ConfirmModal (나가기 확인)
     └─ Toast
  • 핵심 흐름
    • 알림: PATCH /api/chatrooms/{roomId} { isAlarmOn }
    • 이름: PUT /api/chatrooms/{roomId} { roomName }
    • 최근 이미지: GET /api/chatrooms/{roomId}/images?size=4
    • 나가기: DELETE /api/chatrooms/{roomId}/chat 이동 + 목록 invalidate

6️⃣ 상태 관리 전략 (State Management Strategy)

Global State (Zustand Stores)

  • 채팅은 전역이 필요한 상태만 최소로 둠
  • 예시:
    • connectionStatus (connected/disconnected)
    • pendingMessages (전송 실패한 임시 메시지 큐)
    • attachmentDraft (선택한 이미지 목록 등)

Local State (React useState/useReducer)

  • 화면별 입력 상태:
    • 검색어 입력값
    • 모달 open/close
    • 장문 펼치기 토글 상태(메시지별)

Server Cache State (React Query)

  • chatRooms (채팅방 목록) → useInfiniteQuery
  • chatMessages(roomId) (히스토리) → useInfiniteQuery
  • 설정(알림, 제목)은 mutation 후 관련 쿼리 invalidate

7️⃣ API 연동 (API Integration)

호출할 백엔드 API 목록 (V1, 1:1 기준)

(1) 참여 채팅방 목록 조회 (개인 채팅)

  • GET /api/chatrooms?size=n&cursor=k&type={type}
  • Header: Authorization: Bearer {accessToken}
  • Query
    • type: String (default PRIVATE)
    • size: Int (default 10)
    • cursor: DATETIME (선택) (개인/그룹 공통 커서, 목록 pagination)
  • 사용 화면: /chat (목록 무한스크롤)

(2) 새로운 채팅방 생성 (1:1)

  • POST /api/chatrooms
  • Header: Authorization: Bearer {accessToken}
  • Body
{
  "type": "PRIVATE",
  "title": null,
  "tag": null,
  "userIds": [63]
}
  • 사용 화면: /chat/new (완료 버튼)

(3) 채팅방 상세 정보 조회

  • GET /api/chatrooms/{roomId}
  • Header: Authorization: Bearer {accessToken}
  • Path Param
    • roomId: Long (필수)
  • 사용 화면: /chat/[roomId] (헤더), /chat/[roomId]/settings

(4) 대화 내용 불러오기 (메시지 히스토리)

  • GET /api/chatrooms/{roomId}/messages?size=n&lastId=k
  • Header: Authorization: Bearer {accessToken}
  • Path Param
    • roomId: Long (필수)
  • Query
    • size: Int (default 20)
    • lastId: Long (선택) (이전 메시지 pagination 커서)
  • Response(data) 포함
    • messages[], nextCursor, hasNext, lastReadMsgId
  • 사용 화면: /chat/[roomId] (무한스크롤 + UnreadDivider)

(5) 채팅 읽음 정보 갱신

  • PATCH /api/chatrooms/{roomId}
  • Header: Authorization: Bearer {accessToken}
  • Body
{
  "lastReadMsgId": 12345
}
  • 사용 화면: /chat/[roomId] (입장/최신 도달 시 읽음 갱신)

(6) 메시지 삭제

  • DELETE /api/chatrooms/{roomId}/messages/{messageId}
  • Header: Authorization: Bearer {accessToken}
  • Path Param
    • roomId: Long (필수)
    • messageId: Long (필수)
  • 사용 화면: /chat/[roomId] (롱프레스 삭제)

(7) 채팅방 알림 켜기/끄기

  • PATCH /api/chatrooms/{roomId}
  • Header: Authorization: Bearer {accessToken}
  • Body
{
  "isAlarmOn": true
}
  • 사용 화면: /chat/[roomId]/settings

(8) 채팅방 이름 수정 (개인화 roomName)

  • PUT /api/chatrooms/{roomId}
  • Header: Authorization: Bearer {accessToken}
  • Body
{
  "roomName": "이직 준비방"
}
  • 사용 화면: /chat/[roomId]/settings

(9) 채팅방 이미지 모아보기 (최근 이미지 4개)

  • GET /api/chatrooms/{roomId}/images?size=n&lastId=k
  • Header: Authorization: Bearer {accessToken}
  • Query
    • size: Int (default 10) → 설정 페이지에서는 size=4 사용
    • lastId: Long (선택)
  • 사용 화면: /chat/[roomId]/settings (최근 이미지 4개)

(10) 채팅방 나가기(퇴장)

  • DELETE /api/chatrooms/{roomId}
  • Header: Authorization: Bearer {accessToken}
  • Path Param
    • roomId: Long (필수)
  • 사용 화면: /chat/[roomId]/settings

API 호출 처리 (Frontend Policy)

  • HTTP 클라이언트: axios (프로젝트 공통 정책)
  • 인증 헤더: 모든 채팅 API는 Authorization: Bearer {accessToken} 포함
  • React Query 사용
    • 목록/히스토리: useInfiniteQuery
    • 수정/삭제/나가기: useMutation
    • 성공 후 정책
      • 설정 변경(PATCH/PUT): invalidateQueries(['chatRoom', roomId])
      • 나가기(DELETE): /chat 이동 후 invalidateQueries(['chatRooms', { type: 'PRIVATE' }])
  • 에러 처리
    • 401/403/404/500 등은 공통 에러 정책에 따라 Toast/Modal 노출
    • 목록/히스토리는 ErrorState + RetryButton 제공

8️⃣ 라우팅 (Routing)

  • Public / Protected 정책은 공통 AuthGate 흐름을 따른다.
  • 채팅 도메인의 모든 라우트는 Protected Route(로그인 필요) 로 처리한다.

채팅 관련 라우트

  • /chat
    • 채팅방 목록 페이지 (1:1 목록 + 무한 스크롤)
  • /chat/new
    • 채팅 생성 페이지 (1:1 전용, 팔로잉 유저 검색/선택)
  • /chat/[roomId]
    • 채팅방 상세 페이지 (히스토리 + 실시간 송수신)
  • /chat/[roomId]/settings
    • 채팅방 설정 페이지 (알림/이름 수정/최근 이미지/나가기)

인증 실패 처리(공통 정책 연동)

  • API 호출 중 401(인증 실패) 발생 시
    • 공통 refresh/재시도 정책을 따르고, 최종 실패 시 로그인/랜딩으로 이동 (프로젝트 공통 규칙 적용)

9️⃣ 폼 처리 및 유효성 검증 (Forms & Validation)

1) 클라이언트 측

(1) 유저 검색 입력 (/chat/new)

  • 입력 규칙
    • 길이: 2~10자
    • 공백만 입력 불가 (trim() 결과가 빈 문자열이면 실패)
  • 검색 실행 트리거
    • 엔터 키 입력 또는 검색 버튼 클릭 시에만 실행
    • 입력 중 자동 호출(디바운스 자동검색) 하지 않음
    • 입력 중인 검색어는 화면 전환 없이 유지
  • 실패 처리(UI)
    • 입력값이 비어있음/공백만 있음 → helper text: “검색어를 입력해 주세요.”
    • 2자 미만 → helper text: “검색어는 2자 이상 입력해 주세요.”
    • 10자 초과 → 추가 입력 제한 또는 helper text: “검색어는 최대 10자까지 입력할 수 있습니다.”
  • 검색 결과 예외
    • 결과가 없으면 목록 영역에 “해당하는 유저가 없습니다.” 표시

(2) 1:1 채팅방 생성 선택 (/chat/new)

  • 선택 규칙
    • 1:1 채팅은 상대 유저 1명 필수 선택
    • 2명 이상 선택은 허용하지 않음(단일 선택 강제)
  • 완료 버튼 활성화
    • selectedUserId존재할 때만 활성화
  • 실패 처리(UI)
    • 1명 초과 선택 시 토스트: “개인 채팅방은 1인 필수 선택입니다.”
    • 선택 없이 완료 시 AlertModal 또는 토스트로 안내(프로젝트 공통 정책)

(3) 채팅방 이름 수정 모달 (/chat/[roomId]/settings)

  • 입력 규칙
    • 최대 10자
    • 공백만 입력 불가 (trim() 결과 빈 문자열이면 실패)
  • 실패 처리(UI)
    • 10자 초과 → 추가 입력 제한 또는 helper text
    • 공백만 입력/빈 값 → 저장 버튼 비활성화 또는 helper text/토스트로 안내

(4) 첨부(이미지/파일) (/chat/[roomId])

  • 이미지
    • 최대 9장
    • 허용 확장자/타입: jpg, jpeg, png, webp
    • 용량 제한: 요구사항 기준 (프로젝트에서 정한 값으로 상수화 권장)
  • 파일(PDF)
    • 최대 1개
    • 허용 형식: PDF
    • 용량 제한: 요구사항 기준
  • 실패 처리(UI)
    • 제한 위반 시 AlertModal/Toast 노출
    • 실패한 파일은 첨부 목록에 반영하지 않음(기존 선택 유지)

2) 서버 측(백엔드)에서도 별도 검증 로직 존재

  • 채팅방 생성
    • 요청 사용자 인증/권한 확인
    • 초대 대상(userIds) 유효성 검증
    • (1:1) 기존 방 존재 시 처리 정책(기존 방 반환 등)은 서버 정책을 따른다
  • 대화 조회/메시지 전송
    • roomId 접근 권한 검증(참여자 여부)
  • 파일 업로드/메시지 첨부
    • MIME 타입/확장자/용량 제한 검증
    • 위반 시 4xx 에러 반환 → 프론트는 공통 에러 정책으로 토스트/모달 처리

용어 정의 (Glossary) (Optional)

  • REST: 과거 데이터(목록/히스토리)를 요청-응답으로 가져오는 방식
  • WebSocket: 서버와 연결을 유지해서 실시간으로 메시지를 주고받는 연결
  • STOMP: WebSocket 위에서 SEND/SUBSCRIBE 같은 규칙으로 메시지를 주고받는 프로토콜
  • subscribe: 특정 채팅방(roomId)의 메시지를 받아보는 구독
  • publish: 메시지를 서버로 전송하는 동작
  • Optimistic UI: 서버 응답 전에 화면에 먼저 메시지를 보여주는 UX(실패 시 롤백/재시도 필요)
  • Infinite Scroll: 스크롤로 다음 데이터 페이지를 계속 불러오는 패턴
⚠️ **GitHub.com Fallback** ⚠️