var kpiChart = null; var versionListData = []; var weeklyReqStatusTable = null; var originData = null; // 원본 데이터 저장용 var USE_MOCK_DATA = true; var MOCK_DATA_PATH = "js/reportPTR/"; ////////////////// //Document Ready ////////////////// var selectedPdServiceId; // 제품(서비스) 아이디 var selectedVersionId; // 선택된 버전 아이디 //////////////////////////////////////////////////////////////////////////////////////// //Document Ready //////////////////////////////////////////////////////////////////////////////////////// function execDocReady() { var pluginGroups = [ [ "../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", ], [ // Table "js/common/table_new.js", // Data Table "../reference/jquery-plugins/dataTables-1.10.16/media/css/jquery.dataTables_lightblue4.css", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Responsive/css/responsive.dataTables_lightblue4.css", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Select/css/select.dataTables_lightblue4.css", "../reference/jquery-plugins/dataTables-1.10.16/media/js/jquery.dataTables.min.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Responsive/js/dataTables.responsive.min.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Select/js/dataTables.select.min.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/RowGroup/js/dataTables.rowsGroup.min.js" ], [ // Slim Scroll "../reference/lightblue4/docs/lib/slimScroll/jquery.slimscroll.min.js", // Widget "../reference/lightblue4/docs/lib/widgster/widgster.js", // comming soon "../reference/jquery-plugins/timerStyles.js", // echarts "../reference/jquery-plugins/echarts-5.5.0/dist/echarts.min.js", ], ]; loadPluginGroupsParallelAndSequential(pluginGroups) .then(function () { console.log("모든 플러그인 로드 완료"); $(".widget").widgster(); //좌측 메뉴 setSideMenu( "sidebar_menu_insight", "sidebar_menu_report", "sidebar_menu_report_req_status" ); $(".top-menu-div-progress").matchHeight({ target: $(".top-menu-div") }); $(".top-menu-status-graph").matchHeight({ target: $(".top-menu-div") }); //제품(서비스) 셀렉트 박스 이니시에이터 makePdServiceSelectBox(); //버전 멀티 셀렉트 박스 이니시에이터 makeVersionMultiSelectBox(); // 페이지 로드 시 버전 카드 Mock 데이터 표시 loadVersionCards(null, null); }) .catch(function (e) { console.error("플러그인 로드 중 오류 발생"); console.error(e); }); } /////////////////////// //제품 서비스 셀렉트 박스 ////////////////////// function makePdServiceSelectBox() { //제품 서비스 셀렉트 박스 이니시에이터 $(".chzn-select").each(function () { $(this).select2($(this).data()); }); //제품 서비스 셀렉트 박스 데이터 바인딩 $.ajax({ url: "/auth-user/api/arms/pdServicePure/getPdServiceMonitor.do", type: "GET", contentType: "application/json;charset=UTF-8", dataType: "json", progress: true, async: false, 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"); } ////////////////////////////////////////////////////////// } } }); $("#selected_pdService").on("select2:open", function () { //슬림스크롤 makeSlimScroll(".select2-results__options"); }); // --- select2 ( 제품(서비스) 검색 및 선택 ) 이벤트 --- // $("#selected_pdService").on("select2:select", function (e) { selectedPdServiceId = $("#selected_pdService").val(); // 제품( 서비스 ) 선택했으니까 자동으로 버전을 선택할 수 있게 유도 // 디폴트는 base version 을 선택하게 하고 ( select all ) //~> 이벤트 연계 함수 :: Version 표시 jsTree 빌드 bind_VersionData_By_PdService(); }); } // end makePdServiceSelectBox() //////////////////////////////////////////////////////////// //버전 멀티 셀렉트 박스 (버전 옵션 선택 시) //////////////////////////////////////////////////////////// function makeVersionMultiSelectBox() { //버전 선택 셀렉트 박스 이니시에이터 $(".multiple-select").multipleSelect({ filter: true, bubbles: true, cancelable: true, onClose: function () { console.log("onOpen event fire!\n"); const versionTag = $(".multiple-select").val(); if (versionTag === null || versionTag == "") { jError("버전이 선택되지 않았습니다."); $(".ms-parent").css("z-index", 1000); return; } selectedPdServiceId = $("#selected_pdService").val(); selectedVersionId = versionTag.join(','); pdServiceProgress($("#selected_pdService").val(), selectedVersionId); reportWeeklyReqStatusTableLoad(selectedPdServiceId, selectedVersionId); loadVersionCards(selectedPdServiceId, selectedVersionId); loadWeeklyKpiTable(selectedPdServiceId, selectedVersionId); $(".ms-parent").css("z-index", 1000); }, onOpen: function() { console.log("open event"); $(".ms-parent").css("z-index", 9999); } }); } //////////////////////////////////////////////////////////// // 제품서비스 - 버전 데이터 바인딩 (제품 선택 시) //////////////////////////////////////////////////////////// function bind_VersionData_By_PdService() { $(".multiple-select option").remove(); $.ajax({ url: "/auth-user/api/arms/pdService/getVersionList?c_id=" + $("#selected_pdService").val(), type: "GET", dataType: "json", progress: true, statusCode: { 200: function (data) { ////////////////////////////////////////////////////////// var pdServiceVersionIds = []; for (var k in data.response) { var obj = data.response[k]; pdServiceVersionIds.push(obj.c_id); var newOption = new Option(obj.c_title, obj.c_id, true, false); $(".multiple-select").append(newOption); } selectedVersionId = pdServiceVersionIds.join(","); pdServiceProgress($("#selected_pdService").val(), selectedVersionId); reportWeeklyReqStatusTableLoad(selectedPdServiceId, selectedVersionId); loadVersionCards(selectedPdServiceId, selectedVersionId); loadWeeklyKpiTable(selectedPdServiceId, selectedVersionId); if (data.length > 0) { console.log("display 재설정."); } $(".multiple-select").multipleSelect("refresh"); ////////////////////////////////////////////////////////// } } }); } //////////////////////////////////////////////////////////// // Version Cards API 호출 (POST /engine/ptr/version) //////////////////////////////////////////////////////////// function loadVersionCards(pdServiceId, pdServiceVersionLinks) { var isMockEnabled = typeof USE_MOCK_DATA !== "undefined" && USE_MOCK_DATA === true; var hasMockPath = typeof MOCK_DATA_PATH !== "undefined"; renderVersionCards([]); if (isMockEnabled && hasMockPath) { $.ajax({ url: MOCK_DATA_PATH + "ptr-version.json", type: "GET", dataType: "json", success: function (data) { console.log("Version Cards (MOCK):", data); renderVersionCards(data); }, error: function (xhr, status, error) { console.error("Mock Data Load Error:", error); } }); return; } var versionIdList = pdServiceVersionLinks.split(",") .map(function (id) { return parseInt(id, 10); }) .filter(function (id) { return !isNaN(id); }); if (versionIdList.length === 0) { renderVersionCards([]); return; } var requestData = { pdServiceAndIsReq: { pdServiceLink: parseInt(pdServiceId, 10), pdServiceVersionLinks: versionIdList, isReq: true }, sortField: "progressRate", sortOrder: "ASC" }; } function pdServiceProgress(selectId, selecteVersionId) { let endPointUrl = "/T_ARMS_REQADD_" + selectId + "/calculateProgress.do"; if(selecteVersionId) { let versionInfo ="?c_req_pdservice_versionset_link=" + selecteVersionId; endPointUrl += versionInfo; } $.ajax({ url: "/auth-user/api/arms/reqAddStatePure" + endPointUrl, type: "GET", dataType: "json", progress: true, statusCode: { 200: function (result) { $("#total_work").val(result.totalWork); $("#planed_work").val(result.planWork); $("#performance_capability").val(result.performanceCapability.toFixed(0)); $("#plan_progress_rate").val(result.planProgressRate.toFixed(2)); $("#performance_progress_rate").val(result.performanceProgressRate.toFixed(2)); $("#project_progress").val(result.projectProgress.toFixed(2)); if (result.projectProgress < 0) { $("#project_progress").css("color" ,"#DB2A34"); } else { $("#project_progress").css("color", "#a4c6ff"); } } } }); } function reportWeeklyReqStatusTableLoad(pdServiceId, pdServiceVersionLinks) { let urlBuilder = new UrlBuilder().setBaseUrl( `/auth-user/api/arms/reqAddStatePure/T_ARMS_REQADD_${pdServiceId}/weeklyReportReqStatus` ); if (pdServiceVersionLinks !== undefined) { urlBuilder.addQueryParam("c_req_pdservice_versionset_link", pdServiceVersionLinks); } const url = urlBuilder.build(); $.ajax({ url: url, success: function (data) { console.log(data); weeklyChartLoad(data); } }); } // 데이터 테이블 구성 이후 꼭 구현해야 할 메소드 : 열 클릭시 이벤트 function dataTableClick(tempDataTable, selectedData) { console.log(selectedData); selectedIssue = selectedData; } // 데이터 테이블 데이터 렌더링 이후 콜백 함수. function dataTableCallBack(settings, json) { console.log("check"); if (settings.nTable.id !== "workforce_status_table") { return; } } function dataTableDrawCallback(tableInfo) { console.log("dataTableDrawCallback :: scrollPos => ", scrollPos); } var statusCell = function (data) { if (["", undefined, null].includes(data)) return "N/A"; switch (true) { case data >= 100: return ( " " + data ); case data >= 75: return ( " " + data ); case data >= 50: return ( " " + data ); default: return ( " " + data ); } }; var parseDateCell = function (data) { var date = data; if (isEmpty(data) || data === "false") { return "
N/A
"; } else { let displayText = dateFormatSlash(data); let color = "#f8f8f8"; // 기본 텍스트 색상 return "
" + displayText + "
"; } }; const defaultRender = (data) => data || "N/A"; function weeklyChartLoad(data) { const chartData = data.map(d => ({ week: d.key, plan: d.plan.resource, actual: d.actual.resource, planProgress: d.plan.progress, actualProgress: d.actual.progress })); const validActualRates = chartData .map(d => d.actualProgress / 100) .filter(rate => rate > 0 && !isNaN(rate)); const avgRate = validActualRates.length > 0 ? validActualRates.reduce((sum, r) => sum + r, 0) / validActualRates.length : 0; const { year: currentYear, week: currentWeek } = getCurrentWeek(); let currentWeekIndex = chartData.findIndex(d => d.week.includes(`${currentYear}년`) && d.week.includes(`${currentWeek}주차`) ); if (currentWeekIndex === -1) currentWeekIndex = chartData.length - 1; const planData = chartData.map(d => d.plan); const actualData = chartData.map((d, i) => i < currentWeekIndex ? d.actual : null); const forecastData = chartData.map((d, i) => i >= currentWeekIndex ? Math.round(d.plan * avgRate) : null ); const planRate = chartData.map(d => d.planProgress); const cumulativePlanRate = planRate.reduce((acc, curr, idx) => { if (idx === 0) { acc.push(curr); } else { acc.push(acc[idx - 1] + curr); } return acc; }, []); const actualRate = chartData.map((d, i) => i < currentWeekIndex ? d.actualProgress : null ); const forecastRate = chartData.map((d, i) => i >= currentWeekIndex ? Math.round(avgRate * 100) : null ); var dom = document.getElementById("multi-chart-container"); var myChart = echarts.init(dom, null, { renderer: "canvas", useDirtyRect: false }); var option; option = { tooltip: { trigger: 'axis', axisPointer: { type: "shadow" } }, legend: { data: ['계획 작업량', '실적 작업량', '예상 실적', '계획 달성률', '실적 달성률', '예상 실적률'], textStyle: { color: "white" }, tooltip: { show: true } }, xAxis: { type: 'category', data: chartData.map(d => d.week), axisLabel: { textStyle: { color: "white" } }, }, yAxis: [ { type: 'value', name: '작업량', nameTextStyle: { color: 'white' // <- 여기 추가 }, axisLabel: { textStyle: { color: "white" } }, splitLine: { show: true, lineStyle: { color: "rgba(255,255,255,0.2)", width: 1, type: "dashed" } } }, { type: 'value', name: '달성률 (%)', max: 120, nameTextStyle: { color: 'white' // <- 여기 추가 }, axisLabel: { textStyle: { color: "white" } }, splitLine: { show: true, lineStyle: { color: "rgba(255,255,255,0.2)", width: 1, type: "dashed" } } } ], series: [ { name: '계획 작업량', type: 'bar', data: planData }, { name: '실적 작업량', type: 'bar', data: actualData }, { name: '예상 실적', type: 'bar', itemStyle: { color: '#aaa' }, data: forecastData }, { name: '계획 달성률', type: 'line', yAxisIndex: 1, data: cumulativePlanRate }, { name: '실적 달성률', type: 'line', yAxisIndex: 1, data: actualRate }, { name: '예상 실적률', type: 'line', yAxisIndex: 1, lineStyle: { type: 'dashed' }, data: forecastRate } ], backgroundColor: "rgba(255,255,255,0)", }; if (option && typeof option === "object") { myChart.setOption(option, true); } window.addEventListener("resize", function () { myChart.resize(); }); } function getCurrentWeek() { const today = new Date(); const startOfYear = new Date(today.getFullYear(), 0, 1); const pastDaysOfYear = (today - startOfYear) / 86400000; const jan1Day = startOfYear.getDay() || 7; const weekNumber = Math.ceil((pastDaysOfYear + jan1Day - 1) / 7); return { year: today.getFullYear(), week: weekNumber }; } //////////////////////////////////////////////////////////// // 버전 카드 렌더링 //////////////////////////////////////////////////////////// function renderVersionCards(data) { var container = $("#version_card_container"); container.empty(); if (!data || data.length === 0) { container.html('
버전 데이터가 없습니다.
'); return; } data.forEach(function(card) { var statusClass = getStatusBadgeClass(card.status); var statusText = card.status; var progressRate = Number(card.progressRate) || 0; var achievementRate = Number(card.achievementRate) || 0; var cardHtml = `
${card.versionName || 'N/A'} ${statusText}
기간: ${card.period || 'N/A'}
전체: ${card.total || 0} 건
계획 / 완료 / 지연: ${card.planCount || 0} / ${card.completedCount || 0} / ${card.delayedCount || 0}
현재 주차: ${card.currentWeek || 0} / ${card.totalWeeks || 0} 주
진척률: ${progressRate.toFixed(2)}%
달성률: ${achievementRate.toFixed(2)}%
`; container.append(cardHtml); }); } //////////////////////////////////////////////////////////// // 상태별 클래스 반환 //////////////////////////////////////////////////////////// function getStatusBadgeClass(status) { switch (status) { case 'NORMAL': return 'bg-success'; case 'WARNING': return 'bg-warning'; case 'CRITICAL': return 'bg-danger'; default: return ''; } } //////////////////////////////////////////////////////////// // API 에러 핸들링 //////////////////////////////////////////////////////////// function handleApiError(xhr, feature) { var errorMsg = feature + " 로드 중 오류가 발생했습니다."; if (xhr.responseJSON) { var errorCode = xhr.responseJSON.code || xhr.responseJSON.errorCode; var errorMessage = xhr.responseJSON.message || xhr.responseJSON.errorMessage; if (errorCode) { errorMsg = "[" + errorCode + "] " + (errorMessage || errorMsg); } } console.error(errorMsg); } //////////////////////////////////////////////////////////// // 주차별 KPI 테이블 API 호출 //////////////////////////////////////////////////////////// function loadWeeklyKpiTable(pdServiceId, pdServiceVersionLinks) { var isMockEnabled = typeof USE_MOCK_DATA !== "undefined" && USE_MOCK_DATA === true; var hasMockPath = typeof MOCK_DATA_PATH !== "undefined"; if (isMockEnabled && hasMockPath) { $.ajax({ url: MOCK_DATA_PATH + "ptr-kpi-weekly.json", type: "GET", dataType: "json", success: function (data) { renderWeeklyKpiTable(data); }, error: function () { renderWeeklyKpiTable(null); } }); return; } } //////////////////////////////////////////////////////////// // 주차별 KPI 테이블 렌더링 //////////////////////////////////////////////////////////// function renderWeeklyKpiTable(data) { var table = $("#weekly_kpi_table"); table.empty(); var weeks = data.weeks; var versions = data.versions; var thead = $(''); // 1행: 버전, 전체, 주차 라벨들 var headerRow1 = $(''); headerRow1.append('버전'); headerRow1.append('전체'); weeks.forEach(function(week) { headerRow1.append('' + week.weekLabel + ''); }); thead.append(headerRow1); // 2행: 주단위, 누적 (주차 수만큼 반복) var headerRow2 = $(''); weeks.forEach(function() { headerRow2.append('주단위'); headerRow2.append('누적'); }); thead.append(headerRow2); // 3행: 계획, 실적, 달성률 | 계획, 실적, 달성률, 진척률 (주차 수만큼 반복) var headerRow3 = $(''); weeks.forEach(function() { // 주단위 headerRow3.append('계획'); headerRow3.append('실적'); headerRow3.append('달성률'); // 누적 headerRow3.append('계획'); headerRow3.append('실적'); headerRow3.append('달성률'); headerRow3.append('진척률'); }); thead.append(headerRow3); table.append(thead); var tbody = $(''); versions.forEach(function(version) { var row = $(''); // 버전명 row.append('' + version.versionName + ''); // 전체 (totalPlan) row.append('' + version.totalPlan + ''); // 각 주차별 데이터 weeks.forEach(function(week) { // 해당 주차의 데이터 찾기 var weekData = version.weeklyData.find(function(wd) { return wd.weekNumber === week.weekNumber; }); if (weekData) { // 주단위 row.append('' + weekData.weeklyPlan + ''); row.append('' + weekData.weeklyActual + ''); row.append('' + weekData.weeklyAchievementRate.toFixed(2) + '%'); // 누적 row.append('' + weekData.cumulativePlan + ''); row.append('' + weekData.cumulativeActual + ''); row.append('' + weekData.cumulativeAchievementRate.toFixed(2) + '%'); row.append('' + weekData.progressRate.toFixed(2) + '%'); } else { // 데이터 없으면 for (var i = 0; i < 7; i++) { row.append('-'); } } }); tbody.append(row); }); // 프로젝트 합계 행 if (data.projectTotal) { var totalRow = $(''); // 합계 라벨 totalRow.append('합계'); // 전체 (totalPlan) totalRow.append('' + data.projectTotal.totalPlan + ''); // 각 주차별 데이터 weeks.forEach(function(week) { var weekData = data.projectTotal.weeklyData.find(function(wd) { return wd.weekNumber === week.weekNumber; }); if (weekData) { // 주단위 totalRow.append('' + weekData.weeklyPlan + ''); totalRow.append('' + weekData.weeklyActual + ''); totalRow.append('' + weekData.weeklyAchievementRate.toFixed(2) + '%'); // 누적 totalRow.append('' + weekData.cumulativePlan + ''); totalRow.append('' + weekData.cumulativeActual + ''); totalRow.append('' + weekData.cumulativeAchievementRate.toFixed(2) + '%'); totalRow.append('' + weekData.progressRate.toFixed(2) + '%'); } else { for (var i = 0; i < 7; i++) { totalRow.append('-'); } } }); tbody.append(totalRow); } table.append(tbody); }