o2o-site-AEO/solution/site/scripts/mockup/rails-focus.mjs
Mina Choi 43ba33dd65 [feat] site/scripts: /s/stay 시연본 조립 도구 일습 — 일정 생성기·주입분·검증
이 목업은 payload 가 없어 프리렌더 재굽기 대상이 아니다 — `index.html` 한 장이 유일본이고
자산이 지워지면 사람이 되돌려 넣어야 한다(CLAUDE.md 의 ★★ 함정). 지금까지 그 한 장을
이 맥에서만 만들고 있었다. 다른 사람이 이어받을 수 있도록 재료를 전부 올린다.
`build/index.html` 은 생성물이라 이그노어 그대로다 — `patch_stay.py` 로 다시 나온다.

- build_itinerary.py: 테마 21개 일정 생성. 좌표에서 이동시간을 계산하고(도보 4km/h,
  1.5km 초과는 차 25km/h + 주차 5분) 입·퇴실·끼니 창을 맞춘다. 규칙 14종 감사가
  **빌드 안**에 있어 하나라도 어기면 payload 를 쓰지 않고 멈춘다 — 검사가 빌드 밖에
  있던 동안 뼈대를 고칠 때마다 안 보는 규칙이 생겼다(2026-09-11 REVIEW)
- build_story.py: 노래 25곡·인물 57명. 사진은 위키백과 문서 pageimages 만 믿는다
  (이름 검색은 동명이인을 끌고 온다 — 이수현→걸그룹, 박성현→골퍼)
- patch_stay.py: 캐치프레이즈 100개·자작곡 5곡·객실 사진(A동 12/B동 10)을 넣고
  payload 를 갈아 끼운 뒤 inject.css/js 를 `</body>` 앞에 주입해 index.html 을 짠다
- inject.js/css: React 가 다시 그려도 살아남아야 하는 다섯 가지(캐치프레이즈 순환·
  헤더 미니 플레이어·지도 링크·사진 저작자 표시·카로셀 제어). `#root` 밖에 둔다
- audit-all.mjs / rails-test.mjs: 실제 브라우저로 34종 검사, 레일 13개 자동 넘김 전수
- README.md: 이어받는 사람이 먼저 읽는 문서. 배포·함정·§7 "내가 틀렸던 6가지"
- PROMPTS.md / TEXT.md / REVIEW-2026-09-11.md: 문구 생성 프롬프트 · 전체 텍스트 · 검수 보고

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 10:28:52 +09:00

45 lines
2.2 KiB
JavaScript

import {chromium} from 'playwright';
const b = await chromium.launch({channel: 'chrome'});
const page = await b.newPage({viewport: {width: 1280, height: 900}});
await page.goto('http://localhost/s/stay', {waitUntil: 'domcontentloaded'});
await page.waitForTimeout(6000);
await 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.__st = (i) => { const r = window.__rails[i]; const c = r.firstElementChild; let m = 0;
try { m = new DOMMatrix(getComputedStyle(c).transform).m41; } catch {}
const bb = r.getBoundingClientRect();
return {pos: Math.round(r.scrollLeft + m), max: Math.round(r.scrollWidth - r.clientWidth),
label: r.getAttribute('aria-label') || '?', cx: Math.round(bb.left + bb.width/2),
cy: Math.round(bb.top + bb.height/2), top: Math.round(bb.top), h: Math.round(bb.height), vh: innerHeight};
};
});
for (const i of [4, 12]) {
await page.mouse.move(5, 5);
await page.evaluate((k) => window.__rails[k].scrollIntoView({block: 'center'}), i);
await page.waitForTimeout(9000);
let s = await page.evaluate((k) => window.__st(k), i);
console.log(`\n[${s.label}] max=${s.max} pos=${s.pos} 화면 top=${s.top} h=${s.h} vh=${s.vh}`);
await page.waitForTimeout(6000); // 진행 중인 애니메이션까지 가라앉힌다
s = await page.evaluate((k) => window.__st(k), i);
const y = Math.min(Math.max(s.cy, 100), 800);
const p1 = s.pos;
await page.mouse.move(s.cx, y);
await page.mouse.wheel(0, 150);
await page.mouse.move(5, 5);
const marks = [];
for (let t = 1; t <= 14; t++) {
await page.waitForTimeout(1000);
const q = await page.evaluate((k) => window.__st(k), i);
marks.push(`${t}s:${q.pos - p1}`);
}
const f = await page.evaluate((k) => window.__st(k), i);
console.log('휠 뒤 14초:', marks.join(' '), `| 끝까지=${f.max} 현재=${f.pos} 화면top=${f.top}`);
}
await b.close();