fix(m02-df): 재료 자리 값 물음 · 상자 이름 칸 · 작은 칸 · 시작 단추

- 재료 선택 자리 · 조건 찾기를 서버 /values 에 slot(방식 · 기본 품목 · 범위 · 찾기 규칙) · value(이어진 알약 지금 값)로 물음 — 물음 서명에 들어가 값이 바뀌면 다시 받음(관경 600 → 400 = 116,600)
- 상세 내용 설정에 「상자 이름」(입력 · 함수 상자 · 빈 이름 · 같은 이름 안 받음 · 출력은 출력 판 이름 칸)
- 머리 로직 이름 · 결과 단위 칸 공용 작은 크기(compact)
- 시작 화면 「새로 만들기」 단추가 칸 폭으로 늘어나지 않게

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
2026-10-01 20:35:39 +09:00
co-authored by Claude Opus 5.5
parent 3abbe74f58
commit 047f3a2798
2 changed files with 51 additions and 11 deletions
+25 -5
View File
@@ -89,6 +89,8 @@ const TEXT = {
Diagram: ["다이어그램", "Diagram"],
Summary: ["원가", "Cost"],
Color: ["상자 색", "Box colour"],
BoxName: ["상자 이름", "Box name"],
NameTaken: ["「{v}」 — 같은 이름의 상자가 있음", "“{v}” — another box has this name"],
Standards: ["표준 표", "Standards"],
Logics: ["다른 로직을 못 읽음 — {v}", "Couldn't read other logics — {v}"],
NoName: ["로직 이름을 넣어 주세요", "Enter a logic name"],
@@ -307,6 +309,17 @@ function openDataFlow(): void {
className: "m02-df__color",
children: [el("span", { text: te("Color") }), colorBox],
});
// 상자 이름 — 입력 · 함수 상자(출력 상자는 출력 판의 이름 칸) · 빈 이름 · 같은 이름은 안 받음
const boxName = createInputField({ label: te("BoxName"), compact: true });
boxName.input.addEventListener("change", () => {
const box = lg.boxes.find((b) => b.id === pickedBox());
if (!box) return;
const next = boxName.input.value.trim();
const taken = lg.boxes.some((b) => b.id !== box.id && b.name === next);
if (taken) note.textContent = te("NameTaken", { v: next });
if (!next || taken || next === box.name) return void (boxName.input.value = box.name);
edit(M.model.updateBox(lg, box.id, { name: next }));
});
// 판마다 제 상태를 쥠 — 종류마다 따로 두고 숨기기만(갈아끼우면 편집기가 있다고 여김)
const funcHost = el("div");
const outHost = el("div");
@@ -315,7 +328,7 @@ function openDataFlow(): void {
const linkHost = el("div");
const pillHost = el("div");
const detail = detailFrame();
detail.settings.append(colorRow);
detail.settings.append(boxName.root, colorRow);
detail.body.append(inHost, funcHost, outHost, linkHost, pillHost);
const evalNow = (): CfEvalResult => {
@@ -341,10 +354,12 @@ function openDataFlow(): void {
/** 입력 값 다시 받기 — 물음이 바뀐 때만 · 같은 조작의 뒤끝이라 기록을 더 쌓지 않음 */
const refreshValues = (): void => {
const sig = JSON.stringify(queriesOf(lg).map((a) => a.q));
// 조건 찾기 재료는 이어진 알약 값(지금 셈)을 함께 물음 — 값이 바뀌면 다시 받음
const pill = (p: CfPort): unknown => result?.ports[p.box]?.[p.name] ?? null;
const sig = JSON.stringify(queriesOf(lg, pill, M.model.elementPort).map((a) => a.q));
if (sig === valueSig) return;
valueSig = sig;
void fetchValues(M.api.cfValues, lg).then(
void fetchValues(M.api.cfValues, lg, pill, M.model.elementPort).then(
(got) => {
if (sig !== valueSig) return;
values = got.values;
@@ -410,6 +425,7 @@ function openDataFlow(): void {
label: te("Name"),
placeholder: te("NamePlaceholder"),
value: lg.name,
compact: true,
onInput: (v) => edit({ ...lg, name: v }),
});
name.root.classList.add("m02-df__name");
@@ -418,6 +434,7 @@ function openDataFlow(): void {
label: te("Unit"),
options: unitList.map((u) => ({ value: u, text: u.replace(/^원\//, "") })),
value: lg.resultUnit,
compact: true,
onChange: (v) => edit({ ...lg, resultUnit: v }),
});
@@ -458,6 +475,8 @@ function openDataFlow(): void {
const box = lg.boxes.find((b) => b.id === pickedBox());
drawHead(detail, lg, pick);
colorRow.hidden = !box;
boxName.root.hidden = !box || box.kind === "output";
if (box && document.activeElement !== boxName.input) boxName.input.value = box.name;
linkHost.hidden = pick?.kind !== "link";
pillHost.hidden = pick?.kind !== "pill";
if (pick?.kind === "link")
@@ -638,6 +657,7 @@ function part(title: string, className: string, children: HTMLElement[]): HTMLEl
});
}
function bigButton(label: string, onClick: () => void): HTMLButtonElement {
return createButton({ label, variant: "filled", size: "sm", onClick });
/** 보통 크기 단추 — 칸 폭으로 늘어나지 않게 감쌈 */
function bigButton(label: string, onClick: () => void): HTMLElement {
return el("div", { children: [createButton({ label, variant: "filled", size: "sm", onClick })] });
}
@@ -10,7 +10,7 @@ import { el } from "@ui/ui_template_elements";
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";
import type { CfElementRef, CfLogic, CfPort } from "./M02_DF_Model";
const TEXT = {
Detail: ["상세 내용", "Details"],
@@ -169,14 +169,32 @@ export async function unitChoices(): Promise<string[]> {
return units.length ? units : ["원/㎡"];
}
/** 입력 값 물음 — 그룹 입력 상자의 요소(알약 = 요소 이름) · 표(알약 「표」) · 사용자 요소 · 변수는 Eval 이 스스로 봄 */
export function queriesOf(lg: CfLogic): { q: CfQuery; box: string; port: string }[] {
/** 입력 값 물음 — 그룹 입력 상자의 요소(알약 = 요소 이름) · 재료 자리(알약 = 자리 이름 · 조건 찾기는 이어진 알약 값을
* pill 로 읽어 넘김) · 표(알약 「표」) · 사용자 요소 · 변수는 Eval 이 스스로 봄 */
export function queriesOf(
lg: CfLogic,
pill: (p: CfPort) => unknown = () => null,
elementPort: (e: CfElementRef) => string = (e) => e.name,
): { 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 slots = inp.elements
.filter((e) => group === "재료" && e.mode && e.mode !== "fixed")
.map((e) => {
const port = elementPort(e);
const q: CfQuery = {
id: `${b.id}\u0001${port}`,
group,
unit: e.unit,
slot: { ...e, field: e.rule?.field, op: e.rule?.op },
};
if (e.mode === "rule" && e.rule) q.value = pill(e.rule.from);
return { q, box: b.id, port };
});
const els = inp.elements
.filter((e) => e.ref && !e.ref.startsWith("UE-"))
.filter((e) => e.ref && !e.ref.startsWith("UE-") && !slots.some((s) => s.port === e.name))
.map((e) => {
// 기계 요소는 「키#값 칸」 — 서버에는 키와 칸(field)을 따로 넘김
const [ref, field] = group === "기계" ? e.ref.split("#") : [e.ref, undefined];
@@ -187,7 +205,7 @@ export function queriesOf(lg: CfLogic): { q: CfQuery; box: string; port: string
const table = inp.table
? [{ q: { id: `${b.id}\u0001표`, group, table: inp.table }, box: b.id, port: "표" }]
: [];
return [...els, ...table];
return [...slots, ...els, ...table];
});
}
@@ -195,8 +213,10 @@ export function queriesOf(lg: CfLogic): { q: CfQuery; box: string; port: string
export async function fetchValues(
ask: (qs: CfQuery[], els: never) => Promise<CfAnswer[]>,
lg: CfLogic,
pill?: (p: CfPort) => unknown,
elementPort?: (e: CfElementRef) => string,
): Promise<{ values: CfValues; errors: string[] }> {
const asks = queriesOf(lg);
const asks = queriesOf(lg, pill, elementPort);
if (!asks.length) return { values: {}, errors: [] };
const answers = await ask(
asks.map((a) => a.q),