"""/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("", "<\\/")
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*