Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Import.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

214 lines
8.7 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Hopyo_Import.ts
* 일위대가 불러오기 본 칸 보기(PLAN 785-4 · 요구 642-11 — 마법사 `Hopyo_Wizard` 갈음 · 가운데 상세 페이지 없음) —
* 왼쪽 [불러오기] → 층 · 파일 고름 → 올림(`Hopyo_Side`) → 바로 이 보기:
* · 머리 막대 = 엑셀 보기 몸(`Hopyo_Sheet` — 시트 · 호표 · 검산 · 제안)의 머리에 층 · 안 이은 줄 배지 ·
* [안 이은 줄 고르기] · [끝내기]를 덧붙임
* · 아래 = 엑셀(탭 셋) + 오른쪽 호표 · 단가산출 목록(`Hopyo_Sheet` · `Hopyo_SheetSk`) · 만들기 = 목록 아래 막대
* · 안 이은 줄 수 = 엑셀 보기가 읽은 호표 번으로 셈([끝내기] 전 「0」 오표시 흠 고침 — 빈 호표 번 안 씀)
* · [끝내기] = 남은 줄 다시 셈 → 있으면 막음(배지 · 남은 수 · 표) · 없으면 물음 → 서버가 이번 HP · 격자 · 맞춤셈 캐시 지움 ·
* 만든 일위대가 · 단가산출(엑셀 사본 걷고 출처만) · 별칭 장부 남음
* · 작업 중 = 이 화면 동안만(743-1) — 화면 떠남(`leave` · 페이지 닫음) = 이 화면에서 올리거나 이은 파일 지움 ·
* 하루 지난 것은 서버가 지움 · 엑셀 자료는 보관하지 않음(필요하면 다시 올림)
* ========================================================================== */
import { createBadge, type BadgeTone } from "@ui/ui_template_badge";
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
import { t as L } from "@ui/ui_template_locale";
import { createText } from "@ui/ui_template_text";
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
import {
deleteHopyo,
readHopyo,
type HopyoFile,
type HopyoScope,
type HopyoView,
} from "./M02_MasterTemplete_Hopyo_Api";
import { draftKey, hasDraft } from "./M02_MasterTemplete_Hopyo_Check";
import type { HopyoSheetBody } from "./M02_MasterTemplete_Hopyo_Sheet";
import {
countLeft,
leftCount,
openLeft,
type LeftMap,
type LeftScope,
} from "./M02_MasterTemplete_Hopyo_Left";
import { accessFor, type ManageLayer } from "./M02_MasterTemplete_Layer";
import "./M02_MasterTemplete_Hopyo.css";
export const LAYERS: { layer: ManageLayer; text: string }[] = [
{ layer: "personal", text: "개인" },
{ layer: "company", text: "회사" },
{ layer: "system", text: "마스터" },
];
export const layerText = (layer: string): string =>
LAYERS.find((x) => x.layer === layer)?.text ?? layer;
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
/** 쓸 수 있는 자기 층(개인 · 회사 · 마스터 차례) */
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 HopyoImportArgs {
layer: ManageLayer;
file: HopyoFile;
/** [끝내기] 로 올린 파일을 지웠음 */
onEnded: () => void;
}
export interface HopyoImport {
root: HTMLElement;
/** 화면 떠남 — 이 화면에서 올리거나 이은 파일 지움(743-1) */
leave: () => void;
/** 고른 뒤 안 만든 확인 창 캐시가 있음 — 다른 엑셀로 옮기기 전 물음 */
dirty: () => boolean;
}
export function buildHopyoImport(a: HopyoImportArgs): HopyoImport {
const { layer, file } = a;
const scope: HopyoScope = { layer, owner: null };
const at: LeftScope = { scope, target: scope, key: file.키 };
let nos: number[] = [];
let map: LeftMap | null = null;
let held = true;
let body: HopyoSheetBody | null = null;
const root = el("div", {
className: "m02-hopyo-import",
attrs: { "data-hopyo-import": file.키 },
children: [createText("엑셀을 읽는 중…", "muted", 14)],
});
const leave = (): void => {
window.removeEventListener("pagehide", leave);
if (!held) return;
held = false;
void deleteHopyo(scope, file.키, true).catch(() => {});
body?.destroy();
};
window.addEventListener("pagehide", leave);
// ── 머리 막대에 덧붙일 것 ──
const badge = el("span", { attrs: { "data-hopyo-left": "" } });
const finish = createButton({ label: "끝내기", variant: "filled", size: 28, onClick: () => void end() }); // prettier-ignore
finish.dataset.hopyoFinish = "";
const onSk = (번: number): void => body?.showSk(번);
const pickBtn = createButton({
label: L("M02_LeftOpen"),
variant: "ghost",
size: 28,
onClick: () => void openPick(),
});
pickBtn.dataset.hopyoLeftOpen = "";
const layerNote = createBadge(`${layerText(layer)} 층`, "neutral");
/** 안 이은 줄 창 — 호표 번이 아직 없으면 먼저 읽음(빈 번으로 열면 「0」 오표시) */
async function openPick(): Promise<void> {
try {
await ensureNos();
} catch (error) {
return void showToast(why(error), "error");
}
openLeft({ ...at, 파일: file.파일, nos, onSk, ...(map ? { map } : {}) }, paint);
}
async function ensureNos(): Promise<void> {
if (!nos.length) nos = (await readHopyo(scope, file.키)).호표.map((h) => h.번);
}
/** 배지 — 남은 줄 있으면 [끝내기] 막음 · 까닭 · 남은 수 */
function paint(got: LeftMap | null): void {
map = got;
const n = got ? leftCount(got) : null;
const w = body?.skCount();
const skLeft = w?.블록 ? `${L("M02_SkLeft").replace("{n}", String(w.블록)).replace("{m}", String(w.줄))} · ` : ""; // prettier-ignore
finish.disabled = !!n?.줄;
finish.title = n?.줄 ? skLeft + L("M02_LeftBlocked") : "";
badge.dataset.left = n ? String(n.줄) : "";
const tone: BadgeTone = !n ? "info" : n.줄 ? "warning" : "success";
const text = !n
? L("M02_LeftUnknown")
: n.줄
? L("M02_LeftBand").replace("{n}", String(n.줄)).replace("{h}", String(n.호표))
: L("M02_LeftZero");
badge.replaceChildren(createBadge(text, tone));
}
paint(null);
async function recount(): Promise<LeftMap> {
await ensureNos();
const got = await countLeft(at, nos, (i, all) =>
badge.replaceChildren(createText(`${L("M02_LeftCounting")} ${i + 1} / ${all}`, "muted", 14)),
);
paint(got);
return got;
}
async function end(): Promise<void> {
finish.disabled = true;
let got: LeftMap;
try {
got = await recount();
} catch (error) {
showToast(why(error), "error");
finish.disabled = false;
return;
}
if (leftCount(got).줄) {
showToast(L("M02_LeftBlocked"), "warning");
return openLeft({ ...at, 파일: file.파일, nos, onSk, map: got }, paint);
}
const loose = [...got.values()].filter(
(x) => !x.one.호표.만든?.확인 && hasDraft(draftKey(scope, file.키, x.one.호표.번)),
).length;
const w = body?.skCount(); // 안 옮긴 단가산출(일위대가 밖 포함) — 묻기만
const ok = await showConfirmDialog(
`「${file.파일}」 불러오기를 끝냄 — 올린 파일(엑셀 결과 · 격자 · 맞춤 캐시)을 지움 · 만든 일위대가 · 단가산출(출처만 남김)과 별칭 장부는 남음 · 같은 엑셀은 필요하면 다시 올림` +
(loose ? ` · ${L("M02_LeftLoose").replace("{n}", String(loose))}` : "") +
(w?.블록
? ` · ${L("M02_SkUnmoved").replace("{n}", String(w.블록)).replace("{m}", String(w.밖))}`
: ""), // 780-2 막지 않음
"끝내기",
);
if (!ok) return paint(map);
try {
const out = (await deleteHopyo(scope, file.키)) as { 만든?: number };
held = false;
window.removeEventListener("pagehide", leave);
body?.destroy();
showToast(`끝냄 — 만든 일위대가 · 단가산출 ${out.만든 ?? 0} · 올린 파일 지움`, "success");
} catch (error) {
showToast(why(error), "error");
return paint(map);
}
a.onEnded();
}
// ── 몸 — 엑셀 보기 몸을 바로 담음(스프레드시트는 열 때 불러옴) ──
void import("./M02_MasterTemplete_Hopyo_Sheet")
.then((m) => {
if (!held) return;
const view = m.buildHopyoSheet({
scope,
key: file.키,
파일: file.파일,
target: scope,
canWrite: true,
headExtra: [layerNote, badge, pickBtn, finish],
onSkChange: () => paint(map),
onRead: (got: HopyoView) => void (nos = got.호표.map((h) => h.번)),
});
body = view;
root.replaceChildren(...view.parts);
})
.catch((error: unknown) => root.replaceChildren(createText(why(error), "muted", 14)));
return {
root,
leave,
dirty: () => nos.some((no) => hasDraft(draftKey(scope, file.키, no))),
};
}