From 36a9461f5771c500c2cc26c92c618fe18115de7c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 19:32:14 +0900 Subject: [PATCH 1/2] =?UTF-8?q?chore(sync):=20=EA=B3=B5=EC=9C=A0=20docs=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=8B=B4=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/raw/사용자요구/2026-10-05_수정요구.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index c8dbf6477..50ae3392e 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -88,3 +88,19 @@ 8. 구조물 조합의 일위대가 상세화면에서의 로직테이블은 일위대가 목록표임. 여기서 행을 눌렀을때 하단에 상세 내역이 보였으면 함. 상세내역은 M01페이지의 단가산출 로직을 보면 시험계산 컨테이너 내부에 기준단위에 대한 테이블 리스트가 있음. 이걸 B02 구조물 조합의 일위대가 조합에 맞게 넣어주면 됨. 행을 선택했을때만 보여주면 되고 다른 행을 선택하면 닫히면 됨. 9. 구조물 조합의 일위대가 상세화면에 단가산출 로직 테이블의 이름 좌측 열을 추가하고 구분을 추가함. 구분은 출처로 단가산출인지, 일위대가 조합인지, 단가산출을 승격한 정보인지 표기 항목은 4음절로 표기, 일단 상세히 반영 가능성에 대해 상세히 검토후 계획서에 반영 + +## 오른쪽 패널 높이 (10-5 · 원문 그대로) + +하나씩 수정 내용에 대해 요청할께. M01 단가산출 로직 상세와 M02의 구조물 조합의 일위대가와 면적산출에는 우측 패널이 존재함. 이 우측 패널의 높이는 화면에 맞춰서 높이를 결정하게 했으면 함. 스크롤 동작시 움직임. + +추가로 너가 오해한게 있는데 구조물 조합의 일위대가 상세페이지에서 단가산출 로직 행을 선택했을때 나오는 상세정보를 해당행이 아래에 별도의 행으로 구분하여 접었다 폈다하면서 내용을 보줬으면 해. 별도의 공간에 M01처럼 보여달라는게 아니라 테이블에 맞춰서 정보를 넣어달라는 의미였어. + +## 서고 · 고르기 창 (10-5 · 원문 그대로) + +1. 로직고르기 모달창에서 찾기 버튼은 필요없어 보임. 다른 찾기 기능을 보면 없어도 될것같아. 그리고 단가 승격과 사전 조합을 별도의 버튼으로 활성화하는데 이건 그냥 구분정보를 보여줘서 선택을 유도해도 되지 않을까? +2. 일위대가 조합 컨테이너에는 새로, 본떠 만들기, 지우기 버튼은 필요없고 단가산출 승격 이라는 항목과 일위대가 사전조합이라는 항목 두게만 있으면 될것같음. 그리고 해당 항목을 누르면 상세에 테이블형태로 리스트 들을 보여주고 거기에 생성, 복사, 삭제 버튼이 있으면 됨. +3. 사전 조합도 동일함 + +단위를 선택하게 하는 항목들 중 선택 후 나오는 단위 표기 버튼이 너무 작음 저장같은 버튼과 같은 크기로 변경 + +사전조합에 상세에서 보여지는 단가산출 로직의 선택 기능이 없음 From 9aeefd44dcd80740308c5eb1aa7157e4077626bc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 19:35:01 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(M02):=20=EB=A1=9C=EC=A7=81=20=EA=B3=A0?= =?UTF-8?q?=EB=A5=B4=EA=B8=B0=20=EC=B0=BD=20=ED=95=9C=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=20=E2=80=94=20=EC=B0=BE=EA=B8=B0=20=EB=8B=A8=EC=B6=94=20=C2=B7?= =?UTF-8?q?=20=ED=83=AD=20=EC=97=86=EC=95=B0=20=C2=B7=20=EA=B5=AC=EB=B6=84?= =?UTF-8?q?=20=EC=B9=B8=20=C2=B7=20=EC=B9=98=EB=8A=94=20=EB=8C=80=EB=A1=9C?= =?UTF-8?q?=20=EA=B1=B8=EB=9F=AC=EC=A7=90=20(51-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4 --- .../M02_MasterTemplete_Labor_Pick.ts | 203 +++++++++--------- .../M02_MasterTemplete_Labor_Text.ts | 6 - .../tester/ui_checks/51-3_고르기_한목록.js | 171 +++++++++++++++ 3 files changed, 269 insertions(+), 111 deletions(-) create mode 100644 resources/tester/ui_checks/51-3_고르기_한목록.js diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts index d6a92347d..c8cac3e90 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts @@ -1,8 +1,8 @@ /* ============================================================================= * M02_MasterTemplete_Labor_Pick.ts - * [로직 더하기] 창 — 구분 → 상세구분 → 찾기로 사용자가 로직을 하나씩 고름(자동 추천 없음) · - * 탭 단가산출 · 단가승격 · 사전조합(50-3 ④) — 서고 탭은 층 단추(위 층만 · 짝 `M02_Labor_Ref.readable`) + - * 그 층 목록 · 고르면 참조 {층 · 주인 · 키} · 프로젝트 · 카드엔 서고 탭 없음 + * [로직 더하기] 창 — 한 목록에 구분(단가산출 · 단가승격 · 일위조합) 칸 · 치는 대로 걸러짐(찾기 단추 없음) · + * 단가승격 · 일위조합 줄은 층 단추(위 층만 · 짝 `M02_Labor_Ref.readable`) 층의 서고 · 고르면 참조 {층 · 주인 · 키} · + * 프로젝트 · 카드엔 서고 줄 없음 * ========================================================================== */ import { @@ -65,9 +65,39 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra detail.select.disabled = true; const searchField = createInputField({ type: "search", placeholder: tc("Pick_Search") }); const list = el("div", { className: "m01-logic__pick-list" }); - const hint = el("p", { className: "m01-logic__muted", text: tc("Pick_Hint") }); + const hint = el("p", { className: "m01-logic__muted" }); const close = (): void => backdrop.remove(); + /* 한 목록 — 구분(단가산출 · 단가승격 · 일위조합) 칸 · 층 단추는 서고 줄(승격 · 조합)의 층 */ + const layerBar = el("div", { className: "ui-btn-group", attrs: { "data-pick-layers": "" } }); + let scopes: Scope[] = []; + let scope: Scope | null = null; + let libRows: { kind: "Kind_Promote" | "Kind_Combo"; row: LibraryRow }[] = []; + let loading = false; + let seq = 0; + const pickRow = ( + key: string, + kind: string, + title: string, + sub2: string, + unit: string, + ): HTMLElement => + el("button", { + className: "m01-logic__pick-row m01-logic__pick-row--button", + attrs: { type: "button", "data-pick": key }, + children: [ + el("span", { className: "m01-logic__muted", attrs: { "data-pick-kind": "" }, text: kind }), + el("div", { + className: "m01-logic__stack", + children: [ + el("strong", { text: title }), + el("span", { className: "m01-logic__muted", text: sub2 }), + ], + }), + el("span", { text: unit }), + ], + }); + const find = (): void => { const q = searchField.input.value.trim().toLowerCase(); const found = all.filter( @@ -76,96 +106,68 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra (!detail.select.value || x.상세구분 === detail.select.value) && (!q || `${x.키} ${x.이름}`.toLowerCase().includes(q)), ); - hint.textContent = found.length ? `${found.length} / ${all.length}` : tc("Pick_None"); + const libFound = sub.select.value + ? [] + : libRows.filter((x) => !q || `${x.row.이름} ${x.row.제목 ?? ""}`.toLowerCase().includes(q)); + const total = all.length + libRows.length; + const shown = found.length + libFound.length; + hint.textContent = loading ? tc("Loading") : shown ? `${shown} / ${total}` : tc("Pick_None"); list.replaceChildren( ...found.slice(0, 100).map((x) => { - const row = el("button", { - className: "m01-logic__pick-row m01-logic__pick-row--button", - attrs: { type: "button", "data-pick": x.키 }, - children: [ - el("div", { - className: "m01-logic__stack", - children: [ - el("strong", { text: x.이름 }), - el("span", { - className: "m01-logic__muted", - text: `${x.키} · ${x.구분} ${x.상세구분}`.trim(), - }), - ], - }), - el("span", { text: `${x.결과단위}${x.blocked ? ` · ${tc("Pick_Blocked")}` : ""}` }), - ], - }); + const row = pickRow( + x.키, + tc("Kind_Logic"), + x.이름, + `${x.키} · ${x.구분} ${x.상세구분}`.trim(), + `${x.결과단위}${x.blocked ? ` · ${tc("Pick_Blocked")}` : ""}`, + ); row.addEventListener("click", () => { close(); done(x); }); return row; }), - ); - }; - sub.select.addEventListener("change", () => { - const d = subs.find((s) => s.name === sub.select.value)?.details ?? []; - detail.setOptions(opts(d), ""); - detail.select.disabled = d.length === 0; - }); - - /* 서고 탭(50-3 ④) — 탭 · 층 단추 · 목록 */ - const filters = el("div", { - className: "m01-logic__filters", - children: [ - sub.root, - detail.root, - searchField.root, - createButton({ label: tc("Pick_Find"), variant: "filled", onClick: find }), - ], - }); - const tabs = el("div", { className: "ui-btn-group", attrs: { "data-pick-tabs": "" } }); - const layerBar = el("div", { className: "ui-btn-group", attrs: { "data-pick-layers": "" } }); - let tab: "logic" | "promote" | "precombo" = "logic"; - let scopes: Scope[] = []; - let scope: Scope | null = null; - let seq = 0; - const libList = async (): Promise => { - if (!lib || !scope || tab === "logic") return; - const mine = ++seq; - const at = scope; - list.replaceChildren(); - hint.textContent = tc("Loading"); - let rows: LibraryRow[] = []; - try { - rows = await fetchLibrary(at, tab); - } catch (error) { - if (mine === seq) - hint.textContent = error instanceof Error ? error.message : tc("Load_Failed"); - return; - } - if (mine !== seq) return; - hint.textContent = rows.length ? `${rows.length}` : tc("Pick_None"); - list.replaceChildren( - ...rows.map((x) => { - const row = el("button", { - className: "m01-logic__pick-row m01-logic__pick-row--button", - attrs: { type: "button", "data-pick": x.이름 }, - children: [ - el("div", { - className: "m01-logic__stack", - children: [ - el("strong", { text: x.제목 || x.이름 }), - el("span", { className: "m01-logic__muted", text: x.이름 }), - ], - }), - el("span", { text: x.단위 ?? "" }), - ], - }); + ...libFound.map((x) => { + const at = scope as Scope; + const row = pickRow( + x.row.이름, + tc(x.kind), + x.row.제목 || x.row.이름, + x.row.이름, + x.row.단위 ?? "", + ); row.addEventListener("click", () => { close(); - lib.pick({ ...at, 키: x.이름 }, x.제목 || x.이름); + lib?.pick({ ...at, 키: x.row.이름 }, x.row.제목 || x.row.이름); }); return row; }), ); }; + const loadLib = async (): Promise => { + if (!lib || !scope) return; + const mine = ++seq; + const at = scope; + loading = true; + find(); + try { + const [promote, combo] = await Promise.all([ + fetchLibrary(at, "promote"), + fetchLibrary(at, "precombo"), + ]); + if (mine !== seq) return; + libRows = [ + ...promote.map((row) => ({ kind: "Kind_Promote" as const, row })), + ...combo.map((row) => ({ kind: "Kind_Combo" as const, row })), + ]; + } catch (error) { + if (mine !== seq) return; + libRows = []; + showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error"); + } + loading = false; + find(); + }; const drawLayers = (): void => layerBar.replaceChildren( ...scopes.map((x) => @@ -175,41 +177,33 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra onClick: () => { scope = x; drawLayers(); - void libList(); + void loadLib(); }, }), ), ); - const drawTabs = (): void => { - tabs.replaceChildren( - ...(["logic", "promote", "precombo"] as const).map((k) => - createButton({ - label: tc( - k === "logic" ? "Kind_Logic" : k === "promote" ? "Kind_Promote" : "Lib_Precombo", - ), - variant: k === tab ? "filled" : "ghost", - onClick: () => { - tab = k; - drawTabs(); - filters.style.display = k === "logic" ? "" : "none"; // 공용 꼴 display 가 hidden 을 이김 - layerBar.style.display = k === "logic" ? "none" : ""; - if (k === "logic") { - list.replaceChildren(); - hint.textContent = tc("Pick_Hint"); - } else void libList(); - }, - }), - ), - ); - }; + sub.select.addEventListener("change", () => { + const d = subs.find((s) => s.name === sub.select.value)?.details ?? []; + detail.setOptions(opts(d), ""); + detail.select.disabled = d.length === 0; + find(); + }); + detail.select.addEventListener("change", find); + searchField.input.addEventListener("input", find); + + const filters = el("div", { + className: "m01-logic__filters", + children: [sub.root, detail.root, searchField.root], + }); layerBar.style.display = "none"; if (lib) void upper(lib.layer, lib.owner).then((got) => { scopes = got; scope = got[got.length - 1] ?? null; // 기본 = 가장 가까운 층 if (!got.length) return; - drawTabs(); + layerBar.style.display = ""; drawLayers(); + void loadLib(); }); const dialog = el("div", { @@ -223,7 +217,6 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra createButton({ label: tc("Pick_Close"), variant: "ghost", onClick: close }), ], }), - tabs, filters, layerBar, hint, @@ -234,13 +227,13 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra backdrop.addEventListener("click", (ev) => ev.target === backdrop && close()); backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close()); document.body.append(backdrop); - searchField.input.addEventListener("keydown", (ev) => ev.key === "Enter" && find()); searchField.input.focus(); void Promise.all([fetchLogicSubs(), fetchLogics()]) .then(([s, l]) => { subs.push(...s); all = l; + find(); sub.setOptions(opts(s.map((x) => x.name)), ""); }) .catch((error) => diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index 48ed9576c..b7c8341b5 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -45,11 +45,6 @@ const TEXT = { Open: ["열기", "Open"], Pick_Title: ["로직 고르기", "Pick a logic"], Pick_Search: ["이름·키로 찾기", "Search by name or key"], - Pick_Find: ["찾기", "Find"], - Pick_Hint: [ - "구분 → 상세구분을 고르고 [찾기]를 누름 — 하나씩 직접 고름", - "Choose category, detail, then Find — pick one by one", - ], Pick_None: ["찾은 로직 없음", "No logics found"], Pick_Close: ["닫기", "Close"], Pick_Blocked: ["막힘", "Blocked"], @@ -92,7 +87,6 @@ const TEXT = { Labor_LineApplied: ["반영(수량 곱함)", "Applied (× quantity)"], Labor_LineBase: ["기준", "Base"], Labor_FromLogic: ["원 로직", "Source logic"], - Lib_Precombo: ["사전조합", "Pre-combination"], Lib_System: ["마스터", "Master"], Lib_Company: ["회사", "Company"], Lib_Personal: ["개인", "Personal"], diff --git a/resources/tester/ui_checks/51-3_고르기_한목록.js b/resources/tester/ui_checks/51-3_고르기_한목록.js new file mode 100644 index 000000000..d1bf582bc --- /dev/null +++ b/resources/tester/ui_checks/51-3_고르기_한목록.js @@ -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); +})();