Merge remote-tracking branch 'origin/sub_laptop_8' into sub_laptop_2
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
* ① 폴더 이름 · 목록 {항목} · 가져오기 POST · 읽기 {종류, 판, 내용} · 고침 PUT {판, 내용}(판 · 409 · 403)
|
||||
* ② 새로 가져오기 · 계산 한 줄 · 입력 목록(한 번만 받음)
|
||||
* ③ 카드 셈: 범위 · 고르기 원래 모양 · 자리 「선택」 만 묶음
|
||||
* ④ 일위대가(39-1c): 복사본 요약 · 원본 층 계산 요약 · 넣을 값 = 담은 로직 입력 전부
|
||||
* ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_curve_radius_floor.cjs` 머리와 같다.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
@@ -66,6 +67,21 @@ globalThis.fetch = async (url, init = {}) => {
|
||||
const method = init.method ?? "GET";
|
||||
const body = init.body ? JSON.parse(init.body) : undefined;
|
||||
state.calls.push({ method, url, body });
|
||||
if (url.startsWith("/api/m02/logic-lib/inputs?keys=")) {
|
||||
state.logicKeys = decodeURIComponent(url.split("=")[1]).split(",");
|
||||
return reply(200, { 로직: INPUTS.로직, 빠진로직: [] });
|
||||
}
|
||||
if (/^\/api\/m02\/layers\/system\/labor\/rv\/calc$/.test(url)) {
|
||||
return reply(200, {
|
||||
줄: [
|
||||
{ 로직: "GA1", 까닭: null },
|
||||
{ 로직: "GA2", 까닭: "입력 없음" },
|
||||
],
|
||||
자재: [{ 코드: "MT000008" }],
|
||||
기준합: { 계: 500 },
|
||||
반영합: { 계: 1500 },
|
||||
});
|
||||
}
|
||||
if (/^\/api\/m02\/layers\/\w+\/items\/[^/]+\/inputs$/.test(url)) {
|
||||
state.inputsHits += 1;
|
||||
return reply(200, INPUTS);
|
||||
@@ -97,7 +113,13 @@ globalThis.fetch = async (url, init = {}) => {
|
||||
const folder = body.structure.replace("#", "n");
|
||||
state.folders[folder] = {
|
||||
info: { 폴더: folder, 원본: body, 마스터바뀜: false, 가져온날: "2026-09-30T01:00:00" },
|
||||
parts: { 면적: { 시트: [{ id: "s", 칸: { A1: { 값: 1 } } }] }, 입력값: null },
|
||||
parts: {
|
||||
면적: { 시트: [{ id: "s", 칸: { A1: { 값: 1 } } }] },
|
||||
입력값: null,
|
||||
일위대가: {
|
||||
줄: { 일위대가번: "SU-000007", 담은로직: [{ 로직: "GA1" }, { 로직: "GA2" }], 자재: [{}] },
|
||||
},
|
||||
},
|
||||
};
|
||||
return reply(200, { 폴더: folder, 만듦: true, 가져온날: "2026-09-30T01:00:00" });
|
||||
}
|
||||
@@ -132,6 +154,8 @@ globalThis.fetch = async (url, init = {}) => {
|
||||
{ 로직: "GA2", 입력: "지역" },
|
||||
],
|
||||
멈춤: ["GA1"],
|
||||
기준합: { 계: 100 },
|
||||
반영합: { 계: 300 },
|
||||
});
|
||||
if (method === "GET") {
|
||||
const content = item.parts[part] ?? null;
|
||||
@@ -226,6 +250,18 @@ async function main() {
|
||||
out.calc = await items.calc(info.폴더);
|
||||
out.calcMissing = await items.calc("없는폴더");
|
||||
|
||||
// 일위대가 — 복사본 요약(복사본 계산) · 원본 층 요약(층 calc) · 넣을 값 = 담은 로직 입력 전부
|
||||
out.laborCopy = await project.laborOf(row(), items, info.폴더);
|
||||
out.laborOrigin = await project.laborOf(row(), null, null);
|
||||
out.laborNone = await project.laborOf(row({ 열: "없는열" }), null, null);
|
||||
const valueData = await project.laborValueData(["GA1", "GA2"], { GA1: {} });
|
||||
out.laborValues = {
|
||||
keys: state.logicKeys,
|
||||
link: valueData.link.줄.map((l) => `${l.로직}|${l.입력}|${l.자리}`),
|
||||
shown: model.pickedGroups(valueData.list, valueData.link).length,
|
||||
values: valueData.values,
|
||||
};
|
||||
|
||||
// 입력 목록 — 같은 항목은 한 번만 받음
|
||||
const first = await project.loadInputs(row());
|
||||
await project.loadInputs(row());
|
||||
|
||||
@@ -82,7 +82,7 @@ def test_재적용은_덮임을_돌려주고_계산_한_줄(out):
|
||||
assert out["reapplyCall"][0] == "POST /api/projects/P1/items/pipe%4085.05/reapply"
|
||||
assert out["reapply"]["covered"] == ["면적"] # 덮이는 것을 알림으로
|
||||
assert out["reapply"]["info"]["마스터바뀜"] is False
|
||||
assert out["calc"] == {"total": 12345.6, "gaps": 2, "stuck": 1}
|
||||
assert out["calc"] == {"total": 12345.6, "gaps": 2, "stuck": 1, "base": 100, "reflected": 300}
|
||||
assert out["calcMissing"] is None # 없는 복사본 = 계산 줄 없음
|
||||
|
||||
|
||||
@@ -146,8 +146,42 @@ def test_선택_입력_카드는_sub2_파일_한_벌(out):
|
||||
assert "export function createInputsValueCard(" in values # 모달 값 받기(view · value)
|
||||
assert "createInputsValueCard," in inputs and '} from "./M02_Labor_Values";' in inputs
|
||||
for name in ("ui_template_item_picker_detail.ts", "ui_template_item_picker_edit.ts"):
|
||||
text = (UI / name).read_text(encoding="utf-8")
|
||||
assert "M02_MasterTemplete_Inputs" in text and 'InputsCard"' not in text, name
|
||||
text = (UI / name).read_text(encoding="utf-8") # 모달은 값 카드를 바로 받음(39-1c)
|
||||
assert "M02_Labor_Values" in text and 'InputsCard"' not in text, name
|
||||
|
||||
|
||||
def test_일위대가_요약_복사본_원본_넣을_값_전부(out):
|
||||
"""39-1c — 복사본이면 복사본 일위대가 · 계산 · 아니면 원본 층 calc ·
|
||||
넣을 값 = 담은 로직 입력 전부."""
|
||||
assert out["laborCopy"] == {
|
||||
"번호": "SU-000007",
|
||||
"로직": ["GA1", "GA2"],
|
||||
"자재": 1,
|
||||
"base": 100,
|
||||
"reflected": 300,
|
||||
"gaps": 2,
|
||||
"stuck": 1,
|
||||
}
|
||||
assert out["laborOrigin"] == {
|
||||
"번호": None,
|
||||
"로직": ["GA1", "GA2"],
|
||||
"자재": 1,
|
||||
"base": 500,
|
||||
"reflected": 1500,
|
||||
"gaps": 0,
|
||||
"stuck": 1,
|
||||
}
|
||||
assert out["laborNone"] is None # 일위대가 없는 열(층 calc 404)
|
||||
values = out["laborValues"]
|
||||
assert values["keys"] == ["GA1", "GA2"]
|
||||
assert values["link"] == ["GA1|지역|선택", "GA1|소량감률|선택", "GA2|지역|선택"]
|
||||
assert values["shown"] == 2 and values["values"] == {"GA1": {}}
|
||||
|
||||
|
||||
def test_모달_카드_차례_도면_일위대가_면적():
|
||||
detail = (UI / "ui_template_item_picker_detail.ts").read_text(encoding="utf-8")
|
||||
assert 'import("./ui_template_item_picker_labor")' in detail
|
||||
assert "cards.root.insertBefore(labor.root, cards.root.children[1]" in detail
|
||||
|
||||
|
||||
def test_모달은_원본_층에_쓰는_길을_안_부름():
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
"""프로젝트 복사본 · 일위대가 문서(PLAN 39-1c · 임시 저장소 · 임시 마스터 사본).
|
||||
|
||||
- 층 `labor/<열>.json` 이 있으면 스냅샷 `일위대가.json` = {줄: 문서, 로직, 빠진로직, 알약} ·
|
||||
단가 묶음에 자재 MT
|
||||
- `입력값.json` 첫 값 = 문서 입력값(확인 거짓) · 재적용은 프로젝트에서 넣은 값이 이김
|
||||
- 계산 = 수량(손 · 면적 알약 칸) · 반영 = 기준 × 수량 ÷ 배 · 자재 = 기준 단가 × 수량 ·
|
||||
반영합 = 손셈 ·
|
||||
기준 · 반영이 원본 층 문서 계산(`M02_Labor_Doc.calc`)과 같음
|
||||
- 일위대가 쓰기는 400(읽기만)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from config import config_system
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Item_Copy as copies
|
||||
from M02_MasterTemplete import M02_Item_Snapshot as snap
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
from resources.tester.helper_m02_master_sample import SAMPLE
|
||||
|
||||
MASTER = config_system.PROJECT_ROOT / "resources/master_data"
|
||||
PIPE = {"layer": "system", "owner": None, "key": "SS000011", "name": "관공 관매설"}
|
||||
COLUMN = "pp_len"
|
||||
FOLDER = "pipe@40.0"
|
||||
LOGICS = ("GF000176", "GC000088", "GF000113")
|
||||
MATERIAL = "MT000008"
|
||||
HAND = 3 # GC000088 손 수량
|
||||
AREA_QTY = 7.5 # 자재 · GF000113 은 면적 알약 칸 값
|
||||
MAT_QTY = 4
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def master_copy(tmp_path_factory) -> Path:
|
||||
target = tmp_path_factory.mktemp("master_data")
|
||||
for path in MASTER.glob("*.json"):
|
||||
shutil.copy2(path, target / path.name)
|
||||
return target
|
||||
|
||||
|
||||
def _read(path: Path) -> Any:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _write(path: Path, content: Any) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(content, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
|
||||
def _first_input(code: str) -> tuple[str, Any]:
|
||||
whole = m01.loaded()[1]
|
||||
name, value = next(iter(m01.mau.values(whole, whole.logic(code)).items()))
|
||||
return name, int(value) if isinstance(value, Decimal) and value % 1 == 0 else value
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def world(tmp_path: Path, monkeypatch, master_copy: Path) -> dict[str, Any]:
|
||||
system = tmp_path / "master"
|
||||
system.mkdir()
|
||||
for name in (layers.COMBO_FILE, "자재조합.json"):
|
||||
shutil.copy2(SAMPLE / name, system / name)
|
||||
for kind in ("table", "structure", "area", "material"):
|
||||
shutil.copytree(SAMPLE / kind, system / kind)
|
||||
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
|
||||
monkeypatch.setattr(m01, "FOLDER", master_copy)
|
||||
storage = tmp_path / "storage"
|
||||
monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage))
|
||||
root = storage / "3" / "12" / "proj"
|
||||
(root / "B05_Profile/route").mkdir(parents=True)
|
||||
(root / "B04_PreProcess/drainage/edits").mkdir(parents=True)
|
||||
|
||||
name, value = _first_input("GC000088")
|
||||
doc = labor_doc.clean(
|
||||
{
|
||||
"열": COLUMN,
|
||||
"단위": "m",
|
||||
"담은로직": [
|
||||
{"로직": LOGICS[0]},
|
||||
{"로직": LOGICS[1], "입력값": {name: value}, "수량": {"손": HAND}},
|
||||
{"로직": LOGICS[2]},
|
||||
],
|
||||
"자재": [{"코드": MATERIAL}],
|
||||
}
|
||||
)
|
||||
_write(system / "labor" / f"{COLUMN}.json", doc)
|
||||
pills = {p["키"]: p for p in labor_doc.pills(doc)}
|
||||
area_pill = next(k for k, p in pills.items() if LOGICS[2] in p.get("로직", []))
|
||||
area = _read(system / "area" / f"{COLUMN}.json")
|
||||
sheet = area["시트"][0]
|
||||
sheet["칸"] = {"A1": {"값": AREA_QTY}, "A2": {"값": MAT_QTY}}
|
||||
sheet["연결"] = [
|
||||
{"키": area_pill, "칸": "A1"},
|
||||
{"키": labor_doc.material_key(MATERIAL), "칸": "A2"},
|
||||
]
|
||||
_write(system / "area" / f"{COLUMN}.json", area)
|
||||
return {
|
||||
"root": root,
|
||||
"doc": doc,
|
||||
"input": (name, value),
|
||||
"pills": pills,
|
||||
"area_pill": area_pill,
|
||||
}
|
||||
|
||||
|
||||
def _save(root: Path) -> dict[str, list[str]]:
|
||||
_write(root / "B05_Profile/route/structures.json", {"revision": 1, "structures": []})
|
||||
pipe = {"chainage_m": 40.0, "source": "user", "summary_item": PIPE}
|
||||
_write(root / "B04_PreProcess/drainage/edits/pipe_points.json", {"points": [pipe]})
|
||||
return snap.sync_snapshots(root)
|
||||
|
||||
|
||||
def _fill_auto(root: Path) -> None:
|
||||
"""빠진 입력을 자동값으로(확인 참) — 문서 입력값은 그대로 둠."""
|
||||
current = copies.read_file(root, FOLDER, "입력값")
|
||||
values = current["내용"]
|
||||
whole = m01.loaded()[1]
|
||||
for code in LOGICS:
|
||||
for key, auto in m01.mau.values(whole, whole.logic(code)).items():
|
||||
mine = values.setdefault(code, {})
|
||||
if key not in mine and auto is not None:
|
||||
mine[key] = {"값": labor_doc._json(auto), "확인": True}
|
||||
copies.write_file(root, FOLDER, "입력값", current["판"], values)
|
||||
|
||||
|
||||
def test_스냅샷_일위대가는_문서_알약_자재_단가(world):
|
||||
root = world["root"]
|
||||
assert _save(root)["만듦"] == [FOLDER]
|
||||
folder = root / "templates/structures" / FOLDER
|
||||
labor = _read(folder / "일위대가.json")
|
||||
assert labor["줄"] == world["doc"]
|
||||
assert [row["키"] for row in labor["로직"]] == list(LOGICS) and labor["빠진로직"] == []
|
||||
assert {p["키"] for p in labor["알약"]} == set(world["pills"])
|
||||
keys = {
|
||||
row["키"] for data in _read(folder / "단가.json").values() for row in data.get("줄", [])
|
||||
}
|
||||
assert MATERIAL in keys and set(LOGICS) <= keys
|
||||
|
||||
|
||||
def test_입력값_첫_값은_문서_입력값(world):
|
||||
root = world["root"]
|
||||
_save(root)
|
||||
name, value = world["input"]
|
||||
values = _read(root / "templates/structures" / FOLDER / "입력값.json")
|
||||
assert values == {LOGICS[1]: {name: {"값": value, "확인": False}}}
|
||||
|
||||
|
||||
def test_재적용은_프로젝트_값이_이김(world):
|
||||
root = world["root"]
|
||||
_save(root)
|
||||
name, _ = world["input"]
|
||||
current = copies.read_file(root, FOLDER, "입력값")
|
||||
mine = {LOGICS[1]: {name: {"값": 99, "확인": True}}}
|
||||
copies.write_file(root, FOLDER, "입력값", current["판"], mine)
|
||||
got = copies.reapply(root, FOLDER)
|
||||
assert got["값있음"] is True
|
||||
assert _read(root / "templates/structures" / FOLDER / "입력값.json") == mine
|
||||
|
||||
|
||||
def test_계산_반영_합은_손셈_원본과_같음(world):
|
||||
root = world["root"]
|
||||
_save(root)
|
||||
_fill_auto(root)
|
||||
got = copies.calc(root, FOLDER)
|
||||
assert got["멈춤"] == [], got["줄"]
|
||||
lines = {line["로직"]: line for line in got["줄"]}
|
||||
hand, area = lines[LOGICS[1]], lines[LOGICS[2]]
|
||||
assert (hand["수량"], hand["수량출처"]) == (HAND, "손")
|
||||
assert (area["수량"], area["수량출처"]) == (AREA_QTY, "칸")
|
||||
assert lines[LOGICS[0]]["수량출처"] == "없음" and lines[LOGICS[0]]["반영"] is None
|
||||
for line in (hand, area):
|
||||
expect = Decimal(str(line["계"])) * Decimal(str(line["수량"])) / Decimal(str(line["배"]))
|
||||
assert Decimal(str(line["반영"]["계"])) == pytest.approx(expect)
|
||||
(mat,) = got["자재"]
|
||||
assert mat["코드"] == MATERIAL and mat["수량"] == MAT_QTY and mat["기준단가"]
|
||||
assert mat["반영"] == pytest.approx(mat["기준단가"] * MAT_QTY)
|
||||
total = hand["반영"]["계"] + area["반영"]["계"] + mat["반영"]
|
||||
assert got["반영합"]["계"] == pytest.approx(total)
|
||||
# 원본 층 문서 계산(마스터 같은 사본)과 같음 — 입력값은 프로젝트에 채운 것
|
||||
values = _read(root / "templates/structures" / FOLDER / "입력값.json")
|
||||
doc = {
|
||||
**world["doc"],
|
||||
"담은로직": [
|
||||
{**one, "입력값": {k: v["값"] for k, v in values.get(one["로직"], {}).items()}}
|
||||
for one in world["doc"]["담은로직"]
|
||||
],
|
||||
}
|
||||
given = {world["area_pill"]: AREA_QTY, labor_doc.material_key(MATERIAL): MAT_QTY}
|
||||
origin = labor_doc.calc(doc, given)
|
||||
assert got["반영합"] == pytest.approx(origin["반영합"])
|
||||
assert got["기준합"] == pytest.approx(origin["기준합"])
|
||||
|
||||
|
||||
def test_일위대가_쓰기는_막힘(world):
|
||||
root = world["root"]
|
||||
_save(root)
|
||||
current = copies.read_file(root, FOLDER, "일위대가")
|
||||
assert current["내용"]["줄"]["열"] == COLUMN
|
||||
with pytest.raises(ValueError, match="원본"):
|
||||
copies.write_file(root, FOLDER, "일위대가", current["판"], current["내용"])
|
||||
Reference in New Issue
Block a user