Files
sodalive-android/docs/20260319_라이브룸채팅창얼리기기능구현계획.md
klaus 26522cea3f feat(live-room): 채팅창 얼리기 기능을 추가한다
채팅 입력 제어와 룸 상태 동기화를 통합해 지연 입장자도 동일 상태를 적용한다.
2026-03-19 18:00:43 +09:00

15 KiB

20260319_라이브룸채팅창얼리기기능구현계획.md

개요

  • LiveRoomActivity에 채팅창 얼리기(Freeze) 토글을 추가한다.
  • 채팅창 얼리기 상태에서는 방장을 제외한 모든 사용자가 채팅 입력/전송을 할 수 없어야 한다.
  • 본 문서는 계획과 구현/검증 결과를 함께 관리한다.

요구사항 요약

  • 토글 버튼 위치: activity_live_room.xmltv_signature_switch 왼쪽.
  • 얼림(ON): 방장 제외 전체 유저 채팅 입력 불가(포커스/입력/전송 모두 차단).
  • 녹임(OFF): 기존 채팅금지 해제와 동일하게 채팅 가능 상태로 복귀.
  • 상태 메시지: 얼림/녹임 시 모든 유저 채팅 리스트에 시스템 메시지 노출.
  • 상태 메시지 UI: 사용자 입장 알림(LiveRoomJoinChat)과 동일한 UI 사용.
  • 지연 입장: 채팅창이 얼려진 상태로 입장한 사용자도 즉시 상태를 받아 입력 불가여야 함.
  • 상태 변경 패턴: 룰렛과 동일하게 방장 ON/OFF 시 서버 API로 상태를 먼저 반영하고, 성공 시 RTM으로 실시간 전파.

상태 저장 전략 판단 (ROOM_INFO vs 별도 상태)

결론

  • ROOM_INFO에 isChatFrozen 상태를 저장하고, RTM 이벤트는 즉시 반영용으로 병행하는 전략을 채택한다.

판단 근거

  • ROOM_INFO 기반 초기 동기화 경로가 이미 존재한다.
    • LiveRoomViewModel.getRoomInfo -> roomInfoResponse/roomInfoLiveData 갱신.
    • LiveRoomActivityonCreate, onPresenceEvent(REMOTE_JOIN) 등에서 getRoomInfo를 재호출한다.
  • 룸 전역 상태 선례가 존재한다.
    • GetRoomInfoResponse.isActiveRoulette + LiveRoomChatRawMessageType.TOGGLE_ROULETTE 조합으로 운용 중이다.
  • 별도 로컬 상태(예: SharedPreferences)는 디바이스 단위라 전역 정합성에 취약하다.
    • 현재 noChatRoomList는 로컬 단말 재진입 보조 용도이며, 전 유저 상태의 단일 진실원천(SSOT)으로는 부적합하다.
  • 외부 근거(Agora Signaling)상 메시지 채널은 pub/sub 모델이므로, 지연 입장자 상태 보장은 저장소(메타데이터/룸 정보) 병행이 유리하다.

외부 레퍼런스(요약)

완료 기준 (Acceptance Criteria)

  • AC1: 방장이 얼림 ON 시, 방장을 제외한 사용자는 et_chat에 포커스/입력/전송이 모두 불가능하다.
  • AC2: 방장이 얼림 OFF 시, 방장을 제외한 사용자의 채팅 입력/전송이 즉시 복구된다.
  • AC3: 얼림/녹임 이벤트마다 모든 사용자 채팅 리스트에 시스템 상태 메시지가 1회씩 노출된다.
  • AC4: 얼림 상태에서 새로 입장한 사용자는 입장 직후 입력 불가 상태를 즉시 적용받는다.
  • AC5: 얼리기 토글 버튼은 tv_signature_switch의 왼쪽에 배치된다.
  • AC6: 방장 얼림 ON/OFF 시 서버 API가 선행 호출되고, 성공한 경우에만 RTM 상태 브로드캐스트가 전송된다.

구현 체크리스트

1) UI/입력 제어

  • app/src/main/res/layout/activity_live_room.xml 상단 토글 영역에 tv_chat_freeze_switch 추가 (tv_signature_switch 왼쪽).
  • LiveRoomActivity에 전역 상태 변수(isChatFrozen) 및 UI 바인딩 로직 추가.
  • etChat 포커스/입력/전송 경로(setOnFocusChangeListener, inputChat)를 통합 가드로 정리하여 Freeze 상태에서 완전 차단.

2) 상태 전파/수신

  • 서버 API 경로 반영: 전용 endpoint PUT /live/room/info/set/chat-freeze + SetChatFreezeRequest(roomId, isChatFrozen)로 ON/OFF를 서버 선반영.
  • 저장소/뷰모델 경로 추가: LiveRepository/LiveRoomViewModel에 chat freeze ON/OFF API 호출 메서드 추가.
  • 방장 토글 액션은 API 성공 콜백에서만 RTM 브로드캐스트를 전송하도록 순서 보장(룰렛과 동일).
  • API 실패 시 RTM 미전송 + 오류 토스트 처리 시나리오 포함.
  • LiveRoomChatRawMessageType에 Freeze 이벤트 타입 추가(예: TOGGLE_CHAT_FREEZE).
  • LiveRoomChatRawMessage에 Freeze 상태 전달 필드 추가(예: isChatFrozen).
  • 방장 토글 시 RTM 그룹 브로드캐스트 전송 + 수신 분기 처리(rtmEventListener.onMessageEvent) 구현.

3) 지연 입장 동기화

  • GetRoomInfoResponseisChatFrozen 필드 추가.
  • roomInfoLiveData.observe에서 isChatFrozen을 입력 제어 상태에 반영.
  • 입장/재연결/REMOTE_JOIN 시점에서 ROOM_INFO 재조회 흐름으로 상태 재적용이 가능하도록 적용.

4) 시스템 메시지(UI 동일성)

  • 입장 알림과 동일한 UI(item_live_room_join_chat.xml)를 재사용할 수 있도록 시스템 메시지 모델 경로 확장.
  • 얼림/녹임 메시지를 chatAdapter.items.add(...) + invalidateChat() 경로로 주입.

5) 문자열/국제화

  • values/strings.xml, values-en/strings.xml, values-ja/strings.xml에 Freeze ON/OFF 라벨/상태 메시지 문구 추가.

6) 검증

  • 정적 진단: 수정 파일 대상 lsp_diagnostics 확인.
  • 빌드/테스트: ./gradlew :app:testDebugUnitTest, ./gradlew :app:assembleDebug 실행.
  • 수동 QA: 연결 단말 설치/실행 경로 확인 및 Activity 진입 시도 결과 기록.

영향 파일(예상)

  • app/src/main/res/layout/activity_live_room.xml
  • app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt
  • app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatRawMessage.kt
  • app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChat.kt
  • app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt
  • app/src/main/res/values/strings.xml
  • app/src/main/res/values-en/strings.xml
  • app/src/main/res/values-ja/strings.xml
  • app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt
  • app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt
  • app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt

리스크 및 의존성

  • 서버 ROOM_INFO에 isChatFrozen 필드가 제공되지 않으면 지연 입장 정합성 보장이 어렵다.
  • RTM 메시지만으로 구현하면 pub/sub 특성상 지연 입장 사용자에게 과거 상태 스냅샷이 누락될 수 있다.
  • API 성공 이전 RTM을 먼저 전송하면 서버 상태와 클라이언트 UI가 불일치할 수 있으므로, 전송 순서를 API 성공 이후로 강제해야 한다.
  • UI 차단을 포커스 차단만으로 처리하면 키보드/입력 우회가 가능하므로 EditText 자체 비활성 포함이 필요하다.

검증 기록

  • 2026-03-19

    • 무엇/왜/어떻게: 채팅창 얼리기 기능의 저장 전략 판단을 위해 LiveRoom 내부 구현 패턴, RTM 메시지 경로, ROOM_INFO 동기화 지점을 조사하고 계획 문서로 정리했다.
    • 실행 명령/도구:
      • task(subagent_type="explore") x3 (no-chat 흐름, room state sync, system UI 패턴)
      • task(subagent_type="librarian") x2 (Agora 상태 전파 문서/실사례)
      • grep("isNoChatting|NO_CHATTING|..."), grep("LiveRoomChatRawMessageType|ROOM_INFO|...")
      • ast_grep_search("LiveRoomChatRawMessage($$$)")
      • read(LiveRoomActivity.kt, LiveRoomViewModel.kt, Agora.kt, GetRoomInfoResponse.kt, activity_live_room.xml, strings.xml 등)
      • bash("rg -n ...") 시도
    • 결과:
      • no-chat 기존 구현(로컬 저장 + peer 명령 + 타이머)과 ROOM_INFO 기반 전역 상태 동기화 패턴을 분리 확인.
      • isActiveRoulette 선례를 통해 ROOM_INFO + RTM 병행 전략이 지연 입장 정합성에 유리함을 확인.
      • 시스템 메시지 UI는 LiveRoomJoinChat/item_live_room_join_chat.xml 재사용이 가장 요구사항에 부합함을 확인.
      • 환경에서 rg 명령은 미설치(command not found)로 확인되어, 동일 탐색은 grep/ast_grep_search/에이전트 결과로 보완.
  • 2026-03-19

    • 무엇/왜/어떻게: 계획 문서 기준으로 채팅창 얼리기 기능을 구현하고, 룰렛과 동일하게 서버 API 성공 후 RTM 브로드캐스트 순서를 적용했다.
    • 실행 명령/도구:
      • 코드 반영: apply_patch (LiveRoomActivity.kt, LiveRoomViewModel.kt, LiveRepository.kt, LiveRoomChat.kt, LiveRoomChatRawMessage.kt, GetRoomInfoResponse.kt, EditLiveRoomInfoRequest.kt, activity_live_room.xml, strings*.xml)
      • 정적 진단: lsp_diagnostics (현재 환경 .kt, .xml 서버 미설정)
      • 빌드/테스트: ./gradlew :app:testDebugUnitTest :app:assembleDebug
      • 스타일 검증: ./gradlew :app:ktlintCheck
      • 디바이스 수동 확인: adb devices, ./gradlew :app:installDebug, adb shell am start -W -n ...LiveRoomActivity --el roomId 1, adb shell run-as kr.co.vividnext.sodalive.debug am start --user 0 -n ...LiveRoomActivity --el roomId 1
    • 결과:
      • 초기 빌드에서 values-en/strings.xml 신규 문구 1건(screen_live_room_chat_freeze_warning) 리소스 컴파일 오류를 확인하고 문구를 수정했다.
      • 재실행 결과 ./gradlew :app:testDebugUnitTest :app:assembleDebug 성공.
      • ./gradlew :app:ktlintCheck 성공.
      • ./gradlew :app:installDebug 성공(연결 단말 1대 설치 확인).
      • 셸 직접 실행은 non-exported Activity 제약으로 일반 am start가 차단되었고, run-as 기반 실행은 가능했으나 디버그 앱 사용자 데이터(shared_prefs/kr.co.vividnext.sodalive.debug_preferences.xml)가 비어 있어 실제 라이브룸 시나리오(방장/일반유저 2계정)까지는 환경 제약으로 진행하지 못했다.
  • 2026-03-19

    • 무엇/왜/어떻게: 사용자 요청에 따라 채팅 얼리기 API를 editLiveRoomInfo에서 완전히 분리해 전용 URL(/live/room/info/set/chat-freeze)로 교체했다.
    • 실행 명령/도구:
      • 코드 반영: apply_patch (LiveApi.kt, LiveRepository.kt, LiveRoomViewModel.kt, SetChatFreezeRequest.kt, EditLiveRoomInfoRequest.kt)
      • 확인: grep("setChatFreeze|editLiveRoomInfo|/live/room/info/set/chat-freeze"), git diff -- LiveApi.kt LiveRepository.kt LiveRoomViewModel.kt SetChatFreezeRequest.kt
    • 결과:
      • setChatFreeze 호출은 전용 endpoint로 분리되었고 api.editLiveRoomInfo(...) 재사용이 제거되었다.
      • EditLiveRoomInfoRequestisChatFrozen 필드는 제거되어 채팅 얼리기와 라이브 정보 수정 API 계약이 분리되었다.
  • 2026-03-19

    • 무엇/왜/어떻게: 전용 API 분리 이후 실제 단말 실행 가능 여부를 확인해 수동 QA 착수 가능 상태를 점검했다.
    • 실행 명령/도구:
      • adb devices
      • adb shell monkey -p kr.co.vividnext.sodalive.debug -c android.intent.category.LAUNCHER 1
    • 결과:
      • 연결 단말 1대(2cec640c34017ece)가 확인되었고, debug 앱 런처 실행 이벤트 주입이 성공했다.
      • 채팅 얼리기 엔드투엔드 검증(방장/일반 유저 2계정, 동일 룸 입장, ON/OFF 동기화)은 로그인 세션/테스트 계정/실제 룸 시나리오 준비가 필요해 후속 수동 QA 항목으로 유지한다.
  • 2026-03-19

    • 무엇/왜/어떻게: TODO 잔여 항목(회귀 검증)을 완료하기 위해 테스트/빌드/코드스타일 검증을 단일 Gradle 실행으로 재확인했다.
    • 실행 명령/도구:
      • ./gradlew :app:testDebugUnitTest :app:assembleDebug :app:ktlintCheck
    • 결과:
      • :app:testDebugUnitTest, :app:assembleDebug, :app:ktlintCheck 모두 UP-TO-DATE 상태로 성공했고 전체 BUILD SUCCESSFUL을 확인했다.
      • 추가 회귀 및 스타일 위반은 재현되지 않았다.
  • 2026-03-19

    • 무엇/왜/어떻게: 사용자 요청에 따라 채팅창 얼림/해제 시스템 메시지에서 크리에이터 닉네임 노출을 제거하고 고정 문구만 표시되도록 문자열 리소스를 수정했다.
    • 실행 명령/도구:
      • 코드 반영: apply_patch (app/src/main/res/values/strings.xml, app/src/main/res/values-en/strings.xml, app/src/main/res/values-ja/strings.xml)
      • 수동 확인: read(.../values*/strings.xml)screen_live_room_chat_freeze_started/ended 값에서 %1$s 제거 확인
      • 정적 진단: lsp_diagnostics(LiveRoomActivity.kt), lsp_diagnostics(strings.xml) (환경상 .kt/.xml LSP 미구성)
      • 회귀 검증: ./gradlew :app:testDebugUnitTest :app:assembleDebug :app:ktlintCheck
    • 결과:
      • 한국어 메시지는 요청대로 채팅창을 얼렸습니다., 채팅창 얼리기를 해제했습니다로 고정되어 닉네임이 표시되지 않는다.
      • :app:testDebugUnitTest, :app:assembleDebug는 성공했다.
      • :app:ktlintCheckLiveRoomActivity.kt의 기존 광범위 스타일 위반으로 실패했으며, 본 요청에서 수정한 문자열 리소스 3개 파일에서는 신규 위반이 관찰되지 않았다.
  • 2026-03-19

    • 무엇/왜/어떻게: 지연 입장 사용자가 얼림 상태를 인지하지 못하는 문제를 해결하기 위해 isChatFrozen == true 초기 동기화 시 시스템 메시지를 1회 노출하고, 채팅 입력 영역 터치 시 얼림 토스트를 표시하도록 LiveRoomActivity를 수정했다.
    • 실행 명령/도구:
      • 코드 반영: apply_patch (app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt)
      • 정적 확인: read(LiveRoomActivity.kt)hasShownInitialChatFreezeNotice, rlInputChat.setOnTouchListener, roomInfoLiveData.observe 분기 추가 확인
      • 정적 진단: lsp_diagnostics(LiveRoomActivity.kt) (환경상 .kt LSP 미구성)
      • 검증: ./gradlew :app:testDebugUnitTest :app:assembleDebug :app:ktlintCheck, ./gradlew :app:testDebugUnitTest :app:assembleDebug
      • 수동 스모크: adb shell monkey -p kr.co.vividnext.sodalive.debug -c android.intent.category.LAUNCHER 1
    • 결과:
      • 지연 입장 초기 동기화에서 non-host + isChatFrozen=true일 때 채팅 리스트에 얼림 메시지가 1회 표시되도록 반영했다.
      • non-host가 채팅 입력 영역(rl_input_chat)을 터치하면 screen_live_room_chat_freeze_warning 토스트가 노출되도록 반영했다.
      • :app:testDebugUnitTest, :app:assembleDebug는 성공했다.
      • :app:ktlintCheckLiveRoomActivity.kt의 기존 광범위 스타일 위반으로 실패했으며, 이번 수정 라인에서 신규 위반은 확인되지 않았다.