@@ -149,12 +112,61 @@ export function createRouteEditChrome(
`;
parent.append(overlay);
- // 부른 쪽 단추([3D])도 같은 유리 단추로 — 지도 위에 떠 있는 도구줄.
- tools.forEach((tool) => {
- tool.classList.replace("ui-btn--ghost", "ui-btn--glass");
- tool.setAttribute("data-size", "32"); // 도구줄 단 32(553-2)
+ // 스냅 간격 고르기 · 설계 지형 이름 칩 — 단추 아님(도구줄에 요소로 끼움).
+ const extras = document.createElement("div");
+ extras.innerHTML = `
+
+
`;
+ const [snapStep, terrainName] = extras.children as unknown as [HTMLElement, HTMLElement];
+ const mapTools = createMapTools({
+ label: "평면 도구",
+ hint: { lines: HINT_LINES, open: hintOpen, onToggle: (open) => (hintOpen = open) },
+ groups: [
+ [
+ mapToolButton("undo", { title: "되돌리기 (Ctrl+Z)", disabled: true }),
+ mapToolButton("redo", { title: "다시하기 (Ctrl+Y)", disabled: true }),
+ ],
+ tools, // 부른 쪽 단추([3D])
+ [
+ "rotate-ccw",
+ "rotate-cw",
+ mapToolButton("contour-minor", {
+ title: "1m 등고선 — 1m 간격 등고선을 얇고 옅게 겹칩니다 (확대하면 보임)",
+ disabled: true,
+ }),
+ mapToolButton("stations", {
+ title: "측점 — 측점 눈금 · 번호를 평면에 보이기 · 숨기기 (시점 · 종점 이름표는 늘 보임)",
+ }),
+ mapToolButton("slopes", {
+ title: "기울기 — 측점 사이 기울기(%)를 평면에 보이기 · 숨기기 (확대해야 보임)",
+ }),
+ mapToolButton("footprint", {
+ title: "절/성토선 — 도로 끝 · 비탈 끝(깎기 붉음 · 쌓기 청록)을 평면에 겹칩니다",
+ }),
+ mapToolButton("measure", {
+ title: "거리 재기 — 노선 위 두 점을 눌러 거리·기울기를 잽니다 (Shift+클릭도 같음)",
+ }),
+ ],
+ [
+ mapToolButton("station-snap", {
+ title:
+ "측점 스냅 — 켜면 고친 곡선의 가운데(MC)를 고른 간격 배수에 맞춥니다 — 일반측점(20 m)은 그대로 · MC 가 일반측점 위가 아니면 「No.N+10 (MC)」 특별측점을 더함 · 손대지 않은 곡선은 그 자리 유지 · 고정 교각점은 그대로 · 못 맞춘 곡선은 상태줄 「측점 밖 곡선」 · [저장] 때 저장",
+ }),
+ snapStep,
+ ],
+ [terrainName],
+ ],
});
- overlay.querySelector('[data-slot="tools"]')!.replaceWith(...tools);
+ mapTools.root.classList.add("b05-routeedit__tools");
+ mapTools.hint!.classList.add("b05-routeedit__hint"); // 가이드 촬영 · 엿보기 숨김이 잡음
+ overlay.querySelector('[data-slot="map-tools"]')!.replaceWith(mapTools.root);
// 횡단 미리보기 = 패널 양식(382-1 · 손잡이 · 머리 · 폭 끌기 · 열림 · 폭 기억) · 자리는 지도 위 덮개 그대로 ·
// 기본 접힘(393-1 · 키 -v2 = 옛 열림 기억 한 번 버림) · 열림 · 닫힘은 손잡이만(513-2) · 잰 값 창이 판을 비킴(--b05-side-open).
@@ -177,17 +189,6 @@ export function createRouteEditChrome(
overlay.querySelector(".b05-routeedit__side-wrap")!.append(panels.root);
setSideOpen();
- // 도움말 = 기본 접힘 · [?] 한 번에 펼침(345-5 덧 3).
- const hint = overlay.querySelector
(".b05-routeedit__hint")!;
- const hintToggle = overlay.querySelector('[data-act="hint-toggle"]')!;
- const setHint = (open: boolean): void => {
- hintOpen = open;
- hint.classList.toggle("is-open", open);
- hintToggle.setAttribute("aria-expanded", String(open));
- };
- setHint(hintOpen);
- hintToggle.addEventListener("click", () => setHint(!hintOpen));
-
// 상태 · 범례 접기 — 접으면 한 줄 「계획노선 N m」(상태 글에서 읽음 · 345-5 덧 3).
const info = overlay.querySelector(".b05-routeedit__info")!;
const infoToggle = info.querySelector('[data-act="info-toggle"]')!;
@@ -243,16 +244,13 @@ export function createRouteEditChrome(
syncSummary();
const measureBox = overlay.querySelector(".b05-routeedit__measure")!;
const measureText = overlay.querySelector(".b05-routeedit__measure-text")!;
- const measureButton = overlay.querySelector('[data-act="measure"]')!;
+ const measureButton = mapTools.button("measure")!;
const busy = overlay.querySelector(".b05-routeedit__busy")!;
- const minorContourButton = overlay.querySelector(
- '[data-act="contour-minor"]',
- )!;
- const terrainName = overlay.querySelector('[data-slot="terrain-name"]')!;
- const stationSnapButton = overlay.querySelector('[data-act="station-snap"]')!;
- bindFootprintToggle(overlay.querySelector('[data-act="footprint"]')!);
- bindMarkToggle(overlay.querySelector('[data-act="stations"]')!, "stations");
- bindMarkToggle(overlay.querySelector('[data-act="slopes"]')!, "slopes");
+ const minorContourButton = mapTools.button("contour-minor")!;
+ const stationSnapButton = mapTools.button("station-snap")!;
+ bindFootprintToggle(mapTools.button("footprint")!);
+ bindMarkToggle(mapTools.button("stations")!, "stations");
+ bindMarkToggle(mapTools.button("slopes")!, "slopes");
return {
overlay,
diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Measure.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Measure.ts
index 98cda38de..2e4a90118 100644
--- a/B05_Profile/B05_Profile_UI_RouteEdit_Measure.ts
+++ b/B05_Profile/B05_Profile_UI_RouteEdit_Measure.ts
@@ -169,12 +169,14 @@ export function bindMeasureControls(params: MeasureControlsParams): {
button.addEventListener("click", () => {
mode = !mode;
button.classList.toggle("is-active", mode);
+ button.setAttribute("aria-pressed", String(mode)); // 아이콘 단추 눌림 꼴(568-2)
if (!mode) measure.clear();
});
params.closeButton.addEventListener("click", () => {
measure.clear(); // 닫으면 잰 것이 지워진다(㉔).
mode = false;
button.classList.remove("is-active");
+ button.setAttribute("aria-pressed", "false");
sync();
params.redraw();
});
diff --git a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css
index d8fd0a8c2..7c6680997 100644
--- a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css
+++ b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css
@@ -1,8 +1,8 @@
/* 계획노선 편집(평면) — 종단 보기 가운데 위 본문(옛 3D 자리 · 345-3). 덮개 · 전체 화면은 걷음 —
재계산 대기 막만 화면을 덮는다(노선 재탐색 · CLAUDE.md 5장). 색은 전부 테마 토큰을 쓴다. */
-/* 평면 = 가운데 칸 전체(352-1) — 머리 줄 · 테두리 없음. 도구줄은 3D 와 같은 떠 있는 유리 단추
- (`.b05-route__view-controls` · ☰ 비킴 `--wf-burger-pad`) · 횡단 판은 오른쪽을 덮는 판. */
+/* 평면 = 가운데 칸 전체(352-1) — 머리 줄 · 테두리 없음. 도구줄은 공용 지도 도구줄(568-2 · 유리 아이콘 줄 ·
+ ☰ 비킴 `--wf-burger-pad`) · 횡단 판은 오른쪽을 덮는 판. */
.b05-routeedit {
--b05-side-open: 0px; /* 횡단 판이 지도를 덮는 폭(손잡이 포함 · `_Chrome` 이 적음) */
/* 아래 줄 반 폭(408-4) — 횡단 판 왼쪽 빈 폭을 상태 글판 · 잰 값 창이 반씩(안 겹침 · 좁으면 줄바꿈) ·
@@ -14,15 +14,18 @@
min-height: 0;
}
-/* 도구줄 — 3D 도구줄 규격 그대로 · 회전 단추도 이 줄(366-2) · 넘치면 줄바꿈. */
+/* 도구줄 — 지도 왼쪽 위(공용 `ui-map-tools` · 넘치면 줄바꿈 · 빈 자리는 지도 조작 통과) · [?] 안내 판은 그 아래. */
.b05-routeedit__tools {
+ position: absolute;
+ z-index: 2;
+ top: var(--spacing-16);
right: var(--spacing-12);
- flex-wrap: wrap;
- pointer-events: none; /* 빈 자리는 지도 조작을 통과 */
+ left: var(--spacing-16);
}
-.b05-routeedit__tools > * {
- pointer-events: auto;
+/* ☰ 들여쓰기(7-2) */
+html.is-bm-route .b05-routeedit__tools {
+ left: max(var(--spacing-16), var(--wf-burger-pad, 0px));
}
.b05-routeedit__canvas-wrap {
@@ -43,8 +46,9 @@
/* 지도 위에 얹는 판들 — 아래 정보행을 없애고 여기로 옮겼다(2026-09-12 사용자 지시 ⑩·⑫·⑬).
글판은 **클릭을 통과시킨다** — 지도 조작을 가리면 안 된다(접기 단추만 받음 · 345-5 덧 3). */
-.b05-routeedit__hint-body,
.b05-routeedit__info {
+ position: absolute;
+ z-index: 1;
border: 1px solid color-mix(in srgb, var(--color-border) 65%, transparent);
border-radius: var(--radius-cards);
background: color-mix(in srgb, var(--color-surface-raised) 72%, transparent);
@@ -55,41 +59,6 @@
pointer-events: none;
}
-.b05-routeedit__hint,
-.b05-routeedit__info {
- position: absolute;
- z-index: 1;
-}
-
-/* 조작 설명 — 도구줄 아래 왼쪽 · 기본 접힘 · [?] 로 펼침(345-5 덧 3 · 352-1) · 펼치면 **2열**(사용자 지시 ⑩). */
-.b05-routeedit__hint {
- top: calc(var(--spacing-16) + 32px + var(--spacing-8));
- left: var(--spacing-16);
- display: flex;
- align-items: flex-start;
- gap: var(--spacing-8);
- max-width: 60%;
- pointer-events: none;
-}
-
-.b05-routeedit__hint-toggle.ui-btn[data-size] {
- flex: none;
- padding-inline: var(--spacing-8);
- font-weight: 600;
- pointer-events: auto;
-}
-
-.b05-routeedit__hint-body {
- display: none;
- grid-template-columns: auto auto;
- gap: 2px var(--spacing-16);
- padding: var(--spacing-8) var(--spacing-12);
-}
-
-.b05-routeedit__hint.is-open > .b05-routeedit__hint-body {
- display: grid;
-}
-
/* 상태 · 범례 — 지도 왼쪽 아래(사용자 지시 ⑫) · 뜬 종단 판이 덮으면 그 위로(408-2 · `_Chrome` 이 적음).
두 줄 안 · 좁은 폭(345-5 덧 3) — 왼쪽 상태 글(두 줄에서 자름) · 가운데 범례 두 줄 · 오른쪽 ▾. */
.b05-routeedit__info {
@@ -365,7 +334,7 @@
}
/* 분석 지형이 숨으면 앞 구분선도(390-1) · 스냅 끔이면 간격 고르기 흐림(390-2). */
-.b05-route__view-separator:has(+ .b05-routeedit__terrain[hidden]) {
+.ui-toolbar__divider:has(+ .b05-routeedit__terrain[hidden]) {
display: none;
}
diff --git a/resources/tester/helper_345_2_3d_modal.cjs b/resources/tester/helper_345_2_3d_modal.cjs
index c0732987d..fae800bb0 100644
--- a/resources/tester/helper_345_2_3d_modal.cjs
+++ b/resources/tester/helper_345_2_3d_modal.cjs
@@ -52,10 +52,11 @@ global.document = { createElement: (tag) => new FakeNode(tag) };
const modals = [];
const STUBS = {
- "@ui/ui_template_elements": {
- createButton: ({ label, onClick }) => {
+ "@ui/ui_template_map_tools": {
+ mapToolButton: (act, { title, onClick }) => {
const button = new FakeNode("button");
- button.label = label;
+ button.act = act;
+ button.title = title;
if (onClick) button.addEventListener("click", onClick);
return button;
},
diff --git a/resources/tester/test_345_5c_info_hint_collapse.py b/resources/tester/test_345_5c_info_hint_collapse.py
index dec4d6269..01edf58ae 100644
--- a/resources/tester/test_345_5c_info_hint_collapse.py
+++ b/resources/tester/test_345_5c_info_hint_collapse.py
@@ -28,15 +28,13 @@ def _rule(selector: str) -> str:
def test_도움말_기본_접힘_펼침_기억() -> None:
assert "let hintOpen = false;" in CHROME # 기본 접힘 · 모듈 값 = 페이지 동안 기억
- assert 'data-act="hint-toggle" aria-expanded="false"' in CHROME and ">?" in CHROME
- assert "hintOpen = open;" in CHROME and 'hint.classList.toggle("is-open", open);' in CHROME
- assert "setHint(hintOpen);" in CHROME
- assert 'hintToggle.addEventListener("click", () => setHint(!hintOpen));' in CHROME
- # 접힘 = 몸통 없음 · 펼침 = 2열 · 단추만 클릭 받음
- assert "display: none;" in _rule(".b05-routeedit__hint-body")
- assert "display: grid;" in _rule(".b05-routeedit__hint.is-open > .b05-routeedit__hint-body")
- assert "pointer-events: auto;" in _rule(".b05-routeedit__hint-toggle.ui-btn[data-size]")
- assert "pointer-events: none;" in _rule(".b05-routeedit__hint")
+ # 568-2 — [?] 단추 · 안내 판 = 공용 지도 도구줄(접힘 · 2열 · 클릭 통과는 test_568_1)
+ assert (
+ "hint: { lines: HINT_LINES, open: hintOpen, onToggle: (open) => (hintOpen = open) }"
+ in CHROME
+ )
+ assert 'mapTools.hint!.classList.add("b05-routeedit__hint")' in CHROME # 가이드 촬영 선택자
+ assert '"Ctrl+휠 = 돌리기(한 칸 5°)",' in CHROME
def test_상태_범례_접기_한_줄_기억() -> None:
@@ -63,7 +61,8 @@ def test_상태_글판_작게_반투명_자리() -> None:
# 상태 글 두 줄에서 자름 · 범례는 옆 칸 두 줄
assert "-webkit-line-clamp: 2;" in _rule(".b05-routeedit__status")
assert "flex-direction: column;" in _rule(".b05-routeedit__legend")
- glass = _rule(".b05-routeedit__hint-body,\n.b05-routeedit__info")
+ glass = CSS[CSS.index("\n.b05-routeedit__info {") :]
+ glass = glass[: glass.index("}")]
assert "color-mix(in srgb, var(--color-surface-raised) 72%, transparent)" in glass
assert not re.search(r"#[0-9a-fA-F]{3,6}\b", glass) # 바탕 · 글 색은 토큰
diff --git a/resources/tester/test_346_2_footprint_draw.py b/resources/tester/test_346_2_footprint_draw.py
index c5c105ba4..ea72830c3 100644
--- a/resources/tester/test_346_2_footprint_draw.py
+++ b/resources/tester/test_346_2_footprint_draw.py
@@ -104,7 +104,7 @@ def test_편집기가_발자국을_그리고_단추로_켬끔() -> None:
# 등고선 뒤 · 노선 앞(노선이 발자국 위에 보임)
assert render.index("drawFootprint(context") < render.index("scene.expected,")
chrome = (B05 / "B05_Profile_UI_RouteEdit_Chrome.ts").read_text(encoding="utf-8")
- assert 'data-act="footprint"' in chrome and "bindFootprintToggle(" in chrome
+ assert 'mapToolButton("footprint"' in chrome and "bindFootprintToggle(" in chrome
footprint = (B05 / "B05_Profile_UI_RouteEdit_Footprint.ts").read_text(encoding="utf-8")
assert 'subscribe("footprint"' in footprint and "let shown = true;" in footprint
css = (B05 / "B05_Profile_UI_Style_RouteEdit_Footprint.css").read_text(encoding="utf-8")
diff --git a/resources/tester/test_353_1_cut_fill_line_smooth.py b/resources/tester/test_353_1_cut_fill_line_smooth.py
index 2be252fc6..d8d4094fe 100644
--- a/resources/tester/test_353_1_cut_fill_line_smooth.py
+++ b/resources/tester/test_353_1_cut_fill_line_smooth.py
@@ -195,5 +195,5 @@ def test_깎기_쌓기_바뀜_자리는_도로_끝(pieces) -> None:
def test_단추_이름은_절성토선() -> None:
chrome = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Chrome.ts").read_text("utf-8")
- at = chrome.index('data-act="footprint"')
- assert ">절/성토선" in chrome[at : at + 200] and ">발자국<" not in chrome
+ at = chrome.index('mapToolButton("footprint"')
+ assert 'title: "절/성토선 — ' in chrome[at : at + 200] and "발자국" not in chrome
diff --git a/resources/tester/test_b05_routeedit_initial.py b/resources/tester/test_b05_routeedit_initial.py
index 378bb25a2..6d63e1b10 100644
--- a/resources/tester/test_b05_routeedit_initial.py
+++ b/resources/tester/test_b05_routeedit_initial.py
@@ -209,40 +209,39 @@ def test_노선_추천_다듬기_23_3():
return open(os.path.join(page, name), encoding="utf-8").read()
chrome = read("B05_Profile_UI_RouteEdit_Chrome.ts")
- # 366-2 — [?] | ↶ ↷ | 3D | ⟲ ⟳ · 1m 등고선 · 절/성토선 · 거리 재기 | 측점 스냅 · 간격 | 분석 지형(맨 오른쪽)
+ # 366-2 · 568-2 — [?] | ↶ ↷ | 3D | ⟲ ⟳ · 1m 등고선 · 측점 · 기울기 · 절/성토선 · 거리 재기 | 측점 스냅 · 간격 |
+ # 설계 지형 이름(맨 오른쪽) — 공용 지도 도구줄 묶음 차례([?] 는 hint 가 맨 앞에 붙임)
bar = chrome[
- chrome.index("b05-routeedit__tools") : chrome.index('')
+ chrome.index("createMapTools({") : chrome.index('classList.add("b05-routeedit__tools")')
]
order = [
- 'data-act="hint-toggle"',
- "|",
- 'data-act="undo"',
- 'data-act="redo"',
- "|",
- 'data-slot="tools"',
- "|",
- 'data-act="rotate-ccw"',
- 'data-act="rotate-cw"',
- 'data-act="contour-minor"',
- 'data-act="stations"', # 381-2 [측점] [기울기] = 절/성토선 왼쪽
- 'data-act="slopes"',
- 'data-act="footprint"',
- 'data-act="measure"',
- "|",
- 'data-act="station-snap"',
- 'data-act="snap-step"', # 390-2 스냅 간격 = [측점 스냅] 오른쪽
- "|", # 390-1 [측점 스냅] | 설계 지형 이름(509-5)
- 'class="b05-routeedit__terrain" data-slot="terrain-name" hidden',
+ "hint: { lines: HINT_LINES",
+ "groups: [",
+ 'mapToolButton("undo"',
+ 'mapToolButton("redo"',
+ "],",
+ "tools, // 부른 쪽 단추([3D])",
+ '"rotate-ccw",',
+ '"rotate-cw",',
+ 'mapToolButton("contour-minor"',
+ 'mapToolButton("stations"', # 381-2 [측점] [기울기] = 절/성토선 왼쪽
+ 'mapToolButton("slopes"',
+ 'mapToolButton("footprint"',
+ 'mapToolButton("measure"',
+ "],",
+ 'mapToolButton("station-snap"',
+ "snapStep,", # 390-2 스냅 간격 = [측점 스냅] 오른쪽
+ "],", # 390-1 [측점 스냅] | 설계 지형 이름(509-5)
+ "[terrainName],",
]
at = 0
for mark in order:
- key = "b05-route__view-separator" if mark == "|" else mark
- at = bar.index(key, at) + 1
- assert bar.count("b05-route__view-separator") == 5
- assert chrome.count("b05-route__view-separator") == 5
+ at = bar.index(mark, at) + 1
+ assert 'class="b05-routeedit__terrain" data-slot="terrain-name" hidden' in chrome
+ assert "b05-route__view-separator" not in chrome # 나눔선 = 공용 도구줄
# 분석 지형이 숨으면 앞 구분선도 숨김 · 스냅 끔이면 간격 고르기 흐림
style = read("B05_Profile_UI_Style_RouteEdit.css")
- assert ".b05-route__view-separator:has(+ .b05-routeedit__terrain[hidden])" in style
+ assert ".ui-toolbar__divider:has(+ .b05-routeedit__terrain[hidden])" in style
assert '[data-act="station-snap"][aria-pressed="false"] + .b05-routeedit__snap-step' in style
assert "b05-routeedit__spin" not in chrome # 회전 = 도구줄(추천 모달 CSS 는 남음)
shared = read("B05_Profile_UI_RouteEdit_Recommend_Map.ts")
diff --git a/ui_template/ui_template_map_tools.css b/ui_template/ui_template_map_tools.css
index 29457cbfe..d12e6fbb0 100644
--- a/ui_template/ui_template_map_tools.css
+++ b/ui_template/ui_template_map_tools.css
@@ -31,3 +31,9 @@
.ui-map-tools__hint.is-open {
display: grid;
}
+
+/* 숨김 — display 가 [hidden] 을 덮지 않게 */
+.ui-map-tools[hidden],
+.ui-map-tools__hint[hidden] {
+ display: none !important;
+}
diff --git a/ui_template/ui_template_map_tools.ts b/ui_template/ui_template_map_tools.ts
index 1c2496a6f..524b4deb8 100644
--- a/ui_template/ui_template_map_tools.ts
+++ b/ui_template/ui_template_map_tools.ts
@@ -77,9 +77,9 @@ export interface MapTools {
export function createMapTools(opts: MapToolsOptions): MapTools {
const root = document.createElement("div");
root.className = "ui-map-tools";
- const groups = opts.groups.map((group) =>
- group.map((item) => (typeof item === "string" ? mapToolButton(item) : item)),
- );
+ const groups = opts.groups
+ .filter((group) => group.length)
+ .map((group) => group.map((item) => (typeof item === "string" ? mapToolButton(item) : item)));
let hint: HTMLElement | null = null;
if (opts.hint) {
const { lines, onToggle } = opts.hint;