- 새 Hopyo_Import.ts(머리 막대 층 · 안 이은 줄 배지 · [끝내기] · 떠남 지움) · Hopyo_Wizard.ts 지움 - Hopyo_Side.ts = 올림 · 층 작은 고름(둘 이상일 때만) · 작업 중 줄 · 다른 엑셀 물음 - Hopyo_Sheet.ts = 몸(buildHopyoSheet) + 모달 껍데기 · 만들기 셋 → 새 Hopyo_SheetMake.ts · [삭제] 걷음 · 찾기 · 체크 부품으로 - 흠 셋: 안 이은 줄 0 오표시(빈 호표 번) · 맞춤 셈 중 멈춤(3 초 저절로 다시) · 좁은 판 줄 짓눌림(시트 아래 줄 판 자리) - 시험: 785-4 지킴 시험 새로 · 낡은 마법사 · 모달 [삭제] · 좁은 꼴 인자 대조를 새 파일 · 새 길로 고침(Wizard 삭제 · SheetMake 이동 · narrow 걷음)
257 lines
11 KiB
TypeScript
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());
|
|
}
|