feat(B05 노선편집): 345-5 덧 3 상태 · 범례 · 도움말 글판 접기 — 상태 · 범례 글판 작게(두 줄 · 폭 460 px 안 · 반투명 토큰 바탕) · ▾ 접으면 한 줄 「계획노선 N m」 · 도움말 기본 접힘 · [?] 펼침 · 접힘 기억 페이지 동안
- _RouteEdit_Chrome: 접기 단추 둘 · 모듈 값 기억 · 상태 글 바뀌면 요약 글 다시 씀(MutationObserver) - Style_RouteEdit: 글판 격자(상태 두 줄 자름 · 범례 두 줄 · ▾) · 접기 단추만 클릭 받음 - 시험: test_345_5c_info_hint_collapse 4 · 관련 62 통과 · typecheck 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
This commit is contained in:
@@ -45,6 +45,10 @@ export interface RouteEditChrome {
|
||||
terrainSelect: HTMLSelectElement;
|
||||
}
|
||||
|
||||
// 글판 접기 기억(345-5 덧 3) — 페이지 동안만(모듈 값) · 다시 열어도 그대로.
|
||||
let hintOpen = false;
|
||||
let infoCollapsed = false;
|
||||
|
||||
/** 뼈대를 만들어 `parent` 에 붙이고, 자주 쓰는 요소를 집어 돌려준다. */
|
||||
export function createRouteEditChrome(
|
||||
parent: HTMLElement,
|
||||
@@ -90,10 +94,14 @@ export function createRouteEditChrome(
|
||||
<div class="b05-routeedit__canvas-wrap">
|
||||
<canvas class="b05-routeedit__canvas"></canvas>
|
||||
<div class="b05-routeedit__hint">
|
||||
<button type="button" class="ui-btn ui-btn--glass b05-routeedit__hint-toggle"
|
||||
data-act="hint-toggle" aria-expanded="false" title="조작 도움말 펼치기 · 접기">?</button>
|
||||
<div class="b05-routeedit__hint-body">
|
||||
<span>노드 끌기 = 옮기기</span><span>노드 클릭 = R 라벨</span>
|
||||
<span>선 두 번 클릭 = 노드 추가</span><span>노드 오른쪽 클릭 = 삭제</span>
|
||||
<span>측점 눈금 클릭 = 횡단 미리보기</span><span>Shift+클릭 = 거리·기울기</span>
|
||||
<span>가운데(휠) 버튼 끌기 = 지도 이동</span><span>휠 = 확대</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="b05-routeedit__spin">
|
||||
<button type="button" class="ui-btn ui-btn--glass" data-act="rotate-ccw"
|
||||
@@ -108,9 +116,12 @@ export function createRouteEditChrome(
|
||||
<div class="b05-routeedit__info">
|
||||
<span class="b05-routeedit__status">노선을 읽는 중…</span>
|
||||
<span class="b05-routeedit__legend">
|
||||
<i class="is-expected"></i> 예상노선(원본)
|
||||
<i class="is-planned"></i> 계획노선
|
||||
<span><i class="is-expected"></i> 예상노선(원본)</span>
|
||||
<span><i class="is-planned"></i> 계획노선</span>
|
||||
</span>
|
||||
<span class="b05-routeedit__summary"></span>
|
||||
<button type="button" class="b05-routeedit__info-toggle" data-act="info-toggle"
|
||||
aria-expanded="true" title="상태 · 범례 글판 접기 · 펴기">▾</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="b05-routeedit__busy" hidden><span></span></div>
|
||||
@@ -133,6 +144,30 @@ export function createRouteEditChrome(
|
||||
setSide(window.innerWidth >= 1500);
|
||||
sideToggle.addEventListener("click", () => setSide(sideWrap.classList.contains("is-collapsed")));
|
||||
|
||||
// 도움말 = 기본 접힘 · [?] 한 번에 펼침(345-5 덧 3).
|
||||
const hint = overlay.querySelector<HTMLElement>(".b05-routeedit__hint")!;
|
||||
const hintToggle = hint.querySelector<HTMLButtonElement>('[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<HTMLElement>(".b05-routeedit__info")!;
|
||||
const infoToggle = info.querySelector<HTMLButtonElement>('[data-act="info-toggle"]')!;
|
||||
const summary = info.querySelector<HTMLElement>(".b05-routeedit__summary")!;
|
||||
const setInfo = (collapsed: boolean): void => {
|
||||
infoCollapsed = collapsed;
|
||||
info.classList.toggle("is-collapsed", collapsed);
|
||||
infoToggle.textContent = collapsed ? "▸" : "▾";
|
||||
infoToggle.setAttribute("aria-expanded", String(!collapsed));
|
||||
};
|
||||
setInfo(infoCollapsed);
|
||||
infoToggle.addEventListener("click", () => setInfo(!infoCollapsed));
|
||||
|
||||
const measureCloseButton = createButton({ label: "✕", variant: "ghost" });
|
||||
measureCloseButton.classList.add("b05-routeedit__measure-close");
|
||||
measureCloseButton.setAttribute("aria-label", "닫기");
|
||||
@@ -141,6 +176,13 @@ export function createRouteEditChrome(
|
||||
|
||||
const canvas = overlay.querySelector<HTMLCanvasElement>(".b05-routeedit__canvas")!;
|
||||
const status = overlay.querySelector<HTMLElement>(".b05-routeedit__status")!;
|
||||
const syncSummary = (): void => {
|
||||
const text = status.textContent ?? "";
|
||||
const length = /계획노선 ([\d.]+)m/.exec(text)?.[1];
|
||||
summary.textContent = length ? `계획노선 ${length} m` : text;
|
||||
};
|
||||
new MutationObserver(syncSummary).observe(status, { childList: true, characterData: true });
|
||||
syncSummary();
|
||||
const measureBox = overlay.querySelector<HTMLElement>(".b05-routeedit__measure")!;
|
||||
const measureText = overlay.querySelector<HTMLElement>(".b05-routeedit__measure-text")!;
|
||||
const measureButton = overlay.querySelector<HTMLButtonElement>('[data-act="measure"]')!;
|
||||
|
||||
@@ -67,15 +67,12 @@
|
||||
}
|
||||
|
||||
/* 지도 위에 얹는 판들 — 아래 정보행을 없애고 여기로 옮겼다(2026-09-12 사용자 지시 ⑩·⑫·⑬).
|
||||
글판은 **클릭을 통과시킨다** — 지도 조작을 가리면 안 된다. */
|
||||
.b05-routeedit__hint,
|
||||
글판은 **클릭을 통과시킨다** — 지도 조작을 가리면 안 된다(접기 단추만 받음 · 345-5 덧 3). */
|
||||
.b05-routeedit__hint-body,
|
||||
.b05-routeedit__info {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 65%, transparent);
|
||||
border-radius: var(--radius-8, 6px);
|
||||
background: color-mix(in srgb, var(--color-surface-raised) 78%, transparent);
|
||||
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);
|
||||
@@ -83,25 +80,98 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 조작 설명 — **2열**로 묶는다(사용자 지시 ⑩). 한 줄로 늘어놓으면 창이 좁을 때 접힌다. */
|
||||
.b05-routeedit__hint,
|
||||
.b05-routeedit__info {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 조작 설명 — 지도 왼쪽 위 · 기본 접힘 · [?] 로 펼침(345-5 덧 3) · 펼치면 **2열**(사용자 지시 ⑩). */
|
||||
.b05-routeedit__hint {
|
||||
top: var(--spacing-12);
|
||||
left: var(--spacing-12);
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 2px var(--spacing-16);
|
||||
max-width: 52%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-8);
|
||||
max-width: 60%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 상태·범례 — 지도 왼쪽 아래(사용자 지시 ⑫) · 뜬 종단 판이 덮으면 판 바로 위(345-5 덧 ·
|
||||
`--b05-routeedit-cover` = `_RouteEdit_Cover` 가 잰 덮인 높이 · 접으면 0). */
|
||||
.b05-routeedit__hint-toggle {
|
||||
flex: none;
|
||||
padding: var(--spacing-4, 4px) var(--spacing-8);
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
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;
|
||||
}
|
||||
|
||||
/* 상태 · 범례 — 지도 왼쪽 아래(사용자 지시 ⑫) · 뜬 종단 판이 덮으면 판 바로 위(345-5 덧 ·
|
||||
`--b05-routeedit-cover` = `_RouteEdit_Cover` 가 잰 덮인 높이 · 접으면 0).
|
||||
두 줄 안 · 좁은 폭(345-5 덧 3) — 왼쪽 상태 글(두 줄에서 자름) · 가운데 범례 두 줄 · 오른쪽 ▾. */
|
||||
.b05-routeedit__info {
|
||||
bottom: calc(var(--spacing-12) + var(--b05-routeedit-cover, 0px));
|
||||
left: var(--spacing-12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
max-width: 62%;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
align-items: start;
|
||||
gap: 0 var(--spacing-12);
|
||||
max-width: min(460px, 46%);
|
||||
padding: var(--spacing-4, 4px) var(--spacing-8);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.b05-routeedit__status {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
}
|
||||
|
||||
.b05-routeedit__summary {
|
||||
display: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 접힘 = 한 줄 「계획노선 N m」 + ▸ · 기억은 페이지 동안(`_Chrome`). */
|
||||
.b05-routeedit__info.is-collapsed > .b05-routeedit__status,
|
||||
.b05-routeedit__info.is-collapsed > .b05-routeedit__legend {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.b05-routeedit__info.is-collapsed {
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
.b05-routeedit__info.is-collapsed > .b05-routeedit__summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.b05-routeedit__info-toggle {
|
||||
grid-column: -2;
|
||||
padding: 0 2px;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.b05-routeedit__info-toggle:hover {
|
||||
color: var(--color-text-body);
|
||||
}
|
||||
|
||||
/* 회전 단추 — 지도 오른쪽 위(사용자 지시 ⑯). 여기만 클릭을 받는다. */
|
||||
@@ -121,9 +191,9 @@
|
||||
}
|
||||
|
||||
.b05-routeedit__legend {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b05-routeedit__legend i {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""345-5 덧 3 — 노선 편집 평면 상태 · 범례 글판과 도움말 글판 접기.
|
||||
|
||||
`_RouteEdit_Chrome` 의 배선 · 접힘 기억 · 한 줄 요약 글 · CSS 자리를 글로 봄 ·
|
||||
요약 글 정규식은 node 로 돌림.
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_345_5c_info_hint_collapse.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
B05 = ROOT / "B05_Profile"
|
||||
CHROME = (B05 / "B05_Profile_UI_RouteEdit_Chrome.ts").read_text(encoding="utf-8")
|
||||
CSS = (B05 / "B05_Profile_UI_Style_RouteEdit.css").read_text(encoding="utf-8").replace("\r\n", "\n")
|
||||
|
||||
|
||||
def _rule(selector: str) -> str:
|
||||
at = CSS.rindex("\n" + selector + " {")
|
||||
return CSS[at : CSS.index("}", at)]
|
||||
|
||||
|
||||
def test_도움말_기본_접힘_펼침_기억() -> None:
|
||||
assert "let hintOpen = false;" in CHROME # 기본 접힘 · 모듈 값 = 페이지 동안 기억
|
||||
assert 'data-act="hint-toggle" aria-expanded="false"' in CHROME and ">?</button>" 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")
|
||||
assert "pointer-events: none;" in _rule(".b05-routeedit__hint")
|
||||
|
||||
|
||||
def test_상태_범례_접기_한_줄_기억() -> None:
|
||||
assert "let infoCollapsed = false;" in CHROME
|
||||
assert 'data-act="info-toggle"' in CHROME and 'aria-expanded="true"' in CHROME
|
||||
assert "infoCollapsed = collapsed;" in CHROME and "setInfo(infoCollapsed);" in CHROME
|
||||
assert 'infoToggle.textContent = collapsed ? "▸" : "▾";' in CHROME
|
||||
assert 'infoToggle.addEventListener("click", () => setInfo(!infoCollapsed));' in CHROME
|
||||
# 상태 글이 바뀔 때마다 요약 글을 다시 씀
|
||||
assert "new MutationObserver(syncSummary).observe(status," in CHROME
|
||||
collapsed = ".b05-routeedit__info.is-collapsed > .b05-routeedit__"
|
||||
assert "display: none;" in _rule(f"{collapsed}status,\n{collapsed}legend")
|
||||
assert "display: block;" in _rule(f"{collapsed}summary")
|
||||
assert "display: none;" in _rule(".b05-routeedit__summary")
|
||||
assert "pointer-events: auto;" in _rule(".b05-routeedit__info-toggle")
|
||||
|
||||
|
||||
def test_상태_글판_작게_반투명_자리() -> None:
|
||||
info = _rule(".b05-routeedit__info")
|
||||
# 판 바로 위(345-5 덧) 그대로 · 폭 줄임
|
||||
assert "bottom: calc(var(--spacing-12) + var(--b05-routeedit-cover, 0px));" in info
|
||||
assert "max-width: min(460px, 46%);" in info and "grid-template-columns:" in info
|
||||
# 상태 글 두 줄에서 자름 · 범례는 옆 칸 두 줄
|
||||
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")
|
||||
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) # 바탕 · 글 색은 토큰
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 없음")
|
||||
def test_요약_글은_계획노선_길이() -> None:
|
||||
pattern = re.search(r"const length = (/.+?/)\.exec", CHROME).group(1)
|
||||
samples = [
|
||||
"예상노선 1201.4m · 계획노선 1153.2m · 노드 12개 · 고친 계획노선 · LAS 등고선",
|
||||
"노선을 읽는 중…",
|
||||
]
|
||||
script = (
|
||||
f"const re = {pattern};"
|
||||
f"const out = {json.dumps(samples, ensure_ascii=False)}.map((t) => {{"
|
||||
"const m = re.exec(t)?.[1]; return m ? `계획노선 ${m} m` : t; });"
|
||||
"console.log(JSON.stringify(out));"
|
||||
)
|
||||
run = subprocess.run( # noqa: S603 S607 — 고정 node 실행
|
||||
["node", "-e", script], capture_output=True, check=True, encoding="utf-8"
|
||||
)
|
||||
assert json.loads(run.stdout) == ["계획노선 1153.2 m", "노선을 읽는 중…"]
|
||||
Reference in New Issue
Block a user