Files
Aislo/resources/tester/helper_445_save_patch.cjs
T
eomsangdonandClaude Opus 5.5 857b2d89d4 fix(445 B06 저장 patch): 바탕 = 캐시 설계 사용자 칸 + 세션 얹음 · 되돌림 null · 옮긴 측점 입력 · 집수정 범위 · 버린 수
- H3 — 횡단 patch 바탕을 캐시 설계 사용자 칸(측점마다 통째)으로 · 세션 고침은 벽 · 단 · 쪽 열쇠마다 합침(mergeCrossPatches)
- M2 — 벽 ↺ · 연동 풂 = 세션 null(안쪽 열쇠 지움) · 추가 벽 0 · 빈 전체 폭 0 을 세션에 남김 · 새 탭 − 가 저장분 단 수에서 출발
- H2 — 옮긴 관 · 구조물(정본 행 없음) = 지반 등 입력(CARRY_INPUT_KEYS)까지 실음 · 화면도 들고 감 · 서버 기본 설계가 그 입력으로 섬
- H5 — 노선 바꾼 [저장] 본문에 카드 선택 · 암선 · 암 경사 · 표준 횡단면 · 풍선 자리
- M1 — 집수정 내공 화면 범위 = 서버 1.0~2.0
- M5 — /save · /confirm 응답에 버린 patch 측점 · 기본 설계 실패 · 측점 맞춤 실패 · 화면 알림
- M7 — kindreset 을 측점 옮김 · 노선 저장에 따라 옮김
- 시험 test_445_save_patch · 기존 시험 꼴 맞춤(318 · 384 · 428 · 429 · fills_empty)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
2026-10-18 18:55:25 +09:00

195 lines
7.5 KiB
JavaScript

/* PLAN 445 헬퍼 — [저장] 횡단 patch 바탕 · 세션 얹음 · 되돌림 · 옮긴 측점 입력.
* 조정창(`createStationControls`)으로 고친 뒤 횡단 보기 [저장] 본문(`buildCrossPatches` → `cachedCrossPatches`)을
* 그대로 만들어 내놓음 — 시험이 서버 병합(`_apply_section_edits` → `merge_cross_section_design_batches`)에 넣어
* DB 설계를 견줌. TS 는 그 자리에서 트랜스파일 · 화면 · 서버 창구는 가짜. 짝 = `test_445_save_patch.py`.
* node helper_445_save_patch.cjs <job.json>
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const B06 = (name) => path.join(ROOT, "B06_Section", `${name}.ts`);
const memory = () => {
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(),
};
};
globalThis.sessionStorage = memory();
globalThis.localStorage = memory();
globalThis.window = globalThis;
const anything = () =>
new Proxy({}, { get: (_target, key) => (key === "__esModule" ? false : () => undefined) });
function makeLoader(real, stubs = {}) {
const cache = new Map();
const realSet = new Set(real.map((file) => path.resolve(file)));
const load = (file) => {
const full = path.resolve(file);
if (cache.has(full)) return cache.get(full);
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
cache.set(full, box.exports);
const need = (spec) => {
if (spec in stubs) return stubs[spec];
let target = null;
if (spec.startsWith("@util/")) target = path.join(ROOT, "common_util", `${spec.slice(6)}.ts`);
else if (spec.startsWith(".")) target = path.join(path.dirname(full), `${spec}.ts`);
if (target && realSet.has(path.resolve(target))) return load(target);
return anything();
};
new Function("exports", "module", "require", js)(box.exports, box, need);
return box.exports;
};
return load;
}
const bodyStub = () => ({
control: { selectedFor: () => false, select: () => undefined },
byChainage: () => new Map(),
load: () => undefined,
});
const load = makeLoader(
[
B06("B06_Section_Section_Store"),
B06("B06_Section_Section_Canon"),
B06("B06_Section_Section_Adjusts"),
B06("B06_Section_Cross_Refresh"),
B06("B06_Section_Api_Culvert_Options"),
B06("B06_Section_UI_Page_Station_Controls"),
B06("B06_Section_UI_Page_Link_Session"),
B06("B06_Section_UI_Cross_Culvert_Types"),
B06("logic/B06_Section_Station_Swap"),
B06("logic/B06_Section_UI_Page_Patches"),
path.join(ROOT, "A00_Common", "b_page_state.ts"),
path.join(ROOT, "A00_Common", "design", "design_selection.ts"),
],
{
"./B06_Section_Api_Fetch": {
fetchSectionDetail: async () => job.detail,
saveSections: async () => ({}),
},
"./B06_Section_UI_Page_Ford_Controls": {
createBodyControls: () => ({ ford: bodyStub(), box: bodyStub() }),
},
"./B06_Section_UI_Page_Span_Control": {
createSpanControl: () => ({
control: {},
spanKeyOf: (chainageM, wall) => `${chainageM.toFixed(2)}:${wall}`,
}),
},
"./B06_Section_Section_Drafts": {
withDraftStations: async () => undefined,
withDraftShapes: async () => undefined,
forgetDraftStations: () => undefined,
supersedeDraftStations: () => undefined,
},
"./B06_Section_Structure_Layouts": {
applyStructureAreaRows: () => undefined,
structureAreaRows: () => [],
computeStoredLayouts: () => null,
},
"../B05_Profile/B05_Profile_Api_PlusStations": {
loadSavedPlusStations: async () => undefined,
plusStationsOf: () => [],
},
},
);
const job = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const P = "p445";
const R = 7;
const { stateKey } = load(path.join(ROOT, "A00_Common", "b_page_state.ts"));
const store = load(B06("B06_Section_Section_Store"));
const { buildCrossPatches } = load(B06("logic/B06_Section_UI_Page_Patches"));
const { createStationControls } = load(B06("B06_Section_UI_Page_Station_Controls"));
/** 새 탭 = 빈 세션 · 서버 상세(정본) 받아 캐시에 · 조정창 하나. */
async function freshTab(clearSession = true) {
if (clearSession) globalThis.sessionStorage.clear();
store.invalidateSectionDetail(P, R);
const detail = await store.loadSectionDetail(P, R);
const controls = createStationControls({
sessionKey: (kind) => stateKey(kind, P, R),
refreshCard: () => undefined,
detail: () => detail,
crossHalfWidth: () => undefined,
sampledHalfWidth: () => 20,
ensureSampledWidth: async () => true,
projectId: () => P,
});
controls.load();
return { detail, controls };
}
/** 횡단 보기 [저장] 본문 — 세션 고침(`buildCrossPatches`) → 바탕 위에 얹음(`cachedCrossPatches`). */
function savePatches(controls) {
const overlay = buildCrossPatches({
rockOffsets: new Map(),
cutSlopeRatios: new Map(),
stationWidths: controls.widths,
inletStructures: controls.inletStructures,
basinAdjustments: controls.basinAdjustments,
revetAdjusts: controls.revetAdjustsByChainage(),
extraCounts: controls.extraCountsByChainage(),
extraSpans: controls.extraSpansByChainage(),
fordAdjusts: new Map(),
boxAdjusts: new Map(),
linkFlags: controls.linkFlagsByChainage(),
});
return { overlay, patches: store.cachedCrossPatches(P, R, overlay) };
}
(async () => {
const at = job.at;
const out = {};
// H3 — 유출만 고침 → 유입 · 집수 쪽 단 수 남음
{
const { controls } = await freshTab();
controls.revetOffset.update(at, "outlet", { x: 3 });
out.h3 = savePatches(controls);
}
// M2 — 새 탭 ↺(유입) · 추가 벽 − · 빈 전체 폭 · 집수정 내공 2.1(M1)
{
const { controls } = await freshTab();
controls.revetOffset.reset(at, "inlet");
controls.extraWalls.setCount(at, 0, "outlet");
controls.applyGlobalWidth(undefined, [at]);
controls.inletStructure.updateAdjust(at, { innerWidthM: 2.1, innerHeightM: 0.5 });
out.m2 = savePatches(controls);
out.m2.countShown = controls.extraWalls.countFor({ chainage_m: at, design: {} }, "outlet");
// 저장 전 새로고침(세션은 남고 상세는 정본) — 되돌림이 살아 있어야 함
const again = await freshTab(false);
out.m2reload = savePatches(again.controls);
out.m2reload.cachedInlet = again.detail.cross_sections[0].design.revet_adjust?.inlet ?? null;
out.m2reload.cachedCounts = again.detail.cross_sections[0].design.extra_wall_counts ?? null;
}
// H2 — 정본 행이 없는 자리(옮긴 관)의 캐시 설계 = 지반 등 입력까지 실음
{
globalThis.sessionStorage.clear();
store.invalidateSectionDetail(P, R);
await store.loadSectionDetail(P, R); // 정본 = job.detail(at 한 측점)
const moved = await store.loadSectionDetail(P, R);
moved.cross_sections.push({ ...job.moved });
out.h2 = store.cachedCrossPatches(P, R, []);
moved.cross_sections.pop();
}
process.stdout.write(JSON.stringify(out));
})().catch((error) => {
process.stderr.write(String(error?.stack ?? error));
process.exit(1);
});