Files
Aislo/M02_MasterTemplete/DataFlow/M02_DF_Model.ts
T
eomsangdonandClaude Opus 5.5 03736cba18 feat(m02-df): 흐름도 Model · Eval 넓힘 — 선 규칙 · 재료 방식 · 다른 로직 · 끝수 · 흐름 따라가기 · 끊긴 곳
- 선 규칙(lookup · gate · scale · round)을 엑셀 수식으로 바꿔 엔진 한 벌로 셈(M02_DF_Rule) · 선 위 규칙 글 · 막힌 갈래는 빈 값
- 재료 요소 고정 · 선택 자리 · 조건 찾기(알약 이름 = 자리) · 다른 로직 입력(받는 알약 = 설계 때 정할 것 · 그 로직으로 셈)
- 끝수 func.output.round · output.round · 기댐(선 · 규칙 조건 · 재료 조건)으로 차례 · 순환 막음 · setRule
- traceBack · traceForward · designInputs · breaks(빈 받는 자리 · 통과 안 됨 · 표에 없는 조건 · 맞는 재료 없음 · 조건 겹침 · 조건 안 맞음 · 오류)
- 시험 test_m02_df_model.py 22개(견본 셋 · 규칙 수식 · 흐름 · 끊긴 곳 · 설계 때 정할 것)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk
2026-10-01 19:47:53 +09:00

587 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* M02_DF_Model.ts
* 단가산출 흐름도 Model — 입력 · 함수 · 출력 상자를 선으로 이음 · 함수 상자 = 시트 하나(출력 하나)
* 선 규칙(표에서 찾기 · 조건 통과 · 바꾸기 · 끝수) · 재료 방식 셋 · 다른 로직 입력 · 흐름 따라가기 · 설계 때 정할 것
* DOM 없음 · 모양은 `docs/raw/M01_쉬운단가/흐름도_정함.md` 4~8 · 12 · 1판 약속 `원가다이어그램_정함.md`
* ========================================================================== */
import { currentLanguageIndex } from "@ui/ui_template_locale";
import type { Sheet } from "../../A00_Common/spreadsheet/spreadsheet_types";
export type CfKind = "input" | "function" | "output";
export type CfGroup = "인력" | "재료" | "유가전력" | "기계" | "소요량" | "계수" | "환율" | "요율";
export type CfCost = "노무비" | "재료비" | "경비" | "값";
/** 재료 요소 방식 — 고정(품목 하나) · 선택 자리(프로젝트에서 고름) · 조건 찾기(규격 / 이름에 알약 값) */
export type CfMaterialMode = "fixed" | "choice" | "rule";
/** 재료 찾을 범위 */
export interface CfScope {
구분?: string;
상세구분?: string;
찾는말?: string;
}
/** 재료 조건 찾기 — 품목의 규격 / 이름에 [알약 값] 이 포함 · 같음 */
export interface CfMaterialRule {
field: "규격" | "이름";
op: "포함" | "같음";
from: CfPort;
}
/** 입력 상자가 고른 요소 — ref = 마스터 키 또는 사용자 요소 id
* 재료면 mode · scope · rule · default · slot(알약 이름 = 자리 이름 · 없으면 name) */
export interface CfElementRef {
ref: string;
name: string;
spec: string;
unit: string;
mode?: CfMaterialMode;
scope?: CfScope;
rule?: CfMaterialRule;
/** 기본 품목 키(선택 자리 · 조건 찾기) */
default?: string;
slot?: string;
}
/** 변수(입력 정의) — 그룹 밖 정보(면적 · 길이 …) · 함수 상자의 받는 값 */
export interface CfVariable {
name: string;
type: "number" | "choice" | "text";
unit: string;
choices?: (string | number)[];
range?: [number, number];
example: number | string | null;
/** 함수 상자 안 변수의 칸(「B2」) — 입력 상자 변수는 없음 */
cell?: string;
}
/** 다른 로직 입력 — 저장 로직(DF-…) · 견본 id · outputs · designs 는 고를 때 그 로직에서 읽어 둔 것 */
export interface CfLogicRef {
key: string;
name?: string;
/** 내보내는 알약 = 그 로직 출력 상자 이름(비용 · 값) */
outputs: CfPill[];
/** 받는 알약 = 그 로직의 「설계 때 정할 것」(변수 · 재료 자리) */
designs: CfPill[];
}
export interface CfInput {
mode: "group" | "variable" | "logic";
group?: CfGroup;
elements: CfElementRef[];
/** 소요량 · 계수 = 표(row 가 있으면 그 한 줄) */
table?: { key: string; row?: number };
variables: CfVariable[];
/** mode "logic" */
logic?: CfLogicRef;
}
/** 끝수 — 소수 자리 · 방법 */
export interface CfRound {
places: number;
method: "반올림" | "올림" | "버림";
}
export interface CfFunc {
/** 우리 스프레드시트 시트 하나 */
sheet: Sheet;
/** 받는 알약 → 칸 */
inputs: { port: string; cell: string }[];
variables: CfVariable[];
/** 내보내는 알약 하나 */
output: { cell: string; name: string; unit: string; round?: CfRound };
}
export interface CfOutput {
cost: CfCost;
unit: string;
round?: CfRound;
}
export interface CfBox {
id: string;
kind: CfKind;
name: string;
color: string;
/** 오른쪽 위 단위 — 기본 「-」 */
unit: string;
x: number;
y: number;
input?: CfInput;
func?: CfFunc;
output?: CfOutput;
}
/** 알약 한쪽 끝 */
export interface CfPort {
box: string;
name: string;
}
export type CfGateOp = "=" | "≠" | "≥" | "≤" | ">" | "<";
/** 선 규칙(선 하나에 하나 · 없으면 그대로) — 받는 칸의 엑셀 수식으로 바뀌어 엔진 한 벌로 셈
* lookup = 보낸 표에서 조건 칸마다 맞출 알약 · 꺼낼 칸(범위 칸은 「이름·아래」 「이름·위」)
* gate = 알약 값이 조건을 통과할 때만 보냄 · scale = × ÷ +% -% · round = 끝수 */
export type CfRule =
| { kind: "lookup"; conds: { col: string; from: CfPort }[]; col: string }
| { kind: "gate"; from: CfPort; op: CfGateOp; value: number | string }
| { kind: "scale"; op: "×" | "÷" | "+%" | "-%"; value: number }
| ({ kind: "round" } & CfRound);
export interface CfLink {
id: string;
from: CfPort;
to: CfPort;
rule?: CfRule;
}
/** 흐름 한 걸음 — 그 알약 · 그 알약으로 들어온(나간) 선의 규칙 */
export interface CfStep {
box: string;
port: string;
dir: "in" | "out";
link?: string;
rule?: CfRule;
}
/** 설계 때 정할 것 — 변수 · 재료 자리 · 다른 로직의 받는 알약(선이 없을 때) */
export interface CfDesign {
box: string;
port: string;
kind: "변수" | "재료 자리" | "다른 로직";
unit: string;
variable?: CfVariable;
element?: CfElementRef;
}
/** 사용자 요소 — 그룹에 없는 새 요소(마스터에 안 씀 · `_data` 에 저장) */
export interface CfUserElement {
id: string;
group: CfGroup;
name: string;
spec: string;
unit: string;
value: number | null;
}
export interface CfLogic {
key: string | null;
name: string;
resultUnit: string;
boxes: CfBox[];
links: CfLink[];
userElements: CfUserElement[];
}
/** 알약 하나의 모습 */
export interface CfPill {
name: string;
unit: string;
}
const TEXT = {
Input: ["입력", "Input"],
Func: ["함수", "Function"],
Output: ["출력", "Output"],
Result: ["결과", "Result"],
Value: ["값", "Value"],
NoPill: ["알약이 없음", "No such pill"],
FromOutput: ["출력 상자에서는 선이 나가지 않음", "Lines can't leave an output box"],
ToInput: ["입력 상자로는 선이 들어가지 않음", "Lines can't enter an input box"],
Self: ["같은 상자끼리는 못 이음", "A box can't link to itself"],
Taken: ["이미 선이 들어온 자리", "This spot already has a line"],
Loop: ["이으면 돌고 도는 선이 됨", "This line would make a loop"],
CondPill: ["규칙의 조건 알약이 없음", "The rule's condition pill doesn't exist"],
} as const;
const tl = (key: keyof typeof TEXT): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0];
/** 종류별 기본 색 */
export const BOX_COLOR: Record<CfKind, string> = {
input: "#2f80ed",
function: "#7b4fd6",
output: "#27ae60",
};
/** 알약 이름 규칙(브레인 정함) — 화면 말이 아니라 열쇠라 바꾸지 않음 */
export const OUTPUT_PORT = "값";
export const TABLE_PORT = "표";
const clone = <T>(v: T): T => structuredClone(v);
/** 다음 번호 — 있는 id(「b3」)보다 하나 큰 것 */
function nextId(prefix: string, ids: string[]): string {
const nums = ids
.filter((x) => x.startsWith(prefix))
.map((x) => Number(x.slice(prefix.length)))
.filter((n) => Number.isInteger(n));
return `${prefix}${Math.max(0, ...nums) + 1}`;
}
/** 같은 이름이면 「 2」 「 3」 */
function uniqueName(used: string[], base: string): string {
if (!used.includes(base)) return base;
for (let n = 2; ; n++) if (!used.includes(`${base} ${n}`)) return `${base} ${n}`;
}
const boxOf = (lg: CfLogic, id: string): CfBox | undefined => lg.boxes.find((b) => b.id === id);
/** 상자 하나 고치기 — 나머지는 그대로(받은 것은 안 바꿈) */
export function updateBox(
lg: CfLogic,
id: string,
patch: Partial<Omit<CfBox, "id" | "kind">>,
): CfLogic {
return {
...lg,
boxes: lg.boxes.map((b) => (b.id === id ? { ...clone(b), ...clone(patch) } : b)),
};
}
/* ── 만들기 · 상자 ───────────────────────────────────────────────────────── */
export function newLogic(name = "", resultUnit = "원/㎡"): CfLogic {
return { key: null, name, resultUnit, boxes: [], links: [], userElements: [] };
}
/** 상자 더하기 — 종류별 기본 색 · 단위 「-」 · 함수는 빈 시트 하나(A1 결과 · B1 출력 칸) · 출력은 노무비 */
export function addBox(
lg: CfLogic,
kind: CfKind,
x: number,
y: number,
): { lg: CfLogic; box: CfBox } {
const id = nextId(
"b",
lg.boxes.map((b) => b.id),
);
const base = tl(kind === "input" ? "Input" : kind === "function" ? "Func" : "Output");
const name = uniqueName(
lg.boxes.map((b) => b.name),
base,
);
const box: CfBox = { id, kind, name, color: BOX_COLOR[kind], unit: "-", x, y };
if (kind === "input") box.input = { mode: "variable", elements: [], variables: [] };
if (kind === "function") {
// 출력 이름은 로직 안에서 겹치지 않게 「결과 1」 「결과 2」 … — 다른 함수 상자의 받는 알약과 섞이지 않음
const taken = lg.boxes.flatMap((b) => (b.func ? [b.func.output.name] : []));
let n = 1;
while (taken.includes(`${tl("Result")} ${n}`)) n++;
const out = `${tl("Result")} ${n}`;
box.func = {
sheet: { id: `s-${id}`, 이름: name, 칸: { A1: { 값: out } } },
inputs: [],
variables: [],
output: { cell: "B1", name: out, unit: "-" },
};
}
if (kind === "output") box.output = { cost: "노무비", unit: "-" };
return { lg: { ...lg, boxes: [...lg.boxes, box] }, box };
}
/** 상자 지우기 — 이어진 선도 지움 */
export function removeBox(lg: CfLogic, id: string): CfLogic {
// 선을 하나씩 끊어 받는 쪽 함수 상자의 칸 없는 알약도 정리
let next = lg;
for (const l of lg.links) if (l.from.box === id || l.to.box === id) next = unlink(next, l.id);
return { ...next, boxes: next.boxes.filter((b) => b.id !== id) };
}
export function moveBox(lg: CfLogic, id: string, x: number, y: number): CfLogic {
return updateBox(lg, id, { x, y });
}
export function setColor(lg: CfLogic, id: string, color: string): CfLogic {
return updateBox(lg, id, { color });
}
/** 단위 — 상자 오른쪽 위 = 그 상자 출력의 단위 · 함수 · 출력 상자는 안쪽 단위도 같이 */
export function setUnit(lg: CfLogic, id: string, unit: string): CfLogic {
const box = boxOf(lg, id);
if (!box) return lg;
const u = unit.trim() || "-";
const patch: Partial<CfBox> = { unit: u };
if (box.func) patch.func = { ...box.func, output: { ...box.func.output, unit: u } };
if (box.output) patch.output = { ...box.output, unit: u };
return updateBox(lg, id, patch);
}
/* ── 알약 ───────────────────────────────────────────────────────────────── */
/** 요소 알약 이름 — 재료 선택 자리 · 조건 찾기는 자리 이름(slot) · 나머지는 element.name */
export const elementPort = (e: CfElementRef): string =>
e.mode && e.mode !== "fixed" && e.slot?.trim() ? e.slot.trim() : e.name;
/** 내보내는 알약(이름 규칙 · 브레인 정함) — 입력 = 요소(elementPort) · 표면 「표」 · 변수면 variable.name
* · 다른 로직이면 그 로직 출력 · 함수 = func.output.name 하나 · 출력 = 없음 */
export function portsOut(box: CfBox): CfPill[] {
if (box.kind === "function" && box.func)
return [{ name: box.func.output.name, unit: box.func.output.unit || box.unit }];
if (box.kind !== "input" || !box.input) return [];
const inp = box.input;
if (inp.mode === "logic") return (inp.logic?.outputs ?? []).map((p) => ({ ...p }));
return [
...(inp.mode === "group"
? inp.elements.map((e) => ({ name: elementPort(e), unit: e.unit || "-" }))
: []),
...(inp.mode === "group" && inp.table ? [{ name: TABLE_PORT, unit: "-" }] : []),
...inp.variables.map((v) => ({ name: v.name, unit: v.unit || "-" })),
];
}
/** 받는 알약 — 함수 = func.inputs[].port · 출력 = 「값」 하나 · 다른 로직 입력 = 그 로직의 설계 때 정할 것 · 그 밖 입력 = 없음 */
export function portsIn(box: CfBox): CfPill[] {
if (box.kind === "function" && box.func)
return box.func.inputs.map((i) => ({ name: i.port, unit: "-" }));
if (box.kind === "output") return [{ name: OUTPUT_PORT, unit: box.output?.unit ?? box.unit }];
if (box.input?.mode === "logic") return (box.input.logic?.designs ?? []).map((p) => ({ ...p }));
return [];
}
/** 알약 쪽 — 함수 출력 이름이면 out · 출력 상자는 in · 다른 로직 입력은 출력 이름이면 out */
export function portDir(box: CfBox, name: string): "in" | "out" {
if (box.kind === "output") return "in";
if (portsOut(box).some((p) => p.name === name)) return "out";
return "in";
}
/* ── 선 ─────────────────────────────────────────────────────────────────── */
/** 선 규칙이 기대는 알약(조건 찾기 · 조건 통과) */
export function ruleSources(rule: CfRule | undefined): CfPort[] {
if (!rule) return [];
if (rule.kind === "lookup") return rule.conds.map((c) => c.from);
if (rule.kind === "gate") return [rule.from];
return [];
}
/** 상자 사이 기댐 — 선 + 선 규칙의 조건 알약 + 재료 조건 찾기의 알약(→ 그 상자) */
export function depends(lg: CfLogic): { from: string; to: string }[] {
const out: { from: string; to: string }[] = [];
for (const l of lg.links) {
out.push({ from: l.from.box, to: l.to.box });
for (const p of ruleSources(l.rule)) out.push({ from: p.box, to: l.to.box });
}
for (const b of lg.boxes)
for (const e of b.input?.elements ?? [])
if (e.mode === "rule" && e.rule) out.push({ from: e.rule.from.box, to: b.id });
return out;
}
/** from 상자에서 기댐을 따라 to 상자에 닿나 */
export function reaches(lg: CfLogic, from: string, to: string): boolean {
const deps = depends(lg);
const seen = new Set<string>();
const walk = (id: string): boolean => {
if (id === to) return true;
if (seen.has(id)) return false;
seen.add(id);
return deps.filter((d) => d.from === id).some((d) => walk(d.to));
};
return walk(from);
}
/** 선 잇기 — 입력 → 함수 · 출력 · 다른 로직 입력 / 함수 → 함수 · 출력 · 다른 로직 입력 · 출력에서 나감 없음 ·
* 순환 · 같은 받는 자리 두 번 막음 · 규칙(rule)은 함께 붙임(조건 알약이 없으면 막음)
* 받는 쪽이 함수 상자이고 to.name 의 받는 알약이 없으면 새로 만듦(to.name 이 빈 글이면 보낸 알약 이름 ·
* 같은 이름이 있으면 「이름 (2)」) · func.inputs 에 칸 없이(`cell: ""`) 더함 — 칸은 함수 판에서 놓음 */
export function link(
lg: CfLogic,
from: CfPort,
to: CfPort,
rule?: CfRule,
): { lg: CfLogic; why: string | null } {
const a = boxOf(lg, from.box);
const b = boxOf(lg, to.box);
if (!a || !b) return { lg, why: tl("NoPill") };
if (a.kind === "output") return { lg, why: tl("FromOutput") };
if (b.kind === "input" && b.input?.mode !== "logic") return { lg, why: tl("ToInput") };
if (a.id === b.id) return { lg, why: tl("Self") };
if (!portsOut(a).some((p) => p.name === from.name)) return { lg, why: tl("NoPill") };
const has = portsIn(b).some((p) => p.name === to.name);
if (!has && b.kind !== "function") return { lg, why: tl("NoPill") };
const why = ruleWhy(lg, b.id, rule);
if (why) return { lg, why };
if (reaches(lg, b.id, a.id)) return { lg, why: tl("Loop") };
if (has && lg.links.some((l) => l.to.box === to.box && l.to.name === to.name))
return { lg, why: tl("Taken") };
let next = lg;
let port = to.name;
if (!has) {
const used = portsIn(b).map((p) => p.name);
const base = to.name.trim() || from.name;
port = used.includes(base) ? uniqueSuffix(used, base) : base;
const func = b.func!;
next = updateBox(lg, b.id, { func: { ...func, inputs: [...func.inputs, { port, cell: "" }] } });
}
const id = nextId(
"l",
next.links.map((l) => l.id),
);
const made: CfLink = { id, from: { ...from }, to: { box: to.box, name: port } };
if (rule) made.rule = clone(rule);
return { lg: { ...next, links: [...next.links, made] }, why: null };
}
/** 규칙의 조건 알약 검사 — 없거나 돌고 돌면 까닭 */
function ruleWhy(lg: CfLogic, target: string, rule: CfRule | undefined): string | null {
for (const p of ruleSources(rule)) {
const box = boxOf(lg, p.box);
if (!box || !portsOut(box).some((x) => x.name === p.name)) return tl("CondPill");
if (box.id === target || reaches(lg, target, box.id)) return tl("Loop");
}
return null;
}
/** 선 규칙 바꾸기 · 빼기(null) — 조건 알약이 없거나 돌고 돌면 막고 까닭 */
export function setRule(
lg: CfLogic,
id: string,
rule: CfRule | null,
): { lg: CfLogic; why: string | null } {
const l = lg.links.find((x) => x.id === id);
if (!l) return { lg, why: tl("NoPill") };
const why = rule ? ruleWhy(lg, l.to.box, rule) : null;
if (why) return { lg, why };
const links = lg.links.map((x) => {
if (x.id !== id) return x;
const { rule: _old, ...rest } = x;
return rule ? { ...rest, rule: clone(rule) } : rest;
});
return { lg: { ...lg, links }, why: null };
}
/** 같은 이름이면 「이름 (2)」 「이름 (3)」 */
function uniqueSuffix(used: string[], base: string): string {
for (let n = 2; ; n++) if (!used.includes(`${base} (${n})`)) return `${base} (${n})`;
}
/** 선 끊기 — 받는 쪽이 함수 상자이고 그 받는 알약이 칸에 안 놓였으면(cell 빈 글) 알약도 같이 지움 */
export function unlink(lg: CfLogic, id: string): CfLogic {
const gone = lg.links.find((l) => l.id === id);
const next = { ...lg, links: lg.links.filter((l) => l.id !== id) };
const box = gone && boxOf(next, gone.to.box);
if (!gone || !box?.func) return next;
const port = box.func.inputs.find((i) => i.port === gone.to.name);
const still = next.links.some((l) => l.to.box === box.id && l.to.name === gone.to.name);
if (!port || port.cell.trim() || still) return next;
const inputs = box.func.inputs.filter((i) => i !== port);
return updateBox(next, box.id, { func: { ...box.func, inputs } });
}
/* ── 흐름 따라가기 ──────────────────────────────────────────────────────── */
/** 그 알약 값이 온 길 — 출처 사슬(가까운 것부터 · 한 번씩) */
export function traceBack(lg: CfLogic, port: CfPort): CfStep[] {
const steps: CfStep[] = [];
const seen = new Set<string>();
const visit = (p: CfPort, dir: "in" | "out", via?: CfLink): void => {
const box = boxOf(lg, p.box);
const key = `${p.box}|${p.name}|${dir}`;
if (!box || seen.has(key)) return;
seen.add(key);
steps.push({
box: p.box,
port: p.name,
dir,
...(via ? { link: via.id } : {}),
...(via?.rule ? { rule: clone(via.rule) } : {}),
});
if (dir === "in") {
for (const l of lg.links.filter((x) => x.to.box === p.box && x.to.name === p.name)) {
visit(l.from, "out");
for (const c of ruleSources(l.rule)) visit(c, "out");
}
return;
}
// 내보내는 알약 — 함수 · 다른 로직은 받는 알약들에서 옴 · 재료 조건 찾기는 그 알약에서 옴
for (const q of box.kind === "function" || box.input?.mode === "logic" ? portsIn(box) : [])
visit(
{ box: box.id, name: q.name },
"in",
lg.links.find((l) => l.to.box === box.id && l.to.name === q.name),
);
for (const e of box.input?.elements ?? [])
if (e.mode === "rule" && e.rule && elementPort(e) === p.name) visit(e.rule.from, "out");
};
const box = boxOf(lg, port.box);
if (!box) return [];
const dir = portDir(box, port.name);
visit(
port,
dir,
dir === "in"
? lg.links.find((l) => l.to.box === port.box && l.to.name === port.name)
: undefined,
);
return steps;
}
/** 그 알약 값이 가는 길 — 선 · 선 규칙의 조건으로 쓰이는 곳 · 재료 조건 찾기 · 함수 · 다른 로직을 지나 출력까지 */
export function traceForward(lg: CfLogic, port: CfPort): CfStep[] {
const steps: CfStep[] = [];
const seen = new Set<string>();
const visit = (p: CfPort, dir: "in" | "out", via?: CfLink): void => {
const box = boxOf(lg, p.box);
const key = `${p.box}|${p.name}|${dir}`;
if (!box || seen.has(key)) return;
seen.add(key);
steps.push({
box: p.box,
port: p.name,
dir,
...(via ? { link: via.id } : {}),
...(via?.rule ? { rule: clone(via.rule) } : {}),
});
if (dir === "in") {
// 받는 알약 → 그 상자의 내보내는 알약
for (const q of box.kind === "function" || box.input?.mode === "logic" ? portsOut(box) : [])
visit({ box: box.id, name: q.name }, "out");
return;
}
for (const l of lg.links.filter((x) => x.from.box === p.box && x.from.name === p.name))
visit(l.to, "in", l);
// 이 알약을 조건으로 쓰는 선 · 재료 조건 찾기
for (const l of lg.links.filter((x) =>
ruleSources(x.rule).some((c) => c.box === p.box && c.name === p.name),
))
visit(l.to, "in", l);
for (const b of lg.boxes)
for (const e of b.input?.elements ?? [])
if (e.mode === "rule" && e.rule?.from.box === p.box && e.rule.from.name === p.name)
visit({ box: b.id, name: elementPort(e) }, "out");
};
const box = boxOf(lg, port.box);
if (!box) return [];
visit(port, portDir(box, port.name));
return steps;
}
/* ── 설계 때 정할 것 ─────────────────────────────────────────────────────── */
/** 설계 때 정할 것 — 변수(입력 · 함수 상자) · 재료 선택 자리 · 다른 로직의 받는 알약(선이 없을 때) */
export function designInputs(lg: CfLogic): CfDesign[] {
const out: 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) });
for (const e of b.input?.mode === "group" ? b.input.elements : [])
if (e.mode === "choice")
out.push({
box: b.id,
port: elementPort(e),
kind: "재료 자리",
unit: e.unit || "-",
element: clone(e),
});
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 || "-" });
}
return out;
}