CODEX_PM_PROGRESS_2026-05-25
- 상위 Task: VOICE_CHAT_ROOM_수다방
- 작성자: Codex PM
- 작성일: 2026-05-25
- 상태: 진행중
1. 착수 배경
2026-05-25 10:00 KST 예정 작업을 owner 요청으로 즉시 수동 착수했다. 기존 자동화 mmx-10은 중복 실행 방지를 위해 삭제했다.
2. 확인한 상태
확인됨:
- 덱스PM이
STATUS_CHECK_2026-05-24.md를 작성했다. - Cursor가 수정한 Flutter 로비 패치가 아직 커밋되지 않은 상태로 남아 있었다.
- Flutter 로비 패치는 방 생성 후
_openRoom(created)를 호출하지 않고_enterRoomDirectly(created)로 진입하도록 바꾸는 내용이다. /snap/bin/flutter analyze flutter/mmx_player/lib/screens/voice_chat_lobby_screen.dart실행 결과No issues found를 확인했다.
추정:
- 수다방 backend는 여전히 participant/presence 모델 부재로 join/leave/message 권한 정합성 문제가 남아 있다.
- 이 문제는 Claude Code
/goal작업 후보로 분리하는 것이 적절하다.
3. 오늘 1차 완료 단위
SUBTASK_LOBBY_ROOM의 가장 작은 완료 단위로 아래 문제를 먼저 닫는다.
- 문제: 방 생성 API가 이미
current_members=1로 방장을 반영하는데, Flutter가 생성 직후 다시joinRoom을 호출할 수 있었다. - 조치: 생성 직후에는 join API를 부르지 않고 room 화면으로 직접 진입한다.
- 검증: 해당 Flutter 파일 단위 analyze 통과.
4. 다음 작업 후보
SUBTASK_TEXT_CHAT: room 화면 메시지 자동 갱신과 중복 방지 최소 구현SUBTASK_LOBBY_ROOM: 정원 초과, 비밀번호 오류, 방 종료 상태 UX 보강- backend state integrity: participant 모델, 중복 join 방지, non-participant message 차단
5. 2차 완료 단위
SUBTASK_TEXT_CHAT의 가장 작은 완료 단위로 아래 문제를 처리했다.
- 문제: room 화면은 최초 로드와 수동 새로고침 중심이라, 다른 사용자의 메시지가 자동으로 반영되지 않았다.
- 조치: room 화면에 5초 메시지 polling을 추가했다.
- 조치: 서버가 내려주는 message
id를 Flutter 모델에 보존하고, 전송 직후 append 시 id 기준으로 중복 추가를 방지했다. - 검증:
/snap/bin/flutter analyze flutter/mmx_player/lib/screens/voice_chat_room_screen.dart flutter/mmx_player/lib/services/voice_chat_service.dart통과. - 검증:
/snap/bin/flutter test통과, 8개 테스트 모두 성공.
남은 위험:
- 서버는 아직 active participant만 메시지를 보낼 수 있게 강제하지 않는다.
- 메시지 API는 pagination 없이 전체 조회 구조다.
- polling은 v0.1 최소 동기화이며, 최종 실시간 구조는 Socket.io 또는 room event와 다시 맞춰야 한다.
10. 6차 완료 단위 — 방 종료 UX 보강 (Claude Code 작업)
작업 지시: 수동 E2E의 host-leave/room-closed UX 갭을 가장 작은 패치로 닫는다.
변경 파일
api/routers/voice_chat.py—GET /voice-chat/rooms/{room_code}엔드포인트 추가- 활성 방과 종료된 방 모두 200으로 응답;
isActive필드로 구분 - 방 자체가 없으면 404
- 활성 방과 종료된 방 모두 200으로 응답;
api/tests/test_voice_chat_room_detail.py— 신규 테스트 파일 (3 케이스)test_get_active_room_returns_is_active_truetest_get_closed_room_returns_is_active_falsetest_get_nonexistent_room_raises_404
flutter/mmx_player/lib/services/voice_chat_service.dartVoiceChatRoomSummary에isActive필드 추가 (모든fromJson파싱에 반영)getRoomDetail(String roomCode)메서드 추가
flutter/mmx_player/lib/screens/voice_chat_room_screen.dart_roomClosed상태 플래그 추가- silent poll 마다
getRoomDetail을 추가 호출해isActive확인 - 방 종료 감지 시 poll 타이머 취소,
_roomClosed = true - 방 종료 시 노란 배너("방이 종료되었습니다. 호스트가 방을 나갔습니다.") 표시
- 방 종료 시 메시지 입력 필드 비활성화, 힌트 텍스트 변경, 전송 버튼 비활성화
flutter/mmx_player/test/voice_chat_service_test.dart— 3개 케이스 추가getRoomDetail parses active room with isActive=truegetRoomDetail parses closed room with isActive=falsegetRoomDetail throws VoiceChatApiException on 404
검증
/home/sglee/.local/bin/uv run pytest api/tests/test_voice_chat_room_detail.py api/tests/test_voice_chat_integrity.py api/tests/test_voice_chat_participant.py -v통과, 17개 테스트 성공/home/sglee/.local/bin/uv run pytest tests -q통과, 69개 테스트 성공/snap/bin/flutter test통과, 13개 테스트 성공 (신규 3개 포함)/snap/bin/flutter analyze lib/services/voice_chat_service.dart lib/screens/voice_chat_room_screen.dart→No issues found
수동 E2E 검증 방법
- 사용자 A가 방을 생성한다.
- 사용자 B가 같은 방에 입장한다.
- 양쪽에서 메시지를 주고받고 5초 polling으로 상대 메시지가 보이는지 확인한다.
- 사용자 A(host)가 "방 나가기"를 누른다.
- 사용자 B 화면에서 최대 5초 안에:
- 노란 배너가 나타나야 한다.
- 메시지 입력 필드가 비활성화되어야 한다.
- 전송 버튼이 비활성화되어야 한다.
- 마지막 시스템 메시지("퇴장했습니다. 방이 종료되었습니다.")가 채팅 목록에 표시되어야 한다.
남은 위험
- poll 방식이므로 방 종료 감지까지 최대 5초 지연이 있다. v0.1 허용 범위로 본다.
getRoomDetail추가 호출로 silent poll 당 API 요청이 2개(messages + room-detail)로 늘었다. 장기적으로는 Socket.IO 이벤트로 대체한다.- 방 종료 후 로비로 자동 복귀 버튼은 이번 패치에 포함하지 않았다. 사용자는 "방 나가기"를 수동으로 눌러야 한다.
6. 보류
- Agora token, RTC Windows, Android port는 owner secret 확인 전까지 보류한다.
- Claude Code는
/goal기반 작업 지시 후 결과물을 회수해 Codex PM이 검수한다.
7. 3차 완료 단위
Claude Code를 backend state integrity 작업자로 투입했다. Codex PM이 결과 diff를 검수했고, non-participant leave가 host 이름만으로 방을 닫을 수 있는 위험을 발견해 보정했다.
처리 내용:
voice_chat_participants모델과 migration 추가.- 방 생성 시 host participant를 생성.
- 중복 active join 시
current_members를 다시 증가시키지 않도록 차단. - active participant가 없으면 leave가 인원수와 방 상태를 바꾸지 않도록 처리.
- active participant만 채팅 메시지를 보낼 수 있도록 API에서 검사.
- 관련 단위 테스트 추가.
검증:
/home/sglee/.local/bin/uv run pytest tests/test_voice_chat_participant.py통과, 5개 테스트 성공./home/sglee/.local/bin/uv run pytest tests통과, 54개 테스트 성공./home/sglee/.local/bin/uv run python -m py_compile routers/voice_chat.py models/voice_chat_room.py schemas.py통과./home/sglee/.local/bin/uv run alembic heads결과c2d3e4f5a6b7 (head)확인.
남은 위험:
- 실제 Cloud SQL migration 적용 전까지 운영 DB에는 participant 테이블이 없다.
- 기존 active room에는 participant row가 없을 수 있다. 현재 v0.1 개발 중인 수다방 기준으로는 허용 가능한 잔여 위험으로 본다.
- 동시 join 경쟁 상태는 DB unique constraint 없이 완전히 닫히지 않았다. 다음 backend 보강에서 검토한다.
8. 4차 완료 단위
대표님이 별도 Claude Code 세션에서 받은 backend 개선 패치를 검토했다. 해당 패치는 설계 방향은 좋았지만, 이미 배포된 c2d3e4f5a6b7 migration과 별개로 b3c4d5e6f7a8 migration이 같은 voice_chat_participants 테이블을 다시 생성하려고 해서 Alembic head가 두 개로 갈라졌다.
Codex PM 조치:
b3c4d5e6f7a8migration은 채택하지 않았다.- 이미 배포된
c2d3e4f5a6b7이후 migration인d4e5f6a7b8c9를 새로 만들어 unique constraint만 추가하도록 정리했다. VoiceChatParticipant에는(room_code, nickname)unique constraint를 적용했다.- duplicate active join은 409가 아니라 200 no-op으로 정리했다. Flutter 안정성을 우선하여 count 증가와 system message 생성을 하지 않는다.
- inactive participant rejoin은 기존 row를 재활성화하고 count를 증가시킨다.
- leave, message send 판단 helper를 분리하고 관련 순수 로직 테스트를 추가했다.
검증:
/home/sglee/.local/bin/uv run alembic heads결과d4e5f6a7b8c9 (head)하나만 남는 것을 확인했다./home/sglee/.local/bin/uv run pytest tests/test_voice_chat_integrity.py tests/test_voice_chat_participant.py통과, 14개 테스트 성공./home/sglee/.local/bin/uv run pytest tests통과, 63개 테스트 성공./home/sglee/.local/bin/uv run python -m py_compile routers/voice_chat.py models/voice_chat_room.py models/__init__.py통과.
남은 위험:
- unique constraint migration은 운영 DB에 적용되어야 실제로 효력이 생긴다.
- 기존 방의 participant backfill은 아직 없다. 현재 남아 있는 오래된 테스트 방은 수다방 v0.1 검수 전에 정리하거나 별도 backfill 여부를 판단한다.
- nickname 중복 정책은 이제 DB가 막는다. Flutter UX에서는 같은 닉네임 입장 실패/재입장 흐름을 명확히 보여주는 보강이 필요하다.
9. 5차 완료 단위 - 텍스트 채팅 마감 기준 정리 및 오류 UX 보강
대표 지시에 따라 수다방 전체 중 음성 RTC는 뒤로 미루고, 텍스트 채팅만 별도 완료 기준으로 재정리했다.
텍스트 채팅 완료 기준:
- 방에 입장한 사용자만 메시지를 보낼 수 있어야 한다.
- 공백뿐인 닉네임, 방 제목, 비밀번호, 메시지는 서버에서 정리되거나 거부되어야 한다.
- 서버가 내려주는 오류 원인이 Flutter에서 원문 JSON이 아니라 사용자가 읽을 수 있는 문장으로 표시되어야 한다.
- 최소 자동 갱신은 유지되어야 한다. 현재는 5초 polling 기준이다.
- 테스트로 API 입력 검증과 Flutter 오류 표시가 고정되어야 한다.
이번 처리:
VoiceChatMessageCreate.body와authorName을 서버에서 trim한 뒤 검증하도록 했다.VoiceChatRoomCreate,VoiceChatRoomJoin,VoiceChatRoomLeave의 nickname/title/password 입력도 trim하도록 했다.- 공백뿐인 메시지는 Pydantic validation에서 거부되도록 했다.
- Flutter
VoiceChatService에VoiceChatApiException을 추가해 FastAPIdetail메시지를 앱에서 그대로 표시할 수 있게 했다. - Flutter 서비스 테스트를 추가해 한국어 오류 메시지와 validation 오류 메시지 표시를 검증했다.
- API schema validation 테스트를 추가해 trim과 공백 메시지 거부를 검증했다.
검증:
/home/sglee/.local/bin/uv run pytest tests -q통과, 66개 테스트 성공, 1개 warning./snap/bin/flutter test통과, 10개 테스트 성공./snap/bin/flutter analyze lib/services/voice_chat_service.dart lib/screens/voice_chat_room_screen.dart통과.
남은 텍스트 채팅 항목:
- 수동 E2E: 사용자 A/B가 같은 방에 들어가 메시지를 주고받고, 5초 polling으로 상대 메시지가 보이는지 확인.
- 방 종료 UX: host 퇴장 후 다른 사용자가 메시지를 보내려 할 때의 안내 문구와 화면 복귀 정책 확정.
- 참여자 목록 UI: 현재 currentMembers 숫자는 있으나 실제 참여자 닉네임 목록은 아직 없다.
- 메시지 pagination 또는 최신 N개 조회: 현재는 전체 메시지 조회 구조다. v0.1 소량 테스트에서는 허용하되 장시간 방에서는 보강이 필요하다.
11. 7차 완료 단위 - 대표님 동행 수동 E2E 및 즉시 보정
대표님과 Flutter Web on Windows Chrome, http://localhost:8101, 운영 API 조합으로 수다방 텍스트 채팅 수동 E2E를 진행했다.
확인된 통과 항목:
- 방 목록 조회
- 기존 방 입장
- 공개 방 생성 및 생성자 자동 입장
- 비밀번호 방 생성 및 생성자 자동 입장
- 비밀번호 오답 차단
- 비밀번호 정답 입장
- 메시지 전송
- 양방향 메시지 동기화
- 재입장 후 기존 메시지 조회
확인된 이슈:
- 내가 보낸 메시지가 왼쪽 정렬되어 상대 메시지와 구분이 약했다.
- 브라우저 새로고침 시 초기 화면으로 돌아갔다.
- 앱바 뒤로가기 사용 시 leave API가 호출되지 않아 인원 수가 감소하지 않았다.
- 한 세션 퇴장 후 다른 세션의 로비 인원 표시가 즉시 일치하지 않았다.
즉시 조치:
flutter/mmx_player/lib/screens/voice_chat_room_screen.dart- 앱바 뒤로가기 버튼이
leaveRoom을 호출하도록 변경했다. - 브라우저/시스템 뒤로가기 pop 이후에도 leave API를 fire-and-forget으로 호출하도록
PopScope를 추가했다. - 내가 보낸 메시지를 오른쪽 정렬 말풍선으로 표시하도록 구분했다.
- 앱바 뒤로가기 버튼이
flutter/mmx_player/lib/screens/voice_chat_lobby_screen.dart- 로비에 5초 자동 갱신을 추가해 다른 세션의 인원 변경을 주기적으로 반영하도록 했다.
www/public/project_document/reports/VOICE_CHAT_ROOM_수다방/TEXT_CHAT_E2E_2026-05-25.md- 대표님과 진행한 수동 E2E 결과를 별도 문서로 등록했다.
검증:
/snap/bin/flutter analyze lib/screens/voice_chat_room_screen.dart lib/screens/voice_chat_lobby_screen.dart통과./snap/bin/flutter analyze통과./snap/bin/flutter test통과, 14개 테스트 성공./home/sglee/.local/bin/uv run pytest tests -qAPI 기준 통과, 69개 테스트 성공, 1개 warning.
아직 남은 확인:
- 패치 후 브라우저 수동 E2E 재실행.
- 브라우저 새로고침 후 닉네임/방 복구는 아직 미구현이다. 오늘 패치 범위에서는 빠른 텍스트 채팅 정합성에 집중했다.
12. 8차 완료 단위 - 앱/탭 종료 시 자동 퇴장 보강
대표님 지적:
- "나가기" 버튼뿐 아니라 앱을 그냥 닫는 경우에도 나가기 호출이 자동으로 되어야 한다.
판단:
- 명시적 "방 나가기"와 앱바 뒤로가기는 기존 HTTP
leaveRoom으로 처리한다. - Flutter Web의 탭 닫기/새로고침은 일반 async HTTP가 완료된다는 보장이 낮으므로
navigator.sendBeacon을 사용한다. - Native app 종료는 lifecycle
detached에서 best-effortleaveRoom을 호출한다. - 단, OS 강제 종료, 네트워크 단절, 브라우저가 beacon 전송을 버리는 경우까지 100% 보장할 수는 없다. 장기적으로는 heartbeat/TTL presence가 필요하다.
변경:
flutter/mmx_player/lib/services/voice_chat_leave_notifier.dart- 플랫폼별 leave notifier export 추가.
flutter/mmx_player/lib/services/voice_chat_leave_notifier_stub.dart- native/test 기본 no-op 구현.
flutter/mmx_player/lib/services/voice_chat_leave_notifier_web.dartpagehide,beforeunload이벤트에서sendBeacon으로 leave API 호출.
flutter/mmx_player/lib/services/voice_chat_service.dartleaveRoomUri(roomCode)공개 메서드 추가.
flutter/mmx_player/lib/screens/voice_chat_room_screen.dartWidgetsBindingObserver등록.- lifecycle
detached에서 best-effort leave 호출. - 정상 leave 완료 시 beacon 중복 전송 방지.
flutter/mmx_player/pubspec.yaml- Flutter Web interop을 위해
web패키지를 direct dependency로 명시.
- Flutter Web interop을 위해
검증:
/snap/bin/flutter analyze통과./snap/bin/flutter test통과, 14개 테스트 성공./snap/bin/flutter build web통과.
남은 확인:
- 실제 브라우저에서 방 입장 후 탭을 닫았을 때 API가 leave를 받는지 수동 검증 필요.
- Android/Windows native의 강제 종료 처리는 OS 제약이 있으므로 heartbeat/TTL presence 설계가 필요하다.