Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_5

This commit is contained in:
2026-10-01 19:54:08 +09:00
10 changed files with 143 additions and 230 deletions
+60 -178
View File
@@ -1,14 +1,22 @@
/* =============================================================================
* M02_DF_Detail.ts
* 상세 내용 판(선 · 알약) 공용 — 값 읽기 · 규칙 미리 보기 · 겹침/안 맞음 · 흐름 줄 · 끊김 까닭 · DOM 없음
* 모델의 traceBack · traceForward · breaks · applyRule · ruleLabel 이 아직 준비 중이면 같은 뜻의 작은 셈으로 메움
* 상세 내용 판(선 · 알약) 공용 — 값 읽기 · 규칙 미리 보기 · 끊김 글 · 흐름 줄 · DOM 없음
* 셈은 모델 것 그대로(Eval 의 evalLogic · breaks · Model 의 traceBack · traceForward · Rule 의 ruleLabel)
* 글 [한, 영] 짝은 아래 TEXT
* ========================================================================== */
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { breaks, evalLogic, type CfEvalResult, type CfValue, type CfValues } from "./M02_DF_Eval";
import {
portsIn,
breaks,
evalLogic,
type CfBreak,
type CfEvalOptions,
type CfEvalResult,
type CfValue,
type CfValues,
} from "./M02_DF_Eval";
import {
portDir,
portsOut,
traceBack,
traceForward,
@@ -17,26 +25,15 @@ import {
type CfLink,
type CfLogic,
type CfPort,
type CfRound,
type CfRule,
type CfStep,
} from "./M02_DF_Model";
import { applyRule, ruleLabel } from "./M02_DF_Rule";
import { ruleLabel } from "./M02_DF_Rule";
const TEXT = {
Table: ["표 {n}줄", "table, {n} rows"],
Empty: ["값 없음", "no value"],
LabelLookup: ["표에서 「{col}」 찾기", "Look up “{col}” in the table"],
LabelGate: ["「{pill}」 {op} {value} 일 때만 보냄", "Send only when “{pill}” {op} {value}"],
LabelScale: ["값 {op} {value}", "value {op} {value}"],
LabelRound: ["소수 {places}자리 {method}", "{places} decimals, {method}"],
NoRule: ["규칙 없음 — 값이 그대로 감", "No rule — the value goes as is"],
Blocked: ["통과 안 됨", "does not pass"],
NoRowLater: ["표에서 맞는 줄은 계산 뒤에 보임", "The matching row shows after calculation"],
EmptyIn: ["빈 받는 자리 — 선이 없음", "Empty receiving spot — no line"],
NoCell: ["받은 알약을 칸에 놓지 않음", "The received pill is not placed in a cell"],
Dead: ["아직 어디로도 안 감", "Goes nowhere yet"],
NoValue: ["값이 아직 없음", "No value yet"],
} as const;
export const td = (key: keyof typeof TEXT, fill: Record<string, string | number> = {}): string =>
@@ -48,16 +45,22 @@ export const td = (key: keyof typeof TEXT, fill: Record<string, string | number>
export const GATE_OPS: CfGateOp[] = ["=", "≠", "≥", "≤", ">", "<"];
export const SCALE_OPS: ("×" | "÷" | "+%" | "-%")[] = ["×", "÷", "+%", "-%"];
/** 셈에 넘길 것 — 값 묶음 · 다른 로직들 */
export interface DetailOptions {
values?: CfValues;
logics?: CfEvalOptions["logics"];
}
export const boxOf = (lg: CfLogic, id: string): CfBox | undefined =>
lg.boxes.find((b) => b.id === id);
export const portText = (lg: CfLogic, p: CfPort): string =>
`${boxOf(lg, p.box)?.name ?? "?"} › ${p.name}`;
/** 계산 한 번 — 못 하면 null */
export function evalSafe(lg: CfLogic, values?: CfValues): CfEvalResult | null {
/** 계산 한 번 — 한창 만드는 중 막히면 null */
export function evalSafe(lg: CfLogic, o: DetailOptions = {}): CfEvalResult | null {
try {
return evalLogic(lg, values);
return evalLogic(lg, o.values, { logics: o.logics });
} catch {
return null;
}
@@ -79,176 +82,55 @@ export function unitOf(lg: CfLogic, p: CfPort): string {
}
/** 알약이 나가는(out) 쪽인가 */
export const isOut = (box: CfBox, name: string): boolean =>
portsOut(box).some((p) => p.name === name);
export const isOut = (box: CfBox, name: string): boolean => portDir(box, name) === "out";
/* ── 규칙 ─────────────────────────────────────────────────────────────── */
/** 선 위 규칙 글 */
export const labelOf = (rule: CfRule | undefined): string =>
rule ? ruleLabel(rule) : td("NoRule");
const num = (v: unknown): number | null => {
const n = typeof v === "number" ? v : Number(v);
return Number.isFinite(n) && v !== "" && v !== null ? n : null;
};
/** 조건 통과 — 숫자면 숫자끼리 · 아니면 글끼리 */
export function gatePass(op: CfGateOp, v: unknown, value: number | string): boolean {
const a = num(v);
const b = num(value);
const [x, y] = a !== null && b !== null ? [a, b] : [String(v ?? ""), String(value)];
switch (op) {
case "=":
return x === y;
case "≠":
return x !== y;
case "≥":
return x >= y;
case "≤":
return x <= y;
case ">":
return x > y;
default:
return x < y;
}
/** 지나가기 전 값 → 지난 뒤 값 — 지난 뒤 값은 셈이 받는 알약에 실제로 들여보낸 것 · 못 들어갔으면 끊긴 까닭 */
export function preview(link: CfLink, ev: CfEvalResult | null): { before: string; after: string } {
const before = showValue(valueOf(ev, link.from));
if (!link.rule) return { before, after: before };
const arrived = ev?.arrived?.[link.to.box]?.[link.to.name];
if (arrived !== undefined && arrived !== null) return { before, after: showValue(arrived) };
const why = ev?.breaks?.find((b) => b.link === link.id)?.text;
return { before, after: why ?? td("Empty") };
}
export function roundValue(r: CfRound, v: number): number {
const k = 10 ** r.places;
const f = r.method === "올림" ? Math.ceil : r.method === "버림" ? Math.floor : Math.round;
return f(v * k) / k;
/** 이 선에서 알릴 것 — 이 선이 막힌 까닭 · 같은 알약에서 나간 조건 선의 겹침 · 안 맞음 */
export function linkNotes(link: CfLink, ev: CfEvalResult | null): string[] {
return (ev?.breaks ?? [])
.filter(
(b) =>
b.link === link.id ||
((b.kind === "조건 겹침" || b.kind === "조건 안 맞음") &&
b.box === link.from.box &&
b.port === link.from.name),
)
.map((b) => b.text);
}
/** 선 위 규칙 글 — 모델이 준비되면 그것, 아니면 같은 뜻의 글 */
export function labelOf(lg: CfLogic, rule: CfRule | undefined): string {
if (!rule) return td("NoRule");
try {
return ruleLabel(rule);
} catch {
if (rule.kind === "lookup") return td("LabelLookup", { col: rule.col });
if (rule.kind === "gate")
return td("LabelGate", { pill: portText(lg, rule.from), op: rule.op, value: rule.value });
if (rule.kind === "scale") return td("LabelScale", { op: rule.op, value: rule.value });
return td("LabelRound", { places: rule.places, method: rule.method });
}
}
/** 지나가기 전 값 → 지난 뒤 값 미리 보기 */
export function preview(
link: CfLink,
rule: CfRule | undefined,
ev: CfEvalResult | null,
): { before: string; after: string } {
const raw = valueOf(ev, link.from);
const before = showValue(raw);
if (!rule) return { before, after: before };
const conds: Record<string, unknown> = {};
if (rule.kind === "lookup") for (const c of rule.conds) conds[c.col] = valueOf(ev, c.from);
try {
const r = applyRule(rule, raw, conds);
return {
before,
after: r.value === null ? (r.why ?? td("Empty")) : showValue(r.value as CfValue),
};
} catch {
const n = num(raw);
if (rule.kind === "gate")
return {
before,
after: gatePass(rule.op, valueOf(ev, rule.from), rule.value) ? before : td("Blocked"),
};
if (rule.kind === "lookup" || n === null) return { before, after: td("NoRowLater") };
if (rule.kind === "round") return { before, after: showValue(roundValue(rule, n)) };
const v = rule.value;
const out =
rule.op === "×"
? n * v
: rule.op === "÷"
? v
? n / v
: NaN
: rule.op === "+%"
? n * (1 + v / 100)
: n * (1 - v / 100);
return { before, after: Number.isFinite(out) ? showValue(out) : td("Empty") };
}
}
/** 같은 알약에서 나간 조건 선 — 지금 값으로 통과하는 수 */
export function gateReport(
lg: CfLogic,
link: CfLink,
ev: CfEvalResult | null,
): { total: number; pass: string[] } {
const same = lg.links.filter(
(l) =>
l.from.box === link.from.box && l.from.name === link.from.name && l.rule?.kind === "gate",
);
const pass = same
.filter((l) => {
const r = l.rule as Extract<CfRule, { kind: "gate" }>;
return gatePass(r.op, valueOf(ev, r.from), r.value);
})
.map((l) => l.id);
return { total: same.length, pass };
}
/* ── 흐름 · 끊김 ─────────────────────────────────────────────────────── */
/** 거슬러 오르는 길(dir "in" = 온 길) · 따라가는 길(dir "out") — 모델 것, 없으면 작은 셈 */
/** 온 길(dir "in") · 가는 길(dir "out") — 모델 것에서 자기 자신 줄은 뺌 */
export function trace(lg: CfLogic, p: CfPort, dir: "in" | "out"): CfStep[] {
try {
return dir === "in" ? traceBack(lg, p) : traceForward(lg, p);
} catch {
const out: CfStep[] = [];
const seen = new Set<string>();
const walk = (port: CfPort, back: boolean): void => {
const key = `${port.box}/${port.name}`;
if (seen.has(key)) return;
seen.add(key);
const box = boxOf(lg, port.box);
if (!box) return;
if (back) {
const into = lg.links.filter(
(l) => l.to.box === port.box && (isOut(box, port.name) || l.to.name === port.name),
);
for (const l of into) {
out.push({ box: l.from.box, port: l.from.name, dir: "out", link: l.id, rule: l.rule });
walk(l.from, true);
}
} else {
const outs = lg.links.filter((l) => l.from.box === port.box && l.from.name === port.name);
for (const l of outs) {
out.push({ box: l.to.box, port: l.to.name, dir: "in", link: l.id, rule: l.rule });
const next = boxOf(lg, l.to.box);
for (const o of next ? portsOut(next) : []) walk({ box: next!.id, name: o.name }, false);
}
}
};
walk(p, dir === "in");
return out;
}
const steps = dir === "in" ? traceBack(lg, p) : traceForward(lg, p);
return steps.filter((s) => !(s.box === p.box && s.port === p.name));
}
/** 그 상자(알약)에서 끊긴 곳 — 쉬운 말 */
export function breakLines(lg: CfLogic, boxId: string, name?: string): string[] {
export function breakLines(
lg: CfLogic,
boxId: string,
name?: string,
o: DetailOptions = {},
): string[] {
let list: CfBreak[] = [];
try {
return breaks(lg)
.filter((b) => b.box === boxId && (!name || !b.port || b.port === name))
.map((b) => b.text);
list = breaks(lg, o.values, { logics: o.logics });
} catch {
// 모델의 끊긴 곳 목록이 준비되기 전 — 같은 뜻의 작은 셈
// 만드는 중 셈이 막히면 끊긴 곳 없음으로 둠
}
const box = boxOf(lg, boxId);
if (!box) return [];
const out: string[] = [];
for (const p of portsIn(box)) {
if (name && p.name !== name) continue;
if (!lg.links.some((l) => l.to.box === boxId && l.to.name === p.name)) out.push(td("EmptyIn"));
else if (box.func?.inputs.some((i) => i.port === p.name && !i.cell)) out.push(td("NoCell"));
}
for (const p of portsOut(box)) {
if (name && p.name !== name) continue;
if (!lg.links.some((l) => l.from.box === boxId && l.from.name === p.name)) out.push(td("Dead"));
}
return out;
return list
.filter((b) => b.box === boxId && (!name || !b.port || b.port === name))
.map((b) => b.text);
}
export const noValue = (): string => td("NoValue");
@@ -220,22 +220,8 @@ export function traceOf(
return "dim";
}
/** 선 위 규칙 글 — Rule 의 ruleLabel · 준비 전이면 같은 꼴로 */
export function linkLabel(link: CfLink): string {
const r = link.rule;
if (!r) return "";
try {
return ruleLabel(r);
} catch {
if (r.kind === "lookup") return `표: ${r.col} 칸`;
if (r.kind === "gate") return `${r.from.name} ${r.op} ${r.value}일 때`;
if (r.kind === "scale")
return r.op === "×" || r.op === "÷"
? `${r.op} ${r.value}`
: `${r.op === "+%" ? "+" : "−"}${r.value}%`;
return `소수 ${r.places} ${r.method}`;
}
}
/** 선 위 규칙 글 — Rule 의 ruleLabel · 규칙 없으면 「」 */
export const linkLabel = (link: CfLink): string => (link.rule ? ruleLabel(link.rule) : "");
/** 굽은 선 가운데 점 — 양 끝 손잡이가 마주 보는 베지어라 t = 0.5 가 두 끝의 가운데 */
export const midOf = (
+2 -1
View File
@@ -347,7 +347,8 @@ function otherLogic(
}
const inner: CfValues = {};
for (const d of designInputs(other)) {
const v = d.port in arrive ? arrive[d.port] : given[d.port];
// 값 차례 = 이어진 선 > 넘긴 값 > 상자에 둔 예시값 > 그 로직 자기 예시값(inner 에 안 넣음)
const v = d.port in arrive ? arrive[d.port] : (given[d.port] ?? ref?.examples?.[d.port]);
if (v !== undefined && v !== null) (inner[d.box] ??= {})[d.port] = v;
}
const ev = evalLogic(other, inner, { logics: opts.logics, depth: (opts.depth ?? 0) + 1 });
@@ -227,7 +227,11 @@ export function checkMaterial(el: CfElementRef, others: CfElementRef[]): string
if (!el.name) return "자리 이름을 적어 주세요";
if (others.some((o) => o.name === el.name)) return "같은 이름의 알약이 이미 있음";
if (el.mode === "rule" && !el.rule?.from.name) return "규칙에 쓸 변수 알약을 골라 주세요";
if (el.mode !== "fixed" && !Object.keys(el.scope ?? {}).length)
if (
el.mode !== "fixed" &&
!(el.mode === "choice" && el.default) &&
!Object.keys(el.scope ?? {}).length
)
return "범위(구분 · 상세구분 · 찾는 말) 하나는 정해 주세요";
return null;
}
@@ -255,7 +259,7 @@ export function logicRefOf(key: string, other: CfLogic): CfLogicRef {
};
}
/** 다른 로직 입력 넣기 · 빼기(null) — 빼면 변수 상태로 돌아가고 그 알약에서 나간 선도 지움 */
/** 다른 로직 입력 넣기 · 빼기(null) — 빼면 변수 상태로 돌아가고 이 상자의 알약에 닿은 선은 모두 지움 */
export function setLogicInput(lg: CfLogic, id: string, ref: CfLogicRef | null): CfLogic {
return {
...lg,
@@ -266,7 +270,7 @@ export function setLogicInput(lg: CfLogic, id: string, ref: CfLogicRef | null):
: { mode: "variable", elements: [], variables: [] };
return { ...b, input, unit: unitOfInput(input) };
}),
links: lg.links.filter((l) => l.from.box !== id),
links: lg.links.filter((l) => l.from.box !== id && l.to.box !== id), // 나가던 선 · 그 로직의 받는 알약으로 들어오던 선 모두
};
}
@@ -291,7 +291,7 @@ function itemList(
export async function previewOf(
el1: CfElementRef,
lg: CfLogic,
): Promise<{ found: number; names: string[]; cheapest: MatItem | null }> {
): Promise<{ found: number; names: string[]; cheapest: MatItem | null; unit: string }> {
const scope = el1.scope ?? {};
const word = scope.찾는말 ?? "";
if (el1.mode === "rule" && el1.rule) {
@@ -300,7 +300,7 @@ export async function previewOf(
);
const value = pill?.example;
if (value === null || value === undefined || value === "")
return { found: 0, names: [], cheapest: null };
return { found: 0, names: [], cheapest: null, unit: "" };
const r = await searchMaterials({
q: `${word} ${value}`.trim(),
sub: scope.구분,
@@ -313,6 +313,7 @@ export async function previewOf(
found: hit.length,
names: hit.slice(0, 3).map((i) => `${i.name} ${i.spec}`.trim()),
cheapest: cheapestOf(hit),
unit: commonUnit(hit),
};
}
const r = await searchMaterials({
@@ -326,9 +327,14 @@ export async function previewOf(
found: r.total,
names: r.items.slice(0, 3).map((i) => `${i.name} ${i.spec}`.trim()),
cheapest: cheapestOf(r.items),
unit: commonUnit(r.items),
};
}
/** 잡힌 품목 단위가 모두 같으면 그 단위 · 아니면 빈 글 */
const commonUnit = (items: MatItem[]): string =>
items.length && items.every((i) => i.unit === items[0].unit) ? items[0].unit : "";
const cheapestOf = (items: MatItem[]): MatItem | null =>
items
.filter((i) => i.price !== null)
@@ -541,7 +547,11 @@ export function renderMaterial(
const why = checkMaterial(cand, els);
name.setError(why ?? undefined);
if (why) return void showToast(why, "error");
on.change(addEl(lg, cand));
// 잡히는 재료의 단위가 하나로 같으면 그 단위를 알약 단위로
void previewOf(cand, lg).then(
(p) => on.change(addEl(lg, p.unit ? { ...cand, unit: p.unit } : cand)),
() => on.change(addEl(lg, cand)),
);
},
});
const clear = createButton({
@@ -8,7 +8,6 @@
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { createButton, el } from "@ui/ui_template_elements";
import type { CfValues } from "./M02_DF_Eval";
import {
portsOut,
reaches,
@@ -22,12 +21,13 @@ import {
GATE_OPS,
SCALE_OPS,
evalSafe,
gateReport,
labelOf,
linkNotes,
portText,
preview,
unitOf,
valueOf,
type DetailOptions,
} from "./M02_DF_Detail";
import { suggestPlaces } from "./M02_DF_UnitPick";
import "./M02_DF_LinkPanel.css";
@@ -317,12 +317,12 @@ export function renderLinkPanel(
lg: CfLogic,
linkId: string,
on: { change(lg: CfLogic): void },
opts: { values?: CfValues; readOnly?: boolean } = {},
opts: DetailOptions & { readOnly?: boolean } = {},
): void {
const link = lg.links.find((l) => l.id === linkId);
host.classList.add("m02-dflink");
if (!link) return host.replaceChildren();
const ev = evalSafe(lg, opts.values);
const ev = evalSafe(lg, opts);
const table = valueOf(ev, link.from);
const headers =
Array.isArray(table) && Array.isArray(table[0])
@@ -358,7 +358,7 @@ export function renderLinkPanel(
if (rule?.kind === "scale") parts.push(...scaleEditor(ctx, rule));
if (rule?.kind === "round") parts.push(...roundEditor(ctx, rule, unitOf(lg, link.from)));
const pv = preview(link, rule, ev);
const pv = preview(link, ev);
parts.push(
row(
el("span", { className: "m02-dflink-label", text: tk("Before") }),
@@ -369,16 +369,11 @@ export function renderLinkPanel(
),
row(
el("span", { className: "m02-dflink-label", text: tk("Label") }),
el("span", { text: labelOf(lg, rule) }),
el("span", { text: labelOf(rule) }),
),
);
const warns: string[] = [];
if (rule?.kind === "gate") {
const rep = gateReport(lg, link, ev);
if (rep.pass.length > 1) warns.push(tk("WarnBoth", rep.pass.length));
if (rep.total > 0 && rep.pass.length === 0) warns.push(tk("WarnNone", rep.total));
}
const warns: string[] = linkNotes(link, ev);
if (rule?.kind === "lookup") {
if (!rule.conds.length) warns.push(tk("WarnLookup"));
if (!rule.col) warns.push(tk("WarnTake"));
+19 -2
View File
@@ -64,6 +64,8 @@ export interface CfLogicRef {
outputs: CfPill[];
/** 받는 알약 = 그 로직의 「설계 때 정할 것」(변수 · 재료 자리) */
designs: CfPill[];
/** 선 없는 받는 알약의 예시값(키 = 받는 알약 이름) — 값 차례 = 이어진 선 > 넘긴 값 > 이것 > 그 로직 자기 예시값 */
examples?: Record<string, number | string>;
}
export interface CfInput {
@@ -154,6 +156,8 @@ export interface CfDesign {
unit: string;
variable?: CfVariable;
element?: CfElementRef;
/** 보일 예시값 — 변수 = variable.example · 다른 로직 = logic.examples(없으면 빈 칸 → 그 로직 자기 예시값) */
example?: number | string | null;
}
/** 사용자 요소 — 그룹에 없는 새 요소(마스터에 안 씀 · `_data` 에 저장) */
@@ -567,7 +571,14 @@ export function designInputs(lg: CfLogic): CfDesign[] {
for (const b of lg.boxes) {
const vars = b.input?.variables ?? b.func?.variables ?? [];
for (const v of vars)
out.push({ box: b.id, port: v.name, kind: "변수", unit: v.unit || "-", variable: clone(v) });
out.push({
box: b.id,
port: v.name,
kind: "변수",
unit: v.unit || "-",
variable: clone(v),
example: v.example,
});
for (const e of b.input?.mode === "group" ? b.input.elements : [])
if (e.mode === "choice")
out.push({
@@ -580,7 +591,13 @@ export function designInputs(lg: CfLogic): CfDesign[] {
if (b.input?.mode === "logic")
for (const d of b.input.logic?.designs ?? [])
if (!lg.links.some((l) => l.to.box === b.id && l.to.name === d.name))
out.push({ box: b.id, port: d.name, kind: "다른 로직", unit: d.unit || "-" });
out.push({
box: b.id,
port: d.name,
kind: "다른 로직",
unit: d.unit || "-",
example: b.input.logic?.examples?.[d.name] ?? null,
});
}
return out;
}
+11 -13
View File
@@ -7,7 +7,6 @@
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { createButton, el } from "@ui/ui_template_elements";
import type { CfValues } from "./M02_DF_Eval";
import type { CfLogic, CfStep } from "./M02_DF_Model";
import {
boxOf,
@@ -20,6 +19,7 @@ import {
trace,
unitOf,
valueOf,
type DetailOptions,
} from "./M02_DF_Detail";
import "./M02_DF_LinkPanel.css";
@@ -50,6 +50,7 @@ const line = (label: string, ...value: (HTMLElement | string)[]): HTMLElement =>
function steps(
lg: CfLogic,
list: CfStep[],
arrow: string,
empty: string,
onBox?: (id: string) => void,
): HTMLElement {
@@ -58,7 +59,7 @@ function steps(
className: "m02-dfpill-list",
children: list.map((s) => {
const parts: (HTMLElement | string)[] = [
s.dir === "out" ? "◀" : "▶",
arrow,
onBox
? createButton({
label: portText(lg, { box: s.box, name: s.port }),
@@ -68,8 +69,7 @@ function steps(
})
: portText(lg, { box: s.box, name: s.port }),
];
if (s.rule)
parts.push(el("span", { className: "m02-dfpill-rule", text: labelOf(lg, s.rule) }));
if (s.rule) parts.push(el("span", { className: "m02-dfpill-rule", text: labelOf(s.rule) }));
return el("li", { className: "m02-dfpill-step", children: parts });
}),
});
@@ -82,20 +82,18 @@ export function renderPillPanel(
boxId: string,
port: string,
on: { pick?(boxId: string): void } = {},
opts: { values?: CfValues } = {},
opts: DetailOptions = {},
): void {
host.classList.add("m02-dfpill");
const box = boxOf(lg, boxId);
if (!box) return host.replaceChildren();
const ev = evalSafe(lg, opts.values);
const ev = evalSafe(lg, opts);
const out = isOut(box, port);
// 받는 알약의 값 = 그 자리로 들어오는 선의 보낸 알약 값(규칙 지나기 전)
// 받는 알약의 값 = 선 규칙을 지나 실제로 들어온 값(없으면 보낸 알약 값)
const incoming = lg.links.find((l) => l.to.box === boxId && l.to.name === port);
const value = out
? valueOf(ev, { box: boxId, name: port })
: incoming
? valueOf(ev, incoming.from)
: undefined;
: (ev?.arrived?.[boxId]?.[port] ?? (incoming ? valueOf(ev, incoming.from) : undefined));
const unit = out
? unitOf(lg, { box: boxId, name: port })
: incoming
@@ -103,7 +101,7 @@ export function renderPillPanel(
: box.unit;
const back = trace(lg, { box: boxId, name: port }, "in");
const forward = trace(lg, { box: boxId, name: port }, "out");
const breaks = breakLines(lg, boxId, port);
const breaks = breakLines(lg, boxId, port, opts);
host.replaceChildren(
el("div", {
className: "m02-dflink-head",
@@ -118,13 +116,13 @@ export function renderPillPanel(
el("div", {
children: [
el("div", { className: "m02-dflink-label", text: tp("From") }),
steps(lg, back, tp("Start"), on.pick),
steps(lg, back, "◀", tp("Start"), on.pick),
],
}),
el("div", {
children: [
el("div", { className: "m02-dflink-label", text: tp("To") }),
steps(lg, forward, tp("End"), on.pick),
steps(lg, forward, "▶", tp("End"), on.pick),
],
}),
el("div", {
@@ -230,10 +230,13 @@ def test_다른_로직_상자는_출력_단위를_따르고_빼면_변수로_돌
assert box["unit"] == "㎥/시간"
assert call("pillNames", box) == ["작업량"]
lg["links"] = [
{"id": "l1", "from": {"box": "b1", "name": "작업량"}, "to": {"box": "b2", "name": "x"}}
{"id": "l1", "from": {"box": "b1", "name": "작업량"}, "to": {"box": "b2", "name": "x"}},
{"id": "l2", "from": {"box": "b2", "name": "관경"}, "to": {"box": "b1", "name": "q"}},
{"id": "l3", "from": {"box": "b2", "name": "관경"}, "to": {"box": "b3", "name": "y"}},
]
back = call("setLogicInput", lg, "b1", None)
assert back["boxes"][0]["input"]["mode"] == "variable" and back["links"] == []
assert back["boxes"][0]["input"]["mode"] == "variable"
assert [x["id"] for x in back["links"]] == ["l3"]
assert call("reconcileUnit", lg, "b1") == lg # 다른 로직 상자 단위는 안 건드림
+17
View File
@@ -592,3 +592,20 @@ def test_끝수_함수_출력_출력_상자() -> None:
"out([a, E.evalLogic(lg, V).cost.노무비]);"
)
assert got == [537000, 540000]
def test_다_다른_로직_받는_알약_예시값_차례() -> None:
got = _run(
LOGIC
+ "lg = M.updateBox(lg, other.id, { input: { ...lg.boxes[1].input, logic: { ...lg.boxes[1].input.logic, examples: { 계수: 10, 면적: 99 } } } });\n"
"const a = E.evalLogic(lg, {}, { logics: LOGICS }).cost.경비;\n"
"const b = E.evalLogic(lg, { [other.id]: { 계수: 2 } }, { logics: LOGICS }).cost.경비;\n"
"const noEx = M.updateBox(lg, other.id, { input: { ...lg.boxes[1].input, logic: { ...lg.boxes[1].input.logic, examples: undefined } } });\n"
"out([a, b, E.evalLogic(noEx, {}, { logics: LOGICS }).cost.경비,"
" M.designInputs(lg).map(d => [d.kind, d.port, d.example]), M.designInputs(noEx).map(d => d.example)]);"
)
a, b, c, designs, plain = got
# 면적 = 이어진 선(길이 7) > 예시값 99 · 계수 = 상자 예시값 10 · 넘긴 값 2 가 이김 · 없으면 그 로직 예시값 3
assert (a, b, c) == (70000, 14000, 21000)
assert designs == [["변수", "길이", 7], ["다른 로직", "계수", 10]]
assert plain == [7, None]