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

213 lines
8.4 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Hopyo_Side.ts
* 좌측 「일위대가 불러오기」 컨테이너(PLAN 324-4 · 718-1 한시 · 785-4) —
* · [불러오기](구조물 조합 층 단추 꼴 `m02-layers__btn`) = 바로 엑셀 고르기 창 → 올림 → 본 칸 불러오기 보기
* (`M02_MasterTemplete_Hopyo_Import` · 엑셀 + 오른쪽 호표 목록 · 가운데 상세 페이지 없음 · 누름 하나)
* · 결과 층 고름 = 작은 고름(쓸 수 있는 층 둘 이상일 때만 · 일반 = 개인 하나라 안 보임)
* · 「작업 중」 줄 = 층마다 서버 목록(같은 엑셀 작업 중 이어서) · 누르면 본 칸 불러오기 보기
* · 다른 엑셀 [불러오기] · 다른 화면으로 = 지금 보기 떠남(올린 파일 지움 · 743-1) — 고르고 안 만든 확인 캐시가 있으면 물음 한 번
* 아래 [별칭 장부](338-3) → 본 칸 장부 화면(`M02_MasterTemplete_Hopyo_Ledger` · 층 = 구조물 조합 층).
* ========================================================================== */
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
import { createEmpty } from "@ui/ui_template_empty";
import { createListRow } from "@ui/ui_template_list_row";
import { createSegment } from "@ui/ui_template_segment";
import { t as L } from "@ui/ui_template_locale";
import {
listHopyo,
pageScope,
uploadHopyo,
type HopyoFile,
type HopyoScope,
} from "./M02_MasterTemplete_Hopyo_Api";
import { buildHopyoLedger, LEDGER_HINT, LEDGER_TITLE } from "./M02_MasterTemplete_Hopyo_Ledger";
import {
buildHopyoImport,
LAYERS,
layerText,
writableLayers,
type HopyoImport,
} from "./M02_MasterTemplete_Hopyo_Import";
import type { ManageLayer } from "./M02_MasterTemplete_Layer";
export interface HopyoSectionOptions {
/** 이 층 쓰기 권한 — 별칭 장부 고침(구조물 조합 층이 제자리에서 바뀌어 열 때 읽음 · 720-4) */
canWrite: () => boolean;
/** 저장 안 한 고침을 버릴지 확인 — 취소면 false */
confirmLeave: () => Promise<boolean>;
/** 메인 칸에 화면을 띄움 */
show: (label: string, node: HTMLElement) => (dirty: boolean) => void;
/** 화면을 열었음 — 좌측 다른 고름 표시를 지움 */
onOpen: () => void;
}
export interface HopyoSection {
title: string;
body: HTMLElement;
/** 다른 화면이 열렸으면 고름 표시를 지움 */
paint: () => void;
}
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
const scopeOf = (layer: ManageLayer): HopyoScope => ({ layer, owner: null });
export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
const title = L("M02_HopyoImport");
let layers: ManageLayer[] = [];
let pick: ManageLayer = "personal";
const input = el("input", {
attrs: { type: "file", accept: ".xls,.xlsx", "data-hopyo-file": "" },
}) as HTMLInputElement;
input.hidden = true;
const start = createButton({
label: "불러오기",
variant: "filled",
onClick: () => void choose(),
});
start.classList.add("m02-layers__btn");
start.title =
"다른 프로그램 일위대가 엑셀 → 우리 일위대가(한시 · 끝내거나 떠나면 올린 파일 지움)";
start.dataset.hopyoWizardOpen = "";
const seg = el("div", { className: "m02-side__row" });
seg.hidden = true;
const jobs = el("div", { className: "m02-hopyo-side", attrs: { "data-hopyo-jobs": "" } });
const ledger = el("button", {
className: "m02-side__item ui-sidebar-row",
text: LEDGER_TITLE,
attrs: { type: "button", title: LEDGER_HINT, "data-hopyo-ledger-open": "" },
});
ledger.addEventListener("click", () => void showLedger());
const body = el("div", {
className: "m02-hopyo-side",
children: [
el("div", { className: "m02-side__row", children: [start, input] }),
seg,
jobs,
ledger,
],
});
let open: { key: string; view: HTMLElement; imp?: HopyoImport } | null = null;
/** 층 고름 · 작업 중 줄 — 쓸 수 있는 층을 처음 한 번 읽음 */
async function refresh(): Promise<void> {
if (!layers.length) {
layers = await writableLayers();
if (!layers.includes(pick)) pick = layers[0] ?? "personal";
if (layers.length > 1) {
const s = createSegment({
options: LAYERS.filter((x) => layers.includes(x.layer)).map((x) => ({ value: x.layer, label: x.text })), // prettier-ignore
value: pick,
size: 24,
onChange: (v) => (pick = v as ManageLayer),
});
s.root.dataset.hopyoLayer = "";
seg.replaceChildren(s.root);
seg.hidden = false;
}
}
const rows = (
await Promise.all(
layers.map((layer) =>
listHopyo(scopeOf(layer))
.then((list) => list.map((f) => ({ layer, f })))
.catch(() => []),
),
)
).flat();
jobs.replaceChildren(
...(rows.length
? rows.map(({ layer, f }) => {
const row = createListRow({
label: f.파일,
sub: `호표 ${f.호표수} · ${layerText(layer)} — 이어서`,
size: 28,
active: open?.imp !== undefined && open.key === f.키,
onClick: () => void show(layer, f),
});
row.dataset.hopyoKey = f.키;
return row;
})
: [createEmpty({ text: "작업 중인 불러오기 없음", small: true })]),
);
}
/** [불러오기] — 지금 보기를 떠날 수 있는지 묻고 바로 파일 고르기 창 */
async function choose(): Promise<void> {
if (!layers.length) await refresh().catch(() => {});
if (!layers.length) return void showToast("쓸 수 있는 층 없음", "warning");
if (!(await leaveOk())) return;
input.click();
}
input.addEventListener("change", () => {
const file = input.files?.[0];
input.value = "";
if (!file) return;
start.disabled = true;
uploadHopyo(scopeOf(pick), file)
.then((got: HopyoFile & { 이어서?: boolean }) => {
if (got.이어서) showToast(`같은 엑셀 작업 중 — ${got.키} 이어서`, "info");
else showToast(`올림 — 호표 ${got.호표수}`, got.어긋남 ? "warning" : "success");
return show(pick, got, true);
})
.catch((error) => showToast(why(error), "error"))
.finally(() => (start.disabled = false));
});
/** 지금 보기를 떠나도 되는지 — 저장 안 한 고침 · 고르고 안 만든 확인 캐시 */
async function leaveOk(): Promise<boolean> {
if (!(await opt.confirmLeave())) return false;
if (!open?.imp?.dirty()) return true;
return showConfirmDialog(
"지금 불러오기를 떠남 — 올린 파일을 지움 · 고르고 안 만든 호표는 빠짐(만든 것은 남음)",
"떠나기",
);
}
/** 본 칸에 불러오기 보기 — 같은 파일이면 그대로 */
async function show(layer: ManageLayer, file: HopyoFile, checked = false): Promise<void> {
if (open?.imp && open.key === file.키 && open.view.isConnected) return;
if (!checked && !(await leaveOk())) return;
open?.imp?.leave(); // 불러오기 떠남 = 올린 파일 지움(743-1)
const imp = buildHopyoImport({
layer,
file,
onEnded: () => {
open = null;
ledger.classList.remove("is-active");
opt.show(title, el("div", { className: "m02-hopyo" })); // 끝냄 — 빈 본 칸
void refresh();
},
});
opt.show(`${title} — ${file.파일}`, imp.root);
opt.onOpen();
open = { key: file.키, view: imp.root, imp };
ledger.classList.remove("is-active");
void refresh();
}
async function showLedger(): Promise<void> {
if (open?.key === LEDGER_TITLE && open.view.isConnected) return;
if (!(await leaveOk())) return;
open?.imp?.leave();
const view = buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() });
opt.show(`${title} — ${LEDGER_TITLE}`, view);
opt.onOpen();
open = { key: LEDGER_TITLE, view };
ledger.classList.add("is-active");
void refresh();
}
const paint = (): void => {
if (open && !open.view.isConnected) {
open.imp?.leave(); // 다른 화면으로 = 불러오기 떠남
open = null;
ledger.classList.remove("is-active");
void refresh();
}
};
void refresh();
return { title, body, paint };
}