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:
@@ -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)
|
||||
# 원본 층 문서 계산(마스터 같은 사본)과 같음 — 입력값은 프로젝트에 채운 것
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
Reference in New Issue
Block a user