아동 알림 프리셋 트리거 race · 이미지 동기화 수정
마지막 업데이트 2026-07-29
- 증상: 수사 일지 ① — 신고 접수: 세 가지 증상
- 원인: 수사 일지 ⑤ — 3차 검거: 표시 중 진행자 재트리거 차단
- 수정·검증: 여죄 — 알려진 한계 · 후속
결론 — 범인부터 공개한다
아동 알림 프리셋(환경소음 안내)의 트리거/표시에서 발견된 세 가지 증상 — ① 첫 트리거가 가끔 안 뜸, ② 표시 중 이미지가 계속 바뀜, ③ 진행자 미러뷰와 아동 화면의 이미지가 다름 — 을 분석하고 수정했다.
근본 원인은 "진행자가 고른 이미지 스냅샷(HOST_SET, Redis 경유로 느림)"이 "트리거 메시지(host-session-message, 직행으로 빠름)"보다 늦게 도착하는 race와, 게스트가 스냅샷을 못 받은 상태에서 독립적으로 랜덤 이미지를 고르는 설계였다.
최종 해법은 진행자가 고른 단일 이미지를 트리거 메시지에 직접 동봉해, 게스트가 스냅샷 도착 타이밍·랜덤 없이 호스트와 동일한 이미지를 즉시 표시하도록 한 것이다. 함께 "표시 중 이미지 고정"과 "표시 중 진행자 재트리거 차단"도 정리했다.
수사 일지 ① — 신고 접수: 세 가지 증상
- ① 첫 트리거 미발생 — 세션 시작 직후 또는 프리셋을 바꿔 누른 직후의 첫 트리거에서 아동 화면에 알림이 안 뜨고, 두 번째부터 떴다. (AI는 정상적으로 "안 들려"라고 응답 → 시각 알림만 누락)
- ② 표시 중 이미지 교체 — 알림 15초 표시 도중 진행자가 다시 누르거나 늦은 스냅샷이 도착하면 떠 있는 이미지가 계속 바뀌었다.
- ③ 진행자/아동 이미지 불일치 — 진행자 미러뷰가 보여주는 이미지와 아동 화면 이미지가 서로 달랐다.
첫 증상에서 결정적 단서가 나왔다. 트리거가 아예 유실됐다면 AI도 "안 들려" 응답을 못 했을 텐데, AI 응답은 매번 정상이고 시각 알림만 빠졌다. 즉 트리거 메시지는 게스트에 도착하고 있었다 — 문제는 게스트가 그 트리거를 표시로 연결하는 판단 과정에 있다. 프리셋 버튼이 눌린 순간 게스트로 향하는 신호들을 추적했다.
수사 일지 ② — 범행 수법: 스냅샷 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장으로 좁혀 보낸다.
범행 수법이 밝혀졌으니 가장 급한 피해(증상 ① 첫 트리거 누락)부터 막는다. 도착 순서가 어긋나도 트리거를 버리지 않으면 된다.
수사 일지 ③ — 1차 검거: 첫 트리거 race 보정 + X 타이머 누수
트리거가 와도 스냅샷이 없어 매칭 실패하면 버리지 않고 버퍼링하고, 뒤늦게 스냅샷이 도착하는 순간 재평가해 띄운다(양방향 도착 순서 모두 커버).
- 트리거 매칭 실패 →
pendingAmbientMessageRef = {message, at}에 보관 - 스냅샷(HOST_SET) 도착 → 3초 내 버퍼 메시지가 새 sendText를 포함하면 resolved via late snapshot로 표시
덤으로 X(닫기) 버튼(clearAmbientNoiseAlert)이 15초 자동 숨김 타이머를 clearTimeout 안 하던 누수도 수정(토글 OFF·언마운트 경로와 일치).
누락(증상 ①)은 잡았지만 증상 ②·③은 남았다. 표시 도중 재클릭이나 늦은 스냅샷이 떠 있는 이미지를 갈아치우는 문제부터 잠근다.
수사 일지 ④ — 2차 검거: 표시 중 이미지 고정
알림이 한 번 뜨면 그 표시 동안 이미지를 고정한다.
- 게스트:
showAmbientNoiseAlert가 "표시를 새로 시작할 때만" 이미지를 확정하고, 표시 중엔 타이머만 연장. 표시 중 늦은 스냅샷 재적용 제거. - 진행자 미러뷰:
store.current에 클릭 식별용seq를 부여하고, 표시 시작 시점의seq로 잠금. 표시 중에는 같은 pick의 URL resolve만 반영하고 다른 클릭(새 seq)은 무시. - 아동이 X로 닫으면(표시 종료) 잠금 해제 → 다음 트리거부터 다시 변경.
이미지를 고정해도 표시 중에 진행자가 또 트리거를 누를 수 있는 한 혼란의 여지는 남는다. 입구 자체를 잠갔다.
수사 일지 ⑤ — 3차 검거: 표시 중 진행자 재트리거 차단
아동 알림이 표시 중(guestAmbientNoiseAlertVisible)이면 진행자의 프리셋 트리거를 막는다 — 포커스뷰 프리셋 버튼·카드뷰 드롭다운 모두 비활성화. 아동이 X로 닫으면 visible:false가 relay되어 자동 재활성화된다.
앞의 세 수정은 모두 race의 결과를 막는 방어선이다. 그러나 증상 ③(호스트/게스트 이미지 불일치)의 뿌리는 게스트가 스냅샷 타이밍에 따라 독립 랜덤을 고르는 설계 자체 — 게스트가 고를 필요가 없게 만들면 race도 랜덤도 사라진다.
수사 일지 ⑥ — 진범 검거(근본): 트리거에 이미지 동봉
시각 알림의 "표시"는 특정 게스트에게 타겟된 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)을 표시 → 미러뷰와 항상 일치하고, 표시 중 고정·표시 중 차단과도 충돌하지 않는다.
검거 전후 — Before / After
| 상황 | 전 | 후 |
|---|---|---|
| 세션 첫 트리거 | 스냅샷 늦으면 누락 | 버퍼 재평가로 표시 |
| 표시 중 재클릭/늦은 스냅샷 | 이미지 계속 교체 | 이미지 고정 |
| host vs guest 이미지 | 독립 랜덤 → 불일치 | 동봉 이미지로 항상 일치 |
| 표시 중 진행자 트리거 | 가능(혼란) | 차단, X 후 재허용 |
증거물 목록 — 주요 변경 파일 · 커밋
커밋
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된다(미반영 시 레거시 경로로 폴백되어 불일치 재현).여죄 — 알려진 한계 · 후속
- 레거시
{{환경소음}}bare 토큰은 여전히 스냅샷 매칭 경로(이미지 동봉 없음) → 불일치 가능. 실사용은 프리셋 버튼이므로 영향 적음. - 프리셋 OFF/삭제 시 Redis 활성 스냅샷 미정리 이슈는 별도 항목(본 수정 범위 외).
- 다중활성 프리셋을 게스트까지 일관되게 반영하려면 소켓/Redis 스냅샷을 활성 배열로 확장하는 후속이 필요.