- 선 규칙(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
587 lines
23 KiB
TypeScript
587 lines
23 KiB
TypeScript
/* =============================================================================
|
||
* 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;
|
||
}
|