Files
Aislo/tools/est-test/native-schedule.js
T

104 lines
5.2 KiB
JavaScript

const EstSchedule = (() => {
function day(value) {
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value))
throw Error("공정 날짜는 YYYY-MM-DD 형식입니다.");
const time = Date.parse(value + "T00:00:00Z");
if (!Number.isFinite(time) || new Date(time).toISOString().slice(0, 10) !== value)
throw Error("공정 날짜 오류");
return time / 86400000;
}
function validate(project) {
if (project.schedule == null) return;
if (!Array.isArray(project.schedule) || project.schedule.length > 10000)
throw Error("공정표 형식/행 수 오류");
const ids = new Set();
for (const row of project.schedule) {
if (!row || typeof row.id !== "string" || !row.id || ids.has(row.id))
throw Error("공정 항목 ID 오류");
ids.add(row.id);
for (const key of ["Code", "Name", "start", "end"])
if (typeof row[key] !== "string" || row[key].length > 1000) throw Error("공정 문자열 오류");
for (const key of ["weight", "progress"])
if (
typeof row[key] !== "number" ||
!Number.isFinite(row[key]) ||
row[key] < 0 ||
row[key] > Number.MAX_SAFE_INTEGER
)
throw Error("공정 수치 오류");
if (row.progress > 100) throw Error("공정 진도는 0~100%입니다.");
if (row.start) day(row.start);
if (row.end) day(row.end);
if (row.start && row.end && day(row.start) > day(row.end))
throw Error("공정 종료일이 시작일보다 빠릅니다.");
}
}
function calculate(project) {
validate(project);
const rows = project.schedule || [],
dated = rows.filter((r) => r.start && r.end),
first = dated.length ? Math.min(...dated.map((r) => day(r.start))) : 0,
last = dated.length ? Math.max(...dated.map((r) => day(r.end))) : first,
span = last - first + 1,
weight = rows.reduce((sum, r) => sum + r.weight, 0);
return {
rows: rows.map((r) => ({
...r,
left: r.start && r.end ? ((day(r.start) - first) / span) * 100 : 0,
width: r.start && r.end ? ((day(r.end) - day(r.start) + 1) / span) * 100 : 0,
})),
weight,
progress: weight ? rows.reduce((sum, r) => sum + r.weight * r.progress, 0) / weight : 0,
start: dated.length ? new Date(first * 86400000).toISOString().slice(0, 10) : "",
end: dated.length ? new Date(last * 86400000).toISOString().slice(0, 10) : "",
};
}
function render() {
const { esc, number, state } = EstApp,
output = calculate(state.project);
EstApp.byId("work-content").innerHTML =
`<div class="business-tools"><button data-action="schedule-add">공정 추가</button><span>시작 ${esc(output.start)} · 종료 ${esc(output.end)} · 가중치 합계 ${number(output.weight)} · 입력 진도 가중평균 ${number(output.progress)}%</span></div><p>기간과 진도는 직접 입력합니다. 가중치는 공종 금액 또는 사용자가 정한 값을 사용합니다.</p><table class="sheet"><thead><tr>${["코드", "공정명", "시작일", "종료일", "가중치", "진도(%)", "기간", "작업"].map((h) => `<th>${h}</th>`).join("")}</tr></thead><tbody>${output.rows.map((r, i) => `<tr>${["Code", "Name", "start", "end", "weight", "progress"].map((k) => `<td><input data-schedule="${k}" data-index="${i}" type="${["start", "end"].includes(k) ? "date" : ["weight", "progress"].includes(k) ? "number" : "text"}" value="${esc(r[k])}" step="any"></td>`).join("")}<td style="min-width:180px"><div style="height:12px;background:#ddd"><div style="margin-left:${r.left}%;width:${r.width}%;height:12px;background:#316ea5"></div></div></td><td><button data-action="schedule-delete" data-index="${i}">삭제</button></td></tr>`).join("")}</tbody></table>`;
}
function install() {
document.addEventListener("click", (event) => {
const node = event.target.closest("[data-action]");
if (!node) return;
const p = EstApp.state.project;
if (node.dataset.action === "schedule-add")
EstApp.change(() => {
p.schedule ||= [];
const row = p.tables.EstMainDat.find(
(r) => r.Code && !Number(r.Level) && !p.schedule.some((s) => s.Code === r.Code),
);
p.schedule.push({
id: crypto.randomUUID(),
Code: row?.Code || "",
Name: row?.Name || "새 공정",
start: "",
end: "",
weight: Math.max(0, Number(row?.TC) || 0),
progress: 0,
});
}, false);
if (node.dataset.action === "schedule-delete")
EstApp.change(() => p.schedule.splice(Number(node.dataset.index), 1), false);
});
document.addEventListener("change", (event) => {
const node = event.target;
if (!node.dataset.schedule) return;
const p = structuredClone(EstApp.state.project),
row = p.schedule[Number(node.dataset.index)],
key = node.dataset.schedule;
try {
row[key] = ["weight", "progress"].includes(key) ? Number(node.value) : node.value;
validate(p);
EstApp.change(() => (EstApp.state.project = p), false);
} catch (error) {
EstApp.state.notice = error.message;
EstApp.render();
}
});
}
return { validate, calculate, render, install };
})();