Merge remote-tracking branch 'origin/dev' into main_laptop_1
This commit is contained in:
@@ -13,13 +13,14 @@ const ALIAS: Record<string, string> = {
|
||||
리터: "ℓ",
|
||||
set: "조",
|
||||
세트: "조",
|
||||
hr: "시간",
|
||||
};
|
||||
const UNITS: Record<string, Record<string, number>> = {
|
||||
무게: { g: 1, kg: 1000, 톤: 1000000 },
|
||||
길이: { mm: 1, cm: 10, m: 1000, km: 1000000 },
|
||||
넓이: { cm2: 1, m2: 10000 },
|
||||
부피: { cm3: 1, ℓ: 1000, m3: 1000000 },
|
||||
개수: { 개: 1, 본: 1, 매: 1 },
|
||||
개수: { 개: 1, 본: 1, 매: 1, 개소: 1 },
|
||||
};
|
||||
const SCALE: Record<string, [string, number]> = {};
|
||||
for (const [kind, group] of Object.entries(UNITS)) {
|
||||
|
||||
@@ -449,17 +449,22 @@ function formulaCell(e: Entry, ctx: HoContext, text = ""): HTMLElement {
|
||||
|
||||
const tagOf = (x: object): string | undefined => (x as { 꼬리표?: string }).꼬리표 || undefined;
|
||||
const sourceTag = (text: string): HTMLElement => {
|
||||
const tag = createBadge(text, "neutral");
|
||||
const [shown, ...tip] = text.split("\n"); // 첫 줄 = 보임 · 나머지 = 풍선(없으면 같은 글) — 740-2
|
||||
const tag = createBadge(shown, "neutral");
|
||||
tag.classList.add("m01lab__src");
|
||||
tag.title = text;
|
||||
tag.title = tip.length ? tip.join(" ") : shown;
|
||||
return tag;
|
||||
};
|
||||
|
||||
function nameCell(e: Entry, label: string, tag: string): HTMLElement {
|
||||
return el("div", {
|
||||
children: [
|
||||
el("div", { text: label }),
|
||||
...(e.source ? [sourceTag(e.source)] : []),
|
||||
e.source
|
||||
? el("div", {
|
||||
className: "m01lab__namerow", // 이름 옆에 꼬리표 — 줄 높이 안 바뀜(740-2)
|
||||
children: [el("span", { text: label }), sourceTag(e.source)],
|
||||
})
|
||||
: el("div", { text: label }),
|
||||
...(e.share && label === e.name ? [createBadge(`${e.share} ${tl("Share")}`, "accent")] : []),
|
||||
...(e.spec ? [el("div", { className: "ui-text ui-text--muted", text: e.spec })] : []),
|
||||
...(e.warn ? [createBadge(`${tx("Calc_UnitWarn")} · ${e.warn}`, "warning")] : []),
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""M02 일위대가 불러오기 — 못옮김 · 안 이은 기계 줄 → M01 요소 후보 셋(PLAN 746-1 · 요구 585).
|
||||
|
||||
후보 = `[{키, 갈래 노임|기계|자재, 이름, 규격, 단위, 단가}]` 셋까지 — 보여주기만(자동 이음 없음 · 고르면 지금
|
||||
잇기 길 `{노임|기계|자재: 키}`).
|
||||
자재 = `M02_Hopyo_Material.candidates` 점수(이름 글자 묶음 · 규격) · 단가 = M01 자재 값(엑셀 줄 단위)
|
||||
노임 = LB 이름 글자 묶음 닮음 · 단가 = LB 값
|
||||
기계 = EQ 갈래 겹침 · 규격 수(같음 1 · 겹침 0.5) · 이름 낱말 덤 · 단가 없음(기계경비는 로직 셈)
|
||||
차례 = 점수(0.1 단위로 접음) › 엑셀 단가에 가까운 것(엑셀 단가 = 블록 글 「이름 단가 × 식 = 금액」 · 모르면 점수만).
|
||||
서버 단독 · 읽기만.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import mtypes, parse_rhs, specnums
|
||||
|
||||
TOP = 3
|
||||
POOL = 5
|
||||
|
||||
|
||||
def prices(block: dict[str, Any] | None) -> dict[str, float]:
|
||||
"""블록 글 → `{이름: 엑셀 단가}`(직종 줄 「직종 : 단가 × 식」 · 자재 줄 「이름 단가 × 식 = 금액」)."""
|
||||
out: dict[str, float] = {}
|
||||
for raw in (block or {}).get("글") or []:
|
||||
row = str(raw).rstrip()
|
||||
cm = finger._COLON.match(row)
|
||||
mm = None if cm else finger._MAT.match(row)
|
||||
name = (cm or mm).group("name").strip() if (cm or mm) else ""
|
||||
got = parse_rhs((cm or mm).group("rhs")) if name else None
|
||||
if got and got.get("단가"):
|
||||
out.setdefault(re.sub(r"^[0-9가-힣]\.", "", name).strip(), float(got["단가"]))
|
||||
return out
|
||||
|
||||
|
||||
def _gap(price: Any, want: float | None) -> float:
|
||||
try:
|
||||
p = float(price)
|
||||
except (TypeError, ValueError):
|
||||
return math.inf
|
||||
return abs(math.log(p / want)) if want and p > 0 else math.inf
|
||||
|
||||
|
||||
def _order(rows: list[tuple[float, dict]], want: float | None) -> list[dict]:
|
||||
rows.sort(key=lambda x: -x[0])
|
||||
pool = rows[:POOL]
|
||||
pool.sort(key=lambda x: (-round(x[0], 1), _gap(x[1].get("단가"), want)))
|
||||
return [one for _, one in pool[:TOP]]
|
||||
|
||||
|
||||
def _num(x: Any) -> int | float | None:
|
||||
if x is None:
|
||||
return None
|
||||
f = float(x)
|
||||
return int(f) if f == int(f) else round(f, 4)
|
||||
|
||||
|
||||
def materials(name: str, unit: str = "", want: float | None = None) -> list[dict[str, Any]]:
|
||||
whole = m01.loaded()[1]
|
||||
rows = []
|
||||
for c in hmat.candidates({"이름": name, "규격": "", "단위": unit}, POOL):
|
||||
code = c.get("자재")
|
||||
if not code:
|
||||
continue
|
||||
try:
|
||||
price = hmat.mm.element(whole, code, {}, unit or None)[0]
|
||||
except Exception: # noqa: BLE001 — 값 못 읽음 = 단가 없음
|
||||
price = None
|
||||
one = {"키": code, "갈래": "자재", "이름": c.get("이름"), "규격": c.get("규격") or ""}
|
||||
rows.append((c.get("점수") or 0.0, {**one, "단위": c.get("단위"), "단가": _num(price)}))
|
||||
return _order(rows, want)
|
||||
|
||||
|
||||
def labor(name: str, want: float | None = None) -> list[dict[str, Any]]:
|
||||
whole = m01.loaded()[1]
|
||||
me = hm._grams(re.sub(r"\s", "", name))
|
||||
rows = []
|
||||
for row in whole.numbers.get("LB", {}).values():
|
||||
them = hm._grams(re.sub(r"\s", "", str(row.get("이름") or "")))
|
||||
score = 2 * len(me & them) / (len(me) + len(them))
|
||||
if score > 0:
|
||||
one = {"키": str(row.get("키")), "갈래": "노임", "이름": row.get("이름"), "규격": ""}
|
||||
rows.append((score, {**one, "단위": "인", "단가": _num(row.get("값"))}))
|
||||
return _order(rows, want)
|
||||
|
||||
|
||||
def machines(text: str, kinds: set | None = None, nums: set | None = None) -> list[dict]:
|
||||
"""기계 글(이름 규격) → EQ 후보 — 갈래 · 규격 수가 없으면 글에서 읽음."""
|
||||
whole = m01.loaded()[1]
|
||||
kinds = set(kinds or ()) or mtypes(text)
|
||||
nums = set(nums or ()) or set(specnums(text))
|
||||
mine = hm.words(text)
|
||||
rows = []
|
||||
for row in whole.numbers.get("EQ", {}).values():
|
||||
types = mtypes(f"{row.get('상세구분')} {row.get('이름')}")
|
||||
if not kinds & types:
|
||||
continue
|
||||
spec = set(specnums(row.get("규격")))
|
||||
score = len(kinds & types) / len(kinds | types)
|
||||
score += 1.0 if nums and nums == spec else 0.5 if nums & spec else 0.0
|
||||
score += 0.3 * len(mine & hm.words(row.get("이름")))
|
||||
one = {"키": str(row.get("키")), "갈래": "기계", "이름": row.get("이름")}
|
||||
unit = (row.get("단위") or {}).get("내용시간") if isinstance(row.get("단위"), dict) else ""
|
||||
rows.append(
|
||||
(score, {**one, "규격": row.get("규격") or "", "단위": unit or "시간", "단가": None})
|
||||
)
|
||||
return _order(rows, None)
|
||||
|
||||
|
||||
def lost(one: dict[str, Any], want: float | None = None) -> list[dict[str, Any]]:
|
||||
"""못옮김 한 줄(`M02_Hopyo_SkPatch` 꼴) → 후보."""
|
||||
kind, name = one.get("갈래"), str(one.get("이름") or "")
|
||||
if kind == "자재":
|
||||
return materials(name, str(one.get("단위") or ""), want)
|
||||
if kind == "노임":
|
||||
return labor(name, want)
|
||||
if kind == "기계":
|
||||
return machines(f"{name} {one.get('규격') or ''}", set(name.split("·")))
|
||||
return []
|
||||
@@ -377,13 +377,22 @@ def run_job(folder: Path, key: str) -> dict[str, Any]:
|
||||
if unit not in out["블록"]:
|
||||
out["블록"][unit] = compute(block, line, targets)
|
||||
out["셈"] = [at, len(todo)]
|
||||
if at % EVERY == 0:
|
||||
atomic_write_json(path, out)
|
||||
if at % EVERY == 0 and not _put(folder, key, path, out):
|
||||
return out
|
||||
out["끝"] = True
|
||||
atomic_write_json(path, out)
|
||||
_put(folder, key, path, out)
|
||||
return out
|
||||
|
||||
|
||||
def _put(folder: Path, key: str, path: Path, out: dict[str, Any]) -> bool:
|
||||
"""캐시 씀 — 셈 도중 HP 를 끝냈으면(743-1) 안 쓰고 지움 → 거짓."""
|
||||
if not _hp_path(folder, key).is_file():
|
||||
path.unlink(missing_ok=True)
|
||||
return False
|
||||
atomic_write_json(path, out)
|
||||
return True
|
||||
|
||||
|
||||
def _work() -> None:
|
||||
while True:
|
||||
folder, key = _JOBS.get()
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
"""M02 일위대가 불러오기 끝냄(PLAN 743-1 · 요구 582-3 · 584-2) — 올린 엑셀은 그 불러오기 동안만.
|
||||
|
||||
끝냄 한 길 `finish` = [끝내기] · 화면 떠남 · 하루 지남(`Store.EXPIRE`) · 붙여넣기 만듦 뒤 ·
|
||||
엑셀 보기 [삭제]:
|
||||
HP · 격자 · 맞춤셈 캐시 지움 · 그 HP 로 만든 이 층 일위대가(SC) · 단가산출(SK) 엑셀 사본 걷음
|
||||
(`본뜬` → 출처 `{층 「실무자료」, 파일, 때, 누가}` + 전국자재 · 복사원본 ·
|
||||
SC 줄 메모 「실무 …」 걷음 · `본뜬.실무수량` = 모든 값이 그 줄 저장 `수량.손` 과 같을 때만
|
||||
걷음(742-1 · 다르면 남김 = 금액 그대로)) · SK `본뜬.또` = 그 HP 몫만 뺌 ·
|
||||
문서 몸(줄 · 덮개 · 더한줄 · 별칭 · 구분)과 별칭 장부는 그대로 ·
|
||||
끝낸 기록 없음(같은 엑셀 다시 올림 됨 · 키 번은 `불러옴.json` 번으로 다시 안 씀).
|
||||
옛 파일 정리 `tidy`(한 번 · 층 폴더마다 · 미리 보기 = 표만) — 옛 HP(지문 없음) · 하루 지난 HP 끝냄 ·
|
||||
끝낸 기록 지문 비움 · HP 가 없는 SC · SK 엑셀 사본 걷음.
|
||||
`python -m M02_MasterTemplete.M02_Hopyo_Forget <층 폴더>… [--apply]`(없으면 미리 보기).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
|
||||
ORIGIN = "실무자료"
|
||||
KEEP = ("층", "파일", "전국자재", "복사원본")
|
||||
_MEMO = re.compile(r"^실무 ")
|
||||
_HP = re.compile(r"^(HP-\d{6}) ")
|
||||
|
||||
|
||||
# ── 사본 걷음(순수) ───────────────────────────────────────────────────
|
||||
def _slim(got: dict[str, Any]) -> dict[str, Any]:
|
||||
"""출처만 — 파일 · 때 · 누가(+ 전국자재 · 복사원본)."""
|
||||
sure = got.get("확인") if isinstance(got.get("확인"), dict) else {}
|
||||
out = {k: got[k] for k in KEEP if got.get(k) is not None}
|
||||
return {**out, "층": ORIGIN, "때": sure.get("때"), "누가": sure.get("누가")}
|
||||
|
||||
|
||||
def _hand(one: Any) -> Any:
|
||||
qty = one.get("수량") if isinstance(one, dict) else None
|
||||
return qty.get("손") if isinstance(qty, dict) else None
|
||||
|
||||
|
||||
def _carried(doc: dict[str, Any], hands: dict[str, Any]) -> bool:
|
||||
"""실무수량 값이 모두 그 줄 · 자재 저장 수량과 같음(742-1 저장 뒤)."""
|
||||
lines = doc.get("담은로직") or []
|
||||
mats = {m.get("코드"): m for m in doc.get("자재") or [] if isinstance(m, dict)}
|
||||
for at, value in (hands.get("줄") or {}).items():
|
||||
i = int(at) if str(at).isdigit() else -1
|
||||
if not 0 <= i < len(lines) or _hand(lines[i]) != value:
|
||||
return False
|
||||
return all(_hand(mats.get(code)) == value for code, value in (hands.get("자재") or {}).items())
|
||||
|
||||
|
||||
def strip_sc(doc: dict[str, Any]) -> dict[str, Any]:
|
||||
"""일위대가(SC) 엑셀 사본 걷음 — 본뜬 출처만 · 줄 메모 「실무 …」 뺌."""
|
||||
got = doc.get("본뜬") or {}
|
||||
lines = [
|
||||
{k: v for k, v in one.items() if k != "메모"}
|
||||
if isinstance(one, dict) and _MEMO.match(str(one.get("메모") or ""))
|
||||
else one
|
||||
for one in doc.get("담은로직") or []
|
||||
]
|
||||
out = {**doc, "담은로직": lines, "본뜬": _slim(got)}
|
||||
hands = got.get("실무수량")
|
||||
if isinstance(hands, dict) and hands and not _carried(out, hands):
|
||||
out["본뜬"]["실무수량"] = hands # 742 전 저장본 — 셈이 기대므로 남김
|
||||
return out
|
||||
|
||||
|
||||
def strip_sk(doc: dict[str, Any], gone: Callable[[dict], bool]) -> dict[str, Any]:
|
||||
"""단가산출(SK) — 끝낸 출처(`gone(본뜬 · 또 한 칸)`) 몫만 걷음 · 다른 불러오기 몫은 그대로."""
|
||||
got = doc.get("본뜬") or {}
|
||||
also = [x for x in got.get("또") or [] if isinstance(x, dict) and not gone(x)]
|
||||
own = _slim(got) if gone(got) else {k: v for k, v in got.items() if k != "또"}
|
||||
return {**doc, "본뜬": {**own, "또": also} if also else own}
|
||||
|
||||
|
||||
# ── 끝냄 ─────────────────────────────────────────────────────────────
|
||||
def _docs(folder: Path, kind: str):
|
||||
prefix = "SC" if kind == "precombo" else "SK"
|
||||
for path in sorted((folder / kind).glob(f"{prefix}-*.json")):
|
||||
try:
|
||||
doc = json.loads(path.read_bytes())
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
got = doc.get("본뜬") if isinstance(doc, dict) else None
|
||||
if isinstance(got, dict) and got.get("층") == ORIGIN:
|
||||
yield path, doc
|
||||
|
||||
|
||||
def _here(got: dict[str, Any], here: refs.Here) -> bool:
|
||||
"""출처 원본이 이 층(원본 없는 옛 것 = 이 층)."""
|
||||
return not got.get("원본") or tuple(got["원본"]) == tuple(here)
|
||||
|
||||
|
||||
def forget(folder: str | Path, gone: Callable[[str], bool], apply: bool = True) -> list[str]:
|
||||
"""`gone(본뜬 키)` 인 이 층 SC · SK 엑셀 사본 걷음 → 걷은 문서 키(`apply` 거짓 = 셈만)."""
|
||||
folder, out = Path(folder), []
|
||||
here = refs.where(folder)
|
||||
|
||||
def hit(one: dict[str, Any]) -> bool:
|
||||
mark = str(one.get("키") or "")
|
||||
return bool(mark) and _here(one, here) and gone(mark)
|
||||
|
||||
for kind in ("precombo", "sangeun"):
|
||||
for path, doc in _docs(folder, kind):
|
||||
got = doc["본뜬"]
|
||||
if not any(hit(x) for x in [got, *(got.get("또") or [])] if isinstance(x, dict)):
|
||||
continue
|
||||
new = strip_sc(doc) if kind == "precombo" else strip_sk(doc, hit)
|
||||
if new != doc:
|
||||
out.append(path.stem)
|
||||
if apply:
|
||||
atomic_write_json(path, new)
|
||||
return out
|
||||
|
||||
|
||||
def _caches(folder: Path, key: str) -> list[Path]:
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
|
||||
try:
|
||||
got = fit.cache_path(folder, key)
|
||||
except Exception: # noqa: BLE001 — 캐시 자리 못 찾음 = 지울 것 없음
|
||||
return []
|
||||
return list(got.parent.glob(f"{'_'.join(got.name.split('_')[:2])}_*.json"))
|
||||
|
||||
|
||||
def finish(folder: str | Path, key: str) -> dict[str, Any]:
|
||||
"""불러오기 끝냄 — 만든 문서 사본 걷음 · HP · 격자 · 맞춤셈 캐시 지움 → `{지움, 만든}`."""
|
||||
folder = Path(folder)
|
||||
hopyo_store.read(folder, key) # 없으면 404
|
||||
head = f"{key} "
|
||||
made = forget(folder, lambda mark: mark.startswith(head))
|
||||
caches = _caches(folder, key) # HP 지우기 전에 자리 셈(HP 지문)
|
||||
hopyo_store.delete(folder, key) # 먼저 지움 — 뒤 셈은 HP 없으면 캐시 안 씀(`Fit._put`)
|
||||
# ponytail: 뒤 셈이 HP 확인 직후 · 쓰기 직전이면 한 벌 남을 수 있음(ms 틈) — tmp 캐시라 둠
|
||||
for old in caches:
|
||||
old.unlink(missing_ok=True)
|
||||
return {"지움": key, "만든": len(made)}
|
||||
|
||||
|
||||
def expired(folder: str | Path, now: datetime | None = None) -> list[str]:
|
||||
"""하루 지난 작업 중 HP 키(`M02_Hopyo_Store.stale`)."""
|
||||
out = []
|
||||
for path in sorted((Path(folder) / hopyo_store.DIRNAME).glob("HP-*.json")):
|
||||
doc = hopyo_store._load(path)
|
||||
if isinstance(doc, dict) and doc.get("지문") and hopyo_store.stale(doc, now):
|
||||
out.append(path.stem)
|
||||
return out
|
||||
|
||||
|
||||
def sweep(folder: str | Path, now: datetime | None = None) -> list[str]:
|
||||
"""하루 지난 HP 끝냄 → 끝낸 키."""
|
||||
for key in (got := expired(folder, now)):
|
||||
finish(folder, key)
|
||||
return got
|
||||
|
||||
|
||||
# ── 옛 파일 정리(한 번) ───────────────────────────────────────────────
|
||||
def tidy(folder: str | Path, apply: bool = False) -> list[dict[str, str]]:
|
||||
"""옛 실무자료 정리 표 `[{할일, 대상}]` — `apply` 거짓 = 미리 보기(쓰기 없음)."""
|
||||
folder = Path(folder)
|
||||
home = folder / hopyo_store.DIRNAME
|
||||
rows: list[dict[str, str]] = []
|
||||
every = sorted(p.stem for p in home.glob("HP-*.json"))
|
||||
old = [k for k in every if not (hopyo_store._load(home / f"{k}.json") or {}).get("지문")]
|
||||
ended = [*old, *expired(folder)]
|
||||
live = set(every) - set(ended)
|
||||
for key in ended:
|
||||
rows.append({"할일": "HP 끝냄(옛 · 하루 지남)", "대상": f"{hopyo_store.DIRNAME}/{key}"})
|
||||
if apply:
|
||||
finish(folder, key)
|
||||
log = hopyo_store.done(folder)
|
||||
if log["지문"]:
|
||||
rows.append({"할일": f"끝낸 기록 지문 {len(log['지문'])} 비움", "대상": "불러옴.json"})
|
||||
if apply:
|
||||
hopyo_store._put_done(folder, {"번": log["번"]})
|
||||
|
||||
def gone(mark: str) -> bool:
|
||||
m = _HP.match(mark)
|
||||
return bool(m) and m.group(1) not in live
|
||||
|
||||
for key in forget(folder, gone, apply):
|
||||
rows.append({"할일": "엑셀 사본 걷음(출처만)", "대상": key})
|
||||
return rows
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
apply = "--apply" in argv
|
||||
for one in [a for a in argv if a != "--apply"]:
|
||||
rows = tidy(one, apply)
|
||||
print(f"# {one} — {'고침' if apply else '미리 보기'} {len(rows)}")
|
||||
for row in rows:
|
||||
print(f"{row['할일']}\t{row['대상']}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
@@ -309,7 +309,12 @@ def _grouped(got: dict, line: dict, ctx: Ctx) -> dict:
|
||||
return {**c, "뜻겹침": hm.near(mine, one["words"]), "출처같음": same, "책": book}
|
||||
|
||||
named = [mark(c) for c in hm.name_cands(line, ctx.got)]
|
||||
return {**got, "후보": [mark(c) for c in got.get("후보") or []], "이름후보": named}
|
||||
cands = [mark(c) for c in got.get("후보") or []]
|
||||
# 745-1 미리 고름 점 — 「다른 점」 1위 + 출처 같음 · 누르기는 사용자
|
||||
top = cands[0] if cands else {}
|
||||
if top.get("판정") == "다른 점" and top.get("출처같음"):
|
||||
top["미리"] = True
|
||||
return {**got, "후보": cands, "이름후보": named}
|
||||
|
||||
|
||||
def _ledger_off(entry: dict | None, alias: dict) -> bool:
|
||||
|
||||
@@ -24,6 +24,7 @@ from typing import Any
|
||||
from M02_MasterTemplete import M02_Hopyo_Build as hb
|
||||
from M02_MasterTemplete import M02_Hopyo_Excel as hx
|
||||
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
|
||||
from M02_MasterTemplete import M02_Hopyo_Forget as forget
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hstore
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
@@ -296,7 +297,8 @@ def paste(
|
||||
folder: Any, text: str, cols: Any, who: Any, lines_only: bool = False, taken: Any = ()
|
||||
) -> dict[str, Any]:
|
||||
"""[Ctrl+V] — `갈래` 「물음」(열 짝 짐작 + 보기 · 안 씀) · 「줄」(호표 머리 없음 · `lines_only`) ·
|
||||
「만듦」(호표 머리 있음 = 실무자료 「붙여넣기 …」 + 호표마다 사전조합 · 705-3 `build_all` 그대로)."""
|
||||
「만듦」(호표 머리 있음 = 실무자료 「붙여넣기 …」 + 호표마다 사전조합 · 705-3 `build_all` 그대로
|
||||
· 만든 뒤 실무자료 끝냄 743-1)."""
|
||||
try:
|
||||
parsed = read_text(text, cols)
|
||||
except PasteError as error:
|
||||
@@ -310,4 +312,6 @@ def paste(
|
||||
return {**head, "갈래": "줄", **fixed_lines(parsed, taken)}
|
||||
keep = {k: parsed[k] for k in ("꼴", "시트", "호표", "단산", "검산")}
|
||||
row = hstore.save(folder, f"{SHEET} {datetime.now():%m-%d %H:%M}", keep, who)
|
||||
return {**head, "갈래": "만듦", "키": row["키"], **hb.build_all(folder, folder, row["키"], who)}
|
||||
made = hb.build_all(folder, folder, row["키"], who)
|
||||
forget.finish(folder, row["키"]) # 743-1 — 붙인 글 실무자료는 만든 뒤 지움(엑셀 사본 걷음)
|
||||
return {**head, "갈래": "만듦", "키": row["키"], **made}
|
||||
|
||||
@@ -15,6 +15,7 @@ from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
||||
from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
@@ -118,10 +119,13 @@ def kept(
|
||||
|
||||
|
||||
def left(line: dict[str, Any], why: dict | None) -> dict[str, Any]:
|
||||
"""남은 줄 한 줄 — 엑셀 줄 · 갈래 · 까닭 · 후보."""
|
||||
"""남은 줄 한 줄 — 엑셀 줄 · 갈래 · 까닭 · 후보(기계 = M01 EQ 셋 · 746-1)."""
|
||||
kind = finger.kind_of(line)
|
||||
keep = ("차례", "이름", "규격", "단위", "수량")
|
||||
out = {**{k: line.get(k) for k in keep}, "갈래": kind}
|
||||
out["까닭"] = (why or {}).get("까닭") or WHY.get(kind, "이을 것 없음")
|
||||
more = {k: v for k, v in (why or {}).items() if k not in ("까닭", "후보")}
|
||||
return {**out, "후보": (why or {}).get("후보") or [], **more}
|
||||
cands = (why or {}).get("후보") or []
|
||||
if not cands and kind == "기계":
|
||||
cands = elem.machines(f"{line.get('이름') or ''} {line.get('규격') or ''}")
|
||||
return {**out, "후보": cands, **more}
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
`[{id, 노임|기계|자재: 키}]` = SK 더한줄 인력 · 기계 · 재료(엑셀 수량 그대로) · `{id, 뺌: 까닭}` = 놓음.
|
||||
SK 단위(728-2) = 엑셀 줄 단위 · 바탕 기준단위와 성격 다름 = 환산 계수 = 우리 품 ÷ 엑셀 품(인력 · 장비
|
||||
둘 이상이 같은 비일 때만 · `환산` {계수}) · 못 구함 = 고름 대기(까닭 「단위 성격 다름 · 계수 필요」).
|
||||
고름 대기 = 후보 · 숫자 없음 — SK 안 만듦 · 후보 다섯(사용자가 고른 기초 `pick` = 견줘 SK · 기초).
|
||||
고름 대기 = 후보 · 숫자 없음 — SK 안 만듦 · 후보 다섯(사용자가 고른 기초 `pick` = 견줘 SK · 기초) ·
|
||||
앞 셋 `줄비교`(746-1) = 그 기초로 견준 줄마다 꼴(같음 · 품다름 · 엑셀만 · 기초만 · 1 단위 품) · 못옮김 ·
|
||||
견줌 못함(단위 성격 다름) = null — 판정 · 순위 · 미리 고름은 안 바꿈.
|
||||
못옮김 `후보` = M01 요소 셋(`M02_Hopyo_Elem` · 보여주기만).
|
||||
cid = `kind:키#n`(첫 기초 바탕 줄만 · `M02_Sangeun_Doc.line_ids`) · 더한줄 · 덮개 qty = 바탕 행 몫
|
||||
(1 단위 품 × 바탕 결과단위 앞 수 · 셈이 ÷ 배) · 더한줄 로직 수량 = 몫 × 바탕 배 ÷ 그 기초 배.
|
||||
품 견줌 = 우리 셈(`M02_Sangeun_Calc.run` 줄 수량 ÷ 배) · 같음 = `Option.near`(0.5 % + 엑셀 자릿수 반올림).
|
||||
@@ -27,6 +30,7 @@ from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Combine as combine
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Hopyo_Option as option
|
||||
@@ -40,6 +44,8 @@ KEYS = ("덮개", "자재표시", "더한줄", "뺀줄", "못옮김")
|
||||
TINY = 1e-9
|
||||
_ADD = re.compile(r"^add:#(\d+)$")
|
||||
UNIT_WHY = "단위 성격 다름 · 계수 필요"
|
||||
LOOKS = ("같음", "품다름", "엑셀만", "기초만") # 줄비교 꼴(746-1)
|
||||
COMPARE = 3 # 고름 대기 후보 앞 몇에 줄비교
|
||||
LINK = {
|
||||
"노임": ("인력", "LB"),
|
||||
"기계": ("기계", "EQ"),
|
||||
@@ -96,7 +102,7 @@ def parts(doc: dict, whole: Any) -> list[dict[str, Any]]:
|
||||
if kind == "인력":
|
||||
one.update(갈래="인력", 이름=labor.get(str(item.get("요소")), str(item.get("이름"))))
|
||||
elif kind == "재료":
|
||||
one.update(갈래="자재", 이름=str(item.get("이름") or ""))
|
||||
one.update(갈래="자재", 이름=str(item.get("이름") or ""), 단위=item.get("단위") or "")
|
||||
elif kind == "로직" and found:
|
||||
sub = found.group(1)
|
||||
if ours.is_mach_logic(whole.logic(sub)):
|
||||
@@ -138,6 +144,27 @@ class _Plan:
|
||||
self.lines = parts(doc, whole)
|
||||
self.lost: list[dict[str, Any]] = []
|
||||
self.lb = {name: key for key, name in ours._labor_names().items()}
|
||||
self.rows: list[dict[str, Any]] = [] # 줄비교(746-1)
|
||||
self.prices: dict[str, float] = {} # 엑셀 단가(못옮김 후보 차례)
|
||||
|
||||
def note(self, kind: str, name: str, excel: float, mine: float | None, **more: Any) -> None:
|
||||
"""줄비교 한 줄 — 꼴 = 같음 · 품다름 · 엑셀만 · 기초만(`꼴` 주면 그대로)."""
|
||||
look = more.pop("꼴", None)
|
||||
if look is None:
|
||||
if excel and mine:
|
||||
look = LOOKS[0] if _near(excel, mine) else LOOKS[1]
|
||||
else:
|
||||
look = LOOKS[2] if excel else LOOKS[3]
|
||||
num = lambda x: _num(float(f"{x:.10g}")) if x else None # noqa: E731
|
||||
one = {
|
||||
"갈래": kind,
|
||||
"이름": name,
|
||||
"규격": "",
|
||||
"단위": "",
|
||||
"엑셀": num(excel),
|
||||
"기초": num(mine),
|
||||
}
|
||||
self.rows.append({**one, **more, "꼴": look})
|
||||
|
||||
def cover(self, cid: str, **one: float) -> None:
|
||||
self.doc["덮개"][cid] = {k: _num(v) for k, v in one.items()}
|
||||
@@ -155,7 +182,8 @@ class _Plan:
|
||||
n = sum(1 for x in self.lost if (x["갈래"], x["이름"]) == (kind, name))
|
||||
one = {"id": base if not n else f"{base}#{n}", "갈래": kind, "이름": name, "규격": "",
|
||||
"단위": "", "수량": _num(float(f"{qty:.10g}")), "글": text, "후보": []} # fmt: skip
|
||||
self.lost.append({**one, **more})
|
||||
one = {**one, **more}
|
||||
self.lost.append({**one, "후보": elem.lost(one, self.prices.get(name))})
|
||||
|
||||
def link(self, picks: list[dict[str, Any]]) -> None:
|
||||
"""사용자 잇기 — 이은 줄 = 더한줄(엑셀 수량 × 배) · 뺌 = 놓음 · 나머지 못옮김 그대로."""
|
||||
@@ -191,6 +219,7 @@ class _Plan:
|
||||
for t in sorted(set(excel) | {x["이름"] for x in mine if x["품"]} | set(extra)):
|
||||
a, rows = excel.get(t, 0.0), [x for x in mine if x["이름"] == t]
|
||||
b, f = sum(x["품"] for x in rows), extra.get(t, 0.0)
|
||||
self.note("인력", t, a, b + f, 단위="인")
|
||||
if a and _near(a, b + f):
|
||||
continue
|
||||
want = a - f
|
||||
@@ -224,10 +253,11 @@ class _Plan:
|
||||
and _near(h, x["품"])), None) # fmt: skip
|
||||
if hit is not None:
|
||||
used.add(hit["cid"])
|
||||
self._mnote(m, h, hit["품"])
|
||||
continue
|
||||
at = next((i for i, (a, q) in enumerate(fixed) if a & kinds and _near(h, q)), None)
|
||||
if at is not None:
|
||||
fixed.pop(at)
|
||||
self._mnote(m, h, fixed.pop(at)[1])
|
||||
continue
|
||||
rest.append(m)
|
||||
for m in rest:
|
||||
@@ -236,15 +266,23 @@ class _Plan:
|
||||
same.sort(key=lambda x: not (sp and sp & x["규격"]))
|
||||
if same:
|
||||
used.add(same[0]["cid"])
|
||||
self._mnote(m, m["시간"], same[0]["품"])
|
||||
self.cover(same[0]["cid"], qty=m["시간"] * self.tb)
|
||||
else:
|
||||
self._mnote(m, m["시간"], None)
|
||||
self._add_machine(m, line)
|
||||
for x in mine:
|
||||
if x["cid"] not in used:
|
||||
self._mnote({"갈래": sorted(x["종류"]), "규격": sorted(x["규격"])}, 0.0, x["품"])
|
||||
self.drop(x["cid"])
|
||||
for a, q in fixed:
|
||||
self._mnote({"갈래": sorted(a)}, 0.0, q)
|
||||
self.miss("기계", "·".join(sorted(a)), q, "우리만 장비 — 더한 기초 안", 단위="시간")
|
||||
|
||||
def _mnote(self, m: dict[str, Any], excel: float, mine: float | None) -> None:
|
||||
spec = " ".join(f"{x:g}" for x in sorted(m.get("규격") or []))
|
||||
self.note("장비", "·".join(sorted(m["갈래"])), excel, mine, 규격=spec, 단위="시간")
|
||||
|
||||
def _add_machine(self, m: dict[str, Any], line: dict[str, Any]) -> None:
|
||||
"""엑셀만 장비 → 기계경비 로직 줄(기계 = 그 갈래 · 규격 옵션 거꾸로 찾기)."""
|
||||
fp = finger.empty(line)
|
||||
@@ -267,11 +305,14 @@ class _Plan:
|
||||
for m in excel:
|
||||
words = option.mat_words(m["이름"])
|
||||
rows = [x for x in mine if option._named(words, x["이름"])]
|
||||
unit = next((x["단위"] for x in rows if x.get("단위")), "")
|
||||
if rows and all(x["품"] is None for x in rows): # 우리 = 별도계상 · 엑셀 = 블록 안
|
||||
self.note("자재", m["이름"], m["품"], None, 단위=unit, 꼴=LOOKS[1])
|
||||
self.doc["자재표시"][rows[0]["cid"]] = {"별도계상": "직접계산"}
|
||||
self.cover(rows[0]["cid"], qty=m["품"] * self.tb)
|
||||
continue
|
||||
rows = [x for x in rows if x["품"] is not None]
|
||||
self.note("자재", m["이름"], m["품"], sum(x["품"] for x in rows) or None, 단위=unit)
|
||||
if rows and _near(m["품"], sum(x["품"] for x in rows)):
|
||||
continue
|
||||
if rows:
|
||||
@@ -309,11 +350,29 @@ def _need(code: str, unit: str, whole: Any) -> dict[str, Any] | None:
|
||||
return None if swap["배"] is not None else {"가": got[0], "나": unit, "갈래": swap["계수"]}
|
||||
|
||||
|
||||
def _wait(entry: dict, whole: Any, unit: str, why: str | None = None) -> dict[str, Any]:
|
||||
def compare(
|
||||
entry: dict, block: dict | None, line: dict, whole: Any, code: str, efp: dict | None = None
|
||||
) -> dict | None:
|
||||
"""고른 기초 `code` 로 견준 줄비교 `{줄, 수, 못옮김, 환산?}`(만들지 않음) · 견줌 못함 = None."""
|
||||
got = plan(entry, block, line, whole, {"로직": code}, efp)
|
||||
if got["갈래"] == WAIT:
|
||||
return None
|
||||
rows = got["줄비교"]
|
||||
out = {"줄": rows, "수": {k: sum(x["꼴"] == k for x in rows) for k in LOOKS}}
|
||||
out["못옮김"] = got["못옮김"]
|
||||
return out | ({"환산": got["환산"]} if got.get("환산") else {})
|
||||
|
||||
|
||||
def _wait(
|
||||
entry: dict, whole: Any, unit: str, why: str | None = None, at: tuple | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""고름 대기 — `at` = (맞춤셈, 블록, 줄 · 사용자 고름 없을 때) 이면 앞 셋에 줄비교."""
|
||||
keep = ("로직", "이름", "단위", "판정", "글")
|
||||
cands = [{k: c.get(k) for k in keep} for c in (entry.get("후보") or [])[: fit.TOP_OPT]]
|
||||
for c in cands:
|
||||
for i, c in enumerate(cands):
|
||||
c["계수"] = _need(str(c.get("로직") or ""), unit, whole)
|
||||
if at and i < COMPARE and c.get("로직"):
|
||||
c["줄비교"] = compare(*at[:3], whole, str(c["로직"]), *at[3:])
|
||||
if why is None:
|
||||
why = "맞는 기초 없음 — " + ((entry.get("판정") or {}).get("글") or "후보 없음")
|
||||
return {"갈래": WAIT, "까닭": why, "후보": cands}
|
||||
@@ -346,13 +405,14 @@ def plan(
|
||||
line: dict[str, Any],
|
||||
whole: Any,
|
||||
pick: dict | None = None,
|
||||
efp: dict | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""블록 한 칸 → `{갈래 기초|SK|고름 대기, 바탕, 입력값, 환산?, 덮개, 자재표시, 더한줄, 뺀줄, 못옮김,
|
||||
놓음, 엑셀품, 후보?}` · `pick` = 사용자 `{로직?, 입력값?, 환산?, 잇기?}` — 로직 = 고른 기초(옵션 없으면
|
||||
거꾸로 찾음 · 늘 견줌) · 환산 = SK `환산` 꼴 · 잇기 = 못옮김 잇기(`_Plan.link` · 틀림 `LinkError`)."""
|
||||
entry, pick = entry or {}, pick or {}
|
||||
state = (entry.get("판정") or {}).get("상태")
|
||||
efp = finger.excel(block, line)
|
||||
efp = efp or finger.excel(block, line) # 줄비교 = 한 번 푼 지문
|
||||
unit = str(line.get("단위") or "").strip() or "식"
|
||||
if pick.get("로직"):
|
||||
code = str(pick["로직"])
|
||||
@@ -363,7 +423,7 @@ def plan(
|
||||
code = entry["차례"][0]
|
||||
given = ((entry.get("옵션") or {}).get(code) or {}).get("입력") or {}
|
||||
else:
|
||||
return _wait(entry, whole, unit)
|
||||
return _wait(entry, whole, unit, at=(entry, block, line, efp))
|
||||
doc = {"키": "SK-000000", "이름": "견줌", "단위": unit, "바탕": {"로직": code},
|
||||
"입력값": dict(given), "덮개": {}, "뺀줄": [], "자재표시": {}} # fmt: skip
|
||||
if pick.get("환산"):
|
||||
@@ -386,7 +446,8 @@ def plan(
|
||||
if code not in [c.get("로직") for c in cands]:
|
||||
cands = [{"로직": code, "이름": row.get("이름")}, *cands]
|
||||
why = f"{UNIT_WHY} — {str(e).split(' · ', 1)[-1]}"
|
||||
return _wait({"후보": cands}, whole, unit, why)
|
||||
at = None if pick else (entry, block, line, efp)
|
||||
return _wait({"후보": cands}, whole, unit, why, at)
|
||||
doc["환산"] = {"계수": coef}
|
||||
tb = float(calc.ratio_of(doc, row, whole))
|
||||
doc["더한줄"] = extras(tb)
|
||||
@@ -396,11 +457,12 @@ def plan(
|
||||
base = {**dict.fromkeys(KEYS[:2], {}), **dict.fromkeys(KEYS[2:], []), "놓음": []}
|
||||
return {**head, "갈래": BASE, **base}
|
||||
work = _Plan(doc, whole)
|
||||
work.prices = elem.prices(block)
|
||||
work.labor({t: x["품"] for t, x in efp["인력"].items() if x.get("품")})
|
||||
work.machines([m for m in efp["장비"] if m.get("시간")], line)
|
||||
work.materials([m for m in efp["자재"] if m.get("품")])
|
||||
work.link(pick.get("잇기") or [])
|
||||
out = {k: doc[k] for k in KEYS[:4]}
|
||||
changed = any(out.values()) or bool(work.lost)
|
||||
kept = {"못옮김": work.lost, "놓음": doc.get("놓음") or []}
|
||||
kept = {"못옮김": work.lost, "놓음": doc.get("놓음") or [], "줄비교": work.rows}
|
||||
return {**head, **out, "갈래": SK if changed else BASE, **kept}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
파일 = 읽은 결과만(원본 엑셀 안 둠) `{키, 파일, 올린때, 올린이, 시트, 호표, 검산}` · 키 `HP-######`
|
||||
(층마다 다음 번호) · 맞춤 기억 = `{줄: {줄 열쇠: {로직?|자재?|빼기?, 입력값, 환산, 확정}}}`.
|
||||
층 종류(`layers.KINDS`) 밖 폴더 — 양식 목록 · 복사 · 프로젝트 씨앗에 안 섞임 · 권한은 길 몫.
|
||||
한시 불러오기(718-1) — 파일에 `지문`(올린 엑셀 sha256) · 목록 = 지문 있는 것(작업 중)만 · 옛 HP 는
|
||||
파일 그대로 목록에서만 숨김 · 끝낸 기록 `실무자료/불러옴.json`
|
||||
`{번, 지문 {지문: {파일, 키, 끝낸때, 만든}}}` · 번 = 낸 키 가장 큰 번(지운 뒤에도 키를 다시 안 씀).
|
||||
한시 불러오기(718-1 · 743-1) — 파일에 `지문`(올린 엑셀 sha256) · 목록 = 지문 있고 하루 안 지난 것
|
||||
(작업 중)만 · 옛 HP · 하루 지난 HP 는 목록에서 숨김(지움 = `M02_Hopyo_Forget`) ·
|
||||
`실무자료/불러옴.json` `{번}` = 낸 키 가장 큰 번(지운 뒤에도 키를 다시 안 씀 · 옛 `지문` 안 읽음).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -15,6 +15,7 @@ import hashlib
|
||||
import json
|
||||
import re
|
||||
import threading
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -27,6 +28,7 @@ MEMORY = "맞춤"
|
||||
GRID = "격자"
|
||||
DONE = "불러옴"
|
||||
NO_GRID = "격자 없음 — 다시 올리면 보임"
|
||||
EXPIRE = timedelta(days=1) # 작업 중 HP 보관 — 올린 뒤 하루
|
||||
_KEY = re.compile(r"^HP-(\d{6})$")
|
||||
# ponytail: 프로세스 안 잠금 — 서버를 여럿 띄우면 파일 잠금으로
|
||||
_LOCK = threading.Lock()
|
||||
@@ -68,18 +70,27 @@ def fingerprint(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def stale(doc: dict[str, Any], now: datetime | None = None) -> bool:
|
||||
"""하루 지난 HP — 올린때 기준 · 못 읽는 때 = 지난 것."""
|
||||
try:
|
||||
when = datetime.fromisoformat(str(doc.get("올린때") or "").replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return True
|
||||
return (now or datetime.now(timezone.utc)) - when > EXPIRE
|
||||
|
||||
|
||||
def rows(folder: str | Path) -> list[dict[str, Any]]:
|
||||
"""층 실무자료 목록(키 차례) — 작업 중(지문 있음)만 · 옛 HP 는 숨김."""
|
||||
"""층 실무자료 목록(키 차례) — 작업 중(지문 있음 · 하루 안)만 · 옛 HP · 지난 HP 는 숨김."""
|
||||
out = []
|
||||
for path in sorted(_dir(folder).glob("HP-*.json")):
|
||||
doc = _load(path)
|
||||
if isinstance(doc, dict) and _KEY.match(path.stem) and doc.get("지문"):
|
||||
if isinstance(doc, dict) and _KEY.match(path.stem) and doc.get("지문") and not stale(doc):
|
||||
out.append({**brief(doc), "지문": doc["지문"]})
|
||||
return out
|
||||
|
||||
|
||||
def done(folder: str | Path) -> dict[str, Any]:
|
||||
"""끝낸 기록 `{번, 지문 {지문: {파일, 키, 끝낸때, 만든}}}`."""
|
||||
"""키 번 기록 `{번, 지문}`(지문 = 743 전 끝낸 기록 · 안 읽음 · 정리 `Forget.tidy`)."""
|
||||
doc = _load(_dir(folder) / f"{DONE}.json")
|
||||
doc = doc if isinstance(doc, dict) else {}
|
||||
marks = doc.get("지문")
|
||||
@@ -137,22 +148,12 @@ def grid(folder: str | Path, key: str) -> dict[str, Any]:
|
||||
return doc
|
||||
|
||||
|
||||
def delete(folder: str | Path, key: str, made: list[str] | None = None) -> None:
|
||||
"""HP · 격자 지움 · 만든 것(`made` SC 키)이 있으면 지문을 끝낸 기록에 남김(다시 올림 막음)."""
|
||||
def delete(folder: str | Path, key: str) -> None:
|
||||
"""HP · 격자 지움(끝낸 기록 없음 · 같은 엑셀 다시 올림 됨 · 743-1)."""
|
||||
path = _path(folder, key)
|
||||
with _LOCK:
|
||||
doc = _load(path)
|
||||
if not isinstance(doc, dict):
|
||||
if not isinstance(_load(path), dict):
|
||||
raise store.StoreError(404, f"없는 실무자료 「{key}」")
|
||||
if made and doc.get("지문"):
|
||||
log = done(folder)
|
||||
log["지문"][doc["지문"]] = {
|
||||
"파일": doc.get("파일"),
|
||||
"키": key,
|
||||
"끝낸때": utc_now_text(),
|
||||
"만든": made,
|
||||
}
|
||||
_put_done(folder, log)
|
||||
path.unlink()
|
||||
_grid_path(folder, key).unlink(missing_ok=True)
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ from typing import Any
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
|
||||
FIELD = "패치"
|
||||
KEEP = ("덮개", "뺀줄", "더한줄")
|
||||
CALC = ("덮개", "뺀줄", "더한줄") # 셈에 드는 칸(`as_doc` · 화면 짝 꼴)
|
||||
KEEP = (*CALC, "뺀더한줄") # 뺀더한줄 = [되살림] 기억(744-1)
|
||||
TEMP = "SK-000000" # 이름 없는 줄 단가산출 — 저장 안 함
|
||||
|
||||
|
||||
@@ -41,7 +42,9 @@ def clean(one: dict, where: str, whole: Any, row: dict | None, opened: list | No
|
||||
)
|
||||
except sd.DocError as e:
|
||||
raise _bad(f"{where} · {e}") from e
|
||||
out = {k: got[k] for k in KEEP if got[k]}
|
||||
out = {k: got[k] for k in KEEP if got.get(k)}
|
||||
for add in out.get("더한줄") or []: # 처음수량(744-1) = 넣을 때 수량 · 없으면 지금 수량
|
||||
add.setdefault("처음수량", add["수량"])
|
||||
return {FIELD: out} if out else {}
|
||||
|
||||
|
||||
@@ -71,7 +74,7 @@ def _keyed(value: Any, ids: list[str]) -> dict:
|
||||
|
||||
def as_doc(one: dict, whole: Any, name: str = "줄", unit: str = "식") -> dict[str, Any]:
|
||||
"""일위대가 줄 → SK 문서(바탕 = 그 로직 · 그 SK) — 고름 · 자재표시는 cid 로."""
|
||||
body = {k: (one.get(FIELD) or {}).get(k) or ([] if k != "덮개" else {}) for k in KEEP}
|
||||
body = {k: (one.get(FIELD) or {}).get(k) or ([] if k != "덮개" else {}) for k in CALC}
|
||||
head = {"키": TEMP, "이름": name, "단위": unit, **body}
|
||||
if "참조" in one:
|
||||
return {**head, "바탕": {"참조": one["참조"]}}
|
||||
|
||||
@@ -42,6 +42,7 @@ _OWNER = {"company": re.compile(r"^\d+$"), "personal": re.compile(r"^\d+/\d+$")}
|
||||
|
||||
Ref = dict[str, Any]
|
||||
Here = tuple[str, str | None]
|
||||
COPY = "사본" # 740-1 하위 호표 사본 블록 — 참조 꼴 {층: 사본, 주인: 몸 지문, 키: 본 SC, 몸}
|
||||
|
||||
|
||||
def _bad(text: str, status: int = 400) -> store.StoreError:
|
||||
@@ -103,7 +104,28 @@ def ref_path(ref: Ref) -> Path:
|
||||
return folder_of(ref["층"], ref.get("주인")) / kind_of(ref["키"]) / f"{ref['키']}.json"
|
||||
|
||||
|
||||
def is_copy(ref: Any) -> bool:
|
||||
"""사본 블록(740-1) — 일위대가 안 일위대가 몸을 줄 안에 담음(원본 연결 없음 · 키 = 출처)."""
|
||||
return isinstance(ref, dict) and ref.get("층") == COPY and isinstance(ref.get("몸"), dict)
|
||||
|
||||
|
||||
def copy_ref(body: dict[str, Any], key: str) -> Ref:
|
||||
"""사본 블록 참조 — 몸(종류 · 이름 · 단위 · 담은로직 · 자재 · 본뜬) · 주인 = 몸 지문."""
|
||||
import hashlib
|
||||
|
||||
keep = {k: body[k] for k in ("종류", "이름", "단위", "담은로직", "자재", "본뜬") if body.get(k)}
|
||||
raw = json.dumps(keep, ensure_ascii=False, sort_keys=True, default=str)
|
||||
return {
|
||||
"층": COPY,
|
||||
"주인": hashlib.sha256(raw.encode()).hexdigest()[:12],
|
||||
"키": key,
|
||||
"몸": keep,
|
||||
}
|
||||
|
||||
|
||||
def read_ref(ref: Ref) -> dict[str, Any] | None:
|
||||
if is_copy(ref):
|
||||
return ref["몸"]
|
||||
try:
|
||||
got = json.loads(ref_path(ref).read_bytes())
|
||||
except (OSError, ValueError):
|
||||
@@ -114,6 +136,8 @@ def read_ref(ref: Ref) -> dict[str, Any] | None:
|
||||
def readable(at: Here, ref: Ref) -> bool:
|
||||
"""`at` 층이 `ref` 를 가리켜도 되나 — 위 층만(프로젝트는 아무것도 · 굳힌 사본만 씀)."""
|
||||
layer, owner = at
|
||||
if ref.get("층") == COPY: # 사본 블록 = 줄 안 몸(어느 층도 읽음)
|
||||
return True
|
||||
if layer == "project":
|
||||
return False
|
||||
if ref["층"] == "system":
|
||||
@@ -127,6 +151,8 @@ def check_ref(raw: Any, at: str, here_: Here) -> Ref:
|
||||
"""참조 꼴 · 위 층만 · 있는 문서 — 아니면 400."""
|
||||
if not isinstance(raw, dict):
|
||||
raise _bad(f"{at} · 참조는 {{층, 주인, 키}}")
|
||||
if raw.get("층") == COPY:
|
||||
return _check_copy(raw, at)
|
||||
layer, owner, key = raw.get("층"), raw.get("주인"), str(raw.get("키") or "")
|
||||
if layer == "system":
|
||||
owner = None
|
||||
@@ -142,6 +168,20 @@ def check_ref(raw: Any, at: str, here_: Here) -> Ref:
|
||||
return ref
|
||||
|
||||
|
||||
def _check_copy(raw: Ref, at: str) -> Ref:
|
||||
"""사본 블록 검사 — 키 = SC · 몸 줄 · 자재 = 일위대가 검사 그대로 · 지문 다시 셈."""
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
body, key = raw.get("몸"), str(raw.get("키") or "")
|
||||
if not isinstance(body, dict) or not _KEY.match(key) or kind_of(key) != "precombo":
|
||||
raise _bad(f"{at} · 사본 블록은 {{층: 사본, 키: SC-…, 몸}}")
|
||||
try:
|
||||
labor.clean({"담은로직": body.get("담은로직") or [], "자재": body.get("자재") or []})
|
||||
except store.StoreError as e:
|
||||
raise _bad(f"{at} · 사본 블록 · {e.detail}") from e
|
||||
return copy_ref(body, key)
|
||||
|
||||
|
||||
# ── 서고 문서 정리 ─────────────────────────────────────────────────────
|
||||
def check_key(kind: str, name: str) -> None:
|
||||
found = _KEY.match(name)
|
||||
@@ -158,7 +198,7 @@ def clean_doc(kind: str, doc: Any) -> dict[str, Any]:
|
||||
if here()[0] != "project": # 740-1 — M02 참조 줄 = M01 키 줄로 펼침(못 펼친 줄은 그대로)
|
||||
from M02_MasterTemplete import M02_Library_Flat as flat
|
||||
|
||||
doc = flat.flat_doc(kind, doc)
|
||||
doc = flat.flat_doc(kind, doc, me={"층": here()[0], "주인": here()[1], "키": key})
|
||||
name, unit = str(doc.get("이름") or "").strip(), str(doc.get("단위") or "").strip()
|
||||
if kind == "sangeun":
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
일위대가 실무자료(324 · `M02_Hopyo_*` · 올리기 · 지우기 = 층 쓰기 · 목록 · 읽기 = 층 읽기) ·
|
||||
338 = 확인한 줄만 만들기(몸 없음 400 · 옛 즉시 확정 길 없앰) · 별칭 장부 보기 · 고치기 ·
|
||||
올린 뒤 맞춤 뒤 셈(`M02_Hopyo_Fit.ensure`) · 시동 때 우리 지문 데움(`M02_Hopyo_Ours.warm`) ·
|
||||
718-1 한시 불러오기 = 올림 지문(끝낸 같은 엑셀 409 · 작업 중 같은 엑셀 = 그 파일 `이어서`) ·
|
||||
지우기 = [끝내기](HP · 격자 · 맞춤셈 캐시 지움 · 만든 일위대가가 있으면 지문 기록 · 별칭 장부 남음).
|
||||
718-1 한시 불러오기 = 올림 지문(작업 중 같은 엑셀 = 그 파일 `이어서` · 끝낸 것 = 새로 · 743-1) ·
|
||||
지우기 = [끝내기](`M02_Hopyo_Forget.finish` · 올림 때 하루 지난 HP 도 끝냄).
|
||||
층 권한 = `Router_Items._root`(`M02_Layer_Access` 한 벌).
|
||||
⚠ 등록은 `main.py`(로그인만).
|
||||
"""
|
||||
@@ -27,6 +27,7 @@ from pydantic import BaseModel, ConfigDict, Field
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build
|
||||
from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel
|
||||
from M02_MasterTemplete import M02_Hopyo_Forget as hopyo_forget
|
||||
from M02_MasterTemplete import M02_Hopyo_Grid as hopyo_grid
|
||||
from M02_MasterTemplete import M02_Hopyo_Item as hopyo_item
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as hopyo_ledger
|
||||
@@ -229,14 +230,7 @@ def _who(session: Session) -> dict[str, Any]:
|
||||
|
||||
def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]:
|
||||
mark = hopyo_store.fingerprint(data)
|
||||
if old := hopyo_store.done(store.root())["지문"].get(mark):
|
||||
made = len(old.get("만든") or [])
|
||||
when = str(old.get("끝낸때") or "")[:10]
|
||||
raise store.StoreError(
|
||||
409,
|
||||
f"이미 불러온 엑셀 「{old.get('파일')}」({when} · 만든 일위대가 · 단가산출 {made})"
|
||||
" — 다시 만들지 않음",
|
||||
)
|
||||
hopyo_forget.sweep(store.root()) # 하루 지난 작업 중 HP 끝냄(743-1)
|
||||
if same := next((r for r in hopyo_store.rows(store.root()) if r.get("지문") == mark), None):
|
||||
return {**same, "이어서": True}
|
||||
try:
|
||||
@@ -502,29 +496,10 @@ def hopyo_ledger_write(
|
||||
def hopyo_delete(
|
||||
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""실무자료 지우기 = 불러오기 [끝내기](718-1) — HP · 격자 · 맞춤셈 캐시 지움 · 만든 일위대가
|
||||
(이 층 확인된 본뜬 「키 제N호표」)는 그대로 · 있으면 지문 기록(같은 엑셀 다시 올림 막음) ·
|
||||
별칭 장부 그대로 → `{지움, 만든}`."""
|
||||
"""실무자료 지우기 = [끝내기](718-1 · 743-1 · 화면 떠남도) — HP · 격자 · 맞춤셈 캐시 지움 ·
|
||||
만든 일위대가 · 단가산출은 엑셀 사본만 걷고 남음 · 별칭 장부 그대로 → `{지움, 만든}`."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, _finish, folder, key)
|
||||
|
||||
|
||||
def _finish(folder: Path, key: str) -> dict[str, Any]:
|
||||
head = f"{key} 제"
|
||||
made = sorted(
|
||||
{sc for mark, sc in hopyo_build.confirmed(folder).items() if mark.startswith(head)}
|
||||
)
|
||||
made += sorted(hopyo_sangeun.made(folder, key).values()) # 단가산출(SK)
|
||||
if (cache_path := _later("M02_Hopyo_Fit", "cache_path")) is not None:
|
||||
try:
|
||||
got = cache_path(folder, key)
|
||||
except (OSError, store.StoreError):
|
||||
got = None
|
||||
# ponytail: 뒤 셈이 도는 중이면 끝낸 뒤 캐시를 다시 쓸 수 있음 — tmp 캐시라 둠
|
||||
for old in got.parent.glob(f"{'_'.join(got.name.split('_')[:2])}_*.json") if got else []:
|
||||
old.unlink(missing_ok=True)
|
||||
hopyo_store.delete(folder, key, made)
|
||||
return {"지움": key, "만든": len(made)}
|
||||
return _call(folder, hopyo_forget.finish, folder, key)
|
||||
|
||||
|
||||
class SangeunBody(_Strict):
|
||||
|
||||
@@ -10,15 +10,17 @@
|
||||
수량 = 넣은 수 × 줄 수량(저장 `수량.손` › 실무수량 › 1 · 742-1) ÷ 배(물량 알약 배)
|
||||
SC 에 SK 로직 줄 하나 + 패치(= SK 몸) · 별칭 = SK 이름 · 같은 자리(안 줄 알약 키 그대로) ·
|
||||
바탕 없는 SK · 단위 환산 SK = 못 넣음(값 단위가 달라짐)
|
||||
SC 에 SC 찾아 넣기 길만 — 그 자리에 안 줄 · 자재를 펴 넣음 · 줄 수량 · 본뜬 실무수량 =
|
||||
바깥 × 안 · 같은 기초 · 고정 · 자재가 겹치면 못 넣음(일위대가 줄 짝 = 로직 키) ·
|
||||
저장 · 읽기 · 옮기기는 하위 호표 참조 그대로(엑셀 하위 호표 끝수 · B09 하위 대가)
|
||||
SC 에 SC 사본 블록 — 안쪽 SC 몸 통째(M01 키 줄 + 덮개 · 실무수량)를 그 줄 참조 꼴
|
||||
`{층: 사본, 주인: 몸 지문, 키: 본 SC, 몸}` 에 담음(`M02_Labor_Ref.copy_ref` · 원본
|
||||
연결 없음 · 하위 호표 끝수 단계 · B09 하위 대가 블록 그대로 · 같은 기초 겹침도
|
||||
블록 안)
|
||||
바탕 SK 참조 · 패치 있는 SK 참조 줄 = 위 덮개를 아래 덮개에 겹쳐 한 벌(`compose`)
|
||||
못 펼친 줄 = 저장 · 읽기 · 옮기기에선 옛 참조 그대로(값 그대로) · 넣기 길(`strict`)에선 400 까닭.
|
||||
초기화(`reset`) = 덮개 · 뺀줄 · 더한줄 단가 걷음(안쪽 넣은 몸까지) · 항목 · 수량 · 입력값 · 고름 ·
|
||||
자재표시 · 켜쌓기율 · 고정 줄 · 기본정보 남김 → 값 = 연결된 M01 키 값.
|
||||
초기화(`reset`) = 덮개 · 뺀줄 · 더한줄 단가 걷음(안쪽 넣은 몸 · 사본 블록까지) · 이음 기계 고정 줄
|
||||
단가 = M01 기계 한 시간 값 다시 받음(736-3) · 항목 · 수량 · 입력값 · 고름 · 자재표시 · 켜쌓기율 ·
|
||||
기본정보 남김 → 값 = 연결된 M01 키 값.
|
||||
옮기기(`migrate`) = 층 문서 펼쳐 다시 씀 — 전후 1 단위 금액 같을 때만(안 줄 수량 모두 1 로 대조) ·
|
||||
쓰는 구조물 조합이 있는 SC 펼쳐 넣기(차례 바뀜)는 안 옮김 · 공유 storage 는 브레인이 돌림.
|
||||
공유 storage 는 브레인이 돌림.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -193,6 +195,9 @@ def sc_adds(z: dict, q: Decimal, whole: Any, tag: dict) -> list[dict]:
|
||||
"이름": one["고정"].get("이름") or "",
|
||||
"수량": sd.plain(n),
|
||||
}
|
||||
elif refs.is_copy(one.get("참조")): # 하위 호표 사본 블록 = 그 줄들도 더한줄로
|
||||
out += sc_adds(one["참조"]["몸"], n, whole, tag)
|
||||
continue
|
||||
else:
|
||||
raise sd.DocError("비율 고정 줄 · 참조 줄은 단가산출에 못 담음")
|
||||
out.append({**entry, "본뜬": tag})
|
||||
@@ -346,19 +351,13 @@ def _twice(doc: dict) -> str | None:
|
||||
|
||||
|
||||
def flat_sc(doc: dict, read: Read, whole: Any, trail: tuple = (), strict: bool = True) -> dict:
|
||||
"""SC 문서 → M01 키만 — SK 참조 = 로직 줄 · SC 참조 = 안 줄 펴 넣음(`strict` 만 · 넣기 길) ·
|
||||
`strict` False(저장 · 읽기 · 옮기기) = SC 참조 그대로(하위 호표 · 엑셀 끝수 단계) · 못 펼친
|
||||
줄 그대로."""
|
||||
hands = (doc.get("본뜬") or {}).get("실무수량")
|
||||
old_q = dict((hands or {}).get("줄") or {})
|
||||
new_q, mat_q = {}, dict((hands or {}).get("자재") or {})
|
||||
lines, mats = [], list(doc.get("자재") or [])
|
||||
"""SC 문서 → M01 키만 — SK 참조 = 로직 줄 + 패치(같은 자리) · SC 참조 = 사본 블록(같은 자리) ·
|
||||
`strict` False = 못 펼친 줄 그대로(겹침 · 바탕 없음 · 환산 · 없는 참조)."""
|
||||
lines = []
|
||||
for at, one in enumerate(doc.get("담은로직") or []):
|
||||
ref = one.get("참조") if isinstance(one, dict) else None
|
||||
kind = refs.kind_of(ref.get("키")) if isinstance(ref, dict) else None
|
||||
here_q = old_q.get(str(at))
|
||||
if kind != "sangeun" and not (strict and kind == "precombo"): # 하위 호표 = 참조 그대로
|
||||
new_q |= {str(len(lines)): here_q} if here_q is not None else {}
|
||||
if kind not in ("sangeun", "precombo") or refs.is_copy(ref):
|
||||
lines.append(one)
|
||||
continue
|
||||
try:
|
||||
@@ -366,80 +365,39 @@ def flat_sc(doc: dict, read: Read, whole: Any, trail: tuple = (), strict: bool =
|
||||
tag = mark(ref, src)
|
||||
if kind == "sangeun":
|
||||
y = flat_sk(sd.clean(src, None, whole), read, whole, path)
|
||||
put, put_m = [sk_line(y, one, whole, tag)], []
|
||||
put_q = {"0": here_q} if here_q is not None else {}
|
||||
put = sk_line(y, one, whole, tag)
|
||||
else:
|
||||
z = flat_sc(src, read, whole, path)
|
||||
put, put_q, put_m = _spread(z, one, here_q, tag)
|
||||
body = flat_sc(src, read, whole, path)
|
||||
put = {**one, "참조": refs.copy_ref(body, ref["키"]), "본뜬": tag}
|
||||
rest = (doc.get("담은로직") or [])[at + 1 :]
|
||||
trial = {"담은로직": [*lines, *put, *rest], "자재": [*mats, *put_m]}
|
||||
trial = {"담은로직": [*lines, put, *rest], "자재": doc.get("자재") or []}
|
||||
if twice := _twice(trial):
|
||||
raise sd.DocError(f"같은 줄이 겹침 「{twice}」 — 펼쳐 넣지 못함")
|
||||
except FAIL as e:
|
||||
if strict:
|
||||
raise sd.DocError(f"「{ref.get('키')}」 · {e}") from e
|
||||
new_q |= {str(len(lines)): here_q} if here_q is not None else {}
|
||||
lines.append(one)
|
||||
continue
|
||||
new_q |= {str(len(lines) + int(n)): v for n, v in put_q.items()}
|
||||
mat_q |= {m["코드"]: m.pop("_q") for m in put_m if "_q" in m}
|
||||
lines, mats = [*lines, *put], [*mats, *put_m]
|
||||
out = {**doc, "담은로직": lines, "자재": mats}
|
||||
if hands is not None:
|
||||
out["본뜬"] = {**doc["본뜬"], "실무수량": {**hands, "줄": new_q, "자재": mat_q}}
|
||||
return out
|
||||
|
||||
|
||||
def _spread(z: dict, one: dict, here_q: Any, tag: dict) -> tuple[list, dict, list]:
|
||||
"""안 SC 줄 · 자재 펴기 — 실무수량 = 바깥 × 안(둘 다 있을 때만) · 비율 고정 줄 있으면 못 폄
|
||||
(밑 = 그 SC 합 → 바깥 합으로 바뀜)."""
|
||||
if any(refs.is_rate(x) for x in z.get("담은로직") or []):
|
||||
raise sd.DocError("비율 고정 줄이 있는 일위대가는 펴 넣지 못함(밑 합이 바뀜)")
|
||||
hands = (z.get("본뜬") or {}).get("실무수량") or {}
|
||||
inner, inner_m = hands.get("줄") or {}, hands.get("자재") or {}
|
||||
outer = (one.get("수량") or {}).get("손") if isinstance(one.get("수량"), dict) else None
|
||||
|
||||
def times(
|
||||
x: dict,
|
||||
) -> Any: # 줄 수량(742-1) = 바깥 × 안 · 둘 중 하나라도 없으면 쓰는 쪽 몫(null)
|
||||
inner = (x.get("수량") or {}).get("손") if isinstance(x.get("수량"), dict) else None
|
||||
both = outer not in EMPTY and inner not in EMPTY
|
||||
return {"손": sd.plain(_d(outer) * _d(inner))} if both else None
|
||||
|
||||
lines = [{**x, "수량": times(x), "본뜬": tag} for x in z.get("담은로직") or []]
|
||||
if len(lines) == 1 and one.get("별칭"):
|
||||
lines[0]["별칭"] = one["별칭"]
|
||||
both = here_q is not None
|
||||
qty = {
|
||||
str(n): sd.plain(_d(here_q) * _d(inner[str(n)]))
|
||||
for n in range(len(lines))
|
||||
if both and inner.get(str(n)) is not None
|
||||
}
|
||||
mats = []
|
||||
for m in z.get("자재") or []:
|
||||
extra = (
|
||||
{"_q": sd.plain(_d(here_q) * _d(inner_m[m["코드"]]))}
|
||||
if both and m.get("코드") in inner_m
|
||||
else {}
|
||||
)
|
||||
mats.append({**m, "본뜬": tag, **extra})
|
||||
return lines, qty, mats
|
||||
put = one
|
||||
lines.append(put)
|
||||
return {**doc, "담은로직": lines}
|
||||
|
||||
|
||||
# ── 문 ────────────────────────────────────────────────────────────────
|
||||
def flat_doc(kind: str, doc: Any, read: Read = refs.read_ref, strict: bool = False) -> Any:
|
||||
"""층 문서 하나 펼침(저장 · 읽기 · 복사 · 옮기기) — 참조 없으면 받은 그대로."""
|
||||
def flat_doc(
|
||||
kind: str, doc: Any, read: Read = refs.read_ref, strict: bool = False, me: Any = None
|
||||
) -> Any:
|
||||
"""층 문서 하나 펼침(저장 · 읽기 · 복사 · 옮기기) — 참조 없으면 받은 그대로 · `me` = 이 문서
|
||||
참조(자기 자신을 담은 줄 = 안 펼침 → 저장 검사가 422)."""
|
||||
if not isinstance(doc, dict) or kind not in refs.PREFIX or not refs.refs_in(doc):
|
||||
return doc
|
||||
whole = _whole()
|
||||
whole, trail = _whole(), (refs.ref_id(me),) if isinstance(me, dict) else ()
|
||||
if kind == "sangeun":
|
||||
return flat_sk(doc, read, whole, (), strict)
|
||||
return flat_sc(doc, read, whole, (), strict)
|
||||
return flat_sk(doc, read, whole, trail, strict)
|
||||
return flat_sc(doc, read, whole, trail, strict)
|
||||
|
||||
|
||||
def flat_lines(kind: str, ref: Any, qty: Any = 1, doc: Any = None) -> dict[str, Any]:
|
||||
"""찾아 넣기(`-flat` 길) — `kind` 문서에 `ref` 를 넣을 줄 · 못 펼침 = StoreError 400 ·
|
||||
SC = 지금 화면 문서 `doc` 과 겹치면 400."""
|
||||
SC = 로직 줄(SK) · 사본 블록 줄(SC) 하나 · 지금 화면 문서 `doc` 과 겹치면 400."""
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
at = refs.here()
|
||||
@@ -450,15 +408,27 @@ def flat_lines(kind: str, ref: Any, qty: Any = 1, doc: Any = None) -> dict[str,
|
||||
return {"더한줄": into_sk(ref, qty if qty not in EMPTY else 1, refs.read_ref, whole)}
|
||||
got = flat_sc({"담은로직": [{"참조": ref, "수량": None}]}, refs.read_ref, whole)
|
||||
host = doc if isinstance(doc, dict) else {}
|
||||
trial = {k: [*(host.get(k) or []), *got[k]] for k in ("담은로직", "자재")}
|
||||
if twice := _twice(trial):
|
||||
trial = {"담은로직": [*(host.get("담은로직") or []), *got["담은로직"]]}
|
||||
if twice := _twice({**trial, "자재": host.get("자재") or []}):
|
||||
raise sd.DocError(f"이미 담은 줄과 겹침 「{twice}」")
|
||||
return {"담은로직": got["담은로직"], "자재": got["자재"]}
|
||||
return {"담은로직": got["담은로직"], "자재": []}
|
||||
except FAIL as e:
|
||||
raise store.StoreError(getattr(e, "status", 400), str(e)) from e
|
||||
|
||||
|
||||
# ── 초기화 ────────────────────────────────────────────────────────────
|
||||
def _reprice(fx: dict) -> tuple[dict, int]:
|
||||
"""이음 기계 고정 줄(736-3) 단가 = M01 기계 한 시간 값 다시 받음 — (줄, 바뀐 수)."""
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as machine
|
||||
|
||||
key = (fx.get("이음") or {}).get("기계")
|
||||
got = machine.hourly(str(key)) if key else {}
|
||||
if not got.get("ok"):
|
||||
return fx, 0
|
||||
price = {k: got[k] for k in sd.PARTS.values()}
|
||||
return {**fx, "단가": price}, int(price != fx.get("단가"))
|
||||
|
||||
|
||||
def _reset_adds(adds: list) -> tuple[list, int]:
|
||||
out, n = [], 0
|
||||
for one in adds or []:
|
||||
@@ -474,31 +444,50 @@ def _reset_adds(adds: list) -> tuple[list, int]:
|
||||
if one.get("더한줄"):
|
||||
one["더한줄"], k = _reset_adds(one["더한줄"])
|
||||
n += k
|
||||
if isinstance(one.get("고정"), dict):
|
||||
one["고정"], k = _reprice(one["고정"])
|
||||
n += k
|
||||
out.append(one)
|
||||
return out, n
|
||||
|
||||
|
||||
def reset(kind: str, doc: dict) -> tuple[dict, int]:
|
||||
"""[초기화] — (걷은 문서, 걷힌 고침 수) · 먼저 펼침(옛 참조 줄도 M01 키로)."""
|
||||
def _reset_sc(doc: dict) -> tuple[dict, int]:
|
||||
"""일위대가 초기화 — 줄 패치 · 사본 블록 몸 · 이음 기계 고정 줄."""
|
||||
from M02_MasterTemplete import M02_Labor_Patch as patch
|
||||
|
||||
lines, n = [], 0
|
||||
for one in doc.get("담은로직") or []:
|
||||
if not isinstance(one, dict):
|
||||
lines.append(one)
|
||||
continue
|
||||
ref, body = one.get("참조"), one.get(patch.FIELD)
|
||||
if refs.is_copy(ref):
|
||||
inner, k = _reset_sc(ref["몸"])
|
||||
n += k
|
||||
one = {**one, "참조": refs.copy_ref(inner, ref["키"])}
|
||||
elif isinstance(one.get("고정"), dict):
|
||||
fx, k = _reprice(one["고정"])
|
||||
n += k
|
||||
one = {**one, "고정": fx}
|
||||
elif body:
|
||||
adds, k = _reset_adds(body.get("더한줄") or [])
|
||||
n += k + len(body.get("덮개") or {}) + len(body.get("뺀줄") or [])
|
||||
keep = {x: v for x, v in body.items() if x not in ("덮개", "뺀줄", "더한줄")}
|
||||
rest = {**keep, **({"더한줄": adds} if adds else {})}
|
||||
one = {x: v for x, v in one.items() if x != patch.FIELD}
|
||||
one = {**one, **({patch.FIELD: rest} if rest else {})}
|
||||
lines.append(one)
|
||||
return {**doc, "담은로직": lines}, n
|
||||
|
||||
|
||||
def reset(kind: str, doc: dict) -> tuple[dict, int]:
|
||||
"""[초기화] — (걷은 문서, 걷힌 고침 수) · 먼저 펼침(옛 참조 줄도 M01 키로)."""
|
||||
doc = flat_doc(kind, doc)
|
||||
if kind == "sangeun":
|
||||
adds, n = _reset_adds(doc.get("더한줄") or [])
|
||||
n += len(doc.get("덮개") or {}) + len(doc.get("뺀줄") or [])
|
||||
return {**doc, "덮개": {}, "뺀줄": [], "더한줄": adds}, n
|
||||
lines, n = [], 0
|
||||
for one in doc.get("담은로직") or []:
|
||||
body = one.get(patch.FIELD) if isinstance(one, dict) else None
|
||||
if not body:
|
||||
lines.append(one)
|
||||
continue
|
||||
adds, k = _reset_adds(body.get("더한줄") or [])
|
||||
n += k + len(body.get("덮개") or {}) + len(body.get("뺀줄") or [])
|
||||
rest = {**({"더한줄": adds} if adds else {})}
|
||||
line = {x: v for x, v in one.items() if x != patch.FIELD}
|
||||
lines.append({**line, **({patch.FIELD: rest} if rest else {})})
|
||||
return {**doc, "담은로직": lines}, n
|
||||
return _reset_sc(doc)
|
||||
|
||||
|
||||
def reset_doc(kind: str, key: str | None, doc: Any, look: bool, version: str | None) -> dict:
|
||||
@@ -550,7 +539,9 @@ def migrate(apply: bool = False) -> list[dict[str, Any]]:
|
||||
ref = {"층": layer, "주인": owner, "키": path.stem}
|
||||
got = flat_doc(kind, doc)
|
||||
left = [
|
||||
r.get("키") for r in refs.refs_in(got) if refs.kind_of(r.get("키")) != "promote"
|
||||
r.get("키")
|
||||
for r in refs.refs_in(got)
|
||||
if refs.kind_of(r.get("키")) != "promote" and not refs.is_copy(r)
|
||||
]
|
||||
row = {
|
||||
"어디": where_,
|
||||
@@ -565,10 +556,7 @@ def migrate(apply: bool = False) -> list[dict[str, Any]]:
|
||||
|
||||
row["전"], row["후"] = _value(ref, refs.read_ref, whole), _value(ref, mine, whole)
|
||||
row["같음"] = row["전"] == row["후"]
|
||||
used = kind == "precombo" and len(got["담은로직"]) != len(doc.get("담은로직") or [])
|
||||
if used and refs.users(ref):
|
||||
row["까닭"] = "쓰는 곳 있는 일위대가 — 안 줄 차례가 바뀌어 안 옮김"
|
||||
elif not row["같음"]:
|
||||
if not row["같음"]:
|
||||
row["까닭"] = "전후 금액 다름 — 안 옮김"
|
||||
elif apply and got != doc:
|
||||
with store.at(path.parent.parent):
|
||||
|
||||
@@ -24,7 +24,7 @@ from common_util.common_util_derived_store import derived_key
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
|
||||
DROP = {"키", "이름", "구분", "상세구분", "본뜬", "메모", "별칭", "바탕판", "원값", "계산값", "열"}
|
||||
DROP |= {"일위대가번", "종류", "수정일", "출처"}
|
||||
DROP |= {"일위대가번", "종류", "수정일", "출처", "처음수량", "뺀더한줄"} # 744-1 되돌림 기억
|
||||
UNIT = {"m3": "㎥", "m2": "㎡", "m³": "㎥", "m²": "㎡"}
|
||||
EMPTY = (None, "", [], {})
|
||||
|
||||
|
||||
@@ -216,6 +216,17 @@ export const checkLibraryDelete = <T>(
|
||||
method: "POST",
|
||||
body: JSON.stringify({ 대상: picks }),
|
||||
});
|
||||
/** [초기화](740-2) — 미리 보기(`미리보기` true) = 걷힐 고침 수 · 아니면 그 층 문서에 씀(`판` 이 다르면 409) */
|
||||
export const resetLibrary = (
|
||||
kind: LibraryKind,
|
||||
layer: Layer,
|
||||
owner: string | null,
|
||||
body: { 키: string; 미리보기: boolean; 판?: string },
|
||||
): Promise<{ 걷힘: number; 문서: unknown }> =>
|
||||
call(owned(layer, `/layers/${layer}/${kind}-reset`, owner), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
/** 복사해 둔 문서 · 폴더 → 이 층 `dest` 아래 새 키(폴더는 안의 것까지 · 같은 폴더 이름 「(1)」) */
|
||||
export const pasteLibraryTree = (
|
||||
kind: LibraryKind,
|
||||
|
||||
@@ -110,6 +110,8 @@ export interface HopyoTarget {
|
||||
/** 묶음 표시(358-1 · 로직 후보만) — 품명 뜻 낱말 겹침 · 엑셀 출처 조항 같음(≥ 0.9) · 책(산림품셈 · 건설품셈) */
|
||||
뜻겹침?: boolean;
|
||||
출처같음?: boolean;
|
||||
/** 745-1 미리 고름 점 — 「다른 점」 1위 + 출처 같음(서버) · 자동 확정 아님 · 만들기는 사용자가 누름 */
|
||||
미리?: boolean;
|
||||
책?: string;
|
||||
}
|
||||
|
||||
@@ -469,8 +471,9 @@ export const saveLedger = async (
|
||||
})
|
||||
).줄;
|
||||
|
||||
export const deleteHopyo = (scope: HopyoScope, key: string): Promise<unknown> =>
|
||||
call(at(scope, `/hopyo/${enc(key)}`), { method: "DELETE" });
|
||||
/** [끝내기] · 화면 떠남(743-1) — `keepalive` = 페이지를 닫아도 요청은 감 */
|
||||
export const deleteHopyo = (scope: HopyoScope, key: string, keepalive = false): Promise<unknown> =>
|
||||
call(at(scope, `/hopyo/${enc(key)}`), { method: "DELETE", keepalive });
|
||||
|
||||
/** [확인하고 사전조합 만들기] — 확인한 줄만 · 직접 줄 호표 = `통째` → `target` 층 사전조합(파일 층 = `from`) */
|
||||
export const buildHopyo = (
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* 저장 안 함 — 고른 것은 부르는 쪽 캐시(확인 창)로만 감.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import type {
|
||||
@@ -30,6 +31,7 @@ const FOLD_TEXT: Record<string, string> = {
|
||||
"지역 변형": "하나 고르면 그 지역 · 「전국」으로 되돌림은 위",
|
||||
숫자: "이름 · 출처 안 닮음 · 숫자 지문만",
|
||||
};
|
||||
const PRE_TIP = "미리 고름 — 엑셀 인용 조항 = 1위 기초 조항 · 누르기는 사용자";
|
||||
const note = (text: string): HTMLElement =>
|
||||
el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } });
|
||||
const num = (v: number): string => String(Math.round(v * 10000) / 10000);
|
||||
@@ -270,8 +272,12 @@ export function openHopyoChoose(opt: ChooseOptions): void {
|
||||
const tag = basisTag(c.근거);
|
||||
return el("div", {
|
||||
className: `m02-hopyo__cand${weakCand(c) ? " is-weak" : ""}`,
|
||||
attrs: { "data-cand": c.로직 ?? c.자재 ?? (c.전국 ? "전국" : "") },
|
||||
attrs: {
|
||||
"data-cand": c.로직 ?? c.자재 ?? (c.전국 ? "전국" : ""),
|
||||
...(c.미리 ? { "data-pre": "" } : {}),
|
||||
},
|
||||
children: [
|
||||
...(c.미리 ? [createBadge(PRE_TIP, "accent", { kind: "dot" })] : []),
|
||||
box,
|
||||
createButton({
|
||||
label: candText(c, twins),
|
||||
@@ -287,6 +293,7 @@ export function openHopyoChoose(opt: ChooseOptions): void {
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
...(c.미리 ? [note("미리 고름")] : []),
|
||||
...(c.글 ? [note(c.글)] : []),
|
||||
...(weakCand(c) ? [note("이름 뜻 안 겹침")] : []),
|
||||
...(tag && c.근거
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_Side.ts
|
||||
* 좌측 「일위대가 불러오기」 컨테이너(PLAN 324-4 · 718-1 한시) — [불러오기](구조물 조합 층 단추 꼴 `m02-layers__btn`) →
|
||||
* 본 칸 마법사(`M02_MasterTemplete_Hopyo_Wizard` · 결과 층 고름 · 작업 중 이어서 · [끝내기] 때 올린 파일 지움) ·
|
||||
* 본 칸 마법사(`M02_MasterTemplete_Hopyo_Wizard` · 결과 층 고름 · 작업 중 이어서 · [끝내기] · 화면 떠남 때 올린 파일 지움) ·
|
||||
* 옛 파일 목록 없음(옛 HP 는 서버 목록에서 숨김) ·
|
||||
* 아래 [별칭 장부](338-3) → 본 칸 장부 화면(`M02_MasterTemplete_Hopyo_Ledger` · 층 = 구조물 조합 층).
|
||||
* ========================================================================== */
|
||||
@@ -40,7 +40,8 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
onClick: () => void showView(WIZARD),
|
||||
});
|
||||
start.classList.add("m02-layers__btn");
|
||||
start.title = "다른 프로그램 일위대가 엑셀 → 우리 일위대가(한시 · 끝내면 올린 파일 지움)";
|
||||
start.title =
|
||||
"다른 프로그램 일위대가 엑셀 → 우리 일위대가(한시 · 끝내거나 떠나면 올린 파일 지움)";
|
||||
start.dataset.hopyoWizardOpen = "";
|
||||
const ledger = el("button", {
|
||||
className: "m02-side__item ui-sidebar-row",
|
||||
@@ -52,7 +53,7 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
className: "m02-hopyo-side",
|
||||
children: [el("div", { className: "m02-side__row", children: [start] }), ledger],
|
||||
});
|
||||
let open: { key: string; view: HTMLElement } | null = null;
|
||||
let open: { key: string; view: HTMLElement; leave?: () => void } | null = null;
|
||||
|
||||
const paintList = (): void => {
|
||||
ledger.classList.toggle("is-active", open?.key === LEDGER_TITLE);
|
||||
@@ -61,18 +62,18 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
async function showView(key: string): Promise<void> {
|
||||
if (open?.key === key && open.view.isConnected) return;
|
||||
if (!(await opt.confirmLeave())) return;
|
||||
const view =
|
||||
key === WIZARD
|
||||
? buildHopyoWizard()
|
||||
: buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() });
|
||||
open?.leave?.(); // 마법사 떠남 = 올린 파일 지움(743-1)
|
||||
const wiz = key === WIZARD ? buildHopyoWizard() : null;
|
||||
const view = wiz?.root ?? buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() });
|
||||
opt.show(key === WIZARD ? title : `${title} — ${LEDGER_TITLE}`, view);
|
||||
opt.onOpen();
|
||||
open = { key, view };
|
||||
open = { key, view, ...(wiz ? { leave: wiz.leave } : {}) };
|
||||
paintList();
|
||||
}
|
||||
|
||||
const paint = (): void => {
|
||||
if (open && !open.view.isConnected) {
|
||||
open.leave?.(); // 다른 화면으로 = 마법사 떠남
|
||||
open = null;
|
||||
paintList();
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
* M02_MasterTemplete_Hopyo_Wizard.ts
|
||||
* 일위대가 불러오기 = 한시 마법사(PLAN 718-1 · 요구서 내역12 4장 22) — 본 칸 한 화면 · 걸음 셋:
|
||||
* ① 결과 층(쓸 수 있는 층만 · 기본 개인) · 결과(일위대가 늘 · 단가산출 체크) · 엑셀 고르기 → 올림
|
||||
* (그 층에 둠 · 같은 엑셀 작업 중 = 그 파일 이어서 · 끝낸 같은 엑셀 = 서버 409 안내) · 작업 중 파일 이어서
|
||||
* (그 층에 둠 · 같은 엑셀 작업 중 = 그 파일 이어서 · 끝낸 같은 엑셀 = 새로 올림) · 작업 중 파일 이어서
|
||||
* ② 보기 · 확인 · 만들기 = 엑셀 보기 창(`Hopyo_Sheet` — [모두 고정으로 들여오기] · 호표 [수정] →
|
||||
* 확인 창 만들기 · [구조물 조합으로] 그대로) · 만들 층 = 고른 층 ·
|
||||
* 단가산출 체크면 단가산출 칸(`Hopyo_WizardSk` — 기초 · SK · 고름 대기 기초 고름 · 729-3) ·
|
||||
* 안 이은 줄 띠(`Hopyo_Left` — 안 만든 호표의 남은 줄 · [고르기] = 줄마다 이을 것 · 729-3)
|
||||
* ③ [끝내기] = 남은 줄 다시 셈 → 있으면 막음(띠 · 남은 수 · 표) · 없으면 물음 → 서버가 이번 HP · 격자 · 맞춤셈 캐시 지움 ·
|
||||
* 만든 일위대가 · 단가산출 · 별칭 장부 남음 · 지문 기록
|
||||
* 작업 중 = 서버에 보관(떠났다 와도 ① 목록에서 이어서) · 옛 HP(지문 없음)는 서버 목록에서 숨김.
|
||||
* 만든 일위대가 · 단가산출(엑셀 사본 걷고 출처만) · 별칭 장부 남음
|
||||
* 작업 중 = 이 화면 동안만(743-1) — 화면 떠남(`leave` · 페이지 닫음) = 이 화면에서 올리거나 이은 파일 지움 ·
|
||||
* 하루 지난 것은 서버가 지움 · 엑셀 자료는 보관하지 않음(필요하면 다시 올림).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
@@ -61,7 +62,13 @@ export async function writableLayers(): Promise<ManageLayer[]> {
|
||||
return LAYERS.map((x) => x.layer).filter((layer) => accessFor(layer, null, user).canWrite);
|
||||
}
|
||||
|
||||
export function buildHopyoWizard(): HTMLElement {
|
||||
export interface HopyoWizard {
|
||||
root: HTMLElement;
|
||||
/** 화면 떠남 — 이 화면에서 올리거나 이은 파일 지움(743-1) */
|
||||
leave: () => void;
|
||||
}
|
||||
|
||||
export function buildHopyoWizard(): HopyoWizard {
|
||||
const steps = createSegment({
|
||||
options: STEPS.map((s) => ({ ...s, disabled: true })),
|
||||
value: "1",
|
||||
@@ -73,7 +80,7 @@ export function buildHopyoWizard(): HTMLElement {
|
||||
attrs: { "data-hopyo-wizard": "" },
|
||||
children: [
|
||||
createText(
|
||||
"한시 — 다른 프로그램 일위대가 엑셀을 우리 일위대가로 옮김 · 끝내면 올린 파일 지움 · 별칭 장부는 남음",
|
||||
"한시 — 다른 프로그램 일위대가 엑셀을 우리 일위대가로 옮김 · 끝내거나 화면을 떠나면 올린 파일 지움 · 별칭 장부는 남음",
|
||||
"hint",
|
||||
),
|
||||
el("div", { className: "ui-toolbar", children: [steps.root] }),
|
||||
@@ -84,6 +91,16 @@ export function buildHopyoWizard(): HTMLElement {
|
||||
let pick: ManageLayer = "personal";
|
||||
let wantSangeun = false; // 결과 「단가산출」 체크
|
||||
const scopeOf = (layer: ManageLayer): HopyoScope => ({ layer, owner: null });
|
||||
const held = new Map<string, HopyoScope>(); // 이 화면이 쥔 파일 「층|키」 → 층
|
||||
const hold = (layer: ManageLayer, key: string): void =>
|
||||
void held.set(`${layer}|${key}`, scopeOf(layer));
|
||||
const drop = (layer: ManageLayer, key: string): void => void held.delete(`${layer}|${key}`);
|
||||
const leave = (): void => {
|
||||
window.removeEventListener("pagehide", leave);
|
||||
for (const [id, scope] of held) void deleteHopyo(scope, id.split("|")[1], true).catch(() => {});
|
||||
held.clear();
|
||||
};
|
||||
window.addEventListener("pagehide", leave);
|
||||
|
||||
async function first(): Promise<void> {
|
||||
steps.set("1");
|
||||
@@ -166,6 +183,7 @@ export function buildHopyoWizard(): HTMLElement {
|
||||
|
||||
function second(layer: ManageLayer, file: HopyoFile): void {
|
||||
steps.set("2");
|
||||
hold(layer, file.키);
|
||||
const scope = scopeOf(layer);
|
||||
const view = createButton({
|
||||
label: "엑셀 보기 · 확인 · 만들기",
|
||||
@@ -179,7 +197,10 @@ export function buildHopyoWizard(): HTMLElement {
|
||||
파일: file.파일,
|
||||
target: scope,
|
||||
canWrite: true,
|
||||
onDeleted: () => void first(),
|
||||
onDeleted: () => {
|
||||
drop(layer, file.키);
|
||||
void first();
|
||||
},
|
||||
}),
|
||||
)
|
||||
.catch((error: unknown) => showToast(why(error), "error")),
|
||||
@@ -265,13 +286,14 @@ export function buildHopyoWizard(): HTMLElement {
|
||||
(x) => !x.one.호표.만든?.확인 && hasDraft(draftKey(scope, file.키, x.one.호표.번)),
|
||||
).length;
|
||||
const ok = await showConfirmDialog(
|
||||
`「${file.파일}」 불러오기를 끝냄 — 올린 파일(엑셀 결과 · 격자 · 맞춤 캐시)을 지움 · 만든 일위대가 · 단가산출과 별칭 장부는 남음 · 하나라도 만들었으면 같은 엑셀은 다시 올리지 못함` +
|
||||
`「${file.파일}」 불러오기를 끝냄 — 올린 파일(엑셀 결과 · 격자 · 맞춤 캐시)을 지움 · 만든 일위대가 · 단가산출(출처만 남김)과 별칭 장부는 남음 · 같은 엑셀은 필요하면 다시 올림` +
|
||||
(loose ? ` · ${L("M02_LeftLoose").replace("{n}", String(loose))}` : ""),
|
||||
"끝내기",
|
||||
);
|
||||
if (!ok) return steps.set("2");
|
||||
try {
|
||||
const got = (await deleteHopyo(scope, file.키)) as { 만든?: number };
|
||||
drop(layer, file.키);
|
||||
showToast(`끝냄 — 만든 일위대가 · 단가산출 ${got.만든 ?? 0} · 올린 파일 지움`, "success");
|
||||
} catch (error) {
|
||||
showToast(why(error), "error");
|
||||
@@ -282,5 +304,5 @@ export function buildHopyoWizard(): HTMLElement {
|
||||
}
|
||||
|
||||
void first();
|
||||
return root;
|
||||
return { root, leave };
|
||||
}
|
||||
|
||||
@@ -35,6 +35,13 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 740-2 넣어 펼친 줄 출처 꼬리표 — 이름 글 끝에 이어 붙음(줄 높이 그대로 · 넘치면 말줄임) */
|
||||
.m02-labrow__born {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: var(--font-weight-regular, 400);
|
||||
}
|
||||
|
||||
.m02-labrow__key,
|
||||
.m02-labrow__note {
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
@@ -111,6 +111,8 @@ export interface LaborRow {
|
||||
고정?: LaborFixed;
|
||||
/** 그 줄 덮개(715-3 · 717-3 단가산출 모달) — 로직 줄 · SK 참조 줄 · 서버가 검사 */
|
||||
패치?: LinePatch;
|
||||
/** 넣어 펼친 줄 출처(740-1 · 740-2 꼬리표) — {층, 주인, 키, 이름, 때} · 셈에 안 씀 */
|
||||
본뜬?: Record<string, unknown>;
|
||||
}
|
||||
/** 자재 세 칸(148-4 · 서버 `M02_Labor_Doc.MARKS` 와 같음) — 빈 칸 = 읽을 때 기본값 · 금액 무관 */
|
||||
export const MAT_MARKS = {
|
||||
@@ -127,6 +129,9 @@ export type NationalMat = MatMarks & {
|
||||
단위: string;
|
||||
수량: { 손: number };
|
||||
};
|
||||
/** 본뜬 곳 글 — 「층 키」 · 끝낸 실무자료 = 「실무자료 파일」 · 없으면 null */
|
||||
export const originText = (o: LaborDoc["본뜬"]): string | null =>
|
||||
o?.키 || o?.파일 ? `${o.층} ${o.키 ?? o.파일}` : null;
|
||||
/** 읽을 때 기본값(162-1 · 서버 `MARK_DEFAULT` 와 같음) — 저장은 비움 */
|
||||
export const MAT_MARK_DEFAULT: Required<MatMarks> = {
|
||||
자재구분: "사급",
|
||||
@@ -151,6 +156,8 @@ function cleanShown(v: unknown): { 자재표시?: Record<string, MatMarks> } {
|
||||
return Object.keys(out).length ? { 자재표시: out } : {};
|
||||
}
|
||||
export interface LaborMat extends MatMarks {
|
||||
/** 넣어 펼친 자재 출처(740-2) */
|
||||
본뜬?: Record<string, unknown>;
|
||||
코드: string;
|
||||
이름: string | null;
|
||||
규격: string | null;
|
||||
@@ -163,7 +170,14 @@ export interface LaborDoc {
|
||||
열: string;
|
||||
일위대가번?: string;
|
||||
단위: string | null;
|
||||
본뜬?: { 층: string; 주인: string | null; 키: string; 전국자재?: NationalMat[] } | null;
|
||||
/** 실무자료 끝낸 뒤(743-1) = 키 없이 `파일 · 때 · 누가` 출처만 */
|
||||
본뜬?: {
|
||||
층: string;
|
||||
주인?: string | null;
|
||||
키?: string;
|
||||
파일?: string;
|
||||
전국자재?: NationalMat[];
|
||||
} | null;
|
||||
담은로직: LaborRow[];
|
||||
자재: LaborMat[];
|
||||
계산값?: unknown;
|
||||
@@ -192,6 +206,7 @@ export function normalizeLabor(doc: unknown, column: string): LaborDoc {
|
||||
...(r.참조 || r.고정 ? {} : cleanShown(r.자재표시)),
|
||||
...(r.고정 ? { 고정: r.고정 } : {}), // 705-1 — 읽을 때 떨구면 [저장]에서 줄이 사라짐
|
||||
...(r.패치 ? { 패치: r.패치 } : {}), // 717-3 — 떨구면 [저장]에서 줄 덮개가 사라짐
|
||||
...(r.본뜬 ? { 본뜬: r.본뜬 } : {}), // 740-2 — 출처 꼬리표
|
||||
})),
|
||||
자재: (d.자재 ?? []).map((m) => ({
|
||||
코드: m.코드,
|
||||
@@ -199,6 +214,7 @@ export function normalizeLabor(doc: unknown, column: string): LaborDoc {
|
||||
규격: m.규격 ?? null,
|
||||
단위: m.단위 ?? null,
|
||||
수량: m.수량 ?? null,
|
||||
...(m.본뜬 ? { 본뜬: m.본뜬 } : {}), // 740-2
|
||||
...cleanMarks(m),
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Labor_Born.ts
|
||||
* 일위대가 상세 출처 꼬리표(740-2) — 넣어 펼친 줄 · 자재의 `본뜬` 을 짧은 글로(풍선 = 키 · 이름 · 층 · 때).
|
||||
* 일위대가표 격자 = 비고 칸 글 `bornNote` + 풍선 `tipBorn`(칸 폭 · 줄 높이 안 바뀜)
|
||||
* 줄 표(`Labor_Rows` · `Labor_MatTable`) = 이름 글 끝 `bornSpan`(같은 줄 · 넘치면 말줄임)
|
||||
* 색 단추 아님 · 셈에 안 씀.
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { bornText } from "./logic/M02_Born_Tag";
|
||||
|
||||
const SPLIT = "\n"; // 글 첫 줄 = 보임 · 나머지 = 풍선
|
||||
|
||||
/** 일위대가표 격자 비고 칸 풍선 — `at` = 격자 줄 → 일위대가 줄 번호(담은 로직 뒤 자재) */
|
||||
export function tipBorn(
|
||||
root: HTMLElement,
|
||||
at: (row: number) => number | null,
|
||||
born: (n: number) => unknown,
|
||||
): void {
|
||||
root.querySelectorAll<HTMLElement>("tr[data-i]").forEach((tr) => {
|
||||
const n = at(Number(tr.dataset.i));
|
||||
const [, ...tip] = n === null ? [] : bornText(born(n)).split(SPLIT);
|
||||
if (tip.length)
|
||||
tr.querySelector<HTMLElement>("td:nth-last-child(2)")?.setAttribute("title", tip.join(" "));
|
||||
});
|
||||
}
|
||||
|
||||
/** 줄 표 이름 끝 꼬리표 — `본뜬` 이 없으면 null */
|
||||
export function bornSpan(born: unknown): HTMLElement | null {
|
||||
const [shown, ...tip] = bornText(born).split(SPLIT);
|
||||
if (!shown) return null;
|
||||
return el("span", {
|
||||
className: "m02-labrow__born",
|
||||
text: ` ${shown}`,
|
||||
attrs: { title: tip.join(" "), "data-born": "" },
|
||||
});
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
type LaborQty,
|
||||
type LaborRow,
|
||||
type MatMarkKey,
|
||||
originText,
|
||||
} from "./M02_MasterTemplete_Labor_Api";
|
||||
import {
|
||||
buildLaborDiagram,
|
||||
@@ -567,7 +568,7 @@ export function mountLaborDetail(
|
||||
);
|
||||
drawUnit();
|
||||
const origin = createInputField({ type: "text", label: tc("Labor_Origin") });
|
||||
origin.input.value = cur.본뜬?.키 ? `${cur.본뜬.층} ${cur.본뜬.키}` : tc("Origin_Self");
|
||||
origin.input.value = originText(cur.본뜬) ?? tc("Origin_Self");
|
||||
origin.input.readOnly = true;
|
||||
|
||||
const section = (title: string, button: HTMLElement | null, body: HTMLElement): HTMLElement =>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "@ui/ui_template_hopyo_grid";
|
||||
import type { ItemLogic } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import type { LaborCalc, LaborCost, LaborDoc } from "./M02_MasterTemplete_Labor_Api";
|
||||
import { bornNote } from "./logic/M02_Born_Tag";
|
||||
import { lineId } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import { valueLine } from "./M02_Labor_Values_Model";
|
||||
@@ -79,7 +80,8 @@ export function laborBlock(
|
||||
amounts: money(line?.반영),
|
||||
note:
|
||||
line?.까닭 ??
|
||||
(calc && qty === null ? tc("Grid_NoQty") : row.참조 ? layerWord(row.참조.층) : row.로직),
|
||||
(bornNote(row.본뜬) || // 740-2 넣은 줄 = 출처 글
|
||||
(calc && qty === null ? tc("Grid_NoQty") : row.참조 ? layerWord(row.참조.층) : row.로직)),
|
||||
tag: HOPYO_TAG.pum,
|
||||
};
|
||||
});
|
||||
@@ -101,7 +103,7 @@ export function laborBlock(
|
||||
? tc("Grid_NoQty")
|
||||
: mat.별도계상 !== "직접계산"
|
||||
? tc("Grid_Apart") // 짝 `settle` 의 aside 와 같은 규칙
|
||||
: mat.코드),
|
||||
: bornNote(mat.본뜬) || mat.코드),
|
||||
};
|
||||
});
|
||||
const lines = [...logicLines, ...matLines];
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
LaborCalcMat,
|
||||
LaborMat,
|
||||
} from "./M02_MasterTemplete_Labor_Api";
|
||||
import { bornSpan } from "./M02_MasterTemplete_Labor_Born";
|
||||
import { markCells, markHeads } from "./M02_MasterTemplete_Labor_MatMarks";
|
||||
import {
|
||||
moneyCell,
|
||||
@@ -66,7 +67,10 @@ export function matTable(data: MatTableData, on?: Omit<RowHandlers, "move">): HT
|
||||
el("td", {
|
||||
className: "m02-labrow__name",
|
||||
children: [
|
||||
el("strong", { text: mat.이름 ?? "" }),
|
||||
el("strong", {
|
||||
text: mat.이름 ?? "",
|
||||
children: [bornSpan(mat.본뜬)].filter((x): x is HTMLElement => !!x),
|
||||
}),
|
||||
...(line?.까닭
|
||||
? [el("small", { className: "ui-text ui-text--error", text: line.까닭 })]
|
||||
: []),
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
LaborRow,
|
||||
MatMarkKey,
|
||||
} from "./M02_MasterTemplete_Labor_Api";
|
||||
import { bornSpan } from "./M02_MasterTemplete_Labor_Born";
|
||||
import { lineId } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import { attachRowDrag } from "./M02_MasterTemplete_Labor_RowDrag";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
@@ -414,6 +415,7 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
|
||||
}),
|
||||
el("strong", {
|
||||
text: fx?.이름 ?? (row.참조 ? line?.이름 : logic?.이름) ?? row.참조?.키 ?? row.로직,
|
||||
children: [bornSpan(row.본뜬)].filter((x): x is HTMLElement => !!x),
|
||||
}),
|
||||
...(values.empty
|
||||
? [
|
||||
|
||||
@@ -30,9 +30,11 @@ import {
|
||||
type LaborCalc,
|
||||
type LaborDoc,
|
||||
type LaborRow,
|
||||
originText,
|
||||
} from "./M02_MasterTemplete_Labor_Api";
|
||||
import type { LaborDetailHandle, LaborDetailOptions } from "./M02_MasterTemplete_Labor_Detail";
|
||||
import { laborBlock } from "./M02_MasterTemplete_Labor_Grid";
|
||||
import { tipBorn } from "./M02_MasterTemplete_Labor_Born";
|
||||
import { unlinkedBar } from "./M02_MasterTemplete_Labor_Unlinked";
|
||||
import { pasteInto } from "./M02_MasterTemplete_Labor_Paste";
|
||||
import { openLogicPicker, withHopyo } from "./M02_MasterTemplete_Labor_Pick";
|
||||
@@ -187,6 +189,7 @@ export function mountLaborTable(
|
||||
// 칸 초점이 표를 밀면(scroll) 메뉴가 바로 닫힘 — 밀림이 끝난 다음 그림에 엶
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => openTreeMenu(ev, menuAt(n))));
|
||||
});
|
||||
tipBorn(grid.root, at, (n) => (body.담은로직[n] ?? body.자재[n - body.담은로직.length])?.본뜬); // 740-2
|
||||
const bar = unlinkedBar(cur, edit && ((qtyOnly) => changed(qtyOnly))); // 729-3 안 이은 줄 띠
|
||||
gridHost.replaceChildren(...(bar ? [bar] : []), grid.root);
|
||||
showSource();
|
||||
@@ -429,7 +432,7 @@ export function mountLaborTable(
|
||||
);
|
||||
drawUnit();
|
||||
const origin = createInputField({ type: "text", label: tc("Labor_Origin") });
|
||||
origin.input.value = cur.본뜬?.키 ? `${cur.본뜬.층} ${cur.본뜬.키}` : tc("Origin_Self");
|
||||
origin.input.value = originText(cur.본뜬) ?? tc("Origin_Self");
|
||||
origin.input.readOnly = true;
|
||||
|
||||
host.replaceChildren(
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Main_Actions.ts
|
||||
* 단가산출 · 일위대가 상세 머리 단추 [초기화] [삭제](740-2 · 738-2 · 요구 579-3 · 581) — 차례 … [초기화] [삭제] [저장].
|
||||
* [삭제] = 735 경고 창(쓰는 곳 표 · 키) → 나무 지우기 → 목록으로 · 쓰는 곳 없으면 한 줄 물음.
|
||||
* [초기화] = 서버 `-reset` 미리 보기(걷힐 고침 수) → 물음 → 그 층에 씀 → 다시 읽음.
|
||||
* 덮개 · 뺀줄 · 더한줄 단가를 M01 키 값으로 되돌림 · 옵션(입력값) · 기본정보는 그대로(서버 740-1).
|
||||
* 둘 다 쓰기 권한이 있는 층에서만 보임(`update`) · 상세에 저장 안 한 고침이 있으면 먼저 물음.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, showToast } from "@ui/ui_template_elements";
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { askConfirm } from "@ui/ui_template_modal";
|
||||
import {
|
||||
editPrecomboTree,
|
||||
fetchPrecomboTree,
|
||||
resetLibrary,
|
||||
type LibraryKind,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { askLibraryDelete } from "./M02_MasterTemplete_Library_Delete";
|
||||
import { precomboList } from "./M02_MasterTemplete_UI_Side_Precombo";
|
||||
import { sangeunList } from "./M02_MasterTemplete_UI_Side_Sangeun";
|
||||
import type { Selection } from "./M02_MasterTemplete_UI_Main";
|
||||
|
||||
const TEXT = {
|
||||
Reset: ["초기화", "Reset"],
|
||||
ResetTip: [
|
||||
"고친 값을 연결된 M01 키 값으로 되돌림 — 옵션 · 기본정보는 그대로",
|
||||
"Revert edited values to their linked M01 key values — options and basic info stay",
|
||||
],
|
||||
Delete: ["삭제", "Delete"],
|
||||
DeleteTip: [
|
||||
"이 항목을 지움 — 쓰는 곳이 있으면 먼저 알림",
|
||||
"Delete this item — warns first if used",
|
||||
],
|
||||
ResetAsk: [
|
||||
"고친 값 {n} 개를 M01 값으로 되돌림{k} — 옵션 · 기본정보는 그대로",
|
||||
"Revert {n} edited value(s) to M01 values{k} — options and basic info stay",
|
||||
],
|
||||
Restored: [" (빼 둔 줄 {k} 개 되살림)", " ({k} removed line(s) restored)"],
|
||||
Nothing: ["되돌릴 고침이 없음", "Nothing to revert"],
|
||||
Done: ["M01 값으로 되돌렸음", "Reverted to M01 values"],
|
||||
DeleteAsk: ["「{name}」 을(를) 지울까?", "Delete 「{name}」?"],
|
||||
Deleted: ["지웠음", "Deleted"],
|
||||
Failed: ["못 했음 — {why}", "Failed — {why}"],
|
||||
} as const;
|
||||
const tx = (key: keyof typeof TEXT): string =>
|
||||
String(TEXT[key][currentLanguageIndex as 0 | 1] ?? TEXT[key][0]);
|
||||
const fill = (text: string, vars: Record<string, string | number>): string =>
|
||||
Object.entries(vars).reduce((s, [k, v]) => s.replace(`{${k}}`, String(v)), text);
|
||||
|
||||
export interface MainActionsCtx {
|
||||
sel: () => Selection | null;
|
||||
/** 지금 읽은 문서의 판 */
|
||||
version: () => string;
|
||||
/** 지금 읽은 문서(저장본) — 뺀 줄 수를 셈 */
|
||||
loaded: () => unknown;
|
||||
confirmLeave: () => Promise<boolean>;
|
||||
open: (sel: Selection | null) => Promise<void>;
|
||||
/** 좌측 목록을 다시 읽음 */
|
||||
changed: () => void;
|
||||
}
|
||||
|
||||
/** 문서 안 모든 `뺀줄` 길이 합(단가산출 · 일위대가 줄 안 덮개까지) */
|
||||
function removedCount(node: unknown): number {
|
||||
if (Array.isArray(node)) return node.reduce<number>((n, x) => n + removedCount(x), 0);
|
||||
if (!node || typeof node !== "object") return 0;
|
||||
return Object.entries(node as Record<string, unknown>).reduce(
|
||||
(n, [k, v]) => n + (k === "뺀줄" && Array.isArray(v) ? v.length : removedCount(v)),
|
||||
0,
|
||||
);
|
||||
}
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
|
||||
|
||||
export function createMainActions(ctx: MainActionsCtx): {
|
||||
reset: HTMLButtonElement;
|
||||
del: HTMLButtonElement;
|
||||
update: (writable: boolean) => void;
|
||||
} {
|
||||
const reset = createButton({ label: tx("Reset"), variant: "ghost" });
|
||||
const del = createButton({ label: tx("Delete"), variant: "danger-ghost" });
|
||||
reset.title = tx("ResetTip");
|
||||
del.title = tx("DeleteTip");
|
||||
reset.dataset.mainReset = "";
|
||||
del.dataset.mainDelete = "";
|
||||
|
||||
const target = (): { at: Selection; kind: LibraryKind } | null => {
|
||||
const at = ctx.sel();
|
||||
const kind = at?.kind;
|
||||
return at && at.name && (kind === "sangeun" || kind === "precombo") ? { at, kind } : null;
|
||||
};
|
||||
|
||||
reset.addEventListener("click", async () => {
|
||||
const got = target();
|
||||
if (!got || !(await ctx.confirmLeave())) return;
|
||||
const { at, kind } = got;
|
||||
try {
|
||||
const look = await resetLibrary(kind, at.layer, at.owner ?? null, {
|
||||
키: at.name,
|
||||
미리보기: true,
|
||||
});
|
||||
if (!look.걷힘) return void showToast(tx("Nothing"), "success");
|
||||
const k = removedCount(ctx.loaded());
|
||||
const message = fill(tx("ResetAsk"), {
|
||||
n: look.걷힘,
|
||||
k: k ? fill(tx("Restored"), { k }) : "",
|
||||
});
|
||||
if (!(await askConfirm({ message, okLabel: tx("Reset"), danger: true }))) return;
|
||||
await resetLibrary(kind, at.layer, at.owner ?? null, {
|
||||
키: at.name,
|
||||
미리보기: false,
|
||||
판: ctx.version(),
|
||||
});
|
||||
await ctx.open(at); // 다시 그림
|
||||
ctx.changed();
|
||||
showToast(tx("Done"), "success");
|
||||
} catch (error) {
|
||||
showToast(fill(tx("Failed"), { why: why(error) }), "error");
|
||||
}
|
||||
});
|
||||
|
||||
del.addEventListener("click", async () => {
|
||||
const got = target();
|
||||
if (!got || !(await ctx.confirmLeave())) return;
|
||||
const { at, kind } = got;
|
||||
const owner = at.owner ?? null;
|
||||
try {
|
||||
const tree = await fetchPrecomboTree(at.layer, owner, kind);
|
||||
const pick = { 경로: tree.자리?.[at.name] ?? [], 키: at.name };
|
||||
const ask = fill(tx("DeleteAsk"), { name: at.label ?? at.name });
|
||||
const ans = await askLibraryDelete(kind, at.layer, owner, [pick], ask, tx("Delete"));
|
||||
if (!ans.ok) return;
|
||||
await editPrecomboTree(
|
||||
at.layer,
|
||||
{ op: "delete", 대상: [pick], ...(ans.sure ? { 확인: true } : {}) },
|
||||
tree.판,
|
||||
owner,
|
||||
kind,
|
||||
);
|
||||
const scope = { layer: at.layer, owner, canWrite: true } as Parameters<typeof sangeunList>[0];
|
||||
await ctx.open(kind === "sangeun" ? sangeunList(scope) : precomboList(scope)); // 목록으로
|
||||
ctx.changed();
|
||||
showToast(tx("Deleted"), "success");
|
||||
} catch (error) {
|
||||
showToast(fill(tx("Failed"), { why: why(error) }), "error");
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
reset,
|
||||
del,
|
||||
update: (writable) => {
|
||||
const on = writable && !!target();
|
||||
reset.hidden = del.hidden = !on;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -52,10 +52,26 @@
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
/* 740-2 이름 + 출처 꼬리표 한 줄 — 이름이 말줄임으로 줄고 꼬리표는 그대로 */
|
||||
.m02-sk .m01lab__namerow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.m02-sk .m01lab__namerow > span:first-child {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 727-2 기초 출처 꼬리표 — 칸 폭 고정 · 넘치면 말줄임(풍선 = 온 글) · 이름 칸 폭을 안 바꿈 */
|
||||
.m02-sk .m01lab__src {
|
||||
display: inline-block;
|
||||
max-width: 6em;
|
||||
flex: none;
|
||||
max-width: 9em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -39,6 +39,7 @@ import { isPrecombo } from "./M02_MasterTemplete_UI_Side_Precombo";
|
||||
import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { createMainActions } from "./M02_MasterTemplete_Main_Actions";
|
||||
export interface Selection {
|
||||
layer: Layer;
|
||||
projectId: string | null;
|
||||
@@ -141,6 +142,15 @@ export function createMain(
|
||||
const title = el("h2", { className: "m02-main__title", text: L("M02_PickTemplate") });
|
||||
const reload = createButton({ label: L("M02_Reload"), variant: "ghost" });
|
||||
const save = createButton({ label: L("M02_Save"), variant: "filled" });
|
||||
// 단가산출 · 일위대가 상세 [초기화] [삭제](740-2 · 738-2) — 차례 … [초기화] [삭제] [저장]
|
||||
const actions = createMainActions({
|
||||
sel: () => sel,
|
||||
version: () => version,
|
||||
loaded: () => loaded,
|
||||
confirmLeave: () => confirmLeave(),
|
||||
open: (at) => open(at),
|
||||
changed: () => onSaved?.(),
|
||||
});
|
||||
// 구조물 도면 · 일위대가 · 면적산출 상세 화면 → 구조물집계표(그 열 고른 채) · 일반 단추(43-5 · 알약 · ghost 아님)
|
||||
const back = createButton({ label: L("M02_Back") });
|
||||
const helpSlot = el("span", { className: "m02-main__help" }); // 상세 [?](49-7) — 제목 바로 옆
|
||||
@@ -171,6 +181,8 @@ export function createMain(
|
||||
helpSlot,
|
||||
slot,
|
||||
reload,
|
||||
actions.reset,
|
||||
actions.del,
|
||||
save,
|
||||
...(extras.newItem ? [extras.newItem] : []),
|
||||
],
|
||||
@@ -256,6 +268,7 @@ export function createMain(
|
||||
const bar = (): void => {
|
||||
reload.hidden = !sel;
|
||||
save.hidden = !sel || !writable(sel);
|
||||
actions.update(!!sel && writable(sel));
|
||||
back.hidden = !sel || !(sel.kind in NUMBER_FIELD);
|
||||
nav.update(back.hidden ? null : sel); // 보임 조건 = 뒤로 단추와 같음
|
||||
const kind = sel && sel.kind in HELP_TITLE ? (sel.kind as keyof typeof HELP_TITLE) : null;
|
||||
|
||||
@@ -249,7 +249,8 @@ def tag_of(whole: Any, code: str) -> str:
|
||||
|
||||
|
||||
def _detail(one: dict, at: int, whole: Any) -> dict[str, Any]:
|
||||
"""둘째 기초 세부 줄(화면) — 수량 · 금액 · 비목 = 세부 × 더한줄 수량 · cid `add:#k/세부 cid` ·
|
||||
"""둘째 기초 세부 줄(화면) — 수량 · 금액 · 비목 = 세부 × 더한줄 수량(상자 합 몫) · `원문` =
|
||||
그 기초 1 기준단위 몫(M01 원문 품 · 744-1 화면 품 칸) · cid `add:#k/세부 cid` ·
|
||||
`바탕값` = 덮기 전 1 기준단위 품 · 단가 · `뺀세부` = 뺀 줄 이름."""
|
||||
sub = sub_run(one, whole)
|
||||
plain = sub_run(one, whole, False) if sd.sub_patched(one) else sub
|
||||
@@ -272,6 +273,7 @@ def _detail(one: dict, at: int, whole: Any) -> dict[str, Any]:
|
||||
**scaled,
|
||||
"비목": parts,
|
||||
"cid": f"add:#{at}/{cid}",
|
||||
"원문": {k: line.get(k) for k in ("수량", "금액", "비목")},
|
||||
"바탕값": _was(was[cid]) if cid in was else None,
|
||||
**({"덮개": [k for k in sd.COVER_KEYS if k in mine]} if mine else {}),
|
||||
**({"원값": mine["원값"]} if "원값" in mine else {}),
|
||||
@@ -439,6 +441,8 @@ def clean_doc(doc: Any, here_: refs.Here) -> dict[str, Any]:
|
||||
base = _base_of(rough, whole, refs.read_ref, ())
|
||||
out = sd.clean(doc, base["줄"], whole)
|
||||
out["바탕"], out["더한줄"] = rough["바탕"], rough["더한줄"]
|
||||
for one in out["더한줄"]: # 처음수량(744-1) = 넣을 때 수량 · 없으면 지금 수량
|
||||
one.setdefault("처음수량", one["수량"])
|
||||
if out["덮개"]:
|
||||
bare = run({**out, "덮개": {}}, whole) # 덮기 전 값 = 원값(그때 M01)
|
||||
was = {x["cid"]: x for x in bare.get("줄") or []}
|
||||
|
||||
@@ -196,9 +196,25 @@ def _added(raw: Any, where: str, whole: Any) -> dict:
|
||||
else:
|
||||
raise DocError(f"{where} · 종류는 인력 · 재료 · 기계 · 로직 · 참조 · 고정")
|
||||
mark = {"본뜬": raw["본뜬"]} if isinstance(raw.get("본뜬"), dict) else {} # 출처(740-1)
|
||||
if raw.get("처음수량") is not None: # 넣을 때 수량(744-1 [되돌림] 값)
|
||||
mark["처음수량"] = plain(_num(raw["처음수량"], f"{where} 처음수량"))
|
||||
return {**head, "이름": name, "단위": unit, "수량": qty, **mark}
|
||||
|
||||
|
||||
def _gone_adds(raw: Any, whole: Any) -> list[dict]:
|
||||
"""뺀 더한줄 기억(744-1 · [되살림]) `[{차례, 줄}]` — 셈 안 함 · 못 읽는 줄은 버림."""
|
||||
out = []
|
||||
for one in raw if isinstance(raw, list) else []:
|
||||
at = one.get("차례") if isinstance(one, dict) else None
|
||||
if not isinstance(at, int) or isinstance(at, bool) or at < 0:
|
||||
continue
|
||||
try:
|
||||
out.append({"차례": at, "줄": _added(one.get("줄"), "뺀 더한줄", whole)})
|
||||
except DocError:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def _sub_patch(raw: dict, where: str) -> dict:
|
||||
"""둘째 기초 세부 고침(727-2) — `덮개` {세부 cid: 덮개} · `뺀줄` [세부 cid] · 빈 칸은 안 둠 ·
|
||||
`원값` · `바탕판` = 서버 몫(버림)."""
|
||||
@@ -327,6 +343,8 @@ def clean(doc: Any, base_row: dict | None = None, whole: Any = None) -> dict[str
|
||||
if len(set(drop)) != len(drop):
|
||||
raise DocError("뺀줄에 같은 줄 두 번")
|
||||
out["뺀줄"] = drop
|
||||
if gone := _gone_adds(doc.get("뺀더한줄"), whole):
|
||||
out["뺀더한줄"] = gone
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/* =============================================================================
|
||||
* M02_Born_Tag.ts
|
||||
* 넣은 줄 출처 꼬리표(740-2 · 요구 581) — 서버 `본뜬 {층, 주인, 키, 이름, 때}`(740-1) → 화면 글.
|
||||
* 순수 계산 — 화면 · fetch 없음. 첫 줄 = 줄 옆에 보이는 짧은 글 · 둘째 줄 = 풍선(키 · 이름 · 층 · 때).
|
||||
* 공용 상세 꼬리표 칸(`LogicLab_Ho` sourceTag)이 「\n」 앞만 보이고 전체를 풍선으로 씀.
|
||||
* ========================================================================== */
|
||||
|
||||
export interface Born {
|
||||
층?: string | null;
|
||||
주인?: string | null;
|
||||
키?: string | null;
|
||||
이름?: string | null;
|
||||
때?: string | null;
|
||||
}
|
||||
|
||||
const LAYER: Record<string, string> = {
|
||||
system: "마스터",
|
||||
company: "회사",
|
||||
personal: "개인",
|
||||
project: "프로젝트",
|
||||
};
|
||||
|
||||
/** 서버 때(UTC) → 이 컴퓨터 시각 「2026-11-02 20:13」 · 읽을 수 없으면 "" */
|
||||
function localTime(at: unknown): string {
|
||||
const d = new Date(String(at ?? ""));
|
||||
if (Number.isNaN(d.getTime())) return "";
|
||||
const z = (n: number): string => String(n).padStart(2, "0");
|
||||
return `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())} ${z(d.getHours())}:${z(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
/** `본뜬` 한 벌 → 꼬리표 글(없으면 ""). `before` = 이미 있는 꼬리표(기초 원문번호) — 앞에 둠 */
|
||||
export function bornText(born: unknown, before = ""): string {
|
||||
const b = born && typeof born === "object" ? (born as Born) : null;
|
||||
if (!b || (!b.키 && !b.이름)) return before;
|
||||
const shown = `${before ? `${before} · ` : ""}본뜬 ${b.키 ?? b.이름}`;
|
||||
const layer = LAYER[String(b.층 ?? "")] ?? String(b.층 ?? "");
|
||||
const tip = [`${b.키 ?? ""} ${b.이름 ?? ""}`.trim(), layer, localTime(b.때)]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
return `${shown}\n${tip}`;
|
||||
}
|
||||
|
||||
/** 단가산출 줄 열쇠(`add:#k` · `add:#k/…`) → 그 더한줄의 꼬리표 글 */
|
||||
export function bornOfCid(cid: string, adds: readonly object[] | undefined, before = ""): string {
|
||||
const hit = /^add:#(\d+)/.exec(cid);
|
||||
return bornText(
|
||||
hit ? (adds?.[Number(hit[1])] as { 본뜬?: unknown } | undefined)?.본뜬 : undefined,
|
||||
before,
|
||||
);
|
||||
}
|
||||
|
||||
/** 격자 비고 칸 글 — 「본뜬 SK-000012」 · 없으면 "" (첫 줄만) */
|
||||
export const bornNote = (born: unknown): string => bornText(born).split("\n")[0];
|
||||
@@ -13,6 +13,7 @@
|
||||
* 화면 · fetch · sessionStorage 없음(`CLAUDE.md` 4장).
|
||||
* ========================================================================== */
|
||||
|
||||
import { bornOfCid } from "./M02_Born_Tag";
|
||||
import type { SheetGroup } from "../../A00_Common/calc_sheet";
|
||||
|
||||
export type CoverPart = "labor" | "material" | "expense";
|
||||
@@ -470,7 +471,8 @@ export function hoView(
|
||||
const l = lineBy.get(cid);
|
||||
const name = l?.이름 ?? cid;
|
||||
const at = subOf(cid);
|
||||
const tag = tags.has(cid) ? { 꼬리표: tags.get(cid) } : {};
|
||||
const born = bornOfCid(cid, doc.더한줄, tags.get(cid) ?? ""); // 넣은 줄 출처(740-2) — 기초 번호 뒤에 붙임
|
||||
const tag = born ? { 꼬리표: born } : {};
|
||||
if ((at?.cid ?? cid).startsWith("pct:")) {
|
||||
extra.push([cid, { ...((known.get(cid) as HoExtra) ?? { 이름: name, 식: "" }), ...tag }]);
|
||||
continue;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
조합 알약 배(`M01_MasterData_Store_Combo`) · 엑셀 맞춤 배(`M02_Hopyo_Ours` · `_Match`) ·
|
||||
표 기준배(`master_basis`) · 계산서 줄 환산 근거(714-1). 서버 단독(③) — TS 는 서버가 준 글만 씀.
|
||||
- 단 SK 단위 바꿈 환산(`swap` · 728-1)은 짝(①) — `A00_Common/master_unit_swap.ts` · 거울 시험 `test_728_1_unit_swap.py`.
|
||||
- 개수 묶음(개 · EA · 본 · 매) = 1 : 1 같은 성격 · 조 · 세트는 묶음 밖(한 조 = 여럿).
|
||||
- 개수 묶음(개 · EA · 본 · 매 · 개소) = 1 : 1 같은 성격 · hr = 시간(별칭) · 조 · 세트는 묶음 밖(한 조 = 여럿).
|
||||
- 부피 ↔ 무게 = 단위중량 표 CC000337 에서 고른 줄(`unit_weight`)을 받을 때만 · 기본 줄 없음
|
||||
(사용자 확인 거리) · 밀도(kg ↔ ℓ) · ㎾h 등은 환산 없이 경고 글만.
|
||||
"""
|
||||
@@ -17,7 +17,16 @@ import unicodedata
|
||||
from decimal import Decimal
|
||||
|
||||
# 맞대기 꼴(`plain`) 별칭 — NFKC · 소문자 · 빈칸 없앤 뒤
|
||||
ALIAS = {"ea": "개", "ton": "톤", "t": "톤", "l": "ℓ", "리터": "ℓ", "set": "조", "세트": "조"}
|
||||
ALIAS = {
|
||||
"ea": "개",
|
||||
"ton": "톤",
|
||||
"t": "톤",
|
||||
"l": "ℓ",
|
||||
"리터": "ℓ",
|
||||
"set": "조",
|
||||
"세트": "조",
|
||||
"hr": "시간",
|
||||
}
|
||||
# 보이는 꼴(`shown`) 별칭 — 표 기준 · 결과단위 분모(원문 기호 ㎡ · ㎥ 그대로)
|
||||
SHOWN = {"ea": "개", "톤": "ton", "t": "ton", "㎞": "km", "hr": "시간", "min": "분", "㎏": "kg"}
|
||||
# 같은 성격 — {성격: {맞대기 단위: 기준 단위 몇 배}}
|
||||
@@ -26,7 +35,7 @@ UNITS = {
|
||||
"길이": {"mm": 1, "cm": 10, "m": 1000, "km": 1000000},
|
||||
"넓이": {"cm2": 1, "m2": 10000},
|
||||
"부피": {"cm3": 1, "ℓ": 1000, "m3": 1000000},
|
||||
"개수": {"개": 1, "본": 1, "매": 1},
|
||||
"개수": {"개": 1, "본": 1, "매": 1, "개소": 1},
|
||||
}
|
||||
_SCALE = {u: (kind, n) for kind, group in UNITS.items() for u, n in group.items()}
|
||||
WEIGHT_TABLE = "CC000337" # 건설품셈 공통 1-2-4 재료의 단위중량(㎏/㎥)
|
||||
|
||||
@@ -96,6 +96,7 @@ CALC = {"줄": [{"로직": "GF1", "분모": "㎡", "배": 10, "수량": 2.09,
|
||||
def test_M02_문서_calc_를_격자_줄로():
|
||||
files = [GRID, "M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts",
|
||||
"M02_MasterTemplete/M02_MasterTemplete_Labor_Spec.ts",
|
||||
"M02_MasterTemplete/logic/M02_Born_Tag.ts",
|
||||
"M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts"] # fmt: skip
|
||||
logics = [
|
||||
["GF1", {"이름": "돌쌓기 찰쌓기", "입력": [], "결과단위": "㎡"}],
|
||||
@@ -146,6 +147,7 @@ def test_세_곳_같은_부품():
|
||||
def test_번호는_글에_안_냄_사람_글은_그대로():
|
||||
files = [GRID, "M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts",
|
||||
"M02_MasterTemplete/M02_MasterTemplete_Labor_Spec.ts",
|
||||
"M02_MasterTemplete/logic/M02_Born_Tag.ts",
|
||||
"M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts"] # fmt: skip
|
||||
ref = {"로직": "", "별칭": None, "입력값": {}, "수량": {"손": 1},
|
||||
"참조": {"층": "personal", "주인": "1/3", "키": "SC-000002"}} # fmt: skip
|
||||
|
||||
@@ -176,6 +176,7 @@ def test_격자_규격_열_수량_칸_열마다():
|
||||
|
||||
files = [g.GRID, "M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts",
|
||||
"M02_MasterTemplete/M02_MasterTemplete_Labor_Spec.ts",
|
||||
"M02_MasterTemplete/logic/M02_Born_Tag.ts",
|
||||
"M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts"] # fmt: skip
|
||||
doc = {**g.DOC, "규격": {"이름": "H", "열": ["1.5", "2.0"], "기본": "2.0",
|
||||
"수량": {"GF1": {"1.5": 1.5, "2.0": 2.09}, "MT1": {"1.5": 4}}}} # fmt: skip
|
||||
|
||||
@@ -94,10 +94,9 @@ def test_실무_xlsx_넷_붙여넣기_만듦_합계_같음(client, tag):
|
||||
assert got["같음"] == FILES[tag][1] and all(abs(x["차이"]) <= 1 for x in got["다름"])
|
||||
assert len(got["만듦"]) == got["호표"] and got["고정"] + got["호표참조"] == got["줄"]
|
||||
assert master_hash() == before
|
||||
# 718-1 — 붙여넣기 실무자료 = 지문 없음 → 불러오기 목록엔 숨김 · 파일은 남음
|
||||
# 743-1 — 붙여넣기 실무자료 = 만든 뒤 지움(엑셀 보관 안 함)
|
||||
assert client.get("/api/m02/layers/personal/hopyo").json()["줄"] == []
|
||||
doc = client.get(f"/api/m02/layers/personal/hopyo/{got['키']}").json()
|
||||
assert doc["파일"].startswith("붙여넣기") and doc["시트"] == "붙여넣기"
|
||||
assert client.get(f"/api/m02/layers/personal/hopyo/{got['키']}").status_code == 404
|
||||
|
||||
|
||||
def _block(rows, no):
|
||||
|
||||
@@ -181,8 +181,8 @@ def test_내_자료집에_저장_하위_호표_같이_한_번(world, monkeypatch
|
||||
saved = {p.stem: json.loads(p.read_text("utf-8")) for p in (folder / "precombo").glob("*.json")}
|
||||
for key in keys:
|
||||
for one in saved[key]["담은로직"]:
|
||||
if "참조" in one:
|
||||
assert one["참조"]["층"] == "personal" and one["참조"]["키"] in saved
|
||||
if "참조" in one: # 740-1 하위 호표 = 사본 블록(본뜬 = 같이 만든 개인 SC)
|
||||
assert one["참조"]["층"] == "사본" and one["본뜬"]["키"] in saved
|
||||
copied = [d["본뜬"]["복사원본"] for d in saved.values() if "복사원본" in (d.get("본뜬") or {})]
|
||||
assert len(copied) == len({json.dumps(x, sort_keys=True) for x in copied}) # 같은 하위 한 번
|
||||
for p in (folder / "precombo").glob("*.json"): # 다음 시험에 층 쓰기 안 남김
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from decimal import Decimal
|
||||
@@ -32,7 +33,9 @@ def _text(p: Path) -> str:
|
||||
|
||||
|
||||
def _node(body: str) -> object:
|
||||
src = _text(MODEL) + "\n" + body
|
||||
born = _text(M02 / "logic" / "M02_Born_Tag.ts") # 740-2 — 모델이 쓰는 꼬리표 글(import 줄은 뺌)
|
||||
model = re.sub(r"^import .*$", "", _text(MODEL), flags=re.M)
|
||||
src = born + "\n" + model + "\n" + body
|
||||
out = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", src],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""일위대가 불러오기 한시(PLAN 718-1) — 올림 지문 · [끝내기] 지움 · 겹침 막음.
|
||||
"""일위대가 불러오기 한시(PLAN 718-1 · 743-1) — 올림 지문 · [끝내기] 지움.
|
||||
|
||||
같은 엑셀 작업 중 다시 올림 = 그 파일 이어서(새 키 없음) · [끝내기] = HP · 격자 · 맞춤셈 캐시 0 ·
|
||||
만든 일위대가 · 별칭 장부 그대로 · 끝낸 같은 엑셀 다시 올림 409 · 만든 것 없이 지움 = 다시 올림 됨 ·
|
||||
만든 일위대가 · 별칭 장부 그대로 · 끝낸 같은 엑셀 다시 올림 = 새 키(743-1 · 409 걷음) ·
|
||||
옛 HP(지문 없음) = 파일 그대로 목록에서만 숨김 · 키 번 = 지운 뒤에도 다시 안 씀. 층 = 임시 폴더.
|
||||
"""
|
||||
|
||||
@@ -44,9 +44,9 @@ def test_이어서_끝내기_지움_겹침_막음(client):
|
||||
assert hs.memory(folder) == {"장부 줄": {"갈래": "안 씀"}}
|
||||
assert sorted((folder / "precombo").glob("SC-*.json")) == sc_before # 만든 일위대가 그대로
|
||||
|
||||
block = _post(client) # 끝낸 같은 엑셀 = 막음 · 안내
|
||||
assert block.status_code == 409 and "이미 불러온 엑셀" in block.json()["detail"]
|
||||
assert f"단가산출 {n}" in block.json()["detail"] and _keys(client) == []
|
||||
again = _post(client) # 743-1 — 끝낸 같은 엑셀 = 다시 올림 됨(새 키)
|
||||
assert again.status_code == 200 and again.json()["키"] == "HP-000002"
|
||||
assert _keys(client) == ["HP-000002"]
|
||||
assert sorted((folder / "precombo").glob("SC-*.json")) == sc_before
|
||||
|
||||
|
||||
@@ -88,6 +88,6 @@ def test_결과_단가산출_산근_블록_기초_SK_고름대기_겹침_없음(
|
||||
again = client.post(url).json() # 다시 = 그대로(겹침 없음)
|
||||
assert again["만듦"] == [] and again["그대로"] == len(got["만듦"])
|
||||
res = client.delete(URL.format("personal") + "/HP-000001").json()
|
||||
assert res["만든"] == len(got["만듦"]) # 단가산출만 만들어도 지문 기록(블록마다)
|
||||
assert res["만든"] == len(docs) # 같은 내용 SK 한 벌 — 사본 걷은 문서 수
|
||||
assert len(list((folder / "sangeun").glob("SK-*.json"))) == len(docs) # 남음
|
||||
assert _post(client).status_code == 409
|
||||
assert _post(client).status_code == 200 # 743-1 — 다시 올림 됨
|
||||
|
||||
@@ -206,7 +206,13 @@ def _model(ans, doc, script):
|
||||
f"\nconst ans = {json.dumps(ans, ensure_ascii=False)};"
|
||||
f"\nconst doc = {json.dumps(doc, ensure_ascii=False)};\n{script}"
|
||||
)
|
||||
return _node(["M02_MasterTemplete/logic/M02_Sangeun_Cover_Model.ts"], body)
|
||||
return _node(
|
||||
[
|
||||
"M02_MasterTemplete/logic/M02_Born_Tag.ts",
|
||||
"M02_MasterTemplete/logic/M02_Sangeun_Cover_Model.ts",
|
||||
],
|
||||
body,
|
||||
)
|
||||
|
||||
|
||||
def test_727_2_화면_꼴_세부_줄_고침(world): # noqa: F811
|
||||
|
||||
@@ -40,6 +40,9 @@ CASES = [
|
||||
["톤", "㎥", 1600, None],
|
||||
["10㎥", "kg", 2400, None],
|
||||
["조", "개", None, None],
|
||||
["hr", "시간", None, None], # 746-1 별칭
|
||||
["개소", "개", None, None],
|
||||
["10개소", "개", None, None],
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -93,23 +93,23 @@ def test_공유_승인_회사_같은_내용_409_씀_새로(world):
|
||||
|
||||
|
||||
def test_복사_딸림_같은_원본_재사용(world):
|
||||
"""구조물 조합 복사 딸림(733-2) = 같은 원본 사본 재사용 · 740-1 — 일위대가 안 일위대가는
|
||||
넣을 때 사본 블록(딸림 없음)."""
|
||||
client = _client(world)
|
||||
_as(world)
|
||||
mine = layers.personal_dir(7, 42)
|
||||
url = LIB.format("personal", "precombo")
|
||||
inner = client.post(url, json=_doc("안")).json()["이름"]
|
||||
ref = {"층": "personal", "주인": "7/42", "키": inner}
|
||||
outer = {"문서": {"이름": "밖", "단위": "㎡", "담은로직": [{"참조": ref}]}}
|
||||
top = client.post(url, json=outer).json()["이름"]
|
||||
with store.at(mine):
|
||||
doc = store.read("precombo", top)["문서"]
|
||||
src = {"층": "personal", "주인": "7/42", "키": top}
|
||||
with store.at(layers.personal_dir(7, 42)):
|
||||
assert store.read("precombo", top)["문서"]["담은로직"][0]["참조"]["층"] == "사본"
|
||||
company = layers.company_dir(7)
|
||||
combo = {"종류": "일위대가", "열": "rv", "담은로직": [{"참조": ref}], "자재": []}
|
||||
with store.at(company):
|
||||
one = refs.create("precombo", refs.carry(refs.origin_mark(doc, src), ("personal", "7/42")))
|
||||
assert _count(company) == 2 # 밖 + 딸린 안
|
||||
two = refs.create("precombo", refs.carry(refs.origin_mark(doc, src), ("personal", "7/42")))
|
||||
assert _count(company) == 3 # 다시 복사 = 밖만 새로 · 딸린 안 = 앞 사본 재사용
|
||||
with store.at(company):
|
||||
got = [store.read("precombo", m["이름"])["문서"]["담은로직"][0]["참조"] for m in (one, two)]
|
||||
one = refs.carry(combo, ("personal", "7/42"))
|
||||
assert _count(company) == 1 # 딸린 안 = 회사 새 키
|
||||
two = refs.carry(combo, ("personal", "7/42"))
|
||||
assert _count(company) == 1 # 다시 복사 = 앞 사본 재사용
|
||||
got = [x["담은로직"][0]["참조"] for x in (one, two)]
|
||||
assert got[0] == got[1] and got[0]["층"] == "company"
|
||||
|
||||
@@ -136,7 +136,9 @@ def test_폴더_통째_한_기록_되돌림_자리까지_그새_고친_문서는
|
||||
a = _sc(me, "가", [FIX])
|
||||
b = _sc(me, "나", [FIX])
|
||||
_labor(me, [{"참조": {**MINE, "키": a}, "수량": {"손": 1}}, FIX])
|
||||
other = _sc(me, "다른", [{"참조": {**MINE, "키": b}}, FIX])
|
||||
with store.at(me): # 쓰는 곳 = 구조물 조합(740-1 — 일위대가 안 일위대가는 사본 블록)
|
||||
other = [{"참조": {**MINE, "키": b}, "수량": {"손": 1}}, FIX]
|
||||
store.write("labor", "x2", "", {"열": "x2", "담은로직": other, "자재": []})
|
||||
assert _tree(api, op="add-folder", 경로=[], 이름="묶음").status_code == 200
|
||||
for key in (a, b):
|
||||
assert _tree(api, op="move", 키=key, 새경로=["묶음"]).status_code == 200
|
||||
@@ -147,9 +149,10 @@ def test_폴더_통째_한_기록_되돌림_자리까지_그새_고친_문서는
|
||||
assert res.status_code == 200, res.text
|
||||
name = res.json()["알림"]["기록"]
|
||||
assert [g["키"] for g in api.get(SAME).json()["합친것"]] == [name]
|
||||
with store.at(me): # 그새 「다른」 을 고침 → 그 문서는 앞판으로 안 돌림
|
||||
have = store.read("precombo", other)
|
||||
store.write("precombo", other, have["판"], {**have["문서"], "이름": "다른 고침"})
|
||||
with store.at(me): # 그새 x2 를 고침 → 그 문서는 앞판으로 안 돌림
|
||||
have = store.read("labor", "x2")
|
||||
lines = [{**x, "메모": "고침"} for x in have["문서"]["담은로직"]]
|
||||
store.write("labor", "x2", have["판"], {**have["문서"], "담은로직": lines})
|
||||
back = api.post(SAME + "/undo", json={"키": name}).json()
|
||||
assert back["되돌린곳수"] == 1 and "그새 고침" in back["못한곳"][0]
|
||||
assert (me / "precombo" / f"{a}.json").is_file() and (me / "precombo" / f"{b}.json").is_file()
|
||||
|
||||
@@ -140,7 +140,8 @@ def test_SC_에_SK_패치_겹침_E1_원본_고쳐도_그대로(world):
|
||||
doc = _read(me, "precombo", host)
|
||||
assert doc["담은로직"][0]["로직"] == PCT and doc["담은로직"][0]["본뜬"]["키"] == sk
|
||||
assert doc["담은로직"][0]["패치"]["덮개"] == {"labor:LB000036#0": {"qty": 0.5}}
|
||||
assert doc["담은로직"][1]["참조"]["키"] == inner # 하위 호표(SC 안 SC) = 참조 그대로
|
||||
assert doc["담은로직"][1]["참조"]["층"] == "사본" # 하위 호표(SC 안 SC) = 사본 블록
|
||||
assert doc["담은로직"][1]["본뜬"]["키"] == inner
|
||||
assert _sc_value(me, host) == pytest.approx(before, rel=1e-9)
|
||||
with store.at(me): # E1 — 원본 SK 의 덮지 않은 줄 품 2
|
||||
now = store.read("sangeun", sk)
|
||||
@@ -151,33 +152,28 @@ def test_SC_에_SK_패치_겹침_E1_원본_고쳐도_그대로(world):
|
||||
assert _sc_value(me, host) == pytest.approx(before, rel=1e-9) # 펼친 것 = 그대로
|
||||
|
||||
|
||||
def test_SC_에_SC_펴넣기_실무수량_겹침_넣기_400(world):
|
||||
def test_SC_에_SC_사본_블록_실무수량_겹침_원본_지워도(world):
|
||||
"""하위 호표(SC 안 SC) = 사본 블록 — 원본 연결 없음 · 금액 같음 · 같은 기초 겹침도 블록 안."""
|
||||
me = world["personal"]
|
||||
hands = {"층": "실무자료", "실무수량": {"줄": {"0": 3}, "자재": {}}}
|
||||
inner = _make(me, "precombo", 이름="안", 담은로직=[{"로직": PCT}], 본뜬=hands)
|
||||
outer_hands = {"층": "실무자료", "실무수량": {"줄": {"0": 2, "1": 5}, "자재": {}}}
|
||||
lines = [{"로직": GF}, {"참조": {**MINE, "키": inner}, "별칭": "안쪽"}]
|
||||
old = _raw(me, "precombo", 이름="옛", 담은로직=lines, 본뜬=outer_hands)
|
||||
assert flat.flat_doc("precombo", _read(me, "precombo", old))["담은로직"][1][
|
||||
"참조"
|
||||
] # 저장 · 읽기
|
||||
got = flat.flat_sc(_read(me, "precombo", old), refs.read_ref, _whole()) # 넣기 길 = 펴 넣음
|
||||
assert [x["로직"] for x in got["담은로직"]] == [GF, PCT] and got["담은로직"][1][
|
||||
"별칭"
|
||||
] == "안쪽"
|
||||
assert got["본뜬"]["실무수량"]["줄"] == {"0": 2, "1": 15} # 바깥 5 × 안 3
|
||||
|
||||
def mine(r):
|
||||
return got if r["키"] == old else refs.read_ref(r)
|
||||
|
||||
assert _sc_value(me, old, mine) == pytest.approx(_sc_value(me, old), rel=1e-9)
|
||||
# 같은 기초가 겹침 — 넣기 길 = 400
|
||||
dup = [{"로직": PCT}, {"참조": {**MINE, "키": inner}}]
|
||||
kept = _make(me, "precombo", 이름="겹침", 담은로직=dup)
|
||||
assert _read(me, "precombo", kept)["담은로직"][1]["참조"]["키"] == inner
|
||||
with store.at(me), pytest.raises(store.StoreError) as e:
|
||||
flat.flat_lines("precombo", {**MINE, "키": inner}, None, {"담은로직": [{"로직": PCT}]})
|
||||
assert e.value.status == 400 and "겹침" in str(e.value.detail)
|
||||
lines = [{"로직": PCT}, {"참조": {**MINE, "키": inner}, "별칭": "안쪽"}] # 같은 기초 겹침
|
||||
old = _raw(me, "precombo", 이름="옛", 담은로직=lines)
|
||||
before = _sc_value(me, old)
|
||||
host = _make(me, "precombo", 이름="밖", 담은로직=lines)
|
||||
line = _read(me, "precombo", host)["담은로직"][1]
|
||||
block = line["참조"]
|
||||
assert block["층"] == "사본" and block["키"] == inner and line["본뜬"]["키"] == inner
|
||||
assert block["몸"]["담은로직"][0]["로직"] == PCT and line["별칭"] == "안쪽"
|
||||
assert block["몸"]["본뜬"]["실무수량"]["줄"] == {"0": 3} # 엑셀 대조 · 하위 호표 끝수
|
||||
assert _sc_value(me, host) == pytest.approx(before, rel=1e-9)
|
||||
with store.at(me):
|
||||
got = flat.flat_lines("precombo", {**MINE, "키": old}, None, {"담은로직": [{"로직": GF}]})
|
||||
(me / "precombo" / f"{old}.json").unlink() # 옛 꼴 문서(참조) 치움
|
||||
assert refs.users({**MINE, "키": inner}) == []
|
||||
store.delete("precombo", inner)
|
||||
assert _sc_value(me, host) == pytest.approx(before, rel=1e-9) # 원본 지워도 그대로
|
||||
assert got["담은로직"][0]["참조"]["층"] == "사본" # 넣기 길도 블록(겹침 없음)
|
||||
|
||||
|
||||
def test_SK_에_SC_줄마다_더한줄(world):
|
||||
@@ -192,10 +188,9 @@ def test_SK_에_SC_줄마다_더한줄(world):
|
||||
|
||||
hand = flat.sc_adds({"담은로직": [{"로직": GF, "수량": {"손": 3}}]}, Decimal(2), _whole(), {})
|
||||
assert hand[0]["수량"] == 6
|
||||
lines, _, _ = flat._spread(
|
||||
{"담은로직": [{"로직": GF, "수량": {"손": 3}}]}, {"수량": {"손": 2}}, None, {}
|
||||
)
|
||||
assert lines[0]["수량"] == {"손": 6}
|
||||
block = {"참조": refs.copy_ref({"담은로직": [{"로직": GF, "수량": {"손": 3}}]}, "SC-000001")}
|
||||
deep = flat.sc_adds({"담은로직": [{**block, "수량": {"손": 2}}]}, Decimal(1), _whole(), {})
|
||||
assert deep[0]["로직"] == GF and deep[0]["수량"] == 6 # 사본 블록 = 바깥 × 안
|
||||
doc = {"키": "SK-000099", "이름": "x", "단위": "㎥", "바탕": None, "더한줄": got}
|
||||
one = float(labor.run_line(_whole(), GF, {})["기준"]["계"])
|
||||
assert _sk_total(me, doc) == pytest.approx(2 * one, rel=1e-9)
|
||||
@@ -267,7 +262,20 @@ def test_초기화_SK_SC_M01_값_옵션_남음(world):
|
||||
with store.at(me):
|
||||
got = flat.reset_doc("precombo", sc, None, False, None)
|
||||
line = _read(me, "precombo", sc)["담은로직"][0]
|
||||
assert got["걷힘"] == 2 and line["패치"] == {"더한줄": [{**ADD, "수량": 0.5}]}
|
||||
assert got["걷힘"] == 2 and line["패치"] == {"더한줄": [{**ADD, "수량": 0.5, "처음수량": 0.5}]}
|
||||
# 이음 기계 고정 줄(736-3) = M01 기계 한 시간 값 다시 받음
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as machine
|
||||
|
||||
hour = machine.hourly("EQ000019")
|
||||
fx = {"이름": "굴착기", "단위": "시간", "단가": {"노무비": 1, "재료비": 1, "경비": 1}}
|
||||
fx["이음"] = {"기계": "EQ000019"}
|
||||
mc = _make(me, "precombo", 이름="기계", 담은로직=[{"고정": fx}])
|
||||
with store.at(me):
|
||||
assert flat.reset_doc("precombo", mc, None, False, None)["걷힘"] == 1
|
||||
price = _read(me, "precombo", mc)["담은로직"][0]["고정"]["단가"]
|
||||
assert price["노무비"] == pytest.approx(hour["노무비"]) and price["경비"] == pytest.approx(
|
||||
hour["경비"]
|
||||
)
|
||||
|
||||
|
||||
def test_길_넣기_초기화_권한(world):
|
||||
@@ -289,7 +297,7 @@ def test_길_넣기_초기화_권한(world):
|
||||
assert firm.status_code == 403 # 일반 사용자 회사 층 쓰기 막힘
|
||||
|
||||
|
||||
def test_옮기기_함수_전후_같음_겹침_남김(world):
|
||||
def test_옮기기_함수_전후_같음_사본_블록(world):
|
||||
me = world["personal"]
|
||||
y = _y(me)
|
||||
inner = _make(me, "precombo", 이름="안", 담은로직=[{"로직": GF}])
|
||||
@@ -314,18 +322,18 @@ def test_옮기기_함수_전후_같음_겹침_남김(world):
|
||||
sk: _sk_total(me, _read(me, "sangeun", sk))
|
||||
}
|
||||
look = {r["키"]: r for r in flat.migrate(False)}
|
||||
assert look[sk]["같음"] and look[sc]["같음"] and not look[sc]["씀"]
|
||||
assert look[sc]["남은참조"] == look[dup]["남은참조"] == [inner] # 하위 호표 = 그대로
|
||||
assert all(look[k]["같음"] and look[k]["펼침"] and not look[k]["씀"] for k in (sk, sc, dup))
|
||||
rows = {r["키"]: r for r in flat.migrate(True)}
|
||||
assert rows[sk]["씀"] and rows[sc]["씀"] and not rows[dup]["씀"]
|
||||
assert all(rows[k]["씀"] for k in (sk, sc, dup))
|
||||
assert not refs.refs_in(json.loads((me / "sangeun" / f"{sk}.json").read_bytes()))
|
||||
left = refs.refs_in(json.loads((me / "precombo" / f"{sc}.json").read_bytes()))
|
||||
assert [r["키"] for r in left] == [inner]
|
||||
for key in (sc, dup): # 하위 호표 = 사본 블록만 남음(원본 연결 없음)
|
||||
left = refs.refs_in(json.loads((me / "precombo" / f"{key}.json").read_bytes()))
|
||||
assert [(r["층"], r["키"]) for r in left] == [("사본", inner)]
|
||||
assert _sk_total(me, _read(me, "sangeun", sk)) == pytest.approx(before[sk], rel=1e-9)
|
||||
assert _sc_value(me, sc) == pytest.approx(before[sc], rel=1e-9)
|
||||
assert _sc_value(me, dup) == pytest.approx(before[dup], rel=1e-9)
|
||||
again = flat.migrate(False) # 두 번째 = 하위 호표 참조만 남음(바꿀 것 없음)
|
||||
assert [r["키"] for r in again] == [sc, dup] and all(r["남은참조"] == [inner] for r in again)
|
||||
again = flat.migrate(True) # 두 번째 = 바꿀 것 없음
|
||||
assert all(r["펼침"] and not r["씀"] for r in again)
|
||||
|
||||
|
||||
def test_733_지문_옛_참조_펼친_것_같음(world):
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""740-2 · 738-2 상세 머리 [초기화] [삭제] · 출처 꼬리표 지킴 시험(서버 · 화면 없음)."""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
M02 = ROOT / "M02_MasterTemplete"
|
||||
MAIN = (M02 / "M02_MasterTemplete_UI_Main.ts").read_text(encoding="utf-8")
|
||||
ACTIONS = (M02 / "M02_MasterTemplete_Main_Actions.ts").read_text(encoding="utf-8")
|
||||
HO = (ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab_Ho.ts").read_text(encoding="utf-8")
|
||||
COVER = (M02 / "logic" / "M02_Sangeun_Cover_Model.ts").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _born(expr: str):
|
||||
"""순수 TS(`logic/M02_Born_Tag.ts`)를 Node 로 돌려 JSON 답을 받음."""
|
||||
uri = (M02 / "logic" / "M02_Born_Tag.ts").as_uri()
|
||||
code = (
|
||||
f"import {{ bornText, bornOfCid, bornNote }} from '{uri}';"
|
||||
f"console.log(JSON.stringify({expr}));"
|
||||
)
|
||||
out = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", "--experimental-strip-types", "--no-warnings", "--input-type=module", "-e", code],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
cwd=ROOT,
|
||||
)
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
BORN = {
|
||||
"층": "personal",
|
||||
"주인": "u1",
|
||||
"키": "SK-000012",
|
||||
"이름": "강관비계",
|
||||
"때": "2026-11-02T06:30:12Z",
|
||||
}
|
||||
|
||||
|
||||
def test_꼬리표_글_첫줄_짧게_둘째줄_풍선():
|
||||
shown, tip = _born(f"bornText({json.dumps(BORN, ensure_ascii=False)})").split("\n")
|
||||
assert shown == "본뜬 SK-000012"
|
||||
assert "SK-000012 강관비계" in tip and "개인" in tip
|
||||
assert re.search(r"\d{4}-\d\d-\d\d \d\d:\d\d", tip) # 이 컴퓨터 시각 · 시간대 따라 값이 다름
|
||||
|
||||
|
||||
def test_꼬리표_없으면_빈칸_기초번호_뒤에_붙음():
|
||||
assert _born("bornText(null)") == ""
|
||||
assert _born("bornText(undefined, 'A1')") == "A1"
|
||||
got = _born(f"bornText({json.dumps(BORN, ensure_ascii=False)}, 'A1')")
|
||||
assert got.split("\n")[0] == "A1 · 본뜬 SK-000012"
|
||||
|
||||
|
||||
def test_줄_열쇠에서_그_더한줄_출처를_찾음():
|
||||
adds = json.dumps([{}, {"본뜬": BORN}], ensure_ascii=False)
|
||||
assert _born(f"bornOfCid('add:#1', {adds})").startswith("본뜬 SK-000012")
|
||||
assert _born(f"bornOfCid('add:#1/add:#0', {adds})").startswith("본뜬 SK-000012") # 펼침 세부
|
||||
assert _born(f"bornOfCid('add:#0', {adds})") == ""
|
||||
assert _born(f"bornOfCid('hopyo:X#3', {adds})") == ""
|
||||
|
||||
|
||||
def test_머리_단추_차례_초기화_삭제_저장():
|
||||
kids = MAIN[MAIN.index('className: "m02-main__head"') :]
|
||||
kids = kids[: kids.index("]")]
|
||||
order = [kids.index(w) for w in ("reload,", "actions.reset,", "actions.del,", "save,")]
|
||||
assert order == sorted(order)
|
||||
assert "actions.update(" in MAIN # 쓰기 권한 없는 층 = 안 보임
|
||||
|
||||
|
||||
def test_삭제는_735_경고_창을_그대로_씀():
|
||||
assert "askLibraryDelete(" in ACTIONS and '"delete"' in ACTIONS
|
||||
assert "sure ? { 확인: true }" in ACTIONS.replace(
|
||||
"ans.sure", "sure"
|
||||
) # 확인 몸 = 쓰는 곳 줄까지
|
||||
assert "sangeunList(" in ACTIONS and "precomboList(" in ACTIONS # 지운 뒤 목록으로
|
||||
|
||||
|
||||
def test_초기화는_미리보기_뒤_물음_뒤_씀():
|
||||
assert ACTIONS.index("미리보기: true") < ACTIONS.index("미리보기: false")
|
||||
assert "ctx.confirmLeave()" in ACTIONS # 저장 안 한 고침 먼저 물음
|
||||
assert "옵션 · 기본정보는 그대로" in ACTIONS
|
||||
|
||||
|
||||
def test_공용_꼬리표_칸은_첫줄만_보이고_풍선에_나머지():
|
||||
assert 'text.split("\\n")' in HO
|
||||
assert "bornOfCid(" in COVER
|
||||
|
||||
|
||||
def test_격자_비고_칸은_첫줄만():
|
||||
assert _born(f"bornNote({json.dumps(BORN, ensure_ascii=False)})") == "본뜬 SK-000012"
|
||||
assert _born("bornNote(null)") == ""
|
||||
@@ -0,0 +1,157 @@
|
||||
"""불러오기 엑셀 보관 안 함(PLAN 743-1 · 요구 582-3 · 584-2 · `M02_Hopyo_Forget`).
|
||||
|
||||
[끝내기] = 남는 파일 0 · 만든 일위대가 · 단가산출 = 출처(파일 · 때 · 누가)만 · 금액 그대로 ·
|
||||
엑셀 사본(본뜬 키 · 원본 · 확인 · 품셈 · 줄 메모 「실무 …」) 걷음 ·
|
||||
실무수량 = 저장 수량과 같을 때만 걷음 · SK 또 = 그 HP 몫만 ·
|
||||
하루 지난 HP = 목록 숨김 → 올림 때 지움 · 옛 파일 정리 미리 보기(쓰기 0) → 고침.
|
||||
층 = 임시 폴더.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Hopyo_Forget as forget
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hs
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
from resources.tester.test_m02_hopyo_excel import URL, _post, client # noqa: F401
|
||||
|
||||
ALL = "/api/m02/layers/personal/hopyo-build-all"
|
||||
SK_URL = URL.format("personal") + "/HP-000001/sangeun"
|
||||
|
||||
|
||||
def _files(folder):
|
||||
return sorted(p.name for p in (folder / hs.DIRNAME).rglob("HP-*.json"))
|
||||
|
||||
|
||||
def _sums(folder):
|
||||
"""문서마다 1 단위 기준 계 — 끝내기 전후 같아야 함."""
|
||||
whole = refs.Expand(m01.loaded()[1])
|
||||
out = {}
|
||||
with store.at(folder):
|
||||
for kind in ("precombo", "sangeun"):
|
||||
for p in sorted((folder / kind).glob("*.json")):
|
||||
out[p.stem] = refs.summary(folder, kind, p.stem, whole).get("기준금액")
|
||||
return out
|
||||
|
||||
|
||||
def _docs(folder, kind):
|
||||
return {p.stem: json.loads(p.read_bytes()) for p in sorted((folder / kind).glob("*.json"))}
|
||||
|
||||
|
||||
# ── 순수 ──────────────────────────────────────────────────────────────
|
||||
SURE = {"누가": {"id": 42, "이름": "갑"}, "때": "2026-11-02T00:00:00Z"}
|
||||
HEAD = {"층": "실무자료", "주인": None, "키": "HP-000003 제2호표", "파일": "가.xls"}
|
||||
|
||||
|
||||
def test_SC_사본_걷음_출처만_실무수량은_저장_수량과_같을_때만():
|
||||
lines = [
|
||||
{"로직": "L1", "메모": "실무 0.3㎥ × 1 · 규격", "수량": {"손": 0.3}, "별칭": "엑셀 품"},
|
||||
{"로직": "L2", "메모": "사용자 메모", "수량": {"손": 2}},
|
||||
]
|
||||
doc = {
|
||||
"이름": "가",
|
||||
"담은로직": lines,
|
||||
"자재": [{"코드": "MT1", "수량": {"손": 5}}],
|
||||
"본뜬": {**HEAD, "원본": ["personal", "7/42"], "확인": SURE, "품셈": [0],
|
||||
"전국자재": [{"이름": "모래"}], "실무수량": {"줄": {"0": 0.3, "1": 2},
|
||||
"자재": {"MT1": 5}}},
|
||||
} # fmt: skip
|
||||
got = forget.strip_sc(doc)
|
||||
assert got["본뜬"] == {
|
||||
"층": "실무자료",
|
||||
"파일": "가.xls",
|
||||
"전국자재": [{"이름": "모래"}],
|
||||
"때": SURE["때"],
|
||||
"누가": SURE["누가"],
|
||||
}
|
||||
assert "메모" not in got["담은로직"][0] and got["담은로직"][1]["메모"] == "사용자 메모"
|
||||
assert got["담은로직"][0]["별칭"] == "엑셀 품" and got["담은로직"][0]["수량"] == {"손": 0.3}
|
||||
assert got["자재"] == doc["자재"]
|
||||
# 742 전 저장본(수량 null) = 실무수량이 셈 근거 → 남김
|
||||
old = {**doc, "담은로직": [{**x, "수량": None} for x in lines]}
|
||||
assert forget.strip_sc(old)["본뜬"]["실무수량"] == doc["본뜬"]["실무수량"]
|
||||
|
||||
|
||||
def test_SK_또_그_HP_몫만_걷음():
|
||||
other = {"키": "HP-000004 단산1", "원본": None, "갈래": "SK", "엑셀품": {}, "이름": "나"}
|
||||
doc = {"본뜬": {**HEAD, "키": "HP-000003 단산1", "확인": SURE, "갈래": "기초", "엑셀품": {},
|
||||
"못옮김": [], "또": [other]}} # fmt: skip
|
||||
got = forget.strip_sk(doc, lambda x: str(x.get("키")).startswith("HP-000003 "))
|
||||
assert got["본뜬"] == {**{"층": "실무자료", "파일": "가.xls"}, **SURE, "또": [other]}
|
||||
both = forget.strip_sk(doc, lambda x: True)
|
||||
assert "또" not in both["본뜬"] and "키" not in both["본뜬"]
|
||||
|
||||
|
||||
# ── 길 ───────────────────────────────────────────────────────────────
|
||||
def test_끝내기_남는_파일_0_금액_그대로_출처만_다시_올림(client):
|
||||
folder = layers.personal_dir(7, 42)
|
||||
assert _post(client).json()["키"] == "HP-000001"
|
||||
fit.run_job(folder, "HP-000001")
|
||||
assert client.post(SK_URL).status_code == 200
|
||||
made = client.post(ALL, json={"층": "personal", "키": "HP-000001"}).json()
|
||||
assert made["만듦"]
|
||||
before = _sums(folder) # 모두 고정 = 메모 없음(메모 걷음은 순수 시험)
|
||||
cache = fit.cache_path(folder, "HP-000001")
|
||||
assert cache.is_file()
|
||||
|
||||
res = client.delete(URL.format("personal") + "/HP-000001")
|
||||
assert res.status_code == 200
|
||||
assert _files(folder) == [] and not cache.exists() # HP · 격자 · 맞춤셈 캐시 0
|
||||
assert _sums(folder) == before # 금액 그대로
|
||||
for kind in ("precombo", "sangeun"):
|
||||
for key, doc in _docs(folder, kind).items():
|
||||
got = doc["본뜬"]
|
||||
assert got["층"] == "실무자료" and got["파일"] and got["때"], key
|
||||
assert not {"키", "원본", "확인", "품셈", "갈래", "엑셀품", "못옮김", "또"} & set(got)
|
||||
lines = doc.get("담은로직") or []
|
||||
assert not any(str(x.get("메모", "")).startswith("실무 ") for x in lines)
|
||||
assert hs.done(folder)["지문"] == {} # 끝낸 기록 없음
|
||||
again = _post(client) # 같은 엑셀 다시 올림 = 새 키(409 없음)
|
||||
assert again.status_code == 200 and again.json()["키"] == "HP-000002"
|
||||
|
||||
|
||||
def test_하루_지난_HP_목록_숨김_올림_때_지움(client):
|
||||
folder = layers.personal_dir(7, 42)
|
||||
assert _post(client).json()["키"] == "HP-000001"
|
||||
path = folder / hs.DIRNAME / "HP-000001.json"
|
||||
doc = json.loads(path.read_bytes())
|
||||
old = datetime.now(timezone.utc) - timedelta(days=1, minutes=1)
|
||||
doc["올린때"] = old.replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
path.write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8")
|
||||
assert client.get(URL.format("personal")).json()["줄"] == [] # 숨김
|
||||
assert forget.expired(folder) == ["HP-000001"]
|
||||
got = _post(client).json() # 같은 엑셀이어도 지난 것 = 지우고 새로(이어서 아님)
|
||||
assert got["키"] == "HP-000002" and not got.get("이어서")
|
||||
assert _files(folder) == ["HP-000002.json", "HP-000002.json"] # 이번 것 + 격자
|
||||
|
||||
|
||||
def test_옛_파일_정리_미리_보기_쓰기_0_고침(client):
|
||||
folder = layers.personal_dir(7, 42)
|
||||
assert _post(client).json()["키"] == "HP-000001" # 작업 중 — 그대로
|
||||
home = folder / hs.DIRNAME
|
||||
(home / "HP-000007.json").write_text(json.dumps({"키": "HP-000007", "호표": []}), "utf-8")
|
||||
hs._put_done(folder, {"번": 7, "지문": {"ab": {"파일": "옛.xls", "만든": ["SC-000001"]}}})
|
||||
(folder / "precombo").mkdir(exist_ok=True)
|
||||
gone = {"이름": "옛", "단위": "개", "담은로직": [{"로직": "L", "메모": "실무 1개 × 1"}],
|
||||
"본뜬": {**HEAD, "키": "HP-000005 제1호표", "확인": SURE}} # fmt: skip
|
||||
live = {**gone, "본뜬": {**HEAD, "키": "HP-000001 제1호표", "확인": SURE}}
|
||||
for key, doc in (("SC-000001", gone), ("SC-000002", live)):
|
||||
text = json.dumps(doc, ensure_ascii=False)
|
||||
(folder / "precombo" / f"{key}.json").write_text(text, "utf-8")
|
||||
snap = {p: p.read_bytes() for p in folder.rglob("*.json")}
|
||||
|
||||
rows = forget.tidy(folder)
|
||||
assert [r["대상"] for r in rows] == ["실무자료/HP-000007", "불러옴.json", "SC-000001"]
|
||||
assert {p: p.read_bytes() for p in folder.rglob("*.json")} == snap # 미리 보기 = 쓰기 0
|
||||
|
||||
assert forget.tidy(folder, apply=True) == rows
|
||||
assert _files(folder) == ["HP-000001.json", "HP-000001.json"]
|
||||
assert hs.done(folder) == {"번": 7, "지문": {}}
|
||||
docs = _docs(folder, "precombo")
|
||||
assert "키" not in docs["SC-000001"]["본뜬"] and "메모" not in docs["SC-000001"]["담은로직"][0]
|
||||
assert docs["SC-000002"] == live # 작업 중 HP 것 그대로
|
||||
assert forget.tidy(folder) == [] # 두 번째 = 할 일 없음
|
||||
@@ -0,0 +1,89 @@
|
||||
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
||||
"""744-1 서버 — 둘째 기초 세부 품 원문 · 되돌리기 기억(임시 storage · M01 읽기만).
|
||||
|
||||
- 둘째 기초 `펼침` 줄 `원문` = 그 기초 1 기준단위 몫(GF000316 품 그대로) · 줄 수량 · 금액 = 머리
|
||||
수량 곱한 몫(상자 합) 그대로 · 머리 수량 = 더한줄 수량
|
||||
- 더한줄 `처음수량` = 넣을 때 수량(없으면 저장 때 지금 수량) · 셈 · 지문 안 듦
|
||||
- `뺀더한줄 [{차례, 줄}]` = [기초 빼기] · 더한 항목 빼기 기억 · 셈 · 지문 안 듦 · 못 읽는 줄 버림 ·
|
||||
일위대가 줄 패치도 같음
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Library_Same as same
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
from resources.tester.test_m02_labor_ref import world # noqa: F401
|
||||
|
||||
GF = "GF000099" # 원/㎥
|
||||
BIG = "GF000316" # 기준단위 100㎥ — ㎥ 바탕에서 처음 수량 0.01
|
||||
PCT = "GC001109"
|
||||
MAN = {"종류": "인력", "요소": "LB000002", "수량": 1}
|
||||
|
||||
|
||||
def _sk(folder, **doc):
|
||||
with store.at(folder):
|
||||
made = refs.create("sangeun", {"이름": "744", "단위": "㎥", "더한줄": [], **doc})
|
||||
return store.read("sangeun", made["이름"])["문서"]
|
||||
|
||||
|
||||
def _calc(folder, doc):
|
||||
with store.at(folder):
|
||||
return calc.calc_doc(doc)
|
||||
|
||||
|
||||
def test_둘째_기초_세부_원문_품_머리_수량(world):
|
||||
doc = _sk(
|
||||
world["personal"], 바탕={"로직": GF}, 더한줄=[{"종류": "로직", "로직": BIG, "수량": 0.01}]
|
||||
)
|
||||
got = _calc(world["personal"], doc)
|
||||
head = next(x for x in got["줄"] if x["cid"] == "add:#0")
|
||||
m01_lines = labor.run_line(m01.loaded()[1], BIG, {})["줄"]
|
||||
assert head["수량"] == pytest.approx(0.01)
|
||||
for row, line in zip(head["펼침"], m01_lines):
|
||||
assert row["원문"]["수량"] == pytest.approx(float(line["수량"]), rel=1e-9) # 원문 그대로
|
||||
assert row["수량"] == pytest.approx(float(line["수량"]) * 0.01, rel=1e-9) # 상자 몫
|
||||
assert sum(x["금액"] for x in head["펼침"]) == pytest.approx(head["금액"], rel=1e-9)
|
||||
assert sum(x["원문"]["금액"] for x in head["펼침"]) * 0.01 == pytest.approx(head["금액"])
|
||||
|
||||
|
||||
def test_처음수량_넣을때_값_셈_지문_안_듦(world):
|
||||
me = world["personal"]
|
||||
plain = _sk(me, 바탕={"로직": PCT}, 단위="㎡", 더한줄=[{**MAN, "수량": 3}])
|
||||
assert plain["더한줄"][0]["처음수량"] == 3 # 없으면 저장 때 지금 수량
|
||||
kept = _sk(me, 바탕={"로직": PCT}, 단위="㎡", 더한줄=[{**MAN, "수량": 3, "처음수량": 1}])
|
||||
assert kept["더한줄"][0]["처음수량"] == 1
|
||||
assert _calc(me, kept)["기준"]["계"] == _calc(me, plain)["기준"]["계"]
|
||||
assert same.finger("sangeun", kept) == same.finger("sangeun", plain)
|
||||
|
||||
|
||||
def test_뺀더한줄_기억_셈_지문_안_듦_못읽는_줄_버림(world):
|
||||
me = world["personal"]
|
||||
gone = [
|
||||
{"차례": 0, "줄": {"종류": "로직", "로직": BIG, "수량": 0.01, "덮개": {}}},
|
||||
{"차례": 1, "줄": MAN},
|
||||
{"차례": -1, "줄": MAN}, # 차례 틀림
|
||||
{"차례": 2, "줄": {"종류": "로직", "로직": "GX999999", "수량": 1}}, # 없는 기초
|
||||
{"차례": 3}, # 줄 없음
|
||||
]
|
||||
doc = _sk(me, 바탕={"로직": GF}, 뺀더한줄=gone)
|
||||
assert [x["차례"] for x in doc["뺀더한줄"]] == [0, 1]
|
||||
assert doc["뺀더한줄"][1]["줄"]["요소"] == "LB000002"
|
||||
bare = _sk(me, 바탕={"로직": GF})
|
||||
assert _calc(me, doc)["기준"]["계"] == _calc(me, bare)["기준"]["계"]
|
||||
assert same.finger("sangeun", doc) == same.finger("sangeun", bare)
|
||||
|
||||
|
||||
def test_일위대가_줄_패치_기억_처음수량(world):
|
||||
me = world["personal"]
|
||||
patch = {"더한줄": [{**MAN, "수량": 2}], "뺀더한줄": [{"차례": 0, "줄": MAN}]}
|
||||
with store.at(me):
|
||||
sc = refs.create(
|
||||
"precombo", {"이름": "744", "단위": "㎡", "담은로직": [{"로직": PCT, "패치": patch}]}
|
||||
)
|
||||
line = store.read("precombo", sc["이름"])["문서"]["담은로직"][0]
|
||||
assert line["패치"]["더한줄"][0]["처음수량"] == 2
|
||||
assert line["패치"]["뺀더한줄"][0]["줄"]["요소"] == "LB000002"
|
||||
@@ -0,0 +1,104 @@
|
||||
"""746-1 지킴 — 불러오기 고름 대기 후보 블록 줄비교 · 못옮김 · 기계 줄 M01 요소 후보 · 단위 별칭(요구 585).
|
||||
|
||||
고름 대기 블록(북부청 임도 실물 엑셀 · 읽기만) = 판정 · 후보 차례 그대로 + 앞 셋 `줄비교`(같음 · 품다름 · 엑셀만 ·
|
||||
기초만 · 엑셀 품 = 엑셀 지문 품) · 그 기초로 고르면 만드는 SK 와 같은 셈(더한줄 · 뺀줄 수) · 요소 후보 셋 ·
|
||||
hr = 시간 · 개소 = 개(같은 성격). 쓰기 없음 · master_data 원본 그대로.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Ours as ours
|
||||
from M02_MasterTemplete import M02_Hopyo_Sangeun as sg
|
||||
from M02_MasterTemplete import M02_Hopyo_SkLink as sklink
|
||||
from M02_MasterTemplete import M02_Hopyo_SkPatch as skp
|
||||
from M02_MasterTemplete import M02_Hopyo_Verdict as verdict
|
||||
from resources.tester import helper_m02_hopyo_golden as golden
|
||||
from resources.tester.test_m02_hopyo_excel import master_hash
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "resources/master_data/scripts"))
|
||||
import master_unit as mu # noqa: E402
|
||||
|
||||
FILE, BLOCK = "북부청임도", 38 # 고름 대기 · 1위와 견줌 되는 블록
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def wait():
|
||||
before = master_hash()
|
||||
if not ours.ready():
|
||||
ours.build()
|
||||
blocks, first = sg.calls(golden.read(FILE))
|
||||
block, line = blocks[BLOCK], first[BLOCK]
|
||||
entry = verdict.block(block, line, hm.targets())
|
||||
got = skp.plan(entry, block, line, m01.loaded()[1])
|
||||
yield {"entry": entry, "block": block, "line": line, "plan": got}
|
||||
assert master_hash() == before
|
||||
|
||||
|
||||
def test_판정_후보_차례_그대로_앞_셋_줄비교(wait):
|
||||
got, entry = wait["plan"], wait["entry"]
|
||||
assert got["갈래"] == skp.WAIT
|
||||
assert [c["로직"] for c in got["후보"]] == [c["로직"] for c in entry["후보"][: len(got["후보"])]]
|
||||
assert all("줄비교" in c for c in got["후보"][: skp.COMPARE])
|
||||
assert all("줄비교" not in c for c in got["후보"][skp.COMPARE :])
|
||||
first = got["후보"][0]["줄비교"]
|
||||
assert first and first["줄"]
|
||||
assert sum(first["수"].values()) == len(first["줄"])
|
||||
assert {x["꼴"] for x in first["줄"]} <= set(skp.LOOKS)
|
||||
|
||||
|
||||
def test_줄비교_엑셀_품_지문_그대로_고르면_같은_셈(wait):
|
||||
first = wait["plan"]["후보"][0]
|
||||
rows = first["줄비교"]["줄"]
|
||||
efp = finger.excel(wait["block"], wait["line"])
|
||||
labor = {t: x["품"] for t, x in efp["인력"].items() if x.get("품")}
|
||||
for x in rows:
|
||||
if x["갈래"] == "인력":
|
||||
assert x["엑셀"] == pytest.approx(labor.get(x["이름"]) or None), x
|
||||
assert x["단위"] == "인"
|
||||
if x["꼴"] == "같음":
|
||||
assert x["엑셀"] == pytest.approx(x["기초"], rel=0.01), x
|
||||
if x["꼴"] == "엑셀만":
|
||||
assert x["엑셀"] and not x["기초"]
|
||||
if x["꼴"] == "기초만":
|
||||
assert x["기초"] and not x["엑셀"]
|
||||
made = skp.plan(
|
||||
wait["entry"], wait["block"], wait["line"], m01.loaded()[1], {"로직": first["로직"]}
|
||||
)
|
||||
gone = sum(1 for x in rows if x["꼴"] == "기초만" and x["갈래"] != "자재")
|
||||
assert len(made["뺀줄"]) == gone
|
||||
assert made["못옮김"] == first["줄비교"]["못옮김"]
|
||||
|
||||
|
||||
def test_요소_후보_셋():
|
||||
got = elem.labor("작업반장")
|
||||
assert 0 < len(got) <= elem.TOP and {x["이름"] for x in got[:2]} == {"작업반장"}
|
||||
assert all(x["키"].startswith("LB") and x["단가"] for x in got)
|
||||
got = elem.machines("굴착기 0.7㎥")
|
||||
assert got and all(x["키"].startswith("EQ") and "굴착기" in x["이름"] for x in got)
|
||||
got = elem.materials("시멘트", "kg")
|
||||
assert got and all(x["키"].startswith("MT") for x in got) and len(got) <= elem.TOP
|
||||
lost = {"갈래": "기계", "이름": "굴착기", "규격": "0.7"}
|
||||
assert elem.lost(lost)[0]["키"].startswith("EQ")
|
||||
|
||||
|
||||
def test_안_이은_기계_줄_EQ_후보():
|
||||
line = {"차례": 0, "이름": "굴착기(타이어)", "규격": "0.7㎥", "단위": "hr", "갈래": "기계"}
|
||||
got = sklink.left(line, None)
|
||||
assert got["까닭"] == sklink.WHY["기계"]
|
||||
assert got["후보"] and all(x["키"].startswith("EQ") for x in got["후보"])
|
||||
|
||||
|
||||
def test_단위_별칭_hr_시간_개소_개():
|
||||
assert mu.plain("hr") == mu.plain("시간") and mu.plain("HR") == "시간"
|
||||
assert mu.same_kind("개소", "개") and mu.swap("개소", "개")["배"] == 1
|
||||
assert mu.swap("10개소", "개")["배"] == 10 and mu.swap("hr", "시간")["계수"] is None
|
||||
assert mu.swap("조", "개소")["계수"] == "사용자" # 조는 묶음 밖 그대로
|
||||
@@ -250,7 +250,9 @@ def test_하위_호표_먼저_확인_퍼센트_빠짐_통째(client):
|
||||
mine = layers.personal_dir(7, 42)
|
||||
made = _doc(mine, got["참조"]["키"])
|
||||
line = made["담은로직"][0]
|
||||
assert line["참조"] == child["참조"] and line["별칭"] == "고화매트재료비"
|
||||
# 740-1 하위 호표 = 사본 블록(원본 연결 없음 · 키 = 출처)
|
||||
assert line["참조"]["층"] == "사본" and line["참조"]["키"] == child["참조"]["키"]
|
||||
assert line["본뜬"]["키"] == child["참조"]["키"] and line["별칭"] == "고화매트재료비"
|
||||
assert line["수량"] is None and made["본뜬"]["실무수량"]["줄"]["0"] == 2.3 # 723-1
|
||||
assert "고화매트재료비||m2" not in _book(mine) # 호표 참조 줄 = 장부 안 씀
|
||||
whole = _build(client, 3, 통째={"대상": [{"로직": "GF000101", "배": 1}]})
|
||||
|
||||
@@ -433,3 +433,15 @@ def test_목록_열_때_뒤_셈_예약(client, monkeypatch):
|
||||
monkeypatch.setattr(fit, "ensure", lambda folder, key: asked.append(key) or {"끝": False})
|
||||
rows = client.get(URL.format("personal")).json()["줄"]
|
||||
assert [r["키"] for r in rows] == asked == ["HP-000001"]
|
||||
|
||||
|
||||
def test_미리_고름_점은_다른_점_1위_출처같음만(client): # 745-1 · 584-3
|
||||
_ready(client)
|
||||
got = _hopyo(client, 3)
|
||||
for name in ("구조물터파기(인력100%)", "구조물되메우기(인력100%)"): # 다른 점 + 출처 같음
|
||||
cands = got[name]["후보"]
|
||||
assert got[name]["상태"] == "다른 점" and cands[0]["미리"] is True
|
||||
assert not any("미리" in c for c in cands[1:]) # 1위만
|
||||
assert all("미리" not in c for c in got["잔토처리(인력100%)"]["후보"]) # 후보 줄 = 점 없음
|
||||
ok = _hopyo(client, 1)["레미콘타설(장비사용)"] # 맞음 줄도 점 없음(이미 체크)
|
||||
assert ok["상태"] == "맞음" and all("미리" not in c for c in ok["후보"])
|
||||
|
||||
@@ -538,8 +538,12 @@ def test_명사형_단추와_별칭_장부_풀이_359():
|
||||
|
||||
|
||||
def test_372_2_저장은_개인_층에_삭제는_화면_층_권한():
|
||||
"""718-1 — 만들 층 = 마법사에서 고른 층(올린 층과 같음) · [삭제] = 끝내기 → 처음 걸음."""
|
||||
"""718-1 — 만들 층 = 마법사에서 고른 층(올린 층과 같음) · [삭제] = 끝내기 → 처음 걸음 ·
|
||||
743-1 — 지운 파일은 떠남 지움 목록에서 뺌 · 떠남(화면 바뀜 · 페이지 닫음) = 쥔 파일 지움."""
|
||||
side = _text("M02_MasterTemplete_Hopyo_Wizard.ts")
|
||||
open_ = side.split("m.openHopyoSheet({")[1].split("}),")[0]
|
||||
assert "target: scope," in open_ and "canWrite: true," in open_
|
||||
assert "onDeleted: () => void first()," in open_
|
||||
assert "drop(layer, file.키);" in open_ and "void first();" in open_
|
||||
assert 'addEventListener("pagehide", leave)' in side and "return { root, leave };" in side
|
||||
panel = _text("M02_MasterTemplete_Hopyo_Side.ts")
|
||||
assert panel.count("open?.leave?.()") == 1 and panel.count("open.leave?.()") == 1
|
||||
|
||||
@@ -179,18 +179,18 @@ def test_사전조합_틀린_줄_400_순환_422(world):
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
_combo(world["system"], lines)
|
||||
assert e.value.status == 400
|
||||
# 740-1 — 사전조합에 사전조합 = 사본 블록(원본 연결 없음) · 되돌려 담아도 순환 없음
|
||||
a = _combo(world["system"], [one], "가")
|
||||
b = _combo(world["system"], [{"참조": {**SYS, "키": a}}], "나")
|
||||
with store.at(world["system"]):
|
||||
line = store.read("precombo", b)["문서"]["담은로직"][0]
|
||||
assert line["참조"]["층"] == "사본" and line["본뜬"]["키"] == a
|
||||
assert line["참조"]["몸"]["담은로직"][0]["참조"] == one["참조"]
|
||||
have = store.read("precombo", a)
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
store.write(
|
||||
"precombo",
|
||||
a,
|
||||
have["판"],
|
||||
{**have["문서"], "담은로직": [{"참조": {**SYS, "키": b}}]},
|
||||
)
|
||||
assert e.value.status == 422 and f"{a} → {b} → {a}" in e.value.detail
|
||||
store.write(
|
||||
"precombo", a, have["판"], {**have["문서"], "담은로직": [{"참조": {**SYS, "키": b}}]}
|
||||
)
|
||||
assert store.read("precombo", a)["문서"]["담은로직"][0]["참조"]["층"] == "사본"
|
||||
|
||||
|
||||
def test_층_규칙은_위_층만(world):
|
||||
@@ -329,11 +329,12 @@ def test_복사는_못_읽는_참조를_같이_복사_본뜬(world):
|
||||
)
|
||||
got = _read(world["company"], "precombo", made["이름"])
|
||||
assert got["본뜬"] == {**MINE, "키": sc}
|
||||
assert [one["참조"] for one in got["담은로직"]] == [
|
||||
{**COMPANY, "키": "SC-000001"}, # 회사가 못 읽는 개인 사전조합 → 회사 층 새 키
|
||||
{**SYS, "키": sys_sp}, # 마스터는 그대로(옛 승격도)
|
||||
]
|
||||
assert _read(world["company"], "precombo", "SC-000001")["본뜬"] == {**MINE, "키": inner}
|
||||
# 740-1 — 개인 사전조합은 넣을 때 사본 블록(본뜬) → 회사로 따로 복사 안 함 ·
|
||||
# 마스터 옛 승격 그대로
|
||||
block = got["담은로직"][0]
|
||||
assert block["참조"]["층"] == "사본" and block["본뜬"]["키"] == inner
|
||||
assert got["담은로직"][1]["참조"] == {**SYS, "키": sys_sp}
|
||||
assert made["이름"] == "SC-000001" # 딸림 복사 없음
|
||||
# 못 읽는 옛 개인 승격은 따라 복사 안 함(134-2 · 승격 서고 없음)
|
||||
mine_sp = _promote(world["personal"])
|
||||
with store.at(world["company"]), pytest.raises(store.StoreError) as e:
|
||||
|
||||
@@ -119,7 +119,9 @@ def test_분류_고정_폴더_지움_자유_폴더_메뉴_끌어_옮기기():
|
||||
assert 'item(L("M02_Delete"), () => void remove([target]))' in menu
|
||||
assert 'opt.scope().canWrite ? undefined : tc("ReadOnly")' in menu # 읽기만 층 = 흐림
|
||||
remove = side.split("const remove = async (")[1].split("\n };")[0]
|
||||
assert "showConfirmDialog(ask" in remove and 'op: "delete", 대상:' in remove # 안에 든 것째
|
||||
assert (
|
||||
"askLibraryDelete(kind," in remove and 'op: "delete", 대상:' in remove
|
||||
) # 735 쓰는 곳 경고 뒤 그 줄째
|
||||
assert 'newHere: tc("Lib_NewHere")' in pre_side
|
||||
assert '({ 이름: name, 구분: "", 상세구분: "", 단위: null, 담은로직: [] })' in pre_side
|
||||
make = side.split("const make = (path: string[]): void => {")[1].split("\n };")[0]
|
||||
|
||||
Reference in New Issue
Block a user