import {useEffect, useState, type ReactNode} from 'react'; import {PlaceCategory, selectPublishable} from '@o2o/shared'; import {useSite} from '@site/lib/site-context'; import { bookingActionLabel, bookingLinks, channelLabel, isSectionEnabled, publicLinks, stayBookingView, } from '@site/lib/derive'; import {isoDate, naverDirectionsUrl} from '@site/lib/format'; import {SongPlayer} from '@site/sections/SongPlayer'; import {BookingForm} from '../paper/BookingForm'; import {BOOK_EVENT} from '../paper/Rooms'; import {Sheet} from '../paper/Sheet'; import {MapIcon, PhoneIcon, TalkIcon} from './icons'; import '../kit/kit.css'; import './cinema.css'; const HOUSE_WORD: Record = { [PlaceCategory.LODGING]: '펜션', [PlaceCategory.CAFE]: '카페', [PlaceCategory.RESTAURANT]: '가게', [PlaceCategory.CLINIC]: '병원', }; type PageId = 'home' | 'area' | 'stay' | 'story'; const PAGE_HEAD: Record, {label: string; title: string; note: string}> = { area: { label: 'AREA', title: '지역 소개', note: '대문에서 걸어 닿는 자리와, 이 도시가 남긴 이야기입니다.', }, stay: { label: 'STAY', title: '이용안내 · 예약', note: '상세 안내와 요금, 예약하는 방법입니다.', }, story: { label: 'STORY', title: '이야기', note: '사장님이 남기는 글과 다녀가신 분들의 후기, 그리고 엽서 한 장입니다.', }, }; function pageOf(hash: string): PageId { const id = hash.replace(/^#\/?/, ''); return id === 'area' || id === 'stay' || id === 'story' ? id : 'home'; } function usePage(): PageId { const [page, setPage] = useState('home'); useEffect(() => { const sync = () => { const {hash} = window.location; if (hash && !hash.startsWith('#/')) return; setPage(pageOf(hash)); window.scrollTo({top: 0, behavior: 'instant'}); }; sync(); window.addEventListener('hashchange', sync); const again = (event: MouseEvent) => { const link = event.target instanceof Element ? event.target.closest('a[href^="#/"]') : null; const href = link?.getAttribute('href'); if (href && pageOf(href) === pageOf(window.location.hash)) window.scrollTo({top: 0, behavior: 'instant'}); }; document.addEventListener('click', again); return () => { window.removeEventListener('hashchange', sync); document.removeEventListener('click', again); }; }, []); return page; } function useScrolled(): boolean { const [scrolled, setScrolled] = useState(false); useEffect(() => { const sync = () => setScrolled(window.scrollY > 40); sync(); window.addEventListener('scroll', sync, {passive: true}); return () => window.removeEventListener('scroll', sync); }, []); return scrolled; } export function Frame({children}: {children: ReactNode}) { const payload = useSite(); const {place, site} = payload; const page = usePage(); const scrolled = useScrolled(); const [menuOpen, setMenuOpen] = useState(false); const [bookingOpen, setBookingOpen] = useState(false); useEffect(() => { const open = () => setBookingOpen(true); window.addEventListener(BOOK_EVENT, open); return () => window.removeEventListener(BOOK_EVENT, open); }, []); useEffect(() => setMenuOpen(false), [page]); const tabs: {label: string; page: PageId}[] = [ {label: `${HOUSE_WORD[place.category] ?? '가게'} 소개`, page: 'home'}, ]; if (isSectionEnabled(payload, 'local')) tabs.push({label: '지역 소개', page: 'area'}); tabs.push({label: '이용안내 · 예약', page: 'stay'}); tabs.push({label: '이야기', page: 'story'}); const booking = bookingLinks(payload)[0]; const stayBooking = place.category === PlaceCategory.LODGING ? stayBookingView(payload) : null; const kakao = payload.links.find((link) => link.confirmed && /kakao/i.test(link.url)); const address = place.roadAddress ?? place.address; const hasSongs = (payload.songs ?? []).some((song) => song.audioUrl); const facts = selectPublishable(payload.facts); const fact = (key: string) => facts.find((f) => f.key === key)?.value ?? undefined; const hours = [ fact('check_in_time') && `체크인 ${fact('check_in_time')}`, fact('check_out_time') && `체크아웃 ${fact('check_out_time')}`, fact('smoking') === 'false' && '전 구역 금연', ].filter(Boolean); const channels = publicLinks(payload); const instagram = channels.find((link) => /instagram\.com/i.test(link.url)); const areaPills = ( [ ['주변 명소', '#walk', payload.local.attractions.length > 0], ['주변 맛집', '#eat', payload.local.restaurants.length > 0], ['엽서', '#/story', true], ['축제', '#festival', isSectionEnabled(payload, 'festival') && payload.local.festivals.length > 0], ['일정', '#itinerary', isSectionEnabled(payload, 'itinerary')], ['이야기', '#story', isSectionEnabled(payload, 'story')], ['날씨', '#/', isSectionEnabled(payload, 'weather')], ] as [string, string, boolean][] ).filter(([, , on]) => on); const barItems = [address ? 'map' : null, kakao ? 'talk' : null, place.phone ? 'tel' : null].filter(Boolean); const hasCta = Boolean(stayBooking || booking); const showBar = barItems.length > 0 || hasCta; const columns = [...barItems.map(() => '1fr'), ...(hasCta ? ['1.7fr'] : [])].join(' '); const cta = stayBooking ? ( ) : ( booking && ( {bookingActionLabel(booking)} ) ); return (
{place.name}
{place.phone && ( {place.phone} )} {cta}
{(Object.keys(PAGE_HEAD) as Exclude[]).map((id) => (

{PAGE_HEAD[id].label}

{id === 'area' ? (place.addressLocality?.replace(/(시|군)$/, '') ?? PAGE_HEAD.area.title) : PAGE_HEAD[id].title}

{PAGE_HEAD[id].note}

{id === 'area' && areaPills.length > 0 && (
{areaPills.map(([label, href]) => ( {label} ))}
)}
))} {children} {channels.length > 0 && (

CHANNELS

공식 채널

{channels.map((link) => ( {channelLabel(link)} 열기 ↗ ))}
)}

{place.name}

{address &&

{address}

} {hours.length > 0 &&

{hours.join(' · ')}

} {(place.phone || instagram) && (

{place.phone && {place.phone}} {place.phone && instagram && ' · '} {instagram && ( instagram )}

)} {place.legal?.businessRegistrationNumber && (

{place.legal.representative && `대표 ${place.legal.representative} · `} 사업자등록번호 {place.legal.businessRegistrationNumber} {place.legal.mailOrderNumber && ` · 통신판매업신고 ${place.legal.mailOrderNumber}`}

)}

작성·운영 {place.name} · 최종 업데이트{' '}
AI O2O 의 Web4Ai로 만든 사이트입니다.

{hasSongs && (
)} {showBar && ( )} {stayBooking && ( setBookingOpen(false)} label="예약 요청" plain>

예약 요청하기

)}
); }