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

172 lines
12 KiB
Markdown

# 계획/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: 다국어/문구
- [x] **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`에 동일 반영.
- [x] **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 계층
- [x] **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 필드가 확인된다.
- [x] **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 매핑이 확인된다.
- [x] **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)
- [x] **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: 시간 포맷 유틸
- [x] **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: 컴포넌트
- [x] **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 및 탭 연결
- [x] **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`은 존재하지 않는다.
- [x] **Task 6.2: MainView 채팅 탭 교체**
- 대상 파일:
- 수정: `SodaLive/Sources/V2/Main/MainView.swift`
- 작업 내용:
- `contentView``.chat` case를 `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: 프로젝트 등록 및 검증
- [x] **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 **`를 확인했다.