아동 알림 프리셋 트리거 race · 이미지 동기화 수정
마지막 업데이트 2026-07-22
TL;DR
아동 알림 프리셋(환경소음 안내)의 트리거/표시에서 발견된 세 가지 증상 — ① 첫 트리거가 가끔 안 뜸, ② 표시 중 이미지가 계속 바뀜, ③ 진행자 미러뷰와 아동 화면의 이미지가 다름 — 을 분석하고 수정했다.
근본 원인은 "진행자가 고른 이미지 스냅샷(HOST_SET, Redis 경유로 느림)"이 "트리거 메시지(host-session-message, 직행으로 빠름)"보다 늦게 도착하는 race와, 게스트가 스냅샷을 못 받은 상태에서 독립적으로 랜덤 이미지를 고르는 설계였다.
최종 해법은 진행자가 고른 단일 이미지를 트리거 메시지에 직접 동봉해, 게스트가 스냅샷 도착 타이밍·랜덤 없이 호스트와 동일한 이미지를 즉시 표시하도록 한 것이다. 함께 "표시 중 이미지 고정"과 "표시 중 진행자 재트리거 차단"도 정리했다.
1. 증상 — 무엇이 문제였나
- ① 첫 트리거 미발생 — 세션 시작 직후 또는 프리셋을 바꿔 누른 직후의 첫 트리거에서 아동 화면에 알림이 안 뜨고, 두 번째부터 떴다. (AI는 정상적으로 "안 들려"라고 응답 → 시각 알림만 누락)
- ② 표시 중 이미지 교체 — 알림 15초 표시 도중 진행자가 다시 누르거나 늦은 스냅샷이 도착하면 떠 있는 이미지가 계속 바뀌었다.
- ③ 진행자/아동 이미지 불일치 — 진행자 미러뷰가 보여주는 이미지와 아동 화면 이미지가 서로 달랐다.
2. 원인 — 스냅샷 vs 트리거 도착 race
프리셋 버튼을 누르면 두 개의 신호가 거의 동시에 게스트로 향한다.
| 신호 | 내용 | 경로 | 속도 |
|---|---|---|---|
① 스냅샷 HOST_SET_AMBIENT_NOISE_TEMPLATE | 표시할 텍스트·이미지·트리거 sendText | 서버 → Redis 저장(await) → 게스트 | 느림 |
② 트리거 host-session-message | 문자열 {{환경소음N}} (AI 프롬프트 + 알림 트리거) | 서버 → 게스트 직행 | 빠름 |
게스트는 ②가 왔을 때 message.includes(스냅샷.sendText)로 표시 여부를 판단하고, 이미지는 스냅샷에서 가져온다. 그런데 ②가 ①보다 먼저 오면:
- 표시 누락 — 비교 기준 스냅샷이 아직 없어 매칭 실패 → 그 트리거는 버려짐(증상 ①).
- 이미지 불일치 — 게스트가 갖고 있던 전체목록(full-list) 스냅샷(입장/패널 마운트/토글 시 전파)으로 독립 랜덤을 골라, 호스트가 고른 1장과 달라짐(증상 ③).
full-list 스냅샷 출처: 게스트 입장(Redis 저장본), 진행자 패널 마운트(broadcastTemplate), 프리셋 토글 ON/저장(broadcastActive) — 모두 이미지 목록 전체를 보낸다. 버튼 클릭(pickAndBroadcastAmbientTemplate)만 1장으로 좁혀 보낸다.
3. 수정 ① — 첫 트리거 race 보정 + X 타이머 누수
트리거가 와도 스냅샷이 없어 매칭 실패하면 버리지 않고 버퍼링하고, 뒤늦게 스냅샷이 도착하는 순간 재평가해 띄운다(양방향 도착 순서 모두 커버).
- 트리거 매칭 실패 →
pendingAmbientMessageRef = {message, at}에 보관 - 스냅샷(HOST_SET) 도착 → 3초 내 버퍼 메시지가 새 sendText를 포함하면 resolved via late snapshot로 표시
덤으로 X(닫기) 버튼(clearAmbientNoiseAlert)이 15초 자동 숨김 타이머를 clearTimeout 안 하던 누수도 수정(토글 OFF·언마운트 경로와 일치).
4. 수정 ② — 표시 중 이미지 고정
알림이 한 번 뜨면 그 표시 동안 이미지를 고정한다.
- 게스트:
showAmbientNoiseAlert가 "표시를 새로 시작할 때만" 이미지를 확정하고, 표시 중엔 타이머만 연장. 표시 중 늦은 스냅샷 재적용 제거. - 진행자 미러뷰:
store.current에 클릭 식별용seq를 부여하고, 표시 시작 시점의seq로 잠금. 표시 중에는 같은 pick의 URL resolve만 반영하고 다른 클릭(새 seq)은 무시. - 아동이 X로 닫으면(표시 종료) 잠금 해제 → 다음 트리거부터 다시 변경.
5. 수정 ③ — 표시 중 진행자 재트리거 차단
아동 알림이 표시 중(guestAmbientNoiseAlertVisible)이면 진행자의 프리셋 트리거를 막는다 — 포커스뷰 프리셋 버튼·카드뷰 드롭다운 모두 비활성화. 아동이 X로 닫으면 visible:false가 relay되어 자동 재활성화된다.
6. 수정 ④ (근본) — 트리거에 이미지 동봉
시각 알림의 "표시"는 특정 게스트에게 타겟된 host-session-message가 구동하므로(스냅샷은 글로벌·setup용), 이미지를 이 타겟 트리거에 동봉하면 멀티룸에서도 정확하다.
pickAndBroadcastAmbientTemplate가 고른 단일 이미지(AmbientTriggerImage)를 반환- 콜백(
onPresetSelected/onSendMessage) →sendMessagepayload의ambientImage로 동봉 - 서버 relay(
host-session-message) pass-through (web + socket) - 게스트
handleHostMessage: 동봉 이미지로 직접 표시 — 매칭·랜덤·스냅샷 의존 제거
이미지를 동봉하지 않은 트리거(예: {{환경소음}} 수동 입력)는 기존 sendText 매칭/race 보정 경로로 폴백한다.
결과: 게스트가 호스트가 고른 그 1장(동일 pick의 fileName/id)을 표시 → 미러뷰와 항상 일치하고, 표시 중 고정·표시 중 차단과도 충돌하지 않는다.
7. Before / After
| 상황 | 전 | 후 |
|---|---|---|
| 세션 첫 트리거 | 스냅샷 늦으면 누락 | 버퍼 재평가로 표시 |
| 표시 중 재클릭/늦은 스냅샷 | 이미지 계속 교체 | 이미지 고정 |
| host vs guest 이미지 | 독립 랜덤 → 불일치 | 동봉 이미지로 항상 일치 |
| 표시 중 진행자 트리거 | 가능(혼란) | 차단, X 후 재허용 |
8. 주요 변경 파일 · 커밋
커밋
id-053첫 트리거 race 보정 + 진단 로그 + X 타이머 누수id-020게스트 표시 중 이미지 고정id-015진행자 미러뷰 표시 중 이미지 고정(seq)id-023트리거 이미지 동봉 + 표시 중 진행자 재트리거 차단
파일
entities/guest-page-session/model/use-guest-page-session.tsentities/ambient-noise-template/model/broadcast-ambient-template.tsentities/monitor-session/model/use-monitor-session.tsentities/socket-connection/model/use-mediasoup-socket.tsstores/use-ambient-noise-alert-store.tscomponents/sections/host-video.tsxfeatures/session/ui/session-controls.tsx,session-card.tsxwidgets/monitor/ui/child-alert-presets-panel.tsx,monitor-control-sidebar.tsxapp/monitor-dashboard/[group]/[roomId]/page.tsxapps/socket/src/sfu-socket/handlers/session-handlers.ts
ambientImage가 relay된다(미반영 시 레거시 경로로 폴백되어 불일치 재현).9. 알려진 한계 · 후속
- 레거시
{{환경소음}}bare 토큰은 여전히 스냅샷 매칭 경로(이미지 동봉 없음) → 불일치 가능. 실사용은 프리셋 버튼이므로 영향 적음. - 프리셋 OFF/삭제 시 Redis 활성 스냅샷 미정리 이슈는 별도 항목(본 수정 범위 외).
- 다중활성 프리셋을 게스트까지 일관되게 반영하려면 소켓/Redis 스냅샷을 활성 배열로 확장하는 후속이 필요.