o2o-site-AEO/solution/site/scripts/mockup/AUTOPLAY.md
Mina Choi 0da9b92423 [feat] site/mockup: 시연본 갱신 — 주변 맛집 31곳·국가유산 야행·차 동선 분기·번들 최신화
/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
2026-09-14 17:07:01 +09:00

147 lines
7.5 KiB
Markdown

# 자동재생 — 안 된다. 왜 안 되는지와, 그래서 뭘 했는지
`/s/stay` 배경음악을 **새로고침 직후 바로** 나게 하려고 시도한 것 전부와 실측값이다.
같은 얘기가 다시 나오면 이 파일부터 본다.
측정: 2026-09-14 · 킹서버 `https://web4ai.o2osolution.ai/s/stay` · Chrome(정식 채널)
---
## 결론
**브라우저에서 첫 방문에 소리가 저절로 나게 하는 방법은 없다.**
사람 손 입력(`isTrusted: true`) 없이는 크롬이 `play()` 를 `NotAllowedError` 로 거절한다.
자바스크립트로 그 검사를 통과시키는 길은 없고, 그건 우회 대상이 아니라
**그 우회를 막으려고 만들어진 검사**다.
지금 동작: **손님이 화면에 처음 닿으면(클릭·탭·키) 켜진다.** 그 이상은 어떤 사이트도 못 한다.
### 해 본 것과 결과
| 시도 | 결과 |
|---|---|
| 로드 즉시 `audio.play()` | `NotAllowedError` |
| 무음(`muted=true`)으로 시작 → 나중에 unmute | `NotAllowedError` (무음도 거절된다) |
| `<audio muted playsinline>` 태그로 두고 `play()` | `NotAllowedError` |
| 가짜 이벤트 강제 발생 — `dispatchEvent` 로 `pointerdown`·`mousedown`·`pointerup`·`mouseup`·`click`·`keydown` 6종 | `navigator.userActivation` **false 그대로**, 거절 |
| 재생 단추를 코드로 누르기 — `btn.click()`, 0.7초 간격 3회 | `play=0` |
| `--app=URL` 앱 모드로 띄우기 | `play=0` |
| **Web Audio API** — `new AudioContext()` | `state: "suspended"` |
| `ctx.resume()` | **약속이 영원히 안 돌아온다** (제스처를 기다리며 매달린다) |
| mp3 를 `decodeAudioData` 로 풀어 `BufferSource` 재생 | `currentTime` **0.000 → 0.000**, 안 돈다 |
| **`speechSynthesis.speak()`** | `error: not-allowed` |
| 마우스만 움직임 / 스크롤만 | `play=0` (`act=false`) |
| **진짜 클릭·탭·키 입력 한 번** | **켜진다** (`act=true`, `play=1`) |
`dispatchEvent` 와 `el.click()` 이 만든 이벤트는 `isTrusted: false` 로 찍혀 나간다.
크롬의 사용자 활성화 카운터(`navigator.userActivation`)는 `isTrusted: true` 인 입력만 세고,
그게 올라야 소리가 풀린다.
브라우저에서 **소리를 낼 수 있는 API 는 셋뿐이고 셋 다 같은 게이트 뒤에 있다** —
HTMLMediaElement(`<audio>`) · Web Audio(`AudioContext`) · speechSynthesis. 우회로가 없는 이유다.
**활성화를 준다**: `mousedown` · `pointerdown` · `pointerup` · `touchend` · `keydown`(Esc 제외) · `click`
**안 준다**: `mousemove` · `pointermove` · `touchmove` · `wheel` · `scroll`
---
## 안 한 것과 그 이유
크롬 문서(developer.chrome.com/blog/autoplay)에 예외가 넷 있지만 **전부 손님 쪽 설정이라
제품에서는 쓸 수 없다.** 한 번 만들었다가 되돌렸다.
| 예외 | 왜 안 쓰나 |
|---|---|
| PWA 설치본 | **펜션 사이트 보러 온 손님이 앱을 설치할 리 없다.** 매니페스트까지 붙였다가 걷어냈다 |
| MEI (같은 출처에서 7초 이상 여러 번) | 손님이 쌓는 것이라 첫 방문에는 없다 |
| `--autoplay-policy=no-user-gesture-required` | 브라우저를 그 플래그로 띄워야 한다 |
| 기업정책 `AutoplayAllowlist` | 기계 관리자가 넣는 것이다 |
★ **강제 클릭 코드도 뺐다.** 넣어서 배포까지 해 봤지만 기본 크롬에서 `play=0` 이고,
동작하지 않는 코드가 남으면 다음 사람이 "이미 처리돼 있네" 로 읽는다.
---
## 지금 코드가 하는 일 (`inject.js` `startPlayer`)
1. 로드 즉시 `play()` 를 건다. (거절되지만, 자동재생이 허락된 환경이면 여기서 켜진다)
2. 거절되면 **첫 사람 손길**을 기다린다. 창 전체에 걸고, 플레이어 자기 단추만 빼고
어디를 눌러도 걸린다.
- STRONG(활성화를 주는 입력): 스로틀 없이 매번 시도
- WEAK(안 주는 입력): 1초에 한 번만 — 초당 수십 번 오는 mousemove 로 단추가 깜빡이는 것을 막는다
3. 탭이 다시 앞으로 나올 때(`visibilitychange`) 한 번 더 건다.
### 여기서 밟았던 함정 둘
- **가짜 pointerdown 이 대기를 태웠다.** `tameRails()` 가 레일 자동 넘김을 세우려고 레일마다
`pointerdown` 을 쏘는데(정상 동작), 그게 첫 손길 대기를 먼저 먹었다. 그 뒤로는 손님이
아무 데나 눌러도 안 켜졌다. → `onFirstTouch` 가 `event.isTrusted` 를 먼저 본다.
- **스로틀이 클릭을 걸렀다.** 입력을 안 가리고 1초 스로틀을 걸었더니 클릭 직전의 mousemove 가
그 1초를 먹어 **정작 활성화를 주는 mousedown 이 걸러졌다**(실측: `act=true` 인데 `play=0`).
→ STRONG/WEAK 로 갈라 STRONG 에는 스로틀을 안 건다.
---
## "트리플픽은 되던데?" — 실측 결과
**트리플픽도 안 된다. 똑같이 거절당한다.**
`https://triplepick.o2o.kr/match/MLB_BAL_TOR_20260914?song=BAL` (공유 링크 = 자동 로드 경로)에
깨끗한 크롬으로 들어가 `HTMLMediaElement.prototype.play` 를 가로채 찍은 값이다.
```
[T] play() 거절 NotAllowedError .kr/api/songs/audio/815/0?v=89a0fcd6
```
코드도 우리와 같은 한 줄이다 — `usePlayer.tsx:133` `if (shouldPlay) audio.play().catch(() => {})`.
게스처 감지 코드는 0줄이고, 평소 재생 경로는 `MatchupHUD.tsx:55` 의 **「♪ 응원가 듣기」 버튼**이다.
`playlist.ts:19` 의 `PATH_PLAYLISTS` 는 비어 있어서 경로 진입 자동 로드는 실을 곡도 없다.
### 그럼 왜 내 브라우저에서는 나나 — MEI
크롬은 **같은 출처에서 소리를 여러 번 들은 사람에게만** 그 출처의 자동재생을 풀어 준다
(Media Engagement Index). 조건: 7초 초과 재생 · 음소거 아님 · 탭 활성 · (영상이면) 200x140px 초과.
트리플픽을 자주 쓰면서 응원가를 여러 번 들었으면 그 브라우저에는 이미 허락이 쌓여 있다.
**그 브라우저에서만** 자동재생된다. 손님의 새 브라우저에서는 안 된다.
확인: `chrome://media-engagement/` 에서 출처별 점수와 `Has high score` 를 본다.
★ **같은 일이 이 사이트에도 일어난다.** 목업을 자주 열고 음악을 들으면 그 브라우저는
`web4ai.o2osolution.ai` 에 허락을 쌓고, 그 뒤로는 새로고침해도 바로 난다.
**대표님 노트북에서 시연하면 그 상태가 된다** — 다만 손님 브라우저는 아니다.
---
## 재보고 싶을 때 — 측정 주의
★ **Playwright 로 잴 때 `page.evaluate` 를 쓰면 안 된다.** CDP 가 `userGesture: true` 로 넣어서
**상태를 읽으려고 부른 그 호출이 브라우저를 "조작됨" 으로 만든다.** 그 뒤 측정은 전부 거짓
양성이다 — 실제로 이것 때문에 "마우스만 움직여도 켜진다" 는 틀린 결론을 한 번 냈다.
상태는 `addInitScript` 로 심고 `console` 로만 받는다.
```js
await page.addInitScript(() => {
setInterval(() => {
const el = document.getElementById('w4d-play');
console.log('[C] play=' + (el ? el.dataset.playing : '-')
+ ' act=' + navigator.userActivation.isActive);
}, 1000);
});
page.on('console', (m) => { /* 여기서만 읽는다 */ });
```
---
## 지금 무엇을 보고 있는지 가리는 법
`/s/` 는 `max-age=300, must-revalidate` 다. "고쳤는데 안 된다" 의 상당수가 **캐시된 옛 파일**이었다.
콘솔 첫 줄에 빌드 시각이 찍힌다.
```
[w4d] 주입분 로드 · 빌드 2026-09-14T07:12:00+00:00
```
이 시각이 방금 구운 것과 다르면 옛 파일을 보고 있는 것이다. ⌘⇧R.