////////////////////////////////////////////////////////////////////////////////////////
//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 `
`;
} else {
// 이름의 첫 글자 추출
const initial = name ? name.charAt(0) : '?';
// 이름 기반 색상 생성 (일관된 색상 유지)
const colors = generateColorFromName(name);
return `
`;
}
}
////////////////////////////////////////////////////////////////////////////////////////
// 이름으로부터 일관된 색상 생성
////////////////////////////////////////////////////////////////////////////////////////
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 ``;
}
}
},
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 ``;
}
},
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}`;
}