diff --git a/A00_Common/line_stack.css b/A00_Common/line_stack.css index 577140044..97e9a1ed8 100644 --- a/A00_Common/line_stack.css +++ b/A00_Common/line_stack.css @@ -38,7 +38,21 @@ display: none; } -/* 산출근거(요구 683) — 왼쪽을 내려도 화면에 고정 · 길면 자기 스크롤(높이 = 스크롤 영역 · fit()) */ +/* 합계 상자(요구 687-3) — 스크롤해도 화면 아래 고정 */ +.lstk__foot { + position: sticky; + inset-block-end: 0; + z-index: 2; + padding-block: var(--spacing-8); + background: var(--color-surface); + border-block-start: 1px solid var(--color-border); +} + +.lstk__foot:empty { + display: none; +} + +/* 산출근거(요구 683) — 왼쪽을 내려도 화면에 고정 · 길면 자기 스크롤(높이 = 스크롤 영역 − 합계 상자 · fit()) */ .lstk[data-edit="1"] .lstk__right { position: sticky; inset-block-start: 0; @@ -81,7 +95,9 @@ margin-block-start: calc(-1 * var(--spacing-8)); } +/* 그룹 머리 한 줄(요구 687-6 · 688) — 이름 칸이 늘고 줄어 [+] · X 는 늘 오른쪽 끝(접어도 그대로) */ .lstk-ihead { + flex-wrap: nowrap; align-items: center; padding: var(--spacing-4) var(--spacing-8); border-inline-start: 4px solid var(--color-accent); @@ -93,7 +109,18 @@ border-inline-start-color: var(--color-border-strong); } +.lstk-ihead > .lstk-f-l { + flex: 1 1 auto; + min-width: 4em; +} + +.lstk-count { + flex: none; + white-space: nowrap; +} + .lstk-no { + flex: none; min-width: 1.5em; font-weight: var(--font-weight-bold); color: var(--color-text); @@ -109,14 +136,12 @@ } .lstk-ihead, -.lstk-row, .lstk-fhead, .lstk-frow, -.lstk-find, -.lstk-ltools { +.lstk-find { display: flex; flex-wrap: wrap; - align-items: flex-end; + align-items: center; gap: var(--spacing-4) var(--spacing-8); min-width: 0; } @@ -125,28 +150,74 @@ align-items: center; } +/* 줄 카드 — 금액 · 글 = 한 줄(요구 687-7 · [손잡이][종류][이름][수량][단위][비목][X] · 좁으면 접힘) · 공식 = 머리 줄 + 식 줄 */ .lstk-line { - position: relative; display: flex; - flex-direction: column; - gap: var(--spacing-4); - padding: var(--spacing-8); - padding-inline: var(--spacing-24) var(--spacing-32); + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-4) var(--spacing-8); + padding: var(--spacing-4) var(--spacing-8); border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface-raised); } -/* 글 · 근거 칸 = 한 줄 높이(세로 묶음에서 늘지 않게) */ -.lstk-line > .lstk-f-l { +.lstk-line.k-공식 { + flex-direction: column; + flex-wrap: nowrap; + align-items: stretch; + position: relative; + padding-inline-start: var(--spacing-24); +} + +.lstk-line > .lstk-x, +.lstk-fhead > .lstk-x { + margin-inline-start: auto; +} + +.lstk-line > .lstk-f-l, +.lstk-line > .lstk-qty { + flex: 1 1 8em; + min-width: 6em; +} + +.lstk-line > .lstk-f-m { + flex: 1 1 7em; + min-width: 6em; + width: auto; +} + +.lstk-line .lstk-kind { + flex: none; + min-width: 2.5em; +} + +.lstk-wide { + flex-basis: 100%; +} + +.lstk-qty.ui-btn { + justify-content: flex-start; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.lstk-upick { flex: none; } -.lstk-ltools { - position: absolute; - inset-block-start: var(--spacing-4); - inset-inline-end: var(--spacing-4); - gap: 0; +/* 보이는 글을 눌러 고침(요구 687-14) */ +.lstk-tap { + cursor: text; + border-radius: var(--radius-sm); +} + +.lstk-tap:hover, +.lstk-tap:focus-visible { + background: var(--color-surface-sunken); + text-decoration: underline dotted; } /* 끌기(804-5) — 손잡이 · 끄는 것 반투명 · 놓을 자리 = 위 · 아래 선 · 빈 항목 = 점선 */ @@ -155,9 +226,13 @@ } .lstk-line > .lstk-grip { + flex: none; +} + +.lstk-line.k-공식 > .lstk-grip { position: absolute; - inset-block-start: var(--spacing-12); - inset-inline-start: var(--spacing-8); + inset-block-start: var(--spacing-8); + inset-inline-start: var(--spacing-4); } .lstk-edit[data-dragging] { @@ -185,6 +260,8 @@ } .lstk-res { + flex: 0 1 auto; + min-width: 0; max-width: 100%; } @@ -264,8 +341,14 @@ color: var(--color-text-secondary); } +/* 펼친 문서 제목 = 보통 글(링크 꼴 없음 · 요구 687-11) */ .lstk-out tr.subhead .lstk-h { - color: var(--color-accent); + font-weight: var(--font-weight-medium); + color: var(--color-text); +} + +.lstk-ftitle { + cursor: pointer; } .lstk-gap { diff --git a/A00_Common/line_stack_edit.ts b/A00_Common/line_stack_edit.ts index 4e031a658..62f736436 100644 --- a/A00_Common/line_stack_edit.ts +++ b/A00_Common/line_stack_edit.ts @@ -11,11 +11,13 @@ * ========================================================================== */ import { createButton, createIconButton } from "@ui/ui_template_button"; -import { createCheckbox } from "@ui/ui_template_checkbox"; +import { createRowMenu } from "@ui/ui_template_context_menu"; +import { eqText } from "@ui/ui_template_equation"; import { el, showToast } from "@ui/ui_template_elements"; import { createInputField, createSelectField } from "@ui/ui_template_input"; import { varId, type FormulaDoc, type MoneyLine } from "../common_util/common_util_formula_doc"; -import type { DocAnswer } from "../common_util/common_util_formula_doc_calc"; +import { isNum, type DocAnswer } from "../common_util/common_util_formula_doc_calc"; +import { lineMap, offLine, type Values } from "../common_util/common_util_formula_doc_view"; import type { DocTable } from "../common_util/common_util_formula_doc_table"; import { dropSyms, @@ -36,8 +38,11 @@ import { addPicked, aliasText, repick, + shownUnit, tableAlias, } from "../common_util/common_util_line_stack_pick"; +import { createUnitPick, NO_UNIT } from "../M02_MasterTemplete/DataFlow/M02_DF_UnitPick"; +import { reach } from "./formula_doc_render"; import { attachDrag } from "./line_stack_drag"; import { openStackPick, type StackTake } from "./line_stack_pick"; import { buildBlock } from "./line_stack_rows"; @@ -67,12 +72,14 @@ const field = ( size: "s" | "m" | "l" = "m", ): HTMLElement => { const f = createInputField({ - label, + placeholder: label, value: value === undefined || value === null ? "" : String(value), compact: true, size: 24, }); f.root.classList.add(`lstk-f-${size}`); + f.input.setAttribute("aria-label", label); + f.input.title = label; f.input.addEventListener("change", () => set(f.input.value)); return f.root; }; @@ -87,7 +94,7 @@ const KIND_KEY: Record = { 율: "K_율", }; -/** 금액 줄 몸 */ +/** 금액 줄 몸 — 한 줄(804-7 · 요구 687-7): [종류][이름 단추 | 이름 칸 · 단가 식][수량][단위][비목] · 좁으면 접힘 */ function moneyBody(h: EditHooks, i: number): HTMLElement[] { const d = h.doc; const line = (d.금액 ?? [])[i]; @@ -96,64 +103,45 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] { const v = own ? d.변수?.find((x) => varId(x) === own) : undefined; const put = (fn: (l: MoneyLine & { 규격?: string }) => void): void => h.change((doc) => fn(doc.금액![i])); - const head = el("div", { - className: "lstk-row", - children: [ - field(lt("Name"), line.이름, (t) => put((l) => (l.이름 = t)), "l"), - field(lt("Unit"), line.단위, (t) => put((l) => (l.단위 = t)), "s"), - ...(line.비목 === "묶음" - ? [] - : [ - createSelectField({ - label: lt("Cost"), - options: ["노무비", "재료비", "경비"].map((x) => ({ value: x, text: x })), - value: line.비목 ?? "노무비", - compact: true, - size: 24, - onChange: (x) => put((l) => (l.비목 = x)), - }).root, - ]), - ], - }); // 표 값 기호 = 화면 이름(요구 683) — 칸 글은 화면 이름 ↔ 기호로 오감 const alias = tableAlias(d); const shown = (t: unknown): string => aliasText(String(t ?? ""), alias); const real = (t: string): string => aliasText(t.trim(), alias, true); - const qty = field( - lt("Qty"), - shown(line.수량), - (t) => put((l) => (t.trim() ? (l.수량 = real(t)) : delete l.수량)), - "l", - ); - const price: HTMLElement[] = []; + const head: HTMLElement[] = []; if (own && v) { + // 이름 단추(요구 687-9) — 누르면 찾아 넣기 창이 그 이름을 찾은 채 열림 · 고르면 바꿈 const link = v.연결 ?? {}; const mat = link.재료 as Record | undefined; - const spec = [line.이름, (line as { 규격?: string }).규격].filter(Boolean).join(" "); - const label = link.키 - ? kind === "기계" && spec - ? spec // 기계 = 기종 이름 · 규격(679 — 코드 번호 아님) - : link.갈래 === "단가산출" - ? (h.subDoc(link.키)?.이름 ?? h.name(link.키)) - : h.name(link.키) - : mat - ? String(mat.검색어 ?? mat.이름 ?? mat.상세구분 ?? "") - : ""; const size = (line as { 규격?: string }).규격; + const name = String(line.이름 ?? "") || String(mat?.검색어 ?? mat?.이름 ?? ""); + const res = link.키 + ? link.갈래 === "단가산출" + ? (h.subDoc(link.키)?.이름 ?? h.name(link.키)) + : h.name(link.키) + : ""; const pick = createButton({ - label: (size && !label.includes(size) ? `${label} ${size}` : label) || "—", + label: (size && !name.includes(size) ? `${name} ${size}` : name) || "—", variant: "ghost", tone: "link", size: 24, iconStart: "search", - onClick: () => openStackPick(kind, (p) => h.change((doc) => repick(doc, i, own, kind, p))), + onClick: () => + openStackPick( + kind, + (p) => h.change((doc) => repick(doc, i, own, kind, p)), + undefined, + String(mat?.검색어 ?? line.이름 ?? ""), + ), }); pick.classList.add("lstk-res"); - pick.title = lt("Repick"); + pick.title = `${lt("Repick")}${res ? ` — ${res}` : ""}`; pick.dataset.repick = String(i); - price.push(pick); + head.push(pick); } else - price.push(field(lt("Price"), shown(line.단가), (t) => put((l) => (l.단가 = real(t))), "l")); + head.push( + field(lt("Name"), line.이름, (t) => put((l) => (l.이름 = t)), "m"), + field(lt("Price"), shown(line.단가), (t) => put((l) => (l.단가 = real(t))), "l"), + ); const why = (h.answer.줄?.[i] as { 까닭?: string } | undefined)?.까닭; // 율 밑 번지(복사해 온 조각) — 칸 아래 읽는 이름(804-5) const places = /번지\./.test(`${line.단가} ${line.수량 ?? ""}`) ? placeNames(d, h.items) : null; @@ -161,25 +149,97 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] { shown(t).replace(/번지\.([^.\s()]+)\./g, (all, p: string) => places?.[p] ? `${places[p]} ` : all, ); + const unit = el("span", { className: "lstk-upick" }); + const drawUnit = (): void => + unit.replaceChildren( + createUnitPick({ + value: String(line.단위 ?? "") || NO_UNIT, + size: 24, + free: true, + ariaLabel: lt("Unit"), + extra: [...new Set((d.금액 ?? []).map((m) => String(m.단위 ?? "")).filter(Boolean))], + onPick: (u) => put((l) => (u === NO_UNIT ? delete l.단위 : (l.단위 = u))), + }), + ); + drawUnit(); return [ el("span", { className: "lstk-kind", text: lt(KIND_KEY[kind]) }), - head, - el("div", { - className: "lstk-row", - children: [qty, el("span", { className: "lstk-unit", text: "×" }), ...price], - }), + ...head, + // 율 · 식 줄(자원 없음)에 수량이 없으면 칸 안 보임(한 줄 유지) + ...(own || String(line.수량 ?? "") ? [qtyBox(h, i, shown, real)] : []), + unit, + ...(line.비목 === "묶음" + ? [] + : [ + createSelectField({ + options: ["노무비", "재료비", "경비"].map((x) => ({ value: x, text: x })), + value: line.비목 ?? "노무비", + compact: true, + size: 24, + ariaLabel: lt("Cost"), + onChange: (x) => put((l) => (l.비목 = x)), + }).root, + ]), ...(places ? [ el("div", { - className: "lstk-hint", + className: "lstk-hint lstk-wide", text: `= ${[line.수량, line.단가].filter(Boolean).map(read).join(" × ")}`, }), ] : []), - ...(why ? [el("div", { className: "lstk-miss", text: why })] : []), + ...(why ? [el("div", { className: "lstk-miss lstk-wide", text: why })] : []), ]; } +/** 수량 — 기호 하나면 값 + 출처 꼬리표(「0.39 ← 표 · 잡석 수량」 · 요구 687-12 · 기호는 마우스 글) · 누르면 그 자리에서 식 고침 */ +function qtyBox( + h: EditHooks, + i: number, + shown: (t: unknown) => string, + real: (t: string) => string, +): HTMLElement { + const line = (h.doc.금액 ?? [])[i]; + const text = String(line.수량 ?? ""); + const v = h.doc.변수?.find((x) => varId(x) === text); + const at = `qty:${i}`; + const edit = (): HTMLElement => { + const f = field(lt("Qty"), shown(text), (t) => + h.change((doc) => { + h.open.delete(at); + const l = doc.금액![i]; + if (t.trim()) l.수량 = real(t); + else delete l.수량; + }), + ); + f.classList.add("lstk-qty"); + f.dataset.qty = String(i); + return f; + }; + if (!v || h.open.has(at)) return edit(); + const val = (h.answer.값 ?? {})[text]; + const link = v.연결 ?? {}; + const kind = link.갈래 ?? "식"; + const from = + v.이름 || (link.키 || link.표 ? h.name(String(link.키 ?? link.표)) : "") || shown(text); + const b = createButton({ + label: `${isNum(val) ? eqText(val) : "—"}${v.단위 && shownUnit(v.단위) ? ` ${shownUnit(v.단위)}` : ""} ← ${kind} · ${from}`, + variant: "ghost", + size: 24, + onClick: () => { + h.open.add(at); + h.change(() => undefined); + requestAnimationFrame(() => + document.querySelector(`[data-qty="${i}"] input`)?.focus(), + ); + }, + }); + b.classList.add("lstk-qty"); + b.title = `${lt("Qty")} = ${shown(text)}`; + b.dataset.qty = String(i); + return b; +} + /** [+ 줄] 고르개 값 → 새 줄 */ function addLine(h: EditHooks, at: number, kind: string): void { if (["인력", "재료", "기계", "단가산출"].includes(kind)) @@ -202,10 +262,6 @@ function addLine(h: EditHooks, at: number, kind: string): void { (d.변수 ??= []).push(newVar(id, { 갈래: "숫자", 값: 0 })); it.줄.push({ 꼴: "공식", 기호: [id] }); h.open.add(`${at}.${it.줄.length - 1}`); - } else if (kind === "틀") { - const id = freshId(d, "Q", "틀"); - (d.변수 ??= []).push(newVar(id, { 갈래: "단가산출", 키: "", 입력: {} })); - it.줄.push({ 꼴: "공식", 기호: [id] }); } else if (kind === "글") it.줄.push({ 꼴: "글", 글: "" }); }); } @@ -221,28 +277,23 @@ function dropLine(d: StackDoc, items: StackItem[], i: number, j: number): void { } export function buildEdit(h: EditHooks): HTMLElement { + // [+] 줄 더함 메뉴(요구 687-13 · 18) — 인력 · 재료 · 기계 · 단가산출 · 율 · 식 · 근거 글 const add = (at: number): HTMLElement => { - const pick = createSelectField({ - options: [ - { value: "", text: `${lt("Add_Line")} ▾` }, - ...(["인력", "재료", "기계", "단가산출", "율"] as MoneyKind[]).map((k) => ({ - value: k, - text: lt(k === "단가산출" && h.take ? "K_복사" : KIND_KEY[k]), - })), - { value: "공식", text: lt("K_새공식") }, - { value: "틀", text: lt("K_틀") }, - { value: "글", text: lt("K_글") }, - ], - value: "", - compact: true, - size: 24, - ariaLabel: lt("Add_Line"), - onChange: (x) => { - pick.select.value = ""; // 창을 닫아도 [+ 줄 ▾] 로 돌아옴 - if (x) addLine(h, at, x); - }, - }); - return pick.root; + const kinds: [string, LsKey][] = [ + ["인력", "K_인력"], + ["재료", "K_재료"], + ["기계", "K_기계"], + ["단가산출", h.take ? "K_복사" : "K_단가산출"], + ["율", "Add_율"], + ["공식", "Add_식"], + ["글", "K_글"], + ]; + const menu = createRowMenu( + kinds.map(([k, label]) => ({ label: lt(label), onClick: () => addLine(h, at, k) })), + { icon: "plus", title: lt("Add_Line"), size: 24 }, + ); + menu.classList.add("lstk-add"); + return menu; }; // X 지움 — 확인 없이 · 알림 누르면 되돌림(그 뒤 다른 고침이 없을 때만) const drop = (what: string, fn: (d: StackDoc, items: StackItem[]) => void): void => { @@ -278,7 +329,7 @@ export function buildEdit(h: EditHooks): HTMLElement { h.items.forEach((_, k) => h.open.delete(`fold:${k}`)); flags.forEach((f, k) => f && h.open.add(`fold:${k}`)); }; - // 항목 번호 — 산출근거와 같은 셈(제목 있는 항목 1. 2. · 가나다 = 가. 나.) + // 항목 번호 — 산출근거와 같은 셈(제목 있는 항목 1. 2. · 옛 가나다 항목 = 가. 나. · 체크 걷음 요구 687-5) let n = 0; let sub = 0; const nos = h.items.map((it) => { @@ -287,9 +338,21 @@ export function buildEdit(h: EditHooks): HTMLElement { if (!it.가나다) sub = 0; return no; }); + // 만약(IF) 안 탄 갈래(요구 687-10) — 갈래를 다 열면 닿고 지금 값으로는 안 닿는 기호 · 꺼진 금액 줄 = 숨김(산출근거와 같음) + const vals = (h.answer.값 ?? {}) as Values; + const taken = new Set(reach(h.doc, vals)); + const every = new Set(reach(h.doc, {}, true)); + const steps = lineMap(h.doc); + const off = (line: StackItem["줄"][number]): boolean => + line.꼴 === "금액" + ? offLine((h.doc.금액 ?? [])[line.i] ?? {}, vals, steps) + : line.꼴 === "공식" && + line.기호.length > 0 && + line.기호.every((id) => every.has(id) && !taken.has(id)); const items = h.items.map((it, i) => { const folded = h.open.has(`fold:${i}`); const itemGrip = grip(); + // 그룹 머리 한 줄(요구 687-6 · 688) — 손잡이 · 접기 · 번호 · 이름 · [+] · X · 접어도 자리 그대로 const head = el("div", { className: "lstk-ihead", children: [ @@ -306,15 +369,10 @@ export function buildEdit(h: EditHooks): HTMLElement { }), el("b", { className: "lstk-no", text: nos[i] }), field(lt("Title"), it.제목, (t) => h.change((_, items) => (items[i].제목 = t.trim())), "l"), - createCheckbox(lt("Gana"), { - checked: !!it.가나다, - size: 24, - onChange: (c) => - h.change((_, items) => (c ? (items[i].가나다 = true) : delete items[i].가나다)), - }).root, - ...(folded - ? [el("span", { className: "lstk-hint", text: `${lt("K_줄")} ${it.줄.length}` })] - : []), + el("span", { + className: "lstk-hint lstk-count", + text: folded ? `${lt("K_줄")} ${it.줄.length}` : "", + }), add(i), x(lt("Del_Item"), () => drop(it.제목.trim() || lt("Item"), (d, items) => { @@ -325,34 +383,35 @@ export function buildEdit(h: EditHooks): HTMLElement { ), ], }); - const lines = it.줄.map((line, j) => { + const lines = it.줄.flatMap((line, j) => { + if (off(line)) return []; const lineGrip = grip(); - const tools = el("div", { - className: "lstk-ltools", - children: [ - x(lt("Del"), () => - drop(line.꼴 === "금액" ? String(h.doc.금액?.[line.i]?.이름 ?? "") : "", (d, items) => - dropLine(d, items, i, j), - ), - ), - ], - }); + const del = x(lt("Del"), () => + drop(line.꼴 === "금액" ? String(h.doc.금액?.[line.i]?.이름 ?? "") : "", (d, items) => + dropLine(d, items, i, j), + ), + ); const body = line.꼴 === "금액" ? moneyBody(h, line.i) : line.꼴 === "글" ? [ + el("span", { className: "lstk-kind", text: lt("K_글") }), field( - lt("K_글"), + lt("Text_Ph"), line.글, (t) => h.change((_, items) => ((items[i].줄[j] as { 글: string }).글 = t)), "l", ), ] - : buildBlock(h, i, j, line); + : buildBlock(h, i, j, line, (id) => every.has(id) && !taken.has(id)); + // 금액 · 글 = 한 줄(X 끝) · 공식 묶음 = 머리 줄 끝에 X const card = el("div", { className: `lstk-line k-${line.꼴}`, - children: [lineGrip, tools, ...body], + children: + line.꼴 === "공식" + ? [lineGrip, ...body.slice(0, 1).map((hd) => (hd.append(del), hd)), ...body.slice(1)] + : [lineGrip, ...body, del], }); card.dataset.loc = `${i}.${j}`; card.addEventListener("pointerdown", () => h.pair(`${i}.${j}`)); @@ -365,7 +424,7 @@ export function buildEdit(h: EditHooks): HTMLElement { h.change((_, items) => moveLine(items, i, j, k, p)); }, }); - return card; + return [card]; }); const box = el("div", { className: `lstk-item${it.가나다 ? " is-sub" : ""}${folded ? " is-folded" : ""}`, diff --git a/A00_Common/line_stack_out.ts b/A00_Common/line_stack_out.ts index 5f2dffa42..06d241799 100644 --- a/A00_Common/line_stack_out.ts +++ b/A00_Common/line_stack_out.ts @@ -2,12 +2,13 @@ * line_stack_out.ts * 줄 쌓기 산출근거 출력(PLAN 804-2 · 요구 670-1 · 677) — 경쟁사 「단가산출」 시트 꼴 표(산출근거 · 합계 · 노무비 · 재료비 · 경비) * 공식 줄 = 「기호 = 기호식 = 값식 = 결과 단위」(값만인 줄은 넷까지 한 줄) · 금액 줄 = 「이름 : 단가 × 수량 단위 = 금액」 - * 기계 · 단가산출 줄 = 비목마다 「노무비 : 59,020 × … = 금액」 — 단가 수 누름(▸) = 그 문서(기계 시간당 경비 등)를 - * 같은 계산기로 셈해 같은 꼴로 펼침(손료 식 · 연료 · 운전사 · 노 · 재 · 경 · 계) · 접으면 한 줄 · 출력엔 펼친 것만 + * 기계 · 단가산출 줄 = 비목마다 「노무비 : 59,020 × … = 금액」 — 제목 줄(「굴착기 0.6㎥」) 누름 = 그 문서(기계 시간당 경비 등)를 + * 같은 계산기로 셈해 같은 꼴로 펼침(손료 식 · 연료 · 운전사 · 노 · 재 · 경 · 계) · 비목 수는 글(링크 꼴 없음 · 요구 687-11) · + * 공식 줄 단가산출 결과 = 수 누름(▸) · 접으면 한 줄 · 출력엔 펼친 것만 * 값 = 계산기 셈 답 그대로(`calcDoc` · 줄 `answer.줄` · 합 `노무비` …) — 여기서 다시 셈 안 함 · 조판 = KaTeX(`line_stack_tex.ts`) * ========================================================================== */ -import { createButton } from "@ui/ui_template_button"; +import { createButton, createIconButton } from "@ui/ui_template_button"; import { eqText } from "@ui/ui_template_equation"; import { el } from "@ui/ui_template_elements"; import { tableRow, tableShell, type Column } from "@ui/ui_template_table"; @@ -127,6 +128,21 @@ function rows( b.dataset.expand = path; return b; }; + /** 기계 · 단가산출 제목 줄 — 접기 단추 + 제목(누르면 펼침 · 요구 687-11) */ + const fold = (id: string, title: string): HTMLElement[] => { + const path = `${at}/${id}`; + const on = h.open.has(path); + const b = createIconButton({ + icon: on ? "chevronDown" : "chevronRight", + title: lt(on ? "Fold_Sub" : "Unfold_Sub"), + size: 20, + onClick: (e) => (e.stopPropagation(), h.toggle(path)), + }); + b.dataset.expand = path; + const t = el("span", { className: "lstk-ftitle", text: title }); + t.addEventListener("click", (e) => (e.stopPropagation(), h.toggle(path))); + return [b, t]; + }; const expand = (id: string): void => { const path = `${at}/${id}`; const sub = subOf(id); @@ -251,18 +267,14 @@ function rows( const s = priceSym(line); const bundle = (r.단가 ?? {}) as Values; const cols: Cols = {}; - push({ h: [name || (s ? (subOf(s)?.이름 ?? s) : "")], cls: "lname" }); + const title = name || (s ? (subOf(s)?.이름 ?? s) : ""); + push({ h: s && subOf(s) ? fold(s, title) : [title], cls: "lname" }); for (const c of COSTS) { const part = r.비목?.[c]; if (!isNum(part) || (!part && !bundle[c])) continue; cols[c] = part; push({ - h: [ - `${COST_SHOW[c]} : `, - s ? opener(s, eqText(bundle[c])) : eqText(bundle[c]), - ...qtyNode(c), - ` = ${one(part)}`, - ], + h: [`${COST_SHOW[c]} : `, eqText(bundle[c]), ...qtyNode(c), ` = ${one(part)}`], }); } if (s) expand(s); diff --git a/A00_Common/line_stack_pick.ts b/A00_Common/line_stack_pick.ts index 419001e02..b20e75bf3 100644 --- a/A00_Common/line_stack_pick.ts +++ b/A00_Common/line_stack_pick.ts @@ -39,8 +39,14 @@ export interface StackTake { take: (src: string) => void; } -/** 찾아 넣기 — 고르면 `done(고른 것)` · 닫으면 아무것도 안 함 · `m02` 있고 다른 단가산출 = 복사해 펼침 */ -export function openStackPick(kind: MoneyKind, done: (p: Picked) => void, m02?: StackTake): void { +/** 찾아 넣기 — 고르면 `done(고른 것)` · 닫으면 아무것도 안 함 · `m02` 있고 다른 단가산출 = 복사해 펼침 · + * `query` = 찾기 칸에 넣고 연 결과(이름 단추 · 요구 687-9) */ +export function openStackPick( + kind: MoneyKind, + done: (p: Picked) => void, + m02?: StackTake, + query = "", +): void { const want = KINDS[kind]; if (!want) return; const spec = (e: { 규격: string }) => (e.규격 ? { 규격: e.규격 } : {}); @@ -50,6 +56,7 @@ export function openStackPick(kind: MoneyKind, done: (p: Picked) => void, m02?: owner: copy?.owner ?? null, title: `${lt("Find_Title")} — ${lt(copy ? "K_복사" : (`K_${kind}` as never))}`, kinds: copy ? ["logic", "sk", "pre"] : [want], // M01 = M01 전체만(680-1) + query: query.trim() || undefined, ref: copy ? (r) => r.키 === copy.me && r.층 === copy.layer && (!copy.owner || r.주인 === copy.owner) diff --git a/A00_Common/line_stack_rows.ts b/A00_Common/line_stack_rows.ts index 37ca4a443..2a533f817 100644 --- a/A00_Common/line_stack_rows.ts +++ b/A00_Common/line_stack_rows.ts @@ -1,14 +1,16 @@ /* ============================================================================= * line_stack_rows.ts * 줄 쌓기 공식 묶음(PLAN 804-2 · 요구 675) — 「이름 = 값」 · 「이름 = 식」 줄 묶음 · 틀 이름표 · [식으로 풀기] · [틀로 저장] - * 접힘 = 값 줄은 값 칸(연결 값을 고쳐 적으면 숫자 = 고정값 · 요구 664) · 식 줄은 「이름 = 식 = 값」 글 · 단가산출 결과 기호 = [식으로 풀기] - * 풀림 = 줄마다 이름 · 식 · 단위 칸 + 지움 · [+ 줄] — 이름 바꿈 = 문서 전부(`rename`) · 식 = 식 줄 · 수 = 숫자 연결 + * 보임 = 값 줄은 값 칸(연결 값을 고쳐 적으면 숫자 = 고정값 · 요구 664) · 식 줄은 「이름 = 식 = 값」 글 · 단가산출 결과 기호 = [식으로 풀기] + * 고침(804-7 · 요구 687-14) = [식 고치기] 걷음 — 이름 · 식 글을 누르면 그 줄만 그 자리에서 이름 · 식 · 단위 칸 + 지움 · + * 바꾸면 닫힘 · 머리 [+] = 식 줄 더함 — 이름 바꿈 = 문서 전부(`rename`) · 식 = 식 줄 · 수 = 숫자 연결 + * 만약 안 탄 갈래 기호(`hide` · 요구 687-10) = 안 보임(식 글은 그대로) * 틀 = 단가산출 결과 문서(굴착기 Q 등)를 이 문서 식으로 옮김(`unfold`) — 꼴이 바뀌면 「내 틀 (틀 이름 에서 변경)」 · * 빈 틀 = [찾아 넣기](M01 단가산출 결과 — 작업량 등) · [틀로 저장] · 내 틀 목록 걷음(요구 685) · 표 값만 = 머리 「표 값」 * 표 값(804-3) = [표 · 칸] 단추 → 표 고르기 창(`line_stack_pick.ts`) · 값 칸 누름 = 그 줄 · 칸(`pickCell`) * ========================================================================== */ -import { createButton } from "@ui/ui_template_button"; +import { createButton, createIconButton } from "@ui/ui_template_button"; import { eqText } from "@ui/ui_template_equation"; import { el, showToast } from "@ui/ui_template_elements"; import { createInputField } from "@ui/ui_template_input"; @@ -66,10 +68,21 @@ const input = ( return f.root; }; -export function buildBlock(h: EditHooks, i: number, j: number, line: Block): HTMLElement[] { +export function buildBlock( + h: EditHooks, + i: number, + j: number, + line: Block, + hide: (id: string) => boolean = () => false, +): HTMLElement[] { const d = h.doc; const at = `${i}.${j}`; - const open = h.open.has(at); + const editing = (id: string): boolean => h.open.has(`${at}:${id}`); + /** 그 줄 고침 켬 · 끔(화면 상태) */ + const flip = (id: string, on: boolean): void => { + if (on) h.open.add(`${at}:${id}`); + else h.open.delete(`${at}:${id}`); + }; const vals = (h.answer.값 ?? {}) as Record; const vars = new Map((d.변수 ?? []).map((v) => [varId(v), v])); const lines = new Map((d.식 ?? []).map((l) => [String(l.기호 ?? ""), String(l.식 ?? "")])); @@ -104,15 +117,17 @@ export function buildBlock(h: EditHooks, i: number, j: number, line: Block): HTM className: "lstk-fhead", children: [ el("span", { className: "lstk-kind", text: label }), - createButton({ - label: open ? lt("Fold") : lt("Unfold"), - variant: "ghost", + createIconButton({ + icon: "plus", + title: lt("Add_Row"), size: 24, - onClick: () => { - if (open) h.open.delete(at); - else h.open.add(at); - h.change(() => undefined); - }, + onClick: () => + h.change((doc, items) => { + const id = freshId(doc, "x"); + (doc.변수 ??= []).push(newVar(id, { 갈래: "숫자", 값: 0 })); + blockOf(items).기호.push(id); + flip(id, true); + }), }), ], }); @@ -177,118 +192,136 @@ export function buildBlock(h: EditHooks, i: number, j: number, line: Block): HTM items[i].줄.splice(j + 1, 0, { 꼴: "공식", 기호: moved, 틀: key, 틀꼴: shapeOf(doc, moved) }); if (!blk.기호.length) items[i].줄.splice(j, 1); }); - const rows = line.기호.map((id) => { - const v = vars.get(id); - const link = v?.연결; - const expr = lines.get(id); - const sub = link?.갈래 === "단가산출" && link.키 ? h.subDoc(link.키) : null; - const tag = - link?.갈래 === "표" && !expr - ? tableBtn(id, link) - : link && LINK_TAG[link.갈래 ?? ""] && !expr - ? el("span", { - className: "lstk-tag", - text: `${LINK_TAG[link.갈래!]}${link.키 && !link.키.includes("{") ? ` · ${sub?.이름 ?? h.name(link.키) ?? ""}` : ""}`, - }) - : null; - const tools: HTMLElement[] = []; - if (sub?.결과 && link?.키) - tools.push( - createButton({ - label: lt("Unfold"), - variant: "ghost", - size: 24, - onClick: () => unfoldRow(id, sub, link.키!), - }), - ); - if (link?.갈래 === "단가산출" && !link.키) - tools.push( - createButton({ - label: lt("Find_Title"), - variant: "ghost", - size: 24, - onClick: () => - openFindPicker({ - layer: "system", - owner: null, - title: lt("K_틀"), - kinds: ["logic"], - results: true, - logic: (x) => - h.change((doc) => { - const y = doc.변수?.find((z) => varId(z) === id); - if (y) y.연결 = { 갈래: "단가산출", 키: x.키, 입력: {} }; - }), - }), - }), - ); - const name = el("b", { className: "lstk-sym", text: show(id) }); - const valueOf = - expr ?? - (link?.갈래 === "숫자" ? String(link.값 ?? "") : isNum(vals[id]) ? String(vals[id]) : ""); - let body: (HTMLElement | string)[]; - if (open) - body = [ - input(show(id), lt("Row_Name"), "lstk-f-s", (t) => - renameRow(id, t.trim() === show(id) ? id : t), - ), - "=", - input(shownText(valueOf), lt("Row_Expr"), "lstk-f-l", (t) => - h.change((doc) => setRow(doc, id, realText(t))), - ), - input(v?.단위 ?? "", lt("Unit"), "lstk-f-s", (t) => - h.change((doc) => { - let x = doc.변수?.find((y) => varId(y) === id); - if (!x) (doc.변수 ??= []).push((x = newVar(id, { 갈래: "식" }))); - x.단위 = t.trim(); + /** 누르면 그 줄 고침(보이는 글을 고침) */ + const tap = (node: HTMLElement, id: string): HTMLElement => { + node.classList.add("lstk-tap"); + node.tabIndex = 0; + node.setAttribute("role", "button"); + node.title = lt("Edit_Hint"); + const go = (): void => { + flip(id, true); + h.change(() => undefined); + }; + node.addEventListener("click", go); + node.addEventListener("keydown", (e) => e.key === "Enter" && go()); + return node; + }; + /** 고친 뒤 닫음 */ + const done = (id: string, fn: () => void): void => { + flip(id, false); + fn(); + }; + const rows = line.기호 + .filter((id) => !hide(id)) + .map((id) => { + const v = vars.get(id); + const link = v?.연결; + const expr = lines.get(id); + const sub = link?.갈래 === "단가산출" && link.키 ? h.subDoc(link.키) : null; + const tag = + link?.갈래 === "표" && !expr + ? tableBtn(id, link) + : link && LINK_TAG[link.갈래 ?? ""] && !expr + ? el("span", { + className: "lstk-tag", + text: `${LINK_TAG[link.갈래!]}${link.키 && !link.키.includes("{") ? ` · ${sub?.이름 ?? h.name(link.키) ?? ""}` : ""}`, + }) + : null; + const tools: HTMLElement[] = []; + if (sub?.결과 && link?.키) + tools.push( + createButton({ + label: lt("Unfold"), + variant: "ghost", + size: 24, + onClick: () => unfoldRow(id, sub, link.키!), }), - ), - el("span", { className: "lstk-val", text: `= ${valText(id)}` }), - createButton({ - label: "×", - variant: "danger-ghost", - size: 24, - onClick: () => h.change((doc) => dropSyms(doc, [id])), - }), - ]; - else if (expr !== undefined || sub || link?.갈래 === "단가산출") - body = [ - name, - `= ${shownText(expr ?? "")}${expr ? " =" : ""}`, - el("span", { className: "lstk-val", text: valText(id) }), - ]; - else - body = [ - name, - "=", - input(shownText(valueOf), valText(id), "lstk-f-m", (t) => - h.change((doc) => setRow(doc, id, realText(t))), - ), - ...(shownUnit(v?.단위) - ? [el("span", { className: "lstk-unit", text: shownUnit(v?.단위) })] - : []), - ]; - const row = el("div", { - className: `lstk-frow ${open ? "is-open" : ""}`, - children: [...body, ...(tag ? [tag] : []), ...tools], + ); + if (link?.갈래 === "단가산출" && !link.키) + tools.push( + createButton({ + label: lt("Find_Title"), + variant: "ghost", + size: 24, + onClick: () => + openFindPicker({ + layer: "system", + owner: null, + title: lt("K_단가산출"), + kinds: ["logic"], + results: true, + logic: (x) => + h.change((doc) => { + const y = doc.변수?.find((z) => varId(z) === id); + if (y) y.연결 = { 갈래: "단가산출", 키: x.키, 입력: {} }; + }), + }), + }), + ); + const name = tap(el("b", { className: "lstk-sym", text: show(id) }), id); + const valueOf = + expr ?? + (link?.갈래 === "숫자" ? String(link.값 ?? "") : isNum(vals[id]) ? String(vals[id]) : ""); + let body: (HTMLElement | string)[]; + const open = editing(id); + if (open) + body = [ + input(show(id), lt("Row_Name"), "lstk-f-s", (t) => + done(id, () => renameRow(id, t.trim() === show(id) ? id : t)), + ), + "=", + input(shownText(valueOf), lt("Row_Expr"), "lstk-f-l", (t) => + done(id, () => h.change((doc) => setRow(doc, id, realText(t)))), + ), + input(v?.단위 ?? "", lt("Unit"), "lstk-f-s", (t) => + done(id, () => + h.change((doc) => { + let x = doc.변수?.find((y) => varId(y) === id); + if (!x) (doc.변수 ??= []).push((x = newVar(id, { 갈래: "식" }))); + x.단위 = t.trim(); + }), + ), + ), + el("span", { className: "lstk-val", text: `= ${valText(id)}` }), + createButton({ + label: "×", + variant: "danger-ghost", + size: 24, + onClick: () => done(id, () => h.change((doc) => dropSyms(doc, [id]))), + }), + createIconButton({ + icon: "check", + title: lt("Close"), + size: 24, + onClick: () => done(id, () => h.change(() => undefined)), + }), + ]; + else if (expr !== undefined || sub || link?.갈래 === "단가산출") + body = [ + name, + "=", + ...(expr + ? [tap(el("span", { className: "lstk-expr", text: shownText(expr) }), id), "="] + : []), + el("span", { className: "lstk-val", text: valText(id) }), + ]; + else + body = [ + name, + "=", + input(shownText(valueOf), valText(id), "lstk-f-m", (t) => + h.change((doc) => setRow(doc, id, realText(t))), + ), + ...(shownUnit(v?.단위) + ? [el("span", { className: "lstk-unit", text: shownUnit(v?.단위) })] + : []), + ]; + const row = el("div", { + className: `lstk-frow ${open ? "is-open" : ""}`, + children: [...body, ...(tag ? [tag] : []), ...tools], + }); + row.dataset.sym = id; + return row; }); - row.dataset.sym = id; - return row; - }); - const more = open - ? [ - createButton({ - label: lt("Add_Row"), - variant: "ghost", - size: 24, - onClick: () => - h.change((doc, items) => { - const id = freshId(doc, "x"); - (doc.변수 ??= []).push(newVar(id, { 갈래: "숫자", 값: 0 })); - blockOf(items).기호.push(id); - }), - }), - ] - : []; - return [head, ...rows, ...more]; + return [head, ...rows]; } diff --git a/A00_Common/line_stack_screen.ts b/A00_Common/line_stack_screen.ts index 056dc5aaa..37ad0e6cf 100644 --- a/A00_Common/line_stack_screen.ts +++ b/A00_Common/line_stack_screen.ts @@ -2,8 +2,9 @@ * line_stack_screen.ts * 단가산출 「줄 쌓기」 탭(PLAN 804-2 · 요구 670~677) — 공용(M01 단가산출 기초 · M02 단가산출 = 길 앞머리만 바꿈) * 위 = 옵션 가로(`formula_doc_options.ts`) · [고침 버림] · [수식 저장](관리자 · 층 쓰기) · M02 사본 [단가 갱신] · [재생성] + * M01(`page` 있음 · 804-7) = 자기 단추 없음 — 페이지 [저장] · [되돌림]이 `save` · `discard` 를 부름 · 고침이 바뀌면 `page()` * 가운데 = 왼쪽 줄 쌓기 편집(`line_stack_edit.ts`) | 오른쪽 산출근거 출력(`line_stack_out.ts` · 기계 시간당 경비 펼침) - * 아래 = 합계 상자(노무비 · 재료비 · 경비 · 계) + * 아래 = 합계 상자(노무비 · 재료비 · 경비 · 계 · 화면 아래 고정 · 요구 687-3) * 정본 = 수식 문서(탭을 바꾸면 다시 읽음) · 줄 쌓기 차림 = 문서 칸 `쌓기` · 804-6 옛 수식 문서 탭 걷음 = 이 화면이 「수식」 탭 * 셈 = 브라우저 계산기(`calcDoc`) 즉시 · 캐시에 없는 연결만 뒤에서 `POST {길}/preview`(옛 요청 끊음) * 고침 = 캐시(sessionStorage)에 쌓고 [수식 저장] 한 번에 `POST {길}/save`(서버가 정본으로 다시 셈) · 자동저장 없음 @@ -60,6 +61,8 @@ export interface StackScreenOptions { copy?: CopyOptions; /** M02 SK 층(804-4) — 줄 쌓기 다른 단가산출 · 일위대가 복사 길 · 없으면(M01) 복사 없음 */ scope?: { layer: Layer; owner: string | null }; + /** 페이지가 저장 · 되돌림을 맡음(M01 · 804-7) — 고침 있음이 바뀔 때 부름 · 화면 위 [고침 버림] · [수식 저장] 없음 */ + page?: () => void; } type Docs = Record; @@ -90,7 +93,8 @@ export function createStackScreen(opt: StackScreenOptions) { const fit = (): void => { let p = root.parentElement; while (p && !/(auto|scroll)/.test(getComputedStyle(p).overflowY)) p = p.parentElement; - root.style.setProperty("--lstk-view", `${p?.clientHeight ?? innerHeight}px`); + const view = (p?.clientHeight ?? innerHeight) - foot.offsetHeight; + root.style.setProperty("--lstk-view", `${Math.max(240, view)}px`); }; window.addEventListener("resize", fit); const cp = opt.copy; @@ -148,6 +152,7 @@ export function createStackScreen(opt: StackScreenOptions) { } catch { /* 캐시가 막혀도 화면 안 고침은 남음 */ } + opt.page?.(); }; // ── 셈 ── @@ -270,7 +275,7 @@ export function createStackScreen(opt: StackScreenOptions) { }); const busy = dirty(); const tools = - edit() && key + edit() && key && !opt.page ? [ ...(cp ? copyTools(true, (job) => void runJob(job)) : []), createButton({ @@ -384,8 +389,10 @@ export function createStackScreen(opt: StackScreenOptions) { if (await askJob(job, got, cache.names)) await save({ 할일: job, 판: version }, ft("Job_Done")); }; - const save = async (job?: Record, done = ft("Saved")): Promise => { - if (!doc || (!job && !dirty())) return; + let loading: Promise = Promise.resolve(); + const save = async (job?: Record, done = ft("Saved")): Promise => { + await loading; // 새 로직 저장 뒤 여는 중이면 다 읽고(캐시 이어 받음) + if (!doc || (!job && !dirty())) return true; const send = job ?? (cp ? { 문서: doc, 끊음: cut, 판: version } : { 문서: doc }); const was = saved; showLoadingOverlay(); @@ -402,9 +409,11 @@ export function createStackScreen(opt: StackScreenOptions) { recalc(); draw(); if (cp && got.판) cp.onSaved?.(got.판); - showToast(done, "success"); + if (done) showToast(done, "success"); + return true; } catch (e) { showToast(`${ft("Save_Failed")} — ${e instanceof Error ? e.message : ""}`, "error"); + return false; } finally { hideLoadingOverlay(); } @@ -483,9 +492,14 @@ export function createStackScreen(opt: StackScreenOptions) { root, /** 로직 · SK 키로 그림(빈 키 = 저장 전 새 것) — 같은 키는 그대로 · `force` = 다시 읽음(탭 바꿈) */ draw: (next: string, force = false): void => { - if (force || next !== shown) void open(next); + if (force || next !== shown) loading = open(next); }, canLeave, + dirty, + /** 페이지 [저장](804-7) — 고침 있으면 서버 정본 저장 · 알림은 페이지가(`done` 빈 글) · 실패 = false */ + save: (): Promise => save(undefined, ""), + /** 페이지 [되돌림] — 저장 뒤 상태로(캐시 지움) */ + discard: (): Promise => discard(), /** 시험 · 확인용 */ state: () => ({ doc, answer, dirty: dirty(), items: doc ? stackOf(doc) : [] }), }; diff --git a/A00_Common/line_stack_text.ts b/A00_Common/line_stack_text.ts index 691be5295..299be2bdb 100644 --- a/A00_Common/line_stack_text.ts +++ b/A00_Common/line_stack_text.ts @@ -12,7 +12,9 @@ const TEXT = { Sum: ["계", "Sum"], Sub_Sum: ["소 계", "Subtotal"], Add_Item: ["+ 그룹", "+ Group"], - Add_Line: ["+ 줄", "+ Line"], + Add_Line: ["줄 더함", "Add line"], + Add_율: ["율 — 그룹 소계의 %", "Rate — % of subtotal"], + Add_식: ["식 — 공식 틀", "Formula"], Del_Item: ["그룹 지움", "Delete group"], Del: ["지움", "Delete"], Item: ["그룹", "Group"], @@ -20,23 +22,22 @@ const TEXT = { Drag: ["끌어 옮기기", "Drag to move"], Fold_Item: ["그룹 접기", "Collapse group"], Unfold_Item: ["그룹 펼치기", "Expand group"], + Unfold_Sub: ["시간당 경비 · 산출 펼치기", "Show breakdown"], + Fold_Sub: ["접기", "Hide breakdown"], Dropped: ["지움 — 눌러 되돌리기", "deleted — click to undo"], Undo_Late: ["그 뒤 고친 것이 있어 못 되돌림", "Changed since — cannot undo"], - Title: ["그룹 이름(비우면 번호 없음)", "Group title"], - Gana: ["가나다", "Sub"], + Title: ["그룹 이름", "Group title"], K_인력: ["인력", "Labor"], K_재료: ["재료", "Material"], K_기계: ["기계", "Machine"], - K_단가산출: ["다른 단가산출", "Unit price"], - K_복사: ["다른 단가산출 · 일위대가 복사", "Copy unit price · combo"], + K_단가산출: ["단가산출", "Unit price"], + K_복사: ["단가산출 · 일위대가 복사", "Copy unit price · combo"], Self_Take: ["지금 단가산출 자신은 못 가져옴", "Cannot copy itself"], Take_Failed: ["복사 못 함", "Copy failed"], - K_율: ["율 · 식", "Rate"], + K_율: ["율", "Rate"], K_공식: ["공식", "Formula"], K_표값: ["표 값", "Table values"], - K_새공식: ["공식 — 새 공식(빈 묶음)", "Formula — new"], - K_틀: ["공식 — 틀 찾기(단가산출 결과)", "Formula — template"], - K_글: ["글 · 근거", "Text"], + K_글: ["근거 글", "Basis note"], Name: ["이름", "Name"], Unit: ["단위", "Unit"], Cost: ["비목", "Cost"], @@ -46,17 +47,17 @@ const TEXT = { Find: ["찾기", "Find"], Pick: ["고름 ▾", "Pick ▾"], None_Found: ["찾은 것 없음", "Nothing found"], - Unfold: ["식 고치기", "Edit formula"], - Fold: ["접기", "Fold"], + Unfold: ["식으로 풀기", "Unfold"], + Edit_Hint: ["눌러 고침", "Click to edit"], + Add_Row: ["식 줄 더함", "Add row"], New_Formula: ["새 공식", "New formula"], My_Formula: ["내 공식", "My formula"], My_Tpl: ["내 틀", "My template"], Changed_From: ["에서 변경", "changed"], - Add_Row: ["+ 「이름 = 식」 줄", "+ Row"], Row_Name: ["이름", "Name"], Row_Expr: ["값 또는 식", "Value or expr"], Bad_Name: ["못 쓰는 이름 · 같은 이름 있음", "Bad or duplicate name"], - Empty_Item: ["줄 없음 — [+ 줄]", "No lines — [+ Line]"], + Empty_Item: ["줄 없음 — [+]", "No lines — [+]"], Empty: ["빈 단가산출 — [+ 그룹] 으로 시작", "Empty — start with [+ Group]"], Text_Ph: ["< 품셈 근거 > · ※ 메모 · 별산", "Note"], Notes: [ diff --git a/M01_MasterData/M01_MasterData_Doc.py b/M01_MasterData/M01_MasterData_Doc.py index 061820e76..747e295e4 100644 --- a/M01_MasterData/M01_MasterData_Doc.py +++ b/M01_MasterData/M01_MasterData_Doc.py @@ -143,3 +143,57 @@ def table(key: str) -> dict: rec = mr.Rec(ml.Links(store.loaded()[1], {})) rec.see_table(key) return rec.표[key] + + +# ── 로직 지우기(804-7 · 요구 687-1) — 로직 줄 + 수식 문서를 서버 정본에서 함께 ── +def users(key: str) -> list[str]: + """이 로직을 부르는 곳 — 다른 수식 문서(단가산출 연결) · 일위대가 조합 · M02 단가산출(바탕). + 다른 로직 줄이 부르는 것은 로직 저장 검사(422)가 막음.""" + from M01_MasterData import M01_MasterData_Store_Combo as combo_store + from M02_MasterTemplete import M02_Template_Layers as layers # M02 가 M01 을 부름 — 늦게 + + out = [] + for k, d in docs().items(): + if k != key and any( + (v.get("연결") or {}).get("갈래") == "단가산출" + and str((v.get("연결") or {}).get("키")) == key + for v in d.get("변수") or [] + ): + out.append(f"단가산출 {k} {d.get('이름') or ''}".strip()) + for c in combo_store.combos_of_logic(key)["combos"]: + out.append(f"일위대가 조합 {c.get('키')} {c.get('이름') or ''}".strip()) + for where, path in layers.layer_files("sangeun"): + try: + sk = json.loads(path.read_bytes()) + except (OSError, ValueError): + continue + if str((sk.get("바탕") or {}).get("로직") or "") == key: + out.append(f"M02 단가산출 {where} {sk.get('이름') or path.stem}") + return out + + +def drop(key: str) -> None: + """수식 문서 줄 하나 지움(없으면 그냥).""" + name = _where(key) + if not name: + return + path = folder() / name + data = ml.load_file(path) + data["줄"] = [d for d in data.get("줄") or [] if str(d.get("키")) != key] + path.write_text(ml.dump(data), encoding="utf-8", newline="\n") + _memo.clear() + + +def remove(key: str, file: str, version: str, confirmed: bool) -> dict: + """로직 지우기 — 부르는 곳이 있으면 409 {쓰는곳} · 로직 저장 길(개정 확인 · 판 · 검사) 뒤 문서도.""" + from M01_MasterData import M01_MasterData_Store_Make as make + + used = users(key) + if used: + raise store.StoreError(409, {"쓰는곳": used}) + got = make.save( + [{"file": file, "version": version, "changes": [{"op": "delete", "key": key}]}], + confirmed, + ) + drop(key) + return got diff --git a/M01_MasterData/M01_MasterData_Router.py b/M01_MasterData/M01_MasterData_Router.py index 0715b9562..e4f9f3f3d 100644 --- a/M01_MasterData/M01_MasterData_Router.py +++ b/M01_MasterData/M01_MasterData_Router.py @@ -415,6 +415,25 @@ def post_logic_doc_save(body: DocBody) -> dict: return _call(doc_screen.save, body.문서) +class RemoveBody(BaseModel): + key: str + file: str + version: str + 개정확인: bool = False + + +@router.get("/logic/users") +def get_logic_users(key: str) -> dict: + """로직을 부르는 곳(지우기 전 확인 · 읽기만) — {쓰는곳}.""" + return {"쓰는곳": doc_screen.users(key)} + + +@router.post("/logic/remove") +def post_logic_remove(body: RemoveBody) -> dict: + """로직 지우기(804-7) — 로직 줄 + 수식 문서 · 부르는 곳 있으면 409 {쓰는곳}.""" + return _call(doc_screen.remove, body.key, body.file, body.version, body.개정확인) + + @router.post("/logic/doc/links") def post_logic_doc_links(body: LinksBody) -> dict: """연결 후보 — 요소 · 표 · 단가산출(수식 문서) 이름 찾기(읽기만).""" diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab.ts b/M01_MasterData/M01_MasterData_UI_LogicLab.ts index 4b9e661ec..08256219a 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab.ts @@ -6,6 +6,9 @@ * * 데이터 흐름(CLAUDE.md 5장) — 고친 것은 캐시(sessionStorage)에 쌓고 [저장] 한 번에 `POST /save`. * 409 = 그 사이 파일이 바뀜 · 422 = 검사 걸림(아무것도 안 씀) — 까닭은 서버 글 그대로 보임. + * 머리 단추(804-7 · 요구 687-2) = [신규 로직] · [로직 복사/생성] · [로직 삭제] · [되돌림] · [저장] + * [저장] = 기본정보 · 별칭(로직 줄 `/save`) + 수식(줄 쌓기 `/logic/doc/save`) 한 번에 · [되돌림] = 둘 다 저장 뒤 상태로 + * [로직 삭제] = 확인 → 서버 정본에서 바로(`/logic/remove` — 로직 줄 + 수식 문서 · 부르는 곳 있으면 막고 까닭) → 로직 목록 * ========================================================================== */ import { t as L } from "@ui/ui_template_locale"; @@ -23,6 +26,8 @@ import { fetchLogicFiles, fetchLogics, fetchLogicSubs, + fetchLogicUsers, + removeLogic, saveFiles, type CalcLine, type TextAnswer, @@ -108,7 +113,12 @@ export async function mountM01LogicLab( let recalc: number | undefined; const drive = createDrive(); // 수식 = 줄 쌓기(804-2 · 804-6 옛 수식 문서 탭 걷음) — 수식 문서 · 캐시 m01_doc_drafts - const stack = createStackScreen({ base: "/m01/logic/doc", admin, cache: "m01_doc_drafts" }); + const stack = createStackScreen({ + base: "/m01/logic/doc", + admin, + cache: "m01_doc_drafts", + page: () => persist(), + }); const leave = (): Promise => stack.canLeave(); const editor = el("div", { className: "m01-logic__editor" }); // 로직 상세 — 왼쪽 지금 내용 · 오른쪽 접는 패널 「원문」 @@ -166,8 +176,7 @@ export async function mountM01LogicLab( list.setMarks(marks, extra, aliases); const n = Object.keys(drafts).length; pending.textContent = n ? tx("Bar_Pending", { n }) : ""; - saveButton.disabled = discardButton.disabled = n === 0; - saveButton.hidden = discardButton.hidden = n === 0; // 저장 안 한 고친 것이 없으면 숨김 + saveButton.disabled = discardButton.disabled = n === 0 && !stack.dirty(); // 로직 줄 · 수식 둘 다 봄 }; const touch = (): void => { @@ -307,6 +316,7 @@ export async function mountM01LogicLab( }; let allLogics: LogicSummary[] = []; + let saving = false; // [저장] 뒤 목록 다시 읽는 중 — 거름 되살림이 연 로직을 떠나지 않게(저장 뒤 다시 엶) const reload = async (): Promise => { const [logics, subs] = await Promise.all([fetchLogics(), fetchLogicSubs()]); @@ -324,7 +334,7 @@ export async function mountM01LogicLab( restore: true, onPick: (sub, detail) => { list.setPick(sub, detail); - if (opened) void leave().then((ok) => ok && show(null)); + if (opened && !saving) void leave().then((ok) => ok && show(null)); }, }), ); @@ -351,8 +361,21 @@ export async function mountM01LogicLab( } }; + /** 부르는 곳 까닭 띠 */ + const usedBy = (used: string[]): void => { + errors.replaceChildren( + el("strong", { text: tx("Delete_Used") }), + ...used.map((x) => el("div", { text: x })), + ); + errors.hidden = false; + }; + const onDelete = async (): Promise => { if (!opened?.row) return; + if (opened.origKey) { + const used = (await fetchLogicUsers(opened.origKey).catch(() => null))?.쓰는곳 ?? []; + if (used.length) return usedBy(used); // 확인 창 전에 막음 + } if ( !(await showConfirmDialog( tx("Confirm_Delete", { v: opened.row.원문번호 || opened.row.키 }), @@ -360,21 +383,38 @@ export async function mountM01LogicLab( )) ) return; - if (opened.origKey === null) { - delete drafts[opened.id]; + const was = opened; + if (was.origKey === null) { + delete drafts[was.id]; persist(); show(null); return; } - drafts[opened.id] = pick(opened, null); - opened.row = null; - persist(); - show(opened); + const key = was.origKey; + showLoadingOverlay(); + try { + const one = await fetchLogic(key); // 지금 판(캐시에 옛 판이 남았어도) + const done = await saveRevised((ok) => removeLogic(key, one.file, one.version, ok)); + if (!done) return; // 개정 확인 취소 + delete drafts[was.id]; + await stack.discard(); // 그 로직 수식 캐시도 비움 + await reload(); + show(null); // 로직 목록 + showToast(tx("Delete_Done", { v: was.row?.원문번호 || key }), "success"); + } catch (error) { + const used = + error instanceof ApiError ? (error.detail as { 쓰는곳?: string[] })?.쓰는곳 : null; + if (used?.length) usedBy(used); + else failed(error); + } finally { + hideLoadingOverlay(); + } }; const onDiscard = async (): Promise => { if (!(await showConfirmDialog(tx("Confirm_Discard"), tx("Bar_Discard")))) return; drafts = {}; + await stack.discard(); persist(); const was = opened; if (was && was.origKey !== null) @@ -393,7 +433,10 @@ export async function mountM01LogicLab( byFile.set(d.file, part); } if (!byFile.size) { - showToast(tx("Save_Nothing"), "info"); + // 수식만 고침 — 줄 쌓기 정본 저장 + if (!stack.dirty()) return void showToast(tx("Save_Nothing"), "info"); + if (await stack.save()) showToast(tx("Save_Done"), "success"); + persist(); return; } showLoadingOverlay(); @@ -403,13 +446,16 @@ export async function mountM01LogicLab( if (!saved) return; // 개정 확인 취소 = 캐시 그대로 const was = opened; drafts = {}; - await reload(); + saving = true; + await reload().finally(() => (saving = false)); showToast(tx("Save_Done"), "success"); const newKey = was?.row && !was.row.키 ? saved.added?.at(-1)?.key : undefined; const made = newKey ? allLogics.find((x) => x.키 === newKey) : undefined; if (made) await open(logicId(made.구분, made.키), made.구분, made.키); else if (was?.row?.키) await open(logicId(was.sub, was.row.키), was.sub, was.row.키); else show(null); + if (opened) await stack.save(); // 수식 고침도 같은 [저장]에(여는 중이면 다 읽고 · 새 로직 = 새 키로 이어 받은 뒤 · 고침 없으면 그냥) + persist(); } catch (error) { failed(error); } finally { diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Side.css b/M01_MasterData/M01_MasterData_UI_LogicLab_Side.css index 258537609..175dcb073 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Side.css +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Side.css @@ -61,3 +61,8 @@ background: color-mix(in srgb, var(--color-accent) 14%, transparent); box-shadow: -3px 0 0 var(--color-accent); } + +/* 원문 손잡이 = 브라우저 오른쪽 끝(요구 687-17) — 칸을 페이지 오른쪽 여백(로직 칸 12 + 패널 24)만큼 밀어 붙임 · 본문이 그 자리까지 */ +html:not([data-size="compact"], [data-size="medium"]) .m01-master__panel .m01-logic .m01lab-side { + margin-inline-end: calc(-1 * (var(--spacing-12) + var(--spacing-24))); +} diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Api.ts b/M01_MasterData/M01_MasterData_UI_Logic_Api.ts index 042407623..edf0cdcdf 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Api.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Api.ts @@ -307,6 +307,18 @@ export const saveFiles = ( 개정확인 = false, ): Promise<{ files: { file: string; version: string }[] }> => request("/save", { files, 개정확인 }); +/** 로직을 부르는 곳(지우기 전 · 804-7) — 다른 수식 문서 · 일위대가 조합 · M02 단가산출 */ +export const fetchLogicUsers = (key: string): Promise<{ 쓰는곳: string[] }> => + request(`/logic/users?key=${encodeURIComponent(key)}`); + +/** 로직 지우기(804-7) — 로직 줄 + 수식 문서 · 409 {쓰는곳} = 부르는 곳 있어 막음 · 409 {개정} = 정본 개정 확인 */ +export const removeLogic = ( + key: string, + file: string, + version: string, + 개정확인 = false, +): Promise => request("/logic/remove", { key, file, version, 개정확인 }); + /** 본떠 만들기 — 정본·자체 둘 다 본뜸 · 새 키(`GX…`)는 서버가 줌(계약 5장) */ export const copyLogic = ( key: string, diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Text.ts b/M01_MasterData/M01_MasterData_UI_Logic_Text.ts index d53e96f23..9c83a94f6 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Text.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Text.ts @@ -27,7 +27,7 @@ const TEXT = { Bar_Back: ["← 로직 목록", "← Logics"], Bar_Delete: ["로직 삭제", "Delete logic"], Bar_Save: ["저장", "Save"], - Bar_Discard: ["캐시 삭제", "Clear cache"], + Bar_Discard: ["되돌림", "Revert"], Bar_Pending: ["저장 안 한 로직 {n} 개", "{n} unsaved logics"], Head_File: ["파일", "File"], Head_Key: ["키", "Key"], @@ -101,12 +101,14 @@ const TEXT = { Lab_Title: ["단가산출 기초", "Unit-price logic"], Loading: ["불러오는 중", "Loading"], Confirm_Delete: [ - "로직 「{v}」 을 지울까요? [저장] 때 파일에서 빠짐", - "Delete logic “{v}”? Removed on save", + "로직 「{v}」 을 지울까요? 로직 · 수식이 서버에서 바로 지워짐", + "Delete logic “{v}”? Logic and formula are removed on the server now", ], + Delete_Done: ["로직 「{v}」 지움", "Deleted logic “{v}”"], + Delete_Used: ["부르는 곳이 있어 못 지움", "In use — not deleted"], Confirm_Discard: [ - "저장 안 한 고친 것(캐시)을 모두 지울까요?", - "Clear all unsaved edits (cache)?", + "저장 안 한 고친 것(기본정보 · 수식)을 버리고 저장 뒤 상태로 되돌릴까요?", + "Discard unsaved edits (info · formula) and revert to the saved state?", ], Confirm_Leave: [ "저장 안 한 로직이 있음 — 다른 로직을 열어도 고친 것은 남음", diff --git a/M01_MasterData/M01_MasterData_UI_Side.ts b/M01_MasterData/M01_MasterData_UI_Side.ts index c46bd484d..7a83aa253 100644 --- a/M01_MasterData/M01_MasterData_UI_Side.ts +++ b/M01_MasterData/M01_MasterData_UI_Side.ts @@ -517,15 +517,9 @@ ${first.file}` aliasSec .querySelector(".ui-collapsible__title") ?.addEventListener("click", () => void openAlias()); - // 시험 페이지 「줄 쌓기 단가산출」(PLAN 804-1) — 새 탭 · 지울 때 이 덩이 + main.py 마운트 + vite.config.ts 줄 + tools/line-stack-test 폴더 - const lineStackSec = section("시험: 줄 쌓기", null); - lineStackSec.classList.add("m01-side__leaf"); - lineStackSec - .querySelector(".ui-collapsible__title") - ?.addEventListener("click", () => window.open("/tools/line-stack-test/", "_blank")); const root = el("div", { className: "m01-side ui-sidebar-panel", - children: [...groups, logic, createDivider(), optionsSec, aliasSec, lineStackSec], + children: [...groups, logic, createDivider(), optionsSec, aliasSec], }); attachCollapsible(root); void Promise.all(GROUPS.filter((g) => g !== FUEL_GROUP).map(refresh)).catch((error) => diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Router.py b/M02_MasterTemplete/DataFlow/M02_DF_Router.py index 04b9e9728..6d293fa2c 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Router.py +++ b/M02_MasterTemplete/DataFlow/M02_DF_Router.py @@ -1,7 +1,7 @@ """단위 · 금액 표준 표 읽기 — `GET /api/m02/df/standards` → {unit, money, saved}. 부르는 곳 = 단위 고르기(`M02_DF_UnitPick.ts`) · 끝수 표준(`M02_DF_Round.ts`). -표 = `_standard/` 파일(git 이 따라감) · 권한 = 시스템 관리자(`main.py` 한 줄). +표 = `_standard/` 파일(git 이 따라감) · 권한 = 로그인만(읽기만 · `main.py` 한 줄 · M02 줄 쌓기 단위 고르기). """ from __future__ import annotations diff --git a/M02_MasterTemplete/DataFlow/M02_DF_UnitPick.ts b/M02_MasterTemplete/DataFlow/M02_DF_UnitPick.ts index 06c69e67d..d21eceab2 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_UnitPick.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_UnitPick.ts @@ -1,6 +1,6 @@ /* ============================================================================= * M02_DF_UnitPick.ts - * 단가산출 흐름도 공용 단위 고르기 — 치면 걸러진 목록 · 고르기만(손글 안 받음) · 「-」 = 없음 + * 단가산출 흐름도 공용 단위 고르기 — 치면 걸러진 목록 · 고르기만(손글 안 받음 · `free` = 목록에 없는 친 글도 씀) · 「-」 = 없음 * 목록 = 단위 표준(산림품셈 1-2-2) 단위 + 부른 쪽이 주는 마스터 결과단위 등 + 원 * 표준 표 읽는 길은 `setUnitSource` 로 바꿔 끼움(기본 = 서버 /standards) — 다른 판도 이것을 씀 * 글 [한, 영] 짝은 아래 TEXT @@ -8,6 +8,7 @@ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, el } from "@ui/ui_template_elements"; +import type { UiSize } from "@ui/ui_template_button"; import { cfStandards } from "./M02_DF_Api"; import "./M02_DF_UnitPick.css"; import { createEmpty } from "@ui/ui_template_empty"; @@ -17,6 +18,7 @@ const TEXT = { None: ["없음", "none"], Filter: ["단위를 쳐서 찾음", "Type to filter units"], Empty: ["맞는 단위가 없음 — 목록에서만 고름", "No match — pick from the list only"], + Free: ["「{u}」 그대로 씀", "Use “{u}”"], Pick: ["단위 고르기", "Pick a unit"], } as const; @@ -99,6 +101,10 @@ export interface UnitPickOptions { extra?: string[]; readOnly?: boolean; ariaLabel?: string; + /** 크기 단(줄 쌓기 24 등) — 없으면 옛 꼴 */ + size?: UiSize; + /** 목록에 없는 친 글도 단위로 씀(줄 쌓기 · 804-7) */ + free?: boolean; } /** 단위 고르기 — 단추를 누르면 걸러지는 목록 · 목록에서 고르기만 됨 */ @@ -108,6 +114,7 @@ export function createUnitPick(opts: UnitPickOptions): HTMLElement { const open = createButton({ label: opts.value || NO_UNIT, variant: "ghost", + size: opts.size, disabled: opts.readOnly, onClick: () => toggle(), }); @@ -136,13 +143,18 @@ export function createUnitPick(opts: UnitPickOptions): HTMLElement { const q = input.value.trim().toLowerCase(); shown = all.filter((u) => u === NO_UNIT || !q || u.toLowerCase().includes(q)); if (q && !NO_UNIT.includes(q)) shown = shown.filter((u) => u !== NO_UNIT); + const typed = input.value.trim(); + if (opts.free && typed && !all.includes(typed)) shown.push(typed); // 맨 끝 「그대로 씀」 줄 at = Math.min(at, Math.max(0, shown.length - 1)); list.replaceChildren( ...(shown.length ? shown.map((u, i) => { const b = el("button", { className: `ui-list-row ui-sidebar-row m02-df-unitpick__item${i === at ? " is-on" : ""}${u === (opts.value || NO_UNIT) ? " is-now" : ""}`, - text: label(u), + text: + opts.free && u === input.value.trim() && !all.includes(u) + ? tu("Free").replace("{u}", u) + : label(u), attrs: { type: "button", role: "option" }, }); b.addEventListener("click", () => pick(u)); diff --git a/main.py b/main.py index 58ff26c8b..1983be204 100644 --- a/main.py +++ b/main.py @@ -411,17 +411,6 @@ app.mount( ) logger.info(f"✓ B07 CAD 정적 서빙 경로 등록: /b07-cad → {B07_CAD_DIST_DIR}") -# 시험 페이지 「줄 쌓기 단가산출」(PLAN 804-1) — 지울 때 이 덩이 + tools/line-stack-test 폴더 + vite.config.ts 「/tools」 줄 + M01 왼쪽 패널 「시험: 줄 쌓기」 덩이 -app.mount( - "/tools/line-stack-test", - _NoCacheHtmlStatic( - directory=str(Path(__file__).parent / "tools" / "line-stack-test"), - html=True, - check_dir=False, - ), - name="line-stack-test", -) - # ───────────────────────────────────────────────────────────────────────── # 기본 엔드포인트 # ───────────────────────────────────────────────────────────────────────── @@ -564,10 +553,10 @@ app.include_router( __import__("M01_MasterData.M01_MasterData_Alias", fromlist=["router"]).router, dependencies=system_admin_only, ) # 별칭 장부 모으기(718-2) -# 단위 · 금액 표준 표 읽기(`GET /api/m02/df/standards` · 단위 고르기 `M02_DF_UnitPick`) +# 단위 · 금액 표준 표 읽기(`GET /api/m02/df/standards` · 단위 고르기 `M02_DF_UnitPick`) — 읽기만 · 로그인만(M02 줄 쌓기 단위) app.include_router( __import__("M02_MasterTemplete.DataFlow.M02_DF_Router", fromlist=["router"]).router, - dependencies=system_admin_only, + dependencies=protected, ) # 마스터 템플릿 — 시스템 층은 시스템 관리자만 · 층(회사·개인·프로젝트)·도면 길은 로그인만(권한은 길 안에서). app.include_router(m02_master_template_router, dependencies=system_admin_only) diff --git a/resources/master_data/scripts/master_doc_convert.py b/resources/master_data/scripts/master_doc_convert.py index 1ef8c91fa..12b576941 100644 --- a/resources/master_data/scripts/master_doc_convert.py +++ b/resources/master_data/scripts/master_doc_convert.py @@ -60,6 +60,24 @@ def _harvest(master, key: str) -> dict: return out +_CODE = re.compile(r"^(?:EQ|MT|LB)\d") + + +def res_low(master, link: dict, low: str) -> str: + """요소 키가 아래첨자로 샌 것(`k_EQ0006` · 804-7 · 요구 687-18) → 자원 이름 끝 낱말(`k_하베스터`) · + 괄호 · 쉼표 뒤 · 숫자로 시작하는 규격 낱말 뺌 · 겹침은 부른 쪽이 2 · 3.""" + if not _CODE.match(low or "") or (link or {}).get("갈래") != "요소": + return low + try: + row = master.get(str(link.get("키") or "")) + except _ERR: + return low + name = re.sub(r"\(.*?\)", "", str(row.get("이름") or "")).split(",")[0] + words = [w for w in name.split() if not w[:1].isdigit()] + got = msym.short(words[-1]) if words else "" + return got if got and got != "·" else low + + def _clean(text: str) -> str: """기호 · 첨자 글 다듬기 — 낱말 아닌 글 뺌.""" return re.sub(r"[^\w′'·]|_", "", str(text or "")) @@ -138,6 +156,7 @@ class _Conv: else: fam = {"블록": "결과"}.get(family, family) parts = msym.lookup(name or raw, fam if fam in msym.FAMILY else "요소") + parts = (parts[0], res_low(self.master, link, parts[1]), parts[2]) self.reg.want(key, parts) self.order.append(key) label = str(node.get("이름") or name or raw) diff --git a/resources/master_data/수식문서/수식_건설품셈_02장_토목.json b/resources/master_data/수식문서/수식_건설품셈_02장_토목.json index d69b31b57..fee174902 100644 --- a/resources/master_data/수식문서/수식_건설품셈_02장_토목.json +++ b/resources/master_data/수식문서/수식_건설품셈_02장_토목.json @@ -885,7 +885,7 @@ {"열쇠": "P_기계시간당4", "기호": "P", "아래": "기계시간당4", "위": "", "이름": "기계 시간당 손료(연료·조종원 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000273", "입력": {"기계": "'5220-0015'"}}}, {"열쇠": "Q_모르타르믹서^표", "기호": "Q", "아래": "모르타르믹서", "위": "표", "이름": "맨홀보수.모르타르 믹서(0.3㎥)", "단위": "대", "연결": {"갈래": "표", "표": "QC001933", "조건": {"맨홀구분": "S_맨홀구분"}, "칸": "모르타르 믹서(0.3㎥)"}}, {"열쇠": "P_기계시간당5", "기호": "P", "아래": "기계시간당5", "위": "", "이름": "기계 시간당 손료(연료·조종원 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000273", "입력": {"기계": "'7992-0001'"}}}, - {"열쇠": "P_EQ0005", "기호": "P", "아래": "EQ0005", "위": "", "이름": "EQ000532.주연료2", "단위": "", "연결": {"갈래": "요소", "키": "EQ000532", "칸": "주연료2"}}, + {"열쇠": "P_믹서", "기호": "P", "아래": "믹서", "위": "", "이름": "EQ000532.주연료2", "단위": "", "연결": {"갈래": "요소", "키": "EQ000532", "칸": "주연료2"}}, {"열쇠": "P_휘발유", "기호": "P", "아래": "휘발유", "위": "", "이름": "휘발유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:휘발유:{S_지역}"}} ], "식": [ @@ -903,7 +903,7 @@ {"이름": "트럭 2.5ton", "단위": "시간", "비목": "묶음", "수량": "Q_트럭^표 × 8 ÷ Q_시공", "단가": "P_기계시간당3", "묶음": "트럭 2.5ton"}, {"이름": "소형브레이커(전기식) 1.5㎾", "단위": "시간", "비목": "묶음", "수량": "Q_소형브레이커^표 × 8 ÷ Q_시공", "단가": "P_기계시간당4", "출처": "건설품셈 유지관리 2-1-11", "묶음": "소형브레이커(전기식) 1.5㎾"}, {"이름": "모르타르 믹서 0.3㎥", "단위": "시간", "비목": "묶음", "수량": "Q_모르타르믹서^표 × 8 ÷ Q_시공", "단가": "P_기계시간당5", "출처": "건설품셈 유지관리 2-1-11", "묶음": "모르타르 믹서 0.3㎥"}, - {"이름": "모르타르 믹서 연료(휘발유)", "단위": "ℓ", "비목": "재료비", "수량": "P_EQ0005 × Q_모르타르믹서^표 × 8 ÷ Q_시공", "단가": "P_휘발유", "출처": "건설품셈 공통 8-4-8 「1.87㎾ 휘발유1.3」", "묶음": "재료 · 공구손료 및 경장비"}, + {"이름": "모르타르 믹서 연료(휘발유)", "단위": "ℓ", "비목": "재료비", "수량": "P_믹서 × Q_모르타르믹서^표 × 8 ÷ Q_시공", "단가": "P_휘발유", "출처": "건설품셈 공통 8-4-8 「1.87㎾ 휘발유1.3」", "묶음": "재료 · 공구손료 및 경장비"}, {"이름": "공구손료 및 경장비", "단위": "식", "비목": "경비", "단가": "(Q_포장공 × w_포장공 + Q_특별인부 × w_특별인부 + Q_보통인부 × w_보통인부) × 4%", "출처": "건설품셈 유지관리 2-1-11", "묶음": "재료 · 공구손료 및 경장비"} ], "끝수": { diff --git a/resources/master_data/수식문서/수식_건설품셈_03장_토공사.json b/resources/master_data/수식문서/수식_건설품셈_03장_토공사.json index 78b8edcae..1e2796a26 100644 --- a/resources/master_data/수식문서/수식_건설품셈_03장_토공사.json +++ b/resources/master_data/수식문서/수식_건설품셈_03장_토공사.json @@ -1869,7 +1869,7 @@ {"열쇠": "P_굴착기무한궤", "기호": "P", "아래": "굴착기무한궤", "위": "", "이름": "굴착기(무한궤도) 시간당 기계경비", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000267", "입력": {"기계": "'0201-0060'", "지역": "S_지역", "보정작업": "S_보정작업"}}}, {"열쇠": "Q_물탱크살수차^표", "기호": "Q", "아래": "물탱크살수차", "위": "표", "이름": "지능형 다짐공 시험시공 및 다짐관리 기준 수립.물탱크_살수차", "단위": "대", "연결": {"갈래": "표", "표": "QC000264", "조건": {}, "칸": "물탱크_살수차"}}, {"열쇠": "P_기계시간당3", "기호": "P", "아래": "기계시간당3", "위": "", "이름": "기계 시간당 기계경비(손료 보정 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000268", "입력": {"기계": "'7204-0160'", "지역": "S_지역"}}}, - {"열쇠": "P_EQ0000", "기호": "P", "아래": "EQ0000", "위": "", "이름": "EQ000098.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ000098", "칸": "주연료"}}, + {"열쇠": "P_진동롤러", "기호": "P", "아래": "진동롤러", "위": "", "이름": "EQ000098.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ000098", "칸": "주연료"}}, {"열쇠": "r_주연료^표", "기호": "r", "아래": "주연료", "위": "표", "이름": "지능형 다짐 롤러 계수 주연료 할증", "단위": "구분 따름", "연결": {"갈래": "표", "표": "QC000270", "조건": {"구분": "'주연료 할증'"}, "칸": "값"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}} ], @@ -1881,7 +1881,7 @@ {"이름": "IC 진동롤러 10ton", "단위": "시간", "비목": "묶음", "수량": "IC_진동롤러자주^표 × 8", "단가": "P_기계시간당2", "묶음": "IC 진동롤러 10ton"}, {"이름": "굴착기 0.6㎥", "단위": "시간", "비목": "묶음", "수량": "Q_굴착기^표 × 8", "단가": "P_굴착기무한궤", "묶음": "굴착기 0.6㎥"}, {"이름": "물탱크(살수차) 16,000ℓ", "단위": "시간", "비목": "묶음", "수량": "Q_물탱크살수차^표 × 8", "단가": "P_기계시간당3", "묶음": "물탱크(살수차) 16,000ℓ"}, - {"이름": "IC 롤러 주연료 할증", "단위": "ℓ", "비목": "재료비", "수량": "IC_진동롤러자주^표 × 8 × P_EQ0000 × r_주연료^표 ÷ 100", "단가": "P_자동차용경유", "출처": "건설품셈 공통 3-11-5 주⑦", "묶음": "재료"} + {"이름": "IC 롤러 주연료 할증", "단위": "ℓ", "비목": "재료비", "수량": "IC_진동롤러자주^표 × 8 × P_진동롤러 × r_주연료^표 ÷ 100", "단가": "P_자동차용경유", "출처": "건설품셈 공통 3-11-5 주⑦", "묶음": "재료"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -1970,14 +1970,14 @@ {"열쇠": "시공량", "기호": "Q", "아래": "시공", "위": "", "이름": "시공량", "단위": "", "연결": {"갈래": "표", "표": "QC000262", "조건": {"공종": "S_공종"}, "칸": "시공량"}}, {"열쇠": "연료", "기호": "S", "아래": "연료", "위": "", "이름": "연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ000004", "칸": "주연료종류"}}, {"열쇠": "P_기계시간당", "기호": "P", "아래": "기계시간당", "위": "", "이름": "기계 시간당 기계경비(손료 보정 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000268", "입력": {"기계": "'0101-0019'", "지역": "S_지역"}}}, - {"열쇠": "P_EQ0000", "기호": "P", "아래": "EQ0000", "위": "", "이름": "EQ000004.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ000004", "칸": "주연료"}}, + {"열쇠": "P_불도저", "기호": "P", "아래": "불도저", "위": "", "이름": "EQ000004.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ000004", "칸": "주연료"}}, {"열쇠": "r_주연료^표", "기호": "r", "아래": "주연료", "위": "표", "이름": "머신 가이던스(MG) 불도저 계수 주연료 할증", "단위": "구분 따름", "연결": {"갈래": "표", "표": "QC000268", "조건": {"구분": "'주연료 할증'"}, "칸": "값"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:{S_연료}:{S_지역}"}} ], "식": [], "금액": [ {"이름": "MG 불도저 19ton", "단위": "시간", "비목": "묶음", "수량": "8 ÷ Q_시공", "단가": "P_기계시간당", "묶음": "MG 불도저 19ton"}, - {"이름": "주연료 할증", "단위": "ℓ", "비목": "재료비", "수량": "8 ÷ Q_시공 × P_EQ0000 × r_주연료^표 ÷ 100", "단가": "P_자동차용경유", "출처": "건설품셈 공통 3-11-3 주연료 할증", "묶음": "재료"} + {"이름": "주연료 할증", "단위": "ℓ", "비목": "재료비", "수량": "8 ÷ Q_시공 × P_불도저 × r_주연료^표 ÷ 100", "단가": "P_자동차용경유", "출처": "건설품셈 공통 3-11-3 주연료 할증", "묶음": "재료"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -1998,14 +1998,14 @@ {"열쇠": "시공량", "기호": "Q", "아래": "시공", "위": "", "이름": "시공량", "단위": "", "연결": {"갈래": "표", "표": "QC000263", "조건": {"공종": "S_공종"}, "칸": "시공량"}}, {"열쇠": "연료", "기호": "S", "아래": "연료", "위": "", "이름": "연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ000002", "칸": "주연료종류"}}, {"열쇠": "P_기계시간당", "기호": "P", "아래": "기계시간당", "위": "", "이름": "기계 시간당 기계경비(손료 보정 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000268", "입력": {"기계": "'0101-0010'", "지역": "S_지역"}}}, - {"열쇠": "P_EQ0000", "기호": "P", "아래": "EQ0000", "위": "", "이름": "EQ000002.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ000002", "칸": "주연료"}}, + {"열쇠": "P_불도저", "기호": "P", "아래": "불도저", "위": "", "이름": "EQ000002.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ000002", "칸": "주연료"}}, {"열쇠": "r_주연료^표", "기호": "r", "아래": "주연료", "위": "표", "이름": "머신 컨트롤(MC) 불도저 계수 주연료 할증", "단위": "구분 따름", "연결": {"갈래": "표", "표": "QC000269", "조건": {"구분": "'주연료 할증'"}, "칸": "값"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:{S_연료}:{S_지역}"}} ], "식": [], "금액": [ {"이름": "MC 불도저 10ton", "단위": "시간", "비목": "묶음", "수량": "8 ÷ Q_시공", "단가": "P_기계시간당", "묶음": "MC 불도저 10ton"}, - {"이름": "주연료 할증", "단위": "ℓ", "비목": "재료비", "수량": "8 ÷ Q_시공 × P_EQ0000 × r_주연료^표 ÷ 100", "단가": "P_자동차용경유", "출처": "건설품셈 공통 3-11-4 주연료 할증", "묶음": "재료"} + {"이름": "주연료 할증", "단위": "ℓ", "비목": "재료비", "수량": "8 ÷ Q_시공 × P_불도저 × r_주연료^표 ÷ 100", "단가": "P_자동차용경유", "출처": "건설품셈 공통 3-11-4 주연료 할증", "묶음": "재료"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -2035,7 +2035,7 @@ {"열쇠": "P_굴착기무한궤", "기호": "P", "아래": "굴착기무한궤", "위": "", "이름": "굴착기(무한궤도) 시간당 기계경비", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000267", "입력": {"기계": "'0201-0060'", "지역": "S_지역", "보정작업": "S_보정작업"}}}, {"열쇠": "Q_물탱크살수차^표", "기호": "Q", "아래": "물탱크살수차", "위": "표", "이름": "지능형 다짐공 본시공 작업능력.물탱크_살수차", "단위": "대", "연결": {"갈래": "표", "표": "QC000265", "조건": {"두께": "t_두께"}, "칸": "물탱크_살수차"}}, {"열쇠": "P_기계시간당3", "기호": "P", "아래": "기계시간당3", "위": "", "이름": "기계 시간당 기계경비(손료 보정 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000268", "입력": {"기계": "'7204-0160'", "지역": "S_지역"}}}, - {"열쇠": "P_EQ0000", "기호": "P", "아래": "EQ0000", "위": "", "이름": "EQ000098.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ000098", "칸": "주연료"}}, + {"열쇠": "P_진동롤러", "기호": "P", "아래": "진동롤러", "위": "", "이름": "EQ000098.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ000098", "칸": "주연료"}}, {"열쇠": "r_주연료^표", "기호": "r", "아래": "주연료", "위": "표", "이름": "지능형 다짐 롤러 계수 주연료 할증", "단위": "구분 따름", "연결": {"갈래": "표", "표": "QC000270", "조건": {"구분": "'주연료 할증'"}, "칸": "값"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}} ], @@ -2046,7 +2046,7 @@ {"이름": "IC 진동롤러 10ton", "단위": "시간", "비목": "묶음", "수량": "IC_진동롤러자주^표 × 8 ÷ Q_시공", "단가": "P_기계시간당2", "묶음": "IC 진동롤러 10ton"}, {"이름": "굴착기 0.6㎥", "단위": "시간", "비목": "묶음", "수량": "Q_굴착기^표 × 8 ÷ Q_시공", "단가": "P_굴착기무한궤", "묶음": "굴착기 0.6㎥"}, {"이름": "물탱크(살수차) 16,000ℓ", "단위": "시간", "비목": "묶음", "수량": "Q_물탱크살수차^표 × 8 ÷ Q_시공", "단가": "P_기계시간당3", "묶음": "물탱크(살수차) 16,000ℓ"}, - {"이름": "IC 롤러 주연료 할증", "단위": "ℓ", "비목": "재료비", "수량": "IC_진동롤러자주^표 × 8 ÷ Q_시공 × P_EQ0000 × r_주연료^표 ÷ 100", "단가": "P_자동차용경유", "출처": "건설품셈 공통 3-11-5 주⑦", "묶음": "재료"} + {"이름": "IC 롤러 주연료 할증", "단위": "ℓ", "비목": "재료비", "수량": "IC_진동롤러자주^표 × 8 ÷ Q_시공 × P_진동롤러 × r_주연료^표 ÷ 100", "단가": "P_자동차용경유", "출처": "건설품셈 공통 3-11-5 주⑦", "묶음": "재료"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, diff --git a/resources/master_data/수식문서/수식_건설품셈_09장_미장공사.json b/resources/master_data/수식문서/수식_건설품셈_09장_미장공사.json index 4167369a2..89f26abe4 100644 --- a/resources/master_data/수식문서/수식_건설품셈_09장_미장공사.json +++ b/resources/master_data/수식문서/수식_건설품셈_09장_미장공사.json @@ -92,7 +92,7 @@ {"열쇠": "P_기계시간당", "기호": "P", "아래": "기계시간당", "위": "", "이름": "기계 시간당 손료(연료·조종원 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000273", "입력": {"기계": "'7995-0050'"}}}, {"열쇠": "P_기계시간당2", "기호": "P", "아래": "기계시간당2", "위": "", "이름": "기계 시간당 손료(연료·조종원 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000273", "입력": {"기계": "'7991-0500'"}}}, {"열쇠": "P_기계시간당3", "기호": "P", "아래": "기계시간당3", "위": "", "이름": "기계 시간당 손료(연료·조종원 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000273", "입력": {"기계": "'7992-0001'"}}}, - {"열쇠": "P_EQ0005", "기호": "P", "아래": "EQ0005", "위": "", "이름": "EQ000532.주연료2", "단위": "", "연결": {"갈래": "요소", "키": "EQ000532", "칸": "주연료2"}}, + {"열쇠": "P_믹서", "기호": "P", "아래": "믹서", "위": "", "이름": "EQ000532.주연료2", "단위": "", "연결": {"갈래": "요소", "키": "EQ000532", "칸": "주연료2"}}, {"열쇠": "P_휘발유", "기호": "P", "아래": "휘발유", "위": "", "이름": "휘발유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:휘발유:{S_지역}"}}, {"열쇠": "P_기계시간당4", "기호": "P", "아래": "기계시간당4", "위": "", "이름": "기계 시간당 손료(연료·조종원 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000273", "입력": {"기계": "'7993-0020'"}}} ], @@ -108,7 +108,7 @@ {"이름": "배관파이프 ø50-2.6m", "단위": "시간", "비목": "묶음", "수량": "Q_모르타르^표 × 8 ÷ Q_시공", "단가": "P_기계시간당", "묶음": "배관파이프 ø50-2.6m"}, {"이름": "모르타르 펌프 37㎾", "단위": "시간", "비목": "묶음", "수량": "Q_모르타르^표 × 8 ÷ Q_시공", "단가": "P_기계시간당2", "출처": "건설품셈 건축 9-1-3 주③", "묶음": "모르타르 펌프 37㎾"}, {"이름": "모르타르 믹서 0.3㎥", "단위": "시간", "비목": "묶음", "수량": "Q_모르타르^표 × 8 ÷ Q_시공", "단가": "P_기계시간당3", "출처": "건설품셈 건축 9-1-3 주③", "묶음": "모르타르 믹서 0.3㎥"}, - {"이름": "모르타르 믹서 연료(휘발유)", "단위": "ℓ", "비목": "재료비", "수량": "P_EQ0005 × Q_모르타르^표 × 8 ÷ Q_시공", "단가": "P_휘발유", "출처": "건설품셈 공통 8-4-8 「1.87㎾ 휘발유1.3」", "묶음": "재료"}, + {"이름": "모르타르 믹서 연료(휘발유)", "단위": "ℓ", "비목": "재료비", "수량": "P_믹서 × Q_모르타르^표 × 8 ÷ Q_시공", "단가": "P_휘발유", "출처": "건설품셈 공통 8-4-8 「1.87㎾ 휘발유1.3」", "묶음": "재료"}, {"이름": "양수기 1.49㎾", "단위": "시간", "비목": "묶음", "수량": "Q_모르타르^표 × 8 ÷ Q_시공", "단가": "P_기계시간당4", "출처": "건설품셈 건축 9-1-3 주③", "묶음": "양수기 1.49㎾"} ], "끝수": { diff --git a/resources/master_data/수식문서/수식_건설품셈_13장_플랜트설비공사.json b/resources/master_data/수식문서/수식_건설품셈_13장_플랜트설비공사.json index 47355c9f8..4d2b48f66 100644 --- a/resources/master_data/수식문서/수식_건설품셈_13장_플랜트설비공사.json +++ b/resources/master_data/수식문서/수식_건설품셈_13장_플랜트설비공사.json @@ -3375,7 +3375,7 @@ {"열쇠": "w_인력운반공", "기호": "w", "아래": "인력운반공", "위": "", "이름": "인력운반공노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000045"}}, {"열쇠": "w_측량기사", "기호": "w", "아래": "측량기사", "위": "", "이름": "측량기사노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000396"}}, {"열쇠": "w_플랜트제관공", "기호": "w", "아래": "플랜트제관공", "위": "", "이름": "플랜트제관공노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000057"}}, - {"열쇠": "P_LB0003", "기호": "P", "아래": "LB0003", "위": "", "이름": "LB000397", "단위": "", "연결": {"갈래": "요소", "키": "LB000397"}}, + {"열쇠": "P_시험사1급", "기호": "P", "아래": "시험사1급", "위": "", "이름": "LB000397", "단위": "", "연결": {"갈래": "요소", "키": "LB000397"}}, {"열쇠": "w_특수운전공", "기호": "w", "아래": "특수운전공", "위": "", "이름": "특수운전공노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000398"}}, {"열쇠": "w_도장공", "기호": "w", "아래": "도장공", "위": "", "이름": "도장공노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000029"}}, {"열쇠": "w_플랜트용접공", "기호": "w", "아래": "플랜트용접공", "위": "", "이름": "플랜트용접공노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000058"}}, @@ -3420,7 +3420,7 @@ {"이름": "인력운반공", "단위": "인", "비목": "노무비", "수량": "Q_인력운반공", "단가": "w_인력운반공"}, {"이름": "측량기사", "단위": "인", "비목": "노무비", "수량": "Q_측량기사", "단가": "w_측량기사"}, {"이름": "플랜트제관공", "단위": "인", "비목": "노무비", "수량": "Q_플랜트제관공", "단가": "w_플랜트제관공"}, - {"이름": "시험사1급", "단위": "인", "비목": "노무비", "수량": "Q_시험사1급", "단가": "P_LB0003"}, + {"이름": "시험사1급", "단위": "인", "비목": "노무비", "수량": "Q_시험사1급", "단가": "P_시험사1급"}, {"이름": "특수운전공", "단위": "인", "비목": "노무비", "수량": "Q_특수운전공", "단가": "w_특수운전공"}, {"이름": "도장공", "단위": "인", "비목": "노무비", "수량": "Q_도장공", "단가": "w_도장공"}, {"이름": "플랜트용접공", "단위": "인", "비목": "노무비", "수량": "Q_플랜트용접공", "단가": "w_플랜트용접공"}, @@ -3469,7 +3469,7 @@ {"열쇠": "w_마킹공", "기호": "w", "아래": "마킹공", "위": "", "이름": "마킹공노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000394"}}, {"열쇠": "w_도장공", "기호": "w", "아래": "도장공", "위": "", "이름": "도장공노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000029"}}, {"열쇠": "w_플랜트전공", "기호": "w", "아래": "플랜트전공", "위": "", "이름": "플랜트전공노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000083"}}, - {"열쇠": "P_LB0003", "기호": "P", "아래": "LB0003", "위": "", "이름": "LB000397", "단위": "", "연결": {"갈래": "요소", "키": "LB000397"}}, + {"열쇠": "P_시험사1급", "기호": "P", "아래": "시험사1급", "위": "", "이름": "LB000397", "단위": "", "연결": {"갈래": "요소", "키": "LB000397"}}, {"열쇠": "w_특별인부", "기호": "w", "아래": "특별인부", "위": "", "이름": "특별인부노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000003"}}, {"열쇠": "P_산소", "기호": "P", "아래": "산소", "위": "", "이름": "산소단가", "단위": "", "연결": {"갈래": "요소", "재료": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "산소 6000ℓ/병"}, "단위": "L"}}, {"열쇠": "P_아세틸렌", "기호": "P", "아래": "아세틸렌", "위": "", "이름": "아세틸렌단가", "단위": "", "연결": {"갈래": "요소", "재료": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98% (1kg=853ℓ)"}, "단위": "L"}}, @@ -3508,7 +3508,7 @@ {"이름": "마킹공", "단위": "인", "비목": "노무비", "수량": "Q_마킹공", "단가": "w_마킹공"}, {"이름": "도장공", "단위": "인", "비목": "노무비", "수량": "Q_도장공", "단가": "w_도장공"}, {"이름": "플랜트전공", "단위": "인", "비목": "노무비", "수량": "Q_플랜트전공", "단가": "w_플랜트전공"}, - {"이름": "시험사1급", "단위": "인", "비목": "노무비", "수량": "Q_시험사1급", "단가": "P_LB0003"}, + {"이름": "시험사1급", "단위": "인", "비목": "노무비", "수량": "Q_시험사1급", "단가": "P_시험사1급"}, {"이름": "특별인부", "단위": "인", "비목": "노무비", "수량": "Q_특별인부", "단가": "w_특별인부"}, {"이름": "산소", "단위": "L", "비목": "재료비", "수량": "1.35 × 6000", "단가": "P_산소", "출처": "건설품셈 기계설비 13-6-9"}, {"이름": "아세틸렌", "단위": "L", "비목": "재료비", "수량": "0.57 × 2500", "단가": "P_아세틸렌", "출처": "건설품셈 기계설비 13-6-9"}, @@ -3559,7 +3559,7 @@ {"열쇠": "w_산소절단공", "기호": "w", "아래": "산소절단공", "위": "", "이름": "산소절단공노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000395"}}, {"열쇠": "w_도장공", "기호": "w", "아래": "도장공", "위": "", "이름": "도장공노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000029"}}, {"열쇠": "w_플랜트전공", "기호": "w", "아래": "플랜트전공", "위": "", "이름": "플랜트전공노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000083"}}, - {"열쇠": "P_LB0003", "기호": "P", "아래": "LB0003", "위": "", "이름": "LB000397", "단위": "", "연결": {"갈래": "요소", "키": "LB000397"}}, + {"열쇠": "P_시험사1급", "기호": "P", "아래": "시험사1급", "위": "", "이름": "LB000397", "단위": "", "연결": {"갈래": "요소", "키": "LB000397"}}, {"열쇠": "P_용접봉", "기호": "P", "아래": "용접봉", "위": "", "이름": "용접봉단가", "단위": "", "연결": {"갈래": "요소", "재료": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "용접봉 산소 KS E4301"}, "단위": "kg"}}, {"열쇠": "P_탄소봉", "기호": "P", "아래": "탄소봉", "위": "", "이름": "탄소봉단가", "단위": "", "연결": {"갈래": "요소", "재료": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "탄소봉 8ø×350㎜"}, "단위": "본"}}, {"열쇠": "P_산소", "기호": "P", "아래": "산소", "위": "", "이름": "산소단가", "단위": "", "연결": {"갈래": "요소", "재료": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "산소 6000ℓ/병"}, "단위": "L"}}, @@ -3595,7 +3595,7 @@ {"이름": "산소절단공", "단위": "인", "비목": "노무비", "수량": "Q_산소절단공", "단가": "w_산소절단공"}, {"이름": "도장공", "단위": "인", "비목": "노무비", "수량": "Q_도장공", "단가": "w_도장공"}, {"이름": "플랜트전공", "단위": "인", "비목": "노무비", "수량": "Q_플랜트전공", "단가": "w_플랜트전공"}, - {"이름": "시험사1급", "단위": "인", "비목": "노무비", "수량": "Q_시험사1급", "단가": "P_LB0003"}, + {"이름": "시험사1급", "단위": "인", "비목": "노무비", "수량": "Q_시험사1급", "단가": "P_시험사1급"}, {"이름": "용접봉", "단위": "kg", "비목": "재료비", "수량": "9.81", "단가": "P_용접봉", "출처": "건설품셈 기계설비 13-6-10"}, {"이름": "탄소봉", "단위": "본", "비목": "재료비", "수량": "3.53", "단가": "P_탄소봉", "출처": "건설품셈 기계설비 13-6-10"}, {"이름": "산소", "단위": "L", "비목": "재료비", "수량": "0.55 × 6000", "단가": "P_산소", "출처": "건설품셈 기계설비 13-6-10"}, diff --git a/resources/master_data/수식문서/수식_산림품셈_03장_작업장_관리.json b/resources/master_data/수식문서/수식_산림품셈_03장_작업장_관리.json index 64b2c919c..2e14682f0 100644 --- a/resources/master_data/수식문서/수식_산림품셈_03장_작업장_관리.json +++ b/resources/master_data/수식문서/수식_산림품셈_03장_작업장_관리.json @@ -41,7 +41,7 @@ {"열쇠": "P_휘발유", "기호": "P", "아래": "휘발유", "위": "", "이름": "휘발유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:휘발유:{S_지역}"}}, {"열쇠": "Q_주연료2^표", "기호": "Q", "아래": "주연료2", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.주연료", "단위": "수(단위는 줄의 단위 칸 따름)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'체인톱'", "재료명": "'체인오일(일반오일)'"}, "칸": "주연료"}}, {"열쇠": "P_체인오일", "기호": "P", "아래": "체인오일", "위": "", "이름": "체인오일단가", "단위": "", "연결": {"갈래": "요소", "재료": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "작동유"}, "단위": "ℓ"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000620.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000620", "칸": "손료계수"}}, + {"열쇠": "k_체인톱", "기호": "k", "아래": "체인톱", "위": "", "이름": "EQ000620.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000620", "칸": "손료계수"}}, {"열쇠": "P_체인톱손료", "기호": "P", "아래": "체인톱손료", "위": "", "이름": "체인톱 손료단가", "단위": "", "연결": {"갈래": "요소", "재료": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "동력체인톱"}, "단위": "대"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'체인톱'", "재료명": "'보통휘발유(주연료)'"}, "칸": "잡품"}} ], @@ -53,7 +53,7 @@ {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "Q_소요 × 0.5", "단가": "w_보통인부", "출처": "산림품셈 3-3 인력구성 「보통인부 50%」"}, {"이름": "휘발유(체인톱)", "단위": "ℓ", "비목": "재료비", "수량": "N_체인톱 × Q_주연료^표", "단가": "P_휘발유", "출처": "산림품셈 2-1-1 주③", "기호": "C_휘발유"}, {"이름": "체인오일", "단위": "ℓ", "비목": "재료비", "수량": "N_체인톱 × Q_주연료2^표", "단가": "P_체인오일", "출처": "산림품셈 2-1-1 주④"}, - {"이름": "체인톱 손료", "단위": "대", "비목": "경비", "수량": "N_체인톱 × k_EQ0006", "단가": "P_체인톱손료", "출처": "산림품셈 2-2-1"}, + {"이름": "체인톱 손료", "단위": "대", "비목": "경비", "수량": "N_체인톱 × k_체인톱", "단가": "P_체인톱손료", "출처": "산림품셈 2-2-1"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "C_휘발유 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-1 주⑤"} ], "끝수": { diff --git a/resources/master_data/수식문서/수식_산림품셈_04장_나무베기.json b/resources/master_data/수식문서/수식_산림품셈_04장_나무베기.json index e26bc85ef..993d6d945 100644 --- a/resources/master_data/수식문서/수식_산림품셈_04장_나무베기.json +++ b/resources/master_data/수식문서/수식_산림품셈_04장_나무베기.json @@ -47,17 +47,17 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "경유 사용 임업기계 연료.주연료", "단위": "ℓ/일,대", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'하베스터(헤드부착형)'"}, "칸": "주연료"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "경유 사용 임업기계 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'하베스터(헤드부착형)'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000634.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000634", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000634.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000634", "칸": "장비가격"}}, - {"열쇠": "k_EQ0002", "기호": "k", "아래": "EQ0002", "위": "", "이름": "EQ000635.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000635", "칸": "손료계수"}}, - {"열쇠": "P_EQ0002", "기호": "P", "아래": "EQ0002", "위": "", "이름": "EQ000635.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000635", "칸": "장비가격"}} + {"열쇠": "k_하베스터", "기호": "k", "아래": "하베스터", "위": "", "이름": "EQ000634.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000634", "칸": "손료계수"}}, + {"열쇠": "P_하베스터", "기호": "P", "아래": "하베스터", "위": "", "이름": "EQ000634.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000634", "칸": "장비가격"}}, + {"열쇠": "k_굴착기", "기호": "k", "아래": "굴착기", "위": "", "이름": "EQ000635.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000635", "칸": "손료계수"}}, + {"열쇠": "P_굴착기", "기호": "P", "아래": "굴착기", "위": "", "이름": "EQ000635.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000635", "칸": "장비가격"}} ], "식": [], "금액": [ {"이름": "건설기계운전사", "단위": "인", "비목": "노무비", "수량": "1 ÷ Q_공정", "단가": "w_기계운전사", "출처": "산림품셈 4-1-2 「하베스터 오퍼레이터 1인」"}, {"이름": "경유(하베스터)", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 ÷ Q_공정", "단가": "P_자동차용경유", "출처": "산림품셈 2-1-2"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-2 주④"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "(k_EQ0006 × P_EQ0006 + k_EQ0002 × P_EQ0002) × 1000 ÷ Q_공정", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "(k_하베스터 × P_하베스터 + k_굴착기 × P_굴착기) × 1000 ÷ Q_공정", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -79,17 +79,17 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "경유 사용 임업기계 연료.주연료", "단위": "ℓ/일,대", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'하베스터(헤드부착형)'"}, "칸": "주연료"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "경유 사용 임업기계 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'하베스터(헤드부착형)'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000634.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000634", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000634.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000634", "칸": "장비가격"}}, - {"열쇠": "k_EQ0002", "기호": "k", "아래": "EQ0002", "위": "", "이름": "EQ000635.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000635", "칸": "손료계수"}}, - {"열쇠": "P_EQ0002", "기호": "P", "아래": "EQ0002", "위": "", "이름": "EQ000635.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000635", "칸": "장비가격"}} + {"열쇠": "k_하베스터", "기호": "k", "아래": "하베스터", "위": "", "이름": "EQ000634.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000634", "칸": "손료계수"}}, + {"열쇠": "P_하베스터", "기호": "P", "아래": "하베스터", "위": "", "이름": "EQ000634.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000634", "칸": "장비가격"}}, + {"열쇠": "k_굴착기", "기호": "k", "아래": "굴착기", "위": "", "이름": "EQ000635.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000635", "칸": "손료계수"}}, + {"열쇠": "P_굴착기", "기호": "P", "아래": "굴착기", "위": "", "이름": "EQ000635.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000635", "칸": "장비가격"}} ], "식": [], "금액": [ {"이름": "건설기계운전사", "단위": "인", "비목": "노무비", "수량": "1 ÷ Q_공정", "단가": "w_기계운전사", "출처": "산림품셈 4-1-2 「하베스터 오퍼레이터 1인」"}, {"이름": "경유(하베스터)", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 ÷ Q_공정", "단가": "P_자동차용경유", "출처": "산림품셈 2-1-2"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-2 주④"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "(k_EQ0006 × P_EQ0006 + k_EQ0002 × P_EQ0002) × 1000 ÷ Q_공정", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "(k_하베스터 × P_하베스터 + k_굴착기 × P_굴착기) × 1000 ÷ Q_공정", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, diff --git a/resources/master_data/수식문서/수식_산림품셈_06장_생육보조.json b/resources/master_data/수식문서/수식_산림품셈_06장_생육보조.json index d24ffc444..834ac1643 100644 --- a/resources/master_data/수식문서/수식_산림품셈_06장_생육보조.json +++ b/resources/master_data/수식문서/수식_산림품셈_06장_생육보조.json @@ -73,7 +73,7 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.주연료", "단위": "수(단위는 줄의 단위 칸 따름)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'예취기(휘발유)'"}, "칸": "주연료"}}, {"열쇠": "P_휘발유", "기호": "P", "아래": "휘발유", "위": "", "이름": "휘발유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:휘발유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'예취기(휘발유)'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000621.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000621", "칸": "손료계수"}} + {"열쇠": "k_예취기", "기호": "k", "아래": "예취기", "위": "", "이름": "EQ000621.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000621", "칸": "손료계수"}} ], "식": [], "금액": [ @@ -81,7 +81,7 @@ {"이름": "특별인부", "단위": "인", "비목": "노무비", "수량": "Q_특별", "단가": "w_특별인부", "출처": "산림품셈 6-2-2 「줄베기」"}, {"이름": "휘발유(예취기)", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × Q_특별", "단가": "P_휘발유", "출처": "산림품셈 2-1-1"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-1 주「잡품」"}, - {"이름": "예취기 손료", "단위": "식", "비목": "경비", "단가": "Q_특별 × k_EQ0006 × P_예취기", "출처": "산림품셈 2-2-2"} + {"이름": "예취기 손료", "단위": "식", "비목": "경비", "단가": "Q_특별 × k_예취기 × P_예취기", "출처": "산림품셈 2-2-2"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -108,7 +108,7 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.주연료", "단위": "수(단위는 줄의 단위 칸 따름)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'예취기(휘발유)'"}, "칸": "주연료"}}, {"열쇠": "P_휘발유", "기호": "P", "아래": "휘발유", "위": "", "이름": "휘발유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:휘발유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'예취기(휘발유)'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000621.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000621", "칸": "손료계수"}} + {"열쇠": "k_예취기", "기호": "k", "아래": "예취기", "위": "", "이름": "EQ000621.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000621", "칸": "손료계수"}} ], "식": [], "금액": [ @@ -116,7 +116,7 @@ {"이름": "특별인부", "단위": "인", "비목": "노무비", "수량": "Q_특별", "단가": "w_특별인부", "출처": "산림품셈 6-2-3 「모두베기」"}, {"이름": "휘발유(예취기)", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × Q_특별", "단가": "P_휘발유", "출처": "산림품셈 2-1-1"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-1 주「잡품」"}, - {"이름": "예취기 손료", "단위": "식", "비목": "경비", "단가": "Q_특별 × k_EQ0006 × P_예취기", "출처": "산림품셈 2-2-2"} + {"이름": "예취기 손료", "단위": "식", "비목": "경비", "단가": "Q_특별 × k_예취기 × P_예취기", "출처": "산림품셈 2-2-2"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -160,14 +160,14 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.주연료", "단위": "수(단위는 줄의 단위 칸 따름)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'예취기(휘발유)'"}, "칸": "주연료"}}, {"열쇠": "P_휘발유", "기호": "P", "아래": "휘발유", "위": "", "이름": "휘발유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:휘발유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'예취기(휘발유)'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000621.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000621", "칸": "손료계수"}} + {"열쇠": "k_예취기", "기호": "k", "아래": "예취기", "위": "", "이름": "EQ000621.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000621", "칸": "손료계수"}} ], "식": [], "금액": [ {"이름": "특별인부", "단위": "인", "비목": "노무비", "수량": "Q_특별", "단가": "w_특별인부"}, {"이름": "휘발유(예취기)", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × Q_특별", "단가": "P_휘발유", "출처": "산림품셈 2-1-1"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-1 주「잡품」"}, - {"이름": "예취기 손료", "단위": "식", "비목": "경비", "단가": "Q_특별 × k_EQ0006 × P_예취기", "출처": "산림품셈 2-2-2"} + {"이름": "예취기 손료", "단위": "식", "비목": "경비", "단가": "Q_특별 × k_예취기 × P_예취기", "출처": "산림품셈 2-2-2"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -194,7 +194,7 @@ {"열쇠": "Q_주재료^표", "기호": "Q", "아래": "주재료", "위": "표", "이름": "덩굴 약제처리(ha당).주재료", "단위": "병", "연결": {"갈래": "표", "표": "QF000007", "조건": {}, "칸": "주재료"}}, {"열쇠": "P_농약", "기호": "P", "아래": "농약", "위": "", "이름": "농약(Fluroxypyr-meptyl+Triclypyr-TEA 미탁제)단가", "단위": "", "연결": {"갈래": "요소", "재료": {"구분": "식생·종자", "상세구분": "농약·약제", "검색어": "푸른솔(분산성액제) 수간주사(10㎖)"}, "단위": "병"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "덩굴 약제처리(ha당).잡품", "단위": "%(주재료비의)", "연결": {"갈래": "표", "표": "QF000007", "조건": {}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000642.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000642", "칸": "손료계수"}} + {"열쇠": "k_배부식분무기", "기호": "k", "아래": "배부식분무기", "위": "", "이름": "EQ000642.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000642", "칸": "손료계수"}} ], "식": [ {"기호": "r_증감", "식": "만약(S_피복도 == '해당 없음', 0, r_증^표)"} @@ -204,7 +204,7 @@ {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "Q_소요인력2^표", "단가": "w_보통인부", "출처": "산림품셈 6-4-2 「작업보조」"}, {"이름": "농약(Fluroxypyr-meptyl+Triclypyr-TEA 미탁제)", "단위": "병", "비목": "재료비", "수량": "Q_주재료^표 × (1 + r_증감 ÷ 100)", "단가": "P_농약", "출처": "산림품셈 2-1-5-1 · 주②"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-5-1 주③"}, - {"이름": "분무기 손료", "단위": "식", "비목": "경비", "단가": "Q_특별 × k_EQ0006 × P_분무기", "출처": "산림품셈 2-2-4"} + {"이름": "분무기 손료", "단위": "식", "비목": "경비", "단가": "Q_특별 × k_배부식분무기 × P_분무기", "출처": "산림품셈 2-2-4"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -286,7 +286,7 @@ {"열쇠": "P_휘발유", "기호": "P", "아래": "휘발유", "위": "", "이름": "휘발유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:휘발유:{S_지역}"}}, {"열쇠": "Q_주연료2^표", "기호": "Q", "아래": "주연료2", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.주연료", "단위": "수(단위는 줄의 단위 칸 따름)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'체인톱'", "재료명": "'체인오일(일반오일)'"}, "칸": "주연료"}}, {"열쇠": "P_체인오일", "기호": "P", "아래": "체인오일", "위": "", "이름": "체인오일단가", "단위": "", "연결": {"갈래": "요소", "재료": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "작동유"}, "단위": "ℓ"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000620.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000620", "칸": "손료계수"}}, + {"열쇠": "k_체인톱", "기호": "k", "아래": "체인톱", "위": "", "이름": "EQ000620.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000620", "칸": "손료계수"}}, {"열쇠": "P_체인톱손료", "기호": "P", "아래": "체인톱손료", "위": "", "이름": "체인톱 손료단가", "단위": "", "연결": {"갈래": "요소", "재료": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "동력체인톱"}, "단위": "대"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'체인톱'", "재료명": "'보통휘발유(주연료)'"}, "칸": "잡품"}} ], @@ -299,7 +299,7 @@ {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "Q_소요인력2^표 + Q_작업로인력 × 0.5", "단가": "w_보통인부", "출처": "산림품셈 6-5 「보통인부(작업 보조)」 · 소작업로 50%"}, {"이름": "휘발유(체인톱)", "단위": "ℓ", "비목": "재료비", "수량": "Q_특별 × Q_주연료^표", "단가": "P_휘발유", "출처": "산림품셈 2-1-1 주③", "기호": "C_휘발유"}, {"이름": "체인오일", "단위": "ℓ", "비목": "재료비", "수량": "Q_특별 × Q_주연료2^표", "단가": "P_체인오일", "출처": "산림품셈 2-1-1 주④"}, - {"이름": "체인톱 손료", "단위": "대", "비목": "경비", "수량": "Q_특별 × k_EQ0006", "단가": "P_체인톱손료", "출처": "산림품셈 2-2-1"}, + {"이름": "체인톱 손료", "단위": "대", "비목": "경비", "수량": "Q_특별 × k_체인톱", "단가": "P_체인톱손료", "출처": "산림품셈 2-2-1"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "C_휘발유 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-1 주⑤"} ], "끝수": { diff --git a/resources/master_data/수식문서/수식_산림품셈_07장_집재_및_운재.json b/resources/master_data/수식문서/수식_산림품셈_07장_집재_및_운재.json index ed2a51a0d..b9485d569 100644 --- a/resources/master_data/수식문서/수식_산림품셈_07장_집재_및_운재.json +++ b/resources/master_data/수식문서/수식_산림품셈_07장_집재_및_운재.json @@ -79,8 +79,8 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.주연료", "단위": "수(단위는 줄의 단위 칸 따름)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'아키아윈치(휘발유)'"}, "칸": "주연료"}}, {"열쇠": "P_휘발유", "기호": "P", "아래": "휘발유", "위": "", "이름": "휘발유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:휘발유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'아키아윈치(휘발유)'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000622.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000622", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000622.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000622", "칸": "장비가격"}} + {"열쇠": "k_아키아윈치", "기호": "k", "아래": "아키아윈치", "위": "", "이름": "EQ000622.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000622", "칸": "손료계수"}}, + {"열쇠": "P_아키아윈치", "기호": "P", "아래": "아키아윈치", "위": "", "이름": "EQ000622.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000622", "칸": "장비가격"}} ], "식": [ {"기호": "d_일수", "식": "1 ÷ Q"} @@ -90,7 +90,7 @@ {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "d_일수", "단가": "w_보통인부"}, {"이름": "휘발유(아키아윈치(휘발유))", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × d_일수", "단가": "P_휘발유", "출처": "산림품셈 2-1-1"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-1 주「잡품」"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 × P_EQ0006 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_아키아윈치 × P_아키아윈치 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -119,8 +119,8 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.주연료", "단위": "수(단위는 줄의 단위 칸 따름)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'2드럼 케이블윈치(휘발유)'"}, "칸": "주연료"}}, {"열쇠": "P_휘발유", "기호": "P", "아래": "휘발유", "위": "", "이름": "휘발유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:휘발유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'2드럼 케이블윈치(휘발유)'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000623.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000623", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000623.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000623", "칸": "장비가격"}} + {"열쇠": "k_케이블윈치", "기호": "k", "아래": "케이블윈치", "위": "", "이름": "EQ000623.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000623", "칸": "손료계수"}}, + {"열쇠": "P_케이블윈치", "기호": "P", "아래": "케이블윈치", "위": "", "이름": "EQ000623.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000623", "칸": "장비가격"}} ], "식": [ {"기호": "d_일수", "식": "1 ÷ Q × (1 + r_할증 ÷ 100)"} @@ -131,7 +131,7 @@ {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "d_일수", "단가": "w_보통인부"}, {"이름": "휘발유(2드럼 케이블윈치(휘발유))", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × d_일수", "단가": "P_휘발유", "출처": "산림품셈 2-1-1"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-1 주「잡품」"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 × P_EQ0006 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_케이블윈치 × P_케이블윈치 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -162,8 +162,8 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.주연료", "단위": "수(단위는 줄의 단위 칸 따름)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'2드럼 케이블윈치(휘발유)'"}, "칸": "주연료"}}, {"열쇠": "P_휘발유", "기호": "P", "아래": "휘발유", "위": "", "이름": "휘발유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:휘발유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'2드럼 케이블윈치(휘발유)'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000623.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000623", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000623.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000623", "칸": "장비가격"}} + {"열쇠": "k_케이블윈치", "기호": "k", "아래": "케이블윈치", "위": "", "이름": "EQ000623.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000623", "칸": "손료계수"}}, + {"열쇠": "P_케이블윈치", "기호": "P", "아래": "케이블윈치", "위": "", "이름": "EQ000623.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000623", "칸": "장비가격"}} ], "식": [ {"기호": "Q", "식": "Q_수집^표 × (1 + 만약(S_작업 == '숲가꾸기', 0, r_방제산물^표) ÷ 100)"}, @@ -175,7 +175,7 @@ {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "2 × d_일수", "단가": "w_보통인부"}, {"이름": "휘발유(2드럼 케이블윈치(휘발유))", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × d_일수", "단가": "P_휘발유", "출처": "산림품셈 2-1-1"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-1 주「잡품」"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 × P_EQ0006 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_케이블윈치 × P_케이블윈치 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -202,8 +202,8 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "경유 사용 임업기계 연료.주연료", "단위": "ℓ/일,대", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'임업용동력집재기'"}, "칸": "주연료"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "경유 사용 임업기계 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'임업용동력집재기'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000632.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000632", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000632.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000632", "칸": "장비가격"}} + {"열쇠": "k_동력집재기", "기호": "k", "아래": "동력집재기", "위": "", "이름": "EQ000632.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000632", "칸": "손료계수"}}, + {"열쇠": "P_동력집재기", "기호": "P", "아래": "동력집재기", "위": "", "이름": "EQ000632.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000632", "칸": "장비가격"}} ], "식": [ {"기호": "d_일수", "식": "1 ÷ Q"} @@ -214,7 +214,7 @@ {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "d_일수", "단가": "w_보통인부"}, {"이름": "경유(임업용동력집재기)", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × d_일수", "단가": "P_자동차용경유", "출처": "산림품셈 2-1-2"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-2 주「잡품」"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 × P_EQ0006 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_동력집재기 × P_동력집재기 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -243,8 +243,8 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "경유 사용 임업기계 연료.주연료", "단위": "ℓ/일,대", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'임업용동력집재기'"}, "칸": "주연료"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "경유 사용 임업기계 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'임업용동력집재기'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000632.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000632", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000632.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000632", "칸": "장비가격"}} + {"열쇠": "k_동력집재기", "기호": "k", "아래": "동력집재기", "위": "", "이름": "EQ000632.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000632", "칸": "손료계수"}}, + {"열쇠": "P_동력집재기", "기호": "P", "아래": "동력집재기", "위": "", "이름": "EQ000632.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000632", "칸": "장비가격"}} ], "식": [ {"기호": "Q", "식": "Q_수집^표 × (1 + 만약(S_작업 == '소나무재선충병방제', 2, 0) ÷ 100)"}, @@ -256,7 +256,7 @@ {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "d_일수", "단가": "w_보통인부"}, {"이름": "경유(임업용동력집재기)", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × d_일수", "단가": "P_자동차용경유", "출처": "산림품셈 2-1-2"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-2 주「잡품」"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 × P_EQ0006 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_동력집재기 × P_동력집재기 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -284,8 +284,8 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "경유 사용 임업기계 연료.주연료", "단위": "ℓ/일,대", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'스윙야더'"}, "칸": "주연료"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "경유 사용 임업기계 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'스윙야더'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000638.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000638", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000638.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000638", "칸": "장비가격"}} + {"열쇠": "k_스윙야더", "기호": "k", "아래": "스윙야더", "위": "", "이름": "EQ000638.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000638", "칸": "손료계수"}}, + {"열쇠": "P_스윙야더", "기호": "P", "아래": "스윙야더", "위": "", "이름": "EQ000638.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000638", "칸": "장비가격"}} ], "식": [ {"기호": "d_일수", "식": "1 ÷ Q × (1 + r_할증 ÷ 100)"} @@ -296,7 +296,7 @@ {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "d_일수", "단가": "w_보통인부"}, {"이름": "경유(스윙야더)", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × d_일수", "단가": "P_자동차용경유", "출처": "산림품셈 2-1-2"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-2 주「잡품」"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 × P_EQ0006 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_스윙야더 × P_스윙야더 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -365,8 +365,8 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "경유 사용 임업기계 연료.주연료", "단위": "ℓ/일,대", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'트랙터 부착 기계'"}, "칸": "주연료"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "경유 사용 임업기계 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'트랙터 부착 기계'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000633.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000633", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000633.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000633", "칸": "장비가격"}} + {"열쇠": "k_HAM200", "기호": "k", "아래": "HAM200", "위": "", "이름": "EQ000633.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000633", "칸": "손료계수"}}, + {"열쇠": "P_HAM200", "기호": "P", "아래": "HAM200", "위": "", "이름": "EQ000633.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000633", "칸": "장비가격"}} ], "식": [ {"기호": "Q", "식": "Q_수집^표 × (1 + 만약(S_작업 == '소나무재선충병방제', 27, 0) ÷ 100)"}, @@ -378,7 +378,7 @@ {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "d_일수", "단가": "w_보통인부"}, {"이름": "경유(트랙터 부착 기계)", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × d_일수", "단가": "P_자동차용경유", "출처": "산림품셈 2-1-2"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-2 주「잡품」"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 × P_EQ0006 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_HAM200 × P_HAM200 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -457,8 +457,8 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "경유 사용 임업기계 연료.주연료", "단위": "ℓ/일,대", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'타워야더(RME 300T)'"}, "칸": "주연료"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "경유 사용 임업기계 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'타워야더(RME 300T)'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000637.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000637", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000637.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000637", "칸": "장비가격"}} + {"열쇠": "k_타워야더", "기호": "k", "아래": "타워야더", "위": "", "이름": "EQ000637.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000637", "칸": "손료계수"}}, + {"열쇠": "P_타워야더", "기호": "P", "아래": "타워야더", "위": "", "이름": "EQ000637.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000637", "칸": "장비가격"}} ], "식": [ {"기호": "d_일수", "식": "1 ÷ Q × (1 + r_할증 ÷ 100)"} @@ -469,7 +469,7 @@ {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "d_일수", "단가": "w_보통인부"}, {"이름": "경유(타워야더(RME 300T))", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × d_일수", "단가": "P_자동차용경유", "출처": "산림품셈 2-1-2"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-2 주「잡품」"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 × P_EQ0006 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_타워야더 × P_타워야더 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -502,10 +502,10 @@ {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "경유 사용 임업기계 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'콜라타워야더(K-301)'"}, "칸": "잡품"}}, {"열쇠": "Q_잡2^표", "기호": "Q", "아래": "잡2", "위": "표", "이름": "경유 사용 임업기계 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'HAM300(임업용 트랙터 포함)'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000630.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000630", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000630.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000630", "칸": "장비가격"}}, - {"열쇠": "k_EQ0002", "기호": "k", "아래": "EQ0002", "위": "", "이름": "EQ000629.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000629", "칸": "손료계수"}}, - {"열쇠": "P_EQ0002", "기호": "P", "아래": "EQ0002", "위": "", "이름": "EQ000629.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000629", "칸": "장비가격"}} + {"열쇠": "k_콜라타워야더", "기호": "k", "아래": "콜라타워야더", "위": "", "이름": "EQ000630.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000630", "칸": "손료계수"}}, + {"열쇠": "P_콜라타워야더", "기호": "P", "아래": "콜라타워야더", "위": "", "이름": "EQ000630.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000630", "칸": "장비가격"}}, + {"열쇠": "k_HAM300", "기호": "k", "아래": "HAM300", "위": "", "이름": "EQ000629.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000629", "칸": "손료계수"}}, + {"열쇠": "P_HAM300", "기호": "P", "아래": "HAM300", "위": "", "이름": "EQ000629.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000629", "칸": "장비가격"}} ], "식": [ {"기호": "Q", "식": "Q_0"}, @@ -517,7 +517,7 @@ {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "d_일수", "단가": "w_보통인부"}, {"이름": "경유(부착형 타워)", "단위": "ℓ", "비목": "재료비", "수량": "만약(S_기종 == 'K-301', Q_주연료^표, Q_주연료2^표) × d_일수", "단가": "P_자동차용경유", "출처": "산림품셈 2-1-2"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × 만약(S_기종 == 'K-301', Q_잡^표, Q_잡2^표) ÷ 100", "출처": "산림품셈 2-1-2 주「잡품」"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "만약(S_기종 == 'K-301', k_EQ0006 × P_EQ0006, k_EQ0002 × P_EQ0002) × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "만약(S_기종 == 'K-301', k_콜라타워야더 × P_콜라타워야더, k_HAM300 × P_HAM300) × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -553,10 +553,10 @@ {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "경유 사용 임업기계 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'콜라타워야더(K-301)'"}, "칸": "잡품"}}, {"열쇠": "Q_잡2^표", "기호": "Q", "아래": "잡2", "위": "표", "이름": "경유 사용 임업기계 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'HAM300(임업용 트랙터 포함)'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000630.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000630", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000630.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000630", "칸": "장비가격"}}, - {"열쇠": "k_EQ0002", "기호": "k", "아래": "EQ0002", "위": "", "이름": "EQ000629.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000629", "칸": "손료계수"}}, - {"열쇠": "P_EQ0002", "기호": "P", "아래": "EQ0002", "위": "", "이름": "EQ000629.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000629", "칸": "장비가격"}} + {"열쇠": "k_콜라타워야더", "기호": "k", "아래": "콜라타워야더", "위": "", "이름": "EQ000630.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000630", "칸": "손료계수"}}, + {"열쇠": "P_콜라타워야더", "기호": "P", "아래": "콜라타워야더", "위": "", "이름": "EQ000630.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000630", "칸": "장비가격"}}, + {"열쇠": "k_HAM300", "기호": "k", "아래": "HAM300", "위": "", "이름": "EQ000629.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000629", "칸": "손료계수"}}, + {"열쇠": "P_HAM300", "기호": "P", "아래": "HAM300", "위": "", "이름": "EQ000629.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000629", "칸": "장비가격"}} ], "식": [ {"기호": "r_가산", "식": "만약(S_작업 == '소나무재선충병방제', r_가산율^표, 0)"}, @@ -569,7 +569,7 @@ {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "d_일수", "단가": "w_보통인부"}, {"이름": "경유(부착형 타워)", "단위": "ℓ", "비목": "재료비", "수량": "만약(S_기종 == 'K-301', Q_주연료^표, Q_주연료2^표) × d_일수", "단가": "P_자동차용경유", "출처": "산림품셈 2-1-2"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × 만약(S_기종 == 'K-301', Q_잡^표, Q_잡2^표) ÷ 100", "출처": "산림품셈 2-1-2 주「잡품」"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "만약(S_기종 == 'K-301', k_EQ0006 × P_EQ0006, k_EQ0002 × P_EQ0002) × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "만약(S_기종 == 'K-301', k_콜라타워야더 × P_콜라타워야더, k_HAM300 × P_HAM300) × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -595,8 +595,8 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "경유 사용 임업기계 연료.주연료", "단위": "ℓ/일,대", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'우드그래플'"}, "칸": "주연료"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "경유 사용 임업기계 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'우드그래플'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000636.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000636", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000636.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000636", "칸": "장비가격"}} + {"열쇠": "k_동력상하차기", "기호": "k", "아래": "동력상하차기", "위": "", "이름": "EQ000636.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000636", "칸": "손료계수"}}, + {"열쇠": "P_동력상하차기", "기호": "P", "아래": "동력상하차기", "위": "", "이름": "EQ000636.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000636", "칸": "장비가격"}} ], "식": [ {"기호": "Q_작업량값", "식": "만약(Q_평균작업 == '20㎥ 미만', 10, 만약(Q_평균작업 == '20∼40㎥', 30, 50))"}, @@ -607,7 +607,7 @@ {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "d_일수", "단가": "w_보통인부"}, {"이름": "경유(우드그래플)", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × d_일수", "단가": "P_자동차용경유", "출처": "산림품셈 2-1-2"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-2 주「잡품」"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 × P_EQ0006 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_동력상하차기 × P_동력상하차기 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -717,8 +717,8 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "경유 사용 임업기계 연료.주연료", "단위": "ℓ/일,대", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'우드그래플'"}, "칸": "주연료"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "경유 사용 임업기계 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'우드그래플'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000636.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000636", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000636.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000636", "칸": "장비가격"}} + {"열쇠": "k_동력상하차기", "기호": "k", "아래": "동력상하차기", "위": "", "이름": "EQ000636.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000636", "칸": "손료계수"}}, + {"열쇠": "P_동력상하차기", "기호": "P", "아래": "동력상하차기", "위": "", "이름": "EQ000636.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000636", "칸": "장비가격"}} ], "식": [ {"기호": "D_찾을", "식": "만약(D_원목 == 30, 29.5, D_원목)"}, @@ -729,7 +729,7 @@ {"이름": "건설기계운전사", "단위": "인", "비목": "노무비", "수량": "d_일수", "단가": "w_기계운전사"}, {"이름": "경유(우드그래플)", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × d_일수", "단가": "P_자동차용경유", "출처": "산림품셈 2-1-2"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-2 주「잡품」"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 × P_EQ0006 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_동력상하차기 × P_동력상하차기 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -780,15 +780,15 @@ {"열쇠": "Q", "기호": "Q", "아래": "", "위": "", "이름": "운반량", "단위": "", "연결": {"갈래": "표", "표": "QF000205", "조건": {"구분": "'운반량'", "거리": "L_거리"}, "칸": "값"}}, {"열쇠": "일수", "기호": "d", "아래": "일수", "위": "", "이름": "일수", "단위": "", "연결": {"갈래": "식"}}, {"열쇠": "w_기계운전사", "기호": "w", "아래": "기계운전사", "위": "", "이름": "건설기계운전사노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000048"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000639.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000639", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000639.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000639", "칸": "장비가격"}} + {"열쇠": "k_포워더", "기호": "k", "아래": "포워더", "위": "", "이름": "EQ000639.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000639", "칸": "손료계수"}}, + {"열쇠": "P_포워더", "기호": "P", "아래": "포워더", "위": "", "이름": "EQ000639.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000639", "칸": "장비가격"}} ], "식": [ {"기호": "d_일수", "식": "1 ÷ Q"} ], "금액": [ {"이름": "건설기계운전사", "단위": "인", "비목": "노무비", "수량": "d_일수", "단가": "w_기계운전사"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 × P_EQ0006 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_포워더 × P_포워더 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -808,15 +808,15 @@ {"열쇠": "Q", "기호": "Q", "아래": "", "위": "", "이름": "운재량", "단위": "", "연결": {"갈래": "표", "표": "QF000206", "조건": {"구분": "'운재량'", "거리": "L_거리"}, "칸": "값"}}, {"열쇠": "일수", "기호": "d", "아래": "일수", "위": "", "이름": "일수", "단위": "", "연결": {"갈래": "식"}}, {"열쇠": "w_기계운전사", "기호": "w", "아래": "기계운전사", "위": "", "이름": "건설기계운전사노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000048"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000639.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000639", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000639.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000639", "칸": "장비가격"}} + {"열쇠": "k_포워더", "기호": "k", "아래": "포워더", "위": "", "이름": "EQ000639.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000639", "칸": "손료계수"}}, + {"열쇠": "P_포워더", "기호": "P", "아래": "포워더", "위": "", "이름": "EQ000639.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000639", "칸": "장비가격"}} ], "식": [ {"기호": "d_일수", "식": "1 ÷ Q"} ], "금액": [ {"이름": "건설기계운전사", "단위": "인", "비목": "노무비", "수량": "d_일수", "단가": "w_기계운전사"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 × P_EQ0006 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_포워더 × P_포워더 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -840,8 +840,8 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "경유 사용 임업기계 연료.주연료", "단위": "ℓ/일,대", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'소형 포워더'"}, "칸": "주연료"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "경유 사용 임업기계 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'소형 포워더'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000640.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000640", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000640.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000640", "칸": "장비가격"}} + {"열쇠": "k_포워더", "기호": "k", "아래": "포워더", "위": "", "이름": "EQ000640.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000640", "칸": "손료계수"}}, + {"열쇠": "P_포워더", "기호": "P", "아래": "포워더", "위": "", "이름": "EQ000640.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000640", "칸": "장비가격"}} ], "식": [ {"기호": "d_일수", "식": "1 ÷ Q"} @@ -850,7 +850,7 @@ {"이름": "건설기계운전사", "단위": "인", "비목": "노무비", "수량": "d_일수", "단가": "w_기계운전사"}, {"이름": "경유(소형 포워더)", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × d_일수", "단가": "P_자동차용경유", "출처": "산림품셈 2-1-2"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-2 주「잡품」"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 × P_EQ0006 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_포워더 × P_포워더 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -874,8 +874,8 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "경유 사용 임업기계 연료.주연료", "단위": "ℓ/일,대", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'소형트럭'"}, "칸": "주연료"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "경유 사용 임업기계 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'소형트럭'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000641.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000641", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000641.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000641", "칸": "장비가격"}} + {"열쇠": "k_소형트럭", "기호": "k", "아래": "소형트럭", "위": "", "이름": "EQ000641.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000641", "칸": "손료계수"}}, + {"열쇠": "P_소형트럭", "기호": "P", "아래": "소형트럭", "위": "", "이름": "EQ000641.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000641", "칸": "장비가격"}} ], "식": [ {"기호": "d_일수", "식": "1 ÷ Q"} @@ -884,7 +884,7 @@ {"이름": "건설기계운전사", "단위": "인", "비목": "노무비", "수량": "d_일수", "단가": "w_기계운전사"}, {"이름": "경유(소형트럭)", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × d_일수", "단가": "P_자동차용경유", "출처": "산림품셈 2-1-2"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-2 주「잡품」"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 × P_EQ0006 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_소형트럭 × P_소형트럭 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -908,8 +908,8 @@ {"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "경유 사용 임업기계 연료.주연료", "단위": "ℓ/일,대", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'소형 포워더'"}, "칸": "주연료"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "경유 사용 임업기계 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000002", "조건": {"기계명": "'소형 포워더'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000640.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000640", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000640.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000640", "칸": "장비가격"}} + {"열쇠": "k_포워더", "기호": "k", "아래": "포워더", "위": "", "이름": "EQ000640.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000640", "칸": "손료계수"}}, + {"열쇠": "P_포워더", "기호": "P", "아래": "포워더", "위": "", "이름": "EQ000640.장비가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000640", "칸": "장비가격"}} ], "식": [ {"기호": "d_일수", "식": "1 ÷ Q"} @@ -918,7 +918,7 @@ {"이름": "건설기계운전사", "단위": "인", "비목": "노무비", "수량": "d_일수", "단가": "w_기계운전사"}, {"이름": "경유(소형 포워더)", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × d_일수", "단가": "P_자동차용경유", "출처": "산림품셈 2-1-2"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-2 주「잡품」"}, - {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 × P_EQ0006 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} + {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "k_포워더 × P_포워더 × 1000 × d_일수", "출처": "산림품셈 2-2-3"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, diff --git a/resources/master_data/수식문서/수식_산림품셈_08장_방제.json b/resources/master_data/수식문서/수식_산림품셈_08장_방제.json index 95b7309c2..24cac20b7 100644 --- a/resources/master_data/수식문서/수식_산림품셈_08장_방제.json +++ b/resources/master_data/수식문서/수식_산림품셈_08장_방제.json @@ -75,7 +75,7 @@ {"열쇠": "P_휘발유", "기호": "P", "아래": "휘발유", "위": "", "이름": "휘발유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:휘발유:{S_지역}"}}, {"열쇠": "Q_주연료2^표", "기호": "Q", "아래": "주연료2", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.주연료", "단위": "수(단위는 줄의 단위 칸 따름)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'체인톱'", "재료명": "'체인오일(일반오일)'"}, "칸": "주연료"}}, {"열쇠": "P_체인오일", "기호": "P", "아래": "체인오일", "위": "", "이름": "체인오일단가", "단위": "", "연결": {"갈래": "요소", "재료": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "작동유"}, "단위": "ℓ"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000620.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000620", "칸": "손료계수"}}, + {"열쇠": "k_체인톱", "기호": "k", "아래": "체인톱", "위": "", "이름": "EQ000620.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000620", "칸": "손료계수"}}, {"열쇠": "P_체인톱손료", "기호": "P", "아래": "체인톱손료", "위": "", "이름": "체인톱 손료단가", "단위": "", "연결": {"갈래": "요소", "재료": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "동력체인톱"}, "단위": "대"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'체인톱'", "재료명": "'보통휘발유(주연료)'"}, "칸": "잡품"}} ], @@ -88,7 +88,7 @@ {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "N_벌목부", "단가": "w_보통인부", "출처": "산림품셈 8-3 주⑥ 「보통인부 50%」"}, {"이름": "휘발유(체인톱)", "단위": "ℓ", "비목": "재료비", "수량": "N_벌목부 × Q_주연료^표", "단가": "P_휘발유", "출처": "산림품셈 2-1-1 주③", "기호": "C_휘발유"}, {"이름": "체인오일", "단위": "ℓ", "비목": "재료비", "수량": "N_벌목부 × Q_주연료2^표", "단가": "P_체인오일", "출처": "산림품셈 2-1-1 주④"}, - {"이름": "체인톱 손료", "단위": "대", "비목": "경비", "수량": "N_벌목부 × k_EQ0006", "단가": "P_체인톱손료", "출처": "산림품셈 2-2-1"}, + {"이름": "체인톱 손료", "단위": "대", "비목": "경비", "수량": "N_벌목부 × k_체인톱", "단가": "P_체인톱손료", "출처": "산림품셈 2-2-1"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "C_휘발유 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-1 주⑤"} ], "끝수": { @@ -241,7 +241,7 @@ {"열쇠": "Q_수량2^표", "기호": "Q", "아래": "수량2", "위": "표", "이름": "유인 헬기(160ha당).수량", "단위": "수", "연결": {"갈래": "표", "표": "QF000016", "조건": {"구분": "'깃발'"}, "칸": "수량"}}, {"열쇠": "P_깃발", "기호": "P", "아래": "깃발", "위": "", "이름": "깃발단가", "단위": "", "연결": {"갈래": "요소", "재료": {"구분": "안전시설", "상세구분": "표지판·지주", "검색어": "깃발"}, "단위": "개"}}, {"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "유인 헬기(160ha당).잡품", "단위": "%(주재료비의)", "연결": {"갈래": "표", "표": "QF000016", "조건": {"구분": "'휘발유(양수기)'"}, "칸": "잡품"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000644.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000644", "칸": "손료계수"}} + {"열쇠": "k_양수기", "기호": "k", "아래": "양수기", "위": "", "이름": "EQ000644.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000644", "칸": "손료계수"}} ], "식": [ {"기호": "A_면적", "식": "만약(S_구분 == '소형헬기 (160ha당)', Q_1일살포면적^표, Q_1일살포면2^표)"}, @@ -253,7 +253,7 @@ {"이름": "휘발유(양수기)", "단위": "ℓ", "비목": "재료비", "수량": "Q_휘발유", "단가": "P_휘발유", "출처": "산림품셈 2-1-8"}, {"이름": "깃발", "단위": "개", "비목": "재료비", "수량": "Q_수량2^표 ÷ Q_1일살포면적^표", "단가": "P_깃발", "출처": "산림품셈 2-1-8 「20ha당 1개」"}, {"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "Q_휘발유 × P_휘발유 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-8 주④"}, - {"이름": "양수기 손료", "단위": "식", "비목": "경비", "단가": "만약(S_양수기 == '사용', k_EQ0006 × P_양수기 ÷ A_면적, 0)", "출처": "산림품셈 2-2-6"} + {"이름": "양수기 손료", "단위": "식", "비목": "경비", "단가": "만약(S_양수기 == '사용', k_양수기 × P_양수기 ÷ A_면적, 0)", "출처": "산림품셈 2-2-6"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -286,12 +286,12 @@ {"열쇠": "Q_보통인부^표", "기호": "Q", "아래": "보통인부", "위": "표", "이름": "드론방제 무인 헬리콥터 소요인력.보통인부", "단위": "인", "연결": {"갈래": "표", "표": "QF000237", "조건": {"항목": "'계'"}, "칸": "보통인부"}}, {"열쇠": "Q_보통인부2^표", "기호": "Q", "아래": "보통인부2", "위": "표", "이름": "드론방제 무인 멀티콥터 소요인력.보통인부", "단위": "인", "연결": {"갈래": "표", "표": "QF000238", "조건": {"항목": "'계'"}, "칸": "보통인부"}}, {"열쇠": "w_보통인부", "기호": "w", "아래": "보통인부", "위": "", "이름": "보통인부노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000002"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000645.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000645", "칸": "손료계수"}}, - {"열쇠": "P_EQ0006", "기호": "P", "아래": "EQ0006", "위": "", "이름": "EQ000645.취득가액", "단위": "", "연결": {"갈래": "요소", "키": "EQ000645", "칸": "취득가액"}}, - {"열쇠": "k_EQ0002", "기호": "k", "아래": "EQ0002", "위": "", "이름": "EQ000646.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000646", "칸": "손료계수"}}, - {"열쇠": "P_EQ0002", "기호": "P", "아래": "EQ0002", "위": "", "이름": "EQ000646.취득가액", "단위": "", "연결": {"갈래": "요소", "키": "EQ000646", "칸": "취득가액"}}, - {"열쇠": "k_EQ0003", "기호": "k", "아래": "EQ0003", "위": "", "이름": "EQ000647.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000647", "칸": "손료계수"}}, - {"열쇠": "P_EQ0003", "기호": "P", "아래": "EQ0003", "위": "", "이름": "EQ000647.취득가액", "단위": "", "연결": {"갈래": "요소", "키": "EQ000647", "칸": "취득가액"}} + {"열쇠": "k_헬리콥터", "기호": "k", "아래": "헬리콥터", "위": "", "이름": "EQ000645.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000645", "칸": "손료계수"}}, + {"열쇠": "P_헬리콥터", "기호": "P", "아래": "헬리콥터", "위": "", "이름": "EQ000645.취득가액", "단위": "", "연결": {"갈래": "요소", "키": "EQ000645", "칸": "취득가액"}}, + {"열쇠": "k_멀티콥터", "기호": "k", "아래": "멀티콥터", "위": "", "이름": "EQ000646.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000646", "칸": "손료계수"}}, + {"열쇠": "P_멀티콥터", "기호": "P", "아래": "멀티콥터", "위": "", "이름": "EQ000646.취득가액", "단위": "", "연결": {"갈래": "요소", "키": "EQ000646", "칸": "취득가액"}}, + {"열쇠": "k_배터리", "기호": "k", "아래": "배터리", "위": "", "이름": "EQ000647.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000647", "칸": "손료계수"}}, + {"열쇠": "P_배터리", "기호": "P", "아래": "배터리", "위": "", "이름": "EQ000647.취득가액", "단위": "", "연결": {"갈래": "요소", "키": "EQ000647", "칸": "취득가액"}} ], "식": [ {"기호": "A_일방제", "식": "만약(S_기종 == '무인헬기', A_시간당^표 × T_1일작업^표, A_시간당2^표 × T_1일작업2^표)"} @@ -301,7 +301,7 @@ {"이름": "부조종자(건설기계운전사)", "단위": "인", "비목": "노무비", "수량": "만약(S_기종 == '무인헬기', Q_부조종자^표, Q_부조종자2^표)", "단가": "w_기계운전사"}, {"이름": "특별인부(신호수)", "단위": "인", "비목": "노무비", "수량": "만약(S_기종 == '무인헬기', Q_특별인부^표, Q_특별인부2^표)", "단가": "w_특별인부"}, {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "만약(S_기종 == '무인헬기', Q_보통인부^표, Q_보통인부2^표)", "단가": "w_보통인부"}, - {"이름": "드론 손료", "단위": "식", "비목": "경비", "단가": "만약(S_기종 == '무인헬기', k_EQ0006 × P_EQ0006, k_EQ0002 × P_EQ0002 + k_EQ0003 × P_EQ0003) ÷ A_일방제", "출처": "산림품셈 2-2-7 · 2-2-8"} + {"이름": "드론 손료", "단위": "식", "비목": "경비", "단가": "만약(S_기종 == '무인헬기', k_헬리콥터 × P_헬리콥터, k_멀티콥터 × P_멀티콥터 + k_배터리 × P_배터리) ÷ A_일방제", "출처": "산림품셈 2-2-7 · 2-2-8"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -322,13 +322,13 @@ {"열쇠": "w_특별인부", "기호": "w", "아래": "특별인부", "위": "", "이름": "특별인부노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000003"}}, {"열쇠": "Q_보통인부^표", "기호": "Q", "아래": "보통인부", "위": "표", "이름": "지상방제 지상 약제살포.보통인부", "단위": "인", "연결": {"갈래": "표", "표": "QF000239", "조건": {"항목": "'계'"}, "칸": "보통인부"}}, {"열쇠": "w_보통인부", "기호": "w", "아래": "보통인부", "위": "", "이름": "보통인부노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000002"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000649.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000649", "칸": "손료계수"}} + {"열쇠": "k_동력분무기", "기호": "k", "아래": "동력분무기", "위": "", "이름": "EQ000649.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000649", "칸": "손료계수"}} ], "식": [], "금액": [ {"이름": "특별인부", "단위": "인", "비목": "노무비", "수량": "Q_특별인부^표", "단가": "w_특별인부"}, {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "Q_보통인부^표", "단가": "w_보통인부"}, - {"이름": "동력분무기 손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 × P_동력분무기", "출처": "산림품셈 2-2-9"} + {"이름": "동력분무기 손료", "단위": "식", "비목": "경비", "단가": "k_동력분무기 × P_동력분무기", "출처": "산림품셈 2-2-9"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, @@ -479,7 +479,7 @@ {"열쇠": "w_보통인부", "기호": "w", "아래": "보통인부", "위": "", "이름": "보통인부노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000002"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}}, {"열쇠": "Q_잡재료^표", "기호": "Q", "아래": "잡재료", "위": "표", "이름": "이동식 임목 파쇄 주 속 수 잡재료", "단위": "줄 따름", "연결": {"갈래": "표", "표": "CF000096", "조건": {"구분": "'잡재료'"}, "칸": "적용"}}, - {"열쇠": "k_EQ0006", "기호": "k", "아래": "EQ0006", "위": "", "이름": "EQ000650.시간당손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000650", "칸": "시간당손료계수"}} + {"열쇠": "k_파쇄", "기호": "k", "아래": "파쇄", "위": "", "이름": "EQ000650.시간당손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000650", "칸": "시간당손료계수"}} ], "식": [ {"기호": "T_시간", "식": "1 ÷ Q"} @@ -489,7 +489,7 @@ {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "Q_보통인부^표 ÷ T_일 × T_시간", "단가": "w_보통인부"}, {"이름": "경유(파쇄기)", "단위": "ℓ", "비목": "재료비", "수량": "13.5 × T_시간", "단가": "P_자동차용경유", "출처": "산림품셈 8-11 주⑤"}, {"이름": "잡재료", "단위": "식", "비목": "재료비", "단가": "재료비 × Q_잡재료^표 ÷ 100", "출처": "산림품셈 8-11 비고"}, - {"이름": "파쇄기 손료", "단위": "식", "비목": "경비", "단가": "k_EQ0006 ÷ 10000000 × P_파쇄기 × T_시간", "출처": "산림품셈 2-2-10"} + {"이름": "파쇄기 손료", "단위": "식", "비목": "경비", "단가": "k_파쇄 ÷ 10000000 × P_파쇄기 × T_시간", "출처": "산림품셈 2-2-10"} ], "끝수": { "줄": {"방법": "버림", "자리": 1}, diff --git a/resources/master_data/수식문서/수식_산림품셈_10장_자재․장비_운반.json b/resources/master_data/수식문서/수식_산림품셈_10장_자재․장비_운반.json index f36536994..2ce7eada4 100644 --- a/resources/master_data/수식문서/수식_산림품셈_10장_자재․장비_운반.json +++ b/resources/master_data/수식문서/수식_산림품셈_10장_자재․장비_운반.json @@ -475,26 +475,26 @@ {"열쇠": "비율", "기호": "r", "아래": "비율", "위": "", "이름": "비율", "단위": "", "출처": "산림품셈 10-9", "연결": {"갈래": "표", "표": "QF000330", "조건": {"구분": "'2.5톤 덤프 주 연료 대비'"}, "칸": "비율"}}, {"열쇠": "직종", "기호": "S", "아래": "직종", "위": "", "이름": "직종", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "조종원직종"}}, {"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)", "연결": {"갈래": "식"}}, - {"열쇠": "P_EQ0000", "기호": "P", "아래": "EQ0000", "위": "", "이름": "EQ000069.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "가격"}}, - {"열쇠": "k_EQ0000", "기호": "k", "아래": "EQ0000", "위": "", "이름": "EQ000069.상각비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "상각비계수"}}, - {"열쇠": "k_EQ0002", "기호": "k", "아래": "EQ0002", "위": "", "이름": "EQ000069.정비비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "정비비계수"}}, - {"열쇠": "k_EQ0003", "기호": "k", "아래": "EQ0003", "위": "", "이름": "EQ000069.관리비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "관리비계수"}}, + {"열쇠": "P_덤프트럭", "기호": "P", "아래": "덤프트럭", "위": "", "이름": "EQ000069.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "가격"}}, + {"열쇠": "k_덤프트럭", "기호": "k", "아래": "덤프트럭", "위": "", "이름": "EQ000069.상각비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "상각비계수"}}, + {"열쇠": "k_덤프트럭2", "기호": "k", "아래": "덤프트럭2", "위": "", "이름": "EQ000069.정비비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "정비비계수"}}, + {"열쇠": "k_덤프트럭3", "기호": "k", "아래": "덤프트럭3", "위": "", "이름": "EQ000069.관리비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "관리비계수"}}, {"열쇠": "Q_분자^표", "기호": "Q", "아래": "분자", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분자"}}, {"열쇠": "Q_분모^표", "기호": "Q", "아래": "분모", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분모"}}, - {"열쇠": "P_EQ0002", "기호": "P", "아래": "EQ0002", "위": "", "이름": "EQ000069.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "주연료"}}, + {"열쇠": "P_덤프트럭2", "기호": "P", "아래": "덤프트럭2", "위": "", "이름": "EQ000069.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "주연료"}}, {"열쇠": "P_자동차용경유", "기호": "P", "아래": "자동차용경유", "위": "", "이름": "자동차용경유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:경유:{S_지역}"}}, - {"열쇠": "Q_EQ0000", "기호": "Q", "아래": "EQ0000", "위": "", "이름": "EQ000069.조종원", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "조종원"}}, + {"열쇠": "Q_덤프트럭", "기호": "Q", "아래": "덤프트럭", "위": "", "이름": "EQ000069.조종원", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "조종원"}}, {"열쇠": "w_화물차운전사", "기호": "w", "아래": "화물차운전사", "위": "", "이름": "화물차운전사노임", "단위": "", "연결": {"갈래": "요소", "키": "LB:{S_직종}"}}, - {"열쇠": "r_EQ0000", "기호": "r", "아래": "EQ0000", "위": "", "이름": "EQ000069.잡재료율", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "잡재료율"}} + {"열쇠": "r_덤프트럭", "기호": "r", "아래": "덤프트럭", "위": "", "이름": "EQ000069.잡재료율", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "잡재료율"}} ], "식": [ - {"기호": "C_손료", "식": "버림(P_EQ0000 × 1000 × (k_EQ0000 + k_EQ0002 + k_EQ0003) ÷ 10000000, 0)"}, + {"기호": "C_손료", "식": "버림(P_덤프트럭 × 1000 × (k_덤프트럭 + k_덤프트럭2 + k_덤프트럭3) ÷ 10000000, 0)"}, {"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표"} ], "금액": [ - {"이름": "주연료", "단위": "ℓ", "비목": "재료비", "수량": "P_EQ0002 × r_비율 ÷ 100", "단가": "P_자동차용경유", "출처": "산림품셈 10-9"}, - {"이름": "조종원", "단위": "인", "비목": "노무비", "수량": "Q_EQ0000 × Q_시간환산", "단가": "w_화물차운전사", "출처": "건설품셈 공통 8-1-3 6"}, - {"이름": "잡재료비", "단위": "식", "비목": "재료비", "단가": "재료비 × r_EQ0000 ÷ 100", "출처": "건설품셈 공통 8-4 주②"}, + {"이름": "주연료", "단위": "ℓ", "비목": "재료비", "수량": "P_덤프트럭2 × r_비율 ÷ 100", "단가": "P_자동차용경유", "출처": "산림품셈 10-9"}, + {"이름": "조종원", "단위": "인", "비목": "노무비", "수량": "Q_덤프트럭 × Q_시간환산", "단가": "w_화물차운전사", "출처": "건설품셈 공통 8-1-3 6"}, + {"이름": "잡재료비", "단위": "식", "비목": "재료비", "단가": "재료비 × r_덤프트럭 ÷ 100", "출처": "건설품셈 공통 8-4 주②"}, {"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "C_손료", "출처": "건설품셈 공통 8-1-6 2"} ], "끝수": { diff --git a/resources/tester/test_552_3_row_menu.py b/resources/tester/test_552_3_row_menu.py index b7fe5b5a0..1b1b7e40b 100644 --- a/resources/tester/test_552_3_row_menu.py +++ b/resources/tester/test_552_3_row_menu.py @@ -9,7 +9,8 @@ def test_row_menu(): for word in ("꼴", "크기", "상태", "쓰는 법", "대신하는"): assert word in head, word assert "export function createRowMenu" in ts - assert 'createIconButton({ icon: "dots", title: "더 보기", size: 28 })' in ts + # 여는 단추 기본 = ⋯ 28(804-7 — 부르는 쪽이 [+] 등으로 바꿀 수 있음) + assert 'icon: trig.icon ?? "dots",' in ts and "size: trig.size ?? 28," in ts assert "if (open) enterMenu(entry);" in ts and "showConfirmDialog(" in ts css = _text("ui_template_context_menu.css") mine = css[css.index("(552-3)") :] diff --git a/resources/tester/test_804_2_line_stack.py b/resources/tester/test_804_2_line_stack.py index 8f36954c1..2ba1c41b6 100644 --- a/resources/tester/test_804_2_line_stack.py +++ b/resources/tester/test_804_2_line_stack.py @@ -208,7 +208,9 @@ def _src(rel: str) -> str: def test_화면_이음(): lab = _src("M01_MasterData/M01_MasterData_UI_LogicLab.ts") - assert 'createStackScreen({ base: "/m01/logic/doc", admin, cache: "m01_doc_drafts" })' in lab + assert ( + 'base: "/m01/logic/doc",' in lab and 'cache: "m01_doc_drafts",' in lab + ) # 804-7 page = 페이지 [저장] assert "stack: stack.root" in lab and "=> stack.canLeave();" in lab # 804-6 수식 탭 = 줄 쌓기 assert '{ value: "stack", label: tl("View_Equation") }' in _src( "M01_MasterData/M01_MasterData_UI_LogicLab_View.ts" diff --git a/resources/tester/test_804_4_line_stack_take.py b/resources/tester/test_804_4_line_stack_take.py index 828d2d23e..8cf2802fa 100644 --- a/resources/tester/test_804_4_line_stack_take.py +++ b/resources/tester/test_804_4_line_stack_take.py @@ -68,7 +68,9 @@ def test_M01_은_M01전체만(): assert 'kinds: copy ? ["logic", "sk", "pre"] : [want], // M01 = M01 전체만(680-1)' in pick assert 'const copy = kind === "단가산출" && m02 ? m02 : null;' in pick lab = _src("M01_MasterData/M01_MasterData_UI_LogicLab.ts") - assert 'createStackScreen({ base: "/m01/logic/doc", admin, cache: "m01_doc_drafts" })' in lab + assert ( + 'base: "/m01/logic/doc",' in lab and 'cache: "m01_doc_drafts",' in lab + ) # 804-7 page = 페이지 [저장] tabs = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts") assert "scope: opt.scope, // 804-4" in tabs screen = _src("A00_Common/line_stack_screen.ts") diff --git a/resources/tester/test_804_7_line_stack_fix.py b/resources/tester/test_804_7_line_stack_fix.py new file mode 100644 index 000000000..d30650e78 --- /dev/null +++ b/resources/tester/test_804_7_line_stack_fix.py @@ -0,0 +1,205 @@ +"""804-7(요구 687 · 688) — 줄 쌓기 「수식」 탭 수정 2차 지킴. + +① 로직 삭제 = 서버 정본(로직 줄 + 수식 문서)에서 바로 · 부르는 곳(다른 문서 · M02 단가산출) 있으면 409 {쓰는곳} · + 정본 로직은 개정 확인 409 — 임시 폴더만 +② 머리 단추 다섯 — [저장] 한 번 = 로직 줄 + 수식 · [되돌림] = 둘 다 · 줄 쌓기 자기 단추 없음(M01) +③ 수식 문서 1,518 전수 — 요소 키가 기호 이름으로 샌 것(`k_EQ0006`) 0 · 변환기도 자원 이름(`k_하베스터`) +④ 단위 = 공용 단위 고르기(친 글도 씀) · 표준 표 읽기 = 로그인만 +⑤ 시험 페이지(804-1) 걷음 · 줄 쌓기 화면 차림(한 줄 · [+] 메뉴 · 가나다 체크 없음 · 기계 제목 펼침) +""" + +from __future__ import annotations + +import json +import re +import shutil +from pathlib import Path + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from M01_MasterData import M01_MasterData_Doc as doc_screen +from M01_MasterData import M01_MasterData_Router as router_module +from M01_MasterData import M01_MasterData_Store as store + +ROOT = Path(__file__).resolve().parents[2] +REAL = store.FOLDER +CODE = re.compile(r"^(?:EQ|MT|LB)\d") + + +def _src(rel: str) -> str: + return (ROOT / rel).read_text(encoding="utf-8") + + +@pytest.fixture +def client(tmp_path: Path, monkeypatch) -> TestClient: + logic, docs = tmp_path / "logic", tmp_path / "doc" + logic.mkdir() + for path in REAL.glob("*.json"): + if not path.name.startswith("_") or path.name == store.mk.BOOK.name: + shutil.copy(path, logic / path.name) + shutil.copytree(doc_screen.folder(), docs) + monkeypatch.setattr(store, "FOLDER", logic) + monkeypatch.setattr(doc_screen.ml, "FOLDER", docs) + from M02_MasterTemplete import M02_Template_Layers as layers + + sk = tmp_path / "SK-1.json" + monkeypatch.setattr( + layers, "layer_files", lambda kind: [("개인 시험", sk)] if sk.exists() else [] + ) + app = FastAPI() + app.include_router(router_module.router) + c = TestClient(app) + c.sk = sk # type: ignore[attr-defined] + return c + + +def _new_logic(c: TestClient, name: str) -> str: + row = { + "키": "", + "이름": name, + "결과단위": "원/㎥", + "출처": "자체", + "입력": [], + "중간": [], + "호표": [], + "끝수": None, + } + ver = store.read("로직_자체.json")[1] if (store.FOLDER / "로직_자체.json").is_file() else "" + res = c.post( + "/api/m01/save", + json={ + "files": [ + {"file": "로직_자체.json", "version": ver, "changes": [{"op": "add", "row": row}]} + ] + }, + ) + assert res.status_code == 200, res.text + return res.json()["added"][-1]["key"] # fmt: skip + + +def _doc(c: TestClient, key: str, sub: str | None = None) -> None: + doc = c.get("/api/m01/logic/doc", params={"key": key}).json()["문서"] + doc["금액"] = [ + {"이름": "보통인부", "단위": "인", "비목": "노무비", "수량": "1", "단가": "w_보통"} + ] + doc["변수"] = [ + {"기호": "w", "아래": "보통", "위": "", "연결": {"갈래": "요소", "키": "LB000002"}} + ] + if sub: + doc["변수"].append( + {"기호": "B", "아래": "", "위": "", "연결": {"갈래": "단가산출", "키": sub, "입력": {}}} + ) + res = c.post("/api/m01/logic/doc/save", json={"문서": doc}) + assert res.status_code == 200, res.text + + +def _remove(c: TestClient, key: str, ok: bool = False): + one = c.get("/api/m01/logic", params={"key": key}).json() + return c.post( + "/api/m01/logic/remove", + json={"key": key, "file": one["file"], "version": one["version"], "개정확인": ok}, + ) + + +def test_로직_삭제_서버_정본_바로(client: TestClient) -> None: + a, b = _new_logic(client, "sub3시험 지울 것"), _new_logic(client, "sub3시험 부르는 것") + _doc(client, a) + _doc(client, b, sub=a) + # 다른 문서가 부름 → 막힘 · 까닭 = 그 문서 + res = _remove(client, a) + assert res.status_code == 409 and any(b in x for x in res.json()["detail"]["쓰는곳"]) + # M02 단가산출 바탕 = 이 로직 → 막힘 + client.sk.write_text(json.dumps({"이름": "sub3임시", "바탕": {"로직": b}}), encoding="utf-8") # type: ignore[attr-defined] + res = _remove(client, b) + assert res.status_code == 409 and res.json()["detail"]["쓰는곳"] == [ + "M02 단가산출 개인 시험 sub3임시" + ] + client.sk.unlink() # type: ignore[attr-defined] + # 부르는 쪽부터 지움 — 로직 줄 · 수식 문서 둘 다 없어짐 + assert _remove(client, b).status_code == 200 + assert _remove(client, a).status_code == 200 + for k in (a, b): + assert client.get("/api/m01/logic", params={"key": k}).status_code == 404 + assert k not in doc_screen.docs() + # 정본 로직 = 개정 확인 409(아무것도 안 지움) + res = _remove(client, "GF000099") + assert res.status_code == 409 and res.json()["detail"].get("개정") + assert "GF000099" in doc_screen.docs() + + +def test_정본_폴더_안_바뀜() -> None: + assert doc_screen.folder() == ROOT / "resources/master_data/수식문서" + assert store.FOLDER == REAL + + +def test_머리_단추_다섯_저장_한_번() -> None: + lab = _src("M01_MasterData/M01_MasterData_UI_LogicLab.ts") + assert "page: () => persist()," in lab + assert ( + "removeLogic(key, one.file, one.version, ok)" in lab and "show(null); // 로직 목록" in lab + ) + assert "if (opened) await stack.save();" in lab # [저장] = 로직 줄 + 수식(여는 중이면 기다림) + assert "await loading;" in _src("A00_Common/line_stack_screen.ts") + assert "await stack.discard();" in lab # [되돌림] = 둘 다 + assert "n === 0 && !stack.dirty()" in lab + for t in ('tl("New")', "copyButton", 'tx("Bar_Delete")', "discardButton", "saveButton"): + assert t in lab.split("buttons: [")[1].split("]")[0] + text = _src("M01_MasterData/M01_MasterData_UI_Logic_Text.ts") + assert 'Bar_Discard: ["되돌림", "Revert"]' in text and "캐시 삭제" not in text + screen = _src("A00_Common/line_stack_screen.ts") + assert "edit() && key && !opt.page" in screen and "opt.page?.();" in screen + + +def test_요소키_기호_이름_0_전수() -> None: + docs = doc_screen.docs() + assert len(docs) >= 1518 + bad = [ + (k, v.get("기호"), v.get("아래")) + for k, d in docs.items() + for v in d.get("변수") or [] + if CODE.match(str(v.get("아래") or "")) + or CODE.match(str(v.get("열쇠") or "").split("_")[-1]) + ] + assert bad == [] + ids = {doc_screen.md.var_id(v) for v in docs["GF000011"]["변수"]} + assert {"k_하베스터", "P_하베스터", "k_굴착기", "P_굴착기"} <= ids + import master_doc_convert as cv + + whole = store.loaded()[1] + for key in ("GF000011", "GF000145", "GC001043"): + got, _ = cv.convert(whole, store.logic(key)["logic"]) + assert not [v for v in got["변수"] if CODE.match(str(v.get("아래") or ""))], key + + +def test_단위_고르기_공용_친글() -> None: + edit = _src("A00_Common/line_stack_edit.ts") + assert "createUnitPick({" in edit and "free: true," in edit + pick = _src("M02_MasterTemplete/DataFlow/M02_DF_UnitPick.ts") + assert "if (opts.free && typed && !all.includes(typed)) shown.push(typed);" in pick + main = _src("main.py") + at = main.index("M02_MasterTemplete.DataFlow.M02_DF_Router") + assert "dependencies=protected," in main[at : at + 120] + + +def test_시험_페이지_걷음_화면_차림() -> None: + assert not (ROOT / "tools/line-stack-test").exists() + for rel in ("main.py", "vite.config.ts", "M01_MasterData/M01_MasterData_UI_Side.ts"): + assert "line-stack-test" not in _src(rel), rel + edit = _src("A00_Common/line_stack_edit.ts") + assert "createCheckbox" not in edit and "Gana" not in edit # 가나다 체크 걷음 + assert '{ icon: "plus", title: lt("Add_Line"), size: 24 }' in edit # [+] 메뉴 + assert '"틀"' not in edit and "K_틀" not in edit # 틀 찾기 걷음 + assert "every.has(id) && !taken.has(id)" in edit # 만약 안 탄 갈래 숨김 + assert "String(mat?.검색어 ?? line.이름" in edit # 이름 단추 = 그 이름 찾음 + text = _src("A00_Common/line_stack_text.ts") + assert '["단가산출", "Unit price"]' in text and '["근거 글", "Basis note"]' in text + assert "비우면 번호 없음" not in text and "식 고치기" not in text + out = _src("A00_Common/line_stack_out.ts") + assert "s ? opener(s, eqText(bundle[c]))" not in out and "fold(s, title)" in out + css = _src("A00_Common/line_stack.css") + assert ".lstk__foot {\n position: sticky;\n inset-block-end: 0;" in css + assert "html:not" in _src("M01_MasterData/M01_MasterData_UI_LogicLab_Side.css") + for f in (ROOT / "A00_Common").glob("line_stack*.ts"): + assert len(f.read_text(encoding="utf-8").splitlines()) <= 700, f.name diff --git a/tools/line-stack-test/app.js b/tools/line-stack-test/app.js deleted file mode 100644 index 8453ae604..000000000 --- a/tools/line-stack-test/app.js +++ /dev/null @@ -1,247 +0,0 @@ -// 줄 쌓기 단가산출 시험 페이지(요구 669~676 · PLAN 804-1) — 서버 없음 · 저장 = 이 브라우저 localStorage -// 지우기 = 이 폴더 삭제 + main.py 「/tools/line-stack-test」 마운트 덩이 + vite.config.ts 「/tools」 프록시 줄 + M01 왼쪽 패널 「시험: 줄 쌓기」 덩이 -import { compute } from "./calc.js"; -import { renderOut, fmtP, esc } from "./render.js"; -import { parsePaste } from "./paste.js"; -import { LABOR, MACHINE, MATERIAL, SANGEUN, SAMPLES, TABLE_E } from "./samples.js"; -import { renderEdit, allTemplates } from "./edit.js"; - -const KEY_DOC = "lineStackTest.doc"; -const KEY_TPL = "lineStackTest.myTemplates"; -const $ = (s) => document.querySelector(s); -const clone = (x) => structuredClone(x); - -let doc = clone(SAMPLES[0].doc); -let expect = SAMPLES[0].expect; -let expectFrom = SAMPLES[0].label; -let mine = JSON.parse(localStorage.getItem(KEY_TPL) || "[]"); -let res = compute(doc); - -const get = (path) => path.split(".").reduce((o, k) => o?.[k], doc); -const setPath = (path, v) => { - const ks = path.split("."); - const last = ks.pop(); - ks.reduce((o, k) => o[k], doc)[last] = v; -}; - -/** 칸 값만 바뀜 — 오른쪽 출력 · 합계 · 공식 값 칸만 다시(왼쪽은 그대로 → 입력 자리 안 잃음) */ -function update() { - res = compute(doc); - $("#out").innerHTML = renderOut(doc, res); - const t = res.tot; - $("#tot").innerHTML = - ["l", "m", "e"] - .map( - (c) => - `
${{ l: "노무비", m: "재료비", e: "경비" }[c]}${fmtP(t[c])}
`, - ) - .join("") + `
계${fmtP(t.sum)}
`; - const same = expect && ["l", "m", "e", "sum"].every((c) => t[c] === expect[c]); - $("#cmp").innerHTML = expect - ? `${esc(expectFrom)} 거창 파일 ${fmtP(expect.sum)} (노 ${fmtP(expect.l)} · 재 ${fmtP(expect.m)} · 경 ${fmtP(expect.e)}) → ${same ? "같음" : "다름"}` - : ""; - document.querySelectorAll("[data-val]").forEach((el) => { - const m = el.dataset.val.match(/^items\.(\d+)\.lines\.(\d+)\.rows\.(\d+)$/); - const rr = res.items[m[1]]?.lines[m[2]]?.rows?.[m[3]]; - el.textContent = rr?.err ?? rr?.val ?? ""; - el.classList.toggle("bad", !!rr?.err); - el.parentElement.querySelector(".expr")?.classList.toggle("bad", !!rr?.err); - }); -} - -/** 줄 더함 · 지움 · 옮김 등 꼴이 바뀜 — 왼쪽까지 다시 */ -function redraw() { - res = compute(doc); - $("#edit").innerHTML = renderEdit(doc, res, mine); - update(); -} - -function newLine(kind) { - const [k, tplId] = kind.split(":"); - if (k === "formula") { - const tpl = allTemplates(mine).find((t) => t.id === tplId); - return { - k, - tpl: tpl ? tpl.id : null, - rows: tpl ? clone(tpl.rows) : [{ n: "", x: "", u: "" }], - open: !tpl, - }; - } - if (k === "mach") return { k, res: { ...MACHINE[0] }, ops: [{ op: "÷", v: "Q", u: "" }] }; - if (k === "ref") return { k, res: { ...SANGEUN[0] }, ops: [{ op: "×", v: "Q", u: "" }] }; - if (k === "labor") return { k, res: { ...LABOR[0] }, ops: [{ op: "×", v: "1", u: "인" }] }; - if (k === "mat") - return { k, col: "m", res: { ...MATERIAL[0] }, ops: [{ op: "×", v: "1", u: "" }] }; - if (k === "rate") - return { - k, - name: "공구손료 및 경장비", - of: "노무비", - baseName: "", - baseCol: "l", - scope: "item", - rate: "3", - col: "m", - }; - return { k: "text", text: "" }; -} - -function loadSample(s) { - doc = clone(s.doc); - expect = s.expect; - expectFrom = s.label; - redraw(); -} - -// ── 머리 단추 -$("#samples").innerHTML = SAMPLES.map( - (s, i) => ``, -).join(""); -$("#samples").addEventListener("click", (e) => { - const i = e.target.dataset.sample; - if (i !== undefined) loadSample(SAMPLES[i]); -}); -$("#save").onclick = () => { - localStorage.setItem(KEY_DOC, JSON.stringify({ doc, expect, expectFrom })); - $("#msg").textContent = "이 브라우저에 저장함"; -}; -$("#load").onclick = () => { - const s = JSON.parse(localStorage.getItem(KEY_DOC) || "null"); - if (!s) return void ($("#msg").textContent = "저장한 것 없음"); - ({ doc, expect, expectFrom } = s); - redraw(); - $("#msg").textContent = "불러옴"; -}; - -// ── 왼쪽 칸 입력 -$("#edit").addEventListener("input", (e) => { - const p = e.target.dataset.p; - if (!p) return; - setPath(p, e.target.type === "checkbox" ? e.target.checked : e.target.value); - if (e.target.type === "checkbox") return redraw(); - update(); - if (/\.rows\.\d+\.[nx]$/.test(p)) { - // 틀 이름표(「내 틀 … 에서 변경」)만 다시 — 입력 자리는 그대로 - const lp = p.replace(/\.rows\.\d+\.[nx]$/, ""); - const card = e.target.closest(".line"); - const fresh = document.createElement("div"); - fresh.innerHTML = renderEdit(doc, res, mine); - const [, i, j] = lp.match(/items\.(\d+)\.lines\.(\d+)/); - const tag = fresh.querySelector(`.line[data-loc="${i}.${j}"] .tpl`); - if (tag) card.querySelector(".tpl").innerHTML = tag.innerHTML; - } -}); - -$("#edit").addEventListener("change", (e) => { - const act = e.target.dataset.act; - if (!act || !e.target.value) return; - if (act === "addline") { - doc.items[e.target.dataset.i].lines.push(newLine(e.target.value)); - return redraw(); - } - if (act.startsWith("res:")) { - const [, kind, p] = act.split(":"); - const list = { mach: MACHINE, ref: SANGEUN, labor: LABOR, mat: MATERIAL }[kind]; - get(p).res = { ...list[e.target.value] }; - return redraw(); - } -}); - -$("#edit").addEventListener("click", (e) => { - const b = e.target.closest("button"); - if (!b) return; - const { act, p, k, loc, i } = b.dataset; - const at = loc?.split(".").map(Number); - if (act === "additem") doc.items.push({ title: "", lines: [] }); - else if (act === "delitem") doc.items.splice(Number(i), 1); - else if (act === "delline") doc.items[at[0]].lines.splice(at[1], 1); - else if (act === "addop") (get(p).ops ??= []).push({ op: "×", v: "", u: "" }); - else if (act === "delop") get(p).ops.splice(Number(k), 1); - else if (act === "addrow") get(p).rows.push({ n: "", x: "", u: "" }); - else if (act === "delrow") get(p).rows.splice(Number(k), 1); - else if (act === "fold") get(p).open = !get(p).open; - else if (act === "savetpl") { - const ln = get(p); - const name = prompt("틀 이름", "내 틀"); - if (!name?.trim()) return; - const id = `my${Date.now()}`; - mine.push({ id, name: name.trim(), rows: clone(ln.rows).map(({ src, ...r }) => r) }); - localStorage.setItem(KEY_TPL, JSON.stringify(mine)); - ln.tpl = id; - } else if (act === "table") return openTable(p); - else return; - redraw(); -}); - -// ── 끌어 순서 바꿈(줄 위 = 그 앞 · 항목 머리 = 그 항목 끝) -let dragFrom = null; -$("#edit").addEventListener("dragstart", (e) => { - dragFrom = e.target.dataset?.loc?.split(".").map(Number) ?? null; - e.dataTransfer?.setData("text/plain", "line"); -}); -$("#edit").addEventListener("dragover", (e) => { - if (dragFrom && e.target.closest(".line, [data-drop]")) e.preventDefault(); -}); -$("#edit").addEventListener("drop", (e) => { - if (!dragFrom) return; - e.preventDefault(); - const [fi, fj] = dragFrom; - dragFrom = null; - const ln = doc.items[fi].lines.splice(fj, 1)[0]; - const card = e.target.closest(".line"); - if (card) { - let [ti, tj] = card.dataset.loc.split(".").map(Number); - if (ti === fi && tj > fj) tj--; - doc.items[ti].lines.splice(tj, 0, ln); - } else { - doc.items[Number(e.target.closest("[data-drop]").dataset.drop)].lines.push(ln); - } - redraw(); -}); - -// ── 표에서 고르기(견본 표 하나) — 값 + 출처 꼬리표 -function openTable(p) { - const dlg = $("#tbl"); - dlg.querySelector("h3").textContent = TABLE_E.title; - dlg.querySelector("table").innerHTML = - `${TABLE_E.head.map((h) => `${esc(h)}`).join("")}` + - TABLE_E.rows - .map( - (r) => - `${esc(r[0])}${r - .slice(1) - .map((v, c) => - v == null - ? "" - : ``, - ) - .join("")}`, - ) - .join(""); - dlg.onclick = (e) => { - const b = e.target.closest("button[data-v]"); - if (b) { - const row = get(p); - row.x = b.dataset.v; - row.src = b.dataset.src; - dlg.close(); - redraw(); - } else if (e.target.closest("[data-close]")) dlg.close(); - }; - dlg.showModal(); -} - -// ── 엑셀 글 붙여넣기 -$("#pasteGo").onclick = () => { - const text = $("#paste").value; - if (!text.trim()) return void ($("#pasteMsg").textContent = "붙일 글이 없음"); - const { doc: d, expect: ex, stat } = parsePaste(text); - doc = d; - expect = ex; - expectFrom = "붙여넣은 블록"; - redraw(); - $("#pasteMsg").textContent = - `읽은 줄 ${stat.read} · 글 줄 ${stat.text}${ex ? "" : " · 머리 합계 없음(비교 안 함)"}`; -}; - -redraw(); diff --git a/tools/line-stack-test/calc.js b/tools/line-stack-test/calc.js deleted file mode 100644 index 47acde38e..000000000 --- a/tools/line-stack-test/calc.js +++ /dev/null @@ -1,182 +0,0 @@ -// 줄 쌓기 단가산출 시험 — 계산(화면 없음 · node 로도 돎) -// 끝수 = 거창 출력과 같은 자리: 줄 값 0.1 자리 버림 · 비목 합계 원 단위 버림 · 공식 변수(식인 줄) 소수 둘째 반올림 -export const COLS = ["l", "m", "e"]; -export const COL_NAME = { l: "노무비", m: "재료비", e: "경 비" }; - -const TOKEN = /\s*(?:(\d[\d,]*\.?\d*|\.\d+)|([A-Za-z가-힣_][A-Za-z0-9가-힣_]*)|([+\-*/×÷^()]))/y; - -export function tokenize(s) { - const out = []; - TOKEN.lastIndex = 0; - const src = String(s ?? "").trim(); - while (TOKEN.lastIndex < src.length) { - const at = TOKEN.lastIndex; - const m = TOKEN.exec(src); - if (!m || m.index !== at) throw new Error(`읽을 수 없는 글자: ${src.slice(at).trim()[0]}`); - if (m[1] !== undefined) out.push({ t: "num", v: Number(m[1].replace(/,/g, "")) }); - else if (m[2] !== undefined) out.push({ t: "var", n: m[2] }); - else out.push({ t: "op", v: m[3] }); - if (TOKEN.lastIndex >= src.length || !src.slice(TOKEN.lastIndex).trim()) break; - } - return out; -} - -/** 식 → 나무(괄호는 그대로 남겨 화면에 쓴 꼴대로 보임) */ -export function parse(s) { - const tk = tokenize(s); - if (!tk.length) throw new Error("빈 식"); - let i = 0; - const peek = () => tk[i]; - const isOp = (...ops) => peek()?.t === "op" && ops.includes(peek().v); - const add = () => { - let a = mul(); - while (isOp("+", "-")) a = { t: "bin", op: tk[i++].v, a, b: mul() }; - return a; - }; - const mul = () => { - let a = unary(); - while (isOp("*", "/", "×", "÷")) a = { t: "bin", op: tk[i++].v, a, b: unary() }; - return a; - }; - const unary = () => (isOp("-") ? (i++, { t: "neg", a: unary() }) : pow()); - const pow = () => { - const a = atom(); - return isOp("^") ? (i++, { t: "bin", op: "^", a, b: unary() }) : a; - }; - const atom = () => { - const k = tk[i++]; - if (!k) throw new Error("식이 덜 끝남"); - if (k.t === "num" || k.t === "var") return k; - if (k.v === "(") { - const a = add(); - if (!isOp(")")) throw new Error("괄호가 안 닫힘"); - i++; - return { t: "par", a }; - } - throw new Error(`「${k.v}」 자리가 틀림`); - }; - const ast = add(); - if (i < tk.length) throw new Error(`「${tk[i].v ?? tk[i].n}」 앞에서 식이 끊김`); - return ast; -} - -export function varsIn(ast, out = []) { - if (!ast) return out; - if (ast.t === "var") out.includes(ast.n) || out.push(ast.n); - for (const k of ["a", "b"]) if (ast[k]) varsIn(ast[k], out); - return out; -} - -export function evalAst(ast, vars) { - switch (ast.t) { - case "num": - return ast.v; - case "var": - if (!(ast.n in vars)) throw new Error(`없는 이름: ${ast.n}`); - return vars[ast.n]; - case "par": - return evalAst(ast.a, vars); - case "neg": - return -evalAst(ast.a, vars); - } - const a = evalAst(ast.a, vars); - const b = evalAst(ast.b, vars); - if (ast.op === "+") return a + b; - if (ast.op === "-") return a - b; - if (ast.op === "*" || ast.op === "×") return a * b; - if (ast.op === "^") return a ** b; - return a / b; -} - -export const floor1 = (x) => Math.floor(x * 10 + 1e-6) / 10; -export const floor0 = (x) => Math.floor(x + 1e-6); -export const round1 = (x) => Math.round(x * 10) / 10; -export const roundTo = (x, d) => Math.round(x * 10 ** d + 1e-9) / 10 ** d; - -/** 곱나눔 꼬리 하나 값(% 면 100 으로 나눔) */ -export function opValue(op, vars) { - const v = evalAst(parse(op.v), vars); - return op.u?.trim() === "%" ? v / 100 : v; -} - -export function applyOps(x, ops, vars) { - for (const op of ops ?? []) { - const v = opValue(op, vars); - x = op.op === "×" || op.op === "*" ? x * v : x / v; - } - return x; -} - -const isPlain = (ast) => ast.t === "num" || (ast.t === "neg" && ast.a.t === "num"); - -/** 단가산출 하나를 셈 — 줄마다 결과 · 항목 소계 · 합계 항(경쟁사 꼴) · 비목 합계 */ -export function compute(doc) { - const vars = {}; - const terms = { l: [], m: [], e: [] }; - const all = { l: 0, m: 0, e: 0 }; - const items = []; - const errors = []; - for (const item of doc.items ?? []) { - const it = { lines: [], sum: { l: 0, m: 0, e: 0 }, terms: { l: [], m: [], e: [] } }; - it.hasSub = (item.lines ?? []).some((ln) => ln.k === "mach" || ln.k === "ref"); - const put = (col, val, r) => { - it.sum[col] = round1(it.sum[col] + val); - it.terms[col].push(val); - r.cols[col] = val; - }; - for (const ln of item.lines ?? []) { - const r = { cols: {}, err: null, vars: { ...vars } }; - try { - if (ln.k === "formula") { - r.rows = (ln.rows ?? []).map((row) => calcRow(row, vars)); - } else if (ln.k === "mach" || ln.k === "ref") { - for (const c of COLS) { - const price = Number(ln.res?.[c]) || 0; - if (price) put(c, floor1(applyOps(price, ln.cops?.[c] ?? ln.ops, vars)), r); - } - } else if (ln.k === "labor" || ln.k === "mat") { - const col = ln.k === "labor" ? "l" : ln.col || "m"; - put(col, floor1(applyOps(Number(ln.res?.price) || 0, ln.ops, vars)), r); - } else if (ln.k === "rate") { - const bc = ln.baseCol || "l"; - const base = round1(ln.scope === "all" ? all[bc] + it.sum[bc] : it.sum[bc]); - r.base = base; - if (ln.baseName && ln.scope !== "all") it.terms[bc] = base ? [base] : []; - put(ln.col || "m", floor1((base * Number(ln.rate || 0)) / 100), r); - } - } catch (e) { - r.err = e.message; - errors.push(e.message); - } - it.lines.push(r); - } - for (const c of COLS) { - all[c] = round1(all[c] + it.sum[c]); - terms[c].push(...(it.hasSub ? (it.sum[c] ? [it.sum[c]] : []) : it.terms[c])); - } - items.push(it); - } - const tot = { l: floor0(all.l), m: floor0(all.m), e: floor0(all.e) }; - tot.sum = tot.l + tot.m + tot.e; - return { items, terms, all, tot, errors, vars }; -} - -/** 공식 줄 한 칸 — 「이름 = 식」 · 없는 이름은 unknown 으로(빨강) */ -export function calcRow(row, vars) { - const r = { val: null, err: null, unknown: [], env: { ...vars } }; - if (!row.x?.toString().trim()) return r; - try { - r.ast = parse(row.x); - r.unknown = varsIn(r.ast).filter((n) => !(n in vars)); - if (r.unknown.length) { - r.err = `없는 이름: ${r.unknown.join(", ")}`; - return r; - } - const v = evalAst(r.ast, vars); - r.val = isPlain(r.ast) ? v : roundTo(v, row.d ?? 2); - if (row.n?.trim()) vars[row.n.trim()] = r.val; - } catch (e) { - r.err = e.message; - } - return r; -} diff --git a/tools/line-stack-test/check.mjs b/tools/line-stack-test/check.mjs deleted file mode 100644 index 7fe0fd767..000000000 --- a/tools/line-stack-test/check.mjs +++ /dev/null @@ -1,11 +0,0 @@ -// node tools/line-stack-test/check.mjs — 견본 셋 금액 = 거창 출력 숫자(원 단위) -import { compute } from "./calc.js"; -import { SAMPLES } from "./samples.js"; -let bad = 0; -for (const s of SAMPLES.filter((x) => x.expect)) { - const { tot, errors } = compute(s.doc); - const ok = ["l", "m", "e", "sum"].every((c) => tot[c] === s.expect[c]) && !errors.length; - if (!ok) bad++; - console.log(ok ? "같음" : "다름", s.label, JSON.stringify(tot), errors.join(" ")); -} -process.exit(bad ? 1 : 0); diff --git a/tools/line-stack-test/edit.js b/tools/line-stack-test/edit.js deleted file mode 100644 index d8f9cce74..000000000 --- a/tools/line-stack-test/edit.js +++ /dev/null @@ -1,165 +0,0 @@ -// 줄 쌓기 단가산출 시험 — 왼쪽 줄 쌓기 편집(종류마다 필요한 칸만 · 칸은 data-p 경로로 doc 에 바로 씀) -import { LABOR, MACHINE, MATERIAL, SANGEUN, TEMPLATES } from "./samples.js"; -import { esc } from "./render.js"; - -export const KINDS = { - mach: "① 기계", - ref: "② 다른 단산 참조", - labor: "③ 인력", - rate: "④ 율", - mat: "⑤ 재료", - formula: "⑥ 공식", - text: "⑦ 글 · 근거", -}; -const COLSEL = { l: "노무비", m: "재료비", e: "경비" }; - -const inp = (p, v, cls = "", ph = "") => - ``; -const sel = (p, v, opts) => - ``; -const pick = (list, act, label) => - ``; - -export function allTemplates(mine) { - return [...TEMPLATES, ...mine]; -} - -/** 「+ 줄」 고르개 — 공식은 틀마다 하나씩 */ -export function addLineSelect(i, mine) { - const tpls = allTemplates(mine) - .map((t) => ``) - .join(""); - return ``; -} - -function opsEdit(p, ops) { - const one = (o, k) => - `${sel(`${p}.ops.${k}.op`, o.op, { "×": "×", "÷": "÷", "/": "/" })}${inp( - `${p}.ops.${k}.v`, - o.v, - "v", - "값 · 이름", - )}${inp(`${p}.ops.${k}.u`, o.u, "u", "단위")}`; - return `
${(ops ?? []).map(one).join("")}
`; -} - -const copsNote = (ln) => - ln.cops - ? `
비목 따로 꼬리(붙여넣기): ${Object.entries(ln.cops) - .map( - ([c, ops]) => - `${COLSEL[c]} ${ops.map((o) => `${o.op} ${esc(o.v)} ${esc(o.u)}`).join(" ")}`, - ) - .join(" · ")}
` - : ""; - -/** 공식 줄 — 접힘 = 값 칸만 · 풀림 = 「이름 = 식」 글 줄 전부 */ -function formulaEdit(p, ln, r, mine) { - const tpl = allTemplates(mine).find((t) => t.id === ln.tpl); - const changed = tpl && JSON.stringify(shape(tpl.rows)) !== JSON.stringify(shape(ln.rows)); - const label = !tpl - ? ln.tpl === null && !ln.rows?.some((x) => x.x) - ? "새 공식" - : "내 공식" - : changed - ? `내 틀 (${esc(tpl.name)} 에서 변경)` - : esc(tpl.name); - const rows = (ln.rows ?? []) - .map((row, k) => { - const rr = r?.rows?.[k] ?? {}; - const rp = `${p}.rows.${k}`; - const plain = /^-?[\d.]+$/.test(String(row.x ?? "").trim()); - const val = `${rr.err ? esc(rr.err) : (rr.val ?? "")}`; - const src = row.src ? `표` : ""; - const table = ``; - if (!ln.open) { - if (plain || !row.x) - return `
${esc(row.n || "?")} = ${inp(`${rp}.x`, row.x, "num")} ${esc(row.u)} ${table}${src}
`; - return `
${esc(row.n)} = ${esc(row.x)} = ${val} ${esc(row.u)}
`; - } - return `
${inp(`${rp}.n`, row.n, "name", "이름")} = ${inp(`${rp}.x`, row.x, `expr ${rr.err ? "bad" : ""}`, "값 또는 식")}${inp( - `${rp}.u`, - row.u, - "u", - "단위", - )}${val}${table}${src}
`; - }) - .join(""); - return `
${label} - -
${rows} - ${ln.open ? `` : ""}`; -} - -/** 틀과 견줄 꼴 — 값 줄은 이름만 · 식 줄은 이름과 식 */ -export function shape(rows) { - return (rows ?? []).map((x) => - /^-?[\d.]+$/.test(String(x.x).trim()) ? [x.n] : [x.n, String(x.x).replace(/\s/g, "")], - ); -} - -function lineBody(p, ln, r, mine) { - const price3 = () => - `
노무 ${inp(`${p}.res.l`, ln.res?.l, "num")} 재료 ${inp(`${p}.res.m`, ln.res?.m, "num")} 경비 ${inp( - `${p}.res.e`, - ln.res?.e, - "num", - )}
`; - switch (ln.k) { - case "mach": - return `
${pick(MACHINE, `res:mach:${p}`, "견본 기계 ▾")}${inp(`${p}.res.name`, ln.res?.name, "wide", "기계 이름")}
${price3()}${opsEdit(p, ln.ops)}${copsNote(ln)}`; - case "ref": - return `
${pick(SANGEUN, `res:ref:${p}`, "견본 단산 ▾")} 단산 ${inp(`${p}.res.no`, ln.res?.no, "num s")}${inp( - `${p}.res.name`, - ln.res?.name, - "wide", - "단가산출 이름", - )}
${price3()}${opsEdit(p, ln.ops)}${copsNote(ln)}`; - case "labor": - case "mat": - return `
${pick(ln.k === "labor" ? LABOR : MATERIAL, `res:${ln.k}:${p}`, ln.k === "labor" ? "견본 노임 ▾" : "견본 자재 ▾")}${inp( - `${p}.res.name`, - ln.res?.name, - "wide", - "이름", - )} 단가 ${inp(`${p}.res.price`, ln.res?.price, "num")}${ln.k === "mat" ? ` → ${sel(`${p}.col`, ln.col || "m", COLSEL)}` : ""}
${opsEdit(p, ln.ops)}`; - case "rate": - return `
${inp(`${p}.name`, ln.name, "wide", "이름(예: 공구손료 및 경장비)")}
-
기준 ${sel(`${p}.scope`, ln.scope || "item", { item: "이 항목 위 줄", all: "위 전부" })} 의 ${sel(`${p}.baseCol`, ln.baseCol || "l", COLSEL)} - × ${inp(`${p}.rate`, ln.rate, "num s")} % → ${sel(`${p}.col`, ln.col || "m", COLSEL)}
-
글 「( ${inp(`${p}.of`, ln.of, "s2", "노무비")}의 % )」 · 소계 이름 ${inp(`${p}.baseName`, ln.baseName, "s2", "비우면 안 찍음")}
`; - case "formula": - return formulaEdit(p, ln, r, mine); - case "text": - return inp(`${p}.text`, ln.text, "full", "< 품셈 근거 > · ※ 메모 · 별산"); - } - return ""; -} - -export function renderEdit(doc, res, mine) { - const items = (doc.items ?? []) - .map((item, i) => { - const lines = (item.lines ?? []) - .map((ln, j) => { - const p = `items.${i}.lines.${j}`; - const r = res.items[i]?.lines[j]; - return `
⠿${KINDS[ln.k]}${ - r?.err ? `${esc(r.err)}` : "" - }
${lineBody(p, ln, r, mine)}
`; - }) - .join(""); - return `
${inp(`items.${i}.title`, item.title, "title", "항목 이름(비우면 번호 없음)")} - ${addLineSelect(i, mine)} -
${lines || '
줄 없음 — [+ 줄 ▾]
'}
`; - }) - .join(""); - return `
이름 ${inp("name", doc.name, "wide")} 단위 ${inp("unit", doc.unit, "s")}
${items}`; -} diff --git a/tools/line-stack-test/index.html b/tools/line-stack-test/index.html deleted file mode 100644 index 9399c3535..000000000 --- a/tools/line-stack-test/index.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - 시험: 줄 쌓기 단가산출 - - - - -
-

시험: 줄 쌓기 단가산출

- 서버 없음 · 저장은 이 브라우저에만 · 마음에 안 들면 폴더째 지움 - - -
-
-
-
-
-
-
-
-
- - 엑셀 글 붙여넣기 — 경쟁사 「단가산출」 시트 한 블록(머리 줄부터 「계 :」 줄까지) - - - -
-
- -

-
- -
- - - diff --git a/tools/line-stack-test/paste.js b/tools/line-stack-test/paste.js deleted file mode 100644 index 8118e1b6f..000000000 --- a/tools/line-stack-test/paste.js +++ /dev/null @@ -1,257 +0,0 @@ -import { compute } from "./calc.js"; -// 줄 쌓기 단가산출 시험 — 엑셀 「단가산출」 블록 글 → 줄(일곱 종류로 읽히는 만큼 · 못 읽는 줄은 글 줄) -const NUM = /^-?[\d,]*\.?\d+\.?$|^-?\d[\d,]*\.$/; -const isNum = (t) => NUM.test(t); -const num = (t) => Number(String(t).replace(/,/g, "").replace(/\.$/, "")); -const clean = (t) => - String(t) - .replace(/(\d)\.(?!\d)/g, "$1") - .replace(/,(?=\d{3})/g, ""); -const OPS = ["×", "÷", "/", "*"]; -const KEY = { 노무비: "l", 재료비: "m", 경비: "e" }; - -/** 「× 4 인 ÷ 170 ㎡」 → 꼬리 [{op, v, u}] · 괄호 등 못 읽으면 null */ -export function parseOps(s, known = {}) { - const tk = s.trim().split(/\s+/).filter(Boolean); - const ops = []; - let i = 0; - while (i < tk.length) { - const op = tk[i] === "*" ? "×" : tk[i]; - let v = tk[i + 1]; - if (!OPS.includes(tk[i]) || v === undefined) return null; - if (isNum(v)) { - v = clean(v); - // 값이 위 공식의 Q · QH 와 같으면 이름으로 이음(틀을 고치면 따라 바뀜) - const hit = Object.entries(known).find(([n, val]) => /^Q/.test(n) && String(val) === v); - if (hit) v = hit[0]; - } else if (!/^[A-Za-z]\w*$/.test(v)) return null; - i += 2; - const u = []; - while (i < tk.length && !OPS.includes(tk[i])) u.push(tk[i++]); - if (u.some((x) => /[()]/.test(x))) return null; - ops.push({ op, v, u: u.join(" ") }); - } - return ops; -} - -/** 「q = 0.7 ㎥ K = 0.55 f = 1. / 1.325 = 0.75」 → 줄 [{n, x, u}] */ -export function parseDefs(line) { - const re = /(^|\s)([A-Za-z][A-Za-z0-9]*)\s*=\s/g; - const starts = []; - let m; - while ((m = re.exec(line))) { - const before = line.slice(starts.length ? starts.at(-1).body : 0, m.index).trim(); - if (starts.length && /[+\-×÷*/(]$/.test(before)) continue; - starts.push({ n: m[2], at: m.index, body: m.index + m[0].length }); - } - return starts.map((s, i) => { - const seg = line.slice(s.body, starts[i + 1]?.at ?? line.length).trim(); - const parts = seg.split(/\s=\s/); - const last = parts.at(-1).trim(); - const lm = last.match(/^(-?[\d,]*\.?\d+\.?)\s*(.*)$/); - if (parts.length === 1) - return { n: s.n, x: lm ? clean(lm[1]) : clean(last), u: lm ? lm[2] : "" }; - return { n: s.n, x: clean(parts[0]), u: lm ? lm[2] : "", got: lm ? num(lm[1]) : null }; - }); -} - -/** 율 머리 「이름 ( 노무비의 1 % )」 + 다음 줄 「38,290.7 × 1. % = 382.9」 → 율 줄 */ -function rateHead(s, next) { - const m = s.match(/^(.*?)\s*\(\s*(\S+?)의\s*([\d.]+)\s*%\s*\)\s*$/); - if (!m || !/×\s*[\d.]+\s*%\s*=/.test(next ?? "")) return null; - const bc = /노무/.test(m[2]) ? "l" : /경비/.test(m[2]) ? "e" : "m"; - return { k: "rate", name: m[1], of: m[2], baseCol: bc, rate: String(num(m[3])), col: "m" }; -} - -/** 분수 세 줄(분자 · 「Q = ─ … = 23.39 ㎥ / hr」 · 분모) → 줄 하나 · 기호 반 · 값 반으로 나눔 */ -function parseFrac(top, mid, bot) { - const mm = mid.match(/^([A-Za-z][A-Za-z0-9]*)\s*=\s*(.*)$/); - if (!mm) return null; - const parts = mm[2].split(/\s=\s/); - const sym = parts[0]; - const groups = sym.match(/─(\s*─)*/g) ?? []; - const half = (s) => { - const t = s.trim().split(/\s+/); - return t.length % 2 ? null : t.slice(0, t.length / 2); - }; - const a = half(top); - const b = half(bot); - if (!a || !b || !groups.length) return null; - if (groups.length > 1 && (a.length !== groups.length || b.length !== groups.length)) return null; - let k = 0; - const x = sym.replace(/─(\s*─)*/g, () => { - const [na, nb] = groups.length > 1 ? [a[k], b[k]] : [a.join(" "), b.join(" ")]; - k++; - return `(${na}) / (${nb})`; - }); - const tail = parts - .at(-1) - .trim() - .match(/^(-?[\d,]*\.?\d+\.?)\s*(.*)$/); - return { - n: mm[1], - x: clean(x), - u: tail && parts.length > 1 ? tail[2] : "", - got: tail ? num(tail[1]) : null, - }; -} - -export function parsePaste(text) { - const lines = String(text) - .replace(/\t/g, " ") - .split(/\r?\n/) - .map((s) => s.replace(/\s+/g, " ").trim()); - const doc = { name: "붙여넣은 단가산출", unit: "", items: [] }; - let expect = null; - let item = null; - let summary = false; - let pendingBase = ""; - let skipRate = false; - const known = {}; - const sums = { l: [], m: [], e: [] }; - const stat = { read: 0, text: 0 }; - const cur = () => item ?? ((item = { title: "", lines: [] }), doc.items.push(item), item); - const add = (ln) => (cur().lines.push(ln), ln.k === "text" ? stat.text++ : stat.read++); - const formula = () => { - const last = cur().lines.at(-1); - if (last?.k === "formula") return last; - const f = { k: "formula", tpl: null, rows: [] }; - add(f); - return f; - }; - const addRow = (r) => { - formula().rows.push({ n: r.n, x: r.x, u: r.u }); - if (r.got != null) known[r.n] = String(r.got); - else if (isNum(r.x)) known[r.n] = r.x; - }; - for (let i = 0; i < lines.length; i++) { - const s = lines[i]; - if (!s || /^단가산출$|^산 ?출 ?근 ?거/.test(s)) continue; - let m = s.match(/^(\d+)\.(\S.*?)\[(.+?)\]\s*([\d,]+)?\s*([\d,]+)?\s*([\d,]+)?\s*([\d,]+)?\s*$/); - if (m && !doc.items.length) { - doc.name = m[2].trim(); - doc.unit = m[3]; - if (m[4]) - expect = { sum: num(m[4]), l: num(m[5] ?? 0), m: num(m[6] ?? 0), e: num(m[7] ?? 0) }; - continue; - } - // 분수 세 줄 - if (lines[i + 1]?.includes("─")) { - const r = parseFrac(s, lines[i + 1], lines[i + 2] ?? ""); - if (r) { - addRow(r); - i += 2; - continue; - } - for (const t of [s, lines[i + 1], lines[i + 2] ?? ""]) add({ k: "text", text: t }); - i += 2; - continue; - } - if ((m = s.match(/^(\d+)\s*\.\s+(.*)$/)) || (m = s.match(/^([가-하])\s*\.\s+(.*)$/))) { - summary = /^(합계|소계)$/.test(m[2].replace(/\s/g, "")); - if (summary) continue; - item = { title: m[2], lines: [], ...(/[가-하]/.test(m[1]) ? { sub: true } : {}) }; - doc.items.push(item); - const rm = rateHead(m[2], lines[i + 1]); - if (rm) { - add({ ...rm, name: "", scope: "all", baseName: "" }); - pendingBase = ""; - i++; - } - continue; - } - const key = s.split(":")[0].replace(/\s/g, ""); - if (summary && /^(노무비|재료비|경비|계)$/.test(key)) { - if (KEY[key]) { - const nums = - s - .slice(s.indexOf(":") + 1) - .split("=")[0] - .match(/[\d,]+\.?\d*/g) ?? []; - sums[KEY[key]].push(...nums.map(num)); - } - continue; - } - if (/^소계$/.test(key)) continue; - if (s.includes(":") && /소계$/.test(key)) { - pendingBase = key.replace(/소계$/, ""); - continue; - } - // 율 줄 머리 「이름 ( 노무비의 1 % )」 + 다음 줄 「38,290.7 × 1. % = 382.9」 - const rm = rateHead(s, lines[i + 1]); - if (rm) { - add({ ...rm, baseName: pendingBase, scope: "item" }); - pendingBase = ""; - i++; - continue; - } - // 「q = 0.7 K = 0.55」 값 · 식 줄 - if (/^[A-Za-z][A-Za-z0-9]*\s*=\s/.test(s) && !s.includes(":")) { - parseDefs(s).forEach(addRow); - continue; - } - // 「노무비 : 59,020 ÷ 23.39 = 2,523.3」 셋 → 기계 · 참조 줄 - if (KEY[key] && s.includes(":")) { - const rhs = s - .slice(s.indexOf(":") + 1) - .split("=")[0] - .trim(); - const pm = rhs.match(/^([\d,]+\.?)\s+(.*)$/); - const ops = pm && parseOps(pm[2], known); - if (!pm || !ops) { - if (rhs) add({ k: "text", text: s }); - continue; - } - const c = KEY[key]; - let last = cur().lines.at(-1); - if (!( - last && - (last.k === "mach" || last.k === "ref") && - !(c in last.res) && - last.res._open - )) { - const ref = (item?.title ?? "").match(/단산\s*(\d+)\s*참조/); - last = ref - ? { k: "ref", res: { no: Number(ref[1]), name: item.title, _open: true }, ops } - : { k: "mach", res: { name: item?.title ?? "", _open: true }, ops }; - add(last); - } - last.res[c] = num(pm[1]); - if (JSON.stringify(ops) !== JSON.stringify(last.ops)) (last.cops ??= {})[c] = ops; - continue; - } - // 「치즐 ( 223,000. × 0.006 ) ÷ 5.」 → 괄호 걷음(곱나눔 차례 그대로라 값 같음) - const s2 = s.replace(/\(\s*([\d,]+\.?\s+[×÷][^()]*?)\s*\)/, "$1"); - // 「보통인부 : 172,068 ÷ 8. ÷ 23.39 = 919.5」 → 인력 - if ((m = s2.match(/^([^:]+?)\s*:\s*([\d,]+\.?)\s+([×÷/*].*?)\s*=\s*[\d,.]+/))) { - const ops = parseOps(m[3], known); - if (ops) { - add({ k: "labor", res: { name: m[1], price: num(m[2]) }, ops }); - continue; - } - } - // 「합판(내수),대전 (…) 11,455. × 1.03 ㎡ × 38. % = 4,483.4」 → 재료 - if ((m = s2.match(/^(.*\S)\s+([\d,]+\.?)\s+([×÷/*].*?)\s*=\s*[\d,.]+/)) && !s.includes(":")) { - const ops = parseOps(m[3], known); - if (ops) { - add({ k: "mat", col: "m", res: { name: m[1], price: num(m[2]) }, ops }); - continue; - } - } - add({ k: "text", text: s }); - } - for (const it of doc.items) for (const ln of it.lines) if (ln.res) delete ln.res._open; - // 율 줄이 든 비목 = 합계 줄에 그 값이 놓인 칸 - const res = compute(doc); - doc.items.forEach((it, ii) => - it.lines.forEach((ln, li) => { - if (ln.k !== "rate") return; - const v = Object.values(res.items[ii].lines[li].cols)[0]; - const col = - ["m", "e", "l"].find((c) => sums[c].includes(v)) ?? - ["m", "e", "l"].find((c) => sums[c].includes(Math.floor(v))); - if (col) ln.col = col; - }), - ); - return { doc, expect, stat }; -} diff --git a/tools/line-stack-test/render.js b/tools/line-stack-test/render.js deleted file mode 100644 index 9335d4667..000000000 --- a/tools/line-stack-test/render.js +++ /dev/null @@ -1,181 +0,0 @@ -// 줄 쌓기 단가산출 시험 — 산출근거 출력(경쟁사 「단가산출」 시트 꼴 · 분수는 CSS 로 조판) -import { COLS, COL_NAME, parse, varsIn } from "./calc.js"; - -export const esc = (s) => - String(s ?? "").replace( - /[&<>"]/g, - (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c], - ); -export const fmtP = (n) => Math.round(n).toLocaleString("en-US"); -export const fmt1 = (n) => - n.toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 }); -export const fmtV = (n) => - n == null || Number.isNaN(n) ? "?" : n.toLocaleString("en-US", { maximumFractionDigits: 4 }); -const OP = { "*": "×", "/": "/", "×": "×", "÷": "÷", "+": "+", "-": "-", "^": "^" }; - -/** 식 나무 → 글(sym = 기호 · val = 값) · 기호가 든 식의 「/」 는 분수로 */ -export function showAst(ast, mode, env, frac = varsIn(ast).length > 0) { - const go = (a) => showAst(a, mode, env, frac); - const bare = (a) => (a.t === "par" ? go(a.a) : go(a)); - switch (ast.t) { - case "num": - return fmtV(ast.v); - case "var": - return mode === "sym" - ? esc(ast.n) - : ast.n in env - ? fmtV(env[ast.n]) - : `${esc(ast.n)}`; - case "par": - return `( ${go(ast.a)} )`; - case "neg": - return `-${go(ast.a)}`; - } - if (ast.op === "/" && frac) - return `${bare(ast.a)}${bare(ast.b)}`; - return `${go(ast.a)} ${OP[ast.op]} ${go(ast.b)}`; -} - -const opsText = (ops, env) => - (ops ?? []) - .map((o) => { - let v; - try { - v = showAst(parse(o.v), "val", env); - } catch { - v = `${esc(o.v)}`; - } - return `${esc(OP[o.op] ?? o.op)} ${v}${o.u ? " " + esc(o.u) : ""}`; - }) - .join(" "); - -/** 공식 줄 — 값 줄은 넷까지 한 줄에 · 식 줄은 「이름 = 기호식 = 값식 = 결과 단위」 */ -function formulaRows(ln, r, push) { - let pack = []; - const flush = () => - pack.length && (push({ h: pack.join('') }), (pack = [])); - (ln.rows ?? []).forEach((row, i) => { - const rr = r.rows?.[i]; - if (!row.n?.trim() && !row.x?.toString().trim()) return; - const u = row.u ? " " + esc(row.u) : ""; - const name = esc(row.n || "?"); - if (!rr?.ast || rr.err) { - flush(); - return push({ h: `${name} = ${esc(row.x)} ${esc(rr?.err ?? "")}` }); - } - const plain = rr.ast.t === "num"; - const hasVar = varsIn(rr.ast).length > 0; - if (!hasVar) { - pack.push( - `${name} = ${showAst(rr.ast, "sym", rr.env)}${plain ? "" : " = " + fmtV(rr.val)}${u}`, - ); - if (pack.length >= 4) flush(); - return; - } - flush(); - push({ - h: `${name} = ${showAst(rr.ast, "sym", rr.env)} = ${showAst(rr.ast, "val", rr.env)} = ${fmtV(rr.val)}${u}`, - tall: /class="fr"/.test(showAst(rr.ast, "sym", rr.env)), - }); - }); - flush(); -} - -/** 출력 줄 목록 — {h, cols?, cls?} */ -export function outRows(doc, res) { - const rows = []; - const push = (r) => rows.push(r); - push({ - h: `${esc(doc.name)}${doc.unit ? ` [${esc(doc.unit)}]` : ""}`, - cols: { ...res.tot }, - cls: "head", - won: true, - }); - let n = 0; - let sub = 0; - (doc.items ?? []).forEach((item, ii) => { - const it = res.items[ii]; - if (item.title?.trim()) { - const label = item.sub ? `${"가나다라마바사아자차카타파하"[sub++] ?? "?"} . ` : `${++n}. `; - if (!item.sub) sub = 0; - push({ h: label + esc(item.title), cls: item.sub ? "item sub" : "item" }); - } - // 율 줄이 소계 이름을 가지면 그 앞 같은 비목 줄은 칸 값을 안 찍음(소계 줄이 찍음) - const hideCol = (li, col) => - item.lines.some( - (x, j) => - j > li && x.k === "rate" && x.baseName && x.scope !== "all" && (x.baseCol || "l") === col, - ); - item.lines.forEach((ln, li) => { - const r = it.lines[li]; - if (r.err) return push({ h: `${esc(r.err)}`, err: true }); - if (ln.k === "text") return push({ h: esc(ln.text) }); - if (ln.k === "formula") return formulaRows(ln, r, push); - if (ln.k === "mach" || ln.k === "ref") { - for (const c of COLS) { - if (r.cols[c] === undefined) continue; - push({ - h: `${COL_NAME[c]} : ${fmtP(ln.res[c])} ${opsText(ln.cops?.[c] ?? ln.ops, r.vars)} = ${fmt1(r.cols[c])}`, - }); - } - return; - } - if (ln.k === "labor" || ln.k === "mat") { - const col = ln.k === "labor" ? "l" : ln.col || "m"; - const v = r.cols[col]; - const sep = ln.k === "labor" ? " :" : ""; - return push({ - h: `${esc(ln.res?.name)}${sep} ${fmtP(Number(ln.res?.price) || 0)} ${opsText(ln.ops, r.vars)} = ${fmt1(v)}`, - cols: hideCol(li, col) ? null : { sum: v, [col]: v }, - }); - } - if (ln.k === "rate") { - const col = ln.col || "m"; - const bc = ln.baseCol || "l"; - if (ln.baseName && ln.scope !== "all") - push({ - h: `${esc(ln.baseName)}소계 : ${fmt1(r.base)}`, - cols: { sum: r.base, [bc]: r.base }, - cls: "subsum", - }); - push({ h: `${esc(ln.name)} ( ${esc(ln.of || COL_NAME[bc])}의 ${esc(ln.rate)} % )` }); - push({ - h: `${fmt1(r.base)} × ${esc(ln.rate)} % = ${fmt1(r.cols[col])}`, - cols: { sum: r.cols[col], [col]: r.cols[col] }, - }); - } - }); - if (it.hasSub && COLS.some((c) => it.sum[c])) { - const s = Math.round((it.sum.l + it.sum.m + it.sum.e) * 10) / 10; - push({ h: `소 계 : ${fmt1(s)}`, cols: { sum: s, ...it.sum }, cls: "subsum" }); - } - }); - push({ h: `${++n}. 합 계`, cls: "item" }); - for (const c of COLS) { - const t = res.terms[c]; - const body = - t.length > 1 - ? `${t.map(fmt1).join(" + ")} = ${fmtP(res.tot[c])}` - : t.length - ? fmtP(res.tot[c]) - : ""; - push({ h: `${COL_NAME[c]} : ${body}` }); - } - push({ h: `계 : ${fmtP(res.tot.sum)}`, cols: { ...res.tot }, cls: "total", won: true }); - return rows; -} - -export function renderOut(doc, res) { - const cell = (r, k) => { - const v = r.cols?.[k]; - if (v === undefined || v === null || (r.won && !v && k !== "sum")) return ""; - return `${r.won ? fmtP(v) : fmt1(v)}`; - }; - const body = outRows(doc, res) - .map( - (r) => - `${r.h}${["sum", ...COLS].map((k) => cell(r, k)).join("")}`, - ) - .join(""); - return `${body}
산 출 근 거합 계노무비재료비경 비
`; -} diff --git a/tools/line-stack-test/samples.js b/tools/line-stack-test/samples.js deleted file mode 100644 index 4e5d9eb69..000000000 --- a/tools/line-stack-test/samples.js +++ /dev/null @@ -1,221 +0,0 @@ -// 줄 쌓기 단가산출 시험 — 견본 자원 · 공식 틀 · 견본 표 · 견본 단가산출(거창 산불 출력 「단가산출」 시트 1 · 41 · 15 번) - -export const LABOR = [ - { name: "보통인부", price: 172068 }, - { name: "형틀목공", price: 275790 }, - { name: "석공", price: 268908 }, -]; - -export const MATERIAL = [ - { name: "합판(내수),대전 (12t×1220×2440mm)", price: 11455 }, - { name: "외송각재,대전 (30×30×3600mm)", price: 586824 }, - { name: "치즐(0.7㎥)", price: 223000 }, -]; - -/** 기계 = 시간당 기계경비 노무 · 재료 · 경비 */ -export const MACHINE = [ - { name: "굴삭기(무한궤도) (0.7㎥)", l: 59020, m: 19473, e: 24554 }, - { name: "굴삭기(무한궤도) (0.6㎥+부착용집게)", l: 59020, m: 16280, e: 29209 }, - { name: "덤프트럭 (15Ton)", l: 59020, m: 30192, e: 20659 }, -]; - -/** 다른 단가산출 = 그 단산의 노무 · 재료 · 경비 */ -export const SANGEUN = [ - { no: 13, name: "뒷채움돌 기초잡석 채집운반(Φ150m/m) [㎡]", l: 9889, m: 2262, e: 3858 }, - { no: 28, name: "기초다짐및뒷채움(소형장비사용) [㎥]", l: 11206, m: 1077, e: 1259 }, - { no: 1, name: "측구터파기(토사) [㎥]", l: 3442, m: 832, e: 1049 }, -]; - -const row = (n, x, u = "", d) => (d === undefined ? { n, x, u } : { n, x, u, d }); - -/** 공식 틀 — 「이름 = 값」 · 「이름 = 식」 줄 묶음 */ -export const TEMPLATES = [ - { - id: "excavator", - name: "굴삭기 Q (품셈 8-2-3)", - rows: [ - row("q", "0.7", "㎥"), - row("K", "0.55"), - row("f", "1 / 1.325"), - row("E", "0.45"), - row("Cm", "20", "sec ( 135 ˚ )"), - row("Q", "3600 × q × K × f × E / Cm", "㎥ / hr"), - ], - }, - { - id: "dump", - name: "덤프 Q (q · K 는 위 굴삭기 것)", - rows: [ - row("L", "0.1", "km"), - row("V1", "7", "km / hr"), - row("V2", "8", "km / hr"), - row("T", "15", "ton"), - row("rt", "1.9", "ton / ㎥"), - row("E", "0.9"), - row("qt", "T ÷ rt", "㎥"), - row("f", "0.89"), - row("Cms", "22"), - row("Es", "0.35"), - row("n", "qt / (q × K)"), - row("t1", "Cms × n / (60 × Es)"), - row("t2", "(L / V1 + L / V2) × 60", "분 ( 왕복 )"), - row("t3", "1.1", "분 ( 적하 )"), - row("t4", "0.7", "분 ( 대기 )"), - row("Cm", "t1 + t2 + t3 + t4", "분"), - row("QH", "(Cm - t1) / Cm"), - row("Q", "60 × qt × f × E / Cm", "㎥ / hr"), - ], - }, - { id: "directQ", name: "직접 Q", rows: [row("Q", "0.35")] }, -]; - -/** 견본 표 하나 — [표에서 고르기] (표준품셈 8-2-3 굴착기 2. 작업효율 E) */ -export const TABLE_E = { - title: "굴착기 작업효율 E (표준품셈 8-2-3)", - head: ["토질", "자연 양호", "자연 보통", "자연 불량", "흐트 양호", "흐트 보통", "흐트 불량"], - rows: [ - ["모래, 사질토", 0.85, 0.7, 0.55, 0.9, 0.75, 0.6], - ["자갈섞인 흙, 점성토", 0.75, 0.6, 0.45, 0.8, 0.65, 0.5], - ["파쇄암", null, null, null, null, 0.45, 0.35], - ], -}; - -const op = (o, v, u = "") => ({ op: o, v, u }); -const tplRows = (id) => structuredClone(TEMPLATES.find((t) => t.id === id).rows); - -export const SAMPLES = [ - { - key: "s1", - label: "① 측구터파기(토사)", - expect: { l: 3442, m: 832, e: 1049, sum: 5323 }, - doc: { - name: "측구터파기(토사)", - unit: "㎥", - items: [ - { - title: "기 계 ( 100 % ) : 굴삭기(무한궤도) (0.7㎥)", - lines: [ - { k: "formula", tpl: "excavator", rows: tplRows("excavator") }, - { k: "mach", res: { ...MACHINE[0] }, ops: [op("÷", "Q")] }, - ], - }, - { - title: "보조인부 ( 1 일당 1 인 )", - lines: [{ k: "labor", res: { ...LABOR[0] }, ops: [op("÷", "8"), op("÷", "Q")] }], - }, - ], - }, - }, - { - key: "s41", - label: "② 합판거푸집(4회)", - expect: { l: 38290, m: 14506, e: 0, sum: 52796 }, - doc: { - name: "합판거푸집(4회)", - unit: "㎡", - items: [ - { - title: "", - lines: [ - { k: "text", text: "< 토목품셈 6-3-1 합판거푸집(2022개정) >" }, - { k: "text", text: "※ 사용횟수 4 회 : 보통" }, - ], - }, - { - title: "재료비", - lines: [ - { - k: "mat", - col: "m", - res: { ...MATERIAL[0] }, - ops: [op("×", "1.03", "㎡"), op("×", "38", "%")], - }, - { - k: "mat", - col: "m", - res: { ...MATERIAL[1] }, - ops: [op("×", "0.038", "㎥"), op("×", "38", "%")], - }, - { - k: "rate", - name: "소모자재 ( 박리재등 )", - of: "주자재비", - baseName: "주자재", - baseCol: "m", - scope: "item", - rate: "9", - col: "m", - }, - ], - }, - { - title: "인건비 ( 24 년 품셈 개정 )", - lines: [ - { k: "labor", res: { ...LABOR[1] }, ops: [op("×", "5", "인"), op("/", "45", "㎡")] }, - { k: "labor", res: { ...LABOR[0] }, ops: [op("×", "2", "인"), op("/", "45", "㎡")] }, - { - k: "rate", - name: "공구손료 및 경장비 기계경비", - of: "노무비", - baseName: "인부", - baseCol: "l", - scope: "item", - rate: "1", - col: "m", - }, - ], - }, - ], - }, - }, - { - key: "s15", - label: "③ 전석 메쌓기", - expect: { l: 74880, m: 7029, e: 12306, sum: 94215 }, - doc: { - name: "전석 메쌓기", - unit: "㎡", - items: [ - { title: "전석 구입운반 : 별산", lines: [] }, - { - title: "뒷채움돌 운반 및 채집 : 뒷채움돌기초잡석채집운반 (Φ150m/m) [㎡] * 단산 13 참조 *", - lines: [ - { k: "formula", tpl: "directQ", rows: [row("Q", "0.35")] }, - { k: "ref", res: { ...SANGEUN[0] }, ops: [op("×", "Q")] }, - ], - }, - { - title: "뒤채움돌 다짐 기초다짐및뒷채움 (소형장비사용) [㎥] * 단산 28 참조 *", - lines: [ - { k: "formula", tpl: "directQ", rows: [row("Q", "0.35")] }, - { k: "ref", res: { ...SANGEUN[1] }, ops: [op("×", "Q")] }, - ], - }, - { - title: "전석쌓기 기계: 굴삭기(무한궤도) (0.6㎥+부착용집게)", - lines: [ - { k: "formula", tpl: "directQ", rows: [row("Q", "0.36", "㎥ / hr")] }, - { k: "mach", res: { ...MACHINE[1] }, ops: [op("×", "Q")] }, - ], - }, - { - title: "전석쌓기 인부", - lines: [ - { k: "labor", res: { ...LABOR[2] }, ops: [op("×", "0.14", "인")] }, - { k: "labor", res: { ...LABOR[0] }, ops: [op("×", "0.05", "인")] }, - ], - }, - ], - }, - }, - { - key: "new", - label: "④ 새 공식", - expect: null, - doc: { - name: "새 단가산출", - unit: "", - items: [{ title: "", lines: [{ k: "formula", tpl: null, rows: [row("", "")] }] }], - }, - }, -]; diff --git a/tools/line-stack-test/style.css b/tools/line-stack-test/style.css deleted file mode 100644 index 16d1737c9..000000000 --- a/tools/line-stack-test/style.css +++ /dev/null @@ -1,306 +0,0 @@ -/* 시험 페이지 전용 — 본 프로그램 부품 상자 밖(폴더째 지움) */ -:root { - --bg: #f6f7f9; - --panel: #fff; - --line: #d5d9e0; - --text: #1d232b; - --muted: #6b7380; - --accent: #2f6fde; - --bad: #d23b3b; - --ok: #1f8a4c; - --chip: #eef2f8; - color-scheme: light dark; -} -@media (prefers-color-scheme: dark) { - :root { - --bg: #15181d; - --panel: #1e2228; - --line: #343a44; - --text: #e3e7ec; - --muted: #9aa3ae; - --accent: #6c9ef0; - --bad: #ff6b6b; - --ok: #4cc27f; - --chip: #2a3039; - } -} -* { - box-sizing: border-box; -} -body { - margin: 0; - font: - 13px/1.45 "Malgun Gothic", - system-ui, - sans-serif; - background: var(--bg); - color: var(--text); - display: flex; - flex-direction: column; - height: 100vh; -} -header, -footer { - padding: 6px 10px; - background: var(--panel); - border-bottom: 1px solid var(--line); - display: flex; - flex-wrap: wrap; - gap: 6px; - align-items: center; -} -footer { - border-top: 1px solid var(--line); - border-bottom: 0; -} -h1 { - font-size: 15px; - margin: 0 8px 0 0; -} -.hint, -#msg, -#pasteMsg, -.note, -i { - color: var(--muted); - font-size: 12px; -} -main { - flex: 1; - display: grid; - grid-template-columns: minmax(420px, 46%) 1fr; - min-height: 0; -} -#edit, -#outWrap { - overflow: auto; - padding: 8px; -} -#edit { - border-right: 1px solid var(--line); -} -button, -select, -input, -textarea { - font: inherit; - color: inherit; - background: var(--panel); - border: 1px solid var(--line); - border-radius: 4px; - padding: 2px 6px; -} -button { - cursor: pointer; - background: var(--chip); -} -button:hover { - border-color: var(--accent); -} -input.wide { - width: 260px; -} -input.num { - width: 80px; - text-align: right; -} -input.s { - width: 50px; -} -input.s2 { - width: 90px; -} -input.full, -input.title { - width: 100%; -} -input.title { - flex: 1; - font-weight: 600; -} -input.v { - width: 60px; -} -input.u { - width: 50px; -} -input.name { - width: 60px; -} -input.expr { - width: 220px; - font-family: Consolas, monospace; -} -.dochead, -.row, -.ih, -.lh, -.ops, -.frow, -.fhead { - display: flex; - flex-wrap: wrap; - gap: 4px; - align-items: center; - margin: 2px 0; -} -.item { - border: 1px solid var(--line); - border-radius: 6px; - background: var(--panel); - padding: 6px; - margin: 6px 0; -} -.line { - border-left: 3px solid var(--accent); - background: var(--bg); - padding: 4px 6px; - margin: 4px 0 4px 8px; - border-radius: 3px; -} -.line.k-text { - border-left-color: var(--muted); -} -.line.k-formula { - border-left-color: #a46bd8; -} -.line.k-rate { - border-left-color: #d8973b; -} -.grip { - cursor: grab; - padding: 0 4px; - user-select: none; -} -.kind { - font-weight: 600; -} -.lh button { - margin-left: auto; -} -.op { - display: inline-flex; - gap: 2px; - padding: 1px 3px; - background: var(--chip); - border-radius: 4px; -} -.tpl { - font-weight: 600; - color: #a46bd8; -} -.val { - min-width: 50px; - font-family: Consolas, monospace; -} -.src { - background: var(--ok); - color: #fff; - border-radius: 3px; - padding: 0 4px; - font-size: 11px; -} -.empty { - color: var(--muted); - padding: 4px 8px; -} -.bad, -input.bad { - color: var(--bad); - border-color: var(--bad); -} -.ok { - color: var(--ok); -} -table.out { - border-collapse: collapse; - width: 100%; - background: var(--panel); -} -.out th, -.out td { - border: 1px solid var(--line); - padding: 2px 6px; - vertical-align: middle; -} -.out th { - background: var(--chip); -} -.out td.h { - min-width: 380px; -} -.out td.n { - text-align: right; - font-family: Consolas, monospace; - width: 90px; -} -.out tr.head td, -.out tr.item td.h { - font-weight: 600; -} -.out tr.item td.h { - padding-top: 6px; -} -.out tr.subsum td, -.out tr.total td { - background: var(--chip); -} -.gap { - display: inline-block; - width: 18px; -} -.fr { - white-space: nowrap; - display: inline-flex; - flex-direction: column; - vertical-align: middle; - text-align: center; - margin: 0 2px; -} -.fr > span:first-child { - border-bottom: 1px solid currentColor; - padding: 0 3px; -} -.fr > span:last-child { - padding: 0 3px; -} -#tot { - display: flex; - gap: 6px; -} -.box { - border: 1px solid var(--line); - border-radius: 6px; - padding: 2px 10px; - display: flex; - gap: 8px; - align-items: baseline; -} -.box b { - font-size: 15px; -} -.box.sum { - border-color: var(--accent); -} -details { - width: 100%; -} -textarea { - width: 100%; - font-family: Consolas, monospace; -} -dialog { - background: var(--panel); - color: var(--text); - border: 1px solid var(--line); - border-radius: 8px; -} -dialog table { - border-collapse: collapse; - margin: 6px 0; -} -dialog th, -dialog td { - border: 1px solid var(--line); - padding: 2px 6px; -} diff --git a/ui_template/ui_template_context_menu.ts b/ui_template/ui_template_context_menu.ts index d2a6fdee8..d5c16c3e0 100644 --- a/ui_template/ui_template_context_menu.ts +++ b/ui_template/ui_template_context_menu.ts @@ -11,7 +11,7 @@ * 모양은 공용 CSS 한 벌(`ui_template_context_menu.css` · `.ui-ctx-menu`) — 나무 메뉴와 같음(계획서 39-9b). * ========================================================================== */ -import { createIconButton } from "./ui_template_button"; +import { createIconButton, type UiSize } from "./ui_template_button"; import { icon, type IconName } from "./ui_template_icons"; import "./ui_template_context_menu.css"; import { showConfirmDialog } from "./ui_template_modal"; @@ -251,11 +251,19 @@ export interface RowMenuItem { } /** 줄 끝 ⋯ — 열면 항목 · 밖을 누르면 닫힘 · danger 는 확인 뒤 실행 */ -export function createRowMenu(items: RowMenuItem[]): HTMLElement { +export function createRowMenu( + items: RowMenuItem[], + /** 여는 단추 — 기본 ⋯(더 보기 · 28) · 예 [+] 더하기 메뉴 = `{ icon: "plus", title: "줄 더함", size: 24 }` */ + trig: { icon?: IconName; title?: string; size?: UiSize } = {}, +): HTMLElement { const wrap = document.createElement("div"); wrap.className = "ui-row-menu"; if (!items.length) return wrap; - const trigger = createIconButton({ icon: "dots", title: "더 보기", size: 28 }); + const trigger = createIconButton({ + icon: trig.icon ?? "dots", + title: trig.title ?? "더 보기", + size: trig.size ?? 28, + }); trigger.classList.add("ui-row-menu__btn"); trigger.setAttribute("aria-haspopup", "menu"); trigger.setAttribute("aria-expanded", "false"); diff --git a/vite.config.ts b/vite.config.ts index 7fd489fa3..676b2f012 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -61,8 +61,6 @@ export default { target: apiTarget, changeOrigin: true, }, - // 시험 페이지 「줄 쌓기 단가산출」(PLAN 804-1) — 폴더 지울 때 이 줄도 - "/tools/line-stack-test": { target: apiTarget, changeOrigin: true }, }, }, // 빌드판(555-2 · `tools/preview_up.py`) — 폰 · 외부 접속용 · 포트 = 개발 포트 − 1000(5173 → 4173)