아동 알림 프리셋 — 생성·관리 도입 및 개별 ON/OFF 토글 변경

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

아동 알림 프리셋 환경소음 하드코딩 제거 프리셋 CRUD 단일활성 → 다중활성 개별 ON/OFF 이미지 깜빡임 수정 진행자 트리거 · 미러뷰 Redis 동기화 PPI-1014 2026-06-09

TL;DR

기존 일반 채팅 프리셋에 {{환경소음}} 토큰으로 하드코딩되어 있던 환경소음 안내 기능을, 독립적인 "아동 알림 프리셋"으로 생성·편집·관리할 수 있는 구조로 전환했다(1부). 운영자는 알림 텍스트·발송 문구·이미지(복수)를 등록하고, 진행자가 세션 중 프리셋을 선택하면 게스트 화면·진행자 미러뷰·모니터 대시보드에 이미지+배너가 표시된다.

이어서 "항상 정확히 1개만 활성"이던 단일활성 구조를 "프리셋마다 개별 ON/OFF"로 바꾸고, 진행자 화면을 전역 ON일 때만 노출하도록 하며, 알림 표시 시 기본 이미지 깜빡임을 제거했다(2부).

1부

아동 알림 프리셋 생성·관리 기능 도입

하드코딩 {{환경소음}} → 프리셋 CRUD 데이터 구조로 전환.

1.1 배경 — 하드코딩 {{환경소음}}의 한계

이전 develop 구현은 환경소음 알림이 코드에 박혀 있었다.

이 PR은 이를 데이터로 빼내어 운영자가 직접 프리셋을 만들고 관리하도록 했다.

구분BEFORE 하드코딩 토큰AFTER 프리셋 구조
안내 문구컴포넌트 상수 고정프리셋별 text 자유 입력
표시 이미지기본 이미지 1장 고정리소스 이미지 복수 등록 → 표시 시 랜덤 1장
트리거일반 프리셋 메시지의 {{환경소음}} 토큰진행자 화면의 전용 프리셋 선택 UI
편성 단위없음(전역 고정)프리셋 단위 등록·수정·삭제·활성화
문안 변경코드 수정 + 배포관리 화면에서 즉시

레거시 {{환경소음}} 토큰 트리거 경로는 호환성을 위해 남아 있다(use-guest-session-relay.ts). 신규 구조는 그 위에 프리셋 데이터 레이어와 전용 트리거 UI를 얹은 것이다.

1.2 전체 구조 — 데이터 흐름

  1. 등록 — 운영자가 관리 화면(/main/message)에서 프리셋 생성: 제목·표시 텍스트·발송 문구·이미지 N장.
  2. 선택 — 진행자가 세션 중 카드뷰 드롭다운 또는 포커스뷰 패널에서 활성 프리셋 클릭.
  3. 전파pickAndBroadcastAmbientTemplate가 이미지 N장 중 1장을 랜덤 선택, 그 단일 ID만 소켓(HOST_SET_AMBIENT_NOISE_TEMPLATE)으로 게스트에 전파.
  4. 표시 — 게스트는 이미지 ID를 presigned URL로 resolve해 중앙 이미지 + 하단 배너를 표시. 진행자 미러뷰/모니터도 store 구독으로 동일 이미지 표시.
  5. 동기화 — 활성 프리셋 스냅샷을 Redis(settings-store)에 저장, 새로 입장하는 게스트와 다른 인스턴스에 cross-instance 전파.

1.3 데이터 모델 · DynamoDB 쿼리

프리셋을 테이블 ppi-ambient-noise-template-{stage}에 저장한다. 각 템플릿 필드:

lib/db-queries.ts의 CRUD 함수: getAmbientNoiseTemplates, createAmbientNoiseTemplate, updateAmbientNoiseTemplate, deleteAmbientNoiseTemplate, 활성화 토글. 타입은 types/db/ambient-noise-template.types.ts.

1.4 REST API 엔드포인트

경로메서드역할
/api/ambient-noise-templatesGET / POST목록 조회 / 생성
/api/ambient-noise-templates/[id]GET / DELETE조회 / 삭제
/api/ambient-noise-templates/[id]/updatePATCH텍스트·이미지 수정
/api/ambient-noise-templates/[id]/activatePOST활성화 토글

이미지 등록은 /api/resources/upload-url로 발급한 업로드 URL을 사용하며, 리소스 탭의 기존 이미지를 골라 붙이는 방식이다.

1.5 프리셋 관리 화면

components/sections/ambient-noise-templates-section.tsx(신규)는 chat-preset.tsx(/main/message)에 통합된 관리 UI다.

1.6 진행자 세션 트리거

진행자가 프리셋을 선택하면 pickAndBroadcastAmbientTemplate(entities/ambient-noise-template/model/broadcast-ambient-template.ts)가 실행된다.

  1. 이미지 목록에서 1장 랜덤 선택.
  2. 선택 ID 1개만 HOST_SET_AMBIENT_NOISE_TEMPLATE로 게스트에 전파 → 모든 아동이 동일 이미지.
  3. 발송 문구를 채팅 메시지로 전송(스냅샷이 먼저 도착하도록 200ms 지연).
  4. 진행자 미러뷰 store(useAmbientNoiseAlertStore.current)에 반영.

카드뷰(features/session/ui/session-controls.tsx 드롭다운)와 포커스뷰(widgets/monitor/ui/child-alert-presets-panel.tsx 버튼 그리드)가 같은 broadcast 함수를 공유한다. 빠른 연속 클릭 시 stale 선택이 최신을 덮어쓰지 않도록 broadcastSeq 시퀀스 토큰으로 가드.

1.7 게스트 화면 표시

components/ui/ambient-noise-alert.tsx가 중앙 이미지 + 하단 배너를 렌더한다.

1.8 모니터 대시보드 미러뷰

1.9 소켓 · Redis cross-instance 동기화

1.10 1부 주요 변경 파일

영역파일
데이터 모델types/db/ambient-noise-template.types.ts, lib/db-queries.ts
REST APIapp/api/ambient-noise-templates/route.ts[id]/(route|update|activate)
관리 UIcomponents/sections/ambient-noise-templates-section.tsx, components/pages/chat-preset.tsx, entities/ambient-noise-template/model/use-ambient-noise-templates.ts
진행자 트리거features/session/ui/session-controls.tsx, entities/ambient-noise-template/model/broadcast-ambient-template.ts
게스트 표시components/ui/ambient-noise-alert.tsx, entities/guest-page-session/model/use-guest-page-session.ts, stores/use-ambient-noise-alert-store.ts
모니터shared/ui/monitor-media-display.tsx, widgets/monitor/ui/child-alert-presets-panel.tsx, monitor-control-sidebar.tsx
소켓/Redisapps/socket/src/redis/settings-store.ts, sfu-socket/handlers/(room|session)-handlers.ts, packages/shared/src/utils/constants.ts
2부

아동 알림 프리셋 개별 ON/OFF 토글 변경

1부 구조 위에서 진행된 후속 변경 — 단일활성 → 다중활성, 전역 OFF 시 화면 숨김, 이미지 깜빡임 제거.

아동 알림 프리셋이 "항상 정확히 1개만 활성"이던 단일활성 구조에서 "프리셋마다 개별 ON/OFF" 가능한 구조로 바뀌었다. 진행자는 여러 프리셋을 동시에 편성(ON)해 두고, 진행자 화면(포커스뷰/카드뷰)에서는 활성 프리셋만 클릭해 아동에게 전송한다.

함께 두 가지를 정리했다 — (1) 진행자 화면의 알림 프리셋 영역을 전역 ON 상태일 때만 노출, (2) 아동/진행자 화면에서 알림 표시 시 기본 이미지가 먼저 떴다가 선택 이미지로 바뀌는 깜빡임을 제거.

2.1 배경 — 왜 바꿨나

기존 구조는 DB에 isActive: true인 프리셋이 항상 정확히 1개가 되도록 강제했다. 첫 등록 시 자동 활성화하고, 활성 프리셋을 삭제하면 남은 첫 프리셋을 자동 활성화하며, 다른 것을 켜면 나머지를 모두 껐다.

2.2 Before / After — 핵심 동작 변화

구분BEFORE 단일 활성AFTER 개별 토글
활성 개수 항상 정확히 1개 0개 ~ N개 (자유롭게 ON/OFF)
신규 프리셋 기본값 첫 등록 시 자동 활성(ON) 항상 비활성(OFF) — 진행자가 명시적으로 ON
활성 프리셋 삭제 남은 첫 프리셋을 자동 활성화 아무것도 자동 활성화하지 않음
관리 페이지 토글 한 개를 켜면 나머지 모두 OFF 각 프리셋이 독립적으로 ON/OFF
진행자 화면 노출 전역 OFF여도 프리셋 버튼/드롭다운 노출 전역 ON일 때만 노출, OFF면 숨김
비활성 프리셋 클릭 (개념 없음 — 항상 1개만 활성) 비활성 프리셋은 회색 처리 + 클릭 불가
알림 이미지 표시 기본 이미지 먼저 → 선택 이미지로 교체(깜빡임) 선택 이미지가 있으면 resolve 전까지 미표시 → 깜빡임 없음

전역 ON/OFF는 별개 개념이다. 프리셋별 isActive(편성 여부)와, 모든 수업에 알림 기능 자체를 켜고 끄는 전역 토글(HOST_TOGGLE_AMBIENT_NOISE_ALERT)은 서로 다른 축이다. 이번 변경은 isActive를 다중화하고, 진행자 화면 노출 조건에 전역 토글을 새로 연결한 것이다.

2.3 데이터 레이어 — 단일활성 강제 로직 제거

lib/db-queries.ts에서 "항상 1개 활성" 불변식을 만들던 코드를 걷어내고, 토글 함수로 교체했다.

// AS-IS: 지정한 것만 활성, 나머지는 모두 비활성 (정확히 1개)
for (const t of templates) {
  const shouldBeActive = t.id === id;
  if (t.isActive !== shouldBeActive) { /* UpdateCommand ... */ }
}

// TO-BE: 지정한 프리셋의 isActive만 토글
const target = templates.find((t) => t.id === id);
const nextActive = !target.isActive;
// UpdateCommand SET isActive = nextActive

2.4 API · 상태 훅

// AS-IS
return prev.map((t) => ({ ...t, isActive: t.id === id }));
// TO-BE
return prev.map((t) => (t.id === id ? { ...t, isActive: !t.isActive } : t));

2.5 진행자 화면 — 노출 조건 & 비활성 클릭 차단

포커스뷰 패널 (widgets/monitor/ui/child-alert-presets-panel.tsx)

카드뷰 (features/session/ui/session-controls.tsx)

포커스뷰와 카드뷰는 pickAndBroadcastAmbientTemplate(entities/ambient-noise-template)를 공유한다. 두 표면의 동작 규칙(전역 OFF 숨김 · 활성만 클릭)을 일치시켰다.

2.6 알림 이미지 깜빡임 제거

원인은 트리거 시점에 imageUrl: undefined로 먼저 상태를 세팅하고 동시에 알림을 표시(visible)했기 때문이다. 알림 컴포넌트는 imageUrl이 없으면 기본 이미지로 폴백하므로, presigned URL이 resolve되기 전 짧은 순간 기본 이미지가 떴다가 선택 이미지로 교체됐다.

해결: hasCustomImage 플래그를 도입해 선택 이미지가 없을 때만 기본 이미지를 표시한다.

// components/ui/ambient-noise-alert.tsx
// 선택 이미지가 있으면(hasCustomImage) resolve 전까진 미표시, 없을 때만 기본 이미지
const resolvedImage = imageUrl || (hasCustomImage ? undefined : DEFAULT_IMAGE);
// resolvedImage가 없으면 중앙 <img> 자체를 렌더하지 않음
상황표시
선택 이미지 있음 (로딩 중)중앙 이미지 없음 — 기본 이미지 깜빡임 제거
선택 이미지 있음 (resolve 완료)랜덤 선택 이미지
선택 이미지 없음기본 이미지(icon_bgnoise.png)
선택 이미지 resolve 실패기본 이미지로 fallback

진행자 미러뷰에도 동일 적용

아동 화면뿐 아니라 진행자/모니터 미러뷰(monitor-media-display.tsx, host-video.tsx)도 같은 깜빡임이 있었다. 이쪽은 Zustand store(useAmbientNoiseAlertStorecurrent)를 거치므로, CurrentAmbientNoiseAlert 타입과 pickAndBroadcastAmbientTemplate, 3개 렌더 사이트 모두에 hasCustomImage를 전달해 일관되게 제거했다.

연속 트리거 race 가드

알림을 빠르게 연달아 띄울 때, 이전 템플릿의 늦은 URL resolve가 최신 템플릿의 표시 상태를 덮어쓰는 race가 있었다. 게스트 훅(ambientApplySeqRef)과 broadcast 함수(broadcastSeq) 양쪽에 시퀀스 토큰을 두어, .then/.catch가 최신 시퀀스일 때만 상태를 반영하도록 했다.

2.7 2부 주요 변경 파일

파일변경
lib/db-queries.ts신규 기본 비활성, 삭제 자동활성 제거, setActiveAmbientNoiseTemplatetoggleAmbientNoiseTemplate
app/api/ambient-noise-templates/[id]/activate/route.tstoggle 호출로 변경
entities/ambient-noise-template/model/use-ambient-noise-templates.tsactivateTemplatetoggleTemplate(해당 id만 반전)
components/sections/ambient-noise-templates-section.tsx관리 페이지 토글 UI(applyActivetoggleActive), ON 시에만 broadcast
widgets/monitor/ui/child-alert-presets-panel.tsx전역 OFF 숨김, 비활성 버튼 클릭 불가
features/session/ui/session-controls.tsx카드뷰 드롭다운 전역 OFF 숨김 + 비활성 option disabled
components/ui/ambient-noise-alert.tsxhasCustomImage 프롭, 로딩 중 기본 이미지 미표시
entities/guest-page-session/model/use-guest-page-session.ts게스트 hasCustomImage 상태 + 시퀀스 가드
stores/use-ambient-noise-alert-store.tsCurrentAmbientNoiseAlert.hasCustomImage 추가
entities/ambient-noise-template/model/broadcast-ambient-template.ts미러뷰 hasCustomImage 반영 + broadcastSeq 가드
shared/ui/monitor-media-display.tsx, components/sections/host-video.tsx, widgets/guest/guest-layout/ui/guest-layout.tsx렌더 사이트에 hasCustomImage 전달

2.8 알려진 한계 — 후속 논의 필요

다중활성으로 바뀌었지만 소켓/Redis 전파 모델은 여전히 단일 스냅샷(슬롯 1개)이다. 진행자가 클릭하는 시점에 그 프리셋을 게스트에 직접 전파(HOST_SET_AMBIENT_NOISE_TEMPLATE)하므로 클릭 기반 동작은 정상이지만, 다음은 의도적으로 손대지 않았다.

다중활성을 게스트까지 일관되게 반영하려면 소켓 페이로드/Redis를 활성 스냅샷 배열로 확장하는 후속 작업이 필요하다.

관련 문서

아동 알림 프리셋 트리거 race · 이미지 동기화 수정 →
스냅샷이 트리거보다 늦게 도착하는 race로 첫 트리거 누락·표시 중 이미지 교체·host/guest 이미지 불일치를 잡은 후속 버그 수정.