feat(M02): 구조물 조합 일위대가 카드 = 상세 그대로 — 단가산출 로직 2행 표 + 자재목록(Labor_Detail 카드 갈래 · 순서 · 로직 추가 · 다이어그램 · 정보 칸 없음 · 빼기 · 자재 추가 그대로) · 카드 [저장] · Ctrl+S

43-4 — 줄 누름 = 넣을 값 모달 · 수량산출 칸 = 수량 모달(상세와 같은 함수) · 저장 차례 = 면적 먼저 → 일위대가 · 표 감싸개 안 가로 스크롤 · 40-2 한 줄 표(compactTable · Labor_View compact · m02-labline css) 지움 · 빼기만 있는 줄도 끝 칸 · 화면 시나리오 ui_checks/43-4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
2026-10-05 00:33:55 +09:00
co-authored by Claude Opus 5.5
parent 31a753b7fd
commit ca4fcf0364
7 changed files with 195 additions and 262 deletions
+19
View File
@@ -61,3 +61,22 @@ def test_실패는_카드_안_글과_다시_불러오기():
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
def test_일위대가_카드는_상세_카드_갈래():
"""43-4 — 일위대가 카드 몸 = Labor_Detail 카드 갈래(단가산출 로직 2행 표 + 자재목록 · 순서 ·
로직 추가 · 다이어그램 없음 · 빼기 그대로) · 카드 [저장] = 면적 먼저 → 일위대가 ·
한 줄 표 지움."""
cards = _text("M02_MasterTemplete_ItemCards.ts")
assert "mod.mountLaborDetail(laborHost, got.문서, {" in cards and "card: true," in cards
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
assert "Labor_View" not in cards # 한 줄 표(40-2) 안 씀
detail = _text("M02_MasterTemplete_Labor_Detail.ts")
assert "...(opt.card ? {} : { move })" in detail # 순서 단추 없음 · 빼기는 그대로
assert "opt.card ? null : addLogic" in detail and "...(opt.card ? [] : [dgBox])," in detail
rows = _text("M02_MasterTemplete_Labor_Rows.ts")
assert "const tools = !!(move || remove);" in rows and "compactTable" not in rows
assert "compact" not in _text("M02_MasterTemplete_Labor_View.ts").split("*/", 1)[1]
@@ -0,0 +1,96 @@
/* 43-4 — 구조물 조합 일위대가 카드 = 일위대가 상세 카드 갈래(단가산출 로직 2행 표 + 자재목록)
* 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 ·
* 나무에 item1(일위대가 담은 로직 있는 열)이 있음
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌 · 20초 안 — 길면 ORCA 가 끊음)
* 기대 수치:
* - sections = ["단가산출 로직", "자재목록"] · diagram = 0 · info = 0(단위 · 본뜬 칸 없음)
* - addLogic = 0 · up = down = 0 · remove = 로직 줄 수 · addMat = 1 · rows = 담은 로직 수(item1 = 14)
* - money2 = true(금액 칸이 두 줄 — 위 기준 · 아래 반영)
* - scroll.overflowX = "auto" · scroll.wide = 참(item1 표 ≈ 870px > 카드 ≈ 310px · 표 감싸개 안 가로 스크롤)
* - rowModal = 「넣을 값 — …」 · qtyModal = 모달 뜸(수량 · 43-1) · 둘 다 닫기 뒤 dirty = false
* - 빼기 하나 → dirty = true · saveEnabled = true · 다른 항목(item3) → 버림 물음 「확인」 → item1 다시(되돌림)
* → rowsAfter = rows · dirtyAfter = false
*/
(async () => {
const w = (ms) => new Promise((r) => setTimeout(r, ms));
const until = async (fn, ms = 6000) => {
for (let t = 0; t < ms; t += 150) {
const v = fn();
if (v) return v;
await w(150);
}
return null;
};
const vis = (b) => b && !b.hidden && b.offsetParent !== null;
const pick = async () => {
document.querySelector('[data-node="i|item1"]').click();
return until(() => {
const ed = document.querySelector(".m02-struct__labor .m02-labor__edit");
return ed && ed.querySelector("tbody tr[data-labor-logic]") ? ed : null;
});
};
const ed = await pick();
if (!ed) return JSON.stringify({ error: "카드 몸 없음" });
const card = ed.closest(".m02-struct > *");
const btn = (t) =>
[...card.querySelectorAll("button")].filter((b) => vis(b) && b.textContent === t);
const save = [...card.querySelectorAll(".ui-collapsible__title button")].find(
(b) => b.textContent === "저장",
);
const rows = () => ed.querySelectorAll("tbody tr[data-labor-logic]").length;
const money = ed.querySelector("tbody tr[data-labor-logic] td:nth-child(5)");
const out = {
sections: [...ed.querySelectorAll(".m01-logic__section-head h3")].map((h) => h.textContent),
diagram: ed.querySelectorAll(".m02-labor__dg").length,
info: ed.querySelectorAll("[data-labor-field]").length,
addLogic: btn("로직 추가").length,
up: btn("올리기").length,
down: btn("내리기").length,
remove: btn("빼기").length,
addMat: btn("자재 추가").length,
rows: rows(),
money2: !!money && money.children.length >= 2,
// 표마다 공용 감싸개(ui-general-block__table-wrap)가 가로 스크롤 — 카드보다 넓으면 그 안에서
scroll: (() => {
const wrap = ed.querySelector("table")?.parentElement;
return wrap
? {
overflowX: getComputedStyle(wrap).overflowX,
wide: wrap.scrollWidth > wrap.clientWidth,
sw: wrap.scrollWidth,
cw: wrap.clientWidth,
}
: null;
})(),
};
// 줄 누름 = 넣을 값 모달 · 수량산출 칸 = 수량 모달 — 닫기만(값 안 바꿈)
const modal = async (open) => {
open();
const m = await until(() => document.querySelector(".ui-modal"), 2000);
const t = m?.querySelector("h3")?.textContent ?? null;
[...(m?.querySelectorAll("button") ?? [])].find((b) => b.textContent === "닫기")?.click();
await until(() => !document.querySelector(".ui-modal"), 1500);
return t;
};
const first = () => ed.querySelector("tbody tr[data-labor-logic]");
out.rowModal = await modal(() => first().querySelector(".m02-labrow__name").click());
out.qtyModal = await modal(() =>
(
first().querySelector("[data-labor-qty-open]") ?? first().querySelector(".m02-labrow__qty")
).click(),
);
out.dirtyAfterModals = !save.disabled;
// 빼기 하나 → 고침 표시 · 다른 항목으로 갔다 버림 확인 → item1 다시(서버 판으로 되돌림)
btn("빼기").pop().click();
await w(300);
out.dirty = card.classList.contains("is-dirty") || !save.disabled;
out.saveEnabled = !save.disabled;
document.querySelector('[data-node="i|item3"]').click();
const ask = await until(() => document.querySelector(".ui-confirm"), 2000);
[...(ask?.querySelectorAll("button") ?? [])].find((b) => b.textContent === "확인")?.click();
await w(600);
const again = await pick();
out.rowsAfter = again ? again.querySelectorAll("tbody tr[data-labor-logic]").length : null;
out.dirtyAfter = !save.disabled;
return JSON.stringify(out);
})();