feat(M01): 새로 만들기 모달에 새 길 잇기 — 절 목록으로 원문 절 고르기 · 절로 표 거르기 · 표 조건 값 목록 · 초안 시험 계산 · 자체 로직 저장(GX) · 본떠 만들기

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
2026-09-21 09:43:19 +09:00
co-authored by Claude Sonnet 5
parent 63832239d4
commit beea3fac46
11 changed files with 383 additions and 202 deletions
@@ -6,11 +6,7 @@
import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements";
import { searchElements, searchPrice } from "./M01_MasterData_UI_Logic_Api";
import {
fetchSectionTables,
searchJobs,
type TableBrief,
} from "./M01_MasterData_UI_Logic_Wizard_Api";
import { searchJobs, type TableBrief } from "./M01_MasterData_UI_Logic_Wizard_Api";
import { atomEditor, select, textBox } from "./M01_MasterData_UI_Logic_Wizard_Atom";
import {
BASES,
@@ -31,26 +27,11 @@ import {
} from "./M01_MasterData_UI_Logic_Wizard_Model";
import { tw, type WizardTextKey } from "./M01_MasterData_UI_Logic_Wizard_Text";
export interface Ctx {
d: Draft;
/** 값이 바뀜 — 미리보기·다음 단추 다시 셈 */
changed: () => void;
}
export interface Step {
key: WizardTextKey;
question: WizardTextKey;
render: (ctx: Ctx) => HTMLElement;
ready: (d: Draft) => boolean;
}
import { copyBox } from "./M01_MasterData_UI_Logic_Wizard_Copy";
import { sourceStep } from "./M01_MasterData_UI_Logic_Wizard_Source";
import { fail, note, page, type Ctx, type Step } from "./M01_MasterData_UI_Logic_Wizard_Ui";
const page = (question: WizardTextKey, ...body: (HTMLElement | string)[]): HTMLElement =>
el("div", {
className: "m01w__page",
children: [el("h3", { text: tw(question) }), ...body],
});
const note = (text: string): HTMLElement => el("p", { className: "m01w__muted", text });
const fail = (e: unknown): void =>
showToast(e instanceof Error ? e.message : tw("Failed"), "error");
export type { Ctx, Step };
/* ── 1. 이름 ─────────────────────────────────────────────────────────── */
@@ -60,7 +41,7 @@ const nameStep: Step = {
key: "S_Name",
question: "Q_Name",
ready: (d) => d.name.trim() !== "" && d.unit.trim() !== "",
render: ({ d, changed }) => {
render: ({ d, changed, onCopied }) => {
const other = !UNITS.includes(d.unit);
const own = el("div");
const drawOwn = (on: boolean): void =>
@@ -94,104 +75,11 @@ const nameStep: Step = {
),
own,
note(tw("Unit_Money")),
copyBox(onCopied),
);
},
};
/* ── 2. 원문 절 ──────────────────────────────────────────────────────── */
const sourceStep: Step = {
key: "S_Source",
question: "Q_Source",
ready: (d) => d.section.trim() !== "",
render: ({ d, changed }) => {
const list = el("div", { className: "m01w__stack" });
const box = createInputField({
value: d.section,
placeholder: tw("Section_Ph"),
type: "text",
});
const draw = (found: TableBrief[], searched: boolean): void => {
list.replaceChildren(
...(found.length
? found.map((t) => tableRow(t, d, changed))
: searched
? [note(tw("Section_None")), note(tw("Section_Skip"))]
: []),
);
};
const find = (): void => {
d.section = box.input.value.trim();
d.tables = [];
changed();
if (!d.section) return draw([], false);
void fetchSectionTables(d.book, d.section)
.then((found) => {
d.tables = [...found]; // 처음에는 이 절의 표를 모두 씀 — 빼려면 체크를 풂
draw(found, true);
changed();
})
.catch(fail);
};
box.input.addEventListener("keydown", (ev) => ev.key === "Enter" && find());
box.input.addEventListener("input", () => {
d.section = box.input.value.trim();
changed();
});
if (d.section && !d.tables.length) find();
else draw(d.tables, d.tables.length > 0);
return page(
"Q_Source",
select(
["산림품셈", "건설품셈"].map((b) => ({ value: b, text: b })),
d.book,
(v) => {
d.book = v;
find();
},
tw("Book"),
),
el("div", {
className: "m01w__inline",
children: [box.root, createButton({ label: tw("Section_Find"), onClick: find })],
}),
list,
);
},
};
function tableRow(t: TableBrief, d: Draft, changed: () => void): HTMLElement {
const check = el("input", { attrs: { type: "checkbox", "data-table": t. } });
check.checked = d.tables.some((x) => x. === t.);
check.addEventListener("change", () => {
d.tables = check.checked ? [...d.tables, t] : d.tables.filter((x) => x. !== t.);
changed();
});
const usage = t.?.?.join("·") ?? "";
return el("label", {
className: "m01w__table",
children: [
check,
el("div", {
className: "m01w__stack",
children: [
el("strong", { text: `${t.} · ${t.}` }),
el("span", {
className: "m01w__muted",
text: [
t. ? `${tw("Table_Base")} ${t.}` : "",
`${tw("Table_Cols")} ${Object.keys(t. ?? {}).join("·")}`,
usage ? `${tw("Table_Usage")} ${usage}` : "",
]
.filter(Boolean)
.join(" · "),
}),
],
}),
],
});
}
/* ── 3. 중간 값(조건 나누기) ─────────────────────────────────────────── */
const newMiddle = (): Middle => ({ name: "", branches: [], otherwise: emptyAtom() });
@@ -643,8 +531,9 @@ const extraStep: Step = {
select(
[
{ value: "", text: tw("Round_None") },
{ value: "원 미만 버림", text: tw("Round_Down") },
{ value: "원 미만 반올림", text: tw("Round_Half") },
{ value: "버림", text: tw("Round_Down") },
{ value: "올림", text: tw("Round_Up") },
{ value: "반올림", text: tw("Round_Half") },
],
d.round,
(v) => {
@@ -652,7 +541,6 @@ const extraStep: Step = {
changed();
},
),
note(tw("Round_Note")),
);
},
};