Files
sodalive-ios/docs/20260711_유저_크리에이터_1대1_채팅/plan-task.md

31 KiB

계획/TASK: 유저-크리에이터 1:1 채팅

관련 PRD: docs/20260711_유저_크리에이터_1대1_채팅/prd.md

작업 원칙

  • REST는 Moya TargetType로만 구현한다.
  • 실시간 텍스트 채팅은 URLSessionWebSocketTask raw 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
    • WebSocketChatClient raw 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는 추가하지 않는다.
    • 검증 기준:
      • 실행 명령: 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의 request part는 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.
      • ERROR payload는 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-chat URL 생성.
      • URLRequestAuthorization 헤더 추가.
      • connect(roomId:)는 socket open 후 실제 roomId > 0일 때만 JOIN_ROOM 전송.
      • outgoing JSON과 incoming raw text를 DEBUG_LOG로 출력.
      • receive loop에서 JOINED, SEND_ACK, MESSAGE, PONG, ERROR event를 delegate/closure로 전달.
      • sendText, ping, leaveRoomjoined 상태와 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초 주기 PING timer 시작.
      • 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
    • 작업 내용:
      • JOINEDERROR 수신 시 최대 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 상태에서만 WebSocket SEND_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 여부에 따라 좌/우 정렬.
      • TEXTtextMessage 표시.
      • 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 > 0 guard 후 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 허용을 재사용하고, host chat, 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 파일을 앱 타깃 소스에 등록한다.
    • 검증 기준:
      • 실행 명령: 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_TEXT outgoing JSON과 incoming raw text가 debug log에 남는다.
      • SEND_ACK 수신 시 pending 메시지가 확정된다.
      • SEND_ACK timeout 시 실패 상태가 보인다.
      • MESSAGE 중복 수신 시 한 번만 표시된다.
      • 백그라운드/뒤로가기/로그아웃 시 LEAVE_ROOM 후 socket이 닫힌다.
      • JOINEDERROR 3회 실패 시 토스트 후 이전 페이지로 이동한다.
      • voiceon://chat/{roomId} deep link로 동일한 open/join 흐름이 실행된다.
      • VOICE 메시지 송신/수신 UI와 재생 UI가 노출되지 않는다.

Phase 7: 코드 리뷰 후속 수정

  • Task 7.1: 재연결 동기화 및 VOICE 전용 페이지 보완

    • 대상 파일:
      • 수정: SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift
    • 작업 내용:
      • 재연결 후 최신 hasMorenextCursor를 반영해 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가 해제된다.
      • 중복 .closed 이벤트가 들어와도 재연결 예약은 하나만 남는다.
  • Task 7.3: 후속 수정 검증

    • 대상 파일:
      • 확인: 전체 프로젝트
    • 검증 기준:
      • git diff --check가 통과한다.
      • SodaLive, SodaLive-dev Debug 빌드가 성공한다.

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이 중복 노출되지 않게 한다.
    • 검증 기준:
      • 서버 messageId dedupe와 로컬 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로 직렬화한다.
    • 검증 기준:
      • 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 채팅 변경 diff에서 제외된다.
  • Task 8.5: 리뷰 반영 검증

    • 대상 파일:
      • 확인: 전체 프로젝트
    • 검증 기준:
      • git diff --check HEAD가 통과한다.
      • plutil -lint SodaLive.xcodeproj/project.pbxproj가 통과한다.
      • SodaLive, SodaLive-dev Debug 빌드가 성공한다.
  • Task 8.6: 최신 메시지 REST 재동기화 Loading/오류 처리

    • 대상 파일:
      • 수정: SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift
    • 작업 내용:
      • syncLatestMessages REST 요청 동안 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이 실행되지 않는다.
  • 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-dev Debug 빌드가 성공한다.

검증 기록

  • 2026-07-11 문서 작성: docs/agent-guides/documentation-policy.md, 기존 홈 채팅 탭 PRD/계획 문서, 기존 AI ChatRoomView/ChatRoomViewModel, V2 MainChatView, 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 > 0 guard, 신규 파일 프로젝트 등록을 정적 검색으로 확인했다. SodaLive, SodaLive-dev Debug 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 buildSodaLive-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 buildSodaLive-dev 동일 명령이 ** BUILD SUCCEEDED **로 완료됐다. 실제 서버 연동 수동 QA는 Task 6.4에 남겨 두었다.
  • 2026-07-12 최신 메시지 REST 재동기화 보완: JOINEDsyncLatestMessages 완료까지 Loading을 유지하고, 요청 실패·API 실패 응답·디코딩 오류를 applyRestFailure 토스트 경로로 전달하도록 수정했다. git diff --check HEAD가 통과했고, xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug CODE_SIGNING_ALLOWED=NO buildSodaLive-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 buildSodaLive-dev 동일 명령이 종료 코드 0으로 완료됐다. 실제 서버 연동 수동 QA는 Task 6.4에 남겨 두었다.
  • 2026-07-12 크리에이터 채널 후속 UX 수정: 크리에이터 채널 각 탭 empty state 문구의 상단 여백을 20으로 조정해 중앙 배치가 아닌 상단 배치로 변경했다. 본인 채널의 DM 버튼은 DM 확인하기로 표시하고, tap 시 AppState.pendingMainChatFilter(.dm)를 통해 메인 대화 탭의 DM 필터로 이동하도록 반영했다. 타인 채널의 DM 보내기 및 creatorId 기반 DM 채팅방 진입은 유지했다.