31 KiB
계획/TASK: 유저-크리에이터 1:1 채팅
관련 PRD: docs/20260711_유저_크리에이터_1대1_채팅/prd.md
작업 원칙
- REST는 Moya
TargetType로만 구현한다. - 실시간 텍스트 채팅은
URLSessionWebSocketTaskraw WebSocket으로 구현한다. - STOMP/SockJS 클라이언트 및 STOMP frame은 사용하지 않는다.
roomId가 nil 또는 0이면 WebSocket outgoing 명령을 보내지 않는다.- UI 연결 완료 기준은 WebSocket open이 아니라 서버
JOINED수신이다.
재사용 vs 신규
- 재사용:
ApiResponse<T>:SodaLive/Sources/Common/ApiResponse.swift- Moya
TargetType+ Repository + Combine 패턴:SodaLive/Sources/V2/Main/Chat/Repository/MainChatApi.swift,MainChatRepository.swift - AI 채팅방 UI 구조:
SodaLive/Sources/Chat/Talk/Room/ChatRoomView.swift - Loading/Toast:
BaseView(isLoading:),.sodaToast - 로그:
DEBUG_LOG,ERROR_LOG - 앱 라우팅:
AppStep,AppState,ContentView - 기존 음성 multipart 패턴:
SodaLive/Sources/Message/MessageApi.swift,SodaLive/Sources/Message/Voice/VoiceMessageViewModel.swift
- 신규:
- 유저-크리에이터 채팅 REST API/Repository/DTO
WebSocketChatClientraw WebSocket 객체- DM 채팅방 View/ViewModel/전용 메시지 컴포넌트
- DM deep link action 및 route
- 세션 종료 알림 또는 훅
Phase 1: REST DTO/API/Repository
-
Task 1.1: REST/Message DTO 정의
- 대상 파일:
- 생성:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatModels.swift
- 생성:
- 작업 내용:
UserCreatorCreateRoomRequest(creatorId: Int)정의.UserCreatorCreateRoomResponse(roomId: Int)정의.UserCreatorOpenRoomResponse(roomId, opponentNickname, opponentProfileImageUrl, messages, hasMore, nextCursor)정의.UserCreatorChatMessagesResponse(messages, hasMore, nextCursor)정의.UserCreatorChatMessageItem: Codable정의.UserCreatorVoiceMessageResponse(message, deliveredRealtime, pushSent)정의.
- 검증 기준:
- 실행 명령:
rg "UserCreatorChatMessageItem|UserCreatorOpenRoomResponse|UserCreatorVoiceMessageResponse" SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models - 기대 결과: 서버 계약 필드가 Swift DTO에 빠짐없이 선언되어 있다.
- 실행 명령:
- 대상 파일:
-
Task 1.2: Moya TargetType 추가
- 대상 파일:
- 생성:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Repository/UserCreatorChatApi.swift
- 생성:
- 작업 내용:
- case 추가:
createRoom(request:)openRoom(roomId:limit:)getMessages(roomId:cursor:limit:)sendVoiceMessage(roomId:parameters:)
- path:
/api/v2/user-creator-chat/rooms/create/api/v2/user-creator-chat/rooms/{roomId}/open/api/v2/user-creator-chat/rooms/{roomId}/messages/api/v2/user-creator-chat/rooms/{roomId}/messages/voice
Authorization: Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))헤더 추가.- 음성 메시지는
.uploadMultipart사용. - 제거된 event/text REST API는 추가하지 않는다.
- case 추가:
- 검증 기준:
- 실행 명령:
rg "/api/v2/user-creator-chat|uploadMultipart|Authorization" SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Repository/UserCreatorChatApi.swift - 기대 결과: 4개 REST API만 존재하고 제거 API path는 없다.
- 실행 명령:
- 대상 파일:
-
Task 1.3: Repository 추가
- 대상 파일:
- 생성:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Repository/UserCreatorChatRepository.swift
- 생성:
- 작업 내용:
createRoom(creatorId:),openRoom(roomId:limit:),getMessages(roomId:cursor:limit:),sendVoiceMessage(roomId:voiceData:)추가.- 음성 multipart의
requestpart는 JSON 문자열{"recipientId": null}형태로 전송한다.
- 검증 기준:
- 실행 명령:
rg "createRoom|openRoom|getMessages|sendVoiceMessage|voiceMessageFile|recipientId" SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Repository - 기대 결과: Repository 메서드와 multipart part 이름이 서버 계약과 일치한다.
- 실행 명령:
- 대상 파일:
Phase 2: Raw WebSocket Client
-
Task 2.1: WebSocket envelope/상태 모델 정의
- 대상 파일:
- 생성:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/WebSocket/UserCreatorChatWebSocketModels.swift
- 생성:
- 작업 내용:
UserCreatorChatSocketState:disconnected,connecting,socketOpen,joining,joined.- outgoing/incoming envelope 정의:
type,requestId,roomId,payload. ERRORpayload는messageKey를 파싱한다.
- 검증 기준:
- 실행 명령:
rg "socketOpen|joining|joined|messageKey|SEND_TEXT|JOIN_ROOM" SodaLive/Sources/V2/Main/Chat/UserCreatorChat/WebSocket - 기대 결과: 상태와 protocol type/payload 모델이 확인된다.
- 실행 명령:
- 대상 파일:
-
Task 2.2: WebSocketChatClient 구현
- 대상 파일:
- 생성:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/WebSocket/WebSocketChatClient.swift
- 생성:
- 작업 내용:
URLSessionWebSocketTask사용.BASE_URL의 scheme/host를ws/wss로 변환해/ws/v2/user-creator-chatURL 생성.URLRequest에Authorization헤더 추가.connect(roomId:)는 socket open 후 실제roomId > 0일 때만JOIN_ROOM전송.- outgoing JSON과 incoming raw text를
DEBUG_LOG로 출력. - receive loop에서
JOINED,SEND_ACK,MESSAGE,PONG,ERRORevent를 delegate/closure로 전달. sendText,ping,leaveRoom은joined상태와roomId > 0을 모두 만족할 때만 전송.close()는 가능한 경우LEAVE_ROOM전송 후 cancel/close 처리.
- 검증 기준:
- 실행 명령:
rg "URLSessionWebSocketTask|URLRequest|Authorization|/ws/v2/user-creator-chat|DEBUG_LOG|JOIN_ROOM|LEAVE_ROOM" SodaLive/Sources/V2/Main/Chat/UserCreatorChat/WebSocket/WebSocketChatClient.swift - 기대 결과: raw WebSocket 구현과 guard/logging이 확인된다.
- 실행 명령:
rg "Stomp|SockJS|CONNECT|SUBSCRIBE" SodaLive/Sources/V2/Main/Chat/UserCreatorChat - 기대 결과: 검색 결과가 없어야 한다.
- 실행 명령:
- 대상 파일:
-
Task 2.3: heartbeat 및 ACK timeout 기반
- 대상 파일:
- 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/WebSocket/WebSocketChatClient.swift
- 수정:
- 작업 내용:
joined후 30초 주기PINGtimer 시작.PONG수신 로그/상태 갱신.- socket close/error를 상위 ViewModel에 전달해 재연결 판단은 ViewModel이 수행하게 한다.
- 검증 기준:
- 실행 명령:
rg "30|PING|PONG|Timer|joined" SodaLive/Sources/V2/Main/Chat/UserCreatorChat/WebSocket/WebSocketChatClient.swift - 기대 결과: heartbeat가
joined이후에만 동작한다.
- 실행 명령:
- 대상 파일:
Phase 3: ViewModel 상태/메시지 처리
-
Task 3.1: DM 채팅방 ViewModel 생성
- 대상 파일:
- 생성:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift - 생성:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatDisplayMessage.swift
- 생성:
- 작업 내용:
- 상태:
isLoading,isShowPopup,errorMessage,roomId,opponentNickname,opponentProfileImageUrl,messages,hasMore,nextCursor,socketState. enter(roomId:):openRoom성공 후 실제 roomId로 WebSocket connect/join.enter(creatorId:):createRoom성공 후openRoom+ connect/join.isLoading은 REST 호출 및 최초JOINED대기 동안 true 유지.
- 상태:
- 검증 기준:
- 실행 명령:
rg "enter\\(roomId|enter\\(creatorId|openRoom|socketState|opponentNickname|isLoading" SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift - 기대 결과: roomId/creatorId 진입 흐름과 Loading 상태가 확인된다.
- 실행 명령:
- 대상 파일:
-
Task 3.2: JOIN/ERROR 재시도 정책 구현
- 대상 파일:
- 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift
- 수정:
- 작업 내용:
JOINED전ERROR수신 시 최대 3회 재시도.- 3회 실패 시 Loading 해제, 토스트 메시지
대화방에 접속하지 못했습니다., socket close,AppState.shared.back()호출. payload.messageKey를 로그/분기 기준으로 사용한다.
- 검증 기준:
- 실행 명령:
rg "joinRetry|messageKey|대화방에 접속하지 못했습니다|AppState.shared.back" SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift - 기대 결과: JOIN 전 ERROR 3회 재시도와 실패 후 뒤로가기 처리가 확인된다.
- 실행 명령:
- 대상 파일:
-
Task 3.3: 메시지 pending/ACK/중복 제거 구현
- 대상 파일:
- 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift - 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatDisplayMessage.swift
- 수정:
- 작업 내용:
sendText()는joined상태에서만 WebSocketSEND_TEXT전송.- requestId로 pending 메시지를 append.
- 15초 timeout 후 failed 상태 표시.
SEND_ACK의 requestId로 pending 메시지를 서버 메시지로 교체.MESSAGE와 REST 응답은messageId기준 merge/dedupe.
- 검증 기준:
- 실행 명령:
rg "sendText|pending|SEND_ACK|requestId|15|messageId" SodaLive/Sources/V2/Main/Chat/UserCreatorChat - 기대 결과: pending/ACK/timeout/dedupe 흐름이 확인된다.
- 실행 명령:
- 대상 파일:
-
Task 3.4: 과거 메시지/재동기화 구현
- 대상 파일:
- 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift
- 수정:
- 작업 내용:
- 스크롤 상단 도달 시
getMessages(roomId:cursor:limit:)호출. hasMore/nextCursor관리.- 재연결 후 필요 시
openRoom또는getMessages로 최신 메시지 merge.
- 스크롤 상단 도달 시
- 검증 기준:
- 실행 명령:
rg "loadMore|nextCursor|hasMore|merge|openRoom" SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift - 기대 결과: pagination과 재동기화 로직이 확인된다.
- 실행 명령:
- 대상 파일:
-
Task 3.5: lifecycle close/reconnect 구현
- 대상 파일:
- 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift - 수정:
SodaLive/Sources/Settings/SettingsView.swift - 수정:
SodaLive/Sources/Settings/SignOut/SignOutViewModel.swift
- 수정:
- 작업 내용:
- 화면 이탈 시
leaveAndClose(). UIApplication.didEnterBackgroundNotification수신 시LEAVE_ROOM후 close.- 로그아웃/전체 로그아웃/회원탈퇴 성공 직전에 세션 종료 notification을 post하고, ViewModel이 받아 close.
- socket 오류 발생 시 화면이 살아 있는 동안만 재연결한다.
- 화면 이탈 시
- 검증 기준:
- 실행 명령:
rg "didEnterBackgroundNotification|leaveAndClose|userSession|reconnect" SodaLive/Sources/V2/Main/Chat/UserCreatorChat SodaLive/Sources/Settings - 기대 결과: 백그라운드/로그아웃/재연결 처리가 확인된다.
- 실행 명령:
- 대상 파일:
-
Task 3.6: 음성 메시지 통신 계층 연결
- 대상 파일:
- 확인/수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Repository/UserCreatorChatRepository.swift - 확인/수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift
- 확인/수정:
- 작업 내용:
sendVoiceMessage(soundData:)REST 호출 경로를 통신 계층에 준비한다.- 이번 범위에서는 음성 메시지 송신 UI와 수신 음성 메시지 표시 UI를 연결하지 않는다.
- REST 응답의
message,deliveredRealtime,pushSent는 디코딩 가능해야 하며 debug log로 확인할 수 있게 둔다. - 음성 메시지는 채팅방 메시지 목록 UI에 append/merge하지 않는다.
- 검증 기준:
- 실행 명령:
rg "sendVoiceMessage|deliveredRealtime|pushSent|voiceMessageFile" SodaLive/Sources/V2/Main/Chat/UserCreatorChat - 기대 결과: 음성 메시지 REST 통신 경로가 확인되고, 음성 UI/재생 컴포넌트 연결은 없어야 한다.
- 실행 명령:
- 대상 파일:
Phase 4: UI 컴포넌트/화면
-
Task 4.1: DM 메시지 컴포넌트 생성
- 대상 파일:
- 생성:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Components/UserCreatorChatTextMessageItemView.swift
- 생성:
- 작업 내용:
- mine 여부에 따라 좌/우 정렬.
TEXT는textMessage표시.VOICE수신 메시지 표시와 재생 UI는 이번 범위에서 구현하지 않는다.- pending/failed 상태를 표시한다.
- 검증 기준:
- 실행 명령:
rg "UserCreatorChatTextMessageItemView|pending|failed|textMessage" SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Components - 기대 결과: TEXT 렌더링과 pending/failed 상태 표시가 확인된다.
- 실행 명령:
rg "UserCreatorChatVoiceMessageItemView|voiceMessageUrl|AVAudioPlayer" SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Components - 기대 결과: 검색 결과가 없어야 한다.
- 실행 명령:
- 대상 파일:
-
Task 4.2: DM 채팅방 화면 생성
- 대상 파일:
- 생성:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomView.swift
- 생성:
- 작업 내용:
BaseView(isLoading:)로 전체 화면 Loading 표시.- AI
ChatRoomView구조를 기준으로 헤더/메시지 목록/입력창 구성. - 헤더에서
Character / Clone, 보유 캔 UI, 우측 더보기 버튼 제거. - 입력창은
socketState == .joined일 때만 활성화. onAppear에서enter(roomId:)또는enter(creatorId:)호출.onDisappear에서leaveAndClose().
- 검증 기준:
- 실행 명령:
rg "BaseView|opponentNickname|opponentProfileImageUrl|socketState|onDisappear|sendText" SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomView.swift - 기대 결과: Loading/헤더/입력/종료 처리가 확인된다.
- 실행 명령:
- 대상 파일:
-
Task 4.3: I18n 문구 추가
- 대상 파일:
- 수정:
SodaLive/Sources/I18n/I18n.swift
- 수정:
- 작업 내용:
- DM 입력 placeholder.
- 연결 실패 토스트:
대화방에 접속하지 못했습니다. - 메시지 전송 실패 문구.
- 검증 기준:
- 실행 명령:
rg "UserCreatorChat|대화방에 접속하지 못했습니다" SodaLive/Sources/I18n/I18n.swift - 기대 결과: 신규 문구가 다국어
pick(ko:en:ja:)패턴으로 추가되어 있다.
- 실행 명령:
- 대상 파일:
Phase 5: Routing/Deep Link/진입점
-
Task 5.1: AppStep/ContentView 라우팅 추가
- 대상 파일:
- 수정:
SodaLive/Sources/App/AppStep.swift - 수정:
SodaLive/Sources/ContentView.swift
- 수정:
- 작업 내용:
case userCreatorChatRoom(roomId: Int)추가.case userCreatorChatCreator(creatorId: Int)추가.- 각 case를
UserCreatorChatRoomView(roomId:),UserCreatorChatRoomView(creatorId:)에 연결.
- 검증 기준:
- 실행 명령:
rg "userCreatorChatRoom|userCreatorChatCreator|UserCreatorChatRoomView" SodaLive/Sources/App/AppStep.swift SodaLive/Sources/ContentView.swift - 기대 결과: roomId/creatorId 진입 route가 모두 확인된다.
- 실행 명령:
- 대상 파일:
-
Task 5.2: 홈 채팅 탭 DM 아이템 라우팅 연결
- 대상 파일:
- 수정:
SodaLive/Sources/V2/Main/Chat/MainChatView.swift
- 수정:
- 작업 내용:
chatType == "DM"터치 시AppState.shared.setAppStep(step: .userCreatorChatRoom(roomId: roomId)).chatType == "AI"기존AppStep.chatRoom(id:)라우팅 유지.
- 검증 기준:
- 실행 명령:
rg "chatType == \"DM\"|userCreatorChatRoom|chatRoom\\(id:" SodaLive/Sources/V2/Main/Chat/MainChatView.swift - 기대 결과: AI/DM 라우팅 분기가 확인된다.
- 실행 명령:
- 대상 파일:
-
Task 5.3: 크리에이터 채널 DM 시작 진입점 연결
- 대상 파일:
- 수정:
SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift - 수정:
SodaLive/Sources/V2/CreatorChannel/CreatorChannelViewModel.swift - 수정:
SodaLive/Sources/V2/CreatorChannel/Home/Repository/CreatorChannelHomeRepository.swift
- 수정:
- 작업 내용:
- 기존
handleTalkTap은 계속 AI 캐릭터 채팅 진입으로 유지한다. - 지난 범위에서 구현하지 않은
DM 보내기버튼을 추가한다. DM 보내기버튼 터치 시 로그인 여부만 확인한 뒤creatorId기반 route 또는rooms/create흐름으로 신규 DM 채팅방에 진입한다.- AI 캐릭터 채팅 진입에 적용되는 본인인증/성인 콘텐츠 설정 guard는 DM 정책과 맞지 않으므로 적용하지 않는다.
- 기존 AI 채팅방 생성 API(
TalkApi.createChatRoom)와 신규 DM 생성 API(UserCreatorChatApi.createRoom)가 섞이지 않게 분리한다.
- 기존
- 검증 기준:
- 실행 명령:
rg "handleTalkTap|DM|userCreatorChatCreator|createRoom\\(creatorId" SodaLive/Sources/V2/CreatorChannel - 기대 결과:
handleTalkTap은 AI 캐릭터 채팅 경로를 유지하고, 별도DM 보내기경로에서creatorId > 0guard 후 DM 생성/조회 흐름으로 이동한다.
- 실행 명령:
- 대상 파일:
-
Task 5.4: 푸시 deep link 파싱 추가
- 대상 파일:
- 수정:
SodaLive/Sources/App/AppDeepLinkHandler.swift
- 수정:
- 작업 내용:
AppDeepLinkAction.userCreatorChat(roomId: Int)추가.voiceon://chat/{roomId},voiceon-test://chat/{roomId}파싱.- scheme은 기존
voiceon/voiceon-test허용을 재사용하고, hostchat, path 첫 segment를roomId로 읽는다. - apply 시
AppStep.userCreatorChatRoom(roomId:)로 이동한다.
- 검증 기준:
- 실행 명령:
rg "userCreatorChat|case \"chat\"|voiceon-test" SodaLive/Sources/App/AppDeepLinkHandler.swift - 기대 결과: chat deep link가 live/content/message와 별도 action으로 파싱된다.
- 실행 명령:
- 대상 파일:
Phase 6: 프로젝트 등록/검증
-
Task 6.1: Xcode 프로젝트 신규 파일 등록
- 대상 파일:
- 수정:
SodaLive.xcodeproj/project.pbxproj
- 수정:
- 작업 내용:
- Phase 1~5에서 추가한
.swift파일을 앱 타깃 소스에 등록한다.
- Phase 1~5에서 추가한
- 검증 기준:
- 실행 명령:
rg "UserCreatorChatRoomView|WebSocketChatClient|UserCreatorChatApi" SodaLive.xcodeproj/project.pbxproj - 기대 결과: 신규 Swift 파일들이 프로젝트에 등록되어 있다.
- 실행 명령:
- 대상 파일:
-
Task 6.2: 정적 검색 검증
- 대상 파일:
- 확인:
SodaLive/Sources/**
- 확인:
- 작업 내용:
- 제거 API 미사용 확인.
- STOMP/SockJS 미사용 확인.
- WebSocket outgoing guard 확인.
- 검증 기준:
- 실행 명령:
rg "/events|events/disconnect|messages/text|Stomp|SockJS|SUBSCRIBE|CONNECT" SodaLive/Sources - 기대 결과: 신규 유저-크리에이터 채팅 코드에서 제거 API/STOMP 사용이 없어야 한다.
- 실행 명령:
rg "roomId > 0|guard .*roomId" SodaLive/Sources/V2/Main/Chat/UserCreatorChat - 기대 결과: outgoing 명령 전 roomId guard가 확인된다.
- 실행 명령:
- 대상 파일:
-
Task 6.3: 빌드 검증
- 대상 파일:
- 확인: 전체 프로젝트
- 작업 내용:
docs/agent-guides/build-test-verification.md기준으로 빌드한다.
- 검증 기준:
- 실행 명령:
xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build - 기대 결과:
** BUILD SUCCEEDED ** - 실행 명령:
xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build - 기대 결과:
** BUILD SUCCEEDED **
- 실행 명령:
- 대상 파일:
-
Task 6.4: 수동 QA
- 대상 파일:
- 확인: 앱 런타임
- 확인 항목:
- 방 생성/조회 후
roomId > 0으로만JOIN_ROOM이 전송된다. JOINED전에는 입력이 비활성화되고 Loading이 표시된다.SEND_TEXToutgoing JSON과 incoming raw text가 debug log에 남는다.SEND_ACK수신 시 pending 메시지가 확정된다.SEND_ACKtimeout 시 실패 상태가 보인다.MESSAGE중복 수신 시 한 번만 표시된다.- 백그라운드/뒤로가기/로그아웃 시
LEAVE_ROOM후 socket이 닫힌다. JOINED전ERROR3회 실패 시 토스트 후 이전 페이지로 이동한다.voiceon://chat/{roomId}deep link로 동일한 open/join 흐름이 실행된다.VOICE메시지 송신/수신 UI와 재생 UI가 노출되지 않는다.
- 방 생성/조회 후
- 대상 파일:
Phase 7: 코드 리뷰 후속 수정
-
Task 7.1: 재연결 동기화 및 VOICE 전용 페이지 보완
- 대상 파일:
- 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift
- 수정:
- 작업 내용:
- 재연결 후 최신
hasMore와nextCursor를 반영해 20개 초과 누락 구간을 과거 메시지 조회로 복구할 수 있게 한다. - VOICE 메시지만 반환된 페이지는 cursor가 전진하는 동안 다음 페이지를 자동 조회한다.
- 재연결 후 최신
- 검증 기준:
- 재연결 동기화 결과의 pagination 상태가 갱신된다.
- VOICE 전용 페이지 뒤의 TEXT 메시지까지 조회할 수 있다.
- 대상 파일:
-
Task 7.2: WebSocket 종료 및 재연결 단일 실행 보장
- 대상 파일:
- 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/WebSocket/WebSocketChatClient.swift - 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift
- 수정:
- 작업 내용:
- 동일 socket task의
receive실패와 delegate close가 중복 종료 이벤트를 만들지 않게 한다. - 예약된 재연결 작업은 항상 하나만 유지하고 화면 이탈 또는 연결 성공 시 취소한다.
- 동일 socket task의
- 검증 기준:
- socket 종료 처리 후 현재 task가 해제된다.
- 중복
.closed이벤트가 들어와도 재연결 예약은 하나만 남는다.
- 대상 파일:
-
Task 7.3: 후속 수정 검증
- 대상 파일:
- 확인: 전체 프로젝트
- 검증 기준:
git diff --check가 통과한다.SodaLive,SodaLive-devDebug 빌드가 성공한다.
- 대상 파일:
Phase 8: 코드 리뷰 결과 반영
-
Task 8.1: 백그라운드 재연결과 openRoom 성공 gate 보완
- 대상 파일:
- 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift
- 수정:
- 작업 내용:
- 백그라운드 진입 시 예약된 재연결을 취소하고 앱 활성 상태를 별도로 관리한다.
openRoom성공 전에는 foreground 전환이나 재연결 경로에서 WebSocket을 연결하지 않는다.- 백그라운드에서
openRoom응답이 완료되면 방 정보만 반영하고 socket 연결은 foreground까지 미룬다.
- 검증 기준:
openRoom성공 전이거나 앱이 백그라운드인 동안socketClient.connect경로가 차단된다.- foreground 복귀 시 성공적으로 열린 방에 한해 재연결한다.
- 대상 파일:
-
Task 8.2: ACK 유실 시 pending 메시지 재동기화
- 대상 파일:
- 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift
- 수정:
- 작업 내용:
- REST/실시간 서버 메시지 merge 시 동일한 내 텍스트 pending/failed 메시지를 1:1로 재동기화한다.
- ACK만 유실된 경우 서버 메시지와 failed pending이 중복 노출되지 않게 한다.
- 검증 기준:
- 서버
messageIddedupe와 로컬 pending 재동기화가 함께 적용된다. - 동일 텍스트를 연속 전송해도 서버 메시지 하나가 pending 하나만 확정한다.
- 서버
- 대상 파일:
-
Task 8.3: WebSocket mutable state 직렬화
- 대상 파일:
- 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/WebSocket/WebSocketChatClient.swift
- 수정:
- 작업 내용:
- public 명령과
URLSessionWebSocketDelegate/receive/send callback의task,session,state,isClosing접근을 main thread로 직렬화한다.
- public 명령과
- 검증 기준:
- socket open/close/receive가 겹쳐도 현재 task 판별과 종료 이벤트가 단일 실행된다.
- 대상 파일:
-
Task 8.4: 요청 범위 외 변경 원복
- 대상 파일:
- 수정:
SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift - 수정:
SodaLive/Sources/V2/CreatorChannel/CreatorChannelViewModel.swift
- 수정:
- 작업 내용:
- DM 채팅 구현과 관계없는 기존
print(error)삭제 2건을 원복한다.
- DM 채팅 구현과 관계없는 기존
- 검증 기준:
- 두 파일이 DM 채팅 변경 diff에서 제외된다.
- 대상 파일:
-
Task 8.5: 리뷰 반영 검증
- 대상 파일:
- 확인: 전체 프로젝트
- 검증 기준:
git diff --check HEAD가 통과한다.plutil -lint SodaLive.xcodeproj/project.pbxproj가 통과한다.SodaLive,SodaLive-devDebug 빌드가 성공한다.
- 대상 파일:
-
Task 8.6: 최신 메시지 REST 재동기화 Loading/오류 처리
- 대상 파일:
- 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift
- 수정:
- 작업 내용:
syncLatestMessagesREST 요청 동안 Loading을 유지한다.- 요청 실패, API 실패 응답, 디코딩 오류를 기존 토스트 경로로 전달한다.
- 검증 기준:
JOINED직후 Loading은 재동기화 완료 후 해제된다.- 재동기화 실패 시 Loading이 해제되고 오류 토스트가 표시된다.
- 대상 파일:
Phase 9: 추가 코드 리뷰 결과 반영
-
Task 9.1: JOIN 전송 실패 및 JOINED timeout 처리
- 대상 파일:
- 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/WebSocket/UserCreatorChatWebSocketModels.swift - 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/WebSocket/WebSocketChatClient.swift - 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift
- 수정:
- 작업 내용:
JOIN_ROOM전송 오류를 ViewModel의 JOIN 재시도 정책으로 전달한다.JOIN_ROOM전송 후 15초 안에JOINED가 없으면 JOIN 실패 event를 전달한다.- 연결 성공, socket 종료, 재연결 시작 시 예약된 JOIN timeout을 취소한다.
- 검증 기준:
- JOIN 전송 오류 또는 timeout이 발생하면
.joining에 고정되지 않고 최대 3회 재시도한다. - 화면 이탈 또는 연결 성공 후 이전 JOIN timeout이 실행되지 않는다.
- JOIN 전송 오류 또는 timeout이 발생하면
- 대상 파일:
-
Task 9.2: ACK 유실 재동기화 clock-skew 허용
- 대상 파일:
- 수정:
SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift
- 수정:
- 작업 내용:
- 동일한 내 텍스트 서버 메시지와 pending/failed 메시지를 매칭할 때 서버 시각이 로컬 생성 시각보다 최대 5분 이전인 경우까지 허용한다.
- 기존 1:1 매칭과 ACK timeout 취소 동작을 유지한다.
- 검증 기준:
- 서버
createdAt이 로컬 pending 생성 시각보다 5분 이내로 이전이어도 중복 pending/failed 메시지가 제거된다. - 5분을 초과해 오래된 동일 텍스트 메시지는 pending과 매칭하지 않는다.
- 서버
- 대상 파일:
-
Task 9.3: 추가 리뷰 반영 검증
- 대상 파일:
- 확인: 전체 프로젝트
- 검증 기준:
git diff --check HEAD가 통과한다.plutil -lint SodaLive.xcodeproj/project.pbxproj가 통과한다.SodaLive,SodaLive-devDebug 빌드가 성공한다.
- 대상 파일:
검증 기록
- 2026-07-11 문서 작성:
docs/agent-guides/documentation-policy.md, 기존 홈 채팅 탭 PRD/계획 문서, 기존 AIChatRoomView/ChatRoomViewModel, V2MainChatView,AppDeepLinkHandler,BaseView, 기존 multipart 음성 메시지 전송 패턴을 확인하고 PRD 및 구현 계획에 반영했다. 이 기록은 구현 전 문서 작성 시점의 상태다. - 2026-07-11 문서 보완: 사용자 확정 사항을 반영해 기존
handleTalkTap은 AI 캐릭터 채팅 진입으로 유지하고, 별도DM 보내기버튼에서creatorId기반 DM 생성/조회 후 DM 채팅방으로 이동하도록 PRD/계획을 수정했다. 또한VOICE는 DTO/API/Repository 통신 작업만 범위에 남기고, 음성 메시지 송신 UI·수신 표시 UI·재생 UI 작업을 제외하도록 Phase 3/4/5와 QA 항목을 갱신했다. - 2026-07-11 코드 리뷰/검증: 변경사항 기준으로 제거된 event/text REST API 및 STOMP/SockJS 미사용, WebSocket outgoing
roomId > 0guard, 신규 파일 프로젝트 등록을 정적 검색으로 확인했다.SodaLive,SodaLive-devDebug simulator 빌드가 성공했다. - 2026-07-12 코드 리뷰 후속 수정: 재연결 후
openRoom의 최신 pagination 상태를 반영하고, VOICE 전용 페이지에서 cursor가 전진하는 동안 다음 페이지를 자동 조회하도록 수정했다. 동일 WebSocket task의 종료 이벤트와 ViewModel의 재연결 예약을 각각 한 번만 처리하도록 보완했다.git diff --check,plutil -lint SodaLive.xcodeproj/project.pbxproj, 제거 API/STOMP 정적 검색을 통과했고,xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug -derivedDataPath /tmp/SodaLiveReviewDerivedData CODE_SIGNING_ALLOWED=NO build및SodaLive-dev동일 명령이** BUILD SUCCEEDED **로 완료됐다. 실제 서버 연동 수동 QA는 Task 6.4에 남겨 두었다. - 2026-07-12 코드 리뷰 결과 반영: 백그라운드 상태와
openRoom성공 여부를 별도 gate로 관리해 지연된 REST 응답과 예약된 재연결이 백그라운드에서 socket을 다시 열지 않게 했다. 새로 관측된 내 서버 텍스트 메시지는 로컬 pending/failed 메시지와 1:1로 재동기화하고, WebSocket의 public 명령과 delegate/receive/send callback에서 발생하는 mutable state 접근을 main thread로 직렬화했다. DM 채팅과 관계없는 기존print(error)삭제 2건은 원복했다.git diff --check HEAD,plutil -lint SodaLive.xcodeproj/project.pbxproj, 제거 API/STOMP 정적 검색을 통과했고,xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug -derivedDataPath /tmp/SodaLiveCodeReviewDerivedData CODE_SIGNING_ALLOWED=NO build및SodaLive-dev동일 명령이** BUILD SUCCEEDED **로 완료됐다. 실제 서버 연동 수동 QA는 Task 6.4에 남겨 두었다. - 2026-07-12 최신 메시지 REST 재동기화 보완:
JOINED후syncLatestMessages완료까지 Loading을 유지하고, 요청 실패·API 실패 응답·디코딩 오류를applyRestFailure토스트 경로로 전달하도록 수정했다.git diff --check HEAD가 통과했고,xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug CODE_SIGNING_ALLOWED=NO build및SodaLive-dev동일 명령이** BUILD SUCCEEDED **로 완료됐다. - 2026-07-12 추가 코드 리뷰 결과 반영:
JOIN_ROOM전송 오류를 즉시 JOIN 실패 event로 전달하고, 15초 동안JOINED가 없으면 현재 socket을 닫은 뒤 기존 최대 3회 재시도 정책을 적용하도록 보완했다. ACK 유실 재동기화 시 서버createdAt이 로컬 pending 생성 시각보다 최대 5분 이전인 경우까지 동일 메시지 후보로 허용했다.git diff --check HEAD,plutil -lint SodaLive.xcodeproj/project.pbxproj가 통과했고,xcodebuild -quiet -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug -derivedDataPath /tmp/SodaLiveReviewCurrent CODE_SIGNING_ALLOWED=NO build및SodaLive-dev동일 명령이 종료 코드 0으로 완료됐다. 실제 서버 연동 수동 QA는 Task 6.4에 남겨 두었다. - 2026-07-12 크리에이터 채널 후속 UX 수정: 크리에이터 채널 각 탭 empty state 문구의 상단 여백을
20으로 조정해 중앙 배치가 아닌 상단 배치로 변경했다. 본인 채널의 DM 버튼은DM 확인하기로 표시하고, tap 시AppState.pendingMainChatFilter(.dm)를 통해 메인 대화 탭의 DM 필터로 이동하도록 반영했다. 타인 채널의DM 보내기및 creatorId 기반 DM 채팅방 진입은 유지했다.