// programs.jsx — Structured multi-week plans + workout library (Speak-style) const { useState: useStatePg } = React; const PROGRAMS = [ { id: 'posture', cat: 'mobility', title: '자세 교정 4주', sub: '굳은 목·어깨·등을 매일 조금씩', weeks: 4, perWeek: 5, mins: '8분/일', level: '입문', badge: '인기', done: 6, total: 20 }, { id: 'base', cat: 'strength', title: '체력 기초 4주', sub: '스쿼트·힌지·푸시 기본기', weeks: 4, perWeek: 4, mins: '12분/일', level: '입문', done: 0, total: 16 }, { id: 'recover', cat: 'recovery', title: '회복 루틴 2주', sub: '피로·수면을 위한 저강도 호흡', weeks: 2, perWeek: 6, mins: '6분/일', level: '누구나', done: 0, total: 12 }, { id: 'core', cat: 'cardio', title: '코어 강화 4주', sub: '앉아서도 가능한 코어 트레이닝', weeks: 4, perWeek: 4, mins: '10분/일', level: '중급', done: 0, total: 16 }, ]; const LIB = [ { id: 'l1', cat: 'mobility', title: '목·어깨 리셋', mins: '3분', level: '입문', part: '상체' }, { id: 'l2', cat: 'strength', title: '스쿼트 기본', mins: '6분', level: '입문', part: '하체' }, { id: 'l3', cat: 'cardio', title: '코어 서킷', mins: '8분', level: '중급', part: '코어' }, { id: 'l4', cat: 'recovery', title: '수면 전 호흡', mins: '5분', level: '누구나', part: '전신' }, { id: 'l5', cat: 'strength', title: '힙 힌지 연습', mins: '6분', level: '입문', part: '하체' }, { id: 'l6', cat: 'mobility', title: '흉추 열기', mins: '4분', level: '중급', part: '상체' }, ]; function Programs({ go, data, startQuest }) { const [detail, setDetail] = useStatePg(null); if (detail) return setDetail(null)} startQuest={startQuest}/>; return ; } function ProgramsHub({ go, data, openDetail, startQuest }) { const [part, setPart] = useStatePg('전체'); const programs = (data && data.programs && data.programs.length) ? data.programs : PROGRAMS; const library = (data && data.exerciseLibrary && data.exerciseLibrary.length) ? data.exerciseLibrary : LIB; const active = programs.find(p => p.id === (data.activeProgram || 'posture')) || programs[0]; const others = programs.filter(p => p.id !== active.id); const c = catColor(active.cat); const weekNum = Math.min(active.weeks, Math.floor(active.done / active.perWeek) + 1); const dayNum = active.done + 1; const parts = ['전체', '상체', '하체', '코어', '전신']; const lib = library.filter(l => part === '전체' || l.part === part); return ( {/* active program */}
진행 중
openDetail(active)} style={{ marginBottom: 22, overflow: 'hidden', cursor: 'pointer' }}>
{weekNum}주차 / {active.weeks} · {active.level}
{active.done}/{active.total} 세션
{active.title}
오늘 · Day {dayNum} — {active.title.replace(/ \d+주$/, '')} 세션
{active.mins} · 카메라 자세 체크
{/* recommended programs */}
추천 프로그램
{others.map(p => { const pc = catColor(p.cat); return ( ); })}
{/* library */}
라이브러리
{parts.map(p => ( ))}
{lib.map(l => { const lc = catColor(l.cat); return ( ); })}
); } function ProgramDetail({ program, back, startQuest }) { const c = catColor(program.cat); const [openWeek, setOpenWeek] = useStatePg(1); const sessions = (program.sessions && program.sessions.length ? program.sessions.map(s => s.title) : ['워밍업 모빌리티', '코어 액티베이션', '메인 세트 A', '메인 세트 B', '쿨다운 호흡', '회복 스트레칭']); return ( {/* hero */}
{program.weeks}주 프로그램 · {program.level}
{program.title}
{program.sub}
{[[`${program.perWeek}회`, '주간'], [program.mins.replace('/일', ''), '하루'], [`${program.total}`, '세션']].map((s, i) => (
{s[0]}
{s[1]}
))}
매일 같은 시간에 짧게 이어가면 4주 뒤 확실히 달라져요. 무리한 날은 회복 세션으로 바꿔도 돼요.
{/* weeks accordion */} {Array.from({ length: program.weeks }).map((_, wi) => { const week = wi + 1, open = openWeek === week, locked = week > 2; return (
{open && !locked && (
{sessions.slice(0, program.perWeek).map((s, si) => { const sdone = week === 1 || (week === 2 && si < 2); return (
{sdone ? : {si + 1}}
Day {si + 1} · {s}
{!sdone && }
); })}
)}
); })}
startQuest((program.sessions && program.sessions.find(s => s)) || { title: program.title + ' 세션', seconds: 60, tone: c.c, steps: ['워밍업으로 시작', '천천히 호흡', '무리하지 않기'], avaLine: '오늘 세션을 시작해요', xp: 20 })} icon={}>{program.done > 0 ? '이어서 하기' : '프로그램 시작'}
); } Object.assign(window, { Programs });