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:
@@ -2,12 +2,15 @@
|
||||
* M02_DF_Entry_Detail.ts
|
||||
* 단가산출 흐름도 — 「상세 내용」 칸 틀(정함 9): 머리(왼쪽 「상세 내용」 · 오른쪽 고른 상자 제목 · 알약이면 그 옆 알약 이름)
|
||||
* · 설정 · 본문 · 경고 차례. 상자 · 알약 · 선 무엇을 골라도 바뀜 — 본문은 Entry 가 종류별 판을 끼움.
|
||||
* 선 · 알약 본문은 규칙 판 · 알약 판(sub2)이 오기 전까지 이 파일이 흐름 줄(보내는 곳 → 받는 곳 · 출처 사슬 · 가는 곳)만 보임.
|
||||
* 경고 = 끊긴 곳 가운데 고른 것 몫 · 다른 로직 입력이 부르는 로직 읽기(겹쳐 부름) · 입력 값 물음 · 결과 단위 후보.
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import type { CfLogic, CfPort, CfStep } from "./M02_DF_Model";
|
||||
import { fetchLogics } from "../../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import type { CfAnswer, CfQuery } from "./M02_DF_Api";
|
||||
import type { CfBreak, CfValues } from "./M02_DF_Eval";
|
||||
import type { CfLogic } from "./M02_DF_Model";
|
||||
|
||||
const TEXT = {
|
||||
Detail: ["상세 내용", "Details"],
|
||||
@@ -20,14 +23,6 @@ const TEXT = {
|
||||
"Choose a box, pill or line in the diagram",
|
||||
],
|
||||
Line: ["선", "Line"],
|
||||
From: ["보내는 곳", "From"],
|
||||
To: ["받는 곳", "To"],
|
||||
Rule: ["선 규칙", "Line rule"],
|
||||
RuleNone: ["없음 — 값을 그대로 보냄", "None — sends the value as is"],
|
||||
Back: ["온 길(출처)", "Came from"],
|
||||
Forward: ["가는 길", "Goes to"],
|
||||
NoTrace: ["아직 따라갈 수 없음", "Can't trace yet"],
|
||||
Nowhere: ["이어진 곳 없음", "Not connected"],
|
||||
In: ["받는 알약", "Incoming pill"],
|
||||
Out: ["내보내는 알약", "Outgoing pill"],
|
||||
} as const;
|
||||
@@ -39,7 +34,7 @@ const tt = (key: keyof typeof TEXT): string =>
|
||||
export type DfPick =
|
||||
| { kind: "box"; id: string }
|
||||
| { kind: "link"; id: string }
|
||||
| { kind: "pill"; box: string; name: string; dir: "in" | "out" }
|
||||
| { kind: "pill"; box: string; port: string; dir: "in" | "out" }
|
||||
| null;
|
||||
|
||||
export interface DetailParts {
|
||||
@@ -91,7 +86,6 @@ export function detailFrame(): DetailParts {
|
||||
}
|
||||
|
||||
const boxName = (lg: CfLogic, id: string): string => lg.boxes.find((b) => b.id === id)?.name ?? id;
|
||||
const portText = (lg: CfLogic, p: CfPort): string => `${boxName(lg, p.box)} · ${p.name}`;
|
||||
|
||||
/** 머리 · 빈 자리 — 고른 것에 맞춤 · 설정 · 경고 줄은 Entry 가 채움 */
|
||||
export function drawHead(d: DetailParts, lg: CfLogic, pick: DfPick): void {
|
||||
@@ -107,77 +101,113 @@ export function drawHead(d: DetailParts, lg: CfLogic, pick: DfPick): void {
|
||||
? `${tt("Line")} · ${boxName(lg, link.from.box)} → ${boxName(lg, link.to.box)}`
|
||||
: "";
|
||||
d.pill.textContent =
|
||||
pick?.kind === "pill" ? `${tt(pick.dir === "in" ? "In" : "Out")} · ${pick.name}` : "";
|
||||
pick?.kind === "pill" ? `${tt(pick.dir === "in" ? "In" : "Out")} · ${pick.port}` : "";
|
||||
d.pill.hidden = pick?.kind !== "pill";
|
||||
}
|
||||
|
||||
/** 경고 — 화면 계산 · 검사 글 가운데 고른 것(상자 이름 · 알약 이름)이 든 줄만 */
|
||||
export function drawWarnings(d: DetailParts, lg: CfLogic, pick: DfPick, all: string[]): void {
|
||||
const names: string[] = [];
|
||||
if (pick?.kind === "box") names.push(`「${boxName(lg, pick.id)}」`);
|
||||
if (pick?.kind === "pill") names.push(`「${boxName(lg, pick.box)}」`);
|
||||
if (pick?.kind === "link") {
|
||||
const l = lg.links.find((x) => x.id === pick.id);
|
||||
if (l) names.push(`「${boxName(lg, l.from.box)}」`, `「${boxName(lg, l.to.box)}」`);
|
||||
}
|
||||
const mine = [...new Set(all)].filter(
|
||||
(w) => names.some((n) => w.includes(n)) && (pick?.kind !== "pill" || w.includes(pick.name)),
|
||||
/** 경고 — 끊긴 곳(Eval breaks) 가운데 고른 것 몫만 · 상자 = 그 상자 · 알약 = 그 알약 · 선 = 그 선 */
|
||||
export function drawWarnings(d: DetailParts, pick: DfPick, all: CfBreak[]): void {
|
||||
const mine = all.filter((k) =>
|
||||
pick?.kind === "box"
|
||||
? k.box === pick.id
|
||||
: pick?.kind === "pill"
|
||||
? k.box === pick.box && k.port === pick.port && !k.link
|
||||
: pick?.kind === "link"
|
||||
? k.link === pick.id
|
||||
: false,
|
||||
);
|
||||
d.warn.replaceChildren(
|
||||
...(mine.length
|
||||
? mine.map((w) => el("li", { text: w }))
|
||||
? [...new Set(mine.map((k) => k.text))].map((w) => el("li", { text: w }))
|
||||
: [el("li", { className: "m02-df__muted", text: tt("WarnNone") })]),
|
||||
);
|
||||
}
|
||||
|
||||
/** 선 본문 — 보내는 곳 → 받는 곳 · 선 규칙 글(규칙 판이 오면 그쪽이 그림) */
|
||||
export function lineBody(
|
||||
host: HTMLElement,
|
||||
/** 다른 로직 입력이 부르는 로직들 — 그 로직이 또 부르는 것까지(겹쳐 부름 · 한 번씩) · 못 읽은 것은 빼고 까닭 */
|
||||
export async function loadLogics(
|
||||
lg: CfLogic,
|
||||
id: string,
|
||||
ruleText: string | null,
|
||||
): void {
|
||||
const l = lg.links.find((x) => x.id === id);
|
||||
if (!l) return void host.replaceChildren();
|
||||
host.replaceChildren(
|
||||
el("dl", {
|
||||
className: "m02-df__detail-dl",
|
||||
children: [
|
||||
el("dt", { text: tt("From") }),
|
||||
el("dd", { text: portText(lg, l.from) }),
|
||||
el("dt", { text: tt("To") }),
|
||||
el("dd", { text: portText(lg, l.to) }),
|
||||
el("dt", { text: tt("Rule") }),
|
||||
el("dd", { text: ruleText ?? tt("RuleNone") }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
get: (key: string) => Promise<CfLogic | null>,
|
||||
): Promise<{ logics: Record<string, CfLogic>; errors: string[] }> {
|
||||
const logics: Record<string, CfLogic> = {};
|
||||
const errors: string[] = [];
|
||||
const keysOf = (x: CfLogic): string[] =>
|
||||
x.boxes.flatMap((b) =>
|
||||
b.input?.mode === "logic" && b.input.logic?.key ? [b.input.logic.key] : [],
|
||||
);
|
||||
const seen = new Set<string>(lg.key ? [lg.key] : []);
|
||||
let todo = keysOf(lg);
|
||||
while (todo.length) {
|
||||
const key = todo.shift()!;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
try {
|
||||
const got = await get(key);
|
||||
if (!got) throw new Error(key);
|
||||
logics[key] = got;
|
||||
todo = [...todo, ...keysOf(got)];
|
||||
} catch (e) {
|
||||
errors.push(`${key} — ${e instanceof Error ? e.message : String(e)}`);
|
||||
}
|
||||
}
|
||||
return { logics, errors };
|
||||
}
|
||||
|
||||
/** 알약 본문 — 온 길(출처 사슬) · 가는 길 · 흐름 따라가기(Model traceBack · traceForward)가 아직이면 한 줄 */
|
||||
export function pillBody(
|
||||
host: HTMLElement,
|
||||
lg: CfLogic,
|
||||
back: () => CfStep[],
|
||||
forward: () => CfStep[],
|
||||
): void {
|
||||
const list = (steps: () => CfStep[]): HTMLElement => {
|
||||
let got: CfStep[];
|
||||
try {
|
||||
got = steps();
|
||||
} catch {
|
||||
return el("p", { className: "m02-df__muted", text: tt("NoTrace") });
|
||||
/** 결과 단위 후보 — 품셈 로직에 다섯 번 넘게 쓰인 「원/…」 차례(설명 붙은 드문 단위는 뺌) */
|
||||
export 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);
|
||||
}
|
||||
if (!got.length) return el("p", { className: "m02-df__muted", text: tt("Nowhere") });
|
||||
return el("ol", {
|
||||
className: "m02-df__detail-trace",
|
||||
children: got.map((s) => el("li", { text: portText(lg, { box: s.box, name: s.port }) })),
|
||||
});
|
||||
};
|
||||
host.replaceChildren(
|
||||
el("h5", { text: tt("Back") }),
|
||||
list(back),
|
||||
el("h5", { text: tt("Forward") }),
|
||||
list(forward),
|
||||
} 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 이 스스로 봄 */
|
||||
export 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];
|
||||
});
|
||||
}
|
||||
|
||||
/** 입력 값 받기 — 받은 것만 묶고 못 받은 것은 쉬운 말 */
|
||||
export async function fetchValues(
|
||||
ask: (qs: CfQuery[], els: never) => Promise<CfAnswer[]>,
|
||||
lg: CfLogic,
|
||||
): Promise<{ values: CfValues; errors: string[] }> {
|
||||
const asks = queriesOf(lg);
|
||||
if (!asks.length) return { values: {}, errors: [] };
|
||||
const answers = await ask(
|
||||
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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user