diff --git a/B09_Estimation/B09_Estimation_ResourceAxis_Dump.py b/B09_Estimation/B09_Estimation_ResourceAxis_Dump.py deleted file mode 100644 index be17f9c0..00000000 --- a/B09_Estimation/B09_Estimation_ResourceAxis_Dump.py +++ /dev/null @@ -1,94 +0,0 @@ -"""B09 원가계산 — 자원 축 **조사용 덤프** (`B09_Estimation_ResourceAxis` 에서 갈라냄). - -⚠ **정본이 아니다.** 자원 축의 정본은 `build_unit_prices` 가 부를 때마다 - **메모리에서 새로 돈 값**이고, 이 파일이 쓰는 JSON 은 시험·조사용 자취일 뿐이다 - (명세 1장 2026-09-13 정정 — 옛 덤프 418/497 을 정본으로 읽어 추산이 틀렸던 자리). - 부르는 곳이 없어도 지우지 않는다. -⚠ **왜 갈랐나** — 본 파일이 700줄 제한을 넘어(732줄) 조인 키 규칙을 더하기 전에 뗐다. -""" - -from __future__ import annotations - -import hashlib -import json -import os -from typing import Any - -from B09_Estimation.B09_Estimation_ResourceAxis import ( - _MASTER_SUBPATH, - AxisResult, - _project_root, -) - -#: 자원 축 산출물이 나가는 자리 — **메인의 `data_work_item_master/` 안에 넣지 않는다.** -#: 메인이 품셈을 다시 돌리면 그 폴더가 덮이므로 섞으면 사라진다. -OUTPUT_SUBPATH = ("resources", "master_data", "old") - - -def _master_file_fingerprint(master: dict[str, Any]) -> dict[str, str]: - """공종 마스터 **파일 자체**의 지문. 낡은 파생물을 드러내는 유일한 근거다. - - `dataset_version`(품셈 원판 지문)은 마스터가 다시 생성돼도 그대로라, 그것만 - 적어 두면 「내 자원 축이 옛 마스터에서 나왔다」는 사실이 안 보인다. - """ - effective_date = master.get("effective_date", "") - file_name = f"work_item_master_{effective_date}.json" - path = os.path.join(_project_root(), *_MASTER_SUBPATH, file_name) - try: - with open(path, "rb") as handle: - digest = hashlib.sha256(handle.read()).hexdigest() - except OSError: - return {"file": file_name, "sha256": ""} - return {"file": file_name, "sha256": digest} - - -def write_resource_axis( - result: AxisResult, - master: dict[str, Any], - *, - output_dir: str | None = None, -) -> dict[str, str]: - """자원 축과 못 맞춘 목록을 파일로 낸다. 만든 파일 경로를 돌려준다.""" - directory = output_dir or os.path.join(_project_root(), *OUTPUT_SUBPATH) - os.makedirs(directory, exist_ok=True) - effective_date = master.get("effective_date", "") - - axis_path = os.path.join(directory, f"resource_axis_{effective_date}.json") - unmatched_path = os.path.join(directory, f"unmatched_{effective_date}.json") - - axis_payload = { - "schema_version": "1.0", - "dataset_id": "resource_axis_forest", - "effective_date": effective_date, - # 어느 공종 축 판에 붙인 것인지 — 세 쪽을 그대로 옮겨 적는다(PLAN 9-2). - "source_dataset_version": master.get("dataset_version", {}), - # ⚠ 위 지문은 **품셈 원판**의 것이라 B08 이 마스터를 다시 생성해도 안 움직인다. - # 낡음을 실제로 드러내려면 **마스터 파일 자체의 지문**이 있어야 한다. - "source_master_file": _master_file_fingerprint(master), - "policy": { - "axis": "resource_only", - "work_item_axis_owner": "B08", - "material_amounts_are_before_surcharge": True, - }, - "stats": { - "rows": len(result.rows), - "unmatched": len(result.unmatched), - "skipped_forms": result.skipped_forms, - }, - "rows": [r.as_dict() for r in result.rows], - } - unmatched_payload = { - "schema_version": "1.0", - "effective_date": effective_date, - "note": ( - "못 맞춘 자원 이름. 빈칸으로 두지 않고 여기 모은다. " - "기계·자재 카탈로그가 아직 없어 그 계열은 전부 여기로 온다." - ), - "rows": [u.as_dict() for u in result.unmatched], - } - - for path, payload in ((axis_path, axis_payload), (unmatched_path, unmatched_payload)): - with open(path, "w", encoding="utf-8", newline="\n") as handle: - json.dump(payload, handle, ensure_ascii=False, indent=2, sort_keys=True) - - return {"resource_axis": axis_path, "unmatched": unmatched_path} diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab.ts b/M01_MasterData/M01_MasterData_UI_LogicLab.ts index e718224b..236bc196 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab.ts @@ -23,6 +23,7 @@ import { fetchLogicSubs, saveFiles, type CalcLine, + type TextAnswer, type ElementBrief, type LogicRow, type LogicSummary, @@ -52,6 +53,7 @@ interface Opened extends Draft { prices: Record; reasons: string[]; lines: CalcLine[] | null; + text: TextAnswer | null; values: Record; } @@ -151,6 +153,9 @@ export async function mountM01LogicLab( row: current.row as LogicRow, dirty: () => current.id in drafts, values: current.values, + onText: (text) => { + current.text = text; + }, onLines: (lines) => { current.lines = lines; drawEditor(); @@ -178,6 +183,7 @@ export async function mountM01LogicLab( reasons: current.reasons, prices: current.prices, lines: current.lines, + text: current.text, values: current.values, onChange: touch, calcHost: calc, @@ -194,7 +200,16 @@ export async function mountM01LogicLab( const open = async (id: string, sub: string, key: string): Promise => { const draft = drafts[id]; if (draft?.origKey === null) { - show({ ...draft, id, original: null, prices: {}, reasons: [], lines: null, values: {} }); + show({ + ...draft, + id, + original: null, + prices: {}, + reasons: [], + lines: null, + text: null, + values: {}, + }); return; } try { @@ -210,6 +225,7 @@ export async function mountM01LogicLab( prices: one.prices, reasons: one.reasons, lines: null, + text: null, values: {}, }); } catch (error) { diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts index 6ac05a13..ff323a85 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts @@ -13,6 +13,7 @@ import type { HoLine, LogicRow, NamedFormula, + TextAnswer, } from "./M01_MasterData_UI_Logic_Api"; import { formatNumber, qtyKind } from "./M01_MasterData_UI_Logic_Edit"; import { tx } from "./M01_MasterData_UI_Logic_Text"; @@ -28,6 +29,8 @@ export interface DetailContext { prices: Record; /** 마지막 시험 계산의 줄 — 호표 차례 뒤에 덧줄 */ lines: CalcLine[] | null; + /** 마지막 시험 계산의 읽는 식 줄(서버가 줌) */ + text: TextAnswer | null; values: Record; onChange: () => void; /** 시험 계산 칸(`buildCalc` 가 채움) — 넷째 컨테이너 안에 놓음 */ @@ -280,7 +283,7 @@ function hoBox(ctx: DetailContext): HTMLElement { /** 상세 화면 넷을 `host` 에 쌓음 */ export function buildLabDetail(host: HTMLElement, ctx: DetailContext): void { const formula = el("div", { className: "m01lab__formula" }); - buildFormula(formula, { row: ctx.row, lines: ctx.lines }); + buildFormula(formula, { text: ctx.text }); host.replaceChildren( infoBox(ctx), hoBox(ctx), diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts index 80630f74..12b14727 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts @@ -1,19 +1,60 @@ /* ============================================================================= * M01_MasterData_UI_LogicLab_Formula.ts - * 「텍스트 수식」 컨테이너 자리 — 비목 묶음별 `이름 : 단가 * 수량식 = 금액` 줄은 여기서 채움(PLAN 3-2) + * 「텍스트 수식」 컨테이너 — 서버(`POST /text`)가 준 줄을 그대로 찍음(화면에서 다시 계산하지 않음) + * 비목 묶음 제목 · 줄(`이름 : 글`) · 소계 · 끝수 한 마디 · 까닭 있는 줄은 옅게 * ========================================================================== */ import { el } from "@ui/ui_template_elements"; -import type { CalcLine, LogicRow } from "./M01_MasterData_UI_Logic_Api"; +import type { TextAnswer } from "./M01_MasterData_UI_Logic_Api"; +import { formatNumber } from "./M01_MasterData_UI_Logic_Edit"; +import { tx } from "./M01_MasterData_UI_Logic_Text"; import { tl } from "./M01_MasterData_UI_LogicLab_Text"; export interface FormulaContext { - row: LogicRow; - /** 마지막 시험 계산의 줄 — 호표 차례 뒤에 덧줄 */ - lines: CalcLine[] | null; + text: TextAnswer | null; } -/** 자리만 — 줄을 채우는 일감이 `host` 안에 그림 */ -export function buildFormula(host: HTMLElement, _ctx: FormulaContext): void { - host.replaceChildren(el("p", { className: "m01-logic__muted", text: tl("Formula_Slot") })); +const muted = (text: string): HTMLElement => el("p", { className: "m01-logic__muted", text }); + +export function buildFormula(host: HTMLElement, ctx: FormulaContext): void { + const text = ctx.text; + if (!text) { + host.replaceChildren(muted(tl("Formula_Slot"))); + return; + } + if (!text.ok) { + host.replaceChildren( + el("div", { + className: "m01-logic__reasons", + children: [el("strong", { text: tx("Calc_Stopped") }), el("div", { text: text.reason })], + }), + ); + return; + } + const blocks = text.groups.flatMap((g) => [ + el("h4", { className: "m01lab__group", text: g.비목 }), + ...g.줄.map((line) => + el("div", { + className: `m01lab__text${line.까닭 ? " m01lab__text--off" : ""}`, + attrs: { "data-text-line": line.이름 }, + children: [ + el("div", { text: `${line.이름} : ${line.글}` }), + ...(line.까닭 ? [el("div", { className: "m01-logic__muted", text: line.까닭 })] : []), + ], + }), + ), + el("p", { + className: "m01lab__subtotal-text", + attrs: { "data-text-sub": String(g.소계) }, + text: `${g.비목} ${tl("Subtotal")} ${formatNumber(g.소계)}${g.끝수 ? ` (${g.끝수})` : ""}`, + }), + ]); + blocks.push( + el("p", { + className: "m01lab__total", + attrs: { "data-text-sum": String(text.계) }, + text: `${tl("Total")} ${formatNumber(text.계)}`, + }), + ); + host.replaceChildren(...blocks); } diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css index cd64ea84..e5bdfcda 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css @@ -92,3 +92,20 @@ .m01lab__calc > h3 { display: none; } + +.m01lab__text { + font-family: var(--font-mono, monospace); + font-size: var(--text-body-sm); + word-break: break-all; + padding: var(--spacing-4) 0; +} + +.m01lab__text--off { + opacity: 0.55; +} + +.m01lab__subtotal-text { + margin: var(--spacing-4) 0; + font-weight: 600; + text-align: right; +} diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts index 7cf6c637..7c1ffbf3 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts @@ -13,7 +13,7 @@ const TEXT = { ], Ho_Title: ["일위대가 호표", "Unit-cost table"], Formula_Title: ["텍스트 수식", "Text formula"], - Formula_Slot: ["(준비 중)", "(coming)"], + Formula_Slot: ["시험 계산을 누르면 식이 글로 나옴", "Run the trial calc to see the formula"], Calc_Title: ["시험 계산", "Trial calculation"], G_labor: ["인력", "Labor"], G_material: ["자재", "Material"], diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Api.ts b/M01_MasterData/M01_MasterData_UI_Logic_Api.ts index 8d4d8356..f2c06d1e 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Api.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Api.ts @@ -97,6 +97,21 @@ export type CalcAnswer = } | { ok: false; reason: string }; +export interface TextLine { + 이름: string; + 글: string; + 금액: number; + 까닭?: string; +} +/** `POST /text` 답 — 서버가 만든 읽는 식 줄(화면은 그대로 찍음) */ +export type TextAnswer = + | { + ok: true; + groups: { 비목: string; 줄: TextLine[]; 소계: number; 끝수: string }[]; + 계: number; + } + | { ok: false; reason: string }; + export interface SubBrief { name: string; book: string | null; @@ -183,6 +198,13 @@ export const runCalc = (body: { file?: string; }): Promise => request("/calc", body); +export const runText = (body: { + key: string; + inputs: Record; + row?: LogicRow; + file?: string; +}): Promise => request("/text", body); + export const saveFiles = ( files: SaveFile[], ): Promise<{ files: { file: string; version: string }[] }> => request("/save", { files }); diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts b/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts index e8043168..e01d490f 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts @@ -7,8 +7,10 @@ import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements"; import { runCalc, + runText, type CalcAnswer, type CalcLine, + type TextAnswer, type LogicRow, } from "./M01_MasterData_UI_Logic_Api"; import { formatNumber } from "./M01_MasterData_UI_Logic_Edit"; @@ -23,6 +25,8 @@ export interface CalcContext { /** 로직마다 넣은 값 — 다시 그려도 남음 */ values: Record; onLines: (lines: CalcLine[] | null) => void; + /** 읽는 식 줄(`/text`)도 같이 받고 싶을 때 — 호출이 `onLines` 보다 먼저 */ + onText?: (answer: TextAnswer) => void; } const SUMS = ["노무비", "재료비", "경비", "계"]; @@ -68,11 +72,13 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void { } const draft = ctx.dirty() || ctx.savedKey === null; try { - const answer = await runCalc({ + const body = { key: ctx.savedKey ?? ctx.row.키, inputs, ...(draft ? { row: ctx.row, file: ctx.file } : {}), - }); + }; + const answer = await runCalc(body); + if (ctx.onText) ctx.onText(await runText(body)); ctx.onLines(answer.ok ? (answer.lines ?? null) : null); out.replaceChildren(...answerView(answer, draft)); } catch (error) { diff --git a/resources/master_data/ref/_걷을_옛코드.md b/resources/master_data/ref/_걷을_옛코드.md index ac2d3007..558c9326 100644 --- a/resources/master_data/ref/_걷을_옛코드.md +++ b/resources/master_data/ref/_걷을_옛코드.md @@ -8,7 +8,7 @@ | 파일 | 읽는 것 | 부르는 곳 | 판정 | |---|---|---|---| -| B09_Estimation/B09_Estimation_ResourceAxis_Dump.py | old/ 에 씀 | 없음 | 걷음 | +| ~~B09_Estimation/B09_Estimation_ResourceAxis_Dump.py~~ | old/ 에 씀 | 없음 | 걷었음 | | old/_build_mach.py · _build_mat_price.py · _build_pum.py | old/ 자기 폴더 | 없음(폴더째) | old/ 와 함께 걷음 | | resources/master_data/scripts/update_관급단가_보강.py | ref/_관급_보강_후보.json 에 씀 | 손으로만 | 결과물 처분 뒤 걷음 |