Merge remote-tracking branch 'origin/sub_laptop_1' into sub_laptop_6
This commit is contained in:
@@ -323,3 +323,37 @@ 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]
|
||||
|
||||
|
||||
def test_사전조합이_자기_자신을_담으면_422_수량_기본_1(world):
|
||||
sp = _promote(world["system"])
|
||||
a = _combo(world["system"], [{"참조": {**SYS, "키": sp}}], "가")
|
||||
with store.at(world["system"]):
|
||||
have = store.read("precombo", a)
|
||||
assert have["문서"]["담은로직"][0]["수량"] == {"손": 1}
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
store.write(
|
||||
"precombo",
|
||||
a,
|
||||
have["판"],
|
||||
{
|
||||
**have["문서"],
|
||||
"담은로직": [*have["문서"]["담은로직"], {"참조": {**SYS, "키": a}}],
|
||||
},
|
||||
)
|
||||
assert e.value.status == 422 and "자기 자신" in e.value.detail
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/* 51-3 — 일위대가 상세 [로직 추가] 고르기 창 한 목록(찾기 단추 · 탭 없음 · 구분 칸 · 치는 대로 걸러짐)
|
||||
* 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 · item1 있음
|
||||
* 부르는 법: orca eval --expression "$(tr '
|
||||
' ' ' < 이 파일)" (async 식 · JSON 글을 돌려줌)
|
||||
* 공유 마스터에 서고 문서를 안 씀 — 이 탭 fetch 만 바꿔 끼움(층 목록 = 가짜 SP-990001 · SC-990001 더함 · 끝나면 되돌림)
|
||||
* 하는 일: item1 → 일위대가 [수정] → [로직 추가] → 목록 점검 → "SP-990001" 침 → 고름 → 표 구분 · 삭제(저장 안 함)
|
||||
* 기대 수치:
|
||||
* - tabs = 0 · findBtn = 0 · kinds 에 "단가산출" · "단가승격" · "일위조합" 셋 다 · layers = ["마스터"]
|
||||
* - typed = ["SP-990001"] · typedKinds = ["단가승격"]
|
||||
* - added.kinds = ["단가승격"] · added.keys = ["SP-990001"] · removed = true · askedLeave = false · back = true
|
||||
*/
|
||||
(async () => {
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 8000) => {
|
||||
for (let t = 0; t < ms; t += 150) {
|
||||
const v = fn();
|
||||
if (v) return v;
|
||||
await w(150);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const n = (s) => Number(String(s ?? "").replace(/[^0-9.-]/g, "")) || 0;
|
||||
const SP = { 층: "system", 주인: null, 키: "SP-990001" };
|
||||
const SC = { 층: "system", 주인: null, 키: "SC-990001" };
|
||||
const rid = (r) => `참조|${r.층}|${r.주인 ?? ""}|${r.키}`;
|
||||
const real = window.fetch;
|
||||
let first = null;
|
||||
window.fetch = async (url, init) => {
|
||||
const u = String(url);
|
||||
if (/\/layers\/system\/templates$/.test(u)) {
|
||||
const got = await (await real(url, init)).json();
|
||||
got.양식.push(
|
||||
{ 종류: "promote", 이름: SP.키, 제목: "시험 승격", 단위: "㎡" },
|
||||
{ 종류: "precombo", 이름: SC.키, 제목: "시험 조합", 단위: "식" },
|
||||
);
|
||||
return new Response(JSON.stringify(got), { status: 200 });
|
||||
}
|
||||
if (/\/labor\/[^/]+\/calc/.test(u) && init?.body) {
|
||||
const body = JSON.parse(init.body);
|
||||
const refs = (body.doc?.담은로직 ?? []).filter((r) => r.참조);
|
||||
if (!refs.length) return real(url, init);
|
||||
body.doc.담은로직 = body.doc.담은로직.filter((r) => !r.참조);
|
||||
const got = await (await real(url, { ...init, body: JSON.stringify(body) })).json();
|
||||
first = got.줄[0];
|
||||
for (const r of refs) {
|
||||
const base = { ...first.기준 };
|
||||
const line = {
|
||||
참조: r.참조,
|
||||
이름: r.참조.키 === SP.키 ? "시험 승격" : "시험 조합",
|
||||
알약: rid(r.참조),
|
||||
분모: "㎡",
|
||||
배: 1,
|
||||
기준: base,
|
||||
반영: null,
|
||||
수량: null,
|
||||
수량출처: "없음",
|
||||
까닭: null,
|
||||
};
|
||||
if (r.참조.키 === SP.키) Object.assign(line, { 원로직: first.로직, 줄: first.줄 });
|
||||
else
|
||||
line.안 = [{ 참조: SP, 수량: 1, 이름: "시험 승격", 단위: "㎡", 기준: base, 까닭: null }];
|
||||
got.줄.push(line);
|
||||
}
|
||||
return new Response(JSON.stringify(got), { status: 200 });
|
||||
}
|
||||
return real(url, init);
|
||||
};
|
||||
const out = {};
|
||||
try {
|
||||
const node = await until(() => document.querySelector('[data-node="i|item1"]'));
|
||||
if (!node) return JSON.stringify({ error: "item1 없음" });
|
||||
node.click();
|
||||
const card = await until(() =>
|
||||
[...document.querySelectorAll(".m02-struct__card")].find((c) =>
|
||||
c.querySelector("table.m02-labrow tbody tr[data-labor-logic]"),
|
||||
),
|
||||
);
|
||||
if (!card) return JSON.stringify({ error: "일위대가 카드 없음" });
|
||||
[...card.querySelectorAll(".m02-struct__card-tools button")]
|
||||
.find((b) => b.innerText.trim() === "수정")
|
||||
?.click();
|
||||
const table = () =>
|
||||
[...document.querySelectorAll("table.m02-labrow")].find(
|
||||
(t) => !t.closest(".m02-struct__card") && t.querySelector("tr[data-labor-logic]"),
|
||||
);
|
||||
await until(() => table() && !/…/.test(table().querySelector("tbody").innerText), 12000);
|
||||
const host = table().closest("[data-labor]");
|
||||
const before = table().querySelectorAll("tbody tr[data-labor-logic]").length;
|
||||
[...host.querySelectorAll(".m01-logic__section-head button")]
|
||||
.find((b) => b.innerText.trim() === "로직 추가")
|
||||
.click();
|
||||
const dlg = await until(() => document.querySelector(".m01-logic__pick"));
|
||||
await until(
|
||||
() =>
|
||||
dlg.querySelector(`[data-pick="${SP.키}"]`) &&
|
||||
[...dlg.querySelectorAll("[data-pick-kind]")].some(
|
||||
(k) => k.innerText.trim() === "단가산출",
|
||||
),
|
||||
);
|
||||
const kinds = () =>
|
||||
[...dlg.querySelectorAll("[data-pick-kind]")].map((k) => k.innerText.trim());
|
||||
out.tabs = dlg.querySelectorAll("[data-pick-tabs]").length;
|
||||
out.findBtn = [...dlg.querySelectorAll("button")].filter(
|
||||
(b) => b.innerText.trim() === "찾기",
|
||||
).length;
|
||||
out.kinds = [...new Set(kinds())];
|
||||
out.total = dlg.querySelectorAll("[data-pick]").length;
|
||||
out.layers = [...dlg.querySelectorAll("[data-pick-layers] button")].map((b) =>
|
||||
b.innerText.trim(),
|
||||
);
|
||||
const search = dlg.querySelector('input[type="search"]');
|
||||
search.value = "SP-990001";
|
||||
search.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
await w(200);
|
||||
out.typed = [...dlg.querySelectorAll("[data-pick]")].map((b) => b.dataset.pick);
|
||||
out.typedKinds = kinds();
|
||||
dlg.querySelector(`[data-pick="${SP.키}"]`).click();
|
||||
await until(
|
||||
() =>
|
||||
table()?.querySelector(`tr[data-labor-logic="${rid(SP)}"]`) &&
|
||||
!/…/.test(table().querySelector("tbody").innerText),
|
||||
12000,
|
||||
);
|
||||
const rows = [...table().querySelectorAll("tbody tr[data-labor-logic]")];
|
||||
out.added = {
|
||||
kinds: rows.slice(-1).map((r) => r.querySelector(".m02-labrow__kind").innerText.trim()),
|
||||
keys: rows.slice(-1).map((r) => r.querySelector(".m02-labrow__key").innerText.trim()),
|
||||
optBtn: rows
|
||||
.slice(-1)
|
||||
.flatMap((r) =>
|
||||
[...r.querySelectorAll("button")].filter((b) => b.innerText.trim() === "옵션"),
|
||||
).length,
|
||||
};
|
||||
const open = async (ref) => {
|
||||
table()
|
||||
.querySelector(`tr[data-labor-logic="${rid(ref)}"] .m02-labrow__name`)
|
||||
.click();
|
||||
await w(300);
|
||||
return host.querySelector("[data-labor-linedet]");
|
||||
};
|
||||
let det = await open(SP);
|
||||
const grid = det.querySelector("table.m01-logic__grid");
|
||||
out.promote = {
|
||||
from: det.querySelector("p")?.innerText.trim(),
|
||||
rows: grid?.querySelectorAll("tbody tr").length ?? 0,
|
||||
sum: [...(grid?.querySelectorAll("tbody tr") ?? [])].reduce(
|
||||
(a, r) => a + n(r.lastElementChild.innerText),
|
||||
0,
|
||||
),
|
||||
firstBase: first?.기준?.계 ?? null,
|
||||
};
|
||||
for (const ref of [SP]) {
|
||||
const tr = table().querySelector(`tr[data-labor-logic="${rid(ref)}"]`);
|
||||
[...tr.querySelectorAll("button")].find((b) => b.innerText.trim() === "삭제").click();
|
||||
await w(600);
|
||||
}
|
||||
out.removed = table().querySelectorAll("tbody tr[data-labor-logic]").length === before;
|
||||
} finally {
|
||||
window.fetch = real;
|
||||
}
|
||||
document.querySelector('[data-node="i|item1"]')?.click();
|
||||
await w(800);
|
||||
const ask = [...document.querySelectorAll(".ui-modal button, dialog button")].find((b) =>
|
||||
/확인|버림|떠나기/.test(b.innerText),
|
||||
);
|
||||
out.askedLeave = !!ask; /* 저장 안 한 고침이 남았으면 물음 — 지운 뒤라 없어야 함 */
|
||||
ask?.click();
|
||||
await w(1200);
|
||||
out.back = !!document.querySelector(".m02-struct__card table.m02-labrow");
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
@@ -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;
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,208 @@
|
||||
/* 51-6 — 서고 사전조합 상세(일위대가 상세 서고 모드) 줄 넣는 길
|
||||
* 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=일위대가조합&root=master) · 시스템 관리자
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
|
||||
* 쓰기 없음 — 서고 목록 · 문서 읽기 · [저장] PUT 은 가짜 응답(fetch 바꿔 끼움 · 끝에 되돌림) · calc 만 서버
|
||||
* 기대 수치:
|
||||
* - promote = { add: false, rows: 1, move: 1 }(단가승격 상세는 한 줄 고정 · [옵션]만 · 위로 · 아래로 · 삭제 없음)
|
||||
* - precombo.add = true · picker = { filters: 0, kinds: ["단가승격", "단가승격", "일위조합"], layers: 1 }
|
||||
* - added = 2 · again = 「이미 담은」 알림
|
||||
* - moved = ["SP-999902", "SP-999901"](위로) · qty = { start: "1"(새 줄 기본), sheet: false, after: { 손: 3 } } · addedQty = { 손: 3 }
|
||||
* - removed = 1 · selfSave = 「순환」 알림(PUT 422) · putBody 에 자기 키 · left = 버림 확인 뒤 목록
|
||||
*/
|
||||
(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 SYS = { 층: "system", 주인: null };
|
||||
const rows = [
|
||||
{
|
||||
종류: "promote",
|
||||
이름: "SP-999901",
|
||||
판: "a",
|
||||
수정일: "2026-10-05T10:00:00",
|
||||
제목: "시험 승격",
|
||||
단위: "㎡",
|
||||
기준금액: 100,
|
||||
},
|
||||
{
|
||||
종류: "promote",
|
||||
이름: "SP-999902",
|
||||
판: "b",
|
||||
수정일: "2026-10-05T11:00:00",
|
||||
제목: "시험 승격 둘",
|
||||
단위: "㎡",
|
||||
기준금액: 200,
|
||||
},
|
||||
{
|
||||
종류: "precombo",
|
||||
이름: "SC-999901",
|
||||
판: "c",
|
||||
수정일: "2026-10-05T12:00:00",
|
||||
제목: "시험 묶음",
|
||||
단위: "개소",
|
||||
기준금액: 100,
|
||||
},
|
||||
];
|
||||
const docs = {
|
||||
"promote/SP-999901": {
|
||||
종류: "단가승격",
|
||||
키: "SP-999901",
|
||||
이름: "시험 승격",
|
||||
단위: "㎡",
|
||||
담은로직: [{ 로직: "GC000421", 입력값: {} }],
|
||||
},
|
||||
"precombo/SC-999901": {
|
||||
종류: "사전조합",
|
||||
키: "SC-999901",
|
||||
이름: "시험 묶음",
|
||||
단위: "개소",
|
||||
담은로직: [{ 참조: { ...SYS, 키: "SP-999901" }, 수량: { 손: 1 } }],
|
||||
},
|
||||
};
|
||||
const json = (body, status = 200) =>
|
||||
new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
|
||||
const real = window.fetch;
|
||||
let putBody = null;
|
||||
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] });
|
||||
}
|
||||
const hit = Object.keys(docs).find((k) => url.includes(`/templates/${k}`));
|
||||
if (hit && method === "GET")
|
||||
return json({ 종류: hit.split("/")[0], 이름: docs[hit].키, 판: "a", 문서: docs[hit] });
|
||||
if (hit && method === "PUT") {
|
||||
putBody = JSON.parse(init.body);
|
||||
const self = JSON.stringify(putBody).includes('"키":"SC-999901"}');
|
||||
return self
|
||||
? json({ detail: "순환 — 자기 자신을 담음 「SC-999901」" }, 422)
|
||||
: json({ 이름: "SC-999901", 판: "z" });
|
||||
}
|
||||
if (method !== "GET" && !url.includes("/calc"))
|
||||
return json({ detail: "시험 — 쓰기 막음" }, 400);
|
||||
return real(input, init);
|
||||
};
|
||||
const sec = [...document.querySelectorAll(".m02-side__lists > section")].find(
|
||||
(s) => s.querySelector(".ui-collapsible__title")?.textContent?.trim() === "일위대가 조합",
|
||||
);
|
||||
const items = [...sec.querySelectorAll("[data-precombo]")];
|
||||
const btn = (root, text) =>
|
||||
[...root.querySelectorAll("button")].find((b) => b.textContent === text);
|
||||
const edit = () => document.querySelector(".m02-main__host [data-labor]");
|
||||
const lines = () => [...(edit()?.querySelectorAll("[data-labor-logic]") ?? [])];
|
||||
const keys = () => lines().map((tr) => tr.querySelector(".m02-labrow__key")?.textContent);
|
||||
const toast = (re) =>
|
||||
until(() =>
|
||||
[...document.querySelectorAll(".ui-toast")]
|
||||
.map((x) => x.textContent)
|
||||
.find((t) => re.test(t ?? "")),
|
||||
);
|
||||
const openDoc = async (i, key) => {
|
||||
items[i].click();
|
||||
const row = await until(() =>
|
||||
document.querySelector(`[data-lib-key="${key}"] td:nth-child(3)`),
|
||||
);
|
||||
row.click();
|
||||
await until(() => lines().length);
|
||||
await wait(300);
|
||||
};
|
||||
const pickerRow = async (key) => {
|
||||
btn(edit(), "추가").click();
|
||||
const row = await until(() => document.querySelector(`.m01-logic__pick [data-pick="${key}"]`));
|
||||
return row;
|
||||
};
|
||||
try {
|
||||
// 단가승격 상세 — 한 줄 고정
|
||||
await openDoc(0, "SP-999901");
|
||||
const promote = {
|
||||
add: !!btn(edit(), "추가"),
|
||||
rows: lines().length,
|
||||
move: edit().querySelectorAll(".m02-labrow__move button").length,
|
||||
};
|
||||
// 사전조합 상세
|
||||
await openDoc(1, "SC-999901");
|
||||
const precombo = { add: !!btn(edit(), "추가"), rows: lines().length };
|
||||
const first = await pickerRow("SP-999902");
|
||||
const pick = document.querySelector(".m01-logic__pick");
|
||||
const picker = {
|
||||
filters: pick.querySelectorAll(".m01-logic__filters select").length,
|
||||
kinds: [...pick.querySelectorAll("[data-pick-kind]")].map((x) => x.textContent),
|
||||
layers: pick.querySelectorAll("[data-pick-layers] button").length,
|
||||
};
|
||||
first.click();
|
||||
await until(() => lines().length === 2);
|
||||
await wait(300);
|
||||
const added = lines().length;
|
||||
// 같은 참조 두 번 — 막힘
|
||||
(await pickerRow("SP-999901")).click();
|
||||
const again = await toast(/이미 담은/);
|
||||
// 위로 — 둘째 줄을 첫째로
|
||||
btn(lines()[1], "위로").click();
|
||||
await wait(300);
|
||||
const moved = keys();
|
||||
// 수량 손 입력 — 시트 없음 · 3
|
||||
lines()[0].querySelector("[data-labor-qty-open]").click();
|
||||
const hand = await until(() => document.querySelector('.m02-labqty [data-labor-qty="hand"]'));
|
||||
const qty = {
|
||||
start: hand?.value,
|
||||
sheet: !!document.querySelector(".m02-labqty [data-labor-qty-sheet]"),
|
||||
};
|
||||
hand.value = "3";
|
||||
hand.dispatchEvent(new Event("input"));
|
||||
btn(document.querySelector(".m02-labqty"), "확인").click();
|
||||
await wait(400);
|
||||
// 저장 본문으로 수량 확인(가짜 PUT)
|
||||
const save = [...document.querySelectorAll(".m02-main__head button")].find(
|
||||
(b) => b.textContent === "저장",
|
||||
);
|
||||
save.click();
|
||||
await until(() => putBody);
|
||||
const sent = putBody.문서.담은로직;
|
||||
const addedQty = sent.find((x) => x.참조.키 === "SP-999902")?.수량 ?? null;
|
||||
qty.after = sent[0]?.수량 ?? null;
|
||||
// 삭제 — 줄 하나 뺌
|
||||
btn(lines()[1], "삭제").click();
|
||||
await wait(300);
|
||||
const removed = lines().length;
|
||||
// 자기 자신 담고 [저장] → 서버 422 알림
|
||||
putBody = null;
|
||||
(await pickerRow("SC-999901")).click();
|
||||
await until(() => lines().length === 2);
|
||||
save.click();
|
||||
const selfSave = await toast(/순환/);
|
||||
const selfSent = JSON.stringify(putBody ?? {}).includes("SC-999901");
|
||||
// 떠남 — 버림 확인 → 목록
|
||||
items[1].click();
|
||||
const ok = await until(() =>
|
||||
document.querySelector(
|
||||
".ui-confirm__actions .ui-button--danger, .ui-confirm__actions button:last-child",
|
||||
),
|
||||
);
|
||||
ok?.click();
|
||||
const left = !!(await until(() => document.querySelector('[data-lib-list="precombo"]')));
|
||||
return JSON.stringify({
|
||||
promote,
|
||||
precombo,
|
||||
picker,
|
||||
added,
|
||||
addedQty,
|
||||
again,
|
||||
moved,
|
||||
qty,
|
||||
removed,
|
||||
selfSave,
|
||||
selfSent,
|
||||
left,
|
||||
});
|
||||
} finally {
|
||||
window.fetch = real;
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user