feat(M01): 인력 한 표 · 왼쪽 「전체」 + 구분 → 상세구분 두 단 거름 · 준용 모든 줄 고르기

- 인력 열 순서 = 키 … 옛이름 · 비고 · 준용 · 상태 읽기 전용
- 구분·상세구분 목록은 서버 /subs · 고른 거름은 sessionStorage 기억

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
2026-09-20 15:18:26 +09:00
co-authored by Claude Sonnet 5
parent ef040a5749
commit 02b9c8be92
4 changed files with 105 additions and 10 deletions
+80 -2
View File
@@ -35,6 +35,8 @@ export interface SideHandle {
setActive: (id: string | null) => void;
}
const LABOR_FILTER = "m01.laborFilter";
const LABOR_ID = "인력|";
const MATERIAL_NAMES: Record<string, string> = { : "나라장터", : "유가" };
/** 파일 이름 → 목록 글자 — 「소요량_건설품셈_10장_창호…」 → 「건설품셈_10장_창호…」 */
@@ -98,6 +100,78 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si
return button;
};
/** 인력 — 「전체」 한 줄 + 구분 → 상세구분 두 단 거름(서버가 준 목록 · 고른 것은 기억) */
const laborFilter = (
file: FileInfo,
open: (file: FileInfo, sub: string, label: string, detail?: string) => void,
): HTMLElement => {
const list = kinds.get("인력") ?? [];
const saved = (() => {
try {
return JSON.parse(sessionStorage.getItem(LABOR_FILTER) ?? "[]") as string[];
} catch {
return [];
}
})();
const pickSub = list.find((k) => k.name === saved[0]);
const choose = (name: string, values: string[], value: string): HTMLSelectElement => {
const select = el("select", {
className: "m01-side__select",
attrs: { "aria-label": name },
children: [L("M01_All"), ...values].map((v, i) =>
el("option", { text: v, attrs: { value: i ? v : "" } }),
),
});
select.value = value;
return select;
};
const sub = choose(
L("M01_LaborSub"),
list.map((k) => k.name),
pickSub?.name ?? "",
);
const detail = choose(L("M01_LaborDetail"), pickSub?.details ?? [], saved[1] ?? "");
detail.disabled = !pickSub?.details.length;
const run = (): void => {
try {
sessionStorage.setItem(LABOR_FILTER, JSON.stringify([sub.value, detail.value]));
} catch {
/* 기억 못 해도 거름은 됨 */
}
const label = [sub.value, detail.value].filter(Boolean).join(" · ") || L("M01_All");
setActive(LABOR_ID);
open(file, sub.value, label, detail.value);
};
sub.addEventListener("change", () => {
const details = list.find((k) => k.name === sub.value)?.details ?? [];
detail.replaceChildren(
...[L("M01_All"), ...details].map((v, i) =>
el("option", { text: v, attrs: { value: i ? v : "" } }),
),
);
detail.disabled = details.length === 0;
run();
});
detail.addEventListener("change", run);
const all = make({ id: LABOR_ID, label: L("M01_All"), count: file.rows, run }, "", 0, () => {
sub.value = detail.value = "";
sub.dispatchEvent(new Event("change"));
});
return el("div", {
children: [
all,
el("label", {
className: "m01-side__filter",
children: [el("span", { text: L("M01_LaborSub") }), sub],
}),
el("label", {
className: "m01-side__filter",
children: [el("span", { text: L("M01_LaborDetail") }), detail],
}),
],
});
};
const drawGroup = (group: Group): void => {
const files = lists.get(group) ?? [];
const body = bodies.get(group);
@@ -105,7 +179,11 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si
const open = (file: FileInfo, sub: string, label: string, detail = ""): void =>
onOpen({ group, file, sub, detail, label });
const one = files[0];
if (group === "인력" || group === "기계") {
if (group === "인력" && one) {
body.replaceChildren(laborFilter(one, open));
return;
}
if (group === "기계") {
const list = kinds.get(group) ?? [];
const all: TreeNode[] = one
? [
@@ -170,7 +248,7 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si
const subs = await fetchSubs(file);
kinds.set(group, subs);
await Promise.all(
subs.map(async (k) =>
(group === "기계" ? subs : []).map(async (k) =>
counts.set(`${group}\n${k.name}`, (await fetchRows(file, 1, 1, "", false, k.name)).total),
),
);