Files
2026-07-08 15:54:07 +09:00

12 KiB

계획/TASK: 홈 채팅(대화) 탭

관련 PRD: docs/20260708_홈_채팅_탭/prd.md

구현할 UI 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
    • 구성: StatusBar, title-bar(289:2962), tab-bar(177:3468, capsule), chat-list item(177:3469~177:3473), nav(177:3476)
    • 제외: button-floating(177:3474)은 사용자 확인에 따라 이번 범위에서 추가하지 않는다.

재사용 위젯 vs 신규 위젯

  • 재사용:
    • DefaultTitleBar(SodaLive/Sources/V2/Component/DefaultTitleBar.swift)
    • CapsuleTabBar(SodaLive/Sources/V2/Component/CapsuleTabBar.swift) — filter 탭
    • DownsampledKFImage + Circle clip — 원형 프로필
    • I18n.Time(justNow/minutesAgo/hoursAgo/daysAgo) — 7일 이내 상대 시간
    • ApiResponse<T> 디코딩, Moya TargetType + Repository + ViewModel(Combine) 패턴
    • 라우팅: AppStep.chatRoom(id:)ChatRoomView(roomId:)(AI 채팅방), AppStep.canCharge(refresh:)(충전), AppStep.search(기존 검색)
  • 신규:
    • 채팅 탭 root View / ViewModel / API / Repository / Models
    • filter enum(MainChatFilter: ALL/AI/DM)
    • 채팅방 세로 리스트 아이템 컴포넌트, DM 태그(기존 DirectTagView 스타일 기반)
    • 채팅 목록 전용 날짜 포맷 함수(7일 초과 시 M월 d일 / yyyy.MM.dd, 다국어)
    • I18n 문자열(탭 문구 변경, filter 문구, empty state)

Phase 1: 다국어/문구

  • Task 1.1: 탭/타이틀 문구 변경

    • 대상 파일:
      • 수정: SodaLive/Sources/I18n/I18n.swift
    • 작업 내용:
      • I18n.Main.Tab.chat채팅/Chat/チャット대화/Talks/トーク로 변경한다.
    • 검증 기준:
      • 실행 명령: rg "static var chat: String" SodaLive/Sources/I18n/I18n.swift
      • 기대 결과: 대화 / Talks / トーク로 변경된 문구가 확인된다. MainTab.chat.titleBottomTabView에 동일 반영.
  • Task 1.2: 채팅 탭 전용 I18n 추가

    • 대상 파일:
      • 수정: SodaLive/Sources/I18n/I18n.swift
    • 작업 내용:
      • enum MainChat 신설: filter 문구(전체/AI 채팅/DM), empty state 문구를 pick(ko:en:ja:)로 추가한다.
      • 시간 포맷용 다국어 date format helper 추가(§Phase 4에서 사용): M월 d일 / MMM d / M月d日 패턴.
    • 검증 기준:
      • 실행 명령: rg "enum MainChat" SodaLive/Sources/I18n/I18n.swift
      • 기대 결과: filter/empty/date-format 문구가 확인된다.

Phase 2: 모델과 API 계층

  • Task 2.1: 응답 모델 정의

    • 대상 파일:
      • 생성: SodaLive/Sources/V2/Main/Chat/Models/MainChatRoomsResponse.swift
    • 작업 내용:
      • MainChatRoomsResponse(rooms: [MainChatRoomItem], hasMore: Bool, nextCursor: String?) 정의.
      • MainChatRoomItem(roomId: Int, chatType: String, targetName: String, targetImageUrl: String, lastMessage: String, lastMessageAt: String) 정의(Decodable, Identifiable, id = roomId).
    • 검증 기준:
      • 실행 명령: rg "struct MainChatRoomsResponse|struct MainChatRoomItem|hasMore|nextCursor" SodaLive/Sources/V2/Main/Chat/Models
      • 기대 결과: 래퍼/아이템 모델과 pagination 필드가 확인된다.
  • Task 2.2: filter enum 정의

    • 대상 파일:
      • 생성: SodaLive/Sources/V2/Main/Chat/MainChatFilter.swift
    • 작업 내용:
      • enum MainChatFilter: CaseIterable, Hashable { case all, ai, dm } 정의.
      • queryValue(ALL/AI/DM)와 title(I18n) 제공.
    • 검증 기준:
      • 실행 명령: rg "enum MainChatFilter|queryValue|ALL|AI|DM" SodaLive/Sources/V2/Main/Chat/MainChatFilter.swift
      • 기대 결과: 3개 case와 query/title 매핑이 확인된다.
  • Task 2.3: API/Repository 추가

    • 대상 파일:
      • 생성: SodaLive/Sources/V2/Main/Chat/Repository/MainChatApi.swift
      • 생성: SodaLive/Sources/V2/Main/Chat/Repository/MainChatRepository.swift
    • 작업 내용:
      • MainChatApi.getChatRooms(filter: String, cursor: String?) case, path /api/v2/chat/rooms, method .get.
      • query: filter 항상 전달, cursor는 nil이 아닐 때만 전달.
      • MainChatRepository.getChatRooms(filter:cursor:)AnyPublisher<Response, MoyaError>.
    • 검증 기준:
      • 실행 명령: rg "getChatRooms|/api/v2/chat/rooms|filter|cursor|URLEncoding.queryString" SodaLive/Sources/V2/Main/Chat/Repository
      • 기대 결과: endpoint/파라미터/publisher가 확인된다.

Phase 3: ViewModel(cursor pagination + filter)

  • Task 3.1: 목록 조회/filter/pagination 구현
    • 대상 파일:
      • 생성: SodaLive/Sources/V2/Main/Chat/MainChatViewModel.swift
    • 작업 내용:
      • 상태: rooms, selectedFilter, isLoading, isLoadingNextPage, hasMore, nextCursor, hasLoaded, errorMessage, isShowPopup.
      • fetchFirstPage(filter:): cursor 없이 조회, 성공 시 rooms 교체.
      • applyFilter(_:): 선택 filter 변경 시 기존 목록 초기화 후 첫 페이지 조회(같은 filter 재선택은 무시).
      • fetchNextPageIfNeeded(currentItem:): 마지막 아이템 노출 & hasMore & nextCursor 존재 시 다음 페이지 조회 후 append.
      • latestRequestId 경합 방지, ApiResponse<MainChatRoomsResponse> 디코딩.
    • 검증 기준:
      • 실행 명령: rg "final class MainChatViewModel|fetchFirstPage|applyFilter|fetchNextPageIfNeeded|latestRequestId|hasMore|nextCursor" SodaLive/Sources/V2/Main/Chat/MainChatViewModel.swift
      • 기대 결과: 첫 페이지 교체/append와 경합 방지 로직이 확인된다.

Phase 4: 시간 포맷 유틸

  • Task 4.1: 채팅 목록 전용 날짜 포맷 함수 추가
    • 대상 파일:
      • 수정: SodaLive/Sources/Common/DateParser.swift
    • 작업 내용:
      • 신규 static 함수(예: chatListDateText(fromUTC:now:)) 추가. 기존 relativeTimeText는 변경하지 않는다.
      • 디바이스 timezone(Calendar.current) 기준으로:
        • 7일 이내: I18n.Time 상대 시간.
        • 7일 초과 & 올해: 다국어 M월 d일(MMM d/M月d日).
        • 과거 연도: yyyy.MM.dd.
      • 파싱 실패 시 fallback 원문 반환.
    • 검증 기준:
      • 실행 명령: rg "chatListDateText|M월 d일|yyyy.MM.dd" SodaLive/Sources/Common/DateParser.swift SodaLive/Sources/I18n/I18n.swift
      • 기대 결과: 3구간 분기와 다국어 포맷이 확인된다.

Phase 5: 컴포넌트

  • Task 5.1: 채팅방 리스트 아이템
    • 대상 파일:
      • 생성: SodaLive/Sources/V2/Main/Chat/Components/MainChatRoomListItem.swift
    • 작업 내용:
      • 원형 프로필(DownsampledKFImage) + 이름(bold) + 마지막 메시지(1줄 tail) + 시간 텍스트(DateParser.chatListDateText) 레이아웃.
      • chatType == "DM"이면 DM 태그 표시(soda400 배경, 흰색). unread dot 미표시.
      • item tap closure로 roomId, chatType 전달.
    • 검증 기준:
      • 실행 명령: rg "struct MainChatRoomListItem|DownsampledKFImage|chatListDateText|DM" SodaLive/Sources/V2/Main/Chat/Components/MainChatRoomListItem.swift
      • 기대 결과: 아이템 레이아웃과 DM 태그/시간 표시가 확인된다.

Phase 6: root View 및 탭 연결

  • Task 6.1: 채팅 탭 root View

    • 대상 파일:
      • 생성: SodaLive/Sources/V2/Main/Chat/MainChatView.swift
    • 작업 내용:
      • DefaultTitleBar(title: MainTab.chat.title) + 우측 메뉴(ic_bar_cash, ic_bar_search) + CapsuleTabBar(filter) + 세로 목록(ScrollView/LazyVStack)을 구성한다.
      • ic_bar_cash 터치 시 onTapCanCharge, ic_bar_search 터치 시 onTapSearch closure를 호출한다.
      • CapsuleTabBar 선택 변경을 viewModel.applyFilter(_:)에 연결.
      • 마지막 아이템 onAppear에서 fetchNextPageIfNeeded(currentItem:) 호출.
      • 아이템 tap: chatType == "AI"AppState.shared.setAppStep(step: .chatRoom(id: roomId)), "DM" → 다음 범위의 DM 화면 추가 전까지 미연결.
      • 첫 진입 시 fetchFirstPage(filter: .all) 호출, empty state 처리.
      • 플로팅 버튼은 추가하지 않는다.
    • 검증 기준:
      • 실행 명령: rg "struct MainChatView|DefaultTitleBar|ic_bar_cash|ic_bar_search|CapsuleTabBar|applyFilter|fetchNextPageIfNeeded|chatRoom\\(id:" SodaLive/Sources/V2/Main/Chat/MainChatView.swift
      • 기대 결과: title bar 우측 메뉴/filter/목록/pagination/라우팅 연결이 확인되고 MainChatFloatingButton은 존재하지 않는다.
  • Task 6.2: MainView 채팅 탭 교체

    • 대상 파일:
      • 수정: SodaLive/Sources/V2/Main/MainView.swift
    • 작업 내용:
      • contentView.chat case를 MainPlaceholderTabView(title: MainTab.chat.title)MainChatView(...)로 교체한다.
      • 기존 handleHomeCanChargeTap, handleHomeSearchTapMainChatView의 우측 메뉴 closure로 전달한다.
    • 검증 기준:
      • 실행 명령: rg "case .chat:|MainChatView|handleHomeCanChargeTap|handleHomeSearchTap|MainPlaceholderTabView" SodaLive/Sources/V2/Main/MainView.swift
      • 기대 결과: .chat에서 MainChatView가 렌더되고 placeholder는 제거되며 충전/검색 라우팅 closure가 전달된다.

Phase 7: 프로젝트 등록 및 검증

  • Task 7.1: Xcode 프로젝트에 신규 파일 등록 및 빌드
    • 대상 파일:
      • 수정: SodaLive.xcodeproj/project.pbxproj
    • 작업 내용:
      • 신규 .swift 파일을 타깃 소스에 추가한다.
    • 검증 기준:
      • 실행 명령: docs/agent-guides/build-test-verification.md 기준 빌드.
      • 기대 결과: 빌드 성공, 채팅 탭에서 목록/필터/pagination/AI 라우팅 정상 동작.

검증 기록

  • 2026-07-08 문서 작성: Figma 177:3466 design context/metadata 확인, 기존 재사용 위젯(DefaultTitleBar, CapsuleTabBar, DownsampledKFImage, I18n.Time, AppStep.chatRoom)과 신규 필요 항목(리스트 아이템/플로팅/날짜 포맷/API·Repository·ViewModel·Models)을 분류해 PRD와 본 계획 문서에 반영. 본 범위는 PRD와 계획 문서 작성까지이며 구현은 포함하지 않음.

  • 2026-07-08 문서 보완: 사용자 확인 사항을 반영해 플로팅 버튼을 구현 범위에서 제외하고, title bar 우측 메뉴를 ic_bar_cash/ic_bar_search 순서와 충전/검색 라우팅으로 확정했다. chatType == "DM" 라우팅은 다음 범위의 DM 화면 추가 전까지 미연결로 유지하고, 탭 영어/일본어 문구를 Talks/トーク로 갱신했다. 실행 명령: sed -n으로 PRD/계획 문서와 documentation-policy.md 확인, rg로 기존 V2 title bar 메뉴 및 AppStep.canCharge/AppStep.search 관례 확인.

  • 2026-07-08 구현 완료: I18n.Main.Tab.chat대화/Talks/トーク으로 변경하고 I18n.MainChat 문자열, DateParser.chatListDateText(fromUTC:fallback:now:), V2 채팅 탭의 Model/API/Repository/ViewModel/View/Row 컴포넌트를 추가했다. .chat 탭은 MainChatView로 교체했고, 신규 Swift 파일을 SodaLive.xcodeproj/project.pbxproj의 Chat 그룹 및 앱 소스 빌드 단계에 등록했다.

  • 2026-07-08 검증 기록: xcodebuild -workspace "SodaLive.xcworkspace" -list 성공. 최초 xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build는 신규 파일 참조가 루트 경로로 해석되어 실패했고, pbxproj 파일 참조 ID를 정정한 뒤 동일 명령 재실행 결과 ** BUILD SUCCEEDED **를 확인했다. 빌드 로그에는 기존 프로젝트의 중복 Compile Sources 경고가 다수 표시되며, 신규 Chat 파일도 기존 등록 패턴과 동일하게 중복 경고에 포함된다. 추가로 xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build** BUILD SUCCEEDED **를 확인했다.