feat(m02-df): 흐름도 잇기 — 고름 · 상세 내용 판 · 흐름 길 · 끊긴 곳 · 다른 로직 · 표준 표
- 고름 = 상자 · 알약{box,port,dir} · 선 — 다이어그램 · 검증 칸 줄 · 알약 판 상자 이름 누름 모두 같은 고름으로
- 상세 내용 본문 = 입력 · 함수 · 출력 판 · 알약 = renderPillPanel · 선 = renderLinkPanel · 경고 = 끊긴 곳 가운데 고른 것 몫
- 알약 · 선 고름 = Model traceBack/traceForward 로 Diagram opts.trace · Eval breaks 로 Diagram opts.breaks
- 다른 로직 입력이 부르는 로직(저장 DF- · 견본)을 겹쳐 읽어 Eval opts.logics 로(셈 · 검사 · 판 모두)
- 「표준 표」 단추 = renderStandards 판(공용 모달)
- 입력 값 물음 · 결과 단위 후보는 Entry_Detail 로 옮김(700 줄 아래)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
@@ -18,17 +18,21 @@ import {
|
||||
showConfirmDialog,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { formatMoney } from "../../M01_MasterData/M01_MasterData_UI_Logic_Money";
|
||||
import { fetchLogics } from "../../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import type { CfListItem, CfQuery } from "./M02_DF_Api";
|
||||
import type { CfEvalResult, CfValues } from "./M02_DF_Eval";
|
||||
import type { CfKind, CfLogic, CfPort } from "./M02_DF_Model";
|
||||
import type { CfListItem } from "./M02_DF_Api";
|
||||
import type { CfBreak, CfEvalOptions, CfEvalResult, CfValues } from "./M02_DF_Eval";
|
||||
import type { CfKind, CfLogic, CfPort, CfStep } from "./M02_DF_Model";
|
||||
import type { CfSel } from "./M02_DF_Verify";
|
||||
import type { DfPillSel } from "./M02_DF_Diagram_Parts";
|
||||
import {
|
||||
detailFrame,
|
||||
drawHead,
|
||||
drawWarnings,
|
||||
lineBody,
|
||||
pillBody,
|
||||
fetchValues,
|
||||
loadLogics,
|
||||
queriesOf,
|
||||
unitChoices,
|
||||
type DfPick,
|
||||
} from "./M02_DF_Entry_Detail";
|
||||
import "./M02_DF_Entry.css";
|
||||
@@ -45,8 +49,10 @@ const loadMods = () =>
|
||||
import("./M02_DF_Summary"),
|
||||
import("./M02_DF_Sample"),
|
||||
import("./M02_DF_InputPanel"),
|
||||
import("./M02_DF_Rule"),
|
||||
]).then(([model, ev, api, diagram, func, output, summary, sample, input, rule]) => ({
|
||||
import("./M02_DF_PillPanel"),
|
||||
import("./M02_DF_LinkPanel"),
|
||||
import("./M02_DF_Standards"),
|
||||
]).then(([model, ev, api, diagram, func, output, summary, sample, input, pill, line, std]) => ({
|
||||
model,
|
||||
ev,
|
||||
api,
|
||||
@@ -56,7 +62,9 @@ const loadMods = () =>
|
||||
summary,
|
||||
sample,
|
||||
input,
|
||||
rule,
|
||||
pill,
|
||||
line,
|
||||
std,
|
||||
}));
|
||||
type Mods = Awaited<ReturnType<typeof loadMods>>;
|
||||
|
||||
@@ -81,6 +89,8 @@ const TEXT = {
|
||||
Diagram: ["다이어그램", "Diagram"],
|
||||
Summary: ["원가", "Cost"],
|
||||
Color: ["상자 색", "Box colour"],
|
||||
Standards: ["표준 표", "Standards"],
|
||||
Logics: ["다른 로직을 못 읽음 — {v}", "Couldn't read other logics — {v}"],
|
||||
NoName: ["로직 이름을 넣어 주세요", "Enter a logic name"],
|
||||
SaveAnyway: [
|
||||
"고칠 곳이 있음 — {v} · 그래도 저장할까요?",
|
||||
@@ -113,63 +123,6 @@ const te = (key: TextKey, vars: Record<string, string | number> = {}): string =>
|
||||
|
||||
const muted = (text: string): HTMLElement => el("p", { className: "m02-df__muted", text });
|
||||
|
||||
/** 결과 단위 후보 — 품셈 로직에 다섯 번 넘게 쓰인 「원/…」 차례(설명 붙은 드문 단위는 뺌) */
|
||||
async function unitChoices(): Promise<string[]> {
|
||||
const count = new Map<string, number>();
|
||||
try {
|
||||
for (const x of await fetchLogics()) {
|
||||
if (x.결과단위.startsWith("원/")) count.set(x.결과단위, (count.get(x.결과단위) ?? 0) + 1);
|
||||
}
|
||||
} catch {
|
||||
/* 못 받으면 기본 하나 */
|
||||
}
|
||||
const units = [...count.entries()]
|
||||
.filter(([u, n]) => n >= 5 && !/[\s()「·]/.test(u))
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([u]) => u);
|
||||
return units.length ? units : ["원/㎡"];
|
||||
}
|
||||
|
||||
/** 입력 값 물음 — 그룹 입력 상자의 요소(알약 = 요소 이름) · 표(알약 「표」) · 사용자 요소 · 변수는 Eval 이 스스로 봄 */
|
||||
function queriesOf(lg: CfLogic): { q: CfQuery; box: string; port: string }[] {
|
||||
return lg.boxes.flatMap((b) => {
|
||||
const inp = b.input;
|
||||
if (b.kind !== "input" || !inp || inp.mode !== "group" || !inp.group) return [];
|
||||
const group = inp.group;
|
||||
const els = inp.elements
|
||||
.filter((e) => e.ref && !e.ref.startsWith("UE-"))
|
||||
.map((e) => {
|
||||
// 기계 요소는 「키#값 칸」 — 서버에는 키와 칸(field)을 따로 넘김
|
||||
const [ref, field] = group === "기계" ? e.ref.split("#") : [e.ref, undefined];
|
||||
const q: CfQuery = { id: `${b.id}\u0001${e.name}`, group, ref, unit: e.unit };
|
||||
if (field) q.field = field;
|
||||
return { q, box: b.id, port: e.name };
|
||||
});
|
||||
const table = inp.table
|
||||
? [{ q: { id: `${b.id}\u0001표`, group, table: inp.table }, box: b.id, port: "표" }]
|
||||
: [];
|
||||
return [...els, ...table];
|
||||
});
|
||||
}
|
||||
|
||||
/** 입력 값 받기 — 받은 것만 묶고 못 받은 것은 쉬운 말 */
|
||||
async function fetchValues(M: Mods, lg: CfLogic): Promise<{ values: CfValues; errors: string[] }> {
|
||||
const asks = queriesOf(lg);
|
||||
if (!asks.length) return { values: {}, errors: [] };
|
||||
const answers = await M.api.cfValues(
|
||||
asks.map((a) => a.q),
|
||||
lg.userElements as never,
|
||||
);
|
||||
const values: CfValues = {};
|
||||
const errors: string[] = [];
|
||||
for (const a of asks) {
|
||||
const got = answers.find((x) => x.id === a.q.id);
|
||||
if (!got?.ok) errors.push(`${a.port}${got?.reason ? ` — ${got.reason}` : ""}`);
|
||||
else (values[a.box] ??= {})[a.port] = got.rows ?? got.value ?? null;
|
||||
}
|
||||
return { values, errors };
|
||||
}
|
||||
|
||||
/** M01 단가산출 막대의 다른 시험판 단추 옆에 놓는 단추 — 누르면 시험판 페이지 */
|
||||
export function createDataFlowButton(): HTMLButtonElement {
|
||||
return createButton({ label: te("Open"), variant: "filled", size: "sm", onClick: openDataFlow });
|
||||
@@ -330,6 +283,10 @@ function openDataFlow(): void {
|
||||
const pickedBox = (): string | null => (pick?.kind === "box" ? pick.id : null);
|
||||
let values: CfValues = {};
|
||||
let valueSig = "";
|
||||
let logics: NonNullable<CfEvalOptions["logics"]> = {};
|
||||
let logicSig = "";
|
||||
let logicNotes: string[] = [];
|
||||
let broken: CfBreak[] = [];
|
||||
let notes: string[] = [];
|
||||
let result: CfEvalResult;
|
||||
let busy = false;
|
||||
@@ -355,15 +312,15 @@ function openDataFlow(): void {
|
||||
const outHost = el("div");
|
||||
const inHost = el("div");
|
||||
const summaryHost = el("div");
|
||||
const lineHost = el("div");
|
||||
const linkHost = el("div");
|
||||
const pillHost = el("div");
|
||||
const detail = detailFrame();
|
||||
detail.settings.append(colorRow);
|
||||
detail.body.append(inHost, funcHost, outHost, lineHost, pillHost);
|
||||
detail.body.append(inHost, funcHost, outHost, linkHost, pillHost);
|
||||
|
||||
const evalNow = (): CfEvalResult => {
|
||||
try {
|
||||
return M.ev.evalLogic(lg, values);
|
||||
return M.ev.evalLogic(lg, values, { logics });
|
||||
} catch (e) {
|
||||
const cost = { 노무비: 0, 재료비: 0, 경비: 0, 값들: {}, 계: 0 };
|
||||
return { ports: {}, outputs: {}, cost, errors: [M.api.cfPlain(e)] };
|
||||
@@ -387,7 +344,7 @@ function openDataFlow(): void {
|
||||
const sig = JSON.stringify(queriesOf(lg).map((a) => a.q));
|
||||
if (sig === valueSig) return;
|
||||
valueSig = sig;
|
||||
void fetchValues(M, lg).then(
|
||||
void fetchValues(M.api.cfValues, lg).then(
|
||||
(got) => {
|
||||
if (sig !== valueSig) return;
|
||||
values = got.values;
|
||||
@@ -401,6 +358,54 @@ function openDataFlow(): void {
|
||||
);
|
||||
};
|
||||
|
||||
/** 다른 로직 입력이 부르는 로직들 다시 읽기 — 부르는 열쇠가 바뀐 때만 */
|
||||
const refreshLogics = (): void => {
|
||||
const sig = JSON.stringify(lg.boxes.map((b) => b.input?.logic?.key ?? ""));
|
||||
if (sig === logicSig) return;
|
||||
logicSig = sig;
|
||||
const get = async (key: string): Promise<CfLogic | null> =>
|
||||
key.startsWith("DF-") ? M.api.cfGet(key) : M.sample.sampleLoad(key);
|
||||
void loadLogics(lg, get).then((got) => {
|
||||
if (sig !== logicSig) return;
|
||||
logics = got.logics;
|
||||
logicNotes = got.errors.length ? [te("Logics", { v: got.errors.join(" · ") })] : [];
|
||||
redraw();
|
||||
});
|
||||
};
|
||||
|
||||
/** 고름 — 검증 칸 줄 · 알약 판의 상자 이름 누름 */
|
||||
const choose = (sel: CfSel): void => {
|
||||
const box = lg.boxes.find((b) => b.id === sel.box);
|
||||
pick = sel.link
|
||||
? { kind: "link", id: sel.link }
|
||||
: box && sel.port
|
||||
? { kind: "pill", box: box.id, port: sel.port, dir: M.model.portDir(box, sel.port) }
|
||||
: box
|
||||
? { kind: "box", id: box.id }
|
||||
: null;
|
||||
redraw();
|
||||
};
|
||||
|
||||
/** 흐름 길 — 알약 = 그 알약에서 앞뒤 · 선 = 보낸 알약 앞 · 받는 알약 뒤 */
|
||||
const traceNow = (): { back: CfStep[]; forward: CfStep[] } | null => {
|
||||
try {
|
||||
if (pick?.kind === "pill") {
|
||||
const p = { box: pick.box, name: pick.port };
|
||||
return { back: M.model.traceBack(lg, p), forward: M.model.traceForward(lg, p) };
|
||||
}
|
||||
const id = pick?.kind === "link" ? pick.id : "";
|
||||
const link = lg.links.find((l) => l.id === id);
|
||||
if (link)
|
||||
return {
|
||||
back: M.model.traceBack(lg, link.from),
|
||||
forward: M.model.traceForward(lg, link.to),
|
||||
};
|
||||
} catch {
|
||||
/* 따라갈 수 없으면 칠하지 않음 */
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const name = createInputField({
|
||||
label: te("Name"),
|
||||
placeholder: te("NamePlaceholder"),
|
||||
@@ -417,18 +422,16 @@ function openDataFlow(): void {
|
||||
});
|
||||
|
||||
const diagramOn = {
|
||||
select: (id: string | null): void => {
|
||||
pick = !id
|
||||
? null
|
||||
: lg.links.some((l) => l.id === id)
|
||||
? { kind: "link", id }
|
||||
: { kind: "box", id };
|
||||
drawPanel();
|
||||
},
|
||||
/** 알약 고름 — 다이어그램(sub8)이 알려 주면 상세 내용이 알약 판으로 */
|
||||
pill: (p: { box: string; name: string; dir: "in" | "out" } | null): void => {
|
||||
pick = p ? { kind: "pill", ...p } : null;
|
||||
drawPanel();
|
||||
select: (id: string | null, pill?: DfPillSel): void => {
|
||||
pick = pill
|
||||
? { kind: "pill", ...pill }
|
||||
: !id
|
||||
? null
|
||||
: lg.links.some((l) => l.id === id)
|
||||
? { kind: "link", id }
|
||||
: { kind: "box", id };
|
||||
// 흐름 길 · 판을 새로 — 다이어그램이 제 그림을 그린 뒤
|
||||
setTimeout(redraw);
|
||||
},
|
||||
move: (id: string, x: number, y: number): void => edit(M.model.moveBox(lg, id, x, y)),
|
||||
link: (from: CfPort, to: CfPort): string | null => {
|
||||
@@ -455,35 +458,20 @@ function openDataFlow(): void {
|
||||
const box = lg.boxes.find((b) => b.id === pickedBox());
|
||||
drawHead(detail, lg, pick);
|
||||
colorRow.hidden = !box;
|
||||
lineHost.hidden = pick?.kind !== "link";
|
||||
linkHost.hidden = pick?.kind !== "link";
|
||||
pillHost.hidden = pick?.kind !== "pill";
|
||||
if (pick?.kind === "link") {
|
||||
const linkId = pick.id;
|
||||
const rule = lg.links.find((l) => l.id === linkId)?.rule;
|
||||
let text: string | null = null;
|
||||
try {
|
||||
text = rule ? M.rule.ruleLabel(rule) : null;
|
||||
} catch {
|
||||
text = rule ? rule.kind : null;
|
||||
}
|
||||
lineBody(lineHost, lg, pick.id, text);
|
||||
}
|
||||
if (pick?.kind === "pill") {
|
||||
const port = { box: pick.box, name: pick.name };
|
||||
pillBody(
|
||||
if (pick?.kind === "link")
|
||||
M.line.renderLinkPanel(linkHost, lg, pick.id, { change: edit }, { values, logics });
|
||||
if (pick?.kind === "pill")
|
||||
M.pill.renderPillPanel(
|
||||
pillHost,
|
||||
lg,
|
||||
() => M.model.traceBack(lg, port),
|
||||
() => M.model.traceForward(lg, port),
|
||||
pick.box,
|
||||
pick.port,
|
||||
{ pick: (id) => choose({ box: id }) },
|
||||
{ values, logics },
|
||||
);
|
||||
}
|
||||
let checks: string[] = [];
|
||||
try {
|
||||
checks = M.ev.checkLogic(lg, values);
|
||||
} catch {
|
||||
checks = [];
|
||||
}
|
||||
drawWarnings(detail, lg, pick, [...checks, ...(result?.errors ?? [])]);
|
||||
drawWarnings(detail, pick, broken);
|
||||
if (box && /^#[0-9a-f]{6}$/i.test(box.color)) colorBox.value = box.color;
|
||||
funcHost.hidden = box?.kind !== "function";
|
||||
outHost.hidden = box?.kind !== "output";
|
||||
@@ -497,23 +485,34 @@ function openDataFlow(): void {
|
||||
const redraw = (): void => {
|
||||
result = evalNow();
|
||||
refreshValues();
|
||||
refreshLogics();
|
||||
const linkId = pick?.kind === "link" ? pick.id : "";
|
||||
if (linkId && !lg.links.some((l) => l.id === linkId)) pick = null;
|
||||
try {
|
||||
broken = M.ev.breaks(lg, values, { logics });
|
||||
} catch {
|
||||
broken = result.breaks ?? [];
|
||||
}
|
||||
unsaved.hidden = lg === savedLg;
|
||||
undo.disabled = !stack.length;
|
||||
deleteButton.hidden = !lg.key;
|
||||
const selected =
|
||||
pick?.kind === "box" || pick?.kind === "link" ? pick.id : (pick?.box ?? null);
|
||||
M.diagram.renderDiagram(diagramHost, lg, diagramOn, { values: result.ports, selected });
|
||||
M.diagram.renderDiagram(diagramHost, lg, diagramOn, {
|
||||
values: result.ports,
|
||||
selected: pick?.kind === "box" || pick?.kind === "link" ? pick.id : null,
|
||||
selectedPill:
|
||||
pick?.kind === "pill" ? { box: pick.box, port: pick.port, dir: pick.dir } : null,
|
||||
trace: traceNow(),
|
||||
breaks: broken,
|
||||
});
|
||||
drawPanel();
|
||||
// 「고칠 것」 검사가 받은 입력 값으로 하도록 값도 넘김 — Summary 가 opts.values 를 받으면 쓰임
|
||||
// ponytail: 지금 Summary 는 checkLogic(lg) 을 값 없이 불러 「값이 아직 없음」 이 남음(sub5 고침 뒤 이 꼴 그대로 둠)
|
||||
(M.summary.renderSummary as (...args: unknown[]) => void)(
|
||||
M.summary.renderSummary(
|
||||
summaryHost,
|
||||
lg,
|
||||
result,
|
||||
{ change: edit },
|
||||
{ change: edit, select: choose },
|
||||
{ values },
|
||||
);
|
||||
note.textContent = [...notes, ...result.errors].slice(0, 4).join("\n");
|
||||
note.textContent = [...notes, ...logicNotes, ...result.errors].slice(0, 4).join("\n");
|
||||
};
|
||||
|
||||
const save = async (): Promise<void> => {
|
||||
@@ -524,7 +523,7 @@ function openDataFlow(): void {
|
||||
}
|
||||
let problems: string[] = [];
|
||||
try {
|
||||
problems = M.ev.checkLogic(lg, values);
|
||||
problems = M.ev.checkLogic(lg, values, { logics });
|
||||
} catch {
|
||||
problems = [];
|
||||
}
|
||||
@@ -588,6 +587,18 @@ function openDataFlow(): void {
|
||||
el("div", {
|
||||
className: "m02-df__actions",
|
||||
children: [
|
||||
createButton({
|
||||
label: te("Standards"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () =>
|
||||
openModal({
|
||||
title: te("Standards"),
|
||||
closeLabel: te("Close"),
|
||||
mount: (host: HTMLElement) =>
|
||||
M.std.renderStandards(host, { change: () => redraw() }),
|
||||
}),
|
||||
}),
|
||||
undo,
|
||||
createButton({ label: te("Save"), size: "sm", onClick: () => void save() }),
|
||||
deleteButton,
|
||||
|
||||
Reference in New Issue
Block a user