- 서버 `M02_Material_Own`: M01 자재품목과 이름 · 규격 · 단위 같음 = 409(빈칸 · 대소문자 · ㎏ = kg 무시) · 고치기 · 지우기(쓰는 곳 있으면 409 · 밑줄 폴더 보관본 뺌) · 목록 줄마다 쓰는 곳 - 셈 색인: 단가산출 사본 색인 · 사본 모으기 · 빈틈 채우기에 내 재료 줄을 내 기계처럼 얹음 → 개인 층 단가산출은 값 바뀌면 836-2 길로 따라옴 · 프로젝트 층 = 사본 값 - 화면: 개인관리 › 자재 위 「내 재료」 표 + 고침 칸(`M02_MyMaterial_View` · `M02_MyMaterial_Api`) · 아래 「값을 바로 적은 재료」 모아 보기는 읽기 그대로 · 찾아 넣기 재료 탭 M01 위 「내 재료」 묶음(줄 쌓기 개인 층) - M01 재료를 본떠 값만 바꾼 내 재료를 만드는 길 = 없었음(걷을 것 없음) 낡은 시험 고침: test_m02_material_own 목록 줄에 「쓰는곳」 칸 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
186 lines
6.4 KiB
TypeScript
186 lines
6.4 KiB
TypeScript
/* =============================================================================
|
|
* M02_MyMaterial_View.ts
|
|
* M02 개인관리 › 자재 위 「내 재료」 등록 칸(PLAN 840-4 · 요구 854 · 862) — 개인 층
|
|
* 내 재료 = 재료 DB(M01)에 없는 자재를 등록하는 곳 · M01 재료 값 바꿈은 단가산출 안 재료 줄에서
|
|
* 표 = 이름 · 규격 · 단위 · 값 · 쓰는 곳 · 키 · 줄 누름 = 아래 칸에서 고치기 · [새 자재] = 빈 칸
|
|
* [저장] = 서버(M01 과 이름 · 규격 · 단위 같으면 서버가 막음 — 까닭 그대로 알림) · [지우기] = 쓰는 곳 있으면 서버가 막음
|
|
* 값을 고치면 그 재료를 쓰는 개인 층 단가산출이 따라옴(836-2 자동 따라옴 · 프로젝트 층 = 사본 값)
|
|
* 남의 층(owner 있음) = 읽기만
|
|
* ========================================================================== */
|
|
|
|
import { createButton } from "@ui/ui_template_button";
|
|
import { createDataTable } from "@ui/ui_template_data_table";
|
|
import { createInputField, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
|
import { createText } from "@ui/ui_template_text";
|
|
import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick";
|
|
import {
|
|
addMyMaterial,
|
|
dropMyMaterial,
|
|
editMyMaterial,
|
|
listMyMaterials,
|
|
type MyMaterial,
|
|
} from "./M02_MyMaterial_Api";
|
|
|
|
const TEXT = {
|
|
title: "내 재료",
|
|
note: "재료 DB(M01)에 없는 자재를 등록하는 곳 — 단가산출 재료 고르기 「내 재료」 로 씀 · M01 재료 값 바꿈은 단가산출 안 재료 줄에서",
|
|
none: "내 재료 없음",
|
|
add: "새 자재",
|
|
name: "품명",
|
|
spec: "규격",
|
|
unit: "단위",
|
|
value: "값(원)",
|
|
uses: "쓰는 곳",
|
|
key: "키",
|
|
save: "저장",
|
|
drop: "지우기",
|
|
cancel: "취소",
|
|
need: "품명 · 단위는 꼭 적음",
|
|
saved: "저장함 — 쓰는 단가산출이 새 값으로 셈",
|
|
dropped: "지움",
|
|
dropAsk: "「{name}」 을 지움",
|
|
readOnly: "남의 층 — 읽기만",
|
|
} as const;
|
|
|
|
export interface MyMaterialBlockOptions {
|
|
owner: string | null;
|
|
}
|
|
|
|
export function buildMyMaterialBlock(opt: MyMaterialBlockOptions): HTMLElement {
|
|
const canWrite = opt.owner === null;
|
|
let rows: MyMaterial[] = [];
|
|
let editing: MyMaterial | null = null;
|
|
let unit = "";
|
|
const table = createDataTable({
|
|
caption: TEXT.title,
|
|
columns: [
|
|
{ key: "이름", label: TEXT.name, width: 200 },
|
|
{ key: "규격", label: TEXT.spec, width: 140 },
|
|
{ key: "단위", label: TEXT.unit, width: 60 },
|
|
{ key: "값", label: TEXT.value, kind: "number", decimals: 2, trim: true, width: 100 },
|
|
{ key: "쓰는곳", label: TEXT.uses, kind: "number", decimals: 0, width: 70 },
|
|
{ key: "키", label: TEXT.key, width: 90 },
|
|
],
|
|
rows: [],
|
|
empty: TEXT.none,
|
|
onRowClick: (i) => canWrite && open(rows[i]),
|
|
});
|
|
const name = createInputField({ label: TEXT.name, required: true });
|
|
const spec = createInputField({ label: TEXT.spec });
|
|
const price = createInputField({ label: TEXT.value, type: "number", min: 0 });
|
|
const unitHost = el("span");
|
|
const drawUnit = (): void =>
|
|
unitHost.replaceChildren(
|
|
createUnitPick({
|
|
value: unit || NO_UNIT,
|
|
ariaLabel: TEXT.unit,
|
|
onPick: (u) => {
|
|
unit = u === NO_UNIT ? "" : u;
|
|
drawUnit();
|
|
},
|
|
}),
|
|
);
|
|
const head = createText("");
|
|
const saveBtn = createButton({ label: TEXT.save, variant: "filled", onClick: () => void save() });
|
|
const dropBtn = createButton({
|
|
label: TEXT.drop,
|
|
variant: "danger-ghost",
|
|
onClick: () => void drop(),
|
|
});
|
|
const form = el("div", {
|
|
className: "m02-pick__new",
|
|
attrs: { "data-my-material-form": "" },
|
|
children: [
|
|
head,
|
|
name.root,
|
|
spec.root,
|
|
el("div", {
|
|
className: "ui-field",
|
|
children: [el("label", { className: "ui-field__label", text: TEXT.unit }), unitHost],
|
|
}),
|
|
price.root,
|
|
el("div", {
|
|
className: "ui-btn-group",
|
|
children: [
|
|
createButton({ label: TEXT.cancel, variant: "ghost", onClick: () => close() }),
|
|
dropBtn,
|
|
saveBtn,
|
|
],
|
|
}),
|
|
],
|
|
});
|
|
form.hidden = true;
|
|
|
|
function open(row: MyMaterial | null): void {
|
|
editing = row;
|
|
name.input.value = row?.이름 ?? "";
|
|
spec.input.value = row?.규격 ?? "";
|
|
price.input.value = row?.값 === null || row?.값 === undefined ? "" : String(row.값);
|
|
unit = row?.단위 ?? "";
|
|
drawUnit();
|
|
head.textContent = row ? `${row.키} ${row.이름}` : TEXT.add;
|
|
dropBtn.hidden = !row;
|
|
form.hidden = false;
|
|
name.input.focus();
|
|
}
|
|
function close(): void {
|
|
editing = null;
|
|
form.hidden = true;
|
|
}
|
|
async function load(): Promise<void> {
|
|
rows = await listMyMaterials(opt.owner).catch((e) => {
|
|
showToast(e instanceof Error ? e.message : String(e), "error");
|
|
return [];
|
|
});
|
|
table.setRows(rows.map((r) => ({ ...r, 쓰는곳: r.쓰는곳?.length ?? 0 })));
|
|
}
|
|
async function save(): Promise<void> {
|
|
const 이름 = name.input.value.trim();
|
|
if (!이름 || !unit) return showToast(TEXT.need, "error");
|
|
const raw = price.input.value.trim();
|
|
const body = {
|
|
이름,
|
|
규격: spec.input.value.trim(),
|
|
단위: unit,
|
|
값: raw === "" ? null : Number(raw),
|
|
};
|
|
saveBtn.disabled = true;
|
|
try {
|
|
if (editing) await editMyMaterial(opt.owner, editing.키, body);
|
|
else await addMyMaterial(opt.owner, body);
|
|
showToast(TEXT.saved, "success");
|
|
close();
|
|
await load();
|
|
} catch (e) {
|
|
showToast(e instanceof Error ? e.message : String(e), "error");
|
|
} finally {
|
|
saveBtn.disabled = false;
|
|
}
|
|
}
|
|
async function drop(): Promise<void> {
|
|
if (!editing) return;
|
|
if (!(await showConfirmDialog(TEXT.dropAsk.replace("{name}", editing.이름), TEXT.drop))) return;
|
|
try {
|
|
await dropMyMaterial(opt.owner, editing.키);
|
|
showToast(TEXT.dropped, "success");
|
|
close();
|
|
await load();
|
|
} catch (e) {
|
|
showToast(e instanceof Error ? e.message : String(e), "error");
|
|
}
|
|
}
|
|
|
|
const addBtn = createButton({ label: TEXT.add, variant: "filled", onClick: () => open(null) });
|
|
addBtn.hidden = !canWrite;
|
|
void load();
|
|
return el("div", {
|
|
attrs: { "data-my-materials": "" },
|
|
children: [
|
|
el("div", { className: "ui-toolbar", children: [addBtn] }),
|
|
createText(canWrite ? TEXT.note : TEXT.readOnly, "muted"),
|
|
table.root,
|
|
form,
|
|
],
|
|
});
|
|
}
|