feat(748-2): 텍스트 수식 — 행 고르기 걷음 · 변수 이름 · 값 하나하나 누르면 줄 모달(748-1) · 줄마다 옵션 값 칩 걷음(748-3 한 조각)

- 텍스트 수식 줄 hover · 누름 고정(행 단위 선택) 걷음
- 변수 이름 식 = 변수 이름마다 낮은 단추 · 값 든 식 = 수 하나하나 낮은 단추 → 같은 줄의 상세 모달(적용 · 삭제 · 닫기)
- 변수가 시험 입력(옵션)이면 옵션 고르기 칩으로
- 공용 상세 `lineOpener` 새로 — M02 SK 상세 · M01 단가산출 기초 같은 길(M01 = 시험 고침 고리)
- 상세 보기 줄마다 붙던 옵션 값 칩(`쓰는입력`) 걷음 — 위 옵션 칸이 대신(748-3)
- 새 파일 `LogicLab_FormulaTokens`(조각 나누기 순수 셈) · 시험 test_748_2 · 낡은 시험 고침 test_722(inputChip 줄 걷음)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 22:15:33 +09:00
co-authored by Claude Sonnet 5.5
parent 6af23d42e1
commit 0c0d0ce629
9 changed files with 192 additions and 63 deletions
@@ -15,6 +15,7 @@ import { optionGrid, varRow } from "../A00_Common/calc_sheet";
import { formatMoney } from "./M01_MasterData_UI_Logic_Money";
import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text";
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
import { nameTokens, tokenRow, valueTokens } from "./M01_MasterData_UI_LogicLab_FormulaTokens";
import "./M01_MasterData_UI_LogicLab_Formula.css";
/** 로직 열자마자 값 없이 부른 미리보기 응답은 `계산됨` 을 false 로 표시 — [계산] 을 거친 응답은 안 붙어 true 취급 */
@@ -26,6 +27,8 @@ export interface FormulaContext {
inputs?: LogicInput[];
values?: Record<string, string>;
onInput?: (name: string, value: string) => void;
/** 748-2 줄 → 그 줄 자료 모달 열기(변수 · 값을 누르면) — 없으면 글자만 */
open?: (line: TextLine) => (() => void) | undefined;
}
/* ── 순수 셈: 줄이 쓰는 입력 ── */
@@ -128,7 +131,6 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void {
el("strong", { text: tl("Formula_Right") }),
],
});
const rows: HTMLElement[] = [];
const specs = ctx.onInput ? (ctx.inputs ?? []) : [];
// 737-2 옵션 칸 = 비목 상자 맨 위 — 그 상자 줄이 쓰는 입력 + 어느 줄도 안 쓰는 입력(공통 · 737-3)
const names = specs.map((x) => x.이름);
@@ -149,22 +151,41 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void {
);
};
const calculated = text.계산됨 !== false; // [계산] 뒤 응답은 표식이 안 붙음 — 미리보기만 false
const light = (pair: string, on: boolean): void =>
rows.forEach((r) => r.dataset.pair === pair && r.classList.toggle("is-on", on));
const onInput = ctx.onInput;
/** 변수 이름이 시험 입력이면 옵션 고르기 · 아니면 그 줄 모달 */
const spec = (name: string): LogicInput | undefined =>
onInput ? specs.find((x) => x.이름 === name) : undefined;
// 737-2 비목마다 구분 상자 — 맨 위 옵션 칸 · 아래 두 칸 식 줄들 · 소계
const boxes = text.groups.map((g) => {
const lines = g.줄.map((line, i) => {
const pair = String(line.짝 ?? `${g.비목}${i}`);
const row = el("div", {
const open = ctx.open?.(line);
return el("div", {
className: `m01lab-fx__row m01lab__text${line.까닭 ? " m01lab__text--off" : ""}`,
attrs: { "data-text-line": line.이름, "data-pair": pair },
children: [
el("div", { className: "m01lab-fx__cell", text: line.이름글 ?? line.이름 }),
el("div", {
className: "m01lab-fx__cell",
children: [
tokenRow(nameTokens(line.이름글 ?? line.이름), open, (name) => {
const s = spec(name);
return s && onInput
? inputChip(s, ctx.values?.[name] ?? "", (v) => onInput(name, v))
: null;
}),
],
}),
el("div", {
className: "m01lab-fx__cell",
children: calculated
? [
el("div", { text: `${line.이름} : ${line.글}` }),
el("div", {
className: "m01lab-fx__val",
children: [
el("span", { text: `${line.이름} :` }),
tokenRow(valueTokens(line.글), open),
],
}),
...(line.까닭
? [
el("div", {
@@ -178,15 +199,6 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void {
}),
],
});
row.addEventListener("mouseenter", () => light(pair, true));
row.addEventListener("mouseleave", () => light(pair, false));
row.addEventListener("click", () => {
const pinned = row.classList.toggle("is-pin");
rows.forEach((r) => r !== row && r.classList.remove("is-pin"));
if (!pinned) row.classList.remove("is-pin");
});
rows.push(row);
return row;
});
const card = createCard({
title: g.비목,