feat(b08): 거리이정표 경로 지도 테일스케일 주소 안내 (244-10)
- 보안 문맥 아닌 주소(getDisplayMedia 없음) = [지도 캡처] 끔 + 까닭 한 줄 「캡처는 localhost 또는 https 주소에서만 — 사진 올리기를 쓰세요」 - SDK 못 받음(script onerror · 도메인 미등록 401) = 지금 주소(location.origin) 등록 필요 안내 한 줄 · 사진 올리기 - 새 시험 test_244_10 · test_244_9 에 보안 문맥 navigator 가짜 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
* 시점(`mapMarks`) · 경로 전체 맞춤
|
||||
* [지도 캡처] = getDisplayMedia(preferCurrentTab) → 지도 칸만(CropTarget · 없으면 `cropBox` 자리 수치) →
|
||||
* 그 경로 그림(244-1 그림 자리 · 올리는 즉시 서버) · 트랙 곧바로 멈춤
|
||||
* (244-10) 보안 문맥 아닌 주소(테일스케일 http IP 등) = getDisplayMedia 없음 → [지도 캡처] 끔 + 까닭 한 줄 ·
|
||||
* SDK 못 받음(도메인 미등록 401) = 지금 주소(location.origin) 등록 안내 한 줄
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
@@ -22,6 +24,8 @@ import {
|
||||
|
||||
export const NO_MAP =
|
||||
"카카오 JavaScript 키 · 도메인 등록 필요 — 지도 대신 [그림 올리기]로 넣으세요";
|
||||
export const NO_CAPTURE = "캡처는 localhost 또는 https 주소에서만 — 사진 올리기를 쓰세요";
|
||||
const SDK_FAIL = "KakaoSdkFail";
|
||||
const SDK = "https://dapi.kakao.com/v2/maps/sdk.js";
|
||||
const LINE_COLOR = "#E60046"; // 테마 무관 또렷한 길 선
|
||||
|
||||
@@ -63,7 +67,15 @@ export function loadKakaoMaps(projectId: string): Promise<KakaoMaps> {
|
||||
const script = document.createElement("script");
|
||||
script.src = `${SDK}?appkey=${encodeURIComponent(key)}&autoload=false`;
|
||||
script.onload = () => done();
|
||||
script.onerror = () => fail(new Error(NO_MAP));
|
||||
script.onerror = () =>
|
||||
fail(
|
||||
Object.assign(
|
||||
new Error(
|
||||
`카카오 JavaScript 키 도메인에 지금 주소(${location.origin}) 등록 필요 — 지도 대신 [그림 올리기]로 넣으세요`,
|
||||
),
|
||||
{ name: SDK_FAIL },
|
||||
),
|
||||
);
|
||||
document.head.append(script);
|
||||
});
|
||||
}
|
||||
@@ -142,6 +154,8 @@ export function routeMapView(
|
||||
let map: KMap | null = null;
|
||||
let drawn: KOverlay[] = [];
|
||||
let shown = ""; // 같은 표지 · 길 선이면 다시 안 그림(사용자가 옮긴 지도 자리 그대로)
|
||||
const canCapture = Boolean(navigator.mediaDevices?.getDisplayMedia); // http IP 주소 = 없음
|
||||
const shot = canCapture ? "[지도 캡처] = 이 지도 칸 → 경로 그림" : NO_CAPTURE;
|
||||
|
||||
const draw = (): void => {
|
||||
const marks = mapMarks(liveMilestone(projectId), routeId, siteInfo()?.start_lonlat ?? null);
|
||||
@@ -192,13 +206,13 @@ export function routeMapView(
|
||||
map.setBounds(bounds);
|
||||
shown = sig;
|
||||
hint.textContent = lines.length
|
||||
? "길 선 = 카카오 길찾기 · [지도 캡처] = 이 지도 칸 → 경로 그림"
|
||||
: "[길찾기]를 누르면 길 선이 그려짐 · [지도 캡처] = 이 지도 칸 → 경로 그림";
|
||||
? `길 선 = 카카오 길찾기 · ${shot}`
|
||||
: `[길찾기]를 누르면 길 선이 그려짐 · ${shot}`;
|
||||
}),
|
||||
() => {
|
||||
(error: unknown) => {
|
||||
box.hidden = true; // 키 없음 · SDK 못 받음 — 안내 한 줄 · [캡처] 숨김(사진 올리기는 그대로)
|
||||
capture.hidden = true;
|
||||
hint.textContent = NO_MAP;
|
||||
hint.textContent = (error as Error)?.name === SDK_FAIL ? (error as Error).message : NO_MAP;
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -207,6 +221,7 @@ export function routeMapView(
|
||||
label: "지도 캡처",
|
||||
size: "sm",
|
||||
variant: "ghost",
|
||||
disabled: !canCapture,
|
||||
onClick: () =>
|
||||
void captureElement(box)
|
||||
.then((file) => uploadImage(projectId, routeId, file))
|
||||
@@ -216,6 +231,7 @@ export function routeMapView(
|
||||
}),
|
||||
});
|
||||
capture.dataset.b08MilestoneCapture = "";
|
||||
if (!canCapture) capture.title = NO_CAPTURE;
|
||||
const root = el("div", {
|
||||
className: "b08-milestone__map-wrap",
|
||||
children: [el("div", { className: "b08-milestone__bar", children: [capture] }), hint, box],
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""244-10 B08 거리이정표 경로 지도 — 테일스케일 주소(http://100.72.32.27:5173 · 보안 문맥 아님).
|
||||
|
||||
① getDisplayMedia 없음 = [지도 캡처] 끔 + 까닭 한 줄(지도는 그대로)
|
||||
② SDK 못 받음(script onerror · 도메인 미등록 401) = 지금 주소(location.origin) 등록 안내 한 줄 · 지도 · 캡처 숨김
|
||||
(가짜 kakao.maps · document 로 node 에서 돌림)
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
LAYOUT = "B08_Quantity/B08_Quantity_Milestone_Layout.ts"
|
||||
KMAP = "B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts"
|
||||
ORIGIN = "http://100.72.32.27:5173"
|
||||
NO_CAPTURE = "캡처는 localhost 또는 https 주소에서만 — 사진 올리기를 쓰세요"
|
||||
|
||||
|
||||
def _read(rel):
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
STUBS = f"""
|
||||
const el = (tag: string, o: any = {{}}) => ({{ tag, hidden: false, textContent: o.text ?? "",
|
||||
className: o.className, dataset: {{}}, children: o.children ?? [] }});
|
||||
const createButton = (o: any) => ({{ ...el("button", {{ text: o.label }}), disabled: !!o.disabled,
|
||||
title: "", onClick: o.onClick }});
|
||||
const showToast = () => {{}};
|
||||
const uploadImage = async () => {{}};
|
||||
const requestAnimationFrame = (f: () => void) => setTimeout(f, 0);
|
||||
const request = async () => ({{ key: "fake-js-key" }});
|
||||
const injected: string[] = [];
|
||||
(globalThis as any).window = globalThis;
|
||||
(globalThis as any).location = {{ origin: {json.dumps(ORIGIN)} }};
|
||||
const liveMilestone = () => ({{ site: {{}}, base: {{ enabled: false }},
|
||||
routes: [{{ id: "r1", name: "거창군청", lon: 127.9, lat: 35.68, via_base: false }}] }});
|
||||
const siteInfo = () => ({{ start_lonlat: [127.83, 35.64] }});
|
||||
const routePaths = () => [[[35.68, 127.9], [35.64, 127.83]]];
|
||||
"""
|
||||
|
||||
FAKE_KAKAO = """
|
||||
class Fake { setMap() {} relayout() {} setBounds() {} extend() {} }
|
||||
(globalThis as any).kakao = { maps: { load: (f: () => void) => f(), LatLng: Fake,
|
||||
LatLngBounds: Fake, Map: Fake, Polyline: Fake, Marker: Fake, CustomOverlay: Fake } };
|
||||
"""
|
||||
|
||||
|
||||
def _view(setup):
|
||||
script = (
|
||||
STUBS
|
||||
+ setup
|
||||
+ IMPORTS.sub("", _read(KMAP))
|
||||
+ """
|
||||
const view = routeMapView("p1", "r1");
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
const [bar, hint, box] = view.root.children;
|
||||
const cap = bar.children[0];
|
||||
console.log(JSON.stringify({ injected, hint: hint.textContent, boxHidden: box.hidden,
|
||||
capHidden: cap.hidden, capDisabled: cap.disabled, capTitle: cap.title }));
|
||||
"""
|
||||
)
|
||||
src = IMPORTS.sub("", _read(LAYOUT)) + script
|
||||
result = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", src],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert result.returncode == 0, result.stderr
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
NO_MEDIA = """
|
||||
Object.defineProperty(globalThis, "navigator", { configurable: true, value: {} });
|
||||
(globalThis as any).document = { createElement: () => ({}), head: { append: () => {} } };
|
||||
"""
|
||||
|
||||
|
||||
def test_보안_문맥_아님_캡처_끔_까닭_한_줄():
|
||||
got = _view(NO_MEDIA + FAKE_KAKAO)
|
||||
assert not got["boxHidden"] and not got["capHidden"] # 지도는 그대로 뜸
|
||||
assert got["capDisabled"] and got["capTitle"] == NO_CAPTURE
|
||||
assert got["hint"] == f"길 선 = 카카오 길찾기 · {NO_CAPTURE}"
|
||||
assert "[지도 캡처] =" not in got["hint"]
|
||||
|
||||
|
||||
def test_보안_문맥_캡처_켜짐():
|
||||
media = """
|
||||
Object.defineProperty(globalThis, "navigator", { configurable: true,
|
||||
value: { mediaDevices: { getDisplayMedia: async () => ({}) } } });
|
||||
(globalThis as any).document = { createElement: () => ({}), head: { append: () => {} } };
|
||||
"""
|
||||
got = _view(media + FAKE_KAKAO)
|
||||
assert not got["capDisabled"] and got["capTitle"] == ""
|
||||
assert got["hint"] == "길 선 = 카카오 길찾기 · [지도 캡처] = 이 지도 칸 → 경로 그림"
|
||||
|
||||
|
||||
def test_SDK_401_도메인_등록_안내():
|
||||
sdk_401 = """
|
||||
(globalThis as any).document = { createElement: () => ({}),
|
||||
head: { append: (s: any) => (injected.push(s.src), setTimeout(() => s.onerror(), 0)) } };
|
||||
"""
|
||||
got = _view(NO_MEDIA + sdk_401) # window.kakao 없음 → script 넣음 → 401 = onerror
|
||||
assert len(got["injected"]) == 1 and "sdk.js?appkey=fake-js-key" in got["injected"][0]
|
||||
assert got["hint"] == (
|
||||
f"카카오 JavaScript 키 도메인에 지금 주소({ORIGIN}) 등록 필요 — 지도 대신 [그림 올리기]로 넣으세요"
|
||||
)
|
||||
assert got["boxHidden"] and got["capHidden"]
|
||||
@@ -152,6 +152,8 @@ const injected: string[] = [];
|
||||
(globalThis as any).document = { createElement: () => ({}),
|
||||
head: { append: (s: any) => injected.push(s.src) } };
|
||||
(globalThis as any).window = globalThis;
|
||||
Object.defineProperty(globalThis, "navigator", { configurable: true,
|
||||
value: { mediaDevices: { getDisplayMedia: async () => ({}) } } }); // localhost = 보안 문맥
|
||||
const liveMilestone = () => ({ site: {}, base: { enabled: true, name: "병곡리", lon: 127.85,
|
||||
lat: 35.66 }, routes: [{ id: "r1", name: "거창군청", lon: 127.9, lat: 35.68, via_base: true }] });
|
||||
const siteInfo = () => ({ start_lonlat: [127.83, 35.64] });
|
||||
|
||||
Reference in New Issue
Block a user