From 527e50180b7487955e6e942d53a70bb4223b9139 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 11:55:21 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(636-2):=20=EC=B6=94=EC=B2=9C=EB=85=B8?= =?UTF-8?q?=EC=84=A0=20=EB=A9=94=EB=89=B4=20=C2=B7=20=EC=84=A0=20=EA=B7=B8?= =?UTF-8?q?=EB=A6=AC=EA=B8=B0=20=EA=B3=B5=EC=9A=A9(map=5Froute=5Fpreview)?= =?UTF-8?q?=20=C2=B7=20=EC=98=88=EC=A0=95=EB=85=B8=EC=84=A0=20=EA=B0=80?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20=EC=84=A0(=EC=A7=80=EC=A0=90=20=EC=9E=87?= =?UTF-8?q?=EA=B8=B0=20=C2=B7=2020=20m=20=C2=B7=20=C2=B114=20%=20=C2=B7=20?= =?UTF-8?q?=EA=B8=89=EA=B2=BD=EC=82=AC=20=EB=AC=B8=ED=84=B1=20=3D=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=EB=91=98=EC=A7=B8=20=EA=B2=BD=EA=B3=84=20?= =?UTF-8?q?=C2=B7=20=EB=B3=B4=EA=B8=B0=EB=A7=8C)(=EC=9A=94=EA=B5=AC=20546)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - B05 추천 미리보기 = 공용 메뉴 · 그리기 부름(저장본 넷 · 동작 그대로) - 도구 표 recommend 줄 = 두 페이지 - test_626_1 = 메뉴 검사를 공용 파일로 옮김(B05 파일이 createPickMenu 를 직접 안 부름) Co-Authored-By: Claude Opus 5.5 --- A00_Common/map/map_route_preview.ts | 56 ++++++ A00_Common/map/map_toolbar.ts | 4 +- .../B05_Profile_UI_RouteEdit_RecPreview.ts | 29 +--- D03_PlanAnalysis/D03_PlanAnalysis_UI_Guide.ts | 160 ++++++++++++++++++ D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts | 3 + D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts | 14 +- .../tester/test_626_1_recommend_preview.py | 4 +- resources/tester/test_636_2_guide_screen.py | 56 ++++++ 8 files changed, 297 insertions(+), 29 deletions(-) create mode 100644 A00_Common/map/map_route_preview.ts create mode 100644 D03_PlanAnalysis/D03_PlanAnalysis_UI_Guide.ts create mode 100644 resources/tester/test_636_2_guide_screen.py diff --git a/A00_Common/map/map_route_preview.ts b/A00_Common/map/map_route_preview.ts new file mode 100644 index 000000000..a2676335c --- /dev/null +++ b/A00_Common/map/map_route_preview.ts @@ -0,0 +1,56 @@ +/* ============================================================================= + * map_route_preview.ts + * 공용 「추천노선」 미리보기(PLAN 626-1 → 636-2 공용 · 요구 530 · 546) — B05 평면 · 예정노선(D03)이 같이 씀. + * 메뉴 = 도구줄 아이콘 하나(compass · 호버 「추천노선」) + 아래 판(항목 하나만 · 맨 끝 「끔」 · 부품 `createPickMenu`). + * 그리기 = 지도 위 선(노랑 `--draw-map-sketch` · 검은 테 · 기본 점선) — 위성 · 어두운 바탕 모두 보임. + * 항목 · 선 셈은 페이지 몫: B05 = 노선 추천 저장본 넷(`B05_Profile_UI_RouteEdit_RecPreview`) · + * 예정노선 = 가이드 선(`D03_PlanAnalysis_UI_Guide`). + * ========================================================================== */ + +import { createPickMenu, type PickMenu, type PickMenuItem } from "@ui/ui_template_context_menu"; +import { themeColor } from "@ui/ui_template_palette"; + +type Vertex = [number, number] | [number, number, number]; + +/** 「추천노선」 메뉴 — 고름 = `onPick(값)` · 「끔」 = `onPick(null)`. */ +export function createRoutePreviewMenu(opts: { + items: PickMenuItem[]; + onPick: (value: string | null) => void; +}): PickMenu { + const menu = createPickMenu({ + icon: "compass", + title: "추천노선", // 마우스 글 = 공용 표 호버제목(634-2 · `mapToolGroups` 가 맞춤) + items: opts.items, + offLabel: "끔", + onPick: opts.onPick, + }); + menu.root.dataset.act = "recommend"; + return menu; +} + +/** 미리보기 선 하나 — 검은 테(5 px) 위 색 선(3 px) · 기본 노랑 점선. */ +export function paintRoutePreview( + context: CanvasRenderingContext2D, + line: readonly Vertex[], + toScreen: (vertex: [number, number]) => [number, number], + style: { dash?: number[]; color?: string } = {}, +): void { + if (line.length < 2) return; + context.save(); + context.setLineDash(style.dash ?? [10, 6]); + context.lineJoin = "round"; + context.beginPath(); + line.forEach((vertex, index) => { + const [x, y] = toScreen([vertex[0], vertex[1]]); + if (index) context.lineTo(x, y); + else context.moveTo(x, y); + }); + // 지도 위 그림 색(테마 안 따름) + 검은 테 — 위성 · 어두운 바탕 모두 보임 + context.strokeStyle = "rgba(0, 0, 0, 0.6)"; + context.lineWidth = 5; + context.stroke(); + context.strokeStyle = style.color ?? themeColor("--draw-map-sketch", "#ffe94d"); + context.lineWidth = 3; + context.stroke(); + context.restore(); +} diff --git a/A00_Common/map/map_toolbar.ts b/A00_Common/map/map_toolbar.ts index 5d4c388e6..1138d27f1 100644 --- a/A00_Common/map/map_toolbar.ts +++ b/A00_Common/map/map_toolbar.ts @@ -75,8 +75,8 @@ export const MAP_TOOLBAR: readonly (readonly MapToolRow[])[] = [ { key: "recommend", title: "추천노선", - does: "노선 추천 저장본 선 하나만 보기 · 끔(626-1 · 예정노선 = 636-2)", - pages: B05, + does: "B05 = 노선 추천 저장본 선 하나만 · 예정노선 = 가이드 선(지점 잇기 · 636-2) · 끔", + pages: BOTH, }, ], [{ key: "terrain", title: "수치지형도/라이다", does: "지형 원천 보기 · 고르기", pages: BOTH }], diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_RecPreview.ts b/B05_Profile/B05_Profile_UI_RouteEdit_RecPreview.ts index a658eee70..a21b72860 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_RecPreview.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_RecPreview.ts @@ -5,11 +5,11 @@ * 선 = 노선 추천 저장본(`B05_Profile/route_recommend/` · 지금 설계 지형 · 체인이 미리 셈) 그대로 — 다시 셈 · 쓰기 없음 · * 저장본 없으면(409) 알림 · 끔. 옛 추천 모달 · 좌측 「노선 추천」 칸 · [적용](`as_initial`) 은 걷음(서버 길 · 시험은 남김). * 부르는 곳 = `_Chrome`(도구줄 `recommend` 칸) · `_RouteEdit`(그리기 `overStations` 앞). + * 메뉴 · 선 그리기 = 공용 `A00_Common/map/map_route_preview`(636-2 · 예정노선 가이드 선과 같이 씀). * ========================================================================== */ -import { createPickMenu } from "@ui/ui_template_context_menu"; import { showToast } from "@ui/ui_template_elements"; -import { themeColor } from "@ui/ui_template_palette"; +import { createRoutePreviewMenu, paintRoutePreview } from "../A00_Common/map/map_route_preview"; import type { AnalysisTerrain } from "./B05_Profile_Api_Replan"; import { fetchRouteInitial, type RouteInitialLogic } from "./B05_Profile_Api_RouteInitial"; import { LOGIC_LABEL } from "./B05_Profile_UI_RouteEdit_Initial_Model"; @@ -41,14 +41,10 @@ export function createRecPreview(opts: { const lines = new Map(); let shown: Vertex[] | null = null; let turn = 0; - const menu = createPickMenu({ - icon: "compass", - title: "추천노선", // 마우스 글 = 공용 표 호버제목(634-2 · `mapToolGroups` 가 맞춤) + const menu = createRoutePreviewMenu({ items: MODES.map((mode) => ({ value: mode, label: LOGIC_LABEL[mode] })), - offLabel: "끔", onPick: (mode) => void pick(mode as RouteInitialLogic | null), }); - menu.root.dataset.act = "recommend"; async function pick(mode: RouteInitialLogic | null): Promise { const mine = ++turn; @@ -81,24 +77,7 @@ export function createRecPreview(opts: { return { root: menu.root, draw(context, toScreen) { - if (!shown) return; - context.save(); - context.setLineDash([10, 6]); - context.lineJoin = "round"; - context.beginPath(); - shown.forEach((vertex, index) => { - const [x, y] = toScreen(vertex); - if (index) context.lineTo(x, y); - else context.moveTo(x, y); - }); - // 지도 위 그림 색(테마 안 따름) + 검은 테 — 위성 · 어두운 바탕 모두 보임 - context.strokeStyle = "rgba(0, 0, 0, 0.6)"; - context.lineWidth = 5; - context.stroke(); - context.strokeStyle = themeColor("--draw-map-sketch", "#ffe94d"); - context.lineWidth = 3; - context.stroke(); - context.restore(); + if (shown) paintRoutePreview(context, shown, toScreen); }, state: () => ({ mode: menu.value(), points: shown?.length ?? 0 }), }; diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Guide.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Guide.ts new file mode 100644 index 000000000..7aa77d217 --- /dev/null +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Guide.ts @@ -0,0 +1,160 @@ +/* ============================================================================= + * D03_PlanAnalysis_UI_Guide.ts + * 예정노선 가이드 추천선 화면(PLAN 636-2 · 요구 546) — 도구줄 「추천노선」 = 「가이드 선(지점 잇기)」 · 「끔」. + * 지점 = 지점 목록 차례 · 지점 둘 이상일 때만 열림 · 높이 = 지형 칩 원천 격자 · 걸음 20 m · 기울기 ±14 % 고정(사용자 원문 · + * 그리기 칸 값과 따로) · 급경사 문턱 = 급경사 범위 둘째 경계(기본 70 %). + * 셈 = 공용 `map_guide_route`(브라우저만 · 저장 안 함) · 켠 동안 지점 · 지형 · 급경사 범위가 바뀌면 다시 셈(끌기 중엔 멈춘 뒤 한 번). + * 그림 = 닿은 선 노랑 실선 · 못 닿은 구간 = 간 데 → 다음 지점 빨강 점선 + 알림 · 끝 지점 옆 짧은 글(길이 · 최대 기울기 · 급경사 %). + * 메뉴 · 선 그리기 = 공용 `map_route_preview`(B05 추천 미리보기와 같음). + * ========================================================================== */ + +import { showToast } from "@ui/ui_template_elements"; +import { mapToolTip } from "@ui/ui_template_map_tools"; +import { themeColor } from "@ui/ui_template_palette"; +import { + guideRoute, + guideTerrainFromGrid, + type GuideResult, +} from "../A00_Common/map/logic/map_guide_route"; +import { createRoutePreviewMenu, paintRoutePreview } from "../A00_Common/map/map_route_preview"; +import { readSlopeBands } from "../A00_Common/map/map_slope_bands"; +import type { DtmGrid } from "../common_util/common_util_surface_sampler"; +import { planLabel } from "./D03_PlanAnalysis_UI_DrawPaint"; +import type { PlanMapLayer } from "./D03_PlanAnalysis_UI_Map"; + +type Vertex = [number, number]; + +/** 사용자 원문(요구 546) — 걸음 · 기울기 고정. */ +const GUIDE_STEP_M = 20; +const GUIDE_MAX_GRADE = 0.14; +/** 끌기 · 연이은 고침이 멈춘 뒤 셈(ms). */ +const SETTLE_MS = 150; + +export interface GuideState { + on: boolean; + open: boolean; + segments: number; + reached: number; + lengthM: number; + maxGradePct: number; + steepPct: number; + steepLimitPct: number; + ms: number | null; +} + +/** 급경사 문턱 = 범위 경계를 작은 차례로 늘어놓은 둘째(기본 26.8 · 70 · … → 70). */ +function steepLimit(): number { + const edges = [...new Set(readSlopeBands().flatMap((band) => [band.low, band.high]))]; + edges.sort((a, b) => a - b); + return edges[1] ?? edges[0] ?? 70; +} + +export function createPlanGuide(opts: { + points: () => Vertex[]; + /** 지형 칩 원천 격자(없으면 null). */ + grid: () => DtmGrid | null; + redraw: () => void; +}): { root: HTMLElement; layer: PlanMapLayer; state: () => GuideState } { + let result: GuideResult | null = null; + let shownFor: Vertex[] = []; + let doneKey = ""; + let doneGrid: DtmGrid | null = null; + let steepPct = 70; + let timer = 0; + let open: boolean | null = null; + const menu = createRoutePreviewMenu({ + items: [{ value: "guide", label: "가이드 선(지점 잇기)" }], + onPick: (value) => { + [result, doneKey, doneGrid] = [null, "", null]; + if (value) compute(true); + opts.redraw(); + }, + }); + const on = (): boolean => menu.value() === "guide"; + + function compute(picked = false): void { + window.clearTimeout(timer); + const points = opts.points().map(([x, y]) => [x, y] as Vertex); + const grid = opts.grid(); + if (!grid) { + menu.set(null); + result = null; + showToast("가이드 선 — 지형이 아직 없음", "info"); + return opts.redraw(); + } + steepPct = steepLimit(); + [doneKey, doneGrid] = [JSON.stringify([points, steepPct]), grid]; + result = guideRoute(points, guideTerrainFromGrid(grid), { + step: GUIDE_STEP_M, + maxGrade: GUIDE_MAX_GRADE, + steepPct, + }); + shownFor = points; + const missed = result.segments.filter((segment) => !segment.reached).length; + if (missed) showToast(`가이드 선 — ${missed} 구간 ±14 % 로 못 닿음(점선 = 곧은 이음)`, "info"); + else if (picked) showToast(`가이드 선 — ${Math.round(result.ms)} ms`, "info"); + opts.redraw(); + } + + /** 지점 둘 미만 = 막힘(켜져 있으면 끔). */ + function syncOpen(count: number): void { + const next = count >= 2; + if (next === open) return; + open = next; + menu.root.disabled = !next; + mapToolTip(menu.root, next ? null : "지점 둘 이상일 때"); + if (!next && on()) { + menu.set(null); + result = null; + } + } + + function summary(): Omit { + const segments = result?.segments ?? []; + const lengthM = segments.reduce((sum, segment) => sum + segment.length, 0); + const steep = segments.reduce((sum, segment) => sum + segment.steepRatio * segment.length, 0); + return { + segments: segments.length, + reached: segments.filter((segment) => segment.reached).length, + lengthM, + maxGradePct: Math.max(0, ...segments.map((segment) => segment.maxGrade)) * 100, + steepPct: lengthM ? (steep / lengthM) * 100 : 0, + steepLimitPct: steepPct, + ms: result ? result.ms : null, + }; + } + + const layer: PlanMapLayer = { + draw(context, map) { + const points = opts.points(); + syncOpen(points.length); + if (!on()) return; + const key = JSON.stringify([points.map(([x, y]) => [x, y]), steepLimit()]); + if (key !== doneKey || opts.grid() !== doneGrid) { + window.clearTimeout(timer); + timer = window.setTimeout(() => compute(), SETTLE_MS); + } + if (!result) return; + const warn = themeColor("--draw-map-warn", "#ff5a4d"); + result.segments.forEach((segment, index) => { + paintRoutePreview(context, segment.vertices, map.toScreen, { dash: [] }); + const goal = shownFor[index + 1]; + const last = segment.vertices[segment.vertices.length - 1] ?? shownFor[index]; + if (!segment.reached && goal && last) + paintRoutePreview(context, [last, goal], map.toScreen, { color: warn }); + }); + const end = shownFor[shownFor.length - 1]; + if (!end) return; + const s = summary(); + const [x, y] = map.toScreen(end); + const text = `가이드 ${(s.lengthM / 1000).toFixed(2)} km · 최대 ${s.maxGradePct.toFixed(1)} % · 급경사 지남 ${s.steepPct.toFixed(0)} %`; + planLabel(context, map, x, y - 22, text, "bold 12px"); + }, + }; + + return { + root: menu.root, + layer, + state: () => ({ on: on(), open: !!open, ...summary() }), + }; +} diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts index e88ce68dd..bafd0cf12 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts @@ -124,6 +124,8 @@ export interface PlanMapEvents { onMeasure: () => void; /** 도구줄 끝 지형 칸(624-4 · 칩 + LAS 올리기 = `D03_PlanAnalysis_UI_LidarUpload`). */ terrain: HTMLElement; + /** 도구줄 「추천노선」 = 가이드 선 메뉴(636-2 · `D03_PlanAnalysis_UI_Guide`). */ + recommend?: HTMLElement; } export interface PlanTools { @@ -248,6 +250,7 @@ export function createPlanMap(events: PlanMapEvents): PlanMap { slopes: markButton("slopes"), measure: measureButton, terrain: events.terrain, + recommend: events.recommend, }), }); const toolbar = tools.root; diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts index 91bc5c79f..7f72ce695 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts @@ -50,6 +50,7 @@ import { type TerrainStart, } from "./D03_PlanAnalysis_Api"; import { openPlan3d } from "./D03_PlanAnalysis_UI_3D"; +import { createPlanGuide } from "./D03_PlanAnalysis_UI_Guide"; import { bindPlanInput, createPlanKit } from "./D03_PlanAnalysis_UI_Kit"; import { createPlanLayers } from "./D03_PlanAnalysis_UI_Layers"; import { createPlanTerrain } from "./D03_PlanAnalysis_UI_LidarUpload"; @@ -210,8 +211,15 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise { work, onChange: () => (void backdrop.reterrain(), refresh()), }); + // 가이드 추천선(636-2) — 지점 차례 · 지형 칩 원천 · 보기만 + const guide = createPlanGuide({ + points: () => points, + grid: () => terrain.surface(surface)?.grid ?? null, + redraw: () => map.redraw(), + }); const map = createPlanMap({ terrain: terrain.root, + recommend: guide.root, onUndo: undo, onRedo: redo, on3d: open3d, @@ -314,7 +322,10 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise { /** 겹 차례 = 키트(바탕 겹 아래 · 재기 · 보기 가운데 = 고르기 위 · 핀 그림 · 우클릭 메뉴 맨 위). */ const layerList = (): PlanMapLayer[] => - kit.layers(backdrop.layers, [...(measuring ? [measure.layer] : []), viewWatch]); + kit.layers( + [...backdrop.layers, guide.layer], + [...(measuring ? [measure.layer] : []), viewWatch], + ); /** 점들이 받은 범위 안인가. */ const inside = (list: Vertex[]): boolean => @@ -568,6 +579,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise { heightAt: z, layers: backdrop.state, terrain: terrain.state, + guide: guide.state, }; // 키 = 등록부(588-3 · 한글 자판도 자판 자리로) · 되돌리기 · 다시하기 · 재기 끔 = 키트 `onKey` 먼저 const onKey = (event: KeyboardEvent): void => { diff --git a/resources/tester/test_626_1_recommend_preview.py b/resources/tester/test_626_1_recommend_preview.py index c611aa1a2..d7919d859 100644 --- a/resources/tester/test_626_1_recommend_preview.py +++ b/resources/tester/test_626_1_recommend_preview.py @@ -42,7 +42,9 @@ def test_미리보기_넷_하나만_저장본만() -> None: src = read(B05 / "B05_Profile_UI_RouteEdit_RecPreview.ts") modes = re.search(r"const MODES = \[(.*?)\]", src, re.S).group(1) assert re.findall(r'"(\w+)"', modes) == ["gradeline", "earthwork", "gentle", "weighted"] - assert "recompute" not in src and "createPickMenu({" in src and 'offLabel: "끔"' in src + assert "recompute" not in src and "createRoutePreviewMenu({" in src + shared = read(ROOT / "A00_Common" / "map" / "map_route_preview.ts") # 636-2 공용 메뉴 · 그리기 + assert "createPickMenu({" in shared and 'offLabel: "끔"' in shared assert "terrain: opts.terrain()" in src # 지금 설계 지형 저장본(D7) chrome = read(B05 / "B05_Profile_UI_RouteEdit_Chrome.ts") assert "recommend: preview.root" in chrome and "terrain: terrain.root" in chrome diff --git a/resources/tester/test_636_2_guide_screen.py b/resources/tester/test_636_2_guide_screen.py new file mode 100644 index 000000000..ddd19a8fc --- /dev/null +++ b/resources/tester/test_636_2_guide_screen.py @@ -0,0 +1,56 @@ +"""PLAN 636-2 — 「추천노선」 메뉴 · 선 그리기 공용 · 예정노선 가이드 선(요구 546). + +- 공용 `map_route_preview` = 메뉴(compass · 「끔」 · data-act) + 노랑 · 검은 테 선 — B05 · 예정노선이 같이 씀. +- 도구 표 `recommend` 줄 = 두 페이지 · 호버 「추천노선」. +- 예정노선 = 「가이드 선(지점 잇기)」 하나 · 20 m · ±14 % 고정 · 급경사 문턱 = 범위 둘째 경계 · 저장 · 서버 없음. +- B05 = 저장본 넷 그대로 · 가이드 항목 없음. + +./venv/Scripts/python.exe -m pytest resources/tester/test_636_2_guide_screen.py -q +""" + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +read = lambda *parts: ROOT.joinpath(*parts).read_text(encoding="utf-8") # noqa: E731 + + +def test_공용_메뉴_그리기() -> None: + src = read("A00_Common", "map", "map_route_preview.ts") + assert 'icon: "compass"' in src and 'offLabel: "끔"' in src + assert 'dataset.act = "recommend"' in src and "--draw-map-sketch" in src + for name in ( + "B05_Profile/B05_Profile_UI_RouteEdit_RecPreview.ts", + "D03_PlanAnalysis/D03_PlanAnalysis_UI_Guide.ts", + ): + page = read(name) + assert "createRoutePreviewMenu({" in page and "paintRoutePreview(" in page, name + assert "createPickMenu" not in page, name + + +def test_도구_표_두_페이지() -> None: + src = read("A00_Common", "map", "map_toolbar.ts") + row = src[src.index('key: "recommend"') :] + row = row[: row.index("}")] + assert 'title: "추천노선"' in row and "pages: BOTH" in row + assert "recommend: events.recommend" in read("D03_PlanAnalysis", "D03_PlanAnalysis_UI_Map.ts") + + +def test_예정노선_가이드_고정값_보기만() -> None: + src = read("D03_PlanAnalysis", "D03_PlanAnalysis_UI_Guide.ts") + assert "GUIDE_STEP_M = 20" in src and "GUIDE_MAX_GRADE = 0.14" in src + assert 'label: "가이드 선(지점 잇기)"' in src + assert "guideRoute(" in src and "guideTerrainFromGrid(" in src and "readSlopeBands()" in src + assert "count >= 2" in src # 지점 둘 이상일 때만 + for banned in ("fetch(", "writeState", "sessionStorage", "constraint"): + assert banned not in src, banned + page = read("D03_PlanAnalysis", "D03_PlanAnalysis_UI_Page.ts") + assert ( + "recommend: guide.root" in page and "guide.layer" in page and "guide: guide.state" in page + ) + assert len(page.splitlines()) <= 700 + + +def test_B05_저장본_넷_그대로() -> None: + src = read("B05_Profile", "B05_Profile_UI_RouteEdit_RecPreview.ts") + assert '"gradeline",\n "earthwork",\n "gentle",\n "weighted",' in src.replace("\r\n", "\n") + assert "가이드 선(지점 잇기)" not in src and "guideRoute" not in src From a7abcd6c5f30209620058a764540cf6c753c9e38 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 12:03:48 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(636-2):=20=EA=B0=80=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=20=EC=84=A0=20=EA=B8=80=20=EC=9E=90=EB=A6=AC=20=3D=20=EB=81=9D?= =?UTF-8?q?=20=EC=A7=80=EC=A0=90=20=ED=95=80=20=EC=95=84=EB=9E=98=20=C2=B7?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=EC=98=A4=EB=A5=B8=EC=AA=BD=20=EB=B0=98?= =?UTF-8?q?=EC=9D=B4=EB=A9=B4=20=EC=98=A4=EB=A5=B8=EC=AA=BD=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=A4(=EC=A7=80=EC=A0=90=20=EC=9D=B4=EB=A6=84=20=C2=B7=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=81=9D=EC=97=90=20=EC=95=88=20=EA=B1=B8?= =?UTF-8?q?=EB=A6=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- D03_PlanAnalysis/D03_PlanAnalysis_UI_Guide.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Guide.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Guide.ts index 7aa77d217..d7202aabb 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Guide.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Guide.ts @@ -148,7 +148,9 @@ export function createPlanGuide(opts: { const s = summary(); const [x, y] = map.toScreen(end); const text = `가이드 ${(s.lengthM / 1000).toFixed(2)} km · 최대 ${s.maxGradePct.toFixed(1)} % · 급경사 지남 ${s.steepPct.toFixed(0)} %`; - planLabel(context, map, x, y - 22, text, "bold 12px"); + // 끝 지점 핀 아래 · 화면 오른쪽 반이면 오른쪽 맞춤(지점 이름 · 화면 끝에 안 걸림) + const right = x > context.canvas.clientWidth / 2; + planLabel(context, map, x, y + 16, text, "bold 12px", right ? "right" : "left"); }, };