feat(M02): 구조물 조합 고르기 모달 = M02 일위대가 카드 · 사본 셈 = 층 셈 한 벌(142-1)
- 사본 셈 = M02_Labor_Doc.calc 한 벌(사본 단가 · 굳힌 참조사본 · 사본 알약 주입 · 빈 입력 = 자동값) - 모달 일위대가 카드 = ItemCards labor(자료 자리 ItemSource.labor · 쓰는 길 없으면 읽기 전용) - 사본 문서에 사본 입력값 · 고른자재를 줄 입력값 · 고름으로 얹음 · [수정] = 넣을 값 편집 화면 - 옛 요약 카드(picker_labor) · 넣을 값 카드 · 머리 옛 일위대가 이름 줄 지움 · 처음 펼침 = 개인 - 시험: 사본 셈 = 층 셈 · 모달 일위대가 카드 읽기 전용 · 새 셈 꼴로 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
This commit is contained in:
@@ -143,15 +143,18 @@ globalThis.fetch = async (url, init = {}) => {
|
||||
}
|
||||
if (part === "calc")
|
||||
return reply(200, {
|
||||
줄: [],
|
||||
계: 12345.6,
|
||||
줄: [
|
||||
{ 로직: "GA1", 까닭: "없는 요소" },
|
||||
{ 로직: "GA2", 까닭: null },
|
||||
],
|
||||
자재: [],
|
||||
빠진입력: [
|
||||
{ 로직: "GA1", 입력: "지역" },
|
||||
{ 로직: "GA2", 입력: "지역" },
|
||||
],
|
||||
멈춤: ["GA1"],
|
||||
기준합: { 계: 100 },
|
||||
반영합: { 계: 300 },
|
||||
알약: [],
|
||||
});
|
||||
if (method === "GET") {
|
||||
const content = item.parts[part] ?? null;
|
||||
@@ -246,10 +249,25 @@ 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);
|
||||
// 일위대가(142-1) — 모달 카드 셈 = 복사본 셈 답 통째 · 문서 = 복사본 입력값 · 고른자재를 얹음
|
||||
const raw = await items.calcLabor(info.폴더);
|
||||
out.laborCalc = { 줄: raw.줄.length, 반영합: raw.반영합, 빠진입력: raw.빠진입력.length };
|
||||
out.laborCalcMissing = await items.calcLabor("없는폴더").then(
|
||||
() => "성공",
|
||||
(error) => error.message,
|
||||
);
|
||||
out.withValues = project.withCopyValues(
|
||||
{
|
||||
일위대가번: "SU-000007",
|
||||
담은로직: [
|
||||
{ 로직: "GA1", 입력값: { 지역: 1 } },
|
||||
{ 로직: "GA2", 입력값: { 지역: 2 }, 고름: { 0: "MT1" } },
|
||||
{ 참조: { 층: "system", 주인: null, 키: "SC-000001" } },
|
||||
],
|
||||
},
|
||||
{ GA1: { 지역: { 값: 15, 확인: true }, 소량감률: { 값: "", 확인: false } } },
|
||||
{ GA1: { 0: "MT9" } },
|
||||
);
|
||||
const valueData = await project.laborValueData(["GA1", "GA2"], { GA1: {} });
|
||||
out.laborValues = {
|
||||
keys: state.logicKeys,
|
||||
|
||||
@@ -84,6 +84,11 @@ def _read(path: Path) -> Any:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _stuck(got: dict) -> list:
|
||||
"""못 돈 줄(까닭) — 층 셈 꼴(142-1)."""
|
||||
return [line for line in got["줄"] if line.get("까닭")]
|
||||
|
||||
|
||||
def _auto_values(root: Path) -> dict[str, Any]:
|
||||
"""담은 로직마다 자동값을 확인한 값으로 — 사용자가 다 채운 것과 같게."""
|
||||
labor = _read(root / "templates/structures" / FOLDER / "일위대가.json")
|
||||
@@ -168,12 +173,13 @@ def _run(master: Any, key: str, values: dict) -> str:
|
||||
# ── 계산 · 마스터 바뀜 · 새로 가져오기 ───────────────────
|
||||
|
||||
|
||||
def test_입력이_없으면_빠진_입력_목록(world):
|
||||
def test_입력이_없으면_빠진_입력_목록_셈은_자동값(world):
|
||||
"""142-1 — 빈 입력은 M02 층 셈과 같이 자동값으로 돎 · 빠진 입력은 「빈 값 n」 표시용 목록."""
|
||||
root = world["root"]
|
||||
_save(root, [_pipe()])
|
||||
got = copies.calc(root, FOLDER)
|
||||
assert got["계"] == 0 and len(got["멈춤"]) == 3
|
||||
assert {"로직": "GF000176", "입력": "관경"} in got["빠진입력"]
|
||||
assert _stuck(got) == [] and got["기준합"]["계"] > 0
|
||||
|
||||
|
||||
def test_마스터_단가를_바꿔도_계산_그대로_새로_가져오면_바뀜(world):
|
||||
@@ -181,7 +187,7 @@ def test_마스터_단가를_바꿔도_계산_그대로_새로_가져오면_바
|
||||
_save(root, [_pipe()])
|
||||
values = _fill_values(root)
|
||||
before = copies.calc(root, FOLDER)
|
||||
assert before["빠진입력"] == [] and before["멈춤"] == [] and before["계"] > 0
|
||||
assert before["빠진입력"] == [] and _stuck(before) == [] and before["기준합"]["계"] > 0
|
||||
files, whole = m01.loaded()
|
||||
ua = next(
|
||||
r
|
||||
@@ -192,7 +198,8 @@ def test_마스터_단가를_바꿔도_계산_그대로_새로_가져오면_바
|
||||
)
|
||||
given = {k: {n: v["값"] for n, v in one.items()} for k, one in values.items()}
|
||||
expected = prices.to_json(prices.mcb.run(whole, ua, given))
|
||||
assert before["계"] == pytest.approx(expected["계"]) and before["노무비"] > 0
|
||||
assert before["기준합"]["계"] == pytest.approx(expected["계"])
|
||||
assert before["기준합"]["노무비"] > 0
|
||||
labor_file = master_dir / "인력.json"
|
||||
original = labor_file.read_bytes()
|
||||
try:
|
||||
@@ -204,7 +211,8 @@ def test_마스터_단가를_바꿔도_계산_그대로_새로_가져오면_바
|
||||
assert copies.list_items(root)[0]["마스터바뀜"] is True
|
||||
copies.refresh(root, FOLDER)
|
||||
after = copies.calc(root, FOLDER)
|
||||
assert after["노무비"] > before["노무비"] and after["재료비"] == before["재료비"]
|
||||
assert after["기준합"]["노무비"] > before["기준합"]["노무비"]
|
||||
assert after["기준합"]["재료비"] == before["기준합"]["재료비"]
|
||||
assert copies.read_file(root, FOLDER, "입력값")["내용"] == values # 같은 키 살림
|
||||
assert copies.list_items(root)[0]["마스터바뀜"] is False
|
||||
finally:
|
||||
@@ -299,7 +307,7 @@ def test_옛_복사본은_읽을_때_채움(world):
|
||||
assert copies.read_file(root, FOLDER, "입력값")["내용"] == {}
|
||||
filled = _read(folder / "manifest.json")
|
||||
assert filled["가져온날"] == filled["적용일"] and filled["옮겨받음"]
|
||||
assert (folder / "단가.json").is_file() and copies.calc(root, FOLDER)["멈춤"]
|
||||
assert (folder / "단가.json").is_file() and copies.calc(root, FOLDER)["빠진입력"]
|
||||
assert copies.list_items(root)[0]["마스터바뀜"] is False # 원본이 안 바뀐 옛 복사본
|
||||
|
||||
|
||||
@@ -328,7 +336,7 @@ def test_길_권한_남의_프로젝트는_403(world, monkeypatch):
|
||||
stale = client.put(f"{base}/{FOLDER}/입력값", json={"판": got["판"] + "x", "내용": {}})
|
||||
assert stale.status_code == 409 and stale.json()["detail"]["판"] == put.json()["판"]
|
||||
assert client.get(f"{base}/{FOLDER}/모름").status_code == 422
|
||||
assert client.post(f"{base}/{FOLDER}/calc").json()["멈춤"]
|
||||
assert client.post(f"{base}/{FOLDER}/calc").json()["빠진입력"]
|
||||
wall = {"structure": "st9", "layer": "system", "owner": None}
|
||||
assert client.post(base, json={**wall, "summary_item": {"layer": "x"}}).status_code == 400
|
||||
assert client.post(base, json={"layer": "system", "key": "SS000022"}).status_code == 400
|
||||
@@ -388,8 +396,9 @@ def test_같은_항목_구조물_둘은_따로(world):
|
||||
assert copies.read_file(root, "a1", "면적")["내용"]["메모"] == "a1 만 고침"
|
||||
assert "메모" not in copies.read_file(root, "a2", "면적")["내용"] # 다른 쪽 그대로
|
||||
_fill_values(root) # pipe@40.0 에만 값
|
||||
assert copies.calc(root, FOLDER)["멈춤"] == [] and copies.calc(root, FOLDER)["계"] > 0
|
||||
assert len(copies.calc(root, "pipe@60.0")["멈춤"]) == 3 # 계산도 각자
|
||||
got = copies.calc(root, FOLDER)
|
||||
assert _stuck(got) == [] and got["기준합"]["계"] > 0
|
||||
assert copies.calc(root, "pipe@60.0")["빠진입력"] and not got["빠진입력"] # 계산도 각자
|
||||
flags = {i["폴더"]: i["고침"] for i in copies.list_items(root)}
|
||||
assert flags == {"a1": True, "a2": False, FOLDER: True, "pipe@60.0": False}
|
||||
|
||||
|
||||
@@ -82,7 +82,8 @@ 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, "base": 100, "reflected": 300}
|
||||
# 142-1 — 계 = 반영합 · 빈 값 = 빠진입력 · 못 돈 줄 = 까닭 있는 줄
|
||||
assert out["calc"] == {"total": 300, "gaps": 2, "stuck": 1, "base": 100, "reflected": 300}
|
||||
assert out["calcMissing"] is None # 없는 복사본 = 계산 줄 없음
|
||||
|
||||
|
||||
@@ -140,28 +141,20 @@ def test_넣을_값_카드는_sub2_파일_한_벌(out):
|
||||
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)
|
||||
def test_일위대가_카드_셈은_복사본_셈_답_통째_문서는_복사본_값(out):
|
||||
"""142-1 — 모달 일위대가 카드 = M02 카드 · 셈 = 복사본 셈(층 셈 꼴) · 문서 = 복사본 입력값 ·
|
||||
고른자재를 줄 입력값 · 고름으로(빈 값 = 자동값이라 안 얹음 · 서버 `with_values` 와 같은 규칙)."""
|
||||
assert out["laborCalc"] == {"줄": 2, "반영합": {"계": 300}, "빠진입력": 2}
|
||||
assert out["laborCalcMissing"] == "복사본이 없음" # 서버 까닭 그대로(카드 안 글)
|
||||
lines = out["withValues"]["담은로직"]
|
||||
assert lines[0] == {"로직": "GA1", "입력값": {"지역": 15}, "고름": {"0": "MT9"}}
|
||||
assert lines[1] == {
|
||||
"로직": "GA2",
|
||||
"입력값": {},
|
||||
"고름": {"0": "MT1"},
|
||||
} # 복사본에 값 없음 = 자동값
|
||||
assert lines[2] == {"참조": {"층": "system", "주인": None, "키": "SC-000001"}}
|
||||
assert out["withValues"]["일위대가번"] == "SU-000007"
|
||||
values = out["laborValues"]
|
||||
assert values["keys"] == ["GA1", "GA2"]
|
||||
assert values["logics"] == [["GA1", ["지역", "소량감률"]], ["GA2", ["지역"]]]
|
||||
@@ -169,9 +162,27 @@ def test_일위대가_요약_복사본_원본_넣을_값_전부(out):
|
||||
|
||||
|
||||
def test_모달_카드_차례_도면_일위대가_면적():
|
||||
"""142-1 — 일위대가 카드 = M02 카드 한 벌(`createItemCards` 의 `labor`) · 옛 요약 카드 · 넣을 값
|
||||
카드 · 머리 옛 「일위대가 {조합 이름}」 줄 없음."""
|
||||
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
|
||||
assert not (UI / "ui_template_item_picker_labor.ts").exists()
|
||||
assert "ui_template_item_picker_labor" not in detail
|
||||
assert 'labor: () => void edit("inputs"),' in detail # [수정] = 넣을 값 편집 화면
|
||||
assert "createInputsValueCard" not in detail and "extraCards" not in detail
|
||||
assert "folderLines" not in detail and "unitPrice" not in detail
|
||||
|
||||
|
||||
def test_모달_일위대가_카드_읽기_전용():
|
||||
"""142-1 — 모달 자료 자리엔 쓰는 길(`write`)이 없고 카드는 그때 보기만(원본 층 [저장]을 안 엶)."""
|
||||
detail = (UI / "ui_template_item_picker_detail.ts").read_text(encoding="utf-8")
|
||||
body = detail[detail.index("const makeSource") : detail.index("const badge")]
|
||||
assert "write" not in body and "labor: {" in body and "items.calcLabor(at)" in body
|
||||
cards = (M02 / "M02_MasterTemplete_ItemCards.ts").read_text(encoding="utf-8")
|
||||
assert "const laborWritable = (): boolean => opts.editable() && !!opts.source().write;" in cards
|
||||
assert "readOnly: !laborWritable()," in cards
|
||||
assert 'c.kind === "labor" ? laborWritable()' in cards
|
||||
picker = (UI / "ui_template_item_picker.ts").read_text(encoding="utf-8")
|
||||
assert '"personal" : "system"' in picker # 처음 펼침 = 개인(없으면 마스터)
|
||||
|
||||
|
||||
def test_모달은_원본_층에_쓰는_길을_안_부름():
|
||||
|
||||
@@ -60,10 +60,8 @@ def test_실패는_카드_안_글과_다시_불러오기():
|
||||
assert "showToast" not in cards # 토스트 안 씀(저장 알림은 틀)
|
||||
assert "c.shell.fail(" in cards and "reload" in cards
|
||||
picker = (ROOT / "ui_template" / "ui_template_item_picker_detail.ts").read_text("utf-8")
|
||||
# 일위대가 카드 · 값 카드가 다시 불러오기 하나(retry)를 같이 씀(39-1c)
|
||||
assert "const retry = (): void => void paintLabor(row, seq);" in picker
|
||||
assert "inputs.set(null, why(error), retry);" in picker
|
||||
assert "const stops = [labor.loading(), inputs.loading()];" in picker
|
||||
# 모달 일위대가 카드도 M02 카드 틀 안 실패 글(142-1 · 옛 요약 · 값 카드 retry 없음)
|
||||
assert "paintLabor" not in picker and 'labor: () => void edit("inputs"),' in picker
|
||||
|
||||
|
||||
def test_일위대가_카드는_상세_카드_갈래():
|
||||
@@ -75,7 +73,8 @@ def test_일위대가_카드는_상세_카드_갈래():
|
||||
assert 'makeCard("labor", PART_WORD.labor, laborHost, () => saveLabor())' in cards
|
||||
save = cards[cards.index("async function saveLabor()") :]
|
||||
assert save.index("laborEd.saveArea()") < save.index('saveTemplate(layer, "labor"')
|
||||
assert "onChanged: (dirty) => laborCard.saver.mark(dirty && opts.editable())," in cards
|
||||
# 고칠 수 있음 = editable + 쓰는 길(142-1 · 고르기 모달은 보기만)
|
||||
assert "onChanged: (dirty) => laborCard.saver.mark(dirty && laborWritable())," in cards
|
||||
assert "Labor_View" not in cards # 한 줄 표(40-2) 안 씀
|
||||
detail = _text("M02_MasterTemplete_Labor_Detail.ts")
|
||||
assert "move: reorder," in detail # 85-3 — 카드도 순서(▲▼ · 끌기) · 빼기는 그대로
|
||||
|
||||
@@ -169,14 +169,15 @@ def test_계산_반영_합은_손셈_원본과_같음(world):
|
||||
_save(root)
|
||||
_fill_auto(root)
|
||||
got = copies.calc(root, FOLDER)
|
||||
assert got["멈춤"] == [], got["줄"]
|
||||
assert [line for line in got["줄"] if line.get("까닭")] == [], 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["배"]))
|
||||
base = Decimal(str(line["기준"]["계"]))
|
||||
expect = base * Decimal(str(line["수량"])) / Decimal(str(line["배"]))
|
||||
assert Decimal(str(line["반영"]["계"])) == pytest.approx(expect)
|
||||
(mat,) = got["자재"]
|
||||
assert mat["코드"] == MATERIAL and mat["수량"] == MAT_QTY and mat["기준단가"]
|
||||
@@ -208,3 +209,21 @@ def test_일위대가_쓰기는_막힘(world):
|
||||
assert current["내용"]["줄"]["열"] == COLUMN
|
||||
with pytest.raises(ValueError, match="원본"):
|
||||
copies.write_file(root, FOLDER, "일위대가", current["판"], current["내용"])
|
||||
|
||||
|
||||
def test_같은_문서_같은_단가면_사본_셈은_층_셈(world):
|
||||
"""142-1 — 사본 셈 = 층 셈 한 벌(`M02_Labor_Doc.calc`) · 빈 입력은 둘 다 자동값 · 줄 꼴 같음."""
|
||||
root = world["root"]
|
||||
_save(root)
|
||||
got = copies.calc(root, FOLDER)
|
||||
given = {world["area_pill"]: AREA_QTY, labor_doc.material_key(MATERIAL): MAT_QTY}
|
||||
origin = labor_doc.calc(world["doc"], given)
|
||||
keys = ("로직", "알약", "분모", "배", "기준", "수량", "수량출처", "반영", "까닭")
|
||||
assert [{k: line.get(k) for k in keys} for line in got["줄"]] == [
|
||||
{k: line.get(k) for k in keys} for line in origin["줄"]
|
||||
]
|
||||
assert got["자재"] == origin["자재"]
|
||||
assert got["기준합"] == origin["기준합"] and got["반영합"] == origin["반영합"]
|
||||
assert got["반영합"]["계"] > 0
|
||||
assert {p["키"] for p in got["알약"]} == set(world["pills"]) # 사본 알약(가져온 때 것)
|
||||
assert got["빠진입력"] # 비운 입력은 목록으로(화면 「빈 값 n」)
|
||||
|
||||
@@ -78,9 +78,9 @@ def test_문서_계산과_프로젝트_사본_같음() -> None:
|
||||
got = prices.calc(copy, prices.to_json(prices.collect([CODE])), values, {})
|
||||
origin = labor.calc(doc, {})
|
||||
(line,) = got["줄"]
|
||||
assert line["계"] == pytest.approx(origin["줄"][0]["기준"]["계"])
|
||||
assert line["기준"]["계"] == pytest.approx(origin["줄"][0]["기준"]["계"]) # 층 셈 꼴(142-1)
|
||||
full = labor.calc({**doc, "담은로직": [{**doc["담은로직"][0], "켜쌓기율": None}]}, {})
|
||||
assert _d(line["계"]) == pytest.approx(_d(full["줄"][0]["기준"]["계"]) * Decimal("0.9"))
|
||||
assert _d(line["기준"]["계"]) == pytest.approx(_d(full["줄"][0]["기준"]["계"]) * Decimal("0.9"))
|
||||
|
||||
|
||||
def test_옛_단가승격도_율() -> None:
|
||||
|
||||
@@ -373,8 +373,8 @@ def test_프로젝트는_참조사본으로_굳힘_원본을_지워도_같은_
|
||||
(world["system"] / kind / f"{key}.json").unlink()
|
||||
got = prices.calc(rows, bundle, {}, {}, None)
|
||||
line = got["줄"][0]
|
||||
assert line["ok"] and line["참조"] == ref
|
||||
assert line["계"] == pytest.approx(live, rel=1e-9)
|
||||
assert not line["까닭"] and line["참조"] == ref # 층 셈 꼴(142-1)
|
||||
assert line["기준"]["계"] == pytest.approx(live, rel=1e-9)
|
||||
assert line["수량"] == 1 and line["반영"]["계"] == pytest.approx(live, rel=1e-9)
|
||||
|
||||
|
||||
@@ -458,7 +458,7 @@ def test_사전조합_로직_줄_섞임_합은_손셈_지우기_409_굳힘(world
|
||||
for key in (sc, sp): # 원본이 사라져도 굳힌 사본으로 같은 값
|
||||
(world["system"] / refs.kind_of(key) / f"{key}.json").unlink()
|
||||
got = prices.calc(rows, bundle, {}, {}, None)["줄"][0]
|
||||
assert got["계"] == pytest.approx(float(want), rel=1e-9)
|
||||
assert got["기준"]["계"] == pytest.approx(float(want), rel=1e-9)
|
||||
|
||||
|
||||
def test_사전조합이_자기_자신을_담으면_422_수량_기본_1(world):
|
||||
|
||||
Reference in New Issue
Block a user