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));
|
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 {
|
export function trackPageBusy(projectId: string, run: Promise<unknown> | null): void {
|
||||||
if (!run) return;
|
if (!run) return;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { resetDesign } from "./design_reset";
|
|||||||
import type { DesignView } from "./design_dock";
|
import type { DesignView } from "./design_dock";
|
||||||
import { routeDraftBridge, type PagePipelineOptions } from "./design_pipeline_page";
|
import { routeDraftBridge, type PagePipelineOptions } from "./design_pipeline_page";
|
||||||
import { plantedBase } from "./design_pipeline_base";
|
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 { SECTION_STATION_INTERVAL_M } from "../../B05_Profile/logic/B05_Profile_Sections_Stations";
|
||||||
import {
|
import {
|
||||||
applyStructureAreaRows,
|
applyStructureAreaRows,
|
||||||
@@ -261,7 +262,7 @@ export function sharedStructures(shared: DesignPageShared): StructuresSection {
|
|||||||
onPipeRemove: (chainageM) => on().onPipeRemove?.(chainageM),
|
onPipeRemove: (chainageM) => on().onPipeRemove?.(chainageM),
|
||||||
onPipeSelect: (chainageM) => on().onPipeSelect?.(chainageM),
|
onPipeSelect: (chainageM) => on().onPipeSelect?.(chainageM),
|
||||||
onListRelease: () => on().onListRelease?.(),
|
onListRelease: () => on().onListRelease?.(),
|
||||||
onReveal: () => shared.revealLeft(),
|
onReveal: () => isUserPick(shared.projectId) && shared.revealLeft(), // 사용자 고름만(398-1)
|
||||||
},
|
},
|
||||||
{ includeDetail: () => shared.view() === "cross", projectId: shared.projectId },
|
{ 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