- vendor/ 옛 해시 번들을 retired/ 로 옮기고 새 해시로 교체 - build_photo6.py 신설 — /s/stay6 "2안(사진)" 판, 슬러그별로 다시 구울 수 있다 (python3 build_photo6.py <slug>) - build_pension.py·build_reading.py·build_stay6.py·patch_stay.py 정리 - AUTOPLAY.md·README.md·audit-all.mjs 갱신
215 lines
9.1 KiB
Python
215 lines
9.1 KiB
Python
"""/s/stay 를 세 페이지로 나눈 판을 /s/stay6 으로 굽는다.
|
||
|
||
stay3·stay4 는 HTML5 UP 템플릿으로 새로 만든 것이라 렌더러와 무관했다. stay6 은 반대다 —
|
||
**/s/stay 와 같은 리액트 렌더러·같은 payload** 를 쓰고, `theme.sections[].enabled` 만
|
||
페이지별로 갈라 SSR 을 세 번 돌린다. 그래서 캐러셀·미니 플레이어·엽서 캔버스·후기 모달이
|
||
전부 /s/stay 와 같은 것으로 선다.
|
||
|
||
★ 렌더러(solution/site/src)는 건드리지 않는다. 페이지마다 다른 것은 payload 의 enabled 와
|
||
주입하는 css·js 뿐이다.
|
||
★ 후기(ReviewSection)·엽서(PostcardMakerSection)는 HomePage 가 섹션 목록과 무관하게
|
||
항상 그린다. 페이지를 나누려면 렌더러를 고쳐야 하는데 그건 /s/stay 를 같이 바꾸는 일이라,
|
||
여기서는 주입 css 로 감춘다 — DOM 에는 남는다.
|
||
★ 히어로도 항상 나온다. 페이지 머리로 쓴다.
|
||
|
||
입력: build/index.html (patch_stay.py 결과 — 껍데기·head·주입분) · stay-payload-new.json
|
||
출력: build6/{index.html, gunsan/index.html, booking/index.html}
|
||
"""
|
||
import json
|
||
import re
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
SP = Path(__file__).parent
|
||
SITE = SP.parents[1]
|
||
OUT = SP / "build6"
|
||
SSR = SITE / "dist" / "ssr6" / "ssr6.js"
|
||
SHELL = SP / "build" / "index.html"
|
||
PAYLOAD = SP / "build" / "index.html"
|
||
|
||
BASE = "/s/stay6"
|
||
|
||
PAGES = [
|
||
("", "소개", {"hero", "intro", "rooms", "video", "photos", "map", "weather"},
|
||
{"reviews"}),
|
||
# ★ map 을 끄면 HomePage 가 '오시는 길' 을 기본으로 한 번 더 낸다(HomePage.tsx:118).
|
||
# 군산 페이지에는 필요 없어서 주입 css 로 감춘다.
|
||
("gunsan", "군산", {"hero", "festival", "local", "itinerary", "story",
|
||
"songs", "people", "chronicle", "reading"},
|
||
{"reviews", "postcard-maker", "location", "hero"}),
|
||
("booking", "예약·후기", {"hero", "info", "booking", "faq", "rules", "map"},
|
||
{"postcard-maker", "hero"}),
|
||
]
|
||
|
||
# 화면 앵커 id → 그 섹션이 사는 페이지.
|
||
# ★ 페이지를 나누면 헤더 nav 의 `#festival` 같은 앵커가 **그 페이지에 없는 자리**를 가리킨다.
|
||
# 바를 하나 더 얹지 않고, 사이트가 이미 가진 nav 의 href 만 페이지 주소로 고친다.
|
||
ANCHOR_PAGE = {
|
||
"top": "", "about": "", "units": "", "video": "", "gallery": "",
|
||
"location": "", "weather": "", "postcard-maker": "",
|
||
"festival": "gunsan", "guide": "gunsan", "itinerary": "gunsan",
|
||
"story": "gunsan", "songs": "gunsan",
|
||
"info": "booking", "booking": "booking", "faq": "booking", "reviews": "booking",
|
||
}
|
||
|
||
# 하위 페이지의 머리.
|
||
# ★ 실물 대조(2026-09-18, 星のや京都 /dining/ · 모바일 390×844): 하위 페이지에는 히어로가
|
||
# 없다. 얇은 헤더 → 여백 → **페이지 이름 한 줄(가운데·명조)** → 바로 내용이다.
|
||
# 사진도 캐치프레이즈도 CTA 도 없다. 홈 히어로를 하위 페이지가 되풀이하는 판은 없었다.
|
||
NAV_CSS = """
|
||
[data-w4d-here] { font-weight: 800; text-decoration: underline; text-underline-offset: 5px; }
|
||
.w4d-pagehead {
|
||
padding: clamp(2.5rem, 9vw, 4.5rem) 1.25rem clamp(1.75rem, 6vw, 3rem);
|
||
text-align: center;
|
||
}
|
||
.w4d-pagehead h1 {
|
||
margin: 0; font-family: var(--font-serif, serif); font-weight: 400;
|
||
font-size: clamp(1.75rem, 7vw, 2.4rem); letter-spacing: .04em; line-height: 1.3;
|
||
}
|
||
"""
|
||
|
||
# ★ HTML 을 고쳐도 소용없다 — 리액트가 하이드레이션하면서 헤더를 다시 그려 덮는다(실측).
|
||
# 그래서 런타임에 고친다. 다시 그려도 되도록 클릭을 가로채는 쪽이 본체이고,
|
||
# href 를 바꾸는 것은 마우스 올렸을 때 주소가 맞게 보이라고 덤으로 한다.
|
||
NAV_JS = """
|
||
<script>
|
||
(function () {
|
||
var BASE = %(base)s, HERE = %(here)s, PAGE = %(page)s;
|
||
function target(id) {
|
||
var page = PAGE[id];
|
||
if (page === undefined || page === HERE) return null;
|
||
return page ? BASE + '/' + page + '#' + id : BASE + '#' + id;
|
||
}
|
||
function fix() {
|
||
var anchors = document.querySelectorAll('a[href^="#"]');
|
||
for (var i = 0; i < anchors.length; i++) {
|
||
var id = anchors[i].getAttribute('href').slice(1);
|
||
var to = target(id);
|
||
if (to) anchors[i].setAttribute('href', to);
|
||
else if (PAGE[id] === HERE) anchors[i].setAttribute('data-w4d-here', '');
|
||
}
|
||
}
|
||
document.addEventListener('click', function (event) {
|
||
var a = event.target.closest && event.target.closest('a[href^="#"]');
|
||
if (!a) return;
|
||
var to = target(a.getAttribute('href').slice(1));
|
||
if (!to) return;
|
||
event.preventDefault();
|
||
location.href = to;
|
||
}, true);
|
||
var TITLE = %(title)s;
|
||
function head() {
|
||
if (!TITLE) return;
|
||
var header = document.querySelector('#root header');
|
||
if (!header || document.querySelector('.w4d-pagehead')) return;
|
||
var band = document.createElement('div');
|
||
band.className = 'w4d-pagehead';
|
||
var h1 = document.createElement('h1');
|
||
h1.textContent = TITLE;
|
||
band.appendChild(h1);
|
||
header.parentNode.insertBefore(band, header.nextSibling);
|
||
}
|
||
function tick() { fix(); head(); }
|
||
[0, 400, 1200, 3000].forEach(function (ms) { setTimeout(tick, ms); });
|
||
window.addEventListener('pageshow', tick);
|
||
if (window.MutationObserver) {
|
||
new MutationObserver(tick).observe(document.getElementById('root'), {childList: true, subtree: false});
|
||
}
|
||
})();
|
||
</script>
|
||
"""
|
||
|
||
# 하위 페이지는 히어로를 쓰지 않는다(위 실물 대조). 렌더러가 무조건 그리므로 여기서 감춘다.
|
||
HIDE_CSS = {
|
||
"hero": "#top { display: none !important; }",
|
||
"reviews": "#reviews { display: none !important; }",
|
||
"postcard-maker": "#postcard-maker { display: none !important; }",
|
||
"location": "#location { display: none !important; }",
|
||
}
|
||
|
||
|
||
def shell_parts():
|
||
html = SHELL.read_text(encoding="utf-8")
|
||
open_tag = '<div id="root">'
|
||
start = html.index(open_tag) + len(open_tag)
|
||
end = html.index('<script>window.__SITE_PAYLOAD__')
|
||
close = html.rindex("</div>", start, end)
|
||
payload_end = html.index("</script>", end) + len("</script>")
|
||
return html[:start], html[close:end], html[payload_end:], html
|
||
|
||
|
||
def baked_payload():
|
||
"""굽힌 /s/stay 에서 payload 를 그대로 떠 온다 — stay-payload-new.json 은 patch_stay 중간
|
||
스냅샷이라 booking 켜기·탭 비우기·사진 주소 교체가 아직 안 들어가 있다."""
|
||
html = PAYLOAD.read_text(encoding="utf-8")
|
||
start = html.index("window.__SITE_PAYLOAD__=") + len("window.__SITE_PAYLOAD__=")
|
||
end = html.index("</script>", start)
|
||
return json.loads(html[start:end].rstrip().rstrip(";"))
|
||
|
||
|
||
def variant(payload, keep):
|
||
"""stay 가 켜 둔 섹션 중 이 페이지 몫만 남긴다. 꺼져 있던 것을 켜지 않는다 —
|
||
'군산 이야기' 안의 탭(가요·인물·시간의 골목·읽기)이 따로 서 버린다."""
|
||
out = json.loads(json.dumps(payload))
|
||
for section in out["theme"]["sections"]:
|
||
section["enabled"] = bool(section.get("enabled")) and section["id"] in keep
|
||
return out
|
||
|
||
|
||
PAGE_TITLE = {"": None, "gunsan": "군산", "booking": "이용안내 및 예약"}
|
||
|
||
|
||
def nav_js(current):
|
||
return NAV_JS % {
|
||
"base": json.dumps(BASE),
|
||
"here": json.dumps(current),
|
||
"page": json.dumps(ANCHOR_PAGE, ensure_ascii=False),
|
||
"title": json.dumps(PAGE_TITLE[current], ensure_ascii=False),
|
||
}
|
||
|
||
|
||
def main():
|
||
if not SSR.exists():
|
||
sys.exit(f"SSR 번들이 없다: {SSR}\n cd {SITE} && npx vite build --ssr scripts/mockup/ssr6.ts --outDir dist/ssr6")
|
||
if not SHELL.exists():
|
||
sys.exit(f"껍데기가 없다: {SHELL} — 먼저 patch_stay.py 를 돌린다")
|
||
|
||
base_payload = baked_payload()
|
||
head, tail_root, tail, whole = shell_parts()
|
||
|
||
if OUT.exists():
|
||
shutil.rmtree(OUT)
|
||
OUT.mkdir(parents=True)
|
||
|
||
for slug, _label, keep, hide in PAGES:
|
||
page_payload = variant(base_payload, keep)
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
src = Path(tmp) / "p.json"
|
||
dst = Path(tmp) / "p.html"
|
||
src.write_text(json.dumps(page_payload, ensure_ascii=False), encoding="utf-8")
|
||
subprocess.run(["node", str(SSR), str(src), str(dst)], check=True)
|
||
app = dst.read_text(encoding="utf-8")
|
||
|
||
payload_js = f"<script>window.__SITE_PAYLOAD__={json.dumps(page_payload, ensure_ascii=False)}</script>"
|
||
html = head + app + tail_root + payload_js + tail
|
||
html = html.replace("</body>", nav_js(slug) + "</body>", 1)
|
||
|
||
extra = NAV_CSS + "".join(HIDE_CSS[key] for key in sorted(hide))
|
||
html = html.replace("</head>", f"<style>{extra}</style></head>", 1)
|
||
|
||
html = html.replace("/s/stay/", f"{BASE}/")
|
||
html = re.sub(r'(<link rel="canonical" href="[^"]*?)/s/stay"',
|
||
rf'\1{BASE}/{slug}"' if slug else rf'\1{BASE}"', html)
|
||
|
||
target = (OUT / slug / "index.html") if slug else (OUT / "index.html")
|
||
target.parent.mkdir(parents=True, exist_ok=True)
|
||
target.write_text(html, encoding="utf-8")
|
||
print(f"{target.relative_to(OUT)} {target.stat().st_size:,}자 섹션 {len(keep)}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|