/** * 반나절 산책 — 승차권 캐러셀. * * 표 한 장이 정거장 하나다. 절취선·펀치 구멍·완주 도장은 스탬프투어라는 실제 형식을 그대로 옮긴 것. * ★ 링크는 만들지 않는다. searchQuery 만 보여준다 — 지어낸 주소를 링크하지 않는 이 레포의 규약. */ import {SectionBody, SectionFrame} from '../../primitives'; import type {SectionRenderProps} from '../../types'; import {parseSectionData, type CourseItem, type CourseStop} from '@o2o/shared'; import { CarouselNav, ITEM_ACCENT, ITEM_BODY, ITEM_BORDER, ITEM_CARD, ITEM_HEADING, ITEM_INK, ITEM_SURFACE, ParseError, PasteHint, SourceLine, useCarousel, } from '../items/common'; import '../items/items.css'; function Ticket({ stop, courseName, index, isLast, }: { stop: CourseStop; courseName: string; index: number; isLast: boolean; }) { const no = String(stop.order ?? index + 1).padStart(2, '0'); return (
{courseName} NO.{no}

{no}

{stop.name}

{stop.note && (

{stop.note}

)}
{stop.minutes ? `도보 ${stop.minutes}분` : '이동 시간 미정'} {stop.searchQuery && 지도 검색 · {stop.searchQuery}}
{/* 펀치 구멍 — 표를 표로 만드는 자리 */} {isLast && ( 완주
도장
)}
); } function CourseRow({course}: {course: CourseItem}) { const {ref, scrollBy} = useCarousel(); const stops = course.stops ?? []; return (

{course.name}

{[course.duration, course.startsFrom ? `${course.startsFrom} 출발` : undefined, `${stops.length}곳`] .filter(Boolean) .join(' · ')}
{stops.length > 2 && ( scrollBy(-1)} onNext={() => scrollBy(1)} label={course.name} /> )}
{stops.length === 0 ? (

정거장이 아직 없습니다. JSON 의 stops 배열을 채워 주세요.

) : (
{stops.map((stop, index) => ( ))}
)}
); } export function CourseTickets(props: SectionRenderProps) { const {section, isSelected, onSelect} = props; const parsed = parseSectionData(section.type, section.data); return (

{parsed.title || section.name}

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

{parsed.subtitle || section.description}

)}
{parsed.error ? ( ) : parsed.items.length === 0 ? ( ) : (
{parsed.items.map((course, index) => ( ))}
)}
); }