- 파일 M02_CF_* → M02_DF_* · css m02-cf/m01-cf → m02-df · 서버 길 /api/m02/cf → /api/m02/df · 키 CF- → DF- · 사용자 요소 UE- 그대로 - _sample · .gitignore(_data git 밖) 옮김 · DataFlow 는 CostFlow 를 import 안 함 - 시험 test_m02_df_model · router · sample · inputpanel_build(옛 cf 시험은 그대로) - 서버 등록은 sub1 몫 — M02_MasterTemplete.DataFlow.M02_DF_Router 의 router Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk
324 lines
11 KiB
TypeScript
324 lines
11 KiB
TypeScript
/* =============================================================================
|
|
* M02_DF_InputPanel_Table.ts
|
|
* 입력 상자 판 — 소요량 · 계수 표 고르기(절 · 이름 찾기 → 미리 보기 → 표 통째 또는 줄 하나)
|
|
* `M02_DF_InputPanel.ts` 가 부름 · M01 Api 길(`/sections` · `/tables` · `/table`)만 읽음 · 글 [한, 영] 짝은 아래
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
|
import {
|
|
createButton,
|
|
createInputField,
|
|
createSelectField,
|
|
el,
|
|
showToast,
|
|
} from "@ui/ui_template_elements";
|
|
import type { CfGroup, CfLogic } from "./M02_DF_Model";
|
|
import { setTable } from "./M02_DF_InputPanel_Build";
|
|
|
|
const TEXT = {
|
|
Book: ["적용 품셈", "Standard"],
|
|
Search: ["절 번호나 이름 — 예: 13-4-1 메쌓기", "Section number or name"],
|
|
Loading: ["찾는 중…", "Searching…"],
|
|
NoSection: ["찾는 절 없음", "No section"],
|
|
SecLine: ["표 {t} 개", "{t} tables"],
|
|
Tables: ["이 절의 표 — 하나를 고름", "Pick a table"],
|
|
NoTable: ["이 절에는 이 그룹의 표가 없음", "No table of this group here"],
|
|
TableLine: [
|
|
"{b} · 조건 {c} · 결과 {v} · {n} 줄",
|
|
"{b} · {c} conditions · {v} results · {n} rows",
|
|
],
|
|
Chosen: ["고른 표", "Chosen table"],
|
|
Whole: [
|
|
"표 통째 — 함수 상자가 조건에 맞는 줄을 찾음",
|
|
"Whole table — a function box looks it up",
|
|
],
|
|
OneRow: ["줄 하나 고정", "Fix one row"],
|
|
RowNo: ["몇 번째 줄", "Row number"],
|
|
Preview: ["미리 보기", "Preview"],
|
|
Notes: ["주(註)", "Notes"],
|
|
Remove: ["표 빼기", "Remove table"],
|
|
Change: ["다른 표 고르기", "Pick another table"],
|
|
Failed: ["불러오지 못함", "Could not load"],
|
|
} as const;
|
|
|
|
type TKey = keyof typeof TEXT;
|
|
const t = (key: TKey, v: Record<string, string | number> = {}): string =>
|
|
Object.entries(v).reduce(
|
|
(s, [k, x]) => s.replace(`{${k}}`, String(x)),
|
|
TEXT[key][currentLanguageIndex] as string,
|
|
);
|
|
|
|
async function get<T>(path: string, params: Record<string, string>): Promise<T> {
|
|
const res = await fetch(`${API_BASE_URL}/m01${path}?${new URLSearchParams(params)}`, {
|
|
credentials: "include",
|
|
});
|
|
const data = (await res.json().catch(() => ({}))) as { detail?: unknown } & T;
|
|
if (!res.ok) throw new Error(String(data.detail ?? res.statusText));
|
|
return data;
|
|
}
|
|
|
|
interface SectionItem {
|
|
section: string;
|
|
title: string;
|
|
chapter: string;
|
|
tables: number;
|
|
}
|
|
interface TableHead {
|
|
file: string;
|
|
키: string;
|
|
이름: string;
|
|
기준: string;
|
|
조건: Record<string, string>;
|
|
값칸: Record<string, string>;
|
|
count: number;
|
|
}
|
|
interface RawTable {
|
|
키: string;
|
|
원문번호: string;
|
|
이름: string;
|
|
기준: string;
|
|
조건: Record<string, string>;
|
|
값칸: Record<string, string>;
|
|
줄: Record<string, unknown>[];
|
|
주?: string[];
|
|
}
|
|
|
|
const BOOKS = ["산림품셈", "건설품셈"];
|
|
const files = new Map<string, string>();
|
|
const tables = new Map<string, RawTable>();
|
|
|
|
/** 표 한 벌 — 키로 찾아 받음(받은 것은 기억) */
|
|
export async function loadTable(group: CfGroup, key: string): Promise<RawTable> {
|
|
const hit = tables.get(key);
|
|
if (hit) return hit;
|
|
let file = files.get(key);
|
|
if (!file) {
|
|
const found = (await get<{ tables: TableHead[] }>("/tables", { group, q: key })).tables.find(
|
|
(x) => x.키 === key,
|
|
);
|
|
if (!found) throw new Error(t("Failed"));
|
|
file = found.file;
|
|
}
|
|
const raw = (await get<{ table: RawTable }>("/table", { file, key })).table;
|
|
tables.set(key, raw);
|
|
return raw;
|
|
}
|
|
|
|
const cell = (v: unknown): string =>
|
|
Array.isArray(v)
|
|
? v.map((x) => x ?? "").join("~")
|
|
: v === null || v === undefined
|
|
? ""
|
|
: String(v);
|
|
|
|
/** 표 미리 보기 — 앞 8 줄 · `row`(0 부터) 줄은 칠함 */
|
|
function previewOf(tb: RawTable, row?: number): HTMLElement {
|
|
const cols = [...Object.keys(tb.조건), ...Object.keys(tb.값칸)];
|
|
const body = tb.줄.slice(0, Math.max(8, (row ?? 0) + 1));
|
|
return el("div", {
|
|
className: "m02-dfip__preview",
|
|
children: [
|
|
el("table", {
|
|
children: [
|
|
el("tr", { children: cols.map((c) => el("th", { text: c })) }),
|
|
...body.map((r, i) =>
|
|
el("tr", {
|
|
className: i === row ? "is-hit" : "",
|
|
children: cols.map((c) => el("td", { text: cell(r[c]) })),
|
|
}),
|
|
),
|
|
],
|
|
}),
|
|
...(tb.줄.length > body.length
|
|
? [el("p", { className: "m02-dfip__muted", text: `… ${tb.줄.length} 줄` })]
|
|
: []),
|
|
...(tb.주?.length
|
|
? [
|
|
el("details", {
|
|
children: [
|
|
el("summary", { text: t("Notes") }),
|
|
...tb.주.map((n) => el("p", { text: n })),
|
|
],
|
|
}),
|
|
]
|
|
: []),
|
|
],
|
|
});
|
|
}
|
|
|
|
/** 소요량 · 계수 표 고르기 화면 */
|
|
export function renderTablePick(
|
|
host: HTMLElement,
|
|
lg: CfLogic,
|
|
boxId: string,
|
|
group: CfGroup,
|
|
on: { change(lg: CfLogic): void },
|
|
): void {
|
|
const box = lg.boxes.find((b) => b.id === boxId);
|
|
const cur = box?.input?.table;
|
|
if (cur) return void showChosen(host, lg, boxId, group, cur, on);
|
|
let book = BOOKS[0];
|
|
const bookBox = createSelectField({
|
|
label: t("Book"),
|
|
options: BOOKS.map((b) => ({ value: b, text: b })),
|
|
value: book,
|
|
compact: true,
|
|
onChange: (v) => ((book = v), load()),
|
|
});
|
|
const q = createInputField({ type: "search", placeholder: t("Search"), compact: true });
|
|
const list = el("div", { className: "m02-dfip__list" });
|
|
const tableList = el("div", { className: "m02-dfip__list" });
|
|
let seq = 0;
|
|
const load = async (): Promise<void> => {
|
|
const mine = ++seq;
|
|
list.replaceChildren(el("p", { className: "m02-dfip__muted", text: t("Loading") }));
|
|
try {
|
|
const r = await get<{ sections: SectionItem[] }>("/sections", {
|
|
book,
|
|
q: q.input.value.trim(),
|
|
limit: "60",
|
|
});
|
|
if (mine !== seq) return;
|
|
list.replaceChildren(
|
|
...(r.sections.length
|
|
? r.sections.map((s) => {
|
|
const b = el("button", {
|
|
className: "m02-dfip__row",
|
|
attrs: { type: "button" },
|
|
children: [
|
|
el("strong", { text: `${s.section} ${s.title}` }),
|
|
el("span", {
|
|
className: "m02-dfip__muted",
|
|
text: `${s.chapter} · ${t("SecLine", { t: s.tables })}`,
|
|
}),
|
|
],
|
|
});
|
|
b.addEventListener("click", () => void pickSection(s));
|
|
return b;
|
|
})
|
|
: [el("p", { className: "m02-dfip__muted", text: t("NoSection") })]),
|
|
);
|
|
} catch (e) {
|
|
if (mine === seq) showToast(e instanceof Error ? e.message : t("Failed"), "error");
|
|
}
|
|
};
|
|
const pickSection = async (s: SectionItem): Promise<void> => {
|
|
try {
|
|
const heads = (
|
|
await get<{ tables: TableHead[] }>("/tables", {
|
|
group,
|
|
section: s.section,
|
|
sub: book,
|
|
size: "100",
|
|
})
|
|
).tables;
|
|
tableList.replaceChildren(
|
|
el("h5", { text: t("Tables") }),
|
|
...(heads.length
|
|
? heads.map((h) => {
|
|
const b = el("button", {
|
|
className: "m02-dfip__row",
|
|
attrs: { type: "button" },
|
|
children: [
|
|
el("strong", { text: h.이름 }),
|
|
el("span", {
|
|
className: "m02-dfip__muted",
|
|
text: t("TableLine", {
|
|
b: h.기준 || "—",
|
|
c: Object.keys(h.조건).length,
|
|
v: Object.keys(h.값칸).length,
|
|
n: h.count,
|
|
}),
|
|
}),
|
|
],
|
|
});
|
|
b.addEventListener("click", () => {
|
|
files.set(h.키, h.file);
|
|
void loadTable(group, h.키).then(
|
|
(tb) => on.change(setTable(lg, boxId, { key: h.키 }, tb.이름)),
|
|
(e) => showToast(e instanceof Error ? e.message : t("Failed"), "error"),
|
|
);
|
|
});
|
|
return b;
|
|
})
|
|
: [el("p", { className: "m02-dfip__muted", text: t("NoTable") })]),
|
|
);
|
|
} catch (e) {
|
|
showToast(e instanceof Error ? e.message : t("Failed"), "error");
|
|
}
|
|
};
|
|
let timer = 0;
|
|
q.input.addEventListener(
|
|
"input",
|
|
() => (window.clearTimeout(timer), (timer = window.setTimeout(load, 250))),
|
|
);
|
|
host.replaceChildren(bookBox.root, q.root, list, tableList);
|
|
void load();
|
|
}
|
|
|
|
/** 고른 표 — 통째 · 줄 하나 · 미리 보기 */
|
|
function showChosen(
|
|
host: HTMLElement,
|
|
lg: CfLogic,
|
|
boxId: string,
|
|
group: CfGroup,
|
|
cur: { key: string; row?: number },
|
|
on: { change(lg: CfLogic): void },
|
|
): void {
|
|
const body = el("div", { className: "m02-dfip__stack" });
|
|
host.replaceChildren(body);
|
|
void loadTable(group, cur.key).then(
|
|
(tb) => {
|
|
const whole = cur.row === undefined;
|
|
const modeWhole = createButton({
|
|
label: t("Whole"),
|
|
variant: whole ? "filled" : "ghost",
|
|
onClick: () => on.change(setTable(lg, boxId, { key: cur.key })),
|
|
});
|
|
const modeOne = createButton({
|
|
label: t("OneRow"),
|
|
variant: whole ? "ghost" : "filled",
|
|
onClick: () => on.change(setTable(lg, boxId, { key: cur.key, row: cur.row ?? 0 })),
|
|
});
|
|
const rowBox = createSelectField({
|
|
label: t("RowNo"),
|
|
options: tb.줄.map((r, i) => ({
|
|
value: String(i),
|
|
text: `${i + 1} · ${[...Object.keys(tb.조건), ...Object.keys(tb.값칸)]
|
|
.slice(0, 4)
|
|
.map((c) => cell(r[c]))
|
|
.join(" / ")}`,
|
|
})),
|
|
value: String(cur.row ?? 0),
|
|
compact: true,
|
|
onChange: (v) => on.change(setTable(lg, boxId, { key: cur.key, row: Number(v) })),
|
|
});
|
|
body.replaceChildren(
|
|
el("h5", { text: `${t("Chosen")} — ${tb.이름} · ${tb.원문번호} · ${tb.기준 || "—"}` }),
|
|
el("div", { className: "m02-dfip__chips", children: [modeWhole, modeOne] }),
|
|
...(whole ? [] : [rowBox.root]),
|
|
el("h5", { text: t("Preview") }),
|
|
previewOf(tb, cur.row),
|
|
el("div", {
|
|
className: "m02-dfip__chips",
|
|
children: [
|
|
createButton({
|
|
label: t("Change"),
|
|
variant: "ghost",
|
|
onClick: () => on.change(setTable(lg, boxId, null)),
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
},
|
|
(e) =>
|
|
body.replaceChildren(
|
|
el("p", {
|
|
className: "m02-dfip__muted",
|
|
text: e instanceof Error ? e.message : t("Failed"),
|
|
}),
|
|
),
|
|
);
|
|
}
|