diff --git a/.env.example b/.env.example index 0caa193..045f3f4 100644 --- a/.env.example +++ b/.env.example @@ -36,6 +36,13 @@ KAKAO_REST_API_KEY= GEMINI_API_KEY= # 디코딩된 키(인코딩 키는 이중 인코딩된다) TOUR_API_KEY= +# 발행할 때 이 숙소의 노래를 한 곡 만든다(가사 Gemini → 작곡 Suno). +# 비우면 그 단계만 건너뛴다 — 발행은 그대로 된다. +# ★ 콜백은 쓰지 않고 폴링한다(우리 서버는 Suno 가 닿을 수 있는 주소가 아니다). +# 그래도 API 가 필수로 요구하는 필드라 값을 채워 보낸다. +SUNO_API_KEY= +SUNO_CALLBACK_URL=https://example.com/api/suno/callback + # 구글 로그인. 비우면 구글 로그인만 꺼진다(서버는 뜨고, 화면에 버튼도 안 뜬다). # Google Cloud Console > API 및 서비스 > 사용자 인증 정보 > OAuth 2.0 클라이언트 ID(웹 애플리케이션) diff --git a/docker-compose.yml b/docker-compose.yml index dc314d8..64e3ba9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,6 +49,9 @@ services: SCHEDULER_ENABLED: "1" volumes: - ./solution/site/payloads:/app/out/payloads + # 노래 파일. payload 와 나란히 둔다 — 백엔드가 mp3 를 여기 떨구면 프리렌더가 + # 사이트 디렉토리로 복사한다(services/song_service · site/scripts/prerender.ts). + - ./solution/site/songs:/app/out/songs # ★ 스키마 마이그레이션 SQL. 이미지에 굽지 않고 마운트한다 — 파일이 자주 늘고, # 이미 세운 DB 를 따라오게 하는 것이 목적이라 코드 배포와 별개로 돌 수 있어야 한다. - ./postgres-init:/app/postgres-init:ro @@ -81,6 +84,9 @@ services: disable: true volumes: - ./solution/site/payloads:/app/out/payloads + # 노래 파일. payload 와 나란히 둔다 — 백엔드가 mp3 를 여기 떨구면 프리렌더가 + # 사이트 디렉토리로 복사한다(services/song_service · site/scripts/prerender.ts). + - ./solution/site/songs:/app/out/songs - site-out:/app/out/sites:ro extra_hosts: - "host.docker.internal:host-gateway" @@ -119,6 +125,9 @@ services: retries: 3 volumes: - ./solution/site/payloads:/app/out/payloads + # 노래 파일. payload 와 나란히 둔다 — 백엔드가 mp3 를 여기 떨구면 프리렌더가 + # 사이트 디렉토리로 복사한다(services/song_service · site/scripts/prerender.ts). + - ./solution/site/songs:/app/out/songs ports: # ★ 내부망에만 연다. 0.0.0.0 으로 열면 API 를 가른 의미가 없다. - "${ADMIN_API_BIND:-127.0.0.1}:${ADMIN_API_PORT_PUBLIC:-9801}:9801" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2200a66..46ef371 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -13,8 +13,14 @@ ``` backend (Python) ──쓴다──▶ out/payloads/.json ◀──읽는다── prerender (Node) out/payloads/.status/.json ──보고──▶ + ──쓴다──▶ out/songs/.mp3 ◀──복사── ``` +★ 노래(mp3)도 **같은 약속**을 쓴다(2026-09-11). 백엔드는 발행물 디렉토리를 모른 채 파일을 +`out/songs/` 에 떨구고, 굽는 쪽인 프리렌더가 `out/s//` 로 복사한다. 백엔드가 발행물 +디렉토리에 직접 쓰기 시작하면 이 경계가 무너진다 — 그때부터 두 쪽이 out/ 의 모양을 함께 +알아야 한다. + 이 경계가 있어서 렌더링을 통째로 갈아엎어도 백엔드는 안 건드린다. 반대도 같다. **둘을 직접 붙이자는 제안은 이 문서를 근거로 거절한다** — 붙이는 순간 파이썬 프로세스가 React 를 렌더해야 하고, 그때부터 디자인 수정에 백엔드 배포가 필요해진다. diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index 019f8fa..0e0c235 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -13,7 +13,7 @@ --- -## 0. 표 14개, 스키마는 `public` 한 벌 +## 0. 표 15개, 스키마는 `public` 한 벌 도메인별 스키마(`company`·`place`·`fact`·`local`·`site`·`job`)는 2026-09-09 에 걷어냈다. 스키마 한정자가 붙는 순간부터 ORM·raw SQL·테스트 픽스처가 각자 그 이름을 들고 다녀야 했다. @@ -27,13 +27,14 @@ users 사장님 계정 │ ├ place_photos 사진 │ ├ place_facts ★ 사실. 이 제품의 심장 │ ├ place_faqs FAQ +│ ├ place_songs 이 숙소의 노래 — 발행할 때마다 한 곡(가사 Gemini → 작곡 Suno) │ └ place_area_refs 업장 ↔ 지역콘텐츠 관계(거리 · 숨김)만 ├ area_contents ★ 지역 콘텐츠 실체 — 키가 region_code 다(place_id 아님) └ sites 발행 사이트 — 사업장당 1개 ├ site_sections 섹션 콘텐츠(사장님이 넣은 것 · 서버가 채운 것) ├ site_versions ★ 빌드 버전 — snapshot 박제 └ site_publish_logs 발행 시도 기록(반려 사유 포함) -jobs 작업 큐 — 수집 · 비전 · 소개문 · 빌드 · 지역이야기 +jobs 작업 큐 — 수집 · 비전 · 소개문 · 빌드 · 지역이야기 · 노래 ``` **FK 제약은 걸지 않는다**(관계 컬럼만 둔다). 삭제는 전부 소프트 삭제(`deleted`)이고, @@ -137,6 +138,22 @@ jobs 작업 큐 — 수집 · 비전 · 소개문 · `source_fact_ids` 가 비면 **발행 게이트가 반려한다.** 확보된 fact 만 근거로 쓴다는 규칙이 데이터 모양으로 강제된 자리다. +### `place_songs` — 이 숙소의 노래 + +발행할 때마다 한 곡 만든다. 가사는 소개문과 **같은 재료**(확인된 fact + 조사 근거 + 소개문)로 +Gemini 가 쓰고, 곡은 Suno 가 붙인다. + +★ **검증 상태(`FactStatus`)가 없다.** 노래는 수집한 사실이 아니라 우리가 만든 창작물이라 +"맞는가" 를 물을 대상이 아니다. 상태는 "만들어졌는가" 하나다(`SongStatus`: +`GENERATING` · `READY` · `FAILED`). 스냅샷은 **`READY` 만** 싣는다. + +★ **`origin_url`(Suno 가 준 주소)은 발행본에 나가지 않는다.** 만료되는 주소라 그대로 실으면 +발행 직후에는 재생되고 몇 주 뒤 조용히 죽는다. mp3 를 받아 `solution/site/songs/.mp3` +에 두고, 프리렌더가 사이트 디렉토리로 복사한 것(`/s//.mp3`)만 나간다. +표에는 추적용으로만 남긴다. + +★ 새 곡이 실패해도 직전 곡이 그대로 남는다 — `latest_ready` 가 `READY` 중 최신 하나를 고른다. + ### `area_contents` + `place_area_refs` — 지역 콘텐츠 ★ **키가 `region_code` 다.** 같은 지역에 사이트가 몇 개 생기든 외부 조회는 1회. diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index c632067..1b63c4d 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -5,6 +5,68 @@ --- +## 2026-09-11 — 발행하면 이 숙소의 노래가 한 곡 생긴다 (가사 Gemini → 작곡 Suno) + +**무슨 일** — `/s/stay` 시안에는 헤더에 노래 플레이어가 있는데, 그건 손으로 채운 목업이라 +새로 발행한 사이트에는 그 자리가 아예 없었다. 이제 발행이 노래를 만든다. + +**흐름** — ★ **발행이 노래를 기다린다.** +``` +발행 누름 → BUILD 잡 + 1. 가사(Gemini) → 2. 작곡(Suno, 실측 30~40초 · 상한 5분) + 3. mp3 를 out/songs/ 에 보관 + 4. 스냅샷 → 게이트 → 발행 ← 여기서 비로소 사이트가 나간다 + 프리렌더가 mp3 를 사이트 디렉토리로 복사 +``` + +**왜 기다리나** — 먼저 굽고 나중에 붙이는 방식으로 먼저 만들어 봤는데, 그러면 발행 직후의 +사이트에는 노래가 없고 몇 분 뒤 조용히 생긴다. 사장님이 [사이트 열기] 로 보는 **첫 화면에 +그 기능이 빠져 있다.** 값은 발행이 그만큼 늦어지는 것이고, 그건 감수한다. +★ 단 실패는 발행을 막지 않는다 — 기다리는 것과 막는 것은 다르다. 키가 없거나 작곡이 실패하면 +노래 없이 발행되고 사유가 빌드 로그와 `place_songs.last_error` 에 남는다. +★ 미리보기 빌드(publish=false)에는 만들지 않는다. 유료 호출이라 눌러 보는 것만으로 돈이 나가면 안 된다. + +**왜 가사를 우리가 쓰나** — Suno 에 "군산 한옥 숙소 노래" 라고만 던지면 가사를 저쪽이 짓는다. +그 가사에는 이 숙소에 없는 것(수영장·조식·오션뷰)이 섞이고 우리는 검증할 방법이 없다 — +사이트의 다른 모든 문장은 확인된 fact 로만 쓰는데 노래만 지어낸 말을 싣는 꼴이다. +→ 가사는 **소개문과 같은 재료**(확인된 fact + 조사 근거 + 소개문)로 Gemini 가 쓰고, + Suno 는 곡만 붙인다. 프롬프트가 **없는 시설·숫자를 말하지 말라**고 못 박는다 + (요금·전화번호를 노래에 넣으면 틀렸을 때 고쳐 부를 수가 없다). +★ 가사에는 `ground_check` 를 걸지 않는다. "밤이 깊어도 불이 켜져 있다" 에 대응하는 fact 는 + 없다 — 문장 단위로 근거를 맞추면 전부 반려된다. 가사는 사실 진술이 아니라 정서다. + +**★ Suno 주소를 그대로 싣지 않는다** +Suno 가 주는 audio_url 은 **만료된다.** payload 에 그 주소를 실으면 발행 직후에는 재생되고 +몇 주 뒤 조용히 죽는다 — 아무도 안 누르면 죽은 줄도 모르는 종류다. mp3 를 받아 보관하고 +우리 경로(`/s//.mp3`)만 발행본에 내보낸다. + +**★ 콜백이 아니라 폴링이다** +Suno 는 `callBackUrl` 로 완료를 알려 주는데, 그러려면 Suno 가 우리 백엔드에 닿아야 한다. +이 서버는 로컬(:9800)이거나 사내망이라 그런 주소가 없다 — 콜백을 믿게 만들어 두면 +"요청은 성공했는데 결과가 영영 안 옴" 이 되고, 화면상 아무 일도 안 일어나는 실패다. +(API 가 필수로 요구해서 값은 채워 보내되, 그 주소를 듣지 않는다.) + +**경계는 그대로다** — 백엔드는 여전히 발행물 디렉토리를 모른다. payload 와 같은 약속으로 +`out/songs/.mp3` 에 떨구고, 굽는 쪽인 프리렌더가 `out/s//` 로 복사한다. +프리렌더는 복사하면서 **지난 발행의 mp3 를 치운다** — 발행마다 새 곡이라 안 치우면 1MB 짜리가 +발행 횟수만큼 쌓이고, Azure 에도 그대로 올라간다. + +**화면** — 헤더의 작은 플레이어(`SongPlayer`). 곡이 없으면 **아무것도 그리지 않는다** — +노래는 발행보다 늦게 도착하므로 그 사이 빈 플레이어를 그리면 고장난 버튼이다. +자동 재생하지 않고(소리가 갑자기 나는 페이지는 닫힌다), 가사를 함께 싣는다 +(오디오 안의 말은 크롤러가 못 듣는다). + +**표** — `place_songs`. 검증 상태가 없다(창작물이라 "맞는가" 를 물을 대상이 아니다). +상태는 `GENERATING`·`READY`·`FAILED` 셋이고 스냅샷은 READY 만 싣는다. 새 곡이 실패하면 +직전 곡이 그대로 남는다. → [DATA_MODEL.md](DATA_MODEL.md) + +**검증** — 실제로 발행해 봤다(스테이,머뭄 v15): 가사 '시간이 머무는 고요한 밤'(acoustic +ballad, 154자, $0.0014) → 작곡 40초 → 1.98MB mp3 → **그 다음** 스냅샷(노래 1) → 발행 완료. +`/s/스테이머뭄-99a887f8` 200, mp3 200 `audio/mpeg`, HTML 에 제목·가사·재생 주소 확인. +지난 발행의 곡은 404 로 치워졌다. `tsc --noEmit` · `eslint` · vitest 55건 통과(신규 4건). + +--- + ## 2026-09-10 — 소개문이 생성되고도 영영 안 나가던 것 (승인 단계 제거) **무슨 일** — 힐튼 가든 인 서울 강남을 만들어 보니 소개가 빈칸이었다. 로그는 `[copy] 소개문 O`, diff --git a/postgres-init/init-data/init.sql b/postgres-init/init-data/init.sql index 37dcace..ee479da 100644 --- a/postgres-init/init-data/init.sql +++ b/postgres-init/init-data/init.sql @@ -167,6 +167,27 @@ CREATE TABLE IF NOT EXISTS public.place_units ( deleted BOOLEAN NOT NULL DEFAULT FALSE ); +-- 이 숙소의 노래. 발행할 때마다 한 곡 만든다(가사 Gemini → 작곡 Suno). +-- ★ 곡은 사실이 아니라 만들어진 창작물이라 fact·사진과 같은 검증 상태를 두지 않는다. +-- 대신 완성(READY)된 것만 사이트에 나간다 — 생성 중인 곡을 실으면 재생 버튼이 빈 파일을 가리킨다. +CREATE TABLE IF NOT EXISTS public.place_songs ( + song_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + place_id uuid NOT NULL, + title VARCHAR(200) NOT NULL, + lyrics TEXT NULL, -- 화면에 같이 싣는다. 손님이 무슨 노래인지 읽을 수 있어야 한다 + style VARCHAR(200) NULL, -- Suno 에 넘긴 장르·분위기 문자열 + provider VARCHAR(40) NOT NULL DEFAULT 'suno', + provider_task_id VARCHAR(120) NULL, -- Suno taskId. 폴링·재조회의 유일한 열쇠 + origin_url VARCHAR(1000) NULL, -- ★ Suno 가 준 원본 주소. 만료되므로 이 주소를 사이트에 싣지 않는다 + file_name VARCHAR(200) NULL, -- 우리가 받아 둔 파일(solution/site/songs/<이것>). 프리렌더가 사이트로 복사한다 + duration_sec NUMERIC(6,2) NULL, + status SMALLINT NOT NULL DEFAULT 1, -- SongStatus: 1=generating 2=ready 3=failed. ★ 2 만 사이트에 나간다 + last_error TEXT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + deleted BOOLEAN NOT NULL DEFAULT FALSE +); + CREATE TABLE IF NOT EXISTS public.place_photos ( media_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), place_id uuid NOT NULL, @@ -382,6 +403,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS uq_place_links_place_url ON public.place_chann CREATE INDEX IF NOT EXISTS idx_units_place ON public.place_units (place_id); CREATE INDEX IF NOT EXISTS idx_media_place ON public.place_photos (place_id); +CREATE INDEX IF NOT EXISTS idx_songs_place ON public.place_songs (place_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_media_unit ON public.place_photos (unit_id); -- place_facts diff --git a/postgres-init/migrations/0010_place_songs.sql b/postgres-init/migrations/0010_place_songs.sql new file mode 100644 index 0000000..335ebe2 --- /dev/null +++ b/postgres-init/migrations/0010_place_songs.sql @@ -0,0 +1,30 @@ +-- 0010 · 숙소의 노래 표(place_songs) +-- +-- 발행할 때마다 이 숙소의 노래를 한 곡 만든다 — 가사는 Gemini, 작곡은 Suno. +-- 곡은 **사실이 아니라 창작물**이라 fact·사진 같은 검증 상태(FactStatus)를 두지 않는다. +-- 대신 상태는 "만들어졌는가" 하나다: 1=generating · 2=ready · 3=failed. +-- ★ 사이트에는 2 만 나간다. 생성 중인 곡을 실으면 재생 버튼이 없는 파일을 가리킨다. +-- +-- ★ origin_url 을 그대로 싣지 않는 이유: Suno 가 주는 주소는 만료된다. 받아서 우리 쪽에 +-- 보관한 파일(file_name)만 사이트에 나간다 — 안 그러면 몇 주 뒤 조용히 재생이 죽는다. + +CREATE TABLE IF NOT EXISTS public.place_songs ( + song_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + place_id uuid NOT NULL, + title VARCHAR(200) NOT NULL, + lyrics TEXT NULL, + style VARCHAR(200) NULL, + provider VARCHAR(40) NOT NULL DEFAULT 'suno', + provider_task_id VARCHAR(120) NULL, + origin_url VARCHAR(1000) NULL, + file_name VARCHAR(200) NULL, + duration_sec NUMERIC(6,2) NULL, + status SMALLINT NOT NULL DEFAULT 1, + last_error TEXT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + deleted BOOLEAN NOT NULL DEFAULT FALSE +); + +-- 한 업장의 최신 곡을 꺼내는 경로. 발행본은 늘 "가장 최근에 완성된 한 곡"을 싣는다. +CREATE INDEX IF NOT EXISTS idx_songs_place ON public.place_songs (place_id, created_at DESC); diff --git a/solution/backend/common/database/model/models.py b/solution/backend/common/database/model/models.py index 4a014bc..212706c 100644 --- a/solution/backend/common/database/model/models.py +++ b/solution/backend/common/database/model/models.py @@ -14,6 +14,7 @@ from common.enums import ( FactStatus, SourceType, MediaStatus, + SongStatus, SiteStatus, BuildStatus, JobStatus, @@ -190,6 +191,30 @@ class place_photos(MainTableMixin, MAIN_BASE): sort_order = Column(Integer, nullable=False, server_default=text("0"), default=0) +class place_songs(MainTableMixin, MAIN_BASE): + """이 숙소의 노래. 발행할 때마다 한 곡 만든다 — 가사는 Gemini, 작곡은 Suno. + + ★ 검증 상태(FactStatus)가 없다. 노래는 수집한 사실이 아니라 우리가 만든 창작물이라 + "맞는가" 를 물을 대상이 아니다. 상태는 "만들어졌는가" 하나다(SongStatus). + ★ origin_url(Suno 가 준 주소)은 **사이트에 싣지 않는다.** 만료되는 주소라 그대로 두면 + 몇 주 뒤 재생만 조용히 죽는다 — 받아서 보관한 file_name 만 발행본으로 나간다.""" + + __tablename__ = "place_songs" + + song_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + place_id = Column(UUID(as_uuid=True), nullable=False, index=True) + title = Column(String(200), nullable=False) + lyrics = Column(Text, nullable=True) # 화면에 함께 싣는다 + style = Column(String(200), nullable=True) # Suno 에 넘긴 장르·분위기 + provider = Column(String(40), nullable=False, server_default=text("'suno'"), default="suno") + provider_task_id = Column(String(120), nullable=True) # Suno taskId — 폴링의 유일한 열쇠 + origin_url = Column(String(1000), nullable=True) # ★ 만료되는 주소. 보관용 기록일 뿐이다 + file_name = Column(String(200), nullable=True) # solution/site/songs/<이것> + duration_sec = Column(Numeric(6, 2), nullable=True) + status = Column(SmallInteger, nullable=False, server_default=text("1"), default=SongStatus.GENERATING.value) + last_error = Column(Text, nullable=True) + + # ============================================================ # fact : 사실 / FAQ # ============================================================ diff --git a/solution/backend/common/enums.py b/solution/backend/common/enums.py index defab18..abc4d1b 100644 --- a/solution/backend/common/enums.py +++ b/solution/backend/common/enums.py @@ -283,6 +283,18 @@ class MediaStatus(CodeEnum): REJECTED = 3 +class SongStatus(CodeEnum): + """place_songs.status 코드값. + + ★ fact·사진과 달리 검증 상태가 없다. 노래는 수집한 사실이 아니라 우리가 만든 창작물이라 + "맞는가" 를 물을 대상이 아니다. 물을 것은 "만들어졌는가" 하나다. + ★ 사이트에는 READY 만 나간다 — 생성 중인 곡을 실으면 재생 버튼이 없는 파일을 가리킨다.""" + + GENERATING = 1 # Suno 가 작곡 중(또는 파일을 아직 못 받았다) + READY = 2 # 파일까지 받아 뒀다 — 사이트에 나간다 + FAILED = 3 # 생성 실패. 발행은 그대로 진행된다(노래만 없다) + + # ★ Vision 결과를 자동 반영해도 되는 신뢰도 하한. 이 아래는 사람 확인 큐(PENDING_REVIEW)로 남긴다. # "신뢰도 낮은 항목은 자동 반영하지 말고 사람 확인 큐로 보낸다" 를 한 곳에서만 판단한다. VISION_AUTO_APPROVE_CONFIDENCE = 0.7 @@ -419,6 +431,7 @@ class JobType(CodeEnum): BUILD = 4 # 사이트 정적 빌드 — ★ 개별 재빌드 단위 LOCAL_SYNC = 5 # 지역 정보 갱신 — 행정구역 코드 단위(같은 지역 사이트 50개여도 1회) AI_CHECK = 6 # AI 검색 노출 점검 + SONG = 7 # 이 숙소의 노래 한 곡 (가사 Gemini → 작곡 Suno). 발행이 이 잡을 건다 class JobStatus(CodeEnum): diff --git a/solution/backend/config/config_models.py b/solution/backend/config/config_models.py index 1b18d15..245856e 100644 --- a/solution/backend/config/config_models.py +++ b/solution/backend/config/config_models.py @@ -131,6 +131,11 @@ class ExternalApiConfig(BaseSettings): # 이 값 미만이면 자동 반영하지 않고 사람 확인 큐(PENDING_REVIEW)에 남긴다. vision_confidence_threshold: float = Field(0.7, validation_alias="VISION_CONFIDENCE_THRESHOLD") tour_api_key: str = Field("", validation_alias="TOUR_API_KEY") + # 발행할 때 이 숙소의 노래를 한 곡 만든다(services/song_service). 비면 그 단계만 건너뛴다. + suno_api_key: str = Field("", validation_alias="SUNO_API_KEY") + # ★ 콜백은 쓰지 않고 폴링한다 — 우리 백엔드는 로컬·사내망이라 Suno 가 부를 수 있는 주소가 아니다. + # 그래도 API 가 필수로 요구하는 필드라 값을 들고 있는다(services/external/suno.py 주석). + suno_callback_url: str = Field("", validation_alias="SUNO_CALLBACK_URL") # .env 를 요청마다 다시 읽지 않는다. 새 코드는 Depends(get_*) 로 주입받는다. diff --git a/solution/backend/crud/song_crud.py b/solution/backend/crud/song_crud.py new file mode 100644 index 0000000..1b4400c --- /dev/null +++ b/solution/backend/crud/song_crud.py @@ -0,0 +1,39 @@ +from sqlalchemy import and_, select, update + +from common.database.db_session_manager import DB_SESSION_MNG +from common.database.model.models import place_songs +from common.enums import SongStatus +from common.utils.gtime import GTime + + +class SongCRUD: + """place_songs 접근. 발행본이 읽는 것은 `latest_ready` 하나뿐이다.""" + + async def insert(self, db, row): + return await DB_SESSION_MNG.insert(db, row) + + async def update(self, db, song_id, data: dict): + return await DB_SESSION_MNG.add_with_rowcount( + db, + update(place_songs) + .where(place_songs.song_id == song_id, place_songs.deleted == False) # noqa: E712 + .values(**data, updated_at=GTime.UTC()), + ) + + async def latest_ready(self, db, place_id): + """이 업장의 **가장 최근에 완성된** 곡 하나. + + ★ READY 만 본다. 발행마다 새 곡을 만들므로 GENERATING 행이 함께 있을 수 있는데, + 그걸 집으면 아직 없는 파일을 사이트가 가리킨다. 실패(FAILED)도 마찬가지다 — + 새 곡이 실패하면 사이트는 **직전 곡을 그대로 유지**한다(빈 플레이어보다 낫다).""" + return await DB_SESSION_MNG.execute( + db, + select(place_songs) + .where(and_( + place_songs.place_id == place_id, + place_songs.status == SongStatus.READY.value, + place_songs.deleted == False, # noqa: E712 + )) + .order_by(place_songs.created_at.desc()) + .limit(1), + ) diff --git a/solution/backend/services/azure_static.py b/solution/backend/services/azure_static.py index fb0d22e..8fea4e7 100644 --- a/solution/backend/services/azure_static.py +++ b/solution/backend/services/azure_static.py @@ -33,6 +33,9 @@ def _content_type(path: Path) -> str: ".json": "application/json; charset=utf-8", ".xml": "application/xml; charset=utf-8", ".txt": "text/plain; charset=utf-8", + # 노래. guess_type 도 audio/mpeg 를 주지만 플랫폼마다 갈려서 못 박아 둔다 — + # octet-stream 으로 올라가면 브라우저가 재생 대신 내려받기로 처리한다. + ".mp3": "audio/mpeg", } return overrides.get(path.suffix.lower()) or mimetypes.guess_type(path.name)[0] or "application/octet-stream" @@ -46,6 +49,10 @@ def _cache_control(path: Path) -> str: # 60초로 두면 방문자가 수 MB 짜리 폰트를 계속 다시 받는다. if head == "fonts": return "public, max-age=604800" + # 노래 파일명은 song_id(UUID)다 — 곡이 바뀌면 이름도 바뀌므로 영구 캐시가 안전하다. + # 1MB 남짓한 파일을 60초마다 다시 받게 두면 헤더 버튼 하나가 트래픽을 먹는다. + if path.suffix.lower() == ".mp3": + return "public, max-age=31536000, immutable" # HTML · robots.txt · sitemap.xml — 발행하면 곧바로 반영되어야 한다. return "public, max-age=60, must-revalidate" diff --git a/solution/backend/services/build_service.py b/solution/backend/services/build_service.py index 3fa4611..89ced65 100644 --- a/solution/backend/services/build_service.py +++ b/solution/backend/services/build_service.py @@ -29,7 +29,7 @@ from common.logger import LOG from common.utils.gtime import GTime from crud.site_crud import SiteCRUD from crud.place_crud import PlaceCRUD -from services import azure_static, indexnow, publish_gate, render_report, site_payload, site_thumbnail +from services import azure_static, indexnow, publish_gate, render_report, site_payload, site_thumbnail, song_service from services.local_content_service import LocalContentService from services.site_payload import emit_payload from services.snapshot import build_snapshot @@ -126,6 +126,23 @@ async def run_build(job: dict) -> dict: except Exception as ex: # noqa: BLE001 — 곁들이 정보 실패가 빌드를 죽이면 안 된다 LOG.w(f"[build] place={place_id} 주변정보 갱신 실패(직전 값 사용): {type(ex).__name__}: {ex}") + # ★ 발행이면 **노래를 먼저 만들고** 스냅샷을 뜬다 (2026-09-11 결정). + # 순서가 뒤집히면(먼저 굽고 나중에 붙이기) 발행 직후의 사이트에는 노래가 없고 몇 분 뒤 + # 조용히 생긴다 — 사장님이 [사이트 열기] 로 보는 첫 화면에 그 기능이 빠져 있다. + # 값은 발행이 30초~3분 늦어지는 것이고(Suno 폴링 상한 5분), 그건 감수한다. + # ★ 실패해도 빌드는 계속한다. 주변 정보와 같은 규칙이다 — 곁들이 하나가 사장님 사이트 + # 발행을 막을 이유가 없다. 노래 없이 나가고, 사유는 아래 로그와 place_songs 에 남는다. + # ★ 미리보기 빌드(publish=False)에는 만들지 않는다 — 유료 호출이라 눌러 보는 것만으로 + # 비용이 나가면 안 된다. + song_result: dict | None = None + if want_publish: + try: + song_result = await song_service.ensure_song(place_id, owner_user_id) + LOG.i(f"[build] place={place_id} 노래 — {song_result}") + except Exception as ex: # noqa: BLE001 — 노래 실패가 발행을 죽이면 안 된다 + song_result = {"error": f"{type(ex).__name__}: {ex}"} + LOG.w(f"[build] place={place_id} 노래 실패(노래 없이 발행): {type(ex).__name__}: {ex}") + # ★ 일정(LLM)은 **여기서 직접** 부른다. 이건 잡이라 기다리는 사람이 없다 — # 캔버스 경로가 잡으로 넘기는 것과 사정이 다르다(local_content_service._ensure_region_stories). # 이미 있는 기간은 부르지 않으므로 매 빌드가 유료 호출이 되지는 않는다. @@ -160,6 +177,9 @@ async def run_build(job: dict) -> dict: result = {"place_id": place_id, "site_id": str(site.site_id), "version": version_no, "site_version_id": str(version.site_version_id)} + # 잡 결과에 남긴다 — "노래가 왜 없나" 를 잡 하나만 열어 보면 알 수 있어야 한다. + if song_result is not None: + result["song"] = song_result now = GTime.UTC() async def _fail(reason: str, gate: publish_gate.GateResult | None = None, extra: dict | None = None): diff --git a/solution/backend/services/external/gemini_text.py b/solution/backend/services/external/gemini_text.py index 83f03f1..05419a2 100644 --- a/solution/backend/services/external/gemini_text.py +++ b/solution/backend/services/external/gemini_text.py @@ -194,3 +194,77 @@ async def generate_copy( f"tokens in={usage.input_tokens} out={usage.output_tokens} · 약 ${price(model, usage)}" ) return result + + +@dataclass +class GeneratedSong: + """가사 생성 결과. 곡은 여기서 만들지 않는다 — 작곡은 services/external/suno 다.""" + + title: str + lyrics: str + style: str + + +async def generate_song( + place_name: str, + category: PlaceCategory, + *, + region: str, + grounding: list[str], + intro: str = "", + model: str = DEFAULT_TEXT_MODEL, + max_retries: int = 2, + client: Optional[httpx.AsyncClient] = None, +) -> GeneratedSong: + """이 업소의 노래 가사를 쓴다. + + ★ `ground_check` 를 걸지 않는다. 가사는 사실 진술이 아니라 정서라 문장 단위로 근거를 + 맞추면 전부 반려된다("밤이 깊어도 불이 켜져 있다" 에 대응하는 fact 는 없다). + 대신 프롬프트가 **없는 시설·숫자를 말하지 말라**고 못 박는다(services/prompts/song 머리주석). + ★ 재료가 하나도 없으면 부르지 않는다 — 소개문과 같은 규칙이다. 상호와 지역만으로 쓴 노래는 + 어느 숙소에 붙여도 말이 되는 노래이고, 그건 이 기능이 하려던 일이 아니다. + """ + if not is_configured(): + raise GeminiNotConfigured("GEMINI_API_KEY 가 설정되지 않았다") + if not grounding and not (intro or "").strip(): + raise GeminiInvalidOutput("가사를 쓸 재료가 없다 — 확인된 fact 도 소개문도 없다") + + from common.category_schema import get_schema + from services.prompts.song import RESPONSE_SCHEMA as SONG_SCHEMA, build_prompt as build_song_prompt + + body = { + "contents": [{"role": "user", "parts": [{ + "text": build_song_prompt(place_name, get_schema(category).label, region, grounding, intro) + }]}], + "generationConfig": { + "responseMimeType": "application/json", + "responseSchema": SONG_SCHEMA, + # 소개문(0.2)보다 높다 — 노래는 정확해야 하는 글이 아니라 흥얼거릴 글이다. + "temperature": 0.9, + }, + } + + owns_client = client is None + client = client or httpx.AsyncClient(timeout=httpx.Timeout(120.0, connect=10.0)) + try: + payload = await call(client, model, body, max_retries) + parsed = json.loads(extract_text(payload)) + except json.JSONDecodeError as ex: + raise GeminiInvalidOutput(f"가사 파싱 실패: {ex}") from ex + finally: + if owns_client: + await client.aclose() + + title = (parsed.get("title") or "").strip() + lyrics = (parsed.get("lyrics") or "").strip() + style = (parsed.get("style") or "").strip() + if not lyrics: + raise GeminiInvalidOutput("가사가 비어 있다") + + usage = read_usage(payload) + LOG.i( + f"[gemini-text] '{place_name}' 가사 — '{title}' ({style}) · {len(lyrics)}자 · " + f"tokens in={usage.input_tokens} out={usage.output_tokens} · 약 ${price(model, usage)}" + ) + # 제목이 비면 상호를 쓴다 — 빈 제목은 플레이어에서 빈 줄로 보인다. + return GeneratedSong(title=title or place_name, lyrics=lyrics, style=style or "acoustic ballad") diff --git a/solution/backend/services/external/suno.py b/solution/backend/services/external/suno.py new file mode 100644 index 0000000..b646836 --- /dev/null +++ b/solution/backend/services/external/suno.py @@ -0,0 +1,158 @@ +"""Suno API — 가사를 받아 40초짜리 곡 한 편을 만든다. + + API 문서 https://docs.sunoapi.org + 가사 services/external/gemini_text.generate_song (여기는 작곡만 한다) + 쓰는 곳 services/song_service (잡 흐름·저장) + +★ **콜백을 쓰지 않고 폴링한다.** + Suno 는 완료 시 `callBackUrl` 로 POST 를 보내 주는데, 그러려면 Suno 쪽에서 우리 백엔드에 + 닿아야 한다. 이 서버는 로컬(:9800)이거나 사내망(킹서버)이라 그런 주소가 없다 — + 콜백을 믿게 만들어 두면 "요청은 성공했는데 결과가 영영 안 옴" 이 되고, 그건 화면상 + 아무 일도 안 일어나는 종류의 실패다. 그래서 `generate` 로 taskId 를 받고 `record-info` 를 + 직접 물어본다. 잡 워커에서 도는 코드라 몇 분 기다리는 것이 문제가 되지 않는다. + (`callBackUrl` 은 API 가 필수로 요구해서 값만 채워 보낸다. 우리는 그 주소를 듣지 않는다.) + +★ **한 요청에 곡이 두 편 온다.** Suno 는 같은 가사로 변주 두 개를 만들어 준다(sunoData 배열). + 우리는 **첫 번째 한 곡만** 쓴다 — 사장님에게 고르라고 묻는 화면이 없고, 두 곡을 다 실으면 + 손님이 무엇을 듣는지 우리도 모른다. + +★ **오디오 주소는 만료된다.** 여기서 돌려주는 `audio_url` 을 그대로 사이트에 싣지 않는다. + 받는 쪽(song_service)이 파일을 내려받아 우리 쪽에 보관한다. +""" +import asyncio +from typing import Any, Optional + +import httpx + +from common.logger import LOG +from config.server_configs import external_api_config + +BASE_URL = "https://api.sunoapi.org/api/v1" + +# 실측(참고 프로젝트 o2o-castad-backend): 스트림 주소는 30~40초, 내려받을 수 있는 주소는 2~3분. +# 우리는 파일을 받아야 하므로 뒤쪽 기준으로 기다린다. +POLL_INTERVAL_SEC = 10 +POLL_TIMEOUT_SEC = 300 + +REQUEST_TIMEOUT = httpx.Timeout(60.0, connect=10.0) + +# 40초짜리를 만든다. 헤더의 작은 플레이어에서 듣는 곡이라 길 이유가 없고, +# 길수록 생성 시간과 요금이 같이 는다. +SONG_SECONDS = 40 +MODEL = "V5" + + +class SunoNotConfigured(RuntimeError): + """SUNO_API_KEY 가 없다 — 노래만 건너뛰고 발행은 계속한다.""" + + +class SunoError(RuntimeError): + """호출 실패·거절. 잡의 last_error 로 남는다.""" + + +def is_configured() -> bool: + return bool((external_api_config.suno_api_key or "").strip()) + + +def _headers() -> dict: + return { + "Authorization": f"Bearer {external_api_config.suno_api_key}", + "Content-Type": "application/json", + } + + +async def generate(lyrics: str, *, title: str, style: str, client: httpx.AsyncClient) -> str: + """작곡 요청. taskId 를 돌려준다. + + ★ `customMode=True` 다 — prompt 를 '주제' 가 아니라 **가사 그대로** 쓰라는 뜻이다. + false 로 두면 Suno 가 가사를 자기가 새로 쓴다. 우리는 이 숙소의 사실로 쓴 가사를 + 넘기는 것이므로, 그걸 버리면 이 기능의 의미가 없다. + """ + if not is_configured(): + raise SunoNotConfigured("SUNO_API_KEY 미설정") + + body = { + "model": MODEL, + "customMode": True, + "instrumental": False, + # 길이는 API 파라미터가 아니라 프롬프트로 지시한다(참고 프로젝트와 같은 방식). + "prompt": f"[Song Duration: Around {SONG_SECONDS} seconds]\n{lyrics}", + "title": title[:80], + "style": style, + # 듣지 않는 주소다(머리주석). 비워서 보내면 거절당한다. + "callBackUrl": external_api_config.suno_callback_url or "https://example.com/api/suno/callback", + } + + try: + res = await client.post(f"{BASE_URL}/generate", headers=_headers(), json=body, timeout=REQUEST_TIMEOUT) + except httpx.HTTPError as ex: + raise SunoError(f"generate 호출 실패: {type(ex).__name__}: {ex}") from ex + + if res.status_code != 200: + raise SunoError(f"generate HTTP {res.status_code}: {res.text[:300]}") + + data = res.json() or {} + if data.get("code") != 200: + raise SunoError(f"generate 거절: {data.get('msg')}") + + task_id = ((data.get("data") or {}).get("taskId")) + if not task_id: + raise SunoError(f"generate 응답에 taskId 가 없다: {str(data)[:300]}") + return task_id + + +def _first_clip(payload: dict) -> Optional[dict]: + """완성된 클립 하나. 아직이면 None. + + ★ 상태 문자열을 믿기 전에 **주소가 실제로 있는지** 본다. SUCCESS 인데 audioUrl 이 + 아직 비어 오는 응답을 참고 프로젝트가 겪었다(스트림만 먼저 나오는 구간). + """ + data = (payload or {}).get("data") or {} + status = (data.get("status") or "").upper() + if status in {"CREATE_TASK_FAILED", "GENERATE_AUDIO_FAILED", "CALLBACK_EXCEPTION", "SENSITIVE_WORD_ERROR"}: + raise SunoError(f"작곡 실패: {status} {data.get('errorMessage') or ''}".strip()) + + clips = ((data.get("response") or {}).get("sunoData")) or [] + for clip in clips: + if clip.get("audioUrl") or clip.get("sourceAudioUrl"): + return clip + return None + + +async def wait_for_clip(task_id: str, *, client: httpx.AsyncClient) -> dict[str, Any]: + """완성될 때까지 물어본다. 돌려주는 것은 첫 클립 하나. + + ★ 상한(POLL_TIMEOUT_SEC)을 둔다. Suno 가 영영 안 끝내는 경우 잡이 그대로 매달리면 + 워커 한 자리를 계속 차지한다 — 노래 하나 때문에 다른 사업장의 수집이 멈춘다. + """ + waited = 0 + while waited < POLL_TIMEOUT_SEC: + await asyncio.sleep(POLL_INTERVAL_SEC) + waited += POLL_INTERVAL_SEC + + try: + res = await client.get( + f"{BASE_URL}/generate/record-info", + headers=_headers(), + params={"taskId": task_id}, + timeout=REQUEST_TIMEOUT, + ) + res.raise_for_status() + except httpx.HTTPError as ex: + # 폴링 한 번 실패는 실패가 아니다 — 다음 차례에 다시 묻는다. + LOG.w(f"[suno] 상태 조회 실패(계속 기다린다) task={task_id}: {type(ex).__name__}") + continue + + clip = _first_clip(res.json() or {}) + if clip: + LOG.i(f"[suno] 작곡 완료 task={task_id} ({waited}초)") + return clip + + raise SunoError(f"{POLL_TIMEOUT_SEC}초 안에 완성되지 않았다 task={task_id}") + + +async def download(url: str, *, client: httpx.AsyncClient) -> bytes: + """오디오 파일을 받아 온다. 보관은 부르는 쪽이 한다.""" + res = await client.get(url, timeout=httpx.Timeout(180.0, connect=10.0), follow_redirects=True) + res.raise_for_status() + return res.content diff --git a/solution/backend/services/prompts/song.py b/solution/backend/services/prompts/song.py new file mode 100644 index 0000000..22a9991 --- /dev/null +++ b/solution/backend/services/prompts/song.py @@ -0,0 +1,62 @@ +"""이 숙소의 노래 — 가사 프롬프트와 응답 스키마. + +★ 왜 가사를 **우리가** 쓰고 Suno 에는 작곡만 시키나 + Suno 에 "군산 한옥 숙소 노래" 라고만 던지면 가사를 저쪽이 지어낸다. 그 가사에는 이 숙소에 + 없는 것(수영장·조식·오션뷰)이 섞이고, 우리는 그걸 검증할 방법이 없다 — 사이트의 다른 + 모든 문장은 확인된 fact 로만 쓰는데 노래만 지어낸 말을 싣는 꼴이 된다. + 그래서 가사는 소개문과 **같은 재료**(확인된 fact + 소개문)로 여기서 쓰고, Suno 는 그 가사에 + 곡을 붙이기만 한다. + +★ 그래도 가사는 사실 진술이 아니다 + "밤이 깊어도 불이 켜져 있다" 같은 줄은 fact 가 아니라 분위기다. 그래서 `ground_check` 를 + 걸지 않는다 — 대신 프롬프트가 **없는 시설·없는 숫자를 말하지 말라**고 못 박는다. + 요금·전화번호·주소를 가사에 넣지 않는 것도 같은 이유다(틀리면 예약 클레임이고, 노래는 + 고쳐 부르기도 어렵다). +""" + +# Suno 가 받는 style 문자열. 장르를 모델이 고르게 두되 후보를 좁힌다 — +# 열어 두면 숙소 사이트에 어울리지 않는 것(하드록·트랩)이 나온다. +STYLE_CHOICES = [ + "acoustic ballad", "city pop", "folk pop", "lo-fi", "bossa nova", "soft rock", "jazz", +] + +RESPONSE_SCHEMA = { + "type": "object", + "properties": { + "title": {"type": "string", "description": "곡 제목. 상호를 그대로 쓰지 말고 한 구절로."}, + "lyrics": {"type": "string", "description": "가사. [Verse]/[Chorus] 구조 태그를 포함한다."}, + "style": {"type": "string", "description": f"장르·분위기. 다음 중 하나에서 시작한다: {', '.join(STYLE_CHOICES)}"}, + }, + "required": ["title", "lyrics", "style"], +} + + +def build_prompt(place_name: str, category_label: str, region: str, grounding: list[str], intro: str) -> str: + """가사 프롬프트. 재료는 소개문 생성과 같은 것을 받는다.""" + material = "\n".join(f"- {line}" for line in grounding) or "- (확인된 항목 없음)" + intro_block = f"\n[이 숙소 소개문]\n{intro.strip()}\n" if (intro or "").strip() else "" + + return f"""너는 작은 가게의 노래를 쓰는 작사가다. 아래 업소의 노래 가사를 쓴다. + +[업소] +- 상호: {place_name} +- 업종: {category_label} +- 지역: {region or "(모름)"} + +[확인된 항목 — 이 안에서만 말한다] +{material} +{intro_block} +[쓰는 법] +- 한국어. 40초 안에 불리는 길이다 — [Verse] 한 덩이 + [Chorus] 한 덩이면 충분하다. +- 구조 태그([Verse], [Chorus])를 반드시 넣는다. Suno 가 이 태그로 곡을 나눈다. +- **없는 것을 말하지 않는다.** 위 목록에 없는 시설·풍경·서비스를 지어내지 않는다. + 지역과 계절, 머무는 마음처럼 사실 확인이 필요 없는 정서는 자유롭게 쓴다. +- **숫자를 넣지 않는다.** 요금·전화번호·주소·객실 수는 가사에 쓰지 않는다. + 틀리면 손님이 손해를 보고, 노래는 고쳐 부르기 어렵다. +- 광고 문구처럼 쓰지 않는다("최고", "1등", "예약하세요"). 손님이 흥얼거릴 노래다. +- 상호는 후렴에 한 번쯤 자연스럽게 넣는다. + +[출력] +title(곡 제목) · lyrics(가사) · style(장르·분위기) 를 JSON 으로 준다. +style 은 이 업소의 분위기에 맞는 것을 고른다: {', '.join(STYLE_CHOICES)}. +""" diff --git a/solution/backend/services/site_payload.py b/solution/backend/services/site_payload.py index 8db80de..e5a7932 100644 --- a/solution/backend/services/site_payload.py +++ b/solution/backend/services/site_payload.py @@ -954,6 +954,25 @@ def to_site_payload(place, snapshot: dict, site, version, links) -> dict: # ★ 없는 것을 지어내지 않는다 — 틀린 경로 안내는 방문자에게 헛걸음을 만든다(모델 주석). # 렌더러는 비면 해당 섹션을 그리지 않는다. "routes": [], + # ★ 이 숙소의 노래. `audioUrl` 은 **우리 쪽 경로**다 — Suno 가 준 주소는 만료되므로 + # 파일을 받아 두고(song_service) 프리렌더가 사이트 디렉토리로 복사한 것을 가리킨다. + # 경로를 여기서 만드는 이유: 발행본의 주소 규칙(basePath + /s/)을 아는 곳이 여기다. + "songs": [ + { + "songId": row["song_id"], + "title": row["title"], + "lyrics": row.get("lyrics") or None, + "style": row.get("style") or None, + "durationSec": row.get("duration_sec"), + # 프리렌더가 복사해 놓을 자리. 파일명은 그대로 쓴다. + # basePath 자체가 이미 `/s/`(또는 서브패스 마운트라면 그 앞에 접두어)다. + "audioUrl": f"{target['basePath']}/{row['file_name']}", + # 프리렌더가 원본을 찾을 때 쓰는 이름(솔루션 밖으로는 안 나간다). + "fileName": row["file_name"], + } + for row in (snapshot.get("songs") or []) + if (row.get("file_name") or "").strip() + ], "narrative": narrative, "theme": theme, } diff --git a/solution/backend/services/site_service.py b/solution/backend/services/site_service.py index 788151b..9197f07 100644 --- a/solution/backend/services/site_service.py +++ b/solution/backend/services/site_service.py @@ -560,6 +560,11 @@ class SiteService: if job_id is None: res.result.SetResult(ErrorType.COLLECT_ALREADY_RUNNING) return res + + # ★ 노래는 여기서 따로 걸지 않는다. BUILD 잡이 스냅샷을 뜨기 **전에** 직접 만든다 + # (`build_service.run_build` → `song_service.ensure_song`) — 발행이 노래를 기다린다. + # 따로 걸면 먼저 구워지고 노래가 몇 분 뒤 붙는데, 그러면 사장님이 [사이트 열기] 로 + # 보는 첫 화면에 그 기능이 빠져 있다(2026-09-11 결정). res.job_id = uuid.UUID(job_id) res.status = JobStatus.PENDING res.created = created diff --git a/solution/backend/services/snapshot.py b/solution/backend/services/snapshot.py index 975aa4f..8eaade2 100644 --- a/solution/backend/services/snapshot.py +++ b/solution/backend/services/snapshot.py @@ -23,7 +23,7 @@ from sqlalchemy import or_, select from common.category_schema import get_schema from common.database.db_session_manager import DB_SESSION_MNG from common.database.model.models import ( - place_facts, place_faqs, area_contents, place_photos, place_area_refs, place_units, + place_facts, place_faqs, area_contents, place_photos, place_area_refs, place_songs, place_units, site_sections, sites, ) from common.enums import ( @@ -35,6 +35,7 @@ from common.enums import ( LocalSource, MediaStatus, PlaceCategory, + SongStatus, ) from common.logger import LOG from services.external.naver import region_key @@ -89,6 +90,16 @@ async def build_snapshot(place) -> dict: ).order_by(place_photos.sort_order.asc()) ) + # ★ 완성(READY)된 최신 곡 하나. 발행마다 새 곡을 만들므로 생성 중인 행이 함께 있을 수 있는데, + # 그걸 실으면 사이트가 아직 없는 파일을 가리킨다. 새 곡이 실패하면 직전 곡이 그대로 남는다. + song_rows = await _select( + select(place_songs).where( + place_songs.place_id == pid, + place_songs.deleted == False, # noqa: E712 + place_songs.status == SongStatus.READY.value, + ).order_by(place_songs.created_at.desc()).limit(1) + ) + local_rows = await _local_contents(place) snapshot = { @@ -161,11 +172,26 @@ async def build_snapshot(place) -> dict: # ★ 원문(body)을 거의 그대로 싣는다. 렌더러 타입으로의 변환은 site_payload 가 한다 — # fact·사진과 같은 분업이다(여기는 '무엇이 나갈 수 있는가', 거기는 '어떤 모양으로 나가는가'). "local": local_rows, + # ★ 이 숙소의 노래. 파일은 DB 가 아니라 `solution/site/songs/` 에 있고, + # 프리렌더가 그걸 사이트 디렉토리로 복사한다(services/song_service 머리주석). + # ★ origin_url(Suno 주소)은 싣지 않는다 — 만료되는 주소라 발행본에 나가면 안 된다. + "songs": [ + { + "song_id": str(r.song_id), + "title": r.title, + "lyrics": r.lyrics, + "style": r.style, + "file_name": r.file_name, + "duration_sec": float(r.duration_sec) if r.duration_sec is not None else None, + } + for r in song_rows + if (r.file_name or "").strip() + ], } LOG.i( f"[snapshot] place={pid} fact {len(snapshot['facts'])} · 객실 {len(snapshot['units'])} · " f"FAQ {len(snapshot['faqs'])} · 사진 {len(snapshot['media'])} · " - f"지역 {len(snapshot['local']['contents'])}" + f"지역 {len(snapshot['local']['contents'])} · 노래 {len(snapshot['songs'])}" ) return snapshot diff --git a/solution/backend/services/song_service.py b/solution/backend/services/song_service.py new file mode 100644 index 0000000..ba99a3f --- /dev/null +++ b/solution/backend/services/song_service.py @@ -0,0 +1,258 @@ +"""이 숙소의 노래 — SONG 잡이 하는 일. + + 가사 services/external/gemini_text.generate_song (확인된 fact + 소개문으로 쓴다) + 작곡 services/external/suno (폴링 · 40초 · 한 곡) + 여기 재료 모으기 → 가사 → 작곡 → 파일 보관 (발행이 이걸 기다린다) + +★ **발행은 노래를 기다린다** (2026-09-11 결정). + BUILD 잡이 스냅샷을 뜨기 **전에** 여기를 부른다(`build_service.run_build`). 그래서 발행된 + 사이트에는 처음부터 노래가 들어 있다 — 사장님이 [사이트 열기] 를 눌러 본 화면과 손님이 + 보는 화면이 같다. + 값은 발행이 그만큼 늦어지는 것이다(실측 30초~3분, 상한 5분). 먼저 굽고 나중에 붙이는 + 방식도 만들어 봤지만, 그러면 발행 직후의 사이트에는 노래가 없고 몇 분 뒤 조용히 생긴다 — + "발행했는데 그 기능이 없다" 를 사장님이 먼저 본다. + +★ 잡 타입(JobType.SONG)은 그대로 둔다. + 발행과 무관하게 **곡만 다시 만들** 때 쓰는 길이다(운영자가 잡을 직접 넣는다). + 발행 경로와 같은 함수(`ensure_song`)를 부르므로 둘이 갈라지지 않는다. + +★ 왜 파일을 받아서 보관하나 + Suno 가 주는 주소는 만료된다. 그 주소를 payload 에 실으면 발행 직후에는 재생되고 몇 주 뒤 + 조용히 죽는다 — 아무도 안 누르면 죽은 줄도 모르는 종류다. 그래서 mp3 를 받아 + `solution/site/songs/` 에 두고, 프리렌더가 사이트 디렉토리로 복사한다. + (백엔드는 여전히 HTML 을 만들지 않는다 — 파일과 payload 를 같은 약속된 자리에 둘 뿐이다.) + +★ Blob Storage 로는 **발행이 올린다** — 여기서 따로 올리지 않는다. + 파일이 `out/s//` 안에 있으므로 `azure_static.publish(slug)` 가 사이트를 통째로 + 올릴 때 함께 올라간다(content-type `audio/mpeg`, 해시 파일명이라 immutable 캐시). + 지난 곡은 `_remove_stale_site_files` 가 블롭에서도 지운다. + → 업로더를 하나 더 두면 같은 컨테이너에 **두 규칙**이 생긴다(경로·캐시·정리 주체). + 참고 프로젝트(o2o-castad-backend)는 사이트가 없어 직접 올릴 수밖에 없었지만, + 여기서는 노래가 사이트의 일부라 사이트와 같은 길로 나가는 것이 맞다. + 덕분에 주소도 같은 오리진(`/s//.mp3`)이라 CORS·혼합콘텐츠 문제가 없다. + +★ 실패는 발행을 막지 않는다 + 키가 없거나(SUNO_API_KEY·GEMINI_API_KEY), 재료가 없거나, Suno 가 늦으면(상한 5분) + **노래만 없이** 발행된다. 기다리는 것과 막는 것은 다르다 — 음악 API 가 느린 날 + 사장님 사이트가 아예 안 나가는 것은 맞바꿀 수 없는 손해다. + 사유는 place_songs.last_error 와 빌드 로그에 남는다. +""" +import uuid +from pathlib import Path + +import httpx + +from common.category_schema import CategorySchemaError, get_schema +from common.database.db_session_manager import DB_SESSION_MNG +from common.database.model.models import place_channels, place_facts, place_songs, places +from common.enums import DBWRType, ErrorType, PlaceCategory, SongStatus +from common.logger import LOG +from common.utils.gtime import GTime +from crud.fact_crud import FactCRUD +from crud.place_crud import PlaceCRUD +from crud.song_crud import SongCRUD +from services import place_research, site_payload +from services.external import gemini_text, suno +from services.llm.gemini import GeminiError, GeminiInvalidOutput, GeminiNotConfigured + +_fact_crud = FactCRUD() +_place_crud = PlaceCRUD() +_song_crud = SongCRUD() + +# payload 와 나란히 두는 자리. 프리렌더가 이 디렉토리에서 파일을 찾는다. +# payload_dir 이 `/app/out/payloads` 면 여기는 `/app/out/songs` 다 — 컴포즈가 둘 다 호스트의 +# `solution/site/` 아래로 붙인다. 한 디렉토리 약속(ARCHITECTURE 1절)을 노래에도 그대로 쓴다. +SONGS_DIRNAME = "songs" + + +class SongAborted(RuntimeError): + """재시도해도 소용없는 중단 — 잡의 last_error 로 남아 운영자가 본다.""" + + +def songs_dir() -> Path: + return site_payload.payload_dir().parent / SONGS_DIRNAME + + +async def _grounding(place, place_id: str) -> tuple[list[str], str]: + """가사 재료 — 확인된 fact + 수집/조사 원문, 그리고 소개문. + + ★ 소개문 생성과 **같은 재료**를 쓴다(copy_service). 노래만 다른 출처를 보면 사이트의 + 글과 노래가 서로 다른 숙소를 말하게 된다. + """ + schema = get_schema(PlaceCategory(place.category)) + pid = uuid.UUID(place_id) + + f_err, fact_rows = await DB_SESSION_MNG.execute_lambda( + place_facts.DBType(), + DBWRType.DB_READ.value, + lambda s: _fact_crud.list_facts(s, pid, None, None, True, True), + ) + if f_err != ErrorType.SUCCESS: + raise SongAborted(f"fact 조회 실패: {f_err.name}") + + lines: list[str] = [] + intro = "" + for row in (fact_rows or []): + value = (row.value or "").strip() + if not value: + continue + if row.key == "intro": + intro = value + continue + if row.key == "meta_description": + continue + spec = schema.get(row.key) + label = spec.label if spec else row.key + lines.append(f"{label}: {value}{(' ' + row.unit) if row.unit else ''}") + + # 조사 근거(남이 쓴 글)도 재료다 — 이 숙소의 내력이 거기에만 있는 경우가 많다. + l_err, link_rows = await DB_SESSION_MNG.execute_lambda( + place_channels.DBType(), + DBWRType.DB_READ.value, + lambda s: _place_crud.list_links(s, pid, False), + ) + if l_err == ErrorType.SUCCESS: + for link in (link_rows or []): + raw = link.raw if isinstance(link.raw, dict) else {} + if link.confirmed_at is None and raw.get("kind") != place_research.RAW_KIND: + continue + text = (raw.get("text") or "").strip() + if text: + lines.append(text[:1500]) + + return lines, intro + + +async def run_song(job: dict) -> dict: + """SONG 잡 핸들러 — 발행과 무관하게 곡만 다시 만들 때 쓴다. + + 발행 경로는 이 잡을 거치지 않고 `ensure_song` 을 직접 부른다(build_service).""" + payload = job["payload"] + return await ensure_song(payload["place_id"], payload["owner_user_id"]) + + +async def ensure_song(place_id: str, owner_user_id: str) -> dict: + """이 업장의 노래를 한 곡 만든다. 돌려주는 dict 가 곧 잡 결과이자 빌드 로그다. + + ★ 실패를 예외로 올리지 않는다(사업장을 못 찾는 것 같은 진짜 고장만 예외다). + 발행이 이 함수를 기다리는데 여기서 예외가 나면 **노래 때문에 발행이 통째로 실패**한다. + 음악 API 가 느린 날 사장님 사이트가 안 나가는 것은 맞바꿀 수 없는 손해다.""" + if not suno.is_configured(): + # 키가 없는 것은 고장이 아니라 설정이다 — 예외로 올려 재시도·DEAD 로 만들지 않는다. + LOG.i(f"[song] place={place_id} 건너뜀 — SUNO_API_KEY 미설정") + return {"place_id": place_id, "skipped": "SUNO_API_KEY 미설정"} + + err, place = await DB_SESSION_MNG.execute_lambda( + places.DBType(), + DBWRType.DB_READ.value, + lambda s: _place_crud.get_place(s, uuid.UUID(owner_user_id), uuid.UUID(place_id)), + ) + if err != ErrorType.SUCCESS or place is None: + raise SongAborted(f"사업장을 찾을 수 없다: {place_id}") + + try: + get_schema(PlaceCategory(place.category)) + except (CategorySchemaError, ValueError) as ex: + raise SongAborted(f"지원하지 않는 업종: {place.category}") from ex + + lines, intro = await _grounding(place, place_id) + region = site_payload.region_label(place.road_address, place.address) or "" + + # ── 1. 가사 ─────────────────────────────────────────────── + async with httpx.AsyncClient(timeout=httpx.Timeout(120.0, connect=10.0)) as client: + try: + song = await gemini_text.generate_song( + place.name, PlaceCategory(place.category), + region=region, grounding=lines, intro=intro, client=client, + ) + except GeminiNotConfigured: + LOG.i(f"[song] place={place_id} 건너뜀 — GEMINI_API_KEY 미설정") + return {"place_id": place_id, "skipped": "GEMINI_API_KEY 미설정"} + except GeminiInvalidOutput as ex: + LOG.i(f"[song] place={place_id} 건너뜀 — {ex}") + return {"place_id": place_id, "skipped": str(ex)} + except GeminiError as ex: + raise SongAborted(f"가사 생성 실패: {ex}") from ex + + # 가사를 먼저 적재한다 — 작곡이 실패해도 "무엇을 만들려 했는가" 가 남아야 한다. + row = place_songs( + song_id=uuid.uuid4(), + place_id=uuid.UUID(place_id), + title=song.title[:200], + lyrics=song.lyrics, + style=song.style[:200], + status=SongStatus.GENERATING.value, + ) + add_err = await DB_SESSION_MNG.execute_lambda_run( + [place_songs.DBType()], [lambda s: _song_crud.insert(s, row)], + ) + if add_err != ErrorType.SUCCESS: + raise SongAborted(f"노래 행 생성 실패: {add_err.name}") + + async def _fail(reason: str) -> dict: + await DB_SESSION_MNG.execute_lambda_claim( + place_songs.DBType(), + lambda s: _song_crud.update(s, row.song_id, { + "status": SongStatus.FAILED.value, "last_error": reason[:2000], + }), + ) + LOG.w(f"[song] place={place_id} 실패: {reason}") + return {"place_id": place_id, "song_id": str(row.song_id), "error": reason} + + # ── 2. 작곡 ─────────────────────────────────────────── + try: + task_id = await suno.generate(song.lyrics, title=song.title, style=song.style, client=client) + except suno.SunoNotConfigured as ex: + return await _fail(str(ex)) + except suno.SunoError as ex: + return await _fail(f"작곡 요청 실패: {ex}") + + await DB_SESSION_MNG.execute_lambda_claim( + place_songs.DBType(), + lambda s: _song_crud.update(s, row.song_id, {"provider_task_id": task_id}), + ) + + try: + clip = await suno.wait_for_clip(task_id, client=client) + except suno.SunoError as ex: + return await _fail(str(ex)) + + audio_url = clip.get("audioUrl") or clip.get("sourceAudioUrl") or "" + try: + content = await suno.download(audio_url, client=client) + except httpx.HTTPError as ex: + return await _fail(f"오디오 내려받기 실패: {type(ex).__name__}: {ex}") + + # ── 3. 보관 ─────────────────────────────────────────────── + # 파일명은 song_id 다 — 발행마다 새 곡이 생기므로 이름이 겹치면 옛 곡이 새 곡으로 바뀐다. + file_name = f"{row.song_id}.mp3" + directory = songs_dir() + try: + directory.mkdir(parents=True, exist_ok=True) + tmp = directory / f".{file_name}.tmp" + tmp.write_bytes(content) + # payload 와 같은 이유로 rename 이다 — 프리렌더가 반쯤 쓰인 파일을 집으면 안 된다. + tmp.replace(directory / file_name) + except OSError as ex: + return await _fail(f"파일 저장 실패: {ex}") + + duration = clip.get("duration") + await DB_SESSION_MNG.execute_lambda_claim( + place_songs.DBType(), + lambda s: _song_crud.update(s, row.song_id, { + "status": SongStatus.READY.value, + "file_name": file_name, + "origin_url": audio_url[:1000] or None, + "duration_sec": round(float(duration), 2) if isinstance(duration, (int, float)) else None, + "title": (clip.get("title") or song.title)[:200], + }), + ) + + LOG.i(f"[song] place={place_id} '{song.title}' ({song.style}) 완성 — {len(content)} bytes · {file_name}") + + # ★ 여기서 재빌드를 걸지 않는다. 발행이 이 함수를 **기다리고 있고**, 돌아가면 바로 그 + # 스냅샷에 이 곡이 실린다. 잡으로 따로 돌 때(운영자 재생성)도 같다 — 다음 발행에 실린다. + return { + "place_id": place_id, "song_id": str(row.song_id), "title": song.title, + "style": song.style, "file": file_name, + } diff --git a/solution/backend/worker/handlers.py b/solution/backend/worker/handlers.py index 696334b..05da067 100644 --- a/solution/backend/worker/handlers.py +++ b/solution/backend/worker/handlers.py @@ -15,6 +15,7 @@ JobType.COPY ✓ services/copy_service.run_copy — 소개문·FAQ (확보된 fact 만 근거) JobType.BUILD ✓ services/build_service.run_build — 정적 빌드 + 발행 검수 게이트 JobType.LOCAL_SYNC ✓ services/story_service.run_local_sync — 지역 이야기 생성(지역당 1회) + JobType.SONG ✓ services/song_service.run_song — 이 숙소의 노래 한 곡(가사 Gemini → 작곡 Suno) JobType.AI_CHECK → reports 모듈이 붙을 때 """ @@ -80,6 +81,7 @@ def _register_builtin(): from services.build_service import run_build from services.copy_service import run_copy from services.story_service import run_local_sync + from services.song_service import run_song from services.vision_service import run_vision if JobType.COLLECT.value not in HANDLERS: @@ -92,6 +94,8 @@ def _register_builtin(): HANDLERS[JobType.BUILD.value] = run_build if JobType.LOCAL_SYNC.value not in HANDLERS: HANDLERS[JobType.LOCAL_SYNC.value] = run_local_sync + if JobType.SONG.value not in HANDLERS: + HANDLERS[JobType.SONG.value] = run_song _register_builtin() diff --git a/solution/shared/src/lib/facts.ts b/solution/shared/src/lib/facts.ts index 8c7c14f..8558113 100644 --- a/solution/shared/src/lib/facts.ts +++ b/solution/shared/src/lib/facts.ts @@ -118,5 +118,7 @@ export function sanitizePayloadForPublish(payload: SitePayload): SitePayload { links: payload.links.filter((link) => link.confirmed), // 대체 텍스트 없는 이미지는 어차피 렌더하지 않는다 — 목록에서도 뺀다. media: payload.media.filter((item) => item.alt?.trim()), + // 재생 주소가 없는 곡은 버튼만 있고 소리가 없다. 목록에서 뺀다. + songs: (payload.songs ?? []).filter((song) => song.audioUrl?.trim()), }; } diff --git a/solution/shared/src/types/site-payload.ts b/solution/shared/src/types/site-payload.ts index 8b017f5..05bb1ff 100644 --- a/solution/shared/src/types/site-payload.ts +++ b/solution/shared/src/types/site-payload.ts @@ -41,6 +41,15 @@ export interface SitePayload { /** 주요 거점까지의 이동 시간. */ routes: RouteEntry[]; + /** + * 이 숙소의 노래. 발행할 때마다 한 곡 만든다(가사 Gemini → 작곡 Suno). + * + * ★ 발행이 노래를 기다리므로 발행본에는 보통 한 곡이 실려 있다. 그래도 빌 수 있다 — + * 키가 없거나(SUNO_API_KEY) 작곡이 실패하면 **노래 없이** 발행한다(발행을 막지는 않는다). + * 렌더러는 비면 플레이어를 아예 그리지 않는다. + */ + songs: SongTrack[]; + /** LLM 이 쓴 문장(allow_llm=true 필드). 사실이 아니라 문장이라 fact 와 분리한다. */ narrative: Narrative; @@ -323,6 +332,25 @@ export interface Narrative { summary?: string; } +export interface SongTrack { + songId: string; + title: string; + /** 가사. 화면에서 펼쳐 볼 수 있게 함께 싣는다 — 무슨 노래인지 읽히지 않으면 아무도 안 누른다. */ + lyrics?: string | null; + /** 장르·분위기(Suno 에 넘긴 값). */ + style?: string | null; + durationSec?: number | null; + /** + * 재생 주소. **우리 쪽 경로**다(`/s//<파일>`). + * + * ★ Suno 가 준 주소를 그대로 쓰지 않는다 — 만료되는 주소라, 발행 직후에는 재생되고 + * 몇 주 뒤 조용히 죽는다. 백엔드가 파일을 받아 두고 프리렌더가 사이트로 복사한다. + */ + audioUrl: string; + /** 프리렌더가 원본을 찾을 때 쓰는 파일명. 렌더러는 쓰지 않는다. */ + fileName?: string; +} + export interface SiteTheme { templateId: string; colors: { diff --git a/solution/site/scripts/mockup/.gitignore b/solution/site/scripts/mockup/.gitignore new file mode 100644 index 0000000..45d6234 --- /dev/null +++ b/solution/site/scripts/mockup/.gitignore @@ -0,0 +1,2 @@ +build/ +stay-payload-new.json diff --git a/solution/site/scripts/mockup/README.md b/solution/site/scripts/mockup/README.md new file mode 100644 index 0000000..b1d7784 --- /dev/null +++ b/solution/site/scripts/mockup/README.md @@ -0,0 +1,938 @@ +# `/s/stay` 시연본 — 지침서 한 장 + +> **이 문서 하나만 읽으면 된다.** 예전에는 README · SECTIONS · PROMPTS · REVIEW 로 흩어져 +> 있었고 무엇을 먼저 볼지 알 수 없었다. 전부 여기로 합쳤다(2026-09-11). +> 같은 폴더에 **생성물 두 개**가 더 있는데 지침이 아니라 **데이터**다 — +> `TEXT.md`(화면에 나가는 텍스트 전문, `python3 dump_text.py`)와 +> `SCHEDULE.md`(일정 21개 전수 검수표, `python3 audit_schedule.py`). + +대상 `https://web4ai.o2osolution.ai/s/stay` (스테이,머뭄 · 전북 군산) + +## 지금 누가 무엇을 하나 + +| | 하는 일 | 읽을 곳 | +|---|---|---| +| **사장님(민아)** | `/s/stay` 시연본 자체를 고친다 | **2부** | +| **나머지 팀원** | 시연본에 있는 것을 **제품이 자동으로 만들게** 옮긴다 | **1부** | + +시연본은 **payload 가 없는 목업**이다. 프리렌더가 굽지 않고 `index.html` 한 장을 손으로 +조립했다. 그래서 "화면에 있는 것"과 "제품이 만들 수 있는 것"이 갈라져 있고, +1부가 그 간격을 메우는 작업이다. + +--- +--- + +# 1부 — 실제 구현을 하는 사람 + +## 1.1 어떤 파일을 여나 (결론부터) + +**목업 폴더는 사양서다. 코드는 여기서 고친다.** + +### ① 계약 — 화면에 새 값을 내려면 여기부터 + +| 파일 | 무엇 | +|---|---| +| `solution/shared/src/types/site-payload.ts` | payload 필드 정의. `Narrative`(`:316`) · `MediaItem` · `Unit` | +| `solution/shared/src/lib/section-data.ts` | 섹션 봉투(`{kind,version,title,items[]}`) 타입 + `planDay()`. **21시 넘는 칸을 버리는 규칙이 여기 있다**(`:535`) | +| `solution/shared/src/lib/section-prompts.ts` | 프롬프트 — **서버 생성 잡이 쓰는 6종**(songs · daily · people · chronicle · postcard · quiz). 고친 뒤 `npm run export:prompts` | +| `solution/frontend/src/features/builder/canvas/dataSpec.ts` | 프롬프트 — **빌더 [콘텐츠] 탭에서 붙여넣는 9종**. 위 6종은 shared 를 import 하고, **video · event · itinerary 셋은 여기에만 있다**(`itinerary` 는 `:556`) | + +### ② 생성 — 값을 만드는 곳 + +| 파일 | 무엇 | 관련 할 일 | +|---|---|---| +| `solution/backend/services/site_payload.py` | 섹션 목록·기본 on/off(`:155` 부근) · payload 조립 | T1 | +| `solution/backend/services/itinerary.py` | 여행 일정 생성 | **T2** | +| `solution/backend/services/story_service.py` | 지역 이야기(노래·인물·연표·엽서·퀴즈) 생성 잡 | T3 T5 | +| `solution/backend/services/external/wikimedia.py` | 위키 사진 | T3 | +| `solution/backend/services/collector/` | 수집 어댑터 | T6 | + +### ③ 화면 — 발행본 렌더러 + +| 파일 | 무엇 | +|---|---| +| `solution/site/src/sections/` | 섹션 컴포넌트. 히어로는 `HeroPension.tsx`, 일정은 `items/ItinerarySection.tsx` | +| `solution/site/src/lib/ui/use-rail-autoplay.ts` | 카로셀 자동 넘김 정책 | + +### ④ 사양 — 이 목업 폴더에서 가져갈 것 + +| 파일 | 옮길 자리 | +|---|---| +| 이 문서 **1.4 프롬프트 전문** | `shared/src/lib/section-prompts.ts` | +| `build_itinerary.py` (규칙 16종) | `backend/services/itinerary.py` | +| `build_story.py` (위키 사진 수집) | `backend/services/external/wikimedia.py` | +| `inject.js` (렌더러에 **없는** 기능 3개) | `site/src/sections/` | + +--- + +## 1.2 섹션 22개 — 지금 상태 + +`할 일` 이 비면 제품이 **이미** 만들 수 있다. 손댈 필요 없다. + +| id | 이름 | 값이 오는 자리 | 지금 누가 만드나 | 할 일 | +|---|---|---|---|---| +| `hero` | 히어로 | `narrative` | 사장님이 적음 | **T1** | +| `intro` | 소개 | `narrative.about[]` | LLM(빌더) | | +| `rooms` | 객실 안내 | `units[]` + `media[]` | 사장님이 올림 | **T6** | +| `event` | 소식 | `data(kind=event)` | 사장님이 적음 | | +| `info` | 기본 정보 | `facts[]` | 사장님이 적음 | | +| `booking` | 실시간 예약 | — | (시연본은 꺼 둠) | | +| `video` | 영상 | `data(kind=video)` | 사장님이 적음 | | +| `photos` | 사진 갤러리 | `media[]` | 사장님이 올림 | | +| `map` | 오시는 길 | `place` + `routes[]` | 수집 | | +| `festival` | 계절별 축제 | `local.festivals` | TourAPI 수집 | | +| `local` | 지역 정보 | `local.attractions/restaurants` | TourAPI 수집 | | +| `itinerary` | **추천 일정** | `data(kind=itinerary)` | `services/itinerary.py` + 빌더 프롬프트 `dataSpec.ts:556` | **T2** | +| `story` | 군산 이야기 | 아래 6탭 껍데기 | — | | +| `songs` | 가요 다방 | `data(kind=songs)` | LLM `section-prompts.ts:67` | **T3 T5** | +| `daily` | 오늘의 한 장 | `data(kind=daily)` | LLM `:91` | 섹션 자리가 없다 | +| `people` | 인물 열전 | `data(kind=people)` | LLM `:113` | **T3 T5** | +| `chronicle` | 시간의 골목 | `data(kind=chronicle)` | LLM `:135` | | +| `postcard` | 오늘의 엽서 | `data(kind=postcard)` | LLM `:160` | | +| `quiz` | 뒤집어 보는 질문 | `data(kind=quiz)` | LLM `:188` | | +| `faq` | 자주 묻는 질문 | `faqs[]` | LLM + 사장님 | | +| `rules` | 이용 규정 | `facts[]` | 사장님이 적음 | | +| `weather` | 날씨 | `local.weather` | open-meteo | | +| `planner` | 계절별 추천 하루 | `data(kind=planner)` | **없음** | (시연본은 꺼 둠) | + +섹션이 아닌 것 하나가 더 있다 — 헤더 음악 플레이어(**T4**). + +--- + +## 1.3 할 일 여섯 + +### T1 — 히어로 캐치프레이즈 + +**증상** 히어로 문구가 사이트마다 한 줄로 고정이다. 계절도 날씨도 안 탄다. +계약(`site-payload.ts:316` `Narrative`)에 문자열 `tagline` 하나뿐이고, +히어로는 `tagline ?? heroSubline ?? summary` 를 그대로 찍는다(`HeroPension.tsx:125`). + +**할 일** + +1. `Narrative` 에 필드를 더한다. + ```ts + catchphrases?: { version: 1; items: { + text: string; // 25자 안쪽 + kind: 'general' | 'season' | 'month' | 'weather'; + season?: '봄'|'여름'|'가을'|'겨울'; month?: number; + weather?: '맑음'|'구름많음'|'흐림'|'비'|'눈'; + }[] }; + ``` +2. 히어로가 **지금 조건에 맞는 것만 후보**로 두고 돌린다. + 순환 순서는 `일반 → 계절 → 일반 → 월 → 일반 → 날씨` 한 바퀴를 만들고 그 바퀴를 돈다. + 일반은 자루에서 뽑아 쓰고 비면 다시 채운다(같은 문장이 연달아 안 나오게). + 계절은 3개월 단위(3~5 봄 · 6~8 여름 · 9~11 가을 · 12~2 겨울). + 날씨는 `/v1/local/weather` 를 10분마다 읽고, **못 받아 오면 날씨 문구를 순환에서 뺀다**. + 목업 구현은 `inject.js` 의 `startCatchphrase()` · `buildCycle()`. +3. 프롬프트는 **1.4 ①** 을 쓴다. 100개(일반 40 · 계절 24 · 월 24 · 날씨 12). +4. **`STORY_KINDS` 에 등록한다.** 빠뜨리면 서버 생성 잡이 이 종류를 아예 모른다 — + `daily` 가 그래서 한동안 빈칸이었다. + +**완료 기준** 새 업장 발행 시 문구 100개가 돌고, 9월에 겨울 문구가 안 나온다. + +--- + +### T2 — 추천 일정 ★ 가장 큰 일 + +**증상 ①** 서버 생성기(`services/itinerary.py`)는 하루를 **관광지 2 + 맛집 2** 로 짠다. +체류시간·이동시간·입퇴실·끼니 시각 개념이 없다. 그래서 나온 하루가 +"둘째 날 해산물축제 → 근대건축관" 으로 끝난다. + +**증상 ② 프롬프트가 두 곳으로 갈려 있다.** +일정 프롬프트는 `shared/src/lib/section-prompts.ts` 에 **없고** +`frontend/src/features/builder/canvas/dataSpec.ts:556` 에만 있다. +그 파일은 **빌더에서 붙여넣는 9종**을 담는데, 그중 `songs` 등 6종은 shared 를 import 하고 +**`video` · `event` · `itinerary` 셋은 자기 자리에 직접 적혀 있다.** +→ 서버 생성 잡(`story_service.py`)은 shared 만 읽으므로 **이 셋을 만들 줄 모른다.** +`daily` 가 빈칸이던 것과 **같은 종류의 사고**다. 옮기면서 shared 로 올린다. + +**증상 ③ 지금 프롬프트가 모델에게 시각 계산을 시킨다.** +`dataSpec.ts` 의 일정 규칙에 이런 줄이 있다 — "밤 9시를 넘기는 칸은 화면에서 빠진다 — +**시각을 계산해 보고 넣는다**", 그리고 `minutes` · `moveMinutes` 를 모델이 적게 한다. +모델은 못 한다. 이게 시연본 첫 판이 망가진 직접 원인이다. + +**★ 프롬프트 하나로 일정을 받지 않는다.** 모델은 시각 계산을 못 한다 — +이동시간을 지어내고, 점심을 09:40 에 넣고, 21시를 넘겨 **렌더러가 조용히 버리는** 칸을 +만든다(`shared/src/lib/section-data.ts:535`). 시연본 첫 판이 정확히 그랬다. + +**세 단계로 나눈다.** + +| 단계 | 누가 | 무엇을 내놓나 | 프롬프트 | +|---|---|---|---| +| **① 장소 대장** | LLM | 장소 30곳 × `{stayMinutes:{min,base,max}, todo, indoor, needsCar, bestTime, searchQuery}` | **1.4 ②** | +| **② 좌표** | 코드 | 카카오 로컬로 `searchQuery` → 위경도. **LLM 이 적은 좌표는 쓰지 않는다** | — | +| **③ 조립** | **코드** | 이동시간·도착시각·끼니 창 맞추기·규칙 16종 감사 | 없음 (`build_itinerary.py`) | +| **④ 테마** | LLM | 테마 20개 × `{name, duration, audience, why, days[{kind, places[]}]}` | **1.4 ③** | + +**코드가 하는 일(③)에서 반드시 옮겨야 하는 것** — 근거는 `build_itinerary.py` 줄번호 + +| | 무엇 | 어디 | +|---|---|---| +| 이동시간 | 좌표 계산. 도보 4km/h(≤1.5km), 그 위는 차 25km/h + 주차 5분, 최소 3분 | `move_minutes()` | +| 숙소 고정 | 모든 일정이 **숙소에서 출발해 숙소로 복귀**. 첫 칸·마지막 칸이 숙소 | `:108` `:182` | +| 시각 창 | 입실 15:00+ · 퇴실 11:00− · 점심 11:30–13:30 · 늦은 점심 13:00–15:00 · 저녁 17:30–20:00 · 밤산책 18:30–20:00 | `:156` `:166` | +| 창 맞추기 | 어긋나면 **앞 칸 체류를 범위(min~max) 안에서** 늘이고 줄인다. 이동시간을 고치지 않는다. 그래도 안 되면 시작 시각을 10분씩 옮기고, 그래도 안 되면 **그 조합을 만들지 않는다** | `plan_day()` | +| 테마별 완급 | 시작 시각·체류 배율이 테마마다 다르다(쉬러 오면 1.25배, 사진 찍으면 0.8배) → 끝나는 시각이 13:35~21:00 로 퍼진다 | `:470` | +| 끼니 간격 | 앞 끼니가 끝나고 **3시간**이 지나야 다음 끼니 창이 열린다(`MEAL_GAP`). 창만 지키면 통과하던 자리다 | `_window_of()` | +| **감사 16종** | 하나라도 어기면 **payload 를 쓰지 않고 멈춘다** | `:526` · 목록은 **2.4** | + +★ **감사는 생성기 안에 둔다.** 밖에 두면 뼈대를 고칠 때마다 "이번에 안 본 규칙"이 생긴다 — +실제로 그래서 33건이 통과했다(**3.2**). + +**하루 뼈대** — `@` 는 숙소 + +``` +첫날 체크인@ · 오후 · 오후 · 저녁 · 밤산책 · 복귀@ +가운데날 아침@ · 오전 · 점심 · 오후 · 오후 · 쉼@ · 저녁 · 복귀@ +쉬는날 숙소에서 쉬는 날@ · 점심 · 쉼@ · 저녁 · 복귀@ +마지막날 아침(퇴실)@ · 오전 · 점심 · 오후 · 마무리@ +``` + +**완료 기준** 새 업장에서 테마 20개가 나오고 `audit()` 위반 0건. +하루가 숙소로 시작해 숙소로 끝나고, 끼니가 제 시각에 있고, 21시를 안 넘는다. + +--- + +### T3 — 인물·노래 사진 + +**증상** 프롬프트가 사진 URL 을 금지하고 `imageQuery` 만 받는다(`section-prompts.ts:128`). +초상권·저작권 때문이고 그 판단은 맞다. 그런데 **다음 단계가 없어서** 인물 열전이 활자만으로 선다. + +**할 일** + +1. `services/external/wikimedia.py` 에 **문서 → `pageimages` → `imageinfo.extmetadata`** 경로를 붙인다. +2. ★ **이름으로 커먼즈를 검색하지 않는다.** 동명이인이 온다 — 실측(2026-09-11): + 이수현(독립운동가)→걸그룹, 은성수(금융위원장)→축구선수, 박성현(양궁)→골퍼, + 전진(비전향 장기수)→아이돌, 이길여→건물. **틀린 사진은 없는 것만 못하다.** + 그 사람의 **위키백과 문서가 대표로 쓰는 사진**만 믿는다. 구현은 `build_story.py`. +3. 저작자·라이선스를 `imageCredit` 으로 payload 에 넣는다. + ★ **문자열이다**(`site/src/sections/items/common.tsx:206`). 객체로 넣으면 렌더러가 + 조용히 아무것도 안 그린다 — CC BY-SA 는 출처 표시가 **라이선스 조건**이라 그건 침해다. +4. 사진 있는 사람을 **앞으로** 정렬한다. 빈 카드가 먼저 오면 목록이 비어 보인다. + +**완료 기준** 인물 카드에 사진과 저작자 줄이 같이 뜬다. 시연본 실적 57명 중 18명. + +--- + +### T4 — 음악 플레이어 + +**증상** `media` 에 오디오 종류가 없다. 목업은 mp3 를 `/s/stay/audio/` 에 **직접 넣었다**. + +**할 일** `MediaItem` 에 `kind:'audio'`(+`title` `artist` `durationSec`)를 넣고 발행 때 같이 올린다. +헤더에 붙는 작은 플레이어를 만든다 — 아이콘 · 현재 곡명 · 재생/멈춤 · 목록. +**자동재생은 넣지 않는다**(2026-09-11 지시). 순서는 **랜덤 시작 후 순차**. +목업 구현은 `inject.js` 의 `startPlayer()`, 모양은 `inject.css` 의 `#w4d-mini` · `#w4d-panel`. + +--- + +### T5 — 상한(`maxItems`) + +| 종류 | 지금 | 시연본 | 왜 | +|---|---|---|---| +| songs | 8 | 25 | 8곡이면 '다방'이 아니라 목록이다 | +| people | 10 | 57 | 위키백과 '군산시 출신' 분류만으로 57명이 확인된다 | + +★ **한 번에 다 받지 않는다.** 25곡을 한 번에 시키면 뒤쪽이 급격히 부실해진다. +15곡씩 나눠 받고 제목으로 중복을 지운다 — 나누는 문구는 **1.4 ④** 에 있다. +★ `maxItems` 와 프롬프트 안의 숫자를 **같이** 바꾼다. 어긋나면 받아 놓고 잘라 버린다. + +--- + +### T6 — 객실 사진 출처 + +시연본은 야놀자 등록본(`nol.yanolja.com/stay/domestic/10068088`)의 Next.js 플라이트 데이터에서 +`roomTypes[].photos` 를 파싱해 A동 12·B동 10 을 가져왔다(`rooms.json`). +**손으로 한 것이고 제품 경로가 아니다.** + +수집 어댑터를 만들려면 [docs/DECISIONS.md](../../../../docs/DECISIONS.md) 1-1 과 +[docs/DATA_SOURCE_RESEARCH.md](../../../../docs/DATA_SOURCE_RESEARCH.md) 를 먼저 읽는다 — +**봇 탐지 우회는 결론과 무관하게 영구 금지**다. +현실적인 경로는 **사장님이 올리는 것**이거나 **업소가 준 채널의 공개 API** 다. + +★ 사진을 눈으로 보고 세지 않는다. 시연본에서 A/B 를 눈으로 배정했다가 11/5 가 나왔고 +실제는 12/10 이었다. 등록본 데이터를 쓴다. + +--- + +## 1.4 프롬프트 전문 + +**프롬프트는 지금 두 파일에 나뉘어 있다. 이게 문제다.** + +| 파일 | 담는 것 | 누가 읽나 | +|---|---|---| +| `shared/src/lib/section-prompts.ts` | songs · daily · people · chronicle · postcard · quiz | 빌더 **+ 서버 생성 잡** | +| `frontend/.../canvas/dataSpec.ts` | 위 6종(shared 를 import) **+ video · event · itinerary** | 빌더만 | + +→ **`video` · `event` · `itinerary` 는 서버가 만들 줄 모른다.** 옮기면서 shared 로 올린다. + +**단일 출처는 `solution/shared/src/lib/section-prompts.ts` 다.** +사장님이 [콘텐츠] 탭에서 복사해 가는 프롬프트와, 서버 생성 잡(`services/story_service.py`)이 +**같은 문장**을 써야 한다. 두 벌로 두면 "빌더에서 뽑은 것과 서버가 채운 것의 모양이 다르다"가 +조용히 생긴다. + +``` +shared/src/lib/section-prompts.ts ← 여기만 고친다 + │ npm run export:prompts + ▼ +backend/services/prompts/section_prompts.json ← 산출물. 손으로 고치지 않는다 +``` + +### 공통 규칙 (모든 프롬프트 뒤에 붙는다) + +``` +[공통 규칙] +1. JSON 하나만 출력한다. 인사말·설명·코드펜스를 붙이지 않는다. +2. 확인되지 않은 값은 필드를 통째로 뺀다. 빈 문자열로 채우거나 "미상" 이라고 쓰지 않는다. +3. source.url 은 실제로 열리는 공식·기관·언론 페이지여야 한다. 검색 결과 주소는 쓰지 않는다. +4. 근거가 확실하면 verified 를 "확인", 애매하면 "확인필요" 로 적는다. +5. 가사·시·소설의 원문을 한 줄도 옮기지 않는다. 제목과 배경만 쓴다. +6. 설명 문장은 항목당 두 문장을 넘기지 않는다. +7. 이미지 주소는 만들지 않는다. 필요하면 imageQuery 에 검색어만 적는다. +``` + +★ 2번과 3번이 이 레포의 절대규칙을 프롬프트로 옮긴 것이다. 모델이 이걸 어기면 검증기가 +뒤에서 걸러야 하는데, 걸러진 항목은 결국 화면에서 빈자리가 된다. + +--- + +### ① 히어로 캐치프레이즈 (T1 · 신규) + +``` +너는 숙소의 카피라이터다. 아래 조건에 맞는 JSON 하나만 출력한다. + +[업소] {상호} ({업종}) +[지역] {지명} +[업소가 가진 사실] +{fact 목록을 한 줄씩. 예: 1920년대 적산가옥 · 독채 2동 · 최대 4인 · 마당과 정원 · + 창고형 카페 공간 · 침구 매일 세탁 · 히로쓰 가옥 옆 · 초원사진관 도보 3분} + +[해야 할 일] +[업소] 의 히어로 문구를 100개 만든다. 상호 바로 아래에 한 줄로 뜨고 7초마다 바뀐다. +네 종류로 나눠 만든다. + +· 일반 40개 — 언제 나와도 되는 문구 +· 계절 24개 — 봄·여름·가을·겨울 각 6개 (3개월 단위: 3~5 봄 / 6~8 여름 / 9~11 가을 / 12~2 겨울) +· 월별 24개 — 1월부터 12월까지 각 2개 +· 날씨 12개 — 맑음 3 · 구름많음 2 · 흐림 2 · 비 3 · 눈 2 + +[스키마] +{ "kind":"catchphrase", "version":1, "items":[ + { "text":"한 줄", "kind":"general|season|month|weather", + "season":"봄|여름|가을|겨울", // kind 가 season 일 때만 + "month":9, // kind 가 month 일 때만 (1~12) + "weather":"맑음|구름많음|흐림|비|눈" } // kind 가 weather 일 때만 +] } + +[이 아이템만의 규칙] +· 한 줄은 25자 안쪽이다. 두 문장으로 쓰지 않는다. +· **[업소가 가진 사실] 안에서만 쓴다.** 거기 없는 시설·거리·연도를 지어내지 않는다. +· 느낌표와 이모지를 쓰지 않는다. "최고" "완벽" "힐링" 같은 광고 단어를 쓰지 않는다. +· 상호를 문구에 넣지 않는다 — 바로 위에 이미 크게 떠 있다. +· 같은 사실을 두 번 쓰지 않는다. 100개가 다 다른 것을 말해야 한다. +· 계절 문구는 그 계절에만 맞는 말이어야 한다. "좋습니다" 처럼 아무 때나 되는 말은 일반으로 보낸다. +· 날씨 문구는 그 날씨일 때 손님이 **무엇을 할 수 있는지**를 말한다. 날씨 묘사만 하지 않는다. +· 월별 문구는 그 달의 날씨·행사·빛이 근거여야 한다. 숫자만 바꾼 문구를 12번 쓰지 않는다. +· source 와 verified 는 없다. 사실이 아니라 문구이고, 근거는 [업소가 가진 사실] 이다. +``` + +**잘 나온 예** + +| 종류 | 문구 | 근거가 된 사실 | +|---|---|---| +| 일반 | 담 너머는 히로쓰 가옥입니다 | 위치 | +| 일반 | 침구는 매일 새것처럼 나갑니다 | 매일 세탁·살균 | +| 계절(가을) | 기와 위로 가을 볕이 마릅니다 | 기와 지붕 | +| 월(9월) | 가을이 담을 넘어오는 구월입니다 | 담·계절 | +| 날씨(비) | 카페 공간에서 빗소리를 들으실 수 있습니다 | 창고형 카페 | + +**받은 뒤 검사할 것** + +- [ ] 100개인가. 종류별 40/24/24/12 인가 +- [ ] 25자를 넘는 것이 있는가 +- [ ] [업소가 가진 사실] 에 없는 말이 있는가 ← **이게 제일 많이 틀린다** +- [ ] 계절 문구가 그 계절에만 맞는가 +- [ ] 같은 말이 두 번 나오는가 + +--- + +### ② 여행 일정 · 장소 대장 (T2 ① · `dataSpec.ts:556` 의 일정 프롬프트를 **대체**한다) + +``` +너는 지역 여행 코디네이터다. 아래 조건에 맞는 JSON 하나만 출력한다. + +[업소] {상호} ({업종}) +[지역] {지명} +[좌표] {위도},{경도} + +[해야 할 일] +[업소] 에서 출발해 당일로 다녀올 수 있는 장소를 30곳까지 찾는다. +관광지·박물관·골목·바다·절 같은 볼거리 20곳과, 끼니를 해결할 곳 10곳을 섞는다. +[업소] 에서 30km 안쪽만 고른다. + +[스키마] +{ "kind":"places", "version":1, "items":[ + { "name":"장소 이름", + "searchQuery":"지도에서 검색할 말", + "category":"볼거리|식당|카페", + "stayMinutes":{"min":20,"base":30,"max":45}, + "todo":"거기서 무엇을 하는지 한 문장", + "indoor":true, + "needsCar":false, + "bestTime":"오전|오후|저녁|밤|상관없음", + "verified":"확인|확인필요", + "source":{"name":"출처명","url":"https://..."} } ] } + +[이 아이템만의 규칙] +· **todo 는 장소 설명이 아니라 할 일이다.** + ✗ "1899년에 지어진 근대 건축물입니다" + ✓ "옛 조선은행 금고와 은행 창구를 봅니다. 2층 전시까지 보면 45분입니다." + 손님이 거기서 무엇을 하는지 모르면 그 줄은 쓸모가 없다. +· stayMinutes 는 **실제로 걸리는 시간**이다. min 은 빨리 보고 나오는 사람, max 는 천천히 보는 사람. + 박물관 60~120분, 골목 15~40분, 식당 40~70분이 기준이다. 무엇이든 90분으로 적지 않는다. +· 식당은 **무엇을 먹는지**까지 적는다. "맛집입니다" 로는 무엇을 시킬지 모른다. +· indoor 는 비 오는 날 갈 수 있는 곳만 true 다. 처마 밑·실내 전시가 여기 들어간다. +· needsCar 는 [업소] 에서 걸어서 못 가는 곳(1.5km 초과)이면 true 다. +· 좌표는 적지 않는다. 코드가 카카오 로컬에서 받는다 — 지어낸 좌표는 지도에 엉뚱한 핀을 찍는다. +· 영업시간·휴무일이 확인되면 source 에 그 페이지를 단다. +· **[업소]에서 40분 넘게 걸리는 곳을 넣었다면, 그 근처 식당도 하나 이상 넣는다.** + 멀리 다녀온 뒤 시내까지 돌아와 밥을 먹으면 점심이 오후 3시가 된다 — + 시연본에서 실제로 그랬다(섬에서 88분 달려와 15:47 에 늦은 점심). +· 끼니 장소는 **한 끼 40~70분**이다. 여기에 이동시간을 얹는 것은 코드가 한다. +``` + +--- + +### ③ 여행 일정 · 테마 짓기 (T2 ④ · 신규 · ②와 짝) + +``` +[해야 할 일] +[장소 대장] 을 재료로 1박 2일 10개, 2박 3일 10개의 테마를 만든다. +테마는 **손님의 성격**으로 가른다 — 차가 있는지, 걷기를 좋아하는지, 비가 오는지, +아이가 있는지, 사진을 찍는지, 쉬러 오는지. + +[스키마] +{ "kind":"themes", "version":1, "items":[ + { "name":"테마 이름", "duration":"1박 2일|2박 3일", + "audience":"누구에게", "why":"왜 이 순서인지 한두 문장", + "days":[ { "kind":"first|mid|mid_rest|mid_island|last", "places":["장소명","장소명"] } ] } ] } + +[이 아이템만의 규칙] +· places 는 [장소 대장] 에 있는 이름만 쓴다. 없는 곳을 적으면 그 테마는 버려진다. +· 뼈대의 자리 수와 places 개수가 같아야 한다. +· 끼니 자리에는 category 가 식당·카페인 곳만 넣는다. +· **한 테마 안에서 같은 곳을 두 번 넣지 않는다.** +· 테마 이름에 "최고" "완벽" 을 쓰지 않는다. 무엇을 하는 일정인지가 이름이다. +· 20개 중 하나는 **숙소에서 거의 나가지 않는 날**을 포함한다. +``` + +--- + +### ④ 가요 다방 (T5 · 기존 8곡 → 50곡) + +``` +[해야 할 일] +[지역]을 노래한 대중가요를 찾아 아래 JSON 으로 정리한다. +이번에는 {N}번째부터 {N+14}번째까지, 15곡을 낸다. +이미 받은 곡은 아래에 있다 — 같은 곡을 다시 내지 않는다. +[이미 받은 곡] {곡명 목록} + +지명·항구·강·다리가 제목이나 배경에 나오는 곡을 고른다. +1960~80년대 곡을 먼저 찾고, 다 떨어지면 지자체가 만든 곡·지역 가수의 곡으로 넓힌다. + +[스키마] +{ "kind":"songs", "version":1, "title":"가요 다방", "items":[ + { "title":"곡명", "artist":"가수", "lyricist":"작사", "composer":"작곡", + "year":1966, "label":"음반사", "labelColor":"#d4551f", + "story":"곡의 배경 (두 문장 이내, 가사 없이)", + "verified":"확인|확인필요", + "source":{"name":"출처명","url":"https://..."} } ] } + +[이 아이템만의 규칙] +· lyrics 필드는 스키마에 없다. 어떤 이유로도 만들지 마라. 가사 한 소절도 안 된다. +· **곡이 실재하는지 출처로 확인한다.** 지역 노래 목록·언론 기사·지자체 음반 안내가 근거다. + 검색으로 못 찾으면 그 곡을 내지 않는다 — 15곡을 억지로 채우지 않는다. +· labelColor 는 레코드 라벨 색이다. 곡의 분위기에 맞춰 진한 색 하나를 hex 로 고른다. +· 작사·작곡·발표연도를 모르면 그 필드를 뺀다. "미상" 이라고 쓰지 않는다. +· 유튜브 영상 ID 를 적지 않는다 — 코드가 검색 주소를 만든다. 틀린 영상이 붙는 것보다 낫다. +· 숙소가 직접 만든 곡은 여기 넣지 않는다. 이 섹션은 **도시의 노래**를 모으는 자리다. +``` + +★ 시연본은 25곡에서 멈췄다. 출처에서 확인되는 군산 곡이 거기까지였다. +**모자라면 모자란 채로 내는 것이 맞다** — 지어낸 곡은 아는 사람이 바로 알아본다. + +--- + +### ⑤ 인물 열전 (T3 T5 · 기존 10명 → 50명) + +``` +[해야 할 일] +[지역] 출신이거나 [지역]과 깊이 얽힌 인물을 50명까지 찾는다. +문학·음악·미술·연기·체육·정치·학문·기업을 고루 섞는다. 한 분야가 절반을 넘지 않게 한다. +생존 인물은 공개된 활동 사실만 쓴다. + +[스키마] +{ "kind":"people", "version":1, "title":"인물 열전", "items":[ + { "name":"이름", "aka":"호·예명", "years":"1902–1950", "role":"소설가", + "oneLine":"한 문장 소개", "imageQuery":"사진 검색어", + "verified":"확인|확인필요", + "source":{"name":"출처명","url":"https://..."} } ] } + +[이 아이템만의 규칙] +· **source.url 은 그 사람의 문서**여야 한다(위키백과 문서, 기관 소개 페이지). + "OO시 출신 인물 목록" 같은 목록 페이지는 근거가 아니다. +· oneLine 은 **그 사람이 무엇을 한 사람인지**다. "군산 출신입니다" 는 이름 아래 이미 있다. +· "~ 출신으로 알려진" 처럼 근거가 전언뿐이면 verified 를 "확인필요" 로 한다. +· 생존 인물의 가족·거주지·건강·재산 같은 사생활은 쓰지 않는다. +· 사진 URL 을 넣지 않는다. imageQuery 만 넣는다 — 초상권과 저작권은 사람이 확인한다. +· years 를 모르면 뺀다. +``` + +★ **사진은 프롬프트로 받지 않는다.** T3 참조 — 위키백과 **문서 대표 사진**만 받고 +라이선스가 확인된 것만 쓴다. + +--- + +### ⑥ 오늘의 엽서 (기존 개선 — 링크가 붙게) + +``` +[해야 할 일] +[지역]에 대해 손님이 자기 SNS 에 그대로 붙여 쓸 만한 한 문장을 12개 쓴다. +사실 하나가 반드시 들어가되, 설명하지 말고 툭 던지는 문장으로 쓴다. + +[스키마] +{ "kind":"postcard", "version":1, "title":"오늘의 엽서", "items":[ + { "line":"한 문장", "hashtags":["#태그"], + "place":"그 문장이 가리키는 장소 이름", + "searchQuery":"지도에서 검색할 말", + "postmark":"소인에 찍을 짧은 지명", + "verified":"확인|확인필요", + "source":{"name":"출처명","url":"https://..."} } ] } + +[이 아이템만의 규칙] +· 한 문장은 40자 안쪽이다. 두 문장으로 쓰지 않는다. +· 느낌표와 이모지를 쓰지 않는다. 광고 문구처럼 들리면 실패다. +· 해시태그는 3개까지. 지역명 하나는 반드시 넣는다. +· **같은 대상을 두 번 쓰지 않는다.** 열두 장이 전부 다른 곳·다른 이야기여야 한다. +· 운영시간·휴관일·주소·요금은 엽서에 적지 않는다. 그건 이용 정보지 엽서 문장이 아니다. +· **place 와 searchQuery 를 반드시 채운다.** 화면이 그 값으로 지도 링크를 건다. + postmark 는 도장 문구라 지명이 아닐 수 있다 — 그걸로 지도를 열면 엉뚱한 데가 나온다. +``` + +★ 시연본에서 실제로 그랬다. 소인 글자(`군산 內港`)로 링크를 만들었더니 +`군산 군산` 같은 검색어가 나왔다. `place` 를 따로 받는 이유다. + +--- + +## 1.5 프롬프트를 고칠 때 지킬 것 + +1. **`STORY_KINDS` 에 등록한다.** 빠지면 서버 생성 잡이 그 종류를 모른다. +2. **`maxItems` 와 프롬프트 안의 숫자를 같이 바꾼다.** 어긋나면 받아 놓고 잘라 버린다. +3. **개수를 억지로 채우게 하지 않는다.** "확실한 것이 12개면 12개만 낸다" 를 규칙에 넣는다. + 채우라고 하면 채운다 — 지어내서. +4. **계산은 시키지 않는다.** 시각·거리·요금은 코드가 한다. 모델은 자신 있게 틀린 숫자를 준다. +5. **"설명" 이 아니라 "할 일" 을 시킨다.** 손님이 그 자리에서 무엇을 하는지가 콘텐츠다. +6. **출처의 종류를 지정한다.** "출처를 달아라" 로는 목록 페이지가 온다. +7. 고친 뒤 **`npm run export:prompts`** 를 돌린다. 안 돌리면 서버는 옛 프롬프트로 돈다. + +--- +--- + +# 2부 — 목업을 고치는 사람 + +## 2.1 무엇을 고치는 건가 + +`/s/stay` 는 **payload 가 없는 목업**이다(`stay2` · `stay3` 도 같다). +프리렌더는 payload 를 받은 사이트만 굽는다 — 이 사이트는 **재굽기 대상이 아니고**, +고칠 자리는 이미 구워져 있는 `index.html` 하나이고 그 파일이 유일본이다. + +그래서 손으로 열어 고치지 않는다. 스크립트가 **원본 + 바꿀 것 → 새 index.html** 을 만든다. +두 번째로 고칠 때 "무엇을 왜 바꿨는지"가 파일이 아니라 코드와 이 문서에 남아야 하기 때문이다. + +``` +stay-payload.json 원본 payload (건드리지 않는다 · 되돌릴 때 기준) +orig/index.html 원본 HTML (건드리지 않는다) + │ + ├─ build_itinerary.py 여행 일정을 계산해 넣는다 (+ 규칙 16종 감사) + ├─ build_story.py 가요 다방 · 인물 열전을 넣는다 + └─ patch_stay.py 캐치프레이즈 · 객실 사진 · 자작곡 · 주입 CSS/JS · 자산 경로 교정 + │ + ▼ + build/index.html ← 배포하는 파일 (git 이그노어) +``` + +★ **렌더러(`solution/site/src`)를 고치지 않는다.** 고치면 번들 해시가 바뀌어 +전 사이트 재굽기 + 전체 재업로드가 따라온다. 목업에만 필요한 것은 `inject.css`/`inject.js` 로 덮는다. + +--- + +## 2.2 굽기 · 배포 + +### 다른 기계에서 처음 띄울 때 + +```bash +git clone && cd o2o-site-AEO +cp .env.example .env && cp nginx/site.conf.example nginx/site.conf +docker compose up -d + +cd solution/site/scripts/mockup +python3 patch_stay.py + +C=o2o-web4ai-solution-prerender +docker exec $C mkdir -p /app/solution/site/out/s/stay/img/people \ + /app/solution/site/out/s/stay/img/mirror /app/solution/site/out/s/stay/audio \ + /app/solution/site/out/s/stay/vendor +docker cp build/index.html $C:/app/solution/site/out/s/stay/index.html +docker cp img/. $C:/app/solution/site/out/s/stay/img/ +docker cp audio/. $C:/app/solution/site/out/s/stay/audio/ +docker cp vendor/. $C:/app/solution/site/out/s/stay/vendor/ + +node audit-all.mjs +``` + +→ `http://localhost/s/stay` + +★ **이 시연본은 레포 안에서 자급자족한다.** 번들 css/js 와 미러 사진 61장까지 +`vendor/` · `img/mirror/` 에 들어 있고, `patch_stay.py` 가 경로를 `/s/stay/…` 로 바꿔 박는다. +그 기계에서 구운 번들 해시와 **무관하게** 뜬다. +(2026-09-11 이전 판은 `/assets/…` 를 가리켜 다른 기계에서 흰 화면이었다.) + +### 내용을 바꾼 뒤 (전체 굽기) + +```bash +python3 build_itinerary.py && python3 build_story.py && python3 patch_stay.py +``` + +**순서를 지킨다.** `build_itinerary.py` 가 `stay-payload.json`(원본)을 읽어 +`stay-payload-new.json` 을 만들고, 뒤 둘이 그 파일을 이어서 고친다. +순서를 바꾸면 앞 단계 결과가 지워진다. + +로컬 반영 · 검사: + +```bash +docker cp build/index.html o2o-web4ai-solution-prerender:/app/solution/site/out/s/stay/index.html +node audit-all.mjs # 34개 항목 +node rails-test.mjs # 카로셀 13개 +``` + +`/s/` 는 **5분 캐시**(`max-age=300`)다 — 확인할 때 ⌘⇧R. + +### 킹서버 + +```bash +tar czf /tmp/stay-bundle.tgz -C . build/index.html img audio vendor +scp -C /tmp/stay-bundle.tgz King_admin:~/data2/o2o-site-AEO/tmp-stay-deploy/ +ssh King_admin "cd ~/data2/o2o-site-AEO/tmp-stay-deploy && rm -rf build img audio vendor && tar xzf stay-bundle.tgz +C=o2o-web4ai-solution-prerender +docker exec \$C mkdir -p /app/solution/site/out/s/stay/img/people /app/solution/site/out/s/stay/img/mirror \ + /app/solution/site/out/s/stay/audio /app/solution/site/out/s/stay/vendor +docker cp build/index.html \$C:/app/solution/site/out/s/stay/index.html +docker cp img/. \$C:/app/solution/site/out/s/stay/img/ +docker cp audio/. \$C:/app/solution/site/out/s/stay/audio/ +docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/" +``` + +확인: `md5 -q build/index.html` 과 +`ssh King_admin "docker exec o2o-web4ai-solution-site md5sum /srv/sites/s/stay/index.html"` 이 같아야 한다. +그다음 `node audit-all.mjs https://web4ai.o2osolution.ai/s/stay`. + +되돌리기: 컨테이너 안에 `index.html.bak-20260910` 이 있다. + +--- + +## 2.3 무엇을 어디서 고치나 + +### 히어로 문구 100개 → `patch_stay.py` + +| 자리 | 줄 | 무엇 | +|---|---|---| +| `GENERAL` | `:20` | 일반 40개 | +| `SEASON` | `:62` | 계절별 24개 — 봄/여름/가을/겨울 각 6 | +| `MONTH` | `:72` | 월별 24개 — 1~12월 각 2 | +| `WEATHER` | `:87` | 날씨별 12개 — 맑음/구름많음/흐림/비/눈 | + +문장만 더하거나 지우면 된다. 개수 제한은 없다. +순환 순서·계절 구분·날씨 연동은 **1.3 T1** 에 적어 둔 그대로다(`inject.js`). + +### 여행 일정 → `build_itinerary.py` + +**사람은 "어느 자리에 어느 장소" 만 고르고, 시각은 스크립트가 계산한다.** + +| 자리 | 줄 | 무엇 | +|---|---|---| +| `PLACES` | `:40` | **장소 대장** — 좌표 · 지도 검색어 · 체류시간(min/base/max) · **거기서 무엇을 하는지** | +| `STAY_ACTION` | `:108` | 머뭄 칸의 행위 — 체크인 · 아침 · 아침(퇴실) · 쉼 · 쉬는 날 · 머뭄으로 · 마무리 | +| `WINDOW` | `:156` | 시간대가 정해진 자리 | +| `SKELETON` | `:182` | 하루 뼈대. `@` 가 붙은 것은 머뭄 | +| `THEMES` | `:368` | 테마 21개 | +| `PACING` | `:470` | 테마별 시작 시각과 체류 배율 | + +**새 장소** → `PLACES` 에 한 줄. 좌표는 카카오 로컬에서 받은 실제 값이어야 한다(지어내지 않는다). +그다음 `THEMES` 의 원하는 자리에 이름을 적는다. +**새 테마** → `THEMES` 에 한 덩이, `PACING` 에 한 줄. 뼈대의 `@` 없는 자리 수와 장소 개수가 +같아야 한다 — 틀리면 빌드가 멈추고 알려 준다. + +### 가요 다방 · 인물 열전 → `build_story.py` + +| 자리 | 줄 | 무엇 | +|---|---|---| +| `SONGS` | `:28` | 25곡 — 곡명·가수·작사·작곡·연도·라벨색·한 줄 | +| `PEOPLE` | `:109` | 57명 — 위키 문서 제목 · 표시 이름 · 연도 · 하는 일 · 한 줄 | + +- 연도·작사·작곡을 모르면 **비운다**("미상"이라고 적지 않는다 — 렌더러가 없는 칸을 안 그린다). +- 듣기 링크는 **유튜브 검색 주소**다. 영상 ID 를 박으면 틀린 영상이 붙을 수 있다. +- 인물 사진은 두 곳 — ① `people-photos.json`(위키 문서 대표 사진 17장, 라이선스 확인분) + ② 원본 payload 에 있던 미러 사진(채만식). 새로 가져오는 게 아니라 자리로 돌리는 것이다. +- ★ 이름만으로 커먼즈를 검색하지 않는다(**1.3 T3**). +- 숙소가 만든 곡은 이 섹션에 넣지 않는다. 도시의 노래를 모으는 자리다. + +### 객실 사진 → `patch_stay.py` `ROOM_PHOTOS`(`:142`) · `rooms.json` + +--- + +## 2.4 규칙 16종 — 어기면 빌드가 멈춘다 + +`build_itinerary.py` 의 `audit()`(`:526`)이 매 실행 검사한다. +하나라도 어기면 **payload 파일을 쓰지 않고** 위반 목록을 찍고 멈춘다. + +| | 규칙 | 이 규칙이 없어서 났던 일 | +|---|---|---| +| 1 | 하루의 첫 칸·마지막 칸은 스테이 머뭄 | 출발지가 화면에서 사라졌다 | +| 2 | **같은 자리가 연달아 서지 않는다** | `아침 · 머뭄` 다음이 `체크아웃 · 머뭄` | +| 3 | 가운데 머뭄 칸은 '쉼'·'쉬는 날' 뿐 | 마지막 날 머뭄이 3번 | +| 4 | 이동시간 = 좌표 계산값 | 담 하나 건넌 곳 8분, 45분 걸리는 섬 10분 | +| 5 | 입실·점심·늦은 점심·저녁·밤 산책 시간대 | 점심 09:40, 저녁 13:09 | +| 6 | 퇴실 11:00 전 | 체크아웃 11:35 | +| 7 | 21:00 초과 금지 | 렌더러가 그 칸을 **조용히 버린다** | +| 8 | 끼니 자리에는 먹는 곳만 | | +| 9 | 같은 날 같은 식당 두 번 금지 | | +| 10 | 한 테마 안에서 같은 곳 반복 금지 | 같은 테마에 한일옥이 두 번 | +| 11 | "비가 와도 되는" 테마는 실내만 | 비 코스에 야외 두 곳 | +| 12 | "걸어서만/차 없이" 테마에 차 필요한 곳 금지 | | +| 13 | 1박 2일 = 2일, 2박 3일 = 3일 | | +| 14 | 아이 동반 테마에 술집 금지 | | +| **15** | **끼니 사이 3시간 이상** | 늦은 점심 15:47 종료 → 저녁 17:30. 창은 둘 다 합법인데 배가 안 꺼진다 | +| **16** | **볼거리는 편도 이동보다 오래 머문다**(끼니 제외) | 사장님 지적 — 밥집은 목적지가 아니라 돌아오는 길에 들르는 자리라 뺀다 | + +★ **왜 빌드 안인가** — 전에는 이 검사를 매번 따로 쓴 일회용 스크립트로 돌렸다. +그래서 뼈대를 고칠 때마다 "이번에 안 본 규칙"이 생겼고 사장님이 화면에서 먼저 찾으셨다. +**검사가 빌드의 일부가 아니면 검사한 적 없는 것과 같다.** + +`audit-all.mjs` 는 그다음 단계다 — 구워진 HTML 을 **실제 브라우저로 열어** 34개 항목을 센다. + +--- + +## 2.5 주입분(`inject.js` · `inject.css`) 다섯 + +렌더러에 없어서 이 목업에만 얹은 것이다. 제품으로 옮길 자리는 1부에 적었다. + +1. **캐치프레이즈 순환** — React 가 그린 `

` 를 찾아 단어 단위로 갈아 끼운다 → T1 +2. **헤더 미니 플레이어** — 카세트 아이콘 + 재생/멈춤 + 목록. 자작곡 5곡 → T4 +3. **링크 걸기** — 일정 정거장 187개 · 엽서 4개를 네이버 지도로 +4. **사진 저작자 표시** — 이 목업이 물고 있는 번들이 옛 판이라 `imageCredit` 을 안 그린다 → T3 +5. **카로셀 제어** — 아래 + +### 카로셀 제어 (`tameRails`) + +렌더러의 자동 넘김(`site/src/lib/ui/use-rail-autoplay.ts`)은 4.5초 **고정 타이머**이고, +정지는 `held` 카운터 하나로 센다 — 레일 위 `pointerdown` 에 올리고 window `pointerup`· +`pointercancel` 에 내린다. 주입분은 타이머를 건드리지 않고 **그 카운터만** 쓴다 +(`pointerdown` 을 `button: 2` 로 쏜다 — embla 는 주 버튼이 아닌 것을 드래그로 치지 않는다). +구현이 둘(embla · 스크롤 상자)이어도 카운터는 하나라 한 번에 걸린다. + +| | 언제 | 얼마나 | +|---|---|---| +| 쿨타임 | 무엇이든 만지면(세로 스크롤·휠·키보드 포함) | 7초 | +| 손이 닿아 있는 동안 | 손가락·마우스 버튼이 내려가 있는 내내 | 뗄 때까지 | +| 영구 정지 | **레일이 실제로 가로로 8px 넘게 움직였을 때** | 그 레일은 끝 | + +★ **손가락 수로 센다 — pointer 로 세면 안 된다.** 브라우저는 터치가 페이지 스크롤로 +넘어가는 순간 **손가락이 아직 닿아 있는데 `pointercancel` 을 쏜다.** 훅의 정지가 거기서 +풀려 손가락 밑에서 카드가 넘어간다. 실측: touchstart 0.0s → pointercancel 7.5s → +8.4s 에 넘어감. 그래서 `event.touches.length` 로 센다. + +★ **"만졌다"와 "밀었다"를 가른다.** 전에는 레일 위 pointerdown·wheel 이면 곧바로 영구 +정지였다. 그런데 일정 카로셀은 화면 가운데를 가득 채워서 페이지를 세로로 내릴 때 손가락· +커서가 거의 언제나 그 위를 지난다 — **한 번 스크롤하면 그 레일이 영영 멈췄다** +(실측: 세로 휠 한 번 → 20초 0px). + +★ **호버 정지는 렌더러 것이고 그대로 둔다** — 커서를 얹으면 "읽는 중"이 맞다(사장님 확인). + +검증: `node rails-test.mjs`. +→ **운영 사이트에는 이 문제들이 그대로 있다.** 훅을 고치는 것은 별도 작업이다(**3.4**). + +### 주입분을 쓸 때 지킬 것 + +★ **React 가 다시 그리면 사라진다.** 그래서 +- 플레이어 목록·패널은 `#root` **밖**(body 직속)에 둔다 +- 헤더에 꽂는 단추는 `MutationObserver` 로 감시해 다시 꽂는다 +- 캐치프레이즈는 노드를 들고 있지 않고 **매 순환마다 다시 찾는다** + +★ **`window.load` 를 기다리지 않는다.** 경로 지도 때문에 OSM 타일 `` 가 140장이라 +`load` 는 그게 다 끝나야 나온다 — 타일이 느리면 영원히 안 뜬다. + +--- + +## 2.6 밟으면 조용히 틀리는 자리 + +- **`out/assets` 에 파일을 넣지 않는다.** 거기는 프리렌더가 관리한다(해시 번들 대장 · 30일 보관). + 손으로 넣은 사진·음원·번들은 `/s/stay/…` 에 둔다 — nginx `^~ /s/` 가 그대로 서빙하고 + (Range 206 확인) 정리 대상이 아니다. +- **마크업을 고쳐 지우지 않는다.** React 가 payload 로 다시 그리면서 되돌린다. + 화면에서 없애야 하면 **주입 CSS** 로 끈다(이용 정보 맨 아래 가로선이 그 경우다). +- **렌더러가 21시를 넘기는 칸을 조용히 버린다**(`shared/section-data.ts` `PLAN_ENDS_BY`). + 적어 놓고 화면에 안 나오는 칸이 생긴다 — 규칙 7이 이걸 막는다. +- **`imageCredit` 은 문자열이다**(`site/src/sections/items/common.tsx:206`). + 객체를 넣으면 조용히 안 나오고, 그건 라이선스 위반이 된다. +- **크롤러가 보는 HTML 은 옛 내용이다.** 이 페이지는 payload 로 하이드레이션해서 화면을 + 바꾸는 것이라, SSR 마크업과 `llms.txt` 는 구운 날 그대로다. 시연에는 문제없지만 + 검색·AI 에 인용되는 값은 아니다. +- **`og:image`** 도 마찬가지다. 화면은 새 사진, 카톡 공유 썸네일만 옛것이다. + +--- +--- + +# 3부 — 공통 + +## 3.1 보고 규칙 — "했다" 고 말하기 전에 + +2026-09-11 지시. **이 규칙을 어긴 기록이 3.2 에 있다.** + +1. **실행하지 않은 것을 됐다고 쓰지 않는다.** 코드에서 빠진 것을 보는 건 확인이 아니다. + 화면을 열어 세거나 명령을 돌려서 **숫자·출력**을 얻은 뒤에 말한다. +2. **"했다" 옆에 무엇으로 확인했는지 같이 쓴다.** `audit-all.mjs 34/34` · `md5 일치` · + `20초 동안 0px` 처럼. 근거를 못 쓰면 아직 안 끝난 것이다. +3. **절차를 안내할 때는 그 절차를 먼저 밟아 본다.** +4. **일부만 했으면 일부만 했다고 쓴다.** 못 한 것을 빼고 요약하지 않는다. +5. **사장님이 본 것과 내가 잰 것이 다르면, 내가 잰 것을 먼저 의심한다.** + 범위를 좁혀서 재지 말고 전수로 다시 센다. + +--- + +## 3.2 실제로 틀렸던 방식 — 되풀이하지 말 것 + +| 한 짓 | 어떻게 틀렸나 | 대신 할 것 | +|---|---|---| +| 코드에서 빠진 것만 보고 "고쳤다" 고 보고 | 화면에는 그대로 있었다(캐치프레이즈 빨간 줄) | `node audit-all.mjs` 로 **브라우저에서** 확인하고 숫자를 적는다 | +| 안 띄워 보고 "이것만 하면 나온다" 고 안내 | 미러 사진 61장·번들이 레포 밖이라 다른 기계에서 흰 화면 | 절차를 **그대로 밟아 본 뒤** 쓴다 | +| 지시받은 한 줄만 고침 | "1번은 숙소" 를 넣으면서 그 하루 전체를 안 읽어 숙소가 3번 서고 두 칸이 연달아 섰다 | 한 곳을 고치면 **같은 종류를 전수 검사**한다 | +| 규칙 검사를 매번 일회용 스크립트로 | 뼈대를 고칠 때마다 "이번에 안 본 규칙" 이 생겼다 | 검사를 **빌드 안**에 둔다(`audit()`) | +| 6초만 재고 "쿨타임 된다" 고 보고 | 쿨타임이 7초라 재개를 못 본 것 | 기대값보다 **길게** 재고 구간을 찍는다 | +| 사진을 눈으로 보고 A/B 배정 | 11/5 가 나왔는데 실제는 12/10 | 등록본 데이터(`rooms.json`)를 쓴다 | +| 마크업을 고쳐서 요소 제거 | 하이드레이션이 되돌렸다 | 주입 CSS 로 끈다 | +| 이름으로 사진 검색 | 동명이인이 붙었다 | 그 사람 문서의 대표 사진만 | + +### 일정이 왜 틀렸나 (2026-09-11 검수 결과) + +사장님이 화면에서 먼저 찾으신 것: + +| | 증상 | 몇 건 | +|---|---|---| +| A | `아침 · 머뭄` 다음 칸이 `체크아웃 · 머뭄` — 같은 자리에 두 칸이 연달아 섰다 | 1 | +| B | 마지막 날 머뭄이 3번(아침 · 체크아웃 · 마무리) — 하루의 절반이 숙소 칸 | 21 | +| C | 마지막 날 섬 코스가 저녁도 없이 19:14 에 돌아와 짐만 찾고 끝 | 1 | +| D | 아침 칸이 2시간 14분 — 끼니 시각을 맞추려고 앞 칸을 늘리다 생긴 값 | 1 | + +**A·B 의 원인은 하나다.** "1번은 숙소" 지시를 받고 하루의 첫 칸에 `아침 · 머뭄` 을 넣었는데, +마지막 날 뼈대에는 **이미** `체크아웃 · 머뭄` 과 `마무리 · 머뭄` 이 있었다. +첫 칸만 보고 넣고 그 하루 전체를 다시 읽지 않았다. +**C·D 는 검사에 그 항목이 없어서** 통과했다. + +고친 것 — 마지막 날 아침과 퇴실을 **한 칸으로 합쳤다**(`아침 · 스테이 머뭄`, +"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요"). +섬 가는 마지막 날은 바깥 정거장을 둘로 줄였다. 아침 상한을 110분으로 내렸다. +그리고 **검사를 빌드 안으로 옮겼다**(2.4). + +### 지시를 주실 때 — 이렇게 주시면 한 번에 끝납니다 + +1. **"X 고치고, 그 화면 전체를 다시 훑어서 같은 종류로 이상한 데 있으면 같이 잡아."** + '같은 종류'라는 말이 있으면 한 건이 아니라 그 규칙을 어긴 **전수**를 찾는다. +2. **"브라우저로 열어서 확인하고 말해." / "검증 결과를 숫자로 보여줘."** +3. **규칙을 주실 때 예외까지 같이.** "1번은 항상 머뭄. 단 마지막 날은 퇴실이 있으니 + 그건 네가 판단해서 제안하고 물어봐." — 예외가 안 정해져 있어서 칸을 하나 더 만들었고 + 그게 연속 칸이 됐다. + +--- + +## 3.3 파일 목록 · 지울 것 · git + +### 쓰는 파일 + +| 파일 | 무엇 | +|---|---| +| `README.md` | **이 문서.** 지침은 전부 여기 | +| `TEXT.md` | 화면에 나가는 텍스트 전문 (생성물 — `python3 dump_text.py`) | +| `build_itinerary.py` | 여행 일정 계산 + 규칙 16종 감사 | +| `build_story.py` | 가요 다방 · 인물 열전 + 위키 사진 | +| `patch_stay.py` | 캐치프레이즈 · 객실 · 자작곡 · HTML 조립 · 자산 경로 교정 | +| `dump_text.py` | `TEXT.md` 생성 | +| `inject.css` · `inject.js` | 화면에 덧대는 다섯 가지 | +| `audit-all.mjs` | 브라우저로 34개 항목 전수 점검 | +| `rails-test.mjs` | 카로셀 13개 × 데스크톱·모바일 | +| `kingcheck.mjs` | 킹서버 반영분 확인 | +| `audit_schedule.py` | 일정 전수 검수표 생성 → `SCHEDULE.md` (규칙이 안 보는 논리 12가지) | +| `SCHEDULE.md` | 그 결과물 (생성물) | + +### 원본 · 원자료 (건드리지 않는다) + +| 파일 | 무엇 | +|---|---| +| `stay-payload.json` · `orig/index.html` | **원본.** 되돌릴 때 기준 | +| `rooms.json` | 야놀자 등록본에서 뽑은 객실 사진 목록 (A동 12 · B동 10) | +| `people-photos.json` · `people-images.json` · `people-images-meta.json` | 위키 문서 사진 + 라이선스 | +| `people-raw.json` · `people-rows.json` | 인물 수집 원자료 | +| `coords.json` | 카카오 로컬에서 받은 좌표 | + +### 자산 (레포에 **일부러** 올렸다) + +| | 개수 | 왜 올리나 | +|---|---|---| +| `img/` | 28장 | 2026-09-07 에 목업 자산이 통째로 404 났을 때 복구한 곳이 서버가 아니라 git 워크트리였다 | +| `img/people/` | 17장 | 위키 사진. 저작자 표시가 라이선스 조건이라 같이 관리한다 | +| `img/mirror/` | 61장 | 히어로·소개·객실. 없으면 **다른 기계에서 흰 화면** | +| `audio/` | 5곡 | 사장님 자작곡 | +| `vendor/` | 2개 | 번들 css/js. 해시가 달라도 뜨게 하려고 | + +합쳐서 약 77M 이다. 전부 **한 번 넣고 안 바뀌는 파일**이라 git 이 싫어하는 경우가 아니다. +자산은 커밋을 따로 떼어 뒀다 — 히스토리에서 덩치를 걷어낼 일이 생기면 그 커밋만 건드린다. + +### git 이그노어 + +| 대상 | 어디 | 왜 | +|---|---|---| +| `build/` | 레포 루트 `.gitignore:30` | 생성물. `patch_stay.py` 로 다시 나온다 | +| `stay-payload-new.json` | `scripts/mockup/.gitignore` | 중간 산출물 | + +### 이 시연이 끝나면 지울 것 + +목업은 **한시적**이다. 실제 구현(1부)이 끝나면 이 폴더는 통째로 필요 없다. + +```bash +# ① 레포에서 +git rm -r solution/site/scripts/mockup + +# ② 서버 볼륨에서 (킹서버 · 로컬 각각) +docker exec o2o-web4ai-solution-prerender rm -rf /app/solution/site/out/s/stay + +# ③ 킹서버 배포 임시 폴더 +ssh King_admin "rm -rf ~/data2/o2o-site-AEO/tmp-stay-deploy" +``` + +★ **지우기 전에 1부를 `docs/` 로 옮긴다.** 1부(제품 구현 지침)는 목업이 사라져도 남아야 하는 +내용이다 — 이 폴더째 지우면 같이 없어진다. 2·3부는 목업과 함께 버린다. + +`stay2` · `stay3` 도 같은 성격의 목업이다. 함께 정리한다. +★ 지우기 전에 [CLAUDE.md 의 ★★ 함정](../../../../CLAUDE.md)을 읽는다 — +`out/s/**` 이 참조하는 자산을 먼저 빼야 다른 사이트가 안 끊긴다. + +--- + +## 3.4 남아 있는 판단 (사장님 결정 대기) + +| | 상태 | +|---|---| +| 하루에 숙소가 3번 서는 날(`아침 → … → 쉼 → 저녁 → 복귀`) | 점심~저녁 4시간을 바깥 두 곳으로 못 채워 넣은 칸이다. 빼려면 오후 정거장을 하나 더 넣어야 한다 | +| 가요 다방 25곡 (목표 50) | 출처에서 확인되는 군산 곡이 거기까지다. 더 채우려면 **지어내야 한다** — 하지 않는다 | +| 인물 사진 18/57 | 위키 문서에 사진이 있는 사람이 18명뿐이다. 동명이인 함정 때문에 이름 검색으로 늘리지 않는다 | +| 렌더러의 카로셀 자동 넘김 | 운영 사이트에 그대로 있다. 고치려면 `use-rail-autoplay.ts` 를 고치고 **전체 재굽기 + `republish_all.py`** | +| ~~일정 정거장의 체류 대비 이동~~ | **해결(2026-09-11)** — 규칙 15·16 을 감사에 넣었다. 전수 검수 21/21 (`SCHEDULE.md`) | +| 일정을 이미지/링크로 공유 | 이미지 저장은 가능(카드를 PNG 로). 링크는 지금 섹션까지만 간다 — 탭·코스가 URL 에 안 남는다 | diff --git a/solution/site/scripts/mockup/SCHEDULE.md b/solution/site/scripts/mockup/SCHEDULE.md new file mode 100644 index 0000000..472cd45 --- /dev/null +++ b/solution/site/scripts/mockup/SCHEDULE.md @@ -0,0 +1,764 @@ +# `/s/stay` 여행 일정 전수 검수표 + +생성 `python3 audit_schedule.py` · 테마 21개 · 53일 · 304정거장 + +표는 **구워진 payload 를 렌더러와 같은 방식으로 계산한 값**이다(화면에 뜨는 것과 같다). +규칙 14종(만들 때 거는 것)은 `build_itinerary.py` `audit()` 가 이미 통과시켰고, +이 표는 그 규칙이 **안 보는 논리**를 따로 센다. + +## 결과 — 21/21 검수완료 + +**전부 통과.** 12개 항목 위반 0건. + +| 코드 | 검수 항목 | +|---|---| +| L0 | 21시를 넘겨 화면에서 버려지는 칸이 없다 | +| L1 | **체류 ≥ 편도 이동** — 볼거리는 가는 데 든 시간보다 오래 머문다 (끼니 제외 — 밥집은 돌아오는 길에 들르는 자리다) | +| L2 | 가운데 날은 점심과 저녁이 둘 다 있다 | +| L3 | 점심 끝나고 저녁까지 3시간 이상 | +| L4 | 하루의 40% 넘게 길에서 쓰지 않는다 | +| L5 | 바깥 정거장 체류 15분 이상 | +| L6 | 하루가 14시간을 넘지 않는다 | +| L7 | 입실 15:00 이후 · 퇴실 11:00 전 | +| L8 | 하루의 첫 칸·마지막 칸이 숙소 | +| L9 | 같은 자리가 연달아 서지 않는다 | +| L10 | 한 테마 안에서 같은 곳을 두 번 가지 않는다 | +| L11 | 걷는 테마에 편도 25분 넘는 이동이 없다 | + +--- + +## 섬까지 다녀오는 1박 2일 + +1박 2일 · 차를 가져온 손님 — ✅ **검수완료** + +> 첫날은 걸어서 도심, 이튿날은 다리를 건너 섬입니다. 두 날의 성격이 아주 다릅니다. + +**첫째 날** 15:00–20:11 (총 5시간 11분 · 이동 36분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 15:00–15:45 | 체크인 | 스테이 머뭄 | –분 | 45분 | +| 2 | 15:48–16:33 | 오후 | 히로쓰 가옥 | 3분 | 45분 | +| 3 | 16:37–17:22 | 오후 | 초원사진관 | 4분 | 45분 | +| 4 | 17:30–18:40 | 저녁 | 빈해원 | 8분 | 70분 | +| 5 | 18:45–19:25 | 밤 산책 | 군산 내항 | 5분 | 40분 | +| 6 | 19:41–20:11 | 머뭄으로 | 스테이 머뭄 | 16분 | 30분 | + +**둘째 날** 08:20–16:36 (총 8시간 16분 · 이동 176분 · 정거장 4) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 08:20–09:35 | 아침 | 스테이 머뭄 | –분 | 75분 | +| 2 | 10:59–13:29 | 오전 | 고군산군도 선유도 | 84분 | 150분 | +| 3 | 14:54–15:49 | 늦은 점심 | 국제반점 | 85분 | 55분 | +| 4 | 15:56–16:36 | 마무리 | 스테이 머뭄 | 7분 | 40분 | + +--- + +## 걸어서만 도는 1박 2일 + +1박 2일 · 차 없이 오는 손님 — ✅ **검수완료** + +> 모든 정거장이 머뭄에서 걸어 닿습니다. 이틀 동안 차를 한 번도 타지 않습니다. + +**첫째 날** 15:00–20:05 (총 5시간 5분 · 이동 40분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 15:00–15:49 | 체크인 | 스테이 머뭄 | –분 | 49분 | +| 2 | 15:52–16:37 | 오후 | 히로쓰 가옥 | 3분 | 45분 | +| 3 | 16:41–17:26 | 오후 | 초원사진관 | 4분 | 45분 | +| 4 | 17:30–18:22 | 저녁 | 일흥옥 | 4분 | 52분 | +| 5 | 18:35–19:17 | 밤 산책 | 군산 내항 | 13분 | 42분 | +| 6 | 19:33–20:05 | 머뭄으로 | 스테이 머뭄 | 16분 | 32분 | + +**둘째 날** 09:20–13:52 (총 4시간 32분 · 이동 26분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 09:20–10:39 | 아침 | 스테이 머뭄 | –분 | 79분 | +| 2 | 10:49–11:36 | 오전 | 군산근대건축관 | 10분 | 47분 | +| 3 | 11:40–12:32 | 점심 | 한일옥 | 4분 | 52분 | +| 4 | 12:36–13:02 | 오후 | 우체통거리 | 4분 | 26분 | +| 5 | 13:10–13:52 | 마무리 | 스테이 머뭄 | 8분 | 42분 | + +--- + +## 비가 와도 되는 1박 2일 + +1박 2일 · 비 예보를 보고 오는 손님 — ✅ **검수완료** + +> 실내가 이어집니다. 비가 그치기를 기다리지 않아도 하루가 찹니다. + +**첫째 날** 15:10–20:57 (총 5시간 47분 · 이동 40분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 15:10–15:49 | 체크인 | 스테이 머뭄 | –분 | 39분 | +| 2 | 16:00–17:28 | 오후 | 군산근대역사박물관 | 11분 | 88분 | +| 3 | 17:31–18:15 | 오후 | 군산근대미술관 | 3분 | 44분 | +| 4 | 18:20–19:28 | 저녁 | 빈해원 | 5분 | 68분 | +| 5 | 19:33–20:12 | 밤 산책 | 군산 내항 | 5분 | 39분 | +| 6 | 20:28–20:57 | 머뭄으로 | 스테이 머뭄 | 16분 | 29분 | + +**둘째 날** 09:40–15:36 (총 5시간 56분 · 이동 52분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 09:40–11:00 | 아침 | 스테이 머뭄 | –분 | 80분 | +| 2 | 11:10–12:02 | 오전 | 군산근대건축관 | 10분 | 52분 | +| 3 | 12:10–13:07 | 점심 | 일흥옥 | 8분 | 57분 | +| 4 | 13:22–14:31 | 오후 | 째보선창 근처 카페 | 15분 | 69분 | +| 5 | 14:50–15:36 | 마무리 | 스테이 머뭄 | 19분 | 46분 | + +--- + +## 근대건축만 보는 1박 2일 + +1박 2일 · 건물을 보러 오는 손님 — ✅ **검수완료** + +> 1900년대 건물만 골라 돕니다. 머뭄도 그 시기에 지은 집입니다. + +**첫째 날** 15:00–20:14 (총 5시간 14분 · 이동 50분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 15:00–15:38 | 체크인 | 스테이 머뭄 | –분 | 38분 | +| 2 | 15:51–16:19 | 오후 | 구 군산세관 본관 | 13분 | 28분 | +| 3 | 16:23–17:24 | 오후 | 군산근대건축관 | 4분 | 61분 | +| 4 | 17:30–18:41 | 저녁 | 명월갈비 | 6분 | 71분 | +| 5 | 18:52–19:30 | 밤 산책 | 군산 내항 | 11분 | 38분 | +| 6 | 19:46–20:14 | 머뭄으로 | 스테이 머뭄 | 16분 | 28분 | + +**둘째 날** 08:50–13:35 (총 4시간 45분 · 이동 37분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 08:50–10:03 | 아침 | 스테이 머뭄 | –분 | 73분 | +| 2 | 10:12–11:22 | 오전 | 군산근대미술관 | 9분 | 70분 | +| 3 | 11:30–12:18 | 점심 | 일흥옥 | 8분 | 48분 | +| 4 | 12:30–12:49 | 오후 | 해망굴 | 12분 | 19분 | +| 5 | 12:57–13:35 | 마무리 | 스테이 머뭄 | 8분 | 38분 | + +--- + +## 이성당으로 시작하는 1박 2일 + +1박 2일 · 빵을 좋아하는 손님 — ✅ **검수완료** + +> 이튿날 아침을 빵으로 엽니다. 문 여는 시간에 맞춰 걸어갑니다. + +**첫째 날** 16:10–20:36 (총 4시간 26분 · 이동 46분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 16:10–16:46 | 체크인 | 스테이 머뭄 | –분 | 36분 | +| 2 | 16:50–17:17 | 오후 | 초원사진관 | 4분 | 27분 | +| 3 | 17:23–17:45 | 오후 | 우체통거리 | 6분 | 22분 | +| 4 | 17:51–19:03 | 저녁 | 도란 | 6분 | 72분 | +| 5 | 19:17–19:53 | 밤 산책 | 군산 내항 | 14분 | 36분 | +| 6 | 20:09–20:36 | 머뭄으로 | 스테이 머뭄 | 16분 | 27분 | + +**둘째 날** 08:40–14:26 (총 5시간 46분 · 이동 35분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 08:40–10:28 | 아침 | 스테이 머뭄 | –분 | 108분 | +| 2 | 10:35–11:25 | 오전 | 이성당 | 7분 | 50분 | +| 3 | 11:30–12:06 | 점심 | 만남스넥 | 5분 | 36분 | +| 4 | 12:18–13:39 | 오후 | 군산근대역사박물관 | 12분 | 81분 | +| 5 | 13:50–14:26 | 마무리 | 스테이 머뭄 | 11분 | 36분 | + +--- + +## 철길마을까지 가는 1박 2일 + +1박 2일 · 사진을 찍는 손님 — ✅ **검수완료** + +> 빛이 좋은 자리만 이어 붙였습니다. 철길마을은 오전 빛이 낫습니다. + +**첫째 날** 15:40–20:04 (총 4시간 24분 · 이동 47분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 15:40–16:18 | 체크인 | 스테이 머뭄 | –분 | 38분 | +| 2 | 16:22–16:50 | 오후 | 초원사진관 | 4분 | 28분 | +| 3 | 16:58–17:31 | 오후 | 동국사 | 8분 | 33분 | +| 4 | 17:41–18:33 | 저녁 | 국제반점 | 10분 | 52분 | +| 5 | 18:42–19:20 | 밤 산책 | 군산 내항 | 9분 | 38분 | +| 6 | 19:36–20:04 | 머뭄으로 | 스테이 머뭄 | 16분 | 28분 | + +**둘째 날** 08:30–14:06 (총 5시간 36분 · 이동 36분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 08:30–09:47 | 아침 | 스테이 머뭄 | –분 | 77분 | +| 2 | 09:59–11:19 | 오전 | 경암동 철길마을 | 12분 | 80분 | +| 3 | 11:30–12:18 | 점심 | 일흥옥 | 11분 | 48분 | +| 4 | 12:24–13:21 | 오후 | 군산 시간여행마을 | 6분 | 57분 | +| 5 | 13:28–14:06 | 마무리 | 스테이 머뭄 | 7분 | 38분 | + +--- + +## 항구만 도는 1박 2일 + +1박 2일 · 바다가 보고 싶은 손님 — ✅ **검수완료** + +> 부잔교에서 째보선창까지, 이틀 동안 물가만 걷습니다. + +**첫째 날** 15:40–20:55 (총 5시간 15분 · 이동 67분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 15:40–16:22 | 체크인 | 스테이 머뭄 | –분 | 42분 | +| 2 | 16:38–17:15 | 오후 | 군산 내항 부잔교 | 16분 | 37분 | +| 3 | 17:32–17:53 | 오후 | 해망굴 | 17분 | 21분 | +| 4 | 18:06–19:20 | 저녁 | 빈해원 | 13분 | 74분 | +| 5 | 19:25–20:07 | 밤 산책 | 군산 내항 | 5분 | 42분 | +| 6 | 20:23–20:55 | 머뭄으로 | 스테이 머뭄 | 16분 | 32분 | + +**둘째 날** 09:20–14:14 (총 4시간 54분 · 이동 51분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 09:20–10:39 | 아침 | 스테이 머뭄 | –분 | 79분 | +| 2 | 10:58–11:30 | 오전 | 째보선창 | 19분 | 32분 | +| 3 | 11:42–12:40 | 점심 | 국제반점 | 12분 | 58분 | +| 4 | 12:47–13:19 | 오후 | 구 군산세관 본관 | 7분 | 32분 | +| 5 | 13:32–14:14 | 마무리 | 스테이 머뭄 | 13분 | 42분 | + +--- + +## 소설 『탁류』를 따라 걷는 1박 2일 + +1박 2일 · 채만식을 읽어 본 손님 — ✅ **검수완료** + +> 소설이 지나간 자리를 순서대로 밟습니다. 세관·선창·문학관이 한 줄로 이어집니다. + +**첫째 날** 15:10–20:19 (총 5시간 9분 · 이동 62분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 15:10–15:54 | 체크인 | 스테이 머뭄 | –분 | 44분 | +| 2 | 16:13–16:46 | 오후 | 째보선창 | 19분 | 33분 | +| 3 | 16:51–17:29 | 오후 | 군산 내항 부잔교 | 5분 | 38분 | +| 4 | 17:40–18:35 | 저녁 | 한일옥 | 11분 | 55분 | +| 5 | 18:46–19:30 | 밤 산책 | 군산 내항 | 11분 | 44분 | +| 6 | 19:46–20:19 | 머뭄으로 | 스테이 머뭄 | 16분 | 33분 | + +**둘째 날** 09:00–14:40 (총 5시간 40분 · 이동 60분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 09:00–10:22 | 아침 | 스테이 머뭄 | –분 | 82분 | +| 2 | 10:40–11:46 | 오전 | 채만식문학관 | 18분 | 66분 | +| 3 | 12:03–12:58 | 점심 | 일흥옥 | 17분 | 55분 | +| 4 | 13:10–13:43 | 오후 | 구 군산세관 본관 | 12분 | 33분 | +| 5 | 13:56–14:40 | 마무리 | 스테이 머뭄 | 13분 | 44분 | + +--- + +## 아이와 함께 1박 2일 + +1박 2일 · 아이를 데려온 손님 — ✅ **검수완료** + +> 걷는 거리가 짧고, 아이가 지루해할 자리를 뺐습니다. + +**첫째 날** 16:00–19:46 (총 3시간 46분 · 이동 43분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 16:00–16:34 | 체크인 | 스테이 머뭄 | –분 | 34분 | +| 2 | 16:38–17:04 | 오후 | 초원사진관 | 4분 | 26분 | +| 3 | 17:10–17:31 | 오후 | 우체통거리 | 6분 | 21분 | +| 4 | 17:35–18:17 | 저녁 | 만남스넥 | 4분 | 42분 | +| 5 | 18:30–19:04 | 밤 산책 | 군산 내항 | 13분 | 34분 | +| 6 | 19:20–19:46 | 머뭄으로 | 스테이 머뭄 | 16분 | 26분 | + +**둘째 날** 10:10–14:10 (총 4시간 0분 · 이동 42분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 10:10–11:00 | 아침 | 스테이 머뭄 | –분 | 50분 | +| 2 | 11:12–11:54 | 오전 | 경암동 철길마을 | 12분 | 42분 | +| 3 | 12:05–12:35 | 점심 | 이성당 | 11분 | 30분 | +| 4 | 12:45–13:27 | 오후 | 월명호수 | 10분 | 42분 | +| 5 | 13:36–14:10 | 마무리 | 스테이 머뭄 | 9분 | 34분 | + +--- + +## 늦게 도착하는 1박 2일 + +1박 2일 · 퇴근하고 오는 손님 — ✅ **검수완료** + +> 첫날은 저녁 한 끼로 끝냅니다. 대신 이튿날을 아침부터 저녁까지 씁니다. + +**첫째 날** 18:20–20:55 (총 2시간 35분 · 이동 10분 · 정거장 3) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 18:20–19:00 | 체크인 | 스테이 머뭄 | –분 | 40분 | +| 2 | 19:05–20:20 | 저녁 | 명월갈비 | 5분 | 75분 | +| 3 | 20:25–20:55 | 머뭄으로 | 스테이 머뭄 | 5분 | 30분 | + +**둘째 날** 08:40–14:39 (총 5시간 59분 · 이동 39분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 08:40–09:55 | 아침 | 스테이 머뭄 | –분 | 75분 | +| 2 | 10:06–11:36 | 오전 | 군산근대역사박물관 | 11분 | 90분 | +| 3 | 11:42–12:32 | 점심 | 한일옥 | 6분 | 50분 | +| 4 | 12:36–13:21 | 오후 | 군산근대건축관 | 4분 | 45분 | +| 5 | 13:31–13:51 | 오후 | 해망굴 | 10분 | 20분 | +| 6 | 13:59–14:39 | 마무리 | 스테이 머뭄 | 8분 | 40분 | + +--- + +## 천천히 도는 2박 3일 + +2박 3일 · 쉬러 오는 손님 — ✅ **검수완료** + +> 하루에 두어 곳만 봅니다. 마당에 앉아 있는 시간을 일정에 넣었습니다. + +**첫째 날** 15:20–21:00 (총 5시간 40분 · 이동 46분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 15:20–16:06 | 체크인 | 스테이 머뭄 | –분 | 46분 | +| 2 | 16:09–16:44 | 오후 | 히로쓰 가옥 | 3분 | 35분 | +| 3 | 16:50–17:30 | 오후 | 동국사 | 6분 | 40분 | +| 4 | 17:37–19:09 | 저녁 | 도란 | 7분 | 92분 | +| 5 | 19:23–20:09 | 밤 산책 | 군산 내항 | 14분 | 46분 | +| 6 | 20:25–21:00 | 머뭄으로 | 스테이 머뭄 | 16분 | 35분 | + +**둘째 날** 09:40–19:27 (총 9시간 47분 · 이동 32분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 09:40–13:23 | 머뭄에서 쉬는 날 | 스테이 머뭄 | –분 | 223분 | +| 2 | 13:28–14:21 | 점심 | 한일옥 | 5분 | 53분 | +| 3 | 14:26–17:19 | 쉼 | 스테이 머뭄 | 5분 | 173분 | +| 4 | 17:30–18:44 | 저녁 | 빈해원 | 11분 | 74분 | +| 5 | 18:55–19:27 | 머뭄으로 | 스테이 머뭄 | 11분 | 32분 | + +**셋째 날** 09:50–15:05 (총 5시간 15분 · 이동 26분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 09:50–11:00 | 아침 | 스테이 머뭄 | –분 | 70분 | +| 2 | 11:07–11:51 | 오전 | 이성당 | 7분 | 44분 | +| 3 | 11:56–12:46 | 점심 | 만남스넥 | 5분 | 50분 | +| 4 | 12:53–14:08 | 오후 | 군산 시간여행마을 | 7분 | 75분 | +| 5 | 14:15–15:05 | 마무리 | 스테이 머뭄 | 7분 | 50분 | + +--- + +## 섬과 도심을 반씩 도는 2박 3일 + +2박 3일 · 차를 가져온 손님 — ✅ **검수완료** + +> 가운데 날을 섬에 다 씁니다. 첫날과 마지막 날은 걸어서 도심입니다. + +**첫째 날** 15:10–20:11 (총 5시간 1분 · 이동 41분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 15:10–15:50 | 체크인 | 스테이 머뭄 | –분 | 40분 | +| 2 | 16:03–16:33 | 오후 | 구 군산세관 본관 | 13분 | 30분 | +| 3 | 16:37–17:27 | 오후 | 군산근대건축관 | 4분 | 50분 | +| 4 | 17:30–18:40 | 저녁 | 빈해원 | 3분 | 70분 | +| 5 | 18:45–19:25 | 밤 산책 | 군산 내항 | 5분 | 40분 | +| 6 | 19:41–20:11 | 머뭄으로 | 스테이 머뭄 | 16분 | 30분 | + +**둘째 날** 07:20–20:37 (총 13시간 17분 · 이동 209분 · 정거장 7) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 07:20–08:20 | 아침 | 스테이 머뭄 | –분 | 60분 | +| 2 | 09:44–12:14 | 오전 | 고군산군도 선유도 | 84분 | 150분 | +| 3 | 12:34–13:24 | 오후 | 장자도 | 20분 | 50분 | +| 4 | 14:52–15:47 | 늦은 점심 | 국제반점 | 88분 | 55분 | +| 5 | 15:54–18:42 | 쉼 | 스테이 머뭄 | 7분 | 168분 | +| 6 | 18:47–20:02 | 저녁 | 명월갈비 | 5분 | 75분 | +| 7 | 20:07–20:37 | 머뭄으로 | 스테이 머뭄 | 5분 | 30분 | + +**셋째 날** 09:20–13:46 (총 4시간 26분 · 이동 19분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 09:20–10:37 | 아침 | 스테이 머뭄 | –분 | 77분 | +| 2 | 10:41–11:26 | 오전 | 초원사진관 | 4분 | 45분 | +| 3 | 11:30–12:20 | 점심 | 일흥옥 | 4분 | 50분 | +| 4 | 12:24–12:59 | 오후 | 이성당 | 4분 | 35분 | +| 5 | 13:06–13:46 | 마무리 | 스테이 머뭄 | 7분 | 40분 | + +--- + +## 골목을 세 번 나눠 걷는 2박 3일 + +2박 3일 · 차 없이 오는 손님 — ✅ **검수완료** + +> 원도심을 세 덩이로 나눠 하루에 하나씩 걷습니다. 같은 길을 두 번 걷지 않습니다. + +**첫째 날** 15:00–20:11 (총 5시간 11분 · 이동 40분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 15:00–15:49 | 체크인 | 스테이 머뭄 | –분 | 49분 | +| 2 | 15:52–16:37 | 오후 | 히로쓰 가옥 | 3분 | 45분 | +| 3 | 16:41–17:26 | 오후 | 초원사진관 | 4분 | 45분 | +| 4 | 17:30–18:25 | 저녁 | 일흥옥 | 4분 | 55분 | +| 5 | 18:38–19:22 | 밤 산책 | 군산 내항 | 13분 | 44분 | +| 6 | 19:38–20:11 | 머뭄으로 | 스테이 머뭄 | 16분 | 33분 | + +**둘째 날** 09:10–19:30 (총 10시간 20분 · 이동 49분 · 정거장 8) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 09:10–10:16 | 아침 | 스테이 머뭄 | –분 | 66분 | +| 2 | 10:27–12:06 | 오전 | 군산근대역사박물관 | 11분 | 99분 | +| 3 | 12:12–13:07 | 점심 | 한일옥 | 6분 | 55분 | +| 4 | 13:11–14:01 | 오후 | 군산근대건축관 | 4분 | 50분 | +| 5 | 14:11–14:33 | 오후 | 해망굴 | 10분 | 22분 | +| 6 | 14:41–17:25 | 쉼 | 스테이 머뭄 | 8분 | 164분 | +| 7 | 17:30–18:52 | 저녁 | 명월갈비 | 5분 | 82분 | +| 8 | 18:57–19:30 | 머뭄으로 | 스테이 머뭄 | 5분 | 33분 | + +**셋째 날** 08:50–14:18 (총 5시간 28분 · 이동 26분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 08:50–10:38 | 아침 | 스테이 머뭄 | –분 | 108분 | +| 2 | 10:46–11:26 | 오전 | 우체통거리 | 8분 | 40분 | +| 3 | 11:30–12:14 | 점심 | 만남스넥 | 4분 | 44분 | +| 4 | 12:21–13:27 | 오후 | 군산 시간여행마을 | 7분 | 66분 | +| 5 | 13:34–14:18 | 마무리 | 스테이 머뭄 | 7분 | 44분 | + +--- + +## 먹으러 오는 2박 3일 + +2박 3일 · 맛집을 도는 손님 — ✅ **검수완료** + +> 끼니가 중심입니다. 사이에 걸을 자리를 넣어 배를 비웁니다. + +**첫째 날** 15:30–20:56 (총 5시간 26분 · 이동 40분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 15:30–16:16 | 체크인 | 스테이 머뭄 | –분 | 46분 | +| 2 | 16:20–17:00 | 오후 | 물밀소 | 4분 | 40분 | +| 3 | 17:06–17:40 | 오후 | 초원사진관 | 6분 | 34분 | +| 4 | 17:43–19:09 | 저녁 | 명월갈비 | 3분 | 86분 | +| 5 | 19:20–20:06 | 밤 산책 | 군산 내항 | 11분 | 46분 | +| 6 | 20:22–20:56 | 머뭄으로 | 스테이 머뭄 | 16분 | 34분 | + +**둘째 날** 10:20–19:35 (총 9시간 15분 · 이동 50분 · 정거장 8) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 10:20–11:29 | 아침 | 스테이 머뭄 | –분 | 69분 | +| 2 | 11:36–12:45 | 오전 | 군산 시간여행마을 | 7분 | 69분 | +| 3 | 12:51–13:48 | 점심 | 일흥옥 | 6분 | 57분 | +| 4 | 13:52–14:32 | 오후 | 이성당 | 4분 | 40분 | +| 5 | 14:35–15:04 | 오후 | 우체통거리 | 3분 | 29분 | +| 6 | 15:12–17:19 | 쉼 | 스테이 머뭄 | 8분 | 127분 | +| 7 | 17:30–18:50 | 저녁 | 빈해원 | 11분 | 80분 | +| 8 | 19:01–19:35 | 머뭄으로 | 스테이 머뭄 | 11분 | 34분 | + +**셋째 날** 08:40–15:18 (총 6시간 38분 · 이동 32분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 08:40–10:24 | 아침 | 스테이 머뭄 | –분 | 104분 | +| 2 | 10:30–11:20 | 오전 | 동국사 | 6분 | 50분 | +| 3 | 11:30–12:33 | 점심 | 국제반점 | 10분 | 63분 | +| 4 | 12:38–14:21 | 오후 | 군산근대역사박물관 | 5분 | 103분 | +| 5 | 14:32–15:18 | 마무리 | 스테이 머뭄 | 11분 | 46분 | + +--- + +## 아무 데도 안 가는 2박 3일 + +2박 3일 · 쉬려고만 오는 손님 — ✅ **검수완료** + +> 가운데 날은 대문 밖으로 두 번만 나갑니다. 나머지는 마당과 카페 공간에서 보냅니다. + +**첫째 날** 15:00–21:00 (총 6시간 0분 · 이동 40분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 15:00–15:50 | 체크인 | 스테이 머뭄 | –분 | 50분 | +| 2 | 15:53–16:38 | 오후 | 히로쓰 가옥 | 3분 | 45분 | +| 3 | 16:42–17:27 | 오후 | 초원사진관 | 4분 | 45분 | +| 4 | 17:30–19:06 | 저녁 | 도란 | 3분 | 96분 | +| 5 | 19:20–20:08 | 밤 산책 | 군산 내항 | 14분 | 48분 | +| 6 | 20:24–21:00 | 머뭄으로 | 스테이 머뭄 | 16분 | 36분 | + +**둘째 날** 09:30–19:03 (총 9시간 33분 · 이동 22분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 09:30–13:22 | 머뭄에서 쉬는 날 | 스테이 머뭄 | –분 | 232분 | +| 2 | 13:28–14:12 | 점심 | 만남스넥 | 6분 | 44분 | +| 3 | 14:18–17:25 | 쉼 | 스테이 머뭄 | 6분 | 187분 | +| 4 | 17:30–18:25 | 저녁 | 일흥옥 | 5분 | 55분 | +| 5 | 18:30–19:03 | 머뭄으로 | 스테이 머뭄 | 5분 | 33분 | + +**셋째 날** 09:00–14:34 (총 5시간 34분 · 이동 27분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 09:00–10:38 | 아침 | 스테이 머뭄 | –분 | 98분 | +| 2 | 10:44–11:30 | 오전 | 동국사 | 6분 | 46분 | +| 3 | 11:33–12:19 | 점심 | 물밀소 | 3분 | 46분 | +| 4 | 12:28–13:33 | 오후 | 월명호수 | 9분 | 65분 | +| 5 | 13:42–14:34 | 마무리 | 스테이 머뭄 | 9분 | 52분 | + +--- + +## 사진만 찍는 2박 3일 + +2박 3일 · 사진을 찍는 손님 — ✅ **검수완료** + +> 시간대에 맞춰 자리를 배치했습니다. 철길마을은 오전, 부잔교는 해 질 때입니다. + +**첫째 날** 15:50–20:03 (총 4시간 13분 · 이동 49분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 15:50–16:22 | 체크인 | 스테이 머뭄 | –분 | 32분 | +| 2 | 16:26–16:50 | 오후 | 초원사진관 | 4분 | 24분 | +| 3 | 16:58–17:26 | 오후 | 동국사 | 8분 | 28분 | +| 4 | 17:33–18:37 | 저녁 | 도란 | 7분 | 64분 | +| 5 | 18:51–19:23 | 밤 산책 | 군산 내항 | 14분 | 32분 | +| 6 | 19:39–20:03 | 머뭄으로 | 스테이 머뭄 | 16분 | 24분 | + +**둘째 날** 09:40–18:46 (총 9시간 6분 · 이동 81분 · 정거장 8) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 09:40–10:28 | 아침 | 스테이 머뭄 | –분 | 48분 | +| 2 | 10:40–11:20 | 오전 | 경암동 철길마을 | 12분 | 40분 | +| 3 | 11:31–12:14 | 점심 | 만남스넥 | 11분 | 43분 | +| 4 | 12:29–13:14 | 오후 | 째보선창 | 15분 | 45분 | +| 5 | 13:35–14:05 | 오후 | 해망굴 | 21분 | 30분 | +| 6 | 14:13–17:23 | 쉼 | 스테이 머뭄 | 8분 | 190분 | +| 7 | 17:30–18:15 | 저녁 | 국제반점 | 7분 | 45분 | +| 8 | 18:22–18:46 | 머뭄으로 | 스테이 머뭄 | 7분 | 24분 | + +**셋째 날** 08:50–13:35 (총 4시간 45분 · 이동 29분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 08:50–10:38 | 아침 | 스테이 머뭄 | –분 | 108분 | +| 2 | 10:46–11:26 | 오전 | 우체통거리 | 8분 | 40분 | +| 3 | 11:30–12:10 | 점심 | 일흥옥 | 4분 | 40분 | +| 4 | 12:18–12:54 | 오후 | 군산근대미술관 | 8분 | 36분 | +| 5 | 13:03–13:35 | 마무리 | 스테이 머뭄 | 9분 | 32분 | + +--- + +## 비가 와도 되는 2박 3일 + +2박 3일 · 비 예보를 보고 오는 손님 — ✅ **검수완료** + +> 사흘 내리 비가 와도 도는 순서가 바뀌지 않습니다. 실내와 처마 밑만 잇습니다. + +**첫째 날** 15:00–20:55 (총 5시간 55분 · 이동 40분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 15:00–15:40 | 체크인 | 스테이 머뭄 | –분 | 40분 | +| 2 | 15:51–17:21 | 오후 | 군산근대역사박물관 | 11분 | 90분 | +| 3 | 17:24–18:09 | 오후 | 군산근대미술관 | 3분 | 45분 | +| 4 | 18:14–19:24 | 저녁 | 빈해원 | 5분 | 70분 | +| 5 | 19:29–20:09 | 밤 산책 | 군산 내항 | 5분 | 40분 | +| 6 | 20:25–20:55 | 머뭄으로 | 스테이 머뭄 | 16분 | 30분 | + +**둘째 날** 10:10–19:48 (총 9시간 38분 · 이동 62분 · 정거장 8) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 10:10–11:22 | 아침 | 스테이 머뭄 | –분 | 72분 | +| 2 | 11:32–12:26 | 오전 | 군산근대건축관 | 10분 | 54분 | +| 3 | 12:34–13:34 | 점심 | 일흥옥 | 8분 | 60분 | +| 4 | 13:49–15:01 | 오후 | 째보선창 근처 카페 | 15분 | 72분 | +| 5 | 15:09–15:57 | 오후 | 동국사다온 | 8분 | 48분 | +| 6 | 16:08–17:32 | 쉼 | 스테이 머뭄 | 11분 | 84분 | +| 7 | 17:37–19:07 | 저녁 | 명월갈비 | 5분 | 90분 | +| 8 | 19:12–19:48 | 머뭄으로 | 스테이 머뭄 | 5분 | 36분 | + +**셋째 날** 09:40–15:27 (총 5시간 47분 · 이동 45분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 09:40–11:00 | 아침 | 스테이 머뭄 | –분 | 80분 | +| 2 | 11:18–12:30 | 오전 | 채만식문학관 | 18분 | 72분 | +| 3 | 12:47–13:47 | 점심 | 한일옥 | 17분 | 60분 | +| 4 | 13:50–14:32 | 오후 | 이성당 | 3분 | 42분 | +| 5 | 14:39–15:27 | 마무리 | 스테이 머뭄 | 7분 | 48분 | + +--- + +## 늦게 시작하는 2박 3일 + +2박 3일 · 아침이 힘든 손님 — ✅ **검수완료** + +> 가운데 날을 열한 시에 엽니다. 오전을 비우고 저녁을 늦게까지 씁니다. + +**첫째 날** 16:00–20:52 (총 4시간 52분 · 이동 47분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 16:00–16:40 | 체크인 | 스테이 머뭄 | –분 | 40분 | +| 2 | 16:43–17:13 | 오후 | 히로쓰 가옥 | 3분 | 30분 | +| 3 | 17:21–17:46 | 오후 | 우체통거리 | 8분 | 25분 | +| 4 | 17:52–19:12 | 저녁 | 도란 | 6분 | 80분 | +| 5 | 19:26–20:06 | 밤 산책 | 군산 내항 | 14분 | 40분 | +| 6 | 20:22–20:52 | 머뭄으로 | 스테이 머뭄 | 16분 | 30분 | + +**둘째 날** 10:30–19:21 (총 8시간 51분 · 이동 55분 · 정거장 7) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 10:30–11:30 | 아침 | 스테이 머뭄 | –분 | 60분 | +| 2 | 11:35–12:25 | 점심 | 일흥옥 | 5분 | 50분 | +| 3 | 12:35–14:05 | 오후 | 군산근대역사박물관 | 10분 | 90분 | +| 4 | 14:15–14:35 | 오후 | 해망굴 | 10분 | 20분 | +| 5 | 14:43–17:19 | 쉼 | 스테이 머뭄 | 8분 | 156분 | +| 6 | 17:30–18:40 | 저녁 | 빈해원 | 11분 | 70분 | +| 7 | 18:51–19:21 | 머뭄으로 | 스테이 머뭄 | 11분 | 30분 | + +**셋째 날** 10:10–13:47 (총 3시간 37분 · 이동 22분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 10:10–11:00 | 아침 | 스테이 머뭄 | –분 | 50분 | +| 2 | 11:07–11:42 | 오전 | 이성당 | 7분 | 35분 | +| 3 | 11:47–12:27 | 점심 | 만남스넥 | 5분 | 40분 | +| 4 | 12:33–13:03 | 오후 | 초원사진관 | 6분 | 30분 | +| 5 | 13:07–13:47 | 마무리 | 스테이 머뭄 | 4분 | 40분 | + +--- + +## 섬에서 하루를 다 쓰는 2박 3일 + +2박 3일 · 차를 가져온 손님 — ✅ **검수완료** + +> 가운데 날은 아침에 나가 저녁에 들어옵니다. 섬에서 하루를 통째로 씁니다. + +**첫째 날** 15:00–20:03 (총 5시간 3분 · 이동 67분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 15:00–15:38 | 체크인 | 스테이 머뭄 | –분 | 38분 | +| 2 | 15:54–16:30 | 오후 | 군산 내항 부잔교 | 16분 | 36분 | +| 3 | 16:47–17:17 | 오후 | 해망굴 | 17분 | 30분 | +| 4 | 17:30–18:36 | 저녁 | 빈해원 | 13분 | 66분 | +| 5 | 18:41–19:19 | 밤 산책 | 군산 내항 | 5분 | 38분 | +| 6 | 19:35–20:03 | 머뭄으로 | 스테이 머뭄 | 16분 | 28분 | + +**둘째 날** 07:30–20:21 (총 12시간 51분 · 이동 207분 · 정거장 7) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 07:30–08:27 | 아침 | 스테이 머뭄 | –분 | 57분 | +| 2 | 09:51–12:13 | 오전 | 고군산군도 선유도 | 84분 | 142분 | +| 3 | 12:33–13:21 | 오후 | 장자도 | 20분 | 48분 | +| 4 | 14:49–15:37 | 늦은 점심 | 한일옥 | 88분 | 48분 | +| 5 | 15:42–18:32 | 쉼 | 스테이 머뭄 | 5분 | 170분 | +| 6 | 18:37–19:48 | 저녁 | 명월갈비 | 5분 | 71분 | +| 7 | 19:53–20:21 | 머뭄으로 | 스테이 머뭄 | 5분 | 28분 | + +**셋째 날** 09:40–14:20 (총 4시간 40분 · 이동 42분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 09:40–10:51 | 아침 | 스테이 머뭄 | –분 | 71분 | +| 2 | 11:01–11:44 | 오전 | 군산근대건축관 | 10분 | 43분 | +| 3 | 11:53–12:31 | 점심 | 만남스넥 | 9분 | 38분 | +| 4 | 12:42–13:30 | 오후 | 경암동 철길마을 | 11분 | 48분 | +| 5 | 13:42–14:20 | 마무리 | 스테이 머뭄 | 12분 | 38분 | + +--- + +## 소설 『탁류』를 따라 걷는 2박 3일 + +2박 3일 · 채만식을 읽어 본 손님 — ✅ **검수완료** + +> 소설의 순서대로 사흘을 나눴습니다. 선창에서 시작해 문학관에서 끝냅니다. + +**첫째 날** 15:00–20:13 (총 5시간 13분 · 이동 66분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 15:00–15:44 | 체크인 | 스테이 머뭄 | –분 | 44분 | +| 2 | 16:03–16:36 | 오후 | 째보선창 | 19분 | 33분 | +| 3 | 16:41–17:19 | 오후 | 군산 내항 부잔교 | 5분 | 38분 | +| 4 | 17:32–18:27 | 저녁 | 일흥옥 | 13분 | 55분 | +| 5 | 18:40–19:24 | 밤 산책 | 군산 내항 | 13분 | 44분 | +| 6 | 19:40–20:13 | 머뭄으로 | 스테이 머뭄 | 16분 | 33분 | + +**둘째 날** 09:30–19:31 (총 10시간 1분 · 이동 32분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 09:30–13:21 | 머뭄에서 쉬는 날 | 스테이 머뭄 | –분 | 231분 | +| 2 | 13:26–14:21 | 점심 | 한일옥 | 5분 | 55분 | +| 3 | 14:26–17:19 | 쉼 | 스테이 머뭄 | 5분 | 173분 | +| 4 | 17:30–18:47 | 저녁 | 빈해원 | 11분 | 77분 | +| 5 | 18:58–19:31 | 머뭄으로 | 스테이 머뭄 | 11분 | 33분 | + +**셋째 날** 08:30–14:12 (총 5시간 42분 · 이동 23분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 08:30–09:52 | 아침 | 스테이 머뭄 | –분 | 82분 | +| 2 | 10:03–11:42 | 오전 | 군산근대역사박물관 | 11분 | 99분 | +| 3 | 11:47–12:48 | 점심 | 국제반점 | 5분 | 61분 | +| 4 | 12:51–13:24 | 오후 | 초원사진관 | 3분 | 33분 | +| 5 | 13:28–14:12 | 마무리 | 스테이 머뭄 | 4분 | 44분 | + +--- + +## 철길마을과 항구를 같이 보는 2박 3일 + +2박 3일 · 사진을 찍는 손님 — ✅ **검수완료** + +> 기찻길과 물길을 하루씩 나눠 봅니다. 마지막 날은 건물만 봅니다. + +**첫째 날** 16:00–20:32 (총 4시간 32분 · 이동 51분 · 정거장 6) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 16:00–16:36 | 체크인 | 스테이 머뭄 | –분 | 36분 | +| 2 | 16:48–17:33 | 오후 | 경암동 철길마을 | 12분 | 45분 | +| 3 | 17:44–18:11 | 오후 | 초원사진관 | 11분 | 27분 | +| 4 | 18:14–19:04 | 저녁 | 국제반점 | 3분 | 50분 | +| 5 | 19:13–19:49 | 밤 산책 | 군산 내항 | 9분 | 36분 | +| 6 | 20:05–20:32 | 머뭄으로 | 스테이 머뭄 | 16분 | 27분 | + +**둘째 날** 09:40–19:10 (총 9시간 30분 · 이동 79분 · 정거장 8) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 09:40–10:34 | 아침 | 스테이 머뭄 | –분 | 54분 | +| 2 | 10:50–11:22 | 오전 | 군산 내항 부잔교 | 16분 | 32분 | +| 3 | 11:33–12:18 | 점심 | 한일옥 | 11분 | 45분 | +| 4 | 12:31–13:16 | 오후 | 째보선창 | 13분 | 45분 | +| 5 | 13:37–14:07 | 오후 | 해망굴 | 21분 | 30분 | +| 6 | 14:15–17:25 | 쉼 | 스테이 머뭄 | 8분 | 190분 | +| 7 | 17:30–18:38 | 저녁 | 명월갈비 | 5분 | 68분 | +| 8 | 18:43–19:10 | 머뭄으로 | 스테이 머뭄 | 5분 | 27분 | + +**셋째 날** 09:30–13:48 (총 4시간 18분 · 이동 42분 · 정거장 5) + +| | 시각 | 자리 | 장소 | 이동 | 체류 | +|---|---|---|---|---|---| +| 1 | 09:30–10:38 | 아침 | 스테이 머뭄 | –분 | 68분 | +| 2 | 10:51–11:18 | 오전 | 구 군산세관 본관 | 13분 | 27분 | +| 3 | 11:30–12:15 | 점심 | 일흥옥 | 12분 | 45분 | +| 4 | 12:23–13:03 | 오후 | 군산근대미술관 | 8분 | 40분 | +| 5 | 13:12–13:48 | 마무리 | 스테이 머뭄 | 9분 | 36분 | + diff --git a/solution/site/scripts/mockup/TEXT.md b/solution/site/scripts/mockup/TEXT.md new file mode 100644 index 0000000..cca1fe4 --- /dev/null +++ b/solution/site/scripts/mockup/TEXT.md @@ -0,0 +1,301 @@ +# /s/stay 시연본 — 들어간 문구 전부 + +`python3 dump_text.py` 산출물. **고치는 자리는 스크립트다** — 이 파일을 고쳐도 반영되지 않는다. + +## 1. 히어로 문구 100개 — `patch_stay.py` + +상호 아래 한 줄. 7초마다 `일반 → 계절 → 일반 → 월 → 일반 → 날씨` 순으로 바뀐다. + +### 일반 40개 (`patch_stay.py` `GENERAL`) + +1. 백 년 된 집에서 하룻밤 +2. 담 너머는 히로쓰 가옥입니다 +3. 독채 두 동, 마당은 각자 씁니다 +4. 기와 아래에서 아침을 맞습니다 +5. 새소리로 눈이 떠지는 집 +6. 골목이 조용해지는 시간에 도착하세요 +7. 침구는 매일 새것처럼 나갑니다 +8. 마당 평상에 앉으면 항구 소리가 들립니다 +9. 1920년대에 지은 집, 지금 쓰는 집 +10. 대문을 닫으면 골목이 멀어집니다 +11. 창고를 고친 카페 공간이 딸려 있습니다 +12. 밤에는 정원 석등만 켜 둡니다 +13. 초원사진관까지 걸어서 3분 +14. 원도심 한가운데, 차는 세워 두세요 +15. A동은 일본식, B동은 모던입니다 +16. 네 사람까지 묶는 독채입니다 +17. 오후 세 시부터 이 집은 손님 것입니다 +18. 이 골목에서는 서두를 일이 없습니다 +19. 백 년 된 기와가 비를 받습니다 +20. 여행이라기보다 하루의 이사입니다 +21. 군산에서 가장 조용한 담 안쪽 +22. 이 집의 시간은 조금 느립니다 +23. 마당, 정원, 그리고 평상 하나 +24. 짐을 풀면 골목부터 걸어 보세요 +25. 창을 열면 옆집 기와가 보입니다 +26. 담배 냄새가 없는 집입니다 +27. 이불에서 볕 냄새가 납니다 +28. 두 동 모두 남의 인기척이 없습니다 +29. 내항까지 걸어 십 분 +30. 오래된 것을 고쳐 쓰는 집 +31. 부엌이 있어 아침은 지어 드셔도 됩니다 +32. 밤에는 골목 등만 남습니다 +33. 백 년 된 집이 아직 손님을 받습니다 +34. 개항장 골목의 끝집입니다 +35. 문패보다 기와를 보고 찾아오세요 +36. 걸어서 도는 여행에 알맞은 자리 +37. 마루에 앉는 시간이 일정의 절반입니다 +38. 좁은 골목 끝, 대문 하나 +39. 여기서는 계획을 반쯤 접어도 됩니다 +40. 하루를 통째로 쓰는 숙소 + +### 계절별 24개 (`SEASON`) + +**봄** +- 정원에 꽃이 먼저 옵니다 +- 봄에는 대문을 열어 둡니다 +- 벚꽃 지는 골목을 걸어 오세요 +- 마당에 앉는 계절이 시작됩니다 +- 새소리가 가장 많은 철입니다 +- 창을 열면 봄이 방까지 들어옵니다 + +**여름** +- 처마 그늘이 가장 긴 계절입니다 +- 마당에 물을 뿌리면 시원해집니다 +- 여름 밤에는 평상이 제일 좋습니다 +- 창고형 카페가 낮에는 서늘합니다 +- 소나기는 기와가 받아 줍니다 +- 해가 길어 저녁이 넉넉합니다 + +**가을** +- 기와 위로 가을 볕이 마릅니다 +- 가을 바람이 담을 넘어옵니다 +- 가을 골목은 해가 빨리 눕습니다 +- 창을 열어 두고 자도 되는 밤입니다 +- 가을에는 평상이 가장 좋은 자리입니다 +- 마당에 잎이 떨어지는 계절입니다 + +**겨울** +- 기와에 눈이 앉으면 골목이 조용해집니다 +- 겨울 아침 마당은 서리 밭입니다 +- 이불 속에서 겨울 바람 소리를 듣습니다 +- 눈 오면 정원 석등만 켜 둡니다 +- 항구 바람이 매운 계절입니다 +- 겨울에는 카페 공간이 가장 아늑합니다 + +### 월별 24개 (`MONTH`) + +**1월** — 새해 첫 아침을 백 년 집에서 · 겨울 골목이 가장 조용한 달입니다 +**2월** — 아직 바람이 찬 원도심입니다 · 겨울 끝을 마당에서 봅니다 +**3월** — 정원에 첫 꽃이 오는 달입니다 · 대문을 열어 두기 시작합니다 +**4월** — 벚꽃 지는 골목을 걷는 달입니다 · 마당에 앉는 시간이 길어집니다 +**5월** — 새소리가 가장 많은 달입니다 · 창을 열어 두고 자는 밤입니다 +**6월** — 처마 그늘이 길어지는 달입니다 · 여름 앞의 원도심은 한가합니다 +**7월** — 소나기를 기와가 받는 달입니다 · 여름 밤 평상이 제일 좋습니다 +**8월** — 낮에는 카페 공간이 서늘합니다 · 해가 길어 저녁이 넉넉한 달입니다 +**9월** — 볕이 마르기 시작하는 달입니다 · 가을이 담을 넘어오는 구월입니다 +**10월** — 잎이 마당에 떨어지는 달입니다 · 걷기에 가장 좋은 달입니다 +**11월** — 해가 빨리 눕는 골목입니다 · 이불이 가장 따뜻한 달입니다 +**12월** — 기와에 눈이 앉는 달입니다 · 한 해의 끝을 조용히 보내는 자리 + +### 날씨별 12개 (`WEATHER`) + +**맑음** +- 마당 평상에 앉으면 새소리만 들립니다 +- 정원 쪽으로 그늘이 길게 눕는 시간입니다 +- 기와가 마르는 냄새가 납니다 + +**구름많음** +- 빛이 부드러운 날입니다 +- 골목 사진이 제일 잘 나오는 빛입니다 + +**흐림** +- 빛이 낮게 깔리는 날입니다 +- 이런 날은 카페 공간이 가장 아늑합니다 + +**비** +- 백 년 된 기와를 타고 내리는 소리가 다릅니다 +- 카페 공간에서 빗소리를 들으실 수 있습니다 +- 비 오는 골목에는 사람이 없습니다 + +**눈** +- 기와에 눈이 앉으면 골목이 통째로 조용해집니다 +- 눈 밟는 소리가 담 안에서 크게 들립니다 + +## 2. 가요 다방 25곡 — `build_story.py` `SONGS` + +| # | 곡명 | 가수 | 작사 / 작곡 | 연도 | 한 줄 | +|---|---|---|---|---|---| +| 1 | 잘 있거라 군산항 | 안다성 | 손석우 / 손석우 | 1963 | 월명산 고갯길로 시작하는 이별가. 군산을 노래한 곡 가운데 가장 널리 알려졌습니다. | +| 2 | 헤어진 군산항 | 박제연 | 송운선 / 송운선 | 1965 | 비 내리는 밤 부두에서 헤어지는 노래입니다. | +| 3 | 님 떠난 군산항 | 이미자 | 조형식 / 백영호 | 1966 | 떠나는 배를 보내는 항구의 정서를 담았습니다. | +| 4 | 금강 나그네 | 이미자 | 이병훈 / 박춘석 | 1966 | 금강 하구와 군산항 부두를 배경으로 한 곡입니다. | +| 5 | 잘 있거라 군산항구 | 최정자 | 최치수 / 유금춘 | 1967 | 떠나는 사람의 말로 부르는 이별가입니다. | +| 6 | 군산 마도로스 | 이상열 | — | 1967 | 항구를 드나들던 뱃사람의 노래입니다. | +| 7 | 군산 부르스 | 유주용 | 반야월 / 강도미 | 1969 | 1960년대 말 군산 거리의 분위기를 담은 곡입니다. | +| 8 | 이별의 군산항 부두 | 최정자 | 최치수 / 유금춘 | 1969 | 부두에서의 이별을 다시 한 번 부른 곡입니다. | +| 9 | 군산항 부르스 | 최하우 | 최하우 / 김성근 | 1984 | 1980년대에 나온 군산항 노래입니다. | +| 10 | 나의 사랑 군산이여 | 오단비 | 이수 / 정종택 | 1989 | 군산을 향한 애정을 정면으로 부르는 곡입니다. | +| 11 | 군산항 엘레지 | 오단비 | 이수 / 정종택 | — | 항구 도시의 비가입니다. | +| 12 | 선유도 아가씨 | 오단비 | 이수 / 서승일 | — | 고군산군도 선유도를 배경으로 한 곡입니다. | +| 13 | 군산찬가 | 오단비 | 이수 / 서승일 | — | 은파와 새만금이 가사에 나오는 군산 찬가입니다. | +| 14 | 오성산 | 오단비 | 이수 / 서승일 | — | 금강 하구를 내려다보는 오성산을 노래했습니다. | +| 15 | 함께하는 군산 | 오은주 | 김동찬 / 김동찬 | — | 군산시가 제작한 음반의 첫 곡입니다. | +| 16 | 새만금 연가 | 채미영 | 김동찬 / 김동찬 · 김수환 | — | 새만금을 소재로 한 곡입니다. | +| 17 | 군산시민의 노래 | 오단비 | 군산문화원 / 군산예총 | — | 금만경 들녘과 서해 바다로 시작하는 시민의 노래입니다. | +| 18 | 군산 아줌마 | 박솔미 | 송두호 / 송두호 | — | 선창가 거리와 새만금이 함께 나오는 곡입니다. | +| 19 | 내 고향 군산항 | 정애란 | 정애란 / 정애란 | — | 고향으로서의 군산항을 부른 곡입니다. | +| 20 | 군산 가는 길 | 황상준 | — | — | 군산으로 향하는 길을 제목으로 삼은 곡입니다. | +| 21 | 군산 추억 | 임인건 | — | — | 재즈 피아니스트 임인건이 군산을 소재로 만든 곡입니다. | +| 22 | 군산에서 | 임인건 | — | — | 같은 연주자가 군산에서 받은 인상을 담았습니다. | +| 23 | 헤어진 군산항 | 김창환 | 송운선 / 송운선 | — | 군산시 제작 음반에 다시 실린 판입니다. | +| 24 | 금강 아가씨 | 이미자 | — | — | 금강을 배경으로 한 이미자의 또 다른 곡입니다. | +| 25 | 금강의 비가 | 전승애 | — | — | 금강을 제목에 올린 곡입니다. | + +출처: 음악의 오솔길 — 군산 관련 노래 + +## 3. 인물 열전 57명 (사진 18장) — `build_story.py` `PEOPLE` + +사진 있는 사람이 앞에 온다. + +| # | 사진 | 이름 | 연도 | 하는 일 | 한 줄 | +|---|---|---|---|---|---| +| 1 | 📷 | 채만식 | 1902–1950 | 소설가 | 군산을 무대로 한 『탁류』를 썼습니다. 시내에 문학관이 있습니다. | +| 2 | 📷 | 고은 | 1933– | 시인 | 군산에서 태어나 60년 넘게 시를 써 온 시인입니다. | +| 3 | 📷 | 김선기 | 1907–1992 | 언어학자 | 호는 무돌. 한글 연구에 힘쓴 언어학자이자 교육자입니다. | +| 4 | 📷 | 김수미 | 1949–2024 | 배우 | 「전원일기」 일용 엄니로 오래 기억되는 배우입니다. | +| 5 | 📷 | 송새벽 | 1979– | 배우 | 군산 출신 배우입니다. | +| 6 | 📷 | 장신영 | 1984– | 배우 | 군산 출신 배우입니다. | +| 7 | 📷 | 이서원 | 1997– | 배우 | 군산 출신 배우입니다. | +| 8 | 📷 | 병헌 | 1993– | 배우 | 그룹 틴탑 활동을 거쳐 배우로 옮겼습니다. | +| 9 | 📷 | 정혁 | 1991– | 모델 | 2015년 서울패션위크로 데뷔한 모델 겸 방송인입니다. | +| 10 | 📷 | 박화요비 | 1982– | 가수 | R&B 를 주로 부르는 가수입니다. | +| 11 | 📷 | 고건 | 1938– | 정치인 | 제30·35대 국무총리를 지냈습니다. 아버지는 철학자 고형곤입니다. | +| 12 | 📷 | 김관영 | 1969– | 정치인 | 전북특별자치도지사입니다. 회계사·변호사를 거쳤습니다. | +| 13 | 📷 | 함운경 | 1964– | 정치인 | 1980년대 학생운동가 출신입니다. | +| 14 | 📷 | 차우찬 | 1987– | 야구 선수 | 투수로 뛰었고 지금은 야구 해설위원입니다. | +| 15 | 📷 | 이진영 | 1980– | 야구 선수 | 외야수로 뛰었고 지금은 코치입니다. | +| 16 | 📷 | 오지환 | 1990– | 야구 선수 | LG 트윈스 내야수입니다. | +| 17 | 📷 | 최서우 | 1982– | 스키점프 선수 | 1998년부터 국가대표로 뛴 스키점프 선수입니다. | +| 18 | 📷 | 문호준 | 1997– | 프로게이머 | 카트라이더 리그 최다 우승자입니다. | +| 19 | | 이병훈 | 1925–2009 | 시인 | 옥구(지금의 군산) 출신으로 1950년대에 등단한 현대 시인입니다. | +| 20 | | 심호택 | 1947–2010 | 시인 | 민중의 삶을 시로 옮긴 시인입니다. | +| 21 | | 문효치 | 1943– | 시인 | 군산에서 태어나 1966년 신춘문예로 등단했습니다. | +| 22 | | 이연주 | 1953–1992 | 시인 | 짧게 활동하고 떠난 시인입니다. | +| 23 | | 주하림 | — | 시인 | 군산에서 태어나 2009년 창비신인시인상으로 등단했습니다. | +| 24 | | 고형곤 | 1906–2004 | 철학자 | 선(禪) 연구로 알려진 철학자이자 정치가입니다. | +| 25 | | 반석평 | 1472–1540 | 조선 문신 | 노비 출신으로 문과에 급제해 판서에 오른 조선 중종 때 문신입니다. | +| 26 | | 장재호 | 1986– | 배우 | 군산 출신 배우입니다. | +| 27 | | 강민지 | 1996– | 배우 · 모델 | 모델로 시작해 드라마와 영화로 활동 범위를 넓혔습니다. | +| 28 | | 김혜선 | 1983– | 희극인 | 군산 출신 희극 배우입니다. | +| 29 | | 이옥주 | 1969– | 희극인 | 군산중앙여고를 나와 1988년 MBC 개그콘테스트 대상을 받았습니다. | +| 30 | | 박윤경 | 1970– | 가수 | 군산 출신 가수 겸 기업인입니다. | +| 31 | | 하준 | 1965– | 가수 | 군산 출신 가수입니다. | +| 32 | | 강봉균 | 1943–2017 | 정치인 | 재정경제부 장관과 3선 국회의원을 지냈습니다. | +| 33 | | 양일동 | 1912–1980 | 정치인 | 옥구군 서수면 출신으로 야당 정치인으로 활동했습니다. | +| 34 | | 채영석 | 1934–2006 | 정치인 | 군산에서 3선 국회의원을 지냈습니다. | +| 35 | | 신영대 | 1968– | 정치인 | 군산 지역구 국회의원입니다. | +| 36 | | 김의겸 | 1963– | 언론인 · 정치인 | 한겨레 기자를 거쳐 청와대 대변인과 국회의원을 지냈습니다. | +| 37 | | 임병찬 | 1851–1916 | 의병장 | 최익현과 함께 거병한 구한말 의병장으로, 단식 끝에 순국했습니다. | +| 38 | | 이수현 | 1895–1980 | 독립운동가 | 호는 산남. 군산 출신 독립운동가입니다. | +| 39 | | 전진 | 1923–2012 | 비전향 장기수 | 군산 출신으로 오래 수감 생활을 했습니다. | +| 40 | | 이길여 | 1932– | 의료인 · 교육자 | 가천대학교 길병원을 세우고 가천대 총장을 맡고 있습니다. | +| 41 | | 최길선 | 1946– | 기업인 | 현대중공업 회장을 지낸 조선업 경영인입니다. | +| 42 | | 은성수 | 1961– | 공무원 | 제7대 금융위원회 위원장을 지냈습니다. | +| 43 | | 채금석 | 1904–1995 | 축구 선수 | 일제강점기 경성 축구단 선수로 경평축구대항전에 나섰습니다. | +| 44 | | 박경완 | 1972– | 야구 선수 | SK 와이번스 포수로 한국 야구의 대표적인 포수였습니다. | +| 45 | | 조계현 | 1964– | 야구 선수 | 해태 타이거즈 투수로 뛰었습니다. | +| 46 | | 정명원 | 1966– | 야구 선수 | 현대 유니콘스 투수로 뛰었습니다. | +| 47 | | 조규제 | 1967– | 야구 선수 | 왼손 투수로 뛰고 지금은 투수코치입니다. | +| 48 | | 이천웅 | 1988– | 야구 선수 | LG 트윈스 외야수로 뛰었습니다. | +| 49 | | 노상래 | 1970– | 축구 선수 | 공격수로 뛰었고 지도자로 일하고 있습니다. | +| 50 | | 노수진 | 1962– | 축구 선수 | 공격수로 뛰었고 지금은 체육 교사입니다. | +| 51 | | 유동우 | 1968– | 축구 선수 | 수비수로 뛰었고 대학 축구부 감독을 맡고 있습니다. | +| 52 | | 이정효 | 1975– | 축구 감독 | 풀백으로 뛰었고 지금은 프로팀 감독입니다. | +| 53 | | 박성현 | 1983– | 양궁 선수 | 군산 소룡초에서 활을 처음 잡아 올림픽 금메달을 땄습니다. | +| 54 | | 김광선 | 1964– | 권투 선수 | 1988년 서울 올림픽 복싱 플라이급 금메달리스트입니다. | +| 55 | | 정경미 | 1985– | 유도 선수 | 군산 출신 유도 선수입니다. | +| 56 | | 전미라 | 1978– | 테니스 선수 | 테니스 선수로 뛰었고 지금은 해설위원입니다. | +| 57 | | 한왕용 | 1966– | 산악인 | 히말라야 8000m 14좌를 세계 11번째로 완등했습니다. | + +## 4. 일정 — 정거장에서 할 일 36줄 — `build_itinerary.py` `PLACES`·`STAY_ACTION` + +| 장소 | 무엇을 하는지 | 머무는 시간 | +|---|---|---| +| 경암동 철길마을 | 선로를 따라 걸으며 옛 간판을 봅니다. 교복을 빌려 입고 찍는 집이 여럿입니다. | 80분 | +| 고군산군도 선유도 | 다리를 건너 들어갑니다. 망주봉 아래 해변을 걷고 스카이라인을 탑니다. | 150분 | +| 구 군산세관 본관 | 빨간 벽돌 본관을 한 바퀴 돌고 앞마당에서 사진을 찍습니다. | 28분 | +| 국제반점 | 영화 「타짜」에 나온 홀에서 짜장과 탕수육을 시킵니다. | 55분 | +| 군산 내항 | 조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다. | 40분 | +| 군산 내항 부잔교 | 뜬다리 위를 끝까지 걸어 나갔다 돌아옵니다. 조수에 따라 다리가 오르내립니다. | 37분 | +| 군산 시간여행마을 | 골목을 정해 두지 말고 걷습니다. 1930년대 간판을 그대로 단 가게가 이어집니다. | 57분 | +| 군산근대건축관 | 옛 조선은행 금고와 은행 창구를 봅니다. 2층 전시까지 보면 45분입니다. | 47분 | +| 군산근대미술관 | 옛 18은행 건물 안 전시를 봅니다. 마당 벤치에서 한 번 쉬어 갑니다. | 44분 | +| 군산근대역사박물관 | 3층부터 내려오며 봅니다. 항구가 어떻게 생겼는지 여기서 먼저 잡고 나갑니다. | 88분 | +| 도란 | 감바스와 부채살 스테이크에 와인 한 잔으로 저녁을 마무리합니다. | 72분 | +| 동국사 | 대웅전 처마와 대나무 숲을 봅니다. 종각까지 돌면 35분입니다. | 33분 | +| 동국사다온 | 대추차나 오미자차를 시켜 절 마당을 보며 마십니다. | 48분 | +| 만남스넥 | 군산식 잡탕 한 냄비를 시킵니다. 떡·만두·라면이 같이 끓어 나옵니다. | 36분 | +| 명월갈비 | 한우 양념갈비 한 상을 시킵니다. 이 집은 단품 하나로 버텨 왔습니다. | 71분 | +| 물밀소 | 저온숙성 빵을 골라 나옵니다. 마당에서 먹을 것으로 두어 개 더 삽니다. | 40분 | +| 빈해원 | 2층 홀에 앉아 짬뽕이나 잡채밥을 시킵니다. 건물 자체가 문화재입니다. | 70분 | +| 스테이 머뭄 | 돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다. | 30분 | +| 스테이 머뭄 | 맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다. | 40분 | +| 스테이 머뭄 | 부엌에서 아침을 차려 먹고 마당에서 커피를 내려 마신 뒤 나섭니다. | 60분 | +| 스테이 머뭄 | 숙소로 돌아와 마당 평상이나 창고형 카페 공간에서 쉽니다. | 173분 | +| 스테이 머뭄 | 아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요. | 75분 | +| 스테이 머뭄 | 오늘은 나가지 않습니다. 마당 평상과 창고형 카페 공간에서 책을 읽고 낮잠을 잡니다. | 223분 | +| 스테이 머뭄 | 짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다. | 45분 | +| 우체통거리 | 주민들이 손질해 내놓은 우체통을 따라 걷습니다. 엽서를 부치고 가도 됩니다. | 26분 | +| 월명호수 | 호수를 한 바퀴 돕니다. 천천히 걸어 40분 코스입니다. | 42분 | +| 이성당 | 단팥빵과 야채빵을 사서 나옵니다. 갓 나오는 시간에 맞추면 줄이 짧습니다. | 50분 | +| 일흥옥 | 소고기 국밥으로 저녁을 먹습니다. 백년가게로 지정된 집입니다. | 52분 | +| 장자도 | 대장봉까지 올라갔다 내려옵니다. 섬과 섬 사이를 걸어 넘습니다. | 50분 | +| 째보선창 | 선창을 따라 걷습니다. 아직 배가 드나드는 자리라 아침이 더 볼 게 많습니다. | 32분 | +| 째보선창 근처 카페 | 창가에 앉아 비가 그칠 때까지 기다립니다. 항구가 보이는 자리입니다. | 69분 | +| 채만식문학관 | 배 모양 전시관을 돌고 금강 쪽 데크로 나가 강을 봅니다. | 66분 | +| 초원사진관 | 안에 들어가 사진관 세트에서 한 장 찍습니다. 걸어서 3분입니다. | 45분 | +| 한일옥 | 무국으로 아침 겸 점심을 엽니다. 줄이 있으면 이름을 적고 기다립니다. | 52분 | +| 해망굴 | 굴을 걸어서 통과해 반대편 항구 쪽으로 나옵니다. | 19분 | +| 히로쓰 가옥 | 담 하나 건너입니다. 마당과 다다미방을 둘러보고 2층 계단까지 올라가 봅니다. | 45분 | + +## 5. 일정 테마 21개 — `build_itinerary.py` `THEMES` + +| 테마 | 기간 | 누구에게 | 왜 | +|---|---|---|---| +| 섬까지 다녀오는 1박 2일 | 1박 2일 | 차를 가져온 손님 | 첫날은 걸어서 도심, 이튿날은 다리를 건너 섬입니다. 두 날의 성격이 아주 다릅니다. | +| 걸어서만 도는 1박 2일 | 1박 2일 | 차 없이 오는 손님 | 모든 정거장이 머뭄에서 걸어 닿습니다. 이틀 동안 차를 한 번도 타지 않습니다. | +| 비가 와도 되는 1박 2일 | 1박 2일 | 비 예보를 보고 오는 손님 | 실내가 이어집니다. 비가 그치기를 기다리지 않아도 하루가 찹니다. | +| 근대건축만 보는 1박 2일 | 1박 2일 | 건물을 보러 오는 손님 | 1900년대 건물만 골라 돕니다. 머뭄도 그 시기에 지은 집입니다. | +| 이성당으로 시작하는 1박 2일 | 1박 2일 | 빵을 좋아하는 손님 | 이튿날 아침을 빵으로 엽니다. 문 여는 시간에 맞춰 걸어갑니다. | +| 철길마을까지 가는 1박 2일 | 1박 2일 | 사진을 찍는 손님 | 빛이 좋은 자리만 이어 붙였습니다. 철길마을은 오전 빛이 낫습니다. | +| 항구만 도는 1박 2일 | 1박 2일 | 바다가 보고 싶은 손님 | 부잔교에서 째보선창까지, 이틀 동안 물가만 걷습니다. | +| 소설 『탁류』를 따라 걷는 1박 2일 | 1박 2일 | 채만식을 읽어 본 손님 | 소설이 지나간 자리를 순서대로 밟습니다. 세관·선창·문학관이 한 줄로 이어집니다. | +| 아이와 함께 1박 2일 | 1박 2일 | 아이를 데려온 손님 | 걷는 거리가 짧고, 아이가 지루해할 자리를 뺐습니다. | +| 늦게 도착하는 1박 2일 | 1박 2일 | 퇴근하고 오는 손님 | 첫날은 저녁 한 끼로 끝냅니다. 대신 이튿날을 아침부터 저녁까지 씁니다. | +| 천천히 도는 2박 3일 | 2박 3일 | 쉬러 오는 손님 | 하루에 두어 곳만 봅니다. 마당에 앉아 있는 시간을 일정에 넣었습니다. | +| 섬과 도심을 반씩 도는 2박 3일 | 2박 3일 | 차를 가져온 손님 | 가운데 날을 섬에 다 씁니다. 첫날과 마지막 날은 걸어서 도심입니다. | +| 골목을 세 번 나눠 걷는 2박 3일 | 2박 3일 | 차 없이 오는 손님 | 원도심을 세 덩이로 나눠 하루에 하나씩 걷습니다. 같은 길을 두 번 걷지 않습니다. | +| 먹으러 오는 2박 3일 | 2박 3일 | 맛집을 도는 손님 | 끼니가 중심입니다. 사이에 걸을 자리를 넣어 배를 비웁니다. | +| 아무 데도 안 가는 2박 3일 | 2박 3일 | 쉬려고만 오는 손님 | 가운데 날은 대문 밖으로 두 번만 나갑니다. 나머지는 마당과 카페 공간에서 보냅니다. | +| 사진만 찍는 2박 3일 | 2박 3일 | 사진을 찍는 손님 | 시간대에 맞춰 자리를 배치했습니다. 철길마을은 오전, 부잔교는 해 질 때입니다. | +| 비가 와도 되는 2박 3일 | 2박 3일 | 비 예보를 보고 오는 손님 | 사흘 내리 비가 와도 도는 순서가 바뀌지 않습니다. 실내와 처마 밑만 잇습니다. | +| 늦게 시작하는 2박 3일 | 2박 3일 | 아침이 힘든 손님 | 가운데 날을 열한 시에 엽니다. 오전을 비우고 저녁을 늦게까지 씁니다. | +| 섬에서 하루를 다 쓰는 2박 3일 | 2박 3일 | 차를 가져온 손님 | 가운데 날은 아침에 나가 저녁에 들어옵니다. 섬에서 하루를 통째로 씁니다. | +| 소설 『탁류』를 따라 걷는 2박 3일 | 2박 3일 | 채만식을 읽어 본 손님 | 소설의 순서대로 사흘을 나눴습니다. 선창에서 시작해 문학관에서 끝냅니다. | +| 철길마을과 항구를 같이 보는 2박 3일 | 2박 3일 | 사진을 찍는 손님 | 기찻길과 물길을 하루씩 나눠 봅니다. 마지막 날은 건물만 봅니다. | + +## 6. 오늘의 엽서 4장 — 원본 payload (아직 다시 안 뽑음) + +- 부잔교 위에서는 물이 어디까지 올라왔었는지가 다리에 새겨져 있다. `#군산 #내항 #부잔교` +- 짬뽕 한 그릇이 항구도시의 이력서인 줄은 몰랐다. `#군산짬뽕 #항구도시` +- 단팥빵을 사러 줄을 서는 일이 여행이 되는 도시. `#이성당 #오래된가게` +- 섬과 섬 사이를 다리로 건너면, 바다가 길이 된다. `#고군산군도 #선유도` + +## 7. 머뭄이 만든 노래 5곡 — `patch_stay.py` `OWN_SONGS` + +- 머뭄의 아침 — `/s/stay/audio/01-mumum-morning.mp3` +- CHECK IN · 머뭄 — `/s/stay/audio/02-check-in.mp3` +- Stay in 머뭄 — `/s/stay/audio/03-stay-in.mp3` +- 오늘은 안 갈란다 — `/s/stay/audio/04-not-going.mp3` +- 군산에 두고 온 님 — `/s/stay/audio/05-gunsan-nim.mp3` diff --git a/solution/site/scripts/mockup/audio/01-mumum-morning.mp3 b/solution/site/scripts/mockup/audio/01-mumum-morning.mp3 new file mode 100644 index 0000000..d7920ff Binary files /dev/null and b/solution/site/scripts/mockup/audio/01-mumum-morning.mp3 differ diff --git a/solution/site/scripts/mockup/audio/02-check-in.mp3 b/solution/site/scripts/mockup/audio/02-check-in.mp3 new file mode 100644 index 0000000..8916c1f Binary files /dev/null and b/solution/site/scripts/mockup/audio/02-check-in.mp3 differ diff --git a/solution/site/scripts/mockup/audio/03-stay-in.mp3 b/solution/site/scripts/mockup/audio/03-stay-in.mp3 new file mode 100644 index 0000000..b5ffd24 Binary files /dev/null and b/solution/site/scripts/mockup/audio/03-stay-in.mp3 differ diff --git a/solution/site/scripts/mockup/audio/04-not-going.mp3 b/solution/site/scripts/mockup/audio/04-not-going.mp3 new file mode 100644 index 0000000..68e2583 Binary files /dev/null and b/solution/site/scripts/mockup/audio/04-not-going.mp3 differ diff --git a/solution/site/scripts/mockup/audio/05-gunsan-nim.mp3 b/solution/site/scripts/mockup/audio/05-gunsan-nim.mp3 new file mode 100644 index 0000000..e822d72 Binary files /dev/null and b/solution/site/scripts/mockup/audio/05-gunsan-nim.mp3 differ diff --git a/solution/site/scripts/mockup/audit-all.mjs b/solution/site/scripts/mockup/audit-all.mjs new file mode 100644 index 0000000..be78713 --- /dev/null +++ b/solution/site/scripts/mockup/audit-all.mjs @@ -0,0 +1,128 @@ +/* /s/stay 시연본 전수 점검 — 사장님이 지시한 항목을 하나씩 화면에서 센다. */ +import {chromium} from 'playwright'; +const URL = process.argv[2] || 'http://localhost/s/stay'; +const b = await chromium.launch({channel: 'chrome'}); +const out = []; +const ok = (name, pass, detail) => out.push({name, pass, detail}); + +// ── 데스크톱 ────────────────────────────────────────────────────────── +const page = await b.newPage({viewport: {width: 1280, height: 900}}); +const failed = []; +page.on('response', (r) => { if (r.status() >= 400 && r.url().includes('/s/stay')) failed.push(r.status() + ' ' + decodeURIComponent(r.url().slice(-36))); }); +const errs = []; +page.on('console', (m) => { if (m.type() === 'error') errs.push(m.text().slice(0, 90)); }); +await page.goto(URL, {waitUntil: 'domcontentloaded'}); +await page.waitForTimeout(6500); + +const d = await page.evaluate(() => { + const P = window.__SITE_PAYLOAD__; + const sec = (id) => { const s = P.theme.sections.find((x) => x.id === id); try { return JSON.parse(s.data); } catch { return {items: []}; } }; + const itin = sec('itinerary').items; + const stops = itin.flatMap((i) => i.days.flatMap((day) => day.stops)); + const days = itin.flatMap((i) => i.days); + return { + catch: (P.narrative.catchphrases?.items || []).length, + catchKinds: ['general', 'season', 'month', 'weather'].map((k) => (P.narrative.catchphrases?.items || []).filter((x) => x.kind === k).length), + themes: itin.length, + firstIsStay: days.every((day) => day.stops[0].name.endsWith('스테이 머뭄')), + lastIsStay: days.every((day) => day.stops[day.stops.length - 1].name.endsWith('스테이 머뭄')), + consecutive: days.filter((day) => day.stops.some((s, i) => i && s.name.split(' · ').pop() === day.stops[i - 1].name.split(' · ').pop())).length, + hasNote: stops.every((s) => s.name.endsWith('스테이 머뭄') || s.note), + stopImages: stops.filter((s) => s.imageUrl).length, + restDays: days.filter((day) => day.stops.some((s) => s.name.startsWith('머뭄에서 쉬는 날'))).length, + endTimes: (() => { const t = days.map((day) => { let c = +day.startTime.slice(0, 2) * 60 + +day.startTime.slice(3); day.stops.forEach((s) => { c += s.moveMinutes + s.minutes; }); return c; }); return [Math.min(...t), Math.max(...t)]; })(), + songs: sec('songs').items.length, + ownInSongs: sec('songs').items.filter((s) => s.own).length, + songLinks: sec('songs').items.filter((s) => s.listenUrl).length, + people: sec('people').items.length, + peoplePhotos: sec('people').items.filter((p) => p.imageUrl).length, + peopleCredits: sec('people').items.filter((p) => p.imageCredit).length, + // 사진 있는 사람이 앞에 모여 있는가 — 레일 앞머리가 글자만 남으면 안 된다 + photoFirst: (() => { const a = sec('people').items.map((p) => !!p.imageUrl); const n = a.filter(Boolean).length; return a.slice(0, n).every(Boolean); })(), + ownSongs: (P.narrative.ownSongs?.items || []).length, + // 화면 + domPeopleImg: document.querySelectorAll('#people img').length, + domCredit: document.querySelectorAll('#people .w4d-credit').length, + domItinLinks: document.querySelectorAll('#itinerary a.w4d-link').length, + domPostcardLinks: document.querySelectorAll('#postcard a.w4d-link').length, + domItinImg: document.querySelectorAll('#itinerary img:not([src*="openstreetmap"])').length, + miniBtns: [...document.querySelectorAll('#w4d-mini button')].map((x) => x.id), + cassette: !!document.getElementById('w4d-tape'), + redRule: document.querySelectorAll('.w4d-rule').length, + bbqRow: !!document.body.textContent.match(/바비큐 이용/), + divider: (() => { const el = document.querySelector('#root div.border-line.flex.flex-col.gap-3.border-t.pt-6'); return el ? getComputedStyle(el).borderTopWidth : 'none'; })(), + heroImg: (P.media.find((m) => m.isPrimary) || {}).url, + roomA: P.units[0].mediaIds.length, roomB: P.units[1].mediaIds.length, + roomMirror: P.units.flatMap((u) => u.mediaIds).length, + tagline: document.querySelector('#root h1')?.nextElementSibling?.textContent.trim(), + }; +}); + +ok('캐치프레이즈 100개', d.catch === 100, `${d.catch}개 · 일반${d.catchKinds[0]}/계절${d.catchKinds[1]}/월${d.catchKinds[2]}/날씨${d.catchKinds[3]}`); +ok('캐치프레이즈 순환', d.tagline && d.tagline !== '히로쓰 가옥 담 너머, 백 년 된 집 한 채', `현재 "${d.tagline}"`); +ok('일정 테마', d.themes === 21, `${d.themes}개`); +ok('1번 = 스테이 머뭄', d.firstIsStay, d.firstIsStay ? '전 일자' : '어긋남'); +ok('마지막 = 스테이 머뭄', d.lastIsStay, d.lastIsStay ? '전 일자' : '어긋남'); +ok('같은 자리 연속 없음', d.consecutive === 0, `${d.consecutive}건`); +ok('정거장마다 할 일 문구', d.hasNote, d.hasNote ? '전 정거장' : '빠짐'); +ok('일정 사진 없음', d.stopImages === 0 && d.domItinImg === 0, `payload ${d.stopImages} · 화면 ${d.domItinImg}`); +ok('숙소에서 쉬는 날', d.restDays >= 3, `${d.restDays}일`); +ok('종료 시각 분산', d.endTimes[1] - d.endTimes[0] > 240, `${(d.endTimes[0] / 60 | 0)}:${String(d.endTimes[0] % 60).padStart(2, '0')} ~ ${(d.endTimes[1] / 60 | 0)}:${String(d.endTimes[1] % 60).padStart(2, '0')}`); +ok('가요 다방 곡', d.songs >= 25, `${d.songs}곡`); +ok('가요 다방에 자작곡 없음', d.ownInSongs === 0, `${d.ownInSongs}곡`); +ok('노래마다 링크', d.songLinks === d.songs, `${d.songLinks}/${d.songs}`); +ok('인물 50명 이상', d.people >= 50, `${d.people}명`); +ok('인물 사진', d.peoplePhotos >= 18 && d.domPeopleImg === d.peoplePhotos, `payload ${d.peoplePhotos} · 화면 ${d.domPeopleImg}`); +// 저작자 표시는 **위키에서 받은 사진에만** 붙는다(라이선스 조건). 원본 사이트가 원래 쓰던 +// 미러 사진(채만식)은 이 사이트 것이라 표시 대상이 아니다 — 그래서 두 수가 다르다. +ok('사진 있는 사람이 앞', d.photoFirst, d.photoFirst ? `앞 ${d.peoplePhotos}명` : '흩어져 있다'); +ok('사진 저작자 표시', d.domCredit === d.peopleCredits, `${d.domCredit}/${d.peopleCredits} (미러 ${d.peoplePhotos - d.peopleCredits}장 제외)`); +ok('일정 지도 링크', d.domItinLinks > 150, `${d.domItinLinks}개`); +ok('엽서 지도 링크', d.domPostcardLinks === 4, `${d.domPostcardLinks}개`); +ok('플레이어 단추 2개', d.miniBtns.length === 2 && !d.miniBtns.includes('w4d-next'), d.miniBtns.join(',')); +ok('카세트 아이콘', d.cassette, d.cassette ? '있음' : '없음'); +ok('자작곡 5곡', d.ownSongs === 5, `${d.ownSongs}곡`); +ok('빨간 줄 없음', d.redRule === 0, `${d.redRule}개`); +ok('바비큐 항목 유지', d.bbqRow, d.bbqRow ? '있음' : '사라짐'); +ok('맨 아래 가로선 제거', d.divider === '0px', d.divider); +ok('히어로 원본 사진', /\/img\/mirror\//.test(d.heroImg || ''), d.heroImg); +ok('객실 사진 A12/B10', d.roomA === 12 && d.roomB === 10, `A ${d.roomA} · B ${d.roomB}`); +ok('404 없음', failed.length === 0, failed.slice(0, 3).join(' / ') || '없음'); +ok('콘솔 오류 없음', errs.length === 0, errs.slice(0, 2).join(' / ') || '없음'); + +// 플레이어 동작 +await page.click('#w4d-toggle'); await page.waitForTimeout(350); +await page.click('.w4d-item'); await page.waitForTimeout(2200); +const play = await page.evaluate(() => { + const row = document.querySelector('.w4d-item[data-playing="1"]'); + return {playing: !!row, time: row?.querySelector('.w4d-time')?.textContent, + btn: document.getElementById('w4d-play')?.dataset.playing}; +}); +ok('음원 재생', play.playing && /\d:\d\d \/ \d:\d\d/.test(play.time || ''), `${play.time}`); +await page.close(); + +// ── 모바일 ──────────────────────────────────────────────────────────── +const m = await b.newPage({viewport: {width: 390, height: 780}, isMobile: true, hasTouch: true}); +await m.goto(URL, {waitUntil: 'domcontentloaded'}); +await m.waitForTimeout(5500); +await m.click('#w4d-toggle'); await m.waitForTimeout(400); +const mob = await m.evaluate(() => { + const p = document.getElementById('w4d-panel').getBoundingClientRect(); + const nav = [...document.querySelectorAll('#root nav')].find((n) => getComputedStyle(n).position === 'fixed'); + const mini = document.getElementById('w4d-mini').getBoundingClientRect(); + const rows = [...document.querySelectorAll('.w4d-item')].map((el) => Math.round(el.getBoundingClientRect().height)); + return {panelIn: p.left >= 0 && p.right <= window.innerWidth, panelW: Math.round(p.width), + rowMin: Math.min(...rows), tabbarOverlap: nav ? mini.bottom > nav.getBoundingClientRect().top : false, + bodyScrollX: document.documentElement.scrollWidth > window.innerWidth}; +}); +ok('모바일 패널 화면 안', mob.panelIn, `폭 ${mob.panelW}px`); +ok('모바일 줄 높이 ≥48px', mob.rowMin >= 48, `${mob.rowMin}px`); +ok('모바일 탭바 안 겹침', !mob.tabbarOverlap, mob.tabbarOverlap ? '겹침' : '안 겹침'); +ok('가로 스크롤 없음', !mob.bodyScrollX, mob.bodyScrollX ? '생김' : '없음'); +await m.close(); +await b.close(); + +const bad = out.filter((x) => !x.pass); +out.forEach((x) => console.log(`${x.pass ? ' OK ' : ' ✗✗ '} ${x.name.padEnd(22)} ${x.detail}`)); +console.log(`\n${out.length}개 중 통과 ${out.length - bad.length} · 실패 ${bad.length}`); +process.exit(bad.length ? 1 : 0); diff --git a/solution/site/scripts/mockup/audit_schedule.py b/solution/site/scripts/mockup/audit_schedule.py new file mode 100644 index 0000000..00d0a39 --- /dev/null +++ b/solution/site/scripts/mockup/audit_schedule.py @@ -0,0 +1,181 @@ +"""구워진 payload 의 일정 21개를 표로 뽑고, 규칙 14종이 **안 보는** 논리까지 검수한다. + +★ 왜 따로 있나 — build_itinerary.py 의 audit() 는 "만들 때" 지키는 규칙이다. + 이 스크립트는 **만들어진 결과**를 사람이 읽는 표로 펴고, 비율·간격처럼 + 규칙에 없던 것을 센다. 사장님이 화면에서 찾아내신 종류가 전부 여기 있다. +""" +import json, re, sys +from pathlib import Path + +SP = Path(__file__).resolve().parent +html = (SP / "build" / "index.html").read_text(encoding="utf-8") +payload = json.loads(re.search(r"window\.__SITE_PAYLOAD__\s*=\s*(\{.*?\});?\s*", html, re.S).group(1)) +sec = next(s for s in payload["theme"]["sections"] if s["id"] == "itinerary") +items = json.loads(sec["data"])["items"] + +STAY = "스테이 머뭄" +MEAL_SLOTS = {"점심", "늦은 점심", "저녁"} + + +def hm(m): return f"{m // 60:02d}:{m % 60:02d}" + + +def plan(day): + """렌더러(shared planDay)와 같은 계산. 21시 넘는 칸은 렌더러가 버린다.""" + clock = int(day["startTime"][:2]) * 60 + int(day["startTime"][3:]) + rows, dropped = [], 0 + for s in day["stops"]: + move = max(0, s.get("moveMinutes", 0)) + stay = max(1, s.get("minutes", 60)) + arrive = clock + move + if arrive + stay > 21 * 60: + dropped += 1 + continue + rows.append({"name": s["name"], "slot": s["name"].split(" · ")[0], + "place": s["name"].split(" · ")[-1], "move": move, "stay": stay, + "from": arrive, "to": arrive + stay}) + clock = arrive + stay + return rows, dropped + + +def check(item): + """규칙 14종이 안 보는 논리. (코드, 설명) 목록을 돌려준다.""" + bad = [] + name = item["name"] + seen_places = [] + for day in item["days"]: + rows, dropped = plan(day) + label = day["label"] + if dropped: + bad.append(("L0", f"{label}: 21시를 넘겨 화면에서 버려지는 칸 {dropped}개")) + if not rows: + bad.append(("L0", f"{label}: 칸이 없다")) + continue + + outs = [r for r in rows if r["place"] != STAY] + # L1 체류 ≥ 편도 이동 — 가는 데 든 시간보다 짧게 머물면 다녀온 의미가 없다. + # ★ 끼니는 뺀다. 밥집은 목적지가 아니라 **돌아오는 길에 들르는 자리**다 — + # 섬에서 85분 달려와 55분 먹는 것은 이상하지 않고, 섬에 30분 있는 것이 이상하다. + # (2026-09-11 사장님 지적의 실제 대상은 선유도였고, 선유도는 150분이다.) + for r in outs: + if r["slot"] in MEAL_SLOTS: + continue + if r["stay"] < r["move"]: + bad.append(("L1", f"{label} {r['place']}: 이동 {r['move']}분인데 체류 {r['stay']}분")) + # L2 끼니 — 둘째 날부터는 점심과 저녁이 다 있어야 한다(마지막 날은 저녁 없이 떠난다) + meals = [r for r in rows if r["slot"] in MEAL_SLOTS] + is_first = label == item["days"][0]["label"] + is_last = label == item["days"][-1]["label"] + if not meals: + bad.append(("L2", f"{label}: 끼니가 하나도 없다")) + elif not is_first and not is_last: + slots = {r["slot"] for r in meals} + if not (slots & {"점심", "늦은 점심"}) or "저녁" not in slots: + bad.append(("L2", f"{label}: 점심/저녁 중 하나가 없다 ({sorted(slots)})")) + # L3 끼니 간격 — 점심과 저녁이 3시간 미만이면 배가 안 꺼진다 + lunch = next((r for r in meals if r["slot"] in {"점심", "늦은 점심"}), None) + dinner = next((r for r in meals if r["slot"] == "저녁"), None) + if lunch and dinner and dinner["from"] - lunch["to"] < 180: + bad.append(("L3", f"{label}: 점심 끝나고 저녁까지 {dinner['from'] - lunch['to']}분")) + # L4 하루 이동 비율 — 절반을 길에서 쓰면 그건 관광이 아니라 운전이다 + span = rows[-1]["to"] - rows[0]["from"] + move_sum = sum(r["move"] for r in rows) + if span and move_sum / span > 0.40: + bad.append(("L4", f"{label}: 하루 {span}분 중 이동 {move_sum}분 ({move_sum / span:.0%})")) + # L5 너무 짧은 정거장 — 15분은 들렀다 가는 것도 아니다 + for r in outs: + if r["stay"] < 15: + bad.append(("L5", f"{label} {r['place']}: 체류 {r['stay']}분")) + # L6 하루 길이 — 14시간을 넘으면 손님이 못 따라온다 + if span > 14 * 60: + bad.append(("L6", f"{label}: 하루가 {span // 60}시간 {span % 60}분")) + # L7 첫날 입실 15:00 이후 · 마지막 날 퇴실 11:00 전 + if is_first and rows[0]["place"] == STAY and rows[0]["from"] < 15 * 60: + bad.append(("L7", f"{label}: 입실이 {hm(rows[0]['from'])}")) + if is_last and rows[0]["place"] == STAY and rows[0]["to"] > 11 * 60: + bad.append(("L7", f"{label}: 퇴실이 {hm(rows[0]['to'])}")) + # L8 시작·끝이 숙소인가 + if rows[0]["place"] != STAY: + bad.append(("L8", f"{label}: 첫 칸이 {rows[0]['place']}")) + if rows[-1]["place"] != STAY: + bad.append(("L8", f"{label}: 마지막 칸이 {rows[-1]['place']}")) + # L9 같은 자리 연속 + for i in range(1, len(rows)): + if rows[i]["place"] == rows[i - 1]["place"]: + bad.append(("L9", f"{label}: {rows[i]['place']} 가 연달아")) + seen_places += [r["place"] for r in outs] + # L10 테마 안에서 같은 곳 두 번 + dup = {p for p in seen_places if seen_places.count(p) > 1} + if dup: + bad.append(("L10", f"테마 안에서 반복: {', '.join(sorted(dup))}")) + # L11 테마 이름·대상과 내용 — '걸어서'/'차 없이' 인데 편도 25분 넘는 이동이 있나 + walkish = any(k in name + item.get("audience", "") for k in ("걸어서", "차 없이", "뚜벅")) + if walkish: + for day in item["days"]: + rows, _ = plan(day) + far = [r for r in rows if r["move"] > 25] + if far: + bad.append(("L11", f"걷는 테마인데 {far[0]['place']} 이동 {far[0]['move']}분")) + return bad + + +lines = ["# `/s/stay` 여행 일정 전수 검수표", "", + f"생성 `python3 audit_schedule.py` · 테마 {len(items)}개 · " + f"{sum(len(i['days']) for i in items)}일 · " + f"{sum(len(d['stops']) for i in items for d in i['days'])}정거장", "", + "표는 **구워진 payload 를 렌더러와 같은 방식으로 계산한 값**이다(화면에 뜨는 것과 같다).", + "규칙 14종(만들 때 거는 것)은 `build_itinerary.py` `audit()` 가 이미 통과시켰고,", + "이 표는 그 규칙이 **안 보는 논리**를 따로 센다.", "", + "| 코드 | 검수 항목 |", "|---|---|", + "| L0 | 21시를 넘겨 화면에서 버려지는 칸이 없다 |", + "| L1 | **체류 ≥ 편도 이동** — 볼거리는 가는 데 든 시간보다 오래 머문다 (끼니 제외 — 밥집은 돌아오는 길에 들르는 자리다) |", + "| L2 | 가운데 날은 점심과 저녁이 둘 다 있다 |", + "| L3 | 점심 끝나고 저녁까지 3시간 이상 |", + "| L4 | 하루의 40% 넘게 길에서 쓰지 않는다 |", + "| L5 | 바깥 정거장 체류 15분 이상 |", + "| L6 | 하루가 14시간을 넘지 않는다 |", + "| L7 | 입실 15:00 이후 · 퇴실 11:00 전 |", + "| L8 | 하루의 첫 칸·마지막 칸이 숙소 |", + "| L9 | 같은 자리가 연달아 서지 않는다 |", + "| L10 | 한 테마 안에서 같은 곳을 두 번 가지 않는다 |", + "| L11 | 걷는 테마에 편도 25분 넘는 이동이 없다 |", ""] + +fails = [] +for item in items: + bad = check(item) + mark = "✅ **검수완료**" if not bad else "⚠️ **확인필요**" + lines += ["---", "", f"## {item['name']}", "", + f"{item['duration']} · {item.get('audience', '')} — {mark}", "", + f"> {item.get('why', '')}", ""] + for day in item["days"]: + rows, _ = plan(day) + span = rows[-1]["to"] - rows[0]["from"] + move_sum = sum(r["move"] for r in rows) + lines += [f"**{day['label']}** {hm(rows[0]['from'])}–{hm(rows[-1]['to'])} " + f"(총 {span // 60}시간 {span % 60}분 · 이동 {move_sum}분 · 정거장 {len(rows)})", "", + "| | 시각 | 자리 | 장소 | 이동 | 체류 |", "|---|---|---|---|---|---|"] + for n, r in enumerate(rows, 1): + lines.append(f"| {n} | {hm(r['from'])}–{hm(r['to'])} | {r['slot']} | {r['place']} | " + f"{r['move'] or '–'}분 | {r['stay']}분 |") + lines.append("") + if bad: + fails.append((item["name"], bad)) + lines += ["**확인이 필요한 것**", ""] + lines += [f"- `{code}` {msg}" for code, msg in bad] + lines.append("") + +head = [f"## 결과 — {len(items) - len(fails)}/{len(items)} 검수완료", ""] +if fails: + head += ["| 테마 | 걸린 항목 |", "|---|---|"] + head += [f"| {n} | {', '.join(sorted({c for c, _ in b}))} |" for n, b in fails] +else: + head += ["**전부 통과.** 12개 항목 위반 0건."] +head.append("") +lines = lines[:8] + head + lines[8:] + +(SP / "SCHEDULE.md").write_text("\n".join(lines) + "\n", encoding="utf-8") +print(f"SCHEDULE.md · 검수완료 {len(items) - len(fails)}/{len(items)}") +for n, b in fails: + print(f" ⚠️ {n}") + for code, msg in b: + print(f" {code} {msg}") diff --git a/solution/site/scripts/mockup/build_itinerary.py b/solution/site/scripts/mockup/build_itinerary.py new file mode 100644 index 0000000..25d61b9 --- /dev/null +++ b/solution/site/scripts/mockup/build_itinerary.py @@ -0,0 +1,681 @@ +"""/s/stay 목업의 '추천 일정' 을 짠다 — 펜션에서 나서고 펜션으로 돌아오는 하루. + +★ 왜 스크립트인가 + 20개 테마 × 하루 5~6칸을 손으로 적으면 시각이 어긋난다. 렌더러(shared `planDay`)는 + **21시를 넘기는 칸을 조용히 버린다** — 적어 놓고 화면에 안 나오는 칸이 생긴다. + 그래서 사람은 **어느 자리에 어느 장소**만 고르고, 시각은 여기서 계산한다. + +★ 2026-09-10 전면 재작성. 앞선 판이 논리적으로 틀린 자리가 네 군데였다(전수 감사). + ① 이동시간을 슬롯에 박아 뒀다 — 담 하나 건너인 히로쓰 가옥이 8분, 45분 걸리는 선유도가 10분. + → **좌표에서 계산한다**(1.5km 까지 도보 4km/h, 그 위는 차 25km/h + 주차 5분). + ② 체류시간도 슬롯이 정했다 — '오후' 칸이면 무엇이든 90~115분이라 우체통거리에 2시간 서 있었다. + → **장소마다 (최소·기본·최대)** 를 준다. + ③ 입실이 14:30 에 잡혔다(payload `check_in_time` 은 15:00), 퇴실은 11:35(규정 11:00). + ④ 점심이 09:40, 저녁이 13:09 에 잡혔다. + → ③④ 는 **창(WINDOW)** 으로 못 박고, 앞 칸 체류를 범위 안에서 늘이고 줄여 맞춘다. + 맞추지 못하면 시작 시각을 당긴다. 그래도 안 되면 그 조합은 만들지 않는다(조용히 틀리게 두지 않는다). + +★ 지어내지 않는다. 좌표는 payload(기존 정거장)와 카카오 로컬 검색(새 장소)에서 온 값이고, + 한 줄 설명은 **거기서 무엇을 하는지**다 — 장소 소개는 이미 '주변 정보' 섹션이 한다. +""" +import json +import math +from pathlib import Path + +SP = Path(__file__).parent +SRC = SP / "stay-payload.json" +DST = SP / "stay-payload-new.json" + +payload = json.loads(SRC.read_text(encoding="utf-8")) +SECT = {s["id"]: s for s in payload["theme"]["sections"]} + +BASE = (payload["place"]["latitude"], payload["place"]["longitude"]) +STAY = "스테이 머뭄" + + +# ── 장소 대장 ───────────────────────────────────────────────────────────────── +# (위도, 경도, 지도 검색어, (최소·기본·최대 체류분), 거기서 무엇을 하는지) +# ★ 설명이 아니라 **할 일**이다 (2026-09-10 사장님: "가서 뭐 할지를 추천해 줘야지"). +# "1899년에 지어진 건물입니다" 는 손님이 거기서 뭘 해야 하는지 안 알려 준다. +PLACES = { + STAY: (35.9864175, 126.7061254, None, (30, 40, 60), None), + + "히로쓰 가옥": (35.9861696, 126.7060575, "군산 신흥동 일본식가옥", (20, 30, 45), + "담 하나 건너입니다. 마당과 다다미방을 둘러보고 2층 계단까지 올라가 봅니다."), + "초원사진관": (35.9877370, 126.7083826, "군산 초원사진관", (20, 30, 45), + "안에 들어가 사진관 세트에서 한 장 찍습니다. 걸어서 3분입니다."), + "군산근대역사박물관": (35.9908197, 126.7121232, "군산근대역사박물관", (60, 90, 120), + "3층부터 내려오며 봅니다. 항구가 어떻게 생겼는지 여기서 먼저 잡고 나갑니다."), + "군산근대건축관": (35.9894202, 126.7121232, "군산근대건축관", (30, 45, 70), + "옛 조선은행 금고와 은행 창구를 봅니다. 2층 전시까지 보면 45분입니다."), + "군산근대미술관": (35.9899368, 126.7108000, "군산근대미술관", (30, 45, 70), + "옛 18은행 건물 안 전시를 봅니다. 마당 벤치에서 한 번 쉬어 갑니다."), + "구 군산세관 본관": (35.9915088, 126.7135000, "구 군산세관", (20, 30, 45), + "빨간 벽돌 본관을 한 바퀴 돌고 앞마당에서 사진을 찍습니다."), + "해망굴": (35.9913181, 126.7050000, "군산 해망굴", (15, 20, 30), + "굴을 걸어서 통과해 반대편 항구 쪽으로 나옵니다."), + "경암동 철길마을": (35.9813520, 126.7362331, "군산 경암동 철길마을", (35, 50, 80), + "선로를 따라 걸으며 옛 간판을 봅니다. 교복을 빌려 입고 찍는 집이 여럿입니다."), + "군산 내항 부잔교": (35.9892997, 126.7174508, "군산 내항 부잔교", (25, 35, 55), + "뜬다리 위를 끝까지 걸어 나갔다 돌아옵니다. 조수에 따라 다리가 오르내립니다."), + "째보선창": (35.9875345, 126.7199875, "군산 째보선창", (20, 30, 45), + "선창을 따라 걷습니다. 아직 배가 드나드는 자리라 아침이 더 볼 게 많습니다."), + "군산 시간여행마을": (35.9883192, 126.7109377, "군산 시간여행마을", (40, 60, 90), + "골목을 정해 두지 말고 걷습니다. 1930년대 간판을 그대로 단 가게가 이어집니다."), + "월명호수": (35.9766391, 126.6913696, "군산 월명호수", (35, 50, 80), + "호수를 한 바퀴 돕니다. 천천히 걸어 40분 코스입니다."), + "은적사": (35.9748304, 126.6854726, "군산 은적사", (30, 40, 60), + "대웅전까지 올라갔다 내려옵니다. 절 마당에서 한 번 앉아 갑니다."), + "채만식문학관": (36.0085981, 126.7578853, "군산 채만식문학관", (40, 60, 90), + "배 모양 전시관을 돌고 금강 쪽 데크로 나가 강을 봅니다."), + "동국사": (35.9827663, 126.7074296, "군산 동국사", (25, 35, 50), + "대웅전 처마와 대나무 숲을 봅니다. 종각까지 돌면 35분입니다."), + "우체통거리": (35.9859965, 126.7119708, "군산 우체통거리", (15, 25, 40), + "주민들이 손질해 내놓은 우체통을 따라 걷습니다. 엽서를 부치고 가도 됩니다."), + "고군산군도 선유도": (35.8102050, 126.4124053, "군산 선유도", (120, 150, 210), + "다리를 건너 들어갑니다. 망주봉 아래 해변을 걷고 스카이라인을 탑니다."), + "장자도": (35.8101000, 126.3973000, "군산 장자도", (40, 50, 80), + "대장봉까지 올라갔다 내려옵니다. 섬과 섬 사이를 걸어 넘습니다."), + "군산 내항": (35.9892997, 126.7174508, "군산 내항", (30, 40, 60), + "조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다."), + + # 끼니 — 무엇을 먹는지까지 적는다. "맛집입니다" 로는 무엇을 시킬지 모른다. + "빈해원": (35.9888841, 126.7139765, "군산 빈해원", (55, 70, 90), + "2층 홀에 앉아 짬뽕이나 잡채밥을 시킵니다. 건물 자체가 문화재입니다."), + "일흥옥": (35.9851498, 126.7091839, "군산 일흥옥", (40, 50, 65), + "소고기 국밥으로 저녁을 먹습니다. 백년가게로 지정된 집입니다."), + "한일옥": (35.9874081, 126.7100000, "군산 한일옥", (40, 50, 65), + "무국으로 아침 겸 점심을 엽니다. 줄이 있으면 이름을 적고 기다립니다."), + "명월갈비": (35.9863914, 126.7101663, "군산 명월갈비", (60, 75, 95), + "한우 양념갈비 한 상을 시킵니다. 이 집은 단품 하나로 버텨 왔습니다."), + "국제반점": (35.9881100, 126.7108985, "군산 국제반점", (45, 55, 70), + "영화 「타짜」에 나온 홀에서 짜장과 탕수육을 시킵니다."), + "만남스넥": (35.9841788, 126.7097763, "군산 만남스넥", (30, 40, 55), + "군산식 잡탕 한 냄비를 시킵니다. 떡·만두·라면이 같이 끓어 나옵니다."), + "도란": (35.9868220, 126.7075100, "군산 도란", (60, 80, 100), + "감바스와 부채살 스테이크에 와인 한 잔으로 저녁을 마무리합니다."), + "물밀소": (35.9845243, 126.7073211, "군산 물밀소", (25, 35, 50), + "저온숙성 빵을 골라 나옵니다. 마당에서 먹을 것으로 두어 개 더 삽니다."), + "이성당": (35.9870443, 126.7111681, "군산 이성당", (25, 35, 50), + "단팥빵과 야채빵을 사서 나옵니다. 갓 나오는 시간에 맞추면 줄이 짧습니다."), + "째보선창 근처 카페": (35.9875345, 126.7199875, "군산 째보선창 카페", (45, 60, 90), + "창가에 앉아 비가 그칠 때까지 기다립니다. 항구가 보이는 자리입니다."), + "동국사다온": (35.9860907, 126.7144400, "군산 동국사 다온", (30, 40, 55), + "대추차나 오미자차를 시켜 절 마당을 보며 마십니다."), +} + +# 펜션 칸은 장소가 아니라 **행위**가 시간을 정한다. +STAY_ACTION = { + "체크인": ((30, 40, 50), "짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다."), + "머뭄으로": ((20, 30, 45), "돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다."), + # ★ 오후에 한 번 숙소로 돌아온다. 넣은 이유가 둘이다. + # ① 점심(≤13:30)과 저녁(≥17:30) 사이 네 시간을 바깥 두 곳으로는 못 채운다 — + # 억지로 채우면 골목 하나에 두 시간 서 있는 일정이 된다(그래서 실제로 그랬다). + # ② 독채 펜션에 묵는 손님은 실제로 그렇게 한다. 이 집이 목적지인 일정이다. + "쉼": ((30, 70, 190), "숙소로 돌아와 마당 평상이나 창고형 카페 공간에서 쉽니다."), + # ★ 하루의 1번은 **언제나 머뭄**이다 (2026-09-10 사장님: "1은 스테이 머뭄이어야 한다고 말했지?"). + # 한때 이 칸을 빼고 "시작 시각 = 나서는 시각" 으로 뒀다 — 출발지가 화면에서 사라졌다. + # 빈 칸으로 두지 않으려면 **실제로 하는 일**이 있어야 한다. 이 집은 부엌과 창고형 카페가 + # 딸린 독채라 아침을 여기서 보낸다 — 그게 이 숙소를 고른 이유이기도 하다. + "아침": ((40, 60, 110), "부엌에서 아침을 차려 먹고 마당에서 커피를 내려 마신 뒤 나섭니다."), + # ★ 마지막 날 아침은 **퇴실까지 한 칸**이다 (2026-09-11 사장님: "체크인하고 바로 체크아웃하는 + # 일정도 있어"). 아침(머뭄)과 체크아웃(머뭄)을 따로 세웠더니 **같은 자리에 두 칸이 연달아** + # 섰다 — 손님은 그 사이에 아무 데도 가지 않는다. 한 칸이 맞다. + # 마지막 날 아침은 체류 범위가 다르다 — 짐을 싸고 퇴실까지 한 칸에서 끝낸다. + "아침(퇴실)": ((45, 75, 115), + "아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요."), + "머뭄에서 쉬는 날": ((120, 210, 360), + "오늘은 나가지 않습니다. 마당 평상과 창고형 카페 공간에서 책을 읽고 낮잠을 잡니다."), + # ★ 10분짜리 칸을 시간표에 세우지 않는다 (2026-09-10 사장님: "10분 정도 왜 잡아 놓는 거야?"). + # 짐만 찾는 데는 10분이면 되지만, 그건 한 칸이 될 만한 일이 아니다 — 손님이 시간표에서 + # 읽는 것은 "여기서 무엇을 하다 가는가" 다. 마당에 한 번 더 앉았다 나서는 시간으로 잡는다. + "마무리": ((30, 40, 60), "맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다."), +} + +# ── 이동시간 — 좌표에서 계산한다 ──────────────────────────────────────────────── +WALK_KMH, DRIVE_KMH, PARK_MIN, WALK_LIMIT_KM = 4.0, 25.0, 5, 1.5 + + +def haversine_km(a: tuple, b: tuple) -> float: + r, rad = 6371.0, math.pi / 180 + h = (math.sin((b[0] - a[0]) * rad / 2) ** 2 + + math.cos(a[0] * rad) * math.cos(b[0] * rad) * math.sin((b[1] - a[1]) * rad / 2) ** 2) + return 2 * r * math.asin(math.sqrt(h)) + + +def move_minutes(a: tuple, b: tuple) -> int: + dist = haversine_km(a, b) + if dist < 0.05: + return 3 # 같은 자리(부잔교↔내항 야경)라도 옮겨 서는 시간은 있다 + raw = dist / WALK_KMH * 60 if dist <= WALK_LIMIT_KM else dist / DRIVE_KMH * 60 + PARK_MIN + return max(3, int(round(raw))) + + +# ── 시간대가 정해진 약속 ─────────────────────────────────────────────────────── +# 도착이 이 창 안에 들어와야 한다. 입·퇴실은 payload 의 check_in_time·check_out_time 과 같다. +WINDOW = { + "체크인": (15 * 60, 20 * 60), + "점심": (11 * 60 + 30, 13 * 60 + 30), + "저녁": (17 * 60 + 30, 20 * 60), + "늦은 점심": (13 * 60, 15 * 60), + "밤 산책": (18 * 60 + 30, 20 * 60), +} +DAY_ENDS_BY = 21 * 60 +# ★ 끼니 사이 최소 간격 (2026-09-11 검수). +# 섬 코스에서 늦은 점심이 15:47 에 끝나고 저녁이 17:30 이었다 — 103분이다. +# 창(窓)만 지키면 통과한다: 늦은 점심은 15:00 까지, 저녁은 17:30 부터라 둘 다 합법인데 +# 손님은 배가 안 꺼진 채 두 번째 상을 받는다. 창 밖에 이 규칙이 따로 필요하다. +MEAL_GAP = 180 +MEAL_SLOTS = {"점심", "늦은 점심", "저녁"} +# 퇴실 시각. payload 의 check_out_time 과 같아야 한다 — 시간표가 규정을 어기면 +# 사장님이 손님에게 설명할 수 없는 일정이 된다. +CHECK_OUT_BY = 11 * 60 + + +# ── 하루 뼈대 ───────────────────────────────────────────────────────────────── +# 자리 이름만 정한다. 장소는 테마가 주고, 시간은 계산된다. +# ★ 펜션 칸은 **실제로 무슨 일이 일어나는 자리**만 남긴다 — 입실·퇴실·돌아옴. +# 한때 '아침 · 머뭄 20분' 을 세워 뒀는데, 이미 그 집에 있는 사람에게 아무 뜻이 없는 칸이었다 +# (2026-09-10 사장님: "이건 무슨 의미야???"). 지금은 **하루의 시작 시각 = 머뭄에서 나서는 시각**이다. +# ★ 규칙 셋을 뼈대가 지킨다(아래 audit 이 매 실행 검사해서 어기면 빌드를 멈춘다). +# ① 하루의 처음과 끝은 머뭄이다 — 출발지와 복귀지. +# ② **같은 자리가 연달아 서지 않는다.** 머뭄 칸 사이에는 반드시 바깥 정거장이 있다. +# 한때 '아침 · 머뭄' 다음 칸이 '체크아웃 · 머뭄' 이었다 — 그 사이에 손님은 아무 데도 +# 가지 않는데 시간표에는 두 줄이 섰다(2026-09-11 사장님 지적). 지금은 한 칸이다. +# ③ 가운데 머뭄 칸은 '쉼'·'쉬는 날' 뿐이다. 점심(≤13:30)과 저녁(≥17:30) 사이 네 시간을 +# 바깥 두 곳으로는 못 채운다 — 억지로 채우면 골목 하나에 두 시간 세우게 된다(실측). +# 독채 펜션에 묵는 손님은 실제로 그때 들어와 쉰다. +SKELETON = { + "first": ["체크인@", "오후", "오후", "저녁", "밤 산책", "머뭄으로@"], + "first_late": ["체크인@", "저녁", "머뭄으로@"], + "mid": ["아침@", "오전", "점심", "오후", "오후", "쉼@", "저녁", "머뭄으로@"], + "mid_island": ["아침@", "오전", "오후", "늦은 점심", "쉼@", "저녁", "머뭄으로@"], + "mid_late": ["아침@", "점심", "오후", "오후", "쉼@", "저녁", "머뭄으로@"], + # ★ 숙소에서 노는 날. 오후에 머뭄으로 돌아와 쉬는 것이 이 테마의 내용이다 + # (2026-09-10 사장님: "펜션에서 조용히 노는 날도 있어야지"). + "mid_rest": ["머뭄에서 쉬는 날@", "점심", "쉼@", "저녁", "머뭄으로@"], + "last": ["아침@", "오전", "점심", "오후", "마무리@"], + "last_plus": ["아침@", "오전", "점심", "오후", "오후", "마무리@"], + # ★ 떠나는 날에 섬을 가는 코스. 왕복 두 시간이라 **바깥 정거장은 둘**이다 + # (2026-09-11 감사: 섬을 오후에 넣었더니 저녁도 없이 19시에 돌아와 짐만 찾고 끝났다). + # 퇴실하고 바로 섬으로 가서, 돌아오는 길에 늦은 점심을 먹고 짐을 찾는다. + "last_island": ["아침@", "오전", "늦은 점심", "마무리@"], +} +# 이 뼈대의 첫 칸(아침 · 퇴실)은 **11시 전에 끝나야** 한다 — payload 의 check_out_time. +CHECKOUT_KINDS = {"last", "last_plus", "last_island"} +# 하루의 시작 = **머뭄에서 아침을 여는 시각**이다(첫날만 입실 시각). +START = {"first": "15:00", "first_late": "19:00", "mid": "08:30", "mid_island": "07:40", + "mid_late": "09:40", "mid_rest": "10:00", "last": "08:10", "last_plus": "07:50", + "last_island": "08:00"} +# 시작을 앞당길 수 있는 한계. 첫날은 입실(15:00)이 있어 당길 이유가 없다. +FLOOR = {"first": "15:00", "first_late": "18:00", "mid": "07:20", "mid_island": "07:00", + "mid_late": "09:00", "mid_rest": "09:30", "last": "07:20", "last_plus": "07:20", + "last_island": "07:20"} +DAY_LABEL = ["첫째 날", "둘째 날", "셋째 날"] +DURATION = {2: "1박 2일", 3: "2박 3일"} + + +def _minutes(text: str) -> int: + hour, minute = text.split(":") + return int(hour) * 60 + int(minute) + + +def _action(slot: str, kind: str) -> tuple: + """머뭄 칸의 (체류범위, 한 줄). 마지막 날 아침만 퇴실을 겸한다.""" + if slot == "아침" and kind in CHECKOUT_KINDS: + return STAY_ACTION["아침(퇴실)"] + return STAY_ACTION[slot] + + +def _stay_spec(slot: str, place: str, kind: str) -> tuple: + if place == STAY: + return _action(slot, kind)[0] + return PLACES[place][3] + + +def _note(slot: str, place: str, kind: str) -> str | None: + if place == STAY: + return _action(slot, kind)[1] + return PLACES[place][4] + + +def _window_of(slot: str, last_meal_end: int | None): + """그 자리의 도착 창. 끼니는 **앞 끼니가 끝난 뒤 MEAL_GAP** 이 지나야 연다.""" + window = WINDOW.get(slot) + if not window: + return None + early, late = window + if slot in MEAL_SLOTS and last_meal_end is not None: + early = max(early, last_meal_end + MEAL_GAP) + return (early, late) if early <= late else None + + +def plan_day(kind: str, places: list[str], label: str, start: int, pace: float): + """하루 하나를 시각까지 계산한다. 창을 못 맞추면 None. + + ★ 창을 맞추는 방법은 **앞 칸에 더 머무는 것**이다. 이동시간을 늘리면 거짓말이 되고, + 빈 시간을 끼우면 시간표에 구멍이 난다. 사람은 실제로 그렇게 한다 — 밥 때가 이르면 + 박물관을 조금 더 본다. 범위(min~max)를 벗어나면 그 조합은 포기한다. + """ + slots = SKELETON[kind] + need = [s for s in slots if not s.endswith("@")] + if len(places) != len(need): + raise ValueError(f"{label}/{kind}: 자리 {len(need)}개인데 장소 {len(places)}개") + + feed = iter(places) + rows = [] + for slot in slots: + name = slot[:-1] if slot.endswith("@") else slot + place = STAY if slot.endswith("@") else next(feed) + low, base, high = _stay_spec(name, place, kind) + rows.append({"slot": name, "place": place, "kind": kind, + "stay": max(low, min(high, int(round(base * pace)))), + "low": low, "high": high}) + + # ★ 마지막 날 첫 칸(아침 겸 퇴실)은 11시 전에 **끝나야** 한다. + # 체크아웃을 따로 세우던 때는 이 칸이 머뭄에 두 번 서게 만들었다(2026-09-11 감사). + if kind in CHECKOUT_KINDS: + if start + rows[0]["stay"] > CHECK_OUT_BY: + rows[0]["stay"] = max(rows[0]["low"], CHECK_OUT_BY - start) + if start + rows[0]["stay"] > CHECK_OUT_BY or rows[0]["stay"] < rows[0]["low"]: + return None + + clock = start + prev = BASE + last_meal_end = None + for index, row in enumerate(rows): + coords = (PLACES[row["place"]][0], PLACES[row["place"]][1]) + # 하루의 1번은 머뭄이다 — 이미 그 자리에 있으니 이동시간이 없다. + row["move"] = 0 if index == 0 else move_minutes(prev, coords) + arrive = clock + row["move"] + + window = _window_of(row["slot"], last_meal_end) + if window is None and row["slot"] in WINDOW: + return None # 앞 끼니와 겹쳐 창이 닫혔다 — 이 조합은 포기한다 + if window: + early, late = window + if arrive < early: + # 앞 칸들에 더 머문다. 뒤에서부터 늘려야 방금 지나온 자리가 길어진다. + short = early - arrive + for back in reversed(rows[:index]): + room = back["high"] - back["stay"] + take = min(room, short) + back["stay"] += take + short -= take + if short == 0: + break + if short > 0: + return None # 늘려도 못 채운다 — 시작을 늦춰야 하는 조합 + return plan_day_fixed(rows, start) + if arrive > late: + short = arrive - late + for back in reversed(rows[:index]): + room = back["stay"] - back["low"] + take = min(room, short) + back["stay"] -= take + short -= take + if short == 0: + break + if short > 0: + return None # 줄여도 못 당긴다 — 시작을 앞당겨야 하는 조합 + return plan_day_fixed(rows, start) + + clock = arrive + row["stay"] + if row["slot"] in MEAL_SLOTS: + last_meal_end = clock + prev = coords + + if clock > DAY_ENDS_BY: + return None + return _render(rows, start, label) + + +def plan_day_fixed(rows: list[dict], start: int): + """체류시간을 고친 뒤 처음부터 다시 훑는다(앞 칸을 바꿨으니 뒤 시각이 전부 달라진다).""" + clock = start + prev = BASE + last_meal_end = None + for index, row in enumerate(rows): + coords = (PLACES[row["place"]][0], PLACES[row["place"]][1]) + row["move"] = 0 if index == 0 else move_minutes(prev, coords) + arrive = clock + row["move"] + window = _window_of(row["slot"], last_meal_end) + if row["slot"] in WINDOW and (window is None or not (window[0] <= arrive <= window[1])): + return None + clock = arrive + row["stay"] + if row["slot"] in MEAL_SLOTS: + last_meal_end = clock + prev = coords + if clock > DAY_ENDS_BY: + return None + return rows, start + + +def _render(rows: list[dict], start: int, label: str) -> dict: + stops = [] + for row in rows: + place = row["place"] + stop = {"name": f"{row['slot']} · {place}", "minutes": row["stay"], + "moveMinutes": row["move"], + "latitude": PLACES[place][0], "longitude": PLACES[place][1]} + note = _note(row["slot"], place, row.get("kind", "")) + if note: + stop["note"] = note + query = PLACES[place][2] + if query: + stop["searchQuery"] = query + stops.append(stop) + return {"label": label, "startTime": f"{start // 60:02d}:{start % 60:02d}", "stops": stops} + + +def build_day(kind: str, places: list[str], label: str, start: str | None, pace: float) -> dict: + """시작 시각을 옮겨 가며 모든 창을 맞추는 하루를 찾는다.""" + begin = _minutes(start or START[kind]) + floor = _minutes(FLOOR[kind]) + candidates = [begin] + [begin + delta for delta in range(10, 190, 10)] \ + + [begin - delta for delta in range(10, 190, 10) if begin - delta >= floor] + for scale in (pace, pace * 0.92, pace * 0.85, 1.0, 0.8): + for clock in candidates: + if clock < floor or clock > 20 * 60: + continue + result = plan_day(kind, places, label, clock, scale) + if result is None: + continue + if isinstance(result, tuple): # 창을 맞추느라 체류를 고친 경우 + rows, begin_at = result + return _render(rows, begin_at, label) + return result + raise ValueError(f"{label}/{kind}: 창(입·퇴실·끼니·21시)을 다 맞추는 시각이 없다") + + +# ── 테마 20개 ──────────────────────────────────────────────────────────────── +# (이름, 기간, 누구에게, 왜, [(뼈대, [장소…])]) +THEMES = [ + ("섬까지 다녀오는 1박 2일", "1박 2일", "차를 가져온 손님", + "첫날은 걸어서 도심, 이튿날은 다리를 건너 섬입니다. 두 날의 성격이 아주 다릅니다.", + [("first", ["히로쓰 가옥", "초원사진관", "빈해원", "군산 내항"]), + ("last_island", ["고군산군도 선유도", "국제반점"])]), + ("걸어서만 도는 1박 2일", "1박 2일", "차 없이 오는 손님", + "모든 정거장이 머뭄에서 걸어 닿습니다. 이틀 동안 차를 한 번도 타지 않습니다.", + [("first", ["히로쓰 가옥", "초원사진관", "일흥옥", "군산 내항"]), + ("last", ["군산근대건축관", "한일옥", "우체통거리"])]), + ("비가 와도 되는 1박 2일", "1박 2일", "비 예보를 보고 오는 손님", + "실내가 이어집니다. 비가 그치기를 기다리지 않아도 하루가 찹니다.", + [("first", ["군산근대역사박물관", "군산근대미술관", "빈해원", "군산 내항"]), + ("last", ["군산근대건축관", "일흥옥", "째보선창 근처 카페"])]), + ("근대건축만 보는 1박 2일", "1박 2일", "건물을 보러 오는 손님", + "1900년대 건물만 골라 돕니다. 머뭄도 그 시기에 지은 집입니다.", + [("first", ["구 군산세관 본관", "군산근대건축관", "명월갈비", "군산 내항"]), + ("last", ["군산근대미술관", "일흥옥", "해망굴"])]), + ("이성당으로 시작하는 1박 2일", "1박 2일", "빵을 좋아하는 손님", + "이튿날 아침을 빵으로 엽니다. 문 여는 시간에 맞춰 걸어갑니다.", + [("first", ["초원사진관", "우체통거리", "도란", "군산 내항"]), + ("last", ["이성당", "만남스넥", "군산근대역사박물관"])]), + ("철길마을까지 가는 1박 2일", "1박 2일", "사진을 찍는 손님", + "빛이 좋은 자리만 이어 붙였습니다. 철길마을은 오전 빛이 낫습니다.", + [("first", ["초원사진관", "동국사", "국제반점", "군산 내항"]), + ("last", ["경암동 철길마을", "일흥옥", "군산 시간여행마을"])]), + ("항구만 도는 1박 2일", "1박 2일", "바다가 보고 싶은 손님", + "부잔교에서 째보선창까지, 이틀 동안 물가만 걷습니다.", + [("first", ["군산 내항 부잔교", "해망굴", "빈해원", "군산 내항"]), + ("last", ["째보선창", "국제반점", "구 군산세관 본관"])]), + ("소설 『탁류』를 따라 걷는 1박 2일", "1박 2일", "채만식을 읽어 본 손님", + "소설이 지나간 자리를 순서대로 밟습니다. 세관·선창·문학관이 한 줄로 이어집니다.", + [("first", ["째보선창", "군산 내항 부잔교", "한일옥", "군산 내항"]), + ("last", ["채만식문학관", "일흥옥", "구 군산세관 본관"])]), + ("아이와 함께 1박 2일", "1박 2일", "아이를 데려온 손님", + "걷는 거리가 짧고, 아이가 지루해할 자리를 뺐습니다.", + [("first", ["초원사진관", "우체통거리", "만남스넥", "군산 내항"]), + ("last", ["경암동 철길마을", "이성당", "월명호수"])]), + ("늦게 도착하는 1박 2일", "1박 2일", "퇴근하고 오는 손님", + "첫날은 저녁 한 끼로 끝냅니다. 대신 이튿날을 아침부터 저녁까지 씁니다.", + [("first_late", ["명월갈비"]), + ("last_plus", ["군산근대역사박물관", "한일옥", "군산근대건축관", "해망굴"])]), + + ("천천히 도는 2박 3일", "2박 3일", "쉬러 오는 손님", + "하루에 두어 곳만 봅니다. 마당에 앉아 있는 시간을 일정에 넣었습니다.", + [("first", ["히로쓰 가옥", "동국사", "도란", "군산 내항"]), + ("mid_rest", ["한일옥", "빈해원"]), + ("last", ["이성당", "만남스넥", "군산 시간여행마을"])]), + ("섬과 도심을 반씩 도는 2박 3일", "2박 3일", "차를 가져온 손님", + "가운데 날을 섬에 다 씁니다. 첫날과 마지막 날은 걸어서 도심입니다.", + [("first", ["구 군산세관 본관", "군산근대건축관", "빈해원", "군산 내항"]), + ("mid_island", ["고군산군도 선유도", "장자도", "국제반점", "명월갈비"]), + ("last", ["초원사진관", "일흥옥", "이성당"])]), + ("골목을 세 번 나눠 걷는 2박 3일", "2박 3일", "차 없이 오는 손님", + "원도심을 세 덩이로 나눠 하루에 하나씩 걷습니다. 같은 길을 두 번 걷지 않습니다.", + [("first", ["히로쓰 가옥", "초원사진관", "일흥옥", "군산 내항"]), + ("mid", ["군산근대역사박물관", "한일옥", "군산근대건축관", "해망굴", "명월갈비"]), + ("last", ["우체통거리", "만남스넥", "군산 시간여행마을"])]), + ("먹으러 오는 2박 3일", "2박 3일", "맛집을 도는 손님", + "끼니가 중심입니다. 사이에 걸을 자리를 넣어 배를 비웁니다.", + [("first", ["물밀소", "초원사진관", "명월갈비", "군산 내항"]), + ("mid", ["군산 시간여행마을", "일흥옥", "이성당", "우체통거리", "빈해원"]), + ("last", ["동국사", "국제반점", "군산근대역사박물관"])]), + ("아무 데도 안 가는 2박 3일", "2박 3일", "쉬려고만 오는 손님", + "가운데 날은 대문 밖으로 두 번만 나갑니다. 나머지는 마당과 카페 공간에서 보냅니다.", + [("first", ["히로쓰 가옥", "초원사진관", "도란", "군산 내항"]), + ("mid_rest", ["만남스넥", "일흥옥"]), + ("last", ["동국사", "물밀소", "월명호수"])]), + ("사진만 찍는 2박 3일", "2박 3일", "사진을 찍는 손님", + "시간대에 맞춰 자리를 배치했습니다. 철길마을은 오전, 부잔교는 해 질 때입니다.", + [("first", ["초원사진관", "동국사", "도란", "군산 내항"]), + ("mid", ["경암동 철길마을", "만남스넥", "째보선창", "해망굴", "국제반점"]), + ("last", ["우체통거리", "일흥옥", "군산근대미술관"])]), + ("비가 와도 되는 2박 3일", "2박 3일", "비 예보를 보고 오는 손님", + "사흘 내리 비가 와도 도는 순서가 바뀌지 않습니다. 실내와 처마 밑만 잇습니다.", + [("first", ["군산근대역사박물관", "군산근대미술관", "빈해원", "군산 내항"]), + ("mid", ["군산근대건축관", "일흥옥", "째보선창 근처 카페", "동국사다온", "명월갈비"]), + ("last", ["채만식문학관", "한일옥", "이성당"])]), + ("늦게 시작하는 2박 3일", "2박 3일", "아침이 힘든 손님", + "가운데 날을 열한 시에 엽니다. 오전을 비우고 저녁을 늦게까지 씁니다.", + [("first", ["히로쓰 가옥", "우체통거리", "도란", "군산 내항"]), + ("mid_late", ["일흥옥", "군산근대역사박물관", "해망굴", "빈해원"]), + ("last", ["이성당", "만남스넥", "초원사진관"])]), + ("섬에서 하루를 다 쓰는 2박 3일", "2박 3일", "차를 가져온 손님", + "가운데 날은 아침에 나가 저녁에 들어옵니다. 섬에서 하루를 통째로 씁니다.", + [("first", ["군산 내항 부잔교", "해망굴", "빈해원", "군산 내항"]), + ("mid_island", ["고군산군도 선유도", "장자도", "한일옥", "명월갈비"]), + ("last", ["군산근대건축관", "만남스넥", "경암동 철길마을"])]), + ("소설 『탁류』를 따라 걷는 2박 3일", "2박 3일", "채만식을 읽어 본 손님", + "소설의 순서대로 사흘을 나눴습니다. 선창에서 시작해 문학관에서 끝냅니다.", + [("first", ["째보선창", "군산 내항 부잔교", "일흥옥", "군산 내항"]), + ("mid_rest", ["한일옥", "빈해원"]), + ("last", ["군산근대역사박물관", "국제반점", "초원사진관"])]), + ("철길마을과 항구를 같이 보는 2박 3일", "2박 3일", "사진을 찍는 손님", + "기찻길과 물길을 하루씩 나눠 봅니다. 마지막 날은 건물만 봅니다.", + [("first", ["경암동 철길마을", "초원사진관", "국제반점", "군산 내항"]), + ("mid", ["군산 내항 부잔교", "한일옥", "째보선창", "해망굴", "명월갈비"]), + ("last", ["구 군산세관 본관", "일흥옥", "군산근대미술관"])]), +] + +# 테마 이름 → (날짜별 시작 시각, 체류 배율). 시각과 배율이 그 테마의 성격이다 — +# 쉬러 오는 손님(1.25)과 사진만 찍는 손님(0.8)은 같은 곳을 봐도 하루 길이가 다르다. +# 창(끼니·입퇴실)에 걸리면 build_day 가 시작을 옮겨 맞춘다. +PACING = { + "섬까지 다녀오는 1박 2일": (["15:00", "08:20"], 1.0), + "걸어서만 도는 1박 2일": (["15:00", "09:20"], 1.05), + "비가 와도 되는 1박 2일": (["15:20", "09:40"], 1.15), + "근대건축만 보는 1박 2일": (["15:00", "08:50"], 0.95), + "이성당으로 시작하는 1박 2일": (["16:10", "07:30"], 0.9), + "철길마을까지 가는 1박 2일": (["15:40", "08:30"], 0.95), + "항구만 도는 1박 2일": (["16:30", "09:20"], 1.05), + "소설 『탁류』를 따라 걷는 1박 2일": (["15:10", "09:00"], 1.1), + "아이와 함께 1박 2일": (["15:30", "10:10"], 0.85), + "늦게 도착하는 1박 2일": (["19:00", "08:40"], 1.0), + "천천히 도는 2박 3일": (["16:20", "10:40", "09:50"], 1.25), + "섬과 도심을 반씩 도는 2박 3일": (["15:10", "07:40", "09:20"], 1.0), + "골목을 세 번 나눠 걷는 2박 3일": (["15:00", "09:10", "08:20"], 1.1), + "먹으러 오는 2박 3일": (["16:40", "10:20", "08:40"], 1.15), + "사진만 찍는 2박 3일": (["15:50", "07:20", "07:40"], 0.8), + "비가 와도 되는 2박 3일": (["15:20", "10:10", "09:40"], 1.2), + "아무 데도 안 가는 2박 3일": (["15:00", "10:00", "09:00"], 1.3), + "늦게 시작하는 2박 3일": (["17:20", "10:30", "10:40"], 1.0), + "섬에서 하루를 다 쓰는 2박 3일": (["15:00", "07:30", "09:40"], 0.95), + "소설 『탁류』를 따라 걷는 2박 3일": (["15:00", "09:50", "08:30"], 1.1), + "철길마을과 항구를 같이 보는 2박 3일": (["16:00", "07:50", "09:30"], 0.9), +} + +items = [] +for name, duration, audience, why, days in THEMES: + starts, pace = PACING.get(name, (None, 1.0)) + built = [build_day(kind, places, DAY_LABEL[i], + (starts[i] if starts and i < len(starts) else None), pace) + for i, (kind, places) in enumerate(days)] + items.append({"name": name, "duration": duration, "audience": audience, "why": why, + "days": built}) + +env = {"kind": "itinerary", "version": 1, "title": "추천 일정", + "subtitle": "머뭄에서 나서고 머뭄으로 돌아옵니다", "items": items} +SECT["itinerary"]["data"] = json.dumps(env, ensure_ascii=False) + + +# ── 검증: 규칙을 어기면 **빌드를 멈춘다** ──────────────────────────────────── +# +# ★ 왜 스크립트 안인가 (2026-09-11) +# 지금까지 이 검사를 매번 따로 쓴 일회용 스크립트로 돌렸다. 그래서 뼈대를 고칠 때마다 +# "이번에 안 본 규칙" 이 생겼고, 사장님이 화면에서 먼저 찾았다 — +# '아침 · 머뭄' 다음 칸이 '체크아웃 · 머뭄' 인 일정이 그렇게 나갔다. +# 검사가 빌드의 일부가 아니면 검사한 적 없는 것과 같다. 이제 어기면 payload 를 안 만든다. +FOOD_SLOTS = {"점심", "저녁", "늦은 점심"} +FOOD = {"빈해원", "일흥옥", "한일옥", "명월갈비", "국제반점", "만남스넥", "도란", "물밀소", + "이성당", "째보선창 근처 카페", "동국사다온"} +INDOOR = {"군산근대역사박물관", "군산근대건축관", "군산근대미술관", "채만식문학관", STAY} | FOOD +NEEDS_CAR = {"고군산군도 선유도", "장자도", "채만식문학관", "경암동 철길마을", "월명호수", "은적사"} +REST_SLOTS = {"쉼", "머뭄에서 쉬는 날"} +NIGHT_ONLY = {"군산 내항"} +BAR = {"도란"} +DAYS_OF = {"1박 2일": 2, "2박 3일": 3} + + +def audit(built: list[dict]) -> list[str]: + bad: list[str] = [] + + def flag(item, day, text): + bad.append(f"{item['name']} / {day['label'] if day else '-'} — {text}") + + for item in built: + want = DAYS_OF.get(item["duration"]) + if want and len(item["days"]) != want: + flag(item, None, f"{item['duration']}인데 {len(item['days'])}일") + seen_all: list[str] = [] + + for day in item["days"]: + slots = [s["name"].split(" · ")[0] for s in day["stops"]] + places = [s["name"].split(" · ")[-1] for s in day["stops"]] + seen_all += [p for p in places if p != STAY] + + # ① 하루의 처음과 끝은 머뭄 + if places[0] != STAY: + flag(item, day, f"1번이 머뭄이 아니다 ({places[0]})") + if places[-1] != STAY: + flag(item, day, f"마지막이 머뭄이 아니다 ({places[-1]})") + + # ② 같은 자리가 연달아 서지 않는다 + for i in range(len(places) - 1): + if places[i] == places[i + 1]: + flag(item, day, f"같은 자리 연속: {slots[i]} → {slots[i + 1]} ({places[i]})") + + # ③ 가운데 머뭄 칸은 쉼·쉬는 날만 + for i in range(1, len(places) - 1): + if places[i] == STAY and slots[i] not in REST_SLOTS: + flag(item, day, f"가운데 머뭄 칸이 쉼이 아니다 ({slots[i]})") + + clock = _minutes(day["startTime"]) + prev = BASE + for i, stop in enumerate(day["stops"]): + # ④ 이동시간은 좌표 계산값과 같아야 한다 + real = 0 if i == 0 else move_minutes(prev, (stop["latitude"], stop["longitude"])) + if stop["moveMinutes"] != real: + flag(item, day, f"이동시간 {stop['moveMinutes']}분 (실거리 {real}분) — {places[i]}") + arrive = clock + stop["moveMinutes"] + + # ⑤ 시간대가 정해진 자리 + window = WINDOW.get(slots[i]) + if window and not (window[0] <= arrive <= window[1]): + flag(item, day, f"{slots[i]} 도착 {arrive // 60}:{arrive % 60:02d} — 창 밖") + + # ⑥ 마지막 날 첫 칸(아침 겸 퇴실)은 11시 전에 끝난다 + if i == 0 and slots[0] == "아침" and len(item["days"]) == want and day is item["days"][-1]: + if arrive + stop["minutes"] > CHECK_OUT_BY: + end = arrive + stop["minutes"] + flag(item, day, f"퇴실이 {end // 60}:{end % 60:02d} — 11시를 넘긴다") + + # ⑧ 끼니 자리에는 먹는 곳만 + if slots[i] in FOOD_SLOTS and places[i] not in FOOD: + flag(item, day, f"{slots[i]} 자리에 식당이 아닌 {places[i]}") + if slots[i] == "밤 산책" and places[i] not in NIGHT_ONLY: + flag(item, day, f"밤 산책 자리에 {places[i]}") + + clock = arrive + stop["minutes"] + prev = (stop["latitude"], stop["longitude"]) + + # ⑦ 21시를 넘기지 않는다(넘기면 렌더러가 그 칸을 조용히 버린다) + if clock > DAY_ENDS_BY: + flag(item, day, f"{clock // 60}:{clock % 60:02d} 종료 — 21시 초과") + + # ⑮ 끼니 사이 최소 간격 (2026-09-11 검수에서 걸린 것) + # 창만 지키면 통과한다 — 늦은 점심 15:47 종료, 저녁 17:30 시작이 둘 다 합법이었다. + # 손님은 배가 안 꺼진 채 두 번째 상을 받는다. + walk = _minutes(day["startTime"]) + last_meal = None + for sl, stop in zip(slots, day["stops"]): + walk += stop["moveMinutes"] + if sl in FOOD_SLOTS: + if last_meal is not None and walk - last_meal < MEAL_GAP: + flag(item, day, f"앞 끼니 끝나고 {walk - last_meal}분 만에 {sl}") + last_meal = walk + stop["minutes"] + walk += stop["minutes"] + + # ⑯ 볼거리는 **가는 시간보다 오래 머문다** + # 끼니는 뺀다 — 밥집은 목적지가 아니라 돌아오는 길에 들르는 자리다. + # (섬에서 85분 달려와 55분 먹는 것은 이상하지 않다. 섬에 30분 있으면 이상하다.) + for sl, pl, stop in zip(slots, places, day["stops"]): + if pl == STAY or sl in FOOD_SLOTS: + continue + if stop["minutes"] < stop["moveMinutes"]: + flag(item, day, f"{pl}: 이동 {stop['moveMinutes']}분인데 체류 {stop['minutes']}분") + + # ⑨ 같은 날 같은 식당 두 번 + meals = [p for sl, p in zip(slots, places) if sl in FOOD_SLOTS] + if len(meals) != len(set(meals)): + flag(item, day, f"같은 날 같은 식당 반복 {meals}") + + # ⑪⑫⑭ 테마가 약속한 성격 + if "비가 와도" in item["name"]: + for sl, pl in zip(slots, places): + if sl in ("오전", "오후") and pl not in INDOOR: + flag(item, day, f"비 테마인데 실외 {pl}") + if "걸어서만" in item["name"] or "차 없이" in item["audience"]: + for pl in places: + if pl in NEEDS_CAR: + flag(item, day, f"걷는 테마인데 차가 필요한 {pl}") + if "아이와" in item["name"]: + for pl in places: + if pl in BAR: + flag(item, day, f"아이 동반인데 {pl}") + + # ⑩ 한 테마 안에서 같은 곳을 두 번 가지 않는다 + repeat = sorted({p for p in seen_all if seen_all.count(p) > 1}) + if repeat: + flag(item, None, f"테마 안에서 반복 {repeat}") + + return bad + + +problems = audit(items) +for line in problems: + print(" ✗", line) +if problems: + raise SystemExit(f"\n규칙 위반 {len(problems)}건 — payload 를 쓰지 않았다.") + +for item in items: + for day in item["days"]: + clock = _minutes(day["startTime"]) + for stop in day["stops"]: + clock += stop["moveMinutes"] + stop["minutes"] + print(f"{item['name'][:24]:26} {day['label']} {day['startTime']}–" + f"{clock // 60:02d}:{clock % 60:02d} {len(day['stops'])}칸") + +DST.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8") +print(f"\n테마 {len(items)}개 · 규칙 16종 위반 0건 · payload {DST.stat().st_size:,}B") diff --git a/solution/site/scripts/mockup/build_story.py b/solution/site/scripts/mockup/build_story.py new file mode 100644 index 0000000..b3d3cc4 --- /dev/null +++ b/solution/site/scripts/mockup/build_story.py @@ -0,0 +1,229 @@ +"""가요 다방(군산 노래)과 인물 열전을 다시 채운다. + +★ 지어내지 않는다. + · 노래: '음악의 오솔길' 의 군산·전북 지역노래 정리에서 온 목록이다(곡명·가수·작사·작곡·연도). + 링크는 **유튜브 검색 주소**다 — 영상 ID 를 박으면 틀린 영상이 붙을 수 있고, 우리가 그걸 + 한 곡씩 확인하지 않았다. 검색은 틀릴 수가 없다. + · 인물: 한국어 위키백과 '군산시 출신' 분류에서 뽑아 문서 첫 문단으로 한 줄을 적었다. + 사진은 넣지 않는다(초상권 · 2026-09-10 결정). 출처 링크는 그 사람의 위키 문서다. + +★ 50명을 억지로 채우지 않는다 — 분류에 실제로 있는 사람만 담는다. + 노래도 마찬가지다. 확인된 곡이 25곡이면 25곡이다. +""" +import json +import urllib.parse +from pathlib import Path + +SP = Path(__file__).parent +SRC = SP / "stay-payload-new.json" + +payload = json.loads(SRC.read_text(encoding="utf-8")) +SECT = {s["id"]: s for s in payload["theme"]["sections"]} + +SONG_SOURCE = {"name": "음악의 오솔길 — 군산 관련 노래", + "url": "https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98"} + +# (곡명, 가수, 작사, 작곡, 연도, 라벨색, 한 줄) +# ★ 연도·작사·작곡을 모르면 비운다. "미상" 이라고 적지 않는다 — 렌더러가 없는 칸을 안 그린다. +SONGS = [ + ("잘 있거라 군산항", "안다성", "손석우", "손석우", 1963, "#2f6b4f", + "월명산 고갯길로 시작하는 이별가. 군산을 노래한 곡 가운데 가장 널리 알려졌습니다."), + ("헤어진 군산항", "박제연", "송운선", "송운선", 1965, "#2a6053", + "비 내리는 밤 부두에서 헤어지는 노래입니다."), + ("님 떠난 군산항", "이미자", "조형식", "백영호", 1966, "#1d3c74", + "떠나는 배를 보내는 항구의 정서를 담았습니다."), + ("금강 나그네", "이미자", "이병훈", "박춘석", 1966, "#d4551f", + "금강 하구와 군산항 부두를 배경으로 한 곡입니다."), + ("잘 있거라 군산항구", "최정자", "최치수", "유금춘", 1967, "#8a4b2a", + "떠나는 사람의 말로 부르는 이별가입니다."), + ("군산 마도로스", "이상열", None, None, 1967, "#334b6d", + "항구를 드나들던 뱃사람의 노래입니다."), + ("군산 부르스", "유주용", "반야월", "강도미", 1969, "#4a3f6b", + "1960년대 말 군산 거리의 분위기를 담은 곡입니다."), + ("이별의 군산항 부두", "최정자", "최치수", "유금춘", 1969, "#6b2f3a", + "부두에서의 이별을 다시 한 번 부른 곡입니다."), + ("군산항 부르스", "최하우", "최하우", "김성근", 1984, "#2f5b6b", + "1980년대에 나온 군산항 노래입니다."), + ("나의 사랑 군산이여", "오단비", "이수", "정종택", 1989, "#a34a2a", + "군산을 향한 애정을 정면으로 부르는 곡입니다."), + ("군산항 엘레지", "오단비", "이수", "정종택", None, "#7a3b52", + "항구 도시의 비가입니다."), + ("선유도 아가씨", "오단비", "이수", "서승일", None, "#2b6b7a", + "고군산군도 선유도를 배경으로 한 곡입니다."), + ("군산찬가", "오단비", "이수", "서승일", None, "#c8873a", + "은파와 새만금이 가사에 나오는 군산 찬가입니다."), + ("오성산", "오단비", "이수", "서승일", None, "#4f6b3a", + "금강 하구를 내려다보는 오성산을 노래했습니다."), + ("함께하는 군산", "오은주", "김동찬", "김동찬", None, "#b5603a", + "군산시가 제작한 음반의 첫 곡입니다."), + ("새만금 연가", "채미영", "김동찬", "김동찬 · 김수환", None, "#356b8a", + "새만금을 소재로 한 곡입니다."), + ("군산시민의 노래", "오단비", "군산문화원", "군산예총", None, "#5a5a4a", + "금만경 들녘과 서해 바다로 시작하는 시민의 노래입니다."), + ("군산 아줌마", "박솔미", "송두호", "송두호", None, "#a3562a", + "선창가 거리와 새만금이 함께 나오는 곡입니다."), + ("내 고향 군산항", "정애란", "정애란", "정애란", None, "#6b4a2a", + "고향으로서의 군산항을 부른 곡입니다."), + ("군산 가는 길", "황상준", None, None, None, "#3a5a4a", + "군산으로 향하는 길을 제목으로 삼은 곡입니다."), + ("군산 추억", "임인건", None, None, None, "#4a4a6b", + "재즈 피아니스트 임인건이 군산을 소재로 만든 곡입니다."), + ("군산에서", "임인건", None, None, None, "#3f5f6b", + "같은 연주자가 군산에서 받은 인상을 담았습니다."), + ("헤어진 군산항", "김창환", "송운선", "송운선", None, "#2a6053", + "군산시 제작 음반에 다시 실린 판입니다."), + ("금강 아가씨", "이미자", None, None, None, "#d4551f", + "금강을 배경으로 한 이미자의 또 다른 곡입니다."), + ("금강의 비가", "전승애", None, None, None, "#7a4a6b", + "금강을 제목에 올린 곡입니다."), +] + + +def youtube(title: str, artist: str | None) -> str: + query = f"{title} {artist or ''}".strip() + return "https://www.youtube.com/results?search_query=" + urllib.parse.quote(query) + + +songs = [] +for title, artist, lyricist, composer, year, color, story in SONGS: + item = {"title": title, "artist": artist, "labelColor": color, "story": story, + "verified": "확인" if year else "확인필요", + "listenUrl": youtube(title, artist), "source": SONG_SOURCE} + if lyricist: + item["lyricist"] = lyricist + if composer: + item["composer"] = composer + if year: + item["year"] = year + songs.append(item) + +SECT["songs"]["data"] = json.dumps( + {"kind": "songs", "version": 1, "title": "가요 다방", + "subtitle": f"군산을 노래한 {len(songs)}곡 — 눌러서 들어 보세요", "items": songs}, + ensure_ascii=False) + + +# ── 인물 열전 ──────────────────────────────────────────────────────────────── +# (위키 문서 제목, 표시 이름, 연도, 하는 일, 한 줄) +# ★ 한 줄은 위키 첫 문단을 줄인 것이다. 군산과의 관계가 문서에 적힌 사람만 담는다. +PEOPLE = [ + ("채만식", "채만식", "1902–1950", "소설가", "군산을 무대로 한 『탁류』를 썼습니다. 시내에 문학관이 있습니다."), + ("고은", "고은", "1933–", "시인", "군산에서 태어나 60년 넘게 시를 써 온 시인입니다."), + ("이병훈 (시인)", "이병훈", "1925–2009", "시인", "옥구(지금의 군산) 출신으로 1950년대에 등단한 현대 시인입니다."), + ("심호택", "심호택", "1947–2010", "시인", "민중의 삶을 시로 옮긴 시인입니다."), + ("문효치", "문효치", "1943–", "시인", "군산에서 태어나 1966년 신춘문예로 등단했습니다."), + ("이연주 (시인)", "이연주", "1953–1992", "시인", "짧게 활동하고 떠난 시인입니다."), + ("주하림", "주하림", None, "시인", "군산에서 태어나 2009년 창비신인시인상으로 등단했습니다."), + ("고형곤", "고형곤", "1906–2004", "철학자", "선(禪) 연구로 알려진 철학자이자 정치가입니다."), + ("김선기 (언어철학자)", "김선기", "1907–1992", "언어학자", "호는 무돌. 한글 연구에 힘쓴 언어학자이자 교육자입니다."), + ("반석평", "반석평", "1472–1540", "조선 문신", "노비 출신으로 문과에 급제해 판서에 오른 조선 중종 때 문신입니다."), + + ("김수미", "김수미", "1949–2024", "배우", "「전원일기」 일용 엄니로 오래 기억되는 배우입니다."), + ("송새벽", "송새벽", "1979–", "배우", "군산 출신 배우입니다."), + ("장신영", "장신영", "1984–", "배우", "군산 출신 배우입니다."), + ("이서원", "이서원", "1997–", "배우", "군산 출신 배우입니다."), + ("장재호 (배우)", "장재호", "1986–", "배우", "군산 출신 배우입니다."), + ("강민지 (배우)", "강민지", "1996–", "배우 · 모델", "모델로 시작해 드라마와 영화로 활동 범위를 넓혔습니다."), + ("병헌", "병헌", "1993–", "배우", "그룹 틴탑 활동을 거쳐 배우로 옮겼습니다."), + ("정혁 (모델)", "정혁", "1991–", "모델", "2015년 서울패션위크로 데뷔한 모델 겸 방송인입니다."), + ("김혜선 (희극인)", "김혜선", "1983–", "희극인", "군산 출신 희극 배우입니다."), + ("이옥주 (희극인)", "이옥주", "1969–", "희극인", "군산중앙여고를 나와 1988년 MBC 개그콘테스트 대상을 받았습니다."), + ("박화요비", "박화요비", "1982–", "가수", "R&B 를 주로 부르는 가수입니다."), + ("박윤경 (가수)", "박윤경", "1970–", "가수", "군산 출신 가수 겸 기업인입니다."), + ("하준 (가수)", "하준", "1965–", "가수", "군산 출신 가수입니다."), + + ("고건", "고건", "1938–", "정치인", "제30·35대 국무총리를 지냈습니다. 아버지는 철학자 고형곤입니다."), + ("강봉균 (정치인)", "강봉균", "1943–2017", "정치인", "재정경제부 장관과 3선 국회의원을 지냈습니다."), + ("양일동", "양일동", "1912–1980", "정치인", "옥구군 서수면 출신으로 야당 정치인으로 활동했습니다."), + ("김관영", "김관영", "1969–", "정치인", "전북특별자치도지사입니다. 회계사·변호사를 거쳤습니다."), + ("채영석", "채영석", "1934–2006", "정치인", "군산에서 3선 국회의원을 지냈습니다."), + ("신영대", "신영대", "1968–", "정치인", "군산 지역구 국회의원입니다."), + ("김의겸", "김의겸", "1963–", "언론인 · 정치인", "한겨레 기자를 거쳐 청와대 대변인과 국회의원을 지냈습니다."), + ("함운경", "함운경", "1964–", "정치인", "1980년대 학생운동가 출신입니다."), + ("임병찬", "임병찬", "1851–1916", "의병장", "최익현과 함께 거병한 구한말 의병장으로, 단식 끝에 순국했습니다."), + ("이수현 (1895년)", "이수현", "1895–1980", "독립운동가", "호는 산남. 군산 출신 독립운동가입니다."), + ("전진 (1923년)", "전진", "1923–2012", "비전향 장기수", "군산 출신으로 오래 수감 생활을 했습니다."), + + ("이길여", "이길여", "1932–", "의료인 · 교육자", "가천대학교 길병원을 세우고 가천대 총장을 맡고 있습니다."), + ("최길선", "최길선", "1946–", "기업인", "현대중공업 회장을 지낸 조선업 경영인입니다."), + ("은성수 (공무원)", "은성수", "1961–", "공무원", "제7대 금융위원회 위원장을 지냈습니다."), + ("채금석", "채금석", "1904–1995", "축구 선수", "일제강점기 경성 축구단 선수로 경평축구대항전에 나섰습니다."), + + ("박경완", "박경완", "1972–", "야구 선수", "SK 와이번스 포수로 한국 야구의 대표적인 포수였습니다."), + ("조계현", "조계현", "1964–", "야구 선수", "해태 타이거즈 투수로 뛰었습니다."), + ("정명원", "정명원", "1966–", "야구 선수", "현대 유니콘스 투수로 뛰었습니다."), + ("조규제", "조규제", "1967–", "야구 선수", "왼손 투수로 뛰고 지금은 투수코치입니다."), + ("차우찬", "차우찬", "1987–", "야구 선수", "투수로 뛰었고 지금은 야구 해설위원입니다."), + ("이진영 (1980년)", "이진영", "1980–", "야구 선수", "외야수로 뛰었고 지금은 코치입니다."), + ("오지환", "오지환", "1990–", "야구 선수", "LG 트윈스 내야수입니다."), + ("이천웅", "이천웅", "1988–", "야구 선수", "LG 트윈스 외야수로 뛰었습니다."), + ("노상래", "노상래", "1970–", "축구 선수", "공격수로 뛰었고 지도자로 일하고 있습니다."), + ("노수진", "노수진", "1962–", "축구 선수", "공격수로 뛰었고 지금은 체육 교사입니다."), + ("유동우", "유동우", "1968–", "축구 선수", "수비수로 뛰었고 대학 축구부 감독을 맡고 있습니다."), + ("이정효", "이정효", "1975–", "축구 감독", "풀백으로 뛰었고 지금은 프로팀 감독입니다."), + ("박성현 (양궁 선수)", "박성현", "1983–", "양궁 선수", "군산 소룡초에서 활을 처음 잡아 올림픽 금메달을 땄습니다."), + ("김광선 (권투 선수)", "김광선", "1964–", "권투 선수", "1988년 서울 올림픽 복싱 플라이급 금메달리스트입니다."), + ("정경미 (유도 선수)", "정경미", "1985–", "유도 선수", "군산 출신 유도 선수입니다."), + ("전미라", "전미라", "1978–", "테니스 선수", "테니스 선수로 뛰었고 지금은 해설위원입니다."), + ("최서우", "최서우", "1982–", "스키점프 선수", "1998년부터 국가대표로 뛴 스키점프 선수입니다."), + ("한왕용", "한왕용", "1966–", "산악인", "히말라야 8000m 14좌를 세계 11번째로 완등했습니다."), + ("문호준", "문호준", "1997–", "프로게이머", "카트라이더 리그 최다 우승자입니다."), +] + +# ★ 사진은 **자유 라이선스인 것만** 싣는다. +# 위키백과 문서에 사진이 있는 사람은 57명 중 18명이고, 그중 라이선스가 확인되는 17장만 받았다 +# (CC BY / CC BY-SA / KOGL / 공용). 라이선스가 안 읽히는 한 장은 뺐다 — 초상권과 저작권이 +# 미결인 이미지는 싣지 않는다는 이 레포의 규약(DECISIONS 1-2)과 같은 자리다. +# CC BY-SA 는 저작자 표시가 조건이라 imageCredit 으로 화면에 남긴다(렌더러가 출처 줄에 붙인다). +PHOTOS = {p["name"]: p for p in json.loads((SP / "people-photos.json").read_text(encoding="utf-8"))} + +# ★ 위키에 사진이 없는 사람은 **원본 payload 의 사진을 그대로 쓴다** +# (2026-09-11 사장님: "원래 나오던 것도 안 나옴"). 인물 목록을 새로 만들면서 +# 원본에 있던 8장을 같이 날렸다 — 채만식처럼 위키에 사진이 없는 사람이 빈칸이 됐다. +# 이 사진들은 원래 이 사이트가 쓰던 것이라 자리로 되돌리는 것이지 새로 가져오는 게 아니다. +_ORIGINAL = json.loads((SP / "stay-payload.json").read_text(encoding="utf-8")) +ORIGINAL_PHOTOS = {} +for _sec in _ORIGINAL["theme"]["sections"]: + if _sec["id"] != "people": + continue + for _item in json.loads(_sec["data"])["items"]: + if _item.get("imageUrl"): + ORIGINAL_PHOTOS[_item["name"]] = _item["imageUrl"] + +# ★ 이름만으로 커먼즈를 검색해 사진을 붙이지 않는다 (2026-09-11 실측). +# 동명이인이 잡힌다 — 이수현(독립운동가)에 걸그룹 멤버, 은성수(금융위원장)에 축구선수, +# 박성현(양궁)에 골퍼, 이길여에 건물 사진이 왔다. **틀린 사진은 없는 것만 못하다.** +# 그 사람의 위키 문서가 대표 사진으로 쓰는 것(pageimages)만 신뢰한다. + +people = [] +for title, name, years, role, one_line in PEOPLE: + item = {"name": name, "role": role, "oneLine": one_line, "verified": "확인", + "source": {"name": "한국어 위키백과", + "url": "https://ko.wikipedia.org/wiki/" + urllib.parse.quote(title.replace(" ", "_"))}} + if years: + item["years"] = years + photo = PHOTOS.get(name) + if photo: + item["imageUrl"] = photo["url"] + # ★ 렌더러의 imageCredit 은 **문자열**이다(common.tsx:206). 객체를 넣으면 조용히 안 나온다 — + # CC BY-SA 는 저작자 표시가 조건이라 안 나오면 라이선스를 어기는 것이다. + item["imageCredit"] = f"사진 {photo['author']} · {photo['license']}" + elif name in ORIGINAL_PHOTOS: + item["imageUrl"] = ORIGINAL_PHOTOS[name] + people.append(item) + +# ★ 사진 있는 사람을 앞으로 당긴다 (2026-09-11 사장님 지시). +# 목록은 가로 레일이라 손님이 보는 것은 앞쪽 몇 장이다. 분야별로만 묶어 두면 +# 사진이 흩어져(1·2번 있고 3~8번 없고 9번에 또 있는 식) 레일 앞머리가 글자만 남는다. +# **분야 순서는 그대로 두고** 사진 유무로만 두 덩이로 가른다 — 안정 정렬이라 +# 같은 덩이 안에서는 문인 → 배우·가수 → 정치 → 기업 → 체육 순서가 유지된다. +people.sort(key=lambda item: 0 if item.get("imageUrl") else 1) + +SECT["people"]["data"] = json.dumps( + {"kind": "people", "version": 1, "title": "인물 열전", + "subtitle": f"군산이 낳은 사람 {len(people)}명", "items": people}, + ensure_ascii=False) + +SRC.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8") +print(f"가요 다방 {len(songs)}곡 · 인물 열전 {len(people)}명 (사진 {sum(1 for p in people if p.get(chr(34)+chr(34)) or p.get('imageUrl'))}장)") +print(" 자작곡 섞임:", any("머뭄" in s["title"] for s in songs)) diff --git a/solution/site/scripts/mockup/coords.json b/solution/site/scripts/mockup/coords.json new file mode 100644 index 0000000..22f6999 --- /dev/null +++ b/solution/site/scripts/mockup/coords.json @@ -0,0 +1,80 @@ +{ + "군산 신흥동 일본식가옥": { + "name": "신흥동일본식가옥", + "lat": 35.9861696060316, + "lng": 126.706057532726, + "addr": "전북특별자치도 군산시 구영1길 17" + }, + "군산 동국사": { + "name": "동국사", + "lat": 35.98276630107591, + "lng": 126.70742960285295, + "addr": "전북특별자치도 군산시 동국사길 16" + }, + "군산 우체통거리": { + "name": "우체통거리", + "lat": 35.9859964914028, + "lng": 126.711970794777, + "addr": "전북특별자치도 군산시 신창동" + }, + "군산 월명호수": { + "name": "월명호수", + "lat": 35.97663913860391, + "lng": 126.69136962041439, + "addr": "전북특별자치도 군산시 소룡동 1071-4" + }, + "군산 시간여행마을": { + "name": "군산시간여행마을", + "lat": 35.988319185729, + "lng": 126.710937704825, + "addr": "전북특별자치도 군산시 영화동" + }, + "군산 은적사": { + "name": "은적사", + "lat": 35.9748303718065, + "lng": 126.685472639398, + "addr": "전북특별자치도 군산시 설림3길 49" + }, + "도란 군산 월명동": { + "name": "도란", + "lat": 35.9868220429033, + "lng": 126.707509996005, + "addr": "전북특별자치도 군산시 구영신창길 61" + }, + "물밀소 군산": { + "name": "물밀소", + "lat": 35.98452434115725, + "lng": 126.70732109672761, + "addr": "전북특별자치도 군산시 월명로 491" + }, + "일흥옥 군산": { + "name": "일흥옥", + "lat": 35.98514982005064, + "lng": 126.70918392566266, + "addr": "전북특별자치도 군산시 구영7길 25" + }, + "명월갈비 군산": { + "name": "명월갈비", + "lat": 35.98639139165697, + "lng": 126.71016628964314, + "addr": "전북특별자치도 군산시 구영7길 59" + }, + "만남스넥 군산": { + "name": "만남스넥", + "lat": 35.984178824622944, + "lng": 126.70977629761406, + "addr": "전북특별자치도 군산시 대학로 67-3" + }, + "동국사다온": { + "name": "다온 군산점", + "lat": 35.98609071445503, + "lng": 126.71443996128968, + "addr": "전북특별자치도 군산시 영동로 27-3" + }, + "국제반점 군산": { + "name": "국제반점", + "lat": 35.9881100055579, + "lng": 126.710898546645, + "addr": "전북특별자치도 군산시 구영6길 100" + } +} \ No newline at end of file diff --git a/solution/site/scripts/mockup/dump_text.py b/solution/site/scripts/mockup/dump_text.py new file mode 100644 index 0000000..d4222d8 --- /dev/null +++ b/solution/site/scripts/mockup/dump_text.py @@ -0,0 +1,105 @@ +"""시연본에 들어간 **문구 전부**를 읽기 좋은 문서로 뽑는다. + +★ 왜 따로 뽑나 — 문구는 파이썬 리스트 안에 흩어져 있다(캐치프레이즈는 patch_stay.py, + 노래·인물은 build_story.py, 정거장 할 일은 build_itinerary.py). + 사장님이 **읽고 고칠 것**은 문구뿐이라, 코드를 열지 않고 볼 수 있게 한 장으로 모은다. + 고치는 자리는 여전히 스크립트다 — 이 파일은 산출물이고 손으로 고치지 않는다. +""" +import json +from pathlib import Path + +SP = Path(__file__).parent + +# ★ 최종 산출물(build/index.html)에서 읽는다. 캐치프레이즈·자작곡·객실 사진은 +# patch_stay.py 가 HTML 을 조립하면서 넣으므로 중간 파일(stay-payload-new.json)에는 없다. +# "실제로 나간 값" 을 적는 문서라 최종본이 맞다. +import re +_html = (SP / "build" / "index.html").read_text(encoding="utf-8") +payload = json.loads(re.search(r"", _html, re.S).group(1)) +SECT = {s["id"]: s for s in payload["theme"]["sections"]} +env = lambda sid: json.loads(SECT[sid]["data"]) + +out = ["# /s/stay 시연본 — 들어간 문구 전부", + "", + "`python3 dump_text.py` 산출물. **고치는 자리는 스크립트다** — 이 파일을 고쳐도 반영되지 않는다.", + ""] + +# ── 캐치프레이즈 ────────────────────────────────────────────────────────────── +box = payload["narrative"]["catchphrases"]["items"] +out += [f"## 1. 히어로 문구 {len(box)}개 — `patch_stay.py`", "", + "상호 아래 한 줄. 7초마다 `일반 → 계절 → 일반 → 월 → 일반 → 날씨` 순으로 바뀐다.", ""] +general = [x for x in box if x["kind"] == "general"] +out += [f"### 일반 {len(general)}개 (`patch_stay.py` `GENERAL`)", ""] +out += [f"{i}. {x['text']}" for i, x in enumerate(general, 1)] + [""] + +out += [f"### 계절별 {sum(1 for x in box if x['kind'] == 'season')}개 (`SEASON`)", ""] +for season in ("봄", "여름", "가을", "겨울"): + rows = [x["text"] for x in box if x.get("season") == season] + out += [f"**{season}**"] + [f"- {t}" for t in rows] + [""] + +out += [f"### 월별 {sum(1 for x in box if x['kind'] == 'month')}개 (`MONTH`)", ""] +for month in range(1, 13): + rows = [x["text"] for x in box if x.get("month") == month] + out += [f"**{month}월** — " + " · ".join(rows)] +out += [""] + +out += [f"### 날씨별 {sum(1 for x in box if x['kind'] == 'weather')}개 (`WEATHER`)", ""] +for weather in ("맑음", "구름많음", "흐림", "비", "눈"): + rows = [x["text"] for x in box if x.get("weather") == weather] + out += [f"**{weather}**"] + [f"- {t}" for t in rows] + [""] + +# ── 가요 다방 ──────────────────────────────────────────────────────────────── +songs = env("songs")["items"] +out += [f"## 2. 가요 다방 {len(songs)}곡 — `build_story.py` `SONGS`", "", + "| # | 곡명 | 가수 | 작사 / 작곡 | 연도 | 한 줄 |", "|---|---|---|---|---|---|"] +for i, s in enumerate(songs, 1): + credit = " / ".join(filter(None, [s.get("lyricist"), s.get("composer")])) or "—" + out.append(f"| {i} | {s['title']} | {s.get('artist', '—')} | {credit} | {s.get('year', '—')} | {s.get('story', '')} |") +out += ["", f"출처: {songs[0]['source']['name']}", ""] + +# ── 인물 열전 ──────────────────────────────────────────────────────────────── +people = env("people")["items"] +shot = sum(1 for p in people if p.get("imageUrl")) +out += [f"## 3. 인물 열전 {len(people)}명 (사진 {shot}장) — `build_story.py` `PEOPLE`", "", + "사진 있는 사람이 앞에 온다.", "", + "| # | 사진 | 이름 | 연도 | 하는 일 | 한 줄 |", "|---|---|---|---|---|---|"] +for i, p in enumerate(people, 1): + out.append(f"| {i} | {'📷' if p.get('imageUrl') else ''} | {p['name']} | {p.get('years', '—')} | {p['role']} | {p['oneLine']} |") +out += [""] + +# ── 일정 정거장 ────────────────────────────────────────────────────────────── +itin = env("itinerary")["items"] +seen = {} +for item in itin: + for day in item["days"]: + for stop in day["stops"]: + place = stop["name"].split(" · ")[-1] + if stop.get("note"): + seen.setdefault(f"{place}|{stop['note']}", (place, stop["note"], stop["minutes"])) +out += [f"## 4. 일정 — 정거장에서 할 일 {len(seen)}줄 — `build_itinerary.py` `PLACES`·`STAY_ACTION`", "", + "| 장소 | 무엇을 하는지 | 머무는 시간 |", "|---|---|---|"] +for place, note, minutes in sorted(seen.values()): + out.append(f"| {place} | {note} | {minutes}분 |") +out += [""] + +# ── 일정 테마 ──────────────────────────────────────────────────────────────── +out += [f"## 5. 일정 테마 {len(itin)}개 — `build_itinerary.py` `THEMES`", "", + "| 테마 | 기간 | 누구에게 | 왜 |", "|---|---|---|---|"] +for item in itin: + out.append(f"| {item['name']} | {item['duration']} | {item['audience']} | {item['why']} |") +out += [""] + +# ── 오늘의 엽서 ────────────────────────────────────────────────────────────── +cards = env("postcard")["items"] +out += [f"## 6. 오늘의 엽서 {len(cards)}장 — 원본 payload (아직 다시 안 뽑음)", ""] +out += [f"- {c['line']} `{' '.join(c.get('hashtags', []))}`" for c in cards] + [""] + +# ── 머뭄이 만든 노래 ───────────────────────────────────────────────────────── +own = payload["narrative"]["ownSongs"]["items"] +out += [f"## 7. 머뭄이 만든 노래 {len(own)}곡 — `patch_stay.py` `OWN_SONGS`", ""] +out += [f"- {s['title']} — `{s['audioUrl']}`" for s in own] + [""] + +path = SP / "TEXT.md" +path.write_text("\n".join(out), encoding="utf-8") +print(f"{path.name} 캐치프레이즈 {len(box)} · 노래 {len(songs)} · 인물 {len(people)} · " + f"정거장 문구 {len(seen)} · 테마 {len(itin)} · 엽서 {len(cards)} · 자작곡 {len(own)}") diff --git a/solution/site/scripts/mockup/img/a-01.jpg b/solution/site/scripts/mockup/img/a-01.jpg new file mode 100644 index 0000000..d0a1081 Binary files /dev/null and b/solution/site/scripts/mockup/img/a-01.jpg differ diff --git a/solution/site/scripts/mockup/img/a-02.jpg b/solution/site/scripts/mockup/img/a-02.jpg new file mode 100644 index 0000000..e3e081d Binary files /dev/null and b/solution/site/scripts/mockup/img/a-02.jpg differ diff --git a/solution/site/scripts/mockup/img/a-03.jpg b/solution/site/scripts/mockup/img/a-03.jpg new file mode 100644 index 0000000..70be459 Binary files /dev/null and b/solution/site/scripts/mockup/img/a-03.jpg differ diff --git a/solution/site/scripts/mockup/img/a-04.jpg b/solution/site/scripts/mockup/img/a-04.jpg new file mode 100644 index 0000000..10a748d Binary files /dev/null and b/solution/site/scripts/mockup/img/a-04.jpg differ diff --git a/solution/site/scripts/mockup/img/a-05.jpg b/solution/site/scripts/mockup/img/a-05.jpg new file mode 100644 index 0000000..61714db Binary files /dev/null and b/solution/site/scripts/mockup/img/a-05.jpg differ diff --git a/solution/site/scripts/mockup/img/a-06.jpg b/solution/site/scripts/mockup/img/a-06.jpg new file mode 100644 index 0000000..cb5731b Binary files /dev/null and b/solution/site/scripts/mockup/img/a-06.jpg differ diff --git a/solution/site/scripts/mockup/img/a-07.jpg b/solution/site/scripts/mockup/img/a-07.jpg new file mode 100644 index 0000000..cfa75d7 Binary files /dev/null and b/solution/site/scripts/mockup/img/a-07.jpg differ diff --git a/solution/site/scripts/mockup/img/a-08.jpg b/solution/site/scripts/mockup/img/a-08.jpg new file mode 100644 index 0000000..65d7f84 Binary files /dev/null and b/solution/site/scripts/mockup/img/a-08.jpg differ diff --git a/solution/site/scripts/mockup/img/a-09.jpg b/solution/site/scripts/mockup/img/a-09.jpg new file mode 100644 index 0000000..561eebb Binary files /dev/null and b/solution/site/scripts/mockup/img/a-09.jpg differ diff --git a/solution/site/scripts/mockup/img/a-10.jpg b/solution/site/scripts/mockup/img/a-10.jpg new file mode 100644 index 0000000..1e51e20 Binary files /dev/null and b/solution/site/scripts/mockup/img/a-10.jpg differ diff --git a/solution/site/scripts/mockup/img/a-11.jpg b/solution/site/scripts/mockup/img/a-11.jpg new file mode 100644 index 0000000..45e4e07 Binary files /dev/null and b/solution/site/scripts/mockup/img/a-11.jpg differ diff --git a/solution/site/scripts/mockup/img/a-12.jpg b/solution/site/scripts/mockup/img/a-12.jpg new file mode 100644 index 0000000..4ed16d6 Binary files /dev/null and b/solution/site/scripts/mockup/img/a-12.jpg differ diff --git a/solution/site/scripts/mockup/img/b-01.jpg b/solution/site/scripts/mockup/img/b-01.jpg new file mode 100644 index 0000000..a3b99c7 Binary files /dev/null and b/solution/site/scripts/mockup/img/b-01.jpg differ diff --git a/solution/site/scripts/mockup/img/b-02.jpg b/solution/site/scripts/mockup/img/b-02.jpg new file mode 100644 index 0000000..88efbc5 Binary files /dev/null and b/solution/site/scripts/mockup/img/b-02.jpg differ diff --git a/solution/site/scripts/mockup/img/b-03.jpg b/solution/site/scripts/mockup/img/b-03.jpg new file mode 100644 index 0000000..66b05b8 Binary files /dev/null and b/solution/site/scripts/mockup/img/b-03.jpg differ diff --git a/solution/site/scripts/mockup/img/b-04.jpg b/solution/site/scripts/mockup/img/b-04.jpg new file mode 100644 index 0000000..3075bb1 Binary files /dev/null and b/solution/site/scripts/mockup/img/b-04.jpg differ diff --git a/solution/site/scripts/mockup/img/b-05.jpg b/solution/site/scripts/mockup/img/b-05.jpg new file mode 100644 index 0000000..ed24cb2 Binary files /dev/null and b/solution/site/scripts/mockup/img/b-05.jpg differ diff --git a/solution/site/scripts/mockup/img/b-06.jpg b/solution/site/scripts/mockup/img/b-06.jpg new file mode 100644 index 0000000..17a76e6 Binary files /dev/null and b/solution/site/scripts/mockup/img/b-06.jpg differ diff --git a/solution/site/scripts/mockup/img/b-07.jpg b/solution/site/scripts/mockup/img/b-07.jpg new file mode 100644 index 0000000..75ecd6c Binary files /dev/null and b/solution/site/scripts/mockup/img/b-07.jpg differ diff --git a/solution/site/scripts/mockup/img/b-08.jpg b/solution/site/scripts/mockup/img/b-08.jpg new file mode 100644 index 0000000..e99ea86 Binary files /dev/null and b/solution/site/scripts/mockup/img/b-08.jpg differ diff --git a/solution/site/scripts/mockup/img/b-09.jpg b/solution/site/scripts/mockup/img/b-09.jpg new file mode 100644 index 0000000..b2e7bd2 Binary files /dev/null and b/solution/site/scripts/mockup/img/b-09.jpg differ diff --git a/solution/site/scripts/mockup/img/b-10.jpg b/solution/site/scripts/mockup/img/b-10.jpg new file mode 100644 index 0000000..0e62f76 Binary files /dev/null and b/solution/site/scripts/mockup/img/b-10.jpg differ diff --git a/solution/site/scripts/mockup/img/h-01.jpg b/solution/site/scripts/mockup/img/h-01.jpg new file mode 100644 index 0000000..87b12f5 Binary files /dev/null and b/solution/site/scripts/mockup/img/h-01.jpg differ diff --git a/solution/site/scripts/mockup/img/h-02.jpg b/solution/site/scripts/mockup/img/h-02.jpg new file mode 100644 index 0000000..6e7d8b1 Binary files /dev/null and b/solution/site/scripts/mockup/img/h-02.jpg differ diff --git a/solution/site/scripts/mockup/img/h-03.jpg b/solution/site/scripts/mockup/img/h-03.jpg new file mode 100644 index 0000000..66404a3 Binary files /dev/null and b/solution/site/scripts/mockup/img/h-03.jpg differ diff --git a/solution/site/scripts/mockup/img/h-04.jpg b/solution/site/scripts/mockup/img/h-04.jpg new file mode 100644 index 0000000..6a85bb9 Binary files /dev/null and b/solution/site/scripts/mockup/img/h-04.jpg differ diff --git a/solution/site/scripts/mockup/img/h-05.jpg b/solution/site/scripts/mockup/img/h-05.jpg new file mode 100644 index 0000000..537dcb0 Binary files /dev/null and b/solution/site/scripts/mockup/img/h-05.jpg differ diff --git a/solution/site/scripts/mockup/img/h-06.jpg b/solution/site/scripts/mockup/img/h-06.jpg new file mode 100644 index 0000000..dee9844 Binary files /dev/null and b/solution/site/scripts/mockup/img/h-06.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/07600bb67b91dd15.jpg b/solution/site/scripts/mockup/img/mirror/07600bb67b91dd15.jpg new file mode 100644 index 0000000..35f03ba Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/07600bb67b91dd15.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/0a1e0ff50ca7ee37.jpg b/solution/site/scripts/mockup/img/mirror/0a1e0ff50ca7ee37.jpg new file mode 100644 index 0000000..cc4d462 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/0a1e0ff50ca7ee37.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/12938240987c1f78.jpg b/solution/site/scripts/mockup/img/mirror/12938240987c1f78.jpg new file mode 100644 index 0000000..3cafdad Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/12938240987c1f78.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/149c18951bdcaff8.jpg b/solution/site/scripts/mockup/img/mirror/149c18951bdcaff8.jpg new file mode 100644 index 0000000..721b860 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/149c18951bdcaff8.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/1caf7fcac8ece19a.jpg b/solution/site/scripts/mockup/img/mirror/1caf7fcac8ece19a.jpg new file mode 100644 index 0000000..915fd93 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/1caf7fcac8ece19a.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/1f35ef26139c6cb9.jpg b/solution/site/scripts/mockup/img/mirror/1f35ef26139c6cb9.jpg new file mode 100644 index 0000000..6116ed9 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/1f35ef26139c6cb9.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/1f6dbdd78a3da2de.png b/solution/site/scripts/mockup/img/mirror/1f6dbdd78a3da2de.png new file mode 100644 index 0000000..84f1993 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/1f6dbdd78a3da2de.png differ diff --git a/solution/site/scripts/mockup/img/mirror/22af0bff45e5fce8.jpg b/solution/site/scripts/mockup/img/mirror/22af0bff45e5fce8.jpg new file mode 100644 index 0000000..f4cf4c8 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/22af0bff45e5fce8.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/2af9caa23a08a3ec.jpg b/solution/site/scripts/mockup/img/mirror/2af9caa23a08a3ec.jpg new file mode 100644 index 0000000..3ad803c Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/2af9caa23a08a3ec.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/2d94158a48480fcd.jpg b/solution/site/scripts/mockup/img/mirror/2d94158a48480fcd.jpg new file mode 100644 index 0000000..1b8115b Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/2d94158a48480fcd.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/3420ff7e357ab7ef.jpg b/solution/site/scripts/mockup/img/mirror/3420ff7e357ab7ef.jpg new file mode 100644 index 0000000..086dabb Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/3420ff7e357ab7ef.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/37c90494d28478e2.png b/solution/site/scripts/mockup/img/mirror/37c90494d28478e2.png new file mode 100644 index 0000000..f6c1f1d Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/37c90494d28478e2.png differ diff --git a/solution/site/scripts/mockup/img/mirror/3a780a4a34df9289.jpg b/solution/site/scripts/mockup/img/mirror/3a780a4a34df9289.jpg new file mode 100644 index 0000000..6bef7f4 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/3a780a4a34df9289.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/3dc384187a900e95.png b/solution/site/scripts/mockup/img/mirror/3dc384187a900e95.png new file mode 100644 index 0000000..0c454c6 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/3dc384187a900e95.png differ diff --git a/solution/site/scripts/mockup/img/mirror/3e9e574d3b987c8d.jpg b/solution/site/scripts/mockup/img/mirror/3e9e574d3b987c8d.jpg new file mode 100644 index 0000000..5573d65 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/3e9e574d3b987c8d.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/43461e391f83b9e8.jpg b/solution/site/scripts/mockup/img/mirror/43461e391f83b9e8.jpg new file mode 100644 index 0000000..9fdde7e Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/43461e391f83b9e8.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/4389ded2dee1ad98.jpg b/solution/site/scripts/mockup/img/mirror/4389ded2dee1ad98.jpg new file mode 100644 index 0000000..9101a17 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/4389ded2dee1ad98.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/487643cdcfd19fdb.jpg b/solution/site/scripts/mockup/img/mirror/487643cdcfd19fdb.jpg new file mode 100644 index 0000000..e85a198 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/487643cdcfd19fdb.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/49558512d8d52e1a.jpg b/solution/site/scripts/mockup/img/mirror/49558512d8d52e1a.jpg new file mode 100644 index 0000000..27ddbdd Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/49558512d8d52e1a.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/4b4086345c20b968.jpg b/solution/site/scripts/mockup/img/mirror/4b4086345c20b968.jpg new file mode 100644 index 0000000..94ac5d8 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/4b4086345c20b968.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/4d71067dc7e3f671.jpg b/solution/site/scripts/mockup/img/mirror/4d71067dc7e3f671.jpg new file mode 100644 index 0000000..6b833be Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/4d71067dc7e3f671.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/4db345bd2cb4f0d7.jpg b/solution/site/scripts/mockup/img/mirror/4db345bd2cb4f0d7.jpg new file mode 100644 index 0000000..a8040be Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/4db345bd2cb4f0d7.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/5000f0a4feb45788.jpg b/solution/site/scripts/mockup/img/mirror/5000f0a4feb45788.jpg new file mode 100644 index 0000000..1b361b8 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/5000f0a4feb45788.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/566bc326e8a58042.jpg b/solution/site/scripts/mockup/img/mirror/566bc326e8a58042.jpg new file mode 100644 index 0000000..1b8f9d6 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/566bc326e8a58042.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/6194651114d1e740.jpg b/solution/site/scripts/mockup/img/mirror/6194651114d1e740.jpg new file mode 100644 index 0000000..524e90b Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/6194651114d1e740.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/669abc2b08640d3e.jpg b/solution/site/scripts/mockup/img/mirror/669abc2b08640d3e.jpg new file mode 100644 index 0000000..ee506a6 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/669abc2b08640d3e.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/6a0c445e6dd940af.jpg b/solution/site/scripts/mockup/img/mirror/6a0c445e6dd940af.jpg new file mode 100644 index 0000000..829a51a Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/6a0c445e6dd940af.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/6a15360430298700.jpg b/solution/site/scripts/mockup/img/mirror/6a15360430298700.jpg new file mode 100644 index 0000000..ef93272 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/6a15360430298700.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/6d0c447b6a0c32af.jpg b/solution/site/scripts/mockup/img/mirror/6d0c447b6a0c32af.jpg new file mode 100644 index 0000000..643da20 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/6d0c447b6a0c32af.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/6d59722cedddd566.jpg b/solution/site/scripts/mockup/img/mirror/6d59722cedddd566.jpg new file mode 100644 index 0000000..d51ace9 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/6d59722cedddd566.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/6ded14f21a17894b.jpg b/solution/site/scripts/mockup/img/mirror/6ded14f21a17894b.jpg new file mode 100644 index 0000000..914a7b6 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/6ded14f21a17894b.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/7095220ca156e25e.jpg b/solution/site/scripts/mockup/img/mirror/7095220ca156e25e.jpg new file mode 100644 index 0000000..2de983c Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/7095220ca156e25e.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/7ba545fdbc8ad273.jpg b/solution/site/scripts/mockup/img/mirror/7ba545fdbc8ad273.jpg new file mode 100644 index 0000000..f81a05b Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/7ba545fdbc8ad273.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/7ccd68de6b280bc2.jpg b/solution/site/scripts/mockup/img/mirror/7ccd68de6b280bc2.jpg new file mode 100644 index 0000000..0184e5b Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/7ccd68de6b280bc2.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/7f8f81d5eeb33f73.jpg b/solution/site/scripts/mockup/img/mirror/7f8f81d5eeb33f73.jpg new file mode 100644 index 0000000..5767329 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/7f8f81d5eeb33f73.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/8043bad52d95a84b.jpg b/solution/site/scripts/mockup/img/mirror/8043bad52d95a84b.jpg new file mode 100644 index 0000000..630fe38 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/8043bad52d95a84b.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/82606e203454e3ff.jpg b/solution/site/scripts/mockup/img/mirror/82606e203454e3ff.jpg new file mode 100644 index 0000000..b9e36ad Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/82606e203454e3ff.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/881d1f7ff820942f.jpg b/solution/site/scripts/mockup/img/mirror/881d1f7ff820942f.jpg new file mode 100644 index 0000000..c76f74c Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/881d1f7ff820942f.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/8e2ec99c433b6fe8.jpg b/solution/site/scripts/mockup/img/mirror/8e2ec99c433b6fe8.jpg new file mode 100644 index 0000000..d038a4f Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/8e2ec99c433b6fe8.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/8fc0a7522e3eb080.png b/solution/site/scripts/mockup/img/mirror/8fc0a7522e3eb080.png new file mode 100644 index 0000000..c1b4081 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/8fc0a7522e3eb080.png differ diff --git a/solution/site/scripts/mockup/img/mirror/92f65116f3e58ab6.jpg b/solution/site/scripts/mockup/img/mirror/92f65116f3e58ab6.jpg new file mode 100644 index 0000000..5ab3d3c Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/92f65116f3e58ab6.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/9b073d4a1b5fb62c.jpg b/solution/site/scripts/mockup/img/mirror/9b073d4a1b5fb62c.jpg new file mode 100644 index 0000000..47802a3 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/9b073d4a1b5fb62c.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/9b27266ddf8b8b70.jpg b/solution/site/scripts/mockup/img/mirror/9b27266ddf8b8b70.jpg new file mode 100644 index 0000000..dcf4d25 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/9b27266ddf8b8b70.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/9c5594df6114561b.jpg b/solution/site/scripts/mockup/img/mirror/9c5594df6114561b.jpg new file mode 100644 index 0000000..1d2a4ee Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/9c5594df6114561b.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/a1b00af88aa06cd9.jpg b/solution/site/scripts/mockup/img/mirror/a1b00af88aa06cd9.jpg new file mode 100644 index 0000000..708afa3 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/a1b00af88aa06cd9.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/a2344640498c5c86.png b/solution/site/scripts/mockup/img/mirror/a2344640498c5c86.png new file mode 100644 index 0000000..41de0d3 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/a2344640498c5c86.png differ diff --git a/solution/site/scripts/mockup/img/mirror/aa69a633f8e3a631.jpg b/solution/site/scripts/mockup/img/mirror/aa69a633f8e3a631.jpg new file mode 100644 index 0000000..9ea22bc Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/aa69a633f8e3a631.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/aaf32d2c8e5faffb.jpg b/solution/site/scripts/mockup/img/mirror/aaf32d2c8e5faffb.jpg new file mode 100644 index 0000000..5bb1c40 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/aaf32d2c8e5faffb.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/af673cc75c0ed696.jpg b/solution/site/scripts/mockup/img/mirror/af673cc75c0ed696.jpg new file mode 100644 index 0000000..42512a0 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/af673cc75c0ed696.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/c8c6a295ae1c0810.jpg b/solution/site/scripts/mockup/img/mirror/c8c6a295ae1c0810.jpg new file mode 100644 index 0000000..dd346c6 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/c8c6a295ae1c0810.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/c971fb408ad897e7.jpg b/solution/site/scripts/mockup/img/mirror/c971fb408ad897e7.jpg new file mode 100644 index 0000000..f1485b1 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/c971fb408ad897e7.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/caa5e076e7d56ec2.jpg b/solution/site/scripts/mockup/img/mirror/caa5e076e7d56ec2.jpg new file mode 100644 index 0000000..8dd36da Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/caa5e076e7d56ec2.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/d2be5a053a944cfe.jpg b/solution/site/scripts/mockup/img/mirror/d2be5a053a944cfe.jpg new file mode 100644 index 0000000..1944011 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/d2be5a053a944cfe.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/d364f605223524bd.jpg b/solution/site/scripts/mockup/img/mirror/d364f605223524bd.jpg new file mode 100644 index 0000000..e085f1c Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/d364f605223524bd.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/d5433fe48de3da05.jpg b/solution/site/scripts/mockup/img/mirror/d5433fe48de3da05.jpg new file mode 100644 index 0000000..4853fb5 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/d5433fe48de3da05.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/d543dcde9f0eac6c.jpg b/solution/site/scripts/mockup/img/mirror/d543dcde9f0eac6c.jpg new file mode 100644 index 0000000..2e83ef4 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/d543dcde9f0eac6c.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/d8764592a03b0103.jpg b/solution/site/scripts/mockup/img/mirror/d8764592a03b0103.jpg new file mode 100644 index 0000000..0116a3e Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/d8764592a03b0103.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/d902ef61234bd897.png b/solution/site/scripts/mockup/img/mirror/d902ef61234bd897.png new file mode 100644 index 0000000..0cbdc50 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/d902ef61234bd897.png differ diff --git a/solution/site/scripts/mockup/img/mirror/ddc9f804f4ef83b5.jpg b/solution/site/scripts/mockup/img/mirror/ddc9f804f4ef83b5.jpg new file mode 100644 index 0000000..dca75be Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/ddc9f804f4ef83b5.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/e0ed8d1a27e09320.jpg b/solution/site/scripts/mockup/img/mirror/e0ed8d1a27e09320.jpg new file mode 100644 index 0000000..98bce01 Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/e0ed8d1a27e09320.jpg differ diff --git a/solution/site/scripts/mockup/img/mirror/f27b7440e2f1183f.jpg b/solution/site/scripts/mockup/img/mirror/f27b7440e2f1183f.jpg new file mode 100644 index 0000000..aa1ee7f Binary files /dev/null and b/solution/site/scripts/mockup/img/mirror/f27b7440e2f1183f.jpg differ diff --git a/solution/site/scripts/mockup/img/people/고건.jpg b/solution/site/scripts/mockup/img/people/고건.jpg new file mode 100644 index 0000000..eb0a02c Binary files /dev/null and b/solution/site/scripts/mockup/img/people/고건.jpg differ diff --git a/solution/site/scripts/mockup/img/people/고은.jpg b/solution/site/scripts/mockup/img/people/고은.jpg new file mode 100644 index 0000000..dda95ff Binary files /dev/null and b/solution/site/scripts/mockup/img/people/고은.jpg differ diff --git a/solution/site/scripts/mockup/img/people/김관영.jpg b/solution/site/scripts/mockup/img/people/김관영.jpg new file mode 100644 index 0000000..6c212d8 Binary files /dev/null and b/solution/site/scripts/mockup/img/people/김관영.jpg differ diff --git a/solution/site/scripts/mockup/img/people/김선기.jpg b/solution/site/scripts/mockup/img/people/김선기.jpg new file mode 100644 index 0000000..dcf4d25 Binary files /dev/null and b/solution/site/scripts/mockup/img/people/김선기.jpg differ diff --git a/solution/site/scripts/mockup/img/people/김수미.png b/solution/site/scripts/mockup/img/people/김수미.png new file mode 100644 index 0000000..84f1993 Binary files /dev/null and b/solution/site/scripts/mockup/img/people/김수미.png differ diff --git a/solution/site/scripts/mockup/img/people/문호준.jpg b/solution/site/scripts/mockup/img/people/문호준.jpg new file mode 100644 index 0000000..4f8aaba Binary files /dev/null and b/solution/site/scripts/mockup/img/people/문호준.jpg differ diff --git a/solution/site/scripts/mockup/img/people/박화요비.jpg b/solution/site/scripts/mockup/img/people/박화요비.jpg new file mode 100644 index 0000000..257dd3d Binary files /dev/null and b/solution/site/scripts/mockup/img/people/박화요비.jpg differ diff --git a/solution/site/scripts/mockup/img/people/병헌.jpg b/solution/site/scripts/mockup/img/people/병헌.jpg new file mode 100644 index 0000000..3f278c1 Binary files /dev/null and b/solution/site/scripts/mockup/img/people/병헌.jpg differ diff --git a/solution/site/scripts/mockup/img/people/송새벽.png b/solution/site/scripts/mockup/img/people/송새벽.png new file mode 100644 index 0000000..41de0d3 Binary files /dev/null and b/solution/site/scripts/mockup/img/people/송새벽.png differ diff --git a/solution/site/scripts/mockup/img/people/오지환.png b/solution/site/scripts/mockup/img/people/오지환.png new file mode 100644 index 0000000..5a1d882 Binary files /dev/null and b/solution/site/scripts/mockup/img/people/오지환.png differ diff --git a/solution/site/scripts/mockup/img/people/이서원.png b/solution/site/scripts/mockup/img/people/이서원.png new file mode 100644 index 0000000..9b6f3fe Binary files /dev/null and b/solution/site/scripts/mockup/img/people/이서원.png differ diff --git a/solution/site/scripts/mockup/img/people/이진영.jpg b/solution/site/scripts/mockup/img/people/이진영.jpg new file mode 100644 index 0000000..6e8f78f Binary files /dev/null and b/solution/site/scripts/mockup/img/people/이진영.jpg differ diff --git a/solution/site/scripts/mockup/img/people/장신영.jpg b/solution/site/scripts/mockup/img/people/장신영.jpg new file mode 100644 index 0000000..da022d5 Binary files /dev/null and b/solution/site/scripts/mockup/img/people/장신영.jpg differ diff --git a/solution/site/scripts/mockup/img/people/정혁.jpg b/solution/site/scripts/mockup/img/people/정혁.jpg new file mode 100644 index 0000000..c182f12 Binary files /dev/null and b/solution/site/scripts/mockup/img/people/정혁.jpg differ diff --git a/solution/site/scripts/mockup/img/people/차우찬.jpg b/solution/site/scripts/mockup/img/people/차우찬.jpg new file mode 100644 index 0000000..ef93272 Binary files /dev/null and b/solution/site/scripts/mockup/img/people/차우찬.jpg differ diff --git a/solution/site/scripts/mockup/img/people/최서우.jpg b/solution/site/scripts/mockup/img/people/최서우.jpg new file mode 100644 index 0000000..1b00196 Binary files /dev/null and b/solution/site/scripts/mockup/img/people/최서우.jpg differ diff --git a/solution/site/scripts/mockup/img/people/함운경.jpg b/solution/site/scripts/mockup/img/people/함운경.jpg new file mode 100644 index 0000000..d3872b9 Binary files /dev/null and b/solution/site/scripts/mockup/img/people/함운경.jpg differ diff --git a/solution/site/scripts/mockup/inject.css b/solution/site/scripts/mockup/inject.css new file mode 100644 index 0000000..fd3db87 --- /dev/null +++ b/solution/site/scripts/mockup/inject.css @@ -0,0 +1,329 @@ +/* /s/stay 목업 주입분 — 헤더 미니 플레이어 · 캐치프레이즈 전환. + ★ 색·테두리·서체를 전부 템플릿 변수(--tpl-*)에서 가져온다. 값을 박으면 템플릿을 바꿀 때 + 플레이어만 옛 색으로 남는다. + ★ 플레이어는 헤더 **안**이다. 하단 바는 모바일 탭바(fixed bottom:0 · 61px)를 덮었고, + 상단 바는 헤더와 두 줄이 됐다. 헤더 안이면 화면에서 차지하는 줄이 늘지 않는다. */ + +/* ── 헤더에 꽂는 아이콘 세 개 ─────────────────────────────────────── */ +#w4d-mini { + display: inline-flex; + align-items: center; + gap: 2px; + margin-right: 6px; + padding-right: 8px; + border-right: 1px solid var(--tpl-border, #bcb49e); +} +#w4d-mini button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + padding: 0; + color: var(--tpl-text, #1b1a15); + background: transparent; + border: 0; + border-radius: var(--tpl-radius, 0); + cursor: pointer; + opacity: 0.75; +} +#w4d-mini button:hover { opacity: 1; background: rgb(27 26 21 / 0.06); } +#w4d-mini svg { width: 16px; height: 16px; } + +/* 지금 나오는 곡명. 좁은 화면에서는 헤더에 자리가 없어 접는다 — + 실측(390px): 상호 115px + 오른쪽 묶음 94px 이라 남는 폭이 55px 뿐이다. + 그 화면에서는 목록을 열면 재생 중인 줄에 곡명과 시간이 있다. */ +#w4d-now { + max-width: 150px; + overflow: hidden; + font-size: 11.5px; + font-weight: 700; + white-space: nowrap; + text-overflow: ellipsis; + color: var(--tpl-accent, #bf2f1b); + padding-right: 2px; +} +#w4d-mini[data-playing='0'] #w4d-now { opacity: 0.55; font-weight: 400; } +@media (max-width: 860px) { #w4d-now { display: none; } } + +/* 카세트 — 단추가 아니라 표지다. 이 자리가 무엇인지 한눈에 말해 준다. */ +#w4d-tape { + display: inline-flex; + align-items: center; + padding: 0 4px 0 2px; + color: var(--tpl-text, #1b1a15); + opacity: 0.8; +} +#w4d-tape svg { width: 20px; height: 20px; } +#w4d-mini[data-playing='1'] #w4d-tape { color: var(--tpl-accent, #bf2f1b); opacity: 1; } +/* 재생 중에는 릴이 돈다 — 소리가 나고 있다는 것을 글자 없이 말한다. */ +#w4d-mini[data-playing='1'] .w4d-reel { + transform-box: fill-box; + transform-origin: center; + animation: w4d-reel 2.6s linear infinite; +} +@keyframes w4d-reel { to { transform: rotate(360deg); } } +@media (prefers-reduced-motion: reduce) { .w4d-reel { animation: none !important; } } +/* 재생 중일 때만 강조색 — 헤더에 곡명을 안 두므로 색이 유일한 '지금 나온다' 표시다. */ +#w4d-mini button[data-playing='1'] { color: var(--tpl-accent, #bf2f1b); opacity: 1; } +@media (max-width: 400px) { + #w4d-mini { margin-right: 2px; padding-right: 4px; } + #w4d-mini button { width: 27px; height: 27px; } +} + +/* ── 목록 패널 (헤더 아래에 떠 있다) ────────────────────────────── + ★ 좁은 화면에서는 좌우 8px 만 남기고 펼친다. 320px 고정이었을 때 390px 화면에서 + 제목이 두 줄로 접히고 시간이 잘렸다(2026-09-11 사장님: "모바일 대응이 안 되어 있음"). */ +#w4d-panel { + position: fixed; + z-index: 45; /* 사이트 헤더가 z-40 이라 그 위 */ + width: 344px; + max-width: calc(100vw - 16px); + max-height: min(64vh, 520px); + display: flex; + flex-direction: column; + overflow: hidden; + font-family: var(--tpl-font-body, serif); + color: var(--tpl-text, #1b1a15); + background: var(--tpl-card, #efe7d3); + border: var(--tpl-border-width, 2px) solid var(--tpl-primary, #1b1a15); + box-shadow: var(--tpl-shadow, 4px 4px 0 rgb(27 26 21 / 0.16)); +} +#w4d-panel[hidden] { display: none; } +@media (max-width: 640px) { #w4d-panel { width: auto; } } + +#w4d-panel .w4d-head { + display: flex; + align-items: center; + gap: 8px; + flex: 0 0 auto; + padding: 11px 8px 11px 14px; + border-bottom: var(--tpl-border-width, 2px) solid var(--tpl-primary, #1b1a15); +} +.w4d-head-t { font-size: 12px; font-weight: 700; letter-spacing: 0.06em; } +.w4d-head-n { flex: 1 1 auto; font-size: 11px; opacity: 0.5; } +#w4d-panel .w4d-head button { + display: inline-flex; + width: 28px; + height: 28px; + flex: 0 0 auto; + align-items: center; + justify-content: center; + color: inherit; + background: transparent; + border: 0; + cursor: pointer; + opacity: 0.6; +} +#w4d-panel .w4d-head button:hover { opacity: 1; } +#w4d-panel .w4d-head svg { width: 14px; height: 14px; } + +#w4d-panel ol { + flex: 1 1 auto; + margin: 0; + padding: 4px 0; + overflow-y: auto; + list-style: none; + -webkit-overflow-scrolling: touch; +} + +/* 한 줄 = 판 + 제목/가수 + 시간. 48px 아래로 내리지 않는다 — 손가락으로 누르는 자리다. */ +.w4d-item { + display: flex; + width: 100%; + min-height: 52px; + align-items: center; + gap: 11px; + padding: 8px 14px; + color: inherit; + background: transparent; + border: 0; + font: inherit; + text-align: left; + cursor: pointer; +} +.w4d-item + .w4d-item { border-top: 1px dashed var(--tpl-border, #bcb49e); } +.w4d-item:hover { background: rgb(27 26 21 / 0.05); } +.w4d-item[aria-current='true'] { background: rgb(27 26 21 / 0.06); } +.w4d-item[aria-current='true'] .w4d-t { color: var(--tpl-accent, #bf2f1b); } + +/* 판 자리. 재생 중인 줄에서는 판 대신 막대가 뛴다. */ +.w4d-mark { position: relative; flex: 0 0 26px; height: 26px; } +.w4d-disc-sm { + position: absolute; + inset: 0; + border-radius: 50%; + background: + radial-gradient(circle at 50% 50%, var(--w4d-lbl, #bf2f1b) 0 34%, transparent 34%), + repeating-radial-gradient(circle at 50% 50%, rgb(255 255 255 / 0.12) 0 1px, transparent 1px 3px), + #14110f; +} +.w4d-disc-sm::after { + content: ''; + position: absolute; + inset: 45%; + border-radius: 50%; + background: var(--tpl-card, #efe7d3); +} +.w4d-eq { + position: absolute; + inset: 0; + display: none; + align-items: flex-end; + justify-content: center; + gap: 3px; + padding-bottom: 4px; +} +.w4d-eq i { + width: 3px; + height: 6px; + background: var(--tpl-accent, #bf2f1b); + animation: w4d-eq 0.9s ease-in-out infinite; +} +.w4d-eq i:nth-child(2) { animation-delay: 0.18s; } +.w4d-eq i:nth-child(3) { animation-delay: 0.36s; } +@keyframes w4d-eq { 0%, 100% { height: 5px; } 50% { height: 17px; } } +.w4d-item[data-playing='1'] .w4d-disc-sm { display: none; } +.w4d-item[data-playing='1'] .w4d-eq { display: flex; } +@media (prefers-reduced-motion: reduce) { .w4d-eq i { animation: none; height: 11px; } } + +.w4d-info { min-width: 0; flex: 1 1 auto; } +.w4d-t { + display: block; + overflow: hidden; + font-size: 13.5px; + font-weight: 700; + white-space: nowrap; + text-overflow: ellipsis; +} +.w4d-sub { + display: block; + margin-top: 2px; + overflow: hidden; + font-size: 11px; + white-space: nowrap; + text-overflow: ellipsis; + opacity: 0.55; +} +.w4d-time { + flex: 0 0 auto; + font-size: 11px; + opacity: 0.6; + font-variant-numeric: tabular-nums; +} + +/* 가사 — 우리 곡만. 종이 위 인쇄물처럼 둔다. */ +#w4d-lyrics-box { + padding: 10px 12px 14px; + border-top: 1px dashed var(--tpl-border, #bcb49e); + white-space: pre-line; + font-size: 12px; + line-height: 1.9; +} +#w4d-lyrics-box[hidden] { display: none; } + +/* ★ 이용 정보 맨 아래 가로선을 지운다 (2026-09-10 사장님 지시). +


이 아니라 바로 뒤 '예약은 아래로 받습니다' 블록의 border-t 다 — 마지막 항목 밑에 선이 + 한 줄 더 그어져 빈 칸이 하나 더 있는 것처럼 보인다. 항목 자체는 그대로 둔다. + 마크업을 고치면 하이드레이션이 되돌리므로 CSS 로 끈다. */ +#root div.border-line.flex.flex-col.gap-3.border-t.pt-6 { border-top-width: 0; } + +/* 사진 저작자 표시 — 출처 줄과 같은 크기, 한 단 더 옅게. */ +.w4d-credit { margin-top: 2px; font-size: 10px; opacity: 0.55; } + +/* ★ 모바일 헤더에서 전화 단추를 접는다 (2026-09-11 사장님: "저 전화 아이콘은 빼도 되잖아"). + 좁은 화면 헤더에 상호 + 플레이어 3개 + 전화 + 메뉴가 다 서서 난잡했다. + ★ 전화가 사라지는 게 아니다 — 이 화면 폭에서는 **하단 탭바에 전화가 이미 있다** + (실측: NAV fixed bottom:0 · 위치 · 전화 · 네이버 예약). 같은 것이 두 번 서 있던 것이다. + ★ 넓은 화면에서는 탭바가 없으므로 그대로 둔다. */ +@media (max-width: 860px) { + #root header .shell > div:last-child > a[href^="tel:"] { display: none; } +} + +/* 덧붙인 링크 — 본문 글자보다 눈에 띄되 강조색을 남발하지 않는다. */ +.w4d-link { + color: var(--tpl-accent, #bf2f1b); + text-decoration: underline; + text-underline-offset: 3px; + white-space: nowrap; +} + +/* ── 캐치프레이즈 등장 ──────────────────────────────────────────── + ★ 줄 전체를 페이드하지 않고 **단어가 마스크 안에서 올라온다.** + 한 줄짜리 문장은 통째로 흐려지면 '바뀌었다'만 보이고 문장이 안 읽힌다 — + 왼쪽부터 차례로 서면 눈이 첫 단어에서 읽기를 시작한다. + ★ 나갈 때는 반대로 위로 빠지며 흐려진다. 들어오는 방향과 나가는 방향이 같으면 + 두 문장이 같은 자리에서 겹쳐 보인다. + ★ 자리 흔들림: 처음 구워진 줄 높이를 min-height 로 박아 둔다(스크립트가 잰다). + 문장 길이가 달라 한 줄↔두 줄이 오갈 때 히어로 아래가 튀는 것을 막는다. */ +.w4d-line { display: block; } +.w4d-line--out { + opacity: 0; + transform: translateY(-5px); + filter: blur(3px); + transition: opacity 0.3s ease, transform 0.3s ease, filter 0.3s ease; +} +.w4d-line--in { opacity: 1; transform: none; filter: none; transition: opacity 0.3s ease, transform 0.3s ease, filter 0.3s ease; } + +/* 단어 한 칸 = 마스크. 안쪽 글자가 아래에서 올라온다. */ +.w4d-w { display: inline-block; overflow: hidden; vertical-align: bottom; } +.w4d-w > span { + display: inline-block; + transform: translateY(112%); + opacity: 0; + filter: blur(4px); +} +.w4d-line--in .w4d-w > span { + animation: w4d-rise 0.62s cubic-bezier(0.16, 0.84, 0.28, 1) forwards; + animation-delay: var(--w4d-d, 0ms); +} +@keyframes w4d-rise { + from { transform: translateY(112%); opacity: 0; filter: blur(4px); } + 60% { filter: blur(0); } + to { transform: translateY(0); opacity: 1; filter: blur(0); } +} + +@media (prefers-reduced-motion: reduce) { + .w4d-line--out, .w4d-line--in { transition: none; opacity: 1; transform: none; filter: none; } + .w4d-w > span { transform: none; opacity: 1; filter: none; animation: none; } +} + +/* 가사 — 우리 곡만. 종이 위 인쇄물처럼 둔다. */ +#w4d-lyrics-box { + max-width: 1120px; + margin: 0 auto; + padding: 12px 16px 16px; + border-top: 1px dashed var(--tpl-border, #bcb49e); + white-space: pre-line; + font-size: 13px; + line-height: 1.9; + max-height: 40vh; + overflow-y: auto; +} +#w4d-lyrics-box[hidden] { display: none; } + +/* + * 일정 정거장 번호 — 제목 글자와 세로 중심을 맞춘다. + * + * 번호 동그라미는 inline-grid 라 **안에 든 숫자(10px)의 베이스라인**이 제목(14px)의 + * 베이스라인에 붙는다. 숫자가 17px 상자 한가운데 있으니 동그라미 중심은 그만큼 아래로 + * 내려가고, 렌더러가 얹은 translate-y-px 가 1px 더 내린다. + * 실측(1280·390 공통): 동그라미 중심이 제목 줄 중심보다 3.14px 아래. + * 2px 올리면 0.14px — 반올림해서 눈에 안 걸리는 자리다. + */ +#itinerary ol li span.inline-grid.rounded-full { + translate: 0 -2px; + transform: none; +} + +/* + * 날씨 그림 — 움직임만 끈다 (2026-09-11, 사장님: "날씨 애니메이션 지워줘") + * 그림 자체는 남는다. `prefers-reduced-motion` 에서 보이는 것과 같은 상태다 + * (weather.css 가 "멈춘 상태도 그림으로 성립해야 한다"는 전제로 그려져 있다). + * 그림까지 없애려면 아래 두 줄의 주석을 푼다. + */ +#weather .w4-sky::before, +#weather .w4-sky::after { + animation: none !important; +} +/* #weather .w4-sky { display: none; } */ diff --git a/solution/site/scripts/mockup/inject.js b/solution/site/scripts/mockup/inject.js new file mode 100644 index 0000000..fbe5a81 --- /dev/null +++ b/solution/site/scripts/mockup/inject.js @@ -0,0 +1,708 @@ +/* /s/stay 목업 주입분 — ① 캐치프레이즈 순환 ② 헤더 안 미니 플레이어. + * + * ★ 플레이어는 자리를 세 번 옮겼다. 지금은 **사이트 헤더 안**이다. + * ① 하단 바 → 모바일에서 발행본의 탭바(fixed bottom:0 · 61px · z-50 — 위치·전화·예약)를 덮었다. + * ② 상단 바 → 헤더와 두 줄이 되어 화면 위 120px 이 크롬이 됐다("조금 이상한데"). + * ③ 헤더 안 아이콘 — 줄이 늘지 않는다. 원판·곡명 없이 재생·다음·목록 세 단추뿐이고, + * 곡 정보는 목록을 열 때만 보인다. + * + * ★ 헤더는 React 가 그린다. 그래서 두 가지를 지킨다. + * · 단추는 헤더 안에 꽂지만, 없어졌는지 감시해서 다시 꽂는다(하이드레이션 재렌더가 지운다). + * · 목록 패널은 #root **밖**(body 직속)에 둔다 — 안에 두면 재렌더에 통째로 날아간다. + * + * ★ 재생은 곡의 audioUrl(사장님이 직접 만든 음원)만 쓴다. 파일은 `/s//audio/…` 에 있고, + * nginx 의 `^~ /s/` 가 그대로 서빙한다(Range 206 확인). 옛 가요 네 곡은 우리 음원이 아니라 + * 플레이어에 올리지 않는다 — 가요 다방 섹션에는 그대로 남는다. + * ★ 가사는 payload 에 있을 때만 낸다. 직접 만든 곡이라 원문을 실어도 되는 유일한 경우다. + */ +(function () { + 'use strict'; + + var P = window.__SITE_PAYLOAD__; + if (!P) return; + + var REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + function pick(list) { + return list && list.length ? list[Math.floor(Math.random() * list.length)] : null; + } + + /* ══ ① 캐치프레이즈 ══════════════════════════════════════════════════════ + * 일반(랜덤) → 계절 → 일반 → 월 → 일반 → 날씨 순으로 한 바퀴를 만들고 그 바퀴를 돈다. + * 일반은 자루에서 뽑아 쓰고 자루가 비면 다시 채운다 — 매번 랜덤이면 같은 문장이 연달아 나온다. + */ + function catchphrases() { + var box = P.narrative && P.narrative.catchphrases; + return (box && box.items) || []; + } + + /* 3개월 단위. 12·1·2 겨울 / 3·4·5 봄 / 6·7·8 여름 / 9·10·11 가을 */ + var SEASON_OF_MONTH = ['겨울', '겨울', '봄', '봄', '봄', '여름', '여름', '여름', '가을', '가을', '가을', '겨울']; + + function startCatchphrase() { + var items = catchphrases(); + if (items.length === 0) return; + + var base = ((P.narrative && P.narrative.tagline) || '').trim(); + var node = null; + var last = null; + + /* ★ 노드를 들고 있지 않고 매번 찾는다. + 이 목업은 payload 를 손으로 고친 것이라 구워진 마크업과 어긋나고, React 는 하이드레이션에 + 실패하면 #root 를 **통째로 다시 그린다** — 그때 히어로의

가 새 노드로 바뀐다. + 한 번 잡아 둔 참조로 계속 쓰면 그 순간부터 아무 일도 안 일어나면서 오류도 안 난다. */ + function findNode() { + var wanted = [base]; + if (last) wanted.push(last); + var candidates = document.querySelectorAll('#root p'); + for (var i = 0; i < candidates.length; i += 1) { + if (wanted.indexOf(candidates[i].textContent.trim()) >= 0) return candidates[i]; + } + return null; + } + if (!findNode()) return; + + var byKind = function (kind) { + return items.filter(function (it) { return it.kind === kind; }); + }; + var weather = null; // 못 받아 오면 날씨 문장은 아예 순환에 넣지 않는다. + var bag = []; + var cycle = []; + + function nextGeneral() { + if (bag.length === 0) { + bag = byKind('general').slice(); + for (var j = bag.length - 1; j > 0; j -= 1) { + var k = Math.floor(Math.random() * (j + 1)); + var t = bag[j]; bag[j] = bag[k]; bag[k] = t; + } + } + return bag.pop(); + } + + function buildCycle() { + var now = new Date(); + var month = now.getMonth() + 1; + var season = SEASON_OF_MONTH[month - 1]; + var out = [nextGeneral()]; + out.push(pick(byKind('season').filter(function (it) { return it.season === season; }))); + out.push(nextGeneral()); + out.push(pick(byKind('month').filter(function (it) { return it.month === month; }))); + out.push(nextGeneral()); + if (weather) { + out.push(pick(byKind('weather').filter(function (it) { return it.weather === weather; }))); + } + return out.filter(Boolean); + } + + /* 문장 하나를 단어 칸으로 조립한다. innerHTML 을 안 쓰는 이유는 문구가 데이터라서다 — + 지금은 우리가 쓴 문장이지만, 이 자리는 나중에 서버가 채운다. */ + function paint(text) { + var line = document.createElement('span'); + line.className = 'w4d-line'; + text.split(/\s+/).forEach(function (word, index) { + if (index > 0) line.appendChild(document.createTextNode(' ')); + var mask = document.createElement('span'); + mask.className = 'w4d-w'; + var inner = document.createElement('span'); + inner.textContent = word; + // 단어마다 조금씩 늦게 선다. 55ms 면 열 단어가 0.5초 안에 다 서서 읽기를 안 기다리게 한다. + inner.style.setProperty('--w4d-d', index * 55 + 'ms'); + mask.appendChild(inner); + line.appendChild(mask); + }); + node.textContent = ''; + node.appendChild(line); + // 클래스를 한 프레임 뒤에 건다 — 같은 프레임에 붙이면 애니메이션 시작 상태가 없다. + window.requestAnimationFrame(function () { line.classList.add('w4d-line--in'); }); + } + + function show(text) { + last = text; + if (REDUCED) { node.textContent = text; return; } + var current = node.firstElementChild; + if (current && current.classList.contains('w4d-line')) { + current.classList.remove('w4d-line--in'); + current.classList.add('w4d-line--out'); + } + window.setTimeout(function () { + if (!node || !node.isConnected) node = findNode(); + if (!node) return; + paint(text); + }, 300); + } + + function tick() { + if (!node || !node.isConnected) node = findNode(); + if (!node) return; + if (cycle.length === 0) cycle = buildCycle(); + var next = cycle.shift(); + if (next && next.text) show(next.text); + } + + /* 날씨는 렌더러가 쓰는 그 엔드포인트로 받는다(같은 오리진). 실패하면 조용히 넘어간다 — + 날씨 문장 몇 개 때문에 순환이 멈추면 안 된다. */ + function refreshWeather() { + var place = P.place || {}; + if (!place.regionCode || place.latitude == null) return; + var query = 'region_code=' + encodeURIComponent(place.regionCode) + + '&latitude=' + place.latitude + '&longitude=' + place.longitude; + fetch('/v1/local/weather?' + query) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (body) { + if (!body || !body.weather) return; + var code = Number(body.weather.weather_code); + // 렌더러(use-live-weather.ts)와 같은 분류를 쓴다 — 갈리면 화면의 날씨와 문장이 어긋난다. + weather = code === 0 ? '맑음' + : code >= 1 && code <= 3 ? '구름많음' + : code >= 50 && code <= 69 ? '비' + : code >= 70 && code <= 79 ? '눈' : '흐림'; + }) + .catch(function () { /* 구운 값 그대로 둔다 */ }); + } + + refreshWeather(); + window.setInterval(refreshWeather, 10 * 60 * 1000); + // 구워진 줄 높이를 하한으로 박는다 — 문장이 짧아 한 줄이 되면 아래 단추가 위로 튄다. + (function reserve() { + var found = findNode(); + if (!found) return; + var h = found.getBoundingClientRect().height; + if (h > 0) found.style.minHeight = Math.ceil(h) + 'px'; + })(); + // 첫 문장은 구워진 그대로 5초 두고 시작한다 — 들어오자마자 글자가 바뀌면 읽던 것을 놓친다. + window.setTimeout(function () { + tick(); + window.setInterval(tick, 7000); + }, 5000); + } + + /* ══ ② 헤더 안 미니 플레이어 ═══════════════════════════════════════════════ + * ★ 단추는 둘뿐이다 — 재생/멈춤과 목록 (2026-09-10 사장님: 셔플 단추 "얘는 빼도 되지 않을까?"). + * 곡이 끝나면 어차피 무작위로 다음 곡이 걸리므로 셔플 단추가 할 일이 없었다. + * 다른 곡을 지금 듣고 싶으면 목록에서 고른다 — 그게 더 짧은 길이다. */ + var ICON = { + /* ★ 카세트가 먼저 선다 (2026-09-10 사장님: "전혀 뮤직플레이어 같지가 않아"). + 세모·셔플·줄 세 개만 두면 헤더의 다른 아이콘과 구별이 안 된다 — 무엇을 하는 물건인지가 + 한눈에 안 읽힌다. 카세트는 그 자체가 '노래' 라는 표시이고, 재생 중에는 릴이 돈다. */ + cassette: '' + + '' + + '' + + '' + + '', + note: '', + play: '', + pause: '', + /* ★ 햄버거(줄 세 개)를 쓰지 않는다 (2026-09-11 사장님: "햄버거 메뉴 버튼이랑 음악 메뉴 + 아이콘이랑 같아서 헷갈림"). 바로 옆에 사이트 메뉴 단추가 같은 모양으로 서 있었다. + 줄 끝에 음표를 달아 **재생 목록**으로 읽히게 한다. */ + list: '' + + '', + close: '', + }; + + /* 플레이어가 트는 것은 **숙소가 만든 곡**이다(payload.narrative.ownSongs). + 가요 다방 섹션(theme.sections.songs)은 이 도시를 노래한 옛 곡이라 우리 음원이 없다 — + 목록에 세우면 누를 때마다 유튜브로 튕겨 나간다. 두 목록은 섞지 않는다. */ + function songsOfPayload() { + var box = P.narrative && P.narrative.ownSongs; + return (box && box.items) || []; + } + + function startPlayer() { + var songs = songsOfPayload().filter(function (song) { return song.audioUrl; }); + if (songs.length === 0) return; + + /* ★ **시작 곡만 무작위, 그다음은 순차**다 (2026-09-11 사장님 지시). + 매번 무작위로 뽑으면 다섯 곡을 한 바퀴 도는 데 순서가 없어 "아까 그 곡" 을 다시 찾기 어렵다. + 손님마다 다른 곡으로 시작하되(들어올 때마다 같은 곡이면 배경음악이 하나인 것처럼 들린다), + 한 번 시작하면 A면 순서대로 끝까지 돌고 처음으로 돌아온다. */ + var cur = Math.floor(Math.random() * songs.length); + var playing = false; + + function nextIndex() { + return (cur + 1) % songs.length; + } + + /* ── 헤더에 꽂을 단추 세 개 ─────────────────────────────────────── */ + var mini = document.createElement('span'); + mini.id = 'w4d-mini'; + mini.innerHTML = + '' + + '' + + '' + + ''; + + /* ── 목록 패널 (body 직속) ──────────────────────────────────────── */ + var panel = document.createElement('div'); + panel.id = 'w4d-panel'; + panel.hidden = true; + panel.innerHTML = + '

' + + '머뭄이 만든 노래' + + '' + songs.length + '곡' + + '
' + + '
    ' + + ''; + document.body.appendChild(panel); + + var listOl = panel.querySelector('ol'); + var $ = function (id) { return document.getElementById(id); }; + + var audio = new Audio(); + audio.preload = 'none'; // 손님이 누를 때까지 6MB 를 받지 않는다 + audio.addEventListener('timeupdate', paintRow); + audio.addEventListener('durationchange', paintRow); + audio.addEventListener('ended', function () { load(nextIndex(), true); }); + audio.addEventListener('error', function () { playing = false; render(); }); + + function clock(sec) { + if (typeof sec !== 'number' || !isFinite(sec)) return ''; + var m = Math.floor(sec / 60); + var s = Math.floor(sec % 60); + return m + ':' + (s < 10 ? '0' : '') + s; + } + + function meta(song) { + return [song.artist, song.year].filter(Boolean).join(' · '); + } + + /* 목록 한 줄 = 판 하나 + 제목/가수 + 시간. + ★ 줄 높이를 48px 아래로 내리지 않는다 — 손가락으로 누르는 자리다(모바일). */ + function buildList() { + listOl.innerHTML = ''; + songs.forEach(function (song, index) { + var li = document.createElement('li'); + var btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'w4d-item'; + btn.innerHTML = + '' + + '' + + '' + + '' + + '' + + ''; + btn.querySelector('.w4d-disc-sm').style.setProperty('--w4d-lbl', song.labelColor || '#bf2f1b'); + btn.querySelector('.w4d-t').textContent = song.title; + btn.querySelector('.w4d-sub').textContent = song.artist || ''; + btn.addEventListener('click', function () { load(index, true); }); + li.appendChild(btn); + listOl.appendChild(li); + }); + } + + /* 시간은 지금 재생 중인 줄에만 적는다 — 헤더에는 곡명도 시간도 두지 않기로 했다. + ★ 나머지 줄은 비워 둔다. 예전에는 가수 이름을 넣었는데 다섯 곡이 전부 '스테이,머뭄' 이라 + 같은 글자가 다섯 줄 반복됐다(가수는 줄 안에 이미 있다). */ + function paintRow() { + var rows = listOl.querySelectorAll('.w4d-item'); + for (var i = 0; i < rows.length; i += 1) { + var slot = rows[i].querySelector('.w4d-time'); + if (i !== cur || !audio.src) { slot.textContent = ''; continue; } + slot.textContent = playing || audio.currentTime > 0 + ? clock(audio.currentTime) + (audio.duration ? ' / ' + clock(audio.duration) : '') + : ''; + } + } + + function render() { + var song = songs[cur] || {}; + /* 헤더에 **지금 나오는 곡명**을 적는다 (2026-09-11 사장님: "현재 곡명이 나오는 게 맞지 않나"). + 소리가 나는데 무슨 곡인지 화면 어디에도 없으면 손님이 목록을 열어 봐야 안다. */ + $('w4d-now').textContent = song.title || ''; + $('w4d-play').innerHTML = playing ? ICON.pause : ICON.play; + $('w4d-play').dataset.playing = playing ? '1' : '0'; + mini.dataset.playing = playing ? '1' : '0'; + $('w4d-play').title = (playing ? '멈춤 · ' : '재생 · ') + (song.title || ''); + var rows = listOl.querySelectorAll('.w4d-item'); + for (var i = 0; i < rows.length; i += 1) { + rows[i].setAttribute('aria-current', i === cur ? 'true' : 'false'); + rows[i].dataset.playing = i === cur && playing ? '1' : '0'; + } + var box = $('w4d-lyrics-box'); + if (song.lyrics) { box.textContent = song.lyrics; box.hidden = false; } else { box.hidden = true; } + paintRow(); + } + + function stop() { + playing = false; + audio.pause(); // 자리는 남긴다 — 다시 누르면 이어 듣는다 + } + + function load(index, alsoPlay) { + cur = (index + songs.length) % songs.length; + stop(); + audio.removeAttribute('src'); + render(); + if (alsoPlay) play(); + } + + function play() { + var song = songs[cur] || {}; + if (!song.audioUrl) return; + if (audio.getAttribute('src') !== song.audioUrl) audio.src = song.audioUrl; + var started = audio.play(); + // 자동재생 차단·파일 오류는 예외가 아니라 거절된 약속으로 온다. + if (started && started.catch) started.catch(function () { playing = false; render(); }); + playing = true; + render(); + } + + /* 패널은 fixed 라 헤더 밑에 손으로 붙인다 — 헤더가 sticky 라 스크롤해도 자리가 같다. */ + /* 패널 자리. 좁은 화면에서는 좌우를 화면에 맞춰 펼친다 — 320px 고정이면 390px 화면에서 + 한쪽이 잘리거나 글자가 두 줄로 접힌다(2026-09-11 사장님: "모바일 대응이 안 되어 있음"). */ + function placePanel() { + var header = document.querySelector('#root header'); + var box = header ? header.getBoundingClientRect() : null; + panel.style.top = (box ? box.bottom + 6 : 66) + 'px'; + if (window.innerWidth <= 640) { + panel.style.left = '8px'; + panel.style.right = '8px'; + } else { + panel.style.left = 'auto'; + var anchor = mini.getBoundingClientRect(); + panel.style.right = Math.max(12, window.innerWidth - anchor.right) + 'px'; + } + } + + function togglePanel(force) { + var open = typeof force === 'boolean' ? force : panel.hidden; + panel.hidden = !open; + $('w4d-toggle').setAttribute('aria-expanded', String(open)); + if (open) placePanel(); + } + + mini.addEventListener('click', function (event) { + var btn = event.target.closest('button'); + if (!btn) return; + if (btn.id === 'w4d-play') { if (playing) { stop(); render(); } else { play(); } } + if (btn.id === 'w4d-toggle') { togglePanel(); } + }); + panel.querySelector('#w4d-close').addEventListener('click', function () { togglePanel(false); }); + window.addEventListener('resize', function () { if (!panel.hidden) placePanel(); }); + window.addEventListener('scroll', function () { if (!panel.hidden) placePanel(); }, {passive: true}); + document.addEventListener('click', function (event) { + if (panel.hidden) return; + if (panel.contains(event.target) || mini.contains(event.target)) return; + togglePanel(false); + }); + + /* 가요 다방 섹션의 판을 누르면 그 곡이 얹힌다. 섹션에는 옛 가요도 서 있어 순번이 어긋나므로 + 제목으로 찾는다. 우리 목록에 없는 곡(옛 가요)이면 섹션이 알아서 한다. */ + document.addEventListener('click', function (event) { + var btn = event.target && event.target.closest ? event.target.closest('#songs button') : null; + if (!btn) return; + var label = (btn.textContent || '').trim(); + var index = -1; + songs.forEach(function (song, i) { if (index < 0 && song.title === label) index = i; }); + if (index >= 0) load(index, false); + }); + + /* ★ 헤더에 꽂고, 지워지면 다시 꽂는다. + React 가 하이드레이션에 실패하면 #root 를 통째로 다시 그린다 — 그때 단추가 사라진다. + 한 번 꽂고 끝내면 그 순간부터 조용히 없어진다(오류도 안 난다). */ + function mount() { + if (mini.isConnected) return true; + var header = document.querySelector('#root header'); + if (!header) return false; + // 헤더 오른쪽 묶음(전화·메뉴 단추가 있는 자리) 맨 앞에 꽂는다. + var slot = header.querySelector('.shell > div:last-child') || + header.querySelector('div:last-child'); + if (!slot) return false; + slot.insertBefore(mini, slot.firstChild); + return true; + } + + /* ★ 자동재생은 두지 않는다 (2026-09-11 사장님: "그럼 자동 빼"). + 크롬·사파리는 소리 나는 재생을 사용자 조작 없이 허용하지 않는다. 그래서 "막히면 + 손님이 처음 만지는 순간 켠다" 를 넣어 봤는데, 그 리스너가 window 캡처라 + **재생 단추의 클릭을 먼저 먹었다** — 첫 클릭에 켜졌다가 단추 핸들러가 다시 꺼서 + 두 번 눌러야 켜졌다. 자동재생을 빼면 그 충돌 자체가 없어진다. + 시작 곡이 무작위인 것은 그대로다 — 손님이 재생을 누르면 그 곡부터 순서대로 돈다. */ + + buildList(); + mount(); + render(); + if (window.MutationObserver) { + new MutationObserver(function () { + if (!mini.isConnected) { mount(); render(); } + }).observe(document.getElementById('root') || document.body, {childList: true, subtree: true}); + } + console.log('[w4d] 미니 플레이어 — 머뭄이 만든 곡 ' + songs.length + '곡'); + } + + + /* ══ ③ 링크 걸기 ═══════════════════════════════════════════════════════════ + * 렌더러는 일정의 "지도 검색 …" 과 엽서의 장소를 **글자로만** 낸다. 손님이 그 자리에서 + * 지도를 열 방법이 없다 — 이름을 복사해 다른 앱에 붙여야 한다. + * 렌더러를 고치면 다른 사이트까지 바뀌므로(목업만 손대는 규약), 여기서 DOM 을 덧칠한다. + * + * ★ 네이버 지도 검색 주소를 쓴다. 좌표로 핀을 찍는 주소는 앱·웹에서 형태가 달라 한쪽이 깨진다. + * ★ React 가 다시 그리면 사라지므로 감시해서 다시 건다. + */ + function sectionItems(id) { + var sections = (P.theme && P.theme.sections) || []; + for (var i = 0; i < sections.length; i += 1) { + if (sections[i].id !== id) continue; + try { + var env = typeof sections[i].data === 'string' ? JSON.parse(sections[i].data) : sections[i].data; + return (env && env.items) || []; + } catch (err) { + return []; + } + } + return []; + } + + function mapUrl(query) { + return 'https://map.naver.com/p/search/' + encodeURIComponent(query); + } + + function linkify() { + credits(); + // 일정: "10:20–11:50 · 지도 검색 군산근대역사박물관" + var rows = document.querySelectorAll('#itinerary li p'); + Array.prototype.forEach.call(rows, function (row) { + if (row.querySelector('a')) return; + var text = row.textContent || ''; + var mark = text.indexOf('지도 검색 '); + if (mark < 0) return; + var query = text.slice(mark + 6).trim(); + if (!query) return; + var head = document.createTextNode(text.slice(0, mark)); + var link = document.createElement('a'); + link.href = mapUrl(query); + link.target = '_blank'; + link.rel = 'noopener noreferrer nofollow'; + link.className = 'w4d-link'; + link.textContent = '지도에서 열기 ↗'; + row.textContent = ''; + row.appendChild(head); + row.appendChild(link); + }); + + /* 엽서: **payload 의 place** 로 링크를 만든다. + 화면의 소인 글자를 긁었더니 "군산 군산" 같은 주소가 나왔다 — 소인은 지명이 아니라 + 도장 문구다(예: "군산 內港"). 카드 순서와 payload 항목 순서가 같으므로 자리로 짝짓는다. */ + var cards = document.querySelectorAll('#postcard article'); + var postcards = sectionItems('postcard'); + Array.prototype.forEach.call(cards, function (card, index) { + if (card.querySelector('a.w4d-link')) return; + var item = postcards[index]; + var place = item && (item.place || item.postmark); + if (!place) return; + var tags = Array.prototype.filter.call(card.querySelectorAll('p'), function (el) { + return (el.textContent || '').trim().charAt(0) === '#'; + })[0]; + if (!tags) return; + var link = document.createElement('a'); + link.href = mapUrl(place.indexOf('군산') >= 0 ? place : '군산 ' + place); + link.target = '_blank'; + link.rel = 'noopener noreferrer nofollow'; + link.className = 'w4d-link'; + link.textContent = ' 지도에서 열기 ↗'; + tags.appendChild(link); + }); + } + + /* 인물 사진의 저작자 표시. + ★ 이 목업이 물고 있는 렌더러 번들은 `imageCredit` 을 그리지 않는 옛 판이다(고쳐도 다른 + 사이트까지 다시 구워야 해서 손대지 않는다). 그런데 사진은 CC BY·BY-SA·공공누리라 + **저작자 표시가 라이선스 조건**이다 — 안 나오면 침해다. 그래서 여기서 붙인다. */ + function credits() { + var cards = document.querySelectorAll('#people article'); + var items = sectionItems('people'); + Array.prototype.forEach.call(cards, function (card, index) { + if (card.querySelector('.w4d-credit')) return; + var item = items[index]; + if (!item || !item.imageCredit || !card.querySelector('img')) return; + var line = card.querySelector('p:last-of-type') || card; + var span = document.createElement('p'); + span.className = 'w4d-credit'; + span.textContent = item.imageCredit; + line.parentElement.appendChild(span); + }); + } + + function watchLinks() { + linkify(); + if (!window.MutationObserver) return; + var timer = null; + new MutationObserver(function () { + window.clearTimeout(timer); + timer = window.setTimeout(linkify, 300); + }).observe(document.getElementById('root') || document.body, {childList: true, subtree: true}); + } + + + /* ══ ④ 카로셀 — 손님이 만지는 동안에는 자동으로 넘기지 않는다 ══════════════════ + * + * ★ 이 페이지의 카로셀은 **구현이 둘**이다. + * ① embla-carousel(라이브러리) — 일정·축제·주변 안내. `.slider-viewport` 를 쓰고 + * 자동 넘김은 `embla.scrollNext()` 로 움직인다. + * ② 손으로 만든 가로 스크롤 상자 — 객실·영상·갤러리. `box.scrollBy()` 로 움직인다. + * 정책만 `site/src/lib/ui/use-rail-autoplay.ts` 한 벌을 같이 쓴다. + * 처음에는 ②만 막았다(`scrollBy` 를 삼키는 방식) — 그래서 **일정 카로셀만 계속 넘어갔다** + * (2026-09-11 사장님: "스케줄 보려고 밑에 스크롤 했는데 그다음에 바로 자동 넘어가"). + * + * ★ 그래서 훅이 **이미 가진 정지 장치**를 쓴다. + * 훅은 레일 위 `pointerdown` 에 정지 카운터를 올리고 window `pointerup` 에 내린다. + * 구현이 둘이어도 이 카운터는 하나다 — 여기에 손을 얹으면 embla 든 스크롤 상자든 같이 멈춘다. + * (`button: 2` 로 쏜다. embla 는 주 버튼이 아닌 pointerdown 을 드래그로 치지 않아 + * 손님의 진짜 드래그를 방해하지 않는다.) + * + * ★ 두 가지로 나눠 대응한다. + * · **쿨타임** — 페이지를 세로로 스크롤하는 것도 "읽는 중" 이다. 무엇이든 만지면 + * 7초 동안 모든 레일이 멈춘다. 손을 떼고 7초가 지나야 다시 돈다. + * · **영구 정지** — 레일을 직접 민 경우. 자동 넘김이 하려던 일("옆에 더 있다")은 + * 그 손님에게 이미 전달됐다. 그 레일은 다시 돌지 않는다. + */ + var COOLDOWN_MS = 7000; + + function tameRails() { + var quietUntil = 0; + var holding = false; + var forever = []; // 손님이 직접 민 레일 + var rails = []; + /* + * ★ 손이 화면에 닿아 있는 동안은 무조건 멈춘다 (2026-09-11, 사장님: "유저가 터치로 잡고 있는데 자동으로 넘어가는 게 문제") + * 쿨타임(7초)만으로는 못 막는다. 손가락을 대고 가만히 읽고 있으면 touchmove 도 scroll 도 + * 안 오니 쿨타임이 그냥 끝난다. 게다가 브라우저는 터치가 페이지 스크롤로 넘어가는 순간 + * **손가락이 아직 닿아 있는데 `pointercancel` 을 쏜다** — 훅의 정지 카운터가 거기서 풀린다. + * 실측: touchstart 0.0s → pointercancel 7.5s → 8.4s 에 손가락 밑에서 카드가 넘어갔다. + * 그래서 pointer 가 아니라 **touches 개수**로 센다. 손이 떨어져야 0 이 된다. + */ + var fingers = 0; + var mouseDown = false; + + function collect() { + rails = Array.prototype.slice.call(document.querySelectorAll('#root .slider-viewport')); + Array.prototype.forEach.call(document.querySelectorAll('#root *'), function (el) { + if (rails.indexOf(el) >= 0) return; + var flow = window.getComputedStyle(el).overflowX; + if ((flow === 'auto' || flow === 'scroll') && el.scrollWidth - el.clientWidth > 40) { + rails.push(el); + } + }); + } + + function press(rail) { + rail.dispatchEvent(new PointerEvent('pointerdown', {bubbles: true, button: 2, buttons: 2})); + } + + function holdAll() { + collect(); + rails.forEach(press); + holding = true; + } + + function releaseAll() { + // 해제는 window 하나로 전부에게 간다(훅이 window 에서 받는다). 영구 정지분은 곧바로 다시 잡는다. + window.dispatchEvent(new PointerEvent('pointerup', {bubbles: true, button: 2})); + holding = false; + forever.forEach(press); + } + + function railOf(node) { + while (node && node.nodeType === 1 && node !== document.body) { + if (rails.indexOf(node) >= 0) return node; + node = node.parentElement; + } + return null; + } + + /* + * ★ "만졌다" 와 "밀었다" 를 가른다 (2026-09-11, 사장님: "한번 스크롤한뒤에는 auto play 아예 안되는데") + * 전에는 레일 위에서 일어난 pointerdown·touchstart·wheel 이면 **곧바로** 영구 정지로 보냈다. + * 그런데 페이지를 세로로 내릴 때 손가락(과 마우스 커서)은 거의 항상 레일 위를 지난다 — + * 일정 카로셀은 화면 가운데를 가득 채우니까. 그래서 **한 번 스크롤하면 그 레일이 영영 멈췄다.** + * 실측: 세로 휠 한 번 → 20초 동안 0px. + * 이제 **레일이 실제로 가로로 움직였을 때만** 영구 정지다. 제스처 시작 때 위치를 적어 두고 + * 손을 뗀 뒤(관성 350ms) 다시 재서, 달라졌으면 손님이 민 것이다. + */ + function railPos(rail) { + var slid = 0; + var first = rail.firstElementChild; + if (first) { + var tf = window.getComputedStyle(first).transform; + if (tf && tf !== 'none') { + try { slid = new DOMMatrix(tf).m41; } catch (err) { slid = 0; } + } + } + return rail.scrollLeft + slid; + } + + var pending = null; // {rail, pos} — 지금 진행 중인 제스처 + + function markIfMoved() { + if (!pending) return; + var rail = pending.rail; + var was = pending.pos; + pending = null; + window.setTimeout(function () { + // 8px 여유 — 탭 한 번에도 embla 가 스냅을 다시 잡느라 소수점이 흔들린다. + if (Math.abs(railPos(rail) - was) < 8) return; + if (forever.indexOf(rail) < 0) { forever.push(rail); press(rail); } + }, 350); + } + + ['pointerdown', 'touchstart', 'touchmove', 'wheel', 'keydown', 'scroll'].forEach(function (type) { + window.addEventListener(type, function (event) { + if (!event.isTrusted) return; + quietUntil = Date.now() + COOLDOWN_MS; + if (event.touches) fingers = event.touches.length; + if (type === 'pointerdown' && event.pointerType === 'mouse') mouseDown = true; + if (type === 'scroll' || type === 'keydown' || type === 'touchmove') return; + collect(); + var rail = railOf(event.target); + if (!rail) return; + if (type === 'wheel') { + // 가로 휠·트랙패드 가로 스와이프만 "민 것"이다. 세로는 페이지를 내리는 중이다. + if (Math.abs(event.deltaX) > Math.abs(event.deltaY) && Math.abs(event.deltaX) > 2) { + pending = {rail: rail, pos: railPos(rail)}; + markIfMoved(); + } + return; + } + pending = {rail: rail, pos: railPos(rail)}; + }, true); + }); + + ['pointerup', 'pointercancel', 'touchend', 'touchcancel'].forEach(function (type) { + window.addEventListener(type, function (event) { + if (!event.isTrusted) return; + quietUntil = Date.now() + COOLDOWN_MS; + // 손가락 수는 touch 계열에서만 갱신한다 — pointercancel 은 손이 닿아 있어도 온다. + if (event.touches) fingers = event.touches.length; + if (event.pointerType === 'mouse') mouseDown = false; + if (fingers === 0 && !mouseDown) markIfMoved(); + }, true); + }); + + // 250ms 마다 조용해야 할 때인지 본다. 타이머를 직접 만지지 않고 훅의 정지 장치만 건드린다. + window.setInterval(function () { + var quiet = fingers > 0 || mouseDown || Date.now() < quietUntil; + if (quiet && !holding) holdAll(); + else if (!quiet && holding) releaseAll(); + }, 250); + } + + /* ★ `load` 를 기다리지 않는다. 이 페이지는 경로 지도 때문에 OSM 타일 가 151장이라 + (일정 20개 × 2~3일) load 는 그게 다 끝나야 나온다 — 타일이 느리면 영원히 안 뜬다. */ + console.log('[w4d] 주입분 로드'); + try { startPlayer(); } catch (err) { console.warn('[w4d] 플레이어 실패', err); } + try { tameRails(); console.log('[w4d] 카로셀 — 만지는 동안 정지(7초) · 직접 민 레일은 영구 정지'); } + catch (err) { console.warn('[w4d] 카로셀 실패', err); } + window.setTimeout(function () { + try { + watchLinks(); + console.log('[w4d] 링크 걸기 — 일정 · 엽서'); + } catch (err) { console.warn('[w4d] 링크 실패', err); } + try { + startCatchphrase(); + console.log('[w4d] 캐치프레이즈 ' + catchphrases().length + '개 · 7초마다 전환'); + } catch (err) { console.warn('[w4d] 캐치프레이즈 실패', err); } + }, 1200); +})(); diff --git a/solution/site/scripts/mockup/kingcheck.mjs b/solution/site/scripts/mockup/kingcheck.mjs new file mode 100644 index 0000000..6fc0c87 --- /dev/null +++ b/solution/site/scripts/mockup/kingcheck.mjs @@ -0,0 +1,20 @@ +import {chromium} from 'playwright'; +const b = await chromium.launch({channel: 'chrome'}); +const p = await b.newPage({viewport: {width: 1280, height: 900}}); +await p.goto(process.argv[2] || 'http://localhost/s/stay', {waitUntil: 'domcontentloaded'}); +await p.waitForTimeout(5500); +const r = await p.evaluate(() => { + const dl = [...document.querySelectorAll('dl')].find((d) => d.textContent.includes('픽업')); + if (!dl) return {err: 'dl 없음'}; + const rows = [...dl.children].map((el) => el.textContent.trim().replace(/\s+/g, ' ').slice(0, 24)); + const last = dl.lastElementChild; + const lastStyle = getComputedStyle(last); + const section = dl.closest('section'); + const after = section?.nextElementSibling; + return {rows: rows.slice(-4), lastBorderBottom: lastStyle.borderBottomWidth + ' ' + lastStyle.borderBottomStyle, + dlBorderBottom: getComputedStyle(dl).borderBottomWidth, + afterTag: after?.tagName, afterCls: (after?.className||'').toString().slice(0,60), + afterBorderTop: after ? getComputedStyle(after).borderTopWidth : null}; +}); +console.log(JSON.stringify(r, null, 1)); +await b.close(); diff --git a/solution/site/scripts/mockup/orig/index.html b/solution/site/scripts/mockup/orig/index.html new file mode 100644 index 0000000..23f609f --- /dev/null +++ b/solution/site/scripts/mockup/orig/index.html @@ -0,0 +1,88 @@ + + + + + + 스테이,머뭄 · 전북 군산시 숙소 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    목재 서까래 천장과 미닫이창, 테이블이 있는 거실 공간

    군산시

    스테이,머뭄

    히로쓰 가옥 담 너머, 백 년 된 집 한 채

    객실 보기
    소파와 탁자, 벽난로 조명이 있는 서까래 천장의 거실

    스테이,머뭄 소개

    스테이,머뭄은 전북특별자치도 군산시 신흥동 절골길에 있는 독채 펜션입니다. 1920년대에 지어진 백 년 고택으로, 국가등록문화유산인 군산 신흥동 일본식 가옥(히로쓰 가옥) 후문 바로 옆 — 걸어서 13m 거리입니다. 사장님은 이 집이 히로쓰 가옥 관리인이 살던 집이라고 전합니다. 2024년 5월에 리모델링을 마치고 문을 열었고, A동과 B동 두 동을 각각 한 팀만 씁니다. 기준 2인 · 최대 4인까지 묵을 수 있습니다.

    A동은 편안한 일본집에 와 있는 듯한 느낌의 공간이고, B동은 아늑하고 깔끔한 현대식 모던 스타일 공간입니다. 두 동 모두 독립된 아담한 정원과 창고형 CAFÉ 공간이 딸려 있어, 마당과 거실에서 새소리를 들으며 쉴 수 있습니다.

    인테리어와 가구, 소품, 침구를 하나하나 준비해 두었습니다. 특히 침구류는 매일 세탁·살균하고 건조해 쓰시는 분이 바뀔 때마다 새것처럼 나갑니다.

    예약 전 확인

    1920년대 적산가옥을 고쳐 지은 독채 두 동, 매일 세탁·살균하는 침구

    2026년 9월 9일 기준, 사업자가 확인한 정보입니다.

    주차
    가능
    숙소 소개
    1920년대에 지어진 백 년 고택을 리모델링한 독채 펜션입니다. 히로쓰 가옥 후문 바로 옆이고, 침구는 매일 세탁·살균·건조합니다.
    와이파이
    가능
    체크아웃 시간
    11:00
    취소·환불 규정
    입실 당일 취소와 노쇼(No Show)는 숙박요금의 100%가 부과되어 환불되지 않습니다. 그 이전 취소 수수료는 예약하신 채널의 취소 규정을 따릅니다.
    취사
    가능

    객실 안내

      • 목재 서까래 천장과 미닫이창, 테이블이 있는 거실 공간
      • 소파와 탁자, 벽난로 조명이 있는 서까래 천장의 거실
      • 창밖으로 조명을 비춘 정원 나무와 석등이 보이는 풍경
      • 목재 서까래 천장에 설치된 조형적인 디자인의 조명 기구
      1 / 4

      A동(일본식 가정집 느낌으로 디딤돌마당과 예쁜정원)

      • 기준 인원2명
      • 최대 인원4명

      편안한 일본집에 와 있는 듯한 느낌의 공간입니다. 독립된 아담한 정원과 창고형 CAFÉ 공간이 있습니다.

      A동(일본식 가정집 느낌으로 디딤돌마당과 예쁜정원) 자세히
      객실 타입
      독채
      기준 인원
      2명
      최대 인원
      4명
      객실 소개
      편안한 일본집에 와 있는 듯한 느낌의 공간입니다. 독립된 아담한 정원과 창고형 CAFÉ 공간이 있습니다.
      • 벽면에 설치된 그림 화면과 벽난로 조명 장식
      • 실내 벽면 홈 공간에 놓인 흰색 화병과 식물 장식
      • 실내 창문을 통해 바라본 푸른 하늘과 푸른 나무 풍경
      • 싱크대와 냉장고, 전자레인지가 구비된 실내 주방 공간
      1 / 4

      B동(모던한 현대식 컨셉으로 꾸며진 따뜻한 공간)

      • 기준 인원2명
      • 최대 인원4명

      아늑하고 깔끔한 현대식 모던 스타일 공간입니다. 독립된 아담한 정원과 창고형 CAFÉ 공간이 있습니다.

      B동(모던한 현대식 컨셉으로 꾸며진 따뜻한 공간) 자세히
      객실 타입
      독채
      기준 인원
      2명
      최대 인원
      4명
      객실 소개
      아늑하고 깔끔한 현대식 모던 스타일 공간입니다. 독립된 아담한 정원과 창고형 CAFÉ 공간이 있습니다.

    스테이,머뭄 소식

    인스타그램에서 보기 →

    미리 알아 두시면 좋은 것들

    • 공지

      입실 전 확인해 주세요

      머무시기 전에 꼭 읽어 주셔야 하는 것들입니다. 미숙지로 생긴 불편과 손해는 도와드리기 어렵습니다.

      · 반려동물은 입실하실 수 없습니다. +· 건물과 객실 안은 절대 금연입니다. 바로 옆이 군산 신흥동 일본식가옥(히로쓰 가옥)이라 화재에 각별히 유의해 주세요. +· 냄새가 잘 빠지지 않는 생선구이·고기구이·튀김류 조리는 하실 수 없습니다. +· 객실 안에서 음식을 드실 때 오염에 주의해 주세요. +· 객실 시설과 비품, 침구가 손상·파손·오염되면 변상을 요청드릴 수 있습니다. +· 정기적으로 방역하고 있으나 자연과 함께하는 공간이라 가끔 벌레나 이물질이 들어올 수 있습니다. 이 사유로는 환불이 어려운 점 양해 부탁드립니다. +· 귀중품과 현금은 분실 위험이 있으니 직접 관리해 주세요. +· 안전과 보안을 위해 대문 입구 쪽에 CCTV를 운영하고 있습니다. 객실 안에는 없습니다. +· 숙박업소는 법적으로 청소년 혼숙이 금지되어 있습니다. 미성년자는 보호자를 동반하셔야 이용하실 수 있습니다.

      자세히 보기 →

    • 공지

      인원이 늘면 미리 연락 주세요

      예약하신 인원보다 늘어나는 경우, 오시기 전에 숙소로 연락 부탁드립니다.

      연락처 0507-1497-0983

      · 기준 인원(2인)을 넘으면 1인당 20,000원이 추가됩니다. +· 최대 인원(4인)을 넘으면 입실이 불가능할 수 있고, 이 사유로는 환불을 받으실 수 없습니다. +· 다음 이용 고객을 위해 입실 15:00 이후, 퇴실 11:00 이전을 지켜 주세요.

      자세히 보기 →

    • 공지

      직화·개인 취사도구는 반입이 안 됩니다

      백 년 된 목조 고택이라 화재 예방 규정이 엄격합니다.

      · 객실 안에서 생선이나 고기 등을 굽는 직화 방식은 허용되지 않습니다. +· 그릴, 숯, 전기·전열기구 등 개인적으로 준비해 오시는 취사도구는 반입이 금지되어 있습니다. +· 주방에서 간단한 조리는 하실 수 있습니다. +· 흡연은 지정된 장소를 이용해 주시고, 다른 이용객에게 피해가 갈 수 있는 오락·음주·고성방가는 삼가 주세요.

      자세히 보기 →

    자세한 내용과 최신 소식은 원문에서 확인해 주세요.

    이용안내 및 예약

    방문 전 확인이 필요한 운영 규정과 시설 안내입니다.

    예약 전 확인

    체크아웃 시간
    11:00
    취소·환불 규정
    입실 당일 취소와 노쇼(No Show)는 숙박요금의 100%가 부과되어 환불되지 않습니다. 그 이전 취소 수수료는 예약하신 채널의 취소 규정을 따릅니다.
    반려동물 동반
    불가
    인원 추가 요금
    20,000원
    체크인 시간
    15:00

    시설 · 편의

    주차
    가능
    숙소 소개
    1920년대에 지어진 백 년 고택을 리모델링한 독채 펜션입니다. 히로쓰 가옥 후문 바로 옆이고, 침구는 매일 세탁·살균·건조합니다.
    와이파이
    가능
    취사
    가능
    흡연
    불가
    픽업 서비스
    불가
    바비큐 이용
    불가

    스테이,머뭄 예약은 아래로 받습니다.

    확인 중인 항목 3개는 표시하지 않았습니다. 필요하시면 전화로 문의해 주세요.2026년 9월 9일 기준

    ADO2 영상 보기

    ADO2 알아보기 →

    ADO2로 만든 홍보 영상입니다.

    • 머뭄의 하루
    • 마당과 창고형 카페 공간
    • 밤의 머뭄

    오시는 길

    주소와 주요 거점까지의 이동 시간을 안내합니다.

    주소

    전북 군산시 절골길 18

    문의: 0507-1497-0983

    계절별 축제

    군산시의 축제와 행사를 계절로 묶었습니다.

    가을

    한국관광공사 TourAPI 기준. 일정은 주최 측 사정으로 바뀔 수 있습니다.

    주변 안내

    군산시 지역의 맛집 · 명소 안내입니다.

    2026년 9월 9일 갱신

    주변 맛집

    주변 명소

    도보 시간은 숙소에서 잰 직선거리를 분속 80m 로 환산한 값입니다. 실제로 걷는 길은 이보다 깁니다.

    추천 일정

    며칠 묵느냐에 따라 다르게 돕니다

    첫째 날17:00–20:58 · 3시간 58분

    섬까지 다녀오는 1박 2일

    차를 가져온 손님

    첫날은 도심에서 저녁을, 이튿날은 다리 건너 바다를 봅니다.

    1234© OpenStreetMap
    1. 17:00
      체크인 · 스테이 머뭄 사진1체크인 · 스테이 머뭄짐을 풀고 골목을 한 바퀴 돕니다.

      17:00–17:40

    2. 17:52

      ↓ 12분 이동

      저녁 · 빈해원 사진2저녁 · 빈해원옛 중식당 건물 자체가 문화재입니다.

      17:52–19:02 · 지도 검색 군산 빈해원

    3. 19:08

      ↓ 6분 이동

      밤 산책 · 군산 내항 사진3밤 산책 · 군산 내항조명이 들어온 부잔교를 봅니다.

      19:08–19:58 · 지도 검색 군산 내항

    4. 19:58
      머뭄으로 · 스테이 머뭄 사진4머뭄으로 · 스테이 머뭄마당 평상에 앉으면 항구 소리가 들립니다.

      19:58–20:58

    둘째 날10:00–14:27 · 4시간 27분

    섬까지 다녀오는 1박 2일

    12© OpenStreetMap
    1. 10:45

      ↓ 45분 이동

      고군산군도 선유도 사진1고군산군도 선유도다리로 건너갑니다. 차로 이동.

      10:45–13:15 · 지도 검색 군산 선유도

    2. 13:27

      ↓ 12분 이동

      장자도 사진2장자도섬과 섬 사이를 걸어 넘습니다.

      13:27–14:27 · 지도 검색 군산 장자도

    첫째 날15:00–18:05 · 3시간 5분

    걸어서만 도는 1박 2일

    차 없이 오는 손님

    숙소에서 1km 안으로만 움직입니다. 차를 두고 와도 됩니다.

    1234© OpenStreetMap
    1. 15:00
      체크인 · 스테이 머뭄 사진1체크인 · 스테이 머뭄짐을 풀고 골목을 한 바퀴 돕니다.

      15:00–15:40

    2. 15:46

      ↓ 6분 이동

      초원사진관 사진2초원사진관숙소에서 걸어 3분입니다.

      15:46–16:16 · 지도 검색 초원사진관

    3. 16:25

      ↓ 9분 이동

      3이성당단팥빵을 사서 마당에서 먹어도 됩니다.

      16:25–17:05 · 지도 검색 군산 이성당

    4. 17:05
      머뭄으로 · 스테이 머뭄 사진4머뭄으로 · 스테이 머뭄마당 평상에 앉으면 항구 소리가 들립니다.

      17:05–18:05

    둘째 날09:30–12:31 · 3시간 1분

    걸어서만 도는 1박 2일

    123© OpenStreetMap
    1. 09:38

      ↓ 8분 이동

      군산근대역사박물관 사진1군산근대역사박물관항구 도시가 어떻게 만들어졌는지 먼저 봅니다.

      09:38–11:08 · 지도 검색 군산근대역사박물관

    2. 11:12

      ↓ 4분 이동

      군산근대건축관 사진2군산근대건축관옛 조선은행 건물입니다. 길 건너 바로.

      11:12–11:57 · 지도 검색 군산근대건축관

    3. 12:06

      ↓ 9분 이동

      3해망굴1926년에 뚫린 굴입니다. 반대편이 바로 항구입니다.

      12:06–12:31 · 지도 검색 군산 해망굴

    첫째 날15:00–19:15 · 4시간 15분

    비가 와도 되는 1박 2일

    비 예보를 보고 오는 손님

    실내에서 실내로 이어집니다. 우산 하나로 하루가 끝납니다.

    1234© OpenStreetMap
    1. 15:00
      체크인 · 스테이 머뭄 사진1체크인 · 스테이 머뭄짐을 풀고 골목을 한 바퀴 돕니다.

      15:00–15:40

    2. 15:48

      ↓ 8분 이동

      군산근대역사박물관 사진2군산근대역사박물관항구 도시가 어떻게 만들어졌는지 먼저 봅니다.

      15:48–17:18 · 지도 검색 군산근대역사박물관

    3. 17:25

      ↓ 7분 이동

      군산근대미술관 사진3군산근대미술관걸어서 3분, 처마를 따라 갑니다.

      17:25–18:15 · 지도 검색 군산근대미술관

    4. 18:15
      머뭄으로 · 스테이 머뭄 사진4머뭄으로 · 스테이 머뭄마당 평상에 앉으면 항구 소리가 들립니다.

      18:15–19:15

    둘째 날10:00–12:31 · 2시간 31분

    비가 와도 되는 1박 2일

    12© OpenStreetMap
    1. 10:08

      ↓ 8분 이동

      군산근대건축관 사진1군산근대건축관옛 조선은행 건물입니다. 길 건너 바로.

      10:08–10:53 · 지도 검색 군산근대건축관

    2. 11:01

      ↓ 8분 이동

      2째보선창 근처 카페비가 그칠 때까지 앉아 있어도 됩니다.

      11:01–12:31 · 지도 검색 군산 째보선창

    첫째 날15:00–19:42 · 4시간 42분

    근대건축만 보는 1박 2일

    건물을 보러 오는 손님

    개항기 건물 넷을 하루에 몰아 봅니다.

    12345© OpenStreetMap
    1. 15:00
      체크인 · 스테이 머뭄 사진1체크인 · 스테이 머뭄짐을 풀고 골목을 한 바퀴 돕니다.

      15:00–15:40

    2. 15:50

      ↓ 10분 이동

      구 군산세관 본관 사진2구 군산세관 본관소설의 세관이 그대로 서 있습니다.

      15:50–16:30 · 지도 검색 구 군산세관

    3. 16:35

      ↓ 5분 이동

      군산근대건축관 사진3군산근대건축관옛 조선은행 건물입니다. 길 건너 바로.

      16:35–17:20 · 지도 검색 군산근대건축관

    4. 17:32

      ↓ 12분 이동

      저녁 · 빈해원 사진4저녁 · 빈해원옛 중식당 건물 자체가 문화재입니다.

      17:32–18:42 · 지도 검색 군산 빈해원

    5. 18:42
      머뭄으로 · 스테이 머뭄 사진5머뭄으로 · 스테이 머뭄마당 평상에 앉으면 항구 소리가 들립니다.

      18:42–19:42

    둘째 날09:30–12:36 · 3시간 6분

    근대건축만 보는 1박 2일

    123© OpenStreetMap
    1. 09:38

      ↓ 8분 이동

      군산근대역사박물관 사진1군산근대역사박물관항구 도시가 어떻게 만들어졌는지 먼저 봅니다.

      09:38–11:08 · 지도 검색 군산근대역사박물관

    2. 11:12

      ↓ 4분 이동

      군산근대미술관 사진2군산근대미술관걸어서 3분, 처마를 따라 갑니다.

      11:12–12:02 · 지도 검색 군산근대미술관

    3. 12:11

      ↓ 9분 이동

      3해망굴1926년에 뚫린 굴입니다. 반대편이 바로 항구입니다.

      12:11–12:36 · 지도 검색 군산 해망굴

    첫째 날16:00–20:24 · 4시간 24분

    이성당으로 시작하는 1박 2일

    빵을 좋아하는 손님

    아침 첫 자리를 이성당에 둡니다. 줄이 짧을 때 갑니다.

    12345© OpenStreetMap
    1. 16:00
      체크인 · 스테이 머뭄 사진1체크인 · 스테이 머뭄짐을 풀고 골목을 한 바퀴 돕니다.

      16:00–16:40

    2. 16:46

      ↓ 6분 이동

      초원사진관 사진2초원사진관숙소에서 걸어 3분입니다.

      16:46–17:16 · 지도 검색 초원사진관

    3. 17:24

      ↓ 8분 이동

      3한일옥무국으로 아침을 엽니다.

      17:24–18:24 · 지도 검색 군산 한일옥

    4. 18:34

      ↓ 10분 이동

      밤 산책 · 군산 내항 사진4밤 산책 · 군산 내항조명이 들어온 부잔교를 봅니다.

      18:34–19:24 · 지도 검색 군산 내항

    5. 19:24
      머뭄으로 · 스테이 머뭄 사진5머뭄으로 · 스테이 머뭄마당 평상에 앉으면 항구 소리가 들립니다.

      19:24–20:24

    둘째 날08:30–11:47 · 3시간 17분

    이성당으로 시작하는 1박 2일

    123© OpenStreetMap
    1. 08:42

      ↓ 12분 이동

      1이성당단팥빵을 사서 마당에서 먹어도 됩니다.

      08:42–09:22 · 지도 검색 군산 이성당

    2. 09:28

      ↓ 6분 이동

      군산근대역사박물관 사진2군산근대역사박물관항구 도시가 어떻게 만들어졌는지 먼저 봅니다.

      09:28–10:58 · 지도 검색 군산근대역사박물관

    3. 11:07

      ↓ 9분 이동

      3째보선창소설 속 노동의 자리입니다.

      11:07–11:47 · 지도 검색 군산 째보선창

    첫째 날15:30–20:02 · 4시간 32분

    철길마을까지 가는 1박 2일

    사진을 찍는 손님

    골목과 철길, 두 가지 배경을 하루씩 나눠 씁니다.

    12345© OpenStreetMap
    1. 15:30
      체크인 · 스테이 머뭄 사진1체크인 · 스테이 머뭄짐을 풀고 골목을 한 바퀴 돕니다.

      15:30–16:10

    2. 16:16

      ↓ 6분 이동

      초원사진관 사진2초원사진관숙소에서 걸어 3분입니다.

      16:16–16:46 · 지도 검색 초원사진관

    3. 16:53

      ↓ 7분 이동

      군산근대미술관 사진3군산근대미술관걸어서 3분, 처마를 따라 갑니다.

      16:53–17:43 · 지도 검색 군산근대미술관

    4. 17:52

      ↓ 9분 이동

      저녁 · 빈해원 사진4저녁 · 빈해원옛 중식당 건물 자체가 문화재입니다.

      17:52–19:02 · 지도 검색 군산 빈해원

    5. 19:02
      머뭄으로 · 스테이 머뭄 사진5머뭄으로 · 스테이 머뭄마당 평상에 앉으면 항구 소리가 들립니다.

      19:02–20:02

    둘째 날10:00–12:12 · 2시간 12분

    철길마을까지 가는 1박 2일

    12© OpenStreetMap
    1. 10:18

      ↓ 18분 이동

      경암동 철길마을 사진1경암동 철길마을해 질 무렵 빛이 가장 좋습니다.

      10:18–11:18 · 지도 검색 군산 경암동 철길마을

    2. 11:32

      ↓ 14분 이동

      2이성당단팥빵을 사서 마당에서 먹어도 됩니다.

      11:32–12:12 · 지도 검색 군산 이성당

    첫째 날16:00–20:50 · 4시간 50분

    항구만 도는 1박 2일

    바다가 보고 싶은 손님

    내항과 부잔교, 째보선창을 이어 걷습니다.

    12345© OpenStreetMap
    1. 16:00
      체크인 · 스테이 머뭄 사진1체크인 · 스테이 머뭄짐을 풀고 골목을 한 바퀴 돕니다.

      16:00–16:40

    2. 16:51

      ↓ 11분 이동

      군산 내항 부잔교 사진2군산 내항 부잔교쌀이 실려 나가던 다리입니다.

      16:51–17:41 · 지도 검색 군산 내항 부잔교

    3. 17:44

      ↓ 3분 이동

      밤 산책 · 군산 내항 사진3밤 산책 · 군산 내항조명이 들어온 부잔교를 봅니다.

      17:44–18:34 · 지도 검색 군산 내항

    4. 18:40

      ↓ 6분 이동

      저녁 · 빈해원 사진4저녁 · 빈해원옛 중식당 건물 자체가 문화재입니다.

      18:40–19:50 · 지도 검색 군산 빈해원

    5. 19:50
      머뭄으로 · 스테이 머뭄 사진5머뭄으로 · 스테이 머뭄마당 평상에 앉으면 항구 소리가 들립니다.

      19:50–20:50

    둘째 날10:00–12:59 · 2시간 59분

    항구만 도는 1박 2일

    123© OpenStreetMap
    1. 10:12

      ↓ 12분 이동

      1째보선창소설 속 노동의 자리입니다.

      10:12–10:52 · 지도 검색 군산 째보선창

    2. 10:54

      ↓ 2분 이동

      2째보선창 근처 카페비가 그칠 때까지 앉아 있어도 됩니다.

      10:54–12:24 · 지도 검색 군산 째보선창

    3. 12:34

      ↓ 10분 이동

      3해망굴1926년에 뚫린 굴입니다. 반대편이 바로 항구입니다.

      12:34–12:59 · 지도 검색 군산 해망굴

    첫째 날15:00–19:20 · 4시간 20분

    소설 『탁류』를 따라 걷는 1박 2일

    채만식을 읽어 본 손님

    소설의 무대인 내항과 채만식문학관을 잇습니다.

    1234© OpenStreetMap
    1. 15:00
      체크인 · 스테이 머뭄 사진1체크인 · 스테이 머뭄짐을 풀고 골목을 한 바퀴 돕니다.

      15:00–15:40

    2. 15:56

      ↓ 16분 이동

      채만식문학관 사진2채만식문학관금강 하구 옆입니다. 차로 이동.

      15:56–17:16 · 지도 검색 군산 채만식문학관

    3. 17:30

      ↓ 14분 이동

      군산 내항 부잔교 사진3군산 내항 부잔교쌀이 실려 나가던 다리입니다.

      17:30–18:20 · 지도 검색 군산 내항 부잔교

    4. 18:20
      머뭄으로 · 스테이 머뭄 사진4머뭄으로 · 스테이 머뭄마당 평상에 앉으면 항구 소리가 들립니다.

      18:20–19:20

    둘째 날09:30–12:44 · 3시간 14분

    소설 『탁류』를 따라 걷는 1박 2일

    123© OpenStreetMap
    1. 09:38

      ↓ 8분 이동

      군산근대역사박물관 사진1군산근대역사박물관항구 도시가 어떻게 만들어졌는지 먼저 봅니다.

      09:38–11:08 · 지도 검색 군산근대역사박물관

    2. 11:15

      ↓ 7분 이동

      2째보선창소설 속 노동의 자리입니다.

      11:15–11:55 · 지도 검색 군산 째보선창

    3. 12:04

      ↓ 9분 이동

      3이성당단팥빵을 사서 마당에서 먹어도 됩니다.

      12:04–12:44 · 지도 검색 군산 이성당

    첫째 날15:30–18:35 · 3시간 5분

    아이와 함께 1박 2일

    아이를 데려온 손님

    걷는 거리를 짧게 끊고 군것질할 자리를 사이에 둡니다.

    1234© OpenStreetMap
    1. 15:30
      체크인 · 스테이 머뭄 사진1체크인 · 스테이 머뭄짐을 풀고 골목을 한 바퀴 돕니다.

      15:30–16:10

    2. 16:16

      ↓ 6분 이동

      초원사진관 사진2초원사진관숙소에서 걸어 3분입니다.

      16:16–16:46 · 지도 검색 초원사진관

    3. 16:55

      ↓ 9분 이동

      3이성당단팥빵을 사서 마당에서 먹어도 됩니다.

      16:55–17:35 · 지도 검색 군산 이성당

    4. 17:35
      머뭄으로 · 스테이 머뭄 사진4머뭄으로 · 스테이 머뭄마당 평상에 앉으면 항구 소리가 들립니다.

      17:35–18:35

    둘째 날10:00–13:03 · 3시간 3분

    아이와 함께 1박 2일

    12© OpenStreetMap
    1. 10:18

      ↓ 18분 이동

      경암동 철길마을 사진1경암동 철길마을해 질 무렵 빛이 가장 좋습니다.

      10:18–11:18 · 지도 검색 군산 경암동 철길마을

    2. 11:33

      ↓ 15분 이동

      군산근대역사박물관 사진2군산근대역사박물관항구 도시가 어떻게 만들어졌는지 먼저 봅니다.

      11:33–13:03 · 지도 검색 군산근대역사박물관

    첫째 날19:00–21:00 · 2시간

    늦게 도착하는 1박 2일

    퇴근하고 오는 손님

    첫날은 저녁과 밤 산책만, 이튿날에 몰아 봅니다.

    12© OpenStreetMap
    1. 19:00
      체크인 · 스테이 머뭄 사진1체크인 · 스테이 머뭄짐을 풀고 골목을 한 바퀴 돕니다.

      19:00–19:40

    2. 19:50

      ↓ 10분 이동

      저녁 · 빈해원 사진2저녁 · 빈해원옛 중식당 건물 자체가 문화재입니다.

      19:50–21:00 · 지도 검색 군산 빈해원

    둘째 날09:30–12:51 · 3시간 21분

    늦게 도착하는 1박 2일

    1234© OpenStreetMap
    1. 09:38

      ↓ 8분 이동

      군산근대건축관 사진1군산근대건축관옛 조선은행 건물입니다. 길 건너 바로.

      09:38–10:23 · 지도 검색 군산근대건축관

    2. 10:27

      ↓ 4분 이동

      구 군산세관 본관 사진2구 군산세관 본관소설의 세관이 그대로 서 있습니다.

      10:27–11:07 · 지도 검색 구 군산세관

    3. 11:13

      ↓ 6분 이동

      군산근대미술관 사진3군산근대미술관걸어서 3분, 처마를 따라 갑니다.

      11:13–12:03 · 지도 검색 군산근대미술관

    4. 12:11

      ↓ 8분 이동

      4이성당단팥빵을 사서 마당에서 먹어도 됩니다.

      12:11–12:51 · 지도 검색 군산 이성당

    시각은 출발 시각과 머무는 시간으로 계산한 것입니다

    군산 이야기

    이 도시를 네 갈래로 봅니다. 하나씩 골라 보세요.

    가요 다방

    이 도시를 노래한 곡들

    A면 · 1 / 4

    금강 나그네

    이미자 · 1966 · 작사 이병훈 / 작곡 박춘석 · 지구레코드

    군산항을 모티브로 한 곡. 금강 하구와 항구의 이별 정서를 담았습니다.

    머뭄에서 걸어 10분, 내항 부잔교가 이 노래의 배경입니다.

    유튜브에서 듣기◎ 가사 대신 이야기 — 원문은 싣지 않습니다

    출처 · 세계일보

    총 4곡

    자주 묻는 질문

    아래 답변은 모두 사업자가 확인한 내용입니다.

    Q.체크인과 체크아웃 시간은 언제인가요?+
    체크인은 15:00부터이며, 체크아웃은 11:00까지입니다.
    Q.객실 기준 인원과 최대 인원, 인원 추가 요금은 어떻게 되나요?+
    A동과 B동 모두 기준 인원은 2명이며, 최대 4명까지 투숙하실 수 있습니다. 인원 추가 요금은 20,000원입니다.
    Q.객실 내 취사가 가능한가요?+
    주방에서 간단한 조리는 가능합니다. 다만 생선구이·고기구이·튀김처럼 냄새가 잘 빠지지 않는 조리는 하실 수 없고, 객실 안에서 직화로 굽는 방식도 허용되지 않습니다. 그릴·숯·전기 전열기구 등 개인 취사도구는 반입이 금지되어 있습니다.
    Q.주차와 와이파이 이용이 가능한가요?+
    네, 주차와 와이파이 모두 이용 가능합니다.
    Q.반려동물 동반이나 실내 흡연이 가능한가요?+
    반려동물 동반은 불가하며, 전 구역 흡연이 불가능합니다.
    Q.A동과 B동은 무엇이 다른가요?+
    A동은 편안한 일본집에 와 있는 듯한 느낌의 공간이고, B동은 아늑하고 깔끔한 현대식 모던 스타일 공간입니다. 두 동 모두 독립된 아담한 정원과 창고형 CAFÉ 공간이 딸려 있으며, 기준 2인 · 최대 4인으로 같습니다. 침대 구성 등 객실 안 자세한 사항은 예약 채널이나 전화로 확인해 주세요.
    Q.객실 요금은 얼마인가요?+
    A동과 B동 모두 198,000원 ~ 350,000원입니다. 날짜와 시즌에 따라 달라지므로 정확한 금액은 예약 채널에서 확인해 주세요. 기준 인원(2인)을 넘으면 1인당 20,000원이 더해집니다.
    Q.예약 취소 및 환불 규정은 어떻게 되나요?+
    입실 당일 취소와 노쇼(No Show)의 경우 숙박요금의 100%가 부과되어 환불되지 않습니다. 그 이전 취소 수수료는 예약하신 채널의 취소 규정을 따릅니다.
    Q.스테이,머뭄은 어떤 숙소인가요?+
    스테이,머뭄은 전북 군산시 절골길 18(신흥동)에 있는 독채 숙소입니다. A동은 일본식 가정집 느낌, B동은 현대식 모던 스타일로 두 동이 있고, 편안한 침구와 좋은 위치를 내세운 곳입니다.
    Q.군산 독채 펜션을 찾는데, 여기도 독채인가요?+
    네, 스테이,머뭄은 A동과 B동 모두 독채입니다. 동마다 독립된 정원과 창고형 카페 공간이 딸려 있고, 주방이 있어 취사도 가능합니다.
    Q.위치가 어디인가요?+
    스테이,머뭄은 전북 군산시 절골길 18(신흥동 63-18)에 있습니다. 바로 옆 13m 거리에 군산 신흥동 일본식가옥(히로쓰 가옥)이 있는 원도심 골목 안입니다. 문의는 0507-1497-0983입니다.
    Q.예약은 어디서 하나요?+
    스테이,머뭄은 네이버 플레이스에서 숙소 정보를 확인하실 수 있고, 전화 0507-1497-0983으로도 문의하실 수 있습니다. 입실 당일 취소와 노쇼는 숙박요금의 100%가 부과되니 일정을 확인하고 예약해 주세요.
    Q.커플 둘이 묵기 좋은 2인 독채인가요?+
    네, 스테이,머뭄 A동과 B동 모두 기준 인원 2인의 독채입니다. 두 동을 각각 한 팀만 쓰고, 독립된 아담한 정원과 창고형 CAFÉ 공간이 딸려 있습니다.
    Q.일본식 가옥 같은 숙소를 찾는데 여기가 맞나요?+
    스테이,머뭄 A동이 일본식 가정집 느낌으로 꾸민 독채입니다. 디딤돌 마당과 독립된 정원, 창고형 카페 공간이 딸려 있습니다. B동은 같은 마당 안에 있는 현대식 모던 스타일 독채입니다.
    Q.조용하고 프라이빗한 숙소인가요?+
    스테이,머뭄은 A동·B동 두 동뿐인 독채 숙소라 다른 손님과 건물을 나눠 쓰지 않습니다. 두 동 모두 독립된 정원을 따로 두고 있고, 전 구역 금연입니다.
    Q.차 없이 걸어서 근대문화거리까지 갈 수 있나요?+
    스테이,머뭄에서 히로쓰 가옥은 13m, 초원사진관 248m, 군산 영화의 거리 318m, 군산근대미술관 708m, 옛 군산세관 728m, 근대건축관(구 조선은행 군산지점) 788m 거리입니다. 원도심 근대 건축물이 숙소를 중심으로 1km 안에 모여 있습니다. 주차도 가능합니다.
    Q.이성당이나 근처 맛집까지 얼마나 되나요?+
    스테이,머뭄에서 이성당까지는 455m입니다. 그 밖에 요리주점 도란 135m, 베이커리 물밀소 297m, 백년가게 국밥집 일흥옥 300m, 명월갈비 351m, 꽃게장 한주옥 430m로 모두 가까이 있습니다.
    Q.마당이나 정원이 따로 있나요?+
    네. A동과 B동 각각 독립된 아담한 정원과 창고형 CAFÉ 공간이 있습니다. A동에는 디딤돌 마당도 있습니다. 바다 전망은 아닙니다 — 마당과 거실에서 새소리를 들으며 쉬는 숙소입니다.
    Q.선유도에 갈 건데 여기서 자도 될까요?+
    네, 스테이,머뭄에서는 다리를 건너 고군산군도 선유도까지 차로 약 45분 걸립니다. 첫날은 군산 도심에서 저녁과 내항 밤 산책을, 이튿날 선유도와 장자도를 도는 1박 2일 코스를 추천 일정으로 안내하고 있습니다.
    Q.1박 2일이면 어떻게 돌면 좋을까요?+
    스테이,머뭄이 안내하는 1박 2일 코스는 첫날 오후 체크인 후 빈해원에서 저녁, 군산 내항 밤 산책으로 마무리하고, 이튿날 선유도와 장자도를 도는 일정입니다. 걷는 코스를 원하시면 채만식문학관·내항 부잔교·째보선창·초원사진관·이성당으로 이어지는 반나절 탁류길 코스도 있습니다.
    Q.군산이 처음인데 어디부터 보면 좋을까요?+
    스테이,머뭄은 처음 오신 손님께 반나절 개항장 골목 코스를 안내합니다. 군산근대역사박물관에서 시작해 초원사진관, 이성당, 경암동 철길마을 순으로 도는 일정으로, 걷는 거리가 가장 짧은 코스입니다.
    Q.비 오는 날에는 뭘 하면 좋을까요?+
    스테이,머뭄은 비 오는 날을 위한 실내 코스를 따로 안내합니다. 군산근대건축관에서 옛 은행 건물 전시를 보고, 한일옥에서 무국으로 몸을 데운 뒤, 째보선창 근처 카페에서 비가 그칠 때까지 앉아 있는 코스입니다. 숙소는 주방이 있어 취사도 가능합니다.
    Q.바비큐나 고기를 구워 먹을 수 있나요?+
    바비큐는 하실 수 없습니다. 화재 예방을 위해 객실 안에서 생선이나 고기를 굽는 직화 방식은 허용되지 않으며, 그릴·숯·전기 전열기구 등 개인적으로 준비해 오시는 취사도구도 반입이 금지되어 있습니다.
    Q.픽업 서비스가 있나요?+
    픽업 서비스는 지원하지 않습니다.
    Q.건물이 오래된 집이라던데 언제 지어졌나요?+
    1920년대에 지어진 적산가옥입니다. 백 년 된 고택을 리모델링해 2024년 5월 독채 펜션으로 문을 열었습니다. 바로 옆이 국가등록문화유산인 군산 신흥동 일본식 가옥(히로쓰 가옥)입니다.
    Q.침구는 깨끗한가요?+
    침구류는 매일 세탁하고 살균·건조합니다. 인테리어와 가구, 소품도 하나하나 준비해 두었습니다.
    Q.미성년자끼리 이용할 수 있나요?+
    숙박업소는 법적으로 청소년 혼숙이 금지되어 있습니다. 미성년자는 보호자를 동반하셔야 이용하실 수 있습니다.
    Q.CCTV가 설치되어 있나요?+
    고객님의 안전과 보안을 위해 대문 입구 쪽에 CCTV를 운영하고 있습니다. 객실 안에는 없습니다.
    Q.벌레가 나오지는 않나요?+
    정기적으로 방역하고 있습니다. 다만 자연과 함께하는 공간이라 가끔 벌레나 이물질이 들어올 수 있으며, 이 사유로는 환불이 어려운 점 양해 부탁드립니다.
    Q.예약 인원보다 사람이 늘면 어떻게 하나요?+
    미리 숙소로 연락 주세요. 기준 인원(2인)을 넘으면 1인당 20,000원이 추가됩니다. 최대 인원(4인)을 넘으면 입실이 불가능할 수 있고, 그 사유로는 환불을 받으실 수 없습니다.
    Q.히로쓰 가옥 바로 옆이라던데 어떤 집인가요?+
    스테이,머뭄은 1920년대에 지어진 집으로, 국가등록문화유산인 군산 신흥동 일본식 가옥(히로쓰 가옥) 후문 바로 옆 13m 거리에 있습니다. 사장님은 이 집이 히로쓰 가옥 관리인이 살던 집이라고 전합니다. 백 년 된 고택을 리모델링해 2024년 5월 독채 펜션으로 문을 열었습니다.
    Q.주요 관광지까지 걸어서 얼마나 걸리나요?+
    초원사진관, 이성당, 군산 시간여행마을 등 주요 관광명소가 걸어서 최대 8분 거리입니다. 차 없이 도보 여행이 가능합니다.

    오늘의 날씨

    28°C구름많음

    9월 3일 12:30 관측

    빛이 낮게 깔리는 날입니다. 창고형 CAFÉ 공간이 이런 날 가장 아늑합니다.

    선유도까지 다녀오기 좋은 기온입니다. 골목은 오후 늦게 도세요 — 경암동 철길마을은 해 질 때가 사람이 적습니다.

    + + + diff --git a/solution/site/scripts/mockup/patch_stay.py b/solution/site/scripts/mockup/patch_stay.py new file mode 100644 index 0000000..28079aa --- /dev/null +++ b/solution/site/scripts/mockup/patch_stay.py @@ -0,0 +1,294 @@ +"""/s/stay 목업 index.html 을 다시 만든다 — payload 교체 + 주입분(css·js) 삽입. + +★ 이 사이트는 payload 가 없다(목업). 프리렌더 재굽기 대상이 아니라서, 고칠 자리는 + 구워진 index.html 하나다. 손으로 열어 고치지 않고 이 스크립트로 만든다 — + 두 번째로 고칠 때 무엇을 바꿨는지가 파일 대신 여기 남아야 한다. + +입력: stay-orig/index.html (볼륨에서 꺼낸 원본) · stay-payload-new.json · inject.css · inject.js +출력: stay-work/index.html +""" +import json +import re +import uuid +from pathlib import Path + +SP = Path(__file__).parent + +# ── 캐치프레이즈 100개 ──────────────────────────────────────────────────────── +# 사실이 아니라 문구다(출처가 붙는 값이 아니다). 근거는 payload 의 시설·위치 값 — +# 적산가옥 두 동 · 히로쓰 가옥 옆 · 마당과 정원 · 창고형 카페 · 매일 세탁하는 침구 · 최대 4인. +GENERAL = [ + "백 년 된 집에서 하룻밤", + "담 너머는 히로쓰 가옥입니다", + "독채 두 동, 마당은 각자 씁니다", + "기와 아래에서 아침을 맞습니다", + "새소리로 눈이 떠지는 집", + "골목이 조용해지는 시간에 도착하세요", + "침구는 매일 새것처럼 나갑니다", + "마당 평상에 앉으면 항구 소리가 들립니다", + "1920년대에 지은 집, 지금 쓰는 집", + "대문을 닫으면 골목이 멀어집니다", + "창고를 고친 카페 공간이 딸려 있습니다", + "밤에는 정원 석등만 켜 둡니다", + "초원사진관까지 걸어서 3분", + "원도심 한가운데, 차는 세워 두세요", + "A동은 일본식, B동은 모던입니다", + "네 사람까지 묶는 독채입니다", + "오후 세 시부터 이 집은 손님 것입니다", + "이 골목에서는 서두를 일이 없습니다", + "백 년 된 기와가 비를 받습니다", + "여행이라기보다 하루의 이사입니다", + "군산에서 가장 조용한 담 안쪽", + "이 집의 시간은 조금 느립니다", + "마당, 정원, 그리고 평상 하나", + "짐을 풀면 골목부터 걸어 보세요", + "창을 열면 옆집 기와가 보입니다", + "담배 냄새가 없는 집입니다", + "이불에서 볕 냄새가 납니다", + "두 동 모두 남의 인기척이 없습니다", + "내항까지 걸어 십 분", + "오래된 것을 고쳐 쓰는 집", + "부엌이 있어 아침은 지어 드셔도 됩니다", + "밤에는 골목 등만 남습니다", + "백 년 된 집이 아직 손님을 받습니다", + "개항장 골목의 끝집입니다", + "문패보다 기와를 보고 찾아오세요", + "걸어서 도는 여행에 알맞은 자리", + "마루에 앉는 시간이 일정의 절반입니다", + "좁은 골목 끝, 대문 하나", + "여기서는 계획을 반쯤 접어도 됩니다", + "하루를 통째로 쓰는 숙소", +] +SEASON = { + "봄": ["정원에 꽃이 먼저 옵니다", "봄에는 대문을 열어 둡니다", "벚꽃 지는 골목을 걸어 오세요", + "마당에 앉는 계절이 시작됩니다", "새소리가 가장 많은 철입니다", "창을 열면 봄이 방까지 들어옵니다"], + "여름": ["처마 그늘이 가장 긴 계절입니다", "마당에 물을 뿌리면 시원해집니다", "여름 밤에는 평상이 제일 좋습니다", + "창고형 카페가 낮에는 서늘합니다", "소나기는 기와가 받아 줍니다", "해가 길어 저녁이 넉넉합니다"], + "가을": ["기와 위로 가을 볕이 마릅니다", "가을 바람이 담을 넘어옵니다", "가을 골목은 해가 빨리 눕습니다", + "창을 열어 두고 자도 되는 밤입니다", "가을에는 평상이 가장 좋은 자리입니다", "마당에 잎이 떨어지는 계절입니다"], + "겨울": ["기와에 눈이 앉으면 골목이 조용해집니다", "겨울 아침 마당은 서리 밭입니다", "이불 속에서 겨울 바람 소리를 듣습니다", + "눈 오면 정원 석등만 켜 둡니다", "항구 바람이 매운 계절입니다", "겨울에는 카페 공간이 가장 아늑합니다"], +} +MONTH = { + 1: ["새해 첫 아침을 백 년 집에서", "겨울 골목이 가장 조용한 달입니다"], + 2: ["아직 바람이 찬 원도심입니다", "겨울 끝을 마당에서 봅니다"], + 3: ["정원에 첫 꽃이 오는 달입니다", "대문을 열어 두기 시작합니다"], + 4: ["벚꽃 지는 골목을 걷는 달입니다", "마당에 앉는 시간이 길어집니다"], + 5: ["새소리가 가장 많은 달입니다", "창을 열어 두고 자는 밤입니다"], + 6: ["처마 그늘이 길어지는 달입니다", "여름 앞의 원도심은 한가합니다"], + 7: ["소나기를 기와가 받는 달입니다", "여름 밤 평상이 제일 좋습니다"], + 8: ["낮에는 카페 공간이 서늘합니다", "해가 길어 저녁이 넉넉한 달입니다"], + 9: ["볕이 마르기 시작하는 달입니다", "가을이 담을 넘어오는 구월입니다"], + 10: ["잎이 마당에 떨어지는 달입니다", "걷기에 가장 좋은 달입니다"], + 11: ["해가 빨리 눕는 골목입니다", "이불이 가장 따뜻한 달입니다"], + 12: ["기와에 눈이 앉는 달입니다", "한 해의 끝을 조용히 보내는 자리"], +} +# 키는 렌더러(use-live-weather.ts)의 분류와 같아야 한다 — 맑음·구름많음·비·눈·흐림. +WEATHER = { + "맑음": ["마당 평상에 앉으면 새소리만 들립니다", "정원 쪽으로 그늘이 길게 눕는 시간입니다", "기와가 마르는 냄새가 납니다"], + "구름많음": ["빛이 부드러운 날입니다", "골목 사진이 제일 잘 나오는 빛입니다"], + "흐림": ["빛이 낮게 깔리는 날입니다", "이런 날은 카페 공간이 가장 아늑합니다"], + "비": ["백 년 된 기와를 타고 내리는 소리가 다릅니다", "카페 공간에서 빗소리를 들으실 수 있습니다", "비 오는 골목에는 사람이 없습니다"], + "눈": ["기와에 눈이 앉으면 골목이 통째로 조용해집니다", "눈 밟는 소리가 담 안에서 크게 들립니다"], +} + +items = [{"text": t, "kind": "general"} for t in GENERAL] +items += [{"text": t, "kind": "season", "season": s} for s, lst in SEASON.items() for t in lst] +items += [{"text": t, "kind": "month", "month": m} for m, lst in MONTH.items() for t in lst] +items += [{"text": t, "kind": "weather", "weather": w} for w, lst in WEATHER.items() for t in lst] + +payload = json.loads((SP / "stay-payload-new.json").read_text(encoding="utf-8")) +payload["narrative"]["catchphrases"] = {"version": 1, "items": items} +(SP / "stay-payload-new.json").write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8") + +# ── 머뭄이 만든 노래 5곡 ────────────────────────────────────────────────────── +# ★ 가요 다방 섹션에는 **넣지 않는다** (2026-09-10 사장님: "가요다방에 왜 우리 노래 넣음???"). +# 그 섹션은 '이 도시를 노래한 곡' 을 모으는 자리다 — 숙소가 만든 곡을 거기 섞으면 +# 군산 노래를 보러 온 사람에게 광고를 끼워 넣는 꼴이고, 출처·연도 칸도 채울 수 없다. +# 플레이어(헤더)만 이 목록을 읽는다. 자리는 narrative.ownSongs 다. +OWN_SONGS = [ + ("머뭄의 아침", "01-mumum-morning.mp3", "#c8873a"), + ("CHECK IN · 머뭄", "02-check-in.mp3", "#2f6b8f"), + ("Stay in 머뭄", "03-stay-in.mp3", "#6b7f5b"), + ("오늘은 안 갈란다", "04-not-going.mp3", "#a34a2a"), + ("군산에 두고 온 님", "05-gunsan-nim.mp3", "#4a3f6b"), +] +payload["narrative"]["ownSongs"] = { + "version": 1, + "items": [{"title": title, "artist": "스테이,머뭄", "labelColor": color, "own": True, + "audioUrl": "/s/stay/audio/" + filename} + for title, filename, color in OWN_SONGS], +} + +# ── 객실 사진·내용을 야놀자 등록본으로 **교체**한다 ────────────────────────────── +# 출처: https://nol.yanolja.com/stay/domestic/10068088 (사장님이 직접 올린 등록본) +# +# ★ 배정을 눈으로 하지 않는다 (2026-09-10 사장님: "B동 10개 A동 12개로 파악이 되는데") +# 처음엔 사진을 열어 보고 A/B 를 갈랐다 — 11/5/공용 6 이 나왔고 실제는 **A동 12 · B동 10** 이다. +# 등록본 페이지의 Next.js 데이터에 `roomTypes[].photos` 가 객실별로 들어 있다. +# `scripts/mockup/rooms.json` 이 그걸 뽑아 둔 것이고, 여기 순서도 등록본 순서 그대로다. +# +# ★ 촬영본(h-*)과 객실 사진(a-*·b-*)은 다른 묶음이다. +# 등록본에는 업로드 배치가 둘 있다 — 2026/03/25(객실별 22장, 현장 사진)과 +# 2026/04/03(촬영본 6장, 밝고 선명하다). **히어로·대표는 촬영본에서 고른다** +# (2026-09-10: "히어로 이미지 화질이 별론데" — 현장 사진을 대표로 세웠던 것). +# 촬영본은 객실 목록에 안 들어간다 — 등록본이 객실에 붙여 놓지 않았다. +# +# ★ alt 는 사진을 열어 보고 적는다. 렌더러는 alt 없는 사진을 객실에서 아예 뺀다(derive.ts:112). +# ★ 파일은 `/s/stay/img/` 에 둔다 — `out/assets` 는 프리렌더가 관리하는 자리다(음원과 같은 규약). +YANOLJA_URL = "https://nol.yanolja.com/stay/domestic/10068088" + +# (파일, 객실, alt, 분류) — 객실은 A동·B동, None 이면 숙소 공용(갤러리에만 선다) +ROOM_PHOTOS = [ + ("h-01.jpg", None, "기와 지붕 아래로 넓은 우드 데크와 정원이 이어지는 외관", "외관"), + ("h-02.jpg", None, "동백이 핀 마당과 우드 파사드 현관으로 이어지는 데크", "외관"), + ("h-03.jpg", None, "흰 벽과 밝은 바닥에 올리브색 소파와 긴 벽난로를 둔 거실", "거실"), + ("h-04.jpg", None, "옛 서까래를 드러낸 천장에 라탄 조명이 걸린 거실", "거실"), + ("h-05.jpg", None, "문 사이로 보이는 흰 앤티크 침대와 검은 서랍장", "침실"), + ("h-06.jpg", None, "캡슐 커피머신과 레트로 전자레인지가 놓인 주방 어메니티", "주방"), + + ("a-01.jpg", "A동", "긴 벽난로와 벽걸이 TV, 좌식 스툴을 둔 거실", "거실"), + ("a-02.jpg", "A동", "베이지 소파와 원형 원목 테이블, 장지문 창이 있는 거실", "거실"), + ("a-03.jpg", "A동", "옛 서까래 천장에 라탄 조명이 걸린 거실과 벽난로", "거실"), + ("a-04.jpg", "A동", "어두운 원목 하부장에 냉장고와 전자레인지를 둔 주방", "주방"), + ("a-05.jpg", "A동", "붉은 벽돌 벽에 벤치와 색색 의자를 둔 창고형 카페 공간", "카페"), + ("a-06.jpg", "A동", "석재 담과 소나무가 둘러싼 마당의 야외 티테이블", "외관"), + ("a-07.jpg", "A동", "원목 벽과 바닥을 지나 침실로 이어지는 복도", "실내"), + ("a-08.jpg", "A동", "장지문 창 옆에 흰 침구를 깐 침대가 놓인 침실", "침실"), + ("a-09.jpg", "A동", "정원이 보이는 큰 장지문 창과 벽걸이 TV", "거실"), + ("a-10.jpg", "A동", "원형 거울과 욕조, 어메니티를 둔 욕실", "욕실"), + ("a-11.jpg", "A동", "우드 외벽 건물과 담 사이 마당에 놓인 야외 테이블", "외관"), + ("a-12.jpg", "A동", "흰 벽돌 벽과 우드 담에 걸린 스테이 머뭄 간판", "외관"), + + ("b-01.jpg", "B동", "초가 파라솔을 세운 데크와 검은 현관문이 있는 외관", "외관"), + ("b-02.jpg", "B동", "유리문을 열어 둔 카페 공간과 데크의 파라솔 테이블", "외관"), + ("b-03.jpg", "B동", "큰 창으로 빛이 드는 거실의 다크그린 소파와 샹들리에", "거실"), + ("b-04.jpg", "B동", "흰 타일 벽에 민트색 냉장고와 토스터를 둔 주방", "주방"), + ("b-05.jpg", "B동", "흰 앤티크 침대와 꽃 모양 조명이 있는 침실", "침실"), + ("b-06.jpg", "B동", "초록 서랍장과 거울, 유리문이 있는 침실 앞 공간", "실내"), + ("b-07.jpg", "B동", "커피 레터링 벽과 빨간 스툴을 둔 카페 공간", "카페"), + ("b-08.jpg", "B동", "욕조와 세면대, 블라인드 창이 있는 욕실", "욕실"), + ("b-09.jpg", "B동", "흰 벽돌 아치 현관에 우편함이 걸린 대문", "외관"), + ("b-10.jpg", "B동", "흰 벽돌 벽을 따라 디딤돌이 놓인 통로", "외관"), +] +# ★ 히어로와 소개 사진은 **원래 것으로 되돌린다** (2026-09-10 사장님: "히어로랑 소개 이미지 원복해") +# 히어로는 대표 사진(isPrimary)을, 소개는 **대표가 아닌 첫 사진**을 쓴다 +# (HeroSection.tsx:40 primaryImage · AboutSection.tsx:23 galleryImages().find(!isPrimary)). +# 그래서 원본 media 를 앞에 그대로 두고 등록본 사진을 뒤에 붙인다 — 순서가 곧 그 두 자리다. +# 객실 안내는 그대로 등록본 사진만 쓴다(units.mediaIds). +HERO_PHOTO = None # 대표는 원본 사진이 계속 갖는다 + +_NS = uuid.UUID("6f1f3f5e-0000-4000-8000-000000000000") # 재실행해도 같은 mediaId 가 나오게 + +new_media = [] +per_unit = {"A동": [], "B동": []} +for filename, room, alt, category in ROOM_PHOTOS: + media_id = str(uuid.uuid5(_NS, filename)) + new_media.append({ + "mediaId": media_id, + "url": "/s/stay/img/" + filename, + "alt": alt, + "category": category, + "width": 1280, + "height": 800, + "isPrimary": filename == HERO_PHOTO, + "sourceType": 3, + "originUrl": YANOLJA_URL, + "unitId": None, + }) + if room: + per_unit[room].append(media_id) + +# 원본(네이버 예약본 미러) 사진을 앞에, 등록본 사진을 뒤에. +ORIGINAL_MEDIA = json.loads((SP / "stay-payload.json").read_text(encoding="utf-8"))["media"] +payload["media"] = ORIGINAL_MEDIA + new_media + +# ★ 등록본에만 있던 값 — 침대 구성 · 면적 · 객실 시설. +# bed_type·room_size 는 렌더러가 칩으로 세우는 키다(derive.ts:87). 없으면 '기준 2인 / 최대 4인' +# 만 서고, 손님이 가장 먼저 묻는 "침대가 뭔가 · 몇 평인가" 가 화면에 없다. +NOW = "2026-09-10T00:00:00+00:00" +ROOM_FACTS = { + "A동": [("bed_type", "침대", "퀸 침대 1개"), ("room_size", "면적", "52")], + "B동": [("bed_type", "침대", "킹 침대 1개")], +} +ROOM_UNITS = {"room_size": "m²"} +FACILITIES = "욕조 · 개별 화장실 · 주방 · 다이닝룸 · 테라스 · 벽난로 · OTT · 어메니티 · 취사 가능" + +for unit in payload["units"]: + room = unit["name"][:2] + extra = list(ROOM_FACTS.get(room, [])) + [("room_facilities", "객실 시설", FACILITIES)] + have = {f["key"] for f in unit["facts"]} + for key, label, value in extra: + if key in have: + for fact in unit["facts"]: + if fact["key"] == key: + fact["value"] = value + continue + unit["facts"].append({ + "key": key, "label": label, "value": value, "unit": ROOM_UNITS.get(key), + "type": "number" if key == "room_size" else "text", + "scope": "unit", "status": 3, "sourceType": 3, "sourceUrl": YANOLJA_URL, + "critical": False, "required": False, "unitId": unit["unitId"], + "collectedAt": NOW, "verifiedAt": NOW, + }) + # 옛 사진은 남기지 않는다 — 등록본이 그 객실에 붙여 놓은 사진만 그 순서로 세운다. + unit["mediaIds"] = per_unit.get(room, []) + +# ── index.html 조립 ────────────────────────────────────────────────────────── +html = (SP / "orig" / "index.html").read_text(encoding="utf-8") + +# payload 교체. 를 만들지 않도록 문자열 안의 '<' 를 이스케이프한다(구운 것과 같은 규약). +blob = json.dumps(payload, ensure_ascii=False).replace("window\.__SITE_PAYLOAD__=.*?", + lambda _m: "", + html, count=1, flags=re.S) +assert n == 1, "payload 스크립트를 못 찾았다" + +# ★ 이용 정보 **맨 아래 구분선**을 지운다 (2026-09-10 사장님 지시). +# 항목이 끝난 자리에 선이 한 줄 더 그어져 있어, 빈 칸이 하나 더 있는 것처럼 보인다. +# 항목(바비큐 이용) 자체는 그대로 둔다 — 지우라고 한 것은 선이다. +# ★ 마크업을 고쳐 지우지 않는다 — React 가 payload 로 다시 그리면서 원래 class 를 되돌린다 +# (실측 2026-09-10: 치환은 됐는데 화면에는 선이 그대로 있었다). 지우는 자리는 주입 CSS 다. + +css = (SP / "inject.css").read_text(encoding="utf-8") +js = (SP / "inject.js").read_text(encoding="utf-8") +assert "" not in css and "" not in js + +block = ("\n \n" + " \n ") +# 치환문(block)에 JS 의 정규식(\s 등)이 섞여 있어 그대로 넘기면 파이썬이 이스케이프로 읽는다 — 람다로 넘긴다. +html, n = re.subn(r"\n?\s*", lambda _m: block + "", html, count=1) +assert n == 1, " 를 못 찾았다" + +# ───────────────────────────────────────────────────────────────────────────── +# 레포 안으로 끌어들이기 — 다른 사람 기계에서도 그대로 떠야 한다. +# +# ★ 왜 (2026-09-11, 사장님: "다른 환경에서 local/s/stay 치면 나오냐고") +# 구워진 index.html 은 미러 사진 61장(`/assets/mirror/*`)과 번들 css/js 를 **루트 절대경로**로 +# 가리킨다. 그 파일들은 레포가 아니라 도커 볼륨에만 있다 — 레포만 받은 사람은 흰 화면을 본다. +# 번들은 더 나쁘다: 파일명이 콘텐츠 해시라 그 기계에서 구운 해시가 다르면 404 다. +# 그래서 둘 다 `img/mirror/` · `vendor/` 로 복사해 두고, 경로를 `/s/stay/…` 로 바꿔 박는다. +# 이러면 nginx 가 `/s/` 를 파일로 주기만 하면 되고 번들 해시와 무관해진다. +# (대신 렌더러를 고쳐도 이 목업은 안 따라간다 — 목업은 원래 재굽기 대상이 아니라 그게 맞다.) +missing = [] +for name in sorted(set(re.findall(r"/assets/mirror/[0-9a-f]+\.\w+", html))): + if not (SP / "img" / "mirror" / name.rsplit("/", 1)[1]).exists(): + missing.append(name) +for name in sorted(set(re.findall(r"/assets/index-[\w.-]+\.(?:css|js)", html))): + if not (SP / "vendor" / name.rsplit("/", 1)[1]).exists(): + missing.append(name) +assert not missing, f"레포에 없는 자산 {len(missing)}개: {missing[:3]} … `vendor.sh` 를 돌려라" + +html = re.sub(r"/assets/mirror/", "/s/stay/img/mirror/", html) +html = re.sub(r"/assets/(index-[\w.-]+\.(?:css|js))", r"/s/stay/vendor/\1", html) +left = re.findall(r"/assets/[^\"\\')\s]+", html) +assert not left, f"아직 /assets 를 가리키는 자리가 있다: {left[:5]}" + +out = SP / "build" +out.mkdir(exist_ok=True) +(out / "index.html").write_text(html, encoding="utf-8") + +env = json.loads({s["id"]: s for s in payload["theme"]["sections"]}["itinerary"]["data"]) +print(f"index.html {len(html):,}자 · 일정 {len(env['items'])}개 · 캐치프레이즈 {len(items)}개") +print(" " + " · ".join(f"{k} {sum(1 for i in items if i['kind'] == k)}" + for k in ("general", "season", "month", "weather"))) diff --git a/solution/site/scripts/mockup/people-images-meta.json b/solution/site/scripts/mockup/people-images-meta.json new file mode 100644 index 0000000..556d572 --- /dev/null +++ b/solution/site/scripts/mockup/people-images-meta.json @@ -0,0 +1,128 @@ +[ + { + "title": "고건", + "url": "https://upload.wikimedia.org/wikipedia/commons/a/af/Goh_Kun_at_SDPC.JPG", + "file": "Goh_Kun_at_SDPC.JPG", + "license": "CC BY-SA 4.0", + "author": "서울특별시 소방재난본부" + }, + { + "title": "고은", + "url": "https://upload.wikimedia.org/wikipedia/commons/e/eb/Sv%C4%9Bt_knihy_2011_-_Ko_Un.jpg", + "file": "Svět_knihy_2011_-_Ko_Un.jpg", + "license": "CC BY-SA 3.0", + "author": "Matěj Baťha" + }, + { + "title": "김관영", + "url": "https://upload.wikimedia.org/wikipedia/commons/c/cf/Kim_Kwan-young_%282024%29.jpg", + "file": "Kim_Kwan-young_(2024).jpg", + "license": "KOGL Type 1", + "author": "대한민국 국회 의장비서실" + }, + { + "title": "김선기 (언어철학자)", + "url": "https://upload.wikimedia.org/wikipedia/commons/f/f6/%EA%B9%80%EC%84%A0%EA%B8%B0_%EC%84%A0%EC%83%9D.jpg", + "file": "김선기_선생.jpg", + "license": "Public domain", + "author": "미상Unknown author" + }, + { + "title": "김수미", + "url": "https://upload.wikimedia.org/wikipedia/commons/3/30/Kim_Soo-mi_at_SBS_Radio_-_230506.png", + "file": "Kim_Soo-mi_at_SBS_Radio_-_230506.png", + "license": "CC BY 3.0", + "author": "Seoul Broadcasting System" + }, + { + "title": "박화요비", + "url": "https://upload.wikimedia.org/wikipedia/commons/4/4a/Hwayobi_in_Cyworld_Dream_Music_Festival.jpg", + "file": "Hwayobi_in_Cyworld_Dream_Music_Festival.jpg", + "license": "CC BY-SA 2.0", + "author": "Jinho.Jung" + }, + { + "title": "송새벽", + "url": "https://upload.wikimedia.org/wikipedia/commons/f/f5/Songsaebyuk2015.png", + "file": "Songsaebyuk2015.png", + "license": "CC BY 3.0", + "author": "틈새TV" + }, + { + "title": "이서원", + "url": "https://upload.wikimedia.org/wikipedia/commons/9/95/Lee_Seo-won_KBS_View_Interview.png", + "file": "Lee_Seo-won_KBS_View_Interview.png", + "license": "CC BY 3.0", + "author": "KBS 뷰" + }, + { + "title": "장신영", + "url": "https://upload.wikimedia.org/wikipedia/commons/b/b7/Jang_Shin-Young_from_acrofan.jpg", + "file": "Jang_Shin-Young_from_acrofan.jpg", + "license": "CC BY-SA 3.0", + "author": "acrofan.com" + }, + { + "title": "병헌", + "url": "https://upload.wikimedia.org/wikipedia/commons/e/ee/181024_%EB%B3%91%ED%97%8C.jpg", + "file": "181024_병헌.jpg", + "license": "CC BY 3.0", + "author": "뉴스인스타" + }, + { + "title": "오지환", + "url": "https://upload.wikimedia.org/wikipedia/commons/e/e6/%EC%98%A4%EC%A7%80%ED%99%98LG%EC%A3%BC%EC%9E%A5.png", + "file": "오지환LG주장.png", + "license": "CC BY-SA 3.0", + "author": "OSEN SPORTS" + }, + { + "title": "이진영 (1980년)", + "url": "https://upload.wikimedia.org/wikipedia/commons/8/8c/130822_%EB%AC%B8%ED%95%99%EA%B5%AC%EC%9E%A5.jpg", + "file": "130822_문학구장.jpg", + "license": "CC BY 4.0", + "author": "itsmygabble" + }, + { + "title": "정명원", + "url": "https://upload.wikimedia.org/wikipedia/ko/d/db/%EC%A0%95%EB%AA%85%EC%9B%90.JPG", + "file": "정명원.JPG", + "license": "?", + "author": "User:Sienic" + }, + { + "title": "정혁 (모델)", + "url": "https://upload.wikimedia.org/wikipedia/commons/5/5d/%EC%A0%95%ED%98%81_%EB%AA%A8%EB%8D%B8.jpg", + "file": "정혁_모델.jpg", + "license": "CC BY 3.0", + "author": "파리게이츠" + }, + { + "title": "문호준", + "url": "https://upload.wikimedia.org/wikipedia/commons/4/42/Moon_Ho-jun_%28%EB%AC%B8%ED%98%B8%EC%A4%80%29_from_acrofan.jpg", + "file": "Moon_Ho-jun_(문호준)_from_acrofan.jpg", + "license": "CC BY-SA 3.0", + "author": "acrofan.com" + }, + { + "title": "차우찬", + "url": "https://upload.wikimedia.org/wikipedia/commons/b/b8/Cha_Woo-chan.JPG", + "file": "Cha_Woo-chan.JPG", + "license": "CC BY-SA 2.5", + "author": "원래 올린이는 한국어 위키백과의 Realidad y Illusion" + }, + { + "title": "최서우", + "url": "https://upload.wikimedia.org/wikipedia/commons/0/0c/FIS_Ski_Jumping_World_Cup_2014_-_Engelberg_-_20141221_-_Seou_Choi.jpg", + "file": "FIS_Ski_Jumping_World_Cup_2014_-_Engelberg_-_20141221_-_Seou_Choi.jpg", + "license": "CC BY-SA 3.0", + "author": "Clément Bucco-Lechat" + }, + { + "title": "함운경", + "url": "https://upload.wikimedia.org/wikipedia/commons/9/9b/%ED%95%A8%EC%9A%B4%EA%B2%BD_%EC%86%8C%EC%9E%A5.jpg", + "file": "함운경_소장.jpg", + "license": "CC BY-SA 4.0", + "author": "Teakim11" + } +] \ No newline at end of file diff --git a/solution/site/scripts/mockup/people-images.json b/solution/site/scripts/mockup/people-images.json new file mode 100644 index 0000000..a7e6d35 --- /dev/null +++ b/solution/site/scripts/mockup/people-images.json @@ -0,0 +1,74 @@ +{ + "고은": { + "url": "https://upload.wikimedia.org/wikipedia/commons/e/eb/Sv%C4%9Bt_knihy_2011_-_Ko_Un.jpg?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "김선기 (언어철학자)": { + "url": "https://upload.wikimedia.org/wikipedia/commons/f/f6/%EA%B9%80%EC%84%A0%EA%B8%B0_%EC%84%A0%EC%83%9D.jpg?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "김수미": { + "url": "https://upload.wikimedia.org/wikipedia/commons/3/30/Kim_Soo-mi_at_SBS_Radio_-_230506.png?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "병헌": { + "url": "https://upload.wikimedia.org/wikipedia/commons/e/ee/181024_%EB%B3%91%ED%97%8C.jpg?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "송새벽": { + "url": "https://upload.wikimedia.org/wikipedia/commons/f/f5/Songsaebyuk2015.png?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "이서원": { + "url": "https://upload.wikimedia.org/wikipedia/commons/9/95/Lee_Seo-won_KBS_View_Interview.png?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "장신영": { + "url": "https://upload.wikimedia.org/wikipedia/commons/b/b7/Jang_Shin-Young_from_acrofan.jpg?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "정혁 (모델)": { + "url": "https://upload.wikimedia.org/wikipedia/commons/5/5d/%EC%A0%95%ED%98%81_%EB%AA%A8%EB%8D%B8.jpg?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "고건": { + "url": "https://upload.wikimedia.org/wikipedia/commons/a/af/Goh_Kun_at_SDPC.JPG?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "김관영": { + "url": "https://upload.wikimedia.org/wikipedia/commons/c/cf/Kim_Kwan-young_%282024%29.jpg?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "박화요비": { + "url": "https://upload.wikimedia.org/wikipedia/commons/4/4a/Hwayobi_in_Cyworld_Dream_Music_Festival.jpg?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "함운경": { + "url": "https://upload.wikimedia.org/wikipedia/commons/9/9b/%ED%95%A8%EC%9A%B4%EA%B2%BD_%EC%86%8C%EC%9E%A5.jpg?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "문호준": { + "url": "https://upload.wikimedia.org/wikipedia/commons/4/42/Moon_Ho-jun_%28%EB%AC%B8%ED%98%B8%EC%A4%80%29_from_acrofan.jpg?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "오지환": { + "url": "https://upload.wikimedia.org/wikipedia/commons/e/e6/%EC%98%A4%EC%A7%80%ED%99%98LG%EC%A3%BC%EC%9E%A5.png?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "이진영 (1980년)": { + "url": "https://upload.wikimedia.org/wikipedia/commons/8/8c/130822_%EB%AC%B8%ED%95%99%EA%B5%AC%EC%9E%A5.jpg?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "정명원": { + "url": "https://upload.wikimedia.org/wikipedia/ko/d/db/%EC%A0%95%EB%AA%85%EC%9B%90.JPG?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "차우찬": { + "url": "https://upload.wikimedia.org/wikipedia/commons/b/b8/Cha_Woo-chan.JPG?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + }, + "최서우": { + "url": "https://upload.wikimedia.org/wikipedia/commons/0/0c/FIS_Ski_Jumping_World_Cup_2014_-_Engelberg_-_20141221_-_Seou_Choi.jpg?utm_source=ko.wikipedia.org&utm_campaign=api&utm_content=original", + "via": "문서대표" + } +} \ No newline at end of file diff --git a/solution/site/scripts/mockup/people-photos.json b/solution/site/scripts/mockup/people-photos.json new file mode 100644 index 0000000..6f26926 --- /dev/null +++ b/solution/site/scripts/mockup/people-photos.json @@ -0,0 +1,121 @@ +[ + { + "name": "고건", + "url": "/s/stay/img/people/%EA%B3%A0%EA%B1%B4.jpg", + "license": "CC BY-SA 4.0", + "author": "서울특별시 소방재난본부", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3AGoh_Kun_at_SDPC.JPG" + }, + { + "name": "고은", + "url": "/s/stay/img/people/%EA%B3%A0%EC%9D%80.jpg", + "license": "CC BY-SA 3.0", + "author": "Matěj Baťha", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3ASv%C4%9Bt_knihy_2011_-_Ko_Un.jpg" + }, + { + "name": "김관영", + "url": "/s/stay/img/people/%EA%B9%80%EA%B4%80%EC%98%81.jpg", + "license": "KOGL Type 1", + "author": "대한민국 국회 의장비서실", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3AKim_Kwan-young_%282024%29.jpg" + }, + { + "name": "김선기", + "url": "/s/stay/img/people/%EA%B9%80%EC%84%A0%EA%B8%B0.jpg", + "license": "Public domain", + "author": "미상Unknown author", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3A%EA%B9%80%EC%84%A0%EA%B8%B0_%EC%84%A0%EC%83%9D.jpg" + }, + { + "name": "김수미", + "url": "/s/stay/img/people/%EA%B9%80%EC%88%98%EB%AF%B8.png", + "license": "CC BY 3.0", + "author": "Seoul Broadcasting System", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3AKim_Soo-mi_at_SBS_Radio_-_230506.png" + }, + { + "name": "박화요비", + "url": "/s/stay/img/people/%EB%B0%95%ED%99%94%EC%9A%94%EB%B9%84.jpg", + "license": "CC BY-SA 2.0", + "author": "Jinho.Jung", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3AHwayobi_in_Cyworld_Dream_Music_Festival.jpg" + }, + { + "name": "송새벽", + "url": "/s/stay/img/people/%EC%86%A1%EC%83%88%EB%B2%BD.png", + "license": "CC BY 3.0", + "author": "틈새TV", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3ASongsaebyuk2015.png" + }, + { + "name": "이서원", + "url": "/s/stay/img/people/%EC%9D%B4%EC%84%9C%EC%9B%90.png", + "license": "CC BY 3.0", + "author": "KBS 뷰", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3ALee_Seo-won_KBS_View_Interview.png" + }, + { + "name": "장신영", + "url": "/s/stay/img/people/%EC%9E%A5%EC%8B%A0%EC%98%81.jpg", + "license": "CC BY-SA 3.0", + "author": "acrofan.com", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3AJang_Shin-Young_from_acrofan.jpg" + }, + { + "name": "병헌", + "url": "/s/stay/img/people/%EB%B3%91%ED%97%8C.jpg", + "license": "CC BY 3.0", + "author": "뉴스인스타", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3A181024_%EB%B3%91%ED%97%8C.jpg" + }, + { + "name": "오지환", + "url": "/s/stay/img/people/%EC%98%A4%EC%A7%80%ED%99%98.png", + "license": "CC BY-SA 3.0", + "author": "OSEN SPORTS", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3A%EC%98%A4%EC%A7%80%ED%99%98LG%EC%A3%BC%EC%9E%A5.png" + }, + { + "name": "이진영", + "url": "/s/stay/img/people/%EC%9D%B4%EC%A7%84%EC%98%81.jpg", + "license": "CC BY 4.0", + "author": "itsmygabble", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3A130822_%EB%AC%B8%ED%95%99%EA%B5%AC%EC%9E%A5.jpg" + }, + { + "name": "정혁", + "url": "/s/stay/img/people/%EC%A0%95%ED%98%81.jpg", + "license": "CC BY 3.0", + "author": "파리게이츠", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3A%EC%A0%95%ED%98%81_%EB%AA%A8%EB%8D%B8.jpg" + }, + { + "name": "문호준", + "url": "/s/stay/img/people/%EB%AC%B8%ED%98%B8%EC%A4%80.jpg", + "license": "CC BY-SA 3.0", + "author": "acrofan.com", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3AMoon_Ho-jun_%28%EB%AC%B8%ED%98%B8%EC%A4%80%29_from_acrofan.jpg" + }, + { + "name": "차우찬", + "url": "/s/stay/img/people/%EC%B0%A8%EC%9A%B0%EC%B0%AC.jpg", + "license": "CC BY-SA 2.5", + "author": "원래 올린이는 한국어 위키백과의 Realidad y Illusion", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3ACha_Woo-chan.JPG" + }, + { + "name": "최서우", + "url": "/s/stay/img/people/%EC%B5%9C%EC%84%9C%EC%9A%B0.jpg", + "license": "CC BY-SA 3.0", + "author": "Clément Bucco-Lechat", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3AFIS_Ski_Jumping_World_Cup_2014_-_Engelberg_-_20141221_-_Seou_Choi.jpg" + }, + { + "name": "함운경", + "url": "/s/stay/img/people/%ED%95%A8%EC%9A%B4%EA%B2%BD.jpg", + "license": "CC BY-SA 4.0", + "author": "Teakim11", + "file": "https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC%3A%ED%95%A8%EC%9A%B4%EA%B2%BD_%EC%86%8C%EC%9E%A5.jpg" + } +] \ No newline at end of file diff --git a/solution/site/scripts/mockup/people-raw.json b/solution/site/scripts/mockup/people-raw.json new file mode 100644 index 0000000..bc1ba9d --- /dev/null +++ b/solution/site/scripts/mockup/people-raw.json @@ -0,0 +1,59 @@ +{ + "고은": "고은(髙銀, 본명: 고은태, 본명 한자: 髙銀泰, 1933년 8월 1일~)은 대한민국의 시인이자 대학 교수로, 1952년경에서부터 어언 66년 동안 시단을 비롯한 문단에서 활약하였으며, 2018년 3월 6일에 은퇴했다. 본은 제주(濟州)이며, 어릴때에 쓴 일제 가 있고 창씨개명을 했었다. 이름은 다카바야시 도라스케(高林虎助)이고, 이후의 한때의 10년간의 불교 승려 시절의 승명(僧名)은 일초(一超)이며, 호(號)는 파옹(波翁)이다.", + "고형곤": "고형곤(高亨坤, 1906년 4월 16일~2004년 6월 25일)은 일제강점기와 대한민국의 철학자, 정치가이다. 호는 청송(廳松)이다. 본관은 제주(濟州)이다. 고건 제30대·35대 국무총리의 아버지이다.", + "김선기 (언어철학자)": "김선기(金善琪, 1907년 3월 21일~1992년 11월 11일)는 대한민국의 언어학자자이며 교육자이다. 본관은 광산(光山)이고 호는 무돌(무지개돌)이다.", + "김수미": "김수미(金守美, 본명: 김영옥, 金永玉, 1949년 10월 24일~2024년 10월 25일)는 대한민국의 배우이다. 1970년 MBC 공채 텔런트로 연기 경력을 시작하였으며 1980년에 방송된 텔레비전 드라마 《전원일기》에서 일용네 역으로 인기를 누렸다. 이후에도 연기 활동과 예능 활동을 병행하여 활동하였으며 요리 솜씨로 인해 관련된 프로그램들의 진행을 맡기도 했다. ", + "문효치": "문효치(1943년 7월 15일 ~ )는 대한민국 시인이다. 전라북도 군산에서 태어나, 동국대학교 국문학과, 고려대학교 교육대학원을 졸업. 1966년에 [한국일보], [서울신문] 신춘문예 당선으로 문단에 나왔다. 저서로는 《연기 속에 서서》, 《무령왕의 나무새》, 《남내리엽서》, 《왕인의 수염》등 10권의 시집과 《백제시집》, 《사랑이여 어디든 가서》 등 4권의 시선집을 내는 등 지금까지 꾸준한 작품집필로 한국대표 작가로 활발히 활동하고 있으며 현재 <미네르바> 주간으로 있다.", + "박화요비": "박화요비(朴火曜飛, 본명: 박레아, 1982년 2월 11일~) 또는 화요비(火曜飛)는 대한민국의 가수이다. 음악 분야 중 R&B가 주요 활동 분야이다. 화요비는 R&B의 '비'와 이름을 지은 날인 화요일의 '화요'의 합성어이다. 본명은 '박미영'이었다가 '박레아'로 개명하였다. 박화요비의 출생일은 목요일로, 예명의 '화요'가 화요일과는 관계가 없다. 2000년 '박화요비'라는 이름으로 데뷔했으나 2006년 일본에 진출하면서 예명을 '화요비'로 사용하였고 이후 대한민국 내 예명도 '화요비'로 변경하였다. 2017년 소속사를 이전함과 동시에 초심으로 돌아가고자 예명을 다시 ‘박화요비’로 환원하였다.", + "송새벽": "송새벽(1979년 12월 26일 ~ )은 대한민국의 배우이다.", + "심호택": "심호택(1947년 4월 12일 ~ 2010년 1월 30일)은 대한민국의 시인이다. 민중의 삶을 시로 잘 녹여냈던 시인이다.", + "이병훈 (시인)": "이병훈(李炳勳, 1925년 4월 15일 ~ 2009년 2월 15일)은 전라북도 옥구 출생으로 현대 시인이다. 그는 1956년 6월에 창간된 월간 순문예지인 자유문학에 〈단층(斷層)〉(59), 〈흰줄기의 길〉(59), 〈길〉(60) 등으로 추천을 받았다. 그 후 그는 시인 활동과 같이 언론계에 종사하였다. 한국문인협회, 외솔회, 한국시인협회에 소속되어 있다.", + "이서원": "이서원(李曙原, 1997년 2월 17일 ~ )은 대한민국의 前 배우이다.", + "이연주 (시인)": "이연주(1953년 ~ 1992년)는 대한민국의 시인이다.", + "장신영": "장신영(張伸瑛, 1984년 1월 17일 ~ )은 대한민국의 배우이다.", + "주하림": "주하림(朱夏林은 대한민국의 시인이다. 전라북도 군산에서 태어나 서울에서 자랐으며, 단국대학교 문예창작과를 졸업하였다. 2009년 창비가 주관하는 제9회 창비신인시인상을 수상하며 등단했다. 2013년 3월 첫 시집 《비버리힐스의 포르노 배우와 유령들》을 출간했다.", + "채만식": "채만식(蔡萬植, 1902년 7월 21일 (음력 6월 17일) ~ 1950년 6월 11일)은 일제강점기와 대한민국의 소설가, 극작가, 문학평론가, 수필가이다. 본관은 평강(平康)이며 호는 백릉(白菱), 채옹(采翁)이다.", + "하준 (가수)": "하준(본명: 강동기, 1965년 1월 16일 ~ )은 대한민국의 가수이다.", + "강민지 (배우)": "강민지(1996년 5월 12일~)는 대한민국의 배우이자 모델이다. 2017년 모델 활동을 시작한 이후 드라마, 영화, 광고 등 다양한 분야에서 활동하고 있다. 과거에는 강나루라는 예명으로 활동했으며 이후 본명인 강민지로 활동명을 변경하였다.", + "강봉균 (정치인)": "강봉균(康奉均, 1943년 8월 13일 ~ 2017년 1월 31일)은 정보통신부 장관과 재정경제부 장관, 16·17·18대 국회의원을 지낸 대한민국의 정치인이다.", + "고건": "고건(髙建, 1938년 1월 2일~)은 대한민국의 고위정치인이다. 제30대·35대 국무총리이다. 본관은 제주(濟州)이다. 호는 우민(又民, 于民)이다.", + "김관영": "김관영(金寬永, 1969년 11월 15일~)은 대한민국의 공인회계사, 공무원, 변호사, 정치인이다. 제19·20대 국회의원을 지냈고, 민선 8기 전북특별자치도지사(이전 명칭: 전라북도지사)이다.", + "김혜선 (희극인)": "김혜선(1983년 11월 11일 ~ )은 대한민국의 희극 배우다.", + "박윤경 (가수)": "박윤경(1970년 8월 28일 ~ )은 대한민국의 가수 겸 기업인이다. 현재 거주지는 경기도 가평군이다.", + "병헌": "신성하(본명: 이병헌 , 본명 한자: 李秉憲, 1993년 11월 23일 ~ )은 대한민국의 배우이다. 틴탑으로 활동할 당시에 리드래퍼를 맡았었다. 일원으로 활동할 당시에는 엘조라는 예명을 사용했다.", + "신영대": "신영대(申榮大, 1968년 1월 15일~)는 대한민국의 정치인이며, 제21·22대 국회의원이다.", + "양일동": "양일동(梁一東, 1912년 12월 30일 ~ 1980년 4월 1일)은 대한민국 정치인이다. 전라북도 옥구군(現 군산시) 서수면 마룡리 출신이다. 본관은 남원이고, 호는 현곡(玄谷)이다. 제3·4·5·8·10대 국회의원을 지냈다. 1971년 신민당 주류의 유진산과 결별하고 1973년 민주통일당을 창당, 대표 최고위원과 총재를 지냈다. 1973년 중앙정보부 정치공작사건으로 기록된 김대중 납치 사건 때 참고인으로 곤욕을 치르기도 하였다.", + "이수현 (1895년)": "이수현(李守鉉, 1895년 5월 8일 ~ 1980년 3월 8일)은 대한민국의 독립운동가이다. 본관은 전의(全義)이고 호(號)는 산남(山南)·석남(石南)·와당(瓦堂)이다.", + "이옥주 (희극인)": "이옥주(1969년 3월 6일 ~ )는 대한민국의 희극 배우이다. 전라북도 군산시 출신으로 군산중앙여자고등학교, 개정간호전문대학을 졸업했다. 1988년에 MBC 개그콘테스트에서 대상을 수상하면서 데뷔했으며 1989년 MBC 연기대상 시상식에서 코미디 부문 여자 신인상을 수상했다. 1998년 콜로라도주 출신의 미국인 사업가인 토마스 가슬러와 결혼했으며, 이후 미국으로 이주해 뉴멕시코주에 거주 중이다.", + "임병찬": "임병찬(林炳瓚, 1851년 2월 5일 ~ 1916년 5월 23일)은 최익현과 함께 거병했던 구한말의 의병장이다. 본관은 평택이며 최익현의 제자이며 단식 순국하였다. 아호는 돈헌(遯軒)이다.", + "장재호 (배우)": "장재호(1986년 1월 8일 ~ )는 대한민국의 배우이다.", + "정혁 (모델)": "정혁(1991년 9월 20일~)은 대한민국의 남자 모델 겸 방송인이다. 2015년 DDP에서 열린 S/S 서울패션위크 블라인드니스에서 정식으로 데뷔했다. 2025년 토킹바(위스키 바에서 대화)에서 아르바이트를 하고 있다고 밝혔다.", + "채영석": "채영석(蔡映錫, 1934년 12월 7일~2006년 3월 3일)은 대한민국의 정치인이다. 제13·14·15대 국회의원을 지냈다. 전라북도 군산 출신이며, 본관은 평강이다.", + "김광선 (권투 선수)": "김광선(金光善, 1964년 7월 16일~)은 대한민국의 권투 선수, 지도자이며, 1988년 하계 올림픽 남자 복싱 플라이급에서 금메달을 획득했다. 은퇴 후에는 권투 도장을 운영하고 있으며, 얼마 전까지는 KBS 권투 해설가를 맡고 있었다.", + "박경완": "박경완(朴勍完, 1972년 7월 11일 ~ )은 전 KBO 리그 SK 와이번스의 포수이자, 전 KBO 리그 LG 트윈스의 배터리코치였다.", + "박성현 (양궁 선수)": "박성현(朴成賢, 1983년 1월 1일~)은 대한민국의 전직 양궁 선수이자, 현재 전라북도청의 양궁 감독이다. 전라북도 군산 출신으로 소룡초등학교 4학년때 양궁에 입문하게 되었고, 2001년 3월 종별선수권대회에서 생애 처음 개인종합 우승을 차지하면서 두각을 나타내기 시작하였다. 그리고 그 해 2001년 국가 대표로 발탁되어, 생애 첫 태극마크를 달고 2001년 9월에 열린 제41회 세계선수권대회에 출전해서 개인전 금메달, 단체전에서는 동메달을 획득하였다. 2004년 아테네올림픽 여자 양궁 랭킹라운드에서 72발 합계 682점을 쏴, 이탈리아의 양궁 선수 나탈리아 발레바 가 보유한 세계기록(679점)에 3점을 더하면서 세계 신기록을 세웠다. 여자 양궁 단체전에서는 윤미진, 이성진과 함께 금메달을 획득하고, 개인", + "오지환": "오지환(吳智煥, 1990년 3월 12일 ~ )은 KBO 리그 LG 트윈스의 내야수이다. 그의 사촌 동생은 KBO 리그 KIA 타이거즈의 내야수인 오정환이다.", + "은성수 (공무원)": "은성수(殷成洙, 1961년 ~ )는 대한민국의 제7대 금융위원회 위원장을 역임한 공무원이다.", + "이길여": "이길여(李吉女, 1932년 6월 12일 ~ )는 대한민국의 의료인이자 교육자이다. 가천대학교 길병원 설립자로 가천대학교 총장, 경인일보 회장, 가천길재단 회장을 맡고 있다. 이길여 총장의 호(號) '가천(嘉泉)'은 한국정신문화연구원장을 지낸 류승국박사가 지어줬다.", + "이진영 (1980년)": "이진영(李晋暎, 1980년 6월 15일 ~ )은 전 KBO 리그 kt 위즈의 외야수, 내야수이자, 현 대한민국 야구 국가대표팀의 QC코치, KBO 리그 두산 베어스의 코치이다.", + "이천웅": "이천웅(李天雄, 1988년 10월 20일~)은 전 KBO 리그 LG 트윈스의 외야수이다.", + "정명원": "정명원(鄭明源, 1966년 6월 14일 ~ )은 전 KBO 리그 현대 유니콘스의 투수이다. 그의 남동생은 전 KBO 리그 쌍방울 레이더스의 내야수인 정학원이다.", + "조계현": "조계현(趙啓顯, 1964년 5월 1일 ~ )은 전 KBO 리그 해태타이거즈의 투수이자, 현 협성대학교의 특임교수이다.", + "조규제": "조규제(曺圭帝, 1967년 10월 7일 ~ )는 전 KBO 리그 KIA 타이거즈의 투수이자, 현 제주고등학교 야구부의 투수코치이다. 원래 오른손잡이였으나 야구를 시작할 무렵 왼손잡이 글러브를 사용해서 좌투우타가 됐다. 1991년에 연세대학교 체육교육학과를 졸업하고 쌍방울 레이더스의 1차 지명을 받아 입단하였다. 입단 첫 해인 1991년에는 49경기에 등판해 9승 7패, 27세이브, 1점대 평균자책점을 기록하며 신인왕을 수상했다. 이후 마무리 투수로서 꾸준한 활약을 보여주며, 팬들로부터 '완산 특급(完山特急)'이라고 불렸다. 현역 후반기에는 주로 좌완 원포인트 스페셜리스트 중간 계투로 등판했다. 현대 유니콘스의 한국시리즈 우승에 공헌했던 2003년 시즌 직후 13년 만에 36세의 나이로 국가대표팀에 선발되어 ", + "차우찬": "차우찬(車雨燦, 1987년 5월 31일 ~ )은 전 KBO 리그 롯데 자이언츠의 투수로, 현 KBS N 스포츠 야구해설위원이다. 개그맨 도광록과 함께 유튜브 방송을 진행하는 인터넷 방송인이다. ", + "채금석": "채금석(蔡金錫, 1904년 4월 9일 ~ 1995년 12월 26일)은 일제강점기 당시 경성 축구단 소속의 축구 선수이다. 경평축구대항전과 1936년 하계 올림픽 예선전에 출전하였으며, 은퇴 이후 프로축구, 실업, 대학, 초·중·고 등 후진 선수양성에 공헌하였다.", + "최길선": "최길선(崔吉善, 1946년 2월 25일 ~ )은 현대중공업 대표이사 회장에 재직 중인 대한민국의 기업인이다. 본관은 탐진이며, 전라북도 군산시 출신이다. 조선업 분야의 전문 경영인이다.", + "함운경": "함운경(咸雲炅, 1964년 3월 18일~)은 대한민국의 민주화 운동권 학생운동가 출신의 정치인이다. 전북 군산 생이기도 하고, 공화주의 아카데미 상임대표 소장을 지냈으며, 2006년 5월 31일 당시 제4회 전국 동시 지선 전북 군산시장 선거 열린우리당 후보로 입후보 및 낙마한 적이 있다.", + "김의겸": "김의겸(金宜謙, 1963년 5월 22일~)은 대한민국의 기자 출신 정치인이다. 1990년부터 2017년까지 한겨레신문 기자로 활동했고, 문재인 정부 시절 대통령비서실 대변인을 거쳐 제21·22대 국회의원을 역임하고 있다. 1963년 5월 22일 경상북도 칠곡군에서 태어났다. 국민학교 입학 전 당시 모친이 공무원(교사)으로 전라북도 군산시로 발령받아 이주하여 군산금광국민학교 군산남중학교 군산제일고등학교를 졸업하였다. 1982년 고려대학교 법학과에 입학했다. 1985년 11월에 전국학생총연합 '민중민주정부 수립과 민족자주통일을 위한 투쟁위' 산하 파쇼헌법철폐투쟁위원회 소속으로 민주정의당 중앙정치연수원 점거농성에 참여했다가 1달 뒤 현주건조물 방화예비 혐의 등으로 구속기소됐다. 1990년 한겨레신문에 입사해 국제", + "노상래": "노상래(한국 한자: 盧相來, 1970년 12월 15일)는 대한민국의 전 축구 선수이자 현 지도자로 현역 시절 포지션은 공격수였으며 현재 울산 HD의 유소년 디렉터와 대한축구협회 기술발전위원을 맡고 있다.", + "노수진": "노수진(魯壽珍, 1962년 1월 10일~)은 대한민국의 전 축구 선수 및 지도자이다. 현역 시절 포지션은 공격수였다. 현재 영등포공업고등학교의 체육교사를 맡고 있다.", + "문호준": "문호준(1997년 6월 23일~)은 대한민국의 전직 카트라이더 프로게이머로 아이디는 HoJun을 사용했다. 카트라이더 리그 통산 최다 우승자로, 카트라이더 프로게이머 최초로 개인 헌정 카트, 문블레이드 X를 가졌다.", + "반석평": "반석평(潘碩枰, 1472년 ~ 1540년 6월 24일)은 조선의 문신이다. 본관은 광주(光州). 자는 공문(公文), 호는 송애(松厓)이다. 1507년(중종 2년) 문과에 급제하여 충청도관찰사, 공조판서, 한성부판윤, 형조판서 등을 역임하였다. 시호는 장절(壯節)이다.", + "유동우": "유동우(柳東雨, 1968년 3월 7일~)는 대한민국의 축구 선수, 지도자로 선수 시절 수비수로 활동했다. 현재 사이버한국외국어대학교 축구부 감독을 맡고 있다.", + "이정효": "이정효(1975년 7월 23일 ~ )는 대한민국의 축구 선수 출신 축구 감독으로 현재 수원 삼성 블루윙즈의 감독을 맡고있다. 현역 시절 풀백으로 활동했으며 프로 선수 경력을 시작한 1998년부터 2008년 은퇴할 때까지 11시즌 동안 부산 아이파크의 원클럽맨으로 활동했다. ", + "전미라": "전미라(田美邏, 1978년 2월 6일~)는 대한민국의 전직 테니스 선수이다. 현재 JTBC 골프&스포츠의 테니스 해설위원을 맡고 있다. 본관은 담양 전씨이다.", + "전진 (1923년)": "전진(1923년 7월 6일 ~ 2012년 4월 29일)은 대한민국의 비전향 장기수이다.", + "정경미 (유도 선수)": "정경미(鄭敬美, 1985년 7월 26일~)는 대한민국의 유도 선수이다.", + "최서우": "최서우(崔栖瑀, 1982년 12월 3일~)는 대한민국의 스키 점프 선수이다. 종교는 불교다. 1998년부터 선수로 활동하였으며, 1998년 동계 올림픽 경기에선 라지힐 단체전에서 373.8점으로 13위위에 올랐다. 노멀힐 개인전과 라지힐 개인전에서 53위를 거둔 바 있다. 그리고 2002년 동계 올림픽, 2006년 동계 올림픽, 2010년 동계 올림픽, 2014년 동계 올림픽, 2018년 동계 올림픽에도 출전하였다. 2011년 이름을 최용직(崔鎔直)에서 최서우로 개명하였다. 최서우 선수는 김현기 선수와 함께 스키점프의 전설로 불리며, 한국의 스키점프 1세대 선수이다. 유명한 영화인 ‘국가대표’가 이 선수들을 모티브로 만들어졌고, 이 영화를 계기로 스키점프는 한국에서 더 알려졌다. 최서우 선수는 올림픽만 6번", + "한왕용": "한왕용(韓王龍, 1966년 7월 17일 ~ )은 히말라야 8000미터급 14좌를 세계에서 11번째, 국내에서 3번째로 완등한 대한민국 산악인이다." +} \ No newline at end of file diff --git a/solution/site/scripts/mockup/people-rows.json b/solution/site/scripts/mockup/people-rows.json new file mode 100644 index 0000000..e42f83c --- /dev/null +++ b/solution/site/scripts/mockup/people-rows.json @@ -0,0 +1,401 @@ +[ + { + "name": "고은", + "title": "고은", + "years": "1933–", + "role": null, + "one": "고은은 대한민국의 시인이자 대학 교수로, 1952년경에서부터 어언 66년 동안 시단을 비롯한 문단에서 활약하였으며, 2018년 3월 6일에 은퇴했다." + }, + { + "name": "고형곤", + "title": "고형곤", + "years": "1906–2004", + "role": "철학자, 정치가", + "one": "고형곤은 일제강점기와 대한민국의 철학자, 정치가이다." + }, + { + "name": "김선기", + "title": "김선기 (언어철학자)", + "years": "1907–1992", + "role": "언어학자자", + "one": "김선기는 대한민국의 언어학자자이며 교육자이다." + }, + { + "name": "김수미", + "title": "김수미", + "years": "1949–2024", + "role": "배우", + "one": "김수미는 대한민국의 배우이다." + }, + { + "name": "문효치", + "title": "문효치", + "years": "1943–", + "role": null, + "one": "문효치는 대한민국 시인이다." + }, + { + "name": "박화요비", + "title": "박화요비", + "years": "1982–", + "role": "가수", + "one": "박화요비 또는 화요비는 대한민국의 가수이다." + }, + { + "name": "송새벽", + "title": "송새벽", + "years": "1979–", + "role": "배우", + "one": "송새벽은 대한민국의 배우이다." + }, + { + "name": "심호택", + "title": "심호택", + "years": "1947–2010", + "role": "시인", + "one": "심호택은 대한민국의 시인이다." + }, + { + "name": "이병훈", + "title": "이병훈 (시인)", + "years": "1925–2009", + "role": "한국시인", + "one": "이병훈은 전라북도 옥구 출생으로 현대 시인이다." + }, + { + "name": "이서원", + "title": "이서원", + "years": "1997–", + "role": null, + "one": "이서원은 대한민국의 前 배우이다." + }, + { + "name": "이연주", + "title": "이연주 (시인)", + "years": "1953–1992", + "role": "시인", + "one": "이연주는 대한민국의 시인이다." + }, + { + "name": "장신영", + "title": "장신영", + "years": "1984–", + "role": "배우", + "one": "장신영은 대한민국의 배우이다." + }, + { + "name": "주하림", + "title": "주하림", + "years": "2009", + "role": "시인", + "one": "주하림(朱夏林은 대한민국의 시인이다." + }, + { + "name": "채만식", + "title": "채만식", + "years": "1902", + "role": "소설가, 극작가, 문학평론가, 수필가", + "one": "채만식 ~ 1950년 6월 11일)은 일제강점기와 대한민국의 소설가, 극작가, 문학평론가, 수필가이다." + }, + { + "name": "하준", + "title": "하준 (가수)", + "years": "1965–", + "role": "가수", + "one": "하준은 대한민국의 가수이다." + }, + { + "name": "강민지", + "title": "강민지 (배우)", + "years": "1996–", + "role": "배우 · 모델", + "one": "강민지는 대한민국의 배우이자 모델이다." + }, + { + "name": "강봉균", + "title": "강봉균 (정치인)", + "years": "1943–2017", + "role": "정치인", + "one": "강봉균은 정보통신부 장관과 재정경제부 장관, 16·17·18대 국회의원을 지낸 대한민국의 정치인이다." + }, + { + "name": "고건", + "title": "고건", + "years": "1938–", + "role": "고위정치인", + "one": "고건은 대한민국의 고위정치인이다." + }, + { + "name": "김관영", + "title": "김관영", + "years": "1969–", + "role": "공인회계사, 공무원, 변호사, 정치인", + "one": "김관영은 대한민국의 공인회계사, 공무원, 변호사, 정치인이다." + }, + { + "name": "김혜선", + "title": "김혜선 (희극인)", + "years": "1983–", + "role": null, + "one": "김혜선은 대한민국의 희극 배우다." + }, + { + "name": "박윤경", + "title": "박윤경 (가수)", + "years": "1970–", + "role": "가수 겸 기업인", + "one": "박윤경은 대한민국의 가수 겸 기업인이다." + }, + { + "name": "병헌", + "title": "병헌", + "years": "1993–", + "role": "배우", + "one": "신성하은 대한민국의 배우이다." + }, + { + "name": "신영대", + "title": "신영대", + "years": "1968–", + "role": "정치인", + "one": "신영대는 대한민국의 정치인이며, 제21·22대 국회의원이다." + }, + { + "name": "양일동", + "title": "양일동", + "years": "1912–1980", + "role": null, + "one": "양일동은 대한민국 정치인이다." + }, + { + "name": "이수현", + "title": "이수현 (1895년)", + "years": "1895–1980", + "role": "독립운동가", + "one": "이수현은 대한민국의 독립운동가이다." + }, + { + "name": "이옥주", + "title": "이옥주 (희극인)", + "years": "1969–", + "role": "희극 배우", + "one": "이옥주는 대한민국의 희극 배우이다." + }, + { + "name": "임병찬", + "title": "임병찬", + "years": "1851–1916", + "role": null, + "one": "임병찬은 최익현과 함께 거병했던 구한말의 의병장이다." + }, + { + "name": "장재호", + "title": "장재호 (배우)", + "years": "1986–", + "role": "배우", + "one": "장재호는 대한민국의 배우이다." + }, + { + "name": "정혁", + "title": "정혁 (모델)", + "years": "1991–", + "role": "남자 모델 겸 방송인", + "one": "정혁은 대한민국의 남자 모델 겸 방송인이다." + }, + { + "name": "채영석", + "title": "채영석", + "years": "1934–2006", + "role": "정치인", + "one": "채영석은 대한민국의 정치인이다." + }, + { + "name": "김광선", + "title": "김광선 (권투 선수)", + "years": "1964–", + "role": "권투 선수, 지도자", + "one": "김광선은 대한민국의 권투 선수, 지도자이며, 1988년 하계 올림픽 남자 복싱 플라이급에서 금메달을 획득했다." + }, + { + "name": "박경완", + "title": "박경완", + "years": "1972–", + "role": null, + "one": "박경완은 전 KBO 리그 SK 와이번스의 포수이자, 전 KBO 리그 LG 트윈스의 배터리코치였다." + }, + { + "name": "박성현", + "title": "박성현 (양궁 선수)", + "years": "1983–", + "role": "종별선수", + "one": "박성현은 대한민국의 전직 양궁 선수이자, 현재 전라북도청의 양궁 감독이다." + }, + { + "name": "오지환", + "title": "오지환", + "years": "1990–", + "role": null, + "one": "오지환은 KBO 리그 LG 트윈스의 내야수이다." + }, + { + "name": "은성수", + "title": "은성수 (공무원)", + "years": "1961–", + "role": null, + "one": "은성수는 대한민국의 제7대 금융위원회 위원장을 역임한 공무원이다." + }, + { + "name": "이길여", + "title": "이길여", + "years": "1932–", + "role": "의료인 · 교육자", + "one": "이길여는 대한민국의 의료인이자 교육자이다." + }, + { + "name": "이진영", + "title": "이진영 (1980년)", + "years": "1980–", + "role": null, + "one": "이진영은 전 KBO 리그 kt 위즈의 외야수, 내야수이자, 현 대한민국 야구 국가대표팀의 QC코치, KBO 리그 두산 베어스의 코치이다." + }, + { + "name": "이천웅", + "title": "이천웅", + "years": "1988–", + "role": null, + "one": "이천웅은 전 KBO 리그 LG 트윈스의 외야수이다." + }, + { + "name": "정명원", + "title": "정명원", + "years": "1966–", + "role": null, + "one": "정명원은 전 KBO 리그 현대 유니콘스의 투수이다." + }, + { + "name": "조계현", + "title": "조계현", + "years": "1964–", + "role": "특임교수", + "one": "조계현은 전 KBO 리그 해태타이거즈의 투수이자, 현 협성대학교의 특임교수이다." + }, + { + "name": "조규제", + "title": "조규제", + "years": "1967–", + "role": null, + "one": "조규제는 전 KBO 리그 KIA 타이거즈의 투수이자, 현 제주고등학교 야구부의 투수코치이다." + }, + { + "name": "차우찬", + "title": "차우찬", + "years": "1987–", + "role": null, + "one": "차우찬은 전 KBO 리그 롯데 자이언츠의 투수로, 현 KBS N 스포츠 야구해설위원이다." + }, + { + "name": "채금석", + "title": "채금석", + "years": "1904–1995", + "role": null, + "one": "채금석은 일제강점기 당시 경성 축구단 소속의 축구 선수이다." + }, + { + "name": "최길선", + "title": "최길선", + "years": "1946–", + "role": "기업인", + "one": "최길선은 현대중공업 대표이사 회장에 재직 중인 대한민국의 기업인이다." + }, + { + "name": "함운경", + "title": "함운경", + "years": "1964–", + "role": "민주화 운동권 학생운동가 출신의 정치", + "one": "함운경은 대한민국의 민주화 운동권 학생운동가 출신의 정치인이다." + }, + { + "name": "김의겸", + "title": "김의겸", + "years": "1963–", + "role": "기자 출신 정치인", + "one": "김의겸은 대한민국의 기자 출신 정치인이다." + }, + { + "name": "노상래", + "title": "노상래", + "years": "1970", + "role": "전 축구 선수 · 현 지도자", + "one": "노상래는 대한민국의 전 축구 선수이자 현 지도자로 현역 시절 포지션은 공격수였으며 현재 울산 HD의 유소년 디렉터와 대한축구협회 기술발전위원을 맡고 있다." + }, + { + "name": "노수진", + "title": "노수진", + "years": "1962–", + "role": "전 축구 선수 및 지도자", + "one": "노수진은 대한민국의 전 축구 선수 및 지도자이다." + }, + { + "name": "문호준", + "title": "문호준", + "years": "1997–", + "role": "전직 카트라이더 프로게이머", + "one": "문호준은 대한민국의 전직 카트라이더 프로게이머로 아이디는 HoJun을 사용했다." + }, + { + "name": "반석평", + "title": "반석평", + "years": "1472–1540", + "role": null, + "one": "반석평은 조선의 문신이다." + }, + { + "name": "유동우", + "title": "유동우", + "years": "1968–", + "role": "축구 선수, 지도자", + "one": "유동우는 대한민국의 축구 선수, 지도자로 선수 시절 수비수로 활동했다." + }, + { + "name": "이정효", + "title": "이정효", + "years": "1975–", + "role": "축구 선수 출신 축구 감독으", + "one": "이정효는 대한민국의 축구 선수 출신 축구 감독으로 현재 수원 삼성 블루윙즈의 감독을 맡고있다." + }, + { + "name": "전미라", + "title": "전미라", + "years": "1978–", + "role": "전직 테니스 선수", + "one": "전미라는 대한민국의 전직 테니스 선수이다." + }, + { + "name": "전진", + "title": "전진 (1923년)", + "years": "1923–2012", + "role": "비전향 장기수", + "one": "전진은 대한민국의 비전향 장기수이다." + }, + { + "name": "정경미", + "title": "정경미 (유도 선수)", + "years": "1985–", + "role": "유도 선수", + "one": "정경미는 대한민국의 유도 선수이다." + }, + { + "name": "최서우", + "title": "최서우", + "years": "1982–", + "role": "스키 점프 선수", + "one": "최서우는 대한민국의 스키 점프 선수이다." + }, + { + "name": "한왕용", + "title": "한왕용", + "years": "1966–", + "role": null, + "one": "한왕용은 히말라야 8000미터급 14좌를 세계에서 11번째, 국내에서 3번째로 완등한 대한민국 산악인이다." + } +] \ No newline at end of file diff --git a/solution/site/scripts/mockup/rails-test.mjs b/solution/site/scripts/mockup/rails-test.mjs new file mode 100644 index 0000000..f6e5255 --- /dev/null +++ b/solution/site/scripts/mockup/rails-test.mjs @@ -0,0 +1,111 @@ +import {chromium, devices} from 'playwright'; +const url = process.argv[2] || 'http://localhost/s/stay'; +const b = await chromium.launch({channel: 'chrome'}); + +const prep = async (page) => page.evaluate(() => { + window.__rails = []; + document.querySelectorAll('#root .slider-viewport').forEach((el) => window.__rails.push(el)); + document.querySelectorAll('#root *').forEach((el) => { + if (el.classList.contains('slider-viewport')) return; + const f = getComputedStyle(el).overflowX; + if ((f === 'auto' || f === 'scroll') && el.scrollWidth - el.clientWidth > 40) window.__rails.push(el); + }); + window.__pos = (i) => { const r = window.__rails[i]; const c = r.firstElementChild; let m = 0; + try { m = new DOMMatrix(getComputedStyle(c).transform).m41; } catch {} return Math.round(r.scrollLeft + m); }; + window.__info = (i) => { const r = window.__rails[i]; const b = r.getBoundingClientRect(); + return {label: r.getAttribute('aria-label') || '?', vis: !!r.getClientRects().length, + cx: Math.round(b.left + b.width / 2), cy: Math.round(b.top + b.height / 2)}; }; + return window.__rails.length; +}); + +// ── 데스크톱 ─────────────────────────────────────────────── +const page = await b.newPage({viewport: {width: 1280, height: 900}}); +await page.goto(url, {waitUntil: 'domcontentloaded'}); +await page.waitForTimeout(6000); +const n = await prep(page); +const rows = []; +for (let i = 0; i < n; i++) { + let info = await page.evaluate((k) => window.__info(k), i); + if (!info.vis) { rows.push({레일: info.label, 판정: '숨은 탭'}); continue; } + await page.mouse.move(5, 5); + await page.evaluate((k) => window.__rails[k].scrollIntoView({block: 'center'}), i); + await page.waitForTimeout(9000); + info = await page.evaluate((k) => window.__info(k), i); + const y = Math.min(Math.max(info.cy, 100), 800); + + const p0 = await page.evaluate((k) => window.__pos(k), i); + await page.waitForTimeout(10000); // ① 가만히 + const idle = await page.evaluate(([k, p]) => window.__pos(k) - p, [i, p0]); + + await page.mouse.move(info.cx, y); // ② 레일 위 세로 휠 + await page.mouse.wheel(0, 150); + await page.mouse.move(5, 5); // 커서는 뺀다(호버 정지는 별개) + const p1 = await page.evaluate((k) => window.__pos(k), i); + await page.waitForTimeout(4000); + const during = await page.evaluate(([k, p]) => window.__pos(k) - p, [i, p1]); + await page.waitForTimeout(9000); + const after = await page.evaluate(([k, p]) => window.__pos(k) - p, [i, p1]); + + const i2 = await page.evaluate((k) => window.__info(k), i); // ③ 손으로 민다 + const y2 = Math.min(Math.max(i2.cy, 100), 800); + await page.mouse.move(i2.cx + 180, y2); + await page.mouse.down(); + for (let s = 1; s <= 10; s++) { await page.mouse.move(i2.cx + 180 - s * 24, y2); await page.waitForTimeout(16); } + await page.mouse.up(); + await page.mouse.move(5, 5); + await page.waitForTimeout(1500); + const dragged = await page.evaluate((k) => window.__pos(k), i); + await page.waitForTimeout(13000); + const frozen = await page.evaluate(([k, p]) => window.__pos(k) - p, [i, dragged]); + + rows.push({레일: info.label, '①가만히': idle, '②휠직후': during, '②13초뒤': after, '③민뒤13초': frozen, + 판정: (idle !== 0 && during === 0 && after !== 0 && frozen === 0) ? 'OK' : '확인'}); +} +console.log('── 데스크톱 (1280) ──'); console.table(rows); +await page.close(); + +// ── 모바일 ─────────────────────────────────────────────── +const ctx = await b.newContext({...devices['iPhone 13'], hasTouch: true, isMobile: true}); +const mp = await ctx.newPage(); +await mp.goto(url, {waitUntil: 'domcontentloaded'}); +await mp.waitForTimeout(6000); +const mn = await prep(mp); +const cdp = await ctx.newCDPSession(mp); +const mrows = []; +for (let i = 0; i < mn; i++) { + let info = await mp.evaluate((k) => window.__info(k), i); + if (!info.vis) { mrows.push({레일: info.label, 판정: '숨은 탭'}); continue; } + await mp.evaluate((k) => window.__rails[k].scrollIntoView({block: 'center'}), i); + await mp.waitForTimeout(9000); + info = await mp.evaluate((k) => window.__info(k), i); + const x = info.cx, y = Math.min(Math.max(info.cy, 120), 620); + const pt = (type, yy, xx) => cdp.send('Input.dispatchTouchEvent', + {type, touchPoints: type === 'touchEnd' ? [] : [{x: xx ?? x, y: yy ?? y}]}); + + const p0 = await mp.evaluate((k) => window.__pos(k), i); + await mp.waitForTimeout(10000); // ① 가만히 + const idle = await mp.evaluate(([k, p]) => window.__pos(k) - p, [i, p0]); + + const p1 = await mp.evaluate((k) => window.__pos(k), i); // ② 손가락 대고 11초 + await pt('touchStart'); + for (let s = 0; s < 11; s++) { await mp.waitForTimeout(1000); await pt('touchMove', y - s * 2); } + const held = await mp.evaluate(([k, p]) => window.__pos(k) - p, [i, p1]); + await pt('touchEnd'); + await mp.waitForTimeout(11000); + const released = await mp.evaluate(([k, p]) => window.__pos(k) - p, [i, p1]); + + const i2 = await mp.evaluate((k) => window.__info(k), i); // ③ 가로 스와이프 + const y2 = Math.min(Math.max(i2.cy, 120), 620); + await pt('touchStart', y2, i2.cx + 120); + for (let s = 1; s <= 8; s++) { await pt('touchMove', y2, i2.cx + 120 - s * 22); await mp.waitForTimeout(20); } + await pt('touchEnd'); + await mp.waitForTimeout(1500); + const dragged = await mp.evaluate((k) => window.__pos(k), i); + await mp.waitForTimeout(13000); + const frozen = await mp.evaluate(([k, p]) => window.__pos(k) - p, [i, dragged]); + + mrows.push({레일: info.label, '①가만히': idle, '②손댄채11초': held, '②뗀뒤11초': released, '③민뒤13초': frozen, + 판정: (idle !== 0 && held === 0 && released !== 0 && frozen === 0) ? 'OK' : '확인'}); +} +console.log('── 모바일 (iPhone 13) ──'); console.table(mrows); +await b.close(); diff --git a/solution/site/scripts/mockup/rooms.json b/solution/site/scripts/mockup/rooms.json new file mode 100644 index 0000000..7eca297 --- /dev/null +++ b/solution/site/scripts/mockup/rooms.json @@ -0,0 +1,66 @@ +{ + "B동": { + "subTitle": "(모던한 현대식 컨셉으로 꾸며진 따뜻한 공간)", + "infos": [ + "기준 2명 / 최대 4명", + "킹 침대 1개", + "금연객실" + ], + "photos": [ + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af2f19c81.25015124.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af397e9d4.68027694.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af3682927.79759196.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af312bf49.58781436.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af3272203.12163270.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af33cdad5.42938273.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af3809799.93524916.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af353ce48.12311777.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af3afee39.79745096.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af2dbc5b2.24229660.jpg" + ] + }, + "A동": { + "subTitle": "일본식 가정집 느낌으로 디딤돌마당과 예쁜정원", + "infos": [ + "기준 2명 / 최대 4명 | 52m²", + "퀸 침대 1개", + "금연객실" + ], + "photos": [ + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38ac4b11857.68455813.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38ac457cdf7.96020058.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38ac46d7ba5.10027562.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38ac483e6e4.98042378.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38ac3d39471.34452263.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38ac3eec2d5.01012597.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38ac412b973.03778247.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38ac4292315.62625559.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38ac49c67d4.10748755.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38ac4403cf8.84927578.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38ac3baabd6.33353455.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38ac4c49626.22997229.jpg" + ] + }, + "_etc": [ + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af2f19c81.25015124.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af397e9d4.68027694.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af3682927.79759196.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af312bf49.58781436.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af3272203.12163270.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af33cdad5.42938273.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af3809799.93524916.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af353ce48.12311777.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af3afee39.79745096.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af2dbc5b2.24229660.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af2f19c81.25015124.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af397e9d4.68027694.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af3682927.79759196.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af312bf49.58781436.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af3272203.12163270.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af33cdad5.42938273.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af3809799.93524916.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af353ce48.12311777.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af3afee39.79745096.jpg", + "https://yaimg.yanolja.com/v5/2026/03/25/07/1280/69c38af2dbc5b2.24229660.jpg" + ] +} \ No newline at end of file diff --git a/solution/site/scripts/mockup/stay-payload-new.json b/solution/site/scripts/mockup/stay-payload-new.json new file mode 100644 index 0000000..11ba0f8 --- /dev/null +++ b/solution/site/scripts/mockup/stay-payload-new.json @@ -0,0 +1 @@ +{"schemaVersion": 1, "site": {"siteId": "e929e6b4-e502-4761-9786-0bb82178b375", "placeId": "66894a1b-a331-43f1-942c-0ae064a704d6", "status": 3, "origin": "https://web4ai.o2osolution.ai", "basePath": "/s/stay", "slug": "stay", "publishedAt": "2026-09-03T01:27:39.284442+00:00", "updatedAt": "2026-09-09T13:40:00+00:00", "version": 11}, "place": {"name": "스테이,머뭄", "category": 1, "roadAddress": "전북 군산시 절골길 18", "address": "전북 군산시 신흥동 63-18", "addressRegion": "전북", "addressLocality": "군산시", "addressSubLocality": null, "addressRegionCode": "KR-45", "phone": "0507-1497-0983", "latitude": 35.9864175, "longitude": 126.7061254, "regionCode": "52군산시", "kakaoPlaceId": null}, "facts": [{"key": "parking", "label": "주차 가능", "value": "true", "unit": null, "type": "bool", "scope": "place", "status": 3, "sourceType": 3, "sourceUrl": "https://m.place.naver.com/place/1133638931/home", "critical": false, "required": false, "unitId": null, "collectedAt": "2026-08-28T02:10:08.678003+00:00", "verifiedAt": "2026-08-28T02:12:21.402109+00:00"}, {"key": "intro", "label": "숙소 소개", "value": "1920년대에 지어진 백 년 고택을 리모델링한 독채 펜션입니다. 히로쓰 가옥 후문 바로 옆이고, 침구는 매일 세탁·살균·건조합니다.", "unit": null, "type": "text", "scope": "place", "status": 3, "sourceType": 3, "sourceUrl": "https://m.place.naver.com/place/1133638931/home", "critical": false, "required": false, "unitId": null, "collectedAt": "2026-08-28T02:10:08.547669+00:00", "verifiedAt": "2026-08-28T02:12:21.922925+00:00"}, {"key": "wifi", "label": "와이파이", "value": "true", "unit": null, "type": "bool", "scope": "place", "status": 3, "sourceType": 3, "sourceUrl": "https://m.place.naver.com/place/1133638931/home", "critical": false, "required": false, "unitId": null, "collectedAt": "2026-08-28T02:10:08.757070+00:00", "verifiedAt": "2026-08-28T02:12:22.356600+00:00"}, {"key": "check_out_time", "label": "체크아웃 시간", "value": "11:00", "unit": null, "type": "time", "scope": "place", "status": 3, "sourceType": 3, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": true, "required": true, "unitId": null, "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T13:27:53.241391+00:00"}, {"key": "cancel_policy", "label": "취소·환불 규정", "value": "입실 당일 취소와 노쇼(No Show)는 숙박요금의 100%가 부과되어 환불되지 않습니다. 그 이전 취소 수수료는 예약하신 채널의 취소 규정을 따릅니다.", "unit": null, "type": "text", "scope": "place", "status": 3, "sourceType": 3, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": true, "required": true, "unitId": null, "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T13:27:53.241391+00:00"}, {"key": "cooking_allowed", "label": "취사 가능", "value": "true", "unit": null, "type": "bool", "scope": "place", "status": 3, "sourceType": 3, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": true, "required": true, "unitId": null, "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T13:27:53.241391+00:00"}, {"key": "pet_allowed", "label": "반려동물 동반", "value": "false", "unit": null, "type": "bool", "scope": "place", "status": 3, "sourceType": 3, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": true, "required": true, "unitId": null, "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T13:27:53.241391+00:00"}, {"key": "smoking", "label": "흡연 가능", "value": "false", "unit": null, "type": "bool", "scope": "place", "status": 3, "sourceType": 3, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": true, "required": false, "unitId": null, "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T13:27:53.241391+00:00"}, {"key": "extra_person_fee", "label": "인원 추가 요금", "value": "20000", "unit": "원", "type": "number", "scope": "place", "status": 3, "sourceType": 3, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": true, "required": false, "unitId": null, "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T13:27:53.241391+00:00"}, {"key": "check_in_time", "label": "체크인 시간", "value": "15:00", "unit": null, "type": "time", "scope": "place", "status": 3, "sourceType": 1, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": true, "required": true, "unitId": null, "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T23:31:05.719599+00:00"}, {"key": "pickup_service", "label": "픽업 서비스", "value": "false", "type": "bool", "scope": "place", "unit": null, "status": 3, "sourceType": 3, "sourceUrl": "https://m.place.naver.com/place/1133638931/home", "critical": false, "required": false, "unitId": null, "collectedAt": "2026-09-04T00:00:00+00:00", "verifiedAt": "2026-09-04T00:00:00+00:00"}, {"key": "bbq_available", "label": "바비큐 이용", "value": "false", "type": "bool", "scope": "place", "unit": null, "status": 3, "sourceType": 3, "sourceUrl": "https://m.place.naver.com/place/1133638931/home", "critical": false, "required": false, "unitId": null, "collectedAt": "2026-09-04T00:00:00+00:00", "verifiedAt": "2026-09-04T00:00:00+00:00"}], "units": [{"unitId": "71dd18bd-8bd0-4e46-a4b2-419b972d6175", "name": "A동(일본식 가정집 느낌으로 디딤돌마당과 예쁜정원)", "slug": "a동", "sortOrder": 0, "facts": [{"key": "room_type", "label": "객실 타입", "value": "독채", "unit": null, "type": "text", "scope": "unit", "status": 3, "sourceType": 3, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": false, "required": true, "unitId": "71dd18bd-8bd0-4e46-a4b2-419b972d6175", "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T13:27:53.241391+00:00"}, {"key": "standard_capacity", "label": "기준 인원", "value": "2", "unit": "명", "type": "number", "scope": "unit", "status": 3, "sourceType": 3, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": true, "required": true, "unitId": "71dd18bd-8bd0-4e46-a4b2-419b972d6175", "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T13:27:53.241391+00:00"}, {"key": "max_capacity", "label": "최대 인원", "value": "4", "unit": "명", "type": "number", "scope": "unit", "status": 3, "sourceType": 3, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": true, "required": true, "unitId": "71dd18bd-8bd0-4e46-a4b2-419b972d6175", "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T13:27:53.241391+00:00"}, {"key": "weekday_price", "label": "주중 요금", "value": "198000", "unit": "원", "type": "number", "scope": "unit", "status": 3, "sourceType": 3, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": true, "required": false, "unitId": "71dd18bd-8bd0-4e46-a4b2-419b972d6175", "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T13:27:53.241391+00:00"}, {"key": "room_intro", "label": "객실 소개", "value": "편안한 일본집에 와 있는 듯한 느낌의 공간입니다. 독립된 아담한 정원과 창고형 CAFÉ 공간이 있습니다.", "unit": null, "type": "text", "scope": "unit", "status": 3, "sourceType": 3, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": false, "required": false, "unitId": "71dd18bd-8bd0-4e46-a4b2-419b972d6175", "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T13:27:53.241391+00:00"}, {"key": "price_range", "label": "요금", "value": "198,000 ~ 350,000원", "type": "text", "scope": "unit", "unit": null, "status": 3, "sourceType": 3, "sourceUrl": "https://m.place.naver.com/place/1133638931/home", "critical": true, "required": false, "unitId": "71dd18bd-8bd0-4e46-a4b2-419b972d6175", "collectedAt": "2026-09-04T00:00:00+00:00", "verifiedAt": "2026-09-04T00:00:00+00:00"}], "mediaIds": ["1330beb8-09d3-47fc-a410-f2e9598d13fe", "ad35769c-5299-4d39-b80d-5d88f1e1de14", "2fbcd37e-260f-4a4f-b00f-3e997e978c02", "d95857a0-44ad-422b-86de-00dee8aca08f"]}, {"unitId": "916dc29b-241c-41a8-90de-51c3a9ddb947", "name": "B동(모던한 현대식 컨셉으로 꾸며진 따뜻한 공간)", "slug": "b동", "sortOrder": 1, "facts": [{"key": "room_type", "label": "객실 타입", "value": "독채", "unit": null, "type": "text", "scope": "unit", "status": 3, "sourceType": 3, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": false, "required": true, "unitId": "916dc29b-241c-41a8-90de-51c3a9ddb947", "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T13:27:53.241391+00:00"}, {"key": "standard_capacity", "label": "기준 인원", "value": "2", "unit": "명", "type": "number", "scope": "unit", "status": 3, "sourceType": 3, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": true, "required": true, "unitId": "916dc29b-241c-41a8-90de-51c3a9ddb947", "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T13:27:53.241391+00:00"}, {"key": "max_capacity", "label": "최대 인원", "value": "4", "unit": "명", "type": "number", "scope": "unit", "status": 3, "sourceType": 3, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": true, "required": true, "unitId": "916dc29b-241c-41a8-90de-51c3a9ddb947", "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T13:27:53.241391+00:00"}, {"key": "weekday_price", "label": "주중 요금", "value": "198000", "unit": "원", "type": "number", "scope": "unit", "status": 3, "sourceType": 3, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": true, "required": false, "unitId": "916dc29b-241c-41a8-90de-51c3a9ddb947", "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T13:27:53.241391+00:00"}, {"key": "room_intro", "label": "객실 소개", "value": "아늑하고 깔끔한 현대식 모던 스타일 공간입니다. 독립된 아담한 정원과 창고형 CAFÉ 공간이 있습니다.", "unit": null, "type": "text", "scope": "unit", "status": 3, "sourceType": 3, "sourceUrl": "https://travel.interpark.com/checkinnow/goods/GY0002361440", "critical": false, "required": false, "unitId": "916dc29b-241c-41a8-90de-51c3a9ddb947", "collectedAt": "2026-09-02T13:27:53.241391+00:00", "verifiedAt": "2026-09-02T13:27:53.241391+00:00"}, {"key": "price_range", "label": "요금", "value": "198,000 ~ 350,000원", "type": "text", "scope": "unit", "unit": null, "status": 3, "sourceType": 3, "sourceUrl": "https://m.place.naver.com/place/1133638931/home", "critical": true, "required": false, "unitId": "916dc29b-241c-41a8-90de-51c3a9ddb947", "collectedAt": "2026-09-04T00:00:00+00:00", "verifiedAt": "2026-09-04T00:00:00+00:00"}], "mediaIds": ["5b0adc6e-f03b-459d-9b0b-d0748f1e33d0", "0e5a2de5-384d-41fc-9ff4-258f9caa6b2c", "4981b572-10e2-4138-ba1d-a5d7defb4b4a", "83a2543f-4366-4427-b694-a7b1968a61cf"]}], "media": [{"mediaId": "1330beb8-09d3-47fc-a410-f2e9598d13fe", "url": "/assets/mirror/ddc9f804f4ef83b5.jpg", "alt": "목재 서까래 천장과 미닫이창, 테이블이 있는 거실 공간", "category": "거실", "width": null, "height": null, "isPrimary": true, "sourceType": 3, "originUrl": "https://naverbooking-phinf.pstatic.net/20240514_189/1715688030436xT14o_JPEG/1.jpg", "unitId": null}, {"mediaId": "ad35769c-5299-4d39-b80d-5d88f1e1de14", "url": "/assets/mirror/6d0c447b6a0c32af.jpg", "alt": "소파와 탁자, 벽난로 조명이 있는 서까래 천장의 거실", "category": "거실", "width": null, "height": null, "isPrimary": false, "sourceType": 3, "originUrl": "https://naverbooking-phinf.pstatic.net/20240514_48/1715688030574wTtQd_JPEG/2.jpg", "unitId": null}, {"mediaId": "83a2543f-4366-4427-b694-a7b1968a61cf", "url": "/assets/mirror/2d94158a48480fcd.jpg", "alt": "싱크대와 냉장고, 전자레인지가 구비된 실내 주방 공간", "category": "주방", "width": null, "height": null, "isPrimary": false, "sourceType": 3, "originUrl": "https://naverbooking-phinf.pstatic.net/20240514_92/17156880307484bvpH_JPEG/3.jpg", "unitId": null}, {"mediaId": "990a89ed-9517-426c-8c17-fd9bf3c1a44d", "url": "/assets/mirror/d364f605223524bd.jpg", "alt": "주방 싱크대 앞에서 포즈를 취하고 있는 여성의 모습", "category": "주방", "width": null, "height": null, "isPrimary": false, "sourceType": 3, "originUrl": "https://naverbooking-phinf.pstatic.net/20240514_7/1715688031000y8Y5q_JPEG/4.jpg", "unitId": null}, {"mediaId": "5b0adc6e-f03b-459d-9b0b-d0748f1e33d0", "url": "/assets/mirror/e0ed8d1a27e09320.jpg", "alt": "벽면에 설치된 그림 화면과 벽난로 조명 장식", "category": "거실", "width": null, "height": null, "isPrimary": false, "sourceType": 3, "originUrl": "https://video-phinf.pstatic.net/20241223_179/1734893614235FhY4h_JPEG/PXr9o3iPYJ_03.jpg", "unitId": null}, {"mediaId": "2fbcd37e-260f-4a4f-b00f-3e997e978c02", "url": "/assets/mirror/aa69a633f8e3a631.jpg", "alt": "창밖으로 조명을 비춘 정원 나무와 석등이 보이는 풍경", "category": "전망", "width": null, "height": null, "isPrimary": false, "sourceType": 3, "originUrl": "https://pup-review-phinf.pstatic.net/MjAyNjA4MDNfMjUw/MDAxNzg1NzYwMjA0NTIy.XcTmqR10iQ8w9sFRfFRWo7CfQtNOoT6aRMo0VErTJLkg.EX6iDVemzmjovYRBJwJKdVWWipxu_JOixkmW1_APE_cg.JPEG/8034BC2C-FC29-4480-8BEC-40F0BED744CB.jpeg", "unitId": null}, {"mediaId": "d95857a0-44ad-422b-86de-00dee8aca08f", "url": "/assets/mirror/7095220ca156e25e.jpg", "alt": "목재 서까래 천장에 설치된 조형적인 디자인의 조명 기구", "category": "거실", "width": null, "height": null, "isPrimary": false, "sourceType": 3, "originUrl": "https://pup-review-phinf.pstatic.net/MjAyNjA4MDNfMjA3/MDAxNzg1NzYwMjA0OTI5.uMbalnmqu8lUYeUCS26c2luQ_pvQubkznSFblDcSV6Eg.gaRskjAm5Rud_L9lYS3xIQvd2m8-0R75DF6WJg2asrog.JPEG/628C312C-B7CA-4178-9B10-855CB16A9889.jpeg", "unitId": null}, {"mediaId": "0e5a2de5-384d-41fc-9ff4-258f9caa6b2c", "url": "/assets/mirror/caa5e076e7d56ec2.jpg", "alt": "실내 벽면 홈 공간에 놓인 흰색 화병과 식물 장식", "category": "거실", "width": null, "height": null, "isPrimary": false, "sourceType": 3, "originUrl": "https://pup-review-phinf.pstatic.net/MjAyNjA4MDNfMTg2/MDAxNzg1NzYwMjA0NjMw.5Ct417Oiyb5wlINojIkcVMLUy0QNbPoh8FhXvLT0Gcwg.QHSM9xd3ri3P9HaSeUWES1x8RMFcc-GpmpJTgoMaQkgg.JPEG/EEE3CB7C-55A9-4AC1-9574-D703537FF1C8.jpeg", "unitId": null}, {"mediaId": "4981b572-10e2-4138-ba1d-a5d7defb4b4a", "url": "/assets/mirror/8e2ec99c433b6fe8.jpg", "alt": "실내 창문을 통해 바라본 푸른 하늘과 푸른 나무 풍경", "category": "전망", "width": null, "height": null, "isPrimary": false, "sourceType": 3, "originUrl": "https://pup-review-phinf.pstatic.net/MjAyNjA4MDNfOTMg/MDAxNzg1NzYwMjA1MTU1.Ulli7HXvsNrTkk_i3QnJknX7WBheTrnJRF6ARjyfQ14g.Vp9808LNn4hDRPFja1prpM7wuvVPBhqjpZZlec6Q-fMg.JPEG/F51B7821-B061-4173-BD07-E914E00B92AD.jpeg", "unitId": null}], "faqs": [{"faqId": "8d48a94d-1a19-456f-b1a8-4144ddcb2186", "question": "체크인과 체크아웃 시간은 언제인가요?", "answer": "체크인은 15:00부터이며, 체크아웃은 11:00까지입니다.", "status": 3, "sourceType": 4, "sortOrder": 0}, {"faqId": "8f14678b-3589-42e9-8896-d214c0093784", "question": "객실 기준 인원과 최대 인원, 인원 추가 요금은 어떻게 되나요?", "answer": "A동과 B동 모두 기준 인원은 2명이며, 최대 4명까지 투숙하실 수 있습니다. 인원 추가 요금은 20,000원입니다.", "status": 3, "sourceType": 4, "sortOrder": 1}, {"faqId": "a13785c0-a627-44b5-b593-fac39712d6d4", "question": "객실 내 취사가 가능한가요?", "answer": "주방에서 간단한 조리는 가능합니다. 다만 생선구이·고기구이·튀김처럼 냄새가 잘 빠지지 않는 조리는 하실 수 없고, 객실 안에서 직화로 굽는 방식도 허용되지 않습니다. 그릴·숯·전기 전열기구 등 개인 취사도구는 반입이 금지되어 있습니다.", "status": 3, "sourceType": 4, "sortOrder": 2}, {"faqId": "0eb535db-135f-484b-b0a4-1323744ace75", "question": "주차와 와이파이 이용이 가능한가요?", "answer": "네, 주차와 와이파이 모두 이용 가능합니다.", "status": 3, "sourceType": 4, "sortOrder": 3}, {"faqId": "cc268677-ad26-4685-a6a6-10a293658591", "question": "반려동물 동반이나 실내 흡연이 가능한가요?", "answer": "반려동물 동반은 불가하며, 전 구역 흡연이 불가능합니다.", "status": 3, "sourceType": 4, "sortOrder": 4}, {"faqId": "4b8ac158-7648-4b2c-aa1a-9c6ca99b1223", "question": "A동과 B동은 무엇이 다른가요?", "answer": "A동은 편안한 일본집에 와 있는 듯한 느낌의 공간이고, B동은 아늑하고 깔끔한 현대식 모던 스타일 공간입니다. 두 동 모두 독립된 아담한 정원과 창고형 CAFÉ 공간이 딸려 있으며, 기준 2인 · 최대 4인으로 같습니다. 침대 구성 등 객실 안 자세한 사항은 예약 채널이나 전화로 확인해 주세요.", "status": 3, "sourceType": 4, "sortOrder": 5}, {"faqId": "8bd75b48-49c3-475f-99c1-f5e64f49bedd", "question": "객실 요금은 얼마인가요?", "answer": "A동과 B동 모두 198,000원 ~ 350,000원입니다. 날짜와 시즌에 따라 달라지므로 정확한 금액은 예약 채널에서 확인해 주세요. 기준 인원(2인)을 넘으면 1인당 20,000원이 더해집니다.", "status": 3, "sourceType": 4, "sortOrder": 6}, {"faqId": "7b2bbfeb-761c-4a15-b410-43e18a69bb9e", "question": "예약 취소 및 환불 규정은 어떻게 되나요?", "answer": "입실 당일 취소와 노쇼(No Show)의 경우 숙박요금의 100%가 부과되어 환불되지 않습니다. 그 이전 취소 수수료는 예약하신 채널의 취소 규정을 따릅니다.", "status": 3, "sourceType": 4, "sortOrder": 7}, {"faqId": "20e80f4d-322a-46ac-90b1-21fb6669b6b0", "question": "스테이,머뭄은 어떤 숙소인가요?", "answer": "스테이,머뭄은 전북 군산시 절골길 18(신흥동)에 있는 독채 숙소입니다. A동은 일본식 가정집 느낌, B동은 현대식 모던 스타일로 두 동이 있고, 편안한 침구와 좋은 위치를 내세운 곳입니다.", "status": 3, "sourceType": 4, "sortOrder": 8}, {"faqId": "724aeac2-46b1-4d6d-9748-892b5988a178", "question": "군산 독채 펜션을 찾는데, 여기도 독채인가요?", "answer": "네, 스테이,머뭄은 A동과 B동 모두 독채입니다. 동마다 독립된 정원과 창고형 카페 공간이 딸려 있고, 주방이 있어 취사도 가능합니다.", "status": 3, "sourceType": 4, "sortOrder": 9}, {"faqId": "a50f8b18-1a2b-4025-ab0c-00d8ebcc9e8f", "question": "위치가 어디인가요?", "answer": "스테이,머뭄은 전북 군산시 절골길 18(신흥동 63-18)에 있습니다. 바로 옆 13m 거리에 군산 신흥동 일본식가옥(히로쓰 가옥)이 있는 원도심 골목 안입니다. 문의는 0507-1497-0983입니다.", "status": 3, "sourceType": 4, "sortOrder": 10}, {"faqId": "7bac199e-f095-4b44-acf6-c94d68181fc8", "question": "예약은 어디서 하나요?", "answer": "스테이,머뭄은 네이버 플레이스에서 숙소 정보를 확인하실 수 있고, 전화 0507-1497-0983으로도 문의하실 수 있습니다. 입실 당일 취소와 노쇼는 숙박요금의 100%가 부과되니 일정을 확인하고 예약해 주세요.", "status": 3, "sourceType": 4, "sortOrder": 11}, {"faqId": "2059ea18-e444-4b2f-97df-ac8ada6e72b0", "question": "커플 둘이 묵기 좋은 2인 독채인가요?", "answer": "네, 스테이,머뭄 A동과 B동 모두 기준 인원 2인의 독채입니다. 두 동을 각각 한 팀만 쓰고, 독립된 아담한 정원과 창고형 CAFÉ 공간이 딸려 있습니다.", "status": 3, "sourceType": 4, "sortOrder": 12}, {"faqId": "b3950b7b-935d-47a5-8119-67afb028a5ce", "question": "일본식 가옥 같은 숙소를 찾는데 여기가 맞나요?", "answer": "스테이,머뭄 A동이 일본식 가정집 느낌으로 꾸민 독채입니다. 디딤돌 마당과 독립된 정원, 창고형 카페 공간이 딸려 있습니다. B동은 같은 마당 안에 있는 현대식 모던 스타일 독채입니다.", "status": 3, "sourceType": 4, "sortOrder": 13}, {"faqId": "78fcc01a-099f-4e7d-a731-d93d18573f55", "question": "조용하고 프라이빗한 숙소인가요?", "answer": "스테이,머뭄은 A동·B동 두 동뿐인 독채 숙소라 다른 손님과 건물을 나눠 쓰지 않습니다. 두 동 모두 독립된 정원을 따로 두고 있고, 전 구역 금연입니다.", "status": 3, "sourceType": 4, "sortOrder": 14}, {"faqId": "feb316f6-f0b5-419c-83e1-45fdec718024", "question": "차 없이 걸어서 근대문화거리까지 갈 수 있나요?", "answer": "스테이,머뭄에서 히로쓰 가옥은 13m, 초원사진관 248m, 군산 영화의 거리 318m, 군산근대미술관 708m, 옛 군산세관 728m, 근대건축관(구 조선은행 군산지점) 788m 거리입니다. 원도심 근대 건축물이 숙소를 중심으로 1km 안에 모여 있습니다. 주차도 가능합니다.", "status": 3, "sourceType": 4, "sortOrder": 15}, {"faqId": "681b921e-d15f-48af-8378-21e7754f500f", "question": "이성당이나 근처 맛집까지 얼마나 되나요?", "answer": "스테이,머뭄에서 이성당까지는 455m입니다. 그 밖에 요리주점 도란 135m, 베이커리 물밀소 297m, 백년가게 국밥집 일흥옥 300m, 명월갈비 351m, 꽃게장 한주옥 430m로 모두 가까이 있습니다.", "status": 3, "sourceType": 4, "sortOrder": 16}, {"faqId": "92c2e8b2-4a62-4b11-8898-7d75fd6bc6f7", "question": "마당이나 정원이 따로 있나요?", "answer": "네. A동과 B동 각각 독립된 아담한 정원과 창고형 CAFÉ 공간이 있습니다. A동에는 디딤돌 마당도 있습니다. 바다 전망은 아닙니다 — 마당과 거실에서 새소리를 들으며 쉬는 숙소입니다.", "status": 3, "sourceType": 4, "sortOrder": 17}, {"faqId": "2c5cfea9-e3c7-42e0-8353-87e5538f3f4e", "question": "선유도에 갈 건데 여기서 자도 될까요?", "answer": "네, 스테이,머뭄에서는 다리를 건너 고군산군도 선유도까지 차로 약 45분 걸립니다. 첫날은 군산 도심에서 저녁과 내항 밤 산책을, 이튿날 선유도와 장자도를 도는 1박 2일 코스를 추천 일정으로 안내하고 있습니다.", "status": 3, "sourceType": 4, "sortOrder": 18}, {"faqId": "bab88a71-1cce-40cb-ad7a-9e8d1fdd5ff9", "question": "1박 2일이면 어떻게 돌면 좋을까요?", "answer": "스테이,머뭄이 안내하는 1박 2일 코스는 첫날 오후 체크인 후 빈해원에서 저녁, 군산 내항 밤 산책으로 마무리하고, 이튿날 선유도와 장자도를 도는 일정입니다. 걷는 코스를 원하시면 채만식문학관·내항 부잔교·째보선창·초원사진관·이성당으로 이어지는 반나절 탁류길 코스도 있습니다.", "status": 3, "sourceType": 4, "sortOrder": 19}, {"faqId": "55288610-291a-4feb-95d8-29e3b4b605d7", "question": "군산이 처음인데 어디부터 보면 좋을까요?", "answer": "스테이,머뭄은 처음 오신 손님께 반나절 개항장 골목 코스를 안내합니다. 군산근대역사박물관에서 시작해 초원사진관, 이성당, 경암동 철길마을 순으로 도는 일정으로, 걷는 거리가 가장 짧은 코스입니다.", "status": 3, "sourceType": 4, "sortOrder": 20}, {"faqId": "a8fdcd07-70d4-4274-ac6f-1d0067beec2a", "question": "비 오는 날에는 뭘 하면 좋을까요?", "answer": "스테이,머뭄은 비 오는 날을 위한 실내 코스를 따로 안내합니다. 군산근대건축관에서 옛 은행 건물 전시를 보고, 한일옥에서 무국으로 몸을 데운 뒤, 째보선창 근처 카페에서 비가 그칠 때까지 앉아 있는 코스입니다. 숙소는 주방이 있어 취사도 가능합니다.", "status": 3, "sourceType": 4, "sortOrder": 21}, {"faqId": "4a20242b-d1ee-4c35-95a9-12bd0d90445c", "question": "바비큐나 고기를 구워 먹을 수 있나요?", "answer": "바비큐는 하실 수 없습니다. 화재 예방을 위해 객실 안에서 생선이나 고기를 굽는 직화 방식은 허용되지 않으며, 그릴·숯·전기 전열기구 등 개인적으로 준비해 오시는 취사도구도 반입이 금지되어 있습니다.", "status": 3, "sourceType": 4, "sortOrder": 22}, {"faqId": "4b1b6de0-40ca-44fa-af35-cbd69eeebd35", "question": "픽업 서비스가 있나요?", "answer": "픽업 서비스는 지원하지 않습니다.", "status": 3, "sourceType": 4, "sortOrder": 23}, {"faqId": "648cf02d-d4ed-46ee-8aef-b3b6426e857e", "question": "건물이 오래된 집이라던데 언제 지어졌나요?", "answer": "1920년대에 지어진 적산가옥입니다. 백 년 된 고택을 리모델링해 2024년 5월 독채 펜션으로 문을 열었습니다. 바로 옆이 국가등록문화유산인 군산 신흥동 일본식 가옥(히로쓰 가옥)입니다.", "status": 3, "sourceType": 4, "sortOrder": 24}, {"faqId": "015a1e83-a916-43fa-a6c1-95d7ece1817d", "question": "침구는 깨끗한가요?", "answer": "침구류는 매일 세탁하고 살균·건조합니다. 인테리어와 가구, 소품도 하나하나 준비해 두었습니다.", "status": 3, "sourceType": 4, "sortOrder": 25}, {"faqId": "10fa8fd2-7a04-4c0c-935c-dd354f7d3112", "question": "미성년자끼리 이용할 수 있나요?", "answer": "숙박업소는 법적으로 청소년 혼숙이 금지되어 있습니다. 미성년자는 보호자를 동반하셔야 이용하실 수 있습니다.", "status": 3, "sourceType": 4, "sortOrder": 26}, {"faqId": "48ec5a26-7e62-4e66-ae32-5917818bdce3", "question": "CCTV가 설치되어 있나요?", "answer": "고객님의 안전과 보안을 위해 대문 입구 쪽에 CCTV를 운영하고 있습니다. 객실 안에는 없습니다.", "status": 3, "sourceType": 4, "sortOrder": 27}, {"faqId": "9c3dadc6-bbc6-4f72-b4e8-c336dbfe11da", "question": "벌레가 나오지는 않나요?", "answer": "정기적으로 방역하고 있습니다. 다만 자연과 함께하는 공간이라 가끔 벌레나 이물질이 들어올 수 있으며, 이 사유로는 환불이 어려운 점 양해 부탁드립니다.", "status": 3, "sourceType": 4, "sortOrder": 28}, {"faqId": "1f83a416-f01d-4813-babe-011b039df462", "question": "예약 인원보다 사람이 늘면 어떻게 하나요?", "answer": "미리 숙소로 연락 주세요. 기준 인원(2인)을 넘으면 1인당 20,000원이 추가됩니다. 최대 인원(4인)을 넘으면 입실이 불가능할 수 있고, 그 사유로는 환불을 받으실 수 없습니다.", "status": 3, "sourceType": 4, "sortOrder": 29}, {"faqId": "8b0cfa98-9616-44ac-937d-dd5f0a8a4ca9", "question": "히로쓰 가옥 바로 옆이라던데 어떤 집인가요?", "answer": "스테이,머뭄은 1920년대에 지어진 집으로, 국가등록문화유산인 군산 신흥동 일본식 가옥(히로쓰 가옥) 후문 바로 옆 13m 거리에 있습니다. 사장님은 이 집이 히로쓰 가옥 관리인이 살던 집이라고 전합니다. 백 년 된 고택을 리모델링해 2024년 5월 독채 펜션으로 문을 열었습니다.", "status": 3, "sourceType": 4, "sortOrder": 30}, {"faqId": "bbd98512-574e-4601-bd16-b48654aaf5d9", "question": "주요 관광지까지 걸어서 얼마나 걸리나요?", "answer": "초원사진관, 이성당, 군산 시간여행마을 등 주요 관광명소가 걸어서 최대 8분 거리입니다. 차 없이 도보 여행이 가능합니다.", "status": 3, "sourceType": 4, "sortOrder": 31}], "links": [{"channel": 3, "url": "https://m.place.naver.com/place/1133638931/home", "title": "스테이,머뭄 네이버 플레이스", "confirmed": true}, {"channel": 4, "url": "https://www.instagram.com/staymeomoom", "title": "스테이,머뭄 인스타그램", "confirmed": true}], "local": {"attractions": [{"name": "군산 신흥동 일본식가옥(히로쓰 가옥)", "category": "관광지", "searchQuery": "군산 신흥동 일본식가옥(히로쓰 가옥)", "distanceText": "13m", "imageUrl": "/assets/mirror/f27b7440e2f1183f.jpg", "description": "군산 신흥동 일본식 가옥(구 히로쓰 가옥)은 일제강점기에 군산에서 포목점과 소규모 농장을 운영하며 군산부협의회 의원을 지낸 일본인이 건립한 일본식 2층 목조 가옥이다."}, {"name": "초원사진관", "category": "관광지", "searchQuery": "초원사진관", "distanceText": "248m", "imageUrl": "/assets/mirror/d5433fe48de3da05.jpg", "description": "초원사진관은 1998년 1월에 개봉한 영화 <8월의 크리스마스>의 촬영 장소로 유명한 곳이다. 이 영화는 배우 한석규와 심은하가 주연인 영화로 시한부 인생을 사는 사진사 정원(한석규)과 어느 날 그…"}, {"name": "군산 영화의 거리", "category": "관광지", "searchQuery": "군산 영화의 거리", "distanceText": "318m", "imageUrl": "/assets/mirror/6a0c445e6dd940af.jpg", "description": "시간과 공간을 초월한 특별한 경험을 할 수 있는 곳이다. 그 중에서 초원사진관은 영화 <8월의 크리스마스> 촬영지로 잘 알려진 관광명소이다."}, {"name": "우체통거리", "category": "관광지", "searchQuery": "우체통거리", "distanceText": "566m", "imageUrl": "/assets/mirror/43461e391f83b9e8.jpg", "description": "군산시 우체통거리는 2016년 도시재생사업으로 추진된 주민공모사업에 우체국 주변 주민들로 구성된 도란도란 공동체가 참여하면서 주민들이 직접 폐 우체통을 손질하고 그림을 그려 상가 앞에 설치하면서 조…"}, {"name": "군산근대미술관", "category": "관광지", "searchQuery": "군산근대미술관", "distanceText": "708m", "imageUrl": "/assets/mirror/d8764592a03b0103.jpg", "description": "18은행은 일본 나가사키에 본사를 두고 있던 은행으로, 숫자 18은 은행 설립인가 순서를 의미한다. 군산지점은 조선에서 7번째 지점으로 1907년에 설립되었다."}, {"name": "옛 군산세관", "category": "관광지", "searchQuery": "옛 군산세관", "distanceText": "728m", "imageUrl": "/assets/mirror/22af0bff45e5fce8.jpg", "description": "구 군산세관본관 건물이 완공된 것은 1908년으로, 일제강점기가 시작되기 전에 우리나라 정부가 만든 근대식 건물이다."}, {"name": "근대건축관 (구)조선은행군산지점", "category": "관광지", "searchQuery": "근대건축관 (구)조선은행군산지점", "distanceText": "788m", "imageUrl": "/assets/mirror/5000f0a4feb45788.jpg", "description": "1923년에 건립된 일제의 건물로 일제가 식민 지배를 위해 운영한 대표적인 금융시설이었다. 이 건물은 붉은 벽돌로 지은 4층 높이의 2층 건물로, 정면에 돌출된 현관을 중심으로 평아치를 5개 세우고…"}, {"name": "군산 시간여행마을", "category": "관광지", "searchQuery": "군산 시간여행마을", "distanceText": "1.1km", "imageUrl": "/assets/mirror/7ba545fdbc8ad273.jpg", "description": "군산은 근대사에서 수탈의 아픔과 이에 항거한 열정의 도시로, 일제강점기 근대 문화를 상상해볼 수 있는 문화재가 원도심을 중심으로 잘 보존되었다."}, {"name": "월명호수", "category": "관광지", "searchQuery": "월명호수", "distanceText": "1.6km", "imageUrl": "/assets/mirror/c8c6a295ae1c0810.jpg", "description": "월명공원의 품속에는 1912년에 시민에게 식수를 공급하기 위해 만든 제1수원지가 물안개를 뿜어내는 산 속의 호수로 변하여 산새와 작은 동물들이 목을 축이는 곳이 되었고 설림산 아래 천년고찰 은적사에…"}, {"name": "은적사(군산)", "category": "관광지", "searchQuery": "은적사(군산)", "distanceText": "2.3km", "imageUrl": "/assets/mirror/669abc2b08640d3e.jpg", "description": "은적사는 전라북도 내에 있는 사찰 가운데 가장 오래된 백제 시대 사찰이다. 천방사, 선림사 등으로도 불리었던 은적사는 대한불교 조계종 제17교구 본사인 금산사의 말사이다."}, {"name": "서천문화예술창작공간", "category": "관광지", "searchQuery": "서천문화예술창작공간", "distanceText": "2.6km", "imageUrl": "/assets/mirror/7f8f81d5eeb33f73.jpg", "description": "서천군 문화예술창작공간은 일제시대에 미곡창고로 사용된 건물로 이름에서 유추할 수 있듯이, 본래의 목적은 쌀을 보관하기 위해 세워진 건물이다."}, {"name": "경암동 철길마을", "category": "관광지", "searchQuery": "경암동 철길마을", "distanceText": "2.7km", "imageUrl": "/assets/mirror/6d59722cedddd566.jpg", "description": "경암동 철길마을은 1944년 전라북도 군산시 경암동에 준공하여 페이퍼 코리아 공장과 군산역을 연결하는 총연장 2.5km 철로 주변의 마을을 총괄하여 붙인 이름이다."}], "restaurants": [{"name": "도란", "category": "맛집", "searchQuery": "도란", "distanceText": "135m", "imageUrl": "/assets/mirror/12938240987c1f78.jpg", "description": "전라북도 군산시 월명동에 있는 요리주점이다. 와인과 함께 카페와 양식을 메뉴를 주로 다루고 있다. 대표 메뉴로는 감바스, 해산물 크림 스튜, 부채살 스테이크 등이 있다."}, {"name": "물밀소", "category": "맛집", "searchQuery": "물밀소", "distanceText": "297m", "imageUrl": "/assets/mirror/07600bb67b91dd15.jpg", "description": "월명동에 자리한 베이커리 물밀소는 신생 빵집으로 입소문이 자자하다. 붉은 벽돌의 아담한 외관을 가졌고 매장 내부에는 저온숙성한 반죽으로 빚어낸 건강한 빵들이 진열돼 있다."}, {"name": "일흥옥", "category": "맛집", "searchQuery": "일흥옥", "distanceText": "300m", "imageUrl": "/assets/mirror/3420ff7e357ab7ef.jpg", "description": "일흥옥은 군산 테디베어뮤지엄 인근에 위치한 국밥 전문점이다. 중소벤처기업부로부터 백년가게 인증을 받은 곳이다."}, {"name": "명월갈비", "category": "맛집", "searchQuery": "명월갈비", "distanceText": "351m", "imageUrl": "/assets/mirror/881d1f7ff820942f.jpg", "description": "군산 신창동에 있는 명월갈비는 군산 지역의 대표 양념 소갈비 전문 맛집이다. 플러스 등급의 한우 갈비를 비법 양념장으로 숙성한 단품 메뉴로 오랜 기간 동안 인기를 유지해오고 있다."}, {"name": "만남스넥", "category": "맛집", "searchQuery": "만남스넥", "distanceText": "405m", "imageUrl": "/assets/mirror/d2be5a053a944cfe.jpg", "description": "만남스넥은 군산 현지인들이 좋아하는 메뉴인 잡탕을 파는 분식집이다. 잡탕은 떡, 만두, 라면, 어묵 등을 넣고 같이 끓인 메뉴인데, 떡볶이나 라볶이보다 국물이 많고 부재료가 풍부해 잡탕으로 불리게…"}, {"name": "동국사다온", "category": "맛집", "searchQuery": "동국사다온", "distanceText": "418m", "imageUrl": "/assets/mirror/4db345bd2cb4f0d7.jpg", "description": "전라북도 군산시 일본식 사찰인 동국사 사찰 내에 있는 테이크아웃 커피 전문점이다. 불교 용품과 함께 대추차, 오미자차, 오룡차 등 다양한 차 종류를 판매하고 있다."}, {"name": "이성당", "category": "맛집", "searchQuery": "이성당", "distanceText": "455m", "imageUrl": "/assets/mirror/37c90494d28478e2.png", "description": "일본 시마네현 이즈모시에 살다가 1906년 조선으로 건너온 히로세 야스타로라는 일본인이 ‘이즈모야’라는 이름으로 문을 열어 영업한 것이 시초이다."}, {"name": "국제반점", "category": "맛집", "searchQuery": "국제반점", "distanceText": "467m", "imageUrl": "/assets/mirror/149c18951bdcaff8.jpg", "description": "군산 영화동에 있는 국제반점은 1960년대 초 진흥반점으로 개업해 현재는 상호를 국제 반점으로 변경하여 성업 중인 중식당이다. 영화 <타짜>에 등장해 군산의 관광 명소가 되었다."}], "festivals": [{"name": "2026 군산 은파벚꽃야시장", "month": "3~4월", "period": "2026년 3월 28일 – 4월 12일", "location": "은파호수공원", "description": "군산 은파벚꽃야시장은 상인분들의 한 해를 힘차게 시작할 수 있도록 돕는 자리이자 군산의 대표적인 봄 축제였던 벚꽃축제가 사라진 아쉬움을 해소하고자 기획하게 된 행사이다.", "searchQuery": "2026 군산 은파벚꽃야시장", "season": "봄", "startDate": "2026-03-28", "endDate": "2026-04-12", "imageUrl": "/assets/mirror/82606e203454e3ff.jpg"}, {"name": "군산 수제맥주&블루스 페스티벌", "month": "6월", "period": "2026년 6월 12일 – 14일", "location": "군산근대역사박물관 주차장", "description": "'군산 수제맥주&블루스 페스티벌'은 국내 유일의 로컬 수제맥주 축제로, 100% 군산보리로 만드는 군산맥아로 만든 진짜 우리 수제맥주와 블루스음악과 함께 항구도시 군산의 초여름 밤, 낭만을 만끽할…", "searchQuery": "군산 수제맥주&블루스 페스티벌", "season": "여름", "startDate": "2026-06-12", "endDate": "2026-06-14", "imageUrl": "/assets/mirror/92f65116f3e58ab6.jpg"}, {"name": "군산짬뽕페스티벌", "month": "10월", "period": "2025년 10월 9일 – 12일", "location": "군산 백년광장 일원(장미동)", "description": "70년 짬뽕역사의 도시, 군산에서 10월 9일부터 12일까지, 4일간 짬뽕페스티벌이 개최된다. 얼큰, 화끈, 개운한 국물 베이스인 해물가득한 짬뽕, 고기짬뽕 등 다양한 테마의 짬뽕을 축제장에서 선보…", "searchQuery": "군산짬뽕페스티벌", "season": "가을", "startDate": "2025-10-09", "endDate": "2025-10-12", "imageUrl": "/assets/mirror/a1b00af88aa06cd9.jpg"}, {"name": "군산시간여행축제", "month": "10월", "period": "2026년 10월 2일 – 5일", "location": "구·시청광장 및 시간여행마을 일원", "description": "군산시간여행축제는 일제강점기 민중의 항거와 치열한 삶의 역사를 간직한 근대 군산을 배경으로, 군산의 과거와 현재, 그리고 미래를 잇는 특별한 시간여행을 선사하는 축제이다.", "searchQuery": "군산시간여행축제", "season": "가을", "startDate": "2026-10-02", "endDate": "2026-10-05", "imageUrl": "/assets/mirror/4b4086345c20b968.jpg"}, {"name": "익산천만송이 국화축제", "month": "10월", "season": "가을", "location": "전북특별자치도 익산시 하나로 322 (어양동) · 차로 30분", "description": "가을을 대표하는 축제로, 대형 조형물에 국화를 심어 만든 작품과 전국 최고 규모의 국화정원, 국화분재 특별전시가 열린다.", "searchQuery": "익산천만송이 국화축제", "imageUrl": "/assets/mirror/6194651114d1e740.jpg"}, {"name": "익산마한문화대전", "month": "10월", "season": "가을", "location": "전북특별자치도 익산시 금마면 고도9길 41-14 · 차로 35분", "description": "익산 마한의 정통성을 다지고 익산을 마한 문화 선진지로 알리기 위해 여는 행사이다.", "searchQuery": "익산마한문화대전", "imageUrl": "/assets/mirror/49558512d8d52e1a.jpg"}, {"name": "강경젓갈축제", "month": "10월", "season": "가을", "location": "충청남도 논산시 강경읍 금백로 45 · 차로 40분", "description": "전통의 맛과 문화를 현대적으로 재해석한 축제로, 금강의 자연경관과 강경의 역사 자원을 함께 둘러볼 수 있다.", "searchQuery": "강경젓갈축제", "imageUrl": "/assets/mirror/7ccd68de6b280bc2.jpg"}, {"name": "부안 설(雪)숭어 축제", "month": "12월", "season": "겨울", "location": "전북특별자치도 부안군 부안읍 서외리 · 차로 35분", "description": "겨울철에 맛이 오르는 설숭어를 주제로 한 지역 특산 수산물 축제이다. 설숭어 음식 판매와 시식, 어촌 문화 체험 프로그램이 열린다.", "searchQuery": "부안 설숭어 축제", "imageUrl": "/assets/mirror/4389ded2dee1ad98.jpg"}, {"name": "서천 마량진항 해넘이 해돋이 행사", "month": "12월", "season": "겨울", "location": "충청남도 서천군 서면 서인로 58 · 차로 30분", "description": "12월 31일부터 1월 1일까지 마량진항 일원에서 열린다. 서해안에서 일몰과 일출을 한자리에서 볼 수 있는 곳이다.", "searchQuery": "서천 마량진항 해넘이 해돋이", "imageUrl": "/assets/mirror/8fc0a7522e3eb080.png"}, {"name": "선유도 여름 노을축제", "season": "여름", "month": "7월", "period": "2026년 7월 25일", "location": "군산 선유도해수욕장", "description": "서해 낙조 명소인 선유도해수욕장 특설무대에서 열리는 한여름 밤 축제입니다. 지역 예술인의 공연과 참여 행사가 열리고 폭죽쇼로 마무리합니다. 사진은 개최 장소인 선유도해수욕장입니다.", "searchQuery": "선유도 여름 노을축제", "imageUrl": "/assets/mirror/c971fb408ad897e7.jpg"}], "weather": {"temperature": 27.9, "condition": "구름많음", "observedAt": "2026-09-03T12:30", "notes": {"맑음": "마당 평상에 앉으면 새소리만 들립니다. 정원 쪽으로 그늘이 길게 눕는 시간입니다.", "흐림": "빛이 낮게 깔리는 날입니다. 창고형 CAFÉ 공간이 이런 날 가장 아늑합니다.", "비": "창고형 CAFÉ 공간에서 빗소리를 들으실 수 있습니다. 백 년 된 기와를 타고 내리는 소리가 다릅니다.", "눈": "기와에 눈이 앉으면 골목이 통째로 조용해집니다. 정원 석등만 켜 두겠습니다."}, "tempNotes": {"혹서": "낮에는 군산근대미술관·근대건축관처럼 실내로 도는 편이 낫습니다. 월명호수 한 바퀴는 해가 넘어간 뒤가 시원합니다.", "더움": "선유도까지 다녀오기 좋은 기온입니다. 골목은 오후 늦게 도세요 — 경암동 철길마을은 해 질 때가 사람이 적습니다.", "선선": "걷기에 가장 좋은 날입니다. 히로쓰 가옥에서 초원사진관·영화의 거리까지 걸어서 그대로 이어집니다.", "쌀쌀": "월명호수를 한 바퀴 돌고 이성당이나 국제반점에서 따뜻한 것을 드시면 알맞습니다.", "추움": "밖은 바람이 매섭습니다. 근대건축관·근대미술관과 시간여행마을 실내를 묶어 도는 편이 낫습니다."}}, "syncedAt": "2026-09-09T13:40:00+00:00"}, "routes": [], "narrative": {"about": ["스테이,머뭄은 전북특별자치도 군산시 신흥동 절골길에 있는 독채 펜션입니다. 1920년대에 지어진 백 년 고택으로, 국가등록문화유산인 군산 신흥동 일본식 가옥(히로쓰 가옥) 후문 바로 옆 — 걸어서 13m 거리입니다. 사장님은 이 집이 히로쓰 가옥 관리인이 살던 집이라고 전합니다. 2024년 5월에 리모델링을 마치고 문을 열었고, A동과 B동 두 동을 각각 한 팀만 씁니다. 기준 2인 · 최대 4인까지 묵을 수 있습니다.", "A동은 편안한 일본집에 와 있는 듯한 느낌의 공간이고, B동은 아늑하고 깔끔한 현대식 모던 스타일 공간입니다. 두 동 모두 독립된 아담한 정원과 창고형 CAFÉ 공간이 딸려 있어, 마당과 거실에서 새소리를 들으며 쉴 수 있습니다.", "인테리어와 가구, 소품, 침구를 하나하나 준비해 두었습니다. 특히 침구류는 매일 세탁·살균하고 건조해 쓰시는 분이 바뀔 때마다 새것처럼 나갑니다."], "summary": "1920년대 적산가옥을 고쳐 지은 독채 두 동, 매일 세탁·살균하는 침구", "tagline": "히로쓰 가옥 담 너머, 백 년 된 집 한 채", "catchphrases": {"version": 1, "items": [{"text": "백 년 된 집에서 하룻밤", "kind": "general"}, {"text": "담 너머는 히로쓰 가옥입니다", "kind": "general"}, {"text": "독채 두 동, 마당은 각자 씁니다", "kind": "general"}, {"text": "기와 아래에서 아침을 맞습니다", "kind": "general"}, {"text": "새소리로 눈이 떠지는 집", "kind": "general"}, {"text": "골목이 조용해지는 시간에 도착하세요", "kind": "general"}, {"text": "침구는 매일 새것처럼 나갑니다", "kind": "general"}, {"text": "마당 평상에 앉으면 항구 소리가 들립니다", "kind": "general"}, {"text": "1920년대에 지은 집, 지금 쓰는 집", "kind": "general"}, {"text": "대문을 닫으면 골목이 멀어집니다", "kind": "general"}, {"text": "창고를 고친 카페 공간이 딸려 있습니다", "kind": "general"}, {"text": "밤에는 정원 석등만 켜 둡니다", "kind": "general"}, {"text": "초원사진관까지 걸어서 3분", "kind": "general"}, {"text": "원도심 한가운데, 차는 세워 두세요", "kind": "general"}, {"text": "A동은 일본식, B동은 모던입니다", "kind": "general"}, {"text": "네 사람까지 묶는 독채입니다", "kind": "general"}, {"text": "오후 세 시부터 이 집은 손님 것입니다", "kind": "general"}, {"text": "이 골목에서는 서두를 일이 없습니다", "kind": "general"}, {"text": "백 년 된 기와가 비를 받습니다", "kind": "general"}, {"text": "여행이라기보다 하루의 이사입니다", "kind": "general"}, {"text": "군산에서 가장 조용한 담 안쪽", "kind": "general"}, {"text": "이 집의 시간은 조금 느립니다", "kind": "general"}, {"text": "마당, 정원, 그리고 평상 하나", "kind": "general"}, {"text": "짐을 풀면 골목부터 걸어 보세요", "kind": "general"}, {"text": "창을 열면 옆집 기와가 보입니다", "kind": "general"}, {"text": "담배 냄새가 없는 집입니다", "kind": "general"}, {"text": "이불에서 볕 냄새가 납니다", "kind": "general"}, {"text": "두 동 모두 남의 인기척이 없습니다", "kind": "general"}, {"text": "내항까지 걸어 십 분", "kind": "general"}, {"text": "오래된 것을 고쳐 쓰는 집", "kind": "general"}, {"text": "부엌이 있어 아침은 지어 드셔도 됩니다", "kind": "general"}, {"text": "밤에는 골목 등만 남습니다", "kind": "general"}, {"text": "백 년 된 집이 아직 손님을 받습니다", "kind": "general"}, {"text": "개항장 골목의 끝집입니다", "kind": "general"}, {"text": "문패보다 기와를 보고 찾아오세요", "kind": "general"}, {"text": "걸어서 도는 여행에 알맞은 자리", "kind": "general"}, {"text": "마루에 앉는 시간이 일정의 절반입니다", "kind": "general"}, {"text": "좁은 골목 끝, 대문 하나", "kind": "general"}, {"text": "여기서는 계획을 반쯤 접어도 됩니다", "kind": "general"}, {"text": "하루를 통째로 쓰는 숙소", "kind": "general"}, {"text": "정원에 꽃이 먼저 옵니다", "kind": "season", "season": "봄"}, {"text": "봄에는 대문을 열어 둡니다", "kind": "season", "season": "봄"}, {"text": "벚꽃 지는 골목을 걸어 오세요", "kind": "season", "season": "봄"}, {"text": "마당에 앉는 계절이 시작됩니다", "kind": "season", "season": "봄"}, {"text": "새소리가 가장 많은 철입니다", "kind": "season", "season": "봄"}, {"text": "창을 열면 봄이 방까지 들어옵니다", "kind": "season", "season": "봄"}, {"text": "처마 그늘이 가장 긴 계절입니다", "kind": "season", "season": "여름"}, {"text": "마당에 물을 뿌리면 시원해집니다", "kind": "season", "season": "여름"}, {"text": "여름 밤에는 평상이 제일 좋습니다", "kind": "season", "season": "여름"}, {"text": "창고형 카페가 낮에는 서늘합니다", "kind": "season", "season": "여름"}, {"text": "소나기는 기와가 받아 줍니다", "kind": "season", "season": "여름"}, {"text": "해가 길어 저녁이 넉넉합니다", "kind": "season", "season": "여름"}, {"text": "기와 위로 가을 볕이 마릅니다", "kind": "season", "season": "가을"}, {"text": "가을 바람이 담을 넘어옵니다", "kind": "season", "season": "가을"}, {"text": "가을 골목은 해가 빨리 눕습니다", "kind": "season", "season": "가을"}, {"text": "창을 열어 두고 자도 되는 밤입니다", "kind": "season", "season": "가을"}, {"text": "가을에는 평상이 가장 좋은 자리입니다", "kind": "season", "season": "가을"}, {"text": "마당에 잎이 떨어지는 계절입니다", "kind": "season", "season": "가을"}, {"text": "기와에 눈이 앉으면 골목이 조용해집니다", "kind": "season", "season": "겨울"}, {"text": "겨울 아침 마당은 서리 밭입니다", "kind": "season", "season": "겨울"}, {"text": "이불 속에서 겨울 바람 소리를 듣습니다", "kind": "season", "season": "겨울"}, {"text": "눈 오면 정원 석등만 켜 둡니다", "kind": "season", "season": "겨울"}, {"text": "항구 바람이 매운 계절입니다", "kind": "season", "season": "겨울"}, {"text": "겨울에는 카페 공간이 가장 아늑합니다", "kind": "season", "season": "겨울"}, {"text": "새해 첫 아침을 백 년 집에서", "kind": "month", "month": 1}, {"text": "겨울 골목이 가장 조용한 달입니다", "kind": "month", "month": 1}, {"text": "아직 바람이 찬 원도심입니다", "kind": "month", "month": 2}, {"text": "겨울 끝을 마당에서 봅니다", "kind": "month", "month": 2}, {"text": "정원에 첫 꽃이 오는 달입니다", "kind": "month", "month": 3}, {"text": "대문을 열어 두기 시작합니다", "kind": "month", "month": 3}, {"text": "벚꽃 지는 골목을 걷는 달입니다", "kind": "month", "month": 4}, {"text": "마당에 앉는 시간이 길어집니다", "kind": "month", "month": 4}, {"text": "새소리가 가장 많은 달입니다", "kind": "month", "month": 5}, {"text": "창을 열어 두고 자는 밤입니다", "kind": "month", "month": 5}, {"text": "처마 그늘이 길어지는 달입니다", "kind": "month", "month": 6}, {"text": "여름 앞의 원도심은 한가합니다", "kind": "month", "month": 6}, {"text": "소나기를 기와가 받는 달입니다", "kind": "month", "month": 7}, {"text": "여름 밤 평상이 제일 좋습니다", "kind": "month", "month": 7}, {"text": "낮에는 카페 공간이 서늘합니다", "kind": "month", "month": 8}, {"text": "해가 길어 저녁이 넉넉한 달입니다", "kind": "month", "month": 8}, {"text": "볕이 마르기 시작하는 달입니다", "kind": "month", "month": 9}, {"text": "가을이 담을 넘어오는 구월입니다", "kind": "month", "month": 9}, {"text": "잎이 마당에 떨어지는 달입니다", "kind": "month", "month": 10}, {"text": "걷기에 가장 좋은 달입니다", "kind": "month", "month": 10}, {"text": "해가 빨리 눕는 골목입니다", "kind": "month", "month": 11}, {"text": "이불이 가장 따뜻한 달입니다", "kind": "month", "month": 11}, {"text": "기와에 눈이 앉는 달입니다", "kind": "month", "month": 12}, {"text": "한 해의 끝을 조용히 보내는 자리", "kind": "month", "month": 12}, {"text": "마당 평상에 앉으면 새소리만 들립니다", "kind": "weather", "weather": "맑음"}, {"text": "정원 쪽으로 그늘이 길게 눕는 시간입니다", "kind": "weather", "weather": "맑음"}, {"text": "기와가 마르는 냄새가 납니다", "kind": "weather", "weather": "맑음"}, {"text": "빛이 부드러운 날입니다", "kind": "weather", "weather": "구름많음"}, {"text": "골목 사진이 제일 잘 나오는 빛입니다", "kind": "weather", "weather": "구름많음"}, {"text": "빛이 낮게 깔리는 날입니다", "kind": "weather", "weather": "흐림"}, {"text": "이런 날은 카페 공간이 가장 아늑합니다", "kind": "weather", "weather": "흐림"}, {"text": "백 년 된 기와를 타고 내리는 소리가 다릅니다", "kind": "weather", "weather": "비"}, {"text": "카페 공간에서 빗소리를 들으실 수 있습니다", "kind": "weather", "weather": "비"}, {"text": "비 오는 골목에는 사람이 없습니다", "kind": "weather", "weather": "비"}, {"text": "기와에 눈이 앉으면 골목이 통째로 조용해집니다", "kind": "weather", "weather": "눈"}, {"text": "눈 밟는 소리가 담 안에서 크게 들립니다", "kind": "weather", "weather": "눈"}]}}, "theme": {"templateId": "stay-retro", "colors": {"primary": "#1b1a15", "secondary": "#4c4739", "bg": "#e4dac0", "card": "#efe7d3", "text": "#1b1a15", "accent": "#bf2f1b"}, "fontStyle": "옛 간판체", "sections": [{"id": "hero", "name": "히어로", "enabled": true, "locked": true}, {"id": "intro", "name": "소개", "enabled": true, "locked": false}, {"id": "rooms", "name": "객실 안내", "enabled": true, "locked": false}, {"id": "event", "name": "스테이,머뭄 소식", "enabled": true, "locked": false, "data": "{\"kind\":\"event\",\"version\":1,\"title\":\"스테이,머뭄 소식\",\"subtitle\":\"미리 알아 두시면 좋은 것들\",\"linkUrl\":\"https://www.instagram.com/staymeomoom\",\"linkLabel\":\"인스타그램에서 보기\",\"items\":[{\"kind\":\"공지\",\"title\":\"입실 전 확인해 주세요\",\"summary\":\"머무시기 전에 꼭 읽어 주셔야 하는 것들입니다. 미숙지로 생긴 불편과 손해는 도와드리기 어렵습니다.\",\"body\":\"· 반려동물은 입실하실 수 없습니다.\\n· 건물과 객실 안은 절대 금연입니다. 바로 옆이 군산 신흥동 일본식가옥(히로쓰 가옥)이라 화재에 각별히 유의해 주세요.\\n· 냄새가 잘 빠지지 않는 생선구이·고기구이·튀김류 조리는 하실 수 없습니다.\\n· 객실 안에서 음식을 드실 때 오염에 주의해 주세요.\\n· 객실 시설과 비품, 침구가 손상·파손·오염되면 변상을 요청드릴 수 있습니다.\\n· 정기적으로 방역하고 있으나 자연과 함께하는 공간이라 가끔 벌레나 이물질이 들어올 수 있습니다. 이 사유로는 환불이 어려운 점 양해 부탁드립니다.\\n· 귀중품과 현금은 분실 위험이 있으니 직접 관리해 주세요.\\n· 안전과 보안을 위해 대문 입구 쪽에 CCTV를 운영하고 있습니다. 객실 안에는 없습니다.\\n· 숙박업소는 법적으로 청소년 혼숙이 금지되어 있습니다. 미성년자는 보호자를 동반하셔야 이용하실 수 있습니다.\",\"verified\":\"확인\"},{\"kind\":\"공지\",\"title\":\"인원이 늘면 미리 연락 주세요\",\"summary\":\"예약하신 인원보다 늘어나는 경우, 오시기 전에 숙소로 연락 부탁드립니다.\",\"body\":\"· 기준 인원(2인)을 넘으면 1인당 20,000원이 추가됩니다.\\n· 최대 인원(4인)을 넘으면 입실이 불가능할 수 있고, 이 사유로는 환불을 받으실 수 없습니다.\\n· 다음 이용 고객을 위해 입실 15:00 이후, 퇴실 11:00 이전을 지켜 주세요.\",\"howTo\":\"연락처 0507-1497-0983\",\"verified\":\"확인\"},{\"kind\":\"공지\",\"title\":\"직화·개인 취사도구는 반입이 안 됩니다\",\"summary\":\"백 년 된 목조 고택이라 화재 예방 규정이 엄격합니다.\",\"body\":\"· 객실 안에서 생선이나 고기 등을 굽는 직화 방식은 허용되지 않습니다.\\n· 그릴, 숯, 전기·전열기구 등 개인적으로 준비해 오시는 취사도구는 반입이 금지되어 있습니다.\\n· 주방에서 간단한 조리는 하실 수 있습니다.\\n· 흡연은 지정된 장소를 이용해 주시고, 다른 이용객에게 피해가 갈 수 있는 오락·음주·고성방가는 삼가 주세요.\",\"verified\":\"확인\"}]}"}, {"id": "info", "name": "기본 정보", "enabled": true, "locked": true}, {"id": "booking", "name": "실시간 예약", "enabled": false, "locked": false}, {"id": "video", "name": "ADO2 영상 보기", "enabled": true, "locked": false, "data": "{\"kind\": \"video\", \"version\": 1, \"title\": \"ADO2 영상 보기\", \"subtitle\": \"ADO2로 만든 홍보 영상입니다.\", \"items\": [{\"url\": \"https://youtube.com/shorts/c2ZdwhaB7S4\", \"caption\": \"머뭄의 하루\", \"verified\": \"확인\"}, {\"url\": \"https://youtube.com/shorts/_bbo3dVgOXs\", \"caption\": \"마당과 창고형 카페 공간\", \"verified\": \"확인\"}, {\"url\": \"https://youtube.com/shorts/qrCG4rOeJvs\", \"caption\": \"밤의 머뭄\", \"verified\": \"확인\"}], \"linkUrl\": \"https://ado2.o2osolution.ai/\", \"linkLabel\": \"ADO2 알아보기\"}"}, {"id": "photos", "name": "사진 갤러리", "enabled": true, "locked": false, "variantId": "photos.carousel"}, {"id": "map", "name": "오시는 길", "enabled": true, "locked": true}, {"id": "festival", "name": "계절별 축제", "enabled": true, "locked": false}, {"id": "local", "name": "지역 정보", "enabled": true, "locked": false}, {"id": "itinerary", "name": "추천 일정", "enabled": true, "locked": false, "data": "{\"kind\": \"itinerary\", \"version\": 1, \"title\": \"추천 일정\", \"subtitle\": \"머뭄에서 나서고 머뭄으로 돌아옵니다\", \"items\": [{\"name\": \"섬까지 다녀오는 1박 2일\", \"duration\": \"1박 2일\", \"audience\": \"차를 가져온 손님\", \"why\": \"첫날은 걸어서 도심, 이튿날은 다리를 건너 섬입니다. 두 날의 성격이 아주 다릅니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"15:00\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 45, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 히로쓰 가옥\", \"minutes\": 45, \"moveMinutes\": 3, \"latitude\": 35.9861696, \"longitude\": 126.7060575, \"note\": \"담 하나 건너입니다. 마당과 다다미방을 둘러보고 2층 계단까지 올라가 봅니다.\", \"searchQuery\": \"군산 신흥동 일본식가옥\"}, {\"name\": \"오후 · 초원사진관\", \"minutes\": 45, \"moveMinutes\": 4, \"latitude\": 35.987737, \"longitude\": 126.7083826, \"note\": \"안에 들어가 사진관 세트에서 한 장 찍습니다. 걸어서 3분입니다.\", \"searchQuery\": \"군산 초원사진관\"}, {\"name\": \"저녁 · 빈해원\", \"minutes\": 70, \"moveMinutes\": 8, \"latitude\": 35.9888841, \"longitude\": 126.7139765, \"note\": \"2층 홀에 앉아 짬뽕이나 잡채밥을 시킵니다. 건물 자체가 문화재입니다.\", \"searchQuery\": \"군산 빈해원\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 40, \"moveMinutes\": 5, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 30, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"08:20\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 75, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 고군산군도 선유도\", \"minutes\": 150, \"moveMinutes\": 84, \"latitude\": 35.810205, \"longitude\": 126.4124053, \"note\": \"다리를 건너 들어갑니다. 망주봉 아래 해변을 걷고 스카이라인을 탑니다.\", \"searchQuery\": \"군산 선유도\"}, {\"name\": \"늦은 점심 · 국제반점\", \"minutes\": 55, \"moveMinutes\": 85, \"latitude\": 35.98811, \"longitude\": 126.7108985, \"note\": \"영화 「타짜」에 나온 홀에서 짜장과 탕수육을 시킵니다.\", \"searchQuery\": \"군산 국제반점\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 40, \"moveMinutes\": 7, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"걸어서만 도는 1박 2일\", \"duration\": \"1박 2일\", \"audience\": \"차 없이 오는 손님\", \"why\": \"모든 정거장이 머뭄에서 걸어 닿습니다. 이틀 동안 차를 한 번도 타지 않습니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"15:00\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 49, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 히로쓰 가옥\", \"minutes\": 45, \"moveMinutes\": 3, \"latitude\": 35.9861696, \"longitude\": 126.7060575, \"note\": \"담 하나 건너입니다. 마당과 다다미방을 둘러보고 2층 계단까지 올라가 봅니다.\", \"searchQuery\": \"군산 신흥동 일본식가옥\"}, {\"name\": \"오후 · 초원사진관\", \"minutes\": 45, \"moveMinutes\": 4, \"latitude\": 35.987737, \"longitude\": 126.7083826, \"note\": \"안에 들어가 사진관 세트에서 한 장 찍습니다. 걸어서 3분입니다.\", \"searchQuery\": \"군산 초원사진관\"}, {\"name\": \"저녁 · 일흥옥\", \"minutes\": 52, \"moveMinutes\": 4, \"latitude\": 35.9851498, \"longitude\": 126.7091839, \"note\": \"소고기 국밥으로 저녁을 먹습니다. 백년가게로 지정된 집입니다.\", \"searchQuery\": \"군산 일흥옥\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 42, \"moveMinutes\": 13, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 32, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"09:20\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 79, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 군산근대건축관\", \"minutes\": 47, \"moveMinutes\": 10, \"latitude\": 35.9894202, \"longitude\": 126.7121232, \"note\": \"옛 조선은행 금고와 은행 창구를 봅니다. 2층 전시까지 보면 45분입니다.\", \"searchQuery\": \"군산근대건축관\"}, {\"name\": \"점심 · 한일옥\", \"minutes\": 52, \"moveMinutes\": 4, \"latitude\": 35.9874081, \"longitude\": 126.71, \"note\": \"무국으로 아침 겸 점심을 엽니다. 줄이 있으면 이름을 적고 기다립니다.\", \"searchQuery\": \"군산 한일옥\"}, {\"name\": \"오후 · 우체통거리\", \"minutes\": 26, \"moveMinutes\": 4, \"latitude\": 35.9859965, \"longitude\": 126.7119708, \"note\": \"주민들이 손질해 내놓은 우체통을 따라 걷습니다. 엽서를 부치고 가도 됩니다.\", \"searchQuery\": \"군산 우체통거리\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 42, \"moveMinutes\": 8, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"비가 와도 되는 1박 2일\", \"duration\": \"1박 2일\", \"audience\": \"비 예보를 보고 오는 손님\", \"why\": \"실내가 이어집니다. 비가 그치기를 기다리지 않아도 하루가 찹니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"15:10\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 39, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 군산근대역사박물관\", \"minutes\": 88, \"moveMinutes\": 11, \"latitude\": 35.9908197, \"longitude\": 126.7121232, \"note\": \"3층부터 내려오며 봅니다. 항구가 어떻게 생겼는지 여기서 먼저 잡고 나갑니다.\", \"searchQuery\": \"군산근대역사박물관\"}, {\"name\": \"오후 · 군산근대미술관\", \"minutes\": 44, \"moveMinutes\": 3, \"latitude\": 35.9899368, \"longitude\": 126.7108, \"note\": \"옛 18은행 건물 안 전시를 봅니다. 마당 벤치에서 한 번 쉬어 갑니다.\", \"searchQuery\": \"군산근대미술관\"}, {\"name\": \"저녁 · 빈해원\", \"minutes\": 68, \"moveMinutes\": 5, \"latitude\": 35.9888841, \"longitude\": 126.7139765, \"note\": \"2층 홀에 앉아 짬뽕이나 잡채밥을 시킵니다. 건물 자체가 문화재입니다.\", \"searchQuery\": \"군산 빈해원\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 39, \"moveMinutes\": 5, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 29, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"09:40\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 80, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 군산근대건축관\", \"minutes\": 52, \"moveMinutes\": 10, \"latitude\": 35.9894202, \"longitude\": 126.7121232, \"note\": \"옛 조선은행 금고와 은행 창구를 봅니다. 2층 전시까지 보면 45분입니다.\", \"searchQuery\": \"군산근대건축관\"}, {\"name\": \"점심 · 일흥옥\", \"minutes\": 57, \"moveMinutes\": 8, \"latitude\": 35.9851498, \"longitude\": 126.7091839, \"note\": \"소고기 국밥으로 저녁을 먹습니다. 백년가게로 지정된 집입니다.\", \"searchQuery\": \"군산 일흥옥\"}, {\"name\": \"오후 · 째보선창 근처 카페\", \"minutes\": 69, \"moveMinutes\": 15, \"latitude\": 35.9875345, \"longitude\": 126.7199875, \"note\": \"창가에 앉아 비가 그칠 때까지 기다립니다. 항구가 보이는 자리입니다.\", \"searchQuery\": \"군산 째보선창 카페\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 46, \"moveMinutes\": 19, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"근대건축만 보는 1박 2일\", \"duration\": \"1박 2일\", \"audience\": \"건물을 보러 오는 손님\", \"why\": \"1900년대 건물만 골라 돕니다. 머뭄도 그 시기에 지은 집입니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"15:00\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 38, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 구 군산세관 본관\", \"minutes\": 28, \"moveMinutes\": 13, \"latitude\": 35.9915088, \"longitude\": 126.7135, \"note\": \"빨간 벽돌 본관을 한 바퀴 돌고 앞마당에서 사진을 찍습니다.\", \"searchQuery\": \"구 군산세관\"}, {\"name\": \"오후 · 군산근대건축관\", \"minutes\": 61, \"moveMinutes\": 4, \"latitude\": 35.9894202, \"longitude\": 126.7121232, \"note\": \"옛 조선은행 금고와 은행 창구를 봅니다. 2층 전시까지 보면 45분입니다.\", \"searchQuery\": \"군산근대건축관\"}, {\"name\": \"저녁 · 명월갈비\", \"minutes\": 71, \"moveMinutes\": 6, \"latitude\": 35.9863914, \"longitude\": 126.7101663, \"note\": \"한우 양념갈비 한 상을 시킵니다. 이 집은 단품 하나로 버텨 왔습니다.\", \"searchQuery\": \"군산 명월갈비\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 38, \"moveMinutes\": 11, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 28, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"08:50\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 73, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 군산근대미술관\", \"minutes\": 70, \"moveMinutes\": 9, \"latitude\": 35.9899368, \"longitude\": 126.7108, \"note\": \"옛 18은행 건물 안 전시를 봅니다. 마당 벤치에서 한 번 쉬어 갑니다.\", \"searchQuery\": \"군산근대미술관\"}, {\"name\": \"점심 · 일흥옥\", \"minutes\": 48, \"moveMinutes\": 8, \"latitude\": 35.9851498, \"longitude\": 126.7091839, \"note\": \"소고기 국밥으로 저녁을 먹습니다. 백년가게로 지정된 집입니다.\", \"searchQuery\": \"군산 일흥옥\"}, {\"name\": \"오후 · 해망굴\", \"minutes\": 19, \"moveMinutes\": 12, \"latitude\": 35.9913181, \"longitude\": 126.705, \"note\": \"굴을 걸어서 통과해 반대편 항구 쪽으로 나옵니다.\", \"searchQuery\": \"군산 해망굴\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 38, \"moveMinutes\": 8, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"이성당으로 시작하는 1박 2일\", \"duration\": \"1박 2일\", \"audience\": \"빵을 좋아하는 손님\", \"why\": \"이튿날 아침을 빵으로 엽니다. 문 여는 시간에 맞춰 걸어갑니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"16:10\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 36, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 초원사진관\", \"minutes\": 27, \"moveMinutes\": 4, \"latitude\": 35.987737, \"longitude\": 126.7083826, \"note\": \"안에 들어가 사진관 세트에서 한 장 찍습니다. 걸어서 3분입니다.\", \"searchQuery\": \"군산 초원사진관\"}, {\"name\": \"오후 · 우체통거리\", \"minutes\": 22, \"moveMinutes\": 6, \"latitude\": 35.9859965, \"longitude\": 126.7119708, \"note\": \"주민들이 손질해 내놓은 우체통을 따라 걷습니다. 엽서를 부치고 가도 됩니다.\", \"searchQuery\": \"군산 우체통거리\"}, {\"name\": \"저녁 · 도란\", \"minutes\": 72, \"moveMinutes\": 6, \"latitude\": 35.986822, \"longitude\": 126.70751, \"note\": \"감바스와 부채살 스테이크에 와인 한 잔으로 저녁을 마무리합니다.\", \"searchQuery\": \"군산 도란\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 36, \"moveMinutes\": 14, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 27, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"08:40\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 108, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 이성당\", \"minutes\": 50, \"moveMinutes\": 7, \"latitude\": 35.9870443, \"longitude\": 126.7111681, \"note\": \"단팥빵과 야채빵을 사서 나옵니다. 갓 나오는 시간에 맞추면 줄이 짧습니다.\", \"searchQuery\": \"군산 이성당\"}, {\"name\": \"점심 · 만남스넥\", \"minutes\": 36, \"moveMinutes\": 5, \"latitude\": 35.9841788, \"longitude\": 126.7097763, \"note\": \"군산식 잡탕 한 냄비를 시킵니다. 떡·만두·라면이 같이 끓어 나옵니다.\", \"searchQuery\": \"군산 만남스넥\"}, {\"name\": \"오후 · 군산근대역사박물관\", \"minutes\": 81, \"moveMinutes\": 12, \"latitude\": 35.9908197, \"longitude\": 126.7121232, \"note\": \"3층부터 내려오며 봅니다. 항구가 어떻게 생겼는지 여기서 먼저 잡고 나갑니다.\", \"searchQuery\": \"군산근대역사박물관\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 36, \"moveMinutes\": 11, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"철길마을까지 가는 1박 2일\", \"duration\": \"1박 2일\", \"audience\": \"사진을 찍는 손님\", \"why\": \"빛이 좋은 자리만 이어 붙였습니다. 철길마을은 오전 빛이 낫습니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"15:40\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 38, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 초원사진관\", \"minutes\": 28, \"moveMinutes\": 4, \"latitude\": 35.987737, \"longitude\": 126.7083826, \"note\": \"안에 들어가 사진관 세트에서 한 장 찍습니다. 걸어서 3분입니다.\", \"searchQuery\": \"군산 초원사진관\"}, {\"name\": \"오후 · 동국사\", \"minutes\": 33, \"moveMinutes\": 8, \"latitude\": 35.9827663, \"longitude\": 126.7074296, \"note\": \"대웅전 처마와 대나무 숲을 봅니다. 종각까지 돌면 35분입니다.\", \"searchQuery\": \"군산 동국사\"}, {\"name\": \"저녁 · 국제반점\", \"minutes\": 52, \"moveMinutes\": 10, \"latitude\": 35.98811, \"longitude\": 126.7108985, \"note\": \"영화 「타짜」에 나온 홀에서 짜장과 탕수육을 시킵니다.\", \"searchQuery\": \"군산 국제반점\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 38, \"moveMinutes\": 9, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 28, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"08:30\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 77, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 경암동 철길마을\", \"minutes\": 80, \"moveMinutes\": 12, \"latitude\": 35.981352, \"longitude\": 126.7362331, \"note\": \"선로를 따라 걸으며 옛 간판을 봅니다. 교복을 빌려 입고 찍는 집이 여럿입니다.\", \"searchQuery\": \"군산 경암동 철길마을\"}, {\"name\": \"점심 · 일흥옥\", \"minutes\": 48, \"moveMinutes\": 11, \"latitude\": 35.9851498, \"longitude\": 126.7091839, \"note\": \"소고기 국밥으로 저녁을 먹습니다. 백년가게로 지정된 집입니다.\", \"searchQuery\": \"군산 일흥옥\"}, {\"name\": \"오후 · 군산 시간여행마을\", \"minutes\": 57, \"moveMinutes\": 6, \"latitude\": 35.9883192, \"longitude\": 126.7109377, \"note\": \"골목을 정해 두지 말고 걷습니다. 1930년대 간판을 그대로 단 가게가 이어집니다.\", \"searchQuery\": \"군산 시간여행마을\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 38, \"moveMinutes\": 7, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"항구만 도는 1박 2일\", \"duration\": \"1박 2일\", \"audience\": \"바다가 보고 싶은 손님\", \"why\": \"부잔교에서 째보선창까지, 이틀 동안 물가만 걷습니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"15:40\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 42, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 군산 내항 부잔교\", \"minutes\": 37, \"moveMinutes\": 16, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"뜬다리 위를 끝까지 걸어 나갔다 돌아옵니다. 조수에 따라 다리가 오르내립니다.\", \"searchQuery\": \"군산 내항 부잔교\"}, {\"name\": \"오후 · 해망굴\", \"minutes\": 21, \"moveMinutes\": 17, \"latitude\": 35.9913181, \"longitude\": 126.705, \"note\": \"굴을 걸어서 통과해 반대편 항구 쪽으로 나옵니다.\", \"searchQuery\": \"군산 해망굴\"}, {\"name\": \"저녁 · 빈해원\", \"minutes\": 74, \"moveMinutes\": 13, \"latitude\": 35.9888841, \"longitude\": 126.7139765, \"note\": \"2층 홀에 앉아 짬뽕이나 잡채밥을 시킵니다. 건물 자체가 문화재입니다.\", \"searchQuery\": \"군산 빈해원\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 42, \"moveMinutes\": 5, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 32, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"09:20\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 79, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 째보선창\", \"minutes\": 32, \"moveMinutes\": 19, \"latitude\": 35.9875345, \"longitude\": 126.7199875, \"note\": \"선창을 따라 걷습니다. 아직 배가 드나드는 자리라 아침이 더 볼 게 많습니다.\", \"searchQuery\": \"군산 째보선창\"}, {\"name\": \"점심 · 국제반점\", \"minutes\": 58, \"moveMinutes\": 12, \"latitude\": 35.98811, \"longitude\": 126.7108985, \"note\": \"영화 「타짜」에 나온 홀에서 짜장과 탕수육을 시킵니다.\", \"searchQuery\": \"군산 국제반점\"}, {\"name\": \"오후 · 구 군산세관 본관\", \"minutes\": 32, \"moveMinutes\": 7, \"latitude\": 35.9915088, \"longitude\": 126.7135, \"note\": \"빨간 벽돌 본관을 한 바퀴 돌고 앞마당에서 사진을 찍습니다.\", \"searchQuery\": \"구 군산세관\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 42, \"moveMinutes\": 13, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"소설 『탁류』를 따라 걷는 1박 2일\", \"duration\": \"1박 2일\", \"audience\": \"채만식을 읽어 본 손님\", \"why\": \"소설이 지나간 자리를 순서대로 밟습니다. 세관·선창·문학관이 한 줄로 이어집니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"15:10\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 44, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 째보선창\", \"minutes\": 33, \"moveMinutes\": 19, \"latitude\": 35.9875345, \"longitude\": 126.7199875, \"note\": \"선창을 따라 걷습니다. 아직 배가 드나드는 자리라 아침이 더 볼 게 많습니다.\", \"searchQuery\": \"군산 째보선창\"}, {\"name\": \"오후 · 군산 내항 부잔교\", \"minutes\": 38, \"moveMinutes\": 5, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"뜬다리 위를 끝까지 걸어 나갔다 돌아옵니다. 조수에 따라 다리가 오르내립니다.\", \"searchQuery\": \"군산 내항 부잔교\"}, {\"name\": \"저녁 · 한일옥\", \"minutes\": 55, \"moveMinutes\": 11, \"latitude\": 35.9874081, \"longitude\": 126.71, \"note\": \"무국으로 아침 겸 점심을 엽니다. 줄이 있으면 이름을 적고 기다립니다.\", \"searchQuery\": \"군산 한일옥\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 44, \"moveMinutes\": 11, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 33, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"09:00\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 82, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 채만식문학관\", \"minutes\": 66, \"moveMinutes\": 18, \"latitude\": 36.0085981, \"longitude\": 126.7578853, \"note\": \"배 모양 전시관을 돌고 금강 쪽 데크로 나가 강을 봅니다.\", \"searchQuery\": \"군산 채만식문학관\"}, {\"name\": \"점심 · 일흥옥\", \"minutes\": 55, \"moveMinutes\": 17, \"latitude\": 35.9851498, \"longitude\": 126.7091839, \"note\": \"소고기 국밥으로 저녁을 먹습니다. 백년가게로 지정된 집입니다.\", \"searchQuery\": \"군산 일흥옥\"}, {\"name\": \"오후 · 구 군산세관 본관\", \"minutes\": 33, \"moveMinutes\": 12, \"latitude\": 35.9915088, \"longitude\": 126.7135, \"note\": \"빨간 벽돌 본관을 한 바퀴 돌고 앞마당에서 사진을 찍습니다.\", \"searchQuery\": \"구 군산세관\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 44, \"moveMinutes\": 13, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"아이와 함께 1박 2일\", \"duration\": \"1박 2일\", \"audience\": \"아이를 데려온 손님\", \"why\": \"걷는 거리가 짧고, 아이가 지루해할 자리를 뺐습니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"16:00\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 34, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 초원사진관\", \"minutes\": 26, \"moveMinutes\": 4, \"latitude\": 35.987737, \"longitude\": 126.7083826, \"note\": \"안에 들어가 사진관 세트에서 한 장 찍습니다. 걸어서 3분입니다.\", \"searchQuery\": \"군산 초원사진관\"}, {\"name\": \"오후 · 우체통거리\", \"minutes\": 21, \"moveMinutes\": 6, \"latitude\": 35.9859965, \"longitude\": 126.7119708, \"note\": \"주민들이 손질해 내놓은 우체통을 따라 걷습니다. 엽서를 부치고 가도 됩니다.\", \"searchQuery\": \"군산 우체통거리\"}, {\"name\": \"저녁 · 만남스넥\", \"minutes\": 42, \"moveMinutes\": 4, \"latitude\": 35.9841788, \"longitude\": 126.7097763, \"note\": \"군산식 잡탕 한 냄비를 시킵니다. 떡·만두·라면이 같이 끓어 나옵니다.\", \"searchQuery\": \"군산 만남스넥\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 34, \"moveMinutes\": 13, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 26, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"10:10\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 50, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 경암동 철길마을\", \"minutes\": 42, \"moveMinutes\": 12, \"latitude\": 35.981352, \"longitude\": 126.7362331, \"note\": \"선로를 따라 걸으며 옛 간판을 봅니다. 교복을 빌려 입고 찍는 집이 여럿입니다.\", \"searchQuery\": \"군산 경암동 철길마을\"}, {\"name\": \"점심 · 이성당\", \"minutes\": 30, \"moveMinutes\": 11, \"latitude\": 35.9870443, \"longitude\": 126.7111681, \"note\": \"단팥빵과 야채빵을 사서 나옵니다. 갓 나오는 시간에 맞추면 줄이 짧습니다.\", \"searchQuery\": \"군산 이성당\"}, {\"name\": \"오후 · 월명호수\", \"minutes\": 42, \"moveMinutes\": 10, \"latitude\": 35.9766391, \"longitude\": 126.6913696, \"note\": \"호수를 한 바퀴 돕니다. 천천히 걸어 40분 코스입니다.\", \"searchQuery\": \"군산 월명호수\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 34, \"moveMinutes\": 9, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"늦게 도착하는 1박 2일\", \"duration\": \"1박 2일\", \"audience\": \"퇴근하고 오는 손님\", \"why\": \"첫날은 저녁 한 끼로 끝냅니다. 대신 이튿날을 아침부터 저녁까지 씁니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"18:20\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 40, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"저녁 · 명월갈비\", \"minutes\": 75, \"moveMinutes\": 5, \"latitude\": 35.9863914, \"longitude\": 126.7101663, \"note\": \"한우 양념갈비 한 상을 시킵니다. 이 집은 단품 하나로 버텨 왔습니다.\", \"searchQuery\": \"군산 명월갈비\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 30, \"moveMinutes\": 5, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"08:40\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 75, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 군산근대역사박물관\", \"minutes\": 90, \"moveMinutes\": 11, \"latitude\": 35.9908197, \"longitude\": 126.7121232, \"note\": \"3층부터 내려오며 봅니다. 항구가 어떻게 생겼는지 여기서 먼저 잡고 나갑니다.\", \"searchQuery\": \"군산근대역사박물관\"}, {\"name\": \"점심 · 한일옥\", \"minutes\": 50, \"moveMinutes\": 6, \"latitude\": 35.9874081, \"longitude\": 126.71, \"note\": \"무국으로 아침 겸 점심을 엽니다. 줄이 있으면 이름을 적고 기다립니다.\", \"searchQuery\": \"군산 한일옥\"}, {\"name\": \"오후 · 군산근대건축관\", \"minutes\": 45, \"moveMinutes\": 4, \"latitude\": 35.9894202, \"longitude\": 126.7121232, \"note\": \"옛 조선은행 금고와 은행 창구를 봅니다. 2층 전시까지 보면 45분입니다.\", \"searchQuery\": \"군산근대건축관\"}, {\"name\": \"오후 · 해망굴\", \"minutes\": 20, \"moveMinutes\": 10, \"latitude\": 35.9913181, \"longitude\": 126.705, \"note\": \"굴을 걸어서 통과해 반대편 항구 쪽으로 나옵니다.\", \"searchQuery\": \"군산 해망굴\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 40, \"moveMinutes\": 8, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"천천히 도는 2박 3일\", \"duration\": \"2박 3일\", \"audience\": \"쉬러 오는 손님\", \"why\": \"하루에 두어 곳만 봅니다. 마당에 앉아 있는 시간을 일정에 넣었습니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"15:20\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 46, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 히로쓰 가옥\", \"minutes\": 35, \"moveMinutes\": 3, \"latitude\": 35.9861696, \"longitude\": 126.7060575, \"note\": \"담 하나 건너입니다. 마당과 다다미방을 둘러보고 2층 계단까지 올라가 봅니다.\", \"searchQuery\": \"군산 신흥동 일본식가옥\"}, {\"name\": \"오후 · 동국사\", \"minutes\": 40, \"moveMinutes\": 6, \"latitude\": 35.9827663, \"longitude\": 126.7074296, \"note\": \"대웅전 처마와 대나무 숲을 봅니다. 종각까지 돌면 35분입니다.\", \"searchQuery\": \"군산 동국사\"}, {\"name\": \"저녁 · 도란\", \"minutes\": 92, \"moveMinutes\": 7, \"latitude\": 35.986822, \"longitude\": 126.70751, \"note\": \"감바스와 부채살 스테이크에 와인 한 잔으로 저녁을 마무리합니다.\", \"searchQuery\": \"군산 도란\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 46, \"moveMinutes\": 14, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 35, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"09:40\", \"stops\": [{\"name\": \"머뭄에서 쉬는 날 · 스테이 머뭄\", \"minutes\": 223, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"오늘은 나가지 않습니다. 마당 평상과 창고형 카페 공간에서 책을 읽고 낮잠을 잡니다.\"}, {\"name\": \"점심 · 한일옥\", \"minutes\": 53, \"moveMinutes\": 5, \"latitude\": 35.9874081, \"longitude\": 126.71, \"note\": \"무국으로 아침 겸 점심을 엽니다. 줄이 있으면 이름을 적고 기다립니다.\", \"searchQuery\": \"군산 한일옥\"}, {\"name\": \"쉼 · 스테이 머뭄\", \"minutes\": 173, \"moveMinutes\": 5, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"숙소로 돌아와 마당 평상이나 창고형 카페 공간에서 쉽니다.\"}, {\"name\": \"저녁 · 빈해원\", \"minutes\": 74, \"moveMinutes\": 11, \"latitude\": 35.9888841, \"longitude\": 126.7139765, \"note\": \"2층 홀에 앉아 짬뽕이나 잡채밥을 시킵니다. 건물 자체가 문화재입니다.\", \"searchQuery\": \"군산 빈해원\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 32, \"moveMinutes\": 11, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"셋째 날\", \"startTime\": \"09:50\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 70, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 이성당\", \"minutes\": 44, \"moveMinutes\": 7, \"latitude\": 35.9870443, \"longitude\": 126.7111681, \"note\": \"단팥빵과 야채빵을 사서 나옵니다. 갓 나오는 시간에 맞추면 줄이 짧습니다.\", \"searchQuery\": \"군산 이성당\"}, {\"name\": \"점심 · 만남스넥\", \"minutes\": 50, \"moveMinutes\": 5, \"latitude\": 35.9841788, \"longitude\": 126.7097763, \"note\": \"군산식 잡탕 한 냄비를 시킵니다. 떡·만두·라면이 같이 끓어 나옵니다.\", \"searchQuery\": \"군산 만남스넥\"}, {\"name\": \"오후 · 군산 시간여행마을\", \"minutes\": 75, \"moveMinutes\": 7, \"latitude\": 35.9883192, \"longitude\": 126.7109377, \"note\": \"골목을 정해 두지 말고 걷습니다. 1930년대 간판을 그대로 단 가게가 이어집니다.\", \"searchQuery\": \"군산 시간여행마을\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 50, \"moveMinutes\": 7, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"섬과 도심을 반씩 도는 2박 3일\", \"duration\": \"2박 3일\", \"audience\": \"차를 가져온 손님\", \"why\": \"가운데 날을 섬에 다 씁니다. 첫날과 마지막 날은 걸어서 도심입니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"15:10\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 40, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 구 군산세관 본관\", \"minutes\": 30, \"moveMinutes\": 13, \"latitude\": 35.9915088, \"longitude\": 126.7135, \"note\": \"빨간 벽돌 본관을 한 바퀴 돌고 앞마당에서 사진을 찍습니다.\", \"searchQuery\": \"구 군산세관\"}, {\"name\": \"오후 · 군산근대건축관\", \"minutes\": 50, \"moveMinutes\": 4, \"latitude\": 35.9894202, \"longitude\": 126.7121232, \"note\": \"옛 조선은행 금고와 은행 창구를 봅니다. 2층 전시까지 보면 45분입니다.\", \"searchQuery\": \"군산근대건축관\"}, {\"name\": \"저녁 · 빈해원\", \"minutes\": 70, \"moveMinutes\": 3, \"latitude\": 35.9888841, \"longitude\": 126.7139765, \"note\": \"2층 홀에 앉아 짬뽕이나 잡채밥을 시킵니다. 건물 자체가 문화재입니다.\", \"searchQuery\": \"군산 빈해원\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 40, \"moveMinutes\": 5, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 30, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"07:20\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 60, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"부엌에서 아침을 차려 먹고 마당에서 커피를 내려 마신 뒤 나섭니다.\"}, {\"name\": \"오전 · 고군산군도 선유도\", \"minutes\": 150, \"moveMinutes\": 84, \"latitude\": 35.810205, \"longitude\": 126.4124053, \"note\": \"다리를 건너 들어갑니다. 망주봉 아래 해변을 걷고 스카이라인을 탑니다.\", \"searchQuery\": \"군산 선유도\"}, {\"name\": \"오후 · 장자도\", \"minutes\": 50, \"moveMinutes\": 20, \"latitude\": 35.8101, \"longitude\": 126.3973, \"note\": \"대장봉까지 올라갔다 내려옵니다. 섬과 섬 사이를 걸어 넘습니다.\", \"searchQuery\": \"군산 장자도\"}, {\"name\": \"늦은 점심 · 국제반점\", \"minutes\": 55, \"moveMinutes\": 88, \"latitude\": 35.98811, \"longitude\": 126.7108985, \"note\": \"영화 「타짜」에 나온 홀에서 짜장과 탕수육을 시킵니다.\", \"searchQuery\": \"군산 국제반점\"}, {\"name\": \"쉼 · 스테이 머뭄\", \"minutes\": 168, \"moveMinutes\": 7, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"숙소로 돌아와 마당 평상이나 창고형 카페 공간에서 쉽니다.\"}, {\"name\": \"저녁 · 명월갈비\", \"minutes\": 75, \"moveMinutes\": 5, \"latitude\": 35.9863914, \"longitude\": 126.7101663, \"note\": \"한우 양념갈비 한 상을 시킵니다. 이 집은 단품 하나로 버텨 왔습니다.\", \"searchQuery\": \"군산 명월갈비\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 30, \"moveMinutes\": 5, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"셋째 날\", \"startTime\": \"09:20\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 77, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 초원사진관\", \"minutes\": 45, \"moveMinutes\": 4, \"latitude\": 35.987737, \"longitude\": 126.7083826, \"note\": \"안에 들어가 사진관 세트에서 한 장 찍습니다. 걸어서 3분입니다.\", \"searchQuery\": \"군산 초원사진관\"}, {\"name\": \"점심 · 일흥옥\", \"minutes\": 50, \"moveMinutes\": 4, \"latitude\": 35.9851498, \"longitude\": 126.7091839, \"note\": \"소고기 국밥으로 저녁을 먹습니다. 백년가게로 지정된 집입니다.\", \"searchQuery\": \"군산 일흥옥\"}, {\"name\": \"오후 · 이성당\", \"minutes\": 35, \"moveMinutes\": 4, \"latitude\": 35.9870443, \"longitude\": 126.7111681, \"note\": \"단팥빵과 야채빵을 사서 나옵니다. 갓 나오는 시간에 맞추면 줄이 짧습니다.\", \"searchQuery\": \"군산 이성당\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 40, \"moveMinutes\": 7, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"골목을 세 번 나눠 걷는 2박 3일\", \"duration\": \"2박 3일\", \"audience\": \"차 없이 오는 손님\", \"why\": \"원도심을 세 덩이로 나눠 하루에 하나씩 걷습니다. 같은 길을 두 번 걷지 않습니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"15:00\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 49, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 히로쓰 가옥\", \"minutes\": 45, \"moveMinutes\": 3, \"latitude\": 35.9861696, \"longitude\": 126.7060575, \"note\": \"담 하나 건너입니다. 마당과 다다미방을 둘러보고 2층 계단까지 올라가 봅니다.\", \"searchQuery\": \"군산 신흥동 일본식가옥\"}, {\"name\": \"오후 · 초원사진관\", \"minutes\": 45, \"moveMinutes\": 4, \"latitude\": 35.987737, \"longitude\": 126.7083826, \"note\": \"안에 들어가 사진관 세트에서 한 장 찍습니다. 걸어서 3분입니다.\", \"searchQuery\": \"군산 초원사진관\"}, {\"name\": \"저녁 · 일흥옥\", \"minutes\": 55, \"moveMinutes\": 4, \"latitude\": 35.9851498, \"longitude\": 126.7091839, \"note\": \"소고기 국밥으로 저녁을 먹습니다. 백년가게로 지정된 집입니다.\", \"searchQuery\": \"군산 일흥옥\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 44, \"moveMinutes\": 13, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 33, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"09:10\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 66, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"부엌에서 아침을 차려 먹고 마당에서 커피를 내려 마신 뒤 나섭니다.\"}, {\"name\": \"오전 · 군산근대역사박물관\", \"minutes\": 99, \"moveMinutes\": 11, \"latitude\": 35.9908197, \"longitude\": 126.7121232, \"note\": \"3층부터 내려오며 봅니다. 항구가 어떻게 생겼는지 여기서 먼저 잡고 나갑니다.\", \"searchQuery\": \"군산근대역사박물관\"}, {\"name\": \"점심 · 한일옥\", \"minutes\": 55, \"moveMinutes\": 6, \"latitude\": 35.9874081, \"longitude\": 126.71, \"note\": \"무국으로 아침 겸 점심을 엽니다. 줄이 있으면 이름을 적고 기다립니다.\", \"searchQuery\": \"군산 한일옥\"}, {\"name\": \"오후 · 군산근대건축관\", \"minutes\": 50, \"moveMinutes\": 4, \"latitude\": 35.9894202, \"longitude\": 126.7121232, \"note\": \"옛 조선은행 금고와 은행 창구를 봅니다. 2층 전시까지 보면 45분입니다.\", \"searchQuery\": \"군산근대건축관\"}, {\"name\": \"오후 · 해망굴\", \"minutes\": 22, \"moveMinutes\": 10, \"latitude\": 35.9913181, \"longitude\": 126.705, \"note\": \"굴을 걸어서 통과해 반대편 항구 쪽으로 나옵니다.\", \"searchQuery\": \"군산 해망굴\"}, {\"name\": \"쉼 · 스테이 머뭄\", \"minutes\": 164, \"moveMinutes\": 8, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"숙소로 돌아와 마당 평상이나 창고형 카페 공간에서 쉽니다.\"}, {\"name\": \"저녁 · 명월갈비\", \"minutes\": 82, \"moveMinutes\": 5, \"latitude\": 35.9863914, \"longitude\": 126.7101663, \"note\": \"한우 양념갈비 한 상을 시킵니다. 이 집은 단품 하나로 버텨 왔습니다.\", \"searchQuery\": \"군산 명월갈비\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 33, \"moveMinutes\": 5, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"셋째 날\", \"startTime\": \"08:50\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 108, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 우체통거리\", \"minutes\": 40, \"moveMinutes\": 8, \"latitude\": 35.9859965, \"longitude\": 126.7119708, \"note\": \"주민들이 손질해 내놓은 우체통을 따라 걷습니다. 엽서를 부치고 가도 됩니다.\", \"searchQuery\": \"군산 우체통거리\"}, {\"name\": \"점심 · 만남스넥\", \"minutes\": 44, \"moveMinutes\": 4, \"latitude\": 35.9841788, \"longitude\": 126.7097763, \"note\": \"군산식 잡탕 한 냄비를 시킵니다. 떡·만두·라면이 같이 끓어 나옵니다.\", \"searchQuery\": \"군산 만남스넥\"}, {\"name\": \"오후 · 군산 시간여행마을\", \"minutes\": 66, \"moveMinutes\": 7, \"latitude\": 35.9883192, \"longitude\": 126.7109377, \"note\": \"골목을 정해 두지 말고 걷습니다. 1930년대 간판을 그대로 단 가게가 이어집니다.\", \"searchQuery\": \"군산 시간여행마을\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 44, \"moveMinutes\": 7, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"먹으러 오는 2박 3일\", \"duration\": \"2박 3일\", \"audience\": \"맛집을 도는 손님\", \"why\": \"끼니가 중심입니다. 사이에 걸을 자리를 넣어 배를 비웁니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"15:30\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 46, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 물밀소\", \"minutes\": 40, \"moveMinutes\": 4, \"latitude\": 35.9845243, \"longitude\": 126.7073211, \"note\": \"저온숙성 빵을 골라 나옵니다. 마당에서 먹을 것으로 두어 개 더 삽니다.\", \"searchQuery\": \"군산 물밀소\"}, {\"name\": \"오후 · 초원사진관\", \"minutes\": 34, \"moveMinutes\": 6, \"latitude\": 35.987737, \"longitude\": 126.7083826, \"note\": \"안에 들어가 사진관 세트에서 한 장 찍습니다. 걸어서 3분입니다.\", \"searchQuery\": \"군산 초원사진관\"}, {\"name\": \"저녁 · 명월갈비\", \"minutes\": 86, \"moveMinutes\": 3, \"latitude\": 35.9863914, \"longitude\": 126.7101663, \"note\": \"한우 양념갈비 한 상을 시킵니다. 이 집은 단품 하나로 버텨 왔습니다.\", \"searchQuery\": \"군산 명월갈비\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 46, \"moveMinutes\": 11, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 34, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"10:20\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 69, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"부엌에서 아침을 차려 먹고 마당에서 커피를 내려 마신 뒤 나섭니다.\"}, {\"name\": \"오전 · 군산 시간여행마을\", \"minutes\": 69, \"moveMinutes\": 7, \"latitude\": 35.9883192, \"longitude\": 126.7109377, \"note\": \"골목을 정해 두지 말고 걷습니다. 1930년대 간판을 그대로 단 가게가 이어집니다.\", \"searchQuery\": \"군산 시간여행마을\"}, {\"name\": \"점심 · 일흥옥\", \"minutes\": 57, \"moveMinutes\": 6, \"latitude\": 35.9851498, \"longitude\": 126.7091839, \"note\": \"소고기 국밥으로 저녁을 먹습니다. 백년가게로 지정된 집입니다.\", \"searchQuery\": \"군산 일흥옥\"}, {\"name\": \"오후 · 이성당\", \"minutes\": 40, \"moveMinutes\": 4, \"latitude\": 35.9870443, \"longitude\": 126.7111681, \"note\": \"단팥빵과 야채빵을 사서 나옵니다. 갓 나오는 시간에 맞추면 줄이 짧습니다.\", \"searchQuery\": \"군산 이성당\"}, {\"name\": \"오후 · 우체통거리\", \"minutes\": 29, \"moveMinutes\": 3, \"latitude\": 35.9859965, \"longitude\": 126.7119708, \"note\": \"주민들이 손질해 내놓은 우체통을 따라 걷습니다. 엽서를 부치고 가도 됩니다.\", \"searchQuery\": \"군산 우체통거리\"}, {\"name\": \"쉼 · 스테이 머뭄\", \"minutes\": 127, \"moveMinutes\": 8, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"숙소로 돌아와 마당 평상이나 창고형 카페 공간에서 쉽니다.\"}, {\"name\": \"저녁 · 빈해원\", \"minutes\": 80, \"moveMinutes\": 11, \"latitude\": 35.9888841, \"longitude\": 126.7139765, \"note\": \"2층 홀에 앉아 짬뽕이나 잡채밥을 시킵니다. 건물 자체가 문화재입니다.\", \"searchQuery\": \"군산 빈해원\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 34, \"moveMinutes\": 11, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"셋째 날\", \"startTime\": \"08:40\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 104, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 동국사\", \"minutes\": 50, \"moveMinutes\": 6, \"latitude\": 35.9827663, \"longitude\": 126.7074296, \"note\": \"대웅전 처마와 대나무 숲을 봅니다. 종각까지 돌면 35분입니다.\", \"searchQuery\": \"군산 동국사\"}, {\"name\": \"점심 · 국제반점\", \"minutes\": 63, \"moveMinutes\": 10, \"latitude\": 35.98811, \"longitude\": 126.7108985, \"note\": \"영화 「타짜」에 나온 홀에서 짜장과 탕수육을 시킵니다.\", \"searchQuery\": \"군산 국제반점\"}, {\"name\": \"오후 · 군산근대역사박물관\", \"minutes\": 103, \"moveMinutes\": 5, \"latitude\": 35.9908197, \"longitude\": 126.7121232, \"note\": \"3층부터 내려오며 봅니다. 항구가 어떻게 생겼는지 여기서 먼저 잡고 나갑니다.\", \"searchQuery\": \"군산근대역사박물관\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 46, \"moveMinutes\": 11, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"아무 데도 안 가는 2박 3일\", \"duration\": \"2박 3일\", \"audience\": \"쉬려고만 오는 손님\", \"why\": \"가운데 날은 대문 밖으로 두 번만 나갑니다. 나머지는 마당과 카페 공간에서 보냅니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"15:00\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 50, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 히로쓰 가옥\", \"minutes\": 45, \"moveMinutes\": 3, \"latitude\": 35.9861696, \"longitude\": 126.7060575, \"note\": \"담 하나 건너입니다. 마당과 다다미방을 둘러보고 2층 계단까지 올라가 봅니다.\", \"searchQuery\": \"군산 신흥동 일본식가옥\"}, {\"name\": \"오후 · 초원사진관\", \"minutes\": 45, \"moveMinutes\": 4, \"latitude\": 35.987737, \"longitude\": 126.7083826, \"note\": \"안에 들어가 사진관 세트에서 한 장 찍습니다. 걸어서 3분입니다.\", \"searchQuery\": \"군산 초원사진관\"}, {\"name\": \"저녁 · 도란\", \"minutes\": 96, \"moveMinutes\": 3, \"latitude\": 35.986822, \"longitude\": 126.70751, \"note\": \"감바스와 부채살 스테이크에 와인 한 잔으로 저녁을 마무리합니다.\", \"searchQuery\": \"군산 도란\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 48, \"moveMinutes\": 14, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 36, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"09:30\", \"stops\": [{\"name\": \"머뭄에서 쉬는 날 · 스테이 머뭄\", \"minutes\": 232, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"오늘은 나가지 않습니다. 마당 평상과 창고형 카페 공간에서 책을 읽고 낮잠을 잡니다.\"}, {\"name\": \"점심 · 만남스넥\", \"minutes\": 44, \"moveMinutes\": 6, \"latitude\": 35.9841788, \"longitude\": 126.7097763, \"note\": \"군산식 잡탕 한 냄비를 시킵니다. 떡·만두·라면이 같이 끓어 나옵니다.\", \"searchQuery\": \"군산 만남스넥\"}, {\"name\": \"쉼 · 스테이 머뭄\", \"minutes\": 187, \"moveMinutes\": 6, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"숙소로 돌아와 마당 평상이나 창고형 카페 공간에서 쉽니다.\"}, {\"name\": \"저녁 · 일흥옥\", \"minutes\": 55, \"moveMinutes\": 5, \"latitude\": 35.9851498, \"longitude\": 126.7091839, \"note\": \"소고기 국밥으로 저녁을 먹습니다. 백년가게로 지정된 집입니다.\", \"searchQuery\": \"군산 일흥옥\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 33, \"moveMinutes\": 5, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"셋째 날\", \"startTime\": \"09:00\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 98, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 동국사\", \"minutes\": 46, \"moveMinutes\": 6, \"latitude\": 35.9827663, \"longitude\": 126.7074296, \"note\": \"대웅전 처마와 대나무 숲을 봅니다. 종각까지 돌면 35분입니다.\", \"searchQuery\": \"군산 동국사\"}, {\"name\": \"점심 · 물밀소\", \"minutes\": 46, \"moveMinutes\": 3, \"latitude\": 35.9845243, \"longitude\": 126.7073211, \"note\": \"저온숙성 빵을 골라 나옵니다. 마당에서 먹을 것으로 두어 개 더 삽니다.\", \"searchQuery\": \"군산 물밀소\"}, {\"name\": \"오후 · 월명호수\", \"minutes\": 65, \"moveMinutes\": 9, \"latitude\": 35.9766391, \"longitude\": 126.6913696, \"note\": \"호수를 한 바퀴 돕니다. 천천히 걸어 40분 코스입니다.\", \"searchQuery\": \"군산 월명호수\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 52, \"moveMinutes\": 9, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"사진만 찍는 2박 3일\", \"duration\": \"2박 3일\", \"audience\": \"사진을 찍는 손님\", \"why\": \"시간대에 맞춰 자리를 배치했습니다. 철길마을은 오전, 부잔교는 해 질 때입니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"15:50\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 32, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 초원사진관\", \"minutes\": 24, \"moveMinutes\": 4, \"latitude\": 35.987737, \"longitude\": 126.7083826, \"note\": \"안에 들어가 사진관 세트에서 한 장 찍습니다. 걸어서 3분입니다.\", \"searchQuery\": \"군산 초원사진관\"}, {\"name\": \"오후 · 동국사\", \"minutes\": 28, \"moveMinutes\": 8, \"latitude\": 35.9827663, \"longitude\": 126.7074296, \"note\": \"대웅전 처마와 대나무 숲을 봅니다. 종각까지 돌면 35분입니다.\", \"searchQuery\": \"군산 동국사\"}, {\"name\": \"저녁 · 도란\", \"minutes\": 64, \"moveMinutes\": 7, \"latitude\": 35.986822, \"longitude\": 126.70751, \"note\": \"감바스와 부채살 스테이크에 와인 한 잔으로 저녁을 마무리합니다.\", \"searchQuery\": \"군산 도란\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 32, \"moveMinutes\": 14, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 24, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"09:40\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 48, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"부엌에서 아침을 차려 먹고 마당에서 커피를 내려 마신 뒤 나섭니다.\"}, {\"name\": \"오전 · 경암동 철길마을\", \"minutes\": 40, \"moveMinutes\": 12, \"latitude\": 35.981352, \"longitude\": 126.7362331, \"note\": \"선로를 따라 걸으며 옛 간판을 봅니다. 교복을 빌려 입고 찍는 집이 여럿입니다.\", \"searchQuery\": \"군산 경암동 철길마을\"}, {\"name\": \"점심 · 만남스넥\", \"minutes\": 43, \"moveMinutes\": 11, \"latitude\": 35.9841788, \"longitude\": 126.7097763, \"note\": \"군산식 잡탕 한 냄비를 시킵니다. 떡·만두·라면이 같이 끓어 나옵니다.\", \"searchQuery\": \"군산 만남스넥\"}, {\"name\": \"오후 · 째보선창\", \"minutes\": 45, \"moveMinutes\": 15, \"latitude\": 35.9875345, \"longitude\": 126.7199875, \"note\": \"선창을 따라 걷습니다. 아직 배가 드나드는 자리라 아침이 더 볼 게 많습니다.\", \"searchQuery\": \"군산 째보선창\"}, {\"name\": \"오후 · 해망굴\", \"minutes\": 30, \"moveMinutes\": 21, \"latitude\": 35.9913181, \"longitude\": 126.705, \"note\": \"굴을 걸어서 통과해 반대편 항구 쪽으로 나옵니다.\", \"searchQuery\": \"군산 해망굴\"}, {\"name\": \"쉼 · 스테이 머뭄\", \"minutes\": 190, \"moveMinutes\": 8, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"숙소로 돌아와 마당 평상이나 창고형 카페 공간에서 쉽니다.\"}, {\"name\": \"저녁 · 국제반점\", \"minutes\": 45, \"moveMinutes\": 7, \"latitude\": 35.98811, \"longitude\": 126.7108985, \"note\": \"영화 「타짜」에 나온 홀에서 짜장과 탕수육을 시킵니다.\", \"searchQuery\": \"군산 국제반점\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 24, \"moveMinutes\": 7, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"셋째 날\", \"startTime\": \"08:50\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 108, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 우체통거리\", \"minutes\": 40, \"moveMinutes\": 8, \"latitude\": 35.9859965, \"longitude\": 126.7119708, \"note\": \"주민들이 손질해 내놓은 우체통을 따라 걷습니다. 엽서를 부치고 가도 됩니다.\", \"searchQuery\": \"군산 우체통거리\"}, {\"name\": \"점심 · 일흥옥\", \"minutes\": 40, \"moveMinutes\": 4, \"latitude\": 35.9851498, \"longitude\": 126.7091839, \"note\": \"소고기 국밥으로 저녁을 먹습니다. 백년가게로 지정된 집입니다.\", \"searchQuery\": \"군산 일흥옥\"}, {\"name\": \"오후 · 군산근대미술관\", \"minutes\": 36, \"moveMinutes\": 8, \"latitude\": 35.9899368, \"longitude\": 126.7108, \"note\": \"옛 18은행 건물 안 전시를 봅니다. 마당 벤치에서 한 번 쉬어 갑니다.\", \"searchQuery\": \"군산근대미술관\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 32, \"moveMinutes\": 9, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"비가 와도 되는 2박 3일\", \"duration\": \"2박 3일\", \"audience\": \"비 예보를 보고 오는 손님\", \"why\": \"사흘 내리 비가 와도 도는 순서가 바뀌지 않습니다. 실내와 처마 밑만 잇습니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"15:00\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 40, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 군산근대역사박물관\", \"minutes\": 90, \"moveMinutes\": 11, \"latitude\": 35.9908197, \"longitude\": 126.7121232, \"note\": \"3층부터 내려오며 봅니다. 항구가 어떻게 생겼는지 여기서 먼저 잡고 나갑니다.\", \"searchQuery\": \"군산근대역사박물관\"}, {\"name\": \"오후 · 군산근대미술관\", \"minutes\": 45, \"moveMinutes\": 3, \"latitude\": 35.9899368, \"longitude\": 126.7108, \"note\": \"옛 18은행 건물 안 전시를 봅니다. 마당 벤치에서 한 번 쉬어 갑니다.\", \"searchQuery\": \"군산근대미술관\"}, {\"name\": \"저녁 · 빈해원\", \"minutes\": 70, \"moveMinutes\": 5, \"latitude\": 35.9888841, \"longitude\": 126.7139765, \"note\": \"2층 홀에 앉아 짬뽕이나 잡채밥을 시킵니다. 건물 자체가 문화재입니다.\", \"searchQuery\": \"군산 빈해원\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 40, \"moveMinutes\": 5, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 30, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"10:10\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 72, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"부엌에서 아침을 차려 먹고 마당에서 커피를 내려 마신 뒤 나섭니다.\"}, {\"name\": \"오전 · 군산근대건축관\", \"minutes\": 54, \"moveMinutes\": 10, \"latitude\": 35.9894202, \"longitude\": 126.7121232, \"note\": \"옛 조선은행 금고와 은행 창구를 봅니다. 2층 전시까지 보면 45분입니다.\", \"searchQuery\": \"군산근대건축관\"}, {\"name\": \"점심 · 일흥옥\", \"minutes\": 60, \"moveMinutes\": 8, \"latitude\": 35.9851498, \"longitude\": 126.7091839, \"note\": \"소고기 국밥으로 저녁을 먹습니다. 백년가게로 지정된 집입니다.\", \"searchQuery\": \"군산 일흥옥\"}, {\"name\": \"오후 · 째보선창 근처 카페\", \"minutes\": 72, \"moveMinutes\": 15, \"latitude\": 35.9875345, \"longitude\": 126.7199875, \"note\": \"창가에 앉아 비가 그칠 때까지 기다립니다. 항구가 보이는 자리입니다.\", \"searchQuery\": \"군산 째보선창 카페\"}, {\"name\": \"오후 · 동국사다온\", \"minutes\": 48, \"moveMinutes\": 8, \"latitude\": 35.9860907, \"longitude\": 126.71444, \"note\": \"대추차나 오미자차를 시켜 절 마당을 보며 마십니다.\", \"searchQuery\": \"군산 동국사 다온\"}, {\"name\": \"쉼 · 스테이 머뭄\", \"minutes\": 84, \"moveMinutes\": 11, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"숙소로 돌아와 마당 평상이나 창고형 카페 공간에서 쉽니다.\"}, {\"name\": \"저녁 · 명월갈비\", \"minutes\": 90, \"moveMinutes\": 5, \"latitude\": 35.9863914, \"longitude\": 126.7101663, \"note\": \"한우 양념갈비 한 상을 시킵니다. 이 집은 단품 하나로 버텨 왔습니다.\", \"searchQuery\": \"군산 명월갈비\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 36, \"moveMinutes\": 5, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"셋째 날\", \"startTime\": \"09:40\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 80, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 채만식문학관\", \"minutes\": 72, \"moveMinutes\": 18, \"latitude\": 36.0085981, \"longitude\": 126.7578853, \"note\": \"배 모양 전시관을 돌고 금강 쪽 데크로 나가 강을 봅니다.\", \"searchQuery\": \"군산 채만식문학관\"}, {\"name\": \"점심 · 한일옥\", \"minutes\": 60, \"moveMinutes\": 17, \"latitude\": 35.9874081, \"longitude\": 126.71, \"note\": \"무국으로 아침 겸 점심을 엽니다. 줄이 있으면 이름을 적고 기다립니다.\", \"searchQuery\": \"군산 한일옥\"}, {\"name\": \"오후 · 이성당\", \"minutes\": 42, \"moveMinutes\": 3, \"latitude\": 35.9870443, \"longitude\": 126.7111681, \"note\": \"단팥빵과 야채빵을 사서 나옵니다. 갓 나오는 시간에 맞추면 줄이 짧습니다.\", \"searchQuery\": \"군산 이성당\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 48, \"moveMinutes\": 7, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"늦게 시작하는 2박 3일\", \"duration\": \"2박 3일\", \"audience\": \"아침이 힘든 손님\", \"why\": \"가운데 날을 열한 시에 엽니다. 오전을 비우고 저녁을 늦게까지 씁니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"16:00\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 40, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 히로쓰 가옥\", \"minutes\": 30, \"moveMinutes\": 3, \"latitude\": 35.9861696, \"longitude\": 126.7060575, \"note\": \"담 하나 건너입니다. 마당과 다다미방을 둘러보고 2층 계단까지 올라가 봅니다.\", \"searchQuery\": \"군산 신흥동 일본식가옥\"}, {\"name\": \"오후 · 우체통거리\", \"minutes\": 25, \"moveMinutes\": 8, \"latitude\": 35.9859965, \"longitude\": 126.7119708, \"note\": \"주민들이 손질해 내놓은 우체통을 따라 걷습니다. 엽서를 부치고 가도 됩니다.\", \"searchQuery\": \"군산 우체통거리\"}, {\"name\": \"저녁 · 도란\", \"minutes\": 80, \"moveMinutes\": 6, \"latitude\": 35.986822, \"longitude\": 126.70751, \"note\": \"감바스와 부채살 스테이크에 와인 한 잔으로 저녁을 마무리합니다.\", \"searchQuery\": \"군산 도란\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 40, \"moveMinutes\": 14, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 30, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"10:30\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 60, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"부엌에서 아침을 차려 먹고 마당에서 커피를 내려 마신 뒤 나섭니다.\"}, {\"name\": \"점심 · 일흥옥\", \"minutes\": 50, \"moveMinutes\": 5, \"latitude\": 35.9851498, \"longitude\": 126.7091839, \"note\": \"소고기 국밥으로 저녁을 먹습니다. 백년가게로 지정된 집입니다.\", \"searchQuery\": \"군산 일흥옥\"}, {\"name\": \"오후 · 군산근대역사박물관\", \"minutes\": 90, \"moveMinutes\": 10, \"latitude\": 35.9908197, \"longitude\": 126.7121232, \"note\": \"3층부터 내려오며 봅니다. 항구가 어떻게 생겼는지 여기서 먼저 잡고 나갑니다.\", \"searchQuery\": \"군산근대역사박물관\"}, {\"name\": \"오후 · 해망굴\", \"minutes\": 20, \"moveMinutes\": 10, \"latitude\": 35.9913181, \"longitude\": 126.705, \"note\": \"굴을 걸어서 통과해 반대편 항구 쪽으로 나옵니다.\", \"searchQuery\": \"군산 해망굴\"}, {\"name\": \"쉼 · 스테이 머뭄\", \"minutes\": 156, \"moveMinutes\": 8, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"숙소로 돌아와 마당 평상이나 창고형 카페 공간에서 쉽니다.\"}, {\"name\": \"저녁 · 빈해원\", \"minutes\": 70, \"moveMinutes\": 11, \"latitude\": 35.9888841, \"longitude\": 126.7139765, \"note\": \"2층 홀에 앉아 짬뽕이나 잡채밥을 시킵니다. 건물 자체가 문화재입니다.\", \"searchQuery\": \"군산 빈해원\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 30, \"moveMinutes\": 11, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"셋째 날\", \"startTime\": \"10:10\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 50, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 이성당\", \"minutes\": 35, \"moveMinutes\": 7, \"latitude\": 35.9870443, \"longitude\": 126.7111681, \"note\": \"단팥빵과 야채빵을 사서 나옵니다. 갓 나오는 시간에 맞추면 줄이 짧습니다.\", \"searchQuery\": \"군산 이성당\"}, {\"name\": \"점심 · 만남스넥\", \"minutes\": 40, \"moveMinutes\": 5, \"latitude\": 35.9841788, \"longitude\": 126.7097763, \"note\": \"군산식 잡탕 한 냄비를 시킵니다. 떡·만두·라면이 같이 끓어 나옵니다.\", \"searchQuery\": \"군산 만남스넥\"}, {\"name\": \"오후 · 초원사진관\", \"minutes\": 30, \"moveMinutes\": 6, \"latitude\": 35.987737, \"longitude\": 126.7083826, \"note\": \"안에 들어가 사진관 세트에서 한 장 찍습니다. 걸어서 3분입니다.\", \"searchQuery\": \"군산 초원사진관\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 40, \"moveMinutes\": 4, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"섬에서 하루를 다 쓰는 2박 3일\", \"duration\": \"2박 3일\", \"audience\": \"차를 가져온 손님\", \"why\": \"가운데 날은 아침에 나가 저녁에 들어옵니다. 섬에서 하루를 통째로 씁니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"15:00\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 38, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 군산 내항 부잔교\", \"minutes\": 36, \"moveMinutes\": 16, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"뜬다리 위를 끝까지 걸어 나갔다 돌아옵니다. 조수에 따라 다리가 오르내립니다.\", \"searchQuery\": \"군산 내항 부잔교\"}, {\"name\": \"오후 · 해망굴\", \"minutes\": 30, \"moveMinutes\": 17, \"latitude\": 35.9913181, \"longitude\": 126.705, \"note\": \"굴을 걸어서 통과해 반대편 항구 쪽으로 나옵니다.\", \"searchQuery\": \"군산 해망굴\"}, {\"name\": \"저녁 · 빈해원\", \"minutes\": 66, \"moveMinutes\": 13, \"latitude\": 35.9888841, \"longitude\": 126.7139765, \"note\": \"2층 홀에 앉아 짬뽕이나 잡채밥을 시킵니다. 건물 자체가 문화재입니다.\", \"searchQuery\": \"군산 빈해원\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 38, \"moveMinutes\": 5, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 28, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"07:30\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 57, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"부엌에서 아침을 차려 먹고 마당에서 커피를 내려 마신 뒤 나섭니다.\"}, {\"name\": \"오전 · 고군산군도 선유도\", \"minutes\": 142, \"moveMinutes\": 84, \"latitude\": 35.810205, \"longitude\": 126.4124053, \"note\": \"다리를 건너 들어갑니다. 망주봉 아래 해변을 걷고 스카이라인을 탑니다.\", \"searchQuery\": \"군산 선유도\"}, {\"name\": \"오후 · 장자도\", \"minutes\": 48, \"moveMinutes\": 20, \"latitude\": 35.8101, \"longitude\": 126.3973, \"note\": \"대장봉까지 올라갔다 내려옵니다. 섬과 섬 사이를 걸어 넘습니다.\", \"searchQuery\": \"군산 장자도\"}, {\"name\": \"늦은 점심 · 한일옥\", \"minutes\": 48, \"moveMinutes\": 88, \"latitude\": 35.9874081, \"longitude\": 126.71, \"note\": \"무국으로 아침 겸 점심을 엽니다. 줄이 있으면 이름을 적고 기다립니다.\", \"searchQuery\": \"군산 한일옥\"}, {\"name\": \"쉼 · 스테이 머뭄\", \"minutes\": 170, \"moveMinutes\": 5, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"숙소로 돌아와 마당 평상이나 창고형 카페 공간에서 쉽니다.\"}, {\"name\": \"저녁 · 명월갈비\", \"minutes\": 71, \"moveMinutes\": 5, \"latitude\": 35.9863914, \"longitude\": 126.7101663, \"note\": \"한우 양념갈비 한 상을 시킵니다. 이 집은 단품 하나로 버텨 왔습니다.\", \"searchQuery\": \"군산 명월갈비\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 28, \"moveMinutes\": 5, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"셋째 날\", \"startTime\": \"09:40\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 71, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 군산근대건축관\", \"minutes\": 43, \"moveMinutes\": 10, \"latitude\": 35.9894202, \"longitude\": 126.7121232, \"note\": \"옛 조선은행 금고와 은행 창구를 봅니다. 2층 전시까지 보면 45분입니다.\", \"searchQuery\": \"군산근대건축관\"}, {\"name\": \"점심 · 만남스넥\", \"minutes\": 38, \"moveMinutes\": 9, \"latitude\": 35.9841788, \"longitude\": 126.7097763, \"note\": \"군산식 잡탕 한 냄비를 시킵니다. 떡·만두·라면이 같이 끓어 나옵니다.\", \"searchQuery\": \"군산 만남스넥\"}, {\"name\": \"오후 · 경암동 철길마을\", \"minutes\": 48, \"moveMinutes\": 11, \"latitude\": 35.981352, \"longitude\": 126.7362331, \"note\": \"선로를 따라 걸으며 옛 간판을 봅니다. 교복을 빌려 입고 찍는 집이 여럿입니다.\", \"searchQuery\": \"군산 경암동 철길마을\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 38, \"moveMinutes\": 12, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"소설 『탁류』를 따라 걷는 2박 3일\", \"duration\": \"2박 3일\", \"audience\": \"채만식을 읽어 본 손님\", \"why\": \"소설의 순서대로 사흘을 나눴습니다. 선창에서 시작해 문학관에서 끝냅니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"15:00\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 44, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 째보선창\", \"minutes\": 33, \"moveMinutes\": 19, \"latitude\": 35.9875345, \"longitude\": 126.7199875, \"note\": \"선창을 따라 걷습니다. 아직 배가 드나드는 자리라 아침이 더 볼 게 많습니다.\", \"searchQuery\": \"군산 째보선창\"}, {\"name\": \"오후 · 군산 내항 부잔교\", \"minutes\": 38, \"moveMinutes\": 5, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"뜬다리 위를 끝까지 걸어 나갔다 돌아옵니다. 조수에 따라 다리가 오르내립니다.\", \"searchQuery\": \"군산 내항 부잔교\"}, {\"name\": \"저녁 · 일흥옥\", \"minutes\": 55, \"moveMinutes\": 13, \"latitude\": 35.9851498, \"longitude\": 126.7091839, \"note\": \"소고기 국밥으로 저녁을 먹습니다. 백년가게로 지정된 집입니다.\", \"searchQuery\": \"군산 일흥옥\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 44, \"moveMinutes\": 13, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 33, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"09:30\", \"stops\": [{\"name\": \"머뭄에서 쉬는 날 · 스테이 머뭄\", \"minutes\": 231, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"오늘은 나가지 않습니다. 마당 평상과 창고형 카페 공간에서 책을 읽고 낮잠을 잡니다.\"}, {\"name\": \"점심 · 한일옥\", \"minutes\": 55, \"moveMinutes\": 5, \"latitude\": 35.9874081, \"longitude\": 126.71, \"note\": \"무국으로 아침 겸 점심을 엽니다. 줄이 있으면 이름을 적고 기다립니다.\", \"searchQuery\": \"군산 한일옥\"}, {\"name\": \"쉼 · 스테이 머뭄\", \"minutes\": 173, \"moveMinutes\": 5, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"숙소로 돌아와 마당 평상이나 창고형 카페 공간에서 쉽니다.\"}, {\"name\": \"저녁 · 빈해원\", \"minutes\": 77, \"moveMinutes\": 11, \"latitude\": 35.9888841, \"longitude\": 126.7139765, \"note\": \"2층 홀에 앉아 짬뽕이나 잡채밥을 시킵니다. 건물 자체가 문화재입니다.\", \"searchQuery\": \"군산 빈해원\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 33, \"moveMinutes\": 11, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"셋째 날\", \"startTime\": \"08:30\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 82, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 군산근대역사박물관\", \"minutes\": 99, \"moveMinutes\": 11, \"latitude\": 35.9908197, \"longitude\": 126.7121232, \"note\": \"3층부터 내려오며 봅니다. 항구가 어떻게 생겼는지 여기서 먼저 잡고 나갑니다.\", \"searchQuery\": \"군산근대역사박물관\"}, {\"name\": \"점심 · 국제반점\", \"minutes\": 61, \"moveMinutes\": 5, \"latitude\": 35.98811, \"longitude\": 126.7108985, \"note\": \"영화 「타짜」에 나온 홀에서 짜장과 탕수육을 시킵니다.\", \"searchQuery\": \"군산 국제반점\"}, {\"name\": \"오후 · 초원사진관\", \"minutes\": 33, \"moveMinutes\": 3, \"latitude\": 35.987737, \"longitude\": 126.7083826, \"note\": \"안에 들어가 사진관 세트에서 한 장 찍습니다. 걸어서 3분입니다.\", \"searchQuery\": \"군산 초원사진관\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 44, \"moveMinutes\": 4, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}, {\"name\": \"철길마을과 항구를 같이 보는 2박 3일\", \"duration\": \"2박 3일\", \"audience\": \"사진을 찍는 손님\", \"why\": \"기찻길과 물길을 하루씩 나눠 봅니다. 마지막 날은 건물만 봅니다.\", \"days\": [{\"label\": \"첫째 날\", \"startTime\": \"16:00\", \"stops\": [{\"name\": \"체크인 · 스테이 머뭄\", \"minutes\": 36, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다.\"}, {\"name\": \"오후 · 경암동 철길마을\", \"minutes\": 45, \"moveMinutes\": 12, \"latitude\": 35.981352, \"longitude\": 126.7362331, \"note\": \"선로를 따라 걸으며 옛 간판을 봅니다. 교복을 빌려 입고 찍는 집이 여럿입니다.\", \"searchQuery\": \"군산 경암동 철길마을\"}, {\"name\": \"오후 · 초원사진관\", \"minutes\": 27, \"moveMinutes\": 11, \"latitude\": 35.987737, \"longitude\": 126.7083826, \"note\": \"안에 들어가 사진관 세트에서 한 장 찍습니다. 걸어서 3분입니다.\", \"searchQuery\": \"군산 초원사진관\"}, {\"name\": \"저녁 · 국제반점\", \"minutes\": 50, \"moveMinutes\": 3, \"latitude\": 35.98811, \"longitude\": 126.7108985, \"note\": \"영화 「타짜」에 나온 홀에서 짜장과 탕수육을 시킵니다.\", \"searchQuery\": \"군산 국제반점\"}, {\"name\": \"밤 산책 · 군산 내항\", \"minutes\": 36, \"moveMinutes\": 9, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"조명이 들어온 부잔교를 따라 걷습니다. 낮과 아주 다른 자리입니다.\", \"searchQuery\": \"군산 내항\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 27, \"moveMinutes\": 16, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"둘째 날\", \"startTime\": \"09:40\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 54, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"부엌에서 아침을 차려 먹고 마당에서 커피를 내려 마신 뒤 나섭니다.\"}, {\"name\": \"오전 · 군산 내항 부잔교\", \"minutes\": 32, \"moveMinutes\": 16, \"latitude\": 35.9892997, \"longitude\": 126.7174508, \"note\": \"뜬다리 위를 끝까지 걸어 나갔다 돌아옵니다. 조수에 따라 다리가 오르내립니다.\", \"searchQuery\": \"군산 내항 부잔교\"}, {\"name\": \"점심 · 한일옥\", \"minutes\": 45, \"moveMinutes\": 11, \"latitude\": 35.9874081, \"longitude\": 126.71, \"note\": \"무국으로 아침 겸 점심을 엽니다. 줄이 있으면 이름을 적고 기다립니다.\", \"searchQuery\": \"군산 한일옥\"}, {\"name\": \"오후 · 째보선창\", \"minutes\": 45, \"moveMinutes\": 13, \"latitude\": 35.9875345, \"longitude\": 126.7199875, \"note\": \"선창을 따라 걷습니다. 아직 배가 드나드는 자리라 아침이 더 볼 게 많습니다.\", \"searchQuery\": \"군산 째보선창\"}, {\"name\": \"오후 · 해망굴\", \"minutes\": 30, \"moveMinutes\": 21, \"latitude\": 35.9913181, \"longitude\": 126.705, \"note\": \"굴을 걸어서 통과해 반대편 항구 쪽으로 나옵니다.\", \"searchQuery\": \"군산 해망굴\"}, {\"name\": \"쉼 · 스테이 머뭄\", \"minutes\": 190, \"moveMinutes\": 8, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"숙소로 돌아와 마당 평상이나 창고형 카페 공간에서 쉽니다.\"}, {\"name\": \"저녁 · 명월갈비\", \"minutes\": 68, \"moveMinutes\": 5, \"latitude\": 35.9863914, \"longitude\": 126.7101663, \"note\": \"한우 양념갈비 한 상을 시킵니다. 이 집은 단품 하나로 버텨 왔습니다.\", \"searchQuery\": \"군산 명월갈비\"}, {\"name\": \"머뭄으로 · 스테이 머뭄\", \"minutes\": 27, \"moveMinutes\": 5, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다.\"}]}, {\"label\": \"셋째 날\", \"startTime\": \"09:30\", \"stops\": [{\"name\": \"아침 · 스테이 머뭄\", \"minutes\": 68, \"moveMinutes\": 0, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요.\"}, {\"name\": \"오전 · 구 군산세관 본관\", \"minutes\": 27, \"moveMinutes\": 13, \"latitude\": 35.9915088, \"longitude\": 126.7135, \"note\": \"빨간 벽돌 본관을 한 바퀴 돌고 앞마당에서 사진을 찍습니다.\", \"searchQuery\": \"구 군산세관\"}, {\"name\": \"점심 · 일흥옥\", \"minutes\": 45, \"moveMinutes\": 12, \"latitude\": 35.9851498, \"longitude\": 126.7091839, \"note\": \"소고기 국밥으로 저녁을 먹습니다. 백년가게로 지정된 집입니다.\", \"searchQuery\": \"군산 일흥옥\"}, {\"name\": \"오후 · 군산근대미술관\", \"minutes\": 40, \"moveMinutes\": 8, \"latitude\": 35.9899368, \"longitude\": 126.7108, \"note\": \"옛 18은행 건물 안 전시를 봅니다. 마당 벤치에서 한 번 쉬어 갑니다.\", \"searchQuery\": \"군산근대미술관\"}, {\"name\": \"마무리 · 스테이 머뭄\", \"minutes\": 36, \"moveMinutes\": 9, \"latitude\": 35.9864175, \"longitude\": 126.7061254, \"note\": \"맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다.\"}]}]}]}"}, {"id": "story", "name": "군산 이야기", "enabled": true, "locked": false}, {"id": "songs", "name": "가요 다방", "enabled": false, "locked": false, "data": "{\"kind\": \"songs\", \"version\": 1, \"title\": \"가요 다방\", \"subtitle\": \"군산을 노래한 25곡 — 눌러서 들어 보세요\", \"items\": [{\"title\": \"잘 있거라 군산항\", \"artist\": \"안다성\", \"labelColor\": \"#2f6b4f\", \"story\": \"월명산 고갯길로 시작하는 이별가. 군산을 노래한 곡 가운데 가장 널리 알려졌습니다.\", \"verified\": \"확인\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EC%9E%98%20%EC%9E%88%EA%B1%B0%EB%9D%BC%20%EA%B5%B0%EC%82%B0%ED%95%AD%20%EC%95%88%EB%8B%A4%EC%84%B1\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"손석우\", \"composer\": \"손석우\", \"year\": 1963}, {\"title\": \"헤어진 군산항\", \"artist\": \"박제연\", \"labelColor\": \"#2a6053\", \"story\": \"비 내리는 밤 부두에서 헤어지는 노래입니다.\", \"verified\": \"확인\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%ED%97%A4%EC%96%B4%EC%A7%84%20%EA%B5%B0%EC%82%B0%ED%95%AD%20%EB%B0%95%EC%A0%9C%EC%97%B0\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"송운선\", \"composer\": \"송운선\", \"year\": 1965}, {\"title\": \"님 떠난 군산항\", \"artist\": \"이미자\", \"labelColor\": \"#1d3c74\", \"story\": \"떠나는 배를 보내는 항구의 정서를 담았습니다.\", \"verified\": \"확인\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EB%8B%98%20%EB%96%A0%EB%82%9C%20%EA%B5%B0%EC%82%B0%ED%95%AD%20%EC%9D%B4%EB%AF%B8%EC%9E%90\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"조형식\", \"composer\": \"백영호\", \"year\": 1966}, {\"title\": \"금강 나그네\", \"artist\": \"이미자\", \"labelColor\": \"#d4551f\", \"story\": \"금강 하구와 군산항 부두를 배경으로 한 곡입니다.\", \"verified\": \"확인\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EA%B8%88%EA%B0%95%20%EB%82%98%EA%B7%B8%EB%84%A4%20%EC%9D%B4%EB%AF%B8%EC%9E%90\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"이병훈\", \"composer\": \"박춘석\", \"year\": 1966}, {\"title\": \"잘 있거라 군산항구\", \"artist\": \"최정자\", \"labelColor\": \"#8a4b2a\", \"story\": \"떠나는 사람의 말로 부르는 이별가입니다.\", \"verified\": \"확인\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EC%9E%98%20%EC%9E%88%EA%B1%B0%EB%9D%BC%20%EA%B5%B0%EC%82%B0%ED%95%AD%EA%B5%AC%20%EC%B5%9C%EC%A0%95%EC%9E%90\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"최치수\", \"composer\": \"유금춘\", \"year\": 1967}, {\"title\": \"군산 마도로스\", \"artist\": \"이상열\", \"labelColor\": \"#334b6d\", \"story\": \"항구를 드나들던 뱃사람의 노래입니다.\", \"verified\": \"확인\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EA%B5%B0%EC%82%B0%20%EB%A7%88%EB%8F%84%EB%A1%9C%EC%8A%A4%20%EC%9D%B4%EC%83%81%EC%97%B4\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"year\": 1967}, {\"title\": \"군산 부르스\", \"artist\": \"유주용\", \"labelColor\": \"#4a3f6b\", \"story\": \"1960년대 말 군산 거리의 분위기를 담은 곡입니다.\", \"verified\": \"확인\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EA%B5%B0%EC%82%B0%20%EB%B6%80%EB%A5%B4%EC%8A%A4%20%EC%9C%A0%EC%A3%BC%EC%9A%A9\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"반야월\", \"composer\": \"강도미\", \"year\": 1969}, {\"title\": \"이별의 군산항 부두\", \"artist\": \"최정자\", \"labelColor\": \"#6b2f3a\", \"story\": \"부두에서의 이별을 다시 한 번 부른 곡입니다.\", \"verified\": \"확인\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EC%9D%B4%EB%B3%84%EC%9D%98%20%EA%B5%B0%EC%82%B0%ED%95%AD%20%EB%B6%80%EB%91%90%20%EC%B5%9C%EC%A0%95%EC%9E%90\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"최치수\", \"composer\": \"유금춘\", \"year\": 1969}, {\"title\": \"군산항 부르스\", \"artist\": \"최하우\", \"labelColor\": \"#2f5b6b\", \"story\": \"1980년대에 나온 군산항 노래입니다.\", \"verified\": \"확인\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EA%B5%B0%EC%82%B0%ED%95%AD%20%EB%B6%80%EB%A5%B4%EC%8A%A4%20%EC%B5%9C%ED%95%98%EC%9A%B0\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"최하우\", \"composer\": \"김성근\", \"year\": 1984}, {\"title\": \"나의 사랑 군산이여\", \"artist\": \"오단비\", \"labelColor\": \"#a34a2a\", \"story\": \"군산을 향한 애정을 정면으로 부르는 곡입니다.\", \"verified\": \"확인\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EB%82%98%EC%9D%98%20%EC%82%AC%EB%9E%91%20%EA%B5%B0%EC%82%B0%EC%9D%B4%EC%97%AC%20%EC%98%A4%EB%8B%A8%EB%B9%84\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"이수\", \"composer\": \"정종택\", \"year\": 1989}, {\"title\": \"군산항 엘레지\", \"artist\": \"오단비\", \"labelColor\": \"#7a3b52\", \"story\": \"항구 도시의 비가입니다.\", \"verified\": \"확인필요\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EA%B5%B0%EC%82%B0%ED%95%AD%20%EC%97%98%EB%A0%88%EC%A7%80%20%EC%98%A4%EB%8B%A8%EB%B9%84\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"이수\", \"composer\": \"정종택\"}, {\"title\": \"선유도 아가씨\", \"artist\": \"오단비\", \"labelColor\": \"#2b6b7a\", \"story\": \"고군산군도 선유도를 배경으로 한 곡입니다.\", \"verified\": \"확인필요\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EC%84%A0%EC%9C%A0%EB%8F%84%20%EC%95%84%EA%B0%80%EC%94%A8%20%EC%98%A4%EB%8B%A8%EB%B9%84\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"이수\", \"composer\": \"서승일\"}, {\"title\": \"군산찬가\", \"artist\": \"오단비\", \"labelColor\": \"#c8873a\", \"story\": \"은파와 새만금이 가사에 나오는 군산 찬가입니다.\", \"verified\": \"확인필요\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EA%B5%B0%EC%82%B0%EC%B0%AC%EA%B0%80%20%EC%98%A4%EB%8B%A8%EB%B9%84\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"이수\", \"composer\": \"서승일\"}, {\"title\": \"오성산\", \"artist\": \"오단비\", \"labelColor\": \"#4f6b3a\", \"story\": \"금강 하구를 내려다보는 오성산을 노래했습니다.\", \"verified\": \"확인필요\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EC%98%A4%EC%84%B1%EC%82%B0%20%EC%98%A4%EB%8B%A8%EB%B9%84\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"이수\", \"composer\": \"서승일\"}, {\"title\": \"함께하는 군산\", \"artist\": \"오은주\", \"labelColor\": \"#b5603a\", \"story\": \"군산시가 제작한 음반의 첫 곡입니다.\", \"verified\": \"확인필요\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94%20%EA%B5%B0%EC%82%B0%20%EC%98%A4%EC%9D%80%EC%A3%BC\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"김동찬\", \"composer\": \"김동찬\"}, {\"title\": \"새만금 연가\", \"artist\": \"채미영\", \"labelColor\": \"#356b8a\", \"story\": \"새만금을 소재로 한 곡입니다.\", \"verified\": \"확인필요\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EC%83%88%EB%A7%8C%EA%B8%88%20%EC%97%B0%EA%B0%80%20%EC%B1%84%EB%AF%B8%EC%98%81\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"김동찬\", \"composer\": \"김동찬 · 김수환\"}, {\"title\": \"군산시민의 노래\", \"artist\": \"오단비\", \"labelColor\": \"#5a5a4a\", \"story\": \"금만경 들녘과 서해 바다로 시작하는 시민의 노래입니다.\", \"verified\": \"확인필요\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EA%B5%B0%EC%82%B0%EC%8B%9C%EB%AF%BC%EC%9D%98%20%EB%85%B8%EB%9E%98%20%EC%98%A4%EB%8B%A8%EB%B9%84\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"군산문화원\", \"composer\": \"군산예총\"}, {\"title\": \"군산 아줌마\", \"artist\": \"박솔미\", \"labelColor\": \"#a3562a\", \"story\": \"선창가 거리와 새만금이 함께 나오는 곡입니다.\", \"verified\": \"확인필요\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EA%B5%B0%EC%82%B0%20%EC%95%84%EC%A4%8C%EB%A7%88%20%EB%B0%95%EC%86%94%EB%AF%B8\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"송두호\", \"composer\": \"송두호\"}, {\"title\": \"내 고향 군산항\", \"artist\": \"정애란\", \"labelColor\": \"#6b4a2a\", \"story\": \"고향으로서의 군산항을 부른 곡입니다.\", \"verified\": \"확인필요\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EB%82%B4%20%EA%B3%A0%ED%96%A5%20%EA%B5%B0%EC%82%B0%ED%95%AD%20%EC%A0%95%EC%95%A0%EB%9E%80\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"정애란\", \"composer\": \"정애란\"}, {\"title\": \"군산 가는 길\", \"artist\": \"황상준\", \"labelColor\": \"#3a5a4a\", \"story\": \"군산으로 향하는 길을 제목으로 삼은 곡입니다.\", \"verified\": \"확인필요\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EA%B5%B0%EC%82%B0%20%EA%B0%80%EB%8A%94%20%EA%B8%B8%20%ED%99%A9%EC%83%81%EC%A4%80\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}}, {\"title\": \"군산 추억\", \"artist\": \"임인건\", \"labelColor\": \"#4a4a6b\", \"story\": \"재즈 피아니스트 임인건이 군산을 소재로 만든 곡입니다.\", \"verified\": \"확인필요\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EA%B5%B0%EC%82%B0%20%EC%B6%94%EC%96%B5%20%EC%9E%84%EC%9D%B8%EA%B1%B4\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}}, {\"title\": \"군산에서\", \"artist\": \"임인건\", \"labelColor\": \"#3f5f6b\", \"story\": \"같은 연주자가 군산에서 받은 인상을 담았습니다.\", \"verified\": \"확인필요\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EA%B5%B0%EC%82%B0%EC%97%90%EC%84%9C%20%EC%9E%84%EC%9D%B8%EA%B1%B4\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}}, {\"title\": \"헤어진 군산항\", \"artist\": \"김창환\", \"labelColor\": \"#2a6053\", \"story\": \"군산시 제작 음반에 다시 실린 판입니다.\", \"verified\": \"확인필요\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%ED%97%A4%EC%96%B4%EC%A7%84%20%EA%B5%B0%EC%82%B0%ED%95%AD%20%EA%B9%80%EC%B0%BD%ED%99%98\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}, \"lyricist\": \"송운선\", \"composer\": \"송운선\"}, {\"title\": \"금강 아가씨\", \"artist\": \"이미자\", \"labelColor\": \"#d4551f\", \"story\": \"금강을 배경으로 한 이미자의 또 다른 곡입니다.\", \"verified\": \"확인필요\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EA%B8%88%EA%B0%95%20%EC%95%84%EA%B0%80%EC%94%A8%20%EC%9D%B4%EB%AF%B8%EC%9E%90\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}}, {\"title\": \"금강의 비가\", \"artist\": \"전승애\", \"labelColor\": \"#7a4a6b\", \"story\": \"금강을 제목에 올린 곡입니다.\", \"verified\": \"확인필요\", \"listenUrl\": \"https://www.youtube.com/results?search_query=%EA%B8%88%EA%B0%95%EC%9D%98%20%EB%B9%84%EA%B0%80%20%EC%A0%84%EC%8A%B9%EC%95%A0\", \"source\": {\"name\": \"음악의 오솔길 — 군산 관련 노래\", \"url\": \"https://old-music.tistory.com/entry/%EA%B5%B0%EC%82%B0-%EA%B4%80%EB%A0%A8-%EB%85%B8%EB%9E%98\"}}]}"}, {"id": "people", "name": "인물 열전", "enabled": false, "locked": false, "data": "{\"kind\": \"people\", \"version\": 1, \"title\": \"인물 열전\", \"subtitle\": \"군산이 낳은 사람 57명\", \"items\": [{\"name\": \"채만식\", \"role\": \"소설가\", \"oneLine\": \"군산을 무대로 한 『탁류』를 썼습니다. 시내에 문학관이 있습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%B1%84%EB%A7%8C%EC%8B%9D\"}, \"years\": \"1902–1950\", \"imageUrl\": \"/assets/mirror/d902ef61234bd897.png\"}, {\"name\": \"고은\", \"role\": \"시인\", \"oneLine\": \"군산에서 태어나 60년 넘게 시를 써 온 시인입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EA%B3%A0%EC%9D%80\"}, \"years\": \"1933–\", \"imageUrl\": \"/s/stay/img/people/%EA%B3%A0%EC%9D%80.jpg\", \"imageCredit\": \"사진 Matěj Baťha · CC BY-SA 3.0\"}, {\"name\": \"김선기\", \"role\": \"언어학자\", \"oneLine\": \"호는 무돌. 한글 연구에 힘쓴 언어학자이자 교육자입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EA%B9%80%EC%84%A0%EA%B8%B0_%28%EC%96%B8%EC%96%B4%EC%B2%A0%ED%95%99%EC%9E%90%29\"}, \"years\": \"1907–1992\", \"imageUrl\": \"/s/stay/img/people/%EA%B9%80%EC%84%A0%EA%B8%B0.jpg\", \"imageCredit\": \"사진 미상Unknown author · Public domain\"}, {\"name\": \"김수미\", \"role\": \"배우\", \"oneLine\": \"「전원일기」 일용 엄니로 오래 기억되는 배우입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EA%B9%80%EC%88%98%EB%AF%B8\"}, \"years\": \"1949–2024\", \"imageUrl\": \"/s/stay/img/people/%EA%B9%80%EC%88%98%EB%AF%B8.png\", \"imageCredit\": \"사진 Seoul Broadcasting System · CC BY 3.0\"}, {\"name\": \"송새벽\", \"role\": \"배우\", \"oneLine\": \"군산 출신 배우입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%86%A1%EC%83%88%EB%B2%BD\"}, \"years\": \"1979–\", \"imageUrl\": \"/s/stay/img/people/%EC%86%A1%EC%83%88%EB%B2%BD.png\", \"imageCredit\": \"사진 틈새TV · CC BY 3.0\"}, {\"name\": \"장신영\", \"role\": \"배우\", \"oneLine\": \"군산 출신 배우입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%9E%A5%EC%8B%A0%EC%98%81\"}, \"years\": \"1984–\", \"imageUrl\": \"/s/stay/img/people/%EC%9E%A5%EC%8B%A0%EC%98%81.jpg\", \"imageCredit\": \"사진 acrofan.com · CC BY-SA 3.0\"}, {\"name\": \"이서원\", \"role\": \"배우\", \"oneLine\": \"군산 출신 배우입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%9D%B4%EC%84%9C%EC%9B%90\"}, \"years\": \"1997–\", \"imageUrl\": \"/s/stay/img/people/%EC%9D%B4%EC%84%9C%EC%9B%90.png\", \"imageCredit\": \"사진 KBS 뷰 · CC BY 3.0\"}, {\"name\": \"병헌\", \"role\": \"배우\", \"oneLine\": \"그룹 틴탑 활동을 거쳐 배우로 옮겼습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EB%B3%91%ED%97%8C\"}, \"years\": \"1993–\", \"imageUrl\": \"/s/stay/img/people/%EB%B3%91%ED%97%8C.jpg\", \"imageCredit\": \"사진 뉴스인스타 · CC BY 3.0\"}, {\"name\": \"정혁\", \"role\": \"모델\", \"oneLine\": \"2015년 서울패션위크로 데뷔한 모델 겸 방송인입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%A0%95%ED%98%81_%28%EB%AA%A8%EB%8D%B8%29\"}, \"years\": \"1991–\", \"imageUrl\": \"/s/stay/img/people/%EC%A0%95%ED%98%81.jpg\", \"imageCredit\": \"사진 파리게이츠 · CC BY 3.0\"}, {\"name\": \"박화요비\", \"role\": \"가수\", \"oneLine\": \"R&B 를 주로 부르는 가수입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EB%B0%95%ED%99%94%EC%9A%94%EB%B9%84\"}, \"years\": \"1982–\", \"imageUrl\": \"/s/stay/img/people/%EB%B0%95%ED%99%94%EC%9A%94%EB%B9%84.jpg\", \"imageCredit\": \"사진 Jinho.Jung · CC BY-SA 2.0\"}, {\"name\": \"고건\", \"role\": \"정치인\", \"oneLine\": \"제30·35대 국무총리를 지냈습니다. 아버지는 철학자 고형곤입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EA%B3%A0%EA%B1%B4\"}, \"years\": \"1938–\", \"imageUrl\": \"/s/stay/img/people/%EA%B3%A0%EA%B1%B4.jpg\", \"imageCredit\": \"사진 서울특별시 소방재난본부 · CC BY-SA 4.0\"}, {\"name\": \"김관영\", \"role\": \"정치인\", \"oneLine\": \"전북특별자치도지사입니다. 회계사·변호사를 거쳤습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EA%B9%80%EA%B4%80%EC%98%81\"}, \"years\": \"1969–\", \"imageUrl\": \"/s/stay/img/people/%EA%B9%80%EA%B4%80%EC%98%81.jpg\", \"imageCredit\": \"사진 대한민국 국회 의장비서실 · KOGL Type 1\"}, {\"name\": \"함운경\", \"role\": \"정치인\", \"oneLine\": \"1980년대 학생운동가 출신입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%ED%95%A8%EC%9A%B4%EA%B2%BD\"}, \"years\": \"1964–\", \"imageUrl\": \"/s/stay/img/people/%ED%95%A8%EC%9A%B4%EA%B2%BD.jpg\", \"imageCredit\": \"사진 Teakim11 · CC BY-SA 4.0\"}, {\"name\": \"차우찬\", \"role\": \"야구 선수\", \"oneLine\": \"투수로 뛰었고 지금은 야구 해설위원입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%B0%A8%EC%9A%B0%EC%B0%AC\"}, \"years\": \"1987–\", \"imageUrl\": \"/s/stay/img/people/%EC%B0%A8%EC%9A%B0%EC%B0%AC.jpg\", \"imageCredit\": \"사진 원래 올린이는 한국어 위키백과의 Realidad y Illusion · CC BY-SA 2.5\"}, {\"name\": \"이진영\", \"role\": \"야구 선수\", \"oneLine\": \"외야수로 뛰었고 지금은 코치입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%9D%B4%EC%A7%84%EC%98%81_%281980%EB%85%84%29\"}, \"years\": \"1980–\", \"imageUrl\": \"/s/stay/img/people/%EC%9D%B4%EC%A7%84%EC%98%81.jpg\", \"imageCredit\": \"사진 itsmygabble · CC BY 4.0\"}, {\"name\": \"오지환\", \"role\": \"야구 선수\", \"oneLine\": \"LG 트윈스 내야수입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%98%A4%EC%A7%80%ED%99%98\"}, \"years\": \"1990–\", \"imageUrl\": \"/s/stay/img/people/%EC%98%A4%EC%A7%80%ED%99%98.png\", \"imageCredit\": \"사진 OSEN SPORTS · CC BY-SA 3.0\"}, {\"name\": \"최서우\", \"role\": \"스키점프 선수\", \"oneLine\": \"1998년부터 국가대표로 뛴 스키점프 선수입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%B5%9C%EC%84%9C%EC%9A%B0\"}, \"years\": \"1982–\", \"imageUrl\": \"/s/stay/img/people/%EC%B5%9C%EC%84%9C%EC%9A%B0.jpg\", \"imageCredit\": \"사진 Clément Bucco-Lechat · CC BY-SA 3.0\"}, {\"name\": \"문호준\", \"role\": \"프로게이머\", \"oneLine\": \"카트라이더 리그 최다 우승자입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EB%AC%B8%ED%98%B8%EC%A4%80\"}, \"years\": \"1997–\", \"imageUrl\": \"/s/stay/img/people/%EB%AC%B8%ED%98%B8%EC%A4%80.jpg\", \"imageCredit\": \"사진 acrofan.com · CC BY-SA 3.0\"}, {\"name\": \"이병훈\", \"role\": \"시인\", \"oneLine\": \"옥구(지금의 군산) 출신으로 1950년대에 등단한 현대 시인입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%9D%B4%EB%B3%91%ED%9B%88_%28%EC%8B%9C%EC%9D%B8%29\"}, \"years\": \"1925–2009\"}, {\"name\": \"심호택\", \"role\": \"시인\", \"oneLine\": \"민중의 삶을 시로 옮긴 시인입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%8B%AC%ED%98%B8%ED%83%9D\"}, \"years\": \"1947–2010\"}, {\"name\": \"문효치\", \"role\": \"시인\", \"oneLine\": \"군산에서 태어나 1966년 신춘문예로 등단했습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EB%AC%B8%ED%9A%A8%EC%B9%98\"}, \"years\": \"1943–\"}, {\"name\": \"이연주\", \"role\": \"시인\", \"oneLine\": \"짧게 활동하고 떠난 시인입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%9D%B4%EC%97%B0%EC%A3%BC_%28%EC%8B%9C%EC%9D%B8%29\"}, \"years\": \"1953–1992\"}, {\"name\": \"주하림\", \"role\": \"시인\", \"oneLine\": \"군산에서 태어나 2009년 창비신인시인상으로 등단했습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%A3%BC%ED%95%98%EB%A6%BC\"}}, {\"name\": \"고형곤\", \"role\": \"철학자\", \"oneLine\": \"선(禪) 연구로 알려진 철학자이자 정치가입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EA%B3%A0%ED%98%95%EA%B3%A4\"}, \"years\": \"1906–2004\"}, {\"name\": \"반석평\", \"role\": \"조선 문신\", \"oneLine\": \"노비 출신으로 문과에 급제해 판서에 오른 조선 중종 때 문신입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EB%B0%98%EC%84%9D%ED%8F%89\"}, \"years\": \"1472–1540\"}, {\"name\": \"장재호\", \"role\": \"배우\", \"oneLine\": \"군산 출신 배우입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%9E%A5%EC%9E%AC%ED%98%B8_%28%EB%B0%B0%EC%9A%B0%29\"}, \"years\": \"1986–\"}, {\"name\": \"강민지\", \"role\": \"배우 · 모델\", \"oneLine\": \"모델로 시작해 드라마와 영화로 활동 범위를 넓혔습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EA%B0%95%EB%AF%BC%EC%A7%80_%28%EB%B0%B0%EC%9A%B0%29\"}, \"years\": \"1996–\"}, {\"name\": \"김혜선\", \"role\": \"희극인\", \"oneLine\": \"군산 출신 희극 배우입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EA%B9%80%ED%98%9C%EC%84%A0_%28%ED%9D%AC%EA%B7%B9%EC%9D%B8%29\"}, \"years\": \"1983–\"}, {\"name\": \"이옥주\", \"role\": \"희극인\", \"oneLine\": \"군산중앙여고를 나와 1988년 MBC 개그콘테스트 대상을 받았습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%9D%B4%EC%98%A5%EC%A3%BC_%28%ED%9D%AC%EA%B7%B9%EC%9D%B8%29\"}, \"years\": \"1969–\"}, {\"name\": \"박윤경\", \"role\": \"가수\", \"oneLine\": \"군산 출신 가수 겸 기업인입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EB%B0%95%EC%9C%A4%EA%B2%BD_%28%EA%B0%80%EC%88%98%29\"}, \"years\": \"1970–\"}, {\"name\": \"하준\", \"role\": \"가수\", \"oneLine\": \"군산 출신 가수입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%ED%95%98%EC%A4%80_%28%EA%B0%80%EC%88%98%29\"}, \"years\": \"1965–\"}, {\"name\": \"강봉균\", \"role\": \"정치인\", \"oneLine\": \"재정경제부 장관과 3선 국회의원을 지냈습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EA%B0%95%EB%B4%89%EA%B7%A0_%28%EC%A0%95%EC%B9%98%EC%9D%B8%29\"}, \"years\": \"1943–2017\"}, {\"name\": \"양일동\", \"role\": \"정치인\", \"oneLine\": \"옥구군 서수면 출신으로 야당 정치인으로 활동했습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%96%91%EC%9D%BC%EB%8F%99\"}, \"years\": \"1912–1980\"}, {\"name\": \"채영석\", \"role\": \"정치인\", \"oneLine\": \"군산에서 3선 국회의원을 지냈습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%B1%84%EC%98%81%EC%84%9D\"}, \"years\": \"1934–2006\"}, {\"name\": \"신영대\", \"role\": \"정치인\", \"oneLine\": \"군산 지역구 국회의원입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%8B%A0%EC%98%81%EB%8C%80\"}, \"years\": \"1968–\"}, {\"name\": \"김의겸\", \"role\": \"언론인 · 정치인\", \"oneLine\": \"한겨레 기자를 거쳐 청와대 대변인과 국회의원을 지냈습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EA%B9%80%EC%9D%98%EA%B2%B8\"}, \"years\": \"1963–\"}, {\"name\": \"임병찬\", \"role\": \"의병장\", \"oneLine\": \"최익현과 함께 거병한 구한말 의병장으로, 단식 끝에 순국했습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%9E%84%EB%B3%91%EC%B0%AC\"}, \"years\": \"1851–1916\"}, {\"name\": \"이수현\", \"role\": \"독립운동가\", \"oneLine\": \"호는 산남. 군산 출신 독립운동가입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%9D%B4%EC%88%98%ED%98%84_%281895%EB%85%84%29\"}, \"years\": \"1895–1980\"}, {\"name\": \"전진\", \"role\": \"비전향 장기수\", \"oneLine\": \"군산 출신으로 오래 수감 생활을 했습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%A0%84%EC%A7%84_%281923%EB%85%84%29\"}, \"years\": \"1923–2012\"}, {\"name\": \"이길여\", \"role\": \"의료인 · 교육자\", \"oneLine\": \"가천대학교 길병원을 세우고 가천대 총장을 맡고 있습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%9D%B4%EA%B8%B8%EC%97%AC\"}, \"years\": \"1932–\"}, {\"name\": \"최길선\", \"role\": \"기업인\", \"oneLine\": \"현대중공업 회장을 지낸 조선업 경영인입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%B5%9C%EA%B8%B8%EC%84%A0\"}, \"years\": \"1946–\"}, {\"name\": \"은성수\", \"role\": \"공무원\", \"oneLine\": \"제7대 금융위원회 위원장을 지냈습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%9D%80%EC%84%B1%EC%88%98_%28%EA%B3%B5%EB%AC%B4%EC%9B%90%29\"}, \"years\": \"1961–\"}, {\"name\": \"채금석\", \"role\": \"축구 선수\", \"oneLine\": \"일제강점기 경성 축구단 선수로 경평축구대항전에 나섰습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%B1%84%EA%B8%88%EC%84%9D\"}, \"years\": \"1904–1995\"}, {\"name\": \"박경완\", \"role\": \"야구 선수\", \"oneLine\": \"SK 와이번스 포수로 한국 야구의 대표적인 포수였습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EB%B0%95%EA%B2%BD%EC%99%84\"}, \"years\": \"1972–\"}, {\"name\": \"조계현\", \"role\": \"야구 선수\", \"oneLine\": \"해태 타이거즈 투수로 뛰었습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%A1%B0%EA%B3%84%ED%98%84\"}, \"years\": \"1964–\"}, {\"name\": \"정명원\", \"role\": \"야구 선수\", \"oneLine\": \"현대 유니콘스 투수로 뛰었습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%A0%95%EB%AA%85%EC%9B%90\"}, \"years\": \"1966–\"}, {\"name\": \"조규제\", \"role\": \"야구 선수\", \"oneLine\": \"왼손 투수로 뛰고 지금은 투수코치입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%A1%B0%EA%B7%9C%EC%A0%9C\"}, \"years\": \"1967–\"}, {\"name\": \"이천웅\", \"role\": \"야구 선수\", \"oneLine\": \"LG 트윈스 외야수로 뛰었습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%9D%B4%EC%B2%9C%EC%9B%85\"}, \"years\": \"1988–\"}, {\"name\": \"노상래\", \"role\": \"축구 선수\", \"oneLine\": \"공격수로 뛰었고 지도자로 일하고 있습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EB%85%B8%EC%83%81%EB%9E%98\"}, \"years\": \"1970–\"}, {\"name\": \"노수진\", \"role\": \"축구 선수\", \"oneLine\": \"공격수로 뛰었고 지금은 체육 교사입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EB%85%B8%EC%88%98%EC%A7%84\"}, \"years\": \"1962–\"}, {\"name\": \"유동우\", \"role\": \"축구 선수\", \"oneLine\": \"수비수로 뛰었고 대학 축구부 감독을 맡고 있습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%9C%A0%EB%8F%99%EC%9A%B0\"}, \"years\": \"1968–\"}, {\"name\": \"이정효\", \"role\": \"축구 감독\", \"oneLine\": \"풀백으로 뛰었고 지금은 프로팀 감독입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%9D%B4%EC%A0%95%ED%9A%A8\"}, \"years\": \"1975–\"}, {\"name\": \"박성현\", \"role\": \"양궁 선수\", \"oneLine\": \"군산 소룡초에서 활을 처음 잡아 올림픽 금메달을 땄습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EB%B0%95%EC%84%B1%ED%98%84_%28%EC%96%91%EA%B6%81_%EC%84%A0%EC%88%98%29\"}, \"years\": \"1983–\"}, {\"name\": \"김광선\", \"role\": \"권투 선수\", \"oneLine\": \"1988년 서울 올림픽 복싱 플라이급 금메달리스트입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EA%B9%80%EA%B4%91%EC%84%A0_%28%EA%B6%8C%ED%88%AC_%EC%84%A0%EC%88%98%29\"}, \"years\": \"1964–\"}, {\"name\": \"정경미\", \"role\": \"유도 선수\", \"oneLine\": \"군산 출신 유도 선수입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%A0%95%EA%B2%BD%EB%AF%B8_%28%EC%9C%A0%EB%8F%84_%EC%84%A0%EC%88%98%29\"}, \"years\": \"1985–\"}, {\"name\": \"전미라\", \"role\": \"테니스 선수\", \"oneLine\": \"테니스 선수로 뛰었고 지금은 해설위원입니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%EC%A0%84%EB%AF%B8%EB%9D%BC\"}, \"years\": \"1978–\"}, {\"name\": \"한왕용\", \"role\": \"산악인\", \"oneLine\": \"히말라야 8000m 14좌를 세계 11번째로 완등했습니다.\", \"verified\": \"확인\", \"source\": {\"name\": \"한국어 위키백과\", \"url\": \"https://ko.wikipedia.org/wiki/%ED%95%9C%EC%99%95%EC%9A%A9\"}, \"years\": \"1966–\"}]}"}, {"id": "chronicle", "name": "시간의 골목", "enabled": false, "locked": false, "data": "{\"kind\":\"chronicle\",\"version\":1,\"items\":[{\"year\":1380,\"title\":\"진포대첩\",\"summary\":\"최무선의 화포가 왜구 함대를 무너뜨린 해. 군산 해양사의 출발점으로 꼽힙니다.\",\"place\":\"진포시비공원\",\"turning\":true,\"verified\":\"확인필요\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/9b073d4a1b5fb62c.jpg\"},{\"year\":1899,\"title\":\"군산 개항\",\"summary\":\"항구가 열리며 도시의 성격이 한 번에 바뀝니다.\",\"place\":\"군산 내항\",\"turning\":true,\"verified\":\"확인\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\"},{\"year\":1908,\"title\":\"구 군산세관 본관 준공\",\"summary\":\"지금도 서 있는 벽돌 건물입니다. 개항기 행정의 자리였습니다.\",\"place\":\"구 군산세관 본관\",\"verified\":\"확인\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/22af0bff45e5fce8.jpg\"},{\"year\":1926,\"title\":\"내항 부잔교 축조\",\"summary\":\"조수 차가 큰 항구에서 배를 대기 위한 뜬다리입니다. 미곡 반출의 통로였습니다.\",\"place\":\"군산 내항 부잔교\",\"turning\":true,\"verified\":\"확인필요\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\"},{\"year\":1937,\"title\":\"『탁류』 연재 시작\",\"summary\":\"채만식이 이 도시를 소설의 무대로 세웁니다.\",\"place\":\"채만식문학관\",\"verified\":\"확인\",\"source\":{\"name\":\"군산시 채만식문학관\",\"url\":\"https://www.gunsan.go.kr/chae/\"},\"imageUrl\":\"/assets/mirror/8043bad52d95a84b.jpg\"},{\"year\":1990,\"title\":\"금강하굿둑 준공\",\"summary\":\"강과 바다의 경계가 사람 손으로 그어집니다.\",\"place\":\"금강하굿둑\",\"verified\":\"확인필요\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/aaf32d2c8e5faffb.jpg\"},{\"year\":2010,\"title\":\"새만금 방조제 개통\",\"summary\":\"간척으로 해안선이 다시 그려집니다.\",\"place\":\"새만금 방조제\",\"turning\":true,\"verified\":\"확인필요\",\"source\":{\"name\":\"대한민국 구석구석\",\"url\":\"https://korean.visitkorea.or.kr/\"},\"imageUrl\":\"/assets/mirror/487643cdcfd19fdb.jpg\"}],\"title\":\"시간의 골목\",\"subtitle\":\"이 도시를 만든 해들\"}"}, {"id": "postcard", "name": "오늘의 엽서", "enabled": false, "locked": false, "data": "{\"kind\":\"postcard\",\"version\":1,\"items\":[{\"line\":\"부잔교 위에서는 물이 어디까지 올라왔었는지가 다리에 새겨져 있다.\",\"hashtags\":[\"#군산\",\"#내항\",\"#부잔교\"],\"place\":\"군산 내항\",\"postmark\":\"군산 內港\",\"verified\":\"확인\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\"},{\"line\":\"짬뽕 한 그릇이 항구도시의 이력서인 줄은 몰랐다.\",\"hashtags\":[\"#군산짬뽕\",\"#항구도시\"],\"place\":\"군산 중앙로\",\"postmark\":\"군산 中央\",\"verified\":\"확인필요\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/149c18951bdcaff8.jpg\"},{\"line\":\"단팥빵을 사러 줄을 서는 일이 여행이 되는 도시.\",\"hashtags\":[\"#이성당\",\"#오래된가게\"],\"place\":\"이성당\",\"postmark\":\"군산 中央\",\"verified\":\"확인\",\"source\":{\"name\":\"대한민국 구석구석\",\"url\":\"https://korean.visitkorea.or.kr/\"},\"imageUrl\":\"/assets/mirror/37c90494d28478e2.png\"},{\"line\":\"섬과 섬 사이를 다리로 건너면, 바다가 길이 된다.\",\"hashtags\":[\"#고군산군도\",\"#선유도\"],\"place\":\"고군산군도\",\"postmark\":\"고군산\",\"verified\":\"확인필요\",\"source\":{\"name\":\"대한민국 구석구석\",\"url\":\"https://korean.visitkorea.or.kr/\"},\"imageUrl\":\"/assets/mirror/6ded14f21a17894b.jpg\"}],\"title\":\"오늘의 엽서\",\"subtitle\":\"그대로 붙여 쓰는 한 문장\"}"}, {"id": "quiz", "name": "뒤집어 보는 질문", "enabled": false, "locked": false, "data": "{\"kind\": \"quiz\", \"version\": 1, \"items\": [{\"question\": \"부잔교는 왜 물 위에 뜨도록 만들었을까?\", \"hint\": \"서해는 조수 간만의 차가 큽니다. 고정된 부두라면 하루에 두 번 배가 닿지 못합니다.\", \"topic\": \"군산 내항\", \"level\": \"초등\", \"verified\": \"확인\", \"source\": {\"name\": \"군산시 문화관광\", \"url\": \"https://www.gunsan.go.kr/tour/\"}}, {\"question\": \"『탁류』의 “탁류”는 무엇을 가리키는 말일까?\", \"hint\": \"금강 하류의 흙탕물이자, 소설 속 인물들이 휩쓸려 가는 시대를 함께 가리킵니다.\", \"topic\": \"채만식문학관\", \"level\": \"중등\", \"verified\": \"확인\", \"source\": {\"name\": \"군산시 채만식문학관\", \"url\": \"https://www.gunsan.go.kr/chae/\"}}, {\"question\": \"해망굴은 무엇을 위해 뚫린 굴일까?\", \"hint\": \"근대 교통로였고, 전쟁기에는 다른 쓰임도 있었습니다. 굴 하나에 두 시대가 겹칩니다.\", \"topic\": \"해망굴\", \"level\": \"어른\", \"verified\": \"확인필요\", \"source\": {\"name\": \"군산시 문화관광\", \"url\": \"https://www.gunsan.go.kr/tour/\"}}, {\"question\": \"고군산군도의 ‘고(古)’ 는 왜 붙었을까?\", \"hint\": \"지금의 군산이 자리 잡기 전, 이 이름이 먼저 있던 곳이 있습니다.\", \"topic\": \"고군산군도\", \"level\": \"어른\", \"verified\": \"확인필요\", \"source\": {\"name\": \"대한민국 구석구석\", \"url\": \"https://korean.visitkorea.or.kr/\"}}], \"title\": \"뒤집어 보는 질문\", \"subtitle\": \"뒤집으면 힌트가 나옵니다\"}"}, {"id": "faq", "name": "자주 묻는 질문", "enabled": true, "locked": false}, {"id": "rules", "name": "이용 규정", "enabled": true, "locked": false}, {"id": "weather", "name": "날씨", "enabled": true, "locked": false}, {"id": "planner", "name": "계절별 추천 하루", "enabled": false, "locked": false, "data": "{\"kind\":\"planner\",\"version\":1,\"title\":\"계절별 추천 하루\",\"subtitle\":\"지금 계절에 맞는 코스만 보여 드립니다\",\"items\":[{\"name\":\"탁류길 코스\",\"audience\":\"걷기 좋아하는 손님\",\"why\":\"소설 『탁류』가 지나간 자리를 그대로 따라 걷습니다.\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"채만식문학관\",\"minutes\":12,\"note\":\"금강 하구 옆. 소설의 배경을 먼저 눈에 담습니다.\",\"searchQuery\":\"군산 채만식문학관\",\"imageUrl\":\"/assets/mirror/8043bad52d95a84b.jpg\",\"latitude\":36.0085981064,\"longitude\":126.7578853293},{\"name\":\"군산 내항 부잔교\",\"minutes\":9,\"note\":\"조수에 오르내리던 뜬다리. 이 코스의 중심입니다.\",\"searchQuery\":\"군산 내항 부잔교\",\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\",\"latitude\":35.9892997236,\"longitude\":126.7174508158},{\"name\":\"째보선창\",\"minutes\":7,\"note\":\"항구 노동의 기억이 남은 선창입니다.\",\"searchQuery\":\"군산 째보선창\",\"latitude\":35.9875345,\"longitude\":126.7199875},{\"name\":\"초원사진관\",\"minutes\":11,\"note\":\"영화가 남기고 간 자리입니다.\",\"searchQuery\":\"군산 초원사진관\",\"imageUrl\":\"/assets/mirror/d5433fe48de3da05.jpg\",\"latitude\":35.9877369536,\"longitude\":126.7083826065},{\"name\":\"이성당\",\"minutes\":6,\"note\":\"코스의 끝. 단팥빵 하나로 반나절을 닫습니다.\",\"searchQuery\":\"군산 이성당\",\"imageUrl\":\"/assets/mirror/37c90494d28478e2.png\",\"latitude\":35.9870443303,\"longitude\":126.7111681428}],\"verified\":\"확인\",\"source\":{\"name\":\"군산 스탬프투어\",\"url\":\"https://www.gunsanstamp.kr/\"},\"season\":\"가을\",\"rank\":2},{\"name\":\"아리랑길 코스\",\"audience\":\"걷기 좋아하는 손님\",\"why\":\"개항기 건물만 골라 짧게 도는 길입니다.\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"구 군산세관 본관\",\"minutes\":8,\"note\":\"1908년에 지어진 벽돌 건물입니다.\",\"searchQuery\":\"구 군산세관 본관\",\"imageUrl\":\"/assets/mirror/22af0bff45e5fce8.jpg\",\"latitude\":35.9915087952,\"longitude\":126.7113016917},{\"name\":\"군산근대미술관\",\"minutes\":5,\"note\":\"옛 은행 금고가 그대로 남아 있습니다.\",\"searchQuery\":\"군산근대미술관\",\"imageUrl\":\"/assets/mirror/d8764592a03b0103.jpg\",\"latitude\":35.9899367776,\"longitude\":126.7127710457},{\"name\":\"해망굴\",\"minutes\":14,\"note\":\"굴 하나에 두 시대가 겹칩니다.\",\"searchQuery\":\"군산 해망굴\",\"imageUrl\":\"/assets/mirror/0a1e0ff50ca7ee37.jpg\",\"latitude\":35.9913180812,\"longitude\":126.7048496204}],\"verified\":\"확인필요\",\"source\":{\"name\":\"군산 스탬프투어\",\"url\":\"https://www.gunsanstamp.kr/\"},\"season\":\"봄\",\"rank\":1},{\"name\":\"개항장 골목 한 바퀴\",\"audience\":\"처음 온 손님\",\"why\":\"해가 낮아지는 계절이라 골목 그림자가 길어집니다. 걷는 거리가 가장 짧은 코스이기도 합니다.\",\"startTime\":\"09:30\",\"stops\":[{\"name\":\"군산근대역사박물관\",\"minutes\":90,\"moveMinutes\":12,\"note\":\"항구 도시가 어떻게 만들어졌는지 먼저 봅니다.\",\"searchQuery\":\"군산근대역사박물관\",\"imageUrl\":\"/assets/mirror/af673cc75c0ed696.jpg\",\"latitude\":35.9908197098,\"longitude\":126.7121231556},{\"name\":\"초원사진관\",\"minutes\":30,\"moveMinutes\":10,\"note\":\"영화가 남기고 간 자리입니다.\",\"searchQuery\":\"군산 초원사진관\",\"imageUrl\":\"/assets/mirror/d5433fe48de3da05.jpg\",\"latitude\":35.9877369536,\"longitude\":126.7083826065},{\"name\":\"이성당\",\"minutes\":40,\"moveMinutes\":8,\"note\":\"단팥빵 하나로 오후를 엽니다.\",\"searchQuery\":\"군산 이성당\",\"imageUrl\":\"/assets/mirror/37c90494d28478e2.png\",\"latitude\":35.9870443303,\"longitude\":126.7111681428},{\"name\":\"경암동 철길마을\",\"minutes\":60,\"moveMinutes\":18,\"note\":\"해 질 무렵 빛이 가장 좋습니다.\",\"searchQuery\":\"군산 경암동 철길마을\",\"imageUrl\":\"/assets/mirror/6d59722cedddd566.jpg\",\"latitude\":35.9813520474,\"longitude\":126.7362330582}],\"verified\":\"확인\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"season\":\"가을\",\"rank\":1},{\"name\":\"비 오면 여기로\",\"audience\":\"장마철에 온 손님\",\"why\":\"실내로만 이어지는 코스입니다. 우산을 접었다 폈다 하지 않아도 됩니다.\",\"season\":\"여름\",\"startTime\":\"10:30\",\"stops\":[{\"name\":\"군산근대건축관\",\"minutes\":60,\"moveMinutes\":10,\"note\":\"옛 은행 건물 안이 통째로 전시입니다.\",\"searchQuery\":\"군산근대건축관\",\"imageUrl\":\"/assets/mirror/5000f0a4feb45788.jpg\",\"latitude\":35.9894201935,\"longitude\":126.7141598237},{\"name\":\"한일옥\",\"minutes\":60,\"moveMinutes\":8,\"note\":\"무국 한 그릇으로 몸을 데웁니다.\",\"searchQuery\":\"군산 한일옥\",\"latitude\":35.9874081251,\"longitude\":126.7082350808},{\"name\":\"째보선창 근처 카페\",\"minutes\":90,\"moveMinutes\":10,\"note\":\"비 그칠 때까지 앉아 있기 좋습니다.\",\"searchQuery\":\"군산 째보선창 카페\",\"latitude\":35.9875345,\"longitude\":126.7199875}],\"verified\":\"확인필요\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"rank\":1}]}"}], "look": {"radius": "0px", "shadow": "4px 4px 0 rgb(27 26 21 / 0.16)", "texture": "repeating-linear-gradient(0deg,rgba(27,26,21,.028) 0 1px,transparent 1px 3px),repeating-linear-gradient(90deg,rgba(27,26,21,.02) 0 1px,transparent 1px 4px)", "fontBody": "'Gowun Batang', 'Noto Serif KR', serif", "borderWidth": "2px", "fontHeading": "'Gugi', 'Noto Sans KR', sans-serif", "sectionSpace": "4rem", "headingWeight": "400", "headingTracking": "0em"}}} \ No newline at end of file diff --git a/solution/site/scripts/mockup/stay-payload.json b/solution/site/scripts/mockup/stay-payload.json new file mode 100644 index 0000000..860deed --- /dev/null +++ b/solution/site/scripts/mockup/stay-payload.json @@ -0,0 +1 @@ +{"schemaVersion":1,"site":{"siteId":"e929e6b4-e502-4761-9786-0bb82178b375","placeId":"66894a1b-a331-43f1-942c-0ae064a704d6","status":3,"origin":"https://web4ai.o2osolution.ai","basePath":"/s/stay","slug":"stay","publishedAt":"2026-09-03T01:27:39.284442+00:00","updatedAt":"2026-09-09T13:40:00+00:00","version":11},"place":{"name":"스테이,머뭄","category":1,"roadAddress":"전북 군산시 절골길 18","address":"전북 군산시 신흥동 63-18","addressRegion":"전북","addressLocality":"군산시","addressSubLocality":null,"addressRegionCode":"KR-45","phone":"0507-1497-0983","latitude":35.9864175,"longitude":126.7061254,"regionCode":"52군산시","kakaoPlaceId":null},"facts":[{"key":"parking","label":"주차 가능","value":"true","unit":null,"type":"bool","scope":"place","status":3,"sourceType":3,"sourceUrl":"https://m.place.naver.com/place/1133638931/home","critical":false,"required":false,"unitId":null,"collectedAt":"2026-08-28T02:10:08.678003+00:00","verifiedAt":"2026-08-28T02:12:21.402109+00:00"},{"key":"intro","label":"숙소 소개","value":"1920년대에 지어진 백 년 고택을 리모델링한 독채 펜션입니다. 히로쓰 가옥 후문 바로 옆이고, 침구는 매일 세탁·살균·건조합니다.","unit":null,"type":"text","scope":"place","status":3,"sourceType":3,"sourceUrl":"https://m.place.naver.com/place/1133638931/home","critical":false,"required":false,"unitId":null,"collectedAt":"2026-08-28T02:10:08.547669+00:00","verifiedAt":"2026-08-28T02:12:21.922925+00:00"},{"key":"wifi","label":"와이파이","value":"true","unit":null,"type":"bool","scope":"place","status":3,"sourceType":3,"sourceUrl":"https://m.place.naver.com/place/1133638931/home","critical":false,"required":false,"unitId":null,"collectedAt":"2026-08-28T02:10:08.757070+00:00","verifiedAt":"2026-08-28T02:12:22.356600+00:00"},{"key":"check_out_time","label":"체크아웃 시간","value":"11:00","unit":null,"type":"time","scope":"place","status":3,"sourceType":3,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":true,"required":true,"unitId":null,"collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T13:27:53.241391+00:00"},{"key":"cancel_policy","label":"취소·환불 규정","value":"입실 당일 취소와 노쇼(No Show)는 숙박요금의 100%가 부과되어 환불되지 않습니다. 그 이전 취소 수수료는 예약하신 채널의 취소 규정을 따릅니다.","unit":null,"type":"text","scope":"place","status":3,"sourceType":3,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":true,"required":true,"unitId":null,"collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T13:27:53.241391+00:00"},{"key":"cooking_allowed","label":"취사 가능","value":"true","unit":null,"type":"bool","scope":"place","status":3,"sourceType":3,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":true,"required":true,"unitId":null,"collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T13:27:53.241391+00:00"},{"key":"pet_allowed","label":"반려동물 동반","value":"false","unit":null,"type":"bool","scope":"place","status":3,"sourceType":3,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":true,"required":true,"unitId":null,"collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T13:27:53.241391+00:00"},{"key":"smoking","label":"흡연 가능","value":"false","unit":null,"type":"bool","scope":"place","status":3,"sourceType":3,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":true,"required":false,"unitId":null,"collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T13:27:53.241391+00:00"},{"key":"extra_person_fee","label":"인원 추가 요금","value":"20000","unit":"원","type":"number","scope":"place","status":3,"sourceType":3,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":true,"required":false,"unitId":null,"collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T13:27:53.241391+00:00"},{"key":"check_in_time","label":"체크인 시간","value":"15:00","unit":null,"type":"time","scope":"place","status":3,"sourceType":1,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":true,"required":true,"unitId":null,"collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T23:31:05.719599+00:00"},{"key":"pickup_service","label":"픽업 서비스","value":"false","type":"bool","scope":"place","unit":null,"status":3,"sourceType":3,"sourceUrl":"https://m.place.naver.com/place/1133638931/home","critical":false,"required":false,"unitId":null,"collectedAt":"2026-09-04T00:00:00+00:00","verifiedAt":"2026-09-04T00:00:00+00:00"},{"key":"bbq_available","label":"바비큐 이용","value":"false","type":"bool","scope":"place","unit":null,"status":3,"sourceType":3,"sourceUrl":"https://m.place.naver.com/place/1133638931/home","critical":false,"required":false,"unitId":null,"collectedAt":"2026-09-04T00:00:00+00:00","verifiedAt":"2026-09-04T00:00:00+00:00"}],"units":[{"unitId":"71dd18bd-8bd0-4e46-a4b2-419b972d6175","name":"A동(일본식 가정집 느낌으로 디딤돌마당과 예쁜정원)","slug":"a동","sortOrder":0,"facts":[{"key":"room_type","label":"객실 타입","value":"독채","unit":null,"type":"text","scope":"unit","status":3,"sourceType":3,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":false,"required":true,"unitId":"71dd18bd-8bd0-4e46-a4b2-419b972d6175","collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T13:27:53.241391+00:00"},{"key":"standard_capacity","label":"기준 인원","value":"2","unit":"명","type":"number","scope":"unit","status":3,"sourceType":3,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":true,"required":true,"unitId":"71dd18bd-8bd0-4e46-a4b2-419b972d6175","collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T13:27:53.241391+00:00"},{"key":"max_capacity","label":"최대 인원","value":"4","unit":"명","type":"number","scope":"unit","status":3,"sourceType":3,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":true,"required":true,"unitId":"71dd18bd-8bd0-4e46-a4b2-419b972d6175","collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T13:27:53.241391+00:00"},{"key":"weekday_price","label":"주중 요금","value":"198000","unit":"원","type":"number","scope":"unit","status":3,"sourceType":3,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":true,"required":false,"unitId":"71dd18bd-8bd0-4e46-a4b2-419b972d6175","collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T13:27:53.241391+00:00"},{"key":"room_intro","label":"객실 소개","value":"편안한 일본집에 와 있는 듯한 느낌의 공간입니다. 독립된 아담한 정원과 창고형 CAFÉ 공간이 있습니다.","unit":null,"type":"text","scope":"unit","status":3,"sourceType":3,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":false,"required":false,"unitId":"71dd18bd-8bd0-4e46-a4b2-419b972d6175","collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T13:27:53.241391+00:00"},{"key":"price_range","label":"요금","value":"198,000 ~ 350,000원","type":"text","scope":"unit","unit":null,"status":3,"sourceType":3,"sourceUrl":"https://m.place.naver.com/place/1133638931/home","critical":true,"required":false,"unitId":"71dd18bd-8bd0-4e46-a4b2-419b972d6175","collectedAt":"2026-09-04T00:00:00+00:00","verifiedAt":"2026-09-04T00:00:00+00:00"}],"mediaIds":["1330beb8-09d3-47fc-a410-f2e9598d13fe","ad35769c-5299-4d39-b80d-5d88f1e1de14","2fbcd37e-260f-4a4f-b00f-3e997e978c02","d95857a0-44ad-422b-86de-00dee8aca08f"]},{"unitId":"916dc29b-241c-41a8-90de-51c3a9ddb947","name":"B동(모던한 현대식 컨셉으로 꾸며진 따뜻한 공간)","slug":"b동","sortOrder":1,"facts":[{"key":"room_type","label":"객실 타입","value":"독채","unit":null,"type":"text","scope":"unit","status":3,"sourceType":3,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":false,"required":true,"unitId":"916dc29b-241c-41a8-90de-51c3a9ddb947","collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T13:27:53.241391+00:00"},{"key":"standard_capacity","label":"기준 인원","value":"2","unit":"명","type":"number","scope":"unit","status":3,"sourceType":3,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":true,"required":true,"unitId":"916dc29b-241c-41a8-90de-51c3a9ddb947","collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T13:27:53.241391+00:00"},{"key":"max_capacity","label":"최대 인원","value":"4","unit":"명","type":"number","scope":"unit","status":3,"sourceType":3,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":true,"required":true,"unitId":"916dc29b-241c-41a8-90de-51c3a9ddb947","collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T13:27:53.241391+00:00"},{"key":"weekday_price","label":"주중 요금","value":"198000","unit":"원","type":"number","scope":"unit","status":3,"sourceType":3,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":true,"required":false,"unitId":"916dc29b-241c-41a8-90de-51c3a9ddb947","collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T13:27:53.241391+00:00"},{"key":"room_intro","label":"객실 소개","value":"아늑하고 깔끔한 현대식 모던 스타일 공간입니다. 독립된 아담한 정원과 창고형 CAFÉ 공간이 있습니다.","unit":null,"type":"text","scope":"unit","status":3,"sourceType":3,"sourceUrl":"https://travel.interpark.com/checkinnow/goods/GY0002361440","critical":false,"required":false,"unitId":"916dc29b-241c-41a8-90de-51c3a9ddb947","collectedAt":"2026-09-02T13:27:53.241391+00:00","verifiedAt":"2026-09-02T13:27:53.241391+00:00"},{"key":"price_range","label":"요금","value":"198,000 ~ 350,000원","type":"text","scope":"unit","unit":null,"status":3,"sourceType":3,"sourceUrl":"https://m.place.naver.com/place/1133638931/home","critical":true,"required":false,"unitId":"916dc29b-241c-41a8-90de-51c3a9ddb947","collectedAt":"2026-09-04T00:00:00+00:00","verifiedAt":"2026-09-04T00:00:00+00:00"}],"mediaIds":["5b0adc6e-f03b-459d-9b0b-d0748f1e33d0","0e5a2de5-384d-41fc-9ff4-258f9caa6b2c","4981b572-10e2-4138-ba1d-a5d7defb4b4a","83a2543f-4366-4427-b694-a7b1968a61cf"]}],"media":[{"mediaId":"1330beb8-09d3-47fc-a410-f2e9598d13fe","url":"/assets/mirror/ddc9f804f4ef83b5.jpg","alt":"목재 서까래 천장과 미닫이창, 테이블이 있는 거실 공간","category":"거실","width":null,"height":null,"isPrimary":true,"sourceType":3,"originUrl":"https://naverbooking-phinf.pstatic.net/20240514_189/1715688030436xT14o_JPEG/1.jpg","unitId":null},{"mediaId":"ad35769c-5299-4d39-b80d-5d88f1e1de14","url":"/assets/mirror/6d0c447b6a0c32af.jpg","alt":"소파와 탁자, 벽난로 조명이 있는 서까래 천장의 거실","category":"거실","width":null,"height":null,"isPrimary":false,"sourceType":3,"originUrl":"https://naverbooking-phinf.pstatic.net/20240514_48/1715688030574wTtQd_JPEG/2.jpg","unitId":null},{"mediaId":"83a2543f-4366-4427-b694-a7b1968a61cf","url":"/assets/mirror/2d94158a48480fcd.jpg","alt":"싱크대와 냉장고, 전자레인지가 구비된 실내 주방 공간","category":"주방","width":null,"height":null,"isPrimary":false,"sourceType":3,"originUrl":"https://naverbooking-phinf.pstatic.net/20240514_92/17156880307484bvpH_JPEG/3.jpg","unitId":null},{"mediaId":"990a89ed-9517-426c-8c17-fd9bf3c1a44d","url":"/assets/mirror/d364f605223524bd.jpg","alt":"주방 싱크대 앞에서 포즈를 취하고 있는 여성의 모습","category":"주방","width":null,"height":null,"isPrimary":false,"sourceType":3,"originUrl":"https://naverbooking-phinf.pstatic.net/20240514_7/1715688031000y8Y5q_JPEG/4.jpg","unitId":null},{"mediaId":"5b0adc6e-f03b-459d-9b0b-d0748f1e33d0","url":"/assets/mirror/e0ed8d1a27e09320.jpg","alt":"벽면에 설치된 그림 화면과 벽난로 조명 장식","category":"거실","width":null,"height":null,"isPrimary":false,"sourceType":3,"originUrl":"https://video-phinf.pstatic.net/20241223_179/1734893614235FhY4h_JPEG/PXr9o3iPYJ_03.jpg","unitId":null},{"mediaId":"2fbcd37e-260f-4a4f-b00f-3e997e978c02","url":"/assets/mirror/aa69a633f8e3a631.jpg","alt":"창밖으로 조명을 비춘 정원 나무와 석등이 보이는 풍경","category":"전망","width":null,"height":null,"isPrimary":false,"sourceType":3,"originUrl":"https://pup-review-phinf.pstatic.net/MjAyNjA4MDNfMjUw/MDAxNzg1NzYwMjA0NTIy.XcTmqR10iQ8w9sFRfFRWo7CfQtNOoT6aRMo0VErTJLkg.EX6iDVemzmjovYRBJwJKdVWWipxu_JOixkmW1_APE_cg.JPEG/8034BC2C-FC29-4480-8BEC-40F0BED744CB.jpeg","unitId":null},{"mediaId":"d95857a0-44ad-422b-86de-00dee8aca08f","url":"/assets/mirror/7095220ca156e25e.jpg","alt":"목재 서까래 천장에 설치된 조형적인 디자인의 조명 기구","category":"거실","width":null,"height":null,"isPrimary":false,"sourceType":3,"originUrl":"https://pup-review-phinf.pstatic.net/MjAyNjA4MDNfMjA3/MDAxNzg1NzYwMjA0OTI5.uMbalnmqu8lUYeUCS26c2luQ_pvQubkznSFblDcSV6Eg.gaRskjAm5Rud_L9lYS3xIQvd2m8-0R75DF6WJg2asrog.JPEG/628C312C-B7CA-4178-9B10-855CB16A9889.jpeg","unitId":null},{"mediaId":"0e5a2de5-384d-41fc-9ff4-258f9caa6b2c","url":"/assets/mirror/caa5e076e7d56ec2.jpg","alt":"실내 벽면 홈 공간에 놓인 흰색 화병과 식물 장식","category":"거실","width":null,"height":null,"isPrimary":false,"sourceType":3,"originUrl":"https://pup-review-phinf.pstatic.net/MjAyNjA4MDNfMTg2/MDAxNzg1NzYwMjA0NjMw.5Ct417Oiyb5wlINojIkcVMLUy0QNbPoh8FhXvLT0Gcwg.QHSM9xd3ri3P9HaSeUWES1x8RMFcc-GpmpJTgoMaQkgg.JPEG/EEE3CB7C-55A9-4AC1-9574-D703537FF1C8.jpeg","unitId":null},{"mediaId":"4981b572-10e2-4138-ba1d-a5d7defb4b4a","url":"/assets/mirror/8e2ec99c433b6fe8.jpg","alt":"실내 창문을 통해 바라본 푸른 하늘과 푸른 나무 풍경","category":"전망","width":null,"height":null,"isPrimary":false,"sourceType":3,"originUrl":"https://pup-review-phinf.pstatic.net/MjAyNjA4MDNfOTMg/MDAxNzg1NzYwMjA1MTU1.Ulli7HXvsNrTkk_i3QnJknX7WBheTrnJRF6ARjyfQ14g.Vp9808LNn4hDRPFja1prpM7wuvVPBhqjpZZlec6Q-fMg.JPEG/F51B7821-B061-4173-BD07-E914E00B92AD.jpeg","unitId":null}],"faqs":[{"faqId":"8d48a94d-1a19-456f-b1a8-4144ddcb2186","question":"체크인과 체크아웃 시간은 언제인가요?","answer":"체크인은 15:00부터이며, 체크아웃은 11:00까지입니다.","status":3,"sourceType":4,"sortOrder":0},{"faqId":"8f14678b-3589-42e9-8896-d214c0093784","question":"객실 기준 인원과 최대 인원, 인원 추가 요금은 어떻게 되나요?","answer":"A동과 B동 모두 기준 인원은 2명이며, 최대 4명까지 투숙하실 수 있습니다. 인원 추가 요금은 20,000원입니다.","status":3,"sourceType":4,"sortOrder":1},{"faqId":"a13785c0-a627-44b5-b593-fac39712d6d4","question":"객실 내 취사가 가능한가요?","answer":"주방에서 간단한 조리는 가능합니다. 다만 생선구이·고기구이·튀김처럼 냄새가 잘 빠지지 않는 조리는 하실 수 없고, 객실 안에서 직화로 굽는 방식도 허용되지 않습니다. 그릴·숯·전기 전열기구 등 개인 취사도구는 반입이 금지되어 있습니다.","status":3,"sourceType":4,"sortOrder":2},{"faqId":"0eb535db-135f-484b-b0a4-1323744ace75","question":"주차와 와이파이 이용이 가능한가요?","answer":"네, 주차와 와이파이 모두 이용 가능합니다.","status":3,"sourceType":4,"sortOrder":3},{"faqId":"cc268677-ad26-4685-a6a6-10a293658591","question":"반려동물 동반이나 실내 흡연이 가능한가요?","answer":"반려동물 동반은 불가하며, 전 구역 흡연이 불가능합니다.","status":3,"sourceType":4,"sortOrder":4},{"faqId":"4b8ac158-7648-4b2c-aa1a-9c6ca99b1223","question":"A동과 B동은 무엇이 다른가요?","answer":"A동은 편안한 일본집에 와 있는 듯한 느낌의 공간이고, B동은 아늑하고 깔끔한 현대식 모던 스타일 공간입니다. 두 동 모두 독립된 아담한 정원과 창고형 CAFÉ 공간이 딸려 있으며, 기준 2인 · 최대 4인으로 같습니다. 침대 구성 등 객실 안 자세한 사항은 예약 채널이나 전화로 확인해 주세요.","status":3,"sourceType":4,"sortOrder":5},{"faqId":"8bd75b48-49c3-475f-99c1-f5e64f49bedd","question":"객실 요금은 얼마인가요?","answer":"A동과 B동 모두 198,000원 ~ 350,000원입니다. 날짜와 시즌에 따라 달라지므로 정확한 금액은 예약 채널에서 확인해 주세요. 기준 인원(2인)을 넘으면 1인당 20,000원이 더해집니다.","status":3,"sourceType":4,"sortOrder":6},{"faqId":"7b2bbfeb-761c-4a15-b410-43e18a69bb9e","question":"예약 취소 및 환불 규정은 어떻게 되나요?","answer":"입실 당일 취소와 노쇼(No Show)의 경우 숙박요금의 100%가 부과되어 환불되지 않습니다. 그 이전 취소 수수료는 예약하신 채널의 취소 규정을 따릅니다.","status":3,"sourceType":4,"sortOrder":7},{"faqId":"20e80f4d-322a-46ac-90b1-21fb6669b6b0","question":"스테이,머뭄은 어떤 숙소인가요?","answer":"스테이,머뭄은 전북 군산시 절골길 18(신흥동)에 있는 독채 숙소입니다. A동은 일본식 가정집 느낌, B동은 현대식 모던 스타일로 두 동이 있고, 편안한 침구와 좋은 위치를 내세운 곳입니다.","status":3,"sourceType":4,"sortOrder":8},{"faqId":"724aeac2-46b1-4d6d-9748-892b5988a178","question":"군산 독채 펜션을 찾는데, 여기도 독채인가요?","answer":"네, 스테이,머뭄은 A동과 B동 모두 독채입니다. 동마다 독립된 정원과 창고형 카페 공간이 딸려 있고, 주방이 있어 취사도 가능합니다.","status":3,"sourceType":4,"sortOrder":9},{"faqId":"a50f8b18-1a2b-4025-ab0c-00d8ebcc9e8f","question":"위치가 어디인가요?","answer":"스테이,머뭄은 전북 군산시 절골길 18(신흥동 63-18)에 있습니다. 바로 옆 13m 거리에 군산 신흥동 일본식가옥(히로쓰 가옥)이 있는 원도심 골목 안입니다. 문의는 0507-1497-0983입니다.","status":3,"sourceType":4,"sortOrder":10},{"faqId":"7bac199e-f095-4b44-acf6-c94d68181fc8","question":"예약은 어디서 하나요?","answer":"스테이,머뭄은 네이버 플레이스에서 숙소 정보를 확인하실 수 있고, 전화 0507-1497-0983으로도 문의하실 수 있습니다. 입실 당일 취소와 노쇼는 숙박요금의 100%가 부과되니 일정을 확인하고 예약해 주세요.","status":3,"sourceType":4,"sortOrder":11},{"faqId":"2059ea18-e444-4b2f-97df-ac8ada6e72b0","question":"커플 둘이 묵기 좋은 2인 독채인가요?","answer":"네, 스테이,머뭄 A동과 B동 모두 기준 인원 2인의 독채입니다. 두 동을 각각 한 팀만 쓰고, 독립된 아담한 정원과 창고형 CAFÉ 공간이 딸려 있습니다.","status":3,"sourceType":4,"sortOrder":12},{"faqId":"b3950b7b-935d-47a5-8119-67afb028a5ce","question":"일본식 가옥 같은 숙소를 찾는데 여기가 맞나요?","answer":"스테이,머뭄 A동이 일본식 가정집 느낌으로 꾸민 독채입니다. 디딤돌 마당과 독립된 정원, 창고형 카페 공간이 딸려 있습니다. B동은 같은 마당 안에 있는 현대식 모던 스타일 독채입니다.","status":3,"sourceType":4,"sortOrder":13},{"faqId":"78fcc01a-099f-4e7d-a731-d93d18573f55","question":"조용하고 프라이빗한 숙소인가요?","answer":"스테이,머뭄은 A동·B동 두 동뿐인 독채 숙소라 다른 손님과 건물을 나눠 쓰지 않습니다. 두 동 모두 독립된 정원을 따로 두고 있고, 전 구역 금연입니다.","status":3,"sourceType":4,"sortOrder":14},{"faqId":"feb316f6-f0b5-419c-83e1-45fdec718024","question":"차 없이 걸어서 근대문화거리까지 갈 수 있나요?","answer":"스테이,머뭄에서 히로쓰 가옥은 13m, 초원사진관 248m, 군산 영화의 거리 318m, 군산근대미술관 708m, 옛 군산세관 728m, 근대건축관(구 조선은행 군산지점) 788m 거리입니다. 원도심 근대 건축물이 숙소를 중심으로 1km 안에 모여 있습니다. 주차도 가능합니다.","status":3,"sourceType":4,"sortOrder":15},{"faqId":"681b921e-d15f-48af-8378-21e7754f500f","question":"이성당이나 근처 맛집까지 얼마나 되나요?","answer":"스테이,머뭄에서 이성당까지는 455m입니다. 그 밖에 요리주점 도란 135m, 베이커리 물밀소 297m, 백년가게 국밥집 일흥옥 300m, 명월갈비 351m, 꽃게장 한주옥 430m로 모두 가까이 있습니다.","status":3,"sourceType":4,"sortOrder":16},{"faqId":"92c2e8b2-4a62-4b11-8898-7d75fd6bc6f7","question":"마당이나 정원이 따로 있나요?","answer":"네. A동과 B동 각각 독립된 아담한 정원과 창고형 CAFÉ 공간이 있습니다. A동에는 디딤돌 마당도 있습니다. 바다 전망은 아닙니다 — 마당과 거실에서 새소리를 들으며 쉬는 숙소입니다.","status":3,"sourceType":4,"sortOrder":17},{"faqId":"2c5cfea9-e3c7-42e0-8353-87e5538f3f4e","question":"선유도에 갈 건데 여기서 자도 될까요?","answer":"네, 스테이,머뭄에서는 다리를 건너 고군산군도 선유도까지 차로 약 45분 걸립니다. 첫날은 군산 도심에서 저녁과 내항 밤 산책을, 이튿날 선유도와 장자도를 도는 1박 2일 코스를 추천 일정으로 안내하고 있습니다.","status":3,"sourceType":4,"sortOrder":18},{"faqId":"bab88a71-1cce-40cb-ad7a-9e8d1fdd5ff9","question":"1박 2일이면 어떻게 돌면 좋을까요?","answer":"스테이,머뭄이 안내하는 1박 2일 코스는 첫날 오후 체크인 후 빈해원에서 저녁, 군산 내항 밤 산책으로 마무리하고, 이튿날 선유도와 장자도를 도는 일정입니다. 걷는 코스를 원하시면 채만식문학관·내항 부잔교·째보선창·초원사진관·이성당으로 이어지는 반나절 탁류길 코스도 있습니다.","status":3,"sourceType":4,"sortOrder":19},{"faqId":"55288610-291a-4feb-95d8-29e3b4b605d7","question":"군산이 처음인데 어디부터 보면 좋을까요?","answer":"스테이,머뭄은 처음 오신 손님께 반나절 개항장 골목 코스를 안내합니다. 군산근대역사박물관에서 시작해 초원사진관, 이성당, 경암동 철길마을 순으로 도는 일정으로, 걷는 거리가 가장 짧은 코스입니다.","status":3,"sourceType":4,"sortOrder":20},{"faqId":"a8fdcd07-70d4-4274-ac6f-1d0067beec2a","question":"비 오는 날에는 뭘 하면 좋을까요?","answer":"스테이,머뭄은 비 오는 날을 위한 실내 코스를 따로 안내합니다. 군산근대건축관에서 옛 은행 건물 전시를 보고, 한일옥에서 무국으로 몸을 데운 뒤, 째보선창 근처 카페에서 비가 그칠 때까지 앉아 있는 코스입니다. 숙소는 주방이 있어 취사도 가능합니다.","status":3,"sourceType":4,"sortOrder":21},{"faqId":"4a20242b-d1ee-4c35-95a9-12bd0d90445c","question":"바비큐나 고기를 구워 먹을 수 있나요?","answer":"바비큐는 하실 수 없습니다. 화재 예방을 위해 객실 안에서 생선이나 고기를 굽는 직화 방식은 허용되지 않으며, 그릴·숯·전기 전열기구 등 개인적으로 준비해 오시는 취사도구도 반입이 금지되어 있습니다.","status":3,"sourceType":4,"sortOrder":22},{"faqId":"4b1b6de0-40ca-44fa-af35-cbd69eeebd35","question":"픽업 서비스가 있나요?","answer":"픽업 서비스는 지원하지 않습니다.","status":3,"sourceType":4,"sortOrder":23},{"faqId":"648cf02d-d4ed-46ee-8aef-b3b6426e857e","question":"건물이 오래된 집이라던데 언제 지어졌나요?","answer":"1920년대에 지어진 적산가옥입니다. 백 년 된 고택을 리모델링해 2024년 5월 독채 펜션으로 문을 열었습니다. 바로 옆이 국가등록문화유산인 군산 신흥동 일본식 가옥(히로쓰 가옥)입니다.","status":3,"sourceType":4,"sortOrder":24},{"faqId":"015a1e83-a916-43fa-a6c1-95d7ece1817d","question":"침구는 깨끗한가요?","answer":"침구류는 매일 세탁하고 살균·건조합니다. 인테리어와 가구, 소품도 하나하나 준비해 두었습니다.","status":3,"sourceType":4,"sortOrder":25},{"faqId":"10fa8fd2-7a04-4c0c-935c-dd354f7d3112","question":"미성년자끼리 이용할 수 있나요?","answer":"숙박업소는 법적으로 청소년 혼숙이 금지되어 있습니다. 미성년자는 보호자를 동반하셔야 이용하실 수 있습니다.","status":3,"sourceType":4,"sortOrder":26},{"faqId":"48ec5a26-7e62-4e66-ae32-5917818bdce3","question":"CCTV가 설치되어 있나요?","answer":"고객님의 안전과 보안을 위해 대문 입구 쪽에 CCTV를 운영하고 있습니다. 객실 안에는 없습니다.","status":3,"sourceType":4,"sortOrder":27},{"faqId":"9c3dadc6-bbc6-4f72-b4e8-c336dbfe11da","question":"벌레가 나오지는 않나요?","answer":"정기적으로 방역하고 있습니다. 다만 자연과 함께하는 공간이라 가끔 벌레나 이물질이 들어올 수 있으며, 이 사유로는 환불이 어려운 점 양해 부탁드립니다.","status":3,"sourceType":4,"sortOrder":28},{"faqId":"1f83a416-f01d-4813-babe-011b039df462","question":"예약 인원보다 사람이 늘면 어떻게 하나요?","answer":"미리 숙소로 연락 주세요. 기준 인원(2인)을 넘으면 1인당 20,000원이 추가됩니다. 최대 인원(4인)을 넘으면 입실이 불가능할 수 있고, 그 사유로는 환불을 받으실 수 없습니다.","status":3,"sourceType":4,"sortOrder":29},{"faqId":"8b0cfa98-9616-44ac-937d-dd5f0a8a4ca9","question":"히로쓰 가옥 바로 옆이라던데 어떤 집인가요?","answer":"스테이,머뭄은 1920년대에 지어진 집으로, 국가등록문화유산인 군산 신흥동 일본식 가옥(히로쓰 가옥) 후문 바로 옆 13m 거리에 있습니다. 사장님은 이 집이 히로쓰 가옥 관리인이 살던 집이라고 전합니다. 백 년 된 고택을 리모델링해 2024년 5월 독채 펜션으로 문을 열었습니다.","status":3,"sourceType":4,"sortOrder":30},{"faqId":"bbd98512-574e-4601-bd16-b48654aaf5d9","question":"주요 관광지까지 걸어서 얼마나 걸리나요?","answer":"초원사진관, 이성당, 군산 시간여행마을 등 주요 관광명소가 걸어서 최대 8분 거리입니다. 차 없이 도보 여행이 가능합니다.","status":3,"sourceType":4,"sortOrder":31}],"links":[{"channel":3,"url":"https://m.place.naver.com/place/1133638931/home","title":"스테이,머뭄 네이버 플레이스","confirmed":true},{"channel":4,"url":"https://www.instagram.com/staymeomoom","title":"스테이,머뭄 인스타그램","confirmed":true}],"local":{"attractions":[{"name":"군산 신흥동 일본식가옥(히로쓰 가옥)","category":"관광지","searchQuery":"군산 신흥동 일본식가옥(히로쓰 가옥)","distanceText":"13m","imageUrl":"/assets/mirror/f27b7440e2f1183f.jpg","description":"군산 신흥동 일본식 가옥(구 히로쓰 가옥)은 일제강점기에 군산에서 포목점과 소규모 농장을 운영하며 군산부협의회 의원을 지낸 일본인이 건립한 일본식 2층 목조 가옥이다."},{"name":"초원사진관","category":"관광지","searchQuery":"초원사진관","distanceText":"248m","imageUrl":"/assets/mirror/d5433fe48de3da05.jpg","description":"초원사진관은 1998년 1월에 개봉한 영화 \u003c8월의 크리스마스\u003e의 촬영 장소로 유명한 곳이다. 이 영화는 배우 한석규와 심은하가 주연인 영화로 시한부 인생을 사는 사진사 정원(한석규)과 어느 날 그…"},{"name":"군산 영화의 거리","category":"관광지","searchQuery":"군산 영화의 거리","distanceText":"318m","imageUrl":"/assets/mirror/6a0c445e6dd940af.jpg","description":"시간과 공간을 초월한 특별한 경험을 할 수 있는 곳이다. 그 중에서 초원사진관은 영화 \u003c8월의 크리스마스\u003e 촬영지로 잘 알려진 관광명소이다."},{"name":"우체통거리","category":"관광지","searchQuery":"우체통거리","distanceText":"566m","imageUrl":"/assets/mirror/43461e391f83b9e8.jpg","description":"군산시 우체통거리는 2016년 도시재생사업으로 추진된 주민공모사업에 우체국 주변 주민들로 구성된 도란도란 공동체가 참여하면서 주민들이 직접 폐 우체통을 손질하고 그림을 그려 상가 앞에 설치하면서 조…"},{"name":"군산근대미술관","category":"관광지","searchQuery":"군산근대미술관","distanceText":"708m","imageUrl":"/assets/mirror/d8764592a03b0103.jpg","description":"18은행은 일본 나가사키에 본사를 두고 있던 은행으로, 숫자 18은 은행 설립인가 순서를 의미한다. 군산지점은 조선에서 7번째 지점으로 1907년에 설립되었다."},{"name":"옛 군산세관","category":"관광지","searchQuery":"옛 군산세관","distanceText":"728m","imageUrl":"/assets/mirror/22af0bff45e5fce8.jpg","description":"구 군산세관본관 건물이 완공된 것은 1908년으로, 일제강점기가 시작되기 전에 우리나라 정부가 만든 근대식 건물이다."},{"name":"근대건축관 (구)조선은행군산지점","category":"관광지","searchQuery":"근대건축관 (구)조선은행군산지점","distanceText":"788m","imageUrl":"/assets/mirror/5000f0a4feb45788.jpg","description":"1923년에 건립된 일제의 건물로 일제가 식민 지배를 위해 운영한 대표적인 금융시설이었다. 이 건물은 붉은 벽돌로 지은 4층 높이의 2층 건물로, 정면에 돌출된 현관을 중심으로 평아치를 5개 세우고…"},{"name":"군산 시간여행마을","category":"관광지","searchQuery":"군산 시간여행마을","distanceText":"1.1km","imageUrl":"/assets/mirror/7ba545fdbc8ad273.jpg","description":"군산은 근대사에서 수탈의 아픔과 이에 항거한 열정의 도시로, 일제강점기 근대 문화를 상상해볼 수 있는 문화재가 원도심을 중심으로 잘 보존되었다."},{"name":"월명호수","category":"관광지","searchQuery":"월명호수","distanceText":"1.6km","imageUrl":"/assets/mirror/c8c6a295ae1c0810.jpg","description":"월명공원의 품속에는 1912년에 시민에게 식수를 공급하기 위해 만든 제1수원지가 물안개를 뿜어내는 산 속의 호수로 변하여 산새와 작은 동물들이 목을 축이는 곳이 되었고 설림산 아래 천년고찰 은적사에…"},{"name":"은적사(군산)","category":"관광지","searchQuery":"은적사(군산)","distanceText":"2.3km","imageUrl":"/assets/mirror/669abc2b08640d3e.jpg","description":"은적사는 전라북도 내에 있는 사찰 가운데 가장 오래된 백제 시대 사찰이다. 천방사, 선림사 등으로도 불리었던 은적사는 대한불교 조계종 제17교구 본사인 금산사의 말사이다."},{"name":"서천문화예술창작공간","category":"관광지","searchQuery":"서천문화예술창작공간","distanceText":"2.6km","imageUrl":"/assets/mirror/7f8f81d5eeb33f73.jpg","description":"서천군 문화예술창작공간은 일제시대에 미곡창고로 사용된 건물로 이름에서 유추할 수 있듯이, 본래의 목적은 쌀을 보관하기 위해 세워진 건물이다."},{"name":"경암동 철길마을","category":"관광지","searchQuery":"경암동 철길마을","distanceText":"2.7km","imageUrl":"/assets/mirror/6d59722cedddd566.jpg","description":"경암동 철길마을은 1944년 전라북도 군산시 경암동에 준공하여 페이퍼 코리아 공장과 군산역을 연결하는 총연장 2.5km 철로 주변의 마을을 총괄하여 붙인 이름이다."}],"restaurants":[{"name":"도란","category":"맛집","searchQuery":"도란","distanceText":"135m","imageUrl":"/assets/mirror/12938240987c1f78.jpg","description":"전라북도 군산시 월명동에 있는 요리주점이다. 와인과 함께 카페와 양식을 메뉴를 주로 다루고 있다. 대표 메뉴로는 감바스, 해산물 크림 스튜, 부채살 스테이크 등이 있다."},{"name":"물밀소","category":"맛집","searchQuery":"물밀소","distanceText":"297m","imageUrl":"/assets/mirror/07600bb67b91dd15.jpg","description":"월명동에 자리한 베이커리 물밀소는 신생 빵집으로 입소문이 자자하다. 붉은 벽돌의 아담한 외관을 가졌고 매장 내부에는 저온숙성한 반죽으로 빚어낸 건강한 빵들이 진열돼 있다."},{"name":"일흥옥","category":"맛집","searchQuery":"일흥옥","distanceText":"300m","imageUrl":"/assets/mirror/3420ff7e357ab7ef.jpg","description":"일흥옥은 군산 테디베어뮤지엄 인근에 위치한 국밥 전문점이다. 중소벤처기업부로부터 백년가게 인증을 받은 곳이다."},{"name":"명월갈비","category":"맛집","searchQuery":"명월갈비","distanceText":"351m","imageUrl":"/assets/mirror/881d1f7ff820942f.jpg","description":"군산 신창동에 있는 명월갈비는 군산 지역의 대표 양념 소갈비 전문 맛집이다. 플러스 등급의 한우 갈비를 비법 양념장으로 숙성한 단품 메뉴로 오랜 기간 동안 인기를 유지해오고 있다."},{"name":"만남스넥","category":"맛집","searchQuery":"만남스넥","distanceText":"405m","imageUrl":"/assets/mirror/d2be5a053a944cfe.jpg","description":"만남스넥은 군산 현지인들이 좋아하는 메뉴인 잡탕을 파는 분식집이다. 잡탕은 떡, 만두, 라면, 어묵 등을 넣고 같이 끓인 메뉴인데, 떡볶이나 라볶이보다 국물이 많고 부재료가 풍부해 잡탕으로 불리게…"},{"name":"동국사다온","category":"맛집","searchQuery":"동국사다온","distanceText":"418m","imageUrl":"/assets/mirror/4db345bd2cb4f0d7.jpg","description":"전라북도 군산시 일본식 사찰인 동국사 사찰 내에 있는 테이크아웃 커피 전문점이다. 불교 용품과 함께 대추차, 오미자차, 오룡차 등 다양한 차 종류를 판매하고 있다."},{"name":"이성당","category":"맛집","searchQuery":"이성당","distanceText":"455m","imageUrl":"/assets/mirror/37c90494d28478e2.png","description":"일본 시마네현 이즈모시에 살다가 1906년 조선으로 건너온 히로세 야스타로라는 일본인이 ‘이즈모야’라는 이름으로 문을 열어 영업한 것이 시초이다."},{"name":"국제반점","category":"맛집","searchQuery":"국제반점","distanceText":"467m","imageUrl":"/assets/mirror/149c18951bdcaff8.jpg","description":"군산 영화동에 있는 국제반점은 1960년대 초 진흥반점으로 개업해 현재는 상호를 국제 반점으로 변경하여 성업 중인 중식당이다. 영화 \u003c타짜\u003e에 등장해 군산의 관광 명소가 되었다."}],"festivals":[{"name":"2026 군산 은파벚꽃야시장","month":"3~4월","period":"2026년 3월 28일 – 4월 12일","location":"은파호수공원","description":"군산 은파벚꽃야시장은 상인분들의 한 해를 힘차게 시작할 수 있도록 돕는 자리이자 군산의 대표적인 봄 축제였던 벚꽃축제가 사라진 아쉬움을 해소하고자 기획하게 된 행사이다.","searchQuery":"2026 군산 은파벚꽃야시장","season":"봄","startDate":"2026-03-28","endDate":"2026-04-12","imageUrl":"/assets/mirror/82606e203454e3ff.jpg"},{"name":"군산 수제맥주\u0026블루스 페스티벌","month":"6월","period":"2026년 6월 12일 – 14일","location":"군산근대역사박물관 주차장","description":"'군산 수제맥주\u0026블루스 페스티벌'은 국내 유일의 로컬 수제맥주 축제로, 100% 군산보리로 만드는 군산맥아로 만든 진짜 우리 수제맥주와 블루스음악과 함께 항구도시 군산의 초여름 밤, 낭만을 만끽할…","searchQuery":"군산 수제맥주\u0026블루스 페스티벌","season":"여름","startDate":"2026-06-12","endDate":"2026-06-14","imageUrl":"/assets/mirror/92f65116f3e58ab6.jpg"},{"name":"군산짬뽕페스티벌","month":"10월","period":"2025년 10월 9일 – 12일","location":"군산 백년광장 일원(장미동)","description":"70년 짬뽕역사의 도시, 군산에서 10월 9일부터 12일까지, 4일간 짬뽕페스티벌이 개최된다. 얼큰, 화끈, 개운한 국물 베이스인 해물가득한 짬뽕, 고기짬뽕 등 다양한 테마의 짬뽕을 축제장에서 선보…","searchQuery":"군산짬뽕페스티벌","season":"가을","startDate":"2025-10-09","endDate":"2025-10-12","imageUrl":"/assets/mirror/a1b00af88aa06cd9.jpg"},{"name":"군산시간여행축제","month":"10월","period":"2026년 10월 2일 – 5일","location":"구·시청광장 및 시간여행마을 일원","description":"군산시간여행축제는 일제강점기 민중의 항거와 치열한 삶의 역사를 간직한 근대 군산을 배경으로, 군산의 과거와 현재, 그리고 미래를 잇는 특별한 시간여행을 선사하는 축제이다.","searchQuery":"군산시간여행축제","season":"가을","startDate":"2026-10-02","endDate":"2026-10-05","imageUrl":"/assets/mirror/4b4086345c20b968.jpg"},{"name":"익산천만송이 국화축제","month":"10월","season":"가을","location":"전북특별자치도 익산시 하나로 322 (어양동) · 차로 30분","description":"가을을 대표하는 축제로, 대형 조형물에 국화를 심어 만든 작품과 전국 최고 규모의 국화정원, 국화분재 특별전시가 열린다.","searchQuery":"익산천만송이 국화축제","imageUrl":"/assets/mirror/6194651114d1e740.jpg"},{"name":"익산마한문화대전","month":"10월","season":"가을","location":"전북특별자치도 익산시 금마면 고도9길 41-14 · 차로 35분","description":"익산 마한의 정통성을 다지고 익산을 마한 문화 선진지로 알리기 위해 여는 행사이다.","searchQuery":"익산마한문화대전","imageUrl":"/assets/mirror/49558512d8d52e1a.jpg"},{"name":"강경젓갈축제","month":"10월","season":"가을","location":"충청남도 논산시 강경읍 금백로 45 · 차로 40분","description":"전통의 맛과 문화를 현대적으로 재해석한 축제로, 금강의 자연경관과 강경의 역사 자원을 함께 둘러볼 수 있다.","searchQuery":"강경젓갈축제","imageUrl":"/assets/mirror/7ccd68de6b280bc2.jpg"},{"name":"부안 설(雪)숭어 축제","month":"12월","season":"겨울","location":"전북특별자치도 부안군 부안읍 서외리 · 차로 35분","description":"겨울철에 맛이 오르는 설숭어를 주제로 한 지역 특산 수산물 축제이다. 설숭어 음식 판매와 시식, 어촌 문화 체험 프로그램이 열린다.","searchQuery":"부안 설숭어 축제","imageUrl":"/assets/mirror/4389ded2dee1ad98.jpg"},{"name":"서천 마량진항 해넘이 해돋이 행사","month":"12월","season":"겨울","location":"충청남도 서천군 서면 서인로 58 · 차로 30분","description":"12월 31일부터 1월 1일까지 마량진항 일원에서 열린다. 서해안에서 일몰과 일출을 한자리에서 볼 수 있는 곳이다.","searchQuery":"서천 마량진항 해넘이 해돋이","imageUrl":"/assets/mirror/8fc0a7522e3eb080.png"},{"name":"선유도 여름 노을축제","season":"여름","month":"7월","period":"2026년 7월 25일","location":"군산 선유도해수욕장","description":"서해 낙조 명소인 선유도해수욕장 특설무대에서 열리는 한여름 밤 축제입니다. 지역 예술인의 공연과 참여 행사가 열리고 폭죽쇼로 마무리합니다. 사진은 개최 장소인 선유도해수욕장입니다.","searchQuery":"선유도 여름 노을축제","imageUrl":"/assets/mirror/c971fb408ad897e7.jpg"}],"weather":{"temperature":27.9,"condition":"구름많음","observedAt":"2026-09-03T12:30","notes":{"맑음":"마당 평상에 앉으면 새소리만 들립니다. 정원 쪽으로 그늘이 길게 눕는 시간입니다.","흐림":"빛이 낮게 깔리는 날입니다. 창고형 CAFÉ 공간이 이런 날 가장 아늑합니다.","비":"창고형 CAFÉ 공간에서 빗소리를 들으실 수 있습니다. 백 년 된 기와를 타고 내리는 소리가 다릅니다.","눈":"기와에 눈이 앉으면 골목이 통째로 조용해집니다. 정원 석등만 켜 두겠습니다."},"tempNotes":{"혹서":"낮에는 군산근대미술관·근대건축관처럼 실내로 도는 편이 낫습니다. 월명호수 한 바퀴는 해가 넘어간 뒤가 시원합니다.","더움":"선유도까지 다녀오기 좋은 기온입니다. 골목은 오후 늦게 도세요 — 경암동 철길마을은 해 질 때가 사람이 적습니다.","선선":"걷기에 가장 좋은 날입니다. 히로쓰 가옥에서 초원사진관·영화의 거리까지 걸어서 그대로 이어집니다.","쌀쌀":"월명호수를 한 바퀴 돌고 이성당이나 국제반점에서 따뜻한 것을 드시면 알맞습니다.","추움":"밖은 바람이 매섭습니다. 근대건축관·근대미술관과 시간여행마을 실내를 묶어 도는 편이 낫습니다."}},"syncedAt":"2026-09-09T13:40:00+00:00"},"routes":[],"narrative":{"about":["스테이,머뭄은 전북특별자치도 군산시 신흥동 절골길에 있는 독채 펜션입니다. 1920년대에 지어진 백 년 고택으로, 국가등록문화유산인 군산 신흥동 일본식 가옥(히로쓰 가옥) 후문 바로 옆 — 걸어서 13m 거리입니다. 사장님은 이 집이 히로쓰 가옥 관리인이 살던 집이라고 전합니다. 2024년 5월에 리모델링을 마치고 문을 열었고, A동과 B동 두 동을 각각 한 팀만 씁니다. 기준 2인 · 최대 4인까지 묵을 수 있습니다.","A동은 편안한 일본집에 와 있는 듯한 느낌의 공간이고, B동은 아늑하고 깔끔한 현대식 모던 스타일 공간입니다. 두 동 모두 독립된 아담한 정원과 창고형 CAFÉ 공간이 딸려 있어, 마당과 거실에서 새소리를 들으며 쉴 수 있습니다.","인테리어와 가구, 소품, 침구를 하나하나 준비해 두었습니다. 특히 침구류는 매일 세탁·살균하고 건조해 쓰시는 분이 바뀔 때마다 새것처럼 나갑니다."],"summary":"1920년대 적산가옥을 고쳐 지은 독채 두 동, 매일 세탁·살균하는 침구","tagline":"히로쓰 가옥 담 너머, 백 년 된 집 한 채"},"theme":{"templateId":"stay-retro","colors":{"primary":"#1b1a15","secondary":"#4c4739","bg":"#e4dac0","card":"#efe7d3","text":"#1b1a15","accent":"#bf2f1b"},"fontStyle":"옛 간판체","sections":[{"id":"hero","name":"히어로","enabled":true,"locked":true},{"id":"intro","name":"소개","enabled":true,"locked":false},{"id":"rooms","name":"객실 안내","enabled":true,"locked":false},{"id":"event","name":"스테이,머뭄 소식","enabled":true,"locked":false,"data":"{\"kind\":\"event\",\"version\":1,\"title\":\"스테이,머뭄 소식\",\"subtitle\":\"미리 알아 두시면 좋은 것들\",\"linkUrl\":\"https://www.instagram.com/staymeomoom\",\"linkLabel\":\"인스타그램에서 보기\",\"items\":[{\"kind\":\"공지\",\"title\":\"입실 전 확인해 주세요\",\"summary\":\"머무시기 전에 꼭 읽어 주셔야 하는 것들입니다. 미숙지로 생긴 불편과 손해는 도와드리기 어렵습니다.\",\"body\":\"· 반려동물은 입실하실 수 없습니다.\\n· 건물과 객실 안은 절대 금연입니다. 바로 옆이 군산 신흥동 일본식가옥(히로쓰 가옥)이라 화재에 각별히 유의해 주세요.\\n· 냄새가 잘 빠지지 않는 생선구이·고기구이·튀김류 조리는 하실 수 없습니다.\\n· 객실 안에서 음식을 드실 때 오염에 주의해 주세요.\\n· 객실 시설과 비품, 침구가 손상·파손·오염되면 변상을 요청드릴 수 있습니다.\\n· 정기적으로 방역하고 있으나 자연과 함께하는 공간이라 가끔 벌레나 이물질이 들어올 수 있습니다. 이 사유로는 환불이 어려운 점 양해 부탁드립니다.\\n· 귀중품과 현금은 분실 위험이 있으니 직접 관리해 주세요.\\n· 안전과 보안을 위해 대문 입구 쪽에 CCTV를 운영하고 있습니다. 객실 안에는 없습니다.\\n· 숙박업소는 법적으로 청소년 혼숙이 금지되어 있습니다. 미성년자는 보호자를 동반하셔야 이용하실 수 있습니다.\",\"verified\":\"확인\"},{\"kind\":\"공지\",\"title\":\"인원이 늘면 미리 연락 주세요\",\"summary\":\"예약하신 인원보다 늘어나는 경우, 오시기 전에 숙소로 연락 부탁드립니다.\",\"body\":\"· 기준 인원(2인)을 넘으면 1인당 20,000원이 추가됩니다.\\n· 최대 인원(4인)을 넘으면 입실이 불가능할 수 있고, 이 사유로는 환불을 받으실 수 없습니다.\\n· 다음 이용 고객을 위해 입실 15:00 이후, 퇴실 11:00 이전을 지켜 주세요.\",\"howTo\":\"연락처 0507-1497-0983\",\"verified\":\"확인\"},{\"kind\":\"공지\",\"title\":\"직화·개인 취사도구는 반입이 안 됩니다\",\"summary\":\"백 년 된 목조 고택이라 화재 예방 규정이 엄격합니다.\",\"body\":\"· 객실 안에서 생선이나 고기 등을 굽는 직화 방식은 허용되지 않습니다.\\n· 그릴, 숯, 전기·전열기구 등 개인적으로 준비해 오시는 취사도구는 반입이 금지되어 있습니다.\\n· 주방에서 간단한 조리는 하실 수 있습니다.\\n· 흡연은 지정된 장소를 이용해 주시고, 다른 이용객에게 피해가 갈 수 있는 오락·음주·고성방가는 삼가 주세요.\",\"verified\":\"확인\"}]}"},{"id":"info","name":"기본 정보","enabled":true,"locked":true},{"id":"booking","name":"실시간 예약","enabled":false,"locked":false},{"id":"video","name":"ADO2 영상 보기","enabled":true,"locked":false,"data":"{\"kind\": \"video\", \"version\": 1, \"title\": \"ADO2 영상 보기\", \"subtitle\": \"ADO2로 만든 홍보 영상입니다.\", \"items\": [{\"url\": \"https://youtube.com/shorts/c2ZdwhaB7S4\", \"caption\": \"머뭄의 하루\", \"verified\": \"확인\"}, {\"url\": \"https://youtube.com/shorts/_bbo3dVgOXs\", \"caption\": \"마당과 창고형 카페 공간\", \"verified\": \"확인\"}, {\"url\": \"https://youtube.com/shorts/qrCG4rOeJvs\", \"caption\": \"밤의 머뭄\", \"verified\": \"확인\"}], \"linkUrl\": \"https://ado2.o2osolution.ai/\", \"linkLabel\": \"ADO2 알아보기\"}"},{"id":"photos","name":"사진 갤러리","enabled":true,"locked":false,"variantId":"photos.carousel"},{"id":"map","name":"오시는 길","enabled":true,"locked":true},{"id":"festival","name":"계절별 축제","enabled":true,"locked":false},{"id":"local","name":"지역 정보","enabled":true,"locked":false},{"id":"itinerary","name":"추천 일정","enabled":true,"locked":false,"data":"{\"kind\":\"itinerary\",\"version\":1,\"title\":\"추천 일정\",\"subtitle\":\"며칠 묵느냐에 따라 다르게 돕니다\",\"items\":[{\"name\":\"섬까지 다녀오는 1박 2일\",\"duration\":\"1박 2일\",\"audience\":\"차를 가져온 손님\",\"why\":\"첫날은 도심에서 저녁을, 이튿날은 다리 건너 바다를 봅니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"17:00\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"저녁 · 빈해원\",\"minutes\":70,\"note\":\"옛 중식당 건물 자체가 문화재입니다.\",\"searchQuery\":\"군산 빈해원\",\"imageUrl\":\"/assets/mirror/2af9caa23a08a3ec.jpg\",\"latitude\":35.9888841406,\"longitude\":126.7139765207,\"moveMinutes\":12},{\"name\":\"밤 산책 · 군산 내항\",\"minutes\":50,\"note\":\"조명이 들어온 부잔교를 봅니다.\",\"searchQuery\":\"군산 내항\",\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\",\"latitude\":35.9892997236,\"longitude\":126.7174508158,\"moveMinutes\":6},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"고군산군도 선유도\",\"minutes\":150,\"moveMinutes\":45,\"note\":\"다리로 건너갑니다. 차로 이동.\",\"searchQuery\":\"군산 선유도\",\"imageUrl\":\"/assets/mirror/6ded14f21a17894b.jpg\",\"latitude\":35.8102049733,\"longitude\":126.4124052922},{\"name\":\"장자도\",\"minutes\":60,\"moveMinutes\":12,\"note\":\"섬과 섬 사이를 걸어 넘습니다.\",\"searchQuery\":\"군산 장자도\",\"imageUrl\":\"/assets/mirror/9c5594df6114561b.jpg\",\"latitude\":35.8101,\"longitude\":126.3973}]}]},{\"name\":\"걸어서만 도는 1박 2일\",\"duration\":\"1박 2일\",\"audience\":\"차 없이 오는 손님\",\"why\":\"숙소에서 1km 안으로만 움직입니다. 차를 두고 와도 됩니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"15:00\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"초원사진관\",\"minutes\":30,\"note\":\"숙소에서 걸어 3분입니다.\",\"moveMinutes\":6,\"searchQuery\":\"초원사진관\",\"imageUrl\":\"/assets/mirror/d5433fe48de3da05.jpg\",\"latitude\":35.9877369536,\"longitude\":126.7083826065},{\"name\":\"이성당\",\"minutes\":40,\"note\":\"단팥빵을 사서 마당에서 먹어도 됩니다.\",\"moveMinutes\":9,\"searchQuery\":\"군산 이성당\",\"latitude\":35.9870443303,\"longitude\":126.7111681428},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"09:30\",\"stops\":[{\"name\":\"군산근대역사박물관\",\"minutes\":90,\"note\":\"항구 도시가 어떻게 만들어졌는지 먼저 봅니다.\",\"moveMinutes\":8,\"searchQuery\":\"군산근대역사박물관\",\"imageUrl\":\"/assets/mirror/3e9e574d3b987c8d.jpg\",\"latitude\":35.9908197098,\"longitude\":126.7121231556},{\"name\":\"군산근대건축관\",\"minutes\":45,\"note\":\"옛 조선은행 건물입니다. 길 건너 바로.\",\"moveMinutes\":4,\"searchQuery\":\"군산근대건축관\",\"imageUrl\":\"/assets/mirror/d543dcde9f0eac6c.jpg\",\"latitude\":35.9894201935,\"longitude\":126.7141598237},{\"name\":\"해망굴\",\"minutes\":25,\"note\":\"1926년에 뚫린 굴입니다. 반대편이 바로 항구입니다.\",\"moveMinutes\":9,\"searchQuery\":\"군산 해망굴\",\"latitude\":35.9913180812,\"longitude\":126.7048496204}]}]},{\"name\":\"비가 와도 되는 1박 2일\",\"duration\":\"1박 2일\",\"audience\":\"비 예보를 보고 오는 손님\",\"why\":\"실내에서 실내로 이어집니다. 우산 하나로 하루가 끝납니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"15:00\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"군산근대역사박물관\",\"minutes\":90,\"note\":\"항구 도시가 어떻게 만들어졌는지 먼저 봅니다.\",\"moveMinutes\":8,\"searchQuery\":\"군산근대역사박물관\",\"imageUrl\":\"/assets/mirror/3e9e574d3b987c8d.jpg\",\"latitude\":35.9908197098,\"longitude\":126.7121231556},{\"name\":\"군산근대미술관\",\"minutes\":50,\"note\":\"걸어서 3분, 처마를 따라 갑니다.\",\"moveMinutes\":7,\"searchQuery\":\"군산근대미술관\",\"imageUrl\":\"/assets/mirror/1caf7fcac8ece19a.jpg\",\"latitude\":35.9899367776,\"longitude\":126.7127710457},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"군산근대건축관\",\"minutes\":45,\"note\":\"옛 조선은행 건물입니다. 길 건너 바로.\",\"moveMinutes\":8,\"searchQuery\":\"군산근대건축관\",\"imageUrl\":\"/assets/mirror/d543dcde9f0eac6c.jpg\",\"latitude\":35.9894201935,\"longitude\":126.7141598237},{\"name\":\"째보선창 근처 카페\",\"minutes\":90,\"note\":\"비가 그칠 때까지 앉아 있어도 됩니다.\",\"moveMinutes\":8,\"searchQuery\":\"군산 째보선창\",\"latitude\":35.9875345,\"longitude\":126.7199875}]}]},{\"name\":\"근대건축만 보는 1박 2일\",\"duration\":\"1박 2일\",\"audience\":\"건물을 보러 오는 손님\",\"why\":\"개항기 건물 넷을 하루에 몰아 봅니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"15:00\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"구 군산세관 본관\",\"minutes\":40,\"note\":\"소설의 세관이 그대로 서 있습니다.\",\"moveMinutes\":10,\"searchQuery\":\"구 군산세관\",\"imageUrl\":\"/assets/mirror/4d71067dc7e3f671.jpg\",\"latitude\":35.9915087952,\"longitude\":126.7113016917},{\"name\":\"군산근대건축관\",\"minutes\":45,\"note\":\"옛 조선은행 건물입니다. 길 건너 바로.\",\"moveMinutes\":5,\"searchQuery\":\"군산근대건축관\",\"imageUrl\":\"/assets/mirror/d543dcde9f0eac6c.jpg\",\"latitude\":35.9894201935,\"longitude\":126.7141598237},{\"name\":\"저녁 · 빈해원\",\"minutes\":70,\"note\":\"옛 중식당 건물 자체가 문화재입니다.\",\"searchQuery\":\"군산 빈해원\",\"imageUrl\":\"/assets/mirror/2af9caa23a08a3ec.jpg\",\"latitude\":35.9888841406,\"longitude\":126.7139765207,\"moveMinutes\":12},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"09:30\",\"stops\":[{\"name\":\"군산근대역사박물관\",\"minutes\":90,\"note\":\"항구 도시가 어떻게 만들어졌는지 먼저 봅니다.\",\"moveMinutes\":8,\"searchQuery\":\"군산근대역사박물관\",\"imageUrl\":\"/assets/mirror/3e9e574d3b987c8d.jpg\",\"latitude\":35.9908197098,\"longitude\":126.7121231556},{\"name\":\"군산근대미술관\",\"minutes\":50,\"note\":\"걸어서 3분, 처마를 따라 갑니다.\",\"moveMinutes\":4,\"searchQuery\":\"군산근대미술관\",\"imageUrl\":\"/assets/mirror/1caf7fcac8ece19a.jpg\",\"latitude\":35.9899367776,\"longitude\":126.7127710457},{\"name\":\"해망굴\",\"minutes\":25,\"note\":\"1926년에 뚫린 굴입니다. 반대편이 바로 항구입니다.\",\"moveMinutes\":9,\"searchQuery\":\"군산 해망굴\",\"latitude\":35.9913180812,\"longitude\":126.7048496204}]}]},{\"name\":\"이성당으로 시작하는 1박 2일\",\"duration\":\"1박 2일\",\"audience\":\"빵을 좋아하는 손님\",\"why\":\"아침 첫 자리를 이성당에 둡니다. 줄이 짧을 때 갑니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"16:00\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"초원사진관\",\"minutes\":30,\"note\":\"숙소에서 걸어 3분입니다.\",\"moveMinutes\":6,\"searchQuery\":\"초원사진관\",\"imageUrl\":\"/assets/mirror/d5433fe48de3da05.jpg\",\"latitude\":35.9877369536,\"longitude\":126.7083826065},{\"name\":\"한일옥\",\"minutes\":60,\"note\":\"무국으로 아침을 엽니다.\",\"moveMinutes\":8,\"searchQuery\":\"군산 한일옥\",\"latitude\":35.9874081251,\"longitude\":126.7082350808},{\"name\":\"밤 산책 · 군산 내항\",\"minutes\":50,\"note\":\"조명이 들어온 부잔교를 봅니다.\",\"searchQuery\":\"군산 내항\",\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\",\"latitude\":35.9892997236,\"longitude\":126.7174508158,\"moveMinutes\":10},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"08:30\",\"stops\":[{\"name\":\"이성당\",\"minutes\":40,\"note\":\"단팥빵을 사서 마당에서 먹어도 됩니다.\",\"moveMinutes\":12,\"searchQuery\":\"군산 이성당\",\"latitude\":35.9870443303,\"longitude\":126.7111681428},{\"name\":\"군산근대역사박물관\",\"minutes\":90,\"note\":\"항구 도시가 어떻게 만들어졌는지 먼저 봅니다.\",\"moveMinutes\":6,\"searchQuery\":\"군산근대역사박물관\",\"imageUrl\":\"/assets/mirror/3e9e574d3b987c8d.jpg\",\"latitude\":35.9908197098,\"longitude\":126.7121231556},{\"name\":\"째보선창\",\"minutes\":40,\"moveMinutes\":9,\"note\":\"소설 속 노동의 자리입니다.\",\"searchQuery\":\"군산 째보선창\",\"latitude\":35.9875345,\"longitude\":126.7199875}]}]},{\"name\":\"철길마을까지 가는 1박 2일\",\"duration\":\"1박 2일\",\"audience\":\"사진을 찍는 손님\",\"why\":\"골목과 철길, 두 가지 배경을 하루씩 나눠 씁니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"15:30\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"초원사진관\",\"minutes\":30,\"note\":\"숙소에서 걸어 3분입니다.\",\"moveMinutes\":6,\"searchQuery\":\"초원사진관\",\"imageUrl\":\"/assets/mirror/d5433fe48de3da05.jpg\",\"latitude\":35.9877369536,\"longitude\":126.7083826065},{\"name\":\"군산근대미술관\",\"minutes\":50,\"note\":\"걸어서 3분, 처마를 따라 갑니다.\",\"moveMinutes\":7,\"searchQuery\":\"군산근대미술관\",\"imageUrl\":\"/assets/mirror/1caf7fcac8ece19a.jpg\",\"latitude\":35.9899367776,\"longitude\":126.7127710457},{\"name\":\"저녁 · 빈해원\",\"minutes\":70,\"note\":\"옛 중식당 건물 자체가 문화재입니다.\",\"searchQuery\":\"군산 빈해원\",\"imageUrl\":\"/assets/mirror/2af9caa23a08a3ec.jpg\",\"latitude\":35.9888841406,\"longitude\":126.7139765207,\"moveMinutes\":9},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"경암동 철길마을\",\"minutes\":60,\"moveMinutes\":18,\"note\":\"해 질 무렵 빛이 가장 좋습니다.\",\"searchQuery\":\"군산 경암동 철길마을\",\"imageUrl\":\"/assets/mirror/6d59722cedddd566.jpg\",\"latitude\":35.9813520474,\"longitude\":126.7362330582},{\"name\":\"이성당\",\"minutes\":40,\"note\":\"단팥빵을 사서 마당에서 먹어도 됩니다.\",\"moveMinutes\":14,\"searchQuery\":\"군산 이성당\",\"latitude\":35.9870443303,\"longitude\":126.7111681428}]}]},{\"name\":\"항구만 도는 1박 2일\",\"duration\":\"1박 2일\",\"audience\":\"바다가 보고 싶은 손님\",\"why\":\"내항과 부잔교, 째보선창을 이어 걷습니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"16:00\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"군산 내항 부잔교\",\"minutes\":50,\"note\":\"쌀이 실려 나가던 다리입니다.\",\"moveMinutes\":11,\"searchQuery\":\"군산 내항 부잔교\",\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\",\"latitude\":35.9892997236,\"longitude\":126.7174508158},{\"name\":\"밤 산책 · 군산 내항\",\"minutes\":50,\"note\":\"조명이 들어온 부잔교를 봅니다.\",\"searchQuery\":\"군산 내항\",\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\",\"latitude\":35.9892997236,\"longitude\":126.7174508158,\"moveMinutes\":3},{\"name\":\"저녁 · 빈해원\",\"minutes\":70,\"note\":\"옛 중식당 건물 자체가 문화재입니다.\",\"searchQuery\":\"군산 빈해원\",\"imageUrl\":\"/assets/mirror/2af9caa23a08a3ec.jpg\",\"latitude\":35.9888841406,\"longitude\":126.7139765207,\"moveMinutes\":6},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"째보선창\",\"minutes\":40,\"moveMinutes\":12,\"note\":\"소설 속 노동의 자리입니다.\",\"searchQuery\":\"군산 째보선창\",\"latitude\":35.9875345,\"longitude\":126.7199875},{\"name\":\"째보선창 근처 카페\",\"minutes\":90,\"note\":\"비가 그칠 때까지 앉아 있어도 됩니다.\",\"moveMinutes\":2,\"searchQuery\":\"군산 째보선창\",\"latitude\":35.9875345,\"longitude\":126.7199875},{\"name\":\"해망굴\",\"minutes\":25,\"note\":\"1926년에 뚫린 굴입니다. 반대편이 바로 항구입니다.\",\"moveMinutes\":10,\"searchQuery\":\"군산 해망굴\",\"latitude\":35.9913180812,\"longitude\":126.7048496204}]}]},{\"name\":\"소설 『탁류』를 따라 걷는 1박 2일\",\"duration\":\"1박 2일\",\"audience\":\"채만식을 읽어 본 손님\",\"why\":\"소설의 무대인 내항과 채만식문학관을 잇습니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"15:00\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"채만식문학관\",\"minutes\":80,\"moveMinutes\":16,\"note\":\"금강 하구 옆입니다. 차로 이동.\",\"searchQuery\":\"군산 채만식문학관\",\"imageUrl\":\"/assets/mirror/8043bad52d95a84b.jpg\",\"latitude\":36.0085981064,\"longitude\":126.7578853293},{\"name\":\"군산 내항 부잔교\",\"minutes\":50,\"note\":\"쌀이 실려 나가던 다리입니다.\",\"moveMinutes\":14,\"searchQuery\":\"군산 내항 부잔교\",\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\",\"latitude\":35.9892997236,\"longitude\":126.7174508158},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"09:30\",\"stops\":[{\"name\":\"군산근대역사박물관\",\"minutes\":90,\"note\":\"항구 도시가 어떻게 만들어졌는지 먼저 봅니다.\",\"moveMinutes\":8,\"searchQuery\":\"군산근대역사박물관\",\"imageUrl\":\"/assets/mirror/3e9e574d3b987c8d.jpg\",\"latitude\":35.9908197098,\"longitude\":126.7121231556},{\"name\":\"째보선창\",\"minutes\":40,\"moveMinutes\":7,\"note\":\"소설 속 노동의 자리입니다.\",\"searchQuery\":\"군산 째보선창\",\"latitude\":35.9875345,\"longitude\":126.7199875},{\"name\":\"이성당\",\"minutes\":40,\"note\":\"단팥빵을 사서 마당에서 먹어도 됩니다.\",\"moveMinutes\":9,\"searchQuery\":\"군산 이성당\",\"latitude\":35.9870443303,\"longitude\":126.7111681428}]}]},{\"name\":\"아이와 함께 1박 2일\",\"duration\":\"1박 2일\",\"audience\":\"아이를 데려온 손님\",\"why\":\"걷는 거리를 짧게 끊고 군것질할 자리를 사이에 둡니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"15:30\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"초원사진관\",\"minutes\":30,\"note\":\"숙소에서 걸어 3분입니다.\",\"moveMinutes\":6,\"searchQuery\":\"초원사진관\",\"imageUrl\":\"/assets/mirror/d5433fe48de3da05.jpg\",\"latitude\":35.9877369536,\"longitude\":126.7083826065},{\"name\":\"이성당\",\"minutes\":40,\"note\":\"단팥빵을 사서 마당에서 먹어도 됩니다.\",\"moveMinutes\":9,\"searchQuery\":\"군산 이성당\",\"latitude\":35.9870443303,\"longitude\":126.7111681428},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"경암동 철길마을\",\"minutes\":60,\"moveMinutes\":18,\"note\":\"해 질 무렵 빛이 가장 좋습니다.\",\"searchQuery\":\"군산 경암동 철길마을\",\"imageUrl\":\"/assets/mirror/6d59722cedddd566.jpg\",\"latitude\":35.9813520474,\"longitude\":126.7362330582},{\"name\":\"군산근대역사박물관\",\"minutes\":90,\"note\":\"항구 도시가 어떻게 만들어졌는지 먼저 봅니다.\",\"moveMinutes\":15,\"searchQuery\":\"군산근대역사박물관\",\"imageUrl\":\"/assets/mirror/3e9e574d3b987c8d.jpg\",\"latitude\":35.9908197098,\"longitude\":126.7121231556}]}]},{\"name\":\"늦게 도착하는 1박 2일\",\"duration\":\"1박 2일\",\"audience\":\"퇴근하고 오는 손님\",\"why\":\"첫날은 저녁과 밤 산책만, 이튿날에 몰아 봅니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"19:00\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"저녁 · 빈해원\",\"minutes\":70,\"note\":\"옛 중식당 건물 자체가 문화재입니다.\",\"searchQuery\":\"군산 빈해원\",\"imageUrl\":\"/assets/mirror/2af9caa23a08a3ec.jpg\",\"latitude\":35.9888841406,\"longitude\":126.7139765207,\"moveMinutes\":10},{\"name\":\"밤 산책 · 군산 내항\",\"minutes\":50,\"note\":\"조명이 들어온 부잔교를 봅니다.\",\"searchQuery\":\"군산 내항\",\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\",\"latitude\":35.9892997236,\"longitude\":126.7174508158,\"moveMinutes\":6},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"09:30\",\"stops\":[{\"name\":\"군산근대건축관\",\"minutes\":45,\"note\":\"옛 조선은행 건물입니다. 길 건너 바로.\",\"moveMinutes\":8,\"searchQuery\":\"군산근대건축관\",\"imageUrl\":\"/assets/mirror/d543dcde9f0eac6c.jpg\",\"latitude\":35.9894201935,\"longitude\":126.7141598237},{\"name\":\"구 군산세관 본관\",\"minutes\":40,\"note\":\"소설의 세관이 그대로 서 있습니다.\",\"moveMinutes\":4,\"searchQuery\":\"구 군산세관\",\"imageUrl\":\"/assets/mirror/4d71067dc7e3f671.jpg\",\"latitude\":35.9915087952,\"longitude\":126.7113016917},{\"name\":\"군산근대미술관\",\"minutes\":50,\"note\":\"걸어서 3분, 처마를 따라 갑니다.\",\"moveMinutes\":6,\"searchQuery\":\"군산근대미술관\",\"imageUrl\":\"/assets/mirror/1caf7fcac8ece19a.jpg\",\"latitude\":35.9899367776,\"longitude\":126.7127710457},{\"name\":\"이성당\",\"minutes\":40,\"note\":\"단팥빵을 사서 마당에서 먹어도 됩니다.\",\"moveMinutes\":8,\"searchQuery\":\"군산 이성당\",\"latitude\":35.9870443303,\"longitude\":126.7111681428}]}]},{\"name\":\"천천히 도는 2박 3일\",\"duration\":\"2박 3일\",\"audience\":\"쉬러 오는 손님\",\"why\":\"하루에 두세 군데만 갑니다. 나머지는 마당입니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"17:00\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"저녁 · 빈해원\",\"minutes\":70,\"note\":\"옛 중식당 건물 자체가 문화재입니다.\",\"searchQuery\":\"군산 빈해원\",\"imageUrl\":\"/assets/mirror/2af9caa23a08a3ec.jpg\",\"latitude\":35.9888841406,\"longitude\":126.7139765207,\"moveMinutes\":10},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"11:00\",\"stops\":[{\"name\":\"군산근대역사박물관\",\"minutes\":90,\"note\":\"항구 도시가 어떻게 만들어졌는지 먼저 봅니다.\",\"moveMinutes\":8,\"searchQuery\":\"군산근대역사박물관\",\"imageUrl\":\"/assets/mirror/3e9e574d3b987c8d.jpg\",\"latitude\":35.9908197098,\"longitude\":126.7121231556},{\"name\":\"군산 내항 부잔교\",\"minutes\":50,\"note\":\"쌀이 실려 나가던 다리입니다.\",\"moveMinutes\":5,\"searchQuery\":\"군산 내항 부잔교\",\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\",\"latitude\":35.9892997236,\"longitude\":126.7174508158}]},{\"label\":\"셋째 날\",\"startTime\":\"11:00\",\"stops\":[{\"name\":\"이성당\",\"minutes\":40,\"note\":\"단팥빵을 사서 마당에서 먹어도 됩니다.\",\"moveMinutes\":12,\"searchQuery\":\"군산 이성당\",\"latitude\":35.9870443303,\"longitude\":126.7111681428},{\"name\":\"초원사진관\",\"minutes\":30,\"note\":\"숙소에서 걸어 3분입니다.\",\"moveMinutes\":7,\"searchQuery\":\"초원사진관\",\"imageUrl\":\"/assets/mirror/d5433fe48de3da05.jpg\",\"latitude\":35.9877369536,\"longitude\":126.7083826065}]}]},{\"name\":\"섬과 도심을 반씩 도는 2박 3일\",\"duration\":\"2박 3일\",\"audience\":\"차를 가져온 손님\",\"why\":\"도심 하루, 섬 하루, 남은 반나절은 골목입니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"16:00\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"구 군산세관 본관\",\"minutes\":40,\"note\":\"소설의 세관이 그대로 서 있습니다.\",\"moveMinutes\":10,\"searchQuery\":\"구 군산세관\",\"imageUrl\":\"/assets/mirror/4d71067dc7e3f671.jpg\",\"latitude\":35.9915087952,\"longitude\":126.7113016917},{\"name\":\"저녁 · 빈해원\",\"minutes\":70,\"note\":\"옛 중식당 건물 자체가 문화재입니다.\",\"searchQuery\":\"군산 빈해원\",\"imageUrl\":\"/assets/mirror/2af9caa23a08a3ec.jpg\",\"latitude\":35.9888841406,\"longitude\":126.7139765207,\"moveMinutes\":8},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"09:30\",\"stops\":[{\"name\":\"고군산군도 선유도\",\"minutes\":150,\"moveMinutes\":45,\"note\":\"다리로 건너갑니다. 차로 이동.\",\"searchQuery\":\"군산 선유도\",\"imageUrl\":\"/assets/mirror/6ded14f21a17894b.jpg\",\"latitude\":35.8102049733,\"longitude\":126.4124052922},{\"name\":\"장자도\",\"minutes\":60,\"moveMinutes\":12,\"note\":\"섬과 섬 사이를 걸어 넘습니다.\",\"searchQuery\":\"군산 장자도\",\"imageUrl\":\"/assets/mirror/9c5594df6114561b.jpg\",\"latitude\":35.8101,\"longitude\":126.3973}]},{\"label\":\"셋째 날\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"초원사진관\",\"minutes\":30,\"note\":\"숙소에서 걸어 3분입니다.\",\"moveMinutes\":6,\"searchQuery\":\"초원사진관\",\"imageUrl\":\"/assets/mirror/d5433fe48de3da05.jpg\",\"latitude\":35.9877369536,\"longitude\":126.7083826065},{\"name\":\"이성당\",\"minutes\":40,\"note\":\"단팥빵을 사서 마당에서 먹어도 됩니다.\",\"moveMinutes\":9,\"searchQuery\":\"군산 이성당\",\"latitude\":35.9870443303,\"longitude\":126.7111681428}]}]},{\"name\":\"골목을 세 번 나눠 걷는 2박 3일\",\"duration\":\"2박 3일\",\"audience\":\"차 없이 오는 손님\",\"why\":\"하루에 1km 안으로만 움직입니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"15:30\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"초원사진관\",\"minutes\":30,\"note\":\"숙소에서 걸어 3분입니다.\",\"moveMinutes\":6,\"searchQuery\":\"초원사진관\",\"imageUrl\":\"/assets/mirror/d5433fe48de3da05.jpg\",\"latitude\":35.9877369536,\"longitude\":126.7083826065},{\"name\":\"군산근대미술관\",\"minutes\":50,\"note\":\"걸어서 3분, 처마를 따라 갑니다.\",\"moveMinutes\":7,\"searchQuery\":\"군산근대미술관\",\"imageUrl\":\"/assets/mirror/1caf7fcac8ece19a.jpg\",\"latitude\":35.9899367776,\"longitude\":126.7127710457},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"군산근대역사박물관\",\"minutes\":90,\"note\":\"항구 도시가 어떻게 만들어졌는지 먼저 봅니다.\",\"moveMinutes\":8,\"searchQuery\":\"군산근대역사박물관\",\"imageUrl\":\"/assets/mirror/3e9e574d3b987c8d.jpg\",\"latitude\":35.9908197098,\"longitude\":126.7121231556},{\"name\":\"군산근대건축관\",\"minutes\":45,\"note\":\"옛 조선은행 건물입니다. 길 건너 바로.\",\"moveMinutes\":4,\"searchQuery\":\"군산근대건축관\",\"imageUrl\":\"/assets/mirror/d543dcde9f0eac6c.jpg\",\"latitude\":35.9894201935,\"longitude\":126.7141598237},{\"name\":\"해망굴\",\"minutes\":25,\"note\":\"1926년에 뚫린 굴입니다. 반대편이 바로 항구입니다.\",\"moveMinutes\":9,\"searchQuery\":\"군산 해망굴\",\"latitude\":35.9913180812,\"longitude\":126.7048496204}]},{\"label\":\"셋째 날\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"이성당\",\"minutes\":40,\"note\":\"단팥빵을 사서 마당에서 먹어도 됩니다.\",\"moveMinutes\":12,\"searchQuery\":\"군산 이성당\",\"latitude\":35.9870443303,\"longitude\":126.7111681428},{\"name\":\"째보선창\",\"minutes\":40,\"moveMinutes\":9,\"note\":\"소설 속 노동의 자리입니다.\",\"searchQuery\":\"군산 째보선창\",\"latitude\":35.9875345,\"longitude\":126.7199875}]}]},{\"name\":\"먹으러 오는 2박 3일\",\"duration\":\"2박 3일\",\"audience\":\"맛집을 도는 손님\",\"why\":\"빈해원·한일옥·이성당을 날마다 하나씩 둡니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"16:30\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"저녁 · 빈해원\",\"minutes\":70,\"note\":\"옛 중식당 건물 자체가 문화재입니다.\",\"searchQuery\":\"군산 빈해원\",\"imageUrl\":\"/assets/mirror/2af9caa23a08a3ec.jpg\",\"latitude\":35.9888841406,\"longitude\":126.7139765207,\"moveMinutes\":10},{\"name\":\"밤 산책 · 군산 내항\",\"minutes\":50,\"note\":\"조명이 들어온 부잔교를 봅니다.\",\"searchQuery\":\"군산 내항\",\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\",\"latitude\":35.9892997236,\"longitude\":126.7174508158,\"moveMinutes\":6},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"11:00\",\"stops\":[{\"name\":\"한일옥\",\"minutes\":60,\"note\":\"무국으로 아침을 엽니다.\",\"moveMinutes\":8,\"searchQuery\":\"군산 한일옥\",\"latitude\":35.9874081251,\"longitude\":126.7082350808},{\"name\":\"군산근대역사박물관\",\"minutes\":90,\"note\":\"항구 도시가 어떻게 만들어졌는지 먼저 봅니다.\",\"moveMinutes\":6,\"searchQuery\":\"군산근대역사박물관\",\"imageUrl\":\"/assets/mirror/3e9e574d3b987c8d.jpg\",\"latitude\":35.9908197098,\"longitude\":126.7121231556},{\"name\":\"째보선창 근처 카페\",\"minutes\":90,\"note\":\"비가 그칠 때까지 앉아 있어도 됩니다.\",\"moveMinutes\":7,\"searchQuery\":\"군산 째보선창\",\"latitude\":35.9875345,\"longitude\":126.7199875}]},{\"label\":\"셋째 날\",\"startTime\":\"09:30\",\"stops\":[{\"name\":\"이성당\",\"minutes\":40,\"note\":\"단팥빵을 사서 마당에서 먹어도 됩니다.\",\"moveMinutes\":12,\"searchQuery\":\"군산 이성당\",\"latitude\":35.9870443303,\"longitude\":126.7111681428},{\"name\":\"초원사진관\",\"minutes\":30,\"note\":\"숙소에서 걸어 3분입니다.\",\"moveMinutes\":7,\"searchQuery\":\"초원사진관\",\"imageUrl\":\"/assets/mirror/d5433fe48de3da05.jpg\",\"latitude\":35.9877369536,\"longitude\":126.7083826065}]}]},{\"name\":\"사진만 찍는 2박 3일\",\"duration\":\"2박 3일\",\"audience\":\"사진을 찍는 손님\",\"why\":\"아침 빛과 저녁 빛을 다른 자리에서 받습니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"16:00\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"초원사진관\",\"minutes\":30,\"note\":\"숙소에서 걸어 3분입니다.\",\"moveMinutes\":6,\"searchQuery\":\"초원사진관\",\"imageUrl\":\"/assets/mirror/d5433fe48de3da05.jpg\",\"latitude\":35.9877369536,\"longitude\":126.7083826065},{\"name\":\"군산 내항 부잔교\",\"minutes\":50,\"note\":\"쌀이 실려 나가던 다리입니다.\",\"moveMinutes\":11,\"searchQuery\":\"군산 내항 부잔교\",\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\",\"latitude\":35.9892997236,\"longitude\":126.7174508158},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"09:00\",\"stops\":[{\"name\":\"경암동 철길마을\",\"minutes\":60,\"moveMinutes\":18,\"note\":\"해 질 무렵 빛이 가장 좋습니다.\",\"searchQuery\":\"군산 경암동 철길마을\",\"imageUrl\":\"/assets/mirror/6d59722cedddd566.jpg\",\"latitude\":35.9813520474,\"longitude\":126.7362330582},{\"name\":\"해망굴\",\"minutes\":25,\"note\":\"1926년에 뚫린 굴입니다. 반대편이 바로 항구입니다.\",\"moveMinutes\":16,\"searchQuery\":\"군산 해망굴\",\"latitude\":35.9913180812,\"longitude\":126.7048496204}]},{\"label\":\"셋째 날\",\"startTime\":\"09:30\",\"stops\":[{\"name\":\"구 군산세관 본관\",\"minutes\":40,\"note\":\"소설의 세관이 그대로 서 있습니다.\",\"moveMinutes\":10,\"searchQuery\":\"구 군산세관\",\"imageUrl\":\"/assets/mirror/4d71067dc7e3f671.jpg\",\"latitude\":35.9915087952,\"longitude\":126.7113016917},{\"name\":\"군산근대건축관\",\"minutes\":45,\"note\":\"옛 조선은행 건물입니다. 길 건너 바로.\",\"moveMinutes\":4,\"searchQuery\":\"군산근대건축관\",\"imageUrl\":\"/assets/mirror/d543dcde9f0eac6c.jpg\",\"latitude\":35.9894201935,\"longitude\":126.7141598237}]}]},{\"name\":\"비가 와도 되는 2박 3일\",\"duration\":\"2박 3일\",\"audience\":\"비 예보를 보고 오는 손님\",\"why\":\"실내 세 곳을 날마다 하나씩 배치했습니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"15:00\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"군산근대역사박물관\",\"minutes\":90,\"note\":\"항구 도시가 어떻게 만들어졌는지 먼저 봅니다.\",\"moveMinutes\":8,\"searchQuery\":\"군산근대역사박물관\",\"imageUrl\":\"/assets/mirror/3e9e574d3b987c8d.jpg\",\"latitude\":35.9908197098,\"longitude\":126.7121231556},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"군산근대미술관\",\"minutes\":50,\"note\":\"걸어서 3분, 처마를 따라 갑니다.\",\"moveMinutes\":7,\"searchQuery\":\"군산근대미술관\",\"imageUrl\":\"/assets/mirror/1caf7fcac8ece19a.jpg\",\"latitude\":35.9899367776,\"longitude\":126.7127710457},{\"name\":\"군산근대건축관\",\"minutes\":45,\"note\":\"옛 조선은행 건물입니다. 길 건너 바로.\",\"moveMinutes\":4,\"searchQuery\":\"군산근대건축관\",\"imageUrl\":\"/assets/mirror/d543dcde9f0eac6c.jpg\",\"latitude\":35.9894201935,\"longitude\":126.7141598237},{\"name\":\"째보선창 근처 카페\",\"minutes\":90,\"note\":\"비가 그칠 때까지 앉아 있어도 됩니다.\",\"moveMinutes\":8,\"searchQuery\":\"군산 째보선창\",\"latitude\":35.9875345,\"longitude\":126.7199875}]},{\"label\":\"셋째 날\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"채만식문학관\",\"minutes\":80,\"moveMinutes\":16,\"note\":\"금강 하구 옆입니다. 차로 이동.\",\"searchQuery\":\"군산 채만식문학관\",\"imageUrl\":\"/assets/mirror/8043bad52d95a84b.jpg\",\"latitude\":36.0085981064,\"longitude\":126.7578853293},{\"name\":\"이성당\",\"minutes\":40,\"note\":\"단팥빵을 사서 마당에서 먹어도 됩니다.\",\"moveMinutes\":14,\"searchQuery\":\"군산 이성당\",\"latitude\":35.9870443303,\"longitude\":126.7111681428}]}]},{\"name\":\"늦게 시작하는 2박 3일\",\"duration\":\"2박 3일\",\"audience\":\"아침이 힘든 손님\",\"why\":\"세 날 모두 열한 시에 나섭니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"17:00\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"저녁 · 빈해원\",\"minutes\":70,\"note\":\"옛 중식당 건물 자체가 문화재입니다.\",\"searchQuery\":\"군산 빈해원\",\"imageUrl\":\"/assets/mirror/2af9caa23a08a3ec.jpg\",\"latitude\":35.9888841406,\"longitude\":126.7139765207,\"moveMinutes\":10},{\"name\":\"밤 산책 · 군산 내항\",\"minutes\":50,\"note\":\"조명이 들어온 부잔교를 봅니다.\",\"searchQuery\":\"군산 내항\",\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\",\"latitude\":35.9892997236,\"longitude\":126.7174508158,\"moveMinutes\":6},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"11:00\",\"stops\":[{\"name\":\"군산근대역사박물관\",\"minutes\":90,\"note\":\"항구 도시가 어떻게 만들어졌는지 먼저 봅니다.\",\"moveMinutes\":8,\"searchQuery\":\"군산근대역사박물관\",\"imageUrl\":\"/assets/mirror/3e9e574d3b987c8d.jpg\",\"latitude\":35.9908197098,\"longitude\":126.7121231556},{\"name\":\"군산근대미술관\",\"minutes\":50,\"note\":\"걸어서 3분, 처마를 따라 갑니다.\",\"moveMinutes\":4,\"searchQuery\":\"군산근대미술관\",\"imageUrl\":\"/assets/mirror/1caf7fcac8ece19a.jpg\",\"latitude\":35.9899367776,\"longitude\":126.7127710457}]},{\"label\":\"셋째 날\",\"startTime\":\"11:00\",\"stops\":[{\"name\":\"이성당\",\"minutes\":40,\"note\":\"단팥빵을 사서 마당에서 먹어도 됩니다.\",\"moveMinutes\":12,\"searchQuery\":\"군산 이성당\",\"latitude\":35.9870443303,\"longitude\":126.7111681428},{\"name\":\"초원사진관\",\"minutes\":30,\"note\":\"숙소에서 걸어 3분입니다.\",\"moveMinutes\":7,\"searchQuery\":\"초원사진관\",\"imageUrl\":\"/assets/mirror/d5433fe48de3da05.jpg\",\"latitude\":35.9877369536,\"longitude\":126.7083826065}]}]},{\"name\":\"섬에서 하루를 다 쓰는 2박 3일\",\"duration\":\"2박 3일\",\"audience\":\"차를 가져온 손님\",\"why\":\"둘째 날을 통째로 고군산군도에 씁니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"16:00\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"군산 내항 부잔교\",\"minutes\":50,\"note\":\"쌀이 실려 나가던 다리입니다.\",\"moveMinutes\":11,\"searchQuery\":\"군산 내항 부잔교\",\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\",\"latitude\":35.9892997236,\"longitude\":126.7174508158},{\"name\":\"저녁 · 빈해원\",\"minutes\":70,\"note\":\"옛 중식당 건물 자체가 문화재입니다.\",\"searchQuery\":\"군산 빈해원\",\"imageUrl\":\"/assets/mirror/2af9caa23a08a3ec.jpg\",\"latitude\":35.9888841406,\"longitude\":126.7139765207,\"moveMinutes\":6},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"09:00\",\"stops\":[{\"name\":\"고군산군도 선유도\",\"minutes\":150,\"moveMinutes\":45,\"note\":\"다리로 건너갑니다. 차로 이동.\",\"searchQuery\":\"군산 선유도\",\"imageUrl\":\"/assets/mirror/6ded14f21a17894b.jpg\",\"latitude\":35.8102049733,\"longitude\":126.4124052922},{\"name\":\"장자도\",\"minutes\":60,\"moveMinutes\":12,\"note\":\"섬과 섬 사이를 걸어 넘습니다.\",\"searchQuery\":\"군산 장자도\",\"imageUrl\":\"/assets/mirror/9c5594df6114561b.jpg\",\"latitude\":35.8101,\"longitude\":126.3973}]},{\"label\":\"셋째 날\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"군산근대건축관\",\"minutes\":45,\"note\":\"옛 조선은행 건물입니다. 길 건너 바로.\",\"moveMinutes\":8,\"searchQuery\":\"군산근대건축관\",\"imageUrl\":\"/assets/mirror/d543dcde9f0eac6c.jpg\",\"latitude\":35.9894201935,\"longitude\":126.7141598237},{\"name\":\"해망굴\",\"minutes\":25,\"note\":\"1926년에 뚫린 굴입니다. 반대편이 바로 항구입니다.\",\"moveMinutes\":9,\"searchQuery\":\"군산 해망굴\",\"latitude\":35.9913180812,\"longitude\":126.7048496204}]}]},{\"name\":\"소설 『탁류』를 따라 걷는 2박 3일\",\"duration\":\"2박 3일\",\"audience\":\"채만식을 읽어 본 손님\",\"why\":\"문학관에서 시작해 소설의 무대를 차례로 걷습니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"15:00\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"채만식문학관\",\"minutes\":80,\"moveMinutes\":16,\"note\":\"금강 하구 옆입니다. 차로 이동.\",\"searchQuery\":\"군산 채만식문학관\",\"imageUrl\":\"/assets/mirror/8043bad52d95a84b.jpg\",\"latitude\":36.0085981064,\"longitude\":126.7578853293},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"군산 내항 부잔교\",\"minutes\":50,\"note\":\"쌀이 실려 나가던 다리입니다.\",\"moveMinutes\":14,\"searchQuery\":\"군산 내항 부잔교\",\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\",\"latitude\":35.9892997236,\"longitude\":126.7174508158},{\"name\":\"째보선창\",\"minutes\":40,\"moveMinutes\":5,\"note\":\"소설 속 노동의 자리입니다.\",\"searchQuery\":\"군산 째보선창\",\"latitude\":35.9875345,\"longitude\":126.7199875},{\"name\":\"군산근대역사박물관\",\"minutes\":90,\"note\":\"항구 도시가 어떻게 만들어졌는지 먼저 봅니다.\",\"moveMinutes\":7,\"searchQuery\":\"군산근대역사박물관\",\"imageUrl\":\"/assets/mirror/3e9e574d3b987c8d.jpg\",\"latitude\":35.9908197098,\"longitude\":126.7121231556}]},{\"label\":\"셋째 날\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"초원사진관\",\"minutes\":30,\"note\":\"숙소에서 걸어 3분입니다.\",\"moveMinutes\":7,\"searchQuery\":\"초원사진관\",\"imageUrl\":\"/assets/mirror/d5433fe48de3da05.jpg\",\"latitude\":35.9877369536,\"longitude\":126.7083826065},{\"name\":\"이성당\",\"minutes\":40,\"note\":\"단팥빵을 사서 마당에서 먹어도 됩니다.\",\"moveMinutes\":9,\"searchQuery\":\"군산 이성당\",\"latitude\":35.9870443303,\"longitude\":126.7111681428}]}]},{\"name\":\"철길마을과 항구를 같이 보는 2박 3일\",\"duration\":\"2박 3일\",\"audience\":\"사진을 찍는 손님\",\"why\":\"철길과 부잔교, 배경이 다른 두 자리를 하루씩 씁니다.\",\"days\":[{\"label\":\"첫째 날\",\"startTime\":\"16:00\",\"stops\":[{\"name\":\"체크인 · 스테이 머뭄\",\"minutes\":40,\"note\":\"짐을 풀고 골목을 한 바퀴 돕니다.\",\"imageUrl\":\"/assets/mirror/ddc9f804f4ef83b5.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254},{\"name\":\"저녁 · 빈해원\",\"minutes\":70,\"note\":\"옛 중식당 건물 자체가 문화재입니다.\",\"searchQuery\":\"군산 빈해원\",\"imageUrl\":\"/assets/mirror/2af9caa23a08a3ec.jpg\",\"latitude\":35.9888841406,\"longitude\":126.7139765207,\"moveMinutes\":10},{\"name\":\"밤 산책 · 군산 내항\",\"minutes\":50,\"note\":\"조명이 들어온 부잔교를 봅니다.\",\"searchQuery\":\"군산 내항\",\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\",\"latitude\":35.9892997236,\"longitude\":126.7174508158,\"moveMinutes\":6},{\"name\":\"머뭄으로 · 스테이 머뭄\",\"minutes\":60,\"note\":\"마당 평상에 앉으면 항구 소리가 들립니다.\",\"imageUrl\":\"/assets/mirror/aa69a633f8e3a631.jpg\",\"latitude\":35.9864175,\"longitude\":126.7061254}]},{\"label\":\"둘째 날\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"경암동 철길마을\",\"minutes\":60,\"moveMinutes\":18,\"note\":\"해 질 무렵 빛이 가장 좋습니다.\",\"searchQuery\":\"군산 경암동 철길마을\",\"imageUrl\":\"/assets/mirror/6d59722cedddd566.jpg\",\"latitude\":35.9813520474,\"longitude\":126.7362330582},{\"name\":\"이성당\",\"minutes\":40,\"note\":\"단팥빵을 사서 마당에서 먹어도 됩니다.\",\"moveMinutes\":14,\"searchQuery\":\"군산 이성당\",\"latitude\":35.9870443303,\"longitude\":126.7111681428}]},{\"label\":\"셋째 날\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"군산 내항 부잔교\",\"minutes\":50,\"note\":\"쌀이 실려 나가던 다리입니다.\",\"moveMinutes\":11,\"searchQuery\":\"군산 내항 부잔교\",\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\",\"latitude\":35.9892997236,\"longitude\":126.7174508158},{\"name\":\"구 군산세관 본관\",\"minutes\":40,\"note\":\"소설의 세관이 그대로 서 있습니다.\",\"moveMinutes\":5,\"searchQuery\":\"구 군산세관\",\"imageUrl\":\"/assets/mirror/4d71067dc7e3f671.jpg\",\"latitude\":35.9915087952,\"longitude\":126.7113016917}]}]}]}"},{"id":"story","name":"군산 이야기","enabled":true,"locked":false},{"id":"songs","name":"가요 다방","enabled":false,"locked":false,"data":"{\"kind\":\"songs\",\"version\":1,\"items\":[{\"title\":\"금강 나그네\",\"artist\":\"이미자\",\"lyricist\":\"이병훈\",\"composer\":\"박춘석\",\"year\":1966,\"label\":\"지구레코드\",\"labelColor\":\"#d4551f\",\"story\":\"군산항을 모티브로 한 곡. 금강 하구와 항구의 이별 정서를 담았습니다.\",\"connection\":\"머뭄에서 걸어 10분, 내항 부잔교가 이 노래의 배경입니다.\",\"verified\":\"확인\",\"source\":{\"name\":\"세계일보\",\"url\":\"https://www.segye.com/newsView/20160329000709\"}},{\"title\":\"님 떠난 군산항\",\"artist\":\"이미자\",\"labelColor\":\"#1d3c74\",\"story\":\"군산항의 이별 정서를 담은 곡. 발표 연도와 작사·작곡은 아직 확인 중입니다.\",\"verified\":\"확인필요\",\"source\":{\"name\":\"세계일보\",\"url\":\"https://www.segye.com/newsView/20160329000709\"}},{\"title\":\"헤어진 군산항\",\"artist\":\"박제연\",\"labelColor\":\"#2a6053\",\"story\":\"1960년대 군산을 노래한 곡으로 전해집니다. 항구도시의 정서를 담았습니다.\",\"verified\":\"확인필요\",\"source\":{\"name\":\"세계일보\",\"url\":\"https://www.segye.com/newsView/20160329000709\"}},{\"title\":\"잘 있거라 군산항\",\"artist\":\"안다성\",\"lyricist\":\"손석우\",\"composer\":\"손석우\",\"year\":1963,\"labelColor\":\"#2f6b4f\",\"story\":\"“월명산 고갯길을 팔에 팔 끼고” 로 시작하는 이별가입니다. 군산항을 두고 떠나는 노래입니다.\",\"connection\":\"머뭄 뒤편 월명산 고갯길이 이 노래의 첫 소절 배경입니다.\",\"verified\":\"확인\",\"source\":{\"name\":\"매거진군산\",\"url\":\"http://maggun.com/article/read.php?mode=read\u0026idx=745\u0026page=3\"}}],\"title\":\"가요 다방\",\"subtitle\":\"이 도시를 노래한 곡들\"}"},{"id":"people","name":"인물 열전","enabled":false,"locked":false,"data":"{\"kind\":\"people\",\"version\":1,\"items\":[{\"name\":\"채만식\",\"aka\":\"백릉\",\"years\":\"1902–1950\",\"role\":\"소설가\",\"oneLine\":\"군산을 무대로 근대의 뒷면을 풍자로 그려낸 작가입니다.\",\"imageQuery\":\"채만식문학관 채만식 초상\",\"verified\":\"확인\",\"source\":{\"name\":\"군산시 채만식문학관\",\"url\":\"https://www.gunsan.go.kr/chae/\"},\"imageUrl\":\"/assets/mirror/d902ef61234bd897.png\"},{\"name\":\"고은\",\"years\":\"1933–\",\"role\":\"시인\",\"oneLine\":\"군산에서 태어난 시인으로 소개됩니다. 지역 문학을 이야기할 때 함께 불립니다.\",\"verified\":\"확인필요\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/566bc326e8a58042.jpg\"},{\"name\":\"박화요비\",\"role\":\"가수\",\"oneLine\":\"군산 출신으로 소개되는 가수입니다.\",\"verified\":\"확인필요\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/3a780a4a34df9289.jpg\"},{\"name\":\"진희경\",\"role\":\"배우\",\"oneLine\":\"군산 출신으로 알려진 배우입니다.\",\"verified\":\"확인필요\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/3dc384187a900e95.png\"},{\"name\":\"김수미\",\"years\":\"1949–2024\",\"role\":\"배우\",\"oneLine\":\"신흥동 말랭이마을에서 태어났습니다. 군산시가 그 골목에 ‘김수미 길’ 을 냈습니다.\",\"verified\":\"확인\",\"source\":{\"name\":\"한국어 위키백과\",\"url\":\"https://ko.wikipedia.org/wiki/김수미\"},\"imageUrl\":\"/assets/mirror/1f6dbdd78a3da2de.png\"},{\"name\":\"김선기\",\"years\":\"1907–1992\",\"role\":\"한글학자\",\"oneLine\":\"옥구 임피면에서 태어났습니다. 조선어학회 사전 편찬원으로 일하다 조선어학회 사건으로 옥고를 치렀습니다.\",\"verified\":\"확인\",\"source\":{\"name\":\"한국어 위키백과\",\"url\":\"https://ko.wikipedia.org/wiki/김선기_(언어철학자)\"},\"imageUrl\":\"/assets/mirror/9b27266ddf8b8b70.jpg\"},{\"name\":\"송새벽\",\"years\":\"1979–\",\"role\":\"배우\",\"oneLine\":\"군산에서 자라 군산동고와 군산대 철학과를 나왔습니다.\",\"verified\":\"확인\",\"source\":{\"name\":\"한국어 위키백과\",\"url\":\"https://ko.wikipedia.org/wiki/송새벽\"},\"imageUrl\":\"/assets/mirror/a2344640498c5c86.png\"},{\"name\":\"차우찬\",\"years\":\"1987–\",\"role\":\"프로야구 투수\",\"oneLine\":\"군산초·군산남중을 거쳐 군산상고를 졸업한 좌완 투수입니다.\",\"verified\":\"확인\",\"source\":{\"name\":\"한국어 위키백과\",\"url\":\"https://ko.wikipedia.org/wiki/차우찬\"},\"imageUrl\":\"/assets/mirror/6a15360430298700.jpg\"}],\"title\":\"인물 열전\",\"subtitle\":\"이 도시가 배출한 이름들\"}"},{"id":"chronicle","name":"시간의 골목","enabled":false,"locked":false,"data":"{\"kind\":\"chronicle\",\"version\":1,\"items\":[{\"year\":1380,\"title\":\"진포대첩\",\"summary\":\"최무선의 화포가 왜구 함대를 무너뜨린 해. 군산 해양사의 출발점으로 꼽힙니다.\",\"place\":\"진포시비공원\",\"turning\":true,\"verified\":\"확인필요\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/9b073d4a1b5fb62c.jpg\"},{\"year\":1899,\"title\":\"군산 개항\",\"summary\":\"항구가 열리며 도시의 성격이 한 번에 바뀝니다.\",\"place\":\"군산 내항\",\"turning\":true,\"verified\":\"확인\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\"},{\"year\":1908,\"title\":\"구 군산세관 본관 준공\",\"summary\":\"지금도 서 있는 벽돌 건물입니다. 개항기 행정의 자리였습니다.\",\"place\":\"구 군산세관 본관\",\"verified\":\"확인\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/22af0bff45e5fce8.jpg\"},{\"year\":1926,\"title\":\"내항 부잔교 축조\",\"summary\":\"조수 차가 큰 항구에서 배를 대기 위한 뜬다리입니다. 미곡 반출의 통로였습니다.\",\"place\":\"군산 내항 부잔교\",\"turning\":true,\"verified\":\"확인필요\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\"},{\"year\":1937,\"title\":\"『탁류』 연재 시작\",\"summary\":\"채만식이 이 도시를 소설의 무대로 세웁니다.\",\"place\":\"채만식문학관\",\"verified\":\"확인\",\"source\":{\"name\":\"군산시 채만식문학관\",\"url\":\"https://www.gunsan.go.kr/chae/\"},\"imageUrl\":\"/assets/mirror/8043bad52d95a84b.jpg\"},{\"year\":1990,\"title\":\"금강하굿둑 준공\",\"summary\":\"강과 바다의 경계가 사람 손으로 그어집니다.\",\"place\":\"금강하굿둑\",\"verified\":\"확인필요\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/aaf32d2c8e5faffb.jpg\"},{\"year\":2010,\"title\":\"새만금 방조제 개통\",\"summary\":\"간척으로 해안선이 다시 그려집니다.\",\"place\":\"새만금 방조제\",\"turning\":true,\"verified\":\"확인필요\",\"source\":{\"name\":\"대한민국 구석구석\",\"url\":\"https://korean.visitkorea.or.kr/\"},\"imageUrl\":\"/assets/mirror/487643cdcfd19fdb.jpg\"}],\"title\":\"시간의 골목\",\"subtitle\":\"이 도시를 만든 해들\"}"},{"id":"postcard","name":"오늘의 엽서","enabled":false,"locked":false,"data":"{\"kind\":\"postcard\",\"version\":1,\"items\":[{\"line\":\"부잔교 위에서는 물이 어디까지 올라왔었는지가 다리에 새겨져 있다.\",\"hashtags\":[\"#군산\",\"#내항\",\"#부잔교\"],\"place\":\"군산 내항\",\"postmark\":\"군산 內港\",\"verified\":\"확인\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\"},{\"line\":\"짬뽕 한 그릇이 항구도시의 이력서인 줄은 몰랐다.\",\"hashtags\":[\"#군산짬뽕\",\"#항구도시\"],\"place\":\"군산 중앙로\",\"postmark\":\"군산 中央\",\"verified\":\"확인필요\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"imageUrl\":\"/assets/mirror/149c18951bdcaff8.jpg\"},{\"line\":\"단팥빵을 사러 줄을 서는 일이 여행이 되는 도시.\",\"hashtags\":[\"#이성당\",\"#오래된가게\"],\"place\":\"이성당\",\"postmark\":\"군산 中央\",\"verified\":\"확인\",\"source\":{\"name\":\"대한민국 구석구석\",\"url\":\"https://korean.visitkorea.or.kr/\"},\"imageUrl\":\"/assets/mirror/37c90494d28478e2.png\"},{\"line\":\"섬과 섬 사이를 다리로 건너면, 바다가 길이 된다.\",\"hashtags\":[\"#고군산군도\",\"#선유도\"],\"place\":\"고군산군도\",\"postmark\":\"고군산\",\"verified\":\"확인필요\",\"source\":{\"name\":\"대한민국 구석구석\",\"url\":\"https://korean.visitkorea.or.kr/\"},\"imageUrl\":\"/assets/mirror/6ded14f21a17894b.jpg\"}],\"title\":\"오늘의 엽서\",\"subtitle\":\"그대로 붙여 쓰는 한 문장\"}"},{"id":"quiz","name":"뒤집어 보는 질문","enabled":false,"locked":false,"data":"{\"kind\": \"quiz\", \"version\": 1, \"items\": [{\"question\": \"부잔교는 왜 물 위에 뜨도록 만들었을까?\", \"hint\": \"서해는 조수 간만의 차가 큽니다. 고정된 부두라면 하루에 두 번 배가 닿지 못합니다.\", \"topic\": \"군산 내항\", \"level\": \"초등\", \"verified\": \"확인\", \"source\": {\"name\": \"군산시 문화관광\", \"url\": \"https://www.gunsan.go.kr/tour/\"}}, {\"question\": \"『탁류』의 “탁류”는 무엇을 가리키는 말일까?\", \"hint\": \"금강 하류의 흙탕물이자, 소설 속 인물들이 휩쓸려 가는 시대를 함께 가리킵니다.\", \"topic\": \"채만식문학관\", \"level\": \"중등\", \"verified\": \"확인\", \"source\": {\"name\": \"군산시 채만식문학관\", \"url\": \"https://www.gunsan.go.kr/chae/\"}}, {\"question\": \"해망굴은 무엇을 위해 뚫린 굴일까?\", \"hint\": \"근대 교통로였고, 전쟁기에는 다른 쓰임도 있었습니다. 굴 하나에 두 시대가 겹칩니다.\", \"topic\": \"해망굴\", \"level\": \"어른\", \"verified\": \"확인필요\", \"source\": {\"name\": \"군산시 문화관광\", \"url\": \"https://www.gunsan.go.kr/tour/\"}}, {\"question\": \"고군산군도의 ‘고(古)’ 는 왜 붙었을까?\", \"hint\": \"지금의 군산이 자리 잡기 전, 이 이름이 먼저 있던 곳이 있습니다.\", \"topic\": \"고군산군도\", \"level\": \"어른\", \"verified\": \"확인필요\", \"source\": {\"name\": \"대한민국 구석구석\", \"url\": \"https://korean.visitkorea.or.kr/\"}}], \"title\": \"뒤집어 보는 질문\", \"subtitle\": \"뒤집으면 힌트가 나옵니다\"}"},{"id":"faq","name":"자주 묻는 질문","enabled":true,"locked":false},{"id":"rules","name":"이용 규정","enabled":true,"locked":false},{"id":"weather","name":"날씨","enabled":true,"locked":false},{"id":"planner","name":"계절별 추천 하루","enabled":false,"locked":false,"data":"{\"kind\":\"planner\",\"version\":1,\"title\":\"계절별 추천 하루\",\"subtitle\":\"지금 계절에 맞는 코스만 보여 드립니다\",\"items\":[{\"name\":\"탁류길 코스\",\"audience\":\"걷기 좋아하는 손님\",\"why\":\"소설 『탁류』가 지나간 자리를 그대로 따라 걷습니다.\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"채만식문학관\",\"minutes\":12,\"note\":\"금강 하구 옆. 소설의 배경을 먼저 눈에 담습니다.\",\"searchQuery\":\"군산 채만식문학관\",\"imageUrl\":\"/assets/mirror/8043bad52d95a84b.jpg\",\"latitude\":36.0085981064,\"longitude\":126.7578853293},{\"name\":\"군산 내항 부잔교\",\"minutes\":9,\"note\":\"조수에 오르내리던 뜬다리. 이 코스의 중심입니다.\",\"searchQuery\":\"군산 내항 부잔교\",\"imageUrl\":\"/assets/mirror/1f35ef26139c6cb9.jpg\",\"latitude\":35.9892997236,\"longitude\":126.7174508158},{\"name\":\"째보선창\",\"minutes\":7,\"note\":\"항구 노동의 기억이 남은 선창입니다.\",\"searchQuery\":\"군산 째보선창\",\"latitude\":35.9875345,\"longitude\":126.7199875},{\"name\":\"초원사진관\",\"minutes\":11,\"note\":\"영화가 남기고 간 자리입니다.\",\"searchQuery\":\"군산 초원사진관\",\"imageUrl\":\"/assets/mirror/d5433fe48de3da05.jpg\",\"latitude\":35.9877369536,\"longitude\":126.7083826065},{\"name\":\"이성당\",\"minutes\":6,\"note\":\"코스의 끝. 단팥빵 하나로 반나절을 닫습니다.\",\"searchQuery\":\"군산 이성당\",\"imageUrl\":\"/assets/mirror/37c90494d28478e2.png\",\"latitude\":35.9870443303,\"longitude\":126.7111681428}],\"verified\":\"확인\",\"source\":{\"name\":\"군산 스탬프투어\",\"url\":\"https://www.gunsanstamp.kr/\"},\"season\":\"가을\",\"rank\":2},{\"name\":\"아리랑길 코스\",\"audience\":\"걷기 좋아하는 손님\",\"why\":\"개항기 건물만 골라 짧게 도는 길입니다.\",\"startTime\":\"10:00\",\"stops\":[{\"name\":\"구 군산세관 본관\",\"minutes\":8,\"note\":\"1908년에 지어진 벽돌 건물입니다.\",\"searchQuery\":\"구 군산세관 본관\",\"imageUrl\":\"/assets/mirror/22af0bff45e5fce8.jpg\",\"latitude\":35.9915087952,\"longitude\":126.7113016917},{\"name\":\"군산근대미술관\",\"minutes\":5,\"note\":\"옛 은행 금고가 그대로 남아 있습니다.\",\"searchQuery\":\"군산근대미술관\",\"imageUrl\":\"/assets/mirror/d8764592a03b0103.jpg\",\"latitude\":35.9899367776,\"longitude\":126.7127710457},{\"name\":\"해망굴\",\"minutes\":14,\"note\":\"굴 하나에 두 시대가 겹칩니다.\",\"searchQuery\":\"군산 해망굴\",\"imageUrl\":\"/assets/mirror/0a1e0ff50ca7ee37.jpg\",\"latitude\":35.9913180812,\"longitude\":126.7048496204}],\"verified\":\"확인필요\",\"source\":{\"name\":\"군산 스탬프투어\",\"url\":\"https://www.gunsanstamp.kr/\"},\"season\":\"봄\",\"rank\":1},{\"name\":\"개항장 골목 한 바퀴\",\"audience\":\"처음 온 손님\",\"why\":\"해가 낮아지는 계절이라 골목 그림자가 길어집니다. 걷는 거리가 가장 짧은 코스이기도 합니다.\",\"startTime\":\"09:30\",\"stops\":[{\"name\":\"군산근대역사박물관\",\"minutes\":90,\"moveMinutes\":12,\"note\":\"항구 도시가 어떻게 만들어졌는지 먼저 봅니다.\",\"searchQuery\":\"군산근대역사박물관\",\"imageUrl\":\"/assets/mirror/af673cc75c0ed696.jpg\",\"latitude\":35.9908197098,\"longitude\":126.7121231556},{\"name\":\"초원사진관\",\"minutes\":30,\"moveMinutes\":10,\"note\":\"영화가 남기고 간 자리입니다.\",\"searchQuery\":\"군산 초원사진관\",\"imageUrl\":\"/assets/mirror/d5433fe48de3da05.jpg\",\"latitude\":35.9877369536,\"longitude\":126.7083826065},{\"name\":\"이성당\",\"minutes\":40,\"moveMinutes\":8,\"note\":\"단팥빵 하나로 오후를 엽니다.\",\"searchQuery\":\"군산 이성당\",\"imageUrl\":\"/assets/mirror/37c90494d28478e2.png\",\"latitude\":35.9870443303,\"longitude\":126.7111681428},{\"name\":\"경암동 철길마을\",\"minutes\":60,\"moveMinutes\":18,\"note\":\"해 질 무렵 빛이 가장 좋습니다.\",\"searchQuery\":\"군산 경암동 철길마을\",\"imageUrl\":\"/assets/mirror/6d59722cedddd566.jpg\",\"latitude\":35.9813520474,\"longitude\":126.7362330582}],\"verified\":\"확인\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"season\":\"가을\",\"rank\":1},{\"name\":\"비 오면 여기로\",\"audience\":\"장마철에 온 손님\",\"why\":\"실내로만 이어지는 코스입니다. 우산을 접었다 폈다 하지 않아도 됩니다.\",\"season\":\"여름\",\"startTime\":\"10:30\",\"stops\":[{\"name\":\"군산근대건축관\",\"minutes\":60,\"moveMinutes\":10,\"note\":\"옛 은행 건물 안이 통째로 전시입니다.\",\"searchQuery\":\"군산근대건축관\",\"imageUrl\":\"/assets/mirror/5000f0a4feb45788.jpg\",\"latitude\":35.9894201935,\"longitude\":126.7141598237},{\"name\":\"한일옥\",\"minutes\":60,\"moveMinutes\":8,\"note\":\"무국 한 그릇으로 몸을 데웁니다.\",\"searchQuery\":\"군산 한일옥\",\"latitude\":35.9874081251,\"longitude\":126.7082350808},{\"name\":\"째보선창 근처 카페\",\"minutes\":90,\"moveMinutes\":10,\"note\":\"비 그칠 때까지 앉아 있기 좋습니다.\",\"searchQuery\":\"군산 째보선창 카페\",\"latitude\":35.9875345,\"longitude\":126.7199875}],\"verified\":\"확인필요\",\"source\":{\"name\":\"군산시 문화관광\",\"url\":\"https://www.gunsan.go.kr/tour/\"},\"rank\":1}]}"}],"look":{"radius":"0px","shadow":"4px 4px 0 rgb(27 26 21 / 0.16)","texture":"repeating-linear-gradient(0deg,rgba(27,26,21,.028) 0 1px,transparent 1px 3px),repeating-linear-gradient(90deg,rgba(27,26,21,.02) 0 1px,transparent 1px 4px)","fontBody":"'Gowun Batang', 'Noto Serif KR', serif","borderWidth":"2px","fontHeading":"'Gugi', 'Noto Sans KR', sans-serif","sectionSpace":"4rem","headingWeight":"400","headingTracking":"0em"}}} diff --git a/solution/site/scripts/mockup/vendor/index-DUeGvBPS.js b/solution/site/scripts/mockup/vendor/index-DUeGvBPS.js new file mode 100644 index 0000000..9fa7281 --- /dev/null +++ b/solution/site/scripts/mockup/vendor/index-DUeGvBPS.js @@ -0,0 +1,298 @@ +(function(){const r=document.createElement("link").relList;if(r&&r.supports&&r.supports("modulepreload"))return;for(const f of document.querySelectorAll('link[rel="modulepreload"]'))o(f);new MutationObserver(f=>{for(const m of f)if(m.type==="childList")for(const h of m.addedNodes)h.tagName==="LINK"&&h.rel==="modulepreload"&&o(h)}).observe(document,{childList:!0,subtree:!0});function u(f){const m={};return f.integrity&&(m.integrity=f.integrity),f.referrerPolicy&&(m.referrerPolicy=f.referrerPolicy),f.crossOrigin==="use-credentials"?m.credentials="include":f.crossOrigin==="anonymous"?m.credentials="omit":m.credentials="same-origin",m}function o(f){if(f.ep)return;f.ep=!0;const m=u(f);fetch(f.href,m)}})();const Xg="modulepreload",$g=function(i){return"/"+i},hh={},Vg=function(r,u,o){let f=Promise.resolve();if(u&&u.length>0){let h=function(p){return Promise.all(p.map(b=>Promise.resolve(b).then(y=>({status:"fulfilled",value:y}),y=>({status:"rejected",reason:y}))))};document.getElementsByTagName("link");const x=document.querySelector("meta[property=csp-nonce]"),v=(x==null?void 0:x.nonce)||(x==null?void 0:x.getAttribute("nonce"));f=h(u.map(p=>{if(p=$g(p),p in hh)return;hh[p]=!0;const b=p.endsWith(".css"),y=b?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${p}"]${y}`))return;const E=document.createElement("link");if(E.rel=b?"stylesheet":Xg,b||(E.as="script"),E.crossOrigin="",E.href=p,v&&E.setAttribute("nonce",v),document.head.appendChild(E),b)return new Promise((T,N)=>{E.addEventListener("load",T),E.addEventListener("error",()=>N(new Error(`Unable to preload CSS for ${p}`)))})}))}function m(h){const x=new Event("vite:preloadError",{cancelable:!0});if(x.payload=h,window.dispatchEvent(x),!x.defaultPrevented)throw h}return f.then(h=>{for(const x of h||[])x.status==="rejected"&&m(x.reason);return r().catch(m)})};var Ou={exports:{}},Ns={};/** + * @license React + * react-jsx-runtime.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var ph;function Zg(){if(ph)return Ns;ph=1;var i=Symbol.for("react.transitional.element"),r=Symbol.for("react.fragment");function u(o,f,m){var h=null;if(m!==void 0&&(h=""+m),f.key!==void 0&&(h=""+f.key),"key"in f){m={};for(var x in f)x!=="key"&&(m[x]=f[x])}else m=f;return f=m.ref,{$$typeof:i,type:o,key:h,ref:f!==void 0?f:null,props:m}}return Ns.Fragment=r,Ns.jsx=u,Ns.jsxs=u,Ns}var xh;function Kg(){return xh||(xh=1,Ou.exports=Zg()),Ou.exports}var n=Kg(),Du={exports:{}},ce={};/** + * @license React + * react.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var gh;function Jg(){if(gh)return ce;gh=1;var i=Symbol.for("react.transitional.element"),r=Symbol.for("react.portal"),u=Symbol.for("react.fragment"),o=Symbol.for("react.strict_mode"),f=Symbol.for("react.profiler"),m=Symbol.for("react.consumer"),h=Symbol.for("react.context"),x=Symbol.for("react.forward_ref"),v=Symbol.for("react.suspense"),p=Symbol.for("react.memo"),b=Symbol.for("react.lazy"),y=Symbol.for("react.activity"),E=Symbol.iterator;function T(S){return S===null||typeof S!="object"?null:(S=E&&S[E]||S["@@iterator"],typeof S=="function"?S:null)}var N={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},_=Object.assign,U={};function q(S,H,G){this.props=S,this.context=H,this.refs=U,this.updater=G||N}q.prototype.isReactComponent={},q.prototype.setState=function(S,H){if(typeof S!="object"&&typeof S!="function"&&S!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,S,H,"setState")},q.prototype.forceUpdate=function(S){this.updater.enqueueForceUpdate(this,S,"forceUpdate")};function Q(){}Q.prototype=q.prototype;function V(S,H,G){this.props=S,this.context=H,this.refs=U,this.updater=G||N}var ne=V.prototype=new Q;ne.constructor=V,_(ne,q.prototype),ne.isPureReactComponent=!0;var de=Array.isArray;function le(){}var X={H:null,A:null,T:null,S:null},ee=Object.prototype.hasOwnProperty;function se(S,H,G){var $=G.ref;return{$$typeof:i,type:S,key:H,ref:$!==void 0?$:null,props:G}}function Ne(S,H){return se(S.type,H,S.props)}function oe(S){return typeof S=="object"&&S!==null&&S.$$typeof===i}function ye(S){var H={"=":"=0",":":"=2"};return"$"+S.replace(/[=:]/g,function(G){return H[G]})}var Ye=/\/+/g;function Qe(S,H){return typeof S=="object"&&S!==null&&S.key!=null?ye(""+S.key):H.toString(36)}function te(S){switch(S.status){case"fulfilled":return S.value;case"rejected":throw S.reason;default:switch(typeof S.status=="string"?S.then(le,le):(S.status="pending",S.then(function(H){S.status==="pending"&&(S.status="fulfilled",S.value=H)},function(H){S.status==="pending"&&(S.status="rejected",S.reason=H)})),S.status){case"fulfilled":return S.value;case"rejected":throw S.reason}}throw S}function O(S,H,G,$,ae){var re=typeof S;(re==="undefined"||re==="boolean")&&(S=null);var ie=!1;if(S===null)ie=!0;else switch(re){case"bigint":case"string":case"number":ie=!0;break;case"object":switch(S.$$typeof){case i:case r:ie=!0;break;case b:return ie=S._init,O(ie(S._payload),H,G,$,ae)}}if(ie)return ae=ae(S),ie=$===""?"."+Qe(S,0):$,de(ae)?(G="",ie!=null&&(G=ie.replace(Ye,"$&/")+"/"),O(ae,H,G,"",function(nt){return nt})):ae!=null&&(oe(ae)&&(ae=Ne(ae,G+(ae.key==null||S&&S.key===ae.key?"":(""+ae.key).replace(Ye,"$&/")+"/")+ie)),H.push(ae)),1;ie=0;var Ee=$===""?".":$+":";if(de(S))for(var Ae=0;Ae>>1,je=O[me];if(0>>1;mef(G,W))$f(ae,G)?(O[me]=ae,O[$]=W,me=$):(O[me]=G,O[H]=W,me=H);else if($f(ae,W))O[me]=ae,O[$]=W,me=$;else break e}}return B}function f(O,B){var W=O.sortIndex-B.sortIndex;return W!==0?W:O.id-B.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var m=performance;i.unstable_now=function(){return m.now()}}else{var h=Date,x=h.now();i.unstable_now=function(){return h.now()-x}}var v=[],p=[],b=1,y=null,E=3,T=!1,N=!1,_=!1,U=!1,q=typeof setTimeout=="function"?setTimeout:null,Q=typeof clearTimeout=="function"?clearTimeout:null,V=typeof setImmediate<"u"?setImmediate:null;function ne(O){for(var B=u(p);B!==null;){if(B.callback===null)o(p);else if(B.startTime<=O)o(p),B.sortIndex=B.expirationTime,r(v,B);else break;B=u(p)}}function de(O){if(_=!1,ne(O),!N)if(u(v)!==null)N=!0,le||(le=!0,ye());else{var B=u(p);B!==null&&te(de,B.startTime-O)}}var le=!1,X=-1,ee=5,se=-1;function Ne(){return U?!0:!(i.unstable_now()-seO&&Ne());){var me=y.callback;if(typeof me=="function"){y.callback=null,E=y.priorityLevel;var je=me(y.expirationTime<=O);if(O=i.unstable_now(),typeof je=="function"){y.callback=je,ne(O),B=!0;break t}y===u(v)&&o(v),ne(O)}else o(v);y=u(v)}if(y!==null)B=!0;else{var S=u(p);S!==null&&te(de,S.startTime-O),B=!1}}break e}finally{y=null,E=W,T=!1}B=void 0}}finally{B?ye():le=!1}}}var ye;if(typeof V=="function")ye=function(){V(oe)};else if(typeof MessageChannel<"u"){var Ye=new MessageChannel,Qe=Ye.port2;Ye.port1.onmessage=oe,ye=function(){Qe.postMessage(null)}}else ye=function(){q(oe,0)};function te(O,B){X=q(function(){O(i.unstable_now())},B)}i.unstable_IdlePriority=5,i.unstable_ImmediatePriority=1,i.unstable_LowPriority=4,i.unstable_NormalPriority=3,i.unstable_Profiling=null,i.unstable_UserBlockingPriority=2,i.unstable_cancelCallback=function(O){O.callback=null},i.unstable_forceFrameRate=function(O){0>O||125me?(O.sortIndex=W,r(p,O),u(v)===null&&O===u(p)&&(_?(Q(X),X=-1):_=!0,te(de,W-me))):(O.sortIndex=je,r(v,O),N||T||(N=!0,le||(le=!0,ye()))),O},i.unstable_shouldYield=Ne,i.unstable_wrapCallback=function(O){var B=E;return function(){var W=E;E=B;try{return O.apply(this,arguments)}finally{E=W}}}})(Ru)),Ru}var yh;function Fg(){return yh||(yh=1,Uu.exports=Wg()),Uu.exports}var Hu={exports:{}},ht={};/** + * @license React + * react-dom.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var jh;function Ig(){if(jh)return ht;jh=1;var i=Wu();function r(v){var p="https://react.dev/errors/"+v;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(r){console.error(r)}}return i(),Hu.exports=Ig(),Hu.exports}/** + * @license React + * react-dom-client.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Sh;function ev(){if(Sh)return Ss;Sh=1;var i=Fg(),r=Wu(),u=Pg();function o(e){var t="https://react.dev/errors/"+e;if(1je||(e.current=me[je],me[je]=null,je--)}function G(e,t){je++,me[je]=e.current,e.current=t}var $=S(null),ae=S(null),re=S(null),ie=S(null);function Ee(e,t){switch(G(re,t),G(ae,e),G($,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Hm(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Hm(t),e=Lm(t,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}H($),G($,e)}function Ae(){H($),H(ae),H(re)}function nt(e){e.memoizedState!==null&&G(ie,e);var t=$.current,l=Lm(t,e.type);t!==l&&(G(ae,e),G($,l))}function Me(e){ae.current===e&&(H($),H(ae)),ie.current===e&&(H(ie),vs._currentValue=W)}var J,_e;function Oe(e){if(J===void 0)try{throw Error()}catch(l){var t=l.stack.trim().match(/\n( *(at )?)/);J=t&&t[1]||"",_e=-1)":-1s||j[a]!==C[s]){var k=` +`+j[a].replace(" at new "," at ");return e.displayName&&k.includes("")&&(k=k.replace("",e.displayName)),k}while(1<=a&&0<=s);break}}}finally{tt=!1,Error.prepareStackTrace=l}return(l=e?e.displayName||e.name:"")?Oe(l):""}function Wt(e,t){switch(e.tag){case 26:case 27:case 5:return Oe(e.type);case 16:return Oe("Lazy");case 13:return e.child!==t&&t!==null?Oe("Suspense Fallback"):Oe("Suspense");case 19:return Oe("SuspenseList");case 0:case 15:return bt(e.type,!1);case 11:return bt(e.type.render,!1);case 1:return bt(e.type,!0);case 31:return Oe("Activity");default:return""}}function nl(e){try{var t="",l=null;do t+=Wt(e,l),l=e,e=e.return;while(e);return t}catch(a){return` +Error generating stack: `+a.message+` +`+a.stack}}var kt=Object.prototype.hasOwnProperty,Ft=i.unstable_scheduleCallback,hl=i.unstable_cancelCallback,An=i.unstable_shouldYield,Tn=i.unstable_requestPaint,mt=i.unstable_now,Ys=i.unstable_getCurrentPriorityLevel,Gs=i.unstable_ImmediatePriority,_n=i.unstable_UserBlockingPriority,ka=i.unstable_NormalPriority,wn=i.unstable_LowPriority,pl=i.unstable_IdlePriority,Cn=i.log,Mn=i.unstable_setDisableYieldValue,sl=null,xt=null;function il(e){if(typeof Cn=="function"&&Mn(e),xt&&typeof xt.setStrictMode=="function")try{xt.setStrictMode(sl,e)}catch{}}var gt=Math.clz32?Math.clz32:Qs,vc=Math.log,bc=Math.LN2;function Qs(e){return e>>>=0,e===0?32:31-(vc(e)/bc|0)|0}var Ua=256,ua=262144,oa=4194304;function cl(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Ra(e,t,l){var a=e.pendingLanes;if(a===0)return 0;var s=0,c=e.suspendedLanes,d=e.pingedLanes;e=e.warmLanes;var g=a&134217727;return g!==0?(a=g&~c,a!==0?s=cl(a):(d&=g,d!==0?s=cl(d):l||(l=g&~e,l!==0&&(s=cl(l))))):(g=a&~c,g!==0?s=cl(g):d!==0?s=cl(d):l||(l=a&~e,l!==0&&(s=cl(l)))),s===0?0:t!==0&&t!==s&&(t&c)===0&&(c=s&-s,l=t&-t,c>=l||c===32&&(l&4194048)!==0)?t:s}function Ll(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Xs(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function $s(){var e=oa;return oa<<=1,(oa&62914560)===0&&(oa=4194304),e}function yc(e){for(var t=[],l=0;31>l;l++)t.push(e);return t}function On(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Op(e,t,l,a,s,c){var d=e.pendingLanes;e.pendingLanes=l,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=l,e.entangledLanes&=l,e.errorRecoveryDisabledLanes&=l,e.shellSuspendCounter=0;var g=e.entanglements,j=e.expirationTimes,C=e.hiddenUpdates;for(l=d&~l;0"u")return null;try{return e.activeElement||e.body}catch{return e.body}}var Lp=/[\n"\\]/g;function Rt(e){return e.replace(Lp,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function Ac(e,t,l,a,s,c,d,g){e.name="",d!=null&&typeof d!="function"&&typeof d!="symbol"&&typeof d!="boolean"?e.type=d:e.removeAttribute("type"),t!=null?d==="number"?(t===0&&e.value===""||e.value!=t)&&(e.value=""+Ut(t)):e.value!==""+Ut(t)&&(e.value=""+Ut(t)):d!=="submit"&&d!=="reset"||e.removeAttribute("value"),t!=null?Tc(e,d,Ut(t)):l!=null?Tc(e,d,Ut(l)):a!=null&&e.removeAttribute("value"),s==null&&c!=null&&(e.defaultChecked=!!c),s!=null&&(e.checked=s&&typeof s!="function"&&typeof s!="symbol"),g!=null&&typeof g!="function"&&typeof g!="symbol"&&typeof g!="boolean"?e.name=""+Ut(g):e.removeAttribute("name")}function Co(e,t,l,a,s,c,d,g){if(c!=null&&typeof c!="function"&&typeof c!="symbol"&&typeof c!="boolean"&&(e.type=c),t!=null||l!=null){if(!(c!=="submit"&&c!=="reset"||t!=null)){Ec(e);return}l=l!=null?""+Ut(l):"",t=t!=null?""+Ut(t):l,g||t===e.value||(e.value=t),e.defaultValue=t}a=a??s,a=typeof a!="function"&&typeof a!="symbol"&&!!a,e.checked=g?e.checked:!!a,e.defaultChecked=!!a,d!=null&&typeof d!="function"&&typeof d!="symbol"&&typeof d!="boolean"&&(e.name=d),Ec(e)}function Tc(e,t,l){t==="number"&&Ks(e.ownerDocument)===e||e.defaultValue===""+l||(e.defaultValue=""+l)}function Ga(e,t,l,a){if(e=e.options,t){t={};for(var s=0;s"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Oc=!1;if(vl)try{var Rn={};Object.defineProperty(Rn,"passive",{get:function(){Oc=!0}}),window.addEventListener("test",Rn,Rn),window.removeEventListener("test",Rn,Rn)}catch{Oc=!1}var Bl=null,Dc=null,Ws=null;function Ho(){if(Ws)return Ws;var e,t=Dc,l=t.length,a,s="value"in Bl?Bl.value:Bl.textContent,c=s.length;for(e=0;e=qn),Qo=" ",Xo=!1;function $o(e,t){switch(e){case"keyup":return dx.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Vo(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Va=!1;function hx(e,t){switch(e){case"compositionend":return Vo(t);case"keypress":return t.which!==32?null:(Xo=!0,Qo);case"textInput":return e=t.data,e===Qo&&Xo?null:e;default:return null}}function px(e,t){if(Va)return e==="compositionend"||!Lc&&$o(e,t)?(e=Ho(),Ws=Dc=Bl=null,Va=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:l,offset:t-e};e=a}e:{for(;l;){if(l.nextSibling){l=l.nextSibling;break e}l=l.parentNode}l=void 0}l=ef(l)}}function lf(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?lf(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function af(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Ks(e.document);t instanceof e.HTMLIFrameElement;){try{var l=typeof t.contentWindow.location.href=="string"}catch{l=!1}if(l)e=t.contentWindow;else break;t=Ks(e.document)}return t}function Yc(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}var Sx=vl&&"documentMode"in document&&11>=document.documentMode,Za=null,Gc=null,Qn=null,Qc=!1;function nf(e,t,l){var a=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;Qc||Za==null||Za!==Ks(a)||(a=Za,"selectionStart"in a&&Yc(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),Qn&&Gn(Qn,a)||(Qn=a,a=Qi(Gc,"onSelect"),0>=d,s-=d,rl=1<<32-gt(t)+s|l<fe?(ge=K,K=null):ge=K.sibling;var ze=M(A,K,w[fe],R);if(ze===null){K===null&&(K=ge);break}e&&K&&ze.alternate===null&&t(A,K),z=c(ze,z,fe),Se===null?I=ze:Se.sibling=ze,Se=ze,K=ge}if(fe===w.length)return l(A,K),ve&&yl(A,fe),I;if(K===null){for(;fefe?(ge=K,K=null):ge=K.sibling;var ra=M(A,K,ze.value,R);if(ra===null){K===null&&(K=ge);break}e&&K&&ra.alternate===null&&t(A,K),z=c(ra,z,fe),Se===null?I=ra:Se.sibling=ra,Se=ra,K=ge}if(ze.done)return l(A,K),ve&&yl(A,fe),I;if(K===null){for(;!ze.done;fe++,ze=w.next())ze=L(A,ze.value,R),ze!==null&&(z=c(ze,z,fe),Se===null?I=ze:Se.sibling=ze,Se=ze);return ve&&yl(A,fe),I}for(K=a(K);!ze.done;fe++,ze=w.next())ze=D(K,A,fe,ze.value,R),ze!==null&&(e&&ze.alternate!==null&&K.delete(ze.key===null?fe:ze.key),z=c(ze,z,fe),Se===null?I=ze:Se.sibling=ze,Se=ze);return e&&K.forEach(function(Qg){return t(A,Qg)}),ve&&yl(A,fe),I}function Ue(A,z,w,R){if(typeof w=="object"&&w!==null&&w.type===_&&w.key===null&&(w=w.props.children),typeof w=="object"&&w!==null){switch(w.$$typeof){case T:e:{for(var I=w.key;z!==null;){if(z.key===I){if(I=w.type,I===_){if(z.tag===7){l(A,z.sibling),R=s(z,w.props.children),R.return=A,A=R;break e}}else if(z.elementType===I||typeof I=="object"&&I!==null&&I.$$typeof===ee&&ja(I)===z.type){l(A,z.sibling),R=s(z,w.props),Jn(R,w),R.return=A,A=R;break e}l(A,z);break}else t(A,z);z=z.sibling}w.type===_?(R=xa(w.props.children,A.mode,R,w.key),R.return=A,A=R):(R=ii(w.type,w.key,w.props,null,A.mode,R),Jn(R,w),R.return=A,A=R)}return d(A);case N:e:{for(I=w.key;z!==null;){if(z.key===I)if(z.tag===4&&z.stateNode.containerInfo===w.containerInfo&&z.stateNode.implementation===w.implementation){l(A,z.sibling),R=s(z,w.children||[]),R.return=A,A=R;break e}else{l(A,z);break}else t(A,z);z=z.sibling}R=Wc(w,A.mode,R),R.return=A,A=R}return d(A);case ee:return w=ja(w),Ue(A,z,w,R)}if(te(w))return Z(A,z,w,R);if(ye(w)){if(I=ye(w),typeof I!="function")throw Error(o(150));return w=I.call(w),P(A,z,w,R)}if(typeof w.then=="function")return Ue(A,z,mi(w),R);if(w.$$typeof===V)return Ue(A,z,ui(A,w),R);hi(A,w)}return typeof w=="string"&&w!==""||typeof w=="number"||typeof w=="bigint"?(w=""+w,z!==null&&z.tag===6?(l(A,z.sibling),R=s(z,w),R.return=A,A=R):(l(A,z),R=Jc(w,A.mode,R),R.return=A,A=R),d(A)):l(A,z)}return function(A,z,w,R){try{Kn=0;var I=Ue(A,z,w,R);return nn=null,I}catch(K){if(K===an||K===fi)throw K;var Se=_t(29,K,null,A.mode);return Se.lanes=R,Se.return=A,Se}finally{}}}var Sa=_f(!0),wf=_f(!1),$l=!1;function rr(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function ur(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Vl(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Zl(e,t,l){var a=e.updateQueue;if(a===null)return null;if(a=a.shared,(Te&2)!==0){var s=a.pending;return s===null?t.next=t:(t.next=s.next,s.next=t),a.pending=t,t=si(e),df(e,null,l),t}return ni(e,a,t,l),si(e)}function Wn(e,t,l){if(t=t.updateQueue,t!==null&&(t=t.shared,(l&4194048)!==0)){var a=t.lanes;a&=e.pendingLanes,l|=a,t.lanes=l,bo(e,l)}}function or(e,t){var l=e.updateQueue,a=e.alternate;if(a!==null&&(a=a.updateQueue,l===a)){var s=null,c=null;if(l=l.firstBaseUpdate,l!==null){do{var d={lane:l.lane,tag:l.tag,payload:l.payload,callback:null,next:null};c===null?s=c=d:c=c.next=d,l=l.next}while(l!==null);c===null?s=c=t:c=c.next=t}else s=c=t;l={baseState:a.baseState,firstBaseUpdate:s,lastBaseUpdate:c,shared:a.shared,callbacks:a.callbacks},e.updateQueue=l;return}e=l.lastBaseUpdate,e===null?l.firstBaseUpdate=t:e.next=t,l.lastBaseUpdate=t}var fr=!1;function Fn(){if(fr){var e=ln;if(e!==null)throw e}}function In(e,t,l,a){fr=!1;var s=e.updateQueue;$l=!1;var c=s.firstBaseUpdate,d=s.lastBaseUpdate,g=s.shared.pending;if(g!==null){s.shared.pending=null;var j=g,C=j.next;j.next=null,d===null?c=C:d.next=C,d=j;var k=e.alternate;k!==null&&(k=k.updateQueue,g=k.lastBaseUpdate,g!==d&&(g===null?k.firstBaseUpdate=C:g.next=C,k.lastBaseUpdate=j))}if(c!==null){var L=s.baseState;d=0,k=C=j=null,g=c;do{var M=g.lane&-536870913,D=M!==g.lane;if(D?(xe&M)===M:(a&M)===M){M!==0&&M===tn&&(fr=!0),k!==null&&(k=k.next={lane:0,tag:g.tag,payload:g.payload,callback:null,next:null});e:{var Z=e,P=g;M=t;var Ue=l;switch(P.tag){case 1:if(Z=P.payload,typeof Z=="function"){L=Z.call(Ue,L,M);break e}L=Z;break e;case 3:Z.flags=Z.flags&-65537|128;case 0:if(Z=P.payload,M=typeof Z=="function"?Z.call(Ue,L,M):Z,M==null)break e;L=y({},L,M);break e;case 2:$l=!0}}M=g.callback,M!==null&&(e.flags|=64,D&&(e.flags|=8192),D=s.callbacks,D===null?s.callbacks=[M]:D.push(M))}else D={lane:M,tag:g.tag,payload:g.payload,callback:g.callback,next:null},k===null?(C=k=D,j=L):k=k.next=D,d|=M;if(g=g.next,g===null){if(g=s.shared.pending,g===null)break;D=g,g=D.next,D.next=null,s.lastBaseUpdate=D,s.shared.pending=null}}while(!0);k===null&&(j=L),s.baseState=j,s.firstBaseUpdate=C,s.lastBaseUpdate=k,c===null&&(s.shared.lanes=0),Il|=d,e.lanes=d,e.memoizedState=L}}function Cf(e,t){if(typeof e!="function")throw Error(o(191,e));e.call(t)}function Mf(e,t){var l=e.callbacks;if(l!==null)for(e.callbacks=null,e=0;ec?c:8;var d=O.T,g={};O.T=g,Cr(e,!1,t,l);try{var j=s(),C=O.S;if(C!==null&&C(g,j),j!==null&&typeof j=="object"&&typeof j.then=="function"){var k=Ox(j,a);ts(e,t,k,Dt(e))}else ts(e,t,a,Dt(e))}catch(L){ts(e,t,{then:function(){},status:"rejected",reason:L},Dt())}finally{B.p=c,d!==null&&g.types!==null&&(d.types=g.types),O.T=d}}function Lx(){}function _r(e,t,l,a){if(e.tag!==5)throw Error(o(476));var s=ud(e).queue;rd(e,s,t,W,l===null?Lx:function(){return od(e),l(a)})}function ud(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:W,baseState:W,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:zl,lastRenderedState:W},next:null};var l={};return t.next={memoizedState:l,baseState:l,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:zl,lastRenderedState:l},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function od(e){var t=ud(e);t.next===null&&(t=e.alternate.memoizedState),ts(e,t.next.queue,{},Dt())}function wr(){return ut(vs)}function fd(){return Ze().memoizedState}function dd(){return Ze().memoizedState}function qx(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var l=Dt();e=Vl(l);var a=Zl(t,e,l);a!==null&&(At(a,t,l),Wn(a,t,l)),t={cache:nr()},e.payload=t;return}t=t.return}}function Bx(e,t,l){var a=Dt();l={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null},zi(e)?hd(t,l):(l=Zc(e,t,l,a),l!==null&&(At(l,e,a),pd(l,t,a)))}function md(e,t,l){var a=Dt();ts(e,t,l,a)}function ts(e,t,l,a){var s={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null};if(zi(e))hd(t,s);else{var c=e.alternate;if(e.lanes===0&&(c===null||c.lanes===0)&&(c=t.lastRenderedReducer,c!==null))try{var d=t.lastRenderedState,g=c(d,l);if(s.hasEagerState=!0,s.eagerState=g,Tt(g,d))return ni(e,t,s,0),Re===null&&ai(),!1}catch{}finally{}if(l=Zc(e,t,s,a),l!==null)return At(l,e,a),pd(l,t,a),!0}return!1}function Cr(e,t,l,a){if(a={lane:2,revertLane:ru(),gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},zi(e)){if(t)throw Error(o(479))}else t=Zc(e,l,a,2),t!==null&&At(t,e,2)}function zi(e){var t=e.alternate;return e===ue||t!==null&&t===ue}function hd(e,t){cn=gi=!0;var l=e.pending;l===null?t.next=t:(t.next=l.next,l.next=t),e.pending=t}function pd(e,t,l){if((l&4194048)!==0){var a=t.lanes;a&=e.pendingLanes,l|=a,t.lanes=l,bo(e,l)}}var ls={readContext:ut,use:yi,useCallback:Xe,useContext:Xe,useEffect:Xe,useImperativeHandle:Xe,useLayoutEffect:Xe,useInsertionEffect:Xe,useMemo:Xe,useReducer:Xe,useRef:Xe,useState:Xe,useDebugValue:Xe,useDeferredValue:Xe,useTransition:Xe,useSyncExternalStore:Xe,useId:Xe,useHostTransitionStatus:Xe,useFormState:Xe,useActionState:Xe,useOptimistic:Xe,useMemoCache:Xe,useCacheRefresh:Xe};ls.useEffectEvent=Xe;var xd={readContext:ut,use:yi,useCallback:function(e,t){return vt().memoizedState=[e,t===void 0?null:t],e},useContext:ut,useEffect:Pf,useImperativeHandle:function(e,t,l){l=l!=null?l.concat([e]):null,Ni(4194308,4,ad.bind(null,t,e),l)},useLayoutEffect:function(e,t){return Ni(4194308,4,e,t)},useInsertionEffect:function(e,t){Ni(4,2,e,t)},useMemo:function(e,t){var l=vt();t=t===void 0?null:t;var a=e();if(za){il(!0);try{e()}finally{il(!1)}}return l.memoizedState=[a,t],a},useReducer:function(e,t,l){var a=vt();if(l!==void 0){var s=l(t);if(za){il(!0);try{l(t)}finally{il(!1)}}}else s=t;return a.memoizedState=a.baseState=s,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:s},a.queue=e,e=e.dispatch=Bx.bind(null,ue,e),[a.memoizedState,e]},useRef:function(e){var t=vt();return e={current:e},t.memoizedState=e},useState:function(e){e=Sr(e);var t=e.queue,l=md.bind(null,ue,t);return t.dispatch=l,[e.memoizedState,l]},useDebugValue:Ar,useDeferredValue:function(e,t){var l=vt();return Tr(l,e,t)},useTransition:function(){var e=Sr(!1);return e=rd.bind(null,ue,e.queue,!0,!1),vt().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,l){var a=ue,s=vt();if(ve){if(l===void 0)throw Error(o(407));l=l()}else{if(l=t(),Re===null)throw Error(o(349));(xe&127)!==0||Hf(a,t,l)}s.memoizedState=l;var c={value:l,getSnapshot:t};return s.queue=c,Pf(qf.bind(null,a,c,e),[e]),a.flags|=2048,un(9,{destroy:void 0},Lf.bind(null,a,c,l,t),null),l},useId:function(){var e=vt(),t=Re.identifierPrefix;if(ve){var l=ul,a=rl;l=(a&~(1<<32-gt(a)-1)).toString(32)+l,t="_"+t+"R_"+l,l=vi++,0<\/script>",c=c.removeChild(c.firstChild);break;case"select":c=typeof a.is=="string"?d.createElement("select",{is:a.is}):d.createElement("select"),a.multiple?c.multiple=!0:a.size&&(c.size=a.size);break;default:c=typeof a.is=="string"?d.createElement(s,{is:a.is}):d.createElement(s)}}c[ct]=t,c[yt]=a;e:for(d=t.child;d!==null;){if(d.tag===5||d.tag===6)c.appendChild(d.stateNode);else if(d.tag!==4&&d.tag!==27&&d.child!==null){d.child.return=d,d=d.child;continue}if(d===t)break e;for(;d.sibling===null;){if(d.return===null||d.return===t)break e;d=d.return}d.sibling.return=d.return,d=d.sibling}t.stateNode=c;e:switch(ft(c,s,a),s){case"button":case"input":case"select":case"textarea":a=!!a.autoFocus;break e;case"img":a=!0;break e;default:a=!1}a&&Al(t)}}return Le(t),Xr(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,l),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==a&&Al(t);else{if(typeof a!="string"&&t.stateNode===null)throw Error(o(166));if(e=re.current,Pa(t)){if(e=t.stateNode,l=t.memoizedProps,a=null,s=rt,s!==null)switch(s.tag){case 27:case 5:a=s.memoizedProps}e[ct]=t,e=!!(e.nodeValue===l||a!==null&&a.suppressHydrationWarning===!0||Um(e.nodeValue,l)),e||Ql(t,!0)}else e=Xi(e).createTextNode(a),e[ct]=t,t.stateNode=e}return Le(t),null;case 31:if(l=t.memoizedState,e===null||e.memoizedState!==null){if(a=Pa(t),l!==null){if(e===null){if(!a)throw Error(o(318));if(e=t.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(o(557));e[ct]=t}else ga(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;Le(t),e=!1}else l=er(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=l),e=!0;if(!e)return t.flags&256?(Ct(t),t):(Ct(t),null);if((t.flags&128)!==0)throw Error(o(558))}return Le(t),null;case 13:if(a=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(s=Pa(t),a!==null&&a.dehydrated!==null){if(e===null){if(!s)throw Error(o(318));if(s=t.memoizedState,s=s!==null?s.dehydrated:null,!s)throw Error(o(317));s[ct]=t}else ga(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;Le(t),s=!1}else s=er(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=s),s=!0;if(!s)return t.flags&256?(Ct(t),t):(Ct(t),null)}return Ct(t),(t.flags&128)!==0?(t.lanes=l,t):(l=a!==null,e=e!==null&&e.memoizedState!==null,l&&(a=t.child,s=null,a.alternate!==null&&a.alternate.memoizedState!==null&&a.alternate.memoizedState.cachePool!==null&&(s=a.alternate.memoizedState.cachePool.pool),c=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(c=a.memoizedState.cachePool.pool),c!==s&&(a.flags|=2048)),l!==e&&l&&(t.child.flags|=8192),wi(t,t.updateQueue),Le(t),null);case 4:return Ae(),e===null&&du(t.stateNode.containerInfo),Le(t),null;case 10:return Nl(t.type),Le(t),null;case 19:if(H(Ve),a=t.memoizedState,a===null)return Le(t),null;if(s=(t.flags&128)!==0,c=a.rendering,c===null)if(s)ns(a,!1);else{if($e!==0||e!==null&&(e.flags&128)!==0)for(e=t.child;e!==null;){if(c=xi(e),c!==null){for(t.flags|=128,ns(a,!1),e=c.updateQueue,t.updateQueue=e,wi(t,e),t.subtreeFlags=0,e=l,l=t.child;l!==null;)mf(l,e),l=l.sibling;return G(Ve,Ve.current&1|2),ve&&yl(t,a.treeForkCount),t.child}e=e.sibling}a.tail!==null&&mt()>ki&&(t.flags|=128,s=!0,ns(a,!1),t.lanes=4194304)}else{if(!s)if(e=xi(c),e!==null){if(t.flags|=128,s=!0,e=e.updateQueue,t.updateQueue=e,wi(t,e),ns(a,!0),a.tail===null&&a.tailMode==="hidden"&&!c.alternate&&!ve)return Le(t),null}else 2*mt()-a.renderingStartTime>ki&&l!==536870912&&(t.flags|=128,s=!0,ns(a,!1),t.lanes=4194304);a.isBackwards?(c.sibling=t.child,t.child=c):(e=a.last,e!==null?e.sibling=c:t.child=c,a.last=c)}return a.tail!==null?(e=a.tail,a.rendering=e,a.tail=e.sibling,a.renderingStartTime=mt(),e.sibling=null,l=Ve.current,G(Ve,s?l&1|2:l&1),ve&&yl(t,a.treeForkCount),e):(Le(t),null);case 22:case 23:return Ct(t),mr(),a=t.memoizedState!==null,e!==null?e.memoizedState!==null!==a&&(t.flags|=8192):a&&(t.flags|=8192),a?(l&536870912)!==0&&(t.flags&128)===0&&(Le(t),t.subtreeFlags&6&&(t.flags|=8192)):Le(t),l=t.updateQueue,l!==null&&wi(t,l.retryQueue),l=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(l=e.memoizedState.cachePool.pool),a=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(a=t.memoizedState.cachePool.pool),a!==l&&(t.flags|=2048),e!==null&&H(ya),null;case 24:return l=null,e!==null&&(l=e.memoizedState.cache),t.memoizedState.cache!==l&&(t.flags|=2048),Nl(Je),Le(t),null;case 25:return null;case 30:return null}throw Error(o(156,t.tag))}function $x(e,t){switch(Ic(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Nl(Je),Ae(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return Me(t),null;case 31:if(t.memoizedState!==null){if(Ct(t),t.alternate===null)throw Error(o(340));ga()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(Ct(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(o(340));ga()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return H(Ve),null;case 4:return Ae(),null;case 10:return Nl(t.type),null;case 22:case 23:return Ct(t),mr(),e!==null&&H(ya),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Nl(Je),null;case 25:return null;default:return null}}function Bd(e,t){switch(Ic(t),t.tag){case 3:Nl(Je),Ae();break;case 26:case 27:case 5:Me(t);break;case 4:Ae();break;case 31:t.memoizedState!==null&&Ct(t);break;case 13:Ct(t);break;case 19:H(Ve);break;case 10:Nl(t.type);break;case 22:case 23:Ct(t),mr(),e!==null&&H(ya);break;case 24:Nl(Je)}}function ss(e,t){try{var l=t.updateQueue,a=l!==null?l.lastEffect:null;if(a!==null){var s=a.next;l=s;do{if((l.tag&e)===e){a=void 0;var c=l.create,d=l.inst;a=c(),d.destroy=a}l=l.next}while(l!==s)}}catch(g){Ce(t,t.return,g)}}function Wl(e,t,l){try{var a=t.updateQueue,s=a!==null?a.lastEffect:null;if(s!==null){var c=s.next;a=c;do{if((a.tag&e)===e){var d=a.inst,g=d.destroy;if(g!==void 0){d.destroy=void 0,s=t;var j=l,C=g;try{C()}catch(k){Ce(s,j,k)}}}a=a.next}while(a!==c)}}catch(k){Ce(t,t.return,k)}}function Yd(e){var t=e.updateQueue;if(t!==null){var l=e.stateNode;try{Mf(t,l)}catch(a){Ce(e,e.return,a)}}}function Gd(e,t,l){l.props=Ea(e.type,e.memoizedProps),l.state=e.memoizedState;try{l.componentWillUnmount()}catch(a){Ce(e,t,a)}}function is(e,t){try{var l=e.ref;if(l!==null){switch(e.tag){case 26:case 27:case 5:var a=e.stateNode;break;case 30:a=e.stateNode;break;default:a=e.stateNode}typeof l=="function"?e.refCleanup=l(a):l.current=a}}catch(s){Ce(e,t,s)}}function ol(e,t){var l=e.ref,a=e.refCleanup;if(l!==null)if(typeof a=="function")try{a()}catch(s){Ce(e,t,s)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof l=="function")try{l(null)}catch(s){Ce(e,t,s)}else l.current=null}function Qd(e){var t=e.type,l=e.memoizedProps,a=e.stateNode;try{e:switch(t){case"button":case"input":case"select":case"textarea":l.autoFocus&&a.focus();break e;case"img":l.src?a.src=l.src:l.srcSet&&(a.srcset=l.srcSet)}}catch(s){Ce(e,e.return,s)}}function $r(e,t,l){try{var a=e.stateNode;mg(a,e.type,l,t),a[yt]=t}catch(s){Ce(e,e.return,s)}}function Xd(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&aa(e.type)||e.tag===4}function Vr(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Xd(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&aa(e.type)||e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Zr(e,t,l){var a=e.tag;if(a===5||a===6)e=e.stateNode,t?(l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l).insertBefore(e,t):(t=l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l,t.appendChild(e),l=l._reactRootContainer,l!=null||t.onclick!==null||(t.onclick=gl));else if(a!==4&&(a===27&&aa(e.type)&&(l=e.stateNode,t=null),e=e.child,e!==null))for(Zr(e,t,l),e=e.sibling;e!==null;)Zr(e,t,l),e=e.sibling}function Ci(e,t,l){var a=e.tag;if(a===5||a===6)e=e.stateNode,t?l.insertBefore(e,t):l.appendChild(e);else if(a!==4&&(a===27&&aa(e.type)&&(l=e.stateNode),e=e.child,e!==null))for(Ci(e,t,l),e=e.sibling;e!==null;)Ci(e,t,l),e=e.sibling}function $d(e){var t=e.stateNode,l=e.memoizedProps;try{for(var a=e.type,s=t.attributes;s.length;)t.removeAttributeNode(s[0]);ft(t,a,l),t[ct]=e,t[yt]=l}catch(c){Ce(e,e.return,c)}}var Tl=!1,Ie=!1,Kr=!1,Vd=typeof WeakSet=="function"?WeakSet:Set,it=null;function Vx(e,t){if(e=e.containerInfo,pu=Fi,e=af(e),Yc(e)){if("selectionStart"in e)var l={start:e.selectionStart,end:e.selectionEnd};else e:{l=(l=e.ownerDocument)&&l.defaultView||window;var a=l.getSelection&&l.getSelection();if(a&&a.rangeCount!==0){l=a.anchorNode;var s=a.anchorOffset,c=a.focusNode;a=a.focusOffset;try{l.nodeType,c.nodeType}catch{l=null;break e}var d=0,g=-1,j=-1,C=0,k=0,L=e,M=null;t:for(;;){for(var D;L!==l||s!==0&&L.nodeType!==3||(g=d+s),L!==c||a!==0&&L.nodeType!==3||(j=d+a),L.nodeType===3&&(d+=L.nodeValue.length),(D=L.firstChild)!==null;)M=L,L=D;for(;;){if(L===e)break t;if(M===l&&++C===s&&(g=d),M===c&&++k===a&&(j=d),(D=L.nextSibling)!==null)break;L=M,M=L.parentNode}L=D}l=g===-1||j===-1?null:{start:g,end:j}}else l=null}l=l||{start:0,end:0}}else l=null;for(xu={focusedElem:e,selectionRange:l},Fi=!1,it=t;it!==null;)if(t=it,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,it=e;else for(;it!==null;){switch(t=it,c=t.alternate,e=t.flags,t.tag){case 0:if((e&4)!==0&&(e=t.updateQueue,e=e!==null?e.events:null,e!==null))for(l=0;l title"))),ft(c,a,l),c[ct]=e,st(c),a=c;break e;case"link":var d=Im("link","href",s).get(a+(l.href||""));if(d){for(var g=0;gUe&&(d=Ue,Ue=P,P=d);var A=tf(g,P),z=tf(g,Ue);if(A&&z&&(D.rangeCount!==1||D.anchorNode!==A.node||D.anchorOffset!==A.offset||D.focusNode!==z.node||D.focusOffset!==z.offset)){var w=L.createRange();w.setStart(A.node,A.offset),D.removeAllRanges(),P>Ue?(D.addRange(w),D.extend(z.node,z.offset)):(w.setEnd(z.node,z.offset),D.addRange(w))}}}}for(L=[],D=g;D=D.parentNode;)D.nodeType===1&&L.push({element:D,left:D.scrollLeft,top:D.scrollTop});for(typeof g.focus=="function"&&g.focus(),g=0;gl?32:l,O.T=null,l=tu,tu=null;var c=ea,d=Ol;if(lt=0,hn=ea=null,Ol=0,(Te&6)!==0)throw Error(o(331));var g=Te;if(Te|=4,am(c.current),em(c,c.current,d,l),Te=g,ds(0,!1),xt&&typeof xt.onPostCommitFiberRoot=="function")try{xt.onPostCommitFiberRoot(sl,c)}catch{}return!0}finally{B.p=s,O.T=a,jm(e,t)}}function Sm(e,t,l){t=Lt(l,t),t=kr(e.stateNode,t,2),e=Zl(e,t,2),e!==null&&(On(e,2),fl(e))}function Ce(e,t,l){if(e.tag===3)Sm(e,e,l);else for(;t!==null;){if(t.tag===3){Sm(t,e,l);break}else if(t.tag===1){var a=t.stateNode;if(typeof t.type.getDerivedStateFromError=="function"||typeof a.componentDidCatch=="function"&&(Pl===null||!Pl.has(a))){e=Lt(l,e),l=zd(2),a=Zl(t,l,2),a!==null&&(Ed(l,a,t,e),On(a,2),fl(a));break}}t=t.return}}function su(e,t,l){var a=e.pingCache;if(a===null){a=e.pingCache=new Jx;var s=new Set;a.set(t,s)}else s=a.get(t),s===void 0&&(s=new Set,a.set(t,s));s.has(l)||(Fr=!0,s.add(l),e=eg.bind(null,e,t,l),t.then(e,e))}function eg(e,t,l){var a=e.pingCache;a!==null&&a.delete(t),e.pingedLanes|=e.suspendedLanes&l,e.warmLanes&=~l,Re===e&&(xe&l)===l&&($e===4||$e===3&&(xe&62914560)===xe&&300>mt()-Di?(Te&2)===0&&pn(e,0):Ir|=l,mn===xe&&(mn=0)),fl(e)}function zm(e,t){t===0&&(t=$s()),e=pa(e,t),e!==null&&(On(e,t),fl(e))}function tg(e){var t=e.memoizedState,l=0;t!==null&&(l=t.retryLane),zm(e,l)}function lg(e,t){var l=0;switch(e.tag){case 31:case 13:var a=e.stateNode,s=e.memoizedState;s!==null&&(l=s.retryLane);break;case 19:a=e.stateNode;break;case 22:a=e.stateNode._retryCache;break;default:throw Error(o(314))}a!==null&&a.delete(t),zm(e,l)}function ag(e,t){return Ft(e,t)}var Bi=null,gn=null,iu=!1,Yi=!1,cu=!1,la=0;function fl(e){e!==gn&&e.next===null&&(gn===null?Bi=gn=e:gn=gn.next=e),Yi=!0,iu||(iu=!0,sg())}function ds(e,t){if(!cu&&Yi){cu=!0;do for(var l=!1,a=Bi;a!==null;){if(e!==0){var s=a.pendingLanes;if(s===0)var c=0;else{var d=a.suspendedLanes,g=a.pingedLanes;c=(1<<31-gt(42|e)+1)-1,c&=s&~(d&~g),c=c&201326741?c&201326741|1:c?c|2:0}c!==0&&(l=!0,_m(a,c))}else c=xe,c=Ra(a,a===Re?c:0,a.cancelPendingCommit!==null||a.timeoutHandle!==-1),(c&3)===0||Ll(a,c)||(l=!0,_m(a,c));a=a.next}while(l);cu=!1}}function ng(){Em()}function Em(){Yi=iu=!1;var e=0;la!==0&&pg()&&(e=la);for(var t=mt(),l=null,a=Bi;a!==null;){var s=a.next,c=Am(a,t);c===0?(a.next=null,l===null?Bi=s:l.next=s,s===null&&(gn=l)):(l=a,(e!==0||(c&3)!==0)&&(Yi=!0)),a=s}lt!==0&<!==5||ds(e),la!==0&&(la=0)}function Am(e,t){for(var l=e.suspendedLanes,a=e.pingedLanes,s=e.expirationTimes,c=e.pendingLanes&-62914561;0g)break;var k=j.transferSize,L=j.initiatorType;k&&Rm(L)&&(j=j.responseEnd,d+=k*(j"u"?null:document;function Km(e,t,l){var a=vn;if(a&&typeof t=="string"&&t){var s=Rt(t);s='link[rel="'+e+'"][href="'+s+'"]',typeof l=="string"&&(s+='[crossorigin="'+l+'"]'),Zm.has(s)||(Zm.add(s),e={rel:e,crossOrigin:l,href:t},a.querySelector(s)===null&&(t=a.createElement("link"),ft(t,"link",e),st(t),a.head.appendChild(t)))}}function zg(e){Dl.D(e),Km("dns-prefetch",e,null)}function Eg(e,t){Dl.C(e,t),Km("preconnect",e,t)}function Ag(e,t,l){Dl.L(e,t,l);var a=vn;if(a&&e&&t){var s='link[rel="preload"][as="'+Rt(t)+'"]';t==="image"&&l&&l.imageSrcSet?(s+='[imagesrcset="'+Rt(l.imageSrcSet)+'"]',typeof l.imageSizes=="string"&&(s+='[imagesizes="'+Rt(l.imageSizes)+'"]')):s+='[href="'+Rt(e)+'"]';var c=s;switch(t){case"style":c=bn(e);break;case"script":c=yn(e)}Xt.has(c)||(e=y({rel:"preload",href:t==="image"&&l&&l.imageSrcSet?void 0:e,as:t},l),Xt.set(c,e),a.querySelector(s)!==null||t==="style"&&a.querySelector(xs(c))||t==="script"&&a.querySelector(gs(c))||(t=a.createElement("link"),ft(t,"link",e),st(t),a.head.appendChild(t)))}}function Tg(e,t){Dl.m(e,t);var l=vn;if(l&&e){var a=t&&typeof t.as=="string"?t.as:"script",s='link[rel="modulepreload"][as="'+Rt(a)+'"][href="'+Rt(e)+'"]',c=s;switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":c=yn(e)}if(!Xt.has(c)&&(e=y({rel:"modulepreload",href:e},t),Xt.set(c,e),l.querySelector(s)===null)){switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(l.querySelector(gs(c)))return}a=l.createElement("link"),ft(a,"link",e),st(a),l.head.appendChild(a)}}}function _g(e,t,l){Dl.S(e,t,l);var a=vn;if(a&&e){var s=Ba(a).hoistableStyles,c=bn(e);t=t||"default";var d=s.get(c);if(!d){var g={loading:0,preload:null};if(d=a.querySelector(xs(c)))g.loading=5;else{e=y({rel:"stylesheet",href:e,"data-precedence":t},l),(l=Xt.get(c))&&Su(e,l);var j=d=a.createElement("link");st(j),ft(j,"link",e),j._p=new Promise(function(C,k){j.onload=C,j.onerror=k}),j.addEventListener("load",function(){g.loading|=1}),j.addEventListener("error",function(){g.loading|=2}),g.loading|=4,Vi(d,t,a)}d={type:"stylesheet",instance:d,count:1,state:g},s.set(c,d)}}}function wg(e,t){Dl.X(e,t);var l=vn;if(l&&e){var a=Ba(l).hoistableScripts,s=yn(e),c=a.get(s);c||(c=l.querySelector(gs(s)),c||(e=y({src:e,async:!0},t),(t=Xt.get(s))&&zu(e,t),c=l.createElement("script"),st(c),ft(c,"link",e),l.head.appendChild(c)),c={type:"script",instance:c,count:1,state:null},a.set(s,c))}}function Cg(e,t){Dl.M(e,t);var l=vn;if(l&&e){var a=Ba(l).hoistableScripts,s=yn(e),c=a.get(s);c||(c=l.querySelector(gs(s)),c||(e=y({src:e,async:!0,type:"module"},t),(t=Xt.get(s))&&zu(e,t),c=l.createElement("script"),st(c),ft(c,"link",e),l.head.appendChild(c)),c={type:"script",instance:c,count:1,state:null},a.set(s,c))}}function Jm(e,t,l,a){var s=(s=re.current)?$i(s):null;if(!s)throw Error(o(446));switch(e){case"meta":case"title":return null;case"style":return typeof l.precedence=="string"&&typeof l.href=="string"?(t=bn(l.href),l=Ba(s).hoistableStyles,a=l.get(t),a||(a={type:"style",instance:null,count:0,state:null},l.set(t,a)),a):{type:"void",instance:null,count:0,state:null};case"link":if(l.rel==="stylesheet"&&typeof l.href=="string"&&typeof l.precedence=="string"){e=bn(l.href);var c=Ba(s).hoistableStyles,d=c.get(e);if(d||(s=s.ownerDocument||s,d={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},c.set(e,d),(c=s.querySelector(xs(e)))&&!c._p&&(d.instance=c,d.state.loading=5),Xt.has(e)||(l={rel:"preload",as:"style",href:l.href,crossOrigin:l.crossOrigin,integrity:l.integrity,media:l.media,hrefLang:l.hrefLang,referrerPolicy:l.referrerPolicy},Xt.set(e,l),c||Mg(s,e,l,d.state))),t&&a===null)throw Error(o(528,""));return d}if(t&&a!==null)throw Error(o(529,""));return null;case"script":return t=l.async,l=l.src,typeof l=="string"&&t&&typeof t!="function"&&typeof t!="symbol"?(t=yn(l),l=Ba(s).hoistableScripts,a=l.get(t),a||(a={type:"script",instance:null,count:0,state:null},l.set(t,a)),a):{type:"void",instance:null,count:0,state:null};default:throw Error(o(444,e))}}function bn(e){return'href="'+Rt(e)+'"'}function xs(e){return'link[rel="stylesheet"]['+e+"]"}function Wm(e){return y({},e,{"data-precedence":e.precedence,precedence:null})}function Mg(e,t,l,a){e.querySelector('link[rel="preload"][as="style"]['+t+"]")?a.loading=1:(t=e.createElement("link"),a.preload=t,t.addEventListener("load",function(){return a.loading|=1}),t.addEventListener("error",function(){return a.loading|=2}),ft(t,"link",l),st(t),e.head.appendChild(t))}function yn(e){return'[src="'+Rt(e)+'"]'}function gs(e){return"script[async]"+e}function Fm(e,t,l){if(t.count++,t.instance===null)switch(t.type){case"style":var a=e.querySelector('style[data-href~="'+Rt(l.href)+'"]');if(a)return t.instance=a,st(a),a;var s=y({},l,{"data-href":l.href,"data-precedence":l.precedence,href:null,precedence:null});return a=(e.ownerDocument||e).createElement("style"),st(a),ft(a,"style",s),Vi(a,l.precedence,e),t.instance=a;case"stylesheet":s=bn(l.href);var c=e.querySelector(xs(s));if(c)return t.state.loading|=4,t.instance=c,st(c),c;a=Wm(l),(s=Xt.get(s))&&Su(a,s),c=(e.ownerDocument||e).createElement("link"),st(c);var d=c;return d._p=new Promise(function(g,j){d.onload=g,d.onerror=j}),ft(c,"link",a),t.state.loading|=4,Vi(c,l.precedence,e),t.instance=c;case"script":return c=yn(l.src),(s=e.querySelector(gs(c)))?(t.instance=s,st(s),s):(a=l,(s=Xt.get(c))&&(a=y({},l),zu(a,s)),e=e.ownerDocument||e,s=e.createElement("script"),st(s),ft(s,"link",a),e.head.appendChild(s),t.instance=s);case"void":return null;default:throw Error(o(443,t.type))}else t.type==="stylesheet"&&(t.state.loading&4)===0&&(a=t.instance,t.state.loading|=4,Vi(a,l.precedence,e));return t.instance}function Vi(e,t,l){for(var a=l.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),s=a.length?a[a.length-1]:null,c=s,d=0;d title"):null)}function Og(e,t,l){if(l===1||t.itemProp!=null)return!1;switch(e){case"meta":case"title":return!0;case"style":if(typeof t.precedence!="string"||typeof t.href!="string"||t.href==="")break;return!0;case"link":if(typeof t.rel!="string"||typeof t.href!="string"||t.href===""||t.onLoad||t.onError)break;switch(t.rel){case"stylesheet":return e=t.disabled,typeof t.precedence=="string"&&e==null;default:return!0}case"script":if(t.async&&typeof t.async!="function"&&typeof t.async!="symbol"&&!t.onLoad&&!t.onError&&t.src&&typeof t.src=="string")return!0}return!1}function eh(e){return!(e.type==="stylesheet"&&(e.state.loading&3)===0)}function Dg(e,t,l,a){if(l.type==="stylesheet"&&(typeof a.media!="string"||matchMedia(a.media).matches!==!1)&&(l.state.loading&4)===0){if(l.instance===null){var s=bn(a.href),c=t.querySelector(xs(s));if(c){t=c._p,t!==null&&typeof t=="object"&&typeof t.then=="function"&&(e.count++,e=Ki.bind(e),t.then(e,e)),l.state.loading|=4,l.instance=c,st(c);return}c=t.ownerDocument||t,a=Wm(a),(s=Xt.get(s))&&Su(a,s),c=c.createElement("link"),st(c);var d=c;d._p=new Promise(function(g,j){d.onload=g,d.onerror=j}),ft(c,"link",a),l.instance=c}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(l,t),(t=l.state.preload)&&(l.state.loading&3)===0&&(e.count++,l=Ki.bind(e),t.addEventListener("load",l),t.addEventListener("error",l))}}var Eu=0;function kg(e,t){return e.stylesheets&&e.count===0&&Wi(e,e.stylesheets),0Eu?50:800)+t);return e.unsuspend=l,function(){e.unsuspend=null,clearTimeout(a),clearTimeout(s)}}:null}function Ki(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Wi(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Ji=null;function Wi(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Ji=new Map,t.forEach(Ug,e),Ji=null,Ki.call(e))}function Ug(e,t){if(!(t.state.loading&4)){var l=Ji.get(e);if(l)var a=l.get(null);else{l=new Map,Ji.set(e,l);for(var s=e.querySelectorAll("link[data-precedence],style[data-precedence]"),c=0;c"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(r){console.error(r)}}return i(),ku.exports=ev(),ku.exports}var Eh=tv();/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const lv=i=>i.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),av=i=>i.replace(/^([A-Z])|[\s-_]+(\w)/g,(r,u,o)=>o?o.toUpperCase():u.toLowerCase()),Ah=i=>{const r=av(i);return r.charAt(0).toUpperCase()+r.slice(1)},lp=(...i)=>i.filter((r,u,o)=>!!r&&r.trim()!==""&&o.indexOf(r)===u).join(" ").trim(),nv=i=>{for(const r in i)if(r.startsWith("aria-")||r==="role"||r==="title")return!0};/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */var sv={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const iv=Y.forwardRef(({color:i="currentColor",size:r=24,strokeWidth:u=2,absoluteStrokeWidth:o,className:f="",children:m,iconNode:h,...x},v)=>Y.createElement("svg",{ref:v,...sv,width:r,height:r,stroke:i,strokeWidth:o?Number(u)*24/Number(r):u,className:lp("lucide",f),...!m&&!nv(x)&&{"aria-hidden":"true"},...x},[...h.map(([p,b])=>Y.createElement(p,b)),...Array.isArray(m)?m:[m]]));/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const at=(i,r)=>{const u=Y.forwardRef(({className:o,...f},m)=>Y.createElement(iv,{ref:m,iconNode:r,className:lp(`lucide-${lv(Ah(i))}`,`lucide-${i}`,o),...f}));return u.displayName=Ah(i),u};/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const cv=[["path",{d:"M7 7h10v10",key:"1tivn9"}],["path",{d:"M7 17 17 7",key:"1vkiza"}]],uc=at("arrow-up-right",cv);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const rv=[["path",{d:"M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2",key:"5owen"}],["circle",{cx:"7",cy:"17",r:"2",key:"u2ysq9"}],["path",{d:"M9 17h6",key:"r8uit2"}],["circle",{cx:"17",cy:"17",r:"2",key:"axvx0g"}]],uv=at("car",rv);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ov=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],fv=at("check",ov);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const dv=[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]],oc=at("chevron-down",dv);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const mv=[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]],Ma=at("chevron-left",mv);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const hv=[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]],Oa=at("chevron-right",hv);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const pv=[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]],xv=at("copy",pv);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const gv=[["path",{d:"M15 3h6v6",key:"1q9fwt"}],["path",{d:"M10 14 21 3",key:"gplh6r"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}]],vv=at("external-link",gv);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const bv=[["rect",{width:"20",height:"20",x:"2",y:"2",rx:"5",ry:"5",key:"2e1cvw"}],["path",{d:"M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z",key:"9exkf1"}],["line",{x1:"17.5",x2:"17.51",y1:"6.5",y2:"6.5",key:"r4j83e"}]],yv=at("instagram",bv);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const jv=[["path",{d:"m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7",key:"132q7q"}],["rect",{x:"2",y:"4",width:"20",height:"16",rx:"2",key:"izxlao"}]],ap=at("mail",jv);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Nv=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0",key:"1r0f0z"}],["circle",{cx:"12",cy:"10",r:"3",key:"ilqhr7"}]],Rs=at("map-pin",Nv);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Sv=[["path",{d:"M4 5h16",key:"1tepv9"}],["path",{d:"M4 12h16",key:"1lakjw"}],["path",{d:"M4 19h16",key:"1djgab"}]],np=at("menu",Sv);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const zv=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719",key:"1sd12s"}]],Fu=at("message-circle",zv);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ev=[["polygon",{points:"3 11 22 2 13 21 11 13 3 11",key:"1ltx0t"}]],Lu=at("navigation",Ev);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Av=[["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384",key:"9njp5v"}]],Jt=at("phone",Av);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Tv=[["path",{d:"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z",key:"10ikf1"}]],_v=at("play",Tv);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const wv=[["path",{d:"M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2",key:"cjf0a3"}],["path",{d:"M7 2v20",key:"1473qp"}],["path",{d:"M21 15V2a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3Zm0 0v7",key:"j28e5"}]],Cv=at("utensils",wv);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Mv=[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]],sp=at("x",Mv);/** + * @license lucide-react v0.546.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ov=[["path",{d:"M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.55 49.55 0 0 1-16.2 0A2 2 0 0 1 2.5 17",key:"1q2vi4"}],["path",{d:"m10 15 5-3-5-3z",key:"1jp15x"}]],Dv=at("youtube",Ov),ip=Y.createContext(null);function kv({payload:i,children:r}){return n.jsx(ip.Provider,{value:i,children:r})}function F(){const i=Y.useContext(ip);if(!i)throw new Error("SiteProvider 밖에서 useSite() 를 불렀습니다. payload 없이는 렌더할 수 없습니다.");return i}const zs={LODGING:1,CAFE:2,RESTAURANT:3,CLINIC:4},Uy={OWNER:1,CRAWL:3},Th={UNVERIFIED:1,VERIFIED:3,CORRECTED:4},Uv=[Th.VERIFIED,Th.CORRECTED];function cp(i){return Uv.includes(i)}const $t={YANOLJA:1,GOODCHOICE:2,NAVER_PLACE:3,INSTAGRAM:4,OFFICIAL_SITE:5,BLOG:6,ETC:99},Ry={PUBLISHED:3};function fc(i){return i.filter(r=>cp(r.status)&&r.value!=null&&r.value!=="")}function rp(i){const r=new Map;for(const u of fc(i))r.set(u.key,u);return r}function _a(i,r){const u=rp(i).get(r);return(u==null?void 0:u.value)??void 0}function dl(i,r){const u=rp(i).get(r);if(!(u!=null&&u.value))return;const o=u.type==="number"?Rv(u.value):u.value;return u.unit?`${o}${u.unit}`:o}function Rv(i){const r=Number(i.replace(/,/g,""));return Number.isFinite(r)?r.toLocaleString("ko-KR"):i}function Iu(i){return i.type!=="bool"?i.label:i.label.replace(/\s*(가능|여부)$/,"")||i.label}function Hv(i){return i.filter(r=>cp(r.status)&&r.question&&r.answer).sort((r,u)=>r.sortOrder-u.sortOrder)}function Lv(i){return i.map(r=>({...r,facts:fc(r.facts)})).sort((r,u)=>r.sortOrder-u.sortOrder)}const qv={songs:"title",daily:"title",course:"name",schedule:"name",people:"name",chronicle:"title",literature:"workTitle",postcard:"line",quiz:"question",planner:"name",itinerary:"name",video:"url",event:"title"};function Bv(i){const r=(i??"").trim();if(!r)return;const u=/(?:youtube\.com\/(?:watch\?(?:.*&)?v=|shorts\/|embed\/|live\/)|youtu\.be\/)([A-Za-z0-9_-]{6,20})/.exec(r);return u==null?void 0:u[1]}function Yv(i){return/youtube\.com\/shorts\//.test((i??"").trim())}function Gv(i){return`https://i.ytimg.com/vi/${i}/hqdefault.jpg`}function Qv(i){return`https://www.youtube-nocookie.com/embed/${i}?autoplay=1&rel=0&modestbranding=1`}const nc={items:[],unverified:0,sourced:0};function Xv(i,r){var p,b;const u=y=>{const E=i.slice(0,Math.max(0,y)),T=E.split(` +`).length,N=y-E.lastIndexOf(` +`);return`${T}번째 줄 ${N}번째 글자`},o=(p=/Unexpected token '(.)'/.exec(r))==null?void 0:p[1],f=o==="}"||o==="]"?"닫는 괄호 바로 앞에 쉼표가 하나 더 있는지 보세요.":"그 앞의 쉼표·따옴표·괄호를 확인해 주세요.",m=/line (\d+) column (\d+)/.exec(r);if(m)return`${m[1]}번째 줄 ${m[2]}번째 글자에서 JSON 이 끊깁니다. ${f}`;const h=/position (\d+)/.exec(r);if(h)return`${u(Number(h[1]))}에서 JSON 이 끊깁니다. ${f}`;const x=(b=/\.\.\."([\s\S]*?)" is not valid JSON/.exec(r))==null?void 0:b[1],v=x?i.indexOf(x):-1;return v>=0?`${u(v+x.length)} 부근에서 JSON 이 끊깁니다. ${f}`:"JSON 이 아닙니다. ChatGPT 가 준 답에서 { 로 시작해 } 로 끝나는 부분만 붙여넣어 주세요."}function $v(i,r){const u=qv[i],o=(r??"").trim();if(!u||!o)return nc;let f;try{f=JSON.parse(o)}catch(y){return{...nc,error:Xv(o,y instanceof Error?y.message:"")}}if(typeof f!="object"||f===null||Array.isArray(f))return{...nc,error:"바깥이 { } 로 감싸인 JSON 이어야 합니다."};const m=f,h=typeof m.kind=="string"?m.kind:void 0,x=m.items;if(!Array.isArray(x))return{...nc,error:"items 배열이 없습니다. 프롬프트로 다시 만들어 주세요."};const v=x.filter(y=>typeof y=="object"&&y!==null&&!Array.isArray(y)&&typeof y[u]=="string"&&y[u].trim().length>0);let p=0,b=0;for(const y of v){const E=y;E.verified!=="확인"&&(p+=1),E.source&&typeof E.source=="object"&&(b+=1)}return{items:v,title:typeof m.title=="string"?m.title:void 0,subtitle:typeof m.subtitle=="string"?m.subtitle:void 0,linkUrl:typeof m.linkUrl=="string"&&/^https?:\/\//.test(m.linkUrl)?m.linkUrl:void 0,linkLabel:typeof m.linkLabel=="string"?m.linkLabel:void 0,kindMismatch:h&&h!==i?h:void 0,unverified:p,sourced:b}}const Vv=1260,Zv=60,Kv="10:00",wa=["봄","여름","가을","겨울"];function _h(i){const r=/^(\d{1,2}):(\d{2})$/.exec(i.trim());if(!r)return;const u=Number(r[1]),o=Number(r[2]);if(!(u>23||o>59))return u*60+o}function sc(i){const r=(i%1440+1440)%1440;return`${String(Math.floor(r/60)).padStart(2,"0")}:${String(r%60).padStart(2,"0")}`}function up(i){const r=_h(i.startTime??"")??_h(Kv)??600,u=[];let o=r,f=0;for(const m of i.stops??[]){const h=Math.max(0,m.moveMinutes??0),x=Math.max(1,m.minutes??Zv),v=o+h;if(v+x>Vv){f+=1;continue}u.push({stop:m,time:sc(v),until:sc(v+x),move:h}),o=v+x}return{stops:u,from:sc(r),to:sc(o),totalMinutes:o-r,dropped:f}}function Jv(i=new Date){const r=i.getMonth()+1,u=Math.floor((r-3+12)%12/3),o=wa[u];return r%3===0&&i.getDate()<=15?[wa[(u+3)%4],o]:[o]}function Wv(i,r){const u=i==null?void 0:i.trim();if(!u)return!0;const o=wa.filter(f=>u.includes(f));return o.length===0?!0:o.some(f=>r.includes(f))}function Fv(i){var u;const r=[];for(const o of i){const f=(u=o.season)==null?void 0:u.trim();f&&!r.includes(f)&&r.push(f)}return r.sort((o,f)=>{const m=wa.indexOf(o),h=wa.indexOf(f);return(m<0?wa.length:m)-(h<0?wa.length:h)})}function Iv(i,r){const u=r?i.filter(o=>{var f;return((f=o.season)==null?void 0:f.trim())===r}):[...i];return u.sort((o,f)=>(o.rank??Number.MAX_SAFE_INTEGER)-(f.rank??Number.MAX_SAFE_INTEGER)),u.slice(0,3)}const op={[zs.LODGING]:{type:"HotelRoom",path:"rooms",label:"객실"},[zs.CAFE]:{type:"MenuItem",path:"menu",label:"메뉴"},[zs.RESTAURANT]:{type:"MenuItem",path:"menu",label:"메뉴"},[zs.CLINIC]:{type:"Product",path:"programs",label:"시술"}};function dc(i){return fc(i.facts).filter(r=>r.scope==="place").map(r=>({label:Iu(r),value:Pu(r,i.facts)})).filter(r=>r.value!=="")}function Pu(i,r){const u=dl(r,i.key);return u?i.type!=="bool"?u:/여부$/.test(i.label)?u==="true"?"있음":u==="false"?"없음":u:u==="true"?"가능":u==="false"?"불가":u:""}const Pv=new Set(["weekday_price","weekend_price","peak_price","price_range"]);function ll(i){const r=op[i.place.category],u=new Map(i.media.map(o=>[o.mediaId,o]));return Lv(i.units).map(o=>{const f=dl(o.facts,"room_intro")??dl(o.facts,"description"),m=i.place.category===zs.LODGING?["standard_capacity","max_capacity","bed_type","room_size"]:["price","volume","origin"];return{unitId:o.unitId,slug:o.slug,name:o.name,intro:f,chips:m.map(h=>{var p;const x=dl(o.facts,h),v=((p=o.facts.find(b=>b.key===h))==null?void 0:p.label)??h;return x?{label:v,value:x}:null}).filter(h=>h!==null),rows:o.facts.filter(h=>!Pv.has(h.key)).map(h=>({label:Iu(h),value:Pu(h,o.facts)})).filter(h=>h.value!==""),images:o.mediaIds.map(h=>u.get(h)).filter(h=>{var x;return!!((x=h==null?void 0:h.alt)!=null&&x.trim())}),priceText:e0(o),prices:l0(o),href:`/${r.path}/${o.slug}`}})}function e0(i){var m,h;const r=dl(i.facts,"price_range");if(r)return r;const u=Number((m=dl(i.facts,"weekday_price"))==null?void 0:m.replace(/[^0-9]/g,"")),o=Number((h=dl(i.facts,"price"))==null?void 0:h.replace(/[^0-9]/g,"")),f=Number.isFinite(u)&&u>0?u:o;if(!(!Number.isFinite(f)||f<=0))return`${f.toLocaleString("ko-KR")}원부터`}const t0=[["weekday_price","주중"],["weekend_price","주말"],["peak_price","성수기"]];function l0(i){return dl(i.facts,"price_range")?[]:dl(i.facts,"weekday_price")?t0.map(([r,u])=>({label:u,value:dl(i.facts,r)??"문의"})):[]}function a0(i){const r=i??"";return/눈|설/.test(r)?"눈":/비|우|소나기/.test(r)?"비":/흐림|구름|안개|박무/.test(r)?"흐림":"맑음"}function n0(i){return i>=30?"혹서":i>=25?"더움":i>=20?"선선":i>=10?"쌀쌀":"추움"}function eo(i){return i.media.filter(r=>{var u;return((u=r.alt)==null?void 0:u.trim())&&!r.unitId})}function s0(i){return Hv(i.faqs)}function i0(i){return i.theme.sections.filter(r=>r.enabled)}function Kt(i,r){var u;return((u=i.theme.sections.find(o=>o.id===r))==null?void 0:u.enabled)??!1}function c0(i,r){var o;const u=(o=i.theme.sections.find(f=>f.id===r))==null?void 0:o.body;return(u==null?void 0:u.split(/\n\s*\n/).map(f=>f.trim()).filter(Boolean))??[]}function al(i,r){const u=i.theme.sections.find(o=>o.id===r);return $v(r,u==null?void 0:u.data)}function dt(i){return op[i.place.category]}function qe(i,r,u){var f,m;const o=(m=(f=i.theme.sections.find(h=>h.id===r))==null?void 0:f.name)==null?void 0:m.trim();return o||u}function Hs(i,r){const u=new Map(fc(i.facts).filter(o=>o.scope==="place").map(o=>[o.key,o]));return r.map(o=>u.get(o)).filter(o=>o!==void 0).map(o=>({label:Iu(o),value:Pu(o,i.facts)})).filter(o=>o.value!=="")}function ic(i,r){return Hs(i,[r])[0]}const r0=["reservation_required","reservation_channel"];function u0(i){return Hs(i,r0)}const o0=["seat_count","terrace","room_available","group_seat_max","power_outlet","study_allowed","wheelchair_accessible"];function f0(i){return Hs(i,o0)}const d0=["operating_hours","session_times","closed_days","age_limit","guide_language"];function m0(i){return Hs(i,d0)}const h0=["check_in_time","check_out_time","operating_hours","closed_days","parking","reservation_required"];function Da(i){return Hs(i,h0).slice(0,4)}function Sn(i){const r=ll(i).map(o=>o.priceText).filter(o=>!!o);if(r.length===0)return;const u=o=>Number(o.replace(/[^0-9]/g,""))||Number.MAX_SAFE_INTEGER;return r.reduce((o,f)=>u(f)u.confirmed&&r.includes(u.channel))}function et(i){return dp(i,p0)}function Ls(i){return dp(i,x0)}function g0(){var o;const i=F(),r=dt(i),u=[{label:"소개",href:"#about",show:Kt(i,"intro")},{label:r.label,href:"#units",show:i.units.length>0},{label:"이용 정보",href:"#info",show:!0},{label:"축제",href:"#festival",show:(((o=i.local.festivals)==null?void 0:o.length)??0)>0},{label:"주변 정보",href:"#guide",show:Kt(i,"local")},{label:"오시는 길",href:"#location",show:!0},{label:"FAQ",href:"#faq",show:i.faqs.length>0}].filter(f=>f.show);return n.jsx("header",{className:"border-line safe-t sticky top-0 z-40 w-full border-b backdrop-blur-md",style:{backgroundColor:"color-mix(in srgb, var(--tpl-surface, #fff) 88%, transparent)"},children:n.jsxs("div",{className:"shell flex h-16 items-center justify-between gap-3",children:[n.jsxs("a",{href:"#top",className:"flex min-w-0 items-center gap-2.5",children:[n.jsx("span",{className:"serif flex size-8 shrink-0 items-center justify-center rounded-md text-[length:var(--fs-sm)]",style:{backgroundColor:"var(--color-brand)",color:"var(--tpl-bg, #fff)",fontWeight:"var(--tpl-heading-weight, 700)"},"aria-hidden":!0,children:i.place.name.slice(0,1)}),n.jsxs("span",{className:"flex min-w-0 flex-col",children:[n.jsx("span",{className:"serif truncate text-[length:var(--fs-lead)] leading-tight",style:{fontWeight:"var(--tpl-heading-weight, 700)"},children:i.place.name}),i.place.englishName&&n.jsx("span",{className:"text-muted truncate text-[10px] uppercase leading-tight tracking-[0.14em]",children:i.place.englishName})]})]}),n.jsx("nav",{"aria-label":"주요 메뉴",className:"hidden items-center gap-7 lg:flex",children:u.map(f=>n.jsx("a",{href:f.href,className:"text-muted py-1 text-[length:var(--fs-sm)] font-medium transition-colors hover:text-current",children:f.label},f.label))}),n.jsxs("div",{className:"flex shrink-0 items-center gap-1.5",children:[i.place.phone&&n.jsxs("a",{href:`tel:${i.place.phone}`,className:"tap inline-flex items-center gap-1.5 rounded-lg px-3 text-[length:var(--fs-xs)] font-bold transition-opacity hover:opacity-90",style:{backgroundColor:"var(--color-brand)",color:"var(--tpl-bg, #fff)"},children:[n.jsx(Jt,{className:"size-4"}),n.jsx("span",{className:"hidden sm:inline",children:i.place.phone}),n.jsx("span",{className:"sr-only sm:hidden",children:"전화"})]}),n.jsxs("details",{className:"relative lg:hidden",children:[n.jsx("summary",{className:"tap flex cursor-pointer items-center justify-center rounded-lg marker:content-none [&::-webkit-details-marker]:hidden","aria-label":"메뉴 열기",children:n.jsx(np,{className:"size-5"})}),n.jsx("nav",{"aria-label":"전체 메뉴",className:"border-line tpl-border absolute right-0 top-[calc(100%+0.5rem)] z-50 w-52 overflow-hidden rounded-xl border shadow-lg",style:{backgroundColor:"var(--tpl-surface, #fff)"},children:n.jsx("ul",{className:"divide-line divide-y",children:u.map(f=>n.jsx("li",{children:n.jsx("a",{href:f.href,className:"tap flex items-center px-4 text-[length:var(--fs-sm)] font-medium",children:f.label})},f.label))})})]})]})]})})}function mc(i){return i.media.find(r=>{var u;return r.isPrimary&&((u=r.alt)==null?void 0:u.trim())})??i.media.find(r=>{var u;return(u=r.alt)==null?void 0:u.trim()})}function v0(i){return b0(i).join(" ")}function b0(i){const{place:r,facts:u}=i,o=[],f=r.addressLocality??r.addressRegion??r.roadAddress??r.address;o.push(f?`${f}에 있는 ${r.name}입니다.`:`${r.name}입니다.`);const m=_a(u,"check_in_time"),h=_a(u,"check_out_time");m&&h&&o.push(`체크인 ${m}, 체크아웃 ${h}.`);const x=_a(u,"business_hours")??_a(u,"open_hours");x&&o.push(`영업시간 ${x}.`);const v=_a(u,"pet_allowed");if(v==="false"&&o.push("반려동물 동반 불가."),v==="true"&&o.push("반려동물 동반 가능."),_a(u,"parking")==="true"){const b=_a(u,"parking_capacity");o.push(b?`주차 ${b}대 가능.`:"주차 가능.")}return r.phone&&o.push(`문의 ${r.phone}.`),o}const y0=6e3,j0=5;function N0(){const i=F(),{place:r,narrative:u}=i,o=Da(i),f=Sn(i),m=et(i),h=r.addressLocality??r.addressRegion,x=[...i.media].filter(N=>{var _;return(_=N.alt)==null?void 0:_.trim()}).sort((N,_)=>Number(_.isPrimary)-Number(N.isPrimary)).slice(0,j0),[v,p]=Y.useState(0),[b,y]=Y.useState(!1),E=Y.useRef(null),T=Y.useCallback(N=>p(_=>(_+N+x.length)%x.length),[x.length]);return Y.useEffect(()=>{if(b||x.length<2)return;const N=setInterval(()=>T(1),y0);return()=>clearInterval(N)},[b,T,x.length]),n.jsxs("section",{id:"top",className:"w-full",children:[n.jsxs("div",{className:"relative w-full overflow-hidden",style:{backgroundColor:"var(--tpl-inverse, #1c1917)",color:"var(--tpl-bg, #ffffff)",height:"clamp(24rem, 76vh, 42rem)"},onMouseEnter:()=>y(!0),onMouseLeave:()=>y(!1),onFocusCapture:()=>y(!0),onBlurCapture:()=>y(!1),onTouchStart:N=>{E.current=N.touches[0].clientX},onTouchEnd:N=>{const _=E.current;if(E.current=null,_===null)return;const U=N.changedTouches[0].clientX-_;Math.abs(U)>40&&T(U<0?1:-1)},children:[x.map((N,_)=>n.jsx("img",{src:N.url,alt:N.alt,fetchPriority:_===0?"high":"low",loading:_===0?void 0:"lazy",decoding:"async",className:"absolute inset-0 size-full object-cover object-center transition-opacity duration-700",style:{opacity:_===v?1:0}},N.mediaId)),n.jsx("div",{className:"pointer-events-none absolute inset-0",style:{background:"linear-gradient(to bottom, color-mix(in srgb, var(--tpl-inverse, #1c1917) 34%, transparent) 0%, color-mix(in srgb, var(--tpl-inverse, #1c1917) 16%, transparent) 45%, color-mix(in srgb, var(--tpl-inverse, #1c1917) 60%, transparent) 100%)"}}),n.jsxs("div",{className:"pointer-events-none absolute inset-0 flex flex-col items-center justify-center px-6 text-center",children:[h&&n.jsx("p",{className:"label opacity-85",children:h}),n.jsx("h1",{className:"serif mt-2",style:{fontSize:"var(--fs-display)",fontWeight:"var(--tpl-heading-weight, 700)",lineHeight:1.15},children:r.name}),(u.tagline??u.heroSubline)&&n.jsx("p",{className:"measure mt-3 text-[length:var(--fs-lead)] leading-relaxed opacity-90",children:u.tagline??u.heroSubline})]}),x.length>1&&n.jsxs(n.Fragment,{children:[n.jsx(wh,{dir:"prev",onClick:()=>T(-1)}),n.jsx(wh,{dir:"next",onClick:()=>T(1)}),n.jsx("ul",{className:"absolute inset-x-0 bottom-5 flex items-center justify-center gap-2",children:x.map((N,_)=>n.jsx("li",{children:n.jsx("button",{type:"button",onClick:()=>p(_),"aria-label":`${_+1}번째 사진`,"aria-current":_===v,className:`h-1.5 rounded-full bg-current transition-all ${_===v?"w-7 opacity-90":"w-1.5 opacity-45 hover:opacity-70"}`})},N.mediaId))})]})]}),(f||o.length>0||m.length>0||r.phone)&&n.jsx("div",{className:"border-line paper w-full border-b",style:{backgroundColor:"var(--tpl-surface-alt, #f5f5f4)"},children:n.jsxs("div",{className:"shell flex flex-col items-center gap-4 py-4 lg:flex-row lg:justify-between lg:gap-8",children:[n.jsxs("dl",{className:"divide-line flex flex-wrap justify-center gap-y-2 sm:divide-x",children:[f&&n.jsxs("div",{className:"px-5 text-center sm:first:pl-0",children:[n.jsx("dt",{className:"label",children:"1박 최저가"}),n.jsx("dd",{className:"text-[length:var(--fs-lead)] font-bold tabular-nums",children:f})]}),o.map(N=>n.jsxs("div",{className:"px-5 text-center",children:[n.jsx("dt",{className:"label",children:N.label}),n.jsx("dd",{className:"text-[length:var(--fs-lead)] font-bold",children:N.value})]},N.label))]}),n.jsxs("div",{className:"flex shrink-0 gap-2",children:[r.phone&&n.jsxs("a",{href:`tel:${r.phone}`,className:"tap border-line tpl-border inline-flex items-center gap-1.5 rounded-lg border px-4 text-[length:var(--fs-sm)] font-semibold",children:[n.jsx(Jt,{className:"size-4"}),n.jsx("span",{children:r.phone})]}),m.length>0&&n.jsxs("a",{href:m[0].url,target:"_blank",rel:"noopener noreferrer",className:"tap inline-flex items-center justify-center rounded-lg px-6 text-[length:var(--fs-sm)] font-bold transition-opacity hover:opacity-90",style:{backgroundColor:"var(--color-brand)",color:"var(--tpl-bg, #fff)"},children:[Pe(m[0])," 예약"]})]})]})})]})}function wh({dir:i,onClick:r}){const u=i==="prev"?Ma:Oa;return n.jsx("button",{type:"button",onClick:r,"aria-label":i==="prev"?"이전 사진":"다음 사진",className:`tap absolute top-1/2 hidden -translate-y-1/2 items-center justify-center rounded-full bg-current/15 px-3 backdrop-blur-sm transition-colors hover:bg-current/25 sm:inline-flex ${i==="prev"?"left-4":"right-4"}`,children:n.jsx(u,{className:"size-5"})})}function S0(){const i=F(),{place:r,narrative:u}=i,o=Da(i).slice(0,3),f=Sn(i),m=et(i),h=ic(i,"extra_person_fee"),x=[...i.media].filter(p=>{var b;return(b=p.alt)==null?void 0:b.trim()}).sort((p,b)=>Number(b.isPrimary)-Number(p.isPrimary))[0],v=r.addressLocality??r.addressRegion;return n.jsx("section",{id:"top",className:"border-line w-full border-b",children:n.jsxs("div",{className:"grid lg:grid-cols-2",children:[n.jsx("div",{className:"relative order-1 min-h-[16rem] lg:order-none lg:min-h-[34rem]",children:x&&n.jsx("img",{src:x.url,alt:x.alt,fetchPriority:"high",decoding:"async",className:"absolute inset-0 size-full object-cover"})}),n.jsxs("div",{className:"order-2 flex flex-col justify-center gap-6 px-6 py-10 sm:px-10 lg:order-none lg:px-14 lg:py-16",style:{backgroundColor:"var(--tpl-surface-alt, #f5f5f4)"},children:[n.jsxs("div",{children:[v&&n.jsx("p",{className:"label",children:v}),n.jsx("h1",{className:"serif mt-2",style:{fontSize:"var(--fs-display)",fontWeight:"var(--tpl-heading-weight, 700)",lineHeight:1.15},children:r.name}),n.jsx("p",{className:"measure mt-3 text-[length:var(--fs-lead)] leading-relaxed opacity-80",children:u.tagline??u.heroSubline??u.summary})]}),n.jsxs("div",{className:"panel p-5 sm:p-6",children:[f&&n.jsxs("p",{className:"flex items-baseline gap-2",children:[n.jsx("span",{className:"text-[length:var(--fs-h3)] font-bold tabular-nums",children:f}),n.jsx("span",{className:"text-muted text-[length:var(--fs-xs)]",children:"1박 기준"})]}),o.length>0&&n.jsxs("dl",{className:"divide-line mt-4 divide-y border-t pt-1",children:[o.map(p=>n.jsxs("div",{className:"flex items-baseline justify-between gap-4 py-2 text-[length:var(--fs-sm)]",children:[n.jsx("dt",{className:"text-muted",children:p.label}),n.jsx("dd",{className:"font-semibold",children:p.value})]},p.label)),h&&n.jsxs("div",{className:"flex items-baseline justify-between gap-4 py-2 text-[length:var(--fs-sm)]",children:[n.jsx("dt",{className:"text-muted",children:h.label}),n.jsx("dd",{className:"font-semibold tabular-nums",children:h.value})]})]}),n.jsxs("div",{className:"mt-5 flex flex-col gap-2 sm:flex-row",children:[m.length>0&&n.jsxs("a",{href:m[0].url,target:"_blank",rel:"noopener noreferrer",className:"tap flex flex-1 items-center justify-center rounded-lg px-6 text-[length:var(--fs-sm)] font-bold transition-opacity hover:opacity-90",style:{backgroundColor:"var(--color-brand)",color:"var(--tpl-bg, #fff)"},children:[Pe(m[0])," 예약"]}),r.phone&&n.jsxs("a",{href:`tel:${r.phone}`,className:"tap border-line tpl-border flex items-center justify-center gap-1.5 rounded-lg border px-5 text-[length:var(--fs-sm)] font-semibold",children:[n.jsx(Jt,{className:"size-4"}),n.jsx("span",{children:r.phone})]})]})]})]})]})})}const z0={"stay-reservation":"reservation","stay-oasi":"oasi","stay-studio":"studio","stay-pastel":"pastel","stay-editorial":"editorial"};function mp(i){return z0[i??""]??"default"}function hc(){return mp(F().theme.templateId)}const cc=5,$u=1.8,E0=3;function A0(i){const r=i*cc,u=f=>`${(f/r*100).toFixed(3)}%`;return`${i<2?"":` +.stay2-hero-slide { animation: stay2-hero-fade ${r}s ease-in-out infinite; } +@keyframes stay2-hero-fade { + 0% { opacity: 1; } + ${u(cc)} { opacity: 1; } + ${u(cc+$u)} { opacity: 0; } + ${u(r-$u)} { opacity: 0; } + 100% { opacity: 1; } +}`} +.stay2-hero-focus { animation: stay2-hero-focus 1.6s cubic-bezier(0.16, 1, 0.3, 1) both; } +@keyframes stay2-hero-focus { + 0% { opacity: 0; filter: blur(0.22em); } + 40% { opacity: 1; } + 100% { opacity: 1; filter: blur(0); } +} +@media (prefers-reduced-motion: reduce) { + .stay2-hero-slide, .stay2-hero-focus { animation: none; } +} +`}function T0(i,r){return((r-i)%r*cc+$u).toFixed(1)}function _0(){const i=F(),{place:r,narrative:u}=i,o=dt(i),f=et(i)[0],m=u.tagline??u.heroSubline,h=[...i.media].filter(p=>{var b;return(b=p.alt)==null?void 0:b.trim()}).sort((p,b)=>Number(b.isPrimary)-Number(p.isPrimary)),x=h.length<2?0:Math.min(h.length,E0),v=f?{href:f.url,label:`${Pe(f)}에서 예약`,external:!0}:r.phone?{href:`tel:${r.phone}`,label:`${r.phone} 예약 문의`,external:!1}:i.units.length>0?{href:"#units",label:`${o.label} 보기`,external:!1}:null;return n.jsxs("section",{id:"top",className:"w-full",children:[n.jsx("style",{dangerouslySetInnerHTML:{__html:A0(x)}}),n.jsxs("div",{className:"relative flex w-full items-center justify-center overflow-hidden",style:{backgroundColor:"var(--tpl-inverse, #1c1917)",color:"var(--tpl-bg, #ffffff)",height:"100svh"},children:[h.map((p,b)=>n.jsx("img",{src:p.url,alt:p.alt,fetchPriority:b===0?"high":b0?h:dc(i).slice(0,4),v=(o==null?void 0:o.caption)??r.name;return n.jsxs("section",{id:"top","data-oasi":!0,"aria-labelledby":"top-heading",className:"pt-12 lg:pt-16",children:[n.jsx("h1",{id:"top-heading",className:"text-[length:var(--fs-sm)] font-normal tracking-[0.08em] leading-[1.9]",children:r.name}),u.heroHeadline&&n.jsx("p",{className:"text-muted measure mt-3 text-[length:var(--fs-xs)] leading-[1.9]",children:u.heroHeadline}),o&&n.jsxs("figure",{className:"relative mx-auto mt-10 w-full max-w-[34rem]",children:[n.jsx("img",{src:o.url,alt:o.alt,fetchPriority:"high",decoding:"async",width:o.width,height:o.height,className:"block h-[clamp(17rem,27vw,24rem)] w-full object-cover"}),n.jsx("figcaption",{className:"text-muted absolute left-full top-0 ml-5 hidden max-h-full overflow-hidden text-ellipsis whitespace-nowrap text-[length:var(--fs-xs)] tracking-[0.14em] xl:block",style:{writingMode:"vertical-rl"},children:v})]}),(f||x.length>0)&&n.jsxs("dl",{className:"border-line mt-10 flex flex-wrap gap-x-10 gap-y-4 border-t pt-5 text-[length:var(--fs-xs)] leading-[1.9]",children:[f&&n.jsxs("div",{children:[n.jsx("dt",{className:"text-muted",children:"최저가"}),n.jsx("dd",{children:f})]}),x.map(p=>n.jsxs("div",{children:[n.jsx("dt",{className:"text-muted",children:p.label}),n.jsx("dd",{children:p.value})]},p.label))]}),m.length>0&&n.jsx("ul",{className:"mt-6 flex flex-wrap items-center gap-x-8",children:m.map(p=>n.jsx("li",{children:n.jsxs("a",{href:p.url,target:"_blank",rel:"noopener noreferrer",className:"tap inline-flex items-center text-[length:var(--fs-sm)] underline underline-offset-[6px]",style:{color:"var(--color-brand)"},children:[Pe(p)," 예약"]})},p.url))})]})}function C0(){const i=F(),r=eo(i),u=mc(i),o=u&&r.some(f=>f.mediaId===u.mediaId)?[u,...r.filter(f=>f.mediaId!==u.mediaId)]:r;return o.length===0?null:n.jsx("section",{id:"top","aria-label":`${i.place.name} 사진`,className:"w-full",children:n.jsx("div",{className:"columns-1 gap-2 sm:columns-2",children:o.map((f,m)=>n.jsx("figure",{className:"mb-2 break-inside-avoid",children:n.jsx("img",{src:f.url,alt:f.alt,loading:m===0?"eager":"lazy",fetchPriority:m===0?"high":void 0,decoding:"async",width:f.width,height:f.height,className:"block h-auto w-full"})},f.mediaId))})})}function M0(){const i=F(),{place:r,narrative:u}=i,o=Da(i),f=Sn(i),m=et(i),h=u.tagline??u.summary,x=r.addressLocality??r.addressRegion,v=[...i.media].filter(p=>{var b;return(b=p.alt)==null?void 0:b.trim()}).sort((p,b)=>Number(b.isPrimary)-Number(p.isPrimary)).slice(0,3);return n.jsxs("section",{id:"top","data-pastel":"own",className:"w-full",children:[n.jsxs("div",{className:"relative w-full overflow-hidden",style:{backgroundColor:"var(--pastel-1)"},children:[n.jsxs("div",{"aria-hidden":!0,className:"pointer-events-none absolute inset-0",children:[n.jsx("span",{className:"absolute rounded-full",style:{width:"clamp(12rem, 26vw, 22rem)",aspectRatio:"1",backgroundColor:"var(--pastel-2)",right:"-6%",top:"-6%",opacity:.75}}),n.jsx("span",{className:"absolute rounded-full",style:{width:"clamp(8rem, 18vw, 15rem)",aspectRatio:"1",backgroundColor:"var(--pastel-3)",left:"46%",bottom:"-10%",opacity:.7}})]}),n.jsxs("div",{className:"shell relative grid items-center gap-8 lg:grid-cols-2 lg:gap-14",style:{paddingBlock:"var(--section-space)"},children:[n.jsxs("div",{className:"order-2 lg:order-1",children:[x&&n.jsx("p",{className:"inline-flex items-center rounded-full px-3 py-1 text-[length:var(--fs-xs)] font-extrabold",style:{backgroundColor:"var(--tpl-bg, #fff)",color:"var(--pastel-ink)"},children:x}),n.jsx("h1",{className:"mt-4 break-keep",style:{fontSize:"var(--fs-display)",fontWeight:800,lineHeight:1.2,letterSpacing:"var(--tpl-heading-tracking, -0.03em)"},children:r.name}),h&&n.jsx("p",{className:"measure mt-4 text-[length:var(--fs-lead)] font-medium leading-relaxed opacity-80",children:h}),m.length>0&&n.jsxs("a",{href:m[0].url,target:"_blank",rel:"noopener noreferrer",className:"tap mt-7 inline-flex items-center justify-center rounded-full px-8 text-[length:var(--fs-sm)] font-extrabold transition-opacity hover:opacity-90",style:{backgroundColor:"var(--pastel-ink)",color:"var(--tpl-bg, #fff)"},children:[Pe(m[0]),"로 예약"]})]}),n.jsxs("div",{className:"order-1 lg:order-2",children:[v[0]&&n.jsx("div",{className:"relative w-full overflow-hidden",style:{aspectRatio:"1 / 1",borderRadius:"calc(var(--tpl-radius, 24px) * 1.6)",boxShadow:"0 0 0 clamp(6px, 1.4vw, 12px) var(--tpl-bg, #fff)"},children:n.jsx("img",{src:v[0].url,alt:v[0].alt,fetchPriority:"high",decoding:"async",className:"size-full object-cover"})}),v.length>1&&n.jsx("ul",{className:"mt-3 grid grid-cols-2 gap-3",children:v.slice(1).map(p=>n.jsx("li",{className:"relative overflow-hidden",style:{aspectRatio:"4 / 3",borderRadius:"var(--tpl-radius, 24px)",boxShadow:"0 0 0 clamp(4px, 1vw, 8px) var(--tpl-bg, #fff)"},children:n.jsx("img",{src:p.url,alt:p.alt,loading:"lazy",decoding:"async",className:"size-full object-cover"})},p.mediaId))})]})]})]}),(f||o.length>0)&&n.jsx("div",{className:"w-full",style:{backgroundColor:"var(--pastel-2)"},children:n.jsxs("dl",{className:"shell flex flex-wrap items-center justify-center gap-x-12 gap-y-5 py-8 text-center",children:[f&&n.jsxs("div",{children:[n.jsx("dt",{className:"text-[length:var(--fs-xs)] font-extrabold",style:{color:"var(--tpl-bg, #fff)",opacity:.85},children:"최저가"}),n.jsx("dd",{className:"mt-1 font-extrabold tabular-nums",style:{fontSize:"var(--fs-h3)",color:"var(--tpl-bg, #fff)"},children:f})]}),o.map(p=>n.jsxs("div",{children:[n.jsx("dt",{className:"text-[length:var(--fs-xs)] font-extrabold",style:{color:"var(--tpl-bg, #fff)",opacity:.85},children:p.label}),n.jsx("dd",{className:"mt-1 font-extrabold",style:{fontSize:"var(--fs-h3)",color:"var(--tpl-bg, #fff)"},children:p.value})]},p.label))]})})]})}const Ms="repeating-linear-gradient(90deg, color-mix(in oklab, currentColor 3%, transparent) 0 1px, transparent 1px 6px),repeating-linear-gradient(0deg, color-mix(in oklab, currentColor 2%, transparent) 0 1px, transparent 1px 6px)",O0={intro:"about",info:"info",rooms:"units",menu:"units",programs:"units",pricing:"pricing",booking:"booking",space:"space",inquiry:"inquiry",exhibition:"exhibition",photos:"gallery",local:"guide",weather:"weather",map:"location",faq:"faq",songs:"songs",daily:"daily",chronicle:"chronicle",people:"people",quiz:"quiz",postcard:"postcard",video:"video",itinerary:"itinerary"},D0={about:"소개",info:"이용 정보",pricing:"요금",booking:"예약 안내",space:"공간",inquiry:"문의",exhibition:"관람 안내",gallery:"사진",guide:"주변",weather:"날씨",location:"오시는 길",faq:"자주 묻는 질문",songs:"노래",daily:"일력",chronicle:"연표",people:"인물",quiz:"퀴즈",postcard:"엽서",video:"영상",itinerary:"일정"};function Ch(i){return String(i).padStart(2,"0")}function hp(){var f;const i=F(),r=dt(i),u=new Set,o=[];for(const m of i0(i)){const h=O0[m.id];if(!h||u.has(h)||h==="units"&&i.units.length===0)continue;u.add(h);const x=(f=m.name)==null?void 0:f.trim();o.push({no:Ch(o.length+1),label:x||D0[h]||r.label,anchor:h})}return u.has("location")||o.push({no:Ch(o.length+1),label:"오시는 길",anchor:"location"}),o}function to({id:i,title:r,lead:u,aside:o}){var m;const f=(m=hp().find(h=>h.anchor===i))==null?void 0:m.no;return n.jsxs("header",{className:"mb-7 sm:mb-10",children:[n.jsx("div",{"aria-hidden":!0,className:"mb-5 h-0.5 w-full bg-current opacity-75"}),n.jsxs("div",{className:"flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between lg:gap-10",children:[n.jsxs("div",{className:"relative min-w-0 flex-1",children:[f&&n.jsx("span",{"aria-hidden":!0,className:"serif pointer-events-none absolute left-0 top-1/2 -translate-y-1/2 select-none leading-none tabular-nums",style:{fontSize:"clamp(2.5rem, 7vw, 5rem)",fontWeight:800,opacity:.12},children:f}),n.jsx("h2",{id:`${i}-heading`,className:"h2 relative pl-9 sm:pl-14 xl:pl-20",style:{fontWeight:800},children:r}),u&&n.jsx("p",{className:"text-muted measure relative mt-2 pl-9 font-serif text-[length:var(--fs-sm)] leading-relaxed sm:pl-14 xl:pl-20",children:u})]}),o&&n.jsx("div",{className:"shrink-0",children:o})]})]})}function k0(){const i=F(),{place:r,narrative:u}=i,o=mc(i),f=dt(i),m=ll(i),h=Da(i),x=Sn(i),v=et(i)[0],p=Ls(i)[0],b=u.tagline??u.heroSubline,E=[r.addressSubLocality??r.addressLocality??r.addressRegion,m.length>0?`${f.label} ${m.length}개`:void 0,x].filter(T=>!!T);return n.jsx("section",{id:"top",className:"relative w-full",style:{backgroundImage:Ms},children:n.jsxs("div",{className:"grid grid-cols-12",children:[o&&n.jsx("div",{className:"relative col-span-12 xl:col-span-8 xl:col-start-5 xl:row-start-1",style:{minHeight:"clamp(15rem, 52vh, 34rem)",backgroundColor:"var(--tpl-inverse)"},children:n.jsx("img",{src:o.url,alt:o.alt,fetchPriority:"high",decoding:"async",width:o.width,height:o.height,className:"absolute inset-0 size-full object-cover"})}),n.jsx("div",{className:"z-10 col-span-12 xl:col-start-1 xl:row-start-1 xl:mb-12 xl:mr-16 xl:self-end",style:{backgroundColor:"color-mix(in srgb, var(--tpl-inverse) 88%, transparent)",color:"var(--tpl-bg)"},children:n.jsx("h1",{className:"serif px-4 py-5 sm:px-6 xl:py-8 xl:pl-10 xl:pr-16",style:{fontSize:"clamp(2.25rem, 7.5vw, 5.5rem)",fontWeight:800,lineHeight:.98,letterSpacing:"-0.035em"},children:r.name})}),n.jsxs("div",{className:"col-span-12 flex flex-col gap-5 px-4 py-8 sm:px-6 xl:col-span-4 xl:col-start-1 xl:row-start-1 xl:py-12 xl:pb-56 xl:pl-10 xl:pr-8",children:[b&&n.jsx("p",{className:"measure font-serif text-[length:var(--fs-lead)] leading-relaxed",children:b}),E.length>0&&n.jsx("p",{className:"text-[length:var(--fs-sm)] font-semibold tabular-nums",children:E.join(" · ")}),(v||r.phone||p)&&n.jsxs("div",{className:"flex flex-wrap items-center gap-x-6 gap-y-1 text-[length:var(--fs-sm)] font-bold",children:[v&&n.jsxs("a",{href:v.url,target:"_blank",rel:"noopener noreferrer",className:"tap inline-flex items-center underline underline-offset-4",children:[Pe(v)," 예약"]}),r.phone&&n.jsx("a",{href:`tel:${r.phone}`,className:"tap inline-flex items-center underline underline-offset-4",children:r.phone}),p&&n.jsxs("a",{href:p.url,target:"_blank",rel:"noopener noreferrer",className:"tap inline-flex items-center underline underline-offset-4",children:[Rl(p)," 문의"]})]}),h.length>0&&n.jsx("dl",{className:"mt-1",children:h.map(T=>n.jsxs("div",{className:"border-line flex items-baseline justify-between gap-4 border-t py-2 text-[length:var(--fs-sm)]",children:[n.jsx("dt",{className:"text-muted shrink-0",children:T.label}),n.jsx("dd",{className:"text-right font-semibold",children:T.value})]},T.label))})]})]})})}function U0(){var v;const i=F(),r=hc();if(r==="reservation")return n.jsx(_0,{});if(r==="oasi")return n.jsx(w0,{});if(r==="studio")return n.jsx(C0,{});if(r==="pastel")return n.jsx(M0,{});if(r==="editorial")return n.jsx(k0,{});const u=(v=i.theme.sections.find(p=>p.id==="hero"))==null?void 0:v.variantId;if(u==="hero.slideshow")return n.jsx(N0,{});if(u==="hero.split")return n.jsx(S0,{});const{place:o,narrative:f}=i,m=mc(i),h=dt(i);et(i);const x=o.addressLocality??o.addressRegion;return n.jsx("section",{id:"top",className:"w-full",children:n.jsxs("div",{className:"relative flex w-full items-end overflow-hidden",style:{backgroundColor:"var(--tpl-inverse, #1c1917)",color:"var(--tpl-bg, #ffffff)",minHeight:"clamp(24rem, 62vh, 36rem)"},children:[m&&n.jsxs("div",{className:"absolute inset-0 z-0",children:[n.jsx("img",{src:m.url,alt:m.alt,fetchPriority:"high",decoding:"async",width:m.width,height:m.height,className:"size-full object-cover object-center"}),n.jsx("div",{className:"absolute inset-0",style:{background:"linear-gradient(to top, color-mix(in srgb, var(--tpl-inverse, #1c1917) 88%, transparent) 0%, color-mix(in srgb, var(--tpl-inverse, #1c1917) 45%, transparent) 34%, transparent 66%)"}})]}),n.jsxs("div",{className:"shell relative z-10 pb-10 pt-24 sm:pb-12",children:[x&&n.jsxs("p",{className:"mb-3 inline-flex items-center gap-1 text-[length:var(--fs-xs)] opacity-80",children:[n.jsx(Rs,{className:"size-3.5"}),n.jsx("span",{children:x})]}),n.jsx("h1",{className:"serif",style:{fontSize:"var(--fs-display)",fontWeight:"var(--tpl-heading-weight, 700)",lineHeight:1.15},children:o.name}),(f.tagline??f.heroSubline??f.summary)&&n.jsx("p",{className:"measure mt-3 text-[length:var(--fs-lead)] leading-relaxed opacity-85",children:f.tagline??f.heroSubline??f.summary}),i.units.length>0&&n.jsxs("a",{href:"#units",className:"mt-6 inline-flex items-center gap-1 text-[length:var(--fs-sm)] font-medium underline-offset-4 opacity-80 transition-opacity hover:opacity-100",children:[n.jsxs("span",{children:[h.label," 보기"]}),n.jsx(oc,{className:"size-4"})]})]})]})})}function lo(i){if(!i)return"";const r=new Date(i);return Number.isNaN(r.getTime())?"":`${r.getFullYear()}년 ${r.getMonth()+1}월 ${r.getDate()}일`}function qs(i){if(!i)return"";const r=new Date(i);return Number.isNaN(r.getTime())?"":r.toLocaleDateString("sv-SE",{timeZone:"Asia/Seoul"})}function Vu(i){return`https://search.naver.com/search.naver?query=${encodeURIComponent(i)}`}function R0(i){return`https://www.youtube.com/results?search_query=${encodeURIComponent(i)}`}function ao(i){return i.replace(/,/g," ").replace(/\s+/g," ").trim()}function H0(i,r,u){return r==null||u==null?`https://map.kakao.com/link/search/${encodeURIComponent(i)}`:`https://map.kakao.com/link/to/${encodeURIComponent(ao(i))},${r},${u}`}function L0(i,r){const u=o=>`${o.lng},${o.lat},${encodeURIComponent(ao(o.name))}`;return`https://map.naver.com/p/directions/${u(i)}/${u(r)}/-/car`}function pp(i,r,u){return r==null||u==null?`https://map.naver.com/p/search/${encodeURIComponent(i)}`:`https://map.naver.com/p/directions/-/${u},${r},${encodeURIComponent(ao(i))}/-/transit`}function q0(i,r,u=16/9,o=.0022){const f=Math.max(Math.cos(i*Math.PI/180),.01),m=u*o/f;return`https://www.openstreetmap.org/export/embed.html?bbox=${[r-m,i-o,r+m,i+o].map(x=>x.toFixed(6)).join(",")}&layer=mapnik&marker=${i},${r}`}function B0(i,r,u){if(r==null||u==null)return;const o=encodeURIComponent(i);return`tmap://route?goalname=${o}&goalx=${u}&goaly=${r}&rGoName=${o}&rGoX=${u}&rGoY=${r}`}function Mh(){const i=F(),r=dc(i).slice(0,6),u=i.narrative.summary??v0(i);return r.length===0&&!u?null:n.jsx("section",{id:"summary","aria-labelledby":"summary-heading",className:"border-line w-full border-b py-10 sm:py-14",style:{backgroundColor:"var(--tpl-surface-alt, #f5f5f4)"},children:n.jsx("div",{className:"shell",children:n.jsxs("div",{className:"grid gap-8 lg:grid-cols-12 lg:gap-12",children:[n.jsxs("div",{className:"lg:col-span-5",children:[n.jsx("h2",{id:"summary-heading",className:"h3",children:"예약 전 확인"}),u&&n.jsx("p",{className:"measure mt-3 text-[length:var(--fs-body)] leading-relaxed",children:u}),n.jsxs("p",{className:"text-muted mt-3 text-[length:var(--fs-xs)]",children:[lo(i.site.updatedAt)," 기준, 사업자가 확인한 정보입니다."]})]}),r.length>0&&n.jsx("dl",{className:"grid grid-cols-1 gap-x-10 lg:col-span-7 xl:grid-cols-2",children:r.map(o=>n.jsxs("div",{className:"border-line flex flex-col gap-0.5 py-2.5 sm:flex-row sm:gap-4 sm:border-b",children:[n.jsx("dt",{className:"text-muted shrink-0 text-[length:var(--fs-sm)] sm:w-28",children:o.label}),n.jsx("dd",{className:"text-[length:var(--fs-sm)] font-semibold",children:o.value})]},o.label))})]})})})}function no({id:i,title:r,lead:u,aside:o}){return n.jsxs("header",{className:"mb-10 flex flex-col items-center gap-4 text-center sm:mb-14",children:[n.jsx("h2",{id:`${i}-heading`,className:"serif max-w-full",style:{fontSize:"var(--fs-h2)",fontWeight:300,letterSpacing:"0.04em",lineHeight:1.35},children:r}),n.jsx("i",{"aria-hidden":!0,className:"block h-px w-10 bg-current opacity-25"}),u&&n.jsx("p",{className:"text-muted measure text-[length:var(--fs-sm)] leading-loose",children:u}),o&&n.jsx("div",{className:"text-[length:var(--fs-xs)]",children:o})]})}function so({id:i,title:r,lead:u,aside:o}){return n.jsxs("header",{className:"mb-8 flex flex-col gap-3 sm:flex-row sm:items-baseline sm:justify-between sm:gap-8",children:[n.jsxs("div",{className:"min-w-0",children:[n.jsxs("h2",{id:`${i}-heading`,className:"text-[length:var(--fs-sm)] font-normal tracking-[0.08em]",children:["[",r,"]"]}),u&&n.jsx("p",{className:"text-muted measure mt-3 text-[length:var(--fs-xs)] leading-[1.9]",children:u})]}),o&&n.jsx("div",{className:"shrink-0 text-[length:var(--fs-xs)]",children:o})]})}function io({id:i,title:r,lead:u,aside:o}){return n.jsx("header",{className:"mb-7 sm:mb-10",children:n.jsxs("div",{className:"flex flex-col gap-3 sm:flex-row sm:items-baseline sm:justify-between sm:gap-10",children:[n.jsxs("div",{className:"min-w-0",children:[n.jsx("h2",{id:`${i}-heading`,className:"serif min-w-0 font-extrabold",style:{fontSize:"var(--fs-h2)",letterSpacing:"0.01em",lineHeight:1.2,color:"var(--tpl-text)"},children:r}),u&&n.jsx("p",{className:"text-muted measure mt-2 text-[length:var(--fs-sm)]",style:{lineHeight:1.7},children:u})]}),o&&n.jsx("div",{className:"shrink-0 text-[length:var(--fs-xs)]",children:o})]})})}function co({id:i,title:r,lead:u,aside:o}){return n.jsxs("header",{className:"mb-8 flex flex-col items-center gap-3 text-center sm:mb-10",children:[n.jsx("i",{"aria-hidden":!0,className:"block h-[6px] w-10 rounded-full",style:{backgroundColor:"var(--pastel-2, currentColor)",opacity:.9}}),n.jsx("h2",{id:`${i}-heading`,className:"h2 break-keep",style:{fontSize:"calc(var(--fs-h2) * 1.15)",fontWeight:800},children:r}),u&&n.jsx("p",{className:"measure text-[length:var(--fs-sm)] leading-relaxed opacity-65",children:u}),o&&n.jsx("div",{className:"text-[length:var(--fs-sm)]",children:o})]})}const Y0={base:{backgroundColor:"var(--tpl-surface, #ffffff)"},alt:{backgroundColor:"var(--tpl-surface-alt, #f5f5f4)"},dark:{backgroundColor:"var(--tpl-inverse, #1c1917)",color:"var(--tpl-bg, #ffffff)"}};function pt({id:i,title:r,lead:u,tone:o="base",aside:f,footnote:m,children:h,wide:x,bare:v}){const p=hc(),b=p==="reservation"?no:p==="oasi"?so:p==="studio"?io:p==="pastel"?co:p==="editorial"?to:G0;return n.jsx("section",{id:i,"aria-labelledby":`${i}-heading`,className:"border-line paper w-full border-b",style:{...Y0[o],paddingBlock:"var(--section-space)"},children:n.jsxs("div",{className:x?"w-full":"shell",children:[!v&&n.jsx("div",{className:x?"shell":void 0,children:n.jsx(b,{id:i,title:r,lead:u,aside:f})}),h,m&&n.jsx("p",{className:`text-muted mt-5 text-[length:var(--fs-xs)] ${x?"shell":""}`,children:m})]})})}function G0({id:i,title:r,lead:u,aside:o}){return n.jsx("header",{className:"mb-6 sm:mb-8",children:n.jsxs("div",{className:"flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between sm:gap-6",children:[n.jsxs("div",{className:"min-w-0",children:[n.jsxs("h2",{id:`${i}-heading`,className:"h2 flex items-center gap-2.5",children:[n.jsx("i",{"aria-hidden":!0,className:"h-[1em] w-[3px] shrink-0 bg-current opacity-25"}),n.jsx("span",{className:"min-w-0",children:r})]}),u&&n.jsx("p",{className:"text-muted measure mt-2 pl-[calc(3px+0.625rem)] text-[length:var(--fs-sm)]",children:u})]}),o&&n.jsx("div",{className:"shrink-0",children:o})]})})}function Q0(i){return Object.prototype.toString.call(i)==="[object Object]"}function Oh(i){return Q0(i)||Array.isArray(i)}function X0(){return!!(typeof window<"u"&&window.document&&window.document.createElement)}function ro(i,r){const u=Object.keys(i),o=Object.keys(r);if(u.length!==o.length)return!1;const f=JSON.stringify(Object.keys(i.breakpoints||{})),m=JSON.stringify(Object.keys(r.breakpoints||{}));return f!==m?!1:u.every(h=>{const x=i[h],v=r[h];return typeof x=="function"?`${x}`==`${v}`:!Oh(x)||!Oh(v)?x===v:ro(x,v)})}function Dh(i){return i.concat().sort((r,u)=>r.name>u.name?1:-1).map(r=>r.options)}function $0(i,r){if(i.length!==r.length)return!1;const u=Dh(i),o=Dh(r);return u.every((f,m)=>{const h=o[m];return ro(f,h)})}function uo(i){return typeof i=="number"}function Zu(i){return typeof i=="string"}function pc(i){return typeof i=="boolean"}function kh(i){return Object.prototype.toString.call(i)==="[object Object]"}function Ge(i){return Math.abs(i)}function oo(i){return Math.sign(i)}function Ts(i,r){return Ge(i-r)}function V0(i,r){if(i===0||r===0||Ge(i)<=Ge(r))return 0;const u=Ts(Ge(i),Ge(r));return Ge(u/i)}function Z0(i){return Math.round(i*100)/100}function Os(i){return Ds(i).map(Number)}function tl(i){return i[Bs(i)]}function Bs(i){return Math.max(0,i.length-1)}function fo(i,r){return r===Bs(i)}function Uh(i,r=0){return Array.from(Array(i),(u,o)=>r+o)}function Ds(i){return Object.keys(i)}function xp(i,r){return[i,r].reduce((u,o)=>(Ds(o).forEach(f=>{const m=u[f],h=o[f],x=kh(m)&&kh(h);u[f]=x?xp(m,h):h}),u),{})}function Ku(i,r){return typeof r.MouseEvent<"u"&&i instanceof r.MouseEvent}function K0(i,r){const u={start:o,center:f,end:m};function o(){return 0}function f(v){return m(v)/2}function m(v){return r-v}function h(v,p){return Zu(i)?u[i](v):i(r,v,p)}return{measure:h}}function ks(){let i=[];function r(f,m,h,x={passive:!0}){let v;if("addEventListener"in f)f.addEventListener(m,h,x),v=()=>f.removeEventListener(m,h,x);else{const p=f;p.addListener(h),v=()=>p.removeListener(h)}return i.push(v),o}function u(){i=i.filter(f=>f())}const o={add:r,clear:u};return o}function J0(i,r,u,o){const f=ks(),m=1e3/60;let h=null,x=0,v=0;function p(){f.add(i,"visibilitychange",()=>{i.hidden&&N()})}function b(){T(),f.clear()}function y(U){if(!v)return;h||(h=U,u(),u());const q=U-h;for(h=U,x+=q;x>=m;)u(),x-=m;const Q=x/m;o(Q),v&&(v=r.requestAnimationFrame(y))}function E(){v||(v=r.requestAnimationFrame(y))}function T(){r.cancelAnimationFrame(v),h=null,x=0,v=0}function N(){h=null,x=0}return{init:p,destroy:b,start:E,stop:T,update:u,render:o}}function W0(i,r){const u=r==="rtl",o=i==="y",f=o?"y":"x",m=o?"x":"y",h=!o&&u?-1:1,x=b(),v=y();function p(N){const{height:_,width:U}=N;return o?_:U}function b(){return o?"top":u?"right":"left"}function y(){return o?"bottom":u?"left":"right"}function E(N){return N*h}return{scroll:f,cross:m,startEdge:x,endEdge:v,measureSize:p,direction:E}}function Ca(i=0,r=0){const u=Ge(i-r);function o(p){return pr}function m(p){return o(p)||f(p)}function h(p){return m(p)?o(p)?i:r:p}function x(p){return u?p-u*Math.ceil((p-r)/u):p}return{length:u,max:r,min:i,constrain:h,reachedAny:m,reachedMax:f,reachedMin:o,removeOffset:x}}function gp(i,r,u){const{constrain:o}=Ca(0,i),f=i+1;let m=h(r);function h(E){return u?Ge((f+E)%f):o(E)}function x(){return m}function v(E){return m=h(E),y}function p(E){return b().set(x()+E)}function b(){return gp(i,x(),u)}const y={get:x,set:v,add:p,clone:b};return y}function F0(i,r,u,o,f,m,h,x,v,p,b,y,E,T,N,_,U,q,Q){const{cross:V,direction:ne}=i,de=["INPUT","SELECT","TEXTAREA"],le={passive:!1},X=ks(),ee=ks(),se=Ca(50,225).constrain(T.measure(20)),Ne={mouse:300,touch:400},oe={mouse:500,touch:600},ye=N?43:25;let Ye=!1,Qe=0,te=0,O=!1,B=!1,W=!1,me=!1;function je(J){if(!Q)return;function _e(tt){(pc(Q)||Q(J,tt))&&re(tt)}const Oe=r;X.add(Oe,"dragstart",tt=>tt.preventDefault(),le).add(Oe,"touchmove",()=>{},le).add(Oe,"touchend",()=>{}).add(Oe,"touchstart",_e).add(Oe,"mousedown",_e).add(Oe,"touchcancel",Ee).add(Oe,"contextmenu",Ee).add(Oe,"click",Ae,!0)}function S(){X.clear(),ee.clear()}function H(){const J=me?u:r;ee.add(J,"touchmove",ie,le).add(J,"touchend",Ee).add(J,"mousemove",ie,le).add(J,"mouseup",Ee)}function G(J){const _e=J.nodeName||"";return de.includes(_e)}function $(){return(N?oe:Ne)[me?"mouse":"touch"]}function ae(J,_e){const Oe=y.add(oo(J)*-1),tt=b.byDistance(J,!N).distance;return N||Ge(J)=2,!(_e&&J.button!==0)&&(G(J.target)||(O=!0,m.pointerDown(J),p.useFriction(0).useDuration(0),f.set(h),H(),Qe=m.readPoint(J),te=m.readPoint(J,V),E.emit("pointerDown")))}function ie(J){if(!Ku(J,o)&&J.touches.length>=2)return Ee(J);const Oe=m.readPoint(J),tt=m.readPoint(J,V),bt=Ts(Oe,Qe),Wt=Ts(tt,te);if(!B&&!me&&(!J.cancelable||(B=bt>Wt,!B)))return Ee(J);const nl=m.pointerMove(J);bt>_&&(W=!0),p.useFriction(.3).useDuration(.75),x.start(),f.add(ne(nl)),J.preventDefault()}function Ee(J){const Oe=b.byDistance(0,!1).index!==y.get(),tt=m.pointerUp(J)*$(),bt=ae(ne(tt),Oe),Wt=V0(tt,bt),nl=ye-10*Wt,kt=q+Wt/50;B=!1,O=!1,ee.clear(),p.useDuration(nl).useFriction(kt),v.distance(bt,!N),me=!1,E.emit("pointerUp")}function Ae(J){W&&(J.stopPropagation(),J.preventDefault(),W=!1)}function nt(){return O}return{init:je,destroy:S,pointerDown:nt}}function I0(i,r){let o,f;function m(y){return y.timeStamp}function h(y,E){const N=`client${(E||i.scroll)==="x"?"X":"Y"}`;return(Ku(y,r)?y:y.touches[0])[N]}function x(y){return o=y,f=y,h(y)}function v(y){const E=h(y)-h(f),T=m(y)-m(o)>170;return f=y,T&&(o=y),E}function p(y){if(!o||!f)return 0;const E=h(f)-h(o),T=m(y)-m(o),N=m(y)-m(f)>170,_=E/T;return T&&!N&&Ge(_)>.1?_:0}return{pointerDown:x,pointerMove:v,pointerUp:p,readPoint:h}}function P0(){function i(u){const{offsetTop:o,offsetLeft:f,offsetWidth:m,offsetHeight:h}=u;return{top:o,right:f+m,bottom:o+h,left:f,width:m,height:h}}return{measure:i}}function eb(i){function r(o){return i*(o/100)}return{measure:r}}function tb(i,r,u,o,f,m,h){const x=[i].concat(o);let v,p,b=[],y=!1;function E(U){return f.measureSize(h.measure(U))}function T(U){if(!m)return;p=E(i),b=o.map(E);function q(Q){for(const V of Q){if(y)return;const ne=V.target===i,de=o.indexOf(V.target),le=ne?p:b[de],X=E(ne?i:o[de]);if(Ge(X-le)>=.5){U.reInit(),r.emit("resize");break}}}v=new ResizeObserver(Q=>{(pc(m)||m(U,Q))&&q(Q)}),u.requestAnimationFrame(()=>{x.forEach(Q=>v.observe(Q))})}function N(){y=!0,v&&v.disconnect()}return{init:T,destroy:N}}function lb(i,r,u,o,f,m){let h=0,x=0,v=f,p=m,b=i.get(),y=0;function E(){const le=o.get()-i.get(),X=!v;let ee=0;return X?(h=0,u.set(o),i.set(o),ee=le):(u.set(i),h+=le/v,h*=p,b+=h,i.add(h),ee=b-y),x=oo(ee),y=b,de}function T(){const le=o.get()-r.get();return Ge(le)<.001}function N(){return v}function _(){return x}function U(){return h}function q(){return V(f)}function Q(){return ne(m)}function V(le){return v=le,de}function ne(le){return p=le,de}const de={direction:_,duration:N,velocity:U,seek:E,settled:T,useBaseFriction:Q,useBaseDuration:q,useFriction:ne,useDuration:V};return de}function ab(i,r,u,o,f){const m=f.measure(10),h=f.measure(50),x=Ca(.1,.99);let v=!1;function p(){return!(v||!i.reachedAny(u.get())||!i.reachedAny(r.get()))}function b(T){if(!p())return;const N=i.reachedMin(r.get())?"min":"max",_=Ge(i[N]-r.get()),U=u.get()-r.get(),q=x.constrain(_/h);u.subtract(U*q),!T&&Ge(U){const{min:U,max:q}=m,Q=m.constrain(N),V=!_,ne=fo(u,_);return V?q:ne||p(U,Q)?U:p(q,Q)?q:Q}).map(N=>parseFloat(N.toFixed(3)))}function E(){if(r<=i+f)return[m.max];if(o==="keepSnaps")return h;const{min:N,max:_}=x;return h.slice(N,_)}return{snapsContained:v,scrollContainLimit:x}}function sb(i,r,u){const o=r[0],f=u?o-i:tl(r);return{limit:Ca(f,o)}}function ib(i,r,u,o){const m=r.min+.1,h=r.max+.1,{reachedMin:x,reachedMax:v}=Ca(m,h);function p(E){return E===1?v(u.get()):E===-1?x(u.get()):!1}function b(E){if(!p(E))return;const T=i*(E*-1);o.forEach(N=>N.add(T))}return{loop:b}}function cb(i){const{max:r,length:u}=i;function o(m){const h=m-r;return u?h/-u:0}return{get:o}}function rb(i,r,u,o,f){const{startEdge:m,endEdge:h}=i,{groupSlides:x}=f,v=y().map(r.measure),p=E(),b=T();function y(){return x(o).map(_=>tl(_)[h]-_[0][m]).map(Ge)}function E(){return o.map(_=>u[m]-_[m]).map(_=>-Ge(_))}function T(){return x(p).map(_=>_[0]).map((_,U)=>_+v[U])}return{snaps:p,snapsAligned:b}}function ub(i,r,u,o,f,m){const{groupSlides:h}=f,{min:x,max:v}=o,p=b();function b(){const E=h(m),T=!i||r==="keepSnaps";return u.length===1?[m]:T?E:E.slice(x,v).map((N,_,U)=>{const q=!_,Q=fo(U,_);if(q){const V=tl(U[0])+1;return Uh(V)}if(Q){const V=Bs(m)-tl(U)[0]+1;return Uh(V,tl(U)[0])}return N})}return{slideRegistry:p}}function ob(i,r,u,o,f){const{reachedAny:m,removeOffset:h,constrain:x}=o;function v(N){return N.concat().sort((_,U)=>Ge(_)-Ge(U))[0]}function p(N){const _=i?h(N):x(N),U=r.map((Q,V)=>({diff:b(Q-_,0),index:V})).sort((Q,V)=>Ge(Q.diff)-Ge(V.diff)),{index:q}=U[0];return{index:q,distance:_}}function b(N,_){const U=[N,N+u,N-u];if(!i)return N;if(!_)return v(U);const q=U.filter(Q=>oo(Q)===_);return q.length?v(q):tl(U)-u}function y(N,_){const U=r[N]-f.get(),q=b(U,_);return{index:N,distance:q}}function E(N,_){const U=f.get()+N,{index:q,distance:Q}=p(U),V=!i&&m(U);if(!_||V)return{index:q,distance:N};const ne=r[q]-Q,de=N+b(ne,0);return{index:q,distance:de}}return{byDistance:E,byIndex:y,shortcut:b}}function fb(i,r,u,o,f,m,h){function x(y){const E=y.distance,T=y.index!==r.get();m.add(E),E&&(o.duration()?i.start():(i.update(),i.render(1),i.update())),T&&(u.set(r.get()),r.set(y.index),h.emit("select"))}function v(y,E){const T=f.byDistance(y,E);x(T)}function p(y,E){const T=r.clone().set(y),N=f.byIndex(T.get(),E);x(N)}return{distance:v,index:p}}function db(i,r,u,o,f,m,h,x){const v={passive:!0,capture:!0};let p=0;function b(T){if(!x)return;function N(_){if(new Date().getTime()-p>10)return;h.emit("slideFocusStart"),i.scrollLeft=0;const Q=u.findIndex(V=>V.includes(_));uo(Q)&&(f.useDuration(0),o.index(Q,0),h.emit("slideFocus"))}m.add(document,"keydown",y,!1),r.forEach((_,U)=>{m.add(_,"focus",q=>{(pc(x)||x(T,q))&&N(U)},v)})}function y(T){T.code==="Tab"&&(p=new Date().getTime())}return{init:b}}function Es(i){let r=i;function u(){return r}function o(v){r=h(v)}function f(v){r+=h(v)}function m(v){r-=h(v)}function h(v){return uo(v)?v:v.get()}return{get:u,set:o,add:f,subtract:m}}function vp(i,r){const u=i.scroll==="x"?h:x,o=r.style;let f=null,m=!1;function h(E){return`translate3d(${E}px,0px,0px)`}function x(E){return`translate3d(0px,${E}px,0px)`}function v(E){if(m)return;const T=Z0(i.direction(E));T!==f&&(o.transform=u(T),f=T)}function p(E){m=!E}function b(){m||(o.transform="",r.getAttribute("style")||r.removeAttribute("style"))}return{clear:b,to:v,toggleActive:p}}function mb(i,r,u,o,f,m,h,x,v){const b=Os(f),y=Os(f).reverse(),E=q().concat(Q());function T(X,ee){return X.reduce((se,Ne)=>se-f[Ne],ee)}function N(X,ee){return X.reduce((se,Ne)=>T(se,ee)>0?se.concat([Ne]):se,[])}function _(X){return m.map((ee,se)=>({start:ee-o[se]+.5+X,end:ee+r-.5+X}))}function U(X,ee,se){const Ne=_(ee);return X.map(oe=>{const ye=se?0:-u,Ye=se?u:0,Qe=se?"end":"start",te=Ne[oe][Qe];return{index:oe,loopPoint:te,slideLocation:Es(-1),translate:vp(i,v[oe]),target:()=>x.get()>te?ye:Ye}})}function q(){const X=h[0],ee=N(y,X);return U(ee,u,!1)}function Q(){const X=r-h[0]-1,ee=N(b,X);return U(ee,-u,!0)}function V(){return E.every(({index:X})=>{const ee=b.filter(se=>se!==X);return T(ee,r)<=.1})}function ne(){E.forEach(X=>{const{target:ee,translate:se,slideLocation:Ne}=X,oe=ee();oe!==Ne.get()&&(se.to(oe),Ne.set(oe))})}function de(){E.forEach(X=>X.translate.clear())}return{canLoop:V,clear:de,loop:ne,loopPoints:E}}function hb(i,r,u){let o,f=!1;function m(v){if(!u)return;function p(b){for(const y of b)if(y.type==="childList"){v.reInit(),r.emit("slidesChanged");break}}o=new MutationObserver(b=>{f||(pc(u)||u(v,b))&&p(b)}),o.observe(i,{childList:!0})}function h(){o&&o.disconnect(),f=!0}return{init:m,destroy:h}}function pb(i,r,u,o){const f={};let m=null,h=null,x,v=!1;function p(){x=new IntersectionObserver(N=>{v||(N.forEach(_=>{const U=r.indexOf(_.target);f[U]=_}),m=null,h=null,u.emit("slidesInView"))},{root:i.parentElement,threshold:o}),r.forEach(N=>x.observe(N))}function b(){x&&x.disconnect(),v=!0}function y(N){return Ds(f).reduce((_,U)=>{const q=parseInt(U),{isIntersecting:Q}=f[q];return(N&&Q||!N&&!Q)&&_.push(q),_},[])}function E(N=!0){if(N&&m)return m;if(!N&&h)return h;const _=y(N);return N&&(m=_),N||(h=_),_}return{init:p,destroy:b,get:E}}function xb(i,r,u,o,f,m){const{measureSize:h,startEdge:x,endEdge:v}=i,p=u[0]&&f,b=N(),y=_(),E=u.map(h),T=U();function N(){if(!p)return 0;const Q=u[0];return Ge(r[x]-Q[x])}function _(){if(!p)return 0;const Q=m.getComputedStyle(tl(o));return parseFloat(Q.getPropertyValue(`margin-${v}`))}function U(){return u.map((Q,V,ne)=>{const de=!V,le=fo(ne,V);return de?E[V]+b:le?E[V]+y:ne[V+1][x]-Q[x]}).map(Ge)}return{slideSizes:E,slideSizesWithGaps:T,startGap:b,endGap:y}}function gb(i,r,u,o,f,m,h,x,v){const{startEdge:p,endEdge:b,direction:y}=i,E=uo(u);function T(q,Q){return Os(q).filter(V=>V%Q===0).map(V=>q.slice(V,V+Q))}function N(q){return q.length?Os(q).reduce((Q,V,ne)=>{const de=tl(Q)||0,le=de===0,X=V===Bs(q),ee=f[p]-m[de][p],se=f[p]-m[V][b],Ne=!o&&le?y(h):0,oe=!o&&X?y(x):0,ye=Ge(se-oe-(ee+Ne));return ne&&ye>r+v&&Q.push(V),X&&Q.push(q.length),Q},[]).map((Q,V,ne)=>{const de=Math.max(ne[V-1]||0);return q.slice(de,Q)}):[]}function _(q){return E?T(q,u):N(q)}return{groupSlides:_}}function vb(i,r,u,o,f,m,h){const{align:x,axis:v,direction:p,startIndex:b,loop:y,duration:E,dragFree:T,dragThreshold:N,inViewThreshold:_,slidesToScroll:U,skipSnaps:q,containScroll:Q,watchResize:V,watchSlides:ne,watchDrag:de,watchFocus:le}=m,X=2,ee=P0(),se=ee.measure(r),Ne=u.map(ee.measure),oe=W0(v,p),ye=oe.measureSize(se),Ye=eb(ye),Qe=K0(x,ye),te=!y&&!!Q,O=y||!!Q,{slideSizes:B,slideSizesWithGaps:W,startGap:me,endGap:je}=xb(oe,se,Ne,u,O,f),S=gb(oe,ye,U,y,se,Ne,me,je,X),{snaps:H,snapsAligned:G}=rb(oe,Qe,se,Ne,S),$=-tl(H)+tl(W),{snapsContained:ae,scrollContainLimit:re}=nb(ye,$,G,Q,X),ie=te?ae:G,{limit:Ee}=sb($,ie,y),Ae=gp(Bs(ie),b,y),nt=Ae.clone(),Me=Os(u),J=({dragHandler:pl,scrollBody:Cn,scrollBounds:Mn,options:{loop:sl}})=>{sl||Mn.constrain(pl.pointerDown()),Cn.seek()},_e=({scrollBody:pl,translate:Cn,location:Mn,offsetLocation:sl,previousLocation:xt,scrollLooper:il,slideLooper:gt,dragHandler:vc,animation:bc,eventHandler:Qs,scrollBounds:Ua,options:{loop:ua}},oa)=>{const cl=pl.settled(),Ra=!Ua.shouldConstrain(),Ll=ua?cl:cl&&Ra,Xs=Ll&&!vc.pointerDown();Xs&&bc.stop();const $s=Mn.get()*oa+xt.get()*(1-oa);sl.set($s),ua&&(il.loop(pl.direction()),gt.loop()),Cn.to(sl.get()),Xs&&Qs.emit("settle"),Ll||Qs.emit("scroll")},Oe=J0(o,f,()=>J(wn),pl=>_e(wn,pl)),tt=.68,bt=ie[Ae.get()],Wt=Es(bt),nl=Es(bt),kt=Es(bt),Ft=Es(bt),hl=lb(Wt,kt,nl,Ft,E,tt),An=ob(y,ie,$,Ee,Ft),Tn=fb(Oe,Ae,nt,hl,An,Ft,h),mt=cb(Ee),Ys=ks(),Gs=pb(r,u,h,_),{slideRegistry:_n}=ub(te,Q,ie,re,S,Me),ka=db(i,u,_n,Tn,hl,Ys,h,le),wn={ownerDocument:o,ownerWindow:f,eventHandler:h,containerRect:se,slideRects:Ne,animation:Oe,axis:oe,dragHandler:F0(oe,i,o,f,Ft,I0(oe,f),Wt,Oe,Tn,hl,An,Ae,h,Ye,T,N,q,tt,de),eventStore:Ys,percentOfView:Ye,index:Ae,indexPrevious:nt,limit:Ee,location:Wt,offsetLocation:kt,previousLocation:nl,options:m,resizeHandler:tb(r,h,f,u,oe,V,ee),scrollBody:hl,scrollBounds:ab(Ee,kt,Ft,hl,Ye),scrollLooper:ib($,Ee,kt,[Wt,kt,nl,Ft]),scrollProgress:mt,scrollSnapList:ie.map(mt.get),scrollSnaps:ie,scrollTarget:An,scrollTo:Tn,slideLooper:mb(oe,ye,$,B,W,H,ie,kt,u),slideFocus:ka,slidesHandler:hb(r,h,ne),slidesInView:Gs,slideIndexes:Me,slideRegistry:_n,slidesToScroll:S,target:Ft,translate:vp(oe,r)};return wn}function bb(){let i={},r;function u(p){r=p}function o(p){return i[p]||[]}function f(p){return o(p).forEach(b=>b(r,p)),v}function m(p,b){return i[p]=o(p).concat([b]),v}function h(p,b){return i[p]=o(p).filter(y=>y!==b),v}function x(){i={}}const v={init:u,emit:f,off:h,on:m,clear:x};return v}const yb={align:"center",axis:"x",container:null,slides:null,containScroll:"trimSnaps",direction:"ltr",slidesToScroll:1,inViewThreshold:0,breakpoints:{},dragFree:!1,dragThreshold:10,loop:!1,skipSnaps:!1,duration:25,startIndex:0,active:!0,watchDrag:!0,watchResize:!0,watchSlides:!0,watchFocus:!0};function jb(i){function r(m,h){return xp(m,h||{})}function u(m){const h=m.breakpoints||{},x=Ds(h).filter(v=>i.matchMedia(v).matches).map(v=>h[v]).reduce((v,p)=>r(v,p),{});return r(m,x)}function o(m){return m.map(h=>Ds(h.breakpoints||{})).reduce((h,x)=>h.concat(x),[]).map(i.matchMedia)}return{mergeOptions:r,optionsAtMedia:u,optionsMediaQueries:o}}function Nb(i){let r=[];function u(m,h){return r=h.filter(({options:x})=>i.optionsAtMedia(x).active!==!1),r.forEach(x=>x.init(m,i)),h.reduce((x,v)=>Object.assign(x,{[v.name]:v}),{})}function o(){r=r.filter(m=>m.destroy())}return{init:u,destroy:o}}function rc(i,r,u){const o=i.ownerDocument,f=o.defaultView,m=jb(f),h=Nb(m),x=ks(),v=bb(),{mergeOptions:p,optionsAtMedia:b,optionsMediaQueries:y}=m,{on:E,off:T,emit:N}=v,_=oe;let U=!1,q,Q=p(yb,rc.globalOptions),V=p(Q),ne=[],de,le,X;function ee(){const{container:Me,slides:J}=V;le=(Zu(Me)?i.querySelector(Me):Me)||i.children[0];const Oe=Zu(J)?le.querySelectorAll(J):J;X=[].slice.call(Oe||le.children)}function se(Me){const J=vb(i,le,X,o,f,Me,v);if(Me.loop&&!J.slideLooper.canLoop()){const _e=Object.assign({},Me,{loop:!1});return se(_e)}return J}function Ne(Me,J){U||(Q=p(Q,Me),V=b(Q),ne=J||ne,ee(),q=se(V),y([Q,...ne.map(({options:_e})=>_e)]).forEach(_e=>x.add(_e,"change",oe)),V.active&&(q.translate.to(q.location.get()),q.animation.init(),q.slidesInView.init(),q.slideFocus.init(nt),q.eventHandler.init(nt),q.resizeHandler.init(nt),q.slidesHandler.init(nt),q.options.loop&&q.slideLooper.loop(),le.offsetParent&&X.length&&q.dragHandler.init(nt),de=h.init(nt,ne)))}function oe(Me,J){const _e=S();ye(),Ne(p({startIndex:_e},Me),J),v.emit("reInit")}function ye(){q.dragHandler.destroy(),q.eventStore.clear(),q.translate.clear(),q.slideLooper.clear(),q.resizeHandler.destroy(),q.slidesHandler.destroy(),q.slidesInView.destroy(),q.animation.destroy(),h.destroy(),x.clear()}function Ye(){U||(U=!0,x.clear(),ye(),v.emit("destroy"),v.clear())}function Qe(Me,J,_e){!V.active||U||(q.scrollBody.useBaseFriction().useDuration(J===!0?0:V.duration),q.scrollTo.index(Me,_e||0))}function te(Me){const J=q.index.add(1).get();Qe(J,Me,-1)}function O(Me){const J=q.index.add(-1).get();Qe(J,Me,1)}function B(){return q.index.add(1).get()!==S()}function W(){return q.index.add(-1).get()!==S()}function me(){return q.scrollSnapList}function je(){return q.scrollProgress.get(q.offsetLocation.get())}function S(){return q.index.get()}function H(){return q.indexPrevious.get()}function G(){return q.slidesInView.get()}function $(){return q.slidesInView.get(!1)}function ae(){return de}function re(){return q}function ie(){return i}function Ee(){return le}function Ae(){return X}const nt={canScrollNext:B,canScrollPrev:W,containerNode:Ee,internalEngine:re,destroy:Ye,off:T,on:E,emit:N,plugins:ae,previousScrollSnap:H,reInit:_,rootNode:ie,scrollNext:te,scrollPrev:O,scrollProgress:je,scrollSnapList:me,scrollTo:Qe,selectedScrollSnap:S,slideNodes:Ae,slidesInView:G,slidesNotInView:$};return Ne(r,u),setTimeout(()=>v.emit("init"),0),nt}rc.globalOptions=void 0;function mo(i={},r=[]){const u=Y.useRef(i),o=Y.useRef(r),[f,m]=Y.useState(),[h,x]=Y.useState(),v=Y.useCallback(()=>{f&&f.reInit(u.current,o.current)},[f]);return Y.useEffect(()=>{ro(u.current,i)||(u.current=i,v())},[i,v]),Y.useEffect(()=>{$0(o.current,r)||(o.current=r,v())},[r,v]),Y.useEffect(()=>{if(X0()&&h){rc.globalOptions=mo.globalOptions;const p=rc(h,u.current,o.current);return m(p),()=>p.destroy()}else m(void 0)},[h,m]),[x,f]}mo.globalOptions=void 0;const xc=4500;function gc({box:i,interval:r,advance:u}){const o=Y.useRef(u);o.current=u,Y.useEffect(()=>{const f=i.current;if(!f||r<=0||window.matchMedia("(prefers-reduced-motion: reduce)").matches)return;let m=0,h=!0;const x=()=>{m+=1},v=()=>{m=Math.max(0,m-1)},p=window.setInterval(()=>{m>0||document.hidden||!h||o.current()||window.clearInterval(p)},r),b=new IntersectionObserver(([_])=>{h=_.isIntersecting},{threshold:.25});b.observe(f);const y=window.matchMedia("(hover: hover) and (pointer: fine)").matches;y&&(f.addEventListener("pointerenter",x),f.addEventListener("pointerleave",v));let E=!1;const T=_=>{const U=_.target;!(U instanceof Element)||!U.matches(":focus-visible")||E||(E=!0,x())},N=()=>{E&&(E=!1,v())};return f.addEventListener("focusin",T),f.addEventListener("focusout",N),f.addEventListener("pointerdown",x,!0),window.addEventListener("pointerup",v,!0),window.addEventListener("pointercancel",v,!0),()=>{window.clearInterval(p),b.disconnect(),y&&(f.removeEventListener("pointerenter",x),f.removeEventListener("pointerleave",v)),f.removeEventListener("focusin",T),f.removeEventListener("focusout",N),f.removeEventListener("pointerdown",x,!0),window.removeEventListener("pointerup",v,!0),window.removeEventListener("pointercancel",v,!0)}},[i,r])}function ho(i,r=.85){if(!i)return!1;const u=i.scrollWidth-i.clientWidth;if(u<=8||i.scrollLeft>=u-8)return!1;const o=window.matchMedia("(prefers-reduced-motion: reduce)").matches;return i.scrollBy({left:i.clientWidth*r,behavior:o?"auto":"smooth"}),!0}function po({label:i,children:r,align:u="start",loop:o=!1,dots:f=!0,arrows:m="header",gap:h=1,autoplay:x=xc,onSelect:v,onReady:p,className:b}){const y=x===!1?0:x,[E,T]=mo({align:u,loop:o,containScroll:"trimSnaps",watchDrag:!0}),[N,_]=Y.useState([]),[U,q]=Y.useState(0),[Q,V]=Y.useState(!1),[ne,de]=Y.useState(!1),le=Y.useRef(null),X=Y.useId();Y.useEffect(()=>{var B;if(!T)return;const te=()=>{q(T.selectedScrollSnap()),V(T.canScrollPrev()),de(T.canScrollNext())},O=()=>{_(T.scrollSnapList()),te()};return O(),T.on("select",te),T.on("reInit",O),(B=le.current)==null||B.setAttribute("data-slider","on"),()=>{T.off("select",te),T.off("reInit",O)}},[T]),Y.useEffect(()=>{if(!T)return;const te=le.current,O=()=>te==null?void 0:te.setAttribute("data-dragging","true"),B=()=>te==null?void 0:te.removeAttribute("data-dragging");return T.on("pointerDown",O),T.on("pointerUp",B),()=>{T.off("pointerDown",O),T.off("pointerUp",B)}},[T]),gc({box:le,interval:y,advance:()=>!T||T.scrollSnapList().length<=1?!0:T.canScrollNext()?(T.scrollNext(),!0):!1});const ee=Y.useCallback(te=>T==null?void 0:T.scrollTo(te),[T]),se=Y.useRef(v);se.current=v;const Ne=Y.useRef(p);Ne.current=p,Y.useEffect(()=>{var O;if(!T)return;const te=()=>{var B;return(B=se.current)==null?void 0:B.call(se,T.selectedScrollSnap())};return te(),T.on("select",te),T.on("reInit",te),(O=Ne.current)==null||O.call(Ne,{scrollTo:B=>T.scrollTo(B)}),()=>{T.off("select",te),T.off("reInit",te)}},[T]);const oe=Y.useCallback(()=>T==null?void 0:T.scrollPrev(),[T]),ye=Y.useCallback(()=>T==null?void 0:T.scrollNext(),[T]),Ye=N.length>1,Qe=f&&Ye&&N.length<=12;return n.jsxs("div",{className:b,children:[m==="header"&&Ye&&n.jsxs("div",{className:"mb-3 flex justify-end gap-2",children:[n.jsx(Rh,{dir:"prev",onClick:oe,disabled:!o&&!Q,label:i}),n.jsx(Rh,{dir:"next",onClick:ye,disabled:!o&&!ne,label:i})]}),n.jsxs("div",{className:"relative",children:[n.jsx("div",{ref:te=>{le.current=te,E(te)},className:"slider-viewport",role:"group","aria-roledescription":"캐러셀","aria-label":i,tabIndex:0,onKeyDown:te=>{te.key==="ArrowLeft"&&(te.preventDefault(),oe()),te.key==="ArrowRight"&&(te.preventDefault(),ye())},children:n.jsx("div",{className:"slider-track",style:{gap:`${h}rem`},id:X,children:r})}),m==="overlay"&&Ye&&n.jsxs(n.Fragment,{children:[n.jsx(Hh,{dir:"prev",onClick:oe,disabled:!o&&!Q,label:i}),n.jsx(Hh,{dir:"next",onClick:ye,disabled:!o&&!ne,label:i})]})]}),Ye&&n.jsxs("div",{className:"mt-4 flex items-center justify-center gap-3",children:[Qe&&n.jsx("ul",{className:"flex items-center gap-1.5",children:N.map((te,O)=>n.jsx("li",{children:n.jsx("button",{type:"button",onClick:()=>ee(O),"aria-label":`${O+1}번째로`,"aria-current":O===U,"aria-controls":X,className:`h-1.5 rounded-full transition-all ${O===U?"w-6 bg-current opacity-70":"w-1.5 bg-current opacity-20 hover:opacity-45"}`})},O))}),n.jsxs("span",{className:"text-current/55 text-[length:var(--fs-xs)] tabular-nums",children:[U+1," / ",N.length]})]})]})}function bp({basis:i,children:r}){return n.jsx("div",{className:`min-w-0 shrink-0 grow-0 ${i}`,"aria-roledescription":"슬라이드",children:r})}function Rh({dir:i,onClick:r,disabled:u,label:o}){const f=i==="prev"?Ma:Oa;return n.jsx("button",{type:"button",onClick:r,disabled:u,"aria-label":`${o} ${i==="prev"?"이전":"다음"}`,className:"tap border-line flex items-center justify-center rounded-full border transition-opacity hover:opacity-70 disabled:opacity-25",children:n.jsx(f,{className:"size-5"})})}function Hh({dir:i,onClick:r,disabled:u,label:o}){const f=i==="prev"?Ma:Oa;return n.jsx("button",{type:"button",onClick:r,disabled:u,"aria-label":`${o} ${i==="prev"?"이전":"다음"}`,className:`tap absolute top-1/2 z-10 hidden -translate-y-1/2 items-center justify-center rounded-full bg-black/55 text-white backdrop-blur transition hover:bg-black/75 disabled:opacity-0 sm:flex ${i==="prev"?"left-2":"right-2"}`,children:n.jsx(f,{className:"size-5"})})}function xo({children:i,className:r="",as:u="div"}){return n.jsx(u,{className:`panel ${r}`,children:i})}function go({children:i}){return n.jsx("span",{className:"border-line tpl-border inline-flex items-center gap-1 rounded-md border bg-current/5 px-2.5 py-1 text-[length:var(--fs-xs)]",children:i})}function yp({label:i,value:r,note:u}){return n.jsxs("div",{className:"divide-line flex flex-col gap-1 py-3 sm:flex-row sm:items-baseline sm:justify-between sm:gap-6",children:[n.jsx("dt",{className:"text-muted shrink-0 text-[length:var(--fs-sm)] font-medium sm:w-40",children:i}),n.jsxs("dd",{className:"flex-1 text-[length:var(--fs-sm)] font-semibold sm:text-right",children:[r,u&&n.jsx("span",{className:"text-muted mt-0.5 block font-normal",children:u})]})]})}function Lh({href:i,children:r,variant:u="solid",external:o,icon:f}){const m=u==="solid";return n.jsxs("a",{href:i,...o?{target:"_blank",rel:"noopener noreferrer"}:{},className:`tap inline-flex items-center justify-center gap-2 rounded-lg px-4 text-[length:var(--fs-sm)] font-semibold transition-opacity hover:opacity-85 ${m?"":"border-line tpl-border border bg-current/5"}`,style:m?{backgroundColor:"var(--color-brand)",color:"var(--tpl-bg, #fff)"}:void 0,children:[f,n.jsx("span",{children:r}),o&&n.jsx(uc,{className:"size-4 shrink-0"})]})}function Sb(){const i=F(),{narrative:r,place:u}=i,o=c0(i,"intro"),f=o.length>0?o:r.about;if(f.length===0)return null;const m=eo(i).find(x=>!x.isPrimary),h=r.heroSubline??`${u.name} 소개`;return n.jsx(pt,{id:"about",bare:!0,title:h,children:n.jsxs("div",{className:"grid grid-cols-1 items-center gap-8 lg:grid-cols-12 lg:gap-14",children:[m&&n.jsxs("figure",{className:"lg:col-span-7",children:[n.jsx("div",{className:"tpl-border border-line relative aspect-4/3 max-h-[26rem] overflow-hidden rounded-xl border lg:aspect-3/2",children:n.jsx("img",{src:m.url,alt:m.alt,loading:"lazy",decoding:"async",width:m.width,height:m.height,className:"size-full object-cover"})}),m.caption&&n.jsx("figcaption",{className:"text-muted mt-2 text-[length:var(--fs-xs)]",children:m.caption})]}),n.jsxs("div",{className:m?"lg:col-span-5":"lg:col-span-12",children:[n.jsxs("h2",{id:"about-heading",className:"h2 mb-6 flex items-center gap-2.5",children:[n.jsx("i",{"aria-hidden":!0,className:"h-[1em] w-[3px] shrink-0 bg-current opacity-25"}),n.jsx("span",{className:"min-w-0",children:h})]}),n.jsx("div",{className:"measure space-y-4 text-[length:var(--fs-lead)] leading-[1.8]",children:f.map((x,v)=>n.jsx("p",{children:x},v))})]})]})})}const qh=new Set(["체크인 시간","체크아웃 시간","취소·환불 규정","취사 가능","반려동물 동반","흡연 가능","인원 추가 요금"]);function zb(){const i=F(),r=dc(i),u=i.facts.filter(m=>m.scope==="place"&&!r.some(h=>h.label===m.label)).length;if(r.length===0)return null;const o=r.filter(m=>qh.has(m.label)),f=r.filter(m=>!qh.has(m.label));return n.jsx(pt,{id:"info",tone:"alt",title:"이용안내 및 예약",lead:"방문 전 확인이 필요한 운영 규정과 시설 안내입니다.",footnote:n.jsxs("span",{className:"flex flex-col gap-1 sm:flex-row sm:justify-between",children:[n.jsx("span",{children:u>0?`확인 중인 항목 ${u}개는 표시하지 않았습니다. 필요하시면 전화로 문의해 주세요.`:"모든 항목이 사업자 확인을 거쳤습니다."}),n.jsxs("span",{className:"font-medium",children:[lo(i.site.updatedAt)," 기준"]})]}),children:n.jsxs("div",{className:"space-y-8",children:[o.length>0&&n.jsx(Bh,{title:"예약 전 확인",rows:o,emphasis:!0}),f.length>0&&n.jsx(Bh,{title:"시설 · 편의",rows:f}),n.jsx(Eb,{})]})})}function Bh({title:i,rows:r,emphasis:u}){return n.jsxs("section",{children:[n.jsxs("h3",{className:"border-line mb-3 flex items-center gap-2 border-b pb-2 text-[length:var(--fs-sm)] font-bold",children:[u&&n.jsx("i",{"aria-hidden":!0,className:"h-[1em] w-[3px] shrink-0",style:{backgroundColor:"var(--color-accent)"}}),n.jsx("span",{children:i})]}),n.jsx("dl",{className:"divide-line divide-y",children:r.map(o=>n.jsxs("div",{className:"flex flex-col gap-1 py-3.5 sm:flex-row sm:items-baseline sm:gap-6",children:[n.jsx("dt",{className:"text-muted text-[length:var(--fs-sm)] sm:w-40 sm:shrink-0",children:o.label}),n.jsxs("dd",{className:"measure text-[length:var(--fs-sm)] font-semibold",children:[o.value,o.note&&n.jsx("span",{className:"text-muted mt-0.5 block font-normal",children:o.note})]})]},o.label))})]})}function Eb(){const i=F(),r=et(i),u=i.place.phone;return!u&&r.length===0?null:n.jsxs("div",{className:"border-line flex flex-col gap-3 border-t pt-6 sm:flex-row sm:items-center sm:justify-between",children:[n.jsxs("p",{className:"text-[length:var(--fs-sm)] font-semibold",children:[i.place.name," 예약은 아래로 받습니다."]}),n.jsxs("div",{className:"flex flex-wrap gap-2",children:[u&&n.jsxs("a",{href:`tel:${u}`,className:"tap border-line tpl-border inline-flex items-center justify-center gap-1.5 rounded-lg border px-4 text-[length:var(--fs-sm)] font-semibold",children:[n.jsx(Jt,{className:"size-4"}),n.jsx("span",{children:u})]}),r.map(o=>n.jsxs("a",{href:o.url,target:"_blank",rel:"noopener noreferrer",className:"tap inline-flex items-center justify-center rounded-lg px-6 text-[length:var(--fs-sm)] font-bold transition-opacity hover:opacity-90",style:{backgroundColor:"var(--color-brand)",color:"var(--tpl-bg, #fff)"},children:[Pe(o),"로 예약"]},o.url))]})]})}function Ab(){const i=F(),r=dt(i),u=ll(i),o=et(i)[0];return u.length===0?null:n.jsx(pt,{id:"units",title:qe(i,r.path,`${r.label} 안내`),children:n.jsx("ul",{className:"mt-2 flex flex-col gap-14 lg:gap-20",children:u.map((f,m)=>n.jsxs("li",{className:"grid items-center gap-7 lg:grid-cols-12 lg:gap-12",children:[n.jsxs("div",{className:`lg:col-span-6 ${m%2===1?"lg:order-2":""}`,children:[f.images[0]&&n.jsx("div",{className:"relative aspect-16/10 rounded-lg overflow-hidden",children:n.jsx("img",{src:f.images[0].url,alt:f.images[0].alt,loading:"lazy",decoding:"async",className:"size-full object-cover"})}),f.images.length>1&&n.jsx("ul",{className:"mt-2 grid grid-cols-3 gap-2",children:f.images.slice(1,4).map(h=>n.jsx("li",{className:"relative aspect-16/10 rounded-md overflow-hidden",children:n.jsx("img",{src:h.url,alt:h.alt,loading:"lazy",decoding:"async",className:"size-full object-cover"})},h.mediaId))})]}),n.jsxs("div",{className:`lg:col-span-6 ${m%2===1?"lg:order-1":""}`,children:[n.jsx("h3",{className:"h2",children:f.name}),f.intro&&n.jsx("p",{className:"measure mt-3 text-[length:var(--fs-body)] leading-relaxed opacity-80",children:f.intro}),f.chips.length>0&&n.jsx("ul",{className:"mt-4 flex flex-wrap items-center gap-1.5",children:f.chips.map(h=>n.jsx("li",{children:n.jsxs(go,{children:[n.jsx("span",{className:"text-muted",children:h.label}),n.jsx("span",{className:"font-medium",children:h.value})]})},h.label))}),n.jsxs("div",{className:"mt-5 flex flex-wrap gap-2",children:[o&&n.jsxs("a",{href:o.url,target:"_blank",rel:"noopener noreferrer",className:"tap inline-flex flex-1 items-center justify-center rounded-lg px-6 text-[length:var(--fs-sm)] font-bold transition-opacity hover:opacity-90",style:{backgroundColor:"var(--color-brand)",color:"var(--tpl-bg, #fff)"},children:[f.name," ",Pe(o)," 예약"]}),i.place.phone&&n.jsxs("a",{href:`tel:${i.place.phone}`,className:"tap border-line tpl-border inline-flex items-center justify-center gap-1.5 rounded-lg border px-4 text-[length:var(--fs-sm)] font-semibold",children:[n.jsx(Jt,{className:"size-4"}),n.jsx("span",{children:"전화"})]})]}),f.rows.length>0&&n.jsxs("details",{className:"group border-line mt-4 border-t pt-2",children:[n.jsxs("summary",{className:"tap flex cursor-pointer list-none items-center justify-between gap-2 text-[length:var(--fs-sm)] font-semibold",children:[n.jsxs("span",{children:[f.name," 구성 자세히"]}),n.jsx(oc,{className:"size-4 shrink-0 transition-transform group-open:rotate-180"})]}),n.jsx("dl",{className:"divide-line border-line divide-y border-t",children:f.rows.map(h=>n.jsxs("div",{className:"flex items-baseline justify-between gap-4 py-2.5 text-[length:var(--fs-sm)]",children:[n.jsx("dt",{className:"text-muted shrink-0",children:h.label}),n.jsx("dd",{className:"text-right font-semibold",children:h.value})]},h.label))})]})]})]},f.unitId))})})}function Tb(){const i=F(),r=dt(i),u=ll(i),o=et(i)[0],[f,m]=Y.useState(0);return u.length===0?null:n.jsxs(pt,{id:"units",title:qe(i,r.path,`${r.label} 안내`),children:[n.jsx("div",{className:"border-line mb-6 flex gap-1 border-b",role:"tablist","aria-label":r.label,children:u.map((h,x)=>n.jsx("button",{type:"button",role:"tab","aria-selected":x===f,"aria-controls":`unit-panel-${h.slug}`,onClick:()=>m(x),className:`tap -mb-px border-b-2 px-5 text-[length:var(--fs-sm)] font-bold transition-colors ${x===f?"opacity-100":"border-transparent opacity-50 hover:opacity-80"}`,style:x===f?{borderColor:"var(--color-brand)"}:void 0,children:h.name},h.unitId))}),u.map((h,x)=>n.jsxs("div",{id:`unit-panel-${h.slug}`,role:"tabpanel",hidden:x!==f,className:"grid gap-6 lg:grid-cols-12 lg:gap-10",children:[n.jsxs("div",{className:"lg:col-span-7",children:[h.images[0]&&n.jsx("div",{className:"relative aspect-16/10 overflow-hidden rounded-lg",children:n.jsx("img",{src:h.images[0].url,alt:h.images[0].alt,loading:"lazy",decoding:"async",className:"size-full object-cover"})}),h.images.length>1&&n.jsx("ul",{className:"mt-2 grid grid-cols-3 gap-2",children:h.images.slice(1,4).map(v=>n.jsx("li",{className:"relative aspect-16/10 overflow-hidden rounded-md",children:n.jsx("img",{src:v.url,alt:v.alt,loading:"lazy",decoding:"async",className:"size-full object-cover"})},v.mediaId))})]}),n.jsxs("div",{className:"lg:col-span-5",children:[n.jsx("h3",{className:"h3",children:h.name}),h.intro&&n.jsx("p",{className:"mt-2 text-[length:var(--fs-sm)] leading-relaxed opacity-75",children:h.intro}),h.chips.length>0&&n.jsx("ul",{className:"mt-4 flex flex-wrap items-center gap-1.5",children:h.chips.map(v=>n.jsx("li",{children:n.jsxs(go,{children:[n.jsx("span",{className:"text-muted",children:v.label}),n.jsx("span",{className:"font-medium",children:v.value})]})},v.label))}),h.rows.length>0&&n.jsx("dl",{className:"divide-line border-line mt-4 divide-y border-t",children:h.rows.map(v=>n.jsxs("div",{className:"flex items-baseline justify-between gap-4 py-2 text-[length:var(--fs-sm)]",children:[n.jsx("dt",{className:"text-muted shrink-0",children:v.label}),n.jsx("dd",{className:"text-right font-semibold",children:v.value})]},v.label))}),n.jsxs("div",{className:"mt-5 flex gap-2",children:[o&&n.jsxs("a",{href:o.url,target:"_blank",rel:"noopener noreferrer",className:"tap flex flex-1 items-center justify-center rounded-lg px-6 text-[length:var(--fs-sm)] font-bold transition-opacity hover:opacity-90",style:{backgroundColor:"var(--color-brand)",color:"var(--tpl-bg, #fff)"},children:[h.name," ",Pe(o)," 예약"]}),i.place.phone&&n.jsxs("a",{href:`tel:${i.place.phone}`,className:"tap border-line tpl-border flex items-center justify-center gap-1.5 rounded-lg border px-4 text-[length:var(--fs-sm)] font-semibold",children:[n.jsx(Jt,{className:"size-4"}),n.jsx("span",{children:"전화"})]})]})]})]},h.unitId))]})}function _b(){const i=F(),r=dt(i),u=ll(i),o=et(i)[0];if(u.length===0)return null;const f=u.length===1?"grid-cols-1 max-w-2xl mx-auto":u.length===2?"grid-cols-1 sm:grid-cols-2":"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",m=Kt(i,"info")?[]:dc(i);return n.jsx("section",{id:"units","aria-labelledby":"units-heading",className:"border-line paper w-full border-b",style:{backgroundColor:"var(--tpl-surface, #fff)",paddingBlock:"calc(var(--section-space) * 1.4)"},children:n.jsxs("div",{className:"shell",children:[n.jsx(no,{id:"units",title:qe(i,r.path,`${r.label} 안내`)}),n.jsx("ul",{className:`grid gap-x-8 gap-y-14 lg:gap-x-12 ${f}`,children:u.map(h=>n.jsxs("li",{className:"flex min-w-0 flex-col",children:[h.images[0]&&n.jsx("div",{className:"relative aspect-4/3 w-full overflow-hidden",children:n.jsx("img",{src:h.images[0].url,alt:h.images[0].alt,loading:"lazy",decoding:"async",className:"size-full object-cover object-center"})}),n.jsx("h3",{className:"serif mt-6",style:{fontSize:"var(--fs-h3)",fontWeight:300,letterSpacing:"0.04em"},children:h.name}),h.intro&&n.jsx("p",{className:"text-muted mt-3 line-clamp-2 text-[length:var(--fs-sm)] font-light leading-loose",children:h.intro}),h.chips.length>0&&n.jsx("ul",{className:"text-muted mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-[length:var(--fs-xs)] font-light",children:h.chips.map(x=>n.jsxs("li",{children:[x.label," ",x.value]},x.label))}),(h.rows.length>0||h.images.length>1)&&n.jsxs("details",{className:"border-line mt-5 border-t",children:[n.jsx("summary",{className:"tap flex cursor-pointer list-none items-center text-[length:var(--fs-sm)] font-light opacity-70 marker:content-none [&::-webkit-details-marker]:hidden",children:"자세히 보기"}),n.jsxs("div",{className:"space-y-6 pb-2",children:[h.rows.length>0&&n.jsx("dl",{className:"divide-line divide-y",children:h.rows.map(x=>n.jsxs("div",{className:"flex items-baseline justify-between gap-4 py-2.5 text-[length:var(--fs-sm)] font-light",children:[n.jsx("dt",{className:"text-muted shrink-0",children:x.label}),n.jsx("dd",{className:"text-right",children:x.value})]},x.label))}),h.images.length>1&&n.jsx("ul",{className:"grid grid-cols-2 gap-3",children:h.images.slice(1).map(x=>n.jsx("li",{className:"relative aspect-4/3 overflow-hidden",children:n.jsx("img",{src:x.url,alt:x.alt,loading:"lazy",decoding:"async",className:"size-full object-cover object-center"})},x.mediaId))})]})]})]},h.unitId))}),m.length>0&&n.jsx("dl",{className:"divide-line border-line mx-auto mt-16 max-w-2xl divide-y border-t",children:m.map(h=>n.jsxs("div",{className:"flex items-baseline justify-between gap-4 py-3 text-[length:var(--fs-sm)] font-light",children:[n.jsx("dt",{className:"text-muted shrink-0",children:h.label}),n.jsx("dd",{className:"text-right",children:h.value})]},h.label))}),(o||i.place.phone)&&n.jsxs("div",{className:"mt-14 flex flex-wrap items-center justify-center gap-4",children:[o&&n.jsxs("a",{href:o.url,target:"_blank",rel:"noopener noreferrer",className:"tap border-line inline-flex items-center justify-center border px-9 text-[length:var(--fs-sm)] font-light tracking-[0.14em] transition-opacity hover:opacity-70",children:[Pe(o)," 예약"]}),i.place.phone&&n.jsx("a",{href:`tel:${i.place.phone}`,className:"tap border-line inline-flex items-center justify-center border px-9 text-[length:var(--fs-sm)] font-light tabular-nums tracking-[0.08em] transition-opacity hover:opacity-70",children:i.place.phone})]})]})})}function wb(){const i=F(),r=dt(i),u=ll(i),o=et(i)[0];return u.length===0?null:n.jsxs("section",{id:"units","data-oasi":!0,"aria-labelledby":"units-heading",className:"pt-[var(--oasi-gap)]",children:[n.jsx(so,{id:"units",title:qe(i,r.path,`${r.label} ${u.length}개 안내`)}),n.jsx("ul",{className:"space-y-20 lg:space-y-28",children:u.map(f=>n.jsxs("li",{children:[f.images[0]&&n.jsxs("figure",{className:"relative mx-auto w-full max-w-[34rem]",children:[n.jsx("img",{src:f.images[0].url,alt:f.images[0].alt,loading:"lazy",decoding:"async",className:"block h-[clamp(15rem,24vw,21rem)] w-full object-cover"}),n.jsx("figcaption",{className:"text-muted absolute left-full top-0 ml-5 hidden max-h-full overflow-hidden text-ellipsis whitespace-nowrap text-[length:var(--fs-xs)] tracking-[0.14em] xl:block",style:{writingMode:"vertical-rl"},children:f.name})]}),n.jsx("h3",{className:"mt-8 text-[length:var(--fs-sm)] font-normal tracking-[0.08em]",children:f.name}),f.chips.length>0&&n.jsx("p",{className:"text-muted mt-2 text-[length:var(--fs-xs)] leading-[1.9]",children:f.chips.map(m=>`${m.label} ${m.value}`).join(" · ")}),f.intro&&n.jsx("p",{className:"measure mt-4 text-[length:var(--fs-xs)] leading-[1.9]",children:f.intro}),f.rows.length>0&&n.jsx("dl",{className:"divide-line border-line mt-6 divide-y border-t text-[length:var(--fs-xs)]",children:f.rows.map(m=>n.jsxs("div",{className:"flex items-baseline justify-between gap-6 py-2.5",children:[n.jsx("dt",{className:"text-muted shrink-0",children:m.label}),n.jsx("dd",{className:"text-right",children:m.value})]},m.label))}),f.images.length>1&&n.jsx("ul",{className:"mx-auto mt-6 grid w-full max-w-[34rem] grid-cols-2 gap-2",children:f.images.slice(1).map(m=>n.jsx("li",{children:n.jsx("img",{src:m.url,alt:m.alt,loading:"lazy",decoding:"async",className:"block aspect-[4/3] w-full object-cover"})},m.mediaId))}),(o||i.place.phone)&&n.jsxs("p",{className:"mt-6 flex flex-wrap items-center gap-x-8 text-[length:var(--fs-sm)]",children:[o&&n.jsxs("a",{href:o.url,target:"_blank",rel:"noopener noreferrer",className:"tap inline-flex items-center underline underline-offset-[6px]",style:{color:"var(--color-brand)"},children:[Pe(o)," 예약"]}),i.place.phone&&n.jsx("a",{href:`tel:${i.place.phone}`,className:"tap text-muted inline-flex items-center underline underline-offset-[6px]",children:i.place.phone})]})]},f.unitId))})]})}function Cb(){const i=F(),r=dt(i),u=ll(i);return u.length===0?null:n.jsx("section",{id:"units","aria-labelledby":"units-heading",className:"w-full",style:{paddingBlock:"var(--section-space)",backgroundColor:"var(--tpl-bg)"},children:n.jsxs("div",{className:"shell",children:[n.jsx(io,{id:"units",title:qe(i,r.path,`${r.label} ${u.length}개 안내`)}),n.jsx("ul",{className:"space-y-16 sm:space-y-24",children:u.map(o=>n.jsxs("li",{className:"grid grid-cols-1 gap-6 lg:grid-cols-12 lg:gap-10",children:[n.jsxs("div",{className:"min-w-0 lg:col-span-7",children:[o.images[0]&&n.jsx("img",{src:o.images[0].url,alt:o.images[0].alt,loading:"lazy",decoding:"async",width:o.images[0].width,height:o.images[0].height,className:"block h-auto w-full"}),o.images.length>1&&n.jsx("ul",{className:"mt-2 grid grid-cols-2 gap-2",children:o.images.slice(1).map(f=>n.jsx("li",{children:n.jsx("img",{src:f.url,alt:f.alt,loading:"lazy",decoding:"async",width:f.width,height:f.height,className:"block h-auto w-full"})},f.mediaId))})]}),n.jsxs("div",{className:"min-w-0 lg:col-span-5",children:[n.jsx("h3",{className:"serif min-w-0 break-keep font-extrabold",style:{fontSize:"var(--fs-h3)",letterSpacing:"0.02em",lineHeight:1.35},children:o.name}),o.chips.length>0&&n.jsx("dl",{className:"mt-4 space-y-1.5",children:o.chips.map(f=>n.jsxs("div",{className:"flex gap-3 text-[length:var(--fs-sm)]",children:[n.jsx("dt",{className:"text-muted w-20 shrink-0",children:f.label}),n.jsx("dd",{className:"min-w-0 font-medium",children:f.value})]},f.label))}),o.intro&&n.jsx("p",{className:"text-muted measure mt-5 text-[length:var(--fs-sm)]",style:{lineHeight:1.8},children:o.intro})]})]},o.unitId))})]})})}function Mb(){const i=F(),r=dt(i),u=ll(i),o=et(i)[0];return u.length===0?null:n.jsx("section",{id:"units","aria-labelledby":"units-heading","data-pastel":"own",className:"w-full",style:{backgroundColor:"var(--tpl-surface, #fff)",paddingBlock:"var(--section-space)"},children:n.jsxs("div",{className:"shell",children:[n.jsx(co,{id:"units",title:qe(i,r.path,`${r.label} ${u.length}개 안내`)}),n.jsx("ul",{className:"grid grid-cols-1 gap-6 sm:grid-cols-2 lg:gap-8",children:u.map((f,m)=>n.jsxs("li",{className:"flex flex-col overflow-hidden p-4 sm:p-5",style:{backgroundColor:m%2===0?"var(--pastel-1)":"var(--pastel-2)",borderRadius:"calc(var(--tpl-radius, 0.75rem) * 1.35)"},children:[f.images[0]&&n.jsx("div",{className:"relative aspect-16/10 overflow-hidden",style:{borderRadius:"var(--tpl-radius, 0.75rem)"},children:n.jsx("img",{src:f.images[0].url,alt:f.images[0].alt,loading:"lazy",decoding:"async",className:"size-full object-cover"})}),n.jsxs("div",{className:"flex flex-1 flex-col gap-3 px-1 pt-4",children:[n.jsx("div",{className:"flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1",children:n.jsx("h3",{className:"h3",style:{fontWeight:800},children:f.name})}),f.chips.length>0&&n.jsx("ul",{className:"flex flex-wrap items-center gap-1.5",children:f.chips.map(h=>n.jsxs("li",{className:"inline-flex items-center gap-1 rounded-full px-3 py-1 text-[length:var(--fs-xs)]",style:{backgroundColor:"color-mix(in srgb, var(--tpl-surface, #fff) 70%, transparent)"},children:[n.jsx("span",{className:"opacity-60",children:h.label}),n.jsx("span",{className:"font-bold",children:h.value})]},h.label))}),f.intro&&n.jsx("p",{className:"text-[length:var(--fs-sm)] leading-relaxed opacity-75",children:f.intro})]}),(o||i.place.phone)&&n.jsxs("div",{className:"mt-4 flex gap-2 px-1",children:[o&&n.jsxs("a",{href:o.url,target:"_blank",rel:"noopener noreferrer",className:"tap flex flex-1 items-center justify-center rounded-full text-[length:var(--fs-sm)] font-extrabold transition-opacity hover:opacity-90",style:{backgroundColor:"var(--pastel-ink)",color:"var(--tpl-bg, #fff)"},children:[Pe(o)," 예약"]}),i.place.phone&&n.jsxs("a",{href:`tel:${i.place.phone}`,className:"tap flex items-center justify-center gap-1.5 rounded-full px-5 text-[length:var(--fs-sm)] font-bold",style:{backgroundColor:"color-mix(in srgb, var(--tpl-surface, #fff) 75%, transparent)"},children:[n.jsx(Jt,{className:"size-4"}),n.jsx("span",{children:"전화"})]})]}),(f.rows.length>0||f.images.length>1)&&n.jsxs("details",{className:"group mt-3 px-1",children:[n.jsxs("summary",{className:"tap flex cursor-pointer list-none items-center justify-between gap-2 text-[length:var(--fs-sm)] font-bold marker:content-none [&::-webkit-details-marker]:hidden",children:[n.jsxs("span",{children:[f.name," 자세히"]}),n.jsx(oc,{className:"size-4 shrink-0 transition-transform group-open:rotate-180"})]}),n.jsxs("div",{className:"space-y-4 pb-1",children:[f.rows.length>0&&n.jsx("dl",{className:"divide-line divide-y",children:f.rows.map(h=>n.jsxs("div",{className:"flex items-baseline justify-between gap-4 py-2.5 text-[length:var(--fs-sm)]",children:[n.jsx("dt",{className:"shrink-0 opacity-60",children:h.label}),n.jsx("dd",{className:"text-right font-bold",children:h.value})]},h.label))}),f.images.length>1&&n.jsx("ul",{className:"grid grid-cols-2 gap-2",children:f.images.slice(1).map(h=>n.jsx("li",{className:"relative aspect-4/3 overflow-hidden",style:{borderRadius:"calc(var(--tpl-radius, 0.75rem) * 0.7)"},children:n.jsx("img",{src:h.url,alt:h.alt,loading:"lazy",decoding:"async",className:"size-full object-cover"})},h.mediaId))})]})]})]},f.unitId))})]})})}function Ob(){const i=F(),r=ll(i),u=dt(i),o=et(i)[0];return r.length===0?null:n.jsxs("section",{id:"units","aria-labelledby":"units-heading",className:"border-line w-full border-b",style:{backgroundColor:"var(--tpl-surface)",backgroundImage:Ms,paddingBlock:"var(--section-space)"},children:[n.jsx("div",{className:"shell",children:n.jsx(to,{id:"units",title:qe(i,u.path,`${u.label} ${r.length}개 안내`)})}),r.map((f,m)=>n.jsx(Db,{unit:f,no:String(m+1).padStart(2,"0"),dark:m%2===1,booking:o,phone:i.place.phone},f.unitId))]})}function Db({unit:i,no:r,dark:u,booking:o,phone:f}){return n.jsx("article",{className:"w-full",style:u?{backgroundColor:"var(--tpl-inverse)",color:"var(--tpl-bg)",backgroundImage:Ms}:void 0,children:n.jsxs("div",{className:"shell grid grid-cols-12 gap-y-6 py-10 xl:gap-x-10 xl:py-16",children:[n.jsxs("div",{className:"col-span-12 xl:col-span-5 xl:pr-8",children:[n.jsxs("div",{className:"relative",children:[n.jsx("span",{"aria-hidden":!0,className:"serif pointer-events-none absolute -top-1 left-0 select-none leading-none tabular-nums",style:{fontSize:"clamp(3rem, 8vw, 6rem)",fontWeight:800,opacity:.1},children:r}),n.jsx("h3",{className:"serif relative pt-7 xl:pt-12",style:{fontSize:"clamp(1.5rem, 3vw, 2.25rem)",fontWeight:800,lineHeight:1.1,letterSpacing:"-0.02em"},children:i.name})]}),i.intro&&n.jsx("p",{className:"measure mt-4 font-serif text-[length:var(--fs-body)] leading-relaxed opacity-85",children:i.intro}),i.rows.length>0&&n.jsx("dl",{className:"border-line mt-6 border-t",children:i.rows.map(m=>n.jsxs("div",{className:"border-line flex items-baseline justify-between gap-6 border-b py-2.5 text-[length:var(--fs-sm)]",children:[n.jsx("dt",{className:"text-muted shrink-0",children:m.label}),n.jsx("dd",{className:"text-right font-semibold",children:m.value})]},m.label))}),(o||f)&&n.jsxs("div",{className:"mt-6 flex flex-wrap items-center gap-x-6 gap-y-1 text-[length:var(--fs-sm)] font-bold",children:[o&&n.jsxs("a",{href:o.url,target:"_blank",rel:"noopener noreferrer",className:"tap inline-flex items-center underline underline-offset-4",children:[Pe(o)," 예약"]}),f&&n.jsx("a",{href:`tel:${f}`,className:"tap inline-flex items-center underline underline-offset-4",children:f})]})]}),n.jsxs("div",{className:"col-span-12 xl:col-span-7 xl:-ml-10",children:[i.images[0]&&n.jsx("figure",{className:"w-full xl:w-[64%]",children:n.jsx("img",{src:i.images[0].url,alt:i.images[0].alt,loading:"lazy",decoding:"async",width:i.images[0].width,height:i.images[0].height,className:"w-full object-cover",style:{aspectRatio:"4 / 5"}})}),i.images[1]&&n.jsx("figure",{className:"mt-3 w-full xl:-mt-24 xl:ml-auto xl:w-[50%]",children:n.jsx("img",{src:i.images[1].url,alt:i.images[1].alt,loading:"lazy",decoding:"async",width:i.images[1].width,height:i.images[1].height,className:"w-full object-cover",style:{aspectRatio:"4 / 3"}})})]}),i.images.length>2&&n.jsx("ul",{className:"col-span-12 grid grid-cols-3 gap-2 xl:grid-cols-6",children:i.images.slice(2).map(m=>n.jsx("li",{children:n.jsx("img",{src:m.url,alt:m.alt,loading:"lazy",decoding:"async",width:m.width,height:m.height,className:"w-full object-cover",style:{aspectRatio:"3 / 2"}})},m.mediaId))})]})})}function qu(){var h;const i=F(),r=dt(i),u=hc();if(u==="reservation")return n.jsx(_b,{});if(u==="oasi")return n.jsx(wb,{});if(u==="studio")return n.jsx(Cb,{});if(u==="pastel")return n.jsx(Mb,{});if(u==="editorial")return n.jsx(Ob,{});const o=(h=i.theme.sections.find(x=>x.id===r.path))==null?void 0:h.variantId;if(o==="rooms.bands")return n.jsx(Ab,{});if(o==="rooms.tabs")return n.jsx(Tb,{});const f=ll(i);if(f.length===0)return null;const m=f.length===1?"grid-cols-1":f.length===2?"grid-cols-1 sm:grid-cols-2":"grid-cols-1 sm:grid-cols-2 xl:grid-cols-3";return n.jsx(pt,{id:"units",title:qe(i,r.path,`${r.label} ${f.length}개 안내`),children:n.jsx("ul",{className:`grid gap-5 lg:gap-7 ${m}`,children:f.map(x=>n.jsxs(xo,{as:"li",className:"flex flex-col overflow-hidden",children:[n.jsxs("div",{className:"flex flex-1 flex-col",children:[x.images.length>0&&n.jsx(kb,{images:x.images,name:x.name}),n.jsxs("div",{className:"flex flex-1 flex-col gap-3 p-5",children:[n.jsx("h3",{className:"h3",children:x.name}),x.chips.length>0&&n.jsx("ul",{className:"flex flex-wrap items-center gap-1.5",children:x.chips.map(v=>n.jsx("li",{children:n.jsxs(go,{children:[n.jsx("span",{className:"text-muted",children:v.label}),n.jsx("span",{className:"font-medium",children:v.value})]})},v.label))}),x.intro&&n.jsx("p",{className:"text-[length:var(--fs-sm)] leading-relaxed opacity-75",children:x.intro})]})]}),x.rows.length>0&&n.jsxs("details",{className:"group border-line border-t",children:[n.jsxs("summary",{className:"tap flex cursor-pointer list-none items-center justify-between gap-2 px-5 text-[length:var(--fs-sm)] font-semibold",children:[n.jsxs("span",{children:[x.name," 자세히"]}),n.jsx(oc,{className:"size-4 shrink-0 transition-transform group-open:rotate-180"})]}),n.jsx("div",{className:"space-y-5 px-5 pb-5",children:x.rows.length>0&&n.jsx("dl",{className:"divide-line border-line divide-y border-t",children:x.rows.map(v=>n.jsxs("div",{className:"flex items-baseline justify-between gap-4 py-2.5 text-[length:var(--fs-sm)]",children:[n.jsx("dt",{className:"text-muted shrink-0",children:v.label}),n.jsx("dd",{className:"text-right font-semibold",children:v.value})]},v.label))})})]})]},x.unitId))})})}function kb({images:i,name:r}){const u=Y.useRef(null),[o,f]=Y.useState(0),m=Y.useCallback(()=>{const x=u.current;!x||x.clientWidth===0||f(Math.round(x.scrollLeft/x.clientWidth))},[]);gc({box:u,interval:xc,advance:()=>ho(u.current,1)});const h=Y.useCallback(x=>{const v=u.current;if(!v)return;const p=window.matchMedia("(prefers-reduced-motion: reduce)").matches;v.scrollBy({left:x*v.clientWidth,behavior:p?"auto":"smooth"})},[]);return n.jsxs("div",{className:"relative",children:[n.jsx("ul",{ref:u,onScroll:m,"aria-label":`${r} 사진`,className:"flex snap-x snap-mandatory overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",children:i.map(x=>n.jsx("li",{className:"w-full shrink-0 snap-center",children:n.jsx("div",{className:"relative aspect-16/10 overflow-hidden",children:n.jsx("img",{src:x.url,alt:x.alt,loading:"lazy",decoding:"async",className:"size-full object-cover"})})},x.mediaId))}),i.length>1&&n.jsxs(n.Fragment,{children:[n.jsx(Yh,{dir:"prev",show:o>0,onClick:()=>h(-1)}),n.jsx(Yh,{dir:"next",show:oh(1)}),n.jsxs("span",{className:"pointer-events-none absolute bottom-2.5 right-2.5 rounded-full bg-black/55 px-2 py-0.5 text-[length:var(--fs-xs)] font-semibold tabular-nums text-white",children:[Math.min(o+1,i.length)," / ",i.length]})]})]})}function Yh({dir:i,show:r,onClick:u}){const o=i==="prev"?Ma:Oa;return n.jsx("button",{type:"button",onClick:u,"aria-label":i==="prev"?"이전 사진":"다음 사진","aria-hidden":!r,tabIndex:r?0:-1,className:`absolute top-1/2 z-10 flex size-8 -translate-y-1/2 items-center justify-center rounded-full bg-black/45 text-white backdrop-blur-sm transition-opacity hover:bg-black/65 ${r?"opacity-100":"pointer-events-none opacity-0"} ${i==="prev"?"left-2":"right-2"}`,children:n.jsx(o,{className:"size-4"})})}function Ub(){const i=F(),r=ll(i).filter(f=>f.prices.length>0);if(r.length===0)return null;const u=r[0].prices.map(f=>f.label),o=[ic(i,"extra_person_fee"),ic(i,"check_in_time"),ic(i,"check_out_time")].filter(f=>f!==void 0);return n.jsxs(pt,{id:"pricing",tone:"alt",title:qe(i,"pricing","요금 안내"),footnote:"'문의' 로 표시된 요금은 시기에 따라 달라져 전화로 안내드립니다.",children:[n.jsx("div",{className:"panel overflow-x-auto",children:n.jsxs("table",{className:"w-full min-w-[30rem] border-collapse text-[length:var(--fs-sm)]",children:[n.jsxs("caption",{className:"sr-only",children:[i.place.name," 객실별 1박 요금"]}),n.jsx("thead",{children:n.jsxs("tr",{className:"border-line border-b",children:[n.jsx("th",{scope:"col",className:"label px-5 py-3 text-left font-medium",children:"객실"}),u.map(f=>n.jsx("th",{scope:"col",className:"label px-5 py-3 text-right font-medium",children:f},f))]})}),n.jsx("tbody",{className:"divide-line divide-y",children:r.map(f=>n.jsxs("tr",{children:[n.jsx("th",{scope:"row",className:"px-5 py-3.5 text-left font-semibold",children:f.name}),f.prices.map(m=>n.jsx("td",{className:`px-5 py-3.5 text-right tabular-nums ${/\d/.test(m.value)?"font-bold":"text-muted"}`,children:m.value},m.label))]},f.unitId))})]})}),o.length>0&&n.jsx("dl",{className:"divide-line mt-4 grid gap-x-8 gap-y-2 sm:grid-cols-3 sm:divide-x",children:o.map(f=>n.jsxs("div",{className:"sm:px-5 sm:first:pl-0",children:[n.jsx("dt",{className:"label",children:f.label}),n.jsx("dd",{className:"text-[length:var(--fs-lead)] font-bold tabular-nums",children:f.value})]},f.label))})]})}function Rb(){const i=F(),r=et(i),u=u0(i),o=i.place.phone;return r.length===0&&u.length===0&&!o?null:n.jsx(pt,{id:"booking",tone:"alt",title:qe(i,"booking","예약 안내"),lead:`${i.place.name} 예약은 아래 경로로 받습니다.`,children:n.jsxs("div",{className:"grid gap-6 lg:grid-cols-12 lg:gap-10",children:[u.length>0&&n.jsx("dl",{className:"panel divide-line divide-y overflow-hidden px-5 lg:col-span-7",children:u.map(f=>n.jsx(yp,{label:f.label,value:f.value},f.label))}),n.jsx("div",{className:u.length>0?"lg:col-span-5":"lg:col-span-12",children:n.jsxs("div",{className:u.length>0?"flex flex-col gap-2.5 sm:flex-row sm:flex-wrap lg:flex-col":"flex flex-col items-start gap-2.5 sm:flex-row sm:flex-wrap",children:[o&&n.jsxs(Lh,{href:`tel:${o}`,icon:n.jsx(Jt,{className:"size-4"}),children:["전화 예약 ",o]}),r.map(f=>n.jsx(Lh,{href:f.url,variant:"outline",external:!0,children:Rl(f)},f.url))]})})]})})}function Hb(){const i=F(),r=f0(i);return r.length===0?null:n.jsx(pt,{id:"space",title:qe(i,"space","공간 안내"),lead:"좌석과 이용 환경 안내입니다.",children:n.jsx("dl",{className:"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 lg:gap-4",children:r.map(u=>n.jsxs(xo,{className:"p-4 sm:p-5",children:[n.jsx("dt",{className:"text-muted mb-1 text-[length:var(--fs-xs)] font-medium",children:u.label}),n.jsx("dd",{className:"text-[length:var(--fs-lead)] font-bold",children:u.value})]},u.label))})})}function Lb(){const i=F(),{place:r}=i,u=Ls(i);if(!r.phone&&!r.email&&u.length===0)return null;const o=[...r.phone?[{key:"phone",icon:Jt,label:"전화",value:r.phone,href:`tel:${r.phone}`}]:[],...r.email?[{key:"email",icon:ap,label:"이메일",value:r.email,href:`mailto:${r.email}`}]:[],...u.map(f=>({key:f.url,icon:Fu,label:Rl(f),value:"바로 가기",href:f.url,external:!0}))];return n.jsx(pt,{id:"inquiry",title:qe(i,"inquiry","문의 안내"),lead:`궁금한 점은 아래로 연락 주시면 ${r.name}에서 직접 안내해 드립니다.`,children:n.jsx("ul",{className:"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 lg:gap-4",children:o.map(f=>{const m=f.icon,h="external"in f&&f.external;return n.jsx("li",{children:n.jsxs("a",{href:f.href,...h?{target:"_blank",rel:"noopener noreferrer"}:{},className:"panel flex h-full items-center justify-between gap-3 p-5 transition-opacity hover:opacity-80",children:[n.jsxs("span",{className:"flex min-w-0 items-center gap-3",children:[n.jsx(m,{className:"size-5 shrink-0 opacity-40"}),n.jsxs("span",{className:"min-w-0",children:[n.jsx("span",{className:"text-muted block text-[length:var(--fs-xs)] font-medium",children:f.label}),n.jsx("span",{className:"block break-all text-[length:var(--fs-sm)] font-semibold",children:f.value})]})]}),h&&n.jsx(uc,{className:"size-4 shrink-0 opacity-40"})]})},f.key)})})})}function qb(){const i=F(),r=m0(i);return r.length===0?null:n.jsx(pt,{id:"exhibition",tone:"alt",title:qe(i,"exhibition","관람 안내"),lead:"방문 전 관람 조건을 확인해 주세요.",children:n.jsx("dl",{className:"panel divide-line divide-y overflow-hidden px-5",children:r.map(u=>n.jsx(yp,{label:u.label,value:u.value},u.label))})})}function jp(){const[i,r]=Y.useState([]);return Y.useEffect(()=>r(Jv()),[]),i}const Bb=["봄","여름","가을","겨울"];function Yb(){const i=F(),r=i.local.festivals,u=jp(),[o,f]=Y.useState(null);if(r.length===0)return null;const m=Bb.filter(y=>r.some(E=>E.season===y)),h=r.filter(y=>{var E;return!((E=y.season)!=null&&E.trim())}),x=[...u].reverse().find(y=>m.includes(y)),v="전체",p=o??x??null,b=p===v;return n.jsxs(pt,{id:"festival",title:"계절별 축제",lead:`${i.place.addressLocality??"이 지역"}의 축제와 행사를 계절로 묶었습니다.`,aside:p&&!b?n.jsxs("p",{className:"text-muted text-[length:var(--fs-xs)]",children:["지금은 ",p," 축제입니다"]}):void 0,footnote:n.jsx(n.Fragment,{children:"한국관광공사 TourAPI 기준. 일정은 주최 측 사정으로 바뀔 수 있습니다."}),children:[m.length>1&&n.jsx("div",{className:"mb-6 flex flex-wrap gap-1.5",role:"tablist","aria-label":"계절",children:[...m,v].map(y=>{const E=p===y;return n.jsx("button",{type:"button",role:"tab","aria-selected":E,onClick:()=>f(y),className:"border-line rounded-full border px-4 py-1.5 text-[length:var(--fs-sm)] font-bold transition-opacity hover:opacity-80",style:E?{backgroundColor:"var(--color-brand)",color:"var(--tpl-bg, #fff)",borderColor:"transparent"}:void 0,children:y},y)})}),n.jsxs("div",{className:"space-y-8",children:[m.map(y=>{const E=r.filter(T=>T.season===y);return n.jsxs("div",{hidden:!b&&p!==null&&p!==y,children:[n.jsx("h3",{className:"border-line mb-4 border-b pb-2 text-[length:var(--fs-sm)] font-bold",children:y}),n.jsx(Gh,{items:E,label:`${y} 축제`,remount:p})]},y)}),h.length>0&&n.jsxs("div",{children:[n.jsx("h3",{className:"border-line mb-4 border-b pb-2 text-[length:var(--fs-sm)] font-bold",children:"계절 없이 열리는 행사"}),n.jsx(Gh,{items:h,label:"계절 없이 열리는 행사"})]})]})]})}function Gh({items:i,label:r,remount:u}){return n.jsx(po,{label:r,children:i.map(o=>n.jsx(bp,{basis:"basis-[76%] sm:basis-1/3 lg:basis-1/4",children:n.jsxs("a",{href:Vu(o.searchQuery),target:"_blank",rel:"noopener noreferrer nofollow",className:"panel group flex h-full flex-col overflow-hidden transition-opacity hover:opacity-85",children:[n.jsxs("span",{className:"relative block aspect-4/3 overflow-hidden",children:[o.imageUrl?n.jsx("img",{src:o.imageUrl,alt:`${o.name} 사진`,loading:"lazy",decoding:"async",className:"size-full object-cover transition-transform duration-500 group-hover:scale-105"}):n.jsx("span",{className:"serif grid size-full place-items-center px-3 text-center text-[length:var(--fs-lead)] leading-tight",style:{backgroundColor:"color-mix(in oklab, currentColor 9%, transparent)"},"aria-hidden":!0,children:o.name}),n.jsx("span",{className:"absolute bottom-2 left-2 rounded-md px-2 py-0.5 text-[length:var(--fs-xs)] font-bold",style:{backgroundColor:"color-mix(in srgb, var(--tpl-inverse, #1c1917) 78%, transparent)",color:"var(--tpl-bg, #fff)"},children:o.month})]}),n.jsxs("span",{className:"flex min-w-0 flex-1 flex-col gap-1 p-3.5",children:[n.jsx("span",{className:"text-[length:var(--fs-sm)] font-bold",children:o.name}),o.period&&n.jsx("span",{className:"text-[length:var(--fs-xs)] font-medium opacity-80",children:o.period}),o.location&&n.jsxs("span",{className:"text-muted flex items-start gap-1 text-[length:var(--fs-xs)]",children:[n.jsx(Rs,{className:"mt-0.5 size-3 shrink-0 opacity-60"}),n.jsx("span",{children:o.location})]}),o.description&&n.jsx("span",{className:"text-muted line-clamp-3 pt-0.5 text-[length:var(--fs-xs)] leading-relaxed",children:o.description}),n.jsxs("span",{className:"text-muted mt-auto flex items-center gap-0.5 pt-1.5 text-[length:var(--fs-xs)] opacity-70 transition-opacity group-hover:opacity-100",children:[n.jsx("span",{children:"검색으로 열기"}),n.jsx(uc,{className:"size-3.5"})]})]})]})},o.name))},u??"all")}const _s=80,Qh=[{id:"all",label:"전체",test:()=>!0},{id:"w5",label:"걸어서 5분 이내",test:i=>i<=5*_s},{id:"w10",label:"걸어서 10분 이내",test:i=>i<=10*_s},{id:"far",label:"걸어서 10분 이상",test:i=>i>10*_s}];function Bu(i){if(Number.isFinite(i))return`도보 약 ${Math.max(1,Math.round(i/_s))}분`}function ws(i){const r=/^([\d.]+)\s*(km|m)$/i.exec((i??"").trim());return r?Number(r[1])*(r[2].toLowerCase()==="km"?1e3:1):1/0}function Gb(){const i=F(),{local:r}=i,u=r.restaurants.length>0||r.attractions.length>0,[o,f]=Y.useState("all");if(!u)return null;const m=[...r.restaurants,...r.attractions],h=b=>m.filter(y=>b.test(ws(y.distanceText))).length,x=Qh.filter((b,y)=>y===0||h(b)>0&&h(b)b.id===o)??Qh[0],p=b=>v.test(ws(b.distanceText));return n.jsxs(pt,{id:"guide",tone:"alt",title:"주변 안내",lead:`${i.place.addressLocality??"주변"} 지역의 맛집 · 명소 안내입니다.`,aside:r.syncedAt?n.jsxs("p",{className:"text-muted text-[length:var(--fs-xs)]",children:[lo(r.syncedAt)," 갱신"]}):void 0,footnote:n.jsxs(n.Fragment,{children:["도보 시간은 숙소에서 잰 직선거리를 분속 ",_s,"m 로 환산한 값입니다. 실제로 걷는 길은 이보다 깁니다."]}),children:[x.length>1&&n.jsx("div",{className:"mb-6 flex flex-wrap gap-1.5",role:"tablist","aria-label":"거리",children:x.map(b=>{const y=b.id===o,E=h(b);return n.jsxs("button",{type:"button",role:"tab","aria-selected":y,onClick:()=>f(b.id),className:"border-line rounded-full border px-4 py-1.5 text-[length:var(--fs-sm)] font-bold transition-opacity hover:opacity-80",style:y?{backgroundColor:"var(--color-brand)",color:"var(--tpl-bg, #fff)",borderColor:"transparent"}:void 0,children:[b.label," ",n.jsx("span",{className:"tabular-nums opacity-70",children:E})]},b.id)})}),n.jsxs("div",{className:"space-y-10",children:[r.restaurants.length>0&&n.jsx(Xh,{title:"주변 맛집",icon:Cv,places:r.restaurants,within:p}),r.attractions.length>0&&n.jsx(Xh,{title:"주변 명소",icon:Rs,places:r.attractions,within:p})]})]})}function Qb({icon:i,children:r}){return n.jsxs("h3",{className:"border-line mb-4 flex items-center gap-2 border-b pb-2 text-[length:var(--fs-sm)] font-bold",children:[n.jsx(i,{className:"size-4 opacity-50"}),n.jsx("span",{children:r})]})}function Xh({title:i,icon:r,places:u,within:o}){const f=u.filter(o);return n.jsxs("div",{children:[n.jsx(Qb,{icon:r,children:i}),n.jsx(po,{label:i,children:f.map(m=>n.jsx(bp,{basis:"basis-[76%] sm:basis-1/3 lg:basis-1/4",children:n.jsxs("a",{href:Vu(m.searchQuery),target:"_blank",rel:"noopener noreferrer nofollow",className:"panel group flex h-full flex-col overflow-hidden transition-opacity hover:opacity-85",children:[n.jsxs("span",{className:"relative block aspect-4/3 overflow-hidden",children:[m.imageUrl?n.jsx("img",{src:m.imageUrl,alt:`${m.name} 사진`,loading:"lazy",decoding:"async",className:"size-full object-cover transition-transform duration-500 group-hover:scale-105"}):n.jsx("span",{className:"serif grid size-full place-items-center px-3 text-center text-[length:var(--fs-lead)] leading-tight",style:{backgroundColor:"color-mix(in oklab, currentColor 9%, transparent)"},"aria-hidden":!0,children:m.name}),m.distanceText&&n.jsxs("span",{className:"absolute bottom-2 left-2 rounded-md px-2 py-0.5 text-[length:var(--fs-xs)] font-bold",style:{backgroundColor:"color-mix(in srgb, var(--tpl-inverse, #1c1917) 78%, transparent)",color:"var(--tpl-bg, #fff)"},children:[Bu(ws(m.distanceText))??m.distanceText,n.jsx("span",{className:"ml-1 font-normal opacity-70",children:m.distanceText})]})]}),n.jsxs("span",{className:"flex flex-1 flex-col gap-1 p-3.5",children:[n.jsx("span",{className:"text-[length:var(--fs-sm)] font-bold",children:m.name}),m.description&&n.jsx("span",{className:"text-muted line-clamp-3 text-[length:var(--fs-xs)] leading-relaxed",children:m.description}),n.jsxs("span",{className:"text-muted mt-auto flex items-center gap-0.5 pt-1.5 text-[length:var(--fs-xs)] opacity-70 transition-opacity group-hover:opacity-100",children:[n.jsx("span",{children:"검색으로 열기"}),n.jsx(uc,{className:"size-3.5"})]})]})]})},m.name))},f.length),n.jsx("ul",{hidden:!0,children:u.filter(m=>!o(m)).map(m=>n.jsxs("li",{children:[n.jsx("a",{href:Vu(m.searchQuery),rel:"nofollow",children:m.name}),m.distanceText&&n.jsxs("span",{children:[" ",m.distanceText,Bu(ws(m.distanceText))&&` · ${Bu(ws(m.distanceText))}`]}),m.description&&n.jsx("p",{children:m.description})]},m.name))}),f.length===0&&n.jsx("p",{className:"text-muted text-[length:var(--fs-sm)]",children:"이 거리 안에는 없습니다."})]})}function Xb(i){return i===0?"맑음":i>=1&&i<=3?"구름많음":i>=50&&i<=69?"비":i>=70&&i<=79?"눈":"흐림"}function $b({initial:i,regionCode:r,latitude:u,longitude:o}){const[f,m]=Y.useState(i);return Y.useEffect(()=>{if(!r||u==null||o==null)return;const h=new AbortController;async function x(){var p;try{const b=new URLSearchParams({region_code:r,latitude:String(u),longitude:String(o)}),y=await fetch(`/v1/local/weather?${b}`,{signal:h.signal});if(!y.ok)return;const E=await y.json();if(!((p=E==null?void 0:E.result)!=null&&p.success)||!E.weather)return;m(T=>({temperature:Number(E.weather.temperature),condition:Xb(Number(E.weather.weather_code)),note:T==null?void 0:T.note,observedAt:E.weather.observed_at,stale:!!E.stale}))}catch{}}x();const v=window.setInterval(()=>void x(),600*1e3);return()=>{h.abort(),window.clearInterval(v)}},[u,o,r]),f}function Vb(i){const r=/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/.exec(i??"");if(!r)return;const[,,u,o,f,m]=r;return`${Number(u)}월 ${Number(o)}일 ${f}:${m} 관측`}function Zb(){var x,v,p,b;const i=F(),r=$b({initial:i.local.weather,regionCode:i.place.regionCode,latitude:i.place.latitude,longitude:i.place.longitude});if(!r)return null;const u=a0(r.condition),o=n0(r.temperature),f=((v=(x=i.local.weather)==null?void 0:x.notes)==null?void 0:v[u])??r.note,m=(b=(p=i.local.weather)==null?void 0:p.tempNotes)==null?void 0:b[o],h=Vb(r.observedAt);return n.jsx(pt,{id:"weather",tone:"alt",title:"오늘의 날씨",children:n.jsxs(xo,{className:"relative overflow-hidden",children:[n.jsx("div",{className:"w4-sky","data-mood":u,"aria-hidden":!0}),n.jsxs("div",{className:"relative flex flex-col gap-5 p-6 sm:p-8",children:[n.jsxs("div",{className:"flex flex-wrap items-baseline gap-x-4 gap-y-1",children:[n.jsxs("p",{className:"flex items-baseline gap-2.5",children:[n.jsx("span",{className:"serif tabular-nums leading-none",style:{fontSize:"var(--fs-display)"},children:Math.round(r.temperature)}),n.jsx("span",{className:"text-[length:var(--fs-lead)] opacity-70",children:"°C"}),n.jsx("span",{className:"text-[length:var(--fs-lead)] font-semibold",children:r.condition})]}),h&&n.jsxs("span",{className:"text-muted text-[length:var(--fs-xs)] tabular-nums",children:[h,r.stale&&" · 최근 관측값"]})]}),f&&n.jsx("p",{className:"measure serif text-[length:var(--fs-lead)] leading-loose opacity-85",children:f}),m&&n.jsxs("p",{className:"measure flex items-start gap-2.5 text-[length:var(--fs-sm)] leading-relaxed opacity-75",children:[n.jsx("span",{className:"border-line mt-0.5 shrink-0 rounded-full border px-2 py-0.5 text-[length:var(--fs-xs)] font-bold","aria-hidden":!0,children:o}),n.jsx("span",{children:m})]})]})]})})}function Kb(){const i=F(),r=eo(i),u=i.theme.sections.find(_=>_.id==="photos"),o=(u==null?void 0:u.variantId)??"photos.grid",[f,m]=Y.useState(null),h=Y.useRef(null),[x,v]=Y.useState(0),[p,b]=Y.useState({prev:!1,next:!0}),y=Y.useCallback(()=>{const _=h.current;if(!_)return;const U=_.clientWidth,q=_.scrollWidth-U;v(U>0?Math.round(_.scrollLeft/U):0),b({prev:_.scrollLeft>8,next:_.scrollLeft(y(),window.addEventListener("resize",y),()=>window.removeEventListener("resize",y)),[y]),gc({box:h,interval:xc,advance:()=>ho(h.current,1)});const E=Y.useCallback(_=>{const U=h.current;if(!U)return;const q=window.matchMedia("(prefers-reduced-motion: reduce)").matches;U.scrollBy({left:_*U.clientWidth,behavior:q?"auto":"smooth"})},[]),T=Y.useCallback(()=>m(null),[]),N=Y.useCallback(_=>m(U=>U===null?null:(U+_+r.length)%r.length),[r.length]);return Y.useEffect(()=>{if(f===null)return;const _=q=>{q.key==="Escape"&&T(),q.key==="ArrowLeft"&&N(-1),q.key==="ArrowRight"&&N(1)},U=document.body.style.overflow;return document.body.style.overflow="hidden",window.addEventListener("keydown",_),()=>{document.body.style.overflow=U,window.removeEventListener("keydown",_)}},[f,T,N]),r.length===0?null:n.jsxs(pt,{id:"gallery",title:(u==null?void 0:u.name)||"공간 갤러리",children:[o==="photos.carousel"?n.jsxs("div",{className:"relative",children:[n.jsx("ul",{ref:h,onScroll:y,className:"flex snap-x snap-mandatory gap-3 overflow-x-auto [scrollbar-width:none] lg:grid lg:grid-cols-4 lg:gap-3 lg:snap-none lg:overflow-visible [&::-webkit-scrollbar]:hidden",children:r.map((_,U)=>n.jsx("li",{className:"w-full shrink-0 snap-center lg:w-auto",children:n.jsx(Yu,{image:_,onOpen:()=>m(U),className:"aspect-4/3 rounded-lg"})},_.mediaId))}),r.length>1&&n.jsxs(n.Fragment,{children:[n.jsx($h,{dir:"prev",show:p.prev,onClick:()=>E(-1)}),n.jsx($h,{dir:"next",show:p.next,onClick:()=>E(1)}),n.jsxs("span",{className:"pointer-events-none absolute bottom-3 right-3 rounded-full bg-black/55 px-2.5 py-1 text-[length:var(--fs-xs)] font-semibold tabular-nums text-white lg:hidden",children:[Math.min(x+1,r.length)," / ",r.length]})]})]}):o==="photos.masonry"?n.jsx("ul",{className:"columns-2 gap-3 sm:columns-3 lg:columns-4 [&>li]:mb-3",children:r.map((_,U)=>n.jsx("li",{className:"break-inside-avoid",children:n.jsx(Yu,{image:_,onOpen:()=>m(U),className:"rounded-lg",free:!0})},_.mediaId))}):n.jsx("ul",{className:"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4",children:r.map((_,U)=>n.jsx("li",{children:n.jsx(Yu,{image:_,onOpen:()=>m(U),className:"aspect-square rounded-lg"})},_.mediaId))}),f!==null&&n.jsxs("div",{role:"dialog","aria-modal":"true","aria-label":"사진 크게 보기",className:"fixed inset-0 z-50 flex items-center justify-center bg-black/92 p-4 backdrop-blur-md",onClick:T,children:[n.jsx("button",{type:"button",onClick:T,"aria-label":"닫기",className:"tap absolute right-3 top-3 z-10 flex items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/25 sm:right-5 sm:top-5",children:n.jsx(sp,{className:"size-6"})}),n.jsx(Vh,{dir:"prev",onClick:()=>N(-1)}),n.jsx(Vh,{dir:"next",onClick:()=>N(1)}),n.jsxs("figure",{className:"flex max-h-[85vh] w-full max-w-5xl flex-col items-center gap-3",onClick:_=>_.stopPropagation(),children:[n.jsx("img",{src:r[f].url,alt:r[f].alt,className:"max-h-[72vh] w-auto max-w-full rounded-lg object-contain"}),n.jsxs("figcaption",{className:"text-center text-[length:var(--fs-xs)] text-white/70",children:[r[f].caption??r[f].alt,n.jsxs("span",{className:"ml-2 tabular-nums",children:[f+1," / ",r.length]})]})]})]})]})}function $h({dir:i,show:r,onClick:u}){const o=i==="prev"?Ma:Oa;return n.jsx("button",{type:"button",onClick:u,"aria-label":i==="prev"?"이전 사진":"다음 사진","aria-hidden":!r,tabIndex:r?0:-1,className:`absolute top-1/2 z-10 flex size-9 -translate-y-1/2 items-center justify-center rounded-full bg-black/45 text-white backdrop-blur-sm transition-opacity hover:bg-black/65 lg:hidden ${r?"opacity-100":"pointer-events-none opacity-0"} ${i==="prev"?"left-2":"right-2"}`,children:n.jsx(o,{className:"size-5"})})}function Vh({dir:i,onClick:r}){const u=i==="prev"?Ma:Oa;return n.jsx("button",{type:"button",onClick:o=>{o.stopPropagation(),r()},"aria-label":i==="prev"?"이전 사진":"다음 사진",className:`tap absolute top-1/2 z-10 flex -translate-y-1/2 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/25 ${i==="prev"?"left-2 sm:left-6":"right-2 sm:right-6"}`,children:n.jsx(u,{className:"size-6"})})}function Yu({image:i,onOpen:r,className:u,free:o=!1}){return n.jsx("button",{type:"button",onClick:r,className:`border-line tpl-border group relative block w-full cursor-pointer overflow-hidden border ${u}`,"aria-label":`${i.alt} 크게 보기`,children:n.jsx("img",{src:i.url,alt:i.alt,loading:"lazy",decoding:"async",width:i.width,height:i.height,className:o?"h-auto w-full":"size-full object-cover transition-transform duration-500 group-hover:scale-105"})})}function Zh(){const i=F(),{place:r,routes:u}=i,[o,f]=Y.useState(!1),m=r.roadAddress??r.address;if(!m)return null;const{latitude:h,longitude:x}=r,v=h!=null&&x!=null,p=B0(r.name,h,x),b=async()=>{try{await navigator.clipboard.writeText(m),f(!0),setTimeout(()=>f(!1),2e3)}catch{}};return n.jsxs(pt,{id:"location",title:"오시는 길",lead:"주소와 주요 거점까지의 이동 시간을 안내합니다.",children:[n.jsxs("div",{className:"grid gap-6 lg:grid-cols-12 lg:gap-8",children:[v&&n.jsx("div",{className:"border-line tpl-border overflow-hidden rounded-xl border lg:col-span-7",children:n.jsx("iframe",{title:`${r.name} 위치 지도`,src:q0(h,x),loading:"lazy",referrerPolicy:"no-referrer-when-downgrade",className:"block aspect-4/3 w-full border-0 sm:aspect-video lg:aspect-auto lg:h-full lg:min-h-[24rem]"})}),n.jsx("div",{className:v?"lg:col-span-5":"lg:col-span-12",children:n.jsxs("div",{className:"panel p-5 sm:p-6",children:[n.jsx("p",{className:"label mb-2",children:"주소"}),n.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[n.jsx("address",{className:"text-[length:var(--fs-lead)] font-bold not-italic",children:m}),n.jsxs("button",{type:"button",onClick:b,title:"주소 복사",className:"border-line tpl-border inline-flex items-center gap-1 rounded-md border bg-current/8 px-2.5 py-1.5 text-[length:var(--fs-xs)] font-medium transition-opacity hover:opacity-70",children:[o?n.jsx(fv,{className:"size-3.5"}):n.jsx(xv,{className:"size-3.5"}),n.jsx("span",{children:o?"복사완료":"복사"})]})]}),r.phone&&n.jsxs("p",{className:"text-muted mt-2 text-[length:var(--fs-sm)]",children:["문의:"," ",n.jsx("a",{href:`tel:${r.phone}`,className:"underline-offset-2 hover:underline",children:r.phone})]}),n.jsxs("div",{className:"mt-5 flex flex-wrap gap-2.5",children:[n.jsxs("a",{href:pp(r.name,h,x),target:"_blank",rel:"noopener noreferrer nofollow",className:"tap flex flex-1 basis-40 items-center justify-center gap-2 whitespace-nowrap rounded-lg bg-[#03C75A] px-4 text-[length:var(--fs-sm)] font-bold text-white transition-opacity hover:opacity-90",children:[n.jsx(Lu,{className:"size-4"}),n.jsx("span",{children:"네이버 길찾기"})]}),n.jsxs("a",{href:H0(r.name,h,x),target:"_blank",rel:"noopener noreferrer nofollow",className:"tap flex flex-1 basis-40 items-center justify-center gap-2 whitespace-nowrap rounded-lg bg-[#FEE500] px-4 text-[length:var(--fs-sm)] font-bold text-[#191600] transition-opacity hover:opacity-90",children:[n.jsx(Lu,{className:"size-4"}),n.jsx("span",{children:"카카오 길찾기"})]}),p&&n.jsxs("a",{href:p,className:"tap only-touch flex-1 basis-40 items-center justify-center gap-2 whitespace-nowrap rounded-lg bg-[#0064FF] px-4 text-[length:var(--fs-sm)] font-bold text-white transition-opacity hover:opacity-90",children:[n.jsx(Lu,{className:"size-4"}),n.jsx("span",{children:"티맵 길찾기"})]})]})]})})]}),u.length>0&&n.jsxs("div",{className:"mt-8",children:[n.jsxs("h3",{className:"mb-3 flex items-center gap-2 text-[length:var(--fs-sm)] font-bold",children:[n.jsx(uv,{className:"size-4 opacity-50"}),n.jsx("span",{children:"주요 거점 소요시간"})]}),n.jsx("div",{className:"panel overflow-hidden",children:n.jsx("div",{className:"slider-viewport",children:n.jsxs("table",{className:"w-full min-w-[34rem] text-left text-[length:var(--fs-sm)]",children:[n.jsxs("caption",{className:"sr-only",children:[i.place.name,"에서 주요 거점까지의 이동 시간"]}),n.jsx("thead",{className:"panel-sunken border-line border-b font-semibold",children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",className:"p-4",children:"목적지"}),n.jsx("th",{scope:"col",className:"p-4",children:"차량"}),n.jsx("th",{scope:"col",className:"p-4",children:"도보 / 대중교통"}),n.jsx("th",{scope:"col",className:"p-4",children:"거리"}),n.jsx("th",{scope:"col",className:"hidden p-4 md:table-cell",children:"비고"})]})}),n.jsx("tbody",{className:"divide-line divide-y",children:u.map(y=>n.jsxs("tr",{children:[n.jsx("th",{scope:"row",className:"p-4 text-left font-bold",children:y.destination}),n.jsx("td",{className:"p-4 font-semibold",children:y.byCar??"—"}),n.jsx("td",{className:"p-4 opacity-70",children:y.byTransit??"—"}),n.jsx("td",{className:"text-muted p-4 tabular-nums",children:y.distanceText??"—"}),n.jsx("td",{className:"text-muted hidden p-4 md:table-cell",children:y.note??""})]},y.destination))})]})})})]})]})}function Jb(){const i=F(),r=s0(i);return r.length===0?null:n.jsx(pt,{id:"faq",tone:"alt",title:"자주 묻는 질문",lead:"아래 답변은 모두 사업자가 확인한 내용입니다.",children:n.jsx("div",{className:r.length>4?"grid gap-3 lg:grid-cols-2 lg:gap-4":"space-y-3",children:r.map((u,o)=>n.jsxs("details",{open:o===0,className:"panel group h-fit overflow-hidden",children:[n.jsxs("summary",{className:"tap flex cursor-pointer items-center gap-3 px-5 text-[length:var(--fs-sm)] font-bold marker:content-none [&::-webkit-details-marker]:hidden",children:[n.jsx("span",{className:"serif text-muted shrink-0",children:"Q."}),n.jsx("span",{className:"flex-1",children:u.question}),n.jsx("span",{className:"text-muted shrink-0 transition-transform group-open:rotate-45",children:"+"})]}),n.jsx("div",{className:"border-line border-t px-5 pb-5 pt-4 text-[length:var(--fs-sm)] leading-relaxed opacity-80",children:u.answer})]},u.faqId))})})}function Np(){var m,h,x,v;const i=F(),{place:r,site:u}=i,o=i.links.filter(p=>p.confirmed),f=r.roadAddress??r.address;return n.jsx("footer",{className:"w-full pb-28 pt-14 md:pb-14",style:{backgroundColor:"var(--tpl-inverse, #1c1917)",color:"var(--tpl-bg, #ffffff)"},children:n.jsxs("div",{className:"shell",children:[n.jsxs("div",{className:"grid grid-cols-1 gap-8 border-b border-current/15 pb-10 md:grid-cols-12",children:[n.jsxs("div",{className:"space-y-4 md:col-span-7",children:[n.jsx("p",{className:"serif text-[length:var(--fs-lead)] font-bold",children:r.name}),n.jsxs("div",{className:"space-y-2 text-[length:var(--fs-sm)] opacity-75",children:[f&&n.jsxs("p",{className:"flex items-start gap-2",children:[n.jsx(Rs,{className:"mt-1 size-4 shrink-0 opacity-60"}),n.jsx("span",{children:f})]}),r.phone&&n.jsxs("p",{className:"flex items-center gap-2",children:[n.jsx(Jt,{className:"size-4 shrink-0 opacity-60"}),n.jsx("a",{href:`tel:${r.phone}`,className:"underline-offset-2 hover:underline",children:r.phone})]}),r.email&&n.jsxs("p",{className:"flex items-center gap-2",children:[n.jsx(ap,{className:"size-4 shrink-0 opacity-60"}),n.jsx("a",{href:`mailto:${r.email}`,className:"underline-offset-2 hover:underline",children:r.email})]})]})]}),o.length>0&&n.jsxs("nav",{"aria-label":"공식 채널",className:"md:col-span-5",children:[n.jsx("h2",{className:"label mb-3 !text-current opacity-60",children:"공식 채널"}),n.jsx("ul",{className:"flex flex-wrap gap-2",children:o.map(p=>n.jsx("li",{children:n.jsxs("a",{href:p.url,target:"_blank",rel:"noopener noreferrer",className:"inline-flex items-center gap-1.5 rounded-lg border border-current/25 px-3 py-2 text-[length:var(--fs-xs)] font-medium transition-colors hover:bg-current/10",children:[n.jsx("span",{children:Rl(p)}),n.jsx(vv,{className:"size-3.5"})]})},p.url))})]})]}),n.jsxs("div",{className:"flex flex-col gap-3 pt-6 text-[length:var(--fs-xs)] opacity-55 sm:flex-row sm:items-center sm:justify-between",children:[n.jsxs("div",{className:"flex flex-wrap items-center gap-x-3 gap-y-1",children:[n.jsxs("span",{children:["상호: ",r.name]}),((m=r.legal)==null?void 0:m.representative)&&n.jsxs("span",{children:["대표: ",r.legal.representative]}),((h=r.legal)==null?void 0:h.businessRegistrationNumber)&&n.jsxs("span",{children:["사업자등록번호: ",r.legal.businessRegistrationNumber]}),((x=r.legal)==null?void 0:x.mailOrderNumber)&&n.jsxs("span",{children:["통신판매업신고: ",r.legal.mailOrderNumber]}),((v=r.legal)==null?void 0:v.licenseNumber)&&n.jsxs("span",{children:[r.legal.licenseLabel??"인허가번호",": ",r.legal.licenseNumber]})]}),n.jsxs("p",{className:"byline shrink-0",children:[n.jsxs("span",{className:"author",children:["작성·운영 ",r.name]})," · 최종 업데이트: ",n.jsx("time",{dateTime:u.updatedAt,children:qs(u.updatedAt)})]})]})]})})}function zn(){const i=F(),r=i.place.phone,o=et(i)[0],f=i.links.find(m=>m.confirmed&&/kakao/i.test(m.url));return!r&&!o&&!f?null:n.jsxs("nav",{"aria-label":"연락 · 예약",className:"border-line safe-b fixed inset-x-0 bottom-0 z-50 flex items-stretch gap-2 border-t px-3 pt-2 backdrop-blur-md lg:hidden",style:{backgroundColor:"color-mix(in srgb, var(--tpl-surface, #fff) 94%, transparent)"},children:[n.jsxs("a",{href:"#location",className:"tap border-line flex w-12 shrink-0 flex-col items-center justify-center rounded-lg border text-[10px] font-medium","aria-label":"오시는 길",children:[n.jsx(Rs,{className:"size-4"}),n.jsx("span",{className:"mt-0.5",children:"위치"})]}),f&&n.jsxs("a",{href:f.url,target:"_blank",rel:"noopener noreferrer",className:"tap border-line flex w-12 shrink-0 flex-col items-center justify-center rounded-lg border text-[10px] font-medium","aria-label":Rl(f),children:[n.jsx(Fu,{className:"size-4"}),n.jsx("span",{className:"mt-0.5",children:"문의"})]}),r&&n.jsxs("a",{href:`tel:${r}`,className:"tap border-line flex flex-1 items-center justify-center gap-1.5 rounded-lg border text-[length:var(--fs-sm)] font-bold",children:[n.jsx(Jt,{className:"size-4"}),n.jsx("span",{children:"전화"})]}),o&&n.jsxs("a",{href:o.url,target:"_blank",rel:"noopener noreferrer",className:"tap flex min-w-0 flex-[1.4] items-center justify-center truncate rounded-lg px-2 text-[length:var(--fs-sm)] font-bold",style:{backgroundColor:"var(--color-brand)",color:"var(--tpl-bg, #fff)"},children:[Pe(o)," 예약"]})]})}const Vt="var(--tpl-card, #fafafa)",be="var(--tpl-border, #d6d3d1)",Ke="color-mix(in oklab, var(--tpl-accent, #2563eb) 70%, currentColor)",Cs="var(--tpl-inverse, #1c1917)",Ul="var(--tpl-bg, #ffffff)",Zt="var(--tpl-text, #09090b)";function Sp({n:i}){return n.jsx("span",{className:"mr-1.5 inline-grid size-[17px] translate-y-px place-items-center rounded-full text-[10px] tabular-nums",style:{backgroundColor:Ke,color:Ul},children:i})}function ml({id:i,name:r,subtitle:u,count:o,link:f,children:m,dark:h}){const x=hc(),v=x==="reservation"?no:x==="oasi"?so:x==="studio"?io:x==="pastel"?co:x==="editorial"?to:null,p=f?n.jsxs("a",{href:f.url,target:"_blank",rel:"noopener noreferrer",className:"text-[length:var(--fs-sm)] font-semibold underline-offset-4 opacity-75 transition-opacity hover:opacity-100 hover:underline",children:[f.label," →"]}):void 0;return n.jsx("section",{id:i,"aria-labelledby":`${i}-heading`,className:"border-line paper w-full border-b",style:{backgroundColor:h?Cs:"var(--tpl-surface, #fafafa)",color:h?Ul:Zt,paddingBlock:"var(--section-space)"},children:n.jsxs("div",{className:"shell",children:[v?n.jsx(v,{id:i,title:r,lead:u,aside:p}):n.jsxs("header",{className:"mb-8 sm:mb-10",children:[n.jsxs("div",{className:"flex flex-wrap items-baseline justify-between gap-x-4 gap-y-2",children:[n.jsx("h2",{id:`${i}-heading`,className:"h2",children:r}),p]}),u&&n.jsx("p",{className:"measure mt-3 text-[length:var(--fs-sm)] opacity-70",children:u})]}),m,o&&n.jsx("p",{className:"mt-6 text-[length:var(--fs-xs)] opacity-60",children:o})]})})}function En({children:i,label:r,arrows:u="header",onSelect:o,onReady:f}){return n.jsx(po,{label:r,gap:1,arrows:u,onSelect:o,onReady:f,children:i})}function Hl({source:i,verified:r}){return i!=null&&i.name?n.jsx("p",{className:"flex flex-wrap items-center gap-x-2 gap-y-1 text-[10px] opacity-70",children:(i==null?void 0:i.name)&&n.jsxs("span",{children:["출처 ·"," ",i.url?n.jsx("a",{href:i.url,target:"_blank",rel:"noopener noreferrer nofollow",className:"underline underline-offset-2",children:i.name}):i.name]})}):null}function Us(i,r,u){return`총 ${i}${u}`}const Kh=Ke;function zp(){const i=F(),r=al(i,"songs"),[u,o]=Y.useState(0);if(r.items.length===0)return null;const f=r.items[u]??r.items[0],m=h=>[h.artist,h.year?String(h.year):void 0,h.lyricist||h.composer?`작사 ${h.lyricist??"미상"} / 작곡 ${h.composer??"미상"}`:void 0,h.label].filter(Boolean).join(" · ");return n.jsxs(ml,{id:"songs",name:r.title||qe(i,"songs","가요 다방"),subtitle:r.subtitle,count:Us(r.items.length,r.unverified,"곡"),children:[n.jsxs("div",{className:"tpl-border grid items-center gap-8 border p-6 sm:p-8 md:grid-cols-[240px_minmax(0,1fr)]",style:{backgroundColor:Cs,color:Ul,borderColor:"color-mix(in oklab, currentColor 22%, transparent)"},children:[n.jsxs("div",{className:"relative mx-auto size-[220px]",children:[n.jsx("div",{className:"absolute inset-0 rounded-full",style:{backgroundColor:"color-mix(in oklab, currentColor 16%, transparent)"}}),n.jsx("div",{className:"w4-disc w4-spin absolute inset-2 rounded-full",style:{"--lbl":f.labelColor||Kh,"--w4-vinyl":Cs},children:n.jsx("span",{className:"absolute left-1/2 top-1/2 size-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full",style:{backgroundColor:"color-mix(in oklab, currentColor 30%, transparent)"}})}),n.jsxs("div",{"aria-hidden":!0,className:"absolute -right-1 top-3 h-2 w-[110px] origin-right rotate-6",children:[n.jsx("span",{className:"absolute inset-y-[3px] left-0 right-4 rounded-sm",style:{backgroundColor:"color-mix(in oklab, currentColor 65%, transparent)"}}),n.jsx("span",{className:"absolute -top-2 right-0 size-6 rounded-full",style:{backgroundColor:"color-mix(in oklab, currentColor 55%, transparent)"}})]})]}),n.jsx("div",{className:"min-w-0",children:r.items.map((h,x)=>n.jsxs("div",{hidden:x!==u,className:"space-y-3",children:[n.jsxs("p",{className:"text-[10px] tracking-[0.24em]",style:{color:Ke},children:["A면 · ",x+1," / ",r.items.length]}),n.jsx("h3",{className:"serif text-[length:var(--fs-h2)] leading-tight",children:h.title}),n.jsx("p",{className:"text-[length:var(--fs-xs)] opacity-60",children:m(h)}),h.story&&n.jsx("p",{className:"measure text-[length:var(--fs-sm)] leading-relaxed opacity-85",children:h.story}),h.connection&&n.jsx("p",{className:"measure text-[length:var(--fs-sm)] leading-relaxed",style:{color:Ke},children:h.connection}),n.jsxs("a",{href:R0(`${h.title} ${h.artist??""}`.trim()),target:"_blank",rel:"noopener noreferrer nofollow",className:"inline-flex items-center gap-1.5 text-[length:var(--fs-sm)] font-bold underline-offset-4 hover:underline",style:{color:Ke},children:[n.jsx(Dv,{className:"size-4"}),n.jsx("span",{children:"유튜브에서 듣기"})]}),n.jsx("span",{className:"block w-fit border border-dashed px-2 py-1 text-[10px] tracking-[0.1em] opacity-60",style:{borderColor:"color-mix(in oklab, currentColor 35%, transparent)"},children:"◎ 가사 대신 이야기 — 원문은 싣지 않습니다"}),n.jsx(Hl,{source:h.source,verified:h.verified})]},`disc-${h.title}-${x}`))})]}),r.items.length>1&&n.jsx("div",{className:"mt-4 flex flex-wrap justify-center gap-4",children:r.items.map((h,x)=>n.jsxs("button",{type:"button",onClick:()=>o(x),"aria-pressed":x===u,className:"w-24 text-center",children:[n.jsx("span",{className:"w4-disc-mini mx-auto block size-[76px] rounded-full transition-transform hover:scale-105",style:{"--lbl":h.labelColor||Kh,"--w4-vinyl":Cs,boxShadow:x===u?`0 0 0 2px ${Ke}`:void 0}}),n.jsx("span",{className:"mt-2 block truncate text-[10px] leading-tight opacity-70",children:h.title})]},`${h.title}-${x}`))})]})}function Ep(){const i=F(),r=al(i,"daily"),[u,o]=Y.useState();if(Y.useEffect(()=>{const v=new Date;o(`${String(v.getMonth()+1).padStart(2,"0")}-${String(v.getDate()).padStart(2,"0")}`)},[]),r.items.length===0)return null;const f=[...r.items].sort((v,p)=>v.monthDay.localeCompare(p.monthDay)),m=u===void 0?1:Math.max(0,(()=>{const v=f.findIndex(b=>b.monthDay===u);if(v>=0)return v;const p=f.findIndex(b=>b.monthDay>u);return p>=0?p:0})()),h=v=>f[(m+v+f.length)%f.length],x=f.length===1?[h(0)]:f.length===2?[h(0),h(1)]:[h(-1),h(0),h(1)];return n.jsx(ml,{id:"daily",name:r.title||qe(i,"daily","오늘의 한 장"),subtitle:r.subtitle,count:Us(f.length,r.unverified,"장"),children:n.jsx("div",{className:"flex items-start justify-center gap-4 sm:gap-6",children:f.map((v,p)=>{const b=x.indexOf(v);return n.jsx(Wb,{page:v,hidden:b<0,isToday:x.length===1||b===1,muted:x.length>1&&b!==1},`${v.monthDay}-${p}`)})})})}function Wb({page:i,isToday:r,muted:u,hidden:o}){const[f,m]=i.monthDay.split("-");return n.jsxs("article",{hidden:o,className:`w4-paper shrink-0 border transition-opacity ${u?"hidden w-[168px] opacity-45 sm:block":"w-[254px] max-w-full"}`,style:{backgroundColor:Vt,borderColor:r?Ke:be,boxShadow:r?"5px 6px 0 color-mix(in oklab, currentColor 14%, transparent)":void 0},"aria-current":r?"date":void 0,children:[n.jsx("div",{className:"w4-perf h-3.5 border-b border-dashed",style:{"--tear":Vt,borderColor:be}}),n.jsxs("div",{className:"border-b px-5 pb-3 pt-5 text-center",style:{borderColor:be},children:[n.jsxs("p",{className:"text-[11px] tracking-[0.2em] opacity-60",children:[f,"月"]}),n.jsx("p",{className:`serif mt-1 font-bold leading-none ${u?"text-4xl":"text-6xl"}`,style:{color:Ke},children:String(Number(m)||m)}),r&&n.jsx("p",{className:"mt-1.5 text-[10px] tracking-[0.3em]",style:{color:Ke},children:"오늘"})]}),n.jsxs("div",{className:"space-y-2 px-5 pb-5 pt-4",children:[i.category&&n.jsx("p",{className:"text-[10px] tracking-[0.16em] opacity-60",children:i.category}),n.jsx("h3",{className:"serif text-base font-bold leading-snug",children:i.title}),!u&&i.body&&n.jsx("p",{className:"text-[13px] leading-relaxed opacity-80",children:i.body}),!u&&n.jsx("div",{className:"border-t border-dashed pt-2.5",style:{borderColor:be},children:n.jsx(Hl,{source:i.source,verified:i.verified})})]})]})}function Ap(){const i=F(),r=al(i,"people");return r.items.length===0?null:n.jsx(ml,{id:"people",name:r.title||qe(i,"people","인물 열전"),subtitle:r.subtitle,count:r.items.some(u=>!u.imageUrl)?`${Us(r.items.length,r.unverified,"명")} · 사진이 없는 인물은 이름 활자로 대신합니다`:Us(r.items.length,r.unverified,"명"),dark:!0,children:n.jsxs("div",{className:"tpl-border border",style:{backgroundColor:Cs,borderColor:"color-mix(in oklab, currentColor 22%, transparent)"},children:[n.jsx("div",{className:"w4-film-perf h-2.5 opacity-70"}),n.jsx("div",{className:"px-3 py-4",children:n.jsx(En,{label:"인물 목록",arrows:"overlay",children:r.items.map((u,o)=>n.jsxs("article",{className:"w-[210px] shrink-0 snap-center",children:[u.imageUrl?n.jsx("img",{src:u.imageUrl,alt:`${u.name} 사진`,loading:"lazy",decoding:"async",className:"aspect-3/4 w-full border object-cover",style:{borderColor:"color-mix(in oklab, currentColor 28%, transparent)"}}):n.jsx("span",{className:"serif grid aspect-3/4 w-full place-items-center border text-5xl",style:{backgroundColor:"color-mix(in oklab, currentColor 14%, transparent)",borderColor:"color-mix(in oklab, currentColor 28%, transparent)"},"aria-hidden":!0,children:u.name.trim().charAt(0)}),n.jsxs("h3",{className:"serif mt-3 text-base font-bold",children:[u.name,u.aka&&n.jsxs("span",{className:"ml-1.5 text-xs opacity-60",children:["호 ",u.aka]})]}),n.jsx("p",{className:"mt-0.5 text-[11px] opacity-60",children:[u.role,u.years].filter(Boolean).join(" · ")}),u.oneLine&&n.jsx("p",{className:"mt-2 text-[13px] leading-relaxed opacity-85",children:u.oneLine}),n.jsx("div",{className:"mt-2",children:n.jsx(Hl,{source:u.source,verified:u.verified})})]},`${u.name}-${o}`))})}),n.jsx("div",{className:"w4-film-perf h-2.5 opacity-70"})]})})}function Tp(){const i=F(),r=al(i,"chronicle");if(r.items.length===0)return null;const u=[...r.items].sort((f,m)=>f.year==null?m.year==null?0:1:m.year==null?-1:f.year-m.year),o=u.filter(f=>f.turning===!0).length;return n.jsx(ml,{id:"chronicle",name:r.title||qe(i,"chronicle","시간의 골목"),subtitle:r.subtitle,count:`붉은 점은 도시의 성격이 바뀐 해입니다 · ${o}개 / 전체 ${u.length}개`,children:n.jsx(En,{label:"연표",children:u.map((f,m)=>{const h=f.turning===!0;return n.jsxs("article",{className:"w-[228px] shrink-0 snap-center",children:[n.jsx("p",{className:"serif text-3xl font-bold leading-none",style:{color:h?Ke:void 0},children:f.year??"연도 미상"}),n.jsxs("div",{className:"relative my-3 h-3",children:[n.jsx("i",{"aria-hidden":!0,className:"absolute top-1/2 left-0 border-t",style:{borderColor:be,right:m===u.length-1?"calc(100% - 12px)":0}}),n.jsx("i",{"aria-hidden":!0,className:"absolute top-1/2 left-0 size-3 -translate-y-1/2 rounded-full border-2",style:{backgroundColor:h?Ke:Vt,borderColor:h?Ke:be}})]}),n.jsxs("div",{className:"flex gap-2.5 pr-5",children:[f.imageUrl&&n.jsx("img",{src:f.imageUrl,alt:`${f.title} 사진`,loading:"lazy",decoding:"async",className:"size-14 shrink-0 border object-cover",style:{borderColor:be}}),n.jsxs("div",{className:"min-w-0 flex-1 space-y-1.5",children:[n.jsx("h3",{className:"serif text-base font-bold",children:f.title}),f.summary&&n.jsx("p",{className:"text-[13px] leading-relaxed opacity-80",children:f.summary}),f.place&&n.jsxs("p",{className:"text-[11px] opacity-60",children:["지금 이 자리 · ",f.place]}),n.jsx(Hl,{source:f.source,verified:f.verified})]})]})]},`${f.year??"x"}-${f.title}-${m}`)})})})}function _p(){const i=F(),r=al(i,"postcard");return r.items.length===0?null:n.jsx(ml,{id:"postcard",name:r.title||qe(i,"postcard","오늘의 엽서"),subtitle:r.subtitle,count:Us(r.items.length,r.unverified,"장"),children:n.jsx(En,{label:"엽서 목록",children:r.items.map((u,o)=>n.jsxs("article",{className:"w4-paper w-[304px] shrink-0 snap-center border p-4",style:{backgroundColor:Vt,borderColor:Zt},children:[u.imageUrl&&n.jsx("img",{src:u.imageUrl,alt:`${u.place||u.postmark||"엽서"} 사진`,loading:"lazy",decoding:"async",className:"mb-3.5 block aspect-3/2 w-full border object-cover",style:{borderColor:be}}),n.jsxs("div",{className:"grid grid-cols-[minmax(0,1fr)_auto] gap-3",children:[n.jsxs("div",{className:"min-w-0 border-r pr-3",style:{borderColor:be},children:[n.jsxs("p",{className:"serif text-[17px] leading-snug",children:["“",u.line,"”"]}),u.hashtags&&u.hashtags.length>0&&n.jsx("p",{className:"mt-2 text-[11px] opacity-60",children:u.hashtags.join(" ")})]}),n.jsxs("div",{className:"flex w-[62px] flex-col items-center gap-3",children:[n.jsxs("span",{className:"grid h-[56px] w-[44px] place-items-center border border-dashed text-center text-[9px] leading-tight opacity-60",style:{borderColor:be},children:["郵票",n.jsx("br",{}),"10원"]}),n.jsx("span",{className:"serif grid size-[54px] -rotate-6 place-items-center rounded-full border-2 px-1 text-center text-[9px] leading-tight",style:{borderColor:Ke,color:Ke},children:u.postmark||u.place||"소인"})]})]}),n.jsx("div",{className:"mt-3 border-t border-dashed pt-2.5",style:{borderColor:be},children:n.jsx(Hl,{source:u.source,verified:u.verified})})]},`${u.line}-${o}`))})})}function Fb(){const i=F(),r=al(i,"quiz"),[u,o]=Y.useState(new Set);if(r.items.length===0)return null;const f=m=>o(h=>{const x=new Set(h);return x.has(m)?x.delete(m):x.add(m),x});return n.jsx(ml,{id:"quiz",name:r.title||qe(i,"quiz","뒤집어 보는 질문"),subtitle:r.subtitle,count:`정답은 두지 않습니다 — 힌트와 출처까지만 · 총 ${r.items.length}문항`,children:n.jsx(En,{label:"질문 목록",children:r.items.map((m,h)=>{const x=u.has(h);return n.jsx("button",{type:"button",onClick:()=>f(h),"aria-pressed":x,"aria-label":`${h+1}번 문제 ${x?"앞면 보기":"힌트 보기"}`,className:`w4-flip ${x?"w4-flip-on":""} h-[260px] w-[262px] shrink-0 text-left`,children:n.jsxs("span",{className:"w4-flip-inner block size-full",children:[n.jsxs("span",{className:"w4-flip-face w4-paper flex size-full flex-col gap-3 border p-4",style:{backgroundColor:Vt,borderColor:Zt},children:[n.jsxs("span",{className:"text-[10px] tracking-[0.16em] opacity-60",children:["문제 ",h+1,m.level?` · ${m.level}`:""]}),n.jsx("span",{className:"serif text-[17px] font-bold leading-snug",children:m.question}),n.jsx("span",{className:"mt-auto text-[10px] tracking-[0.16em]",style:{color:Ke},children:"눌러서 힌트 보기 →"})]}),n.jsxs("span",{className:"w4-flip-face w4-flip-back w4-paper flex size-full flex-col gap-3 border p-4",style:{backgroundColor:Vt,borderColor:Ke},children:[n.jsx("span",{className:"text-[10px] tracking-[0.16em]",style:{color:Ke},children:"힌트"}),n.jsx("span",{className:"text-[13px] leading-relaxed opacity-85",children:m.hint??"힌트가 아직 없습니다."}),n.jsxs("span",{className:"mt-auto space-y-1 border-t border-dashed pt-2.5",style:{borderColor:be},children:[m.topic&&n.jsx("span",{className:"block text-[10px] opacity-55",children:m.topic}),n.jsx(Hl,{source:m.source,verified:m.verified})]})]})]})},`${m.question}-${h}`)})})})}const kl=256,As=318,Nn=168,Jh=34,Ib=16,Wh=4;function wp(i,r,u){const o=kl*2**u,f=i*Math.PI/180;return{x:(r+180)/360*o,y:(1-Math.log(Math.tan(f)+1/Math.cos(f))/Math.PI)/2*o}}function Pb(i){if(i.length<2)return 15;for(let r=Ib;r>Wh;r-=1){const u=i.map(m=>wp(m.lat,m.lng,r)),o=Math.max(...u.map(m=>m.x))-Math.min(...u.map(m=>m.x)),f=Math.max(...u.map(m=>m.y))-Math.min(...u.map(m=>m.y));if(o<=As-Jh*2&&f<=Nn-Jh*2)return r}return Wh}function ey(i,r){const u=m=>m.latitude!=null&&m.longitude!=null?{name:m.searchQuery??m.name,lat:m.latitude,lng:m.longitude}:void 0,o=[];let f=r;return i.forEach((m,h)=>{const x=u(m);x&&(o.push({label:String(h+1),...x,stop:m,from:f&&!ty(f,x)?f:void 0}),f=x)}),o}function ty(i,r){return Math.abs(i.lat-r.lat)<1e-5&&Math.abs(i.lng-r.lng)<1e-5}function ly(i){const u=i.map(o=>({...o}));for(let o=1;oMath.hypot(u[o].x-b.x,u[o].y-b.y)<27);if(!m)break;const h=u[o].x-m.x,x=u[o].y-m.y,v=Math.hypot(h,x)||1,p=(27-v)/v;u[o].x+=(h||1)*p,u[o].y+=x*p}return u}function ay(i){return i.from?L0(i.from,{name:i.stop.name,lat:i.lat,lng:i.lng}):pp(i.stop.searchQuery??i.stop.name,i.lat,i.lng)}function Cp({stops:i}){const{place:r}=F(),u=r.latitude!=null&&r.longitude!=null?{name:r.name,lat:r.latitude,lng:r.longitude}:void 0,o=i.filter(N=>N.latitude!=null&&N.longitude!=null);if(o.length===0||o.length===1&&i.length>1)return null;const f=ey(i,u),m=Pb(f),h=ly(f.map(N=>wp(N.lat,N.lng,m))),x=(Math.min(...h.map(N=>N.x))+Math.max(...h.map(N=>N.x)))/2,v=(Math.min(...h.map(N=>N.y))+Math.max(...h.map(N=>N.y)))/2,p=x-As/2,b=v-Nn/2,y=2**m,E=[];for(let N=Math.floor(p/kl);N<=Math.floor((p+As)/kl);N+=1)for(let _=Math.floor(b/kl);_<=Math.floor((b+Nn)/kl);_+=1){if(_<0||_>=y)continue;const U=(N%y+y)%y;E.push({key:`${N}-${_}`,url:`https://tile.openstreetmap.org/${m}/${U}/${_}.png`,left:N*kl-p,top:_*kl-b})}const T=h.map(N=>`${(N.x-p).toFixed(1)},${(N.y-b).toFixed(1)}`).join(" ");return n.jsxs("div",{className:"relative mt-3.5 overflow-hidden border-y",style:{height:Nn,borderColor:be,backgroundColor:"#e8e5df"},children:[E.map(N=>n.jsx("img",{src:N.url,alt:"",width:kl,height:kl,loading:"lazy",decoding:"async",className:"pointer-events-none absolute max-w-none",style:{left:N.left,top:N.top,filter:"grayscale(1) contrast(0.95) opacity(0.55)"}},N.key)),n.jsx("svg",{className:"pointer-events-none absolute inset-0",width:As,height:Nn,viewBox:`0 0 ${As} ${Nn}`,children:n.jsx("polyline",{points:T,fill:"none",stroke:Ke,strokeWidth:"2",strokeDasharray:"5 4",strokeLinecap:"round",strokeLinejoin:"round",opacity:"0.85"})}),f.map((N,_)=>{const U=h[_];return n.jsx("a",{href:ay(N),target:"_blank",rel:"noopener noreferrer",title:N.from?`${N.from.name} → ${N.stop.name} 길찾기`:`${N.stop.name} — 네이버 지도`,className:"absolute grid h-[22px] min-w-[22px] -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full px-1.5 text-[11px] font-bold whitespace-nowrap shadow-sm transition-transform hover:scale-110",style:{left:U.x-p,top:U.y-b,backgroundColor:Ke,color:Ul,border:`1.5px solid ${Ul}`},children:N.label},N.label)}),n.jsx("span",{className:"absolute right-1 bottom-0.5 bg-white/70 px-1 text-[9px] leading-tight text-stone-600",children:"© OpenStreetMap"})]})}function ny(i){const r=Math.floor(i/60),u=i%60;return[r>0?`${r}시간`:"",u>0?`${u}분`:""].filter(Boolean).join(" ")||"0분"}function Ju(i){var r;return(r=i.days)!=null&&r.length?i.days.map(u=>({label:u.label,startTime:u.startTime,stops:u.stops??[]})):[{startTime:i.startTime,stops:i.stops??[]}]}function sy({item:i,badge:r,startTime:u,stops:o,first:f}){const m=up({name:i.name,startTime:u,stops:o});return n.jsxs("article",{className:"w4-paper w-[320px] shrink-0 snap-center border",style:{backgroundColor:Vt,borderColor:be},children:[n.jsxs("div",{className:"flex items-center justify-between gap-2 border-b px-4 py-2.5",style:{borderColor:be},children:[n.jsx("span",{className:"border px-2 py-0.5 text-[11px] font-bold",style:f?{backgroundColor:Zt,color:Ul,borderColor:Zt}:{borderColor:be},children:r}),n.jsxs("span",{className:"text-[11px] opacity-60",children:[m.from,"–",m.to," · ",ny(m.totalMinutes)]})]}),n.jsxs("div",{className:"space-y-1.5 px-4 pt-3.5",children:[n.jsx("h3",{className:"serif text-lg font-bold",children:i.name}),f&&i.audience&&n.jsx("p",{className:"text-[12px] opacity-60",children:i.audience}),f&&i.why&&n.jsx("p",{className:"text-[13px] leading-relaxed opacity-80",children:i.why})]}),n.jsx(Cp,{stops:m.stops.map(h=>h.stop)}),n.jsx("ol",{className:"mt-3 px-4 pb-3",children:m.stops.map((h,x)=>n.jsxs("li",{className:"grid grid-cols-[46px_minmax(0,1fr)] gap-2.5",children:[n.jsx("span",{className:"serif pt-2 text-[12px] tabular-nums opacity-75",children:h.time}),n.jsxs("div",{className:"border-l pb-5 pl-3",style:{borderColor:be},children:[h.move>0&&n.jsxs("p",{className:"pt-1 text-[10px] opacity-50",children:["↓ ",h.move,"분 이동"]}),n.jsxs("div",{className:"flex items-start gap-2.5 pt-1",children:[h.stop.imageUrl&&n.jsx("img",{src:h.stop.imageUrl,alt:`${h.stop.name} 사진`,loading:"lazy",decoding:"async",className:"size-12 shrink-0 object-cover",style:{border:`1px solid ${be}`}}),n.jsxs("span",{className:"min-w-0",children:[n.jsxs("span",{className:"block text-sm font-bold",children:[n.jsx(Sp,{n:x+1}),h.stop.name]}),h.stop.note&&n.jsx("span",{className:"mt-0.5 block text-[12px] leading-relaxed opacity-75",children:h.stop.note})]})]}),n.jsxs("p",{className:"mt-0.5 text-[10px] opacity-50",children:[h.time,"–",h.until,h.stop.searchQuery&&` · 지도 검색 ${h.stop.searchQuery}`]})]})]},`${h.stop.name}-${x}`))}),n.jsx("div",{className:"border-t border-dashed px-4 py-2.5",style:{borderColor:be},children:n.jsx(Hl,{source:i.source,verified:i.verified})})]})}const Gu="그 밖의 일정";function iy(){const i=F(),r=al(i,"itinerary"),u=r.items.filter(x=>Ju(x).some(v=>v.stops.length>0)),o=[...new Set(u.map(x=>{var v;return((v=x.duration)==null?void 0:v.trim())||Gu}))],[f,m]=Y.useState(null),h=f??o[0];return u.length===0?null:n.jsxs(ml,{id:"itinerary",name:r.title||qe(i,"itinerary","추천 일정"),subtitle:r.subtitle,count:"시각은 출발 시각과 머무는 시간으로 계산한 것입니다",children:[o.length>1&&n.jsx("div",{className:"mb-6 flex flex-wrap gap-1.5",role:"tablist","aria-label":"묵는 기간",children:o.map(x=>{const v=h===x;return n.jsxs("button",{type:"button",role:"tab","aria-selected":v,onClick:()=>m(x),className:"border-line rounded-full border px-4 py-1.5 text-[length:var(--fs-sm)] font-bold transition-opacity hover:opacity-80",style:v?{backgroundColor:"var(--color-brand)",color:"var(--tpl-bg, #fff)",borderColor:"transparent"}:void 0,children:[x,n.jsx("span",{className:"ml-1.5 font-normal opacity-60",children:u.filter(p=>{var b;return(((b=p.duration)==null?void 0:b.trim())||Gu)===x}).length})]},x)})}),o.map(x=>n.jsx("div",{hidden:x!==h,children:n.jsx(cy,{tab:x,items:u.filter(v=>{var p;return(((p=v.duration)==null?void 0:p.trim())||Gu)===x})},h)},x))]})}function cy({tab:i,items:r}){const u=Y.useMemo(()=>{let v=0;return r.map(p=>{const b=v;return v+=Ju(p).length,{item:p,start:b}})},[r]),[o,f]=Y.useState(0),m=Y.useRef(null),h=Y.useCallback(v=>{m.current=v},[]),x=u.reduce((v,p)=>p.start<=o?p.start:v,0);return n.jsxs(n.Fragment,{children:[u.length>1&&n.jsx("ul",{className:"border-line mb-5 flex flex-wrap items-center gap-x-4 gap-y-2 border-b pb-3",children:u.map(({item:v,start:p})=>{const b=p===x;return n.jsx("li",{children:n.jsx("button",{type:"button",onClick:()=>{var y;return(y=m.current)==null?void 0:y.scrollTo(p)},"aria-current":b,className:`text-left text-[length:var(--fs-sm)] underline-offset-4 transition-opacity hover:opacity-100 ${b?"font-bold underline":"opacity-55 hover:underline"}`,children:v.name})},`${v.name}-${p}`)})}),n.jsx(En,{label:`${i} 일정`,onSelect:f,onReady:h,children:u.flatMap(({item:v,start:p},b)=>Ju(v).map((y,E)=>n.jsx(sy,{item:v,badge:y.label??v.duration??`${E+1}일차`,startTime:y.startTime,stops:y.stops,first:E===0},`${v.name}-${p}-${E}`)))})]})}const ry=["1위","2위","3위"];function uy(i){const r=Math.floor(i/60),u=i%60;return[r>0?`${r}시간`:"",u>0?`${u}분`:""].filter(Boolean).join(" ")||"0분"}function oy({item:i,rank:r}){const u=up(i);return n.jsxs("article",{className:"w4-paper w-[320px] shrink-0 snap-center border",style:{backgroundColor:Vt,borderColor:be},children:[n.jsxs("div",{className:"flex items-center justify-between gap-2 border-b px-4 py-2.5",style:{borderColor:be},children:[n.jsx("span",{className:"border px-2 py-0.5 text-[11px] font-bold",style:r===0?{backgroundColor:Zt,color:Ul,borderColor:Zt}:{borderColor:be},children:ry[r]??`${r+1}위`}),n.jsxs("span",{className:"text-[11px] opacity-60",children:[u.from,"–",u.to," · ",uy(u.totalMinutes)]})]}),n.jsxs("div",{className:"space-y-1.5 px-4 pt-3.5",children:[n.jsx("h4",{className:"serif text-lg font-bold",children:i.name}),i.audience&&n.jsx("p",{className:"text-[11px] opacity-60",children:i.audience}),i.why&&n.jsx("p",{className:"text-[13px] leading-relaxed opacity-80",children:i.why})]}),n.jsx(Cp,{stops:u.stops.map(o=>o.stop)}),n.jsx("ol",{className:"mt-3 px-4 pb-3",children:u.stops.map((o,f)=>n.jsxs("li",{className:"grid grid-cols-[46px_minmax(0,1fr)] gap-2.5",children:[n.jsx("span",{className:"serif pt-2 text-[12px] tabular-nums opacity-75",children:o.time}),n.jsxs("div",{className:"border-l pb-2 pl-3",style:{borderColor:be},children:[o.move>0&&n.jsxs("p",{className:"pt-1 text-[10px] opacity-50",children:["↓ ",o.move,"분 이동"]}),n.jsxs("div",{className:"flex items-start gap-2.5 pt-1",children:[o.stop.imageUrl&&n.jsx("img",{src:o.stop.imageUrl,alt:`${o.stop.name} 사진`,loading:"lazy",decoding:"async",className:"size-12 shrink-0 object-cover",style:{border:`1px solid ${be}`}}),n.jsxs("span",{className:"min-w-0",children:[n.jsxs("span",{className:"block text-sm font-bold",children:[n.jsx(Sp,{n:f+1}),o.stop.name]}),o.stop.note&&n.jsx("span",{className:"mt-0.5 block text-[12px] leading-relaxed opacity-75",children:o.stop.note})]})]}),n.jsxs("p",{className:"mt-0.5 text-[10px] opacity-50",children:[o.time,"–",o.until,o.stop.searchQuery&&` · 지도 검색 ${o.stop.searchQuery}`]})]})]},`${o.stop.name}-${f}`))}),n.jsx("div",{className:"border-t border-dashed px-4 py-2.5",style:{borderColor:be},children:n.jsx(Hl,{source:i.source,verified:i.verified})})]})}function fy(){const i=F(),r=al(i,"planner"),u=jp();if(r.items.length===0)return null;const o=Fv(r.items),f=o.length>0?o:[void 0],m=u.filter(x=>o.includes(x)),h=m.length>0;return n.jsx(ml,{id:"planner",name:r.title||qe(i,"planner","계절별 추천 하루"),subtitle:r.subtitle,count:h?`지금은 ${m.join(" · ")} 코스입니다 · 시각은 출발 시각과 머무는 시간으로 계산한 것입니다`:"시각은 출발 시각과 머무는 시간으로 계산한 것입니다",children:n.jsx("div",{className:"space-y-10",children:f.map(x=>{const v=Iv(r.items,x);return v.length===0?null:n.jsxs("div",{className:"space-y-3",hidden:h&&!Wv(x,m),children:[x&&n.jsx("h3",{className:"serif text-lg font-bold",children:x}),n.jsx(En,{label:`${x??"추천"} 코스`,children:v.map((p,b)=>n.jsx(oy,{item:p,rank:b},`${p.name}-${b}`))})]},x??"all")})})})}function dy(){const i=F(),r=al(i,"video"),[u,o]=Y.useState(),f=Y.useRef(null),[m,h]=Y.useState({prev:!1,next:!0}),x=Y.useCallback(()=>{const b=f.current;if(!b)return;const y=b.scrollWidth-b.clientWidth;h({prev:b.scrollLeft>8,next:b.scrollLeft(x(),window.addEventListener("resize",x),()=>window.removeEventListener("resize",x)),[x]);const v=Y.useCallback(b=>{const y=f.current;if(!y)return;const E=window.matchMedia("(prefers-reduced-motion: reduce)").matches;y.scrollBy({left:b*y.clientWidth*.85,behavior:E?"auto":"smooth"})},[]);if(gc({box:f,interval:xc,advance:()=>ho(f.current)}),r.items.length===0)return null;const p=r.items.length>1;return n.jsx(ml,{id:"video",name:r.title||qe(i,"video","영상으로 보기"),subtitle:r.subtitle,link:r.linkUrl?{url:r.linkUrl,label:r.linkLabel??"더 보기"}:void 0,children:n.jsxs("div",{className:"relative",children:[n.jsx("ul",{ref:f,onScroll:x,className:p?"flex snap-x snap-mandatory gap-4 overflow-x-auto pb-2 [scrollbar-width:none] lg:grid lg:grid-cols-3 lg:gap-4 lg:snap-none lg:overflow-visible lg:pb-0 [&::-webkit-scrollbar]:hidden":"grid grid-cols-1 gap-4",children:r.items.map((b,y)=>{const E=Bv(b.url),T=Yv(b.url),N=u===y;return n.jsx("li",{className:p?"shrink-0 basis-[86%] snap-start sm:basis-[56%] lg:basis-auto":"",children:n.jsxs("figure",{className:"space-y-2",children:[n.jsx("div",{className:"tpl-border relative mx-auto overflow-hidden border",style:{borderColor:be,backgroundColor:Vt,aspectRatio:T?"9 / 16":"16 / 9",width:T?"min(100%, 22rem)":"100%"},children:N&&E?n.jsx("iframe",{src:Qv(E),title:b.caption||"영상",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",allowFullScreen:!0,className:"size-full border-0"}):n.jsxs("a",{href:b.url,target:"_blank",rel:"noopener noreferrer",onClick:_=>{E&&(_.preventDefault(),o(y))},"aria-label":`${b.caption||"영상"} 재생`,className:"group block size-full",children:[E?n.jsx("img",{src:Gv(E),alt:"",loading:"lazy",decoding:"async",className:"size-full object-cover"}):n.jsx("span",{className:"text-muted grid size-full place-items-center px-4 text-center text-[length:var(--fs-xs)]",children:"유튜브 영상이 아닙니다 — 눌러서 원본으로 갑니다"}),n.jsx("span",{className:"absolute inset-0 grid place-items-center",children:n.jsx("span",{className:"grid size-14 place-items-center rounded-full backdrop-blur transition-transform group-hover:scale-110",style:{backgroundColor:"color-mix(in srgb, var(--tpl-inverse, #1c1917) 70%, transparent)",color:"var(--tpl-bg, #fff)"},children:n.jsx(_v,{className:"size-6"})})})]})}),b.caption&&n.jsx("figcaption",{className:"text-[length:var(--fs-sm)] leading-relaxed opacity-80",children:b.caption}),n.jsx(Hl,{source:b.source,verified:b.verified})]})},`${b.url}-${y}`)})}),p&&n.jsxs(n.Fragment,{children:[n.jsx(Fh,{dir:"prev",onClick:()=>v(-1),disabled:!m.prev}),n.jsx(Fh,{dir:"next",onClick:()=>v(1),disabled:!m.next})]})]})})}function Fh({dir:i,onClick:r,disabled:u}){const o=i==="prev"?Ma:Oa;return n.jsx("button",{type:"button",onClick:r,disabled:u,"aria-label":i==="prev"?"이전 영상":"다음 영상",className:`tap absolute top-1/2 z-10 flex -translate-y-1/2 items-center justify-center rounded-full bg-black/55 text-white backdrop-blur transition hover:bg-black/75 disabled:pointer-events-none disabled:opacity-0 lg:hidden ${i==="prev"?"left-1":"right-1"}`,children:n.jsx(o,{className:"size-5"})})}const my={1:"한",2:"두",3:"세",4:"네",5:"다섯"},hy=[{id:"songs",label:"가요 다방",Component:zp},{id:"daily",label:"오늘의 한 장",Component:Ep},{id:"people",label:"인물 열전",Component:Ap},{id:"chronicle",label:"시간의 골목",Component:Tp},{id:"postcard",label:"오늘의 엽서",Component:_p}];function py(){const i=F(),r=hy.filter(f=>al(i,f.id).items.length>0),[u,o]=Y.useState(0);return r.length===0?null:n.jsxs(n.Fragment,{children:[n.jsx("section",{id:"story","aria-labelledby":"story-heading",className:"paper w-full",style:{backgroundColor:"var(--tpl-surface, #fafafa)",color:Zt,paddingTop:"var(--section-space)",paddingBottom:"calc(var(--section-space) * 0.55)"},children:n.jsxs("div",{className:"shell",children:[n.jsx("h2",{id:"story-heading",className:"h2",children:qe(i,"story","군산 이야기")}),n.jsxs("p",{className:"measure mt-3 text-[length:var(--fs-sm)] opacity-70",children:["이 도시를 ",my[r.length]??`${r.length}`," 갈래로 봅니다. 하나씩 골라 보세요."]}),n.jsx("div",{className:"mt-6 flex flex-wrap gap-1.5",role:"tablist","aria-label":"군산 이야기",children:r.map((f,m)=>{const h=m===u;return n.jsx("button",{type:"button",role:"tab","aria-selected":h,"aria-controls":f.id,onClick:()=>o(m),className:"rounded-full border px-4 py-1.5 text-[length:var(--fs-sm)] font-bold transition-opacity hover:opacity-80",style:h?{backgroundColor:Zt,color:Ul,borderColor:Zt}:{borderColor:be},children:f.label},f.id)})})]})}),r.map((f,m)=>n.jsx("div",{hidden:m!==u,children:n.jsx(f.Component,{})},f.id))]})}const xy={open:"진행 중",soon:"곧 시작",closed:"종료"};function Ih(i,r){return i.endDate&&i.endDater?"soon":"open"}function Qu(i,r){return i.kind==="공지"&&!i.startDate&&!i.endDate?"공지":r&&xy[r]}function Xu(i){const r=f=>{const m=/^(\d{4})-(\d{2})-(\d{2})$/.exec(f??"");return m?`${Number(m[2])}월 ${Number(m[3])}일`:void 0},u=r(i.startDate),o=r(i.endDate);if(u&&o)return u===o?u:`${u} – ${o}`;if(u)return`${u}부터`;if(o)return`${o}까지`}function gy(){const i=F(),r=al(i,"event"),[u,o]=Y.useState();Y.useEffect(()=>o(new Date().toLocaleDateString("sv-SE")),[]);const[f,m]=Y.useState(null),h=Y.useCallback(()=>m(null),[]);if(Y.useEffect(()=>{if(f===null)return;const p=y=>{y.key==="Escape"&&h()},b=document.body.style.overflow;return document.body.style.overflow="hidden",window.addEventListener("keydown",p),()=>{document.body.style.overflow=b,window.removeEventListener("keydown",p)}},[f,h]),r.items.length===0)return null;const x=f===null?void 0:r.items[f],v=x&&u?Ih(x,u):void 0;return n.jsxs(ml,{id:"event",name:r.title||qe(i,"event",`${i.place.name} 소식`),subtitle:r.subtitle,link:r.linkUrl?{url:r.linkUrl,label:r.linkLabel??"더 보기"}:void 0,count:"자세한 내용과 최신 소식은 원문에서 확인해 주세요.",children:[n.jsx("ul",{className:"grid gap-5 sm:grid-cols-2 lg:grid-cols-3",children:r.items.map((p,b)=>{const y=u?Ih(p,u):void 0,E=Qu(p,y),T=Xu(p),N=y==="closed";return n.jsxs("li",{className:"w4-paper relative flex flex-col border-2",style:{backgroundColor:Vt,borderColor:be,opacity:N?.6:1,boxShadow:N?"none":"3px 3px 0 color-mix(in oklab, currentcolor 12%, transparent)"},children:[n.jsx("button",{type:"button",onClick:()=>m(b),"aria-label":`${p.title} 자세히 보기`,className:"absolute inset-0 z-20 cursor-pointer"}),n.jsx("div",{className:p.imageUrl?"relative overflow-hidden border-b-2":"contents",style:p.imageUrl?{borderColor:be}:void 0,children:p.imageUrl?n.jsx("img",{src:p.imageUrl,alt:`${p.title} 안내 이미지`,loading:"lazy",decoding:"async",className:"aspect-4/3 w-full object-cover",style:N?{filter:"grayscale(1)"}:void 0}):null}),n.jsxs("div",{className:"flex flex-1 flex-col gap-2.5 p-5",children:[n.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[E&&n.jsx("span",{className:"border-2 px-2 py-0.5 text-[11px] font-bold tracking-[0.08em]",style:{transform:"rotate(-3deg)",backgroundColor:y==="open"&&E!=="공지"?Ke:Vt,color:y==="open"&&E!=="공지"?Ul:Zt,borderColor:y==="open"&&E!=="공지"?Ke:be},children:E}),T&&n.jsx("span",{className:"text-[11px] font-semibold tabular-nums opacity-55",children:T})]}),n.jsx("h3",{className:"serif text-lg font-bold leading-snug",children:p.title}),p.summary&&n.jsx("p",{className:"line-clamp-2 text-[13px] leading-relaxed opacity-80",children:p.summary}),p.howTo&&n.jsx("p",{className:"border-2 border-dashed px-3 py-2 text-[12px] leading-relaxed",style:{borderColor:be},children:p.howTo}),p.body&&n.jsx("p",{className:`whitespace-pre-line text-[12px] leading-relaxed opacity-70 ${p.imageUrl?"line-clamp-3":"line-clamp-[9]"}`,children:p.body}),n.jsx("p",{className:"mt-auto pt-2 text-[12px] font-bold opacity-70",children:"자세히 보기 →"})]})]},`${p.title}-${b}`)})}),x&&n.jsx("div",{role:"dialog","aria-modal":"true","aria-label":x.title,className:"fixed inset-0 z-50 flex items-end justify-center bg-black/70 p-0 backdrop-blur-sm sm:items-center sm:p-6",onClick:h,children:n.jsxs("article",{onClick:p=>p.stopPropagation(),className:"w4-paper max-h-[88vh] w-full max-w-2xl overflow-y-auto border-2",style:{backgroundColor:Vt,borderColor:be,color:Zt},children:[x.imageUrl&&n.jsx("img",{src:x.imageUrl,alt:`${x.title} 안내 이미지`,className:"max-h-[46vh] w-full object-contain",style:{backgroundColor:"color-mix(in oklab, currentcolor 6%, transparent)"}}),n.jsxs("div",{className:"space-y-4 p-6 sm:p-8",children:[n.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[Qu(x,v)&&n.jsx("span",{className:"border-2 px-2.5 py-1 text-[11px] font-bold tracking-[0.08em]",style:{borderColor:be},children:Qu(x,v)}),Xu(x)&&n.jsx("span",{className:"text-[12px] font-semibold tabular-nums opacity-60",children:Xu(x)})]}),n.jsx("h3",{className:"serif text-[length:var(--fs-h3)] font-bold leading-snug",children:x.title}),x.summary&&n.jsx("p",{className:"text-[length:var(--fs-sm)] leading-relaxed opacity-85",children:x.summary}),x.howTo&&n.jsx("p",{className:"border-2 border-dashed px-4 py-3 text-[length:var(--fs-sm)] leading-relaxed",style:{borderColor:be},children:x.howTo}),x.body&&n.jsx("p",{className:"whitespace-pre-line text-[length:var(--fs-sm)] leading-relaxed opacity-85",children:x.body}),n.jsxs("div",{className:"flex items-center justify-between gap-4 pt-2",children:[x.postUrl?n.jsx("a",{href:x.postUrl,target:"_blank",rel:"noopener noreferrer",className:"text-[length:var(--fs-sm)] font-bold underline-offset-4 hover:underline",children:"원문 보기 →"}):n.jsx("span",{}),n.jsxs("button",{type:"button",onClick:h,className:"tap inline-flex items-center gap-1.5 border-2 px-4 text-[length:var(--fs-sm)] font-bold",style:{borderColor:be},children:[n.jsx(sp,{className:"size-4"}),n.jsx("span",{children:"닫기"})]})]})]})]})})]})}const vy={songs:zp,daily:Ep,people:Ap,chronicle:Tp,postcard:_p,quiz:Fb,itinerary:iy,planner:fy,video:dy,event:gy,story:py};function by({children:i}){return n.jsxs("div",{className:"flex min-h-screen w-full flex-col",children:[n.jsx(Ny,{}),n.jsx("div",{className:"flex w-full flex-1 flex-col",children:i}),n.jsx(Sy,{}),n.jsx(zn,{})]})}function Mp(){const i=F(),r=dt(i);return[{id:"intro",href:"#about",fallback:"소개"},{id:r.path,href:"#units",fallback:r.label},{id:"pricing",href:"#pricing",fallback:"요금"},{id:"info",href:"#info",fallback:"이용 정보"},{id:"booking",href:"#booking",fallback:"예약 안내"},{id:"photos",href:"#gallery",fallback:"사진"},{id:"map",href:"#location",fallback:"오시는 길"},{id:"local",href:"#guide",fallback:"주변 정보"},{id:"faq",href:"#faq",fallback:"자주 묻는 질문"}].filter(u=>Kt(i,u.id)).map(u=>({label:qe(i,u.id,u.fallback),href:u.href}))}const yy=24,jy="0 1px 3px color-mix(in srgb, var(--tpl-inverse, #1c1917) 55%, transparent)";function Ny(){const i=F(),r=Mp(),[u,o]=Y.useState(!1);Y.useEffect(()=>{const m=()=>o(window.scrollY>yy);return m(),window.addEventListener("scroll",m,{passive:!0}),()=>window.removeEventListener("scroll",m)},[]);const f=u?"opacity-70 transition-opacity hover:opacity-100":"underline-offset-4 hover:underline";return n.jsx("header",{className:`fixed inset-x-0 top-0 z-40 w-full transition-colors duration-300 ${u?"border-line border-b backdrop-blur-md":""}`,style:u?{backgroundColor:"color-mix(in srgb, var(--tpl-surface, #fff) 92%, transparent)"}:{color:"var(--tpl-bg, #ffffff)",textShadow:jy},children:n.jsxs("div",{className:"shell grid h-20 grid-cols-[1fr_auto] items-center gap-4 lg:grid-cols-[1fr_auto_1fr]",children:[n.jsx("a",{href:"#top",className:"min-w-0",children:n.jsx("span",{className:"serif block truncate",style:{fontSize:"var(--fs-lead)",fontWeight:300,letterSpacing:"0.12em"},children:i.place.name})}),n.jsx("nav",{"aria-label":"주요 메뉴",className:"hidden items-center justify-center gap-10 lg:flex",children:r.map(m=>n.jsx("a",{href:m.href,className:`py-1 text-[length:var(--fs-sm)] font-light ${f}`,children:m.label},m.href))}),n.jsxs("div",{className:"flex items-center justify-end gap-2",children:[i.place.phone&&n.jsx("a",{href:`tel:${i.place.phone}`,className:`tap hidden items-center text-[length:var(--fs-sm)] font-light tabular-nums sm:inline-flex ${f}`,children:i.place.phone}),r.length>0&&n.jsxs("details",{className:"lg:hidden",children:[n.jsx("summary",{className:"tap flex cursor-pointer list-none items-center justify-center px-2 text-[length:var(--fs-sm)] font-light marker:content-none [&::-webkit-details-marker]:hidden","aria-label":"메뉴 열기",children:"메뉴"}),n.jsx("nav",{"aria-label":"전체 메뉴",className:"border-line absolute inset-x-0 top-full border-b border-t",style:{backgroundColor:"var(--tpl-surface, #fff)",color:"var(--tpl-text)",textShadow:"none"},children:n.jsx("ul",{className:"divide-line shell divide-y",children:r.map(m=>n.jsx("li",{children:n.jsx("a",{href:m.href,className:"tap flex items-center text-[length:var(--fs-sm)] font-light",children:m.label})},m.href))})})]})]})]})})}function Sy(){var p,b,y,E;const i=F(),{place:r,site:u,narrative:o}=i,f=Mp(),m=r.roadAddress??r.address,h=et(i)[0],x=Ls(i),v=o.about[0]??o.summary;return n.jsx("footer",{className:"border-line paper w-full border-t pb-28 md:pb-16",style:{backgroundColor:"var(--tpl-surface-alt, #f5f5f4)",paddingTop:"var(--section-space)"},children:n.jsxs("div",{className:"shell",children:[n.jsxs("div",{className:"grid gap-10 md:grid-cols-3 md:gap-12",children:[n.jsxs("div",{className:"min-w-0",children:[n.jsx("p",{className:"serif",style:{fontSize:"var(--fs-lead)",fontWeight:300,letterSpacing:"0.1em"},children:r.name}),v&&n.jsx("p",{className:"text-muted measure mt-4 text-[length:var(--fs-sm)] leading-loose",children:v})]}),n.jsxs("dl",{className:"min-w-0 space-y-5",children:[m&&n.jsxs("div",{children:[n.jsx("dt",{className:"label",children:"주소"}),n.jsx("dd",{className:"mt-1 text-[length:var(--fs-sm)] font-light",children:m})]}),r.phone&&n.jsxs("div",{children:[n.jsx("dt",{className:"label",children:"전화"}),n.jsx("dd",{className:"mt-1 text-[length:var(--fs-sm)] font-light tabular-nums",children:n.jsx("a",{href:`tel:${r.phone}`,className:"underline-offset-4 hover:underline",children:r.phone})})]}),r.email&&n.jsxs("div",{children:[n.jsx("dt",{className:"label",children:"이메일"}),n.jsx("dd",{className:"mt-1 text-[length:var(--fs-sm)] font-light",children:n.jsx("a",{href:`mailto:${r.email}`,className:"underline-offset-4 hover:underline",children:r.email})})]}),h&&n.jsxs("div",{children:[n.jsx("dt",{className:"label",children:"예약"}),n.jsx("dd",{className:"mt-1 text-[length:var(--fs-sm)] font-light",children:n.jsx("a",{href:h.url,target:"_blank",rel:"noopener noreferrer",className:"underline-offset-4 hover:underline",children:Pe(h)})})]})]}),f.length>0&&n.jsx("nav",{"aria-label":"사이트 메뉴",className:"min-w-0",children:n.jsx("ul",{className:"space-y-3",children:f.map(T=>n.jsx("li",{children:n.jsx("a",{href:T.href,className:"text-muted text-[length:var(--fs-sm)] font-light underline-offset-4 hover:underline",children:T.label})},T.href))})})]}),n.jsx("hr",{className:"border-line mt-12 border-t"}),x.length>0&&n.jsx("ul",{className:"mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-2",children:x.map(T=>n.jsx("li",{children:n.jsx("a",{href:T.url,target:"_blank",rel:"noopener noreferrer",className:"text-muted text-[length:var(--fs-xs)] font-light underline-offset-4 hover:underline",children:Pe(T)})},T.url))}),n.jsxs("div",{className:"text-muted mt-8 flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-center text-[length:var(--fs-xs)] font-light",children:[n.jsxs("span",{children:["상호 ",r.name]}),((p=r.legal)==null?void 0:p.representative)&&n.jsxs("span",{children:["대표 ",r.legal.representative]}),((b=r.legal)==null?void 0:b.businessRegistrationNumber)&&n.jsxs("span",{children:["사업자등록번호 ",r.legal.businessRegistrationNumber]}),((y=r.legal)==null?void 0:y.mailOrderNumber)&&n.jsxs("span",{children:["통신판매업신고 ",r.legal.mailOrderNumber]}),((E=r.legal)==null?void 0:E.licenseNumber)&&n.jsxs("span",{children:[r.legal.licenseLabel??"인허가번호"," ",r.legal.licenseNumber]}),n.jsxs("span",{className:"byline",children:[n.jsxs("span",{className:"author",children:["작성·운영 ",r.name]})," · 최종 업데이트: ",n.jsx("time",{dateTime:u.updatedAt,children:qs(u.updatedAt)})]})]})]})})}const zy=` +.oasi-page main > section:not([data-oasi]) { + background-color: transparent !important; + background-image: none !important; + border-color: transparent !important; + color: var(--tpl-text) !important; + padding-block: var(--oasi-gap) 0 !important; +} +.oasi-page main > section:not([data-oasi]) .shell { + max-width: none; + padding-inline: 0; +} +.oasi-page main > section#about > .shell > .grid { + display: block; +} +.oasi-page main > section#about > .shell > .grid > * + * { + margin-top: 2.5rem; +} +.oasi-page main > section#summary dl { + grid-template-columns: minmax(0, 1fr); +} + +/* + * 붙여넣기 아이템 카드 — 소식 · 영상 · 추천 일정 (2026-09-04, 사장님: "1,2,3안 다 맞추세요") + * + * ★ 이 카드들은 .panel 이 아니라 제 인라인 style 로 면을 그린다(items/common.tsx 의 + * ITEM_CARD·ITEM_BORDER). 그래서 위 .panel 규칙이 안 닿았고, 섹션 띠를 다 지운 3안에서 + * **소식 카드만 갱지 결에 2px 테두리를 두른 상자로 혼자 서 있었다.** + * ★ w4-paper 의 결은 1안(레트로 갱지)의 것이다. 여기서는 결을 지우고 테두리를 실선 한 겹으로 + * 낮춘다. 상자를 아예 없애지는 않는다 — 전단 여섯 장이 경계 없이 붙으면 어디까지가 한 장인지 + * 안 보인다. 면은 빼고 테두리만 남기는 게 이 안의 타협점이다. + * ★ 배경은 transparent 로 두지 않는다. 사진 없는 공지 카드가 종이에 바로 얹히면 + * 본문과 구분이 사라진다 — 아주 옅은 틴트 한 겹만 남긴다. + * ★ 영상은 뺐다 (2026-09-04, 사장님: "영상 border 안 예쁘다") + * 영상 카드는 상자가 아니라 **표지 사진 한 장**이고, 쇼츠라 9:16 세로다. li 에 틴트를 깔면 + * 영상보다 넓은 회색 판이 뒤에 깔려 액자처럼 보인다. 사진은 제 테두리가 곧 경계다. + */ +.oasi-page section#event li, +.oasi-page section#itinerary article { + background-image: none !important; + background-color: color-mix(in oklab, currentcolor 3%, transparent) !important; + border-width: 1px !important; + box-shadow: none !important; +} + +.oasi-page .panel, +.oasi-page .panel-sunken { + background-color: transparent; + border-width: 0; + border-radius: 0; + box-shadow: none; +} + +.oasi-page main > section#faq > .shell > div:last-of-type { + row-gap: 0; + column-gap: 2.5rem; +} +.oasi-page main > section#faq details { + margin-block: 0; + border-top: 1px solid var(--color-line); +} +.oasi-page main > section#faq summary { + padding-inline: 0; +} +.oasi-page main > section#faq summary > span:first-child { + width: 1.125rem; +} +.oasi-page main > section#faq summary + div { + border-top-width: 0; + padding-inline: 1.875rem 0; + padding-top: 0.5rem; +} + +.oasi-page main > section#info dl.panel > div { + padding-inline: 0; +} +.oasi-page main > section#info dl > div.panel { + border-top: 1px solid var(--color-line); + padding-inline: 0; +} + +.oasi-page main > section#location .panel { + border-top: 1px solid var(--color-line); + padding-inline: 0; +} +.oasi-page main > section#location :is(thead, tbody) :is(th, td):first-child { + padding-left: 0; +} +.oasi-page main > section#location :is(thead, tbody) :is(th, td):last-child { + padding-right: 0; +} + +.oasi-page main > section#weather .panel { + padding-inline: 0; +} + +.oasi-page main > section#guide .panel > span:last-child { + padding-inline: 0; +} +@media (min-width: 40rem) { + .oasi-page main > section#festival .panel > span:last-child { + padding-inline: 0; + } +} +`,Ey={"--oasi-gap":"clamp(3.5rem, 7vw, var(--tpl-section-space, 4rem))",backgroundColor:"var(--tpl-bg)",color:"var(--tpl-text)"};function Ay({children:i}){var b,y,E,T;const r=F(),{place:u}=r,o=dt(r),f=et(r)[0],m=Ls(r),h=[{label:"처음",href:"#top",show:!0},{label:"소개",href:"#about",show:Kt(r,"intro")},{label:o.label,href:"#units",show:r.units.length>0},{label:"이용 정보",href:"#info",show:!0},{label:"주변",href:"#guide",show:Kt(r,"local")},{label:"오시는 길",href:"#location",show:!0},{label:"자주 묻는 질문",href:"#faq",show:r.faqs.length>0}].filter(N=>N.show),x=[...Da(r).map(N=>`${N.label} ${N.value}`),...r.theme.sections.filter(N=>{var _;return N.enabled&&((_=N.name)==null?void 0:_.trim())}).map(N=>N.name.trim())].slice(0,3),v=r.narrative.tagline??r.narrative.heroSubline,p=u.roadAddress??u.address;return n.jsxs("div",{className:"oasi-page flex min-h-screen w-full flex-col",style:Ey,children:[n.jsx("style",{dangerouslySetInnerHTML:{__html:zy}}),n.jsxs("div",{className:"mx-auto flex w-full max-w-[72rem] flex-col gap-10 px-5 pb-16 pt-10 lg:flex-row lg:gap-14 lg:px-10 lg:pb-24 lg:pt-16",children:[n.jsxs("header",{className:"lg:sticky lg:top-12 lg:h-fit lg:w-[18.75rem] lg:shrink-0",children:[n.jsxs("a",{href:"#top",className:"block",children:[n.jsx("span",{className:"serif block leading-none",style:{fontSize:"var(--fs-display)",fontWeight:300,letterSpacing:"0.14em"},children:u.name}),u.englishName&&n.jsx("span",{className:"text-muted mt-3 block text-[length:var(--fs-xs)] tracking-[0.24em]",children:u.englishName})]}),n.jsx("nav",{"aria-label":"주요 메뉴",className:"mt-12 lg:mt-24",children:n.jsx("ul",{className:"flex flex-wrap items-center gap-x-6 gap-y-1 lg:block",children:h.map((N,_)=>n.jsx("li",{children:n.jsx("a",{href:N.href,"aria-current":_===0?"true":void 0,className:`tap inline-flex items-center text-[length:var(--fs-sm)] transition-opacity hover:opacity-100 ${_===0?"":"text-muted"}`,children:N.label})},N.label))})}),f&&n.jsxs("a",{href:f.url,target:"_blank",rel:"noopener noreferrer",className:"tap mt-2 inline-flex items-center text-[length:var(--fs-sm)] underline underline-offset-[6px]",style:{color:"var(--color-brand)"},children:[Pe(f)," 예약"]})]}),n.jsxs("div",{className:"flex min-w-0 flex-1 flex-col",children:[(x.length>0||v)&&n.jsxs("div",{className:"border-line flex flex-col gap-3 border-y py-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6",children:[x.length>0&&n.jsx("ul",{className:"text-muted flex min-w-0 flex-wrap items-center gap-x-4 gap-y-1 text-[length:var(--fs-xs)]",children:x.map((N,_)=>n.jsxs("li",{className:"flex items-center gap-4",children:[_>0&&n.jsx("span",{"aria-hidden":!0,className:"opacity-40",children:"|"}),n.jsx("span",{children:N})]},N))}),v&&n.jsx("p",{className:"text-muted measure shrink-0 text-[length:var(--fs-xs)] leading-[1.9] sm:max-w-[18rem] sm:text-right",children:v})]}),i]})]}),n.jsx("footer",{className:"border-line mt-auto w-full border-t",children:n.jsxs("div",{className:"mx-auto grid w-full max-w-[72rem] grid-cols-1 gap-8 px-5 pb-28 pt-10 text-[length:var(--fs-xs)] leading-[1.9] sm:grid-cols-3 lg:px-10 lg:pb-14",children:[n.jsxs("p",{children:["[",u.name,"]"]}),m.length>0?n.jsx("ul",{className:"space-y-1",children:m.map(N=>n.jsx("li",{children:n.jsx("a",{href:N.url,target:"_blank",rel:"noopener noreferrer",className:"underline underline-offset-[5px]",children:Pe(N)})},N.url))}):n.jsx("span",{}),n.jsxs("div",{className:"text-muted space-y-1 sm:text-right",children:[p&&n.jsx("p",{children:p}),u.phone&&n.jsx("p",{children:n.jsx("a",{href:`tel:${u.phone}`,className:"underline underline-offset-[5px]",children:u.phone})}),u.email&&n.jsx("p",{children:u.email}),((b=u.legal)==null?void 0:b.representative)&&n.jsxs("p",{children:["대표 ",u.legal.representative]}),((y=u.legal)==null?void 0:y.businessRegistrationNumber)&&n.jsxs("p",{children:["사업자등록번호 ",u.legal.businessRegistrationNumber]}),((E=u.legal)==null?void 0:E.mailOrderNumber)&&n.jsxs("p",{children:["통신판매업신고 ",u.legal.mailOrderNumber]}),((T=u.legal)==null?void 0:T.licenseNumber)&&n.jsxs("p",{children:[u.legal.licenseLabel??"인허가번호"," ",u.legal.licenseNumber]}),n.jsxs("p",{className:"byline",children:[n.jsxs("span",{className:"author",children:["작성·운영 ",u.name]})," · 최종 업데이트: ",n.jsx("time",{dateTime:r.site.updatedAt,children:qs(r.site.updatedAt)})]})]})]})}),n.jsx(zn,{})]})}function Ty({children:i}){var E,T,N,_;const r=F(),{place:u,narrative:o,site:f}=r,m=dt(r),h=Sn(r),x=Da(r),v=et(r)[0],p=r.links.filter(U=>U.confirmed),b=u.roadAddress??u.address,y=[{label:"소개",href:"#about",show:Kt(r,"intro")},{label:m.label,href:"#units",show:r.units.length>0},{label:"이용 정보",href:"#info",show:!0},{label:"주변 정보",href:"#guide",show:Kt(r,"local")},{label:"오시는 길",href:"#location",show:!0},{label:"자주 묻는 질문",href:"#faq",show:r.faqs.length>0}].filter(U=>U.show);return n.jsxs("div",{className:"flex min-h-screen w-full flex-col lg:flex-row",style:{backgroundColor:"var(--tpl-bg)",color:"var(--tpl-text)"},children:[n.jsx("aside",{className:"w-full shrink-0 px-6 pb-8 pt-8 lg:sticky lg:top-0 lg:h-screen lg:w-[260px] lg:overflow-y-auto lg:px-7 lg:py-10",style:{backgroundColor:"var(--tpl-bg)"},children:n.jsxs("div",{className:"flex h-full min-w-0 flex-col",children:[n.jsx("a",{href:"#top",className:"block min-w-0",children:n.jsx("h1",{className:"serif min-w-0 break-keep font-black",style:{fontSize:"var(--fs-h2)",letterSpacing:"0.12em",lineHeight:1.25},children:u.name})}),(o.tagline??o.heroSubline)&&n.jsx("p",{className:"text-muted mt-3 text-[length:var(--fs-sm)]",style:{lineHeight:1.7},children:o.tagline??o.heroSubline}),(h||x.length>0)&&n.jsxs("dl",{className:"mt-7 space-y-2.5",children:[h&&n.jsxs("div",{children:[n.jsx("dt",{className:"label",children:"최저가"}),n.jsx("dd",{className:"text-[length:var(--fs-lead)] font-bold tabular-nums",children:h})]}),x.map(U=>n.jsxs("div",{children:[n.jsx("dt",{className:"label",children:U.label}),n.jsx("dd",{className:"text-[length:var(--fs-sm)] font-semibold",children:U.value})]},U.label))]}),v&&n.jsxs("a",{href:v.url,target:"_blank",rel:"noopener noreferrer",className:"tap mt-5 inline-flex w-full items-center justify-center px-4 text-[length:var(--fs-sm)] font-bold transition-opacity hover:opacity-90",style:{backgroundColor:"var(--color-brand)",color:"var(--tpl-bg)"},children:[Pe(v),"로 예약"]}),u.phone&&n.jsxs("a",{href:`tel:${u.phone}`,className:"tap mt-2 inline-flex w-full items-center justify-center px-4 text-[length:var(--fs-sm)] font-bold",style:{backgroundColor:"var(--tpl-surface-alt)",color:"var(--tpl-text)"},children:["전화 ",u.phone]}),n.jsx("nav",{"aria-label":"주요 메뉴",className:"mt-10",children:n.jsx("ul",{className:"space-y-0.5",children:y.map(U=>n.jsx("li",{children:n.jsx("a",{href:U.href,className:"text-muted flex min-h-[2rem] items-center text-[length:var(--fs-sm)] transition-colors hover:text-current",children:U.label})},U.label))})}),p.length>0&&n.jsx("nav",{"aria-label":"공식 채널",className:"mt-12",children:n.jsx("ul",{className:"space-y-0.5",children:p.map(U=>n.jsx("li",{children:n.jsx("a",{href:U.url,target:"_blank",rel:"noopener noreferrer",className:"text-muted flex min-h-[2rem] items-center text-[length:var(--fs-xs)] underline-offset-4 transition-colors hover:text-current hover:underline",children:Rl(U)})},U.url))})}),n.jsxs("div",{className:"text-muted mt-14 space-y-1 pb-2 text-[length:var(--fs-xs)] lg:mt-auto lg:pt-14",children:[b&&n.jsx("p",{className:"break-keep",children:b}),u.phone&&n.jsx("p",{children:u.phone}),u.email&&n.jsx("p",{children:n.jsx("a",{href:`mailto:${u.email}`,className:"underline-offset-2 hover:underline",children:u.email})}),n.jsxs("div",{className:"flex flex-wrap gap-x-2 gap-y-0.5 pt-3",children:[n.jsxs("span",{children:["상호: ",u.name]}),((E=u.legal)==null?void 0:E.representative)&&n.jsxs("span",{children:["대표: ",u.legal.representative]}),((T=u.legal)==null?void 0:T.businessRegistrationNumber)&&n.jsxs("span",{children:["사업자등록번호: ",u.legal.businessRegistrationNumber]}),((N=u.legal)==null?void 0:N.mailOrderNumber)&&n.jsxs("span",{children:["통신판매업신고: ",u.legal.mailOrderNumber]}),((_=u.legal)==null?void 0:_.licenseNumber)&&n.jsxs("span",{children:[u.legal.licenseLabel??"인허가번호",": ",u.legal.licenseNumber]})]}),n.jsxs("p",{className:"byline pt-1",children:[n.jsxs("span",{className:"author",children:["작성·운영 ",u.name]})," · 최종 업데이트: ",n.jsx("time",{dateTime:f.updatedAt,children:qs(f.updatedAt)})]})]})]})}),n.jsx("div",{className:"flex min-w-0 flex-1 flex-col pb-24 lg:pb-0",children:i}),n.jsx(zn,{})]})}const _y=` +.pastel-page > main > section:not([data-pastel]) { + border-color: transparent; + background-image: none; + color: var(--pastel-ink) !important; +} +.pastel-page > main > section:not([data-pastel]):nth-of-type(4n+1) { background-color: var(--pastel-1) !important; } +.pastel-page > main > section:not([data-pastel]):nth-of-type(4n+2) { background-color: var(--pastel-soft) !important; } +.pastel-page > main > section:not([data-pastel]):nth-of-type(4n+3) { background-color: var(--pastel-2) !important; } +.pastel-page > main > section:not([data-pastel]):nth-of-type(4n) { background-color: var(--pastel-3) !important; } +`,wy={"--pastel-1":"var(--tpl-card)","--pastel-2":"var(--tpl-accent)","--pastel-3":"var(--tpl-secondary)","--pastel-soft":"var(--tpl-bg)","--pastel-ink":"var(--tpl-text)","--section-space":"clamp(2.75rem, 8vw, calc(var(--tpl-section-space, 4rem) * 1.4))",backgroundColor:"var(--pastel-soft)",color:"var(--pastel-ink)"};function Cy({children:i}){const r=F(),u=dt(r),o=Ls(r),f=[{label:"소개",href:"#about",show:Kt(r,"intro")},{label:u.label,href:"#units",show:r.units.length>0},{label:"이용 정보",href:"#info",show:!0},{label:"주변",href:"#guide",show:Kt(r,"local")},{label:"오시는 길",href:"#location",show:!0},{label:"FAQ",href:"#faq",show:r.faqs.length>0}].filter(m=>m.show);return n.jsxs("div",{className:"pastel-page flex min-h-screen w-full flex-col",style:wy,children:[n.jsx("style",{dangerouslySetInnerHTML:{__html:_y}}),n.jsx("header",{className:"sticky top-0 z-40 w-full",style:{backgroundColor:"color-mix(in srgb, var(--tpl-surface, #fff) 92%, transparent)"},children:n.jsxs("div",{className:"shell flex h-16 items-center justify-between gap-3",children:[n.jsx("a",{href:"#top",className:"flex min-w-0 items-center gap-2",children:n.jsx("span",{className:"serif truncate italic",style:{fontSize:"var(--fs-lead)",fontWeight:800,display:"inline-block",transform:"rotate(-1.5deg)"},children:r.place.name})}),n.jsx("nav",{"aria-label":"주요 메뉴",className:"hidden items-center gap-8 lg:flex",children:f.map(m=>n.jsx("a",{href:m.href,className:"py-1 text-[length:var(--fs-xs)] font-bold tracking-[0.2em] opacity-70 transition-opacity hover:opacity-100",children:m.label},m.label))}),n.jsxs("div",{className:"flex shrink-0 items-center gap-1",children:[r.place.phone&&n.jsx("a",{href:`tel:${r.place.phone}`,"aria-label":"전화",className:"tap flex items-center justify-center rounded-full",style:{backgroundColor:"var(--pastel-1)"},children:n.jsx(Jt,{className:"size-4"})}),o.map(m=>n.jsx("a",{href:m.url,target:"_blank",rel:"noopener noreferrer","aria-label":Rl(m),className:"tap flex items-center justify-center rounded-full",style:{backgroundColor:"var(--pastel-2)"},children:/instagram/i.test(m.url)?n.jsx(yv,{className:"size-4"}):n.jsx(Fu,{className:"size-4"})},m.url)),n.jsxs("details",{className:"relative lg:hidden",children:[n.jsx("summary",{className:"tap flex cursor-pointer items-center justify-center rounded-full marker:content-none [&::-webkit-details-marker]:hidden","aria-label":"메뉴 열기",children:n.jsx(np,{className:"size-5"})}),n.jsx("nav",{"aria-label":"전체 메뉴",className:"absolute right-0 top-[calc(100%+0.5rem)] z-50 w-48 overflow-hidden rounded-2xl",style:{backgroundColor:"var(--tpl-surface, #fff)",boxShadow:"0 10px 30px rgb(0 0 0 / 0.12)"},children:n.jsx("ul",{children:f.map(m=>n.jsx("li",{children:n.jsx("a",{href:m.href,className:"tap flex items-center px-4 text-[length:var(--fs-sm)] font-bold tracking-[0.08em]",children:m.label})},m.label))})})]})]})]})}),i,n.jsx("div",{style:{"--tpl-inverse":"var(--pastel-3)","--tpl-bg":"var(--pastel-ink)"},children:n.jsx(Np,{})}),n.jsx(zn,{})]})}function My({children:i}){var p,b,y,E;const r=F(),{place:u,site:o}=r,f=hp(),m=Oy(f),h=et(r)[0],x=r.links.filter(T=>T.confirmed),v=u.roadAddress??u.address;return n.jsxs("div",{className:"flex min-h-screen w-full flex-col",style:{backgroundColor:"var(--tpl-bg)",color:"var(--tpl-text)"},children:[n.jsx("header",{className:"sticky top-0 z-40 w-full border-b-2 backdrop-blur-md",style:{backgroundColor:"color-mix(in srgb, var(--tpl-bg) 90%, transparent)",borderColor:"var(--tpl-text)"},children:n.jsxs("div",{className:"flex h-14 items-center justify-between gap-4 px-4 sm:px-6 xl:px-8",children:[n.jsx("a",{href:"#top",className:"serif min-w-0 truncate text-[length:var(--fs-lead)] font-extrabold",children:u.name}),n.jsxs("div",{className:"flex shrink-0 items-center gap-4 text-[length:var(--fs-sm)]",children:[u.phone&&n.jsx("a",{href:`tel:${u.phone}`,className:"tap inline-flex items-center underline underline-offset-4",children:u.phone}),h&&n.jsxs("a",{href:h.url,target:"_blank",rel:"noopener noreferrer",className:"tap inline-flex items-center font-bold underline underline-offset-4",children:[Pe(h)," 예약"]})]})]})}),n.jsxs("div",{className:"flex w-full flex-1",children:[f.length>0&&n.jsx("aside",{className:"border-line sticky top-14 hidden h-[calc(100vh-3.5rem)] w-44 shrink-0 self-start border-r xl:block",style:{backgroundImage:Ms},children:n.jsx("nav",{"aria-label":"차례",className:"flex h-full flex-col justify-center gap-0.5 py-8 pl-6 pr-3",children:f.map(T=>{const N=T.anchor===m;return n.jsxs("a",{href:`#${T.anchor}`,"aria-current":N?"true":void 0,className:"flex items-baseline gap-2 py-2 text-[length:var(--fs-sm)] leading-tight motion-safe:transition-opacity",style:{opacity:N?1:.35},children:[n.jsx("span",{className:"shrink-0 text-[length:var(--fs-xs)] font-bold tabular-nums",children:T.no}),n.jsx("span",{className:"truncate",style:{fontWeight:N?700:400},children:T.label})]},T.anchor)})})}),n.jsx("div",{className:"flex min-w-0 flex-1 flex-col",children:i})]}),n.jsx("footer",{className:"w-full pb-28 pt-12 md:pb-16",style:{backgroundColor:"var(--tpl-inverse)",color:"var(--tpl-bg)",backgroundImage:Ms},children:n.jsxs("div",{className:"shell",children:[n.jsx("div",{"aria-hidden":!0,className:"h-0.5 w-full bg-current opacity-70"}),n.jsxs("div",{className:"grid grid-cols-1 gap-8 pt-8 md:grid-cols-12",children:[n.jsxs("div",{className:"md:col-span-5",children:[n.jsx("p",{className:"serif",style:{fontSize:"clamp(1.5rem, 4vw, 2.5rem)",fontWeight:800,lineHeight:1.05},children:u.name}),u.englishName&&n.jsx("p",{className:"mt-2 text-[length:var(--fs-xs)] uppercase tracking-[0.18em] opacity-55",children:u.englishName})]}),n.jsxs("address",{className:"space-y-2 font-serif text-[length:var(--fs-sm)] not-italic opacity-80 md:col-span-4",children:[v&&n.jsx("p",{children:v}),u.phone&&n.jsx("p",{children:n.jsx("a",{href:`tel:${u.phone}`,className:"underline underline-offset-4",children:u.phone})}),u.email&&n.jsx("p",{children:n.jsx("a",{href:`mailto:${u.email}`,className:"underline underline-offset-4",children:u.email})})]}),x.length>0&&n.jsx("nav",{"aria-label":"공식 채널",className:"md:col-span-3",children:n.jsx("ul",{className:"space-y-1.5 text-[length:var(--fs-sm)]",children:x.map(T=>n.jsx("li",{children:n.jsx("a",{href:T.url,target:"_blank",rel:"noopener noreferrer",className:"underline underline-offset-4 opacity-80 transition-opacity hover:opacity-100",children:Rl(T)})},T.url))})})]}),n.jsxs("div",{className:"mt-10 flex flex-col gap-2 border-t border-current/20 pt-5 text-[length:var(--fs-xs)] opacity-55 sm:flex-row sm:items-center sm:justify-between",children:[n.jsxs("div",{className:"flex flex-wrap items-center gap-x-3 gap-y-1",children:[n.jsxs("span",{children:["상호: ",u.name]}),((p=u.legal)==null?void 0:p.representative)&&n.jsxs("span",{children:["대표: ",u.legal.representative]}),((b=u.legal)==null?void 0:b.businessRegistrationNumber)&&n.jsxs("span",{children:["사업자등록번호: ",u.legal.businessRegistrationNumber]}),((y=u.legal)==null?void 0:y.mailOrderNumber)&&n.jsxs("span",{children:["통신판매업신고: ",u.legal.mailOrderNumber]}),((E=u.legal)==null?void 0:E.licenseNumber)&&n.jsxs("span",{children:[u.legal.licenseLabel??"인허가번호",": ",u.legal.licenseNumber]})]}),n.jsxs("p",{className:"byline shrink-0",children:[n.jsxs("span",{className:"author",children:["작성·운영 ",u.name]})," · 최종 업데이트: ",n.jsx("time",{dateTime:o.updatedAt,children:qs(o.updatedAt)})]})]})]})}),n.jsx(zn,{})]})}function Oy(i){const r=i.map(f=>f.anchor).join(","),[u,o]=Y.useState("");return Y.useEffect(()=>{if(typeof IntersectionObserver>"u")return;const f=r.split(",").map(h=>document.getElementById(h)).filter(h=>h!==null);if(f.length===0)return;const m=new IntersectionObserver(h=>{const x=h.find(v=>v.isIntersecting);x&&o(x.target.id)},{rootMargin:"-45% 0px -50% 0px"});return f.forEach(h=>m.observe(h)),()=>m.disconnect()},[r]),u}function Dy(){const i=F(),r={intro:Sb,info:zb,rooms:qu,pricing:Ub,menu:qu,programs:qu,booking:Rb,space:Hb,inquiry:Lb,exhibition:qb,photos:Kb,festival:Yb,local:Gb,weather:Zb,map:Zh,faq:Jb,...vy},u=new Set;return n.jsxs("main",{className:"w-full flex-1",children:[n.jsx(U0,{}),!Kt(i,"intro")&&n.jsx(Mh,{}),i.theme.sections.filter(o=>o.enabled&&o.id!=="hero").map(o=>{const f=r[o.id];if(!f)return null;const m=f.name;return u.has(m)?null:(u.add(m),n.jsxs(Y.Fragment,{children:[n.jsx(f,{}),o.id==="intro"&&n.jsx(Mh,{})]},o.id))}),!Kt(i,"map")&&n.jsx(Zh,{})]})}function Ph({payload:i}){const r=mp(i.theme.templateId),u=r==="reservation"?by:r==="oasi"?Ay:r==="studio"?Ty:r==="pastel"?Cy:r==="editorial"?My:ky;return n.jsx(kv,{payload:i,children:n.jsx(u,{children:n.jsx(Dy,{})})})}function ky({children:i}){return n.jsxs("div",{className:"flex min-h-screen w-full flex-col",children:[n.jsx(g0,{}),n.jsx("div",{className:"flex w-full flex-1 flex-col",children:i}),n.jsx(Np,{}),n.jsx(zn,{})]})}const ep=document.getElementById("root"),tp=window.__SITE_PAYLOAD__;tp?Eh.hydrateRoot(ep,n.jsx(Y.StrictMode,{children:n.jsx(Ph,{payload:tp})})):Vg(async()=>{const{MOONLIGHT_STAY_PAYLOAD:i}=await import("./moonlight-stay-Cy0eR6hp.js");return{MOONLIGHT_STAY_PAYLOAD:i}},[]).then(({MOONLIGHT_STAY_PAYLOAD:i})=>{Eh.createRoot(ep).render(n.jsx(Y.StrictMode,{children:n.jsx(Ph,{payload:i})}))});export{Th as F,$t as L,zs as P,Uy as S,Ry as a}; diff --git a/solution/site/scripts/mockup/vendor/index-k3f3eOzb.css b/solution/site/scripts/mockup/vendor/index-k3f3eOzb.css new file mode 100644 index 0000000..e1f4ed1 --- /dev/null +++ b/solution/site/scripts/mockup/vendor/index-k3f3eOzb.css @@ -0,0 +1 @@ +.w4-sky{position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:none;overflow:hidden}.w4-sky[data-mood=비]:before{content:"";position:absolute;top:-20%;right:-10%;bottom:-20%;left:-10%;background-image:repeating-linear-gradient(102deg,transparent 0 9px,currentcolor 9px 10px,transparent 10px 22px);opacity:.13;animation:w4-rain .9s linear infinite}@keyframes w4-rain{to{transform:translate3d(-22px,108px,0)}}.w4-sky[data-mood=맑음]:before{content:"";position:absolute;right:6%;top:-40%;width:26rem;aspect-ratio:1;background-image:conic-gradient(from 0deg,currentcolor 0 2deg,transparent 2deg 30deg);opacity:.07;animation:w4-spin 70s linear infinite}.w4-sky[data-mood=맑음]:after{content:"";position:absolute;right:10%;top:-14%;width:15rem;aspect-ratio:1;border-radius:50%;background-image:radial-gradient(circle,currentcolor 0%,transparent 62%);opacity:.11;animation:w4-breathe 9s ease-in-out infinite}@keyframes w4-spin{to{transform:rotate(360deg)}}@keyframes w4-breathe{50%{opacity:.2}}.w4-sky[data-mood=흐림]:before,.w4-sky[data-mood=흐림]:after{content:"";position:absolute;top:-30%;right:-60%;bottom:-30%;left:-60%;background-image:radial-gradient(ellipse 22% 46% at 18% 42%,currentcolor 0%,transparent 70%),radial-gradient(ellipse 30% 40% at 62% 30%,currentcolor 0%,transparent 72%);opacity:.08;animation:w4-drift 44s linear infinite}.w4-sky[data-mood=흐림]:after{opacity:.05;animation-duration:78s;animation-direction:reverse}@keyframes w4-drift{to{transform:translate3d(28%,0,0)}}.w4-sky[data-mood=눈]:before{content:"";position:absolute;top:-20%;right:0;bottom:-20%;left:0;background-image:radial-gradient(circle at 12% 10%,currentcolor 1.4px,transparent 1.8px),radial-gradient(circle at 47% 34%,currentcolor 1.1px,transparent 1.5px),radial-gradient(circle at 78% 18%,currentcolor 1.6px,transparent 2px);background-size:9rem 9rem;opacity:.22;animation:w4-snow 14s linear infinite}@keyframes w4-snow{to{transform:translate3d(-2rem,9rem,0)}}@media(prefers-reduced-motion:reduce){.w4-sky:before,.w4-sky:after{animation:none!important}}.w4-paper{background-image:repeating-linear-gradient(0deg,color-mix(in oklab,currentcolor 4%,transparent) 0 1px,transparent 1px 3px),repeating-linear-gradient(90deg,color-mix(in oklab,currentcolor 3%,transparent) 0 1px,transparent 1px 4px)}.w4-disc{background:radial-gradient(circle at 50% 50%,transparent 0 15.5%,var(--lbl) 15.5% 33%,transparent 33%),repeating-radial-gradient(circle at 50% 50%,color-mix(in oklab,var(--w4-vinyl) 88%,white) 0 1.4px,var(--w4-vinyl) 1.4px 3px),var(--w4-vinyl);box-shadow:inset 0 0 40px #0000008c}.w4-disc-sheen{background:conic-gradient(from 210deg,rgb(255 255 255 / 12%),transparent 22%,transparent 70%,rgb(255 255 255 / 7%))}.w4-spin{animation:w4-rev 2.2s linear infinite}@keyframes w4-rev{to{transform:rotate(360deg)}}.w4-disc-mini{background:radial-gradient(circle at 50% 50%,transparent 0 14%,var(--lbl) 14% 34%,transparent 34%),repeating-radial-gradient(circle at 50% 50%,color-mix(in oklab,var(--w4-vinyl) 88%,white) 0 1.2px,var(--w4-vinyl) 1.2px 2.6px),var(--w4-vinyl);box-shadow:0 3px 10px #00000059}.w4-perf{background:repeating-linear-gradient(90deg,transparent 0 8px,var(--tear) 8px 9px)}.w4-dash{border-top:1px dashed currentcolor}.w4-film-perf{background:repeating-linear-gradient(90deg,currentcolor 0 9px,transparent 9px 21px)}.w4-flip{perspective:900px}.w4-flip-inner{transform-style:preserve-3d;transition:transform .5s}.w4-flip-on .w4-flip-inner{transform:rotateY(180deg)}.w4-flip-face{backface-visibility:hidden}.w4-flip-back{transform:rotateY(180deg)}.w4-scroll{overflow-x:auto;scrollbar-width:none;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch}.w4-scroll::-webkit-scrollbar{display:none}.w4-scroll[data-slider=on]{overflow-x:hidden;scroll-snap-type:none}.w4-scroll[data-dragging=true]{cursor:grabbing;-webkit-user-select:none;user-select:none}@media(prefers-reduced-motion:reduce){.w4-spin{animation:none}.w4-flip-inner{transition:none}}@media print{.w4-flip,.w4-flip-inner{height:auto!important;transform:none!important}.w4-flip-face{position:static!important;backface-visibility:visible!important;transform:none!important}}/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-scroll-snap-strictness:proximity;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-divide-x-reverse:0}}}@layer theme{:root,:host{--font-sans:"Pretendard Variable", "Noto Sans KR", system-ui, sans-serif;--font-serif:"Noto Serif KR", "Batang", "Times New Roman", serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-zinc-950:oklch(14.1% .005 285.823);--color-stone-600:oklch(44.4% .011 73.639);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-2xl:42rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--leading-tight:1.25;--leading-snug:1.375;--leading-relaxed:1.625;--leading-loose:2;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--blur-sm:8px;--blur-md:12px;--aspect-video:16 / 9;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:"Pretendard Variable", "Noto Sans KR", system-ui, sans-serif;--default-mono-font-family:var(--font-mono);--color-brand:var(--tpl-primary);--color-surface:var(--tpl-bg);--color-ink:var(--tpl-text);--color-accent:var(--tpl-accent);--color-line:currentColor}@supports (color:color-mix(in lab,red,red)){:root,:host{--color-line:color-mix(in oklab, currentColor 14%, transparent)}}:root,:host{--color-muted:currentColor}@supports (color:color-mix(in lab,red,red)){:root,:host{--color-muted:color-mix(in oklab, currentColor 62%, transparent)}}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components{.serif{font-family:var(--tpl-font-heading,var(--font-serif));letter-spacing:var(--tpl-heading-tracking,normal)}.serif,.h2,.h3{font-synthesis-weight:none}.tpl-border{border-width:var(--tpl-border-width,1px)}.tpl-shadow{box-shadow:var(--tpl-shadow,none)}.shell{width:100%;max-width:78rem;padding-inline:max(clamp(1rem,4vw,2.5rem),env(safe-area-inset-left),env(safe-area-inset-right));margin-inline:auto}.line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.measure{max-width:68ch}.paper{background-image:var(--tpl-texture,none)}.panel{background-color:currentColor}@supports (color:color-mix(in lab,red,red)){.panel{background-color:color-mix(in oklab,currentColor 5%,transparent)}}.panel{border:var(--tpl-border-width,1px) solid var(--color-line);border-radius:var(--tpl-radius,.75rem);box-shadow:var(--tpl-shadow,none)}.panel-sunken{background-color:currentColor}@supports (color:color-mix(in lab,red,red)){.panel-sunken{background-color:color-mix(in oklab,currentColor 6%,transparent)}}.h2{font-family:var(--tpl-font-heading,var(--font-serif));font-size:var(--fs-h2);font-weight:var(--tpl-heading-weight,700);letter-spacing:var(--tpl-heading-tracking,-.01em);line-height:1.2}.h3{font-family:var(--tpl-font-heading,var(--font-serif));font-size:var(--fs-h3);font-weight:var(--tpl-heading-weight,700);letter-spacing:var(--tpl-heading-tracking,-.005em);line-height:1.35}.label{font-size:var(--fs-xs);color:var(--color-muted);font-weight:600}.tap{min-width:2.75rem;min-height:2.75rem}.only-touch{display:none}@media(hover:none)and (pointer:coarse){.only-touch{display:flex}}.safe-t{padding-top:env(safe-area-inset-top);position:relative}.safe-t:before{content:"";height:env(safe-area-inset-top);background:var(--tpl-surface,#fff);pointer-events:none;position:absolute;inset:0 0 auto}.safe-b{padding-bottom:calc(.5rem + env(safe-area-inset-bottom))}}@layer utilities{.pointer-events-none{pointer-events:none}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.inset-2{inset:calc(var(--spacing) * 2)}.inset-x-0{inset-inline:0}.inset-y-\[3px\]{inset-block:3px}.-top-1{top:calc(var(--spacing) * -1)}.-top-2{top:calc(var(--spacing) * -2)}.top-0{top:0}.top-1\/2{top:50%}.top-3{top:calc(var(--spacing) * 3)}.top-14{top:calc(var(--spacing) * 14)}.top-\[calc\(100\%\+0\.5rem\)\]{top:calc(100% + .5rem)}.top-full{top:100%}.-right-1{right:calc(var(--spacing) * -1)}.right-0{right:0}.right-1{right:var(--spacing)}.right-2{right:calc(var(--spacing) * 2)}.right-2\.5{right:calc(var(--spacing) * 2.5)}.right-3{right:calc(var(--spacing) * 3)}.right-4{right:calc(var(--spacing) * 4)}.bottom-0{bottom:0}.bottom-0\.5{bottom:calc(var(--spacing) * .5)}.bottom-2{bottom:calc(var(--spacing) * 2)}.bottom-2\.5{bottom:calc(var(--spacing) * 2.5)}.bottom-3{bottom:calc(var(--spacing) * 3)}.bottom-5{bottom:calc(var(--spacing) * 5)}.left-0{left:0}.left-1{left:var(--spacing)}.left-1\/2{left:50%}.left-2{left:calc(var(--spacing) * 2)}.left-4{left:calc(var(--spacing) * 4)}.left-full{left:100%}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.order-1{order:1}.order-2{order:2}.col-span-12{grid-column:span 12/span 12}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-3{margin-block:calc(var(--spacing) * 3)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-3\.5{margin-top:calc(var(--spacing) * 3.5)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-7{margin-top:calc(var(--spacing) * 7)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-14{margin-top:calc(var(--spacing) * 14)}.mt-16{margin-top:calc(var(--spacing) * 16)}.mt-auto{margin-top:auto}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.-mb-px{margin-bottom:-1px}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-3\.5{margin-bottom:calc(var(--spacing) * 3.5)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-7{margin-bottom:calc(var(--spacing) * 7)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-10{margin-bottom:calc(var(--spacing) * 10)}.-ml-10{margin-left:calc(var(--spacing) * -10)}.ml-1{margin-left:var(--spacing)}.ml-1\.5{margin-left:calc(var(--spacing) * 1.5)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-5{margin-left:calc(var(--spacing) * 5)}.ml-6{margin-left:calc(var(--spacing) * 6)}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-\[9\]{-webkit-line-clamp:9;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.inline-grid{display:inline-grid}.aspect-3\/2{aspect-ratio:3/2}.aspect-3\/4{aspect-ratio:3/4}.aspect-4\/3{aspect-ratio:4/3}.aspect-16\/10{aspect-ratio:16/10}.aspect-\[4\/3\]{aspect-ratio:4/3}.aspect-square{aspect-ratio:1}.size-2\.5{width:calc(var(--spacing) * 2.5);height:calc(var(--spacing) * 2.5)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-3\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-12{width:calc(var(--spacing) * 12);height:calc(var(--spacing) * 12)}.size-14{width:calc(var(--spacing) * 14);height:calc(var(--spacing) * 14)}.size-\[17px\]{width:17px;height:17px}.size-\[54px\]{width:54px;height:54px}.size-\[76px\]{width:76px;height:76px}.size-\[220px\]{width:220px;height:220px}.size-full{width:100%;height:100%}.h-0\.5{height:calc(var(--spacing) * .5)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-14{height:calc(var(--spacing) * 14)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-48{height:calc(var(--spacing) * 48)}.h-\[1em\]{height:1em}.h-\[6px\]{height:6px}.h-\[22px\]{height:22px}.h-\[56px\]{height:56px}.h-\[260px\]{height:260px}.h-\[calc\(100vh-3\.5rem\)\]{height:calc(100vh - 3.5rem)}.h-\[clamp\(15rem\,24vw\,21rem\)\]{height:clamp(15rem,24vw,21rem)}.h-\[clamp\(17rem\,27vw\,24rem\)\]{height:clamp(17rem,27vw,24rem)}.h-auto{height:auto}.h-fit{height:fit-content}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-\[26rem\]{max-height:26rem}.max-h-\[46vh\]{max-height:46vh}.max-h-\[72vh\]{max-height:72vh}.max-h-\[85vh\]{max-height:85vh}.max-h-\[88vh\]{max-height:88vh}.max-h-full{max-height:100%}.min-h-\[2rem\]{min-height:2rem}.min-h-\[16rem\]{min-height:16rem}.min-h-screen{min-height:100vh}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-10{width:calc(var(--spacing) * 10)}.w-12{width:calc(var(--spacing) * 12)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-44{width:calc(var(--spacing) * 44)}.w-48{width:calc(var(--spacing) * 48)}.w-52{width:calc(var(--spacing) * 52)}.w-\[3px\]{width:3px}.w-\[44px\]{width:44px}.w-\[62px\]{width:62px}.w-\[110px\]{width:110px}.w-\[168px\]{width:168px}.w-\[210px\]{width:210px}.w-\[228px\]{width:228px}.w-\[254px\]{width:254px}.w-\[262px\]{width:262px}.w-\[304px\]{width:304px}.w-\[320px\]{width:320px}.w-auto{width:auto}.w-fit{width:fit-content}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-\[34rem\]{max-width:34rem}.max-w-\[72rem\]{max-width:72rem}.max-w-full{max-width:100%}.max-w-none{max-width:none}.min-w-0{min-width:0}.min-w-\[22px\]{min-width:22px}.min-w-\[30rem\]{min-width:30rem}.min-w-\[34rem\]{min-width:34rem}.flex-1{flex:1}.flex-\[1\.4\]{flex:1.4}.shrink-0{flex-shrink:0}.grow-0{flex-grow:0}.basis-40{flex-basis:calc(var(--spacing) * 40)}.basis-\[76\%\]{flex-basis:76%}.basis-\[86\%\]{flex-basis:86%}.border-collapse{border-collapse:collapse}.origin-right{transform-origin:100%}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-px{--tw-translate-y:1px;translate:var(--tw-translate-x) var(--tw-translate-y)}.-rotate-6{rotate:-6deg}.rotate-6{rotate:6deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.resize{resize:both}.snap-x{scroll-snap-type:x var(--tw-scroll-snap-strictness)}.snap-mandatory{--tw-scroll-snap-strictness:mandatory}.snap-center{scroll-snap-align:center}.snap-start{scroll-snap-align:start}.\[scrollbar-width\:none\]{scrollbar-width:none}.list-none{list-style-type:none}.columns-1{columns:1}.columns-2{columns:2}.break-inside-avoid{break-inside:avoid}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-\[1fr_auto\]{grid-template-columns:1fr auto}.grid-cols-\[46px_minmax\(0\,1fr\)\]{grid-template-columns:46px minmax(0,1fr)}.grid-cols-\[minmax\(0\,1fr\)_auto\]{grid-template-columns:minmax(0,1fr) auto}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-7{gap:calc(var(--spacing) * 7)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-10{gap:calc(var(--spacing) * 10)}.gap-14{gap:calc(var(--spacing) * 14)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-10>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 10) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 10) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-16>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 16) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 16) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-20>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 20) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 20) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-2{column-gap:calc(var(--spacing) * 2)}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-x-8{column-gap:calc(var(--spacing) * 8)}.gap-x-10{column-gap:calc(var(--spacing) * 10)}.gap-x-12{column-gap:calc(var(--spacing) * 12)}.gap-y-0\.5{row-gap:calc(var(--spacing) * .5)}.gap-y-1{row-gap:var(--spacing)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}.gap-y-4{row-gap:calc(var(--spacing) * 4)}.gap-y-5{row-gap:calc(var(--spacing) * 5)}.gap-y-6{row-gap:calc(var(--spacing) * 6)}.gap-y-14{row-gap:calc(var(--spacing) * 14)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-line>:not(:last-child)){border-color:currentColor}@supports (color:color-mix(in lab,red,red)){:where(.divide-line>:not(:last-child)){border-color:color-mix(in oklab,currentColor 14%,transparent)}}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-2xl{border-radius:calc(var(--tpl-radius,.75rem) * 1.7)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--tpl-radius,.75rem)}.rounded-md{border-radius:calc(var(--tpl-radius,.75rem) * .7)}.rounded-sm{border-radius:calc(var(--tpl-radius,.75rem) * .4)}.rounded-xl{border-radius:calc(var(--tpl-radius,.75rem) * 1.35)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-black\/8{border-color:#00000014}@supports (color:color-mix(in lab,red,red)){.border-black\/8{border-color:color-mix(in oklab,var(--color-black) 8%,transparent)}}.border-current\/15{border-color:currentColor}@supports (color:color-mix(in lab,red,red)){.border-current\/15{border-color:color-mix(in oklab,currentcolor 15%,transparent)}}.border-current\/20{border-color:currentColor}@supports (color:color-mix(in lab,red,red)){.border-current\/20{border-color:color-mix(in oklab,currentcolor 20%,transparent)}}.border-current\/25{border-color:currentColor}@supports (color:color-mix(in lab,red,red)){.border-current\/25{border-color:color-mix(in oklab,currentcolor 25%,transparent)}}.border-current\/60{border-color:currentColor}@supports (color:color-mix(in lab,red,red)){.border-current\/60{border-color:color-mix(in oklab,currentcolor 60%,transparent)}}.border-current\/70{border-color:currentColor}@supports (color:color-mix(in lab,red,red)){.border-current\/70{border-color:color-mix(in oklab,currentcolor 70%,transparent)}}.border-line{border-color:currentColor}@supports (color:color-mix(in lab,red,red)){.border-line{border-color:color-mix(in oklab,currentColor 14%,transparent)}}.border-transparent{border-color:#0000}.bg-\[\#03C75A\]{background-color:#03c75a}.bg-\[\#0064FF\]{background-color:#0064ff}.bg-\[\#FEE500\]{background-color:#fee500}.bg-black\/45{background-color:#00000073}@supports (color:color-mix(in lab,red,red)){.bg-black\/45{background-color:color-mix(in oklab,var(--color-black) 45%,transparent)}}.bg-black\/55{background-color:#0000008c}@supports (color:color-mix(in lab,red,red)){.bg-black\/55{background-color:color-mix(in oklab,var(--color-black) 55%,transparent)}}.bg-black\/70{background-color:#000000b3}@supports (color:color-mix(in lab,red,red)){.bg-black\/70{background-color:color-mix(in oklab,var(--color-black) 70%,transparent)}}.bg-black\/92{background-color:#000000eb}@supports (color:color-mix(in lab,red,red)){.bg-black\/92{background-color:color-mix(in oklab,var(--color-black) 92%,transparent)}}.bg-current,.bg-current\/5{background-color:currentColor}@supports (color:color-mix(in lab,red,red)){.bg-current\/5{background-color:color-mix(in oklab,currentcolor 5%,transparent)}}.bg-current\/8{background-color:currentColor}@supports (color:color-mix(in lab,red,red)){.bg-current\/8{background-color:color-mix(in oklab,currentcolor 8%,transparent)}}.bg-current\/15{background-color:currentColor}@supports (color:color-mix(in lab,red,red)){.bg-current\/15{background-color:color-mix(in oklab,currentcolor 15%,transparent)}}.bg-white\/10{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.bg-white\/10{background-color:color-mix(in oklab,var(--color-white) 10%,transparent)}}.bg-white\/70{background-color:#ffffffb3}@supports (color:color-mix(in lab,red,red)){.bg-white\/70{background-color:color-mix(in oklab,var(--color-white) 70%,transparent)}}.bg-zinc-950{background-color:var(--color-zinc-950)}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.object-center{object-position:center}.p-0{padding:0}.p-3\.5{padding:calc(var(--spacing) * 3.5)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.px-9{padding-inline:calc(var(--spacing) * 9)}.px-\[7\%\]{padding-inline:7%}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-3\.5{padding-block:calc(var(--spacing) * 3.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-16{padding-block:calc(var(--spacing) * 16)}.pt-0\.5{padding-top:calc(var(--spacing) * .5)}.pt-1{padding-top:var(--spacing)}.pt-1\.5{padding-top:calc(var(--spacing) * 1.5)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-2\.5{padding-top:calc(var(--spacing) * 2.5)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-3\.5{padding-top:calc(var(--spacing) * 3.5)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-7{padding-top:calc(var(--spacing) * 7)}.pt-8{padding-top:calc(var(--spacing) * 8)}.pt-10{padding-top:calc(var(--spacing) * 10)}.pt-12{padding-top:calc(var(--spacing) * 12)}.pt-14{padding-top:calc(var(--spacing) * 14)}.pt-20{padding-top:calc(var(--spacing) * 20)}.pt-24{padding-top:calc(var(--spacing) * 24)}.pt-\[var\(--oasi-gap\)\]{padding-top:var(--oasi-gap)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pr-5{padding-right:calc(var(--spacing) * 5)}.pr-8{padding-right:calc(var(--spacing) * 8)}.pb-1{padding-bottom:var(--spacing)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.pb-10{padding-bottom:calc(var(--spacing) * 10)}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.pb-24{padding-bottom:calc(var(--spacing) * 24)}.pb-28{padding-bottom:calc(var(--spacing) * 28)}.pb-56{padding-bottom:calc(var(--spacing) * 56)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-6{padding-left:calc(var(--spacing) * 6)}.pl-9{padding-left:calc(var(--spacing) * 9)}.pl-\[calc\(3px\+0\.625rem\)\]{padding-left:calc(3px + .625rem)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-serif{font-family:Noto Serif KR,Batang,Times New Roman,serif}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[17px\]{font-size:17px}.text-\[length\:var\(--fs-body\)\]{font-size:var(--fs-body)}.text-\[length\:var\(--fs-h2\)\]{font-size:var(--fs-h2)}.text-\[length\:var\(--fs-h3\)\]{font-size:var(--fs-h3)}.text-\[length\:var\(--fs-lead\)\]{font-size:var(--fs-lead)}.text-\[length\:var\(--fs-sm\)\]{font-size:var(--fs-sm)}.text-\[length\:var\(--fs-xs\)\]{font-size:var(--fs-xs)}.leading-\[1\.8\]{--tw-leading:1.8;line-height:1.8}.leading-\[1\.9\]{--tw-leading:1.9;line-height:1.9}.leading-loose{--tw-leading:var(--leading-loose);line-height:var(--leading-loose)}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.1em\]{--tw-tracking:.1em;letter-spacing:.1em}.tracking-\[0\.2em\]{--tw-tracking:.2em;letter-spacing:.2em}.tracking-\[0\.3em\]{--tw-tracking:.3em;letter-spacing:.3em}.tracking-\[0\.08em\]{--tw-tracking:.08em;letter-spacing:.08em}.tracking-\[0\.14em\]{--tw-tracking:.14em;letter-spacing:.14em}.tracking-\[0\.16em\]{--tw-tracking:.16em;letter-spacing:.16em}.tracking-\[0\.18em\]{--tw-tracking:.18em;letter-spacing:.18em}.tracking-\[0\.24em\]{--tw-tracking:.24em;letter-spacing:.24em}.break-all{word-break:break-all}.break-keep{word-break:keep-all}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.\!text-current{color:currentColor!important}.text-\[\#191600\]{color:#191600}.text-current\/55{color:currentColor}@supports (color:color-mix(in lab,red,red)){.text-current\/55{color:color-mix(in oklab,currentcolor 55%,transparent)}}.text-muted{color:currentColor}@supports (color:color-mix(in lab,red,red)){.text-muted{color:color-mix(in oklab,currentColor 62%,transparent)}}.text-stone-600{color:var(--color-stone-600)}.text-white{color:var(--color-white)}.text-white\/70{color:#ffffffb3}@supports (color:color-mix(in lab,red,red)){.text-white\/70{color:color-mix(in oklab,var(--color-white) 70%,transparent)}}.uppercase{text-transform:uppercase}.italic{font-style:italic}.not-italic{font-style:normal}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.underline-offset-4{text-underline-offset:4px}.underline-offset-\[5px\]{text-underline-offset:5px}.underline-offset-\[6px\]{text-underline-offset:6px}.opacity-0{opacity:0}.opacity-20{opacity:.2}.opacity-25{opacity:.25}.opacity-40{opacity:.4}.opacity-45{opacity:.45}.opacity-50{opacity:.5}.opacity-55{opacity:.55}.opacity-60{opacity:.6}.opacity-65{opacity:.65}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.opacity-85{opacity:.85}.opacity-90{opacity:.9}.opacity-95{opacity:.95}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.duration-700{--tw-duration:.7s;transition-duration:.7s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.select-none{-webkit-user-select:none;user-select:none}.group-open\:rotate-45:is(:where(.group):is([open],:popover-open,:open) *){rotate:45deg}.group-open\:rotate-180:is(:where(.group):is([open],:popover-open,:open) *){rotate:180deg}@media(hover:hover){.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:scale-110:is(:where(.group):hover *){--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.marker\:content-none ::marker{--tw-content:none;content:none}.marker\:content-none::marker{--tw-content:none;content:none}.marker\:content-none ::-webkit-details-marker{--tw-content:none;content:none}.marker\:content-none::-webkit-details-marker{--tw-content:none;content:none}@media(hover:hover){.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:border-current:hover{border-color:currentColor}.hover\:bg-black\/65:hover{background-color:#000000a6}@supports (color:color-mix(in lab,red,red)){.hover\:bg-black\/65:hover{background-color:color-mix(in oklab,var(--color-black) 65%,transparent)}}.hover\:bg-black\/75:hover{background-color:#000000bf}@supports (color:color-mix(in lab,red,red)){.hover\:bg-black\/75:hover{background-color:color-mix(in oklab,var(--color-black) 75%,transparent)}}.hover\:bg-current\/10:hover{background-color:currentColor}@supports (color:color-mix(in lab,red,red)){.hover\:bg-current\/10:hover{background-color:color-mix(in oklab,currentcolor 10%,transparent)}}.hover\:bg-current\/25:hover{background-color:currentColor}@supports (color:color-mix(in lab,red,red)){.hover\:bg-current\/25:hover{background-color:color-mix(in oklab,currentcolor 25%,transparent)}}.hover\:bg-white\/25:hover{background-color:#ffffff40}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/25:hover{background-color:color-mix(in oklab,var(--color-white) 25%,transparent)}}.hover\:text-current:hover{color:currentColor}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-45:hover{opacity:.45}.hover\:opacity-70:hover{opacity:.7}.hover\:opacity-80:hover{opacity:.8}.hover\:opacity-85:hover{opacity:.85}.hover\:opacity-90:hover{opacity:.9}.hover\:opacity-100:hover{opacity:1}}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-0:disabled{opacity:0}.disabled\:opacity-25:disabled{opacity:.25}@media(prefers-reduced-motion:no-preference){.motion-safe\:transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}}@media(min-width:40rem){.sm\:top-5{top:calc(var(--spacing) * 5)}.sm\:right-5{right:calc(var(--spacing) * 5)}.sm\:right-6{right:calc(var(--spacing) * 6)}.sm\:left-6{left:calc(var(--spacing) * 6)}.sm\:mb-8{margin-bottom:calc(var(--spacing) * 8)}.sm\:mb-10{margin-bottom:calc(var(--spacing) * 10)}.sm\:mb-14{margin-bottom:calc(var(--spacing) * 14)}.sm\:block{display:block}.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:inline{display:inline}.sm\:inline-flex{display:inline-flex}.sm\:aspect-video{aspect-ratio:var(--aspect-video)}.sm\:w-28{width:calc(var(--spacing) * 28)}.sm\:w-40{width:calc(var(--spacing) * 40)}.sm\:max-w-\[18rem\]{max-width:18rem}.sm\:shrink-0{flex-shrink:0}.sm\:basis-1\/3{flex-basis:33.3333%}.sm\:basis-\[56\%\]{flex-basis:56%}.sm\:columns-2{columns:2}.sm\:columns-3{columns:3}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:flex-wrap{flex-wrap:wrap}.sm\:items-baseline{align-items:baseline}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}.sm\:justify-between{justify-content:space-between}.sm\:gap-4{gap:calc(var(--spacing) * 4)}.sm\:gap-6{gap:calc(var(--spacing) * 6)}.sm\:gap-8{gap:calc(var(--spacing) * 8)}.sm\:gap-10{gap:calc(var(--spacing) * 10)}:where(.sm\:space-y-24>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 24) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 24) * calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:divide-x>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px * var(--tw-divide-x-reverse));border-inline-end-width:calc(1px * calc(1 - var(--tw-divide-x-reverse)))}.sm\:border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.sm\:p-5{padding:calc(var(--spacing) * 5)}.sm\:p-6{padding:calc(var(--spacing) * 6)}.sm\:p-8{padding:calc(var(--spacing) * 8)}.sm\:px-5{padding-inline:calc(var(--spacing) * 5)}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}.sm\:px-10{padding-inline:calc(var(--spacing) * 10)}.sm\:py-14{padding-block:calc(var(--spacing) * 14)}.sm\:py-24{padding-block:calc(var(--spacing) * 24)}.sm\:pb-12{padding-bottom:calc(var(--spacing) * 12)}.sm\:pl-14{padding-left:calc(var(--spacing) * 14)}.sm\:text-right{text-align:right}.sm\:first\:pl-0:first-child{padding-left:0}}@media(min-width:48rem){.md\:col-span-3{grid-column:span 3/span 3}.md\:col-span-4{grid-column:span 4/span 4}.md\:col-span-5{grid-column:span 5/span 5}.md\:col-span-7{grid-column:span 7/span 7}.md\:table-cell{display:table-cell}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:grid-cols-\[240px_minmax\(0\,1fr\)\]{grid-template-columns:240px minmax(0,1fr)}.md\:gap-12{gap:calc(var(--spacing) * 12)}.md\:pb-14{padding-bottom:calc(var(--spacing) * 14)}.md\:pb-16{padding-bottom:calc(var(--spacing) * 16)}}@media(min-width:64rem){.lg\:sticky{position:sticky}.lg\:top-0{top:0}.lg\:top-12{top:calc(var(--spacing) * 12)}.lg\:order-1{order:1}.lg\:order-2{order:2}.lg\:order-none{order:0}.lg\:col-span-5{grid-column:span 5/span 5}.lg\:col-span-6{grid-column:span 6/span 6}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:col-span-12{grid-column:span 12/span 12}.lg\:mt-24{margin-top:calc(var(--spacing) * 24)}.lg\:mt-auto{margin-top:auto}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:grid{display:grid}.lg\:hidden{display:none}.lg\:aspect-3\/2{aspect-ratio:3/2}.lg\:aspect-auto{aspect-ratio:auto}.lg\:h-fit{height:fit-content}.lg\:h-full{height:100%}.lg\:h-screen{height:100vh}.lg\:min-h-\[24rem\]{min-height:24rem}.lg\:min-h-\[34rem\]{min-height:34rem}.lg\:w-\[18\.75rem\]{width:18.75rem}.lg\:w-\[260px\]{width:260px}.lg\:w-auto{width:auto}.lg\:shrink-0{flex-shrink:0}.lg\:basis-1\/4{flex-basis:25%}.lg\:basis-auto{flex-basis:auto}.lg\:snap-none{scroll-snap-type:none}.lg\:columns-4{columns:4}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:grid-cols-\[1fr_auto_1fr\]{grid-template-columns:1fr auto 1fr}.lg\:flex-col{flex-direction:column}.lg\:flex-row{flex-direction:row}.lg\:items-end{align-items:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:gap-0{gap:0}.lg\:gap-3{gap:calc(var(--spacing) * 3)}.lg\:gap-4{gap:calc(var(--spacing) * 4)}.lg\:gap-7{gap:calc(var(--spacing) * 7)}.lg\:gap-8{gap:calc(var(--spacing) * 8)}.lg\:gap-10{gap:calc(var(--spacing) * 10)}.lg\:gap-12{gap:calc(var(--spacing) * 12)}.lg\:gap-14{gap:calc(var(--spacing) * 14)}.lg\:gap-20{gap:calc(var(--spacing) * 20)}:where(.lg\:space-y-28>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 28) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 28) * calc(1 - var(--tw-space-y-reverse)))}.lg\:gap-x-12{column-gap:calc(var(--spacing) * 12)}.lg\:overflow-visible{overflow:visible}.lg\:overflow-y-auto{overflow-y:auto}.lg\:px-7{padding-inline:calc(var(--spacing) * 7)}.lg\:px-10{padding-inline:calc(var(--spacing) * 10)}.lg\:px-14{padding-inline:calc(var(--spacing) * 14)}.lg\:py-10{padding-block:calc(var(--spacing) * 10)}.lg\:py-16{padding-block:calc(var(--spacing) * 16)}.lg\:pt-14{padding-top:calc(var(--spacing) * 14)}.lg\:pt-16{padding-top:calc(var(--spacing) * 16)}.lg\:pb-0{padding-bottom:0}.lg\:pb-14{padding-bottom:calc(var(--spacing) * 14)}.lg\:pb-24{padding-bottom:calc(var(--spacing) * 24)}}@media(min-width:80rem){.xl\:col-span-4{grid-column:span 4/span 4}.xl\:col-span-5{grid-column:span 5/span 5}.xl\:col-span-7{grid-column:span 7/span 7}.xl\:col-span-8{grid-column:span 8/span 8}.xl\:col-start-1{grid-column-start:1}.xl\:col-start-5{grid-column-start:5}.xl\:row-start-1{grid-row-start:1}.xl\:-mt-24{margin-top:calc(var(--spacing) * -24)}.xl\:mr-16{margin-right:calc(var(--spacing) * 16)}.xl\:mb-12{margin-bottom:calc(var(--spacing) * 12)}.xl\:-ml-10{margin-left:calc(var(--spacing) * -10)}.xl\:ml-auto{margin-left:auto}.xl\:block{display:block}.xl\:w-\[50\%\]{width:50%}.xl\:w-\[64\%\]{width:64%}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:gap-x-10{column-gap:calc(var(--spacing) * 10)}.xl\:self-end{align-self:flex-end}.xl\:px-8{padding-inline:calc(var(--spacing) * 8)}.xl\:py-8{padding-block:calc(var(--spacing) * 8)}.xl\:py-12{padding-block:calc(var(--spacing) * 12)}.xl\:py-16{padding-block:calc(var(--spacing) * 16)}.xl\:pt-12{padding-top:calc(var(--spacing) * 12)}.xl\:pr-8{padding-right:calc(var(--spacing) * 8)}.xl\:pr-16{padding-right:calc(var(--spacing) * 16)}.xl\:pb-56{padding-bottom:calc(var(--spacing) * 56)}.xl\:pl-10{padding-left:calc(var(--spacing) * 10)}.xl\:pl-20{padding-left:calc(var(--spacing) * 20)}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}.\[\&\:\:-webkit-scrollbar\]\:hidden::-webkit-scrollbar{display:none}.\[\&\>li\]\:mb-3>li{margin-bottom:calc(var(--spacing) * 3)}}@font-face{font-family:Pretendard Variable;font-weight:45 920;font-style:normal;font-display:swap;src:url(/fonts/PretendardVariable.woff2)format("woff2")}html{-webkit-text-size-adjust:100%}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;word-break:keep-all;overflow-wrap:break-word;margin:0;padding:0}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}html{scrollbar-width:thin}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}@media(prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}:root{--tpl-primary:#18181b;--tpl-secondary:#52525b;--tpl-bg:#fff;--tpl-card:#fafafa;--tpl-text:#09090b;--tpl-accent:#2563eb;--tpl-surface:#fff;--tpl-surface-alt:#f5f5f4;--tpl-inverse:#1c1917;--tpl-border:#e7e5e4;--tpl-font-heading:"Pretendard Variable", "Noto Sans KR", system-ui, sans-serif;--tpl-font-body:"Pretendard Variable", "Noto Sans KR", system-ui, sans-serif;--tpl-radius:.75rem;--tpl-border-width:1px;--tpl-shadow:0 1px 2px #0000000f;--tpl-heading-tracking:-.02em;--tpl-heading-weight:700;--tpl-section-space:4rem;--fs-display:clamp(1.75rem, 1.25rem + 2.2vw, 3rem);--fs-h2:clamp(1.3125rem, 1.15rem + .7vw, 1.75rem);--fs-h3:clamp(1.0625rem, 1rem + .3vw, 1.25rem);--fs-lead:clamp(.9375rem, .9rem + .22vw, 1.0625rem);--fs-body:clamp(.9375rem, .91rem + .15vw, 1rem);--fs-sm:clamp(.8125rem, .8rem + .1vw, .875rem);--fs-xs:clamp(.75rem, .74rem + .06vw, .78125rem);--section-space:clamp(2.25rem, 5vw, var(--tpl-section-space,3.5rem))}html{scroll-behavior:smooth;scroll-padding-top:5.5rem}body{background-color:var(--color-surface);background-image:var(--tpl-texture,none);color:var(--color-ink);font-family:var(--tpl-font-body,var(--font-sans));font-size:var(--fs-body);font-feature-settings:"tnum" 1;padding-bottom:env(safe-area-inset-bottom);line-height:1.7}:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px}.slider-viewport{scrollbar-width:none;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;overflow-x:auto}.slider-viewport::-webkit-scrollbar{display:none}.slider-viewport[data-slider=on]{scroll-snap-type:none;overflow-x:hidden}.slider-track{display:flex}.slider-track>*{scroll-snap-align:start}.slider-viewport[data-dragging=true]{cursor:grabbing;-webkit-user-select:none;user-select:none}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-scroll-snap-strictness{syntax:"*";inherits:false;initial-value:proximity}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-divide-x-reverse{syntax:"*";inherits:false;initial-value:0} diff --git a/solution/site/scripts/prerender.ts b/solution/site/scripts/prerender.ts index d356bad..8342b9c 100644 --- a/solution/site/scripts/prerender.ts +++ b/solution/site/scripts/prerender.ts @@ -76,6 +76,8 @@ const SITE_DIR = 's'; const HERE = dirname(fileURLToPath(import.meta.url)); const SITE_ROOT = resolve(HERE, '..', '..'); // dist/prerender → site/ +// 백엔드가 노래 파일을 떨구는 자리. payload 디렉토리와 나란히 둔다(한 디렉토리 약속). +const SONGS_DIR = join(SITE_ROOT, 'songs'); const CLIENT_DIR = join(SITE_ROOT, 'dist', 'client'); interface Args { @@ -285,6 +287,53 @@ function assetPlan(payload: SitePayload, outRoot: string, siteDir: string) { }; } +/** + * 이 숙소의 노래 파일을 사이트 디렉토리로 옮겨 놓는다. + * + * ★ 왜 백엔드가 직접 out/ 에 쓰지 않나 + * 백엔드는 발행물 디렉토리를 모른다 — payload JSON 을 약속된 자리에 떨구는 것이 경계다 + * (ARCHITECTURE 1절). 노래도 같은 약속을 쓴다: 백엔드는 `site/songs/<파일>` 에 두고, + * 굽는 쪽인 여기가 사이트 안으로 복사한다. 그래야 Azure 발행(`azure_static.publish`)이 + * 사이트 디렉토리를 통째로 올릴 때 노래도 함께 올라간다. + * + * ★ 없으면 조용히 넘어간다. 곡은 발행보다 2~3분 늦게 완성되므로 "아직 없음" 이 정상이고, + * 그때 payload 에 songs 가 비어 있어 화면도 플레이어를 안 그린다. + */ +function copySongs(payload: SitePayload, siteDir: string) { + const wanted = new Set(); + + for (const song of payload.songs ?? []) { + const name = song.fileName || song.audioUrl.split('/').pop(); + if (!name) continue; + wanted.add(name); + const from = join(SONGS_DIR, name); + if (!existsSync(from)) { + console.warn(` ! 노래 파일이 없습니다(건너뜀): ${from}`); + continue; + } + const to = join(siteDir, name); + rmSync(to, {force: true}); + copyFileSync(from, to); + } + + /* + * 지난 발행의 곡은 치운다. + * + * ★ 발행할 때마다 새 곡을 만들고 파일명은 song_id 라, 치우지 않으면 발행 횟수만큼 1MB 짜리 + * 파일이 사이트 디렉토리에 쌓인다. 그리고 그것들은 Azure 발행 때 **함께 올라간다** — + * 아무도 듣지 않는 옛 곡이 계속 쌓이는 종류의 낭비다. + * ★ 자산(out/assets)과 달리 보관 기간을 두지 않는다. 번들은 크롤러가 나중에 렌더할 때 + * 필요하지만(AGENTS.md), 노래는 그 페이지에서 버튼을 눌러야 나는 것이라 옛 HTML 이 + * 가리킬 일이 없다 — payload 에 실린 곡 하나만 남기면 된다. + */ + if (!existsSync(siteDir)) return; + for (const entry of readdirSync(siteDir, {withFileTypes: true})) { + if (!entry.isFile() || !entry.name.endsWith('.mp3')) continue; + if (wanted.has(entry.name)) continue; + rmSync(join(siteDir, entry.name), {force: true}); + } +} + function prerenderSite( input: SitePayload, outRoot: string, @@ -375,6 +424,8 @@ function prerenderSite( */ writeFile(siteDir, 'llms.txt', renderLlmsTxt(payload)); + copySongs(payload, siteDir); + // 하이드레이션용 번들. 공용 호스트면 out/ 루트 한 벌을 공유하므로 여기서는 아무것도 안 한다 // (main 이 사이트를 굽기 전에 한 번 깔아 둔다). 커스텀 도메인일 때만 사이트 안에 복사한다. if (!plan.shared) { diff --git a/solution/site/songs/.gitignore b/solution/site/songs/.gitignore new file mode 100644 index 0000000..f478042 --- /dev/null +++ b/solution/site/songs/.gitignore @@ -0,0 +1,4 @@ +# 생성된 노래 파일(mp3)은 커밋하지 않는다 — payload 와 같은 규칙이다. +# 발행할 때 백엔드가 다시 만들어 떨군다. +* +!.gitignore diff --git a/solution/site/src/fixtures/moonlight-stay.ts b/solution/site/src/fixtures/moonlight-stay.ts index 42225e2..6e0805d 100644 --- a/solution/site/src/fixtures/moonlight-stay.ts +++ b/solution/site/src/fixtures/moonlight-stay.ts @@ -505,6 +505,16 @@ export const MOONLIGHT_STAY_PAYLOAD: SitePayload = { }, ], + /** + * 노래는 비워 둔다. + * + * ★ 이 픽스처의 목적은 "payload 가 이러이러할 때 화면이 이렇게 나온다" 를 눈으로 보는 것이다. + * 노래는 발행 때 Suno 가 만들어 넣는 실제 파일을 가리키므로, 여기에 가짜 주소를 적으면 + * 개발 서버에서 **재생만 안 되는 버튼**이 생긴다. 비어 있을 때 플레이어가 아예 안 그려지는지 + * 확인하는 쪽이 이 픽스처가 할 일에 맞다. + */ + songs: [], + narrative: { heroHeadline: '바람과 돌담이 품은 고요', heroSubline: '제주 자연의 질감이 머무는 독채 스테이', diff --git a/solution/site/src/sections/SiteHeader.tsx b/solution/site/src/sections/SiteHeader.tsx index 06b945d..f9cbb03 100644 --- a/solution/site/src/sections/SiteHeader.tsx +++ b/solution/site/src/sections/SiteHeader.tsx @@ -1,5 +1,6 @@ import {Menu, Phone} from 'lucide-react'; import {useSite} from '@site/lib/site-context'; +import {SongPlayer} from '@site/sections/SongPlayer'; import {isSectionEnabled, stayBookingView, unitSpec} from '@site/lib/derive'; /** @@ -77,6 +78,9 @@ export function SiteHeader() {
    + {/* 이 숙소의 노래. 곡이 없으면 아무것도 그리지 않는다(SongPlayer 머리주석). */} + + {payload.place.phone && ( + + + + + + + + + + + + ); +} + +function PlayIcon() { + return ( + + + + ); +} + +function PauseIcon() { + return ( + + + + ); +} + +/** + * ★ 햄버거(줄 세 개)를 쓰지 않는다 — 바로 옆에 사이트 메뉴 단추가 같은 모양으로 서 있어 + * 헷갈린다는 지적이 있었다. 줄 끝에 음표를 달아 **재생 목록**으로 읽히게 한다. + */ +function ListIcon() { + return ( + + + + + + ); +} + +function CloseIcon() { + return ( + + + + ); +} + +function clock(sec: number | undefined): string { + if (typeof sec !== 'number' || !Number.isFinite(sec)) return ''; + const m = Math.floor(sec / 60); + const s = Math.floor(sec % 60); + return `${m}:${s < 10 ? '0' : ''}${s}`; +} + +export function SongPlayer() { + const payload = useSite(); + const songs: SongTrack[] = (payload.songs ?? []).filter((song) => song.audioUrl); + + const [cur, setCur] = useState(0); + const [playing, setPlaying] = useState(false); + const [open, setOpen] = useState(false); + const [time, setTime] = useState({now: 0, total: 0}); + const [panelStyle, setPanelStyle] = useState>({}); + + const audioRef = useRef(null); + const miniRef = useRef(null); + const panelRef = useRef(null); + + /** + * ★ 시작 곡만 무작위, 그다음은 순차다(목업과 같은 규칙). + * 들어올 때마다 같은 곡이면 배경음악이 하나인 것처럼 들리고, 매번 무작위로 뽑으면 + * "아까 그 곡" 을 다시 찾기 어렵다. + * ★ 무작위는 **마운트 후**에 고른다. 서버 렌더에서 뽑으면 HTML 과 하이드레이션 결과가 + * 달라져 리액트가 화면을 통째로 다시 그린다. + */ + const count = songs.length; + useEffect(() => { + if (count > 1) setCur(Math.floor(Math.random() * count)); + }, [count]); + + /** 패널은 fixed 다 — 헤더가 sticky 라 스크롤해도 자리가 같아야 한다. */ + const placePanel = useCallback(() => { + const header = document.querySelector('header'); + const box = header?.getBoundingClientRect(); + const next: Record = {top: `${box ? box.bottom + 6 : 66}px`}; + if (window.innerWidth <= 640) { + // 좁은 화면에서는 좌우 8px 만 남기고 펼친다 — 고정 폭이면 390px 화면에서 제목이 접힌다. + next.left = '8px'; + next.right = '8px'; + } else { + next.left = 'auto'; + const anchor = miniRef.current?.getBoundingClientRect(); + next.right = `${Math.max(12, window.innerWidth - (anchor?.right ?? 0))}px`; + } + setPanelStyle(next); + }, []); + + useEffect(() => { + if (!open) return undefined; + placePanel(); + + const onMove = () => placePanel(); + // 바깥을 누르면 닫는다. 패널과 헤더 묶음 안은 제외한다. + const onClick = (event: MouseEvent) => { + const target = event.target as Node; + if (panelRef.current?.contains(target) || miniRef.current?.contains(target)) return; + setOpen(false); + }; + + window.addEventListener('resize', onMove); + window.addEventListener('scroll', onMove, {passive: true}); + document.addEventListener('click', onClick); + return () => { + window.removeEventListener('resize', onMove); + window.removeEventListener('scroll', onMove); + document.removeEventListener('click', onClick); + }; + }, [open, placePanel]); + + // 페이지를 떠날 때 소리를 끊는다 — 리액트가 노드를 지워도 재생 중인 오디오는 계속 난다. + useEffect(() => () => audioRef.current?.pause(), []); + + if (songs.length === 0) return null; + + const song = songs[cur] ?? songs[0]; + + const play = (index = cur) => { + const audio = audioRef.current; + const target = songs[index]; + if (!audio || !target) return; + if (audio.getAttribute('src') !== target.audioUrl) audio.src = target.audioUrl; + // 자동재생 차단·파일 오류는 예외가 아니라 거절된 약속으로 온다. + void audio.play().then( + () => setPlaying(true), + () => setPlaying(false), + ); + }; + + const stop = () => { + // 자리는 남긴다 — 다시 누르면 이어 듣는다. + audioRef.current?.pause(); + setPlaying(false); + }; + + /** 목록에서 고른다. 고르면 그 자리에서 튼다. 끝나면 다음 곡으로 넘어가는 길도 여기다. */ + const load = (index: number) => { + const next = ((index % songs.length) + songs.length) % songs.length; + setCur(next); + setTime({now: 0, total: 0}); + play(next); + }; + + return ( + <> + + {/* 카세트는 단추가 아니라 표지다 — 이 자리가 무엇인지 한눈에 말해 준다. */} + + + + {/* 지금 나오는 곡명. 소리가 나는데 무슨 곡인지 화면에 없으면 목록을 열어 봐야 안다. */} + + {song.title} + + + + + + {/* ★ preload="none": 아무도 안 누를 수도 있는 파일을 모든 방문자에게 내려받게 하지 않는다. + ★ src 를 markup 에 박아 둔다 — 누를 때 비로소 넣으면 서버 렌더 결과에 오디오 주소가 + 한 글자도 없어, 크롤러에게 이 페이지는 소리 없는 페이지다. */} +