Files
Aislo/resources/tester/helper_179_4_detail_table.cjs
T
eomsangdonandClaude Opus 5.5 10eb54f5e5 feat(B06): [상세] 그래프 고르기 — 교차점 분할 · 여러 고름(원지반 포함) · 닫힌 넓이 · 고른 것 값 · 서버 조각 검산 (209-1)
- 선(설계 · 구조물 · 원지반 · 사토장)을 만나는 점에서 조각으로 나눔 · 조각마다 판정 선
- 누름 = 하나 · Shift/Ctrl+누름 = 더하기/빼기 · [+ − ⤢] 왼쪽 값 두 줄(길이 합 · 면적 · 닫히지 않음)
- 칸 넣기 = 우클릭 고르기 길 그대로 · 길이 한 번 누름 = 바로 · 여럿 = Enter · [칸에 넣기]
- 저장 = 조각 두 끝 목록(pieces) · 서버가 설계선 · 원지반선 · 사토장선 위 검산 · 구조물 선은 두 끝 직선 · 옛 두 끝 꼴 그대로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
2026-10-10 15:21:50 +09:00

202 lines
7.2 KiB
JavaScript

/* PLAN 179-4 단위검증 헬퍼 — [상세] 표 손질(`B06_Section_UI_Cross_Detail_Table.ts`).
* 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림. 입력 = 표준입력 JSON {design, sources} ·
* 결과 = 표준출력 JSON 한 벌. 짝 = `test_179_4_detail_table.py`(파이썬 `cell_value` 거울).
*/
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 = path.join(ROOT, "B06_Section");
const common = fs.readFileSync(path.join(B06, "B06_Section_UI_Section_Common.ts"), "utf8");
const CROSS_PAD = Function(`return ${/export const CROSS_PAD = (\{[^}]*\})/.exec(common)[1]}`)();
// 세션 저장소 — 진짜 b_page_state 를 그대로 돌림
const store = () => {
const map = new Map();
return {
getItem: (key) => (map.has(key) ? map.get(key) : null),
setItem: (key, value) => map.set(key, String(value)),
removeItem: (key) => map.delete(key),
key: (index) => [...map.keys()][index] ?? null,
get length() {
return map.size;
},
};
};
globalThis.window = { sessionStorage: store(), localStorage: store() };
// 조정창 공용 부품(Select → Design_Pick → Cross_Panel_Base)이 불러올 때 문서에 귀를 닮
globalThis.document = { addEventListener: () => {}, removeEventListener: () => {} };
const STUBS = {
"@ui/ui_template_context_menu": { createMapContextMenu: () => ({}) },
"./B06_Section_UI_Section_Common": { CROSS_PAD },
};
const resolve = (base) => (fs.existsSync(`${base}.ts`) ? `${base}.ts` : base);
function loadTs(full) {
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
const localRequire = (request) => {
if (STUBS[request]) return STUBS[request];
if (request.endsWith(".css")) return {};
if (request.startsWith("@util/"))
return loadTs(resolve(path.join(ROOT, "common_util", request.slice(6))));
return loadTs(resolve(path.join(path.dirname(full), request)));
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
return box.exports;
}
const T = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail_Table.ts"));
const input = JSON.parse(fs.readFileSync(0, "utf8"));
const out = {};
// ① 칸 값 거울 — 같은 출처 · 설계를 파이썬 `cell_value` 와 견줌
out.local = input.sources.map((source) => T.localCellValue(source, input.design));
// ② 그래프 고르기 — 띠 → 표 키 · 판정 선 화면 px → m(카드 x · toDisplayY 의 역)
const fake = (classes, attrs = {}) => {
const self = {
classList: { contains: (name) => classes.includes(name) },
getAttribute: (name) => attrs[name] ?? null,
closest: (selector) =>
selector.split(",").some((part) => classes.includes(part.trim().slice(1))) ? self : null,
};
return self;
};
out.area = {
cut_soil: T.areaSourceOf(fake(["b06-chart__area", "b06-chart__area--cut_soil"])),
cut_rock: T.areaSourceOf(
fake(["b06-chart__area", "b06-chart__area--cut_rock", "b06-chart__area--cut_rr"]),
),
fill: T.areaSourceOf(fake(["b06-chart__area", "b06-chart__area--fill"])),
spoil: T.areaSourceOf(fake(["b06-chart__spoil-fill"])),
other: T.areaSourceOf(fake(["b06-chart__ground"])),
};
const AXES = {
widthPx: 600,
heightPx: 300,
pixelsPerMeter: 10,
maxOffset: 24,
displayMax: 120,
elevationMid: 110,
exaggeration: 2,
};
const px = ([offset, elevation]) => [
CROSS_PAD.left + (AXES.maxOffset - offset) * AXES.pixelsPerMeter,
CROSS_PAD.top +
(AXES.displayMax - (AXES.elevationMid + (elevation - AXES.elevationMid) * AXES.exaggeration)) *
AXES.pixelsPerMeter,
];
// 화면 px → m(209-1 `_Cross_Detail_Select.pxToM`) · 길이 = 옛 저장 꼴(두 끝) 그대로 읽음
const S = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail_Select.ts"));
out.pxBack = input.pick.map(px).map(S.pxToM(AXES));
out.length = { src: "length", start: input.pick[0], end: input.pick[1] };
out.lengthValue = T.localCellValue(out.length, input.design);
// ③ 우클릭 메뉴 항목
const noop = () => {};
const act = {
pick: noop,
auto: noop,
addRow: noop,
addColumn: noop,
removeRow: noop,
removeColumn: noop,
};
const labels = (items) => items.map((item) => (typeof item === "string" ? item : item[0]));
const empty = { cells: {}, rows: [], columns: [] };
out.menuHeader = labels(
T.cellMenuItems(empty, { key: "ground", rowId: null, columnId: null }, act),
);
out.menuBase = labels(
T.cellMenuItems(
{ ...empty, cells: { cut_soil: { src: "area", key: "embankment" } } },
{ key: "cut_soil", rowId: "b0", columnId: null },
act,
),
);
out.menuAdded = labels(
T.cellMenuItems(
{
cells: {},
rows: [{ id: "r1", after: "b0", grouped: true, labels: [] }],
columns: [{ id: "c1", label: "" }],
},
{ key: "c:c1:r1", rowId: "r1", columnId: "c1" },
act,
),
);
const full = {
cells: {},
rows: Array.from({ length: T.MAX_ROWS }, (_, i) => ({
id: `r${i + 1}`,
after: null,
grouped: false,
labels: [],
})),
columns: Array.from({ length: T.MAX_COLUMNS }, (_, i) => ({ id: `c${i + 1}`, label: "" })),
};
out.menuFull = labels(T.cellMenuItems(full, { key: "fence", rowId: "b0", columnId: null }, act));
let picked = null;
T.cellMenuItems(
empty,
{ key: "fence", rowId: "b0", columnId: null },
{
...act,
pick: (kind) => (picked = kind),
},
).find((item) => item[0] === "그래프에서 고르기 — 길이")[1]();
out.menuPicks = picked;
// ④ 표 손질 → 세션 초안 → patch(Persist 가 싣는 꼴)
const section = { station_id: "s40", chainage_m: 40, design: { ...input.design } };
const table = T.detailTableOf(section, "p1", 7);
out.fresh = JSON.parse(JSON.stringify(table));
T.setCell(table, "cut_soil", { src: "area", key: "embankment" });
T.setCell(table, "fence", out.length);
const r1 = T.addRow(table, "b0", true);
const r2 = T.addRow(table, null, false);
const r3 = T.addRow(table, r1, true);
const c1 = T.addColumn(table);
T.setRowLabel(table, r1, 0, "풍화암");
T.setColumnLabel(table, c1, "비고");
T.setCell(table, `c:${c1}:${r1}`, { src: "text", text: "L형" });
T.setCell(table, `r:${r2}:2`, { src: "area", key: "cut_rock" });
T.writeDetailTable(section, table, "p1", 7);
out.ids = [r1, r2, r3, c1];
out.cacheHas = section.design.detail_table === table;
out.drafts = Object.fromEntries(T.readDetailTables("p1", 7));
out.forTable = T.designForTable({ ...section, design: { paved: true } }, "p1", 7).detail_table;
// 지움 — r1 을 지우면 r3 은 r1 자리(b0 아래)로 · 그 행 열 칸도 지움
const removed = T.detailTableOf(section, "p1", 7);
T.removeRow(removed, r1);
T.removeColumn(removed, c1);
out.removed = removed;
out.autoBack = (() => {
const back = T.detailTableOf(section, "p1", 7);
T.setCell(back, "cut_soil", null);
return Object.keys(back.cells);
})();
out.span = [
T.groupSpan(
[
["깍기", "토사", "a"],
[null, "x", "r"],
[null, "암석", "b"],
["쌓기", null, "c"],
],
0,
),
T.groupSpan(
[
["쌓기", null, "c"],
["층따기", null, "d"],
],
0,
),
];
process.stdout.write(JSON.stringify(out));