feat(est): reproduce native tables and calculations

This commit is contained in:
2026-11-05 06:07:05 +09:00
parent 646eab6245
commit 60ccedcd9a
30 changed files with 5249 additions and 22 deletions
+121
View File
@@ -0,0 +1,121 @@
const EstViews = (() => {
const enums = {
LinkIndex: [
[0, "직접입력"],
[5, "자재"],
[6, "노임"],
[21, "표준"],
[101, "견적"],
[103, "일위"],
[104, "단가산출"],
[105, "기계"],
[51, "소계"],
[52, "낙찰"],
[54, "순공사비"],
[55, "도급액"],
[57, "조달수수료"],
[58, "비율"],
[59, "경계"],
[61, "자릿수"],
[62, "구간비율"],
[67, "내역비율"],
[68, "묶음끝"],
],
CalcIndex: [
[0, "계산·합산"],
[1, "고정·합산"],
[2, "계산·제외"],
[3, "고정·제외"],
],
ValueIndex: [
[0, "최저"],
[1, "평균"],
[2, "최고"],
[3, "출처선택"],
[4, "직접입력"],
],
DigetIndex: [
[0, "AUTO"],
[1, "사사오입"],
[2, "절사"],
[3, "올림식"],
],
SCheck: [
[0, "비목별"],
[1, "재료"],
[2, "노무"],
[3, "경비"],
[-1, "노무 소계"],
[-2, "재료 소계"],
[-3, "경비 소계"],
[-4, "실적 소계"],
[-5, "합계"],
],
BaseField: [
["", "직접 입력"],
["NC", "직접 노무"],
["GC", "직접 재료"],
["JC", "직접 경비"],
["SC", "직접 실적"],
["TC", "순공사비"],
],
};
function cell(table, index, key, value) {
const { esc } = EstApp,
label = EstModel.labels[key] || key;
const attrs = `data-table="${table}" data-row="${index}" data-field="${key}" aria-label="${esc(label)}"`;
if (enums[key])
return `<select ${attrs}>${enums[key].map(([v, name]) => `<option value="${v}" ${String(v) === String(value ?? (key === "BaseField" ? "" : 0)) ? "selected" : ""}>${esc(name)}</option>`).join("")}</select>`;
if (key === "LineData")
return `<textarea ${attrs} rows="1" spellcheck="false">${esc(EstText.display(value))}</textarea><button class="tiny" data-action="raw-line" data-row="${index}" title="산출근거 편집">…</button>`;
if (value && typeof value === "object") return `<span class="muted">바이너리 자료</span>`;
if (EstModel.numeric.has(key))
return `<input ${attrs} inputmode="decimal" type="number" step="any" value="${value ?? ""}">`;
return `<input ${attrs} type="text" value="${esc(value)}" maxlength="10000">`;
}
function grid(table, list, columns) {
const { esc, number, state } = EstApp;
return `<table class="sheet" data-grid="${table}"><thead><tr><th class="row-number">선택</th>${columns.map((key) => `<th class="col-${key}">${esc(EstModel.labels[key] || key)}</th>`).join("")}</tr></thead><tbody>${list.map(({ row, index }) => `<tr data-index="${index}" class="${state.selection.has(index) ? "selected " : ""}${Number(row.Index) > 0 ? "heading " : ""}${Number(row.Level) > 0 ? "level " : ""}${row.Error ? "error-row" : ""}"><th class="row-number"><label><input type="checkbox" data-select="${index}" ${state.selection.has(index) ? "checked" : ""}>${index + 1}</label></th>${columns.map((key) => `<td class="col-${key}">${cell(table, index, key, row[key])}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
}
function render(state) {
const { project, view } = state,
{ byId, esc, number } = EstApp;
if (project.tables[view]) EstApp.visibleRows();
const grouped = ["HeadMainDat", "CostMainDat", "MachMainDat"].includes(view);
byId("group-pane").hidden = !grouped;
if (grouped) {
const records = EstNative.groups(project.tables[view] || []);
byId("group-list").innerHTML =
[...records]
.map(
([code, group]) =>
`<button data-group="${esc(code)}" class="${state.group === code ? "active" : ""}"><b>${esc(group.rows[0].Name || EstText.clean(group.rows[0].LineData))}</b><small>${esc(code)}</small><span>${number(group.rows[0].TC)}</span></button>`,
)
.join("") || '<p class="muted">자료가 없습니다. 묶음 추가를 누르세요.</p>';
}
byId("search").value = state.query;
byId("standard-tools").hidden = !project.tables[view];
byId("add-group").hidden = !grouped;
if (project.tables[view]) {
const all = EstApp.visibleRows(),
start = state.page * 100,
list = all.slice(start, start + 100),
columns = EstModel.columns(view);
byId("work-content").innerHTML = grid(view, list, columns);
byId("paging").hidden = false;
byId("page-info").textContent =
`${all.length ? start + 1 : 0}–${Math.min(start + 100, all.length)} / ${all.length}행`;
byId("previous-page").disabled = state.page === 0;
byId("next-page").disabled = start + 100 >= all.length;
} else {
byId("paging").hidden = true;
EstExtra.render(state);
}
const chosen = [...state.selection][0],
row = project.tables[view]?.[chosen];
byId("selected-info").textContent = row
? `${chosen + 1}행 · ${row.Code || ""} ${row.Name || EstText.clean(row.LineData)}`
: "행 왼쪽을 선택한 뒤 복사·삭제·레벨 지정 등을 사용할 수 있습니다.";
}
return { render, grid, cell, enums };
})();