Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Imported.ts
T
eomsangdonandClaude Opus 5.5 c5067aa2b6 feat(809-1a): 일위대가 불러오기 = 경쟁사 로직 인정 · 요소만 연결 · [수정] 모달 = 일위대가 컨테이너 부품 · 기초 판정 걷음
- [수정] 모달 = Labor_Table(읽기 · 덧칸 표지 · 엑셀 금액) · 줄 = 로직(단가산출 · 일위대가) / 요소(연결됨 · 후보 · 못 이음) · 단가산출 줄 = 가져온 식 SK 수식 탭 모달(Labor_SkEdit) · 서버 hopyo-view(M02_Hopyo_View)
- 엑셀 보기 목록 · 칠 = 줄 표지(View.listing) · 단가산출 탭 = 블록 목록 + [가져온 식으로 열기] · [내 단가산출로]
- 걷음(요구 703): 판정 체계 785(Judge · Solve · Bind · San_Basis 다섯 · Make.Move · Row 1차 판정 · 화면 Check · CheckRows · Rows · Block · JudgeApi · Picks · Judge.css) · [우리 기초로 바꾸기](to-base) · 근거 기초 제안 · Clause 기초 판정
- 시험 지움: 642_2(Bind · Solve) · 642_3(Row 1차 판정) · 642_5(옮기기 Move) · 642_6(판정 화면) · 786_2(옛 [수정] 모달) · 793_3a(근거 → 기초 제안) — 모두 걷은 기초 판정 길 몫
- 시험 고침: 642_1 · 794_2a2 · 794_2b(근거 제안 부분만 뺌) · 793_2 · 793_3b(기초 바꾸기 기대 → 없음) · 627 · 779_2 · 785_4 · 807_1 · m02_hopyo_excel · grid · stmate(judge 길 → view 길)
- 새 시험 test_809_1_import_labor(기초 길 0 · 표지 1,350 줄 · 가져온 식 SK · 모달 = Labor_Table · 금액 = 엑셀)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 19:17:58 +09:00

237 lines
8.6 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Hopyo_Imported.ts
* 불러오기 단산 블록 [가져온 식으로 열기] 모달(PLAN 793-3b · 809-1 · 요구 646 · 702) —
* 엑셀 보기 단가산출 탭 목록(`Hopyo_SheetSk`)이 엶 · 경쟁사 로직 그대로(기초로 바꾸기 없음 · 요구 703).
* · 열기 = 서버 `…/sangeun/from-hopyo`(그 블록 → 가져온 식 SK · 만들 층 · 엑셀 단가 그대로 · 같은 내용 = 그 SK 다시 씀)
* → 그 SK 를 읽어 우리 단가산출 화면(`Sangeun_Detail` 시트 · 고침 = 시트 그대로) + 머리 판(`Hopyo_ImportedPanel`)
* · [우리 단가로] · [엑셀 단가로] · [이음 고르기] = 서버 `…/sangeun/imported/{price|link}`
* (화면 캐시 문서 고침 · 저장 없음) → 시트 다시 엶
* · [이음 보기](797-2) = 소요량 · 계수 후보 모달(`Hopyo_QtyLinks`) — 고르기 · 버리기 = 서버 `…/imported/{qty|qty-pick|qty-drop}`(기록만 · 금액 그대로)
* · [저장] = 그 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 { 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)"],
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;
}
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
});
interface EditAnswer {
문서: ImportedDoc;
전?: Money4 | null;
후?: Money4 | null;
이음없음?: { 자리: string; 이름: 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
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 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 = "";
saveBtn.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();
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: "1100px",
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();
}