Merge remote-tracking branches 'origin/CODEX', 'origin/sub_laptop_1' and 'origin/sub_laptop_2' into sub_laptop_3
This commit is contained in:
@@ -258,59 +258,32 @@ def _user(uid: str, local: list[dict]) -> dict:
|
||||
|
||||
MACHINE_SEP = "#" # 입력 판(sub3) 기계 요소 ref 「EQ키#칸」
|
||||
HOURLY = "시간당 경비" # 기계 칸 이름 — 기계 시간당 경비 로직으로 셈(기종 → 로직 짝)
|
||||
_MACHINE_PLAIN = {"기계", "지역", "보정작업", "조종원직종"} # 이 밖의 입력 = 부착 장비 전용
|
||||
|
||||
|
||||
def _machine_family() -> list[dict]:
|
||||
"""기계 시간당 경비 로직 — 「건설기계」 장 파일 안 · 입력 「기계」 고르기가 있는 돈 로직."""
|
||||
files = store.loaded()[0]
|
||||
return [
|
||||
row
|
||||
for name, data in files.items()
|
||||
if data.get("그룹") == "로직" and "건설기계" in name
|
||||
for row in data.get("줄") or []
|
||||
if str(row.get("결과단위") or "").startswith("원")
|
||||
and any(i.get("이름") == "기계" and i.get("고르기") for i in row.get("입력") or [])
|
||||
]
|
||||
|
||||
|
||||
def _machine_rank(row: dict) -> tuple:
|
||||
"""짝 차례(옛 EasyCalc `pickMachine` 와 같음) — 부착 전용 · 손료만 · 보정 · 기종 수 · 키."""
|
||||
names = [i.get("이름") for i in row.get("입력") or []]
|
||||
codes = next(i.get("고르기") for i in row["입력"] if i.get("이름") == "기계")
|
||||
attach = any(n not in _MACHINE_PLAIN for n in names)
|
||||
loss = "지역" not in names
|
||||
return (attach, loss, loss and "보정작업" in names, len(codes), str(row.get("키")))
|
||||
|
||||
|
||||
def _machine_hourly(key: str, inputs: dict) -> dict:
|
||||
"""기계 한 시간 값 — 기종을 받는 기계경비 로직 가운데 짝 차례 첫째 · 빈 입력은 첫 고르기."""
|
||||
"""기계 한 시간 값 — 식 고름 = `M02_Machine_Hourly.pick`(목록 › 꼴 · 822-2) · 빈 입력은 첫 고르기."""
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as mh
|
||||
|
||||
row, why = _brief(key, "기계")
|
||||
if row is None:
|
||||
return {"ok": False, "reason": why}
|
||||
code = str(row.get("원문번호") or "")
|
||||
fit = [r for r in _machine_family() if code in [str(c) for c in _codes(r)]]
|
||||
if not fit:
|
||||
return {"ok": False, "reason": f"「{row.get('이름')}」 은 시간당 경비를 셀 자료가 없음"}
|
||||
logic = min(fit, key=_machine_rank)
|
||||
logic = mh.pick(row)
|
||||
if lack := mh.why(row, logic):
|
||||
return {"ok": False, "reason": f"「{row.get('이름')}」 — {lack}"}
|
||||
given = {}
|
||||
for spec in logic.get("입력") or []:
|
||||
name = spec.get("이름")
|
||||
if name != "기계":
|
||||
given[name] = inputs.get(name, (spec.get("고르기") or [None])[0])
|
||||
args = "".join(f", {n}='{v}'" for n, v in given.items())
|
||||
ref = f"로직({logic['키']}, 기계='{code}'{args})"
|
||||
args = "".join(f", {n if n.isidentifier() else repr(n)}='{v}'" for n, v in given.items())
|
||||
mine = f", 기계='{code}'" if mh.takes_machine(logic) else ""
|
||||
ref = f"로직({logic['키']}{mine}{args})"
|
||||
line = {"종류": "로직", "요소": ref, "이름": "기계", "단위": "시간", "수량": "1"}
|
||||
got = _money(_calc({}, _row(line, ())), "계")
|
||||
return {**got, "name": row.get("이름"), "unit": "원/시간"} if got["ok"] else got
|
||||
|
||||
|
||||
def _codes(row: dict) -> list:
|
||||
return next(
|
||||
(i.get("고르기") or [] for i in row.get("입력") or [] if i.get("이름") == "기계"), []
|
||||
)
|
||||
|
||||
|
||||
def one_value(item: dict, local: list[dict] | None = None) -> dict:
|
||||
"""물음 한 줄 `CfQuery {id, group, ref? · table? · unit? · field? · inputs?}` → `CfAnswer`.
|
||||
|
||||
|
||||
@@ -186,10 +186,11 @@ class _Collector:
|
||||
return
|
||||
self.add(base)
|
||||
if field == dv.HOURLY:
|
||||
code = str((self.whole.index.get(base[:2], {}).get(base) or {}).get("원문번호") or "")
|
||||
for fit in dv._machine_family():
|
||||
if code in [str(c) for c in dv._codes(fit)]:
|
||||
self.logic(str(fit.get("키")))
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as mh
|
||||
|
||||
row = self.whole.index.get(base[:2], {}).get(base) or {}
|
||||
if got := mh.pick(row): # 식 고름 한 곳(822-2)
|
||||
self.logic(str(got.get("키")))
|
||||
|
||||
def logic(self, key: str) -> None:
|
||||
if key in self.logics:
|
||||
|
||||
@@ -24,6 +24,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from contextvars import ContextVar
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -125,11 +126,18 @@ def copy_ref(body: dict[str, Any], key: str) -> Ref:
|
||||
}
|
||||
|
||||
|
||||
#: 참조 파일을 읽기 직전에 부름(826-1 층 목록 색인 기댐 · `M02_Layer_Index`) — None = 안 부름.
|
||||
SEEN: ContextVar[Callable[[Path], None] | None] = ContextVar("m02_refs_seen", default=None)
|
||||
|
||||
|
||||
def read_ref(ref: Ref) -> dict[str, Any] | None:
|
||||
if is_copy(ref):
|
||||
return ref["몸"]
|
||||
try:
|
||||
got = json.loads(ref_path(ref).read_bytes())
|
||||
path = ref_path(ref)
|
||||
if (note := SEEN.get()) is not None:
|
||||
note(path)
|
||||
got = json.loads(path.read_bytes())
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
return got if isinstance(got, dict) else None
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
"""M02 층 목록 요약 색인(826-1 · 요구 804 · 조사 804 안 A) — 층 목록이 항목 파일을 다시 열지 않게.
|
||||
|
||||
항목 기록 `{자국, 판, 바탕?, 기댐?, 요약?}`(열쇠 「종류/이름」):
|
||||
자국 파일 고친 때 ns : 크기 — 같으면 기억한 판(목록 표시만 · 저장 · 지움 409 는 그대로
|
||||
`layers.version_of` 파일 내용)
|
||||
요약 서고 줄 `M02_Labor_Ref.summary`(쓰는곳수 뺌) — 자국 · 바탕 · 기댐이 모두 같을 때만 씀
|
||||
바탕 셈 코드 내용 · M01 폴더 · 층 곁 파일(내 기계 등 — 항목 폴더 · manifest 뺌) 자국 지문
|
||||
기댐 셈 때 읽은 참조 파일(SC · SK) 자국(`refs.SEEN`)
|
||||
쓰는곳수 = 요청마다 쓰는 곳 파일을 한 번 훑어 거꾸로 셈(`refs._ids_of` 기억 · 파일당 stat 한 번).
|
||||
자리 = 프로세스 메모리 + storage `_derived/m02_layer_index/`(파생 저장본 · 층마다 한 파일 ·
|
||||
참조 이름표 기억 `ids.json`) — 없거나 깨지면 다시 지음 · 지워도 됨 · 다른 PC · 옛 코드는 안 읽음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import threading
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_derived_store import derived_dir, derived_key, file_stamp, read_derived
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from config import config_system
|
||||
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
|
||||
|
||||
KIND = "m02_layer_index"
|
||||
SHAPE = 1 # 기록 꼴이 바뀌면 올림(옛 기록 = 다시 셈)
|
||||
IDS = "ids"
|
||||
CODE_DIRS = ("M02_MasterTemplete", "M01_MasterData", "resources/master_data/scripts", "common_util")
|
||||
_LOCK = threading.RLock()
|
||||
_MEM: dict[str, dict[str, Any]] = {} # 층 열쇠 → {항목: 기록}
|
||||
_IDS_LOADED: list[bool] = []
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _code() -> str:
|
||||
"""셈 코드 내용 지문 — 프로세스에 한 번(코드가 바뀌면 서버가 다시 뜸 · 같은 코드 = 같은 값)."""
|
||||
digest = hashlib.blake2b(digest_size=16)
|
||||
for sub in CODE_DIRS:
|
||||
for path in sorted((config_system.PROJECT_ROOT / sub).glob("*.py")):
|
||||
digest.update(path.name.encode())
|
||||
digest.update(path.read_bytes())
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _stamps(folder: Path, skip: tuple[str, ...] = ()) -> list[tuple[str, str | None]]:
|
||||
"""폴더 안 파일 자국 — 첫 단 이름이 `skip` · `_` · `.` 머리인 것은 뺌."""
|
||||
if not folder.is_dir():
|
||||
return []
|
||||
out = []
|
||||
for path in sorted(folder.rglob("*")):
|
||||
rel = path.relative_to(folder).parts
|
||||
if rel[0] in skip or any(p.startswith(("_", ".")) for p in rel) or not path.is_file():
|
||||
continue
|
||||
out.append(("/".join(rel), file_stamp(path)))
|
||||
return out
|
||||
|
||||
|
||||
def _ground(folder: Path) -> str:
|
||||
"""바탕 지문 — 셈 코드 · M01 폴더 · 이 층 · 지금 층 기본 자리의 곁 파일."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
skip = (*layers.KINDS, layers.MANIFEST_NAME)
|
||||
side = [_stamps(f, skip) for f in dict.fromkeys((folder, store.root()))]
|
||||
return derived_key(SHAPE, _code(), str(m01.FOLDER), _stamps(Path(m01.FOLDER)), side)
|
||||
|
||||
|
||||
# ── 기록 자리 ─────────────────────────────────────────────────────────
|
||||
def _book(folder: Path) -> tuple[str, dict[str, Any]]:
|
||||
key = derived_key(SHAPE, str(folder.resolve()))
|
||||
if key not in _MEM:
|
||||
got = read_derived(layers.storage_root(), KIND, key)
|
||||
value = got.get("value") if got else None
|
||||
_MEM[key] = value if isinstance(value, dict) else {}
|
||||
return key, _MEM[key]
|
||||
|
||||
|
||||
def _save(key: str, value: Any) -> None:
|
||||
path = derived_dir(layers.storage_root(), KIND) / f"{key}.json"
|
||||
try:
|
||||
atomic_write_json(path, {"key": key, "value": value})
|
||||
except OSError: # 파생 저장본 — 못 써도 다음에 다시 셈
|
||||
pass
|
||||
|
||||
|
||||
# ── 목록 · 판 ─────────────────────────────────────────────────────────
|
||||
def list_rows(folder: str | Path) -> list[dict[str, Any]]:
|
||||
"""`layers.list_templates` 와 같은 줄 — 판은 자국이 같으면 기억한 것(파일 안 엶)."""
|
||||
folder = Path(folder)
|
||||
with _LOCK:
|
||||
key, recs = _book(folder)
|
||||
dirty: list[str] = []
|
||||
|
||||
def version(path: Path) -> str | None:
|
||||
name = f"{path.parent.name}/{path.stem}"
|
||||
stamp = file_stamp(path) # 읽기 전 자국 — 그새 바뀌면 다음에 다시 셈
|
||||
rec = recs.get(name)
|
||||
if stamp and isinstance(rec, dict) and rec.get("자국") == stamp and "판" in rec:
|
||||
return rec["판"]
|
||||
recs[name] = {"자국": stamp, "판": layers.version_of(path)}
|
||||
dirty.append(name)
|
||||
return recs[name]["판"]
|
||||
|
||||
rows = layers.list_templates(folder, version)
|
||||
have = {f"{row['종류']}/{row['이름']}" for row in rows}
|
||||
gone = [name for name in recs if name not in have]
|
||||
for name in gone:
|
||||
del recs[name]
|
||||
if dirty or gone:
|
||||
_save(key, recs)
|
||||
return rows
|
||||
|
||||
|
||||
# ── 서고 요약 ─────────────────────────────────────────────────────────
|
||||
def _good(rec: Any, stamp: str | None, ground: str) -> bool:
|
||||
if not (stamp and isinstance(rec, dict) and rec.get("자국") == stamp):
|
||||
return False
|
||||
deps = rec.get("기댐")
|
||||
if rec.get("바탕") != ground or not isinstance(rec.get("요약"), dict):
|
||||
return False
|
||||
return isinstance(deps, dict) and all(file_stamp(Path(p)) == s for p, s in deps.items())
|
||||
|
||||
|
||||
def _fresh(folder: Path, kind: str, name: str, rec: Any, stamp: str | None, whole: Any) -> dict:
|
||||
"""그 항목만 다시 읽어 셈 — 참조는 읽기 직전 자국을 기댐에 적음."""
|
||||
from M02_MasterTemplete.M02_Labor_Expand import Expand
|
||||
|
||||
path = folder / kind / f"{name}.json"
|
||||
deps: dict[str, str | None] = {}
|
||||
|
||||
def note(p: Path) -> None:
|
||||
deps.setdefault(str(p), file_stamp(p))
|
||||
|
||||
token = refs.SEEN.set(note)
|
||||
try: # 셈마다 새 펼침 — 다른 항목 셈 기억을 나눠 쓰면 기댐이 빠짐
|
||||
out = refs.summary(folder, kind, name, Expand(whole), [])
|
||||
finally:
|
||||
refs.SEEN.reset(token)
|
||||
out.pop("쓰는곳수", None)
|
||||
same = isinstance(rec, dict) and rec.get("자국") == stamp and "판" in rec
|
||||
return {
|
||||
"자국": stamp,
|
||||
"판": rec["판"] if same else layers.version_of(path),
|
||||
"기댐": deps,
|
||||
"요약": out,
|
||||
}
|
||||
|
||||
|
||||
def summaries(folder: str | Path, rows: list[dict[str, Any]]) -> None:
|
||||
"""`refs.summaries` 와 같은 칸 · 차례 — 기록이 맞으면 파일 안 엶 · 다르면 그 항목만 셈."""
|
||||
folder = Path(folder)
|
||||
lib = [row for row in rows if row["종류"] in layers.LIBRARY_KINDS]
|
||||
if not lib:
|
||||
return
|
||||
with _LOCK:
|
||||
key, recs = _book(folder)
|
||||
ground, whole, dirty = _ground(folder), None, False
|
||||
for row in lib:
|
||||
name = f"{row['종류']}/{row['이름']}"
|
||||
path = folder / row["종류"] / f"{row['이름']}.json"
|
||||
stamp = file_stamp(path)
|
||||
if not _good(recs.get(name), stamp, ground):
|
||||
if whole is None:
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
whole = m01.loaded()[1]
|
||||
rec = _fresh(folder, row["종류"], row["이름"], recs.get(name), stamp, whole)
|
||||
recs[name] = {**rec, "바탕": ground}
|
||||
dirty = True
|
||||
row.update(recs[name]["요약"])
|
||||
if dirty:
|
||||
_save(key, recs)
|
||||
sk = [row for row in lib if row["종류"] == "sangeun" and "고친줄수" in row]
|
||||
if sk:
|
||||
uses, (layer, owner) = _uses(), refs.where(folder)
|
||||
for row in sk:
|
||||
rid = refs.ref_id({"층": layer, "주인": owner, "키": row["이름"]})
|
||||
row["쓰는곳수"] = uses.get(rid, 0)
|
||||
|
||||
|
||||
# ── 쓰는 곳 수 ────────────────────────────────────────────────────────
|
||||
def _uses() -> dict[str, int]:
|
||||
"""참조 이름표 → 담은 파일 수(`refs.users` 개수와 같음) — 쓰는 곳 파일 한 번 훑음."""
|
||||
_load_ids()
|
||||
before = {path: hit[0] for path, hit in refs._IDS.items()}
|
||||
files = [path for _what, _at, path in refs.use_files()]
|
||||
count: dict[str, int] = {}
|
||||
for path in files:
|
||||
got = refs._ids_of(path)
|
||||
for rid in got[1] if got else ():
|
||||
count[rid] = count.get(rid, 0) + 1
|
||||
if any(before.get(path) != refs._IDS[path][0] for path in files if path in refs._IDS):
|
||||
_save_ids()
|
||||
return count
|
||||
|
||||
|
||||
def _load_ids() -> None:
|
||||
"""참조 이름표 기억을 파생 저장본에서 채움 — 프로세스에 한 번 · 자국은 `_ids_of` 가 다시 봄."""
|
||||
if _IDS_LOADED:
|
||||
return
|
||||
_IDS_LOADED.append(True)
|
||||
got = read_derived(layers.storage_root(), KIND, IDS)
|
||||
for text, one in ((got or {}).get("value") or {}).items():
|
||||
try:
|
||||
mtime, size, name, ids = one
|
||||
refs._IDS.setdefault(Path(text), ((int(mtime), int(size)), (str(name), frozenset(ids))))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
|
||||
def _save_ids() -> None:
|
||||
"""지금 기억을 디스크 것에 덧씀(다른 서버 몫 유지) · 없어진 파일 뺌."""
|
||||
got = read_derived(layers.storage_root(), KIND, IDS)
|
||||
value = dict((got or {}).get("value") or {})
|
||||
for path, ((mtime, size), (name, ids)) in list(refs._IDS.items()):
|
||||
value[str(path)] = [mtime, size, name, sorted(ids)]
|
||||
value = {text: one for text, one in value.items() if Path(text).is_file()}
|
||||
_save(IDS, value)
|
||||
@@ -7,17 +7,30 @@
|
||||
마스터 파일은 읽기만 · 로그인 사용자 모두.
|
||||
기계표 = M01 + 지금 층 내 기계(821-1 · `M02_MyMachine` — 식은 채운 칸으로 고른 같은 GC 식 · 직접 = 그 값) ·
|
||||
층을 모르는 길(`/api/m02/logic-lib/machine-hourly`)은 M01 만.
|
||||
식 고름 한 곳(822-2 · `pick`) = ㉠ 원문번호가 식 「기계」 목록에 있으면 그 식(짝 차례) ㉡ 그 기계만의 식
|
||||
(`_own`) ㉢ 없으면 꼴(채운 칸 · `M02_MyMachine.logic_key`) — M01 [새 기계] · 불러오기 · 데이터 흐름 ·
|
||||
줄 쌓기 · 단가산출 기초가 다 이것을 씀 · 못 고름 · 값 모자람 = 까닭 글(`why`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
from M02_MasterTemplete import M02_MyMachine as mym
|
||||
|
||||
cm = store.cm
|
||||
_PLAIN = {"기계", "지역", "보정작업", "조종원직종"} # 이 밖의 입력 = 부착 장비 전용
|
||||
_PLAIN = {
|
||||
"기계",
|
||||
"지역",
|
||||
"보정작업",
|
||||
"조종원직종",
|
||||
"전력 계약종별",
|
||||
} # 이 밖의 입력 = 부착 장비 전용
|
||||
_TEXT = {"주연료종류", "조종원직종"} # 글 칸 — 나머지 기계 줄 값 칸은 수
|
||||
_FIELD = re.compile(r"EQ:\{기계\}\.([^\s*/+()\-,]+)")
|
||||
HOUR_UNIT = "10^-7/시간"
|
||||
|
||||
|
||||
def _family() -> list[dict]:
|
||||
@@ -54,12 +67,27 @@ def _operated(code: str) -> bool:
|
||||
|
||||
|
||||
def logic_of(code: str) -> dict | None:
|
||||
"""기계 원문번호 → 그 기계 시간당 경비 로직(짝 차례 첫째) · 없으면 None(813-2 M01 화면 ·
|
||||
불러오기 `M02_Doc_Import.hourly_link` 도 씀). 기계표 조종원 > 0 = 조종원 호표 있는 식 먼저
|
||||
(「조종원 없는 기종」 목록에 겹친 기계 — 진동롤러(핸드가이드식) · 고소작업차 …)."""
|
||||
fit = [r for r in _family() if code in [str(c) for c in _codes(r)]]
|
||||
"""기계 원문번호 → 그 기계 시간당 경비 로직 · 없으면 None(813-2 M01 화면 · 불러오기
|
||||
`M02_Doc_Import.hourly_link` 도 씀) — `pick` 과 같음(그 원문번호 기계 줄로)."""
|
||||
index = mym.whole()
|
||||
row = (index.numbers.get("EQ") or {}).get(code) or {"원문번호": code}
|
||||
return pick(row)
|
||||
|
||||
|
||||
def listed(code: str) -> bool:
|
||||
"""원문번호가 어느 기계경비식 「기계」 목록에 있는지(M01 [새 기계] 식 등록 · 상세 「쓰는 계산식」)."""
|
||||
return bool(code) and any(code in [str(c) for c in _codes(r)] for r in _family())
|
||||
|
||||
|
||||
def pick(row: dict) -> dict | None:
|
||||
"""기계 줄 → 시간당 경비 식 — ㉠ 목록 ㉡ 그 기계만의 식 ㉢ 꼴 · 못 고름 = None(까닭 = `why`)."""
|
||||
code = str(row.get("원문번호") or "")
|
||||
fit = [r for r in _family() if code and code in [str(c) for c in _codes(r)]]
|
||||
if not fit:
|
||||
return _own(code) or mym.logic_for(code)
|
||||
if got := _own(code):
|
||||
return got
|
||||
key = None if _day_loss(row) else mym.logic_key(row)
|
||||
return mym.whole().logic(key) if key else None
|
||||
op = _operated(code)
|
||||
|
||||
def order(row: dict) -> tuple:
|
||||
@@ -84,6 +112,45 @@ def _own(code: str) -> dict | None:
|
||||
return None
|
||||
|
||||
|
||||
def _day_loss(row: dict) -> str:
|
||||
"""손료계수 단위가 시간당(10⁻⁷)이 아님(산림품셈 「1일 1대당」) — 그 단위 글 · 아니면 빈 글."""
|
||||
unit = str((row.get("단위") or {}).get("손료계수") or HOUR_UNIT)
|
||||
return "" if unit == HOUR_UNIT or "손료계수" not in (row.get("값") or {}) else unit
|
||||
|
||||
|
||||
def _is_num(v: Any) -> bool:
|
||||
try:
|
||||
float(v)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return not isinstance(v, bool)
|
||||
|
||||
|
||||
def why(row: dict, logic: dict | None) -> str:
|
||||
"""시간당 경비를 못 내는 까닭 한 줄(값이 정말 모자람) · 셀 수 있으면 빈 글."""
|
||||
vals = row.get("값") or {}
|
||||
if logic is None:
|
||||
if unit := _day_loss(row):
|
||||
return f"손료계수 단위 「{unit}」 — 시간당 식 없음(일 단위 손료)"
|
||||
if not _is_num(vals.get("주연료")) and vals.get("주연료") not in (None, ""):
|
||||
return f"주연료 「{vals['주연료']}」 — 수 아님"
|
||||
if float(vals.get("조종원") or 0) > 0 and not vals.get("조종원직종"):
|
||||
return "조종원직종 없음"
|
||||
return "손료 계수 셋(상각 · 정비 · 관리) 없음"
|
||||
text = " ".join(str(x.get(k)) for k in ("식", "수량") for p in ("중간", "호표", "덧줄")
|
||||
for x in logic.get(p) or []) # fmt: skip
|
||||
lost, odd = [], []
|
||||
for name in dict.fromkeys(_FIELD.findall(text)):
|
||||
v = vals.get(name)
|
||||
if v in (None, ""):
|
||||
lost.append(name)
|
||||
elif name not in _TEXT and not _is_num(v):
|
||||
odd.append(f"{name} 「{v}」 — {'범위' if '∼' in str(v) else '수 아님'}")
|
||||
elif name == "주연료종류" and f"{v}:전국평균" not in (mym.whole().numbers.get("MO") or {}):
|
||||
odd.append(f"주연료종류 「{v}」 — 유가 표에 없음")
|
||||
return " · ".join([*([f"{' · '.join(lost)} 없음"] if lost else []), *odd])
|
||||
|
||||
|
||||
def takes_machine(logic: dict) -> bool:
|
||||
"""식이 「기계」 입력(원문번호 고르기)을 받는지 — 안 받는 식(GF000145)엔 기계='…' 를 안 넘김."""
|
||||
return any(i.get("이름") == "기계" for i in logic.get("입력") or [])
|
||||
@@ -100,9 +167,9 @@ def hourly(key: str) -> dict[str, Any]:
|
||||
if mym.mine(key):
|
||||
return mym.hourly(row, whole)
|
||||
code = str(row.get("원문번호") or "")
|
||||
logic = logic_of(code)
|
||||
if not logic:
|
||||
return {"ok": False, "reason": f"「{row.get('이름')}」 은 시간당 경비를 셀 자료가 없음"}
|
||||
logic = pick(row)
|
||||
if lack := why(row, logic):
|
||||
return {"ok": False, "reason": lack, **({"로직": logic["키"]} if logic else {})}
|
||||
args = "".join( # 빈칸 든 입력 이름(「전력 계약종별」 · 820-1 파워트로웰)은 따옴표로
|
||||
f", {n if (n := i['이름']).isidentifier() else repr(n)}='{(i.get('고르기') or [''])[0]}'"
|
||||
for i in logic.get("입력") or []
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* · (785-3) `oil` = M01 유가전력(MO · `/logic-lib/elements?group=재료` 의 MO 줄) 고름 · 고정 줄 꼴 = 이음 유가 MO ·
|
||||
* 재료비 = 지금 값(셈은 M01 값) · 「M01 전체」 탭 재료 아래
|
||||
* · (821-3) `mine` = 개인 층 내 기계 묶음(품셈 기계 위 · 구분 「내 기계」 · 출처 「개인 키」 · 금액 = 서버 시간당) ·
|
||||
* 머리 [새 기계 만들기](양식 창 → 저장하면 그 기계로 고름) · 「시간당 값 직접」 내 기계 = 줄 쌓기 식 없음 → 회색
|
||||
* 머리 [새 기계 만들기](양식 창 → 저장하면 그 기계로 고름) · 「시간당 값 직접」 내 기계 = 직접 값 식 GC001199(822-2) · 식 없는 내 기계 = 회색
|
||||
* ========================================================================== */
|
||||
|
||||
import { createDataTable } from "@ui/ui_template_data_table";
|
||||
@@ -288,7 +288,7 @@ export function openFindPicker(opt: FindPickOptions): void {
|
||||
: take(() => put(e)),
|
||||
};
|
||||
});
|
||||
/** 821-3 내 기계 줄 — 금액 = 서버 시간당 · 직접 = 회색(줄 쌓기 식 없음) */
|
||||
/** 821-3 내 기계 줄 — 금액 = 서버 시간당 · 식 없음(서버 로직 없음) = 회색 · 직접 = GC001199(822-2) */
|
||||
const mineLines = (q: string): Line[] => {
|
||||
const words = q.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
return mines
|
||||
|
||||
@@ -27,6 +27,7 @@ from common_util.common_util_storage import resolve_stored_project_path
|
||||
from config.config_db import get_db_pool, run_with_connection
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Layer_Access as access
|
||||
from M02_MasterTemplete import M02_Layer_Index as index
|
||||
from M02_MasterTemplete import M02_Library_Flat as flat
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Items as items_store
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Router_Items as items_router
|
||||
@@ -275,14 +276,14 @@ async def list_layer(
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
) -> dict[str, Any]:
|
||||
folder = await _layer_dir(session, layer, project_id=project_id, user_id=user_id, owner=owner)
|
||||
rows = await asyncio.to_thread(layers.list_templates, folder)
|
||||
rows = await asyncio.to_thread(index.list_rows, folder) # 판 = 자국 기억(826-1)
|
||||
manifest = await asyncio.to_thread(layers.read_manifest, folder)
|
||||
for row in rows:
|
||||
row["출처"] = manifest.get(f"{row['종류']}/{row['이름']}")
|
||||
if (row["종류"], row["이름"]) in store.IN_USE: # 화면이 지우기 확인 창 전에 막음
|
||||
row["쓰는곳"] = store.IN_USE[(row["종류"], row["이름"])]
|
||||
# 서고(50-3) — 보이는 이름 · 단위 · 기준 금액(51-4)
|
||||
await asyncio.to_thread(refs.summaries, folder, rows)
|
||||
await asyncio.to_thread(index.summaries, folder, rows) # 요약 색인(826-1)
|
||||
return {"층": layer, "양식": rows}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_UI_Side.ts
|
||||
* 마스터 템플릿 좌측 패널 차례(716-3 · 717-2a) — 「단가산출」(층 단추 따로 · 기본 개인 · `UI_Side_Sangeun`) › 「일위대가」(층 단추 따로 · 2단 폴더 나무 · `UI_Side_Precombo` ·
|
||||
* 50-3 · 134-3) › 「내 기계」 · 「내 품목」(821-3 · 개인 층 · 펼치면 가운데 화면) › 「구조물 조합」(층 단추 · 나무) › 수평 구분선 › 「일위대가 불러오기」(구조물 조합 층 ·
|
||||
* 50-3 · 134-3) › 「구조물 조합」(층 단추 · 나무) › 「개인관리」(824-1 · 단추 「기계」 · 「자재」 · 가운데 화면) › 수평 구분선 › 「일위대가 불러오기」(구조물 조합 층 ·
|
||||
* `Hopyo_Side` · 324-4) › 「도면 양식」(새로 · 본떠 · 지우기).
|
||||
* 구조물 옵션 정의는 M01 로 옮김(716-1).
|
||||
* 도면 양식 = 시스템 관리자면 늘(마스터 층 것 · 구조물 조합 층과 무관 · 720-4).
|
||||
@@ -157,17 +157,16 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
getOpen: opt.getOpen,
|
||||
});
|
||||
const sangeunSec = section(L("M02_SangeunTitle"), sangeun.body);
|
||||
// 내 기계 · 내 품목(821-3) — 자기 개인 층 · 펼치면 가운데 화면(목록 · 양식)
|
||||
const hint = (key: "Side_Hint" | "Items_Side_Hint"): HTMLElement =>
|
||||
createEmpty({ text: mt(key), small: true });
|
||||
const myMachSec = openOnExpand(section(mt("Title"), hint("Side_Hint")), async () => {
|
||||
// 개인관리(824-1) — 단추 「기계」 · 「자재」(821-3 · 자기 개인 층) · 누르면 가운데 화면(목록 · 양식)
|
||||
const myMachBtn = createButton({ label: mt("Title"), variant: "ghost", size: "sm" });
|
||||
myMachBtn.addEventListener("click", async () => {
|
||||
if (!(await opt.confirmLeave())) return;
|
||||
let mark = (_d: boolean): void => {};
|
||||
const node = buildMyMachineView({ owner: null, setDirty: (d) => mark(d) });
|
||||
mark = opt.showView(mt("Title"), node);
|
||||
mark = opt.showView(mt("Crumb_Machine"), node);
|
||||
});
|
||||
myMachSec.dataset.myMachines = "";
|
||||
const myItemsSec = openOnExpand(section(mt("Items_Title"), hint("Items_Side_Hint")), async () => {
|
||||
const myItemsBtn = createButton({ label: mt("Items_Title"), variant: "ghost", size: "sm" });
|
||||
myItemsBtn.addEventListener("click", async () => {
|
||||
if (!(await opt.confirmLeave())) return;
|
||||
const node = buildMyItemsView({
|
||||
owner: null,
|
||||
@@ -184,8 +183,13 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
});
|
||||
},
|
||||
});
|
||||
opt.showView(mt("Items_Title"), node);
|
||||
opt.showView(mt("Crumb_Items"), node);
|
||||
});
|
||||
const personalSec = section(
|
||||
mt("Personal_Title"),
|
||||
el("div", { className: "m02-side__row", children: [myMachBtn, myItemsBtn] }),
|
||||
);
|
||||
personalSec.dataset.myMachines = "";
|
||||
// 실무자료로 일위대가를 만들었음(확인 창 · 엑셀 보기 창) → 목록 다시 · 이 패널이 내려갔으면 듣기 끝
|
||||
const onBuilt = (): void =>
|
||||
precombo.body.isConnected
|
||||
@@ -205,9 +209,8 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
children: [
|
||||
sangeunSec,
|
||||
precomboSec,
|
||||
myMachSec,
|
||||
myItemsSec,
|
||||
summary,
|
||||
personalSec,
|
||||
createDivider(),
|
||||
section(hopyo.title, hopyo.body),
|
||||
drawing,
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
/* 개인관리(824-1) — 제목 아래 단추 줄 띄움 */
|
||||
[data-my-machines] > .m02-side__row {
|
||||
margin-top: var(--spacing-12);
|
||||
}
|
||||
|
||||
/* 일위대가 조합 서고 목록표(51-4) — 표 꼴은 M01 목록(`m01-master__grid`) · 첫 칸 = 고름 */
|
||||
.m01-master__grid .m02-lib__pick {
|
||||
width: 2.5rem;
|
||||
|
||||
@@ -28,7 +28,7 @@ export function buildMyItemsView(opt: MyItemsViewOptions): HTMLElement {
|
||||
const uses = createFindList({ placeholder: mt("Open_Sk") });
|
||||
uses.root.hidden = true;
|
||||
const table = createDataTable({
|
||||
caption: mt("Items_Title"),
|
||||
caption: mt("Crumb_Items"),
|
||||
columns: [
|
||||
{ key: "이름", label: mt("Name"), width: 200 },
|
||||
{ key: "규격", label: mt("Spec"), width: 140 },
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
내 기계 없는 층 = M01 그대로. 단가산출 사본 · [단가 갱신] · 「단가새값」(`M02_Sangeun_Copy`) ·
|
||||
기계 찾기(`M02_Machine_Find`) · 시간당(`M02_Machine_Hourly`) · 사본 모으기(`M02_Item_Prices.collect`)가
|
||||
이 색인을 읽음 — 층을 모르는 길(`/api/m02/logic-lib/machine-hourly` 등)은 M01 만.
|
||||
식 고름(채운 칸) = 연료 · 조종원 둘 다 GC000268 · 연료만 GC000272 · 조종원만 GC000276 · 없음 GC000273.
|
||||
전력 칸은 담기만(M01 기계경비 식에 전력 줄 없음).
|
||||
식 고름(채운 칸 · `logic_key` — M01 기계 목록 밖 줄도 같은 규칙 · 822-2) = 연료 + 조종원 GC000268(직종 없음
|
||||
GC000275) · 연료만 GC000272(손료계수 한 칸 GC001198) · 조종원만 GC000276 · 전력 GC001196(+ 조종원 GC001197) ·
|
||||
연료 + 전력 GC001200(+ 조종원 GC001201 · 822-2b 경쟁사 기계) ·
|
||||
없음 GC000273 · 직접 GC001199(시간당 값 = 셈 꼴 값 칸 노무비 · 재료비 · 경비).
|
||||
권한(본인 개인 층 · 맡은 회사 관리자)은 길(`M02_MyMachine_Router`) 몫.
|
||||
"""
|
||||
|
||||
@@ -43,6 +45,9 @@ UNITS = {
|
||||
} # fmt: skip
|
||||
LOGIC = {(True, True): "GC000268", (True, False): "GC000272",
|
||||
(False, True): "GC000276", (False, False): "GC000273"} # fmt: skip
|
||||
JOB_IN, ONE_FUEL = "GC000275", "GC001198" # 직종 입력 · 손료계수 한 칸 연료
|
||||
POWER, POWER_CREW, DIRECT_KEY = "GC001196", "GC001197", "GC001199"
|
||||
FUEL_POWER, FUEL_POWER_CREW = "GC001200", "GC001201" # 연료 + 전력(+ 조종원) — 경쟁사 구성(822-2b)
|
||||
|
||||
|
||||
def mine(key: Any) -> bool:
|
||||
@@ -80,6 +85,8 @@ def rows(folder: Path | None = None) -> list[dict]:
|
||||
def engine(row: dict) -> dict:
|
||||
"""셈 꼴 — 바로 = 상각비계수 ← 손료계수 · 정비 · 관리 0 · 빈 연료 · 조종원 칸 = 0 · 수 = Decimal."""
|
||||
vals = dict(row.get("값") or {})
|
||||
if row.get("방식") == DIRECT: # 직접 식(GC001199)이 읽는 칸
|
||||
vals = {k: (row.get("시간당") or {}).get(k) or 0 for k in COSTS}
|
||||
if row.get("방식") == EASY:
|
||||
vals |= {"상각비계수": vals.get("손료계수") or 0, "정비비계수": 0, "관리비계수": 0}
|
||||
if row.get("방식") != DIRECT:
|
||||
@@ -123,18 +130,41 @@ def keys_in(doc: Any) -> list[str]:
|
||||
return sorted(set(FIND.findall(json.dumps(doc, ensure_ascii=False, default=str))))
|
||||
|
||||
|
||||
def logic_key(row: dict) -> str:
|
||||
def _pos(v: Any) -> bool:
|
||||
try:
|
||||
return not isinstance(v, bool) and float(v) > 0
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def logic_key(row: dict) -> str | None:
|
||||
"""꼴(채운 칸) → 시간당 경비 식 키 · 못 고름(직종 없는 조종원 · 수 아닌 연료 글) = None ·
|
||||
계수 셋(상각 · 정비 · 관리) 없음 = 손료계수 한 칸 식(연료만) · 그 밖은 그 식 + 모자란 칸 까닭."""
|
||||
if row.get("방식") == DIRECT:
|
||||
return DIRECT_KEY
|
||||
vals = row.get("값") or {}
|
||||
return LOGIC[(float(vals.get("주연료") or 0) > 0, float(vals.get("조종원") or 0) > 0)]
|
||||
fuel, crew, power = (_pos(vals.get(k)) for k in ("주연료", "조종원", "전력"))
|
||||
job = bool(vals.get("조종원직종"))
|
||||
three = all(vals.get(k) is not None for k in COEFS)
|
||||
if vals.get("주연료") not in (None, "", 0) and not fuel:
|
||||
return None
|
||||
if fuel and power:
|
||||
return FUEL_POWER_CREW if crew and job else None if crew else FUEL_POWER
|
||||
if power:
|
||||
return POWER_CREW if crew and job else None if crew else POWER
|
||||
if fuel and not crew:
|
||||
return LOGIC[(True, False)] if three else ONE_FUEL
|
||||
if crew:
|
||||
return LOGIC[(fuel, True)] if job else JOB_IN if fuel else None
|
||||
return LOGIC[(False, False)]
|
||||
|
||||
|
||||
def logic_for(code: str) -> dict | None:
|
||||
"""내 기계 원문번호 → 시간당 경비 식(채운 칸으로 고름) · 내 기계 아님 = None."""
|
||||
index = whole()
|
||||
row = (index.numbers.get("EQ") or {}).get(code)
|
||||
if row is None or not mine(row.get("키")) or row.get("방식") == DIRECT:
|
||||
return None
|
||||
return index.logic(logic_key(row))
|
||||
key = logic_key(row) if row is not None and mine(row.get("키")) else None
|
||||
return index.logic(key) if key else None
|
||||
|
||||
|
||||
def hourly(row: dict, index: Any) -> dict[str, Any]:
|
||||
@@ -142,11 +172,21 @@ def hourly(row: dict, index: Any) -> dict[str, Any]:
|
||||
head = {"이름": row.get("이름"), "규격": row.get("규격"), "원문번호": row.get("원문번호")}
|
||||
if row.get("방식") == DIRECT:
|
||||
sums = {k: float((row.get("시간당") or {}).get(k) or 0) for k in COSTS}
|
||||
return {"ok": True, **head, "로직": None, "직접": True, **sums, "계": sum(sums.values())}
|
||||
return {
|
||||
"ok": True,
|
||||
**head,
|
||||
"로직": DIRECT_KEY,
|
||||
"직접": True,
|
||||
**sums,
|
||||
"계": sum(sums.values()),
|
||||
}
|
||||
import master_cut as mcut
|
||||
|
||||
mf = m01.cm.mf
|
||||
logic = index.logic(logic_key(row))
|
||||
key = logic_key(row)
|
||||
if key is None:
|
||||
return {"ok": False, **head, "reason": "식 없음 — 조종원직종 없음"}
|
||||
logic = index.logic(key)
|
||||
given = {i["이름"]: (i.get("고르기") or [""])[0] for i in logic.get("입력") or []}
|
||||
given["기계"] = str(row.get("원문번호"))
|
||||
try:
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
* ========================================================================== */
|
||||
|
||||
const TEXT = {
|
||||
Title: "내 기계",
|
||||
Items_Title: "내 품목",
|
||||
Personal_Title: "개인관리",
|
||||
Title: "기계",
|
||||
Items_Title: "자재",
|
||||
Crumb_Machine: "개인관리 › 기계",
|
||||
Crumb_Items: "개인관리 › 자재",
|
||||
New: "새 기계",
|
||||
Copy_Title: "복사해 내 기계로",
|
||||
Find: "이름 · 규격 · 키로 찾기",
|
||||
@@ -38,8 +41,6 @@ const TEXT = {
|
||||
No_Uses: "쓰는 단가산출 없음",
|
||||
Leave_Ask: "저장 안 한 고침을 버리고 닫을까?",
|
||||
Mine: "내 기계",
|
||||
Side_Hint: "펼치면 가운데에 목록 · 양식",
|
||||
Items_Side_Hint: "펼치면 가운데에 모아 보기(읽기)",
|
||||
Won: "원",
|
||||
Labor: "노",
|
||||
Material: "재",
|
||||
@@ -50,7 +51,7 @@ const TEXT = {
|
||||
// 기계 고르기 창 · 줄 쌓기
|
||||
Pick_Group: "내 기계",
|
||||
Pick_New: "새 기계 만들기",
|
||||
Pick_Direct: "시간당 값 직접 기계 — 줄 쌓기 식이 없어 못 넣음",
|
||||
Pick_Direct: "시간당 경비 식이 없는 기계 — 줄 쌓기에 못 넣음",
|
||||
To_Mine: "복사해 내 기계로",
|
||||
// 내 품목
|
||||
Items_Note:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* =============================================================================
|
||||
* M02_MyMachine_View.ts
|
||||
* M02 「내 기계」 화면(PLAN 821-3 · 요구서 796 3 장) — 좌측 「내 기계」 칸을 펼치면 가운데에 뜸(개인 층).
|
||||
* M02 「내 기계」 화면(PLAN 821-3 · 요구서 796 3 장) — 좌측 「개인관리」 › [기계] 단추로 가운데에 뜸(개인 층).
|
||||
* 목록 = 이름 · 규격 · 방식 · 시간당 경비(서버 셈) · 쓰는 곳 수 · 넣은 날 · 고친 날 · 찾기 · [새 기계]
|
||||
* 줄 누름 = 양식 한 장(`M02_MyMachine_Form`) · [목록] = 저장 안 한 고침이면 물음 · 저장 · 지우기 뒤 목록 다시
|
||||
* ========================================================================== */
|
||||
@@ -33,7 +33,7 @@ export function buildMyMachineView(opt: MyMachineViewOptions): HTMLElement {
|
||||
const hint = createText("");
|
||||
const fail = el("div");
|
||||
const table = createDataTable({
|
||||
caption: mt("Title"),
|
||||
caption: mt("Crumb_Machine"),
|
||||
columns: [
|
||||
{ key: "이름", label: mt("Name"), width: 180 },
|
||||
{ key: "규격", label: mt("Spec"), width: 120 },
|
||||
|
||||
@@ -132,8 +132,10 @@ def _modified(path: Path) -> str:
|
||||
return utc_from_timestamp(path.stat().st_mtime)
|
||||
|
||||
|
||||
def list_templates(layer_dir: str | Path) -> list[dict[str, Any]]:
|
||||
"""층 하나의 양식 목록 `[{종류, 이름, 판, 수정일}]` — 종류 · 이름 차례."""
|
||||
def list_templates(layer_dir: str | Path, version: Any = None) -> list[dict[str, Any]]:
|
||||
"""층 하나의 양식 목록 `[{종류, 이름, 판, 수정일}]` — 종류 · 이름 차례 ·
|
||||
`version` = 판 셈(없으면 `version_of` · 층 목록은 자국 기억 `M02_Layer_Index`)."""
|
||||
version = version or version_of
|
||||
base = Path(layer_dir)
|
||||
rows: list[dict[str, Any]] = []
|
||||
for kind in KINDS:
|
||||
@@ -147,7 +149,7 @@ def list_templates(layer_dir: str | Path) -> list[dict[str, Any]]:
|
||||
{
|
||||
"종류": kind,
|
||||
"이름": path.stem,
|
||||
"판": version_of(path),
|
||||
"판": version(path),
|
||||
"수정일": _modified(path),
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user