- 새 M02_Hopyo_Sangeun.py — 단산 블록을 부르는 첫 엑셀 줄 1 단위 단가를 고정 더한줄 하나로 · 본뜬 「HP-###### 단산N」 · 같은 본뜬은 그대로(겹침 없음)
- 길 POST /layers/{층}/hopyo/{키}/sangeun · [끝내기] 지문 기록에 단가산출도 셈
- 마법사: 결과 「단가산출」 체크 켬 → ② 걸음 [단가산출 만들기] · 알림 글 「일위대가 · 단가산출」
- 시험: test_718_1 단가산출 더함 · 낡은 대조 고침 — 716-3 왼쪽 판 차례(extras 없어짐) · SANGEUN_READY 대조
- 고정 더한줄 셈 값(엔진 값묶음)은 715-1b 뒤
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
239 lines
10 KiB
TypeScript
239 lines
10 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Hopyo_Wizard.ts
|
|
* 일위대가 불러오기 = 한시 마법사(PLAN 718-1 · 요구서 내역12 4장 22) — 본 칸 한 화면 · 걸음 셋:
|
|
* ① 결과 층(쓸 수 있는 층만 · 기본 개인) · 결과(일위대가 늘 · 단가산출 체크) · 엑셀 고르기 → 올림
|
|
* (그 층에 둠 · 같은 엑셀 작업 중 = 그 파일 이어서 · 끝낸 같은 엑셀 = 서버 409 안내) · 작업 중 파일 이어서
|
|
* ② 보기 · 확인 · 만들기 = 엑셀 보기 창(`Hopyo_Sheet` — [모두 고정으로 들여오기] · 호표 [수정] →
|
|
* 확인 창 만들기 · [구조물 조합으로] 그대로) · 만들 층 = 고른 층 ·
|
|
* 단가산출 체크면 [단가산출 만들기] = 산근 블록마다 바탕 없는 단가산출(서버 `M02_Hopyo_Sangeun` · 겹침 없음)
|
|
* ③ [끝내기] = 물음 → 서버가 이번 HP · 격자 · 맞춤셈 캐시 지움 · 만든 일위대가 · 단가산출 · 별칭 장부 남음 · 지문 기록
|
|
* 작업 중 = 서버에 보관(떠났다 와도 ① 목록에서 이어서) · 옛 HP(지문 없음)는 서버 목록에서 숨김.
|
|
* ========================================================================== */
|
|
|
|
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
|
import { createCheckbox } from "@ui/ui_template_checkbox";
|
|
import { createEmpty } from "@ui/ui_template_empty";
|
|
import { createPanelHead } from "@ui/ui_template_panel_head";
|
|
import { createSegment } from "@ui/ui_template_segment";
|
|
import { createText } from "@ui/ui_template_text";
|
|
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
|
import {
|
|
deleteHopyo,
|
|
listHopyo,
|
|
uploadHopyo,
|
|
type HopyoFile,
|
|
type HopyoScope,
|
|
} from "./M02_MasterTemplete_Hopyo_Api";
|
|
import { call } from "./M02_MasterTemplete_Api_Fetch";
|
|
import { accessFor, type ManageLayer } from "./M02_MasterTemplete_Layer";
|
|
|
|
const LAYERS: { layer: ManageLayer; text: string }[] = [
|
|
{ layer: "personal", text: "개인" },
|
|
{ layer: "company", text: "회사" },
|
|
{ layer: "system", text: "마스터" },
|
|
];
|
|
const STEPS = [
|
|
{ value: "1", label: "① 층 · 엑셀" },
|
|
{ value: "2", label: "② 보기 · 확인 · 만들기" },
|
|
{ value: "3", label: "③ 끝내기" },
|
|
];
|
|
|
|
type Got = HopyoFile & { 이어서?: boolean };
|
|
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
|
const textOf = (layer: string): string => LAYERS.find((x) => x.layer === layer)?.text ?? layer;
|
|
|
|
/** 쓸 수 있는 자기 층(개인 · 회사 · 마스터 차례) */
|
|
export async function writableLayers(): Promise<ManageLayer[]> {
|
|
const user = await fetchSessionUser().catch(() => null);
|
|
return LAYERS.map((x) => x.layer).filter((layer) => accessFor(layer, null, user).canWrite);
|
|
}
|
|
|
|
export function buildHopyoWizard(): HTMLElement {
|
|
const steps = createSegment({
|
|
options: STEPS.map((s) => ({ ...s, disabled: true })),
|
|
value: "1",
|
|
});
|
|
steps.root.dataset.hopyoSteps = "";
|
|
const body = el("div", { className: "m02-hopyo" });
|
|
const root = el("div", {
|
|
className: "m02-hopyo",
|
|
attrs: { "data-hopyo-wizard": "" },
|
|
children: [
|
|
createText(
|
|
"한시 — 다른 프로그램 일위대가 엑셀을 우리 일위대가로 옮김 · 끝내면 올린 파일 지움 · 별칭 장부는 남음",
|
|
"hint",
|
|
),
|
|
el("div", { className: "ui-toolbar", children: [steps.root] }),
|
|
body,
|
|
],
|
|
});
|
|
let layers: ManageLayer[] = [];
|
|
let pick: ManageLayer = "personal";
|
|
let wantSangeun = false; // 결과 「단가산출」 체크
|
|
const scopeOf = (layer: ManageLayer): HopyoScope => ({ layer, owner: null });
|
|
|
|
async function first(): Promise<void> {
|
|
steps.set("1");
|
|
layers = await writableLayers();
|
|
if (!layers.includes(pick)) pick = layers[0] ?? "personal";
|
|
const input = el("input", {
|
|
attrs: { type: "file", accept: ".xls,.xlsx", "data-hopyo-file": "" },
|
|
}) as HTMLInputElement;
|
|
input.hidden = true;
|
|
const upload = createButton({
|
|
label: "엑셀 고르기",
|
|
variant: "filled",
|
|
disabled: !layers.length,
|
|
onClick: () => input.click(),
|
|
});
|
|
upload.title = layers.length ? "설계내역서 엑셀(일위대가 시트)" : "쓸 수 있는 층 없음";
|
|
input.addEventListener("change", () => {
|
|
const file = input.files?.[0];
|
|
input.value = "";
|
|
if (!file) return;
|
|
upload.disabled = true;
|
|
uploadHopyo(scopeOf(pick), file)
|
|
.then((got: Got) => {
|
|
if (got.이어서) showToast(`같은 엑셀 작업 중 — ${got.키} 이어서`, "info");
|
|
else showToast(`올림 — 호표 ${got.호표수}`, got.어긋남 ? "warning" : "success");
|
|
second(pick, got);
|
|
})
|
|
.catch((error) => showToast(why(error), "error"))
|
|
.finally(() => (upload.disabled = !layers.length));
|
|
});
|
|
const seg = createSegment({
|
|
options: LAYERS.map((x) => ({
|
|
value: x.layer,
|
|
label: x.text,
|
|
disabled: !layers.includes(x.layer),
|
|
})),
|
|
value: pick,
|
|
onChange: (v) => (pick = v as ManageLayer),
|
|
});
|
|
seg.root.dataset.hopyoLayer = "";
|
|
const ilwi = createCheckbox("일위대가(고정 줄)", { checked: true, disabled: true });
|
|
const sangeun = createCheckbox("단가산출(산근 블록 · 바탕 없음)", {
|
|
checked: wantSangeun,
|
|
onChange: (on) => (wantSangeun = on),
|
|
});
|
|
sangeun.root.dataset.hopyoSangeun = "";
|
|
const busy = el("div", { className: "m02-hopyo" });
|
|
body.replaceChildren(
|
|
createText("결과 층", "muted", 14),
|
|
el("div", { className: "ui-toolbar", children: [seg.root] }),
|
|
createText("결과", "muted", 14),
|
|
el("div", { className: "ui-toolbar", children: [ilwi.root, sangeun.root] }),
|
|
el("div", { className: "ui-toolbar", children: [upload, input] }),
|
|
createPanelHead({ title: "작업 중", level: 4, divider: true }).root,
|
|
busy,
|
|
);
|
|
const rows = (
|
|
await Promise.all(
|
|
layers.map((layer) =>
|
|
listHopyo(scopeOf(layer))
|
|
.then((list) => list.map((f) => ({ layer, f })))
|
|
.catch(() => []),
|
|
),
|
|
)
|
|
).flat();
|
|
busy.replaceChildren(
|
|
...(rows.length
|
|
? rows.map(({ layer, f }) => {
|
|
const row = createButton({
|
|
label: `${f.파일} · 호표 ${f.호표수} · ${textOf(layer)} — 이어서`,
|
|
variant: "ghost",
|
|
onClick: () => second(layer, f),
|
|
});
|
|
row.dataset.hopyoKey = f.키;
|
|
return row;
|
|
})
|
|
: [createEmpty({ text: "작업 중인 불러오기 없음", small: true })]),
|
|
);
|
|
}
|
|
|
|
function second(layer: ManageLayer, file: HopyoFile): void {
|
|
steps.set("2");
|
|
const scope = scopeOf(layer);
|
|
const view = createButton({
|
|
label: "엑셀 보기 · 확인 · 만들기",
|
|
variant: "filled",
|
|
onClick: () =>
|
|
void import("./M02_MasterTemplete_Hopyo_Sheet")
|
|
.then((m) =>
|
|
m.openHopyoSheet({
|
|
scope,
|
|
key: file.키,
|
|
파일: file.파일,
|
|
target: scope,
|
|
canWrite: true,
|
|
onDeleted: () => void first(),
|
|
}),
|
|
)
|
|
.catch((error: unknown) => showToast(why(error), "error")),
|
|
});
|
|
const sangeun = createButton({
|
|
label: "단가산출 만들기",
|
|
variant: "filled",
|
|
onClick: () => void makeSangeun(),
|
|
});
|
|
sangeun.title =
|
|
"엑셀 산근 블록마다 바탕 없는 단가산출(엑셀 단가 그대로 · 이미 만든 블록은 그대로)";
|
|
sangeun.dataset.hopyoSangeunMake = "";
|
|
const finish = createButton({ label: "끝내기", variant: "ghost", onClick: () => void end() });
|
|
finish.dataset.hopyoFinish = "";
|
|
const back = createButton({ label: "처음으로", variant: "link", onClick: () => void first() });
|
|
back.title = "작업은 그대로 보관 — 처음 화면 「작업 중」에서 이어서";
|
|
const wrong = file.어긋남 ? ` · 검산 어긋남 ${file.어긋남}` : "";
|
|
body.replaceChildren(
|
|
createText(`${file.파일} · ${textOf(layer)} 층 · 호표 ${file.호표수}${wrong}`, "muted", 14),
|
|
createText(
|
|
"엑셀 보기 창에서 [모두 고정으로 들여오기] 또는 호표 [수정] → 확인하고 만들기 · [구조물 조합으로] · 만든 일위대가 · 단가산출은 고른 층에 들어감",
|
|
"hint",
|
|
),
|
|
el("div", {
|
|
className: "ui-toolbar",
|
|
children: [view, ...(wantSangeun ? [sangeun] : []), finish, back],
|
|
}),
|
|
);
|
|
|
|
async function makeSangeun(): Promise<void> {
|
|
sangeun.disabled = true;
|
|
try {
|
|
const got = await call<{ 블록: number; 만듦: unknown[]; 그대로: number }>(
|
|
`/layers/${layer}/hopyo/${encodeURIComponent(file.키)}/sangeun`,
|
|
{ method: "POST" },
|
|
);
|
|
const kind = got.블록 ? "success" : "warning";
|
|
showToast(
|
|
`단가산출 ${got.만듦.length} 만듦 · 그대로 ${got.그대로} · 산근 블록 ${got.블록}`,
|
|
kind,
|
|
);
|
|
} catch (error) {
|
|
showToast(why(error), "error");
|
|
} finally {
|
|
sangeun.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function end(): Promise<void> {
|
|
steps.set("3");
|
|
const ok = await showConfirmDialog(
|
|
`「${file.파일}」 불러오기를 끝냄 — 올린 파일(엑셀 결과 · 격자 · 맞춤 캐시)을 지움 · 만든 일위대가 · 단가산출과 별칭 장부는 남음 · 하나라도 만들었으면 같은 엑셀은 다시 올리지 못함`,
|
|
"끝내기",
|
|
);
|
|
if (!ok) return steps.set("2");
|
|
try {
|
|
const got = (await deleteHopyo(scope, file.키)) as { 만든?: number };
|
|
showToast(`끝냄 — 만든 일위대가 · 단가산출 ${got.만든 ?? 0} · 올린 파일 지움`, "success");
|
|
} catch (error) {
|
|
showToast(why(error), "error");
|
|
return steps.set("2");
|
|
}
|
|
await first();
|
|
}
|
|
}
|
|
|
|
void first();
|
|
return root;
|
|
}
|