//////////////////////////////////////////////////////////////////////////////////////// // KPI Dashboard //////////////////////////////////////////////////////////////////////////////////////// let selectedPdServiceId; // 제품(서비스) 아이디 let selectedPdService; // 제품(서비스) 이름 let selectedVersionIds = []; // 선택된 버전 아이디 배열 let selectedVersions = []; // 선택된 버전 이름 배열 let selectedAssigneeId; // 선택된 작업자 계정 ID let selectedAssigneeName; // 선택된 작업자 이름 let selectedStartDate; // 시작 날짜 let selectedEndDate; // 종료 날짜 function execDocReady() { var pluginGroups = [ [ "../reference/light-blue/lib/bootstrap-datepicker.js", "../reference/lightblue4/docs/lib/widgster/widgster.js", "../reference/lightblue4/docs/lib/slimScroll/jquery.slimscroll.min.js", "../reference/jquery-plugins/datetimepicker-2.5.20/build/jquery.datetimepicker.min.css", "../reference/jquery-plugins/datetimepicker-2.5.20/build/jquery.datetimepicker.full.min.js" ], [ "https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js" ], [ "../reference/jquery-plugins/select2-4.0.2/dist/css/select2_lightblue4.css", "../reference/jquery-plugins/lou-multi-select-0.9.12/css/multiselect-lightblue4.css", "../reference/jquery-plugins/multiple-select-1.5.2/dist/multiple-select-bluelight.css", "../reference/jquery-plugins/select2-4.0.2/dist/js/select2.min.js", "../reference/jquery-plugins/lou-multi-select-0.9.12/js/jquery.quicksearch.js", "../reference/jquery-plugins/lou-multi-select-0.9.12/js/jquery.multi-select.js", "../reference/jquery-plugins/multiple-select-1.5.2/dist/multiple-select.min.js" ] // 추가적인 플러그인 그룹들을 이곳에 추가하면 됩니다. ]; loadPluginGroupsParallelAndSequential(pluginGroups) .then(function() { console.log('모든 플러그인 로드 완료'); // 사이드 메뉴 처리 $('.widget').widgster(); setSideMenu("sidebar_menu_requirement", "sidebar_menu_requirement_kpi"); // 스크립트 실행 로직을 이곳에 추가합니다. // 1. 제품(서비스) 셀렉트 박스 이니시에이터 makePdServiceSelectBox(); // 2. 버전 멀티 셀렉트 박스 이니시에이터 makeVersionMultiSelectBox(); // 3. 작업자 셀렉트 박스 이니시에이터 makeAssigneeSelectBox(); // 4. 날짜 범위 선택기 이니시에이터 makeDateRangePicker(); // 차트는 작업자 선택 후에 렌더링됨 console.log('KPI 대시보드 초기화 완료'); }) .catch(function(e) { console.error('플러그인 로드 중 오류 발생'); console.error(e); }); } //////////////////////////////////////////////////////////////////////////////////////// // Utility Functions //////////////////////////////////////////////////////////////////////////////////////// /** * 날짜 포맷 함수 (yyyy-MM-dd) */ function formatDate(date) { var year = date.getFullYear(); var month = String(date.getMonth() + 1).padStart(2, '0'); var day = String(date.getDate()).padStart(2, '0'); return year + '-' + month + '-' + day; } //////////////////////////////////////////////////////////////////////////////////////// // 제품 서비스 셀렉트 박스 //////////////////////////////////////////////////////////////////////////////////////// function makePdServiceSelectBox() { // 제품 서비스 셀렉트 박스 이니시에이터 (작업자 셀렉트 제외) $(".chzn-select").not("#selected_assignee").each(function() { $(this).select2($(this).data()); }); // 작업자 셀렉트 박스는 별도로 초기화 $("#selected_assignee").select2({ placeholder: "Select Assignee", allowClear: true, width: '100%' }); console.log("작업자 셀렉트 박스 초기 초기화 완료"); // 제품 서비스 셀렉트 박스 데이터 바인딩 $.ajax({ url: "/auth-user/api/arms/pdServicePure/getPdServiceMonitor.do", type: "GET", contentType: "application/json;charset=UTF-8", dataType: "json", progress: true, statusCode: { 200: function(data) { for (var k in data.response) { var obj = data.response[k]; var newOption = new Option(obj.c_title, obj.c_id, false, false); $("#selected_pdService").append(newOption).trigger("change"); } jSuccess("제품(서비스) 조회가 완료 되었습니다."); } }, error: function (e) { jError("제품(서비스) 조회 중 에러가 발생했습니다."); } }); $("#selected_pdService").on("select2:open", function() { makeSlimScroll(".select2-results__options"); }); // --- select2 ( 제품(서비스) 검색 및 선택 ) 이벤트 --- // $("#selected_pdService").on("select2:select", function(e) { selectedPdServiceId = $("#selected_pdService").val(); selectedPdService = $("#selected_pdService").select2("data")[0].text; console.log("선택된 제품(서비스):", selectedPdService, "ID:", selectedPdServiceId); // 제품(서비스) 선택 시 자동으로 버전 데이터 바인딩 bind_VersionData_By_PdService(); }); } //////////////////////////////////////////////////////////////////////////////////////// // 버전 멀티 셀렉트 박스 //////////////////////////////////////////////////////////////////////////////////////// function makeVersionMultiSelectBox() { $(".multiple-select").multipleSelect({ filter: true, onClose: function () { selectedVersions = []; selectedVersionIds = []; $("#multiversion option:selected").map(function (a, item) { selectedVersions.push(item.innerText); selectedVersionIds.push(item.value); }); console.log("선택된 버전:", selectedVersions); console.log("선택된 버전 IDs:", selectedVersionIds); // 버전 선택 완료 시 작업자 목록 갱신 if (selectedVersionIds.length > 0) { fetchAssignees(); } else { jError("버전을 선택해주세요."); } } }); } function bind_VersionData_By_PdService() { $(".multiple-select option").remove(); selectedVersionIds = []; selectedVersions = []; $.ajax({ url: "/auth-user/api/arms/pdService/getVersionList?c_id=" + selectedPdServiceId, type: "GET", dataType: "json", progress: true, statusCode: { 200: function (data) { for (var k in data.response) { var obj = data.response[k]; selectedVersions.push(obj.c_title); selectedVersionIds.push(obj.c_id); var newOption = new Option(obj.c_title, obj.c_id, true, false); $(".multiple-select").append(newOption); } console.log("버전 데이터 바인딩 완료"); console.log("selectedPdServiceId:", selectedPdServiceId); console.log("selectedVersionIds:", selectedVersionIds); $(".multiple-select").multipleSelect("refresh"); // 제품/버전 선택 후 작업자 목록 갱신 fetchAssignees(); } }, error: function (e) { jError("버전 조회 중 에러가 발생했습니다."); } }); } //////////////////////////////////////////////////////////////////////////////////////// // 날짜 범위 선택기 //////////////////////////////////////////////////////////////////////////////////////// function makeDateRangePicker() { // 기본값: 최근 30일 var endDate = new Date(); var startDate = new Date(); startDate.setDate(startDate.getDate() - 30); selectedStartDate = formatDate(startDate); selectedEndDate = formatDate(endDate); $("#dateRange").val(selectedStartDate + " ~ " + selectedEndDate); $("#dateRange").datetimepicker({ timepicker: false, format: 'Y-m-d', onShow: function(ct) { this.setOptions({ maxDate: new Date() }); }, onSelectDate: function(ct, $input) { if (!$input.data('startDate')) { $input.data('startDate', ct); $input.val(formatDate(ct) + " ~ "); } else { var start = $input.data('startDate'); var end = ct; if (start > end) { var temp = start; start = end; end = temp; } selectedStartDate = formatDate(start); selectedEndDate = formatDate(end); $input.val(selectedStartDate + " ~ " + selectedEndDate); $input.data('startDate', null); console.log("선택된 기간:", selectedStartDate, "~", selectedEndDate); } } }); } //////////////////////////////////////////////////////////////////////////////////////// // 작업자 셀렉트 박스 //////////////////////////////////////////////////////////////////////////////////////// function makeAssigneeSelectBox() { // 작업자 셀렉트 박스는 이미 chzn-select로 초기화되었으므로 // 이벤트 핸들러만 등록 bindAssigneeEvents(); } /** * 작업자 셀렉트 박스 이벤트 바인딩 */ function bindAssigneeEvents() { $("#selected_assignee").on("select2:open", function() { makeSlimScroll(".select2-results__options"); }); // 작업자 선택 이벤트 - accountId 사용 $("#selected_assignee").on("select2:select", function(e) { selectedAssigneeId = $("#selected_assignee").val(); selectedAssigneeName = $("#selected_assignee").select2("data")[0].text; if(selectedAssigneeId !== null && selectedAssigneeName !== null){ loadAllKpiData(); } }); // 작업자 선택 해제 이벤트 $("#selected_assignee").on("select2:clear", function(e) { selectedAssigneeId = null; selectedAssigneeName = null; // 작업자 선택 해제 시 차트 초기화 clearAllCharts(); }); } /** * 작업자 목록 가져오기 */ function fetchAssignees() { var urlBuilder = new UrlBuilder() .setBaseUrl('/auth-user/api/arms/report/full-data/assignee-list'); if (selectedPdServiceId) { urlBuilder.addQueryParam('pdServiceId', selectedPdServiceId); } if (selectedVersionIds && selectedVersionIds.length > 0) { urlBuilder.addQueryParam('pdServiceVersionIds', selectedVersionIds.join(',')); } $.ajax({ url: urlBuilder.build(), type: "GET", dataType: "json", progress: true, success: function(data) { // 기존 옵션 제거 $("#selected_assignee").empty(); // 빈 옵션 추가 (placeholder용) $("#selected_assignee").append(new Option("", "", false, false)); // 작업자 옵션 추가 if (data.response && data.response.length > 0) { for (var k in data.response) { var obj = data.response[k]; var newOption = new Option(obj.displayName, obj.accountId, false, false); $("#selected_assignee").append(newOption); } jSuccess("작업자 목록 조회가 완료되었습니다. (" + data.response.length + "명)"); } else { jWarning("선택한 제품/버전에 대한 작업자가 없습니다."); } // Select2에 변경 알림 $("#selected_assignee").trigger("change"); }, error: function (xhr, status, error) { jError("작업자 목록 조회 중 에러가 발생했습니다."); } }); } //////////////////////////////////////////////////////////////////////////////////////// // KPI Data Loading //////////////////////////////////////////////////////////////////////////////////////// /** * 필터 유효성 검사 */ function validateFilters() { if (!selectedPdServiceId) { console.log("제품(서비스)가 선택되지 않았습니다."); return false; } if (!selectedVersionIds || selectedVersionIds.length === 0) { console.log("버전이 선택되지 않았습니다."); return false; } if (!selectedStartDate || !selectedEndDate) { console.log("조회 기간이 설정되지 않았습니다."); return false; } return true; } /** * 모든 KPI 데이터 로딩 * 작업자 선택 시 호출됨 */ function loadAllKpiData() { if (!validateFilters()) { return; } // 작업자가 선택되지 않은 경우 데이터 로드하지 않음 if (!selectedAssigneeId) { console.log("작업자가 선택되지 않아 데이터를 로드하지 않습니다."); return; } // Mock 데이터로 차트 렌더링 (백엔드 작업 완료 후 실제 API 호출로 교체 예정) loadMockDataAndRenderCharts(); // TODO: 백엔드 작업 완료 후 아래 주석 해제 // loadScheduleComplianceData(); } //////////////////////////////////////////////////////////////////////////////////////// // INS-QL-4231: 일정 준수율 분석 //////////////////////////////////////////////////////////////////////////////////////// // 차트 인스턴스 저장 var reqDelayRateChartInstance = null; var reqDelayRateGaugeInstance = null; /** * 일정 준수율 데이터 로딩 */ function loadScheduleComplianceData() { console.log("loadScheduleComplianceData 호출"); if (!selectedAssigneeId) { return; } var requestData = { pdServiceLink: parseInt(selectedPdServiceId), pdServiceVersionLinks: selectedVersionIds.map(function(id) { return parseInt(id); }), assigneeId: selectedAssigneeId, startDate: selectedStartDate, endDate: selectedEndDate }; $.ajax({ url: '/auth-user/api/arms/engine-fire/insight/schedule-compliance', type: 'POST', contentType: 'application/json', data: JSON.stringify(requestData), success: function(data) { if (data.success && data.response) { renderScheduleComplianceData(data.response); } }, error: function(xhr, status, error) { jError("일정 준수율 데이터 조회 중 에러가 발생했습니다."); } }); } /** * 일정 준수율 데이터 렌더링 */ function renderScheduleComplianceData(data) { console.log("call renderScheduleComplianceData"); // 유형별 완료 지표 TypeBar 차트 렌더링 var completionData = {}; var colorMap = {}; if (data.issueCompletionRate) { completionData['이슈 완료율'] = data.issueCompletionRate.completionRate; colorMap['이슈 완료율'] = '#3b82f6'; } if (data.requirementCompletionRate) { completionData['요구사항 완료율'] = data.requirementCompletionRate.completionRate; colorMap['요구사항 완료율'] = '#0ea5e9'; } if (data.requirementComplianceRate) { completionData['일정 준수율'] = data.requirementComplianceRate.complianceRate; colorMap['일정 준수율'] = '#f59e0b'; } // TypeBar 스타일로 유형별 완료 지표 차트 렌더링 if (Object.keys(completionData).length > 0) { renderTypeBarChart('completionIndicatorChart', 'completionIndicatorLegend', completionData, colorMap); } // 평균 지연 일수 (프로필영역) if (data.requirementAverageDelayDays !== undefined) { $("#avgDelayDays").text(data.requirementAverageDelayDays); $("#avgDelayDaysProfile").text(data.requirementAverageDelayDays); } // 요구사항별 지연율 게이지 차트 if (data.requirementComplianceRate) { var rcrp = data.requirementComplianceRate; var delayRate = 100 - rcrp.complianceRate; renderReqDelayRateGauge(delayRate, rcrp.onTimeCompletedCount, rcrp.delayedCount); } } /** * 요구사항별 지연율 게이지 차트 렌더링 */ function renderReqDelayRateGauge(delayRate, onTimeCount, delayedCount) { console.log("renderReqDelayRateGauge - delayRate:", delayRate); var canvas = document.getElementById('reqDelayRateGauge'); if (!canvas) return; if (reqDelayRateGaugeInstance) { reqDelayRateGaugeInstance.destroy(); } var color = '#ffce56'; // Yellow/Orange for delay rate var bgColor = '#2c2c2c'; reqDelayRateGaugeInstance = new Chart(canvas.getContext('2d'), { type: 'doughnut', data: { datasets: [{ data: [delayRate, 100 - delayRate], backgroundColor: [color, bgColor], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, rotation: -90, // 위쪽 반원 circumference: 180, // 반원 (180도) cutout: '65%', plugins: { legend: { display: false }, tooltip: { enabled: false }, gaugeNeedle: { needleValue: delayRate, needleColor: '#f8f8f8' } } }, plugins: [gaugeNeedlePlugin] // 화살표만 표시 }); $("#reqDelayRateText").text(delayRate.toFixed(1) + "%"); } //////////////////////////////////////////////////////////////////////////////////////// // 샘플데이터 호출 & 차트 데이터 //////////////////////////////////////////////////////////////////////////////////////// // 차트 인스턴스 저장 var chartInstances = { requirementStatusChart: null, issueStatusChart: null, requirementProcessingChart: null, issueProcessingChart: null, requirementProgressGauge: null, reqDelayRateGauge: null, completionIndicatorChart: null }; /** * 모든 차트 및 프로필 초기화 (작업자 선택 해제 시 호출) */ function clearAllCharts() { console.log("차트 및 프로필 초기화 중..."); // chartInstances에 저장된 모든 차트 destroy Object.keys(chartInstances).forEach(function(key) { if (chartInstances[key]) { chartInstances[key].destroy(); chartInstances[key] = null; } }); // reqDelayRateGaugeInstance도 초기화 if (reqDelayRateGaugeInstance) { reqDelayRateGaugeInstance.destroy(); reqDelayRateGaugeInstance = null; } // 이슈 유형 컨테이너 초기화 var issueTypeContainer = document.getElementById('issueTypeContainer'); if (issueTypeContainer) { issueTypeContainer.innerHTML = ''; } // 작업자 프로필 정보 초기화 clearAssigneeProfile(); console.log("차트 및 프로필 초기화 완료"); } /** * 작업자 프로필 정보 초기화 */ function clearAssigneeProfile() { $("#assigneeName").text("-"); $("#assigneeEmail").text("-"); $("#assigneeRole").find("span").text("-"); $("#avgDelayDaysProfile").text("-"); // 프로필 이미지를 기본 아이콘으로 복원 $("#assigneeProfileImage").html(''); } /** * 작업자 프로필 정보 렌더링 */ function renderAssigneeProfile(profileData) { if (!profileData) { clearAssigneeProfile(); return; } // 선택된 작업자 이름 사용 (Mock 데이터보다 실제 선택된 값 우선) var displayName = selectedAssigneeName || profileData.name || "-"; $("#assigneeName").text(displayName); $("#assigneeEmail").text(profileData.email || "-"); $("#assigneeRole").find("span").text(profileData.role || "-"); // 프로필 이미지가 있으면 표시, 없으면 이름 이니셜 표시 if (profileData.profileImage) { $("#assigneeProfileImage").html(''); } else { // 이름의 첫 글자를 이니셜로 표시 var initial = displayName.charAt(0).toUpperCase(); $("#assigneeProfileImage").html('' + initial + ''); } } /** * Mock 데이터 로드 및 차트 렌더링 */ function loadMockDataAndRenderCharts() { console.log(" loadMockDataAndRenderCharts Mock 데이터 로드 중..."); $.ajax({ url: '/arms/js/data/kpiMockData.json', type: 'GET', dataType: 'json', success: function(mockData) { console.log("Mock 데이터 로드 완료:", mockData); // 작업자 프로필 정보 렌더링 if (mockData.assigneeProfile && mockData.assigneeProfile.response) { renderAssigneeProfile(mockData.assigneeProfile.response); } // INS-WL-4111: Workload 측정 차트 렌더링 if (mockData.workload && mockData.workload.response) { renderWorkloadCharts(mockData.workload.response); } // INS-PD-4121: 이슈 처리 속도 차트 렌더링 if (mockData.processingSpeed && mockData.processingSpeed.response) { renderProcessingSpeedCharts(mockData.processingSpeed.response); } // INS-PD-4122: 계획 대비 효율 차트 렌더링 if (mockData.efficiency && mockData.efficiency.response) { renderEfficiencyChart(mockData.efficiency.response); } // INS-QL-4231: 일정 준수율 분석 렌더링 if (mockData.scheduleCompliance && mockData.scheduleCompliance.response) { renderScheduleComplianceData(mockData.scheduleCompliance.response); } jSuccess("Mock 데이터로 차트가 초기화되었습니다."); }, error: function(xhr, status, error) { console.error("Mock 데이터 로드 실패:", error); jError("Mock 데이터 로드 실패: " + error); } }); } /** * INS-WL-4111: Workload 측정 차트 렌더링 */ function renderWorkloadCharts(data) { console.log("call renderWorkloadCharts"); // 요구사항 상태별 분포 (도넛 차트) if (data.requirementsByStatus) { renderDoughnutChart('requirementStatusChart', data.requirementsByStatus, { 'open': { label: 'Open', color: '#3b82f6' }, 'in_progress': { label: 'In Progress', color: '#f59e0b' }, 'resolved': { label: 'Resolved', color: '#10b981' }, 'closed': { label: 'Closed', color: '#64748b' } }, 'total'); } // 이슈 상태별 분포 (도넛 차트) if (data.issuesByStatus) { renderDoughnutChart('issueStatusChart', data.issuesByStatus, { 'open': { label: 'Open', color: '#3b82f6' }, 'in_progress': { label: 'In Progress', color: '#f59e0b' }, 'resolved': { label: 'Resolved', color: '#10b981' }, 'closed': { label: 'Closed', color: '#64748b' } }, 'total'); } // 이슈 타입별 분포 (HTML 기반 수평 막대) if (data.issuesByType) { renderIssueTypeBar('issueTypeContainer', data.issuesByType, { 'Bug': '#ef4444', 'Story': '#3b82f6', 'Task': '#10b981', 'Sub-task': '#a855f7' }); } } /** * INS-PD-4121: 이슈 처리 속도 차트 렌더링 */ function renderProcessingSpeedCharts(data) { console.log("call renderProcessingSpeedCharts"); if (data.processingTimeByPriority) { var priorityData = data.processingTimeByPriority; var labels = Object.keys(priorityData); var reqData = labels.map(function(key) { return priorityData[key].requirement; }); var issueData = labels.map(function(key) { return priorityData[key].issue; }); // 요구사항 평균 처리 속도 renderProcessingChart('requirementProcessingChart', labels, reqData, '요구사항 처리 일수'); // 이슈 평균 처리 속도 renderProcessingChart('issueProcessingChart', labels, issueData, '이슈 처리 일수'); } } /** * INS-PD-4122: 계획 대비 효율 차트 렌더링 */ function renderEfficiencyChart(data) { console.log("call renderEfficiencyChart"); // 전체 효율에서 실제 진척도 계산 (첫 번째 요구사항 기준 또는 평균) var actualProgress = 0; if (data.requirementList && data.requirementList.length > 0) { var totalActual = 0; data.requirementList.forEach(function(req) { totalActual += req.actualProgress; }); actualProgress = totalActual / data.requirementList.length; } renderProgressGauge('requirementProgressGauge', actualProgress); } /** * 도넛 차트 렌더링 * 중앙에 표시할 라벨추가 */ function renderDoughnutChart(canvasId, data, colorMap, centerLabel) { console.log("call renderDoughnutChart"); var canvas = document.getElementById(canvasId); if (!canvas) return; if (chartInstances[canvasId]) { chartInstances[canvasId].destroy(); } var labels = []; var values = []; var colors = []; Object.keys(data).forEach(function(key) { if (colorMap[key]) { labels.push(colorMap[key].label); colors.push(colorMap[key].color); } else { labels.push(key); colors.push('#999'); } values.push(data[key]); }); // 총 개수 계산 var total = values.reduce(function(sum, val) { return sum + val; }, 0); chartInstances[canvasId] = new Chart(canvas.getContext('2d'), { type: 'doughnut', data: { labels: labels, datasets: [{ data: values, backgroundColor: colors, borderWidth: 0, hoverOffset: 4 }] }, options: { responsive: true, maintainAspectRatio: false, cutout: '78%', plugins: { legend: { display: true, position: 'right', labels: { color: '#ccc', font: { size: 11, weight: '500' }, usePointStyle: true, pointStyle: 'circle', boxWidth: 6, boxHeight: 6, padding: 15 } }, tooltip: { backgroundColor: 'rgba(51, 51, 51, 0.9)', titleColor: '#f8f8f8', bodyColor: '#ccc', borderColor: 'rgba(255, 255, 255, 0.1)', borderWidth: 1, padding: 8, displayColors: true, callbacks: { label: function(context) { return context.label + ': ' + context.parsed; } } } } }, plugins: [{ id: 'centerText', beforeDraw: function(chart) { var ctx = chart.ctx; var chartArea = chart.chartArea; var centerX = (chartArea.left + chartArea.right) / 2; var centerY = (chartArea.top + chartArea.bottom) / 2; ctx.save(); // 총 개수 (큰 글씨) ctx.fillStyle = '#f8f8f8'; ctx.font = 'bold 24px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(total, centerX, centerY - 8); // 라벨 (작은 글씨) if (centerLabel) { ctx.fillStyle = '#999'; ctx.font = '13px sans-serif'; ctx.fillText(centerLabel, centerX, centerY + 14); } ctx.restore(); } }] }); } /** * 이슈 유형 바 렌더링 (HTML/CSS 기반) -> 글로스효과 Chart.js로 표현이 어려움 */ function renderIssueTypeBar(containerId, data, colorMap) { console.log("call renderIssueTypeBar"); var container = document.getElementById(containerId); if (!container) return; var labels = Object.keys(data); var values = Object.values(data); var max = Math.max.apply(null, values); var html = labels.map(function(label, index) { var count = values[index]; var color = colorMap[label] || '#999'; var widthPercent = (count / max) * 100; return `
${label} ${count}
`; }).join(''); container.innerHTML = html; } /** * 수평 막대 차트 렌더링 (단순 수평 바 차트 - Y축에 라벨 표시) */ function renderHorizontalBarChart(canvasId, data, colorMap) { console.log("call renderHorizontalBarChart"); var canvas = document.getElementById(canvasId); if (!canvas) return; if (chartInstances[canvasId]) { chartInstances[canvasId].destroy(); } var labels = Object.keys(data); var values = Object.values(data); var colors = labels.map(function(key) { return colorMap[key] || '#999'; }); chartInstances[canvasId] = new Chart(canvas.getContext('2d'), { type: 'bar', data: { labels: labels, datasets: [{ label: 'Count', data: values, backgroundColor: colors, borderWidth: 0, borderRadius: { topLeft: 0, topRight: 4, bottomLeft: 0, bottomRight: 4 }, borderSkipped: false, barThickness: 20 }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, layout: { padding: { left: 0, right: 10 } }, plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(51, 51, 51, 0.9)', titleColor: '#f8f8f8', bodyColor: '#ccc', borderColor: 'rgba(255, 255, 255, 0.1)', borderWidth: 1, padding: 8 } }, scales: { x: { beginAtZero: true, display: false, grid: { display: false } }, y: { ticks: { color: '#ccc', font: { size: 10, weight: '500' } }, grid: { display: false }, border: { display: false } } } } }); } /** * TypeBar 스타일 차트 렌더링 (차트 위에 라벨 + 우측 범례) */ function renderTypeBarChart(canvasId, legendId, data, colorMap) { console.log("call renderTypeBarChart"); var canvas = document.getElementById(canvasId); if (!canvas) return; if (chartInstances[canvasId]) { chartInstances[canvasId].destroy(); } var labels = Object.keys(data); var values = labels.map(function(key) { return data[key]; }); var colors = labels.map(function(key) { return colorMap[key] || '#999'; }); // 범례 생성 (우측에 % 값 표시) var legendContainer = document.getElementById(legendId); if (legendContainer) { var legendHtml = ''; labels.forEach(function(label, index) { var displayValue = typeof values[index] === 'number' ? values[index].toFixed(1) + '%' : values[index]; legendHtml += '
' + '
' + '
' + '
' + '
' + displayValue + '
' + '
'; }); legendContainer.innerHTML = legendHtml; } chartInstances[canvasId] = new Chart(canvas.getContext('2d'), { type: 'bar', data: { labels: labels, datasets: [{ label: 'Rate', data: values, backgroundColor: colors, borderWidth: 0, borderRadius: { topLeft: 0, topRight: 4, bottomLeft: 0, bottomRight: 4 }, borderSkipped: false, barPercentage: 0.3, categoryPercentage: 0.7 }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, layout: { padding: { left: 0, right: 0, top: 0, bottom: 0 } }, plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(51, 51, 51, 0.9)', titleColor: '#f8f8f8', bodyColor: '#ccc', borderColor: 'rgba(255, 255, 255, 0.1)', borderWidth: 1, padding: 8, callbacks: { label: function(context) { return context.parsed.x.toFixed(1) + '%'; } } } }, scales: { x: { beginAtZero: true, max: 100, display: true, ticks: { stepSize: 20, color: '#666', font: { size: 9 }, callback: function(value) { return value + '%'; } }, grid: { display: true, color: 'rgba(255, 255, 255, 0.1)', borderDash: [3, 3], drawTicks: false }, border: { display: false } }, y: { position: 'left', ticks: { color: '#ccc', font: { size: 10, weight: '500' }, padding: 0 }, grid: { display: false }, border: { display: false } } } } }); } /** * 처리 속도 차트 렌더링 (수평 막대) */ function renderProcessingChart(canvasId, labels, data, label) { console.log("call renderProcessingChart"); var canvas = document.getElementById(canvasId); if (!canvas) return; if (chartInstances[canvasId]) { chartInstances[canvasId].destroy(); } // 색상 : Highest=Red, High=Orange, Medium=Blue, Low=Teal var colors = ['#FF6384', '#FFCE56', '#36A2EB', '#4BC0C0']; chartInstances[canvasId] = new Chart(canvas.getContext('2d'), { type: 'bar', data: { labels: labels, datasets: [{ label: label, data: data, backgroundColor: colors, borderWidth: 0, borderRadius: { topLeft: 0, topRight: 4, bottomLeft: 0, bottomRight: 4 }, borderSkipped: false, barThickness: 20 }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(51, 51, 51, 0.9)', titleColor: '#f8f8f8', bodyColor: '#ccc', borderColor: 'rgba(255, 255, 255, 0.1)', borderWidth: 1, padding: 8, callbacks: { label: function(context) { return context.parsed.x + ' days'; } } } }, scales: { x: { beginAtZero: true, display: false }, y: { ticks: { color: '#ccc', font: { size: 10, weight: '500' } }, grid: { display: false }, border: { display: false } } } } }); } /** * 게이지 차트 * 9시(왼쪽)에서 3시(오른쪽)까지 위쪽 반원 */ var gaugeNeedlePlugin = { id: 'gaugeNeedle', afterDatasetDraw: function(chart, args, options) { var needleValue = options.needleValue; var needleColor = options.needleColor || '#f8f8f8'; if (needleValue === undefined) return; var ctx = chart.ctx; var meta = chart.getDatasetMeta(0); if (!meta.data[0]) return; var arc = meta.data[0]; var centerX = arc.x; var centerY = arc.y; var innerRadius = arc.innerRadius; var outerRadius = arc.outerRadius; // 9시에서 시작 → 12시 → 3시 (위쪽 반원) // Canvas rotate: 0=12시, PI/2=3시, PI=6시, -PI/2=9시 var angle = -Math.PI / 2 + (needleValue / 100) * Math.PI; // 화살표 길이는 도넛 안쪽까지 var needleLength = innerRadius - 5; var needleBaseWidth = 6; ctx.save(); // 화살표 그림자 ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'; ctx.shadowBlur = 4; ctx.shadowOffsetX = 1; ctx.shadowOffsetY = 1; // 화살표 그리기 (삼각형 형태) ctx.beginPath(); ctx.translate(centerX, centerY); ctx.rotate(angle); // 화살표 본체 ctx.moveTo(-needleBaseWidth / 2, 0); ctx.lineTo(0, -needleLength); ctx.lineTo(needleBaseWidth / 2, 0); ctx.lineTo(0, 8); //화살표 꼬리부분 ctx.closePath(); ctx.fillStyle = needleColor; ctx.fill(); ctx.restore(); // 중앙 원 ctx.save(); ctx.beginPath(); ctx.arc(centerX, centerY, 8, 0, Math.PI * 2); ctx.fillStyle = '#444'; ctx.fill(); // 중앙 원 내부 하이라이트 ctx.beginPath(); ctx.arc(centerX, centerY, 5, 0, Math.PI * 2); ctx.fillStyle = needleColor; ctx.fill(); ctx.beginPath(); ctx.arc(centerX, centerY, 2, 0, Math.PI * 2); ctx.fillStyle = '#222'; ctx.fill(); ctx.restore(); } }; /** * 진척도 게이지 차트 렌더링 */ function renderProgressGauge(canvasId, progress) { console.log("renderProgressGauge - progress:", progress); var canvas = document.getElementById(canvasId); if (!canvas) return; if (chartInstances[canvasId]) { chartInstances[canvasId].destroy(); } var color = '#2ecc71'; // Green for progress var bgColor = '#2c2c2c'; chartInstances[canvasId] = new Chart(canvas.getContext('2d'), { type: 'doughnut', data: { datasets: [{ data: [progress, 100 - progress], backgroundColor: [color, bgColor], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, rotation: -90, // 위쪽 반원 circumference: 180, // 반원 (180도) cutout: '65%', plugins: { legend: { display: false }, tooltip: { enabled: false }, gaugeNeedle: { needleValue: progress, needleColor: '#f8f8f8' } } }, plugins: [gaugeNeedlePlugin] // 화살표만 표시 }); $("#reqActualProgress").text(progress.toFixed(1) + "%"); }