feat(ui_template): 지도 도구줄 부품 · 아이콘 여덟 · 부품 모음 견본 (PLAN 568-1 · 요구 395-1)

- ui_template_map_tools.ts · .css — 아이콘 단추 한 벌(차례 · 나눔선 · 켬끔 aria-pressed · [?] 안내 판)
- 아이콘 더함 — ripple · tallymarks · percentage · magnet · satellite · droplet · borderAll · angle(Tabler MIT)
- 부품 모음 틀 묶음에 「지도 도구줄」 견본 · 시험 test_568_1 · test_552_4 목록에 map-tools

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 06:59:33 +09:00
co-authored by Claude Opus 5.5
parent 7bc33ded6e
commit 38c40f70bc
7 changed files with 274 additions and 0 deletions
+1
View File
@@ -25,6 +25,7 @@ PARTS = {
"float-panel": ("ui_template_float_panel", "createFloatPanel", "ui-float-panel", (32,)),
"drawer": ("ui_template_side_panels", "createSidePanels", "ui-side-panels", ()),
"toolbar": ("ui_template_toolbar", "createToolbar", "ui-toolbar", (24, 28, 32)),
"map-tools": ("ui_template_map_tools", "createMapTools", "ui-map-tools", ()),
"steps": ("ui_template_workflow_layout", "createStepBar", "ui-workflow-layout__step", (28, 40)),
"panel-head": ("ui_template_panel_head", "createPanelHead", "ui-panel-head", (32,)),
"list-row": ("ui_template_list_row", "createListRow", "ui-list-row", (28, 32, 36)),
+81
View File
@@ -0,0 +1,81 @@
"""568-1 지도 도구줄 부품 지킴 시험 — 화면 확인은 부품 모음(`#/b01-account?view=ui-kit`) · ORCA.
· 단추 목록의 아이콘이 모두 아이콘 묶음에 있음 · 새 아이콘 여덟(도구 넷 · 겹 넷)
· 켬끔 단추 = aria-pressed · [?] = data-act="hint-toggle" + aria-expanded · 안내 판 기본 접힘
./venv/Scripts/python.exe -m pytest resources/tester/test_568_1_map_tools.py -q
"""
from __future__ import annotations
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
UI = ROOT / "ui_template"
TS = (UI / "ui_template_map_tools.ts").read_text(encoding="utf-8")
ICONS = (UI / "ui_template_icons.ts").read_text(encoding="utf-8")
CSS = (UI / "ui_template_map_tools.css").read_text(encoding="utf-8")
def _icon_names() -> set[str]:
body = ICONS[ICONS.index("const PATHS") : ICONS.index("} as const")]
return set(re.findall(r"^ (\w+):", body, re.M))
def test_새_아이콘_여덟():
names = _icon_names()
for n in (
"ripple",
"tallymarks",
"percentage",
"magnet",
"satellite",
"droplet",
"borderAll",
"angle",
):
assert n in names, n
def test_단추_목록_아이콘_차례():
table = TS[TS.index("export const MAP_TOOLS") : TS.index("satisfies")]
rows = re.findall(
r'^ "?([\w-]+)"?: \{ icon: "(\w+)", title: "[^"]+"(, toggle: true)? \}', table, re.M
)
acts = [a for a, _, _ in rows]
assert acts == [
"undo",
"redo",
"view3d",
"rotate-ccw",
"rotate-cw",
"contour-minor",
"stations",
"slopes",
"footprint",
"measure",
"station-snap",
]
names = _icon_names()
assert all(icon in names for _, icon, _ in rows)
toggles = {a for a, _, t in rows if t}
assert toggles == {
"contour-minor",
"stations",
"slopes",
"footprint",
"measure",
"station-snap",
}
def test_켬끔_안내_판():
assert "pressed: tool.toggle ? (opts.pressed ?? false) : undefined" in TS
assert "button.dataset.act = act;" in TS
assert 'toggle.dataset.act = "hint-toggle";' in TS
assert 'toggle.setAttribute("aria-expanded", String(open));' in TS
assert "setOpen(opts.hint.open ?? false);" in TS # 기본 접힘
hint = CSS[CSS.index(".ui-map-tools__hint {") :]
assert "display: none;" in hint[: hint.index("}")]
assert "display: grid;" in CSS[CSS.index(".ui-map-tools__hint.is-open {") :]
assert "pointer-events: none;" in CSS # 빈 자리 · 글판은 지도 조작 통과
+15
View File
@@ -123,6 +123,21 @@ const PATHS = {
mail: "M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10|M3 7l9 6l9 -6",
inbox:
"M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12|M4 13h3l3 3h4l3 -3h3",
// 568-1 — 지도 도구(1m 등고선 · 측점 · 기울기 · 측점 스냅)
ripple: "M3 7c3 -2 6 -2 9 0s6 2 9 0|M3 17c3 -2 6 -2 9 0s6 2 9 0|M3 12c3 -2 6 -2 9 0s6 2 9 0",
tallymarks: "M6 5l0 14|M10 5l0 14|M14 5l0 14|M18 5l0 14|M3 17l18 -10",
percentage: "M16 17a1 1 0 1 0 2 0a1 1 0 0 0 -2 0|M6 7a1 1 0 1 0 2 0a1 1 0 0 0 -2 0|M6 18l12 -12",
magnet:
"M4 13v-8a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v8a2 2 0 0 0 6 0v-8a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v8a8 8 0 0 1 -16 0|M4 8l5 0|M15 8l4 0",
// 568-1 — 지도 겹(위성 · 세류선 · 지적도 · 급경사)
satellite:
"M3.707 6.293l2.586 -2.586a1 1 0 0 1 1.414 0l5.586 5.586a1 1 0 0 1 0 1.414l-2.586 2.586a1 1 0 0 1 -1.414 0l-5.586 -5.586a1 1 0 0 1 0 -1.414z|M6 10l-3 3l3 3l3 -3|M10 6l3 -3l3 3l-3 3|M12 12l1.5 1.5|M14.5 17a2.5 2.5 0 0 0 2.5 -2.5|M15 21a6 6 0 0 0 6 -6",
droplet:
"M7.502 19.423c2.602 2.105 6.395 2.105 8.996 0c2.602 -2.105 3.262 -5.708 1.566 -8.546l-4.89 -7.26c-.42 -.625 -1.287 -.803 -1.936 -.397a1.376 1.376 0 0 0 -.41 .397l-4.893 7.26c-1.695 2.838 -1.035 6.441 1.567 8.546z",
borderAll:
"M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z|M4 12l16 0|M12 4l0 16",
angle:
"M21 19h-18l9 -15|M20.615 15.171h.015|M19.515 11.771h.015|M17.715 8.671h.015|M15.415 5.971h.015",
} as const;
export type IconName = keyof typeof PATHS;
+33
View File
@@ -0,0 +1,33 @@
/* 지도 화면 제어 도구줄(568-1) — 도구줄 + 아래 [?] 안내 판 · 자리는 페이지가 줌 · 빈 자리는 지도 조작 통과 */
.ui-map-tools {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-8);
pointer-events: none;
}
.ui-map-tools > .ui-toolbar {
max-width: 100%;
pointer-events: auto;
}
/* 안내 판 — 기본 접힘 · 2열 · 글판은 클릭 통과 */
.ui-map-tools__hint {
display: none;
grid-template-columns: auto auto;
gap: 2px var(--spacing-16);
max-width: 100%;
padding: var(--spacing-8) var(--spacing-12);
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);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
color: var(--color-text-secondary);
font-size: var(--text-caption);
}
.ui-map-tools__hint.is-open {
display: grid;
}
+119
View File
@@ -0,0 +1,119 @@
/* =============================================================================
* ui_template_map_tools.ts
* 지도 화면 제어 도구줄(568-1) — 평면 지도 위 아이콘 단추 한 벌 · B05 평면 · D03 평면이 같이 씀.
* 꼴 = 유리 도구줄(`createToolbar` glass · 32) 한 줄 · 묶음 사이 나눔선 · 단추는 모두 아이콘(마우스 올리면 이름)
* [?] 안내 판 = 도구줄 아래 2열 글판(기본 접힘 · 클릭은 지도로 통과)
* 크기 = 단 32 하나(지도 위 도구)
* 상태 = 켬끔 단추는 `aria-pressed`(눌림 꼴) · 막힘 `disabled` · [?] 는 `aria-expanded`
* 쓰는 법 = createMapTools({ label: "평면 도구", groups: [["undo", "redo"], ["rotate-ccw", "rotate-cw", "measure"]],
* hint: { lines: ["휠 = 확대"] } }) → { root, bar, hint, button(act) }
* 페이지 문구가 다르면 `mapToolButton("measure", { title })` 를 묶음에 그대로 끼움 · 고르기 · 칩도 요소로 끼움
* 누름 동작은 페이지가 붙임(`onClick` 또는 `[data-act]` 로 찾아 붙임) · 자리는 페이지 CSS(root)
* 대신하는 옛 변종 = B05 `b05-route__view-controls` 글자 유리 단추 줄 · `b05-routeedit__hint` 안내 판 · D03 `tool()` 지역 함수
* ========================================================================== */
import { createIconButton } from "./ui_template_button";
import type { IconName } from "./ui_template_icons";
import { createToolbar } from "./ui_template_toolbar";
import "./ui_template_map_tools.css";
/** 단추 목록 — 차례 = 도구줄 차례 · toggle = 켬끔 단추(aria-pressed) */
export const MAP_TOOLS = {
undo: { icon: "undo", title: "되돌리기 (Ctrl+Z)" },
redo: { icon: "redo", title: "다시하기 (Ctrl+Y)" },
view3d: { icon: "cube", title: "3D 보기" },
"rotate-ccw": { icon: "rotateCcw", title: "반시계로 돌리기" },
"rotate-cw": { icon: "rotateCw", title: "시계로 돌리기" },
"contour-minor": { icon: "ripple", title: "1m 등고선", toggle: true },
stations: { icon: "tallymarks", title: "측점", toggle: true },
slopes: { icon: "percentage", title: "기울기", toggle: true },
footprint: { icon: "road", title: "절/성토선", toggle: true },
measure: { icon: "ruler", title: "거리 재기", toggle: true },
"station-snap": { icon: "magnet", title: "측점 스냅", toggle: true },
} satisfies Record<string, { icon: IconName; title: string; toggle?: boolean }>;
export type MapToolAct = keyof typeof MAP_TOOLS;
export interface MapToolButtonOptions {
/** 마우스 글 — 없으면 목록 이름 */
title?: string;
/** 켬끔 단추 처음 값(기본 false) */
pressed?: boolean;
disabled?: boolean;
onClick?: (ev: MouseEvent) => void;
}
/** 아이콘 단추 하나(`data-act` = act) */
export function mapToolButton(act: MapToolAct, opts: MapToolButtonOptions = {}): HTMLButtonElement {
const tool: { icon: IconName; title: string; toggle?: boolean } = MAP_TOOLS[act];
const button = createIconButton({
icon: tool.icon,
title: opts.title ?? tool.title,
size: 32,
pressed: tool.toggle ? (opts.pressed ?? false) : undefined,
disabled: opts.disabled,
onClick: opts.onClick,
});
button.dataset.act = act;
return button;
}
export interface MapToolsOptions {
/** 묶음들 — 사이에 나눔선 · act 이름 또는 페이지가 만든 요소 */
groups: (MapToolAct | HTMLElement)[][];
/** 보조기기 이름 */
label: string;
/** [?] 안내 판(맨 앞 묶음) — 줄마다 한 칸 · 2열 */
hint?: { lines: string[]; open?: boolean; onToggle?: (open: boolean) => void };
}
export interface MapTools {
root: HTMLElement;
bar: HTMLElement;
hint: HTMLElement | null;
button(act: MapToolAct): HTMLButtonElement | null;
}
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)),
);
let hint: HTMLElement | null = null;
if (opts.hint) {
const { lines, onToggle } = opts.hint;
const toggle = createIconButton({ icon: "help", title: "조작 도움말 펼치기 · 접기", size: 32 });
toggle.dataset.act = "hint-toggle";
const panel = document.createElement("div");
panel.className = "ui-map-tools__hint";
for (const line of lines)
panel.append(Object.assign(document.createElement("span"), { textContent: line }));
const setOpen = (open: boolean): void => {
panel.classList.toggle("is-open", open);
toggle.setAttribute("aria-expanded", String(open));
};
setOpen(opts.hint.open ?? false);
toggle.addEventListener("click", () => {
const open = !panel.classList.contains("is-open");
setOpen(open);
onToggle?.(open);
});
groups.unshift([toggle]);
hint = panel;
}
const bar = createToolbar({
glass: true,
size: 32,
label: opts.label,
items: groups.flatMap((group, i) => (i ? ["divider" as const, ...group] : group)),
});
root.append(bar);
if (hint) root.append(hint);
return {
root,
bar,
hint,
button: (act) => root.querySelector<HTMLButtonElement>(`[data-act="${act}"]`),
};
}
+23
View File
@@ -10,6 +10,7 @@ import { createFloatPanel } from "./ui_template_float_panel";
import { createFormGrid, createPageFrame } from "./ui_template_general_layout";
import { createInputField } from "./ui_template_input";
import { createListRow } from "./ui_template_list_row";
import { createMapTools, mapToolButton } from "./ui_template_map_tools";
import { createWorkflowPanelHandle } from "./ui_template_overlay";
import { createPanelHead } from "./ui_template_panel_head";
import { createPublicFrame } from "./ui_template_public_layout";
@@ -218,6 +219,27 @@ function toolbar(): HTMLElement {
return box;
}
function mapTools(): HTMLElement {
const tools = createMapTools({
label: "평면 도구",
hint: { lines: ["휠 = 확대", "가운데 끌기 = 이동", "Ctrl+휠 = 돌리기", "Esc = 그리기 끝"] },
groups: [
["undo", "redo"],
["view3d", "rotate-ccw", "rotate-cw"],
[
"contour-minor",
"stations",
mapToolButton("slopes", { pressed: true }),
"footprint",
"measure",
],
["station-snap"],
],
});
tools.button("contour-minor")!.disabled = true;
return tools.root;
}
/** 열쇠 → 견본(상태별 · null = 그 상태 없음) */
export const FRAME_DEMOS: Record<string, (state: KitState) => HTMLElement | null> = {
"work-frame": (s) => (s === "default" ? workFrame() : null),
@@ -226,6 +248,7 @@ export const FRAME_DEMOS: Record<string, (state: KitState) => HTMLElement | null
"float-panel": (s) => (s === "default" ? floatPanel() : null),
drawer: (s) => (s === "default" ? drawer() : null),
toolbar: (s) => (s === "default" ? toolbar() : null),
"map-tools": (s) => (s === "default" ? mapTools() : null),
steps,
"panel-head": (s) => (s === "default" ? panelHead() : null),
"list-row": listRow,
+2
View File
@@ -363,6 +363,8 @@ export const KIT_PARTS: KitPart[] = [
demo: FRAME_DEMOS["drawer"] },
{ key: "toolbar", name: "도구줄", group: "frame", kinds: "단추 묶음 · 나눔선 · 밀기 · 유리 · 24 · 28 · 32", file: "ui_template_toolbar.ts",
demo: FRAME_DEMOS["toolbar"] },
{ key: "map-tools", name: "지도 도구줄", group: "frame", kinds: "아이콘 단추 한 벌 · 묶음 나눔선 · 켬끔(aria-pressed) · [?] 안내 판 · 페이지가 쓸 단추만", file: "ui_template_map_tools.ts",
demo: FRAME_DEMOS["map-tools"] },
{ key: "steps", name: "단계 줄", group: "frame", kinds: "가로 막대 · compact(28) · 세로 목록(40 · ☰ 서랍)", file: "ui_template_workflow_layout.ts",
demo: FRAME_DEMOS.steps },
{ key: "panel-head", name: "패널 머리", group: "frame", kinds: "제목 · 부제 · 오른쪽 단추 · 밑줄", file: "ui_template_panel_head.ts",