fix(B05 배수유역): 349 수동 [업데이트] 답 머묾 · 저장 뒤 낡음 — 답 받기 = 차례 + 입력 지문
- 묶음 drainage 단계 입력 지문(노선 초안 rev 뺌 · 관 id · 자리 cm) — 같은 노선 · 관으로 다시 돈 묶음은 흘려보냄(떠 있는 요청 안 끊음 · 다시 안 흐림) - 차례 번호는 요청마다 하나 · 답은 차례 + 지문이 지금과 같을 때만 받음 - 서버 답이 「낡음」(노선 [저장] 뒤 등) = 수동은 흐림 + [업데이트] · 자동은 3 s 뒤 조용히(markStale) - 시험 test_349_drainage_live_flow(가짜 DOM · 시계 · fetch 로 화면 파일 실행 — 수동 · 자동 · 제안 [적용]) · 347-2 헬퍼 새 받기 규칙 - PLAN.md 349-0b Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
@@ -7,6 +7,8 @@
|
||||
* 기다리지 않고 null 을 돌려줌(배수유역은 뒤 단계 · 출력 없음).
|
||||
* - 노선 초안이 있으면 「낡음」 — 자동 = 3 s 조용한 뒤 셈 · 수동 = 칸만 흐림 + 가운데 [업데이트].
|
||||
* - 다시 조작하면 처음으로 — 떠 있는 요청을 끊고(AbortController) 다시 흐림 또는 다시 3 s.
|
||||
* 묶음이 같은 노선 · 관으로 다시 돌면(입력 지문 같음) 그대로 둠 — 떠 있는 답을 받음.
|
||||
* - 서버가 「낡음」으로 답하면(노선 [저장] 뒤 등) 같은 흐림 + [업데이트] · 자동이면 3 s 뒤.
|
||||
* - 답은 패널 요청 감싸기(`DetailRunner`, quiet)로 올림 — 옛 차례 · 서버가 그만둔 답은 버림.
|
||||
* - 덮개 · 잠금 없음 — 다른 칸 조작은 그대로.
|
||||
* ========================================================================== */
|
||||
@@ -24,6 +26,7 @@ import {
|
||||
import {
|
||||
acceptLive,
|
||||
draftMakesStale,
|
||||
inputKey,
|
||||
liveRequestBody,
|
||||
parseDrainageMode,
|
||||
QUIET_MS,
|
||||
@@ -50,6 +53,8 @@ export interface DrainageLive {
|
||||
stage: StageFn<"drainage">;
|
||||
/** 낡음을 지금 노선으로 다시 셈(자동 · [업데이트]와 같은 길). */
|
||||
refresh(): void;
|
||||
/** 서버 답이 「낡음」 — 흐림 + [업데이트](자동이면 3 s 뒤 셈). 이미 낡음이면 그대로. */
|
||||
markStale(): void;
|
||||
/** [확정]이 서버에서 다시 셈 — 낡음 지움(떠 있는 요청 · 기다림도 끊음). */
|
||||
markFresh(): void;
|
||||
readonly stale: boolean;
|
||||
@@ -93,6 +98,8 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive {
|
||||
let route: unknown = null;
|
||||
let stale = false;
|
||||
let generation = 0;
|
||||
/** 마지막 단계 입력 지문 — 같으면 묶음이 다시 돈 것뿐. */
|
||||
let key = "";
|
||||
let timer = 0;
|
||||
let inflight: AbortController | null = null;
|
||||
|
||||
@@ -134,16 +141,16 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive {
|
||||
const projectId = deps.runner.projectId();
|
||||
if (!projectId) return;
|
||||
cancel();
|
||||
const sent = generation;
|
||||
const sent = { generation: ++generation, key };
|
||||
const controller = new AbortController();
|
||||
inflight = controller;
|
||||
paint();
|
||||
const body = liveRequestBody(sent, route, deps.pipeRequest());
|
||||
const body = liveRequestBody(sent.generation, route, deps.pipeRequest());
|
||||
void deps.runner
|
||||
.run(async () => {
|
||||
const response = await fetchLive(projectId, body, controller.signal);
|
||||
// 옛 차례 · 서버가 그만둔 답 — 감싸기가 조용히 버리게 끊김으로 돌려줌.
|
||||
if (!acceptLive(response, sent, generation))
|
||||
if (!acceptLive(response, sent, { generation, key }))
|
||||
throw new DOMException("superseded", "AbortError");
|
||||
return response;
|
||||
}, true)
|
||||
@@ -157,7 +164,6 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive {
|
||||
|
||||
/** 평면 · 관 자리 조작 — 처음으로(요청 끊기 · 다시 흐림 또는 다시 3 s). */
|
||||
function invalidate(): void {
|
||||
generation += 1;
|
||||
cancel();
|
||||
stale = true;
|
||||
if (mode === "auto") timer = window.setTimeout(refresh, QUIET_MS);
|
||||
@@ -208,11 +214,20 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive {
|
||||
return {
|
||||
stage(ctx) {
|
||||
route = ctx.inputs.route;
|
||||
const next = inputKey(route, ctx.inputs.pipes);
|
||||
if (next === key) return null;
|
||||
key = next;
|
||||
if (draftMakesStale(route) || stale) invalidate();
|
||||
return null;
|
||||
},
|
||||
refresh,
|
||||
suggest,
|
||||
markStale() {
|
||||
if (stale) return;
|
||||
stale = true;
|
||||
if (mode === "auto" && !inflight) timer = window.setTimeout(refresh, QUIET_MS);
|
||||
paint();
|
||||
},
|
||||
markFresh() {
|
||||
cancel();
|
||||
stale = false;
|
||||
|
||||
@@ -340,6 +340,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
function apply(response: DetailBasinResponse): void {
|
||||
basins = response.basins;
|
||||
live.suggest.update(response);
|
||||
if (response.stale) live.markStale(); // 노선 [저장] 뒤 등 — 흐림 + [업데이트](자동이면 3 s 뒤)
|
||||
mainBoundary = response.main_polygon_lonlat ?? [];
|
||||
upstreamLines = (response.upstream_lonlat ?? []) as Array<Array<[number, number]>>;
|
||||
flowArrows = (response.flow_arrows ?? []) as FlowArrow[];
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
* - 대상 = 평면(노선 초안) · 관 자리만 — 종단 · 횡단 조작은 배수유역 결과를 안 바꿈(묶음 deps 와 같음).
|
||||
* - 노선 초안이 없으면(저장 노선 그대로) 낡음 아님 — 관 조작은 세부 유역 나눔(0.2 s)이 바로 맞춤.
|
||||
* - 자동 = 조용한 뒤 `QUIET_MS` · 수동 = 흐림 + [업데이트] · 기본 수동.
|
||||
* - 답 채택 = 보낸 차례가 지금 차례와 같고 서버가 그만두지 않았을 때만(늦은 답 버림).
|
||||
* - 묶음이 같은 입력으로 다시 돌면(입력 지문 같음) 낡음 · 떠 있는 요청을 그대로 둠.
|
||||
* - 답 채택 = 보낸 차례 · 입력 지문이 지금과 같고 서버가 그만두지 않았을 때만(늦은 답 버림).
|
||||
* ========================================================================== */
|
||||
|
||||
export type DrainageMode = "manual" | "auto";
|
||||
@@ -20,6 +21,15 @@ export const parseDrainageMode = (raw: string | null | undefined): DrainageMode
|
||||
/** 묶음 drainage 단계 입력 → 낡았는가 — 노선 초안이 있을 때만. */
|
||||
export const draftMakesStale = (route: unknown): boolean => route !== null && route !== undefined;
|
||||
|
||||
/** 묶음 drainage 입력 지문 — 노선 초안(판 번호 rev 뺌) · 관 id · 자리(cm). */
|
||||
export function inputKey(route: unknown, pipes: unknown): string {
|
||||
const draft = route && typeof route === "object" ? { ...route, rev: undefined } : null;
|
||||
const spots = Array.isArray(pipes)
|
||||
? pipes.map((pipe) => `${pipe?.id ?? ""}@${Math.round(Number(pipe?.chainage_m) * 100)}`).sort()
|
||||
: [];
|
||||
return JSON.stringify([draft, spots]);
|
||||
}
|
||||
|
||||
/** 「지금 노선으로 셈」 몸통 — 서버 `post_drainage_live`. */
|
||||
export function liveRequestBody<P>(
|
||||
generation: number,
|
||||
@@ -29,15 +39,16 @@ export function liveRequestBody<P>(
|
||||
return { generation, route: route ?? null, points };
|
||||
}
|
||||
|
||||
/** 답을 화면에 올리는가 — 서버가 그만둔 답 · 옛 차례 답은 버림. */
|
||||
/** 답을 화면에 올리는가 — 서버가 그만둔 답 · 옛 차례 · 입력이 바뀐 뒤 답은 버림. */
|
||||
export function acceptLive(
|
||||
response: { status?: string; generation?: number },
|
||||
sentGeneration: number,
|
||||
currentGeneration: number,
|
||||
sent: { generation: number; key: string },
|
||||
now: { generation: number; key: string },
|
||||
): boolean {
|
||||
return (
|
||||
response.status !== "superseded" &&
|
||||
sentGeneration === currentGeneration &&
|
||||
(response.generation === undefined || response.generation === sentGeneration)
|
||||
sent.generation === now.generation &&
|
||||
sent.key === now.key &&
|
||||
(response.generation === undefined || response.generation === sent.generation)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ const basins = [
|
||||
];
|
||||
const candidates = [{ chainage_m: 200 }, { chainage_m: 500.2 }];
|
||||
const before = JSON.stringify(pipes);
|
||||
const at = (generation) => ({ generation, key: "k" }); // 349 — 차례 + 입력 지문
|
||||
const suggestions = sug.collectSuggestions(pipes, basins, candidates);
|
||||
const applied = sug.applySuggestions(pipes, suggestions);
|
||||
|
||||
@@ -44,10 +45,10 @@ process.stdout.write(
|
||||
],
|
||||
stale: [live.draftMakesStale(null), live.draftMakesStale({ rev: 1 })],
|
||||
accept: [
|
||||
live.acceptLive({ status: "success", generation: 3 }, 3, 3),
|
||||
live.acceptLive({ status: "success", generation: 3 }, 3, 4),
|
||||
live.acceptLive({ status: "superseded" }, 3, 3),
|
||||
live.acceptLive({ status: "success", generation: 2 }, 3, 3),
|
||||
live.acceptLive({ status: "success", generation: 3 }, at(3), at(3)),
|
||||
live.acceptLive({ status: "success", generation: 3 }, at(3), at(4)),
|
||||
live.acceptLive({ status: "superseded" }, at(3), at(3)),
|
||||
live.acceptLive({ status: "success", generation: 2 }, at(3), at(3)),
|
||||
],
|
||||
body: live.liveRequestBody(5, undefined, [{ chainage_m: 1 }]),
|
||||
suggestions,
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
/* 349 배수유역 자동/수동 흐름 헬퍼 — 화면 파일 `B05_Profile_UI_Drainage_Live.ts` · `_Run.ts` ·
|
||||
* `_Suggest.ts` 를 그 자리에서 트랜스파일해 가짜 DOM · 시계 · fetch 로 돌리고 단계별 상태를 JSON 으로 냄.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const B05 = path.join(__dirname, "..", "..", "B05_Profile");
|
||||
|
||||
/* ---- 가짜 DOM ---- */
|
||||
function el(tag) {
|
||||
const set = new Set();
|
||||
return {
|
||||
tag,
|
||||
children: [],
|
||||
hidden: false,
|
||||
className: "",
|
||||
textContent: "",
|
||||
title: "",
|
||||
disabled: false,
|
||||
checked: false,
|
||||
listeners: {},
|
||||
classList: {
|
||||
toggle: (name, on) => (on ? set.add(name) : set.delete(name)),
|
||||
add: (name) => set.add(name),
|
||||
contains: (name) => set.has(name),
|
||||
},
|
||||
append(...kids) {
|
||||
this.children.push(...kids);
|
||||
},
|
||||
before(kid) {
|
||||
this.beforeNode = kid;
|
||||
},
|
||||
replaceChildren(...kids) {
|
||||
this.children = kids;
|
||||
},
|
||||
addEventListener(type, fn) {
|
||||
(this.listeners[type] ??= []).push(fn);
|
||||
},
|
||||
fire(type) {
|
||||
(this.listeners[type] ?? []).forEach((fn) => fn());
|
||||
},
|
||||
};
|
||||
}
|
||||
global.document = { createElement: el };
|
||||
|
||||
/* ---- 가짜 시계 ---- */
|
||||
let clock = 0;
|
||||
let timerId = 0;
|
||||
const timers = new Map();
|
||||
global.window = {
|
||||
setTimeout(fn, ms) {
|
||||
timers.set(++timerId, { fn, at: clock + ms });
|
||||
return timerId;
|
||||
},
|
||||
clearTimeout(id) {
|
||||
timers.delete(id);
|
||||
},
|
||||
};
|
||||
function advance(ms) {
|
||||
clock += ms;
|
||||
for (const [id, timer] of [...timers]) {
|
||||
if (timer.at <= clock) {
|
||||
timers.delete(id);
|
||||
timer.fn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 가짜 fetch(배수유역 live) ---- */
|
||||
const calls = [];
|
||||
global.fetch = (url, init) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const call = { url, body: JSON.parse(init.body), aborted: false };
|
||||
call.answer = (payload) => resolve({ ok: true, status: 200, json: async () => payload });
|
||||
init.signal.addEventListener("abort", () => {
|
||||
call.aborted = true;
|
||||
reject(new DOMException("aborted", "AbortError"));
|
||||
});
|
||||
calls.push(call);
|
||||
});
|
||||
const flush = async () => {
|
||||
for (let i = 0; i < 10; i += 1) await new Promise((r) => setImmediate(r));
|
||||
};
|
||||
|
||||
/* ---- 모듈 읽기(그 자리 트랜스파일 · 바깥 모듈은 가짜) ---- */
|
||||
const state = new Map();
|
||||
const detailCalls = [];
|
||||
const stubs = {
|
||||
"@config/config_frontend": { API_BASE_URL: "/api" },
|
||||
"@ui/ui_template_progress": { createProgressCircle: () => ({ root: el("progress") }) },
|
||||
// ⚙ 창 — 둘째 줄(자동) 라디오를 고름
|
||||
"@ui/ui_template_modal": {
|
||||
openModal: (options) => {
|
||||
const body = el("modal");
|
||||
options.mount(body, () => undefined);
|
||||
body.children[1].children[0].fire("change");
|
||||
},
|
||||
},
|
||||
"../A00_Common/b_page_state": {
|
||||
readStateRaw: (key) => state.get(key) ?? null,
|
||||
writeStateRaw: (key, value) => state.set(key, value),
|
||||
},
|
||||
"../B04_PreProcess/B04_PreProcess_Api_Fetch": {
|
||||
computeDetailBasins: async (projectId, points) => {
|
||||
detailCalls.push(points);
|
||||
return { basins: [], pipe_points: points, stale: null };
|
||||
},
|
||||
},
|
||||
"./B05_Profile_UI_Drainage_Parts": { analyzeFailed: (_error, fallback) => fallback },
|
||||
};
|
||||
const cache = new Map();
|
||||
function load(file) {
|
||||
if (cache.has(file)) return cache.get(file);
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(file, box.exports);
|
||||
const local = (name) => {
|
||||
if (stubs[name]) return stubs[name];
|
||||
if (name.startsWith(".")) return load(path.join(path.dirname(file), `${name}.ts`));
|
||||
return require(name);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, local);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const { createDrainageLive } = load(path.join(B05, "B05_Profile_UI_Drainage_Live.ts"));
|
||||
const { createDetailRunner } = load(path.join(B05, "B05_Profile_UI_Drainage_Run.ts"));
|
||||
|
||||
async function main() {
|
||||
const out = {};
|
||||
const viewport = el("viewport");
|
||||
const header = el("header");
|
||||
const root = el("root");
|
||||
root.querySelector = (selector) =>
|
||||
selector === ".b05-drainage__viewport"
|
||||
? viewport
|
||||
: selector === ".b05-drainage__header"
|
||||
? header
|
||||
: null;
|
||||
let pipes = [
|
||||
{ chainage_m: 100, id: "a1", source: "stream", options: { pipe_diameter_mm: 1000 } },
|
||||
];
|
||||
let applied = 0;
|
||||
let remembered = 0;
|
||||
let live;
|
||||
const status = el("status");
|
||||
const runner = createDetailRunner({
|
||||
projectId: () => "p1",
|
||||
analyzeButton: el("button"),
|
||||
status,
|
||||
showProgress: () => undefined,
|
||||
apply: (response) => {
|
||||
applied += 1;
|
||||
live.suggest.update(response);
|
||||
if (response.stale) live.markStale(); // 패널 apply 와 같은 줄
|
||||
},
|
||||
basinCount: () => 0,
|
||||
scheduleDraw: () => undefined,
|
||||
label: (key) => key,
|
||||
});
|
||||
live = createDrainageLive({
|
||||
root,
|
||||
basinList: el("list"),
|
||||
pipeRequest: () => pipes,
|
||||
runner,
|
||||
rememberPipes: () => (remembered += 1),
|
||||
});
|
||||
const [veil, busy] = viewport.children;
|
||||
const update = veil.children[0];
|
||||
const gear = header.children[0];
|
||||
const look = () => ({
|
||||
stale: live.stale,
|
||||
blur: root.classList.contains("is-stale"),
|
||||
veil: !veil.hidden,
|
||||
busy: !busy.hidden,
|
||||
fetches: calls.length,
|
||||
});
|
||||
const draft = (rev, x) => ({
|
||||
rev,
|
||||
nodes: [
|
||||
[0, 0],
|
||||
[x, 300],
|
||||
],
|
||||
curveOn: [true, true],
|
||||
});
|
||||
const stage = (route) => live.stage({ inputs: { route, pipes } });
|
||||
const answer = (call, extra = {}) =>
|
||||
call.answer({
|
||||
status: "success",
|
||||
generation: call.body.generation,
|
||||
basins: [{ chainage_m: 100, recommended_facility: "pipe", recommended_diameter_mm: 1200 }],
|
||||
pipe_points: pipes,
|
||||
pipe_candidates: [{ chainage_m: 200 }],
|
||||
stale: null,
|
||||
live: true,
|
||||
...extra,
|
||||
});
|
||||
|
||||
// 수동 — 꼭짓점 옮김 = 흐림 + [업데이트] · 판 번호만 바뀐 같은 노선 = 그대로
|
||||
stage(draft(1, 50));
|
||||
out.moved = look();
|
||||
stage(draft(2, 50));
|
||||
out.sameAgain = look();
|
||||
// [업데이트] → 요청 하나 · 묶음이 같은 입력으로 다시 돌아도 답을 받음
|
||||
update.fire("click");
|
||||
out.sent = { ...look(), body: calls[0].body };
|
||||
stage(draft(3, 50));
|
||||
out.rerunWhileBusy = { ...look(), aborted: calls[0].aborted };
|
||||
answer(calls[0]);
|
||||
await flush();
|
||||
out.answered = { ...look(), applied };
|
||||
out.suggest = {
|
||||
shown: !live.suggest.root.hidden,
|
||||
title: live.suggest.root.children[0].children[0].textContent,
|
||||
rows: live.suggest.root.children[1].children.map((row) => row.textContent),
|
||||
};
|
||||
// 제안 [적용] — 관 초안으로 세부 유역 다시 나눔 · 세션 초안에 담음
|
||||
live.suggest.root.children[0].children[1].fire("click");
|
||||
await flush();
|
||||
out.applied = { points: detailCalls[0], remembered };
|
||||
|
||||
// 다시 옮김 → 요청 → 그 사이 또 옮김 = 앞 요청 끊김 · 다시 흐림
|
||||
stage(draft(4, 80));
|
||||
update.fire("click");
|
||||
stage(draft(5, 90));
|
||||
out.movedWhileBusy = { ...look(), aborted: calls[1].aborted };
|
||||
update.fire("click");
|
||||
answer(calls[2]);
|
||||
await flush();
|
||||
out.fresh = look();
|
||||
|
||||
// 노선 [저장] 뒤 — 서버 답이 「낡음」 = 흐림 + [업데이트](수동)
|
||||
stage(null);
|
||||
await runner.run(async () => ({ basins: [], pipe_points: pipes, stale: "배수유역 낡음" }));
|
||||
out.savedStale = { ...look(), status: status.textContent };
|
||||
|
||||
// ⚙ 자동 — 바꾸는 순간 3 s 뒤 셈 · 흐림 막 · [업데이트] 없음
|
||||
gear.fire("click");
|
||||
out.autoSwitch = look();
|
||||
advance(2999);
|
||||
out.auto2999 = look();
|
||||
advance(1);
|
||||
out.auto3000 = { ...look(), route: calls[3].body.route };
|
||||
answer(calls[3]);
|
||||
await flush();
|
||||
out.autoDone = look();
|
||||
// 자동 · 꼭짓점 옮김 = 조용히 3 s 뒤 · 그 사이 또 옮기면 처음부터
|
||||
stage(draft(6, 120));
|
||||
advance(2000);
|
||||
stage(draft(7, 130));
|
||||
advance(2000);
|
||||
out.autoRestart = look();
|
||||
advance(1000);
|
||||
answer(calls[4]);
|
||||
await flush();
|
||||
out.autoMoved = { ...look(), mode: state.get("drainage-mode") };
|
||||
// 자동 · 저장 뒤 낡음 = 3 s 뒤 조용히
|
||||
await runner.run(async () => ({ basins: [], pipe_points: pipes, stale: "배수유역 낡음" }));
|
||||
out.autoSavedStale = look();
|
||||
advance(3000);
|
||||
answer(calls[5]);
|
||||
await flush();
|
||||
out.autoSavedDone = look();
|
||||
// 입력 지문 — 판 번호 · 관 차례는 안 봄 · 꼭짓점 · 관 자리(cm) · id 는 봄
|
||||
const { inputKey } = load(path.join(B05, "logic", "B05_Profile_Drainage_Live.ts"));
|
||||
const a = { id: "a1", chainage_m: 100 };
|
||||
const b = { id: "b2", chainage_m: 250 };
|
||||
const base = inputKey(draft(1, 50), [a, b]);
|
||||
out.keys = {
|
||||
rev: inputKey(draft(9, 50), [a, b]) === base,
|
||||
order: inputKey(draft(1, 50), [b, a]) === base,
|
||||
node: inputKey(draft(1, 51), [a, b]) === base,
|
||||
pipe: inputKey(draft(1, 50), [a, { ...b, chainage_m: 250.2 }]) === base,
|
||||
id: inputKey(draft(1, 50), [a, { ...b, id: "c3" }]) === base,
|
||||
saved: inputKey(null, null) === inputKey(undefined, undefined),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
process.stderr.write(String(error && error.stack ? error.stack : error));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
"""349 배수유역 자동/수동 흐름 — 화면 파일(`B05_Profile_UI_Drainage_Live` · `_Run` · `_Suggest`)을
|
||||
가짜 DOM · 시계 · fetch 로 돌림(`helper_349_drainage_live_flow.cjs`).
|
||||
|
||||
- 수동: 꼭짓점 옮김 = 흐림 + [업데이트] · [업데이트] 뒤 묶음이 같은 노선 · 관으로 다시 돌아도 답을 받음
|
||||
(입력 지문) · 답이 오면 흐림 · 「계산 중」 사라짐 · 제안 「제안 N건」 · [적용].
|
||||
- 요청 중 또 옮기면 앞 요청 끊고 다시 흐림.
|
||||
- 노선 [저장] 뒤 서버 답이 「낡음」 = 수동은 흐림 + [업데이트] · 자동은 3 s 뒤 조용히.
|
||||
- ⚙ 자동: 3 s 조용한 뒤 셈 · 그 사이 또 옮기면 처음부터 · [업데이트] 막 없음.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_349_drainage_live_flow.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def state(stale, veil, busy, fetches):
|
||||
return {"stale": stale, "blur": stale, "veil": veil, "busy": busy, "fetches": fetches}
|
||||
|
||||
|
||||
def test_manual_update_answer_lands_after_same_rerun(r):
|
||||
assert r["moved"] == state(True, True, False, 0)
|
||||
assert r["sameAgain"] == state(True, True, False, 0) # 판 번호만 바뀜 — 다시 안 흐림
|
||||
assert r["sent"]["fetches"] == 1 and r["sent"]["busy"] and not r["sent"]["veil"]
|
||||
rerun = r["rerunWhileBusy"]
|
||||
assert not rerun["aborted"] and rerun["busy"] # 같은 입력 다시 돎 — 요청 그대로
|
||||
assert r["answered"] == {**state(False, False, False, 1), "applied": 1}
|
||||
|
||||
|
||||
def test_suggestions_and_apply(r):
|
||||
assert r["suggest"]["shown"] and r["suggest"]["title"] == "제안 2건"
|
||||
assert len(r["suggest"]["rows"]) == 2
|
||||
points = {p["chainage_m"]: p for p in r["applied"]["points"]}
|
||||
assert points[100]["options"]["pipe_diameter_mm"] == 1200
|
||||
assert points[200]["source"] == "spacing"
|
||||
assert r["applied"]["remembered"] == 1
|
||||
|
||||
|
||||
def test_move_while_busy_cancels(r):
|
||||
moved = r["movedWhileBusy"]
|
||||
assert moved["aborted"] and moved["veil"] and not moved["busy"]
|
||||
assert r["fresh"] == state(False, False, False, 3)
|
||||
|
||||
|
||||
def test_saved_stale_shows_update(r):
|
||||
assert {k: v for k, v in r["savedStale"].items() if k != "status"} == state(
|
||||
True, True, False, 3
|
||||
)
|
||||
assert r["savedStale"]["status"] == "배수유역 낡음"
|
||||
|
||||
|
||||
def test_auto_mode(r):
|
||||
assert r["autoSwitch"] == state(True, False, False, 3) # 자동 — 흐림만 · [업데이트] 없음
|
||||
assert r["auto2999"]["fetches"] == 3
|
||||
assert r["auto3000"]["fetches"] == 4 and r["auto3000"]["route"] is None
|
||||
assert r["autoDone"] == state(False, False, False, 4)
|
||||
assert r["autoRestart"] == state(True, False, False, 4) # 2 s + 2 s — 처음부터 다시 3 s
|
||||
assert r["autoMoved"] == {**state(False, False, False, 5), "mode": "auto"}
|
||||
assert r["autoSavedStale"] == state(True, False, False, 5)
|
||||
assert r["autoSavedDone"] == state(False, False, False, 6)
|
||||
|
||||
|
||||
def test_input_key(r):
|
||||
assert r["keys"] == {
|
||||
"rev": True,
|
||||
"order": True,
|
||||
"node": False,
|
||||
"pipe": False,
|
||||
"id": False,
|
||||
"saved": True,
|
||||
}
|
||||
Reference in New Issue
Block a user