Index: arms/html/analysisCostV2/content-container.html
===================================================================
diff -u
--- arms/html/analysisCostV2/content-container.html (revision 0)
+++ arms/html/analysisCostV2/content-container.html (revision 9720335b98ca814f2aa59276d5a5113c482909a9)
@@ -0,0 +1,1552 @@
+
태그 제거
+ }
+ }
+ })
+ }
+ });
+}
+
+function 비용분석계산버튼() {
+ $("#cost-analysis-calculation").click(function() {
+
+ if(!selectedPdServiceId || !selectedVersionId) {
+ alert("제품(서비스), 버전을 선택해주세요.");
+ return;
+ }
+
+ let isEmpty = true;
+ // 연봉 정보 유효성 체크 및 세팅, 담당자목록 성과 초기화
+ for (let owner in 전체담당자목록) {
+ 전체담당자목록[owner].인력별소모비용 = 0;
+ 전체담당자목록[owner].완료성과 = 0;
+
+ if (isNaN(전체담당자목록[owner].연봉)) {
+ alert(owner + "의 연봉 정보가 잘못되었습니다. 숫자만 입력해주세요.");
+ return;
+ }
+ isEmpty = false;
+ }
+
+ if (isEmpty) {
+ alert("요구사항에 할당된 담당자가 없습니다.");
+ return;
+ }
+
+ 비용계산데이터_초기화();
+
+ console.log(" [ analysisCost :: 비용 분석 계산 ] :: 전체담당자목록 -> ");
+ console.log(전체담당자목록);
+
+ const url = new UrlBuilder()
+ .setBaseUrl("/auth-user/api/arms/analysis/cost/req-linked-issue")
+ .addQueryParam("pdServiceLink", selectedPdServiceId)
+ .addQueryParam("pdServiceVersionLinks", selectedVersionId)
+ .build();
+
+ const url2 = new UrlBuilder()
+ .setBaseUrl("/auth-user/api/arms/analysis/cost/T_ARMS_REQADD_"+ selectedPdServiceId + "/req-difficulty-priority-list")
+ .addQueryParam("c_req_pdservice_versionset_link", selectedVersionId)
+ .build();
+
+ const completeKeywordUrl = new UrlBuilder()
+ .setBaseUrl("/auth-user/api/arms/reqState/complete-keyword")
+ .build();
+
+ Promise.all([
+ $.ajax({ url: url, type: "GET", dataType: "json" }),
+ $.ajax({ url: url2, type: "GET", dataType: "json" }),
+ $.ajax({ url: completeKeywordUrl, type: "GET", dataType: "json" })
+ ]).then(function([data1, data2, data3]) {
+ console.log("[ analysisCost :: 비용분석계산 API 1 ] :: data1 => ");
+ console.log(data1);
+ console.log("[ analysisCost :: 비용분석계산 API 2 ] :: data2 => ");
+ console.log(data2);
+ console.log("[ analysisCost :: 비용분석계산 완료 요구사항 키워드 ] :: data3 => ");
+ console.log(data3);
+
+ 요구사항별_키목록 = data1.버전별_요구사항별_연결된지_지라이슈;
+ 요구사항전체목록 = data2.requirement;
+
+ console.log("[ analysisCost :: 비용분석계산 ] :: 게산을 위한 데이터들 => ");
+ console.log(요구사항별_키목록);
+ console.log(요구사항전체목록);
+ console.log(버전_요구사항_담당자);
+ console.log(전체담당자목록);
+
+
+ /////////////////////////////////////////////////////////////////////
+ ////////////////////////// 비용 분석 계산 /////////////////////////////
+ /////////////////////////////////////////////////////////////////////
+ Object.values(요구사항전체목록).forEach((요구사항) => {
+ // 요구사항 전체목록을 반복문 돌기 및 요구사항별 단가 초기화
+ 요구사항.요구사항금액 = 0;
+
+ // 해당 요구사항이 멀티 버전일 수 있으니 버전목록을 가져옴
+ let 요구사항이포함된버전목록 = JSON.parse(요구사항.c_req_pdservice_versionset_link);
+
+ // 요구사항의 버전목록을 반복문 돌기
+ 요구사항이포함된버전목록.forEach((버전) => {
+ // 해당 버전에 요구사항 키 목록을 가져옴
+ let 버전_요구사항_키목록 = 요구사항별_키목록[버전];
+
+ // 버전의 요구사항 목록 유무 확인
+ if (버전_요구사항_키목록 == null) {
+ }
+ else {
+ // 있을 시 계산, 버전과 요구사항 c_id로 해당 요구사항이 가진 키목록 데이터 가져오기
+ let 요구사항_키목록 = 버전_요구사항_키목록[요구사항.c_id];
+
+ // 요구사항 키 목록 유무 확인
+ if (요구사항_키목록 == null) {
+ // console.log("버전 -> " + 버전 + "\n요구사항 -> " +요구사항.c_id);
+ }
+ else {
+ // 있을 시 키목록을 반목문 돌기
+ 요구사항_키목록.forEach((요구사항키) => {
+
+ // 키 별 담당자목록 조회를 위한 버전_요구사항_담당자 중 버전 유무 확인
+ let 요구사항_담당자목록 = 버전_요구사항_담당자[버전];
+ if (요구사항_담당자목록 == null) {
+
+ }
+ else {
+ // 있으면 버전_요구사항_담당자 중 담당자목록 유무 확인
+ let 담당자목록 = 요구사항_담당자목록[요구사항키.c_issue_key];
+ if (담당자목록 == null) {
+ // console.log("요구사항 키 -> " + 요구사항키.c_issue_key + "n\요구사항_담당자목록 -> " +요구사항.c_id);
+ }
+ else {
+ // console.log("요구사항 키 -> " + 요구사항키.c_issue_key + "\n담당자 -> " + JSON.stringify(담당자목록));
+
+ // 있으면 담당자목록을 반목문 돌기
+ Object.entries(담당자목록).forEach(([key, value]) => {
+
+ // 요구사항의 비용 계산을 위한 날짜 카운팅 ****** 수정필요(정책) ******
+
+ // 요구사항 단가 계산 및 인력 성과 계산 등
+ 최종비용분석계산(key, 요구사항, 버전, 요구사항키, data3);
+ });
+ }
+ }
+ });
+ }
+ }
+ });
+ });
+
+ data2.requirement = 요구사항전체목록;
+ $("#req-cost-analysis-chart").height("500px");
+ 요구사항비용분석차트(data2);
+
+ // 인력별 성과 측정 차트
+ $("#manpower-analysis-chart2").height("500px");
+ 인력_연봉성과분포차트(전체담당자목록);
+ $("#manpower-analysis-chart").height("500px");
+ 인력별_연봉대비_성과차트(전체담당자목록);
+
+ // 버전별 투자 대비 소모 비용 차트
+ // $("#compare_costs").height("500px");
+ // compareCostsChart();
+
+ let pdServiceName;
+ pdServiceListData.forEach(elements => {
+ if (elements["pdServiceId"] === +selectedPdServiceId) {
+ pdServiceName = elements["pdServiceName"];
+ }
+ });
+
+ $("#version-stack-container").height("500px");
+ 버전소모비용스택차트();
+ }).catch(function(error) {
+ console.log('Error:', error);
+ jError("비용 분석 계산 중 에러가 발생했습니다.");
+ });
+
+ });
+}
+
+function 비용계산데이터_초기화() {
+ // 버전 소모비용 초기화
+ let 선택된_버전아이디목록 = selectedVersionId.split(',');
+ 선택된_버전아이디목록.forEach((아이디) => {
+ versionListData[아이디].버전비용 = 0;
+ });
+
+ // 버전 요구사항별 담당자별 금액 초기화
+ for (let 버전 in 버전_요구사항_담당자) {
+ for (let 요구사항키 in 버전_요구사항_담당자[버전]) {
+ for (let key in 버전_요구사항_담당자[버전][요구사항키]) {
+ 버전_요구사항_담당자[버전][요구사항키][key].버전별담당자소모비용 = 0;
+ }
+ }
+ }
+}
+
+//////////////////////////////////////////////////////////////
+/////////////// 요구사항의 일수 계산을 위한 함수///////////////////
+//////////////////////////////////////////////////////////////
+function 날짜계산(요구사항) {
+ let startDate, endDate;
+
+ if (요구사항.reqStateEntity == null) {
+ }
+ else {
+ if (요구사항.reqStateEntity.c_id === 12) {
+ startDate = new Date(formatDate(요구사항.c_req_start_date));
+ endDate = new Date(formatDate(요구사항.c_req_end_date));
+ }
+ else {
+ startDate = new Date(formatDate(요구사항.c_req_start_date));
+ endDate = new Date(formatDate(new Date()));
+ }
+ }
+
+ return {startDate, endDate};
+}
+
+function 최종비용분석계산(key, 요구사항, 버전, 요구사항키, 완료_요구사항_키워드) {
+
+ const 완료_요구사항_키워드SET = new Set(완료_요구사항_키워드);
+
+ let startDate = 요구사항.c_req_start_date
+ ? new Date(formatDate(요구사항.c_req_start_date))
+ : null;
+
+ let endDate = 요구사항.c_req_end_date
+ ? new Date(formatDate(요구사항.c_req_end_date))
+ : new Date(formatDate(new Date()));
+
+ if (요구사항.reqStateEntity != null) {
+ if (완료_요구사항_키워드SET.has(요구사항.reqStateEntity.c_title)) {
+ // 완료된 요구사항만 계산하여 완료성과 측정
+ if (startDate && endDate && (startDate <= endDate)) {
+ let cost = 담당자별_비용계산(startDate, endDate, 전체담당자목록[key].연봉);
+ 전체담당자목록[key].완료성과 += cost;
+ }
+ }
+ }
+
+ if (startDate && endDate && (startDate <= endDate)) {
+ let cost = 담당자별_비용계산(startDate, endDate, 전체담당자목록[key].연봉);
+
+ // 요구사항별 금액 측정 차트 데이터
+ 요구사항.요구사항금액 += cost;
+
+ // 인력별 성과 차트 데이터
+ 전체담당자목록[key].인력별소모비용 += cost;
+
+ // 버전별 소모비용 차트 데이터
+ versionListData[버전].버전비용 += cost;
+
+ // 요구사항 금액별 버블 차트 데이터
+ 요구사항키.cost += cost;
+
+ // 버전별 소모비용 -> 버전별 인력비용 스택차트로 변경 데이터
+ if (버전_요구사항_담당자[버전][요구사항키.c_issue_key][key].버전별담당자소모비용 == null) {
+ 버전_요구사항_담당자[버전][요구사항키.c_issue_key][key].버전별담당자소모비용 = cost;
+ } else {
+ 버전_요구사항_담당자[버전][요구사항키.c_issue_key][key].버전별담당자소모비용 += cost;
+ }
+ }
+}
+
+function 담당자별_비용계산(startDate, endDate, salary) {
+ let 업무일수 = Math.round((endDate.getTime() - startDate.getTime()) / (1000 * 3600 * 24));
+ let 일급 = Math.round((salary / 365)) * 10000;
+ return 업무일수 * 일급;
+}
+
+function 차트초기화() {
+ $("#person-select-box").hide();
+ clearChart('compare_costs');
+ clearChart('circularPacking');
+ clearChart('income_status_chart');
+ clearChart('req-cost-analysis-chart');
+ clearChart('manpower-analysis-chart');
+ clearChart('manpower-analysis-chart2');
+ clearChart('version-stack-container');
+
+
+ $("#compare_costs").height("0px");
+ $("#circularPacking").height("0px");
+ $("#income_status_chart").height("0px");
+ $("#req-cost-analysis-chart").height("0px");
+ $("#manpower-analysis-chart").height("0px");
+ $("#manpower-analysis-chart2").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 요구사항비용분석차트(data) {
+
+ console.log(" [ analysisCost :: 요구사항비용분석차트 :: data -> ");
+ console.log(data);
+ let requirementJson = data.requirement;
+ let difficultyJson = data.difficulty;
+ let priorityJson = data. priority;
+
+ // let requirementList = {};
+ // Object.values(requirementJson).forEach(item => {
+ // requirementList[item.c_title] = item.요구사항금액;
+ // });
+ //
+ // let reqTotalPrice = 0;
+ // for (let key in requirementList) {
+ // reqTotalPrice += requirementList[key];
+ // }
+ //
+ // let requirementKeys = Object.keys(requirementList);
+ // let requirementData = requirementKeys.map(key => requirementList[key]);
+ // let requirementTotalData = requirementKeys.map(key => reqTotalPrice - requirementList[key]);
+
+ let requirementList = Object.values(requirementJson).reduce((result, item) => {
+ result[item.c_title] = item.요구사항금액;
+ 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('req-cost-analysis-chart');
+ var myChart = echarts.init(dom, null, {
+ renderer: 'canvas',
+ useDirtyRect: false
+ });
+ var app = {};
+
+ 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: '#FFFFFFFF',
+ rotate: 45,
+ formatter: function(value) {
+ // 최대 10자까지 표시
+ if (value.length > 9) {
+ return value.substring(0, 9) + '...';
+ } 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%'],
+ data: difficultyData,
+ },
+ {
+ type: 'pie',
+ radius: [0, '30%'],
+ center: ['80%', '75%'],
+ 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 버전소모비용스택차트(){
+
+ const defaultValue = 0;
+
+ let selectedVersions = selectedVersionId.split(','); // 문자열을 배열로 변환
+ let stackVersionList = {};
+
+ for (let i = 0; i < selectedVersions.length; i++) {
+ const selectedVersion = selectedVersions[i];
+ const 버전별_담당자데이터 = {};
+ const item = versionListData[selectedVersion];
+ const 요구사항_담당자목록들 = 버전_요구사항_담당자[selectedVersion];
+
+ for (let 요구사항_담당자목록 in 요구사항_담당자목록들) {
+ const 담당자목록 = 요구사항_담당자목록들[요구사항_담당자목록];
+
+ for (let key in 담당자목록) {
+ const 담당자 = 담당자목록[key];
+ const newKey = `${담당자.이름}[${key}]`;
+
+ if (버전별_담당자데이터[newKey] == null) {
+ 버전별_담당자데이터[newKey] = 0;
+ }
+
+ 버전별_담당자데이터[newKey] += 담당자.버전별담당자소모비용;
+ }
+ }
+
+ stackVersionList[item.c_title] = 버전별_담당자데이터;
+ }
+
+ let stackTypeList = Object.keys(전체담당자목록).map(key => {
+ let data;
+ data = 전체담당자목록[key].이름 + "["+key+"]";
+ return data;
+ });
+
+ let chartDom = document.getElementById('version-stack-container');
+
+ let myChart = echarts.init(chartDom);
+
+ console.log(" [ analysisCost :: 버전소모비용스택차트 ] :: 버전 데이터 -> ");
+ console.log(stackVersionList);
+
+ console.log(" [ analysisCost :: 버전소모비용스택차트 ] :: 버전별 담당자 성과 누적 데이터 -> ");
+ 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 요구사항_담당자_조회(요구사항_정보) {
+ let 버전 = 요구사항_정보.versionId;
+ let 요구사항키_목록 = 요구사항_정보.issueKey;
+ let result = {};
+
+ if (버전_요구사항_담당자.hasOwnProperty(버전)) {
+ 요구사항키_목록.forEach(key => {
+ if (버전_요구사항_담당자[버전].hasOwnProperty(key)) {
+ result[key] = 버전_요구사항_담당자[버전][key];
+ }
+ });
+ }
+
+ let 요구사항_담당자_목록 = Object.keys(result).flatMap(key => Object.keys(result[key]));
+
+ console.log(" [ analysisCost :: 요구사항별_소모비용_차트 :: 선택한 요구사항 참여 인원 정보 -> ");
+ console.log(요구사항_담당자_목록);
+
+ let 일급_합산 = 0;
+
+ 요구사항_담당자_목록.forEach(key => {
+ if (전체담당자목록.hasOwnProperty(key)) {
+ let 연봉 = 전체담당자목록[key].연봉;
+ let 일급데이터 = Math.round((연봉 / 365)) * 10000;
+ 일급_합산 += 일급데이터;
+ }
+ });
+
+ return 일급_합산;
+}
+
+function 요구사항_하위이슈_일자별_소모비용(요구사항_시작일, 일급, 요구사항_이슈키별_업데이트_데이터){
+ let 일자별_소모비용 = [];
+ let assigneeList = new Set();
+
+ for (let key in 요구사항_이슈키별_업데이트_데이터) {
+ 요구사항_이슈키별_업데이트_데이터[key].forEach(data => {
+ if (data.assignee !== null) {
+ assigneeList.add(data.assignee);
+ let updated = new Date(data.updated).toISOString().split('T')[0];
+ 일자별_소모비용.push({
+ updated: updated,
+ 일급: 일급
+ });
+ }
+ });
+ }
+
+ 요구사항_시작일 = new Date(요구사항_시작일).toISOString().split('T')[0];
+
+ // Set을 사용하여 중복된 assignee를 제거하고 요구사항 시작일에 대한 데이터가 없으면 추가.
+ if (!일자별_소모비용.some(data => data.updated === 요구사항_시작일)) {
+ 일자별_소모비용.push({
+ updated: 요구사항_시작일,
+ 일급: 0
+ });
+ }
+
+ // 'updated' 필드를 기준으로 오름차순 정렬
+ 일자별_소모비용.sort((a, b) => new Date(a.updated) - new Date(b.updated));
+
+ let resultData = [일자별_소모비용[0]];
+
+ for (let i = 1; i < 일자별_소모비용.length; i++) {
+ //let 시작일 = new Date(resultData[0].updated);
+ let 이전_데이터 = new Date(일자별_소모비용[i - 1].updated);
+ let 현재_데이터 = new Date(일자별_소모비용[i].updated);
+
+ // 이전 업데이트 일자와의 차이를 일수로 계산
+ let 일자_차이 = (현재_데이터.getTime() - 이전_데이터.getTime()) / (1000 * 60 * 60 * 24);
+
+ // 일자 차이와 일급을 곱하여 일급을 다시 계산
+ // 즉 업데이트 일까지의 소모비용을 계산 한 것
+ 일자별_소모비용[i].일급 = 일자_차이 * 일자별_소모비용[i].일급;
+
+ // 중복된 날짜가 아닐 경우에만 결과 데이터에 추가
+ if (resultData[resultData.length - 1].updated !== 일자별_소모비용[i].updated) {
+ resultData.push(일자별_소모비용[i]);
+ }
+ }
+// console.log(" [ analysisCost :: 요구사항별_소모비용_차트 :: 선택한 요구사항항 일자벌 소모 비용 -> ");
+// console.log(resultData);
+
+ return resultData;
+}
+
+function 요구사항_일자별_소모비용(요구사항_시작일,요구사항_목표_종료일, 일급,요구사항_이슈키별_업데이트_데이터){
+ // 요구사항_종료일(DB에 저장된 요구사항 종료 시점) 이 없거나
+ // 요구사항_이슈키별_업데이트_데이터에서 레졸루션 데이트(es에서 수집한 데이터)가 없으면 오늘 날짜까지 만듬 있으면 해당중 가장 큰날까지 만듬
+
+ let 일자별_소모비용 = [];
+ let 오늘 = new Date();
+ console.log(요구사항_시작일);
+ console.log(요구사항_목표_종료일);
+ console.log(오늘);
+
+ if(요구사항_목표_종료일 >= 오늘){
+ for (let d = 요구사항_시작일; d <= 오늘; d.setDate(d.getDate() + 1)) {
+ let dateString = d.toISOString().split('T')[0];
+ 일자별_소모비용.push({
+ updated: dateString,
+ 일급: 일급
+ });
+ }
+ }else{
+ for (let d = 요구사항_시작일; d <= 요구사항_목표_종료일; d.setDate(d.getDate() + 1)) {
+ let dateString = d.toISOString().split('T')[0];
+ 일자별_소모비용.push({
+ updated: dateString,
+ 일급: 일급
+ });
+ }
+ }
+
+// console.log(" [ analysisCost :: 요구사항별_소모비용_차트 :: 일자별 소모 비용 -> ");
+// console.log(일자별_소모비용);
+
+ return 일자별_소모비용;
+}
+
+function reqCostStatusChart(data){
+ var chartDom = document.getElementById('income_status_chart');
+
+ if(data != null && data.versionId !== undefined){
+
+ let 요구사항_정보;
+ 요구사항_정보 = 요구사항전체목록[data.reqId];
+ console.log(" [ analysisCost :: 요구사항별_소모비용_차트 :: 선택한 요구사항 정보 -> ");
+ console.log(요구사항_정보);
+ // 요구사항이 생성된 일자를 시작일로 설정
+ let 요구사항_시작일 = new Date(요구사항_정보.c_req_start_date); // c_req_start_date
+ let 요구사항_계획일 = 요구사항_정보.c_req_plan_time;
+ let 요구사항_목표_종료일 ;
+ let 요구사항_종료일 = new Date(요구사항_정보.c_req_end_date);
+
+ if(요구사항_계획일 == null){ // 요구사항 계획일이 없으면 버전 종료일로 처리
+ 요구사항_목표_종료일 = new Date(versionListData[data.versionId].c_pds_version_end_date);
+ }else{
+ let 임시데이터 = new Date(요구사항_시작일.getTime());
+ 임시데이터.setDate(임시데이터.getDate() + 요구사항_계획일);
+ 요구사항_목표_종료일 = 임시데이터;
+ }
+
+ const url = new UrlBuilder()
+ .setBaseUrl('/auth-user/api/arms/analysis/cost/req-updated-list')
+ .addQueryParam('issueList', data.issueKey)
+ .build();
+
+ $.ajax({
+ url: url,
+ type: "GET",
+ contentType: "application/json;charset=UTF-8",
+ dataType: "json",
+ progress: true,
+ statusCode: {
+ 200: function (apiResponse) {
+ console.log(" [ analysisCost :: 요구사항별_소모비용_차트 :: data -> ");
+ console.log(apiResponse.body);
+ let 요구사항_이슈키별_업데이트_데이터 = apiResponse.body;
+
+ let allIsReqTrue = Object.values(요구사항_이슈키별_업데이트_데이터).flat().every(item => item.isReq === true);
+ let 일급 = 요구사항_담당자_조회(data);
+ let 일자별_소모비용_데이터;
+
+ if (allIsReqTrue) {// 모든 사람이 요구사항을 직접 처리 하는 경우
+ 일자별_소모비용_데이터 = 요구사항_일자별_소모비용(요구사항_시작일, 요구사항_목표_종료일, 일급,요구사항_이슈키별_업데이트_데이터);
+
+ } else {// 참여 하는 사람 중 하나라도 하위 이슈 생성하여 작업하는 경우
+ 일자별_소모비용_데이터 = 요구사항_하위이슈_일자별_소모비용(요구사항_시작일, 일급, 요구사항_이슈키별_업데이트_데이터);
+ }
+ drawReqCostStatusChart(chartDom,요구사항_정보,일급,data,요구사항_목표_종료일,일자별_소모비용_데이터);
+ }
+ }
+ });
+ }else{
+ chartDom.style.display = 'flex';
+ chartDom.style.justifyContent = 'center';
+ chartDom.style.alignItems = 'center';
+ chartDom.innerHTML = '좌측 요구사항을 선택해주세요.
';
+ }
+}
+
+function drawReqCostStatusChart(chartDom,요구사항_정보, 일급,data,요구사항_목표_종료일,일자별_소모비용_데이터){
+
+ var 예상비용 = 일급 * 요구사항_정보.c_req_plan_time;
+ 요구사항_목표_종료일 = 요구사항_목표_종료일.toISOString().substring(0, 10);
+
+ let dates = 일자별_소모비용_데이터.map(item => item.updated);
+ dates.push(요구사항_목표_종료일);
+
+ let costData = 일자별_소모비용_데이터.map(item => item.일급);
+
+ let accumulatedData = 일자별_소모비용_데이터.reduce((acc, item) => {
+ let accumulatedCost = (acc.length > 0 ? acc[acc.length - 1] : 0) + item.일급;
+ return [...acc, accumulatedCost];
+ }, []);
+
+ accumulatedData.unshift(0);
+
+ let 누적합 = costData.map((num, idx) => num + (accumulatedData[idx] || 0));
+
+ var myChart = echarts.init(chartDom, null, {
+ renderer: "canvas",
+ useDirtyRect: false
+ });
+ var option;
+ option = {
+ title: {
+ text: 요구사항_정보.c_title,
+ left: 'center',
+ textStyle: {
+ fontSize: 12,
+ color: '#FFFFFF'
+ }
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ containLabel: true
+ },
+ xAxis: {
+ type: 'category',
+ data: dates,
+ axisLabel: {
+ color: '#FFFFFF'
+ },
+ scale: true
+ },
+ yAxis: {
+ type: 'value',
+ axisLabel: {
+ color: '#FFFFFF'
+ },
+ scale: true,
+ },
+ series: [
+ {
+ name: '누적 소모 비용',
+ type: 'line',
+ lineStyle: {
+ color: 'green',
+ type: 'dashed',
+ width: 3
+ },
+ itemStyle: {
+ color: 'green'
+ },
+ data: 누적합
+ },
+ {
+ type: 'line',
+ label: {
+ show: true,
+ position: 'top'
+ },
+ markLine: {
+ lineStyle: {
+ color: '#5470c6', // line color
+ type: 'dashed', // line style
+ width: 2 // line width
+ },
+ label: {
+ position: 'middle', // label이 markLine의 중간에 위치하도록 설정
+ formatter: '예상 비용', // label의 텍스트 설정
+ fontSize: 15, // label의 폰트 크기 설정
+ color: '#FFFFFF',
+ formatter: function(){
+ return '예상 비용: '+예상비용.toLocaleString();
+ }
+ },
+ data: [
+ {
+ yAxis: 예상비용,
+ name: '예상 비용' // line label
+ }
+ ]
+ }
+ },
+ {
+ type: 'line',
+ label: {
+ show: true,
+ position: 'top'
+ },
+ markLine: {
+ lineStyle: {
+ color: '#5470c6', // line color
+ type: 'dashed', // line style
+ width: 2 // line width
+ },
+ label: {
+ position: 'middle', // label이 markLine의 중간에 위치하도록 설정
+ formatter: '요구사항 기한', // label의 텍스트 설정
+ fontSize: 15, // label의 폰트 크기 설정
+ color: '#FFFFFF',
+ formatter: function(){
+ return '요구사항 기한: '+ 요구사항_목표_종료일;
+ }
+ },
+ data: [
+ {
+ xAxis: 요구사항_목표_종료일
+ }
+ ]
+ }
+ },
+ {
+ name: '누적 소모 비용',
+ type: 'bar',
+ stack: 'Total',
+ silent: true,
+ itemStyle: {
+ borderColor: 'transparent',
+ color: 'transparent'
+ },
+ tooltip :{
+ show:false
+ },
+ emphasis: {
+ itemStyle: {
+ borderColor: 'transparent',
+ color: 'transparent'
+ }
+ },
+ data: accumulatedData // 누적값
+ },
+ {
+ name: '소모 비용',
+ type: 'bar',
+ stack: 'Total',
+ label: {
+ show: true,
+ color: '#FFFFFF',
+ position: 'top'
+ },
+ itemStyle: {
+ color: '#eb5454' // 바의 색상을 빨간색으로 변경
+ },
+ data:costData// 증폭
+ }
+ ],
+ tooltip: {
+ trigger: "axis",
+ position: "top",
+ borderWidth: 1,
+ axisPointer: {
+ type: "shadow"
+ }
+ },
+ };
+
+ if (option && typeof option === "object") {
+ myChart.setOption(option);
+ }
+ window.addEventListener("resize", myChart.resize);
+}
+
+/////////////////////////////////////////////////////////
+// 인력 연봉 대비 성과 차트
+/////////////////////////////////////////////////////////
+function 인력_연봉성과분포차트() {
+
+ const tooltipFormatter = function (params) {
+
+ let data = dataAll.filter(item => item[0] === params.value[0] && item[1] === params.value[1]);
+ let tooltipContent = '';
+
+ if (data.length > 1) {
+ for (let i = 0; i < data.length; i++) {
+ tooltipContent += data[i][2] + "
연봉 : " + data[i][0].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") +'원' + ", 성과 : " + data[i][1].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") +'원' + '
';
+ }
+ }
+ else if (data.length === 1) {
+ tooltipContent = data[0][2] + "
연봉 : " + data[0][0].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") +'원' + "
성과 : " + data[0][1].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") +'원';
+ }
+
+ tooltipContent += "
";
+
+ return tooltipContent;
+ };
+
+ let dataAll = Object.entries(전체담당자목록).map(([key, value]) => {
+ return [Number(value.연봉) *10000, Number(value.완료성과), value.이름+"["+key+"]"];
+ });
+
+ var dom = document.getElementById('manpower-analysis-chart2');
+ var myChart = echarts.init(dom, null, {
+ renderer: 'canvas',
+ useDirtyRect: false
+ });
+ var app = {};
+
+ var option;
+
+ let maxX = Math.max(...dataAll.map(item => item[0]));
+ let maxX2 = Math.max(...dataAll.map(item => item[1]));
+
+ let max = Math.max(maxX, maxX2);
+
+ const markLineOpt = {
+ animation: false,
+ label: {
+ formatter: '성과 기준선',
+ align: 'right',
+ color: 'white'
+ },
+ lineStyle: {
+ type: 'dashed',
+ color: '#EE6666',
+ width: 2
+ },
+ tooltip: {
+ formatter: '성과 기준선'
+ },
+ data: [
+ [
+ {
+ coord: [0, 0],
+ symbol: 'none'
+ },
+ {
+ coord: [max, max],
+ symbol: 'none'
+ }
+ ]
+ ]
+ };
+ option = {
+ grid: [
+ { left: '15%', top: '5%'}
+ ],
+ tooltip: {
+ confine: true,
+ /* formatter: function (params) {
+ return params.value[2] + "
연봉 : " + params.value[0] + "
성과 : " + params.value[1];
+ },*/
+ formatter: tooltipFormatter
+ },
+ xAxis: [
+ {
+ gridIndex: 0,
+ min: 0,
+ max: max,
+ axisLabel: {
+ color: 'white',
+ interval: 1,
+ rotate: 45,
+ formatter: function (value) {
+ return value === 0 ? '' : value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+ },
+ },
+ splitLine: {
+ lineStyle: {
+ color: 'gray',
+ type: 'dashed'
+ }
+ }
+ }
+ ],
+ yAxis: [
+ {
+ gridIndex: 0,
+ min: 0,
+ max: max,
+ axisLabel: {
+ color: 'white',
+ interval: 1,
+ rotate: 45,
+ },
+ splitLine: {
+ lineStyle: {
+ color: 'gray',
+ type: 'dashed'
+ }
+ }
+ }
+ ],
+ series: [
+ {
+ name: 'I',
+ type: 'scatter',
+ xAxisIndex: 0,
+ yAxisIndex: 0,
+ data: dataAll,
+ markLine: markLineOpt
+ }
+ ],
+ toolbox: {
+ show: true,
+ orient: "vertical",
+ left: "right",
+ bottom: "50px",
+ feature: {
+ mark: { show: true },
+ dataView: {show: true, readOnly: true},
+ dataZoom: {show: true}
+ },
+ iconStyle: {
+ borderColor: "white"
+ }
+ },
+ };
+
+ if (option && typeof option === 'object') {
+ myChart.setOption(option);
+ }
+
+ window.addEventListener('resize', myChart.resize);
+}
+
+function 인력별_연봉대비_성과차트(전체담당자목록) {
+ console.log(" [ analysisCost :: 인력별_연봉대비_성과차트 :: data -> ");
+ console.log(전체담당자목록);
+
+ let userData = [];
+ let salaryData = [];
+ let performanceData = [];
+
+ Object.keys(전체담당자목록).forEach((key) => {
+ userData.push(전체담당자목록[key].이름 + "[" + key + "]");
+ salaryData.push(전체담당자목록[key].연봉 * 10000);
+ performanceData.push(전체담당자목록[key].완료성과);
+ });
+
+ console.log(userData, salaryData, performanceData);
+
+ 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 전역인력맵확인() {
+ return new Promise(resolve => {
+ let intervalId = setInterval(() => {
+ console.log(전체담당자목록);
+ if (전체담당자목록.length > 0 ) {
+ clearInterval(intervalId);
+ resolve(전체담당자목록);
+ }
+ }, 500); // 100ms마다 globalDeadline 값 확인
+ });
+}
+
+/////////////////////////////////////////////////////////
+// 투입 비용 현황 차트
+/////////////////////////////////////////////////////////
+function compareCostsChart(){
+
+ console.log(" [ analysisCost :: compareCostsChart :: data -> ");
+ console.log(versionListData);
+
+ let selectedVersions = selectedVersionId.split(','); // 문자열을 배열로 변환
+
+ let selectVersionData = [];
+ for (let i = 0; i < selectedVersions.length; i++) {
+ let item = versionListData[selectedVersions[i]];
+ selectVersionData.push(item);
+ }
+
+ let chartDom = document.getElementById("compare_costs");
+ let myChart = echarts.init(chartDom);
+ let option;
+
+ let titles = selectVersionData.map(item => item.c_title);
+ let consumptionCosts = selectVersionData.map(item => item.버전비용);
+
+ option = {
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ legend: {
+ textStyle: {
+ color: '#FFFFFF'
+ }
+ },
+ grid: {
+ left: '3%',
+ right: '10%',
+ bottom: '3%',
+ containLabel: true
+ },
+ xAxis: {
+ type: 'value',
+ boundaryGap: [0, 0.01],
+ axisLabel: {
+ color: '#FFFFFF',
+ rotate: 45
+ }
+ },
+ yAxis: {
+ type: 'category',
+ data: titles,
+ axisLabel: {
+ color: '#FFFFFF'
+ }
+ },
+ series: [
+ {
+ name: '소모 비용',
+ type: 'bar',
+ data: consumptionCosts
+ }
+ ]
+ };
+
+ if (option && typeof option === "object") {
+ myChart.setOption(option, true);
+ }
+
+ window.addEventListener("resize", myChart.resize);
+}
\ No newline at end of file