From 337edb3f200f419be4dfccb2f0a69c5b4e5ef445 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 14:09:13 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05=20=EB=85=B8=EC=84=A0=ED=8E=B8=EC=A7=91)?= =?UTF-8?q?:=20349=20=ED=85=8C=EB=A7=88=20=EB=B0=94=EA=BF=88=20=EB=92=A4?= =?UTF-8?q?=20=ED=8F=89=EB=A9=B4=20=EC=BA=94=EB=B2=84=EC=8A=A4=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EA=B7=B8=EB=A6=BC=20=E2=80=94=20=EB=B0=9D=EC=9D=8C?= =?UTF-8?q?=20=E2=86=94=20=EC=96=B4=EB=91=90=EC=9B=80(=EB=8B=A8=EC=B6=94?= =?UTF-8?q?=20=C2=B7=20=EC=8B=9C=EC=8A=A4=ED=85=9C)=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=EC=97=90=20=EB=8B=A4=EC=9D=8C=20=ED=94=84=EB=A0=88=EC=9E=84=20?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=20=EA=B7=B8=EB=A6=AC=EA=B8=B0=20=C2=B7=20?= =?UTF-8?q?=EB=B0=94=ED=83=95=20=C2=B7=20=EB=93=B1=EA=B3=A0=EC=84=A0=20?= =?UTF-8?q?=C2=B7=20=EB=85=B8=EC=84=A0=20=C2=B7=20=EB=B0=9C=EC=9E=90?= =?UTF-8?q?=EA=B5=AD=20=EC=83=89=20=ED=86=A0=ED=81=B0=20=EB=8B=A4=EC=8B=9C?= =?UTF-8?q?=20=EC=9D=BD=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _RouteEdit_Render.watchTheme(data-theme · prefers-color-scheme · 공용 색 캐시가 먼저 비워지게 다음 프레임) · 편집기가 붙이고 떠날 때 뗌 - 시험: test_349_routeedit_theme_redraw 3 · 관련 37 통과 · typecheck 0 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf --- B05_Profile/B05_Profile_UI_RouteEdit.ts | 4 +- .../B05_Profile_UI_RouteEdit_Render.ts | 20 +++++++++ .../tester/test_349_routeedit_theme_redraw.py | 45 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 resources/tester/test_349_routeedit_theme_redraw.py diff --git a/B05_Profile/B05_Profile_UI_RouteEdit.ts b/B05_Profile/B05_Profile_UI_RouteEdit.ts index 584fa0aa6..842080a4e 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit.ts @@ -42,7 +42,7 @@ import { type EditedCurve, type EditedNode, } from "./logic/B05_Profile_UI_RouteEdit_Curve"; -import { drawRouteEditScene, polylineLengthM } from "./B05_Profile_UI_RouteEdit_Render"; +import { drawRouteEditScene, polylineLengthM, watchTheme } from "./B05_Profile_UI_RouteEdit_Render"; import { bindNodeSplice, bindRouteEditNavigation } from "./B05_Profile_UI_RouteEdit_Input"; import { bindRouteEditPointer } from "./B05_Profile_UI_RouteEdit_Pointer"; import { createCrossPreview } from "./B05_Profile_UI_RouteEdit_Cross"; @@ -271,6 +271,7 @@ export async function mountRouteEditor( viewMemory.flush(); closed = true; sizeWatch.disconnect(); + themeWatch(); historyControls.dispose(); // 단축키는 창(window)에 달려 있어 안 떼면 떠난 뒤에도 산다. curveLabelBox.destroy(); overlay.remove(); @@ -305,6 +306,7 @@ export async function mountRouteEditor( // 칸 크기를 따라감 — 창 크기 · 아래 종단 판 끌기 · 좌측 접기 모두(345-3). const sizeWatch = new ResizeObserver(() => resize()); sizeWatch.observe(overlay.querySelector(".b05-routeedit__canvas-wrap")!); + const themeWatch = watchTheme(() => draw()); // 밝음 · 어두움 바꿈 — 색 다시 읽어 그림(349). let viewWanted = false; // 자료를 읽었다 — 칸 크기가 서면 보던 화면(18 ③) · 없으면 노선 전체로. function openView(): void { if (!viewWanted || !meta || view.width <= 0) return; diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts index 11d93c8b1..22b7631ff 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts @@ -131,6 +131,26 @@ function upright( context.restore(); } +/** 테마 바꿈(`data-theme` · 시스템 밝음/어두움)마다 부름(349) — 그리기가 그릴 때마다 토큰을 + * 다시 읽으므로 다시 그리기만 하면 바탕 · 등고선 · 노선 · 발자국 색이 바뀜. 돌려준 함수로 뗌. */ +export function watchTheme(onChange: () => void): () => void { + // 다음 프레임에 — 공용 색 캐시(`themeColor`)가 같은 알림에서 먼저 비워지게. + let frame = 0; + const later = (): void => { + cancelAnimationFrame(frame); + frame = requestAnimationFrame(onChange); + }; + const observer = new MutationObserver(later); + observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] }); + const system = window.matchMedia("(prefers-color-scheme: dark)"); + system.addEventListener("change", later); + return () => { + cancelAnimationFrame(frame); + observer.disconnect(); + system.removeEventListener("change", later); + }; +} + export function drawRouteEditScene(context: CanvasRenderingContext2D, scene: RouteEditScene): void { const { view, toScreen } = scene; const style = getComputedStyle(document.documentElement); diff --git a/resources/tester/test_349_routeedit_theme_redraw.py b/resources/tester/test_349_routeedit_theme_redraw.py new file mode 100644 index 000000000..a7f52a79c --- /dev/null +++ b/resources/tester/test_349_routeedit_theme_redraw.py @@ -0,0 +1,45 @@ +"""349 — 밝음 · 어두움을 바꾸면 노선 편집 평면 캔버스를 다시 그림(바탕 · 등고선 · 노선 · 발자국 색). + +그리기는 그릴 때마다 테마 토큰을 다시 읽음 — 테마 바꿈 알림에 다시 그리기만 붙었는지 글로 봄. +./venv/Scripts/python.exe -m pytest resources/tester/test_349_routeedit_theme_redraw.py -q +""" + +from __future__ import annotations + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +B05 = ROOT / "B05_Profile" +RENDER = (B05 / "B05_Profile_UI_RouteEdit_Render.ts").read_text(encoding="utf-8") +EDITOR = (B05 / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8") + + +def _body(text: str, head: str) -> str: + at = text.index(head) + return text[at : text.index("\n}", at)] + + +def test_테마_알림_받아_다음_프레임에_다시_그림() -> None: + watch = _body(RENDER, "export function watchTheme(") + assert 'attributeFilter: ["data-theme"]' in watch # 단추로 바꿈 + assert 'matchMedia("(prefers-color-scheme: dark)")' in watch # 시스템 바꿈 + # 공용 색 캐시가 먼저 비워지게 다음 프레임에 · 뗄 때 모두 뗌 + assert "requestAnimationFrame(onChange)" in watch + assert "observer.disconnect();" in watch and 'removeEventListener("change", later)' in watch + + +def test_편집기가_붙이고_떠날_때_뗌() -> None: + assert "const themeWatch = watchTheme(() => draw());" in EDITOR + dispose = _body(EDITOR, "const dispose = (): void => {") + assert "themeWatch();" in dispose + assert len(EDITOR.splitlines()) <= 700 + + +def test_그릴_때마다_토큰을_다시_읽음() -> None: + scene = _body(RENDER, "export function drawRouteEditScene(") + assert "const style = getComputedStyle(document.documentElement);" in scene + assert 'style.getPropertyValue("--color-surface")' in scene # 바탕 + assert 'style.getPropertyValue("--map-contour-major")' in scene # 등고선 + assert 'style.getPropertyValue("--map-route")' in scene # 노선 + footprint = (B05 / "B05_Profile_UI_RouteEdit_Footprint.ts").read_text(encoding="utf-8") + assert "context.strokeStyle = style.getPropertyValue(token).trim() || fallback;" in footprint