fix(751-1): 원화 꼴 흠 — M01 · M02 · 서버 글 — 공용 꼴 갈음 · 율 「4.0」 · 중간 값 · 마법사 콤마 · 서고 금액

- 파이썬 짝을 common_util/common_util_money.py 로 옮김(TS 와 같은 이름 · 서버 어디서나 불러옴)
- 시험 계산: 율 글자 「4.0 %」 → 숫자 4(서버 _pct) · 중간 값 경비 · 공구손료 등 비목 이름은 1,000 미만도 돈 꼴(870.2455 → 870)
- 기초 편집 단가 = 공용 단가 꼴 · 금액 = 소수 1 · 서고 기준 금액 · 구조물 조합 일위대가 금액 = 공용 정수 꼴(124,419.85 → 124,420)
- 마법사(확인 · 고르기 · 별칭 장부 · 엑셀 · 우리 금액): 콤마 없음 · 소수 셋 기본 · 부동소수 꼬리 걷음 · 품 비교 꼬리표 지수 꼴(1.235e+05) 걷음
- 다시 잇기 · 붙여넣기 대조 · 찾아 넣기 금액 열 끝 0 없음(22.9 원이 23 으로 숨지 않음) · 흐름 로직 텍스트 수식 비목 소계 정수
- 낡은 시험 고침: test_m02_hopyo_screen 품 비교 보통인부 0.0364 → 0.03636(수량 꼴 유효숫자 넷 규칙으로 파이썬 · TS 통일)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 22:54:02 +09:00
co-authored by Claude Sonnet 5.5
parent 27f33a8c53
commit 9e0746058e
25 changed files with 72 additions and 45 deletions
+1 -1
View File
@@ -266,7 +266,7 @@ def _pct(got: dict, body: CalcBody) -> dict:
except store.cm.mf.FormulaError:
return got
lines = labor_doc.pct_extras(whole, logic, got["lines"], got.get("middle") or {})
return {**got, "lines": lines}
return {**got, "lines": _num(lines)} # 율 Decimal(4.0) 이 글자 「4.0」 으로 나가지 않게
@router.post("/text")
+3 -1
View File
@@ -19,6 +19,7 @@ from contextvars import ContextVar
from decimal import Decimal, InvalidOperation
from typing import Any
from common_util import common_util_money as mfm
from M01_MasterData import M01_MasterData_Store as store
from M01_MasterData import M01_MasterData_Store_Make as make
from M01_MasterData import M01_MasterData_Store_Revise as revise
@@ -265,7 +266,8 @@ def text(key: str, inputs: dict, row: dict | None = None) -> dict | None:
groups: dict[str, list] = {}
for o in got["outputs"]:
value = o["value"]
shown = f"{value:,}" if isinstance(value, (int, Decimal)) else str(value)
# 비목 소계 = 정수 · 그 밖 = 수량 꼴
shown = (mfm.fmt_money if o["cost"] else mfm.fmt)(value)
line = {"이름": o["name"], "글": shown, "이름글": o["name"], "금액": value, "조각": []}
groups.setdefault(o["cost"] or "값", []).append(line)
out = [{"비목": k, "줄": v} for k, v in groups.items()]
@@ -213,6 +213,9 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
);
}
/** 중간 값 이름이 돈 꼴(경비 · 노무비 · 공구손료 · 금액 · 단가 · 노임) — 1,000 미만이어도 돈 꼴 */
const MONEY_NAME = /(비|료|액|가|임)$/;
/** `money` = 결과가 돈(원) — 그때만 결과 값도 정수 · 수량·비율 결과는 소수 그대로 */
export function answerView(answer: CalcAnswer, draft: boolean, money: boolean): HTMLElement[] {
const note = draft
@@ -303,7 +306,7 @@ export function answerView(answer: CalcAnswer, draft: boolean, money: boolean):
el("dt", { text: k }),
el("dd", {
text:
money && typeof v === "number" && Math.abs(v) >= 1000
money && typeof v === "number" && (Math.abs(v) >= 1000 || MONEY_NAME.test(k))
? formatMoney(v)
: formatNumber(v),
}),
@@ -20,7 +20,7 @@ import { tx } from "./M01_MasterData_UI_Logic_Text";
import { createBadge } from "@ui/ui_template_badge";
import { createText } from "@ui/ui_template_text";
import { createIconButton } from "@ui/ui_template_button";
import { formatNumber } from "../common_util/common_util_money";
import { formatLineMoney, formatNumber, formatPrice } from "../common_util/common_util_money";
export type PickMode = "element" | "table";
export type PickDone = (item: ElementBrief, column?: string) => void;
@@ -272,14 +272,14 @@ function inputsSection(
function priceCell(ctx: EditContext, item: HoLine, i: number): HTMLElement {
const line = ctx.lines?.[i];
if (line) return el("span", { className: "m01-logic__money", text: formatNumber(line.단가) });
if (line) return el("span", { className: "m01-logic__money", text: formatPrice(line.단가) });
if (item.종류 === "로직" || item.요소.includes("{")) {
return el("span", { className: "ui-text ui-text--muted", text: tx("Ho_PriceLater") });
}
const brief = ctx.prices[item.요소];
if (brief === undefined) return el("span", { className: "ui-text ui-text--muted", text: "" });
if (brief === null) return createText(tx("Ho_Missing"), "error");
return el("span", { className: "m01-logic__money", text: formatNumber(brief.값) });
return el("span", { className: "m01-logic__money", text: formatPrice(brief.값) });
}
function hoSection(
@@ -343,7 +343,10 @@ function hoSection(
textBox(item.단위 ?? "", (v) => set(() => (item.단위 = v)), "m01-logic__unit"),
el("div", { className: "m01-logic__pair", children: [tag, qty, table] }),
priceCell(ctx, item, i),
el("span", { className: "m01-logic__money", text: formatNumber(ctx.lines?.[i]?.금액 ?? "") }),
el("span", {
className: "m01-logic__money",
text: formatLineMoney(ctx.lines?.[i]?.금액 ?? ""),
}),
cost,
dropButton(() => {
list.splice(i, 1);
+11 -2
View File
@@ -29,6 +29,7 @@ import re
from decimal import Decimal
from typing import Any
from common_util import common_util_money as money_fmt
from M02_MasterTemplete import M02_Hopyo_Combine as combine
from M02_MasterTemplete import M02_Hopyo_Elem as elem
from M02_MasterTemplete import M02_Hopyo_Finger as finger
@@ -227,14 +228,22 @@ class _Plan:
for x in rows:
self.drop(x["cid"])
if want < -TINY:
self.miss("노임", t, a, f"엑셀 {a:.6g} · 더한 기초만 {f:.6g}", 단위="인")
self.miss(
"노임",
t,
a,
f"엑셀 {money_fmt.fmt(a)} · 더한 기초만 {money_fmt.fmt(f)}",
단위="인",
)
elif not rows:
if t in self.lb:
unit = "인"
self.add({"종류": "인력", "요소": self.lb[t], "이름": t, "단위": unit,
"수량": _num(want * self.tb)}) # fmt: skip
else:
self.miss("노임", t, want, f"엑셀 {a:.6g} — 우리 인력에 없음", 단위="인")
self.miss(
"노임", t, want, f"엑셀 {money_fmt.fmt(a)} — 우리 인력에 없음", 단위="인"
)
elif len(rows) == 1 or not b:
self.cover(rows[0]["cid"], qty=want * self.tb)
for x in rows[1:]:
+2 -2
View File
@@ -12,6 +12,7 @@ from __future__ import annotations
from typing import Any
from common_util import common_util_money as money_fmt
from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis
from M02_MasterTemplete import M02_Hopyo_Combine as combine
from M02_MasterTemplete import M02_Hopyo_Finger as finger
@@ -20,8 +21,7 @@ from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
from M02_MasterTemplete import M02_Hopyo_Option as option
def _num(x: float) -> str:
return f"{x:.4g}"
_num = money_fmt.fmt # 꼬리표 수 — 공용 수량 꼴(1만 넘어도 지수 꼴 아님)
def excel_qty(efp: dict[str, Any]) -> dict[str, Any]:
@@ -175,7 +175,7 @@ export function openFindPicker(opt: FindPickOptions): void {
{ key: "품명", label: tc("Std_Name"), width: 200 },
{ key: "규격", label: tc("Std_Spec"), width: 110 },
{ key: "단위", label: tc("Std_Unit"), width: 44 },
{ key: "금액", label: tc("Std_Money"), kind: "number", width: 84 },
{ key: "금액", label: tc("Std_Money"), kind: "number", decimals: 1, trim: true, width: 84 },
{ key: "구분", label: L("M02_FindClass"), width: 180 },
{ key: "출처", label: tc("Std_Source"), width: 120 },
],
@@ -53,6 +53,7 @@ import { openLaborInput } from "./M02_MasterTemplete_Labor_Input";
import { draftDoc } from "./logic/M02_MasterTemplete_Hopyo_Draft";
import { toItem } from "./M02_MasterTemplete_Hopyo_ToItem";
import { openLeftOf } from "./M02_MasterTemplete_Hopyo_Left";
import { formatNumber, formatPrice } from "../common_util/common_util_money";
import "./M02_MasterTemplete_Hopyo.css";
export const POLL_MS = 3000;
@@ -161,8 +162,7 @@ export function leftOut(lines: HopyoLine[], d: CheckDraft): Record<string, numbe
return out;
}
const n4 = (v: number | undefined): string =>
v === undefined ? "–" : String(Math.round(v * 10000) / 10000);
const n4 = (v: number | undefined): string => (v === undefined ? "–" : formatNumber(v));
/** 품 비교 글 — 「콘크리트공 0.1091 → 0.1091 ✓ · 굴착기 0.29 → 0.31 ≠」 */
export function compareText(q: HopyoMatch["품"] | undefined): string {
@@ -201,7 +201,7 @@ export const sureLines = (lines: HopyoLine[], d: CheckDraft): HopyoLine[] =>
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
const won = (v: number | null | undefined): string =>
v === null || v === undefined ? "" : v.toLocaleString("ko-KR");
v === null || v === undefined ? "" : formatPrice(v);
const fail = (error: unknown): void => showToast(`실패 — ${why(error)}`, "error");
const note = (text: string): HTMLElement =>
el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } });
@@ -467,7 +467,7 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
children: [
td(x.이름),
td(x.규격),
td(x.수량 === null ? "" : String(x.수량), "m02-hopyo__num"),
td(x.수량 === null ? "" : formatNumber(x.수량), "m02-hopyo__num"),
td(x.단위),
td(won(x.금액), "m02-hopyo__num"),
td(x.까닭),
@@ -26,6 +26,7 @@ import type { LaborCalc } from "./M02_MasterTemplete_Labor_Api";
import { markCells, markHeads } from "./M02_MasterTemplete_Labor_MatMarks";
import { moneyCell, PRICE_SLOTS } from "./M02_MasterTemplete_Labor_Rows";
import { tc } from "./M02_MasterTemplete_Labor_Text";
import { formatPrice } from "../common_util/common_util_money";
import { tableOf } from "./logic/M02_MasterTemplete_Hopyo_Draft";
/** 드롭다운 — 저장 값(서버 뺌 까닭) → 화면 글 */
@@ -248,7 +249,7 @@ function matRow(line: HopyoLine, ctx: RowsContext): HTMLElement {
const first = mats[0];
const ours = mats.length ? mats.reduce((s, m) => s + (m.반영 ?? 0), 0) : null;
const show = (v: number | null | undefined): string =>
wait ? "…" : v === null || v === undefined ? "—" : v.toLocaleString("ko-KR");
wait ? "…" : v === null || v === undefined ? "—" : formatPrice(v);
const p = parts[0];
return rowOf(line, ctx, [
el("td", { className: "m02-labrow__no", text: String(line.차례 + 1) }),
@@ -24,6 +24,7 @@ import type {
} from "./M02_MasterTemplete_Hopyo_Api";
import { getActiveLayer, getActiveOwner } from "./M02_MasterTemplete_Api_Fetch";
import { type FindKind, openFindPicker } from "./M02_MasterTemplete_Find_Pick";
import { formatNumber } from "../common_util/common_util_money";
import "./M02_MasterTemplete_Hopyo.css";
const MATERIAL = new Set(["자재", "별도계상"]);
@@ -34,7 +35,7 @@ const FOLD_TEXT: Record<string, string> = {
const PRE_TIP = "미리 고름 — 엑셀 인용 조항 = 1위 기초 조항 · 누르기는 사용자";
const note = (text: string): HTMLElement =>
el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } });
const num = (v: number): string => String(Math.round(v * 10000) / 10000);
const num = formatNumber;
/** 대상 글 — 「이름 [단위]」 · 자재는 규격도 · 호표 = 「제N호표」 */
export const targetText = (t: HopyoTarget | null | undefined): string => {
@@ -52,7 +53,10 @@ export const inputsText = (
): string =>
[
Object.entries(values)
.map(([k, v]) => `${k}=${String(v)}${from[k] && from[k] !== "자동값" ? `(${from[k]})` : ""}`)
.map(
([k, v]) =>
`${k}=${formatNumber(v)}${from[k] && from[k] !== "자동값" ? `(${from[k]})` : ""}`,
)
.join(", "),
unread.length ? `⚠ 못 가름: ${unread.join(", ")}` : "",
]
@@ -27,6 +27,7 @@ import { fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api";
import { dateTimeText } from "../common_util/common_util_time";
import { openLaborInput } from "./M02_MasterTemplete_Labor_Input";
import { openFindPicker } from "./M02_MasterTemplete_Find_Pick";
import { formatNumber } from "../common_util/common_util_money";
import "./M02_MasterTemplete_Hopyo.css";
export const LEDGER_TITLE = "별칭 장부";
@@ -108,7 +109,7 @@ export const pairText = (p: LedgerPair): string =>
`${p.로직 ?? p.자재 ?? ""} × ${p.배}`,
p.몫 != null ? `몫 ${p.몫}` : "",
Object.entries(p.입력값 ?? {})
.map(([k, v]) => `${k}=${String(v)}`)
.map(([k, v]) => `${k}=${formatNumber(v)}`)
.join(", "),
]
.filter(Boolean)
@@ -22,6 +22,7 @@ import {
} from "./M02_MasterTemplete_Hopyo_Api";
import type { LaborDoc } from "./M02_MasterTemplete_Labor_Api";
import { lineId } from "./M02_MasterTemplete_Labor_Calc";
import { formatMoney } from "../common_util/common_util_money";
type Kind = "노임" | "자재";
const COST: Record<Kind, "노무비" | "재료비"> = { 노임: "노무비", 자재: "재료비" };
@@ -37,8 +38,7 @@ export interface RelinkSource {
price?: (kind: Kind, key: string) => number | null | undefined;
}
const won = (v: number | null | undefined): string =>
v == null ? "-" : Math.round(v).toLocaleString("ko-KR");
const won = (v: number | null | undefined): string => (v == null ? "-" : formatMoney(v));
/** 사전조합 여럿(붙여넣기 · 실무자료 들여오기 뒤) — 서버가 쓰고 전후 1 단위 값 */
export function relinkKeys(keys: string[]): RelinkSource {
@@ -152,11 +152,11 @@ export async function openRelink(src: RelinkSource): Promise<void> {
{ key: "pick", label: "고름", width: 44 },
{ key: "unit", label: "단위", width: 44 },
{ key: "qty", label: "수량", kind: "number", decimals: 4, trim: true, width: 60 },
{ key: "pasted", label: "붙인 단가", kind: "number", decimals: 1, width: 84 },
{ key: "pasted", label: "붙인 단가", kind: "number", decimals: 3, trim: true, width: 84 },
{ key: "now", label: src.priceLabel, width: 84 },
{ key: "cand", label: "이음 후보 (누름 = 다음)", width: 210 },
{ key: "price", label: "후보 단가", kind: "number", decimals: 1, width: 84 },
{ key: "gap", label: "금액 차이", kind: "number", decimals: 1, width: 84 },
{ key: "price", label: "후보 단가", kind: "number", decimals: 3, trim: true, width: 84 },
{ key: "gap", label: "금액 차이", kind: "number", decimals: 1, trim: true, width: 84 },
],
rows: data,
frozen: 1,
@@ -58,6 +58,7 @@ import {
import { builtWord } from "./M02_MasterTemplete_Hopyo_Choose";
import { openRelink, relinkKeys } from "./M02_MasterTemplete_Hopyo_Relink";
import { leftButton } from "./M02_MasterTemplete_Hopyo_Left";
import { formatPrice } from "../common_util/common_util_money";
import "./M02_MasterTemplete_Hopyo.css";
export interface HopyoSheetOptions {
@@ -86,7 +87,7 @@ const DANSAN = "단가산출";
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
const won = (v: number | null | undefined): string =>
v === null || v === undefined ? "" : v.toLocaleString("ko-KR");
v === null || v === undefined ? "" : formatPrice(v);
const note = (text: string): HTMLElement =>
el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } });
@@ -16,6 +16,7 @@ import "../M01_MasterData/M01_MasterData_UI_Style.css";
import { fetchTree, type Layer, type TreeItem } from "./M02_MasterTemplete_Api_Fetch";
import { folderOf, leafName, matchText } from "./M02_MasterTemplete_TreePanel_Model";
import { createEmpty } from "@ui/ui_template_empty";
import { formatMoney } from "../common_util/common_util_money";
/** 목록 종류 — 하위 컨테이너 셋 + 항목 통째(복사 생성) */
export type ListKind = "structure" | "area" | "labor" | "item";
@@ -54,7 +55,7 @@ const SIZE = 30;
const DASH = "—";
const day = (iso?: string): string => dateText(iso, "");
const money = (value: number | null | undefined): string =>
typeof value === "number" ? value.toLocaleString("ko-KR", { maximumFractionDigits: 0 }) : DASH;
typeof value === "number" ? formatMoney(value) : DASH;
/** 가장 늦게 고친 날 — 항목 통째 */
const latest = (row: ItemRow): string =>
day(
@@ -23,6 +23,7 @@ import {
import { openRelink, relinkDoc, relinkKeys } from "./M02_MasterTemplete_Hopyo_Relink";
import type { LaborDoc } from "./M02_MasterTemplete_Labor_Api";
import { lineId } from "./M02_MasterTemplete_Labor_Calc";
import { formatMoney } from "../common_util/common_util_money";
const COLS_KEY = "aislo:hopyo-paste-cols";
const FIELDS = ["품명", "규격", "수량", "단위", "금액", "노", "재", "경"] as const;
@@ -40,8 +41,7 @@ const NEED = new Set(["품명", "수량", "단위"]);
const letter = (n: number): string =>
n < 26 ? String.fromCharCode(65 + n) : `${letter(Math.floor(n / 26) - 1)}${letter(n % 26)}`;
const won = (v: number | null | undefined): string =>
v == null ? "-" : Math.round(v).toLocaleString("ko-KR");
const won = (v: number | null | undefined): string => (v == null ? "-" : formatMoney(v));
export function rememberedCols(): PasteCols | null {
try {
@@ -169,7 +169,7 @@ export function builtNotice(got: HopyoBuiltAll, keys: string[]): void {
{ key: "name", label: "합계 다른 호표", width: 260 },
{ key: "excel", label: "엑셀", kind: "number" },
{ key: "ours", label: "우리", kind: "number" },
{ key: "gap", label: "차이", kind: "number", decimals: 1 },
{ key: "gap", label: "차이", kind: "number", decimals: 1, trim: true },
],
rows,
}).root,
@@ -154,7 +154,7 @@ export function openLogicPicker(
{ key: "품명", label: tc("Std_Name"), width: 220 },
{ key: "규격", label: tc("Std_Spec"), width: 220 },
{ key: "단위", label: tc("Std_Unit"), width: 56 },
{ key: "금액", label: tc("Std_Money"), kind: "number", width: 96 },
{ key: "금액", label: tc("Std_Money"), kind: "number", decimals: 1, trim: true, width: 96 },
{ key: "출처", label: tc("Std_Source"), width: 150 },
],
rows: [],
@@ -34,6 +34,7 @@ import {
type TemplateInfo,
} from "./M02_MasterTemplete_Api_Fetch";
import { tc } from "./M02_MasterTemplete_Labor_Text";
import { formatMoney } from "../common_util/common_util_money";
/** 목록 줄 — 서버가 문서 `이름`(제목) · 단위 · 1 단위 기준 계를 실어 보냄 */
type Row = TemplateInfo & {
@@ -63,8 +64,7 @@ export interface LibraryListOptions {
const SIZE = 30;
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
const money = (v: number | null | undefined): string =>
v == null ? "" : v.toLocaleString("ko-KR", { maximumFractionDigits: 2 });
const money = (v: number | null | undefined): string => (v == null ? "" : formatMoney(v));
export const libraryLabel = (row: Row): string => (row.제목 ? `${row.이름} ${row.제목}` : row.이름);
/** 새 단가산출 몸(717-2a) — 바탕 없음 · 빈 더한줄 · 단위 「식」(서버 필수 · 상세에서 고침 · 717-2b) */
@@ -40,6 +40,7 @@ import {
} from "./M02_MasterTemplete_ItemCards";
import { layerSource } from "./M02_MasterTemplete_ItemSource";
import { pickHere, pickedName, type From } from "./M02_MasterTemplete_ItemPick";
import { formatMoney } from "../common_util/common_util_money";
import "./M02_MasterTemplete_Structure.css";
export type { CardKind, EditKind, SheetKind } from "./M02_MasterTemplete_ItemCards";
@@ -76,7 +77,7 @@ export type ColumnNumbers = Record<string, StructureNumbers>;
/** 집계표 「일위대가 합계」 줄 글(86-3) — 반영합 · 없으면 기준합 · 계산값 없음 = 빈 글(「—」) · 색은 `laborSumState` */
export function laborSumText(numbers: StructureNumbers | undefined): string {
if (typeof numbers?.일위대가합계 !== "number") return "";
return numbers.일위대가합계.toLocaleString("ko-KR", { maximumFractionDigits: 0 });
return formatMoney(numbers.일위대가합계);
}
/** 합계 칸 색 상태 · 툴팁 까닭(101-1) — 계산값 없음 · 담은 것 없음 = 색 없음 */
+1 -1
View File
@@ -6,7 +6,7 @@
* formatPrice 단가 — 200 원 미만 = 유효숫자 넷 · 그 밖 = 최대 소수 3 · 끝 0 뗌
* formatNumber 수량 · 율 · 품 — 소수 넷째 자리 · 유효숫자 넷 중 많은 쪽 · 끝 0 뗌
* 값이 숫자가 아니면 빈 값 = 「」 · 객체 = JSON 글 · 그 밖 = String.
* 짝: `resources/master_data/scripts/master_fmt.py` (거울 시험 `resources/tester/test_588_2_money_fmt.py`).
* 짝: `common_util/common_util_money.py` (거울 시험 `resources/tester/test_588_2_money_fmt.py`).
* ========================================================================== */
const LOCALE = "ko-KR";
+3 -3
View File
@@ -1,4 +1,4 @@
"""원화 꼴 한 벌 거울 시험 — `master_fmt.py`(파이썬) ↔ `common_util_money.ts`(TS · Node) 가 같은 글.
"""원화 꼴 한 벌 거울 시험 — `common_util_money.py`(파이썬) ↔ `common_util_money.ts`(TS · Node) 가 같은 글.
CLAUDE.md 5장 ① 짝. 값은 숫자 글(`num`) · 숫자 아닌 글(`text`) · 빈 값(None).
"""
@@ -10,9 +10,9 @@ import sys
from decimal import Decimal
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "master_data", "scripts"))
sys.path.insert(0, os.path.join(HERE, "..", ".."))
import master_fmt as mf # noqa: E402
from common_util import common_util_money as mf # noqa: E402
NUMS = [
"0", "1", "2.5", "-1234.5", "1.00005", "0.12345", "0.00167", "22.9", "1846.39",
+2 -2
View File
@@ -413,13 +413,13 @@ def test_실물_응답_대역_확인_창():
assert remicon["상태"] == "맞음" and remicon["checked"] and remicon["checkable"]
assert (
remicon["compare"]
== "콘크리트공 0.1091 → 0.1091 ✓ · 보통인부 0.0364 → 0.0364 ✓ · 굴착기 0.29 → 0.2909 ✓"
== "콘크리트공 0.1091 → 0.1091 ✓ · 보통인부 0.03636 → 0.03636 ✓ · 굴착기 0.29 → 0.2909 ✓"
)
assert (
remicon["inputs"]
== "타설방법=장비사용타설(규격), 구조물=철근구조물(규격), 소량감률=50(블록)"
)
assert remicon["qty"] == "콘크리트공 0.1091 · 보통인부 0.0364 · 굴착기 0.29hr"
assert remicon["qty"] == "콘크리트공 0.1091 · 보통인부 0.03636 · 굴착기 0.29hr"
assert remicon["cands"][0][1] == "맞음" and remicon["cands"][0][2].startswith("조항 건설품셈")
rebar = by["철근가공조립(현장)"]
assert rebar["상태"] == "다른 점" and not rebar["checked"] and rebar["checkable"]
+2 -2
View File
@@ -13,6 +13,7 @@
import "./ui_template_group_cards.css";
import { createButton } from "./ui_template_elements";
import { formatLineMoney } from "../common_util/common_util_money";
/** 비목 셋 — 노무 · 재료 · 경비 */
export type GroupParts = [number | null, number | null, number | null];
@@ -81,9 +82,8 @@ export const PART_LABELS = ["노무", "재료", "경비"] as const;
export const clampColumns = (value: unknown): number =>
GROUP_COLUMNS.includes(value as 1) ? (value as number) : 2;
const won = new Intl.NumberFormat("ko-KR", { maximumFractionDigits: 1 });
const plain = (value: number | null | undefined): string =>
value === null || value === undefined || !Number.isFinite(value) ? "" : won.format(value);
value === null || value === undefined || !Number.isFinite(value) ? "" : formatLineMoney(value);
/** 줄 비목 풍선 — 「노무 1,234 · 재료 0 · 경비 56」 · 셋 다 빈칸 = 없음 */
export function partsTip(
+2 -2
View File
@@ -53,6 +53,7 @@ import { createEditView, type EditView } from "./ui_template_item_picker_edit";
import { createComboTable } from "./ui_template_item_picker_table";
import { createToolbar } from "./ui_template_toolbar";
import { createText } from "./ui_template_text";
import { formatMoney } from "../common_util/common_util_money";
export type { SummaryItem } from "./ui_template_item_picker_data";
@@ -212,8 +213,7 @@ export function openItemPicker(opts: ItemPickerOptions): Promise<ItemPickerResul
const spec = chosen ? specOf(chosen) : null;
specHost.hidden = !spec || spec.열.length < 2;
if (!spec || specHost.hidden) return void specHost.replaceChildren();
const sum = (n: number | null): string =>
n === null ? "" : ` · ${n.toLocaleString("ko-KR", { maximumFractionDigits: 0 })}`;
const sum = (n: number | null): string => (n === null ? "" : ` · ${formatMoney(n)}`);
const buttons = spec.열.map((col) => {
const button = createButton({
label: `${spec.이름}=${col.이름}${sum(col.합계)}`,
@@ -15,6 +15,7 @@
import { createButton, el, showConfirmDialog, showToast } from "./ui_template_elements";
import { readTemplate } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
import { formatMoney } from "../common_util/common_util_money";
import { dateText } from "../common_util/common_util_time";
import { calcLabor, type LaborDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api";
import type { EditKind, ItemCardsHandle } from "../M02_MasterTemplete/M02_MasterTemplete_ItemCards";
@@ -239,7 +240,7 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
async function paintCalc(from: ProjectItemInfo, mine: number): Promise<void> {
const got = await calcCopy(from.폴더).then(summarize, () => null);
if (!got || mine !== seq) return;
const total = Math.round(got.total).toLocaleString();
const total = formatMoney(got.total);
status.append(
el("span", {
className: "ui-item-picker__calc",