104 lines
5.2 KiB
JavaScript
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 };
|
|
})();
|