Files
sodalive-ios/docs/20260708_홈_채팅_탭/prd.md
2026-07-08 15:54:07 +09:00

11 KiB

PRD: 홈 채팅(대화) 탭

1. Overview

메인 하단 탭의 채팅 탭(MainTab.chat)에 진입했을 때 표시하는 V2 대화 목록 화면을 제작한다. 화면은 상단 DefaultTitleBar(좌측 화면명 대화), 필터용 Capsule Tab(전체 / AI 채팅 / DM), 채팅방 목록으로 구성한다.

목록 API는 GET /api/v2/chat/rooms를 사용하며 filter(ALL / AI / DM)와 cursor 파라미터로 조회한다. 응답의 hasMorenextCursor로 스크롤 pagination을 처리한다.

Figma 참조:

  • 채팅 탭 전체: 177:3466(chat_001), https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=177-3466&m=dev

2. Problem

  • 현재 채팅 탭은 MainViewcontentView에서 MainPlaceholderTabView(title: MainTab.chat.title)로만 연결되어 실제 대화 목록이 없다.
  • 홈 팔로잉 탭의 최근 대화(MainHomeFollowingChatSection)는 가로 스크롤 카드 형태라 채팅 탭의 세로 목록 UI와 구조가 다르다.
  • 채팅방 목록 조회 API(GET /api/v2/chat/rooms)와 filter/cursor 기반 pagination이 연결되어 있지 않다.
  • 탭명과 title bar 좌측 문구가 현재 채팅으로 노출되어 기획의 대화와 다르다.

3. Goals

  • 채팅 탭 진입 시 GET /api/v2/chat/rooms?filter=ALL(첫 페이지)를 호출해 대화 목록을 표시한다.
  • 탭명과 title bar 좌측 화면명을 대화로 변경한다.
  • title bar 우측 메뉴에 ic_bar_cash, ic_bar_search를 순서대로 배치하고 각각 충전 페이지, 기존 검색 페이지로 이동한다.
  • Capsule Tab(전체 / AI 채팅 / DM)으로 filter를 전환하고, 미선택 탭 터치 시 화면의 기존 데이터를 모두 지운 뒤 해당 filter의 첫 페이지를 다시 조회해 표시한다.
  • hasMore == true이고 nextCursor가 있으면 마지막 아이템 노출 시 다음 페이지를 조회해 append한다.
  • 채팅방 아이템은 프로필 이미지, 크리에이터 이름, DM 태그(chatType == "DM"), 마지막 메시지 미리보기, 마지막 메시지 상대/절대 시간을 표시한다.
  • lastMessageAt(ISO-8601, UTC)을 디바이스 timezone 기준으로 변환해 규칙에 맞는 시간 텍스트를 다국어로 표시한다.
  • 채팅방 아이템 터치 시 chatType에 따라 이동한다. AI이면 ChatRoomView(AppStep.chatRoom(id:))로 이동하고, DM이면 이번 범위에서는 이동 대상 화면이 없으므로 라우팅을 연결하지 않는다.

4. Non-Goals

  • unread dot(안 읽음 표시)은 이번 범위에서 표시하지 않는다.
  • 플로팅 버튼은 이번 범위에서 추가하지 않는다.
  • chatType == "DM" 아이템 터치 시 이동할 신규 DM 화면은 이번 범위를 벗어나며 다음 범위에서 만든다.
  • 채팅방 생성/삭제/나가기 등 목록 조회 외 mutation은 다루지 않는다.
  • 기존 홈 팔로잉 MainHomeFollowingChatSection(가로 카드)은 변경하지 않는다.
  • Figma localhost asset URL을 앱 코드에 직접 사용하지 않는다.
  • Pods/**, generated/**, build/**는 수정하지 않는다.

5. Target Users

  • AI 캐릭터 또는 크리에이터와의 대화 목록을 한 곳에서 확인하려는 사용자
  • filter로 AI 채팅 / DM만 골라 보려는 사용자

6. User Stories

  • 사용자는 채팅 탭에 들어가 자신의 대화 목록을 최신 순으로 확인할 수 있다.
  • 사용자는 AI 채팅 탭을 눌러 AI 대화만, DM 탭을 눌러 DM 대화만 볼 수 있다.
  • 사용자는 목록을 아래로 스크롤하면 다음 페이지가 이어서 로드되는 것을 확인할 수 있다.
  • 사용자는 각 대화의 마지막 메시지와 마지막 메시지 시간을 확인할 수 있다.
  • 사용자는 AI 대화 아이템을 눌러 해당 AI 채팅방으로 이동할 수 있다.

7. Core Requirements

7.1 API

신규 V2 조회 API는 채팅 탭 전용 API/Repository로 추가한다. 응답 래퍼는 기존 V2 관례대로 ApiResponse<...>로 디코딩하고, Kotlin Long은 Swift Int로 선언한다.

목록:

  • Method: GET
  • Path: /api/v2/chat/rooms
  • Query parameters:
    • filter: ALL / AI / DM 중 하나. 선택된 Capsule Tab에 대응한다.
    • cursor: 다음 페이지 조회에 사용할 커서. 첫 페이지 조회 시에는 전달하지 않는다.

서버 응답(HomeChatModels)은 채팅 탭 스펙에 맞춰 아래 이름으로 매핑한다. 서버 필드명은 유지하되 Swift 모델 이름만 목적에 맞게 정한다.

// 서버 응답 (참고: HomeChatModels)
data class ChatRoomsResponse(
    val rooms: List<ChatRoomItem>,
    @JsonProperty("hasMore")
    val hasMore: Boolean,
    val nextCursor: String?
)

data class ChatRoomItem(
    val roomId: Long,
    val chatType: String,        // "AI" | "DM"
    val targetName: String,
    val targetImageUrl: String,
    val lastMessage: String,
    val lastMessageAt: String    // ISO-8601 (UTC)
)

Swift 모델 네이밍(결정):

  • 목록 래퍼: MainChatRoomsResponse (rooms, hasMore, nextCursor)
  • 아이템: MainChatRoomItem (roomId, chatType, targetName, targetImageUrl, lastMessage, lastMessageAt)

참고: 홈 팔로잉의 기존 ChatRoomListItemResponse가 동일한 필드 구성을 가지지만, 페이지네이션 응답(hasMore/nextCursor)이 없고 다른 화면(HomeFollowingTabResponse) 전용이므로, 채팅 탭에서는 별도 MainChatRoomItem 모델을 둔다.

7.2 화면 구성

  • root View는 MainContentView 패턴을 따른다: DefaultTitleBar + Capsule Tab + 목록 + Color.black.ignoresSafeArea().
  • title bar: DefaultTitleBar(title: MainTab.chat.title)를 사용하고, 좌측 화면명은 대화로 노출된다(§7.6 참고). 우측 메뉴는 ic_bar_cash, ic_bar_search 순서로 노출한다. ic_bar_cash 터치 시 충전 페이지(AppStep.canCharge(refresh:)), ic_bar_search 터치 시 기존 검색 페이지(AppStep.search)로 이동한다.
  • filter Capsule Tab: 기존 CapsuleTabBar<Item>를 재사용하고 전체 / AI 채팅 / DM 3개를 노출한다.
  • 목록: 세로 리스트(스크롤). 각 행은 신규 채팅방 리스트 아이템 컴포넌트로 표시한다.

7.3 채팅방 리스트 아이템

  • 좌측: 원형 프로필 이미지(DownsampledKFImage + Circle clip 패턴 재사용).
  • 우측 영역: 크리에이터 이름(bold), 마지막 메시지 미리보기(1줄, tail 말줄임), 마지막 메시지 시간 텍스트.
  • chatType == "DM"이면 DM 태그를 함께 표시한다(기존 DirectTagView 스타일: Color.soda400 배경, 흰색 텍스트).
  • unread dot은 표시하지 않는다.

7.4 filter 전환 동작

  • 현재 선택되어 있지 않은 Capsule Tab을 터치하면 해당 filter로 첫 페이지 API를 호출한다.
  • 첫 페이지 로딩 시 화면의 기존 목록 데이터를 모두 비우고, 응답으로 새로 세팅해 표시한다.
  • 이미 선택된 탭을 다시 터치하면 재요청하지 않는다.

7.5 pagination

  • 첫 페이지: cursor 없이 조회하고 결과로 목록을 교체한다.
  • 다음 페이지: 마지막 아이템 노출 시 hasMore == true이고 nextCursor가 있으면 cursor = nextCursor로 조회하고 결과를 append한다.
  • 중복 호출 방지를 위해 로딩 상태 guard와 요청 경합 방지(latestRequestId 패턴)를 둔다.

7.6 탭/타이틀 문구

  • I18n.Main.Tab.chat의 한국어 값을 채팅대화로 변경한다. 이 값은 하단 탭명과 title bar 좌측 화면명에 모두 사용된다.
  • 영어/일본어 값은 대화(conversation) 의미에 맞춰 Talks / トーク로 변경한다.
  • 이 문구는 V2 MainTab과 V1 BottomTabView에서 공유되므로 두 곳 모두 동일하게 대화로 노출된다.

7.7 시간 표시 규칙(다국어)

lastMessageAt(ISO-8601, UTC)을 디바이스 timezone으로 변환 후 아래 규칙으로 표시한다.

  1. 일주일(7일) 이내: 상대 시간(다국어). 기존 I18n.Time(justNow/minutesAgo/hoursAgo/daysAgo)을 재사용한다.
  2. 일주일 초과 & 올해 수신: M월 d일 형태(다국어).
    • 한국어: M월 d일
    • 영어: MMM d
    • 일본어: M月d日
  3. 올해가 아닌 과거 연도 수신: yyyy.MM.dd 형태.

기존 DateParser.relativeTimeText는 7일 초과 시 개월/년 전을 반환하므로 이 규칙과 다르다. 채팅 목록 전용 날짜 포맷 함수를 신규로 추가한다(기존 함수는 변경하지 않는다).

7.8 아이템 터치 라우팅

  • chatType == "AI": AppState.shared.setAppStep(step: .chatRoom(id: roomId))ChatRoomView(roomId:)로 이동한다(extras는 roomId로 충분).
  • chatType == "DM": 이동할 화면이 다음 범위이므로 라우팅을 연결하지 않는다(액션 미연결).

7.9 Edge Cases

  • 빈 목록: empty state 문구를 표시한다.
  • 첫 페이지 로딩 중 filter 재전환: 이전 요청 결과가 화면에 반영되지 않도록 요청 경합을 방지한다.
  • nextCursor가 없거나 hasMore == false: 추가 조회를 하지 않는다.
  • lastMessageAt 파싱 실패: 원문 문자열을 fallback으로 표시한다(기존 DateParser 관례와 동일).

8. UX / UI Expectations

  • 배경은 Color.black, 폰트/컬러/spacing은 기존 V2 토큰(appFont, SodaSpacing, Color.soda400/gray900 등)을 따른다.
  • Capsule Tab 선택 시 Color.soda400 배경으로 강조(기존 CapsuleTabBar 동작 그대로).
  • 목록 스크롤 시 하단 도달 전에 자연스럽게 다음 페이지가 이어지도록 한다.

9. Technical Constraints

  • 신규 View/ViewModel/Repository/Model은 SodaLive/Sources/V2/Main/Chat/** 아래에 작성한다(콘텐츠 탭 V2/Main/Content/** 구조와 동일한 규칙).
  • 여러 페이지 공용 컴포넌트는 V2/Component/**, 채팅 탭 전용 컴포넌트는 채팅 폴더 하위 Components에 배치한다.
  • API는 Moya TargetType enum + Repository(requestPublisherResponse) + ViewModel(Combine, ApiResponse<T> 디코딩) 패턴을 따른다.
  • 다국어 문자열은 I18npick(ko:en:ja:)로 추가한다.

10. Open Questions

  • 현재 없음.

11. Confirmed Decisions (2026-07-08)

  • 플로팅 버튼은 추가하지 않는다.
  • title bar 우측 메뉴는 ic_bar_cash, ic_bar_search 순서로 노출한다.
  • ic_bar_cash는 충전 페이지, ic_bar_search는 기존 검색 페이지로 이동한다.
  • chatType == "DM" 터치 시 이동할 DM 화면은 다음 범위에서 추가한다.
  • I18n.Main.Tab.chat의 영어/일본어 문구는 Talks / トーク로 확정한다.