Files
Aislo/resources/tester/helper_342_1_station_line.mjs
T
eomsangdonandClaude Opus 5.5 da3dfc1ca8 feat(종단보기): 346-2 덧 2 B05 종단 보기 · 3D 측점선도 갈아끼운 상세로 — 보일 때만 · 숨었으면 다시 보일 때 · 횡단 보기 없이도
- design_view_profile_sections: 노선마다 상세 갈아끼움을 듣고 종단도 · 표 · 3D 측점선 다시(보일 때만 · 숨으면 flushSwap 이 다시 보일 때) · 가게 stations 도 들어 횡단 보기를 안 열어도 갈아끼움(먼저 부른 쪽만 · 다음은 「같은 목록」)
- design_view_profile: 받는 줄만(보임 · 떠날 때 끊음 · 다시 보일 때 flushSwap)
- 갈아끼움 알림판 B06_Section_Swap_Bus(가져오기 없음 · 돌고 도는 가져오기 막음) — 조정창 값 · 암 경계 옮김을 어느 보기가 갈아끼웠든 한 번 · 횡단 보기는 알림으로 다시 그림
- 시험: 종단 보기 보임 7 → 8 · 숨음 = 안 그림 · 다시 보임 = 6 · 3D 측점선 같은 차례 · 옛 시험 둘(342-1 화면 선 = listedBase · 323-9 열쇠 모듈 진짜로) 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
2026-10-16 12:28:11 +09:00

327 lines
12 KiB
JavaScript

/* 342-1 · 346-2 헬퍼 — vite ssr 로 묶은 진짜 모듈(helper_342_1_entry.ts)을 부르고 결과를 JSON 한 벌로.
* node helper_342_1_station_line.mjs <묶음.mjs> <입력.json>
* 입력 = { standard: 파이썬 config 표준단면, policy: 종단 정책 } · 화면 선 = design_profile.ts 의 진짜
* `listed()` 본문(소스에서 떼어 트랜스파일 · helper_177_3 방식).
*/
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, "..", "..");
const require = createRequire(import.meta.url);
const ts = require(join(ROOT, "config", "node_modules", "typescript"));
const store = () => {
const m = new Map();
return {
getItem: (k) => (m.has(k) ? m.get(k) : null),
setItem: (k, v) => m.set(k, String(v)),
removeItem: (k) => m.delete(k),
key: (i) => [...m.keys()][i] ?? null,
get length() {
return m.size;
},
clear: () => m.clear(),
keys: () => [...m.keys()],
};
};
globalThis.window = globalThis;
globalThis.sessionStorage = store();
globalThis.localStorage = store();
// 토스트 등 DOM 을 건드리는 자리 — 무엇을 불러도 받아 주는 빈 객체
const any = new Proxy(function () {}, {
get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any),
apply: () => any,
construct: () => any,
});
globalThis.document = any;
globalThis.requestAnimationFrame = (run) => setTimeout(run, 0);
const warns = [];
console.warn = (...args) => warns.push(args.map(String).join(" "));
const [bundlePath, inputPath] = process.argv.slice(2);
const m = await import(pathToFileURL(bundlePath).href);
const input = JSON.parse(readFileSync(inputPath, "utf8"));
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const transpile = (code) =>
ts.transpileModule(code, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const PID = "p342";
const RID = 7;
m.writeState("std-cross", input.standard, PID);
/* ── 견본 노선 0~160 m — 서버 비정규 측점 85.054(0.001 m) · 관 지점 85.05(0.01 m) · 종단곡선 안 ── */
const SERVER_IRR = 85.054;
const PIPE = 85.05;
const regular = [0, 20, 40, 60, 80, 100, 120, 140, 160];
const serverStations = [...regular, SERVER_IRR]
.sort((a, b) => a - b)
.map((c) => ({
station_id: `s${c}`,
chainage_m: c,
kind: c === SERVER_IRR ? "irregular" : c === 0 ? "bp" : "regular",
label: String(c),
...(c === SERVER_IRR ? { structure: "파형강관 D800" } : {}),
}));
const groundAt = (c) => 100 + 0.08 * c - 6 * Math.sin(c / 23);
const chainage = Array.from({ length: 161 }, (_, i) => i);
const basePvi = serverStations.map((s, i) => ({
chainage_m: s.chainage_m,
elevation_m: groundAt(s.chainage_m) + (i % 2 ? 1.6 : -1.3),
}));
const base = {
policy: input.policy,
onlyExplicitCurves: false,
basePvi,
chainage,
ground: chainage.map(groundAt),
stations: serverStations.map((s) => ({ station_id: s.station_id, chainage_m: s.chainage_m })),
};
const stored = m.buildAlignment(base, m.emptyEdits());
const crossSamples = (c) =>
Array.from({ length: 49 }, (_, i) => {
const offset = -12 + i * 0.5;
return { offset_m: offset, elevation_m: groundAt(c) + 0.3 * offset, valid: true };
});
const freshDesign = () => ({
design_elevation_m: 0,
ground_type: "soil",
section_mode: "left_cut",
ditch_side: "left",
ditch_type: "standard",
two_stage_slope: false,
});
const makeDetail = () => ({
longitudinal: {
length_m: 160,
stations: serverStations.map((s) => ({ ...s })),
samples: chainage.map((c) => ({ chainage_m: c, elevation_m: groundAt(c), valid: true })),
profile_alignment: JSON.parse(JSON.stringify(stored)),
design_profiles: [m.toDesignProfile(stored, undefined)],
},
cross_sections: serverStations.map((s) => ({
station_id: s.station_id,
chainage_m: s.chainage_m,
samples: crossSamples(s.chainage_m),
design: freshDesign(),
})),
});
const irregularStations = [
{ id: "pipe-1", station: 4, remainder: 5.05, chainage_m: PIPE, structure: "파형강관 D800" },
];
/* 화면 선 = 종단도가 쓰는 진짜 `listedBase`(design_profile_base · 옛 design_profile 안 listed()) */
const screenBase = (detail) => m.listedBase(m.toAlignmentBase(stored), detail, irregularStations);
const staleOf = (detail, line) =>
m.staleKey({
longitudinal: { design_profiles: [m.toDesignProfile(line, undefined)] },
cross_sections: detail.cross_sections,
});
const refresh = (detail, extra = {}) =>
m.refreshCrossDesigns({
projectId: PID,
routeId: RID,
detail,
edits: m.emptyEdits(),
...extra,
});
const out = {};
/* 1. 머묾 재현 — 고치기 전 길(저장분 선) 대 화면 선 */
{
const detail = makeDetail();
await refresh(detail, { alignment: stored }); // 저장본 = 서버가 저장분 선으로 낸 설계
const screen = m.buildAlignment(screenBase(detail), m.emptyEdits());
out.screenStations = screen.stations.map((s) => s.chainage_m);
out.staleBefore = m.staleChainages(staleOf(detail, screen));
await refresh(detail); // 옛 길 — 맡긴 선 없음 → 저장분 선으로 다시 셈
out.staleOldPath = m.staleChainages(staleOf(detail, screen));
const release = m.holdScreenAlignment((id) => (id === RID ? screen : null));
out.staleAgainstScreenBefore = m.staleAgainstScreen(detail, RID);
await refresh(detail); // 새 길 — 종단도가 맡긴 화면 선(B06 · 3D 와 같은 길)
out.staleNewPath = m.staleChainages(staleOf(detail, screen));
out.staleAgainstScreenAfter = m.staleAgainstScreen(detail, RID);
// B06 판정(다시 셈이 얹은 design_profiles)과 B05 판정(화면 선)이 같은가
out.staleB06View = m.staleChainages(
m.staleKey({
longitudinal: detail.longitudinal,
cross_sections: detail.cross_sections,
}),
);
release();
}
/* 2. 국부 연산 — 같은 입력 다시 셈 0 · 계획고 ▲ 한 번 = 몇 측점만 · 결과는 통째 셈과 같음 */
{
const detail = makeDetail();
const screen = m.buildAlignment(screenBase(detail), m.emptyEdits());
await refresh(detail, { alignment: screen });
await refresh(detail, { alignment: screen }); // 첫 셈이 결과 칸(측구 유무 등)을 채운 뒤 한 번 더 — 저장본 상태
const ids = () => detail.cross_sections.map((s) => s.design);
let before = ids();
await refresh(detail, { alignment: screen });
out.recomputedSame = ids().filter((d, i) => d !== before[i]).length;
const edited = m.buildAlignment(screenBase(detail), {
station_offsets: { "140.000": 0.3 },
curve_radii: {},
});
before = ids();
const changed = await refresh(detail, { alignment: edited });
out.recomputedEdit = ids().filter((d, i) => d !== before[i]).length;
out.changedEdit = changed.length;
out.total = detail.cross_sections.length;
// 통째 셈(새 측점 객체 = 기억 없음)과 결과 같음
const whole = makeDetail();
whole.cross_sections.forEach((s, i) => (s.design = { ...detail.cross_sections[i].design }));
await refresh(whole, { alignment: stored });
await refresh(whole, { alignment: edited });
out.sameAsWhole =
JSON.stringify(whole.cross_sections.map((s) => s.design)) ===
JSON.stringify(detail.cross_sections.map((s) => s.design));
}
/* 3. 다시 셈 뒤에도 남은 낡음 — 기다림(true) → 남음(false) · 콘솔 경고 · 다시 그림 */
{
const detail = makeDetail();
const screen = m.buildAlignment(screenBase(detail), m.emptyEdits());
await refresh(detail, { alignment: screen });
const broken = detail.cross_sections[3];
broken.samples = []; // 다시 셈이 못 고치는 측점
broken.design.design_elevation_m += 1;
const applied = [];
const preview = m.createCrossPreview({
projectId: PID,
detail: () => detail,
routeId: () => RID,
edits: () => m.emptyEdits(),
alignment: () => screen,
debounceMs: () => 0,
onApplied: (updated) => applied.push(updated.length),
});
const key = staleOf(detail, screen);
out.settleFirst = preview.scheduleIfStale(key);
await sleep(50);
const again = staleOf(detail, screen);
out.settleSameKey = again === key;
out.settleAfter = preview.scheduleIfStale(again);
out.settleWarns = warns.filter((w) => w.includes("낡은 측점")).length;
out.settleRedraws = applied.length;
out.settleClear = preview.scheduleIfStale("");
preview.dispose();
}
/* 4. 흙 배분 — 실패한 열쇠 다시 보냄 · 받은 열쇠는 다시 안 물음 */
{
const replies = [false, true, true];
const posts = [];
globalThis.fetch = async (_url, init) => {
posts.push(JSON.parse(init.body).result.points.length);
const ok = replies.shift() ?? true;
return {
ok,
json: async () => ({ haul_plan: { tag: JSON.parse(init.body).result.points.length } }),
};
};
let ready = 0;
const prefetch = m.createHaulPlanPrefetch(
PID,
() => RID,
() => (ready += 1),
);
const A = { points: [{ cumulative_volume_m3: 1 }, { cumulative_volume_m3: 2 }] };
const B = {
points: [{ cumulative_volume_m3: 1 }, { cumulative_volume_m3: 2 }, { cumulative_volume_m3: 3 }],
};
prefetch.schedule(A);
await sleep(450);
out.haulAfterFail = posts.length; // 1 — 실패
prefetch.schedule(A); // 같은 열쇠 — 다시 보냄
await sleep(450);
out.haulRetry = posts.length; // 2
prefetch.schedule(B);
await sleep(450);
out.haulB = posts.length; // 3
prefetch.schedule(A); // 받아 둔 열쇠 — 안 물음
await sleep(450);
out.haulKept = posts.length; // 3
out.haulKeptPlan = prefetch.current()?.tag; // 2 (A 의 점 2개)
out.haulReady = ready;
prefetch.dispose();
}
/* 5. [전체 측점 반영] — 다시 셈 한 번 · 측점 선택을 세션에 안 박음 */
{
const detail = makeDetail();
const screen = m.buildAlignment(screenBase(detail), m.emptyEdits());
const release = m.holdScreenAlignment((id) => (id === RID ? screen : null));
const cardsCalls = [];
const sync = m.createDesignSync({
projectId: PID,
routeId: () => RID,
detail: () => detail,
view: () => ({
refreshCards: (updated) => cardsCalls.push(updated.length),
refreshCard: () => cardsCalls.push(-1),
detailChart: () => null,
}),
});
const t0 = performance.now();
await sync.applyPanelToAll();
out.applyAllMs = performance.now() - t0;
out.applyAllCalls = cardsCalls;
out.applyAllChoiceKeys = sessionStorage.keys().filter((k) => k.includes("crossdesign")).length;
release();
}
/* 6. 횡단 카드 보이는 것 먼저 — B06_Section_UI_Section_View.ts 의 진짜 refreshCards 본문 */
{
const viewSrc = readFileSync(join(ROOT, "B06_Section", "B06_Section_UI_Section_View.ts"), "utf8");
const body = viewSrc.match(/\r?\n const refreshCards = [\s\S]*?\r?\n \};\r?\n/)[0];
const rebuilt = [];
let redraws = 0;
const top = { 0: 0, 20: 150, 40: 300, 60: 450, 80: 600 }; // 카드 위치(px) · 화면 = 0~320
const factory = new Function(
"cards",
"cardAt",
"rebuildCard",
"offscreen",
"currentDetail",
"redrawProfile",
"fillSlopeNotice",
"cachedStationInterval",
`${transpile(body)}\nreturn refreshCards;`,
);
let offscreen;
const rebuildCard = (c) => {
offscreen.queue.delete(c);
rebuilt.push(c);
return true;
};
offscreen = m.createIdleQueue(rebuildCard, 2);
const refreshCards = factory(
{ getBoundingClientRect: () => ({ top: 0, bottom: 320 }) },
(c) => ({
section: {},
el: { getBoundingClientRect: () => ({ top: top[c], bottom: top[c] + 140, height: 140 }) },
}),
rebuildCard,
offscreen,
{ cross_sections: [] },
() => (redraws += 1),
{ update: () => undefined },
20,
);
refreshCards([80, 0, 60, 20, 40]);
out.cardsNow = [...rebuilt];
out.cardsRedraws = redraws;
await sleep(120);
out.cardsAll = [...rebuilt];
}
process.stdout.write(JSON.stringify(out));