Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Wizard.ts
T
eomsangdonandClaude Opus 5.5 82037faeaa feat(743-1): 불러오기 엑셀 보관 안 함 — 그 불러오기 동안만 · 끝내기 · 화면 떠남 · 하루 지남 지움 · 같은 엑셀 다시 올림 · 만든 문서 엑셀 사본 걷음
- 끝냄 한 길 M02_Hopyo_Forget.finish — HP · 격자 · 맞춤셈 캐시 지움 · 만든 SC · SK 본뜬 = 출처(파일 · 때 · 누가 + 전국자재 · 복사원본)만 · SC 줄 메모 「실무 …」 걷음 · 실무수량 = 저장 수량.손 과 같을 때만 걷음(742 전 저장본은 남김 = 금액 그대로) · SK 또 = 그 HP 몫만
- 같은 엑셀 다시 올림 409 걷음(끝낸 기록 지문 안 씀 · 키 번은 그대로 이어감)
- 하루 지난 HP = 목록 숨김 · 올림 때 끝냄 · 붙여넣기 만듦 뒤 실무자료 끝냄
- 마법사 떠남(다른 화면 · 페이지 닫음) = 이 화면이 쥔 파일 지움(keepalive DELETE)
- 뒤 셈이 끝낸 HP 캐시를 다시 쓰지 않음(Fit._put) · 끝냄은 HP 먼저 지우고 캐시
- 옛 파일 정리 tidy(미리 보기 표 · --apply) — 옛 HP · 지문 기록 · HP 없는 문서 사본
- 본뜬 곳 칸 = 끝낸 실무자료는 「실무자료 파일」(originText)
- 시험: 718-1 「끝낸 같은 엑셀 409」 → 「다시 올림 새 키」 · 705-2 붙여넣기 실무자료 「남음」 → 「지움」(요구 584-2 로 뜻 바뀜) · 372-2 화면 시험 onDeleted 꼴 바뀜

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:22:42 +09:00

309 lines
13 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Hopyo_Wizard.ts
* 일위대가 불러오기 = 한시 마법사(PLAN 718-1 · 요구서 내역12 4장 22) — 본 칸 한 화면 · 걸음 셋:
* ① 결과 층(쓸 수 있는 층만 · 기본 개인) · 결과(일위대가 늘 · 단가산출 체크) · 엑셀 고르기 → 올림
* (그 층에 둠 · 같은 엑셀 작업 중 = 그 파일 이어서 · 끝낸 같은 엑셀 = 새로 올림) · 작업 중 파일 이어서
* ② 보기 · 확인 · 만들기 = 엑셀 보기 창(`Hopyo_Sheet` — [모두 고정으로 들여오기] · 호표 [수정] →
* 확인 창 만들기 · [구조물 조합으로] 그대로) · 만들 층 = 고른 층 ·
* 단가산출 체크면 단가산출 칸(`Hopyo_WizardSk` — 기초 · SK · 고름 대기 기초 고름 · 729-3) ·
* 안 이은 줄 띠(`Hopyo_Left` — 안 만든 호표의 남은 줄 · [고르기] = 줄마다 이을 것 · 729-3)
* ③ [끝내기] = 남은 줄 다시 셈 → 있으면 막음(띠 · 남은 수 · 표) · 없으면 물음 → 서버가 이번 HP · 격자 · 맞춤셈 캐시 지움 ·
* 만든 일위대가 · 단가산출(엑셀 사본 걷고 출처만) · 별칭 장부 남음
* 작업 중 = 이 화면 동안만(743-1) — 화면 떠남(`leave` · 페이지 닫음) = 이 화면에서 올리거나 이은 파일 지움 ·
* 하루 지난 것은 서버가 지움 · 엑셀 자료는 보관하지 않음(필요하면 다시 올림).
* ========================================================================== */
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 { createBanner } from "@ui/ui_template_banner";
import { t as L } from "@ui/ui_template_locale";
import { readHopyo } from "./M02_MasterTemplete_Hopyo_Api";
import { hasDraft, draftKey } from "./M02_MasterTemplete_Hopyo_Check";
import {
countLeft,
leftCount,
openLeft,
type LeftMap,
type LeftScope,
} from "./M02_MasterTemplete_Hopyo_Left";
import { sangeunStep } from "./M02_MasterTemplete_Hopyo_WizardSk";
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 interface HopyoWizard {
root: HTMLElement;
/** 화면 떠남 — 이 화면에서 올리거나 이은 파일 지움(743-1) */
leave: () => void;
}
export function buildHopyoWizard(): HopyoWizard {
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 });
const held = new Map<string, HopyoScope>(); // 이 화면이 쥔 파일 「층|키」 → 층
const hold = (layer: ManageLayer, key: string): void =>
void held.set(`${layer}|${key}`, scopeOf(layer));
const drop = (layer: ManageLayer, key: string): void => void held.delete(`${layer}|${key}`);
const leave = (): void => {
window.removeEventListener("pagehide", leave);
for (const [id, scope] of held) void deleteHopyo(scope, id.split("|")[1], true).catch(() => {});
held.clear();
};
window.addEventListener("pagehide", leave);
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(L("M02_WizSkCheck"), {
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");
hold(layer, file.키);
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: () => {
drop(layer, file.키);
void first();
},
}),
)
.catch((error: unknown) => showToast(why(error), "error")),
});
const finish = createButton({ label: "끝내기", variant: "ghost", onClick: () => void end() });
finish.dataset.hopyoFinish = "";
const at: LeftScope = { scope, target: scope, key: file.키 };
const band = el("div", { attrs: { "data-hopyo-left": "" } });
let nos: number[] = [];
let map: LeftMap | null = null;
const pick = createButton({
label: L("M02_LeftOpen"),
variant: "ghost",
size: 28,
onClick: () => openLeft({ ...at, 파일: file.파일, nos, ...(map ? { map } : {}) }, paint),
});
pick.dataset.hopyoLeftOpen = "";
/** 띠 — 남은 줄 있으면 [끝내기] 막음 · 까닭 · 남은 수 */
function paint(got: LeftMap | null): void {
map = got;
const n = got ? leftCount(got) : null;
finish.disabled = !!n?.줄;
finish.title = n?.줄 ? L("M02_LeftBlocked") : "";
band.dataset.left = n ? String(n.줄) : "";
band.replaceChildren(
createBanner({
tone: !n ? "info" : n.줄 ? "warning" : "success",
text: !n
? L("M02_LeftUnknown")
: n.줄
? `${L("M02_LeftBand").replace("{n}", String(n.줄)).replace("{h}", String(n.호표))} · ${L("M02_LeftBlocked")}`
: L("M02_LeftZero"),
actions: [pick],
}),
);
}
paint(null);
async function recount(): Promise<LeftMap> {
if (!nos.length) nos = (await readHopyo(scope, file.키)).호표.map((h) => h.번);
const got = await countLeft(at, nos, (i, all) =>
band.replaceChildren(createText(`${L("M02_LeftCounting")} ${i + 1} / ${all}`, "muted", 14)),
);
paint(got);
return got;
}
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),
...(wantSangeun
? [
createPanelHead({ title: L("M02_WizSkHead"), level: 4, divider: true }).root,
sangeunStep(scope, file.키).root,
]
: []),
createPanelHead({ title: L("M02_WizIlwiHead"), level: 4, divider: true }).root,
createText(
"엑셀 보기 창에서 [모두 고정으로 들여오기] 또는 호표 [수정] → 확인하고 만들기 · [구조물 조합으로] · 만든 일위대가 · 단가산출은 고른 층에 들어감",
"hint",
),
band,
el("div", { className: "ui-toolbar", children: [view, finish, back] }),
);
async function end(): Promise<void> {
steps.set("3");
finish.disabled = true;
let got: LeftMap;
try {
got = await recount();
} catch (error) {
showToast(why(error), "error");
finish.disabled = false;
return steps.set("2");
}
if (leftCount(got).줄) {
showToast(L("M02_LeftBlocked"), "warning");
openLeft({ ...at, 파일: file.파일, nos, map: got }, paint);
return steps.set("2");
}
const loose = [...got.values()].filter(
(x) => !x.one.호표.만든?.확인 && hasDraft(draftKey(scope, file.키, x.one.호표.번)),
).length;
const ok = await showConfirmDialog(
`「${file.파일}」 불러오기를 끝냄 — 올린 파일(엑셀 결과 · 격자 · 맞춤 캐시)을 지움 · 만든 일위대가 · 단가산출(출처만 남김)과 별칭 장부는 남음 · 같은 엑셀은 필요하면 다시 올림` +
(loose ? ` · ${L("M02_LeftLoose").replace("{n}", String(loose))}` : ""),
"끝내기",
);
if (!ok) return steps.set("2");
try {
const got = (await deleteHopyo(scope, file.키)) as { 만든?: number };
drop(layer, file.키);
showToast(`끝냄 — 만든 일위대가 · 단가산출 ${got.만든 ?? 0} · 올린 파일 지움`, "success");
} catch (error) {
showToast(why(error), "error");
return steps.set("2");
}
await first();
}
}
void first();
return { root, leave };
}