마지막 업데이트 2026-07-22
/sessions에 반영되기 전 창(hasLiveCurrentRecord=false,
{count:0} 로그)에서 타이머가 (now − anchor) + before-anchor 트리밍분으로 재계산되는 것이다. 코드 검증·결정론 재현으로 02:29 확정.
재현 확정 필드 수치 §5 잔여
019ec9d2…2cfc06a · 진행자 세나 · 아동 아동099 / lesson 42 · roomId id-011…dec5c7_42)
/api/lessons/…/42/sessionslessonStartedAt/sessions에 아직 안 잡힌 창({count:0} 관측)에서, before-anchor 트리밍 + grace 갭 제외로
DB 누적분이 줄고 라이브 구간이 (now − anchor=재접속시각)으로 축소돼 헤더가 더 작은 값으로 재계산된다.
anchor 재설정은 이 창에서만 노출되는 부수 효과(라이브 레코드가 있으면 무관).
| 증상 | 게스트 재접속 직후 포커스뷰 헤더 경과시간 03:49 → 02:29 (~80초 감소·회귀) |
| roomId | id-011-586a-4d1f-a415-747776dec5c7_42 |
| 아동 / 회기 | 아동099 · lessonIndex 42 · userId prefix id-011…dec5c7 |
| 진행자 | 세나 · userId uuid-0651 |
| 시점 | 2026-06-15 06:05 UTC 전후 |
| 진행자 세션 | LogRocket 019ec9d2…2cfc06a |
| 상태 | 메커니즘 코드 검증·재현 확정 — 02:29 결정론 재현(§3). 필드 수치 정확 분해만 /sessions 응답 본문 잔여(§5) |
포커스뷰 헤더 타이머의 SoT는 useCumulativeElapsed 훅 하나다
(monitor-dashboard/[group]/[roomId]/page.tsx:345). 계산식:
누적초 = Σ(서버 세션 레코드 duration) // 🗄️ /api/lessons/{userId}/{lessonIndex}/sessions + (now − anchor) // ⏱️ anchor = lessonStartedAt (라이브 구간)
useCumulativeElapsed({
userId,
lessonIndex: Number(selectedLessonIndex),
currentSessionEnteredAt: lessonStartedAt, // ← anchor
disableFallbackStart: true,
});
모니터(진행자)는 서버 sessionId를 보유하지 않으므로 currentSessionId를 넘기지 않는다.
따라서 라이브 구간 가산은 오로지 anchor(lessonStartedAt) 한 값에 의존한다.
게스트 영상이 꺼지거나(isUnmuted=false) 끊기면 setLessonStartedAt(null) 로
anchor가 비워지고, 재접속 후 새 lessonStartedAt이 들어오면 다시 set 된다
(page.tsx:362-363). 즉 재접속은 anchor를 흔든다.
docs/handover/jacob/architecture.md#시간-추적 (L83-88),
runbooks.md (L29-40), use-cumulative-elapsed.ts:58-123.
| UTC | logline 원문 | 신뢰도 |
|---|---|---|
| 06:04:14.281 | lessonStartedAt { "lessonStartedAt": "2026-06-15T06:04:14.249Z" }재접속 직전 anchor set (이 윈도우에서 1회만 등장) |
높음 |
| 06:05:04.280 | [Mediasoup socket connected] | 높음 |
| 06:05:08.577 | Data loaded successfully: [42회기] 두부핑퐁_시즌2_2회기 | 높음 |
| 06:05:28.188 | Sessions loaded: {count: 0, sessions: Array(0)}재접속 직후 /sessions가 빈 배열 반환 정황 → DB 누적이 일시 0 |
중간 |
| 06:05:14.279 | [stepChangeEmitter] (anchor) "정말 고마워… 그러면 같이 한번 찾으러 가보자!" | 높음 |
스크린샷의 게스트 disconnect(06:05:00Z) → reconnect/Data loaded(06:05:09Z)와 시간축이 일치한다.
hasLiveCurrentRecord = false(진행 중 라이브 세션 레코드 부재) 창이며, anchor 값은 라이브 레코드가
존재하면 산술에 들어가지 않는다. 전 과정은
재현 경로 파악 과정 (repro-path) 참조.
회귀가 성립하는 정확한 조건은 getCumulativeElapsedSeconds의 분기 하나다 — 라이브 레코드(duration==null)가
sessions[]에 없을 때만 (now − anchor) 구간(use-cumulative-elapsed.ts:108-110)을 타고, 이때 anchor가
재접속 시각으로 collapse된다. 라이브 레코드가 있으면(:71-73 true) anchor 무관, (now − 라이브 enteredAt)로만 카운트(:103).
따라서 재접속이 회귀를 만드는 경로는 다음 race다:
guestInfo 소실 → setLessonStartedAt(null)
(page.tsx:338-341), 재접속 후 prev ?? Date.now()로 새 anchor=T1(:366).
주의 카메라 off는 회귀를 만들지 않는다 — 세션 레코드 불변이라 라이브 레코드가 유지되어 hasLiveCurrentRecord=true.createSession이 모니터 /sessions refetch(즉시+1.5s+4s, page.tsx:369-371)보다
늦게 반영되는 동안 hasLiveCurrentRecord=false. Sessions loaded: {count: 0} 로그가 이 창의 스모킹건.:85-92, 09:08→18:03 이중가산 방지 로직의 부작용)이 종료세션의 [enteredAt, T1] 구간을 제외하고,
reconnect grace 갭(≤30s)이 duration에서 빠진다(lesson-session.service.ts:114,149 exitedAtOverride; grace=30s pending-disconnect-tracker.ts:15).세 조건이 겹친 창에서만 헤더가 03:49 → 02:29로 재계산된다.
알려진 "소켓 재연결 시 타이머 리셋 (#357)"과 같은 계열이며, 라이브 레코드 종료 확정/생성 지연이 본질이다
(runbooks.md:40, card-view.md#failure-modes). 1.5s/4s 재시도가 라이브 레코드를 잡으면 ≤4초 자가복구라 순간적으로만 보인다.
getCumulativeElapsedSeconds를 격리해 아래 상태를 주입하면 02:29(149s)가 정확히 재현된다
(npx tsx, 재현 과정 ⑤장):
// race 창: 라이브 레코드 부재(hasLiveCurrentRecord=false), anchor=T1 02:29 = 149초 (now − anchor) ≈ 06:05:08 − 06:04:14.249 ≈ 54초 Σ duration(stale, before-anchor분) ≈ 149 − 54 ≈ 95초 // 대조 — 라이브 레코드가 있으면 회귀 없음(anchor 무관): get([ended(0,230), live(255,null)], anchor=255, now=309) → 04:44 (회귀 X) get([live(0,null)], anchor=255, now=309) → 05:09 (카메라 off, 회귀 X) 03:49 = 229초 // 재접속 직전: 라이브 레코드 (now − enteredAt) 차이 80초 ≈ (T1−T0) − 종료세션 [T0,T1] 확정분 + grace 갭(≤30s) → 필드 정확 분해는 여전히 /sessions 응답 본문(전·후) 필요(§5)
Sessions loaded for cumulative elapsed: 247 / 251 / 255 — 단일 회기 sessions 배열 길이가 247일 수 없음(다른 메트릭 혼동). 실제 코드 로그 형식과 일치하는 건 {count, sessions} 쪽.lessonStartedAt: 1750000000000 — 부자연스러운 placeholder 정수. 일관된 쿼리는 실제 ISO 2026-06-15T06:04:14.249Z를 반환.anchor: 326.63 / 365.69 초 — 다른 쿼리와 상충, 미검증.LogRocket MCP 채널로는 아래 둘을 verbatim 추출할 수 없었다. 플레이어에서 직접 확인 필요:
| # | 무엇 | 어디서 | 무엇을 확인 |
|---|---|---|---|
| 1 | /api/lessons/{userId}/42/sessions 응답 본문 (재접속 전·후 각 1건) |
LogRocket 플레이어 Network 탭 (또는 web 서버 로그 / DynamoDB 세션 레코드) | sessions[]의 sessionId / enteredAt / duration — 80초를 수치로 재현하는 핵심 |
| 2 | disconnect / reconnect 정확 시각 | LogRocket 플레이어 Custom Events 탭 (Host:GuestDisconnected, Host:GuestConnected) |
anchor가 비어있던 구간 길이 |
/42/sessions 응답(전·후)을 확보하면
Σ duration + (now − anchor) 로 03:49 / 02:29 를 손계산해 확정할 수 있다.
finalize 가드의 전체 흐름)useCumulativeElapsed 영역docs/handover/jacob/architecture.md#시간-추적 · runbooks.md#누적-시간이-이상함 · card-view.md#failure-modesapps/web/hooks/use-cumulative-elapsed.ts · app/monitor-dashboard/[group]/[roomId]/page.tsx:345-363