"""/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
GENERAL = json.loads((SP.parents[1] / "src/lib/lodging-catchphrases.json").read_text(encoding="utf-8"))
items = [{"text": text, "kind": "general"} for text in GENERAL]
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("", "<\\/")
html, n = re.subn(r"",
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*