From 2fc3f931636340686a39b80316abfb6ca4ff1ec3 Mon Sep 17 00:00:00 2001 From: Haewon Kam Date: Wed, 9 Sep 2026 14:01:16 +0900 Subject: [PATCH] =?UTF-8?q?fix(supporters):=20=EB=8F=99=EC=84=A0=20?= =?UTF-8?q?=EC=A7=80=EB=8F=84=EB=A5=BC=20=EC=8B=A4=EC=A0=9C=20OpenStreetMa?= =?UTF-8?q?p=20=ED=83=80=EC=9D=BC=EB=A1=9C=20=EA=B5=90=EC=B2=B4=20+=20?= =?UTF-8?q?=ED=95=AB=EB=A7=81=ED=81=AC=20=EC=B0=A8=EB=8B=A8=20=EB=8C=80?= =?UTF-8?q?=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 지도가 회색 바탕에 점만 찍혀 있어 어디인지 알 수 없었다. OSM 타일을 좌표로 직접 깔고 그 위에 핀을 얹었다. 지도 라이브러리는 부르지 않는다. - Web Mercator 로 lat/lng → 월드 픽셀을 계산하고, 모든 지점이 여백 안에 들어오는 가장 확대된 배율을 고른다(뷰성형외과 동선은 z=15). - 타일은 img 가 아니라 CSS 배경으로 둔다. 장식 요소이고, 지도의 뜻은 감싸는 요소의 aria-label 이 전한다. 스크린리더가 타일 조각을 하나씩 읽지 않는다. - OSM 정책에 따라 "© OpenStreetMap contributors" 를 표기한다. 사진 핫링크 - 모든 사진에 referrerpolicy="no-referrer" 를 넣었다. 네이버 썸네일 CDN 이 리퍼러를 보고 403 을 준다. 서버에서 검사할 때는 리퍼러가 없어 통과해 놓치기 쉽다. - 사진 alt 를 채웠다. npm run build 의 접근성 게이트가 빈 alt 를 오류로 잡는다. 로컬에서 astro build 만 돌려 게이트를 건너뛴 탓에 배포에서 처음 걸렸다. Co-Authored-By: Claude Opus 5 (1M context) --- supporters/src/pages/en/stay.astro | 67 ++++++++++++++----- supporters/src/styles/global.css | 15 +++++ .../supporters-astro/src/pages/en/stay.astro | 67 ++++++++++++++----- .../supporters-astro/src/styles/global.css | 15 +++++ 4 files changed, 134 insertions(+), 30 deletions(-) diff --git a/supporters/src/pages/en/stay.astro b/supporters/src/pages/en/stay.astro index cf76b89..5906845 100644 --- a/supporters/src/pages/en/stay.astro +++ b/supporters/src/pages/en/stay.astro @@ -113,15 +113,44 @@ const ROUTES = [ }, ].filter((r) => r.route.stops.length > 1); -/* 좌표를 그대로 그린 미니 지도. 외부 지도 라이브러리를 부르지 않는다. */ +/* OpenStreetMap 타일을 좌표로 직접 깔아 지도를 만든다. 지도 라이브러리를 부르지 않는다. + 빈 회색 칸에 점만 찍으면 어디인지 알 수 없어서, 실제 거리·블록이 보이게 한다. + 타일은 OSM 정책상 출처를 반드시 표기한다. */ +const TILE = 256, MAPW = 680, MAPH = 340; +const lngToPx = (lng: number, z: number) => ((lng + 180) / 360) * TILE * 2 ** z; +const latToPx = (lat: number, z: number) => { + const r = (lat * Math.PI) / 180; + return ((1 - Math.log(Math.tan(r) + 1 / Math.cos(r)) / Math.PI) / 2) * TILE * 2 ** z; +}; + function miniMap(stops: any[]) { - const W = 640, H = 200, PAD = 26; const lats = stops.map((s) => s.lat), lngs = stops.map((s) => s.lng); const [la0, la1] = [Math.min(...lats), Math.max(...lats)]; const [ln0, ln1] = [Math.min(...lngs), Math.max(...lngs)]; - const sx = (ln: number) => (ln1 - ln0 < 1e-6 ? W / 2 : PAD + ((ln - ln0) / (ln1 - ln0)) * (W - PAD * 2)); - const sy = (la: number) => (la1 - la0 < 1e-6 ? H / 2 : H - PAD - ((la - la0) / (la1 - la0)) * (H - PAD * 2)); - return stops.map((s, i) => ({ x: +sx(s.lng).toFixed(1), y: +sy(s.lat).toFixed(1), n: i + 1, label: s.label })); + // 모든 지점이 여백 안에 들어오는 가장 확대된 배율을 고른다. + let z = 17; + for (; z > 10; z--) { + const w = lngToPx(ln1, z) - lngToPx(ln0, z); + const h = latToPx(la0, z) - latToPx(la1, z); + if (w <= MAPW - 90 && h <= MAPH - 90) break; + } + const cx = (lngToPx(ln0, z) + lngToPx(ln1, z)) / 2; + const cy = (latToPx(la0, z) + latToPx(la1, z)) / 2; + const left = cx - MAPW / 2, top = cy - MAPH / 2; + const tiles: Array<{ src: string; x: number; y: number }> = []; + for (let tx = Math.floor(left / TILE); tx <= Math.floor((left + MAPW) / TILE); tx++) { + for (let ty = Math.floor(top / TILE); ty <= Math.floor((top + MAPH) / TILE); ty++) { + const n = 2 ** z; + if (tx < 0 || ty < 0 || tx >= n || ty >= n) continue; + tiles.push({ src: `https://tile.openstreetmap.org/${z}/${tx}/${ty}.png`, x: tx * TILE - left, y: ty * TILE - top }); + } + } + const pins = stops.map((s, i) => ({ + x: +(lngToPx(s.lng, z) - left).toFixed(1), + y: +(latToPx(s.lat, z) - top).toFixed(1), + n: i + 1, label: s.label, + })); + return { tiles, pins, z }; } const allPlaces: Array = GROUPS.flatMap((g) => @@ -198,7 +227,7 @@ const ld = [ {items.map((p) => (
- {p.image ? : {shortName(p.title)}} + {p.image ? {`${p.title} : {shortName(p.title)}} {p.travel && {p.travel.label} {p.distanceM! < 1000 ? `${p.distanceM} m` : `${(p.distanceM! / 1000).toFixed(1)} km`}}
@@ -233,7 +262,7 @@ const ld = [ {T.festivals.map((e: any) => (
- {e.image ? : {shortName(e.title)}} + {e.image ? {`${e.title} : {shortName(e.title)}} {MONTHS[e.month] ?? ''}
@@ -271,14 +300,22 @@ const ld = [
{r.route.from}–{r.route.to} · {Math.floor(r.route.total / 60)}h {r.route.total % 60}m

{r.blurb}

- - - `${p.x},${p.y}`).join(' ')} fill="none" stroke="#C5CBF5" stroke-width="2" stroke-dasharray="5 4" /> - {pins.map((p) => ( - {p.n} - ))} - -

Positions are drawn from coordinates, to scale with each other. Not a street map.

+