feat(M02): 일위대가 자재 줄 2행 · 가격 다섯(43-3) — M01 값 열 다섯(물가자료 · 유통물가 · 물가정보 · 거래가격 · 관급) · 기준단가 칸 강조

- 서버 calc 답 자재 줄에 가격 · 기준칸(마스터 · 회사 · 개인 층 calc · 프로젝트 복사본 calc 같은 함수 material_price) · 원본 자료는 읽기만
- 자재목록 표 = 코드 · 이름 · 규격 · 수량산출(위 1 단위 / 아래 지금 수량) · 단위 · 가격 다섯(빈 것 「—」) · 기준단가 / 반영 금액
- 화면 판정 시나리오 resources/tester/ui_checks/43-3_자재가격.js

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
This commit is contained in:
2026-10-05 00:33:28 +09:00
co-authored by Claude Opus 5.5
parent 31a753b7fd
commit bc1314e94c
8 changed files with 145 additions and 41 deletions
+3
View File
@@ -181,6 +181,9 @@ def test_계산_반영_합은_손셈_원본과_같음(world):
(mat,) = got["자재"]
assert mat["코드"] == MATERIAL and mat["수량"] == MAT_QTY and mat["기준단가"]
assert mat["반영"] == pytest.approx(mat["기준단가"] * MAT_QTY)
assert (
mat["가격"][mat["기준칸"]] == mat["기준단가"]
) # 복사본 단가 묶음으로도 가격 다섯 · 기준칸(43-3)
total = hand["반영"]["계"] + area["반영"]["계"] + mat["반영"]
assert got["반영합"]["계"] == pytest.approx(total)
# 원본 층 문서 계산(마스터 같은 사본)과 같음 — 입력값은 프로젝트에 채운 것
+3
View File
@@ -98,6 +98,9 @@ def test_수량_손이_칸보다_앞섬_칸_값은_알약_키로():
assert (thousand["수량"], thousand["수량출처"]) == (1, "손")
mat = got["자재"][0]
assert (mat["기준단가"], mat["수량"], mat["반영"]) == (83000, 3, 249000)
# 43-3 — M01 값 열 다섯(빈 것 null) · 기준단가가 된 열 이름
assert list(mat["가격"]) == ["물가자료", "유통물가", "물가정보", "거래가격", "관급"]
assert mat["기준칸"] == "물가정보" and mat["가격"]["물가정보"] == 83000
nothing = labor.calc(doc, {})["줄"][0]
assert (nothing["수량출처"], nothing["반영"]) == ("없음", None)
@@ -0,0 +1,70 @@
// 43-3 자재 줄 2행 · 가격 다섯 — `orca eval --expression "$(cat 이 파일)"` 한 번에 돎(async 식 · JSON 돌려줌).
// 시작 화면 = M02 마스터 템플릿(#/m02-master-template · 시스템 관리자 · 마스터 층).
// 하는 일 = 좌측 일위대가 SU-000001 상세 → [자재 추가] → MT004177(시멘트, 전국) 고름 → 줄 수치 읽음 → [빼기]로 되돌림(저장 안 함).
// 기대 = heads 에 물가자료 · 유통물가 · 물가정보 · 거래가격 · 관급 · prices = 6,545 · 6,727 · 6,090 · 6,181 · — ·
// base = 물가정보(굵게 · 강조) · money.base = 6,090 · qty.base = 「1 포」 · after = 0(빼기 뒤 줄 없음).
(async () => {
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const until = async (fn, ms = 6000) => {
for (let t = 0; t < ms; t += 150) {
const got = fn();
if (got) return got;
await wait(150);
}
return null;
};
const CODE = "MT004177";
const group = [...document.querySelectorAll(".m02-side__group")].find((s) =>
s.querySelector(".ui-collapsible__title")?.textContent.trim().startsWith("일위대가"),
);
if (!group) return JSON.stringify({ error: "좌측 일위대가 없음" });
if (group.classList.contains("is-collapsed"))
group.querySelector(".ui-collapsible__title").click();
const su = [...group.querySelectorAll("button")].find((b) =>
b.textContent.startsWith("SU-000001"),
);
su?.click();
const add = await until(() =>
[...document.querySelectorAll("button")].find((b) => b.textContent.trim() === "자재 추가"),
);
if (!add) return JSON.stringify({ error: "자재 추가 단추 없음" });
add.click();
const box = await until(() => document.querySelector(".m02-pick input"));
let item = null;
for (let tries = 0; tries < 3 && !item; tries += 1) {
// 서버가 막 다시 떴으면 첫 찾기가 늦음 — 다시 침(찾기 칸 글 = 코드)
box.value = tries ? `${CODE} ` : CODE;
box.dispatchEvent(new Event("input", { bubbles: true }));
item = await until(() => {
const items = [...document.querySelectorAll(".m02-pick__item")];
return items.length === 1 && items[0].dataset.material === CODE ? items[0] : null;
}, 4000);
}
if (!item) return JSON.stringify({ error: "고르기 창에 코드 줄 없음" });
item.click();
const row = await until(() => {
const tr = document.querySelector(`tr[data-labor-mat="${CODE}"]`);
return tr && !tr.textContent.includes("…") ? tr : null;
}, 8000);
if (!row) return JSON.stringify({ error: "자재 줄이 안 섬(계산 대기)" });
const table = row.closest("table");
const out = {
heads: [...table.querySelectorAll("thead th")].map((th) => th.textContent.trim()),
prices: [...row.querySelectorAll("[data-price-slot]")].map((td) => td.textContent.trim()),
base: [...row.querySelectorAll(".is-base-price")].map((td) => td.dataset.priceSlot),
baseWeight: getComputedStyle(row.querySelector(".is-base-price") ?? row).fontWeight,
qty: {
base: row.querySelector(".m02-labrow__qty .m02-labrow__base")?.textContent.trim(),
applied: row.querySelector(".m02-labrow__qty .m02-labrow__applied")?.textContent.trim(),
},
money: {
base: row.querySelector(".m02-labrow__money .m02-labrow__base")?.textContent.trim(),
applied: row.querySelector(".m02-labrow__money .m02-labrow__applied")?.textContent.trim(),
},
};
// 되돌림 — 그 줄 [빼기](저장 안 함)
[...row.querySelectorAll("button")].find((b) => b.textContent.trim() === "빼기")?.click();
await wait(400);
out.after = document.querySelectorAll(`tr[data-labor-mat="${CODE}"]`).length;
return JSON.stringify(out);
})();