Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_3
This commit is contained in:
@@ -17,7 +17,7 @@ import { createInputField, createSelectField } from "@ui/ui_template_input";
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { formatLineMoney, formatMoney } from "../common_util/common_util_money";
|
||||
import { nameMachines, optionText } from "./formula_doc_options";
|
||||
import { liveOptions, nameMachines, optionText } from "./formula_doc_options";
|
||||
import "./calc_sheet.css";
|
||||
|
||||
export interface SheetLine {
|
||||
@@ -164,7 +164,7 @@ export function varRow(spec: SheetInput, value: string, onInput: (v: string) =>
|
||||
let control: HTMLElement;
|
||||
if (spec.고르기?.length) {
|
||||
const list = [...(value ? [] : [""]), ...spec.고르기.map(String)];
|
||||
const items = () => list.map((o) => ({ value: o, text: optionText(o) })); // 기계 번호 = 이름(요구 692-2)
|
||||
const items = () => liveOptions(list, value).map((o) => ({ value: o, text: optionText(o) })); // 기계 번호 = 이름(요구 692-2) · 끈 기계 뺌(842-2)
|
||||
const f = createSelectField({
|
||||
options: items(),
|
||||
value,
|
||||
|
||||
@@ -9,21 +9,31 @@
|
||||
import { createButton } from "@ui/ui_template_button";
|
||||
import { createInputField, createSelectField } from "@ui/ui_template_input";
|
||||
import { varId, type DocVar, type FormulaDoc } from "../common_util/common_util_formula_doc";
|
||||
import { cfMachineNames } from "../M02_MasterTemplete/DataFlow/M02_DF_Api";
|
||||
import { cfMachineNames, cfMachineOff } from "../M02_MasterTemplete/DataFlow/M02_DF_Api";
|
||||
import { mt } from "../M02_MasterTemplete/M02_MyMachine_Text";
|
||||
import { ft } from "./formula_doc_text";
|
||||
|
||||
const MACHINE = /^\d{4}-\d{4}$/;
|
||||
let names: Record<string, string> = {};
|
||||
let off = new Set<string>(); // 842-2 끈 기계 원문번호
|
||||
const machineInfo = (): Promise<Record<string, string>> =>
|
||||
Promise.all([cfMachineNames(), cfMachineOff()]).then(
|
||||
([got, gone]) => ((off = new Set(gone)), got),
|
||||
);
|
||||
|
||||
/** 고르기 글 — 기계 원문번호는 이름 · 규격을 앞에 */
|
||||
/** 고르기 글 — 기계 원문번호는 이름 · 규격을 앞에 · 끈 기계는 「· 비활성 기계」(842-2) */
|
||||
export const optionText = (x: string, known: Record<string, string> = names): string =>
|
||||
known[x] ? `${known[x]} (${x})` : x;
|
||||
known[x] ? `${known[x]} (${x})${off.has(x) ? ` · ${ft("Machine_Off")}` : ""}` : x;
|
||||
|
||||
/** 고르기 목록 — 끈 기계 뺌(지금 고른 값은 남김 · 842-2) */
|
||||
export const liveOptions = (list: string[], now: string): string[] =>
|
||||
list.filter((x) => x === now || !off.has(x));
|
||||
|
||||
/** 목록이 모두 원문번호(빈 칸 빼고)면 이름을 받아 `redraw`(글만 다시) — 수식 탭 옵션 · 단가산출 상세 옵션 */
|
||||
export function nameMachines(list: string[], redraw: () => void): void {
|
||||
const codes = list.filter(Boolean);
|
||||
if (codes.length && codes.every((x) => MACHINE.test(x)) && codes.some((x) => !(x in names)))
|
||||
void cfMachineNames().then((got) => {
|
||||
void machineInfo().then((got) => {
|
||||
names = got;
|
||||
redraw();
|
||||
});
|
||||
@@ -35,7 +45,7 @@ export function machineText(x: unknown, redraw: () => void): string | null {
|
||||
if (typeof x !== "string" || !MACHINE.test(x)) return null;
|
||||
if (!(x in names) && !asked) {
|
||||
asked = true;
|
||||
void cfMachineNames().then((got) => {
|
||||
void machineInfo().then((got) => {
|
||||
names = { ...names, ...got };
|
||||
redraw();
|
||||
});
|
||||
@@ -59,7 +69,8 @@ export function buildOptions(
|
||||
const now = k in given ? given[k] : v.연결?.값;
|
||||
if (kind === "선택") {
|
||||
const list = (v.연결?.목록 ?? []).map(String);
|
||||
const items = () => list.map((x) => ({ value: x, text: optionText(x) }));
|
||||
const items = () =>
|
||||
liveOptions(list, String(now ?? "")).map((x) => ({ value: x, text: optionText(x) }));
|
||||
const f = createSelectField({
|
||||
label,
|
||||
options: items(),
|
||||
|
||||
@@ -49,6 +49,7 @@ const TEXT = {
|
||||
By_Line: ["식 줄로 정의", "Defined by a line"],
|
||||
Pick_First: ["찾아 고름", "Search and pick"],
|
||||
Material: ["재료 고르기", "Material pick"],
|
||||
Machine_Off: ["비활성 기계", "Inactive machine"],
|
||||
None: ["없음", "None"],
|
||||
Open_Sub: ["식 펼침", "Show equation"],
|
||||
Bad_Sign: ["기호 꼴 아님 · 같은 기호 있음", "Bad or duplicate symbol"],
|
||||
|
||||
@@ -139,14 +139,18 @@ def _none() -> dict[str, float | None]:
|
||||
def _fixed(line: dict[str, Any], books: dict[str, Any]) -> dict[str, float | None]:
|
||||
"""고정 줄 단가(705-1) — 줄 단가 그대로 · 이은 자재가 관급 · 별도계상이면 재료 0(841-1 단가 책
|
||||
값으로 다시 안 덮음)."""
|
||||
own, link = line.get("own") or {}, line.get("link") or {}
|
||||
out = {part: num(own.get(part)) or 0.0 for part in PARTS}
|
||||
row = books["material"].get(link.get("material")) if link.get("material") else None
|
||||
if row and row.get("supply") in SUPPLY:
|
||||
out = {part: num((line.get("own") or {}).get(part)) or 0.0 for part in PARTS}
|
||||
if _fixed_apart(line, books):
|
||||
out["material"] = 0.0
|
||||
return out
|
||||
|
||||
|
||||
def _fixed_apart(line: dict[str, Any], books: dict[str, Any]) -> bool:
|
||||
"""고정 줄이 이은 자재가 관급 · 별도(842-1) — 대가 안 0 · 그 몫은 자재대(`aside`)로."""
|
||||
key = (line.get("link") or {}).get("material")
|
||||
return bool(key) and (books["material"].get(key) or {}).get("supply") in SUPPLY
|
||||
|
||||
|
||||
def _base(spec: str, lines: list[dict[str, Any]], src: list) -> float | None:
|
||||
"""% 줄 밑수 — `part:비목` 그 비목 줄 금액 합 · `lines:labor` 노무 줄 금액 합 ·
|
||||
`line:이름` 그 줄 · `bundle:labor|material|machine` 그 종류 줄(+ 그 이음 고정 줄) 합 ·
|
||||
@@ -212,6 +216,9 @@ def _block(
|
||||
}
|
||||
if line["kind"] == "material" and _apart(line, books): # 관급 · 별도계상 = 대가 안 0원
|
||||
extra["aside"], prices["material"], amounts["material"] = amounts["material"], 0.0, 0.0
|
||||
if line["kind"] == "fixed" and _fixed_apart(line, books): # 842-1 이은 자재 = 자재대 몫
|
||||
mat = num((line.get("own") or {}).get("material")) or 0.0
|
||||
extra["aside"] = None if qty is None else cut(mat * qty, line.get("place"))
|
||||
ref = {"ref": {"key": line["key"]}} if line.get("key") else {} # 집계표(145-5) 이음
|
||||
ask = {"why": line["ask"]} if qty is None and line.get("ask") else {} # 723-4
|
||||
lines.append(
|
||||
|
||||
@@ -121,14 +121,18 @@ function linePrices(line: Row, books: Row, sums: Record<string, Row>): Parts {
|
||||
/** 고정 줄 단가(705-1) — 줄 단가 그대로 · 이은 자재가 관급 · 별도계상이면 재료 0(짝 `_fixed`) */
|
||||
function fixedPrices(line: Row, books: Row): Parts {
|
||||
const own: Row = line.own || {};
|
||||
const link: Row = line.link || {};
|
||||
const out = zeros();
|
||||
for (const part of PARTS) out[part] = num(own[part]) ?? 0;
|
||||
const row: Row | undefined = link.material ? books.material[link.material] : undefined;
|
||||
if (row && (SUPPLY as readonly string[]).includes(row.supply)) out.material = 0;
|
||||
if (fixedApart(line, books)) out.material = 0;
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 고정 줄이 이은 자재가 관급 · 별도(842-1) — 대가 안 0 · 그 몫은 자재대(`aside`)로(짝 `_fixed_apart`) */
|
||||
function fixedApart(line: Row, books: Row): boolean {
|
||||
const key: string | undefined = (line.link || {}).material;
|
||||
return !!key && (SUPPLY as readonly string[]).includes((books.material[key] || {}).supply);
|
||||
}
|
||||
|
||||
/** % 줄 밑수 — part:비목 그 비목 줄 금액 합 · lines:labor 노무 줄 금액 합 · line:이름 그 줄 ·
|
||||
* bundle:labor|material|machine 그 종류 줄(+ 그 이음 고정 줄) 합 · rows:0,2 블록 그 차례 줄(src cid) 합
|
||||
* (778-4 · 짝 `.py` `_base`) */
|
||||
@@ -204,6 +208,11 @@ function block(b: Row, books: Row, sums: Record<string, Row>, gaps: Gap[], own:
|
||||
prices.material = 0;
|
||||
amounts.material = 0;
|
||||
}
|
||||
if (line.kind === "fixed" && fixedApart(line, books)) {
|
||||
// 842-1 이은 자재 = 자재대 몫(짝 `_block`)
|
||||
extra.aside =
|
||||
qty === null ? null : cut((num((line.own || {}).material) ?? 0) * qty, line.place);
|
||||
}
|
||||
const ref = line.key ? { ref: { key: line.key } } : {}; // 집계표(145-5) 이음
|
||||
const ask: Row = qty === null && line.ask ? { why: line.ask } : {}; // 723-4
|
||||
lines.push({ ...line, ...ref, ...ask, qty, prices, amounts, ...extra });
|
||||
|
||||
@@ -168,6 +168,8 @@ def _fixed_lines(block: dict[str, Any], line: dict[str, Any]) -> list[dict[str,
|
||||
link = line.get("link") or {}
|
||||
pairs = [("machine", link.get("machine"))] if link.get("machine") else []
|
||||
pairs = pairs or [(k, link.get(k)) for k in ("labor", "material") if link.get(k)]
|
||||
if "aside" in line and link.get("material"): # 842-1 이은 자재 별도 · 관급 = 자재대 몫
|
||||
pairs = [(k, v) for k, v in pairs if k != "material"] + [("aside", link["material"])]
|
||||
if not pairs:
|
||||
pairs = [("fixed", f"{block['key']}|{line.get('cid') or line.get('name')}")]
|
||||
return [{"kind": k, "key": str(v), "qty": line["qty"]} for k, v in pairs]
|
||||
|
||||
@@ -237,7 +237,7 @@ def find(q: str, limit: int = 30) -> list[dict]:
|
||||
for r in _rows():
|
||||
hay = " ".join(str(r.get(k) or "") for k in ("이름", "규격", "별칭", "상세구분", "키"))
|
||||
hay = hay.lower()
|
||||
if all(w in hay for w in words):
|
||||
if all(w in hay for w in words) and not r.get("비활성"): # 끈 기계 뺌(842-2)
|
||||
out.append(_brief(r))
|
||||
if len(out) >= limit:
|
||||
break
|
||||
|
||||
@@ -241,8 +241,10 @@ def rows(
|
||||
hits = [r for r in hits if sub in {r.get(s) for s in cm.MERGED.values()}]
|
||||
if detail: # 인력 상세구분 · 기계 상세구분(기종)
|
||||
hits = [r for r in hits if r.get("상세구분") == detail]
|
||||
if state: # 인력 상태
|
||||
hits = [r for r in hits if r.get("상태") == state]
|
||||
if state: # 인력 상태 · 기계 「켜짐」 = 비활성 뺌(842-2)
|
||||
hits = [
|
||||
r for r in hits if (not r.get(cm.OFF) if state == "켜짐" else r.get("상태") == state)
|
||||
]
|
||||
if unlinked: # 품셈재료 — 아직 못 이은 줄
|
||||
hits = [r for r in hits if not r.get("연결")]
|
||||
page, size = max(page, 1), min(max(size, 1), 500)
|
||||
@@ -463,7 +465,7 @@ def elements(group: str, q: str, limit: int) -> dict:
|
||||
if data.get("그룹") != group:
|
||||
continue
|
||||
for row in data.get(items_key(data)) or []:
|
||||
if _hit(row, q):
|
||||
if _hit(row, q) and not row.get(cm.OFF): # 끈 기계 뺌(842-2)
|
||||
more = {"별칭": row.get("별칭") or ""} if group == "로직" else {} # 208-1
|
||||
hits.append({**_brief(str(row.get("키")), row), "file": name, **more})
|
||||
return {"total": len(hits), "items": hits[: min(max(limit, 1), 200)]}
|
||||
|
||||
@@ -70,7 +70,8 @@ export function openPickTable(opt: PickTableOptions): void {
|
||||
const mine = ++seq;
|
||||
try {
|
||||
const q = search.value.trim();
|
||||
const got = await fetchRows(FILES[opt.kind], page, size, q);
|
||||
const on = opt.kind === "기계" ? "켜짐" : ""; // 끈 기계 뺌(842-2)
|
||||
const got = await fetchRows(FILES[opt.kind], page, size, q, false, "", "", on);
|
||||
if (mine !== seq) return; // 최신 응답만
|
||||
if (!got.total && fallback) {
|
||||
const next = [...(q.includes(" ") ? [q.split(/\s+/)[0]] : []), ...(opt.alt ?? [])].find(
|
||||
|
||||
@@ -66,3 +66,17 @@ table.m01-master__grid.m01-machine__grid {
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 「사용」 열(842-2) — 스위치 · 끈 줄은 흐림 */
|
||||
.m01-machine__grid th.m01-machine__use {
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
.m01-machine__grid td.m01-machine__use {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.m01-machine__row.is-off td:not(.m01-machine__use) {
|
||||
color: var(--color-text-muted);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@@ -3,15 +3,18 @@
|
||||
* M01 기계 목록(PLAN 821-2 · 요구 790-2) — 표는 목록용 열만(이름 · 규격 · 구분 · 가격 · 손료계수 ·
|
||||
* 주연료 · 인력 줄 요약 · 시간당 경비(서버 값) · 별칭 · 비고) · 줄을 누르면 상세 페이지 · [새 기계] = 양식.
|
||||
* 단위 · 출처 열 없음 · 고친 것 · 새 줄 · 지울 줄은 초안(노랑 · 새 · 지움 표시) — 저장은 페이지 [저장].
|
||||
* 「사용」 열(842-2) = 스위치 · 끄면 줄 맨 위 칸 `비활성: true`(초안 → [저장]) · 끈 줄은 흐림 ·
|
||||
* 머리 「비활성 같이 보기」 끄면 서버가 끈 줄을 뺌(`rows` state 「켜짐」).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { createToggle } from "@ui/ui_template_toggle";
|
||||
import { guessRows, pageKeepFirst, watchFit } from "@ui/ui_template_pager";
|
||||
import { code, crewOf } from "../common_util/common_util_machine_crew";
|
||||
import { fetchRows, type Row, type RowsPage } from "./M01_MasterData_Api_Fetch";
|
||||
import { onDraftChange, peek, same } from "./M01_MasterData_Draft";
|
||||
import { editRow, onDraftChange, peek, same } from "./M01_MasterData_Draft";
|
||||
import { fetchMachineHourly } from "./M01_MasterData_Machine_Api";
|
||||
import { buildPager } from "./M01_MasterData_UI_Cells";
|
||||
import { saveView, type Pick } from "./M01_MasterData_UI_Side";
|
||||
@@ -41,6 +44,8 @@ const COLS: [string, (r: Row) => string][] = [
|
||||
],
|
||||
];
|
||||
const val = (r: Row): Record<string, unknown> => (r["값"] ?? {}) as Record<string, unknown>;
|
||||
const OFF = "비활성"; // 842-2 기계 줄 맨 위 칸 — true 만 · 켜짐 = 칸 없음
|
||||
let showOff = true; // 「비활성 같이 보기」(페이지 안에서 기억)
|
||||
|
||||
/** 돌려받은 함수 = 걷기. `open(target)` = 상세(키 · 새 줄 `add:<차례>` · 새 기계 `new`). */
|
||||
export function renderMachines(
|
||||
@@ -56,9 +61,31 @@ export function renderMachines(
|
||||
const hourly = new Map<string, string>();
|
||||
|
||||
const hourText = (key: string): string => hourly.get(key) ?? "…";
|
||||
/** 「사용」 칸 — 스위치(새 줄은 빈 칸) · 바꾸면 초안 */
|
||||
const useCell = (row: Row, cur: Row, key: string): HTMLElement => {
|
||||
const td = el("td", { className: "m01-machine__use" });
|
||||
if (!key || !data) return td;
|
||||
const version = data.version;
|
||||
const sw = createToggle({
|
||||
label: "",
|
||||
size: 20,
|
||||
checked: !cur[OFF],
|
||||
onChange: (on) => {
|
||||
const next: Row = { ...cur };
|
||||
if (on) delete next[OFF];
|
||||
else next[OFF] = true;
|
||||
editRow(file, version, key, row, next);
|
||||
},
|
||||
});
|
||||
sw.root.setAttribute("aria-label", L("M01_MachineUse"));
|
||||
sw.root.addEventListener("click", (e) => e.stopPropagation());
|
||||
td.classList.toggle("is-changed", !!cur[OFF] !== !!row[OFF]);
|
||||
td.append(sw.root);
|
||||
return td;
|
||||
};
|
||||
const line = (row: Row, cur: Row, cls: string, target: string, key = ""): HTMLElement => {
|
||||
const tr = el("tr", {
|
||||
className: `m01-machine__row ${cls}`.trim(),
|
||||
className: `m01-machine__row ${cls} ${cur[OFF] ? "is-off" : ""}`.trim(),
|
||||
attrs: { "data-key": key || target },
|
||||
children: [
|
||||
...COLS.map(([, get]) => {
|
||||
@@ -74,6 +101,7 @@ export function renderMachines(
|
||||
}),
|
||||
el("td", { text: String(cur["별칭"] ?? ""), attrs: { title: String(cur["별칭"] ?? "") } }),
|
||||
el("td", { text: String(cur["비고"] ?? ""), attrs: { title: String(cur["비고"] ?? "") } }),
|
||||
useCell(row, cur, key),
|
||||
],
|
||||
});
|
||||
tr.addEventListener("click", () => open(target));
|
||||
@@ -94,12 +122,21 @@ export function renderMachines(
|
||||
}
|
||||
const add = createButton({ label: L("M01_MachineNew"), variant: "ghost", iconStart: "plus" });
|
||||
add.addEventListener("click", () => open("new"));
|
||||
const offSwitch = createToggle({
|
||||
label: L("M01_MachineShowOff"),
|
||||
checked: showOff,
|
||||
onChange: (on) => {
|
||||
showOff = on;
|
||||
page = 1;
|
||||
void load();
|
||||
},
|
||||
});
|
||||
const empty =
|
||||
d.rows.length || draft?.adds.length
|
||||
? []
|
||||
: [createEmpty({ text: L("M01_NoRows"), small: true })];
|
||||
host.replaceChildren(
|
||||
el("div", { className: "ui-toolbar m01-master__bar", children: [add] }),
|
||||
el("div", { className: "ui-toolbar m01-master__bar", children: [add, offSwitch.root] }),
|
||||
el("div", {
|
||||
className: "ui-table-wrap m01-master__grid-wrap m01-machine__wrap",
|
||||
children: [
|
||||
@@ -114,6 +151,7 @@ export function renderMachines(
|
||||
el("th", { text: `${L("M01_MachineHourly")}(원)` }),
|
||||
el("th", { text: "별칭" }),
|
||||
el("th", { text: "비고" }),
|
||||
el("th", { className: "m01-machine__use", text: L("M01_MachineUse") }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
@@ -155,7 +193,8 @@ export function renderMachines(
|
||||
const load = async (): Promise<void> => {
|
||||
const mine = ++ticket;
|
||||
try {
|
||||
const got = await fetchRows(file, page, size, q, false, pick.sub, pick.detail, pick.state);
|
||||
const state = showOff ? pick.state : "켜짐";
|
||||
const got = await fetchRows(file, page, size, q, false, pick.sub, pick.detail, state);
|
||||
if (mine !== ticket) return;
|
||||
data = got;
|
||||
saveView(pick.group, { page: data.page });
|
||||
|
||||
@@ -28,3 +28,9 @@ export function cfMachineNames(): Promise<Record<string, string>> {
|
||||
.catch(() => ((machineNames = null), {}));
|
||||
return machineNames;
|
||||
}
|
||||
|
||||
/** 끈 기계 원문번호(842-2) — 이름과 함께 한 번 받아 둠(못 받으면 빈 목록) */
|
||||
export const cfMachineOff = (): Promise<string[]> =>
|
||||
fetch(`${API_BASE_URL}/m02/df/machine-off`, { credentials: "include" })
|
||||
.then((r) => (r.ok ? (r.json() as Promise<string[]>) : []))
|
||||
.catch(() => []);
|
||||
|
||||
@@ -33,6 +33,17 @@ def get_machine_names() -> dict:
|
||||
return out
|
||||
|
||||
|
||||
@router.get("/machine-off")
|
||||
def get_machine_off() -> list[str]:
|
||||
"""끈 기계 원문번호(842-2) — 그 번호 기계가 모두 「비활성」 · 옵션 고르기 목록에서 뺌(고른 값은 남김)."""
|
||||
on, off = set(), set()
|
||||
for data in store.loaded()[0].values():
|
||||
if data.get("그룹") == "기계":
|
||||
for row in data.get(store.items_key(data)) or []:
|
||||
(off if row.get("비활성") else on).add(str(row.get("원문번호") or ""))
|
||||
return sorted(off - on - {""})
|
||||
|
||||
|
||||
@router.get("/standards")
|
||||
def get_standards() -> dict:
|
||||
try:
|
||||
|
||||
@@ -48,7 +48,8 @@ def get_materials(
|
||||
def get_rows(file: str, page: int = 1, size: int = 50, q: str = "", sub: str = "") -> dict:
|
||||
if not file.startswith(ROW_FILES):
|
||||
raise HTTPException(status_code=403, detail="요소 파일(인력 · 재료 · 기계)만 읽음")
|
||||
return m01.get_rows(file, page, size, q, False, sub, "", "")
|
||||
on = "켜짐" if file.startswith("기계") else "" # 끈 기계 뺌(842-2)
|
||||
return m01.get_rows(file, page, size, q, False, sub, "", on)
|
||||
|
||||
|
||||
@router.get("/table")
|
||||
|
||||
@@ -145,7 +145,7 @@ class Row:
|
||||
|
||||
@lru_cache(maxsize=4)
|
||||
def _table(whole: Any) -> tuple[list[Row], frozenset[str], dict[str, Row]]:
|
||||
rows = [Row(r) for r in whole.numbers.get("EQ", {}).values()]
|
||||
rows = [Row(r) for r in whole.numbers.get("EQ", {}).values() if not r.get("비활성")] # 842-2
|
||||
words = frozenset(w for r in rows for w in r.words)
|
||||
return rows, words, {r.key: r for r in rows}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ def find(q: str, limit: int = 30) -> list[dict]:
|
||||
if all(w in f"{r.get('이름')} {r.get('규격')} {r.get('키')}".lower() for w in words)
|
||||
]
|
||||
exact = (m01.loaded()[1].numbers.get("EQ") or {}).get(q.strip())
|
||||
head = [exact] if exact is not None and not mym.mine(exact.get("키")) else []
|
||||
head = [exact] if exact and not mym.mine(exact.get("키")) and not exact.get("비활성") else []
|
||||
m01_rows = [*head, *_m01m().find(q, limit)]
|
||||
seen = set()
|
||||
for r in m01_rows:
|
||||
|
||||
@@ -137,6 +137,9 @@ COLS = {
|
||||
}
|
||||
COLS["한국은행환율"] = COLS["유가전력"]
|
||||
ALIAS = "별칭" # 요소 줄 덧칸(815-2) — 열 한 벌 밖 · 있으면 빈 글 아님
|
||||
OFF = (
|
||||
"비활성" # 기계 줄 맨 위 칸(842-2) — 있으면 true 만 · 없음 = 켜짐 · 찾기 · 고르기 · 잇기에서 뺌
|
||||
)
|
||||
|
||||
|
||||
def _cols_of(row: dict) -> tuple:
|
||||
@@ -300,6 +303,8 @@ def check_form(name: str, data: dict) -> list[str]:
|
||||
out += _check_labor(where, row)
|
||||
if ALIAS in row and not (isinstance(row[ALIAS], str) and row[ALIAS].strip()):
|
||||
out.append(f"{where} · 「{ALIAS}」 이 빈 글 · 글 아님(빈 칸이면 칸을 두지 않음)")
|
||||
if OFF in row and (row[OFF] is not True or data.get("그룹") != "기계"):
|
||||
out.append(f"{where} · 「{OFF}」 는 기계 줄에만 true(켜짐이면 칸을 두지 않음)")
|
||||
if cols and _cols_of(row) != cols:
|
||||
out.append(f"{where} · 열이 한 벌이 아님 — 「{' · '.join(cols)}」")
|
||||
if book == "자재품목" and row.get(mm.LOAD) not in mm.LOADS:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000541",
|
||||
"비활성": true,
|
||||
"원문번호": "9010-0003",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000542",
|
||||
"비활성": true,
|
||||
"원문번호": "9010-0006",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000543",
|
||||
"비활성": true,
|
||||
"원문번호": "9010-0010",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000544",
|
||||
"비활성": true,
|
||||
"원문번호": "9010-0012",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000545",
|
||||
"비활성": true,
|
||||
"원문번호": "9010-0020",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000546",
|
||||
"비활성": true,
|
||||
"원문번호": "9010-0022",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000547",
|
||||
"비활성": true,
|
||||
"원문번호": "9010-0033",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000548",
|
||||
"비활성": true,
|
||||
"원문번호": "9010-0040",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000549",
|
||||
"비활성": true,
|
||||
"원문번호": "9010-0044",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000550",
|
||||
"비활성": true,
|
||||
"원문번호": "9010-0060",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000551",
|
||||
"비활성": true,
|
||||
"원문번호": "9010-0080",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000552",
|
||||
"비활성": true,
|
||||
"원문번호": "9010-0120",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000553",
|
||||
"비활성": true,
|
||||
"원문번호": "9010-0200",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000554",
|
||||
"비활성": true,
|
||||
"원문번호": "9020-0010",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000555",
|
||||
"비활성": true,
|
||||
"원문번호": "9020-0015",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000556",
|
||||
"비활성": true,
|
||||
"원문번호": "9020-0016",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000557",
|
||||
"비활성": true,
|
||||
"원문번호": "9020-0022",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000558",
|
||||
"비활성": true,
|
||||
"원문번호": "9020-0035",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000559",
|
||||
"비활성": true,
|
||||
"원문번호": "9020-0050",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000560",
|
||||
"비활성": true,
|
||||
"원문번호": "9020-0072",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000561",
|
||||
"비활성": true,
|
||||
"원문번호": "9020-0160",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000562",
|
||||
"비활성": true,
|
||||
"원문번호": "9020-0180",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000563",
|
||||
"비활성": true,
|
||||
"원문번호": "9020-0200",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000564",
|
||||
"비활성": true,
|
||||
"원문번호": "9030-0016",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000565",
|
||||
"비활성": true,
|
||||
"원문번호": "9030-0018",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000566",
|
||||
"비활성": true,
|
||||
"원문번호": "9030-0025",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000567",
|
||||
"비활성": true,
|
||||
"원문번호": "9030-0035",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000568",
|
||||
"비활성": true,
|
||||
"원문번호": "9030-0045",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000569",
|
||||
"비활성": true,
|
||||
"원문번호": "9030-0050",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000570",
|
||||
"비활성": true,
|
||||
"원문번호": "9030-0080",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000571",
|
||||
"비활성": true,
|
||||
"원문번호": "9030-0100",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000572",
|
||||
"비활성": true,
|
||||
"원문번호": "9030-0240",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000573",
|
||||
"비활성": true,
|
||||
"원문번호": "9040-0010",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000574",
|
||||
"비활성": true,
|
||||
"원문번호": "9040-0030",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000575",
|
||||
"비활성": true,
|
||||
"원문번호": "9040-0050",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000576",
|
||||
"비활성": true,
|
||||
"원문번호": "9040-0060",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000577",
|
||||
"비활성": true,
|
||||
"원문번호": "9040-0100",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000578",
|
||||
"비활성": true,
|
||||
"원문번호": "9040-0120",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000579",
|
||||
"비활성": true,
|
||||
"원문번호": "9040-0200",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000580",
|
||||
"비활성": true,
|
||||
"원문번호": "9040-0250",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000581",
|
||||
"비활성": true,
|
||||
"원문번호": "9040-0300",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000582",
|
||||
"비활성": true,
|
||||
"원문번호": "9040-0380",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000583",
|
||||
"비활성": true,
|
||||
"원문번호": "9040-0680",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000584",
|
||||
"비활성": true,
|
||||
"원문번호": "9050-0075",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000585",
|
||||
"비활성": true,
|
||||
"원문번호": "9050-0150",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000586",
|
||||
"비활성": true,
|
||||
"원문번호": "9050-0450",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000587",
|
||||
"비활성": true,
|
||||
"원문번호": "9050-0750",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000588",
|
||||
"비활성": true,
|
||||
"원문번호": "9050-0850",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000589",
|
||||
"비활성": true,
|
||||
"원문번호": "9060-0060",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000590",
|
||||
"비활성": true,
|
||||
"원문번호": "9060-0100",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000591",
|
||||
"비활성": true,
|
||||
"원문번호": "9060-0200",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000592",
|
||||
"비활성": true,
|
||||
"원문번호": "9060-0300",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000593",
|
||||
"비활성": true,
|
||||
"원문번호": "9060-0500",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000594",
|
||||
"비활성": true,
|
||||
"원문번호": "9060-0600",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000595",
|
||||
"비활성": true,
|
||||
"원문번호": "9070-0015",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000596",
|
||||
"비활성": true,
|
||||
"원문번호": "9070-0020",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000597",
|
||||
"비활성": true,
|
||||
"원문번호": "9080-0050",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000598",
|
||||
"비활성": true,
|
||||
"원문번호": "9080-0080",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000599",
|
||||
"비활성": true,
|
||||
"원문번호": "9080-0100",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000600",
|
||||
"비활성": true,
|
||||
"원문번호": "9080-0120",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000601",
|
||||
"비활성": true,
|
||||
"원문번호": "9080-0150",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000602",
|
||||
"비활성": true,
|
||||
"원문번호": "9080-0200",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000603",
|
||||
"비활성": true,
|
||||
"원문번호": "9080-0300",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000604",
|
||||
"비활성": true,
|
||||
"원문번호": "9080-0500",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000605",
|
||||
"비활성": true,
|
||||
"원문번호": "9080-0700",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000606",
|
||||
"비활성": true,
|
||||
"원문번호": "9080-1000",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000607",
|
||||
"비활성": true,
|
||||
"원문번호": "9080-1100",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000608",
|
||||
"비활성": true,
|
||||
"원문번호": "9080-1400",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000609",
|
||||
"비활성": true,
|
||||
"원문번호": "9080-1500",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000610",
|
||||
"비활성": true,
|
||||
"원문번호": "9080-1750",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000611",
|
||||
"비활성": true,
|
||||
"원문번호": "9080-2000",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000612",
|
||||
"비활성": true,
|
||||
"원문번호": "9080-3000",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000613",
|
||||
"비활성": true,
|
||||
"원문번호": "9090-0800",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000614",
|
||||
"비활성": true,
|
||||
"원문번호": "9090-1000",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000615",
|
||||
"비활성": true,
|
||||
"원문번호": "9090-1200",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000616",
|
||||
"비활성": true,
|
||||
"원문번호": "9090-1300",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000617",
|
||||
"비활성": true,
|
||||
"원문번호": "9090-1400",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000618",
|
||||
"비활성": true,
|
||||
"원문번호": "9090-1500",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"키": "EQ000619",
|
||||
"비활성": true,
|
||||
"원문번호": "9090-1600",
|
||||
"원문": "건설품셈",
|
||||
"구분": "[90]해상장비",
|
||||
|
||||
@@ -35,8 +35,8 @@ FILES = {
|
||||
#: 영월 = 내역 줄이 코드로만 가리킴(번 채움) · STmate 하위 호표 반올림 끝수 28 줄
|
||||
#: (줄당 1 원/단위 · 브레인 정함 = 그래도 이음)
|
||||
WANT = {
|
||||
# 841-1 이은 안내판 줄 = M02 사본 셈(엑셀보다 50,000 적음 · 노임 · 기계 덮개 걷음)
|
||||
"거창": (14, 68, 56, 12, 11, 8, 8, 384916227, 0, 50000),
|
||||
# 842-1 이은 안내판 줄 표지판지주 = 별도 → 대가 안 0 · 자재대 50,000 (직접비 합 = 엑셀)
|
||||
"거창": (14, 68, 56, 12, 11, 8, 8, 384916227, 0, 0),
|
||||
"대흥": (34, 171, 143, 28, 0, 26, 45, 710359136, 0, 0),
|
||||
"소광": (25, 117, 98, 19, 0, 0, 0, 173311386, 0, 0),
|
||||
"영월": (23, 138, 135, 3, 0, 21, 34, 767148700, 28, 2951),
|
||||
@@ -117,6 +117,28 @@ def test_실무_엑셀_통째_빠짐_0_합계_같음_층_쓰기_0(world, name):
|
||||
assert not [p for p in world["storage"].rglob("*") if p.is_file()] # 층 쓰기 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["거창", "대흥", "소광", "영월"])
|
||||
def test_대가_안_0_원_재료_자재대에_한_번(world, name):
|
||||
"""842-1 전수 — 별도 · 관급 이은 자재(재료 줄 · 고정 줄)마다 집계 `aside` 줄 · 4 자재대 · 관급
|
||||
자재대 수량 = 그 모음(사라짐 0) · 재료 줄 아닌 같은 자재 이중 집계 0."""
|
||||
from B09_Estimation.B09_Estimation_Summary import block_lines
|
||||
|
||||
got = world["run"](name)["payload"]
|
||||
sheets, aside = got["sheets"], got["inputs"].get("aside_qty") or {}
|
||||
apart = {r["key"] for r in sheets["compare"] if r["supply"] in ("별도", "관급")}
|
||||
items = {r["ref"]["key"]: r["qty"] for r in sheets["bill"]["rows"]
|
||||
if r.get("kind") == "item" and str(r["key"]).startswith("mat:")} # fmt: skip
|
||||
for tab in ("sg", "daega_blocks"):
|
||||
for block in sheets[tab]:
|
||||
lines = block_lines([block])[str(block["key"])]["lines"]
|
||||
for ln in block["lines"]:
|
||||
key = (ln.get("ref") or {}).get("key") or (ln.get("link") or {}).get("material")
|
||||
if key in apart and ln["kind"] in ("material", "fixed") and ln.get("qty"):
|
||||
assert any(x["kind"] == "aside" and x["key"] == key for x in lines), ln["name"]
|
||||
assert all(x["kind"] != "material" or x["key"] != key for x in lines)
|
||||
assert items == {k: v for k, v in aside.items() if k in apart}
|
||||
|
||||
|
||||
def test_대흥_총공사비_엑셀과_같음_하위는_참조사본(world):
|
||||
got = world["run"]("대흥")
|
||||
assert got["합계"]["우리총"] == got["합계"]["엑셀총"] == 1201879000
|
||||
@@ -145,7 +167,7 @@ def test_다시_부르기_같은_사본_두_번_셈_0(world):
|
||||
first = world["run"]("거창")
|
||||
again = world["run"]("거창", fresh=False)
|
||||
assert _libs(again["root"]) == _libs(first["root"])
|
||||
assert again["합계"]["우리직접"] == first["합계"]["우리직접"] == 384866227
|
||||
assert again["합계"]["우리직접"] == first["합계"]["우리직접"] == 384916227
|
||||
rows = again["user"]["bill"]["rows"]
|
||||
assert len({r["key"] for r in rows}) == len(rows)
|
||||
|
||||
|
||||
@@ -35,8 +35,11 @@ def test_기계_이름_표_문서_목록_전수() -> None:
|
||||
|
||||
def test_옵션_부품_글만_바꿈() -> None:
|
||||
src = (ROOT / "A00_Common/formula_doc_options.ts").read_text(encoding="utf-8")
|
||||
assert "known[x] ? `${known[x]} (${x})` : x" in src
|
||||
assert "list.map((x) => ({ value: x, text: optionText(x) }))" in src
|
||||
assert "known[x] ? `${known[x]} (${x})${off.has(x)" in src # 842-2 끈 기계 꼬리
|
||||
assert (
|
||||
'liveOptions(list, String(now ?? "")).map((x) => ({ value: x, text: optionText(x) }))'
|
||||
in src
|
||||
)
|
||||
assert "nameMachines(list, () => f.setOptions(items(), f.select.value));" in src
|
||||
api = (ROOT / "M02_MasterTemplete/DataFlow/M02_DF_Api.ts").read_text(encoding="utf-8")
|
||||
assert "machineNames ??= fetch(" in api # 한 번만 받음
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""842-1 B09 — 대가 안 0 원이 된 재료는 자재대(`aside`)에 꼭 한 번 · 겹쳐 세지 않음.
|
||||
|
||||
고정 줄이 이은 자재(`link.material`)가 별도 · 관급이면 대가 안 0 원 + 그 몫 = 줄 `aside` →
|
||||
집계표 `aside` 줄(자재대 수량) · 재료 줄 · 붙인 줄 · 관급도 같은 길 · 직접계산 = `aside` 없음.
|
||||
거울: `B09_Estimation_Calc_Labor.py` ↔ `.ts` (`fixedApart`).
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Summary import block_lines, totals # noqa: E402
|
||||
from resources.tester.test_145_2_b09_labor_calc import _b, _l, _same, _ts # noqa: E402
|
||||
|
||||
SHEETS = {
|
||||
"labor": [],
|
||||
"material": [
|
||||
{"key": "MA", "price": 50000, "supply": "별도"},
|
||||
{"key": "MB", "price": 800, "supply": "관급"},
|
||||
{"key": "MC", "price": 300, "supply": None},
|
||||
],
|
||||
"machine": [],
|
||||
"gaps": [],
|
||||
}
|
||||
|
||||
|
||||
def _fix(name, qty, mat, price, **link):
|
||||
own = {"labor": 0.0, "material": price, "expense": 0.0}
|
||||
return {
|
||||
"kind": "fixed",
|
||||
"name": name,
|
||||
"qty": qty,
|
||||
"own": own,
|
||||
"link": {"material": mat, **link},
|
||||
}
|
||||
|
||||
|
||||
DAEGA = {
|
||||
"blocks": [
|
||||
_b(
|
||||
"DG1",
|
||||
"품셈",
|
||||
"안내판",
|
||||
[
|
||||
_fix("지주", 1, "MA", 50000), # 별도 → 대가 안 0 · 자재대 50000
|
||||
_fix("관급 줄", 2, "MB", 800), # 관급 → 자재대(관급)
|
||||
_fix("직접 줄", 3, "MC", 300), # 직접계산 → 대가 안
|
||||
_fix(
|
||||
"기계 겸", 1, "MA", 50000, machine="X1"
|
||||
), # 기계 이음이 먼저여도 자재대 몫은 감
|
||||
{**_l("material", "MA", 4), "own": {"material": 50000}}, # 재료 줄도 같은 자재
|
||||
],
|
||||
),
|
||||
],
|
||||
"owners": {"x": "DG1"},
|
||||
}
|
||||
|
||||
|
||||
def test_고정_줄_자재_대가_안_0_자재대에_한_번():
|
||||
got = compute_labor(DAEGA, SHEETS)["daega_blocks"][0]
|
||||
by = {ln["name"]: ln for ln in got["lines"]}
|
||||
assert by["지주"]["amounts"]["material"] == 0 and by["지주"]["aside"] == 50000
|
||||
assert by["관급 줄"]["amounts"]["material"] == 0 and by["관급 줄"]["aside"] == 1600
|
||||
assert by["직접 줄"]["amounts"]["material"] == 900 and "aside" not in by["직접 줄"]
|
||||
assert got["material"] == 900 # 대가 안 = 직접계산 줄만
|
||||
lines = block_lines([got])[got["key"]]["lines"]
|
||||
assert [x for x in lines if x["kind"] == "material"] == [ # 직접 줄만 일반 재료 줄
|
||||
{"kind": "material", "key": "MC", "qty": 3}
|
||||
]
|
||||
aside = totals({"bill": {"rows": [{"kind": "item", "qty": 1, "ref": {"key": got["key"]}}]}},
|
||||
{got["key"]: {"lines": lines}})[0]["aside"] # fmt: skip
|
||||
assert aside == {"MA": 1 + 1 + 4, "MB": 2} # 재료 줄 4 + 고정 줄 둘 = 한 번씩 모음
|
||||
|
||||
|
||||
def test_거울_파이썬_TS():
|
||||
py = json.loads(json.dumps(compute_labor(DAEGA, SHEETS), ensure_ascii=False))
|
||||
assert _same(py, _ts(DAEGA, SHEETS, {})["out"])
|
||||
@@ -0,0 +1,118 @@
|
||||
"""842-2 기계 비활성 — 지킴 시험(쓰기는 임시 사본만).
|
||||
|
||||
① 틀 검사 — 기계 줄 맨 위 칸 `비활성` = true 만 · 다른 그룹 줄 · false 는 오류
|
||||
② 저장 왕복 — M01 저장 길이 표지를 받고(그 대 파일 하나) 지우면 칸이 사라짐 · 셈 값 그대로
|
||||
③ 끈 기계는 찾기 · 고르기 · 잇기에서 안 나옴 — 요소 찾기 창 · 비슷한 기계 · 불러오기 기계 잇기 ·
|
||||
목록 거름 「켜짐」 · 끈 원문번호 목록 · 셈(시간당)은 그대로
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "resources" / "master_data" / "scripts"))
|
||||
import master_machine_files as mmf # noqa: E402
|
||||
|
||||
from M01_MasterData import M01_MasterData_Machine as m01m # noqa: E402
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind # noqa: E402
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as mh # noqa: E402
|
||||
from M02_MasterTemplete.DataFlow import M02_DF_Router as dfr # noqa: E402
|
||||
|
||||
REAL = store.FOLDER
|
||||
cm, mk, M = store.cm, store.mk, mmf.NAME
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def master(tmp_path: Path, monkeypatch) -> Path:
|
||||
for path in REAL.glob("*.json"):
|
||||
if not path.name.startswith("_") or path.name == mk.BOOK.name:
|
||||
shutil.copy(path, tmp_path / path.name)
|
||||
shutil.copytree(REAL / mmf.DIR, tmp_path / mmf.DIR)
|
||||
monkeypatch.setattr(store, "FOLDER", tmp_path)
|
||||
return tmp_path
|
||||
|
||||
|
||||
def _rows() -> list[dict]:
|
||||
return store.read(M)[0]["줄"]
|
||||
|
||||
|
||||
def _part(*changes: dict) -> dict:
|
||||
return {"file": M, "version": store.read(M)[1], "changes": list(changes)}
|
||||
|
||||
|
||||
# ① 틀 검사 ──────────────────────────────────────────────────────────
|
||||
def test_form_check() -> None:
|
||||
data = copy.deepcopy(store.read(M)[0])
|
||||
data["줄"] = data["줄"][:3]
|
||||
assert cm.check_form(M, data) == []
|
||||
data["줄"][0][cm.OFF] = True
|
||||
assert cm.check_form(M, data) == []
|
||||
data["줄"][0][cm.OFF] = False
|
||||
assert any(cm.OFF in e for e in cm.check_form(M, data))
|
||||
labor = copy.deepcopy(store.read("인력.json")[0])
|
||||
labor["줄"] = labor["줄"][:1]
|
||||
labor["줄"][0][cm.OFF] = True
|
||||
assert any(cm.OFF in e for e in cm.check_form("인력.json", labor))
|
||||
|
||||
|
||||
# ② 저장 왕복 ───────────────────────────────────────────────────────
|
||||
def test_save_round_trip(master: Path) -> None:
|
||||
key = next(r["키"] for r in _rows() if not r.get(cm.OFF) and r["이름"].startswith("불도저"))
|
||||
row = copy.deepcopy(next(r for r in _rows() if r["키"] == key))
|
||||
before = mh.hourly(key)
|
||||
snap = {p.name: p.read_bytes() for p in (master / mmf.DIR).glob("*.json")}
|
||||
store.save([_part({"op": "edit", "key": key, "row": {**row, cm.OFF: True}})])
|
||||
after = {p.name: p.read_bytes() for p in (master / mmf.DIR).glob("*.json")}
|
||||
assert {n for n in after if after[n] != snap[n]} == {f"{key}.json"}
|
||||
got = next(r for r in _rows() if r["키"] == key)
|
||||
assert got[cm.OFF] is True and got["값"] == row["값"]
|
||||
assert mh.hourly(key) == before # 셈은 그대로(쓰던 단가산출 · 사본이 깨지지 않음)
|
||||
assert key not in {x["ref"] for x in store.elements("기계", "불도저", 200)["items"]}
|
||||
store.save([_part({"op": "edit", "key": key, "row": row})])
|
||||
assert (master / mmf.DIR / f"{key}.json").read_bytes() == snap[f"{key}.json"]
|
||||
|
||||
|
||||
# ③ 찾기 · 고르기 · 잇기에서 뺌 ──────────────────────────────────────
|
||||
def _off() -> list[dict]:
|
||||
got = [r for r in _rows() if r.get(cm.OFF)]
|
||||
if not got:
|
||||
pytest.skip("끈 기계 없음")
|
||||
return got
|
||||
|
||||
|
||||
def test_hidden_everywhere() -> None:
|
||||
off = _off()
|
||||
keys = {r["키"] for r in off}
|
||||
for r in off[:: max(1, len(off) // 6)]:
|
||||
name = str(r["상세구분"] or r["이름"]).replace(" ", "")
|
||||
assert not keys & {x["ref"] for x in store.elements("기계", name, 200)["items"]}
|
||||
assert not keys & {x["키"] for x in m01m.find(name, 200)}
|
||||
with mfind.m01_only():
|
||||
got = mfind.find(r["이름"], r.get("규격") or "")
|
||||
assert got["키"] not in keys and not keys & {x["키"] for x in got["후보"]}
|
||||
listed = store.rows(M, 1, 500, "", state="켜짐")
|
||||
assert not keys & {r["키"] for r in listed["rows"]}
|
||||
assert listed["total"] == len(_rows()) - len(keys)
|
||||
assert store.rows(M, 1, 1, "")["total"] == len(_rows()) # 같이 보기 = 전부
|
||||
codes = {str(r["원문번호"]) for r in off} - {
|
||||
str(r["원문번호"]) for r in _rows() if not r.get(cm.OFF)
|
||||
}
|
||||
assert set(dfr.get_machine_off()) == codes
|
||||
assert all(c in dfr.get_machine_names() for c in codes) # 고른 값 글은 그대로
|
||||
|
||||
|
||||
def test_sea_machines_off() -> None:
|
||||
"""842-2 ㉣ 해상 기계([90]해상장비 = 선원류 칸 있는 대)는 끔 — 사용자가 다시 켤 수 있으므로 켠 대는 알림만."""
|
||||
sea = [r for r in _rows() if "해상" in str(r.get("구분"))]
|
||||
assert len(sea) == 79
|
||||
on = [r["키"] for r in sea if not r.get(cm.OFF)]
|
||||
if on:
|
||||
pytest.skip(f"다시 켠 해상 기계 {len(on)} 대 — 사용자 판단")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user