Merge remote-tracking branch 'origin/sub_laptop_3' into sub_laptop_1
This commit is contained in:
@@ -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<void> => {
|
||||
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<void> => {
|
||||
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) =>
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
Reference in New Issue
Block a user