feat(M02): 일위대가 로직 줄 구분 드롭다운 단가산출 · 단가승격 — 줄 승격 칸 저장(134-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""M02 일위대가 문서 `labor/<열id>.json` — 정리 · 검사 · 계산 · 알약(PLAN 39-1a · 설계_정함 1장).
|
||||
|
||||
문서 = `{종류, 판, 열, 일위대가번, 단위, 본뜬,
|
||||
담은로직: [{로직, 별칭, 메모, 입력값, 수량, 켜쌓기율?, 고름?} | {참조: {층, 주인, 키}, 별칭, 메모, 수량}],
|
||||
담은로직: [{로직, 별칭, 메모, 입력값, 수량, 켜쌓기율?, 고름?, 승격?} | {참조: {층, 주인, 키}, 별칭, 메모, 수량}],
|
||||
자재: [{코드, 이름, 규격, 단위, 수량}], 계산값}` — 이름은 구조물 열 이름(따로 안 둠).
|
||||
수량 = `null`(면적산출 알약 칸 값 · 로직 알약 「이름|단위」 · 자재 알약 「자재|MT키」) | `{손: 수}`
|
||||
(손이 칸보다 앞섬) · 같은 로직 두 번 금지 · 입력값 = 그 로직 입력 이름만(빈 값 = 자동값).
|
||||
@@ -17,6 +17,7 @@
|
||||
로직 자재(103-1) = 담은 로직 맨 위 층 호표 재료 줄(`logic_mats` · 줄마다 `재료`) ·
|
||||
고름 = `{호표 차례: MT키}`(엔진 `choosing` 그대로) · 금액은 로직 재료비 안이라 합 · 알약에 안 더함(보기만) · 사용자 자재와
|
||||
같은 품목 두 번은 [저장] 때 400.
|
||||
승격(134-1) = 로직 줄 「단가승격」 표시(`true` 만 · 없음 = 단가산출) · 금액 무관 · B09 호표 몫.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -124,6 +125,7 @@ def clean(doc: Any) -> dict[str, Any]:
|
||||
"수량": _qty(one.get("수량"), where),
|
||||
**({"켜쌓기율": rate} if rate is not None else {}),
|
||||
**({"고름": picks} if picks else {}),
|
||||
**({"승격": True} if one.get("승격") is True else {}),
|
||||
}
|
||||
)
|
||||
mats, codes = [], set()
|
||||
|
||||
@@ -204,6 +204,16 @@
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* 구분 드롭다운(134-1) — 칸 너비만 · 품셈 책 글은 아래 */
|
||||
.m02-labrow__kind .ui-field {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.m02-labrow__kind .ui-select {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 줄 접고 폄 표(51-2) · 고른 줄 하위 행 — 이름 들여쓰기 · 흐린 바탕 · 위 줄보다 촘촘(51-7) */
|
||||
.m02-labrow .m02-labrow__sub td {
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
|
||||
@@ -133,6 +133,8 @@ export interface LaborRow {
|
||||
켜쌓기율?: number | null;
|
||||
/** 고른 로직 자재(103-1) — {호표 차례: MT키} · 없으면 엔진 기본 */
|
||||
고름?: Record<string, string>;
|
||||
/** 단가승격(134-1 · 로직 줄 구분 드롭다운) — 없음 = 단가산출 · 금액 무관 · B09 호표 몫 */
|
||||
승격?: boolean;
|
||||
}
|
||||
export interface LaborMat {
|
||||
코드: string;
|
||||
@@ -170,6 +172,7 @@ export function normalizeLabor(doc: unknown, column: string): LaborDoc {
|
||||
...(r.참조 ? { 참조: r.참조 } : {}),
|
||||
...(r.켜쌓기율 != null ? { 켜쌓기율: r.켜쌓기율 } : {}),
|
||||
...(r.고름 && Object.keys(r.고름).length ? { 고름: { ...r.고름 } } : {}),
|
||||
...(r.승격 === true && !r.참조 ? { 승격: true } : {}),
|
||||
})),
|
||||
자재: (d.자재 ?? []).map((m) => ({
|
||||
코드: m.코드,
|
||||
|
||||
@@ -204,6 +204,8 @@ export function mountLaborDetail(
|
||||
move: reorder,
|
||||
remove: removeRow,
|
||||
alias: setAlias,
|
||||
// 134-1 — 구조물 일위대가 로직 줄만(사전조합 안 로직 줄엔 없음)
|
||||
...(lib ? {} : { promote: setPromote }),
|
||||
}
|
||||
: { open: pick, option: openRow, ref: openRef },
|
||||
),
|
||||
@@ -361,6 +363,12 @@ export function mountLaborDetail(
|
||||
cur.담은로직[i].별칭 = value;
|
||||
opt.onChanged(dirty());
|
||||
}
|
||||
/** 구분 드롭다운(134-1) — 화면 캐시만 · 금액 무관이라 다시 셈 없음 */
|
||||
function setPromote(i: number, on: boolean): void {
|
||||
if (on) cur.담은로직[i].승격 = true;
|
||||
else delete cur.담은로직[i].승격;
|
||||
opt.onChanged(dirty());
|
||||
}
|
||||
function removeMat(i: number): void {
|
||||
cur.자재.splice(i, 1);
|
||||
changed();
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* 상세는 줄 누름 = 고름(`selected` 줄 하이라이트 · 그 줄 바로 아래 하위 행 · 51-2 · 화살표 없음 51-7) · 넣을 값 모달은 [옵션](47-5).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { createButton, createSelectField, el } from "@ui/ui_template_elements";
|
||||
import { formatMoney } from "../M01_MasterData/M01_MasterData_UI_Logic_Money";
|
||||
import type { ItemLogic } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import type {
|
||||
@@ -41,14 +41,36 @@ const HEAD: Record<(typeof COSTS)[number], Parameters<typeof tc>[0]> = {
|
||||
/** 옵션 값 줄이 걸치는 칸 수 — 구분 · 이름 · 별칭(113-1 · 나머지 칸은 위 줄에서 rowspan 2) */
|
||||
const SPAN = 3;
|
||||
|
||||
/** 구분(50-4 · 저장 안 함) — 줄 모양(참조 키 머리)에서 읽음 · SP- = 단가승격 · SC- = 일위조합(50-3) */
|
||||
export const kindOf = (row: Pick<LaborRow, "로직" | "참조">): string => {
|
||||
/** 구분(50-4) — 로직 줄 = `승격`(134-1 · 단가승격 · 없음 = 단가산출) · 참조 줄 = 키 머리(SC- = 일위조합 ·
|
||||
* 옛 SP- = 단가승격 글 고정 · 134-2) */
|
||||
export const kindOf = (row: Pick<LaborRow, "로직" | "참조" | "승격">): string => {
|
||||
const key = row.참조?.키 ?? row.로직;
|
||||
return tc(
|
||||
key.startsWith("SP-") ? "Kind_Promote" : key.startsWith("SC-") ? "Kind_Combo" : "Kind_Logic",
|
||||
row.승격 || key.startsWith("SP-")
|
||||
? "Kind_Promote"
|
||||
: key.startsWith("SC-")
|
||||
? "Kind_Combo"
|
||||
: "Kind_Logic",
|
||||
);
|
||||
};
|
||||
|
||||
/** 구분 드롭다운(134-1) — 로직 줄만 [단가산출 | 단가승격] · 줄 누름과 따로 */
|
||||
function kindPick(on: boolean, set: (on: boolean) => void): HTMLElement {
|
||||
const pick = createSelectField({
|
||||
options: [
|
||||
{ value: "", text: tc("Kind_Logic") },
|
||||
{ value: "1", text: tc("Kind_Promote") },
|
||||
],
|
||||
value: on ? "1" : "",
|
||||
compact: true,
|
||||
ariaLabel: tc("Sub"),
|
||||
onChange: (v) => set(v === "1"),
|
||||
});
|
||||
pick.select.dataset.laborKind = "";
|
||||
pick.root.addEventListener("click", (e) => e.stopPropagation()); // 줄 누름(고름)과 따로
|
||||
return pick.root;
|
||||
}
|
||||
|
||||
/** 품셈 책(102-3) — 출처 첫 낱말 */
|
||||
const BOOK = /^(건설|산림)품셈/;
|
||||
|
||||
@@ -189,6 +211,8 @@ export interface RowHandlers {
|
||||
remove?: (i: number) => void;
|
||||
/** 별칭 칸 고침(44-6 · 글자마다 · 빈 글 = null) — 없으면 글만 보임 */
|
||||
alias?: (i: number, value: string | null) => void;
|
||||
/** 구분 드롭다운(134-1 · 로직 줄 단가산출 ↔ 단가승격) — 없으면 글만 */
|
||||
promote?: (i: number, on: boolean) => void;
|
||||
}
|
||||
|
||||
/** 별칭 칸(44-6) — 고칠 수 있으면 칸 안 글상자(누르면 바로 고침 · 줄 누름과 따로) · 아니면 글 · 비면 빈 칸 */
|
||||
@@ -361,7 +385,9 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
|
||||
el("td", {
|
||||
className: "m02-labrow__kind",
|
||||
children: [
|
||||
kindOf(row),
|
||||
on?.promote && !row.참조
|
||||
? kindPick(!!row.승격, (v) => on.promote?.(i, v))
|
||||
: kindOf(row),
|
||||
// 102-3 — 단가산출 줄만 품셈 책(출처 첫 낱말)
|
||||
...(book ? [el("small", { className: "m02-labrow__book", text: book })] : []),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/* M02 일위대가 문서 화면 정리(`M02_MasterTemplete_Labor_Api.ts` `normalizeLabor`) 헬퍼(134-1) —
|
||||
* 그 자리에서 트랜스파일해 Node 로 돌림 · 가져오는 길은 빈 대역 · stdin = {doc, column} · stdout = 정리한 문서 JSON. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const file = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"M02_MasterTemplete",
|
||||
"M02_MasterTemplete_Labor_Api.ts",
|
||||
);
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const stub = new Proxy({}, { get: () => function () {} });
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, () => stub);
|
||||
|
||||
const input = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
process.stdout.write(JSON.stringify(box.exports.normalizeLabor(input.doc, input.column)));
|
||||
@@ -0,0 +1,90 @@
|
||||
"""일위대가 로직 줄 단가승격 표시(PLAN 134-1) — 줄에 `승격: true` · 금액 무관.
|
||||
|
||||
- 서버 `clean` = `true` 만 남김(거짓 · 글 · 참조 줄은 버림) · 저장 왕복에 남음 · 금액 그대로
|
||||
- 사전조합 안 로직 줄엔 승격 없음(정리 때 버림)
|
||||
- 화면 `normalizeLabor` 도 `true` 만 통과(Node 헬퍼)
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from resources.tester.test_m02_structure import write_sample_table
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
NET = "GC000337"
|
||||
BARE = "GC000994"
|
||||
|
||||
|
||||
def _doc(lines):
|
||||
return {"열": "rv", "담은로직": lines, "자재": []}
|
||||
|
||||
|
||||
def test_clean_은_참만_남김():
|
||||
got = labor.clean(_doc([{"로직": NET, "승격": True}, {"로직": BARE, "승격": "yes"}]))[
|
||||
"담은로직"
|
||||
]
|
||||
assert got[0]["승격"] is True and "승격" not in got[1]
|
||||
|
||||
|
||||
def test_승격은_금액을_안_바꿈():
|
||||
plain = labor.calc(labor.clean(_doc([{"로직": NET, "수량": {"손": 2}}])), {})
|
||||
flag = labor.calc(labor.clean(_doc([{"로직": NET, "수량": {"손": 2}, "승격": True}])), {})
|
||||
assert flag["반영합"] == plain["반영합"] and flag["기준합"] == plain["기준합"]
|
||||
|
||||
|
||||
def test_사전조합_안_로직_줄엔_승격_없음(monkeypatch):
|
||||
monkeypatch.setattr(refs, "here", lambda: ("system", None))
|
||||
lines = refs._combo_lines({"담은로직": [{"로직": NET, "승격": True}]}, "SC-000009")
|
||||
assert "승격" not in lines[0]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def system(tmp_path, monkeypatch):
|
||||
write_sample_table(tmp_path)
|
||||
monkeypatch.setattr(store, "FOLDER", tmp_path)
|
||||
store.create_structure("rv")
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_저장_왕복에_승격_남음(system):
|
||||
have = store.read("labor", "rv")
|
||||
sent = {
|
||||
**have["문서"],
|
||||
"담은로직": [
|
||||
{"로직": NET, "수량": None, "승격": True},
|
||||
{"로직": BARE, "수량": None},
|
||||
],
|
||||
}
|
||||
store.write("labor", "rv", have["판"], sent)
|
||||
rows = store.read("labor", "rv")["문서"]["담은로직"]
|
||||
assert rows[0]["승격"] is True and "승격" not in rows[1]
|
||||
# 내리면(드롭다운 단가산출) 칸째 사라짐
|
||||
have = store.read("labor", "rv")
|
||||
store.write("labor", "rv", have["판"], {**have["문서"], "담은로직": [{"로직": NET}]})
|
||||
assert "승격" not in store.read("labor", "rv")["문서"]["담은로직"][0]
|
||||
|
||||
|
||||
def test_화면_정리도_참만_통과():
|
||||
doc = {
|
||||
"담은로직": [
|
||||
{"로직": NET, "승격": True},
|
||||
{"로직": BARE, "승격": False},
|
||||
{"로직": "", "참조": {"층": "system", "주인": None, "키": "SC-000001"}, "승격": True},
|
||||
]
|
||||
}
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_m02_labor_normalize.cjs")],
|
||||
input=json.dumps({"doc": doc, "column": "rv"}, ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=True,
|
||||
)
|
||||
rows = json.loads(proc.stdout)["담은로직"]
|
||||
assert [r.get("승격") for r in rows] == [True, None, None]
|
||||
Reference in New Issue
Block a user