- 새 Hopyo_Check(확인 창 · 본 칸 · 모달) — 상태 색 · 체크 제안 · 우리 로직 여럿 · 배 · 몫 · 옵션 · 품 비교 · 뺌 · 원문 보기 · 캐시 m02-hopyo-check · 셈 중 3 초 다시 읽음 · 최신 응답만 - 새 Hopyo_Choose(고르기 창 · 후보 판정 · 여럿 고름 · 몫 · 찾기 글 미리 넣음) · 새 Hopyo_Ledger(장부 화면 · 고친 것 캐시 · [저장] 때만 PUT) - 고침 View(호표 하나 = 확인 창) · Sheet(펼침 뒤 한 번 · 하위 호표 먼저) · Pick(확인된 것 바로 참조 · 아니면 확인 창) · Api(3-3 꼴 · 만들기 몸 · 장부 길) · Side([별칭 장부]) · css · Labor_Pick(query) - 없앰 saveHopyoMemory · [확정] · [같음 모두 확정] · [근거 맞음 모두 확정] · 바로 만들기 - 시험 test_m02_hopyo_screen.py 새로 · helper_m02_hopyo_check.cjs 새 · pick · seam 도우미 손질 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
173 lines
6.6 KiB
TypeScript
173 lines
6.6 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Hopyo_View.ts
|
|
* 일위대가 실무자료 본 화면(PLAN 324-4 · 338-3) — 파일 하나: 파일 · 시트 · 호표 수 · 검산 · 제안 n/m · 호표 표 + 찾기 ·
|
|
* [지우기] · [엑셀로 보기](329 엑셀 보기 창 · 늦게 불러옴) / 호표 하나 = 확인 창(`M02_MasterTemplete_Hopyo_Check` ·
|
|
* 체크한 줄만 [확인하고 사전조합 만들기]) — 즉시 확정 · 바로 만들기 단추 없음.
|
|
* 맞춤 · 만들기는 서버(`M02_Hopyo_Match` · `M02_Hopyo_Build`) · 쓰기 단추는 층 쓰기 권한만.
|
|
* ========================================================================== */
|
|
|
|
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
|
import { loadingError, showLoading } from "@ui/ui_template_loading";
|
|
import {
|
|
deleteHopyo,
|
|
readHopyo,
|
|
type HopyoScope,
|
|
type HopyoView,
|
|
} from "./M02_MasterTemplete_Hopyo_Api";
|
|
import { buildHopyoCheck } from "./M02_MasterTemplete_Hopyo_Check";
|
|
import type { HopyoSheetOptions } from "./M02_MasterTemplete_Hopyo_Sheet";
|
|
import "./M02_MasterTemplete_Hopyo.css";
|
|
|
|
export interface HopyoViewOptions {
|
|
scope: HopyoScope;
|
|
/** 층 쓰기 권한 — 없으면 체크 · 만들기 · 지우기 단추 없음 */
|
|
canWrite: boolean;
|
|
/** 파일을 지웠음 — 좌측 목록 다시 */
|
|
onDeleted: () => void;
|
|
}
|
|
|
|
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
|
const won = (v: number | null | undefined): string =>
|
|
v === null || v === undefined ? "" : v.toLocaleString("ko-KR");
|
|
const fail = (error: unknown): void => showToast(`실패 — ${why(error)}`, "error");
|
|
/** 엑셀 보기 창(329) — 열 때 불러옴 */
|
|
const openSheet = (o: HopyoSheetOptions): void =>
|
|
void import("./M02_MasterTemplete_Hopyo_Sheet").then((m) => m.openHopyoSheet(o), fail);
|
|
|
|
const grid = (heads: string[], body: HTMLElement): HTMLElement =>
|
|
el("div", {
|
|
className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head",
|
|
children: [
|
|
el("table", {
|
|
className: "ui-general-block__table m02-hopyo__table",
|
|
children: [
|
|
el("thead", {
|
|
children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })],
|
|
}),
|
|
body,
|
|
],
|
|
}),
|
|
],
|
|
});
|
|
const td = (text: string, cls = ""): HTMLElement =>
|
|
el("td", { text, className: cls, attrs: { title: text } });
|
|
const note = (text: string): HTMLElement => el("span", { className: "m02-hopyo__note", text });
|
|
|
|
export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement {
|
|
const view = el("div", { className: "m02-hopyo", attrs: { "data-hopyo-view": key } });
|
|
let nav = 0; // 늦게 온 응답 버림
|
|
let query = "";
|
|
|
|
async function load<T>(get: () => Promise<T>, retry: () => void): Promise<T | null> {
|
|
const my = ++nav;
|
|
const stop = showLoading(view);
|
|
try {
|
|
const got = await get();
|
|
return my === nav ? got : null;
|
|
} catch (error) {
|
|
if (my === nav) view.replaceChildren(loadingError(why(error), retry));
|
|
return null;
|
|
} finally {
|
|
stop();
|
|
}
|
|
}
|
|
|
|
/* ── 파일 하나 — 호표 표 ─────────────────────────────────────── */
|
|
async function drawFile(): Promise<void> {
|
|
const file = await load(
|
|
() => readHopyo(opt.scope, key),
|
|
() => void drawFile(),
|
|
);
|
|
if (!file) return;
|
|
const search = el("input", {
|
|
className: "ui-input ui-input--sm m02-hopyo__search",
|
|
attrs: { type: "search", placeholder: "호표 · 품명 · 규격 찾기", value: query },
|
|
}) as HTMLInputElement;
|
|
const body = el("tbody");
|
|
const paint = (): void => {
|
|
const q = search.value.trim().toLowerCase();
|
|
query = search.value;
|
|
body.replaceChildren(
|
|
...file.호표
|
|
.filter((h) => !q || `제${h.번}호표 ${h.이름} ${h.규격}`.toLowerCase().includes(q))
|
|
.map((h) => {
|
|
const tr = el("tr", {
|
|
className: "m02-hopyo__row",
|
|
attrs: { "data-hopyo-no": String(h.번) },
|
|
children: [
|
|
td(`제${h.번}호표`),
|
|
td(h.이름),
|
|
td(h.규격),
|
|
td(h.단위),
|
|
td(won(h.금액.계), "m02-hopyo__num"),
|
|
td(`${h.맞춤[0]} / ${h.맞춤[1]}`, "m02-hopyo__num"),
|
|
],
|
|
});
|
|
tr.addEventListener("click", () => void drawOne(h.번));
|
|
return tr;
|
|
}),
|
|
);
|
|
};
|
|
search.addEventListener("input", paint);
|
|
paint();
|
|
const wrong = file.검산.어긋남;
|
|
view.replaceChildren(
|
|
el("div", {
|
|
className: "m02-hopyo__bar",
|
|
children: [
|
|
el("strong", { text: file.파일 }),
|
|
note(`시트 ${file.시트}`),
|
|
note(`호표 ${file.호표수}`),
|
|
note(wrong.length ? `검산 어긋남 ${wrong.length}` : "검산 맞음"),
|
|
note(`제안 ${file.맞춤[0]} / 줄 ${file.맞춤[1]}`),
|
|
createButton({
|
|
label: "엑셀로 보기",
|
|
size: "sm",
|
|
onClick: () =>
|
|
openSheet({ scope: opt.scope, key, 파일: file.파일, canWrite: opt.canWrite }),
|
|
}),
|
|
...(opt.canWrite ? [dropButton(file)] : []),
|
|
],
|
|
}),
|
|
...(wrong.length ? [el("p", { className: "m02-hopyo__warn", text: wrong.join("\n") })] : []),
|
|
search,
|
|
grid(["호표", "품명", "규격", "단위", "엑셀 단가", "제안 / 줄"], body),
|
|
);
|
|
}
|
|
|
|
const dropButton = (file: HopyoView): HTMLButtonElement =>
|
|
createButton({
|
|
label: "지우기",
|
|
variant: "danger",
|
|
size: "sm",
|
|
onClick: async () => {
|
|
const ask = `실무자료 「${file.파일}」을 지움 — 만든 사전조합 · 별칭 장부는 그대로`;
|
|
if (!(await showConfirmDialog(ask, "지우기"))) return;
|
|
try {
|
|
await deleteHopyo(opt.scope, key);
|
|
showToast("지움", "success");
|
|
opt.onDeleted();
|
|
} catch (error) {
|
|
fail(error);
|
|
}
|
|
},
|
|
});
|
|
|
|
/* ── 호표 하나 — 확인 창 ────────────────────────────────────── */
|
|
function drawOne(no: number): void {
|
|
nav += 1; // 파일 표 늦은 응답 버림
|
|
view.replaceChildren(
|
|
buildHopyoCheck({
|
|
scope: opt.scope,
|
|
key,
|
|
no,
|
|
canWrite: opt.canWrite,
|
|
onBack: () => void drawFile(),
|
|
}),
|
|
);
|
|
}
|
|
|
|
void drawFile();
|
|
return view;
|
|
}
|