- 표지 = 기계 줄 맨 위 칸 `비활성: true`(없음 = 켜짐 · 값 묶음 밖) · 틀 검사 = 기계 줄에만 true(check_master OFF) - M01 기계 목록 = 끝 열 「사용」 스위치(초안 → 페이지 [저장] · 자동저장 없음) · 끈 줄 흐림 · 머리 「비활성 같이 보기」(끄면 서버 rows state 「켜짐」) - 뺌 = 요소 찾기 창(elements · 찾아 넣기 · 단가산출 기계 창) · logic-lib rows · M01 시험 계산 기계 고르기 표 · 내 기계 「비슷한 기계에서 시작」 · 불러오기 기계 잇기(M02_Machine_Find 표) · 기계경비식 「기계」 옵션(새 GET /api/m02/df/machine-off · 고른 값은 남기고 「· 비활성 기계」 꼬리) - 셈 길은 그대로 — 끈 기계를 쓰던 단가산출 · 사본 값 같음 - 자료 = 해상장비 79 대(펌프 준설선 13 · 그래브 준설선 10 · 하천골재채취선 7 · 기중기선 5 · 예선 9 · 양묘선 11 · 토운선 6 · 이우선 2 · 대선 16) 끔 - 시험 = test_842_2_machine_off 새로 · test_804_10 옵션 글 단언을 새 글로 고침(끈 기계 거름이 들어간 줄) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
5.2 KiB
TypeScript
148 lines
5.2 KiB
TypeScript
/* =============================================================================
|
|
* M01_MasterData_UI_Logic_PickTable.ts
|
|
* 시험 계산의 재료 · 기계 고르기 모달 — 마스터 컨테이너와 같은 표(`GET /rows`) · 쪽 나눔 · 칸 글
|
|
* 검색 칸을 채워 열고 · 잡힌 품목 칠함 · 한 줄 누르면 고름 · 저장 안 함(읽기만)
|
|
* ========================================================================== */
|
|
|
|
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";
|
|
|
|
export type PickTableKind = "재료" | "기계";
|
|
|
|
const FILES: Record<PickTableKind, string> = { 재료: "재료_자재품목.json", 기계: "기계.json" };
|
|
/** 표에 보일 칸 — 컨테이너 표의 칸 이름(묶음 값은 「값›가격」) */
|
|
const COLS: Record<PickTableKind, string[]> = {
|
|
재료: [
|
|
"키",
|
|
"구분",
|
|
"상세구분",
|
|
"이름",
|
|
"규격",
|
|
"단위",
|
|
"물가자료",
|
|
"유통물가",
|
|
"물가정보",
|
|
"거래가격",
|
|
"일반가격",
|
|
],
|
|
기계: ["원문번호", "구분", "상세구분", "이름", "규격", "값›가격"],
|
|
};
|
|
/** 모달 안 표 칸 + 쪽 단추가 쓸 높이 — 모달 최대 높이(80vh)에서 제목 · 찾기 줄을 뺌 */
|
|
const room = (): number => window.innerHeight * 0.8 - 130;
|
|
|
|
export interface PickTableOptions {
|
|
kind: PickTableKind;
|
|
title: string;
|
|
/** 검색 칸을 채워 엶 — 결과가 없으면 첫 낱말만으로 다시 · 그래도 없으면 `alt` 를 차례로 */
|
|
seed: string;
|
|
alt?: string[];
|
|
/** 지금 잡힌 줄이면 true — 그 줄을 칠함 */
|
|
isCurrent: (row: Row) => boolean;
|
|
onPick: (row: Row) => void;
|
|
}
|
|
|
|
export function openPickTable(opt: PickTableOptions): void {
|
|
const searchField = createInputField({
|
|
type: "search",
|
|
placeholder: tx("Pick_Search"),
|
|
value: opt.seed,
|
|
});
|
|
const search = searchField.input;
|
|
const host = el("div", { className: "m01-logic__pick-list" });
|
|
// 모달 부품 — 제목줄 [닫기] · 바깥 누르기 · Esc · 위 고정 · 폭만 표 모달 곁값
|
|
const { close } = openModal({
|
|
title: opt.title,
|
|
closeLabel: tx("Pick_Close"),
|
|
dialogClass: "m01-logic__pick m01-logic__pick--table",
|
|
backdropClass: "m01-logic__backdrop",
|
|
mount: (body) => body.append(searchField.root, host),
|
|
});
|
|
|
|
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 on = opt.kind === "기계" ? "켜짐" : ""; // 끈 기계 뺌(842-2)
|
|
const got = await fetchRows(FILES[opt.kind], page, size, q, false, "", "", on);
|
|
if (mine !== seq) return; // 최신 응답만
|
|
if (!got.total && fallback) {
|
|
const next = [...(q.includes(" ") ? [q.split(/\s+/)[0]] : []), ...(opt.alt ?? [])].find(
|
|
(w) => w && !tried.has(w),
|
|
);
|
|
if (next) {
|
|
tried.add(next);
|
|
search.value = next;
|
|
return load(true);
|
|
}
|
|
}
|
|
const cols = COLS[opt.kind];
|
|
const body = got.rows.map((row) => {
|
|
const flat = flatten(row);
|
|
const tr = el("tr", {
|
|
className: `m01lab__pickrow${opt.isCurrent(row) ? " is-picked" : ""}`,
|
|
children: cols.map((c) =>
|
|
el("td", { text: show(flat[c]), attrs: { title: show(flat[c]) } }),
|
|
),
|
|
});
|
|
tr.addEventListener("click", () => {
|
|
close();
|
|
opt.onPick(row);
|
|
});
|
|
return tr;
|
|
});
|
|
host.replaceChildren(
|
|
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: cols.map((c) => el("th", { text: c })) })],
|
|
}),
|
|
el("tbody", { children: body }),
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
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);
|
|
timer = window.setTimeout(() => {
|
|
page = 1;
|
|
void load();
|
|
}, 300);
|
|
});
|
|
void load(true);
|
|
}
|