Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Hopyo_WizardSk.ts
T
eomsangdonandClaude Opus 5.5 fe78849463 feat(729-3): 단가산출 못옮김 잇기 창 · 단위 성격 다른 후보 계수 칸 · 남은 줄 표 못옮김 단추(729-4 이음)
- 새 Hopyo_Lost: 못옮김 줄마다 찾기(노임 LB · 자재 MT) · 기계(EQ) · 우리에 없음(뺌) → 캐시 m02-hopyo-lost 모두를 [잇기 만들기] 한 번에(서버가 부를 때마다 그 블록 잇기 전부로 다시 셈) · 바탕 = 그 SK 문서 바탕(로직 · 입력값 · 환산)
- 단가산출 칸: 만든 줄 표 못옮김 열 · [잇기 (n)] · 만든 줄 모아 둠 · 고름 대기 후보 계수(성격 다른 단위) = 계수 칸 → 환산 {계수} · 729-5 남은못옮김 칸 미리 받음
- 안 이은 줄 표: 못옮김 남은 SK 를 쓰는 줄(단산 · SK · 못옮김) = [못옮김 잇기 (n)]
- 시험 test_729_3_hopyo_left_screen 소스 확인 더함(계수 · 잇기 모두 · 바탕)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 11:57:47 +09:00

197 lines
8.3 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Hopyo_WizardSk.ts
* 일위대가 불러오기 마법사 ② 단가산출 단계(PLAN 729-3 · 요구서 내역19 1장 7 · 2장 3) —
* · [단가산출 만들기] = 서버 `sangeun`(산근 블록마다 매칭 → 같으면 기초 · 다르면 SK = 기초 + 엑셀 차이 · 맞는 기초 없음 = 고름 대기)
* · 만든 줄 표 = 블록 · 이름 · 키 · 갈래(기초 · SK 차이 있음) · 이미 만든 블록은 「그대로」 수만
* · 고름 대기 표 = 블록마다 후보(판정 · 글) 고르기 칸 → [고른 기초로 만들기](바탕 = 고른 기초 · 엑셀 품 견줘 SK) ·
* 단위 성격 다른 후보(729-4 `계수`) = 계수 칸(가 1 = 나 몇) 함께 → `환산 {계수}`
* · 못옮김(729-4) = 만든 줄마다 수 · [잇기] = 못옮김 잇기 창(`Hopyo_Lost`) · 만든 줄은 이 칸이 모아 둠(누를 때마다 더함) ·
* 이미 만든 SK 의 남은 못옮김(729-5 `남은못옮김`)도 같은 표
* · 별칭 제안 = 이 층 별칭 장부에 남음 · 모두에게 쓰이려면 시스템 관리자가 M01 › 별칭 장부에서 승인(링크 · 716 길 그대로)
* 계산 · 판정은 서버 — 화면은 보이기 · 고름만 · 자동 저장 없음(누를 때만 서버).
* ========================================================================== */
import { ROUTES } from "@config/config_frontend";
import { createBadge } from "@ui/ui_template_badge";
import {
createButton,
createInputField,
createSelectField,
el,
showToast,
} from "@ui/ui_template_elements";
import { t as L } from "@ui/ui_template_locale";
import { createTable } from "@ui/ui_template_table";
import { createText } from "@ui/ui_template_text";
import {
makeSangeun,
type HopyoScope,
type SangeunBuilt,
type SangeunPick,
} from "./M02_MasterTemplete_Hopyo_Api";
import { openLost } from "./M02_MasterTemplete_Hopyo_Lost";
type Made = SangeunBuilt["만듦"][number];
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
/** 고름 대기 후보 글 — 「이름 [단위] · 판정 · 글」 */
export const candLabel = (c: { 이름: string; 단위: string; 판정: string; 글: string }): string =>
`${c.이름} [${c.단위}] · ${c.판정}${c.글 && c.글 !== c.판정 ? ` · ${c.글}` : ""}`;
/** 만든 갈래 꼬리표 — 기초 = 초록 · SK = 차이 있음 */
const kindBadge = (kind: string): HTMLElement =>
kind === "SK"
? createBadge(L("M02_WizSkDiff"), "accent")
: createBadge(L("M02_WizSkBase"), "success");
/** ② 단가산출 단계 칸 — `root` 를 마법사 본 칸에 붙임 */
export function sangeunStep(scope: HopyoScope, key: string): { root: HTMLElement } {
const out = el("div", { className: "m02-hopyo", attrs: { "data-wiz-sk": "" } });
const make = createButton({
label: L("M02_WizSkMake"),
variant: "filled",
onClick: () => void run(),
});
make.title = L("M02_WizSkMakeTip");
make.dataset.hopyoSangeunMake = "";
const root = el("div", {
className: "m02-hopyo",
children: [el("div", { className: "ui-toolbar", children: [make] }), out],
});
const made = new Map<number, Made>(); // 이 칸에서 만든 줄(누를 때마다 더함)
async function run(picks?: SangeunPick): Promise<void> {
make.disabled = true;
try {
const got = await makeSangeun(scope, key, picks);
draw(got);
showToast(summary(got), got.고름대기.length ? "warning" : "success");
} catch (error) {
showToast(why(error), "error");
} finally {
make.disabled = false;
}
}
const summary = (got: SangeunBuilt): string =>
L("M02_WizSkSummary")
.replace("{b}", String(got.블록))
.replace("{base}", String(got.기초))
.replace("{sk}", String(got.SK))
.replace("{keep}", String(got.그대로))
.replace("{wait}", String(got.고름대기.length));
/** 못옮김 칸 — 수 + [잇기](남은 것 있을 때) */
const lostCell = (x: Made, got: SangeunBuilt): HTMLElement => {
const n = x.못옮김?.length ?? 0;
if (!n) return el("span", { text: "0" });
const go = createButton({
label: `${L("M02_LostOpen")} (${n})`,
size: 24,
onClick: () =>
openLost({ scope, key, 번: x.번, sk: x.키, 이름: x.이름, lost: x.못옮김 ?? [],
done: (next) => draw({ ...got, 만듦: next.만듦, 별칭: got.별칭 }) }), // prettier-ignore
});
go.dataset.wizLost = String(x.번);
return go;
};
function draw(got: SangeunBuilt): void {
for (const x of got.남은못옮김 ?? []) if (!made.has(x.번)) made.set(x.번, { ...x, 갈래: "SK" });
for (const x of got.만듦) made.set(x.번, x);
const table = createTable(
[
{ label: L("M02_WizSkBlock"), width: "80px" },
{ label: L("M02_SangeunColName") },
{ label: L("M02_WizSkKey"), width: "110px" },
{ label: L("M02_WizSkKind"), width: "120px" },
{ label: L("M02_LostCol"), width: "120px" },
],
[...made.values()]
.sort((a, b) => a.번 - b.번)
.map((x) => [`단산${x.번}`, x.이름, x.키, kindBadge(x.갈래), lostCell(x, got)]),
{ density: "data", empty: L("M02_WizSkNoneMade") },
);
const selects = new Map<number, HTMLSelectElement>();
const coefs = new Map<number, HTMLInputElement>();
const wait = createTable(
[
{ label: L("M02_WizSkBlock"), width: "80px" },
{ label: L("M02_SangeunColName") },
{ label: L("M02_SangeunColUnit"), width: "64px" },
{ label: L("M02_LeftWhy") },
{ label: L("M02_LeftBase"), width: "40%" },
],
got.고름대기.map((x) => {
const pick = createSelectField({
options: [{ value: "", text: L("M02_WizSkNoPick") }].concat(
x.후보.map((c) => ({ value: c.로직, text: candLabel(c) })),
),
compact: true,
size: 24,
ariaLabel: `${L("M02_LeftBase")} 단산${x.번}`,
});
pick.select.dataset.wizWait = String(x.번);
selects.set(x.번, pick.select);
const coef = createInputField({ type: "number", min: 0, compact: true, size: 24,
placeholder: L("M02_WizSkCoef") }); // prettier-ignore
coef.input.dataset.wizCoef = String(x.번);
const need = (): void => {
const c = x.후보.find((y) => y.로직 === pick.select.value)?.계수;
coef.root.hidden = !c;
coef.input.title = c ? `${c.가} 1 = ${c.나} ?` : "";
};
pick.select.addEventListener("change", need);
need();
coefs.set(x.번, coef.input);
return [`단산${x.번}`, x.이름, x.단위 ?? "", x.까닭, el("div", { className: "ui-toolbar", children: [pick.root, coef.root] })]; // prettier-ignore
}),
{ density: "data", empty: L("M02_WizSkNoWait") },
);
const again = createButton({
label: L("M02_WizSkRemake"),
variant: "filled",
size: 28,
onClick: () => {
const picks: SangeunPick = {};
for (const [no, s] of selects) {
if (!s.value) continue;
const c = Number(coefs.get(no)?.value);
const need = got.고름대기.find((x) => x.번 === no)?.후보.find((y) => y.로직 === s.value)?.계수; // prettier-ignore
if (need && !(c > 0)) return showToast(L("M02_WizSkCoefNeed").replace("{n}", String(no)), "warning"); // prettier-ignore
picks[no] = { 로직: s.value, ...(need ? { 환산: { 계수: c } } : {}) };
}
if (!Object.keys(picks).length) return showToast(L("M02_WizSkPickFirst"), "warning");
void run(picks);
},
});
again.dataset.wizRemake = "";
const ledger = createButton({
label: L("M02_WizSkLedger"),
variant: "link",
onClick: () => void window.open(`#/${ROUTES.M01_MASTER_DATA}`, "_blank"),
});
out.replaceChildren(
createText(summary(got), "muted", 14),
table,
...(got.고름대기.length
? [
createText(L("M02_WizSkWaitHint"), "hint", 14),
wait,
el("div", { className: "ui-toolbar", children: [again] }),
]
: []),
el("div", {
className: "ui-toolbar",
children: [
createText(L("M02_WizSkAlias").replace("{n}", String(got.별칭 ?? 0)), "muted", 14),
ledger,
],
}),
);
}
return { root };
}