Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ImportedPanel.ts
T
eomsangdonandClaude Opus 5.5 bed35e3bc3 feat(814-1 3·4·6·7·8·9·10): 불러오기 결정 반영 — 세 줄 묶기 · 꼬리 뗀 맞대기 · 유가 · 재료 · 까닭 · 가벼움
- 3 연결 찾기 = 엑셀 이름 규격 · 단위 꼬리 뗀 이름으로 맞댐(`M02_Doc_Match_Note.bare` · 보이는 이름 그대로) · STmate 블록 규격도 가져온 칸에
- 4 기계 세 줄 = 기계 한 줄 — STmate X 코드 노 · 재 · 경 세 참조 줄(같은 수량 · 번지) 묶음(묶어서 셈 못 하는 세 줄만 그대로) ·
  문서 단계 묶기 넓힘(차례 아무렇게 · 시간당 경비 이음 없으면 엑셀 값 묶음 · 번지 같을 때) · 조합 다음 줄 = 세 줄 뒤 · 같은 기계 한 항목
- 6 공구손료 율 비목 차이(재료비 ↔ 경비) = 참고(`M02_Doc_Match_Value.tool_cost`) — 확정 안 막음
- 7 못 이은 줄 까닭(`M02_San_Link.why` — 자재 없음 · 운임표 없음 · 기계 표에 없음 · 별칭 대기 · 후보 여럿 · 시간당 경비 없음 · 조합) =
  문서 설명 · 이음 없는 자원 띠 · 연결 「없음」 띠 = 「우리 기초 없음 — 우리 정보로 셈 n/m 줄」 · 형제만 = 「조합」
- 8 유가 = 전국평균 줄 + 지역 옵션(`MO:경유:{S_지역}`) · 전력 = 일반용(갑)Ⅰ 저압 + 계약종별 옵션 · 엑셀 값 = 검산 칸
- 9 재료 = 지역 꼬리 뗀 이름 · 규격 정확 일치면 이음(지역 여럿 = 줄 지역 › 서울)
- 10 조합 첫 줄 규격 = 그 기계 · 얼린 줄도 소계 식에(북부청임도 33) · 기계 줄 찾기 판정 늘 붙음 · 올린 직후 409(HP 보이기 전 산근 보관) ·
  엑셀 블록 「엑셀 읽기 확정」 글 · 전수 도우미 str() 뗌 · 줄 쌓기 이름에 규격이 들면 한 번만
- M02_San_Link 새 파일(San_Emit 700 줄 막음 — 요소 잇기 · 시간당 · 조합 · 까닭)
- 시험: 새 test_814_1 · 812-2 · 810-4 거푸집 41 = 근접 → 확정(공구손료 비목 = 값 · 요구 744 C1) · 바꾸기 거절 견본 = 19 ·
  812-9 조합 첫 줄 규격 「0.7+브레이카」 → 「0.7」(칩 글 · 요구 738 흠 12)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:47:08 +09:00

168 lines
7.5 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Hopyo_ImportedPanel.ts
* 가져온 식 단가산출 모달 머리 판(PLAN 793-3b · 요구 646 · 647 · 652) — `Hopyo_Imported` 가 엶 · 시트(`Sangeun_Detail`) 위.
* · 줄 1 = 「새」 · 원본(파일 · 시트 · 행) · 검산 표지(같음 / 다름 n · 줄마다 까닭) · 단가 기준 [우리 단가로] / [엑셀 단가로] ·
* 바꾸기 전후 금액 · [원본 글 보기]
* · 줄 2 = 이음 없는 자원 = 표지 + [이음 고르기]
* · 줄 2-2 = 소요량 · 계수 이음(797-2 · 후보 집계 + [이음 보기] → `Hopyo_QtyLinks` 모달)
* · 경쟁사 로직 그대로 — 기초로 바꾸기 · 근거 기초 제안 없음(809-1 · 요구 703)
* 부품 = 단추 · 배지 · 글 · 입력 · 띠 · 표 · 모달(날 단추 · 입력 없음) · 서버 길은 부르는 쪽.
* ========================================================================== */
import { createBadge } from "@ui/ui_template_badge";
import { createButton } from "@ui/ui_template_button";
import { el } from "@ui/ui_template_elements";
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { openModal } from "@ui/ui_template_modal";
import { createTable } from "@ui/ui_template_table";
import { createText } from "@ui/ui_template_text";
import { createToolbar } from "@ui/ui_template_toolbar";
import { formatMoney } from "../common_util/common_util_money";
import { tallyBadges, type QtyTally } from "./M02_MasterTemplete_Hopyo_QtyLinks";
import {
gapText,
isImported,
wrongLines,
type ImportedDoc,
type Money4,
} from "./logic/M02_Hopyo_Imported_Model";
const TEXT = {
Imported: ["가져온 식", "Imported formula"],
Same: ["검산 같음", "Check matches"],
Diff: ["검산 다름 {n}", "Check differs {n}"],
NoCheck: ["검산 없음", "No check"],
Basis: ["단가 기준", "Price basis"],
ToOurs: ["우리 단가로", "Use our prices"],
ToExcel: ["엑셀 단가로", "Use Excel prices"],
Source: ["원본 글 보기", "Show source text"],
SourceTitle: ["원본 글", "Source text"],
Row: ["행", "Row"],
Text: ["글", "Text"],
Excel: ["엑셀", "Excel"],
Now: ["지금", "Now"],
Unlinked: ["이음 없는 자원 {n}", "Unlinked resources {n}"],
Link: ["이음 고르기", "Pick link"],
Gap: ["바꾸기 전 {a} → 후 {b} ({d})", "Before {a} → after {b} ({d})"],
QtyLinks: ["소요량 · 계수 이음 {n}", "Quantity / coefficient links {n}"],
QtyOpen: ["이음 보기", "Show links"],
Close: ["닫기", "Close"],
} as const satisfies Record<string, readonly [string, string]>;
export const tx = (k: keyof typeof TEXT): string =>
TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0];
export interface PanelState {
doc: ImportedDoc;
/** 이음 없는 자원(서버 price 답) */
unlinked: { 자리: string; 이름: string; 까닭?: string }[];
/** 소요량 · 계수 이음 집계(서버 qty 답 · 797-2) */
qty: QtyTally | null;
/** 마지막 단가 바꿈 전후 */
price: { 전: Money4 | null; 후: Money4 | null } | null;
busy: boolean;
readOnly: boolean;
}
export interface PanelActs {
price: (basis: "엑셀" | "우리") => void;
link: (spot: string, name: string) => void;
qty: () => void;
}
const won = (m: Money4 | null | undefined): string => (m ? formatMoney(m.계) : "—");
const muted = (s: string): HTMLElement => createText(s, "muted", 14);
/** [원본 글 보기] — 엑셀 글줄 · 엑셀 값 · 지금 셈 */
function showSource(doc: ImportedDoc): void {
const src = doc.가져온?.원본;
const lines = new Map((doc.가져온?.검산?.줄 ?? []).map((x) => [x.행, x]));
const rows = (src?.글줄 ?? []).map((g) => {
const hit = g.행 != null ? lines.get(g.행) : undefined;
return [
String(g.행 ?? ""),
g.글 ?? "",
g.값 == null ? "" : formatMoney(g.값),
hit?.셈 == null ? "" : formatMoney(hit.셈),
hit ? createBadge(hit.같음 ? "=" : (hit.까닭 ?? "≠"), hit.같음 ? "success" : "danger") : "",
];
});
openModal({
title: `${tx("SourceTitle")} — ${[src?.파일, src?.시트].filter(Boolean).join(" · ")}`,
closeLabel: tx("Close"),
wide: true,
dialogClass: "m02-modal m02-hopyo__src",
mount: (body) => {
const heads = [tx("Row"), tx("Text"), tx("Excel"), tx("Now"), ""];
const t = createTable(heads.map((h, i) => ({ label: h, ...(i > 1 && i < 4 ? { align: "end" as const } : {}) })), rows, { density: "data" }); // prettier-ignore
t.dataset.importedSource = "";
body.append(t);
},
});
}
function checkBadge(doc: ImportedDoc): HTMLElement {
const chk = doc.가져온?.검산;
const bad = wrongLines(doc);
const b = !chk
? createBadge(tx("NoCheck"), "neutral")
: chk.같음
? createBadge(tx("Same"), "success")
: createBadge(tx("Diff").replace("{n}", String(bad.length || 1)), "danger");
b.dataset.importedCheck = chk ? String(chk.같음) : "";
b.title = [chk?.까닭, ...bad.map((x) => `${tx("Row")} ${x.행 ?? "?"}: ${x.까닭 ?? ""}`)]
.filter(Boolean)
.join("\n");
return b;
}
/** 머리 판 */
export function importedPanel(s: PanelState, a: PanelActs): HTMLElement {
const meta = s.doc.가져온 ?? {};
const src = meta.원본;
const here = isImported(s.doc);
const head: (HTMLElement | "divider")[] = [
createBadge(tx("Imported"), "accent"),
muted([src?.파일, src?.시트, src?.행?.join("~")].filter(Boolean).join(" · ")),
checkBadge(s.doc),
];
if (here) {
const ours = meta.단가기준 === "우리";
const flip = createButton({
label: ours ? tx("ToExcel") : tx("ToOurs"),
size: 28,
variant: "ghost",
disabled: s.readOnly || s.busy,
onClick: () => a.price(ours ? "엑셀" : "우리"),
});
flip.dataset.importedPrice = ours ? "엑셀" : "우리";
head.push("divider", muted(`${tx("Basis")} ${meta.단가기준 ?? "엑셀"}`), flip);
if (s.price) {
const g = tx("Gap").replace("{a}", won(s.price.전)).replace("{b}", won(s.price.후));
const t = muted(g.replace("{d}", gapText(s.price.전, s.price.후)));
t.dataset.importedGap = "";
head.push(t);
}
}
const srcBtn = createButton({ label: tx("Source"), size: 28, variant: "ghost", disabled: !src?.글줄?.length, onClick: () => showSource(s.doc) }); // prettier-ignore
srcBtn.dataset.importedSourceBtn = "";
head.push("divider", srcBtn);
const kids: HTMLElement[] = [createToolbar({ items: head })];
if (here && s.unlinked.length) {
const items = s.unlinked.map((u) => {
const label = `${u.이름}${u.까닭 ? ` (${u.까닭})` : ""} · ${tx("Link")}`; // 못 이은 까닭(814-1)
const b = createButton({ label, size: 24, variant: "ghost", disabled: s.readOnly || s.busy, onClick: () => a.link(u.자리, u.이름) }); // prettier-ignore
b.dataset.importedLink = u.자리;
return b;
});
const tag = createBadge(tx("Unlinked").replace("{n}", String(s.unlinked.length)), "warning");
kids.push(createToolbar({ items: [tag, ...items] }));
}
if (here && s.qty?.자리) {
const open = createButton({ label: tx("QtyOpen"), size: 24, variant: "ghost", onClick: a.qty });
open.dataset.importedQty = String(s.qty.자리);
const tag = createBadge(tx("QtyLinks").replace("{n}", String(s.qty.자리)), "info");
kids.push(createToolbar({ items: [tag, ...tallyBadges(s.qty), open] }));
}
return el("div", { className: "m02-std-sheet__body", attrs: { "data-imported-panel": "" }, children: kids }); // prettier-ignore
}