Files
Aislo/tools/line-stack-test/edit.js
T
eomsangdonandClaude Opus 5.5 3e64aa2a79 feat(804-1): 시험 페이지 「줄 쌓기 단가산출」 tools/line-stack-test + M01 왼쪽 패널 단추
- 왼쪽 줄 쌓기(항목 · 줄 일곱 종류 · 공식 틀 셋 · 식으로 풀기 · 틀로 저장 · 표에서 고르기 · 끌어 순서) · 오른쪽 경쟁사 꼴 산출근거(분수 기호식 = 값식 = 결과 · 소계 · 합계) · 아래 비목 상자 · 엑셀 글 붙여넣기 · 저장 = 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>
2026-11-07 23:19:38 +09:00

166 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 줄 쌓기 단가산출 시험 — 왼쪽 줄 쌓기 편집(종류마다 필요한 칸만 · 칸은 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>`;
}