Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetMake.ts
T
eomsangdon 73f113ba34 feat(785-4): 불러오기 화면 껍데기 — 가운데 상세 페이지(마법사) 없앰 · 왼쪽 [불러오기] → 바로 본 칸 엑셀 + 오른쪽 목록
- 새 Hopyo_Import.ts(머리 막대 층 · 안 이은 줄 배지 · [끝내기] · 떠남 지움) · Hopyo_Wizard.ts 지움
- Hopyo_Side.ts = 올림 · 층 작은 고름(둘 이상일 때만) · 작업 중 줄 · 다른 엑셀 물음
- Hopyo_Sheet.ts = 몸(buildHopyoSheet) + 모달 껍데기 · 만들기 셋 → 새 Hopyo_SheetMake.ts · [삭제] 걷음 · 찾기 · 체크 부품으로
- 흠 셋: 안 이은 줄 0 오표시(빈 호표 번) · 맞춤 셈 중 멈춤(3 초 저절로 다시) · 좁은 판 줄 짓눌림(시트 아래 줄 판 자리)
- 시험: 785-4 지킴 시험 새로 · 낡은 마법사 · 모달 [삭제] · 좁은 꼴 인자 대조를 새 파일 · 새 길로 고침(Wizard 삭제 · SheetMake 이동 · narrow 걷음)
2026-11-04 23:08:39 +09:00

257 lines
11 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Hopyo_SheetMake.ts
* 엑셀 보기 몸의 만들기 셋(PLAN 785-4 · `Hopyo_Sheet` 에서 옮김 — 줄 바꿈 없음) —
* · [저장] = 체크한 호표 + 확인 안 된 하위 호표(777-1 · `Hopyo_Kids`)마다 체크 줄(확인 창 캐시 · 없으면 서버 제안)로
* 하위 호표부터 차례로 `/hopyo-build` — 물음 한 번 · 체크 줄 없는 고른 호표 건너뜀 · 하위 실패 = 그 하위 [고르기] ·
* 결과 표(사전조합 키 · 빠진 줄 · 값 차이) · `onImport` 가 있으면 호표마다 알림([로직 추가] 참조 줄)
* · [모두 고정으로 들여오기](705-3) = 물음 한 번 → 서버 `/hopyo-build-all`(호표 모두 · 하위 먼저 · 엑셀 숫자 그대로 ·
* 이미 만든 호표 그대로) → 알림 「호표 n · 줄 m · 품셈 줄 a · 고정 줄 b · 빠짐 0 · 엑셀 합계 = 우리 합계 n/n」 + 다음 걸음
* ========================================================================== */
import {
createButton,
el,
hideLoadingOverlay,
showConfirmDialog,
showLoadingOverlay,
showToast,
} from "@ui/ui_template_elements";
import { t as L } from "@ui/ui_template_locale";
import {
buildHopyo,
buildHopyoAll,
readHopyoOne,
type HopyoBrief,
type HopyoBuilt,
type HopyoBuiltAll,
type HopyoOne,
type HopyoScope,
} from "./M02_MasterTemplete_Hopyo_Api";
import {
draftKey,
hasDraft,
HOPYO_BUILT,
loadDraft,
partsOf,
} from "./M02_MasterTemplete_Hopyo_Check";
import { bodyPair, kidsText, leftAt, readyFor, unmadeKids } from "./M02_MasterTemplete_Hopyo_Kids";
import { builtWord } from "./M02_MasterTemplete_Hopyo_Choose";
import { leftButton } from "./M02_MasterTemplete_Hopyo_Left";
import { openRelink, relinkKeys } from "./M02_MasterTemplete_Hopyo_Relink";
import { formatPrice } from "../common_util/common_util_money";
export const why = (error: unknown): string =>
error instanceof Error ? error.message : String(error);
export const won = (v: number | null | undefined): string =>
v === null || v === undefined ? "" : formatPrice(v);
export const note = (text: string): HTMLElement =>
el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } });
/** 만들 차례 — 하위 호표(호표 참조 줄이 가리키는 고른 호표)부터 · 순환은 남은 차례대로 */
export function buildOrder(ones: { 번: number; 아래: number[] }[]): number[] {
const left = new Map(ones.map((x) => [x.번, x.아래.filter((n) => n !== x.번)]));
const out: number[] = [];
while (left.size) {
const ready = [...left].find(([, kids]) => kids.every((n) => !left.has(n)));
const no = ready ? ready[0] : [...left.keys()][0];
out.push(no);
left.delete(no);
}
return out;
}
export interface MakeContext {
scope: HopyoScope;
key: string;
파일: string;
/** 만들 층 */
target: HopyoScope;
onImport?: (got: HopyoBuilt, hopyo: HopyoBrief) => void;
checked: Set<number>;
briefs: () => HopyoBrief[];
closed: () => boolean;
go: HTMLButtonElement;
fixAll: HTMLButtonElement;
result: HTMLElement;
paintGo: () => void;
}
/** [저장] · [모두 고정으로 들여오기] 누름을 이음 */
export function wireMake(c: MakeContext): void {
const { scope, key, target, go, fixAll, result } = c;
const table = (heads: string[], rows: HTMLElement[]): HTMLElement =>
el("table", {
className: "ui-general-block__table m02-hopyo__table",
children: [
el("thead", {
children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })],
}),
el("tbody", { children: rows }),
],
});
const body = (one: HopyoOne) => bodyPair(scope, key, one);
const headCell = (no: number, name: string): HTMLElement =>
el("td", { text: `제${no}호표 ${name}`.trim() });
/** [저장] — 물음 한 번 → 고른 호표마다 서버 보기 → 체크 줄 있는 것만 만들기(체크 줄 없으면 건너뜀) */
async function save(): Promise<void> {
const nos = [...c.checked].sort((a, b) => a - b);
const fresh = nos.filter((no) => !hasDraft(draftKey(scope, key, no))).length;
const ask =
`고른 호표 ${nos.length}개를 체크한 줄로 ${L("M02_PrecomboTitle")} 생성` +
(fresh ? ` — 확인 창에서 안 고친 ${fresh}개는 서버 제안 체크 줄` : "");
if (!(await showConfirmDialog(ask, "저장"))) return;
go.disabled = true;
const ones: HopyoOne[] = [];
const skipped: HTMLElement[] = [];
showLoadingOverlay();
try {
for (const no of nos) {
if (c.closed()) return;
try {
ones.push(await readHopyoOne(scope, key, no, target));
} catch (error) {
skipped.push(el("tr", { children: [headCell(no, ""), el("td", { text: why(error) })] }));
}
}
ones.push(...(await unmadeKids(scope, key, target, ones))); // 777-1 하위 함께
} finally {
hideLoadingOverlay();
c.paintGo();
}
const ready = ones.filter((one) => readyFor(scope, key, one, nos)); // 하위 함께
for (const one of ones)
if (!ready.includes(one))
skipped.push(
el("tr", {
children: [
headCell(one.호표.번, one.호표.이름),
el("td", { text: "건너뜀 — 체크 줄 없음" }),
],
}),
);
await makeAll(ready, skipped);
}
/** 하위 호표부터 차례로 만들기 — 하위 확인 전(409)은 안내 */
async function makeAll(ones: HopyoOne[], skipped: HTMLElement[]): Promise<void> {
const by = new Map(ones.map((one) => [one.호표.번, one]));
const order = buildOrder(
ones.map((one) => {
const d = loadDraft(draftKey(scope, key, one.호표.번));
const kids = one.호표.줄.flatMap((l) =>
partsOf(l, d).flatMap((p) => (p.호표 !== undefined ? [p.호표] : [])),
);
return { 번: one.호표.번, 아래: kids };
}),
);
const rows: HTMLElement[] = [];
let ok = 0;
showLoadingOverlay();
try {
for (const no of order) {
const one = by.get(no)!;
const h = c.briefs().find((x) => x.번 === no) ?? one.호표;
const cell = headCell(no, h.이름);
try {
const from = { 층: scope.layer, 주인: scope.owner, 키: key, 번: no };
const got = await buildHopyo(target, from, ...body(one));
ok += 1;
c.onImport?.(got, h);
rows.push(
el("tr", {
attrs: { "data-built": got.참조.키 },
children: [
cell,
el("td", { text: `${got.참조.키} · ${builtWord(got)}${kidsText(got)}` }),
el("td", { text: String(got.빠진줄.length), className: "m02-hopyo__num" }),
el("td", { text: won(got.비교.차이), className: "m02-hopyo__num" }),
],
}),
);
} catch (error) {
const [err, kid] = leftAt(error, no); // 하위 실패 = 그 하위 남은 줄
const left = leftButton(err, { scope, target, key, 파일: c.파일, 번: kid }); // prettier-ignore
const text = `실패 — ${why(error)}`;
rows.push(el("tr", { children: [cell, el("td", { text }), ...(left ? [el("td", { children: [left] })] : [])] })); // prettier-ignore
}
}
} finally {
hideLoadingOverlay();
}
if (ok) document.dispatchEvent(new Event(HOPYO_BUILT));
result.replaceChildren(
table(
["호표", `${L("M02_PrecomboTitle")}`, "빠진 줄", "값 차이(참고)"],
[...rows, ...skipped],
),
);
const all = order.length + skipped.length;
showToast(`만듦 ${ok} / ${all}`, ok === all ? "success" : "warning");
}
go.addEventListener("click", () => void save());
/** 들여오기 알림 글(705-3) */
const allText = (got: HopyoBuiltAll): string =>
`호표 ${got.호표} · 줄 ${got.줄} · 품셈 줄 ${got.품셈} · 고정 줄 ${got.고정} · ` +
`하위 호표 참조 ${got.호표참조} · 빠짐 ${got.빠짐} · 엑셀 합계 = 우리 합계 ${got.같음}/${got.호표}` +
(got.그대로 ? ` · 이미 만든 호표 ${got.그대로} 그대로` : "");
/** [모두 고정으로 들여오기](705-3) — 물음 한 번 · 서버가 호표 모두를 하위부터(숫자 = 서버가 파일에서) */
async function importAll(): Promise<void> {
const ask =
`호표 ${c.briefs().length}개를 엑셀 숫자 그대로(고정 줄) ${L("M02_PrecomboTitle")}으로 들여옴 — ` +
"이미 만든 호표는 그대로 · 품셈 맞춤은 뒤에 호표 [수정]에서 [품셈으로 바꾸기]";
if (!(await showConfirmDialog(ask, "들여오기"))) return;
fixAll.disabled = true;
showLoadingOverlay();
try {
const from = { 층: scope.layer, 주인: scope.owner, 키: key };
const got = await buildHopyoAll(target, from);
if (got.호표 > got.그대로) document.dispatchEvent(new Event(HOPYO_BUILT));
const next =
"다음 — 호표 [수정]에서 [품셈으로 바꾸기]" +
(got.이음후보
? ` · 이음 후보 ${got.이음후보} 줄(이름은 같고 단가가 다름)은 엑셀 단가 그대로 — 아래 [다음 — 이 공사 단가로 다시 잇기]에서 바꿈`
: "");
const rows = [
...got.다름.map((x) =>
el("tr", {
children: [
el("td", { text: `제${x.번}호표 ${x.이름}` }),
el("td", {
text: `엑셀 ${won(x.엑셀)} · 우리 ${won(x.우리)}`,
className: "m02-hopyo__num",
}),
el("td", { text: won(x.차이), className: "m02-hopyo__num" }),
],
}),
),
...got.실패.map((x) => {
const left = leftButton({ detail: x }, { scope, target, key, 파일: c.파일, 번: x.번 }); // prettier-ignore
return el("tr", { children: [el("td", { text: `제${x.번}호표` }), el("td", { text: x.까닭 }), ...(left ? [el("td", { children: [left] })] : [])] }); // prettier-ignore
}),
];
const relink = createButton({
label: "다음 — 이 공사 단가로 다시 잇기",
size: "sm",
onClick: () => void openRelink(relinkKeys((got.만듦 ?? []).map((x) => x.키))),
}); // 705-2
result.replaceChildren(
note(allText(got)),
note(next),
relink,
...(rows.length ? [table(["호표", "합계 다름 · 실패", "차이"], rows)] : []),
);
showToast(allText(got), got.빠짐 || got.실패.length ? "warning" : "success");
} catch (error) {
showToast(`실패 — ${why(error)}`, "error");
} finally {
hideLoadingOverlay();
fixAll.disabled = false;
}
}
fixAll.addEventListener("click", () => void importAll());
}