/s/stay 시연본에 오늘 쌓인 변경을 한꺼번에 반영한다. 원본 8곳이 전부 467m 안이라 '걸어서 10분 이상' 탭의 맛집 칸이 비었던 것, xlsx 대조로 사이트에 없던 축제(국가유산 야행)가 빠져 있던 것, 차로 온 손님도 걸어온 손님과 같은 동선(짐 맡기고 저녁에 복귀)을 따르던 것, 번들이 2026-09-09 판이라 숙박 예약 섹션이 아예 안 그려지던 것 — 전부 발견한 순서대로 고쳤다. - patch_stay.py: 날짜 값(UPDATED_AT) 한 곳으로 모음 · 날씨 문구 케이스당 1개→5개 무작위 · '오늘의 엽서' 탭 비움(엽서 쓰기 섹션으로 대체) · 주변 맛집 8→31곳 + 사진 없는 집에 시연 전용 사진 채움 · 군산 국가유산 야행 추가(날짜 미확정이라 기간 값은 안 넣음) · 오늘의 한 장 탭 숨김 · 예약 섹션 켬 · head 메타의 날짜도 같이 갱신 · 번들을 최신 빌드로 교체하고 `<body class="site">` 보정(빠지면 카로셀 overflow 가 죽어 가로 스크롤이 생겼다) · 군산 읽기/엽서 카로셀에 쓸 embla-carousel UMD 번들 도입 - build_itinerary.py: 차로 온 손님의 마지막 날은 복귀 칸 없이 마지막 정거장에서 끝나도록 동선을 가름(도보 뼈대에서 복귀 칸만 떼어 파생 — 두 벌로 안 둔다) - build_story.py: 진희경은 익산 출생·군산에서 통학 — "군산 출신"으로 뭉뚱그리지 않게 정정 - fetch_restaurants.py · fill_restaurant_photos.py · restaurants.json · restaurant-photos.json · img/mirror/*: TourAPI 로 받은 맛집 31곳과, 사진 없는 집에 채운 시연 전용 사진(공공누리 아님 — 제품으로는 안 옮긴다) - vendor/: embla-carousel.umd.js 추가, 번들을 index-C3fFNl3r.css·index-DDxwteyn.js 로 교체하고 옛 파일은 vendor/retired/ 로 보관 - audit-all.mjs · audit_schedule.py · SCHEDULE.md: 위 변경에 맞춘 점검 항목·산출물 갱신 - AUTOPLAY.md: 새로고침 직후 배경음악 자동재생을 시도한 것과 실측값 — 사람 입력 없이는 브라우저가 막아서 안 된다는 결론 기록 검증: `python3 patch_stay.py` 정상 종료, 킹서버 반영 후 md5 대조. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYUb8ZfNiDd15tZRYrusCX
192 lines
11 KiB
Python
192 lines
11 KiB
Python
"""구워진 payload 의 일정 21개를 표로 뽑고, 규칙 17종이 **안 보는** 논리까지 검수한다.
|
||
|
||
★ 왜 따로 있나 — 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*</script>", 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 시작·끝이 숙소인가
|
||
# ★ 떠나는 날은 갈린다 (2026-09-14 대표 의견). 차로 온 손님은 퇴실할 때 짐을 싣고
|
||
# 나서므로 복귀 칸이 없다 — 그 하루는 마지막 정거장에서 끝나는 것이 맞다.
|
||
# 판별은 **퇴실 칸의 문구**로 한다(payload 에 남는 값이라 표와 화면이 같은 것을 본다).
|
||
car_out = is_last and "차에 싣고" in (day["stops"][0].get("note") or "")
|
||
if rows[0]["place"] != STAY:
|
||
bad.append(("L8", f"{label}: 첫 칸이 {rows[0]['place']}"))
|
||
if rows[-1]["place"] != STAY and not car_out:
|
||
bad.append(("L8", f"{label}: 마지막 칸이 {rows[-1]['place']}"))
|
||
# L12 떠나는 날의 끝맺음이 퇴실 문구와 같은 말을 하는가
|
||
if car_out and rows[-1]["place"] == STAY:
|
||
bad.append(("L12", f"{label}: 짐을 차에 싣고 나섰는데 숙소로 돌아온다"))
|
||
if is_last and not car_out and rows[-1]["slot"] != "마무리":
|
||
bad.append(("L12", f"{label}: 짐을 맡겨 두고 나섰는데 찾으러 돌아오지 않는다"))
|
||
# 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 를 렌더러와 같은 방식으로 계산한 값**이다(화면에 뜨는 것과 같다).",
|
||
"규칙 17종(만들 때 거는 것)은 `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분 넘는 이동이 없다 |",
|
||
"| L12 | 떠나는 날의 끝맺음이 퇴실 문구와 같다 — 짐을 차에 실었으면 안 돌아오고, 맡겼으면 찾으러 온다 |", ""]
|
||
|
||
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 += ["**전부 통과.** 13개 항목 위반 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}")
|