feat(M02-흐름도): 함수 판 식 보기 · 칸 연결 표 · 칸 색 · 조건 만들기 연결 + 선 조건 여러 줄(그리고/또는 · 값 목록 고르기 · 범위 검사)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2nUMUMTsqm93PbQtipo6c
This commit is contained in:
@@ -9,8 +9,12 @@
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import {
|
||||
gateConds,
|
||||
makeGate,
|
||||
portsOut,
|
||||
reaches,
|
||||
valueChoices,
|
||||
type CfGateCond,
|
||||
type CfGateOp,
|
||||
type CfLogic,
|
||||
type CfPort,
|
||||
@@ -77,6 +81,12 @@ const TEXT = {
|
||||
],
|
||||
WarnTake: ["꺼낼 칸을 고르지 않음", "No column to take"],
|
||||
Count: ["선", "Line"],
|
||||
And: ["그리고", "and"],
|
||||
Or: ["또는", "or"],
|
||||
If: ["조건", "If"],
|
||||
AddLine: ["+ 조건 줄", "+ Condition line"],
|
||||
Join: ["잇는 말", "Join with"],
|
||||
OutRange: ["범위 {n} 밖", "Outside the range {n}"],
|
||||
Range: ["{n} (범위)", "{n} (range)"],
|
||||
} as const;
|
||||
|
||||
@@ -92,6 +102,7 @@ interface Ctx {
|
||||
linkId: string;
|
||||
readOnly: boolean;
|
||||
headers: string[];
|
||||
detail: DetailOptions;
|
||||
change(lg: CfLogic): void;
|
||||
}
|
||||
|
||||
@@ -240,28 +251,122 @@ function lookupEditor(ctx: Ctx, rule: Extract<CfRule, { kind: "lookup" }>): HTML
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 보낸 알약이 입력 상자의 숫자 변수면 그 범위 */
|
||||
function rangeOf(ctx: Ctx, p: CfPort): [number, number] | null {
|
||||
const v = ctx.lg.boxes
|
||||
.find((b) => b.id === p.box)
|
||||
?.input?.variables.find((x) => x.name === p.name);
|
||||
return v?.type === "number" && v.range ? v.range : null;
|
||||
}
|
||||
|
||||
/** 값 칸 — 목록이 있으면 고르기 · 없으면 숫자 칸(범위 밖이면 한 줄 알림) */
|
||||
function gateValue(ctx: Ctx, c: CfGateCond, onPick: (v: number | string) => void): HTMLElement[] {
|
||||
const list = valueChoices(ctx.lg, c.from, { values: ctx.detail.values });
|
||||
if (list?.length) {
|
||||
const key = (v: string | number): string => `${typeof v}:${v}`;
|
||||
const options = list.map((v) => ({ value: key(v), text: String(v) }));
|
||||
return [
|
||||
select(
|
||||
ctx,
|
||||
options,
|
||||
key(c.value),
|
||||
(k) => {
|
||||
const hit = list.find((v) => key(v) === k);
|
||||
if (hit !== undefined) onPick(hit);
|
||||
},
|
||||
tk("Value"),
|
||||
),
|
||||
];
|
||||
}
|
||||
const range = rangeOf(ctx, c.from);
|
||||
const out: HTMLElement[] = [numberBox(ctx, c.value, onPick, tk("Value"))];
|
||||
const n = Number(c.value);
|
||||
if (range && Number.isFinite(n) && (n < range[0] || n > range[1])) {
|
||||
out.push(
|
||||
el("span", {
|
||||
className: "m02-dflink-warn",
|
||||
text: `⚠ ${tk("OutRange", `${range[0]} ~ ${range[1]}`)}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function gateEditor(ctx: Ctx, rule: Extract<CfRule, { kind: "gate" }>): HTMLElement[] {
|
||||
const put = (next: Partial<typeof rule>): void => setRule(ctx, { ...rule, ...next });
|
||||
return [
|
||||
el("p", { className: "m02-dflink-note", text: tk("GateHint") }),
|
||||
row(
|
||||
select(
|
||||
ctx,
|
||||
pillOptions(ctx),
|
||||
toKey(rule.from),
|
||||
(v) => put({ from: fromKey(v) }),
|
||||
tk("Match"),
|
||||
const g = gateConds(rule);
|
||||
const put = (conds: CfGateCond[], join = g.join): void => setRule(ctx, makeGate(conds, join));
|
||||
const edit = (i: number, patch: Partial<CfGateCond>): void =>
|
||||
put(g.conds.map((c, j) => (j === i ? { ...c, ...patch } : c)));
|
||||
const out: HTMLElement[] = [el("p", { className: "m02-dflink-note", text: tk("GateHint") })];
|
||||
g.conds.forEach((c, i) => {
|
||||
out.push(
|
||||
row(
|
||||
el("span", {
|
||||
className: "m02-dflink-label",
|
||||
text: i ? tk(g.join === "그리고" ? "And" : "Or") : tk("If"),
|
||||
}),
|
||||
select(
|
||||
ctx,
|
||||
pillOptions(ctx),
|
||||
toKey(c.from),
|
||||
(v) => {
|
||||
const from = fromKey(v);
|
||||
edit(i, {
|
||||
from,
|
||||
value: valueChoices(ctx.lg, from, { values: ctx.detail.values })?.[0] ?? 0,
|
||||
});
|
||||
},
|
||||
tk("Match"),
|
||||
),
|
||||
select(
|
||||
ctx,
|
||||
GATE_OPS.map((o) => ({ value: o, text: o })),
|
||||
c.op,
|
||||
(v) => edit(i, { op: v as CfGateOp }),
|
||||
tk("Kinds"),
|
||||
),
|
||||
...gateValue(ctx, c, (v) => edit(i, { value: v })),
|
||||
ctx.readOnly || g.conds.length < 2
|
||||
? ""
|
||||
: createButton({
|
||||
label: tk("Remove"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => put(g.conds.filter((_, j) => j !== i)),
|
||||
}),
|
||||
),
|
||||
select(
|
||||
ctx,
|
||||
GATE_OPS.map((o) => ({ value: o, text: o })),
|
||||
rule.op,
|
||||
(v) => put({ op: v as CfGateOp }),
|
||||
tk("Kinds"),
|
||||
);
|
||||
});
|
||||
if (!ctx.readOnly) {
|
||||
const first = g.conds[0];
|
||||
out.push(
|
||||
row(
|
||||
createButton({
|
||||
label: tk("AddLine"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () =>
|
||||
put([...g.conds, { from: first?.from ?? { box: "", name: "" }, op: "=", value: 0 }]),
|
||||
}),
|
||||
...(g.conds.length > 1
|
||||
? [
|
||||
el("span", { className: "m02-dflink-label", text: tk("Join") }),
|
||||
select(
|
||||
ctx,
|
||||
[
|
||||
{ value: "그리고", text: tk("And") },
|
||||
{ value: "또는", text: tk("Or") },
|
||||
],
|
||||
g.join,
|
||||
(v) => put(g.conds, v as "그리고" | "또는"),
|
||||
tk("Join"),
|
||||
),
|
||||
]
|
||||
: []),
|
||||
),
|
||||
numberBox(ctx, rule.value, (v) => put({ value: v }), tk("Value")),
|
||||
),
|
||||
];
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function scaleEditor(ctx: Ctx, rule: Extract<CfRule, { kind: "scale" }>): HTMLElement[] {
|
||||
@@ -323,7 +428,7 @@ function roundEditor(
|
||||
|
||||
const DEFAULT: Record<Exclude<Kind, "none">, (from: CfPort) => CfRule> = {
|
||||
lookup: () => ({ kind: "lookup", conds: [], col: "" }),
|
||||
gate: (from) => ({ kind: "gate", from, op: "≥", value: 0 }),
|
||||
gate: (from) => makeGate([{ from, op: "≥", value: 0 }]),
|
||||
scale: () => ({ kind: "scale", op: "×", value: 1 }),
|
||||
round: () => ({ kind: "round", places: 0, method: "반올림" }),
|
||||
};
|
||||
@@ -345,14 +450,23 @@ export function renderLinkPanel(
|
||||
Array.isArray(table) && Array.isArray(table[0])
|
||||
? table[0].map((h) => String(h ?? "")).filter(Boolean)
|
||||
: [];
|
||||
const ctx: Ctx = { lg, linkId, readOnly: !!opts.readOnly, headers, change: on.change };
|
||||
const ctx: Ctx = {
|
||||
lg,
|
||||
linkId,
|
||||
readOnly: !!opts.readOnly,
|
||||
headers,
|
||||
detail: opts,
|
||||
change: on.change,
|
||||
};
|
||||
const rule = link.rule;
|
||||
const kind: Kind = rule?.kind ?? "none";
|
||||
const parts: HTMLElement[] = [
|
||||
el("div", {
|
||||
className: "m02-dflink-head",
|
||||
children: [
|
||||
el("strong", { text: `${portText(lg, link.from)} → ${portText(lg, link.to)}` }),
|
||||
el("strong", {
|
||||
text: `${portText(lg, link.from)} → ${rule ? `(${labelOf(rule)}) → ` : ""}${portText(lg, link.to)}`,
|
||||
}),
|
||||
el("span", { className: "m02-dflink-note", text: `(${unitOf(lg, link.from)})` }),
|
||||
],
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user