src/main/java/kr/wisestone/owl/service/IssueService.java
@@ -30,8 +30,12 @@ Issue addRelIssue(IssueForm issueForm, List<MultipartFile> files); Issue addDownIssue(IssueForm issueForm, List<MultipartFile> files); Issue addRelIssue(User user, IssueForm issueForm, List<MultipartFile> multipartFiles); Issue addDownIssue(User user, IssueForm issueForm, List<MultipartFile> multipartFiles); List<Issue> addApiIssue(IssueApiForm issueApiForm) throws CloneNotSupportedException; List<Issue> modifyIssue(IssueApiForm issueApiForm, List<MultipartFile> files); src/main/java/kr/wisestone/owl/service/impl/IssueServiceImpl.java
@@ -523,6 +523,91 @@ return addRelIssue(user, issueForm, multipartFiles); } // 하위이슈를 생성한다. @Override @Transactional public Issue addDownIssue(IssueForm issueForm, List<MultipartFile> multipartFiles) { User user = this.webAppUtil.getLoginUserObject(); return addDownIssue(user, issueForm, multipartFiles); } // 하위이슈를 생성한다. @Override @Transactional public Issue addDownIssue(User user, IssueForm issueForm, List<MultipartFile> multipartFiles) { // 사용하고 있는 업무 공간이 활성 상태인지 확인한다. 사용 공간에서 로그인한 사용자가 비활성인지 확인한다. Workspace workspace = this.workspaceService.checkUseWorkspace(user, user.getLastWorkspaceId()); // 프로젝트 유효성 체크 Project project = this.projectService.getProject(issueForm.getProjectId()); // 이슈 유형 유효성 체크 IssueType issueType = this.issueTypeService.getIssueType(issueForm.getIssueTypeId()); // 우선순위 유효성 체크 Priority priority = this.priorityService.getPriority(issueForm.getPriorityId()); // 중요도 유효성 체크 Severity severity = this.severityService.getSeverity(issueForm.getSeverityId()); // 제목 유효성 체크 this.verifyTitle(issueForm.getTitle()); // 날짜 유효성 체크 this.checkStartCompleteDate(issueForm.getStartDate(), issueForm.getCompleteDate()); // 담당 부서 유효성 체크 //this.verifyIssueDepartment(project, issueForm); // 이슈 상태 유형이 '대기' 인 이슈 상태 가져오기 IssueStatus issueStatus = this.issueStatusService.findByIssueStatusTypeIsReady(issueType.getWorkflow()); Issue issue = ConvertUtil.copyProperties(issueForm, Issue.class); issue.setProject(project); issue.setIssueStatus(issueStatus); issue.setIssueType(issueType); issue.setPriority(priority); issue.setSeverity(severity); if (issueForm.getParentIssueId() != null){ Issue parentIssue = this.getIssue(issueForm.getParentIssueId()); issue.setParentIssue(parentIssue); } issue.setIssueNumber(this.issueNumberGeneratorService.generateIssueNumber(project)); // 각 프로젝트의 고유 이슈 번호 생성 issue = this.issueRepository.saveAndFlush(issue); issue.setReverseIndex(issue.getId() * -1); // 쿼리 속도 개선을 위해 리버스 인덱스 생성 // 담당자 지정 //this.issueUserService.modifyIssueUser(issue, project.getWorkspace(), issueForm.getUserIds()); // 담당부서 지정 this.issueDepartmentService.modifyIssueDepartment(issue, user, project.getWorkspace(), issueForm.getDepartmentIds()); // 업체 정보 저장 this.issueCompanyService.modifyIssueCompanyField(issue, issueForm.getIssueCompanyFields()); // ISP 정보 저장 this.issueIspService.modifyIssueIspField(issue, issueForm.getIssueIspFields()); // HOSTING 정보 저장 this.issueHostingService.modifyIssueHostingField(issue, issueForm.getIssueHostingFields()); // 첨부 파일 저장 // multipartFile 을 file Map List 객체로 변경한다. List<Map<String, Object>> convertFileMaps = this.convertMultipartFileToFile(multipartFiles); this.attachedFileService.addAttachedFile(convertFileMaps, issue, user.getAccount()); // 텍스트 에디터에 첨부한 파일을 이슈와 연결 this.checkNotHaveIssueIdAttachedFile(issue, issueForm); // 사용자 정의 필드 저장 this.issueCustomFieldValueService.modifyIssueCustomFieldValue(issue, issueForm.getIssueCustomFields()); // 이슈 이력 생성 this.issueHistoryService.addIssueHistory(issue, user, IssueHistoryType.ADD, null); // 이슈 위험 관리 생성 this.issueRiskService.addIssueRisk(issue, project.getWorkspace()); // 영속성 컨텍스트 비우기 this.clear(); // 이슈 생성, 삭제시 예약 이메일에 등록해놓는다. this.reservationIssueEmail(issue, EmailType.ISSUE_ADD); // 사용자 시스템 기능 사용 정보 수집 UserVo userVo = ConvertUtil.copyProperties(user, UserVo.class); log.info(ElasticSearchUtil.makeUserActiveHistoryMessage(userVo, ElasticSearchConstants.ISSUE_ADD)); return issue; } // 연관이슈를 생성한다. @Override @Transactional src/main/java/kr/wisestone/owl/web/controller/IssueController.java
@@ -66,6 +66,21 @@ return this.setSuccessMessage(resJsonData); } // 하위이슈 생성 @RequestMapping(value = "/issue/downIssueAdd", method = RequestMethod.POST) public @ResponseBody Map<String, Object> downIssueAdd(MultipartHttpServletRequest request) { Map<String, Object> resJsonData = new HashMap<>(); // 이슈 생성 Issue issue = this.issueService.addDownIssue(IssueForm.make(ConvertUtil.convertJsonToMap(request.getParameter(Constants.REQ_KEY_CONTENT))), request.getFiles("file")); // 버전 생성 this.issueService.addIssueVersion(issue.getId()); resJsonData.put(Constants.RES_KEY_CONTENTS, issue.getId()); //하위이슈 ID return this.setSuccessMessage(resJsonData); } // 이슈 조회 @RequestMapping(value = "/issue/find", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public src/main/webapp/scripts/app/issue/issue.js
@@ -33,7 +33,7 @@ loadController: ["$q", function ($q) { var deferred = $q.defer(); require([ 'issueListTimelineController', 'issueManagerController', 'issueListController', 'issueAddController', 'issueModifyController', 'issueDetailController', 'issueAddRelationController', 'issueImportExcelController', 'issueListTimelineController', 'issueManagerController', 'issueListController', 'issueAddController', 'issueModifyController', 'issueDetailController', 'issueAddRelationController', 'issueAddDownController', 'issueImportExcelController', 'chartLoader', 'jsTable', 'jsTree', 'tableColumnGenerator', 'treeColumnGenerator', 'modalFormAutoScroll', 'summerNote', 'summerNote-ko-KR', 'fullScroll', 'workflowService', 'priorityService', 'issueSearchService', 'issueTableConfigService', 'inputRegex', 'severityService', 'issueTypeService', 'issueTypeCustomFieldService', 'issueService', 'issueStatusService', 'emailTemplateService','issueUserService','issueDepartmentService','issueModifyUserController', 'issueModifyDepartmentController', 'customFieldService', 'issueSearchFieldKeyViewElement', 'issueSearchCustomFieldViewElement', 'tableUserImage', 'fullScroll', 'issueCommentService', 'detectIssueEditor', 'formSubmit', 'issueModifyStatusController', 'downIssueModifyStatusController', 'jsShortCut', src/main/webapp/scripts/app/issue/issueAdd.controller.js
@@ -399,21 +399,26 @@ $scope.vm.companyUrl = result[0].url; $scope.vm.companyMemo = result[0].memo; $scope.vm.ispName = ispFieldVo.name; $scope.vm.ispCode = ispFieldVo.code; $scope.vm.ispManager = ispFieldVo.manager; $scope.vm.ispTel = ispFieldVo.tel; $scope.vm.ispEmail = ispFieldVo.email; $scope.vm.ispUrl = ispFieldVo.url; $scope.vm.ispMemo = ispFieldVo.memo; $scope.vm.hostingName = hostingFieldVo.name; $scope.vm.hostingCode = hostingFieldVo.code; $scope.vm.hostingManager = hostingFieldVo.manager; $scope.vm.hostingTel = hostingFieldVo.tel; $scope.vm.hostingEmail = hostingFieldVo.email; $scope.vm.hostingUrl = hostingFieldVo.url; $scope.vm.hostingMemo = hostingFieldVo.memo; if (ispFieldVo != null){ $scope.vm.ispId = ispFieldVo.id; $scope.vm.ispName = ispFieldVo.name; $scope.vm.ispCode = ispFieldVo.code; $scope.vm.ispManager = ispFieldVo.manager; $scope.vm.ispTel = ispFieldVo.tel; $scope.vm.ispEmail = ispFieldVo.email; $scope.vm.ispUrl = ispFieldVo.url; $scope.vm.ispMemo = ispFieldVo.memo; } if (hostingFieldVo != null){ $scope.vm.hostingId = hostingFieldVo.id; $scope.vm.hostingName = hostingFieldVo.name; $scope.vm.hostingCode = hostingFieldVo.code; $scope.vm.hostingManager = hostingFieldVo.manager; $scope.vm.hostingTel = hostingFieldVo.tel; $scope.vm.hostingEmail = hostingFieldVo.email; $scope.vm.hostingUrl = hostingFieldVo.url; $scope.vm.hostingMemo = hostingFieldVo.memo; } }); //ISP정보 결과 값 Event 처리(set) @@ -472,7 +477,11 @@ ispId : (function () { // ISP 아이디 var ispId = -1; if ($scope.vm.form.issueIspFields != null && $scope.vm.form.issueIspFields.length > 0) { if ($scope.vm.form.issueCompanyFields != null && $scope.vm.form.issueCompanyFields.length > 0) { if ($scope.vm.form.issueCompanyFields[0].ispId != null){ ispId = $scope.vm.form.issueCompanyFields[0].ispId; } }else if ($scope.vm.form.issueIspFields != null && $scope.vm.form.issueIspFields.length > 0) { ispId = $scope.vm.form.issueIspFields[0].id; } return ispId; @@ -480,7 +489,11 @@ hostingId : (function () { // Hosting 아이디 var hostingId = -1; if ($scope.vm.form.issueHostingFields != null && $scope.vm.form.issueHostingFields.length > 0) { if ($scope.vm.form.issueCompanyFields != null && $scope.vm.form.issueCompanyFields.length > 0) { if ($scope.vm.form.issueCompanyFields[0].hostingId != null){ hostingId = $scope.vm.form.issueCompanyFields[0].hostingId; } }else if ($scope.vm.form.issueHostingFields != null && $scope.vm.form.issueHostingFields.length > 0) { hostingId = $scope.vm.form.issueHostingFields[0].id; } return hostingId; @@ -524,7 +537,6 @@ var companyField = $scope.vm.form.issueCompanyFields[0]; issueCompanyFields.push({ id : companyField.id, companyId : $scope.vm.companyId, name : $scope.vm.companyName, manager : $scope.vm.companyManager, @@ -540,11 +552,11 @@ issueIspFields : (function () { var issueIspFields = []; if ($scope.vm.form.issueIspFields != null && $scope.vm.form.issueIspFields.length > 0 ){ if ($scope.vm.form.issueCompanyFields[0].ispFieldVo != null || $scope.vm.form.issueIspFields != null && $scope.vm.form.issueIspFields.length > 0 ){ var ispField = $scope.vm.form.issueIspFields[0]; issueIspFields[0] = { id : ispField.id, issueIspFields.push({ ispId : $scope.vm.ispId, name : $scope.vm.ispName, code : $scope.vm.ispCode, @@ -553,7 +565,7 @@ email :$scope.vm.ispEmail, url :$scope.vm.ispUrl, memo : $scope.vm.ispMemo }; }); } @@ -562,12 +574,11 @@ issueHostingFields : (function () { var issueHostingFields = []; if ($scope.vm.form.issueHostingFields != null && $scope.vm.form.issueHostingFields.length > 0 ){ if ($scope.vm.form.issueCompanyFields[0].hostingFieldVo != null || $scope.vm.form.issueHostingFields != null && $scope.vm.form.issueHostingFields.length > 0 ){ var hostingField = $scope.vm.form.issueHostingFields[0]; issueHostingFields[0] = { id : hostingField.id, hostingId : $scope.vm.hostingId, name : $scope.vm.hostingName, code : $scope.vm.hostingCode, @@ -578,7 +589,6 @@ memo : $scope.vm.hostingMemo }; } return issueHostingFields; })(), src/main/webapp/scripts/app/issue/issueAddDown.controller.js
New file @@ -0,0 +1,919 @@ /** * Created by wisestone on 2017-12-15. */ 'use strict'; define([ 'app', 'angular' ], function (app, angular) { app.controller('issueAddDownController', ['$scope', '$rootScope', '$log', '$resourceProvider', '$uibModalInstance', '$uibModal', '$injector', '$controller', '$tableProvider', 'parameter' ,'SweetAlert', '$timeout', '$stateParams', '$q', 'Issue', 'User', 'AttachedFile', 'IssueType', 'Priority', 'Severity','IssueTypeCustomField', '$filter', '$state', function ($scope, $rootScope, $log, $resourceProvider, $uibModalInstance, $uibModal, $injector, $controller, $tableProvider, parameter, SweetAlert, $timeout, $stateParams, $q, Issue, User, AttachedFile, IssueType, Priority, Severity, IssueTypeCustomField, $filter, $state) { $scope.fn = { cancel : cancel, // 팝업 창 닫기 formSubmit : formSubmit, // 폼 전송 formCheck : formCheck, // 폼 체크 getUserListCallBack : getUserListCallBack, // 담당자 autocomplete 페이징 getProjectListCallBack : getProjectListCallBack, // 프로젝트 autocomplete 페이징 getIssueCompanyFieldListCallBack : getIssueCompanyFieldListCallBack, // 업체정보 autocomplete 페이징 getIssueDepartmentListCallBack : getIssueDepartmentListCallBack, // 담당자 -> 담당부서 autocomplete 페이징 getIssueIspFieldListCallBack : getIssueIspFieldListCallBack, // ISP정보 autocomplete 페이징 getIssueHostingFieldListCallBack : getIssueHostingFieldListCallBack, // 호스팅정보 autocomplete 페이징 getOptionColor : getOptionColor, // 우선순위, 중요도 색상으로 Select 태그 적용 onFileSelect : onFileSelect, // 파일 첨부 infiniteAddForm : infiniteAddForm, // 계속 생성 imageUpload : imageUpload, // 섬머노트 이미지 업로드 getIssueTypes : getIssueTypes, // 이슈 타입 목록 가져오기 getPriorities : getPriorities, // 우선순위 목록 가져오기 getSeverities : getSeverities, // 중요도 목록 가져오기 getIssueTypeCustomFields : getIssueTypeCustomFields, // 이슈 유형에 연결된 사용자 정의 필드 목록 가져오기 removeUploadFile : removeUploadFile, // 업로드하려는 특정 파일을 삭제 removeManager : removeManager, // 담당자 삭제 removeDepartment : removeDepartment, // 담당부서 삭제 setIssueTypeTemplate : setIssueTypeTemplate, // 이슈 유형 템플릿 적용하기 startExecute : startExecute, // 컨트롤 로딩시 처음으로 시작되는 함수 containsPartner : containsPartner, getPartners : getPartners, addDownIssue : addDownIssue, }; $scope.vm = { partnerVos : "", form : { title : "", // 제목 description : "", // 내용 issueStatusId : "", // 이슈 상태 projects : [], // 프로젝트 issueCompanyFields : [], // 업체정보 issueIspFields : [], // ISP 정보 issueHostingFields : [], // 호스팅정보 issueTypeId : "", // 이슈 유형 아이디 priorityId : "", // 우선순위 아이디 severityId : "", // 중요도 아이디 users : [], // 담당자 departments : [], // 딤당부서 files : [], // 업로드 파일 attachedFiles : [], // 섬머노트로 파일 업로드를 할 경우 서버에서 pk를 따고 issue id와 연동 작업이 필요하다. startCompleteDateRange : "", // 시작일 ~ 종료일 detectingDateRange : "", // 탐지일 issueCustomFields : [], // 이슈에서 사용되는 사용자 정의 필드 removeFiles : [], // 삭제 파일 }, id : parameter.id, infiniteAdd : false, // 연속 생성 projectName : "", // 프로젝트 명 검색 userName : "", // 사용자 검색 departmentName : "", // 부서명 검색 companyId : -1, // 부서 ID companyName : "", // 업체명 검색 companyManager : "", // 업체 담당자 companyTel : "", // 업체 전화번호 companyEmail : "", // 업체 이메일 companyUrl : "", // 업체 url companyMemo : "", // 업체 비고 ispId : -1, // ISP ID ispName : "", // ISP 명 ispCode : "", // ISP 코드 ispManager : "", // ISP 담당자 ispTel : "", // ISP 전화번호 ispEmail : "", // ISP 이메일 ispUrl : "", // ISP url ispMemo : "", // ISP 비고 hostingId : -1, // 호스팅 ID hostingName : "", // 호스팅명 검색 hostingManager : "", // 호스팅 담당자 hostingTel : "", // 호스팅 전화번호 hostingCode : "", // 호스팅 코드 hostingEmail : "", // 호스팅 이메일 hostingUrl : "", // 호스팅 url hostingMemo :"", // 호스팅 비고 autoCompletePage : { user : { page : 0, totalPage : 0 }, project : { page : 0, totalPage : 0 }, companyField : { page : 0, totalPage : 0 }, department : { page : 0, totalPage : 0 }, ispField : { page : 0, totalPage : 0 }, hostingField : { page : 0, totalPage : 0 } }, summerNote : { editable : null, editor : null }, issueTypes : [], // 이슈 유형 전체 목록 priorities : [], // 우선순위 정보 severities : [], // 중요도 정보 fileTableConfigs : [], // 파일 업로드 정보 테이블 }; angular.extend(this, $controller('autoCompleteController', {$scope : $scope, $injector : $injector})); function getStartProjectListCallback(result){ // 프로젝트 autocomplete page 업데이트 $scope.vm.autoCompletePage.project.totalPage = result.data.page.totalPage; var projectVo = result.data.data[0]; $scope.vm.form.projects.push(projectVo); } // 프로젝트가 변경되면 담당자 초기화 $scope.$watch("vm.form.projects", function (newValue, oldValue) { if (angular.isDefined(newValue)) { if (newValue.length < 1) { $scope.vm.form.users = []; } else { // 이슈 유형에 연결된 사용자 정의 필드 가져오기 $scope.fn.getIssueTypeCustomFields(); } } }); $scope.$watch("vm.form.issueTypeId", function (newValue, oldValue) { $scope.vm.partnerVos = $scope.fn.getPartners(); }); // 섬머노트 이미지 업로드 function imageUpload($files) { var listFiles = []; var uploadFileSize = 0; for (var count in $files) { var $file = $files[count]; if (typeof ($file) == "object") { uploadFileSize += $file.size; // 파일당 용량 제한 10MB if ($file.size > $rootScope.fileByte.image) { SweetAlert.error($filter("translate")("issue.capacityExceededImageFile"), $filter("translate")("issue.attachedOnlyImageFiles10mb")); // "이미지 파일 용량 초과", "10MB 이하의 이미지 파일만 첨부가 가능합니다." listFiles = []; break; } // 여러건의 파일을 한번에 업로드할 경우 제한 300MB if (uploadFileSize > $rootScope.fileByte.file) { SweetAlert.error($filter("translate")("issue.capacityExceededImageFile"), $filter("translate")("issue.attachedMultipleImageFiles100mb")); // "이미지 파일 용량 초과", "여러 건의 이미지를 한번에 첨부할 경우 100MB 이하까지만 첨부가 가능합니다." listFiles = []; break; } if (!$rootScope.checkImageType($file)) { SweetAlert.error($filter("translate")("issue.limitImageFile"), $filter("translate")("issue.canBeUploadedOnlyImageFiles")); // "이미지 파일 제한", "이미지 파일만 업로드 가능합니다. - bmp, jpg, jpeg, png, tif" listFiles = []; break; } if (!angular.isDefined($file.name)) { var fileType = $file.type.split("/"); var imageType = ""; if (fileType[0] === "image") { imageType = "." + fileType[1]; } $file.name = new Date().getTime() + imageType; } else { if ($file.name.indexOf(';') !== -1) { SweetAlert.error($filter("translate")("issue.nameErrorImageFile"), $filter("translate")("issue.cannotUploadFileNameSpecialCharacters")); // "이미지 파일명 오류", "파일명에 특수문자(;)가 들어가면 업로드 할 수 없습니다." listFiles = []; break; } } listFiles.push($file); } } // 파일 업로드 검증을 거친 파일이 1개이상 존재할 경우에만 실행 if (listFiles.length > 0) { AttachedFile.add({ method : "POST", file : listFiles, // data 속성으로 별도의 데이터 전송 fields : { content : { workspaceId : $rootScope.user.lastWorkspaceId } }, fileFormDataName : "file" }) .then(function (result) { if (result.data.message.status === "success") { angular.forEach(result.data.attachedFiles, function (fileInfo) { $scope.vm.summerNote.editor.summernote("editor.insertImage", fileInfo.path); $scope.vm.form.attachedFiles.push(fileInfo); }); } else { SweetAlert.error($filter("translate")("issue.errorFileUpload"), result.data.message.message); // 파일 업로드 오류 } }); } } // 연속으로 이슈를 등록할 때 입력 폼 초기화 function infiniteAddForm() { $scope.vm.form.title = ""; $scope.vm.form.description = ""; $scope.vm.form.files = []; $scope.vm.form.attachedFiles = []; // 이슈 유형 템플릿 적용하기 $scope.fn.setIssueTypeTemplate(); $(".modal-body").animate({ scrollTop : 0 }, 500); $timeout(function () { $("[name='title']").trigger("focus") }, 100); } // 파일 업로드에 사용 function onFileSelect($files) { var uploadFileSize = 0; // 이전에 첨부한 파일이 있을 경우 전체 업로드 용량에 포함 angular.forEach($scope.vm.form.files, function ($file) { uploadFileSize += $file.size; }); for (var count in $files) { var $file = $files[count]; if (typeof ($file) == "object") { uploadFileSize += $file.size; // 파일당 용량 제한 300MB if (($file.size > $rootScope.fileByte.file) || (uploadFileSize > $rootScope.fileByte.file)) { SweetAlert.error($filter("translate")("issue.attachmentCapacityExceeded"), $filter("translate")("issue.canAttachFileUpTo100mb")); // "첨부 파일 용량 초과", "100MB 이하까지만 파일 첨부가 가능합니다." break; } // 파일을 업로드할 때 파일 유형을 확인해주는 기능 - 허용되지 않은 확장자일 때는 첨부 금지 if (!$rootScope.checkFileType($file)) { SweetAlert.error($filter("translate")("issue.limitAttachmentExtensions"), $filter("translate")("issue.notAllowedAttachment")); // "첨부 파일 확장자 제한", "첨부가 허용되지 않는 파일입니다." break; } if ($file.name.indexOf(';') !== -1) { SweetAlert.error($filter("translate")("issue.nameErrorAttachment"), $filter("translate")("issue.cannotUploadFileNameSpecialCharacters")); // "첨부 파일명 오류", "파일명에 특수문자(;)가 들어가면 업로드 할 수 없습니다." break; } $file.index = count; $scope.vm.form.files.push($file); } } } // 셀렉트 박스에서 중요도, 우선순위 색상 표시 function getOptionColor(list, key) { var color = "#353535"; // 기본색은 검은색. for (var count in list) { if (String(list[count].id) === key) { color = list[count].color; break; } } return color; } // 담당자 삭제 function removeManager(index) { $scope.vm.form.departments.splice(index, 1); } // 담당부서 삭제 function removeDepartment(index) { $scope.vm.form.departments.splice(index, 1); } // 업로드 파일 삭제 function removeUploadFile(index) { $scope.vm.form.files.splice(index, 1); angular.forEach($scope.vm.form.files, function (file, index) { file.index = index; }); } // 업체/ISP/호스팅 이름이 포함 여부 확인 function containsPartner(name) { var result = false; if ($scope.vm.partnerVos != null) { $scope.vm.partnerVos.forEach(function (partnerVo) { if (name === partnerVo.name) { result = true; } }); } return result; } // 담당자 autocomplete page 업데이트트 function getUserListCallBack(result) { $scope.vm.autoCompletePage.user.totalPage = result.data.page.totalPage; } // 프로젝트 autocomplete page 업데이트 function getProjectListCallBack(result) { $scope.vm.autoCompletePage.project.totalPage = result.data.page.totalPage; } // 업체정보 autocomplete page 업데이트 function getIssueCompanyFieldListCallBack(result) { $scope.vm.autoCompletePage.companyField.totalPage = result.data.page.totalPage; } // 부서정보 autocomplete page 업데이트 function getIssueDepartmentListCallBack(result) { $scope.vm.autoCompletePage.department.totalPage = result.data.page.totalPage; } // ISP정보 autocomplete page 업데이트 function getIssueIspFieldListCallBack(result) { $scope.vm.autoCompletePage.ispField.totalPage = result.data.page.totalPage; } // 호스팅정보 autocomplete page 업데이트 function getIssueHostingFieldListCallBack(result) { $scope.vm.autoCompletePage.hostingField.totalPage = result.data.page.totalPage; } // 폼 체크 function formCheck(formInvalid) { if (formInvalid) { return true; } return false; } // 업체정보 결과 값 Event 처리(set) $scope.$on("companyFieldEvent", function (event, result) { var ispFieldVo = result[0].ispFieldVo; var hostingFieldVo = result[0].hostingFieldVo; $scope.vm.companyId = result[0].id; $scope.vm.companyName = result[0].name; $scope.vm.companyManager = result[0].manager; $scope.vm.companyTel = result[0].tel; $scope.vm.companyEmail = result[0].email; $scope.vm.companyUrl = result[0].url; $scope.vm.companyMemo = result[0].memo; $scope.vm.ispId = ispFieldVo.id; $scope.vm.ispName = ispFieldVo.name; $scope.vm.ispCode = ispFieldVo.code; $scope.vm.ispManager = ispFieldVo.manager; $scope.vm.ispTel = ispFieldVo.tel; $scope.vm.ispEmail = ispFieldVo.email; $scope.vm.ispUrl = ispFieldVo.url; $scope.vm.ispMemo = ispFieldVo.memo; $scope.vm.hostingId = hostingFieldVo.id; $scope.vm.hostingName = hostingFieldVo.name; $scope.vm.hostingCode = hostingFieldVo.code; $scope.vm.hostingManager = hostingFieldVo.manager; $scope.vm.hostingTel = hostingFieldVo.tel; $scope.vm.hostingEmail = hostingFieldVo.email; $scope.vm.hostingUrl = hostingFieldVo.url; $scope.vm.hostingMemo = hostingFieldVo.memo; }); // ISP정보 결과 값 Event 처리(set) $scope.$on("ispFieldEvent", function (event, result) { $scope.vm.ispId = result[0].id; $scope.vm.ispName = result[0].name; $scope.vm.ispCode = result[0].code; $scope.vm.ispManager = result[0].manager; $scope.vm.ispTel = result[0].tel; $scope.vm.ispEmail = result[0].email; $scope.vm.ispUrl = result[0].url; $scope.vm.ispMemo = result[0].memo; }); // 호스팅정보 결과 값 Event 처리(set) $scope.$on("hostingFieldEvent", function (event, result) { $scope.vm.hostingId = result[0].id; $scope.vm.hostingName = result[0].name; $scope.vm.hostingCode = result[0].code; $scope.vm.hostingManager = result[0].manager; $scope.vm.hostingTel = result[0].tel; $scope.vm.hostingEmail = result[0].email; $scope.vm.hostingUrl = result[0].url; $scope.vm.hostingMemo = result[0].memo; }); // 폼 전송 function formSubmit() { $rootScope.spinner = true; var content = { //id : parameter.id, title : $rootScope.preventXss($scope.vm.form.title), // 제목 description : $rootScope.preventXss($scope.vm.form.description), // 내용 companyName : $scope.vm.companyName, companyManager : $scope.vm.companyManager, companyTel : $scope.vm.companyTel, companyEmail :$scope.vm.companyEmail, companyUrl : $scope.vm.companyUrl, companyMemo : $scope.vm.companyMemo, ispName : $scope.vm.ispName, ispCode : $scope.vm.ispCode, ispManager : $scope.vm.ispManager, ispTel : $scope.vm.ispTel, ispEmail : $scope.vm.ispEmail, ispUrl : $scope.vm.ispUrl, ispMemo : $scope.vm.ispMemo, hostingName : $scope.vm.hostingName, hostingCode : $scope.vm.hostingCode, hostingManager : $scope.vm.hostingManager, hostingTel : $scope.vm.hostingTel, hostingEmail : $scope.vm.hostingEmail, hostingUrl : $scope.vm.hostingUrl, hostingMemo : $scope.vm.hostingMemo, projectId : (function () { // 프로젝트 아이디 var projectId = ""; if ($scope.vm.form.projects.length > 0) { projectId = $scope.vm.form.projects[0].id; } return projectId; })(), issueTypeId : $scope.vm.form.issueTypeId, // 이슈 유형 아이디 priorityId : $scope.vm.form.priorityId, // 우선순위 아이디 severityId : $scope.vm.form.severityId, // 중요도 아이디 issueStatusId : $scope.vm.form.issueStatusId, // 이슈 상태 아이디 companyId : (function () { var companyId = -1; if ($scope.vm.form.issueCompanyFields.length > 0) { companyId = $scope.vm.form.issueCompanyFields[0].id; } return companyId; }), ispId : (function () { var ispId = -1; if ($scope.vm.form.issueIspFields.length > 0) { ispId = $scope.vm.form.issueIspFields[0].id; } return ispId; }), hostingId : (function () { var hostingId = -1; if ($scope.vm.form.issueHostingFields != null && $scope.vm.form.issueHostingFields.length > 0) { hostingId = $scope.vm.form.issueHostingFields[0].id; } return hostingId; }), userIds : (function () { var userIds = []; angular.forEach($scope.vm.form.users, function (user) { userIds.push(user.id); }); return userIds; })(), departmentIds : (function () { var departmentIds = []; angular.forEach($scope.vm.form.departments, function (department) { departmentIds.push(department.id); }); return departmentIds; })(), attachedFileIds : (function () { var attachedFileIds = []; angular.forEach($scope.vm.form.attachedFiles, function (attachedFile) { if ($scope.vm.form.description.indexOf(attachedFile.path) !== -1) { attachedFileIds.push(attachedFile.id); } }); return attachedFileIds; })(), issueCompanyFields : (function () { var issueCompanyFields = []; if ($scope.vm.form.issueCompanyFields != null && $scope.vm.form.issueCompanyFields.length > 0 ){ var companyField = $scope.vm.form.issueCompanyFields[0]; issueCompanyFields.push({ id : companyField.id, companyId : $scope.vm.companyId, name : $scope.vm.companyName, manager : $scope.vm.companyManager, tel : $scope.vm.companyTel, email :$scope.vm.companyEmail, url :$scope.vm.companyUrl, memo : $scope.vm.companyMemo }); } return issueCompanyFields; })(), issueIspFields : (function () { var issueIspFields = []; if ($scope.vm.form.issueIspFields != null && $scope.vm.form.issueIspFields.length > 0 ){ var ispField = $scope.vm.form.issueIspFields[0]; issueIspFields.push({ id : ispField.id, ispId : $scope.vm.ispId, code : $scope.vm.ispCode, name : $scope.vm.ispName, manager : $scope.vm.ispManager, tel : $scope.vm.ispTel, email :$scope.vm.ispEmail, url :$scope.vm.ispUrl, memo : $scope.vm.ispMemo }); } return issueIspFields; })(), issueHostingFields : (function () { var issueHostingFields = []; if ($scope.vm.form.issueHostingFields != null && $scope.vm.form.issueHostingFields.length > 0 ){ var hostingField = $scope.vm.form.issueHostingFields[0]; issueHostingFields.push({ id : hostingField.id, hostingId : $scope.vm.hostingId, name : $scope.vm.hostingName, code : $scope.vm.hostingCode, manager : $scope.vm.hostingManager, tel : $scope.vm.hostingTel, email :$scope.vm.hostingEmail, url :$scope.vm.hostingUrl, memo : $scope.vm.hostingMemo }); } return issueHostingFields; })(), removeFiles : $scope.vm.form.removeFiles, startCompleteDateRange : $scope.vm.form.startCompleteDateRange, issueCustomFields : (function () { // 이슈에서 사용되는 사용자 정의 필드 var issueCustomFields = []; angular.forEach($scope.vm.form.issueCustomFields, function (issueCustomField) { var useValues = []; if (angular.isArray(issueCustomField.useValues)) { angular.forEach(issueCustomField.useValues, function (useValue) { useValues.push(useValue.value); }); } else { useValues.push(issueCustomField.useValues); } // useValues 를 배열로 변환한다. var temp = angular.copy(issueCustomField); temp.useValues = useValues; issueCustomFields.push(temp); }); return issueCustomFields; })() }; Issue.downAdd({ method : "POST", file : (function () { var files = []; angular.forEach($scope.vm.form.files, function (file) { if (angular.isUndefined(file.id)) { files.push(file); } }); return files; })(), // data 속성으로 별도의 데이터 전송 fields : { content : content }, fileFormDataName : "file" }).then(function (result) { if (result.data.message.status === "success") { $scope.fn.addDownIssue(result.data.data); $scope.fn.cancel(); // 이슈 상세 화면 요청 $rootScope.$broadcast("getIssueDetail", { id : parameter.id }); } else { SweetAlert.error($filter("translate")("issue.failedIssueModify"), result.data.message.message); // 이슈 수정 실패 } $rootScope.spinner = false; }); } // 연관 이슈 추가 function addDownIssue(downId) { /*if ($scope.vm.issueName.length == 0 || $scope.vm.form.issues.length == 0 || $scope.vm.issueName != $scope.vm.form.issues[0].title) { SweetAlert.error($filter("translate")("issue.errorSelectRelationIssue"), ""); return; }*/ var contents = { //relationIssueType : $scope.vm.form.relationIssueTypeId, // issueId : $rootScope.currentDetailIssueId, issueId : parameter.id, id : downId, parentIssueId : parameter.id }; Issue.modifyParentIssue($resourceProvider.getContent( contents, $resourceProvider.getPageContent(0, 10))).then(function (result) { if (result.data.message.status === "success") { // 이슈 상세 화면 요청 $rootScope.$broadcast("getIssueDetail", { id : parameter.id }); } else { SweetAlert.error($filter("translate")("issue.failedToIssueAddIssueDown"), result.data.message.message); // "연관일감 생성 실패" } }); } // 팝업 창 닫기 function cancel() { SweetAlert.close(); // 알림 창 닫기 $rootScope.$broadcast("closeLayer"); // 팝업이 열리고 나서 js-multi, js-single 등에서 body 이벤트가 날아가는 현상 수정 $uibModalInstance.dismiss('cancel'); $(document).unbind("keydown"); // 단축키 이벤트 제거 } // 이슈 유형 목록 function getIssueTypes() { var deferred = $q.defer(); IssueType.find($resourceProvider.getContent({}, $resourceProvider.getPageContent(0, 1000))).then(function (result) { if (result.data.message.status === "success") { $scope.vm.issueTypes = result.data.data; } else { SweetAlert.swal($filter("translate")("issue.failedToIssueTypeListLookup"), result.data.message.message, "error"); // 이슈 타입 목록 조회 실패 } deferred.resolve(result.data.data); }); return deferred.promise; } // 우선순위 목록 function getPriorities() { var deferred = $q.defer(); Priority.find($resourceProvider.getContent({}, $resourceProvider.getPageContent(0, 1000))).then(function (result) { if (result.data.message.status === "success") { $scope.vm.priorities = result.data.data; } else { SweetAlert.swal($filter("translate")("issue.failedToPriorityListLookup"), result.data.message.message, "error"); // 우선순위 목록 조회 실패 } deferred.resolve(result.data.data); }); return deferred.promise; } // 중요도 목록 function getSeverities() { var deferred = $q.defer(); Severity.find($resourceProvider.getContent({}, $resourceProvider.getPageContent(0, 1000))).then(function (result) { if (result.data.message.status === "success") { $scope.vm.severities = result.data.data; } else { SweetAlert.swal($filter("translate")("issue.failedToCriticalListLookup"), result.data.message.message, "error"); // 중요도 목록 조회 실패 } deferred.resolve(result.data.data); }); return deferred.promise; } // 이슈 유형에 연결된 사용자 정의 필드 function getIssueTypeCustomFields() { $scope.vm.form.issueCustomFields = []; // 이슈 타입 아이디나 프로젝트 아이디가 없으면 통신을 하지 않는다. if (!$rootScope.isDefined($scope.vm.form.issueTypeId) || $scope.vm.form.projects.length < 1) { return; } // 이슈 유형 템플릿 적용하기 $scope.fn.setIssueTypeTemplate(); var deferred = $q.defer(); IssueTypeCustomField.find($resourceProvider.getContent({projectId : $scope.vm.form.projects[0].id, issueTypeId : $scope.vm.form.issueTypeId}, $resourceProvider.getPageContent(0, 1000))).then(function (result) { if (result.data.message.status === "success") { $scope.vm.form.issueCustomFields = []; angular.forEach(result.data.data, function (issueTypeCustomField) { switch (issueTypeCustomField.customFieldVo.customFieldType) { case "INPUT" : case "NUMBER" : case "DATETIME" : case "IP_ADDRESS" : case "EMAIL" : case "SITE" : case "TEL" : issueTypeCustomField.useValues = issueTypeCustomField.customFieldVo.defaultValue; break; case "SINGLE_SELECT" : issueTypeCustomField.useValues = issueTypeCustomField.customFieldVo.defaultValue.replace("#", ""); break; case "MULTI_SELECT" : issueTypeCustomField.useValues = []; angular.forEach(issueTypeCustomField.customFieldVo.defaultValue.split("#"), function (value) { if ($rootScope.isDefined(value)) { issueTypeCustomField.useValues.push({ value : value }); } }); break; } $scope.vm.form.issueCustomFields.push(issueTypeCustomField); }); } else { SweetAlert.swal($filter("translate")("issue.failedToUserDefinedFieldListAssociatedLookup"), result.data.message.message, "error"); // 이슈 유형에 연결된 사용자 정의 필드 목록 조회 실패 } deferred.resolve(result.data.data); }); return deferred.promise; } // 이슈 타입에 있는 템플릿을 적용한다. function setIssueTypeTemplate() { for (var count in $scope.vm.issueTypes) { var issueType = $scope.vm.issueTypes[count]; if ($scope.vm.form.issueTypeId === String(issueType.id)) { // 템플릿이 작성되어 있는지 확인 if ($rootScope.isDefined(issueType.description)) { // 이슈 내용이 작성되어 있지 않으면 바로 템플릿 적용 if (!$rootScope.isDefined($scope.vm.form.description)) { $scope.vm.form.description = issueType.description; } else { // 이미 내용이 작성되어 있으면 확인 후 적용 SweetAlert.swal({ title : $filter("translate")("issue.applyTemplate"), // 템플릿 적용하기 text : $filter("translate")("issue.issueContentIsWrittenApplyTheTemplate"), // 이슈 내용이 작성되어 있습니다. 템플릿을 적용하겠습니까? 템플릿이 적용되면 이미 작성된 내용이 사라집니다. type : "warning", showCancelButton : true, confirmButtonColor : "#DD6B55", confirmButtonText : $filter("translate")("issue.applyTemplate"), // 템플릿 적용하기 cancelButtonText : $filter("translate")("common.cancel"), // 취소 closeOnConfirm : false, closeOnCancel : true }, function (isConfirm) { SweetAlert.close(); if (isConfirm) { $scope.vm.form.description = issueType.description; } }); } } break; } } } function getPartners() { if($scope.vm.form.issueTypeId === ""){ $scope.vm.form.issueTypeId = $rootScope.issueTypeMenu.id } var content = { issueTypeId : $scope.vm.form.issueTypeId, }; Issue.findPartners($resourceProvider.getContent( content, $resourceProvider.getPageContent(0, 1))).then(function (result) { if (result.data.message.status === "success") { $scope.vm.partnerVos = result.data.data; } }); } // 최초 실행 function startExecute() { var promises = { getIssueTypes : $scope.fn.getIssueTypes(), getPriorities : $scope.fn.getPriorities(), getSeverities : $scope.fn.getSeverities(), getPartners : $scope.fn.getPartners() }; $q.all(promises).then(function (results) { // 현재 프로젝트 설정 if ($rootScope.workProject != null && $rootScope.workProject.id > -1) { $scope.vm.projectName = $rootScope.workProject.name; $scope.vm.form.projects = []; $scope.vm.form.projects.push($rootScope.workProject); } // 현재 이슈타입 유형 설정 var id = $rootScope.getCurrentIssueTypeId(); if (id != null) { $scope.vm.form.issueTypeId = id.toString(); } $log.debug("promises 결과 ", results); }); } $scope.fn.startExecute(); }]); }); src/main/webapp/scripts/app/issue/issueAddRelation.controller.js
@@ -406,6 +406,7 @@ $scope.vm.companyUrl = result[0].url; $scope.vm.companyMemo = result[0].memo; $scope.vm.ispId = ispFieldVo.id; $scope.vm.ispName = ispFieldVo.name; $scope.vm.ispCode = ispFieldVo.code; $scope.vm.ispManager = ispFieldVo.manager; @@ -414,6 +415,7 @@ $scope.vm.ispUrl = ispFieldVo.url; $scope.vm.ispMemo = ispFieldVo.memo; $scope.vm.hostingId = hostingFieldVo.id; $scope.vm.hostingName = hostingFieldVo.name; $scope.vm.hostingCode = hostingFieldVo.code; $scope.vm.hostingManager = hostingFieldVo.manager; src/main/webapp/scripts/app/issue/issueList.controller.js
@@ -24,6 +24,7 @@ add : add, // 이슈 생성 modify : modify, // 이슈 수정 addRelationIssueForm : addRelationIssueForm, // 연관 이슈 추가 addDownIssueForm : addDownIssueForm, // 하위 이슈 추가 modifyMultiIssueStatus : modifyMultiIssueStatus, // 이슈 다중 상태 변경 removes : removes, // 이슈 삭제 addIssueTableConfig : addIssueTableConfig, // 이슈 목록 테이블 설정 @@ -801,6 +802,22 @@ }); } function addDownIssueForm(id) { $uibModal.open({ templateUrl : 'views/issue/issueAddDown.html', size : "lg", controller : 'issueAddDownController', backdrop : 'static', resolve : { parameter : function () { return { id : id, }; } } }); } // 이슈 삭제 function removes() { var removeIds = []; src/main/webapp/scripts/app/issue/issueModify.controller.js
@@ -382,6 +382,7 @@ $scope.vm.companyUrl = result[0].url; $scope.vm.companyMemo = result[0].memo; $scope.vm.ispId = ispFieldVo.id; $scope.vm.ispName = ispFieldVo.name; $scope.vm.ispCode = ispFieldVo.code; $scope.vm.ispManager = ispFieldVo.manager; @@ -390,6 +391,7 @@ $scope.vm.ispUrl = ispFieldVo.url; $scope.vm.ispMemo = ispFieldVo.memo; $scope.vm.hostingId = hostingFieldVo.id; $scope.vm.hostingName = hostingFieldVo.name; $scope.vm.hostingCode = hostingFieldVo.code; $scope.vm.hostingManager = hostingFieldVo.manager; src/main/webapp/scripts/components/issue/issue.service.js
@@ -50,7 +50,6 @@ return response; }); }, relAdd : function (conditions) { conditions.url = "issue/relIssueAdd"; return $upload.upload(conditions).progress(function (evt) { @@ -62,6 +61,17 @@ return response; }); }, downAdd : function (conditions) { conditions.url = "issue/downIssueAdd"; return $upload.upload(conditions).progress(function (evt) { // 파일 업로드 진행율을 표시해준다. fileUploadProgress(evt); }).then(function (response) { $log.debug("이슈 생성 결과 : ", response); return response; }); }, modifyParentIssue : function (conditions) { return $http.post("issue/modifyParentIssue", conditions).then(function (response) { $log.debug("상위 일감 수정 결과 : ", response); src/main/webapp/scripts/main.js
@@ -179,7 +179,8 @@ /* 이슈 */ 'issueRoute' : 'app/issue/issue', // 이슈에 관련된 route 정보 'issueAddController' : 'app/issue/issueAdd.controller', // 이슈 생성 컨트롤러 'issueAddRelationController' : 'app/issue/issueAddRelation.controller', // 이슈 생성 컨트롤러 'issueAddRelationController' : 'app/issue/issueAddRelation.controller', // 연관이슈 생성 컨트롤러 'issueAddDownController' : 'app/issue/issueAddDown.controller', // 하위이슈 생성 컨트롤러 'issueModifyController' : 'app/issue/issueModify.controller', // 이슈 수정 컨트롤러 'issueDetailController' : 'app/issue/issueDetail.controller', // 이슈 상세 컨트롤러 'issueListController' : 'app/issue/issueList.controller', // 이슈 목록 컨트롤러 @@ -508,7 +509,8 @@ 'autoCompleteController', 'userInviteController', 'issueAddController', // 이슈 만들기에서 사용 'issueAddRelationController', // 이슈 만들기에서 사용 'issueAddRelationController', // 연관 이슈 만들기에서 사용 'issueAddDownController', //하위 이슈 만들기에서 사용 'issueService', // 이슈 만들기에서 사용 'issueTypeService', // 이슈 만들기에서 사용 'priorityService', // 이슈 만들기에서 사용 src/main/webapp/views/issue/issueAddDown.html
New file @@ -0,0 +1,767 @@ <div class="formModal"> <div class="modal-header faded smaller"> <div class="modal-title"> <strong>하위 이슈 추가</strong> </div> <button aria-label="Close" class="close" type="button" ng-click="fn.cancel()"> <span aria-hidden="true"> ×</span> </button> </div> <div class="modal-body"> <form role="form" name="issueAddDownForm"> <div class="form-group mb10"> <label for="issueAddDownForm1" class="issue-label"><span translate="issue.issueTitle">이슈 제목</span> <code class="highlighter-rouge">*</code></label> <input id="issueAddDownForm1" class="form-control input-sm" ng-model="vm.form.title" name="title" required kr-input maxlength="300" autocomplete="off" autofocus owl-auto-focus> <small class="help-block form-text text-danger" ng-if="issueAddDownForm.title.$touched && issueAddDownForm.title.$error.required" translate="issue.requireIssueTitle">이슈 제목을 입력하세요. </small> </div> <div class="row"> <div class="col-lg-4"> <div class="form-group mb10"> <label class="issue-label"> <span translate="common.project">프로젝트</span> <code class="highlighter-rouge">*</code></label> <js-autocomplete-single data-input-name="project" selected-model="vm.form.projects" search="vm.projectName" source="fn.getProjectList(vm.projectName, vm.form.projects, vm.autoCompletePage.project.page, fn.getProjectListCallBack)" page="vm.autoCompletePage.project.page" total-page="vm.autoCompletePage.project.totalPage" input-disabled="vm.form.projects != null ? vm.form.projects.length > 0 : false" translation-texts="{ empty : 'common.emptyProject' }" extra-settings="{ displayProp : 'name' , idProp : 'id', imageable : false, imagePathProp : '', type : '', maxlength : 200, autoResize : false, stopRemoveBodyEvent : true }"></js-autocomplete-single> </div> </div> <div class="col-lg-8 bdl1"> <div class="row"> <div class="col-md-4"> <div class="form-group mb10"> <label for="issueAddDownForm4" class="issue-label"> <span translate="issue.issueType">이슈 타입</span> <code class="highlighter-rouge">*</code></label> <select id="issueAddDownForm4" name="issueType" class="form-control input-sm issue-select-label" ng-model="vm.form.issueTypeId" ng-change="fn.getIssueTypeCustomFields()" ng-style="{ 'color' : fn.getOptionColor(vm.issueTypes, vm.form.issueTypeId) }" required disabled> <option value="" translate="common.selectTarget" ng-style="{ 'color' : '#353535' }"><span translate="common.selectTarget">대상 선택</span> </option> <option ng-repeat="issueType in vm.issueTypes" ng-style="{ 'color' : issueType.color, 'font-weight': 600 }" value="{{issueType.id}}">● {{issueType.name}} </option> </select> </div> </div> <div class="col-md-4"> <div class="form-group mb10"> <label for="issueAddDownForm2" class="issue-label"> <span translate="common.priority">우선 순위</span> <code class="highlighter-rouge">*</code> </label> <select id="issueAddDownForm2" name="priority" class="form-control input-sm issue-select-label" ng-model="vm.form.priorityId" ng-style="{ 'color' : fn.getOptionColor(vm.priorities, vm.form.priorityId) }" required> <option value="" translate="common.selectTarget" ng-style="{ 'color' : '#353535' }"> <span translate="common.selectTarget">대상 선택</span> </option> <option ng-repeat="priority in vm.priorities" ng-style="{ 'color' : priority.color, 'font-weight': 600 }" value="{{priority.id}}" translate="{{priority.name}}"> </option> </select> </div> </div> <div class="col-md-4"> <div class="form-group mb10"> <label for="issueAddDownForm3" class="issue-label"> <span translate="common.importance">중요도</span> <code class="highlighter-rouge">*</code></label> <select id="issueAddDownForm3" name="severity" class="form-control input-sm issue-select-label" ng-model="vm.form.severityId" ng-style="{ 'color' : fn.getOptionColor(vm.severities, vm.form.severityId) }" required> <option value="" translate="common.selectTarget" ng-style="{ color : '#353535' }"> <span translate="common.selectTarget">대상 선택</span> </option> <option ng-repeat="severity in vm.severities" ng-style="{ color : severity.color, 'font-weight': 600 }" value="{{severity.id}}" translate="{{severity.name}}"> </option> </select> </div> </div> </div> </div> </div> <hr> <div class="row"> <div class="col-lg-4"> <div class="form-group mb10"> <label class="issue-label"> <span translate="common.assigneeTeam">담당부서</span> </label> <js-autocomplete-multi data-input-name="departments" selected-model="vm.form.departments" search="vm.departmentName" source="fn.getIssueDepartmentList(vm.form.issueTypeId, vm.departmentName, vm.form.departments)" input-disabled="false" translation-texts="{ count : 'common.userNum', empty : 'common.emptyProjectDepartment'}" extra-settings="{ displayProp : 'byName' , idProp : 'id', imageable : false, maxlength : 100, autoResize : true}"></js-autocomplete-multi> <div class="select3-selection__choicediv mt-10"> <span class="select3-selection__choice" ng-repeat="department in vm.form.departments"> <span>{{department.byName}}</span> <span class="select3-selection__choice__remove" ng-click="fn.removeDepartment($index)">×</span> </span> </div> </div> </div> <div class="col-lg-4"> <div class="form-group mb10"> <label for="issueAddDownForm5" class="issue-label"> <span translate="common.period">기간</span></label> <input id="issueAddDownForm5" tabindex="-1" type="text" readonly class="form-control cursor" placeholder="{{'issue.clickToSelectDate' | translate}}" ng-model="vm.form.startCompleteDateRange" modal-form-auto-scroll date-format="YYYY-MM-DD" parent-el="'#createdWidget'" date-range-picker> <div class="row"> <div class="col-xs-12"> <div id="createdWidget" class="bootstrap-datepicker"></div> </div> </div> </div> </div> <div class="col-lg-4"> <div class="form-group mgb5"> <label class="issue-label"><span translate="common.attachFiles">파일 첨부</span></label> <div class="filebox input-group"> <input class="upload-name form-control" placeholder="{{'users.pleaseSelectFile' | translate}}" tabindex="-1" disabled="disabled"> <label for="uploadFileField"><span translate="common.selectFile">파일선택</span></label> <input id="uploadFileField" tabindex="-1" type="file" class="form-control" multiple ng-file-select="fn.onFileSelect($files)"> </div> <div class="select2-selection__choicediv"> <div class="select2-selection__choice2" ng-repeat="file in vm.form.files"> <div class="select2-selection__choice2__remove" ng-click="fn.removeUploadFile($index)"> × </div> <div class="ssg-items ssg-items-blocks"> <div class="ssg-item"> <div class="item-icon"> <!-- 문서 --> <i class="os-icon os-icon-file-text" ng-if="file.fileType == 'DOC'"></i> <!-- 미디어 --> <i class="os-icon os-icon-film" ng-if="file.fileType == 'MEDIA'"></i> <!-- 이미지(업로드 전) --> <i class="os-icon os-icon-documents-07" ng-if="file.fileType == 'IMAGE'"></i> <!-- 기타 --> <i class="os-icon os-icon-ui-51" ng-if="file.fileType == 'ETC'"></i> </div> <div class="item-name"> <small>{{file.name}}</small> </div> <div class="item-amount"> ({{file.size/1024/1024 | number:2}} MB) </div> </div> </div> </div> </div> </div> </div> </div> <hr> <div class="row"> <div class="col-lg-4 fontcolor_green"> <label class="issue-label"><span class="fontcolor_green" translate="common.detectingInfo">탐지정보</span></label> </div> </div> <div class="col-lg-12"> <div class="row"> <div class="col-md-12"> <div class="form-group mgb5" ng-show="vm.form.issueCustomFields.length < 1"> <label class="issue-label" translate="issue.notIssueTypeCustomFields">이슈 타입에 연결된 사용자 정의 필드가 없습니다.</label> </div> </div> <div class="col-md-4" ng-repeat="issueCustomField in vm.form.issueCustomFields"> <div class="form-group mgb5"> <label class="issue-label">{{issueCustomField.customFieldVo.name}}</label> <div ng-switch on="issueCustomField.customFieldVo.customFieldType"> <!-- 기본 입력 --> <div ng-switch-when="INPUT"> <input type="text" class="form-control input-sm" name="input" ng-model="issueCustomField.useValues" maxlength="100" autocomplete="off" kr-input ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'"> <small class="help-block form-text text-danger" ng-show="issueCustomField.customFieldVo.requiredData == 'Y'" ng-if="issueAddDownForm.input.$error.required" translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자정의필드는 필수 입력 값 입니다. </small> </div> <div ng-switch-when="NUMBER"> <input type="text" class="form-control input-sm" name="number" ng-model="issueCustomField.useValues" maxlength="100" autocomplete="off" kr-input ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'"> <small class="help-block form-text text-danger" ng-show="issueCustomField.customFieldVo.requiredData == 'Y'" ng-if="issueAddDownForm.number.$error.required" translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다. </small> </div> <div ng-switch-when="DATETIME"> <input type="text" class="form-control input-sm" name="dateTime" ng-model="issueCustomField.useValues" maxlength="100" autocomplete="off" kr-input ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'"> <small class="help-block form-text text-danger" ng-show="issueCustomField.customFieldVo.requiredData == 'Y'" ng-if="issueAddDownForm.dateTime.$error.required" translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다. </small> </div> <div ng-switch-when="IP_ADDRESS"> <input type="text" class="form-control input-sm" name="ipAddress" ng-model="issueCustomField.useValues" maxlength="100" autocomplete="off" kr-input ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'"> <small class="help-block form-text text-danger" ng-show="issueCustomField.customFieldVo.requiredData == 'Y'" ng-if="issueAddDownForm.ipAddress.$error.required" translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다. </small> </div> <div ng-switch-when="SITE"> <input type="text" class="form-control input-sm" name="site" ng-model="issueCustomField.useValues" maxlength="100" autocomplete="off" kr-input ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'"> <small class="help-block form-text text-danger" ng-show="issueCustomField.customFieldVo.requiredData == 'Y'" ng-if="issueAddDownForm.site.$error.required" translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다. </small> </div> <div ng-switch-when="TEL"> <input type="text" class="form-control input-sm" name="tel" ng-model="issueCustomField.useValues" maxlength="100" autocomplete="off" kr-input ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'"> <small class="help-block form-text text-danger" ng-show="issueCustomField.customFieldVo.requiredData == 'Y'" ng-if="issueAddDownForm.tel.$error.required" translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다. </small> </div> <!-- 단일 셀렉트 --> <div ng-switch-when="SINGLE_SELECT"> <select class="form-control input-sm issue-select-label" name="singleSelect" ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'" ng-model="issueCustomField.useValues"> <option value="" value="" translate="common.choose">선택하세요.</option> <option ng-repeat="customFieldValueVo in issueCustomField.customFieldVo.customFieldValueVos" value="{{customFieldValueVo.value}}" ng-selected="$root.selectOption(issueCustomField.useValues, customFieldValueVo.value)"> {{customFieldValueVo.value}} </option> </select> <small class="help-block form-text text-danger" ng-show="issueCustomField.customFieldVo.requiredData == 'Y'" ng-if="issueAddDownForm.singleSelect.$error.required" translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다. </small> </div> <!-- 멀티 셀렉트 --> <div ng-switch-when="MULTI_SELECT"> <ng-dropdown-multiselect class="multiSelect cursor" name="multiSelect" ng-required="issueCustomField.customFieldVo.requiredData == 'Y'" data-input-name="" modal-form-auto-scroll selected-model="issueCustomField.useValues" extra-settings="{ 'idProp' : 'value', 'externalIdProp' : 'value', 'displayProp' : 'value', 'stringTypeOption' : 'true', stopRemoveBodyEvent : true }" options="issueCustomField.customFieldVo.customFieldValueVos"></ng-dropdown-multiselect> <small class="help-block form-text text-danger" ng-show="issueCustomField.customFieldVo.requiredData == 'Y'" ng-if="issueAddDownForm.multiSelect.$error.required" translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다. </small> </div> </div> </div> </div> </div> </div> <hr> <div ng-show="fn.containsPartner('업체')" class="row"> <div class="col-lg-8 fontcolor_green"> <label class="issue-label"><span class="fontcolor_green" translate="companyField.info">업체정보</span> <span class="select3-selection__choice" style="position: relative; bottom: 2px;"><code class="highlighter-rouge">*</code> 업체 이름 클릭시 선택된 이름의 업체 정보가 조회되며, 업체정보에서 추가한 ISP, 호스팅 정보를 불러옵니다.</span> </label> </div> </div> <div ng-show="fn.containsPartner('업체')" class="row"> <div class="col-lg-4"> <div class="form-group mb10"> <label class="issue-label"> <span translate="companyField.name">업체이름</span> </label> <js-autocomplete-single data-input-name="issueCompanyField" selected-model="vm.form.issueCompanyFields" search="vm.companyName" source="fn.getIssueCompanyFieldList(vm.companyName, vm.form.issueCompanyFields, vm.autoCompletePage.companyField.page, fn.getIssueCompanyFieldListCallBack)" page="vm.autoCompletePage.companyField.page" total-page="vm.autoCompletePage.companyField.totalPage" input-disabled="false" translation-texts="{ empty : 'common.emptyCompany' }" broad-cast="companyFieldEvent" extra-settings="{ displayProp : 'name' , idProp : 'id', imageable : false, imagePathProp : '', type : '', maxlength : 200, autoResize : false, stopRemoveBodyEvent : true }"></js-autocomplete-single> </div> </div> <div class="col-lg-4"> <div class="form-group mb10"> <label for="companyFieldManagerAddForm" class="issue-label"><span translate="companyField.manager">담당자</span></label> <input id="companyFieldManagerAddForm" name="companyManager" type="text" class="form-control" autofocus kr-input input-regex="[^a-zA-Z0-9 가-힣ㄱ-ㅎㅏ-ㅣ\u318D\u119E\u11A2\u2022\u2025a\u00B7\uFE55]" autocomplete="off" ng-model="vm.companyManager" ng-maxlength="100" maxlength="100" > </div> </div> <div class="col-lg-4"> <div class="form-group mgb5"> <label for="companyFieldTelAddForm" class="issue-label"><span translate="companyField.tel">전화번호</span></label> <input id="companyFieldTelAddForm" name="companyTel" type="text" class="form-control" kr-input ng-pattern="/^\d{2,3}-\d{3,4}-\d{4}$/" autocomplete="off" ng-model="vm.companyTel" maxlength="20"> <div ng-show="issueAddDownForm.companyTel.$error.pattern" class="help-block form-text text-danger" translate="companyField.invalidTelFormat">전화번호 형식이 맞지 않습니다. xxx-xxx-xxxx 형식으로 입력하세요. </div> </div> </div> <div class="col-lg-4"> <div class="form-group mgb5"> <label for="companyFieldEmailAddForm" class="issue-label"><span translate="companyField.email">이메일</span></label> <input id="companyFieldEmailAddForm" name="companyEmail" type="email" class="form-control" autocomplete="off" maxLength="50" ng-model="vm.companyEmail" kr-input ng-pattern="/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/"> <div ng-show="issueAddDownForm.companyEmail.$error.pattern" class="help-block form-text text-danger" translate="users.invalidEmailFormat">이메일 형식이 맞지 않습니다. </div> </div> </div> <div class="col-lg-4"> <div class="form-group mgb5"> <label for="companyFieldUrlAddForm" class="issue-label"><span translate="companyField.url">url</span></label> <input id="companyFieldUrlAddForm" name="companyUrl" type="text" class="form-control" kr-input autocomplete="off" ng-maxlength="200" ng-model="vm.companyUrl" maxlength="200"> </div> </div> <div class="col-lg-4"> <div class="form-group mgb5"> <label for="companyFieldDescAddForm" class="issue-label"><span translate="companyField.memo">비고</span></label> <input id="companyFieldDescAddForm" name="companyMemo" type="text" class="form-control" kr-input autocomplete="off" ng-maxlength="200" ng-model="vm.companyMemo" maxlength="200"> </div> </div> </div> <p></p> <div ng-show="fn.containsPartner('ISP')" class="row"> <div class="col-lg-8 fontcolor_green"> <label class="issue-label"><span class="fontcolor_green" translate="ispField.info">ISP 정보</span> <!--<span class="select3-selection__choice" style="position: relative; bottom: 2px;"><code class="highlighter-rouge">*</code> ISP 이름 클릭시 선택된 이름의 ISP 정보가 조회됩니다.</span>--> </label> </div> </div> <div ng-show="fn.containsPartner('ISP')" class="row"> <div class="col-lg-4"> <div class="form-group mb10"> <label class="issue-label"> <span translate="ispField.name">ISP 이름</span> </label> <input name="ispName" type="text" class="form-control" kr-input autocomplete="off" ng-model="vm.ispName" ng-maxlength="100" maxlength="100"> <!--<js-autocomplete-single data-input-name="ispField" selected-model="vm.form.issueIspFields" search="vm.ispName" source="fn.getIssueIspFieldList(vm.ispName, vm.form.issueIspFields, vm.autoCompletePage.ispField.page, fn.getIssueIspFieldListCallBack)" page="vm.autoCompletePage.ispField.page" total-page="vm.autoCompletePage.ispField.totalPage" input-disabled="false" translation-texts="{ empty : 'common.emptyIsp' }" broad-cast="ispFieldEvent" extra-settings="{ displayProp : 'name' , idProp : 'id', imageable : false, imagePathProp : '', type : '', maxlength : 200, autoResize : false, stopRemoveBodyEvent : true }"></js-autocomplete-single>--> </div> </div> <div class="col-lg-4"> <div class="form-group mb10"> <label for="ispFieldCodeAddForm" class="issue-label"><span translate="ispField.code">코드</span></label> <input id="ispFieldCodeAddForm" name="ispCode" type="text" class="form-control" kr-input input-regex="[^a-zA-Z0-9 가-힣ㄱ-ㅎㅏ-ㅣ\u318D\u119E\u11A2\u2022\u2025a\u00B7\uFE55]" autocomplete="off" ng-model="vm.ispCode" ng-maxlength="100" maxlength="100"> </div> </div> <div class="col-lg-4"> <div class="form-group mb10"> <label for="ispFieldManagerAddForm" class="issue-label"><span translate="ispField.manager">담당자</span></label> <input id="ispFieldManagerAddForm" name="ispManager" type="text" class="form-control" kr-input input-regex="[^a-zA-Z0-9 가-힣ㄱ-ㅎㅏ-ㅣ\u318D\u119E\u11A2\u2022\u2025a\u00B7\uFE55]" autocomplete="off" ng-model="vm.ispManager" ng-maxlength="100" maxlength="100"> </div> </div> <div class="col-lg-4"> <div class="form-group mgb5"> <label for="ispFieldTelAddForm" class="issue-label"><span translate="ispField.tel">전화번호</span></label> <input id="ispFieldTelAddForm" name="ispTel" type="text" class="form-control" kr-input ng-pattern="/^\d{2,3}-\d{3,4}-\d{4}$/" autocomplete="off" ng-model="vm.ispTel" maxlength="20"> <div ng-show="issueAddDownForm.ispTel.$error.pattern" class="help-block form-text text-danger" translate="companyField.invalidTelFormat">전화번호 형식이 맞지 않습니다. xxx-xxx-xxxx 형식으로 입력하세요. </div> </div> </div> <div class="col-lg-4"> <div class="form-group mgb5"> <label for="ispFieldEmailAddForm" class="issue-label"><span translate="ispField.email">이메일</span></label> <input id="ispFieldEmailAddForm" name="ispEmail" type="email" class="form-control" autocomplete="off" maxLength="50" ng-model="vm.ispEmail" kr-input ng-pattern="/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/"> <div ng-show="issueAddDownForm.ispEmail.$error.pattern" class="help-block form-text text-danger" translate="users.invalidEmailFormat">이메일 형식이 맞지 않습니다. </div> </div> </div> <div class="col-lg-4"> <div class="form-group mgb5"> <label for="ispFieldUrlAddForm" class="issue-label"><span translate="companyField.url">url</span></label> <input id="ispFieldUrlAddForm" name="ispUrl" type="text" class="form-control" kr-input autocomplete="off" ng-maxlength="200" ng-model="vm.ispUrl" maxlength="200"> </div> </div> <div class="col-lg-4"> <div class="form-group mgb5"> <label for="ispFieldDescAddForm" class="issue-label"><span translate="ispField.memo">비고</span></label> <input id="ispFieldDescAddForm" name="ispMemo" type="text" class="form-control" kr-input autocomplete="off" ng-model="vm.ispMemo" ng-maxlength="200" maxlength="200"> </div> </div> </div> <p></p> <div ng-show="fn.containsPartner('호스팅')" class="row"> <div class="col-lg-8 fontcolor_green"> <label class="issue-label"><span class="fontcolor_green" translate="hostingField.info">호스팅 정보</span> <!--<span class="select3-selection__choice" style="position: relative; bottom: 2px;"><code class="highlighter-rouge">*</code> 호스팅 이름 클릭시 선택된 이름의 호스팅 정보가 조회됩니다.</span>--> </label> </div> </div> <div ng-show="fn.containsPartner('호스팅')" class="row"> <div class="col-lg-4"> <div class="form-group mb10"> <label class="issue-label"> <span translate="hostingField.name">호스팅 이름</span> </label> <input name="hostingName" type="text" class="form-control" kr-input autocomplete="off" ng-model="vm.hostingName" ng-maxlength="100" maxlength="100"> <!--<js-autocomplete-single data-input-name="hostingField" selected-model="vm.form.issueHostingFields" search="vm.hostingName" source="fn.getIssueHostingFieldList(vm.hostingName, vm.form.issueHostingFields, vm.autoCompletePage.hostingField.page, fn.getIssueHostingFieldListCallBack)" page="vm.autoCompletePage.hostingField.page" total-page="vm.autoCompletePage.hostingField.totalPage" input-disabled="false" translation-texts="{ empty : 'common.emptyHosting' }" broad-cast="hostingFieldEvent" extra-settings="{ displayProp : 'name' , idProp : 'id', imageable : false, imagePathProp : '', type : '', maxlength : 200, autoResize : false, stopRemoveBodyEvent : true }"></js-autocomplete-single>--> </div> </div> <div class="col-lg-4"> <div class="form-group mb10"> <label for="hostingCodeManagerAdd" class="issue-label"><span translate="hostingField.code">담당자</span></label> <input id="hostingCodeManagerAdd" name="hostingCode" type="text" class="form-control" kr-input input-regex="[^a-zA-Z0-9 가-힣ㄱ-ㅎㅏ-ㅣ\u318D\u119E\u11A2\u2022\u2025a\u00B7\uFE55]" autocomplete="off" ng-model="vm.hostingCode" ng-maxlength="100" maxlength="100"> </div> </div> <div class="col-lg-4"> <div class="form-group mb10"> <label for="hostingFieldManagerAdd" class="issue-label"><span translate="hostingField.manager">담당자</span></label> <input id="hostingFieldManagerAdd" name="hostingManager" type="text" class="form-control" kr-input input-regex="[^a-zA-Z0-9 가-힣ㄱ-ㅎㅏ-ㅣ\u318D\u119E\u11A2\u2022\u2025a\u00B7\uFE55]" autocomplete="off" ng-model="vm.hostingManager" ng-maxlength="100" maxlength="100"> </div> </div> <div class="col-lg-4"> <div class="form-group mgb5"> <label for="hostingFieldTelAdd" class="issue-label"><span translate="hostingField.tel">전화번호</span></label> <input id="hostingFieldTelAdd" name="hostingTel" type="text" class="form-control" kr-input ng-pattern="/^\d{2,3}-\d{3,4}-\d{4}$/" autocomplete="off" ng-model="vm.hostingTel" maxlength="20"> <div ng-show="issueAddDownForm.hostingTel.$error.pattern" class="help-block form-text text-danger" translate="companyField.invalidTelFormat">전화번호 형식이 맞지 않습니다. xxx-xxx-xxxx 형식으로 입력하세요. </div> </div> </div> <div class="col-lg-4"> <div class="form-group mgb5"> <label for="hostingFieldEmailAdd" class="issue-label"><span translate="hostingField.email">이메일</span></label> <input id="hostingFieldEmailAdd" name="hostingEmail" type="email" class="form-control" autocomplete="off" maxLength="50" ng-model="vm.hostingEmail" kr-input ng-pattern="/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/"> <div ng-show="issueAddDownForm.hostingEmail.$error.pattern" class="help-block form-text text-danger" translate="users.invalidEmailFormat">이메일 형식이 맞지 않습니다. </div> </div> </div> <div class="col-lg-4"> <div class="form-group mgb5"> <label for="hostingFieldUrlAddForm" class="issue-label"><span translate="companyField.url">url</span></label> <input id="hostingFieldUrlAddForm" name="hostingUrl" type="text" class="form-control" kr-input autocomplete="off" ng-maxlength="200" ng-model="vm.hostingUrl" maxlength="200"> </div> </div> <div class="col-lg-4"> <div class="form-group mgb5"> <label for="hostingFieldDescAdd" class="issue-label"><span translate="hostingField.memo">비고</span></label> <input id="hostingFieldDescAdd" name="hostingMemo" type="text" class="form-control" kr-input autocomplete="off" ng-model="vm.hostingMemo" ng-maxlength="200" maxlength="200"> </div> </div> </div> </form> <hr> <div class="form-group mb10"> <label class="issue-label"><span translate="common.content">내용</span></label> <summernote class="summernote" lang="ko-KR" summer-note-auto-focus ng-model="vm.form.description" data-editor="vm.summerNote.editor" data-editable="vm.summerNote.editable" on-image-upload="fn.imageUpload(files)" target=".note-editable"></summernote> </div> </div> </div> <div class="modal-footer buttons-on-right"> <div class="pull-left"> <label> <input class="form-control issue-continue-checkbox pull-left" type="checkbox" ng-model="vm.infiniteAdd" tabindex="-1"> <span translate="issue.continueCreateIssue">이 화면에서 이슈를 계속 생성합니다.</span> </label> </div> <button type="button" class="btn btn-md btn-grey" ng-click="fn.cancel()" tabindex="-1"><span translate="common.cancel">취소</span></button> <button type="button" class="btn btn-md btn-primary bold" js-short-cut js-short-cut-action="(fn.formCheck(issueAddDownForm.$invalid) || $root.spinner) ? null : fn.formSubmit()" ng-disabled="fn.formCheck(issueAddDownForm.$invalid)" ng-click="fn.formSubmit()"><span translate="common.save">저장</span> </button> </div> src/main/webapp/views/issue/issueDetail.html
@@ -574,6 +574,11 @@ ng-if="vm.viewer.modifyPermissionCheck" ng-click="fn.addDownIssue()" translate="issue.addDownIssue">추가</button> <button type="button" class="btn btn-sm btn-primary btn-roundRel offset-1" ng-if="vm.viewer.modifyPermissionCheck" ng-click="fn.addDownIssueForm(vm.viewer.id)"> <i class="os-icon os-icon-plus"><span></span></i> </button> </div> </div>