feat(est-test): 엑셀데이타를 EST로 변환 7개 탭
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ for (const file of [
|
||||
"native-extra.js",
|
||||
"native-business-actions.js",
|
||||
"native-forms.js",
|
||||
"native-excel-to-est.js",
|
||||
]) {
|
||||
vm.runInContext(fs.readFileSync(path.join(base, file), "utf8"), context, { filename: file });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""EST 테스트 앱 엑셀데이타를 EST로 변환 7개 탭(원본 TfrmExcelToEst — 셀 규칙·머리 줄 꼴·호표 만들기)."""
|
||||
|
||||
from test_est_rea_parity import run
|
||||
|
||||
|
||||
def test_convert_tabs_and_heads():
|
||||
result = run(
|
||||
"math",
|
||||
r"""(()=>{
|
||||
const X=EstExcelToEst;
|
||||
const sheets={노임단가:[['L1',' 보통인부 ','','M2',172698,'']],
|
||||
견적단가:[['','.펌프','','1','EA','1,500','500','1,000','0','']],
|
||||
표준시장단가:[['C1','포장','t=5','m2',1000,300,600,100,0.3,'포함',''],],
|
||||
단가산출:[['','산근 1 호표 : 거푸집 합판 (1 ㎡ 당)',100,60,40,0],['','합판 1*2',40,0,40,0],['','#.산근2|철근가공|D10|ton',9,9,0,0]],
|
||||
일위대가:[['','No 5 호표 터파기','','','',0,10,0,10,0,0,0,0,''],['','인부','','0.1','인',100,10,100,10,0,0,0,0,''],['','[합계]','','','',0,10,0,0,0,0,0,0,'']]};
|
||||
const cols=(n)=>Array.from({length:n},(_,i)=>String.fromCharCode(65+i));
|
||||
const tabs={LaborcDat:{sheet:'노임단가',start:1,end:1,columns:cols(6)},EstiMateDat:{sheet:'견적단가',start:1,end:1,columns:cols(10)},
|
||||
SilJukDat:{sheet:'표준시장단가',start:1,end:1,columns:cols(11)},CostMainDat:{sheet:'단가산출',start:1,end:3,columns:cols(6)},
|
||||
HeadMainDat:{sheet:'일위대가',start:1,end:3,columns:cols(14)},StuffcDat:{sheet:'없는시트',start:1,end:1,columns:cols(18)}};
|
||||
const r=X.convert(sheets,tabs,{name:'시험',wonga:[]}), t=r.project.tables;
|
||||
return {failed:r.failed, lab:[t.LaborcDat[0].Name,t.LaborcDat[0].Unite,t.LaborcDat[0].TC],
|
||||
est:[t.EstiMateDat[0].Name,t.EstiMateDat[0].TC,t.EstiMateDat[0].OTC,t.EstiMateDat[0].Code.slice(0,1)],
|
||||
sil:[t.SilJukDat[0].NCRatio,t.SilJukDat[0].Cost1,t.SilJukDat[0].ONC],
|
||||
cost:t.CostTitleDat.map(x=>[x.Name,x.Size,x.Unite]), line:t.CostMainDat[1].LineData,
|
||||
head:[t.HeadTitleDat.map(x=>x.Name),t.HeadMainDat.length], rows:t.EstMainDat.length, auto:r.project.options.AutoCalc};
|
||||
})()""",
|
||||
)
|
||||
assert result["failed"] == ["없는시트 시트가 없습니다."]
|
||||
assert result["lab"] == ["보통인부", "㎡", 172698]
|
||||
assert result["est"] == ["펌프", 1500, 1500, "N"]
|
||||
assert result["sil"] == [30, 1000, 300]
|
||||
assert result["cost"] == [["거푸집", "합판", "㎡"], ["철근가공", "D10", "ton"]]
|
||||
assert result["line"] == "합판 1×2"
|
||||
assert result["head"] == [["터파기"], 2]
|
||||
assert result["rows"] == 36
|
||||
assert result["auto"] is False
|
||||
@@ -489,6 +489,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
||||
- 남음: 구성비·요율 전환(CalcBase), 약어 표시(SName), 제비율 기준 문서 보기(원본 한글 파일).
|
||||
- [x] 수량산출서 검색·전체보기(원본 TFRMMULLANG btnSerch·btnAllDisplay — 빈칸·|·괄호 무시 포함 검색) — 고르기 줄을 `native-quantity-sheets.js` 로 옮겨 700줄 제한 유지.
|
||||
- 검증완료: `test_est_quantity_sheets.py` 검색 시험 포함 수량 시험 22개 통과. ORCA 브라우저에서 「집수」 검색 → 항목에 집수정이 있는 「배수」만·선택 이동, 전체보기 → 3개 복귀, 샘플 복구.
|
||||
- [x] 엑셀데이타를 EST로 변환 7개 탭(원본 TfrmExcelToEst 0x109c3bc — spec_pc §1) — 노임·자재·견적·표준시장·단가산출·일위대가·내역서 탭마다 시트명·시작/끝행·필드별 열 글자(.xlsx·.xls), 노임 → 자재 → 견적 → 표준 → 단산 → 일위 → 내역 차례로 새 공사(현재 원가 서식, 자동계산 끔 — Excel 값 그대로). 셀 규칙(Trim·줄바꿈·앞 「.」·㎡/㎥·「*」→「×」·쉼표·노무비율 ×100·「B:D」 이어붙임), 노임 단가 → Cost1·TC, 견적·표준 → O 칸 함께, 빈 코드 N+10자리, 내역 36행 채움. 단가산출·일위대가 머리 줄 꼴로 호표 만들기(코드·TitleCode·Index·LineData 품명/규격/단위 표지, [합계] 줄 지움), 못 읽은 시트 안내. 하지 않음: 글자 굵기 판정·고정폭 머리말·단가산출 목록 시트형(굵기·폭 정보 없음).
|
||||
- 검증완료: `test_est_excel_to_est.py`(없는 시트 안내, 노임 ㎡·TC, 견적 앞점·쉼표·O칸·N코드, 표준 노무비율 30, 단산 호표 2개·×, 일위 호표·[합계] 지움, 36행, 자동계산 끔) 통과. ORCA 브라우저에서 탭 7개·단가산출 열 6칸 표시(파일 고르기는 수동).
|
||||
- [x] 전회기성 불러오기·조달수수료·원가에 내역 금액·원가 구성비(spec_pb1 1·9·10 · spec_pb2 8) — 전회기성: 완료 회차 아무것이나 골라 그 누계를 전회로(현재 회차 거부, `round.junFrom`). 조달수수료 입력(Ctrl+J): 현재 줄을 Code AJJ01·LinkIndex 57·GN %·자리방법 Index·자리수 Gangub·계산 비목 OCheck·저장 칸 SCheck 로(57 줄이면 수정, 허용 검사·원본 문구). 원가계산서에 내역 금액 넣기: 원본은 끌어 놓기 — 웹은 줄 고르기 + 합계/노무/재료/경비 고르기 → 원가 줄 Cost 고정(LinkIndex 1, 덮어쓰기 확인). 구성비 표시: 원가 요율 칸을 「비율」로 바꿔 기준 행(CalcBase 또는 도급액) 대비 대상 비목 Cost 를 0.01% 절사·최대 나머지로 100% 맞춤(계산 불변).
|
||||
- 검증완료: 기성·바인딩 시험 14개 통과. ORCA 브라우저에서 구성비 20줄 합 100.00%·머리 「비율」, 내역 금액 넣기 노무비 384,804, Ctrl+J 조달수수료 AJJ01·57·0.54%·절하, 샘플 복구.
|
||||
- [x] 단가 도구(spec_pb2) — 중복 항목 정리(자재단가: 품명·규격·단위 빈칸·| 무시 같으면 TC 가장 큰 행만, 같으면 뒤 행, 묻지 않음), 단가정의 엑셀변환(표준시장단가 설명문 있는 항목만 「코드 / 번호. 품명 (규격) [단위]」+설명 줄, .xlsx), 할증·낙찰(고른 2행 이상 또는 전 행 Ratio·NachalRatio, 원래 단가 OTC 보관, 100 이면 자리 해제)·복원(두 율 0), 바꾸기(Main Data 로 현재 표 하나만 갱신, 못 찾음 표시), SUM(현재 줄을 항목선택계산 62 줄로: Code 'A'+두 자리 색인·적용율·비목, 원본 거부 문구, 줄 자신 SKind = 묶음 최대+1). 엔진: 62 줄 Code 를 두 자리 색인 번호로 읽음(설치 main.estx 의 「A01」 꼴, 예전 글자 해석은 A0103 의 C 를 놓쳤음).
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<script defer src="native-quantity-list.js"></script>
|
||||
<script defer src="native-price-tools.js"></script>
|
||||
<script defer src="native-misc-tools.js"></script>
|
||||
<script defer src="native-excel-to-est.js"></script>
|
||||
<script defer src="native-actions-more.js"></script>
|
||||
<script defer src="native-wonga.js"></script>
|
||||
<script defer src="native-jungsang.js"></script>
|
||||
@@ -164,7 +165,8 @@
|
||||
><button data-action="jodal-fee">조달수수료 입력(Ctrl+J)</button
|
||||
><button data-action="cost-to-wonga">원가계산서에 내역 금액 넣기</button
|
||||
><button data-action="wonga-biul-mode">원가 구성비·요율 표시 전환</button
|
||||
><button data-action="jun-load">전회기성 불러오기</button>
|
||||
><button data-action="jun-load">전회기성 불러오기</button
|
||||
><button data-action="excel-to-est">엑셀 데이타 => EST 데이타(7개 표)</button>
|
||||
<hr />
|
||||
<button data-action="find">찾기·다음찾기(Ctrl+F)</button>
|
||||
<button data-action="multi-search">통합 검색(저장된 다른 작업)</button>
|
||||
|
||||
@@ -5,6 +5,7 @@ const EstActionsMore = (() => {
|
||||
typeof EstQuantityList !== "undefined" && EstQuantityList,
|
||||
typeof EstPriceTools !== "undefined" && EstPriceTools,
|
||||
typeof EstMiscTools !== "undefined" && EstMiscTools,
|
||||
typeof EstExcelToEst !== "undefined" && EstExcelToEst,
|
||||
].filter(Boolean);
|
||||
function handle(name, state, node) {
|
||||
for (const module of modules()) if (module.handle(name, state, node)) return true;
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
// 원본 「엑셀데이타를 EST로 변환」(mnuExcelToEst → TfrmExcelToEst btnOkClick 0x109c3bc — spec_pc §1) 7개 탭:
|
||||
// 노임 → 자재 → 견적 → 표준시장 → 단가산출 → 일위대가 → 내역서 차례로 한 Excel 파일의 시트들을 새 공사로 옮긴다(금액은 Excel 값 그대로,
|
||||
// 다시 셈 끔). 셀 규칙: 문자 Trim·줄바꿈 제거·품명/규격 앞 「.」 제거·단위 M2→㎡ M3→㎥·산출근거 「*」→「×」, 수치는 쉼표 빼고,
|
||||
// 노무비율 ≤ 1 이면 ×100, 열 「B:D」 는 이어 붙임. 단가산출·일위대가는 머리 줄 꼴(#.산근 · #| · 제n호표 · 산근 n 호표 : … (… 당) ·
|
||||
// (산근 · No n 호표 · No.n)을 찾아 호표를 만든다(원본의 글자 굵기 판정은 웹 Excel 읽기가 굵기를 몰라 하지 않음).
|
||||
const EstExcelToEst = (() => {
|
||||
const TABS = [
|
||||
["LaborcDat", "노임단가", ["Code", "Name", "Size", "Unite", "Cost1", "Bigo"]],
|
||||
[
|
||||
"StuffcDat",
|
||||
"자재단가",
|
||||
[
|
||||
"Code",
|
||||
"Name",
|
||||
"Size",
|
||||
"Unite",
|
||||
"Cost1",
|
||||
"Page1",
|
||||
"Cost2",
|
||||
"Page2",
|
||||
"Cost3",
|
||||
"Page3",
|
||||
"Cost4",
|
||||
"Page4",
|
||||
"Cost5",
|
||||
"Page5",
|
||||
"Cost6",
|
||||
"Page6",
|
||||
"TC",
|
||||
"Bigo",
|
||||
],
|
||||
],
|
||||
[
|
||||
"EstiMateDat",
|
||||
"견적단가",
|
||||
["Code", "Name", "Size", "GN", "Unite", "TC", "NC", "GC", "JC", "Bigo"],
|
||||
],
|
||||
[
|
||||
"SilJukDat",
|
||||
"표준시장단가",
|
||||
["Code", "Name", "Size", "Unite", "TC", "NC", "GC", "JC", "NCRatio", "EXPLEAN", "Bigo"],
|
||||
],
|
||||
["CostMainDat", "단가산출", ["Code", "LineData", "TC", "NC", "GC", "JC"]],
|
||||
[
|
||||
"HeadMainDat",
|
||||
"일위대가",
|
||||
[
|
||||
"Code",
|
||||
"Name",
|
||||
"Size",
|
||||
"GN",
|
||||
"Unite",
|
||||
"TU",
|
||||
"TC",
|
||||
"NU",
|
||||
"NC",
|
||||
"GU",
|
||||
"GC",
|
||||
"JU",
|
||||
"JC",
|
||||
"Bigo",
|
||||
],
|
||||
],
|
||||
[
|
||||
"EstMainDat",
|
||||
"내역서",
|
||||
[
|
||||
"Code",
|
||||
"Pr",
|
||||
"Name",
|
||||
"Size",
|
||||
"GN",
|
||||
"Unite",
|
||||
"TU",
|
||||
"TC",
|
||||
"NU",
|
||||
"NC",
|
||||
"GU",
|
||||
"GC",
|
||||
"JU",
|
||||
"JC",
|
||||
"Bigo",
|
||||
],
|
||||
],
|
||||
];
|
||||
const TEXT = new Set([
|
||||
"Code",
|
||||
"Pr",
|
||||
"Name",
|
||||
"Size",
|
||||
"Unite",
|
||||
"Bigo",
|
||||
"LineData",
|
||||
"EXPLEAN",
|
||||
"Page1",
|
||||
"Page2",
|
||||
"Page3",
|
||||
"Page4",
|
||||
"Page5",
|
||||
"Page6",
|
||||
]);
|
||||
const num = (v) => Number(v) || 0;
|
||||
const letter = (n) => {
|
||||
let s = "";
|
||||
for (n += 1; n > 0; n = Math.floor((n - 1) / 26))
|
||||
s = String.fromCharCode(65 + ((n - 1) % 26)) + s;
|
||||
return s;
|
||||
};
|
||||
const col = (s) =>
|
||||
[...String(s).toUpperCase()].reduce((n, c) => n * 26 + c.charCodeAt(0) - 64, 0) - 1;
|
||||
const unit = (s) =>
|
||||
String(s)
|
||||
.replace(/^[Mm]2$/, "㎡")
|
||||
.replace(/^[Mm]3$/, "㎥");
|
||||
|
||||
function cell(table, r, spec) {
|
||||
const [a, b] = String(spec).toUpperCase().split(":");
|
||||
if (!b) return table[r]?.[col(a)];
|
||||
let out = "";
|
||||
for (let c = col(a); c <= col(b); c++) {
|
||||
const v = table[r]?.[c];
|
||||
if (v != null && String(v) !== "") out += String(v);
|
||||
else if (out.length <= 1) out += " ";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function value(field, raw) {
|
||||
if (TEXT.has(field)) {
|
||||
let t = String(raw ?? "").replace(/\r?\n/g, "");
|
||||
t = field === "LineData" ? t.replaceAll("*", "×") : t.trim();
|
||||
if (field === "Unite") t = unit(t);
|
||||
if (field === "Name" || field === "Size") t = t.replace(/^\.+/, "");
|
||||
return t;
|
||||
}
|
||||
let v = typeof raw === "number" ? raw : Number(String(raw ?? "").replaceAll(",", "")) || 0;
|
||||
if (field === "NCRatio" && v <= 1) v *= 100;
|
||||
return v;
|
||||
}
|
||||
function readRows(table, tab, fields, columns) {
|
||||
const rows = [];
|
||||
for (let r = tab.start - 1; r <= tab.end - 1; r++) {
|
||||
const row = { Index: 0 };
|
||||
fields.forEach((f, i) => {
|
||||
if (columns[i]) row[f] = value(f, cell(table, r, columns[i]));
|
||||
});
|
||||
rows.push(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
// 머리 줄 꼴 → { Name, Size, Unite, skipNext }
|
||||
function costHead(text, next) {
|
||||
const t = String(text ?? "").trim(),
|
||||
split = (s) => {
|
||||
const parts = s.trim().split(/\s+/);
|
||||
return {
|
||||
Name: parts[0] || "",
|
||||
Size: parts.slice(1, -1).join(" "),
|
||||
Unite: parts.length > 1 ? parts.at(-1) : "",
|
||||
};
|
||||
};
|
||||
if (/^#\./.test(t) && t.includes("산근")) {
|
||||
if (t.includes("|")) {
|
||||
const [, n = "", s = "", u = ""] = t.split("|");
|
||||
return { Name: n.trim(), Size: s.trim(), Unite: u.trim() };
|
||||
}
|
||||
return split(t.replace(/^.*?산근\s*\d*/, "").replace(/\//g, " "));
|
||||
}
|
||||
if (/^#/.test(t) && t.includes("|")) {
|
||||
const [, n = "", s = "", u = ""] = t.split("|");
|
||||
return { Name: n.trim(), Size: s.trim(), Unite: u.trim() };
|
||||
}
|
||||
if (/산근.*호표/.test(t) && t.includes("당")) {
|
||||
const body = t.split(/[::]/).slice(1).join(":"),
|
||||
u = /\(\s*[\d.]*\s*([^()]*?)\s*당\s*\)/.exec(body)?.[1] || "",
|
||||
rest = body
|
||||
.replace(/\([^()]*당\s*\)/, "")
|
||||
.trim()
|
||||
.split(/\s+/);
|
||||
return { Name: rest[0] || "", Size: rest.slice(1).join(" "), Unite: u };
|
||||
}
|
||||
if (/제\s*\d+\s*호표/.test(t) && next != null)
|
||||
return { ...split(String(next)), skipNext: true };
|
||||
if (t.replace(/\s/g, "").includes("(산근")) return split(t.replace(/\(산근[^)]*\)/, ""));
|
||||
return null;
|
||||
}
|
||||
function headHead(name) {
|
||||
const t = String(name ?? "").trim();
|
||||
if (/No/.test(t) && t.includes("호표"))
|
||||
return { Name: t.split("호표").slice(1).join("호표").trim(), Size: "", Unite: "" };
|
||||
if (/^No\.\s*[1-9]/.test(t))
|
||||
return { Name: t.replace(/^No\.\s*\d+\s*/, ""), Size: "", Unite: "" };
|
||||
if (/\(호표/.test(t)) {
|
||||
const parts = t
|
||||
.replace(/\(호표[^)]*\)/, "")
|
||||
.trim()
|
||||
.split(/\s+/);
|
||||
return { Name: parts[0] || "", Size: parts.slice(1).join(" "), Unite: "" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function groups(rows, titles, kind, headOf) {
|
||||
let pk = 0;
|
||||
const out = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
if (kind === 103 && String(row.Name ?? "").replace(/\s/g, "") === "[합계]") continue;
|
||||
const h = headOf(row, rows[i + 1]);
|
||||
if (!h) {
|
||||
out.push(row);
|
||||
continue;
|
||||
}
|
||||
const index = titles.length + 1,
|
||||
code = `N${String(++pk).padStart(10, "0")}`;
|
||||
titles.push({
|
||||
Index: index,
|
||||
Code: code,
|
||||
Name: h.Name,
|
||||
Size: h.Size,
|
||||
Unite: unit(h.Unite),
|
||||
TC: num(row.TC),
|
||||
NC: num(row.NC),
|
||||
GC: num(row.GC),
|
||||
JC: num(row.JC),
|
||||
});
|
||||
Object.assign(row, { Code: code, TitleCode: code, Index: index });
|
||||
if (kind === 104)
|
||||
row.LineData = `\x0e${h.Name}\x0e${h.Size ? `\x13${h.Size}\x13` : ""}${h.Unite ? `\x15${unit(h.Unite)}\x15` : ""}`;
|
||||
out.push(row);
|
||||
if (h.skipNext) i++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function convert(sheets, tabs, template) {
|
||||
const project = EstNative.empty();
|
||||
project.name = template.name;
|
||||
project.options = { ...project.options, AutoCalc: false, SourceRefresh: false };
|
||||
project.tables.Wonga = structuredClone(template.wonga || []);
|
||||
const failed = [];
|
||||
for (const [table, label, fields] of TABS) {
|
||||
const tab = tabs[table];
|
||||
if (!tab?.sheet || !(tab.start > 0) || !(tab.end > 0)) continue;
|
||||
const data = sheets[tab.sheet];
|
||||
if (!data) {
|
||||
failed.push(`${tab.sheet || label} 시트가 없습니다.`);
|
||||
continue;
|
||||
}
|
||||
let rows = readRows(data, tab, fields, tab.columns);
|
||||
if (table === "LaborcDat") rows.forEach((r) => (r.TC = r.Cost1));
|
||||
if (table === "SilJukDat" || table === "EstiMateDat")
|
||||
rows.forEach((r) =>
|
||||
Object.assign(r, {
|
||||
OTC: r.TC,
|
||||
ONC: r.NC,
|
||||
OGC: r.GC,
|
||||
OJC: r.JC,
|
||||
...(table === "SilJukDat" && { Cost1: r.TC }),
|
||||
}),
|
||||
);
|
||||
if (table === "CostMainDat") {
|
||||
project.tables.CostTitleDat = [];
|
||||
rows = groups(rows, project.tables.CostTitleDat, 104, (r, n) =>
|
||||
costHead(r.LineData, n?.LineData),
|
||||
);
|
||||
} else if (table === "HeadMainDat") {
|
||||
project.tables.HeadTitleDat = [];
|
||||
rows = groups(rows, project.tables.HeadTitleDat, 103, (r) => headHead(r.Name));
|
||||
}
|
||||
project.tables[table] = rows;
|
||||
}
|
||||
while (project.tables.EstMainDat.length < 36) project.tables.EstMainDat.push({ Index: 0 });
|
||||
let pk = 0;
|
||||
for (const t of Object.keys(project.tables))
|
||||
for (const r of project.tables[t])
|
||||
if (r && typeof r === "object" && r.PkID == null) r.PkID = ++pk;
|
||||
for (const t of ["LaborcDat", "StuffcDat", "SilJukDat", "EstiMateDat"])
|
||||
for (const r of project.tables[t] || [])
|
||||
if (!String(r.Code ?? "").trim()) r.Code = `N${String(r.PkID).padStart(10, "0")}`;
|
||||
return { project, failed };
|
||||
}
|
||||
|
||||
// 화면
|
||||
function form(state) {
|
||||
const x = (state.e2e ||= {
|
||||
tab: "LaborcDat",
|
||||
tabs: Object.fromEntries(
|
||||
TABS.map(([t, label, fields]) => [
|
||||
t,
|
||||
{ sheet: label, start: 0, end: 0, columns: fields.map((_, i) => letter(i)) },
|
||||
]),
|
||||
),
|
||||
});
|
||||
const { esc } = EstApp,
|
||||
[, , fields] = TABS.find(([t]) => t === x.tab),
|
||||
tab = x.tabs[x.tab];
|
||||
EstApp.showDialog(
|
||||
"엑셀데이타를 EST로 변환",
|
||||
`<div class="business-tools">${TABS.map(([t, l]) => `<button data-action="e2e-tab" data-tab="${t}"${t === x.tab ? ' style="font-weight:bold"' : ""}>${l}</button>`).join("")}</div><p><button data-action="e2e-open">엑셀파일열기</button> ${esc(x.file || "")} ${x.sheets ? `(시트 ${Object.keys(x.sheets).length}개)` : ""}</p><div class="form-fields"><label>시트명<input name="e2e-sheet" value="${esc(tab.sheet)}"></label><label>시작행<input name="e2e-start" type="number" value="${tab.start}"></label><label>끝행<input name="e2e-end" type="number" value="${tab.end}"></label></div><p>해당 칼럼명칭 입력(엑셀 열 글자, 비우면 건너뜀, 「B:D」 는 이어 붙임)</p><div class="form-fields">${fields.map((f, i) => `<label>${f}<input name="e2e-col" data-i="${i}" value="${esc(tab.columns[i] || "")}" size="4"></label>`).join("")}</div><button data-action="e2e-run"${x.sheets ? "" : " disabled"}>변 환</button>`,
|
||||
);
|
||||
}
|
||||
function read(state) {
|
||||
const x = state.e2e,
|
||||
tab = x.tabs[x.tab],
|
||||
q = (n) => document.querySelector(`#dialog-content [name="${n}"]`);
|
||||
if (!q("e2e-sheet")) return;
|
||||
tab.sheet = q("e2e-sheet").value.trim();
|
||||
tab.start = Number(q("e2e-start").value) || 0;
|
||||
tab.end = Number(q("e2e-end").value) || 0;
|
||||
document
|
||||
.querySelectorAll('#dialog-content [name="e2e-col"]')
|
||||
.forEach((n) => (tab.columns[Number(n.dataset.i)] = n.value.trim().toUpperCase()));
|
||||
}
|
||||
async function run(name, state, node) {
|
||||
if (name === "excel-to-est") return form(state);
|
||||
read(state);
|
||||
const x = state.e2e;
|
||||
if (name === "e2e-tab") x.tab = node.dataset.tab;
|
||||
else if (name === "e2e-open") {
|
||||
const file = await new Promise((resolve) => {
|
||||
const picker = document.createElement("input");
|
||||
picker.type = "file";
|
||||
picker.accept = ".xlsx,.xls";
|
||||
picker.addEventListener("change", () => resolve(picker.files[0]));
|
||||
picker.click();
|
||||
});
|
||||
if (!file) return;
|
||||
const body = await file.arrayBuffer();
|
||||
x.sheets = /\.xls$/i.test(file.name)
|
||||
? EstJdlCompare.xlsTables(await EstApp.api("/api/read-xls", { method: "POST", body }))
|
||||
: EstExcelMap.sheetTables(await EstApp.api("/api/import-xlsx", { method: "POST", body }));
|
||||
x.file = file.name;
|
||||
} else if (name === "e2e-run") {
|
||||
const tabs = Object.values(x.tabs);
|
||||
if (
|
||||
tabs.every((t) => !t.start) ||
|
||||
tabs.every((t) => !t.end) ||
|
||||
tabs.some((t) => t.end < t.start)
|
||||
)
|
||||
throw Error("변환할 엑셀자료의 범위설정이 잘못되었습니다.");
|
||||
if (!confirm("현재 작업을 엑셀 자료로 만든 새 공사로 바꾸시겠습니까?")) return;
|
||||
const out = convert(x.sheets, x.tabs, {
|
||||
name: x.file.replace(/\.xlsx?$/i, ""),
|
||||
wonga: (state.project.tables.Wonga || []).map(({ Cost, ...rest }) => rest),
|
||||
});
|
||||
if (
|
||||
out.failed.length &&
|
||||
!confirm(`${out.failed.join("\n")}\n변환을 못한 시트가 있습니다.\n\n종료하시겠습니까?`)
|
||||
)
|
||||
return;
|
||||
EstApp.byId("dialog").close();
|
||||
EstApp.setProject(out.project);
|
||||
state.notice =
|
||||
"엑셀 역변환 작업이 완료되었습니다. 금액은 엑셀 값 그대로이며 자동계산은 꺼 두었습니다.";
|
||||
EstApp.render();
|
||||
return;
|
||||
}
|
||||
form(state);
|
||||
}
|
||||
const names = new Set(["excel-to-est", "e2e-tab", "e2e-open", "e2e-run"]);
|
||||
function handle(name, state, node) {
|
||||
if (!names.has(name)) return false;
|
||||
run(name, state, node).catch((error) => {
|
||||
state.notice = error.message;
|
||||
EstApp.render();
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return { convert, costHead, headHead, handle };
|
||||
})();
|
||||
@@ -99,6 +99,7 @@ const allowed = new Set([
|
||||
"native-quantity-list.js",
|
||||
"native-price-tools.js",
|
||||
"native-misc-tools.js",
|
||||
"native-excel-to-est.js",
|
||||
"native-actions-more.js",
|
||||
"native-wonga-tools.js",
|
||||
"native-estimate-print.js",
|
||||
|
||||
Reference in New Issue
Block a user