diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts index d55e2e80a..46bf45d9e 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts @@ -2,6 +2,7 @@ import { createButton } from "@ui/ui_template_elements"; import { createSidePanels, type SidePanels } from "@ui/ui_template_side_panels"; import { bindFootprintToggle } from "./B05_Profile_UI_RouteEdit_Footprint"; import { bindMarkToggle } from "./B05_Profile_UI_RouteEdit_Marks"; +import { infoLiftPx } from "./logic/B05_Profile_RouteEdit_InfoLift"; /* ============================================================================= * B05_Profile_UI_RouteEdit_Chrome.ts @@ -205,6 +206,24 @@ export function createRouteEditChrome( setInfo(infoCollapsed); infoToggle.addEventListener("click", () => setInfo(!infoCollapsed)); + // 뜬 종단 판(이웃 칸)이 지도 아래를 덮으면 상태 · 범례를 그 위로(408-2) — 판 높이 끌기 · 접기 · + // 창 분할(판이 다른 창으로 감 = 못 찾음)을 판 크기 관찰로 따라감. + let watchedProfile: Element | null = null; + const liftInfo = (): void => { + if (!overlay.isConnected) return; // 붙기 전 첫 알림 · 걷힌 뒤 — 판 관찰은 판과 함께 사라짐 + const profile = + overlay.closest(".design-page__main")?.querySelector(".b05-route-profile") ?? null; + if (profile && profile !== watchedProfile) lift.observe((watchedProfile = profile)); + const map = overlay.getBoundingClientRect(); + const top = profile?.isConnected ? profile.getBoundingClientRect().top : null; + overlay.style.setProperty( + "--b05-profile-cover", + `${infoLiftPx(map.bottom, map.height, top)}px`, + ); + }; + const lift = new ResizeObserver(liftInfo); + lift.observe(overlay); + const measureCloseButton = createButton({ label: "✕", variant: "ghost" }); measureCloseButton.classList.add("b05-routeedit__measure-close"); measureCloseButton.setAttribute("aria-label", "닫기"); diff --git a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css index 135c686ac..7e643f4f0 100644 --- a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css +++ b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css @@ -88,10 +88,10 @@ display: grid; } -/* 상태 · 범례 — 지도 왼쪽 아래(사용자 지시 ⑫) · 뜬 종단 판이 덮으면 가려진 채(349-0). +/* 상태 · 범례 — 지도 왼쪽 아래(사용자 지시 ⑫) · 뜬 종단 판이 덮으면 그 위로(408-2 · `_Chrome` 이 적음). 두 줄 안 · 좁은 폭(345-5 덧 3) — 왼쪽 상태 글(두 줄에서 자름) · 가운데 범례 두 줄 · 오른쪽 ▾. */ .b05-routeedit__info { - bottom: var(--spacing-12); + bottom: calc(var(--spacing-12) + var(--b05-profile-cover, 0px)); left: var(--spacing-12); display: grid; grid-template-columns: minmax(0, 1fr) auto auto; diff --git a/B05_Profile/logic/B05_Profile_RouteEdit_InfoLift.ts b/B05_Profile/logic/B05_Profile_RouteEdit_InfoLift.ts new file mode 100644 index 000000000..85671a2c8 --- /dev/null +++ b/B05_Profile/logic/B05_Profile_RouteEdit_InfoLift.ts @@ -0,0 +1,19 @@ +/* ============================================================================= + * B05_Profile/logic/B05_Profile_RouteEdit_InfoLift.ts + * 평면 상태 · 범례 올림(408-2) — 뜬 종단 판이 지도 아래를 덮은 높이만큼 위로. + * 파이썬 짝 아님. 순수 계산 — 화면 · fetch · sessionStorage 없음. 부르는 쪽은 `_UI_RouteEdit_Chrome`. + * ========================================================================== */ + +/** 상태 글판이 이 높이(px)는 지도 위에 남게 — 판을 끝까지 올려도 지도 밖으로 안 나감. */ +const KEEP_PX = 120; + +/** 올릴 높이(px) — `profileTop` = 종단 판 윗변(없음 · 접힘 · 창 분할 = null) · 지도 칸 아래 · 높이. */ +export function infoLiftPx( + mapBottom: number, + mapHeight: number, + profileTop: number | null, +): number { + if (profileTop === null) return 0; + const cover = Math.max(0, mapBottom - profileTop); + return Math.round(Math.min(cover, Math.max(0, mapHeight - KEEP_PX))); +} diff --git a/resources/tester/helper_408_2_status_lift.cjs b/resources/tester/helper_408_2_status_lift.cjs new file mode 100644 index 000000000..887add89c --- /dev/null +++ b/resources/tester/helper_408_2_status_lift.cjs @@ -0,0 +1,33 @@ +/* 408-2 평면 상태줄 올림 — `logic/B05_Profile_RouteEdit_InfoLift.ts` 를 Node 로 돌려 JSON 한 줄 + * (`test_408_2_status_lift.py` 가 판정). + * node helper_408_2_status_lift.cjs + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const file = path.join( + __dirname, + "..", + "..", + "B05_Profile", + "logic", + "B05_Profile_RouteEdit_InfoLift.ts", +); +const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, +}).outputText; +const box = { exports: {} }; +new Function("exports", "module", "require", js)(box.exports, box, require); +const { infoLiftPx } = box.exports; + +// 화면 1078 px · 지도 칸 0~1078 +process.stdout.write( + JSON.stringify({ + open: infoLiftPx(1078, 1078, 430), // 종단 판 열림(648 px) + collapsed: infoLiftPx(1078, 1078, 1077), // 접힘(테두리 1 px) + popout: infoLiftPx(1078, 1078, null), // 창 분할 = 판 없음 + below: infoLiftPx(600, 600, 700), // 횡단 보기 = 판이 지도 아래 흐름 + tall: infoLiftPx(1078, 1078, 20), // 판을 끝까지 올림 = 지도 안에 남김 + }), +); diff --git a/resources/tester/test_345_5c_info_hint_collapse.py b/resources/tester/test_345_5c_info_hint_collapse.py index fb674d3b4..70d0c8b9d 100644 --- a/resources/tester/test_345_5c_info_hint_collapse.py +++ b/resources/tester/test_345_5c_info_hint_collapse.py @@ -56,8 +56,8 @@ def test_상태_범례_접기_한_줄_기억() -> None: def test_상태_글판_작게_반투명_자리() -> None: info = _rule(".b05-routeedit__info") - # 왼쪽 아래 제자리 — 뜬 판이 덮으면 가려진 채(349-0) · 폭 줄임 - assert "bottom: var(--spacing-12);" in info + # 왼쪽 아래 제자리 — 뜬 판이 덮으면 그 위로(408-2) · 폭 줄임 + assert "bottom: calc(var(--spacing-12) + var(--b05-profile-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") diff --git a/resources/tester/test_408_2_status_lift.py b/resources/tester/test_408_2_status_lift.py new file mode 100644 index 000000000..2d8bd3f49 --- /dev/null +++ b/resources/tester/test_408_2_status_lift.py @@ -0,0 +1,47 @@ +"""408-2 창 나눔(종단 판 열림) 때 평면 상태줄이 종단 표 밑에 깔림 — 판이 덮은 높이만큼 상태 · 범례를 +위로(`logic/B05_Profile_RouteEdit_InfoLift.ts` · `_Chrome` 이 판 크기 관찰로 `--b05-profile-cover` 적음). + +TS 셈은 `helper_408_2_status_lift.cjs`(Node). + + ./venv/Scripts/python.exe -m pytest resources/tester/test_408_2_status_lift.py -q +""" + +import json +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +B05 = ROOT / "B05_Profile" +HELPER = Path(__file__).with_name("helper_408_2_status_lift.cjs") + + +@pytest.fixture(scope="module") +def got() -> dict: + run = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", str(HELPER)], capture_output=True, text=True, encoding="utf-8", timeout=60 + ) + assert run.returncode == 0, run.stderr + return json.loads(run.stdout) + + +def test_올릴_높이(got) -> None: + assert got["open"] == 648 # 판 윗변 위로 + assert got["collapsed"] == 1 and got["popout"] == 0 and got["below"] == 0 + assert got["tall"] == 1078 - 120 # 지도 안에 120 px 남김 + + +def test_상태_글판이_올림_값을_씀() -> None: + css = (B05 / "B05_Profile_UI_Style_RouteEdit.css").read_text(encoding="utf-8") + block = css[css.index(".b05-routeedit__info {\n bottom") :] + assert block.startswith( + ".b05-routeedit__info {\n bottom: calc(var(--spacing-12) + var(--b05-profile-cover, 0px));" + ) + + +def test_편집기가_판_크기를_관찰() -> None: + chrome = (B05 / "B05_Profile_UI_RouteEdit_Chrome.ts").read_text(encoding="utf-8") + assert "new ResizeObserver(liftInfo)" in chrome and "lift.observe(overlay)" in chrome + assert '"--b05-profile-cover"' in chrome and ".b05-route-profile" in chrome + assert "lift.disconnect" not in chrome # 붙기 전 첫 알림에 끊으면 영영 안 올라감