Merge remote-tracking branch 'origin/main_desktop_1' into main_desktop_1
This commit is contained in:
@@ -27,6 +27,9 @@ class CalcBody(BaseModel):
|
||||
inputs: dict[str, Any] = {}
|
||||
row: dict[str, Any] | None = None # 저장 전 고친 로직 줄(없으면 저장된 파일로)
|
||||
file: str | None = None # 새 로직이면 더할 로직 파일
|
||||
고름: dict[
|
||||
str, str
|
||||
] = {} # {호표 줄 차례(0부터): 자재 키} — 그 줄만 이 품목으로 셈 · 저장 안 함
|
||||
|
||||
|
||||
class Change(BaseModel):
|
||||
@@ -212,6 +215,12 @@ def get_logic(key: str) -> dict:
|
||||
return _call(store.logic, key)
|
||||
|
||||
|
||||
@router.get("/logic/auto")
|
||||
def get_logic_auto(key: str) -> dict:
|
||||
"""견본 입력 — 마스터 보기용 자동값(프로젝트 기본값 아님)."""
|
||||
return _call(store.auto, key)
|
||||
|
||||
|
||||
@router.get("/elements")
|
||||
def get_elements(group: str, q: str = "", limit: int = 50) -> dict:
|
||||
return _call(store.elements, group, q, limit)
|
||||
@@ -238,13 +247,15 @@ def get_materials(
|
||||
|
||||
@router.post("/calc")
|
||||
def post_calc(body: CalcBody) -> dict:
|
||||
return _call(store.calc, body.key, body.inputs, body.row, body.file)
|
||||
with store.cm.mf.mp.choosing(body.고름):
|
||||
return _call(store.calc, body.key, body.inputs, body.row, body.file)
|
||||
|
||||
|
||||
@router.post("/text")
|
||||
def post_text(body: CalcBody) -> dict:
|
||||
"""읽는 식 줄 — 몸은 `/calc` 와 같음."""
|
||||
return _call(store.text, body.key, body.inputs, body.row, body.file)
|
||||
with store.cm.mf.mp.choosing(body.고름):
|
||||
return _call(store.text, body.key, body.inputs, body.row, body.file)
|
||||
|
||||
|
||||
@router.post("/save")
|
||||
|
||||
@@ -19,6 +19,7 @@ SCRIPTS = Path(__file__).resolve().parents[1] / "resources/master_data/scripts"
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
import check_master as cm # noqa: E402
|
||||
import master_auto as mau # noqa: E402
|
||||
import master_combo as mcb # noqa: E402,F401 (`_Store_Combo` 가 `store.mcb` 로 씀)
|
||||
import master_copy as mcp # noqa: E402,F401 (`_Store_Make` 가 `store.mcp` 로 씀)
|
||||
import master_keys as mk # noqa: E402
|
||||
@@ -87,10 +88,8 @@ def chapter_of(file: str, data: dict | None = None) -> str:
|
||||
|
||||
|
||||
def _hit(row: dict, q: str) -> bool:
|
||||
q = q.lower()
|
||||
return not q or any(
|
||||
q in str(row.get(k, "")).lower() for k in ("키", "원문번호", "이름", "옛이름")
|
||||
)
|
||||
q = _squash(q) # 띄어쓰기 무시 — 「골 함 석」 ↔ 「함석」
|
||||
return not q or any(q in _squash(row.get(k, "")) for k in ("키", "원문번호", "이름", "옛이름"))
|
||||
|
||||
|
||||
def _book(data: dict) -> str | None:
|
||||
@@ -547,6 +546,7 @@ def calc(key: str, inputs: dict, row: dict | None = None, file: str | None = Non
|
||||
if row is not None:
|
||||
key = _swap(files, str(row.get("키") or key), row, file)
|
||||
try:
|
||||
given = cm.mf.fill_empty(cm.mf.Master(files).logic(key), given)
|
||||
result = cm.calc(files, key, given)
|
||||
except (cm.mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError) as e:
|
||||
return {"ok": False, "reason": str(e) or type(e).__name__}
|
||||
@@ -560,6 +560,15 @@ def calc(key: str, inputs: dict, row: dict | None = None, file: str | None = Non
|
||||
}
|
||||
|
||||
|
||||
def auto(key: str) -> dict:
|
||||
"""견본 입력 — 마스터 보기용 자동값(`master_auto`) · 프로젝트 기본값 아님 · 저장 안 함."""
|
||||
whole = loaded()[1]
|
||||
try:
|
||||
return {"값": mau.values(whole, whole.logic(key))}
|
||||
except cm.mf.FormulaError as e:
|
||||
raise StoreError(404, str(e)) from e
|
||||
|
||||
|
||||
def text(key: str, inputs: dict, row: dict | None = None, file: str | None = None):
|
||||
"""사람이 읽는 식 줄 — 비목 묶음마다 「이름 : 단가 * 수량식 = 금액」 과 소계(`master_text`)."""
|
||||
given = {k: _dec(v) for k, v in inputs.items()}
|
||||
@@ -567,6 +576,7 @@ def text(key: str, inputs: dict, row: dict | None = None, file: str | None = Non
|
||||
if row is not None:
|
||||
key = _swap(files, str(row.get("키") or key), row, file)
|
||||
try:
|
||||
given = cm.mf.fill_empty(cm.mf.Master(files).logic(key), given)
|
||||
return mtx.lines(files, key, given)
|
||||
except (cm.mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError) as e:
|
||||
return {"ok": False, "reason": str(e) or type(e).__name__}
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { createInputField, createSelectField, el } from "@ui/ui_template_elements";
|
||||
import { fetchLogic, type LogicInput } from "./M01_MasterData_UI_Logic_Api";
|
||||
import { fetchAuto, fetchLogic, type LogicInput } from "./M01_MasterData_UI_Logic_Api";
|
||||
import { previewCombo, type Combo } from "./M01_MasterData_UI_Combo_Api";
|
||||
import { tc, type ComboTextKey } from "./M01_MasterData_UI_Combo_Text";
|
||||
import { formatMoney } from "./M01_MasterData_UI_Logic_Money";
|
||||
|
||||
const COSTS = ["노무비", "재료비", "경비"] as const;
|
||||
const LABEL: Record<(typeof COSTS)[number], ComboTextKey> = {
|
||||
@@ -17,8 +18,8 @@ const LABEL: Record<(typeof COSTS)[number], ComboTextKey> = {
|
||||
};
|
||||
|
||||
const num = (v: string): number | string => (v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v);
|
||||
/** 원 단위 소수점 0 — 조합 미리 보기는 모두 돈 자리(노무비·재료비·경비·계) */
|
||||
const fmt = (n: number): string => n.toLocaleString("ko-KR", { maximumFractionDigits: 0 });
|
||||
/** 조합 미리 보기는 모두 돈 자리(노무비·재료비·경비·계) */
|
||||
const fmt = formatMoney;
|
||||
|
||||
/** 로직 입력 정의 → 처음 값(고르기 첫째 · 범위 아래끝) */
|
||||
const initial = (i: LogicInput): string => String(i.고르기?.[0] ?? i.범위?.[0] ?? "");
|
||||
@@ -28,6 +29,7 @@ function inputCell(
|
||||
input: LogicInput,
|
||||
key: string,
|
||||
values: Record<string, Record<string, string>>,
|
||||
onEdit: () => void,
|
||||
): HTMLElement {
|
||||
const mine = (values[key] ??= {});
|
||||
const value = mine[input.이름] ?? initial(input);
|
||||
@@ -39,8 +41,14 @@ function inputCell(
|
||||
const control = "select" in field ? field.select : field.input;
|
||||
control.value = value;
|
||||
control.setAttribute("data-input", `${key}|${input.이름}`);
|
||||
control.addEventListener("input", () => (mine[input.이름] = control.value));
|
||||
control.addEventListener("change", () => (mine[input.이름] = control.value));
|
||||
const edit = (): void => {
|
||||
mine[input.이름] = control.value;
|
||||
};
|
||||
control.addEventListener("input", edit);
|
||||
control.addEventListener("change", () => {
|
||||
edit();
|
||||
onEdit(); // 바꾸면 다시 미리 보기 계산
|
||||
});
|
||||
return field.root;
|
||||
}
|
||||
|
||||
@@ -54,6 +62,8 @@ export function buildPreview(combo: () => Combo): PreviewHandle {
|
||||
const values: Record<string, Record<string, string>> = {};
|
||||
/** 서버가 받을 꼴 — 원래 숫자·고르기 값으로 되돌림 */
|
||||
const inputs = new Map<string, LogicInput[]>();
|
||||
/** 로직마다 자동값은 한 번만 받음 */
|
||||
const asked = new Set<string>();
|
||||
const form = el("div", { className: "m01-logic__stack" });
|
||||
const out = el("div", {
|
||||
className: "m01-master__grid-wrap",
|
||||
@@ -130,7 +140,17 @@ export function buildPreview(combo: () => Combo): PreviewHandle {
|
||||
inputs.clear();
|
||||
for (const row of combo().담은로직) {
|
||||
try {
|
||||
const one = await fetchLogic(row.로직);
|
||||
const [one, auto] = await Promise.all([
|
||||
fetchLogic(row.로직),
|
||||
asked.has(row.로직) ? Promise.resolve({}) : fetchAuto(row.로직),
|
||||
]);
|
||||
asked.add(row.로직);
|
||||
const mine = (values[row.로직] ??= {});
|
||||
for (const i of one.logic.입력) {
|
||||
const got = (auto as Record<string, unknown>)[i.이름];
|
||||
if (got === undefined || got === null || (mine[i.이름] ?? "").trim() !== "") continue;
|
||||
mine[i.이름] = String(got);
|
||||
}
|
||||
inputs.set(row.로직, one.logic.입력);
|
||||
blocks.push(
|
||||
el("div", {
|
||||
@@ -140,7 +160,7 @@ export function buildPreview(combo: () => Combo): PreviewHandle {
|
||||
className: "m01lab__cell--wide",
|
||||
text: `${row.로직} ${one.logic.이름}`,
|
||||
}),
|
||||
...one.logic.입력.map((i) => inputCell(i, row.로직, values)),
|
||||
...one.logic.입력.map((i) => inputCell(i, row.로직, values, () => void run())),
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -149,7 +169,9 @@ export function buildPreview(combo: () => Combo): PreviewHandle {
|
||||
}
|
||||
}
|
||||
form.replaceChildren(...blocks);
|
||||
out.replaceChildren(el("p", { className: "m01-logic__muted", text: tc("Prev_Wait") }));
|
||||
if (blocks.length)
|
||||
await run(); // 열자마자 한 번 미리 보기 계산
|
||||
else out.replaceChildren(el("p", { className: "m01-logic__muted", text: tc("Prev_Wait") }));
|
||||
};
|
||||
|
||||
const root = el("section", {
|
||||
|
||||
@@ -34,6 +34,7 @@ import { buildList, logicId, type ListItem, type ListMark } from "./M01_MasterDa
|
||||
import type { SideHandle } from "./M01_MasterData_UI_Side";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { buildLabDetail, isOwnLogic } from "./M01_MasterData_UI_LogicLab_Detail";
|
||||
import { setLogicOpener } from "./M01_MasterData_UI_LogicLab_Modal";
|
||||
import { openCopyScreen } from "./M01_MasterData_UI_LogicLab_Copy";
|
||||
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
|
||||
import { buildNewLogic } from "./M01_MasterData_UI_LogicLab_New";
|
||||
@@ -58,6 +59,8 @@ interface Opened extends Draft {
|
||||
reasons: string[];
|
||||
lines: CalcLine[] | null;
|
||||
text: TextAnswer | null;
|
||||
/** 마지막으로 잘 받은 읽는 식 — 멈춘 뒤에도 수량 칸 조각을 이걸로 */
|
||||
lastText?: TextAnswer;
|
||||
ownHost: HTMLElement;
|
||||
values: Record<string, string>;
|
||||
}
|
||||
@@ -165,10 +168,12 @@ export async function mountM01LogicLab(
|
||||
savedKey: current.origKey,
|
||||
file: current.file,
|
||||
row: current.row as LogicRow,
|
||||
prices: current.prices,
|
||||
dirty: () => current.id in drafts,
|
||||
values: current.values,
|
||||
onText: (text) => {
|
||||
current.text = text;
|
||||
if (text.ok) current.lastText = text;
|
||||
drawEditor(); // [계산] 전 미리보기 응답도 왼쪽 이름 식 칸에 바로 비치게 — onLines 와 같은 자리
|
||||
},
|
||||
onLines: (lines) => {
|
||||
@@ -201,6 +206,7 @@ export async function mountM01LogicLab(
|
||||
prices: current.prices,
|
||||
lines: current.lines,
|
||||
text: current.text,
|
||||
lastText: current.lastText,
|
||||
values: current.values,
|
||||
ownHost: current.ownHost,
|
||||
onChange: touch,
|
||||
@@ -441,5 +447,6 @@ export async function mountM01LogicLab(
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
setLogicOpener((key) => void openKeyOn(key));
|
||||
return { openKey: openKeyOn };
|
||||
}
|
||||
|
||||
@@ -14,57 +14,45 @@ import type {
|
||||
LogicRow,
|
||||
NamedFormula,
|
||||
TextAnswer,
|
||||
TextLine,
|
||||
TextPiece,
|
||||
} from "./M01_MasterData_UI_Logic_Api";
|
||||
import { buildEditor, formatNumber, qtyKind } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { buildEditor, qtyKind } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { formatMoney, formatPrice } from "./M01_MasterData_UI_Logic_Money";
|
||||
import { openPicker } from "./M01_MasterData_UI_Logic_Pick";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { buildFormula } from "./M01_MasterData_UI_LogicLab_Formula";
|
||||
import { openMaterialModal, openTableModal } from "./M01_MasterData_UI_LogicLab_Modal";
|
||||
import {
|
||||
condText,
|
||||
openMaterialModal,
|
||||
openNoteModal,
|
||||
openRefModal,
|
||||
openLogic,
|
||||
openTableModal,
|
||||
type PickCond,
|
||||
} from "./M01_MasterData_UI_LogicLab_Modal";
|
||||
import { fragmentRow, type Fragment } from "./M01_MasterData_UI_LogicLab_Pill";
|
||||
import { tl, type LabTextKey } from "./M01_MasterData_UI_LogicLab_Text";
|
||||
import "./M01_MasterData_UI_LogicLab_Style.css";
|
||||
import "./M01_MasterData_UI_LogicLab_Pill.css";
|
||||
|
||||
/** 원 단위 소수점 0 — 돈 자리(단가·금액·소계·계)만. 수량·비율은 `formatNumber` 그대로 둠. */
|
||||
const formatMoney = (value: unknown): string =>
|
||||
typeof value === "number"
|
||||
? value.toLocaleString("ko-KR", { maximumFractionDigits: 0 })
|
||||
: formatNumber(value);
|
||||
|
||||
/** 조각 계약(`ref/_분석_알약_상세줄.md` ㉮ 표) — 수량 식 안 찾기(...) 표 호출만 알약,
|
||||
* 그 밖(숫자·연산기호·중간값·설계입력 이름)은 글자. 서버가 조각 목록을 아직 안 주므로
|
||||
* 여기서 임시로 쪼갬(`Logic_Note.ts` 찾기() 정규식과 같은 규칙) — 서버가 붙으면 이 파싱을 걷어냄. */
|
||||
const FIND_RE = /찾기\(\s*([A-Za-z0-9_]+)\s*,([^)]*)\)\s*\.\s*([A-Za-z0-9_가-힣]+)/g;
|
||||
|
||||
function splitFindConds(raw: string): [string, string][] {
|
||||
return raw
|
||||
.split(",")
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean)
|
||||
.map((p): [string, string] => {
|
||||
const i = p.indexOf("=");
|
||||
return i < 0 ? [p, p] : [p.slice(0, i).trim(), p.slice(i + 1).trim()];
|
||||
});
|
||||
}
|
||||
|
||||
function qtyFragments(expr: string, values: Record<string, string>): Fragment[] {
|
||||
const out: Fragment[] = [];
|
||||
let last = 0;
|
||||
for (const m of expr.matchAll(FIND_RE)) {
|
||||
const at = m.index ?? 0;
|
||||
if (at > last) out.push({ text: expr.slice(last, at) });
|
||||
const table = m[1];
|
||||
const label = `${tl("Pill_Table")} ${table}`;
|
||||
out.push({
|
||||
pill: {
|
||||
label,
|
||||
onClick: () => openTableModal({ title: label, table, conds: splitFindConds(m[2]), values }),
|
||||
},
|
||||
});
|
||||
last = at + m[0].length;
|
||||
}
|
||||
if (last < expr.length) out.push({ text: expr.slice(last) });
|
||||
return out.length ? out : [{ text: expr }];
|
||||
/** 서버 조각(`/text` 줄의 `조각`) → 수량 칸 조각 — 요소·표·로직은 알약(글 = 조각 글) · 나머지는 글자 */
|
||||
function toFragments(pieces: TextPiece[], ctx: DetailContext): Fragment[] {
|
||||
return pieces.map((piece): Fragment => {
|
||||
const dim = piece.깊이 ? { dim: true } : {};
|
||||
if (piece.kind !== "요소" && piece.kind !== "표" && piece.kind !== "로직")
|
||||
return { text: piece.글, ...dim };
|
||||
const ref = piece.참조;
|
||||
const open = (): void => {
|
||||
if (!ref) return openNoteModal(piece.글, tl("Pill_NoPick"));
|
||||
if (piece.kind === "표")
|
||||
openTableModal({ title: piece.글, table: ref.키, row: ref.줄 ?? ref.행, col: ref.열 });
|
||||
else if (piece.kind === "요소")
|
||||
openRefModal({ title: piece.글, ref: ref.키, prices: ctx.prices });
|
||||
else openLogic(ref.키);
|
||||
};
|
||||
return { pill: { label: piece.글, onClick: open }, ...dim };
|
||||
});
|
||||
}
|
||||
|
||||
export interface DetailContext {
|
||||
@@ -76,6 +64,8 @@ export interface DetailContext {
|
||||
lines: CalcLine[] | null;
|
||||
/** 마지막 시험 계산의 읽는 식 줄(서버가 줌) */
|
||||
text: TextAnswer | null;
|
||||
/** 마지막으로 잘 받은 읽는 식 — `text` 가 멈춤이면 조각을 이걸로 */
|
||||
lastText?: TextAnswer;
|
||||
values: Record<string, string>;
|
||||
/** 자체 로직 편집기 자리 — 다시 그려도 같은 칸을 씀(고치던 값이 안 날아감) */
|
||||
ownHost: HTMLElement;
|
||||
@@ -94,6 +84,8 @@ const groupOfHo = (item: HoLine): Group =>
|
||||
OF_KIND[item.종류] ?? (item.종류 === "로직" ? OF_COST[item.비목 ?? ""] : undefined) ?? "other";
|
||||
|
||||
interface Entry {
|
||||
/** 호표 줄 차례(덧줄은 호표 수 + 덧줄 차례) — 서버 조각을 찾는 열쇠 */
|
||||
idx: number;
|
||||
group: Group;
|
||||
name: string;
|
||||
spec: string;
|
||||
@@ -101,13 +93,29 @@ interface Entry {
|
||||
qty: string;
|
||||
qtyTag: string;
|
||||
qtyFrag: Fragment[];
|
||||
/** 한 줄이 비목 몫으로 갈렸을 때 이 묶음의 몫 이름(예 「노무비」) */
|
||||
share?: string;
|
||||
price: string;
|
||||
amount: number | null;
|
||||
/** 덧줄이면 true */
|
||||
extra: boolean;
|
||||
open: () => void;
|
||||
open: (self: Entry) => void;
|
||||
/** 서버 조각으로 그린 단가 자리 · 수량 풀이 — 줄 모달이 씀(붙기 전엔 없음) */
|
||||
unitFrag?: Fragment[];
|
||||
piecesFrag?: Fragment[];
|
||||
/** 서버 요소조각의 첫 요소 — 자리 표시 든 줄이 입력으로 풀린 키 · 글 */
|
||||
unitRef?: string;
|
||||
unitText?: string;
|
||||
/** 자재 단위가 줄 단위와 성격이 다름 — 「줄 m · 자재 Ton」 */
|
||||
warn?: string;
|
||||
}
|
||||
|
||||
/** 재료 고르기 줄 — `요소` 가 글 대신 {구분·상세구분·규격·대표} 객체 */
|
||||
const condOf = (item: HoLine): PickCond | null =>
|
||||
typeof item.요소 === "object" && item.요소 !== null ? (item.요소 as unknown as PickCond) : null;
|
||||
const condLabel = (cond: PickCond): string =>
|
||||
[cond.구분, cond.상세구분, cond.규격].filter(Boolean).join(" · ");
|
||||
|
||||
const groupName = (group: Group): string => tl(`G_${group}` as LabTextKey);
|
||||
|
||||
const section = (title: string, body: HTMLElement[], hint = ""): HTMLElement =>
|
||||
@@ -162,7 +170,7 @@ function infoBox(ctx: DetailContext): HTMLElement {
|
||||
className: "m01-logic__reasons",
|
||||
children: [
|
||||
el("strong", { text: tx("Head_Blocked") }),
|
||||
...ctx.reasons.map((r) => el("div", { text: r })),
|
||||
...ctx.reasons.map((r) => el("div", { text: safeReason(r) })),
|
||||
],
|
||||
}),
|
||||
]
|
||||
@@ -196,7 +204,20 @@ function infoBox(ctx: DetailContext): HTMLElement {
|
||||
const spread = (e: Entry, line?: CalcLine): Entry[] => {
|
||||
const parts = Object.entries(line?.비목 ?? {}).filter(([, v]) => v !== 0);
|
||||
if (!parts.length) return [e];
|
||||
return parts.map(([k, v]) => ({ ...e, group: OF_COST[k] ?? "other", amount: v }));
|
||||
return parts.map(([k, v]) => ({
|
||||
...e,
|
||||
group: OF_COST[k] ?? "other",
|
||||
amount: v,
|
||||
...(parts.length > 1
|
||||
? {
|
||||
share: k,
|
||||
// 몫 줄 단가 = 단가 × (몫 금액 ÷ 줄 금액) — 곱하기 수량이 몫 금액과 맞게
|
||||
...(line && line.금액 && line.단가 != null
|
||||
? { price: formatPrice((line.단가 * v) / line.금액) }
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
}));
|
||||
};
|
||||
|
||||
function entries(ctx: DetailContext): Entry[] {
|
||||
@@ -204,24 +225,31 @@ function entries(ctx: DetailContext): Entry[] {
|
||||
const ho = ctx.row.호표 ?? [];
|
||||
const out: Entry[] = ho.flatMap((item, i) => {
|
||||
const line = ctx.lines?.[i];
|
||||
const brief = ctx.prices[item.요소];
|
||||
const cond = condOf(item);
|
||||
const brief = cond ? ctx.prices[condText(cond)] : ctx.prices[item.요소];
|
||||
const price = line ? line.단가 : brief?.값;
|
||||
const entry: Entry = {
|
||||
idx: i,
|
||||
group: groupOfHo(item),
|
||||
name: item.이름 ?? item.요소,
|
||||
name: item.이름 ?? (cond ? condLabel(cond) : item.요소),
|
||||
spec: item.규격 ?? brief?.규격 ?? "",
|
||||
unit: item.단위 ?? "",
|
||||
qty: item.수량,
|
||||
qtyTag: qtyKind(item.수량) === tx("Ho_QtyTable") ? tl("Tag_Table") : qtyKind(item.수량),
|
||||
qtyFrag: qtyFragments(item.수량, ctx.values),
|
||||
price: price === undefined || price === null ? "" : formatMoney(price),
|
||||
qtyFrag: [{ text: item.수량 }],
|
||||
price: price === undefined || price === null ? "" : formatPrice(price),
|
||||
amount: line ? line.금액 : null,
|
||||
warn: line?.단위경고,
|
||||
extra: false,
|
||||
open: () =>
|
||||
open: (self) =>
|
||||
openMaterialModal({
|
||||
title: item.이름 ?? item.요소,
|
||||
unitFrag: self.unitFrag,
|
||||
unitRef: self.unitRef,
|
||||
unitText: self.unitText,
|
||||
qtyFrag: self.piecesFrag,
|
||||
title: item.이름 ?? (cond ? condLabel(cond) : item.요소),
|
||||
expr: item.수량,
|
||||
element: item.요소,
|
||||
...(cond ? { cond, unit: item.단위 } : { element: item.요소 }),
|
||||
prices: ctx.prices,
|
||||
middles,
|
||||
values: ctx.values,
|
||||
@@ -232,18 +260,21 @@ function entries(ctx: DetailContext): Entry[] {
|
||||
(ctx.row.덧줄 ?? []).forEach((extra, i) => {
|
||||
const line = ctx.lines?.[ho.length + i];
|
||||
const entry: Entry = {
|
||||
idx: ho.length + i,
|
||||
group: OF_COST[extra.비목 ?? ""] ?? "other",
|
||||
name: extra.이름,
|
||||
spec: "",
|
||||
unit: "",
|
||||
qty: extra.식,
|
||||
qtyTag: tl("Extra"),
|
||||
qtyFrag: qtyFragments(extra.식, ctx.values),
|
||||
qtyFrag: [{ text: extra.식 }],
|
||||
price: "",
|
||||
amount: line ? line.금액 : null,
|
||||
extra: true,
|
||||
open: () =>
|
||||
open: (self) =>
|
||||
openMaterialModal({
|
||||
unitFrag: self.unitFrag,
|
||||
qtyFrag: self.piecesFrag,
|
||||
title: extra.이름,
|
||||
expr: extra.식,
|
||||
prices: ctx.prices,
|
||||
@@ -253,9 +284,49 @@ function entries(ctx: DetailContext): Entry[] {
|
||||
};
|
||||
out.push(...spread(entry, line));
|
||||
});
|
||||
attachPieces(out, ctx);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 서버 `/text` 줄의 조각을 호표 줄 차례(덧줄은 덧줄 차례)로 이음 — 서버는 묶음마다 호표 차례대로 줄을 주므로
|
||||
* 이름을 차례로 따라가며 줄 차례에 맞춤 · 금액 0 로직 부름 줄처럼 묶음이 달라도 찾음 ·
|
||||
* 끝내 못 찾으면 날식 대신 빈칸(계산 전 · 서버 실패면 식 글자 그대로) */
|
||||
function attachPieces(all: Entry[], ctx: DetailContext): void {
|
||||
const source = ctx.text?.ok ? ctx.text : ctx.lastText;
|
||||
if (!source?.ok) {
|
||||
if (ctx.text) for (const e of all) e.qtyFrag = e.piecesFrag = []; // 멈춤 · 받은 조각 없음 — 날식 대신 빈칸
|
||||
return;
|
||||
}
|
||||
const ho = ctx.row.호표 ?? [];
|
||||
const names = [
|
||||
...ho.map((item) => item.이름 ?? (condOf(item) ? condLabel(condOf(item)!) : item.요소)),
|
||||
...(ctx.row.덧줄 ?? []).map((extra) => extra.이름),
|
||||
];
|
||||
const found = new Map<string, TextLine>();
|
||||
for (const g of source.groups) {
|
||||
let from = 0;
|
||||
for (const line of g.줄) {
|
||||
const at = names.findIndex((n, i) => i >= from && n === line.이름);
|
||||
if (at < 0) continue;
|
||||
found.set(`${g.비목}|${at}`, line);
|
||||
from = at + 1;
|
||||
}
|
||||
}
|
||||
const costs = Object.keys(OF_COST);
|
||||
for (const e of all) {
|
||||
const own = costs.find((c) => OF_COST[c] === e.group);
|
||||
const line = [own, ...costs].map((c) => found.get(`${c}|${e.idx}`)).find(Boolean);
|
||||
const pieces = line?.조각;
|
||||
e.qtyFrag = e.piecesFrag = pieces?.length ? toFragments(pieces, ctx) : [];
|
||||
if (line?.요소조각?.length) {
|
||||
e.unitFrag = toFragments(line.요소조각, ctx);
|
||||
const first = line.요소조각.find((piece) => piece.kind === "요소");
|
||||
e.unitRef = first?.참조?.키;
|
||||
e.unitText = first?.글;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cellOf = (text: string, className = ""): HTMLElement => el("td", { className, text });
|
||||
|
||||
function lineRow(e: Entry): HTMLElement {
|
||||
@@ -266,7 +337,18 @@ function lineRow(e: Entry): HTMLElement {
|
||||
el("td", {
|
||||
children: [
|
||||
el("div", { text: e.name }),
|
||||
...(e.share
|
||||
? [el("div", { className: "m01-logic__tag", text: `${e.share} ${tl("Share")}` })]
|
||||
: []),
|
||||
...(e.spec ? [el("div", { className: "m01-logic__muted", text: e.spec })] : []),
|
||||
...(e.warn
|
||||
? [
|
||||
el("div", {
|
||||
className: "m01-logic__tag m01lab__warn",
|
||||
text: `${tx("Calc_UnitWarn")} · ${e.warn}`,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
cellOf(e.unit),
|
||||
@@ -281,8 +363,8 @@ function lineRow(e: Entry): HTMLElement {
|
||||
cellOf(e.amount === null ? "" : formatMoney(e.amount), "m01-logic__money"),
|
||||
],
|
||||
});
|
||||
tr.addEventListener("click", e.open);
|
||||
tr.addEventListener("keydown", (ev) => ev.key === "Enter" && e.open());
|
||||
tr.addEventListener("click", () => e.open(e));
|
||||
tr.addEventListener("keydown", (ev) => ev.key === "Enter" && e.open(e));
|
||||
return tr;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import type { TextAnswer } from "./M01_MasterData_UI_Logic_Api";
|
||||
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { formatMoney } from "./M01_MasterData_UI_Logic_Money";
|
||||
import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
|
||||
import "./M01_MasterData_UI_LogicLab_Formula.css";
|
||||
|
||||
@@ -31,7 +31,10 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void {
|
||||
host.replaceChildren(
|
||||
el("div", {
|
||||
className: "m01-logic__reasons",
|
||||
children: [el("strong", { text: tx("Calc_Stopped") }), el("div", { text: text.reason })],
|
||||
children: [
|
||||
el("strong", { text: tx("Calc_Stopped") }),
|
||||
el("div", { text: safeReason(text.reason) }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
return;
|
||||
@@ -62,7 +65,7 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void {
|
||||
? [
|
||||
el("div", { text: `${line.이름} : ${line.글}` }),
|
||||
...(line.까닭
|
||||
? [el("div", { className: "m01-logic__muted", text: line.까닭 })]
|
||||
? [el("div", { className: "m01-logic__muted", text: safeReason(line.까닭) })]
|
||||
: []),
|
||||
]
|
||||
: [el("div", { className: "m01-logic__muted", text: tl("Formula_Slot") })],
|
||||
@@ -82,7 +85,7 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void {
|
||||
el("p", {
|
||||
className: "m01lab__subtotal-text m01lab-fx__wide",
|
||||
attrs: { "data-text-sub": String(g.소계) },
|
||||
text: `${g.비목} ${tl("Subtotal")} ${formatNumber(g.소계)}${g.끝수 ? ` (${g.끝수})` : ""}`,
|
||||
text: `${g.비목} ${tl("Subtotal")} ${formatMoney(g.소계)}${g.끝수 ? ` (${g.끝수})` : ""}`,
|
||||
}),
|
||||
]);
|
||||
blocks.unshift(heads);
|
||||
@@ -90,7 +93,7 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void {
|
||||
el("p", {
|
||||
className: "m01lab__total m01lab-fx__wide",
|
||||
attrs: { "data-text-sum": String(text.계) },
|
||||
text: `${tl("Total")} ${formatNumber(text.계)}`,
|
||||
text: `${tl("Total")} ${formatMoney(text.계)}`,
|
||||
}),
|
||||
);
|
||||
host.replaceChildren(el("div", { className: "m01lab-fx", children: blocks }));
|
||||
|
||||
@@ -9,17 +9,54 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import type { ElementBrief, NamedFormula } from "./M01_MasterData_UI_Logic_Api";
|
||||
import {
|
||||
searchElements,
|
||||
type ElementBrief,
|
||||
type NamedFormula,
|
||||
} from "./M01_MasterData_UI_Logic_Api";
|
||||
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { explain, loadTable, matchRow, type TableRow } from "./M01_MasterData_UI_Logic_Note";
|
||||
import { formatMoney, formatPrice } from "./M01_MasterData_UI_Logic_Money";
|
||||
import { explain, loadTable, type TableRow } from "./M01_MasterData_UI_Logic_Note";
|
||||
import { jumpToMaster, type MasterJump } from "./M01_MasterData_UI_Side";
|
||||
import { fragmentRow, type Fragment } from "./M01_MasterData_UI_LogicLab_Pill";
|
||||
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
|
||||
|
||||
/** 원 단위 소수점 0 — 돈 자리만(수량·비율은 `formatNumber` 그대로) */
|
||||
const formatMoney = (value: unknown): string =>
|
||||
typeof value === "number"
|
||||
? value.toLocaleString("ko-KR", { maximumFractionDigits: 0 })
|
||||
: formatNumber(value);
|
||||
/** 로직 알약 → 그 로직 열기 — 로직 화면이 자기 열기를 등록 · 열려 있는 모달은 다 닫음 */
|
||||
let logicOpener: (key: string) => void = () => {};
|
||||
export const setLogicOpener = (fn: (key: string) => void): void => {
|
||||
logicOpener = fn;
|
||||
};
|
||||
export function openLogic(key: string): void {
|
||||
document.querySelectorAll(".m01-logic__backdrop").forEach((b) => b.remove());
|
||||
logicOpener(key);
|
||||
}
|
||||
|
||||
/** 재료 고르기 조건(호표 줄 `요소` 가 글 대신 객체일 때) — 서버 `master_material.PICK_KEYS` 와 같은 칸 */
|
||||
export interface PickCond {
|
||||
구분?: string;
|
||||
상세구분?: string;
|
||||
이름?: string;
|
||||
규격?: string;
|
||||
대표?: string;
|
||||
[extra: string]: unknown;
|
||||
}
|
||||
|
||||
/** 서버가 `prices` 열쇠로 쓰는 조건 글 — 「구분|상세구분|이름|규격|대표|지역|계약종별」 */
|
||||
export const condText = (cond: PickCond): string =>
|
||||
["구분", "상세구분", "이름", "규격", "대표", "지역", "계약종별"]
|
||||
.map((k) => String(cond[k] ?? ""))
|
||||
.join("|") + (cond["검색어"] ? `|${cond["검색어"]}` : "");
|
||||
|
||||
/** 재료 모달 검색 칸 글 — 검색어 첫 낱말의 괄호 앞(「합판(내수) 12t」 → 「합판」) */
|
||||
export const headWord = (text: unknown): string =>
|
||||
String(text ?? "")
|
||||
.trim()
|
||||
.split(/\s+/)[0]
|
||||
.replace(/[((].*$/, "");
|
||||
|
||||
/** 고르기 줄 후보 찾기 글 — 검색어 → 이름 → 상세구분 → 구분 차례 첫 낱말 */
|
||||
export const seedOf = (cond: PickCond): string =>
|
||||
[cond["검색어"], cond.이름, cond.상세구분, cond.구분].map(headWord).find(Boolean) ?? "";
|
||||
|
||||
export interface ModalTarget {
|
||||
title: string;
|
||||
@@ -27,40 +64,68 @@ export interface ModalTarget {
|
||||
expr: string;
|
||||
/** 호표 줄의 단가 요소 키 — 덧줄은 없음 */
|
||||
element?: string;
|
||||
/** 재료 고르기 줄 — `element` 대신 조건 객체 · 잡힌 품목 단가 + 후보 목록을 보임 */
|
||||
cond?: PickCond;
|
||||
/** 호표 줄 단위 — 후보를 같은 단위 줄로만 거름 */
|
||||
unit?: string;
|
||||
prices: Record<string, ElementBrief | null>;
|
||||
middles: NamedFormula[];
|
||||
/** 시험 계산 칸에 넣은 값 — 걸린 줄을 맞히는 데 씀 */
|
||||
values: Record<string, string>;
|
||||
/** 서버 `요소조각` — 단가 자리(요소 · 로직 부르기)를 조각으로 · 없으면 로직 키만 알약으로 */
|
||||
unitFrag?: Fragment[];
|
||||
/** 서버가 입력으로 푼 단가 요소 키 · 글 — 자리 표시(`{…}`) 든 줄이 씀 */
|
||||
unitRef?: string;
|
||||
unitText?: string;
|
||||
/** 서버 수량 조각 — 한 줄 풀이를 조각으로(알약 포함) · 없으면 식 풀이 글 */
|
||||
qtyFrag?: Fragment[];
|
||||
}
|
||||
|
||||
/** 수량 칸 표 알약 하나가 여는 자료 — 조각 계약의 「표 찾기」 자리 */
|
||||
/** 수량 칸 표 알약 하나가 여는 자료 — 서버 조각 `참조`(키 · 행 · 열)가 곧 찾은 자리 */
|
||||
export interface TableTarget {
|
||||
title: string;
|
||||
table: string;
|
||||
conds: [string, string][];
|
||||
values: Record<string, string>;
|
||||
/** 찾은 줄 — 찾기 조건(예 {구분: 합판}) · 줄의 키·원문번호 글 · 줄 차례(0부터) */
|
||||
row?: Record<string, unknown> | string | number | null;
|
||||
/** 찾은 값 칸 이름 */
|
||||
col?: string | null;
|
||||
}
|
||||
|
||||
function tableView(
|
||||
table: TableRow,
|
||||
conds: [string, string][],
|
||||
values: Record<string, string>,
|
||||
): HTMLElement {
|
||||
// 표 줄의 빈 칸은 「무엇이든」 — 서버 찾기()와 같게 그 칸은 조건에서 뺌(줄마다 따로 봄)
|
||||
const sameCond = (have: unknown, want: unknown, range: boolean): boolean => {
|
||||
if (range && Array.isArray(have)) {
|
||||
const n = Number(want);
|
||||
const [low, high] = have as (number | null)[];
|
||||
return (low == null || n >= low) && (high == null || n <= high);
|
||||
}
|
||||
return String(have) === String(want);
|
||||
};
|
||||
|
||||
function tableView(table: TableRow, target: TableTarget): HTMLElement {
|
||||
const lines = table.줄 ?? [];
|
||||
const at = target.row;
|
||||
const kinds = table.조건 ?? {};
|
||||
const hit =
|
||||
(table.줄 ?? []).find((line) =>
|
||||
matchRow(
|
||||
{ ...table, 줄: [line] },
|
||||
conds.filter(([col]) => line[col] !== undefined && line[col] !== null && line[col] !== ""),
|
||||
{ values, middle: {} },
|
||||
),
|
||||
) ?? null;
|
||||
at === undefined || at === null
|
||||
? null
|
||||
: typeof at === "number"
|
||||
? (lines[at] ?? null)
|
||||
: typeof at === "string"
|
||||
? (lines.find((line) => line["키"] === at || line["원문번호"] === at) ?? null)
|
||||
: (lines.find((line) =>
|
||||
Object.entries(at).every(([c, want]) => sameCond(line[c], want, kinds[c] === "범위")),
|
||||
) ?? null);
|
||||
const cols = [...Object.keys(table.조건 ?? {}), ...Object.keys(table.값칸 ?? {})];
|
||||
const rows = (table.줄 ?? []).map((line) =>
|
||||
const rows = lines.map((line) =>
|
||||
el("tr", {
|
||||
className: hit === line ? "m01lab__hit" : "",
|
||||
attrs: hit === line ? { "data-hit": "1" } : {},
|
||||
children: cols.map((c) => el("td", { text: formatNumber(line[c]) })),
|
||||
children: cols.map((c) =>
|
||||
el("td", {
|
||||
// 찾은 줄을 알면 그 줄의 칸만 · 줄을 못 받았으면(표 줄에 키가 없는 표) 값 칸 세로줄만 색
|
||||
className: c === target.col && (hit === line || !hit) ? "m01lab__hit-cell" : "",
|
||||
text: formatNumber(line[c]),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
const use = table.용도
|
||||
@@ -77,7 +142,7 @@ function tableView(
|
||||
}),
|
||||
el("p", {
|
||||
className: "m01-logic__muted",
|
||||
text: hit ? tl("Modal_Hit") : tl("Modal_NoHit"),
|
||||
text: hit ? tl("Modal_Hit") : target.col ? tl("Modal_ColOnly") : tl("Modal_NoHit"),
|
||||
}),
|
||||
goButton({ kind: "table", key: table.키 }),
|
||||
el("div", {
|
||||
@@ -111,6 +176,7 @@ function goButton(target: MasterJump): HTMLElement {
|
||||
|
||||
function priceView(ref: string, brief: ElementBrief): HTMLElement {
|
||||
const cols = Object.entries(brief.값칸 ?? {});
|
||||
const slots = Object.entries(brief.값들 ?? {}).filter(([, v]) => v !== null && v !== undefined);
|
||||
return el("div", {
|
||||
className: "m01lab__data",
|
||||
attrs: { "data-price": ref },
|
||||
@@ -123,11 +189,15 @@ function priceView(ref: string, brief: ElementBrief): HTMLElement {
|
||||
? [el("dt", { text: tl("Modal_Spec") }), el("dd", { text: brief.규격 })]
|
||||
: []),
|
||||
el("dt", { text: tl("Modal_Value") }),
|
||||
el("dd", { className: "m01-logic__money", text: formatMoney(brief.값) }),
|
||||
el("dd", { className: "m01-logic__money", text: formatPrice(brief.값) }),
|
||||
...(brief.file
|
||||
? [el("dt", { text: tl("Modal_Source") }), el("dd", { text: brief.file })]
|
||||
: []),
|
||||
...cols.flatMap(([k, v]) => [el("dt", { text: k }), el("dd", { text: String(v) })]),
|
||||
...slots.flatMap(([k, v]) => [
|
||||
el("dt", { text: k }),
|
||||
el("dd", { className: "m01-logic__money", text: formatMoney(v) }),
|
||||
]),
|
||||
],
|
||||
}),
|
||||
goButton({ kind: "element", group: groupOf(ref), ref: brief.ref ?? ref, file: brief.file }),
|
||||
@@ -162,46 +232,207 @@ function openDialog(title: string, mount: (body: HTMLElement) => void): void {
|
||||
dialog.focus();
|
||||
}
|
||||
|
||||
/** 줄을 눌렀을 때 — 그 줄의 단가(재료·인력·기계 또는 다른 로직)를 바로 보임(알약 없이) */
|
||||
export function openMaterialModal(target: ModalTarget): void {
|
||||
openDialog(target.title, (body) => {
|
||||
const base = target.element?.split(".")[0] ?? "";
|
||||
const price = target.element
|
||||
? (target.prices[target.element] ?? target.prices[base])
|
||||
: undefined;
|
||||
const logicRef = target.element?.startsWith("로직(") ? target.element : "";
|
||||
const rateParts: HTMLElement[] = [];
|
||||
if (price && target.element) {
|
||||
rateParts.push(priceView(target.element, price));
|
||||
} else if (logicRef) {
|
||||
rateParts.push(el("p", { text: `${tl("Modal_Logic")} · ${logicRef}` }));
|
||||
} else if (target.element) {
|
||||
rateParts.push(
|
||||
el("p", {
|
||||
className: "m01-logic__muted",
|
||||
text: `${target.element} — ${tl(target.element.includes("{") ? "Pill_NoPick" : "Pill_NoValue")}`,
|
||||
const muted = (text: string): HTMLElement => el("p", { className: "m01-logic__muted", text });
|
||||
|
||||
/** 재료 고르기 줄 후보 목록 — 잡힌 품목(`pickedRef`)을 노란 줄로 · 후보가 100건을 넘으면 앞 100건만 옴 */
|
||||
function candidateView(
|
||||
cond: PickCond,
|
||||
pickedRef: string,
|
||||
onPicked: (item: ElementBrief | null) => void,
|
||||
): HTMLElement {
|
||||
const box = el("div", { className: "m01lab__data", attrs: { "data-candidates": "1" } });
|
||||
box.append(muted("…"));
|
||||
void searchElements("재료", seedOf(cond))
|
||||
.then((got) => {
|
||||
onPicked(got.items.find((it) => it.ref === pickedRef) ?? null);
|
||||
const rows = got.items.map((it) =>
|
||||
el("tr", {
|
||||
className: it.ref === pickedRef ? "m01lab__hit" : "",
|
||||
attrs: it.ref === pickedRef ? { "data-hit": "1" } : {},
|
||||
children: [
|
||||
el("td", { text: it.이름 ?? it.ref }),
|
||||
el("td", { text: it.규격 ?? "" }),
|
||||
el("td", { text: String(it.단위 ?? "") }),
|
||||
el("td", { className: "m01-logic__money", text: formatPrice(it.값) }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
const more = got.total > got.items.length ? ` (${got.items.length})` : "";
|
||||
box.replaceChildren(
|
||||
el("strong", { text: `${tl("Modal_Candidates")} ${got.total}${more}` }),
|
||||
el("div", {
|
||||
className: "m01-logic__scroll",
|
||||
children: [
|
||||
el("table", {
|
||||
className: "m01-logic__grid m01lab__table",
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [
|
||||
el("tr", {
|
||||
children: [
|
||||
tl("Col_Name"),
|
||||
tl("Modal_Spec"),
|
||||
tl("Col_Unit"),
|
||||
tl("Modal_Value"),
|
||||
].map((h) => el("th", { text: h })),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
el("tbody", { children: rows }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch(() => box.replaceChildren(muted(tl("Pill_NoPick"))));
|
||||
return box;
|
||||
}
|
||||
|
||||
/** 고르기 줄에서 시험 계산이 잡은 품목 키 — 대표가 `{입력이름}` 이면 넣은 값 · 글자 키면 그대로 · 없으면 서버 미리보기 줄 */
|
||||
function pickedOf(cond: PickCond, values: Record<string, string>, fallback: string): string {
|
||||
const rep = String(cond["대표"] ?? "");
|
||||
const named = /^\{(.+)\}$/.exec(rep);
|
||||
return (named ? (values[named[1]] ?? "") : rep) || fallback;
|
||||
}
|
||||
|
||||
/** 줄을 눌렀을 때 — 그 줄의 단가(재료·인력·기계 또는 다른 로직)를 바로 보임(알약 없이) ·
|
||||
* 재료 고르기 줄(`cond`)은 잡힌 품목 단가 + 후보 목록 */
|
||||
export function openMaterialModal(target: ModalTarget): void {
|
||||
openDialog(target.title, (body) => {
|
||||
const cond = target.cond;
|
||||
const element = target.element ?? "";
|
||||
const base = element.split(".")[0];
|
||||
// 고르기 줄 = 서버가 조건 글을 열쇠로 준 줄(대표가 안 정해졌으면 없음)
|
||||
const priced = cond ? target.prices[condText(cond)] : undefined;
|
||||
const known = cond
|
||||
? target.unitRef && priced?.ref !== target.unitRef
|
||||
? undefined // 서버가 잡은 품목이 다르면 그 품목으로
|
||||
: priced
|
||||
: element
|
||||
? (target.prices[element] ?? target.prices[base])
|
||||
: undefined;
|
||||
const priceRef = cond
|
||||
? target.unitRef || pickedOf(cond, target.values, known?.ref ?? "")
|
||||
: element;
|
||||
const logicKey = /^로직\((\w+)/.exec(element)?.[1] ?? "";
|
||||
const sum = el("p", { className: "m01lab__sum" });
|
||||
const setSum = (price: ElementBrief | null | undefined): void => {
|
||||
const unit = price ? `${tl("Modal_UnitPrice")} ${formatPrice(price.값)} × ` : "";
|
||||
if (target.qtyFrag) {
|
||||
sum.replaceChildren(`${target.title} = ${unit}`, fragmentRow(target.qtyFrag));
|
||||
} else sum.textContent = `${target.title} = ${unit}${explain(target.expr)}`;
|
||||
};
|
||||
setSum(known);
|
||||
const rate = el("div", { className: "m01lab__modal-body" });
|
||||
if (known && priceRef) {
|
||||
rate.append(priceView(priceRef, known));
|
||||
} else if (logicKey) {
|
||||
rate.append(
|
||||
el("p", {
|
||||
children: [
|
||||
`${tl("Modal_Logic")} · `,
|
||||
fragmentRow(
|
||||
target.unitFrag ?? [
|
||||
{ pill: { label: logicKey, onClick: () => openLogic(logicKey) } },
|
||||
],
|
||||
),
|
||||
],
|
||||
}),
|
||||
);
|
||||
} else if (cond && priceRef) {
|
||||
rate.append(muted("…")); // 후보 목록이 오면 그 줄로 채움 — 100건 밖이면 키로 따로 찾아 옴
|
||||
} else if (cond) {
|
||||
rate.append(muted(tl("Pill_NoPick")));
|
||||
} else if (element.includes("{")) {
|
||||
// 자리 표시 든 요소 — 날글 대신 서버가 푼 키로 단가 · 못 풀면 입력 이름만
|
||||
const ref = target.unitRef;
|
||||
if (!ref) {
|
||||
const names = [...element.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]).join(" · ");
|
||||
rate.append(muted(`${target.unitText || names} — ${tl("Pill_Unresolved")}`));
|
||||
} else {
|
||||
const show = (brief: ElementBrief | null): void => {
|
||||
rate.replaceChildren(
|
||||
brief
|
||||
? priceView(ref, brief)
|
||||
: muted(`${target.unitText ?? ref} — ${tl("Pill_NoValue")}`),
|
||||
);
|
||||
setSum(brief);
|
||||
};
|
||||
rate.append(muted("…"));
|
||||
const cached = target.prices[ref];
|
||||
if (cached) show(cached);
|
||||
else
|
||||
void searchElements(groupOf(ref), ref)
|
||||
.then((got) => show(got.items.find((it) => it.ref === ref) ?? null))
|
||||
.catch(() => show(null));
|
||||
}
|
||||
} else if (element) {
|
||||
rate.append(muted(`${element} — ${tl("Pill_NoValue")}`));
|
||||
}
|
||||
const unit = price ? `${tl("Modal_UnitPrice")} ${formatMoney(price.값)} × ` : "";
|
||||
body.replaceChildren(
|
||||
...rateParts,
|
||||
el("p", { className: "m01-logic__muted", text: tl("Modal_Sum") }),
|
||||
el("p", {
|
||||
className: "m01lab__sum",
|
||||
text: `${target.title} = ${unit}${explain(target.expr)}`,
|
||||
}),
|
||||
rate,
|
||||
...(cond
|
||||
? [
|
||||
candidateView(cond, priceRef, (item) => {
|
||||
if (known || !priceRef) return;
|
||||
const show = (brief: ElementBrief | null): void => {
|
||||
rate.replaceChildren(
|
||||
brief ? priceView(priceRef, brief) : muted(`${priceRef} — ${tl("Pill_NoValue")}`),
|
||||
);
|
||||
setSum(brief);
|
||||
};
|
||||
if (item) return show(item);
|
||||
void searchElements("재료", priceRef)
|
||||
.then((got) => show(got.items.find((it) => it.ref === priceRef) ?? null))
|
||||
.catch(() => show(null));
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
muted(tl("Modal_Sum")),
|
||||
sum,
|
||||
el("details", {
|
||||
className: "m01lab__raw",
|
||||
children: [
|
||||
el("summary", { text: tl("Modal_Qty") }),
|
||||
el("pre", { className: "m01lab__expr", text: target.expr }),
|
||||
el("pre", {
|
||||
className: "m01lab__expr",
|
||||
text: logicKey
|
||||
? `${element}
|
||||
${target.expr}`
|
||||
: target.expr,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** 수량 칸 요소 알약을 눌렀을 때 — 그 요소의 단가 자료(이미 받은 `prices` 에 없으면 키로 찾아 옴) */
|
||||
export function openRefModal(target: {
|
||||
title: string;
|
||||
ref: string;
|
||||
prices: Record<string, ElementBrief | null>;
|
||||
}): void {
|
||||
openDialog(target.title, (body) => {
|
||||
const show = (brief: ElementBrief | null): void =>
|
||||
body.replaceChildren(
|
||||
brief ? priceView(target.ref, brief) : muted(`${target.ref} — ${tl("Pill_NoValue")}`),
|
||||
);
|
||||
const known = target.prices[target.ref];
|
||||
if (known) return show(known);
|
||||
body.replaceChildren(muted("…"));
|
||||
void searchElements(groupOf(target.ref), target.ref)
|
||||
.then((got) => show(got.items.find((it) => it.ref === target.ref) ?? null))
|
||||
.catch(() => show(null));
|
||||
});
|
||||
}
|
||||
|
||||
/** 알약이 가리킬 자료를 못 찾을 때(참조 없음) · 다른 로직을 부르는 알약 — 한 마디만 */
|
||||
export function openNoteModal(title: string, text: string): void {
|
||||
openDialog(title, (body) => body.replaceChildren(el("p", { text })));
|
||||
}
|
||||
|
||||
/** 수량 칸 표 알약을 눌렀을 때 — 그 표만 보임(걸린 줄 강조) */
|
||||
export function openTableModal(target: TableTarget): void {
|
||||
openDialog(target.title, (body) => {
|
||||
@@ -209,7 +440,7 @@ export function openTableModal(target: TableTarget): void {
|
||||
void loadTable(target.table).then((table) => {
|
||||
body.replaceChildren(
|
||||
table
|
||||
? tableView(table, target.conds, target.values)
|
||||
? tableView(table, target)
|
||||
: el("p", { className: "m01-logic__bad", text: `${target.table} ?` }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -33,3 +33,14 @@
|
||||
.m01lab__pill-stage {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 계산 안 된 만약() 가지 조각 — 흐리게 */
|
||||
.m01lab__qty-dim {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* 표 모달 — 찾은 칸(참조.열) 한 번 더 진하게 */
|
||||
td.m01lab__hit-cell,
|
||||
.m01lab__hit td.m01lab__hit-cell {
|
||||
background: color-mix(in srgb, orange 65%, transparent);
|
||||
}
|
||||
|
||||
@@ -28,17 +28,20 @@ export function pillButton(p: Pill): HTMLButtonElement {
|
||||
return b;
|
||||
}
|
||||
|
||||
/** 수량 칸 조각 — 알약 아니면 그냥 글자(연산기호·숫자·중간값 이름) */
|
||||
export type Fragment = { pill: Pill } | { text: string };
|
||||
/** 수량 칸 조각 — 알약 아니면 그냥 글자(연산기호·숫자·중간값 이름) · `dim` = 계산 안 된 갈래(흐리게) */
|
||||
export type Fragment = { pill: Pill; dim?: boolean } | { text: string; dim?: boolean };
|
||||
|
||||
/** 조각을 이어 그림 — 칸 폭을 넘으면 자동으로 줄바꿈(`flex-wrap`) */
|
||||
export function fragmentRow(fragments: Fragment[]): HTMLElement {
|
||||
return el("span", {
|
||||
className: "m01lab__qty-frags",
|
||||
children: fragments.map((f) =>
|
||||
"pill" in f
|
||||
? pillButton(f.pill)
|
||||
: el("span", { className: "m01lab__qty-text", text: f.text }),
|
||||
),
|
||||
children: fragments.map((f) => {
|
||||
const one =
|
||||
"pill" in f
|
||||
? pillButton(f.pill)
|
||||
: el("span", { className: "m01lab__qty-text", text: f.text });
|
||||
if (f.dim) one.classList.add("m01lab__qty-dim");
|
||||
return one;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -175,3 +175,25 @@
|
||||
.m01lab__edit {
|
||||
margin-top: var(--spacing-12);
|
||||
}
|
||||
|
||||
/* 시험 계산 — 재료 · 기계 고르기 표 모달 */
|
||||
.m01-logic__pick--table {
|
||||
width: min(1040px, 94vw);
|
||||
}
|
||||
|
||||
.m01lab__pickrow {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.m01lab__pickrow:hover,
|
||||
.m01lab__pickrow.is-picked {
|
||||
background: var(--color-paper);
|
||||
}
|
||||
|
||||
.m01lab__pickrow.is-picked {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.m01lab__warn {
|
||||
color: var(--color-warning, #b45309);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ const TEXT = {
|
||||
G_other: ["그 밖 (인력·자재·경비 어디에도 안 맞는 줄)", "Other (fits none of the three)"],
|
||||
Tag_Table: ["표", "Table"],
|
||||
Subtotal: ["소계", "Subtotal"],
|
||||
Share: ["몫", "Share"],
|
||||
Total: ["계", "Total"],
|
||||
Extra: ["덧줄", "Extra"],
|
||||
Col_Name: ["이름", "Name"],
|
||||
@@ -58,15 +59,24 @@ const TEXT = {
|
||||
"고르기 조건에 맞는 마스터 줄이 없음 — 단가를 못 찾음",
|
||||
"No master row matches the pick conditions",
|
||||
],
|
||||
Pill_Unresolved: ["입력이 비어 풀리지 않음", "Not resolved — input empty"],
|
||||
Pill_NoValue: ["관리자 값 미확보 — 마스터에 단가가 없음", "No price in master yet"],
|
||||
Modal_Qty: ["수량 식", "Quantity formula"],
|
||||
Modal_Price: ["단가 자료", "Price source"],
|
||||
Modal_Value: ["값", "Value"],
|
||||
Modal_Source: ["출처", "Source"],
|
||||
Modal_Spec: ["규격", "Spec"],
|
||||
Modal_Candidates: [
|
||||
"후보 목록 (노란 줄 = 시험 계산이 쓰는 품목)",
|
||||
"Candidates (yellow = used in the trial)",
|
||||
],
|
||||
Modal_Logic: ["다른 로직을 부름", "Calls another logic"],
|
||||
Modal_NoData: ["표나 단가 자료가 없는 줄 — 수량 식만 있음", "No table or price data"],
|
||||
Modal_Hit: ["노란 줄 = 지금 넣은 값으로 걸린 줄", "Highlighted row matches the trial values"],
|
||||
Modal_ColOnly: [
|
||||
"진한 칸 = 찾은 값 칸 — 서버가 줄 자리는 안 줌",
|
||||
"Dark cells = the value column found; the server gave no row",
|
||||
],
|
||||
Modal_NoHit: [
|
||||
"지금 값으로는 걸린 줄이 없음 — 값을 바꿔 보거나, 이 표를 안 쓰는 갈래일 수 있음",
|
||||
"No row matches the current values — change them, or this table is on an unused branch",
|
||||
|
||||
@@ -66,6 +66,8 @@ export interface ElementBrief {
|
||||
단위?: unknown;
|
||||
값?: unknown;
|
||||
값칸?: Record<string, string> | null;
|
||||
/** 재료 후보 줄 — 값 열 다섯(지역·계약종별별 값) */
|
||||
값들?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface LogicOne {
|
||||
@@ -86,6 +88,10 @@ export interface CalcLine {
|
||||
비목: Record<string, number>;
|
||||
/** 값 없는 요소를 다른 값으로 셈 — 「준용: <직종>」 · 「산정: …」 */
|
||||
출처?: string;
|
||||
/** 자재 단위가 줄 단위와 성격이 달라 환산 못 하고 셈 — 「줄 m · 자재 Ton」 */
|
||||
단위경고?: string;
|
||||
/** 재료 줄이 잡은 품목 — 키 · 이름 · 규격 · 단위 · 값 칸 */
|
||||
품목?: { 키: string; 이름?: string; 규격?: string; 단위?: string; 값칸?: string };
|
||||
}
|
||||
export type CalcAnswer =
|
||||
| {
|
||||
@@ -97,11 +103,28 @@ export type CalcAnswer =
|
||||
}
|
||||
| { ok: false; reason: string };
|
||||
|
||||
/** `/text` 줄의 조각 — 알약 글 = 조각 글 · `깊이` 있으면 계산 안 된 만약() 가지(흐리게) */
|
||||
export interface TextPiece {
|
||||
kind: "요소" | "표" | "로직" | "값" | "기호" | "글";
|
||||
글: string;
|
||||
참조?: {
|
||||
테이블?: string;
|
||||
키: string;
|
||||
행?: string | number | null;
|
||||
줄?: number;
|
||||
열?: string | null;
|
||||
};
|
||||
깊이?: number;
|
||||
}
|
||||
export interface TextLine {
|
||||
이름: string;
|
||||
글: string;
|
||||
/** 같은 줄을 변수 이름으로 적은 식 — 왼쪽 칸 */
|
||||
이름글?: string;
|
||||
/** 수량 식을 쪼갠 조각 — 알약(요소·표·로직) · 글자(값·기호·글) */
|
||||
조각?: TextPiece[];
|
||||
/** 단가 자리(요소 · 로직 부르기)를 쪼갠 조각 — 줄 모달 윗줄 */
|
||||
요소조각?: TextPiece[];
|
||||
/** 줄 번호(답 전체에서 하나씩) — 두 칸 줄 맞춤 · 밝히기 */
|
||||
짝?: number;
|
||||
금액: number;
|
||||
@@ -192,14 +215,24 @@ export const fetchMaterials = (cond: {
|
||||
detail?: string;
|
||||
spec?: string;
|
||||
region?: string;
|
||||
/** 호표 줄 단위 — 주면 그 단위 줄만 */
|
||||
unit?: string;
|
||||
}): Promise<{ total: number; 기본: string | null; items: ElementBrief[] }> =>
|
||||
request(`/materials?${query({ ...cond, limit: "100" })}`);
|
||||
|
||||
/** 자동값 길(`GET /logic/auto`) — 입력 이름 → 견본값 · 서버에 아직 없거나 값이 없으면 빈 채로(화면은 그냥 안 채움) */
|
||||
export const fetchAuto = (key: string): Promise<Record<string, unknown>> =>
|
||||
request<{ 값?: Record<string, unknown> }>(`/logic/auto?${query({ key })}`)
|
||||
.then((d) => d.값 ?? {})
|
||||
.catch(() => ({}));
|
||||
|
||||
export const runCalc = (body: {
|
||||
key: string;
|
||||
inputs: Record<string, unknown>;
|
||||
row?: LogicRow;
|
||||
file?: string;
|
||||
/** 시험 계산에서 고른 재료 — 호표 줄 차례 → 자재품목 키 · 저장 안 함 */
|
||||
고름?: Record<string, string>;
|
||||
}): Promise<CalcAnswer> => request("/calc", body);
|
||||
|
||||
export const runText = (body: {
|
||||
@@ -207,6 +240,8 @@ export const runText = (body: {
|
||||
inputs: Record<string, unknown>;
|
||||
row?: LogicRow;
|
||||
file?: string;
|
||||
/** 시험 계산에서 고른 재료 — 호표 줄 차례 → 자재품목 키 · 저장 안 함 */
|
||||
고름?: Record<string, string>;
|
||||
}): Promise<TextAnswer> => request("/text", body);
|
||||
|
||||
export const saveFiles = (
|
||||
|
||||
@@ -6,16 +6,26 @@
|
||||
|
||||
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
fetchAuto,
|
||||
runCalc,
|
||||
runText,
|
||||
searchElements,
|
||||
type CalcAnswer,
|
||||
type CalcLine,
|
||||
type ElementBrief,
|
||||
type TextAnswer,
|
||||
type LogicRow,
|
||||
} from "./M01_MasterData_UI_Logic_Api";
|
||||
import {
|
||||
isMaterialKeyInput,
|
||||
machineControl,
|
||||
materialRows,
|
||||
pickBody,
|
||||
showItems,
|
||||
} from "./M01_MasterData_UI_Logic_CalcPick";
|
||||
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { formatMoney } from "./M01_MasterData_UI_Logic_Money";
|
||||
import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
|
||||
/** 고르기 값이 마스터 키(자재·기계·직종)면 이름으로 바꿔 보임 — 값 칸은 키 그대로 보냄 */
|
||||
const KEY_RE = /^[A-Z]{2}\d{6}$/;
|
||||
@@ -45,6 +55,8 @@ export interface CalcContext {
|
||||
/** 저장 전 새 초안이면 비움 — 서버가 임시 자리에 얹어 셈 */
|
||||
file?: string;
|
||||
row: LogicRow;
|
||||
/** 잡힌 품목을 보이는 데 씀 — 재료 줄의 단가 요소 · 조건 글 → 요소 */
|
||||
prices?: Record<string, ElementBrief | null>;
|
||||
dirty: () => boolean;
|
||||
/** 로직마다 넣은 값 — 다시 그려도 남음 */
|
||||
values: Record<string, string>;
|
||||
@@ -55,6 +67,9 @@ export interface CalcContext {
|
||||
|
||||
const SUMS = ["노무비", "재료비", "경비", "계"];
|
||||
|
||||
/** 로직마다 자동값을 한 번만 받음(다시 그려도 안 부름) */
|
||||
const autoAsked = new WeakSet<Record<string, string>>();
|
||||
|
||||
/** 로직을 열 때마다 새로 부르는 미리보기 — 늦게 온 응답이 새 로직을 덮지 않게 순번으로 거름 */
|
||||
let previewSeq = 0;
|
||||
|
||||
@@ -63,46 +78,58 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
// 지금 쓰는 LogicLab 화면은 그 칸에 안 걸려 폭이 이미 넓음(실측 730px) · 따로 안 풂
|
||||
const optionText = (o: string | number): string =>
|
||||
(typeof o === "string" && nameCache.get(o)) || String(o);
|
||||
const fields = (ctx.row.입력 ?? []).map((spec) => {
|
||||
const name = spec.이름;
|
||||
let control: HTMLElement;
|
||||
const picks = spec.고르기;
|
||||
if (picks?.length) {
|
||||
const pick = createSelectField({
|
||||
options: ["", ...picks.map(String)].map((o) => ({ value: o, text: optionText(o) })),
|
||||
value: ctx.values[name] ?? "",
|
||||
compact: true,
|
||||
onChange: (v) => (ctx.values[name] = v),
|
||||
});
|
||||
control = pick.root;
|
||||
const keys = picks.filter((o): o is string => typeof o === "string" && KEY_RE.test(o));
|
||||
if (keys.length) {
|
||||
void Promise.all(keys.map(keyName)).then(() => {
|
||||
pick.setOptions(
|
||||
["", ...picks.map(String)].map((o) => ({ value: o, text: optionText(o) })),
|
||||
ctx.values[name] ?? "",
|
||||
);
|
||||
const again = (): void => host.querySelector<HTMLButtonElement>("[data-calc-run]")?.click();
|
||||
const fields = (ctx.row.입력 ?? [])
|
||||
.filter((spec) => !isMaterialKeyInput(spec)) // 재료 키 목록은 표 모달로 바뀌어 걷음
|
||||
.map((spec) => {
|
||||
const name = spec.이름;
|
||||
let control: HTMLElement;
|
||||
const picks = spec.고르기;
|
||||
const machine = machineControl(spec, ctx.values, again);
|
||||
if (machine) {
|
||||
control = machine;
|
||||
} else if (picks?.length) {
|
||||
const pick = createSelectField({
|
||||
options: ["", ...picks.map(String)].map((o) => ({ value: o, text: optionText(o) })),
|
||||
value: ctx.values[name] ?? "",
|
||||
compact: true,
|
||||
onChange: (v) => {
|
||||
ctx.values[name] = v;
|
||||
},
|
||||
});
|
||||
control = pick.root;
|
||||
const keys = picks.filter((o): o is string => typeof o === "string" && KEY_RE.test(o));
|
||||
if (keys.length) {
|
||||
void Promise.all(keys.map(keyName)).then(() => {
|
||||
pick.setOptions(
|
||||
["", ...picks.map(String)].map((o) => ({ value: o, text: optionText(o) })),
|
||||
ctx.values[name] ?? "",
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const box = el("input", {
|
||||
className: "m01-logic__input",
|
||||
attrs: { type: "text", inputmode: "decimal" },
|
||||
});
|
||||
if (spec.범위) box.placeholder = `${spec.범위[0]} ∼ ${spec.범위[1]}`;
|
||||
box.value = ctx.values[name] ?? "";
|
||||
box.addEventListener("input", () => {
|
||||
ctx.values[name] = box.value;
|
||||
});
|
||||
control = box;
|
||||
}
|
||||
} else {
|
||||
const box = el("input", {
|
||||
className: "m01-logic__input",
|
||||
attrs: { type: "text", inputmode: "decimal" },
|
||||
const label = spec.단위 ? `${name} (${spec.단위})` : name;
|
||||
// 긴 이름 줄만 한 칸 다 씀 — 고르기 줄은 짧으면 그대로 2열에 맞춰 나란히
|
||||
const wide = label.length > 10;
|
||||
return el("label", {
|
||||
className: `m01-logic__field${wide ? " m01-logic__field--wide" : ""}`,
|
||||
children: [el("span", { text: label }), control],
|
||||
});
|
||||
if (spec.범위) box.placeholder = `${spec.범위[0]} ∼ ${spec.범위[1]}`;
|
||||
box.value = ctx.values[name] ?? "";
|
||||
box.addEventListener("input", () => (ctx.values[name] = box.value));
|
||||
control = box;
|
||||
}
|
||||
const label = spec.단위 ? `${name} (${spec.단위})` : name;
|
||||
// 긴 이름 줄만 한 칸 다 씀 — 고르기 줄은 짧으면 그대로 2열에 맞춰 나란히
|
||||
const wide = label.length > 10;
|
||||
return el("label", {
|
||||
className: `m01-logic__field${wide ? " m01-logic__field--wide" : ""}`,
|
||||
children: [el("span", { text: label }), control],
|
||||
});
|
||||
});
|
||||
const materials = materialRows(ctx.row, ctx.prices ?? {}, ctx.values, again);
|
||||
const out = el("div", { className: "m01-logic__calc-out" });
|
||||
const mySeqAtOpen = ctx.onText ? previewSeq + 1 : previewSeq; // 이 그리기가 받을 순번
|
||||
let manualRun = false; // [계산] 이 이미 답했으면 뒤늦게 오는 미리보기로 안 덮음
|
||||
if (ctx.onText) {
|
||||
const mySeq = ++previewSeq;
|
||||
@@ -118,6 +145,23 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
ctx.onText?.(answer);
|
||||
});
|
||||
}
|
||||
if (!fields.length && !autoAsked.has(ctx.values)) {
|
||||
autoAsked.add(ctx.values); // 입력이 없으면 채울 것이 없음 — 열 때 한 번 바로 계산
|
||||
queueMicrotask(() => {
|
||||
if (previewSeq === mySeqAtOpen) again();
|
||||
});
|
||||
} else if (ctx.savedKey !== null && !autoAsked.has(ctx.values)) {
|
||||
autoAsked.add(ctx.values);
|
||||
void fetchAuto(ctx.savedKey).then((auto) => {
|
||||
const names = (ctx.row.입력 ?? [])
|
||||
.map((spec) => spec.이름)
|
||||
.filter((n) => auto[n] !== undefined && auto[n] !== null && !(ctx.values[n] ?? "").trim());
|
||||
if (!names.length || previewSeq !== mySeqAtOpen) return; // 값이 없거나 다른 로직이 이미 열림
|
||||
for (const n of names) ctx.values[n] = String(auto[n]);
|
||||
buildCalc(host, ctx); // 채운 값을 새로 그리고 바로 계산(결과 · 텍스트 수식까지)
|
||||
again();
|
||||
});
|
||||
}
|
||||
const run = async (): Promise<void> => {
|
||||
manualRun = true;
|
||||
const inputs: Record<string, unknown> = {};
|
||||
@@ -134,34 +178,42 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
const body = {
|
||||
key: ctx.savedKey ?? ctx.row.키,
|
||||
inputs,
|
||||
...pickBody(ctx.values),
|
||||
...(draft ? { row: ctx.row, file: ctx.file } : {}),
|
||||
};
|
||||
const answer = await runCalc(body);
|
||||
if (ctx.onText) ctx.onText(await runText(body));
|
||||
ctx.onLines(answer.ok ? (answer.lines ?? null) : null);
|
||||
out.replaceChildren(...answerView(answer, draft));
|
||||
if (answer.ok && answer.lines) showItems(host, ctx.values, answer.lines);
|
||||
out.replaceChildren(...answerView(answer, draft, (ctx.row.결과단위 ?? "").startsWith("원")));
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
|
||||
}
|
||||
};
|
||||
const runButton = createButton({ label: tx("Calc_Run"), onClick: () => void run() });
|
||||
runButton.setAttribute("data-calc-run", "");
|
||||
host.replaceChildren(
|
||||
el("h3", { text: tx("Calc_Title") }),
|
||||
...(fields.length
|
||||
? [el("div", { className: "m01-logic__head", children: fields })] // 요소 표가 쓰는 auto-fill 그리드 재사용 — 2열
|
||||
...(fields.length || materials.length
|
||||
? [el("div", { className: "m01-logic__head", children: [...fields, ...materials] })] // 요소 표가 쓰는 auto-fill 그리드 재사용 — 2열
|
||||
: [el("p", { className: "m01-logic__muted", text: tx("Calc_NoInputs") })]),
|
||||
createButton({ label: tx("Calc_Run"), onClick: () => void run() }),
|
||||
runButton,
|
||||
out,
|
||||
);
|
||||
}
|
||||
|
||||
function answerView(answer: CalcAnswer, draft: boolean): HTMLElement[] {
|
||||
/** `money` = 결과가 돈(원) — 그때만 결과 값도 정수 · 수량·비율 결과는 소수 그대로 */
|
||||
function answerView(answer: CalcAnswer, draft: boolean, money: boolean): HTMLElement[] {
|
||||
const note = draft ? [el("p", { className: "m01-logic__muted", text: tx("Calc_Draft") })] : [];
|
||||
if (!answer.ok) {
|
||||
return [
|
||||
...note,
|
||||
el("div", {
|
||||
className: "m01-logic__reasons",
|
||||
children: [el("strong", { text: tx("Calc_Stopped") }), el("div", { text: answer.reason })],
|
||||
children: [
|
||||
el("strong", { text: tx("Calc_Stopped") }),
|
||||
el("div", { text: safeReason(answer.reason) }),
|
||||
],
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -170,9 +222,25 @@ function answerView(answer: CalcAnswer, draft: boolean): HTMLElement[] {
|
||||
const rows = answer.lines.map((line) =>
|
||||
el("tr", {
|
||||
children: [
|
||||
el("td", { text: line.출처 ? `${line.이름} (${line.출처})` : line.이름 }),
|
||||
el("td", {
|
||||
children: [
|
||||
el("span", {
|
||||
text: line.출처
|
||||
? `${line.이름} (${line.출처.replace(/[A-Z]{2}\d{6}\./g, "")})`
|
||||
: line.이름,
|
||||
}),
|
||||
...(line.단위경고
|
||||
? [
|
||||
el("span", {
|
||||
className: "m01-logic__tag m01lab__warn",
|
||||
text: `${tx("Calc_UnitWarn")} · ${line.단위경고}`,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
el("td", { text: `${formatNumber(line.수량)} ${line.단위}` }),
|
||||
el("td", { className: "m01-logic__money", text: formatNumber(line.금액) }),
|
||||
el("td", { className: "m01-logic__money", text: formatMoney(line.금액) }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -200,7 +268,7 @@ function answerView(answer: CalcAnswer, draft: boolean): HTMLElement[] {
|
||||
className: "m01-logic__sums",
|
||||
children: SUMS.flatMap((k) => [
|
||||
el("dt", { text: k === "계" ? tx("Calc_Sum") : k }),
|
||||
el("dd", { className: "m01-logic__money", text: formatNumber(answer.sums?.[k] ?? 0) }),
|
||||
el("dd", { className: "m01-logic__money", text: formatMoney(answer.sums?.[k] ?? 0) }),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
@@ -211,12 +279,13 @@ function answerView(answer: CalcAnswer, draft: boolean): HTMLElement[] {
|
||||
className: "m01-logic__sums",
|
||||
children: [
|
||||
el("dt", { text: tx("Calc_Result") }),
|
||||
el("dd", { text: formatNumber(answer.result) }),
|
||||
el("dd", { text: money ? formatMoney(answer.result) : formatNumber(answer.result) }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
const middle = Object.entries(answer.middle ?? {});
|
||||
// 줄 묶음(객체)은 위 상세와 겹쳐 숨김 · 돈 로직의 큰 수는 정수
|
||||
const middle = Object.entries(answer.middle ?? {}).filter(([, v]) => typeof v !== "object");
|
||||
if (middle.length) {
|
||||
parts.push(
|
||||
el("h4", { text: tx("Middle_Title") }),
|
||||
@@ -224,7 +293,12 @@ function answerView(answer: CalcAnswer, draft: boolean): HTMLElement[] {
|
||||
className: "m01-logic__sums",
|
||||
children: middle.flatMap(([k, v]) => [
|
||||
el("dt", { text: k }),
|
||||
el("dd", { text: formatNumber(v) }),
|
||||
el("dd", {
|
||||
text:
|
||||
money && typeof v === "number" && Math.abs(v) >= 1000
|
||||
? formatMoney(v)
|
||||
: formatNumber(v),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Logic_CalcPick.ts
|
||||
* 시험 계산의 재료 · 기계 고르기 — 재료 줄마다 「잡힌 품목 + 품목 고르기」 · 기계 입력은 단추 하나
|
||||
* 누르면 마스터 표 모달(`PickTable`) · 고른 재료는 `고름`(호표 줄 차례 → 키)에 실어 다시 계산 · 저장 안 함
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { fetchRows, type Row } from "./M01_MasterData_Api_Fetch";
|
||||
import type { CalcLine, ElementBrief, LogicInput, LogicRow } from "./M01_MasterData_UI_Logic_Api";
|
||||
import { openPickTable } from "./M01_MasterData_UI_Logic_PickTable";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { condText, headWord, type PickCond } from "./M01_MasterData_UI_LogicLab_Modal";
|
||||
|
||||
interface Chosen {
|
||||
ref: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/** 로직마다 넣은 값(`values`)에 딸린 고른 재료 — 다시 그려도 남고 서버에는 계산 요청에만 실림 */
|
||||
const chosen = new WeakMap<Record<string, string>, Map<number, Chosen>>();
|
||||
const picksOf = (values: Record<string, string>): Map<number, Chosen> => {
|
||||
let mine = chosen.get(values);
|
||||
if (!mine) chosen.set(values, (mine = new Map()));
|
||||
return mine;
|
||||
};
|
||||
|
||||
/** 계산 요청 몸의 `고름` — 고른 것이 없으면 안 실음 */
|
||||
export function pickBody(values: Record<string, string>): { 고름?: Record<string, string> } {
|
||||
const mine = picksOf(values);
|
||||
return mine.size
|
||||
? { 고름: Object.fromEntries([...mine].map(([i, c]) => [String(i), c.ref])) }
|
||||
: {};
|
||||
}
|
||||
|
||||
const nameOf = (row: Row): string => `${row["이름"] ?? ""} ${row["규격"] ?? ""}`.trim();
|
||||
|
||||
const MATERIAL_KEYS = /^MT\d{6}$/;
|
||||
const MACHINE_CODE = /^\d{4}-\d{4}$/;
|
||||
const MACHINE_KEY = /^EQ\d{6}$/;
|
||||
|
||||
/** 재료 키 목록 입력 — 표 모달로 바뀌어 걷음 */
|
||||
export const isMaterialKeyInput = (spec: LogicInput): boolean =>
|
||||
!!spec.고르기?.length && spec.고르기.every((o) => MATERIAL_KEYS.test(String(o)));
|
||||
|
||||
const isMachineInput = (spec: LogicInput): boolean =>
|
||||
!!spec.고르기?.length &&
|
||||
spec.고르기.every((o) => MACHINE_CODE.test(String(o)) || MACHINE_KEY.test(String(o)));
|
||||
|
||||
const pickButton = (label: string, onClick: () => void): HTMLButtonElement =>
|
||||
createButton({ label, variant: "ghost", onClick });
|
||||
|
||||
const line = (label: string, current: HTMLElement, button: HTMLElement): HTMLElement =>
|
||||
el("div", {
|
||||
className: "m01-logic__field m01-logic__field--wide",
|
||||
children: [el("span", { text: label }), el("div", { children: [current, button] })],
|
||||
});
|
||||
|
||||
/** 재료 줄마다 한 칸 — 지금 잡힌 품목 + 품목 고르기 */
|
||||
export function materialRows(
|
||||
row: LogicRow,
|
||||
prices: Record<string, ElementBrief | null>,
|
||||
values: Record<string, string>,
|
||||
again: () => void,
|
||||
): HTMLElement[] {
|
||||
const mine = picksOf(values);
|
||||
return (row.호표 ?? [])
|
||||
.map((item, i) => ({ item, i }))
|
||||
.filter((x) => x.item.종류 === "재료")
|
||||
.map(({ item, i }) => {
|
||||
const cond =
|
||||
typeof item.요소 === "object" && item.요소 !== null
|
||||
? (item.요소 as unknown as PickCond)
|
||||
: null;
|
||||
const brief = (cond ? prices[condText(cond)] : prices[item.요소]) ?? null;
|
||||
const named = item.이름 && !/^[A-Z]{2}\d{6}/.test(item.이름) ? item.이름 : "";
|
||||
const label = named || (brief?.이름 as string | undefined) || "";
|
||||
const held = mine.get(i);
|
||||
const nowName =
|
||||
held?.label ?? (brief ? `${brief.이름 ?? ""} ${brief.규격 ?? ""}`.trim() : "") ?? "";
|
||||
const seeds = [cond?.["검색어"], cond?.이름, cond?.상세구분, cond?.구분, label]
|
||||
.map(headWord)
|
||||
.filter((w, k, all) => w && all.indexOf(w) === k);
|
||||
const current = el("span", {
|
||||
className: "m01-logic__muted",
|
||||
text: `${nowName || tx("Mat_NoPrice")} `,
|
||||
attrs: { "data-mat-idx": String(i), "data-ref": brief?.ref ?? "" },
|
||||
});
|
||||
const button = pickButton(tx("Pick_Item"), () =>
|
||||
openPickTable({
|
||||
kind: "재료",
|
||||
title: `${tx("Pick_MatTitle")} · ${label}`,
|
||||
seed: seeds[0] ?? "",
|
||||
alt: seeds.slice(1),
|
||||
isCurrent: (r) => String(r["키"]) === (mine.get(i)?.ref ?? current.dataset.ref),
|
||||
onPick: (r) => {
|
||||
mine.set(i, { ref: String(r["키"]), label: nameOf(r) });
|
||||
current.textContent = `${nameOf(r)} `;
|
||||
again();
|
||||
},
|
||||
}),
|
||||
);
|
||||
return line(`${label}${item.규격 ? ` ${item.규격}` : ""}`, current, button);
|
||||
});
|
||||
}
|
||||
|
||||
/** 계산 답의 `품목` 을 잡힌 품목 글로 — 고른 재료가 있는 줄은 그대로 */
|
||||
export function showItems(
|
||||
host: HTMLElement,
|
||||
values: Record<string, string>,
|
||||
lines: CalcLine[],
|
||||
): void {
|
||||
const mine = picksOf(values);
|
||||
host.querySelectorAll<HTMLElement>("[data-mat-idx]").forEach((span) => {
|
||||
const i = Number(span.dataset.matIdx);
|
||||
const item = lines[i]?.품목;
|
||||
if (!item || mine.has(i)) return;
|
||||
span.dataset.ref = item.키;
|
||||
span.textContent = `${`${item.이름 ?? ""} ${item.규격 ?? ""}`.trim() || item.키} `;
|
||||
});
|
||||
}
|
||||
|
||||
/** 기계 입력 칸 — 표 모달로 고르면 입력값(원문 분류번호 · 키 목록이면 키) */
|
||||
const machines = new Map<string, Row | null>();
|
||||
async function machineRow(code: string): Promise<Row | null> {
|
||||
if (!machines.has(code)) {
|
||||
let found: Row | null = null;
|
||||
try {
|
||||
const got = await fetchRows("기계.json", 1, 5, code);
|
||||
found = got.rows.find((r) => r["원문번호"] === code || r["키"] === code) ?? null;
|
||||
} catch {
|
||||
found = null;
|
||||
}
|
||||
machines.set(code, found);
|
||||
}
|
||||
return machines.get(code) ?? null;
|
||||
}
|
||||
|
||||
export function machineControl(
|
||||
spec: LogicInput,
|
||||
values: Record<string, string>,
|
||||
again: () => void,
|
||||
): HTMLElement | null {
|
||||
if (!isMachineInput(spec)) return null;
|
||||
const current = el("span", { className: "m01-logic__muted" });
|
||||
const paint = (): void => {
|
||||
const code = values[spec.이름] ?? "";
|
||||
const known = machines.get(code);
|
||||
current.textContent = `${code ? `${code}${known ? ` · ${nameOf(known)}` : ""}` : tx("Mat_NoPrice")} `;
|
||||
if (code && !machines.has(code)) void machineRow(code).then(paint);
|
||||
};
|
||||
paint();
|
||||
const button = pickButton(tx("Pick_Machine"), () => {
|
||||
const code = values[spec.이름] ?? "";
|
||||
const known = machines.get(code);
|
||||
openPickTable({
|
||||
kind: "기계",
|
||||
title: `${tx("Pick_Machine")} · ${spec.이름}`,
|
||||
seed: String(known?.["이름"] ?? spec.이름),
|
||||
isCurrent: (r) => !!code && (r["원문번호"] === code || r["키"] === code),
|
||||
onPick: (r) => {
|
||||
const useKey = MACHINE_KEY.test(String(spec.고르기?.[0]));
|
||||
const value = String(useKey ? r["키"] : r["원문번호"]);
|
||||
machines.set(value, r);
|
||||
values[spec.이름] = value;
|
||||
paint();
|
||||
again();
|
||||
},
|
||||
});
|
||||
});
|
||||
return el("div", { children: [current, button] });
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Logic_Money.ts
|
||||
* 돈 모양 한 벌 — 원 단위 정수(반올림). 단가·금액·소계·계 자리만 씀 · 수량·비율은 `formatNumber` 그대로
|
||||
* ========================================================================== */
|
||||
|
||||
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
||||
|
||||
export const formatMoney = (value: unknown): string =>
|
||||
typeof value === "number"
|
||||
? value.toLocaleString("ko-KR", { maximumFractionDigits: 0 })
|
||||
: formatNumber(value);
|
||||
|
||||
/** 단가 모양 — 정수로 보이되 200 원 미만은 반올림 오차가 커서 소수 넷째 자리·유효숫자 4 자리 중 많은 쪽(뒤 0 뗌) */
|
||||
export const formatPrice = (value: unknown): string =>
|
||||
typeof value === "number" && value !== 0 && Math.abs(value) < 200
|
||||
? value.toLocaleString("ko-KR", {
|
||||
maximumFractionDigits: Math.max(4, 3 - Math.floor(Math.log10(Math.abs(value)))),
|
||||
})
|
||||
: formatMoney(value);
|
||||
@@ -0,0 +1,142 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Logic_PickTable.ts
|
||||
* 시험 계산의 재료 · 기계 고르기 모달 — 마스터 컨테이너와 같은 표(`GET /rows`) · 쪽 나눔 · 칸 글
|
||||
* 검색 칸을 채워 열고 · 잡힌 품목 칠함 · 한 줄 누르면 고름 · 저장 안 함(읽기만)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements";
|
||||
import { fetchRows, type Row } from "./M01_MasterData_Api_Fetch";
|
||||
import { buildPager, flatten, show } from "./M01_MasterData_UI_Cells";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
|
||||
export type PickTableKind = "재료" | "기계";
|
||||
|
||||
const FILES: Record<PickTableKind, string> = { 재료: "재료_자재품목.json", 기계: "기계.json" };
|
||||
/** 표에 보일 칸 — 컨테이너 표의 칸 이름(묶음 값은 「값›가격」) */
|
||||
const COLS: Record<PickTableKind, string[]> = {
|
||||
재료: [
|
||||
"키",
|
||||
"구분",
|
||||
"상세구분",
|
||||
"이름",
|
||||
"규격",
|
||||
"단위",
|
||||
"물가자료",
|
||||
"유통물가",
|
||||
"물가정보",
|
||||
"거래가격",
|
||||
],
|
||||
기계: ["원문번호", "구분", "상세구분", "이름", "규격", "값›가격"],
|
||||
};
|
||||
const SIZE = 30;
|
||||
|
||||
export interface PickTableOptions {
|
||||
kind: PickTableKind;
|
||||
title: string;
|
||||
/** 검색 칸을 채워 엶 — 결과가 없으면 첫 낱말만으로 다시 · 그래도 없으면 `alt` 를 차례로 */
|
||||
seed: string;
|
||||
alt?: string[];
|
||||
/** 지금 잡힌 줄이면 true — 그 줄을 칠함 */
|
||||
isCurrent: (row: Row) => boolean;
|
||||
onPick: (row: Row) => void;
|
||||
}
|
||||
|
||||
export function openPickTable(opt: PickTableOptions): void {
|
||||
const close = (): void => back.remove();
|
||||
const searchField = createInputField({
|
||||
type: "search",
|
||||
placeholder: tx("Pick_Search"),
|
||||
value: opt.seed,
|
||||
});
|
||||
const search = searchField.input;
|
||||
const host = el("div", { className: "m01-logic__pick-list" });
|
||||
const dialog = el("div", {
|
||||
className: "m01-logic__pick m01-logic__pick--table",
|
||||
attrs: { role: "dialog", "aria-label": opt.title },
|
||||
children: [
|
||||
el("div", {
|
||||
className: "m01-logic__section-head",
|
||||
children: [
|
||||
el("h3", { text: opt.title }),
|
||||
createButton({ label: tx("Pick_Close"), variant: "ghost", onClick: close }),
|
||||
],
|
||||
}),
|
||||
searchField.root,
|
||||
host,
|
||||
],
|
||||
});
|
||||
const back = el("div", { className: "m01-logic__backdrop", children: [dialog] });
|
||||
back.addEventListener("click", (ev) => ev.target === back && close());
|
||||
back.addEventListener("keydown", (ev) => ev.key === "Escape" && close());
|
||||
|
||||
let page = 1;
|
||||
const tried = new Set([opt.seed]);
|
||||
let seq = 0;
|
||||
const load = async (fallback = false): Promise<void> => {
|
||||
const mine = ++seq;
|
||||
try {
|
||||
const q = search.value.trim();
|
||||
const got = await fetchRows(FILES[opt.kind], page, SIZE, q);
|
||||
if (mine !== seq) return; // 최신 응답만
|
||||
if (!got.total && fallback) {
|
||||
const next = [...(q.includes(" ") ? [q.split(/\s+/)[0]] : []), ...(opt.alt ?? [])].find(
|
||||
(w) => w && !tried.has(w),
|
||||
);
|
||||
if (next) {
|
||||
tried.add(next);
|
||||
search.value = next;
|
||||
return load(true);
|
||||
}
|
||||
}
|
||||
const cols = COLS[opt.kind];
|
||||
const body = got.rows.map((row) => {
|
||||
const flat = flatten(row);
|
||||
const tr = el("tr", {
|
||||
className: `m01lab__pickrow${opt.isCurrent(row) ? " is-picked" : ""}`,
|
||||
children: cols.map((c) =>
|
||||
el("td", { text: show(flat[c]), attrs: { title: show(flat[c]) } }),
|
||||
),
|
||||
});
|
||||
tr.addEventListener("click", () => {
|
||||
close();
|
||||
opt.onPick(row);
|
||||
});
|
||||
return tr;
|
||||
});
|
||||
host.replaceChildren(
|
||||
buildPager(got.total, SIZE, got.page, (next) => {
|
||||
page = next;
|
||||
void load();
|
||||
}),
|
||||
el("div", {
|
||||
className: "m01-master__grid-wrap",
|
||||
children: [
|
||||
el("table", {
|
||||
className: "m01-master__grid",
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [el("tr", { children: cols.map((c) => el("th", { text: c })) })],
|
||||
}),
|
||||
el("tbody", { children: body }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
host.querySelector(".is-picked")?.scrollIntoView({ block: "nearest" });
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
|
||||
}
|
||||
};
|
||||
let timer = 0;
|
||||
search.addEventListener("input", () => {
|
||||
window.clearTimeout(timer);
|
||||
timer = window.setTimeout(() => {
|
||||
page = 1;
|
||||
void load();
|
||||
}, 300);
|
||||
});
|
||||
document.body.append(back);
|
||||
search.focus();
|
||||
void load(true);
|
||||
}
|
||||
@@ -84,6 +84,13 @@
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
/* 시험 계산 입력만 2열 고정 — 요소 표 쪽(auto-fill)은 그대로 둠
|
||||
실제 그릇은 LogicLab 이 씌우는 `.m01lab__calc`(`.m01-logic__calc` 는 옛 테스트 컨테이너 몫이라 안 걸림) */
|
||||
.m01lab__calc .m01-logic__head,
|
||||
.m01-logic__calc .m01-logic__head {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.m01-logic__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -67,6 +67,11 @@ const TEXT = {
|
||||
Pick_Title: ["요소 찾기", "Find element"],
|
||||
Pick_Search: ["이름·키·원문번호", "Name · key · source no."],
|
||||
Pick_Total: ["{n} 개 중 앞 100 개", "first 100 of {n}"],
|
||||
Pick_Item: ["품목 고르기", "Pick item"],
|
||||
Pick_Machine: ["기계 고르기", "Pick machine"],
|
||||
Pick_MatTitle: ["자재품목 고르기", "Pick a material item"],
|
||||
Pick_Chosen: ["고름", "Picked"],
|
||||
Calc_UnitWarn: ["단위 다름", "Unit differs"],
|
||||
Pick_Close: ["닫기", "Close"],
|
||||
Pick_All: ["(전체)", "(All)"],
|
||||
Mat_Title: ["쓰이는 재료·기계", "Materials and machines"],
|
||||
@@ -137,6 +142,10 @@ const TEXT = {
|
||||
"「{name}」 규격이 여럿이라 정하지 못함 — 위 「쓰이는 재료·기계」에서 하나를 골라 주세요",
|
||||
"「{name}」 has several specs — pick one in the materials list above",
|
||||
],
|
||||
Stop_Generic: [
|
||||
"이 조건으로는 셈하지 못함 — 입력값 · 표에 그 조합이 있는지 확인",
|
||||
"Cannot be calculated with these inputs — check the values and the table",
|
||||
],
|
||||
Stop_NoPrice: [
|
||||
"「{name}」 단가가 자료에 비어 있어 셈하지 못함 — 자료를 채워야 함",
|
||||
"「{name}」 has no price in the data yet",
|
||||
@@ -157,6 +166,12 @@ export function plainReason(reason: string): string {
|
||||
return reason;
|
||||
}
|
||||
|
||||
/** 화면에 찍는 까닭의 마지막 방어 — 중괄호 · 따옴표 · Decimal( 이 섞인 날글은 사람 말 한 줄로 */
|
||||
export function safeReason(reason: string): string {
|
||||
const plain = plainReason(reason);
|
||||
return /[{}"']|Decimal\(/.test(plain) ? tx("Stop_Generic") : plain;
|
||||
}
|
||||
|
||||
/** 흐름 그림 머리 「이렇게 씀」 — 줄 목록 */
|
||||
|
||||
/** 공용 `t()` 와 같은 뜻 — `{n}`·`{v}` 는 채움 */
|
||||
|
||||
+144
-120
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"다음": {"LB": 409, "EQ": 655, "RT": 20, "MN": 7000, "MT": 30719, "MO": 74, "MP": 364, "FX": 6, "CF": 159, "QF": 477, "GF": 252, "CC": 341, "GC": 1104, "QC": 2060, "GX": 1, "UA": 2},
|
||||
"다음": {"LB": 409, "EQ": 655, "RT": 20, "MN": 7000, "MT": 30719, "MO": 74, "MP": 364, "FX": 6, "CF": 159, "QF": 477, "GF": 252, "CC": 341, "GC": 1104, "QC": 2060, "GX": 1, "UA": 26},
|
||||
"폐기": {"MT000001": "자재품목 휘발유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000002": "자재품목 경유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000003": "자재품목 선박용경유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000004": "자재품목 중유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000005": "자재품목 등유 — 값이 비고 글뿐 · 유가 테이블로 감", "LB000399": "이름 「〃」 — 직종이 아니라 되풀이 기호 · 소요량 13-6-9 에서 윗줄 직종으로 바로잡음", "MN": "재료_나라장터자재 — old 로 옮김 · 로직은 자재품목·품셈재료로 갈아 끼움", "LB000372": "미확보 고급기능사 — 측량노임 항공사진고급기능사 옛이름으로 옮김", "LB000393": "미확보 리베팅공 — 건설노임 철골공 옛이름으로 옮김(2010 통합표 연번10)", "LB000405": "미확보 Belt Conveyor 설치공 — 품셈 13-10-1 주② 배분으로 다섯 직종에 나눔", "MT000006": "자재품목 전력 — 재료_유가전력 전력 줄로 감", "MT000007": "자재품목 전력 — 재료_유가전력 전력 줄로 감", "LB000368": "미확보 기술사 — 기술사(건설) 옛이름으로 옮김", "LB000369": "미확보 특급기술자 — 특급기술자(건설) 옛이름으로 옮김", "LB000370": "미확보 고급기술자 — 고급기술자(건설) 옛이름으로 옮김", "LB000361": "미확보 중급기술자 — 중급기술자(건설) 옛이름으로 옮김", "LB000364": "미확보 초급기술자 — 초급기술자(건설) 옛이름으로 옮김", "LB000365": "미확보 고급숙련기술자 — 고급숙련기술자(건설) 옛이름으로 옮김", "LB000366": "미확보 중급숙련기술자 — 중급숙련기술자(건설) 옛이름으로 옮김", "LB000367": "미확보 초급숙련기술자 — 초급숙련기술자(건설) 옛이름으로 옮김", "LB000389": "미확보 기계기사 — 초급기술자(기계·설비) 옛이름으로 옮김", "LB000388": "미확보 기계산업기사 — 초급숙련기술자(기계·설비) 옛이름으로 옮김", "MO000055": {"원문번호": "주택용저압:기본_200이하", "파일": "재료_유가전력.json"}, "MO000056": {"원문번호": "주택용저압:기본_201~400", "파일": "재료_유가전력.json"}, "MO000057": {"원문번호": "주택용저압:기본_400초과", "파일": "재료_유가전력.json"}, "MO000059": {"원문번호": "주택용저압:전력량_다음200", "파일": "재료_유가전력.json"}, "MO000060": {"원문번호": "주택용저압:전력량_400초과", "파일": "재료_유가전력.json"}, "MO000061": {"원문번호": "주택용저압:월간최저요금", "파일": "재료_유가전력.json"}, "MO000062": {"원문번호": "일반용갑I저압:기본", "파일": "재료_유가전력.json"}, "MO000063": {"원문번호": "일반용갑I저압:전력량_여름철", "파일": "재료_유가전력.json"}, "MO000064": {"원문번호": "일반용갑I저압:전력량_봄가을철", "파일": "재료_유가전력.json"}, "MO000065": {"원문번호": "일반용갑I저압:전력량_겨울철", "파일": "재료_유가전력.json"}, "MO000066": {"원문번호": "산업용갑I저압:기본", "파일": "재료_유가전력.json"}, "MO000067": {"원문번호": "산업용갑I저압:전력량_여름철", "파일": "재료_유가전력.json"}, "MO000068": {"원문번호": "산업용갑I저압:전력량_봄가을철", "파일": "재료_유가전력.json"}, "MO000069": {"원문번호": "산업용갑I저압:전력량_겨울철", "파일": "재료_유가전력.json"}, "MO000070": {"원문번호": "임시전력갑:기본요금적용", "파일": "재료_유가전력.json"}, "MO000071": {"원문번호": "임시전력갑:전력량요금적용", "파일": "재료_유가전력.json"}, "MO000072": {"원문번호": "임시전력갑:월간최저요금", "파일": "재료_유가전력.json"}},
|
||||
"키": {
|
||||
"CC000001": {"원문번호": "공통 1-3-1 콘크리트 및 포장용 재료", "파일": "계수_건설품셈_01장_적용기준.json"},
|
||||
@@ -33918,6 +33918,125 @@
|
||||
"MT030597": {"원문번호": "조달청 조경용수목 다년초, 황금무늬실유카, 수관폭15~30cm, 화분18cm", "파일": "재료_자재품목.json"},
|
||||
"MT030598": {"원문번호": "조달청 조경용수목 다년초, 황금무늬실유카, 수관폭20~30cm, 포트17cm", "파일": "재료_자재품목.json"},
|
||||
"MT030599": {"원문번호": "조달청 조경용수목 다년초, 황금무늬실유카, 수관폭20~40cm, 화분12cm", "파일": "재료_자재품목.json"},
|
||||
"MT030600": {"원문번호": "품셈재료 MP000012", "파일": "재료_자재품목.json"},
|
||||
"MT030601": {"원문번호": "품셈재료 MP000038", "파일": "재료_자재품목.json"},
|
||||
"MT030602": {"원문번호": "품셈재료 MP000048", "파일": "재료_자재품목.json"},
|
||||
"MT030603": {"원문번호": "품셈재료 MP000052", "파일": "재료_자재품목.json"},
|
||||
"MT030604": {"원문번호": "품셈재료 MP000055", "파일": "재료_자재품목.json"},
|
||||
"MT030605": {"원문번호": "품셈재료 MP000056", "파일": "재료_자재품목.json"},
|
||||
"MT030606": {"원문번호": "품셈재료 MP000057", "파일": "재료_자재품목.json"},
|
||||
"MT030607": {"원문번호": "품셈재료 MP000060", "파일": "재료_자재품목.json"},
|
||||
"MT030608": {"원문번호": "품셈재료 MP000065", "파일": "재료_자재품목.json"},
|
||||
"MT030609": {"원문번호": "품셈재료 MP000070", "파일": "재료_자재품목.json"},
|
||||
"MT030610": {"원문번호": "품셈재료 MP000076", "파일": "재료_자재품목.json"},
|
||||
"MT030611": {"원문번호": "품셈재료 MP000077", "파일": "재료_자재품목.json"},
|
||||
"MT030612": {"원문번호": "품셈재료 MP000095", "파일": "재료_자재품목.json"},
|
||||
"MT030613": {"원문번호": "품셈재료 MP000096", "파일": "재료_자재품목.json"},
|
||||
"MT030614": {"원문번호": "품셈재료 MP000101", "파일": "재료_자재품목.json"},
|
||||
"MT030615": {"원문번호": "품셈재료 MP000113", "파일": "재료_자재품목.json"},
|
||||
"MT030616": {"원문번호": "품셈재료 MP000129", "파일": "재료_자재품목.json"},
|
||||
"MT030617": {"원문번호": "품셈재료 MP000134", "파일": "재료_자재품목.json"},
|
||||
"MT030618": {"원문번호": "품셈재료 MP000139", "파일": "재료_자재품목.json"},
|
||||
"MT030619": {"원문번호": "품셈재료 MP000144", "파일": "재료_자재품목.json"},
|
||||
"MT030620": {"원문번호": "품셈재료 MP000148", "파일": "재료_자재품목.json"},
|
||||
"MT030621": {"원문번호": "품셈재료 MP000150", "파일": "재료_자재품목.json"},
|
||||
"MT030622": {"원문번호": "품셈재료 MP000169", "파일": "재료_자재품목.json"},
|
||||
"MT030623": {"원문번호": "품셈재료 MP000172", "파일": "재료_자재품목.json"},
|
||||
"MT030624": {"원문번호": "품셈재료 MP000176", "파일": "재료_자재품목.json"},
|
||||
"MT030625": {"원문번호": "품셈재료 MP000177", "파일": "재료_자재품목.json"},
|
||||
"MT030626": {"원문번호": "품셈재료 MP000201", "파일": "재료_자재품목.json"},
|
||||
"MT030627": {"원문번호": "품셈재료 MP000210", "파일": "재료_자재품목.json"},
|
||||
"MT030628": {"원문번호": "품셈재료 MP000211", "파일": "재료_자재품목.json"},
|
||||
"MT030629": {"원문번호": "품셈재료 MP000212", "파일": "재료_자재품목.json"},
|
||||
"MT030630": {"원문번호": "품셈재료 MP000213", "파일": "재료_자재품목.json"},
|
||||
"MT030631": {"원문번호": "품셈재료 MP000216", "파일": "재료_자재품목.json"},
|
||||
"MT030632": {"원문번호": "품셈재료 MP000218", "파일": "재료_자재품목.json"},
|
||||
"MT030633": {"원문번호": "품셈재료 MP000220", "파일": "재료_자재품목.json"},
|
||||
"MT030634": {"원문번호": "품셈재료 MP000221", "파일": "재료_자재품목.json"},
|
||||
"MT030635": {"원문번호": "품셈재료 MP000223", "파일": "재료_자재품목.json"},
|
||||
"MT030636": {"원문번호": "품셈재료 MP000235", "파일": "재료_자재품목.json"},
|
||||
"MT030637": {"원문번호": "품셈재료 MP000239", "파일": "재료_자재품목.json"},
|
||||
"MT030638": {"원문번호": "품셈재료 MP000240", "파일": "재료_자재품목.json"},
|
||||
"MT030639": {"원문번호": "품셈재료 MP000241", "파일": "재료_자재품목.json"},
|
||||
"MT030640": {"원문번호": "품셈재료 MP000242", "파일": "재료_자재품목.json"},
|
||||
"MT030641": {"원문번호": "품셈재료 MP000250", "파일": "재료_자재품목.json"},
|
||||
"MT030642": {"원문번호": "품셈재료 MP000253", "파일": "재료_자재품목.json"},
|
||||
"MT030643": {"원문번호": "품셈재료 MP000254", "파일": "재료_자재품목.json"},
|
||||
"MT030644": {"원문번호": "품셈재료 MP000255", "파일": "재료_자재품목.json"},
|
||||
"MT030645": {"원문번호": "품셈재료 MP000257", "파일": "재료_자재품목.json"},
|
||||
"MT030646": {"원문번호": "품셈재료 MP000258", "파일": "재료_자재품목.json"},
|
||||
"MT030647": {"원문번호": "품셈재료 MP000259", "파일": "재료_자재품목.json"},
|
||||
"MT030648": {"원문번호": "품셈재료 MP000260", "파일": "재료_자재품목.json"},
|
||||
"MT030649": {"원문번호": "품셈재료 MP000261", "파일": "재료_자재품목.json"},
|
||||
"MT030650": {"원문번호": "품셈재료 MP000262", "파일": "재료_자재품목.json"},
|
||||
"MT030651": {"원문번호": "품셈재료 MP000265", "파일": "재료_자재품목.json"},
|
||||
"MT030652": {"원문번호": "품셈재료 MP000269", "파일": "재료_자재품목.json"},
|
||||
"MT030653": {"원문번호": "품셈재료 MP000271", "파일": "재료_자재품목.json"},
|
||||
"MT030654": {"원문번호": "품셈재료 MP000272", "파일": "재료_자재품목.json"},
|
||||
"MT030655": {"원문번호": "품셈재료 MP000273", "파일": "재료_자재품목.json"},
|
||||
"MT030656": {"원문번호": "품셈재료 MP000274", "파일": "재료_자재품목.json"},
|
||||
"MT030657": {"원문번호": "품셈재료 MP000275", "파일": "재료_자재품목.json"},
|
||||
"MT030658": {"원문번호": "품셈재료 MP000276", "파일": "재료_자재품목.json"},
|
||||
"MT030659": {"원문번호": "품셈재료 MP000280", "파일": "재료_자재품목.json"},
|
||||
"MT030660": {"원문번호": "품셈재료 MP000281", "파일": "재료_자재품목.json"},
|
||||
"MT030661": {"원문번호": "품셈재료 MP000286", "파일": "재료_자재품목.json"},
|
||||
"MT030662": {"원문번호": "품셈재료 MP000287", "파일": "재료_자재품목.json"},
|
||||
"MT030663": {"원문번호": "품셈재료 MP000288", "파일": "재료_자재품목.json"},
|
||||
"MT030664": {"원문번호": "품셈재료 MP000290", "파일": "재료_자재품목.json"},
|
||||
"MT030665": {"원문번호": "품셈재료 MP000293", "파일": "재료_자재품목.json"},
|
||||
"MT030666": {"원문번호": "품셈재료 MP000294", "파일": "재료_자재품목.json"},
|
||||
"MT030667": {"원문번호": "품셈재료 MP000295", "파일": "재료_자재품목.json"},
|
||||
"MT030668": {"원문번호": "품셈재료 MP000296", "파일": "재료_자재품목.json"},
|
||||
"MT030669": {"원문번호": "품셈재료 MP000297", "파일": "재료_자재품목.json"},
|
||||
"MT030670": {"원문번호": "품셈재료 MP000298", "파일": "재료_자재품목.json"},
|
||||
"MT030671": {"원문번호": "품셈재료 MP000299", "파일": "재료_자재품목.json"},
|
||||
"MT030672": {"원문번호": "품셈재료 MP000301", "파일": "재료_자재품목.json"},
|
||||
"MT030673": {"원문번호": "품셈재료 MP000302", "파일": "재료_자재품목.json"},
|
||||
"MT030674": {"원문번호": "품셈재료 MP000303", "파일": "재료_자재품목.json"},
|
||||
"MT030675": {"원문번호": "품셈재료 MP000304", "파일": "재료_자재품목.json"},
|
||||
"MT030676": {"원문번호": "품셈재료 MP000305", "파일": "재료_자재품목.json"},
|
||||
"MT030677": {"원문번호": "품셈재료 MP000306", "파일": "재료_자재품목.json"},
|
||||
"MT030678": {"원문번호": "품셈재료 MP000308", "파일": "재료_자재품목.json"},
|
||||
"MT030679": {"원문번호": "품셈재료 MP000309", "파일": "재료_자재품목.json"},
|
||||
"MT030680": {"원문번호": "품셈재료 MP000310", "파일": "재료_자재품목.json"},
|
||||
"MT030681": {"원문번호": "품셈재료 MP000314", "파일": "재료_자재품목.json"},
|
||||
"MT030682": {"원문번호": "품셈재료 MP000315", "파일": "재료_자재품목.json"},
|
||||
"MT030683": {"원문번호": "품셈재료 MP000319", "파일": "재료_자재품목.json"},
|
||||
"MT030684": {"원문번호": "품셈재료 MP000325", "파일": "재료_자재품목.json"},
|
||||
"MT030685": {"원문번호": "품셈재료 MP000326", "파일": "재료_자재품목.json"},
|
||||
"MT030686": {"원문번호": "품셈재료 MP000327", "파일": "재료_자재품목.json"},
|
||||
"MT030687": {"원문번호": "품셈재료 MP000328", "파일": "재료_자재품목.json"},
|
||||
"MT030688": {"원문번호": "품셈재료 MP000330", "파일": "재료_자재품목.json"},
|
||||
"MT030689": {"원문번호": "품셈재료 MP000334", "파일": "재료_자재품목.json"},
|
||||
"MT030690": {"원문번호": "품셈재료 MP000335", "파일": "재료_자재품목.json"},
|
||||
"MT030691": {"원문번호": "품셈재료 MP000336", "파일": "재료_자재품목.json"},
|
||||
"MT030692": {"원문번호": "품셈재료 MP000337", "파일": "재료_자재품목.json"},
|
||||
"MT030693": {"원문번호": "품셈재료 MP000338", "파일": "재료_자재품목.json"},
|
||||
"MT030694": {"원문번호": "품셈재료 MP000340", "파일": "재료_자재품목.json"},
|
||||
"MT030695": {"원문번호": "품셈재료 MP000341", "파일": "재료_자재품목.json"},
|
||||
"MT030696": {"원문번호": "품셈재료 MP000342", "파일": "재료_자재품목.json"},
|
||||
"MT030697": {"원문번호": "품셈재료 MP000343", "파일": "재료_자재품목.json"},
|
||||
"MT030698": {"원문번호": "품셈재료 MP000344", "파일": "재료_자재품목.json"},
|
||||
"MT030699": {"원문번호": "품셈재료 MP000345", "파일": "재료_자재품목.json"},
|
||||
"MT030700": {"원문번호": "품셈재료 MP000349", "파일": "재료_자재품목.json"},
|
||||
"MT030701": {"원문번호": "품셈재료 MP000351", "파일": "재료_자재품목.json"},
|
||||
"MT030702": {"원문번호": "품셈재료 MP000352", "파일": "재료_자재품목.json"},
|
||||
"MT030703": {"원문번호": "품셈재료 MP000353", "파일": "재료_자재품목.json"},
|
||||
"MT030704": {"원문번호": "품셈재료 MP000359", "파일": "재료_자재품목.json"},
|
||||
"MT030705": {"원문번호": "품셈재료 MP000363", "파일": "재료_자재품목.json"},
|
||||
"MT030706": {"원문번호": "품셈재료 MP000003 전국", "파일": "재료_자재품목.json"},
|
||||
"MT030707": {"원문번호": "품셈재료 MP000003 지역별", "파일": "재료_자재품목.json"},
|
||||
"MT030708": {"원문번호": "품셈재료 MP000005 전국", "파일": "재료_자재품목.json"},
|
||||
"MT030709": {"원문번호": "품셈재료 MP000005 지역별", "파일": "재료_자재품목.json"},
|
||||
"MT030710": {"원문번호": "품셈재료 MP000209", "파일": "재료_자재품목.json"},
|
||||
"MT030711": {"원문번호": "품셈재료 MP000214", "파일": "재료_자재품목.json"},
|
||||
"MT030712": {"원문번호": "품셈재료 MP000040", "파일": "재료_자재품목.json"},
|
||||
"MT030713": {"원문번호": "품셈재료 MP000094", "파일": "재료_자재품목.json"},
|
||||
"MT030714": {"원문번호": "품셈재료 MP000202", "파일": "재료_자재품목.json"},
|
||||
"MT030715": {"원문번호": "품셈재료 MP000205", "파일": "재료_자재품목.json"},
|
||||
"MT030716": {"원문번호": "품셈재료 MP000206", "파일": "재료_자재품목.json"},
|
||||
"MT030717": {"원문번호": "품셈재료 MP000207", "파일": "재료_자재품목.json"},
|
||||
"MT030718": {"원문번호": "품셈재료 MP000208", "파일": "재료_자재품목.json"},
|
||||
"QC000001": {"원문번호": "공통 2-1-1 현장사무소 등의 규모(토목)", "파일": "소요량_건설품셈_02장_가설공사.json"},
|
||||
"QC000002": {"원문번호": "공통 2-1-2 현장사무소 등의 규모(건축 및 기계설비)", "파일": "소요량_건설품셈_02장_가설공사.json"},
|
||||
"QC000003": {"원문번호": "공통 2-1-2 시멘트 창고, 동력소 및 변전소 필요면적", "파일": "소요량_건설품셈_02장_가설공사.json"},
|
||||
@@ -36473,124 +36592,29 @@
|
||||
"RT000018": {"원문번호": "rate_profit", "파일": "요율_조달청제비율.json"},
|
||||
"RT000019": {"원문번호": "rate_vat", "파일": "요율_조달청제비율.json"},
|
||||
"UA000001": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"MT030600": {"원문번호": "품셈재료 MP000012", "파일": "재료_자재품목.json"},
|
||||
"MT030601": {"원문번호": "품셈재료 MP000038", "파일": "재료_자재품목.json"},
|
||||
"MT030602": {"원문번호": "품셈재료 MP000048", "파일": "재료_자재품목.json"},
|
||||
"MT030603": {"원문번호": "품셈재료 MP000052", "파일": "재료_자재품목.json"},
|
||||
"MT030604": {"원문번호": "품셈재료 MP000055", "파일": "재료_자재품목.json"},
|
||||
"MT030605": {"원문번호": "품셈재료 MP000056", "파일": "재료_자재품목.json"},
|
||||
"MT030606": {"원문번호": "품셈재료 MP000057", "파일": "재료_자재품목.json"},
|
||||
"MT030607": {"원문번호": "품셈재료 MP000060", "파일": "재료_자재품목.json"},
|
||||
"MT030608": {"원문번호": "품셈재료 MP000065", "파일": "재료_자재품목.json"},
|
||||
"MT030609": {"원문번호": "품셈재료 MP000070", "파일": "재료_자재품목.json"},
|
||||
"MT030610": {"원문번호": "품셈재료 MP000076", "파일": "재료_자재품목.json"},
|
||||
"MT030611": {"원문번호": "품셈재료 MP000077", "파일": "재료_자재품목.json"},
|
||||
"MT030612": {"원문번호": "품셈재료 MP000095", "파일": "재료_자재품목.json"},
|
||||
"MT030613": {"원문번호": "품셈재료 MP000096", "파일": "재료_자재품목.json"},
|
||||
"MT030614": {"원문번호": "품셈재료 MP000101", "파일": "재료_자재품목.json"},
|
||||
"MT030615": {"원문번호": "품셈재료 MP000113", "파일": "재료_자재품목.json"},
|
||||
"MT030616": {"원문번호": "품셈재료 MP000129", "파일": "재료_자재품목.json"},
|
||||
"MT030617": {"원문번호": "품셈재료 MP000134", "파일": "재료_자재품목.json"},
|
||||
"MT030618": {"원문번호": "품셈재료 MP000139", "파일": "재료_자재품목.json"},
|
||||
"MT030619": {"원문번호": "품셈재료 MP000144", "파일": "재료_자재품목.json"},
|
||||
"MT030620": {"원문번호": "품셈재료 MP000148", "파일": "재료_자재품목.json"},
|
||||
"MT030621": {"원문번호": "품셈재료 MP000150", "파일": "재료_자재품목.json"},
|
||||
"MT030622": {"원문번호": "품셈재료 MP000169", "파일": "재료_자재품목.json"},
|
||||
"MT030623": {"원문번호": "품셈재료 MP000172", "파일": "재료_자재품목.json"},
|
||||
"MT030624": {"원문번호": "품셈재료 MP000176", "파일": "재료_자재품목.json"},
|
||||
"MT030625": {"원문번호": "품셈재료 MP000177", "파일": "재료_자재품목.json"},
|
||||
"MT030626": {"원문번호": "품셈재료 MP000201", "파일": "재료_자재품목.json"},
|
||||
"MT030627": {"원문번호": "품셈재료 MP000210", "파일": "재료_자재품목.json"},
|
||||
"MT030628": {"원문번호": "품셈재료 MP000211", "파일": "재료_자재품목.json"},
|
||||
"MT030629": {"원문번호": "품셈재료 MP000212", "파일": "재료_자재품목.json"},
|
||||
"MT030630": {"원문번호": "품셈재료 MP000213", "파일": "재료_자재품목.json"},
|
||||
"MT030631": {"원문번호": "품셈재료 MP000216", "파일": "재료_자재품목.json"},
|
||||
"MT030632": {"원문번호": "품셈재료 MP000218", "파일": "재료_자재품목.json"},
|
||||
"MT030633": {"원문번호": "품셈재료 MP000220", "파일": "재료_자재품목.json"},
|
||||
"MT030634": {"원문번호": "품셈재료 MP000221", "파일": "재료_자재품목.json"},
|
||||
"MT030635": {"원문번호": "품셈재료 MP000223", "파일": "재료_자재품목.json"},
|
||||
"MT030636": {"원문번호": "품셈재료 MP000235", "파일": "재료_자재품목.json"},
|
||||
"MT030637": {"원문번호": "품셈재료 MP000239", "파일": "재료_자재품목.json"},
|
||||
"MT030638": {"원문번호": "품셈재료 MP000240", "파일": "재료_자재품목.json"},
|
||||
"MT030639": {"원문번호": "품셈재료 MP000241", "파일": "재료_자재품목.json"},
|
||||
"MT030640": {"원문번호": "품셈재료 MP000242", "파일": "재료_자재품목.json"},
|
||||
"MT030641": {"원문번호": "품셈재료 MP000250", "파일": "재료_자재품목.json"},
|
||||
"MT030642": {"원문번호": "품셈재료 MP000253", "파일": "재료_자재품목.json"},
|
||||
"MT030643": {"원문번호": "품셈재료 MP000254", "파일": "재료_자재품목.json"},
|
||||
"MT030644": {"원문번호": "품셈재료 MP000255", "파일": "재료_자재품목.json"},
|
||||
"MT030645": {"원문번호": "품셈재료 MP000257", "파일": "재료_자재품목.json"},
|
||||
"MT030646": {"원문번호": "품셈재료 MP000258", "파일": "재료_자재품목.json"},
|
||||
"MT030647": {"원문번호": "품셈재료 MP000259", "파일": "재료_자재품목.json"},
|
||||
"MT030648": {"원문번호": "품셈재료 MP000260", "파일": "재료_자재품목.json"},
|
||||
"MT030649": {"원문번호": "품셈재료 MP000261", "파일": "재료_자재품목.json"},
|
||||
"MT030650": {"원문번호": "품셈재료 MP000262", "파일": "재료_자재품목.json"},
|
||||
"MT030651": {"원문번호": "품셈재료 MP000265", "파일": "재료_자재품목.json"},
|
||||
"MT030652": {"원문번호": "품셈재료 MP000269", "파일": "재료_자재품목.json"},
|
||||
"MT030653": {"원문번호": "품셈재료 MP000271", "파일": "재료_자재품목.json"},
|
||||
"MT030654": {"원문번호": "품셈재료 MP000272", "파일": "재료_자재품목.json"},
|
||||
"MT030655": {"원문번호": "품셈재료 MP000273", "파일": "재료_자재품목.json"},
|
||||
"MT030656": {"원문번호": "품셈재료 MP000274", "파일": "재료_자재품목.json"},
|
||||
"MT030657": {"원문번호": "품셈재료 MP000275", "파일": "재료_자재품목.json"},
|
||||
"MT030658": {"원문번호": "품셈재료 MP000276", "파일": "재료_자재품목.json"},
|
||||
"MT030659": {"원문번호": "품셈재료 MP000280", "파일": "재료_자재품목.json"},
|
||||
"MT030660": {"원문번호": "품셈재료 MP000281", "파일": "재료_자재품목.json"},
|
||||
"MT030661": {"원문번호": "품셈재료 MP000286", "파일": "재료_자재품목.json"},
|
||||
"MT030662": {"원문번호": "품셈재료 MP000287", "파일": "재료_자재품목.json"},
|
||||
"MT030663": {"원문번호": "품셈재료 MP000288", "파일": "재료_자재품목.json"},
|
||||
"MT030664": {"원문번호": "품셈재료 MP000290", "파일": "재료_자재품목.json"},
|
||||
"MT030665": {"원문번호": "품셈재료 MP000293", "파일": "재료_자재품목.json"},
|
||||
"MT030666": {"원문번호": "품셈재료 MP000294", "파일": "재료_자재품목.json"},
|
||||
"MT030667": {"원문번호": "품셈재료 MP000295", "파일": "재료_자재품목.json"},
|
||||
"MT030668": {"원문번호": "품셈재료 MP000296", "파일": "재료_자재품목.json"},
|
||||
"MT030669": {"원문번호": "품셈재료 MP000297", "파일": "재료_자재품목.json"},
|
||||
"MT030670": {"원문번호": "품셈재료 MP000298", "파일": "재료_자재품목.json"},
|
||||
"MT030671": {"원문번호": "품셈재료 MP000299", "파일": "재료_자재품목.json"},
|
||||
"MT030672": {"원문번호": "품셈재료 MP000301", "파일": "재료_자재품목.json"},
|
||||
"MT030673": {"원문번호": "품셈재료 MP000302", "파일": "재료_자재품목.json"},
|
||||
"MT030674": {"원문번호": "품셈재료 MP000303", "파일": "재료_자재품목.json"},
|
||||
"MT030675": {"원문번호": "품셈재료 MP000304", "파일": "재료_자재품목.json"},
|
||||
"MT030676": {"원문번호": "품셈재료 MP000305", "파일": "재료_자재품목.json"},
|
||||
"MT030677": {"원문번호": "품셈재료 MP000306", "파일": "재료_자재품목.json"},
|
||||
"MT030678": {"원문번호": "품셈재료 MP000308", "파일": "재료_자재품목.json"},
|
||||
"MT030679": {"원문번호": "품셈재료 MP000309", "파일": "재료_자재품목.json"},
|
||||
"MT030680": {"원문번호": "품셈재료 MP000310", "파일": "재료_자재품목.json"},
|
||||
"MT030681": {"원문번호": "품셈재료 MP000314", "파일": "재료_자재품목.json"},
|
||||
"MT030682": {"원문번호": "품셈재료 MP000315", "파일": "재료_자재품목.json"},
|
||||
"MT030683": {"원문번호": "품셈재료 MP000319", "파일": "재료_자재품목.json"},
|
||||
"MT030684": {"원문번호": "품셈재료 MP000325", "파일": "재료_자재품목.json"},
|
||||
"MT030685": {"원문번호": "품셈재료 MP000326", "파일": "재료_자재품목.json"},
|
||||
"MT030686": {"원문번호": "품셈재료 MP000327", "파일": "재료_자재품목.json"},
|
||||
"MT030687": {"원문번호": "품셈재료 MP000328", "파일": "재료_자재품목.json"},
|
||||
"MT030688": {"원문번호": "품셈재료 MP000330", "파일": "재료_자재품목.json"},
|
||||
"MT030689": {"원문번호": "품셈재료 MP000334", "파일": "재료_자재품목.json"},
|
||||
"MT030690": {"원문번호": "품셈재료 MP000335", "파일": "재료_자재품목.json"},
|
||||
"MT030691": {"원문번호": "품셈재료 MP000336", "파일": "재료_자재품목.json"},
|
||||
"MT030692": {"원문번호": "품셈재료 MP000337", "파일": "재료_자재품목.json"},
|
||||
"MT030693": {"원문번호": "품셈재료 MP000338", "파일": "재료_자재품목.json"},
|
||||
"MT030694": {"원문번호": "품셈재료 MP000340", "파일": "재료_자재품목.json"},
|
||||
"MT030695": {"원문번호": "품셈재료 MP000341", "파일": "재료_자재품목.json"},
|
||||
"MT030696": {"원문번호": "품셈재료 MP000342", "파일": "재료_자재품목.json"},
|
||||
"MT030697": {"원문번호": "품셈재료 MP000343", "파일": "재료_자재품목.json"},
|
||||
"MT030698": {"원문번호": "품셈재료 MP000344", "파일": "재료_자재품목.json"},
|
||||
"MT030699": {"원문번호": "품셈재료 MP000345", "파일": "재료_자재품목.json"},
|
||||
"MT030700": {"원문번호": "품셈재료 MP000349", "파일": "재료_자재품목.json"},
|
||||
"MT030701": {"원문번호": "품셈재료 MP000351", "파일": "재료_자재품목.json"},
|
||||
"MT030702": {"원문번호": "품셈재료 MP000352", "파일": "재료_자재품목.json"},
|
||||
"MT030703": {"원문번호": "품셈재료 MP000353", "파일": "재료_자재품목.json"},
|
||||
"MT030704": {"원문번호": "품셈재료 MP000359", "파일": "재료_자재품목.json"},
|
||||
"MT030705": {"원문번호": "품셈재료 MP000363", "파일": "재료_자재품목.json"},
|
||||
"MT030706": {"원문번호": "품셈재료 MP000003 전국", "파일": "재료_자재품목.json"},
|
||||
"MT030707": {"원문번호": "품셈재료 MP000003 지역별", "파일": "재료_자재품목.json"},
|
||||
"MT030708": {"원문번호": "품셈재료 MP000005 전국", "파일": "재료_자재품목.json"},
|
||||
"MT030709": {"원문번호": "품셈재료 MP000005 지역별", "파일": "재료_자재품목.json"},
|
||||
"MT030710": {"원문번호": "품셈재료 MP000209", "파일": "재료_자재품목.json"},
|
||||
"MT030711": {"원문번호": "품셈재료 MP000214", "파일": "재료_자재품목.json"},
|
||||
"MT030712": {"원문번호": "품셈재료 MP000040", "파일": "재료_자재품목.json"},
|
||||
"MT030713": {"원문번호": "품셈재료 MP000094", "파일": "재료_자재품목.json"},
|
||||
"MT030714": {"원문번호": "품셈재료 MP000202", "파일": "재료_자재품목.json"},
|
||||
"MT030715": {"원문번호": "품셈재료 MP000205", "파일": "재료_자재품목.json"},
|
||||
"MT030716": {"원문번호": "품셈재료 MP000206", "파일": "재료_자재품목.json"},
|
||||
"MT030717": {"원문번호": "품셈재료 MP000207", "파일": "재료_자재품목.json"},
|
||||
"MT030718": {"원문번호": "품셈재료 MP000208", "파일": "재료_자재품목.json"}
|
||||
"UA000002": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000003": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000004": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000005": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000006": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000007": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000008": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000009": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000010": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000011": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000012": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000013": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000014": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000015": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000016": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000017": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000018": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000019": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000020": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000021": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000022": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000023": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000024": {"원문번호": "", "파일": "일위대가조합.json"},
|
||||
"UA000025": {"원문번호": "", "파일": "일위대가조합.json"}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
- **원문번호 칸 없음** — 원문(ESTX)이 준 품번 `M0005250505` 는 남의 번호라 이름 몫을 못 함 · 대조는 `_키대장.json` 에만 둠(조달청에서 새로 더한 줄의 조달청 번호도 같음). 로직·연결은 키로 적음.
|
||||
- 유류·전력은 뗌 — 유가·전기요금은 `재료_유가전력`(유류는 값이 비고 글뿐 · 중유는 받지 않음).
|
||||
- `재료_유가전력` · `환율_한국은행환율` 도 열 한 벌 — 키 · 원문번호 · 구분 · 상세구분 · 이름 · 규격 · 단위 · 값 · 출처 · 비고. 유가전력 `구분` = 유류 · 전력 · `상세구분` = 유류(지역 이름) · 전력(계약종별) · `규격` = 기준일(유류 수집일 · 전력 시행일). 환율은 `규격` = 기준일 · 구분·상세구분 비움.
|
||||
- 로직 호표가 자재품목 키(`MT…`)를 바로 가리키면 그 줄의 값 열 가운데 낮은 값 — **시험 계산용**.
|
||||
- 재료 줄 단가 = 고른 품목(없으면 자동값 줄)의 값 열 가운데 낮은 값 — **시험 계산용**.
|
||||
- `재료_품셈재료`(PLAN 7-4b 뒤 절반 — `old/` 로 옮김 · 살아 있는 마스터엔 안 읽힘 · 자료로만 둠) = 품셈이 부르는 재료 목록이던 것 — `{ 키, 원문번호, 이름, 규격, 단위, 요구절: [출처 모양], 연결, 고르기 }` · 값 칸 없음. 로직 호표·덧줄의 `MP…` 직접 참조는 다 걷어 자재품목 고르기 조건(7장)이나 대표 키로 바꿈 — 아래 두 줄은 그 옮기던 때의 자취.
|
||||
- `연결` = 자재품목·유가전력 키 하나 · 후보 조건 `{ 이름, 규격, 지역: "입력" }`(지역이 갈리는 레미콘·골재·아스콘) · `{ 이름, 계약종별: "입력" }`(전력 — 로직 입력 `전력 계약종별`) · `null`(못 이음). 이 지역·계약종별 조건 묶음은 이제 7장의 고르기 조건(`PICK_KEYS` 의 `지역`·`계약종별`)에 그대로 들어가 호표에 바로 박힘 — 품셈재료 간접 없이 `master_material._region`·`_tariff` 가 품.
|
||||
- 단가 = 연결된 줄의 값 열 다섯 가운데 낮은 값 · 호표 결과 줄에 `출처`(`<키>.<열>`) — **시험 계산용**. 후보 조건 = 품명 뿌리가 `이름` 으로 끝남 · 이름에 로직 입력 `자재지역` 글자 · 단위 같음 · 규격이 둘 이상 갈리면 멈춤 「후보 여럿」 · 그 지역 줄이 없으면 가까운 시군을 고르지 않고 멈춤 「지역 값 없음」.
|
||||
@@ -209,7 +209,7 @@
|
||||
```json
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": { "구분": "관·수로", "상세구분": "흄관", "규격": "D300", "대표": "MT012345" },
|
||||
"요소": { "구분": "관·수로", "상세구분": "흄관", "검색어": "흄관 D300" },
|
||||
"이름": "흄관",
|
||||
"단위": "m",
|
||||
"수량": "1",
|
||||
@@ -218,17 +218,20 @@
|
||||
}
|
||||
```
|
||||
|
||||
- 조건 칸 = `구분`(반드시 — `지역`·`계약종별` 조건은 예외) · `상세구분` · `이름` · `규격` · `대표` · `지역`("입력" 만) · `계약종별`("입력" 만) · 그 밖의 칸은 두지 않음. `종류` 는 「재료」만.
|
||||
- 조건 칸 = `검색어` · `구분` · `상세구분` · `이름` · `규격` · `지역`("입력" 만) · `계약종별`("입력" 만) · 그 밖의 칸은 두지 않음. `대표` 칸은 두지 않음. `종류` 는 「재료」만.
|
||||
- `지역`·`계약종별` 조건(PLAN 7-4b 뒤 절반) = 구분·상세구분 없이 `이름` 하나로 자재품목(지역)·유가전력(계약종별)을 넓게 찾음 — 옛 품셈재료 `연결` 묶음과 같은 모양·같은 매칭(`master_material._region`·`_tariff`) · 로직 입력 `자재지역`·`전력 계약종별` 이 계산 때 좁힘 · 값은 `null`(지역·전력 마스터에 안 굳힘).
|
||||
- 구분·상세구분 = 자재품목 줄의 같은 이름 칸과 똑같이 적음(묶음 목록은 `ref/_묶음_자재품목.md`).
|
||||
- 규격 조건 = 원문이 규격을 밝힌 것만 좁힘 — 낱말이 **모두** 든 줄이 후보(「D300」 · 「D300 B종」) · 대소문자·빈칸·`φ` 는 안 가림. 원문이 규격을 안 밝히면 칸을 두지 않음.
|
||||
- 이름 조건 = 구분·상세구분·규격이 같아도 품명이 갈리는 자재품목을 가를 때만 둠(예: 파형강관 커플링밴드 vs 접속티 — 둘 다 관경만 규격에 있어 규격 조건으로는 안 갈림). 규격과 같은 방식(낱말이 **모두** 이름에 든 줄) — 대소문자·빈칸은 안 가림.
|
||||
- 대표 줄 = 관리자가 고른 자재품목 키 — 시험 계산이 그 줄의 값을 씀. 조건 밖 키는 검사가 잡음.
|
||||
- 후보는 **호표 줄 단위와 같은 자재품목 줄만** — 단위가 다르면 다른 물건(우드칩 ㎥ · 원목후로링 ㎡). 조건 안 줄이 모두 다른 단위면 `check_master 단위` 가 알림.
|
||||
- **키를 못 박음** — 재료 줄은 늘 요소 묶음 + `검색어`(「합판(내수) 12t 서울」) · 줄에 자재품목 키를 바로 적지 않음 · 재료 입력에 키 목록(`고르기`)을 두지 않음.
|
||||
- 찾기 = 검색어 낱말(띄어쓰기로 나눔)이 **모두** 이름 또는 규격에 든 자재품목 줄 · 요소에 `구분` · `상세구분` 이 있으면 그 안에서만 · 단위는 같은 성격까지(아래 환산).
|
||||
- 자동값 차례 = 값 있는 줄 → 줄 단위와 똑같은 단위 → 검색어 안 지역 낱말(서울 등)과 맞는 줄 → 키 차례 · 그 첫째.
|
||||
- 고른 품목 = 계산 요청에만 실음(`_화면_계약.md` 10장 `고름`) · 로직 파일에 저장하지 않음 · 폐기 품목은 지움.
|
||||
- 단위 환산 = 같은 성격만 — 무게 g · kg · ton(톤 · Ton) / 길이 mm · cm · m · km / 넓이 ㎠ · ㎡ / 부피 ㎤ · ℓ · ㎥ · 단가를 줄 단위로 바꿔 계산.
|
||||
- 단위 경고 = 성격이 다르면 막지 않음 — 계산은 하되 `/calc` 줄에 `단위경고` · `check_master` 가 검색어 후보 0 · 단위 경고 줄을 셈.
|
||||
- 조건 값에 `{입력}` 을 넣을 수 있음 — `"규격": "{관경}"` 은 계산 때 입력값으로 채워짐(검사에서는 그 칸을 뺌).
|
||||
- 대표가 없으면 시험 계산은 **조건 안 값 있는 첫 줄** · 로직 입력 `자재지역` 이 있으면 이름에 그 지역이 든 줄부터(레미콘·골재처럼 지역이 갈리는 품목).
|
||||
- 실제 줄 고르기는 프로젝트 몫 — 후보 목록은 `GET /api/m01/materials`(화면·테스트 컨테이너가 같이 씀).
|
||||
- 옛 방식(자재품목 키를 호표에 바로 적은 `MT…`)도 그대로 돎 — 그 줄은 그 자재품목 줄 값을 바로 씀. 품셈재료 간접(`MP…`)은 PLAN 7-4b 뒤 절반에서 다 걷음(위 4장) — 남았던 22줄(색소·에폭시·버너·퓨즈 등)도 자재품목 새 줄이나 대표 키로 이음.
|
||||
- 기계 = 입력값에 원문 분류번호(목록 밖 번호도 기계 마스터에 있으면 받음) · 기본값 = 원문 분류번호 목록 첫째 · 인력은 고르지 않음(지정값).
|
||||
|
||||
- 근거가 덜 선 줄은 `비고` 칸에 적음 — 「환산 근거 미확인」.
|
||||
- 입력 칸 = `이름`(반드시) · `설명` · `단위` · `고르기`·`범위`(둘 중 하나까지) · `출처`(원문 절) · `비고` · `시험입력`은 줄 머리에 따로.
|
||||
@@ -241,7 +244,7 @@
|
||||
## 8. 변수 · 식
|
||||
|
||||
- 요소 — 키 `LB000123` · 값 묶음은 `.칸` — `EQ000045.주연료`.
|
||||
- 입력·중간 값으로 요소를 고르면 `ID:원문번호` 에 `{이름}` 을 끼움 — `EQ:{기계}.가격` · `MO:경유:{지역}` · `LB:{조종원직종}`. 끼운 값이 키여도 됨 — `MT:{철선}` 의 고르기는 키 목록. 번호가 겹치는 표·로직은 `ID:원문번호` 로 못 부름 — 키로 적음.
|
||||
- 입력·중간 값으로 요소를 고르면 `ID:원문번호` 에 `{이름}` 을 끼움 — `EQ:{기계}.가격` · `MO:경유:{지역}` · `LB:{조종원직종}`. 재료는 `MT:{…}` 로 고르지 않음 — 7장 검색어. 번호가 겹치는 표·로직은 `ID:원문번호` 로 못 부름 — 키로 적음.
|
||||
- 표 찾기 — `찾기(QF000041, 뒷길이=뒷길이, 돌=돌, 쌓기=쌓기).석공`
|
||||
- 맞는 줄이 여럿이면 찾는 값 칸이 있는 줄만 봄. 값 칸·조건 이름에 괄호·빈칸이 있으면 작은따옴표 — `.'시공량(m)'` · `'규준틀 높이'=높이`
|
||||
- 찾기·로직 인자의 글자 값은 작은따옴표 — `구분='V(다짐속도,km/hr)'` · 따옴표 없으면 이름(입력·중간).
|
||||
|
||||
@@ -47,9 +47,9 @@
|
||||
|
||||
## 3. 시험 계산
|
||||
|
||||
`POST /calc` 받음 `{key, inputs: {이름: 값}, row?, file?}` — 새 로직은 `key` ""
|
||||
`POST /calc` 받음 `{key, inputs: {이름: 값}, row?, file?, 고름?}` — 새 로직은 `key` "" · `고름` 은 10장
|
||||
|
||||
- 됨 — `{ok: true, lines: [{이름, 단위, 수량, 단가, 금액, 비목: {비목: 금액}, 출처?}], sums: {노무비, 재료비, 경비, 계}, middle: {이름: 값}}` · `출처` = 품셈재료 줄의 단가 칸
|
||||
- 됨 — `{ok: true, lines: [{이름, 단위, 수량, 단가, 금액, 비목: {비목: 금액}, 출처?, 단위경고?}], sums: {노무비, 재료비, 경비, 계}, middle: {이름: 값}}` · `출처` = 재료 줄의 단가 칸 · `단위경고` 는 10장
|
||||
- 돈 아닌 로직 — `{ok: true, result, middle}`
|
||||
- 멈춤 — `{ok: false, reason}` (200). 까닭 = 엔진 글 그대로(「입력 「돌」 없음」 · 「값 없음 — 관리자가 채울 값」 …).
|
||||
- `row` 없음 = 저장된 파일로 계산 · `row` 있음 = 그 로직만 고친 줄로 바꿔(없던 로직이면 `file` 에 더해) 메모리에서 계산 — 파일에 안 씀. 요소 값은 늘 저장된 파일.
|
||||
@@ -192,3 +192,18 @@
|
||||
- `줄` = `{차례, 로직, 메모, 이름, 원문번호, 결과단위, 구분, 상세구분, 출처, ok, 갈래, 노무비, 재료비, 경비, 계}` · 돈이 아닌 로직은 `갈래` 「결과」 와 `결과` 수 하나(비목이 없어 합계에 안 얹음).
|
||||
- 못 도는 줄은 통째로 멈추지 않고 그 줄만 `ok` false · `까닭` · `멈춤` 에 한 줄 — 나머지 줄의 합계는 그대로 냄.
|
||||
- 답 — 200 · 400(틀린 몸 · 이름 없음 · 모르는 소유) · 404(없는 키) · 409(낡은 판본) · 422 `{detail: {errors: […]}}`(검사 걸림 — 없는 로직 키 · 같은 로직 두 번 · 빈 조합 · 조합을 담음).
|
||||
|
||||
## 10. 재료 · 기계 고르기
|
||||
|
||||
재료 줄은 키 대신 기본 검색어로 품목을 찾음 — 파일 모양 · 찾기 규칙 · 자동값 차례 · 단위 환산은 `_틀.md` 7장.
|
||||
|
||||
| 무엇 | 모양 |
|
||||
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 고른 품목 | `/calc` · `/text` 몸에 `"고름": {"<호표 줄 차례>": "MT000019"}` · 저장 안 함 · 기계는 그 입력값에 원문 분류번호(목록 밖 번호도 기계 마스터에 있으면 받음) |
|
||||
| 단위 경고 | 성격이 다르면 계산은 하되 `/calc` 줄에 `"단위경고": "줄 m · 자재 Ton"` · 화면은 그 줄에 딱지 |
|
||||
| 모달 검색 | 재료 = 자재품목 표 · 검색 칸 = 검색어 첫 낱말(예 「합판」) · 잡힌 품목 칠함 / 기계 = 기계 표 · 검색 칸 = 기계 이름(예 「크레인(타이어)」) |
|
||||
|
||||
- `고름` 이 없거나 그 줄에 없으면 자동값(`_틀.md` 7장 차례의 첫째).
|
||||
- 고르면 다시 계산 · 고른 품목은 파일에 저장하지 않고 계산 요청에만 실음.
|
||||
- 드롭다운 고르기는 걷음 — 재료 줄 · 기계 입력의 「품목 고르기」 가 모달을 검색 칸 채워 엶(재료 컨테이너 표 재사용).
|
||||
- 인력은 고르지 않음(지정값).
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""로직 재료 줄의 자재품목 키를 「기본 검색어」 로 바꿈 (PLAN 8-3 · 계약 8-1).
|
||||
|
||||
· 대표 키 박은 줄 · 대표가 재료 입력(`{철선}`)인 줄 · 조건만 있는 줄 — 요소를
|
||||
`{구분, 상세구분, 검색어}` 로. 검색어는 지금 잡히는 품목(자동값)이 8-1 찾기 규칙 ·
|
||||
자동값 차례로 첫째가 되게 지음(이름 핵심 + 규격 핵심 + 지역 낱말 · 모자라면 낱말을 더함)
|
||||
· 재료 입력 키 목록(고르기가 자재품목 키인 입력)은 걷음 — 그 입력을 넘기던 `이름=이름` 인자도
|
||||
· 지역·계약종별 조건 줄 · 유가전력 틀(`MO:경유:{지역}`)은 그대로 — 자재품목 키가 아님
|
||||
· 글 모양 그대로 — 바뀐 값 자리만 갈음 · 다시 돌려도 같은 결과
|
||||
|
||||
사용: python scripts/build_재료_검색어.py (마른 돌리기 — 파일 안 씀 · 못 맞춘 줄 목록)
|
||||
python scripts/build_재료_검색어.py --쓰기 (로직 파일 갱신)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
import check_master as cm # noqa: E402
|
||||
import master_auto as mau # noqa: E402
|
||||
import master_formula as mf # noqa: E402
|
||||
import master_material as mm # noqa: E402
|
||||
import master_pick as mp # noqa: E402
|
||||
|
||||
MASTER = HERE.parent
|
||||
KEY = re.compile(r"MT\d{6}")
|
||||
|
||||
|
||||
# ── 판정 — 서버 찾기(`master_pick.search`) 그대로 ─────────────────────────
|
||||
def _has(word: str, row: dict) -> bool:
|
||||
"""`master_pick.search` 와 같은 맞대기 — 낱말이 이름 또는 규격에 듦."""
|
||||
w = mm.plain_spec(word)
|
||||
return w in mm.plain(row.get("이름")).replace("×", "x") or w in mm.plain_spec(row.get("규격"))
|
||||
|
||||
|
||||
def found(master: mf.Master, elem: dict, unit: str | None, env: dict) -> list[str]:
|
||||
return mp.search(master, elem, env, unit)
|
||||
|
||||
|
||||
def first(master, elem, unit, env) -> str | None:
|
||||
got = found(master, elem, unit, env)
|
||||
return got[0] if got else None
|
||||
|
||||
|
||||
# ── 지금 잡히는 품목 ──────────────────────────────────────────────────
|
||||
def material_inputs(row: dict) -> dict[str, list]:
|
||||
"""고르기가 자재품목 키 목록인 입력 — 걷을 것."""
|
||||
return {
|
||||
s["이름"]: s["고르기"]
|
||||
for s in row.get("입력", [])
|
||||
if isinstance(s.get("고르기"), list)
|
||||
and s["고르기"]
|
||||
and all(isinstance(x, str) and KEY.fullmatch(x) for x in s["고르기"])
|
||||
}
|
||||
|
||||
|
||||
def _holes(elem: dict) -> list[str]:
|
||||
return sorted({h for v in elem.values() if isinstance(v, str) for h in mf._HOLE.findall(v)})
|
||||
|
||||
|
||||
def envs(master: mf.Master, row: dict, elem: dict) -> list[dict]:
|
||||
"""줄을 판정할 입력 벌 — 자동값 하나 · 조건에 끼운 입력은 고르기 값마다."""
|
||||
base = mau.values(master, row)
|
||||
out = [base]
|
||||
for hole in _holes(elem):
|
||||
spec = next((s for s in row.get("입력", []) if s["이름"] == hole), {})
|
||||
if spec.get("고르기") and hole not in material_inputs(row):
|
||||
out = [{**e, hole: v} for e in out for v in spec["고르기"]]
|
||||
return out
|
||||
|
||||
|
||||
def old_pick(master: mf.Master, elem: dict, env: dict, unit: str | None) -> str | None:
|
||||
try:
|
||||
cond = {k: mf.fill(v, env) if isinstance(v, str) else v for k, v in elem.items()}
|
||||
return mm.pick(master, cond, env, unit)
|
||||
except mf.FormulaError:
|
||||
return None
|
||||
|
||||
|
||||
# ── 검색어 짓기 ───────────────────────────────────────────────────────
|
||||
def _name(v) -> str:
|
||||
"""품명 — 띄어 쓴 한글 글자는 붙임(「골 함 석」) · 쉼표 둘레 빈칸 뗌 · 영문 낱말 사이는 둠."""
|
||||
s = re.sub(r"\s*,\s*", ",", str(v or "").strip())
|
||||
return re.sub(r"(?<=[가-힣])\s+(?=[가-힣])", "", s)
|
||||
|
||||
|
||||
def _root(name: str) -> str:
|
||||
"""이름 핵심 — 빈칸 뗌 · 첫 괄호·쉼표 앞(「박 리 제(중유)」 → 박리제)."""
|
||||
return re.split(r"[(\[,]", _name(name))[0]
|
||||
|
||||
|
||||
def _pieces(spec) -> list[str]:
|
||||
"""규격 조각 — 빈칸·쉼표로 · 괄호 속 쉼표는 안 가름(「저유황(목재,합판용)」)."""
|
||||
return [p for p in re.split(r"\s+|,(?![^()]*\))", str(spec or "")) if p]
|
||||
|
||||
|
||||
def _core(row: dict) -> list[str]:
|
||||
"""이름 핵심(첫 쉼표 앞 · 괄호 그대로 「합판(내수)」) + 규격 핵심(첫 조각 · 「12t×1220×…」 →
|
||||
글자 든 첫 토막 12t)."""
|
||||
name = _name(row.get("이름")).split(",")[0]
|
||||
pieces = _pieces(row.get("규격"))
|
||||
spec = "".join(pieces[:2]) if pieces and len(pieces[0]) < 2 else (pieces or [""])[0]
|
||||
head = re.split(r"[×xX*]", spec)[0]
|
||||
if head != spec and re.search(r"[^\d.]", head):
|
||||
spec = head
|
||||
return [w for w in (name, spec) if w]
|
||||
|
||||
|
||||
def _tokens(row: dict) -> list[str]:
|
||||
"""더할 낱말 후보 — 이름 조각(쉼표로) · 이름 괄호 · 규격 조각(쉼표·빈칸·×로)."""
|
||||
name = _name(row.get("이름"))
|
||||
parts = [p for p in name.split(",") if p]
|
||||
out = parts + re.findall(r"\([^()]*\)", name)
|
||||
pieces = _pieces(row.get("규격"))
|
||||
out += pieces + [q for p in pieces for q in re.split(r"[×xX*]", p) if q and q != p]
|
||||
return list(dict.fromkeys(w for w in out if w))
|
||||
|
||||
|
||||
def _place(row: dict) -> str | None:
|
||||
tail = _name(row.get("이름")).split(",")[-1]
|
||||
return tail if tail in mp.REGIONS else None
|
||||
|
||||
|
||||
def _ok(master, elem, unit, cases) -> bool:
|
||||
"""판정 — 입력 벌마다 서버 찾기 첫째가 지금 품목."""
|
||||
for env, want in cases:
|
||||
filled = {k: mf.fill(v, env) if isinstance(v, str) else v for k, v in elem.items()}
|
||||
if first(master, filled, unit, env) != want:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def words_for(master, base: dict, words: list[str], unit, cases) -> list[str] | None:
|
||||
"""모자라면 첫 품목의 낱말을 하나씩 더함 — 앞선 줄을 가장 많이 떨구는 낱말부터."""
|
||||
target_rows = {want for _, want in cases}
|
||||
for _ in range(6):
|
||||
elem = {**base, "검색어": " ".join(words)}
|
||||
if _ok(master, elem, unit, cases):
|
||||
return words
|
||||
if len(target_rows) != 1 or None in target_rows:
|
||||
return None # 입력 따라 품목이 갈리는 줄 — 끼운 낱말로만
|
||||
want = master.get(next(iter(target_rows)))
|
||||
env = cases[0][0]
|
||||
got = found(master, {k: mf.fill(v, env) for k, v in elem.items()}, unit, env)
|
||||
ahead = got[: got.index(want["키"])] if want["키"] in got else got
|
||||
pick = None
|
||||
for w in _tokens(want):
|
||||
if w in words or not _has(w, want):
|
||||
continue
|
||||
cut = sum(not _has(w, master.get(k)) for k in set(ahead))
|
||||
if cut and (pick is None or cut > pick[0]):
|
||||
pick = (cut, w)
|
||||
if not pick:
|
||||
return None
|
||||
words.append(pick[1])
|
||||
return None
|
||||
|
||||
|
||||
def convert(master: mf.Master, row: dict, item: dict) -> tuple[dict | None, list]:
|
||||
"""재료 줄 하나 → 새 요소 · 판정 벌. 못 맞추면 (None, 벌)."""
|
||||
elem, unit = item["요소"], item.get("단위")
|
||||
cases = []
|
||||
for env in envs(master, row, elem):
|
||||
cases.append((env, old_pick(master, elem, env, unit)))
|
||||
base = {k: elem[k] for k in ("구분", "상세구분") if elem.get(k)}
|
||||
# 이미 좁혀 둔 이름·규격 낱말은 그대로 검색어로(`{관경}` 같은 끼운 칸도)
|
||||
words = [w for k in ("이름", "규격") for w in str(elem.get(k) or "").split()]
|
||||
targets = {want for _, want in cases}
|
||||
if targets == {None}:
|
||||
# 지금 같은 단위 후보 0 인 빈 줄 — 후보 단위가 줄 단위와 같은 성격(g ↔ kg 등)일 때만
|
||||
# 검색어로(값이 생김 · 금액 바뀜 · 줄마다 보고) · 성격이 다르면 빈칸 그대로
|
||||
probe = {**base, "검색어": _root(item.get("이름"))}
|
||||
got = mp.search(master, probe, mau.values(master, row), unit)
|
||||
if not got or not mp.same_kind(master.get(got[0])["단위"], unit):
|
||||
return None, cases
|
||||
name = _core(master.get(got[0]))[0]
|
||||
return {**base, "검색어": name}, cases
|
||||
if len(targets) == 1:
|
||||
want = master.get(next(iter(targets)))
|
||||
name, *spec = _core(want)
|
||||
if not any(mm.plain(w) in mm.plain(name) for w in words):
|
||||
words.insert(0, name) # 조건 이름 낱말이 이미 품명을 가리키면 그대로
|
||||
if spec and not elem.get("규격") and spec[0] not in words:
|
||||
words.append(spec[0])
|
||||
place = _place(want)
|
||||
if place and place not in words:
|
||||
words.append(place)
|
||||
else:
|
||||
roots = {_root(master.get(t)["이름"]) for t in targets if t}
|
||||
if (
|
||||
len(roots) == 1
|
||||
and (root := roots.pop())
|
||||
and not any(mm.plain(root) in mm.plain(w) for w in words)
|
||||
):
|
||||
words.insert(0, root)
|
||||
got = words_for(master, base, list(words), unit, cases)
|
||||
return ({**base, "검색어": " ".join(got)} if got else None), cases
|
||||
|
||||
|
||||
# ── 돌리기 ────────────────────────────────────────────────────────────
|
||||
def _targets(item: dict) -> bool:
|
||||
elem = item.get("요소")
|
||||
return (
|
||||
item.get("종류") == "재료"
|
||||
and isinstance(elem, dict)
|
||||
and "검색어" not in elem
|
||||
and elem.get("지역") != "입력"
|
||||
and elem.get("계약종별") != "입력"
|
||||
)
|
||||
|
||||
|
||||
def plan(master: mf.Master, files: dict[str, dict]) -> tuple[dict, list]:
|
||||
"""파일마다 {로직 키: {"요소": {호표 차례: 새 요소}, "걷음": [입력 이름]}} · 못 맞춘 줄."""
|
||||
out, misses = {}, []
|
||||
for name, data in files.items():
|
||||
if not name.startswith("로직_"):
|
||||
continue
|
||||
for row in data["줄"]:
|
||||
elems = {}
|
||||
for i, item in enumerate(row.get("호표", [])):
|
||||
if not _targets(item):
|
||||
continue
|
||||
elem, cases = convert(master, row, item)
|
||||
if elem is None:
|
||||
misses.append((row["키"], i, item.get("이름"), item["요소"], cases))
|
||||
else:
|
||||
elems[i] = elem
|
||||
gone = list(material_inputs(row))
|
||||
if elems or gone:
|
||||
out.setdefault(name, {})[row["키"]] = {"요소": elems, "걷음": gone}
|
||||
return out, misses
|
||||
|
||||
|
||||
_ARG = r"\s*,\s*{n}\s*=\s*{n}(?=\s*[,)])"
|
||||
# 걷는 입력을 식에서 품목 키로 쓰던 자리(`MT:{합판}`) — 같은 값의 호표 줄 금액으로
|
||||
# (GC000211 소모자재: 줄 금액 = 찾기 수량 × 합판율 × 단가 이므로 합판율을 줄 금액이 품음)
|
||||
SWAP = {
|
||||
"(찾기(QC000427).합판 * MT:{합판} + 찾기(QC000427).각재 * MT:{각재}) * 합판율 * ": (
|
||||
"(줄.'합판(내수 12㎜)' + 줄.'각재') * "
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _strip(text: str, names) -> str:
|
||||
"""부르는 식의 `, 철선=철선` 인자를 뗌 · 품목 키 자리는 줄 금액으로."""
|
||||
for n in names:
|
||||
text = re.sub(_ARG.format(n=re.escape(n)), "", text)
|
||||
for old, new in SWAP.items():
|
||||
text = text.replace(old, new)
|
||||
return text
|
||||
|
||||
|
||||
def apply(row: dict, change: dict) -> dict:
|
||||
"""바꿈을 줄 사본에 — 새 요소 · 재료 입력 걷음 · 그 입력을 넘기던 인자 뗌."""
|
||||
new = _copy(row)
|
||||
for i, elem in change["요소"].items():
|
||||
new["호표"][i]["요소"] = elem
|
||||
gone = change["걷음"]
|
||||
if gone:
|
||||
new["입력"] = [s for s in new["입력"] if s["이름"] not in gone]
|
||||
for group in ("중간", "호표", "덧줄"):
|
||||
for part in new.get(group, []):
|
||||
for k in ("식", "요소", "수량"):
|
||||
if isinstance(part.get(k), str):
|
||||
part[k] = _strip(part[k], gone)
|
||||
if "결과" in new:
|
||||
new["결과"]["식"] = _strip(new["결과"]["식"], gone)
|
||||
return new
|
||||
|
||||
|
||||
def _copy(v):
|
||||
if isinstance(v, dict):
|
||||
return {k: _copy(x) for k, x in v.items()}
|
||||
if isinstance(v, list):
|
||||
return [_copy(x) for x in v]
|
||||
return v
|
||||
|
||||
|
||||
# ── 글 모양 그대로 쓰기 ──────────────────────────────────────────────────
|
||||
_DEC = json.JSONDecoder()
|
||||
|
||||
|
||||
def _skip(text: str, pos: int) -> int:
|
||||
while text[pos] in " \t\r\n":
|
||||
pos += 1
|
||||
return pos
|
||||
|
||||
|
||||
def _items(text: str, pos: int) -> list[tuple[int, int]]:
|
||||
"""`[` 바로 뒤 `pos` 부터 목록 칸마다 (처음, 끝)."""
|
||||
spans = []
|
||||
while text[pos := _skip(text, pos)] != "]":
|
||||
_, end = _DEC.raw_decode(text, pos)
|
||||
spans.append((pos, end))
|
||||
pos = _skip(text, end)
|
||||
pos += text[pos] == ","
|
||||
return spans
|
||||
|
||||
|
||||
def _patch_row(text: str, row: dict, change: dict) -> str:
|
||||
"""줄 글 하나 — 바뀐 요소 값 자리 · 걷은 입력 칸 · 뗀 인자만 갈음."""
|
||||
spots = [m.end() for m in re.finditer(r'"요소"\s*:\s*', text)]
|
||||
if len(spots) != len(row.get("호표", [])):
|
||||
raise ValueError(f"{row['키']} 요소 자리 수가 호표와 다름")
|
||||
for i in sorted(change["요소"], reverse=True):
|
||||
at = spots[i]
|
||||
_, end = _DEC.raw_decode(text, at)
|
||||
text = text[:at] + json.dumps(change["요소"][i], ensure_ascii=False) + text[end:]
|
||||
gone = change["걷음"]
|
||||
if gone:
|
||||
m = re.search(r'"입력"\s*:\s*\[', text)
|
||||
spans = _items(text, m.end())
|
||||
keep = [text[s:e] for s, e in spans if json.loads(text[s:e]).get("이름") not in gone]
|
||||
lead, close = text[m.end() : spans[0][0]], _skip(text, spans[-1][1])
|
||||
tail = text[spans[-1][1] : close]
|
||||
body = lead + ("," + lead).join(keep) + tail if keep else ""
|
||||
text = text[: m.end()] + body + text[close:]
|
||||
text = re.sub(r'"(?:[^"\\]|\\.)*"', lambda s: _strip(s.group(0), gone), text)
|
||||
return text
|
||||
|
||||
|
||||
def patch(text: str, changes: dict) -> str:
|
||||
"""파일 글 — 바뀐 로직 줄만 손봄."""
|
||||
m = re.search(r'"줄"\s*:\s*\[', text)
|
||||
spans = _items(text, m.end())
|
||||
parts, at = [], 0
|
||||
for s, e in spans:
|
||||
row = json.loads(text[s:e])
|
||||
if row["키"] in changes:
|
||||
parts += [text[at:s], _patch_row(text[s:e], row, changes[row["키"]])]
|
||||
at = e
|
||||
return "".join(parts) + text[at:]
|
||||
|
||||
|
||||
def main(write: bool) -> int:
|
||||
files = cm.load()
|
||||
master = mf.Master(files)
|
||||
changes, misses = plan(master, files)
|
||||
lines = sum(len(c["요소"]) for rows in changes.values() for c in rows.values())
|
||||
inputs = sum(len(c["걷음"]) for rows in changes.values() for c in rows.values())
|
||||
for name, rows in changes.items():
|
||||
path = MASTER / name
|
||||
text = path.read_text(encoding="utf-8")
|
||||
new_text = patch(text, rows)
|
||||
want = json.loads(text)
|
||||
want["줄"] = [apply(r, rows[r["키"]]) if r["키"] in rows else r for r in want["줄"]]
|
||||
if json.loads(new_text) != want:
|
||||
raise SystemExit(f"{name} 글 갈음이 바꿈과 다름 — 안 씀")
|
||||
if write:
|
||||
path.write_text(new_text, encoding="utf-8", newline="\n")
|
||||
print(
|
||||
f"검색어로 바꾼 재료 줄 {lines} · 걷은 재료 입력 {inputs} · "
|
||||
f"로직 {sum(map(len, changes.values()))} · 파일 {len(changes)}"
|
||||
+ ("" if write else " (마른 돌리기)")
|
||||
)
|
||||
for key, i, name, elem, cases in misses:
|
||||
wants = sorted({str(w) for _, w in cases})
|
||||
print(f" 못 맞춤 {key} 호표[{i}] {name} {json.dumps(elem, ensure_ascii=False)} → {wants}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.exit(main("--쓰기" in sys.argv))
|
||||
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""견본 입력 — 마스터 화면 보기용 자동값(프로젝트 기본값 아님 · 저장 안 함).
|
||||
|
||||
고르기 = 목록 첫째 · 범위 = 최솟값 · 찾기 조건 값으로 쓰이는 수 = 그 표 그 칸 가장 낮은 줄 값
|
||||
(범위 줄이면 그 줄에 드는 아래 끝) · 나누는 자리에 오는 수 = 1 · 그 밖 0.
|
||||
찾기·나누기 쓰임은 로직 식(중간 · 결과 · 호표 수량·요소 · 덧줄)을 풀어 알아냄.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import master_formula as mf
|
||||
|
||||
|
||||
def _texts(row: dict):
|
||||
for step in row.get("중간", []):
|
||||
yield step["식"]
|
||||
if "결과" in row:
|
||||
yield row["결과"]["식"]
|
||||
for item in row.get("호표", []):
|
||||
yield item["수량"]
|
||||
if isinstance(item.get("요소"), str):
|
||||
yield item["요소"]
|
||||
for extra in row.get("덧줄", []):
|
||||
yield extra["식"]
|
||||
|
||||
|
||||
def _names(node) -> set:
|
||||
return {part[1] for part in mf.walk(node) if part[0] == "name"}
|
||||
|
||||
|
||||
def _uses(row: dict) -> tuple[dict, set]:
|
||||
"""(찾기 조건 값으로 바로 쓰인 이름 → [(표, 조건)], 나누는 자리에 든 이름)
|
||||
— 나누는 자리에 든 중간의 식에 쓰인 이름도 나누는 자리로 봄."""
|
||||
finds, divs = {}, set()
|
||||
for text in _texts(row):
|
||||
try:
|
||||
node = mf.parse(text)
|
||||
except mf.FormulaError:
|
||||
continue
|
||||
for part in mf.walk(node):
|
||||
if part[0] == "bin" and part[1] == "/":
|
||||
divs |= _names(part[3])
|
||||
if part[0] == "find":
|
||||
for cond, arg in part[2]:
|
||||
if arg[0] == "name":
|
||||
finds.setdefault(arg[1], []).append((part[1], cond))
|
||||
for step in reversed(row.get("중간", [])):
|
||||
if step["이름"] in divs:
|
||||
divs |= _names(mf.parse(step["식"]))
|
||||
return finds, divs
|
||||
|
||||
|
||||
def _fits(master: mf.Master, ref: str, cond: str) -> list:
|
||||
"""표 `ref` 조건 칸 `cond` 에 드는 수 — 낮은 차례 · 범위 칸이면 줄마다 그 줄에 드는 끝
|
||||
(아래 끝이 열린 줄 「60 미만」 은 0)."""
|
||||
try:
|
||||
table = master.get(ref)
|
||||
except mf.FormulaError:
|
||||
return []
|
||||
ranged = table.get("조건", {}).get(cond) == "범위"
|
||||
out = set()
|
||||
for r in table.get("줄", []):
|
||||
if r.get(cond) is None: # 빈 조건 칸은 무엇이든 맞음 — 값을 정하지 않음
|
||||
continue
|
||||
low, high = r[cond] if ranged else (r[cond], None)
|
||||
ends = [Decimal(0) if low is None else low, high]
|
||||
out |= {v for v in ends if isinstance(v, Decimal) and mf._match(table, r, cond, v)}
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def _lowest(master: mf.Master, uses: list):
|
||||
"""찾기 여럿이 같은 입력을 쓰면 모든 표에 드는 가장 낮은 수 · 없으면 첫 표의 가장 낮은 수."""
|
||||
sets = [_fits(master, ref, cond) for ref, cond in uses]
|
||||
both = sorted(set.intersection(*map(set, sets))) if sets else []
|
||||
return (both or next((s for s in sets if s), [None]))[0]
|
||||
|
||||
|
||||
def values(master: mf.Master, row: dict) -> dict:
|
||||
"""로직 줄 하나의 입력마다 자동값."""
|
||||
finds, divs = _uses(row)
|
||||
out = {}
|
||||
for spec in row.get("입력", []):
|
||||
name = spec["이름"]
|
||||
if spec.get("고르기"):
|
||||
out[name] = spec["고르기"][0]
|
||||
elif spec.get("범위"):
|
||||
out[name] = spec["범위"][0]
|
||||
else:
|
||||
got = _lowest(master, finds.get(name, []))
|
||||
if got is None and "원" in str(spec.get("단위") or ""):
|
||||
out[name] = None # 가격 입력 — 0 원처럼 보이지 않게 비워 둠
|
||||
else:
|
||||
out[name] = got if got is not None else Decimal(1 if name in divs else 0)
|
||||
return out
|
||||
@@ -14,9 +14,12 @@ import re
|
||||
|
||||
import master_formula as mf
|
||||
|
||||
MAX_NAME = 12 # 이 글자 수를 넘는 표 이름은 억지로 줄이지 않고 표 키를 그대로 보임
|
||||
MAX_NAME = 12 # 짧은 이름의 길이 상한
|
||||
MAX_WORD = 8 # 별칭 꼬리(찾은 줄 값) 한 낱말의 길이 상한
|
||||
_DROP = re.compile(r"[^\w]", re.UNICODE) # 별칭에 못 쓰는 글자(빈칸 · 괄호 · 가운뎃점 …)
|
||||
_SPACE = re.compile(r"\s+")
|
||||
_PAREN = re.compile(r"\([^()]*\)")
|
||||
_WORDS = re.compile(r"[\s·・,/:+×*~∼]+")
|
||||
_QUOTED = re.compile(r"'[^']*'")
|
||||
RESERVED = frozenset((*mf.FUNCS, *mf.COST_ITEMS, "찾기", "로직", "그리고", "또는", "계", "줄"))
|
||||
|
||||
@@ -98,15 +101,146 @@ def plain(text) -> str:
|
||||
return _DROP.sub("", str(text or ""))
|
||||
|
||||
|
||||
def _unparen(text: str, keep: bool) -> str:
|
||||
"""괄호 묶음을 벗김 — `keep` 이면 숫자 없는 묶음(공기식 · 건설)만 글로 남김."""
|
||||
while True:
|
||||
got = _PAREN.sub(
|
||||
lambda m: f" {m.group()[1:-1]} " if keep and not re.search(r"\d", m.group()) else " ",
|
||||
text,
|
||||
)
|
||||
if got == text:
|
||||
return got
|
||||
text = got
|
||||
|
||||
|
||||
def _core(text: str) -> list[str]:
|
||||
"""앞 핵심 낱말 — 머리 번호(1급 · 5-3)는 건너뛰고, 그 뒤 숫자 든 낱말(규격)에서 끊음."""
|
||||
out: list[str] = []
|
||||
for word in _WORDS.split(text):
|
||||
word = plain(word)
|
||||
if not word:
|
||||
continue
|
||||
if re.search(r"\d", word):
|
||||
if out:
|
||||
break
|
||||
continue
|
||||
out.append(word)
|
||||
return out
|
||||
|
||||
|
||||
def _fit(words: list[str], cap: int) -> str:
|
||||
"""낱말들을 이어 이름 하나로 — 길면 첫 낱말 + 끝 낱말 · 그래도 길면 첫 낱말을 자름."""
|
||||
tries = ["".join(words), *([words[0] + words[-1], words[0][:cap]] if words else [])]
|
||||
return next((one for one in tries if one and len(one) <= cap), "")
|
||||
|
||||
|
||||
def short(text, cap: int = MAX_NAME) -> str:
|
||||
"""사람이 읽는 짧은 이름 — 규격·코드를 떼고, 길면 괄호 속 · 가운데 낱말 순으로 버림.
|
||||
늘 같은 글이면 같은 이름 · 글자 · 숫자 · 밑줄만 · 숫자로 시작하지 않음(못 지으면 빈 글)."""
|
||||
text = str(text or "")
|
||||
kept = "".join(_core(_unparen(text, True)))
|
||||
return kept if kept and len(kept) <= cap else _fit(_core(_unparen(text, False)), cap)
|
||||
|
||||
|
||||
def _base(ref: str, master) -> str:
|
||||
"""별칭 밑이름 — 표·로직의 `이름` 칸에서 · 비거나 너무 길면 키를 그대로 보임."""
|
||||
"""별칭 밑이름 — 표·로직 이름을 짧게 · 이름이 없으면 공종 · 상세구분 · 그래도 없으면 키."""
|
||||
try:
|
||||
name = plain(master.get(ref).get("이름"))
|
||||
row = master.get(ref)
|
||||
except mf.FormulaError:
|
||||
name = ""
|
||||
if not name or len(name) > MAX_NAME or name[0].isdigit():
|
||||
return _DROP.sub("", ref) or "표"
|
||||
return name
|
||||
row = {}
|
||||
use = row.get("용도") if isinstance(row.get("용도"), dict) else {}
|
||||
for text in (row.get("이름"), use.get("공종"), row.get("상세구분")):
|
||||
got = short(text)
|
||||
if got:
|
||||
return got
|
||||
return _DROP.sub("", ref) or "표"
|
||||
|
||||
|
||||
def _words(node, master) -> list[str]:
|
||||
"""부르기 인자 하나의 낱말 — 글은 그 값(요소 코드면 품명 + 규격) · 수 · 이름은 그대로."""
|
||||
kind = node[0]
|
||||
if kind == "name":
|
||||
return [node[1]]
|
||||
if kind == "num":
|
||||
return [plain(node[1])]
|
||||
if kind != "str": # 만약(…) 따위 — 안에 적힌 글 가운데 처음 읽히는 것
|
||||
return next(filter(None, (_core(_unparen(one, False)) for one in _texts(node))), [])
|
||||
for ref in (node[1], f"EQ:{node[1]}"): # 기계 입력은 원문번호(0201-0020)
|
||||
try:
|
||||
row = master.get(ref)
|
||||
except mf.FormulaError:
|
||||
continue
|
||||
if row.get("이름"):
|
||||
return [short(row["이름"]), *filter(None, [plain(row.get("규격"))])]
|
||||
return _core(_unparen(str(node[1]), False)) or [plain(node[1])]
|
||||
|
||||
|
||||
def _texts(node):
|
||||
"""마디 안의 글 값(작은따옴표) — 적힌 차례로."""
|
||||
if isinstance(node, tuple) and node[:1] == ("str",):
|
||||
yield str(node[1])
|
||||
elif isinstance(node, (tuple, list)):
|
||||
for one in node:
|
||||
yield from _texts(one)
|
||||
|
||||
|
||||
def _columns(signs: list[str], refs: list[str], master) -> list[dict[str, str]]:
|
||||
"""부르기마다 {칸: 꼬리 낱말} — 다른 표가 섞이면 표 제목도 한 칸(`""`).
|
||||
한 칸 값들이 모두 가진 낱말(`구체콘크리트`)은 떼어 서로 가르는 낱말만 남김."""
|
||||
rows: list[dict[str, list[str]]] = [{} for _ in signs]
|
||||
if len(set(refs)) > 1:
|
||||
for one, ref in zip(rows, refs):
|
||||
try:
|
||||
one[""] = _core(_unparen(str(master.get(ref).get("이름") or ""), False))
|
||||
except mf.FormulaError:
|
||||
one[""] = []
|
||||
for one, sign in zip(rows, signs):
|
||||
try:
|
||||
args = mf.parse(sign)[2]
|
||||
except (mf.FormulaError, IndexError, TypeError):
|
||||
args = []
|
||||
one.update({key: _words(value, master) for key, value in args})
|
||||
out: list[dict[str, str]] = [{} for _ in signs]
|
||||
for key in dict.fromkeys(key for one in rows for key in one):
|
||||
lists = [one.get(key, []) for one in rows]
|
||||
common = set.intersection(*map(set, lists))
|
||||
if not all(set(words) - common for words in lists):
|
||||
common = set()
|
||||
for got, words in zip(out, lists):
|
||||
got[key] = _fit([w for w in words if w not in common], MAX_WORD)
|
||||
return out
|
||||
|
||||
|
||||
def _tails(signs: list[str], refs: list[str], master) -> list[str]:
|
||||
"""같은 밑이름의 부르기들 — 서로 가르는 인자 값만 이어 꼬리로(`철망` · `외업_고급기술자`).
|
||||
가르는 칸은 적게 — 가장 잘 가르는 칸부터 모두 갈릴 때까지 고르고, 줄마다 없어도 되는 칸은 뺌."""
|
||||
cols = _columns(signs, refs, master)
|
||||
order = list(dict.fromkeys(key for one in cols for key in one))
|
||||
|
||||
def seen(one: dict, keys: list[str]) -> tuple:
|
||||
return tuple(one.get(key, "") for key in keys)
|
||||
|
||||
picked: list[str] = []
|
||||
left = list(order)
|
||||
while len({seen(one, picked) for one in cols}) < len(cols) and left:
|
||||
best = max(left, key=lambda key: len({seen(one, [*picked, key]) for one in cols}))
|
||||
if len({seen(one, [*picked, best]) for one in cols}) == len(
|
||||
{seen(o, picked) for o in cols}
|
||||
):
|
||||
break
|
||||
picked.append(best)
|
||||
left.remove(best)
|
||||
out = []
|
||||
for at, one in enumerate(cols):
|
||||
mine = list(picked)
|
||||
for key in reversed(picked): # 늦게 고른(덜 가르는) 칸부터 빼 봄
|
||||
rest = [k for k in mine if k != key]
|
||||
others = (seen(o, rest) for j, o in enumerate(cols) if j != at)
|
||||
if not rest or seen(one, rest) in set(others):
|
||||
break
|
||||
mine = rest
|
||||
out.append("_".join(filter(None, seen(one, sorted(mine, key=order.index)))))
|
||||
return out
|
||||
|
||||
|
||||
def _free(want: str, taken: set[str]) -> str:
|
||||
@@ -144,8 +278,11 @@ def aliases(row: dict, master) -> list[dict]:
|
||||
taken |= set(_about(kind, ref, master)["값칸"])
|
||||
for base, signs in bunch.items():
|
||||
gap = "_" if base[-1:].isdigit() else "" # 키를 밑이름으로 쓴 자리 — 번호가 붙지 않게
|
||||
for at, sign in enumerate(signs):
|
||||
name_of[sign] = _free(f"{base}{gap}{at + 1}" if len(signs) > 1 else base, taken)
|
||||
tails = _tails(signs, [seen[s][1] for s in signs], master) if len(signs) > 1 else [""]
|
||||
for at, (sign, tail) in enumerate(zip(signs, tails)):
|
||||
alone = len(signs) == 1 or any(tails) # 꼬리 없는 하나는 밑이름만
|
||||
want = f"{base}_{tail}" if tail else base if alone else f"{base}{gap}{at + 1}"
|
||||
name_of[sign] = _free(want, taken)
|
||||
|
||||
out = []
|
||||
for sign in order:
|
||||
|
||||
@@ -32,6 +32,26 @@ class FormulaError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class EmptyInput(FormulaError):
|
||||
"""값을 안 넣은(null) 입력을 씀 — 그 줄만 비우고 로직은 계속(`run`)."""
|
||||
|
||||
|
||||
class NoRow(EmptyInput):
|
||||
"""표에서 조건에 맞는 줄이 하나가 아님 — 그 찾기를 쓰는 줄만 비움(값이 비어 있는 입력과 같은 길)."""
|
||||
|
||||
|
||||
class Blank:
|
||||
"""못 구한 중간값 — 이 값을 쓰는 줄만 까닭과 함께 비움."""
|
||||
|
||||
def __init__(self, why: str):
|
||||
self.why = why
|
||||
|
||||
|
||||
def _plain(v) -> str:
|
||||
"""사람이 읽는 값 글 — 파이썬 날글(Decimal('1000') · 따옴표) 없이."""
|
||||
return format(v.normalize(), "f") if isinstance(v, Decimal) else str(v)
|
||||
|
||||
|
||||
# ── 읽기 ──────────────────────────────────────────────────────────────
|
||||
class _Parser:
|
||||
def __init__(self, text: str):
|
||||
@@ -247,6 +267,7 @@ class Master:
|
||||
if number in slot:
|
||||
self.twice.add((key[:2], number))
|
||||
slot.setdefault(number, row)
|
||||
mp.EQ.set(self) # 기계 입력이 목록 밖 원문 분류번호도 받게
|
||||
|
||||
def get(self, ref: str) -> dict:
|
||||
tid, _, number = ref.partition(":")
|
||||
@@ -295,7 +316,9 @@ def fill(template: str, env: dict) -> str:
|
||||
if m.group(1) not in env:
|
||||
raise FormulaError(f"원문번호의 {{{m.group(1)}}} 값 없음")
|
||||
value = env[m.group(1)]
|
||||
return format(value.normalize(), "f") if isinstance(value, Decimal) else str(value)
|
||||
if isinstance(value, Blank):
|
||||
raise EmptyInput(value.why)
|
||||
return _plain(value)
|
||||
|
||||
return _HOLE.sub(one, template)
|
||||
|
||||
@@ -349,7 +372,13 @@ def find(table: dict, conds: dict, want: str | None = None) -> dict:
|
||||
if len(hits) > 1 and want: # 여러 줄이면 찾는 값 칸이 있는 줄만
|
||||
hits = [row for row in hits if want in row]
|
||||
if len(hits) != 1:
|
||||
raise FormulaError(f"「{_name(table)}」 {conds} 맞는 줄 {len(hits)}개")
|
||||
where = f"표 {table.get('키')}({table.get('원문번호')})"
|
||||
what = " · ".join(f"{c} {_plain(v)}" for c, v in conds.items())
|
||||
raise NoRow(
|
||||
f"{where}에 {what} 인 줄이 없음"
|
||||
if not hits
|
||||
else f"{where}에 {what} 인 줄이 {len(hits)}개"
|
||||
)
|
||||
return hits[0]
|
||||
|
||||
|
||||
@@ -360,6 +389,10 @@ def evaluate(node, env: dict, master: Master, depth: int = 0):
|
||||
if kind == "name":
|
||||
if node[1] not in env:
|
||||
raise FormulaError(f"모르는 이름 「{node[1]}」")
|
||||
if env[node[1]] is None:
|
||||
raise EmptyInput(f"입력 「{node[1]}」 비어 있음")
|
||||
if isinstance(env[node[1]], Blank):
|
||||
raise EmptyInput(env[node[1]].why)
|
||||
return env[node[1]]
|
||||
if kind == "ref":
|
||||
value = element(master, fill(node[1], env), env)[0]
|
||||
@@ -443,6 +476,16 @@ def cut(row: dict, value: Decimal, on: str) -> Decimal:
|
||||
return _round(value, rule.get("자리") or 0, way)
|
||||
|
||||
|
||||
def fill_empty(row: dict, given: dict) -> dict:
|
||||
"""화면이 안 보낸 가격 입력(단위에 「원」)은 null 로 채움 — 빈 입력과 같이 그 줄만 비움."""
|
||||
gap = {
|
||||
s["이름"]: None
|
||||
for s in row.get("입력", [])
|
||||
if s["이름"] not in given and "원" in str(s.get("단위") or "")
|
||||
}
|
||||
return {**given, **gap}
|
||||
|
||||
|
||||
def _inputs(row: dict, given: dict) -> dict:
|
||||
env = {}
|
||||
for spec in row.get("입력", []):
|
||||
@@ -460,11 +503,12 @@ def _inputs(row: dict, given: dict) -> dict:
|
||||
pass
|
||||
if isinstance(value, (int, float)):
|
||||
value = Decimal(str(value))
|
||||
if "고르기" in spec and value not in spec["고르기"]:
|
||||
value = mp.resolve(spec, value) # 기계 EQ 키 → 원문 분류번호
|
||||
if "고르기" in spec and value not in spec["고르기"] and not mp.machine(spec, value):
|
||||
raise FormulaError(f"입력 「{name}」={value} 고르기 밖")
|
||||
if "범위" in spec:
|
||||
low, high = spec["범위"]
|
||||
if not low <= value <= high:
|
||||
if value is not None and not low <= value <= high:
|
||||
raise FormulaError(f"입력 「{name}」={value} 범위 [{low}, {high}] 밖")
|
||||
env[name] = value
|
||||
unknown = set(given) - set(env)
|
||||
@@ -473,22 +517,51 @@ def _inputs(row: dict, given: dict) -> dict:
|
||||
return env
|
||||
|
||||
|
||||
def _shown(env: dict) -> dict:
|
||||
"""돌려줄 중간값 — 못 구한 값은 null."""
|
||||
return {k: None if isinstance(v, Blank) else v for k, v in env.items()}
|
||||
|
||||
|
||||
def _blank_line(item: dict, why: str) -> dict:
|
||||
"""비어 있는 입력·못 구한 값을 쓰는 호표 줄 — 단가·금액만 비우고 까닭."""
|
||||
return {
|
||||
"이름": item.get("이름", ""),
|
||||
"단위": item.get("단위", ""),
|
||||
"수량": None,
|
||||
"단가": None,
|
||||
"금액": None,
|
||||
"비목": {item["비목"]: None} if "비목" in item else {},
|
||||
"까닭": why,
|
||||
}
|
||||
|
||||
|
||||
def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
|
||||
"""로직 한 줄 계산 — 돈 로직은 호표 줄 · 비목 합 · 계, 돈 아닌 로직은 결과 수 하나."""
|
||||
row = master.logic(ref)
|
||||
env = _inputs(row, given)
|
||||
for step in row.get("중간", []):
|
||||
env[step["이름"]] = evaluate(parse(step["식"]), env, master, depth)
|
||||
try:
|
||||
env[step["이름"]] = evaluate(parse(step["식"]), env, master, depth)
|
||||
except EmptyInput as e: # 못 구한 중간값 — 쓰는 줄만 비움
|
||||
env[step["이름"]] = Blank(str(e))
|
||||
if "결과" in row:
|
||||
value = evaluate(parse(row["결과"]["식"]), env, master, depth)
|
||||
return {"결과": cut(row, value, "결과"), "중간": env}
|
||||
return {"결과": cut(row, value, "결과"), "중간": _shown(env)}
|
||||
sums = dict.fromkeys(COST_ITEMS, Decimal(0))
|
||||
lines = []
|
||||
for item in row.get("호표", []):
|
||||
qty = evaluate(parse(item["수량"]), env, master, depth)
|
||||
source = None
|
||||
for n, item in enumerate(row.get("호표", [])):
|
||||
source = why = extra = None
|
||||
try:
|
||||
qty = evaluate(parse(item["수량"]), env, master, depth)
|
||||
except EmptyInput as e: # 비어 있는 입력을 쓰는 줄만 비움
|
||||
lines.append(_blank_line(item, str(e)))
|
||||
continue
|
||||
if item["종류"] == "로직":
|
||||
bundle = evaluate(parse(item["요소"]), env, master, depth)
|
||||
try:
|
||||
bundle = evaluate(parse(item["요소"]), env, master, depth)
|
||||
except EmptyInput as e:
|
||||
lines.append(_blank_line(item, str(e)))
|
||||
continue
|
||||
price = bundle["계"]
|
||||
split = (
|
||||
{k: qty * bundle[k] for k in COST_ITEMS}
|
||||
@@ -498,42 +571,51 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
|
||||
else:
|
||||
# 고르기 조건 묶음은 그대로 넘김 — `element` 가 대표 줄·첫 줄로 풂
|
||||
# 조건 값의 `{입력}`(관경 등)도 채움 — 규격이 입력 따라 바뀌는 관 줄
|
||||
ref = (
|
||||
{k: fill(v, env) if isinstance(v, str) else v for k, v in item["요소"].items()}
|
||||
if isinstance(item["요소"], dict)
|
||||
else fill(item["요소"], env)
|
||||
)
|
||||
try:
|
||||
price, source = element(master, ref, env, item.get("단위"))
|
||||
except FormulaError:
|
||||
if qty != 0:
|
||||
raise
|
||||
price = None
|
||||
ref = (
|
||||
{k: fill(v, env) if isinstance(v, str) else v for k, v in item["요소"].items()}
|
||||
if isinstance(item["요소"], dict)
|
||||
else fill(item["요소"], env)
|
||||
)
|
||||
except EmptyInput as e:
|
||||
lines.append(_blank_line(item, str(e)))
|
||||
continue
|
||||
# 후보 0 · 값 빈 품목은 그 줄만 단가·금액을 비우고 까닭을 닮 — 로직은 계속 · 합에서 뺌
|
||||
# (없는 키는 틀린 로직 — 지금처럼 멈춤) · 검색어 · 고름 · 단위 환산은 `master_pick`
|
||||
if isinstance(ref, str) and qty != 0:
|
||||
master.get(ref)
|
||||
price, source, why, extra = mp.priced(master, item, ref, env, n if depth == 0 else None)
|
||||
if price is None and qty == 0:
|
||||
price = Decimal(0) # 안 쓰는 재료 줄 — 값이 비어도 금액 0
|
||||
if price is None:
|
||||
raise FormulaError(f"「{master.label(ref)}」 값 없음 — 관리자가 채울 값")
|
||||
split = {item["비목"]: qty * price}
|
||||
price, why = Decimal(0), None # 안 쓰는 재료 줄 — 값이 비어도 금액 0
|
||||
split = {item["비목"]: None if price is None else qty * price}
|
||||
for k, v in split.items():
|
||||
sums[k] += v
|
||||
sums[k] += v or 0
|
||||
lines.append(
|
||||
{
|
||||
"이름": item.get("이름", ""),
|
||||
"단위": item.get("단위", ""),
|
||||
"수량": qty,
|
||||
"단가": price,
|
||||
"금액": qty * price,
|
||||
"금액": None if price is None else qty * price,
|
||||
"비목": split,
|
||||
**({"출처": source} if source else {}),
|
||||
**({"까닭": why} if why else {}),
|
||||
**(extra or {}),
|
||||
}
|
||||
)
|
||||
# 덧줄이 호표 줄 금액 하나를 빼고 더할 수 있게 — `줄.'호표 줄 이름'`
|
||||
env["줄"] = {item["이름"]: item["금액"] for item in lines if item["이름"]}
|
||||
# 덧줄이 호표 줄 금액 하나를 빼고 더할 수 있게 — `줄.'호표 줄 이름'`(빈 줄은 뺌)
|
||||
env["줄"] = {x["이름"]: x["금액"] for x in lines if x["이름"] and x["금액"] is not None}
|
||||
blank = {x["이름"]: x["까닭"] for x in lines if x.get("까닭")}
|
||||
for extra in row.get("덧줄", []):
|
||||
env.update(sums)
|
||||
value = evaluate(parse(extra["식"]), env, master, depth)
|
||||
env[extra["이름"]] = value
|
||||
sums[extra["비목"]] += value
|
||||
try: # 덧줄도 값 빈 품목·빈 줄을 부르면 그 줄만 비움
|
||||
value, why = evaluate(parse(extra["식"]), env, master, depth), None
|
||||
env[extra["이름"]] = value
|
||||
except FormulaError as e:
|
||||
gone = next((n for n in blank if n in extra["식"]), None)
|
||||
value, why = None, f"「{gone}」 줄 비어 못 셈 — {blank[gone]}" if gone else str(e)
|
||||
blank[extra["이름"]] = why
|
||||
sums[extra["비목"]] += value or 0
|
||||
lines.append(
|
||||
{
|
||||
"이름": extra["이름"],
|
||||
@@ -542,10 +624,11 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
|
||||
"단가": value,
|
||||
"금액": value,
|
||||
"비목": {extra["비목"]: value},
|
||||
**({"까닭": why} if why else {}),
|
||||
}
|
||||
)
|
||||
sums = {k: cut(row, v, "계") for k, v in sums.items()} # 비목 합마다 끊고 계는 그 합
|
||||
return {"줄": lines, **sums, "계": sum(sums.values()), "중간": env}
|
||||
return {"줄": lines, **sums, "계": sum(sums.values()), "중간": _shown(env)}
|
||||
|
||||
|
||||
# ── 검사 ──────────────────────────────────────────────────────────────
|
||||
@@ -765,3 +848,4 @@ from master_material import ( # noqa: E402, F401
|
||||
element,
|
||||
pick,
|
||||
)
|
||||
import master_pick as mp # noqa: E402 — 검색어 · 고름 · 단위
|
||||
|
||||
@@ -16,7 +16,15 @@ LINKED = "MP" # 품셈재료 테이블ID
|
||||
MARKET = "MT" # 자재품목 테이블ID
|
||||
PRICED = "MO" # 유가전력 테이블ID — 값 한 칸
|
||||
SLOTS = ("물가자료", "유통물가", "물가정보", "거래가격", "관급") # 자재품목 값 열
|
||||
PICK_KEYS = ("구분", "상세구분", "이름", "규격", "대표", "지역", "계약종별") # 재료 고르기 조건 칸 (`_틀.md` 7장)
|
||||
PICK_KEYS = (
|
||||
"구분",
|
||||
"상세구분",
|
||||
"이름",
|
||||
"규격",
|
||||
"대표",
|
||||
"지역",
|
||||
"계약종별",
|
||||
) # 재료 고르기 조건 칸 (`_틀.md` 7장)
|
||||
REGION = "자재지역" # 로직 입력 — 「지역」 은 유가 고르기가 씀
|
||||
TARIFF = "전력 계약종별" # 로직 입력 — 품셈·계약예규에 계약종별 규정 없음
|
||||
_UNITS = {"ea": "개", "ton": "톤", "t": "톤", "l": "ℓ", "리터": "ℓ", "set": "조", "세트": "조"}
|
||||
@@ -98,7 +106,9 @@ def _region(master: mf.Master, label_ref, cond: dict, env: dict, unit) -> list[s
|
||||
if not hits:
|
||||
raise mf.FormulaError(f"「{master.label(label_ref)}」 지역 값 없음 — {env.get(REGION)}")
|
||||
if len({s for s, _ in hits}) > 1:
|
||||
raise mf.FormulaError(f"「{master.label(label_ref)}」 후보 여럿 — 규격 선택 필요 ({len(hits)}줄)")
|
||||
raise mf.FormulaError(
|
||||
f"「{master.label(label_ref)}」 후보 여럿 — 규격 선택 필요 ({len(hits)}줄)"
|
||||
)
|
||||
return [key for _, key in hits]
|
||||
|
||||
|
||||
@@ -110,7 +120,9 @@ def static(cond: dict) -> dict:
|
||||
|
||||
def cond_text(cond: dict) -> str:
|
||||
"""고르기 조건 한 글자 열쇠 — 「구분|상세구분|규격|대표」 · 화면 단가 미리보기가 줄을 찾는 이름."""
|
||||
return "|".join(str(cond.get(k) or "") for k in PICK_KEYS)
|
||||
return "|".join(str(cond.get(k) or "") for k in PICK_KEYS) + (
|
||||
f"|{cond['검색어']}" if cond.get("검색어") else ""
|
||||
)
|
||||
|
||||
|
||||
def by_class(master: mf.Master) -> dict[str, dict]:
|
||||
@@ -130,7 +142,10 @@ def candidates(
|
||||
) -> list[str]:
|
||||
"""고르기 조건 안 자재품목 키 — 구분 · 상세구분 · 이름 · 규격 낱말이 모두 든 줄 · 파일 차례대로.
|
||||
`unit` 을 주면 그 단위(호표 줄 단위)와 같은 줄만 — 단위가 다르면 다른 물건.
|
||||
입력 `자재지역` 이 있으면 이름에 그 지역이 든 줄만(그런 줄이 없으면 조건 안 전부)."""
|
||||
입력 `자재지역` 이 있으면 이름에 그 지역이 든 줄만(그런 줄이 없으면 조건 안 전부).
|
||||
조건에 `검색어` 가 있으면 기본 검색어 찾기(`master_pick`)."""
|
||||
if cond.get("검색어"):
|
||||
return mp.search(master, cond, env, unit)
|
||||
want = [plain_spec(w) for w in str(cond.get("규격") or "").split()]
|
||||
want_name = [plain(w) for w in str(cond.get("이름") or "").split()]
|
||||
detail = cond.get("상세구분")
|
||||
@@ -150,18 +165,23 @@ def candidates(
|
||||
|
||||
def pick(master: mf.Master, cond: dict, env: dict, unit: str | None = None) -> str:
|
||||
"""시험 계산이 쓸 줄 — 관리자가 정한 `대표`, 없으면 조건 안 값 있는 첫 줄(단위 같은 줄만)."""
|
||||
if cond.get("검색어"):
|
||||
return mp.pick(master, cond, env, unit)
|
||||
if cond.get("대표"):
|
||||
return str(cond["대표"])
|
||||
keys = candidates(master, cond, env, unit)
|
||||
keys = [k for k in keys if _offers(master, k)] or keys
|
||||
if not keys:
|
||||
raise mf.FormulaError(f"재료 고르기 후보 없음 — {cond}")
|
||||
what = " · ".join(f"{k} {mf._plain(v)}" for k, v in cond.items())
|
||||
raise mf.FormulaError(f"재료 고르기 후보 없음 — {what}")
|
||||
return keys[0]
|
||||
|
||||
|
||||
def check_unit(master: mf.Master, where: str, cond: dict, unit: str | None) -> list[str]:
|
||||
"""단위 경고 — 호표 줄·품셈재료 줄 단위와 맞는 후보가 없거나 대표 줄 단위가 다름.
|
||||
틀린 데이터가 아니라 좁혀야 할 조건이라 `check_pick` 과 따로 봄(`check_master 단위`)."""
|
||||
if cond.get("검색어"):
|
||||
return mp.check_warn(master, where, cond, unit)
|
||||
cond = static(cond)
|
||||
rows = master.index.get(MARKET, {})
|
||||
rep = str(cond.get("대표") or "")
|
||||
@@ -183,6 +203,8 @@ def check_pick(master: mf.Master, where: str, cond: dict, unit: str | None = Non
|
||||
값에 `{입력}` 이 든 칸(관경 등)은 계산 때 채워지므로 검사에서 뺌.
|
||||
지역·계약종별 조건은 구분 없이 이름 하나로 자재품목·유가전력을 넓게 찾음(_region · _tariff)
|
||||
— 여기서는 이름 칸만 있는지 봄(후보 0 은 계산 때 판단)."""
|
||||
if cond.get("검색어") is not None and not set(cond) - {*PICK_KEYS, "검색어"}:
|
||||
return mp.check_zero(master, where, cond)
|
||||
if set(cond) - set(PICK_KEYS):
|
||||
return [f"{where} · 재료 고르기 조건 모양 「{cond}」 — {' · '.join(PICK_KEYS)}"]
|
||||
if ("지역" in cond and cond["지역"] != "입력") or (
|
||||
@@ -246,3 +268,4 @@ def element(
|
||||
|
||||
|
||||
import master_formula as mf # noqa: E402 — 서로 부름 · 이 모듈 이름이 다 선 뒤에 읽음
|
||||
import master_pick as mp # noqa: E402 — 검색어 찾기
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""재료 줄의 기본 검색어 찾기 · 고른 품목 · 단위 환산 · 단위 경고 (PLAN 8-1 계약).
|
||||
|
||||
`master_formula.run` 이 재료 줄 단가를 풀 때 부름 — 요소 객체에 `검색어` 가 있으면 여기서 찾고,
|
||||
없으면 옛 길(`master_material` 조건 · 대표). 서로 부르는 모듈이라 `master_formula` 는 끝에서 읽음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from contextvars import ContextVar
|
||||
from decimal import Decimal
|
||||
|
||||
import master_material as mm
|
||||
|
||||
# 「같은 성격」 단위 — {성격: {정규 단위: 기준 단위 몇 배}} (키는 `plain_unit` 을 거친 꼴)
|
||||
UNITS = {
|
||||
"무게": {"g": 1, "kg": 1000, "톤": 1000000},
|
||||
"길이": {"mm": 1, "cm": 10, "m": 1000, "km": 1000000},
|
||||
"넓이": {"cm2": 1, "m2": 10000},
|
||||
"부피": {"cm3": 1, "ℓ": 1000, "m3": 1000000},
|
||||
}
|
||||
_SCALE = {u: (kind, n) for kind, group in UNITS.items() for u, n in group.items()}
|
||||
REGIONS = (
|
||||
"전국평균 전국 서울 경기 강원 충북 충남 전북 전남 경북 경남 부산 대구 인천 대전 울산 세종 제주 광주"
|
||||
" 전남광주"
|
||||
).split()
|
||||
|
||||
CHOSEN: ContextVar[dict | None] = ContextVar("고름", default=None) # {호표 줄 차례(0부터): 자재 키}
|
||||
EQ: ContextVar = ContextVar("기계번호", default=None) # 지금 쓰는 마스터 — 기계 줄을 찾음
|
||||
|
||||
|
||||
@contextmanager
|
||||
def choosing(picks: dict | None):
|
||||
"""`/calc` · `/text` 몸의 `고름` — 그 요청 동안만 · 저장 안 함."""
|
||||
token = CHOSEN.set({str(k): str(v) for k, v in (picks or {}).items()})
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
CHOSEN.reset(token)
|
||||
|
||||
|
||||
def _numbers() -> dict:
|
||||
return EQ.get().numbers.get("EQ", {}) if EQ.get() else {}
|
||||
|
||||
|
||||
def resolve(spec: dict, value):
|
||||
"""기계 입력이 EQ 키로 오면 원문 분류번호로 풂 — 고르기 목록(원문번호)과 같은 꼴."""
|
||||
if not (isinstance(value, str) and value.strip()[:2] == "EQ" and EQ.get()):
|
||||
return value
|
||||
row = EQ.get().index.get("EQ", {}).get(value.strip())
|
||||
number = str(row.get("원문번호") or "") if row else ""
|
||||
return number if number and machine(spec, number) else value
|
||||
|
||||
|
||||
def machine(spec: dict, value) -> bool:
|
||||
"""기계 입력 — 고르기 목록 밖 원문 분류번호도 기계 마스터에 있으면 받음."""
|
||||
known = _numbers()
|
||||
picks = spec.get("고르기") or []
|
||||
return bool(picks) and str(value).strip() in known and all(str(o) in known for o in picks)
|
||||
|
||||
|
||||
# ── 단위 ───────────────────────────────────────────────────────────────
|
||||
def kind_of(unit) -> str | None:
|
||||
got = _SCALE.get(mm.plain_unit(unit))
|
||||
return got[0] if got else None
|
||||
|
||||
|
||||
def same_kind(a, b) -> bool:
|
||||
"""단위가 같거나 같은 성격(무게 · 길이 · 넓이 · 부피)."""
|
||||
pa, pb = mm.plain_unit(a), mm.plain_unit(b)
|
||||
return pa == pb or (kind_of(a) is not None and kind_of(a) == kind_of(b))
|
||||
|
||||
|
||||
def convert(price, have, want):
|
||||
"""`have` 단위 단가 → `want` 단위 단가 — 같은 성격이 아니면 그대로."""
|
||||
if price is None or not want or mm.plain_unit(have) == mm.plain_unit(want):
|
||||
return price
|
||||
if not same_kind(have, want) or kind_of(have) is None:
|
||||
return price
|
||||
return price * Decimal(_SCALE[mm.plain_unit(want)][1]) / Decimal(_SCALE[mm.plain_unit(have)][1])
|
||||
|
||||
|
||||
# ── 찾기 ───────────────────────────────────────────────────────────────
|
||||
def _words(term) -> tuple[list[str], list[str]]:
|
||||
"""(이름·규격에 든 낱말, 지역 낱말) — 지역 낱말은 고르는 차례에만 쓰임."""
|
||||
every = [mm.plain_spec(w) for w in str(term or "").split()]
|
||||
region = [w for w in every if w in REGIONS]
|
||||
return [w for w in every if w not in REGIONS], region
|
||||
|
||||
|
||||
def search(master, cond: dict, env: dict | None = None, unit: str | None = None) -> list[str]:
|
||||
"""검색어 안 자재품목 키 — 낱말이 모두 이름 또는 규격에 든 줄(요소의 구분 · 상세구분 안에서) ·
|
||||
단위는 같은 성격까지(하나도 없으면 다 — 경고가 붙음) · 고르는 차례:
|
||||
값 있는 줄 → 줄 단위와 똑같은 단위 → 지역 낱말(입력 `자재지역` 포함)과 맞는 줄 → 키 차례."""
|
||||
words, region = _words(cond.get("검색어"))
|
||||
region += [mm.plain(env[mm.REGION])] if (env or {}).get(mm.REGION) else []
|
||||
rows = master.index.get(mm.MARKET, {})
|
||||
hits = [
|
||||
key
|
||||
for _, name, spec, _, key in mm._market_index(master)
|
||||
if all(w in name.replace("×", "x") or w in spec for w in words)
|
||||
and (not cond.get("구분") or rows[key].get("구분") == cond["구분"])
|
||||
and (not cond.get("상세구분") or rows[key].get("상세구분") == cond["상세구분"])
|
||||
]
|
||||
if unit:
|
||||
kin = [k for k in hits if same_kind(rows[k].get("단위"), unit)]
|
||||
hits = kin or hits
|
||||
want = mm.plain_unit(unit) if unit else None
|
||||
|
||||
def rank(key):
|
||||
one = rows[key]
|
||||
name = mm.plain(one.get("이름"))
|
||||
return (
|
||||
not mm._offers(master, key),
|
||||
bool(want) and mm.plain_unit(one.get("단위")) != want,
|
||||
not (region and all(r in name for r in region)),
|
||||
)
|
||||
|
||||
return sorted(hits, key=rank) # 정렬은 안정 — 남는 차례는 파일 차례
|
||||
|
||||
|
||||
def pick(master, cond: dict, env: dict, unit: str | None = None) -> str:
|
||||
keys = search(master, cond, env, unit)
|
||||
if not keys:
|
||||
raise mm.mf.FormulaError(f"재료 검색어 후보 없음 — {cond.get('검색어')}")
|
||||
return keys[0]
|
||||
|
||||
|
||||
def priced(master, item: dict, ref, env: dict, index: int | None):
|
||||
"""재료 줄 단가 — (단가, 출처, 까닭, 단위경고). 고른 품목(`고름`)이 있으면 그것 · 검색어가 있으면 찾기 ·
|
||||
없으면 옛 길. 자재 단가는 줄 단위로 환산 · 성격이 다르면 그대로 두고 경고.
|
||||
넷째 = 줄에 덧붙일 칸 — `단위경고` · `품목`(잡힌 품목 키 · 이름 · 규격 · 단위 · 값 칸)."""
|
||||
price, source, why, warn = _priced(master, item, ref, env, index)
|
||||
extra = {"단위경고": warn} if warn else {}
|
||||
key, _, slot = (source or "").partition(".")
|
||||
try:
|
||||
row = master.get(key) if key else None
|
||||
except mm.mf.FormulaError:
|
||||
row = None
|
||||
if row:
|
||||
extra["품목"] = {
|
||||
"키": key,
|
||||
"이름": row.get("이름"),
|
||||
"규격": row.get("규격"),
|
||||
"단위": row.get("단위"),
|
||||
"값칸": slot,
|
||||
}
|
||||
return price, source, why, extra
|
||||
|
||||
|
||||
def _priced(master, item: dict, ref, env: dict, index: int | None):
|
||||
unit = item.get("단위")
|
||||
picked = (CHOSEN.get() or {}).get(str(index)) if index is not None else None
|
||||
try:
|
||||
if picked or (isinstance(ref, dict) and ref.get("검색어")):
|
||||
ref = picked or pick(master, ref, env, unit)
|
||||
have = master.get(ref).get("단위") if ref[:2] == mm.MARKET else None
|
||||
price, source = mm.element(master, ref, env, None)
|
||||
warn = None
|
||||
if have and unit and not same_kind(have, unit):
|
||||
warn = f"줄 {unit} · 자재 {have}"
|
||||
price = convert(price, have, unit) if have and unit else price
|
||||
else:
|
||||
price, source = mm.element(master, ref, env, unit)
|
||||
warn = None
|
||||
except mm.mf.FormulaError as e:
|
||||
return None, None, str(e), None
|
||||
why = None if price is not None else f"「{master.label(ref)}」 값 없음 — 관리자가 채울 값"
|
||||
return price, source, why, warn
|
||||
|
||||
|
||||
# ── 검사 ───────────────────────────────────────────────────────────────
|
||||
def check_zero(master, where: str, cond: dict) -> list[str]:
|
||||
"""검색어 후보 0 — `{입력}` 이 든 검색어는 계산 때 채워져 여기서 못 봄."""
|
||||
term = str(cond.get("검색어") or "")
|
||||
if not term.strip() or "{" in term or any("{" in str(v) for v in cond.values()):
|
||||
return [] if term.strip() else [f"{where} · 검색어 비어 있음"]
|
||||
return [] if search(master, cond) else [f"{where} · 검색어 「{term}」 후보 0"]
|
||||
|
||||
|
||||
def check_warn(master, where: str, cond: dict, unit: str | None) -> list[str]:
|
||||
"""단위 경고 줄 — 검색어 후보가 있으나 줄 단위와 같은 성격이 하나도 없음."""
|
||||
term = str(cond.get("검색어") or "")
|
||||
if not unit or "{" in term or "{" in str(cond.get("구분") or ""):
|
||||
return []
|
||||
keys = search(master, cond)
|
||||
rows = master.index.get(mm.MARKET, {})
|
||||
if keys and not any(same_kind(rows[k].get("단위"), unit) for k in keys):
|
||||
return [f"{where} · 검색어 「{term}」 후보 단위가 줄 단위 「{unit}」 과 성격이 다름"]
|
||||
return []
|
||||
|
||||
|
||||
import master_formula as mf # noqa: E402, F401 — 서로 부름 · 이 모듈 이름이 다 선 뒤에 읽음
|
||||
@@ -17,10 +17,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
import master_copy as mc
|
||||
import master_formula as mf
|
||||
import master_text_item as mti
|
||||
|
||||
PLACES = Decimal("0.0001") # 화면 `formatNumber` 와 같음(소수 넷째 자리까지)
|
||||
PRICE_TAIL = {"인력": "노임", "기계": "손료"} # 단가 자리의 이름 꼬리(그 밖은 「단가」)
|
||||
@@ -33,10 +34,13 @@ _CALC_ERRORS = (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueErro
|
||||
|
||||
|
||||
def fmt(value) -> str:
|
||||
"""화면에 보이는 자릿수 그대로 — 소수 넷째 자리까지 · 뒤 0 은 떼고 · 천 단위 쉼표."""
|
||||
"""보이는 자릿수 — 소수 넷째 자리·유효숫자 4 자리 중 많은 쪽 · 뒤 0 뗌 · 천 단위 쉼표."""
|
||||
if not isinstance(value, Decimal):
|
||||
return str(value)
|
||||
cut = value.quantize(PLACES) if value.as_tuple().exponent < -4 else value
|
||||
places = max(4, 3 - value.adjusted()) if value else 4 # 0.00167 → 0.001667
|
||||
cut = value
|
||||
if value.as_tuple().exponent < -places:
|
||||
cut = value.quantize(Decimal(1).scaleb(-places))
|
||||
sign, digits = ("-", -cut) if cut < 0 else ("", cut)
|
||||
whole, _, rest = f"{digits.normalize():f}".partition(".")
|
||||
if "E" in whole or "e" in whole: # 아주 큰 수 — 지수 꼴을 펴서 씀
|
||||
@@ -44,6 +48,19 @@ def fmt(value) -> str:
|
||||
return sign + f"{int(whole):,}" + (f".{rest}" if rest else "")
|
||||
|
||||
|
||||
def fmt_money(value) -> str:
|
||||
"""돈(단가 · 금액)은 원 단위 정수로 보임 — 글만 그렇고 계산 값은 소수 그대로."""
|
||||
if isinstance(value, Decimal):
|
||||
value = value.quantize(Decimal(1), rounding=ROUND_HALF_UP)
|
||||
return fmt(value)
|
||||
|
||||
|
||||
def fmt_price(value) -> str:
|
||||
"""단가 — 정수로 보이되 200 원 미만은 반올림 오차가 커서 유효숫자로."""
|
||||
small = isinstance(value, Decimal) and abs(value) < 200
|
||||
return fmt(value) if small else fmt_money(value)
|
||||
|
||||
|
||||
# ── 식을 글로 — 값 꼴과 이름 꼴 ────────────────────────────────────────
|
||||
def render(node, env: dict, master, depth: int = 0) -> str:
|
||||
"""식 한 마디 → 값 꼴 글. 값이 정해지는 마디(요소 · 찾기 · 로직 · 이름)는 그 수로 바뀜."""
|
||||
@@ -73,7 +90,7 @@ def named(show, real, env: dict, master, depth: int = 0) -> str:
|
||||
if kind == "name":
|
||||
return show[1]
|
||||
if kind == "ref":
|
||||
return _elem_name(master, show[1])
|
||||
return _elem_name(master, show[1], env)
|
||||
if kind == "field":
|
||||
return f"{named(show[1], real[1], env, master, depth)}.{show[2]}"
|
||||
if kind == "neg":
|
||||
@@ -82,8 +99,12 @@ def named(show, real, env: dict, master, depth: int = 0) -> str:
|
||||
return _bin(show, real, env, master, depth, named)
|
||||
if kind == "call":
|
||||
if show[1] == "만약": # 값 꼴과 같은 쪽만 적어 두 칸이 짝이 되게
|
||||
at = 1 if mf.evaluate(real[2][0], env, master, depth) else 2
|
||||
return named(show[2][at], real[2][at], env, master, depth)
|
||||
try:
|
||||
at = 1 if mf.evaluate(real[2][0], env, master, depth) else 2
|
||||
except _CALC_ERRORS: # 조건 입력이 비면 식 그대로(아래 부르기 꼴)
|
||||
at = 0
|
||||
if at:
|
||||
return named(show[2][at], real[2][at], env, master, depth)
|
||||
args = ", ".join(named(s, r, env, master, depth) for s, r in zip(show[2], real[2]))
|
||||
return f"{show[1]}({args})"
|
||||
if kind in ("num", "str"):
|
||||
@@ -91,17 +112,48 @@ def named(show, real, env: dict, master, depth: int = 0) -> str:
|
||||
return _value(real, env, master, depth) # 별칭이 안 붙은 부르기 — 값으로
|
||||
|
||||
|
||||
def _elem_name(master, ref: str) -> str:
|
||||
def _hole(master, ref: str, env: dict | None) -> tuple[list[str], dict | None]:
|
||||
"""자리 표시(`XX:품목:{입력}` · `XX:{입력}`) 든 참조 → (읽는 글 조각들, 입력으로 풀린 요소 줄).
|
||||
글 = 앞 글 + 지금 입력값(「휘발유 · 전국평균」) · 구멍뿐이면 입력 이름이 앞(「장비 · 굴착기」).
|
||||
입력이 비면 입력 이름만(「휘발유 · 지역」) · 못 풀면 줄은 None."""
|
||||
env = env or {}
|
||||
tail = ref[3:]
|
||||
holes = mf._HOLE.findall(tail)
|
||||
words = [w.strip(": ") for w in mf._HOLE.split(tail)[::2] if w.strip(": ")]
|
||||
try:
|
||||
row = master.get(mf.fill(ref, env))
|
||||
except mf.FormulaError:
|
||||
row = None
|
||||
pure = not words and len(holes) == 1
|
||||
shown = []
|
||||
for name in holes:
|
||||
value = env.get(name)
|
||||
if value is None:
|
||||
shown.append(name)
|
||||
elif pure and row is not None: # 입력 하나가 요소 자리 전부 — 그 요소 이름 + 규격
|
||||
shown.append(f"{row.get('이름') or value} {row.get('규격') or ''}".strip())
|
||||
else:
|
||||
shown.append(fmt(value) if isinstance(value, Decimal) else str(value))
|
||||
head = words or holes[:1]
|
||||
return head + [one for one in shown if one not in head], row
|
||||
|
||||
|
||||
def _elem_name(master, ref: str, env: dict | None = None) -> str:
|
||||
"""식 안 요소 한 줄의 이름 — 「석공노임」 꼴(같은 이름의 중간 값과 헷갈리지 않게)."""
|
||||
if (
|
||||
str(ref)[:2] not in ELEM_KEYS
|
||||
): # 별칭 이름이 표 열쇠 그대로인 것(QC…) — 밑이름을 풀어쓰지 않음
|
||||
return ref
|
||||
tail = ELEM_TAIL.get(str(ref)[:2], "단가")
|
||||
try:
|
||||
name = mc.plain(master.get(ref).get("이름"))
|
||||
if "{" in str(ref): # 입력으로 풀린 요소 이름 · 못 풀면 입력 이름
|
||||
parts, row = _hole(master, ref, env)
|
||||
name = mc.short(row.get("이름")) if row is not None else mc.plain("".join(parts))
|
||||
else:
|
||||
name = mc.short(master.get(ref).get("이름"))
|
||||
except mf.FormulaError:
|
||||
name = ""
|
||||
return f"{name}{ELEM_TAIL.get(str(ref)[:2], '단가')}" if name else ref
|
||||
return f"{name}{tail}" if name else ref
|
||||
|
||||
|
||||
def _value(node, env: dict, master, depth: int) -> str:
|
||||
@@ -121,8 +173,20 @@ def _bin(show, real, env: dict, master, depth: int, draw) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _shown(show, real, env: dict, master, depth: int):
|
||||
"""만약() 은 걸린 갈래만 글이 됨 — 괄호를 정할 때는 그 갈래의 마디로 봄(못 가리면 그대로)."""
|
||||
while show[0] == "call" and show[1] == "만약":
|
||||
try:
|
||||
at = 1 if mf.evaluate(real[2][0], env, master, depth) else 2
|
||||
except _CALC_ERRORS:
|
||||
break
|
||||
show, real = show[2][at], real[2][at]
|
||||
return show
|
||||
|
||||
|
||||
def _side(show, real, parent, env: dict, master, depth: int, draw, right: bool) -> str:
|
||||
text = draw(show, real, env, master, depth)
|
||||
show = _shown(show, real, env, master, depth)
|
||||
if show[0] != "bin":
|
||||
return text
|
||||
mine, up = _PREC[show[1]], _PREC[parent[1]]
|
||||
@@ -148,12 +212,17 @@ def _alias_index(marks: list[dict]) -> dict[str, dict]:
|
||||
return {m["이름"]: m for m in marks}
|
||||
|
||||
|
||||
def _elem_piece(master, ref: str) -> dict:
|
||||
"""요소 알약 — 글은 이름(못 찾아도 이름만). 자리 표시(`{…}`) 든 참조 14파일은
|
||||
값(참조 키)을 못 구하므로 이름만 적고 참조 자리는 비움."""
|
||||
piece = {"kind": "요소", "글": _elem_name(master, ref)}
|
||||
def _elem_piece(master, ref: str, env: dict | None = None) -> dict:
|
||||
"""요소 알약 — 글은 이름(못 찾아도 이름만). 자리 표시 든 참조는 글 = 이름 + 지금 입력값 ·
|
||||
참조 = 입력으로 풀린 실제 키(못 풀면 참조 없음)."""
|
||||
if "{" in str(ref):
|
||||
parts, row = _hole(master, ref, env)
|
||||
piece = {"kind": "요소", "글": " · ".join(parts)}
|
||||
if row is not None:
|
||||
key = str(row.get("키") or ref)
|
||||
piece["참조"] = {"테이블": key[:2], "키": key}
|
||||
return piece
|
||||
piece = {"kind": "요소", "글": _elem_name(master, ref)}
|
||||
try:
|
||||
row = master.get(ref)
|
||||
except mf.FormulaError:
|
||||
@@ -163,8 +232,22 @@ def _elem_piece(master, ref: str) -> dict:
|
||||
return piece
|
||||
|
||||
|
||||
def _row_label(table: dict, row: dict, conds: dict) -> list[str]:
|
||||
"""찾은 줄의 이름 — 조건마다 그 줄의 값(범위 칸은 찾은 범위)."""
|
||||
out = []
|
||||
for cond, want in conds.items():
|
||||
have = row.get(cond, want)
|
||||
if table.get("조건", {}).get(cond) == "범위" and isinstance(have, list):
|
||||
low, high = ("" if x is None else fmt(x) for x in have)
|
||||
out.append(f"{low}~{high}")
|
||||
else:
|
||||
out.append(fmt(have))
|
||||
return out
|
||||
|
||||
|
||||
def _alias_piece(mark: dict, env: dict, master, column: str | None) -> dict:
|
||||
"""표 찾기·로직 부르기 알약 — 글은 별칭 이름. 참조는 다시 찾아 봐서 되면만 붙임."""
|
||||
"""표 찾기·로직 부르기 알약 — 표는 글 = 「찾은 줄 · 칸」 · 참조.행 = 찾기 조건.
|
||||
다시 못 찾으면(입력이 비어서 따위) 별칭 이름만 남김."""
|
||||
piece = {"kind": "표" if mark["종류"] == "찾기" else "로직", "글": mark["이름"]}
|
||||
try:
|
||||
node = mf.parse(mark["원문"])
|
||||
@@ -173,32 +256,60 @@ def _alias_piece(mark: dict, env: dict, master, column: str | None) -> dict:
|
||||
table = master.get(ref)
|
||||
conds = {k: mf.evaluate(v, env, master, 0) for k, v in node[2]}
|
||||
row = mf.find(table, conds, column)
|
||||
piece["참조"] = {
|
||||
"테이블": ref[:2],
|
||||
"키": ref,
|
||||
"행": row.get("키") or row.get("원문번호"),
|
||||
"열": column,
|
||||
}
|
||||
tail = column.rpartition("_")[2] if column else ""
|
||||
piece["글"] = " · ".join([*_row_label(table, row, conds), *([tail] if tail else [])])
|
||||
at = next(i for i, one in enumerate(table["줄"]) if one is row) # find 가 고른 줄
|
||||
piece["참조"] = {"테이블": ref[:2], "키": ref, "행": conds, "열": column, "줄": at}
|
||||
else:
|
||||
piece["글"] = str(master.logic(ref).get("이름") or ref) # 요소조각처럼 로직 이름
|
||||
piece["참조"] = {"테이블": ref[:2], "키": ref}
|
||||
except _CALC_ERRORS:
|
||||
pass # 다시 못 찾음 — 별칭 이름만 남김(값 자리 비움)
|
||||
return piece
|
||||
|
||||
|
||||
def _if_pieces(show, real, env: dict, master, alias_by_name: dict, ifdepth: int) -> list[dict]:
|
||||
"""만약() 조각 — 계산되면 걸린 가지만 · 안 되면 양쪽 다(깊이를 적어 접을 수 있게)."""
|
||||
def _ga(word: str) -> str:
|
||||
"""받침이 없으면 「가」 · 있으면 「이」."""
|
||||
last = word[-1:]
|
||||
return "이" if "가" <= last <= "힣" and (ord(last) - 0xAC00) % 28 else "가"
|
||||
|
||||
|
||||
def _easy(show, real, env: dict, master) -> str:
|
||||
if show[0] == "str":
|
||||
return show[1]
|
||||
try:
|
||||
taken = mf.evaluate(real[2][0], env, master, 0)
|
||||
return named(show, real, env, master)
|
||||
except _CALC_ERRORS:
|
||||
deeper = ifdepth + 1
|
||||
left = _pieces(show[2][1], real[2][1], env, master, alias_by_name, deeper)
|
||||
right = _pieces(show[2][2], real[2][2], env, master, alias_by_name, deeper)
|
||||
for one in (*left, *right):
|
||||
one["깊이"] = deeper
|
||||
return [*left, _p_sym("/"), *right]
|
||||
at = 1 if taken else 2
|
||||
return _pieces(show[2][at], real[2][at], env, master, alias_by_name, ifdepth)
|
||||
return ""
|
||||
|
||||
|
||||
def _cond_text(show, real, env: dict, master) -> str:
|
||||
"""만약 조건 → 쉬운 말 — 「폼타이 가 사용 이면」(같음) · 그 밖은 「식 이면」."""
|
||||
if show[0] == "bin" and show[1] == "==":
|
||||
left = _easy(show[2], real[2], env, master)
|
||||
return f"{left} {_ga(left)} {_easy(show[3], real[3], env, master)} 이면"
|
||||
return f"{_easy(show, real, env, master)} 이면"
|
||||
|
||||
|
||||
def _if_pieces(show, real, env: dict, master, alias_by_name: dict, ifdepth: int) -> list[dict]:
|
||||
"""만약() 조각 — 「조건 이면 A · 아니면 B」 · 안 걸린 갈래는 흐림 ·
|
||||
조건이 안 셈해지면(입력이 비어서 따위) 흐림 없이 두 갈래 다(깊이를 적어 접을 수 있게)."""
|
||||
try:
|
||||
taken = bool(mf.evaluate(real[2][0], env, master, 0))
|
||||
except _CALC_ERRORS:
|
||||
taken = None
|
||||
deeper = ifdepth if taken is not None else ifdepth + 1
|
||||
then = _pieces(show[2][1], real[2][1], env, master, alias_by_name, deeper)
|
||||
other = _pieces(show[2][2], real[2][2], env, master, alias_by_name, deeper)
|
||||
other.insert(0, {"kind": "글", "글": "· 아니면"})
|
||||
for one in then if taken is None else ():
|
||||
one["깊이"] = deeper
|
||||
for one in other if taken is None else ():
|
||||
one["깊이"] = deeper
|
||||
for one in then if taken is False else other if taken else ():
|
||||
one["흐림"] = True
|
||||
cond = {"kind": "글", "글": _cond_text(show[2][0], real[2][0], env, master)}
|
||||
return [cond, *then, *other]
|
||||
|
||||
|
||||
def _side_pieces(
|
||||
@@ -237,15 +348,22 @@ def _pieces(show, real, env: dict, master, alias_by_name: dict, ifdepth: int = 0
|
||||
if kind == "call" and show[1] == "만약":
|
||||
return _if_pieces(show, real, env, master, alias_by_name, ifdepth)
|
||||
if kind == "ref":
|
||||
return [_elem_piece(master, show[1])]
|
||||
return [_elem_piece(master, show[1], env)]
|
||||
if kind == "field" and show[1][0] == "name" and show[1][1] in alias_by_name:
|
||||
return [_alias_piece(alias_by_name[show[1][1]], env, master, show[2])]
|
||||
if kind == "name" and show[1] in alias_by_name:
|
||||
return [_alias_piece(alias_by_name[show[1]], env, master, None)]
|
||||
piece = _p_val(show[1] if kind == "name" else "") # 중간 값은 글에 그 이름
|
||||
if kind == "field" and show[1] == ("name", "줄"): # 덧줄이 부른 호표 줄 — 비면 그 줄 이름
|
||||
piece["글"] = show[2]
|
||||
try:
|
||||
return [_p_val(_value(real, env, master, 0))]
|
||||
value = _value(real, env, master, 0)
|
||||
if value == "None": # 값이 빈 줄 — 「None」 을 글로 안 찍음
|
||||
raise mf.FormulaError("값 없음")
|
||||
piece.update({"값": value} if kind == "name" else {"글": value})
|
||||
except _CALC_ERRORS:
|
||||
return [_p_val("")] # 그 밖(값 계산 자체가 막힘) — 빈 값 조각으로 줄이 안 죽게
|
||||
pass # 값 계산 자체가 막힘 — 이름만(그 밖은 빈 글) 남겨 줄이 안 죽게
|
||||
return [piece]
|
||||
|
||||
|
||||
def _formula_pieces(text, env: dict, master, name_of: dict, alias_by_name: dict) -> list[dict]:
|
||||
@@ -260,10 +378,18 @@ def _formula_pieces(text, env: dict, master, name_of: dict, alias_by_name: dict)
|
||||
|
||||
# ── 줄 세우기 ─────────────────────────────────────────────────────────
|
||||
def _env_of(master, row: dict, given: dict, depth: int = 0) -> dict:
|
||||
"""입력 + 중간 — `run` 과 같은 차례로 다시 셈(덧줄 전 상태)."""
|
||||
"""입력 + 중간 — `run` 과 같은 차례로 다시 셈(덧줄 전 상태).
|
||||
아직 안 고른 입력이 있으면(값 넣기 전) 받은 것만 담아 돌려줌 — 이름 식은 값 없이도 지어짐
|
||||
(고른 값이 틀린 경우(고르기·범위 밖)는 지금처럼 그대로 막힘 — `mf._inputs` 가 걸러냄)."""
|
||||
specs = row.get("입력", [])
|
||||
if any(spec["이름"] not in given for spec in specs):
|
||||
return {spec["이름"]: given[spec["이름"]] for spec in specs if spec["이름"] in given}
|
||||
env = mf._inputs(row, given)
|
||||
for step in row.get("중간", []):
|
||||
env[step["이름"]] = mf.evaluate(mf.parse(step["식"]), env, master, depth)
|
||||
try:
|
||||
env[step["이름"]] = mf.evaluate(mf.parse(step["식"]), env, master, depth)
|
||||
except mf.EmptyInput as e: # 못 구한 중간값 — `run` 과 같이 쓰는 줄만 비움
|
||||
env[step["이름"]] = mf.Blank(str(e))
|
||||
return env
|
||||
|
||||
|
||||
@@ -293,29 +419,64 @@ def _shares(line: dict) -> list[tuple[str, Decimal]]:
|
||||
return [(part, money) for part, money in parts if money != 0] or parts[:1]
|
||||
|
||||
|
||||
def _loose(node, env: dict, master) -> bool:
|
||||
"""수량식 맨 위가 덧셈·뺄셈이면(곱 뒤에 오면 괄호가 필요함) · 만약은 걸린 갈래 기준."""
|
||||
if node[0] == "bin":
|
||||
return _PREC[node[1]] < _PREC["*"]
|
||||
if node[0] == "call" and node[1] == "만약":
|
||||
try:
|
||||
at = 1 if mf.evaluate(node[2][0], env, master, 0) else 2
|
||||
except _CALC_ERRORS:
|
||||
return False # 이름 꼴은 「만약(…)」 부르기 그대로라 괄호 불필요
|
||||
return _loose(node[2][at], env, master)
|
||||
return False
|
||||
|
||||
|
||||
def _times(text: str, item: dict, env: dict, master) -> str:
|
||||
"""「단가 * 수량식」 의 수량식 — 덧셈·뺄셈 묶음이면 괄호."""
|
||||
return f"({text})" if _loose(mf.parse(item["수량"]), env, master) else text
|
||||
|
||||
|
||||
def _line_text(item: dict, line: dict, env: dict, master, part: str | None) -> str:
|
||||
"""「단가 * 수량식 = 금액」 — 비목이 나뉜 로직 줄은 그 비목 몫만."""
|
||||
qty = render(mf.parse(item["수량"]), env, master)
|
||||
"""「단가 * 수량식 = 금액」 — 비목이 나뉜 로직 줄은 그 비목 몫만 · 빈 줄(금액 null)은 빈 글."""
|
||||
if line["금액"] is None:
|
||||
return ""
|
||||
qty = _times(render(mf.parse(item["수량"]), env, master), item, env, master)
|
||||
money = line["비목"][part] if part else line["금액"]
|
||||
price = line["단가"] if part is None or line["수량"] == 0 else money / line["수량"]
|
||||
return f"{fmt(price)} * {qty} = {fmt(money)}"
|
||||
return f"{fmt_price(price)} * {qty} = {fmt_money(money)}"
|
||||
|
||||
|
||||
def _line_names(item: dict, line: dict, env: dict, master, part: str | None, name_of: dict) -> str:
|
||||
def _item_names(items: list) -> list[tuple[str, str]]:
|
||||
"""호표 줄마다 (짧은 품명(규격·코드 뗌), 겹침 꼬리) — 한 로직 안에서 겹치면 꼬리 `_2`."""
|
||||
taken: set[str] = set()
|
||||
out = []
|
||||
for item in items:
|
||||
name = mc.short(item.get("이름")) or "값"
|
||||
out.append((name, mc._free(name, taken)[len(name) :]))
|
||||
return out
|
||||
|
||||
|
||||
def _line_names(
|
||||
item: dict, env: dict, master, part: str | None, name_of: dict, label: tuple[str, str]
|
||||
) -> str:
|
||||
"""같은 줄을 변수 이름으로 — 「석공노임 * 메쌓기인력.석공 * (1 + 증가율) = 석공비」."""
|
||||
name = mc.plain(item.get("이름") or line.get("이름")) or "값"
|
||||
name, dup = label
|
||||
tail = PRICE_TAIL.get(str(item.get("종류") or ""), "단가")
|
||||
price = f"{name}{part or ''}{tail}"
|
||||
money = f"{name}{part}" if part else f"{name}비"
|
||||
return f"{price} * {_names(item['수량'], env, master, name_of)} = {money}"
|
||||
price = f"{name}{part or ''}{tail}{dup}"
|
||||
money = f"{name}{part}{dup}" if part else f"{name}비{dup}"
|
||||
qty = _times(_names(item["수량"], env, master, name_of), item, env, master)
|
||||
return f"{price} * {qty} = {money}"
|
||||
|
||||
|
||||
def _extra_text(extra: dict, line: dict, env: dict, master) -> str:
|
||||
return f"{render(mf.parse(extra['식']), env, master)} = {fmt(line['금액'])}"
|
||||
if line["금액"] is None: # 빈 덧줄 — 식을 다시 셈하지 않음
|
||||
return ""
|
||||
return f"{render(mf.parse(extra['식']), env, master)} = {fmt_money(line['금액'])}"
|
||||
|
||||
|
||||
def _extra_names(extra: dict, env: dict, master, name_of: dict) -> str:
|
||||
got = mc.plain(extra.get("이름")) or "덧줄"
|
||||
got = mc.short(extra.get("이름")) or "덧줄"
|
||||
return f"{_names(extra['식'], env, master, name_of)} = {got}"
|
||||
|
||||
|
||||
@@ -346,12 +507,16 @@ def lines(files: dict, key: str, given: dict) -> dict:
|
||||
|
||||
if "결과" in row:
|
||||
조각 = _formula_pieces(row["결과"]["식"], env, master, name_of, alias_by_name)
|
||||
got = mc.plain(row.get("이름")) or "결과"
|
||||
got = mc.short(row.get("이름")) or "결과"
|
||||
shown = (
|
||||
fmt_money if str(row.get("결과단위") or "").startswith("원") else fmt
|
||||
) # 돈 결과만 정수
|
||||
이름글 = f"{_names(row['결과']['식'], env, master, name_of)} = {got}"
|
||||
if result is not None:
|
||||
one = {
|
||||
"이름": row.get("이름") or "",
|
||||
"글": f"{render(mf.parse(row['결과']['식']), env, master)} = {fmt(result['결과'])}",
|
||||
"글": f"{render(mf.parse(row['결과']['식']), env, master)} = "
|
||||
f"{shown(result['결과'])}",
|
||||
"이름글": 이름글,
|
||||
"금액": result["결과"],
|
||||
"조각": 조각,
|
||||
@@ -377,19 +542,22 @@ def lines(files: dict, key: str, given: dict) -> dict:
|
||||
|
||||
made: dict[str, list] = {name: [] for name in mf.COST_ITEMS}
|
||||
items = row.get("호표") or []
|
||||
labels = _item_names(items)
|
||||
if result is not None:
|
||||
shown = result["줄"]
|
||||
env["줄"] = {r["이름"]: r["금액"] for r in shown if r["이름"]}
|
||||
for item, line in zip(items, shown):
|
||||
for n, (item, line, label) in enumerate(zip(items, shown, labels)):
|
||||
조각 = _formula_pieces(item["수량"], env, master, name_of, alias_by_name)
|
||||
요소 = _item_pieces(item, env, master, line, n)
|
||||
for part, money in _shares(line):
|
||||
split = part if len(line["비목"]) > 1 else None
|
||||
one = {
|
||||
"이름": _label(item, line),
|
||||
"글": _line_text(item, line, env, master, split),
|
||||
"이름글": _line_names(item, line, env, master, split, name_of),
|
||||
"이름글": _line_names(item, env, master, split, name_of, label),
|
||||
"금액": money,
|
||||
"조각": 조각,
|
||||
"요소조각": 요소,
|
||||
}
|
||||
why = _why(item, line)
|
||||
made[part].append({**one, "까닭": why} if why else one)
|
||||
@@ -398,13 +566,14 @@ def lines(files: dict, key: str, given: dict) -> dict:
|
||||
끝수 = _cut_note(row)
|
||||
계 = result["계"]
|
||||
else:
|
||||
for item in items:
|
||||
for n, (item, label) in enumerate(zip(items, labels)):
|
||||
parts = (
|
||||
list(mf.COST_ITEMS)
|
||||
if item.get("종류") == "로직" and "비목" not in item
|
||||
else [item.get("비목")]
|
||||
)
|
||||
조각 = _formula_pieces(item["수량"], env, master, name_of, alias_by_name)
|
||||
요소 = _item_pieces(item, env, master, None, n)
|
||||
for part in parts:
|
||||
if part not in mf.COST_ITEMS:
|
||||
continue
|
||||
@@ -413,9 +582,10 @@ def lines(files: dict, key: str, given: dict) -> dict:
|
||||
{
|
||||
"이름": _label(item, {}),
|
||||
"글": "",
|
||||
"이름글": _line_names(item, {}, env, master, split, name_of),
|
||||
"이름글": _line_names(item, env, master, split, name_of, label),
|
||||
"금액": None,
|
||||
"조각": 조각,
|
||||
"요소조각": 요소,
|
||||
}
|
||||
)
|
||||
for extra in row.get("덧줄") or []:
|
||||
@@ -425,7 +595,7 @@ def lines(files: dict, key: str, given: dict) -> dict:
|
||||
"글": "",
|
||||
"이름글": (
|
||||
f"{_names(extra['식'], env, master, name_of)} = "
|
||||
f"{mc.plain(extra.get('이름')) or '덧줄'}"
|
||||
f"{mc.short(extra.get('이름')) or '덧줄'}"
|
||||
),
|
||||
"금액": None,
|
||||
"조각": _formula_pieces(extra["식"], env, master, name_of, alias_by_name),
|
||||
@@ -443,6 +613,45 @@ def lines(files: dict, key: str, given: dict) -> dict:
|
||||
return _paired({"ok": True, "groups": groups, "계": 계, "별칭": marks})
|
||||
|
||||
|
||||
def _arg_text(node, env: dict, master) -> str:
|
||||
"""로직 부르기 인자 값을 사람이 읽는 글로 — 요소 키는 이름 · 입력 자리는 지금 값."""
|
||||
try:
|
||||
value = mf.evaluate(node, env, master, 0)
|
||||
except _CALC_ERRORS: # 값 없는 입력 자리 — 이름만
|
||||
return str(node[1]) if node[0] in ("name", "str") else ""
|
||||
if isinstance(value, str):
|
||||
for ref in (value, f"EQ:{value}"): # 기계 입력은 원문번호(0602-0025)
|
||||
try:
|
||||
return mc.plain(master.get(ref).get("이름")) or value
|
||||
except mf.FormulaError:
|
||||
pass
|
||||
return value
|
||||
return fmt(value)
|
||||
|
||||
|
||||
def _item_pieces(
|
||||
item: dict, env: dict, master, line: dict | None = None, index: int = 0
|
||||
) -> list[dict]:
|
||||
"""줄의 단가 자리(요소 · 로직 부르기) 조각 — PLAN 3-1 요소조각."""
|
||||
ref = item.get("요소")
|
||||
if item.get("종류") != "로직":
|
||||
name = str(item.get("이름") or "")
|
||||
if isinstance(ref, dict): # 재료 고르기 조건 — 잡힌 품목 이름 · 참조(고름 · 검색어 · 대표)
|
||||
found = mti.piece(item, line, index, env, master)
|
||||
return [found or {"kind": "요소", "글": name}]
|
||||
piece = _elem_piece(master, ref, env)
|
||||
return [{**piece, "글": name or piece["글"]}]
|
||||
try:
|
||||
node = mf.parse(ref)
|
||||
key = mf.fill(node[1], env)
|
||||
name = str(master.logic(key).get("이름") or key)
|
||||
except _CALC_ERRORS:
|
||||
return [{"kind": "로직", "글": str(item.get("이름") or "")}]
|
||||
args = " · ".join(f"{k} {_arg_text(v, env, master)}" for k, v in node[2])
|
||||
out = [{"kind": "로직", "글": name, "참조": {"테이블": key[:2], "키": key}}]
|
||||
return out + ([{"kind": "글", "글": args}] if args else [])
|
||||
|
||||
|
||||
def _paired(answer: dict) -> dict:
|
||||
"""줄마다 짝 번호 — 두 칸(이름 꼴 · 값 꼴)이 같은 차례로 놓이게 1 부터."""
|
||||
at = 0
|
||||
@@ -450,6 +659,7 @@ def _paired(answer: dict) -> dict:
|
||||
for one in group["줄"]:
|
||||
at += 1
|
||||
one["짝"] = at
|
||||
one.setdefault("요소조각", [])
|
||||
return answer
|
||||
|
||||
|
||||
@@ -457,7 +667,9 @@ def _extras(
|
||||
row: dict, shown: list, made: dict, env: dict, master, name_of: dict, alias_by_name: dict
|
||||
) -> None:
|
||||
"""덧줄 — 앞선 줄까지의 비목 합을 넣고 한 줄씩(식 안의 비목 이름은 그 수로 바뀜)."""
|
||||
sums = {name: sum((one["금액"] for one in made[name]), Decimal(0)) for name in mf.COST_ITEMS}
|
||||
sums = {
|
||||
name: sum((one["금액"] or 0 for one in made[name]), Decimal(0)) for name in mf.COST_ITEMS
|
||||
}
|
||||
for extra, line in zip(row.get("덧줄") or [], shown):
|
||||
env.update(sums)
|
||||
one = {
|
||||
@@ -468,8 +680,9 @@ def _extras(
|
||||
"조각": _formula_pieces(extra["식"], env, master, name_of, alias_by_name),
|
||||
}
|
||||
made[extra["비목"]].append(one)
|
||||
sums[extra["비목"]] += line["금액"]
|
||||
env[extra["이름"]] = line["금액"]
|
||||
if line["금액"] is not None: # 빈 덧줄은 합·이름 값에 안 넣음
|
||||
sums[extra["비목"]] += line["금액"]
|
||||
env[extra["이름"]] = line["금액"]
|
||||
|
||||
|
||||
def _cut_note(row: dict, on: str = "계") -> str:
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""재료 줄 요소조각 — 잡힌 자재품목 이름 · 참조(PLAN 8-5). `master_text._item_pieces` 가 부름."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
import master_formula as mf
|
||||
import master_pick as mp
|
||||
|
||||
_ERRORS = (mf.FormulaError, KeyError, TypeError)
|
||||
|
||||
|
||||
def picked(item: dict, line: dict | None, index: int, env: dict, master) -> dict | None:
|
||||
"""재료 줄이 잡은 자재품목 줄 — 계산한 줄의 출처(`MT000018.거래가격`) 먼저 ·
|
||||
빈 줄이면 고름 · 검색어 · 대표로 다시 찾음. 못 찾으면 None."""
|
||||
got = str((line or {}).get("출처") or "")[:8]
|
||||
try:
|
||||
if got[:2] == "MT":
|
||||
return master.get(got)
|
||||
cond = {k: mf.fill(v, env) if isinstance(v, str) else v for k, v in item["요소"].items()}
|
||||
key = (mp.CHOSEN.get() or {}).get(str(index))
|
||||
if not key and cond.get("검색어"):
|
||||
key = mp.pick(master, cond, env, item.get("단위"))
|
||||
key = key or cond.get("대표")
|
||||
return master.get(str(key)) if key and str(key)[:2] == "MT" else None
|
||||
except _ERRORS:
|
||||
return None
|
||||
|
||||
|
||||
def piece(item: dict, line: dict | None, index: int, env: dict, master) -> dict | None:
|
||||
"""요소조각 — 글 = 품목 이름(지역 포함) · 참조 = 테이블 MT · 그 키. 못 찾으면 None."""
|
||||
row = picked(item, line, index, env, master)
|
||||
if row is None:
|
||||
return None
|
||||
key = str(row["키"])
|
||||
name = re.sub(r"\s+", " ", str(row.get("이름") or "")).strip()
|
||||
return {"kind": "요소", "글": name, "참조": {"테이블": key[:2], "키": key}}
|
||||
@@ -3173,7 +3173,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "도료"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "도료", "검색어": "신구접착및방청제 LDS-MP"},
|
||||
"이름": "프라이머(융착식 도료)",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC000546).프라이머 / 10",
|
||||
@@ -3182,7 +3182,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "일반용", "대표": "MT003175"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "프로판가스 일반용"},
|
||||
"이름": "프로판가스(건설용 LPG)",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC000546).프로판가스 / 10",
|
||||
@@ -4278,7 +4278,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철망·와이어메쉬"},
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철망·와이어메쉬", "검색어": "철망 Φ3.2."},
|
||||
"이름": "철망(낙석방지망)",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC000569, 구분='철망').수량",
|
||||
@@ -4287,7 +4287,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철선"},
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철선", "검색어": "보통철선 #8 서울"},
|
||||
"이름": "결속선",
|
||||
"단위": "m",
|
||||
"수량": "찾기(QC000569, 구분='결속선').수량",
|
||||
@@ -4296,7 +4296,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "도료", "규격": "M-Poxy I(습식)", "대표": "MT007705"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "도료", "검색어": "주입용에폭시 M-Poxy I(습식)"},
|
||||
"이름": "에폭시",
|
||||
"단위": "kg",
|
||||
"수량": "만약(방지망 == '포켓식', 찾기(QC000569, 구분='에폭시').수량, 0)",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98%"},
|
||||
"이름": "동 및 동합금용 용접봉",
|
||||
"규격": "BCUP3",
|
||||
"단위": "㎏",
|
||||
@@ -116,7 +116,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98%"},
|
||||
"이름": "플럭스",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001401, '규격(㎜)'=규격).플럭스 / 1000",
|
||||
@@ -125,7 +125,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "6000ℓ/병", "대표": "MT003165"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "산소 6000ℓ/병"},
|
||||
"이름": "산소가스",
|
||||
"단위": "ℓ",
|
||||
"수량": "찾기(QC001401, '규격(㎜)'=규격).산소",
|
||||
@@ -134,7 +134,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "98% (용접용)", "대표": "MT003163"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98% (용접용)"},
|
||||
"이름": "아세틸렌가스",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001401, '규격(㎜)'=규격).아세틸렌 / 1000",
|
||||
@@ -194,7 +194,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98%"},
|
||||
"이름": "TIG 용접봉",
|
||||
"규격": "스테인리스",
|
||||
"단위": "㎏",
|
||||
@@ -204,7 +204,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98%"},
|
||||
"이름": "아르곤가스",
|
||||
"단위": "ℓ",
|
||||
"수량": "찾기(QC001404, '규격(㎜)'=규격).Argon",
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "6000ℓ/병", "대표": "MT003165"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "산소 6000ℓ/병"},
|
||||
"이름": "산소",
|
||||
"단위": "L",
|
||||
"수량": "만약(부재 == 'H형강부재', 찾기(QC001188, 재료='산소').H형강부재, 찾기(QC001188, 재료='산소').전용접부재) * 1000",
|
||||
@@ -76,7 +76,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "대표": "MT003176"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "LPG 가스"},
|
||||
"이름": "L.P.G",
|
||||
"단위": "kg",
|
||||
"수량": "만약(부재 == 'H형강부재', 찾기(QC001188, 재료='L.P.G').H형강부재, 찾기(QC001188, 재료='L.P.G').전용접부재)",
|
||||
@@ -86,7 +86,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "볼트·너트"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "볼트·너트", "검색어": "세파볼트 D형"},
|
||||
"이름": "서비스볼트",
|
||||
"단위": "본",
|
||||
"수량": "만약(부재 == 'H형강부재', 찾기(QC001188, 재료='서비스볼트').H형강부재, 찾기(QC001188, 재료='서비스볼트').전용접부재)",
|
||||
@@ -95,7 +95,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "철근·강재", "상세구분": "형강", "대표": "MT030619"},
|
||||
"요소": {"구분": "철근·강재", "상세구분": "형강", "검색어": "보조강재"},
|
||||
"이름": "보조강재",
|
||||
"단위": "㎏",
|
||||
"수량": "만약(부재 == 'H형강부재', 찾기(QC001188, 재료='보조강재').H형강부재, 찾기(QC001188, 재료='보조강재').전용접부재)",
|
||||
@@ -175,7 +175,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "산소 KS E4301", "대표": "MT003187"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "용접봉 산소 KS E4301"},
|
||||
"이름": "용접봉",
|
||||
"단위": "kg",
|
||||
"수량": "만약(용접방식 == '수용접', 환산용접길이 * 찾기(QC001190, 재료='용접봉').수용접, 0)",
|
||||
@@ -185,7 +185,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98%"},
|
||||
"이름": "CO_2 와이어",
|
||||
"단위": "㎏",
|
||||
"수량": "만약(용접방식 == '반자동용접', 환산용접길이 * 찾기(QC001190, 재료='CO_2 와이어').반자동용접, 0)",
|
||||
@@ -194,7 +194,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98%"},
|
||||
"이름": "탄산가스",
|
||||
"단위": "㎏",
|
||||
"수량": "만약(용접방식 == '반자동용접', 환산용접길이 * 찾기(QC001190, 재료='탄산가스').반자동용접, 0)",
|
||||
@@ -203,7 +203,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98%"},
|
||||
"이름": "잠호용접와이어",
|
||||
"단위": "㎏",
|
||||
"수량": "만약(용접방식 == '자동용접', 환산용접길이 * 찾기(QC001190, 재료='잠호용접와이어').자동용접, 0)",
|
||||
@@ -212,7 +212,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "대표": "MT030620"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "FLUX"},
|
||||
"이름": "FLUX",
|
||||
"단위": "㎏",
|
||||
"수량": "만약(용접방식 == '자동용접', 환산용접길이 * 찾기(QC001190, 재료='FLUX').자동용접, 0)",
|
||||
@@ -332,7 +332,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "볼트·너트"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "볼트·너트", "검색어": "세파볼트 D형"},
|
||||
"이름": "보통볼트",
|
||||
"단위": "본",
|
||||
"수량": "찾기(QC001192, 구분='보통볼트', 규격='가조임').수량",
|
||||
@@ -648,7 +648,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98%"},
|
||||
"이름": "CO_2 와이어",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001197, 구분='CO_2 와이어').수량",
|
||||
@@ -657,7 +657,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98%"},
|
||||
"이름": "탄산가스",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001197, 구분='탄산가스').수량",
|
||||
@@ -835,7 +835,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "6000ℓ/병", "대표": "MT003165"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "산소 6000ℓ/병"},
|
||||
"이름": "산소",
|
||||
"단위": "L",
|
||||
"수량": "만약(가스 == '아세틸렌', 찾기(QC001202, '판두께(㎜)'=판두께).산소, 0) * 1000",
|
||||
@@ -845,7 +845,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "98% (용접용)", "대표": "MT003163"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98% (용접용)"},
|
||||
"이름": "아세틸렌",
|
||||
"단위": "kg",
|
||||
"수량": "만약(가스 == '아세틸렌', 찾기(QC001202, '판두께(㎜)'=판두께).아세틸렌, 0)",
|
||||
@@ -854,7 +854,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "대표": "MT003176"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "LPG 가스"},
|
||||
"이름": "L.P.G",
|
||||
"단위": "kg",
|
||||
"수량": "만약(가스 == 'L.P.G', 찾기(QC001202, '판두께(㎜)'=판두께).'L.P.G', 0)",
|
||||
@@ -1198,7 +1198,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "단열·보온", "상세구분": "단열재", "대표": "MT030621"},
|
||||
"요소": {"구분": "단열·보온", "상세구분": "단열재", "검색어": "질석"},
|
||||
"이름": "질석",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001208, 구분='질석').수량",
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"호표": [
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "규격": "두께 2.0㎜ 이상", "대표": "MT030612"},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "BASECHANNEL 두께 2.0㎜ 이상"},
|
||||
"이름": "BASECHANNEL 두께:2.0㎜이상",
|
||||
"단위": "m",
|
||||
"수량": "찾기(QC000012, 구분='BASECHANNEL', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -55,7 +55,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "규격": "두께 2.0㎜ 이상", "대표": "MT030613"},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "TOPCHANNEL 두께 2.0㎜ 이상"},
|
||||
"이름": "TOPCHANNEL 두께:2.0㎜이상",
|
||||
"단위": "m",
|
||||
"수량": "찾기(QC000012, 구분='TOPCHANNEL', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -63,10 +63,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재"
|
||||
},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "T챤넬(K-SPAN) 4.5×610×2440"},
|
||||
"이름": "외부PANEL(벽) 1,200×2,400㎜",
|
||||
"단위": "매",
|
||||
"수량": "찾기(QC000012, 구분='외부PANEL(벽)', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -74,10 +71,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재"
|
||||
},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "T챤넬(K-SPAN) 4.5×610×2440"},
|
||||
"이름": "외부PANEL (창문) 1,200×2,400㎜",
|
||||
"단위": "매",
|
||||
"수량": "찾기(QC000012, 구분='외부PANEL (창문)', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -85,10 +79,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재"
|
||||
},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "T챤넬(K-SPAN) 4.5×610×2440"},
|
||||
"이름": "외부PANEL (철재문) 1,200×2,400㎜",
|
||||
"단위": "매",
|
||||
"수량": "찾기(QC000012, 구분='외부PANEL (철재문)', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -96,10 +87,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재"
|
||||
},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "T챤넬(K-SPAN) 4.5×610×2440"},
|
||||
"이름": "내부PANEL(벽) 1,200×2,400㎜",
|
||||
"단위": "매",
|
||||
"수량": "만약(종류 == '사무실', 찾기(QC000012, 구분='내부PANEL(벽)', 종류=주자재종류).수량 * 손율 / 100, 0)",
|
||||
@@ -107,7 +95,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재", "규격": "1,200×2,400㎜", "대표": "MT030614"},
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재", "검색어": "내부PANEL (목재문) 1,200×2,400㎜"},
|
||||
"이름": "내부PANEL (목재문) 1,200×2,400㎜",
|
||||
"단위": "매",
|
||||
"수량": "만약(종류 == '사무실', 찾기(QC000012, 구분='내부PANEL (목재문)', 종류=주자재종류).수량 * 손율 / 100, 0)",
|
||||
@@ -115,10 +103,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재"
|
||||
},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "판넬조인트 L=2400"},
|
||||
"이름": "PANEL JOINT(AL-BAR) L=2,400㎜",
|
||||
"단위": "조",
|
||||
"수량": "찾기(QC000012, 구분='PANEL JOINT(AL-BAR)', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -126,10 +111,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재"
|
||||
},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "T챤넬(K-SPAN) 4.5×610×2440"},
|
||||
"이름": "CANOPY(출입구채양) 600×1,200㎜",
|
||||
"단위": "매",
|
||||
"수량": "찾기(QC000012, 구분='CANOPY(출입구채양)', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -137,10 +119,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재"
|
||||
},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "T챤넬(K-SPAN) 4.5×610×2440"},
|
||||
"이름": "박공PANEL",
|
||||
"단위": "매",
|
||||
"수량": "찾기(QC000012, 구분='박공PANEL', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -148,11 +127,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재",
|
||||
"대표": "MT007262"
|
||||
},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "지붕판 (ROOF SHEET) 0.5T"},
|
||||
"이름": "ROOFSHEET 0.5㎜COLORSHEET",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC000012, 구분='ROOFSHEET', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -160,7 +135,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "철근·강재", "상세구분": "형강"},
|
||||
"요소": {"구분": "철근·강재", "상세구분": "형강", "검색어": "트러스 (TRUSS) SPAN:L=7.2"},
|
||||
"이름": "트러스 L=7.2m",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000012, 구분='트러스', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -168,7 +143,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "철근·강재", "상세구분": "형강"},
|
||||
"요소": {"구분": "철근·강재", "상세구분": "형강", "검색어": "트러스 (TRUSS) SPAN:L=7.2"},
|
||||
"이름": "중도리(PURIN) 두께:2.0이상",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000012, 구분='중도리(PURIN)', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -176,7 +151,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재"},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "T챤넬(K-SPAN) 4.5×610×2440"},
|
||||
"이름": "천장판 미장합판+50㎜ GLASSWOOL",
|
||||
"단위": "매",
|
||||
"수량": "만약(종류 == '사무실', 찾기(QC000012, 구분='천장판', 종류=주자재종류).수량 * 손율 / 100, 0)",
|
||||
@@ -184,7 +159,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재"},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "처마후레싱 C/S"},
|
||||
"이름": "T-BAR",
|
||||
"단위": "m",
|
||||
"수량": "만약(종류 == '사무실', 찾기(QC000012, 구분='T-BAR', 종류=주자재종류).수량 * 손율 / 100, 0)",
|
||||
@@ -238,7 +213,7 @@
|
||||
"호표": [
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "가설건축물"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "가설건축물", "검색어": "콘테이너(사무실) 12.0*3.0*2.6m"},
|
||||
"이름": "콘테이너형 가설건축물 3.0x3.0m",
|
||||
"단위": "동",
|
||||
"수량": "만약(규모 == '3.0x3.0m', 손율 / 100, 0)",
|
||||
@@ -246,7 +221,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "가설건축물"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "가설건축물", "검색어": "콘테이너(사무실) 12.0*3.0*2.6m"},
|
||||
"이름": "콘테이너형 가설건축물 3.0x6.0m",
|
||||
"단위": "동",
|
||||
"수량": "만약(규모 == '3.0x6.0m', 손율 / 100, 0)",
|
||||
@@ -254,7 +229,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "가설건축물"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "가설건축물", "검색어": "콘테이너(사무실) 12.0*3.0*2.6m"},
|
||||
"이름": "콘테이너형 가설건축물 3.0x9.0m",
|
||||
"단위": "동",
|
||||
"수량": "만약(규모 == '3.0x9.0m', 손율 / 100, 0)",
|
||||
@@ -291,7 +266,7 @@
|
||||
"호표": [
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "측정·시험기기", "대표": "MT030615"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "측정·시험기기", "검색어": "축중계(이동식)"},
|
||||
"이름": "축중계(이동식)",
|
||||
"단위": "대",
|
||||
"수량": "찾기(QC000018, 구분='손율', 기간=기간).값 / 100",
|
||||
@@ -1198,7 +1173,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재"},
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재", "검색어": "미송각재 30×30×3600mm 서울"},
|
||||
"이름": "목재",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QC000038, 구분='목재', 종별=종별).수량 * 찾기(QC000019, 규준틀='목재규준틀').손율 / 100",
|
||||
@@ -2056,7 +2031,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "규격": "1219×1700mm", "대표": "MT007846"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "검색어": "수직문형지주(조립식틀비계) 1219×1700mm"},
|
||||
"이름": "비계기본틀(기둥) H1700×W1219",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000063, 구분='비계기본틀(기둥)', 규격='H1700×W1219').수량 * 찾기(QC000016, 공기=공기, 재료='강관, 비계기본틀, 비계장선틀, 가새').손율 / 100",
|
||||
@@ -2072,7 +2047,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "검색어": "안전발판(PSP대용) 250×3000"},
|
||||
"이름": "수평띠장 L1829",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000063, 구분='수평띠장', 규격='L1829').수량 * 찾기(QC000016, 공기=공기, 재료='강관, 비계기본틀, 비계장선틀, 가새').손율 / 100",
|
||||
@@ -2080,7 +2055,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "검색어": "안전발판(PSP대용) 250×3000"},
|
||||
"이름": "손잡이기둥",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000063, 구분='손잡이기둥').수량 * 찾기(QC000016, 공기=공기, 재료='강관, 비계기본틀, 비계장선틀, 가새').손율 / 100",
|
||||
@@ -2088,7 +2063,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "검색어": "안전발판(PSP대용) 250×3000"},
|
||||
"이름": "손잡이 L1219",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000063, 구분='손잡이', 규격='L1219').수량 * 찾기(QC000016, 공기=공기, 재료='강관, 비계기본틀, 비계장선틀, 가새').손율 / 100",
|
||||
@@ -2096,7 +2071,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "검색어": "안전발판(PSP대용) 250×3000"},
|
||||
"이름": "손잡이 L1829",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000063, 구분='손잡이', 규격='L1829').수량 * 찾기(QC000016, 공기=공기, 재료='강관, 비계기본틀, 비계장선틀, 가새').손율 / 100",
|
||||
@@ -2104,7 +2079,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "검색어": "안전발판(PSP대용) 250×3000"},
|
||||
"이름": "바퀴",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000063, 구분='바퀴').수량 * 찾기(QC000016, 공기=공기, 재료='받침철물 조절받침철물 비계안전발판').손율 / 100",
|
||||
@@ -2112,7 +2087,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "검색어": "안전발판(PSP대용) 250×3000"},
|
||||
"이름": "자키",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000063, 구분='자키').수량 * 찾기(QC000016, 공기=공기, 재료='받침철물 조절받침철물 비계안전발판').손율 / 100",
|
||||
@@ -2120,7 +2095,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "규격": "45×200×2000"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "검색어": "발판 45×200×2000"},
|
||||
"이름": "발판 45×200×2000",
|
||||
"단위": "장",
|
||||
"수량": "찾기(QC000063, 구분='발판', 규격='45×200×2000').수량 * 찾기(QC000016, 공기=공기, 재료='받침철물 조절받침철물 비계안전발판').손율 / 100",
|
||||
@@ -2353,15 +2328,6 @@
|
||||
],
|
||||
"출처": "건설품셈 공통 2-2-5",
|
||||
"비고": "강관·부속철물 손율 기간"
|
||||
},
|
||||
{
|
||||
"이름": "철선",
|
||||
"고르기": [
|
||||
"MT000199",
|
||||
"MT000204",
|
||||
"MT000159",
|
||||
"MT000164"
|
||||
]
|
||||
}
|
||||
],
|
||||
"중간": [
|
||||
@@ -2398,7 +2364,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "검색어": "강관비계 Φ48.6 서울"},
|
||||
"이름": "강관 ø48.6㎜×2.4㎜",
|
||||
"단위": "m",
|
||||
"수량": "찾기(QC000071, 구분='강관', 규격='ø48.6㎜×2.4㎜').수량 * 찾기(QC000016, 공기=공기, 재료='강관, 비계기본틀, 비계장선틀, 가새').손율 / 100",
|
||||
@@ -2406,7 +2372,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "검색어": "안전발판(PSP대용) 250×3000"},
|
||||
"이름": "브라켓",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000071, 구분='브라켓').수량 * 찾기(QC000016, 공기=공기, 재료='조임철물 이음철물').손율 / 100",
|
||||
@@ -2414,7 +2380,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철선", "대표": "{철선}"},
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철선", "검색어": "어닐링철선 #8 서울"},
|
||||
"이름": "철선",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC000071, 구분='철선').수량",
|
||||
@@ -2422,7 +2388,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "와이어로프·클램프"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "와이어로프·클램프", "검색어": "데크고정클립(NS클립)"},
|
||||
"이름": "클램프",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000071, 구분='클램프').수량 * 찾기(QC000016, 공기=공기, 재료='조임철물 이음철물').손율 / 100",
|
||||
@@ -2430,7 +2396,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유"},
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유", "검색어": "직포 옥상방수용직포"},
|
||||
"이름": "그물망",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC000071, 구분='그물망').수량",
|
||||
@@ -2509,7 +2475,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "검색어": "강관비계 Φ48.6 서울"},
|
||||
"이름": "강관 ø48.6㎜×2.4㎜",
|
||||
"단위": "m",
|
||||
"수량": "찾기(QC000074, 구분='강관', 규격='ø48.6㎜×2.4㎜').수량 * 찾기(QC000016, 공기=공기, 재료='강관, 비계기본틀, 비계장선틀, 가새').손율 / 100",
|
||||
@@ -2517,7 +2483,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "검색어": "안전발판(PSP대용) 250×3000"},
|
||||
"이름": "브라켓",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000074, 구분='브라켓').수량 * 찾기(QC000016, 공기=공기, 재료='조임철물 이음철물').손율 / 100",
|
||||
@@ -2525,7 +2491,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "검색어": "강관비계 Φ48.6 서울"},
|
||||
"이름": "사다리 폭 30㎝×길이 3m 기준",
|
||||
"단위": "m",
|
||||
"수량": "찾기(QC000074, 구분='사다리', 규격='폭 30㎝×길이 3m 기준').수량 * 찾기(QC000016, 공기=공기, 재료='강관, 비계기본틀, 비계장선틀, 가새').손율 / 100",
|
||||
@@ -2533,7 +2499,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "와이어로프·클램프", "규격": "6mm", "대표": "MT007180"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "와이어로프·클램프", "검색어": "와이어로프(철심) 6mm"},
|
||||
"이름": "와이어로프 ø6",
|
||||
"단위": "m",
|
||||
"수량": "찾기(QC000074, 구분='와이어로프', 규격='ø6').수량 * 찾기(QC000016, 공기=공기, 재료='조임철물 이음철물').손율 / 100",
|
||||
@@ -2541,7 +2507,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "와이어로프·클램프"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "와이어로프·클램프", "검색어": "데크고정클립(NS클립)"},
|
||||
"이름": "클램프",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000074, 구분='클램프').수량 * 찾기(QC000016, 공기=공기, 재료='조임철물 이음철물').손율 / 100",
|
||||
@@ -2549,7 +2515,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유"},
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유", "검색어": "직포 옥상방수용직포"},
|
||||
"이름": "그물망",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC000074, 구분='그물망').수량",
|
||||
@@ -2919,14 +2885,6 @@
|
||||
0.4
|
||||
],
|
||||
"출처": "건설품셈 공통 2-8-7 재료량"
|
||||
},
|
||||
{
|
||||
"이름": "결속선",
|
||||
"고르기": [
|
||||
"MT000204",
|
||||
"MT000164"
|
||||
],
|
||||
"비고": "해석: # 10 = Φ3.2㎜"
|
||||
}
|
||||
],
|
||||
"중간": [
|
||||
@@ -2958,7 +2916,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유"},
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유", "검색어": "직포 옥상방수용직포"},
|
||||
"이름": "그물망",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC000085, 구분='그물망').수량 / 10",
|
||||
@@ -2966,7 +2924,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "안전시설", "상세구분": "낙석방지·휀스"},
|
||||
"요소": {"구분": "안전시설", "상세구분": "낙석방지·휀스", "검색어": "칼라각관(적색) 25*25*1.6t"},
|
||||
"이름": "보강재",
|
||||
"단위": "m",
|
||||
"수량": "찾기(QC000085, 구분='보강재').수량 / 10",
|
||||
@@ -2974,7 +2932,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철선", "대표": "{결속선}"},
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철선", "검색어": "어닐링철선 #10 서울"},
|
||||
"이름": "결속선 # 10",
|
||||
"단위": "kg",
|
||||
"수량": "결속선량 / 10",
|
||||
@@ -3009,7 +2967,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유"},
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유", "검색어": "직포 옥상방수용직포"},
|
||||
"이름": "보호망",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC000088, 구분='보호망').수량",
|
||||
@@ -3044,7 +3002,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유"},
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유", "검색어": "직포 옥상방수용직포"},
|
||||
"이름": "보호망",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC000091, 구분='보호망').수량",
|
||||
@@ -3954,7 +3912,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "도료", "대표": "MT030616"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "도료", "검색어": "보호막"},
|
||||
"이름": "보호막",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC000130, 구분='보호막').수량",
|
||||
@@ -3971,17 +3929,7 @@
|
||||
"출처": "건설품셈 공통 2-10-2",
|
||||
"소유": "공용",
|
||||
"비고": "재료량 할증 포함 · 방진망 손율 100% · 해석: 철선 소모(손율 100%) · 비계 별도",
|
||||
"입력": [
|
||||
{
|
||||
"이름": "철선",
|
||||
"고르기": [
|
||||
"MT000199",
|
||||
"MT000204",
|
||||
"MT000159",
|
||||
"MT000164"
|
||||
]
|
||||
}
|
||||
],
|
||||
"입력": [],
|
||||
"중간": [
|
||||
{
|
||||
"이름": "비계공",
|
||||
@@ -3999,10 +3947,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {
|
||||
"구분": "잡재료",
|
||||
"상세구분": "소모품"
|
||||
},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "PVC천막지 T-210"},
|
||||
"이름": "방진망",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC000133, 구분='방진망').수량",
|
||||
@@ -4010,7 +3955,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철선", "대표": "{철선}"},
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철선", "검색어": "어닐링철선 #8 서울"},
|
||||
"이름": "철선",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC000133, 구분='철선').수량",
|
||||
@@ -4594,15 +4539,6 @@
|
||||
"부직포 깔기",
|
||||
"보양지 붙이기"
|
||||
]
|
||||
},
|
||||
{
|
||||
"이름": "못",
|
||||
"고르기": [
|
||||
"MT000246",
|
||||
"MT000247",
|
||||
"MT000248",
|
||||
"MT000249"
|
||||
]
|
||||
}
|
||||
],
|
||||
"중간": [
|
||||
@@ -4634,7 +4570,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유"},
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유", "검색어": "직포 옥상방수용직포"},
|
||||
"이름": "부직포",
|
||||
"단위": "㎡",
|
||||
"수량": "만약(작업 == '부직포 깔기', 찾기(QC000144, 구분=작업, 자재='부직포').수량, 0)",
|
||||
@@ -4642,7 +4578,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "규격": "하드롱지", "대표": "MT008073"},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "도배지(벽지) 하드롱지"},
|
||||
"이름": "하드롱지",
|
||||
"단위": "㎡",
|
||||
"수량": "만약(작업 == '보양지 붙이기', 찾기(QC000144, 구분=작업, 자재='하드롱지').수량, 0)",
|
||||
@@ -4650,7 +4586,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "그리스(일반범용) CROWN"},
|
||||
"이름": "풀",
|
||||
"단위": "kg",
|
||||
"수량": "만약(작업 == '보양지 붙이기', 찾기(QC000144, 구분=작업, 자재='풀').수량, 0)",
|
||||
@@ -4658,7 +4594,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재"},
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재", "검색어": "미송각재 30×30×3600mm 서울"},
|
||||
"이름": "목재",
|
||||
"단위": "㎥",
|
||||
"수량": "만약(작업 == '목재 붙이기', 찾기(QC000144, 구분=작업, 자재='목재').수량, 0)",
|
||||
@@ -4666,7 +4602,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "못", "대표": "{못}"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "못", "검색어": "철못 N25 서울"},
|
||||
"이름": "못",
|
||||
"단위": "kg",
|
||||
"수량": "만약(작업 == '목재 붙이기', 찾기(QC000144, 구분=작업, 자재='못').수량, 0)",
|
||||
|
||||
@@ -666,7 +666,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "앵커"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "앵커", "검색어": "L형앵커볼트 5/8\""},
|
||||
"이름": "앵커",
|
||||
"규격": "1/2〃",
|
||||
"단위": "개",
|
||||
@@ -676,7 +676,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "볼트·너트"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "볼트·너트", "검색어": "핀볼트 M12"},
|
||||
"이름": "블라인드리벳",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC001430, 구분='블라인드리벳').수량",
|
||||
@@ -685,7 +685,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철근"},
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철근", "검색어": "철근부식방지재 MT-3"},
|
||||
"이름": "철물",
|
||||
"규격": "D22 철근",
|
||||
"단위": "㎏",
|
||||
@@ -695,7 +695,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "대표": "MT030626"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "실리콘"},
|
||||
"이름": "실리콘",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001430, 구분='실리콘').수량",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "콘크리트제품", "상세구분": "경계석·블록"},
|
||||
"요소": {"구분": "콘크리트제품", "상세구분": "경계석·블록", "검색어": "주차라인블록 1000X300X150"},
|
||||
"이름": "시멘트벽돌",
|
||||
"단위": "매",
|
||||
"수량": "찾기(QC001211, 벽두께=벽두께).'벽돌(19×9×5.7㎝)'",
|
||||
@@ -131,7 +131,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "규격": "1:3"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "검색어": "모르타르 1:3 서울"},
|
||||
"이름": "모르타르",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QC001211, 벽두께=벽두께).모르타르",
|
||||
@@ -269,7 +269,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "콘크리트제품", "상세구분": "경계석·블록"},
|
||||
"요소": {"구분": "콘크리트제품", "상세구분": "경계석·블록", "검색어": "주차라인블록 1000X300X150"},
|
||||
"이름": "치장벽돌",
|
||||
"단위": "매",
|
||||
"수량": "찾기(QC001213, 벽두께=벽두께).'벽돌(19×9×5.7㎝)'",
|
||||
@@ -278,7 +278,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "규격": "1:3"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "검색어": "모르타르 1:3 서울"},
|
||||
"이름": "모르타르",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QC001213, 벽두께=벽두께).'모르타르 쌓기'",
|
||||
@@ -287,7 +287,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "규격": "1:1"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "검색어": "모르타르 1:1 서울"},
|
||||
"이름": "모르타르",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QC001213, 벽두께=벽두께).'모르타르 치장줄눈'",
|
||||
@@ -350,7 +350,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "규격": "1:2"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "검색어": "모르타르 1:2 서울"},
|
||||
"이름": "모르타르",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QC001216, 벽두께=벽두께).'모르타르 쌓기'",
|
||||
@@ -401,7 +401,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "규격": "1:1"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "검색어": "모르타르 1:1 서울"},
|
||||
"이름": "모르타르",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QC001216, 벽두께=벽두께).'모르타르 치장줄눈'",
|
||||
@@ -564,7 +564,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "규격": "1:3"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "검색어": "모르타르 1:3 서울"},
|
||||
"이름": "모르타르",
|
||||
"단위": "㎥",
|
||||
"수량": "만약(블록규격mm == '390x190x190', 찾기(QC001219, 블록규격='390×190×190mm').모르타르, 만약(블록규격mm == '390x190x150', 찾기(QC001219, 블록규격='390×190×150mm').모르타르, 찾기(QC001219, 블록규격='390×190×100mm').모르타르))",
|
||||
@@ -705,7 +705,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "규격": "1:3"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "검색어": "모르타르 1:3 서울"},
|
||||
"이름": "모르타르",
|
||||
"단위": "㎥",
|
||||
"수량": "만약(블록규격mm == '390x190x190', 찾기(QC001221, 블록규격='390×190×190mm').모르타르, 만약(블록규격mm == '390x190x150', 찾기(QC001221, 블록규격='390×190×150mm').모르타르, 찾기(QC001221, 블록규격='390×190×100mm').모르타르))",
|
||||
@@ -832,7 +832,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "규격": "조적용(25kg/포)", "대표": "MT005805"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "검색어": "ALC 시멘트(몰탈) 조적용(25kg/포)"},
|
||||
"이름": "ALC모르타르",
|
||||
"단위": "kg",
|
||||
"수량": "만약(블록규격mm == '600x400x100', 찾기(QC001223, '블록규격(mm)'='600×400×100').모르타르, 만약(블록규격mm == '600x400x125', 찾기(QC001223, '블록규격(mm)'='600×400×125').모르타르, 만약(블록규격mm == '600x300x150', 찾기(QC001223, '블록규격(mm)'='600×300×150').모르타르, 찾기(QC001223, '블록규격(mm)'='600×300×200').모르타르)))",
|
||||
|
||||
@@ -280,7 +280,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "팁(날) 노면파쇄기용"},
|
||||
"이름": "팁(날)",
|
||||
"단위": "개",
|
||||
"수량": "0.69 * 절삭깊이 / 1000",
|
||||
@@ -454,7 +454,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "팁(날) 노면파쇄기용"},
|
||||
"이름": "팁(날)",
|
||||
"단위": "개",
|
||||
"수량": "0.69 * 100 / 1000",
|
||||
@@ -613,7 +613,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "팁(날) 노면파쇄기용"},
|
||||
"이름": "팁(날)",
|
||||
"단위": "개",
|
||||
"수량": "0.69 * 만약(밀링깊이 == '100㎜', 100, 150) / 1000",
|
||||
@@ -3007,7 +3007,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "도료"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "도료", "검색어": "신구접착및방청제 LDS-MP"},
|
||||
"이름": "프라이머",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC001944, 구분='프라이머').수량 / 10",
|
||||
@@ -3015,7 +3015,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98%"},
|
||||
"이름": "프로판가스",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC001944, 구분='프로판가스').수량 / 10",
|
||||
@@ -3548,7 +3548,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "6000ℓ/병", "대표": "MT003165"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "산소 6000ℓ/병"},
|
||||
"이름": "산소",
|
||||
"단위": "L",
|
||||
"수량": "찾기(QC001951, 종별=난간규격).산소 * 1000 / 10",
|
||||
@@ -3556,7 +3556,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "98% (용접용)", "대표": "MT003163"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98% (용접용)"},
|
||||
"이름": "아세틸렌",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC001951, 종별=난간규격).아세틸렌 / 10",
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "규격": "1:1"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "검색어": "모르타르 1:1 서울"},
|
||||
"이름": "모르타르",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QC001227, 붙임방식=붙임방식).'줄눈 모르타르량'",
|
||||
@@ -208,7 +208,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "규격": "1:3"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "검색어": "모르타르 1:3 서울"},
|
||||
"이름": "모르타르",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QC001229, '바름두께(㎜)'=바름두께).'붙임 모르타르(벽체)'",
|
||||
@@ -315,7 +315,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "규격": "1:2"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "검색어": "모르타르 1:2 서울"},
|
||||
"이름": "모르타르",
|
||||
"단위": "㎥",
|
||||
"수량": "만약(시공면 == '바닥면', 찾기(QC001231, '바름두께(㎜)'=바름두께).'붙임 모르타르 바닥면', 찾기(QC001231, '바름두께(㎜)'=바름두께).'붙임 모르타르 벽면')",
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "방수석고보드 12.5t"},
|
||||
"이름": "일체식 방수시트",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC000635, 구분='일체식 방수시트').수량",
|
||||
|
||||
@@ -1089,7 +1089,7 @@
|
||||
"호표": [
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "화약"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "화약", "검색어": "폭약 28mm(메가마이트I)"},
|
||||
"이름": "폭약",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC000199).폭약",
|
||||
@@ -1098,7 +1098,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "화약"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "화약", "검색어": "전기뇌관 3.5m"},
|
||||
"이름": "뇌관",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000199).뇌관",
|
||||
@@ -1107,7 +1107,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "비트",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000199).비트",
|
||||
@@ -1296,7 +1296,7 @@
|
||||
"호표": [
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "화약"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "화약", "검색어": "폭약 28mm(메가마이트I)"},
|
||||
"이름": "폭약",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC000203, 발파=발파).폭약",
|
||||
@@ -1305,7 +1305,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "화약"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "화약", "검색어": "전기뇌관 3.5m"},
|
||||
"이름": "뇌관",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000203, 발파=발파).뇌관",
|
||||
@@ -1314,7 +1314,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "비트",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000203, 발파=발파).비트",
|
||||
@@ -3177,13 +3177,7 @@
|
||||
"출처": "건설품셈 공통 3-8-3",
|
||||
"소유": "공용",
|
||||
"비고": "해석: 재료량 표(기준 칸 빈칸)를 m당으로 읽음 · 각재 수종 설계자 고름(자재품목 각재 ㎥) · 해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외)",
|
||||
"입력": [
|
||||
{
|
||||
"이름": "각재",
|
||||
"고르기": ["MT000339", "MT000338"],
|
||||
"출처": "자재품목 각재(㎥) — 외송 · 미송"
|
||||
}
|
||||
],
|
||||
"입력": [],
|
||||
"중간": [],
|
||||
"호표": [
|
||||
{
|
||||
@@ -3204,7 +3198,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재", "대표": "{각재}"},
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재", "검색어": "외송각재 30×30×3600mm 서울"},
|
||||
"이름": "각재 10㎝×10㎝",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QC000245).각재",
|
||||
|
||||
@@ -784,7 +784,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "98% (용접용)", "대표": "MT003163"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98% (용접용)"},
|
||||
"이름": "아세틸렌가스",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC000648, 품명='아세틸렌', 레일규격=레일규격).수량",
|
||||
@@ -793,7 +793,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "6000ℓ/병", "대표": "MT003165"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "산소 6000ℓ/병"},
|
||||
"이름": "산소가스",
|
||||
"단위": "L",
|
||||
"수량": "찾기(QC000648, 품명='산소', 레일규격=레일규격, 규격='KSM 1101, 99.5%').수량 * 1000",
|
||||
@@ -802,7 +802,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "빗자루 도로청소용"},
|
||||
"이름": "바퀴숫돌 단면용 A36m B11호 A150×8×22 KSL 6501",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000648, 품명='바퀴숫돌', 레일규격=레일규격, 규격='단면용 A36m B11호 A150×8×22 KSL 6501').수량",
|
||||
@@ -811,7 +811,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "빗자루 도로청소용"},
|
||||
"이름": "바퀴숫돌 측면용 A24 QWV1호 A205×25×25 KSL 6501",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000648, 품명='바퀴숫돌', 레일규격=레일규격, 규격='측면용 A24 QWV1호 A205×25×25 KSL 6501').수량",
|
||||
@@ -820,7 +820,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "빗자루 도로청소용"},
|
||||
"이름": "바퀴숫돌 평면용 A24 QWV1호 A205×25×25 KSL 6501",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000648, 품명='바퀴숫돌', 레일규격=레일규격, 규격='평면용 A24 QWV1호 A205×25×25 KSL 6501').수량",
|
||||
@@ -829,7 +829,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "빗자루 도로청소용"},
|
||||
"이름": "바퀴숫돌 최종용 A24 QWV 5호 A205×22×22",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000648, 품명='바퀴숫돌', 레일규격=레일규격, 규격='최종용 A24 QWV 5호 A205×22×22').수량",
|
||||
@@ -838,7 +838,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "보일러·공조", "규격": "압접가열용", "대표": "MT030710"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "보일러·공조", "검색어": "버너 압접가열용"},
|
||||
"이름": "버너 압접가열용",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000648, 품명='버너', 레일규격=레일규격, 규격='압접가열용').수량",
|
||||
@@ -847,7 +847,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "규격": "압접버너용", "대표": "MT030627"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "노즐 압접버너용"},
|
||||
"이름": "노즐 압접버너용",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000648, 품명='노즐', 레일규격=레일규격, 규격='압접버너용').수량",
|
||||
@@ -914,7 +914,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "점화용", "대표": "MT030628"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "테르밋용재 점화용"},
|
||||
"이름": "테르밋용재 점화용",
|
||||
"단위": "포",
|
||||
"수량": "찾기(QC000650, 품명='테르밋용재', 레일규격=레일규격, 규격='점화용').수량",
|
||||
@@ -923,7 +923,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "테르밋 용접용", "대표": "MT030629"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "몰드 테르밋 용접용"},
|
||||
"이름": "몰드",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000650, 품명='몰드', 레일규격=레일규격).수량",
|
||||
@@ -932,7 +932,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "테르밋 용접용", "대표": "MT030630"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "골무 테르밋 용접용"},
|
||||
"이름": "골무",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000650, 품명='골무', 레일규격=레일규격).수량",
|
||||
@@ -941,7 +941,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "전기·통신", "상세구분": "판넬·차단기", "규격": "테르밋 용접용", "대표": "MT030711"},
|
||||
"요소": {"구분": "전기·통신", "상세구분": "판넬·차단기", "검색어": "퓨즈 테르밋 용접용"},
|
||||
"이름": "퓨즈",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000650, 품명='퓨즈', 레일규격=레일규격).수량",
|
||||
@@ -950,7 +950,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "6000ℓ/병", "대표": "MT003165"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "산소 6000ℓ/병"},
|
||||
"이름": "산소가스",
|
||||
"단위": "L",
|
||||
"수량": "찾기(QC000650, 품명='산소', 레일규격=레일규격).수량 * 1000",
|
||||
@@ -959,7 +959,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "대표": "MT003176"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "LPG 가스"},
|
||||
"이름": "프로판가스(건설용 LPG)",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC000650, 품명='프로판가스', 레일규격=레일규격).수량",
|
||||
|
||||
@@ -409,7 +409,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "석고본드"},
|
||||
"이름": "접착제",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001242, 구분='접착제').수량",
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "식생·종자", "상세구분": "종자"},
|
||||
"요소": {"구분": "식생·종자", "상세구분": "종자", "검색어": "종자 혼합종자"},
|
||||
"이름": "종자",
|
||||
"단위": "㎏",
|
||||
"수량": "종자량 / 100",
|
||||
@@ -190,7 +190,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "식생·종자", "상세구분": "비료·토양개량", "대표": "MT006489"},
|
||||
"요소": {"구분": "식생·종자", "상세구분": "비료·토양개량", "검색어": "복합비료"},
|
||||
"이름": "비료(복합비료)",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC000274, 구분='비료').수량 / 100",
|
||||
@@ -199,7 +199,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "도료", "규격": "화이버/펄프류", "대표": "MT030610"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "도료", "검색어": "피복제 화이버/펄프류"},
|
||||
"이름": "피복제(화이버/펄프류)",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC000274, 구분='피복제').수량 / 100",
|
||||
@@ -208,7 +208,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "식생·종자", "상세구분": "비료·토양개량", "규격": "합성접착제", "대표": "MT030611"},
|
||||
"요소": {"구분": "식생·종자", "상세구분": "비료·토양개량", "검색어": "침식방지안정제 합성접착제"},
|
||||
"이름": "침식방지안정제(합성접착제)",
|
||||
"단위": "㎏",
|
||||
"수량": "안정제량 / 100",
|
||||
@@ -217,7 +217,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "규격": "색소(M-Green)", "대표": "MT006740"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "착색제 색소(M-Green)"},
|
||||
"이름": "색소(착색제)",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC000274, 구분='색소').수량 / 100",
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
"호표": [
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98%"},
|
||||
"이름": "용접봉(강교 제작)",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC000664, 품명='용접봉').수량",
|
||||
@@ -117,7 +117,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "6000ℓ/병", "대표": "MT003165"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "산소 6000ℓ/병"},
|
||||
"이름": "산소가스",
|
||||
"단위": "L",
|
||||
"수량": "찾기(QC000664, 품명='산소').수량 * 1000",
|
||||
@@ -126,7 +126,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "대표": "MT003176"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "LPG 가스"},
|
||||
"이름": "LPG(건설용)",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC000664, 품명='LPG가스').수량",
|
||||
@@ -164,7 +164,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "대표": "MT030631"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "철구(Shot ball)"},
|
||||
"이름": "철구(Shot ball)",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC000665, 구분='철구(Shot ball)').수량",
|
||||
@@ -173,7 +173,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "도료"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "도료", "검색어": "목재전용도료 오일스테인"},
|
||||
"이름": "무기질아연말 샵프라이머 도막두께 20μm",
|
||||
"단위": "ℓ",
|
||||
"수량": "찾기(QC000665, 구분='무기질아연말샵프라이머', 규격='도막두께20μm').수량",
|
||||
@@ -204,7 +204,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "대표": "MT030632"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "철편(Grit)"},
|
||||
"이름": "철편(Grit)",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC000666, 구분='철편(Grit)').수량",
|
||||
|
||||
@@ -2386,7 +2386,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유"},
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유", "검색어": "직포 옥상방수용직포"},
|
||||
"이름": "매트(토목섬유)",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC000334, 구분='육상부설').매트 / 100",
|
||||
@@ -2394,7 +2394,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "와이어로프·클램프", "규격": "9㎜"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "와이어로프·클램프", "검색어": "와이어로프(철심) 9㎜"},
|
||||
"이름": "P.P로프(9㎜)",
|
||||
"단위": "m",
|
||||
"수량": "찾기(QC000334, 구분='육상부설').'P.P로프(9㎜)' / 100",
|
||||
@@ -2402,7 +2402,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "골재", "대표": "MT030617"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "골재", "검색어": "모래주머니"},
|
||||
"이름": "모래주머니",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000334, 구분='육상부설').모래주머니 / 100",
|
||||
@@ -3057,7 +3057,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유"},
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유", "검색어": "고무바 2.2cm"},
|
||||
"이름": "드레인 보드",
|
||||
"단위": "m",
|
||||
"수량": "1 + 찾기(QC000356, 항목='5-2-3 재료량(주②)', 구분='드레인 보드 재료할증').비율 / 100",
|
||||
@@ -4443,7 +4443,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "6000ℓ/병", "대표": "MT003165"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "산소 6000ℓ/병"},
|
||||
"이름": "산소",
|
||||
"단위": "ℓ",
|
||||
"수량": "찾기(QC000377, 구분='산소', 규격=규격).수량",
|
||||
@@ -4451,7 +4451,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "대표": "MT003176"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "LPG 가스"},
|
||||
"이름": "LPG",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC000377, 구분='LPG', 규격=규격).수량",
|
||||
@@ -5331,7 +5331,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유"},
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유", "검색어": "직포 옥상방수용직포"},
|
||||
"이름": "부직포",
|
||||
"단위": "㎡",
|
||||
"수량": "만약(재료 == '부직포', 찾기(QC000399, 구분='부직포').수량, 0)",
|
||||
@@ -5339,7 +5339,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유"},
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유", "검색어": "직포 옥상방수용직포"},
|
||||
"이름": "지오콤포지트",
|
||||
"단위": "㎡",
|
||||
"수량": "만약(재료 == '지오콤포지트', 찾기(QC000399, 구분='지오콤포지트').수량, 0)",
|
||||
@@ -5347,7 +5347,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "매트"},
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "매트", "검색어": "PET 매트 10T/m"},
|
||||
"이름": "벤토나이트매트",
|
||||
"단위": "㎡",
|
||||
"수량": "만약(재료 == '벤토나이트매트', 찾기(QC000399, 구분='벤토나이트매트').수량, 0)",
|
||||
@@ -5355,7 +5355,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "관·이음관", "상세구분": "PE관", "규격": "2∼2.5㎜", "대표": "MT030618"},
|
||||
"요소": {"구분": "관·이음관", "상세구분": "PE관", "검색어": "HDPE 시트 2∼2.5㎜"},
|
||||
"이름": "HDPE 시트",
|
||||
"단위": "㎡",
|
||||
"수량": "만약(재료 == 'HDPE시트', 찾기(QC000399, 구분='HDPE 시트').수량, 0)",
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "석고본드"},
|
||||
"이름": "접착제",
|
||||
"단위": "㎏",
|
||||
"수량": "만약(시공방식 == '타일형', 타일접착제량, 만약(시공방식 == '시트형 (전면접착 방식)', 찾기(QC001249, 구분='바닥 시트 전면접착 방식').접착제, 찾기(QC001249, 구분='바닥 시트 부분접착 방식').접착제))",
|
||||
@@ -131,7 +131,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재"},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "불연천장재(마이톤) 12mm"},
|
||||
"이름": "카페트",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001251, 구분='카페트').수량",
|
||||
@@ -140,10 +140,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {
|
||||
"구분": "매트·토목섬유",
|
||||
"상세구분": "부직포·토목섬유"
|
||||
},
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유", "검색어": "직포 옥상방수용직포"},
|
||||
"이름": "펠트",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001251, 구분='펠트').수량",
|
||||
@@ -152,7 +149,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "석고본드"},
|
||||
"이름": "접착제",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001251, 구분='접착제').수량",
|
||||
@@ -625,7 +622,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "석고본드"},
|
||||
"이름": "접착제",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001260, 구분='접착제').수량",
|
||||
@@ -851,7 +848,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재"},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "불연천장재(마이톤) 12mm"},
|
||||
"이름": "흡음판",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001264, 구분='흡음판', 규격='1,000×2,000×50㎜').수량",
|
||||
@@ -860,7 +857,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재"},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "처마후레싱 C/S"},
|
||||
"이름": "조이너",
|
||||
"단위": "m",
|
||||
"수량": "찾기(QC001264, 구분='조이너', 규격='P.V.C 50T').수량",
|
||||
@@ -869,7 +866,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "석고본드"},
|
||||
"이름": "접착제",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001264, 구분='접착제').수량",
|
||||
@@ -960,7 +957,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재"},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "처마후레싱 C/S"},
|
||||
"이름": "테라조",
|
||||
"단위": "m",
|
||||
"수량": "만약(구분 == '석재류', 찾기(QC001266, 구분=구분).테라조, 0)",
|
||||
@@ -969,7 +966,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재"},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "처마후레싱 C/S"},
|
||||
"이름": "합성수지 걸레받이",
|
||||
"단위": "m",
|
||||
"수량": "만약(구분 == '합성수지류', 찾기(QC001266, 구분=구분).합성수지, 0)",
|
||||
@@ -987,7 +984,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "석고본드"},
|
||||
"이름": "접착제",
|
||||
"단위": "㎏",
|
||||
"수량": "만약(구분 == '석재류', 0, 걸레받이접착제량)",
|
||||
@@ -1109,11 +1106,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재",
|
||||
"대표": "MT008074"
|
||||
},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "종이벽지(초배지)"},
|
||||
"이름": "초배지",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001269, 바탕면=바탕면).초배지",
|
||||
@@ -1122,7 +1115,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "대표": "MT030622"},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "정배지"},
|
||||
"이름": "정배지",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001269, 바탕면=바탕면).정배지",
|
||||
@@ -1131,7 +1124,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "그리스(일반범용) CROWN"},
|
||||
"이름": "풀",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC001269, 바탕면=바탕면).풀",
|
||||
@@ -1245,7 +1238,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "단열·보온", "상세구분": "단열재"},
|
||||
"요소": {"구분": "단열·보온", "상세구분": "단열재", "검색어": "샌드위치판넬(그라스울) T50"},
|
||||
"이름": "단열재",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001272, 구분='단열재').수량",
|
||||
@@ -1254,7 +1247,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "석고본드"},
|
||||
"이름": "접착제",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001272, 구분='접착제').수량",
|
||||
@@ -1334,7 +1327,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "단열·보온", "상세구분": "단열재"},
|
||||
"요소": {"구분": "단열·보온", "상세구분": "단열재", "검색어": "샌드위치판넬(그라스울) T50"},
|
||||
"이름": "단열재",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001274, 구분='단열재').수량",
|
||||
@@ -1343,7 +1336,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "석고본드"},
|
||||
"이름": "접착제",
|
||||
"단위": "㎏",
|
||||
"수량": "단열접착제량",
|
||||
@@ -1421,7 +1414,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "단열·보온", "상세구분": "단열재"},
|
||||
"요소": {"구분": "단열·보온", "상세구분": "단열재", "검색어": "샌드위치판넬(그라스울) T50"},
|
||||
"이름": "단열재",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001276, 구분='단열재').수량",
|
||||
@@ -1484,7 +1477,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "단열·보온", "상세구분": "단열재"},
|
||||
"요소": {"구분": "단열·보온", "상세구분": "단열재", "검색어": "샌드위치판넬(그라스울) T50"},
|
||||
"이름": "단열재",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001278, 구분='단열재').수량",
|
||||
@@ -1493,7 +1486,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "석고본드"},
|
||||
"이름": "접착제",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001278, 구분='접착제').수량",
|
||||
@@ -1502,7 +1495,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "대표": "MT030623"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "알루미늄핀"},
|
||||
"이름": "알루미늄핀",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC001278, 구분='알루미늄핀').수량",
|
||||
@@ -1633,7 +1626,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "단열·보온", "상세구분": "단열재"},
|
||||
"요소": {"구분": "단열·보온", "상세구분": "단열재", "검색어": "샌드위치판넬(그라스울) T50"},
|
||||
"이름": "단열재",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001281, 구분='단열재').수량",
|
||||
@@ -1712,7 +1705,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "단열·보온", "상세구분": "단열재"},
|
||||
"요소": {"구분": "단열·보온", "상세구분": "단열재", "검색어": "샌드위치판넬(그라스울) T50"},
|
||||
"이름": "단열재",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001283, 구분='단열재').수량",
|
||||
@@ -1721,7 +1714,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "석고본드"},
|
||||
"이름": "접착제",
|
||||
"단위": "㎏",
|
||||
"수량": "만약(접착제 == '씀', 찾기(QC001283, 구분='접착제').수량, 0)",
|
||||
@@ -1777,7 +1770,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "PVC천막지 T-210"},
|
||||
"이름": "방습필름",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001285, 구분=구분).방습필름",
|
||||
@@ -1846,7 +1839,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "단열·보온", "상세구분": "단열재"},
|
||||
"요소": {"구분": "단열·보온", "상세구분": "단열재", "검색어": "샌드위치판넬(그라스울) T50"},
|
||||
"이름": "단열판",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001288, 구분='외벽단열').단열판",
|
||||
@@ -1855,7 +1848,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "석고본드"},
|
||||
"이름": "접착제",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001288, 구분='외벽단열').접착제",
|
||||
@@ -1864,7 +1857,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "시멘트", "대표": "MT004176"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "시멘트", "검색어": "시멘트 전국"},
|
||||
"이름": "시멘트",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001288, 구분='외벽단열').시멘트",
|
||||
@@ -1873,7 +1866,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철망·와이어메쉬", "대표": "MT030624"},
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철망·와이어메쉬", "검색어": "표준보강메시"},
|
||||
"이름": "표준보강메시",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001288, 구분='외벽단열').표준보강메시",
|
||||
@@ -1928,7 +1921,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "석고본드"},
|
||||
"이름": "접착제",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001288, 구분='하부보강').접착제",
|
||||
@@ -1937,7 +1930,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "시멘트", "대표": "MT004176"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "시멘트", "검색어": "시멘트 전국"},
|
||||
"이름": "시멘트",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001288, 구분='하부보강').시멘트",
|
||||
@@ -1946,7 +1939,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철망·와이어메쉬", "대표": "MT030625"},
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철망·와이어메쉬", "검색어": "고강도메시"},
|
||||
"이름": "고강도메시",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001288, 구분='하부보강').고강도메시",
|
||||
|
||||
@@ -2052,7 +2052,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "전기·통신", "상세구분": "케이블·전선", "규격": "2.0㎜"},
|
||||
"요소": {"구분": "전기·통신", "상세구분": "케이블·전선", "검색어": "전선(인입용) 2.0㎜"},
|
||||
"이름": "IV전선 2.0㎜",
|
||||
"단위": "m",
|
||||
"수량": "찾기(QC000707).IV전선",
|
||||
@@ -2061,7 +2061,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "전기·통신", "상세구분": "부속·기타", "규격": "100W"},
|
||||
"요소": {"구분": "전기·통신", "상세구분": "부속·기타", "검색어": "백열전구 100W"},
|
||||
"이름": "백열등 100W",
|
||||
"단위": "EA",
|
||||
"수량": "찾기(QC000707).백열등",
|
||||
|
||||
@@ -406,7 +406,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "방수석고보드 12.5t"},
|
||||
"이름": "방수시트",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001300, 구분='시트').수량",
|
||||
@@ -469,7 +469,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "방수석고보드 12.5t"},
|
||||
"이름": "방수시트",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001300, 구분='시트').수량",
|
||||
@@ -532,7 +532,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "방수석고보드 12.5t"},
|
||||
"이름": "방수시트",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001300, 구분='시트').수량",
|
||||
@@ -949,7 +949,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "방수석고보드 12.5t"},
|
||||
"이름": "벤토나이트방수재",
|
||||
"단위": "㎡",
|
||||
"수량": "만약(종류 == '벤토나이트 매트', 찾기(QC001307, 구분='벤토나이트방수재', 종류='매트', 부위=부위).수량, 0)",
|
||||
@@ -958,7 +958,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "방수석고보드 12.5t"},
|
||||
"이름": "벤토나이트방수재",
|
||||
"단위": "㎡",
|
||||
"수량": "만약(종류 == '벤토나이트 시트', 찾기(QC001307, 구분='벤토나이트방수재', 종류='시트', 부위=부위).수량, 0)",
|
||||
@@ -967,7 +967,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "KL-지수재 급결"},
|
||||
"이름": "벤토나이트씰재",
|
||||
"단위": "L",
|
||||
"수량": "찾기(QC001307, 구분='벤토나이트씰재', 종류=만약(종류 == '벤토나이트 매트', '매트', '시트'), 부위=부위).수량",
|
||||
@@ -976,7 +976,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "대표": "MT007749"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "벤토나이트알갱이"},
|
||||
"이름": "벤토나이트알갱이",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC001307, 구분='벤토나이트알갱이', 종류=만약(종류 == '벤토나이트 매트', '매트', '시트'), 부위=부위).수량",
|
||||
@@ -985,7 +985,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "PVC천막지 T-210"},
|
||||
"이름": "PE필름 0.04㎜",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001307, 구분='PE필름', 종류=만약(종류 == '벤토나이트 매트', '매트', '시트'), 부위=부위).수량",
|
||||
@@ -994,7 +994,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "앵커", "규격": "화약", "대표": "MT007751"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "앵커", "검색어": "카트리지 화약"},
|
||||
"이름": "카트리지",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC001307, 구분='카트리지', 종류=만약(종류 == '벤토나이트 매트', '매트', '시트'), 부위=부위).수량",
|
||||
@@ -1003,7 +1003,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "못", "규격": "32㎜"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "못", "검색어": "타정못 32㎜"},
|
||||
"이름": "콘크리트못",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC001307, 구분='콘크리트못', 종류=만약(종류 == '벤토나이트 매트', '매트', '시트'), 부위=부위).수량",
|
||||
@@ -1012,7 +1012,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "볼트·너트"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "볼트·너트", "검색어": "핀볼트 M12"},
|
||||
"이름": "와셔",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC001307, 구분='와셔', 종류=만약(종류 == '벤토나이트 매트', '매트', '시트'), 부위=부위).수량",
|
||||
@@ -1021,7 +1021,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "PVC코팅철선 #8"},
|
||||
"이름": "조인트테이프",
|
||||
"단위": "m",
|
||||
"수량": "만약(종류 == '벤토나이트 시트', 찾기(QC001307, 구분='조인트테이프', 종류='시트', 부위=부위).수량, 0)",
|
||||
@@ -1064,7 +1064,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "백업재(내열성) 10mm"},
|
||||
"이름": "실링재",
|
||||
"단위": "m",
|
||||
"수량": "찾기(QC001309, 구분='실링재').수량",
|
||||
|
||||
@@ -498,7 +498,7 @@
|
||||
"호표": [
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "석고본드"},
|
||||
"이름": "에폭시 신구콘크리트 접착제",
|
||||
"단위": "kg",
|
||||
"수량": "만약(용도 == '신구-콘크리트 접착제바르기', 접착제량, 0)",
|
||||
@@ -506,7 +506,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "석고본드"},
|
||||
"이름": "에폭시 콘크리트고무 접착제",
|
||||
"단위": "kg",
|
||||
"수량": "만약(용도 == '콘크리트 및 고무 기타 접착제바르기', 접착제량, 0)",
|
||||
@@ -514,7 +514,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "도료"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "도료", "검색어": "목재전용도료 오일스테인"},
|
||||
"이름": "시너",
|
||||
"단위": "ℓ",
|
||||
"수량": "찾기(QC000412, 용도=용도, 재료명='시너').수량",
|
||||
@@ -725,13 +725,6 @@
|
||||
50
|
||||
],
|
||||
"출처": "건설품셈 공통 6-2-3 비고"
|
||||
},
|
||||
{
|
||||
"이름": "결속선",
|
||||
"고르기": [
|
||||
"MT000209",
|
||||
"MT000174"
|
||||
]
|
||||
}
|
||||
],
|
||||
"중간": [
|
||||
@@ -768,7 +761,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철선", "대표": "{결속선}"},
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철선", "검색어": "어닐링철선(결속선) #20 서울"},
|
||||
"이름": "결속선",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC000421, Type=Type).사용량",
|
||||
@@ -933,7 +926,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "98% (1kg=853ℓ)", "대표": "MT003164"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98% (1kg=853ℓ)"},
|
||||
"이름": "아세틸렌",
|
||||
"단위": "ℓ",
|
||||
"수량": "찾기(QC000423).아세틸렌",
|
||||
@@ -941,7 +934,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "6000ℓ/병", "대표": "MT003165"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "산소 6000ℓ/병"},
|
||||
"이름": "산소",
|
||||
"단위": "ℓ",
|
||||
"수량": "찾기(QC000423).산소",
|
||||
@@ -1033,19 +1026,6 @@
|
||||
],
|
||||
"비고": "1회 합판 비율 원문 「-」 — 1회 사용 수량(QC000427) 그대로 100%"
|
||||
},
|
||||
{
|
||||
"이름": "합판",
|
||||
"고르기": [
|
||||
"MT000018"
|
||||
]
|
||||
},
|
||||
{
|
||||
"이름": "각재",
|
||||
"고르기": [
|
||||
"MT000339",
|
||||
"MT000338"
|
||||
]
|
||||
},
|
||||
{
|
||||
"이름": "높이감률",
|
||||
"단위": "%",
|
||||
@@ -1137,7 +1117,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "목재", "상세구분": "합판", "대표": "{합판}"},
|
||||
"요소": {"구분": "목재", "상세구분": "합판", "검색어": "합판(내수) 12t 서울"},
|
||||
"이름": "합판(내수 12㎜)",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC000427).합판 * 합판율",
|
||||
@@ -1145,7 +1125,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재", "대표": "{각재}"},
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재", "검색어": "외송각재 30×30×3600mm 서울"},
|
||||
"이름": "각재",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QC000427).각재 * 합판율",
|
||||
@@ -1180,7 +1160,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "시멘트", "대표": "MT004176"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "시멘트", "검색어": "시멘트 전국"},
|
||||
"이름": "구멍땜 시멘트",
|
||||
"단위": "kg",
|
||||
"수량": "구멍땜 * 찾기(QC000430).시멘트 / 100",
|
||||
@@ -1215,7 +1195,7 @@
|
||||
},
|
||||
{
|
||||
"이름": "소모자재",
|
||||
"식": "(찾기(QC000427).합판 * MT:{합판} + 찾기(QC000427).각재 * MT:{각재}) * 합판율 * 찾기(QC000428, 사용횟수=사용횟수).소모자재 / 100",
|
||||
"식": "(줄.'합판(내수 12㎜)' + 줄.'각재') * 찾기(QC000428, 사용횟수=사용횟수).소모자재 / 100",
|
||||
"비목": "재료비",
|
||||
"출처": "건설품셈 공통 6-3-1 소모자재"
|
||||
}
|
||||
@@ -1292,7 +1272,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "패널(K-SPAN PANEL) 0.6×305mm"},
|
||||
"이름": "강재거푸집 손료",
|
||||
"단위": "㎡",
|
||||
"수량": "(1 - 10 / 100) / 사용횟수",
|
||||
@@ -1530,7 +1510,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "패널(K-SPAN PANEL) 0.6×305mm"},
|
||||
"이름": "문양거푸집(판넬)",
|
||||
"단위": "㎡",
|
||||
"수량": "1",
|
||||
@@ -1603,7 +1583,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "폼타이 D형"},
|
||||
"이름": "원형맨홀거푸집 ø740 손료",
|
||||
"단위": "조",
|
||||
"수량": "만약(규격 == 'ø740', 1 / 10, 0)",
|
||||
@@ -1612,7 +1592,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "폼타이 D형"},
|
||||
"이름": "원형맨홀거푸집 ø900 손료",
|
||||
"단위": "조",
|
||||
"수량": "만약(규격 == 'ø900', 1 / 10, 0)",
|
||||
@@ -1621,7 +1601,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "폼타이 D형"},
|
||||
"이름": "원형맨홀거푸집 ø1200 손료",
|
||||
"단위": "조",
|
||||
"수량": "만약(규격 == 'ø1200', 1 / 10, 0)",
|
||||
@@ -1630,7 +1610,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "폼타이 D형"},
|
||||
"이름": "원형맨홀거푸집 ø1500 손료",
|
||||
"단위": "조",
|
||||
"수량": "만약(규격 == 'ø1500', 1 / 10, 0)",
|
||||
@@ -1639,7 +1619,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "폼타이 D형"},
|
||||
"이름": "원형맨홀거푸집 ø1800 손료",
|
||||
"단위": "조",
|
||||
"수량": "만약(규격 == 'ø1800', 1 / 10, 0)",
|
||||
@@ -2095,16 +2075,7 @@
|
||||
"출처": "건설품셈 공통 6-3-9",
|
||||
"소유": "공용",
|
||||
"비고": "재료량 할증 포함 · 설계에 따라 증감",
|
||||
"입력": [
|
||||
{
|
||||
"이름": "철선",
|
||||
"고르기": [
|
||||
"MT000199",
|
||||
"MT000159"
|
||||
],
|
||||
"비고": "해석: 철선 #8 = Φ4.0㎜"
|
||||
}
|
||||
],
|
||||
"입력": [],
|
||||
"중간": [
|
||||
{
|
||||
"이름": "특별인부",
|
||||
@@ -2134,7 +2105,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "관·이음관", "상세구분": "PVC관", "규격": "200×5T", "대표": "MT006544"},
|
||||
"요소": {"구분": "관·이음관", "상세구분": "PVC관", "검색어": "PVC지수판 200×5T"},
|
||||
"이름": "PVC 지수판 200×5t",
|
||||
"단위": "m",
|
||||
"수량": "찾기(QC000449, 구분='PVC 지수판').수량",
|
||||
@@ -2142,7 +2113,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "규격": "2.5mm", "대표": "MT003197"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "PVC용접봉 2.5mm"},
|
||||
"이름": "PVC 용접봉",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC000449, 구분='PVC 용접봉').수량",
|
||||
@@ -2150,7 +2121,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철선", "대표": "{철선}"},
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철선", "검색어": "어닐링철선 #8 서울"},
|
||||
"이름": "철선 #8",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC000449, 구분='철선').수량",
|
||||
|
||||
@@ -370,7 +370,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "레미콘", "상세구분": "레미콘", "규격": "{굵은골재최대치수}-{강도}-{슬럼프}"},
|
||||
"요소": {"구분": "레미콘", "상세구분": "레미콘", "검색어": "레미콘 {굵은골재최대치수}-{강도}-{슬럼프}"},
|
||||
"이름": "채움콘크리트",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QC000492).채움콘크리트",
|
||||
|
||||
@@ -340,7 +340,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "규격": "336×1,000×3㎜", "대표": "MT030705"},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "아스팔트싱글 336×1,000×3㎜"},
|
||||
"이름": "아스팔트싱글",
|
||||
"단위": "매",
|
||||
"수량": "찾기(QC001317, 구분='아스팔트싱글', 규격='336×1.000×3㎜').수량",
|
||||
@@ -407,10 +407,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재"
|
||||
},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "불연천장재(마이톤) 12mm"},
|
||||
"이름": "폴리카보네이트",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001319, 구분='폴리카보네이트').수량",
|
||||
@@ -468,7 +465,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재"},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "처마후레싱 C/S"},
|
||||
"이름": "후레싱",
|
||||
"단위": "m",
|
||||
"수량": "찾기(QC001321, 구분='후레싱').수량",
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철망·와이어메쉬"},
|
||||
"요소": {"구분": "철근·강재", "상세구분": "철망·와이어메쉬", "검색어": "메쉬스크린 H=0.9m"},
|
||||
"이름": "와이어메시",
|
||||
"단위": "매",
|
||||
"수량": "찾기(QC001331, 구분='와이어메시', 규격='1,800×1,800㎜').수량",
|
||||
@@ -169,7 +169,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "앵커"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "앵커", "검색어": "L형앵커볼트 5/8\""},
|
||||
"이름": "인서트",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC001333, 구분='인서트').수량",
|
||||
@@ -221,7 +221,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재"},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "처마후레싱 C/S"},
|
||||
"이름": "조이너 및 몰딩",
|
||||
"단위": "m",
|
||||
"수량": "찾기(QC001335, 구분='조이너 및 몰딩').수량",
|
||||
@@ -438,7 +438,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "앵커", "규격": "ø 10㎜"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "앵커", "검색어": "셋트앵커(철) ø 10㎜"},
|
||||
"이름": "앵커",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC001339, 구분='앵커', 규격='ø 10㎜').수량",
|
||||
@@ -447,7 +447,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "볼트·너트", "규격": "ø 4.2㎜"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "볼트·너트", "검색어": "작은나사(스크류) ø 4.2㎜"},
|
||||
"이름": "AL리벳",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC001339, 구분='AL리벳', 규격='ø 4.2㎜').수량",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "싱글코아바렐",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000776, 층=층, 종별='싱글코아바렐').수량",
|
||||
@@ -108,7 +108,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "메탈크라운비트",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000776, 층=층, 종별='메탈크라운비트').수량",
|
||||
@@ -117,7 +117,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "쵸핑비트",
|
||||
"단위": "개",
|
||||
"수량": "만약(층 == '호박돌층', 찾기(QC000776, 층=층, 종별='쵸핑비트').수량, 0)",
|
||||
@@ -126,7 +126,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "드라이브파이프헤드",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000776, 층=층, 종별='드라이브파이프헤드').수량",
|
||||
@@ -135,7 +135,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "드라이브파이프슈",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000776, 층=층, 종별='드라이브파이프슈').수량",
|
||||
@@ -144,7 +144,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "드라이브파이프",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000776, 층=층, 종별='드라이브파이프').수량",
|
||||
@@ -211,7 +211,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "더블코아바렐",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000777, 층=층, 종별='더블코아바렐').수량",
|
||||
@@ -220,7 +220,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "메탈크라운비트",
|
||||
"단위": "개",
|
||||
"수량": "만약(층 == '풍화암' 또는 층 == '연암' 또는 층 == '보통암', 찾기(QC000777, 층=층, 종별='메탈크라운비트').수량, 0)",
|
||||
@@ -229,7 +229,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "다이아몬드비트",
|
||||
"단위": "개",
|
||||
"수량": "만약(층 == '경암' 또는 층 == '극경암', 찾기(QC000777, 층=층, 종별='다이아몬드비트').수량, 0)",
|
||||
@@ -238,7 +238,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "메탈리밍쉘",
|
||||
"단위": "개",
|
||||
"수량": "만약(층 == '풍화암' 또는 층 == '연암' 또는 층 == '보통암', 찾기(QC000777, 층=층, 종별='메탈리밍쉘').수량, 0)",
|
||||
@@ -247,7 +247,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "다이아몬드리밍쉘",
|
||||
"단위": "개",
|
||||
"수량": "만약(층 == '경암' 또는 층 == '극경암', 찾기(QC000777, 층=층, 종별='다이아몬드리밍쉘').수량, 0)",
|
||||
@@ -256,7 +256,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "코아리프터",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000777, 층=층, 종별='코아리프터').수량",
|
||||
@@ -337,7 +337,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "측정·시험기기", "규격": "표준관입시험용", "대표": "MT030636"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "측정·시험기기", "검색어": "슈 표준관입시험용"},
|
||||
"이름": "슈",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000779, 종별='슈').수량",
|
||||
@@ -346,7 +346,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "샘플러",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000779, 종별='샘플러').수량",
|
||||
@@ -445,7 +445,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "신월튜브",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000781, 종별='신월튜브').수량",
|
||||
@@ -522,7 +522,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "베인 블레이드",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000780, 종별='재료비', 세목='vane blade(대형)').수량",
|
||||
@@ -531,7 +531,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "측정·시험기기", "규격": "ø16×750", "대표": "MT030637"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "측정·시험기기", "검색어": "베인전용로드 ø16×750"},
|
||||
"이름": "베인 전용로드",
|
||||
"단위": "본",
|
||||
"수량": "찾기(QC000780, 종별='재료비', 세목='전용로드(ø16×750)').수량",
|
||||
@@ -540,7 +540,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "측정·시험기기", "규격": "ø40.5×1m", "대표": "MT030638"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "측정·시험기기", "검색어": "로드 ø40.5×1m"},
|
||||
"이름": "로드",
|
||||
"단위": "본",
|
||||
"수량": "찾기(QC000780, 종별='재료비', 세목='로드(ø40.5×1m)').수량",
|
||||
@@ -596,7 +596,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "측정·시험기기", "규격": "평판재하시험용", "대표": "MT030639"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "측정·시험기기", "검색어": "표준사 평판재하시험용"},
|
||||
"이름": "표준사",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC000782, 종별='표준사').수량",
|
||||
@@ -685,7 +685,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "측정·시험기기", "규격": "정재하시험용", "대표": "MT030640"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "측정·시험기기", "검색어": "단독콘 정재하시험용"},
|
||||
"이름": "단독콘",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000785, 종별='단독콘').수량",
|
||||
@@ -971,7 +971,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "윙비트",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000789, 종별='윙비트').수량",
|
||||
@@ -980,7 +980,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "쉬트파일수팽창지수재 DPS-500"},
|
||||
"이름": "벤토나이트",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC000789, 규격=규격, 종별='벤토나이트').수량",
|
||||
@@ -1084,7 +1084,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "윙비트",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000790, 종별='윙비트').수량",
|
||||
@@ -1093,7 +1093,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "쉬트파일수팽창지수재 DPS-500"},
|
||||
"이름": "벤토나이트",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC000790, 규격=규격, 종별='벤토나이트').수량",
|
||||
@@ -1197,7 +1197,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "윙비트",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000791, 종별='윙비트').수량",
|
||||
@@ -1206,7 +1206,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "쉬트파일수팽창지수재 DPS-500"},
|
||||
"이름": "벤토나이트",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC000791, 규격=규격, 종별='벤토나이트').수량",
|
||||
@@ -1310,7 +1310,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "윙비트",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000792, 종별='윙비트').수량",
|
||||
@@ -1319,7 +1319,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "쉬트파일수팽창지수재 DPS-500"},
|
||||
"이름": "벤토나이트",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC000792, 규격=규격, 종별='벤토나이트').수량",
|
||||
@@ -1423,7 +1423,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "윙비트",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000793, 종별='윙비트').수량",
|
||||
@@ -1432,7 +1432,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "쉬트파일수팽창지수재 DPS-500"},
|
||||
"이름": "벤토나이트",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC000793, 규격=규격, 종별='벤토나이트').수량",
|
||||
@@ -1536,7 +1536,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "동물성단백기포제"},
|
||||
"이름": "기포제",
|
||||
"단위": "ℓ",
|
||||
"수량": "찾기(QC000794, 규격=규격, 종별='기포제').수량",
|
||||
@@ -1545,7 +1545,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "에어해머",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000794, 종별='에어해머').수량",
|
||||
@@ -1554,7 +1554,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "버튼비트",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000794, 종별='버튼(Button) 비트').수량",
|
||||
@@ -1658,7 +1658,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "동물성단백기포제"},
|
||||
"이름": "기포제",
|
||||
"단위": "ℓ",
|
||||
"수량": "찾기(QC000795, 규격=규격, 종별='기포제').수량",
|
||||
@@ -1667,7 +1667,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "에어해머",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000795, 종별='에어해머').수량",
|
||||
@@ -1676,7 +1676,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "버튼비트",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000795, 종별='버튼(Button) 비트').수량",
|
||||
@@ -1780,7 +1780,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "동물성단백기포제"},
|
||||
"이름": "기포제",
|
||||
"단위": "ℓ",
|
||||
"수량": "찾기(QC000796, 규격=규격, 종별='기포제').수량",
|
||||
@@ -1789,7 +1789,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "에어해머",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000796, 종별='에어해머').수량",
|
||||
@@ -1798,7 +1798,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"이름": "버튼비트",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000796, 종별='버튼(Button) 비트').수량",
|
||||
|
||||
@@ -270,7 +270,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "대표": "MT030631"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "철구(Shot ball)"},
|
||||
"이름": "steel shot",
|
||||
"단위": "㎏",
|
||||
"수량": "만약(구분 == 'Shot Blast', 강구수량, 0)",
|
||||
@@ -280,7 +280,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "골재"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "골재", "검색어": "모래 강모래"},
|
||||
"이름": "규사",
|
||||
"단위": "㎥",
|
||||
"수량": "만약(구분 == 'Sand Blast', 0.0508, 0)",
|
||||
@@ -289,7 +289,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "대표": "MT030633"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "동력 Brush"},
|
||||
"이름": "동력 Brush",
|
||||
"단위": "개",
|
||||
"수량": "만약(구분 == 'Power Tool', 0.03, 0)",
|
||||
@@ -307,7 +307,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "대표": "MT030634"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "Wire Brush"},
|
||||
"이름": "Wire Brush",
|
||||
"단위": "개",
|
||||
"수량": "만약(구분 == 'Wire Brush', 0.016, 0)",
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "시멘트", "대표": "MT004176"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "시멘트", "검색어": "시멘트 전국"},
|
||||
"이름": "시멘트",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC001347, 배합용적비=배합용적비).시멘트",
|
||||
@@ -466,7 +466,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "시멘트", "대표": "MT004176"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "시멘트", "검색어": "시멘트 전국"},
|
||||
"이름": "시멘트",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC001356, 구분='시멘트').수량",
|
||||
@@ -475,7 +475,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "접착·혼화", "검색어": "무수축혼화제"},
|
||||
"이름": "혼화제",
|
||||
"단위": "g",
|
||||
"수량": "만약(혼화제 == '씀', 찾기(QC001356, 구분='혼화제').수량, 0)",
|
||||
@@ -524,7 +524,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "시멘트", "대표": "MT004176"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "시멘트", "검색어": "시멘트 전국"},
|
||||
"이름": "시멘트",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QC001358, 구분='시멘트').수량",
|
||||
@@ -637,7 +637,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "검색어": "단면복구용몰탈 LDS-Mortar"},
|
||||
"이름": "무수축모르타르",
|
||||
"단위": "㎏",
|
||||
"수량": "찾기(QC001361, '규격(㎜)'=규격).무수축모르타르",
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "대표": "MT030635"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "컴파운드"},
|
||||
"이름": "컴파운드",
|
||||
"단위": "g",
|
||||
"수량": "만약(규격 == 'ø20', 찾기(QC001578, '구경(㎜)'='ø20').컴파운드, 만약(규격 == '25', 찾기(QC001578, '구경(㎜)'='25').컴파운드, 만약(규격 == '32', 찾기(QC001578, '구경(㎜)'='30').컴파운드, 만약(규격 == '40', 찾기(QC001578, '구경(㎜)'='40').컴파운드, 찾기(QC001578, '구경(㎜)'='50').컴파운드))))",
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "대표": "MT030635"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "컴파운드"},
|
||||
"이름": "컴파운드",
|
||||
"단위": "g",
|
||||
"수량": "찾기(QC001583, '구경(㎜)'='컴파운드(g)', 항목=만약(구분 == 'ø15㎜', 'ø15', 'ø20∼25')).수량",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -292,7 +292,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "식생·종자", "상세구분": "농약·약제"},
|
||||
"요소": {"구분": "식생·종자", "상세구분": "농약·약제", "검색어": "푸른솔(분산성액제) 수간주사(10㎖)"},
|
||||
"이름": "농약(Fluroxypyr-meptyl+Triclypyr-TEA 미탁제)",
|
||||
"단위": "병",
|
||||
"수량": "찾기(QF000007).주재료 * (1 + 증감 / 100)",
|
||||
@@ -490,7 +490,7 @@
|
||||
"호표": [
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재"},
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재", "검색어": "미송각재 30×30×3600mm 서울"},
|
||||
"이름": "우드칩",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QF000177, 구분='우드칩').수량",
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
{ "종류": "재료", "요소": "MO:휘발유:{지역}", "이름": "휘발유(양수기)", "단위": "ℓ", "수량": "휘발유", "비목": "재료비", "출처": "산림품셈 2-1-8" },
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "안전시설", "상세구분": "표지판·지주", "대표": "MT030609"},
|
||||
"요소": {"구분": "안전시설", "상세구분": "표지판·지주", "검색어": "깃발"},
|
||||
"이름": "깃발",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QF000016, 구분='깃발').수량 / 찾기(CF000090, 구분='1일 살포면적 소형헬기').적용",
|
||||
|
||||
@@ -328,7 +328,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "98% (용접용)", "대표": "MT003163"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98% (용접용)"},
|
||||
"이름": "아세틸렌가스",
|
||||
"단위": "kg",
|
||||
"수량": "찾기(QF000260, 이름='아세틸렌').적용",
|
||||
@@ -336,7 +336,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "규격": "6000ℓ/병", "대표": "MT003165"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "산소 6000ℓ/병"},
|
||||
"이름": "산소가스",
|
||||
"단위": "ℓ",
|
||||
"수량": "찾기(QF000260, 이름='산소').적용",
|
||||
@@ -344,7 +344,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "고철·기타금속"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "고철·기타금속", "검색어": "봉땜납 Sn"},
|
||||
"이름": "고철공제",
|
||||
"단위": "kg",
|
||||
"수량": "-50",
|
||||
|
||||
@@ -452,7 +452,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재"},
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재", "검색어": "미송각재 30×30×3600mm 서울"},
|
||||
"이름": "소재(원목)",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QF000323, 구분='운반대').소재",
|
||||
@@ -460,7 +460,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재"},
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재", "검색어": "미송각재 30×30×3600mm 서울"},
|
||||
"이름": "각재·판재",
|
||||
"단위": "㎥",
|
||||
"수량": "0.2",
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재"},
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재", "검색어": "미송각재 30×30×3600mm 서울"},
|
||||
"이름": "목재 손료",
|
||||
"단위": "㎥",
|
||||
"수량": "목재량 * 찾기(QF000346).손율 / 100",
|
||||
@@ -116,7 +116,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재"},
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재", "검색어": "미송각재 30×30×3600mm 서울"},
|
||||
"이름": "목재 손료",
|
||||
"단위": "㎥",
|
||||
"수량": "목재량 * 찾기(QF000348).손율 / 100",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -403,7 +403,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "골재"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "골재", "검색어": "모래 강모래"},
|
||||
"이름": "잡석(채움재)",
|
||||
"단위": "㎥",
|
||||
"수량": "잡석량",
|
||||
@@ -517,7 +517,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "레미콘", "상세구분": "레미콘", "규격": "{굵은골재최대치수}-{강도}-{슬럼프}"},
|
||||
"요소": {"구분": "레미콘", "상세구분": "레미콘", "검색어": "레미콘 {굵은골재최대치수}-{강도}-{슬럼프}"},
|
||||
"이름": "채움 콘크리트",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QF000434, 뒷길이=뒷길이, 종별=만약(돌 == '호박돌 및 야면석', '호박돌', 돌)).소요량",
|
||||
@@ -526,7 +526,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "검색어": "모르타르 1:1 서울"},
|
||||
"이름": "줄눈메꿈 모르타르",
|
||||
"단위": "㎥",
|
||||
"수량": "0.009",
|
||||
@@ -660,7 +660,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "레미콘", "상세구분": "레미콘", "규격": "{굵은골재최대치수}-{강도}-{슬럼프}"},
|
||||
"요소": {"구분": "레미콘", "상세구분": "레미콘", "검색어": "레미콘 {굵은골재최대치수}-{강도}-{슬럼프}"},
|
||||
"이름": "채움 콘크리트",
|
||||
"단위": "㎥",
|
||||
"수량": "채움콘크리트량",
|
||||
@@ -783,7 +783,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "모르타르", "검색어": "모르타르 1:1 서울"},
|
||||
"이름": "줄눈메꿈 모르타르",
|
||||
"단위": "㎥",
|
||||
"수량": "0.009",
|
||||
@@ -792,7 +792,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "석재", "상세구분": "조경석·자연석", "대표": "MT030608"},
|
||||
"요소": {"구분": "석재", "상세구분": "조경석·자연석", "검색어": "틈메우기돌"},
|
||||
"이름": "틈메우기돌",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QF000432, 뒷길이=뒷길이, 종별=만약(돌 == '조약돌 및 야면석' 또는 돌 == '호박돌 및 야면석', '야면석', 돌)).소요량 * 틈메우기율 / 100",
|
||||
@@ -801,7 +801,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "레미콘", "상세구분": "레미콘", "규격": "{굵은골재최대치수}-{강도}-{슬럼프}"},
|
||||
"요소": {"구분": "레미콘", "상세구분": "레미콘", "검색어": "레미콘 {굵은골재최대치수}-{강도}-{슬럼프}"},
|
||||
"이름": "채움 콘크리트(찰붙임)",
|
||||
"단위": "㎥",
|
||||
"수량": "만약(붙임 == '찰붙임', 찾기(QF000434, 뒷길이=뒷길이, 종별=만약(돌 == '조약돌 및 야면석', '야면석', 만약(돌 == '호박돌 및 야면석', '호박돌', 돌))).소요량, 0)",
|
||||
@@ -890,7 +890,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "골재"},
|
||||
"요소": {"구분": "시멘트·골재", "상세구분": "골재", "검색어": "모래 강모래"},
|
||||
"이름": "잡석(채움재)",
|
||||
"단위": "㎥",
|
||||
"수량": "잡석량",
|
||||
@@ -1024,7 +1024,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "레미콘", "상세구분": "레미콘", "규격": "{굵은골재최대치수}-{강도}-{슬럼프}"},
|
||||
"요소": {"구분": "레미콘", "상세구분": "레미콘", "검색어": "레미콘 {굵은골재최대치수}-{강도}-{슬럼프}"},
|
||||
"이름": "채움 콘크리트",
|
||||
"단위": "㎥",
|
||||
"수량": "채움콘크리트량",
|
||||
@@ -1661,7 +1661,7 @@
|
||||
"호표": [
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "돌망태·사방"},
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "돌망태·사방", "검색어": "기초사석 0.015~0.03㎥"},
|
||||
"이름": "철망태",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QF000460, 구분='자재', 명칭='철망태').수량",
|
||||
@@ -1669,7 +1669,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "돌망태·사방"},
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "돌망태·사방", "검색어": "기초사석 0.015~0.03㎥"},
|
||||
"이름": "채움재",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QF000460, 구분='자재', 명칭='채움재').수량",
|
||||
|
||||
@@ -13,6 +13,425 @@
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "브레인이 넣은 견본 · 조합 화면을 바로 볼 수 있게 함 · 지워도 됨"
|
||||
},
|
||||
{
|
||||
"키": "UA000002",
|
||||
"이름": "임도 준공표지석",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "개소",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000198", "메모": "레미콘타설(장비) 무근,소형" },
|
||||
{ "로직": "GC000211", "메모": "합판거푸집(간단) 간단" },
|
||||
{ "로직": "GC000088", "메모": "구조물 터파기(토사) 굴삭기 0.2m3" },
|
||||
{ "로직": "GF000113", "메모": "구조물 되메우기 굴삭기 0.2m3 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GF000156", "메모": "콘크리트비빔 채움재 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000214", "메모": "유로폼 설치 및 해체 간단" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "글자새김 자당 6*6 — 우리 로직에 없음\n잔토처리 굴삭기 0.2m3 — 우리 로직에 없음\n실무 호표 — 봉화 B00005 · 울진대흥 B01224"
|
||||
},
|
||||
{
|
||||
"키": "UA000003",
|
||||
"이름": "횡단개거(개소)",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "개소",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000088", "메모": "구조물 터파기(토사) 굴삭기 0.2m3" },
|
||||
{ "로직": "GF000113", "메모": "구조물 되메우기 굴삭기 0.2m3 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000198", "메모": "레미콘타설(장비) 무근" },
|
||||
{ "로직": "GC000211", "메모": "합판거푸집(간단) 간단" },
|
||||
{ "로직": "GF000158", "메모": "표면마무리 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GF000224", "메모": "깬잡석메붙임 L3=30cm내외" },
|
||||
{ "로직": "GF000101", "메모": "구조물터파기(육상토사0~1m) 기계90%+인력10%(굴삭기 0.7M3) (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GF000218", "메모": "잡석운반및다짐 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GF000124", "메모": "면고르기" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "깬잡석채집 L=30cm 내외 — 후보 「막돌 채집」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n고임돌채집 기계(0.7㎥,100%) — 후보 「큰돌 채집(기계)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n잔토처리 굴삭기 0.7m3 — 우리 로직에 없음\n측구수로관부설 500∼700kg미만 — 우리 로직에 없음\n기초잡석채집및운반부설 4.5톤(L=500) — 우리 로직에 없음\n실무 호표 — 봉화 B00008 · 봉화 B00030 · 영월 B02019 · 영월 B02052 · 영월 B02134"
|
||||
},
|
||||
{
|
||||
"키": "UA000004",
|
||||
"이름": "기슭막이",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "m",
|
||||
"담은로직": [
|
||||
{ "로직": "GF000222", "메모": "깬잡석찰쌓기 L3=45cm내외" },
|
||||
{ "로직": "GF000218", "메모": "기초다짐 및 뒤채움 소형 장비사용 시공 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000214", "메모": "유로폼 설치 및 해체 간단" },
|
||||
{ "로직": "GC000198", "메모": "레미콘타설(장비) 무근" },
|
||||
{ "로직": "GF000156", "메모": "콘크리트믹서사용 0.45 m3 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GF000220", "메모": "깬잡석메쌓기 L3=45cm내외" },
|
||||
{ "로직": "GC000677", "메모": "모르터바르기 1:3" },
|
||||
{ "로직": "GF000214", "메모": "야면석채집 L3=45cm내외 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GF000221", "메모": "돌쌓기 찰(켜)쌓기,L3=55cm이하 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000088", "메모": "구조물터파기(토사) 굴착기0.6m3" },
|
||||
{ "로직": "GF000113", "메모": "구조물되메우기 굴착기0.6m3 (실무 수는 옛 판 값)" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "깬잡석채집 L=45cm 내외 — 후보 「막돌 채집」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n고임돌채집 기계(0.7㎥,100%) — 후보 「큰돌 채집(기계)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n막자갈채집 기계(0.7㎥,100%) — 후보 「모래·자갈·약돌 채집(인력)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n고임돌채집 기계 — 후보 「큰돌 채집(기계)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n막자갈채집 기계 — 후보 「모래·자갈·약돌 채집(인력)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n고임돌채집 굴삭기0.7m3 — 후보 「큰돌 채집(기계)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n막자갈채우기 150mm내외 — 후보 「자갈고르기」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n구조물잔토처리 굴착기0.6m3 — 우리 로직에 없음\n실무 호표 — 봉화 B00010 · 봉화 B00011 · 봉화 B00022 · 봉화 B00047 · 울진대흥 B01212 · 울진대흥 B01213 · 울진대흥 B01214 · 울진대흥 B01215 · 울진대흥 B01216 · 울진대흥 B01225 · 울진대흥 B01226 · 울진대흥 B01227 · 울진대흥 B01231 · 울진소광 B02122"
|
||||
},
|
||||
{
|
||||
"키": "UA000005",
|
||||
"이름": "취수장",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "개소",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000198", "메모": "레미콘타설(장비) 무근,소형" },
|
||||
{ "로직": "GC000214", "메모": "유로폼 설치 및 해체 보통" },
|
||||
{ "로직": "GC000206", "메모": "철근가공 및 조립 Type-Ⅰ-1,현장가공조립" },
|
||||
{ "로직": "GC000207", "메모": "철근가공 및 조립 Type-Ⅰ-1,현장가공조립" },
|
||||
{ "로직": "GC000088", "메모": "구조물 터파기(토사) 굴삭기 0.2m3" },
|
||||
{ "로직": "GF000113", "메모": "구조물 되메우기 굴삭기 0.2m3 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000224", "메모": "지수판 설치(PVC 용접) 200×5t" },
|
||||
{ "로직": "GC000393", "메모": "배수관부설및접합(P.V.C) Ø 50 MM - VG2 일반관" },
|
||||
{ "로직": "GC000211", "메모": "합판거푸집 4회" },
|
||||
{ "로직": "GC000015", "메모": "강관동바리 2.5m이하(1개월) (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000821", "메모": "잡철물제작및설치(현장) 일반철재" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "PE관 부설 및 접합 D50 — 후보 「부단수 할정자관 부설 및 접합」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n기초잡석채집및운반부설 4.5톤(L=500) — 우리 로직에 없음\n실무 호표 — 봉화 B00012 · 영월 B02002 · 거창 제12호표"
|
||||
},
|
||||
{
|
||||
"키": "UA000006",
|
||||
"이름": "콘크리트포장(㎡)",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "㎡",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000297", "메모": "콘크리트포장포설 A-Type,t=20CM이하" },
|
||||
{ "로직": "GC000665", "메모": "와이어메쉬 바닥깔기" },
|
||||
{ "로직": "GF000156", "메모": "콘크리트믹서사용 0.45 m3 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GF000162", "메모": "콘크리트포장(B-type) t=20cm (실무 수는 옛 판 값)" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "실무 호표 — 봉화 B00014 · 울진대흥 B01232"
|
||||
},
|
||||
{
|
||||
"키": "UA000007",
|
||||
"이름": "콘크리트포장(m)",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "m",
|
||||
"담은로직": [{ "로직": "GF000162", "메모": "콘크리트포장(인력) T=20㎝" }, { "로직": "GC000211", "메모": "합판거푸집(6회) 간단" }, { "로직": "GF000163", "메모": "콘크리트포장절단공 임도 (실무 수는 옛 판 값)" }],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "기초잡석채집및운반부설 4.5톤(L=500) — 우리 로직에 없음\n실무 호표 — 영월 B02059"
|
||||
},
|
||||
{
|
||||
"키": "UA000008",
|
||||
"이름": "관보호공(유입·유출구)",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "개소",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000198", "메모": "레미콘타설(장비) 무근,소형" },
|
||||
{ "로직": "GC000214", "메모": "유로폼 설치 및 해체 보통" },
|
||||
{ "로직": "GC000206", "메모": "철근가공 및 조립 Type-Ⅰ-1,현장가공조립" },
|
||||
{ "로직": "GC000207", "메모": "철근가공 및 조립 Type-Ⅰ-1,현장가공조립" },
|
||||
{ "로직": "GF000218", "메모": "기초다짐 및 뒤채움 소형 장비사용 시공 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GF000225", "메모": "깬잡석찰붙임 L3=30cm내외" },
|
||||
{ "로직": "GF000156", "메모": "콘크리트(기계비빔) 무근구조물 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GF000226", "메모": "큰돌메쌓기 뒷길이 0.6~0.8m" },
|
||||
{ "로직": "GF000214", "메모": "야면석채집 L3=45cm내외 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GF000222", "메모": "찰쌓기(켜쌓기) L3=55cm이하 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000677", "메모": "모르터바르기 1:3" },
|
||||
{ "로직": "GF000221", "메모": "돌쌓기 찰(켜)쌓기,L3=55cm이하 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000088", "메모": "구조물터파기(토사) 굴착기0.6m3" },
|
||||
{ "로직": "GF000113", "메모": "구조물되메우기 굴착기0.6m3 (실무 수는 옛 판 값)" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "막자갈채집 기계(0.7㎥,100%) — 후보 「모래·자갈·약돌 채집(인력)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n깬잡석채집 L=30cm 내외 — 후보 「막돌 채집」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n고임돌채집 기계(0.7㎥,100%) — 후보 「큰돌 채집(기계)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n기초잡석채집및운반부설 4.5톤(L=500) — 우리 로직에 없음\n야면석 채집 운반 (25x25x35㎝)장비 — 후보 「야면석 채집(인력)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n고임돌채집운반 4.5톤덤프(L=500) — 후보 「큰돌 채집(기계)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n큰돌채집운반(뒷길이 0.6~0.8m) 장비(80%) + 인력(20%) — 후보 「모래·자갈·약돌 채집(인력)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n녹생토(종자배합) 암절개지 및 특수지반 녹화용 (700kg/㎥) — 우리 로직에 없음\n막자갈채집 기계 — 후보 「모래·자갈·약돌 채집(인력)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n고임돌채집 기계 — 후보 「큰돌 채집(기계)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n고임돌채집 굴삭기0.7m3 — 후보 「큰돌 채집(기계)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n막자갈(파쇄석)채집 — 후보 「막돌 채집」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n막자갈채우기 150mm내외 — 후보 「자갈고르기」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n구조물잔토처리 굴착기0.6m3 — 우리 로직에 없음\n실무 호표 — 봉화 B00017 · 봉화 B00018 · 봉화 B00029 · 봉화 B00048 · 봉화 B00049 · 봉화 B00055 · 봉화 B00056 · 영월 B01981 · 영월 B01983 · 영월 B01988 · 영월 B02045 · 영월 B02155 · 울진대흥 B01219 · 울진대흥 B01220 · 울진소광 B02120"
|
||||
},
|
||||
{
|
||||
"키": "UA000009",
|
||||
"이름": "돌붙임",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "㎡",
|
||||
"담은로직": [
|
||||
{ "로직": "GF000225", "메모": "깬잡석찰붙임 L3=30cm내외" },
|
||||
{ "로직": "GF000156", "메모": "콘크리트(기계비빔) 무근구조물 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GF000224", "메모": "깬잡석메붙임 L3=30cm내외" },
|
||||
{ "로직": "GF000101", "메모": "구조물터파기(육상토사0~1m) 기계90%+인력10%(굴삭기 0.7M3) (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000198", "메모": "레미콘타설(장비) 무근" },
|
||||
{ "로직": "GF000214", "메모": "야면석채집 L3=45cm내외 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000677", "메모": "모르터바르기 1:3" },
|
||||
{ "로직": "GC000088", "메모": "구조물터파기(토사) 굴착기0.6m3" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "깬잡석채집 L=30cm 내외 — 후보 「막돌 채집」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n고임돌채집 기계(0.7㎥,100%) — 후보 「큰돌 채집(기계)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n잔토처리 굴삭기 0.7m3 — 우리 로직에 없음\n깬잡석채집 L=60cm 내외 — 후보 「막돌 채집」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n고임돌채집 기계 — 후보 「큰돌 채집(기계)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n파쇄석 채집 L3=35cm내외 — 후보 「막돌 채집」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n고임돌채집 굴삭기0.2m3 — 후보 「큰돌 채집(기계)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n구조물잔토처리 굴착기0.6m3 — 우리 로직에 없음\n실무 호표 — 봉화 B00020 · 봉화 B00032 · 봉화 B00036 · 봉화 B00064 · 울진대흥 B01222 · 울진대흥 B01223 · 울진대흥 B01229 · 울진대흥 B01230 · 울진소광 B02125"
|
||||
},
|
||||
{
|
||||
"키": "UA000010",
|
||||
"이름": "임도차단기",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "개소",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000821", "메모": "잡철물 제작및설치(현장) 일반철재" },
|
||||
{ "로직": "GC000198", "메모": "레미콘타설(장비) 무근,소형" },
|
||||
{ "로직": "GC000206", "메모": "철근가공 및 조립 Type-Ⅰ-1,현장가공조립" },
|
||||
{ "로직": "GC000207", "메모": "철근가공 및 조립 Type-Ⅰ-1,현장가공조립" },
|
||||
{ "로직": "GC000214", "메모": "유로폼 설치 및 해체 간단" },
|
||||
{ "로직": "GC000714", "메모": "철부도장페인트 녹막이1회조합2회 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000712", "메모": "철부도장페인트 녹막이1회조합2회 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000088", "메모": "구조물 터파기(토사) 굴삭기 0.2m3" },
|
||||
{ "로직": "GF000113", "메모": "구조물 되메우기 굴삭기 0.2m3 (실무 수는 옛 판 값)" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "잔토처리 굴삭기 0.2m3 — 우리 로직에 없음\n실무 호표 — 봉화 B00025"
|
||||
},
|
||||
{
|
||||
"키": "UA000011",
|
||||
"이름": "돌수로",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "m",
|
||||
"담은로직": [
|
||||
{ "로직": "GF000225", "메모": "깬잡석찰붙임 L3=35cm내외" },
|
||||
{ "로직": "GF000156", "메모": "콘크리트믹서사용 0.45 m3 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GF000224", "메모": "야면석메붙임 뒷길이 35Cm" },
|
||||
{ "로직": "GF000221", "메모": "돌쌓기 찰(켜)쌓기,L3=55cm이하 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000677", "메모": "모르타르배합 1:3" },
|
||||
{ "로직": "GC000088", "메모": "구조물터파기(토사) 굴착기0.6m3" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "깬잡석채집 L=35cm 내외 — 후보 「막돌 채집」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n고임돌채집 기계(0.7㎥,100%) — 후보 「큰돌 채집(기계)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n야면석 채집 운반 (25x25x35㎝)장비 — 후보 「야면석 채집(인력)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n고임돌채집운반 4.5톤덤프(L=500) — 후보 「큰돌 채집(기계)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n야면석소운반(L=35CM) (L=20m) — 우리 로직에 없음\n콘크리트소운반 (L=20m) — 우리 로직에 없음\n고임돌채집 굴삭기0.7m3 — 후보 「큰돌 채집(기계)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n구조물잔토처리 굴착기0.6m3 — 우리 로직에 없음\n실무 호표 — 봉화 B00034 · 영월 B02016 · 영월 B02017 · 울진소광 B02124"
|
||||
},
|
||||
{
|
||||
"키": "UA000012",
|
||||
"이름": "포장 난간",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "m",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000198", "메모": "레미콘타설(장비) 무근,소형" },
|
||||
{ "로직": "GC000214", "메모": "유로폼 설치 및 해체 간단" },
|
||||
{ "로직": "GC000206", "메모": "철근가공및조립(현장) 일반(Type-Ⅰ-1)" },
|
||||
{ "로직": "GC000207", "메모": "철근가공및조립(현장) 일반(Type-Ⅰ-1)" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "실무 호표 — 봉화 B00038"
|
||||
},
|
||||
{
|
||||
"키": "UA000013",
|
||||
"이름": "관보호공 안전난간",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "개소",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000198", "메모": "레미콘타설(장비) 무근,소형" },
|
||||
{ "로직": "GC000211", "메모": "합판거푸집 3회(복잡)" },
|
||||
{ "로직": "GC000206", "메모": "철근가공 및 조립 Type-Ⅰ-1,현장가공조립" },
|
||||
{ "로직": "GC000207", "메모": "철근가공 및 조립 Type-Ⅰ-1,현장가공조립" },
|
||||
{ "로직": "GC000712", "메모": "유성페인트칠(붓칠)-황색 콘크리트,모르타르면3회 (실무 수는 옛 판 값)" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "실무 호표 — 봉화 B00041"
|
||||
},
|
||||
{
|
||||
"키": "UA000014",
|
||||
"이름": "BOX 암거",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "m",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000198", "메모": "레미콘타설(장비) 철근" },
|
||||
{ "로직": "GC000214", "메모": "유로폼 설치 및 해체 보통" },
|
||||
{ "로직": "GC000206", "메모": "철근가공 및 조립 Type-Ⅱ-1,현장가공조립" },
|
||||
{ "로직": "GC000207", "메모": "철근가공 및 조립 Type-Ⅱ-1,현장가공조립" },
|
||||
{ "로직": "GC000015", "메모": "강관동바리 암거용-3개월사용 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000022", "메모": "강관비계 3개월사용 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000211", "메모": "합판거푸집 6회 (실무 수는 옛 판 값)" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "실무 호표 — 봉화 B00058 · 거창 제5호표 · 거창 제6호표"
|
||||
},
|
||||
{
|
||||
"키": "UA000015",
|
||||
"이름": "BOX 난간",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "m",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000198", "메모": "레미콘타설(장비) 무근" },
|
||||
{ "로직": "GC000214", "메모": "유로폼 설치 및 해체 간단" },
|
||||
{ "로직": "GC000206", "메모": "철근가공 및 조립 Type-Ⅰ-1,현장가공조립" },
|
||||
{ "로직": "GC000207", "메모": "철근가공 및 조립 Type-Ⅰ-1,현장가공조립" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "실무 호표 — 봉화 B00062 · 거창 제9호표"
|
||||
},
|
||||
{
|
||||
"키": "UA000016",
|
||||
"이름": "국가지점번호판",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "개소",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000198", "메모": "무근콘크리트타설 인력운반타설" },
|
||||
{ "로직": "GC000211", "메모": "합판거푸집(6회) 간단" },
|
||||
{ "로직": "GF000101", "메모": "구조물터파기(인력) 경질토사 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GF000113", "메모": "구조물되메우기 (인력) (실무 수는 옛 판 값)" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "잔토처리 인력100% — 우리 로직에 없음\n실무 호표 — 영월 B00704"
|
||||
},
|
||||
{
|
||||
"키": "UA000017",
|
||||
"이름": "임도 안전표지판",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "개소",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000308", "메모": "임도안전표지판설치 삼각,D=900 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000198", "메모": "무근콘크리트타설 인력운반타설" },
|
||||
{ "로직": "GC000211", "메모": "합판거푸집(6회) 간단" },
|
||||
{ "로직": "GF000101", "메모": "구조물터파기(인력) 경질토사 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GF000113", "메모": "구조물되메우기 (인력) (실무 수는 옛 판 값)" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "기초잡석채집및운반부설 4.5톤(L=500) — 우리 로직에 없음\n잔토처리 인력100% — 우리 로직에 없음\n실무 호표 — 영월 B01982"
|
||||
},
|
||||
{
|
||||
"키": "UA000018",
|
||||
"이름": "도로반사경 설치",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "개소",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000198", "메모": "콘크리트타설 임도품셈 5-5 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000211", "메모": "합판거푸집(6회) 간단" },
|
||||
{ "로직": "GF000101", "메모": "구조물터파기 인력(경질토사) (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GF000113", "메모": "구조물되메우기 (인력) (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000310", "메모": "도로반사경설치 D=1000(STS)" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "기초잡석채집및운반부설 4.5톤(L=500) — 우리 로직에 없음\n잔토처리 인력100% — 우리 로직에 없음\n실무 호표 — 영월 B01997"
|
||||
},
|
||||
{
|
||||
"키": "UA000019",
|
||||
"이름": "취수장 안내판",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "개소",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000198", "메모": "무근콘크리트타설 인력운반타설" },
|
||||
{ "로직": "GC000211", "메모": "합판거푸집(6회) 간단" },
|
||||
{ "로직": "GF000101", "메모": "구조물터파기(인력) 경질토사 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GF000113", "메모": "구조물되메우기 (인력) (실무 수는 옛 판 값)" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "기초잡석채집및운반부설 4.5톤(L=500) — 우리 로직에 없음\n잔토처리 인력100% — 우리 로직에 없음\n실무 호표 — 영월 B02152"
|
||||
},
|
||||
{
|
||||
"키": "UA000020",
|
||||
"이름": "자생수종 이식",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "㎡",
|
||||
"담은로직": [{ "로직": "GC000149", "메모": "관목식재(단식) H=0.3~0.7m" }, { "로직": "GC000148", "메모": "굴취(관목) H=0.3~0.7m" }],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "인력운반 기타 — 우리 로직에 없음\n실무 호표 — 계류보전 B01099"
|
||||
},
|
||||
{
|
||||
"키": "UA000021",
|
||||
"이름": "기초 콘크리트(m)",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "m",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000198", "메모": "레미콘타설 무근(장비)" },
|
||||
{ "로직": "GC000214", "메모": "유로폼 설치 및 해체 간단" },
|
||||
{ "로직": "GC000088", "메모": "구조물터파기(토사) 굴착기0.6m3" },
|
||||
{ "로직": "GF000113", "메모": "구조물되메우기 굴착기0.6m3 (실무 수는 옛 판 값)" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "구조물잔토처리 굴착기0.6m3 — 우리 로직에 없음\n실무 호표 — 울진소광 B02121"
|
||||
},
|
||||
{
|
||||
"키": "UA000022",
|
||||
"이름": "기초 콘크리트(개소)",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "개소",
|
||||
"담은로직": [{ "로직": "GC000198", "메모": "레미콘타설 무근(장비)" }, { "로직": "GC000214", "메모": "유로폼 설치 및 해체 간단" }],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "실무 호표 — 울진소광 B02129"
|
||||
},
|
||||
{
|
||||
"키": "UA000023",
|
||||
"이름": "돌조공",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "m",
|
||||
"담은로직": [
|
||||
{ "로직": "GF000220", "메모": "돌쌓기(깬돌) 메(골)쌓기,L3=55cm이하" },
|
||||
{ "로직": "GC000088", "메모": "구조물터파기(토사) 굴착기0.6m3" },
|
||||
{ "로직": "GF000113", "메모": "구조물되메우기 굴착기0.6m3 (실무 수는 옛 판 값)" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "고임돌채집 굴삭기0.7m3 — 후보 「큰돌 채집(기계)」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n막자갈채우기 150mm내외 — 후보 「자갈고르기」 와 수가 다름(옛 판 값) · 이름이 달라 뺌\n구조물잔토처리 굴착기0.6m3 — 우리 로직에 없음\n실무 호표 — 울진소광 B02123"
|
||||
},
|
||||
{
|
||||
"키": "UA000024",
|
||||
"이름": "L형옹벽 측구",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "m",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000198", "메모": "레미콘타설 무근(장비)" },
|
||||
{ "로직": "GC000214", "메모": "유로폼 설치 및 해체 보통" },
|
||||
{ "로직": "GF000161", "메모": "문양 거푸집" },
|
||||
{ "로직": "GC000206", "메모": "철근(현장)가공 Type-Ⅰ-----아님 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000207", "메모": "철근(현장)조립 Type-Ⅰ-----아님" },
|
||||
{ "로직": "GC000088", "메모": "구조물터파기(토사) 굴착기0.6m3" },
|
||||
{ "로직": "GF000113", "메모": "구조물되메우기 굴착기0.6m3 (실무 수는 옛 판 값)" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "물구멍설치 D=50mm — 우리 로직에 없음\n실무 호표 — 울진소광 B02128"
|
||||
},
|
||||
{
|
||||
"키": "UA000025",
|
||||
"이름": "날개벽",
|
||||
"구분": "실무 일위대가",
|
||||
"상세구분": null,
|
||||
"단위": "개소",
|
||||
"담은로직": [
|
||||
{ "로직": "GC000198", "메모": "레미콘타설(장비사용타설) 철근구조물 (실무 수는 옛 판 값)" },
|
||||
{ "로직": "GC000214", "메모": "유로폼 보통,0~7m" },
|
||||
{ "로직": "GC000206", "메모": "철근가공및조립(현장) Type-Ⅱ-1" },
|
||||
{ "로직": "GC000207", "메모": "철근가공및조립(현장) Type-Ⅱ-1" }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": "실무 호표 — 거창 제7호표 · 거창 제8호표"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -369,10 +369,10 @@ def test_구분_목록(client: TestClient) -> None:
|
||||
assert steel["details"]
|
||||
|
||||
|
||||
def test_재료_고르기_줄_단가는_대표_자재품목의_낮은_값(client: TestClient) -> None:
|
||||
"""옛 「MP:시멘트」 줄이 고르기 조건+대표로 바뀜 — 값은 대표 자재품목 줄의 낮은 값."""
|
||||
def test_재료_고르기_줄_단가는_검색어_첫째_자재품목의_낮은_값(client: TestClient) -> None:
|
||||
"""옛 대표 키 줄이 기본 검색어로 바뀜(PLAN 8-3) — 값은 검색어 첫째(옛 대표) 줄의 낮은 값."""
|
||||
one = _get(client, "/api/m01/logic", key=key_of("GC000628"))
|
||||
cond = {"구분": "시멘트·골재", "상세구분": "시멘트", "대표": "MT004176"}
|
||||
cond = {"구분": "시멘트·골재", "상세구분": "시멘트", "검색어": "시멘트 전국"}
|
||||
cement = one["prices"][store.cm.mf.cond_text(cond)]
|
||||
assert cement["ref"] == "MT004176" and cement["값"] > 0 and cement["후보"] >= 1
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
"""M01 견본 입력(`GET /logic/auto`) · 값 빈 품목은 그 줄만 비움 — PLAN 3-2 · 계약 3-1 표.
|
||||
|
||||
자동값 = 고르기 목록 첫째 · 범위 최솟값 · 찾기 조건 값은 그 표 가장 낮은 줄 · 나누는 수 1 · 그 밖 0.
|
||||
정본 폴더를 읽기만 함(쓰지 않음).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from M01_MasterData import M01_MasterData_Router as router_module
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client() -> TestClient:
|
||||
app = FastAPI()
|
||||
app.include_router(router_module.router)
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def _auto(client: TestClient, key: str) -> dict:
|
||||
got = client.get("/api/m01/logic/auto", params={"key": key})
|
||||
assert got.status_code == 200, got.text
|
||||
return got.json()["값"]
|
||||
|
||||
|
||||
def _calc(client: TestClient, key: str) -> dict:
|
||||
got = client.post("/api/m01/calc", json={"key": key, "inputs": _auto(client, key)}).json()
|
||||
assert got["ok"] is True, got
|
||||
return got
|
||||
|
||||
|
||||
def test_고르기는_목록_첫째_범위는_최솟값(client) -> None:
|
||||
got = _auto(client, "GF000160") # 12-4 합판거푸집
|
||||
assert got["횟수"] == "1회사용시" and got["폼타이"] == "아님"
|
||||
assert "합판" not in got # 재료 입력 키 목록은 걷음 — 품목은 기본 검색어(PLAN 8-3)
|
||||
assert got["소형할증"] == 0 # 범위 [0, 30]
|
||||
assert got["높이할증"] == 0 # 찾기·나누기에 안 쓰임
|
||||
|
||||
|
||||
def test_찾기_조건_값은_표_가장_낮은_줄(client) -> None:
|
||||
assert _auto(client, "GC000561")["층수층"] == 0 # 가장 낮은 줄 「6 미만」 — 아래 끝이 열림
|
||||
assert _auto(client, "GC000559")["강재총사용량"] == 0 # 「60 미만」
|
||||
# 두 표가 같은 입력을 씀 — 한 표는 6 부터 · 한 표는 15 부터 → 둘 다에 드는 가장 낮은 값
|
||||
assert _auto(client, "GC000724")["규격"] == 15
|
||||
|
||||
|
||||
def test_나누는_수는_1(client) -> None:
|
||||
assert _auto(client, "GC000998")["수직고초과단계"] == 1
|
||||
assert _auto(client, "GC000278")["두께"] == 1
|
||||
|
||||
|
||||
def test_없는_로직은_404(client) -> None:
|
||||
assert client.get("/api/m01/logic/auto", params={"key": "GF999999"}).status_code == 404
|
||||
|
||||
|
||||
def test_견본_금액은_앞과_같음(client) -> None:
|
||||
assert Decimal(str(_calc(client, "GF000160")["sums"]["계"])) == Decimal("117485.212")
|
||||
assert Decimal(str(_calc(client, "GF000219")["sums"]["계"])) == Decimal("79279.668")
|
||||
|
||||
|
||||
def test_후보_0_값_빈_품목은_그_줄만_비움(client) -> None:
|
||||
got = _calc(client, "GC000027") # 가새(후보 0) · 발판(값 없음)
|
||||
blank = {one["이름"]: one for one in got["lines"] if one.get("까닭")}
|
||||
assert len(blank) == 2
|
||||
for one in blank.values():
|
||||
assert one["단가"] is None and one["금액"] is None
|
||||
assert "후보 없음" in blank["가새 L1518-2개"]["까닭"]
|
||||
assert "값 없음" in blank["발판 45×200×2000"]["까닭"]
|
||||
rest = sum(Decimal(str(one["금액"])) for one in got["lines"] if one["금액"] is not None)
|
||||
assert Decimal(str(got["sums"]["계"])) == rest > 0 # 빈 줄은 빼고 더함
|
||||
|
||||
|
||||
def test_빈_줄을_부르는_덧줄도_비움(client) -> None:
|
||||
got = _calc(client, "GF000185") # 레미콘 후보 0 → 레미콘 금액을 부르는 덧줄
|
||||
blank = [one for one in got["lines"] if one.get("까닭")]
|
||||
assert [one["이름"] for one in blank] == ["구체콘크리트(철근) 레미콘", "콘크리트다짐"]
|
||||
assert "구체콘크리트(철근) 레미콘" in blank[1]["까닭"] and blank[1]["금액"] is None
|
||||
|
||||
|
||||
def test_자동값으로_로직_전부_멈춤_0() -> None:
|
||||
files, whole = store.loaded()
|
||||
stuck = []
|
||||
for data in files.values():
|
||||
if data.get("그룹") != "로직":
|
||||
continue
|
||||
for row in data.get("줄") or []:
|
||||
try:
|
||||
store.cm.mf.run(whole, str(row["키"]), store.mau.values(whole, row))
|
||||
except (store.cm.mf.FormulaError, ArithmeticError, KeyError, TypeError) as e:
|
||||
stuck.append(f"{row['키']} {e}")
|
||||
assert stuck == []
|
||||
|
||||
|
||||
def test_원_단위_입력은_자동값_null(client) -> None:
|
||||
got = _auto(client, "GF000056") # 체인톱가격(원) · 체인오일단가(원/ℓ)
|
||||
assert got["체인톱가격"] is None and got["체인오일단가"] is None
|
||||
assert got["소작업로"] == 0 # 원 아닌 수는 그대로 0
|
||||
|
||||
|
||||
def test_빈_입력을_쓰는_줄만_비움(client) -> None:
|
||||
got = _calc(client, "GF000056")
|
||||
blank = {one["이름"]: one for one in got["lines"] if one.get("까닭")}
|
||||
assert set(blank) == {"체인오일", "체인톱 손료"}
|
||||
for one in blank.values():
|
||||
assert one["단가"] is None and one["금액"] is None and "비어 있음" in one["까닭"]
|
||||
rest = sum(Decimal(str(one["금액"])) for one in got["lines"] if one["금액"] is not None)
|
||||
assert Decimal(str(got["sums"]["계"])) == rest > 0 # 빈 줄은 빼고 더함
|
||||
assert got["sums"]["경비"] == 0
|
||||
|
||||
|
||||
def test_빠진_원_입력도_null_처럼_줄만_비움(client) -> None:
|
||||
given = {
|
||||
k: v
|
||||
for k, v in _auto(client, "GF000056").items()
|
||||
if k not in ("체인톱가격", "체인오일단가")
|
||||
}
|
||||
for path in ("calc", "text"):
|
||||
got = client.post(f"/api/m01/{path}", json={"key": "GF000056", "inputs": given}).json()
|
||||
assert got["ok"] is True, (path, got)
|
||||
lines = client.post("/api/m01/calc", json={"key": "GF000056", "inputs": given}).json()["lines"]
|
||||
assert {o["이름"] for o in lines if o.get("까닭")} == {"체인오일", "체인톱 손료"}
|
||||
|
||||
|
||||
def test_표_찾기_실패는_그_줄만_비우고_까닭은_날글_없이(client) -> None:
|
||||
given = {**_auto(client, "GC001000"), "시공구분": "기계시공"} # 표 1-2-3 에 그 조합 줄이 없음
|
||||
got = client.post("/api/m01/calc", json={"key": "GC001000", "inputs": given}).json()
|
||||
assert got["ok"] is True, got
|
||||
blank = [one for one in got["lines"] if one.get("까닭")]
|
||||
assert blank and all(one["금액"] is None for one in blank)
|
||||
why = blank[0]["까닭"]
|
||||
assert "표 QC001869(1-2-3)에 시공구분 기계시공" in why and "인 줄이 없음" in why
|
||||
assert not any(ch in why for ch in "{}'") and "Decimal" not in why
|
||||
rest = sum(Decimal(str(one["금액"])) for one in got["lines"] if one["금액"] is not None)
|
||||
assert Decimal(str(got["sums"]["계"])) == rest
|
||||
@@ -3,7 +3,7 @@
|
||||
계약 `resources/master_data/_화면_계약.md` 7장 · 규칙 `resources/master_data/ref/_설계_식_복사.md`.
|
||||
못박는 것
|
||||
- 13-4-1 을 식으로 펴 아무것도 안 고치고 되돌려 저장하면 원본과 계산 결과가 같음.
|
||||
- 12-4 처럼 같은 표를 다른 인자로 쓰는 로직은 별칭이 번호로 갈림.
|
||||
- 12-4 처럼 같은 표를 다른 인자로 쓰는 로직은 별칭이 찾은 줄 값으로 갈림.
|
||||
- 로직 전부(1,354줄)가 펴고 되돌리면 원래 식 글자 그대로 돌아옴.
|
||||
"""
|
||||
|
||||
@@ -83,10 +83,12 @@ def test_별칭이_식에서_이름_하나로_보이고_원문은_뒤에_남는
|
||||
assert "높이에대한증가율표.증가율" in 가로["중간[0].식"]
|
||||
|
||||
|
||||
def test_같은_표를_다른_인자로_쓰면_별칭이_번호로_갈린다(client: TestClient) -> None:
|
||||
def test_같은_표를_다른_인자로_쓰면_별칭이_찾은_줄_값으로_갈린다(client: TestClient) -> None:
|
||||
got = _get(client, "/api/m01/logic/formula", key=FORM)
|
||||
형틀 = [one for one in got["별칭"] if one["참조"] == "QF000357"]
|
||||
assert [one["이름"] for one in 형틀] == [f"합판거푸집{n}" for n in range(1, 8)]
|
||||
assert [one["이름"] for one in 형틀] == [
|
||||
f"합판거푸집_{n}" for n in ("합판", "각재", "철선", "못", "박리제", "형틀목공", "보통인부")
|
||||
]
|
||||
assert [one["원문"] for one in 형틀][:2] == [
|
||||
"찾기(QF000357, 구분='합판')",
|
||||
"찾기(QF000357, 구분='각재')",
|
||||
@@ -96,8 +98,8 @@ def test_같은_표를_다른_인자로_쓰면_별칭이_번호로_갈린다(cli
|
||||
assert len(한벌) == 1 and 한벌[0]["자리"] == ["중간[0].식", "중간[1].식"]
|
||||
|
||||
가로 = {one["자리"]: one["식"] for one in got["줄"]}
|
||||
assert 가로["호표[0].수량"] == "합판거푸집1.기준수량_1회사용 * 재료율"
|
||||
assert 가로["호표[1].수량"] == "합판거푸집2.기준수량_1회사용 * 재료율"
|
||||
assert 가로["호표[0].수량"] == "합판거푸집_합판.기준수량_1회사용 * 재료율"
|
||||
assert 가로["호표[1].수량"] == "합판거푸집_각재.기준수량_1회사용 * 재료율"
|
||||
|
||||
|
||||
def test_로직_부르기도_별칭으로_보인다(client: TestClient) -> None:
|
||||
@@ -155,7 +157,7 @@ def test_식을_고쳐_저장하면_그_값으로_계산된다(client: TestClien
|
||||
|
||||
view = _get(client, "/api/m01/logic/formula", key=FORM)
|
||||
row = view["로직"]
|
||||
row["호표"][6]["수량"] = "합판거푸집6.기준수량_1회사용 * 인력율 * 2" # 형틀목공 품 두 배
|
||||
row["호표"][6]["수량"] = "합판거푸집_형틀목공.기준수량_1회사용 * 인력율 * 2" # 형틀목공 품 두 배
|
||||
made = _post(
|
||||
client,
|
||||
"/api/m01/logic/formula/save",
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
"""M01 재료 기본 검색어 · 고름 · 단위 환산 · 단위 경고 — PLAN 8-1 · 8-2 계약.
|
||||
|
||||
검색어 든 견본 로직 줄을 시험 안에서 만들어(`POST /calc` 의 `row` — 저장 안 함) 돌림. 정본 폴더는 읽기만.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from M01_MasterData import M01_MasterData_Router as router_module
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
|
||||
mf = store.cm.mf
|
||||
mp = mf.mp
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client() -> TestClient:
|
||||
app = FastAPI()
|
||||
app.include_router(router_module.router)
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def _sample(term: str, unit: str, **cond) -> dict:
|
||||
"""재료 줄 하나짜리 견본 로직 — 검색어 줄 하나 · 수량 1."""
|
||||
return {
|
||||
"이름": "검색어 견본",
|
||||
"구분": "자체",
|
||||
"입력": [],
|
||||
"호표": [
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"검색어": term, **cond},
|
||||
"이름": "견본 재료",
|
||||
"단위": unit,
|
||||
"수량": "1",
|
||||
"비목": "재료비",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _line(client: TestClient, row: dict, picks: dict | None = None, path: str = "calc") -> dict:
|
||||
body = {"key": "GF999999", "inputs": {}, "row": row, **({"고름": picks} if picks else {})}
|
||||
got = client.post(f"/api/m01/{path}", json=body).json()
|
||||
assert got.get("ok", True) is True, got
|
||||
return got["lines"][0] if path == "calc" else got
|
||||
|
||||
|
||||
def test_검색어로_찾고_지역_낱말이_맞는_줄이_먼저(client) -> None:
|
||||
seoul = _line(client, _sample("합판(내수) 12t 서울", "㎡"))
|
||||
assert seoul["출처"].startswith("MT000018") and seoul["단가"] == 11354 # 값 열 가운데 낮은 값
|
||||
busan = _line(client, _sample("합판(내수) 12t 부산", "㎡"))
|
||||
assert busan["출처"].startswith("MT000020") and busan["단가"] == 11488
|
||||
assert "단위경고" not in seoul
|
||||
|
||||
|
||||
def test_고름은_그_줄만_그_품목으로_셈(client) -> None:
|
||||
row = _sample("합판(내수) 12t 서울", "㎡")
|
||||
got = _line(client, row, {"0": "MT000019"})
|
||||
assert got["출처"].startswith("MT000019") and got["단가"] == 11556
|
||||
assert _line(client, row)["단가"] == 11354 # 요청마다 — 저장 안 함
|
||||
text = _line(client, row, {"0": "MT000019"}, "text")
|
||||
assert text["ok"] is True # 읽는 식도 같은 몸으로 돎
|
||||
|
||||
|
||||
def test_같은_성격_단위는_줄_단위로_환산(client) -> None:
|
||||
got = _line(client, _sample("어닐링철선 서울 4.0mm", "톤")) # 자재 kg → 줄 톤
|
||||
assert got["단가"] == 1610000 and "단위경고" not in got
|
||||
got = _line(client, _sample("어닐링철선 서울 4.0mm", "g"))
|
||||
assert got["단가"] == pytest.approx(1.61)
|
||||
|
||||
|
||||
def test_성격이_다르면_계산은_하고_단위경고(client) -> None:
|
||||
got = _line(client, _sample("어닐링철선 서울 4.0mm", "m"))
|
||||
assert got["단가"] == 1610 and got["단위경고"] == "줄 m · 자재 kg"
|
||||
assert got["금액"] == 1610
|
||||
|
||||
|
||||
def test_검색어_후보가_없으면_그_줄만_비움(client) -> None:
|
||||
got = client.post(
|
||||
"/api/m01/calc", json={"key": "GF999999", "inputs": {}, "row": _sample("없는물건zzz", "개")}
|
||||
).json()
|
||||
assert got["ok"] is True and got["lines"][0]["금액"] is None
|
||||
assert "후보 없음" in got["lines"][0]["까닭"]
|
||||
|
||||
|
||||
def test_기계_입력은_목록_밖_원문번호도_받음(client) -> None:
|
||||
key = "GC000267"
|
||||
inputs = {**store.auto(key)["값"], "기계": "0101-0007"} # 고르기 목록 밖 · 기계 마스터엔 있음
|
||||
got = client.post("/api/m01/calc", json={"key": key, "inputs": inputs}).json()
|
||||
assert got["ok"] is True, got
|
||||
bad = client.post("/api/m01/calc", json={"key": key, "inputs": {**inputs, "기계": "9999-9999"}})
|
||||
assert bad.json()["ok"] is False # 기계 마스터에 없는 번호는 그대로 막힘
|
||||
|
||||
|
||||
def test_단위_환산_표() -> None:
|
||||
assert mp.convert(Decimal(1610), "kg", "톤") == 1610000
|
||||
assert mp.convert(Decimal(5), "Ton", "kg") == Decimal("0.005")
|
||||
assert mp.convert(Decimal(100), "㎡", "㎠") == Decimal("0.01")
|
||||
assert mp.convert(Decimal(7), "kg", "m") == 7 # 성격이 다르면 그대로
|
||||
assert mp.same_kind("ℓ", "㎥") and not mp.same_kind("kg", "m")
|
||||
|
||||
|
||||
def test_check_master_검색어_후보0_단위경고(client) -> None:
|
||||
whole = store.loaded()[1]
|
||||
assert mf.check_pick(whole, "줄", {"검색어": "합판(내수) 12t 서울"}) == []
|
||||
assert "후보 0" in mf.check_pick(whole, "줄", {"검색어": "없는물건zzz"})[0]
|
||||
assert mf.check_unit(whole, "줄", {"검색어": "어닐링철선 서울 4.0mm"}, "kg") == []
|
||||
assert "성격이 다름" in mf.check_unit(whole, "줄", {"검색어": "어닐링철선 서울 4.0mm"}, "m")[0]
|
||||
|
||||
|
||||
def test_기계_입력은_EQ_키도_원문번호로_풀어_받음(client) -> None:
|
||||
key = "GC000267"
|
||||
number = "0101-0007" # 고르기 목록 밖
|
||||
eq = next(k for k, r in store.loaded()[1].index["EQ"].items() if r.get("원문번호") == number)
|
||||
base = {**store.auto(key)["값"]}
|
||||
by_number = client.post(
|
||||
"/api/m01/calc", json={"key": key, "inputs": {**base, "기계": number}}
|
||||
).json()
|
||||
by_key = client.post("/api/m01/calc", json={"key": key, "inputs": {**base, "기계": eq}}).json()
|
||||
assert by_key["ok"] is True, by_key
|
||||
assert by_key["sums"] == by_number["sums"] # EQ 키 = 원문번호와 같은 계산
|
||||
bad = client.post("/api/m01/calc", json={"key": key, "inputs": {**base, "기계": "EQ999999"}})
|
||||
assert bad.json()["ok"] is False
|
||||
|
||||
|
||||
def test_재료_줄마다_품목_칸(client) -> None:
|
||||
key = "GF000160"
|
||||
body = {"key": key, "inputs": {k: str(v) for k, v in store.auto(key)["값"].items()}}
|
||||
lines = client.post("/api/m01/calc", json=body).json()["lines"]
|
||||
idx = next(
|
||||
i for i, x in enumerate(lines) if x.get("품목", {}).get("이름", "").startswith("합판(내수)")
|
||||
)
|
||||
first = lines[idx]["품목"]
|
||||
assert first["이름"] == "합판(내수), 서울" and first["값칸"]
|
||||
assert {"키", "이름", "규격", "단위", "값칸"} <= set(first)
|
||||
lines = client.post("/api/m01/calc", json={**body, "고름": {str(idx): "MT000019"}}).json()[
|
||||
"lines"
|
||||
]
|
||||
assert lines[idx]["품목"]["키"] == "MT000019" and "인천" in lines[idx]["품목"]["이름"]
|
||||
|
||||
|
||||
def test_목록_검색은_띄어쓰기를_무시(client) -> None:
|
||||
for q in ("함석", "골 함 석", "골함석"):
|
||||
got = client.get("/api/m01/rows", params={"file": "재료_자재품목.json", "q": q}).json()
|
||||
assert any(r["이름"] == "골 함 석" for r in got["rows"]), q
|
||||
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PLAN 8-3 — 로직 재료 줄 키 → 기본 검색어 바꿔 넣기(`build_재료_검색어.py`)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "resources/master_data/scripts"))
|
||||
|
||||
import build_재료_검색어 as bw # noqa: E402
|
||||
import check_master as cm # noqa: E402
|
||||
import master_auto as mau # noqa: E402
|
||||
import master_formula as mf # noqa: E402
|
||||
import master_pick as mp # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def master() -> mf.Master:
|
||||
return mf.Master(cm.load())
|
||||
|
||||
|
||||
def _material(master, key: str, name: str) -> dict:
|
||||
return next(h for h in master.logic(key)["호표"] if h.get("이름") == name)
|
||||
|
||||
|
||||
def test_결속선은_원문대로_고른_어닐링철선이_그대로_첫째(master) -> None:
|
||||
item = _material(master, "GC000037", "결속선 # 10")
|
||||
assert "대표" not in item["요소"] and item["요소"]["검색어"].startswith("어닐링철선")
|
||||
row = master.logic("GC000037")
|
||||
got = mp.search(master, item["요소"], mau.values(master, row), item["단위"])
|
||||
assert got[0] == "MT000204" # 어닐링철선 #10 서울 — 옛 입력 「결속선」 목록 첫째
|
||||
|
||||
|
||||
def test_재료_입력_키_목록은_걷히고_넘기던_인자도_없음(master) -> None:
|
||||
for name, data in cm.load().items():
|
||||
if not name.startswith("로직_"):
|
||||
continue
|
||||
for row in data["줄"]:
|
||||
assert not bw.material_inputs(row), row["키"]
|
||||
call = next(h["요소"] for h in master.logic("GF000169")["호표"] if "GF000160" in h["요소"])
|
||||
assert "횟수=" in call and "철선=철선" not in call and "합판=합판" not in call
|
||||
|
||||
|
||||
def test_남은_대표는_값_빈_자리표_품목만(master) -> None:
|
||||
"""검색어로 못 가르는 줄(규격 빈 자리표 MT030…)만 대표가 남음 — 목록으로 알린 줄."""
|
||||
reps = {
|
||||
str(h["요소"]["대표"])
|
||||
for data in cm.load().values()
|
||||
for row in data.get("줄", [])
|
||||
if "호표" in row
|
||||
for h in row["호표"]
|
||||
if isinstance(h.get("요소"), dict) and h["요소"].get("대표")
|
||||
}
|
||||
assert reps and all(k.startswith("MT0306") for k in reps), reps
|
||||
|
||||
|
||||
def test_글_갈음은_바뀐_값_자리만(master) -> None:
|
||||
text = (
|
||||
'{ "키": "GX1", "입력": [\n { "이름": "철선", "고르기": ["MT000199"] },\n'
|
||||
' { "이름": "횟수", "고르기": ["1회"] }\n],\n "호표": [\n'
|
||||
' { "종류": "재료", "요소": {"구분": "철근·강재", "대표": "{철선}"}, "수량": "1" },\n'
|
||||
' { "종류": "로직", "요소": "로직(GF000160, 횟수=횟수, 철선=철선)", "수량": "1" }\n] }'
|
||||
)
|
||||
row = json.loads(text)
|
||||
change = {"요소": {0: {"구분": "철근·강재", "검색어": "어닐링철선 #8 서울"}}, "걷음": ["철선"]}
|
||||
got = bw._patch_row(text, row, change)
|
||||
assert json.loads(got) == bw.apply(row, change)
|
||||
assert '"이름": "횟수"' in got and "철선=철선" not in got and "횟수=횟수" in got
|
||||
assert got.count("\n") == text.count("\n") - 1 # 걷은 입력 한 줄만 빠짐
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from decimal import Decimal
|
||||
@@ -93,7 +94,10 @@ def test_펌프카는_하위로직이_비목마다_한_줄씩이고_덧줄이_
|
||||
pump = [one for one in _all_lines(got) if one["이름"] == "콘크리트펌프차"]
|
||||
assert len(pump) == 3 # 노무비 · 재료비 · 경비 몫이 따로
|
||||
extra = next(one for one in _all_lines(got) if one["이름"].startswith("공구손료"))
|
||||
assert extra["글"].endswith(f"= {mt.fmt(Decimal(str(extra['금액'])))}") and "%" in extra["글"]
|
||||
assert (
|
||||
extra["글"].endswith(f"= {mt.fmt_money(Decimal(str(extra['금액'])))}")
|
||||
and "%" in extra["글"]
|
||||
)
|
||||
|
||||
|
||||
def test_몫이_모두_0인_장비_줄도_글에_남는다(client: TestClient) -> None:
|
||||
@@ -136,9 +140,20 @@ def test_이름_꼴_식은_요소_이름과_별칭으로_적힌다(client: TestC
|
||||
assert extra["이름글"] == "노무비 * 5 % = 공구손료및경장비"
|
||||
|
||||
|
||||
def test_멈추는_로직은_까닭_한_줄(client: TestClient) -> None:
|
||||
def test_입력_없어도_이름_식은_보임(client: TestClient) -> None:
|
||||
"""값을 고르기 전(PLAN 2-5) — 멈추지 않고 왼쪽 이름 식은 그대로, 글·금액만 빔."""
|
||||
got = client.post("/api/m01/text", json={"key": STACK, "inputs": {}}).json()
|
||||
assert got["ok"] is False and "입력" in got["reason"]
|
||||
assert got["ok"] is True
|
||||
one = got["groups"][0]["줄"][0]
|
||||
assert one["이름글"] and "석공노임" in one["이름글"]
|
||||
assert one["글"] == "" and one["금액"] is None
|
||||
|
||||
|
||||
def test_고른_값이_틀리면_그대로_막힘(client: TestClient) -> None:
|
||||
"""값을 넣었는데 틀린 경우(고르기 밖)는 지금처럼 멈춤 — 안 고른 것과 다름."""
|
||||
bad = dict(_inputs(STACK), 뒷길이=999)
|
||||
got = client.post("/api/m01/text", json={"key": STACK, "inputs": bad}).json()
|
||||
assert got["ok"] is False and "고르기" in got["reason"]
|
||||
|
||||
|
||||
def test_끝수는_줄이_아니라_소계에_붙는다(client: TestClient) -> None:
|
||||
@@ -153,13 +168,58 @@ def test_끝수는_줄이_아니라_소계에_붙는다(client: TestClient) -> N
|
||||
assert not any("버림" in one["글"] for one in group["줄"])
|
||||
|
||||
|
||||
def test_이름_꼴_찾기_별칭이_표_열쇠_그대로면_밑이름으로_풀지_않는다(client: TestClient) -> None:
|
||||
"""GC001008 덧줄 — 별칭 이름이 QC001882 뿐이라 이름 식도 별칭 목록의 그 이름을 씀."""
|
||||
def test_이름_꼴_찾기_별칭은_표_열쇠_대신_짧은_표_제목(client: TestClient) -> None:
|
||||
"""GC001008 덧줄 — 긴 표 제목(잔디깎기 공구손료·경장비 기계경비 요율)은 첫 + 끝 낱말."""
|
||||
got, _ = _both(client, "GC001008")
|
||||
extra = next(one for one in _all_lines(got) if one["이름"].startswith("공구손료"))
|
||||
assert "QC001882.요율 %" in extra["이름글"]
|
||||
assert "잔디깎기공구손료" not in extra["이름글"]
|
||||
assert any(one["이름"] == "QC001882" for one in got["별칭"])
|
||||
assert "잔디깎기요율.요율 %" in extra["이름글"]
|
||||
assert "QC001882" not in extra["이름글"]
|
||||
assert any(one["이름"] == "잔디깎기요율" for one in got["별칭"])
|
||||
|
||||
|
||||
def test_짧은_이름은_규격_코드를_떼고_길면_줄인다() -> None:
|
||||
short = mt.mc.short
|
||||
assert short("바퀴숫돌 평면용 A24 QWV1호 A205×25×25 KSL 6501") == "바퀴숫돌평면용"
|
||||
assert short("크롤러드릴(공기식)(17㎥/min)") == "크롤러드릴공기식"
|
||||
assert short("콘크리트 피니셔(중앙분리대용) 105.9㎾") == "콘크리트피니셔"
|
||||
assert short("1급 기준점 측량") == "기준점측량" # 숫자로 시작하지 않음
|
||||
assert short("잔디깎기 연간 시공횟수별 시공량 할증율") == "잔디깎기할증율"
|
||||
|
||||
|
||||
def test_이름글_별칭은_찾은_줄_값으로_갈리고_표_키_번호가_없다() -> None:
|
||||
"""번호(가스압접1) · 표 키(QC000568) 대신 표 제목 + 찾은 줄 값 · 되돌리면 원래 부르기."""
|
||||
files = store.cm.load(folder=REAL)
|
||||
master = store.cm.master(REAL)
|
||||
want = {
|
||||
"GC000337": "낙석방지망설치재료_철망.수량",
|
||||
"GC000360": "가스압접_용접공.수량",
|
||||
"GC000350": "터널바닥암반청소_굴착기_02.",
|
||||
"GF000185": "구체콘크리트펌프카타설_레미콘.",
|
||||
}
|
||||
for key, part in want.items():
|
||||
got = mt.lines(files, key, dict(store.cm.시험입력(master.logic(key))))
|
||||
names = [one["이름글"] for one in _all_lines(got)]
|
||||
assert any(part in one for one in names), (key, names)
|
||||
assert not any(re.search(r"\b[A-Z]{2}\d{6}", one) for one in names), (key, names)
|
||||
raw_of = {m["이름"]: m["원문"] for m in got["별칭"]}
|
||||
for one in names: # 이름글 속 별칭은 모두 원문 부르기로 되돌려짐
|
||||
back = mt.mc._unmask(one, raw_of)
|
||||
left = [n for n in raw_of if re.search(rf"(?<![\w.]){re.escape(n)}(?!\w)", back)]
|
||||
assert not left, (key, back)
|
||||
assert any("찾기(" in mt.mc._unmask(one, raw_of) for one in names)
|
||||
|
||||
|
||||
def test_긴_품명은_앞_핵심만_겹치면_꼬리_2() -> None:
|
||||
files = store.cm.load(folder=REAL)
|
||||
master = store.cm.master(REAL)
|
||||
for key in ("GC000325", "GC000360"):
|
||||
got = mt.lines(files, key, dict(store.cm.시험입력(master.logic(key))))
|
||||
names = [one["이름글"] for one in _all_lines(got)]
|
||||
assert not any(len(w) > 20 for one in names for w in re.findall(r"[^\W\d]\w*", one))
|
||||
if key == "GC000325":
|
||||
assert any(one.startswith("콘크리트피니셔노무비단가 * ") for one in names)
|
||||
else:
|
||||
assert "보통인부노임_2 * 0 = 보통인부비_2" in names
|
||||
|
||||
|
||||
# ── PLAN 2-1 · 2-2 — 조각(알약) ─────────────────────────────────────────
|
||||
@@ -205,19 +265,25 @@ def test_조각_자리_표시_든_요소_참조는_이름만_있고_참조는_
|
||||
assert piece["글"] # 이름 자리는 비지 않음(못 풀어도 원문 그대로)
|
||||
|
||||
|
||||
def test_조각_만약은_계산되면_걸린_가지만() -> None:
|
||||
node = mt.mf.parse("만약(1 > 0, 2, 3)")
|
||||
pieces = mt._pieces(node, node, {}, mt.mf.Master({}), {})
|
||||
assert pieces == [{"kind": "값", "글": "2"}]
|
||||
assert not any("깊이" in p for p in pieces)
|
||||
def test_조각_만약은_쉬운_말로_안_걸린_갈래가_흐리다() -> None:
|
||||
"""PLAN 3-3 — 「조건 이면 A · 아니면 B」 · 안 걸린 갈래 조각에 흐림."""
|
||||
env = {"폼타이": "사용"}
|
||||
node = mt.mf.parse("만약(폼타이 == '사용', 2.14 / 10, 0)")
|
||||
pieces = mt._pieces(node, node, env, mt.mf.Master({}), {})
|
||||
assert [p["글"] for p in pieces] == ["폼타이 가 사용 이면", "2.14", "÷", "10", "· 아니면", "0"]
|
||||
assert [bool(p.get("흐림")) for p in pieces] == [False, False, False, False, True, True]
|
||||
off = mt._pieces(node, node, {"폼타이": "아님"}, mt.mf.Master({}), {})
|
||||
assert [bool(p.get("흐림")) for p in off] == [False, True, True, True, False, False]
|
||||
|
||||
|
||||
def test_조각_만약은_계산_안_되면_양쪽_다_깊이가_붙는다() -> None:
|
||||
"""PLAN 2-2 ① — 계산 안 되면(모르는 이름) 양쪽 다 · 깊이를 적어 접을 수 있게."""
|
||||
node = mt.mf.parse("만약(모르는이름 > 0, 2, 3)")
|
||||
def test_조각_만약은_조건_입력이_비면_두_갈래_다_흐림_없음() -> None:
|
||||
"""조건이 안 셈해지면(모르는 이름) 두 갈래 다 · 흐림 없음 · 깊이를 적어 접을 수 있게."""
|
||||
node = mt.mf.parse("만약(폼타이 == '사용', 2, 3)")
|
||||
pieces = mt._pieces(node, node, {}, mt.mf.Master({}), {})
|
||||
vals = [(p["kind"], p["글"], p.get("깊이")) for p in pieces]
|
||||
assert vals == [("값", "2", 1), ("기호", "/", None), ("값", "3", 1)]
|
||||
assert [p["글"] for p in pieces] == ["폼타이 가 사용 이면", "2", "· 아니면", "3"]
|
||||
assert not any("흐림" in p for p in pieces)
|
||||
assert [p.get("깊이") for p in pieces] == [None, 1, 1, 1]
|
||||
assert mt.named(node, node, {}, mt.mf.Master({})) == "만약(폼타이 == '사용', 2, 3)"
|
||||
|
||||
|
||||
def test_조각_로직_부르는_줄_990개_세_비목에_다_걸치고_계산_전엔_금액이_빈다(
|
||||
@@ -240,3 +306,241 @@ def test_조각_로직_부르는_줄_990개_세_비목에_다_걸치고_계산_
|
||||
assert one["금액"] is None and one["글"] == "" and "까닭" not in one
|
||||
assert one["조각"] # 수량 식은 깨진 요소와 무관해 구조로 그대로 남음
|
||||
assert [g["소계"] for g in out["groups"]] == [None]
|
||||
|
||||
|
||||
def test_자리_표시_든_참조는_이름_입력값으로_읽히고_참조는_풀린_키() -> None:
|
||||
"""GF000081 잡품 — 「MO:휘발유:{지역}」 날글 대신 「휘발유 · 전국평균」 · 참조 = 풀린 MO 키."""
|
||||
files = store.cm.load(folder=REAL)
|
||||
master = store.cm.master(REAL)
|
||||
row = master.logic("GF000081")
|
||||
|
||||
def pills(given: dict) -> tuple[list, str]:
|
||||
got = mt.lines(files, "GF000081", given)
|
||||
one = next(o for o in _all_lines(got) if o["이름"] == "잡품")
|
||||
return [p for p in one["조각"] if p["kind"] == "요소"], one["이름글"]
|
||||
|
||||
filled, name_text = pills(dict(store.cm.시험입력(row)))
|
||||
assert [p["글"] for p in filled] == ["휘발유 · 전국평균"]
|
||||
assert filled[0]["참조"]["키"] == "MO000001" and "MO:" not in name_text
|
||||
blank, name_text = pills({})
|
||||
assert [p["글"] for p in blank] == ["휘발유 · 지역"] and "참조" not in blank[0]
|
||||
assert "{" not in name_text and "MO:" not in name_text
|
||||
|
||||
|
||||
def test_자리_표시_참조가_조각_요소조각_이름식에_날글로_남지_않는다() -> None:
|
||||
files = store.cm.load(folder=REAL)
|
||||
master = store.cm.master(REAL)
|
||||
for key in ("GC000138", "GC000211", "GC000267", "GC000559", "GF000081"):
|
||||
for given in (dict(store.cm.시험입력(master.logic(key))), {}):
|
||||
for one in _all_lines(mt.lines(files, key, given)):
|
||||
shown = [one["이름글"], *(p["글"] for p in one["조각"] + one["요소조각"])]
|
||||
assert not any(re.search(r"[A-Z]{2}:|[{}]", t) for t in shown), (key, shown)
|
||||
|
||||
|
||||
def test_표_조각_참조_줄은_엔진이_찾은_줄의_차례다() -> None:
|
||||
"""GC000010 크레인 — 줄에 없는 조건 칸은 맞음이라 흉내 내면 틀림 · GF000160 합판도."""
|
||||
files = store.cm.load(folder=REAL)
|
||||
master = store.cm.master(REAL)
|
||||
for key in ("GC000010", "GF000160"):
|
||||
got = mt.lines(files, key, dict(store.cm.시험입력(master.logic(key))))
|
||||
tables = [p for one in _all_lines(got) for p in one["조각"] if p["kind"] == "표"]
|
||||
assert tables and all("줄" in p["참조"] for p in tables), key
|
||||
for p in tables:
|
||||
rows = master.get(p["참조"]["키"])["줄"]
|
||||
hit = rows[p["참조"]["줄"]]
|
||||
assert hit is mt.mf.find(master.get(p["참조"]["키"]), p["참조"]["행"], p["참조"]["열"])
|
||||
got = mt.lines(files, "GC000010", dict(store.cm.시험입력(master.logic("GC000010"))))
|
||||
pill = next(
|
||||
p
|
||||
for one in _all_lines(got)
|
||||
for p in one["조각"]
|
||||
if p["kind"] == "표" and p["참조"]["키"] == "QC000031" and "크레인" in p["글"]
|
||||
)
|
||||
assert master.get("QC000031")["줄"][pill["참조"]["줄"]]["구분"] == "크레인"
|
||||
|
||||
|
||||
def test_합판거푸집_표_조각_글은_줄마다_다르고_참조에_찾기_조건이_있다(client: TestClient) -> None:
|
||||
"""PLAN 3-3 — 글 = 「찾은 줄 · 칸」 · 참조.행 = 찾기 조건 · 중간 값은 글에 이름."""
|
||||
got, _ = _both(client, FORM)
|
||||
tables = [p for one in _all_lines(got) for p in one["조각"] if p["kind"] == "표"]
|
||||
assert len(tables) == 7 and len({p["글"] for p in tables}) == 7
|
||||
plywood = next(p for p in tables if p["글"] == "합판 · 1회사용")
|
||||
assert plywood["참조"]["행"] == {"구분": "합판"} and plywood["참조"]["열"] == "기준수량_1회사용"
|
||||
rate = next(p for p in _all_lines(got)[0]["조각"] if p["kind"] == "값")
|
||||
assert rate["글"] == "인력율" and rate["값"] == "1.31"
|
||||
tie = _all_lines(got)[-1]["조각"]
|
||||
assert tie[0]["글"] == "폼타이 가 사용 이면" and [bool(p.get("흐림")) for p in tie][1] is True
|
||||
|
||||
|
||||
def test_값_없이_열어도_텍스트가_멈추지_않는다(client: TestClient) -> None:
|
||||
got = client.post("/api/m01/text", json={"key": FORM, "inputs": {}}).json()
|
||||
assert got["ok"] is True
|
||||
tie = _all_lines(got)[-1]
|
||||
assert "만약(폼타이 == '사용'" in tie["이름글"] and not any("흐림" in p for p in tie["조각"])
|
||||
|
||||
|
||||
def test_모든_로직을_값_없이_돌려도_멈추지_않는다() -> None:
|
||||
files = store.cm.load(folder=REAL)
|
||||
master = store.cm.master(REAL)
|
||||
stops = []
|
||||
for rows in master.index.values():
|
||||
for key in rows:
|
||||
if key[:2] in mt.mf.mk.GROUP_OF and mt.mf.mk.GROUP_OF[key[:2]] == "로직":
|
||||
try:
|
||||
mt.lines(files, key, {})
|
||||
except mt._CALC_ERRORS as e:
|
||||
stops.append((key, str(e)))
|
||||
assert not stops, stops[:3]
|
||||
|
||||
|
||||
def test_글_안의_돈은_정수_계산_값은_소수_그대로(client: TestClient) -> None:
|
||||
assert mt.fmt_money(Decimal("79482.678")) == "79,483"
|
||||
assert mt.fmt_money(Decimal("0.4")) == "0"
|
||||
got, _ = _both(client, FORM)
|
||||
first = _all_lines(got)[0]
|
||||
assert first["글"] == "275,790 * 0.22 * 1.31 = 79,483" # 수량·비율은 소수 그대로
|
||||
assert Decimal(str(first["금액"])) == Decimal("79482.678") # 값은 그대로
|
||||
|
||||
|
||||
def test_자동값으로_모든_로직을_돌려도_멈추지_않는다() -> None:
|
||||
"""빈 줄(후보 0 · 값 빈 품목) — 그 줄만 빈 글 · 로직은 계속(PLAN 3-2)."""
|
||||
files = store.cm.load(folder=REAL)
|
||||
master = store.cm.master(REAL)
|
||||
stops = []
|
||||
for rows in master.index.values():
|
||||
for key in rows:
|
||||
if mt.mf.mk.GROUP_OF.get(key[:2]) != "로직":
|
||||
continue
|
||||
given = {n: store._dec(v) for n, v in store.auto(key)["값"].items()}
|
||||
try:
|
||||
mt.lines(files, key, given)
|
||||
except mt._CALC_ERRORS as e:
|
||||
stops.append((key, str(e)))
|
||||
assert not stops, stops[:3]
|
||||
|
||||
|
||||
def test_빈_줄은_빈_글이고_소계는_빈_줄_빼고_더한다(client: TestClient) -> None:
|
||||
key = "GC000027"
|
||||
given = {n: str(v) for n, v in store.auto(key)["값"].items()}
|
||||
got = client.post("/api/m01/text", json={"key": key, "inputs": given}).json()
|
||||
calc = client.post("/api/m01/calc", json={"key": key, "inputs": given}).json()
|
||||
assert got["ok"] is True
|
||||
blank = [one for one in _all_lines(got) if one["금액"] is None]
|
||||
assert blank and all(one["글"] == "" and one["까닭"] for one in blank)
|
||||
assert Decimal(str(got["계"])) == Decimal(str(calc["sums"]["계"]))
|
||||
|
||||
|
||||
def test_곱_뒤_덧셈_묶음은_괄호로_이름_식_값_식_모두() -> None:
|
||||
"""GC000467 촌락지대가 — 노임 * (아홉 항 합) · 첫 항에만 곱하는 것처럼 읽히지 않게."""
|
||||
files = store.cm.load(folder=REAL)
|
||||
row = store.cm.master(REAL).logic("GC000467")
|
||||
given = {n: store._dec(v) for n, v in store.auto("GC000467")["값"].items()}
|
||||
got = mt.lines(files, "GC000467", given)
|
||||
one = _all_lines(got)[0]
|
||||
assert one["글"].startswith("295,138 * (1 + 1 + 0.5") and " = 3,984,363" in one["글"]
|
||||
left = one["이름글"].split(" = ")[0]
|
||||
assert left.startswith("중급기술자건설노임 * (") and left.endswith(".수량)")
|
||||
assert Decimal(str(one["금액"])) == Decimal("295138") * Decimal("13.5")
|
||||
blank = mt.lines(files, "GC000467", {})
|
||||
assert _all_lines(blank)[0]["이름글"].split(" = ")[0].count("(") == 1 # 값 없이도 그대로
|
||||
assert row # 로직이 있음
|
||||
|
||||
|
||||
def test_이름글_번호_별칭은_별칭표로_되돌려진다() -> None:
|
||||
"""이름글 속 별칭(가1~가9)은 별칭표 이름 그대로여야 식으로 되돌려짐(찾은 줄 이름 X)."""
|
||||
files = store.cm.load(folder=REAL)
|
||||
given = {n: store._dec(v) for n, v in store.auto("GC000467")["값"].items()}
|
||||
got = mt.lines(files, "GC000467", given)
|
||||
left = _all_lines(got)[0]["이름글"].split(" = ")[0].split(" * ", 1)[1].strip("()")
|
||||
raw_of = {m["이름"]: m["원문"] for m in got["별칭"]}
|
||||
back = mt.mc._unmask(left, raw_of)
|
||||
assert back.count("찾기(") == 9 and "촌락지대가" not in back
|
||||
|
||||
|
||||
def test_요소조각_로직_부르기는_이름과_인자_글_날식_없음() -> None:
|
||||
"""GC001093 트럭 줄 — 로직 조각(키 GC000268) + 인자 글(기계 이름 · 지역 지금 값)."""
|
||||
files = store.cm.load(folder=REAL)
|
||||
row = store.cm.master(REAL).logic("GC001093")
|
||||
given = dict(store.cm.시험입력(row))
|
||||
for got in (mt.lines(files, "GC001093", given), mt.lines(files, "GC001093", {})):
|
||||
lines = _all_lines(got)
|
||||
assert all("요소조각" in one for one in lines)
|
||||
truck = [one for one in lines if one["이름"].startswith("트럭")]
|
||||
assert truck
|
||||
for one in truck:
|
||||
head, args = one["요소조각"]
|
||||
assert head["kind"] == "로직" and head["참조"]["키"] == "GC000268"
|
||||
assert args["kind"] == "글" and "덤프트럭" in args["글"]
|
||||
assert "GC000268" not in args["글"] and "(" not in args["글"].split("덤프트럭")[0]
|
||||
labor = lines[0]["요소조각"]
|
||||
assert labor[0]["kind"] == "요소" and labor[0]["글"] == "배관공(수도)"
|
||||
|
||||
|
||||
def test_값_식_작은_수는_유효숫자로_보인_수끼리_곱해도_금액과_맞음() -> None:
|
||||
"""GC001064 수량 0.001667 — 넷째 자리로 잘라 456 이 465 로 읽히던 것."""
|
||||
files = store.cm.load(folder=REAL)
|
||||
row = store.cm.master(REAL).logic("GC001064")
|
||||
got = mt.lines(files, "GC001064", dict(store.cm.시험입력(row)))
|
||||
first = _all_lines(got)[0]["글"]
|
||||
assert first == "273,308 * 0.001667 = 456"
|
||||
assert mt.fmt(Decimal("0.00166667")) == "0.001667" and mt.fmt(Decimal("12.34567")) == "12.3457"
|
||||
assert mt.fmt_price(Decimal("3.5")) == "3.5" and mt.fmt_price(Decimal("3500.4")) == "3,500"
|
||||
|
||||
|
||||
def _text_of(key: str, **given) -> list[dict]:
|
||||
auto = {k: v for k, v in (store.auto(key).get("값") or {}).items() if v is not None}
|
||||
return _all_lines(store.text(key, {**auto, **given}))
|
||||
|
||||
|
||||
def test_빈_줄을_부른_덧줄_값_조각은_None_대신_그_줄_이름() -> None:
|
||||
line = next(x for x in _text_of("GF000185") if x["이름"] == "콘크리트다짐")
|
||||
texts = [p["글"] for p in line["조각"]]
|
||||
assert "None" not in texts and "구체콘크리트(철근) 레미콘" in texts
|
||||
|
||||
|
||||
def test_만약이_고른_갈래가_덧셈이면_곱셈_안에서_괄호로_묶임() -> None:
|
||||
line = next(x for x in _text_of("GC000376", 면="BOX 형상 내면") if x["이름"] == "도장공")
|
||||
assert line["글"] == "263,017 * 0.031 * (1 + 60 %) = 13,046"
|
||||
outer = next(x for x in _text_of("GC000376", 면="외면") if x["이름"] == "도장공")
|
||||
assert "(" not in outer["글"]
|
||||
|
||||
|
||||
def test_수량_식_안_로직_조각은_그_로직_이름() -> None:
|
||||
line = next(x for x in _text_of("GC000111") if x["이름"] == "리퍼 기계경비")
|
||||
pill = next(p for p in line["조각"] if p["kind"] == "로직")
|
||||
assert pill["글"] == "굴착기(무한궤도) 시간당 기계경비" and pill["참조"]["키"] == "GC000267"
|
||||
|
||||
|
||||
def test_표에_없는_입력_조합은_그_줄만_비우고_글은_그대로() -> None:
|
||||
"""/calc 와 같은 빈 줄 규칙 — 풀리지 않는 중간값은 /text 도 멈추지 않음."""
|
||||
got = store.text("GC001000", {**store.auto("GC001000")["값"], "시공구분": "기계시공"})
|
||||
lines = _all_lines(got)
|
||||
blank = [x for x in lines if x["금액"] is None]
|
||||
assert blank and all(x["이름글"] and x["조각"] for x in blank)
|
||||
assert len(blank) < len(lines) or got["계"] is not None
|
||||
calc = store.calc("GC001000", {**store.auto("GC001000")["값"], "시공구분": "기계시공"})
|
||||
assert got["계"] == calc["sums"]["계"]
|
||||
|
||||
|
||||
def _mat_piece(lines: list[dict], name: str) -> dict:
|
||||
return next(x for x in lines if x["이름"] == name)["요소조각"][0]
|
||||
|
||||
|
||||
def test_재료_줄_요소조각은_잡힌_품목_이름과_MT_참조() -> None:
|
||||
piece = _mat_piece(_text_of("GF000159"), "결속선 0.9㎜")
|
||||
assert piece["글"].startswith("어닐링철선(결속선)") and piece["글"].endswith("서울")
|
||||
assert piece["참조"] == {"테이블": "MT", "키": "MT000209"}
|
||||
auto = {k: v for k, v in store.auto("GF000159")["값"].items() if v is not None}
|
||||
assert store.text("GF000159", auto)["계"] == store.calc("GF000159", auto)["sums"]["계"]
|
||||
|
||||
|
||||
def test_고름으로_바꾼_재료_줄_요소조각은_고른_품목() -> None:
|
||||
import check_master as cm
|
||||
import master_pick as mp
|
||||
|
||||
auto = {k: v for k, v in store.auto("GF000159")["값"].items() if v is not None}
|
||||
items = cm.master().logic("GF000159")["호표"]
|
||||
at = next(i for i, h in enumerate(items) if h.get("이름") == "결속선 0.9㎜")
|
||||
with mp.choosing({at: "MT000174"}):
|
||||
got = _all_lines(store.text("GF000159", auto))
|
||||
assert _mat_piece(got, "결속선 0.9㎜")["참조"]["키"] == "MT000174"
|
||||
|
||||
@@ -176,11 +176,11 @@ def test_범위규칙_이하_위끝만(whole):
|
||||
|
||||
|
||||
def test_범위규칙_마지막칸_이하_포함(whole):
|
||||
"""13-6-1 직경 「80㎝이상∼100㎝이하」 — 100 은 맞고 101 은 막힘."""
|
||||
"""13-6-1 직경 「80㎝이상∼100㎝이하」 — 100 은 맞고 101 은 표에 없는 줄이라 그 줄만 비고 까닭."""
|
||||
given = {"직경": 100, "지역": "전국평균", "보정작업": "해당 없음"}
|
||||
assert mf.run(whole, "GF000226", given)["줄"][0]["수량"] == Decimal("0.68")
|
||||
with pytest.raises(mf.FormulaError):
|
||||
mf.run(whole, "GF000226", {**given, "직경": 101})
|
||||
line = mf.run(whole, "GF000226", {**given, "직경": 101})["줄"][0]
|
||||
assert line["금액"] is None and line["단가"] is None and "인 줄이 없음" in line["까닭"]
|
||||
|
||||
|
||||
def test_굴착기_부착용_집게(whole):
|
||||
@@ -632,11 +632,11 @@ def test_미공표_준용():
|
||||
got = mf.run(mf.Master(files), "GX999999", {})
|
||||
assert got["줄"][0]["출처"] == "준용: 보통인부"
|
||||
assert got["노무비"] == 2 * Decimal(172068)
|
||||
lines[0]["요소"] = "LB:절단공" # 값도 준용도 없으면 멈춤 — 준용을 골라야 값이 섬
|
||||
lines[0]["요소"] = "LB:절단공" # 값도 준용도 없으면 그 줄이 빔 — 준용을 골라야 값이 섬
|
||||
alias = mf.Master(files).get("LB:절단공")
|
||||
alias["상태"], alias["값"], alias["준용"] = "미확보", None, None
|
||||
with pytest.raises(mf.FormulaError):
|
||||
mf.run(mf.Master(files), "GX999999", {})
|
||||
got = mf.run(mf.Master(files), "GX999999", {})
|
||||
assert got["줄"][0]["금액"] is None and got["줄"][0]["까닭"] and got["노무비"] == 0
|
||||
alias["준용"] = common
|
||||
assert mf.run(mf.Master(files), "GX999999", {})["줄"][0]["출처"] == "준용: 보통인부"
|
||||
lines[0]["요소"] = "LB:1003" # 옛 직종 갱부를 품은 현행 줄(특별인부) — 값 그대로
|
||||
|
||||
Reference in New Issue
Block a user