- 셈 길 calcSangeun 한 벌 — 답 꼴 제네릭(기본 717-2b SkAnswer · 717-3 모달은 SkCalc) · 시험 입력 칸(입력값) 그대로 · owner 는 시스템 층 밖만 - 717-3 일위대가 모달 표 = coverTable · sangeunFromLine 그대로 · 717-2b SK 상세 표 = skCoverTable(옛 coverTable 이름 바꿈 · 옵션 = SkCoverOptions) - 모달 부르는 자리 셋에 <SkCalc> 만 더함 - 시험 test_717_2b · test_717_3 · 715 · 719 통과 · typecheck 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
242 lines
9.1 KiB
TypeScript
242 lines
9.1 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Labor_SangeunModal.ts
|
|
* 일위대가 줄 [단가산출 보기] 모달(PLAN 717-3 · 요구서 내역12 4장 4 · 16 · 24 · 4-1 확정 5) —
|
|
* 로직 줄 · 단가산출(SK) 참조 줄 하나의 단가산출을 보고 그 일위대가에서만 고침.
|
|
* · 위 = 일위대가 머리(「1 ㎡ 당」) · 이 줄 수량 · 1 단위 금액 · 짝 잃은 덮개 알림
|
|
* · 보기 고르기 = 인력·재료·경비(기본 · 공용 계산서 `calc_sheet`) | 단가산출 상세(품 · 단가 칸 고침 · `Sangeun_Cover`)
|
|
* · 고침 = 그 일위대가 줄 칸 `패치 {덮개, 뺀줄, 더한줄}` · [적용] = 화면 캐시(저장은 페이지 [저장] ·
|
|
* 원값은 저장 때 서버가 셈) · 적용 안 하고 닫으면 물음
|
|
* · [단가산출로 저장] = 개인 층 새 SK(이름 받음 · `from-line`) · 일위대가가 자기 개인 층이면 그 줄을 새 SK 참조 줄로
|
|
* · 셈 = 서버 셈 길(로그인 · 저장 안 함 · 최신 응답만 · CLAUDE.md 5장)
|
|
* ========================================================================== */
|
|
|
|
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
|
import { askConfirm, askText, openModal } from "@ui/ui_template_modal";
|
|
import { createSegment } from "@ui/ui_template_segment";
|
|
import { createText } from "@ui/ui_template_text";
|
|
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
|
import { calcSheetRows } from "../A00_Common/calc_sheet";
|
|
import { formatMoney } from "../M01_MasterData/M01_MasterData_UI_Logic_Money";
|
|
import type { LaborRow } from "./M02_MasterTemplete_Labor_Api";
|
|
import {
|
|
calcSangeun,
|
|
coverTable,
|
|
sangeunFromLine,
|
|
type SkScope,
|
|
} from "./M02_MasterTemplete_Sangeun_Cover";
|
|
import {
|
|
cleanPatch,
|
|
lineDoc,
|
|
needsIds,
|
|
setCover,
|
|
sheetGroups,
|
|
type LinePatch,
|
|
type SkCalc,
|
|
type SkLine,
|
|
} from "./M02_Sangeun_Cover_Model";
|
|
|
|
const TEXT = {
|
|
Title: ["단가산출", "Unit-price build-up"],
|
|
Sheet: ["인력·재료·경비", "Labor · material · expense"],
|
|
Detail: ["단가산출 상세", "Detail"],
|
|
Qty: ["이 줄 수량", "Line quantity"],
|
|
Per: ["1 {u} 당", "per 1 {u}"],
|
|
Sum: ["계", "Total"],
|
|
Name: ["이름", "Name"],
|
|
Poom: ["품 · 수량", "Qty"],
|
|
Unit: ["단위", "Unit"],
|
|
Price: ["단가", "Unit price"],
|
|
Amount: ["금액", "Amount"],
|
|
Was: ["원래", "was"],
|
|
Tag: ["이 일위대가에서 고침", "Edited in this unit price"],
|
|
Empty: ["줄 없음", "No lines"],
|
|
Loading: ["셈하는 중…", "Calculating…"],
|
|
Lost: ["덮개 짝 잃음", "Override lost"],
|
|
Apply: ["적용", "Apply"],
|
|
ToSk: ["단가산출로 저장", "Save as unit-price build-up"],
|
|
AskName: ["새 단가산출 이름(내 개인 층)", "New build-up name (my layer)"],
|
|
Saved: ["{k} 저장함(내 개인 층)", "{k} saved (my layer)"],
|
|
Replaced: ["이 줄을 {k} 참조로 바꿈 — [저장]으로 남김", "Line now refers to {k} — press [Save]"],
|
|
Discard: ["적용 안 한 고침이 있음 — 버리고 닫을까?", "Discard unapplied edits?"],
|
|
Close: ["닫기", "Close"],
|
|
ReadOnly: ["읽기만 — 고친 값은 [단가산출로 저장]으로만 남김", "Read only — use [Save as]"],
|
|
} as const satisfies Record<string, readonly [string, string]>;
|
|
const t = (k: keyof typeof TEXT): string => TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0];
|
|
|
|
export interface RowSangeunOptions {
|
|
/** 그 일위대가 층(셈 길 · 참조 위 층 검사) */
|
|
scope: SkScope;
|
|
row: LaborRow;
|
|
/** 줄 이름(로직 키 · 이름) */
|
|
title: string;
|
|
/** 일위대가 머리(이름 · 「1 ㎡ 당」) */
|
|
head: string;
|
|
/** 이 줄 지금 수량 글(「수기 5 ㎥」) */
|
|
qty: string;
|
|
/** 줄 결과 단위(분모 · SK 단위 기본) */
|
|
unit: string;
|
|
readOnly: boolean;
|
|
/** [적용] — 그 줄 패치(null = 고침 없음) */
|
|
apply: (patch: LinePatch | null) => void;
|
|
/** [단가산출로 저장] 뒤 그 줄을 새 SK 참조 줄로(없으면 안 바꿈) */
|
|
replace?: (row: LaborRow) => void;
|
|
}
|
|
|
|
const same = (a: LinePatch | null, b: LinePatch | null): boolean =>
|
|
JSON.stringify(a) === JSON.stringify(b);
|
|
|
|
export function openRowSangeun(o: RowSangeunOptions): void {
|
|
let applied = cleanPatch(o.row.패치);
|
|
let patch = applied;
|
|
let ids: string[] | null = null;
|
|
let orig = new Map<string, SkLine>();
|
|
let calc: SkCalc | null = null;
|
|
let fail: string | null = null;
|
|
let view = "sheet";
|
|
let seq = 0;
|
|
let timer = 0;
|
|
const info = el("div", { className: "m02-std-sheet__body", attrs: { "data-sk-head": "" } });
|
|
const main = el("div", { className: "m02-std-sheet__body", attrs: { "data-sk-body": "" } });
|
|
const seg = createSegment({
|
|
options: [
|
|
{ value: "sheet", label: t("Sheet") },
|
|
{ value: "detail", label: t("Detail") },
|
|
],
|
|
value: view,
|
|
ariaLabel: t("Title"),
|
|
onChange: (v) => {
|
|
view = v;
|
|
draw();
|
|
},
|
|
});
|
|
|
|
const draw = (): void => {
|
|
const unit = o.unit || "식";
|
|
const one = calc?.["1단위"]?.계;
|
|
info.replaceChildren(
|
|
createText(o.head, "muted", 14),
|
|
createText(`${o.title} · ${t("Qty")} ${o.qty}`, "muted", 14),
|
|
...(one != null ? [createText(`${t("Per").replace("{u}", unit)} ${formatMoney(one)}`)] : []),
|
|
...(o.readOnly ? [createText(t("ReadOnly"), "hint")] : []),
|
|
...(calc?.짝잃음 ?? []).map((x) =>
|
|
createText(`${t("Lost")} ${x.cid} · ${x.까닭}`, "warning"),
|
|
),
|
|
);
|
|
if (!calc)
|
|
return void main.replaceChildren(createText(fail ?? t("Loading"), fail ? "error" : "muted"));
|
|
const base = calc.배 && calc.배 !== 1 ? `${calc.배} ${unit}` : `1 ${unit}`;
|
|
main.replaceChildren(
|
|
...(fail ? [createText(fail, "error")] : []),
|
|
...(view === "sheet"
|
|
? calcSheetRows({
|
|
묶음: sheetGroups(calc),
|
|
계: calc.기준?.계 ?? null,
|
|
sumLabel: `${t("Sum")} (${base})`,
|
|
})
|
|
: [
|
|
coverTable(calc, orig, patch, {
|
|
edit: true,
|
|
text: {
|
|
name: t("Name"),
|
|
qty: t("Poom"),
|
|
unit: t("Unit"),
|
|
price: t("Price"),
|
|
amount: t("Amount"),
|
|
was: t("Was"),
|
|
tag: t("Tag"),
|
|
empty: t("Empty"),
|
|
},
|
|
onCover: (cid, key, value) => {
|
|
patch = setCover(patch, cid, key, value);
|
|
recalc();
|
|
},
|
|
}),
|
|
]),
|
|
);
|
|
applyBtn.disabled = o.readOnly || same(patch, applied);
|
|
};
|
|
|
|
/** 서버 셈 — 늦게 온 답은 버림 */
|
|
const recalc = (): void => {
|
|
window.clearTimeout(timer);
|
|
timer = window.setTimeout(async () => {
|
|
const mine = ++seq;
|
|
try {
|
|
const got = await calcSangeun<SkCalc>(o.scope, lineDoc(o.row, ids, patch));
|
|
if (mine !== seq) return;
|
|
calc = got;
|
|
fail = null;
|
|
} catch (error) {
|
|
if (mine !== seq) return;
|
|
fail = error instanceof Error ? error.message : String(error);
|
|
}
|
|
draw();
|
|
}, 150);
|
|
};
|
|
|
|
let close = (): void => {};
|
|
const applyBtn = createButton({
|
|
label: t("Apply"),
|
|
variant: "filled",
|
|
onClick: () => {
|
|
o.apply(patch);
|
|
applied = patch;
|
|
close();
|
|
},
|
|
});
|
|
applyBtn.dataset.skApply = "";
|
|
const saveBtn = createButton({
|
|
label: t("ToSk"),
|
|
variant: "ghost",
|
|
onClick: async () => {
|
|
const name = (await askText({ message: t("AskName"), value: o.title }))?.trim();
|
|
if (!name) return;
|
|
const line: Record<string, unknown> = { ...o.row };
|
|
delete line.패치;
|
|
if (patch) line.패치 = patch;
|
|
try {
|
|
const got = await sangeunFromLine(
|
|
{ layer: "personal", owner: null },
|
|
line,
|
|
name,
|
|
o.unit || "식",
|
|
);
|
|
showToast(t("Saved").replace("{k}", got.이름), "success");
|
|
if (!o.replace) return;
|
|
o.replace({ 로직: "", 입력값: {}, 메모: null, 별칭: null, 수량: o.row.수량, ...got.줄 });
|
|
showToast(t("Replaced").replace("{k}", got.이름), "info");
|
|
applied = patch;
|
|
close();
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : String(error), "error");
|
|
}
|
|
},
|
|
});
|
|
saveBtn.dataset.skSave = "";
|
|
({ close } = openModal({
|
|
title: `${t("Title")} — ${o.title}`,
|
|
closeLabel: t("Close"),
|
|
width: "1100px",
|
|
wide: true,
|
|
actions: [seg.root, saveBtn, applyBtn],
|
|
beforeClose: () => same(patch, applied) || askConfirm({ message: t("Discard"), danger: true }),
|
|
mount: (body) => body.append(info, main),
|
|
}));
|
|
draw();
|
|
|
|
// 원래 값(그 일위대가 고침 없음) 먼저 — 고름 · 자재표시가 있으면 바탕 줄 정체부터
|
|
void (async () => {
|
|
try {
|
|
if (needsIds(o.row))
|
|
ids = (await calcSangeun<SkCalc>(o.scope, lineDoc(o.row, null, null))).정체 ?? null;
|
|
const base = await calcSangeun<SkCalc>(o.scope, lineDoc(o.row, ids, null));
|
|
orig = new Map(base.줄.map((x) => [x.cid, x]));
|
|
if (patch) return recalc();
|
|
calc = base;
|
|
} catch (error) {
|
|
fail = error instanceof Error ? error.message : String(error);
|
|
}
|
|
draw();
|
|
})();
|
|
}
|