- 가게 이름 한 줄로 시작해 보세요 -
+ {buildMode && ( ++ 가게 이름 한 줄로 시작해 보세요 +
+ )} {/* ★ 입력 카드와 발행 사이트 띠를 **같은 밴드**에 겹친다(아임웹과 같은 구조). 띠를 카드 아래 따로 두면 첫 화면이 세로로 길어지고, 카드가 뜬금없이 혼자 뜬다. */} -+ 숙박 · 카페 · 음식점 · 피부과 · 성형외과 +
+ +- {post.body} -
-{openPost.body}
+ )} +{hint}
} diff --git a/solution/site/src/sections/EssentialInfoSection.tsx b/solution/site/src/sections/EssentialInfoSection.tsx index 942ddaf..5053988 100644 --- a/solution/site/src/sections/EssentialInfoSection.tsx +++ b/solution/site/src/sections/EssentialInfoSection.tsx @@ -1,7 +1,7 @@ -import {Check, Phone, X} from 'lucide-react'; +import {Check, X} from 'lucide-react'; import {selectPublishable} from '@o2o/shared'; import {useSite} from '@site/lib/site-context'; -import {bookingActionLabel, bookingLinks, essentialRows} from '@site/lib/derive'; +import {essentialRows} from '@site/lib/derive'; import {factualSummary} from '@site/seo/meta'; import type {InfoRow} from '@site/lib/derive'; import {Section} from '@site/lib/ui'; @@ -34,9 +34,8 @@ const AMENITY_VALUES = new Set(['가능', '불가', '있음', '없음']); const isAmenity = (row: InfoRow) => !row.note && AMENITY_VALUES.has(row.value.trim()); -const TWO_LINE_LABELS = new Set(['체크인 시간', '체크아웃 시간']); - const RULE_LABELS = new Set([ + '체크인 · 체크아웃', '체크인 시간', '체크아웃 시간', '취소·환불 규정', @@ -45,6 +44,19 @@ const RULE_LABELS = new Set([ '흡연 가능', '인원 추가 요금', ]); + +/** 체크인·체크아웃은 둘이 한 쌍이라 줄을 나눠 봐야 비교만 어렵다 — 한 줄로 합친다. */ +function mergeCheckInOut(rows: InfoRow[]): InfoRow[] { + const checkIn = rows.find((row) => row.key === 'check_in_time'); + const checkOut = rows.find((row) => row.key === 'check_out_time'); + if (!checkIn || !checkOut) return rows; + return rows + .map((row) => (row.key === 'check_in_time' + ? {key: row.key, label: '체크인 · 체크아웃', value: `${checkIn.value} · ${checkOut.value}`} + : row)) + .filter((row) => row.key !== 'check_out_time'); +} + export function EssentialInfoSection() { const payload = useSite(); const rows = essentialRows(payload); @@ -53,7 +65,7 @@ export function EssentialInfoSection() { const guides = payload.links.filter((link) => link.confirmed && [link.stayGuide?.policy, link.stayGuide?.service, link.stayGuide?.reservation].some((text) => text?.trim())); const structured = guides.flatMap((link) => link.stayGuide?.fields ?? []); - const mergedRows = [...rows]; + const mergedRows = mergeCheckInOut([...rows]); const seen = new Set(rows.map((row) => row.key)); for (const field of structured) { // 직접 입력한 노출값을 우선한다. 같은 항목을 출처마다 반복하지 않는다. @@ -88,7 +100,7 @@ export function EssentialInfoSection() { /* ★ 예약 섹션을 여기로 합쳤다 (2026-09-03, 사장님 지시) "이용 및 예약 안내" 와 "예약 안내" 두 섹션이 목차에 나란히 떠서 손님은 어느 쪽에서 예약하는지 몰랐다. 이용 정보를 읽고 그 자리에서 바로 누르는 게 맞다. */ - title="이용안내 및 예약" + title="이용안내" lead="방문 전 확인이 필요한 운영 규정과 시설 안내입니다." >- {payload.place.name} 예약은 아래로 받습니다. -
-{open.period}
)} diff --git a/solution/site/src/sections/GallerySection.tsx b/solution/site/src/sections/GallerySection.tsx index 8ef403d..e5a3ebc 100644 --- a/solution/site/src/sections/GallerySection.tsx +++ b/solution/site/src/sections/GallerySection.tsx @@ -1,9 +1,8 @@ -import {useCallback, useEffect, useRef, useState} from 'react'; +import {useCallback, useEffect, useState} from 'react'; import {ChevronLeft, ChevronRight, X} from 'lucide-react'; import {useSite} from '@site/lib/site-context'; import {galleryImages} from '@site/lib/derive'; -import {Section} from '@site/lib/ui'; -import {AUTOPLAY_MS, scrollRailNext, useRailAutoplay} from '@site/lib/ui/use-rail-autoplay'; +import {Carousel, CarouselSlide, Section, useScrollLock} from '@site/lib/ui'; /** * 사진 갤러리. @@ -19,45 +18,6 @@ export function GallerySection() { const variantId = setting?.variantId ?? 'photos.grid'; const [openIndex, setOpenIndex] = useState
{walkText(distanceOf(open)) ?? open.distanceText}
diff --git a/solution/site/src/sections/MobileTabBar.tsx b/solution/site/src/sections/MobileTabBar.tsx
index f5beaaf..ddff550 100644
--- a/solution/site/src/sections/MobileTabBar.tsx
+++ b/solution/site/src/sections/MobileTabBar.tsx
@@ -1,6 +1,10 @@
+import {useState} from 'react';
import {MapPin, MessageCircle, Phone} from 'lucide-react';
+import {PlaceCategory} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
-import {bookingActionLabel, bookingLinks, channelLabel} from '@site/lib/derive';
+import {bookingActionLabel, bookingLinks, channelLabel, stayBookingView} from '@site/lib/derive';
+import {Modal} from '@site/lib/ui';
+import {StayBookingDemo} from './StayBookingDemo';
/**
* 좁은 화면 하단 고정 바.
@@ -19,10 +23,13 @@ export function MobileTabBar() {
const links = bookingLinks(payload);
const booking = links[0];
const kakao = payload.links.find((link) => link.confirmed && /kakao/i.test(link.url));
+ const stayBooking = payload.place.category === PlaceCategory.LODGING ? stayBookingView(payload) : null;
+ const [bookingOpen, setBookingOpen] = useState(false);
if (!phone && !booking && !kakao) return null;
return (
+ <>
+
+ {stayBooking && (
+
@@ -166,23 +155,8 @@ export function ReviewSection() {
후기 남기기
- {open && (
-
{offer.baseRateText} @@ -176,45 +174,51 @@ export function UnitsSection() { ))} )} -
예약
- {booking.phone && ( - -예약
+ + {booking.phone && ( + +