//////////////////////////////////////////////////////////////////////////////////////// //Document Ready //////////////////////////////////////////////////////////////////////////////////////// let selectedPdServiceId; // 제품(서비스) 아이디 let selectedPdServiceVersionIds; // 선택된 버전 아이디 let reportRTMTable; // DataTable 변수 // 상태 const statusColorMap = { "열림": { bg: "#db2a34", text: "#ffffff" }, "진행중": { bg: "#e49400", text: "#ffffff" }, "해결됨": { bg: "#2d8515", text: "#ffffff" }, "닫힘": { bg: "#2477ff", text: "#ffffff" }, "기타": { bg: "#8c8c8c", text: "#ffffff" } }; // 난이도 const difficultyColorMap = { "매우 어려움": { icon: "🔴", fa: "fa-exclamation-triangle", bg: "#db2a34", text: "#ffffff" }, "어려움": { icon: "🟠", fa: "fa-fire", bg: "#db2a34", text: "#ffffff" }, "보통": { icon: "🟡", fa: "fa-cogs", bg: "#e49400", text: "#ffffff" }, "쉬움": { icon: "🟢", fa: "fa-check-circle", bg: "#2477ff", text: "#ffffff" }, "매우 쉬움": { icon: "🔵", fa: "fa-leaf", bg: "#2477ff", text: "#ffffff" } }; // 중요도 const importanceColorMap = { "매우 중요": { icon: "🔴", fa: "fa-star", bg: "#db2a34", text: "#ffffff" }, "중요": { icon: "🟠", fa: "fa-star-half-o", bg: "#db2a34", text: "#ffffff" }, "보통": { icon: "🟡", fa: "fa-star-o", bg: "#e49400", text: "#ffffff" }, "낮음": { icon: "🟢", fa: "fa-flag-o", bg: "#2477ff", text: "#ffffff" }, "매우 낮음": { icon: "🔵", fa: "fa-ellipsis-h", bg: "#2477ff", text: "#ffffff" } }; // 긴급도 const urgencyColorMap = { "매우 긴급": { icon: "🔴", fa: "fa-rocket", bg: "#db2a34", text: "#ffffff" }, "긴급": { icon: "🟠", fa: "fa-bell-o", bg: "#db2a34", text: "#ffffff" }, "보통": { icon: "🟡", fa: "fa-hourglass-half", bg: "#e49400", text: "#ffffff" }, "검토": { icon: "🟢", fa: "fa-clock-o", bg: "#2477ff", text: "#ffffff" }, "보류": { icon: "🔵", fa: "fa-bed", bg: "#2477ff", text: "#ffffff" } }; function execDocReady() { var pluginGroups = [ [ "../reference/light-blue/lib/bootstrap-datepicker.js", "../reference/lightblue4/docs/lib/widgster/widgster.js", "../reference/lightblue4/docs/lib/slimScroll/jquery.slimscroll.min.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/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" ], [ // lightblue4 "../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", // table_new "./js/common/table_new.js" ] // 추가적인 플러그인 그룹들을 이곳에 추가하면 됩니다. ]; 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_report", "sidebar_menu_report_rtm"); // 스크립트 실행 로직을 이곳에 추가합니다. //1번 //제품(서비스) 셀렉트 박스 이니시에이터 makePdServiceSelectBox(); //버전 멀티 셀렉트 박스 이니시에이터 makeVersionMultiSelectBox(); // 데이터 테이블 초기화 (빈 데이터) dataTableLoad([]); // Export 버튼 이벤트 등록 $("#csvchecker").on("click", function () { reportRTMTable.button(".buttons-csv").trigger(); }); $("#excelchecker").on("click", function () { reportRTMTable.button(".buttons-excel").trigger(); }); }) .catch(function(e) { console.error('플러그인 로드 중 오류 발생'); console.error(e); }); } //////////////////////////////////////////////////////////////////////////////////////// //제품 서비스 셀렉트 박스 //////////////////////////////////////////////////////////////////////////////////////// function makePdServiceSelectBox() { //제품 서비스 셀렉트 박스 이니시에이터 $(".chzn-select").each(function() { $(this).select2($(this).data()); }); //제품 서비스 셀렉트 박스 데이터 바인딩 $.ajax({ url: "/auth-user/api/arms/pdServicePure/getPdServiceMonitor.do", type: "GET", contentType: "application/json;charset=UTF-8", dataType: "json", progress: true, statusCode: { 200: function(data) { ////////////////////////////////////////////////////////// for (var k in data.response) { var obj = data.response[k]; var newOption = new Option(obj.c_title, obj.c_id, false, false); $("#selected_pdService").append(newOption).trigger("change"); } ////////////////////////////////////////////////////////// jSuccess("제품(서비스) 조회가 완료 되었습니다."); } }, error: function (e) { jError("제품(서비스) 조회 중 에러가 발생했습니다."); } }); $("#selected_pdService").on("select2:open", function() { //슬림스크롤 makeSlimScroll(".select2-results__options"); }); // --- select2 ( 제품(서비스) 검색 및 선택 ) 이벤트 --- // $("#selected_pdService").on("select2:select", function(e) { selectedPdServiceId = $("#selected_pdService").val(); //refreshDetailChart(); 변수값_초기화(); // 제품( 서비스 ) 선택했으니까 자동으로 버전을 선택할 수 있게 유도 // 디폴트는 base version 을 선택하게 하고 ( select all ) //~> 이벤트 연계 함수 :: Version 표시 jsTree 빌드 bind_VersionData_By_PdService(); }); } // end makePdServiceSelectBox() //////////////////////////////////////// //버전 멀티 셀렉트 박스 //////////////////////////////////////// function makeVersionMultiSelectBox() { console.log("makeVersionMultiSelectBox"); //버전 선택시 셀렉트 박스 이니시에이터 $("#multiversion").multipleSelect({ filter: true, onClose: function () { let versions = []; let versionIds = []; $("#multiversion option:selected").map(function (a, item) { versions.push(item.innerText); versionIds.push(item.value); }); selectedPdServiceVersionIds = versionIds; if (selectedPdServiceVersionIds.length === 0) { // 데이터 없음으로 띄어주면 될듯? jError("버전을 선택해주세요."); return; } // 요구사항 추적 시스템 setReportRTM(); } }); } function bind_VersionData_By_PdService() { $("#multiversion option").remove(); $.ajax({ url: "/auth-user/api/arms/pdService/getVersionList?c_id=" + selectedPdServiceId, type: "GET", dataType: "json", progress: true, statusCode: { 200: function (data) { ////////////////////////////////////////////////////////// let versions = []; let versionIds = []; for (var k in data.response) { var obj = data.response[k]; versions.push(obj.c_title); versionIds.push(obj.c_id); var newOption = new Option(obj.c_title, obj.c_id, true, true); $("#multiversion").append(newOption); } if (data.length > 0) { console.log("[ reportRTM :: bind_VersionData_By_PdService ] :: result = display 재설정."); } selectedPdServiceVersionIds = versionIds.join(","); console.log("bind_VersionData_By_PdService :: selectedPdServiceId - " + selectedPdServiceId); console.log("bind_VersionData_By_PdService :: selectedPdServiceVersionIds - " + selectedPdServiceVersionIds); // 요구사항 추적 시스템 setReportRTM(); $("#multiversion").multipleSelect("refresh"); ////////////////////////////////////////////////////////// } }, error: function (e) { jError("버전 조회 중 에러가 발생했습니다."); } }); } //////////////////////////////////////////////////////////////////////////////////////// // 요구사항 추적 시스템 데이터 가져오기 //////////////////////////////////////////////////////////////////////////////////////// function setReportRTM() { $.getJSON('./mock/req_trace_matrix.json', function (response) { console.log("[ reportRTM :: setreportRTM ] :: data => "); console.table(response.data); dataTableLoad(response.data); }).fail(function(jqxhr, textStatus, error) { console.error("Error loading JSON file: " + textStatus + ", " + error); jError("데이터 로드 중 에러가 발생했습니다."); }); } //////////////////////////////////////////////////////////////////////////////////////// // 여러 사람 아이콘 렌더링 함수 (겹쳐서 표시, 3개 이후 ... 처리) //////////////////////////////////////////////////////////////////////////////////////// function renderMultiplePersonIcons(issueInfoList) { // 중복 제거 const uniqueOwners = [...new Set(issueInfoList.map(issue => issue.issueOwnerLastName))]; const totalCount = uniqueOwners.length; const displayCount = Math.min(3, totalCount); // 최대 3개까지만 표시 const hasMore = totalCount > 3; let iconsHtml = ''; // 표시할 아이콘들 생성 (겹치게) for (let i = 0; i < displayCount; i++) { let initial; let owner; let colors; if (isEmpty(uniqueOwners[i])) { owner = "N/A"; initial = "N/A"; colors = { "bg": "#797e93", "text": "#ffffff" }; } else { owner = uniqueOwners[i]; initial = owner ? owner.charAt(0) : '?'; colors = generateColorFromName(owner); } const zIndex = displayCount - i; // 앞쪽 아이콘이 위로 오도록 const leftOffset = i * - 8; // 각 아이콘을 8px씩 왼쪽으로 겹치게 iconsHtml += `
${initial}
`; } // 더 많은 사람이 있으면 ... 표시 if (hasMore) { const remainingCount = totalCount - 3; const leftOffset = displayCount * - 8; const remainingNames = uniqueOwners.slice(3).join(', '); iconsHtml += `
+${remainingCount}
`; } // 아이콘들을 감싸는 컨테이너 // padding-left로 겹친 만큼의 공간 확보 const paddingLeft = (displayCount - 1) * 8 + (hasMore ? 8 : 0); return `
${iconsHtml}
`; } //////////////////////////////////////////////////////////////////////////////////////// // 사람 아이콘 렌더링 함수 //////////////////////////////////////////////////////////////////////////////////////// function renderPersonIcon(name) { if (isEmpty(name)) { return `
N/A
`; } else { // 이름의 첫 글자 추출 const initial = name ? name.charAt(0) : '?'; // 이름 기반 색상 생성 (일관된 색상 유지) const colors = generateColorFromName(name); return `
${initial}
`; } } //////////////////////////////////////////////////////////////////////////////////////// // 이름으로부터 일관된 색상 생성 //////////////////////////////////////////////////////////////////////////////////////// function generateColorFromName(name) { const colorPalette = [ { bg: "#579bfc", text: "#ffffff" }, // 파란색 { bg: "#00c875", text: "#ffffff" }, // 녹색 { bg: "#fdab3d", text: "#ffffff" }, // 주황색 { bg: "#e44258", text: "#ffffff" }, // 빨간색 { bg: "#a25ddc", text: "#ffffff" }, // 보라색 { bg: "#037f4c", text: "#ffffff" }, // 진한 녹색 { bg: "#9cd326", text: "#ffffff" }, // 연두색 { bg: "#ff642e", text: "#ffffff" }, // 주황-빨강 { bg: "#7f5347", text: "#ffffff" }, // 갈색 { bg: "#784bd1", text: "#ffffff" }, // 진한 보라 { bg: "#ff158a", text: "#ffffff" }, // 핑크 { bg: "#0086c0", text: "#ffffff" } // 청록색 ]; // 이름의 문자열 해시값 계산 let hash = 0; for (let i = 0; i < name.length; i++) { hash = name.charCodeAt(i) + ((hash << 5) - hash); } // 해시값을 팔레트 인덱스로 변환 (절대값으로 항상 양수) const index = Math.abs(hash) % colorPalette.length; return colorPalette[index]; } //////////////////////////////////////////////////////////////////////////////////////// // 상태 렌더링 함수 //////////////////////////////////////////////////////////////////////////////////////// function renderStatusCell(statusName) { const colors = statusColorMap[statusName] || { bg: "#c4c4c4", text: "#323338" }; return `
${statusName}
`; } //////////////////////////////////////////////////////////////////////////////////////// // 난이도 렌더링 함수 //////////////////////////////////////////////////////////////////////////////////////// function renderDifficultyCell(difficultyName) { const colors = difficultyColorMap[difficultyName] || { bg: "#c4c4c4", text: "#323338" }; return `  ${difficultyName} `; } //////////////////////////////////////////////////////////////////////////////////////// // 중요도 렌더링 함수 //////////////////////////////////////////////////////////////////////////////////////// function renderImportanceCell(importanceName) { const colors = importanceColorMap[importanceName] || { bg: "#c4c4c4", text: "#323338" }; return `  ${importanceName} `; } //////////////////////////////////////////////////////////////////////////////////////// // 긴급도 렌더링 함수 //////////////////////////////////////////////////////////////////////////////////////// function renderUrgencyCell(urgencyName) { const colors = urgencyColorMap[urgencyName] || { bg: "#c4c4c4", text: "#323338" }; return `  ${urgencyName} `; } //////////////////////////////////////////////////////////////////////////////////////// // 데이터 테이블 구성 //////////////////////////////////////////////////////////////////////////////////////// function dataTableLoad(tableData) { var columnList = [ // 1. 요구사항명 { name: "reqName", title: "요구사항명", data: "reqName", render: function (data, type, row) { if (isEmpty(data)) {} else { return `
${data}
`; } }, className: "dt-body-left scrollable-cell", orderable: false, visible: true }, // 2. 요구사항 담당자 { name: "reqOwnerLastName", title: "요구사항 담당자", data: "reqOwnerLastName", render: function (data, type, row) { if (isEmpty(data)) { return renderPersonIcon(); } else { return renderPersonIcon(data); } }, className: "dt-body-center", orderable: false, visible: true }, // 3. 이슈 담당자 { name: "issueOwners", title: "이슈 담당자", data: "issueInfo", render: function (data, type, row) { if (isEmpty(data)) { return renderPersonIcon(); } else { return renderMultiplePersonIcons(data); } }, className: "dt-body-center", orderable: false, visible: true }, // 4. 상태 { name: "reqStatusName", title: "상태", data: "reqStatusName", render: function (data, type, row) { if (isEmpty(data)) {} else { // return `
${renderStatusCell(data)}
`; } }, // ----td에 적용이 필요할 경우 createdCell: function (td, cellData) { const color = statusColorMap[cellData]; if (!color) return; td.style.backgroundColor = color.bg; td.style.color = color.text; td.style.textAlign = "center"; td.style.fontWeight = "500"; td.textContent = cellData; }, className: "dt-body-center", orderable: false, visible: true }, // 5. 긴급도 { name: "reqUrgencyName", title: "긴급도", data: "reqUrgencyName", render: function (data, type, row) { if (isEmpty(data)) {} else { return `
${renderUrgencyCell(data)}
`; } }, className: "dt-body-center", orderable: false, visible: true }, // 6. 중요도 { name: "reqImportanceName", title: "중요도", data: "reqImportanceName", render: function (data, type, row) { if (isEmpty(data)) {} else { return `
${renderImportanceCell(data)}
`; } }, className: "dt-body-center", orderable: false, visible: true }, // 7. 난이도 { name: "reqDifficultyName", title: "난이도", data: "reqDifficultyName", render: function (data, type, row) { if (isEmpty(data)) {} else { return `
${renderDifficultyCell(data)}
`; } }, className: "dt-body-center", orderable: false, visible: true }, // 8. 우선순위 FP로 표현 { name: "reqPriorityValue", title: "우선순위(FP)", data: "reqPriorityValue", render: function (data, type, row) { if (isEmpty(data)) {} else { return `
${data}
`; } }, className: "dt-body-center", orderable: false, visible: true }, // 9. 날짜 (시작일 ~ 종료일) { name: "reqDate", title: "날짜", data: null, render: function (data, type, row) { const start = formatDate(row.reqStartDate); const end = formatDate(row.reqEndDate); return `
${start} ~ ${end}
`; }, className: "dt-body-center", orderable: false, visible: true }, // 10. 산출물 링크 { name: "reqOutputName", title: "산출물", data: "reqOutputName", render: function (data, type, row) { if (isEmpty(data)) {} else { // reqOutputLink가 존재한 경우에만 링크 표시 if (isEmpty(row.reqOutputLink)) {} else { return `
 ${data}
`; } } }, className: "dt-body-center", orderable: false, visible: true }, // TODO: 요구사항 링크 길어질 시 scroll 또는 ellipsis 처리 (좀더 깔끔하게) // 11. 요구사항 링크 { name: "reqLink", title: "요구사항 링크", data: "reqKey", render: function (data, type, row) { if (isEmpty(data)) {} else { return `
${data}
`; } }, className: "dt-body-center scrollable-cell", orderable: false, visible: true }, // TODO: 상세 이슈 키값이 길어질 시 ellipsis처리를 할건지 확인 (좀더 깔끔하게) // 12. 상세 이슈 { name: "issueAggregation", title: "상세 이슈", data: "issueInfo", render: function (data, type, row) { if (isEmpty(data)) {} else { const issueStats = data.map(issue => { // 상태에 따른 색상 가져오기 const statusColor = statusColorMap[issue.issueStatusName]?.bg || "#797e93"; // 이슈 타입에 따른 아이콘 선택 const icon = issue.isLinkedIssue ? "fa-link" : "fa-sitemap"; // TODO: jira는 {jiraServerBaseUrl}/browse/{issueKey}, redmine은 {jiraServerBaseUrl}/issues/{issueKey}로 들어가니 front에서 합치기보다는 backend에서? //jiraServer에서 c_jira_server_type에 내용 존재 return ` ${issue.issueKey} : ${issue.issueStatusName} `; }).join('
'); return `
${issueStats}
`; } }, className: "dt-body-left scrollable-cell", orderable: false, visible: true }, // 13. 최종 업데이트(정렬용으로 필수) { name: "latestUpdateDate", title: "최종 업데이트", data: "latestUpdateDate", visible: false }, ]; // 값이 같을 경우 rowspan이 되어서 null로 막음 var rowsGroupList = null; var columnDefList = [ { targets: "_all", defaultContent: "
N/A
" }, // width 설정 { targets: 0, width: "250px" }, //요구사항명 { targets: 1, width: "50px" }, //요구사항 담당자 { targets: 2, width: "50px" }, //이슈 담당자 { targets: 3, width: "70px" }, //상태 { targets: 4, width: "70px" }, //긴급도 { targets: 5, width: "70px" }, //중요도 { targets: 6, width: "70px" }, //난이도 { targets: 7, width: "50px" }, //우선순위(FP) { targets: 8, width: "150px" }, //날짜 { targets: 9, width: "150px" }, //산출물 { targets: 10, width: "120px" }, //요구사항 링크 { targets: 11, width: "150px" }, //상세 이슈 ]; // 1. 기본정렬: latestUpdateDate 최신순 (내림차순) var orderList = [[12, "desc"]]; // latestUpdateDate 컬럼 인덱스 var jquerySelector = "#reportRTMTable"; var ajaxUrl = ""; var jsonRoot = ""; var buttonList = [ "copy", "excel", "print", { extend: "csv", text: "Export csv", charset: "utf-8", extension: ".csv", fieldSeparator: ",", fieldBoundary: "", bom: true } ]; var selectList = {}; var scrollY = true; var isServerSide = false; var isAjax = false; reportRTMTable = dataTable_build( jquerySelector, ajaxUrl, jsonRoot, columnList, rowsGroupList, columnDefList, selectList, orderList, buttonList, isServerSide, scrollY, tableData, isAjax ); reportRTMTable.columns.adjust(); } //////////////////////////////////////////////////////////////////////////////////////// // 데이터 테이블 클릭 이벤트 //////////////////////////////////////////////////////////////////////////////////////// function dataTableClick(tempDataTable, selectedData) { console.log("[ reportRTM :: dataTableClick ] :: selectedData => ", selectedData); } //////////////////////////////////////////////////////////////////////////////////////// // 데이터 테이블 렌더링 후 콜백 //////////////////////////////////////////////////////////////////////////////////////// function dataTableCallBack(settings, json) { console.log("[ reportRTM :: dataTableCallBack ] :: callback"); if (settings.nTable.id !== "reportRTMTable") { return; } } function dataTableDrawCallback(tableInfo) { console.log("[ reportRTM :: dataTableDrawCallback ]"); } //////////////////////////////////////////////////////////////////////////////////////// // 날짜 포맷 (yyyy/MM/dd) Instant -> String 으로 오는 걸 가정하였음 //////////////////////////////////////////////////////////////////////////////////////// function formatDate(dateValue) { if (!dateValue) return ''; const date = new Date(dateValue); if (isNaN(date)) return ''; const yyyy = date.getFullYear(); const mm = String(date.getMonth() + 1).padStart(2, "0"); const dd = String(date.getDate()).padStart(2, "0"); return `${yyyy}/${mm}/${dd}`; }