var selectedPdServiceId; // 제품(서비스) 아이디 var selectedVersionId; // 선택된 버전 아이디 var dataTableRef; // 최상단 메뉴 변수 var req_state, resource_info, issue_info, period_info, total_days_progress; var modifiedRows = {}; let fileName = "인력별_연봉정보_템플릿.xlsx"; var versionRequirementAssignee = {}; // 버전 - 요구사항 - 담당자 데이터 var allAssignees = {}; // 선택된 버전의 전체 담당자 목록 var assigneeSalaryInfo = {}; // 인력별 연봉정보 데이터 var reqCostChartId = "req-cost-analysis-chart"; var costAnalysisState = false; //////////////////////////////////////////////////////////////////////////////////////// //Document Ready //////////////////////////////////////////////////////////////////////////////////////// function execDocReady() { var pluginGroups = [ [ "../reference/light-blue/lib/vendor/jquery.ui.widget.js", "../reference/light-blue/lib/vendor/http_blueimp.github.io_JavaScript-Templates_js_tmpl.js", "../reference/light-blue/lib/vendor/http_blueimp.github.io_JavaScript-Load-Image_js_load-image.js", "../reference/light-blue/lib/vendor/http_blueimp.github.io_JavaScript-Canvas-to-Blob_js_canvas-to-blob.js", "../reference/light-blue/lib/jquery.iframe-transport.js", "../reference/light-blue/lib/jquery.fileupload.js", "../reference/light-blue/lib/jquery.fileupload-fp.js", "../reference/light-blue/lib/jquery.fileupload-ui.js", "../reference/lightblue4/docs/lib/widgster/widgster.js", "../reference/lightblue4/docs/lib/slimScroll/jquery.slimscroll.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", "../reference/jquery-plugins/jspreadsheet-ce-4.13.1/dist/jsuites.js", "../reference/jquery-plugins/jspreadsheet-ce-4.13.1/dist/index.js", "../reference/jquery-plugins/jspreadsheet-ce-4.13.1/dist/jsuites.css", "../reference/jquery-plugins/jspreadsheet-ce-4.13.1/dist/jspreadsheet.css", "../reference/jquery-plugins/jspreadsheet-ce-4.13.1/dist/jspreadsheet.datatables.css", "../reference/jquery-plugins/jspreadsheet-ce-4.13.1/dist/jspreadsheet.theme.css", "./js/common/jspreadsheet/spreadsheet.js", "./css/jspreadsheet/custom_sheet.css" ], [ "../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", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/dataTables.buttons.min.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/buttons.html5.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/buttons.print.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/jszip.min.js" ], [ //chart Colors "./js/common/colorPalette.js", // Apache Echarts "../reference/jquery-plugins/echarts-5.4.3/dist/echarts.min.js", // 투입 인력별 요구사항 관여 차트 "../reference/jquery-plugins/Jit-2.0.1/jit.js", "../reference/jquery-plugins/Jit-2.0.1/Examples/css/Treemap.css", // 제품-버전-투입인력 차트 "../reference/jquery-plugins/d3-sankey-v0.12.3/d3-sankey.min.js", // d3-5.16.0 네트워크 차트 "../reference/jquery-plugins/d3-5.16.0/d3.min.js", // 최상단 메뉴 "./js/analysis/topmenu/topMenuApi.js", "./js/common/chart/eCharts/basicRadar.js", "./js/common/chart/d3/sankey.js" ] // 추가적인 플러그인 그룹들을 이곳에 추가하면 됩니다. ]; loadPluginGroupsParallelAndSequential(pluginGroups) .then(function () { //vfs_fonts 파일이 커서 defer 처리 함. setTimeout(function () { var script = document.createElement("script"); script.src = "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/vfs_fonts.js"; script.defer = true; // defer 속성 설정 document.head.appendChild(script); }, 5000); // 5초 후에 실행됩니다. //pdfmake 파일이 커서 defer 처리 함. setTimeout(function () { var script = document.createElement("script"); script.src = "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/pdfmake.min.js"; script.defer = true; // defer 속성 설정 document.head.appendChild(script); }, 5000); // 5초 후에 실행됩니다. // 사이드 메뉴 색상 설정 $(".widget").widgster(); setSideMenu("sidebar_menu_insight", "sidebar_menu_analysis", "sidebar_menu_analysis_cost"); //제품(서비스) 셀렉트 박스 이니시에이터 makePdServiceSelectBox(); //버전 멀티 셀렉트 박스 이니시에이터 makeVersionMultiSelectBox(); // 높이 조정 $(".top-menu-div").matchHeight({ target: $(".top-menu-div-scope") }); calculateCostButtonClickEvent(); costExcelUpdateButtonClick(); }) .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", type: "GET", contentType: "application/json;charset=UTF-8", dataType: "json", progress: true, async: true, statusCode: { 200: function (data) { console.log("[analysisCost :: makePdServiceSelectBox] :: pdServiceListData => "); console.log(data.response); 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(); initializeChart(); modifiedRows = {}; // 연봉정보 변경점 초기화 //refreshDetailChart(); 변수값_초기화(); // 제품( 서비스 ) 선택했으니까 자동으로 버전을 선택할 수 있게 유도 // 디폴트는 base version 을 선택하게 하고 ( select all ) //~> 이벤트 연계 함수 :: Version 표시 jsTree 빌드 bind_VersionData_By_PdService(); costAnalysisState = false; }); } // end makePdServiceSelectBox() //////////////////////////////////////// //버전 멀티 셀렉트 박스 //////////////////////////////////////// function makeVersionMultiSelectBox() { //버전 선택시 셀렉트 박스 이니시에이터 $("#multiversion").multipleSelect({ filter: true, onClose: function () { console.log("onOpen event fire!\n"); costAnalysisState = false; var checked = $("#checkbox1").is(":checked"); var endPointUrl = ""; var versionTag = $(".multiple-select").val(); console.log("[ analysisCost :: makeVersionMultiSelectBox ] :: versionTag"); console.log(versionTag); selectedVersionId = versionTag.join(","); if (versionTag === null || versionTag == "") { jError("버전이 선택되지 않았습니다."); $(".ms-parent").css("z-index", 1000); return; } initializeChart(); modifiedRows = {}; // 연봉정보 변경점 초기화 // 최상단 메뉴 세팅 TopMenuApi.톱메뉴_초기화(); TopMenuApi.톱메뉴_세팅(); getHumanResourceInfo(selectedPdServiceId, selectedVersionId); $(".ms-parent").css("z-index", 1000); }, onOpen: function () { console.log("open event"); $(".ms-parent").css("z-index", 9999); } }); } function productCostChart() { $.ajax({ url: "/auth-admin/api/arms/analysis/cost/product-accumulate-cost-by-month", type: "POST", contentType: "application/json;charset=UTF-8", dataType: "json", data: JSON.stringify({ pdServiceAndIsReq: { pdServiceLink: selectedPdServiceId, pdServiceVersionLinks: selectedVersionId.split(",").map(Number), isReq: false } }), progress: true, statusCode: { 200: function (apiResponse) { var todayStr = new Date().toISOString().split("T")[0]; var line = apiResponse.line; var bar = apiResponse.bar; var candleStick = apiResponse.candleStick; var maxCost = Math.max(...Object.values(line)); var productChartDom = document.getElementById("product-accumulate-cost-by-month"); $(productChartDom).height("500px"); var productCostChart = echarts.init(productChartDom); var option; var dates = Object.keys(line); var lineCosts = Object.values(line); var barCosts = Object.values(bar); var candleStickCosts = Object.values(candleStick); option = { dataZoom: [ { type: "slider", start: 0, end: 100 } ], tooltip: { trigger: "axis", formatter: function (params) { var lineTooltip = new Intl.NumberFormat().format(params[0].value); var barTooltip = new Intl.NumberFormat().format(params[1].value); var candleStickTooltip = params[2] ? params[2].value : null; var candleText = ""; if (candleStickTooltip) { var 시가 = new Intl.NumberFormat().format(candleStickTooltip[1]); var 종가 = new Intl.NumberFormat().format(candleStickTooltip[2]); var 최저가 = new Intl.NumberFormat().format(candleStickTooltip[3]); var 최고가 = new Intl.NumberFormat().format(candleStickTooltip[4]); candleText = `시가: ${시가}
종가: ${종가}
최저가: ${최저가}
최고가: ${최고가}`; return ( "날짜: " + params[0].name + "
성과 기준선: " + lineTooltip + "
성과: " + barTooltip + (candleText ? "
" + candleText : "") ); } else { return "날짜: " + params[0].name + "
성과 기준선: " + lineTooltip + "
성과: " + barTooltip; } } }, xAxis: { type: "category", data: dates, axisLabel: { textStyle: { color: "white" } } }, yAxis: [ { type: "value", min: 0, max: maxCost, interval: Math.floor(maxCost / 10), name: "누적 성과 비용", nameTextStyle: { color: "white" }, axisLabel: { textStyle: { color: "white" } } }, { type: "value", scale: true, name: "총 연봉 비용 변동 추이", nameTextStyle: { color: "white" }, axisLabel: { textStyle: { color: "white" } } } ], legend: { data: ["누적 성과 기준선", "누적 성과 비용", "총 연봉 비용 변동 추이"], textStyle: { color: "white" } }, series: [ { name: "누적 성과 기준선", data: lineCosts, type: "line", yAxisIndex: 0, textStyle: { color: "white" } }, { name: "누적 성과 비용", data: barCosts, type: "bar", yAxisIndex: 0, textStyle: { color: "white" } }, { name: "총 연봉 비용 변동 추이", type: "candlestick", data: candleStickCosts, yAxisIndex: 1, itemStyle: { color: "red", // 양봉 색상 color0: "blue", // 음봉 색상 borderColor: "red", // 테두리 색상 borderColor0: "blue" // 음봉 테두리 색상 // borderWidth: 3, // 테두리 두께 }, textStyle: { color: "white" } // barWidth: 3, // 캔들 가로 두께 } ] }; if (dates.includes(todayStr)) { option.series.push({ name: "Today", type: "line", markLine: { symbol: "none", lineStyle: { color: "yellow", type: "dashed", width: 2 }, label: { formatter: "Today", position: "insideEndTop", color: "yellow", fontWeight: "bold" }, data: [ { xAxis: todayStr } ] } }); } if (option && typeof option === "object") { productCostChart.setOption(option); } window.addEventListener("resize", productCostChart.resize); } } }); } function bind_VersionData_By_PdService() { $(".multiple-select option").remove(); $.ajax({ url: "/auth-user/api/arms/pdServiceVersion/pure?c_id=" + $("#selected_pdService").val(), type: "GET", dataType: "json", progress: true, statusCode: { 200: function (data) { ////////////////////////////////////////////////////////// //console.log(data.response); 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); } console.log("[ analysisCost :: bind_VersionData_By_PdService ] :: versionTag"); selectedVersionId = pdServiceVersionIds.join(","); // 최상단 메뉴 세팅 TopMenuApi.톱메뉴_초기화(); TopMenuApi.톱메뉴_세팅(); modifiedRows = {}; // 연봉정보 변경점 초기화 getHumanResourceInfo(selectedPdServiceId, selectedVersionId); if (data.length > 0) { console.log("display 재설정."); } //$('#multiversion').multipleSelect('refresh'); //$('#edit_multi_version').multipleSelect('refresh'); $(".multiple-select").multipleSelect("refresh"); ////////////////////////////////////////////////////////// } } }); } //////////////////////////////////////////////////////////////////////////////////////// // 연봉 정보 수정 PUT API 호출 //////////////////////////////////////////////////////////////////////////////////////// function getHumanResourceInfo(pdServiceLink, pdServiceVersionLinks) { $(".spinner").html( '로딩 ' + "인력정보를 조회 중 입니다..." ); $.ajax({ url: "/auth-admin/api/arms/analysis/cost/version-req-assignees", type: "POST", contentType: "application/json;charset=UTF-8", dataType: "json", data: JSON.stringify({ pdServiceAndIsReq: { pdServiceLink: pdServiceLink, pdServiceVersionLinks: pdServiceVersionLinks.split(",").map(Number) } }), progress: true, statusCode: { 200: function (apiResponse) { console.log(" [ analysisCost :: getHumanResourceInfo ] :: response data -> "); console.log(apiResponse); allAssignees = Object.fromEntries( apiResponse.allAssignees.map(item => [item.id, item]) ); console.log("analysisCost :: getHumanResourceInfo.allAssignees => ", allAssignees); costInput(allAssignees); } } }); } function formatDate(date) { return new Date(date).toISOString().split("T")[0]; } // 엑셀 파일 업로드 function file_upload_setting() { // 업로드 영역 로드 $(".body-middle").html(`
Drop files here
 
Add files...
`); // Initialize the jQuery File Upload widget: var $fileupload = $("#fileupload"); $fileupload.fileupload({ // Uncomment the following to send cross-domain cookies: //xhrFields: {withCredentials: true}, autoUpload: true, url: "/auth-admin/api/arms/salaries/excel-upload.do", dropZone: $("#dropzone"), limitMultiFileUploads: 1, paramName: "excelFile", // Callback for successful uploads: fail: function (e, data) { console.log("--------------------------"); console.log(data); jError(data.jqXHR.responseJSON.error.message); }, done: function (e, data) { console.log("--------------------------"); console.log(data); if (data.textStatus == "success") { jNotify("업로드한 연봉 정보가 반영되었습니다."); } else { jError("데이터 반영 중 에러가 발생했습니다. 엑셀 파일을 확인해주세요"); } getHumanResourceInfo(selectedPdServiceId, selectedVersionId); manpowerInput(allAssignees); //데이터 테이블 재 로드 $("#cost-analysis-calculation").click(); // 비용 계산 버튼 클릭 } }); } // 버전 비용 및 인력 비용 입력 function costInput(allAssigneesMap) { console.log(" [ analysisCost :: costInput ] :: 인력데이터 => " + JSON.stringify(allAssigneesMap)); file_upload_setting(); manpowerInput(allAssigneesMap); } function manpowerInput(allAssigneesMap) { assigneeSalaryInfo = Object.keys(allAssigneesMap).map((key) => { let data = {}; data.name = allAssigneesMap[key].name; data.key = key; data.salary = allAssigneesMap[key].salary; return data; }); console.log(" [ analysisCost :: manpowerInput ] :: assigneeSalaryInfo => " + JSON.stringify(assigneeSalaryInfo)); drawExcel("spreadsheet", assigneeSalaryInfo); // 템플릿 다운로드 excel_download(assigneeSalaryInfo); } function excel_download(assigneeSalaryInfo) { console.log(" [ analysisCost :: excel_download ] :: assigneeSalaryInfo => " + JSON.stringify(assigneeSalaryInfo)); let fileName = "인력별_연봉정보_템플릿.xlsx"; $("#excel-annual-income-template-download").click(function () { if (Object.keys(assigneeSalaryInfo).length === 0) { jError("다운로드할 인력 정보가 없습니다."); } else { $.ajax({ url: "/auth-admin/api/arms/salaries/excel-download.do?excelFileName=" + fileName, type: "POST", data: JSON.stringify(assigneeSalaryInfo), contentType: "application/json", xhrFields: { responseType: "blob" // 응답 데이터 타입을 blob으로 설정 }, statusCode: { 200: function (data) { var url = window.URL.createObjectURL(data); // blob 데이터로 URL 생성 var a = document.createElement("a"); // 다운로드 링크를 위한 태그 생성 a.href = url; // url 설정 a.download = fileName; // 파일명 설정 a.style.display = "none"; // 태그를 브라우저에 보이지 않게 설정 document.body.appendChild(a); // 태그를 body에 추가 a.click(); // 다운로드 링크 클릭 document.body.removeChild(a); // 태그 제거 } } }); } }); } function calculateCostButtonClickEvent() { $("#cost-analysis-calculation").on("click", async function () { //spinnerSettingWithText("", "비용분석 계산 중 입니다..."); costAnalysisState = true; if (!selectedPdServiceId || !selectedVersionId) { jError("제품(서비스), 버전을 선택해주세요."); return; } let isEmpty = true; // 연봉 정보 유효성 체크 및 세팅, 담당자목록 성과 초기화 for (let owner in allAssignees) { if (isNaN(allAssignees[owner].salary)) { jError(owner + "의 연봉 정보가 잘못되었습니다. 숫자만 입력해주세요."); return; } isEmpty = false; } if (isEmpty) { jError("요구사항에 할당된 담당자가 없습니다."); return; } // 투자 비용 대비 성과 차트 productCostChart(); console.log("비용분석계산 :: allAssignees -> ", allAssignees); try { $(".spinner").html( " 비용분석계산 API 실행 중입니다..." ); // checked ( Map> ) const responseData = await $.ajax({ url: "/auth-admin/api/arms/analysis/cost/calculation", type: "POST", dataType: "json", contentType: "application/json", data: JSON.stringify({ pdServiceAndIsReq: { pdServiceLink: selectedPdServiceId, pdServiceVersionLinks: selectedVersionId.split(",").map(Number) } }) }); versionRequirementAssignee = responseData.versionRequirementAssignee; const reqEntityWithDiffAndPriorityList = { requirement: responseData.requirement, difficulty: responseData.difficulty, priority: responseData.priority } console.log("[ analysisCost :: 비용분석계산 ] :: 게산을 위한 데이터들 => "); console.log(versionRequirementAssignee); console.log(allAssignees); console.log("versionCostMap => ", responseData.versionCostMap); $("#version-stack-container").height("500px"); versionCostStackChart(responseData.versionCostMap); console.log("reqEntityWithDiffAndPriorityList => ", reqEntityWithDiffAndPriorityList); $("#" + reqCostChartId).height("500px"); requirementsCostAnalysisChart(reqCostChartId, reqEntityWithDiffAndPriorityList); console.log("assigneeTimeDiffVOs => ", responseData.assigneeTimeDiffVOs); $("#manpower-analysis-chart").height("500px"); performanceToSalaryByPersonnelChart(responseData.assigneeTimeDiffVOs); } catch (error) { console.log("Error:", error); jError("비용 분석 계산 중 에러가 발생했습니다."); } }); } function initializeChart() { $("#person-select-box").hide(); clearChart("product-accumulate-cost-by-month"); clearChart("compare_costs"); clearChart(reqCostChartId); clearChart("manpower-analysis-chart"); clearChart("version-stack-container"); $("#product-accumulate-cost-by-month").height("0px"); $("#compare_costs").height("0px"); $("#" + reqCostChartId).height("0px"); $("#manpower-analysis-chart").height("0px"); $("#version-stack-container").height("0px"); } function clearChart(elementId) { var element = document.getElementById(elementId); if (element) { var chartInstance = echarts.getInstanceByDom(element); if (chartInstance) { chartInstance.clear(); } } } ///////////////////////////////////////////////////////// // 요구사항 금액 분석 그래프 ///////////////////////////////////////////////////////// function requirementsCostAnalysisChart(chartId, data) { console.log(" [ analysisCost :: requirementsCostAnalysisChart :: data -> "); console.log(data); let requirementJson = data.requirement; let difficultyJson = data.difficulty; let priorityJson = data.priority; let requirementList = Object.values(requirementJson).reduce((result, item) => { result[item.c_title] = item.reqAmount; return result; }, {}); let reqTotalPrice = Object.values(requirementList).reduce((total, amount) => total + amount, 0); let sortedRequirementList = Object.entries(requirementList).sort((a, b) => b[1] - a[1]); let requirementKeys = sortedRequirementList.map((entry) => entry[0]); let requirementData = sortedRequirementList.map((entry) => entry[1]); let requirementTotalData = sortedRequirementList.map((entry) => reqTotalPrice - entry[1]); let difficultyData = Object.keys(difficultyJson).map((key) => ({ name: key.replace(".js", ""), value: difficultyJson[key] })); let priorityData = Object.keys(priorityJson).map((key) => ({ name: key.replace(".js", ""), value: priorityJson[key] })); let size = requirementKeys.length; let zoomPersent = 1; if (size > 0) { zoomPersent = (15 / size) * 100; } var dom = document.getElementById(chartId); var myChart = echarts.init(dom, null, { renderer: "canvas", useDirtyRect: false }); var option; option = { tooltip: { confine: true }, title: [ { // text: '요구사항', subtext: "전체 " + reqTotalPrice.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + "만 원", left: "25%", textAlign: "center", textStyle: { color: "#ffffff" // 제목의 색상을 하얀색으로 변경 }, subtextStyle: { color: "#ffffff" // 부제목의 색상을 하얀색으로 변경 } }, { text: "", subtext: "난이도 및 우선순위 분포", left: "80%", bottom: "0%", textAlign: "center", textStyle: { color: "#ffffff" // 제목의 색상을 하얀색으로 변경 }, subtextStyle: { color: "#ffffff" // 부제목의 색상을 하얀색으로 변경 } } ], grid: [ { top: 50, left: "5%", right: "0%", width: "55%", bottom: "5%", containLabel: true } ], xAxis: [ { type: "value", max: reqTotalPrice, splitLine: { show: false }, axisLabel: { rotate: 45, color: "#FFFFFFFF" } } ], yAxis: [ { type: "category", data: requirementKeys, splitLine: { show: false }, axisLabel: { color: "#a4c6ff", rotate: 0, formatter: function (value) { // 최대 20자까지 표시 if (value.length > 40) { return value.substring(0, 40) + "..."; } else { return value; } } } } ], series: [ { type: "bar", stack: "chart", z: 3, label: { position: "right", show: true }, data: requirementData }, { type: "bar", stack: "chart", silent: true, itemStyle: { color: "#FFFFFF" }, data: requirementTotalData }, { type: "pie", radius: [0, "30%"], center: ["80%", "25%"], label: { show: true, textStyle: { color: "white", fontSize: 12 } }, data: difficultyData }, { type: "pie", radius: [0, "30%"], center: ["80%", "75%"], label: { show: true, textStyle: { color: "white", fontSize: 12 } }, data: priorityData } ], dataZoom: [ { type: "inside", yAxisIndex: [0], // y축에만 dataZoom 기능 적용 start: 0, end: zoomPersent }, { show: true, type: "slider", left: "0%", backgroundColor: "rgba(0,0,0,0)", // 슬라이더의 배경색 dataBackgroundColor: "rgba(255,255,255,1)", // 데이터 배경색 yAxisIndex: [0], start: 0, end: zoomPersent } ] }; if (option && typeof option === "object") { myChart.setOption(option); } window.addEventListener("resize", myChart.resize); } function versionCostStackChart(versionCostMap) { const defaultValue = 0; let selectedVersions = selectedVersionId.split(","); // 문자열을 배열로 변환 let stackVersionList = {}; for (let i = 0; i < selectedVersions.length; i++) { const selectedVersion = selectedVersions[i]; const reqAssigneesByVersion = {}; const item = versionCostMap[selectedVersion]; const reqAssigneesMap = versionRequirementAssignee[selectedVersion]; for (let reqAssignees in reqAssigneesMap) { const assigneeList = reqAssigneesMap[reqAssignees]; for (let key in assigneeList) { const assignee = assigneeList[key]; const newKey = `${assignee.name}[${key}]`; if (reqAssigneesByVersion[newKey] == null) { reqAssigneesByVersion[newKey] = 0; } reqAssigneesByVersion[newKey] += assignee.consumedCostByVersionAssignee; } } stackVersionList[item.title] = reqAssigneesByVersion; } let stackTypeList = Object.keys(allAssignees).map((key) => { let data; data = allAssignees[key].name + "[" + key + "]"; return data; }); let chartDom = document.getElementById("version-stack-container"); let myChart = echarts.init(chartDom); console.log(" [ analysisCost :: versionCostStackChart ] :: 버전 데이터 -> "); console.log(stackVersionList); console.log(" [ analysisCost :: versionCostStackChart ] :: 버전별 담당자 성과 누적 데이터 -> "); console.log(stackTypeList); let size = stackTypeList.length; let zoomPersent = 1; if (size > 0) { zoomPersent = (8 / size) * 100; } const option = { tooltip: { confine: true, trigger: "axis", axisPointer: { type: "shadow" // 'line' or 'shadow'. 기본값은 'shadow' }, formatter: function (params) { const tooltip = params.reduce((acc, param) => { const { marker, seriesName, value } = param; if (param.value > 0) { let data = param.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + "만 원"; acc += `${marker}${seriesName}: ${data}
`; } return acc; }, ""); const totalCount = params .reduce((acc, param) => acc + param.value, 0) .toString() .replace(/\B(?=(\d{3})+(?!\d))/g, ",") + "만 원"; const versionName = params[0].name; const totalTooltip = `[${versionName}] - Total: ${totalCount}
`; return totalTooltip + tooltip; } }, legend: { type: "scroll", orient: "horizontal", scrollDataIndex: 0, pageIconColor: "#2477ff", // 활성화된 페이지 버튼의 아이콘 색상 pageIconInactiveColor: "#aaa", // 비활성화된 페이지 버튼의 아이콘 색상 pageTextStyle: { color: "#fff" }, pageButtonPosition: "end", left: "center", data: stackTypeList, textStyle: { color: "white", fontSize: 11 }, formatter: function (value) { // 최대 10자까지 표시 if (value.length > 15) { return value.substring(0, 15) + "..."; } else { return value; } }, tooltip: { show: true // tooltip 활성화 } }, grid: { left: "5%", right: "0%", bottom: "0%", containLabel: true }, xAxis: { type: "value", axisLabel: { textStyle: { color: "white", fontWeight: "", fontSize: "11" }, rotate: 45 }, splitLine: { lineStyle: { type: "dashed", color: "white", width: 0.2, opacity: 0.5 } } }, yAxis: { type: "category", data: Object.keys(stackVersionList), axisLabel: { textStyle: { color: "white", fontWeight: "", fontSize: "11" } } }, series: stackTypeList.map((stackTypeData) => { const data = Object.values(stackVersionList).map( (stackVersionData) => stackVersionData[stackTypeData] || defaultValue ); return { name: stackTypeData, type: "bar", stack: "total", label: { show: false }, emphasis: { focus: "series" }, data: data }; }), dataZoom: [ { type: "inside", yAxisIndex: [0], // y축에만 dataZoom 기능 적용 start: 100 - zoomPersent, end: 100 }, { show: true, type: "slider", left: "0%", backgroundColor: "rgba(0,0,0,0)", // 슬라이더의 배경색 dataBackgroundColor: "rgba(255,255,255,1)", // 데이터 배경색 yAxisIndex: [0], start: 100 - zoomPersent, end: 100 } ] }; option && myChart.setOption(option); window.addEventListener("resize", function () { myChart.resize(); }); } function performanceToSalaryByPersonnelChart(allAssignees) { console.log(" [ analysisCost :: performanceToSalaryByPersonnelChart :: data -> "); console.log(allAssignees); let userData = []; let salaryData = []; let performanceData = []; let performanceDataByHours = []; allAssignees.forEach((element) => { let nameWithMail = element.assigneeName; if (element.emailAddress) { nameWithMail = nameWithMail + "(" + element.emailAddress + ")"; } userData.push(nameWithMail); salaryData.push(element.salary); performanceData.push(element.estimatedCostForDaysPeriod); performanceDataByHours.push(element.estimatedCostForHoursPeriod); }); let size = userData.length; let zoomPersent = 1; if (size > 0) { zoomPersent = (5 / size) * 100; } const fullScreenIconPath = "M18.25 10V5.75H14M18.25 14v4.25H14m-4 0H5.75V14m0-4V5.75H10"; var dom = document.getElementById("manpower-analysis-chart"); var myChart = echarts.init(dom, null, { renderer: "canvas", useDirtyRect: false }); var option; option = { grid: { top: "5%", left: "15%", bottom: "15%" }, tooltip: { trigger: "axis", axisPointer: { type: "none" }, confine: true }, xAxis: { data: userData, axisTick: { show: false }, axisLine: { show: false }, axisLabel: { color: "#FFFFFFFF", opacity: 1, fontSize: 11, formatter: function (value) { // 최대 10자까지 표시 if (value.length > 10) { return value.substring(0, 10) + "..."; } else { return value; } } }, scale: true }, yAxis: { splitLine: { show: false }, axisTick: { show: true }, axisLine: { show: false }, axisLabel: { show: true, color: "#FFFFFFFF", opacity: 1, rotate: 45 } }, /*color: ['#e54035'],*/ series: [ { name: "연봉", type: "pictorialBar", barCategoryGap: "0%", // symbol: 'path://M0,10 L10,10 L5,0 L0,10 z', symbol: "path://M0,10 C10,10 10,0 20,0 C30,0 30,10 40,10", itemStyle: { opacity: 0.5 }, emphasis: { itemStyle: { opacity: 0.7 } }, data: salaryData, z: 10, label: { show: false } }, { name: "성과", type: "pictorialBar", barCategoryGap: "0%", // symbol: 'path://M0,10 L10,10 L5,0 L0,10 z', symbol: "path://M0,10 C10,10 10,0 20,0 C30,0 30,10 40,10", itemStyle: { opacity: 0.5 /*color: "blue"*/ }, emphasis: { itemStyle: { opacity: 0.7 } }, data: performanceData, z: 10, label: { show: false, position: "outside", color: "#FFFFFFFF" } }, { show: false, name: "연봉 라벨", type: "pictorialBar", barCategoryGap: "0%", symbol: "path://M0,0", // 심볼을 비워서 별도의 바가 보이지 않도록 합니다. data: salaryData, z: 11, // z 값을 더 크게 설정하여 라벨이 다른 요소들 위에 오도록 합니다. label: { show: true, position: "outside", color: "white", formatter: function (params) { return params.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } }, tooltip: { show: false } }, { show: false, name: "성과 라벨", type: "pictorialBar", barCategoryGap: "0%", symbol: "path://M0,0", // 심볼을 비워서 별도의 바가 보이지 않도록 합니다. data: performanceData, z: 11, // z 값을 더 크게 설정하여 라벨이 다른 요소들 위에 오도록 합니다. label: { show: true, position: "outside", color: "white", formatter: function (params) { return params.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } }, tooltip: { show: false } } ], toolbox: { show: true, orient: "vertical", left: "right", bottom: "50px", feature: { dataZoom: { show: true }, myTool1: { show: false, title: "Full screen", icon: `path://${fullScreenIconPath}`, onclick: function () { $("#my_modal2").modal("show"); $("#my_modal2_title").text("인력별 성과 분석"); $("#my_modal2_description").text("인력별 연봉 대비 성과를 한눈에 확인할 수 있습니다."); let heights = screen.height; // window.innerHeight; console.log(heights); $("#my_modal2_body").height(heights - 450 + "px"); $("#my_modal2_body").append(`
`); setTimeout(function () { myChart.resize(); }, 500); } } }, iconStyle: { borderColor: "white" } }, dataZoom: [ { type: "inside", xAxisIndex: [0], start: 0, end: zoomPersent }, { show: true, type: "slider", bottom: "3%", backgroundColor: "rgba(0,0,0,0)", dataBackgroundColor: "rgba(255,255,255,1)", xAxisIndex: [0], start: 0, end: zoomPersent } ] }; if (option && typeof option === "object") { myChart.setOption(option); } window.addEventListener("resize", myChart.resize); } ///////////////////////////////////////////////// // 엑셀 저장 버튼 ///////////////////////////////////////////////// function costExcelUpdateButtonClick() { $("#cost-excel-batch-update").on("click", function () { if (Object.keys(modifiedRows).length === 0) { jError("수정된 연봉 데이터가 없습니다."); return; } var data = $("#spreadsheet").jexcel("getData"); var isValid = data.every(function (row) { if (!row[0] || !row[1] || !row[2]) { return true; } return !isNaN(row[2]) && row[2] !== ""; }); if (!isValid) { jError("연봉 데이터는 숫자만 입력해야하며, 값이 존재해야 합니다."); } else { $(".spinner").html( " 연봉 데이터 업데이트 중입니다..." ); $.ajax({ url: "/auth-admin/api/arms/salaries", type: "PUT", data: JSON.stringify(modifiedRows), contentType: "application/json", statusCode: { 200: function (apiResponse) { var response = apiResponse.response; getHumanResourceInfo(selectedPdServiceId, selectedVersionId); modifiedRows = {}; jSuccess("연봉 정보가 수정되었습니다."); } } }); } }); } ///////////////////////////////////////////////// // 엑셀 그리기 ///////////////////////////////////////////////// function drawExcel(target, data) { var columnList = [ { type: "text", title: "이름", wRatio: 0.25, readOnly: true }, { type: "text", title: "키", wRatio: 0.25, readOnly: true }, { type: "text", title: "연봉", wRatio: 0.5 } ]; var customOption = { search: true, allowInsertRow: false, allowInsertColumn: false, onchange: function (instance, cell, x, y, value) { var key = instance.jexcel.getValueFromCoords(1, y); // key값 if (!modifiedRows[key]) { modifiedRows[key] = {}; } modifiedRows[key].name = instance.jexcel.getValueFromCoords(0, y); modifiedRows[key].key = instance.jexcel.getValueFromCoords(1, y); modifiedRows[key].salary = value; var changedRowData = SpreadsheetFunctions.getDataAtIndex(y); changedRowData.salary = value; SpreadsheetFunctions.updateDataAtIndex(y, changedRowData); }, updateTable: function (instance, cell, col, row, val, id) { cell.style.textAlign = "left"; cell.style.whiteSpace = "normal"; }, oneditionstart: function () { SpreadsheetFunctions.isEditingCell = true; SpreadsheetFunctions.stopObserver(); }, oneditionend: function () { SpreadsheetFunctions.isEditingCell = false; SpreadsheetFunctions.startObserver(); } }; SpreadsheetFunctions.setTargetId(target); SpreadsheetFunctions.setDefaultTargetRect(); SpreadsheetFunctions.setColumns(columnList); SpreadsheetFunctions.setColumnWidth(SpreadsheetFunctions.getTargetRect("width")); SpreadsheetFunctions.setOptions(customOption); SpreadsheetFunctions.startObserver(); SpreadsheetFunctions.setExcelData(data); SpreadsheetFunctions.drawExcel(SpreadsheetFunctions.getTargetId()); } var SpreadsheetFunctions = (function () { let targetId = { v: "", jq: "" }; let targetRect = { width: 0, height: 0 }; let excelData; // 엑셀 데이터 let excelColumns; // 엑셀 컬럼 let customOptions; // 엑셀 커스텀 옵션들 :: 정의 안할 경우 default let isEditingCell = false; var setDefaultTargetRect = function () { let defaultWidth = $(getTargetId("jq")).width(); let defaultHeight = $(getTargetId("jq")).height(); setTargetRect(defaultWidth, defaultHeight); }; var setTargetRect = function (width, height) { targetRect.width = width; targetRect.height = height; }; var getTargetRect = function (type) { if (type === "width") { return targetRect.width; } else if (type === "height") { return targetRect.height; } else { return targetRect; } }; var setTargetId = function (target) { targetId.v = target; targetId.jq = "#" + target; }; var getTargetId = function (type) { if (type === "jq") { return targetId.jq; } else { return targetId.v; } }; var setExcelData = function (data) { excelData = data; }; var getExcelData = function () { return excelData; }; var updateDataAtIndex = function (index, changedData) { if (excelData) { excelData[index] = changedData; } else { console.log("updateDataAtIndex :: excelData 가 없습니다."); return false; } }; var getDataAtIndex = function (index) { if (excelData) { return excelData[index]; } else { console.log("getDataAtIndex :: excelData 가 없습니다."); return false; } }; var setColumns = function (columns) { excelColumns = columns; }; var getColumns = function () { return excelColumns; }; var setColumnWidth = function (width) { if (excelColumns) { excelColumns = excelColumns.map((column) => ({ ...column, width: width * column.wRatio - 1 })); } }; function setColumnWidthAsync(width) { return new Promise((resolve) => { if (excelColumns) { excelColumns = excelColumns.map((column) => ({ ...column, width: width * column.wRatio - 1 })); } resolve(); // 컬럼 너비 설정이 완료된 후 resolve 호출 }); } var setOptions = function (options) { customOptions = options; }; var getOptions = function () { return customOptions ? customOptions : null; }; var resizeObserver = new ResizeObserver(function (entries) { if (isEditingCell) { // 수정중에는 resize 비활성화 return; } for (let entry of entries) { setTargetRect(entry.contentRect.width, entry.contentRect.height); handleResize(entry.target.id, getTargetRect("width"), getTargetRect("height")); } }); // 모달요소 크기 변화 관찰(Observer) function startObserver() { resizeObserver.observe($(getTargetId("jq"))[0]); } // Observer 멈추기 function stopObserver() { console.log("Stopping ResizeObserver"); resizeObserver.disconnect(); // 크기 변화를 더 이상 감지하지 않음 } function handleResize(id, width, height) { if (id === getTargetId() && height !== 0) { if (excelData) { drawResizedExcel(getTargetId()); } else { console.log("Spreadsheet.handleResize :: 엑셀 데이터 없음"); } } else { console.log("Spreadsheet.handleResize :: id 불일치 또는 height 가 0 입니다."); } } function drawResizedExcel(target) { let $targetId = "#" + target; if ($($targetId).length > 0 && $($targetId)[0].jexcel) { $($targetId)[0].jexcel.destroy(); } setColumnWidthAsync(getTargetRect("width") - 50).then(() => { $($targetId).spreadsheet( $.extend( {}, { columns: getColumns(), data: getExcelData() }, getOptions() ) ); let jexcel_content_height = getTargetRect("height"); $($targetId + " .jexcel_content").css("max-height", jexcel_content_height); $($targetId + " .jexcel_content").css("width", "100%"); }); } function drawExcel(target) { let $targetId = "#" + target; if ($($targetId).length > 0 && $($targetId)[0].jexcel) { $($targetId)[0].jexcel.destroy(); } $($targetId).spreadsheet( $.extend( {}, { columns: getColumns(), data: getExcelData() }, getOptions() ) ); let jexcel_content_height = getTargetRect("height"); $($targetId + " .jexcel_content").css("max-height", jexcel_content_height); $($targetId + " .jexcel_content").css("width", "100%"); } return { setTargetId, getTargetId, setTargetRect, getTargetRect, setDefaultTargetRect, setExcelData, getExcelData, updateDataAtIndex, getDataAtIndex, setColumns, getColumns, setColumnWidth, setOptions, getOptions, startObserver, stopObserver, drawExcel }; })(); function fullScreenValidate() { if (!selectedPdServiceId || !selectedVersionId) { jError("제품(서비스), 버전을 선택해주세요."); return false; } else if (!costAnalysisState) { $("#cost-analysis-calculation").click(); } else { return true; } }