- 왼쪽 줄 쌓기(항목 · 줄 일곱 종류 · 공식 틀 셋 · 식으로 풀기 · 틀로 저장 · 표에서 고르기 · 끌어 순서) · 오른쪽 경쟁사 꼴 산출근거(분수 기호식 = 값식 = 결과 · 소계 · 합계) · 아래 비목 상자 · 엑셀 글 붙여넣기 · 저장 = localStorage - 견본 셋 금액 = 거창 파일 원 단위(5,323 · 52,796 · 94,215 비목 넷) · 확인 = node tools/line-stack-test/check.mjs - 접근 = main.py 정적 마운트 /tools/line-stack-test · vite 프록시 한 줄(개발 포트에서 열림) · M01 왼쪽 패널 맨 아래 「시험: 줄 쌓기」 새 탭 - 라이브러리 0(분수 = CSS) · 지우기 = 폴더 + 표시 주석 셋 덩이 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
166 lines
8.1 KiB
JavaScript
166 lines
8.1 KiB
JavaScript
// 줄 쌓기 단가산출 시험 — 왼쪽 줄 쌓기 편집(종류마다 필요한 칸만 · 칸은 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>`;
|
||
}
|