Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_3

This commit is contained in:
2026-10-06 23:09:59 +09:00
5 changed files with 548 additions and 0 deletions
@@ -0,0 +1,58 @@
/* 공용 표(`ui_template/ui_template_data_table.ts`) 견본 — 토적표 모양 · 머리 3줄 · 30열 · 합계.
* 그냥 글(스크립트)로 읽어 씀 — Node 헬퍼와 화면 eval 이 같은 견본을 씀.
* const s = sampleDataTable(66); // { columns, rows, total, frozen } */
function sampleDataTable(n) {
const num = (key, label, group, decimals) => ({ key, label, group, kind: "number", decimals });
const trio = (pre, group) => [
num(pre + "_area", "단면적", group, 2),
num(pre + "_vol", "입적", group, 1),
num(pre + "_adj", "보정량", group, 1),
];
const pair = (pre, group) => [
num(pre + "_len", "거리", group, 1),
num(pre + "_area", "면적", group, 1),
];
const columns = [
{ key: "sta", label: "측점", width: 80 },
num("dist", "거리", [], 0),
...trio("cs", ["절토", "토사"]),
...trio("cr", ["절토", "연암"]),
...trio("ds", ["절토", "측구파기 토사"]),
...trio("dr", ["절토", "측구파기 암"]),
num("adj_sum", "보정량계", ["절토"], 1),
num("fill_area", "단면적", ["성토"], 2),
num("fill_vol", "입적", ["성토"], 1),
num("reuse", "유용토", [], 1),
num("diff", "차인토량", [], 1),
num("cum", "누가토량", [], 1),
...pair("gf", ["면고르기", "성토면"]),
...pair("gc", ["면고르기", "절토면"]),
...pair("tf", ["지장목제거", "성토"]),
...pair("tc", ["지장목제거", "절토"]),
...pair("rd", ["노면정리"]),
];
const rows = [];
let cum = 0;
for (let i = 0; i < n; i++) {
const r = {
sta: i % 5 ? `${120 + Math.floor(i / 5)}+${(i % 5) * 4}` : String(120 + i / 5),
dist: i ? 4 : 0,
};
columns.slice(2).forEach((c, j) => {
// 빈 값 · 큰 값 · 음수가 섞이게
r[c.key] =
(i + j) % 7 === 0
? null
: Math.round(((i * 37 + j * 101) % 997) * (j % 4 === 0 ? 13.7 : 1.13) * 100) / 100;
});
r.diff = Math.round((r.fill_vol ?? 0) - (r.cs_vol ?? 0)) / 1;
cum += r.diff;
r.cum = cum;
rows.push(r);
}
const total = { sta: "합계" };
columns.slice(1).forEach((c) => {
if (c.key !== "cum") total[c.key] = rows.reduce((s, r) => s + (r[c.key] ?? 0), 0);
});
return { columns, rows, total, frozen: 1 };
}
+79
View File
@@ -0,0 +1,79 @@
/* 공용 표(`ui_template/ui_template_data_table.ts`) 단위검증 헬퍼 —
* 트랜스파일해 Node 로 견본 표 HTML 을 만들고 판정할 값을 JSON 으로 냄. */
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const file = path.join(__dirname, "..", "..", "ui_template", "ui_template_data_table.ts");
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
const req = (p) => (p.endsWith(".css") ? {} : require(p));
new Function("exports", "module", "require", js)(box.exports, box, req);
const m = box.exports;
const src = fs.readFileSync(path.join(__dirname, "fixture_data_table_sample.js"), "utf8");
const sampleDataTable = new Function(src + "; return sampleDataTable;")();
const s = sampleDataTable(66);
const html = m.renderDataTableHtml(s);
const head = html.split("<thead>")[1].split("</thead>")[0];
const headRows = head.split("</tr>").filter(Boolean);
const body = html.split("<tbody>")[1].split("</tbody>")[0];
const bodyRows = body.split("</tr>").filter(Boolean);
const foot = html.split("<tfoot>")[1].split("</tfoot>")[0];
const t0 = process.hrtime.bigint();
const big = m.renderDataTableHtml(sampleDataTable(1000));
const ms = Number(process.hrtime.bigint() - t0) / 1e6;
const one = (c, v) =>
m.formatCell(
v,
c.kind === "number"
? new Intl.NumberFormat("ko-KR", {
minimumFractionDigits: c.decimals ?? 0,
maximumFractionDigits: c.decimals ?? 0,
})
: null,
)[0];
const nc = { key: "a", label: "a", kind: "number", decimals: 2 };
const tc = { key: "b", label: "b" };
console.log(
JSON.stringify({
cols: s.columns.length,
headRows: headRows.length,
firstHeadRow: headRows[0],
secondHeadRow: headRows[1],
bodyRows: bodyRows.length,
firstBodyRow: bodyRows[0],
foot,
frozenCells: (body.match(/class="is-frozen is-edge"/g) || []).length,
bigRows: big.split("<tbody>")[1].split("</tbody>")[0].split("</tr>").length - 1,
bigMs: ms,
fmt: {
thousands: one(nc, 1234567.891),
neg: one(nc, -97.9),
zero: one(nc, 0),
nul: one(nc, null),
undef: one(nc, undefined),
blank: one(nc, ""),
nan: one(nc, NaN),
text: one(tc, "120+18"),
textNum: one(tc, 5),
},
win: (() => {
const b = m
.renderDataTableHtml(sampleDataTable(1000), { from: 61, to: 121, rowH: 25 })
.split("<tbody>")[1]
.split("</tbody>")[0]
.split("</tr>")
.filter(Boolean);
return { n: b.length, top: b[0], second: b[1].slice(0, 20), bottom: b[b.length - 1] };
})(),
escaped: m.renderDataTableHtml({ columns: [{ key: "x", label: "<b>" }], rows: [{ x: "a&<" }] }),
noTotal: m.renderDataTableHtml({ columns: [tc], rows: [] }).includes("<tfoot>"),
}),
);
+87
View File
@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
"""공용 표 양식(PLAN 70-1) 회귀검증 — 묶음 머리 · 고정 열 · 숫자 꼴 · 빈 값 · 합계 · 1,000줄.
Node 헬퍼(`helper_ui_data_table.cjs`)가 `ui_template/ui_template_data_table.ts` 를 트랜스파일해
견본(`fixture_data_table_sample.js` · 머리 3줄 · 30열 · 66줄)으로 HTML 을 만들고, 여기서 판정함.
"""
import json
import os
import subprocess
import pytest
HERE = os.path.dirname(os.path.abspath(__file__))
@pytest.fixture(scope="module")
def r():
proc = subprocess.run(
["node", os.path.join(HERE, "helper_ui_data_table.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_견본_크기(r):
assert r["cols"] == 30
assert r["headRows"] == 3
assert r["bodyRows"] == 66
def test_묶음_머리_합침(r):
first = r["firstHeadRow"]
# 측점 · 거리 · 유용토 등 묶음 없는 열은 머리 3줄을 세로로 차지
assert 'rowspan="3" class="is-frozen is-edge" style="left:0px" title="측점">측점</th>' in first
assert first.count('rowspan="3"') == 5 # 측점 거리 유용토 차인토량 누가토량
assert 'colspan="13">절토</th>' in first # 토사 · 연암 · 측구파기 토사 · 암 각 3 + 보정량계
assert 'colspan="4">면고르기</th>' in first
assert 'colspan="2">노면정리</th>' in first
second = r["secondHeadRow"]
assert 'colspan="3">토사</th>' in second and 'colspan="3">연암</th>' in second
assert 'rowspan="2" data-num="" title="보정량계"' in second # 한 단 묶음은 머리 2줄
def test_고정_열과_숫자_칸(r):
row = r["firstBodyRow"]
assert row.startswith('<tr><td class="is-frozen is-edge" style="left:0px">120</td>')
assert r["frozenCells"] == 66
assert '<td data-num="">0</td>' in row # 거리 0 은 빈 값 아님
def test_숫자_꼴(r):
f = r["fmt"]
assert f["thousands"] == "1,234,567.89"
assert f["neg"] == "-97.90"
assert f["zero"] == "0.00"
assert f["nul"] == f["undef"] == f["blank"] == f["nan"] == "—"
assert f["text"] == "120+18" and f["textNum"] == "5"
def test_합계_줄(r):
assert r["foot"].startswith('<tr><td class="is-frozen is-edge" style="left:0px">합계</td>')
assert "is-empty" in r["foot"] # 누가토량 합계는 비움
assert r["noTotal"] is False
def test_글_새지_않음(r):
assert "&lt;b&gt;" in r["escaped"] and "a&amp;&lt;" in r["escaped"]
def test_천_줄_빠름(r):
assert r["bigRows"] == 1000
assert r["bigMs"] < 300
def test_보이는_줄만_그림(r):
w = r["win"]
assert w["n"] == 62 # 위 빈 줄 + 60줄 + 아래 빈 줄
assert 'style="height:1525px"><td colspan="30">' in w["top"] # 61줄 × 25
assert w["second"].startswith(
'<tr class="is-alt">'
) # 61 번째(홀수) 줄 — 번갈아 색은 전체 줄 번호로
assert 'style="height:21975px"' in w["bottom"] # (1000 - 121) × 25