700 lines
29 KiB
JavaScript
700 lines
29 KiB
JavaScript
const EstActions = (() => {
|
|
let pending = null,
|
|
clipboard = [],
|
|
clipboardTable = "",
|
|
restoring = false;
|
|
function form(title, fields, callback) {
|
|
const { esc, showDialog } = EstApp;
|
|
showDialog(
|
|
title,
|
|
`<div class="form-fields">${fields.map((f) => `<label>${esc(f.label)}${f.choices ? `<select name="${f.name}">${f.choices.map(([v, label]) => `<option value="${esc(v)}" ${String(v) === String(f.value) ? "selected" : ""}>${esc(label)}</option>`).join("")}</select>` : `<input name="${f.name}" type="${f.type || "text"}" value="${esc(f.value ?? "")}" step="any">`}</label>`).join("")}</div><button data-action="dialog-apply">적용</button>`,
|
|
);
|
|
pending = () =>
|
|
callback(
|
|
Object.fromEntries(
|
|
fields.map((f) => {
|
|
const node = document.querySelector(`#dialog-content [name="${f.name}"]`);
|
|
return [f.name, f.type === "number" ? Number(node.value) : node.value];
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
function selected() {
|
|
return [...EstApp.state.selection].sort((a, b) => a - b);
|
|
}
|
|
function activeRows() {
|
|
const { state } = EstApp;
|
|
const rows = state.project.tables[state.view];
|
|
if (!rows) throw Error("작업 표를 먼저 선택하세요.");
|
|
return rows;
|
|
}
|
|
async function undo(redo = false) {
|
|
const { state } = EstApp,
|
|
source = redo ? state.future : state.history,
|
|
target = redo ? state.history : state.future;
|
|
if (!source.length || restoring) return;
|
|
const entry = source.at(-1),
|
|
revision = state.revision,
|
|
documentKey = state.documentKey;
|
|
restoring = true;
|
|
try {
|
|
const project = typeof entry === "string" ? JSON.parse(entry) : await EstApp.restore(entry);
|
|
if (revision !== state.revision || documentKey !== state.documentKey) return;
|
|
target.push(EstApp.snapshot());
|
|
if (target.length > 8) target.shift();
|
|
source.pop();
|
|
const id = state.project.id;
|
|
state.project = project;
|
|
if (id) state.project.id = id;
|
|
state.revision++;
|
|
state.selection.clear();
|
|
EstApp.cache();
|
|
EstApp.render();
|
|
} finally {
|
|
restoring = false;
|
|
}
|
|
}
|
|
function insertRow() {
|
|
const rows = activeRows(),
|
|
{ state } = EstApp;
|
|
EstApp.change(() => {
|
|
const row = EstModel.newRow(state.view, rows, state.project);
|
|
if (state.view === "MachMainDat") row.Index = -3;
|
|
const group = EstNative.groups(rows).get(state.group);
|
|
const at =
|
|
selected().at(-1) ?? (group ? group.start + group.rows.length - 1 : rows.length - 1);
|
|
rows.splice(at + 1, 0, row);
|
|
state.selection = new Set([at + 1]);
|
|
});
|
|
}
|
|
function insertGroup() {
|
|
const { state } = EstApp,
|
|
rows = activeRows();
|
|
form(
|
|
"새 묶음 만들기",
|
|
[
|
|
{ name: "code", label: "코드", value: `U${Date.now().toString(36)}` },
|
|
{ name: "name", label: "품명" },
|
|
{ name: "size", label: "규격" },
|
|
{ name: "unit", label: "단위", value: "식" },
|
|
],
|
|
(values) => {
|
|
if (!values.code || rows.some((r) => r.Code === values.code && Number(r.Index) > 0))
|
|
throw Error("코드를 입력하거나 중복 코드를 바꾸세요.");
|
|
EstApp.change(() => {
|
|
const index = Math.max(0, ...rows.map((r) => Number(r.Index) || 0)) + 1;
|
|
const row = {
|
|
...EstModel.newRow(state.view, rows, state.project),
|
|
Index: index,
|
|
Code: values.code,
|
|
Name: values.name,
|
|
Size: values.size,
|
|
Unite: values.unit,
|
|
GN: 0,
|
|
};
|
|
if (state.view === "CostMainDat")
|
|
row.LineData = `\x0e${values.name}\x0e \x13${values.size}\x13 \x15${values.unit}\x15`;
|
|
rows.push(row);
|
|
state.group = values.code;
|
|
});
|
|
},
|
|
);
|
|
}
|
|
function deleteRows() {
|
|
const { state } = EstApp,
|
|
rows = activeRows(),
|
|
indexes = new Set(selected());
|
|
if (!indexes.size) throw Error("삭제할 행을 선택하세요.");
|
|
for (const group of ["HeadMainDat", "CostMainDat", "MachMainDat"].includes(state.view)
|
|
? EstNative.groups(rows).values()
|
|
: [])
|
|
if (indexes.has(group.start))
|
|
for (let i = group.start; i < group.start + group.rows.length; i++) indexes.add(i);
|
|
const codes = [...indexes].map((i) => rows[i].Code).filter(Boolean),
|
|
kind = Number(
|
|
Object.entries(EstNative.sourceTables).find(([, table]) => table === state.view)?.[0] ||
|
|
{ HeadMainDat: 103, CostMainDat: 104, MachMainDat: 105 }[state.view],
|
|
);
|
|
if (
|
|
kind &&
|
|
Object.entries(state.project.tables).some(([table, list]) =>
|
|
list.some((row, i) =>
|
|
table !== state.view || !indexes.has(i)
|
|
? row.LinkIndex === kind && codes.includes(row.Code)
|
|
: false,
|
|
),
|
|
)
|
|
)
|
|
throw Error("다른 항목에서 사용 중입니다. 연결을 먼저 해제하세요.");
|
|
if (!confirm(`${indexes.size}개 행을 삭제하시겠습니까?`)) return;
|
|
EstApp.change(() => {
|
|
[...indexes].sort((a, b) => b - a).forEach((i) => rows.splice(i, 1));
|
|
state.selection.clear();
|
|
});
|
|
}
|
|
async function action(name, node) {
|
|
const { state, api, esc, number, showDialog } = EstApp,
|
|
project = state.project;
|
|
state.notice = "";
|
|
if (name === "new") {
|
|
if (confirm("저장하지 않은 입력을 비우고 새 내역서를 만드시겠습니까?"))
|
|
EstApp.setProject(EstNative.empty());
|
|
} else if (name === "sample") {
|
|
if (confirm("현재 작업을 REA 샘플로 바꾸시겠습니까?"))
|
|
EstApp.setProject(await api("/api/sample"));
|
|
} else if (name === "open") EstApp.byId("open-file").click();
|
|
else if (name === "save") await EstApp.save();
|
|
else if (name === "save-as")
|
|
form(
|
|
"다른 이름으로 저장",
|
|
[{ name: "name", label: "작업 이름", value: project.name }],
|
|
async (values) => {
|
|
const nextName = values.name.trim();
|
|
const input = document.querySelector('#dialog-content [name="name"]');
|
|
input.setCustomValidity(nextName ? "" : "작업 이름을 입력하세요.");
|
|
if (!input.reportValidity()) throw Error("작업 이름을 입력하세요.");
|
|
EstApp.change(() => (project.name = nextName), false);
|
|
await EstApp.save(true);
|
|
},
|
|
);
|
|
else if (name === "undo") await undo();
|
|
else if (name === "redo") await undo(true);
|
|
else if (name === "add-row") insertRow();
|
|
else if (name === "add-group") insertGroup();
|
|
else if (name === "delete-row") deleteRows();
|
|
else if (name === "copy-row" || name === "cut-row") {
|
|
const rows = activeRows(),
|
|
indices = new Set(selected());
|
|
if (["HeadMainDat", "CostMainDat", "MachMainDat"].includes(state.view))
|
|
for (const group of EstNative.groups(rows).values())
|
|
if (indices.has(group.start))
|
|
for (let i = group.start; i < group.start + group.rows.length; i++) indices.add(i);
|
|
clipboard = [...indices].sort((a, b) => a - b).map((i) => structuredClone(rows[i]));
|
|
if (!clipboard.length) throw Error("복사할 행을 선택하세요.");
|
|
clipboardTable = state.view;
|
|
if (name === "cut-row") deleteRows();
|
|
} else if (name === "paste-row") {
|
|
if (!clipboard.length) throw Error("복사한 행이 없습니다.");
|
|
if (clipboardTable !== state.view)
|
|
throw Error("복사한 행과 같은 종류의 자료에 붙여넣으세요.");
|
|
const rows = activeRows();
|
|
EstApp.change(() => {
|
|
let at = (selected().at(-1) ?? rows.length - 1) + 1;
|
|
const grouped = ["HeadMainDat", "CostMainDat", "MachMainDat"].includes(state.view);
|
|
if (grouped && clipboard.some((r) => Number(r.Index) > 0))
|
|
for (const group of EstNative.groups(rows).values())
|
|
if (at > group.start && at <= group.start + group.rows.length) {
|
|
at = group.start + group.rows.length;
|
|
break;
|
|
}
|
|
const copies = structuredClone(clipboard);
|
|
let next = EstModel.newRow(state.view, rows, state.project).PkID - 1;
|
|
let groupCode = "",
|
|
nextIndex = Math.max(0, ...rows.map((r) => Number(r.Index) || 0));
|
|
copies.forEach((r) => {
|
|
r.PkID = ++next;
|
|
r.NewItem = true;
|
|
delete r.PreviousCodes;
|
|
if (["StuffcDat", "LaborcDat", "SilJukDat", "EstiMateDat"].includes(state.view)) {
|
|
const base = r.Code || "U";
|
|
let suffix = next;
|
|
do {
|
|
r.Code = `${base}_${suffix++}`;
|
|
} while (
|
|
rows.some((row) => row.Code === r.Code) ||
|
|
copies.some((row) => row !== r && row.Code === r.Code)
|
|
);
|
|
}
|
|
if (grouped && Number(r.Index) > 0 && r.Code) {
|
|
const base = r.Code;
|
|
let suffix = next;
|
|
do {
|
|
r.Code = `${base}_${suffix++}`;
|
|
} while (
|
|
rows.some((row) => row.Code === r.Code) ||
|
|
copies.some((row) => row !== r && Number(row.Index) > 0 && row.Code === r.Code)
|
|
);
|
|
r.Index = ++nextIndex;
|
|
groupCode = r.Code;
|
|
}
|
|
if (grouped && groupCode) r.TitleCode = groupCode;
|
|
});
|
|
rows.splice(at, 0, ...copies);
|
|
});
|
|
} else if (name === "recalculate") {
|
|
if (project.options.AutoCalc === false)
|
|
throw Error("내역서 자동계산이 꺼져 있어 전체 재계산을 실행할 수 없습니다.");
|
|
EstApp.calculate();
|
|
EstApp.cache();
|
|
EstApp.render();
|
|
} else if (name === "find") EstApp.byId("search").focus();
|
|
else if (name === "level") {
|
|
if (!selected().length) throw Error("레벨을 바꿀 행을 선택하세요.");
|
|
const level = Number(node.dataset.level);
|
|
EstApp.change(() => selected().forEach((i) => (activeRows()[i].Level = level)));
|
|
} else if (name === "export-xlsx") await EstExchange.exportExcel();
|
|
else if (name === "export-report-xlsx") await EstReport.exportExcel();
|
|
else if (name === "export-estx") await EstExchange.exportNative();
|
|
else if (name === "export-jdl") await EstExchange.exportJdl();
|
|
else if (name === "import-jdl") EstApp.byId("jdl-file").click();
|
|
else if (name === "export-xml")
|
|
form(
|
|
"조달청 XML 작성정보",
|
|
[
|
|
{ name: "company", label: "작성업체명", value: project.exportMeta?.company || "" },
|
|
{
|
|
name: "kind",
|
|
label: "공사구분",
|
|
value: project.exportMeta?.kind || "",
|
|
choices: [
|
|
["", "선택하세요"],
|
|
...[
|
|
"토목공사",
|
|
"건축공사",
|
|
"설비공사",
|
|
"전기공사",
|
|
"통신공사",
|
|
"조경공사",
|
|
"소방공사",
|
|
"문화재수리공사",
|
|
"국가유산수리공사",
|
|
].map((v) => [v, v]),
|
|
],
|
|
},
|
|
{ name: "number", label: "공사관리번호", value: project.exportMeta?.number || "" },
|
|
],
|
|
(meta) => {
|
|
const output = EstC3r.output(project, meta);
|
|
if (
|
|
output.warnings.length &&
|
|
!confirm(
|
|
output.warnings.slice(0, 15).join("\n") +
|
|
`\n총 ${output.warnings.length}건. 단가 전개 결과를 XML로 저장할까요?`,
|
|
)
|
|
)
|
|
return;
|
|
EstApp.change(() => (project.exportMeta = meta), false);
|
|
EstApp.download(`${project.name}.xml`, output.xml, "application/xml;charset=utf-8");
|
|
},
|
|
);
|
|
else if (name === "import-xml") EstApp.byId("xml-file").click();
|
|
else if (name === "import-xlsx") EstApp.byId("excel-file").click();
|
|
else if (name === "print") await EstReport.open();
|
|
else if (name === "print-document") EstReport.print();
|
|
else if (name === "csv") {
|
|
const rows = activeRows(),
|
|
keys = EstModel.columns(state.view);
|
|
const quote = (value) => `"${String(value ?? "").replaceAll('"', '""')}"`;
|
|
const csv = [
|
|
keys.map((key) => EstModel.labels[key] || key),
|
|
...rows.map((row) =>
|
|
keys.map((key) =>
|
|
typeof row[key] === "number"
|
|
? row[key]
|
|
: /^[=+\-@]/.test(String(row[key] || ""))
|
|
? `'${row[key]}`
|
|
: EstText.clean(row[key]),
|
|
),
|
|
),
|
|
]
|
|
.map((row) => row.map(quote).join(","))
|
|
.join("\r\n");
|
|
EstApp.download(
|
|
`${state.project.name}_${state.view}.csv`,
|
|
"\uFEFF" + csv,
|
|
"text/csv;charset=utf-8",
|
|
);
|
|
} else if (name === "projects") {
|
|
const projects = await api("/api/projects");
|
|
showDialog(
|
|
"저장한 작업",
|
|
projects.length
|
|
? projects
|
|
.map(
|
|
(p) =>
|
|
`<p><button data-action="load-project" data-id="${p.id}">${esc(p.name)}</button> <small>${esc(p.modified)}</small></p>`,
|
|
)
|
|
.join("")
|
|
: "저장된 작업이 없습니다.",
|
|
);
|
|
} else if (name === "load-project") {
|
|
if (confirm("현재 작업을 저장한 작업으로 바꾸시겠습니까?")) {
|
|
EstApp.setProject(await api(`/api/projects/${node.dataset.id}`));
|
|
EstApp.byId("dialog").close();
|
|
}
|
|
} else if (name === "parity") {
|
|
const result = await api("/api/parity");
|
|
showDialog(
|
|
"REA 샘플 대조",
|
|
`<p><b>${result.checked}개 중 ${result.passed}개 일치 · 불일치 ${result.failed}개</b></p><p>보관된 EST 샘플의 저장값과 계산 결과를 비교했습니다. 원본 실행 비교는 포함하지 않습니다.</p>${result.errors.length ? `<p>${esc(result.errors.join(" / "))}</p>` : ""}<table><thead><tr><th>영역</th><th>검사 수</th><th>일치</th></tr></thead><tbody>${[...new Set(result.results.map((r) => r.group))].map((group) => `<tr><td>${esc(group)}</td><td>${result.results.filter((r) => r.group === group).length}</td><td>${result.results.filter((r) => r.group === group && r.pass).length}</td></tr>`).join("")}</tbody></table>`,
|
|
);
|
|
} else if (name === "settings") EstSettings.show();
|
|
else if (name === "raw-line") {
|
|
const row = activeRows()[Number(node.dataset.row)];
|
|
showDialog(
|
|
"산출근거 편집",
|
|
`<p>표지는 짝으로 지정합니다. 구간지정 A~Z는 결과 앞에, 구간합계는 합산할 같은 문자를 감쌉니다.</p><div class="marker-tools">${["품명", "규격", "단위", "단가", "변수", "다른줄", "결과", "합계", "구간지정", "구간합계"].map((m) => `<button data-marker="${m}">${m}</button>`).join("")}</div><textarea id="line-editor" rows="10">${esc(EstText.display(row.LineData))}</textarea><button data-action="line-apply" data-row="${node.dataset.row}">적용</button>`,
|
|
);
|
|
} else if (name === "line-apply") {
|
|
const text = EstApp.byId("line-editor").value;
|
|
EstApp.change(() => (activeRows()[Number(node.dataset.row)].LineData = EstText.encode(text)));
|
|
EstApp.byId("dialog").close();
|
|
} else if (name === "template") {
|
|
const templates = project.wongaTemplates || [];
|
|
showDialog(
|
|
"원가 서식 선택",
|
|
`<p>현재 원가 표를 바꾸므로 작업을 저장한 뒤 선택하세요.</p>${templates.map((t, i) => `<button data-action="use-template" data-row="${i}">원본 서식 ${t.index}</button>`).join("")}<button data-action="basic-wonga">비목 기초 뼈대 만들기</button>`,
|
|
);
|
|
} else if (name === "use-template") {
|
|
if (confirm("현재 원가 표를 선택한 서식으로 바꾸시겠습니까?"))
|
|
EstApp.change(
|
|
() =>
|
|
(project.tables.Wonga = structuredClone(
|
|
project.wongaTemplates[Number(node.dataset.row)].rows,
|
|
)),
|
|
);
|
|
EstApp.byId("dialog").close();
|
|
} else if (name === "basic-wonga") {
|
|
if (confirm("현재 원가 표를 기본 뼈대로 바꾸시겠습니까?"))
|
|
EstApp.change(() => {
|
|
project.options.TotalFix = false;
|
|
project.tables.Wonga = [
|
|
{ Gubun: "N", Name: "직접 노무비", BaseField: "NC", Cost: 0 },
|
|
{ Gubun: "G", Name: "직접 재료비", BaseField: "GC", Cost: 0 },
|
|
{ Gubun: "J", Name: "직접 경비", BaseField: "JC", Cost: 0 },
|
|
{ Gubun: "S", Name: "표준시장단가", BaseField: "SC", Cost: 0 },
|
|
{ Gubun: "T", Name: "순공사비", VSusic: "N+G+J+S" },
|
|
{ Gubun: "R", Name: "도급액", VSusic: "T", LinkIndex: 55 },
|
|
];
|
|
});
|
|
EstApp.byId("dialog").close();
|
|
} else if (name === "capture-baseline") {
|
|
if (
|
|
confirm(
|
|
"현재 계산값을 당초·계약 내역으로 보존하시겠습니까? 기존 당초 사본이 있으면 교체됩니다.",
|
|
)
|
|
)
|
|
EstApp.change(() => (state.project = EstBusiness.baseline(project)));
|
|
} else if (name === "capture-bid") {
|
|
if (confirm("현재 단가를 입찰 당시 단가로 기록하시겠습니까?"))
|
|
EstApp.change(() =>
|
|
project.tables.EstMainDat.forEach((row) =>
|
|
EstMath.units.forEach((key) => (row[`A${key}`] = EstMath.num(row[key]))),
|
|
),
|
|
);
|
|
} else if (name === "recalculate-original")
|
|
form(
|
|
"당초분 재계산",
|
|
[
|
|
{
|
|
name: "method",
|
|
value: String(project.options.OldRecalcMethod || 0),
|
|
label: "계산 방법",
|
|
choices: [
|
|
["0", "신규공종만 계산"],
|
|
["1", "변경된 단가만"],
|
|
["2", "당초분 전체"],
|
|
],
|
|
},
|
|
],
|
|
(values) =>
|
|
EstApp.change(() => {
|
|
project.baseline = EstBusiness.original(project, Number(values.method));
|
|
project.options.OldRecalcMethod = Number(values.method);
|
|
}),
|
|
);
|
|
else if (name === "bid-rates")
|
|
form(
|
|
"비목별 낙찰률",
|
|
[
|
|
{ name: "N", label: "노무 %", type: "number", value: 100 },
|
|
{ name: "G", label: "재료 %", type: "number", value: 100 },
|
|
{ name: "J", label: "경비 %", type: "number", value: 100 },
|
|
{ name: "S", label: "실적 %", type: "number", value: 100 },
|
|
],
|
|
(values) => {
|
|
if (Object.values(values).some((n) => !Number.isFinite(n) || n < 0))
|
|
throw Error("낙찰률은 0 이상의 수치입니다.");
|
|
EstApp.change(
|
|
() =>
|
|
(state.project = EstBusiness.bid(project, [values.N, values.G, values.J, values.S])),
|
|
);
|
|
},
|
|
);
|
|
else if (name === "restore-bid") {
|
|
if (confirm("당초단가(100%)로 복원하시겠습니까?"))
|
|
EstApp.change(() => {
|
|
state.project = EstBusiness.restoreBid(project);
|
|
});
|
|
} else if (name === "add-quantity")
|
|
EstApp.change(() => {
|
|
project.quantities.push({
|
|
Code: EstQuantity.newCode(project.quantities),
|
|
Name: "새 수량",
|
|
TargetCode: "",
|
|
formula: "1",
|
|
digits: 2,
|
|
});
|
|
}, false);
|
|
else if (name === "apply-quantity") {
|
|
const totals = EstQuantity.totals(project, state.quantitySheet);
|
|
if (confirm("연결된 내역 수량을 산출 결과로 바꾸시겠습니까?"))
|
|
EstApp.change(() => EstQuantity.applyTotals(project, totals));
|
|
} else if (name === "add-round") {
|
|
const next = {
|
|
id: EstApp.uid(),
|
|
name: EstBusiness.newProgressName(project.progress),
|
|
inputs: {},
|
|
approved: false,
|
|
locked: false,
|
|
};
|
|
EstApp.change(() => {
|
|
project.progress.push(next);
|
|
state.roundId = next.id;
|
|
}, false);
|
|
} else if (name === "lock-round") {
|
|
if (
|
|
confirm("이 회차를 확정하고 저장하시겠습니까? 전회 누계로 보존되고 직접 편집이 잠깁니다.")
|
|
) {
|
|
EstApp.change(() => (state.project = EstProgress.lock(project, state.roundId)), false);
|
|
await EstApp.save();
|
|
}
|
|
} else if (name === "add-annual")
|
|
EstApp.change(
|
|
() =>
|
|
project.annual.push({
|
|
name: EstBusiness.newAnnualName(project.annual),
|
|
formula: "[기준]",
|
|
}),
|
|
false,
|
|
);
|
|
else if (name === "move-annual") {
|
|
const index = Number(node.dataset.row),
|
|
next = index + Number(node.dataset.direction);
|
|
if (index >= 0 && index < project.annual.length && next >= 0 && next < project.annual.length)
|
|
EstApp.change(() => {
|
|
[project.annual[index], project.annual[next]] = [
|
|
project.annual[next],
|
|
project.annual[index],
|
|
];
|
|
}, false);
|
|
} else if (name === "open-annual") {
|
|
const result = EstBusiness.annualCopy(project, Number(node.dataset.row));
|
|
if (confirm("현재 화면을 차수 내역 사본으로 바꾸시겠습니까?")) {
|
|
EstApp.setProject(result);
|
|
EstApp.navigate("EstMainDat");
|
|
}
|
|
} else if (name === "delete-extra") {
|
|
if (confirm("이 항목을 삭제하시겠습니까?"))
|
|
EstApp.change(
|
|
() => project[node.dataset.section].splice(Number(node.dataset.row), 1),
|
|
false,
|
|
);
|
|
} else if (name === "dialog-apply" && pending) {
|
|
await pending();
|
|
pending = null;
|
|
EstApp.byId("dialog").close();
|
|
} else if (name === "close-dialog") EstApp.byId("dialog").close();
|
|
else await EstBusinessActions.handle(name, node);
|
|
}
|
|
function install() {
|
|
EstSchedule.install();
|
|
EstExchange.install();
|
|
EstBusinessActions.install();
|
|
EstIssues.install();
|
|
document.addEventListener("click", async (event) => {
|
|
try {
|
|
const marker = event.target.closest("[data-marker]");
|
|
if (marker) {
|
|
const editor = EstApp.byId("line-editor"),
|
|
start = editor.selectionStart,
|
|
end = editor.selectionEnd,
|
|
tag = `⟦${marker.dataset.marker}⟧`;
|
|
editor.setRangeText(tag + editor.value.slice(start, end) + tag, start, end, "select");
|
|
editor.focus();
|
|
return;
|
|
}
|
|
const view = event.target.closest("[data-view]");
|
|
if (view) {
|
|
EstApp.navigate(view.dataset.view);
|
|
return;
|
|
}
|
|
const group = event.target.closest("[data-group]");
|
|
if (group) {
|
|
EstApp.navigate(EstApp.state.view, group.dataset.group);
|
|
return;
|
|
}
|
|
const node = event.target.closest("[data-action]");
|
|
if (node) {
|
|
await action(node.dataset.action, node);
|
|
node.closest("details")?.removeAttribute("open");
|
|
}
|
|
} catch (error) {
|
|
EstApp.state.notice = error.message;
|
|
EstApp.render();
|
|
}
|
|
});
|
|
document.addEventListener("change", (event) => {
|
|
const node = event.target,
|
|
{ state } = EstApp,
|
|
project = state.project;
|
|
state.notice = "";
|
|
try {
|
|
if (EstBusinessActions.change(node)) return;
|
|
if (node.matches("[data-select]")) {
|
|
const index = Number(node.dataset.select);
|
|
node.checked ? state.selection.add(index) : state.selection.delete(index);
|
|
EstApp.render();
|
|
return;
|
|
}
|
|
if (node.id === "project-name") {
|
|
EstApp.change(() => (project.name = node.value), false);
|
|
return;
|
|
}
|
|
if (node.id === "automatic" || node.id === "refresh-sources") {
|
|
EstApp.change(
|
|
() =>
|
|
(project.options[node.id === "automatic" ? "AutoCalc" : "SourceRefresh"] =
|
|
node.checked),
|
|
);
|
|
return;
|
|
}
|
|
if (node.id === "round-choice") {
|
|
state.roundId = node.value;
|
|
EstApp.render();
|
|
return;
|
|
}
|
|
if (node.id === "escalation-component") {
|
|
state.escalationComponent = node.value;
|
|
EstApp.render();
|
|
return;
|
|
}
|
|
if (node.id === "use-approved") {
|
|
const round = project.progress.find((r) => r.id === state.roundId);
|
|
if (round.locked) throw Error("확정 회차입니다.");
|
|
EstApp.change(() => (round.approved = node.checked), false);
|
|
return;
|
|
}
|
|
if (node.matches("[data-progress]")) {
|
|
const round = project.progress.find((r) => r.id === state.roundId);
|
|
if (round.locked) throw Error("확정 회차입니다.");
|
|
if (EstProgress.ambiguousKeys(project.tables.EstMainDat).has(node.dataset.key))
|
|
throw Error("기성 내역 식별자가 중복되어 입력할 수 없습니다.");
|
|
const value = node.type === "number" ? Number(node.value) : node.value;
|
|
if (node.type === "number" && !Number.isFinite(value)) throw Error("수치 입력 오류");
|
|
EstApp.change(() => {
|
|
round.inputs[node.dataset.key] ||= {};
|
|
round.inputs[node.dataset.key][node.dataset.progress] = value;
|
|
}, false);
|
|
return;
|
|
}
|
|
if (node.matches("[data-extra]")) {
|
|
const value = node.type === "number" ? Number(node.value) : node.value;
|
|
if (node.type === "number" && !Number.isFinite(value)) throw Error("수치 입력 오류");
|
|
EstApp.change(() => {
|
|
const row = project[node.dataset.extra][Number(node.dataset.row)];
|
|
row[node.dataset.field] = value;
|
|
if (node.dataset.extra === "quantities" && node.dataset.field === "TargetCode")
|
|
row.TargetKey = "";
|
|
}, false);
|
|
return;
|
|
}
|
|
if (node.matches("[data-table][data-field]")) {
|
|
const row = project.tables[node.dataset.table][Number(node.dataset.row)],
|
|
key = node.dataset.field;
|
|
const numeric = EstModel.numeric.has(key),
|
|
value = numeric
|
|
? node.value === ""
|
|
? null
|
|
: Number(node.value)
|
|
: key === "LineData"
|
|
? EstText.encode(node.value)
|
|
: node.value;
|
|
if (
|
|
numeric &&
|
|
value !== null &&
|
|
(!Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER)
|
|
)
|
|
throw Error("유효한 수치를 입력하세요.");
|
|
if (key === "Level" && (value < 0 || value > 9 || !Number.isInteger(value)))
|
|
throw Error("레벨은 0~9 정수입니다.");
|
|
if (key === "Precision" && (value < 0 || value > 6 || !Number.isInteger(value)))
|
|
throw Error("소수 자릿수는 0~6 정수입니다.");
|
|
EstApp.remember();
|
|
if (key === "Code") {
|
|
const previous = row.Code;
|
|
EstModel.editCode(project, node.dataset.table, Number(node.dataset.row), value);
|
|
if (state.group === previous && Number(row.Index) > 0) state.group = row.Code;
|
|
} else if (key === "SCheck")
|
|
EstBusinessActions.setClassification(project, row, node, value);
|
|
else row[key] = value;
|
|
if (state.view === "escalation" && EstMath.units.includes(key))
|
|
project.options.SourceRefresh = false;
|
|
if (["StuffcDat", "LaborcDat", "EstiMateDat", "SilJukDat"].includes(node.dataset.table)) {
|
|
if (key === "TC") {
|
|
row.InputTC = value;
|
|
row.ValueIndex = 4;
|
|
}
|
|
if (["NC", "GC", "JC", "SC"].includes(key)) row[`Input${key}`] = value;
|
|
project.options.SourceRefresh = true;
|
|
}
|
|
if (project.options.AutoCalc !== false) EstApp.calculate();
|
|
EstApp.cache();
|
|
setTimeout(() => EstApp.render(), 0);
|
|
}
|
|
} catch (error) {
|
|
state.notice = error.message;
|
|
EstApp.render();
|
|
}
|
|
});
|
|
EstApp.byId("open-file").addEventListener("change", async (event) => {
|
|
try {
|
|
await EstApp.open(event.target.files[0]);
|
|
} catch (error) {
|
|
EstApp.state.notice = error.message;
|
|
EstApp.render();
|
|
} finally {
|
|
event.target.value = "";
|
|
}
|
|
});
|
|
EstApp.byId("search").addEventListener("input", (event) => {
|
|
EstApp.state.query = event.target.value;
|
|
EstApp.state.page = 0;
|
|
EstApp.render();
|
|
});
|
|
EstApp.byId("previous-page").addEventListener("click", () => {
|
|
EstApp.state.page--; EstApp.render();
|
|
});
|
|
EstApp.byId("next-page").addEventListener("click", () => {
|
|
EstApp.state.page++; EstApp.render();
|
|
});
|
|
document.addEventListener("keydown", (event) => {
|
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "s") {
|
|
event.preventDefault();
|
|
EstApp.save().catch((error) => {
|
|
EstApp.state.notice = error.message; EstApp.render();
|
|
});
|
|
}
|
|
});
|
|
document.addEventListener("dblclick", (event) => {
|
|
const input = event.target.closest('[data-field="Code"]');
|
|
if (!input) return;
|
|
const row = EstApp.state.project.tables[input.dataset.table][Number(input.dataset.row)],
|
|
kind = Number(row.LinkIndex);
|
|
const table =
|
|
{ 103: "HeadMainDat", 104: "CostMainDat", 105: "MachMainDat" }[kind] ||
|
|
EstNative.sourceTables[kind];
|
|
if (table) {
|
|
EstApp.navigate(table, row.Code);
|
|
if (![103, 104, 105].includes(kind)) {
|
|
EstApp.state.query = row.Code;
|
|
EstApp.render();
|
|
}
|
|
}
|
|
});
|
|
EstAttachments.install();
|
|
EstStandard.install();
|
|
}
|
|
return { install, form, undo };
|
|
})();
|