Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_3
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
@@ -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>"),
|
||||
}),
|
||||
);
|
||||
@@ -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 "<b>" in r["escaped"] and "a&<" 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
|
||||
Reference in New Issue
Block a user