Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Hopyo_WizardSk.ts
T
eomsangdonandClaude Sonnet 5.5 6732842a8e feat(746-2): 고르기 창 후보 블록 줄 비교 표 · 못옮김 · 안 이은 기계 줄 M01 요소 후보 단추
- 고름 대기 칸에 후보(앞 셋)마다 줄 비교 표(줄 · 엑셀 품 · 기초 품 · 꼴 배지) · 후보 고름 칸과 동기 · 칸 글에 같음 · 품 다름 수 · 판정 · 미리 고름 그대로
- 못옮김 잇기 창 · 안 이은 줄 창에 M01 요소 후보 셋 단추(누르면 이음 고름) · 안 이은 줄 창 기초 후보 칸에는 요소 후보가 안 섞이게 가름
- 새 파일 Hopyo_Compare.ts · 새 지킴 시험 test_746_2_hopyo_compare_screen

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 22:17:48 +09:00

239 lines
10 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Hopyo_WizardSk.ts
* 일위대가 불러오기 마법사 ② 단가산출 단계(PLAN 729-3 · 요구서 내역19 1장 7 · 2장 3) —
* · [단가산출 만들기] = 서버 `sangeun`(산근 블록마다 매칭 → 같으면 기초 · 다르면 SK = 기초 + 엑셀 차이 · 맞는 기초 없음 = 고름 대기)
* · 만든 줄 표 = 블록 · 이름 · 키 · 갈래(기초 · SK 차이 있음) · 이미 만든 블록은 「그대로」 수만
* · 고름 대기 표 = 블록마다 후보(판정 · 글) 고르기 칸 → [고른 기초로 만들기](바탕 = 고른 기초 · 엑셀 품 견줘 SK) ·
* 단위 성격 다른 후보(729-4 `계수`) = 계수 칸(가 1 = 나 몇) 함께 → `환산 {계수}`
* 후보마다 줄 비교 표(746-2 · `Hopyo_Compare` · 서버 `줄비교`) — 고르기 칸 아래 · 후보 단추로 바꿔 봄
* · 못옮김(729-4) = 만든 줄마다 수 · [잇기] = 못옮김 잇기 창(`Hopyo_Lost`) · 만든 줄은 이 칸이 모아 둠(누를 때마다 더함) ·
* 이미 만든 SK 의 남은 못옮김(729-5 `남은못옮김`)도 같은 표
* · 같은 내용(733-1) = 갈래 칸 꼬리표 「같은 것 씀」(이 층 같은 SK 를 씀) · 「위 층에 같은 것 — 연결 권함」(회사 · 마스터 · 알림만) ·
* 수는 요약 글에
* · 별칭 제안 = 이 층 별칭 장부에 남음 · 모두에게 쓰이려면 시스템 관리자가 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 { compareBrief, compareView } from "./M02_MasterTemplete_Hopyo_Compare";
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>(); // 이 칸에서 만든 줄(누를 때마다 더함)
const reused = new Set<number>(); // 733-1 같은 것 씀 블록
const above = new Map<number, string[]>(); // 733-1 위 층 같은 것 「층 키」
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)) +
(got.같은것씀?.length || got.위층같음?.length
? L("M02_WizSkSameSum")
.replace("{same}", String(got.같은것씀?.length ?? 0))
.replace("{above}", String(new Set(got.위층같음?.map((x) => x.번)).size))
: "");
/** 갈래 칸 — 갈래 + 같은 것 씀 · 위 층 같은 것 꼬리표 */
const kindCell = (x: Made): HTMLElement => {
const tags: HTMLElement[] = [kindBadge(x.갈래)];
if (reused.has(x.번)) {
const tag = createBadge(L("M02_WizSkSame"), "info");
tag.title = L("M02_WizSkSameTip");
tag.dataset.wizSame = String(x.번);
tags.push(tag);
}
const up = above.get(x.번);
if (up?.length) {
const tag = createBadge(L("M02_WizSkAbove"), "warning");
tag.title = up.join(" · ");
tag.dataset.wizAbove = String(x.번);
tags.push(tag);
}
return el("div", { className: "ui-toolbar", children: tags });
};
/** 못옮김 칸 — 수 + [잇기](남은 것 있을 때) */
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);
for (const x of got.같은것씀 ?? []) reused.add(x.번);
for (const x of got.위층같음 ?? []) {
const at = x.층 === "system" ? L("M02_PickerSystem") : L("M02_PickerCompany");
above.set(x.번, [...(above.get(x.번) ?? []), `${at} ${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: "260px" },
{ label: L("M02_LostCol"), width: "120px" },
],
[...made.values()]
.sort((a, b) => a.번 - b.번)
.map((x) => [`단산${x.번}`, x.이름, x.키, kindCell(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: "62%" },
],
got.고름대기.map((x) => {
const pick = createSelectField({
options: [{ value: "", text: L("M02_WizSkNoPick") }].concat(
x.후보.map((c) => ({
value: c.로직,
text: candLabel(c) + (c.줄비교 ? ` · ${compareBrief(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.나} ?` : "";
};
const cmp = compareView(x.후보);
pick.select.addEventListener("change", () => {
need();
cmp?.show(pick.select.value);
});
need();
coefs.set(x.번, coef.input);
return [`단산${x.번}`, x.이름, x.단위 ?? "", x.까닭, el("div", { children: [el("div", { className: "ui-toolbar", children: [pick.root, coef.root] }), ...(cmp ? [cmp.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 };
}