/* ============================================================================= * line_stack_material.ts * 줄 쌓기 가져온 재료 줄 판정 표지 · 같은 파일 물음(PLAN 827-3 · 요구 811-2 · 812-1) * 표지 = 문서 머리 `가져온.재료`(서버 `M02_San_MatPick.entry`) — 아직 못 이은 줄 「규격 없음 — 고르기」 · * 「후보 여럿」 · 「우리 표에 없음」 / 그 파일 고름 줄 「파일 고름」 / 별도계상 품목 · 풍선말 = 판정 · 방법 · 단서 · 후보 * 물음 = 이름 단추로 고른 뒤 한 번 — 「같은 파일의 같은 재료 n 줄에도」 예 = 개인 층 그 파일 고름 + * 그 파일 다른 단가산출의 가져온 그대로인 줄(서버 `…/material-picks/{지문}`) · 이 줄 = 화면 [저장] · 별칭 아님 * ========================================================================== */ import { createBadge } from "@ui/ui_template_badge"; import { showToast } from "@ui/ui_template_elements"; import { askConfirm } from "@ui/ui_template_modal"; import type { FormulaDoc } from "../common_util/common_util_formula_doc"; import type { Picked } from "../common_util/common_util_line_stack_pick"; import { call, type Layer } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch"; import { at } from "../M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api"; import type { MachineCtx } from "./line_stack_machine"; interface Cand { 키: string; 이름?: string; 규격?: string; 단위?: string; } export interface MatEntry { 기호: string; 종: string; 원래: { 이름: string; 규격: string; 단위: string }; 판정: string; 방법?: string; 키?: string | null; 후보?: Cand[]; 단서?: string[]; 별도계상?: boolean; 고름?: Cand; } interface Head { 재료?: MatEntry[]; 지문?: string; 파일?: string; } const LINKED = "이어짐"; /** 같은 파일 고름을 저장했음(문서 이벤트) — 불러오기 판이 다시 읽음 */ export const MAT_PICKED = "aislo:mat-picked"; const enc = encodeURIComponent; const headOf = (d: FormulaDoc): Head | undefined => (d as { 가져온?: Head }).가져온; const text = (c: { 이름?: string; 규격?: string }): string => `${c.이름 ?? ""} ${c.규격 ?? ""}`.trim(); /** 그 단가 기호 줄의 재료 판정 칸 */ export const matEntry = (d: FormulaDoc, sym: string | undefined): MatEntry | undefined => sym ? headOf(d)?.재료?.find((x) => x.기호 === sym) : undefined; /** 풍선말 글 — 판정 · 방법 · 단서 · 후보(앞 다섯) */ export function matTip(e: MatEntry): string { const cands = (e.후보 ?? []).slice(0, 5).map((c, i) => `${i ? "" : "1 순위 "}${text(c)}`); return [ `가져온 「${text(e.원래)}」 — ${e.판정}${e.방법 ? ` (${e.방법})` : ""}`, ...(e.고름 ? [`파일 고름: ${text(e.고름)}`] : []), ...(e.단서 ?? []).map((x) => `단서: ${x}`), ...(cands.length ? [`후보: ${cands.join(" · ")}`] : []), ].join("\n"); } /** 줄 표지 — 못 이은 줄 판정 · 파일 고름 · 별도계상 품목(이어진 줄 = 없음) */ export function materialMark( e: MatEntry | undefined, link: { 갈래?: string; 키?: string }, ): HTMLElement | null { if (!e) return null; const filePick = !!e.고름 && link.키 === e.고름.키; const label = e.별도계상 ? `별도계상 · ${e.고름 ? `품목 ${text(e.고름)}` : e.키 ? "품목 이음" : e.판정}` : filePick ? "파일 고름" : e.판정 !== LINKED && link.갈래 === "숫자" ? e.판정 : ""; if (!label) return null; const done = filePick || (e.별도계상 && !!(e.고름 || e.키)); const badge = createBadge(label, done ? "info" : e.판정 === "우리 표에 없음" ? "neutral" : "warning", { size: 18 }); // prettier-ignore badge.title = matTip(e); badge.dataset.matMark = e.판정; return badge; } /** 같은 파일 물음 몸 — 층 · 이 SK · 파일 지문 · 종 · 가져온 글 · 고른 것 */ export interface SameFile { scope: { layer: Layer; owner: string | null }; me: string; 지문: string; 파일: string; 종: string; 원래: string; p: Picked; } /** 이름 단추로 고른 뒤 — 같은 파일 같은 재료에도(물음 하나 · 예일 때만 서버) */ export async function askFileWide( ctx: MachineCtx | undefined, d: FormulaDoc, sym: string | undefined, p: Picked, ): Promise { const e = matEntry(d, sym); const head = headOf(d); if (!e || e.별도계상 || !head?.지문 || !ctx?.scope || !ctx.me || p.키 === e.키) return; await askSameFile({ scope: ctx.scope, me: ctx.me, 지문: head.지문, 파일: head.파일 ?? "", 종: e.종, 원래: text(e.원래), p }); // prettier-ignore } /** 같은 파일 같은 재료 물음 — 예 = 개인 층 그 파일 고름 + 그 파일 다른 SK(가져온 그대로인 줄) · 이 SK = 화면 [저장] */ export async function askSameFile(a: SameFile): Promise { const path = `/material-picks/${enc(a.지문)}`; const got = await call<{ 수: { 블록: number; 줄: number } | null; 만든: string[] }>( at(a.scope, `${path}?종=${enc(a.종)}&me=${enc(a.me)}`), ).catch(() => null); if (!got) return; const many = got.수 ? `${got.수.줄} 줄(${got.수.블록} 블록)` : "줄"; const made = got.만든.length ? ` · 이미 만든 단가산출 ${got.만든.length} 건의 가져온 그대로인 줄도 바뀜` : ""; // prettier-ignore const ok = await askConfirm({ message: `같은 파일(${a.파일})의 같은 재료 「${a.원래}」 ${many}에도 「${text(a.p)}」 적용할까? — 다음에 그 파일을 올리면 고른 것이 보임${made} · 별칭 아님`, okLabel: "같은 파일에 적용", cancelLabel: "이 줄만", }); if (!ok) return; try { const out = await call<{ 고침: string[] }>(at(a.scope, path), { method: "POST", body: JSON.stringify({ 종: a.종, 고름: { 키: a.p.키, 이름: a.p.이름, 규격: a.p.규격 ?? "", 단위: a.p.단위 ?? "" }, 파일: a.파일, 빼기: [a.me], }), }); document.dispatchEvent(new CustomEvent(MAT_PICKED)); // 불러오기 「고를 재료」 판 다시 읽기 showToast(`같은 파일 고름 저장 · 단가산출 ${out.고침.length} 건 고침 — 이 줄은 [저장] 때`, "success"); // prettier-ignore } catch (err) { showToast(err instanceof Error ? err.message : String(err), "error"); } }