feat(796-2a): 수식 탭 화면 — 공통 수식 부품 · M01 수식 탭(첫 탭) · 고침 · 변수 창 · 옵션 가로
- ui_template_equation(.ts · _line.ts · .css): 분수 · 첨자 · 괄호 · % · 조건 칩 · 하위 로직 블록(접힘) · 결과명 = 이름 식 = 값 식 = 결과 + 변수 풀이 · 부품 모음 견본 한 줄 - M01 LogicLab_Equation: 옵션 가로(도구줄 + 값 칸 · 찾기형 · 말줄임) · 줄 고르면 식 칸 + 숨은 아이콘 줄 · 서버 미리 셈 다시 그림 · 고침 목록 캐시 → [수식 저장](개정 확인) · 떠나기 전 버림 물음 - 변수 창(이름 · 기호 · 위 · 아래첨자 · 단위 · 값 · 보이는 자리) · 변수 더함 · 지움(쓰는 곳 있으면 서버 까닭 그대로) - 보기 고르기 = 수식 첫 탭 · 단가산출 상세 · 시트 · 인력재료경비 · 시험계산 · 텍스트 수식은 뒤로(걷지 않음) - test_796_2_equation_screen 새로 · test_794_1 기대 갱신(첫 탭 수식 · LogicLab 700 줄 상한 — 수식 이음 14 줄) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_LogicLab_EquationVar.ts
|
||||
* 수식 탭 변수 창(PLAN 796-2 · 요구 657) — 변수 고침 · 변수 더함 · 변수 지움(쓰는 곳 있으면 막고 목록 보임).
|
||||
* 고침 창 = 이름 · 기호 · 위첨자 · 아래첨자 · 단위 · 값(입력 변수만 · 시험 값 · 저장 안 함) · [적용] · [변수 지움]
|
||||
* 더함 창 = 갈래(입력 · 중간) · 열쇠 · 이름 · 기호 · 첨자 · 단위 · 기본값(입력) | 식 · 자리(중간)
|
||||
* 적용 = 부르는 쪽 `commit` 이 서버 미리 셈(`/logic/equation/preview`)을 거쳐 되면 [] · 안 되면 까닭 — 까닭은 창 안 띠에 그대로
|
||||
* 부품 = 모달 · 입력 · 고르기 · 단추 · 띠 알림 · 열쇠 · 식 검사 · 이름 겹침은 서버 몫
|
||||
* ========================================================================== */
|
||||
|
||||
import type { EqVarRow } 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 { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { addEdit, varEdits, type EqEdit, type VarForm } from "./logic/M01_Logic_Equation_Model";
|
||||
|
||||
const TEXT = {
|
||||
VarTitle: ["변수 고침", "Edit variable"],
|
||||
AddTitle: ["변수 더함", "Add variable"],
|
||||
Close: ["닫기", "Close"],
|
||||
Apply: ["적용", "Apply"],
|
||||
Drop: ["변수 지움", "Delete variable"],
|
||||
Key: ["열쇠 (식이 부르는 이름 · 안 바뀜)", "Key (used by formulas · fixed)"],
|
||||
Name: ["이름", "Name"],
|
||||
Sign: ["기호", "Symbol"],
|
||||
Up: ["위첨자", "Superscript"],
|
||||
Down: ["아래첨자", "Subscript"],
|
||||
Unit: ["단위", "Unit"],
|
||||
Value: ["값 (시험 값 · 저장 안 함)", "Value (trial · not saved)"],
|
||||
Digits: ["보이는 자리 (0~6 · 빈 칸 = 전체 자리)", "Digits (0-6 · blank = full)"],
|
||||
Kind: ["갈래", "Kind"],
|
||||
Input: ["입력", "Input"],
|
||||
Mid: ["중간", "Intermediate"],
|
||||
Default: ["기본값", "Default"],
|
||||
Formula: ["식", "Formula"],
|
||||
After: ["자리", "Place"],
|
||||
End: ["맨 끝", "End"],
|
||||
Top: ["맨 위", "Top"],
|
||||
Why: ["안 됨", "Not applied"],
|
||||
Hint: ["이름 · 기호 · 첨자를 바꾸면 쓰는 모든 식에 함께 보임", "Renames show in every formula"],
|
||||
} as const satisfies Record<string, readonly [string, string]>;
|
||||
const t = (k: keyof typeof TEXT): string => TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0];
|
||||
|
||||
export type Commit = (edits: EqEdit[]) => Promise<string[]>;
|
||||
|
||||
const grid = (...kids: HTMLElement[]): HTMLElement =>
|
||||
el("div", { className: "m01-eq__form", children: kids });
|
||||
|
||||
/** 까닭 띠 — 있으면 보임 · 없으면 비움 */
|
||||
function reasonBox(): { root: HTMLElement; set: (why: string[]) => void } {
|
||||
const root = el("div", { className: "m01-eq__why" });
|
||||
return {
|
||||
root,
|
||||
set: (why) =>
|
||||
root.replaceChildren(
|
||||
...(why.length
|
||||
? [createBanner({ tone: "danger", title: t("Why"), text: why.join(" · ") })]
|
||||
: []),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export interface VarDialog {
|
||||
row: EqVarRow;
|
||||
/** 입력 변수 시험 값(옵션 칸 글) */
|
||||
value: string;
|
||||
commit: Commit;
|
||||
/** 시험 값을 바꿈(저장 안 함) */
|
||||
onValue: (value: string) => void;
|
||||
/** 변수를 지운 뒤 — 시험 값 비움 */
|
||||
onDropped: (key: string) => void;
|
||||
}
|
||||
|
||||
export function openVarDialog(o: VarDialog): void {
|
||||
const v = o.row;
|
||||
const field = (label: string, value: string): ReturnType<typeof createInputField> =>
|
||||
createInputField({ label, value, size: 28 });
|
||||
const name = field(t("Name"), v.이름);
|
||||
const sign = field(t("Sign"), v.기호);
|
||||
const up = field(t("Up"), v.위);
|
||||
const down = field(t("Down"), v.아래);
|
||||
const unit = field(t("Unit"), v.단위);
|
||||
const val = field(t("Value"), o.value);
|
||||
const shown = v.표시 === undefined ? "" : String(v.표시);
|
||||
const digits = field(t("Digits"), shown);
|
||||
const why = reasonBox();
|
||||
openModal({
|
||||
title: `${t("VarTitle")} · ${v.열쇠}`,
|
||||
closeLabel: t("Close"),
|
||||
width: "40rem",
|
||||
mount: (body, close, actions) => {
|
||||
body.append(
|
||||
el("p", { className: "ui-text ui-text--muted", text: t("Hint") }),
|
||||
grid(
|
||||
name.root,
|
||||
sign.root,
|
||||
up.root,
|
||||
down.root,
|
||||
unit.root,
|
||||
...(v.갈래 === "입력" ? [val.root] : [digits.root]),
|
||||
),
|
||||
why.root,
|
||||
);
|
||||
actions.append(
|
||||
createButton({
|
||||
label: t("Drop"),
|
||||
variant: "danger-ghost",
|
||||
onClick: async () => {
|
||||
const bad = await o.commit([{ 할일: "지움", 열쇠: v.열쇠 }]);
|
||||
why.set(bad);
|
||||
if (bad.length) return;
|
||||
o.onDropped(v.열쇠);
|
||||
close();
|
||||
},
|
||||
}),
|
||||
createButton({
|
||||
label: t("Apply"),
|
||||
onClick: async () => {
|
||||
const now: VarForm = {
|
||||
열쇠: v.열쇠,
|
||||
이름: name.input.value.trim(),
|
||||
기호: sign.input.value.trim(),
|
||||
위: up.input.value.trim(),
|
||||
아래: down.input.value.trim(),
|
||||
단위: unit.input.value.trim(),
|
||||
표시: digits.input.value.trim(),
|
||||
};
|
||||
const edits = varEdits(
|
||||
{
|
||||
열쇠: v.열쇠,
|
||||
이름: v.이름,
|
||||
기호: v.기호,
|
||||
위: v.위,
|
||||
아래: v.아래,
|
||||
단위: v.단위,
|
||||
표시: shown,
|
||||
},
|
||||
now,
|
||||
);
|
||||
const bad = edits.length ? await o.commit(edits) : [];
|
||||
why.set(bad);
|
||||
if (bad.length) return;
|
||||
if (v.갈래 === "입력" && val.input.value.trim() !== o.value)
|
||||
o.onValue(val.input.value.trim());
|
||||
close();
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export interface AddDialog {
|
||||
/** 중간 줄 열쇠 차례(자리 고르기) */
|
||||
mids: string[];
|
||||
commit: Commit;
|
||||
}
|
||||
|
||||
export function openAddDialog(o: AddDialog): void {
|
||||
const field = (label: string, value = ""): ReturnType<typeof createInputField> =>
|
||||
createInputField({ label, value, size: 28 });
|
||||
const kind = createSelectField({
|
||||
label: t("Kind"),
|
||||
options: [
|
||||
{ value: "입력", text: t("Input") },
|
||||
{ value: "중간", text: t("Mid") },
|
||||
],
|
||||
value: "입력",
|
||||
size: 28,
|
||||
});
|
||||
const key = field(t("Key"));
|
||||
const name = field(t("Name"));
|
||||
const sign = field(t("Sign"));
|
||||
const up = field(t("Up"));
|
||||
const down = field(t("Down"));
|
||||
const unit = field(t("Unit"));
|
||||
const dflt = field(t("Default"));
|
||||
const expr = field(t("Formula"));
|
||||
const place = createSelectField({
|
||||
label: t("After"),
|
||||
options: [
|
||||
{ value: "", text: t("End") },
|
||||
{ value: "\u0000top", text: t("Top") },
|
||||
...o.mids.map((m) => ({ value: m, text: m })),
|
||||
],
|
||||
value: "",
|
||||
size: 28,
|
||||
});
|
||||
const why = reasonBox();
|
||||
const sync = (): void => {
|
||||
const mid = kind.select.value === "중간";
|
||||
dflt.root.hidden = mid;
|
||||
expr.root.hidden = place.root.hidden = !mid;
|
||||
};
|
||||
kind.select.addEventListener("change", sync);
|
||||
sync();
|
||||
openModal({
|
||||
title: t("AddTitle"),
|
||||
closeLabel: t("Close"),
|
||||
width: "40rem",
|
||||
mount: (body, close, actions) => {
|
||||
body.append(
|
||||
grid(
|
||||
kind.root,
|
||||
key.root,
|
||||
name.root,
|
||||
sign.root,
|
||||
up.root,
|
||||
down.root,
|
||||
unit.root,
|
||||
dflt.root,
|
||||
expr.root,
|
||||
place.root,
|
||||
),
|
||||
why.root,
|
||||
);
|
||||
actions.append(
|
||||
createButton({
|
||||
label: t("Apply"),
|
||||
onClick: async () => {
|
||||
const mid = kind.select.value === "중간";
|
||||
const at = place.select.value;
|
||||
const edit = addEdit(
|
||||
{
|
||||
열쇠: key.input.value,
|
||||
이름: name.input.value,
|
||||
기호: sign.input.value,
|
||||
위: up.input.value,
|
||||
아래: down.input.value,
|
||||
단위: unit.input.value,
|
||||
갈래: mid ? "중간" : "입력",
|
||||
기본값: dflt.input.value.trim(),
|
||||
식: expr.input.value.trim(),
|
||||
},
|
||||
at === "\u0000top" ? "" : at || undefined,
|
||||
);
|
||||
const bad = await o.commit([edit]);
|
||||
why.set(bad);
|
||||
if (!bad.length) close();
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user