- 영월 꼴(STmate 엑셀 내역 줄 = 호표 코드만) = 일위대가표 같은 코드 호표 번 채움 → 서고 사본 21 · 이음 34 - 이은 줄 차가 비목마다 1 원/단위 안 = 「끝수」(STmate 하위 호표 반올림) · 끝 알림 한 줄 「엑셀 합계와 차 n 원 = 하위 호표 반올림 끝수 m 줄」(브레인 정함 = 끝수 줄도 이음) - 요율 못 읽은 공사(.estx · .STC) = 총공사비 칸 안 보임 - 시험 test_707_7 11(영월 끝수 · 일위대가 시트 없음 까닭) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
498 lines
21 KiB
TypeScript
498 lines
21 KiB
TypeScript
/* =============================================================================
|
|
* B09_Estimation_UI_Import.ts
|
|
* B09 설계내역서 [엑셀 내역 불러오기](PLAN 707-1) — 경쟁사 내역 엑셀(STmate · EST) 줄을 화면 캐시로.
|
|
*
|
|
* 누름 = 파일 고름 → 서버 읽기 · 미리 보기(`POST …/estimation/import/bill`) → 창 하나에서 한 번 고름
|
|
* 수량 갈래(가 넘겨받기 · 나 단가만) · 이름 같은 자동 줄에 붙임(두 번 셈 막음) · 남는 자동 줄 지움
|
|
* [불러오기] = `POST …/import/bill/apply` → 받은 원가 자료 · 새 캐시(내역 덮개 · 조달수수료율) → [저장]해야 남음
|
|
* 어긋난 줄(엑셀 금액 ≠ 우리 금액) = 같은 창 목록(줄 · 엑셀 · 우리 · 까닭) · 없으면 창 닫고 알림
|
|
* DRM 파일 = 서버 400 「읽을 수 없음」 알림
|
|
*
|
|
* 경쟁사 공사 통째(PLAN 707-7) = 같은 창에 불러올 것 체크(기본 다 켬 · 못 읽는 것 흐리게 + 까닭) —
|
|
* 일위대가 · 단가산출(이 공사 서고 사본 · 층 쓰기 0) · 내역 줄(늘) · 원가계산서 요율 · 단가표 →
|
|
* [불러오기] 한 번 = 서버가 차례대로(apply 의 hopyo · rates · prices) → 지난번 이음(707-4) · 남은 것만 잇기 창 →
|
|
* 끝 알림 한 장(`report` — 호표 · 내역 줄 · 직접비 · 총공사비 엑셀 = 우리 · 어긋난 줄 · 다음 걸음 ·
|
|
* 줄 어긋남 없으면 알림 한 줄)
|
|
*
|
|
* 요율 · 기초 단가표 불러오기(PLAN 707-2 · 707-3) — 저장 안 함.
|
|
* 단추 = 보조 탭 줄 도구 자리(`…_UI_TabBar` setTools) — 총괄 · 원가계산서 탭 [엑셀에서 요율 불러오기] ·
|
|
* 노무비목록표 · 중기목록표 · 자재단가대비표 탭 [엑셀에서 단가표 불러오기]
|
|
* 길 = `B09_Estimation_Router_Import.py` import/rates · import/prices(file | file_key · user = 화면 캐시) →
|
|
* 확인 창(짝 표 · 식 다름 · 짝 없음 n) → [적용] = 답 `patch` 를 캐시에 얹음 · [저장] 때 서버 정본
|
|
* 올린 파일 = 내역 줄 · 요율 · 단가표 세 길이 같은 표지(file_key · `last`) — 같은 파일 다시 안 올림
|
|
*
|
|
* EST 원본(.estx · PLAN 707-5) = 같은 단추 · 같은 창 — 내역 줄 + 단가표 ·
|
|
* 일위대가 · 요율 = 흐림 「같은 공사 엑셀에서」 · 호표 목록 = 수만(이음 후보 · 구성 줄 못 읽음)
|
|
* STmate 원본(.STC · PLAN 707-6) = .estx 와 같은 자리 · 안 읽은 몫(다공구 · 변경 차수) = 창 알림 줄
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
import { createCheckbox } from "@ui/ui_template_checkbox";
|
|
import { createDataTable, type DataColumn, type DataRow } from "@ui/ui_template_data_table";
|
|
import { pickFiles } from "@ui/ui_template_drop";
|
|
import {
|
|
createButton,
|
|
el,
|
|
hideLoadingOverlay,
|
|
showLoadingOverlay,
|
|
showToast,
|
|
} from "@ui/ui_template_elements";
|
|
import { openModal } from "@ui/ui_template_modal";
|
|
import { createText } from "@ui/ui_template_text";
|
|
import { currentProjectId } from "../A00_Common/b_project_switch";
|
|
import type { Row } from "./B09_Estimation_Calc_Base";
|
|
import { openBillLink } from "./B09_Estimation_UI_Link";
|
|
import { money, qty, text, type TabContext, type TabId } from "./B09_Estimation_UI_Tabs";
|
|
|
|
const won = (v: unknown): string => Number(v || 0).toLocaleString("ko-KR");
|
|
|
|
async function post(projectId: string, path: string, body: BodyInit, json = false): Promise<Row> {
|
|
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/estimation/import/${path}`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
...(json ? { headers: { "Content-Type": "application/json" } } : {}),
|
|
body,
|
|
});
|
|
const got = (await response.json().catch(() => null)) as Row | null;
|
|
if (!response.ok || !got) {
|
|
const detail = got?.detail;
|
|
throw new Error(typeof detail === "string" ? detail : `HTTP ${response.status}`);
|
|
}
|
|
return got;
|
|
}
|
|
|
|
/** 받은 캐시 = 지금 캐시 + 내역 덮개 · 원가계산서 값(조달수수료율 · 요율) · 단가표 덮개(707-7) */
|
|
function merged(user: Row, made: Row): Row {
|
|
const keep = ["labor_cover", "machine_cover", "materials"].filter((k) => k in made);
|
|
return {
|
|
...user,
|
|
bill: made.bill,
|
|
estimate: { ...(user.estimate || {}), ...(made.estimate || {}) },
|
|
...Object.fromEntries(keep.map((k) => [k, made[k]])),
|
|
};
|
|
}
|
|
|
|
function differences(rows: Row[]): HTMLElement {
|
|
return createDataTable({
|
|
columns: [
|
|
{ key: "행", label: "엑셀 행", kind: "number", width: 48 },
|
|
{ key: "이름", label: "명칭", width: 150 },
|
|
{ key: "엑셀", label: "엑셀 금액", kind: "number", width: 96 },
|
|
{ key: "우리", label: "우리 금액", kind: "number", width: 96 },
|
|
{ key: "까닭", label: "까닭", width: 130 },
|
|
],
|
|
rows,
|
|
}).root;
|
|
}
|
|
|
|
/** 끝 알림 한 장(707-7) — 호표 · 내역 줄 · 엑셀 합계 = 우리 합계 · 어긋난 줄 · 다음 걸음 ·
|
|
* 줄 어긋남 없으면 알림 한 줄(누름 0) */
|
|
function report(got: Row, user: () => Row, done: (payload: Row, user: Row) => void): void {
|
|
const n = got.알림 || {};
|
|
const step: Row = got.단계 || {};
|
|
const sum: Row = got.합계 || {};
|
|
const same = (a: unknown, b: unknown): boolean =>
|
|
typeof a === "number" && typeof b === "number" && Math.abs(a - b) < 1;
|
|
const pair = (name: string, excel: unknown, ours: unknown): string =>
|
|
excel == null
|
|
? `${name} 우리 ${won(ours)}`
|
|
: same(excel, ours)
|
|
? `${name} 엑셀 = 우리 ${won(ours)}`
|
|
: `${name} 엑셀 ${won(excel)} · 우리 ${won(ours)}`;
|
|
const part = (name: string, s: Row | undefined, text: (s: Row) => string): string | null =>
|
|
!s ? null : s.까닭 ? `${name} — ${s.까닭}` : `${name} ${text(s)}`;
|
|
const hp: Row | undefined = step.일위대가;
|
|
const lines = [
|
|
part(
|
|
"일위대가",
|
|
hp,
|
|
(s) =>
|
|
`호표 ${s.호표} · 줄 ${s.줄}(고정 ${s.고정} · 하위 ${s.호표참조} · 빠짐 ${s.빠짐}) · 이 공사 서고 사본 ${s.사본}`,
|
|
),
|
|
`내역 줄 새 ${n.실음} · 붙임 ${n.붙임} · 지움 ${n.지움} · 일위대가 이음 ${n.이음}`,
|
|
part(
|
|
"요율",
|
|
step.요율,
|
|
(s) => `${s.줄} 줄${s.다름?.length ? ` · 식 다름 ${s.다름.join(", ")}` : ""}`,
|
|
),
|
|
part("단가표", step.단가표, (s) =>
|
|
Object.entries(PART)
|
|
.map(([k, name]) => `${name} 이음 ${s[k]?.linked ?? 0}`)
|
|
.join(" · "),
|
|
),
|
|
pair("직접비", sum.엑셀직접, sum.우리직접) +
|
|
(sum.우리총 == null ? "" : ` · ${pair("총공사비", sum.엑셀총, sum.우리총)}`), // 요율 없음 = 셈 없음
|
|
].filter((t): t is string => !!t);
|
|
const off: Row[] = got.어긋남 || [];
|
|
const tail = off.filter((x) => x.까닭 === "끝수").length; // 이은 줄 1 원/단위 안 = 하위 호표 반올림
|
|
if (tail) {
|
|
const gap = Math.abs(Number(sum.엑셀직접 || 0) - Number(sum.우리직접 || 0));
|
|
lines.push(`엑셀 합계와 차 ${won(gap)} 원 = 하위 호표 반올림 끝수 ${tail} 줄(줄당 1 원/단위)`);
|
|
}
|
|
const other: Row[] = hp?.단가다름 || [];
|
|
const next =
|
|
"다음 — 붙인 고정 줄은 펼친 일위대가에서 이 공사 단가로 다시 잇기 · 우리 공종 잇기는 줄 우클릭 · [저장]해야 남음";
|
|
const clean = !off.length && !other.length && same(sum.엑셀직접, sum.우리직접);
|
|
if (clean) {
|
|
const tone = sum.엑셀총 == null || same(sum.엑셀총, sum.우리총) ? "success" : "warning";
|
|
return void showToast(`${lines.join(" · ")} — [저장]해야 남음`, tone, 9000);
|
|
}
|
|
openModal({
|
|
title: "경쟁사 공사 불러오기 — 결과",
|
|
closeLabel: "닫기",
|
|
width: "40rem",
|
|
mount: (box, close, actions) => {
|
|
const link = createButton({
|
|
label: "우리 공종으로 잇기",
|
|
variant: "ghost",
|
|
size: "sm",
|
|
onClick: () => {
|
|
close();
|
|
void openBillLink(user(), done);
|
|
},
|
|
});
|
|
actions.append(link);
|
|
box.append(
|
|
...lines.map((t) => createText(t, "muted", 14)),
|
|
...(other.length
|
|
? [
|
|
createText(
|
|
`내역 단가 ≠ 일위대가표 단가 ${other.length} 줄 — 내역 단가 그대로(잇지 않음)`,
|
|
"warning",
|
|
14,
|
|
),
|
|
createDataTable({
|
|
columns: [
|
|
{ key: "행", label: "엑셀 행", kind: "number", width: 48 },
|
|
{ key: "이름", label: "명칭", width: 150 },
|
|
{ key: "번", label: "호표", kind: "number", width: 48 },
|
|
{ key: "내역", label: "내역 단가", kind: "number", width: 96 },
|
|
{ key: "일위대가", label: "일위대가표", kind: "number", width: 96 },
|
|
],
|
|
rows: other.map((x, i) => ({ key: String(i), ...x })),
|
|
}).root,
|
|
]
|
|
: []),
|
|
...(off.length
|
|
? [
|
|
createText(
|
|
`엑셀 금액과 다른 줄 ${off.length}${tail ? `(끝수 ${tail})` : ""}`,
|
|
"warning",
|
|
14,
|
|
),
|
|
differences(off),
|
|
]
|
|
: []),
|
|
createText(next, "muted", 14),
|
|
);
|
|
},
|
|
});
|
|
}
|
|
|
|
/** [엑셀 내역 불러오기] 단추 — user = 지금 화면 캐시 · done = 받은 원가 자료 + 새 캐시 */
|
|
export function createBillImport(
|
|
projectId: string,
|
|
user: () => Row,
|
|
done: (payload: Row, user: Row) => void,
|
|
): HTMLButtonElement {
|
|
const open = (seen: Row, mode: { value: string }): void => {
|
|
const total = seen.합계 || {};
|
|
const raw = ({ estx: ".estx", stc: ".STC" } as Record<string, string>)[seen.꼴];
|
|
const lines = [
|
|
`${seen.파일} · ${seen.꼴} 「${seen.시트}」 · 공종 ${seen.묶음} · 항목 ${seen.항목}`,
|
|
`${raw ?? "엑셀"} 직접비 합 ${won(total.직접)} 원 · 제외(관급 등) ${won(total.제외)} 원` +
|
|
(seen.조달수수료율 != null ? ` · 조달수수료율 ${seen.조달수수료율} %` : ""),
|
|
...(seen.호표
|
|
? [
|
|
`호표 목록 대가 ${seen.호표.대가} · 산근 ${seen.호표.산근} — 내역 줄 ${seen.호표.가리킴} 개가 코드로 가리킴(이음 후보 · 구성 줄은 못 읽음)`,
|
|
]
|
|
: []),
|
|
...((seen.알림 as string[] | undefined) ?? []),
|
|
];
|
|
// 707-7 불러올 것 — 기본 다 켬 · 못 읽는 것 = 흐리게 + 까닭
|
|
const later = raw && `${raw} 에서 못 읽음 — 같은 공사 엑셀에서`;
|
|
const hopyo = createCheckbox(
|
|
later
|
|
? `일위대가 · 단가산출 — 구성 줄은 ${later}`
|
|
: "일위대가 · 단가산출 — 이 공사 서고 사본으로(내 자료집 · 회사에 안 씀)",
|
|
{ checked: !later, disabled: !!later },
|
|
);
|
|
const billLines = createCheckbox(`내역 줄 — 공종 ${seen.묶음} · 항목 ${seen.항목}(늘 불러옴)`, {
|
|
checked: true,
|
|
disabled: true,
|
|
});
|
|
const rates = createCheckbox(
|
|
later ? `원가계산서 요율 — ${later}` : "원가계산서 요율 — 율 덮개",
|
|
{ checked: !later, disabled: !!later },
|
|
);
|
|
const prices = createCheckbox("단가표 — 노임 · 자재 · 기계 단가를 이 공사 단가 책에 덮음", {
|
|
checked: true,
|
|
});
|
|
const take = createCheckbox("넘겨받기 — 불러온 수량을 씀(수량 산출 값은 곁에)", {
|
|
radio: "b09-import-mode",
|
|
value: "take",
|
|
checked: mode.value === "take",
|
|
});
|
|
const price = createCheckbox(
|
|
"단가만 — 수량 산출 수량을 씀(불러온 수량은 비교 · 차이 큰 줄 붉게)",
|
|
{
|
|
radio: "b09-import-mode",
|
|
value: "price",
|
|
checked: mode.value === "price",
|
|
},
|
|
);
|
|
const attach = createCheckbox(`이름이 같은 자동 줄 ${seen.같은이름} 개에 붙임(두 번 셈 막음)`, {
|
|
checked: seen.같은이름 > 0,
|
|
disabled: !seen.같은이름,
|
|
});
|
|
const drop = createCheckbox(`그 밖 자동 줄 ${seen.남는자동} 개 지움`, {
|
|
disabled: !seen.남는자동,
|
|
});
|
|
const run = createButton({ label: "불러오기", variant: "filled" });
|
|
run.dataset.b09ImportRun = "";
|
|
openModal({
|
|
title: "경쟁사 공사 불러오기",
|
|
closeLabel: "닫기",
|
|
width: "40rem",
|
|
mount: (body, close, actions) => {
|
|
body.append(
|
|
...lines.map((t) => createText(t, "muted", 14)),
|
|
el("div", {
|
|
className: "b09-import__choices",
|
|
children: [hopyo.root, billLines.root, rates.root, prices.root],
|
|
}),
|
|
el("div", { className: "b09-import__choices", children: [take.root, price.root] }),
|
|
el("div", { className: "b09-import__choices", children: [attach.root, drop.root] }),
|
|
);
|
|
actions.append(run);
|
|
run.addEventListener("click", async () => {
|
|
mode.value = price.input.checked ? "price" : "take";
|
|
const draft = user();
|
|
const ask = {
|
|
file_key: seen.file_key,
|
|
mode: mode.value,
|
|
attach: attach.input.checked,
|
|
drop: drop.input.checked,
|
|
hopyo: hopyo.input.checked,
|
|
rates: rates.input.checked,
|
|
prices: prices.input.checked,
|
|
user: draft,
|
|
};
|
|
showLoadingOverlay();
|
|
let got: Row;
|
|
try {
|
|
got = await post(projectId, "bill/apply", JSON.stringify(ask), true);
|
|
} catch (error) {
|
|
const why = error instanceof Error ? error.message : error;
|
|
return void showToast(`불러오지 못함 — ${why}`, "error");
|
|
} finally {
|
|
hideLoadingOverlay();
|
|
}
|
|
// 창 닫고 다시 그림 — 표 높이(fillViewport)가 열린 창 가림막을 아래 자리로 셈
|
|
close();
|
|
done(got.payload, merged(draft, got.user));
|
|
await openBillLink(user(), done, mode.value, true); // 707-4 지난번 이음 바로 · 남은 것만 창
|
|
requestAnimationFrame(() => requestAnimationFrame(() => report(got, user, done)));
|
|
});
|
|
},
|
|
});
|
|
};
|
|
const mode = { value: "take" };
|
|
const button = createButton({
|
|
label: "엑셀 내역 불러오기",
|
|
variant: "ghost",
|
|
onClick: () =>
|
|
pickFiles({ accept: ".xls,.xlsx,.estx,.stc" }, ([file]) => {
|
|
const form = new FormData();
|
|
form.append("file", file);
|
|
form.append("user", JSON.stringify(user()));
|
|
showLoadingOverlay();
|
|
post(projectId, "bill", form)
|
|
.then((seen) => {
|
|
last = { key: String(seen.file_key), name: String(seen.파일) }; // 요율 · 단가표가 다시 씀
|
|
open(seen, mode);
|
|
})
|
|
.catch((error: unknown) =>
|
|
showToast(`읽지 못함 — ${error instanceof Error ? error.message : error}`, "error"),
|
|
)
|
|
.finally(hideLoadingOverlay);
|
|
}),
|
|
});
|
|
button.dataset.b09Import = "";
|
|
return button;
|
|
}
|
|
|
|
/* ── 707-2 · 3 요율 · 기초 단가표 ── */
|
|
|
|
type Kind = "rates" | "prices";
|
|
const TABS: Partial<Record<TabId, Kind>> = {
|
|
total: "rates",
|
|
cost: "rates",
|
|
labor: "prices",
|
|
machine: "prices",
|
|
compare: "prices",
|
|
};
|
|
const LABEL: Record<Kind, string> = {
|
|
rates: "엑셀에서 요율 불러오기",
|
|
prices: "엑셀에서 단가표 불러오기",
|
|
};
|
|
const PART: Record<string, string> = { labor: "노임", material: "자재", machine: "기계" };
|
|
|
|
/** 마지막에 올린 파일(서버 기억 열쇠) — 같은 파일 다시 씀 */
|
|
let last: { key: string; name: string } | null = null;
|
|
|
|
async function postSheet(kind: Kind, user: Row, file: File | null): Promise<Row> {
|
|
const form = new FormData();
|
|
if (file) form.append("file", file);
|
|
else if (last) form.append("file_key", last.key);
|
|
form.append("user", JSON.stringify(user));
|
|
const url = `${API_BASE_URL}/projects/${currentProjectId()}/estimation/import/${kind}`;
|
|
const response = await fetch(url, { method: "POST", body: form, credentials: "include" });
|
|
const body = (await response.json().catch(() => null)) as Row | null;
|
|
if (!response.ok) {
|
|
if (response.status === 404 && !file) last = null; // 지난 파일 = 다시 고름
|
|
throw new Error(typeof body?.detail === "string" ? body.detail : `HTTP ${response.status}`);
|
|
}
|
|
last = { key: String(body!.file_key), name: String(body!.name) };
|
|
return body!;
|
|
}
|
|
|
|
const wonOrBlank = (v: unknown): string =>
|
|
typeof v === "number" ? Math.round(v).toLocaleString("en-US") : "";
|
|
|
|
function ratesView(got: Row): { table: DataRow[]; lines: string[]; columns: DataColumn[] } {
|
|
const check: Row[] = got.check?.lines || [];
|
|
const ours = Object.fromEntries(check.map((ln) => [ln.key, ln]));
|
|
const table = (got.rows as Row[])
|
|
.filter((r) => r.key && r.key !== "subtotal")
|
|
.map((r, i) => ({
|
|
key: String(i),
|
|
name: r.name,
|
|
rate: r.rate,
|
|
excel: r.amount,
|
|
ours: ours[r.key]?.ours ?? null,
|
|
// 산업안전보건관리비 A · B 두 줄(EST) = 작은 쪽만 우리 식과 견줌
|
|
same: !ours[r.key]
|
|
? ""
|
|
: Math.abs((r.amount ?? NaN) - ours[r.key].ours) < 1
|
|
? "같음"
|
|
: ours[r.key].same
|
|
? "안 고른 쪽"
|
|
: "식 다름",
|
|
}));
|
|
const grand = got.check?.grand;
|
|
const lines = [
|
|
`율 ${Object.keys(got.rates || {}).length} 줄 실음 · 총공사비 우리 ${wonOrBlank(grand)} / 엑셀 ${wonOrBlank(
|
|
got.check?.excel_grand,
|
|
)}`,
|
|
...(got.notes as string[]),
|
|
];
|
|
const columns = [
|
|
text("name", "행 이름", 180),
|
|
qty("rate", "율(%)", 4),
|
|
money("excel", "엑셀 금액"),
|
|
money("ours", "우리 식"),
|
|
text("same", "대조", 72),
|
|
];
|
|
return { table, lines, columns };
|
|
}
|
|
|
|
function pricesView(got: Row): { table: DataRow[]; lines: string[]; columns: DataColumn[] } {
|
|
const show = (v: unknown): string =>
|
|
v && typeof v === "object"
|
|
? ["labor", "material", "expense"].map((p) => wonOrBlank((v as Row)[p])).join(" / ")
|
|
: wonOrBlank(v);
|
|
const table = (got.linked as Row[]).map((x, i) => ({
|
|
key: String(i),
|
|
part: PART[x.kind] || x.kind,
|
|
from: x.from,
|
|
name: x.name,
|
|
excel: show(x.excel),
|
|
book: show(x.book),
|
|
same: x.same ? "같음" : "덮음",
|
|
}));
|
|
const counts: Row = got.counts || {};
|
|
const lines = Object.entries(PART).map(([k, name]) => {
|
|
const c: Row = counts[k] || {};
|
|
const miss: string[] = got.missing?.[k] || [];
|
|
return `${name} — 읽음 ${c.read ?? 0} · 이음 ${c.linked ?? 0}(덮음 ${c.covered ?? 0}) · 이 공사 줄 ${
|
|
c.book ?? 0
|
|
} · 짝 없음 ${c.missing ?? 0}${miss.length ? `(${miss.slice(0, 8).join(", ")}${miss.length > 8 ? " …" : ""})` : ""}`;
|
|
});
|
|
const columns = [
|
|
text("part", "갈래", 56),
|
|
text("from", "엑셀 이름", 200),
|
|
text("name", "이 공사 줄", 160),
|
|
text("excel", "엑셀 단가", 150),
|
|
text("book", "이 공사 단가", 150),
|
|
text("same", "", 56),
|
|
];
|
|
return { table, lines, columns };
|
|
}
|
|
|
|
function openResult(kind: Kind, ctx: TabContext, got: Row): void {
|
|
const view = kind === "rates" ? ratesView(got) : pricesView(got);
|
|
openModal({
|
|
title: `${LABEL[kind]} — ${got.name}`,
|
|
closeLabel: "닫기",
|
|
width: "960px",
|
|
mount: (body, close, actions) => {
|
|
const apply = createButton({
|
|
label: "적용",
|
|
variant: "filled",
|
|
size: "sm",
|
|
onClick: () => {
|
|
ctx.setUser({ ...ctx.user, ...got.patch });
|
|
showToast("적용 — [저장] 해야 남음", "success");
|
|
close();
|
|
},
|
|
});
|
|
const other = createButton({
|
|
label: "다른 파일",
|
|
variant: "ghost",
|
|
size: "sm",
|
|
onClick: () => {
|
|
close();
|
|
pick(kind, ctx);
|
|
},
|
|
});
|
|
actions.append(other, apply);
|
|
const table = createDataTable({ columns: view.columns, rows: view.table, empty: "" });
|
|
body.append(...view.lines.map((t) => el("p", { text: t })), table.root);
|
|
},
|
|
});
|
|
}
|
|
|
|
const fail = (error: unknown): void => showToast(`불러오지 못함 — ${String(error)}`, "error");
|
|
|
|
function pick(kind: Kind, ctx: TabContext): void {
|
|
pickFiles(
|
|
{ accept: ".xlsx,.xls,.estx,.stc" },
|
|
([file]) =>
|
|
void postSheet(kind, ctx.user, file).then((got) => openResult(kind, ctx, got), fail),
|
|
);
|
|
}
|
|
|
|
/** 탭 도구 자리 단추 — 불러오기 없는 탭 = 빈 목록 */
|
|
export function importTools(tab: TabId, ctx: TabContext): HTMLElement[] {
|
|
const kind = TABS[tab];
|
|
if (!kind) return [];
|
|
const button = createButton({
|
|
label: LABEL[kind],
|
|
variant: "ghost",
|
|
size: "sm",
|
|
onClick: () =>
|
|
last
|
|
? void postSheet(kind, ctx.user, null).then(
|
|
(got) => openResult(kind, ctx, got),
|
|
(error: unknown) => {
|
|
fail(error); // 지난 파일이 .estx · .STC(요율) · 없음 = 까닭 알리고 다시 고름
|
|
pick(kind, ctx);
|
|
},
|
|
)
|
|
: pick(kind, ctx),
|
|
});
|
|
if (last) button.title = `마지막 파일 ${last.name} 로 · 다른 파일은 창에서`;
|
|
return [button];
|
|
}
|