Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Labor_SangeunModal.ts
T
eomsangdonandClaude Opus 5.5 04f3489481 merge(717-2b): origin/main_laptop_1 합침 — Sangeun_Cover.ts 같은 이름 새 파일 충돌(AA) 풂 · 두 표 한 파일
- 셈 길 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>
2026-11-01 17:27:50 +09:00

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();
})();
}