/* ============================================================================= * M01_MasterData_UI_LogicLab_EquationDefs.ts * 수식 탭 변수 정의 영역 속 고침(PLAN 800-3 · 요구 660-5) — 796-2a 변수 창을 정의 영역 안으로 옮겨 다시 씀. * ① 이름 · 기호 · 첨자 고침 · 지움 = 정의 줄 아래 칸(`renameEditor`) — 쓰는 변수 지움은 서버가 막고 쓰는 곳을 알림 * ② 새 기호 연결 지정 = 식에 처음 쓴 기호마다 줄(`draftRow`) — 국문 이름 + 연결(입력 · 표 찾기 · 요소 · 로직 · 상수) → 고침 `연결` 한 건 * 표 · 요소 · 로직 후보 = 서버 `…/equation/links`(읽기만) · 연결 글(엔진 식)은 서버가 지음 — 화면은 고른 것만 보냄 * 기호 글 쪼개기(`symbolParts`) = 서버 키보드 식 변수 글(`master_equation_edit._TOKEN` 기호_아래^위)과 같은 꼴 — 시험 `test_800_2_equation_layout` * 값 · 단위는 고치지 않음(요구 660-6) · 계산 · 검사는 서버 한 벌 * ========================================================================== */ import type { EqDef } from "@ui/ui_template_equation"; import { createBanner } from "@ui/ui_template_banner"; import { createButton } from "@ui/ui_template_button"; import { el } from "@ui/ui_template_elements_base"; import { createInputField, createSelectField } from "@ui/ui_template_input"; import { varEdits, type EqEdit } from "./logic/M01_Logic_Equation_Model"; export interface Candidate { 키: string; 이름: string; 규격?: string; 조건?: string[]; 칸?: string[]; 입력?: string[]; } export interface DefsHost { commit: (edits: EqEdit[]) => Promise; links: (ask: Record) => Promise<{ 후보: Candidate[]; 더있음: boolean }>; onDropped: (key: string) => void; } /** 새 기호 글 → 열쇠 · 기호 · 아래 · 위(`Q_d^2` → 열쇠 `Q_d2` · 기호 Q · 아래 d · 위 2) */ export function symbolParts(token: string): Pick { const [main = "", up = ""] = token.split("^"); const [sign = "", ...low] = main.split("_"); return { 열쇠: token.replace(/[^\p{L}\p{N}_]/gu, ""), 기호: sign, 아래: low.join("_"), 위: up }; } const grid = (...kids: HTMLElement[]): HTMLElement => el("div", { className: "m01-eq__form", children: kids }); const field = (label: string, value = ""): ReturnType => createInputField({ label, value, size: 24 }); const reasonBox = (): { root: HTMLElement; say: (list: string[]) => void } => { const root = el("div", { className: "m01-eq__why" }); return { root, say: (list) => root.replaceChildren( ...(list.length ? [createBanner({ tone: "danger", title: "안 됨", text: list.join(" · ") })] : []), ), }; }; /** 정의 줄 고침 칸 — 이름 · 기호 · 위 · 아래 · [적용] · [변수 지움] */ export function renameEditor(d: EqDef, host: DefsHost, close: () => void): HTMLElement { const name = field("이름", d.이름); const sign = field("기호", d.기호); const up = field("위첨자", d.위); const down = field("아래첨자", d.아래); const why = reasonBox(); const form = (v: Pick) => ({ 열쇠: d.열쇠, ...v, 단위: "", }); const apply = createButton({ label: "적용", size: 24, onClick: async () => { const edits = varEdits( form(d), form({ 이름: name.input.value.trim(), 기호: sign.input.value.trim(), 위: up.input.value.trim(), 아래: down.input.value.trim(), }), ); const bad = edits.length ? await host.commit(edits) : []; why.say(bad); if (!bad.length) close(); }, }); const drop = createButton({ label: "변수 지움", variant: "danger-ghost", size: 24, onClick: async () => { const bad = await host.commit([{ 할일: "지움", 열쇠: d.열쇠 }]); why.say(bad); if (!bad.length) host.onDropped(d.열쇠); }, }); const shut = createButton({ label: "닫기", variant: "ghost", size: 24, onClick: close }); const box = el("div", { className: "m01-eq__def-edit", children: [ grid(name.root, sign.root, up.root, down.root), el("div", { className: "m01-eq__def-actions", children: [drop, shut, apply] }), why.root, ], }); box.dataset.eqDefEditor = d.기호; return box; } export interface Draft { root: HTMLElement; /** 연결 지정 고침 — 아직 덜 골랐으면 까닭 글 */ read: () => EqEdit | string; say: (why: string) => void; } const KINDS = [ { value: "입력", text: "입력 (옵션 칸에 생김)" }, { value: "표", text: "표 찾기 (표 · 조건 · 칸)" }, { value: "요소", text: "요소 (인력 · 재료 · 기계)" }, { value: "로직", text: "로직 부르기" }, { value: "상수", text: "상수 (고정 수)" }, ]; const CATS = [ { value: "인력", text: "인력" }, { value: "재료", text: "재료" }, { value: "기계", text: "기계" }, ]; /** 후보 찾기 — 검색 글을 칠수록 서버 후보 · 고르기 */ function pick( host: DefsHost, ask: () => Record, onPick: (c: Candidate | null) => void, label: string, ): { root: HTMLElement; get: () => Candidate | null; refresh: () => void } { const box = field(label); const list = createSelectField({ options: [{ value: "", text: "— 검색해서 고름 —" }], size: 24, ariaLabel: label, }); let found: Candidate[] = []; let timer = 0; const get = (): Candidate | null => found[Number(list.select.value)] ?? null; const refresh = (): void => { window.clearTimeout(timer); timer = window.setTimeout(async () => { const got = await host.links({ ...ask(), 검색: box.input.value.trim() }).catch(() => null); found = got?.후보 ?? []; const more = got?.더있음 ? [{ value: "", text: "… 더 있음 — 검색을 더 칠 것" }] : []; list.setOptions( [ { value: "", text: found.length ? "— 고름 —" : "— 없음 —" }, ...found.map((c, i) => ({ value: String(i), text: `${c.이름}${c.규격 ? ` · ${c.규격}` : ""}`, })), ...more, ], "", ); onPick(null); }, 250); }; box.input.addEventListener("input", refresh); list.select.addEventListener("change", () => onPick(list.select.value === "" ? null : get())); return { root: grid(box.root, list.root), get: () => (list.select.value === "" ? null : get()), refresh, }; } type Reader = () => EqEdit | string; const inputKind = (): { nodes: HTMLElement[]; read: Reader } => { const base = field("기본값 (빈 칸 = 옵션에서 고름)"); const text = (): string => base.input.value.trim(); return { nodes: [base.root], read: () => ({ 할일: "연결", 갈래: "입력", ...(text() ? { 기본값: text() } : {}) }), }; }; const constKind = (): { nodes: HTMLElement[]; read: Reader } => { const val = field("값"); return { nodes: [val.root], read: () => val.input.value.trim() ? { 할일: "연결", 갈래: "상수", 값: val.input.value.trim() } : "상수 값을 적을 것", }; }; const elementKind = (host: DefsHost): { nodes: HTMLElement[]; read: Reader } => { const cat = createSelectField({ label: "분류", options: CATS, value: "인력", size: 24 }); const find = pick( host, () => ({ 갈래: "요소", 분류: cat.select.value }), () => undefined, "요소 이름", ); cat.select.addEventListener("change", find.refresh); find.refresh(); return { nodes: [cat.root, find.root], read: () => { const one = find.get(); return one ? { 할일: "연결", 갈래: "요소", 요소: one.키 } : "요소를 고를 것"; }, }; }; const logicKind = (host: DefsHost): { nodes: HTMLElement[]; read: Reader } => { const sub = grid(); const args = new Map>(); const find = pick( host, () => ({ 갈래: "로직" }), (c) => { args.clear(); sub.replaceChildren( ...(c?.입력 ?? []).map((n) => { const f = field(`${n} = (식 · 빈 칸 = 기본)`); args.set(n, f); return f.root; }), ); }, "로직 이름", ); return { nodes: [find.root, sub], read: () => { const one = find.get(); if (!one) return "부를 로직을 고를 것"; const 인자 = Object.fromEntries([...args].map(([n, f]) => [n, f.input.value.trim()])); return { 할일: "연결", 갈래: "로직", 로직: one.키, 인자 }; }, }; }; const tableKind = (host: DefsHost): { nodes: HTMLElement[]; read: Reader } => { const sub = grid(); const conds = new Map>(); const cols = createSelectField({ label: "칸", options: [{ value: "", text: "—" }], size: 24 }); const find = pick( host, () => ({ 갈래: "표" }), (c) => { conds.clear(); cols.setOptions( [{ value: "", text: "— 칸 —" }, ...(c?.칸 ?? []).map((v) => ({ value: v, text: v }))], "", ); sub.replaceChildren( ...(c?.조건 ?? []).map((n) => { const f = field(`조건 ${n} = (변수 기호 · 식)`); conds.set(n, f); return f.root; }), ); }, "표 이름", ); find.refresh(); return { nodes: [find.root, cols.root, sub], read: () => { const one = find.get(); if (!one || !cols.select.value) return "표와 칸을 고를 것"; const 조건 = Object.fromEntries([...conds].map(([n, f]) => [n, f.input.value.trim()])); return { 할일: "연결", 갈래: "표", 표: one.키, 칸: cols.select.value, 조건 }; }, }; }; /** 새 기호 줄 — 국문 이름 + 연결 고르기 · `read` 가 고침 `연결` 한 건을 줌 */ export function draftRow(token: string, host: DefsHost): Draft { const parts = symbolParts(token); const name = field("국문 이름"); const kind = createSelectField({ label: "연결", options: KINDS, value: "입력", size: 24 }); const body = grid(); const why = reasonBox(); let read: Reader = () => "연결을 고를 것"; const mount = (): void => { const made = { 입력: inputKind, 상수: constKind }[kind.select.value as "입력" | "상수"]?.(); const picked = made ?? { 요소: elementKind, 로직: logicKind, 표: tableKind }[ kind.select.value as "요소" | "로직" | "표" ](host); body.replaceChildren(...picked.nodes); read = picked.read; }; kind.select.addEventListener("change", mount); mount(); const head = el("div", { className: "m01-eq__def-actions", children: [ el("span", { className: "ui-text ui-text--muted", text: `새 기호 ${token} — 연결을 지정` }), ], }); const root = el("div", { className: "ui-eq__def m01-eq__draft", children: [head, grid(name.root, kind.root), body, why.root], }); root.dataset.eqDraft = token; return { root, say: (text) => why.say(text ? [text] : []), read: () => { const made = read(); if (typeof made === "string") return made; const nameText = name.input.value.trim(); return { ...made, ...parts, ...(nameText ? { 이름: nameText } : {}) }; }, }; }