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
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { openProcureModal } from "./M01_MasterData_UI_Procure";
|
||||
import { openPickModal } from "./M01_MasterData_UI_Pick";
|
||||
import { fetchRows, type Row, type RowsPage } from "./M01_MasterData_Api_Fetch";
|
||||
import {
|
||||
addRow,
|
||||
@@ -21,16 +21,22 @@ import {
|
||||
buildCell,
|
||||
buildPager,
|
||||
coerce,
|
||||
deep,
|
||||
flatten,
|
||||
isScalar,
|
||||
SEP,
|
||||
show,
|
||||
withCell,
|
||||
withDeep,
|
||||
} from "./M01_MasterData_UI_Cells";
|
||||
|
||||
const SIZE = 50;
|
||||
const MARKET = "재료_시중물가.json";
|
||||
const LINK = `조달${SEP}연결`;
|
||||
const LINKED = "재료_품셈재료.json";
|
||||
const WAGE = "인력_건설노임.json";
|
||||
const SUPPLY = ["값", "조달"];
|
||||
const SOURCE = [...SUPPLY, "출처"];
|
||||
const NARA = "나라장터:";
|
||||
const FALLBACK: Row = { 열쇠: "", 이름: "", 규격: "", 단위: "", 값: null, 출처: "" };
|
||||
|
||||
/** 새 줄 바탕 — 본 줄과 같은 칸 · 글 칸은 "" · 수·값 칸은 null. */
|
||||
@@ -43,10 +49,29 @@ function blank(sample: Row): Row {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 요소별 전용 칸으로 그리는 칸 — 일반 칸에서 뺌. */
|
||||
function hidden(file: string, col: string): boolean {
|
||||
if (file === MARKET) return col === `값${SEP}조달`;
|
||||
if (file === LINKED) return col.startsWith("연결");
|
||||
return file === WAGE && col === "준용";
|
||||
}
|
||||
|
||||
const HEADS: Record<string, string[]> = {
|
||||
[MARKET]: ["조달 값", "조달 기준", "M01_ProcureCol"],
|
||||
[LINKED]: ["M01_PriceCol"],
|
||||
[WAGE]: ["M01_JobCol"],
|
||||
};
|
||||
|
||||
const linkText = (link: unknown): string =>
|
||||
link && typeof link === "object"
|
||||
? `${L("M01_PriceCondUsed")}: ${(link as Row)["이름"]} ${(link as Row)["규격"] ?? ""}`.trim()
|
||||
: String(link ?? "");
|
||||
|
||||
/** 돌려받은 함수 = 이 화면을 걷을 때 부를 해제. */
|
||||
export function renderRows(host: HTMLElement, file: string, q: string): () => void {
|
||||
let page = 1;
|
||||
let data: RowsPage | null = null;
|
||||
let unlinked = false;
|
||||
|
||||
const paint = (): void => {
|
||||
if (!data) return;
|
||||
@@ -55,14 +80,16 @@ export function renderRows(host: HTMLElement, file: string, q: string): () => vo
|
||||
const adds = draft?.adds ?? [];
|
||||
const cols: string[] = [];
|
||||
for (const row of [...adds, ...d.rows]) {
|
||||
for (const k of Object.keys(flatten(row))) if (k !== LINK && !cols.includes(k)) cols.push(k);
|
||||
for (const k of Object.keys(flatten(row)))
|
||||
if (!hidden(file, k) && !cols.includes(k)) cols.push(k);
|
||||
}
|
||||
const market = file === MARKET;
|
||||
const head = el("tr", {
|
||||
children: [
|
||||
el("th"),
|
||||
...cols.map((c) => el("th", { text: c })),
|
||||
...(market ? [el("th", { text: L("M01_ProcureCol") })] : []),
|
||||
...(HEADS[file] ?? []).map((h) =>
|
||||
el("th", { text: h.startsWith("M01_") ? L(h as "M01_PriceCol") : h }),
|
||||
),
|
||||
],
|
||||
});
|
||||
const body = el("tbody");
|
||||
@@ -107,23 +134,29 @@ export function renderRows(host: HTMLElement, file: string, q: string): () => vo
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (market) {
|
||||
const link = String(now[LINK] ?? "");
|
||||
const pick = (ref: string | null): void =>
|
||||
editRow(file, d.version, key, row, withCell(cur, LINK, ref));
|
||||
const btn = el("button", {
|
||||
className: "m01-master__row-btn",
|
||||
text: L("M01_ProcureFind"),
|
||||
attrs: { type: "button" },
|
||||
});
|
||||
btn.addEventListener("click", () => openProcureModal(link, pick));
|
||||
const td = buildCell(link, { changed: link !== String(orig[LINK] ?? "") });
|
||||
td.append(btn);
|
||||
tr.append(td);
|
||||
}
|
||||
const put = deleted
|
||||
? undefined
|
||||
: (next: Row): void => editRow(file, d.version, key, row, next);
|
||||
if (file === MARKET) tr.append(...marketCells(row, cur, put));
|
||||
if (file === LINKED) tr.append(linkedCell(row, cur, put));
|
||||
if (file === WAGE) tr.append(wageCell(row, cur, put));
|
||||
body.append(tr);
|
||||
}
|
||||
|
||||
const only =
|
||||
file === LINKED
|
||||
? [
|
||||
createButton({
|
||||
label: L("M01_OnlyUnlinked"),
|
||||
variant: unlinked ? "filled" : "ghost",
|
||||
onClick: () => {
|
||||
unlinked = !unlinked;
|
||||
page = 1;
|
||||
void load();
|
||||
},
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
const add = createButton({ label: L("M01_RowAdd"), variant: "ghost" });
|
||||
add.addEventListener("click", () => addRow(file, d.version, blank(d.rows[0] ?? FALLBACK)));
|
||||
const empty =
|
||||
@@ -133,7 +166,7 @@ export function renderRows(host: HTMLElement, file: string, q: string): () => vo
|
||||
host.replaceChildren(
|
||||
el("div", {
|
||||
className: "m01-master__bar",
|
||||
children: [add, buildPager(d.total, SIZE, d.page, go)],
|
||||
children: [add, ...only, buildPager(d.total, SIZE, d.page, go)],
|
||||
}),
|
||||
el("div", {
|
||||
className: "m01-master__grid-wrap",
|
||||
@@ -155,7 +188,7 @@ export function renderRows(host: HTMLElement, file: string, q: string): () => vo
|
||||
|
||||
const load = async (): Promise<void> => {
|
||||
try {
|
||||
data = await fetchRows(file, page, SIZE, q);
|
||||
data = await fetchRows(file, page, SIZE, q, unlinked);
|
||||
paint();
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error");
|
||||
@@ -176,3 +209,78 @@ function actionCell(label: string, onClick: () => void): HTMLTableCellElement {
|
||||
button.addEventListener("click", onClick);
|
||||
return el("td", { children: [button] });
|
||||
}
|
||||
|
||||
type Put = (row: Row) => void;
|
||||
|
||||
const rowBtn = (text: string): HTMLButtonElement =>
|
||||
el("button", { className: "m01-master__row-btn", text, attrs: { type: "button" } });
|
||||
|
||||
/** 시중물가 — 조달 값 · 기준 · 조달 연결(「나라장터:<열쇠>」 · 「가격정보」 줄은 원천이라 그대로). */
|
||||
function marketCells(row: Row, cur: Row, put?: Put): HTMLTableCellElement[] {
|
||||
const link = show(deep(cur, SOURCE));
|
||||
const td = buildCell(link, { changed: link !== show(deep(row, SOURCE)) });
|
||||
if (put && (!link || link.startsWith(NARA))) {
|
||||
const btn = rowBtn(L("M01_ProcureFind"));
|
||||
btn.addEventListener("click", () =>
|
||||
openPickModal({
|
||||
kind: "procure",
|
||||
title: L("M01_ProcureTitle"),
|
||||
current: link,
|
||||
seed: `${cur["이름"] ?? ""} ${cur["규격"] ?? ""}`.trim(),
|
||||
onPick: (ref) => put(withDeep(cur, SOURCE, ref)),
|
||||
}),
|
||||
);
|
||||
td.append(btn);
|
||||
}
|
||||
return [
|
||||
buildCell(show(deep(cur, [...SUPPLY, "값"])), { changed: false }),
|
||||
buildCell(show(deep(cur, [...SUPPLY, "기준"])), { changed: false }),
|
||||
td,
|
||||
];
|
||||
}
|
||||
|
||||
/** 품셈재료 — 가격 연결(시중물가 열쇠 · 후보 조건 · 없음) · 고르기 모달. */
|
||||
function linkedCell(row: Row, cur: Row, put?: Put): HTMLTableCellElement {
|
||||
const link = cur["연결"];
|
||||
const td = buildCell(linkText(link), {
|
||||
changed: JSON.stringify(link ?? null) !== JSON.stringify(row["연결"] ?? null),
|
||||
});
|
||||
if (put) {
|
||||
const cond = link && typeof link === "object" ? (link as Row) : null;
|
||||
const name = String(cond?.["이름"] ?? cur["이름"] ?? "");
|
||||
const spec = String(cond?.["규격"] ?? cur["규격"] ?? "");
|
||||
const btn = rowBtn(L("M01_PriceFind"));
|
||||
btn.addEventListener("click", () =>
|
||||
openPickModal({
|
||||
kind: "price",
|
||||
title: `${L("M01_PriceTitle")} · ${cur["이름"] ?? ""}`,
|
||||
current: linkText(link),
|
||||
seed: `${name} ${spec}`.trim(),
|
||||
onPick: (ref) => put(withCell(cur, "연결", ref)),
|
||||
cond: { 이름: name, 규격: spec, onCond: (c) => put(withCell(cur, "연결", c)) },
|
||||
}),
|
||||
);
|
||||
td.append(btn);
|
||||
}
|
||||
return td;
|
||||
}
|
||||
|
||||
/** 건설노임 — 미공표(값 없음) 줄의 준용 = 공표 직종 고르기. */
|
||||
function wageCell(row: Row, cur: Row, put?: Put): HTMLTableCellElement {
|
||||
const text = show(cur["준용"]);
|
||||
const td = buildCell(text, { changed: text !== show(row["준용"]) });
|
||||
if (put && row["값"] === null) {
|
||||
const btn = rowBtn(L("M01_JobFind"));
|
||||
btn.addEventListener("click", () =>
|
||||
openPickModal({
|
||||
kind: "job",
|
||||
title: `${L("M01_JobTitle")} · ${cur["이름"] ?? ""}`,
|
||||
current: text,
|
||||
seed: "",
|
||||
onPick: (ref) => put(withCell(cur, "준용", ref)),
|
||||
}),
|
||||
);
|
||||
td.append(btn);
|
||||
}
|
||||
return td;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user