4 Commits
Author SHA1 Message Date
eomsangdon af2d9826cf Merge branch 'sub_laptop_3' of C:/Aislo_wt/sub_laptop_3 into main_laptop_1 2026-11-08 13:36:24 +09:00
eomsangdonandClaude Opus 5.5 abe722f378 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>
2026-11-08 13:33:36 +09:00
eomsangdonandClaude Fable 5.1 fd188fdf2b knowledge(요구688): 그룹 닫으면 X 밀림
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-08 12:46:16 +09:00
eomsangdonandClaude Fable 5.1 89292a75df knowledge(요구687): 줄 쌓기 수정 18 · 그림 셋
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-08 12:45:09 +09:00
47 changed files with 1056 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));
@@ -407,3 +407,34 @@ M02 의 엑셀 불러오기에서 경쟁사 2개의 프로그램의 출력을
1. 현 단가산출의 표현 중 수식 탭은 삭제하고 현재 줄 쌓기 탭을 기본보기로 변걍 후 탭 제목을 수식으로 변경.
2. 전역 깃 싱크. 현재 데스크탑 워크트리에 기술정보 db폴더변경되었고 tool 폴더 하위 est 테스트 프로그램은 개발 중이나 전역 깃 싱크.
## 요구 687 (11-08 · 사용자 · 전역 싱크 뒤 줄 쌓기 수정 18 — 그림 `요구687_1_기계줄.png` · `요구687_2_재료수량.png` · `요구687_3_요소키.png`)
전역 깃 싱크 후 이어서 작업
1. M01 단가산출 기초의 항목에서 로직 삭제 안됨. 캐시에만 삭제하는것으로 보임. 삭제후 리스트로 이동
2. M01 단가산출 기초의 페이지에서 상단의 신규로직 부터 저장 버튼까지 역할 점검, 고침 버림과 수식 저장도 같이 확인
3. M01 단가산출 기초의 하단 합계 컨테이너 상시보이기
4. 좌측 영역 그룹이(비우면 번호 없음) 라벨 삭제, 의미 없음.
5. 좌측영역 체크박스와 가나다 라벨 필요없음.
6. 좌측 영역 그룹제목 영역 행배치 정렬 안됨.
7. 좌측영역의 세부항목을 잘하면 1행 구조로도 표현할 수 있을것같은데... 검토
8. 좌측 영역의에서 재료인 경우 수량은
9. 좌측영역에서 재료, 인력,기계인 경우 버튼을 누르면 조회창에 해당 단어를 검색한 결과가 보이게 변경
10. 좌측영역에서 IF문에 의해 결정되는 경우 전체를 나열하는걸로 보이는데 옵션이나 값에 의해 결정된 항목만 보여주는게 어떤지. 공식 표기는 일단 유지.
11. [Image #1] 지난 수정에서 잘못 수정한 부분이 그림의 노무비, 재료비, 경비의 항목을 선택하면 아래 공통항목이 드롭다운으로 열리는게 아니라 상단 굴착기+부작용 집게 0.6을 누르면 보이게 변경. 열린 내부의 제목이 링크 걸린걸로 보임
12. [Image #2] 수량 부분이 옵션에 결려 있는걸로 보임. 다른 표기법이 있을지 검토
13. 좌측영역의 +줄 드랍다운 메뉴는 + 버튼으로 변경
14. 좌측영역에서 식 고치기가 의미가 있는지 검토
15. 단위는 드롭다운 항목으로 변경하는게 있을 것 같음. 다른 곳에 적용된 이력있음. 대신 누락된 단위가 있는지는 검토 필요.
16. M01의 좌측 패널에서 시험:줄 쌓기는 이제 필요없어보임.
17. M01의 단가산출 항목 내부의 페이지를 보면 우측에 원문 펼침 버튼이 중간에 애매하게 배치되어 있음. 좌측 여유때문으로 보여짐. 브라우져 우측에 붙게하여 내부공간을 더 효율적으로 사용
18. + 버튼으로 변경된 + 줄 드롭다운의 메뉴 중
- 율/식은 인력/기계/재료 등에 종속되어 있는 정보가 아닌지.
- 다른 단가산출의 이름을 단가산출로 변경해도 무관해보임.
- 틀찾기는 이제 삭제한 기능으로 보임
- 글/근거의 의미는 모르겠음.
- [Image #3] 요소 키값을 사용하고 있는데 확인 어려움. 원인 분석
## 요구 688 (11-08 · 사용자 · 그룹 닫으면 삭제 단추 밀림)
추가 좌측영역의 그룹을 닫으면 삭제 버튼이 아래로 밀림 개선 필요
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

+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)