- to-base 몸 환산? · 환산 없이 단위 성격 다름 = 후 None · 필요 ["환산"](막지 않음 · 화면 붉은 칸) - 화면 bind = 블록 풀이 그 기초 후보 바인딩(엑셀 값 · 기본) + 블록 검산 입력 필요 · 줄 검산(빈 입력값)은 옵션을 못 읽었음 - 가져온 식 SK = M01 짝 없음 — 시트 M01바뀜 · 짝잃음 안 띄움(저장 M01판이 바탕 줄과 달라 늘 떴음) - 시험 test_793_3b 8 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
285 lines
11 KiB
TypeScript
285 lines
11 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Hopyo_Imported.ts
|
|
* 불러오기 단산 줄 [가져온 식으로 열기] 모달(PLAN 793-3b · 요구 646 · 647 · 652 갈래 C) —
|
|
* [수정] 모달 단산 줄(`Hopyo_Check`) · 오른쪽 목록 단가산출 탭 블록(`Hopyo_Block`)이 엶.
|
|
* · 열기 = 서버 `…/sangeun/from-hopyo`(그 블록 → 가져온 식 SK · 만들 층 · 엑셀 단가 그대로 · 같은 내용 = 그 SK 다시 씀)
|
|
* → 그 SK 를 읽어 우리 단가산출 화면(`Sangeun_Detail` 시트 · 고침 = 시트 그대로) + 머리 판(`Hopyo_ImportedPanel`)
|
|
* · [우리 단가로] · [엑셀 단가로] · [이음 고르기] · [우리 기초로 바꾸기] = 서버 `…/sangeun/imported/{price|link|to-base}`
|
|
* (화면 캐시 문서 고침 · 저장 없음) → 시트 다시 엶 · 기초로 바꿀 때 옵션 = 블록 풀이 그 기초 후보 바인딩(엑셀 값 · 기본) →
|
|
* 블록 검산으로 입력 필요 · 단위 성격이 다르면 「환산 계수」도 입력 필요
|
|
* · [저장] = 그 SK 저장(서버가 다시 셈) · [이 줄에 잇기] = 저장 뒤 단산 줄 고른 것 = 그 SK(갈래 단가산출 · 키) —
|
|
* 확인 · [사전조합 생성]은 기존 길(자동 확인 · 옮김 없음) · 입력 필요가 비면 그 줄 확인 막힘
|
|
* ========================================================================== */
|
|
|
|
import { createButton } from "@ui/ui_template_button";
|
|
import { el, showToast } from "@ui/ui_template_elements";
|
|
import { loadingError, showLoading } from "@ui/ui_template_loading";
|
|
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
|
import { askConfirm, openModal } from "@ui/ui_template_modal";
|
|
import { call, readTemplate, saveTemplate } from "./M02_MasterTemplete_Api_Fetch";
|
|
import { openFindPicker } from "./M02_MasterTemplete_Find_Pick";
|
|
import { at, type HopyoScope } from "./M02_MasterTemplete_Hopyo_Api";
|
|
import { checkJudge, readJudgeBlock } from "./M02_MasterTemplete_Hopyo_JudgeApi";
|
|
import { importedPanel, type PanelState } from "./M02_MasterTemplete_Hopyo_ImportedPanel";
|
|
import { mountSangeunDetail } from "./M02_MasterTemplete_Sangeun_Detail";
|
|
import {
|
|
marksOf,
|
|
skPick,
|
|
type BasisCand,
|
|
type ImportedDoc,
|
|
type Money4,
|
|
type Tier,
|
|
} from "./logic/M02_Hopyo_Imported_Model";
|
|
import { bare, type DraftPick } from "./logic/M02_MasterTemplete_Hopyo_Picks";
|
|
|
|
const TEXT = {
|
|
Title: ["가져온 식 단가산출 — 단산{n}", "Imported build-up — block {n}"],
|
|
Reused: ["같은 내용 {k} 를 다시 씀", "Reusing same content {k}"],
|
|
Made: ["{k} 만듦(엑셀 단가 그대로)", "{k} made (Excel prices)"],
|
|
Save: ["저장", "Save"],
|
|
Saved: ["{k} 저장함", "{k} saved"],
|
|
Link: ["이 줄에 잇기", "Use for this line"],
|
|
Linked: ["단산 줄 = {k} — [확인] 뒤 [사전조합 생성]", "Line uses {k} — confirm, then build"],
|
|
Discard: ["저장 안 한 고침이 있음 — 버리고 닫을까?", "Discard unsaved edits?"],
|
|
Close: ["닫기", "Close"],
|
|
} as const satisfies Record<string, readonly [string, string]>;
|
|
const t = (k: keyof typeof TEXT): string => TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0];
|
|
const why = (e: unknown): string => (e instanceof Error ? e.message : String(e));
|
|
|
|
export interface ImportedOptions {
|
|
/** 실무자료 파일 층 */
|
|
file: HopyoScope;
|
|
key: string;
|
|
/** 단산 블록 번 */
|
|
no: number;
|
|
/** SK 를 만들 층 */
|
|
target: HopyoScope;
|
|
canWrite: boolean;
|
|
/** 단산 줄에서 열었을 때만 — [이 줄에 잇기] */
|
|
onLink?: (pick: DraftPick) => void;
|
|
}
|
|
|
|
interface Made {
|
|
이름: string;
|
|
다시씀: boolean;
|
|
}
|
|
|
|
const fromHopyo = (o: ImportedOptions): Promise<Made> =>
|
|
call(at(o.target, "/sangeun/from-hopyo"), {
|
|
method: "POST",
|
|
body: JSON.stringify({ 층: o.file.layer, ...(o.file.owner ? { 주인: o.file.owner } : {}), 키: o.key, 번: o.no }), // prettier-ignore
|
|
});
|
|
|
|
/** 기초 옵션 — 블록 풀이 그 기초 후보 바인딩(엑셀 값 · 기본 · 근거 옵션) → 블록 검산 입력 필요 + 엇갈림 */
|
|
async function bind(
|
|
o: ImportedOptions,
|
|
code: string,
|
|
given: Record<string, unknown>,
|
|
): Promise<{ 입력값: Record<string, unknown>; 필요: string[] }> {
|
|
const blk = await readJudgeBlock(o.file, o.key, o.no, o.target).catch(() => null);
|
|
const b = blk?.절.flatMap((s) => s.후보).find((c) => c.키 === code)?.바인딩;
|
|
const values = { ...(b?.기본 ?? {}), ...(b?.입력값 ?? {}), ...given };
|
|
const pick = bare({ 갈래: "기초", 키: code, 이름: code, 입력값: values, 출처: {}, 몫: null });
|
|
const res = await checkJudge(o.file, o.key, { 블록: o.no, 고른: [pick] });
|
|
const need = [...(res.고른[0]?.입력필요 ?? []), ...Object.keys(b?.엇갈림 ?? {})];
|
|
return { 입력값: values, 필요: [...new Set(need)] };
|
|
}
|
|
|
|
interface EditAnswer {
|
|
문서: ImportedDoc;
|
|
필요?: string[];
|
|
전?: Money4 | null;
|
|
후?: Money4 | null;
|
|
이음없음?: { 자리: string; 이름: string }[];
|
|
}
|
|
|
|
export function openImported(o: ImportedOptions): void {
|
|
const readOnly = !o.canWrite;
|
|
const root = el("div", {
|
|
className: "m02-hopyo",
|
|
attrs: { "data-imported": `${o.key}|${o.no}` },
|
|
});
|
|
const top = el("div");
|
|
const host = el("div", { attrs: { "data-sk-body": "" } });
|
|
let sk = "";
|
|
let 판 = "";
|
|
let tier: Tier | null = null;
|
|
let dirty = false;
|
|
let close = (): void => {};
|
|
let handle: ReturnType<typeof mountSangeunDetail> | null = null;
|
|
const st: PanelState = { doc: {}, unlinked: [], price: null, need: [], busy: false, readOnly };
|
|
|
|
const edit = (how: string, body: Record<string, unknown>): Promise<EditAnswer> =>
|
|
call(at(o.target, `/sangeun/imported/${how}`), { method: "POST", body: JSON.stringify(body) });
|
|
|
|
function mount(doc: ImportedDoc): void {
|
|
handle?.destroy();
|
|
st.doc = doc;
|
|
handle = mountSangeunDetail(host, doc, {
|
|
layer: o.target.layer,
|
|
owner: o.target.owner,
|
|
readOnly,
|
|
onChanged: (d) => (dirty ||= d),
|
|
});
|
|
draw();
|
|
}
|
|
const now = (): ImportedDoc => (handle?.getDoc() as ImportedDoc | undefined) ?? st.doc;
|
|
|
|
function draw(): void {
|
|
top.replaceChildren(importedPanel(st, { price, link, toBase, pickBase, fill }));
|
|
saveBtn.disabled = readOnly || st.busy;
|
|
linkBtn.disabled = readOnly || st.busy;
|
|
}
|
|
|
|
/** 서버 고침 한 번(눌림 막음) → 시트 다시 엶 */
|
|
async function run(how: string, body: Record<string, unknown>): Promise<EditAnswer | null> {
|
|
st.busy = true;
|
|
draw();
|
|
try {
|
|
const got = await edit(how, { 문서: now(), ...body });
|
|
dirty = true;
|
|
mount(got.문서);
|
|
return got;
|
|
} catch (e) {
|
|
showToast(why(e), "error");
|
|
return null;
|
|
} finally {
|
|
st.busy = false;
|
|
draw();
|
|
}
|
|
}
|
|
|
|
/** 이음 없는 자원 — 단가 기준 그대로 price 답(전 · 후 = 지금) */
|
|
async function unlinked(): Promise<void> {
|
|
if (!st.doc.가져온?.로직) return void (st.unlinked = []);
|
|
const got = await edit("price", { 문서: st.doc, 기준: st.doc.가져온.단가기준 ?? "엑셀" }).catch(() => null); // prettier-ignore
|
|
st.unlinked = got?.이음없음 ?? [];
|
|
draw();
|
|
}
|
|
|
|
async function price(basis: "엑셀" | "우리"): Promise<void> {
|
|
const got = await run("price", { 기준: basis });
|
|
if (got) st.price = { 전: got.전 ?? null, 후: got.후 ?? null };
|
|
draw();
|
|
}
|
|
|
|
function link(spot: string, name: string): void {
|
|
const done = (code: string): void => void run("link", { 자리: spot, 이음: code }).then(() => unlinked()); // prettier-ignore
|
|
openFindPicker({
|
|
layer: o.target.layer,
|
|
owner: o.target.owner,
|
|
query: name,
|
|
logic: (x) => done(`로직(${x.키})`),
|
|
element: (e) => done(e.코드),
|
|
machine: (e) => done(e.코드),
|
|
oil: (e) => done(e.코드),
|
|
});
|
|
}
|
|
|
|
async function toBase(code: string, cand: BasisCand | null): Promise<void> {
|
|
st.busy = true;
|
|
draw();
|
|
let bound: Awaited<ReturnType<typeof bind>>;
|
|
try {
|
|
bound = await bind(o, code, cand?.옵션 ?? {});
|
|
} catch (e) {
|
|
st.busy = false;
|
|
draw();
|
|
return void showToast(why(e), "error");
|
|
}
|
|
st.busy = false;
|
|
const got = await run("to-base", { 로직: code, 입력값: bound.입력값 });
|
|
if (!got) return;
|
|
tier = cand?.단 ?? null;
|
|
st.need = [...bound.필요, ...(got.필요 ?? [])];
|
|
st.unlinked = [];
|
|
draw();
|
|
}
|
|
|
|
function pickBase(): void {
|
|
openFindPicker({ layer: o.target.layer, owner: o.target.owner, kinds: ["logic"], logic: (x) => void toBase(x.키, null) }); // prettier-ignore
|
|
}
|
|
|
|
/** 입력 필요 칸 — 옵션 = SK 입력값 · 「환산」 = SK 환산 계수 */
|
|
function fill(name: string, value: string): void {
|
|
const doc = now();
|
|
const v = value.trim();
|
|
dirty = true;
|
|
if (name === "환산") {
|
|
const { 환산: _old, ...rest } = doc;
|
|
return mount(v && Number(v) > 0 ? { ...rest, 환산: { 계수: Number(v) } } : rest);
|
|
}
|
|
const kept = { ...(doc.입력값 ?? {}) };
|
|
if (v) kept[name] = v;
|
|
else delete kept[name];
|
|
mount({ ...doc, 입력값: kept });
|
|
}
|
|
|
|
async function save(): Promise<boolean> {
|
|
try {
|
|
const doc = now();
|
|
const info = await saveTemplate(o.target.layer, "sangeun", sk, null, 판, doc, o.target.owner);
|
|
판 = info.판;
|
|
st.doc = doc;
|
|
dirty = false;
|
|
showToast(t("Saved").replace("{k}", sk), "success");
|
|
return true;
|
|
} catch (e) {
|
|
showToast(why(e), "error");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const saveBtn = createButton({ label: t("Save"), variant: "filled", onClick: () => void save() });
|
|
saveBtn.dataset.importedSave = "";
|
|
const linkBtn = createButton({
|
|
label: t("Link"),
|
|
variant: "ghost",
|
|
onClick: async () => {
|
|
if (dirty && !(await save())) return;
|
|
o.onLink!(skPick(sk, st.doc, o.target, st.need, marksOf(st.doc, tier)));
|
|
showToast(t("Linked").replace("{k}", sk), "info");
|
|
dirty = false;
|
|
close();
|
|
},
|
|
});
|
|
linkBtn.dataset.importedLinkRow = "";
|
|
linkBtn.hidden = !o.onLink;
|
|
saveBtn.disabled = linkBtn.disabled = true;
|
|
|
|
async function load(): Promise<void> {
|
|
const stop = showLoading(root);
|
|
try {
|
|
const made = await fromHopyo(o);
|
|
sk = made.이름;
|
|
showToast((made.다시씀 ? t("Reused") : t("Made")).replace("{k}", sk), "info");
|
|
const got = await readTemplate(o.target.layer, "sangeun", sk, null, o.target.owner);
|
|
판 = got.판 ?? "";
|
|
root.replaceChildren(top, host);
|
|
mount(got.문서 as ImportedDoc);
|
|
void unlinked();
|
|
} catch (e) {
|
|
root.replaceChildren(loadingError(why(e), () => void load()));
|
|
} finally {
|
|
stop();
|
|
}
|
|
}
|
|
|
|
({ close } = openModal({
|
|
title: t("Title").replace("{n}", String(o.no)),
|
|
closeLabel: t("Close"),
|
|
width: "1100px",
|
|
wide: true,
|
|
dialogClass: "m02-modal m02-hopyo__imported-modal",
|
|
actions: [saveBtn, linkBtn],
|
|
beforeClose: async () => {
|
|
if (dirty && !readOnly && !(await askConfirm({ message: t("Discard"), danger: true })))
|
|
return false;
|
|
handle?.destroy();
|
|
return true;
|
|
},
|
|
mount: (body) => body.append(root),
|
|
}));
|
|
void load();
|
|
}
|