// bodycheck.jsx — Body Camera / Body Check (optional, private, no-shame) const { useState: useStateBC, useEffect: useEffectBC } = React; function BodyCheck({ initial, exit, startQuest }) { const [step, setStep] = useStateBC(initial || 'history'); const [snapshot, setSnapshot] = useStateBC(null); const go = (s, payload) => { if (payload !== undefined) setSnapshot(payload); setStep(s); }; const map = { history: , guide: , capture: , analyzing: , result: , fallback: , privacy: , }; return map[step]; } // ── Chronicle: timeline of past body snapshots ───────────── function BCHistory({ go, exit }) { const entries = [ { date: '오늘', rel: '방금', label: '체크하기', today: true }, { date: '5월 25일', rel: '1주 전', tags: [['상체 정렬', T.teal], ['긴장도', T.violet]], note: '상체가 한결 편해졌어요' }, { date: '5월 18일', rel: '2주 전', tags: [['움직임 준비도', T.calm]], note: '꾸준함이 보이기 시작했어요' }, { date: '5월 4일', rel: '4주 전', tags: [['첫 기록', T.coral]], note: '첫 Body Check를 남겼어요' }, ]; return ( 지금까지의 변화예요. 점수가 아니라 ‘방향’만 기록해요. {/* chronicle timeline */}
{entries.map((e, i) => { const first = i === 0, last = i === entries.length - 1; return (
{!first &&
} {!last &&
}
{e.today && }
{e.today ? ( ) : (
{e.date} {e.rel}
{e.note}
{e.tags.map((t, k) => ( {t[0]} ))}
)}
); })}
); } function BCGuide({ go, exit }) { return ( go('history')}/>
{[['밝은 곳에 서주세요', 'sparkle'], ['휴대폰을 수평으로 세워주세요', 'cameraline'], ['가능하면 머리부터 발까지', 'scan'], ['편한 운동복이면 충분해요', 'check']].map((g, i) => (
{i + 1}
{g[0]}
))}
전신이 부담스럽다면 상반신 체크만 해도 괜찮아요.
go('capture')} icon={}>정면 촬영 시작 go('capture')}>상반신만 체크 오늘은 건너뛰기
); } function BCCapture({ go }) { const [count, setCount] = useStateBC(null); const [frame, setFrame] = useStateBC(null); useEffectBC(() => { if (count === null) return; if (count === 0) { const captured = window.FitbodyCamera ? window.FitbodyCamera.captureFrame('body_check') : frame; if (captured && captured.ok) setFrame(captured); const t = setTimeout(() => go('analyzing', captured || frame), 400); return () => clearTimeout(t); } const t = setTimeout(() => setCount(c => c - 1), 800); return () => clearTimeout(t); }, [count]); return (
go('guide')}/>
{count > 0 &&
{count}
}
{[['조명', cameraBrightnessLabel(frame), frame && frame.ok && frame.brightness >= 45 && frame.brightness <= 215], ['화면 안정도', cameraStabilityLabel(frame), frame && frame.ok && frame.motion <= 18], ['자세 구분', cameraContrastLabel(frame), frame && frame.ok && frame.contrast >= 18]].map((c, i) => (
{c[0]} · {c[1]}
))}
setCount(3)} disabled={count !== null}>{count !== null ? '촬영 중…' : '3초 타이머 촬영'}
); } function BCAnalyzing({ go, exit, snapshot }) { useEffectBC(() => { const frame = snapshot && snapshot.ok ? snapshot : (window.FitbodyCamera && window.FitbodyCamera.latest && window.FitbodyCamera.latest()); const report = buildBodyCheckReport(frame); if (window.FitbodyBackend) window.FitbodyBackend.recordBodyCheck(report).catch(() => {}); const t = setTimeout(() => go('result', report), 2300); return () => clearTimeout(t); }, []); return (

체크 중이에요

사진을 평가하는 게 아니라, 자세와 움직임 신호를 확인하고 있어요.

{['촬영 안정도 확인', '기기 내 프레임 신호 분석', '이번 주 Quest와 연결'].map((s, i) => (
{s}
))}
); } function BCResultRow({ label, value, tone }) { return (
{label} {value}
); } function buildBodyCheckReport(frame) { const usable = frame && frame.ok; const stable = usable && frame.motion <= 18; const lit = usable && frame.brightness >= 45 && frame.brightness <= 215; const clear = usable && frame.contrast >= 18; return { id: `bc-${Date.now().toString(36)}`, capturedAt: (frame && frame.capturedAt) || new Date().toISOString(), frame: usable ? { brightness: frame.brightness, contrast: frame.contrast, motion: frame.motion, width: frame.width, height: frame.height } : null, upperAlignment: clear && stable ? '안정적인 편' : clear ? '확인 가능' : '참고용', pelvisBalance: usable ? '오늘은 참고용' : '분석 불가', readiness: lit && stable ? '좋음' : lit ? '보통' : '조명 확인', recommendation: stable ? '흉추 열기' : '회복 호흡', }; } function BCResult({ go, exit, startQuest, snapshot }) { const report = snapshot && snapshot.recommendation ? snapshot : buildBodyCheckReport(snapshot); const quest = report.recommendation === '회복 호흡' ? { title: '회복 호흡', seconds: 60, tone: T.calm, steps: ['편하게 앉아요', '천천히 들이마셔요', '길게 내쉬어요'], avaLine: '오늘은 안정적인 호흡부터 시작해요', xp: 12 } : { title: '흉추 열기', seconds: 180, tone: T.calm, steps: ['네발 자세로 앉아요', '한 손을 머리 뒤에 대요', '천천히 가슴을 열어요'], avaLine: '깊이보다 방향이 중요해요', xp: 25 }; return (
{/* weekly change — qualitative, no scores */}
지난주와 비교 5월 18일 → 오늘
{[['상체 정렬', '조금 더 편해졌어요', T.teal, 'up'], ['움직임 준비도', '비슷해요', T.ink3, 'same'], ['긴장도', '약간 줄었어요', T.violet, 'up']].map((r, i) => (
{r[0]} {r[1]}
))}
점수가 아니라 ‘방향’만 봐요. 촬영 조건에 따라 달라질 수 있어요.
오늘은 흉추를 가볍게 열고 시작하면 움직임이 더 편해질 수 있어요. 골반은 촬영 조건 영향이 있어 판단하지 않을게요.
추천 QUEST
3분 흉추 열기
startQuest(quest)} icon={}>Quest 시작
결과 숨기기 go('privacy')}>데이터 삭제
); } function BCFallback({ go, exit }) { return ( go('result')}/>

괜찮아요

이 체크가 불편하게 느껴졌다면 지금은 결과를 보지 않아도 괜찮아요. Fitbody는 몸을 평가하지 않아요. 오늘은 컨디션과 회복 중심으로 도와드릴게요.
go('result')}>에너지 중심으로 보기 Body Check 잠시 끄기 go('privacy')}>사진·데이터 삭제하기
); } function BCPrivacy({ go, exit }) { const [s, setS] = useStateBC({ savePhoto: false, report: true, coach: true, train: false }); const t = (k) => setS(c => ({ ...c, [k]: !c[k] })); return ( go('history')}/> t('savePhoto')}/> t('report')}/> t('coach')}/>
t('train')}/>
데이터 삭제
{[['사진 삭제', 'cameraline'], ['특정 기록 삭제', 'scan'], ['모든 바디 체크 데이터 삭제', 'close'], ['내 데이터 내보내기', 'share']].map((r, i, a) => ( ))}
삭제해도 Quest, XP, Streak는 유지돼요. 바디 체크 데이터만 삭제됩니다.
); } Object.assign(window, { BodyCheck });