/** * 추천 일정 — 캔버스. 승차권 한 장이 정거장 하나다. * * ★ 셋을 합친 자리다(반나절 산책 · 여행 스케줄 · 계절별 추천 하루). 축이 계절이 아니라 **기간**이다. * ★ 순위를 매기지 않는다 — 어느 일정이 1위인지는 우리가 정할 일이 아니다. * ★ 시각은 사장님이 적는 게 아니라 **계산한다**. 출발 시각 + 이동 + 머무는 시간. * 출발을 당기면 하루가 통째로 밀린다. */ import { currentSeasons, inSeason, itineraryDays, itineraryDurations, parseSectionData, type ItineraryItem, type PlannedStop, } from '@o2o/shared'; import {SectionBody, SectionFrame, Rail} from '../../primitives'; import type {SectionRenderProps} from '../../types'; import { ITEM_ACCENT, ITEM_BODY, ITEM_BORDER, ITEM_CARD, ITEM_HEADING, ParseError, PasteHint, SourceLine, } from '../items/common'; import '../items/items.css'; export function ItineraryTickets(props: SectionRenderProps) { const {section, isSelected, onSelect} = props; const parsed = parseSectionData(section.type, section.data); const live = currentSeasons(); const durations = itineraryDurations(parsed.items); const groups: (string | undefined)[] = durations.length > 0 ? [...durations, ...(parsed.items.some((i) => !i.duration?.trim()) ? [undefined] : [])] : [undefined]; return (

{parsed.title || section.name}

{(parsed.subtitle || section.description) && (

{parsed.subtitle || section.description}

)}
{parsed.error ? ( ) : parsed.items.length === 0 ? ( ) : (
{groups.map((duration) => { const items = parsed.items.filter((item) => duration === undefined ? !item.duration?.trim() : item.duration?.trim() === duration, ); if (items.length === 0) return null; return (
{duration && (

{duration}

)} {items.map((item, index) => (

{item.name}

{[item.audience, item.season].filter(Boolean).join(' · ')} {/* 계절을 적으면 손님 화면에서는 그 계절에만 나간다 — 사장님이 알아야 한다. */} {item.season && ( {inSeason(item.season, live) ? '· 지금 나갑니다' : `· 손님 화면에는 ${item.season} 에만 나갑니다`} )}
{item.why && (

{item.why}

)} {itineraryDays(item).map((entry, dayIndex) => (
{entry.label && (

{entry.label} {entry.day.from}–{entry.day.to}

)} {entry.day.stops.map((planned, stopIndex) => ( ))} {entry.day.dropped > 0 && (

밤 9시를 넘기는 {entry.day.dropped}곳은 화면에서 뺐습니다.

)}
))}
))}
); })}
)}
); } /** 승차권 한 장. 마지막 장에는 완주 도장(아티팩트의 스탬프투어 형식). */ function Ticket({planned, no, isLast}: {planned: PlannedStop; no: number; isLast: boolean}) { const {stop, time, until, move} = planned; return (
NO.{String(no).padStart(2, '0')} {time} –{until}
{stop.name}
{stop.note &&

{stop.note}

} {move > 0 &&

↓ 앞 칸에서 {move}분 이동

}
{isLast && ( 완주 )}
); }