feat(M02): 일위대가 조합 서고 — 좌측 항목 둘 · 상세 목록표(51-4)
- 좌측 컨테이너 = 층 단추 + 「단가산출 승격」 · 「일위대가 사전조합」 항목만 · 새로 · 본떠 · 지우기 단추 없앰 - 항목 누름 = 상세 칸 목록표(고름 · 키 · 이름 · 단위 · 기준 금액 · 고친 날) · 줄 누름 = 그 문서 상세 - 머리 [생성] · [복사](고른 줄) · [삭제](고른 줄 · 쓰는 곳 있으면 서버 까닭 알림) - 층 목록 서고 줄에 1 단위 기준 계(`기준금액`) 실음 · 시험 한 개 · 화면 시나리오 51-4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
@@ -323,3 +323,18 @@ def test_새_프로젝트_씨앗은_서고를_안_가져감(world, tmp_path):
|
||||
_promote(world["system"])
|
||||
got = layers.seed_project(tmp_path / "p1")
|
||||
assert not any(k.startswith(layers.LIBRARY_KINDS) for k in got["작업본"])
|
||||
|
||||
|
||||
def test_층_목록_서고_줄은_이름_단위_기준금액을_실음(world):
|
||||
sp = _promote(world["system"])
|
||||
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}, "수량": {"손": 3}}], "셋")
|
||||
rows = [
|
||||
{"종류": "promote", "이름": sp},
|
||||
{"종류": "precombo", "이름": sc},
|
||||
{"종류": "labor", "이름": "rv"},
|
||||
]
|
||||
refs.summaries(world["system"], rows)
|
||||
one = float(Decimal(str(_base(TEN)["계"])) / 10)
|
||||
assert rows[0]["기준금액"] == pytest.approx(one)
|
||||
assert (rows[1]["제목"], rows[1]["기준금액"]) == ("셋", pytest.approx(one * 3))
|
||||
assert "기준금액" not in rows[2]
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
/* 51-4 — M02 좌측 「일위대가 조합」 = 층 단추 + 항목 둘 · 항목 누름 = 상세 목록표 · 머리 [생성] · [복사] · [삭제]
|
||||
* 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=일위대가조합&root=master) · 시스템 관리자
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
|
||||
* 쓰기 없음 — 서고 목록 · 문서 읽기 · 지우기는 가짜 응답(fetch 바꿔 끼움 · 끝에 되돌림) · 새로 창은 열고 닫기만
|
||||
* 기대 수치:
|
||||
* - side.items = ["단가산출 승격", "일위대가 사전조합"] · side.otherButtons = 0(새로 · 본떠 · 지우기 없음)
|
||||
* - promote.heads = ["", "키", "이름", "단위", "기준 금액", "고친 날"] · rows = 2 · base = ["1,234.5", ""]
|
||||
* - promote.tools = ["생성", "복사", "삭제"] · disabledBefore = [false, true, true] · disabledAfter = [false, false, false]
|
||||
* - promote.active = true(좌측 항목 고름) · picker = true(생성 = 로직 고르기 창)
|
||||
* - blocked = 쓰는 곳 까닭 알림 · rowsAfterBlocked = 2
|
||||
* - open = { title: "SP-999901 시험 승격", name: "시험 승격" }(줄 누름 = 그 문서 상세)
|
||||
* - precombo.rows = 1 · dialog = true(생성 = 이름 창)
|
||||
*/
|
||||
(async () => {
|
||||
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 4000) => {
|
||||
for (let t = 0; t < ms; t += 100) {
|
||||
const v = fn();
|
||||
if (v) return v;
|
||||
await wait(100);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const rows = [
|
||||
{
|
||||
종류: "promote",
|
||||
이름: "SP-999901",
|
||||
판: "a",
|
||||
수정일: "2026-10-05T10:00:00",
|
||||
제목: "시험 승격",
|
||||
단위: "㎡",
|
||||
기준금액: 1234.5,
|
||||
},
|
||||
{
|
||||
종류: "promote",
|
||||
이름: "SP-999902",
|
||||
판: "b",
|
||||
수정일: "2026-10-05T11:00:00",
|
||||
제목: "값 없음",
|
||||
단위: null,
|
||||
기준금액: null,
|
||||
},
|
||||
{
|
||||
종류: "precombo",
|
||||
이름: "SC-999901",
|
||||
판: "c",
|
||||
수정일: "2026-10-05T12:00:00",
|
||||
제목: "시험 묶음",
|
||||
단위: "개소",
|
||||
기준금액: 10,
|
||||
},
|
||||
];
|
||||
const doc = {
|
||||
종류: "단가승격",
|
||||
키: "SP-999901",
|
||||
이름: "시험 승격",
|
||||
단위: "㎡",
|
||||
담은로직: [{ 로직: "L-0001", 입력값: {} }],
|
||||
};
|
||||
const json = (body, status = 200) =>
|
||||
new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
|
||||
const real = window.fetch;
|
||||
window.fetch = async (input, init = {}) => {
|
||||
const url = String(input?.url ?? input);
|
||||
const method = (init.method ?? "GET").toUpperCase();
|
||||
if (method === "GET" && /\/m02\/layers\/system\/templates(\?|$)/.test(url)) {
|
||||
const got = await (await real(input, init)).json();
|
||||
return json({ ...got, 양식: [...got.양식.filter((r) => !/^S[PC]-/.test(r.이름)), ...rows] });
|
||||
}
|
||||
if (method === "GET" && url.includes("/templates/promote/SP-999901"))
|
||||
return json({ 종류: "promote", 이름: "SP-999901", 판: "a", 문서: doc });
|
||||
if (method === "DELETE" && /SP-9999/.test(url))
|
||||
return json({ detail: "쓰는 곳이 있어 못 지움 「SP-999901」 — 일위대가 시험" }, 409);
|
||||
return real(input, init);
|
||||
};
|
||||
try {
|
||||
const sec = [...document.querySelectorAll(".m02-side__lists > section")].find(
|
||||
(s) => s.querySelector(".ui-collapsible__title")?.textContent?.trim() === "일위대가 조합",
|
||||
);
|
||||
const items = [...sec.querySelectorAll("[data-precombo]")];
|
||||
const side = {
|
||||
items: items.map((b) => b.textContent),
|
||||
otherButtons: [
|
||||
...sec.querySelectorAll(".ui-collapsible__body button, .m02-side__items button"),
|
||||
].filter((b) => !b.matches("[data-precombo], .m02-layers__btn")).length,
|
||||
};
|
||||
const tools = () => [...document.querySelectorAll(".m02-main__slot .m02-main__tools button")];
|
||||
const list = (kind) => document.querySelector(`[data-lib-list="${kind}"]`);
|
||||
// 단가산출 승격
|
||||
items[0].click();
|
||||
await until(() => list("promote")?.querySelectorAll("[data-lib-key]").length);
|
||||
const L = list("promote");
|
||||
const promote = {
|
||||
title: document.querySelector(".m02-main__title")?.textContent,
|
||||
active: items[0].classList.contains("is-active"),
|
||||
heads: [...L.querySelectorAll("th")].map((x) => x.textContent),
|
||||
rows: L.querySelectorAll("[data-lib-key]").length,
|
||||
base: [...L.querySelectorAll("[data-lib-key]")].map((tr) => tr.children[4].textContent),
|
||||
tools: tools().map((b) => b.textContent),
|
||||
disabledBefore: tools().map((b) => b.disabled),
|
||||
};
|
||||
L.querySelector('[data-lib-key="SP-999901"] .m02-lib__pick').click();
|
||||
await wait(100);
|
||||
promote.disabledAfter = tools().map((b) => b.disabled);
|
||||
promote.stillList = !!list("promote"); // 고름 칸 누름은 안 엶
|
||||
// [생성] = 로직 고르기 창 → 닫기
|
||||
tools()[0].click();
|
||||
await wait(400);
|
||||
const pick = document.querySelector(".m01-logic__pick");
|
||||
promote.picker = !!pick;
|
||||
pick?.closest(".m01-logic__backdrop")?.remove();
|
||||
// [삭제] — 확인 창 [확인] → 쓰는 곳 409 까닭 알림
|
||||
tools()[2].click();
|
||||
const okBtn = await until(() =>
|
||||
document.querySelector(
|
||||
".ui-confirm__actions .ui-button--danger, .ui-confirm__actions button:last-child",
|
||||
),
|
||||
);
|
||||
okBtn?.click();
|
||||
const toast = await until(() =>
|
||||
[...document.querySelectorAll(".ui-toast")]
|
||||
.map((x) => x.textContent)
|
||||
.find((t) => t?.includes("쓰는 곳")),
|
||||
);
|
||||
const blocked = toast ?? null;
|
||||
const rowsAfterBlocked = list("promote")?.querySelectorAll("[data-lib-key]").length ?? 0;
|
||||
// 줄 누름 = 그 문서 상세
|
||||
list("promote").querySelector('[data-lib-key="SP-999901"] td:nth-child(3)').click();
|
||||
const nameInput = await until(() => document.querySelector('[data-labor-field="이름"] input'));
|
||||
const open = {
|
||||
title: document.querySelector(".m02-main__title")?.textContent,
|
||||
name: nameInput?.value ?? null,
|
||||
sideActive: items[0].classList.contains("is-active"),
|
||||
};
|
||||
// 일위대가 사전조합
|
||||
items[1].click();
|
||||
await until(() => list("precombo")?.querySelectorAll("[data-lib-key]").length);
|
||||
const precombo = {
|
||||
rows: list("precombo")?.querySelectorAll("[data-lib-key]").length ?? 0,
|
||||
active: items[1].classList.contains("is-active") && !items[0].classList.contains("is-active"),
|
||||
};
|
||||
tools()[0].click();
|
||||
await wait(300);
|
||||
const modal = document.querySelector(".m02-modal");
|
||||
precombo.dialog = !!modal;
|
||||
modal?.querySelector(".ui-modal__top button")?.click();
|
||||
await wait(200);
|
||||
precombo.dialogClosed = !document.querySelector(".m02-modal");
|
||||
return JSON.stringify({ side, promote, blocked, rowsAfterBlocked, open, precombo });
|
||||
} finally {
|
||||
window.fetch = real;
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user