feat(M01): 로직 화면에 「새로 만들기」 단추 · 저장 뒤 방식 C 로 열기 · 테스트 컨테이너와 방식 A·B 화면 걷음
- 로직 화면 바의 새로 만들기 단추가 모달을 엶 · 저장·본떠 저장 뒤 새 자체 로직을 흐름 그림으로 바로 엶 - UI_Test* 다섯 파일 삭제 · 왼쪽 「테스트」 줄 제거 · 모달의 까닭 글은 UI_Logic_Text 것을 씀 - 견본 10개 키 목록은 시험(test_m01_test_container)으로 옮김 · 흐름 그림 시험의 옛 파일 이름을 바로잡음 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
@@ -35,6 +35,7 @@ import { buildCalc } from "./M01_MasterData_UI_Logic_Calc";
|
||||
import { buildEditor } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { isOwnLogic, mountFlow } from "./M01_MasterData_UI_Logic_Flow";
|
||||
import { buildList, logicId, type ListItem, type ListMark } from "./M01_MasterData_UI_Logic_List";
|
||||
import { openLogicWizard } from "./M01_MasterData_UI_Logic_Wizard";
|
||||
import { openPicker } from "./M01_MasterData_UI_Logic_Pick";
|
||||
import type { SideHandle } from "./M01_MasterData_UI_Side";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
@@ -286,41 +287,14 @@ export async function mountM01Logic(
|
||||
persist();
|
||||
};
|
||||
|
||||
const onNew = (): void => {
|
||||
const f = files.find((x) => x.file === opened?.file) ?? files[0];
|
||||
if (!f) return;
|
||||
const id = `new:${Date.now()}`;
|
||||
// 원문번호는 절 번호만(부문은 구분 칸) · 키는 저장 때 서버가 줌
|
||||
const row: LogicRow = {
|
||||
키: "",
|
||||
원문번호: `${tx("New_Key")} ${Object.keys(drafts).length + 1}`,
|
||||
이름: "",
|
||||
결과단위: "원/",
|
||||
출처: "",
|
||||
소유: "공용",
|
||||
입력: [],
|
||||
중간: [],
|
||||
호표: [],
|
||||
덧줄: [],
|
||||
끝수: null,
|
||||
};
|
||||
const next: Opened = {
|
||||
id,
|
||||
sub: subOf(f),
|
||||
file: f.file,
|
||||
version: f.version,
|
||||
origKey: null,
|
||||
row,
|
||||
original: null,
|
||||
prices: {},
|
||||
reasons: [],
|
||||
lines: null,
|
||||
values: {},
|
||||
};
|
||||
drafts[id] = pick(next, clone(row));
|
||||
persist();
|
||||
show(next);
|
||||
};
|
||||
/** 「새로 만들기」 모달 — 저장·본떠 저장이 끝나면 그 새 자체 로직을 흐름 그림으로 바로 엶 */
|
||||
const onNew = (): void =>
|
||||
openLogicWizard({
|
||||
onSaved: (key) =>
|
||||
void reload()
|
||||
.then(() => open(logicId("자체", key), "자체", key))
|
||||
.catch(failed),
|
||||
});
|
||||
|
||||
/** 「본떠 만들기」 — 정본을 자체 파일로 복제하고(계약 5장) 그 새 로직을 바로 엶 */
|
||||
const onCopy = async (from: Opened): Promise<void> => {
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from "./M01_MasterData_UI_Logic_Wizard_Model";
|
||||
import { STEPS, type Step } from "./M01_MasterData_UI_Logic_Wizard_Steps";
|
||||
import { tw } from "./M01_MasterData_UI_Logic_Wizard_Text";
|
||||
import { plainReason } from "./M01_MasterData_UI_Test_Text";
|
||||
import { plainReason } from "./M01_MasterData_UI_Logic_Text";
|
||||
import "./M01_MasterData_UI_Logic_Wizard_Style.css";
|
||||
|
||||
export interface WizardOptions {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Page.ts
|
||||
* 마스터 요소 화면 — 좌측 도킹 패널(컨테이너 아홉: 인력·재료·기계·소요량·계수·환율·요율·일위대가 로직·로직 테스트)
|
||||
* 마스터 요소 화면 — 좌측 도킹 패널(컨테이너: 인력·재료·기계·소요량·계수·환율·요율·일위대가 로직·로직 테스트)
|
||||
* / 우측 요소 표 · 표 목록 · 로직 편집
|
||||
*
|
||||
* 시스템 관리자만. 고친 것은 초안(sessionStorage)에 쌓이고 [저장] 한 번에 서버로 —
|
||||
@@ -54,7 +54,6 @@ function buildPage(): HTMLElement {
|
||||
let query = "";
|
||||
let dispose = (): void => {};
|
||||
let logicMounted = false;
|
||||
let testMounted = false;
|
||||
|
||||
/* --- 우측: 머리(제목·찾기·저장) + 알림 + 본문 --- */
|
||||
const title = el("h2", { className: "m01-master__title", text: L("M01_PickFile") });
|
||||
@@ -70,7 +69,6 @@ function buildPage(): HTMLElement {
|
||||
children: [title, searchField.root, summary, drop, save],
|
||||
});
|
||||
const logicHost = el("div", { className: "m01-master__logic", attrs: { hidden: "" } });
|
||||
const testHost = el("div", { className: "m01-master__logic", attrs: { hidden: "" } });
|
||||
const elementView = el("div", {
|
||||
className: "m01-master__elements",
|
||||
children: [head, notice, body],
|
||||
@@ -88,9 +86,8 @@ function buildPage(): HTMLElement {
|
||||
onDraftChange(refreshBar);
|
||||
refreshBar();
|
||||
|
||||
const showMode = (mode: "elements" | "logic" | "test"): void => {
|
||||
const showMode = (mode: "elements" | "logic"): void => {
|
||||
logicHost.hidden = mode !== "logic";
|
||||
testHost.hidden = mode !== "test";
|
||||
elementView.hidden = mode !== "elements";
|
||||
};
|
||||
|
||||
@@ -196,20 +193,8 @@ function buildPage(): HTMLElement {
|
||||
});
|
||||
};
|
||||
|
||||
/* --- 좌측: 컨테이너 아홉 --- */
|
||||
const side = buildSide(
|
||||
openFile,
|
||||
() => openLogicTab(),
|
||||
() => {
|
||||
dispose();
|
||||
dispose = (): void => {};
|
||||
current = null;
|
||||
showMode("test");
|
||||
if (testMounted) return;
|
||||
testMounted = true;
|
||||
void import("./M01_MasterData_UI_Test").then((m) => m.mountM01Test(testHost));
|
||||
},
|
||||
);
|
||||
/* --- 좌측: 컨테이너 --- */
|
||||
const side = buildSide(openFile, () => openLogicTab());
|
||||
|
||||
const layout = el("div", { className: "ui-workflow-layout m01-master" });
|
||||
const main = el("main", {
|
||||
@@ -217,7 +202,7 @@ function buildPage(): HTMLElement {
|
||||
children: [
|
||||
el("div", {
|
||||
className: "m01-master__panel",
|
||||
children: [elementView, logicHost, testHost],
|
||||
children: [elementView, logicHost],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -127,13 +127,8 @@ const LEAF: Group[] = ["환율", "요율"];
|
||||
/** 거름이 있는 컨테이너 — 펼치면 「전체」(또는 닫기 전 거름) 이 바로 열림 */
|
||||
const FILTERED: Group[] = ["인력", "기계", "소요량", "계수"];
|
||||
const LOGIC_ID = "로직|";
|
||||
const TEST_ID = "테스트|";
|
||||
|
||||
export function buildSide(
|
||||
onOpen: (pick: Pick) => void,
|
||||
onLogic: () => void,
|
||||
onTest: () => void,
|
||||
): SideHandle {
|
||||
export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): SideHandle {
|
||||
const lists = new Map<Group, FileInfo[]>();
|
||||
const bodies = new Map<Group, HTMLElement>();
|
||||
const buttons = new Map<string, HTMLElement>();
|
||||
@@ -408,16 +403,7 @@ export function buildSide(
|
||||
onLogic();
|
||||
});
|
||||
|
||||
// 테스트 컨테이너 — 눌러 펼치는 목록이 없는 한 줄(환율·요율과 같은 모양)
|
||||
const test = section(L("M01_GroupTest"), null);
|
||||
test.classList.add("m01-side__leaf");
|
||||
buttons.set(TEST_ID, test);
|
||||
test.querySelector(".ui-collapsible__title")?.addEventListener("click", () => {
|
||||
setActive(TEST_ID);
|
||||
onTest();
|
||||
});
|
||||
|
||||
const root = el("div", { className: "m01-side", children: [...groups, logic, test] });
|
||||
const root = el("div", { className: "m01-side", children: [...groups, logic] });
|
||||
attachCollapsible(root);
|
||||
void Promise.all(GROUPS.map(refresh)).catch((error) =>
|
||||
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error"),
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Test.ts
|
||||
* 로직 테스트 컨테이너 틀 — 공종(로직) 10 개 고르기 + 방식 탭 A·B·C
|
||||
* 읽기와 시험 계산만 — 저장 없음 · 정본 로직은 안 고침 · 계산은 로직 화면과 같은 `/calc`
|
||||
* 접속 계약 — 방식 파일마다 `render(host, logicKey)` 하나
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
||||
import { fetchLogics } from "./M01_MasterData_UI_Logic_Api";
|
||||
import { openLogicWizard } from "./M01_MasterData_UI_Logic_Wizard";
|
||||
import { render as renderA } from "./M01_MasterData_UI_Test_A";
|
||||
import { render as renderB } from "./M01_MasterData_UI_Test_B";
|
||||
import { render as renderC } from "./M01_MasterData_UI_Logic_Flow";
|
||||
import { tt } from "./M01_MasterData_UI_Test_Text";
|
||||
import "./M01_MasterData_UI_Test_Style.css";
|
||||
|
||||
/** 시험 대상 산림품셈 로직 10 개 — 복잡도(입력·표 찾기·로직 부르기·덧줄·재료)로 잰 순서.
|
||||
* 재는 법 `resources/master_data/scripts/로직_복잡도_재기.py` · 메쌓기·노체·트럭 운반은 끝까지 눌러 보는 세 개. */
|
||||
export const TEST_KEYS: readonly string[] = [
|
||||
"GF000181", // 12-16 맨홀
|
||||
"GF000177", // 12-12 날개벽
|
||||
"GF000173", // 12-11-1 VR관(소켓식) 부설
|
||||
"GF000174", // 12-11-2-1 흄관 부설
|
||||
"GF000160", // 12-4 합판거푸집
|
||||
"GF000182", // 12-17-1 펌프카 타설
|
||||
"GF000069", // 7-11 동력상하차기 집재
|
||||
"GF000219", // 13-4-1 돌쌓기 메쌓기(인력)
|
||||
"GF000119", // 9-16-1 노체포설
|
||||
"GF000137", // 10-4 중기운반 트레일러
|
||||
];
|
||||
|
||||
type Render = (host: HTMLElement, logicKey: string) => void;
|
||||
|
||||
/** 방식 파일이 오면 여기 한 줄씩 잇는다 — null 은 준비 중 */
|
||||
const METHODS: { id: string; label: () => string; render: Render | null }[] = [
|
||||
{ id: "A", label: () => tt("Tab_A"), render: renderA },
|
||||
{ id: "B", label: () => tt("Tab_B"), render: renderB },
|
||||
{ id: "C", label: () => tt("Tab_C"), render: renderC },
|
||||
];
|
||||
|
||||
export function mountM01Test(host: HTMLElement): void {
|
||||
let logicKey = TEST_KEYS[0];
|
||||
let method = METHODS[0].id;
|
||||
const body = el("div", { className: "m01-test__body" });
|
||||
const tabs = el("div", { className: "m01-test__tabs" });
|
||||
const select = createSelectField({
|
||||
label: tt("Pick_Logic"),
|
||||
options: TEST_KEYS.map((k) => ({ value: k, text: k })),
|
||||
value: logicKey,
|
||||
onChange: (v) => {
|
||||
logicKey = v;
|
||||
draw();
|
||||
},
|
||||
});
|
||||
|
||||
const draw = (): void => {
|
||||
tabs.replaceChildren(
|
||||
...METHODS.map((m) =>
|
||||
el("button", {
|
||||
className: `m01-test__tab${m.id === method ? " is-active" : ""}`,
|
||||
text: m.label(),
|
||||
attrs: { type: "button", "data-method": m.id },
|
||||
}),
|
||||
),
|
||||
);
|
||||
tabs.querySelectorAll("button").forEach((b) =>
|
||||
b.addEventListener("click", () => {
|
||||
method = b.dataset.method ?? "A";
|
||||
draw();
|
||||
}),
|
||||
);
|
||||
const one = METHODS.find((m) => m.id === method);
|
||||
if (one?.render) one.render(body, logicKey);
|
||||
else body.replaceChildren(el("p", { className: "m01-test__muted", text: tt("Soon") }));
|
||||
};
|
||||
|
||||
host.replaceChildren(
|
||||
el("div", {
|
||||
className: "m01-test",
|
||||
children: [
|
||||
el("div", {
|
||||
className: "m01-test__bar",
|
||||
children: [
|
||||
el("h2", { text: tt("Title") }),
|
||||
select.root,
|
||||
tabs,
|
||||
createButton({ label: tt("New_Logic"), onClick: () => openLogicWizard() }),
|
||||
],
|
||||
}),
|
||||
el("p", { className: "m01-test__muted", text: tt("Read_Only") }),
|
||||
body,
|
||||
],
|
||||
}),
|
||||
);
|
||||
// 목록에서 이름을 받아 「번호 이름」 으로 바꿈 — 못 받아도 키로 쓸 수 있음
|
||||
fetchLogics()
|
||||
.then((all) => {
|
||||
const label = new Map(all.map((r) => [r.키, `${r.원문번호} ${r.이름}`]));
|
||||
select.setOptions(
|
||||
TEST_KEYS.map((k) => ({ value: k, text: label.get(k) ?? k })),
|
||||
logicKey,
|
||||
);
|
||||
})
|
||||
.catch((error) => showToast(error instanceof Error ? error.message : tt("Failed"), "error"));
|
||||
draw();
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Test_A.ts
|
||||
* 방식 A — 질문·답 마법사: 공종 → 조건 질문(한 번에 하나) → 재료·기계 고르기 → 결과 호표
|
||||
* 식은 안 보임 · 시험 계산은 로직 화면과 같은 `/calc` · 저장 없음
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
fetchLogic,
|
||||
runCalc,
|
||||
type CalcAnswer,
|
||||
type ElementBrief,
|
||||
type LogicInput,
|
||||
type LogicOne,
|
||||
} from "./M01_MasterData_UI_Logic_Api";
|
||||
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { hasPickable, pickPanel, swappedRow } from "./M01_MasterData_UI_Logic_Pick";
|
||||
import { guideLines, plainReason, tt } from "./M01_MasterData_UI_Test_Text";
|
||||
|
||||
type Step =
|
||||
{ kind: "job" } | { kind: "ask"; spec: LogicInput } | { kind: "pick" } | { kind: "result" };
|
||||
|
||||
export function render(host: HTMLElement, logicKey: string): void {
|
||||
host.replaceChildren(el("p", { className: "m01-test__muted", text: tt("Loading") }));
|
||||
fetchLogic(logicKey)
|
||||
.then((one) => wizard(host, one))
|
||||
.catch((error) => showToast(error instanceof Error ? error.message : tt("Failed"), "error"));
|
||||
}
|
||||
|
||||
function wizard(host: HTMLElement, one: LogicOne): void {
|
||||
const { logic } = one;
|
||||
const values: Record<string, string> = {};
|
||||
const swaps = new Map<number, ElementBrief>();
|
||||
const steps: Step[] = [
|
||||
{ kind: "job" },
|
||||
...(logic.입력 ?? []).map((spec): Step => ({ kind: "ask", spec })),
|
||||
...(hasPickable(logic) ? [{ kind: "pick" } as Step] : []),
|
||||
{ kind: "result" },
|
||||
];
|
||||
const picker = pickPanel(one, swaps, () => undefined);
|
||||
let at = 0;
|
||||
|
||||
const go = (next: number): void => {
|
||||
at = next;
|
||||
draw();
|
||||
};
|
||||
|
||||
const frame = (
|
||||
title: string,
|
||||
note: string,
|
||||
content: HTMLElement[],
|
||||
nextOk: boolean,
|
||||
): HTMLButtonElement => {
|
||||
const last = steps[at].kind === "result";
|
||||
const back = createButton({ label: tt("Back"), variant: "ghost", onClick: () => go(at - 1) });
|
||||
back.disabled = at === 0;
|
||||
const next = createButton({ label: tt("Next"), onClick: () => go(at + 1) });
|
||||
next.disabled = !nextOk;
|
||||
const again = createButton({ label: tt("Again"), variant: "ghost", onClick: () => go(0) });
|
||||
host.replaceChildren(
|
||||
el("div", {
|
||||
className: "m01-test__card",
|
||||
children: [
|
||||
el("p", {
|
||||
className: "m01-test__step",
|
||||
text: tt("Step", { n: at + 1, total: steps.length }),
|
||||
}),
|
||||
el("h3", { text: title }),
|
||||
...(note ? [el("p", { className: "m01-test__muted", text: note })] : []),
|
||||
...content,
|
||||
el("div", { className: "m01-test__nav", children: last ? [back, again] : [back, next] }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
return next;
|
||||
};
|
||||
|
||||
const draw = (): void => {
|
||||
const step = steps[at];
|
||||
if (step.kind === "job") {
|
||||
frame(
|
||||
`${logic.원문번호} ${logic.이름}`,
|
||||
tt("Job_Note"),
|
||||
[
|
||||
el("p", { text: `${tt("Job_Title")} · ${logic.결과단위}` }),
|
||||
el("ul", { children: guideLines("A").map((t) => el("li", { text: t })) }),
|
||||
],
|
||||
true,
|
||||
);
|
||||
} else if (step.kind === "ask") drawAsk(step.spec);
|
||||
else if (step.kind === "pick") drawPick();
|
||||
else void drawResult();
|
||||
};
|
||||
|
||||
const drawAsk = (spec: LogicInput): void => {
|
||||
const name = spec.이름;
|
||||
const ready = (): boolean => (values[name] ?? "").trim() !== "";
|
||||
let next: HTMLButtonElement | null = null;
|
||||
const changed = (v: string): void => {
|
||||
values[name] = v;
|
||||
if (next) next.disabled = !ready();
|
||||
};
|
||||
let control: HTMLElement;
|
||||
if (spec.고르기?.length) {
|
||||
control = createSelectField({
|
||||
options: ["", ...spec.고르기.map(String)].map((o) => ({ value: o, text: o })),
|
||||
value: values[name] ?? "",
|
||||
onChange: changed,
|
||||
}).root;
|
||||
} else {
|
||||
const box = el("input", {
|
||||
className: "m01-test__input",
|
||||
attrs: { type: "text", inputmode: "decimal", placeholder: tt("Enter_Value") },
|
||||
});
|
||||
box.value = values[name] ?? "";
|
||||
box.addEventListener("input", () => changed(box.value));
|
||||
control = box;
|
||||
}
|
||||
const question = spec.고르기?.length
|
||||
? tt("Ask_Pick", { name })
|
||||
: tt("Ask_Number", { name, unit: spec.단위 ? ` (${spec.단위})` : "" });
|
||||
const hint = spec.범위 ? tt("Ask_Range", { min: spec.범위[0], max: spec.범위[1] }) : "";
|
||||
next = frame(question, hint, [control], ready());
|
||||
};
|
||||
|
||||
const drawPick = (): void => {
|
||||
frame(tt("Mat_Title"), tt("Mat_Note"), [picker], true);
|
||||
};
|
||||
|
||||
const drawResult = async (): Promise<void> => {
|
||||
const inputs: Record<string, unknown> = {};
|
||||
for (const spec of logic.입력 ?? []) {
|
||||
const raw = (values[spec.이름] ?? "").trim();
|
||||
if (raw === "") continue;
|
||||
const option = spec.고르기?.find((o) => String(o) === raw);
|
||||
inputs[spec.이름] =
|
||||
option !== undefined ? option : Number.isNaN(Number(raw)) ? raw : Number(raw);
|
||||
}
|
||||
const row = swappedRow(logic, swaps); // 바꿔 본 것이 있으면 복사본으로만 셈
|
||||
let answer: CalcAnswer;
|
||||
try {
|
||||
answer = await runCalc({ key: logic.키, inputs, ...(row ? { row, file: one.file } : {}) });
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : tt("Failed"), "error");
|
||||
return;
|
||||
}
|
||||
frame(tt("Result_Title"), "", resultView(answer), true);
|
||||
};
|
||||
|
||||
draw();
|
||||
}
|
||||
|
||||
function resultView(answer: CalcAnswer): HTMLElement[] {
|
||||
if (!answer.ok) {
|
||||
return [
|
||||
el("div", {
|
||||
className: "m01-test__reasons",
|
||||
children: [
|
||||
el("strong", { text: tt("Result_Stopped") }),
|
||||
el("div", { text: plainReason(answer.reason) }),
|
||||
],
|
||||
}),
|
||||
];
|
||||
}
|
||||
const out: HTMLElement[] = [];
|
||||
if (answer.lines) {
|
||||
out.push(
|
||||
el("table", {
|
||||
className: "m01-test__grid",
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [
|
||||
el("tr", {
|
||||
children: [tt("Col_Name"), tt("Col_Qty"), tt("Col_Amount")].map((h) =>
|
||||
el("th", { text: h }),
|
||||
),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
el("tbody", {
|
||||
children: answer.lines.map((l) =>
|
||||
el("tr", {
|
||||
children: [
|
||||
el("td", { text: l.출처 ? `${l.이름} (${l.출처})` : l.이름 }),
|
||||
el("td", { text: `${formatNumber(l.수량)} ${l.단위}` }),
|
||||
el("td", { className: "m01-test__money", text: formatNumber(l.금액) }),
|
||||
],
|
||||
}),
|
||||
),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (answer.sums) {
|
||||
out.push(
|
||||
el("p", {
|
||||
className: "m01-test__total",
|
||||
attrs: { "data-total": String(answer.sums["계"] ?? 0) },
|
||||
text: `${tt("Sum_Total")} ${formatNumber(answer.sums["계"] ?? 0)}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (answer.result !== undefined) {
|
||||
out.push(
|
||||
el("p", {
|
||||
className: "m01-test__total",
|
||||
attrs: { "data-total": String(answer.result) },
|
||||
text: `${tt("Sum_Result")} ${formatNumber(answer.result)}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -1,291 +0,0 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Test_B.ts
|
||||
* 로직 사용성 테스트 컨테이너 — 방식 B: 호표 + 옆 설명 카드 (PLAN.md 1-2)
|
||||
*
|
||||
* 접속 계약: export function render(host, logicKey) 하나.
|
||||
* 읽기 + 시험 계산만 — 저장 없음 · 정본 로직 안 고침. 로직 읽기·시험 계산은 지금
|
||||
* 로직 화면이 쓰는 Store·API(fetchLogic · runCalc · elements · table)를 그대로
|
||||
* 씀 — 새 엔진·새 계산 없음(만약·찾기 식 자체는 서버가 풂 · 여기선 찾기(...) 가
|
||||
* 가리키는 원문 표를 다시 읽어 「걸린 줄」만 강조).
|
||||
*
|
||||
* 줄을 누르면 옆 카드 — 원문 표 미리보기(걸린 줄 강조) · 적용된 할증 · 쉬운 말
|
||||
* 풀이(식을 문장으로, 안 되면 원문 식 그대로) · 출처. 입력을 바꾸면 호표와
|
||||
* 카드가 같이 바뀜(자동 계산 — [계산] 버튼 없음).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createSelectField, el } from "@ui/ui_template_elements";
|
||||
import {
|
||||
fetchLogic,
|
||||
runCalc,
|
||||
type CalcAnswer,
|
||||
type CalcLine,
|
||||
type HoLine,
|
||||
type LogicOne,
|
||||
} from "./M01_MasterData_UI_Logic_Api";
|
||||
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { buildNote, relatedFinds, type EvalCtx } from "./M01_MasterData_UI_Logic_Note";
|
||||
import { hasPickable, pickPanel, swappedRow, type Swaps } from "./M01_MasterData_UI_Logic_Pick";
|
||||
import { guideLines, plainReason, tt } from "./M01_MasterData_UI_Test_Text";
|
||||
|
||||
/* ── 화면 상태 ────────────────────────────────────────────────────────── */
|
||||
|
||||
interface State {
|
||||
one: LogicOne;
|
||||
values: Record<string, string>;
|
||||
answer: CalcAnswer | null;
|
||||
selected: number;
|
||||
calcSeq: number;
|
||||
swaps: Swaps;
|
||||
picker: HTMLElement;
|
||||
}
|
||||
|
||||
export function render(host: HTMLElement, logicKey: string): void {
|
||||
host.replaceChildren(el("p", { className: "m01-logic__muted", text: "불러오는 중…" }));
|
||||
void fetchLogic(logicKey)
|
||||
.then((one) => {
|
||||
const swaps: Swaps = new Map();
|
||||
const state: State = {
|
||||
one,
|
||||
// 고르기 칸은 첫 값으로 시작 — 방식 C 와 같게(바로 계산이 돎)
|
||||
values: Object.fromEntries(
|
||||
(one.logic.입력 ?? [])
|
||||
.filter((spec) => spec.고르기?.length)
|
||||
.map((spec) => [spec.이름, String(spec.고르기?.[0])]),
|
||||
),
|
||||
answer: null,
|
||||
selected: 0,
|
||||
calcSeq: 0,
|
||||
swaps,
|
||||
picker: pickPanel(one, swaps, () => void recalc(host, state)),
|
||||
};
|
||||
paint(host, state);
|
||||
void recalc(host, state);
|
||||
})
|
||||
.catch((error) => {
|
||||
host.replaceChildren(
|
||||
el("p", { className: "m01-logic__reasons", text: String(error?.message ?? error) }),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function recalc(host: HTMLElement, state: State): Promise<void> {
|
||||
const seq = ++state.calcSeq;
|
||||
const inputs: Record<string, unknown> = {};
|
||||
for (const spec of state.one.logic.입력 ?? []) {
|
||||
const raw = (state.values[spec.이름] ?? "").trim();
|
||||
if (raw === "") continue;
|
||||
const option = spec.고르기?.find((o) => String(o) === raw);
|
||||
inputs[spec.이름] =
|
||||
option !== undefined ? option : Number.isNaN(Number(raw)) ? raw : Number(raw);
|
||||
}
|
||||
try {
|
||||
const answer = await runCalc({
|
||||
key: state.one.logic.키,
|
||||
inputs,
|
||||
row: swappedRow(state.one.logic, state.swaps) ?? state.one.logic,
|
||||
file: state.one.file,
|
||||
});
|
||||
if (seq !== state.calcSeq) return; // 최신 응답만 채택
|
||||
state.answer = answer;
|
||||
} catch (error) {
|
||||
if (seq !== state.calcSeq) return;
|
||||
state.answer = { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
paint(host, state);
|
||||
}
|
||||
|
||||
/* ── 그리기 ───────────────────────────────────────────────────────────── */
|
||||
|
||||
function paint(host: HTMLElement, state: State): void {
|
||||
const row = state.one.logic;
|
||||
|
||||
const inputWrap = el("div", { className: "m01-logic__fields" });
|
||||
inputWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:12px;margin-bottom:12px;";
|
||||
for (const spec of row.입력 ?? []) {
|
||||
let control: HTMLElement;
|
||||
if (spec.고르기?.length) {
|
||||
const pick = createSelectField({
|
||||
options: ["", ...spec.고르기.map(String)].map((o) => ({ value: o, text: o || "(선택)" })),
|
||||
value: state.values[spec.이름] ?? "",
|
||||
compact: true,
|
||||
onChange: (v) => {
|
||||
state.values[spec.이름] = v;
|
||||
void recalc(host, state);
|
||||
},
|
||||
});
|
||||
control = pick.root;
|
||||
} else {
|
||||
const box = el("input", {
|
||||
className: "m01-logic__input",
|
||||
attrs: { type: "text", inputmode: "decimal", placeholder: tt("Enter_Value") },
|
||||
});
|
||||
box.value = state.values[spec.이름] ?? "";
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
box.addEventListener("input", () => {
|
||||
state.values[spec.이름] = box.value;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => void recalc(host, state), 300);
|
||||
});
|
||||
control = box;
|
||||
}
|
||||
const label = spec.단위 ? `${spec.이름} (${spec.단위})` : spec.이름;
|
||||
inputWrap.append(
|
||||
el("label", {
|
||||
className: "m01-logic__field",
|
||||
children: [el("span", { text: label }), control],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const main = el("div", {});
|
||||
main.style.cssText = "flex:1 1 60%;min-width:0;";
|
||||
main.append(
|
||||
el("h3", { text: `${row.원문번호} ${row.이름}` }),
|
||||
el("ul", {
|
||||
className: "m01-logic__muted",
|
||||
children: guideLines("B").map((t) => el("li", { text: t })),
|
||||
}),
|
||||
inputWrap,
|
||||
...(hasPickable(state.one.logic)
|
||||
? [
|
||||
el("details", {
|
||||
attrs: { open: "" },
|
||||
children: [el("summary", { text: tt("Mat_Title") }), state.picker],
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
buildHoTable(host, state),
|
||||
);
|
||||
if (state.answer && !state.answer.ok) {
|
||||
main.append(
|
||||
el("div", {
|
||||
className: "m01-logic__reasons",
|
||||
children: [
|
||||
el("strong", { text: "멈춤" }),
|
||||
el("div", { text: plainReason(state.answer.reason) }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const side = el("div", { className: "m01-logic__side" });
|
||||
side.style.cssText =
|
||||
"flex:1 1 36%;min-width:280px;border:1px solid var(--ui-border,#ddd);border-radius:8px;padding:12px;";
|
||||
buildCard(side, state);
|
||||
|
||||
const layout = el("div", {});
|
||||
layout.style.cssText = "display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap;";
|
||||
layout.append(main, side);
|
||||
host.replaceChildren(layout);
|
||||
}
|
||||
|
||||
function buildHoTable(host: HTMLElement, state: State): HTMLElement {
|
||||
const lines: CalcLine[] | null = state.answer?.ok ? (state.answer.lines ?? null) : null;
|
||||
const rows = (state.one.logic.호표 ?? []).map((ho, i) => {
|
||||
const calc = lines?.[i];
|
||||
const tr = el("tr", {
|
||||
className: i === state.selected ? "m01-logic__row is-selected" : "m01-logic__row",
|
||||
children: [
|
||||
el("td", { text: ho.이름 ?? ho.요소 }),
|
||||
el("td", { text: ho.규격 ?? "" }),
|
||||
el("td", { text: calc ? `${formatNumber(calc.수량)} ${calc.단위}` : (ho.단위 ?? "") }),
|
||||
el("td", { className: "m01-logic__money", text: calc ? formatNumber(calc.금액) : "" }),
|
||||
],
|
||||
});
|
||||
tr.style.cssText =
|
||||
"cursor:pointer;" + (i === state.selected ? "background:var(--ui-accent-bg,#eef4ff);" : "");
|
||||
tr.addEventListener("click", () => {
|
||||
state.selected = i;
|
||||
paint(host, state);
|
||||
});
|
||||
return tr;
|
||||
});
|
||||
return el("table", {
|
||||
className: "m01-logic__grid",
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [
|
||||
el("tr", {
|
||||
children: ["이름", "규격", "수량", "금액"].map((h) => el("th", { text: h })),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
el("tbody", {
|
||||
children: [
|
||||
...rows,
|
||||
// 하위 로직이 여러 줄로 풀린 경우 — 호표 줄 수보다 많은 줄도 빠짐없이 보임
|
||||
...(lines ?? []).slice((state.one.logic.호표 ?? []).length).map((calc) =>
|
||||
el("tr", {
|
||||
children: [
|
||||
el("td", { text: calc.이름 }),
|
||||
el("td", { text: "" }),
|
||||
el("td", { text: `${formatNumber(calc.수량)} ${calc.단위}` }),
|
||||
el("td", { className: "m01-logic__money", text: formatNumber(calc.금액) }),
|
||||
],
|
||||
}),
|
||||
),
|
||||
...(state.answer?.ok && state.answer.sums
|
||||
? [
|
||||
el("tr", {
|
||||
attrs: { "data-total": String(state.answer.sums["계"] ?? 0) },
|
||||
children: [
|
||||
el("td", { text: tt("Sum_Total") }),
|
||||
el("td", { text: "" }),
|
||||
el("td", { text: "" }),
|
||||
el("td", { text: formatNumber(state.answer.sums["계"] ?? 0) }),
|
||||
],
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function buildCard(side: HTMLElement, state: State): void {
|
||||
const ho: HoLine | undefined = (state.one.logic.호표 ?? [])[state.selected];
|
||||
if (!ho) {
|
||||
side.replaceChildren(el("p", { className: "m01-logic__muted", text: "호표 줄이 없음" }));
|
||||
return;
|
||||
}
|
||||
const lines = state.answer?.ok ? (state.answer.lines ?? []) : [];
|
||||
const calc = lines[state.selected];
|
||||
const middle = state.answer?.ok ? (state.answer.middle ?? {}) : {};
|
||||
const ctx: EvalCtx = { values: state.values, middle };
|
||||
const related = relatedFinds(ho.수량, state.one.logic.중간 ?? []);
|
||||
|
||||
side.replaceChildren(
|
||||
el("h4", { text: `이 수량은 어디서 왔나 — ${ho.이름 ?? ho.요소}` }),
|
||||
el("dl", {
|
||||
className: "m01-logic__sums",
|
||||
children: [
|
||||
el("dt", { text: "지금 값" }),
|
||||
el("dd", {
|
||||
text: calc
|
||||
? `${formatNumber(calc.수량)} ${calc.단위} · ${formatNumber(calc.금액)}원`
|
||||
: "값을 넣어야 계산됨",
|
||||
}),
|
||||
el("dt", { text: "출처" }),
|
||||
el("dd", { text: calc?.출처 ?? ho.비목 ?? "-" }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const surcharge = related.filter((r) => r.source !== "이 줄" && r.source.includes("할증"));
|
||||
if (surcharge.length) {
|
||||
side.append(
|
||||
el("h4", { text: "적용된 할증" }),
|
||||
...surcharge.map((r) =>
|
||||
el("p", {
|
||||
text: `${r.source} = ${middle[r.source] !== undefined ? formatNumber(middle[r.source]) : "(입력 필요)"}`,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 쉬운 말 풀이 · 원문 표 미리보기 · 고급(식 그대로) — 흐름 그림과 같은 카드
|
||||
const note = el("div");
|
||||
side.append(note);
|
||||
buildNote(note, { expr: ho.수량, middles: state.one.logic.중간 ?? [], ctx });
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
/* M01 로직 테스트 컨테이너 — 틀 + 방식 A (방식 B·C 는 각자 파일에서 같은 접두 m01-test__ 를 씀) */
|
||||
.m01-test {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
height: 100%;
|
||||
padding: var(--spacing-12);
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
color: var(--color-text-body);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.m01-test__bar,
|
||||
.m01-test__tabs,
|
||||
.m01-test__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.m01-test__bar h2 {
|
||||
margin: 0 auto 0 0;
|
||||
font-size: 18px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.m01-test__tab {
|
||||
padding: var(--spacing-4) var(--spacing-12);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.m01-test__tab.is-active {
|
||||
background: var(--color-surface);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.m01-test__muted {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.m01-test__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-12);
|
||||
max-width: 640px;
|
||||
padding: var(--spacing-12);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.m01-test__card h3,
|
||||
.m01-test__step {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.m01-test__input {
|
||||
padding: var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.m01-test__grid {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.m01-test__grid th,
|
||||
.m01-test__grid td {
|
||||
padding: var(--spacing-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.m01-test__money {
|
||||
text-align: right !important;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.m01-test__total {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.m01-test__reasons {
|
||||
padding: var(--spacing-8);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-danger, #c0392b);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Test_Text.ts
|
||||
* 로직 테스트 컨테이너 글자 — [한국어, 영어] · 틀과 방식 파일(A·B·C)이 같이 씀
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
|
||||
const TEXT = {
|
||||
Title: ["일위대가 로직 테스트", "Logic test"],
|
||||
Pick_Logic: ["공종 고르기", "Pick a job"],
|
||||
Tab_A: ["A 질문·답", "A Wizard"],
|
||||
Tab_B: ["B 호표 + 설명", "B Table + notes"],
|
||||
Tab_C: ["C 흐름 그림", "C Flow"],
|
||||
Soon: ["준비 중", "Coming soon"],
|
||||
New_Logic: ["새로 만들기", "Create new"],
|
||||
Loading: ["불러오는 중", "Loading"],
|
||||
Failed: ["불러오지 못함", "Load failed"],
|
||||
Read_Only: ["시험 계산만 — 저장하지 않음", "Trial only — nothing is saved"],
|
||||
Step: ["{n} / {total}", "{n} / {total}"],
|
||||
Back: ["이전", "Back"],
|
||||
Next: ["다음", "Next"],
|
||||
Again: ["처음부터", "Start over"],
|
||||
Job_Title: ["이 공종의 단가를 셈합니다", "Price this job"],
|
||||
Job_Note: [
|
||||
"몇 가지만 물어봅니다 — 답을 마치면 결과 표가 나옵니다",
|
||||
"A few questions, then the result table",
|
||||
],
|
||||
Ask_Pick: ["「{name}」 — 어느 것으로 할까요?", "「{name}」 — which one?"],
|
||||
Ask_Number: ["「{name}」 값을 넣어 주세요{unit}", "Enter 「{name}」{unit}"],
|
||||
Ask_Range: ["범위 {min} ∼ {max}", "Range {min} ∼ {max}"],
|
||||
Mat_Title: ["쓰이는 재료·기계", "Materials and machines"],
|
||||
Mat_Note: [
|
||||
"기본은 원문 그대로 — 바꿔 보면 결과가 달라짐(저장 안 함) · 값이 여럿이면 그중 낮은 값으로 셈",
|
||||
"Default is the source as is — swapping only changes this trial; the lowest listed price is used",
|
||||
],
|
||||
Mat_None: ["이 공종은 재료·기계를 따로 고르지 않습니다", "No materials or machines to pick"],
|
||||
Mat_Default: ["기본 (원문 그대로)", "Default (as in source)"],
|
||||
Pick_All: ["(전체)", "(All)"],
|
||||
Use_Of: ["이 표의 용도", "Used for"],
|
||||
Mat_Price: ["단가", "Unit price"],
|
||||
Mat_NoPrice: ["단가 없음", "No price"],
|
||||
Stop_Missing: [
|
||||
"「{name}」 값을 아직 안 넣었습니다 — 값을 넣으면 계산됩니다",
|
||||
"「{name}」 is empty — enter it to calculate",
|
||||
],
|
||||
Stop_NoRow: [
|
||||
"넣은 값에 맞는 표 줄이 없습니다 — 값을 바꿔 보세요",
|
||||
"No table row fits these values — try other values",
|
||||
],
|
||||
Stop_Spec: [
|
||||
"「{name}」 규격이 여럿이라 정하지 못함 — 위 「쓰이는 재료·기계」에서 하나를 골라 주세요",
|
||||
"「{name}」 has several specs — pick one in the materials list above",
|
||||
],
|
||||
Stop_NoPrice: [
|
||||
"「{name}」 단가가 자료에 비어 있어 셈하지 못함 — 자료를 채워야 함",
|
||||
"「{name}」 has no price in the data yet",
|
||||
],
|
||||
Guide_A: [
|
||||
"한 번에 하나씩 묻는 대로 답하면 끝에 결과 표가 나옴|값을 잘못 넣었으면 「이전」으로 돌아가 고침|재료·기계를 바꿔 보는 칸은 뒤쪽 단계에 있음",
|
||||
"Answer one question at a time; the result table comes last|Use Back to fix a value|Swap materials in a later step",
|
||||
],
|
||||
Guide_B: [
|
||||
"위쪽 칸에 값을 넣으면 아래 호표(재료·인력·기계 줄)의 수량과 금액이 바로 바뀜|줄을 누르면 오른쪽에 그 수량이 어디서 왔는지 나옴|맨 아래 「합계」가 최종 금액",
|
||||
"Type values above and the table below updates at once|Click a row to see where its quantity comes from|The last row is the total",
|
||||
],
|
||||
Guide_C: [
|
||||
"왼쪽 「설계 값」을 넣으면 오른쪽으로 수량 → 단가 → 합계 순으로 흐름이 이어짐|상자를 누르면 값과 출처가 펼쳐짐|빨간 글이 뜨면 그 까닭 그대로 값을 채우거나 바꿈",
|
||||
"Enter design values on the left; quantity, price and total follow to the right|Click a box for details|Red text says what is missing",
|
||||
],
|
||||
Enter_Value: ["값을 넣어 주세요", "Enter a value"],
|
||||
Result_Title: ["결과", "Result"],
|
||||
Result_Stopped: ["계산이 멈춤", "Calculation stopped"],
|
||||
Col_Name: ["이름", "Name"],
|
||||
Col_Qty: ["수량", "Qty"],
|
||||
Col_Amount: ["금액", "Amount"],
|
||||
Sum_Total: ["합계", "Total"],
|
||||
Sum_Result: ["결과 값", "Result"],
|
||||
} as const satisfies Record<string, readonly [string, string]>;
|
||||
|
||||
export type TestTextKey = keyof typeof TEXT;
|
||||
|
||||
/** 엔진이 멈춘 까닭을 설계자 말로 — 모르는 글은 그대로 */
|
||||
export function plainReason(reason: string): string {
|
||||
const missing = /입력 「([^」]+)」 없음/.exec(reason);
|
||||
if (missing) return tt("Stop_Missing", { name: missing[1] });
|
||||
if (/맞는 줄 0개/.test(reason)) return tt("Stop_NoRow");
|
||||
const spec = /「([^」]+)」 후보 여럿/.exec(reason);
|
||||
if (spec) return tt("Stop_Spec", { name: spec[1] });
|
||||
const noPrice = /「([^」]+)」 값 없음/.exec(reason);
|
||||
if (noPrice) return tt("Stop_NoPrice", { name: noPrice[1] });
|
||||
return reason;
|
||||
}
|
||||
|
||||
/** 방식 첫 화면 「이 방식은 이렇게 씀」 — 줄 목록 */
|
||||
export const guideLines = (id: "A" | "B" | "C"): string[] =>
|
||||
tt(`Guide_${id}` as TestTextKey).split("|");
|
||||
|
||||
export function tt(key: TestTextKey, fill: Record<string, string | number> = {}): string {
|
||||
const entry = TEXT[key];
|
||||
const text: string = entry[currentLanguageIndex as 0 | 1] ?? entry[0];
|
||||
return text.replace(/\{(\w+)\}/g, (m, k: string) => (k in fill ? String(fill[k]) : m));
|
||||
}
|
||||
@@ -6,7 +6,7 @@ const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, "..", "..", "M01_MasterData", "M01_MasterData_UI_Test_C_Model.ts"),
|
||||
path.join(__dirname, "..", "..", "M01_MasterData", "M01_MasterData_UI_Logic_Flow_Model.ts"),
|
||||
"utf8",
|
||||
);
|
||||
const js = ts.transpileModule(source, {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""M01 로직 테스트 방식 C(흐름 그림) 뼈대 검증 — `M01_MasterData_UI_Test_C_Model.ts`.
|
||||
"""M01 로직 테스트 방식 C(흐름 그림) 뼈대 검증 — `M01_MasterData_UI_Logic_Flow_Model.ts`.
|
||||
|
||||
프론트에 JS 테스트 러너가 없어 Node 헬퍼(helper_m01_flow_c.cjs)가 TS를 트랜스파일해
|
||||
돌리고, pytest 는 그 결과(JSON)를 본다. 뼈대만 봄 — 금액은 엔진이 준 답을 그대로 옮기는지.
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
"""M01 로직 테스트 컨테이너 — 대상 10 개(틀 파일 TEST_KEYS)가 실제 산림품셈 로직이고 막히지 않았는지.
|
||||
"""M01 로직 테스트 컨테이너 — 견본 10 개가 실제 산림품셈 로직이고 막히지 않았는지.
|
||||
|
||||
방식 A·B·C 는 같은 `/calc` 를 씀 — 여기서는 10 개가 모두 첫 선택값으로 셈이 끝까지 가는지만 봄.
|
||||
흐름 그림(방식 C)과 고급 화면은 같은 `/calc` 를 씀 — 여기서는 10 개가 모두 첫 선택값으로 셈이 끝까지 가는지만 봄.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
|
||||
FRAME = Path(store.__file__).with_name("M01_MasterData_UI_Test.ts")
|
||||
KEYS = re.findall(r'^\s+"(GF\d{6})",', FRAME.read_text(encoding="utf-8"), re.M)
|
||||
# 견본 10 개 — 복잡도(입력·표 찾기·로직 부르기·덧줄·재료)로 잰 순서 · 재는 법 `resources/master_data/scripts/로직_복잡도_재기.py`
|
||||
KEYS = [
|
||||
"GF000181", # 12-16 맨홀
|
||||
"GF000177", # 12-12 날개벽
|
||||
"GF000173", # 12-11-1 VR관(소켓식) 부설
|
||||
"GF000174", # 12-11-2-1 흄관 부설
|
||||
"GF000160", # 12-4 합판거푸집
|
||||
"GF000182", # 12-17-1 펌프카 타설
|
||||
"GF000069", # 7-11 동력상하차기 집재
|
||||
"GF000219", # 13-4-1 돌쌓기 메쌓기(인력)
|
||||
"GF000119", # 9-16-1 노체포설
|
||||
"GF000137", # 10-4 중기운반 트레일러
|
||||
]
|
||||
|
||||
|
||||
def _first_inputs(logic: dict) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user