Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -5,13 +5,8 @@
|
||||
* 프로젝트 · 카드엔 서고 줄 없음
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
createSelectField,
|
||||
el,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
||||
import { createFindList } from "@ui/ui_template_find_list";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import type { LogicSummary, SubBrief } from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
||||
@@ -65,9 +60,6 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
const sub = createSelectField({ options: opts([]), compact: true, label: tc("Sub") });
|
||||
const detail = createSelectField({ options: opts([]), compact: true, label: tc("Detail") });
|
||||
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" });
|
||||
const close = (): void => backdrop.remove();
|
||||
|
||||
/* 한 목록 — 구분(단가산출 · 단가승격 · 일위조합) 칸 · 층 단추는 서고 줄(승격 · 조합)의 층 */
|
||||
@@ -77,31 +69,15 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
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 }),
|
||||
],
|
||||
});
|
||||
/* 공용 찾기 목록 — 구분 · 상세구분 2열 · 이름 왼쪽 맞춤 · ↑ ↓ Enter */
|
||||
const finder = createFindList({
|
||||
placeholder: tc("Pick_Search"),
|
||||
filters: lib?.only ? [] : [sub.root, detail.root],
|
||||
extra: [layerBar],
|
||||
});
|
||||
|
||||
const find = (): void => {
|
||||
const q = searchField.input.value.trim().toLowerCase();
|
||||
const q = finder.search.value.trim().toLowerCase();
|
||||
const found = all.filter(
|
||||
(x) =>
|
||||
(!sub.select.value || x.구분 === sub.select.value) &&
|
||||
@@ -113,38 +89,38 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
: 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 = pickRow(
|
||||
x.키,
|
||||
tc("Kind_Logic"),
|
||||
x.이름,
|
||||
`${x.키} · ${x.구분} ${x.상세구분}`.trim(),
|
||||
`${x.결과단위}${x.blocked ? ` · ${tc("Pick_Blocked")}` : ""}`,
|
||||
);
|
||||
row.addEventListener("click", () => {
|
||||
finder.hint.textContent = loading
|
||||
? tc("Loading")
|
||||
: shown
|
||||
? `${shown} / ${total}`
|
||||
: tc("Pick_None");
|
||||
finder.setRows([
|
||||
...found.slice(0, 100).map((x) => ({
|
||||
key: x.키,
|
||||
tag: tc("Kind_Logic"),
|
||||
name: x.이름,
|
||||
sub: `${x.키} · ${x.구분} ${x.상세구분}`.trim(),
|
||||
unit: `${x.결과단위}${x.blocked ? ` · ${tc("Pick_Blocked")}` : ""}`,
|
||||
onPick: () => {
|
||||
close();
|
||||
done(x);
|
||||
});
|
||||
return row;
|
||||
}),
|
||||
},
|
||||
})),
|
||||
...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.row.이름 }, x.row.제목 || x.row.이름);
|
||||
});
|
||||
return row;
|
||||
return {
|
||||
key: x.row.이름,
|
||||
tag: tc(x.kind),
|
||||
name: x.row.제목 || x.row.이름,
|
||||
sub: x.row.이름,
|
||||
unit: x.row.단위 ?? "",
|
||||
onPick: () => {
|
||||
close();
|
||||
lib?.pick({ ...at, 키: x.row.이름 }, x.row.제목 || x.row.이름);
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
]);
|
||||
};
|
||||
const loadLib = async (): Promise<void> => {
|
||||
if (!lib || !scope) return;
|
||||
@@ -191,12 +167,8 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
find();
|
||||
});
|
||||
detail.select.addEventListener("change", find);
|
||||
searchField.input.addEventListener("input", find);
|
||||
finder.search.addEventListener("input", find);
|
||||
|
||||
const filters = el("div", {
|
||||
className: "m01-logic__filters",
|
||||
children: [...(lib?.only ? [] : [sub.root, detail.root]), searchField.root],
|
||||
});
|
||||
layerBar.style.display = "none";
|
||||
if (lib)
|
||||
void upper(lib.layer, lib.owner).then((got) => {
|
||||
@@ -219,17 +191,14 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
createButton({ label: tc("Pick_Close"), variant: "ghost", onClick: close }),
|
||||
],
|
||||
}),
|
||||
filters,
|
||||
layerBar,
|
||||
hint,
|
||||
list,
|
||||
finder.root,
|
||||
],
|
||||
});
|
||||
const backdrop = el("div", { className: "m01-logic__backdrop", children: [dialog] });
|
||||
backdrop.addEventListener("click", (ev) => ev.target === backdrop && close());
|
||||
backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close());
|
||||
document.body.append(backdrop);
|
||||
searchField.input.focus();
|
||||
finder.search.focus();
|
||||
|
||||
if (lib?.only) return;
|
||||
void Promise.all([fetchLogicSubs(), fetchLogics()])
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/* 51-9 — [로직 추가] 고르기 창이 공용 찾기 양식(ui_template_find_list)을 씀
|
||||
* 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 · item1 있음
|
||||
* 부르는 법: orca eval --expression "$(tr '
|
||||
' ' ' < 이 파일)" (async 식 · JSON 글을 돌려줌)
|
||||
* 하는 일: item1 → 일위대가 [수정] → [로직 추가] → 칸 배치 · 줄 맞춤 · ↓ Enter 점검 → 창 닫음(저장 안 함)
|
||||
* 기대 수치:
|
||||
* - template = true · filters = 2 · sameLine = true(구분 · 상세구분 top 같음 · left 다름)
|
||||
* - nameAlign = "left" · nameLeftGap ≤ 꼬리표 폭 + 16 · rowHeight ≤ 40 · oneLine = true
|
||||
* - active0 = 0 · activeAfterDown = 1 · enterPicked = true(Enter 로 창이 닫힘) · undone = 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 out = {};
|
||||
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;
|
||||
const openPick = async () => {
|
||||
[...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.querySelectorAll(".ui-find__row").length > 1, 20000);
|
||||
return dlg;
|
||||
};
|
||||
const dlg = await openPick();
|
||||
out.template = !!dlg.querySelector(".ui-find .ui-find__filters") && !!dlg.querySelector(".ui-find__list");
|
||||
const sels = [...dlg.querySelectorAll(".ui-find__filters > *")];
|
||||
out.filters = sels.length;
|
||||
const [a, b] = sels.map((x) => x.getBoundingClientRect());
|
||||
out.sameLine = Math.abs(a.top - b.top) < 2 && b.left > a.right - 1;
|
||||
const row = dlg.querySelector(".ui-find__row");
|
||||
const name = row.querySelector(".ui-find__name");
|
||||
const tag = row.querySelector(".ui-find__tag");
|
||||
out.nameAlign = getComputedStyle(name).textAlign;
|
||||
out.rowAlign = getComputedStyle(row).textAlign;
|
||||
out.nameLeftGap = Math.round(name.getBoundingClientRect().left - row.getBoundingClientRect().left);
|
||||
out.tagWidth = Math.round(tag.getBoundingClientRect().width);
|
||||
out.rowHeight = Math.round(row.getBoundingClientRect().height);
|
||||
out.oneLine = getComputedStyle(row).whiteSpace === "nowrap";
|
||||
const rows = () => [...dlg.querySelectorAll(".ui-find__row")];
|
||||
const active = () => rows().findIndex((r) => r.classList.contains("is-active"));
|
||||
out.active0 = active();
|
||||
const search = dlg.querySelector('input[type="search"]');
|
||||
search.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
|
||||
out.activeAfterDown = active();
|
||||
out.rows = rows().length;
|
||||
/* Enter = 고름 — 창이 닫히고 표에 붙은 줄은 바로 지움 */
|
||||
const pickedKey = rows()[active()].dataset.pick;
|
||||
search.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
out.enterPicked = !!(await until(() => !document.querySelector(".m01-logic__pick") || null));
|
||||
out.pickedKey = pickedKey;
|
||||
await until(() => table().querySelectorAll("tbody tr[data-labor-logic]").length > before, 8000);
|
||||
const added = [...table().querySelectorAll("tbody tr[data-labor-logic]")].slice(before);
|
||||
for (const tr of added) {
|
||||
[...tr.querySelectorAll("button")].find((x) => x.innerText.trim() === "삭제")?.click();
|
||||
await w(600);
|
||||
}
|
||||
out.undone = table().querySelectorAll("tbody tr[data-labor-logic]").length === before;
|
||||
document.querySelector('[data-node="i|item1"]')?.click();
|
||||
await w(800);
|
||||
const ask = [...document.querySelectorAll(".ui-modal button, dialog button")].find((x) =>
|
||||
/확인|버림|떠나기/.test(x.innerText),
|
||||
);
|
||||
ask?.click();
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
@@ -0,0 +1,73 @@
|
||||
/* 공용 찾기 목록(ui_template_find_list.ts) — 거름 2열 · 결과 한 줄 · 이름 왼쪽 맞춤 */
|
||||
.ui-find {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ui-find__filters {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.ui-find__hint {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.ui-find__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ui-find__row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--spacing-8);
|
||||
width: 100%;
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: none;
|
||||
color: var(--color-text-body);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui-find__row.is-active {
|
||||
background: var(--color-mist-violet);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.ui-find__tag,
|
||||
.ui-find__sub,
|
||||
.ui-find__unit {
|
||||
flex: none;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.ui-find__tag {
|
||||
min-width: 4.5em;
|
||||
}
|
||||
|
||||
.ui-find__name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ui-find__sub {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/* =============================================================================
|
||||
* ui_template_find_list.ts
|
||||
* 공용 찾기 목록 — 걸러내기 고르기 칸(2열 격자) · 찾기 칸 · 알림 줄 · 결과 목록(이름 왼쪽 맞춤 · 한 줄).
|
||||
* 키보드 — 찾기 칸에서 ↑ ↓ 로 옮기고 Enter 로 고름. 창 겉틀은 부르는 쪽 몫.
|
||||
*
|
||||
* const find = createFindList({ placeholder, filters: [sub.root, detail.root] });
|
||||
* find.setRows([{ key, tag, name, sub, unit, onPick }]);
|
||||
* ========================================================================== */
|
||||
|
||||
import "./ui_template_find_list.css";
|
||||
import { createInputField, el } from "./ui_template_elements";
|
||||
|
||||
export interface FindRow {
|
||||
/** 줄 표지(`data-pick`) */
|
||||
key: string;
|
||||
name: string;
|
||||
/** 앞 흐린 꼬리표(종류 등) */
|
||||
tag?: string;
|
||||
/** 이름 뒤 흐린 보조 글(키 · 구분 등) */
|
||||
sub?: string;
|
||||
/** 끝 흐린 글(단위 등) */
|
||||
unit?: string;
|
||||
onPick: () => void;
|
||||
}
|
||||
|
||||
export interface FindList {
|
||||
root: HTMLElement;
|
||||
search: HTMLInputElement;
|
||||
hint: HTMLElement;
|
||||
setRows: (rows: FindRow[]) => void;
|
||||
}
|
||||
|
||||
export function createFindList(opts: {
|
||||
placeholder: string;
|
||||
filters?: HTMLElement[];
|
||||
/** 찾기 칸과 알림 줄 사이(층 단추 등) */
|
||||
extra?: HTMLElement[];
|
||||
}): FindList {
|
||||
const field = createInputField({ type: "search", placeholder: opts.placeholder });
|
||||
const hint = el("p", { className: "ui-find__hint" });
|
||||
const list = el("div", { className: "ui-find__list", attrs: { role: "listbox" } });
|
||||
let rows: FindRow[] = [];
|
||||
let at = -1;
|
||||
|
||||
const mark = (i: number): void => {
|
||||
list.children[at]?.classList.remove("is-active");
|
||||
at = i;
|
||||
const row = list.children[at];
|
||||
row?.classList.add("is-active");
|
||||
row?.scrollIntoView({ block: "nearest" });
|
||||
};
|
||||
field.input.addEventListener("keydown", (ev) => {
|
||||
if (ev.key === "ArrowDown" || ev.key === "ArrowUp") {
|
||||
ev.preventDefault();
|
||||
if (!rows.length) return;
|
||||
const step = ev.key === "ArrowDown" ? 1 : -1;
|
||||
mark((at + step + rows.length) % rows.length);
|
||||
} else if (ev.key === "Enter" && rows[at]) {
|
||||
ev.preventDefault();
|
||||
rows[at].onPick();
|
||||
}
|
||||
});
|
||||
|
||||
const setRows = (next: FindRow[]): void => {
|
||||
rows = next;
|
||||
at = -1;
|
||||
list.replaceChildren(
|
||||
...rows.map((r, i) => {
|
||||
const row = el("button", {
|
||||
className: "ui-find__row",
|
||||
attrs: { type: "button", role: "option", "data-pick": r.key, title: r.name },
|
||||
children: [
|
||||
...(r.tag
|
||||
? [
|
||||
el("span", {
|
||||
className: "ui-find__tag",
|
||||
attrs: { "data-pick-kind": "" },
|
||||
text: r.tag,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
el("span", { className: "ui-find__name", text: r.name }),
|
||||
el("span", { className: "ui-find__sub", text: r.sub ?? "" }),
|
||||
el("span", { className: "ui-find__unit", text: r.unit ?? "" }),
|
||||
],
|
||||
});
|
||||
row.addEventListener("click", r.onPick);
|
||||
row.addEventListener("mousemove", () => at !== i && mark(i));
|
||||
return row;
|
||||
}),
|
||||
);
|
||||
if (rows.length) mark(0);
|
||||
};
|
||||
|
||||
const filters = opts.filters?.length
|
||||
? [el("div", { className: "ui-find__filters", children: opts.filters })]
|
||||
: [];
|
||||
const root = el("div", {
|
||||
className: "ui-find",
|
||||
children: [...filters, field.root, ...(opts.extra ?? []), hint, list],
|
||||
});
|
||||
return { root, search: field.input, hint, setRows };
|
||||
}
|
||||
Reference in New Issue
Block a user