Refine project status workflows and dashboards

This commit is contained in:
b17301
2026-04-07 20:31:34 +09:00
parent 27a29e8e4c
commit d72a8377fb
6 changed files with 1621 additions and 278 deletions
+45 -16
View File
@@ -134,7 +134,7 @@
<section class="panel">
<div class="section-title">
<h2>/비용/영업수지 그래프</h2>
<h2>/비용/영업수지 그래프</h2>
</div>
<div class="chart-box">
<div class="legend chart-legend" id="balanceLegend"></div>
@@ -162,7 +162,7 @@
};
const labels = {
revenue_sum: "수",
revenue_sum: "수",
project_cost_sum: "원가(프로젝트)",
support_cost_sum: "원가(지원부서)",
support_sga_sum: "판관비(지원부서)",
@@ -180,16 +180,18 @@
function formatAxisLabel(value) {
const numeric = Number(value || 0);
if (!numeric) return "0.0";
if (numeric >= 100000000) return `${(numeric / 100000000).toFixed(1)}`;
if (numeric >= 1000000) return `${(numeric / 1000000).toFixed(1)}백만`;
if (numeric >= 1000) return `${(numeric / 1000).toFixed(1)}`;
const sign = numeric < 0 ? "-" : "";
const absolute = Math.abs(numeric);
if (absolute >= 100000000) return `${sign}${(absolute / 100000000).toFixed(1)}`;
if (absolute >= 1000000) return `${sign}${(absolute / 1000000).toFixed(1)}백만`;
if (absolute >= 1000) return `${sign}${(absolute / 1000).toFixed(1)}`;
return formatNumber(numeric);
}
function pickTickStep(maxValue) {
const baseUnit = maxValue < 1000000 ? 1000 : 1000000;
const units = [1, 2, 5];
const raw = Math.max(maxValue / 5 / baseUnit, 1);
const raw = Math.max(maxValue / baseUnit, 1);
let power = 1;
while (power * 10 <= raw) power *= 10;
for (const unit of units) {
@@ -199,6 +201,14 @@
return power * 10 * baseUnit;
}
function buildPositiveAxisScale(maxValue, tickCount = 4) {
const safeMax = Math.max(Number(maxValue || 0), 1);
const paddedMax = safeMax * (safeMax < 1000 ? 1.12 : 1.08);
const tickStep = pickTickStep(paddedMax / tickCount);
const tickMax = Math.max(tickStep, Math.ceil(paddedMax / tickStep) * tickStep);
return { tickStep, tickMax };
}
function renderLegend(targetId, keys) {
const target = document.getElementById(targetId);
if (!target) return;
@@ -271,8 +281,7 @@
}
function buildAxis(maxValue, width, height, margin) {
const tickStep = pickTickStep(maxValue);
const tickMax = Math.ceil(maxValue / tickStep) * tickStep;
const { tickStep, tickMax } = buildPositiveAxisScale(maxValue, 4);
let axis = "";
for (let value = 0; value <= tickMax; value += tickStep) {
const y = height - margin.bottom - ((height - margin.top - margin.bottom) * value) / tickMax;
@@ -283,6 +292,21 @@
return { axis, tickMax };
}
function buildSignedAxis(maxPositiveValue, minNegativeValue, width, height, margin) {
const rangeMax = Math.max(Math.abs(maxPositiveValue || 0), Math.abs(minNegativeValue || 0), 1);
const { tickStep, tickMax } = buildPositiveAxisScale(rangeMax, 4);
const plotHeight = height - margin.top - margin.bottom;
const zeroY = margin.top + (plotHeight * tickMax) / (tickMax * 2);
let axis = "";
for (let value = -tickMax; value <= tickMax; value += tickStep) {
const y = zeroY - (plotHeight * value) / (tickMax * 2);
axis += `<line x1="${margin.left}" y1="${y}" x2="${width - margin.right}" y2="${y}" stroke="#d8dee5" stroke-dasharray="3 7" />`;
axis += `<text x="${margin.left - 12}" y="${y + 4}" text-anchor="end" fill="#5a6672" font-size="11" font-weight="700">${formatAxisLabel(value)}</text>`;
}
axis += `<line x1="${margin.left}" y1="${zeroY}" x2="${width - margin.right}" y2="${zeroY}" stroke="#8ba0ae" stroke-width="1.2" />`;
return { axis, tickMax, zeroY };
}
function renderEmptyChart(svgId, message) {
const svg = document.getElementById(svgId);
if (!svg) return;
@@ -375,12 +399,13 @@
const margin = { top: 30, right: 24, bottom: 74, left: 98 };
const plotWidth = width - margin.left - margin.right;
const plotHeight = height - margin.top - margin.bottom;
const maxValue = Math.max(...series.flatMap((item) => [
const maxPositiveValue = Math.max(...series.flatMap((item) => [
item.revenue_sum || 0,
item.total_expense || 0,
Math.abs(item.operating_balance || 0),
Math.max(item.operating_balance || 0, 0),
]), 1);
const { axis, tickMax } = buildAxis(maxValue, width, height, margin);
const minNegativeValue = Math.min(...series.map((item) => Math.min(item.operating_balance || 0, 0)), 0);
const { axis, tickMax, zeroY } = buildSignedAxis(maxPositiveValue, minNegativeValue, width, height, margin);
const groupWidth = plotWidth / Math.max(series.length, 1);
const groupGap = groupWidth * 0.22;
const innerGap = 0;
@@ -399,13 +424,17 @@
series.forEach((item, index) => {
const baseX = margin.left + index * groupWidth;
metrics.forEach((key, metricIndex) => {
const value = key === "operating_balance" ? Math.abs(item[key] || 0) : (item[key] || 0);
const barHeight = (plotHeight * value) / tickMax;
const rawValue = Number(item[key] || 0);
const value = key === "operating_balance" ? rawValue : Math.max(rawValue, 0);
const barHeight = (plotHeight * Math.abs(value)) / (tickMax * 2);
const x = baseX + groupStartOffset + metricIndex * (barWidth + innerGap);
const y = height - margin.bottom - barHeight;
const y = value < 0 ? zeroY : zeroY - barHeight;
markup += `<rect x="${x}" y="${y}" width="${barWidth}" height="${barHeight}" rx="2" fill="${palette[key]}" filter="url(#balanceShadow)" />`;
if (value > 0) {
markup += `<text x="${x + barWidth / 2}" y="${Math.max(y - 8, margin.top + 12)}" text-anchor="middle" fill="#314555" font-size="9.5" font-weight="800">${formatAxisLabel(value)}</text>`;
if (value !== 0) {
const labelY = value < 0
? Math.min(y + barHeight + 14, height - margin.bottom + 6)
: Math.max(y - 8, margin.top + 12);
markup += `<text x="${x + barWidth / 2}" y="${labelY}" text-anchor="middle" fill="#314555" font-size="9.5" font-weight="800">${formatAxisLabel(value)}</text>`;
}
});
markup += `<text x="${baseX + groupWidth / 2}" y="${height - margin.bottom + 20}" text-anchor="middle" fill="#5a6672" font-size="11.5" font-weight="700">${item.label}</text>`;