"""B08 거리이정표 경로 그림 자동 캡처(PLAN 244-11 · 사용자 139번) — 서버 헤드리스 Edge. [등록] · [길찾기] 뒤 화면이 그 경로의 길 선(마지막 길찾기 경로 좌표) · 표지를 보냄 → 서버가 헤드리스 Edge(Playwright channel msedge · 없으면 chrome · 번들 chromium 안 씀)로 카카오 지도 JS + 길 선 + 표지 + 경로 맞춤 → tilesloaded 뒤 1200×800 PNG → `<폴더>/<경로 id>.png` (사용자 그림이 있으면 화면은 사용자 그림 우선). 페이지 주소 = .env KAKAO_CAPTURE_ORIGIN(없으면 http://localhost:5173) + /__route_capture — page.route 로 가로채 채움(카카오 JS 키 도메인 검사만 통과 · vite 안 떠도 됨). 경로 좌표는 그림 만든 뒤 버림(저장 = 그림 · km) · 동시 요청 = 하나씩(잠금) · 실패 = 까닭 한 줄 · 키 값은 돌려주지도 적지도 않음. 캡처는 자식 파이썬 프로세스(이 파일을 스크립트로 · 입력 = JSON stdin · 출력 = 결과 JSON 한 줄 · 키는 환경 그대로) — 서버(uvicorn · Windows Selector 루프) 안 스레드에선 Playwright 가 드라이버 하위 프로세스를 못 만듦(249-1). 장소 그림(256-1) = 같은 길 · 작은 지도(480×320 · 표지 하나 · 주소 둘레 줌) → `place/<상자 id>.png`. """ from __future__ import annotations import json import os import subprocess import sys import threading import time import urllib.parse from pathlib import Path from typing import Any KAKAO_JS = "KAKAO_JS_KEY" ORIGIN_KEY = "KAKAO_CAPTURE_ORIGIN" DEFAULT_ORIGIN = "http://localhost:5173" PAGE = "/__route_capture" WIDTH, HEIGHT = 1200, 800 TIMEOUT_S = 20 CHILD_TIMEOUT_S = 25 # 자식 프로세스 전체(파이썬 · 드라이버 기동 포함) CHILD = __file__ # 자식으로 돌릴 스크립트 NO_BROWSER = "서버에서 브라우저(Edge · Chrome)를 못 띄움" CHANNELS = ("msedge", "chrome") # 시스템 브라우저 — 번들 chromium 은 안 받아 둠 LINE_COLOR = "#E60046" # 화면 지도(`…_UI_Milestone_KakaoMap`) 길 선과 같은 색 TAIL = " — [그림 변경]으로 넣으세요." PLACE_VIEW = {"w": 480, "h": 320, "level": 4} # 장소 그림(256-1) — 표지 가운데 · 고정 줌 _lock = threading.Lock() # ponytail: 서버 전체 한 줄 — 브라우저 한 번에 하나(2~3초) class CaptureError(Exception): """까닭 한 줄(키 · 예외 원문 없음).""" def page_html( key: str, lines: list[Any], marks: list[dict[str, Any]], view: dict[str, int] | None = None ) -> str: """카카오 지도 + 길 선 + 표지 + 경로 맞춤 · 다 그림 = window.__done · SDK 못 받음 = __fail. view = 크기 · 줌 고정(장소 그림 · 첫 표지 가운데 · 경로 맞춤 안 함).""" w, h = (view["w"], view["h"]) if view else (WIDTH, HEIGHT) level = view["level"] if view else 7 data = json.dumps({"lines": lines, "marks": marks}, ensure_ascii=False).replace("<", "\\u003c") sdk = f"https://dapi.kakao.com/v2/maps/sdk.js?appkey={urllib.parse.quote(key)}&autoload=false" return f"""
""" def shoot(html: str, origin: str) -> bytes: """헤드리스 브라우저로 그 페이지 → 지도 칸 PNG. 실패 = CaptureError(까닭 한 줄).""" try: from playwright.sync_api import Error as PwError from playwright.sync_api import TimeoutError as PwTimeout from playwright.sync_api import sync_playwright except ImportError as error: raise CaptureError("서버에 Playwright 가 없습니다(pip install playwright)") from error url = origin + PAGE deadline = time.monotonic() + TIMEOUT_S def left() -> float: return max(1.0, deadline - time.monotonic()) * 1000 try: with sync_playwright() as p: browser = None for channel in CHANNELS: try: browser = p.chromium.launch(channel=channel, headless=True, timeout=left()) break except PwError: continue if browser is None: raise CaptureError(NO_BROWSER) try: page = browser.new_page(viewport={"width": WIDTH, "height": HEIGHT}) page.route( url, lambda route: route.fulfill( status=200, content_type="text/html; charset=utf-8", body=html ), ) page.goto(url, timeout=left()) page.wait_for_function("window.__done || window.__fail", timeout=left()) if page.evaluate("window.__fail"): raise CaptureError( f"카카오 지도 JS 가 캡처 주소({origin})를 받지 않음(키 도메인 등록 확인)" ) page.wait_for_timeout(500) # 마지막 타일 · 표지 그림 return page.locator("#m").screenshot(timeout=left()) finally: browser.close() except CaptureError: raise except PwTimeout as error: raise CaptureError(f"지도 그림이 {TIMEOUT_S}초 안에 안 끝남") from error except Exception as error: # 드라이버 못 뜸 등 — 예외 글은 내지 않음 raise CaptureError(NO_BROWSER) from error def capture(job: dict[str, Any]) -> dict[str, Any]: """자식 프로세스 몸통 — 그림 찍어 `