fix(B05·B06 한 페이지): 교점 옮기면 좌측 패널 열림 — 펴기는 사용자 고름만 (398-1)
- design_user_pick.ts 새 파일 — 사용자 고름 표시 한 칸(진짜 누름 안 · 묶음 차례 없음) - design_shared onReveal 한 곳이 그 칸만 봄 — 폼(고름 · 우클릭 추가)은 그대로 펴기를 부름 - design_page_data pageBusy — 묶음 차례가 떠 있나 - 배수유역 다시 나눔 응답 · 방향키 · 끌기 · 묶음 다시 돎의 다시 칠하기 = 안 폄 - 시험 test_398_1_reveal_user_pick(헬퍼 helper_398_1_user_pick.cjs) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
This commit is contained in:
@@ -69,6 +69,9 @@ const notify = (projectId: string): void => {
|
||||
busyListeners.get(projectId)?.forEach((fn) => fn(now));
|
||||
};
|
||||
|
||||
/** 묶음 차례가 떠 있나 — 그 사이 폼이 다시 실려도 좌측 패널은 안 폄(`design_user_pick` · 398-1). */
|
||||
export const pageBusy = (projectId: string): boolean => (busy.get(projectId) ?? 0) > 0;
|
||||
|
||||
/** 묶음 차례 하나가 뜸 — 끝나면(밀림 · 실패 포함) 내림. */
|
||||
export function trackPageBusy(projectId: string, run: Promise<unknown> | null): void {
|
||||
if (!run) return;
|
||||
|
||||
@@ -31,6 +31,7 @@ import { resetDesign } from "./design_reset";
|
||||
import type { DesignView } from "./design_dock";
|
||||
import { routeDraftBridge, type PagePipelineOptions } from "./design_pipeline_page";
|
||||
import { plantedBase } from "./design_pipeline_base";
|
||||
import { isUserPick } from "./design_user_pick";
|
||||
import { SECTION_STATION_INTERVAL_M } from "../../B05_Profile/logic/B05_Profile_Sections_Stations";
|
||||
import {
|
||||
applyStructureAreaRows,
|
||||
@@ -261,7 +262,7 @@ export function sharedStructures(shared: DesignPageShared): StructuresSection {
|
||||
onPipeRemove: (chainageM) => on().onPipeRemove?.(chainageM),
|
||||
onPipeSelect: (chainageM) => on().onPipeSelect?.(chainageM),
|
||||
onListRelease: () => on().onListRelease?.(),
|
||||
onReveal: () => shared.revealLeft(),
|
||||
onReveal: () => isUserPick(shared.projectId) && shared.revealLeft(), // 사용자 고름만(398-1)
|
||||
},
|
||||
{ includeDetail: () => shared.view() === "cross", projectId: shared.projectId },
|
||||
);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/design_user_pick.ts
|
||||
* 「사용자 고름」 표시 한 칸(PLAN 398-1) — 좌측 패널 저절로 펴기(09-04 「바깥에서 고르면 좌측 패널도」)는
|
||||
* 사용자가 직접 누른 고름에서만.
|
||||
*
|
||||
* 사용자 고름 = 지금 브라우저가 진짜 누름(포인터 · 마우스 · 우클릭)을 처리하는 중 — 눈금 · 마름모 · 3D ·
|
||||
* 종단 알약 · 배수유역도 · 횡단 카드 · 우클릭 추가가 다 그 누름 안에서 폼을 엶.
|
||||
* 아님 = 누름 밖(서버 응답 · 타이머 — 배수유역 다시 나눔 · 횡단 다시 그림) · 방향키 · 묶음이 도는 동안
|
||||
* (교점 끌기 · 곡선 지우기 · 되돌리기가 띄운 차례의 관 · 구조물 따라가기 · 다시 칠하기).
|
||||
* 폼이 고른 것을 다시 실을 때마다 부르는 쪽을 막지 않고 펴기 한 곳(`design_shared` onReveal)이 여기만 봄.
|
||||
* ========================================================================== */
|
||||
|
||||
import { pageBusy } from "./design_page_data";
|
||||
|
||||
const PICK_EVENTS = new Set([
|
||||
"pointerdown",
|
||||
"pointerup",
|
||||
"mousedown",
|
||||
"mouseup",
|
||||
"click",
|
||||
"dblclick",
|
||||
"contextmenu",
|
||||
]);
|
||||
|
||||
/** 지금 사용자 누름 안이고 묶음이 안 도는가 — 좌측 패널을 펴도 되는가. */
|
||||
export function isUserPick(projectId: string | null): boolean {
|
||||
const event = (globalThis as { event?: Event }).event; // 처리 중인 이벤트(그 마이크로태스크까지)
|
||||
if (!event?.isTrusted || !PICK_EVENTS.has(event.type)) return false;
|
||||
return !projectId || !pageBusy(projectId);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/* PLAN 398-1 단위검증 헬퍼 — 진짜 `design_user_pick.ts` + 진짜 `design_page_data.ts`(묶음 차례 · 「계산 중」)로
|
||||
* 좌측 패널 펴기 한 곳(`design_shared` onReveal 과 같은 식)을 길마다 셈. 짝 = `test_398_1_reveal_user_pick.py`.
|
||||
* 브라우저가 처리 중인 이벤트(`window.event`)는 `globalThis.event` 로 흉내. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const DESIGN = path.join(ROOT, "A00_Common", "design");
|
||||
|
||||
function load(file, stubs) {
|
||||
const js = ts.transpileModule(fs.readFileSync(path.join(DESIGN, file), "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, (name) => {
|
||||
if (!stubs[name]) throw new Error(`stub 없음: ${name}`);
|
||||
return stubs[name];
|
||||
});
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const data = load("design_page_data.ts", {
|
||||
"./design_page_route": { setPageRouteId: () => undefined, pageRouteId: () => null },
|
||||
"../../B04_PreProcess/B04_PreProcess_Api_Fetch": { fetchDetailPipePoints: async () => ({}) },
|
||||
"../../B05_Profile/B05_Profile_Api_Structures": {
|
||||
fetchStructures: async () => ({}),
|
||||
dropStaleStructureTypes: async () => false,
|
||||
},
|
||||
});
|
||||
const pick = load("design_user_pick.ts", { "./design_page_data": data });
|
||||
|
||||
const P = "p1";
|
||||
let reveals = 0;
|
||||
/** 폼이 고른 것을 실을 때마다 부르는 펴기(`design_shared` onReveal 그대로). */
|
||||
const onReveal = () => pick.isUserPick(P) && (reveals += 1);
|
||||
/** 브라우저가 그 이벤트를 처리하는 동안(그 마이크로태스크까지) — 끝나면 이벤트 없음. */
|
||||
async function during(event, body) {
|
||||
globalThis.event = event;
|
||||
try {
|
||||
await body();
|
||||
} finally {
|
||||
globalThis.event = undefined;
|
||||
}
|
||||
}
|
||||
const trusted = (type) => ({ type, isTrusted: true });
|
||||
const count = async (event, body) => {
|
||||
const before = reveals;
|
||||
await during(event, body);
|
||||
return reveals - before;
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const out = {};
|
||||
// 사용자 고름 — 종단 알약 · 횡단 카드(click) · 3D 부재(pointerup) · 우클릭 추가(contextmenu)
|
||||
out.click = await count(trusted("click"), () => onReveal());
|
||||
out.pointerup = await count(trusted("pointerup"), () => onReveal());
|
||||
out.contextmenu = await count(trusted("contextmenu"), () => onReveal());
|
||||
// 누름 밖 — 배수유역 다시 나눔 응답 · 횡단 다시 그림(서버 응답 · 타이머)
|
||||
out.outside = await count(undefined, () => onReveal());
|
||||
// 방향키로 교점 옮김 · 끌기 도중(pointermove)
|
||||
out.arrow = await count(trusted("keydown"), () => onReveal());
|
||||
out.drag = await count(trusted("pointermove"), () => onReveal());
|
||||
// 스크립트가 낸 가짜 누름
|
||||
out.synthetic = await count({ type: "click", isTrusted: false }, () => onReveal());
|
||||
// 교점 끌기 놓음 · 곡선 지우기 · 되돌리기 단추 — 묶음 차례가 떠 그 마이크로태스크에서 관 · 구조물 따라가며 다시 칠함
|
||||
let finish;
|
||||
out.pipelineRepaint = await count(trusted("pointerup"), async () => {
|
||||
data.trackPageBusy(P, new Promise((resolve) => (finish = resolve)));
|
||||
await Promise.resolve();
|
||||
onReveal(); // follow → 폼 다시 실음
|
||||
});
|
||||
out.busyPick = await count(trusted("click"), () => onReveal()); // 차례가 도는 사이 고름은 안 폄
|
||||
finish();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
out.afterRun = await count(trusted("click"), () => onReveal()); // 차례가 끝나면 다시 폄
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
"""PLAN 398-1 sub7 — 교점 옮기면 좌측 패널 열림(요구 248).
|
||||
|
||||
못박는 것
|
||||
- 좌측 패널 저절로 펴기는 사용자 고름만 — 진짜 누름(click · pointerup · contextmenu) 안 = 1.
|
||||
- 다시 칠하기 · 누가거리 따라가기 · 되돌리기 · 묶음 다시 돎 = 0 — 누름 밖(배수유역 다시 나눔 응답) ·
|
||||
방향키 · 끌기 도중 · 가짜 누름 · 묶음 차례가 떠 있는 동안(교점 놓음 · 곡선 지우기 · 되돌리기).
|
||||
- 뿌리 한 곳 — 폼(`selectPipeByChainage` · `selectById` · `addAt`)은 그대로 펴기를 부르고 `design_shared`
|
||||
onReveal 하나가 `isUserPick` 으로 거름.
|
||||
|
||||
짝: `helper_398_1_user_pick.cjs`(진짜 `design_user_pick` · `design_page_data`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
|
||||
def _read(rel: str) -> str:
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r() -> dict:
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_398_1_user_pick.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("way", ["click", "pointerup", "contextmenu", "afterRun"])
|
||||
def test_사용자_고름은_폄(r, way):
|
||||
assert r[way] == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"way", ["outside", "arrow", "drag", "synthetic", "pipelineRepaint", "busyPick"]
|
||||
)
|
||||
def test_다시_칠하기는_안_폄(r, way):
|
||||
assert r[way] == 0
|
||||
|
||||
|
||||
def test_뿌리_한_곳():
|
||||
shared = _read("A00_Common/design/design_shared.ts")
|
||||
assert "onReveal: () => isUserPick(shared.projectId) && shared.revealLeft()" in shared
|
||||
panel = _read("B05_Profile/B05_Profile_UI_Structures_Panel.ts")
|
||||
assert panel.count("callbacks.onReveal?.()") == 3 # 고름 둘 · 우클릭 추가 — 부르는 쪽은 안 막음
|
||||
found = subprocess.run( # noqa: S603, S607 — 고정 실행 파일
|
||||
["git", "grep", "-l", "isUserPick(", "--", "*.ts"],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
users = set(found.stdout.split()) | {
|
||||
"A00_Common/design/design_user_pick.ts",
|
||||
"A00_Common/design/design_shared.ts",
|
||||
} # 새 파일(추적 전)
|
||||
assert sorted(users) == [
|
||||
"A00_Common/design/design_shared.ts",
|
||||
"A00_Common/design/design_user_pick.ts",
|
||||
]
|
||||
Reference in New Issue
Block a user