Files
Aislo/M01_MasterData/M01_MasterData_UI_LogicLab_EquationVar.ts
T
eomsangdonandClaude Sonnet 5.5 eaadb48a2b 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>
2026-11-07 11:46:28 +09:00

248 lines
8.5 KiB
TypeScript

/* =============================================================================
* 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();
},
}),
);
},
});
}