Files
Aislo/M01_MasterData/M01_MasterData_UI_Machine.ts
T
eomsangdonandClaude Opus 5.5 6015525efd feat(842-2): 기계 비활성 — M01 기계 목록 「사용」 스위치 · 끈 기계는 찾기 · 고르기 · 잇기에서 뺌 · 해상 기계 79 대 끔
- 표지 = 기계 줄 맨 위 칸 `비활성: 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>
2026-11-15 18:29:05 +09:00

223 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* M01_MasterData_UI_Machine.ts
* M01 기계 목록(PLAN 821-2 · 요구 790-2) — 표는 목록용 열만(이름 · 규격 · 구분 · 가격 · 손료계수 ·
* 주연료 · 조종원 · 시간당 경비(서버 값) · 별칭 · 비고) · 줄을 누르면 상세 페이지 · [새 기계] = 양식.
* 단위 · 출처 열 없음 · 고친 것 · 새 줄 · 지울 줄은 초안(노랑 · 새 · 지움 표시) — 저장은 페이지 [저장].
* 「사용」 열(842-2) = 스위치 · 끄면 줄 맨 위 칸 `비활성: true`(초안 → [저장]) · 끈 줄은 흐림 ·
* 머리 「비활성 같이 보기」 끄면 서버가 끈 줄을 뺌(`rows` state 「켜짐」).
* ========================================================================== */
import { createButton, el, showToast } from "@ui/ui_template_elements";
import { t as L } from "@ui/ui_template_locale";
import { createEmpty } from "@ui/ui_template_empty";
import { createToggle } from "@ui/ui_template_toggle";
import { guessRows, pageKeepFirst, watchFit } from "@ui/ui_template_pager";
import { fetchRows, type Row, type RowsPage } from "./M01_MasterData_Api_Fetch";
import { editRow, onDraftChange, peek, same } from "./M01_MasterData_Draft";
import { fetchMachineHourly } from "./M01_MasterData_Machine_Api";
import { buildPager } from "./M01_MasterData_UI_Cells";
import { saveView, type Pick } from "./M01_MasterData_UI_Side";
const BAR_H = 52;
const num = (v: unknown): string =>
typeof v === "number" ? v.toLocaleString("ko-KR", { maximumFractionDigits: 4 }) : "";
/** 목록 열 — 이름 · 줄 → 글 */
const COLS: [string, (r: Row) => string][] = [
["이름", (r) => String(r["이름"] ?? "")],
["규격", (r) => String(r["규격"] ?? "")],
["구분", (r) => [r["구분"], r["상세구분"]].filter(Boolean).join(" · ")],
["가격(천원)", (r) => num(val(r)["가격"])],
["손료계수", (r) => num(val(r)["손료계수"])],
[
"주연료",
(r) =>
val(r)["주연료"] ? `${val(r)["주연료종류"] ?? ""} ${num(val(r)["주연료"])}ℓ`.trim() : "",
],
[
"조종원",
(r) =>
val(r)["조종원"] ? `${num(val(r)["조종원"])}인 ${val(r)["조종원직종"] ?? ""}`.trim() : "",
],
];
const val = (r: Row): Record<string, unknown> => (r["값"] ?? {}) as Record<string, unknown>;
const OFF = "비활성"; // 842-2 기계 줄 맨 위 칸 — true 만 · 켜짐 = 칸 없음
let showOff = true; // 「비활성 같이 보기」(페이지 안에서 기억)
/** 돌려받은 함수 = 걷기. `open(target)` = 상세(키 · 새 줄 `add:<차례>` · 새 기계 `new`). */
export function renderMachines(
host: HTMLElement,
pick: Pick,
q: string,
open: (target: string) => void,
): () => void {
const file = pick.file.file;
let size = guessRows(host, BAR_H);
let page = pick.view?.page ?? 1;
let data: RowsPage | null = null;
const hourly = new Map<string, string>();
const hourText = (key: string): string => hourly.get(key) ?? "…";
/** 「사용」 칸 — 스위치(새 줄은 빈 칸) · 바꾸면 초안 */
const useCell = (row: Row, cur: Row, key: string): HTMLElement => {
const td = el("td", { className: "m01-machine__use" });
if (!key || !data) return td;
const version = data.version;
const sw = createToggle({
label: "",
size: 20,
checked: !cur[OFF],
onChange: (on) => {
const next: Row = { ...cur };
if (on) delete next[OFF];
else next[OFF] = true;
editRow(file, version, key, row, next);
},
});
sw.root.setAttribute("aria-label", L("M01_MachineUse"));
sw.root.addEventListener("click", (e) => e.stopPropagation());
td.classList.toggle("is-changed", !!cur[OFF] !== !!row[OFF]);
td.append(sw.root);
return td;
};
const line = (row: Row, cur: Row, cls: string, target: string, key = ""): HTMLElement => {
const tr = el("tr", {
className: `m01-machine__row ${cls} ${cur[OFF] ? "is-off" : ""}`.trim(),
attrs: { "data-key": key || target },
children: [
...COLS.map(([, get]) => {
const text = get(cur);
const td = el("td", { text, attrs: { title: text } });
td.classList.toggle("is-changed", cls !== "is-new" && text !== get(row));
return td;
}),
el("td", {
className: "m01-machine__money",
text: key ? hourText(key) : "",
attrs: key ? { "data-hourly": key } : {},
}),
el("td", { text: String(cur["별칭"] ?? ""), attrs: { title: String(cur["별칭"] ?? "") } }),
el("td", { text: String(cur["비고"] ?? ""), attrs: { title: String(cur["비고"] ?? "") } }),
useCell(row, cur, key),
],
});
tr.addEventListener("click", () => open(target));
return tr;
};
const paint = (): void => {
if (!data) return;
const d = data;
const draft = peek(file);
const body = el("tbody");
(draft?.adds ?? []).forEach((r, i) => body.append(line(r, r, "is-new", `add:${i}`)));
for (const row of d.rows) {
const key = String(row["키"]);
const cur = draft?.edits[key] ?? row;
const cls = draft?.deletes.includes(key) ? "is-deleted" : same(cur, row) ? "" : "is-changed";
body.append(line(row, cur, cls, key, key));
}
const add = createButton({ label: L("M01_MachineNew"), variant: "ghost", iconStart: "plus" });
add.addEventListener("click", () => open("new"));
const offSwitch = createToggle({
label: L("M01_MachineShowOff"),
checked: showOff,
onChange: (on) => {
showOff = on;
page = 1;
void load();
},
});
const empty =
d.rows.length || draft?.adds.length
? []
: [createEmpty({ text: L("M01_NoRows"), small: true })];
host.replaceChildren(
el("div", { className: "ui-toolbar m01-master__bar", children: [add, offSwitch.root] }),
el("div", {
className: "ui-table-wrap m01-master__grid-wrap m01-machine__wrap",
children: [
el("table", {
className: "ui-table m01-master__grid m01-machine__grid",
children: [
el("thead", {
children: [
el("tr", {
children: [
...COLS.map(([h]) => el("th", { text: h })),
el("th", { text: `${L("M01_MachineHourly")}(원)` }),
el("th", { text: "별칭" }),
el("th", { text: "비고" }),
el("th", { className: "m01-machine__use", text: L("M01_MachineUse") }),
],
}),
],
}),
body,
],
}),
],
}),
buildPager(d.total, size, d.page, go),
...empty,
);
fit.ask();
};
/** 시간당 경비 — 표를 그린 뒤 서버 값으로 채움(마스터 판마다 서버가 한 번 셈) */
const fillHourly = async (keys: string[]): Promise<void> => {
const want = keys.filter((k) => !hourly.has(k));
if (!want.length) return;
try {
const got = await fetchMachineHourly(want);
for (const [k, h] of Object.entries(got))
hourly.set(k, h.계 === undefined ? (h.reason ?? "—") : num(h.계));
} catch {
for (const k of want) hourly.set(k, "—");
}
host.querySelectorAll<HTMLElement>("td[data-hourly]").forEach((td) => {
const text = hourly.get(td.dataset.hourly!);
if (text) td.textContent = text;
});
};
const go = (next: number): void => {
page = next;
void load();
};
let ticket = 0; // 늦게 온 옛 응답이 새 목록을 덮지 않게(829-1)
const load = async (): Promise<void> => {
const mine = ++ticket;
try {
const state = showOff ? pick.state : "켜짐";
const got = await fetchRows(file, page, size, q, false, pick.sub, pick.detail, state);
if (mine !== ticket) return;
data = got;
saveView(pick.group, { page: data.page });
paint();
void fillHourly(data.rows.map((r) => String(r["키"])));
} catch (error) {
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error");
}
};
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 () => {
ticket++;
fit.stop();
off();
};
}