feat(M01): 화면 부품 공용화 — 드롭다운·찾기 칸·단추·확인창을 공용 템플릿으로 · 재료 컨테이너 차례

- createSelectField 에 선택 인자 더함(compact · ariaLabel · setOptions) — 기본 동작 그대로
- 인력 2단 거름 · 로직 편집 고르기 · 계산 입력 · 요소 찾기 창 그룹 select 를 공용으로 · m01-side__select 걷음
- 찾기 칸은 createInputField · 표 칸·모달 단추는 createButton · 버리기 확인은 showConfirmDialog
- 재료 컨테이너 = 자재품목 · 유가 · 품셈재료 차례 · 안 쓰는 조달 문구 걷음

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 16:27:06 +09:00
co-authored by Claude Sonnet 5
parent 9de61eb251
commit 21f04c64bb
12 changed files with 141 additions and 142 deletions
+50 -46
View File
@@ -5,7 +5,7 @@
* 펼치면 하위 목록(거름 · 파일) · 환율/요율은 누르면 바로 표 · 로직은 안에 로직 목록
* ========================================================================== */
import { el, showToast } from "@ui/ui_template_elements";
import { createSelectField, el, showToast } from "@ui/ui_template_elements";
import { attachCollapsible } from "@ui/ui_template_collapsible";
import { t as L } from "@ui/ui_template_locale";
import {
@@ -38,6 +38,8 @@ export interface SideHandle {
const LABOR_FILTER = "m01.laborFilter";
const LABOR_ID = "인력|";
const MATERIAL_NAMES: Record<string, string> = { 나라장터자재: "나라장터", 오피넷유가: "유가" };
/** 재료 컨테이너 차례 — 여기 없는 파일은 뒤에 */
const MATERIAL_ORDER = ["자재품목", "오피넷유가", "품셈재료"];
/** 파일 이름 → 목록 글자 — 「소요량_건설품셈_10장_창호…」 → 「건설품셈_10장_창호…」 */
export const fileLabel = (file: string): string =>
@@ -114,64 +116,59 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si
}
})();
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 opts = (values: string[]): { value: string; text: string }[] =>
[L("M01_All"), ...values].map((v, i) => ({ value: i ? v : "", text: v }));
const sub = createSelectField({
options: opts(list.map((k) => k.name)),
value: pickSub?.name ?? "",
compact: true,
label: L("M01_LaborSub"),
});
const detail = createSelectField({
options: opts(pickSub?.details ?? []),
value: saved[1] ?? "",
compact: true,
label: L("M01_LaborDetail"),
disabled: !pickSub?.details.length,
});
const run = (): void => {
try {
sessionStorage.setItem(LABOR_FILTER, JSON.stringify([sub.value, detail.value]));
sessionStorage.setItem(
LABOR_FILTER,
JSON.stringify([sub.select.value, detail.select.value]),
);
} catch {
/* 기억 못 해도 거름은 됨 */
}
const label = [sub.value, detail.value].filter(Boolean).join(" · ") || L("M01_All");
const label =
[sub.select.value, detail.select.value].filter(Boolean).join(" · ") || L("M01_All");
setActive(LABOR_ID);
open(file, sub.value, label, detail.value);
open(file, sub.select.value, label, detail.select.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;
sub.select.addEventListener("change", () => {
const details = list.find((k) => k.name === sub.select.value)?.details ?? [];
detail.setOptions(opts(details), "");
detail.select.disabled = details.length === 0;
run();
});
detail.addEventListener("change", run);
detail.select.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"));
sub.select.value = "";
sub.select.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],
}),
el("div", { className: "m01-side__filter", children: [sub.root, detail.root] }),
],
});
};
const order = (file: string): number => {
const i = MATERIAL_ORDER.indexOf(fileLabel(file));
return i < 0 ? MATERIAL_ORDER.length : i;
};
const drawGroup = (group: Group): void => {
const files = lists.get(group) ?? [];
const body = bodies.get(group);
@@ -231,11 +228,18 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si
}
body.replaceChildren(
...renderTree(
files.map((f) => {
const name = fileLabel(f.file);
const label = group === "재료" ? (MATERIAL_NAMES[name] ?? name) : name;
return { id: `${group}\n${f.file}`, label, count: f.rows, run: () => open(f, "", label) };
}),
[...files]
.sort((a, b) => order(a.file) - order(b.file))
.map((f) => {
const name = fileLabel(f.file);
const label = group === "재료" ? (MATERIAL_NAMES[name] ?? name) : name;
return {
id: `${group}\n${f.file}`,
label,
count: f.rows,
run: () => open(f, "", label),
};
}),
make,
),
);