var selectedPdServiceId; // 제품(서비스) 아이디 var selectedVersionId; // 선택된 버전 아이디 // 최상단 메뉴 변수 var req_state, resource_info, issue_info, period_info, total_days_progress; var pdServiceData; var pdServiceListData; var versionListData; var versionMap; var stateInfoMap var selectedPdServiceName; const modalStatusIcons = { 열림: { icon: '' }, 진행중: { icon: '' }, 해결됨: { icon: '' }, 닫힘: { icon: '' }, 기타: { icon: '' } }; const modalPriorityIcons = { "매우 높음": { icon: '' }, 높음: { icon: '' }, 중간: { icon: '' }, 낮음: { icon: '' }, "매우 낮음": { icon: '' } }; const modalDifficultyIcons = { "매우 어려움": { icon: '' }, 어려움: { icon: '' }, 보통: { icon: '' }, 쉬움: { icon: '' }, "매우 쉬움": { icon: '' } }; //////////////////////////////////////////////////////////////////////////////////////// //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/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/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", "../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" ], [ // Apache Echarts "../reference/jquery-plugins/echarts-5.4.3/dist/echarts.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", // 버전 별 요구사항 현황 (RadialPolarBarChart) "./js/common/chart/eCharts/RadialPolarBarChart.js", //CirclePacking with d3 Chart "./js/analysis/scope/circularPackingChart.js", //chart Colors "./js/common/colorPalette.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_scope"); //제품(서비스) 셀렉트 박스 이니시에이터 makePdServiceSelectBox(); //버전 멀티 셀렉트 박스 이니시에이터 makeVersionMultiSelectBox(); // 높이 조정 $(".top-menu-div").matchHeight({ target: $(".top-menu-div-scope") }); }) .catch(function () { console.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) { ////////////////////////////////////////////////////////// pdServiceListData = []; for (var k in data.response) { var obj = data.response[k]; pdServiceListData.push({ pdServiceId: obj.c_id, pdServiceName: obj.c_title }); var newOption = new Option(obj.c_title, obj.c_id, false, false); $("#selected_pdService").append(newOption).trigger("change"); } ////////////////////////////////////////////////////////// console.log("[analysisScope :: makePdServiceSelectBox] :: pdServiceListData => "); console.table(pdServiceListData); // 상태정보 조회 getStateInfo(); } } }); $("#selected_pdService").on("select2:open", function () { //슬림스크롤 makeSlimScroll(".select2-results__options"); }); // --- select2 ( 제품(서비스) 검색 및 선택 ) 이벤트 --- // $("#selected_pdService").on("select2:select", function (e) { selectedPdServiceId = $("#selected_pdService").val(); selectedPdServiceName = pdServiceListData.find((s) => s.pdServiceId == selectedPdServiceId).pdServiceName; //refreshDetailChart(); 변수값_초기화(); // 제품( 서비스 ) 선택했으니까 자동으로 버전을 선택할 수 있게 유도 // 디폴트는 base version 을 선택하게 하고 ( select all ) //~> 이벤트 연계 함수 :: Version 표시 jsTree 빌드 bind_VersionData_By_PdService(); }); } // end makePdServiceSelectBox() //////////////////////////////////////// //버전 멀티 셀렉트 박스 //////////////////////////////////////// function makeVersionMultiSelectBox() { //버전 선택시 셀렉트 박스 이니시에이터 $(".multiple-select").multipleSelect({ filter: true, onClose: function () { console.log("onOpen event fire!\n"); var checked = $("#checkbox1").is(":checked"); var endPointUrl = ""; var versionTag = $(".multiple-select").val(); console.log("[ analysisScope :: makeVersionMultiSelectBox ] :: versionTag"); console.log(versionTag); selectedVersionId = versionTag.join(","); if (versionTag === null || versionTag == "") { jError("버전이 선택되지 않았습니다."); $(".ms-parent").css("z-index", 1000); return; } // 최상단 메뉴 세팅 TopMenuApi.톱메뉴_초기화(); TopMenuApi.톱메뉴_세팅(); // 나이팅게일로즈 차트(pie) - 버전별 요구사항 getReqPerMappedVersions(selectedPdServiceId, selectedVersionId); // 네트워크 차트 getReqNetworkData($("#selected_pdService").val(), selectedVersionId); // 트리바 차트 treeBar(selectedPdServiceId, selectedVersionId, 10); // Circular Packing with D3 차트 getReqStatusAndInvolvedAssignees(selectedPdServiceId, selectedVersionId); //요구사항 현황 데이터 테이블 로드 var requestUrl = "/T_ARMS_REQADD_" + selectedPdServiceId + "/req-property-list?" + "pdServiceId=" + selectedPdServiceId + "&pdServiceVersionLinks=" + selectedVersionId; reqPropertyTable(selectedPdServiceId, requestUrl); $(".ms-parent").css("z-index", 1000); }, onOpen: function () { console.log("open event"); $(".ms-parent").css("z-index", 9999); } }); } function bind_VersionData_By_PdService() { $(".multiple-select option").remove(); $.ajax({ url: "/auth-user/api/arms/pdService/getVersionList?c_id=" + $("#selected_pdService").val(), type: "GET", dataType: "json", progress: true, statusCode: { 200: function (data) { ////////////////////////////////////////////////////////// var pdServiceVersionIds = []; versionListData = []; for (var k in data.response) { var obj = data.response[k]; pdServiceVersionIds.push(obj.c_id); versionListData.push(obj); var newOption = new Option(obj.c_title, obj.c_id, true, false); $(".multiple-select").append(newOption); } versionMap = versionListData.reduce((map, item) => { map[item.c_id] = item.c_title; return map; }, {}); selectedVersionId = pdServiceVersionIds.join(","); console.log("[ analysisScope :: bind_VersionData_By_PdService ] :: pdServiceVersionIds => ", pdServiceVersionIds); console.log("bind_VersionData_By_PdService :: selectedVersionId => ", selectedVersionId); if (data.length > 0) { console.log("display 재설정."); } $(".multiple-select").multipleSelect("refresh"); // 최상단 메뉴 세팅 TopMenuApi.톱메뉴_초기화(); TopMenuApi.톱메뉴_세팅(); // 나이팅게일로즈 차트(pie) - 버전별 요구사항 getReqPerMappedVersions(selectedPdServiceId, selectedVersionId); // Circular Packing with D3 차트 getReqStatusAndInvolvedAssignees(selectedPdServiceId, selectedVersionId); // 네트워크 차트 getReqNetworkData($("#selected_pdService").val(), selectedVersionId); // 트리바 treeBar(selectedPdServiceId, selectedVersionId, 10); //요구사항 현황 데이터 테이블 로드 var requestUrl = "/T_ARMS_REQADD_" + selectedPdServiceId + "/req-property-list?" + "pdServiceId=" + selectedPdServiceId + "&pdServiceVersionLinks=" + selectedVersionId; reqPropertyTable(selectedPdServiceId, requestUrl); ////////////////////////////////////////////////////////// } } }); } ///////////////////////////////////////////////////////// // NetWork Chart - 요구사항 및 연결된 이슈 분포 // API 호출: getReqNetworkData() // 차트 Render: renderNetworkChart() ///////////////////////////////////////////////////////// function getReqNetworkData(pdServiceLink, pdServiceVersionLinks) { let requestUrl = `/auth-admin/api/arms/analysis/scope/T_ARMS_REQADD_${pdServiceLink}/issue-network`; $.ajax({ url: requestUrl, type: "POST", data: JSON.stringify({ pdServiceAndIsReq: { pdServiceLink: pdServiceLink, pdServiceVersionLinks: pdServiceVersionLinks.split(",").map(Number) } }), contentType: "application/json;charset=UTF-8", dataType: "json", progress: true, statusCode: { 200: function (data) { console.log("[ analysisScope :: getRelationJ :: iraIssueByPdServiceAndVersions ] 네트워크차트 그리기"); renderNetworkChart(data); } } }); } function renderNetworkChart(network_chart_data) { d3.select("#NETWORK_GRAPH").remove(); d3.select("#network-zoom-toggle-btn").remove(); if (network_chart_data.nodes.length === 0) { // 데이터가 없는 경우를 체크 d3.select("#NETWORK_GRAPH").remove(); d3.select(".network-graph").append("p").text("데이터가 없습니다."); return; } // 줌 활성화 상태 변수 let isZoomEnabled = false; d3.select(".network-graph").append("svg").attr("id", "NETWORK_GRAPH"); /******** network graph config ********/ var networkGraph = { createGraph: function () { let links = network_chart_data.links.map(function (d) { return Object.create(d); }); let nodes = network_chart_data.nodes.map(function (d) { return Object.create(d); }); // 확인 var fillCircle = function (g) { if (g.type === "pdService") { return "#c67cff"; } else if (g.type === "version") { return "rgb(255,127,14)"; } else if (g.isReq === true) { return "rgb(214,39,40)"; } else if (g.isReq === false) { return "rgb(31,119,180)"; } else { return "rgb(44,160,44)"; } }; // 확인 var typeBinding = function (g) { let name = ""; if (g.type === "pdService") { name = "제품(서비스)"; } else if (g.type === "version") { name = "버전"; } else if (g.type === "requirement") { name = "요구사항 제목"; } else if (g.isReq === true) { name = "요구사항 키"; } else if (g.isReq === false) { name = "연결된 이슈"; } return "[" + name + "]"; }; var nameBinding = function (g) { let name = ""; if (g.type === "pdService") { return selectedPdServiceName; } else if (g.type === "version") { return versionMap[g.key]; } else { return g.key; } }; let baseWidth = parseInt($(".network-graph").width() / 1.5); let baseHeight = parseInt($(".network-graph").height() / 1.5); let nodeCount = nodes.length; let increaseFactor = Math.floor(nodeCount / 100); if (nodeCount % 100 !== 0) { increaseFactor++; } baseWidth += increaseFactor * 100; baseHeight += increaseFactor * 100; let width = baseWidth; let height = baseHeight; let simulation = d3 .forceSimulation(nodes) .force( "link", d3 .forceLink(links) .id(function (d) { return d.id; }) .distance(30) ) .force("charge", d3.forceManyBody().strength(-1000)) .force("center", d3.forceCenter(width / 2, height / 2)) .force("collide", d3.forceCollide().radius(30)) .force( "radial", d3 .forceRadial() .radius(10) .x(width / 2) .y(height / 2) .strength(0.5) ); let svg = d3.select("#NETWORK_GRAPH").attr("viewBox", [0, 0, width, height]); let gHolder = svg.append("g").attr("class", "g-holder"); let link = gHolder .append("g") .attr("fill", "none") .attr("stroke", "gray") .attr("stroke-opacity", 1) .selectAll("path") .data(links) .join("path") .attr("stroke-width", 1.5); let node = gHolder .append("g") .attr("class", "circle-node-holder") .attr("stroke", "white") .attr("stroke-width", 1) .selectAll("g") .data(nodes) .enter() .append("g") .each(function (d) { d3.select(this).append("circle").attr("r", 6).attr("fill", fillCircle(d)); }) .call(networkGraph.drag(simulation)); node .append("text") .attr("x", 11) .attr("dy", ".31em") .style("font-family", "Nanum Gothic") .style("font-size", "9px") .style("fill", (d) => fillCircle(d)) .style("font-weight", "5") .attr("stroke", (d) => fillCircle(d)) .attr("stroke-width", "0.3") .text((d) => typeBinding(d)); node .append("text") .attr("x", 12) .attr("dy", "1.5em") .style("font-family", "Nanum Gothic") .style("font-size", "9px") //.style("font-weight", "5") .attr("stroke-width", "0") .each(function (d) { let nodeText = d3.select(this); let key = nameBinding(d); let match = key.match(/(.+)\((\d+)\)$/); if (match) { nodeText.append("tspan").text(match[1]).attr("stroke", "white").attr("fill", "white"); nodeText.append("tspan").text(`(${match[2]})`).attr("stroke", "#38BDF8").attr("fill", "#38BDF8"); } else { nodeText.text(key); } }); simulation.on("tick", function () { link .attr("x1", function (d) { return d.source.x; }) .attr("y1", function (d) { return d.source.y; }) .attr("x2", function (d) { return d.target.x; }) .attr("y2", function (d) { return d.target.y; }); //circle 노드에서 g 노드로 변경 node.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; }); link.attr("d", function (d) { var dx = d.target.x - d.source.x, dy = d.target.y - d.source.y, dr = Math.sqrt(dx * dx + dy * dy); return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y; }); }); let zoom = d3 .zoom() .scaleExtent([0.1, 5]) .filter(function() { // 버튼이 활성화된 경우에만 휠 이벤트로 줌 허용 // 드래그는 항상 허용 (마우스 버튼 클릭) if (d3.event.type === 'wheel') { return isZoomEnabled; } return true; }) .on("zoom", zoomed); // SVG에 확대/축소 기능 적용 svg.call(zoom); let legendGroup = d3.select(".network-graph").append("div").attr("class", "legend-group"); let legendData = [ { color: "#9467bd", label: "제품(서비스)" }, { color: "#FF7F0E", label: "버전" }, { color: "#2CA02C", label: "요구사항 제목" }, { color: "#d62728", label: "요구사항 키" }, { color: "#38BDF8", label: "연결된 이슈 개수" } ]; legendData.forEach((d, i) => { let legendItem = legendGroup .append("div") .style("display", "flex") .style("align-items", "center") .style("margin-bottom", "5px"); legendItem .append("div") .style("width", "12px") .style("height", "12px") .style("border-radius", "50%") .style("background-color", d.color) .style("margin-right", "8px"); legendItem.append("span").text(d.label).style("font-size", "12px"); }); // 줌 토글 버튼 추가 (범례 그룹 내부에 추가) legendGroup .append("button") .attr("id", "network-zoom-toggle-btn") .attr("class", "btn btn-sm btn-default") .style("margin-top", "10px") .html(' 줌 기능: OFF') .on("click", function() { isZoomEnabled = !isZoomEnabled; if (isZoomEnabled) { d3.select(this) .attr("class", "btn btn-sm btn-primary") .html(' 줌 기능: ON'); } else { d3.select(this) .attr("class", "btn btn-sm btn-default") .html(' 줌 기능: OFF'); } }); function zoomed() { // 현재 확대/축소 변환을 얻음 let transform = d3.event.transform; // 모든 노드 및 링크를 현재 확대/축소 변환으로 이동/확대/축소 gHolder.attr("transform", transform); } return svg.node(); }, drag: function (simulation) { function dragstarted(d) { if (!d3.event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(d) { d.fx = d3.event.x; d.fy = d3.event.y; } function dragended(d) { if (!d3.event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended); } }; /******** network graph create ********/ networkGraph.createGraph(); } function formatDate(date) { var year = date.getFullYear().toString(); // 연도의 마지막 두 자리를 얻습니다. var month = (date.getMonth() + 1).toString().padStart(2, "0"); var day = date.getDate().toString().padStart(2, "0"); return year + "-" + month + "-" + day; } ///////////////////////////////////////////////////////// // Circular Packing Chart- 버전별 진행중 요구사항 상태 및 관여한 작업자수 현황 // API 호출: getReqStatusAndInvolvedAssignees() // 차트 랜더 : drawCircularPacking() ///////////////////////////////////////////////////////// function getReqStatusAndInvolvedAssignees(pdServiceLink, pdServiceVersionLinks) { $.ajax({ url: "/auth-admin/api/arms/analysis/scope/getCircularPackingChartData/T_ARMS_REQADD_" + pdServiceLink +"/getReqAddListByFilter", type: "POST", contentType: "application/json;charset=UTF-8", dataType: "json", data: JSON.stringify({ pdServiceAndIsReq: { isReq: true, pdServiceLink: pdServiceLink, pdServiceVersionLinks: pdServiceVersionLinks.split(",").map(Number) } }), progress: true, statusCode: { 200: function (result) { console.log("[ analysisScope :: getReqStatusAndInvolvedAssignees ] :: result"); console.log(result); let pdServiceName; pdServiceListData.forEach((elements) => { if (elements["pdServiceId"] === +pdServiceLink) { pdServiceName = elements["pdServiceName"]; } }); const responseResult = Array.isArray(result?.response) ? result.response : []; if (responseResult.length === 0) { return; } const dataObject = buildCircularPackingData4Levels_noNormalize( responseResult, pdServiceName, { minBubble: 1, orphanGroupTitle: '연결되지 않은 작업' } ); drawCircularPacking("circularPacking", pdServiceName, dataObject); } } }); } function buildCircularPackingData4Levels_noNormalize(responseArray, psServiceName, { minBubble = 1, orphanGroupTitle = '연결되지 않은 작업' } = {}) { const root = { $count: 0, $status: psServiceName }; const armsByCreq = new Map(); const almsByCreq = new Map(); for (const rec of responseArray) { const key = rec.creqLink; if (rec.type === 'ARMS') { armsByCreq.set(key, rec); } else if (rec.type === 'ALM') { const list = almsByCreq.get(key) || []; list.push(rec); almsByCreq.set(key, list); } } const getVersionLabels = (rec) => { const ids = Array.isArray(rec?.versions) ? rec.versions : []; const labels = ids.map(v => versionMap[v] || String(v)); return labels.length ? [...new Set(labels)] : ['Unversioned']; }; const armsStatusLabel = (stateLink) => { const code = String(stateLink ?? '').trim(); return stateInfoMap[code] || stateInfoMap[+code] || code || '기타'; }; for (const [creq, arms] of armsByCreq.entries()) { const armsVersions = getVersionLabels(arms); const armsStatus = armsStatusLabel(arms.stateLink); const armsTitle = arms.title || `REQ ${creq}`; const childrenAlms = almsByCreq.get(creq) || []; for (const ver of armsVersions) { if (!root[ver]) root[ver] = { $count: 0, $status: ver }; const armsKey = `ARMS:${creq}`; if (!root[ver][armsKey]) { root[ver][armsKey] = { $count: 0, $status: armsStatus, $title: armsTitle }; } for (const alm of childrenAlms) { const linked = +alm.linkedIssueCount || 0; const sub = +alm.subTaskCount || 0; const size = Math.max(linked + sub, minBubble); const almStatus = (alm.almState ?? '기타'); const almKey = `${alm.issueKey}`; root[ver][armsKey][almKey] = { $count: size, $status: almStatus, $linked: linked, // (툴팁용) $sub: sub, // (툴팁용) $issueKey: alm.issueKey, $workers : alm.workersList }; } } almsByCreq.delete(creq); } for (const [, almList] of almsByCreq.entries()) { for (const alm of almList) { const vers = getVersionLabels(alm); const linked = +alm.linkedIssueCount || 0; const sub = +alm.subTaskCount || 0; const size = Math.max(linked + sub, minBubble); const almStatus = (alm.almState ?? '기타'); for (const ver of vers) { if (!root[ver]) root[ver] = { $count: 0, $status: ver }; const dummyArmsKey = `ARMS_ORPHAN:${orphanGroupTitle}`; if (!root[ver][dummyArmsKey]) { root[ver][dummyArmsKey] = { $count: 0, $status: '기타', $title: orphanGroupTitle }; } const almKey = `ALM:${alm.issueKey || alm.recentId || `ALM_${Math.random().toString(36).slice(2)}`}`; root[ver][dummyArmsKey][almKey] = { $count: size, $status: almStatus, $linked: linked, $sub: sub, $issueKey: alm.issueKey }; } } } return root; } function getStateInfo() { $.ajax({ url: "/auth-user/api/arms/reqState/getNodesWithoutRoot.do", type: "GET", dataType: "json", progress: true, statusCode: { 200: function (data) { stateInfo = data.result; stateInfoMap = Object.fromEntries( stateInfo.map(item => [item.c_id, item.c_title || item.data]) ); } } }); } ///////////////////////////////////////////////////////// // Radial Polar Bar Chart - 제품(서비스)의 버전별 요구사항 수 // API 호출: getReqPerMappedVersions() // 차트 랜더 : drawRadialPolarBarChart() ///////////////////////////////////////////////////////// function getReqPerMappedVersions(pdServiceLink, pdServiceVersionLinks) { let reqAddUrl = "/T_ARMS_REQADD_" + pdServiceLink + "/getReqAddListByFilter?"; $.ajax({ url: "/auth-admin/api/arms/analysis/scope/req-per-version" + reqAddUrl, 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 (result) { console.log("[ analysisScope :: getReqPerMappedVersions ] :: result"); console.log(result); let colorArr = ColorPalette.eCharts.radialPolarBarChart; drawRadialPolarBarChart("reqPerVersionRoseChart", result["response"], colorArr); } } }); } function chord(data) { const $container = document.getElementById("circular_sankey"); const $chart = makeChart(data); $container.append($chart); } function makeChart(data) { const width = 640; const height = width; const outerRadius = Math.min(width, height) * 0.5 - 30; const innerRadius = outerRadius - 20; const { names, colors } = data; const sum = d3.sum(data.flat()); const tickStep = d3.tickStep(0, sum, 100); const tickStepMajor = d3.tickStep(0, sum, 20); const formatValue = d3.formatPrefix(",.0", tickStep); const chord = d3 .chord() .padAngle(20 / innerRadius) .sortSubgroups(d3.descending); const arc = d3.arc().innerRadius(innerRadius).outerRadius(outerRadius); const ribbon = d3.ribbon().radius(innerRadius); const svg = d3 .create("svg") .attr("width", width) .attr("height", height) .attr("viewBox", [-width / 2, -height / 2, width, height]) .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;"); const chords = chord(data); const group = svg.append("g").selectAll().data(chords.groups).join("g"); group .append("path") .attr("fill", (d) => colors[d.index]) .attr("d", arc) .append("title") .text((d) => `${d.value.toLocaleString("en-US")} ${names[d.index]}`); const groupTick = group .append("g") .selectAll() .data((d) => groupTicks(d, tickStep)) .join("g") .attr("transform", (d) => `rotate(${(d.angle * 180) / Math.PI - 90}) translate(${outerRadius},0)`); groupTick.append("line").attr("stroke", "currentColor").attr("x2", 6); groupTick .filter((d) => d.value % tickStepMajor === 0) .append("text") .attr("x", 8) .attr("dy", ".35em") .attr("transform", (d) => (d.angle > Math.PI ? "rotate(180) translate(-16)" : null)) .attr("text-anchor", (d) => (d.angle > Math.PI ? "end" : null)) .text((d) => formatValue(d.value)); svg .append("g") .attr("fill-opacity", 0.7) .selectAll() .data(chords) .join("path") .attr("d", ribbon) .attr("fill", (d) => colors[d.target.index]) .attr("opacity", 0.6) .on("mouseenter", function (d) { d3.select(this).transition().attr("opacity", 1); }) .on("mouseout", function () { d3.select(this).transition().attr("opacity", 0.6); }) .attr("stroke", "white") .append("title") .text( (d) => `${d.source.value.toLocaleString("en-US")} ${names[d.source.index]} → ${names[d.target.index]}${ d.source.index !== d.target.index ? `\n${d.target.value.toLocaleString("en-US")} ${names[d.target.index]} → ${names[d.source.index]}` : `` }` ); return svg.node(); } function groupTicks(d, step) { const k = (d.endAngle - d.startAngle) / d.value; return d3.range(0, d.value, step).map((value) => { return { value: value, angle: value * k + d.startAngle }; }); } ///////////////////////////////////////////////////////// // treeBar Chart - 가장 많은 인력이 투입 된 요구사항 (Top 10) // API 호출: treeBar() // 차트 랜더 : renderTreeBar() ///////////////////////////////////////////////////////// function treeBar(pdServiceId, pdServiceVersions, topN) { $.ajax({ url: "/auth-admin/api/arms/analysis/scope/tree-bar-data", type: "POST", data: JSON.stringify({ pdServiceAndIsReq: { pdServiceLink: pdServiceId, pdServiceVersionLinks: pdServiceVersions.split(",").map(Number), isReq: false }, topN: topN}), contentType: "application/json;charset=UTF-8", dataType: "json", progress: true }) .done(function (apiResponse) { const data = apiResponse.response; console.log("[ analysisScope :: treeBar ] data.legnth => ", data.length); d3.select("#tree_bar_container svg").selectAll("*").remove(); let assigneeData = data.filter((item) => item.type === "assignee"); if (assigneeData.length === 0) { return; } if (assigneeData.length === 1 && assigneeData[0].name === "No Data") { return; } let maxValue = Math.max(...assigneeData.map((item) => item.value)); setupResponsiveTreeBar(data, assigneeData, maxValue); }) .fail(function (e) {}) .always(function () {}); } function renderTreeBar(data, assigneeData, maxValue) { d3.select("#tree_bar_container svg").selectAll("*").remove(); const charts = document.getElementById("tree_bar_container"); const $container = document.getElementById("tree_bar_container"), width = $container.offsetWidth, nodeHeight = 40, totalNodes = data.length, dynamicHeight = Math.max(200, totalNodes * nodeHeight); const svg = d3.select("#tree_bar_container svg"); const g = svg .append("g") .attr("transform", "translate(10,10)") .attr("width", width) .call(responsiveTreeBar(dynamicHeight)), experienceName = Array(maxValue) .fill("") .map((_, i) => (i + 1 === maxValue ? maxValue.toString() : "")), formatSkillPoints = function (d) { return experienceName[d % maxValue]; }, xScale = d3.scaleLinear().domain([0, maxValue]).range([0, 100]), xAxis = d3.axisTop().scale(xScale).ticks(maxValue).tickFormat(formatSkillPoints), tree = d3.cluster().size([dynamicHeight, width - 145]), stratify = d3 .stratify() .id((d) => d.id) .parentId((d) => d.parent), root = stratify(data); d3.select("#treeChart").attr("height", dynamicHeight).attr("width", width); tree(root); const link = g .selectAll(".link") .data(root.descendants().slice(1)) .enter() .append("path") .attr("class", "link") .attr("d", function (d) { return `M${d.y},${d.x}C${d.parent.y + 100},${d.x} ${d.parent.y + 100},${d.parent.x} ${d.parent.y},${d.parent.x}`; }); const node = g .selectAll(".node") .data(root.descendants()) .enter() .append("g") .attr("class", function (d) { return `node${d.children ? " node--internal" : " node--leaf"}`; }) .attr("transform", function (d) { return `translate(${d.y}, ${d.x})`; }); node.append("circle").attr("r", 4); const leafNodeG = g .selectAll(".node--leaf") .append("g") .attr("class", "node--leaf-g") .attr("transform", "translate(" + 8 + "," + -13 + ")"); leafNodeG .append("rect") .style("fill", function (d) { return d.data.color; }) .attr("width", 2) .attr("height", 20) .attr("rx", 2) .attr("ry", 2) .transition() .duration(800) .attr("width", function (d) { return xScale(d.data.value); }); leafNodeG .append("text") .attr("dy", 14) .attr("x", 8) .style("text-anchor", "start") .style("font-weight", "normal") .text(function (d) { return d.data.name; }); const internalNode = g.selectAll(".node--internal"); internalNode .append("text") .attr("class", (d) => d.data.id === "1" && "root") .attr("y", -10) .style("text-anchor", "start") .style("font-weight", "normal") .text(function (d) { return d.data.name; }); const firstEndNode = g.select(".node--leaf"); firstEndNode .insert("g") .attr("class", "xAxis") .attr("transform", "translate(" + 7 + "," + -14 + ")") .call(xAxis); firstEndNode .insert("g") .attr("class", "grid") .attr("transform", "translate(7," + (dynamicHeight - 15) + ")") .call(d3.axisBottom().scale(xScale).ticks(5).tickSize(-dynamicHeight, 0, 0).tickFormat("")); svg.selectAll(".grid").select("line").style("stroke-dasharray", "1,1").style("stroke", "white"); } function responsiveTreeBar(height) { return function (svg) { const container = d3.select(svg.node().parentNode); let width = container.node().getBoundingClientRect().width; svg.attr("viewBox", `0 0 ${width} ${height}`).attr("preserveAspectRatio", "xMinYMid").call(resize); d3.select(window).on("resize." + container.attr("id"), resize); function resize() { const w = parseInt(container.style("width")); svg.attr("width", w); svg.attr("height", height); // height 고정 } }; } function setupResponsiveTreeBar(data, assigneeData, maxValue) { renderTreeBar(data, assigneeData, maxValue); window.removeEventListener("resize", handleResize); function handleResize() { renderTreeBar(data, assigneeData, maxValue); } window.addEventListener("resize", handleResize); } //////////////////////////////////////////////////////////////////////////////////////// //요구사항 - 항목(우선순위,난이도,상태) 데이터 테이블 //////////////////////////////////////////////////////////////////////////////////////// function reqPropertyTable(selectId, endPointUrl) { var columnList = [ { name: "pdServiceId", title: "제품(서비스) 아이디", data: "pdServiceId", visible: false, defaultContent: "N/A" }, { name: "reqId", title: "요구사항 아이디", data: "reqId", visible: false, defaultContent: "N/A" }, { name: "versionNames", title: "버전", data: "versionNames", render: function (data, type, row, meta) { if (isEmpty(data) || data === "unknown") { return "