- 자동 저장 취소(요구 852-1) — 가져온 식 열기 = 미리 보기 `…/sangeun/from-hopyo/preview`(층 · 재료 고름 안 씀 · 같은 내용 SK 있으면 그것을 읽음) · [저장] = `from-hopyo` 몸 `문서`(고친 문서째 · 안쪽 산근 참조 · 내 기계 그때) · 옛 길: 단가산출표 목록 [내 단가산출로] · [수정] 모달 단산 줄 누름이 서버 make 로 SK 를 바로 만들었음 (`FromHopyo.make` 를 `_prep` · `draft` · `make(doc)` 로 · `Imported.new_doc` · `keep` · `draft` · `MatPick.merge`) - 산출근거 표 비목 줄(노무비 : … = 금액) = 합계 · 그 비목 칸에 금액(엑셀 단가산출 시트와 같음 · 852-2 · `line_stack_out`) - 단가산출 상세 시트 블록 안 줄 이어짐 초록 안 칠함 — 출처 행 = 인력 · 기계 노무비 행(852-3 · 다른 칠 · 머리 행 그대로) - 단가산출 목록 [내 단가산출로] 걷음 → 줄 고르면 개인 층 가져온 식 모달(852-4) - 일위대가표 목록 [내 일위대가로] 걷음 → 줄 고르면 [수정] 모달(852-5 · 모달 머리 [내 일위대가로] 그대로) - 부품 상자 모달 큰 중간 창 `1400px`(1400 × 880) — 불러오기 [수정] · 가져온 식 모달(852-6) - 시험: 새 test_835_1(미리 보기 쓰기 0 · [저장] = 옛 길과 같은 SK) · 낡은 시험 고침 — test_627(저장 단추 걷음 · MatPick 가짜 없어 import.meta 로 죽던 것) · test_779_5(이어짐 초록 안 칠함) · test_793_3b · test_809_1(수정 모달 단산 줄 = 가져온 식 모달) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
277 lines
11 KiB
TypeScript
277 lines
11 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Hopyo_Imported.ts
|
|
* 불러오기 단산 블록 [가져온 식으로 열기] 모달(PLAN 793-3b · 809-1 · 요구 646 · 702) —
|
|
* 엑셀 보기 단가산출 탭 목록(`Hopyo_SheetSk`)이 엶 · 경쟁사 로직 그대로(기초로 바꾸기 없음 · 요구 703).
|
|
* · 열기 = 미리 보기(835-1 · 요구 852-1 — 층에 안 씀) 서버 `…/sangeun/from-hopyo/preview`(그 블록 → 가져온 식 문서 ·
|
|
* 엑셀 단가 그대로 · 같은 내용 SK 가 있으면 그 SK 를 읽음) → 우리 단가산출 화면(`Sangeun_Detail` 시트 · 고침 = 시트 그대로)
|
|
* + 머리 판(`Hopyo_ImportedPanel`)
|
|
* · [우리 단가로] · [엑셀 단가로] · [이음 고르기] = 서버 `…/sangeun/imported/{price|link}`
|
|
* (화면 캐시 문서 고침 · 저장 없음) → 시트 다시 엶
|
|
* · [이음 보기](797-2) = 소요량 · 계수 후보 모달(`Hopyo_QtyLinks`) — 고르기 · 버리기 = 서버 `…/imported/{qty|qty-pick|qty-drop}`(기록만 · 금액 그대로)
|
|
* · [저장] = 아직 없으면 `…/sangeun/from-hopyo` 몸 `문서`(고친 문서째 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 { askSameFile } from "../A00_Common/line_stack_material";
|
|
import { at, type HopyoScope } from "./M02_MasterTemplete_Hopyo_Api";
|
|
import { importedPanel, type PanelState } from "./M02_MasterTemplete_Hopyo_ImportedPanel";
|
|
import { openQtyLinks, type QtyRow, type QtyTally } from "./M02_MasterTemplete_Hopyo_QtyLinks";
|
|
import { mountSangeunDetail } from "./M02_MasterTemplete_Sangeun_Detail";
|
|
import { type ImportedDoc, type Money4 } from "./logic/M02_Hopyo_Imported_Model";
|
|
|
|
const TEXT = {
|
|
Title: ["가져온 식 단가산출 — 단산{n}", "Imported build-up — block {n}"],
|
|
Reused: ["같은 내용 {k} 를 다시 씀", "Reusing same content {k}"],
|
|
Made: ["{k} 만듦(엑셀 단가 그대로)", "{k} made (Excel prices)"],
|
|
Have: ["같은 내용 {k} 가 이미 있음 — 그것을 엶", "Same content {k} exists — opened"],
|
|
Save: ["저장", "Save"],
|
|
Saved: ["{k} 저장함", "{k} saved"],
|
|
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;
|
|
/** 827-3 저장 안 한 재료 고름(만들 때 서버가 저장 · 그 고름째 만듦) */
|
|
picks?: Record<string, unknown>;
|
|
/** 만들었음(재료 판 다시 읽기) */
|
|
onMade?: () => void;
|
|
}
|
|
|
|
interface Made {
|
|
이름: string;
|
|
다시씀: boolean;
|
|
}
|
|
|
|
/** 미리 보기 답(835-1) — 같은것 = 같은 내용 SK 키(있으면 그것을 엶) */
|
|
interface Draft {
|
|
문서: ImportedDoc;
|
|
같은것: string | null;
|
|
}
|
|
|
|
/** 서버 몸 — 파일 층 · HP · 블록 · 저장 안 한 재료 고름 · `문서` = [저장] 때 고친 문서 */
|
|
const fromBody = (o: ImportedOptions, doc?: ImportedDoc): string =>
|
|
JSON.stringify({ 층: o.file.layer, ...(o.file.owner ? { 주인: o.file.owner } : {}), 키: o.key, 번: o.no, ...(o.picks ? { 고름: o.picks } : {}), ...(doc ? { 문서: doc } : {}) }); // prettier-ignore
|
|
|
|
/** 보기(쓰기 없음) */
|
|
export const previewImported = (o: ImportedOptions): Promise<Draft> =>
|
|
call(at(o.target, "/sangeun/from-hopyo/preview"), { method: "POST", body: fromBody(o) });
|
|
|
|
/** [저장] 처음 — 고친 문서째 SK 만듦 */
|
|
const fromHopyo = (o: ImportedOptions, doc: ImportedDoc): Promise<Made> =>
|
|
call(at(o.target, "/sangeun/from-hopyo"), { method: "POST", body: fromBody(o, doc) });
|
|
|
|
interface EditAnswer {
|
|
문서: ImportedDoc;
|
|
전?: Money4 | null;
|
|
후?: Money4 | null;
|
|
이음없음?: { 자리: string; 이름: string; 까닭?: string; 견적?: boolean; 종?: string }[];
|
|
/** qty 답(797-2) */
|
|
줄?: QtyRow[];
|
|
집계?: QtyTally;
|
|
}
|
|
|
|
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 dirty = false;
|
|
let handle: ReturnType<typeof mountSangeunDetail> | null = null;
|
|
const st: PanelState = { doc: {}, unlinked: [], qty: null, price: null, busy: false, readOnly }; // prettier-ignore
|
|
|
|
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, qty: openQty }));
|
|
saveBtn.disabled = readOnly || st.busy;
|
|
}
|
|
|
|
/** 서버 고침 한 번(눌림 막음) → 시트 다시 엶 */
|
|
async function run(
|
|
how: string,
|
|
body: Record<string, unknown>,
|
|
from: ImportedDoc = now(),
|
|
): Promise<EditAnswer | null> {
|
|
st.busy = true;
|
|
draw();
|
|
try {
|
|
const got = await edit(how, { 문서: from, ...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();
|
|
}
|
|
|
|
/** 소요량 · 계수 이음 집계(797-2) — 가져온 식일 때만 */
|
|
async function qtyTally(): Promise<void> {
|
|
if (!st.doc.가져온?.로직) return void (st.qty = null);
|
|
const got = await edit("qty", { 문서: st.doc }).catch(() => null);
|
|
st.qty = got?.집계 ?? null;
|
|
draw();
|
|
}
|
|
|
|
function openQty(): void {
|
|
openQtyLinks(
|
|
{
|
|
view: async () => {
|
|
const got = await edit("qty", { 문서: now() });
|
|
return { 줄: got.줄 ?? [], 집계: got.집계! };
|
|
},
|
|
pick: async (spot, c) => {
|
|
await run("qty-pick", { 자리: spot, 칸: { 표: c.표, 줄: c.줄, 칸: c.칸 } });
|
|
await qtyTally();
|
|
},
|
|
drop: async (spot, undo) => {
|
|
await run("qty-drop", { 자리: spot, 되돌리기: undo });
|
|
await qtyTally();
|
|
},
|
|
},
|
|
readOnly,
|
|
);
|
|
}
|
|
|
|
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
|
|
const kind = st.unlinked.find((u) => u.자리 === spot)?.종; // 827-3 재료 줄 = 같은 파일 물음
|
|
const src = (st.doc.가져온 as { 원본?: { 지문?: string; 파일?: string } } | undefined)?.원본;
|
|
const same = (e: { 코드: string; 이름: string; 규격?: string; 단위?: string }): void => {
|
|
if (kind && src?.지문 && !readOnly)
|
|
void askSameFile({ scope: o.target, me: sk, 지문: src.지문, 파일: src.파일 ?? "", 종: kind, 원래: name, p: { ...e, 키: e.코드 } }); // prettier-ignore
|
|
};
|
|
openFindPicker({
|
|
layer: o.target.layer,
|
|
owner: o.target.owner,
|
|
query: name,
|
|
logic: (x) => done(`로직(${x.키})`),
|
|
element: (e) => {
|
|
done(e.코드);
|
|
same(e);
|
|
},
|
|
machine: (e) => done(e.코드),
|
|
oil: (e) => done(e.코드),
|
|
});
|
|
}
|
|
|
|
async function save(): Promise<boolean> {
|
|
try {
|
|
const doc = now();
|
|
if (!sk) {
|
|
const made = await fromHopyo(o, doc); // 처음 [저장] = 만듦(835-1)
|
|
o.onMade?.();
|
|
sk = made.이름;
|
|
const got = await readTemplate(o.target.layer, "sangeun", sk, null, o.target.owner);
|
|
판 = got.판 ?? "";
|
|
mount(got.문서 as ImportedDoc);
|
|
dirty = false;
|
|
showToast((made.다시씀 ? t("Reused") : t("Made")).replace("{k}", sk), "success");
|
|
return true;
|
|
}
|
|
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 = "";
|
|
saveBtn.disabled = true;
|
|
|
|
async function load(): Promise<void> {
|
|
const stop = showLoading(root);
|
|
try {
|
|
const got = await previewImported(o); // 보기 = 쓰기 없음(835-1)
|
|
sk = got.같은것 ?? "";
|
|
const have = sk ? await readTemplate(o.target.layer, "sangeun", sk, null, o.target.owner) : null; // prettier-ignore
|
|
판 = have?.판 ?? "";
|
|
if (sk) showToast(t("Have").replace("{k}", sk), "info");
|
|
root.replaceChildren(top, host);
|
|
mount((have?.문서 ?? got.문서) as ImportedDoc);
|
|
void unlinked();
|
|
void qtyTally();
|
|
} catch (e) {
|
|
root.replaceChildren(loadingError(why(e), () => void load()));
|
|
} finally {
|
|
stop();
|
|
}
|
|
}
|
|
|
|
openModal({
|
|
title: t("Title").replace("{n}", String(o.no)),
|
|
closeLabel: t("Close"),
|
|
width: "1400px",
|
|
wide: true,
|
|
dialogClass: "m02-modal m02-hopyo__imported-modal",
|
|
actions: [saveBtn],
|
|
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();
|
|
}
|