feat(804-7): 줄 쌓기 「수식」 탭 수정 2차(요구 687 · 688)
- 로직 삭제 = 서버 정본에서 바로(로직 줄 + 수식 문서 · `/logic/remove`) → 로직 목록 · 부르는 곳(다른 수식 문서 · 일위대가 조합 · M02 단가산출 바탕)이 있으면 확인 창 전에 막고 까닭(`/logic/users`) - 머리 단추 다섯 — [신규 로직] · [로직 복사/생성] · [로직 삭제] · [되돌림] · [저장] · [저장] 한 번 = 기본정보 + 수식 · [되돌림] = 둘 다 저장 뒤 상태 · 줄 쌓기 [고침 버림] · [수식 저장]은 M01 에서 걷음(M02 사본은 그대로) - 합계 상자 화면 아래 고정 · 원문 손잡이 브라우저 오른쪽 끝 - 왼쪽: 그룹 머리 한 줄(접어도 [+] · X 자리 그대로) · 「비우면 번호 없음」 · 가나다 체크 걷음 · 금액 줄 한 줄 배치 · 이름 단추 = 그 이름 찾은 찾기 창 · 수량 기호 = 값 + 출처 꼬리표 · [+] 메뉴(단가산출 · 율 · 식 · 근거 글 · 틀 찾기 걷음) · 식 글 눌러 그 자리 고침([식 고치기] 걷음) · 단위 = 공용 단위 고르기(친 글도 씀) · 만약 안 탄 갈래 숨김 - 오른쪽 기계 줄: 비목 수 링크 걷음 · 제목 줄 누름 = 시간당 경비 펼침 · 펼친 제목 보통 글 - 요소 키가 기호 이름으로 샌 것(`k_EQ0006`) — 변환기가 자원 이름으로(`k_하베스터`) · 수식 문서 다시 빌드(10 파일 · 78 기호 · 금액 같음) - 시험 페이지 `tools/line-stack-test` · main.py 마운트 · vite 프록시 · M01 왼쪽 「시험: 줄 쌓기」 걷음 - 단위 표준 표 읽기 = 로그인만(M02 일반 사용자 단위 목록) - 시험: test_804_7 새로 · 804_2 · 804_4 이음 글 고침(createStackScreen 줄 나뉨) · 552_3 여는 단추 기본 ⋯ 꼴로 고침(부르는 쪽이 [+] 로 바꿀 수 있음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+103
-20
@@ -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
@@ -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" : ""}`,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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) : [] }),
|
||||
};
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
"""연결 후보 — 요소 · 표 · 단가산출(수식 문서) 이름 찾기(읽기만)."""
|
||||
|
||||
@@ -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: [
|
||||
"저장 안 한 로직이 있음 — 다른 로직을 열어도 고친 것은 남음",
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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"}
|
||||
],
|
||||
"끝수": {
|
||||
|
||||
@@ -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)") :]
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>`;
|
||||
}
|
||||
@@ -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 1. 기 계 ( 100 % ) : 굴삭기(무한궤도) (0.7㎥) …"
|
||||
></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>
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
// 줄 쌓기 단가산출 시험 — 산출근거 출력(경쟁사 「단가산출」 시트 꼴 · 분수는 CSS 로 조판)
|
||||
import { COLS, COL_NAME, parse, varsIn } from "./calc.js";
|
||||
|
||||
export const esc = (s) =>
|
||||
String(s ?? "").replace(
|
||||
/[&<>"]/g,
|
||||
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c],
|
||||
);
|
||||
export const fmtP = (n) => Math.round(n).toLocaleString("en-US");
|
||||
export const fmt1 = (n) =>
|
||||
n.toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 });
|
||||
export const fmtV = (n) =>
|
||||
n == null || Number.isNaN(n) ? "?" : n.toLocaleString("en-US", { maximumFractionDigits: 4 });
|
||||
const OP = { "*": "×", "/": "/", "×": "×", "÷": "÷", "+": "+", "-": "-", "^": "^" };
|
||||
|
||||
/** 식 나무 → 글(sym = 기호 · val = 값) · 기호가 든 식의 「/」 는 분수로 */
|
||||
export function showAst(ast, mode, env, frac = varsIn(ast).length > 0) {
|
||||
const go = (a) => showAst(a, mode, env, frac);
|
||||
const bare = (a) => (a.t === "par" ? go(a.a) : go(a));
|
||||
switch (ast.t) {
|
||||
case "num":
|
||||
return fmtV(ast.v);
|
||||
case "var":
|
||||
return mode === "sym"
|
||||
? esc(ast.n)
|
||||
: ast.n in env
|
||||
? fmtV(env[ast.n])
|
||||
: `<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>`;
|
||||
}
|
||||
@@ -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("", "")] }] }],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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,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");
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user