/** * Created by wisestone on 2018-05-10. */ 'use strict'; define(['app', 'angular', 'd3'], function (app, angular, d3) { app.directive('jsWorkflow', ["$log", "$rootScope", "IssueStatus", "$resourceProvider", "SweetAlert", '$filter', '$injector', '$controller', function ($log, $rootScope, IssueStatus, $resourceProvider, SweetAlert, $filter, $injector, $controller) { return { scope : { issueStatusList : '=issueStatusList', vm : '=ngModel', isolationWorkflow : '=isolationWorkflow', firstStatusExist : "=firstStatusExist", middleStatusExist : "=middleStatusExist", lastStatusExist : "=lastStatusExist", departments : "=departments" }, restrict : 'E', replace : true, templateUrl : 'custom_components/js-workflow/js-workflow.html', controller : function ($scope, $element, $attrs) { // 함수 모음 $scope.fn = { initializeLayout : initializeLayout, // 기초 레이아웃을 설정한다. makeNodeAndLink : makeNodeAndLink, // 노드와 링크 정보를 생성한다. forceLayout : forceLayout, // 워크플로우 다이어그램을 그린다. initCircleColor : initCircleColor, // 노드 색상을 초기화한다. initLinkColor : initLinkColor, // 전이선 색상을 초기화한다. makeNode : makeNode, // 노드를 만든다. makeLink : makeLink, // 전이선을 만든다. makeDragEvent : makeDragEvent, // 드래그 이벤트 생성 makeGridLine : makeGridLine, // 표시되는 선을 생성한다. executeForce : executeForce, // 포스 레이아웃 실행 setLink : setLink, // 링크를 셋팅한다. setNode : setNode, // 노드를 셋팅한다. setNodeTexts : setNodeTexts, // 노드 텍스트를 셋팅한다. targetClick : targetClick, // 타겟을 클릭했을 때 색상을 변경한다. tick : tick, // 다이어그램에서 도형을 움직일 때 실행 startDiagram : startDiagram, // 다이어그램을 그리기 시작한다. getIssueStatusList : getIssueStatusList, // 전체 이슈 상태를 가져온다. addStatus : addStatus, // 이슈 상태를 다이어그램에 추가한다. addTransition : addTransition, // 전이선을 다이어그램에 추가한다. getAvailableStartStatus : getAvailableStartStatus, // 시작하는 이슈 상태의 선택 목록을 가져온다. getAvailableNextStatus : getAvailableNextStatus, // 이동할 이슈 상태의 선택 목록을 가져온다. setLastStatusPosition : setLastStatusPosition, // 상태 마지막 위치 정보 저장 setLastTransitionPosition : setLastTransitionPosition, // 전이선 마지막 위치 정보 저장 updateClientChangePosition : updateClientChangePosition, // 클라이언트에서 변경한 위치 정보를 클라이언트에서 관리하는 데이터에 업데이트한다. checkIsolationWorkflowAndExceptionGenerate : checkIsolationWorkflowAndExceptionGenerate, // 고립된 이슈 상태가 없는 워크플로우인지 확인한다. - 고립되었을 경우에는 저장 금지 checkRequireIssueStatusTypeExceptionGenerate : checkRequireIssueStatusTypeExceptionGenerate, // 상태 속성 별로 1개씩 존재하는지 확인한다. - 부족할 경우 저장 금지 generatorTransitionId : generatorTransitionId, // 전이선 임시 id 얻기 removeWorkflowTransition : removeWorkflowTransition, // 전이선을 삭제한다. removeIssueStatus : removeIssueStatus, // 이슈 상태를 제거한다. defaultAddIssueStatusId : defaultAddIssueStatusId, // 이슈 상태 추가 셀렉트 박스에서 첫번째 옵션 항목을 기본 선택되게 한다. checkReadyType : checkReadyType, // 다이어그램안에 상태 속성 '대기'인 이슈 상태가 존재하는지 확인한다. getOptionColor : getOptionColor, // 이슈 상태의 색상 정보를 가져온다. changeDepartment : changeDepartment, removeDepartment : removeDepartment }; // 변수 모음 $scope.vm.targetIssueStatusList = []; $scope.vm.issueStatuses = []; // 선택 가능한 이슈 상태 목록 $scope.vm.addIssueStatusId = null; // 추가하는 이슈 상태 아이디 $scope.vm.issueStatusVos = []; // 화면에 있는 이슈 상태 목록 $scope.vm.width = 0; // 넓이 $scope.vm.height = 0; // 높이 $scope.vm.svg = null; // svg 정보 $scope.vm.rect = null; // 다이어그램이 그려지는 사각형 정보 $scope.vm.nodes = []; // 원 정보 모음 $scope.vm.links = []; // 링크 정보 모음 $scope.vm.activeTarget = null; // 현재 선택한 대상 정보 $scope.vm.drag = null; $scope.vm.force = null; $scope.vm.node = null; $scope.vm.link = null; $scope.vm.nodeTexts = null; // 원에 표시되는 텍스트 $scope.vm.sourceStatusId = null; // 출발하는 이슈 상태 아이디 $scope.vm.targetStatusId = null; // 연결하는 이슈 상태 아이디 $scope.vm.transitionIdGenerator = 100; // 전이선 임시 id 값 $scope.vm.departments = []; // 부서 목록 $scope.vm.departmentName = "" // 선택된 부서 이름 angular.extend(this, $controller('autoCompleteController', {$scope : $scope, $injector : $injector})); $scope.$watch("issueStatusList", function (newValue) { if ($rootScope.isDefined(newValue)) { $scope.fn.startDiagram(); setWorkflowDepartments(); } }) // 저장된 담당부서 설정하기 function setWorkflowDepartments() { var workflowDepartmentVos = []; angular.forEach($scope.issueStatusList, function (issueStatus) { angular.forEach($scope.vm.issueStatusVos, function (issueStatusVo) { if (issueStatusVo.id === issueStatus.id) { var issueStatusWorkflowDepartments = { workflowDepartmentVos : workflowDepartmentVos, workflowDepartmentName : "" } issueStatusVo = Object.assign(issueStatusVo, issueStatusWorkflowDepartments); } }); }) } // 이슈 상태 추가 셀렉트 박스에서 첫번째 옵션 항목을 기본 선택되게 한다. function defaultAddIssueStatusId() { if ($scope.vm.issueStatuses.length > 0) { $scope.vm.addIssueStatusId = String($scope.vm.issueStatuses[0].id); } else { $scope.vm.addIssueStatusId = null; } } // 다이어그램을 그리기 시작한다. function startDiagram() { $("#svgContent").empty(); $scope.fn.getIssueStatusList(); $scope.vm.width = $(".workflowbox").width(); // 넓이 $scope.vm.height = $(".workflowbox").height(); // 높이 $scope.vm.activeTarget = null; // 타겟 초기화 $scope.fn.initCircleColor(null, true); // 마지막 선택 노드 색상 초기화 $scope.fn.forceLayout(); // 다이어그램 에디터 그리기 시작 $scope.fn.checkRequireIssueStatusTypeExceptionGenerate(); // 상태 속성 별로 1개씩 존재하는지 확인한다. - 부족할 경우 저장 금지 $scope.fn.checkIsolationWorkflowAndExceptionGenerate(); // 고립된 이슈 상태가 없는 워크플로우인지 확인한다. - 고립되었을 경우에는 저장 금지 } // 전이선 임시 id 얻기 function generatorTransitionId() { return "transition" + $scope.vm.transitionIdGenerator++; } // 상태 마지막 위치 정보 저장 function setLastStatusPosition(issueStatus) { issueStatus.xLocation = $scope.vm.nodes[issueStatus.name].x; issueStatus.yLocation = $scope.vm.nodes[issueStatus.name].y; } // 전이선 마지막 위치 정보 저장 function setLastTransitionPosition(workflowTransition, index) { workflowTransition.correctX = $scope.vm.links[index].correctX; workflowTransition.correctY = $scope.vm.links[index].correctY; workflowTransition.direct = $scope.vm.links[index].direct; } // 클라이언트에서 변경한 위치 정보를 클라이언트에서 관리하는 데이터에 업데이트한다. function updateClientChangePosition() { // 서버에서 내려온 데이터에 전이선 변경 정보를 업데이트한다. angular.forEach($scope.vm.issueStatusVos, function (issueStatusVo) { angular.forEach(issueStatusVo.workflowTransitionVos, function (workflowTransition) { for (var count in $scope.vm.links) { if ($scope.vm.links[count].id == workflowTransition.id) { $scope.fn.setLastTransitionPosition(workflowTransition, count); break; } } }); $scope.fn.setLastStatusPosition(issueStatusVo); }); } // 시작하는 이슈 상태의 선택 목록을 가져온다. function getAvailableStartStatus() { var availableStatus = []; angular.forEach($scope.vm.issueStatusVos, function (startStatus) { if ($scope.vm.targetStatusId != startStatus.id) { switch (startStatus.issueStatusType) { case 'READY' : startStatus.issueStatusTypeKr = '대기' break; case 'OPEN' : startStatus.issueStatusTypeKr = '진행' break; case 'CLOSE' : startStatus.issueStatusTypeKr = '종료' break; } availableStatus.push(startStatus); } }); availableStatus.sort(function (a, b) { return a.name > b.name ? 1 : 0 }); return availableStatus; } // 이동할 이슈 상태의 선택 목록을 가져온다. function getAvailableNextStatus() { $scope.vm.targetIssueStatusList = []; $scope.vm.targetStatusId = null; if (!$rootScope.isDefined($scope.vm.sourceStatusId)) { return; } var excludeIds = []; angular.forEach($scope.vm.issueStatusVos, function (issueStatus) { // 선택한 노드가 있을 때 if ($scope.vm.sourceStatusId == issueStatus.id) { angular.forEach(issueStatus.workflowTransitionVos, function (workflowTransition) { excludeIds.push(workflowTransition.targetStatusId); }); } }); // 현재 선택한 대상도 제외대상에 추가 excludeIds.push($scope.vm.sourceStatusId); angular.forEach($scope.vm.issueStatusVos, function (issueStatus) { var notAvailable = false; for (var count in excludeIds) { if (excludeIds[count] == issueStatus.id) { notAvailable = true; break; } } if (!notAvailable) { $scope.vm.targetIssueStatusList.push(issueStatus); } }); $scope.vm.targetIssueStatusList.sort(function (a, b) { return a.name > b.name ? 1 : 0 }); } // 다이어그램안에 상태 속성 '대기'인 이슈 상태가 존재하는지 확인한다. function checkReadyType(addIssueStatusId) { var addIssueStatus = null; for (var count in $scope.vm.issueStatuses) { if ($scope.vm.issueStatuses[count].id == addIssueStatusId) { addIssueStatus = $scope.vm.issueStatuses[count]; break; } } $log.debug("$scope.vm.issueStatuses 확인 : ", $scope.vm.issueStatuses); $log.debug("$scope.vm.issueStatusVos 확인 : ", $scope.vm.issueStatusVos); $log.debug("addIssueStatusId 확인 : ", addIssueStatusId); $log.debug("addIssueStatus 확인 : ", addIssueStatus); // 추가하려는 이슈 상태의 상태 속성 확인 if ($rootScope.isDefined(addIssueStatus)) { if (addIssueStatus.issueStatusType === "READY") { var readyCount = 0; angular.forEach($scope.vm.issueStatusVos, function (issueStatusVo) { if (issueStatusVo.issueStatusType === "READY") { readyCount++; } }); $log.debug("readyCount 확인 : ", readyCount); if (readyCount > 0) { return false; } } } return true; } // 이슈 상태의 색상 정보를 가져온다 function getOptionColor(list, id) { var color = "#353535"; // 기본색은 검은색. for (var count in list) { if (String(list[count].id) === id) { color = list[count].color; break; } } return color; } // 이슈 상태를 다이어그램에 추가한다. function addStatus() { if ($rootScope.isDefined($scope.vm.addIssueStatusId)) { // 클라이언트 변경 사항 $scope.vm.issueStatusVos 에 저장 $scope.fn.updateClientChangePosition(); // 상태 속성이 대기인 이슈 상태가 이미 존재할 경우에는 상태를 넣을 수 없어야 한다. if (!$scope.fn.checkReadyType($scope.vm.addIssueStatusId)) { SweetAlert.error($filter("translate")("managementWorkflow.failedToPutIssueStatus"), $filter("translate")("managementWorkflow.onlyOneInTheWorkflow")); // "이슈 상태 넣기 실패", "워크플로우에는 상태 속성 '대기'인 이슈는 1개만 존재해야 합니다." return; } // 추가할 이슈 상태를 넣는다. for (var count in $scope.vm.issueStatuses) { var issueStatus = $scope.vm.issueStatuses[count]; if (issueStatus.id == $scope.vm.addIssueStatusId) { issueStatus.workflowTransitionVos = []; issueStatus.xLocation = Math.random() * ($scope.vm.width - 50); issueStatus.yLocation = Math.random() * ($scope.vm.height - 50); $scope.vm.issueStatusVos.push(issueStatus); // 담당 부서 추가 addWorkflowDepartment(issueStatus); break; } } // 이슈 상태를 추가하면 시작점을 초기화해준다. -> 종점 업데이트를 위해 $scope.vm.sourceStatusId = null; $scope.fn.defaultAddIssueStatusId(); $scope.fn.startDiagram(); } } // 이슈 상태를 제거한다. function removeIssueStatus(targetIssueStatus) { $scope.fn.initCircleColor(null, true); // 전체 노드 색상 초기화 var tempWorkflowStatusVos = []; // 삭제 대상 상태 제거 angular.forEach($scope.vm.issueStatusVos, function (issueStatusVo) { if (issueStatusVo.id != targetIssueStatus.id) { $scope.fn.setLastStatusPosition(issueStatusVo); tempWorkflowStatusVos.push(issueStatusVo); } }); $scope.vm.issueStatusVos = angular.copy(tempWorkflowStatusVos); // 삭제 대상 상태 정보를 갖고있는 전이 제거 angular.forEach($scope.vm.issueStatusVos, function (issueStatusVo) { var tempWorkflowTransitionVos = []; angular.forEach(issueStatusVo.workflowTransitionVos, function (workflowTransition) { if (workflowTransition.sourceStatusId != targetIssueStatus.id && workflowTransition.targetStatusId != targetIssueStatus.id) { // 보정 값 업데이트 하기 for (var count in $scope.vm.links) { if ($scope.vm.links[count].id == workflowTransition.id) { $scope.fn.setLastTransitionPosition(workflowTransition, count); break; } } tempWorkflowTransitionVos.push(workflowTransition); } }); issueStatusVo.workflowTransitionVos = tempWorkflowTransitionVos; }); // 선택 대상 정보 초기화 $scope.vm.activeTarget = null; // 클라이언트 변경 사항 $scope.vm.issueStatusVos 에 저장 $scope.fn.updateClientChangePosition(); $scope.fn.startDiagram(); removeWorkflowDepartment(targetIssueStatus); } // 전이선을 다이어그램에 추가한다. function addTransition() { if (!$rootScope.isDefined($scope.vm.targetStatusId)) { return; } // 클라이언트 변경 사항 $scope.vm.issueStatusVos 에 저장 $scope.fn.updateClientChangePosition(); var insertWorkflowTransition = { id : $scope.fn.generatorTransitionId(), sourceStatusId : "", sourceStatusName : "", targetStatusId : "", targetStatusName : "", correctX : null, correctY : null, direct : false, type : "02", colorClass : "linkBlue", workflowTransition : null }; // source, target 이름 추출 for (var count in $scope.vm.issueStatusVos) { var issueStatus = $scope.vm.issueStatusVos[count]; if ($scope.vm.sourceStatusId == issueStatus.id) { insertWorkflowTransition.sourceStatusId = issueStatus.id; insertWorkflowTransition.sourceStatusName = issueStatus.name; } if ($scope.vm.targetStatusId == issueStatus.id) { insertWorkflowTransition.targetStatusId = issueStatus.id; insertWorkflowTransition.targetStatusName = issueStatus.name; } } for (var count in $scope.vm.issueStatusVos) { var issueStatus = $scope.vm.issueStatusVos[count]; if ($scope.vm.sourceStatusId == issueStatus.id) { issueStatus.workflowTransitionVos.push(insertWorkflowTransition); $scope.vm.sourceStatusId = null; $scope.vm.targetStatusId = null; $scope.fn.startDiagram(); break; } } } // 전이선을 삭제한다. function removeWorkflowTransition(targetWorkflowTransition) { $scope.fn.initLinkColor(null, true); // 전이선 색상 전체 초기화 // 삭제 대상 전이 제거 angular.forEach($scope.vm.issueStatusVos, function (issueStatus) { var tempWorkflowTransitionVos = []; issueStatus.xLocation = $scope.vm.nodes[issueStatus.name].x; issueStatus.yLocation = $scope.vm.nodes[issueStatus.name].y; angular.forEach(issueStatus.workflowTransitionVos, function (workflowTransition) { if (workflowTransition.id != targetWorkflowTransition.id) { // 보정 값 업데이트 하기 for (var count in $scope.vm.links) { if ($scope.vm.links[count].id == workflowTransition.id) { workflowTransition.correctX = $scope.vm.links[count].correctX; workflowTransition.correctY = $scope.vm.links[count].correctY; workflowTransition.direct = $scope.vm.links[count].direct; break; } } tempWorkflowTransitionVos.push(workflowTransition); } }); issueStatus.workflowTransitionVos = tempWorkflowTransitionVos; }); // 선택 대상 정보 초기화 $scope.vm.activeTarget = null; // 클라이언트 변경 사항 $scope.vm.issueStatusVos 에 저장 $scope.fn.updateClientChangePosition(); $scope.fn.startDiagram(); } // 고립된 이슈 상태가 없는 워크플로우인지 확인한다. - 고립되었을 경우에는 저장 금지 function checkIsolationWorkflowAndExceptionGenerate() { var firstCheck = false; var middleCheck = false; var lastCheck = true; var connectCheck = false; // source/target 연결 찾을 경우 값 변경 var transitionList = []; // 전체 전이선을 추출한다. angular.forEach($scope.vm.issueStatusVos, function (issueStatusVo) { angular.forEach(issueStatusVo.workflowTransitionVos, function (workflowTransition) { transitionList.push(workflowTransition); }); }); // 시작 상태 정상 확인 angular.forEach($scope.vm.issueStatusVos, function (issueStatusVo) { switch (issueStatusVo.issueStatusType) { case "READY" : for (var count in transitionList) { if (transitionList[count].sourceStatusId == issueStatusVo.id) { firstCheck = true; break; } } break; case "OPEN" : var normalSourceCheck = false; // 일반 상태일 때 source 연결 여부 확인 var normalTargetCheck = false; // 일반 상태일 때 target 연결 여부 확인 middleCheck = true; for (var count in transitionList) { if (transitionList[count].sourceStatusId == issueStatusVo.id) { normalSourceCheck = true; } if (transitionList[count].targetStatusId == issueStatusVo.id) { normalTargetCheck = true; } } if (!normalSourceCheck || !normalTargetCheck) { connectCheck = true; } break; case "CLOSE" : // '종료'인 상태에 다른 상태가 있는지 확인 // 한번도 없는걸 찾는다. // 이번 상태를 타겟으로 하는 전이선 정보가 있는지 확인한다. // 있으면 그냥 통과 // 없으면 문제 발생 var tempLastCheck = false; for (var count in transitionList) { if (transitionList[count].targetStatusId == issueStatusVo.id) { tempLastCheck = true; break; } } // 없으면 문제 발생 if (!tempLastCheck) { lastCheck = false; } break; } }); // 고립된 상태가 존재하는지 확인한다. if (!firstCheck || !middleCheck || !lastCheck || connectCheck) { $scope.isolationWorkflow = true; } else { $scope.isolationWorkflow = false; } } // 상태 속성 별로 1개씩 존재하는지 확인한다. - 부족할 경우 저장 금지 function checkRequireIssueStatusTypeExceptionGenerate() { $scope.firstStatusExist = true; $scope.middleStatusExist = true; $scope.lastStatusExist = true; angular.forEach($scope.vm.issueStatusVos, function (issueStatusVo) { switch (issueStatusVo.issueStatusType) { case "READY" : $scope.firstStatusExist = false; break; case "OPEN" : $scope.middleStatusExist = false; break; case "CLOSE" : $scope.lastStatusExist = false; break; } }); } // 전체 이슈 상태를 가져온다. function getIssueStatusList() { $scope.vm.issueStatuses = []; IssueStatus.findAll($resourceProvider.getContent({}, $resourceProvider.getPageContent(0, 0))).then(function (result) { if (result.data.message.status === "success") { angular.forEach(result.data.data, function (issueStatus) { var exist = false; for (var count in $scope.vm.issueStatusVos) { if ($scope.vm.issueStatusVos[count].id == issueStatus.id) { exist = true; break; } } if (!exist) { switch (issueStatus.issueStatusType) { case 'READY' : issueStatus.issueStatusTypeKr = '대기' break; case 'OPEN' : issueStatus.issueStatusTypeKr = '진행' break; case 'CLOSE' : issueStatus.issueStatusTypeKr = '종료' break; } $scope.vm.issueStatuses.push(issueStatus); } }); // 이슈 상태 추가 셀렉트 박스에서 첫번째 옵션 항목을 기본 선택되게 한다. $scope.fn.defaultAddIssueStatusId(); } else { SweetAlert.error($filter("translate")("common.failedToIssueStatusListLookup"), result.data.message.message); // "이슈 상태 목록 조회 실패" } }); } // 노드 색상을 초기화한다. function initCircleColor(target, all) { d3.selectAll("circle").each(function () { var circle = d3.select(this)[0][0]["__data__"]; if (all) { d3.select(this).style("fill", circle.originalColor); } else { if (circle.id != target.id) { d3.select(this).style("fill", circle.originalColor); } } }); } // 전이선 색상을 초기화한다. function initLinkColor(target, all) { d3.selectAll(".link").each(function () { var link = d3.select(this)[0][0]["__data__"]; // 전체 초기화일경우 if (all) { d3.select(this).classed("linkGreen", false); d3.select(this).classed(link.colorClass, true); } else { if (link.id != target.id) { d3.select(this).classed("linkGreen", false); d3.select(this).classed(link.colorClass, true); } } d3.select(this).attr("marker-end", "url(#licensing)"); }); } // function getWorkflowDepartments() { // $scope.vm.departments = []; // // if ($scope.vm.activeTarget != null) { // return issueStatus.workflowDepartments; // } // return null; // } // 담당부서 설정 function setWorkflowDepartment(targetIssueStatus) { if (targetIssueStatus != null) { angular.forEach($scope.vm.issueStatusVos, function (issueStatusVo) { if (issueStatusVo.id === targetIssueStatus.id) { $scope.vm.departmentName = issueStatusVo.workflowDepartmentName; $scope.vm.departments = []; angular.forEach(issueStatusVo.workflowDepartmentVos, function (workflowDepartment) { $scope.vm.departments.push(workflowDepartment.departmentVo); }); } }); } } // 선택 한 부서 제거 function removeDepartment(index) { if (index < $scope.vm.departments.length) { $scope.vm.departments.splice(index, 1); changeDepartment(); } } function changeDepartment() { if ($scope.vm.activeTarget != null) { var targetIssueStatus = $scope.vm.activeTarget.issueStatus; // null 체크 if (targetIssueStatus != null) { var myIssueStatus = null; angular.forEach($scope.vm.issueStatusVos, function (issueStatusVo) { if (issueStatusVo.id === targetIssueStatus.id) { myIssueStatus = issueStatusVo; } }); if (myIssueStatus != null) { if ($scope.vm.departments != null) { var workflowDepartments = []; angular.forEach($scope.vm.departments, function (department) { var workflowDepartment = { departmentVo : department } workflowDepartments.push(workflowDepartment); }); myIssueStatus.workflowDepartmentVos = workflowDepartments; } } } } } // 담당부서 추가 function addWorkflowDepartment(targetIssueStatus) { if ($scope.departments != null && $scope.departments.length > 0) { var workflowDepartmentVos = []; angular.forEach($scope.vm.issueStatusVos, function (issueStatusVo) { if (issueStatusVo.id === targetIssueStatus.id) { var issueStatusWorkflowDepartments = { workflowDepartmentVos : workflowDepartmentVos, workflowDepartmentName : "" } issueStatusVo = Object.assign(issueStatusVo, issueStatusWorkflowDepartments); } }); } } // 담당부서 삭제 function removeWorkflowDepartment(targetIssueStatus) { if ($scope.vm.workflowDepartmentVos != null && $scope.vm.workflowDepartmentVos.length > 0) { $scope.vm.workflowDepartmentVos = $scope.vm.workflowDepartmentVos.filter((el) => el.issueStatus.id === targetIssueStatus.id); } } // 타겟을 클릭했을 때 색상을 변경한다. // 담당 부서를 불러온다 function targetClick(target) { $scope.vm.activeTarget = angular.copy(target[0][0]["__data__"]); setWorkflowDepartment($scope.vm.activeTarget.issueStatus); // node 일경우 if ($scope.vm.activeTarget.type == "01") { // 노드, 링크 전체 초기화 $scope.fn.initCircleColor($scope.vm.activeTarget, false); $scope.fn.initLinkColor(null, true); // 대상 노드 색 변경 target.style("fill", "#1DDB16"); } else if ($scope.vm.activeTarget.type == "02") { // 노드, 링크 전체 초기화 $scope.fn.initCircleColor(null, true); $scope.fn.initLinkColor($scope.vm.activeTarget, false); // 대상 링크 색 변경 target.classed("linkGreen", true); target.classed($scope.vm.activeTarget.colorClass, false); target.attr("marker-end", "url(#greenLicensing)"); } if ($scope.$root.$$phase != '$apply' && $scope.$root.$$phase != '$digest') { $scope.$apply(); } } // 다이어그램에서 도형을 움직일 때 실행 function tick() { //IE mark bug fix $scope.vm.link.each(function () { this.parentNode.insertBefore(this, this); }); $scope.vm.link.attr("d", function (d) { //svg 영역 내에 표시 if (d.target.x > $scope.vm.width) { d.target.x = ($scope.vm.width - 31); } else if (d.target.x < 30) { d.target.x = 30; } if (d.source.x > $scope.vm.width) { d.source.x = ($scope.vm.width - 31); } else if (d.source.x < 30) { d.source.x = 30; } if (d.target.y > $scope.vm.height) { d.target.y = ($scope.vm.height - 31); } else if (d.target.y < 30) { d.target.y = 30; } if (d.source.y > $scope.vm.height) { d.source.y = ($scope.vm.height - 31); } else if (d.source.y < 30) { d.source.y = 30; } if (d.correctX == null) { d.correctX = d.source.x; } if (d.correctY == null) { d.correctY = d.source.y; } // 직선화 유지 상태 일 경우 if (d.direct) { d.correctX = d.source.x; d.correctY = d.source.y; } return "M" + d.source.x + "," + d.source.y + " Q" + d.correctX + ", " + d.correctY + " " + d.target.x + ", " + d.target.y; }); $scope.vm.node.attr("transform", function (d) { if (d.x > $scope.vm.width) { d.x = ($scope.vm.width - 31); } else if (d.x < 30) { d.x = 30; } if (d.y > $scope.vm.height) { d.y = ($scope.vm.height - 31); } else if (d.y < 30) { d.y = 30; } return "translate(" + d.x + "," + d.y + ")"; }); $scope.vm.nodeTexts.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; }); // on 이벤트가 과다하게 발생하는 현상을 방지 $scope.vm.force.stop(); } // 노드를 만든다. function makeNode(issueStatus, sXLocation, sYLocation) { var xLocation = 0; var yLocation = 0; var color = "FAED7D"; // x좌표 if (issueStatus.xLocation == 0 || issueStatus.xLocation == null) { xLocation = sXLocation; } else { xLocation = issueStatus.xLocation; } // y좌표 if (issueStatus.yLocation == 0 || issueStatus.yLocation == null) { yLocation = sYLocation; } else { yLocation = issueStatus.yLocation; } if (issueStatus.color != null) { color = issueStatus.color; } return { id : issueStatus.id, name : issueStatus.name, x : xLocation, y : yLocation, color : color, issueStatusType : issueStatus.issueStatusType, originalColor : issueStatus.color, fixed : true, type : "01", // 수정 모드시 구분 issueStatus : issueStatus } } // 전이선을 만든다. function makeLink(workflowTransition) { return { id : workflowTransition.id, source : workflowTransition.sourceStatusName, target : workflowTransition.targetStatusName, correctX : workflowTransition.correctX, correctY : workflowTransition.correctY, direct : workflowTransition.direct, type : "02", colorClass : "linkBlue", workflowTransition : workflowTransition } } // 1. 서버에서 데이터가 내려올때 사용된 이슈 상태 전체 목록과 각 이슈 상태의 전이선 정보가 내려와야 한다. // 노드와 링크 정보를 생성한다. function makeNodeAndLink() { $scope.vm.nodes = []; // 원 정보 모음 $scope.vm.links = []; // 링크 정보 모음 var startX = 50; // 새로운 도형의 X축 위치 var startY = 50; // 새로운 도형의 Y축 위치 // 노드와 링크 정보를 만든다. angular.forEach($scope.vm.issueStatusVos, function (issueStatusVo, index) { $scope.vm.nodes[issueStatusVo.name] = $scope.fn.makeNode(issueStatusVo, startX, startY); angular.forEach(issueStatusVo.workflowTransitionVos, function (workflowTransition) { $scope.vm.links.push($scope.fn.makeLink(workflowTransition)); }); // 기본 좌표가 없을 경우 계속 60씩 증가 startX += 60; if (index % 2 == 0) { startY += 30; } else { startY += 30; } }); angular.forEach($scope.vm.links, function (link) { if (link.source == $scope.vm.nodes[link.source].name) { link.source = $scope.vm.nodes[link.source]; } if (link.target == $scope.vm.nodes[link.target].name) { link.target = $scope.vm.nodes[link.target]; } }); } // 기초 레이아웃을 설정한다. function initializeLayout() { $scope.vm.svg = d3.select("#svgContent") .append("svg") .attr("width", $scope.vm.width) .attr("height", $scope.vm.height); $scope.vm.rect = $scope.vm.svg.append("rect") .attr("width", $scope.vm.width) .attr("height", $scope.vm.height) .attr("fill", "white") .on("click", function (d) { // 원 색상 초기화 $scope.fn.initCircleColor(null, true); // 링크 색상 초기화 $scope.fn.initLinkColor(null, true); // 현재 선택한 대상 정보 $scope.vm.activeTarget = null; if ($scope.$root.$$phase != '$apply' && $scope.$root.$$phase != '$digest') { $scope.$apply(); } }); } // 드래그 이벤트 생성 function makeDragEvent() { // 드래그 $scope.vm.drag = d3.behavior.drag().on("drag", function (d) { d.direct = false; // 링크 드래그시 직선화 유지 해제 d.correctX += d3.event.dx; d.correctY += d3.event.dy; $scope.fn.tick(); }); } // 표시되는 선을 생성한다. function makeGridLine() { // 기본 표시되는 선 $scope.vm.svg.append("marker") .attr("orient", "auto") .attr("markerHeight", 4) .attr("markerWidth", 4) .attr("refX", 18) .attr("viewBox", "0 -5 10 10") .attr("id", "licensing") .append("path") .attr("d", function () { return "M0,-5 L10,0 L0,5"; }); // 선을 눌러서 활성화되었을때 $scope.vm.svg.append("marker") .attr("orient", "auto") .attr("markerHeight", 5) .attr("markerWidth", 5) .attr("refX", 16) .attr("viewBox", "0 -5 10 10") .attr("id", "greenLicensing") .append("path") .attr("d", function () { return "M0,-5 L10,0 L0,5"; }); } // 포스 레이아웃 실행 function executeForce() { $scope.vm.force = d3.layout.force() .nodes(d3.values($scope.vm.nodes)) .links($scope.vm.links) .size([$scope.vm.width, $scope.vm.height]) .on("tick", $scope.fn.tick) .start(); } // 링크를 셋팅한다. function setLink() { var linkMenu = [{ title : $filter("translate")("managementWorkflow.removeMetastaticLine"), // 전이선 제거 action : function (element) { $scope.fn.removeWorkflowTransition(d3.select(element)[0][0]); } }]; $scope.vm.link = $scope.vm.svg.append("svg:g") .selectAll(".link") .data($scope.vm.force.links()) .enter() .append("path") .attr("class", function (d) { return "link " + d.colorClass; }) .attr("id", function (d, i) { return "link_" + i; }) .attr("marker-end", function (d) { return "url(#licensing)"; }) .attr("cursor", "pointer") .on("contextmenu", d3.contextMenu(linkMenu)) .on("click", function () { $scope.fn.targetClick(d3.select(this)); }); } // 노드를 셋팅한다. function setNode() { var nodeMenu = [{ title : $filter("translate")("managementWorkflow.removeIssueStatus"), // "이슈 상태 제거" action : function (element) { $scope.fn.removeIssueStatus(d3.select(element)[0][0]); // 이슈 상태가 제거되었을 때 시작점, 종점 초기화를 위해서.. $scope.vm.sourceStatusId = null; $scope.fn.getAvailableNextStatus(); } }]; $scope.vm.node = $scope.vm.svg.append("svg:g") .selectAll("circle") .data($scope.vm.force.nodes()) .enter() .append("circle") .attr("class", "node") .style("fill", function (d) { return d.color; }) .style("cursor", "pointer") .attr("r", 15) .on("contextmenu", d3.contextMenu(nodeMenu)) .on("click", function () { $scope.fn.targetClick(d3.select(this)); }); } // 노드 텍스트를 셋팅한다. function setNodeTexts() { $scope.vm.nodeTexts = $scope.vm.svg.append("svg:g") .selectAll("text") .data($scope.vm.force.nodes()) .enter() .append("text") .attr("class", "label") .attr("fill", "black") .attr("dy", 30) .style("font-size", "12px") .style("text-anchor", "middle") .text(function (d) { var name = ""; if (d.name != null) { if (d.name.length > 10) { name = d.name.substring(0, 10) + "..."; } else { name = d.name; } } return name; }); } // 워크플로우 계획 다이어그램을 그린다. function forceLayout() { $scope.fn.initializeLayout($scope.vm.width, $scope.vm.height); $scope.fn.makeNodeAndLink(); $scope.fn.makeDragEvent(); $scope.fn.makeGridLine(); $scope.fn.executeForce($scope.vm.width, $scope.vm.height); $scope.fn.setLink(); $scope.fn.setNode(); $scope.fn.setNodeTexts(); $scope.vm.link.call($scope.vm.drag); $scope.vm.node.call($scope.vm.force.drag); } }, link : function (scope, element, attrs) { } } }]) });