Merge branch 'main_laptop_1' of C:/Program_coding/임도설계 및 견적자동화 프로그램 개발 into sub_laptop_3
This commit is contained in:
@@ -30,6 +30,7 @@ import {
|
||||
newVar,
|
||||
setRow,
|
||||
shapeOf,
|
||||
sourceOf,
|
||||
unfold,
|
||||
type StackItem,
|
||||
type StackLine,
|
||||
@@ -254,7 +255,9 @@ export function buildBlock(
|
||||
const was = h.before?.get(id);
|
||||
const freed = !iffy && raw !== undefined && !!was && hasIf(tryParse(was) ?? ["num", 0, "0"]);
|
||||
const sub = link?.갈래 === "단가산출" && link.키 ? h.subDoc(link.키) : null;
|
||||
const tag = expr ? null : sourceTag(h, id, link);
|
||||
const src = v ? sourceOf(h.doc, v) : null; // 가져온 값(812-3) = 옵션 가로 칸과 같은 값 · 꼬리표만
|
||||
const srcTag = src === "원파일" ? lt("Src_Native") : src === "엑셀" ? lt("Src_Excel") : src;
|
||||
const tag = expr ? null : srcTag ? clipTag(srcTag) : sourceTag(h, id, link);
|
||||
const tools: HTMLElement[] = [];
|
||||
if (sub?.결과 && link?.키)
|
||||
tools.push(
|
||||
|
||||
@@ -51,7 +51,7 @@ export function buildSourced(
|
||||
const n = Number(f.input.value.trim().replace(/,/g, ""));
|
||||
if (f.input.value.trim() === "" || !Number.isFinite(n))
|
||||
return void (f.input.value = String(v.연결?.값 ?? "")); // 수만 — 빈 값 · 글 = 그대로
|
||||
set(id, v, String(n));
|
||||
set(id, v, n); // 수 그대로(글 「0.7」 = 셈 「—」 · 공식 묶음 값 칸 `setRow` 와 같은 꼴 · 812-3)
|
||||
});
|
||||
f.root.dataset.option = id;
|
||||
const tagText = tag === "원파일" ? lt("Src_Native") : tag === "엑셀" ? lt("Src_Excel") : tag;
|
||||
|
||||
@@ -161,7 +161,10 @@ def machines(doc: dict, title: dict[str, str]) -> None:
|
||||
|
||||
def shape(doc: dict, meta: dict) -> dict:
|
||||
"""derive 문서 다듬기 — 원본 글줄 · 엔진 줄 자원 이름."""
|
||||
from M02_MasterTemplete import M02_Doc_NativeLayout as lay
|
||||
|
||||
src = meta.get("원본") or {}
|
||||
lay.tag(doc, meta) # 812-3 원파일 차림 — 금액 줄 ↔ 로직 줄(끝에 `apply`)
|
||||
names(doc, list(src.get("글줄") or []))
|
||||
title = {}
|
||||
for item in (meta.get("로직") or {}).get("호표") or []:
|
||||
@@ -169,4 +172,5 @@ def shape(doc: dict, meta: dict) -> dict:
|
||||
if res.get("갈래") == "기계" and res.get("이름"):
|
||||
title[str(item.get("이름"))] = " ".join(x for x in (res["이름"], res.get("규격")) if x)
|
||||
machines(doc, title)
|
||||
lay.apply(doc, meta)
|
||||
return doc
|
||||
|
||||
@@ -19,6 +19,7 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_NativeLayout as lay
|
||||
from M02_MasterTemplete import M02_San_Emit as em
|
||||
from M02_MasterTemplete import M02_San_Est as est
|
||||
from M02_MasterTemplete import M02_San_Expr as ex
|
||||
@@ -76,6 +77,8 @@ class Block:
|
||||
self.groups: dict[str, list[tuple[str, float]]] = {} # 구간 글자 → (부름 글, 결과)
|
||||
self.extra: dict[int, dict] = {} # 행 → 소계가 정한 비목(아래 줄)
|
||||
self.got = {"확정": 0, "표지": 0, "추정": 0}
|
||||
self.memo: list[int] = [] # 글 줄 행(812-3 차림 · 분수 그림 위아래 줄 뺌)
|
||||
self.frac: set[int] = set()
|
||||
|
||||
# ── 작은 도구 ──────────────────────────────────────────────────
|
||||
def mark(self, ln: dict, why: str) -> None:
|
||||
@@ -182,6 +185,7 @@ class Block:
|
||||
return re.sub(r"\s*([×÷·+*/−-])\s*", r"\1", t).strip().split()
|
||||
|
||||
tops, bottoms, at = cols(i - 1), cols(i + 1), [0]
|
||||
self.frac |= {self.lines[k]["행"] for k in (i - 1, i + 1) if 0 <= k < len(self.lines)}
|
||||
|
||||
def one(_: re.Match) -> str:
|
||||
k = at[0]
|
||||
@@ -485,6 +489,7 @@ class Block:
|
||||
|
||||
def text_line(self, ln: dict) -> None:
|
||||
"""글 줄 — 품셈 · 조건 이름만 있는 글(값 없음) = 「추정」 표지(연결 안 함 · 810-3 화면)."""
|
||||
self.memo.append(ln["행"])
|
||||
if _COND.search(ln["보임"]):
|
||||
self.o.glyph(
|
||||
ln["행"], ln["보임"], mark=f"{NOTE}{GUESS} — 품셈 조건 글(값 없음 · 연결 안 함)"
|
||||
@@ -493,6 +498,22 @@ class Block:
|
||||
return
|
||||
self.o.glyph(ln["행"], ln["보임"])
|
||||
|
||||
def tags(self) -> None:
|
||||
"""812-3 차림 표지(`M02_Doc_NativeLayout`) — 글줄 `차림` = 머리(단 > 0 ·
|
||||
소계 구간 첫 글 · 번호 글) · 합계 · 글(근거 · 메모)."""
|
||||
self.o.head[lay.TAG] = True
|
||||
for ln in self.lines:
|
||||
one = self.o.at.get(ln["행"])
|
||||
if one is None or ln["행"] in self.frac or "자리" in one:
|
||||
continue
|
||||
head = (ln.get("단") or 0) > 0 or ln.get("색인") == -20
|
||||
if ln.get("합계"):
|
||||
one[lay.TAG] = "합계"
|
||||
elif head or (ln["행"] in self.memo and lay.NUMBERED.match(one["글"])):
|
||||
one[lay.TAG] = "머리"
|
||||
elif ln["행"] in self.memo:
|
||||
one[lay.TAG] = "글"
|
||||
|
||||
# ── 한 벌 ─────────────────────────────────────────────────────
|
||||
def run(self) -> dict:
|
||||
for k, ln in enumerate(self.lines): # 「노무비 : ‹단가›」(식 · 결과 없음) = 단가 × 1
|
||||
@@ -544,6 +565,7 @@ class Block:
|
||||
elif (ln.get("보임") or "").strip():
|
||||
self.text_line(ln)
|
||||
i += 1
|
||||
self.tags()
|
||||
saved = self.blk.get("저장") or {}
|
||||
check = {COMP[c]: saved.get(c) or 0 for c in COMP} | {"계": saved.get("T") or 0}
|
||||
out = self.o.done("비목버림", None, check, "산근", self.file)
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
"""가져온 단가산출 줄 쌓기 차림(PLAN 812-3 · 요구 713 · 669~677) — 경쟁사 산출근거 꼴.
|
||||
|
||||
금액 · 셈 그대로. `M02_Doc_Import_Shape.shape` 가 부름(`tag` = 다듬기 앞 · `apply` = 끝) ·
|
||||
원본 `차림` 표지 있는 블록만(EST = `M02_Doc_NativeEst` · STmate = `M02_San_Emit.stmate`):
|
||||
항목 = 머리 줄(글줄 `차림` 「머리」 · 「1. …」 「가. …」 글) · 번호 글은 떼고 화면이 매김 ·
|
||||
가 · 나 = `가나다` · STmate 는 계 줄(`단계:`)마다 끊고 그 뒤 첫 글 줄 = 제목 ·
|
||||
머리 앞 줄 = 제목 없는 항목 · 합계 줄(「합계」 · 「소계」 절)만 있는 머리 = 뺌
|
||||
(화면 소계 · 합계가 냄)
|
||||
금액 줄 = 그 자리(원본 행 차례) — EST 는 셈이 같을 때만 금액 차례를 원본 차례로
|
||||
(단계 · 누계 이름 없고 기호 부름이 앞 줄일 때)
|
||||
공식 묶음 = 변수 대입 줄(`mid:` · 쓰는 차례) · 글 줄 = 근거 · 메모(글줄 `차림` 「글」)
|
||||
저장 = 금액 줄 `묶음` = 항목 제목 + 문서 `쌓기`(기본 차림과 다를 때만).
|
||||
파이썬 · TS 짝 — `common_util/common_util_line_stack.ts` `stackOf` · `saveOf` ·
|
||||
`hiddenOf`(안 놓인 기호 자리 · 기본 차림)의 거울 · 거울 시험
|
||||
`resources/tester/test_812_3_native_layout.py`(TS 왕복). 고칠 때 둘을 같이 고침.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_SCRIPTS = Path(__file__).resolve().parents[1] / "resources" / "master_data" / "scripts"
|
||||
if str(_SCRIPTS) not in sys.path:
|
||||
sys.path.insert(0, str(_SCRIPTS))
|
||||
|
||||
import master_doc as md # noqa: E402
|
||||
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd # noqa: E402
|
||||
|
||||
TAG = "차림" # 글줄 칸 — 머리 | 합계 | 글 · 원본 칸 = 차림 낼 블록
|
||||
KEEP = "_로직줄" # 문서 금액 줄 ↔ 로직 줄 차례(다듬기 사이 임시)
|
||||
GANADA = "가나다라마바사아자차카타파하"
|
||||
NUMBERED = re.compile(rf"^\s*'?\s*(\d{{1,2}}|[{GANADA}])\s*[.)]\s*(?!\d)(\S.*)$")
|
||||
_TOTAL = {"합계", "소계", "계"}
|
||||
|
||||
|
||||
def tag(doc: dict, meta: dict) -> None:
|
||||
"""다듬기(이름 · 기계 합침) 앞 — 금액 줄마다 로직 줄 차례(호표 → 덧줄)를 적어 둠."""
|
||||
row = meta.get("로직") or {}
|
||||
n = len(row.get("호표") or []) + len(row.get("덧줄") or [])
|
||||
money = doc.get("금액") or []
|
||||
if (meta.get("원본") or {}).get(TAG) and money and len(money) == n:
|
||||
for k, line in enumerate(money):
|
||||
line[KEEP] = k
|
||||
|
||||
|
||||
def title_of(text: str) -> tuple[str, bool]:
|
||||
"""「 1. 기 계(100%)」 → (「기 계(100%)」, 거짓) · 「'가. 굴삭기'」 → (「굴삭기」, 가나다)."""
|
||||
flat = " ".join(str(text or "").replace("'", " ").split())
|
||||
m = NUMBERED.match(flat)
|
||||
if not m:
|
||||
return flat, False
|
||||
return m.group(2).strip(), m.group(1) in GANADA
|
||||
|
||||
|
||||
# ── 차림 ──────────────────────────────────────────────────────────────
|
||||
def apply(doc: dict, meta: dict) -> None:
|
||||
money = doc.get("금액") or []
|
||||
keys = [line.pop(KEEP, None) for line in money]
|
||||
src = meta.get("원본") or {}
|
||||
if not src.get(TAG) or not money or None in keys:
|
||||
return
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
|
||||
row = meta.get("로직") or {}
|
||||
ids = sd.line_ids(
|
||||
imp.row_of({imp.KEY: {"로직": row}}) or {}
|
||||
) # 글줄 자리 = 줄 정체(`imp._named`)
|
||||
lines = [*(row.get("호표") or []), *(row.get("덧줄") or [])]
|
||||
by: dict[str, int] = {}
|
||||
for k, (cid, one) in enumerate(zip(ids, lines)):
|
||||
by.setdefault(cid, k)
|
||||
by.setdefault(str(one.get("이름")), k)
|
||||
inv = {k: j for j, k in enumerate(keys)}
|
||||
texts = sorted(
|
||||
(t for t in src.get("글줄") or [] if isinstance(t.get("행"), int)), key=lambda t: t["행"]
|
||||
)
|
||||
at: dict[int, int] = {} # 문서 금액 줄 → 원본 행
|
||||
for t in texts:
|
||||
for spot in t.get("자리들") or ([t["자리"]] if t.get("자리") else []):
|
||||
j = inv.get(by.get(str(spot), -1))
|
||||
if j is not None:
|
||||
at[j] = min(at.get(j, t["행"]), t["행"])
|
||||
rows = _fill([at.get(j) for j in range(len(money))])
|
||||
order = sorted(range(len(money)), key=lambda j: (rows[j], j))
|
||||
if order != list(range(len(money))) and _safe(doc, order):
|
||||
doc["금액"] = money = [money[j] for j in order]
|
||||
rows = [rows[j] for j in order]
|
||||
else:
|
||||
for j in range(1, len(rows)): # 차례 그대로 — 앞 줄보다 위면 앞 줄 자리로
|
||||
rows[j] = max(rows[j], rows[j - 1])
|
||||
items = _items(doc, texts, rows, src.get("갈래") == "STmate")
|
||||
for it in items:
|
||||
for kind, j in it["줄"]:
|
||||
if kind == "금액":
|
||||
money[j].pop("묶음", None)
|
||||
money[j] |= {"묶음": it["제목"]} if it["제목"] else {}
|
||||
raw = [_save_item(it, money) for it in items]
|
||||
doc.pop("쌓기", None)
|
||||
plain = save_of(stack_of(doc), money)
|
||||
got = save_of(stack_of({**doc, "쌓기": raw}), money)
|
||||
if got != plain:
|
||||
doc["쌓기"] = got
|
||||
|
||||
|
||||
def _fill(rows: list[int | None]) -> list[int]:
|
||||
"""원본 행 없는 줄(글줄에 안 붙은 소계 덧줄 등) = 다음 줄 행 · 없으면 앞 줄 행."""
|
||||
out = list(rows)
|
||||
for j in range(len(out) - 1, -1, -1):
|
||||
if out[j] is None and j + 1 < len(out):
|
||||
out[j] = out[j + 1]
|
||||
for j in range(len(out)):
|
||||
if out[j] is None:
|
||||
out[j] = out[j - 1] if j else 0
|
||||
return [int(x or 0) for x in out]
|
||||
|
||||
|
||||
def _safe(doc: dict, order: list[int]) -> bool:
|
||||
"""금액 차례를 바꿔도 셈이 같음 — 단계 없음 · 누계 · 번지 · 단계 이름 안 씀 ·
|
||||
줄 기호 부름 = 앞 줄."""
|
||||
if doc.get("단계"):
|
||||
return False
|
||||
money = doc.get("금액") or []
|
||||
pos = {j: p for p, j in enumerate(order)}
|
||||
sym = {str(x["기호"]): pos[j] for j, x in enumerate(money) if x.get("기호")}
|
||||
for j, line in enumerate(money):
|
||||
for text in (line.get("수량"), line.get("단가")):
|
||||
if text is None or str(text).strip() == "":
|
||||
continue
|
||||
try:
|
||||
got = md.names(md.parse(text))
|
||||
except md.DocError:
|
||||
return False
|
||||
if any(n in md.MONEY_NAMES or n == "계" for n in got):
|
||||
return False
|
||||
if any(n in sym and sym[n] >= pos[j] for n in got):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _items(doc: dict, texts: list[dict], rows: list[int], stm: bool) -> list[dict]:
|
||||
"""원본 행 차례로 항목 · 줄을 쌓음 — 줄 = (금액, 문서 줄) | (공식, [기호]) | (글, 글)."""
|
||||
have = set(symbols(doc))
|
||||
hidden = hidden_of(doc)
|
||||
ev: list[tuple[int, int, int, str, Any]] = []
|
||||
for t in texts:
|
||||
mark, text = t.get(TAG), str(t.get("글") or "")
|
||||
spots = t.get("자리들") or ([t["자리"]] if t.get("자리") else [])
|
||||
mids = [s[4:] for s in spots if str(s).startswith("mid:")]
|
||||
if any(str(s).startswith("단계:") for s in spots):
|
||||
ev.append((t["행"], 0, 0, "끝", None))
|
||||
elif mark == "합계":
|
||||
ev.append((t["행"], 0, 0, "합계", None))
|
||||
elif not spots and (mark == "머리" or (mark == "글" and NUMBERED.match(text))):
|
||||
ev.append((t["행"], 0, 0, "머리", title_of(text)))
|
||||
elif not spots and mark == "글" and text.strip():
|
||||
ev.append((t["행"], 1, 0, "글", " ".join(text.replace("'", " ").split())))
|
||||
ids = [m for m in mids if m in have and m not in hidden]
|
||||
if ids:
|
||||
ev.append((t["행"], 1, 0, "공식", ids))
|
||||
ev += [(r, 2, j, "금액", j) for j, r in enumerate(rows)]
|
||||
ev.sort(key=lambda e: e[:3])
|
||||
items: list[dict] = []
|
||||
cur: dict | None = None
|
||||
cut = False # STmate 계 줄 뒤 — 다음 첫 글 줄 = 제목
|
||||
|
||||
def new(title: str = "", sub: bool = False) -> dict:
|
||||
it = {"제목": title, "가나다": sub, "줄": [], "합계": False}
|
||||
items.append(it)
|
||||
return it
|
||||
|
||||
for _, _, _, kind, val in ev:
|
||||
if kind == "끝":
|
||||
cur, cut = None, stm
|
||||
elif kind == "합계":
|
||||
if cur is not None:
|
||||
cur["합계"] = True
|
||||
elif kind == "머리" or (kind == "글" and cut and cur is None):
|
||||
title, sub = val if kind == "머리" else title_of(val)
|
||||
if cur is not None and not cur["줄"] and not cur["제목"]:
|
||||
cur["제목"], cur["가나다"] = title, sub
|
||||
else:
|
||||
cur = new(title, sub)
|
||||
cut = False
|
||||
else:
|
||||
cur = cur if cur is not None else new()
|
||||
if kind == "공식" and cur["줄"] and cur["줄"][-1][0] == "공식":
|
||||
cur["줄"][-1][1].extend(x for x in val if x not in cur["줄"][-1][1])
|
||||
else:
|
||||
cur["줄"].append((kind, list(val) if kind == "공식" else val))
|
||||
keep = []
|
||||
for it in items:
|
||||
flat = re.sub(r"\s+", "", it["제목"])
|
||||
if not it["줄"] and (it["합계"] or not flat or flat in _TOTAL):
|
||||
continue
|
||||
keep.append(it)
|
||||
return keep
|
||||
|
||||
|
||||
def _save_item(it: dict, money: list[dict]) -> dict:
|
||||
out: dict[str, Any] = {}
|
||||
if it["제목"]:
|
||||
out["제목"] = it["제목"]
|
||||
if it["가나다"]:
|
||||
out["가나다"] = True
|
||||
lines = []
|
||||
for kind, val in it["줄"]:
|
||||
if kind == "금액":
|
||||
lines.append({"금액": val, "이름": money[val].get("이름") or ""})
|
||||
elif kind == "공식":
|
||||
lines.append({"공식": val})
|
||||
else:
|
||||
lines.append({"글": val})
|
||||
return out | {"줄": lines}
|
||||
|
||||
|
||||
# ── 줄 쌓기 모델 거울(`common_util_line_stack.ts`) ──────────────────────
|
||||
def _names_in(text: Any) -> list[str]:
|
||||
if text is None or str(text).strip() == "":
|
||||
return []
|
||||
try:
|
||||
return md.names(md.parse(text))
|
||||
except md.DocError:
|
||||
return []
|
||||
|
||||
|
||||
def symbols(doc: dict) -> list[str]:
|
||||
out = [md.var_id(v) for v in doc.get("변수") or []]
|
||||
for line in doc.get("식") or []:
|
||||
sid = str(line.get("기호") or "")
|
||||
if sid and sid not in out:
|
||||
out.append(sid)
|
||||
return out
|
||||
|
||||
|
||||
def _is_option(v: dict | None) -> bool:
|
||||
return bool(v and v.get("열쇠") and (v.get("연결") or {}).get("갈래") in ("선택", "입력"))
|
||||
|
||||
|
||||
def uses_of(doc: dict) -> dict[str, list[str]]:
|
||||
out: dict[str, list[str]] = {}
|
||||
|
||||
def add(sid: str, more: list[str]) -> None:
|
||||
now = out.setdefault(sid, [])
|
||||
now += [n for n in more if n not in now]
|
||||
|
||||
for v in doc.get("변수") or []:
|
||||
add(md.var_id(v), md.link_names(v["연결"]) if v.get("연결") else [])
|
||||
for line in doc.get("식") or []:
|
||||
add(str(line.get("기호") or ""), _names_in(line.get("식")))
|
||||
return out
|
||||
|
||||
|
||||
def price_sym(line: dict) -> str | None:
|
||||
text = line.get("단가")
|
||||
if text is None or str(text).strip() == "":
|
||||
return None
|
||||
try:
|
||||
n = md.parse(text)
|
||||
except md.DocError:
|
||||
return None
|
||||
if n[0] == "var":
|
||||
return n[1]
|
||||
if n[0] == "field" and n[1][0] == "var":
|
||||
return n[1][1]
|
||||
return None
|
||||
|
||||
|
||||
def owned_of(doc: dict) -> dict[int, str]:
|
||||
count: dict[str, int] = {}
|
||||
for ns in (
|
||||
*(_names_in(x.get("수량")) + _names_in(x.get("단가")) for x in doc.get("금액") or []),
|
||||
*uses_of(doc).values(),
|
||||
*([[doc["결과"]]] if doc.get("결과") else []),
|
||||
):
|
||||
for n in ns:
|
||||
count[n] = count.get(n, 0) + 1
|
||||
vars_ = {md.var_id(v): v for v in doc.get("변수") or []}
|
||||
lines = {str(x.get("기호") or "") for x in doc.get("식") or []}
|
||||
out = {}
|
||||
for i, line in enumerate(doc.get("금액") or []):
|
||||
s = price_sym(line)
|
||||
kind = ((vars_.get(s) or {}).get("연결") or {}).get("갈래") if s else None
|
||||
if s and kind in ("요소", "단가산출") and count.get(s) == 1 and s not in lines:
|
||||
out[i] = s
|
||||
return out
|
||||
|
||||
|
||||
def hidden_of(doc: dict) -> set[str]:
|
||||
"""차림에 안 넣는 기호 — 옵션 · 금액 줄이 가진 연결(가져온 값 옵션 칸은 공식 묶음에도 보임)."""
|
||||
out = set(owned_of(doc).values())
|
||||
out |= {md.var_id(v) for v in doc.get("변수") or [] if _is_option(v)}
|
||||
return out
|
||||
|
||||
|
||||
def _post_order(doc: dict, roots: list[str], uses: dict[str, list[str]]) -> list[str]:
|
||||
out: list[str] = []
|
||||
seen: set[str] = set()
|
||||
stack: list[tuple[str, int]] = []
|
||||
for start in [*roots, *symbols(doc)]: # 되부름 없이(깊은 식 사슬)
|
||||
if start in seen:
|
||||
continue
|
||||
seen.add(start)
|
||||
stack.append((start, 0))
|
||||
while stack:
|
||||
sid, k = stack.pop()
|
||||
kids = uses.get(sid) or []
|
||||
if k < len(kids):
|
||||
stack.append((sid, k + 1))
|
||||
if kids[k] not in seen:
|
||||
seen.add(kids[k])
|
||||
stack.append((kids[k], 0))
|
||||
else:
|
||||
out.append(sid)
|
||||
return out
|
||||
|
||||
|
||||
def _reach(line: dict, uses: dict[str, list[str]]) -> set[str]:
|
||||
out: set[str] = set()
|
||||
todo = _names_in(line.get("수량")) + _names_in(line.get("단가"))
|
||||
while todo:
|
||||
sid = todo.pop()
|
||||
if sid in out:
|
||||
continue
|
||||
out.add(sid)
|
||||
todo += uses.get(sid) or []
|
||||
return out
|
||||
|
||||
|
||||
def stack_of(doc: dict) -> list[dict]:
|
||||
"""`stackOf` — 저장 차림 + 안 놓인 것은 기본 자리 ·
|
||||
줄 = {꼴 금액 i | 꼴 공식 기호 틀? | 꼴 글 글}."""
|
||||
money = doc.get("금액") or []
|
||||
all_ = set(symbols(doc))
|
||||
hidden = hidden_of(doc)
|
||||
items: list[dict] = []
|
||||
where: dict[int, dict] = {}
|
||||
placed: set[str] = set()
|
||||
saved = doc.get("쌓기") if isinstance(doc.get("쌓기"), list) else []
|
||||
taken: set[int] = set()
|
||||
for s in saved:
|
||||
s = s if isinstance(s, dict) else {}
|
||||
it: dict[str, Any] = {
|
||||
"제목": str(s.get("제목") if s.get("제목") is not None else ""),
|
||||
"줄": [],
|
||||
}
|
||||
if s.get("가나다"):
|
||||
it["가나다"] = True
|
||||
for line in s.get("줄") if isinstance(s.get("줄"), list) else []:
|
||||
if isinstance(line, dict) and "금액" in line:
|
||||
i = int(line["금액"])
|
||||
name = line.get("이름")
|
||||
if not (
|
||||
0 <= i < len(money) and (money[i].get("이름") or "") == name and i not in taken
|
||||
):
|
||||
free = (k for k, m in enumerate(money) if k not in taken)
|
||||
i = next((k for k in free if (money[k].get("이름") or "") == name), -1)
|
||||
if i < 0 or i in taken:
|
||||
continue
|
||||
taken.add(i)
|
||||
where[i] = it
|
||||
it["줄"].append({"꼴": "금액", "i": i})
|
||||
elif isinstance(line, dict) and "공식" in line:
|
||||
got = [
|
||||
x
|
||||
for x in line["공식"] or []
|
||||
if x in all_ and x not in hidden and x not in placed
|
||||
]
|
||||
placed |= set(got)
|
||||
one: dict[str, Any] = {"꼴": "공식", "기호": got}
|
||||
if line.get("틀"):
|
||||
one["틀"] = str(line["틀"])
|
||||
if line.get("틀꼴"):
|
||||
one["틀꼴"] = str(line["틀꼴"])
|
||||
it["줄"].append(one)
|
||||
elif isinstance(line, dict) and "글" in line:
|
||||
it["줄"].append(
|
||||
{"꼴": "글", "글": str(line["글"] if line["글"] is not None else "")}
|
||||
)
|
||||
items.append(it)
|
||||
for i, m in enumerate(money):
|
||||
if i in taken:
|
||||
continue
|
||||
title = str(m.get("묶음") if m.get("묶음") is not None else "")
|
||||
prev = where.get(i - 1)
|
||||
if prev is not None and prev["제목"] == title:
|
||||
it = prev
|
||||
at = next(k for k, x in enumerate(prev["줄"]) if x["꼴"] == "금액" and x["i"] == i - 1)
|
||||
prev["줄"].insert(at + 1, {"꼴": "금액", "i": i})
|
||||
else:
|
||||
it = next((x for x in items if x["제목"] == title), None) if saved else None
|
||||
if it is None:
|
||||
it = {"제목": title, "줄": []}
|
||||
items.append(it)
|
||||
it["줄"].append({"꼴": "금액", "i": i})
|
||||
taken.add(i)
|
||||
where[i] = it
|
||||
uses = uses_of(doc)
|
||||
reach = [_reach(m, uses) for m in money]
|
||||
for sid in _post_order(doc, [doc["결과"]] if doc.get("결과") else [], uses):
|
||||
if sid not in all_ or sid in hidden or sid in placed:
|
||||
continue
|
||||
i = next((k for k, r in enumerate(reach) if sid in r), -1)
|
||||
it = where.get(i) if i >= 0 else (items[0] if items else None)
|
||||
if it is None:
|
||||
it = {"제목": "", "줄": []}
|
||||
items.append(it)
|
||||
block = next((x for x in it["줄"] if x["꼴"] == "공식"), None)
|
||||
if block is None:
|
||||
block = {"꼴": "공식", "기호": []}
|
||||
it["줄"].insert(0, block)
|
||||
block["기호"].append(sid)
|
||||
placed.add(sid)
|
||||
return items
|
||||
|
||||
|
||||
def save_of(items: list[dict], money: list[dict]) -> list[dict]:
|
||||
"""`saveOf`(차례 그대로)."""
|
||||
out = []
|
||||
for it in items:
|
||||
one: dict[str, Any] = {}
|
||||
if it["제목"]:
|
||||
one["제목"] = it["제목"]
|
||||
if it.get("가나다"):
|
||||
one["가나다"] = True
|
||||
lines = []
|
||||
for x in it["줄"]:
|
||||
if x["꼴"] == "금액":
|
||||
lines.append({"금액": x["i"], "이름": money[x["i"]].get("이름") or ""})
|
||||
elif x["꼴"] == "글":
|
||||
lines.append({"글": x["글"]})
|
||||
else:
|
||||
lines.append(
|
||||
{"공식": list(x["기호"])}
|
||||
| ({"틀": x["틀"]} if x.get("틀") else {})
|
||||
| ({"틀꼴": x["틀꼴"]} if x.get("틀꼴") else {})
|
||||
)
|
||||
out.append(one | {"줄": lines})
|
||||
return out
|
||||
@@ -485,11 +485,15 @@ def stmate(
|
||||
me = prices.get(block["코드"]) or {}
|
||||
meta = me.get("meta") or {}
|
||||
o = Out(block.get("이름"), me.get("단위"), "STmate", block["코드"])
|
||||
o.head["차림"] = True # 812-3 — 계 단으로 항목 · 글 줄(`M02_Doc_NativeLayout`)
|
||||
st = _Stm(o, block, prices, keys)
|
||||
for node in nodes:
|
||||
kind, src = node["종류"], {"행": node.get("행"), "글": node.get("글") or ""}
|
||||
if kind in ("빈", "글"):
|
||||
if src["글"].strip():
|
||||
if src["글"].strip().strip("'").strip():
|
||||
o.glyph(src["행"], src["글"])
|
||||
o.at[src["행"]]["차림"] = "글" # 812-3 줄 쌓기 글 줄 · 제목(`M02_Doc_NativeLayout`)
|
||||
elif src["글"].strip():
|
||||
o.glyph(src["행"], src["글"])
|
||||
elif kind == "대입":
|
||||
st.assign(node, src)
|
||||
|
||||
@@ -150,10 +150,11 @@ export function sourceOf(doc: FormulaDoc, v: DocVar): string | null {
|
||||
return String((v as { 출처?: string }).출처 ?? (head.원파일 ? "원파일" : "엑셀"));
|
||||
}
|
||||
|
||||
/** 차림에 안 넣는 기호 — 옵션 · 금액 줄이 가진 연결 · 가져온 식 값 옵션 칸 */
|
||||
/** 차림에 안 넣는 기호 — 옵션 · 금액 줄이 가진 연결(가져온 식 값 옵션 칸은 공식 묶음에도 보임 · 같은 값 · 812-3)
|
||||
* 파이썬 거울 = `M02_Doc_NativeLayout.py`(stackOf · saveOf · hiddenOf · 가져온 단가산출 차림) — 고칠 때 같이 고침 */
|
||||
export function hiddenOf(doc: FormulaDoc): Set<string> {
|
||||
const out = new Set(ownedOf(doc).values());
|
||||
for (const v of doc.변수 ?? []) if (isOption(v) || sourceOf(doc, v)) out.add(varId(v));
|
||||
for (const v of doc.변수 ?? []) if (isOption(v)) out.add(varId(v));
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -561,3 +561,7 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의
|
||||
## 요구 712 (11-09 · 사용자 · 매칭은 로직으로(돈만 보지 말 것) · 권장 순서 뒤 정합성 추정 · 엑셀 엔진 새로 짜는 게 나은지)
|
||||
|
||||
결국 전부 단가산출을 제대로 연결할 수 있어야한다인데. 결과적으로 단가산출은 로직과 값으로 되어있음. 경쟁사도 값은 주기적으로 업데이트 됨. 그렇다면 매칭해야하는 자료는 로직임. 단순히 총합계나 계산된 돈만 보면 판단이 어려워지니 조심할 것. 다음으로 권장 순서대로 했을때 정합성이 어느정도 될것같아? 그리고 현 엑셀엔진을 처음부터 코드를 만드는게 더 효율적이고 체계적일 것 같다는 생각이드는데 어떻게 생각함?
|
||||
|
||||
## 요구 713 (11-09 · 사용자 · 제안대로 진행(엑셀 읽기기 새로 + 연결 기준 = 로직) · 가져온 단가산출이 좌측에서 읽히게(나열만 X))
|
||||
|
||||
다음으로 중요한 점은 경쟁사의 단가산출을 가져왔다 가정하고 현재의 경쟁사 단가산출은 좌측영역에서 제대로 읽기가 힘든 그냥 노무비, 경비, 재료 1,2, 3 형태로 죽 나열만 되어있음. 분석도 중요하지만 보여지는것도 중요함. 너의 제안대로 잘 끝까지 진행해줘
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/* 812-3 가져온 단가산출 차림 헬퍼 — 줄 쌓기 모델(common_util_line_stack.ts)을 그 자리에서 트랜스파일해 Node 로 돌림.
|
||||
* stdin = [{키, 문서, 파이썬 [{제목, 가나다?, 줄 [{꼴, i | 기호 | 글}]}]}]
|
||||
* stdout = [{키, 같음(문서 → 줄 → 문서 · 칸 차례 무시), 거울(TS stackOf = 파이썬 stack_of), 항목 · 제목 · 공식 · 글 · 쌓기}]
|
||||
* 짝 = test_812_3_native_layout.py · 파이썬 거울 = M02_Doc_NativeLayout.py */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const cache = new Map();
|
||||
function load(at) {
|
||||
if (cache.has(at)) return cache.get(at);
|
||||
const js = ts.transpileModule(fs.readFileSync(at, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(at, box.exports);
|
||||
const need = (p) =>
|
||||
p.startsWith(".") ? load(path.resolve(path.dirname(at), `${p}.ts`)) : require(p);
|
||||
new Function("exports", "module", "require", js)(box.exports, box, need);
|
||||
return box.exports;
|
||||
}
|
||||
const ls = load(path.join(__dirname, "..", "..", "common_util", "common_util_line_stack.ts"));
|
||||
|
||||
function canon(v) {
|
||||
if (Array.isArray(v)) return v.map(canon);
|
||||
if (v && typeof v === "object") {
|
||||
const out = {};
|
||||
for (const k of Object.keys(v).sort()) out[k] = canon(v[k]);
|
||||
return out;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
const same = (a, b) => JSON.stringify(canon(a)) === JSON.stringify(canon(b));
|
||||
|
||||
const input = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const out = input.map(({ 키, 문서, 파이썬 }) => {
|
||||
const items = ls.stackOf(문서);
|
||||
const back = ls.docOf(문서, items);
|
||||
const lines = items.flatMap((it) => it.줄);
|
||||
return {
|
||||
키,
|
||||
같음: same(back, 문서),
|
||||
거울: same(items, 파이썬),
|
||||
항목: items.length,
|
||||
제목: items.filter((it) => it.제목.trim()).length,
|
||||
공식: lines.filter((l) => l.꼴 === "공식" && l.기호.length).length,
|
||||
글: lines.filter((l) => l.꼴 === "글").length,
|
||||
쌓기: "쌓기" in 문서,
|
||||
...(same(items, 파이썬) ? {} : { ts: items }),
|
||||
};
|
||||
});
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -4,7 +4,7 @@
|
||||
(a) 화면 셈 거울 — EST 여섯 · STC 일곱 산근 블록 모두: 화면 길(TS importView → calcDoc) = 서버 길
|
||||
(`M02_Doc_Import.run` 계산기) 답 전부 같음(줄 · 비목 · 합 · 표지)
|
||||
(b) 옵션 칸 — 원파일 수 하나 변수(출처 「원파일」) = 옵션 가로 칸(`sourceOf`) ·
|
||||
줄 쌓기 공식 묶음에 안 들어감 · 금액 줄 단가로 쓰는 수 · 엑셀 단가 기억 연결 = 옵션 칸 아님
|
||||
줄 쌓기 공식 묶음에도 보임(812-3) · 금액 줄 단가로 쓰는 수 · 엑셀 단가 기억 연결 = 옵션 칸 아님
|
||||
(c) 추정 칸 — 문서 머리 `가져온.추정` 줄 수 = 원본 확정 추정 수(EST) · 원코드 · 규격 머리
|
||||
(d) 소계 줄 — 합 밖 · 비목 없음(화면 비목 칸 빈칸 · 종류 「소계」 —
|
||||
`line_stack_edit` · `line_stack_out` 글 대조)
|
||||
@@ -121,7 +121,8 @@ def test_screen_calc_mirror_and_option_cells(kind: str, tag: str) -> None:
|
||||
and not any(str(m.get("단가")) == v["열쇠"] for m in doc.get("금액") or [])
|
||||
]
|
||||
assert set(nums) <= set(shape["옵션"]), (tag, key, nums, shape["옵션"])
|
||||
assert not set(shape["옵션"]) & set(shape["공식"]), (tag, key) # 옵션 칸 = 공식 묶음 밖
|
||||
# 옵션 칸 = 공식 묶음에도 보임(812-3 · 같은 값)
|
||||
assert set(shape["옵션"]) <= set(shape["공식"]), (tag, key)
|
||||
for v in doc["변수"]:
|
||||
if v["열쇠"] in shape["옵션"]:
|
||||
assert v["연결"]["갈래"] == "숫자" and "엑셀" not in v["연결"]
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
"""812-3 지킴 — 가져온 단가산출(원파일) 줄 쌓기 차림 = 경쟁사 산출근거 꼴(요구 713).
|
||||
|
||||
차림 = `M02_Doc_NativeLayout`.
|
||||
(a) 견본(거창 덤프 꼴) — 1 측구터파기(토사) 「기 계 … 굴삭기」 항목 + 공식 묶음
|
||||
q · K · f · E · Cm · Q + 보조인부 항목 · 41 합판거푸집(4회) 근거 글 ·
|
||||
재료비(재료 · 소계 · 율 · 글) · 인건비(인력 · 소계 · 율 · 글) ·
|
||||
15 전석 메쌓기 별산 · 단산 참조 Q 직접 · 기계 · 인부 ·
|
||||
2 가 · 나 소항목 · 봉화 STC = 계 단마다 항목
|
||||
(b) 전수 — 원파일 EST · STC 산근 블록 모두: 차림 전후 셈(비목 · 계 · 줄 금액) 같음 ·
|
||||
TS 왕복(문서 → 줄 → 문서) 같음 · TS `stackOf` = 파이썬 거울 `stack_of` ·
|
||||
수 표 = tmp/s8123/sweep.json(일회 기록)
|
||||
(c) M01 문서 1,518 — 차림 안 바뀜(쌓기 없음 · 왕복 같음 · 거울 같음)
|
||||
원파일 없으면 그 시험만 skip(다른 PC).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HERE = Path(__file__).resolve().parent
|
||||
for p in (ROOT, HERE):
|
||||
if str(p) not in sys.path:
|
||||
sys.path.insert(0, str(p))
|
||||
|
||||
import test_810_2_native_doc as t2 # noqa: E402
|
||||
from test_810_1_estx import FILES as EST # noqa: E402
|
||||
from test_810_1_stc import FILES as STC # noqa: E402
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc # noqa: E402
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim # noqa: E402
|
||||
from M02_MasterTemplete import M02_Doc_NativeLayout as lay # noqa: E402
|
||||
|
||||
COSTS = ("노무비", "재료비", "경비", "계")
|
||||
|
||||
|
||||
def _ts(cases: list[dict]) -> list[dict]:
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_812_3_layout.cjs")],
|
||||
input=json.dumps(dc.ml._norm(cases), ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr[-3000:]
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _doc(kind: str, tag: str, no: int, laid: bool = True) -> tuple[dict, dict]:
|
||||
sk = t2.sk_of(t2.fn.emit(t2.native(kind, tag), no))
|
||||
keep = lay.apply
|
||||
if not laid:
|
||||
lay.apply = lambda doc, meta: [x.pop(lay.KEEP, None) for x in doc.get("금액") or []]
|
||||
try:
|
||||
return sk, dim.derive(sk, None, t2.whole())
|
||||
finally:
|
||||
lay.apply = keep
|
||||
|
||||
|
||||
def _items(doc: dict) -> list[dict]:
|
||||
return lay.stack_of(doc)
|
||||
|
||||
|
||||
def _show(doc: dict) -> list[tuple]:
|
||||
"""(제목, 가나다, [줄 글]) — 금액 = 이름 · 공식 = 「=기호…」 · 글 = 「#글」."""
|
||||
money = doc["금액"]
|
||||
out = []
|
||||
for it in _items(doc):
|
||||
lines = []
|
||||
for x in it["줄"]:
|
||||
if x["꼴"] == "금액":
|
||||
lines.append(money[x["i"]]["이름"])
|
||||
elif x["꼴"] == "공식":
|
||||
lines.append("=" + " ".join(x["기호"]))
|
||||
else:
|
||||
lines.append("#" + x["글"])
|
||||
out.append((it["제목"], bool(it.get("가나다")), lines))
|
||||
return out
|
||||
|
||||
|
||||
# ── (a) 견본 ─────────────────────────────────────────────────────────────
|
||||
def test_sample_excavation_machine_and_formula() -> None:
|
||||
_, doc = _doc("EST", "거창", 1)
|
||||
got = _show(doc)
|
||||
assert got == [
|
||||
("기 계(100%) : 굴삭기(무한궤도) (0.7㎥)", False, ["=q K f E Cm Q", "굴삭기(무한궤도)"]),
|
||||
("보조인부(1일당 1인)", False, ["보통인부"]),
|
||||
], got
|
||||
assert [x["묶음"] for x in doc["금액"]] == [got[0][0], got[1][0]]
|
||||
|
||||
|
||||
def test_sample_formwork_text_rate_subtotal() -> None:
|
||||
_, doc = _doc("EST", "거창", 41)
|
||||
got = _show(doc)
|
||||
assert got[0] == (
|
||||
"",
|
||||
False,
|
||||
["#< 토목품셈 6-3-1 합판거푸집(2022개정) >", "#※ 사용횟수 4회 : 보통"],
|
||||
)
|
||||
assert got[1] == (
|
||||
"재료비",
|
||||
False,
|
||||
[
|
||||
"합판(내수),대전",
|
||||
"외송각재,대전",
|
||||
"주자재소계",
|
||||
"#소모자재(박리재등) (주자재비의 9%)",
|
||||
"율",
|
||||
],
|
||||
)
|
||||
assert got[2][0] == "인건비 (24년 품셈 개정)" and got[2][2][:3] == [
|
||||
"형틀목공",
|
||||
"보통인부",
|
||||
"인부소계",
|
||||
]
|
||||
assert got[2][2][3].startswith("#공구손료") and len(got) == 3
|
||||
sub = next(x for x in doc["금액"] if x["이름"] == "주자재소계")
|
||||
assert sub["합밖"] and "비목" not in sub # 소계 = 합 밖 · 비목 칸 빈칸 그대로(810-2)
|
||||
|
||||
|
||||
def test_sample_stone_wall_reference_and_direct_q() -> None:
|
||||
_, doc = _doc("EST", "거창", 15)
|
||||
got = _show(doc)
|
||||
titles = [g[0] for g in got]
|
||||
assert titles[0] == "전석 구입운반 : 별산" and got[0][2] == []
|
||||
assert (
|
||||
titles[1].startswith("뒷채움돌 운반 및 채집")
|
||||
and got[1][2][-1] == "뒷채움돌기초잡석채집운반"
|
||||
)
|
||||
assert got[1][2][0].split()[0] == "=Q" # 단산 참조 Q 직접 = 그 항목 공식 묶음
|
||||
assert titles[3].startswith("전석쌓기 기계") and got[3][2] == ["=Q_3", "굴삭기(무한궤도)"]
|
||||
assert got[4] == ("전석쌓기 인부", False, ["석공", "보통인부"])
|
||||
assert "합" not in "".join(titles) # 합계 절 = 화면 자동 합계(두 번 안 보임)
|
||||
|
||||
|
||||
def test_sample_stc_steps_are_items() -> None:
|
||||
_, doc = _doc("STC", "봉화", 14)
|
||||
got = [
|
||||
(t, g, [x for x in ls if not x.startswith("=P_")]) for t, g, ls in _show(doc)
|
||||
] # 손 단가 = 기본 자리
|
||||
assert got == [
|
||||
("", False, ["#▣ 토공의 비탈규준틀[개소]2-6-1"]),
|
||||
("재 료 비", False, ["각재(거푸집용,외송)", "판재", "일반못(철못)"]),
|
||||
("인 건 비", False, ["건축목공", "보통인부"]),
|
||||
], got
|
||||
|
||||
|
||||
def test_sample_ganada_sub_items() -> None:
|
||||
_, doc = _doc("EST", "거창", 2)
|
||||
got = _show(doc)
|
||||
assert got[0] == ("기 계(100%)", False, [])
|
||||
assert got[1][:2] == ("굴삭기(무한궤도) (0.7+브레이카)", True)
|
||||
assert got[2] == ("치즐(0.7㎥용)", True, ["치즐(0.7㎥)"]) # 「다. 소 계」 절 = 뺌
|
||||
|
||||
|
||||
# ── (b) 전수 ─────────────────────────────────────────────────────────────
|
||||
def _money(sk: dict, doc: dict) -> tuple:
|
||||
got = dim.run(doc, sk, t2.whole())
|
||||
lines = sorted(str(x.get("금액")) for x in got.get("줄") or [])
|
||||
return tuple(str(got.get(c)) for c in COSTS), lines
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _sweep() -> dict:
|
||||
stat = {"블록": 0, "금액같음": 0, "다름": [], "cases": []}
|
||||
for kind, files in (("EST", EST), ("STC", STC)):
|
||||
for tag, path in files.items():
|
||||
if not path.exists():
|
||||
continue
|
||||
for no in t2.fn.numbers(t2.native(kind, tag)):
|
||||
sk, doc = _doc(kind, tag, no)
|
||||
_, plain = _doc(kind, tag, no, laid=False)
|
||||
stat["블록"] += 1
|
||||
if _money(sk, doc) == _money(sk, plain):
|
||||
stat["금액같음"] += 1
|
||||
else:
|
||||
stat["다름"].append((kind, tag, no))
|
||||
stat["cases"].append(
|
||||
{"키": f"{kind}|{tag}|{no}", "문서": doc, "파이썬": _items(doc)}
|
||||
)
|
||||
return stat
|
||||
|
||||
|
||||
def test_sweep_money_same_and_ts_round_trip() -> None:
|
||||
stat = _sweep()
|
||||
if not stat["블록"]:
|
||||
pytest.skip("원파일 없음")
|
||||
assert not stat["다름"] and stat["금액같음"] == stat["블록"], stat["다름"][:5]
|
||||
got = _ts(stat["cases"])
|
||||
bad = {f: [g["키"] for g in got if not g[f]] for f in ("같음", "거울")}
|
||||
assert bad == {"같음": [], "거울": []}, {k: v[:5] for k, v in bad.items()}
|
||||
by = {}
|
||||
for g in got:
|
||||
kind = g["키"].split("|")[0]
|
||||
row = by.setdefault(
|
||||
kind, dict.fromkeys(("블록", "항목", "제목", "공식", "글", "쌓기", "무제목만"), 0)
|
||||
)
|
||||
row["블록"] += 1
|
||||
for f in ("항목", "제목", "공식", "글"):
|
||||
row[f] += g[f]
|
||||
row["쌓기"] += g["쌓기"]
|
||||
row["무제목만"] += g["항목"] > 0 and g["제목"] == 0
|
||||
out = ROOT / "tmp" / "s8123"
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
(out / "sweep.json").write_text(json.dumps(by, ensure_ascii=False, indent=1), "utf-8")
|
||||
assert by.get("EST", {}).get("제목", 0) > by.get("EST", {}).get("블록", 0)
|
||||
|
||||
|
||||
# ── (c) M01 ──────────────────────────────────────────────────────────────
|
||||
def test_m01_docs_layout_unchanged() -> None:
|
||||
docs = dc.folder()
|
||||
assert len(docs) >= 1518
|
||||
cases = [{"키": k, "문서": d, "파이썬": _items(d)} for k, d in docs.items()]
|
||||
got = _ts(cases)
|
||||
bad = {f: [g["키"] for g in got if not g[f]] for f in ("같음", "거울")}
|
||||
assert bad == {"같음": [], "거울": []}, {k: v[:5] for k, v in bad.items()}
|
||||
assert not any(g["쌓기"] for g in got)
|
||||
Reference in New Issue
Block a user