feat(383-2): 평면 교각점 방향키 — 화면 축 0.1 m · Shift ×10 · 뗄 때 한 걸음(스냅이면 맞춤 + 실제 이동 · 함께 밀린 IP)
- 새 파일 B05_Profile_UI_RouteEdit_Keys.ts(창 keydown 가로채기 · 마지막 누름 평면 안 · 고정 · 입력칸 제외 · 하한 검사) + logic/KeyNudge - 평면을 누른 뒤 방향키는 평면이 삼킴 — 종단 ▲▼ · 횡단 조정창으로 안 샘 - B06 Panel_Base lastPointerWithin 넓힘 · Pointer.ts 걸기 한 줄 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_RouteEdit_Keys.ts
|
||||
* 계획노선 편집 평면의 **교각점 방향키**(383-2 · 요구 241-16) — 고른 IP 를 화면 축으로 0.1 m
|
||||
* (Shift 1 m)씩. 누르는 동안 옮기기만 · 뗄 때 되돌리기 한 걸음(측점 스냅 켬이면 그때 MC 맞춤).
|
||||
*
|
||||
* 받는 때 = 마지막 누름이 평면 편집 창(`.b05-routeedit`) 안 · 입력칸 아님. 그때 방향키는 평면이
|
||||
* 삼킴(IP 를 안 골랐어도) — 종단 계획고 ▲▼ · 횡단 조정창으로 새지 않음. 하한 검사 · 놓을 때 할
|
||||
* 일은 끌기(`_Pointer`)와 같음. 걸음 · 결과 한 줄 셈은 `logic/B05_Profile_RouteEdit_KeyNudge`.
|
||||
* ========================================================================== */
|
||||
|
||||
import { lastPointerWithin } from "../B06_Section/B06_Section_UI_Cross_Panel_Base";
|
||||
import { HOLD_DELAY_MS, HOLD_INTERVAL_MS } from "./B05_Profile_UI_Profile_Edit";
|
||||
import type { RouteEditPointerParams } from "./B05_Profile_UI_RouteEdit_Pointer";
|
||||
import {
|
||||
arrowStep,
|
||||
isArrowKey,
|
||||
nudgeMessage,
|
||||
nudgeReport,
|
||||
type ArrowKey,
|
||||
} from "./logic/B05_Profile_RouteEdit_KeyNudge";
|
||||
import type { Vertex } from "./logic/B05_Profile_UI_RouteEdit_Curve";
|
||||
import { curveShortfalls, shortfallCrossed } from "./logic/B05_Profile_UI_RouteEdit_Edits";
|
||||
|
||||
/** 누르는 한 번(누름 ~ 뗌) — 뗄 때 결과 한 줄을 셈. */
|
||||
interface Hold {
|
||||
key: ArrowKey;
|
||||
shift: boolean;
|
||||
index: number;
|
||||
before: Vertex[];
|
||||
start: Vertex;
|
||||
moved: boolean;
|
||||
}
|
||||
|
||||
export function bindRouteEditKeys(params: RouteEditPointerParams, dragging: () => boolean): void {
|
||||
const { canvas, status } = params;
|
||||
let hold: Hold | null = null;
|
||||
let timer = 0;
|
||||
|
||||
const metricAt = (x: number, y: number): Vertex => params.toMetric(...params.unrotate(x, y));
|
||||
|
||||
/** 한 걸음 — 하한을 새로 깨면 그 걸음만 되돌림(끌기와 같은 검사). */
|
||||
function step(now: Hold): void {
|
||||
const state = params.state();
|
||||
const node = state.planned[now.index];
|
||||
if (!node) return;
|
||||
const [dx, dy] = arrowStep(now.key, now.shift, metricAt);
|
||||
const before = curveShortfalls(state.nodeInfo, state.limitRadiusM, state.limitArcM);
|
||||
state.planned[now.index] = [node[0] + dx, node[1] + dy];
|
||||
params.markEdited();
|
||||
const after = params.state();
|
||||
if (
|
||||
shortfallCrossed(before, curveShortfalls(after.nodeInfo, after.limitRadiusM, after.limitArcM))
|
||||
) {
|
||||
state.planned[now.index] = node;
|
||||
params.markEdited();
|
||||
status.textContent =
|
||||
`${params.routeHead()} — 하한에 걸려 더 못 옮깁니다` +
|
||||
`(곡선반지름 ${state.limitRadiusM}m${state.limitArcM > 0 ? ` · 곡선 길이 ${state.limitArcM}m` : ""}).`;
|
||||
params.draw();
|
||||
return;
|
||||
}
|
||||
now.moved = true;
|
||||
status.textContent = `${params.routeHead()} — 방향키로 옮기는 중(뗄 때 한 걸음). ${params.curveHint()}`;
|
||||
params.draw();
|
||||
}
|
||||
|
||||
/** 뗌 — 옮겼으면 되돌리기 한 걸음(스냅 켬이면 맞춤) · 횡단 · 기울기 다시 · 결과 한 줄. */
|
||||
function release(): void {
|
||||
window.clearTimeout(timer);
|
||||
window.clearInterval(timer);
|
||||
const done = hold;
|
||||
hold = null;
|
||||
if (!done?.moved) return;
|
||||
const end = params.state().planned[done.index] ?? done.start;
|
||||
params.commit();
|
||||
void params.crossPreview.refresh();
|
||||
params.slopeRefresh();
|
||||
const report = nudgeReport(done.before, params.state().planned, done.index, done.start, end);
|
||||
const message = nudgeMessage(report, params.stations() !== null);
|
||||
status.textContent = `${params.routeHead()} — ${message} ${params.curveHint()}`;
|
||||
params.draw();
|
||||
}
|
||||
|
||||
const detach = (): void => {
|
||||
window.removeEventListener("keydown", onKeyDown, true);
|
||||
window.removeEventListener("keyup", onKeyUp, true);
|
||||
window.removeEventListener("blur", release);
|
||||
};
|
||||
|
||||
function onKeyDown(event: KeyboardEvent): void {
|
||||
if (!canvas.isConnected) return detach();
|
||||
if (!isArrowKey(event.key) || !lastPointerWithin(".b05-routeedit")) return;
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (target?.closest?.("input, select, textarea, [contenteditable='true']")) return;
|
||||
// 평면이 삼킴 — 종단도(▲▼ 계획고 · `defaultPrevented` 면 비킴) · 횡단 조정창으로 안 감
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.repeat || hold || dragging() || params.measureMode()) return; // 반복은 아래 타이머
|
||||
const state = params.state();
|
||||
const index = state.picked;
|
||||
const node = state.planned[index];
|
||||
if (!node || state.apexLock[index] === true) return;
|
||||
const now: Hold = {
|
||||
key: event.key,
|
||||
shift: event.shiftKey,
|
||||
index,
|
||||
before: state.planned.map(([x, y]): Vertex => [x, y]),
|
||||
start: [node[0], node[1]],
|
||||
moved: false,
|
||||
};
|
||||
hold = now;
|
||||
step(now);
|
||||
// 누르고 있으면 종단 편집과 같은 속도(0.5초 뒤 초당 10회) — OS 키 반복은 기기마다 갈림
|
||||
timer = window.setTimeout(() => {
|
||||
timer = window.setInterval(() => hold === now && step(now), HOLD_INTERVAL_MS);
|
||||
}, HOLD_DELAY_MS);
|
||||
}
|
||||
|
||||
function onKeyUp(event: KeyboardEvent): void {
|
||||
if (!canvas.isConnected) return detach();
|
||||
if (!hold || event.key !== hold.key) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
release();
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", onKeyDown, true);
|
||||
window.addEventListener("keyup", onKeyUp, true);
|
||||
window.addEventListener("blur", release);
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
import { pickPx } from "../A00_Common/touch_bridge";
|
||||
import { hitPreparedLayer, type ViewState } from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
||||
import type { RouteEditContours } from "./B05_Profile_UI_RouteEdit_Contour";
|
||||
import { bindRouteEditKeys } from "./B05_Profile_UI_RouteEdit_Keys";
|
||||
import {
|
||||
dragHandleTo as curveDragTo,
|
||||
type EditedCurve,
|
||||
@@ -331,6 +332,7 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
};
|
||||
canvas.addEventListener("pointerup", endDrag);
|
||||
canvas.addEventListener("pointercancel", endDrag);
|
||||
bindRouteEditKeys(params, () => dragNode >= 0 || dragHandle !== null); // 교각점 방향키(383-2)
|
||||
|
||||
return { dragging: () => dragNode >= 0 || dragHandle !== null, nodeAt, segmentAt };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile/logic/B05_Profile_RouteEdit_KeyNudge.ts
|
||||
* 교각점 방향키(383-2 · 요구 241-16) — 화면 축 걸음 · 뗄 때 맞춘 결과 한 줄.
|
||||
*
|
||||
* · 한 번 0.1 m · Shift ×10 · 위 = 화면 위(지도를 돌려도 화면 기준).
|
||||
* · 측점 스냅 켬이면 뗄 때 MC 를 일반측점에 맞춤 → 실제 이동 · 함께 밀린 IP 를 알림.
|
||||
* 파이썬 짝 아님. 순수 계산 — 화면 · fetch · sessionStorage 없음. 부르는 쪽은 `_UI_RouteEdit_Keys`.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { Vertex } from "./B05_Profile_UI_RouteEdit_Curve";
|
||||
|
||||
/** 방향키 한 번(m) · Shift 배수. */
|
||||
export const KEY_STEP_M = 0.1;
|
||||
export const KEY_SHIFT_TIMES = 10;
|
||||
/** 다른 IP 가 이보다 더 움직였으면 「함께 밀림」(m). */
|
||||
const SAME_M = 0.005;
|
||||
/** 고른 IP 가 이보다 덜 움직였으면 「제자리」(m) — MC 측점 허용(`MC_TOL_M`)만큼. */
|
||||
const STAY_M = 0.05;
|
||||
|
||||
export type ArrowKey = "ArrowUp" | "ArrowDown" | "ArrowLeft" | "ArrowRight";
|
||||
|
||||
export function isArrowKey(key: string): key is ArrowKey {
|
||||
return key === "ArrowUp" || key === "ArrowDown" || key === "ArrowLeft" || key === "ArrowRight";
|
||||
}
|
||||
|
||||
/**
|
||||
* 방향키 한 번의 도면 좌표 걸음 — `metricAt(화면 x, y)` 로 화면 위 · 오른쪽 1 px 를 도면 좌표로 바꿔
|
||||
* 그 방향 단위 벡터 × 걸음. 돌린 지도 · 확대와 무관하게 화면 축.
|
||||
*/
|
||||
export function arrowStep(
|
||||
key: ArrowKey,
|
||||
shift: boolean,
|
||||
metricAt: (x: number, y: number) => Vertex,
|
||||
): Vertex {
|
||||
const [x0, y0] = metricAt(0, 0);
|
||||
const [dx, dy] =
|
||||
key === "ArrowUp"
|
||||
? [0, -1]
|
||||
: key === "ArrowDown"
|
||||
? [0, 1]
|
||||
: key === "ArrowLeft"
|
||||
? [-1, 0]
|
||||
: [1, 0];
|
||||
const [x1, y1] = metricAt(dx, dy);
|
||||
const length = Math.hypot(x1 - x0, y1 - y0);
|
||||
if (!(length > 0)) return [0, 0];
|
||||
const step = KEY_STEP_M * (shift ? KEY_SHIFT_TIMES : 1);
|
||||
return [((x1 - x0) / length) * step, ((y1 - y0) / length) * step];
|
||||
}
|
||||
|
||||
export interface NudgeReport {
|
||||
/** 누르기 전 자리에서 뗀 뒤(맞춘 뒤) 자리까지(m). */
|
||||
movedM: number;
|
||||
/** 고른 IP 말고 자리가 바뀐 IP 수(맞춤이 함께 민 것). */
|
||||
pushed: number;
|
||||
}
|
||||
|
||||
/** 뗀 뒤 결과 — `before` = 누르기 전 노드 · `after` = 맞춘 뒤 노드 · `start` = 고른 IP 처음 자리 ·
|
||||
* `end` = 키로 옮긴 자리(맞추기 전). 노드 수가 바뀌었으면(합침) `end` 에 가장 가까운 노드를 고른 IP 로. */
|
||||
export function nudgeReport(
|
||||
before: ReadonlyArray<Vertex>,
|
||||
after: ReadonlyArray<Vertex>,
|
||||
picked: number,
|
||||
start: Vertex,
|
||||
end: Vertex,
|
||||
): NudgeReport {
|
||||
const same = before.length === after.length;
|
||||
let index = picked;
|
||||
if (!same || !after[index]) {
|
||||
index = 0;
|
||||
after.forEach((node, i) => {
|
||||
if (
|
||||
Math.hypot(node[0] - end[0], node[1] - end[1]) <
|
||||
Math.hypot(after[index][0] - end[0], after[index][1] - end[1])
|
||||
)
|
||||
index = i;
|
||||
});
|
||||
}
|
||||
const node = after[index] ?? start;
|
||||
const moved = (a: Vertex, b: Vertex): boolean => Math.hypot(a[0] - b[0], a[1] - b[1]) > SAME_M;
|
||||
const pushed = same
|
||||
? after.filter((xy, i) => i !== index && moved(xy, before[i])).length
|
||||
: after.filter((xy, i) => i !== index && before.every((old) => moved(xy, old))).length;
|
||||
return { movedM: Math.hypot(node[0] - start[0], node[1] - start[1]), pushed };
|
||||
}
|
||||
|
||||
/** 상태줄 한 마디 — 스냅 켬이면 실제 이동 · 함께 밀린 IP · 제자리면 미세 조정 안내. */
|
||||
export function nudgeMessage(report: NudgeReport, snapping: boolean): string {
|
||||
if (!snapping)
|
||||
return `교각점을 ${report.movedM.toFixed(1)} m 옮겼습니다(방향키 0.1 m · Shift 1 m).`;
|
||||
if (report.movedM < STAY_M) return "같은 측점 — 스냅 끄고 미세 조정하세요.";
|
||||
return `측점에 맞춰 실제 이동 ${report.movedM.toFixed(1)} m · 함께 밀린 IP ${report.pushed}곳.`;
|
||||
}
|
||||
@@ -87,6 +87,11 @@ if (typeof document !== "undefined")
|
||||
true,
|
||||
);
|
||||
|
||||
/** 마지막으로 누른 자리가 `selector` 안인가 — 평면 노선 편집 방향키(B05 383-2)도 이것으로 가림. */
|
||||
export function lastPointerWithin(selector: string): boolean {
|
||||
return !!lastPointer?.closest(selector);
|
||||
}
|
||||
|
||||
/** 방향키가 종단도 몫인가 — 마지막으로 누른 자리가 종단도 안이면 참. */
|
||||
export function arrowsBelongToProfile(): boolean {
|
||||
// 새 창으로 뗀 유토 · 배수 판도 종단도 몫(325-2).
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/* 383-2 교각점 방향키 — `logic/B05_Profile_RouteEdit_KeyNudge.ts` 를 Node 로 돌려 JSON 한 줄을 냄
|
||||
* (`test_383_2_route_keys.py` 가 판정). `.cjs` = 루트 package.json 이 "type": "module" 이라서.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) =>
|
||||
request.startsWith(".")
|
||||
? loadTs(path.join(path.dirname(full), `${request}.ts`))
|
||||
: require(request);
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const nudge = loadTs(path.join(ROOT, "B05_Profile", "logic", "B05_Profile_RouteEdit_KeyNudge.ts"));
|
||||
|
||||
// 화면 → 도면: 1 px = 0.5 m · 화면 y 아래 = 도면 y 위로 뒤집힘 · 지도 30° 돌림
|
||||
const turn = (Math.PI / 180) * 30;
|
||||
const plain = (x, y) => [x * 0.5, -y * 0.5];
|
||||
const rotated = (x, y) => {
|
||||
const [mx, my] = plain(x, y);
|
||||
return [mx * Math.cos(turn) - my * Math.sin(turn), mx * Math.sin(turn) + my * Math.cos(turn)];
|
||||
};
|
||||
const keys = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"];
|
||||
const steps = (metricAt, shift) =>
|
||||
Object.fromEntries(keys.map((key) => [key, nudge.arrowStep(key, shift, metricAt)]));
|
||||
|
||||
const before = [
|
||||
[0, 0],
|
||||
[50, 0],
|
||||
[100, 20],
|
||||
[150, 0],
|
||||
];
|
||||
const snapped = [
|
||||
[0, 0],
|
||||
[50.8, 0],
|
||||
[100, 20.6],
|
||||
[150, 0],
|
||||
];
|
||||
const stayed = [
|
||||
[0, 0],
|
||||
[50.02, 0],
|
||||
[100, 20],
|
||||
[150, 0],
|
||||
];
|
||||
const merged = [
|
||||
[0, 0],
|
||||
[50.4, 0],
|
||||
[150, 0],
|
||||
];
|
||||
const out = {
|
||||
plain: steps(plain, false),
|
||||
shift: steps(plain, true),
|
||||
rotated: steps(rotated, false),
|
||||
arrows: keys.map(nudge.isArrowKey).concat(nudge.isArrowKey("Enter")),
|
||||
snapped: nudge.nudgeReport(before, snapped, 1, [50, 0], [50.3, 0]),
|
||||
stayed: nudge.nudgeReport(before, stayed, 1, [50, 0], [50.3, 0]),
|
||||
merged: nudge.nudgeReport(before, merged, 1, [50, 0], [50.3, 0]),
|
||||
messages: {
|
||||
snapped: nudge.nudgeMessage({ movedM: 0.8, pushed: 1 }, true),
|
||||
stayed: nudge.nudgeMessage({ movedM: 0.02, pushed: 0 }, true),
|
||||
free: nudge.nudgeMessage({ movedM: 0.3, pushed: 0 }, false),
|
||||
},
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,80 @@
|
||||
"""383-2 교각점 방향키 — `logic/B05_Profile_RouteEdit_KeyNudge.ts` 를
|
||||
Node(`helper_383_2_key_nudge.cjs`)로 돌려 판정(화면 축 0.1 m · Shift ×10 · 돌린 지도 · 스냅 뒤
|
||||
실제 이동 · 함께 밀린 IP · 제자리 안내) + 이음(`_RouteEdit_Keys` · 끌기 걸기 한 줄 · 평면이
|
||||
삼킴 → 종단 ▲▼ 비킴).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_383_2_route_keys.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
B05 = ROOT / "B05_Profile"
|
||||
HELPER = Path(__file__).with_name("helper_383_2_key_nudge.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 _read(name: str) -> str:
|
||||
return (B05 / name).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_화면_축_0_1m_Shift_10배(got) -> None:
|
||||
plain = got["plain"]
|
||||
assert plain["ArrowUp"] == pytest.approx([0, 0.1]) # 화면 위 = 도면 북쪽(뒤집힌 y)
|
||||
assert plain["ArrowDown"] == pytest.approx([0, -0.1])
|
||||
assert plain["ArrowLeft"] == pytest.approx([-0.1, 0])
|
||||
assert plain["ArrowRight"] == pytest.approx([0.1, 0])
|
||||
assert got["shift"]["ArrowRight"] == pytest.approx([1.0, 0])
|
||||
assert got["arrows"] == [True, True, True, True, False]
|
||||
|
||||
|
||||
def test_돌린_지도도_화면_위(got) -> None:
|
||||
up = got["rotated"]["ArrowUp"]
|
||||
turn = math.radians(30)
|
||||
assert math.hypot(*up) == pytest.approx(0.1)
|
||||
assert up == pytest.approx([-0.1 * math.sin(turn), 0.1 * math.cos(turn)])
|
||||
|
||||
|
||||
def test_스냅_뒤_실제_이동_함께_밀린_IP(got) -> None:
|
||||
assert got["snapped"]["movedM"] == pytest.approx(0.8)
|
||||
assert got["snapped"]["pushed"] == 1
|
||||
assert got["stayed"]["movedM"] == pytest.approx(0.02) and got["stayed"]["pushed"] == 0
|
||||
assert got["merged"]["movedM"] == pytest.approx(0.4) and got["merged"]["pushed"] == 0
|
||||
messages = got["messages"]
|
||||
assert "실제 이동 0.8 m" in messages["snapped"] and "함께 밀린 IP 1곳" in messages["snapped"]
|
||||
assert "같은 측점 — 스냅 끄고 미세 조정" in messages["stayed"]
|
||||
assert "0.3 m 옮겼습니다" in messages["free"]
|
||||
|
||||
|
||||
def test_이음_끌기_걸기_한_줄_창_가로채기() -> None:
|
||||
pointer = _read("B05_Profile_UI_RouteEdit_Pointer.ts")
|
||||
assert pointer.count("bindRouteEditKeys(") == 1
|
||||
keys = _read("B05_Profile_UI_RouteEdit_Keys.ts")
|
||||
assert 'window.addEventListener("keydown", onKeyDown, true)' in keys # 앞서 받음(capture)
|
||||
assert 'lastPointerWithin(".b05-routeedit")' in keys
|
||||
assert "event.preventDefault();\n event.stopPropagation();" in keys
|
||||
assert "state.apexLock[index] === true" in keys # 고정 IP 는 안 옮김
|
||||
assert "shortfallCrossed(" in keys # 하한 검사 = 끌기와 같음
|
||||
assert "params.commit();" in keys # 뗄 때 한 걸음(스냅이면 그때 맞춤)
|
||||
# 종단 ▲▼ 는 앞선 preventDefault 를 보고 비킴
|
||||
tools = _read("B05_Profile_UI_Profile_Panel_Tools.ts")
|
||||
assert "if (event.defaultPrevented) return;" in tools
|
||||
base = (ROOT / "B06_Section/B06_Section_UI_Cross_Panel_Base.ts").read_text(encoding="utf-8")
|
||||
assert "export function lastPointerWithin(selector: string)" in base
|
||||
|
||||
|
||||
def test_RouteEdit_본문_줄_안_늘림() -> None:
|
||||
assert len(_read("B05_Profile_UI_RouteEdit.ts").splitlines()) <= 700
|
||||
Reference in New Issue
Block a user