- 선(설계 · 구조물 · 원지반 · 사토장)을 만나는 점에서 조각으로 나눔 · 조각마다 판정 선 - 누름 = 하나 · Shift/Ctrl+누름 = 더하기/빼기 · [+ − ⤢] 왼쪽 값 두 줄(길이 합 · 면적 · 닫히지 않음) - 칸 넣기 = 우클릭 고르기 길 그대로 · 길이 한 번 누름 = 바로 · 여럿 = Enter · [칸에 넣기] - 저장 = 조각 두 끝 목록(pieces) · 서버가 설계선 · 원지반선 · 사토장선 위 검산 · 구조물 선은 두 끝 직선 · 옛 두 끝 꼴 그대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
202 lines
7.2 KiB
JavaScript
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));
|