feat(m02-df): 재료 조건 찾기 검색어 틀 — 틀 칸 · 자리별 알약 · 지금 잡히는 재료 미리 보기
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ACSGaHdLgnkEkoA4LDtMU
This commit is contained in:
@@ -15,9 +15,12 @@ import {
|
||||
} from "@ui/ui_template_elements";
|
||||
import { fetchFiles, fetchSubs, type SubInfo } from "../../M01_MasterData/M01_MasterData_Api_Fetch";
|
||||
import { cfMaterials, cfValues } from "./M02_DF_Api";
|
||||
import { templateBox } from "./M02_DF_InputPanel_Template";
|
||||
import type { CfElementRef, CfLogic, CfMaterialMode, CfMaterialRule } from "./M02_DF_Model";
|
||||
import {
|
||||
catchNote,
|
||||
fillTemplate,
|
||||
templateRule,
|
||||
checkMaterial,
|
||||
elementOf,
|
||||
matches,
|
||||
@@ -59,6 +62,11 @@ const TEXT = {
|
||||
Default: ["기본 품목", "Default item"],
|
||||
DefaultNone: ["아직 안 고름 — 아래 목록에서 눌러 고름", "Not chosen — pick one below"],
|
||||
DefaultClear: ["기본 품목 비움", "Clear"],
|
||||
RuleHow: ["찾는 방식", "How to find"],
|
||||
HowPlain: ["규격 · 이름에 알약 값", "Spec / name vs a pill"],
|
||||
HowTemplate: ["검색어 틀", "Search template"],
|
||||
TemplateLine: ["검색어 틀 「{t}」", "template “{t}”"],
|
||||
Missing: ["자리 「{n}」 알약 값이 비어 있음", "Slot “{n}” has no value"],
|
||||
RuleField: ["찾는 칸", "Field"],
|
||||
FieldSpec: ["규격", "Spec"],
|
||||
FieldName: ["이름", "Name"],
|
||||
@@ -251,9 +259,35 @@ function itemList(
|
||||
export async function previewOf(
|
||||
el1: CfElementRef,
|
||||
lg: CfLogic,
|
||||
): Promise<{ found: number; names: string[]; cheapest: MatItem | null; unit: string }> {
|
||||
): Promise<{
|
||||
found: number;
|
||||
names: string[];
|
||||
cheapest: MatItem | null;
|
||||
unit: string;
|
||||
missing?: string[];
|
||||
}> {
|
||||
const scope = el1.scope ?? {};
|
||||
const word = scope.찾는말 ?? "";
|
||||
if (el1.mode === "rule" && el1.rule?.field === "검색어") {
|
||||
// 틀의 자리마다 알약 값을 끼운 글로 찾음(띄어쓴 말은 모두 들어간 것)
|
||||
const valueOf = (port: { box: string; name: string }): unknown =>
|
||||
variablePills(lg).find((p) => p.port.box === port.box && p.port.name === port.name)?.example;
|
||||
const { text, missing } = fillTemplate(el1.rule.template ?? "", el1.rule.slots ?? [], valueOf);
|
||||
if (missing.length) return { found: 0, names: [], cheapest: null, unit: "", missing };
|
||||
const r = await searchMaterials({
|
||||
q: `${word} ${text}`.trim(),
|
||||
sub: scope.구분,
|
||||
detail: scope.상세구분,
|
||||
page: 1,
|
||||
size: 200,
|
||||
});
|
||||
return {
|
||||
found: r.total,
|
||||
names: r.items.slice(0, 3).map((i) => `${i.name} ${i.spec}`.trim()),
|
||||
cheapest: cheapestOf(r.items),
|
||||
unit: commonUnit(r.items),
|
||||
};
|
||||
}
|
||||
if (el1.mode === "rule" && el1.rule) {
|
||||
const pill = variablePills(lg).find(
|
||||
(p) => p.port.box === el1.rule!.from.box && p.port.name === el1.rule!.from.name,
|
||||
@@ -300,6 +334,16 @@ const cheapestOf = (items: MatItem[]): MatItem | null =>
|
||||
.filter((i) => i.price !== null)
|
||||
.sort((a, b) => (a.price as number) - (b.price as number))[0] ?? null;
|
||||
|
||||
/** 미리 보기 글 — 틀 자리 값이 비면 그 까닭 · 아니면 잡힌 수에 따른 알림 */
|
||||
const noteOf = (p: {
|
||||
found: number;
|
||||
names: string[];
|
||||
missing?: string[];
|
||||
}): ReturnType<typeof catchNote> =>
|
||||
p.missing?.length
|
||||
? { kind: "none", text: t("Missing", { n: p.missing.join(" · ") }) }
|
||||
: catchNote(p.found, p.names[0]);
|
||||
|
||||
/** 요소 한 줄 요약 — 방식 · 범위 · 규칙 */
|
||||
function summaryOf(e: CfElementRef, lg: CfLogic): string {
|
||||
const parts: string[] = [];
|
||||
@@ -307,7 +351,8 @@ function summaryOf(e: CfElementRef, lg: CfLogic): string {
|
||||
.map(([, v]) => v)
|
||||
.join(" / ");
|
||||
if (scope) parts.push(t("ScopeLine", { s: scope }));
|
||||
if (e.rule) {
|
||||
if (e.rule?.field === "검색어") parts.push(t("TemplateLine", { t: e.rule.template ?? "" }));
|
||||
else if (e.rule) {
|
||||
const p = variablePills(lg).find(
|
||||
(x) => x.port.box === e.rule!.from.box && x.port.name === e.rule!.from.name,
|
||||
);
|
||||
@@ -336,6 +381,7 @@ export function renderMaterial(
|
||||
const chosen = el("div", { className: "m02-dfip__chosen" });
|
||||
const body = el("div", { className: "m02-dfip__stack" });
|
||||
let mode: CfMaterialMode = "fixed";
|
||||
let how: "plain" | "template" = "plain";
|
||||
|
||||
const drawChosen = (): void => {
|
||||
chosen.replaceChildren(
|
||||
@@ -380,7 +426,7 @@ export function renderMaterial(
|
||||
if (e.mode && e.mode !== "fixed")
|
||||
void previewOf(e, lg).then(
|
||||
(p) => {
|
||||
const n = catchNote(p.found, p.names[0]);
|
||||
const n = noteOf(p);
|
||||
note.textContent = `${t("Preview")}: ${n.text}`;
|
||||
note.classList.toggle("is-bad", n.kind !== "one");
|
||||
if (p.cheapest?.price != null && !ref) val.textContent = fmt(p.cheapest.price);
|
||||
@@ -463,9 +509,30 @@ export function renderMaterial(
|
||||
compact: true,
|
||||
onChange: () => void refresh(),
|
||||
});
|
||||
const howBox = createSelectField({
|
||||
label: t("RuleHow"),
|
||||
options: [
|
||||
{ value: "plain", text: t("HowPlain") },
|
||||
{ value: "template", text: t("HowTemplate") },
|
||||
],
|
||||
value: how,
|
||||
compact: true,
|
||||
onChange: (v) => ((how = v as "plain" | "template"), drawBody()),
|
||||
});
|
||||
const tpl = templateBox(lg, () => void refresh());
|
||||
const defNote = el("p", { className: "m02-dfip__muted" });
|
||||
const build = (): CfElementRef => {
|
||||
const sc = scope.get();
|
||||
if (mode === "rule" && howBox.select.value === "template") {
|
||||
const g = tpl.get();
|
||||
const sc2 = scope.get();
|
||||
return materialElement(
|
||||
mode,
|
||||
name.input.value,
|
||||
{ 구분: sc2.sub, 상세구분: sc2.detail, 찾는말: word.input.value.trim() },
|
||||
{ rule: templateRule(g.template, g.slots) },
|
||||
);
|
||||
}
|
||||
const rule: CfMaterialRule | undefined =
|
||||
mode === "rule" && pills[Number(pillBox.select.value)]
|
||||
? {
|
||||
@@ -490,7 +557,7 @@ export function renderMaterial(
|
||||
try {
|
||||
const p = await previewOf(cand, lg);
|
||||
if (mine !== seq) return;
|
||||
const n = catchNote(p.found, p.names[0]);
|
||||
const n = noteOf(p);
|
||||
note.textContent = `${t("Preview")}: ${n.text}${p.cheapest?.price != null ? ` · ${t("Price", { p: fmt(p.cheapest.price), u: p.cheapest.unit })}` : ""}`;
|
||||
note.className = `m02-dfip__note${n.kind === "one" ? "" : " is-bad"}`;
|
||||
} catch {
|
||||
@@ -530,9 +597,14 @@ export function renderMaterial(
|
||||
scope.root,
|
||||
word.root,
|
||||
...(mode === "rule"
|
||||
? pills.length
|
||||
? [fieldBox.root, opBox.root, pillBox.root]
|
||||
: [el("p", { className: "m02-dfip__muted is-bad", text: t("NoPill") })]
|
||||
? [
|
||||
howBox.root,
|
||||
...(howBox.select.value === "template"
|
||||
? [tpl.root]
|
||||
: pills.length
|
||||
? [fieldBox.root, opBox.root, pillBox.root]
|
||||
: [el("p", { className: "m02-dfip__muted is-bad", text: t("NoPill") })]),
|
||||
]
|
||||
: [
|
||||
el("h5", { text: t("Default") }),
|
||||
defNote,
|
||||
|
||||
Reference in New Issue
Block a user