Merge remote-tracking branch 'origin/sub_laptop_1' into sub_laptop_7

This commit is contained in:
2026-11-01 15:31:58 +09:00
12 changed files with 265 additions and 107 deletions
+19 -6
View File
@@ -8,6 +8,7 @@
import { createInputField, el } from "@ui/ui_template_elements";
import { aliasText, matchLogic } from "@ui/ui_template_logic_find";
import { buildPager } from "./M01_MasterData_UI_Cells";
import { pageKeepFirst, watchFit } from "@ui/ui_template_pager";
import type { LogicSummary } from "./M01_MasterData_UI_Logic_Api";
import { tx } from "./M01_MasterData_UI_Logic_Text";
import "./M01_MasterData_UI_Head.css";
@@ -41,7 +42,7 @@ export interface ListHandle {
export const logicId = (sub: string, key: string): string => `${sub}\n${key}`;
const SIZE = 30;
const DEFAULT_SIZE = 30; // 붙기 전 어림 — 붙은 뒤 화면 높이로 다시 셈
const COLS = [
"키",
"원문번호",
@@ -64,6 +65,7 @@ export function buildList(
let aliases = new Map<string, string>();
const aliasOf = (x: ListItem): string => aliases.get(x.id) ?? x.alias;
let page = 1;
let size = DEFAULT_SIZE;
const pick = { sub: "", detail: "" };
const searchField = createInputField({ type: "search", placeholder: tx("List_Search") });
const search = searchField.input;
@@ -84,9 +86,9 @@ export function buildList(
별칭: aliasOf(x),
}).ok,
);
page = Math.min(page, Math.max(1, Math.ceil(shown.length / SIZE)));
const rows = shown.slice((page - 1) * SIZE, page * SIZE).map(line);
pager.replaceWith((pager = buildPager(shown.length, SIZE, page, go)));
page = Math.min(page, Math.max(1, Math.ceil(shown.length / size)));
const rows = shown.slice((page - 1) * size, page * size).map(line);
pager.replaceWith((pager = buildPager(shown.length, size, page, go)));
grid.replaceChildren(
el("table", {
className: "ui-table m01-master__grid",
@@ -144,18 +146,29 @@ export function buildList(
};
search.addEventListener("input", () => go(1));
let pager = buildPager(0, SIZE, 1, go);
let pager = buildPager(0, size, 1, go);
const root = el("div", {
className: "m01-logic__listview",
children: [
el("div", {
className: "m01-head__filters",
children: [searchField.root, pager],
children: [searchField.root],
}),
grid,
pager,
],
});
watchFit(
root,
() => grid,
(rows) => {
if (rows === size) return;
page = pageKeepFirst(page, size, rows);
size = rows;
draw();
},
);
return {
root,
setItems: (logics) => {
@@ -7,6 +7,7 @@
import { createInputField, el, showToast } from "@ui/ui_template_elements";
import { fetchRows, type Row } from "./M01_MasterData_Api_Fetch";
import { buildPager, flatten, show } from "./M01_MasterData_UI_Cells";
import { fitRows, PAGER_ROOM, pageKeepFirst, watchFit } from "@ui/ui_template_pager";
import { tx } from "./M01_MasterData_UI_Logic_Text";
import { openModal } from "@ui/ui_template_modal";
@@ -29,7 +30,8 @@ const COLS: Record<PickTableKind, string[]> = {
],
기계: ["원문번호", "구분", "상세구분", "이름", "규격", "값›가격"],
};
const SIZE = 30;
/** 모달 안 표 칸 + 쪽 단추가 쓸 높이 — 모달 최대 높이(80vh)에서 제목 · 찾기 줄을 뺌 */
const room = (): number => window.innerHeight * 0.8 - 130;
export interface PickTableOptions {
kind: PickTableKind;
@@ -60,13 +62,14 @@ export function openPickTable(opt: PickTableOptions): void {
});
let page = 1;
let size = fitRows(room() - PAGER_ROOM); // 처음 어림 — 붙은 뒤 다시 재서 어긋나면 바로잡음
const tried = new Set([opt.seed]);
let seq = 0;
const load = async (fallback = false): Promise<void> => {
const mine = ++seq;
try {
const q = search.value.trim();
const got = await fetchRows(FILES[opt.kind], page, SIZE, q);
const got = await fetchRows(FILES[opt.kind], page, size, q);
if (mine !== seq) return; // 최신 응답만
if (!got.total && fallback) {
const next = [...(q.includes(" ") ? [q.split(/\s+/)[0]] : []), ...(opt.alt ?? [])].find(
@@ -94,10 +97,6 @@ export function openPickTable(opt: PickTableOptions): void {
return tr;
});
host.replaceChildren(
buildPager(got.total, SIZE, got.page, (next) => {
page = next;
void load();
}),
el("div", {
className: "ui-table-wrap m01-master__grid-wrap",
children: [
@@ -112,12 +111,28 @@ export function openPickTable(opt: PickTableOptions): void {
}),
],
}),
buildPager(got.total, size, got.page, (next) => {
page = next;
void load();
}),
);
fit.ask();
host.querySelector(".is-picked")?.scrollIntoView({ block: "nearest" });
} catch (error) {
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
}
};
const fit = watchFit(
host,
() => host.querySelector<HTMLElement>(".m01-master__grid-wrap"),
(rows) => {
if (rows === size) return;
page = pageKeepFirst(page, size, rows);
size = rows;
void load();
},
{ cap: room, once: true },
);
let timer = 0;
search.addEventListener("input", () => {
window.clearTimeout(timer);
+22 -4
View File
@@ -29,8 +29,10 @@ import {
withCell,
} from "./M01_MasterData_UI_Cells";
import { createEmpty } from "@ui/ui_template_empty";
import { guessRows, pageKeepFirst, watchFit } from "@ui/ui_template_pager";
const SIZE = 50;
/** 표 칸 위에 오는 도구줄(단추 줄 + 간격) 높이 — 첫 줄 수 어림에만 씀 */
const BAR_H = 52;
const MARKET = "재료_자재품목.json";
const LINKED = "재료_품셈재료.json";
const JOB = ["인력.json"];
@@ -94,6 +96,7 @@ export function renderRows(host: HTMLElement, pick: Pick, q: string): () => void
const { sub, detail, state } = pick;
const file = pick.file.file;
let page = pick.view?.page ?? 1;
let size = guessRows(host, BAR_H); // 화면 높이로 셈 — 그린 뒤 다시 재서 어긋나면 바로잡음
let data: RowsPage | null = null;
let unlinked = false;
@@ -210,7 +213,7 @@ export function renderRows(host: HTMLElement, pick: Pick, q: string): () => void
host.replaceChildren(
el("div", {
className: "ui-toolbar m01-master__bar",
children: [add, ...only, buildPager(d.total, SIZE, d.page, go)],
children: [add, ...only],
}),
el("div", {
className: "ui-table-wrap m01-master__grid-wrap",
@@ -221,8 +224,10 @@ export function renderRows(host: HTMLElement, pick: Pick, q: string): () => void
}),
],
}),
buildPager(d.total, size, d.page, go),
...empty,
);
fit.ask();
};
const go = (next: number): void => {
@@ -232,7 +237,7 @@ export function renderRows(host: HTMLElement, pick: Pick, q: string): () => void
const load = async (): Promise<void> => {
try {
data = await fetchRows(file, page, SIZE, q, unlinked, sub, detail, state);
data = await fetchRows(file, page, size, q, unlinked, sub, detail, state);
for (const [key, name] of Object.entries(data.refs ?? {})) names.set(key, name);
saveView(pick.group, { page: data.page });
paint();
@@ -241,9 +246,22 @@ export function renderRows(host: HTMLElement, pick: Pick, q: string): () => void
}
};
const fit = watchFit(
host,
() => host.querySelector<HTMLElement>(".m01-master__grid-wrap"),
(rows) => {
if (rows === size) return;
page = pageKeepFirst(data?.page ?? page, size, rows);
size = rows;
void load();
},
);
const off = onDraftChange(paint);
void load();
return off;
return () => {
fit.stop();
off();
};
}
function actionCell(label: string, onClick: () => void): HTMLTableCellElement {
+6 -10
View File
@@ -84,9 +84,9 @@ html.is-bm-route .m01-master__headbox {
min-width: 0;
}
/* 겉 = 표 부품(ui-table-wrap · ui-table) · 여기는 높이 · 폭 · 머리 고정 · 칸 상태만 */
.m01-master__grid-wrap {
max-height: calc(100dvh - 300px);
/* 겉 = 표 부품(ui-table-wrap · ui-table) · 여기는 높이 · 폭 · 머리 고정 · 칸 상태만
쪽 있는 표 = 높이 제한 없음(줄 수를 화면 높이로 셈 · 쪽 단추 부품 watchFit) · 쪽 없는 한 장 표(--sheet) = 화면 아래까지 안에서 스크롤 */
.m01-master__grid-wrap--sheet {
overflow: auto;
}
@@ -130,13 +130,9 @@ table.m01-master__grid {
padding: 2px var(--spacing-4);
}
.m01-master__pager {
display: flex;
align-items: center;
gap: var(--spacing-12);
margin-left: auto;
color: var(--color-text-secondary);
font-size: var(--text-body-sm);
/* 쪽 단추(부품 ui-pager) — 간격은 부품이 줌(표 아래 한 칸) · 이미 칸 사이 간격이 있는 몸통은 겹치지 않게 뺌 */
.m01-master__body > .ui-table-wrap + .ui-pager {
margin-top: 0;
}
.m01-master__table-row {
+52 -28
View File
@@ -30,8 +30,10 @@ import {
toNumberish,
toRange,
} from "./M01_MasterData_UI_Cells";
import { guessRows, pageKeepFirst, watchFit, watchHeight } from "@ui/ui_template_pager";
const LIST_SIZE = 30;
/** 표 칸 위에 오는 거름 줄 높이 — 첫 줄 수 어림에만 씀 */
const FILTER_H = 52;
const LIST_COLS = ["키", "원문번호", "구분", "상세구분", "이름", "기준", "출처", "비고"] as const;
/** 용도 대상 거름 드롭다운 후보 — PLAN 1-1 셋째 줄 그대로. */
const USAGE_TARGETS = ["인력", "기계", "재료", "할증", "환산", "작업량 조건"];
@@ -45,16 +47,22 @@ export function renderTables(
): () => void {
let stop = (): void => {};
let usage = pick.view?.usage ?? "";
let size = guessRows(host, FILTER_H); // 화면 높이로 셈 — 그린 뒤 다시 재서 어긋나면 바로잡음
let at = 0; // 목록을 보고 있는 쪽(0 = 표 하나를 보는 중)
const list = (page = pick.view?.page ?? 1): void => {
stop();
stop = (): void => {};
at = page;
void showList(
host,
pick,
q,
usage,
page,
size,
list,
(file, key) => {
at = 0;
stop = openTable(host, file, key, () => list(page), openLogic);
},
(next) => {
@@ -62,11 +70,22 @@ export function renderTables(
saveView(pick.group, { usage, page: 1 });
list(1);
},
);
).then(() => fit.ask());
};
const fit = watchFit(
host,
() => host.querySelector<HTMLElement>(".m01-master__tables"),
(rows) => {
if (!at || rows === size) return;
const page = pageKeepFirst(at, size, rows);
size = rows;
list(page);
},
);
list();
const off = onDraftChange(() => host.querySelector(".m01-master__tables") && markChanged(host));
return () => {
fit.stop();
stop();
off();
};
@@ -86,12 +105,14 @@ async function showList(
q: string,
usage: string,
page: number,
size: number,
goPage: (page: number) => void,
open: (file: string, key: string) => void,
onUsageChange: (usage: string) => void,
): Promise<void> {
let got: { total: number; tables: TableHead[] };
try {
got = await fetchTables(pick.group, pick.sub, pick.detail, q, page, LIST_SIZE, usage);
got = await fetchTables(pick.group, pick.sub, pick.detail, q, page, size, usage);
} catch (error) {
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error");
return;
@@ -121,12 +142,6 @@ async function showList(
usageField.select.addEventListener("change", () => onUsageChange(usageField.select.value));
host.replaceChildren(
el("div", { className: "m01-master__filter", children: [usageField.root] }),
buildPager(
got.total,
LIST_SIZE,
page,
(p) => void showList(host, pick, q, usage, p, open, onUsageChange),
),
el("div", {
className: "ui-table-wrap m01-master__grid-wrap m01-master__tables",
children: [
@@ -141,6 +156,7 @@ async function showList(
}),
],
}),
buildPager(got.total, size, page, goPage),
);
}
@@ -153,6 +169,7 @@ function openTable(
openLogic: (key: string) => void,
): () => void {
let off = (): void => {};
let unfit = (): void => {}; // 쪽 없는 한 장 표 — 화면 아래까지 높이 묶음
void (async () => {
let version: string;
let table: Row & { 줄: Row[] };
@@ -211,6 +228,7 @@ function openTable(
const backButton = createButton({ label: L("M01_BackToTables"), variant: "ghost" });
backButton.addEventListener("click", () => {
off();
unfit();
back();
});
const notes = ((table.주 ?? []) as string[]).map((n) =>
@@ -224,6 +242,24 @@ function openTable(
btn.addEventListener("click", () => openLogic(k));
return i === 0 ? btn : el("span", { text: ", ", children: [btn] });
});
const sheet = el("div", {
className: "ui-table-wrap m01-master__grid-wrap m01-master__grid-wrap--sheet",
children: [
el("table", {
className: "ui-table m01-master__grid",
children: [
el("thead", {
children: [
el("tr", {
children: [el("th"), ...cols.map((c) => el("th", { text: headText(c) }))],
}),
],
}),
body,
],
}),
],
});
host.replaceChildren(
el("div", {
className: "ui-toolbar m01-master__bar",
@@ -246,29 +282,17 @@ function openTable(
add,
],
}),
el("div", {
className: "ui-table-wrap m01-master__grid-wrap",
children: [
el("table", {
className: "ui-table m01-master__grid",
children: [
el("thead", {
children: [
el("tr", {
children: [el("th"), ...cols.map((c) => el("th", { text: headText(c) }))],
}),
],
}),
body,
],
}),
],
}),
sheet,
...notes,
);
unfit();
unfit = watchHeight(sheet);
};
off = onDraftChange(paint);
paint();
})();
return () => off();
return () => {
off();
unfit();
};
}