Files
Aislo/M01_MasterData/M01_MasterData_UI_Pick.ts
T
eomsangdonandClaude Sonnet 5 d2a6646fc4 feat(M01): 조달 찾기 확정 모양 · 품셈재료 가격 연결 · 건설노임 준용 직종 고르기
- 고르기 API GET /api/m01/pick (나라장터자재 · 시중물가 · 공표 직종) — 조달 찾기 옛 API 대체
- 시중물가 조달›출처 「나라장터:<열쇠>」 연결 · 끊기 뒤 변화 없음 · 조달 값·기준 칸
- 품셈재료 「가격 연결」 모달(이름·규격 미리 찾기 · 관급 표시 · 후보 조건 · 끊기) · 못 이은 줄만 보기
- 건설노임 미공표 줄 「준용」 직종 고르기

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
2026-09-19 23:53:28 +09:00

134 lines
4.7 KiB
TypeScript

/* =============================================================================
* M01_MasterData_UI_Pick.ts
* 고르기 모달 — 조달 자료(나라장터) · 가격 연결(시중물가) · 준용 직종을 이름·규격으로 찾아 고르면 ref 를 돌려줌
* ========================================================================== */
import { el, showToast } from "@ui/ui_template_elements";
import { t as L } from "@ui/ui_template_locale";
import { fetchPick, type PickItem, type PickKind } from "./M01_MasterData_Api_Fetch";
import { show } from "./M01_MasterData_UI_Cells";
/** 지역이 갈리는 재료 — 이름·규격만 적어 두고 지역은 계산 때 입력. */
export interface Cond {
이름: string;
규격: string;
지역: "입력";
}
export interface PickOptions {
kind: PickKind;
title: string;
/** 지금 연결 글 — 비면 「연결 끊기」 꺼짐. */
current: string;
/** 처음 찾을 글 — 그 재료 이름·규격 (없는 결과면 첫 낱말만으로 다시). */
seed: string;
/** 고르면 ref · 연결 끊기 = null. */
onPick: (ref: string | null) => void;
/** 있으면 「후보 조건으로 연결」 칸이 뜸. */
cond?: { 이름: string; 규격: string; onCond: (c: Cond) => void };
}
const button = (text: string): HTMLButtonElement =>
el("button", { className: "m01-master__row-btn", text, attrs: { type: "button" } });
const input = (value: string, placeholder: string): HTMLInputElement => {
const box = el("input", {
className: "m01-master__search",
attrs: { type: "search", placeholder },
});
box.value = value;
return box;
};
export function openPickModal(opt: PickOptions): void {
const close = (): void => back.remove();
const search = input(opt.seed, L("M01_ProcureSearch"));
const list = el("div", { className: "m01-procure__list" });
const cut = button(L("M01_ProcureCut"));
cut.disabled = !opt.current;
cut.addEventListener("click", () => {
opt.onPick(null);
close();
});
const shut = button(L("M01_ProcureClose"));
shut.addEventListener("click", close);
const condRow: HTMLElement[] = [];
if (opt.cond) {
const { onCond } = opt.cond;
const name = input(opt.cond.이름, L("M01_PriceCondName"));
const spec = input(opt.cond.규격, L("M01_PriceCondSpec"));
const go = button(L("M01_PriceCond"));
go.addEventListener("click", () => {
if (!name.value.trim()) return;
onCond({ 이름: name.value.trim(), 규격: spec.value.trim(), 지역: "입력" });
close();
});
condRow.push(el("div", { className: "m01-procure__cond", children: [name, spec, go] }));
}
const box = el("div", {
className: "m01-procure__box",
children: [
el("h3", { text: opt.title }),
el("p", { className: "m01-master__muted", text: opt.current }),
search,
list,
...condRow,
el("div", { className: "m01-procure__foot", children: [cut, shut] }),
],
});
const back = el("div", { className: "m01-procure", children: [box] });
back.addEventListener("click", (e) => {
if (e.target === back) close();
});
document.body.append(back);
const paint = (items: PickItem[], total: number): void => {
list.replaceChildren(
...(items.length
? items.map((it) => {
const tag = it.관급 ? ` · ${L("M01_Govt")}` : "";
const b = el("button", {
className: "m01-procure__item",
attrs: { type: "button" },
children: [
el("strong", { text: it.이름 }),
el("span", { text: `${it.규격} · ${it.단위} · ${show(it.)}${tag}` }),
el("span", { className: "m01-master__muted", text: it.ref }),
],
});
b.addEventListener("click", () => {
opt.onPick(it.ref);
close();
});
return b;
})
: [el("p", { className: "m01-master__empty", text: L("M01_NoRows") })]),
...(total > items.length
? [el("p", { className: "m01-master__muted", text: `${items.length} / ${total}` })]
: []),
);
};
let timer: number | undefined;
const run = async (fallback = false): Promise<void> => {
try {
const q = search.value.trim();
const r = await fetchPick(opt.kind, q);
if (!r.total && fallback && q.includes(" ")) {
search.value = q.split(/\s+/)[0];
return run();
}
paint(r.items, r.total);
} catch (error) {
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error");
}
};
search.addEventListener("input", () => {
window.clearTimeout(timer);
timer = window.setTimeout(() => void run(), 300);
});
void run(true);
search.focus();
}