Merge branch 'sub_laptop_4' of C:/Aislo_wt/sub_laptop_4 into main_laptop_1
This commit is contained in:
@@ -79,3 +79,19 @@ html:not([data-size="compact"], [data-size="medium"]) .m01-master__panel .m01-lo
|
||||
.m01lab-src__find .ui-find__list {
|
||||
max-height: 40vh;
|
||||
}
|
||||
|
||||
/* 표 + 표 아래 비고 · [주] 글(833-1) — 글은 패널 폭 그대로 접힘 */
|
||||
.m01lab-src__tblwrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 서술 칸(한 줄 30자 이상) · 한 칸짜리 표 — 접고, 너무 좁게 찌그러지지 않게 */
|
||||
.m01lab-src .m01lab-src__table td.m01lab-src__wrapcell {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
min-width: 14em;
|
||||
max-width: 28em;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { splitTableNotes, wrapCell } from "./logic/M01_MasterData_logic_source_notes";
|
||||
import "./M01_MasterData_UI_LogicLab_Side.css";
|
||||
|
||||
const TEXT = {
|
||||
@@ -48,22 +49,45 @@ function blockNode(b: Block): HTMLElement {
|
||||
}
|
||||
|
||||
function rawNode(b: Block): HTMLElement {
|
||||
if (b.table)
|
||||
return el("div", {
|
||||
if (b.table) {
|
||||
// 표 맨 아래 비고 · [주] · ※ 줄 = 표 밖 글로(833-1) — 긴 글이 열 폭을 밀지 않게
|
||||
const { rows, notes } = splitTableNotes(b.table);
|
||||
const grid = el("div", {
|
||||
className: "m01-logic__scroll",
|
||||
children: [
|
||||
el("table", {
|
||||
className: "ui-table ui-table--data m01-logic__grid m01lab-src__table",
|
||||
children: [
|
||||
el("tbody", {
|
||||
children: b.table.map((r) =>
|
||||
el("tr", { children: r.map((c) => el("td", { text: c })) }),
|
||||
children: rows.map((r) =>
|
||||
el("tr", {
|
||||
children: r.map((c) =>
|
||||
el("td", {
|
||||
text: c,
|
||||
className: wrapCell(c, r.length) ? "m01lab-src__wrapcell" : "",
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
if (!notes.length) return grid;
|
||||
return el("div", {
|
||||
className: "m01lab-src__tblwrap",
|
||||
children: [
|
||||
grid,
|
||||
...notes.map((n) =>
|
||||
el("p", {
|
||||
className: "m01lab-src__p m01lab-src__tnote",
|
||||
children: [...(n.label ? [el("strong", { text: n.label }), " "] : []), n.text],
|
||||
}),
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
if (b.h) return el("h4", { className: "m01lab-src__head", text: b.text ?? "" });
|
||||
return el("p", { className: "m01lab-src__p", text: b.p ?? "" });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_logic_source_notes.ts
|
||||
* 원문 표 맨 아래 서술 줄(비고 · [주] · ※ …) 가르기 — 순수 계산(화면 · fetch 없음 · PLAN 833-1)
|
||||
* 표 끝에 이어 붙은 서술 줄 = 표 밖 글로 · 글 순서 · 내용은 그대로 · 위쪽 줄(표 몸 · 묶음 머리)은 안 건드림
|
||||
* ========================================================================== */
|
||||
|
||||
/** 표 밖으로 나가는 서술 줄 하나 — `label` = 「비고」 · 「[주]」 머리(없으면 빈 글) */
|
||||
export interface NoteLine {
|
||||
label: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const LABEL = /^(비고|\[?주\]?\)?|※|참고)\s*\d*$/;
|
||||
const MARK = /^(※|-|∙|·|\[주\]|주\)|비고|참고)/;
|
||||
const LONG = 30;
|
||||
|
||||
/** 한 줄이 서술 줄이면 글 — 아니면 null (머리 줄 · 숫자 줄 · 묶음 머리(짧은 한 칸)는 아님) */
|
||||
export function noteLine(row: string[]): NoteLine | null {
|
||||
if (row.length < 2) return null;
|
||||
const full = row.map((c, k) => [k, c.trim()] as const).filter(([, c]) => c);
|
||||
if (full.length === 1) {
|
||||
const [[, text]] = full;
|
||||
return MARK.test(text) || text.length >= LONG ? { label: "", text } : null;
|
||||
}
|
||||
if (full.length === 2 && full[0][0] === 0 && LABEL.test(full[0][1]))
|
||||
return { label: full[0][1], text: full[1][1] };
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 표 → 몸 줄 + 맨 아래 이어 붙은 서술 줄 — 머리(첫 줄)는 늘 몸 */
|
||||
export function splitTableNotes(table: string[][]): { rows: string[][]; notes: NoteLine[] } {
|
||||
let end = table.length;
|
||||
while (end > 1 && noteLine(table[end - 1])) end--;
|
||||
return {
|
||||
rows: table.slice(0, end),
|
||||
notes: table.slice(end).map((r) => noteLine(r) as NoteLine),
|
||||
};
|
||||
}
|
||||
|
||||
/** 칸 글이 줄바꿈 대상 — 한 줄이 30자 이상(서술 칸) · 한 칸짜리 표(글 상자) · 숫자 칸은 안 접음 */
|
||||
export function wrapCell(text: string, cols: number): boolean {
|
||||
return cols === 1 || text.split("\n").some((l) => l.length >= LONG);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
"""833-1 지킴 — 원문 표 맨 아래 서술 줄(비고 · [주] · ※) 가르기 · 서술 칸 줄바꿈 판정.
|
||||
|
||||
순수 TS(`M01_MasterData/logic/M01_MasterData_logic_source_notes.ts`)를 node 로 돌림 · 서버 · 화면 없음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
TS = (ROOT / "M01_MasterData/logic/M01_MasterData_logic_source_notes.ts").as_uri()
|
||||
|
||||
|
||||
def _node(body: str, data: object = None) -> object:
|
||||
src = f'import {{ noteLine, splitTableNotes, wrapCell }} from "{TS}";\n' + body
|
||||
done = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--input-type=module", "-e", src],
|
||||
input=json.dumps(data, ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
cwd=ROOT,
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
return json.loads(done.stdout)
|
||||
|
||||
|
||||
def test_보기_서술_줄_가르기() -> None:
|
||||
got = _node(
|
||||
r"""
|
||||
const t = [
|
||||
["구분", "단위", "수량"],
|
||||
["보통인부", "인", "1"],
|
||||
["비고", "- 비계주위 보호망을 시공하지 않는 경우 본 시공량의 8%를 증하여 적용한다.", ""],
|
||||
["※ 위 표에서 ‘노’는 ‘노지묘’이다.", "", ""],
|
||||
];
|
||||
console.log(JSON.stringify([splitTableNotes(t), noteLine(["공종", "Belt폭", ""]),
|
||||
noteLine(["뒷채움 자재비 및 운반비 별산", "", ""]), noteLine(["1", "2", "3"]),
|
||||
noteLine(["단독"]), splitTableNotes([["비고", "긴 글이 머리 줄에 있어도 머리는 몸"]]).notes.length,
|
||||
[wrapCell("7", 3), wrapCell("가".repeat(30), 3), wrapCell("글", 1), wrapCell("짧음\n" + "나".repeat(31), 2)]]));
|
||||
"""
|
||||
)
|
||||
split = got[0]
|
||||
assert split["rows"] == [["구분", "단위", "수량"], ["보통인부", "인", "1"]]
|
||||
assert [n["label"] for n in split["notes"]] == ["비고", ""]
|
||||
assert split["notes"][1]["text"].startswith("※ 위 표에서")
|
||||
assert got[1:6] == [
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
] # 짧은 묶음 머리 · 숫자 줄 · 한 칸 · 머리 줄은 서술 아님
|
||||
assert got[6] == [False, True, True, True]
|
||||
|
||||
|
||||
def test_원문_전수_글_그대로_표_밖으로() -> None:
|
||||
"""품셈 본문 표 전부 — 줄 수 · 글 합계가 가르기 앞뒤 같음 · 머리 줄은 몸에 남음 · 표 밖 글 표는 200 넘음."""
|
||||
from M01_MasterData import M01_MasterData_Store as _s # noqa: F401
|
||||
from M01_MasterData import M01_MasterData_Store_Make as mk
|
||||
|
||||
tables = []
|
||||
for book in mk.BOOKS:
|
||||
for _d, _n, _t, folder in mk._chapters(book):
|
||||
for md in sorted(folder.glob("*.md")):
|
||||
for b in mk._blocks(md.read_text(encoding="utf-8").split("\n")):
|
||||
if "table" in b:
|
||||
tables.append(b["table"])
|
||||
got = _node(
|
||||
r"""
|
||||
const tables = JSON.parse(await new Promise((ok) => { let s = ""; process.stdin.on("data", (d) => (s += d)); process.stdin.on("end", () => ok(s)); }));
|
||||
let lost = 0, moved = 0, headBad = 0;
|
||||
for (const t of tables) {
|
||||
const { rows, notes } = splitTableNotes(t);
|
||||
const had = t.flat().join("").replace(/\s/g, "");
|
||||
const now = [...rows.flat(), ...notes.flatMap((n) => [n.label, n.text])].join("").replace(/\s/g, "");
|
||||
if (had !== now || rows.length + notes.length !== t.length) lost++;
|
||||
if (rows.length < 1) headBad++;
|
||||
if (notes.length) moved++;
|
||||
}
|
||||
console.log(JSON.stringify({ n: tables.length, lost, moved, headBad }));
|
||||
""",
|
||||
tables,
|
||||
)
|
||||
assert got["n"] > 2500 and got["lost"] == 0 and got["headBad"] == 0
|
||||
assert got["moved"] > 200
|
||||
Reference in New Issue
Block a user