Merge remote-tracking branch 'origin/sub_desktop_1' into main_desktop_1
This commit is contained in:
@@ -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}
|
||||
@@ -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<string, ElementBrief | null>;
|
||||
reasons: string[];
|
||||
lines: CalcLine[] | null;
|
||||
text: TextAnswer | null;
|
||||
values: Record<string, string>;
|
||||
}
|
||||
|
||||
@@ -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<void> => {
|
||||
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) {
|
||||
|
||||
@@ -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<string, ElementBrief | null>;
|
||||
/** 마지막 시험 계산의 줄 — 호표 차례 뒤에 덧줄 */
|
||||
lines: CalcLine[] | null;
|
||||
/** 마지막 시험 계산의 읽는 식 줄(서버가 줌) */
|
||||
text: TextAnswer | null;
|
||||
values: Record<string, string>;
|
||||
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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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<CalcAnswer> => request("/calc", body);
|
||||
|
||||
export const runText = (body: {
|
||||
key: string;
|
||||
inputs: Record<string, unknown>;
|
||||
row?: LogicRow;
|
||||
file?: string;
|
||||
}): Promise<TextAnswer> => request("/text", body);
|
||||
|
||||
export const saveFiles = (
|
||||
files: SaveFile[],
|
||||
): Promise<{ files: { file: string; version: string }[] }> => request("/save", { files });
|
||||
|
||||
@@ -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<string, string>;
|
||||
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) {
|
||||
|
||||
@@ -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 에 씀 | 손으로만 | 결과물 처분 뒤 걷음 |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user