/////////////////// //Page 전역 변수 /////////////////// var selectedPdServiceId; var selectedVersionId; var versionListData; var globalDeadline; // 최상단 메뉴 변수 var req_state, resource_info, issue_info, period_info, total_days_progress; // 필요시 작성 //////////////////////////////////////////////////////////////////////////////////////// //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/lightblue4/docs/lib/slimScroll/jquery.slimscroll.min.js", "../reference/jquery-plugins/unityping-0.1.0/dist/jquery.unityping.min.js", "../reference/light-blue/lib/bootstrap-datepicker.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", "../reference/lightblue4/docs/lib/widgster/widgster.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/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", // jspreadsheet "../reference/jquery-plugins/jspreadsheet-ce-4.13.1/dist/jsuites.js", "../reference/jquery-plugins/jspreadsheet-ce-4.13.1/dist/jsuites.css", "../reference/jquery-plugins/jspreadsheet-ce-4.13.1/dist/index.js", "../reference/jquery-plugins/jspreadsheet-ce-4.13.1/dist/jspreadsheet.css", "../reference/jquery-plugins/jspreadsheet-ce-4.13.1/dist/jspreadsheet.theme.css" ], [ // echarts "../reference/jquery-plugins/echarts-5.5.0/dist/echarts.min.js", // d3(게이지 차트 사용) "../reference/jquery-plugins/d3-5.16.0/d3.min.js", // chart Colors "./js/common/colorPalette.js", // 최상단 메뉴 "./js/analysis/topmenu/topMenuApi.js", "./js/common/chart/eCharts/basicRadar.js", // 버전 timeline js, css "./js/analysis/time/D_analysisTime.js", "./js/analysis/time/timeline_analysisTime.js", "./js/dashboard/chart/infographic_custom.css", // 히트맵 사용 js, css "./js/analysis/time/calendar_yearview_blocks_analysisTime.js", "../reference/jquery-plugins/github-calendar-heatmap/css/calendar_yearview_blocks.css" ] // 추가적인 플러그인 그룹들을 이곳에 추가하면 됩니다. ]; loadPluginGroupsParallelAndSequential(pluginGroups) .then(function () { console.log("모든 플러그인 로드 완료"); //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_time"); //제품(서비스) 셀렉트 박스 이니시에이터 makePdServiceSelectBox(); //버전 멀티 셀렉트 박스 이니시에이터 makeVersionMultiSelectBox(); // 높이 조정 $(".top-menu-div").matchHeight({ target: $(".top-menu-div-scope") }); }) .catch(function (error) { console.error("플러그인 로드 중 오류 발생" + error); }); } /////////////////////// //제품 서비스 셀렉트 박스 ////////////////////// 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, statusCode: { 200: function (data) { ////////////////////////////////////////////////////////// for (let k in data.response) { let obj = data.response[k]; let 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) { // 제품( 서비스 ) 선택했으니까 자동으로 버전을 선택할 수 있게 유도 // 디폴트는 base version 을 선택하게 하고 ( select all ) //~> 이벤트 연계 함수 :: Version 표시 jsTree 빌드 dateTimePickerBinding(); dailyChartDataSearchEvent(); baseDateReset(); bindVersionDataByPdService(); console.log( "[ analysisTime :: makePdServiceSelectBox ] :: 선택된 제품(서비스) c_id = " + $("#selected_pdService").val() ); }); } // end makePdServiceSelectBox() function bindVersionDataByPdService() { $(".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) { console.log("[ analysisTime :: bindVersionDataByPdService ] :: 선택된 버전 데이터 = "); console.log(data.response); // versionData versionListData = data.response.reduce((obj, item) => { obj[item.c_id] = item; return obj; }, {}); let pdServiceVersionIds = []; for (let k in data.response) { let obj = data.response[k]; pdServiceVersionIds.push(obj.c_id); let newOption = new Option(obj.c_title, obj.c_id, true, false); $(".multiple-select").append(newOption); } selectedPdServiceId = $("#selected_pdService").val(); selectedVersionId = pdServiceVersionIds.join(","); if ( !selectedPdServiceId || selectedPdServiceId === "" ) { return; } baseDateReset(); // 최상단 메뉴 세팅 TopMenuApi.톱메뉴_초기화(); TopMenuApi.톱메뉴_세팅(); // 버전 및 게이지차트, 버전 타임라인 차트 초기화 statisticsMonitor(selectedPdServiceId, selectedVersionId); // 히트맵 차트 초기화 calendarHeatMap(selectedPdServiceId, selectedVersionId); // 요구사항 및 연결된 이슈 생성 누적 개수 및 업데이트 상태 현황 멀티 스택바 차트 dailyUpdatedStatusScatterChart(selectedPdServiceId, selectedVersionId); dailyCreatedCountAndUpdatedStatusesMultiStackCombinationChart(selectedPdServiceId, selectedVersionId); // Vertical Time Line timeLineChart(selectedPdServiceId, selectedVersionId); if (data.length > 0) { console.log("display 재설정."); } //$('#multiversion').multipleSelect('refresh'); //$('#edit_multi_version').multipleSelect('refresh'); $(".multiple-select").multipleSelect("refresh"); ////////////////////////////////////////////////////////// } } }); } //////////////////// //버전 멀티 셀렉트 박스 //////////////////// function makeVersionMultiSelectBox() { //버전 선택시 셀렉트 박스 이니시에이터 $(".multiple-select").multipleSelect({ filter: true, onClose: function () { console.log("[ analysisTime :: makeVersionMultiSelectBox ] :: onOpen event fire!\n"); let checked = $("#checkbox1").is(":checked"); let endPointUrl = ""; let versionTag = $(".multiple-select").val(); if (versionTag === null || versionTag == "") { jError("버전이 선택되지 않았습니다."); $(".ms-parent").css("z-index", 1000); return; } selectedPdServiceId = $("#selected_pdService").val(); selectedVersionId = versionTag.join(","); if (selectedPdServiceId === null || selectedPdServiceId === undefined || selectedPdServiceId === "") { return; } baseDateReset(); // 최상단 메뉴 통계 TopMenuApi.톱메뉴_초기화(); TopMenuApi.톱메뉴_세팅(); // 버전 및 게이지차트, 버전 타임라인 차트 초기화 statisticsMonitor(selectedPdServiceId, selectedVersionId); // 히트맵 차트 초기화 calendarHeatMap(selectedPdServiceId, selectedVersionId); // 스캐터 dailyUpdatedStatusScatterChart(selectedPdServiceId, selectedVersionId); // 요구사항 및 연결된 이슈 생성 누적 개수 및 업데이트 상태 현황 멀티 스택바 차트 dailyCreatedCountAndUpdatedStatusesMultiStackCombinationChart(selectedPdServiceId, selectedVersionId); // timeline chart (비동기 유지 API 2개) timeLineChart(selectedPdServiceId, selectedVersionId); $(".ms-parent").css("z-index", 1000); }, onOpen: function () { console.log("open event"); $(".ms-parent").css("z-index", 9999); } }); } function dateTimePickerBinding() { // Scatter Chart initializeScatterChart(); // Multi Stack Chart initializeMultiStackChart(); // Timeline Chart initializeTimelineChart(); } function bindStartDatePicker(startSelector, endSelector, maxRangeDays) { $(startSelector).datetimepicker({ theme: "dark", lang: "kr", onShow: function (ct) { this.setOptions({ maxDate: $(endSelector).val() ? $(endSelector).datetimepicker("getValue") : false }); }, timepicker: false, format: "Y-m-d", onSelectDate: function (ct, $i) { var startDate = $(startSelector).datetimepicker("getValue"); var endDate = $(endSelector).datetimepicker("getValue"); if (!endDate) { var newEndDate = new Date(startDate); newEndDate.setDate(startDate.getDate() + maxRangeDays); $(endSelector).val(formatDate(newEndDate)); } else { var dayDifference = (endDate - startDate) / (1000 * 60 * 60 * 24); if (dayDifference > maxRangeDays) { var newEndDate = new Date(startDate); newEndDate.setDate(startDate.getDate() + maxRangeDays); $(endSelector).val(formatDate(newEndDate)); } } } }); } function bindEndDatePicker(startSelector, endSelector, maxRangeDays) { $(endSelector).datetimepicker({ theme: "dark", lang: "kr", onShow: function (ct) { this.setOptions({ maxDate: new Date() }); }, timepicker: false, format: "Y-m-d", onSelectDate: function (ct, $i) { var startDate = $(startSelector).datetimepicker("getValue"); var endDate = $(endSelector).datetimepicker("getValue"); if (!startDate) { var newStartDate = new Date(endDate); newStartDate.setDate(endDate.getDate() - maxRangeDays); $(startSelector).val(formatDate(newStartDate)); } else { var dayDifference = (endDate - startDate) / (1000 * 60 * 60 * 24); if (dayDifference > maxRangeDays) { var newStartDate = new Date(endDate); newStartDate.setDate(endDate.getDate() - maxRangeDays); $(startSelector).val(formatDate(newStartDate)); } } } }); } function initializeScatterChart() { bindStartDatePicker("#scatter_start_date", "#scatter_end_date", 30); bindEndDatePicker("#scatter_start_date", "#scatter_end_date", 30); } function initializeMultiStackChart() { bindStartDatePicker("#multi_stack_start_date", "#multi_stack_end_date", 30); bindEndDatePicker("#multi_stack_start_date", "#multi_stack_end_date", 30); } function initializeTimelineChart() { bindStartDatePicker("#timeline_start_date", "#timeline_end_date", 180); bindEndDatePicker("#timeline_start_date", "#timeline_end_date", 180); } function baseDateReset() { globalDeadline = undefined; let today = new Date(); $("#scatter_end_date").val(formatDate(today)); $("#multi_stack_end_date").val(formatDate(today)); $("#timeline_end_date").val(formatDate(today)); let aMonthAgo = new Date(); aMonthAgo.setDate(today.getDate() - 30); $("#scatter_start_date").val(formatDate(aMonthAgo)); $("#multi_stack_start_date").val(formatDate(aMonthAgo)); $("#timeline_start_date").val(formatDate(aMonthAgo)); } function waitForGlobalDeadline() { return new Promise((resolve) => { let intervalId = setInterval(() => { if (globalDeadline !== undefined) { clearInterval(intervalId); resolve(globalDeadline); } }, 100); // 100ms마다 globalDeadline 값 확인 }); } function formatDate(date) { var year = date.getFullYear(); var month = (date.getMonth() + 1).toString().padStart(2, "0"); var day = date.getDate().toString().padStart(2, "0"); return year + "-" + month + "-" + day; } function statisticsMonitor(pdserviceId, pdserviceVersionId) { console.log("[ analysisTime :: statisticsMonitor ] :: 선택된 서비스 ===> " + pdserviceId); console.log("[ analysisTime :: statisticsMonitor ] :: 선택된 버전 리스트 ===> " + pdserviceVersionId); $(".spinner").html( '로딩 ' + "진행 현황 정보를 가져오는 중입니다..." ); //1. 좌상 게이지 차트 및 타임라인 //2. Time ( 작업일정 ) - 버전 개수 삽입 $.ajax({ url: "/auth-user/api/arms/pdService/versions-with-date?c_id=" + pdserviceId, type: "GET", contentType: "application/json;charset=UTF-8", dataType: "json", progress: true, async: false, statusCode: { 200: function (json) { let versionData = json.response; versionData.sort((a, b) => a.c_id - b.c_id); let versionCount = versionData.length; console.log("[ analysisTime :: statisticsMonitor ] :: 등록된 버전 개수 = " + versionCount); if (versionCount !== undefined) { $("#version_prgress").text(versionCount); if (versionCount >= 0) { let today = new Date(); $("#notifyNoVersion").slideUp(); $("#project-start").show(); $("#project-end").show(); $("#versionGaugeChart").html(""); //게이지 차트 초기화 var versionGauge = []; var versionTimeline = []; var versionCustomTimeline = []; versionData.forEach(function (versionElement, idx) { if (pdserviceVersionId.includes(versionElement.c_id)) { var gaugeElement = { current_date: today.toString(), version_name: versionElement.c_title, version_id: versionElement.c_id, start_date: versionElement.c_pds_version_start_date === "start" ? today : versionElement.c_pds_version_start_date, end_date: versionElement.c_pds_version_end_date === "end" ? today : versionElement.c_pds_version_end_date, ratio: 0 }; versionGauge.push(gaugeElement); } var timelineElement = { id: versionElement.c_id, title: "버전: " + versionElement.c_title, startDate: versionElement.c_pds_version_start_date === "start" ? today : versionElement.c_pds_version_start_date, endDate: versionElement.c_pds_version_end_date === "end" ? today : versionElement.c_pds_version_end_date }; versionTimeline.push(timelineElement); var versionTimelineCustomData = { title: versionElement.c_title, startDate: versionElement.c_pds_version_start_date === "start" ? today : versionElement.c_pds_version_start_date, endDate: versionElement.c_pds_version_end_date === "end" ? today : versionElement.c_pds_version_end_date }; versionCustomTimeline.push(versionTimelineCustomData); }); console.log("versionGauge => ", versionGauge); drawVersionProgress(versionGauge); // 버전 게이지 // 이번 달의 첫째 날 구하기 var firstDay = new Date(today.getFullYear(), today.getMonth(), 1); // 이번 달의 마지막 날 구하기 var lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0); // 이번달 일수 구하기 var daysCount = lastDay.getDate(); // 오늘 일자 구하기 var day = today.getDate(); var today_flag = { title: "오늘", startDate: formatDate(firstDay), endDate: formatDate(lastDay), id: "today_flag" }; versionTimeline.push(today_flag); $("#version-timeline-bar").show(); Timeline.init($("#version-timeline-bar"), versionTimeline); var basePosition = $("#today_flag").css("left"); var baseWidth = $(".month").css("width"); var calFlagPosition = (parseFloat(baseWidth) / daysCount) * day; var flagPosition = parseFloat(basePosition) + calFlagPosition + "px"; $("#today_flag").removeAttr("style"); $("#today_flag").removeClass("block"); $("#today_flag").css("position", "absolute"); $("#today_flag").css("height", "170px"); $("#today_flag").css("bottom", "-35px"); $("#today_flag span").remove(); $(".block .label").css("text-align", "left"); $("#today_flag").css("left", flagPosition); $("#today_flag").css("position", "relative"); $("#today_flag").prepend("
오늘
"); $("#today_flag").css("text-align", "center"); // 박스 위치 수정 versionData.forEach(function (version) { var id = version.c_id; var start = new Date(version.c_pds_version_start_date); var startDate = start.getDate(); var daysCount = new Date(start.getFullYear(), start.getMonth() + 1, 0).getDate(); var end = new Date(version.c_pds_version_end_date); var pos = $("#" + id).css("left"); var baseWidth = parseFloat($(".month").css("width")) / daysCount; var diffTime = Math.abs(end - start); var diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); var realWidth = baseWidth * diffDays; var realPos = parseFloat(pos) + startDate * baseWidth; $("#" + id).css("left", realPos + "px"); $("#" + id).css("width", realWidth + "px"); }); window.addEventListener("resize", $("#version-timeline-bar").width); } } } } }); } //////////////////// // 두번째 박스 //////////////////// async function drawVersionProgress(data) { let gaugeChartColor = ColorPalette.d3Chart.gaugeChart; if (gaugeChartColor.length < 0) { gaugeChartColor = [ "rgba(158, 1, 66, 0.8)", "rgba(213, 62, 79, 0.8)", "rgba(244, 109, 67, 0.8)", "rgba(253, 174, 97, 0.8)", "rgba(254, 224, 139, 0.8)", "rgba(230, 245, 152, 0.8)", "rgba(171, 221, 164, 0.8)", "rgba(102, 194, 165, 0.8)", "rgba(50, 136, 189, 0.8)", "rgba(94, 79, 162, 0.8)" ]; } let versionData = data; versionData.sort((a, b) => new Date(a.start_date) - new Date(b.start_date)); // 날짜 계산 let fastestStartDate = versionData[0].start_date; let latestEndDate = versionData.reduce( (latest, current) => (new Date(current.end_date) > new Date(latest) ? current.end_date : latest), versionData[0].end_date ); let today = new Date(versionData[0].current_date); today.setHours(0, 0, 0, 0); // 시간, 분, 초, 밀리초를 0으로 설정하여 날짜만 비교 // 시작일과 종료일은 'YYYY-MM-DD' 형식의 문자열로 가정 let startDate = new Date(fastestStartDate); startDate.setHours(0, 0, 0, 0); let endDate = new Date(latestEndDate); endDate.setHours(0, 0, 0, 0); const dayDifference = (date1, date2) => Math.floor((date1 - date2) / (1000 * 60 * 60 * 24)); let diffStart = dayDifference(today, startDate); let diffEnd = dayDifference(today, endDate); let totalDate = Math.floor(Math.abs(endDate - startDate) / (1000 * 60 * 60 * 24)) + 1; globalDeadline = formatDate(new Date(latestEndDate)); console.log("[ analysisTime :: globalDeadline ] :: globalDeadline = " + globalDeadline); // DOM 업데이트 $("#fastestStartDate").text(new Date(fastestStartDate).toLocaleDateString()); $("#latestEndDate").text(new Date(latestEndDate).toLocaleDateString()); $("#startDDay").css("color", ""); $("#endDDay").css("color", ""); if (diffStart > 0) { $("#startDDay").text("D + " + diffStart); } else if (diffStart === 0) { $("#startDDay").text("D - day"); } else { diffStart *= -1; $("#startDDay").text("D - " + diffStart); } if (diffEnd > 0) { $("#endDDay") .css("color", "#FF4D4D") .css("font-weight", "bold") .text("D + " + diffEnd) .append(" 초과"); } else if (diffEnd === 0) { $("#endDDay").text("D - day"); } else { diffEnd *= -1; $("#endDDay").text("D - " + diffEnd); } // 각 버전의 비율 계산 versionData.forEach((version) => { const versionStartDate = new Date(version.start_date); const versionEndDate = new Date(version.end_date); const versionDuration = dayDifference(versionEndDate, versionStartDate) + 1; version.ratio = (versionDuration / totalDate) * 100; // 각 버전이 전체 기간에서 차지하는 비율 }); const getTodayPosition = (start, end) => { const rangeStart = new Date(start); const rangeEnd = new Date(end); if (today < rangeStart) return 0; // 오늘이 시작 전 if (today >= rangeEnd) return 100; // 오늘이 종료 후 const rangeDuration = Math.floor((rangeEnd - rangeStart) / (1000 * 60 * 60 * 24)); const elapsedDays = Math.floor((today - rangeStart) / (1000 * 60 * 60 * 24)); if (rangeDuration <= 0) return 0; // 범위가 0일이면 0 반환 (예외 처리) return (elapsedDays / rangeDuration) * 100; }; const todayPercentage = getTodayPosition(fastestStartDate, latestEndDate); const chartDom = document.getElementById("versionGaugeChart"); if (!chartDom) return; echarts.dispose(chartDom); // 기존 차트 제거 const myChart = echarts.init(chartDom); // 새 차트 생성 // ECharts 옵션 구성 const option = { tooltip: { trigger: "item", //padding: [8, 8], textStyle: { fontSize: 12 }, formatter: (params) => { const version = versionData.find((v) => v.version_name === params.name); if (version) { return `버전명: ${params.name}
기간: ${formatDate(new Date(version.start_date))} ~ ${formatDate( new Date(version.end_date) )}`; } return params.name; } }, series: [ // 반원 도넛 차트 { name: "Version Timeline", top: "-40%", left: "-10%", right: "-10%", bottom: "-20%", type: "pie", radius: ["50%", "80%"], // 반원 크기 설정 center: ["50%", "80%"], // 반원 위치 설정 startAngle: 180, // 반원을 시작하는 각도 endAngle: 360, // 반원의 끝 각도 label: { show: true, formatter: "{b}", fontSize: 12, color: "#FFFFFF", backgroundColor: "transparent", distance: 5 }, data: versionData.map((version) => ({ value: version.ratio, // 비율 데이터 사용 name: version.version_name, itemStyle: { color: gaugeChartColor[versionData.indexOf(version) % gaugeChartColor.length], borderColor: "#FFFFFF", borderWidth: 1 } })) }, // 게이지 화살표 { name: "Today Marker", type: "gauge", radius: "80%", // 반원의 크기와 동일하게 설정 center: ["50%", "88%"], // 반원의 위치와 일치하도록 설정 startAngle: 180, endAngle: 0, splitLine: { show: false }, // 눈금 숨기기 axisLine: { lineStyle: { width: 0 } }, // 게이지 배경 숨기기 axisTick: { show: false }, // 축 틱 숨기기 axisLabel: { show: false }, // 축 레이블 숨기기 pointer: { icon: "path://M2090.36389,615.30999 L2090.36389,615.30999 C2091.48372,615.30999 2092.40383," + "616.194028 2092.44859,617.312956 L2096.90698,728.755929 C2097.05155,732.369577 2094.2393," + "735.416212 2090.62566,735.56078 C2090.53845,735.564269 2090.45117,735.566014 2090.36389," + "735.566014 L2090.36389,735.566014 C2086.74736,735.566014 2083.81557,732.63423 2083.81557," + "729.017692 C2083.81557,728.930412 2083.81732,728.84314 2083.82081,728.755929 L2088.2792," + "617.312956 C2088.32396,616.194028 2089.24407,615.30999 2090.36389,615.30999 Z", width: 7, length: "80%", itemStyle: { color: "white" } }, detail: { show: false // 텍스트 숨기기 }, data: [{ value: todayPercentage }] // 오늘 날짜에 해당하는 위치 } ] }; myChart.setOption(option); // 반응형 지원 window.addEventListener("resize", () => { myChart.resize(); }); } //////////////////// // 스캐터 차트 //////////////////// async function dailyUpdatedStatusScatterChart(pdServiceLink, pdServiceVersionLinks) { let deadline = await waitForGlobalDeadline(); let startDate = $("#scatter_start_date").val(); let endDate = $("#scatter_end_date").val(); if (!validateSearchDateWithChart(startDate, endDate)) { return; } $(".spinner").html( '로딩 ' + "일별 업데이트 상태 차트를 로딩 중입니다..." ); $.ajax({ url: "/auth-admin/api/arms/analysis/time/scatter-data", type: "POST", data: JSON.stringify({ pdServiceAndIsReq: { pdServiceLink: pdServiceLink, pdServiceVersionLinks: pdServiceVersionLinks.split(",").map(Number) }, startDate: startDate, endDate: endDate }), contentType: "application/json;charset=UTF-8", dataType: "json", progress: true, async: false, statusCode: { 200: function (data) { console.log("[ analysisTime :: dailyUpdatedStatusScatterChart ] :: 일별 업데이트 상태 스캐터 차트 데이터 = ", data); let result = data.reduce( (acc, entry) => { if (entry.requirement !== 0 || entry.relationIssue !== 0) { acc.dates.push(entry.date); acc.requirement.push(entry.requirement); acc.relationIssue.push(entry.relationIssue); } return acc; }, { dates: [], requirement: [], relationIssue: [] } ); let dates = result.dates; let totalRequirements = result.requirement; let totalRelationIssues = result.relationIssue; let deadlineSeries = createDeadlineSeries( dates, totalRequirements, totalRelationIssues, globalDeadline, false, 2 ); var dom = document.getElementById("scatter-chart-container"); var myChart = echarts.init(dom, "dark", { renderer: "canvas", useDirtyRect: false }); var option; if ( (totalRequirements && totalRequirements.length > 0) || (totalRelationIssues && totalRelationIssues.length > 0) ) { option = { aria: { show: true }, legend: { data: ["요구사항", "연결된 이슈"], textStyle: { color: "white" } }, grid: { left: "3%", right: "3%", bottom: "1%", containLabel: true }, xAxis: { type: "category", axisTick: { show: false }, data: dates, axisLabel: { textStyle: { color: "white" } } }, yAxis: { type: "value", splitLine: { show: true, lineStyle: { color: "rgba(255,255,255,0.2)", width: 1, type: "dashed" } }, axisLabel: { textStyle: { color: "white" } } }, series: [ { name: "요구사항", data: totalRequirements, type: "scatter", symbol: "diamond", clip: false, label: { show: false }, symbolSize: function (val) { return val > 10 ? val * 1.1 : val === 0 ? 0 : 10; } }, { name: "연결된 이슈", data: totalRelationIssues, type: "scatter", clip: false, label: { show: false }, symbolSize: function (val) { return val > 10 ? val * 1.1 : val === 0 ? 0 : 10; }, itemStyle: { color: "#13de57" } }, ...deadlineSeries ], tooltip: { trigger: "axis", position: "top", borderWidth: 1, axisPointer: { type: "line", label: { formatter: function (params) { return formatDate(new Date(params.value)); } } } }, backgroundColor: "rgba(255,255,255,0)", animationDelay: function (idx) { return idx * 20; }, animationDelayUpdate: function (idx) { return idx * 20; } }; myChart.setOption(option, true); adjustScatterChartHeight(); myChart.on("mouseover", function (params) { if (params.seriesType === "scatter" && params.seriesIndex !== undefined) { var option = myChart.getOption(); var series = option.series[params.seriesIndex]; if (series) { series.label.show = false; myChart.setOption(option, false, true); } } }); myChart.on("mouseout", function (params) { if (params.seriesType === "scatter" && params.seriesIndex !== undefined) { var option = myChart.getOption(); var series = option.series[params.seriesIndex]; if (series) { series.label.show = false; myChart.setOption(option, false, true); } } }); } else { option = { title: { text: "데이터가 없습니다.", left: "center", top: "middle", textStyle: { color: "#fff", fontFamily: "Nanum Gothic", fontWeight: "normal", fontSize: "13px" } }, backgroundColor: "rgba(255,255,255,0)" }; myChart.setOption(option, true); } window.addEventListener("resize", () => { myChart.resize(); adjustScatterChartHeight(); }); } } }); } //////////////////// // 히트맵 차트 //////////////////// function calendarHeatMap(pdServiceLink, pdServiceVersionLinks) { $("#calendar_yearview_blocks_chart_1 svg").remove(); $("#calendar_yearview_blocks_chart_2 svg").remove(); $.ajax({ url: "/auth-admin/api/arms/analysis/time/heatmap-data", type: "POST", data: JSON.stringify({ pdServiceAndIsReq: { pdServiceLink: pdServiceLink, pdServiceVersionLinks: pdServiceVersionLinks.split(",").map(Number) } }), contentType: "application/json;charset=UTF-8", dataType: "json", progress: true, async: false, statusCode: { 200: function (data) { console.log("[ analysisTime :: calendarHeatMap ] :: 누적 업데이트 히트맵 차트데이터 = "); console.log(data); $(".update-title").show(); $("#calendar_yearview_blocks_chart_1").calendar_yearview_blocks({ data: JSON.stringify(data.requirement), start_monday: true, always_show_tooltip: true, month_names: ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sept", "oct", "nov", "dec"], day_names: ["mon", "wed", "fri", "sun"] }); $("#calendar_yearview_blocks_chart_2").calendar_yearview_blocks({ data: JSON.stringify(data.relationIssue), start_monday: true, always_show_tooltip: true, month_names: ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"], day_names: ["mon", "wed", "fri", "sun"] }); } } }); } function adjustScatterChartHeight() { const heatmapBody = document.getElementById("heatmap-body"); const scatterChartContainer = document.getElementById("scatter-chart-container"); if (!heatmapBody || !scatterChartContainer) return; setTimeout(() => { const heatmapHeight = heatmapBody.offsetHeight; const adjustedHeight = heatmapHeight > 415 ? heatmapHeight - 49 : 366; scatterChartContainer.style.minHeight = `${adjustedHeight}px`; scatterChartContainer.style.height = `${adjustedHeight}px`; }, 10); } //////////////// // 멀티 콤비네이션 차트 /////////////// async function dailyCreatedCountAndUpdatedStatusesMultiStackCombinationChart(pdServiceLink, pdServiceVersionLinks) { let deadline = await waitForGlobalDeadline(); let startDate = $("#multi_stack_start_date").val(); let endDate = $("#multi_stack_end_date").val(); if (!validateSearchDateWithChart(startDate, endDate)) { return; } $(".spinner").html( '로딩 ' + "생성 개수 및 업데이트 상태 현황 정보를 로딩 중입니다..." ); $.ajax({ url: "/auth-admin/api/arms/analysis/time/updated-issue/multi-combination-data", type: "POST", data: JSON.stringify({ pdServiceAndIsReq: { pdServiceLink: pdServiceLink, pdServiceVersionLinks: pdServiceVersionLinks.split(",").map(Number) }, startDate: startDate, endDate: endDate }), contentType: "application/json;charset=UTF-8", dataType: "json", progress: true, async: false, statusCode: { 200: function (data) { console.log( "[ analysisTime :: dailyCreatedCountAndUpdatedStatusesMultiStackCombinationChart ] :: 일별 이슈 생성 개수 및 업데이트 현황 데이터 => ", data ); var accumulateRequirementCount = 0; var accumulateRelationIssueCount = 0; let result = data.reduce( (acc, item) => { if ( item.totalRequirements !== 0 || item.totalRelationIssues !== 0 || item.requirementStatuses.length > 0 || item.relationIssueStatuses.length > 0 ) { acc.dates.push(item.date); accumulateRequirementCount += item.totalRequirements; accumulateRelationIssueCount += item.totalRelationIssues; acc.totalRequirements.push(accumulateRequirementCount); acc.totalRelationIssues.push(accumulateRelationIssueCount); } item.requirementStatuses.forEach((statusObj) => { if (!acc.statusKeys.includes(statusObj.status)) { acc.statusKeys.push(statusObj.status); } }); item.relationIssueStatuses.forEach((statusObj) => { if (!acc.statusKeys.includes(statusObj.status)) { acc.statusKeys.push(statusObj.status); } }); return acc; }, { dates: [], totalRequirements: [], totalRelationIssues: [], statusKeys: [] } ); var dom = document.getElementById("multi-chart-container"); var myChart = echarts.init(dom, null, { renderer: "canvas", useDirtyRect: false }); var option; if (result && result.dates.length > 0) { var labelOption = { show: false, position: "top", distance: 0, align: "center", verticalAlign: "top", rotate: 0, formatter: "{c}", fontSize: 14, rich: { name: {} } }; let dates = result.dates; let totalRequirements = result.totalRequirements; let totalRelationIssues = result.totalRelationIssues; let statusKeys = result.statusKeys; let requirementStatusSeries = statusKeys.map((key) => { let stackType = "요구사항"; return { name: key, type: "bar", stack: stackType, label: labelOption, emphasis: { focus: "series" }, data: dates.map((date) => { let item = data.find((d) => d.date === date); let status = item.requirementStatuses.find((s) => s.status === key); return { value: status ? status.count : 0, stackType: stackType }; }) }; }); let relationIssueStatusSeries = statusKeys.map((key) => { let stackType = "연결된 이슈"; return { name: key, type: "bar", stack: stackType, label: labelOption, emphasis: { focus: "series" }, data: dates.map((date) => { let item = data.find((d) => d.date === date); let status = item.relationIssueStatuses.find((s) => s.status === key); return { value: status ? status.count : 0, stackType: stackType }; }) }; }); statusKeys.push("요구사항"); statusKeys.push("연결된 이슈"); let multiCombinationChartSeries = [ ...requirementStatusSeries, ...relationIssueStatusSeries, { name: "요구사항", type: "line", emphasis: { focus: "series" }, symbolSize: 10, data: totalRequirements }, { name: "연결된 이슈", type: "line", emphasis: { focus: "series" }, symbolSize: 10, data: totalRelationIssues } ]; var legendData = statusKeys; var xAiasData = dates; option = { tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, formatter: function (params) { var tooltipText = ""; tooltipText += params[0].axisValue + "
"; params.forEach(function (item) { if (item.value !== 0) { if (item.seriesType === "bar") { var stackType = item.data.stackType; tooltipText += item.marker + item.seriesName + "[" + stackType + "]" + '   ' + item.value + "" + "
"; } else if (item.seriesType === "line") { tooltipText += item.marker + item.seriesName + '   ' + item.value + "" + "
"; } } }); return tooltipText; } }, legend: { data: legendData, textStyle: { color: "white" }, tooltip: { show: true } }, grid: { left: "3%", right: "3%", bottom: "1%", containLabel: true }, toolbox: { show: true, orient: "vertical", left: "right", bottom: "50px", feature: { mark: { show: true }, dataZoom: { show: false } }, iconStyle: { borderColor: "white" } }, xAxis: [ { type: "category", axisTick: { show: false }, data: xAiasData, axisLabel: { textStyle: { color: "white" } } } ], yAxis: [ { type: "value", axisLabel: { textStyle: { color: "white" } }, splitLine: { show: true, lineStyle: { color: "rgba(255,255,255,0.2)", width: 1, type: "dashed" } } }, { type: "value", position: "right", axisLabel: { textStyle: { color: "white" } } } ], series: multiCombinationChartSeries, backgroundColor: "rgba(255,255,255,0)", animationDelay: function (idx) { return idx * 20; }, animationDelayUpdate: function (idx) { return idx * 20; } }; } else { option = { title: { text: "데이터가 없습니다.", left: "center", top: "middle", textStyle: { color: "#fff", fontFamily: "Nanum Gothic", fontWeight: "normal", fontSize: "13px" } }, backgroundColor: "rgba(255,255,255,0)" }; } if (option && typeof option === "object") { myChart.setOption(option, true); } window.addEventListener("resize", function () { myChart.resize(); }); myChart.on("mouseover", function (params) { var option = myChart.getOption(); option.series[params.seriesIndex].label.show = false; myChart.setOption(option); }); myChart.on("mouseout", function (params) { var option = myChart.getOption(); option.series[params.seriesIndex].label.show = false; myChart.setOption(option); }); // 자동 색상 배열 가져오기 const actualSeries = myChart.getOption().series; const requirementLineIndex = actualSeries.findIndex((s) => s.name === "요구사항" && s.type === "line"); const relationIssueLineIndex = actualSeries.findIndex((s) => s.name === "연결된 이슈" && s.type === "line"); // 색상 초기화 let requirementLineColor = "transparent"; let relationIssueLineColor = "transparent"; // 데이터가 있는 경우에만 실제 색상 가져오기 if (requirementLineIndex !== -1 && relationIssueLineIndex !== -1) { // 진짜 색상 가져오기 requirementLineColor = myChart.getVisual({ seriesIndex: requirementLineIndex }, "color"); relationIssueLineColor = myChart.getVisual({ seriesIndex: relationIssueLineIndex }, "color"); } // series가 배열인지 확인하고 처리 if (!option.series || !Array.isArray(option.series)) { option.series = []; console.warn('시리즈 데이터 없거나 배열 형식이 아닙니다.'); myChart.setOption(option, false); return; } // bar 시리즈 업데이트 const newSeries = option.series.map((s) => { if (s.stack === "요구사항" && s.type === "bar") { return { ...s, data: s.data.map((d) => { const hasValue = d.value > 0; return { ...d, itemStyle: { ...d.itemStyle, borderColor: hasValue ? requirementLineColor : "transparent", borderLeftColor: hasValue ? requirementLineColor : "transparent", borderRightColor: hasValue ? requirementLineColor : "transparent", borderTopColor: d.itemStyle?.borderTopWidth ? requirementLineColor : "transparent", borderBottomColor: "transparent", borderWidth: 2, borderTopWidth: d.itemStyle?.borderTopWidth || 0, borderLeftWidth: 2, borderRightWidth: 2, borderBottomWidth: 0, borderType: "solid" } }; }) }; } if (s.stack === "연결된 이슈" && s.type === "bar") { return { ...s, data: s.data.map((d) => { const hasValue = d.value > 0; return { ...d, itemStyle: { ...d.itemStyle, borderColor: hasValue ? relationIssueLineColor : "transparent", borderLeftColor: hasValue ? relationIssueLineColor : "transparent", borderRightColor: hasValue ? relationIssueLineColor : "transparent", borderTopColor: d.itemStyle?.borderTopWidth ? relationIssueLineColor : "transparent", borderBottomColor: "transparent", borderWidth: 2, borderTopWidth: d.itemStyle?.borderTopWidth || 0, borderLeftWidth: 2, borderRightWidth: 2, borderBottomWidth: 0, borderType: "solid" } }; }) }; } return s; }); myChart.setOption({ series: newSeries }, false); } } }); } // 마감일 함수 function createDeadlineSeries(dates, totalRelationIssues, totalRequirements, deadline, usePreviousValue, lineWidth) { var chartStart = dates.reduce((earliest, date) => (date < earliest ? date : earliest), dates[0]); var chartEnd = dates.reduce((latest, date) => (date > latest ? date : latest), dates[0]); chartStart = new Date(chartStart); chartEnd = new Date(chartEnd); var deadlineSeries = []; if (new Date(deadline) <= chartEnd) { if (!dates.includes(deadline)) { dates.push(deadline); dates.sort((a, b) => new Date(a) - new Date(b)); let dateIndex = dates.indexOf(deadline); if (dateIndex > 0 && usePreviousValue) { totalRequirements.splice(dateIndex, 0, totalRequirements[dateIndex - 1]); totalRelationIssues.splice(dateIndex, 0, totalRelationIssues[dateIndex - 1]); } else { totalRequirements.splice(dateIndex, 0, 0); totalRelationIssues.splice(dateIndex, 0, 0); } } // 데이터 추가 var vs = { name: "마감일", type: "line", data: [ [deadline, 0], [deadline, 1] ], tooltip: { show: false }, markLine: { silent: true, symbol: "none", data: [ { xAxis: deadline } ], lineStyle: { color: "red", width: lineWidth, type: "dashed" }, label: { formatter: "마감일 : {c}", color: "white", fontSize: 14, fontWeight: "bold" } }, lineStyle: { color: "red", type: "dashed" }, symbol: "none" }; deadlineSeries.push(vs); } return deadlineSeries; } function dailyChartDataSearchEvent() { let scatter_start_date,scatter_end_date; let multi_stack_start_date,multi_stack_end_date; let timeline_start_date,timeline_end_date; function updateChart(chartType) { switch (chartType) { case "scatter": let scatterStartDate = $("#scatter_start_date").val(); let scatterEndDate = $("#scatter_end_date").val(); if(scatter_start_date===scatterStartDate&&scatter_end_date===scatterEndDate){ break; } scatter_start_date = scatterStartDate; scatter_end_date = scatterEndDate; if (scatter_start_date && scatter_end_date) { dailyUpdatedStatusScatterChart(selectedPdServiceId, selectedVersionId); } break; case "multi_stack": let multiStackStartDate = $("#multi_stack_start_date").val(); let multiStackEndDate = $("#multi_stack_end_date").val(); if(multi_stack_start_date===multiStackStartDate&&multi_stack_end_date===multiStackEndDate){ break; } multi_stack_start_date = multiStackStartDate; multi_stack_end_date = multiStackEndDate; if (multi_stack_start_date && multi_stack_end_date) { dailyCreatedCountAndUpdatedStatusesMultiStackCombinationChart(selectedPdServiceId, selectedVersionId); } break; case "timeline": let timelineStartDate = $("#timeline_start_date").val(); let timelineEndDate = $("#timeline_end_date").val(); if(timeline_start_date===timelineStartDate&&timeline_end_date===timelineEndDate){ break; } timeline_start_date = timelineStartDate; timeline_end_date = timelineEndDate; if (timeline_start_date && timeline_end_date) { timeLineChart(selectedPdServiceId, selectedVersionId); } break; } } // 날짜 선택 이벤트 $("#scatter_start_date, #scatter_end_date").on("change", function () { updateChart("scatter"); }); $("#multi_stack_start_date, #multi_stack_end_date").on("change", function () { updateChart("multi_stack"); }); $("#timeline_start_date, #timeline_end_date").on("change", function () { updateChart("timeline"); }); } function validateSearchDateWithChart(startDate, endDate) { let result = true; if (!selectedPdServiceId || !selectedVersionId) { jError("제품(서비스) 혹은 버전 선택이 되지 않았습니다."); result = false; } if (!startDate || !endDate) { jError("일자를 지정하지 않았습니다."); result = false; } return result; } //////////////////////////////////////////// // 버전Id -> 버전이름[, 버전이름...] 형태 변경 //////////////////////////////////////////// function convertVersionIdsToTitle(versionIds) { // 숫자인 경우 문자열로 변환 if (typeof versionIds === "number") { versionIds = String(versionIds); } if (typeof versionIds !== "string") return "Unknown Version"; const versionIdList = versionIds.split(",") .map(s => s.trim()).filter(Boolean); if (versionIdList.length === 0) {return "Unknown Version"; } if (versionIdList.length === 1) { const v = versionListData?.[versionIdList[0]]; // v 가 null/undefined 면 c_title 에 접근하지 않고 undefined 반환. return v?.c_title ?? "Unknown Version"; } let versionTitles = ""; versionIdList.forEach((versionId, index) => { const v = versionListData?.[versionId]; if (v?.c_title) { versionTitles += v.c_title; if (index < versionIdList.length - 1) { versionTitles += ", "; // 마지막이 아닐 때만 콤마 추가 } } }); return versionTitles || "Unknown Version"; } function verticalTimeLineChart(data) { let contentSet = {}; // 객체로 선언 data.reduce((acc, versionData) => { versionData.issues.forEach((item) => { if (!contentSet[item.summary]) { // 중복 체크 contentSet[item.summary] = { version: versionData.pdServiceVersion, summary: item.summary, projectName: [item.project.project_name], date: formatDateTime(item.updated) }; } else { // projectName에 item.project.project_name이 없는 경우에만 추가 if (!contentSet[item.summary].projectName.includes(item.project.project_name)) { contentSet[item.summary].projectName.push(item.project.project_name); contentSet[item.summary].projectName.sort(); } } }); return acc; }, []); let items = Object.values(contentSet).map((item) => ({ ...item, projectName: item.projectName })); // 날짜를 기준으로 오름차순 정렬 items.sort((a, b) => new Date(b.date) - new Date(a.date)); makeVerticalTimeline(items); } function makeVerticalTimeline(data) { // 데이터 세팅 const $container = $(".timeline-container"); $container.empty(); if (data.length == 0) { const noDataMessage = $("

").text("데이터가 없습니다.").css({ position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)" }); $container.append(noDataMessage); } else { // 날짜별로 데이터 그룹화 let groupedData = data.reduce((group, item) => { let date = item.date; if (!group[date]) group[date] = []; group[date].push(item); return group; }, {}); const $ul = $(""); Object.entries(groupedData).forEach(([date, items]) => { items.forEach(({ version, summary, projectName }, index) => { const $li = $("
  • ").addClass("session"); if (index === 0) { $li.append(` ${date} `); } const $sessionContent = $(`
    ${convertVersionIdsToTitle( version )}
    ${summary}
    `); const $projectNameDiv = $("
    ").addClass("project-names"); // projectName 배열의 각 요소를 추가 projectName.forEach((name) => { if (name !== undefined) { const $button = $("").addClass("project-name").text(name); $projectNameDiv.append($button); } }); $sessionContent.append($projectNameDiv); $li.append($sessionContent); $ul.append($li); }); }); $container.append($ul); } adjustHeight(); } function formatDateTime(dateTime) { var date = dateTime.split("T")[0]; return date; } async function timeLineChart(pdServiceLink, pdServiceVersionLinks) { let deadline = await waitForGlobalDeadline(); let startDate = $("#timeline_start_date").val(); let endDate = $("#timeline_end_date").val(); if (!validateSearchDateWithChart(startDate, endDate)) { return; } $(".spinner").html( '로딩 ' + "수직 타임라인 차트를 로딩 중입니다..." ); $.ajax({ url: "/auth-admin/api/arms/analysis/time/updated-timeline", type: "POST", data: JSON.stringify({ pdServiceAndIsReq: { pdServiceLink: pdServiceLink, pdServiceVersionLinks: pdServiceVersionLinks.split(",").map(Number) }, startDate: startDate, endDate: endDate }), contentType: "application/json;charset=UTF-8", dataType: "json", progress: true, statusCode: { 200: function (data) { console.log("[ analysisTime :: TimeLineData ] :: = "); console.log(data); verticalTimeLineChart(data); } } }); var traffic; function executeAjaxCall() { $(".spinner").html( '로딩 ' + "요구사항 업데이트 현황(능선차트)를 로딩 중입니다..." ); $.ajax({ url: "/auth-admin/api/arms/analysis/time/updated-ridgeline", type: "POST", contentType: "application/json;charset=UTF-8", dataType: "json", data: JSON.stringify({ pdServiceAndIsReq: { pdServiceLink: pdServiceLink, pdServiceVersionLinks: pdServiceVersionLinks.split(",").map(Number) }, startDate: startDate, endDate: endDate }), progress: true, statusCode: { 200: function (data) { console.log("[ analysisTime :: ridgeLineData ] :: = ", data.response); traffic = data.response; updateRidgeLine(data.response); } } }); } executeAjaxCall(); window.addEventListener("resize", function () { adjustHeight(); updateRidgeLine(traffic); }); } function getColorByVersion(version) { var colorPalette = [ //e chart 컬러 팔레트 "#61a0a8", "#d48265", "#91c7ae", "#749f83", "#ca8622", "#bda29a", "#6e7074", "#546570", "#c4ccd3", "#c23531", "#2f4554", ]; var versionNumber = parseInt(version); if (isNaN(versionNumber)) { return "#c4ccd3"; // version이 숫자로 변환 불가능할 경우 지정된 색상 반환 } return colorPalette[versionNumber % colorPalette.length]; } function updateRidgeLine(traffic) { // 데이터가 없을 경우 if (!traffic || traffic.length === 0) { document.getElementById("overlapInputDiv").style.display = "none"; document.getElementById("updateRidgeLine").innerHTML = "

    " + "데이터가 없습니다.

    "; return; } else { document.getElementById("overlapInputDiv").style.display = "flex"; } function setOverlapInputListener() { var overlap = this.value; overlapNumberInput.value = overlap; drawGraph(traffic, overlap); } function setOverlapNumberInputListener() { var overlap = this.value; overlapInput.value = overlap; drawGraph(traffic, overlap); } overlapInput.removeEventListener("input", setOverlapInputListener); overlapNumberInput.removeEventListener("input", setOverlapNumberInputListener); overlapInput.addEventListener("input", setOverlapInputListener); overlapNumberInput.addEventListener("input", setOverlapNumberInputListener); var initialOverlap = traffic.length > 30 ? 5 : 2; document.getElementById("overlapInput").value = initialOverlap; document.getElementById("overlapNumberInput").value = initialOverlap; drawGraph(traffic, initialOverlap); } function drawGraph(traffic, overlap) { document.getElementById("updateRidgeLine").innerHTML = ""; var nestedDataByDate = d3 .nest() .key(function (d) { return +new Date(d.updateDate); }) .entries(traffic); var dates = nestedDataByDate .map(function (d) { return +d.key; }) .sort(d3.ascending); var nestedDataByName = d3 .nest() .key(function (d) { return d.summary; }) .entries(traffic); var series = nestedDataByName.map(function (d) { var valuesMap = d3.map(d.values, function (e) { return String(+new Date(e.updateDate)); }); var values = dates.map(function (updateDate) { var valueObj = valuesMap.get(String(updateDate)); return valueObj ? valueObj.updatedCount : null; }); var version = d.values[0] ? d.values[0].version : null; // version 필드 추가 var summary = d.values[0] ? d.values[0].summary : null; // summary 필드 추가 var key = d.values[0] ? d.values[0].key : null; // key 필드 추가 return { name: key + ": " + summary, values: values, version: version, key: key }; // version 값 포함하여 반환 }); //const overlap = 4; //const height = series.length * 30; //const height = Math.max(minHeight, Math.min(maxHeight, series.length * 16)); const width = Math.max(parseInt($("#updateRidgeLine").width()) - 15, 655); const marginTop = 50; const marginRight = 0; const marginBottom = 20; //const marginLeft = 280; const displayCount = series.length; const rowHeight = 30; const height = displayCount * rowHeight + marginTop + marginBottom; const marginLeft = calculateDynamicMarginLeft( getMarginSafeLabels( series.map((d) => d.name), 42 ), 12, 20 ); // Create the scales. const x = d3 .scaleTime() .domain(d3.extent(dates)) .range([marginLeft, width - marginRight]); const y = d3 .scalePoint() .domain(series.map((d) => d.name)) .range([marginTop, height - marginBottom]); const z = d3 .scaleLinear() .domain([0, d3.max(series, (d) => d3.max(d.values))]) .nice() .range([0, -overlap * y.step()]); // Create the area generator and its top-line generator. const area = d3 .area() .curve(d3.curveBasis) .defined((d) => !isNaN(d)) .x((d, i) => x(dates[i])) .y0(0) .y1((d) => z(d)); const line = area.lineY1(); // Create the SVG container. const svg = d3.create("svg").attr("width", width).attr("height", height); // Append the axes. svg .append("g") .attr("transform", `translate(0,${height - marginBottom})`) .call( d3 .axisBottom(x) .ticks(width / 80) .tickSizeOuter(0) ); svg .append("g") .attr("transform", `translate(${marginLeft},0)`) .call(d3.axisLeft(y).tickSize(0).tickPadding(4)) .call((g) => g.select(".domain").remove()) .selectAll(".tick text") .text(function (d) { return d.length > 42 ? d.slice(0, 35) + " . . ." : d; // 긴 레이블은 축약 }) .style("font-size", "13px"); // Append a layer for each series. const group = svg .append("g") .selectAll("g") .data(series) .join("g") .attr("transform", (d) => `translate(0,${y(d.name) + 1})`); var div = d3 .select("body") .append("div") .attr("class", "tooltip") .style("opacity", 0) .style("display", "none") .style("pointer-events", "none"); group .append("path") .attr("fill", (d) => getColorByVersion(d.version)) .attr("d", (d) => area(d.values)) .on("mouseover", function (d) { var event = d3.event; d3.select(this).transition().duration(20).style("opacity", 0.4); div.transition().duration(20).style("display", "block").style("opacity", 0.9); div .html( "버전 정보: " + convertVersionIdsToTitle(d.version) + "
    요구사항 키: " + d.key + "
    요구사항 제목: " + d.name ) .style("left", event.pageX + "px") .style("top", event.pageY - 28 + "px"); }) .on("mouseout", function () { d3.select(this).transition().duration(20).style("opacity", 1); div.transition().duration(20).style("opacity", 0).style("display", "none"); }); group .append("path") .attr("fill", "none") .attr("stroke", "#EBEDF0") //.attr("stroke", d => getColorByVersion(d.version)) .attr("stroke-width", 0.5) .attr("d", (d) => line(d.values)); $("#overlapInputDiv").css("display", "flex"); $("#updateRidgeLine").append(svg.node()); adjustHeight(); } function getMarginSafeLabels(labels, maxChars = 42) { return labels.map((label) => (label.length > maxChars ? label.slice(0, 35) : label)); } function calculateDynamicMarginLeft(labels, fontSize = 12, buffer = 10) { const tempSvg = d3.select("body").append("svg").attr("visibility", "hidden").style("position", "absolute"); const dummy = tempSvg .selectAll("text") .data(labels) .enter() .append("text") .text((d) => d) .style("font-size", `${fontSize}px`) .style("font-family", "sans-serif"); const maxTextWidth = Math.max(...dummy.nodes().map((n) => n.getBBox().width)); tempSvg.remove(); return Math.ceil(maxTextWidth + buffer); } // 차트 높이 조정 function adjustHeight() { var verticalTimeline = $("#vertical-timeline"); var updateRidgeLine = $("#updateRidgeLine"); if (verticalTimeline && updateRidgeLine) { verticalTimeline.height(updateRidgeLine.height() + 20); } }