fix(M01): 계산 멈춤에도 수량 칸 날식 · 까닭 날글 안 찍음 — 마지막 조각 · 빈칸 · 까닭 걸러냄 (PLAN 6장 셋째 줄)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
2026-09-24 14:35:19 +09:00
co-authored by Claude Sonnet 5
parent 5e9df81cbd
commit 209654f311
5 changed files with 35 additions and 9 deletions
@@ -59,6 +59,8 @@ interface Opened extends Draft {
reasons: string[];
lines: CalcLine[] | null;
text: TextAnswer | null;
/** 마지막으로 잘 받은 읽는 식 — 멈춘 뒤에도 수량 칸 조각을 이걸로 */
lastText?: TextAnswer;
ownHost: HTMLElement;
values: Record<string, string>;
}
@@ -170,6 +172,7 @@ export async function mountM01LogicLab(
values: current.values,
onText: (text) => {
current.text = text;
if (text.ok) current.lastText = text;
drawEditor(); // [계산] 전 미리보기 응답도 왼쪽 이름 식 칸에 바로 비치게 — onLines 와 같은 자리
},
onLines: (lines) => {
@@ -202,6 +205,7 @@ export async function mountM01LogicLab(
prices: current.prices,
lines: current.lines,
text: current.text,
lastText: current.lastText,
values: current.values,
ownHost: current.ownHost,
onChange: touch,
@@ -20,7 +20,7 @@ import type {
import { buildEditor, qtyKind } from "./M01_MasterData_UI_Logic_Edit";
import { formatMoney, formatPrice } from "./M01_MasterData_UI_Logic_Money";
import { openPicker } from "./M01_MasterData_UI_Logic_Pick";
import { tx } from "./M01_MasterData_UI_Logic_Text";
import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text";
import { buildFormula } from "./M01_MasterData_UI_LogicLab_Formula";
import {
condText,
@@ -64,6 +64,8 @@ export interface DetailContext {
lines: CalcLine[] | null;
/** 마지막 시험 계산의 읽는 식 줄(서버가 줌) */
text: TextAnswer | null;
/** 마지막으로 잘 받은 읽는 식 — `text` 가 멈춤이면 조각을 이걸로 */
lastText?: TextAnswer;
values: Record<string, string>;
/** 자체 로직 편집기 자리 — 다시 그려도 같은 칸을 씀(고치던 값이 안 날아감) */
ownHost: HTMLElement;
@@ -166,7 +168,7 @@ function infoBox(ctx: DetailContext): HTMLElement {
className: "m01-logic__reasons",
children: [
el("strong", { text: tx("Head_Blocked") }),
...ctx.reasons.map((r) => el("div", { text: r })),
...ctx.reasons.map((r) => el("div", { text: safeReason(r) })),
],
}),
]
@@ -287,14 +289,18 @@ function entries(ctx: DetailContext): Entry[] {
* 이름을 차례로 따라가며 줄 차례에 맞춤 · 금액 0 로직 부름 줄처럼 묶음이 달라도 찾음 ·
* 끝내 못 찾으면 날식 대신 빈칸(계산 전 · 서버 실패면 식 글자 그대로) */
function attachPieces(all: Entry[], ctx: DetailContext): void {
if (!ctx.text?.ok) return;
const source = ctx.text?.ok ? ctx.text : ctx.lastText;
if (!source?.ok) {
if (ctx.text) for (const e of all) e.qtyFrag = e.piecesFrag = []; // 멈춤 · 받은 조각 없음 — 날식 대신 빈칸
return;
}
const ho = ctx.row. ?? [];
const names = [
...ho.map((item) => item. ?? (condOf(item) ? condLabel(condOf(item)!) : item.)),
...(ctx.row. ?? []).map((extra) => extra.),
];
const found = new Map<string, TextLine>();
for (const g of ctx.text.groups) {
for (const g of source.groups) {
let from = 0;
for (const line of g.) {
const at = names.findIndex((n, i) => i >= from && n === line.);
@@ -8,7 +8,7 @@
import { el } from "@ui/ui_template_elements";
import type { TextAnswer } from "./M01_MasterData_UI_Logic_Api";
import { formatMoney } from "./M01_MasterData_UI_Logic_Money";
import { tx } from "./M01_MasterData_UI_Logic_Text";
import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text";
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
import "./M01_MasterData_UI_LogicLab_Formula.css";
@@ -31,7 +31,10 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void {
host.replaceChildren(
el("div", {
className: "m01-logic__reasons",
children: [el("strong", { text: tx("Calc_Stopped") }), el("div", { text: text.reason })],
children: [
el("strong", { text: tx("Calc_Stopped") }),
el("div", { text: safeReason(text.reason) }),
],
}),
);
return;
@@ -62,7 +65,7 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void {
? [
el("div", { text: `${line.} : ${line.}` }),
...(line.
? [el("div", { className: "m01-logic__muted", text: line.까닭 })]
? [el("div", { className: "m01-logic__muted", text: safeReason(line.) })]
: []),
]
: [el("div", { className: "m01-logic__muted", text: tl("Formula_Slot") })],
@@ -17,7 +17,7 @@ import {
} from "./M01_MasterData_UI_Logic_Api";
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
import { formatMoney } from "./M01_MasterData_UI_Logic_Money";
import { tx } from "./M01_MasterData_UI_Logic_Text";
import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text";
/** 고르기 값이 마스터 키(자재·기계·직종)면 이름으로 바꿔 보임 — 값 칸은 키 그대로 보냄 */
const KEY_RE = /^[A-Z]{2}\d{6}$/;
@@ -201,7 +201,10 @@ function answerView(answer: CalcAnswer, draft: boolean, money: boolean): HTMLEle
...note,
el("div", {
className: "m01-logic__reasons",
children: [el("strong", { text: tx("Calc_Stopped") }), el("div", { text: answer.reason })],
children: [
el("strong", { text: tx("Calc_Stopped") }),
el("div", { text: safeReason(answer.reason) }),
],
}),
];
}
@@ -138,6 +138,10 @@ const TEXT = {
"「{name}」 규격이 여럿이라 정하지 못함 — 위 「쓰이는 재료·기계」에서 하나를 골라 주세요",
"「{name}」 has several specs — pick one in the materials list above",
],
Stop_Generic: [
"이 조건으로는 셈하지 못함 — 입력값 · 표에 그 조합이 있는지 확인",
"Cannot be calculated with these inputs — check the values and the table",
],
Stop_NoPrice: [
"「{name}」 단가가 자료에 비어 있어 셈하지 못함 — 자료를 채워야 함",
"「{name}」 has no price in the data yet",
@@ -158,6 +162,12 @@ export function plainReason(reason: string): string {
return reason;
}
/** 화면에 찍는 까닭의 마지막 방어 — 중괄호 · 따옴표 · Decimal( 이 섞인 날글은 사람 말 한 줄로 */
export function safeReason(reason: string): string {
const plain = plainReason(reason);
return /[{}"']|Decimal\(/.test(plain) ? tx("Stop_Generic") : plain;
}
/** 흐름 그림 머리 「이렇게 씀」 — 줄 목록 */
/** 공용 `t()` 와 같은 뜻 — `{n}`·`{v}` 는 채움 */