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-listitem(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+Circleclip — 원형 프로필I18n.Time(justNow/minutesAgo/hoursAgo/daysAgo) — 7일 이내 상대 시간ApiResponse<T>디코딩, MoyaTargetType+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.title과BottomTabView에 동일 반영.
- 실행 명령:
- 대상 파일:
-
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.
- 7일 이내:
- 파싱 실패 시 fallback 원문 반환.
- 신규 static 함수(예:
- 검증 기준:
- 실행 명령:
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터치 시onTapSearchclosure를 호출한다.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의.chatcase를MainPlaceholderTabView(title: MainTab.chat.title)→MainChatView(...)로 교체한다.- 기존
handleHomeCanChargeTap,handleHomeSearchTap을MainChatView의 우측 메뉴 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:3466design 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 **를 확인했다.