feat(714-2): 공용 단가산출 계산서 묶음 꼴 — 인력 · 재료 · 경비(장비) · 하위(714-1 줄 덧칸 묶음) · 장비 · 하위 로직 = 한 줄 + 노 · 재 · 경 칸 · 누르면 몫 식 석 줄 펼침 · 묶음 머리 기준단위 · 환산 근거 · 쓰는 입력 칸 · 맨 아래 비목 소계 셋 + 계
- calc_sheet.ts bundle 꼴 · bundleLines(순수 · 같은 이름 k 번째끼리 짝) · 새 calc_sheet.css(장비 줄 누름 · 몫 들여씀) - M01 「인력·재료·경비」 = bundle 꼴 · 변수 칸 = 서버 쓰는입력 - 714-3 텍스트 수식 단추도 서버 쓰는입력 먼저(조건 · 재료 고르기 입력까지) · 없으면 조각 - 704-2 M02 계산서 보기 = 비목 꼴 그대로(DOM 전후 같음) - 장비 펼침 넉 줄(손료 · 주연료 · 잡품 · 운전사) = sub3 「펼침」 덧칸 뒤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
/* 공용 단가산출 계산서(714-2) — 장비 · 하위 로직 한 줄 = 누르면 펼침 */
|
||||||
|
[data-sheet-equip] {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-sheet-equip]:hover,
|
||||||
|
[data-sheet-equip]:focus-visible {
|
||||||
|
background: var(--color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-sheet-part] .ui-calc-row__label {
|
||||||
|
padding-left: var(--spacing-16);
|
||||||
|
}
|
||||||
+178
-31
@@ -1,22 +1,34 @@
|
|||||||
/* =============================================================================
|
/* =============================================================================
|
||||||
* calc_sheet.ts
|
* calc_sheet.ts
|
||||||
* 공용 「단가산출 계산서」(PLAN 714-2 · 요구서 내역12 4장 5) — 704-2 M02 계산서 보기를 올린 것.
|
* 공용 「단가산출 계산서」(PLAN 714-2 · 요구서 내역12 4장 5 · 6) — 704-2 M02 계산서 보기를 올린 것.
|
||||||
* 쓰는 곳 = M01 「인력·재료·경비」 보기 · M02 기준 목록 계산서 보기(704-2) · (뒤에) 일위대가 모달 · B09 단가산출근거
|
* 쓰는 곳 = M01 「인력·재료·경비」 보기 · M02 기준 목록 계산서 보기(704-2) · (뒤에) 일위대가 모달 · B09 단가산출근거
|
||||||
* 꼴 = 묶음마다 [머리(변수 칸이 있을 때만) · 쓰는 입력 값 줄] · 식 줄(이름 | 단가 × 품 | 금액) · 소계 · 맨 아래 계
|
* 꼴 둘 —
|
||||||
* 값 = 서버 셈 그대로 찍음(화면에서 다시 셈 안 함) · 변수 칸 바꾸면 `onInput(이름, 값)` — 부르는 쪽이 다시 셈
|
* 비목(기본 · 704-2) = 비목 묶음마다 식 줄(이름 | 단가 × 품 | 금액) · 소계 · 맨 아래 계
|
||||||
* 부품 = `ui_template_calc_row`(값 줄 · 식 줄) · 입력 · 고르기 · 글 — 날 요소 · 페이지 CSS 없음
|
* 묶음(`bundle`) = 인력 · 재료 · 경비(장비) · 하위 — 줄의 `묶음`(714-1 서버 덧칸) 으로 다시 묶음 ·
|
||||||
|
* 장비 · 하위 로직 = 한 줄(노 · 재 · 경 칸 · 합) + 누르면 펼침(그 줄의 노 · 재 · 경 식) ·
|
||||||
|
* 묶음 머리 = 이름 · 기준단위 · 환산 근거 · 그 묶음이 쓰는 입력 칸(`쓰는입력`) · 맨 아래 비목 소계 셋 + 계
|
||||||
|
* 값 = 서버 셈 그대로(묶음 소계만 서버 줄 금액의 합) · 변수 칸 바꾸면 `onInput(이름, 값)` — 부르는 쪽이 다시 셈
|
||||||
|
* 부품 = `ui_template_calc_row`(값 줄 · 식 줄) · 입력 · 고르기 · 글 — 날 단추 · 표 · 페이지 CSS 없음
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import { createCalcFormula, createCalcFormulaRow, createCalcRow } from "@ui/ui_template_calc_row";
|
import { createCalcFormula, createCalcFormulaRow, createCalcRow } from "@ui/ui_template_calc_row";
|
||||||
import { el } from "@ui/ui_template_elements";
|
import { el } from "@ui/ui_template_elements";
|
||||||
import { createInputField, createSelectField } from "@ui/ui_template_input";
|
import { createInputField, createSelectField } from "@ui/ui_template_input";
|
||||||
|
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||||
import { createText } from "@ui/ui_template_text";
|
import { createText } from "@ui/ui_template_text";
|
||||||
|
import "./calc_sheet.css";
|
||||||
|
|
||||||
export interface SheetLine {
|
export interface SheetLine {
|
||||||
이름: string;
|
이름: string;
|
||||||
/** 서버 값 식 「단가 * 품 = 금액」 — 「= 금액」 은 떼고 「*」 → 「×」 */
|
/** 서버 값 식 「단가 * 품 = 금액」 — 「= 금액」 은 떼고 「*」 → 「×」 */
|
||||||
글: string;
|
글: string;
|
||||||
금액: number | null;
|
금액: number | null;
|
||||||
|
/** 714-1 서버 덧칸 — 그 줄 값이 쓰는 로직 입력 · 묶음 · 장비 · 찾는 표 기준 · 환산 근거 */
|
||||||
|
쓰는입력?: string[];
|
||||||
|
묶음?: string | null;
|
||||||
|
장비?: boolean | null;
|
||||||
|
기준단위?: string | null;
|
||||||
|
환산근거?: string;
|
||||||
}
|
}
|
||||||
export interface SheetGroup {
|
export interface SheetGroup {
|
||||||
비목: string;
|
비목: string;
|
||||||
@@ -34,22 +46,103 @@ export interface SheetOptions {
|
|||||||
계: number | null;
|
계: number | null;
|
||||||
/** 맨 아래 계 줄 이름(「계 (원/㎡)」) */
|
/** 맨 아래 계 줄 이름(「계 (원/㎡)」) */
|
||||||
sumLabel: string;
|
sumLabel: string;
|
||||||
/** 변수 칸 — 입력 꼴 · 지금 값 · 묶음이 쓰는 입력 이름 · 바꾼 뒤 할 일(없으면 변수 칸 없음) */
|
/** 인력 · 재료 · 경비(장비) 묶음으로 다시 묶음(없으면 서버 비목 묶음 그대로 · 704-2) */
|
||||||
|
bundle?: boolean;
|
||||||
|
/** 변수 칸 — 입력 꼴 · 지금 값 · 바꾼 뒤 할 일(없으면 변수 칸 없음) · 칸 = 묶음 줄들의 `쓰는입력` */
|
||||||
inputs?: SheetInput[];
|
inputs?: SheetInput[];
|
||||||
values?: Record<string, string>;
|
values?: Record<string, string>;
|
||||||
uses?: (group: SheetGroup, index: number) => string[];
|
|
||||||
onInput?: (name: string, value: string) => void;
|
onInput?: (name: string, value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── 순수 셈 ── */
|
||||||
|
/** 값 식 → 식 칸 글 — 「268,908 * 0.12 = 32,269」 → 「268,908 × 0.12」 */
|
||||||
|
export const sheetFormula = (text: string): string =>
|
||||||
|
text.replace(/ = [^=]*$/, "").replaceAll(" * ", " × ");
|
||||||
|
|
||||||
|
/** 장비 · 하위 로직 한 줄 — 비목마다 몫 줄(서버 줄 그대로) · 합 */
|
||||||
|
export interface SheetMerged {
|
||||||
|
이름: string;
|
||||||
|
몫: { 비목: string; 줄: SheetLine }[];
|
||||||
|
금액: number | null;
|
||||||
|
}
|
||||||
|
export interface SheetBundle {
|
||||||
|
묶음: string;
|
||||||
|
줄: (SheetLine | SheetMerged)[];
|
||||||
|
소계: number;
|
||||||
|
쓰는입력: string[];
|
||||||
|
기준단위: string[];
|
||||||
|
환산근거: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ORDER = ["인력", "재료", "경비", "하위"];
|
||||||
|
const OF_COST: Record<string, string> = { 노무비: "인력", 재료비: "재료", 경비: "경비" };
|
||||||
|
const add = (a: number | null, b: number | null): number | null =>
|
||||||
|
a === null && b === null ? null : (a ?? 0) + (b ?? 0);
|
||||||
|
const merges = (l: SheetLine): boolean => !!l.장비 || l.묶음 === "하위";
|
||||||
|
|
||||||
|
/** 비목 묶음 → 인력 · 재료 · 경비 · 하위 묶음 — 장비 · 하위 줄은 비목마다 갈린 몫을 한 줄로
|
||||||
|
* (같은 이름 k 번째끼리 짝 — 서버 줄 차례 = 호표 차례 · 비목마다 같은 차례) */
|
||||||
|
export function bundleLines(groups: SheetGroup[]): SheetBundle[] {
|
||||||
|
const bundles = new Map<string, SheetBundle>();
|
||||||
|
const merged = new Map<string, SheetMerged>();
|
||||||
|
const take = (name: string): SheetBundle => {
|
||||||
|
let b = bundles.get(name);
|
||||||
|
if (!b) {
|
||||||
|
b = { 묶음: name, 줄: [], 소계: 0, 쓰는입력: [], 기준단위: [], 환산근거: [] };
|
||||||
|
bundles.set(name, b);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
for (const g of groups) {
|
||||||
|
const seen = new Map<string, number>();
|
||||||
|
for (const line of g.줄) {
|
||||||
|
const b = take(line.묶음 || OF_COST[g.비목] || g.비목);
|
||||||
|
b.소계 += line.금액 ?? 0;
|
||||||
|
for (const n of line.쓰는입력 ?? []) if (!b.쓰는입력.includes(n)) b.쓰는입력.push(n);
|
||||||
|
if (line.기준단위 && !b.기준단위.includes(line.기준단위)) b.기준단위.push(line.기준단위);
|
||||||
|
if (line.환산근거 && !b.환산근거.includes(line.환산근거)) b.환산근거.push(line.환산근거);
|
||||||
|
if (!merges(line)) {
|
||||||
|
b.줄.push(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const k = seen.get(line.이름) ?? 0;
|
||||||
|
seen.set(line.이름, k + 1);
|
||||||
|
const id = `${b.묶음}\n${line.이름}\n${k}`;
|
||||||
|
let m = merged.get(id);
|
||||||
|
if (!m) {
|
||||||
|
m = { 이름: line.이름, 몫: [], 금액: null };
|
||||||
|
merged.set(id, m);
|
||||||
|
b.줄.push(m);
|
||||||
|
}
|
||||||
|
m.몫.push({ 비목: g.비목, 줄: line });
|
||||||
|
m.금액 = add(m.금액, line.금액);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rank = (x: string): number => (ORDER.includes(x) ? ORDER.indexOf(x) : ORDER.length);
|
||||||
|
return [...bundles.values()].sort((a, b) => rank(a.묶음) - rank(b.묶음));
|
||||||
|
}
|
||||||
|
/* ── 순수 셈 끝 ── */
|
||||||
|
|
||||||
|
const TEXT = {
|
||||||
|
인력: ["인력", "Labor"],
|
||||||
|
재료: ["재료", "Material"],
|
||||||
|
경비: ["경비(장비)", "Expense (equipment)"],
|
||||||
|
하위: ["하위 로직", "Sub logic"],
|
||||||
|
Basis: ["기준", "Basis"],
|
||||||
|
Short: ["노 {a} · 재 {b} · 경 {c}", "L {a} · M {b} · E {c}"],
|
||||||
|
Fold: ["누르면 노 · 재 · 경 식 펼침", "Click to expand labor · material · expense"],
|
||||||
|
} as const satisfies Record<string, readonly [string, string]>;
|
||||||
|
const t = (k: keyof typeof TEXT): string => TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0];
|
||||||
|
const bundleName = (b: string): string => (b in TEXT ? t(b as keyof typeof TEXT) : b);
|
||||||
|
|
||||||
const won = (v: number | null, digits = 0): string =>
|
const won = (v: number | null, digits = 0): string =>
|
||||||
v === null ? "-" : v.toLocaleString("ko-KR", { maximumFractionDigits: digits });
|
v === null ? "-" : v.toLocaleString("ko-KR", { maximumFractionDigits: digits });
|
||||||
|
|
||||||
const out = (v: number | null, key = false, digits = 0): HTMLElement =>
|
const out = (v: number | null, key = false, digits = 0): HTMLElement =>
|
||||||
el("span", { className: `ui-calc-row__out${key ? " is-key" : ""}`, text: won(v, digits) });
|
el("span", { className: `ui-calc-row__out${key ? " is-key" : ""}`, text: won(v, digits) });
|
||||||
|
|
||||||
/** 값 식 → 식 칸 글 — 「268,908 * 0.12 = 32,269」 → 「268,908 × 0.12」 */
|
const lineRow = (line: SheetLine, label = line.이름): HTMLElement =>
|
||||||
export const sheetFormula = (text: string): string =>
|
createCalcFormulaRow(label, createCalcFormula(sheetFormula(line.글)), out(line.금액, false, 1));
|
||||||
text.replace(/ = [^=]*$/, "").replaceAll(" * ", " × ");
|
|
||||||
|
|
||||||
/** 변수 칸 하나 — 고르기 있으면 고르기 · 아니면 글 칸(Enter · 떠남 때 넘김) */
|
/** 변수 칸 하나 — 고르기 있으면 고르기 · 아니면 글 칸(Enter · 떠남 때 넘김) */
|
||||||
function varRow(spec: SheetInput, value: string, onInput: (v: string) => void): HTMLElement {
|
function varRow(spec: SheetInput, value: string, onInput: (v: string) => void): HTMLElement {
|
||||||
@@ -75,32 +168,86 @@ function varRow(spec: SheetInput, value: string, onInput: (v: string) => void):
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 장비 · 하위 한 줄 — 노 · 재 · 경 칸 + 합 · 누르면(Enter · Space) 몫 식 줄 펼침 */
|
||||||
|
function mergedRows(m: SheetMerged): HTMLElement[] {
|
||||||
|
const share = (cost: string): number | null =>
|
||||||
|
m.몫.filter((x) => x.비목 === cost).reduce<number | null>((s, x) => add(s, x.줄.금액), null);
|
||||||
|
const short = t("Short")
|
||||||
|
.replace("{a}", won(share("노무비"), 1))
|
||||||
|
.replace("{b}", won(share("재료비"), 1))
|
||||||
|
.replace("{c}", won(share("경비"), 1));
|
||||||
|
const head = createCalcFormulaRow(`▸ ${m.이름}`, createCalcFormula(short), out(m.금액, false, 1));
|
||||||
|
const parts = m.몫.map((x) => lineRow(x.줄, `${x.비목}`));
|
||||||
|
for (const p of parts) {
|
||||||
|
p.hidden = true;
|
||||||
|
p.dataset.sheetPart = m.이름;
|
||||||
|
}
|
||||||
|
head.dataset.sheetEquip = m.이름;
|
||||||
|
head.title = t("Fold");
|
||||||
|
head.tabIndex = 0;
|
||||||
|
head.setAttribute("role", "button");
|
||||||
|
head.setAttribute("aria-expanded", "false");
|
||||||
|
const label = head.firstElementChild as HTMLElement;
|
||||||
|
const toggle = (): void => {
|
||||||
|
const open = head.getAttribute("aria-expanded") !== "true";
|
||||||
|
head.setAttribute("aria-expanded", String(open));
|
||||||
|
label.textContent = `${open ? "▾" : "▸"} ${m.이름}`;
|
||||||
|
parts.forEach((p) => (p.hidden = !open));
|
||||||
|
};
|
||||||
|
head.addEventListener("click", toggle);
|
||||||
|
head.addEventListener("keydown", (ev) => {
|
||||||
|
if (ev.key !== "Enter" && ev.key !== " ") return;
|
||||||
|
ev.preventDefault();
|
||||||
|
toggle();
|
||||||
|
});
|
||||||
|
return [head, ...parts];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 묶음 머리 — 이름 · 기준 · 환산 근거 · 쓰는 입력 칸 */
|
||||||
|
function headRows(
|
||||||
|
name: string,
|
||||||
|
uses: string[],
|
||||||
|
o: SheetOptions,
|
||||||
|
specs: Map<string, SheetInput>,
|
||||||
|
basis: string[] = [],
|
||||||
|
notes: string[] = [],
|
||||||
|
): HTMLElement[] {
|
||||||
|
const vars = o.onInput ? uses.filter((n) => specs.has(n)) : [];
|
||||||
|
if (!vars.length && !o.bundle) return [];
|
||||||
|
const title = [name, basis.length ? `${t("Basis")} ${basis.join(" · ")}` : ""].filter(Boolean);
|
||||||
|
const head = createText(title.join(" — "), "muted", 14);
|
||||||
|
head.dataset.sheetHead = name;
|
||||||
|
const rows: HTMLElement[] = [head];
|
||||||
|
if (notes.length) rows.push(createText(notes.join(" · "), "hint"));
|
||||||
|
const onInput = o.onInput;
|
||||||
|
if (onInput)
|
||||||
|
for (const n of vars)
|
||||||
|
rows.push(varRow(specs.get(n)!, o.values?.[n] ?? "", (v) => onInput(n, v)));
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subtotal = (g: SheetGroup): HTMLElement =>
|
||||||
|
createCalcFormulaRow(`${g.비목}${g.끝수 ? ` (${g.끝수})` : ""}`, null, out(g.소계, true));
|
||||||
|
|
||||||
/** 계산서 줄들 — 부르는 쪽 세로 칸(flex column)에 그대로 붙임 */
|
/** 계산서 줄들 — 부르는 쪽 세로 칸(flex column)에 그대로 붙임 */
|
||||||
export function calcSheetRows(o: SheetOptions): HTMLElement[] {
|
export function calcSheetRows(o: SheetOptions): HTMLElement[] {
|
||||||
const rows: HTMLElement[] = [];
|
const rows: HTMLElement[] = [];
|
||||||
const specs = new Map((o.onInput ? (o.inputs ?? []) : []).map((s) => [s.이름, s]));
|
const specs = new Map((o.inputs ?? []).map((s) => [s.이름, s]));
|
||||||
o.묶음.forEach((g, gi) => {
|
if (o.bundle) {
|
||||||
const vars = (o.uses?.(g, gi) ?? []).filter((n) => specs.has(n));
|
for (const b of bundleLines(o.묶음)) {
|
||||||
if (vars.length && o.onInput) {
|
const name = bundleName(b.묶음);
|
||||||
const onInput = o.onInput;
|
rows.push(...headRows(name, b.쓰는입력, o, specs, b.기준단위, b.환산근거));
|
||||||
const head = createText(g.비목, "muted", 14);
|
for (const line of b.줄) rows.push(...("몫" in line ? mergedRows(line) : [lineRow(line)]));
|
||||||
head.dataset.sheetHead = g.비목;
|
rows.push(createCalcFormulaRow(name, null, out(b.소계, true, 1)));
|
||||||
rows.push(head);
|
|
||||||
for (const n of vars)
|
|
||||||
rows.push(varRow(specs.get(n)!, o.values?.[n] ?? "", (v) => onInput(n, v)));
|
|
||||||
}
|
}
|
||||||
for (const line of g.줄)
|
rows.push(...o.묶음.map(subtotal)); // 맨 아래 비목 소계(서버 끝수 그대로) — 장비 노무는 여기서 인력 노무와 더해짐
|
||||||
rows.push(
|
} else {
|
||||||
createCalcFormulaRow(
|
for (const g of o.묶음) {
|
||||||
line.이름,
|
const uses = [...new Set(g.줄.flatMap((l) => l.쓰는입력 ?? []))];
|
||||||
createCalcFormula(sheetFormula(line.글)),
|
rows.push(...headRows(g.비목, uses, o, specs));
|
||||||
out(line.금액, false, 1),
|
rows.push(...g.줄.map((line) => lineRow(line)), subtotal(g));
|
||||||
),
|
}
|
||||||
);
|
}
|
||||||
rows.push(
|
|
||||||
createCalcFormulaRow(`${g.비목}${g.끝수 ? ` (${g.끝수})` : ""}`, null, out(g.소계, true)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
rows.push(createCalcFormulaRow(o.sumLabel, null, out(o.계, true)));
|
rows.push(createCalcFormulaRow(o.sumLabel, null, out(o.계, true)));
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* =============================================================================
|
/* =============================================================================
|
||||||
* M01_MasterData_UI_LogicLab_Est.ts
|
* M01_MasterData_UI_LogicLab_Est.ts
|
||||||
* 「인력·재료·경비」 보기(PLAN 714-2) — 공용 단가산출 계산서(`A00_Common/calc_sheet.ts`)에 시험 계산 읽는 식(`/text`)을 그대로.
|
* 「인력·재료·경비」 보기(PLAN 714-2) — 공용 단가산출 계산서(`A00_Common/calc_sheet.ts`)에 시험 계산 읽는 식(`/text`)을 그대로.
|
||||||
* 묶음 머리 변수 칸 = 그 묶음 줄들이 쓰는 시험 입력(714-3 `usedInputs`) · 바꾸면 시험 계산 입력 고침 + 다시 셈(정본 안 바뀜)
|
* 인력 · 재료 · 경비(장비) 묶음(714-1 줄 덧칸 `묶음`) · 묶음 머리 변수 칸 = 줄들의 `쓰는입력` · 바꾸면 시험 계산 입력 고침 + 다시 셈(정본 안 바뀜)
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import { el } from "@ui/ui_template_elements";
|
import { el } from "@ui/ui_template_elements";
|
||||||
@@ -9,7 +9,7 @@ import { createText } from "@ui/ui_template_text";
|
|||||||
import { calcSheetRows } from "../A00_Common/calc_sheet";
|
import { calcSheetRows } from "../A00_Common/calc_sheet";
|
||||||
import type { LogicInput } from "./M01_MasterData_UI_Logic_Api";
|
import type { LogicInput } from "./M01_MasterData_UI_Logic_Api";
|
||||||
import { safeReason } from "./M01_MasterData_UI_Logic_Text";
|
import { safeReason } from "./M01_MasterData_UI_Logic_Text";
|
||||||
import { usedInputs, type PreviewTextAnswer } from "./M01_MasterData_UI_LogicLab_Formula";
|
import type { PreviewTextAnswer } from "./M01_MasterData_UI_LogicLab_Formula";
|
||||||
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
|
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
|
||||||
|
|
||||||
export interface EstContext {
|
export interface EstContext {
|
||||||
@@ -31,18 +31,15 @@ export function buildEst(ctx: EstContext): HTMLElement {
|
|||||||
host.append(createText(safeReason(text.reason), "error"));
|
host.append(createText(safeReason(text.reason), "error"));
|
||||||
return host;
|
return host;
|
||||||
}
|
}
|
||||||
const names = ctx.inputs.map((x) => x.이름);
|
|
||||||
host.append(
|
host.append(
|
||||||
...calcSheetRows({
|
...calcSheetRows({
|
||||||
묶음: text.groups,
|
묶음: text.groups,
|
||||||
계: text.계,
|
계: text.계,
|
||||||
sumLabel: `${tl("Total")} (${ctx.unit || "원"})`,
|
sumLabel: `${tl("Total")} (${ctx.unit || "원"})`,
|
||||||
|
bundle: true,
|
||||||
inputs: ctx.inputs,
|
inputs: ctx.inputs,
|
||||||
values: ctx.values,
|
values: ctx.values,
|
||||||
onInput: ctx.onInput,
|
onInput: ctx.onInput,
|
||||||
uses: (_, gi) => [
|
|
||||||
...new Set(text.groups[gi].줄.flatMap((line) => usedInputs(line, names).map(([n]) => n))),
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return host;
|
return host;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 「텍스트 수식」 컨테이너 — 서버(`POST /text`)가 준 줄을 그대로 찍음(화면에서 다시 계산하지 않음)
|
* 「텍스트 수식」 컨테이너 — 서버(`POST /text`)가 준 줄을 그대로 찍음(화면에서 다시 계산하지 않음)
|
||||||
* 두 칸 — 왼쪽 = 변수 이름 식(`이름글`) · 오른쪽 = 값이 들어간 식(`글`) · 줄은 `짝` 번호로 같은 높이
|
* 두 칸 — 왼쪽 = 변수 이름 식(`이름글`) · 오른쪽 = 값이 들어간 식(`글`) · 줄은 `짝` 번호로 같은 높이
|
||||||
* 한쪽에 올리거나 누르면 같은 줄이 밝아짐 · 비목 제목 · 소계 · 끝수 한 마디 · 까닭 있는 줄은 옅게
|
* 한쪽에 올리거나 누르면 같은 줄이 밝아짐 · 비목 제목 · 소계 · 끝수 한 마디 · 까닭 있는 줄은 옅게
|
||||||
* 714-3 — 줄이 쓰는 입력(표 찾기 행 값 · 이름 조각)을 오른쪽 칸 아래 작은 단추로 · 누르면 그 자리에 고르기/글 칸 →
|
* 714-3 — 줄이 쓰는 입력(서버 `쓰는입력` · 없으면 표 찾기 행 값 · 이름 조각)을 오른쪽 칸 아래 작은 단추로 · 누르면 그 자리에 고르기/글 칸 →
|
||||||
* 바꾸면 `onInput`(시험 계산 입력 고침 + 다시 셈) · 정본은 안 바뀜(요구서 4-1 확정 8)
|
* 바꾸면 `onInput`(시험 계산 입력 고침 + 다시 셈) · 정본은 안 바뀜(요구서 4-1 확정 8)
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
@@ -123,10 +123,12 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void {
|
|||||||
const rows: HTMLElement[] = [];
|
const rows: HTMLElement[] = [];
|
||||||
const specs = ctx.onInput ? (ctx.inputs ?? []) : [];
|
const specs = ctx.onInput ? (ctx.inputs ?? []) : [];
|
||||||
const vars = (line: TextLine): HTMLElement[] => {
|
const vars = (line: TextLine): HTMLElement[] => {
|
||||||
const used = usedInputs(
|
const names = specs.map((x) => x.이름);
|
||||||
line,
|
const seen = new Map(usedInputs(line, names));
|
||||||
specs.map((x) => x.이름),
|
// 서버 「쓰는입력」(714-1 · 조건 · 재료 고르기 · 중간값까지) 이 있으면 그것 · 없으면 조각에서
|
||||||
);
|
const used: [string, string][] = line.쓰는입력
|
||||||
|
? line.쓰는입력.filter((n) => names.includes(n)).map((n) => [n, seen.get(n) ?? ""])
|
||||||
|
: [...seen];
|
||||||
if (!used.length || !ctx.onInput) return [];
|
if (!used.length || !ctx.onInput) return [];
|
||||||
const onInput = ctx.onInput;
|
const onInput = ctx.onInput;
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -148,6 +148,12 @@ export interface TextLine {
|
|||||||
짝?: number;
|
짝?: number;
|
||||||
금액: number;
|
금액: number;
|
||||||
까닭?: string;
|
까닭?: string;
|
||||||
|
/** 714-1 계산서 줄 덧칸 — 쓰는 로직 입력 · 묶음(인력 · 재료 · 경비 · 하위) · 장비 · 찾는 표 기준 · 환산 근거 */
|
||||||
|
쓰는입력?: string[];
|
||||||
|
묶음?: string | null;
|
||||||
|
장비?: boolean | null;
|
||||||
|
기준단위?: string | null;
|
||||||
|
환산근거?: string;
|
||||||
}
|
}
|
||||||
/** `POST /text` 답 — 서버가 만든 읽는 식 줄(화면은 그대로 찍음) */
|
/** `POST /text` 답 — 서버가 만든 읽는 식 줄(화면은 그대로 찍음) */
|
||||||
export type TextAnswer =
|
export type TextAnswer =
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ def test_부품_상자만():
|
|||||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||||
def test_식_글():
|
def test_식_글():
|
||||||
s = _text(SHEET)
|
s = _text(SHEET)
|
||||||
pure = s[s.index("export const sheetFormula") : s.index("/** 변수 칸 하나")]
|
pure = s[s.index("/* ── 순수 셈 ── */") : s.index("/* ── 순수 셈 끝 ── */")]
|
||||||
body = 'console.log(JSON.stringify([sheetFormula("268,908 * 0.12 = 32,269"), sheetFormula("59,020 * 1 * 8 / 165 = 2,862"), sheetFormula("")]))'
|
body = 'console.log(JSON.stringify([sheetFormula("268,908 * 0.12 = 32,269"), sheetFormula("59,020 * 1 * 8 / 165 = 2,862"), sheetFormula("")]))'
|
||||||
out = subprocess.run(
|
out = subprocess.run(
|
||||||
["node", "--experimental-strip-types", "--no-warnings",
|
["node", "--experimental-strip-types", "--no-warnings",
|
||||||
@@ -45,3 +45,40 @@ def test_식_글():
|
|||||||
) # fmt: skip
|
) # fmt: skip
|
||||||
assert out.returncode == 0, out.stderr
|
assert out.returncode == 0, out.stderr
|
||||||
assert json.loads(out.stdout) == ["268,908 × 0.12", "59,020 × 1 × 8 / 165", ""]
|
assert json.loads(out.stdout) == ["268,908 × 0.12", "59,020 × 1 × 8 / 165", ""]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||||
|
def test_묶음_장비_한_줄():
|
||||||
|
s = _text(SHEET)
|
||||||
|
pure = s[s.index("/* ── 순수 셈 ── */") : s.index("/* ── 순수 셈 끝 ── */")]
|
||||||
|
body = """
|
||||||
|
const eq = (n, a, 쓰는입력 = []) => ({ 이름: n, 글: "", 금액: a, 묶음: "경비", 장비: true, 쓰는입력, 기준단위: "일" });
|
||||||
|
const groups = [
|
||||||
|
{ 비목: "노무비", 소계: 0, 줄: [
|
||||||
|
{ 이름: "포설공", 글: "", 금액: 10, 묶음: "인력", 쓰는입력: [] },
|
||||||
|
eq("굴착기", 100, ["지역"]), eq("굴착기", 7),
|
||||||
|
{ 이름: "보통인부", 글: "", 금액: 5, 묶음: "인력", 쓰는입력: ["두께"], 기준단위: "10㎡", 환산근거: "10㎡당 → 1㎡당 ÷10" } ] },
|
||||||
|
{ 비목: "재료비", 소계: 0, 줄: [ eq("굴착기", 20), eq("굴착기", 2), { 이름: "모래", 글: "", 금액: 3 } ] },
|
||||||
|
{ 비목: "경비", 소계: 0, 줄: [ eq("굴착기", 30), eq("굴착기", null), { 이름: "공구손료", 글: "", 금액: 1, 묶음: "경비" } ] },
|
||||||
|
];
|
||||||
|
const got = bundleLines(groups).map((b) => ({ 묶음: b.묶음, 소계: b.소계, 쓰는입력: b.쓰는입력, 기준단위: b.기준단위, 환산근거: b.환산근거,
|
||||||
|
줄: b.줄.map((l) => "몫" in l ? [l.이름, l.금액, l.몫.map((x) => x.비목)] : l.이름) }));
|
||||||
|
console.log(JSON.stringify(got));
|
||||||
|
"""
|
||||||
|
out = subprocess.run(
|
||||||
|
["node", "--experimental-strip-types", "--no-warnings",
|
||||||
|
"--input-type=module-typescript", "-e", pure + chr(10) + body],
|
||||||
|
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||||
|
) # fmt: skip
|
||||||
|
assert out.returncode == 0, out.stderr
|
||||||
|
got = json.loads(out.stdout)
|
||||||
|
# 차례 = 인력 · 재료 · 경비 · 같은 이름 장비 둘은 k 번째끼리 한 줄(노 · 재 · 경 몫) · 비목 노무비 장비는 경비(장비) 묶음
|
||||||
|
assert [b["묶음"] for b in got] == ["인력", "재료", "경비"]
|
||||||
|
assert got[0]["줄"] == ["포설공", "보통인부"] and got[0]["소계"] == 15
|
||||||
|
assert got[0]["쓰는입력"] == ["두께"] and got[0]["기준단위"] == ["10㎡"]
|
||||||
|
assert got[0]["환산근거"] == ["10㎡당 → 1㎡당 ÷10"]
|
||||||
|
assert got[1]["줄"] == ["모래"]
|
||||||
|
eq = got[2]
|
||||||
|
assert eq["줄"][0] == ["굴착기", 150, ["노무비", "재료비", "경비"]]
|
||||||
|
assert eq["줄"][1] == ["굴착기", 9, ["노무비", "재료비", "경비"]]
|
||||||
|
assert eq["줄"][2] == "공구손료" and eq["소계"] == 160 and eq["쓰는입력"] == ["지역"]
|
||||||
|
|||||||
Reference in New Issue
Block a user