feat(est-test): 견적서 작성과 공급자정보 .einf
- 원본 처리 근거대로 레벨 행 선택, 7열, 1쪽 머리·공급자·한글 합계금액, 쪽 나눔 - 공사별 설정 저장, 원본 INI 형식 공급자정보 저장·불러오기, 미리보기·인쇄 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
"""EST 테스트 앱 견적서 작성(원본 TfrmEstiMatePrn — 레벨 행 선택·열·합계금액·쪽 나눔·.einf)."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
FILES = [
|
||||
"native-math.js",
|
||||
"native-formula.js",
|
||||
"native-machine.js",
|
||||
"native-rows.js",
|
||||
"native-text.js",
|
||||
"native-quantity.cjs",
|
||||
"native-wonga.js",
|
||||
"native-engine.js",
|
||||
"native-row-match.js",
|
||||
"native-escalation.js",
|
||||
"native-progress.js",
|
||||
"native-business.js",
|
||||
"native-review.js",
|
||||
"native-resources.js",
|
||||
"native-schedule.js",
|
||||
"native-model.js",
|
||||
"native-verify.js",
|
||||
"native-docs.js",
|
||||
"native-estimate-print.js",
|
||||
]
|
||||
|
||||
|
||||
def run_js(code: str):
|
||||
runner = f"""
|
||||
const fs = require('fs'), vm = require('vm');
|
||||
const ctx = vm.createContext({{ structuredClone, console }});
|
||||
for (const f of {json.dumps(FILES)})
|
||||
vm.runInContext(fs.readFileSync('tools/est-test/' + f, 'utf8'), ctx, {{ filename: f }});
|
||||
ctx.source = JSON.parse(fs.readFileSync('tools/est-test/sample.json', 'utf8'));
|
||||
process.stdout.write(JSON.stringify(vm.runInContext({json.dumps(code)}, ctx)));
|
||||
"""
|
||||
return json.loads(subprocess.check_output(["node", "-e", runner], cwd=ROOT).decode("utf-8"))
|
||||
|
||||
|
||||
def test_rows_total_pages_and_ini():
|
||||
result = run_js(
|
||||
"""(() => {
|
||||
const p = EstNative.calculate(EstModel.validate(structuredClone(source)));
|
||||
const all = EstEstimatePrint.rows(p, 0).length, est = p.tables.EstMainDat.length;
|
||||
const lvl = EstEstimatePrint.rows(p, 2);
|
||||
const okLevel = lvl.every((r) => Number(r.Level) >= 2 || [54, 55, 58].includes(Number(r.LinkIndex)));
|
||||
const pages = EstEstimatePrint.pages(Array.from({ length: 70 }, (_, i) => i), 27, 36).map((x) => x.length);
|
||||
const s = { ...EstEstimatePrint.settings(p), ComPany_Name: '(주)시험', Company_Show: false, F_PageCount: 20 };
|
||||
const ini = EstEstimatePrint.toIni(s);
|
||||
const back = EstEstimatePrint.fromIni(ini);
|
||||
const html = EstEstimatePrint.html(p, { ...s, Level: 2 });
|
||||
let bad = ''; try { EstEstimatePrint.pages([1], 0, 1); } catch (e) { bad = e.message; }
|
||||
return { all: all === est, okLevel, some: lvl.length > 0 && lvl.length < est, pages,
|
||||
total: EstEstimatePrint.totalText(EstEstimatePrint.total(p)),
|
||||
ini: ini.includes('[EstiPrnCompanyInfo]') && ini.includes('ComPany_Name=(주)시험') && ini.includes('Company_Show=0'),
|
||||
back: [back.ComPany_Name, back.Company_Show, back.F_PageCount, back.Project_Name === undefined],
|
||||
html: [html.includes('견 적 서'), html.includes('아래와 같이 견적합니다'), html.includes('공<br>급'), html.includes('공급가액')],
|
||||
bad };
|
||||
})()"""
|
||||
)
|
||||
assert result["all"] is True and result["okLevel"] is True and result["some"] is True
|
||||
assert result["pages"] == [27, 36, 7]
|
||||
assert result["total"] == " 일금:구천칠백일십구만구천원정 (₩97,199,000원정)"
|
||||
assert result["ini"] is True
|
||||
assert result["back"] == ["(주)시험", False, 20, True]
|
||||
assert result["html"] == [True, True, False, True]
|
||||
assert result["bad"] == "쪽당 줄 수는 1 이상입니다."
|
||||
@@ -484,6 +484,9 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
||||
- [x] 수량산출서 검색·전체보기(원본 TFRMMULLANG btnSerch·btnAllDisplay — 빈칸·|·괄호 무시 포함 검색) — 고르기 줄을 `native-quantity-sheets.js` 로 옮겨 700줄 제한 유지.
|
||||
- 검증완료: `test_est_quantity_sheets.py` 검색 시험 포함 수량 시험 22개 통과. ORCA 브라우저에서 「집수」 검색 → 항목에 집수정이 있는 「배수」만·선택 이동, 전체보기 → 3개 복귀, 샘플 복구.
|
||||
- [ ] 원가 행 자리수(Diget·DigetIndex) 반영 — 웹 원가 엔진은 행 자리수를 계산에 쓰지 않음(선택항목 자리수 조정은 값만 기록). 설치 자료 중 sample.estx 만 Diget 사용(도급액 3·산업안전 3, DigetIndex 0)이고 산업안전 1,660,273 처럼 천단위가 아닌 값이 있어 DigetIndex 0 의 뜻(사사오입/없음) 확정 불가 — 근거 확보 전 엔진 변경 보류. 기성 원가계산서 편집(절하·한줄 삽입 등)도 같은 근거가 필요해 보류.
|
||||
- [x] 견적서 작성(원본 mnuEstiMatePrn → TfrmEstiMatePrn 0x10c1994·그리기 0x10c4768 — 디스어셈블 근거) — 공사별 설정(`project.estimatePrint`), 원본 `.einf` INI 세 구역 저장·두 구역 불러오기, 레벨 행 선택, 7열, 1쪽 머리·공급자·합계금액(아래에서 위로 Checked 금액, 한글), 첫/나머지 쪽 줄수, 미리보기·인쇄. 수탁공사변환·XML 역변환 비교는 원본에서 숨긴 개발용 메뉴라 범위 밖.
|
||||
- 검증완료: `test_est_estimate_print.py`(전체/레벨 2 행, 쪽 27·36·7, 합계 「일금:구천칠백일십구만구천원정 (₩97,199,000원정)」, INI 왕복, 미리보기 글) 통과. ORCA 브라우저에서 상호·레벨 2 → 2쪽·28줄·합계·공급자 표, 공사 설정 저장 확인 후 샘플 복구.
|
||||
- 남음: 견적서 Excel 변환, 글꼴·크기·여백 세부값.
|
||||
- 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133.
|
||||
- [ ] 남은 핵심(실무 빈도순):
|
||||
3. 수량산출서 — 누계·4방향 합계, 제출용 전체 인쇄.
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
`편집 → 선택 행 자재단가 등록`·`견적단가 등록`은 원본처럼 내역·일위대가 행을 새 자재(M)·견적(W) 단가로 등록하고 그 행을 새 코드에 연결합니다. 같은 품명·규격이 있으면 거부하고, 일위대가 호표 제목 줄은 등록하지 않습니다. `품명정리`는 고른 일위대가 호표의 품명을 커서 자리에서 나눠 뒤쪽을 규격 앞에 붙이고, 단위가 비어 있으면 채웁니다.
|
||||
|
||||
`파일 → 견적서 작성`은 원본처럼 제목·공사명·발주처·견적일자와 공급자(등록번호·상호·대표자·주소·업태·종목·전화)를 넣고, 설계내역 레벨(0 = 전체, N = N 이상 공종과 순공사비·비율 줄)·첫/나머지 페이지 줄수·정렬·비고란을 정해 미리보기·인쇄합니다. 합계금액은 원가계산서 아래에서 위로 첫 체크된 금액을 `일금:…원정 (₩…원정)`으로 적습니다. 공급자정보는 원본과 같은 `.einf`(INI) 파일로 저장·불러오기 합니다. 설정은 공사마다 저장됩니다.
|
||||
|
||||
`파일 → 갑지·표지·공사설명서·시방서 편집`은 원본 ESTX의 문서 레코드(DatGabgi·DatCover·DatExplean·DatSiBang)를 읽어 공사명·공사위치·발주처·설계일자·공사개요·공사금액·결재란, 표지의 부제·설계년도를 고칩니다. 공사명은 공사 이름과 함께 바뀝니다. `공사금액 만들기`는 원가계산서에서 고른 비목을 `금 일천오백만원(₩15,000,000)` 또는 `금15,000,000원(금일천오백만원)` 방식으로 적습니다. 설명서·시방서에서는 갑지의 공사명·위치·개요·금액을 커서 자리에 붙입니다. 인쇄 서식 8·9·10·11 미리보기는 이 내용을 씁니다. 서식·여백·글꼴은 다루지 않고, 문서를 고친 작업은 ESTX 저장을 거부합니다.
|
||||
|
||||
`편집 → 통합 검색`은 원본 통합 검색처럼 일위대가·단가산출·기계경비·견적·실적·자재·노임 중 고른 자료를 검색어로 찾습니다. 검색 위치는 이 앱에 저장된 다른 작업입니다. 찾은 자료를 고르면 하위 연결 자료와 함께 현재 공사로 가져오고(같은 코드는 현재 자료 유지), 단가 고르기로 행에 넣습니다.
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
<script defer src="native-multi-search.js"></script>
|
||||
<script defer src="native-docs.js"></script>
|
||||
<script defer src="native-wonga-tools.js"></script>
|
||||
<script defer src="native-estimate-print.js"></script>
|
||||
<script defer src="native-actions.js"></script>
|
||||
<script defer src="native-business-actions.js"></script>
|
||||
<script defer src="native-app.js"></script>
|
||||
@@ -85,6 +86,7 @@
|
||||
<button data-action="export-estx">원본 구조 유지 ESTX 수정 저장</button>
|
||||
<button data-action="blank-save">공내역 파일로 저장</button>
|
||||
<hr />
|
||||
<button data-action="estimate-print">견적서 작성</button>
|
||||
<button data-action="doc-edit" data-kind="gabgi">갑지 편집</button
|
||||
><button data-action="doc-edit" data-kind="cover">표지 편집</button
|
||||
><button data-action="doc-edit" data-kind="explan">공사설명서 편집</button
|
||||
|
||||
@@ -613,7 +613,8 @@ const EstBusinessActions = (() => {
|
||||
(typeof EstTidy !== "undefined" && EstTidy.handle(name, state)) ||
|
||||
(typeof EstMultiSearch !== "undefined" && EstMultiSearch.handle(name, state)) ||
|
||||
(typeof EstDocs !== "undefined" && EstDocs.handle(name, state, node)) ||
|
||||
(typeof EstWongaTools !== "undefined" && EstWongaTools.handle(name, state))
|
||||
(typeof EstWongaTools !== "undefined" && EstWongaTools.handle(name, state)) ||
|
||||
(typeof EstEstimatePrint !== "undefined" && EstEstimatePrint.handle(name, state))
|
||||
) {
|
||||
EstApp.render();
|
||||
} else if (name === "load-contract" || name === "load-bid") {
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
// 원본 견적서작성(mnuEstiMatePrn → TfrmEstiMatePrn 0x10c1994, 그리기 0x10c4768).
|
||||
// 설정은 공사마다 project.estimatePrint(원본 EstiPrintInfo.einf 세 구역), 공급자정보는 원본과 같은 INI(.einf)로 내보내기·불러오기
|
||||
// (불러오기는 EstiPrnCompanyInfo·EstiPrnInfo 두 구역만). 행: 레벨 0 = 전 행, N = Level ≥ N 공종 머리 + LinkIndex 54·55·58.
|
||||
// 열: 품명(Pr+Name)·규격·수량·단위·단가·공급가액·비고(선택). 1쪽만 머리·공급자·합계금액, 1쪽 F_PageCount·이후 L_PageCount 줄.
|
||||
// 합계금액 = 원가계산서를 아래에서 위로 훑어 Checked 이고 금액 있는 첫 행(없으면 도급액 55 행) — 「일금: …원정 (₩…원정)」.
|
||||
const EstEstimatePrint = (() => {
|
||||
const num = (value) => Number(value) || 0;
|
||||
const defaults = {
|
||||
Title_Name: "견 적 서",
|
||||
Project_Name: "",
|
||||
Order_name: "",
|
||||
Esti_Date: "",
|
||||
Reg_Num: "",
|
||||
ComPany_Name: "",
|
||||
Ceo_Name: "",
|
||||
Addr: "",
|
||||
Uptae: "",
|
||||
Jongmog: "",
|
||||
Tel: "",
|
||||
Company_Show: true,
|
||||
F_PageCount: 27,
|
||||
L_PageCount: 36,
|
||||
Aligment: 0,
|
||||
Bigo_Show: false,
|
||||
Level: 0,
|
||||
};
|
||||
const sections = {
|
||||
EstiProjectInfo: ["Title_Name", "Project_Name", "Order_name", "Esti_Date"],
|
||||
EstiPrnCompanyInfo: [
|
||||
"Reg_Num",
|
||||
"ComPany_Name",
|
||||
"Ceo_Name",
|
||||
"Addr",
|
||||
"Uptae",
|
||||
"Jongmog",
|
||||
"Tel",
|
||||
"Company_Show",
|
||||
],
|
||||
EstiPrnInfo: ["F_PageCount", "L_PageCount", "Aligment", "Bigo_Show"],
|
||||
};
|
||||
|
||||
function settings(project) {
|
||||
const today = new Date();
|
||||
return {
|
||||
...defaults,
|
||||
Project_Name: project.name || "",
|
||||
Esti_Date: `${today.getFullYear()}년 ${String(today.getMonth() + 1).padStart(2, "0")}월 ${String(today.getDate()).padStart(2, "0")}일`,
|
||||
...(project.estimatePrint || {}),
|
||||
};
|
||||
}
|
||||
|
||||
function rows(project, level) {
|
||||
const list = project.tables.EstMainDat || [];
|
||||
return level > 0
|
||||
? list.filter((r) => num(r.Level) >= level || [54, 55, 58].includes(num(r.LinkIndex)))
|
||||
: list;
|
||||
}
|
||||
|
||||
function total(project) {
|
||||
const wonga = project.tables.Wonga || [];
|
||||
for (let i = wonga.length - 1; i >= 0; i--)
|
||||
if (wonga[i].Checked === true && num(wonga[i].Cost)) return num(wonga[i].Cost);
|
||||
return num(wonga.find((r) => num(r.LinkIndex) === 55)?.Cost ?? project.calculation?.total);
|
||||
}
|
||||
|
||||
function totalText(value) {
|
||||
return ` 일금:${EstDocs.korean(value)}원정 (₩${Math.round(value).toLocaleString("en-US")}원정)`;
|
||||
}
|
||||
|
||||
function pages(list, first, rest) {
|
||||
if (!(first > 0 && rest > 0)) throw Error("쪽당 줄 수는 1 이상입니다.");
|
||||
const out = [list.slice(0, first)];
|
||||
for (let at = first; at < list.length; at += rest) out.push(list.slice(at, at + rest));
|
||||
return out;
|
||||
}
|
||||
|
||||
// 원본 TIniFile 형식(.einf)
|
||||
function toIni(s, keys = Object.keys(sections)) {
|
||||
return keys
|
||||
.map(
|
||||
(section) =>
|
||||
`[${section}]\r\n${sections[section].map((key) => `${key}=${typeof s[key] === "boolean" ? (s[key] ? "1" : "0") : (s[key] ?? "")}`).join("\r\n")}`,
|
||||
)
|
||||
.join("\r\n\r\n");
|
||||
}
|
||||
function fromIni(text, keys = ["EstiPrnCompanyInfo", "EstiPrnInfo"]) {
|
||||
const out = {};
|
||||
let section = "";
|
||||
for (const line of String(text).split(/\r?\n/)) {
|
||||
const head = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
||||
if (head) {
|
||||
section = head[1];
|
||||
continue;
|
||||
}
|
||||
const pair = line.match(/^\s*([^=]+?)\s*=(.*)$/);
|
||||
if (!pair || !keys.includes(section) || !sections[section].includes(pair[1])) continue;
|
||||
const key = pair[1],
|
||||
value = pair[2].trim(),
|
||||
base = defaults[key];
|
||||
out[key] =
|
||||
typeof base === "boolean"
|
||||
? ["1", "true", "-1"].includes(value.toLowerCase())
|
||||
: typeof base === "number"
|
||||
? Number(value) || 0
|
||||
: value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function html(project, s) {
|
||||
const esc = (v) =>
|
||||
String(v ?? "").replace(
|
||||
/[&<>"]/g,
|
||||
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c],
|
||||
);
|
||||
const fmt = (v) => (num(v) ? num(v).toLocaleString("ko-KR", { maximumFractionDigits: 4 }) : "");
|
||||
const lines = (text) => esc(text).split("|").join("<br>");
|
||||
const align = num(s.Aligment) ? "center" : "left";
|
||||
const head = [
|
||||
"품 명",
|
||||
"규 격",
|
||||
"수 량",
|
||||
"단위",
|
||||
"단 가",
|
||||
"공급가액",
|
||||
...(s.Bigo_Show ? ["비 고"] : []),
|
||||
];
|
||||
const table = (list) =>
|
||||
`<table class="esti"><thead><tr>${head.map((h) => `<th>${h}</th>`).join("")}</tr></thead><tbody>${list
|
||||
.map((r) => {
|
||||
const bold =
|
||||
num(r.Level) > 0 || num(r.LinkIndex) === 54 ? ' style="font-weight:bold"' : "";
|
||||
return `<tr${bold}><td style="text-align:${align}">${esc(`${r.Pr ?? ""}${r.Name ?? ""}`)}</td><td style="text-align:${align}">${esc(r.Size)}</td><td class="n">${fmt(r.GN)}</td><td style="text-align:center">${esc(r.Unite)}</td><td class="n">${fmt(r.TU)}</td><td class="n">${fmt(r.TC)}</td>${s.Bigo_Show ? `<td>${esc(typeof r.Bigo === "string" ? r.Bigo : "")}</td>` : ""}</tr>`;
|
||||
})
|
||||
.join("")}</tbody></table>`;
|
||||
const company = s.Company_Show
|
||||
? `<table class="supplier"><tr><th rowspan="7">공<br>급<br>자</th><th>등록번호</th><td>${esc(s.Reg_Num)}</td></tr>${[
|
||||
["상 호", s.ComPany_Name],
|
||||
["성 명", s.Ceo_Name],
|
||||
["사업장주소", s.Addr],
|
||||
["업 태", s.Uptae],
|
||||
["종 목", s.Jongmog],
|
||||
["전화(FAX)", s.Tel],
|
||||
]
|
||||
.map(([k, v]) => `<tr><th>${k}</th><td>${esc(v)}</td></tr>`)
|
||||
.join("")}</table>`
|
||||
: "";
|
||||
const all = pages(rows(project, num(s.Level)), num(s.F_PageCount), num(s.L_PageCount));
|
||||
const sum = total(project);
|
||||
const body = all
|
||||
.map(
|
||||
(list, i) =>
|
||||
`<section class="page">${
|
||||
i === 0
|
||||
? `<h1>${esc(s.Title_Name)}</h1><div class="top"><div><p>공사명: ${lines(s.Project_Name)}</p><p>${lines(s.Order_name)} 귀하</p><p>${esc(s.Esti_Date)}</p><p>아래와 같이 견적합니다</p></div>${company}</div><p class="sum">합계금액 ${esc(totalText(sum))}</p>`
|
||||
: ""
|
||||
}${table(list)}<div class="footer">${i + 1} / ${all.length}</div></section>`,
|
||||
)
|
||||
.join("");
|
||||
return `<!doctype html><html><head><meta charset="utf-8"><style>body{font-family:굴림체,monospace;font-size:10pt}.page{page-break-after:always;padding:10mm}h1{text-align:center;letter-spacing:8px}.top{display:flex;justify-content:space-between;gap:12px}table{border-collapse:collapse;width:100%}th,td{border:1px solid #333;padding:2px 4px}.n{text-align:right}.supplier{width:55%}.sum{font-weight:bold;font-size:12pt;border:1px solid #333;padding:6px}.footer{text-align:center;margin-top:6px}</style></head><body>${body}</body></html>`;
|
||||
}
|
||||
|
||||
// 화면
|
||||
const field = (name) => document.querySelector(`#dialog-content [name="ep-${name}"]`);
|
||||
function open(state) {
|
||||
const s = settings(state.project),
|
||||
{ esc } = EstApp;
|
||||
const input = (k, label, type = "text") =>
|
||||
`<label>${label}<input name="ep-${k}" type="${type}" value="${esc(s[k])}"></label>`;
|
||||
const check = (k, label) =>
|
||||
`<label><input name="ep-${k}" type="checkbox" ${s[k] ? "checked" : ""}>${label}</label>`;
|
||||
EstApp.showDialog(
|
||||
"견적서 작성",
|
||||
`<div class="form-fields">${input("Title_Name", "제목")}${input("Project_Name", "공사명(| 줄바꿈)")}${input("Order_name", "발주처")}${input("Esti_Date", "견적일자")}${input("Level", "설계내역 레벨표시(0~9)", "number")}${input("F_PageCount", "첫 페이지 줄수", "number")}${input("L_PageCount", "나머지 페이지 줄수", "number")}<label>품명·규격 정렬<select name="ep-Aligment"><option value="0" ${num(s.Aligment) ? "" : "selected"}>왼쪽</option><option value="1" ${num(s.Aligment) ? "selected" : ""}>가운데</option></select></label>${check("Bigo_Show", "비고란 보기")}${check("Company_Show", "공급자 정보 인쇄")}</div><h4>공급자</h4><div class="form-fields">${input("Reg_Num", "등록번호")}${input("ComPany_Name", "상호")}${input("Ceo_Name", "대표자")}${input("Addr", "사업장주소")}${input("Uptae", "업태")}${input("Jongmog", "종목")}${input("Tel", "전화,(FAX)")}</div><button data-action="estimate-print-preview">미리보기</button> <button data-action="estimate-print-print">인쇄</button> <button data-action="estimate-print-save">공급자정보 저장하기(.einf)</button> <button data-action="estimate-print-load">공급자정보 불러오기</button><iframe id="estimate-print-frame" style="width:100%;height:420px;border:1px solid #ccc;margin-top:8px"></iframe>`,
|
||||
);
|
||||
}
|
||||
function read(state) {
|
||||
const s = settings(state.project);
|
||||
for (const key of Object.keys(defaults)) {
|
||||
const node = field(key);
|
||||
if (!node) continue;
|
||||
s[key] =
|
||||
node.type === "checkbox"
|
||||
? node.checked
|
||||
: typeof defaults[key] === "number"
|
||||
? Number(node.value) || 0
|
||||
: node.value;
|
||||
}
|
||||
if (!(s.Level >= 0 && s.Level <= 9)) throw Error("레벨은 0~9 입니다.");
|
||||
if (JSON.stringify(s) !== JSON.stringify(settings(state.project)))
|
||||
EstApp.change((project) => (project.estimatePrint = s), false);
|
||||
return s;
|
||||
}
|
||||
function preview(state) {
|
||||
const s = read(state),
|
||||
frame = EstApp.byId("estimate-print-frame");
|
||||
frame.srcdoc = html(state.project, s);
|
||||
return frame;
|
||||
}
|
||||
|
||||
function handle(name, state) {
|
||||
if (name === "estimate-print") open(state);
|
||||
else if (name === "estimate-print-preview") preview(state);
|
||||
else if (name === "estimate-print-print") {
|
||||
const frame = preview(state);
|
||||
frame.onload = () => frame.contentWindow.print();
|
||||
} else if (name === "estimate-print-save")
|
||||
EstApp.download("견적서 기본정보.einf", toIni(read(state)), "text/plain");
|
||||
else if (name === "estimate-print-load") {
|
||||
const picker = document.createElement("input");
|
||||
picker.type = "file";
|
||||
picker.accept = ".einf,.ini,.txt";
|
||||
picker.addEventListener("change", async () => {
|
||||
const file = picker.files[0];
|
||||
if (!file) return;
|
||||
const bytes = new Uint8Array(await file.arrayBuffer());
|
||||
let text;
|
||||
try {
|
||||
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
||||
} catch {
|
||||
text = new TextDecoder("euc-kr").decode(bytes);
|
||||
}
|
||||
const next = { ...read(state), ...fromIni(text) };
|
||||
EstApp.change((project) => (project.estimatePrint = next), false);
|
||||
open(state);
|
||||
});
|
||||
picker.click();
|
||||
} else return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return { settings, rows, total, totalText, pages, toIni, fromIni, html, handle };
|
||||
})();
|
||||
@@ -75,6 +75,7 @@ const allowed = new Set([
|
||||
"native-multi-search.js",
|
||||
"native-docs.js",
|
||||
"native-wonga-tools.js",
|
||||
"native-estimate-print.js",
|
||||
"native-issues.js",
|
||||
"native-attachments.js",
|
||||
"native-standard.js",
|
||||
|
||||
Reference in New Issue
Block a user