From abe6168f3f8725cd462b83dc9117c262ad29cb07 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 13:03:26 +0900 Subject: [PATCH] =?UTF-8?q?feat(B05=20=EB=85=B8=EC=84=A0=ED=8E=B8=EC=A7=91?= =?UTF-8?q?):=20345-5=20=EB=8D=A7=203=20=EC=83=81=ED=83=9C=20=C2=B7=20?= =?UTF-8?q?=EB=B2=94=EB=A1=80=20=C2=B7=20=EB=8F=84=EC=9B=80=EB=A7=90=20?= =?UTF-8?q?=EA=B8=80=ED=8C=90=20=EC=A0=91=EA=B8=B0=20=E2=80=94=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=C2=B7=20=EB=B2=94=EB=A1=80=20=EA=B8=80=ED=8C=90=20?= =?UTF-8?q?=EC=9E=91=EA=B2=8C(=EB=91=90=20=EC=A4=84=20=C2=B7=20=ED=8F=AD?= =?UTF-8?q?=20460=20px=20=EC=95=88=20=C2=B7=20=EB=B0=98=ED=88=AC=EB=AA=85?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=20=EB=B0=94=ED=83=95)=20=C2=B7=20?= =?UTF-8?q?=E2=96=BE=20=EC=A0=91=EC=9C=BC=EB=A9=B4=20=ED=95=9C=20=EC=A4=84?= =?UTF-8?q?=20=E3=80=8C=EA=B3=84=ED=9A=8D=EB=85=B8=EC=84=A0=20N=20m?= =?UTF-8?q?=E3=80=8D=20=C2=B7=20=EB=8F=84=EC=9B=80=EB=A7=90=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EC=A0=91=ED=9E=98=20=C2=B7=20[=3F]=20=ED=8E=BC?= =?UTF-8?q?=EC=B9=A8=20=C2=B7=20=EC=A0=91=ED=9E=98=20=EA=B8=B0=EC=96=B5=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=8F=99=EC=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _RouteEdit_Chrome: 접기 단추 둘 · 모듈 값 기억 · 상태 글 바뀌면 요약 글 다시 씀(MutationObserver) - Style_RouteEdit: 글판 격자(상태 두 줄 자름 · 범례 두 줄 · ▾) · 접기 단추만 클릭 받음 - 시험: test_345_5c_info_hint_collapse 4 · 관련 62 통과 · typecheck 0 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf --- .../B05_Profile_UI_RouteEdit_Chrome.ts | 46 +++++++- .../B05_Profile_UI_Style_RouteEdit.css | 110 ++++++++++++++---- .../tester/test_345_5c_info_hint_collapse.py | 86 ++++++++++++++ 3 files changed, 220 insertions(+), 22 deletions(-) create mode 100644 resources/tester/test_345_5c_info_hint_collapse.py diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts index 280d33dde..ab2d9b032 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts @@ -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(
+ +
노드 끌기 = 옮기기노드 클릭 = R 라벨 선 두 번 클릭 = 노드 추가노드 오른쪽 클릭 = 삭제 측점 눈금 클릭 = 횡단 미리보기Shift+클릭 = 거리·기울기 가운데(휠) 버튼 끌기 = 지도 이동휠 = 확대 +
@@ -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(".b05-routeedit__hint")!; + const hintToggle = hint.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"]')!; + const summary = info.querySelector(".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(".b05-routeedit__canvas")!; const status = overlay.querySelector(".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(".b05-routeedit__measure")!; const measureText = overlay.querySelector(".b05-routeedit__measure-text")!; const measureButton = overlay.querySelector('[data-act="measure"]')!; diff --git a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css index a2d8481e0..26f4fbc70 100644 --- a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css +++ b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css @@ -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 { diff --git a/resources/tester/test_345_5c_info_hint_collapse.py b/resources/tester/test_345_5c_info_hint_collapse.py new file mode 100644 index 000000000..c1684cc4c --- /dev/null +++ b/resources/tester/test_345_5c_info_hint_collapse.py @@ -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 ">?" 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", "노선을 읽는 중…"]