/** * 추천 일정 — 며칠 묵느냐로 갈린다. * * ★ 마크업은 목업(/s/stay 의 `
`)에서 그대로 옮겼다. * 기간 탭 → 일정마다 날짜 카드를 가로로 밀고, 카드 안에 지도와 타임라인이 함께 있다. * ★ 시각은 **계산한다.** 출발 시각과 머무는 분을 더해 나가므로, 출발을 당기면 하루가 * 통째로 밀린다. 모델이 시각을 적어 내면 합이 안 맞는 표가 나온다. * ★ 전 일정을 HTML 에 편다. 탭은 화면에서만 접는다 — 접힌 쪽이 HTML 에 없으면 * 검색·AI 가 나머지 일정을 못 읽는다. */ import {useState} from 'react'; import type {ItineraryDay, ItineraryItem, ItineraryStop} from '@o2o/shared'; import {useSite} from '@/lib/site-context'; import {sectionItems, sectionName} from '@/lib/derive'; import {ITEM_ACCENT, ITEM_BORDER, ITEM_CARD, ItemSection, SourceLine, tally} from './common'; import {TripMap, type TripPoint} from './TripMap'; /** 'HH:MM' → 분. 형식이 아니면 null(시각을 지어내지 않는다). */ function toMinutes(time?: string): number | null { const m = /^(\d{1,2}):(\d{2})$/.exec((time ?? '').trim()); if (!m) return null; return Number(m[1]) * 60 + Number(m[2]); } /** '3시간 40분' — 첫 칸 시작부터 마지막 칸 끝까지. */ function spanLabel(rows: {from?: string; to?: string}[]): string | null { const from = toMinutes(rows[0]?.from); const to = toMinutes(rows[rows.length - 1]?.to); if (from === null || to === null || to <= from) return null; const total = to - from; const h = Math.floor(total / 60); const m = total % 60; return [h ? `${h}시간` : '', m ? `${m}분` : ''].filter(Boolean).join(' '); } function toClock(minutes: number): string { const h = Math.floor(minutes / 60) % 24; return `${String(h).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`; } /** 정거장마다 시작·끝 시각을 계산한다. 출발 시각이 없으면 시각을 아예 안 그린다. */ function schedule(day: ItineraryDay): {stop: ItineraryStop; from?: string; to?: string}[] { let cursor = toMinutes(day.startTime); return (day.stops ?? []).map((stop) => { if (cursor === null) return {stop}; const from = cursor; const to = from + (stop.minutes ?? 0); cursor = to; return {stop, from: toClock(from), to: toClock(to)}; }); } export function ItinerarySection() { const payload = useSite(); const parsed = sectionItems(payload, 'itinerary'); const durations = Array.from( new Set(parsed.items.map((item) => item.duration?.trim()).filter(Boolean) as string[]), ); const [active, setActive] = useState(durations[0] ?? ''); if (parsed.items.length === 0) return null; return ( {durations.length > 1 && (
{durations.map((duration) => ( ))}
)}
{parsed.items.map((plan, planIndex) => ( ))}
); }