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
+12 -23
View File
@@ -35,7 +35,6 @@ import { formatMoney, formatPrice } from "./M01_MasterData_UI_Logic_Money";
import { tx } from "./M01_MasterData_UI_Logic_Text";
import { pickMaterial } from "./M01_MasterData_UI_Logic_CalcPick";
import { coverForm, type CoverPart, type LineCover } from "./M01_MasterData_UI_LogicLab_Cover";
import { inputChip } from "./M01_MasterData_UI_LogicLab_Formula";
import {
condText,
openMaterialModal,
@@ -419,31 +418,14 @@ const COLS: Column[] = [
const money = (v: number | null): string => (v === null ? "" : formatMoney(v));
/** 이 줄이 쓰는 입력 칩 — 서버 `쓰는입력` ∩ 로직 입력 */
function chips(e: Entry, ctx: HoContext): HTMLElement[] {
const onInput = ctx.onInput;
const uses = e.textLine?.쓰는입력 ?? [];
const specs = (ctx.row.입력 ?? []).filter((s) => uses.includes(s.이름));
if (!onInput || !specs.length) return [];
return [
el("div", {
className: "m01lab-fx__vars",
children: specs.map((s) => inputChip(s, ctx.values[s.이름] ?? "", (v) => onInput(s.이름, v))),
}),
];
}
/** 계산식 칸 — 값 든 식(계산 뒤) · 계산 전 = 수량 식 · 알약 · 입력 칩 · 이름 식은 풍선 */
function formulaCell(e: Entry, ctx: HoContext, text = ""): HTMLElement {
/** 계산식 칸 — 값 든 식(계산 뒤) · 계산 전 = 수량 식 · 알약 · 이름 식은 풍선(748-3 옵션 값 꼬리표 없음) */
function formulaCell(e: Entry, text = ""): HTMLElement {
const tline = e.textLine;
const main = text || (tline ? sheetFormula(tline.글) : e.qty);
const pills = e.qtyFrag.some((f) => "pill" in f) ? [fragmentRow(e.qtyFrag)] : [];
const cell = el("div", {
className: "m01lab__qty-cell",
children: [
el("div", { className: "m01lab__qty-text", text: main }),
...(text ? [] : [...pills, ...chips(e, ctx)]),
],
children: [el("div", { className: "m01lab__qty-text", text: main }), ...(text ? [] : pills)],
});
cell.title = tline?.이름글 ?? e.qty;
return cell;
@@ -485,7 +467,7 @@ function lineRow(e: Entry, ctx: HoContext, label = e.name): HTMLTableRowElement
nameCell(e, label, e.share ? "" : tag),
e.qtyValue,
e.unit,
formulaCell(e, ctx),
formulaCell(e),
e.price,
e.line?.까닭 ? gapCell(e.line.까닭) : money(e.amount),
]);
@@ -528,7 +510,7 @@ function mergedRows(m: HoMerged<Entry>, ctx: HoContext): HTMLTableRowElement[] {
nameCell(first, `▸ ${first.name}`, tag),
first.qtyValue,
first.unit,
formulaCell(first, ctx, short),
formulaCell(first, short),
first.linePrice,
money(m.금액),
]);
@@ -657,6 +639,13 @@ function commonCard(rows: HTMLElement[]): HTMLElement {
return card;
}
/** 748-2 텍스트 수식 줄 → 그 줄 자료 모달 열기(상세 줄과 같은 모달 · 못 찾으면 없음) */
export function lineOpener(ctx: HoContext): (line: TextLine) => (() => void) | undefined {
const by = new Map<TextLine, () => void>();
for (const e of entries(ctx)) if (e.textLine) by.set(e.textLine, () => e.open(e));
return (line) => by.get(line);
}
/** 공용 단가산출 상세 몸 — 박스들(맨 위 옵션 칸 · 공통 옵션 포함) · 맨 아래 한 줄(비목 소계 셋 · 계) */
export function buildHo(ctx: HoContext): HTMLElement[] {
const all = entries(ctx);