Files
Aislo/M02_MasterTemplete/M02_MyMaterial_View.ts
T
eomsangdonandClaude Opus 5.5 207be5bd71 feat(840-4): 내 재료 = 재료 DB(M01)에 없는 자재를 등록하는 자리 — 개인관리 › 자재 등록 칸(만들기 · 고치기 · 지우기) · M01 과 같은 이름 · 규격 · 단위 막음 · 단가산출 재료 고르기 「내 재료」 · 값 바꾸면 자동 따라옴
- 서버 `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>
2026-11-15 16:40:08 +09:00

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,
],
});
}