feat(804-7): 줄 쌓기 「수식」 탭 수정 2차(요구 687 · 688)

- 로직 삭제 = 서버 정본에서 바로(로직 줄 + 수식 문서 · `/logic/remove`) → 로직 목록 · 부르는 곳(다른 수식 문서 · 일위대가 조합 · M02 단가산출 바탕)이 있으면 확인 창 전에 막고 까닭(`/logic/users`)
- 머리 단추 다섯 — [신규 로직] · [로직 복사/생성] · [로직 삭제] · [되돌림] · [저장] · [저장] 한 번 = 기본정보 + 수식 · [되돌림] = 둘 다 저장 뒤 상태 · 줄 쌓기 [고침 버림] · [수식 저장]은 M01 에서 걷음(M02 사본은 그대로)
- 합계 상자 화면 아래 고정 · 원문 손잡이 브라우저 오른쪽 끝
- 왼쪽: 그룹 머리 한 줄(접어도 [+] · X 자리 그대로) · 「비우면 번호 없음」 · 가나다 체크 걷음 · 금액 줄 한 줄 배치 · 이름 단추 = 그 이름 찾은 찾기 창 · 수량 기호 = 값 + 출처 꼬리표 · [+] 메뉴(단가산출 · 율 · 식 · 근거 글 · 틀 찾기 걷음) · 식 글 눌러 그 자리 고침([식 고치기] 걷음) · 단위 = 공용 단위 고르기(친 글도 씀) · 만약 안 탄 갈래 숨김
- 오른쪽 기계 줄: 비목 수 링크 걷음 · 제목 줄 누름 = 시간당 경비 펼침 · 펼친 제목 보통 글
- 요소 키가 기호 이름으로 샌 것(`k_EQ0006`) — 변환기가 자원 이름으로(`k_하베스터`) · 수식 문서 다시 빌드(10 파일 · 78 기호 · 금액 같음)
- 시험 페이지 `tools/line-stack-test` · main.py 마운트 · vite 프록시 · M01 왼쪽 「시험: 줄 쌓기」 걷음
- 단위 표준 표 읽기 = 로그인만(M02 일반 사용자 단위 목록)
- 시험: test_804_7 새로 · 804_2 · 804_4 이음 글 고침(createStackScreen 줄 나뉨) · 552_3 여는 단추 기본 ⋯ 꼴로 고침(부르는 쪽이 [+] 로 바꿀 수 있음)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-08 13:33:36 +09:00
co-authored by Claude Opus 5.5
parent 89292a75df
commit abe722f378
43 changed files with 1025 additions and 2061 deletions
+103 -20
View File
@@ -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 {
+161 -102
View File
@@ -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<MoneyKind, LsKey> = {
율: "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<string, unknown> | 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<HTMLInputElement>(`[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" : ""}`,
+22 -10
View File
@@ -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);
+9 -2
View File
@@ -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)
+158 -125
View File
@@ -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<string, unknown>;
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];
}
+21 -7
View File
@@ -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<string, FormulaDoc>;
@@ -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<string, unknown>, done = ft("Saved")): Promise<void> => {
if (!doc || (!job && !dirty())) return;
let loading: Promise<void> = Promise.resolve();
const save = async (job?: Record<string, unknown>, done = ft("Saved")): Promise<boolean> => {
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<boolean> => save(undefined, ""),
/** 페이지 [되돌림] — 저장 뒤 상태로(캐시 지움) */
discard: (): Promise<void> => discard(),
/** 시험 · 확인용 */
state: () => ({ doc, answer, dirty: dirty(), items: doc ? stackOf(doc) : [] }),
};
+14 -13
View File
@@ -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: [
+54
View File
@@ -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
+19
View File
@@ -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:
"""연결 후보 — 요소 · 표 · 단가산출(수식 문서) 이름 찾기(읽기만)."""
+58 -12
View File
@@ -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<boolean> => 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<void> => {
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<void> => {
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<void> => {
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 {
@@ -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)));
}
@@ -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<unknown> => request("/logic/remove", { key, file, version, 개정확인 });
/** 본떠 만들기 — 정본·자체 둘 다 본뜸 · 새 키(`GX…`)는 서버가 줌(계약 5장) */
export const copyLogic = (
key: string,
@@ -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: [
"저장 안 한 로직이 있음 — 다른 로직을 열어도 고친 것은 남음",
+1 -7
View File
@@ -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) =>
+1 -1
View File
@@ -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
+14 -2
View File
@@ -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));
+2 -13
View File
@@ -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)
@@ -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)
@@ -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", "묶음": "재료 · 공구손료 및 경장비"}
],
"끝수": {
@@ -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},
@@ -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㎾"}
],
"끝수": {
@@ -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"},
@@ -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 주⑤"}
],
"끝수": {
@@ -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},
@@ -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 주⑤"}
],
"끝수": {
@@ -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},
@@ -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},
@@ -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"}
],
"끝수": {
+2 -1
View File
@@ -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)") :]
+3 -1
View File
@@ -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"
@@ -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")
@@ -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
-247
View File
@@ -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) =>
`<div class="box"><span>${{ l: "노무비", m: "재료비", e: "경비" }[c]}</span><b>${fmtP(t[c])}</b></div>`,
)
.join("") + `<div class="box sum"><span>계</span><b>${fmtP(t.sum)}</b></div>`;
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)}) → <b class="${same ? "ok" : "bad"}">${same ? "같음" : "다름"}</b>`
: "";
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) => `<button data-sample="${i}">${esc(s.label)}</button>`,
).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 =
`<tr>${TABLE_E.head.map((h) => `<th>${esc(h)}</th>`).join("")}</tr>` +
TABLE_E.rows
.map(
(r) =>
`<tr><th>${esc(r[0])}</th>${r
.slice(1)
.map((v, c) =>
v == null
? "<td></td>"
: `<td><button data-v="${v}" data-src="${esc(`${TABLE_E.title} · ${r[0]} · ${TABLE_E.head[c + 1]}`)}">${v}</button></td>`,
)
.join("")}</tr>`,
)
.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();
-182
View File
@@ -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;
}
-11
View File
@@ -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);
-165
View File
@@ -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 = "") =>
`<input data-p="${p}" value="${esc(v ?? "")}" class="${cls}" placeholder="${esc(ph)}" />`;
const sel = (p, v, opts) =>
`<select data-p="${p}">${Object.entries(opts)
.map(([k, t]) => `<option value="${k}"${k === v ? " selected" : ""}>${esc(t)}</option>`)
.join("")}</select>`;
const pick = (list, act, label) =>
`<select data-act="${act}"><option value="">${label}</option>${list
.map((r, i) => `<option value="${i}">${esc(r.no ? `단산 ${r.no} ${r.name}` : r.name)}</option>`)
.join("")}</select>`;
export function allTemplates(mine) {
return [...TEMPLATES, ...mine];
}
/** 「+ 줄」 고르개 — 공식은 틀마다 하나씩 */
export function addLineSelect(i, mine) {
const tpls = allTemplates(mine)
.map((t) => `<option value="formula:${t.id}">⑥ 공식 — ${esc(t.name)}</option>`)
.join("");
return `<select data-act="addline" data-i="${i}"><option value="">+ 줄 ▾</option>
<option value="mach">${KINDS.mach}</option><option value="ref">${KINDS.ref}</option>
<option value="labor">${KINDS.labor}</option><option value="rate">${KINDS.rate}</option>
<option value="mat">${KINDS.mat}</option>${tpls}
<option value="formula:">⑥ 공식 — 새 공식(빈 묶음)</option><option value="text">${KINDS.text}</option></select>`;
}
function opsEdit(p, ops) {
const one = (o, k) =>
`<span class="op">${sel(`${p}.ops.${k}.op`, o.op, { "×": "×", "÷": "÷", "/": "/" })}${inp(
`${p}.ops.${k}.v`,
o.v,
"v",
"값 · 이름",
)}${inp(`${p}.ops.${k}.u`, o.u, "u", "단위")}<button data-act="delop" data-p="${p}" data-k="${k}" title="지움">×</button></span>`;
return `<div class="ops">${(ops ?? []).map(one).join("")}<button data-act="addop" data-p="${p}">+ 곱나눔</button></div>`;
}
const copsNote = (ln) =>
ln.cops
? `<div class="note">비목 따로 꼬리(붙여넣기): ${Object.entries(ln.cops)
.map(
([c, ops]) =>
`${COLSEL[c]} ${ops.map((o) => `${o.op} ${esc(o.v)} ${esc(o.u)}`).join(" ")}`,
)
.join(" · ")}</div>`
: "";
/** 공식 줄 — 접힘 = 값 칸만 · 풀림 = 「이름 = 식」 글 줄 전부 */
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 = `<span class="val ${rr.err ? "bad" : ""}" data-val="${rp}">${rr.err ? esc(rr.err) : (rr.val ?? "")}</span>`;
const src = row.src ? `<span class="src" title="${esc(row.src)}">표</span>` : "";
const table = `<button data-act="table" data-p="${rp}" title="표에서 고르기">표</button>`;
if (!ln.open) {
if (plain || !row.x)
return `<div class="frow"><b>${esc(row.n || "?")}</b> = ${inp(`${rp}.x`, row.x, "num")} <i>${esc(row.u)}</i> ${table}${src}</div>`;
return `<div class="frow calc"><b>${esc(row.n)}</b> = <code>${esc(row.x)}</code> = ${val} <i>${esc(row.u)}</i></div>`;
}
return `<div class="frow open">${inp(`${rp}.n`, row.n, "name", "이름")} = ${inp(`${rp}.x`, row.x, `expr ${rr.err ? "bad" : ""}`, "값 또는 식")}${inp(
`${rp}.u`,
row.u,
"u",
"단위",
)}${val}${table}${src}<button data-act="delrow" data-p="${p}" data-k="${k}" title="줄 지움">×</button></div>`;
})
.join("");
return `<div class="fhead"><span class="tpl">${label}</span>
<button data-act="fold" data-p="${p}">${ln.open ? "접기" : "식으로 풀기"}</button>
<button data-act="savetpl" data-p="${p}">틀로 저장</button></div>${rows}
${ln.open ? `<button data-act="addrow" data-p="${p}">+ 「이름 = 식」 줄</button>` : ""}`;
}
/** 틀과 견줄 꼴 — 값 줄은 이름만 · 식 줄은 이름과 식 */
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 = () =>
`<div class="row">노무 ${inp(`${p}.res.l`, ln.res?.l, "num")} 재료 ${inp(`${p}.res.m`, ln.res?.m, "num")} 경비 ${inp(
`${p}.res.e`,
ln.res?.e,
"num",
)}</div>`;
switch (ln.k) {
case "mach":
return `<div class="row">${pick(MACHINE, `res:mach:${p}`, "견본 기계 ▾")}${inp(`${p}.res.name`, ln.res?.name, "wide", "기계 이름")}</div>${price3()}${opsEdit(p, ln.ops)}${copsNote(ln)}`;
case "ref":
return `<div class="row">${pick(SANGEUN, `res:ref:${p}`, "견본 단산 ▾")} 단산 ${inp(`${p}.res.no`, ln.res?.no, "num s")}${inp(
`${p}.res.name`,
ln.res?.name,
"wide",
"단가산출 이름",
)}</div>${price3()}${opsEdit(p, ln.ops)}${copsNote(ln)}`;
case "labor":
case "mat":
return `<div class="row">${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)}` : ""}</div>${opsEdit(p, ln.ops)}`;
case "rate":
return `<div class="row">${inp(`${p}.name`, ln.name, "wide", "이름(예: 공구손료 및 경장비)")}</div>
<div class="row">기준 ${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)}</div>
<div class="row">글 「( ${inp(`${p}.of`, ln.of, "s2", "노무비")}의 % )」 · 소계 이름 ${inp(`${p}.baseName`, ln.baseName, "s2", "비우면 안 찍음")}</div>`;
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 `<div class="line k-${ln.k}" data-loc="${i}.${j}"><div class="lh"><span class="grip" draggable="true" data-loc="${i}.${j}" title="끌어 순서 바꿈">⠿</span><span class="kind">${KINDS[ln.k]}</span>${
r?.err ? `<span class="bad">${esc(r.err)}</span>` : ""
}<button data-act="delline" data-loc="${i}.${j}" title="줄 지움">지움</button></div>${lineBody(p, ln, r, mine)}</div>`;
})
.join("");
return `<div class="item" data-i="${i}"><div class="ih" data-drop="${i}">${inp(`items.${i}.title`, item.title, "title", "항목 이름(비우면 번호 없음)")}
<label><input type="checkbox" data-p="items.${i}.sub" ${item.sub ? "checked" : ""}/>가나다</label>${addLineSelect(i, mine)}
<button data-act="delitem" data-i="${i}">항목 지움</button></div>${lines || '<div class="empty" data-drop="' + i + '">줄 없음 — [+ 줄 ▾]</div>'}</div>`;
})
.join("");
return `<div class="dochead">이름 ${inp("name", doc.name, "wide")} 단위 ${inp("unit", doc.unit, "s")}</div>${items}<button data-act="additem">+ 항목</button>`;
}
-43
View File
@@ -1,43 +0,0 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>시험: 줄 쌓기 단가산출</title>
<link rel="icon" href="data:," />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header>
<h1>시험: 줄 쌓기 단가산출</h1>
<span class="hint">서버 없음 · 저장은 이 브라우저에만 · 마음에 안 들면 폴더째 지움</span>
<span id="samples"></span>
<button id="save">저장</button><button id="load">불러오기</button><span id="msg"></span>
</header>
<main>
<section id="edit"></section>
<section id="outWrap"><div id="out"></div></section>
</main>
<footer>
<div id="tot"></div>
<div id="cmp"></div>
<details>
<summary>
엑셀 글 붙여넣기 — 경쟁사 「단가산출」 시트 한 블록(머리 줄부터 「계 :」 줄까지)
</summary>
<textarea
id="paste"
rows="8"
placeholder="1.측구터파기(토사)[㎥] 5323 3,442 832 1,049&#10;1. 기 계 ( 100 % ) : 굴삭기(무한궤도) (0.7㎥)&#10;…"
></textarea>
<button id="pasteGo">줄로 넣기</button><span id="pasteMsg"></span>
</details>
</footer>
<dialog id="tbl">
<h3></h3>
<table></table>
<button data-close>닫기</button>
</dialog>
<script type="module" src="app.js"></script>
</body>
</html>
-257
View File
@@ -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 };
}
-181
View File
@@ -1,181 +0,0 @@
// 줄 쌓기 단가산출 시험 — 산출근거 출력(경쟁사 「단가산출」 시트 꼴 · 분수는 CSS 로 조판)
import { COLS, COL_NAME, parse, varsIn } from "./calc.js";
export const esc = (s) =>
String(s ?? "").replace(
/[&<>"]/g,
(c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[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])
: `<b class="bad">${esc(ast.n)}</b>`;
case "par":
return `( ${go(ast.a)} )`;
case "neg":
return `-${go(ast.a)}`;
}
if (ast.op === "/" && frac)
return `<span class="fr"><span>${bare(ast.a)}</span><span>${bare(ast.b)}</span></span>`;
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 = `<b class="bad">${esc(o.v)}</b>`;
}
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('<span class="gap"></span>') }), (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)} <b class="bad">${esc(rr?.err ?? "")}</b>` });
}
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: `<b class="bad">${esc(r.err)}</b>`, 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 "<td></td>";
return `<td class="n">${r.won ? fmtP(v) : fmt1(v)}</td>`;
};
const body = outRows(doc, res)
.map(
(r) =>
`<tr class="${r.cls ?? ""}${r.tall ? " tall" : ""}"><td class="h">${r.h}</td>${["sum", ...COLS].map((k) => cell(r, k)).join("")}</tr>`,
)
.join("");
return `<table class="out"><thead><tr><th>산 출 근 거</th><th>합 계</th><th>노무비</th><th>재료비</th><th>경 비</th></tr></thead><tbody>${body}</tbody></table>`;
}
-221
View File
@@ -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("", "")] }] }],
},
},
];
-306
View File
@@ -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;
}
+11 -3
View File
@@ -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");
-2
View File
@@ -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)