- B04 Router_Watershed — 분석 응답 · 단계 산출물 저장을 Output.write_analysis 로 뗌(primary-region 같은 길) · 도엽 좌표 저장본 메모리 보관(2 벌) - B04 Router_Watershed_Live — 저장 노선 기준(초안 없음 · 지문 같음) 답이면 마지막 끊김 확인 뒤 분석을 남김 · design/save 잠금 중이면 안 남김 · 남길 때 같은 잠금 쥠 · POST /drainage/live/cancel(그 차례가 마지막일 때만 지움) · 셈 멈춤 로그 - B05 Drainage_Live — 요청 끊을 때 서버에 끊김 알림 · 저장 노선 [업데이트] 답이 저장분 낡음 걷음 · 되돌리기로 초안이 없어지면 저장분의 마지막 낡음으로 표시 바로 - 시험 test_349_drainage_live_flow 고침 — 되돌리기 직후 낡음 기대가 옛 동작(답 올 때까지 낡음) · 새 동작 = 저장분 마지막 낡음(여기선 꺼짐) + 끊김 알림 확인 - 시험 test_347_2 — 「작업본에 안 씀」 옛 설명 고침 · 남김 · 초안 안 남김 · 잠금 중 안 남김 · 끊김 알림 시험 더함 · test_347_1 메모리 보관 확인 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
323 lines
11 KiB
JavaScript
323 lines
11 KiB
JavaScript
/* 349 · 326-6 배수유역 [업데이트] 흐름 헬퍼 — 화면 파일 `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 = [];
|
|
const cancels = []; // 끊김 알림(520-2) — 끊은 차례 번호
|
|
global.fetch = (url, init) =>
|
|
new Promise((resolve, reject) => {
|
|
const call = { url, body: JSON.parse(init.body), aborted: false };
|
|
if (url.endsWith("/drainage/live/cancel")) {
|
|
cancels.push(call.body.generation);
|
|
return resolve({ ok: true, status: 200, json: async () => ({ status: "success" }) });
|
|
}
|
|
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 = [];
|
|
let records = 0; // 사진첩 한 걸음(369)
|
|
const stubs = {
|
|
"@config/config_frontend": { API_BASE_URL: "/api" },
|
|
"@ui/ui_template_progress": { createProgressCircle: () => ({ root: el("progress") }) },
|
|
"@ui/ui_template_locale": {
|
|
currentLanguageIndex: 0,
|
|
ui_locales: new Proxy({}, { get: (_target, key) => [key, key] }),
|
|
},
|
|
"../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");
|
|
const propose = el("button");
|
|
const update = el("button");
|
|
header.querySelector = (selector) => (selector === ".b05-drainage__propose" ? propose : null);
|
|
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 savedLoads = 0;
|
|
let savedAnswer = null;
|
|
let remembered = 0;
|
|
let live;
|
|
const status = el("status");
|
|
const runner = createDetailRunner({
|
|
projectId: () => "p1",
|
|
updateButton: update,
|
|
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,
|
|
updateButton: update,
|
|
basinList: el("list"),
|
|
pipeRequest: () => pipes,
|
|
runner,
|
|
// 패널이 넘기는 꼴 그대로 — 초안에 담음 + 사진첩 한 걸음(369-4 · onPipesDrafted)
|
|
rememberPipes: () => ((remembered += 1), (records += 1)),
|
|
// 저장분 다시 받기(349-0i) — 답은 시험이 정할 때 옴
|
|
loadSaved: () => {
|
|
savedLoads += 1;
|
|
return runner.run(() => new Promise((resolve) => (savedAnswer = resolve)), true);
|
|
},
|
|
});
|
|
const [busy] = viewport.children;
|
|
const look = () => ({
|
|
stale: live.stale,
|
|
pending: update.classList.contains("is-pending"),
|
|
off: update.disabled,
|
|
label: update.textContent,
|
|
busy: !busy.hidden,
|
|
fetches: calls.length,
|
|
applyOff: live.suggest.root.children[0].children[1].disabled,
|
|
applyTitle: live.suggest.root.children[0].children[1].title,
|
|
});
|
|
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,
|
|
});
|
|
|
|
out.start = look();
|
|
// 꼭짓점 옮김 = 낡음 표시만 · 저절로 셈 없음 · 판 번호만 바뀐 같은 노선 = 그대로
|
|
stage(draft(1, 50));
|
|
out.moved = look();
|
|
stage(draft(2, 50));
|
|
out.sameAgain = look();
|
|
advance(60000);
|
|
out.noAuto = 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 };
|
|
// 낡은 동안 제안 [적용] — 끔 · 눌러도 옛 제안이 안 들어감
|
|
live.suggest.root.children[0].children[1].fire("click");
|
|
await flush();
|
|
out.staleApply = detailCalls.length;
|
|
// [업데이트] 약속 — 답이 화면에 오르면 참(B06 [>] 확인 창 뒤 이동 · 326-6)
|
|
const done = live.refresh();
|
|
answer(calls[2]);
|
|
out.promised = await done;
|
|
await flush();
|
|
out.fresh = look();
|
|
// 낡지 않아도 [업데이트] = 다시 셈(노선 · 관 그대로 · 326-6)
|
|
update.fire("click");
|
|
out.again = { ...look(), route: calls[3].body.route };
|
|
answer(calls[3]);
|
|
await flush();
|
|
out.againDone = look();
|
|
|
|
// 노선 [저장] 뒤 — 서버 답이 「낡음」 = 낡음 표시 · 저절로 셈 없음
|
|
stage(null);
|
|
await runner.run(async () => ({ basins: [], pipe_points: pipes, stale: "배수유역 낡음" }));
|
|
out.savedStale = { ...look(), status: status.textContent };
|
|
advance(60000);
|
|
out.savedStaleNoAuto = look();
|
|
update.fire("click");
|
|
answer(calls[4]);
|
|
await flush();
|
|
out.savedDone = look();
|
|
|
|
// 되돌리기 — 끌기 → [업데이트] 요청 중 → Ctrl+Z = 초안용 답 끊고(서버에도 알림) 저장분 ·
|
|
// 표시는 저장분의 마지막 낡음으로 바로(520-2)
|
|
const savedOk = {
|
|
basins: [{ chainage_m: 100, recommended_facility: "pipe", recommended_diameter_mm: 1200 }],
|
|
pipe_points: pipes,
|
|
stale: null,
|
|
};
|
|
stage(draft(21, 310));
|
|
update.fire("click");
|
|
const draftCall = calls[calls.length - 1];
|
|
stage(null);
|
|
out.undoWaiting = { ...look(), aborted: draftCall.aborted, savedLoads };
|
|
out.undoCancelled = cancels.includes(draftCall.body.generation);
|
|
savedAnswer(savedOk);
|
|
await flush();
|
|
out.undo = { ...look(), savedLoads };
|
|
// 저장분 자체가 낡음 = 낡음 표시 그대로
|
|
stage(draft(22, 320));
|
|
stage(null);
|
|
savedAnswer({ basins: [], pipe_points: pipes, stale: "배수유역 낡음" });
|
|
await flush();
|
|
out.undoSavedStale = { ...look(), savedLoads };
|
|
// [자동 제안] — 낡은 동안 끔 · 새 답이면 켬
|
|
out.proposeStale = { off: propose.disabled, title: propose.title };
|
|
live.markFresh();
|
|
out.proposeFresh = { off: propose.disabled, title: propose.title };
|
|
out.records = records;
|
|
// 입력 지문 — 판 번호 · 관 차례는 안 봄 · 꼭짓점 · 관 자리(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);
|
|
});
|