feat(793-3b): 불러오기 단산 줄 = 가져온 식 단가산출 모달 — 블록 → 가져온 식 SK 길 · 검산 · 엑셀/우리 단가 · 이음 고르기 · 근거 세 단 · 우리 기초로 바꾸기 · 입력 필요 · 줄 잇기 · 결과 칩
- 서버: 올림 때 산근 시트 둠(실무자료/산근/<HP>.json · HP 지울 때 같이) · POST …/sangeun/from-hopyo(블록 → 793-2a 내보내기 → 793-2 만들기 · 같은 내용 = 그 SK 다시 씀 · 옛 HP 409) · STmate 끝전 = 엑셀 계와 맞는 쪽 - 화면: [수정] 모달 단산 줄 · 오른쪽 블록 판 [가져온 식으로 열기](새) → 시트 모달 + 머리 판 · [이 줄에 잇기] = 줄 고른 것 SK(입력 필요 남으면 확인 막힘) · 단산 줄 결과 칩 - 시험: test_793_3b_import_screen 6 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_ImportedPanel.ts
|
||||
* 가져온 식 단가산출 모달 머리 판(PLAN 793-3b · 요구 646 · 647 · 652) — `Hopyo_Imported` 가 엶 · 시트(`Sangeun_Detail`) 위.
|
||||
* · 줄 1 = 「새」 · 원본(파일 · 시트 · 행) · 검산 표지(같음 / 다름 n · 줄마다 까닭) · 단가 기준 [우리 단가로] / [엑셀 단가로] ·
|
||||
* 바꾸기 전후 금액 · [원본 글 보기]
|
||||
* · 줄 2 = 이음 없는 자원 = 표지 + [이음 고르기]
|
||||
* · 줄 3 = 근거 제안 세 단(확정 · 근접(차이 · 까닭) · 사람 고름 · 793-3a) — 후보마다 [우리 기초로 바꾸기] · [기초 고르기]
|
||||
* · 우리 기초로 바꾼 뒤 = 전후 차이 띠 · 「입력 필요」 붉은 칸(채우기 전엔 그 줄 확인 · 옮김 막힘)
|
||||
* 부품 = 단추 · 배지 · 글 · 입력 · 띠 · 표 · 모달(날 단추 · 입력 없음) · 서버 길은 부르는 쪽.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import { createButton } from "@ui/ui_template_button";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { createInputField } from "@ui/ui_template_input";
|
||||
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 { newBadge } from "./M02_MasterTemplete_Hopyo_CheckRows";
|
||||
import {
|
||||
gapText,
|
||||
isImported,
|
||||
stillNeed,
|
||||
tiers,
|
||||
TIERS,
|
||||
wrongLines,
|
||||
type BasisCand,
|
||||
type ImportedDoc,
|
||||
type Money4,
|
||||
type Tier,
|
||||
} from "./logic/M02_Hopyo_Imported_Model";
|
||||
|
||||
const TEXT = {
|
||||
Imported: ["가져온 식", "Imported formula"],
|
||||
Based: ["우리 기초로 바꿈", "Converted to our basis"],
|
||||
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"],
|
||||
Basis3: ["근거 제안", "Basis suggestions"],
|
||||
확정: ["확정", "Confirmed"],
|
||||
근접: ["근접", "Near"],
|
||||
사람: ["사람 고름", "Human pick"],
|
||||
None: ["없음", "None"],
|
||||
NoCand: ["근거 제안 없음 — [기초 고르기]로 고름", "No suggestion — pick a basis"],
|
||||
ToBase: ["우리 기초로 바꾸기", "Convert to our basis"],
|
||||
PickBase: ["기초 고르기", "Pick basis"],
|
||||
Gap: ["바꾸기 전 {a} → 후 {b} ({d})", "Before {a} → after {b} ({d})"],
|
||||
Need: ["입력 필요", "Input needed"],
|
||||
NeedHint: ["채워야 이 줄을 확인 · 옮김", "Fill to confirm and move this line"],
|
||||
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 }[];
|
||||
/** 마지막 단가 바꿈 전후 */
|
||||
price: { 전: Money4 | null; 후: Money4 | null } | null;
|
||||
/** 우리 기초로 바꾼 뒤 입력 필요 칸 */
|
||||
need: string[];
|
||||
busy: boolean;
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
export interface PanelActs {
|
||||
price: (basis: "엑셀" | "우리") => void;
|
||||
link: (spot: string, name: string) => void;
|
||||
toBase: (code: string, cand: BasisCand | null) => void;
|
||||
pickBase: () => void;
|
||||
fill: (name: string, value: string) => 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;
|
||||
}
|
||||
|
||||
function cand(c: BasisCand, s: PanelState, a: PanelActs): HTMLElement {
|
||||
const opts = Object.entries(c.옵션 ?? {})
|
||||
.map(([k, v]) => `${k} ${String(v)}`)
|
||||
.join(" · ");
|
||||
const go = createButton({ label: tx("ToBase"), size: 24, variant: "ghost", disabled: s.readOnly || s.busy, onClick: () => a.toBase(c.기초, c) }); // prettier-ignore
|
||||
go.dataset.importedToBase = c.기초;
|
||||
return el("div", {
|
||||
className: "ui-toolbar m02-judge__wrap",
|
||||
attrs: { "data-basis-cand": c.기초, "data-basis-tier": c.단 },
|
||||
children: [
|
||||
createBadge(`${c.기초}${c.이름 ? ` ${c.이름}` : ""}`, "accent"),
|
||||
...(opts ? [muted(opts)] : []),
|
||||
...(c.차이 !== undefined ? [muted(String(c.차이))] : []),
|
||||
...(c.까닭 ? [muted(c.까닭)] : []),
|
||||
go,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/** 근거 제안 세 단 */
|
||||
function basisPart(s: PanelState, a: PanelActs): HTMLElement {
|
||||
const by = tiers(s.doc.가져온?.근거후보);
|
||||
const any = TIERS.some((k) => by[k].length);
|
||||
const pick = createButton({ label: tx("PickBase"), size: 24, variant: "ghost", disabled: s.readOnly || s.busy, onClick: a.pickBase }); // prettier-ignore
|
||||
pick.dataset.importedPickBase = "";
|
||||
const tierRow = (k: Tier): HTMLElement =>
|
||||
el("div", {
|
||||
attrs: { "data-basis-tiers": k },
|
||||
children: [
|
||||
createBadge(`${tx(k)} ${by[k].length}`, k === "확정" ? "success" : k === "근접" ? "warning" : "neutral"), // prettier-ignore
|
||||
...(by[k].length ? by[k].map((c) => cand(c, s, a)) : [muted(tx("None"))]),
|
||||
],
|
||||
});
|
||||
return el("div", {
|
||||
attrs: { "data-imported-basis": "" },
|
||||
children: [
|
||||
createToolbar({ items: [newBadge(), createText(tx("Basis3"), "muted", 14), pick] }),
|
||||
...(any ? TIERS.map(tierRow) : [muted(tx("NoCand"))]),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/** 머리 판 */
|
||||
export function importedPanel(s: PanelState, a: PanelActs): HTMLElement {
|
||||
const meta = s.doc.가져온 ?? {};
|
||||
const src = meta.원본;
|
||||
const here = isImported(s.doc);
|
||||
const head: (HTMLElement | "divider")[] = [
|
||||
newBadge(),
|
||||
createBadge(here ? tx("Imported") : tx("Based"), here ? "accent" : "info"),
|
||||
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.후) || "0"));
|
||||
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 b = createButton({ label: `${u.이름} · ${tx("Link")}`, 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] }));
|
||||
}
|
||||
const gone = meta.바꿈;
|
||||
if (!here && gone) {
|
||||
const g = tx("Gap").replace("{a}", won(gone.전)).replace("{b}", won(gone.후));
|
||||
kids.push(createBanner({ tone: "info", text: g.replace("{d}", gapText(gone.전, gone.후) || "0") })); // prettier-ignore
|
||||
}
|
||||
const left = stillNeed(s.need, s.doc.입력값);
|
||||
if (s.need.length) {
|
||||
const fields = s.need.map((k) => {
|
||||
const f = createInputField({ label: k, size: 28, value: String(s.doc.입력값?.[k] ?? "") });
|
||||
f.input.dataset.importedNeed = k;
|
||||
f.input.disabled = s.readOnly || s.busy;
|
||||
if (left.includes(k)) f.setError(tx("Need"));
|
||||
f.input.addEventListener("change", () => a.fill(k, f.input.value));
|
||||
return f.root;
|
||||
});
|
||||
kids.push(
|
||||
el("div", {
|
||||
attrs: { "data-imported-needs": String(left.length) },
|
||||
children: [
|
||||
createToolbar({ items: [createBadge(`${tx("Need")} ${left.length}`, left.length ? "danger" : "success"), muted(tx("NeedHint"))] }), // prettier-ignore
|
||||
el("div", { className: "ui-toolbar m02-judge__wrap", children: fields }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
kids.push(basisPart(s, a));
|
||||
return el("div", { className: "m02-std-sheet__body", attrs: { "data-imported-panel": "" }, children: kids }); // prettier-ignore
|
||||
}
|
||||
Reference in New Issue
Block a user