- 단가산출 칸 = 서버 줄 판정(빈 확인) 블록 표: 판정 · 우리 금액 · 엑셀 금액(참고) · 근거 꼬리표 · 만든 SK · [확인] 칩(고를 줄 0 일 때만) · [줄] 펼침 · [보기]
- 줄 표(새 Hopyo_SkLines) = 묶음 · 이름 · 규격 · 매개값 · 상태 칩(엑셀 칠과 한 규칙 unitTone) · 꼬리표 · 방법 · 금액 · 차 · 고름(후보 · 찾기 · 율 기준 · 뺌 + 까닭 · 지움) · 엑셀 보기 뛰기
- 고름 · 확인 = 캐시 m02-hopyo-sk · [확인한 것 옮기기] = 몸 {확인: [{번, 고름}]} · 서버가 다시 셈 · 자동 확인 · 「모두 확인」 없음
- 순수 꼴 = 새 logic/Hopyo_SkPick · 길 = 새 Hopyo_SkApi(Hopyo_Api 안 늘림)
- 마법사 [끝내기] 막힘 글 앞에 「단가산출 확인 안 한 블록 n · 고를 줄 m」(Hopyo_Wizard 한 곳)
- 걷음: 옛 고름 대기 기초 고름 · 줄 비교(Compare.compareView · compareBrief · Api SangeunCompare · CompareLook · CSS) — 쓰는 곳 없음 · 못옮김 창 잇기 만들기(옛 바탕 몸 = 서버가 걷어 422) → 보기만 + 줄 찾기 단추 lineFind 로 다시 씀 · 안 쓰는 문구 39
- 낡은 시험 고침: test_746_2(줄 비교 · 못옮김 요소 단추 시험 걷음 — 기능 걷음) · test_729_3(candLabel · 옛 잇기 몸 기대값 걷음) · test_m02_hopyo_screen(sangeunStep 인자) · 새 test_778_5_sk_pick_screen + helper_778_5_sk_pick.cjs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
251 lines
9.5 KiB
TypeScript
251 lines
9.5 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Hopyo_WizardSk.ts
|
|
* 일위대가 불러오기 마법사 ② 단가산출 칸(PLAN 778-5 D-5 · 요구 621 · 622 · 623 · 624) — 모두 줄로만 · 자동 없음.
|
|
* · 열면 서버 줄 판정(`POST …/sangeun` 빈 확인 = 안 만듦 · D-3) → 블록 표: 단산N · 이름 · 판정(「옮길 수 있음」 ·
|
|
* 「고를 줄 n」 — 캐시 고름 겹침) · 우리 금액 · 엑셀 금액(참고) · 근거 꼬리표(가까운 기초 이름 · 참고) · 만든 SK ·
|
|
* [확인] 칩(고를 줄 0 일 때만 켜짐) · [줄] 펼침 = 줄 표(`Hopyo_SkLines`) · [보기] = 엑셀 보기 그 블록 첫 행
|
|
* · 고름 · 확인 = 캐시 `m02-hopyo-sk:{층}:{주인}:{키}`(`SkPick` 꼴) — 누를 때 서버에 안 감 · 자동 저장 없음
|
|
* · [확인한 것 옮기기 (n)] = 몸 `{확인: [{번, 고름}]}` → 서버가 M01 지금 값으로 다시 셈 · 단위 SK(확인 안 한 블록 안 만듦) ·
|
|
* 만든 블록은 확인에서 뺌 · 못만듦 = 띠(까닭 그대로) · 「모두 확인」 단추 없음
|
|
* · `count()` = 아직 안 옮긴 블록 · 고를 줄(마법사 [끝내기] 막힘 글)
|
|
* ========================================================================== */
|
|
|
|
import { createBadge } from "@ui/ui_template_badge";
|
|
import { createBanner } from "@ui/ui_template_banner";
|
|
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
|
import { t as L } from "@ui/ui_template_locale";
|
|
import { tableRow, tableShell, type Column } from "@ui/ui_template_table";
|
|
import { createText } from "@ui/ui_template_text";
|
|
import { createToggle } from "@ui/ui_template_toggle";
|
|
import { formatPrice } from "../common_util/common_util_money";
|
|
import type { HopyoScope } from "./M02_MasterTemplete_Hopyo_Api";
|
|
import { judgeSangeun } from "./M02_MasterTemplete_Hopyo_SkApi";
|
|
import { skLineTable } from "./M02_MasterTemplete_Hopyo_SkLines";
|
|
import {
|
|
GOOD,
|
|
canConfirm,
|
|
picksOf,
|
|
readDraft,
|
|
setPick,
|
|
setSure,
|
|
sureBody,
|
|
tidy,
|
|
verdictOf,
|
|
waiting,
|
|
type SkBlock,
|
|
type SkDraft,
|
|
type SkJudged,
|
|
} from "./logic/M02_MasterTemplete_Hopyo_SkPick";
|
|
|
|
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
|
const won = (v: number | null | undefined): string =>
|
|
v === null || v === undefined ? "—" : formatPrice(v);
|
|
|
|
export const skDraftKey = (scope: HopyoScope, key: string): string =>
|
|
`m02-hopyo-sk:${scope.layer}:${scope.owner ?? ""}:${key}`;
|
|
|
|
export interface SkStep {
|
|
root: HTMLElement;
|
|
/** 아직 안 옮긴 블록 · 고를 줄 — 판정 전 null */
|
|
count: () => { 블록: number; 줄: number } | null;
|
|
}
|
|
|
|
/** ② 단가산출 칸 — `파일` = 엑셀 보기 창 머리 · `onChange` = 판정 · 옮김 뒤(끝내기 글) */
|
|
export function sangeunStep(
|
|
scope: HopyoScope,
|
|
key: string,
|
|
파일: string,
|
|
onChange?: () => void,
|
|
): SkStep {
|
|
const dk = skDraftKey(scope, key);
|
|
const load = (): SkDraft => readDraft(sessionStorage.getItem(dk));
|
|
const save = (d: SkDraft): void => sessionStorage.setItem(dk, JSON.stringify(d));
|
|
let got: SkJudged | null = null;
|
|
let fails: SkJudged["못만듦"] = [];
|
|
const open = new Set<number>(); // 펼친 블록
|
|
const out = el("div", { className: "m02-hopyo", attrs: { "data-wiz-sk": "" } });
|
|
const again = createButton({ label: L("M02_SkJudge"), variant: "ghost", size: 28, onClick: () => void judge() }); // prettier-ignore
|
|
again.dataset.skJudge = "";
|
|
const move = createButton({ label: L("M02_SkMove").replace("{n}", "0"), variant: "filled", size: 28, disabled: true, onClick: () => void run() }); // prettier-ignore
|
|
move.title = L("M02_SkMoveTip");
|
|
move.dataset.skMove = "";
|
|
const root = el("div", {
|
|
className: "m02-hopyo",
|
|
children: [
|
|
createText(L("M02_SkHint"), "hint", 14),
|
|
out,
|
|
el("div", { className: "ui-toolbar", children: [move, again] }),
|
|
],
|
|
});
|
|
|
|
const sheet = (행: number): void =>
|
|
void import("./M02_MasterTemplete_Hopyo_Sheet")
|
|
.then((m) =>
|
|
m.openHopyoSheet({ scope, key, 파일, canWrite: true, target: scope, 단산행: 행 }),
|
|
)
|
|
.catch((error: unknown) => showToast(why(error), "error"));
|
|
|
|
async function judge(): Promise<void> {
|
|
again.disabled = true;
|
|
out.replaceChildren(createText(L("M02_SkJudging"), "muted", 14));
|
|
try {
|
|
got = await judgeSangeun(scope, key);
|
|
fails = [];
|
|
draw();
|
|
} catch (error) {
|
|
out.replaceChildren(createBanner({ tone: "warning", text: why(error) }));
|
|
} finally {
|
|
again.disabled = false;
|
|
onChange?.();
|
|
}
|
|
}
|
|
|
|
async function run(): Promise<void> {
|
|
if (!got) return;
|
|
const body = sureBody(load(), got.판정);
|
|
if (!body.length) return showToast(L("M02_SkSureFirst"), "warning");
|
|
move.disabled = true;
|
|
try {
|
|
const next = await judgeSangeun(scope, key, body);
|
|
const made = new Set(next.만듦.map((x) => x.번));
|
|
const d = load();
|
|
save({ ...d, 확인: d.확인.filter((n) => !made.has(n)) });
|
|
got = next;
|
|
fails = next.못만듦;
|
|
showToast(
|
|
L("M02_SkMoved")
|
|
.replace("{made}", String(made.size))
|
|
.replace("{bad}", String(fails.length)),
|
|
fails.length ? "warning" : "success",
|
|
);
|
|
draw();
|
|
} catch (error) {
|
|
showToast(why(error), "error");
|
|
draw();
|
|
} finally {
|
|
onChange?.();
|
|
}
|
|
}
|
|
|
|
const cols: Column[] = [
|
|
{ label: L("M02_SkColBlock"), width: "72px" },
|
|
{ label: L("M02_SangeunColName") },
|
|
{ label: L("M02_SangeunColUnit"), width: "56px" },
|
|
{ label: L("M02_SkColVerdict") },
|
|
{ label: L("M02_SkColOurs"), align: "end" },
|
|
{ label: L("M02_SkColExcel"), align: "end" },
|
|
{ label: L("M02_SkColBasis") },
|
|
{ label: L("M02_SkColMade"), width: "110px" },
|
|
{ label: L("M02_SkSure"), width: "72px" },
|
|
{ label: "", width: "96px" },
|
|
];
|
|
|
|
/** 블록 한 줄 + 펼친 줄 표 */
|
|
function blockRows(b: SkBlock, d: SkDraft): HTMLTableRowElement[] {
|
|
const picks = picksOf(d, b.번);
|
|
const word = verdictOf(b, picks);
|
|
const verdict = createBadge(word, word === GOOD ? "success" : "info");
|
|
verdict.dataset.skVerdict = String(b.번);
|
|
const basis = b.근거 ? createBadge(`${L("M02_SkBasisTag")} ${b.근거.이름 ?? b.근거.로직}`, "neutral") : "—"; // prettier-ignore
|
|
if (basis instanceof HTMLElement) basis.title = `${b.근거!.로직} · ${b.근거!.판정 ?? ""}`;
|
|
const made = b.만든 ? createBadge(b.만든, "success") : "—";
|
|
const ok = canConfirm(b, picks);
|
|
const sure = createToggle({
|
|
label: L("M02_SkSure"),
|
|
kind: "chip",
|
|
size: 24,
|
|
checked: d.확인.includes(b.번),
|
|
disabled: !ok,
|
|
onChange: (on) => {
|
|
save(setSure(load(), b, on));
|
|
draw();
|
|
},
|
|
});
|
|
sure.root.dataset.skSure = String(b.번);
|
|
sure.root.title = ok ? L("M02_SkSureTip") : L("M02_SkSureOff");
|
|
const lines = createButton({
|
|
label: `${L("M02_SkOpen")} ${b.줄.length}`,
|
|
size: 24,
|
|
variant: open.has(b.번) ? "filled" : "ghost",
|
|
onClick: () => {
|
|
if (open.has(b.번)) open.delete(b.번);
|
|
else open.add(b.번);
|
|
draw();
|
|
},
|
|
});
|
|
lines.dataset.skOpen = String(b.번);
|
|
const first = b.줄.find((l) => l.행 !== null)?.행 ?? null;
|
|
const view = first !== null ? createButton({ label: L("M02_SkSheet"), size: 24, variant: "link", onClick: () => sheet(first) }) : ""; // prettier-ignore
|
|
const head = tableRow(cols, [
|
|
`단산${b.번}`,
|
|
b.이름 ?? "",
|
|
b.단위 ?? "",
|
|
verdict,
|
|
won(b.우리금액),
|
|
won(b.엑셀금액),
|
|
basis,
|
|
made,
|
|
sure.root,
|
|
el("div", { className: "ui-toolbar", children: [lines, view] }),
|
|
]);
|
|
head.dataset.skBlock = String(b.번);
|
|
if (!open.has(b.번)) return [head];
|
|
const table = skLineTable({
|
|
scope,
|
|
block: b,
|
|
picks,
|
|
onPick: (at, p) => {
|
|
save(tidy(setPick(load(), b.번, at, p), got!.판정));
|
|
draw();
|
|
},
|
|
onSheet: sheet,
|
|
});
|
|
const sub = tableRow(cols, [{ content: table, colSpan: cols.length }]);
|
|
sub.className = "is-sub";
|
|
return [head, sub];
|
|
}
|
|
|
|
function draw(): void {
|
|
if (!got) return;
|
|
const d = tidy(load(), got.판정);
|
|
save(d);
|
|
const blocks = got.판정;
|
|
const good = blocks.filter((b) => canConfirm(b, picksOf(d, b.번))).length;
|
|
const made = blocks.filter((b) => b.만든).length;
|
|
const sum = L("M02_SkSummary")
|
|
.replace("{b}", String(blocks.length))
|
|
.replace("{ok}", String(good))
|
|
.replace("{wait}", String(blocks.length - good))
|
|
.replace("{made}", String(made))
|
|
.replace("{sure}", String(d.확인.length));
|
|
const { root: table, tbody } = tableShell(cols, { density: "data" });
|
|
for (const b of blocks) tbody.append(...blockRows(b, d));
|
|
const band = fails.length
|
|
? [
|
|
createBanner({
|
|
tone: "warning",
|
|
text: L("M02_SkFailed").replace("{n}", String(fails.length)),
|
|
details: fails.map((x) => `단산${x.번} ${x.이름 ?? ""} — ${x.까닭}`).join("\n"),
|
|
}),
|
|
]
|
|
: [];
|
|
out.replaceChildren(
|
|
createText(sum, "muted", 14),
|
|
...band,
|
|
blocks.length ? table : createText(L("M02_SkNone"), "muted", 14),
|
|
);
|
|
move.disabled = !d.확인.length;
|
|
move.querySelector(".ui-btn__label")!.textContent = L("M02_SkMove").replace(
|
|
"{n}",
|
|
String(d.확인.length),
|
|
);
|
|
onChange?.();
|
|
}
|
|
|
|
void judge();
|
|
return {
|
|
root,
|
|
count: () => (got ? waiting(load(), got.판정) : null),
|
|
};
|
|
}
|