Monitor 대시보드 (카드뷰·포커스뷰·통합) — 코드레벨 동작 흐름 P1코드레벨

마지막 업데이트 2026-07-29

Monitor 대시보드 (카드뷰·포커스뷰·통합) — 코드레벨 동작 흐름 P…: 입력: 개요 · 범위, 주요 처리 단계: 3계층 라우팅 app/monitor-dashboard/, 결과: 파일 · 라인 레퍼런스 흐름
동작 흐름 요약
  1. 입력: 개요 · 범위
  2. 주요 처리 단계: 3계층 라우팅 app/monitor-dashboard/
  3. 결과: 파일 · 라인 레퍼런스
작성일: 2026-06-14 대상: 개발자 — 진행자 모니터링 화면 구조 파악 핵심 파일: app/monitor-dashboard/, widgets/monitor-dashboard/, stores/use-monitor-*-store.ts
💬 대화로 먼저 이해하기 — "CCTV 관제실 비유" (비개발자·처음 읽는 사람용)
Q이 화면은 누가, 어떻게 쓰는 건가요?
A진행자(치료사)가 여러 수업을 동시에 지켜보는 CCTV 관제실이에요. 전체 세션 목록(홈) → 여러 화면을 나란히 띄운 그리드(카드뷰) → 한 화면만 크게(포커스뷰)의 3계층으로 들어갑니다. 카드 한 장이 아동 한 명의 라이브 수업이죠.
Q화면을 그렇게 많이 띄우면 무겁지 않나요?
A그리드에서는 일부러 저화질 썸네일만 받아요. 특정 아동을 확대하는 순간에만 고화질(layer 2)을 요청하죠 — "보이는 것만 고화질". 소리도 기본은 꺼져 있고, 카드별 청취 토글로 원하는 아동만 켭니다(포커스뷰에 들어가면 카드 청취는 자동 OFF).
Q모니터 자리가 자꾸 바뀌면 헷갈릴 텐데요?
A맞아요, 관제실 모니터 배치는 고정돼야 하죠. 예약 세션 폴링 응답이 잠깐 흔들려도 카드 자리가 안 바뀌도록 roomId 기준 sticky 정렬 + 보존 캐시를 씁니다.
Q코드는 어디를 보면 되나요?
A라우팅 3계층은 app/monitor-dashboard/, 카드 위젯은 widgets/monitor-dashboard/(V2만 대상, V1 잔존 주의)예요. 회귀가 반복된 지점은 본문 "함정 · 회귀 패턴" 섹션에 모여 있습니다.

개요 · 범위

진행자(치료사)가 모니터링 중인 모든 세션(1:1 + 그룹)을 한눈에 보는 화면. 대시보드 홈 → 카드뷰(그리드) → 포커스뷰(단일 세션) 3계층. @handover-monitor-dashboard/@handover-card-view/@handover-focus-view 영역.

AI-driven + Jacob 영역: 설계 의도·결정·함정의 원본은 docs/handover/jacob/{monitor-dashboard,card-view,focus-view}.md. 기획 의도(왜 통합했나 등)는 기획자 리지 담당. 본 문서는 코드 흐름 + 그 md의 개발 의도를 인용한다. Jacob 평가: "2주 더 있어도 손볼 거 없음"(안정적).

3계층 라우팅 app/monitor-dashboard/

경로역할
/monitor-dashboard (page.tsx)홈 — 전체 세션 목록(통합 모니터링)
/monitor-dashboard/[group]카드뷰 — 그룹 내 여러 아동 동시 모니터링(그리드)
/monitor-dashboard/[group]/[roomId]포커스뷰 — 단일 세션 집중

세션 분류: oneOnOne / multi(group) / test 3종(useGroupMonitoring + useScheduledSessions 응답으로 판정, #614). 1:1은 바로 포커스뷰, 그룹은 카드 그리드 경유. 카드 UI는 1:1·그룹 공통(#496). 출처: monitor-dashboard.md#5분

상태·데이터 흐름

/api/scheduled-lessons → useScheduledSessions ─┐ useGroupMonitoring ───────────────────────────┤→ monitor stores ─→ 홈/카드뷰/포커스뷰 Socket.io (host-socket) ───────────────────────┘ (use-monitor-data-store / use-monitor-ui-store) └ disconnect backstop → 타이머 리셋 방지(#357, #448)
  • 세션 카드 병합: 활성(active, 소켓 라이브) + 예약(scheduled API)을 합쳐 카드 목록 구성. 예약 폴링 응답이 잠시 누락돼도 카드 자리가 흔들리지 않게 roomId 기준 sticky 정렬 + 보존 캐시(#578, #622).
  • 전역 상태: use-monitor-data-store(rooms/sessions/toasts/alerts), use-monitor-ui-store(패널 토글 등).
  • 사용자명: UUID 노출 금지, 항상 실제 이름(profile 캐싱, #554/#569).
출처: monitor-dashboard.md#상태·데이터-흐름, card-view.md#invariants

카드뷰 (세션 카드) widgets/monitor-dashboard/session-card/ · entities/monitor-session/use-monitor-session.ts

카드 한 장 = 한 룸(roomId)에 진입한 아동 1명의 라이브 수업. useMonitorSession이 WebRTC 세션 연결·상태를, useHostSocket이 호스트-게스트 소켓을 담당.

기능동작커밋
청취 토글 + 볼륨해당 아동 핑퐁이 음성, 카드별 독립 슬라이더 0~300%#581/#596/#638
포커스뷰 진입카드 청취 자동 OFF(의도된 동작) — 포커스뷰에서 다시 ON#436
자동 응답('응')핑퐁이 무응답 시 진행자 대신 자동 발송(카드별 수동 모드). 이후 수동 발송만 허용으로 정책 변경#399→#455
누적 시간useCumulativeElapsed — lessonStartedAt 기반 + 폴백 타이머#630
스텝 네비/개입 폼/이슈 스탬프카드에서 직접 제어#462/#454/#555
V1/V2 주의: V1 카드뷰(features/session/ui/*, 현 코드에 session-card.tsx 잔존)는 작업 대상 아님. V2(widgets/monitor-dashboard/)만 카드뷰 인수인계 대상. card-view.md#핵심-파일-맵

비디오 확대 features/monitor/video-expansion/use-video-expansion.ts

카드 썸네일은 저화질(mediasoup spatialLayer 0, #10). 특정 아동을 확대하면 expandVideo(peerId, ...)setConsumerLayer(peerId, 2)고화질(L2) 요청, 축소 시 복귀. "보이는 것만 고화질"(SFU selective forwarding). 상세 → #10.

헤드셋 모드 · 환경소음 (Redis settings) widgets/monitor-dashboard/headset-selector/

헤드셋 모드(far/near field)·환경소음 토글은 Redis settings-store에 저장된다(PPI-879/PPI-962, #636). 다중 인스턴스에서 cross-instance hydration fallback 있음(id-006). 저장 순서·롤백 규칙은 #5 Router 매니저 및 redis-infra.md.

룸 설정 변경 규칙(redis-infra.md): Redis 저장 → broadcast 순서(id-019), ack 실패 시 UI 롤백(id-033). 낙관적 업데이트 금지.

예약 세션 · 미접속 알림톡 · 토스트

  • 오늘 예약 세션: /api/scheduled-lessonsuse-today-scheduled-sessions(today-scheduled/ 위젯).
  • 미접속/장시간 감지: use-session-checker. 게스트 미접속 5/10분 시 알림톡 자동 발송.
  • 토스트: 도움 요청·미접속·자동응답 카운트다운 등. 이름은 실제 사용자명(#554), 발송 버튼 포함(#421).
미해결: 미접속 알림톡 재접속 케이스 오발송은 여전히 미해결(방지 PR #378→revert #381은 QA 오류). 임시 회피는 진행자 수동 발송(#421). monitor-dashboard.md#결정과-거절된-대안, known-gaps.md

함정 · 회귀 패턴 출처: card-view.md / monitor-dashboard.md#failure-modes

  • 누적 시간 정합성: lessonStartedAt 기반 계산과 폴백 타이머가 동시 존재 → 어긋남 회귀 반복(#507/#527/#630). 누적시간 변경 시 게스트 자동전환 판정값과 호스트 UI 양쪽 동시 갱신 필수.
  • 카드 자리 흔들림: scheduled 폴링 응답 변동 시 카드 순서 변경 → sticky 캐시로 해결(#578/#622). 정렬 변경 시 sticky 위에 정렬 레이어 권장.
  • 청취 토글 race: 빠른 클릭/포커스 전환 시 OFF 누락(#438/#436).
  • 1:1/그룹 분류 깜빡임: scheduled 응답·렌더 타이밍 의존(#614). 분류 변경 시 useGroupMonitoring + scheduled 파싱 + 카드 라우팅 분기 동시 확인.
  • 소켓 재연결 시 타이머 리셋: disconnect backstop으로 해결(#357/#448). 소켓 이벤트 추가 시 entities/monitor-socket + apps/socket 양쪽 + Redis 채널 prefix(#640) 확인.
  • 테스트 세션 격리: 운영 세션 카운트에 섞이지 않게(#299).

파일 · 라인 레퍼런스

파일역할
app/monitor-dashboard/{page,[group]/page,[group]/[roomId]/page}.tsx홈/카드뷰/포커스뷰 라우팅
widgets/monitor-dashboard/{session-card,live-sessions,today-scheduled,headset-selector}/V2 위젯
stores/use-monitor-data-store.ts, use-monitor-ui-store.ts전역 상태
entities/monitor-session/use-monitor-session.tsWebRTC 세션 연결·상태
features/monitor/video-expansion/use-video-expansion.ts확대 시 layer 2
features/monitor/hooks/{use-scheduled-sessions,use-today-scheduled-sessions}.ts예약 세션
docs/handover/jacob/{monitor-dashboard,card-view,focus-view}.md설계 의도·결정·함정 원본