fix(M01): 자리 표시 든 요소 줄 모달 — 요소조각의 푼 키로 단가 · 날글 안 찍음 (PLAN 5장 다시 돌림 3)

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 13:30:16 +09:00
co-authored by Claude Sonnet 5
parent c1181bc9b5
commit 5e9df81cbd
3 changed files with 39 additions and 4 deletions
@@ -101,6 +101,9 @@ interface Entry {
/** 서버 조각으로 그린 단가 자리 · 수량 풀이 — 줄 모달이 씀(붙기 전엔 없음) */
unitFrag?: Fragment[];
piecesFrag?: Fragment[];
/** 서버 요소조각의 첫 요소 — 자리 표시 든 줄이 입력으로 풀린 키 · 글 */
unitRef?: string;
unitText?: string;
}
/** 재료 고르기 줄 — `요소` 가 글 대신 {구분·상세구분·규격·대표} 객체 */
@@ -236,6 +239,8 @@ function entries(ctx: DetailContext): Entry[] {
open: (self) =>
openMaterialModal({
unitFrag: self.unitFrag,
unitRef: self.unitRef,
unitText: self.unitText,
qtyFrag: self.piecesFrag,
title: item.이름 ?? (cond ? condLabel(cond) : item.),
expr: item.수량,
@@ -304,7 +309,12 @@ function attachPieces(all: Entry[], ctx: DetailContext): void {
const line = [own, ...costs].map((c) => found.get(`${c}|${e.idx}`)).find(Boolean);
const pieces = line?.;
e.qtyFrag = e.piecesFrag = pieces?.length ? toFragments(pieces, ctx) : [];
if (line?.?.length) e.unitFrag = toFragments(line., ctx);
if (line?.?.length) {
e.unitFrag = toFragments(line., ctx);
const first = line..find((piece) => piece.kind === "요소");
e.unitRef = first?.?.;
e.unitText = first?.;
}
}
}
@@ -64,6 +64,9 @@ export interface ModalTarget {
values: Record<string, string>;
/** 서버 `요소조각` — 단가 자리(요소 · 로직 부르기)를 조각으로 · 없으면 로직 키만 알약으로 */
unitFrag?: Fragment[];
/** 서버가 입력으로 푼 단가 요소 키 · 글 — 자리 표시(`{…}`) 든 줄이 씀 */
unitRef?: string;
unitText?: string;
/** 서버 수량 조각 — 한 줄 풀이를 조각으로(알약 포함) · 없으면 식 풀이 글 */
qtyFrag?: Fragment[];
}
@@ -333,10 +336,31 @@ export function openMaterialModal(target: ModalTarget): void {
rate.append(muted("…")); // 후보 목록이 오면 그 줄로 채움 — 100건 밖이면 키로 따로 찾아 옴
} else if (cond) {
rate.append(muted(tl("Pill_NoPick")));
} else if (element.includes("{")) {
// 자리 표시 든 요소 — 날글 대신 서버가 푼 키로 단가 · 못 풀면 입력 이름만
const ref = target.unitRef;
if (!ref) {
const names = [...element.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]).join(" · ");
rate.append(muted(`${target.unitText || names}${tl("Pill_Unresolved")}`));
} else {
const show = (brief: ElementBrief | null): void => {
rate.replaceChildren(
brief
? priceView(ref, brief)
: muted(`${target.unitText ?? ref}${tl("Pill_NoValue")}`),
);
setSum(brief);
};
rate.append(muted("…"));
const cached = target.prices[ref];
if (cached) show(cached);
else
void searchElements(groupOf(ref), ref)
.then((got) => show(got.items.find((it) => it.ref === ref) ?? null))
.catch(() => show(null));
}
} else if (element) {
rate.append(
muted(`${element}${tl(element.includes("{") ? "Pill_NoPick" : "Pill_NoValue")}`),
);
rate.append(muted(`${element}${tl("Pill_NoValue")}`));
}
body.replaceChildren(
rate,
@@ -59,6 +59,7 @@ const TEXT = {
"고르기 조건에 맞는 마스터 줄이 없음 — 단가를 못 찾음",
"No master row matches the pick conditions",
],
Pill_Unresolved: ["입력이 비어 풀리지 않음", "Not resolved — input empty"],
Pill_NoValue: ["관리자 값 미확보 — 마스터에 단가가 없음", "No price in master yet"],
Modal_Qty: ["수량 식", "Quantity formula"],
Modal_Price: ["단가 자료", "Price source"],