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); +})();