Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ACSGaHdLgnkEkoA4LDtMU
629 lines
23 KiB
TypeScript
629 lines
23 KiB
TypeScript
/* =============================================================================
|
|
* M01_LB_Wizard.ts
|
|
* 로직 만들기 판 — 단계 안내 6단계 · openWizard() → LbLogic | null(취소) · 저장은 Entry 몫
|
|
* ① 어느 품셈 절 ② 표 칸 역할 ③ 결과 단위 ④ 계산(Calc.ts) ⑤ 항목 연결(Calc.ts) ⑥ 이름 · 결과 종류
|
|
* 답은 Build.ts 가 로직 한 줄로 짜고 Blocks.readLogic 이 블록으로 읽음 · 답할 때마다 곁에 예시값
|
|
* 글 [한, 영] 짝은 아래 TEXT · 용어는 정함 9 · 이 폴더째 지워도 됨
|
|
* ========================================================================== */
|
|
|
|
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
|
import {
|
|
createButton,
|
|
createInputField,
|
|
createSelectField,
|
|
el,
|
|
showToast,
|
|
} from "@ui/ui_template_elements";
|
|
import { openModal } from "@ui/ui_template_modal";
|
|
import { type CalcAnswer } from "../M01_MasterData_UI_Logic_Api";
|
|
import { calcLater, plainError } from "../EasyCalc/M01_EasyCalc_Api";
|
|
import { readLogic, type LbLogic } from "./M01_LB_Blocks";
|
|
import { autoLinks, stepCalc, stepLink } from "./M01_LB_Wizard_Calc";
|
|
import {
|
|
buildRow,
|
|
colsOf,
|
|
trialOf,
|
|
unitOfBase,
|
|
type Role,
|
|
type WAnswers,
|
|
type WCtx,
|
|
type WTable,
|
|
} from "./M01_LB_Wizard_Build";
|
|
import {
|
|
get,
|
|
tablePreview,
|
|
toTable,
|
|
type SectionItem,
|
|
type TableHead,
|
|
} from "./M01_LB_Wizard_Parts";
|
|
import "./M01_LB_Wizard.css";
|
|
|
|
const TEXT = {
|
|
Title: ["로직 만들기 — 단계 안내", "Build a logic — step by step"],
|
|
Close: ["닫기", "Close"],
|
|
Back: ["← 뒤로", "← Back"],
|
|
Next: ["다음 →", "Next →"],
|
|
Make: ["만들기", "Create"],
|
|
StepOf: ["{n} / 6 단계", "Step {n} of 6"],
|
|
Q1: ["어느 품셈 절인가요?", "Which section of the standard?"],
|
|
Q2: ["표의 칸마다 무엇인가요?", "What is each column?"],
|
|
Q3: ["결과는 무슨 단위인가요?", "What unit is the result?"],
|
|
Q4: ["더하거나 나눌 것이 있나요?", "Anything to add or divide?"],
|
|
Q5: ["값은 어느 항목으로 가나요?", "Where does each value go?"],
|
|
Q6: ["이름과 결과 종류", "Name and kind of result"],
|
|
Book: ["적용 품셈", "Standard"],
|
|
Search: ["절 번호나 이름을 쳐서 찾기 — 예: 13-4-1 메쌓기", "Search — e.g. 13-4-1"],
|
|
Loading: ["찾는 중…", "Searching…"],
|
|
NoSection: ["찾는 절 없음", "No section"],
|
|
SecLine: ["표 {t} 개 · 만든 로직 {l} 개", "{t} tables · {l} logics"],
|
|
Tables: ["이 절의 표 — 하나를 고름", "Tables in this section — pick one"],
|
|
TableLine: [
|
|
"{b} · 조건 {c} · 결과 {v} · {n} 줄",
|
|
"{b} · {c} conditions · {v} results · {n} rows",
|
|
],
|
|
Notes: ["주", "Notes"],
|
|
Preview: ["원문 표 미리 보기", "Table preview"],
|
|
RoleHint: [
|
|
"칸 머리를 누르면 조건 → 고정 → 결과 로 바뀜 · 조건 칸의 선택 목록은 표 줄에서 저절로 만듦",
|
|
"Press a header to change its role",
|
|
],
|
|
Cond: ["조건", "Condition"],
|
|
Fixed: ["고정", "Fixed"],
|
|
Result: ["결과", "Result"],
|
|
Unused: ["쓰지 않음", "Unused"],
|
|
Choices: ["선택 목록", "Choices"],
|
|
Free: ["숫자 입력", "Number input"],
|
|
PickTable: ["표를 하나 골라 주세요", "Pick a table"],
|
|
NeedResult: ["결과 칸이 하나는 있어야 함", "At least one result column"],
|
|
FixedVal: ["고정값", "Fixed value"],
|
|
BaseUnit: ["표 기준 「{b}」", "Table base “{b}”"],
|
|
UnitPick: ["결과단위", "Result unit"],
|
|
Name: ["이름", "Name"],
|
|
KindMoney: ["1 {p} 당 금액", "Amount per {p}"],
|
|
KindValue: ["다른 로직이 쓸 값", "A value other logics use"],
|
|
ValueCol: ["내보낼 값(결과 칸)", "Value to give"],
|
|
ValueUnit: ["값의 단위", "Unit of the value"],
|
|
NeedName: ["이름을 적어 주세요", "Enter a name"],
|
|
Sample: ["예시값", "Sample values"],
|
|
SampleHint: ["값을 바꾸면 곁의 결과가 바로 바뀜", "Change a value to see the result"],
|
|
Waiting: ["계산 중…", "Calculating…"],
|
|
AmountPer: ["1 {p} 당 약 {m} 원", "About {m} won per {p}"],
|
|
ValueIs: ["{c} = {v} {u}", "{c} = {v} {u}"],
|
|
NotYet: ["아직 계산할 것이 없음", "Nothing to calculate yet"],
|
|
Fail: ["읽지 못함", "Could not read"],
|
|
} as const;
|
|
|
|
type TKey = keyof typeof TEXT;
|
|
const t = (key: TKey, v: Record<string, string | number> = {}): string =>
|
|
Object.entries(v).reduce(
|
|
(s, [k, x]) => s.replace(`{${k}}`, String(x)),
|
|
TEXT[key][currentLanguageIndex] as string,
|
|
);
|
|
|
|
const fmt = (n: number): string => Number(n.toPrecision(6)).toLocaleString("ko-KR");
|
|
|
|
const LBL = { preview: t("Preview"), notes: t("Notes") };
|
|
const BOOKS = ["산림품셈", "건설품셈"];
|
|
const UNITS = ["㎡", "㎥", "m", "km", "ha", "개소", "개", "ton", "본", "100㎡", "100m", "10m"];
|
|
|
|
const emptyTable = (): WTable => ({
|
|
key: "",
|
|
이름: "",
|
|
기준: "",
|
|
출처: "",
|
|
조건: {},
|
|
값칸: {},
|
|
줄: [],
|
|
주: [],
|
|
});
|
|
|
|
/** 단계 안내 — 끝나면 LbLogic · 취소(닫기 · Esc)는 null */
|
|
export function openWizard(): Promise<LbLogic | null> {
|
|
return new Promise((resolve) => {
|
|
let settled = false;
|
|
const finish = (r: LbLogic | null): void => {
|
|
if (!settled) resolve(r);
|
|
settled = true;
|
|
};
|
|
const a: WAnswers = {
|
|
book: BOOKS[0],
|
|
section: "",
|
|
table: emptyTable(),
|
|
roles: {},
|
|
fixed: {},
|
|
options: {},
|
|
resultUnit: "원/㎡",
|
|
mods: [],
|
|
links: [],
|
|
extras: [],
|
|
name: "",
|
|
kind: "금액",
|
|
valueCol: "",
|
|
};
|
|
const ctx: WCtx = { a, sectionTables: [], changed: () => runPreview() };
|
|
let step = 1;
|
|
let sectionTitle = "";
|
|
let trial: Record<string, unknown> = {};
|
|
let linked = false;
|
|
const main = el("div", { className: "m01-lbw__main" });
|
|
const side = el("aside", { className: "m01-lbw__side" });
|
|
const nav = el("div", { className: "m01-lbw__nav" });
|
|
const ask = el("h4", { className: "m01-lbw__ask" });
|
|
const sample = el("div", { className: "m01-lbw__sample" });
|
|
const result = el("p", { className: "m01-lbw__result", attrs: { "aria-live": "polite" } });
|
|
side.append(
|
|
el("h5", { text: t("Sample") }),
|
|
el("p", { className: "m01-lbw__muted", text: t("SampleHint") }),
|
|
sample,
|
|
result,
|
|
);
|
|
|
|
/* ── 예시값 ── */
|
|
const previewRow = () => {
|
|
const money = step >= 5 && a.kind === "금액" && a.links.some((l) => l.kind !== "없음");
|
|
const col = a.valueCol || colsOf(a, "결과")[0];
|
|
const row = buildRow({ ...a, kind: money ? "금액" : "값", valueCol: col });
|
|
return { row, money, col };
|
|
};
|
|
let paintedFor = "";
|
|
const runPreview = (): void => {
|
|
if (step < 2 || !colsOf(a, "결과").length) {
|
|
sample.replaceChildren();
|
|
result.textContent = t("NotYet");
|
|
return;
|
|
}
|
|
const { row, money, col } = previewRow();
|
|
const defaults = trialOf(a, row);
|
|
trial = Object.fromEntries(
|
|
row.입력.map((i) => [i.이름, i.이름 in trial ? trial[i.이름] : defaults[i.이름]]),
|
|
);
|
|
const sig = JSON.stringify(row.입력);
|
|
if (sig !== paintedFor) {
|
|
paintedFor = sig;
|
|
sample.replaceChildren(
|
|
...row.입력.map((i) => {
|
|
const label = el("span", { className: "m01-lbw__muted", text: i.이름 });
|
|
if (i.고르기?.length) {
|
|
const f = createSelectField({
|
|
options: i.고르기.map((v) => ({ value: String(v), text: String(v) })),
|
|
value: String(trial[i.이름]),
|
|
compact: true,
|
|
onChange: (v) => (
|
|
(trial[i.이름] = i.고르기!.find((x) => String(x) === v)),
|
|
runPreview()
|
|
),
|
|
});
|
|
return el("label", { className: "m01-lbw__line", children: [label, f.root] });
|
|
}
|
|
const f = createInputField({
|
|
type: "text",
|
|
value: String(trial[i.이름] ?? ""),
|
|
compact: true,
|
|
});
|
|
f.input.addEventListener("input", () => {
|
|
const n = Number(f.input.value);
|
|
trial[i.이름] = f.input.value.trim() !== "" && Number.isFinite(n) ? n : f.input.value;
|
|
runPreview();
|
|
});
|
|
return el("label", { className: "m01-lbw__line", children: [label, f.root] });
|
|
}),
|
|
);
|
|
}
|
|
result.textContent = t("Waiting");
|
|
calcLater(row, trial, (ans: CalcAnswer | null, plain: string | null) => {
|
|
if (!ans?.ok) return void (result.textContent = plain ?? t("Fail"));
|
|
const per = a.resultUnit.replace(/^원\//, "");
|
|
const v = money ? (ans.sums?.["계"] ?? Number(ans.result ?? 0)) : Number(ans.result);
|
|
result.textContent = money
|
|
? t("AmountPer", { p: per, m: Math.round(v).toLocaleString("ko-KR") })
|
|
: t("ValueIs", { c: col, v: fmt(v), u: a.table.값칸[col] ?? "" });
|
|
});
|
|
};
|
|
|
|
/* ── 1단계 절 · 표 ── */
|
|
const stepSection = (): (() => string | null) => {
|
|
const bookBox = createSelectField({
|
|
label: t("Book"),
|
|
options: BOOKS.map((b) => ({ value: b, text: b })),
|
|
value: a.book,
|
|
compact: true,
|
|
onChange: (v) => ((a.book = v), load()),
|
|
});
|
|
const q = createInputField({ type: "search", placeholder: t("Search"), compact: true });
|
|
const list = el("div", { className: "m01-lbw__list" });
|
|
const tables = el("div", { className: "m01-lbw__tables" });
|
|
const preview = el("div", { className: "m01-lbw__preview" });
|
|
let seq = 0;
|
|
const load = async (): Promise<void> => {
|
|
const mine = ++seq;
|
|
list.replaceChildren(el("p", { className: "m01-lbw__muted", text: t("Loading") }));
|
|
try {
|
|
const r = await get<{ sections: SectionItem[] }>("/sections", {
|
|
book: a.book,
|
|
q: q.input.value.trim(),
|
|
limit: "60",
|
|
});
|
|
if (mine !== seq) return;
|
|
list.replaceChildren(
|
|
...(r.sections.length
|
|
? r.sections.map((s) => {
|
|
const b = el("button", {
|
|
className: `m01-lbw__row${a.section === s.section && sectionTitle === s.title ? " is-on" : ""}`,
|
|
attrs: { type: "button" },
|
|
children: [
|
|
el("strong", { text: `${s.section} ${s.title}` }),
|
|
el("span", {
|
|
className: "m01-lbw__muted",
|
|
text: `${s.chapter} · ${t("SecLine", { t: s.tables, l: s.logics })}`,
|
|
}),
|
|
],
|
|
});
|
|
b.addEventListener("click", () => void pickSection(s));
|
|
return b;
|
|
})
|
|
: [el("p", { className: "m01-lbw__muted", text: t("NoSection") })]),
|
|
);
|
|
} catch (e) {
|
|
if (mine === seq) showToast(plainError(e), "error");
|
|
}
|
|
};
|
|
const pickSection = async (s: SectionItem): Promise<void> => {
|
|
a.section = s.section;
|
|
sectionTitle = s.title;
|
|
a.name = a.name || s.title;
|
|
try {
|
|
const heads: TableHead[] = [];
|
|
for (const group of ["소요량", "계수"])
|
|
heads.push(
|
|
...(
|
|
await get<{ tables: TableHead[] }>("/tables", {
|
|
group,
|
|
section: s.section,
|
|
sub: a.book,
|
|
size: "100",
|
|
})
|
|
).tables,
|
|
);
|
|
ctx.sectionTables = [];
|
|
tables.replaceChildren(
|
|
el("h5", { text: t("Tables") }),
|
|
...heads.map((h) => {
|
|
const b = el("button", {
|
|
className: `m01-lbw__row${a.table.key === h.키 ? " is-on" : ""}`,
|
|
attrs: { type: "button" },
|
|
children: [
|
|
el("strong", { text: h.이름 }),
|
|
el("span", {
|
|
className: "m01-lbw__muted",
|
|
text: t("TableLine", {
|
|
b: h.기준 || "—",
|
|
c: Object.keys(h.조건).length,
|
|
v: Object.keys(h.값칸).length,
|
|
n: h.count,
|
|
}),
|
|
}),
|
|
],
|
|
});
|
|
b.addEventListener("click", () => void pickTable(h, heads));
|
|
return b;
|
|
}),
|
|
);
|
|
if (heads.length === 1) await pickTable(heads[0], heads);
|
|
main.dispatchEvent(new Event("m01lbw"));
|
|
} catch (e) {
|
|
showToast(plainError(e), "error");
|
|
}
|
|
};
|
|
const pickTable = async (h: TableHead, heads: TableHead[]): Promise<void> => {
|
|
const [full, opt] = await Promise.all([
|
|
get<{ table: WTable & { 키: string } }>("/table", { file: h.file, key: h.키 }),
|
|
get<{ 값: Record<string, (string | number)[]> }>("/table/options", {
|
|
file: h.file,
|
|
key: h.키,
|
|
}),
|
|
]);
|
|
a.table = toTable(full.table, h.file);
|
|
a.roles = Object.fromEntries([
|
|
...Object.keys(a.table.조건).map((c) => [c, "조건" as Role]),
|
|
...Object.keys(a.table.값칸).map((c) => [c, "결과" as Role]),
|
|
]);
|
|
a.options = opt.값 ?? {};
|
|
a.fixed = {};
|
|
a.links = [];
|
|
a.mods = [];
|
|
a.resultUnit = unitOfBase(a.table.기준);
|
|
linked = false;
|
|
ctx.sectionTables = await Promise.all(
|
|
heads
|
|
.filter((x) => x.키 !== h.키)
|
|
.map(async (x) =>
|
|
toTable(
|
|
(
|
|
await get<{ table: WTable & { 키: string } }>("/table", {
|
|
file: x.file,
|
|
key: x.키,
|
|
})
|
|
).table,
|
|
x.file,
|
|
),
|
|
),
|
|
);
|
|
tables
|
|
.querySelectorAll(".m01-lbw__row")
|
|
.forEach((n, i) => n.classList.toggle("is-on", heads[i]?.키 === h.키));
|
|
preview.replaceChildren(tablePreview(a.table, LBL));
|
|
renderNav();
|
|
};
|
|
let timer = 0;
|
|
q.input.addEventListener(
|
|
"input",
|
|
() => (window.clearTimeout(timer), (timer = window.setTimeout(load, 250))),
|
|
);
|
|
main.replaceChildren(bookBox.root, q.root, list, tables, preview);
|
|
void load();
|
|
if (a.table.key) preview.replaceChildren(tablePreview(a.table, LBL));
|
|
return () => (a.table.key ? null : t("PickTable"));
|
|
};
|
|
|
|
/* ── 2단계 칸 역할 ── */
|
|
const stepRoles = (): (() => string | null) => {
|
|
const box = el("div", { className: "m01-lbw__roles" });
|
|
const draw = (): void => {
|
|
const cols = [...Object.keys(a.table.조건), ...Object.keys(a.table.값칸)];
|
|
const chips = cols.map((c) => {
|
|
const isCond = c in a.table.조건;
|
|
const role = a.roles[c];
|
|
const b = el("button", {
|
|
className: `m01-lbw__role is-${role}`,
|
|
attrs: { type: "button", title: t("RoleHint") },
|
|
children: [
|
|
el("strong", { text: c }),
|
|
el("span", {
|
|
text:
|
|
role === "조건"
|
|
? t("Cond")
|
|
: role === "고정"
|
|
? t("Fixed")
|
|
: role === "결과"
|
|
? t("Result")
|
|
: t("Unused"),
|
|
}),
|
|
],
|
|
});
|
|
b.addEventListener("click", () => {
|
|
a.roles[c] = isCond
|
|
? role === "조건"
|
|
? "고정"
|
|
: "조건"
|
|
: role === "결과"
|
|
? "쓰지 않음"
|
|
: "결과";
|
|
if (a.roles[c] === "고정" && a.fixed[c] === undefined)
|
|
a.fixed[c] = a.options[c]?.[0] ?? "";
|
|
ctx.changed();
|
|
draw();
|
|
});
|
|
const extra: HTMLElement[] = [];
|
|
if (role === "조건")
|
|
extra.push(
|
|
el("span", {
|
|
className: "m01-lbw__muted",
|
|
text:
|
|
a.options[c]?.length && a.table.조건[c] !== "범위"
|
|
? `${t("Choices")}: ${a.options[c].join(" · ")}`
|
|
: t("Free"),
|
|
}),
|
|
);
|
|
if (role === "고정") {
|
|
const list = a.options[c] ?? [];
|
|
if (list.length)
|
|
extra.push(
|
|
createSelectField({
|
|
options: list.map((v) => ({ value: String(v), text: String(v) })),
|
|
value: String(a.fixed[c]),
|
|
compact: true,
|
|
ariaLabel: t("FixedVal"),
|
|
onChange: (v) => (
|
|
(a.fixed[c] = list.find((x) => String(x) === v) ?? v),
|
|
ctx.changed()
|
|
),
|
|
}).root,
|
|
);
|
|
else {
|
|
const f = createInputField({
|
|
type: "text",
|
|
value: String(a.fixed[c] ?? ""),
|
|
compact: true,
|
|
});
|
|
f.input.addEventListener(
|
|
"input",
|
|
() => (
|
|
(a.fixed[c] =
|
|
Number.isFinite(Number(f.input.value)) && f.input.value.trim() !== ""
|
|
? Number(f.input.value)
|
|
: f.input.value),
|
|
ctx.changed()
|
|
),
|
|
);
|
|
extra.push(f.root);
|
|
}
|
|
}
|
|
return el("div", { className: "m01-lbw__rolebox", children: [b, ...extra] });
|
|
});
|
|
box.replaceChildren(
|
|
el("p", { className: "m01-lbw__muted", text: t("RoleHint") }),
|
|
...chips,
|
|
tablePreview(a.table, LBL, a.roles),
|
|
);
|
|
renderNav();
|
|
};
|
|
main.replaceChildren(box);
|
|
draw();
|
|
return () => (colsOf(a, "결과").length ? null : t("NeedResult"));
|
|
};
|
|
|
|
/* ── 3단계 결과 단위 ── */
|
|
const stepUnit = (): (() => string | null) => {
|
|
const per = a.resultUnit.replace(/^원\//, "");
|
|
const opts = [...new Set([per, ...UNITS])];
|
|
const box = createSelectField({
|
|
label: t("UnitPick"),
|
|
options: opts.map((u) => ({ value: u, text: `원/${u}` })),
|
|
value: per,
|
|
onChange: (v) => ((a.resultUnit = `원/${v}`), ctx.changed()),
|
|
});
|
|
main.replaceChildren(
|
|
el("p", { className: "m01-lbw__muted", text: t("BaseUnit", { b: a.table.기준 || "—" }) }),
|
|
box.root,
|
|
);
|
|
return () => null;
|
|
};
|
|
|
|
/* ── 6단계 이름 · 결과 종류 ── */
|
|
const stepName = (): (() => string | null) => {
|
|
const name = createInputField({
|
|
type: "text",
|
|
label: t("Name"),
|
|
value: a.name,
|
|
placeholder: "예: 돌쌓기 메쌓기",
|
|
});
|
|
name.input.addEventListener("input", () => ((a.name = name.input.value), renderNav()));
|
|
const per = a.resultUnit.replace(/^원\//, "");
|
|
const kinds = el("div", { className: "m01-lbw__chips" });
|
|
const valueBox = el("div", { className: "m01-lbw__stack" });
|
|
const drawKind = (): void => {
|
|
for (const [k, label] of [
|
|
["금액", t("KindMoney", { p: per })],
|
|
["값", t("KindValue")],
|
|
] as const) {
|
|
const b = el("button", {
|
|
className: `m01-lbw__chip${a.kind === k ? " is-on" : ""}`,
|
|
attrs: { type: "button" },
|
|
text: label,
|
|
});
|
|
b.addEventListener(
|
|
"click",
|
|
() => (
|
|
(a.kind = k),
|
|
(a.valueCol = a.valueCol || colsOf(a, "결과")[0]),
|
|
a.kind === "값" &&
|
|
a.resultUnit.startsWith("원/") &&
|
|
(a.resultUnit = a.table.값칸[a.valueCol] || ""),
|
|
drawKind(),
|
|
ctx.changed(),
|
|
renderNav()
|
|
),
|
|
);
|
|
kinds.append(b);
|
|
}
|
|
};
|
|
const paint = (): void => {
|
|
kinds.replaceChildren();
|
|
drawKind();
|
|
if (a.kind !== "값") return valueBox.replaceChildren();
|
|
const col = createSelectField({
|
|
label: t("ValueCol"),
|
|
options: colsOf(a, "결과").map((c) => ({ value: c, text: c })),
|
|
value: a.valueCol,
|
|
onChange: (v) => (
|
|
(a.valueCol = v),
|
|
(a.resultUnit = a.table.값칸[v] || a.resultUnit),
|
|
ctx.changed()
|
|
),
|
|
});
|
|
const unit = createInputField({
|
|
type: "text",
|
|
label: t("ValueUnit"),
|
|
value: a.resultUnit,
|
|
placeholder: "예: ㎥/시간",
|
|
});
|
|
unit.input.addEventListener(
|
|
"input",
|
|
() => ((a.resultUnit = unit.input.value.trim()), ctx.changed()),
|
|
);
|
|
valueBox.replaceChildren(col.root, unit.root);
|
|
};
|
|
kinds.addEventListener("click", () => queueMicrotask(paint));
|
|
paint();
|
|
main.replaceChildren(name.root, kinds, valueBox);
|
|
return () => (a.name.trim() ? null : t("NeedName"));
|
|
};
|
|
|
|
/* ── 틀 ── */
|
|
let valid: () => string | null = () => null;
|
|
const draw = (): void => {
|
|
ask.textContent = t(`Q${step}` as TKey);
|
|
const body = el("div");
|
|
main.replaceChildren(body);
|
|
if (step === 1) valid = stepSection();
|
|
else if (step === 2) valid = stepRoles();
|
|
else if (step === 3) valid = stepUnit();
|
|
else if (step === 4) {
|
|
stepCalc(ctx, main);
|
|
valid = () => null;
|
|
} else if (step === 5) {
|
|
if (linked) stepLink(ctx, main);
|
|
else {
|
|
linked = true;
|
|
main.replaceChildren(el("p", { className: "m01-lbw__muted", text: t("Loading") }));
|
|
void autoLinks(ctx).then(() => step === 5 && stepLink(ctx, main));
|
|
}
|
|
valid = () => null;
|
|
} else valid = stepName();
|
|
side.hidden = step === 1;
|
|
renderNav();
|
|
runPreview();
|
|
};
|
|
const renderNav = (): void => {
|
|
const why = valid();
|
|
const back = createButton({
|
|
label: t("Back"),
|
|
variant: "ghost",
|
|
disabled: step === 1,
|
|
onClick: () => ((step -= 1), draw()),
|
|
});
|
|
const next = createButton({
|
|
label: step === 6 ? t("Make") : t("Next"),
|
|
disabled: !!why,
|
|
onClick: () => (step === 6 ? make() : ((step += 1), draw())),
|
|
});
|
|
nav.replaceChildren(
|
|
el("span", {
|
|
className: "m01-lbw__muted",
|
|
text: `${t("StepOf", { n: step })}${why ? ` · ${why}` : ""}`,
|
|
}),
|
|
back,
|
|
next,
|
|
);
|
|
};
|
|
const make = (): void => {
|
|
try {
|
|
const row = buildRow({ ...a, valueCol: a.valueCol || colsOf(a, "결과")[0] });
|
|
const lb = readLogic(row, { key: null, version: null, isBase: false });
|
|
finish(lb);
|
|
closeModal();
|
|
} catch (e) {
|
|
showToast(e instanceof Error ? e.message : t("Fail"), "error");
|
|
}
|
|
};
|
|
main.addEventListener("m01lbw", renderNav);
|
|
let closeModal = (): void => undefined;
|
|
const modal = openModal({
|
|
title: t("Title"),
|
|
closeLabel: t("Close"),
|
|
dialogClass: "m01-lbw",
|
|
beforeClose: () => (finish(null), true),
|
|
mount: (body, close) => {
|
|
closeModal = close;
|
|
body.append(ask, el("div", { className: "m01-lbw__cols", children: [main, side] }), nav);
|
|
body.addEventListener(
|
|
"keydown",
|
|
(ev) => ev.altKey && ev.key === "ArrowLeft" && step > 1 && ((step -= 1), draw()),
|
|
);
|
|
},
|
|
});
|
|
void modal;
|
|
draw();
|
|
});
|
|
}
|