채팅(1:1) 도메인 테크 스펙 - 100-hours-a-week/9-team-Devths-WIKI GitHub Wiki
- Devths의 채팅 도메인(V1)은 사용자가 팔로잉 관계의 상대와 1:1 대화를 빠르게 시작하고, 대화 내역을 안정적으로 조회/전송하며, 읽지 않은 메시지를 놓치지 않도록 하는 것을 목표로 함
-
핵심 결과 (Key Result) 1:
- 사용자가 채팅방 생성 → 상세 진입 → 메시지 전송까지 끊김 없이 완료(에러/무한 로딩/라우팅 실패 0% 지향)
-
핵심 결과 (Key Result) 2:
- 채팅방 목록에서 최신 메시지/시간/안읽음 표시가 정확히 동작(정렬 오류/표기 오류 최소화)
-
핵심 결과 (Key Result) 3:
- 실시간(WebSocket/STOMP)과 히스토리(REST) 결합 구조를 표준화해서 유지보수 가능한 구조 확보
- 구독/해제 누락, 중복 연결, 스크롤 점프 등 채팅 특유 버그 방지
- 채팅은 목록/상세/설정/생성 화면이 분리되어 있고, REST(과거 조회) + WebSocket(실시간)을 같이 다뤄야 해서 상태가 복잡해지기 쉬움
- 무한 스크롤(목록/메시지) + 정렬
- 방 진입 시점에 히스토리 로드 + 실시간 구독 순서
- 전송 실패 시 UI(재전송/삭제)처리
- 서버 상태(채팅방 목록/메시지 히스토리)는 TanStack Query로 관리함
- 캐싱/무한스크롤/리패치 같은 서버 데이터 관리를 라이브러리가 표준 방식으로 처리해줘서, 목록 정렬/페이지네이션 버그를 줄일 수 있음
- 실시간 연결/구독 상태는 WebSocket(STOMP) 매니저를 별도 모듈로 분리해서 관리함
- 채팅방 이동 시 구독 해제 누락, 중복 연결로 인한 메시지 중복 수신 같은 실시간 특유 버그를 방지할 수 있음
- UI 상태(모달 열림/닫힘, 선택된 유저/첨부 파일, 입력값)는 Zustand 또는 로컬 상태(usState/userReducer)로 관리함
- 서버 데이터와 섞이지 않아서 화면 로직이 단순해지고, 모달/입력 상태가 리렌더링이나 캐시 업데이트에 흔들리지 않음
⇒ 채팅 기능을 안정적으로 구현하고 버그를 줄일 수 있음
- 백엔드 API 명세
- 화면 설계서
- 그룹 채팅
- 음성/영상 통화
- 파일 첨부 확장(docx, zip 등) / 대용량 업로드 최적화
채팅 도메인은 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) 매니저(싱글톤)에서 통합 관리한다.
→ 방 이동 시 중복 구독, 재진입 시 중복 연결 같은 실시간 채팅의 대표 버그를 예방하기 위함
(1) ChatRoomListPage : 채팅 생성 페이지
- 주요 기능
- 1:1 채팅방 목록 조회 (10개 단위 무한 스크롤)
- 최신 메시지 기준 정렬 (최근 메시지 시간이 최신인 방이 상단)
- 안 읽음 표시 (읽지 않은 메시지가 있으면 빨간 점 표시)
- 채팅방 생성 페이지 이동
-
FloatingCreateChatButton(+)클릭 →/chat/new이동
-
- 빈 상태 처리
- 채팅방이 없으면
"새롭게 채팅을 시작해보세요"문구 노출
- 채팅방이 없으면
- 사용 컴포넌트
-
ChatRoomCategoryToggle- V1은 개인(PRIVATE) 고정이지만, 추후 확장 대비 UI 유지
ChatRoomListChatRoomListItemCardFloatingCreateChatButtonEmptyState
-
- 데이터 로딩 시점
- 페이지 진입 즉시
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}로 이동
- 완료 클릭 시
- 팔로잉 유저 검색(닉네임 기반)
- 사용 컴포넌트
UserSearchBarFollowingUserListUserSelectRow-
PrimaryButton(완료) -
Toast/AlertModal
- 입력값 검증 및 헬퍼 텍스트(검색)
- 검색어 미입력(빈 값/공백만) →
"검색어를 입력해 주세요."(검색 실행 X) - 2자 미만 →
"검색어는 2자 이상 입력해 주세요."(검색 실행 X) - 10자 초과 → 추가 입력 제한 또는
"검색어는 최대 10자까지 입력할 수 있습니다."
- 검색어 미입력(빈 값/공백만) →
- 검색 결과 표시
- 결과가 없으면:
"해당하는 유저가 없습니다." - 입력 중 검색어는 화면 전환 없이 유지
- 결과가 없으면:
- 예외 처리(팔로잉 0명)
-
"아직 팔로우 하고 있는 유저가 없습니다"문구 노출
-
- 유효성 검사 실패 토스트
- 1:1에서 1명 초과 선택 시:
"개인 채팅방은 1인 필수 선택입니다." - (그룹 관련 문구는 V1 범위에서 제거)
- 1:1에서 1명 초과 선택 시:
- 데이터 로딩 시점
- 페이지 진입 즉시 팔로잉 목록 첫 페이지 로드
- 스크롤 하단 도달 시 다음 페이지 로드
- 검색은 엔터/버튼 클릭 시 실행
- 라우팅:
/chat/new
(3) ChatRoomDetailPage : 채팅방 상세 페이지 (1:1)
-
주요 기능
- 과거 메시지 로드(REST) + 무한 스크롤
- 진입 시 최근 메시지 10개를 먼저 조회
- 사용자가 스크롤을 위로 올리면
lastId(cursor)를 이용해 이전 메시지 페이지를 추가 로드 - 추가 로드된 메시지는 리스트 앞쪽(prepend)에 붙여서 과거로 확장되는 구조
- 실시간 송수신(WebSocket/STOMP)
- 방 진입 후 해당
roomId를 subscribe - 새 메시지 수신 시 메시지 리스트에 즉시 append
- 방 진입 후 해당
- 메시지 전송(TEXT / IMAGE)
- TEXT: 입력값 전송
- IMAGE: 이미지 첨부 후 전송(썸네일 표시 → 클릭 시 원본 뷰어)
- 메시지 삭제
- 메시지 2초 롱프레스 → 삭제 확인 모달 → 삭제 시 메시지 내용이
"삭제된 메시지"로 변경
- 메시지 2초 롱프레스 → 삭제 확인 모달 → 삭제 시 메시지 내용이
- 읽지 않은 메시지 구분선
- 읽지 않은 메시지가 있는 상태로 입장하면 마지막 읽은 지점에
"여기까지 읽었습니다"구분선 표시 + 해당 지점으로 스크롤 이동 - 채팅방에 들어갈 때 모든 메시지를 읽음 처리
- 읽지 않은 메시지가 있는 상태로 입장하면 마지막 읽은 지점에
- 실패 메시지 처리
- 네트워크/서버 문제로 전송 실패 시 메시지를 실패 상태로 보관
- 사용자는 해당 메시지에 대해 재전송 또는 삭제 가능
- 서버 오류는 최대 3회 재시도 후 실패로 확정
- 과거 메시지 로드(REST) + 무한 스크롤
-
사용 컴포넌트
-
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명만 노출
- 유저 클릭 시 유저 조회 모달/프로필 모달로 연결 가능(요구사항에 맞춰 선택)
- 나가기(모달 확인)
- 나가기 버튼 클릭 → 확인 모달 → 확인 시 채팅방 퇴장 처리
- 내 채팅 목록에서만 제거되고 상대방은 유지(요구사항 정책)
- 알림 토글 ON/OFF 저장
- 사용 컴포넌트
AlarmToggleRow-
EditTitleRow+EditTitleModal RecentImagesPreviewParticipantList-
DangerButton(나가기) +ConfirmModal
- 입력값 검증 및 예외 처리
- 채팅방 이름 수정(모달)
- 최대 10자
- 공백만 입력 불가
- 10자 초과는 추가 입력 제한 또는 helper text 처리
- 최근 이미지 미리보기
- 이미지가 1개도 없으면 해당 섹션 숨김 또는 “공유된 이미지가 없습니다” 표시(UX 정책 중 택1)
- 상대가 나간/탈퇴 등으로 정보가 없을 때
- 상대 닉네임/프로필은
(알 수 없음)또는 기본 UI로 표시
- 상대 닉네임/프로필은
- 채팅방 이름 수정(모달)
- API 연동
- 알림 토글 저장
-
PATCH
/api/chatrooms/{roomId} -
Body:
{ "isAlarmOn": true | false }
-
PATCH
- 채팅방 이름 개인화 수정
-
PUT
/api/chatrooms/{roomId} -
Body:
{ "roomName": "이직 준비방" }
-
PUT
- 이미지 조회 (최근 이미지 미리보기 4개)
- 백엔드 문서에 최근 4개 전용 필드는 보이지 않고, 대신 이미지 목록 조회 API가 제공됨
- GET
/api/chatrooms/{roomId}/images?size=n&lastId=k - 여기서 프론트는
size=4로 호출해서 최근 4개 썸네일로 쓰면 됨
- GET
- GET
/api/chatrooms/{roomId}/images?size=4
- 백엔드 문서에 최근 4개 전용 필드는 보이지 않고, 대신 이미지 목록 조회 API가 제공됨
- 채팅방 나가기(퇴장)
-
DELETE
/api/chatrooms/{roomId} -
Header:
Authorization: Bearer {accessToken}
-
DELETE
- 알림 토글 저장
- 데이터 로딩 시점
- 진입 시 채팅방 설정 정보 로드
- 서버에서 설정 조회 API가 없으면:
- 기존
room정보 + 히스토리 일부를 기반으로 화면 구성(대체 플로우)
- 기존
- 서버에서 설정 조회 API가 없으면:
- 진입 시 채팅방 설정 정보 로드
- 라우팅:
/chat/[roomId]/settings
- [Page]: 라우트에 매핑되는 페이지 컴포넌트
- [Container]: 데이터/이벤트를 묶는 상위 컴포넌트(페이지 안에서 분리 가능)
- [UI]: 순수 UI 표시 컴포넌트
- [Modal/Sheet]: 모달/바텀시트/뷰어
- [Common]: 채팅 도메인이 “사용”하지만 공통 도메인에 있는 재사용 컴포넌트(이름 예시)
(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()호출 → 전체 다시 로드
- 내부 상태는 React Query가 대부분 담당
- 에러 처리 / 엣지 케이스
- 중복 데이터 방지
- 서버가 중복 아이템을 내려줄 가능성 대비해서
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
LoadingInitialLoadedEmptyErrorFetchingNext
(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을 내려줘도 됨
- (권장) ISO를 받고 내부에서
-
hasUnread- true면 빨간 점 표시
-
- 내부 상태 & 이벤트
- 상태 없음(표시용)
- 이벤트 흐름
- 카드 클릭 →
onClick(roomId)- 상위에서
router.push(/chat/${roomId})
- 상위에서
- 카드 클릭 →
- 에러 처리 / 엣지 케이스
-
recentMessage없으면" "로 대체(레이아웃 유지) -
thumbnailUrlnull이면 기본 아바타(회색 원) - title 6자 초과 시 … 처리(공백 포함 기준)
-
- 스타일링
- 제목/메시지는 한 줄 ellipsis
- 빨간 점은 시간 왼쪽
- Storybook
DefaultNoThumbnailHasUnreadLongTitleEllipsisNoRecentMessage
(5) FloatingCreateChatButton [UI]
- 역할
- 채팅 생성(
/chat/new)으로 진입하는 플로팅+버튼 - “새 채팅 시작” 흐름의 시작점
- 채팅 생성(
- 디자인/동작 핵심
- 화면 우하단 고정
- BottomNav/세이프에어리어 위로 떠야 함
- props & Interface
type FloatingCreateChatButtonProps = {
onClick: () => void;
disabled?: boolean;
};- Props 설명
-
disabled- 목록 로딩 중/권한 문제 등으로 생성 진입을 잠깐 막고 싶을 때 사용
-
- 내부 상태 & 이벤트
- 상태 없음
- 클릭 →
onClick()- 상위에서
router.push('/chat/new')
- 상위에서
- 에러 처리 / 엣지 케이스
- disabled면 클릭 불가
- 모바일에서 키보드/바텀바와 겹치지 않게 위치 조정
- 스타일링
- 원형 버튼 + 그림자
- 접근성: 최소 터치 영역
- Storybook
DefaultDisabled
- 사용자가
/chat진입 →ChatRoomListPage마운트 -
ChatRoomListContainer가 채팅방 목록 1페이지 요청- 예:
GET /api/chatrooms?size=10
- 예:
- 응답 도착
- 성공:
items를ChatRoomList에 전달 →ChatRoomListItemCard목록 렌더 - 실패:
ChatRoomList가ErrorState + RetryButton노출
- 성공:
- 사용자가 스크롤 하단 도달 →
ChatRoomList가onEndReached()호출 -
ChatRoomListContainer.fetchNext()실행 → 다음 페이지 요청- 로딩 중: 리스트 하단에
Spinner표시
- 로딩 중: 리스트 하단에
- 다음 페이지 응답 성공 시
itemsappend → 리스트 갱신(무한 스크롤 반복) - 사용자가 카드 클릭(
ChatRoomListItemCard) →router.push(/chat/${roomId})로 상세 진입 - 사용자가
FloatingCreateChatButton클릭 →router.push('/chat/new')로 생성 페이지 진입
(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)
- 로잉 0명 →
- 스타일링
- 하단 버튼은 키보드 올라올 때 가려지지 않게 처리
(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)
- Enter →
- 에러 처리 / 엣지 케이스
- 공백만 입력, 2자 미만, 10자 초과 →
onSearch에서 요청 막고 helperText 표시(상위/컨테이너) - disabled면 입력/검색 실행 불가
- 공백만 입력, 2자 미만, 10자 초과 →
- 스타일링
- 입력창 + 검색 버튼(또는 아이콘)
- helperText는 입력 하단에 작게 표시
- Storybook
DefaultWithHelperTextDisabled
(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 && !isFetchingNext→onEndReached()
- 하단 도달 +
- 에러 처리 / 엣지 케이스
- 초기 로딩이면 스켈레톤/로딩 표시
- 에러면
errorState렌더(+ 재시도) -
hasNext=false면 추가 로드 트리거 중단
- 스타일링
- Row 간격 고정
- 스크롤 영역 확보(하단 고정 버튼과 겹치지 않게 padding-bottom)
- Storybook
LoadingInitialLoadedEmptyErrorFetchingNext
(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
SelectedUnselectedNoAvatarDisabled
(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
CheckedUncheckedDisabled
(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
DisabledEnabledLoading
(1) 페이지 진입 → 팔로잉 목록 1페이지 로드
- 사용자가
/chat/new진입 →ChatCreatePage마운트 -
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) 응답 처리 → 목록 렌더
- 응답 성공 →
FollowingUserList가 목록 렌더 (UserSelectRow로 그려짐) - 응답 실패(401/500 등) → 공통 정책대로
ErrorState/Toast등 노출
(3) 무한 스크롤 → 다음 페이지 로드
- 사용자가 스크롤 하단 도달 →
FollowingUserList.onEndReached() - 다음 페이지 요청
-
GET
/api/users/me/followings?size=n&lastId=k&nickname={name}
(4) 검색(닉네임 기반) → 엔터/버튼 시에만 요청
- 사용자가 검색어 입력 (
UserSearchBar.onChange)- 입력 중에는 요청 안 함(상태만 저장)
- 사용자가 Enter/검색 버튼 클릭 → 검색 실행
- GET
/api/users/me/followings?size=n&lastId=k&nickname={name}
(5) 1명 선택 → 완료 버튼 활성화
- 사용자가
UserSelectRow클릭 →selectedUserId갱신 - 1명 선택 상태가 되면
CreateChatButton활성화- 체크 표시는
SingleSelectCheckbox가checked=true로 표시
- 체크 표시는
(6) 완료 클릭 → 채팅방 생성(또는 기존 방 반환) → 상세로 이동
- 사용자가 완료 클릭 → 채팅방 생성 요청
-
POST
/api/chatrooms -
Header:
Authorization: Bearer {accessToken}
- 생성 성공(201) → 응답의
roomId확보 →router.push(/chat/${roomId}) - 생성 실패 → 공통 에러 정책(Toast/AlertModal 등) 노출하고 이동 없음
(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페이지 로드 시작
- 히스토리 로드 성공 → WebSocket 연결 및 room subscribe 시작
- 새 메시지 수신 → 메시지 리스트에 append
- 전송(텍스트/첨부) → optimistic 추가(선택) → 실패 시 Failed 처리
- 롱프레스 삭제 → ConfirmModal → 삭제 요청 → UI에 “삭제된 메시지” 반영
- 미읽음 존재 시 →
UnreadDivider를 특정 message 위치에 렌더
- 내부 상태: Container로 위임
- 에러 처리 / 엣지 케이스
-
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로 확정
- REST/WS 중복 메시지:
- 스타일링
- 없음(로직 전용)
(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
DefaultLongTitleEmptyTitleFallback
(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 && !isFetchingPrev→onFetchPrev() - 롱프레스 →
onLongPressMessage(messageId)
- 스크롤 상단 근처 +
- 내부 상태
- 에러 처리 / 엣지 케이스
- prepend 보정 실패하면 “스크롤 튐” 버그 발생 → 반드시 보정 로직 포함
- 메시지 0개일 때 빈 화면 처리(또는 안내 문구)
- 스타일링
- 메시지 간격/그룹핑(연속 메시지) 정책이 있으면 여기서 처리
- Storybook
LoadedWithUnreadDividerFetchingPrevEmpty
(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
DefaultDisabledWithLongText
(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
ClosedImagePickValidImagePickTooManyPdfPickValidInvalidType
(7) UnreadDivider [UI]
- 역할
- 미읽음 메시지 존재 시, “마지막 읽은 메시지”와 “새 메시지” 사이에 구분선 표시
- 입장 시 해당 위치로 스크롤 이동 UX(스크롤 이동은 상위에서)
- 디자인/동작 핵심
- 문구: “여기까지 읽었습니다.”
- 리스트에서 특정 messageId 기준으로 삽입됨
- props & Interface
type UnreadDividerProps = {
visible: boolean;
label?: string; // default: "여기까지 읽었습니다."
};- Props 설명
-
visible: 해당 방에 미읽음이 있을 때 true
-
- 내부 상태 & 이벤트
- 상태 없음
- 에러 처리 / 엣지 케이스
- unread 기준 messageId가 로드 범위 밖이면 표시 못 할 수 있음
- 이 경우 가장 가까운 로드된 지점에 표시하거나, 로드 전략을 조정
- unread 기준 messageId가 로드 범위 밖이면 표시 못 할 수 있음
- 스타일링
- 양쪽 라인 + 가운데 라벨
- Storybook
VisibleHidden
(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
FailedFailedWithLongText
(0) 라우팅 진입
- 사용자가
/chat/{roomId}진입 →ChatRoomDetailPage마운트 -
roomId를number로 파싱해서 이후 API에 사용
(1) (REST) 채팅방 기본 정보 로드 (타이틀/알림설정/최근이미지 등)
- 채팅방 상세 정보 요청
-
GET
/api/chatrooms/{roomId} -
Header:
Authorization: Bearer {accessToken} -
Path Param:
roomId: Long (필수)
- 성공(200) →
ChatHeader에title(roomName/title)세팅, 화면 기본 구성 완료 - 실패(401/403/404/500) → 공통 에러 정책(토스트/모달) + 필요 시
/chat로 이동
(2) (REST) 메시지 히스토리 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 (필수)
- 성공(200) →
MessageList에 메시지 렌더 - 실패(401/403/404/500) → 공통 에러 정책(토스트/모달) + 재시도 UI 제공
(3) (REST) 위로 스크롤 무한 로딩
- 사용자가 위로 스크롤(상단 근접) → 이전 메시지 요청
-
GET
/api/chatrooms/{roomId}/messages?size=20&lastId={현재_가장_오래된_messageId}- 응답의
nextCursor/hasNext를 기반으로 계속 paging
- 응답의
- 성공 시 prepend + 스크롤 보정(점프 방지)
(4) (WS/STOMP) 실시간 연결 + 구독
- 히스토리 1페이지 로드 성공 이후, WebSocket 연결
-
STOMP URL:
ws://{domain}/ws-chat -
Header:
Authorization: Bearer {accessToken} -
응답
-
101: 연결 성공 -
401: accessToken 만료 -
404: 엔드포인트 경로 오류
-
- 연결 성공 후 해당 room 구독
-
SUBSCRIBE:
/sub/chat/room/{roomId}
- 새 메시지 수신 시(payload) →
MessageList에 append
- payload에
isDeleted가 true면 “삭제된 메시지”로 렌더
(4) (전송: TEXT) 입력 → WS 송신
- 사용자가
ChatComposer에서 텍스트 전송 - STOMP로 송신
-
PUBLISH:
/pub/chat/message - Payload
{
"roomId":123,
"type":"TEXT",
"content":"안녕하세요!",
"s3Key":null
}- 서버가 브로드캐스트 → 구독(
/sub/chat/room/{roomId})으로 수신 → 리스트 append
(5) (전송: IMAGE) 첨부 → Presigned 발급 → S3 업로드 → (메타 등록) → WS 송신
- 사용자가
AttachmentSheet에서 이미지 선택 - 업로드용 Presigned URL 발급
-
POST
/api/files/presigned -
Header:
Authorization: Bearer {accessToken} - Body
{
"fileName":"photo.jpg",
"mimeType":"image/jpeg"
}
- FE가 presignedUrl로 S3에 직접 업로드
- (권장) 파일 메타 등록
-
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
}
- WS로 이미지 메시지 송신
-
PUBLISH
/pub/chat/message
{
"roomId":123,
"type":"IMAGE",
"content":null,
"s3Key":"chat/123/photo.jpg"
}
- 구독으로 수신 →
ImageMessageBubble렌더(썸네일)
(7) (삭제) 롱프레스 → ConfirmModal → REST 삭제
- 메시지 롱프레스 →
ConfirmModal오픈 - 삭제 확정 클릭 → 삭제 요청
-
DELETE
/api/chatrooms/{roomId}/messages/{messageId} -
Header:
Authorization: Bearer {accessToken} -
Path Param
roomId: LongmessageId: Long
-
성공:
204 No Content
- 성공 시 UI에서 해당 메시지를 “삭제된 메시지”로 표시(또는 서버 이벤트 수신으로 반영)
(8) (UnreadDivider) “여기까지 읽었습니다” + 읽음 커서 처리
- 메시지 히스토리 조회 응답에
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: LongnextCursor: LonghasNext: Boolean
- 성공(200) 시
MessageList렌더 단계에서,
-
lastReadMsgId를 기준으로 “여기까지 읽었습니다” 구분선을 표시한다.- 예:
messageId === lastReadMsgId인 메시지 다음 위치에UnreadDivider삽입
- 예:
-
lastReadMsgId가 없거나(첫 입장 등) 메시지 범위에 없으면 구분선은 생략하거나 기본 위치로 처리한다.
- 사용자가 채팅방에 들어오면(정책에 따라) 읽음 커서를 갱신한다.
-
PATCH
/api/chatrooms/{roomId} -
Header:
Authorization: Bearer {accessToken} - Body
{
"lastReadMsgId":12345
}
- 갱신 타이밍(정책 택1)
- (A) 입장 즉시: 현재 로드된 메시지 중 최신
messageId로 갱신 - (B) 스크롤이 최신 도달 시: 사용자가 실제로 최신까지 확인했을 때 갱신
- 성공(200) 시 다음 재진입에서
lastReadMsgId가 갱신된 값으로 내려와 구분선 위치가 업데이트된다. - 실패(400/401/403/404/500) 시 공통 에러 정책(토스트/모달)에 따라 안내한다.
(1) ChatRoomSettingsPage [Page]
- 역할
-
/chat/[roomId]/settings라우트 엔트리(Page) - 설정 화면의 전체 UI 조립(알림 토글/이름 수정/최근 이미지/나가기)
- 성공 시 토스트 노출, 필요 시 라우팅(
/chat이동) 처리
-
- 디자인/동작 핵심
- 상단: 설정 타이틀 + 뒤로가기(선택)
- 섹션 1: 알림 토글 (
AlarmToggleRow) - 섹션 2: 채팅방 이름 수정 진입 (
EditTitleRow→EditTitleModal) - 섹션 3: 최근 이미지 4개 (
RecentImagesPreview) - 섹션 4: 나가기 (
DangerButton→ConfirmModal)
- props & Interface
type ChatRoomSettingsPageProps = {
params: { roomId: string };
};- Props 설명
-
params.roomId: string → 내부에서 number로 파싱해 사용
-
- 내부 상태 & 이벤트
- 상태(권장: Container로 위임)
isEditTitleOpenisLeaveConfirmOpen
- 이벤트 흐름
- 페이지 마운트 → 설정 정보 로드(없으면 room 기본값으로 구성)
- 토글 변경 → PATCH
- 이름 수정 → 모달 열기 → PUT
- 나가기 → ConfirmModal → DELETE →
/chat이동
- 상태(권장: Container로 위임)
- 에러 처리 / 엣지 케이스
- roomId 파싱 실패 → 안내 후
/chat이동(정책) - 401/403 → 공통 인증/인가 처리(토스트/로그인 이동 등 공통 정책)
- 404 → 채팅방 없음 안내 후 목록 이동
- roomId 파싱 실패 → 안내 후
- 스타일링
- 섹션 간 간격, 하단 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 }
-
PATCH
- 이름 수정
-
PUT
/api/chatrooms/{roomId} - Body:
{ "roomName": "..." }
-
PUT
- 최근 이미지 4개
-
GET
/api/chatrooms/{roomId}/images?size=4
-
GET
- 나가기
-
DELETE
/api/chatrooms/{roomId} - 성공(204) →
/chat이동 + 목록 캐시 invalidate
-
DELETE
- 알림 토글 저장
- 상태
- 에러 처리 / 엣지 케이스
- 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
OnOffDisabled
(4) EditTitleRow [UI]
- 역할
- “채팅방 이름 수정” 모달을 여는 진입 포인트
- 디자인/동작 핵심
- 현재 표시되는 이름(roomName)을 함께 보여주면 사용자가 맥락 파악 쉬움
- props & Interface
type EditTitleRowProps = {
currentTitle: string;
onOpen: () => void;
disabled?: boolean;
};- 내부 상태 & 이벤트
- 상태 없음
- 클릭 →
onOpen()→ 상위에서EditTitleModalopen
- 에러 처리 / 엣지 케이스
- currentTitle이 비어있으면 placeholder 처리(예: “채팅방”)
- 스타일링
- 오른쪽 chevron 아이콘 등으로 “수정 가능” 표현(선택)
- Storybook
DefaultDisabled
(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
OpenDefaultInvalidBlankMaxLengthLoading
(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
LoadingWith4ItemsWithLessItemsEmpty
(7) DangerButton [UI]
- 역할
- “채팅방 나가기(퇴장)”을 시작하는 버튼(진입 포인트)
- 클릭 시
ConfirmModal을 열도록 상위에 이벤트 전달
- 디자인/동작 핵심
- 위험 액션임을 색/라벨로 강조
- 버튼 클릭 → 확인 모달 필수
- props & Interface
type DangerButtonProps = {
onClick: () => void;
disabled?: boolean; // 나가기 요청 중 등
};- 내부 상태 & 이벤트
- 상태 없음
- 클릭 →
onClick()(상위에서 confirm modal open)
- 에러 처리 / 엣지 케이스
- disabled면 클릭 불가(연타 방지)
- 스타일링
- Danger 스타일(빨강 계열)
- 터치 영역 확보
- Storybook
DefaultDisabled
(0) 라우팅 진입
- 사용자가
/chat/{roomId}/settings진입 →ChatRoomSettingsPage마운트 -
roomId를 number로 파싱해서 이후 API에 사용
(1) (REST) 설정 기본 정보 로드
- 채팅방 상세(설정에 필요한 값 포함) 요청
-
GET
/api/chatrooms/{roomId} -
Header:
Authorization: Bearer {accessToken} -
Path Param:
roomId: Long (필수)
- 성공(200) → 화면 초기값 세팅
- 알림 토글:
isAlarmOn - 채팅방 이름:
roomName - 참여자 정보(있다면): 본인/상대 정보
- 실패(401/403/404/500) → 공통 에러 정책(토스트/모달) + 필요 시
/chat이동
(2) (REST) 최근 이미지 4개 로드
- 최근 이미지 썸네일 요청
-
GET
/api/chatrooms/{roomId}/images?size=4 -
Header:
Authorization: Bearer {accessToken} -
Path Param:
roomId: Long (필수)
- 성공(200) →
RecentImagesPreview에 최대 4개 렌더 - 실패 → 이미지 섹션만 비우고(또는 “로드 실패”) 토스트 안내
(3) (Mutation) 알림 토글 저장
- 사용자가
AlarmToggleRow에서 토글 변경 → 저장 요청
-
PATCH
/api/chatrooms/{roomId} -
Header:
Authorization: Bearer {accessToken} -
Body:
{ "isAlarmOn": true | false }
- 성공 → 토글 상태 유지 +
Toast로 “저장됨” 안내 - 실패(400/401/403/404/500) → 토스트/모달 안내 + (권장) 토글 값 롤백
(4) (Mutation) 채팅방 이름 수정(모달 → 저장)
- 사용자가
EditTitleRow클릭 →EditTitleModal오픈 - 사용자가 이름 입력 후 확인 → 저장 요청
-
PUT
/api/chatrooms/{roomId} -
Header:
Authorization: Bearer {accessToken} -
Body:
{ "roomName": "이직 준비방" }
- 성공 → 모달 닫힘 + 화면의 roomName 즉시 갱신 +
Toast - 실패 → 모달 유지(권장) + 토스트로 실패 안내
(5) (Mutation) 채팅방 나가기(퇴장)
- 사용자가
DangerButton클릭 →ConfirmModal오픈 - 확인 클릭 → 나가기 요청
-
DELETE
/api/chatrooms/{roomId} -
Header:
Authorization: Bearer {accessToken} -
Path Param:
roomId: Long (필수) -
성공:
204 No Content
- 성공 시 처리
-
/chat로 이동 - 채팅방 목록 캐시 invalidate → 목록에서 해당 방 제거
- 실패 처리
-
400요청 파라미터 오류 -
401인증 실패 -
403인가 실패 -
404채팅방 없음 -
500서버 내부 오류→ 공통 에러 정책에 따라 토스트/모달 노출 (필요 시 목록으로 이동)
(1) /chat 채팅방 목록 컴포넌트 간 관계
ChatRoomListPage
└─(ChatRoomListContainer)//ReactQuery+ 무한스크롤 로직
├─ChatRoomCategoryToggle
├─ChatRoomList
│ └─ChatRoomListItemCard(반복)
├─FloatingCreateChatButton
└─EmptyState/Skeleton/ErrorState
-
핵심 흐름
- Container가
GET /api/chatrooms...로 목록 가져옴 -
ChatRoomList가 렌더 + 바닥 닿으면 다음 페이지 요청 -
ChatRoomListItemCard클릭 →/chat/{roomId} -
FloatingCreateChatButton클릭 →/chat/new
- Container가
(2) /chat/new 채팅 생성(1:1) 컴포넌트 간 관계
ChatCreatePage
└─ (ChatCreateContainer)// 팔로잉 조회 + 검색 + 선택 + 생성 mutation
├─ UserSearchBar
├─ FollowingUserList
│ └─UserSelectRow (반복)
│ └─ SingleSelectCheckbox
├─ CreateChatButton
└─ NoFollowingEmptyState / NoSearchResultState / Toast / AlertModal
-
핵심 흐름
- Container가 팔로잉 목록 로드(+ 무한스크롤)
-
UserSearchBar가 검색 트리거 -
UserSelectRow에서 1명 선택 -
CreateChatButton→POST /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
- 삭제는
ConfirmModal→DELETE /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
- 알림:
- 채팅은 전역이 필요한 상태만 최소로 둠
- 예시:
-
connectionStatus(connected/disconnected) -
pendingMessages(전송 실패한 임시 메시지 큐) -
attachmentDraft(선택한 이미지 목록 등)
-
- 화면별 입력 상태:
- 검색어 입력값
- 모달 open/close
- 장문 펼치기 토글 상태(메시지별)
-
chatRooms(채팅방 목록) →useInfiniteQuery -
chatMessages(roomId)(히스토리) →useInfiniteQuery - 설정(알림, 제목)은 mutation 후 관련 쿼리 invalidate
(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
- HTTP 클라이언트:
axios(프로젝트 공통 정책) - 인증 헤더: 모든 채팅 API는
Authorization: Bearer {accessToken}포함 - React Query 사용
- 목록/히스토리:
useInfiniteQuery - 수정/삭제/나가기:
useMutation - 성공 후 정책
- 설정 변경(PATCH/PUT):
invalidateQueries(['chatRoom', roomId]) - 나가기(DELETE):
/chat이동 후invalidateQueries(['chatRooms', { type: 'PRIVATE' }])
- 설정 변경(PATCH/PUT):
- 목록/히스토리:
- 에러 처리
- 401/403/404/500 등은 공통 에러 정책에 따라
Toast/Modal노출 - 목록/히스토리는
ErrorState + RetryButton제공
- 401/403/404/500 등은 공통 에러 정책에 따라
- Public / Protected 정책은 공통 AuthGate 흐름을 따른다.
- 채팅 도메인의 모든 라우트는 Protected Route(로그인 필요) 로 처리한다.
-
/chat- 채팅방 목록 페이지 (1:1 목록 + 무한 스크롤)
-
/chat/new- 채팅 생성 페이지 (1:1 전용, 팔로잉 유저 검색/선택)
-
/chat/[roomId]- 채팅방 상세 페이지 (히스토리 + 실시간 송수신)
-
/chat/[roomId]/settings- 채팅방 설정 페이지 (알림/이름 수정/최근 이미지/나가기)
- API 호출 중 401(인증 실패) 발생 시
- 공통 refresh/재시도 정책을 따르고, 최종 실패 시 로그인/랜딩으로 이동 (프로젝트 공통 규칙 적용)
(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 노출
- 실패한 파일은 첨부 목록에 반영하지 않음(기존 선택 유지)
-
채팅방 생성
- 요청 사용자 인증/권한 확인
- 초대 대상(userIds) 유효성 검증
- (1:1) 기존 방 존재 시 처리 정책(기존 방 반환 등)은 서버 정책을 따른다
-
대화 조회/메시지 전송
- roomId 접근 권한 검증(참여자 여부)
-
파일 업로드/메시지 첨부
- MIME 타입/확장자/용량 제한 검증
- 위반 시 4xx 에러 반환 → 프론트는 공통 에러 정책으로 토스트/모달 처리
- REST: 과거 데이터(목록/히스토리)를 요청-응답으로 가져오는 방식
- WebSocket: 서버와 연결을 유지해서 실시간으로 메시지를 주고받는 연결
- STOMP: WebSocket 위에서 SEND/SUBSCRIBE 같은 규칙으로 메시지를 주고받는 프로토콜
- subscribe: 특정 채팅방(roomId)의 메시지를 받아보는 구독
- publish: 메시지를 서버로 전송하는 동작
- Optimistic UI: 서버 응답 전에 화면에 먼저 메시지를 보여주는 UX(실패 시 롤백/재시도 필요)
- Infinite Scroll: 스크롤로 다음 데이터 페이지를 계속 불러오는 패턴