From c43c4f36209b3db32606b2d513f6ecbe826a8e86 Mon Sep 17 00:00:00 2001 From: hbyang Date: Wed, 9 Sep 2026 10:37:17 +0900 Subject: [PATCH] =?UTF-8?q?[fix]=20solution/frontend:=20=EB=B9=8C=EB=8D=94?= =?UTF-8?q?=20=EC=BA=94=EB=B2=84=EC=8A=A4=EC=9D=98=20=EC=98=88=EC=95=BD=20?= =?UTF-8?q?=EC=84=B9=EC=85=98=EC=9D=84=20=EB=B0=9C=ED=96=89=EB=B3=B8?= =?UTF-8?q?=EA=B3=BC=20=EB=A7=9E=EC=B6=98=EB=8B=A4=20=E2=80=94=20=EC=8B=A4?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=98=88=EC=95=BD=20=EB=AC=B8=EA=B5=AC=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 편집 화면이 "네이버 실시간 온라인 예약 / 캘린더에서 바로 확정 예약하실 수 있습니다" 를 그리고 있었다. 우리는 실시간 재고를 갖지 않고(PRODUCT.md 6절), 발행본은 날짜·시간을 고르는 화면이다 — 사장님이 에디터에서 본 것과 발행된 사이트가 서로 다른 물건이었다. 에디터가 보여주는 것이 곧 발행될 것이어야 한다. - booking/BookingCard: 발행본 구성(날짜 칩·도착 시간·인원·예약 요청·전화 창구)의 미리보기로 교체. 캔버스 클릭은 섹션 선택이라 상태를 두지 않고 첫 칸 선택 모습으로 고정. 시간 칸은 발행본과 같은 규칙 — 체크인 fact 가 있을 때만 그린다 - booking/BookingBanner: "실시간 캘린더에서 남은 날짜" → "날짜와 시간을 고르고 예약 창구로" - rooms/RoomCard: "실시간 예약 신청" → "예약 안내 보기" - hero/HeroEditorial: "실시간 예약" → "예약 안내" - api/generated linkChannel: NAVER_BOOKING=7 추가. ★ npm run orval 을 그대로 돌리면 141파일 6,400줄이 바뀌는데 전부 따옴표·줄바꿈 포매팅 드리프트다 — 생성물을 되돌리고 스펙 변경분 한 줄만 남겼다 tsc·eslint 통과(frontend·site), vitest 51 passed. solution-site 재빌드 후 번들에서 옛 문구 0건 확인. --- docs/DEVLOG.md | 14 ++ .../src/api/generated/model/linkChannel.ts | 1 + .../model/placeSearchItemNaverPlaceUrl.ts | 8 + .../canvas/variants/booking/BookingBanner.tsx | 8 +- .../canvas/variants/booking/BookingCard.tsx | 150 +++++++++++++++--- .../canvas/variants/hero/HeroEditorial.tsx | 2 +- .../canvas/variants/rooms/RoomCard.tsx | 4 +- 7 files changed, 164 insertions(+), 23 deletions(-) create mode 100644 solution/frontend/src/api/generated/model/placeSearchItemNaverPlaceUrl.ts diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index 27b6462..3f2d834 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -36,6 +36,20 @@ **연동을 붙일 자리** — `ConfirmPanel` 한 곳이다. 실시간 재고·접수가 생기면 그 함수만 바뀐다. +**빌더 캔버스도 같이 맞췄다** — 사장님 편집 화면은 여전히 "네이버 실시간 온라인 예약 / +캘린더에서 바로 확정 예약" 을 그리고 있었다. 우리는 실시간 예약을 하지 않는데다, +**에디터에서 본 것과 발행된 사이트가 서로 다른 물건**이었다. +- `booking/BookingCard`: 발행본 구성(날짜 칩 · 도착 시간 · 인원 · 예약 요청 · 전화 창구)의 + 미리보기로 갈아엎었다. 캔버스의 클릭은 "이 섹션을 고른다" 는 뜻이라 상태를 두지 않고 + 첫 칸이 골라진 모습으로 고정한다. 시간 칸은 발행본과 같은 규칙으로 **체크인 fact 가 있을 + 때만** 그린다 +- `booking/BookingBanner` "실시간 캘린더" → "날짜와 시간을 고르고 예약 창구로 이어집니다", + `rooms/RoomCard` "실시간 예약 신청" → "예약 안내 보기", `hero/HeroEditorial` "실시간 예약" + → "예약 안내" +- `LinkChannel.NAVER_BOOKING` 을 orval 생성물에 반영. ★ `npm run orval` 을 그대로 돌리면 + **141파일 6,400줄**이 바뀐다 — 전부 따옴표·줄바꿈 포매팅 드리프트고 스펙 변경은 enum + 한 줄뿐이다. 그래서 생성물을 되돌리고 그 한 줄만 남겼다(실측 2026-09-09) + **검증** — `tsc·eslint` 통과, `vitest` 51 passed(신규 4건: 날짜가 HTML 에 안 박히는지 · JSON-LD 무영향 · llms.txt 무영향 · 객실 0개면 안 그림). 실제 발행본 재굽기 후 `/s/` 에서 데모 껍데기와 안내 문구 확인. diff --git a/solution/frontend/src/api/generated/model/linkChannel.ts b/solution/frontend/src/api/generated/model/linkChannel.ts index 58cf6b4..6ae5e7d 100644 --- a/solution/frontend/src/api/generated/model/linkChannel.ts +++ b/solution/frontend/src/api/generated/model/linkChannel.ts @@ -19,5 +19,6 @@ export const LinkChannel = { INSTAGRAM: 4, OFFICIAL_SITE: 5, BLOG: 6, + NAVER_BOOKING: 7, ETC: 99, } as const; diff --git a/solution/frontend/src/api/generated/model/placeSearchItemNaverPlaceUrl.ts b/solution/frontend/src/api/generated/model/placeSearchItemNaverPlaceUrl.ts new file mode 100644 index 0000000..8e7a6d9 --- /dev/null +++ b/solution/frontend/src/api/generated/model/placeSearchItemNaverPlaceUrl.ts @@ -0,0 +1,8 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ + +export type PlaceSearchItemNaverPlaceUrl = string | null; diff --git a/solution/frontend/src/features/builder/canvas/variants/booking/BookingBanner.tsx b/solution/frontend/src/features/builder/canvas/variants/booking/BookingBanner.tsx index 1db4c76..a288c1c 100644 --- a/solution/frontend/src/features/builder/canvas/variants/booking/BookingBanner.tsx +++ b/solution/frontend/src/features/builder/canvas/variants/booking/BookingBanner.tsx @@ -1,6 +1,10 @@ /** * 예약 · 띠배너 — 브랜드 색으로 화면을 가로지르는 강한 전환 유도. * 예약이 유일한 목표인 사이트에 맞는다(대신 자주 쓰면 광고처럼 보인다). + * + * ★ 문구에서 "실시간 캘린더" 를 걷어냈다(2026-09-09). 우리는 실시간 재고를 갖지 않는다 + * (PRODUCT.md 6절). 발행본이 하는 일 그대로 — 날짜·시간을 고르고 예약 창구로 잇는다 — + * 을 말한다. 에디터가 보여주는 것이 곧 발행될 것이어야 한다. */ import {Calendar} from 'lucide-react'; import {CtaLink, SectionFrame} from '../../primitives'; @@ -19,14 +23,14 @@ export function BookingBanner(props: SectionRenderProps) { {section.name}

- {section.body || section.description || `${storeName} · 실시간 캘린더에서 남은 날짜를 확인할 수 있습니다`} + {section.body || section.description || `${storeName} · 날짜와 시간을 고르고 예약 창구로 이어집니다`}

- 예약 캘린더 + 날짜 · 시간 고르기
diff --git a/solution/frontend/src/features/builder/canvas/variants/booking/BookingCard.tsx b/solution/frontend/src/features/builder/canvas/variants/booking/BookingCard.tsx index 1dd1ee3..e82fc5a 100644 --- a/solution/frontend/src/features/builder/canvas/variants/booking/BookingCard.tsx +++ b/solution/frontend/src/features/builder/canvas/variants/booking/BookingCard.tsx @@ -1,34 +1,146 @@ /** - * 예약 · 카드 — 가운데 놓인 조용한 안내 카드. 다른 섹션의 흐름을 끊지 않는다. + * 예약 · 카드 — **발행본의 예약 안내를 그대로 미리 보여준다.** + * + * ★ 왜 갈아엎었나 (2026-09-09) + * 이 카드는 "네이버 실시간 온라인 예약 / 캘린더에서 바로 확정 예약" 을 그리고 있었다. + * 우리는 실시간 예약을 하지 않는다(PRODUCT.md 6절 — 재고도 결제도 갖지 않는다). + * 게다가 발행본은 날짜·시간을 고르는 화면인데 캔버스만 다른 문구를 보여줘서, + * 사장님이 편집 화면에서 본 것과 발행된 사이트가 서로 다른 물건이었다. + * **에디터가 보여주는 것이 곧 발행될 것**이어야 한다. + * + * ★ 여기서는 **누르지 않는다.** 캔버스의 클릭은 "이 섹션을 고른다" 는 뜻이라 + * 상태를 가진 위젯을 넣으면 선택과 싸운다. 첫 칸이 골라진 모습으로 고정해 두고, + * 실제 동작은 발행본(`solution/site` StayBookingDemo)이 한다. + * + * ★ 시간 칸은 **체크인 fact 가 있을 때만** 그린다. 발행본과 같은 규칙이다 — + * 체크인이 16:00 인데 미리보기가 14:00 을 보여주면 사장님은 그 선택지가 생긴 줄 안다. */ -import {Calendar} from 'lucide-react'; -import {CtaLink, SectionBody, SectionFrame, SectionHeading} from '../../primitives'; +import {CalendarDays, Clock, Minus, Phone, Plus} from 'lucide-react'; +import type {InfoField} from '@o2o/shared'; +import {SectionBody, SectionFrame, SectionHeading} from '../../primitives'; import type {SectionRenderProps} from '../../types'; -import {bookingHref} from '../common'; + +const WEEKDAY_LABEL = ['일', '월', '화', '수', '목', '금', '토'] as const; + +/** 오늘부터 7칸. 캔버스는 브라우저에서만 도므로 날짜를 그대로 그려도 된다 + * (발행본은 정적 HTML 이라 날짜를 굽지 않는다 — StayBookingDemo 머리주석). */ +function nextDays(count: number) { + const today = new Date(); + return Array.from({length: count}, (_, index) => { + const date = new Date(today.getFullYear(), today.getMonth(), today.getDate() + index); + return {day: date.getDate(), weekday: WEEKDAY_LABEL[date.getDay()]}; + }); +} + +/** 확인된 값만 읽는다 — 캔버스도 발행본과 같은 규칙이다. */ +function verified(fields: InfoField[], id: string): string | undefined { + const field = fields.find((f) => f.id === id); + if (!field || field.requiresVerification || !field.value?.trim()) return undefined; + return field.value.trim(); +} + +function arrivalSlots(checkIn?: string): string[] { + const match = /(\d{1,2})\s*:\s*(\d{2})/.exec(checkIn ?? ''); + if (!match) return []; + const start = Number(match[1]); + if (!Number.isFinite(start)) return []; + return Array.from({length: 4}, (_, i) => start + i) + .filter((hour) => hour <= 23) + .map((hour) => `${String(hour).padStart(2, '0')}:${match[2]}`); +} export function BookingCard(props: SectionRenderProps) { - const {section, isSelected, onSelect, industryId, storeName, template} = props; + const {section, isSelected, onSelect, template, infoFields} = props; + const days = nextDays(7); + const checkIn = verified(infoFields, 'check_in_time'); + const slots = arrivalSlots(checkIn); + const phone = verified(infoFields, 'phone'); + const accent = template.colors.primary; return ( -
-

- 네이버 실시간 온라인 예약 -

-

- {section.body || '원하시는 날짜와 객실을 선택하여 캘린더에서 바로 확정 예약하실 수 있습니다.'} +

+

+ + 날짜 · 시간 선택

- - - 네이버 예약 캘린더 열기 - + +
+
+

날짜

+
    + {days.map((day, index) => ( +
  • + {day.weekday} + {day.day} +
  • + ))} +
+
+ + {slots.length > 0 && ( +
+

+ + 도착 예정 시간 (체크인 {checkIn} 이후) +

+
    + {slots.map((time, index) => ( +
  • + {time} +
  • + ))} +
+
+ )} + +
+

인원

+
+ + + + 2명 + + + +
+
+ +
+ 예약 요청 확인하기 +
+ + {/* ★ 실제 예약이 되는 창구. 발행본과 같은 순서다 — 데모 다음에 진짜 창구가 온다. */} + {phone && ( +

+ + 전화 예약 {phone} +

+ )} +
diff --git a/solution/frontend/src/features/builder/canvas/variants/hero/HeroEditorial.tsx b/solution/frontend/src/features/builder/canvas/variants/hero/HeroEditorial.tsx index 6c16470..868cc5e 100644 --- a/solution/frontend/src/features/builder/canvas/variants/hero/HeroEditorial.tsx +++ b/solution/frontend/src/features/builder/canvas/variants/hero/HeroEditorial.tsx @@ -55,7 +55,7 @@ export function HeroEditorial(props: SectionRenderProps) { colors={template.colors} > - 실시간 예약 + 예약 안내
diff --git a/solution/frontend/src/features/builder/canvas/variants/rooms/RoomCard.tsx b/solution/frontend/src/features/builder/canvas/variants/rooms/RoomCard.tsx index c983f54..88e50cb 100644 --- a/solution/frontend/src/features/builder/canvas/variants/rooms/RoomCard.tsx +++ b/solution/frontend/src/features/builder/canvas/variants/rooms/RoomCard.tsx @@ -129,7 +129,9 @@ export function RoomCard({ const action = (
- 실시간 예약 신청 + {/* ★ "실시간 예약 신청" 이었다(2026-09-09 수정). 실시간 재고를 갖지 않으므로 + 누르면 예약이 확정되는 것처럼 읽히면 안 된다 — 발행본과 같은 말로 맞춘다. */} + 예약 안내 보기