OWL ITS + 탐지시스템(인터넷 진흥원)
wyu
2021-12-10 49a02aa2071bcaf94f0f8c19e2578cd7650160a4
Merge branch 'master' of http://192.168.0.25:9001/r/owl-kisa
4개 파일 추가됨
30개 파일 변경됨
1802 ■■■■■ 파일 변경됨
src/main/java/kr/wisestone/owl/constant/MsgConstants.java 1 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/domain/CompanyField.java 18 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/IssueService.java 3 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/CompanyFieldServiceImpl.java 60 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/HostingFieldServiceImpl.java 27 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/IspFieldServiceImpl.java 24 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/IssueRelationServiceImpl.java 18 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/IssueServiceImpl.java 181 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/vo/CompanyFieldVo.java 39 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/vo/IssueVo.java 12 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/condition/CompanyFieldCondition.java 18 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/condition/IssueCondition.java 13 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/condition/IssueCustomFieldValueCondition.java 9 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/controller/ApiController.java 1 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/form/CompanyFieldForm.java 19 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/form/IssueCustomFieldValueForm.java 9 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/resources/migration/V1_13__Alter_Table.sql 10 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/resources/mybatis/query-template/companyField-template.xml 4 ●●● 패치 | 보기 | raw | blame | 히스토리
src/main/resources/mybatis/query-template/issue-template.xml 41 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/js-tree/js-tree.directive.js 36 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/js-tree/js-tree.html 16 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/js-tree/tree.provider.js 234 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/js-tree/treeColumnGenerator.directive.js 64 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/i18n/ko/global.json 2 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/companyField/companyFieldAdd.controller.js 35 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/companyField/companyFieldModify.controller.js 65 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/issue/issue.js 2 ●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/issue/issueAdd.controller.js 54 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/issue/issueList.controller.js 131 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/main.js 10 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/companyField/companyFieldAdd.html 9 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/companyField/companyFieldModify.html 35 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/issue/issueAdd.html 2 ●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/issue/issueListNormal.html 600 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/constant/MsgConstants.java
@@ -240,6 +240,7 @@
    public static final String API_PARAMETER_ERROR = "API_PARAMETER_ERROR";     // api 파라미터 오류
    public static final String API_USER_ERROR = "API_USER_ERROR";     // api 사용자 오류
    public static final String API_OVERLAP_ERROR = "API_OVERLAP_ERROR";     // API 중복된 상위 이슈가 여러개일 경우
    public static final String API_OVERLAP_SETTING_NOT_EXIST = "API_OVERLAP_SETTING_NOT_EXIST";     // API 중복된 설정이 안되어 있을 경우
    public static final String API_ISSUE_NOT_EXIST = "API_ISSUE_NOT_EXIST";     // 수정할 이슈를 찾을수 없습니다.
    public static final String API_COMPLETE_ISSUE_STATUS_NOT_EXIST = "API_COMPLETE_ISSUE_STATUS_NOT_EXIST";     // 자동 종료 처리할 상태가 설정되지 않았습니다.
}
src/main/java/kr/wisestone/owl/domain/CompanyField.java
@@ -19,6 +19,8 @@
    private String url;
    private String email;
    private String memo;
    private Long ispId;
    private Long hostingId;
    public CompanyField() {}
@@ -77,4 +79,20 @@
    public void setUrl(String url) {
        this.url = url;
    }
    public Long getIspId() {
        return ispId;
    }
    public void setIspId(Long ispId) {
        this.ispId = ispId;
    }
    public Long getHostingId() {
        return hostingId;
    }
    public void setHostingId(Long hostingId) {
        this.hostingId = hostingId;
    }
}
src/main/java/kr/wisestone/owl/service/IssueService.java
@@ -35,7 +35,8 @@
    List<Issue> modifyIssue(IssueApiForm issueApiForm, List<MultipartFile> files);
    List<IssueVo> findIssue(IssueApiForm issueApiform);
    List<Issue> findIssue(IssueApiForm issueApiform);
    List<IssueVo> findIssue(Map<String, Object> resJsonData,
                            IssueCondition condition, Pageable pageable);
src/main/java/kr/wisestone/owl/service/impl/CompanyFieldServiceImpl.java
@@ -6,10 +6,11 @@
import kr.wisestone.owl.domain.IspField;
import kr.wisestone.owl.repository.HostingFieldRepository;
import kr.wisestone.owl.repository.IspFieldRepository;
import kr.wisestone.owl.service.UserService;
import kr.wisestone.owl.service.*;
import kr.wisestone.owl.web.condition.CompanyFieldCondition;
import kr.wisestone.owl.web.form.CompanyFieldForm;
import org.apache.commons.lang3.StringUtils;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import com.google.common.collect.Lists;
import kr.wisestone.owl.common.ExcelConditionCheck;
@@ -18,8 +19,6 @@
import kr.wisestone.owl.exception.OwlRuntimeException;
import kr.wisestone.owl.mapper.CompanyFieldMapper;
import kr.wisestone.owl.repository.CompanyFieldRepository;
import kr.wisestone.owl.service.CompanyFieldService;
import kr.wisestone.owl.service.WorkspaceService;
import kr.wisestone.owl.util.ConvertUtil;
import kr.wisestone.owl.vo.*;
import kr.wisestone.owl.web.view.ExcelView;
@@ -33,6 +32,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service
public class CompanyFieldServiceImpl extends AbstractServiceImpl<CompanyField, Long, JpaRepository<CompanyField, Long>> implements CompanyFieldService {
@@ -48,6 +48,12 @@
    @Autowired
    private HostingFieldRepository hostingFieldRepository;
    @Autowired
    private IspFieldService ispFieldService;
    @Autowired
    private HostingFieldService hostingFieldService;
    @Autowired
    private UserService userService;
@@ -112,13 +118,30 @@
    // 업체 상세 조회한다.
    @Override
    @Transactional
    public void detailCompany(Map<String, Object> resJsonData, CompanyFieldCondition companyFieldCondition) {
        CompanyFieldVo companyFieldVo = new CompanyFieldVo();
        IspFieldVo ispFieldVo = new IspFieldVo();
        HostingFieldVo hostingFieldVo = new HostingFieldVo();
        IspField ispField = new IspField();
        HostingField hostingField = new HostingField();
        Long companyId = companyFieldCondition.getId();
        if (companyId != null) {
            CompanyField companyField = this.getCompany(companyId);
            if(companyField.getIspId() != null){
                ispField = this.ispFieldRepository.getOne(companyField.getIspId());
            }
            if(companyField.getHostingId() != null){
                hostingField = this.hostingFieldRepository.getOne(companyField.getHostingId());
            }
            companyFieldVo = ConvertUtil.copyProperties(companyField, CompanyFieldVo.class);
            ispFieldVo = ConvertUtil.copyProperties(ispField, IspFieldVo.class);
            hostingFieldVo = ConvertUtil.copyProperties(hostingField, HostingFieldVo.class);
            companyFieldVo.setIspFieldVo(ispFieldVo);
            companyFieldVo.setHostingFieldVo(hostingFieldVo);
        }
        resJsonData.put(Constants.REQ_KEY_CONTENT, companyFieldVo);
    }
@@ -185,28 +208,25 @@
    //  검색 결과를 CompanyFieldVo 로 변환한다.
    private List<CompanyFieldVo> convertCompanyVoToMap(List<Map<String, Object>> results, Long totalCompanyCount, Pageable pageable, Map<String, Object> resJsonData) {
        List<CompanyFieldVo> companyFieldVos = Lists.newArrayList();
        List<IspFieldVo> ispFieldVos = Lists.newArrayList();
        List<HostingFieldVo> hostingFieldVos = Lists.newArrayList();
        for (Map<String, Object> result : results) {
            CompanyFieldVo companyFieldVo = ConvertUtil.convertMapToClass(result, CompanyFieldVo.class);
            String url = companyFieldVo.getUrl();
            IspField ispField = this.ispFieldRepository.findByUrl(url);
            if(ispField != null){
                IspFieldVo ispFieldVo = ConvertUtil.copyProperties(ispField, IspFieldVo.class);
                ispFieldVos.add(ispFieldVo);
            if(companyFieldVo.getIspId() != null && companyFieldVo.getIspId() != -1){
                //IspField ispField = this.ispFieldRepository.getOne(companyFieldVo.getIspId());
                IspField ispField = this.ispFieldService.getIsp(companyFieldVo.getIspId());
                if(ispField != null){
                    IspFieldVo ispFieldVo = ConvertUtil.copyProperties(ispField, IspFieldVo.class);
                    companyFieldVo.setIspFieldVo(ispFieldVo);
                }
            }
            companyFieldVo.setIspFieldVos(ispFieldVos);
            HostingField hostingField = this.hostingFieldRepository.findByUrl(url);
            if(hostingField != null){
                HostingFieldVo hostingFieldVo = ConvertUtil.copyProperties(hostingField, HostingFieldVo.class);
                hostingFieldVos.add(hostingFieldVo);
            if(companyFieldVo.getHostingId() != null && companyFieldVo.getIspId() != -1){
                //HostingField hostingField = this.hostingFieldRepository.getOne(companyFieldVo.getHostingId());
                HostingField hostingField = this.hostingFieldService.getHosting(companyFieldVo.getHostingId());
                if(hostingField != null){
                    HostingFieldVo hostingFieldVo = ConvertUtil.copyProperties(hostingField, HostingFieldVo.class);
                    companyFieldVo.setHostingFieldVo(hostingFieldVo);
                }
            }
            companyFieldVo.setHostingFieldVos(hostingFieldVos);
            companyFieldVos.add(companyFieldVo);
        }
src/main/java/kr/wisestone/owl/service/impl/HostingFieldServiceImpl.java
@@ -58,30 +58,9 @@
    // Hosting 추가
    @Override
    public HostingField add(HostingFieldForm HostingFieldForm) {
        if(HostingFieldForm.getUrl() != null){
            //  url 유효성 체크
            this.verifyUrl(HostingFieldForm.getUrl(), null);
        }
        HostingField HostingField = ConvertUtil.copyProperties(HostingFieldForm, HostingField.class);
        hostingFieldRepository.saveAndFlush(HostingField);
        return HostingField;
    }
    //  url 유효성 체크
    private void verifyUrl(String url, Long id) {
        HostingField hostingField;
        if(id == null){
            hostingField = this.hostingFieldRepository.findByUrl(url);
        } else {
            hostingField = this.hostingFieldRepository.findByUrlAndIdNot(url,id);
        }
        if (hostingField != null) {
            throw new OwlRuntimeException(
                    this.messageAccessor.getMessage(MsgConstants.COMPANYFIELD_USED_URL));
        }
    }
    // Hosting 목록을 가져온다.
@@ -114,15 +93,9 @@
    // Hosting 정로를 수정한다.
    @Override
    public void modify(HostingFieldForm HostingFieldForm) {
        //  url 유효성 체크
        if(HostingFieldForm.getUrl() != null){
            this.verifyUrl(HostingFieldForm.getUrl(), HostingFieldForm.getId());
        }
        HostingField HostingField = ConvertUtil.copyProperties(HostingFieldForm, HostingField.class);
        hostingFieldRepository.saveAndFlush(HostingField);
    }
    // Hosting를 삭제한다.
    @Override
src/main/java/kr/wisestone/owl/service/impl/IspFieldServiceImpl.java
@@ -58,11 +58,6 @@
    // Isp 추가
    @Override
    public IspField add(IspFieldForm IspFieldForm) {
        //  url 유효성 체크
        if(IspFieldForm.getUrl() != null){
            this.verifyUrl(IspFieldForm.getUrl(), null);
        }
        IspField IspField = ConvertUtil.copyProperties(IspFieldForm, IspField.class);
        ispFieldRepository.saveAndFlush(IspField);
        return IspField;
@@ -81,23 +76,6 @@
        return this.convertIspVoToMap(results, totalIspCount, pageable, resJsonData);
    }
    //  url 유효성 체크
    private void verifyUrl(String url, Long id) {
        IspField ispField;
        if(id == null){
            ispField = this.ispFieldRepository.findByUrl(url);
        } else {
            ispField = this.ispFieldRepository.findByUrlAndIdNot(url,id);
        }
        if (ispField != null) {
            throw new OwlRuntimeException(
                    this.messageAccessor.getMessage(MsgConstants.COMPANYFIELD_USED_URL));
        }
    }
    // Isp 상세 조회한다.
    @Override
    public void detail(Map<String, Object> resJsonData, IspFieldCondition ispFieldCondition) {
@@ -115,8 +93,6 @@
    @Override
    public void modify(IspFieldForm IspFieldForm) {
        if(IspFieldForm.getUrl() != null){
            //  url 유효성 체크
            this.verifyUrl(IspFieldForm.getUrl(), IspFieldForm.getId());
        }
        IspField IspField = ConvertUtil.copyProperties(IspFieldForm, IspField.class);
src/main/java/kr/wisestone/owl/service/impl/IssueRelationServiceImpl.java
@@ -65,24 +65,16 @@
    // 연관 일감 가져오기
    @Override
    public List<IssueVo> findRelationIssue(Map<String, Object> resJsonData, IssueRelationCondition condition, Pageable pageable) {
        List<IssueRelation> issueRelations = issueRelationRepository.findAllByIssueId(condition.getIssueId());
        List<IssueVo> issueVos = new ArrayList<>();
        List<IssueVo> issueVos = findRelationIssue(condition.getIssueId());
        if (issueRelations != null) {
            for (IssueRelation issueRelation : issueRelations) {
                issueVos.add(ConvertUtil.copyProperties(issueRelation.getRelationIssue(), IssueVo.class));
            }
            int totalCount = issueVos.size();
            resJsonData.put(Constants.RES_KEY_CONTENTS, issueVos);
            resJsonData.put(Constants.REQ_KEY_PAGE_VO, new ResPage(pageable.getPageNumber(), pageable.getPageSize(),
                1, totalCount));
        }
        resJsonData.put(Constants.RES_KEY_CONTENTS, issueVos);
        resJsonData.put(Constants.REQ_KEY_PAGE_VO, new ResPage(pageable.getPageNumber(), pageable.getPageSize(),
            1, issueVos.size()));
        return  issueVos;
    }
    // 연관 일감 가져오기
    @Override
    public List<IssueVo> findRelationIssue(Long issueId) {
src/main/java/kr/wisestone/owl/service/impl/IssueServiceImpl.java
@@ -257,15 +257,24 @@
            // 상위일감에 사용할 중복값 설정
            List<CustomFieldApiOverlap> customFieldApiOverlaps = this.customFieldApiOverlapService.find(user.getId(), issueApiForm.getIssueTypeId());
            if (customFieldApiOverlaps == null || customFieldApiOverlaps.size() == 0){
                throw new OwlRuntimeException(this.messageAccessor.getMessage(MsgConstants.API_OVERLAP_SETTING_NOT_EXIST));
            }
            for(int i=0; i < customFieldApiOverlaps.size() ; i++ ){
                CustomFieldApiOverlap customFieldApiOverlap = customFieldApiOverlaps.get(i);
                issueApiForm.addUseIssueCustomFieldId(customFieldApiOverlap.getCustomField().getId());
            }
            List<IssueVo> issueVos = this.findIssue(issueApiForm, customFieldApiOverlaps, user.getId());
            int size = issueVos.size();
            // 중복된 이슈검색
            List<Issue> issues = this.findIssue(issueApiForm, customFieldApiOverlaps, user.getId());
            int size = issues.size();
            if (size > 0) {
                issueForm.setParentIssueId(issueVos.get(0).getId());
                Issue targetIssue = issues.get(0);
                if (targetIssue.getParentIssue() != null) {
                    issueForm.setParentIssueId(targetIssue.getParentIssue().getId());
                } else {
                    issueForm.setParentIssueId(targetIssue.getId());
                }
            }
            issueForm.setIsApi(Issue.IS_API_YES);
@@ -326,9 +335,9 @@
    }
    // 중복된 상위 이슈 검색
    private List<IssueVo> findIssue(IssueApiForm issueApiform, List<CustomFieldApiOverlap> customFieldApiOverlaps, Long userId) {
    private List<Issue> findIssue(IssueApiForm issueApiform, List<CustomFieldApiOverlap> customFieldApiOverlaps, Long userId) {
        List<IssueCustomFieldValueForm> issueCustomFieldValueForms = issueApiform.getIssueCustomFieldValues();
        List<IssueVo> resultIssueVos = Lists.newArrayList();
        List<Issue> resultIssueVos = Lists.newArrayList();
        String comma = ",";
        if (issueCustomFieldValueForms.size() > 0) {
@@ -352,10 +361,11 @@
            IssueCustomFieldValueCondition issueCustomFieldValueCondition = new IssueCustomFieldValueCondition();
            issueCustomFieldValueCondition.setUseValue(concatUseValue);
            issueCustomFieldValueCondition.setIssueTypeId(issueApiform.getIssueTypeId());
            List<Map<String, Object>> results = this.issueMapper.findByCustomFieldValue(issueCustomFieldValueCondition);
            if (results != null && results.size() > 0) {
                for (Map<String, Object> result : results) {
                    resultIssueVos.add(ConvertUtil.convertMapToClass(result, IssueVo.class));
                    resultIssueVos.add(this.getIssue(MapUtil.getLong(result, "id")));
                }
            }
        }
@@ -718,11 +728,11 @@
    public List<IssueVo> findIssue(Map<String, Object> resJsonData, IssueCondition issueCondition, Pageable pageable) {
        //  검색 조건을 만든다
        /*if (!this.makeIssueSearchCondition(issueCondition, Lists.newArrayList("01", "02", "03"), pageable)) {
        if (!this.makeIssueSearchCondition(issueCondition, Lists.newArrayList("01", "02", "03"), pageable)) {
            //  이슈 목록을 찾지 못할 경우 기본 정보로 리턴한다.
            this.notFoundIssueList(resJsonData, pageable);
            return Lists.newArrayList();
        }*/
        }
        Set<String> issueIds = new HashSet<>(); //  사용자 정의 필드 검색시 나오는 이슈 아이디 저장 컬렉션
@@ -771,12 +781,19 @@
        int totalPage = (int) Math.ceil((totalCount - 1) / pageable.getPageSize()) + 1;
        //  이슈 아이디 초기화
        issueCondition.setIsApi(issueCondition.getIsApi());
        issueCondition.setIssueIds(Lists.newArrayList());
        //  Map 에 있는 데이터를 IssueVo 데이터로 변환한다.
        this.setMapToIssueVo(results, issueVos, issueCondition, user);
        if (issueCondition.getTree()) {
            this.setDownIssues(issueVos);
            this.setRelationIssues(issueVos);
        }
        this.setCountDownIssues(results, issueVos);
        this.SetWorkflowDepartment(issueVos); //워크플로우에 설정한 담당부서 가져오기
        resJsonData.put(Constants.RES_KEY_CONTENTS, issueVos);
@@ -786,6 +803,27 @@
        //  사용자 시스템 기능 사용 정보 수집
        log.info(ElasticSearchUtil.makeUserActiveHistoryMessage(this.webAppUtil.getLoginUser(), ElasticSearchConstants.ISSUE_FIND));
        return issueVos;
    }
    // 하위 이슈 세팅
    private void setDownIssues(List<IssueVo> issueVos) {
        for(IssueVo issueVo : issueVos) {
            List<Issue> downIssues = this.issueRepository.findByParentIssueId(issueVo.getId());
            for(Issue downIssue : downIssues){
                issueVo.addIssueDownVo(ConvertUtil.copyProperties(downIssue, IssueVo.class));
            }
        }
    }
    // 연관 이슈 세팅
    private void setRelationIssues(List<IssueVo> issueVos) {
        for(IssueVo issueVo : issueVos) {
            List<IssueVo> relationIssues = this.issueRelationService.findRelationIssue(issueVo.getId());
            for(IssueVo relationIssue : relationIssues){
                issueVo.addRelationIssueVo(ConvertUtil.copyProperties(relationIssue, IssueVo.class));
            }
        }
    }
    @Override
@@ -1038,7 +1076,6 @@
        condition.setWorkspaceId(this.userService.getUser(this.webAppUtil.getLoginId()).getLastWorkspaceId());
        projectCondition.setWorkspaceId(condition.getWorkspaceId());
        //  프로젝트 키가 존재할 경우 프로젝트 키에 해당하는 프로젝트를 조회하고 검색 조건에 셋팅한다.
        if (!this.getProjectByProjectKey(condition.getProjectKey(), condition)) {
            return false;
@@ -1047,9 +1084,7 @@
        //  프로젝트를 선택하지 않았으면 해당 업무 공간에서 참여하고 있는 프로젝트를 찾는다.
        if (condition.getProjectIds().size() < 1) {
            List<Map<String, Object>> projects = null;
            UserLevel userLevel = this.userLevelService.getUserLevel(user.getUserLevel().getId());
            if (this.userWorkspaceService.checkWorkspaceManager(user)
                    || MngPermission.checkMngPermission(userLevel.getPermission(), MngPermission.USER_PERMISSION_MNG_ISSUE_PROJECT_ALL)) {
            if (this.userWorkspaceService.checkWorkspaceManager(user)) {
                projects = this.projectMapper.findByWorkspaceManagerAll(projectCondition);
            } else  {
                projects = this.projectService.findByWorkspaceIdAndIncludeProjectAll(projectCondition);
@@ -1266,26 +1301,26 @@
    // 하위 이슈 정보를 셋팅한다
    private void setDownIssues(Issue issue, IssueVo issueVo) {
        List<Issue> downIssues = this.issueRepository.findByParentIssueId(issue.getId());
            if(downIssues != null && downIssues.size()>0){
                List<IssueVo> resultList = new ArrayList<>();
                for(Issue downIssue : downIssues){
                    IssueVo downIssueVo = ConvertUtil.copyProperties(downIssue, IssueVo.class);
                    downIssueVo.setIssueTypeVo(ConvertUtil.copyProperties(downIssue.getIssueType(), IssueTypeVo.class));
                    downIssueVo.setPriorityVo(ConvertUtil.copyProperties(downIssue.getPriority(), PriorityVo.class));
                    downIssueVo.setSeverityVo(ConvertUtil.copyProperties(downIssue.getSeverity(), SeverityVo.class));
                    //이슈 상태 추가
                    IssueStatusVo issueStatusVo = ConvertUtil.copyProperties(downIssue.getIssueStatus(), IssueStatusVo.class, "issueStatusType");
                    issueStatusVo.setIssueStatusType(downIssue.getIssueStatus().getIssueStatusType().toString());
                    downIssueVo.setIssueStatusVo(issueStatusVo);
        if(downIssues != null && downIssues.size()>0){
            List<IssueVo> resultList = new ArrayList<>();
            for(Issue downIssue : downIssues){
                IssueVo downIssueVo = ConvertUtil.copyProperties(downIssue, IssueVo.class);
                downIssueVo.setIssueTypeVo(ConvertUtil.copyProperties(downIssue.getIssueType(), IssueTypeVo.class));
                downIssueVo.setPriorityVo(ConvertUtil.copyProperties(downIssue.getPriority(), PriorityVo.class));
                downIssueVo.setSeverityVo(ConvertUtil.copyProperties(downIssue.getSeverity(), SeverityVo.class));
                //이슈 상태 추가
                IssueStatusVo issueStatusVo = ConvertUtil.copyProperties(downIssue.getIssueStatus(), IssueStatusVo.class, "issueStatusType");
                issueStatusVo.setIssueStatusType(downIssue.getIssueStatus().getIssueStatusType().toString());
                downIssueVo.setIssueStatusVo(issueStatusVo);
                    this.setRegister(downIssue, downIssueVo); // 등록자
                    this.setIssueDepartment(downIssue, downIssueVo);  //  담당부서 정보 셋팅
                    this.setIssueCustomFields(downIssue, downIssueVo);   // 사용자정의필드 정보 세팅
                this.setRegister(downIssue, downIssueVo); // 등록자
                this.setIssueDepartment(downIssue, downIssueVo);  //  담당부서 정보 셋팅
                this.setIssueCustomFields(downIssue, downIssueVo);   // 사용자정의필드 정보 세팅
                    resultList.add(downIssueVo);
                }
                issueVo.setIssueDownVos(resultList);
                resultList.add(downIssueVo);
            }
            issueVo.setIssueDownVos(resultList);
        }
    }
    //  이슈 상세 정보를 셋팅한다.
@@ -1482,10 +1517,10 @@
    // 사용자 정의 필드 값이 같은 이슈 찾기
    @Override
    @Transactional
    public List<IssueVo> findIssue(IssueApiForm issueApiform) {
    public List<Issue> findIssue(IssueApiForm issueApiform) {
        List<IssueCustomFieldValueForm> issueCustomFieldValueForms = issueApiform.getIssueCustomFieldValues();
        List<IssueVo> resultIssueVos = Lists.newArrayList();
        List<Issue> resultIssueVos = Lists.newArrayList();
        String comma = ",";
        if (issueCustomFieldValueForms.size() > 0) {
@@ -1500,10 +1535,11 @@
            IssueCustomFieldValueCondition issueCustomFieldValueCondition = new IssueCustomFieldValueCondition();
            issueCustomFieldValueCondition.setUseValue(concatUseValue);
            issueCustomFieldValueCondition.setIssueTypeId(issueApiform.getIssueTypeId());
            List<Map<String, Object>> results = this.issueMapper.findByCustomFieldValue(issueCustomFieldValueCondition);
            if (results != null && results.size() > 0) {
                for (Map<String, Object> result : results) {
                    resultIssueVos.add(ConvertUtil.convertMapToClass(result, IssueVo.class));
                    resultIssueVos.add(this.getIssue(MapUtil.getLong(result, "id")));
                }
            }
        }
@@ -1529,11 +1565,10 @@
        User user = this.convertToUser(issueApiForm.getToken());
        IssueForm issueForm = this.convertToIssueForm(issueApiForm, user);
        List<IssueVo> issueVos = this.findIssue(issueApiForm);
        if (issueVos != null && issueVos.size() > 0) {
            List<Issue> issue = Lists.newArrayList();
            for (IssueVo issueVo : issueVos) {
                IssueVo parentIssueVo = issueVo.getParentIssueVo();
        List<Issue> issue = this.findIssue(issueApiForm);
        if (issue != null && issue.size() > 0) {
            List<Issue> issues = Lists.newArrayList();
            for (Issue issueVo : issue) {
                issueForm.setId(issueVo.getId());
                // 자동 종료 상태 설정이 되어 있지 않으면 오류발생
@@ -1555,9 +1590,9 @@
                    }
                }
                issue.add(modifyIssue);
                issues.add(modifyIssue);
            }
            return issue;
            return issues;
        } else {
            throw new OwlRuntimeException(
                    this.messageAccessor.getMessage(MsgConstants.API_ISSUE_NOT_EXIST));
@@ -1879,8 +1914,8 @@
        //hasPermission = this.checkIssueModifyPermission(hasPermission, Issue.ASSIGNEE, issueVo, issueUserVos);
        //  담당자가 없으면 모든 사용자가 수정 권한을 갖는다.
        hasPermission = this.checkIssueModifyPermission(hasPermission, Issue.ALL_ISSUE_MANAGER, issueVo, null, null, user);
        hasPermission = this.checkIssueModifyPermission(hasPermission, Issue.ALL_PROJECT_MANAGER, issueVo, null, null, user);
        //hasPermission = this.checkIssueModifyPermission(hasPermission, Issue.ALL_ISSUE_MANAGER, issueVo, null, null, user);
        //hasPermission = this.checkIssueModifyPermission(hasPermission, Issue.ALL_PROJECT_MANAGER, issueVo, null, null, user);
        return hasPermission;
    }
@@ -2163,38 +2198,39 @@
    }
    private Issue issueRemoves(Long issueId, User user) {
        Issue issue = this.findOne(issueId);
        if(issue != null){
            //  이슈 수정 권한을 갖고 있는지 확인
            this.verifyIssueModifyPermission(issue, user);
            //  이슈 첨부 파일을 삭제한다.
            if (issue.getAttachedFiles().size() > 0) {
                List<Long> attachedFileIds = Lists.newArrayList();
                for (AttachedFile attachedFile : issue.getAttachedFiles()) {
                    attachedFileIds.add(attachedFile.getId());
                }
                //  첨부파일 삭제
                this.attachedFileService.removeAttachedFiles(attachedFileIds);
            }
            // 지울 이슈가 연관이슈인지 체크 후 연관이슈 테이블에서도 삭제한다.
            List<IssueRelation> issueRelationList = this.issueRelationRepository.findByRelationIssueId(issueId);
            if (issueRelationList != null && issueRelationList.size() > 0) {
                for(IssueRelation issueRelation : issueRelationList){
                    StringBuilder sb = new StringBuilder();
                    issueHistoryService.detectRelationIssue(IssueHistoryType.DELETE, issueRelation, sb);
                    issueHistoryService.addIssueHistory(issueRelation.getIssue(), IssueHistoryType.MODIFY, sb.toString());
                    this.issueRelationRepository.delete(issueRelation);
                }
            }
            //  이슈 생성, 삭제시 예약 이메일에 등록해놓는다.
            this.reservationIssueEmail(issue, EmailType.ISSUE_REMOVE);
            //  이슈 삭제
            this.issueRepository.delete(issue);
        Issue issue = null;
        if(issueId != null){
            issue = this.getIssue(issueId);
        }
        //  이슈 수정 권한을 갖고 있는지 확인
        this.verifyIssueModifyPermission(issue, user);
        //  이슈 첨부 파일을 삭제한다.
        if (issue.getAttachedFiles().size() > 0) {
            List<Long> attachedFileIds = Lists.newArrayList();
            for (AttachedFile attachedFile : issue.getAttachedFiles()) {
                attachedFileIds.add(attachedFile.getId());
            }
            //  첨부파일 삭제
            this.attachedFileService.removeAttachedFiles(attachedFileIds);
        }
        // 지울 이슈가 연관이슈인지 체크 후 연관이슈 테이블에서도 삭제한다.
        List<IssueRelation> issueRelationList = this.issueRelationRepository.findByRelationIssueId(issueId);
        if (issueRelationList != null && issueRelationList.size() > 0) {
            for(IssueRelation issueRelation : issueRelationList){
                StringBuilder sb = new StringBuilder();
                issueHistoryService.detectRelationIssue(IssueHistoryType.DELETE, issueRelation, sb);
                issueHistoryService.addIssueHistory(issueRelation.getIssue(), IssueHistoryType.MODIFY, sb.toString());
                this.issueRelationRepository.delete(issueRelation);
            }
        }
        //  이슈 생성, 삭제시 예약 이메일에 등록해놓는다.
        this.reservationIssueEmail(issue, EmailType.ISSUE_REMOVE);
        //  이슈 삭제
        this.issueRepository.delete(issue);
        return issue;
    }
@@ -3458,7 +3494,6 @@
        }
        this.issueHistoryService.addIssueHistory(parentIssue, IssueHistoryType.MODIFY, sb.toString()); //parentIssue = myIssue(기록은 현재 상세페이지에 해야하니까)
        this.issueRepository.saveAndFlush(issue);
    }
    @Override
src/main/java/kr/wisestone/owl/vo/CompanyFieldVo.java
@@ -11,14 +11,15 @@
    private String email;
    private String url;
    private String memo;
    private Long ispId;
    private Long hostingId;
    private List<IspFieldVo> ispFieldVos = Lists.newArrayList();
    private List<HostingFieldVo> hostingFieldVos = Lists.newArrayList();
    private IspFieldVo ispFieldVo;
    private HostingFieldVo hostingFieldVo;
    public CompanyFieldVo() {}
    public CompanyFieldVo(String manager, String tel, String email, String memo) {
        this.manager = manager;
        this.tel = tel;
        this.email = email;
@@ -66,19 +67,35 @@
        this.url = url;
    }
    public List<IspFieldVo> getIspFieldVos() {
        return ispFieldVos;
    public IspFieldVo getIspFieldVo() {
        return ispFieldVo;
    }
    public void setIspFieldVos(List<IspFieldVo> ispFieldVos) {
        this.ispFieldVos = ispFieldVos;
    public void setIspFieldVo(IspFieldVo ispFieldVo) {
        this.ispFieldVo = ispFieldVo;
    }
    public List<HostingFieldVo> getHostingFieldVos() {
        return hostingFieldVos;
    public HostingFieldVo getHostingFieldVo() {
        return hostingFieldVo;
    }
    public void setHostingFieldVos(List<HostingFieldVo> hostingFieldVos) {
        this.hostingFieldVos = hostingFieldVos;
    public void setHostingFieldVo(HostingFieldVo hostingFieldVo) {
        this.hostingFieldVo = hostingFieldVo;
    }
    public Long getIspId() {
        return ispId;
    }
    public void setIspId(Long ispId) {
        this.ispId = ispId;
    }
    public Long getHostingId() {
        return hostingId;
    }
    public void setHostingId(Long hostingId) {
        this.hostingId = hostingId;
    }
}
src/main/java/kr/wisestone/owl/vo/IssueVo.java
@@ -420,6 +420,12 @@
        this.issueRelations.add(issueRelationVo);
    }
    public void addRelationIssueVo(IssueVo issueVo) {
        if (this.issueRelationVos != null) {
            this.issueRelationVos.add(issueVo);
        }
    }
    /*public void addIssueDownVo(IssueDownVo issueDownVo) {
        if (this.issueDownVos == null){
            this.issueDownVos = new ArrayList<>();
@@ -456,6 +462,12 @@
        this.issueDownVos = issueDownVos;
    }
    public void addIssueDownVo(IssueVo issueVo) {
        if (this.issueDownVos != null) {
            this.issueDownVos.add(issueVo);
        }
    }
    public IssueVo getParentIssueVo() {
        return parentIssueVo;
    }
src/main/java/kr/wisestone/owl/web/condition/CompanyFieldCondition.java
@@ -12,6 +12,8 @@
    private String email;
    private String url;
    private String memo;
    private Long ispId;
    private Long hostingId;
    private Integer Page;
    private Integer PageSize;
@@ -91,4 +93,20 @@
    public void setUrl(String url) {
        this.url = url;
    }
    public Long getIspId() {
        return ispId;
    }
    public void setIspId(Long ispId) {
        this.ispId = ispId;
    }
    public Long getHostingId() {
        return hostingId;
    }
    public void setHostingId(Long hostingId) {
        this.hostingId = hostingId;
    }
}
src/main/java/kr/wisestone/owl/web/condition/IssueCondition.java
@@ -54,6 +54,7 @@
    private List<Long> excludeIds = Lists.newArrayList();
    private List<Long> myDepartmentIds; // 내가 속해있는 부서 ID
    private Boolean hideIssue;
    private Boolean isTree; // 트리구조 모드 일때
    public IssueCondition(){}
@@ -182,6 +183,10 @@
        if (MapUtil.getBoolean(conditions, "hideIssue") != null) {
            condition.setHideIssue(MapUtil.getBoolean(conditions, "hideIssue"));
        }
        if (MapUtil.getBoolean(conditions, "isTree") != null) {
            condition.setTree(MapUtil.getBoolean(conditions, "isTree"));
        }
        return condition;
@@ -510,4 +515,12 @@
    public void setHideIssue(Boolean hideIssue) {
        this.hideIssue = hideIssue;
    }
    public Boolean getTree() {
        return isTree;
    }
    public void setTree(Boolean tree) {
        isTree = tree;
    }
}
src/main/java/kr/wisestone/owl/web/condition/IssueCustomFieldValueCondition.java
@@ -13,6 +13,7 @@
 * Created by wisestone on 2018-06-07.
 */
public class IssueCustomFieldValueCondition {
    private Long issueTypeId;
    private Long workspaceId;
    private Long customFieldId;
    private String customFieldType;
@@ -61,6 +62,14 @@
        return condition;
    }
    public Long getIssueTypeId() {
        return issueTypeId;
    }
    public void setIssueTypeId(Long issueTypeId) {
        this.issueTypeId = issueTypeId;
    }
    public Long getWorkspaceId() {
        return workspaceId;
    }
src/main/java/kr/wisestone/owl/web/controller/ApiController.java
@@ -72,7 +72,6 @@
        Pageable pageable = this.pageUtil.convertPageable(this.getPageVo(params));
        // todo
        return this.setSuccessMessage(resJsonData);
    }
}
src/main/java/kr/wisestone/owl/web/form/CompanyFieldForm.java
@@ -12,6 +12,9 @@
    private String tel;
    private String url;
    private String memo;
    private Long ispId;
    private Long hostingId;
    private List<Long> removeIds = Lists.newArrayList();
    public CompanyFieldForm() {
@@ -57,6 +60,22 @@
        this.memo = memo;
    }
    public Long getIspId() {
        return ispId;
    }
    public void setIspId(Long ispId) {
        this.ispId = ispId;
    }
    public Long getHostingId() {
        return hostingId;
    }
    public void setHostingId(Long hostingId) {
        this.hostingId = hostingId;
    }
    public List<Long> getRemoveIds() {
        return removeIds;
    }
src/main/java/kr/wisestone/owl/web/form/IssueCustomFieldValueForm.java
@@ -1,11 +1,20 @@
package kr.wisestone.owl.web.form;
public class IssueCustomFieldValueForm {
    private Long issueTypeId;
    private Long customFieldId;
    private String useValue;
    public IssueCustomFieldValueForm(){}
    public Long getIssueTypeId() {
        return issueTypeId;
    }
    public void setIssueTypeId(Long issueTypeId) {
        this.issueTypeId = issueTypeId;
    }
    public Long getCustomFieldId() {
        return customFieldId;
    }
src/main/resources/migration/V1_13__Alter_Table.sql
@@ -15,3 +15,13 @@
-- 업체의 url 컬럼 INDEX 추가
ALTER TABLE `company_field` ADD INDEX `urlIndex`(`url`);
ALTER TABLE `company_field` ADD COLUMN `isp_id` bigint(20) DEFAULT NULL;
ALTER TABLE `company_field` ADD COLUMN `hosting_id` bigint(20) DEFAULT NULL;
ALTER TABLE `company_field` ADD INDEX `ispIdIndex`(`isp_id`);
ALTER TABLE `company_field` ADD INDEX `hostingIdIndex`(`hosting_id`);
src/main/resources/mybatis/query-template/companyField-template.xml
@@ -11,7 +11,9 @@
        cf.tel as tel,
        cf.email as email,
        cf.url as url,
        cf.memo as memo
        cf.memo as memo,
        cf.isp_id as ispId,
        cf.hosting_id as hostingId
        FROM
        company_field cf
        WHERE 1=1
src/main/resources/mybatis/query-template/issue-template.xml
@@ -200,7 +200,7 @@
        AND issue.reverse_index <![CDATA[ < ]]> 0
        AND workspace.id = #{workspaceId}
        GROUP BY issue.id
        ORDER BY issue.register_date DESC
        ORDER BY issue.modify_date DESC
        <if test="page != null and !page.equals('')">
            limit #{pageSize} offset #{page};
        </if>
@@ -252,7 +252,7 @@
        LEFT OUTER JOIN (SELECT issue_id, COUNT(id) as issueCommentCount FROM issue_comment GROUP BY issue_id)
        temp_issue_comment on (temp_issue_comment.issue_id = issue.id)
        WHERE 1=1
        <if test="keyWord != null and !keyWord.equals('') ">
        <if test="keyWord != null and !keyWord.equals('') and myDepartmentIds.size != 0">
            AND issue.title like CONCAT('%',#{keyWord},'%')
            OR issue.description like CONCAT('%',#{keyWord},'%')
            OR issue.start_date like CONCAT('%',#{keyWord},'%')
@@ -395,7 +395,7 @@
        AND issue.reverse_index <![CDATA[ < ]]> 0
        AND workspace.id = #{workspaceId}
        GROUP BY issue.id
        ORDER BY issue.register_date DESC
        ORDER BY issue.modify_date DESC
        <if test="page != null and !page.equals('')">
            limit #{pageSize} offset #{page};
        </if>
@@ -808,11 +808,11 @@
    <select id="findByIssueTypeId" resultType="java.util.HashMap"
            parameterType="java.lang.Long">
        SELECT
        i.id as issueId,
        iss.id as issueStatusId,
        iss.name as issueStatusName
            i.id as issueId,
            iss.id as issueStatusId,
            iss.name as issueStatusName
        FROM issue i
        INNER JOIN issue_status iss on iss.id = i.issue_status_id
                 INNER JOIN issue_status iss on iss.id = i.issue_status_id
        WHERE i.issue_type_id = #{issueTypeId}
    </select>
@@ -832,7 +832,7 @@
    <select id="findByProjectId" resultType="java.util.HashMap"
            parameterType="kr.wisestone.owl.web.condition.IssueCondition">
        SELECT
        id
            id
        FROM issue
        WHERE project_id = #{projectId}
    </select>
@@ -955,24 +955,24 @@
    <!--    이슈 유형을 사용하는 이슈 갯수를 조회한다 -->
    <select id="countByIssueTypeId" resultType="java.lang.Long" parameterType="java.lang.Long">
      SELECT COUNT(DISTINCT id) FROM
      issue WHERE issue_type_id = #{issueTypeId};
        SELECT COUNT(DISTINCT id) FROM
            issue WHERE issue_type_id = #{issueTypeId};
    </select>
    <!--    이슈 유형을 사용하는 이슈 갯수를 조회한다(기간) -->
    <select id="countByIssueTypeIdAndDate" resultType="java.lang.Long" parameterType="kr.wisestone.owl.web.condition.IssueTypeCondition">
        SELECT
               COUNT(DISTINCT id)
            COUNT(DISTINCT id)
        FROM issue
        WHERE issue_type_id = #{id}
                    AND register_date BETWEEN #{startDate} AND #{endDate}
                    AND is_api = #{isApi};
          AND register_date BETWEEN #{startDate} AND #{endDate}
          AND is_api = #{isApi};
    </select>
    <!--    이슈 상태를 사용하는 이슈 갯수를 조회한다. -->
    <select id="countByIssueStatusId" resultType="java.lang.Long" parameterType="java.lang.Long">
        SELECT COUNT(DISTINCT id) FROM
        issue WHERE issue_status_id = #{issueStatusId};
            issue WHERE issue_status_id = #{issueStatusId};
    </select>
@@ -985,12 +985,13 @@
            GROUP_CONCAT(customFieldValue.useValue) AS concatUseValue
        FROM issue issue FORCE INDEX(reverseIndex)
        INNER JOIN issue_status as issStatus ON issue.issue_status_id = issStatus.id
        LEFT OUTER JOIN (
            LEFT OUTER JOIN (
            SELECT cf.id AS customFieldId, cf.custom_field_type AS customFieldType, issue_custom.use_value AS useValue, issue_custom.issue_id AS issueId
            FROM issue_custom_field_value issue_custom
            INNER JOIN custom_field cf ON cf.id = issue_custom.custom_field_id
            ORDER BY issue_custom.id ASC) customFieldValue ON customFieldValue.issueId = issue.id
        WHERE issStatus.issue_status_type != 'CLOSE'
          AND issue.issue_type_id = #{issueTypeId}
        GROUP BY issue.id
        HAVING concatUseValue LIKE CONCAT('%', #{useValue}, '%')
    </select>
@@ -999,12 +1000,12 @@
    <!--  종료 안된 하위 이슈 가져오기 -->
    <select id="findNotCompleteByParentIssueId" resultType="java.util.HashMap" parameterType="kr.wisestone.owl.web.condition.IssueCondition">
        SELECT
        iss.id as id,
        iss.title as title
            iss.id as id,
            iss.title as title
        FROM issue iss
        INNER JOIN issue_status issueStatus on iss.issue_status_id = issueStatus.id
                 INNER JOIN issue_status issueStatus on iss.issue_status_id = issueStatus.id
        WHERE iss.parent_issue_id = #{parentIssueId}
        AND iss.id != #{id}
        AND issueStatus.issue_status_type != 'CLOSE'
          AND iss.id != #{id}
          AND issueStatus.issue_status_type != 'CLOSE'
    </select>
</mapper>
src/main/webapp/custom_components/js-tree/js-tree.directive.js
New file
@@ -0,0 +1,36 @@
'use strict';
define(['app'],
    function (app) {
        app.directive('jsTree', ['$log',
            function ($log) {
                return {
                    restrict : 'E',
                    scope : {
                        event : '=',
                        data : '=',
                        tableConfigs : '=',
                        useSort : '=', // 정령 기능 사용 여부
                        detailView : "="    //  이슈 목록 상세형 변경을 위해 사용. 다른 화면은 사용하지 않음.
                    },
                    replace : true,
                    templateUrl : '/custom_components/js-tree/js-tree.html',
                    controller : function ($scope, $element, $attrs) {
                        $scope.fn = {
                            getResponseData : getResponseData
                        }
                        //  테이블 정보 가져오기
                        function getResponseData() {
                            return $scope.data;
                        }
                    },
                    link : function (scope, element, attrs) {
                    },
                };
            }])
    });
src/main/webapp/custom_components/js-tree/js-tree.html
New file
@@ -0,0 +1,16 @@
<ul>
    <li ng-repeat="row in fn.getResponseData()">
        <span>{{row.title}}</span>
        <ul>
            <li ng-repeat="downRow in row.issueDownVos">
                <span>{{downRow.title}}</span>
            </li>
        </ul>
    </li>
    <li ng-if="fn.getResponseData().length == 0">
        <span translate="common.noData">데이터가 없습니다.</span>
    </li>
</ul>
src/main/webapp/custom_components/js-tree/tree.provider.js
New file
@@ -0,0 +1,234 @@
'use strict';
define(['app', 'angular'],
    function (app, angular) {
        app.provider("$treeProvider", function () {
            return {
                $get : function ($log) {
                    return {
                        config : function () {
                            var tableConfig = {
                                hName : "",    //    헤더 이름
                                hWidth : "",    //    칼럼 길이
                                hChecked : false,    //    체크 박스 선택 여부
                                hAlign : "text-center",    //    헤더 정렬 기준
                                hSort : true,    //    정렬 가능 여부
                                dName : "",    //    데이터 이름
                                dAlign : "text-left",    //    데이터 정렬 기준
                                dRenderer : "",    //    렌더러 여부
                                dVisible : "",  //      bootstrap 반응형 컬럼 표시 여부
                                dType : "none",        // 태그 타입
                                dDateFormat : "",   //  날짜 형식
                                rowSpan : 0,    //  rowspan 을 지원한다.
                                colSpan : 0,    //  colspan 을 지원한다.
                                subHead : false,    //  만약 rowspan, colspan 을 사용하게 되면 true 로 셋팅.
                                columnHint : "",    //  컬럼 정보를 추출하기 위한 힌트 정보를 준다 - tableColumnGenerator 의 사용자 정의 필드 부분에서 사용
                                setHName : function (hName) {
                                    this.hName = hName;
                                    return this;
                                },
                                setHWidth : function (hWidth) {
                                    this.hWidth = hWidth;
                                    return this;
                                },
                                setHChecked : function (hChecked) {
                                    this.hChecked = hChecked;
                                    return this;
                                },
                                setHAlign : function (hAlign) {
                                    this.hAlign = hAlign;
                                    return this;
                                },
                                setHSort : function (hSort) {
                                    this.hSort = hSort;
                                    return this;
                                },
                                setDName : function (dName) {
                                    this.dName = dName;
                                    return this;
                                },
                                setDAlign : function (dAlign) {
                                    this.dAlign = dAlign;
                                    return this;
                                },
                                setDRenderer : function (dRenderer) {
                                    this.dRenderer = dRenderer;
                                    return this;
                                },
                                setDVisible : function (dVisible) {
                                    this.dVisible = dVisible;
                                    return this;
                                },
                                setDType : function (dType) {
                                    this.dType = dType;
                                    return this;
                                },
                                setDDateFormat : function (dDateFormat) {
                                    this.dDateFormat = dDateFormat;
                                    return this;
                                },
                                setRowSpan : function (dRowSpan) {
                                    this.rowSpan = dRowSpan;
                                    return this;
                                },
                                setColSpan : function (dColSpan) {
                                    this.colSpan = dColSpan;
                                    return this;
                                },
                                setSubHead : function (dSubHead) {
                                    this.subHead = dSubHead;
                                    return this;
                                },
                                setColumnHint : function (dColumnHint) {
                                    this.columnHint = dColumnHint;
                                    return this;
                                }
                            };
                            return tableConfig;
                        },
                        toggleChecked : function (checkStatus, datas) {
                            //  전체 선택 체크 박스를 클릭했을 경우
                            angular.forEach(datas, function (data) {
                                if (angular.isDefined(data.defaultYn)) {
                                    if (!data.defaultYn) {
                                        data.checked = checkStatus;
                                    }
                                }
                                else {
                                    data.checked = checkStatus;
                                }
                            });
                        },
                        radioChecked : function (target, datas) {
                            //  해당 row 가 라디오 버튼일 경우
                            angular.forEach(datas, function (data) {
                                if (target.id == data.id) {
                                    data.checked = true;
                                }
                                else {
                                    data.checked = false;
                                }
                            });
                        },
                        rowChecked : function (tableConfig, target, datas) {
                            //  각 row 의 체크박스/라디오 버튼을 클릭했을 경우
                            if (tableConfig[0].dType == "checkbox") {
                                target.checked = !target.checked;
                                for (var data in datas) {
                                    if (!data.checked) {
                                        this.hChecked = false;
                                        break;
                                    }
                                }
                            }
                            else if (tableConfig[0].dType == "radio") {
                                this.radioChecked(target, datas);
                            }
                        },
                        orderByColumn : "",  // table order By column name
                        reverse : true,
                        setOrderByColumn : function (column) {
                            if (column == "") {
                                return;
                            }
                            if (this.orderByColumn == column) {
                                this.reverse = !this.reverse;
                            }
                            else {
                                this.reverse = true;
                            }
                            this.orderByColumn = column;
                            return this;
                        },
                        getDateFormat : function (formatType, date) {
                            if (formatType == "" || formatType == null) {
                                formatType = "01";
                            }
                            Date.prototype.format = function (f) {
                                if (!this.valueOf()) {
                                    return " ";
                                }
                                var weekName = ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"];
                                var d = this;
                                return f.replace(/(yyyy|yy|MM|dd|E|hh|mm|ss|a\/p)/gi, function ($1) {
                                    switch ($1) {
                                        case "yyyy":
                                            return d.getFullYear();
                                        case "yy":
                                            return (d.getFullYear() % 1000).zf(2);
                                        case "MM":
                                            return (d.getMonth() + 1).zf(2);
                                        case "dd":
                                            return d.getDate().zf(2);
                                        case "E":
                                            return weekName[d.getDay()];
                                        case "HH":
                                            return d.getHours().zf(2);
                                        case "hh":
                                            var h = d.getHours();
                                            return ((h = d.getHours() % 12) ? h : 12).zf(2);
                                        case "mm":
                                            return d.getMinutes().zf(2);
                                        case "ss":
                                            return d.getSeconds().zf(2);
                                        case "a/p":
                                            return d.getHours() < 12 ? "오전" : "오후";
                                        default:
                                            return $1;
                                    }
                                });
                            };
                            String.prototype.string = function (len) {
                                var s = '', i = 0;
                                while (i++ < len) {
                                    s += this;
                                }
                                return s;
                            };
                            String.prototype.zf = function (len) {
                                return "0".string(len - this.length) + this;
                            };
                            Number.prototype.zf = function (len) {
                                return this.toString().zf(len);
                            };
                            var dateFormat = "";
                            var dynamicTime = false;
                            var today = new Date().format("yyyy-MM-dd");
                            var compareDate = new Date(date).format("yyyy-MM-dd");
                            if (today == compareDate) {
                                dynamicTime = true;
                            }
                            switch (formatType) {
                                case "01":  //  날짜
                                    dateFormat = "yyyy-MM-dd";
                                    break;
                                case "02":  //  날짜 + 시간
                                    dateFormat = "yyyy-MM-dd HH:mm";
                                    break;
                                case "03":  //  유동적 표시
                                    if (dynamicTime) {
                                        dateFormat = "HH:mm";
                                    }
                                    else {
                                        dateFormat = "yyyy-MM-dd HH:mm";
                                    }
                                    break;
                            }
                            return dateFormat;
                        }
                    }
                }
            }
        });
    });
src/main/webapp/custom_components/js-tree/treeColumnGenerator.directive.js
New file
@@ -0,0 +1,64 @@
'use strict';
define(['app', 'angular'],
    function (app, angular) {
        app.directive('treeColumnGenerator', ['$compile', '$log', '$rootScope', '$tableProvider', '$filter',
            function ($compile, $log, $rootScope, $tableProvider, $filter) {
                return {
                    restrict : "A",
                    compile : function (tElement, tAttrs) {
                        return function (scope, element, attrs) {
                            scope.data = scope[attrs["treeColumnGenerator"]];
                            var makeTag = "";
                            var treeStartToken = "└";
                            scope.tableConfigs.forEach(function (tableConfig, index) {
                                if (tableConfig.colSpan > 0) {
                                    return;
                                }
                                makeTag = '<li class="' + tableConfig.dAlign + ' ' + tableConfig.dVisible + '">';
                                if (tableConfig.dType === "checkbox") {
                                    //  체크 박스일때
                                    if (scope.data.defaultYn) {
                                        makeTag += '<input type="checkbox" ng-checked="data.checked == true ? true : false" disabled ng-click="$root.$tableProvider.rowChecked(tableConfigs, data, fn.getResponseData())">';
                                    }
                                    else {
                                        makeTag += '<input type="checkbox" ng-checked="data.checked == true ? true : false" ng-click="$root.$tableProvider.rowChecked(tableConfigs, data, fn.getResponseData())">';
                                    }
                                    tableConfig.hChecked = false;
                                }
                                else if (tableConfig.dType === "radio") {
                                    //  라디오 버튼일때
                                    makeTag += '<input type="radio" ng-checked="data.checked == true ? true : false" ng-click="$root.$tableProvider.rowChecked(tableConfigs, data, fn.getResponseData())">';
                                }
                                else if (tableConfig.dType === "date") {
                                    makeTag += $filter('date')(scope.data[tableConfig.dName], $tableProvider.getDateFormat(tableConfig.dDateFormat, scope.data[tableConfig.dName]));
                                }
                                else {
                                    if (angular.isDefined(scope.data[tableConfig.dName]) && scope.data[tableConfig.dName] != null) {
                                        makeTag += '<span>' + scope.data[tableConfig.dName] + '</span>';
                                    }
                                    else {
                                        makeTag += '<span></span>';
                                    }
                                }
                                makeTag += '</li>';
                                var linkFn = $compile(makeTag);
                                var content = linkFn(scope);
                                element.append(content);
                            });
                        }
                    }
                }
            }]);
    });
src/main/webapp/i18n/ko/global.json
@@ -925,6 +925,7 @@
    },
    "ispField" : {
        "info": "ISP정보",
        "select": "ISP 선택",
        "add" : "ISP생성",
        "name" : "ISP이름",
        "ispName" : "ISP명",
@@ -944,6 +945,7 @@
    },
    "hostingField" : {
        "info": "호스팅정보",
        "select": "호스팅 선택",
        "add" : "호스팅생성",
        "name" : "호스팅이름",
        "manager" : "호스팅이름",
src/main/webapp/scripts/app/companyField/companyFieldAdd.controller.js
@@ -21,12 +21,10 @@
                $scope.vm = {
                    form : {
                        name : "",  //업체명
                        ispId : "",
                        ispName : "",
                        hostingName : "",
                        //companyType : "", //업체분류
                        //profitYN : "",  //영리/비영리
                        //industry : "",  //산업분류
                        //domain : "",  //도메인
                        hostingId : "",
                        manager : "",   //담당자
                        tel : "",  //전화번호
                        email : "",  //이메일
@@ -44,7 +42,6 @@
                        }
                    }
                };
                angular.extend(this, $controller('autoCompleteController', {$scope : $scope, $injector : $injector}));
@@ -67,18 +64,34 @@
                    return false;
                }
                $scope.$on("ispFieldEvent", function (event, result) {
                    $scope.vm.form.ispId = result[0].id;
                });
                $scope.$on("hostingFieldEvent", function (event, result) {
                    $scope.vm.form.hostingId = result[0].id;
                });
                //  폼 전송
                function formSubmit(condition) {
                    $rootScope.spinner = true;
                    var content = {
                        name : $rootScope.preventXss($scope.vm.form.name),    //  업체명
                        ispName : $rootScope.preventXss($scope.vm.form.ispName),    //  업체명
                        hostingName : $rootScope.preventXss($scope.vm.form.hostingName),    //  업체명
                        //companyType : $scope.vm.form.companyType, //업체분류
                        //profitYN : $scope.vm.form.profitYN,  //영리/비영리
                        //industry : $scope.vm.form.industry,  //산업분류
                        //domain : $scope.vm.form.domain,  //도메인
                        ispId : (function () {  // ISP 아이디
                            var ispId = -1;
                            if ($scope.vm.form.issueIspFields != null) {
                                ispId = $scope.vm.form.ispId;
                            }
                            return ispId;
                        })(),
                        hostingId : (function () {  // Hosting 아이디
                            var hostingId = -1;
                            if ($scope.vm.form.issueHostingFields != null) {
                                hostingId = $scope.vm.form.hostingId;
                            }
                            return hostingId;
                        })(),
                        manager : $scope.vm.form.manager,   //담당자
                        tel : $scope.vm.form.tel,  //전화번호
                        email : $scope.vm.form.email,  //이메일
src/main/webapp/scripts/app/companyField/companyFieldModify.controller.js
@@ -8,27 +8,56 @@
        'angular'
    ],
    function (app, angular) {
        app.controller('companyFieldModifyController', ['$scope', '$rootScope', '$log', '$resourceProvider', '$uibModalInstance', 'CompanyField', 'parameter', 'SweetAlert', '$filter', '$q',
            function ($scope, $rootScope, $log, $resourceProvider, $uibModalInstance, CompanyField, parameter, SweetAlert, $filter, $q) {
        app.controller('companyFieldModifyController', ['$scope', '$rootScope', '$log', '$resourceProvider', '$uibModalInstance', 'CompanyField', 'parameter', 'SweetAlert', '$filter', '$q', '$injector','$controller',
            function ($scope, $rootScope, $log, $resourceProvider, $uibModalInstance, CompanyField, parameter, SweetAlert, $filter, $q, $injector, $controller) {
                $scope.fn = {
                    detail : detail,  //  상세 조회
                    cancel : cancel,    //  팝업 창 닫기
                    formSubmit : formSubmit,    //  폼 전송
                    formCheck : formCheck,  //  폼 체크
                    getIssueIspFieldListCallBack : getIssueIspFieldListCallBack,
                    getIssueHostingFieldListCallBack : getIssueHostingFieldListCallBack
                };
                $scope.vm = {
                    id : parameter.id,
                    form : {
                        name : "",  //업체명
                        ispId : "",
                        ispName : "",
                        hostingName : "",
                        hostingId : "",
                        manager : "",   //담당자
                        tel : "",  //전화번호
                        email : "",  //이메일
                        url : "", // url
                        memo : ""  //메모(비고)
                    },
                    autoCompletePage : {
                        ispField : {
                            page : 0,
                            totalPage : 0
                        },
                        hostingField : {
                            page : 0,
                            totalPage : 0
                        }
                    }
                };
                angular.extend(this, $controller('autoCompleteController', {$scope : $scope, $injector : $injector}));
                // 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) {
@@ -38,6 +67,14 @@
                    return false;
                }
                $scope.$on("ispFieldEvent", function (event, result) {
                    $scope.vm.form.ispId = result[0].id;
                });
                $scope.$on("hostingFieldEvent", function (event, result) {
                    $scope.vm.form.hostingId = result[0].id;
                });
                //  폼 전송
                function formSubmit() {
                    $rootScope.spinner = true;
@@ -45,10 +82,20 @@
                    var content = {
                        id : parameter.id,
                        name : $rootScope.preventXss($scope.vm.form.name),
                        /*companyType : $rootScope.preventXss($scope.vm.form.companyType),
                        profitYN : $rootScope.preventXss($scope.vm.form.profitYN),
                        industry : $rootScope.preventXss($scope.vm.form.industry),
                        domain : $rootScope.preventXss($scope.vm.form.domain),*/
                        ispId : (function () {  // ISP 아이디
                            var ispId = -1;
                            if ($scope.vm.form.issueIspFields.length > 0) {
                                ispId = $scope.vm.form.ispId;
                            }
                            return ispId;
                        })(),
                        hostingId : (function () {  // Hosting 아이디
                            var hostingId = -1;
                            if ($scope.vm.form.issueHostingFields.length > 0) {
                                hostingId = $scope.vm.form.hostingId;
                            }
                            return hostingId;
                        })(),
                        manager : $rootScope.preventXss($scope.vm.form.manager),
                        tel : $rootScope.preventXss($scope.vm.form.tel),
                        email : $rootScope.preventXss($scope.vm.form.email),
@@ -102,6 +149,12 @@
                                $scope.vm.form.tel = result.data.content.tel;
                                $scope.vm.form.url = result.data.content.url;
                                $scope.vm.form.memo = result.data.content.memo;
                                if(result.data.content.ispFieldVo != null){
                                    $scope.vm.form.ispName = result.data.content.ispFieldVo.name;
                                }
                                if(result.data.content.hostingFieldVo != null){
                                    $scope.vm.form.hostingName = result.data.content.hostingFieldVo.name;
                                }
                            }
                        }
                        else {
src/main/webapp/scripts/app/issue/issue.js
@@ -34,7 +34,7 @@
                            var deferred = $q.defer();
                            require([
                                'issueListTimelineController', 'issueManagerController', 'issueListController', 'issueAddController', 'issueModifyController', 'issueDetailController', 'issueAddRelationController', 'issueImportExcelController',
                                'chartLoader', 'jsTable', 'tableColumnGenerator', 'modalFormAutoScroll', 'summerNote', 'summerNote-ko-KR', 'fullScroll', 'workflowService', 'priorityService', 'issueSearchService', 'issueTableConfigService', 'inputRegex',
                                'chartLoader', 'jsTable', 'jsTree', 'tableColumnGenerator', 'treeColumnGenerator', 'modalFormAutoScroll', 'summerNote', 'summerNote-ko-KR', 'fullScroll', 'workflowService', 'priorityService', 'issueSearchService', 'issueTableConfigService', 'inputRegex',
                                'severityService', 'issueTypeService', 'issueTypeCustomFieldService', 'issueService', 'issueStatusService', 'issueUserService','issueDepartmentService','issueModifyUserController', 'issueModifyDepartmentController', 'customFieldService', 'issueSearchFieldKeyViewElement',
                                'issueSearchCustomFieldViewElement', 'tableUserImage', 'fullScroll', 'issueCommentService', 'detectIssueEditor', 'formSubmit', 'issueModifyStatusController', 'downIssueModifyStatusController', 'jsShortCut',
                                'issueAddTableConfigController','issueAddRelationTableConfigController','issueAddDownTableConfigController','domAppend', 'issueDetailImagePreview', 'issueSendMailPartnersController', 'htmlDiff', 'issueVersionViewController', 'issueVersionService',
src/main/webapp/scripts/app/issue/issueAdd.controller.js
@@ -388,8 +388,8 @@
                // 업체정보 결과 값 Event 처리(set)
                $scope.$on("companyFieldEvent", function (event, result) {
                    var ispFieldVos = result[0].ispFieldVos[0];
                    var hostingFieldVos = result[0].hostingFieldVos[0];
                    var ispFieldVo = result[0].ispFieldVo;
                    var hostingFieldVo = result[0].hostingFieldVo;
                    $scope.vm.companyId = result[0].id;
                    $scope.vm.companyName = result[0].name;
@@ -399,34 +399,34 @@
                    $scope.vm.companyUrl = result[0].url;
                    $scope.vm.companyMemo = result[0].memo;
                    $scope.vm.ispName = ispFieldVos.name;
                    $scope.vm.ispCode = ispFieldVos.code;
                    $scope.vm.ispManager = ispFieldVos.manager;
                    $scope.vm.ispTel = ispFieldVos.tel;
                    $scope.vm.ispEmail = ispFieldVos.email;
                    $scope.vm.ispUrl = ispFieldVos.url;
                    $scope.vm.ispMemo = ispFieldVos.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 = hostingFieldVos.name;
                    $scope.vm.hostingCode = hostingFieldVos.code;
                    $scope.vm.hostingManager = hostingFieldVos.manager;
                    $scope.vm.hostingTel = hostingFieldVos.tel;
                    $scope.vm.hostingEmail = hostingFieldVos.email;
                    $scope.vm.hostingUrl = hostingFieldVos.url;
                    $scope.vm.hostingMemo = hostingFieldVos.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;
                });
                // 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;
                // });
                //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) {
src/main/webapp/scripts/app/issue/issueList.controller.js
@@ -8,9 +8,9 @@
        'angular'
    ],
    function (app, angular) {
        app.controller('issueListController', ['$scope', '$rootScope', '$log', '$resourceProvider', '$tableProvider', '$state', '$uibModal', '$q',
        app.controller('issueListController', ['$scope', '$rootScope', '$log', '$resourceProvider', '$tableProvider', '$treeProvider', '$state', '$uibModal', '$q',
            '$controller', '$injector', 'SweetAlert', 'Issue', 'IssueType', 'Priority', 'Severity', 'IssueStatus', 'CustomField', 'IssueSearch', 'IssueTableConfig', '$timeout', '$filter',
            function ($scope, $rootScope, $log, $resourceProvider, $tableProvider, $state, $uibModal, $q, $controller, $injector, SweetAlert, Issue, IssueType, Priority, Severity, IssueStatus, CustomField,
            function ($scope, $rootScope, $log, $resourceProvider, $tableProvider, $treeProvider, $state, $uibModal, $q, $controller, $injector, SweetAlert, Issue, IssueType, Priority, Severity, IssueStatus, CustomField,
                      IssueSearch, IssueTableConfig, $timeout, $filter) {
                //  함수
@@ -20,6 +20,7 @@
                    changePageRowCount : changePageRowCount,    //  페이지 변경
                    makeTableConfigs : makeTableConfigs, //  테이블 설정
                    setTableColumn : setTableColumn,    //  테이블의 컬럼을 만들어준다.
                    setTreeColumn : setTreeColumn,
                    add : add,   //  이슈 생성
                    modify : modify,    //  이슈 수정
                    addRelationIssueForm : addRelationIssueForm,    //  연관 이슈 추가
@@ -39,7 +40,8 @@
                    makeSearchConditions : makeSearchConditions,    //  검색 조건을 만든다.
                    getIssueTableConfigs : getIssueTableConfigs,  //  사용자 이슈 목록 테이블 설정 값을 가져와서 적용한다.
                    startExecute : startExecute, //  컨트롤 로딩시 처음으로 시작되는 함수
                    getResponseData : getResponseData //  컨트롤 로딩시 처음으로 시작되는 함수
                    getResponseData : getResponseData, //  컨트롤 로딩시 처음으로 시작되는 함수
                    onClickListMode : onClickListMode   // 리스트 모드 변경시 실행되는 함수
                };
                //  변수
@@ -67,6 +69,7 @@
                        selectedPageRowCount : String(10)
                    },
                    tableConfigs : [],  //  테이블 셋팅 정보
                    treeConfigs : [], // 트리 셋팅 정보
                    responseData : {
                        data : []
                    },
@@ -85,7 +88,8 @@
                    customFields : [],  //  사용자 정의 필드
                    issueTableConfigs : [],  //  이슈 테이블 설정
                    parentIssueId : "",
                    hideIssue : false
                    hideIssue : false,
                    listMode : 0, // 목록 모드 0:기본 리스트 1:트리구조 리스트
                };
                //  테이블 이벤트
@@ -233,8 +237,109 @@
                        if (issueTableConfig.display) {
                            //  테이블의 컬럼을 만들어준다.
                            $scope.fn.setTableColumn(issueTableConfig);
                            $scope.fn.setTreeColumn(issueTableConfig);
                        }
                    });
                }
                //  트리 컬럼을 만들어준다.
                function setTreeColumn(issueTableConfig) {
                    //  일반 컬럼
                    switch(issueTableConfig.key) {
                        case "ISSUE_TITLE" :   //  이슈 제목
                            $scope.vm.treeConfigs.push($treeProvider.config()
                                .setHName("issue.issueTitle")
                                .setDType("renderer")
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("ISSUE_TITLE"));
                            break;
                        case "PRIORITY" :   //  우선순위
                            $scope.vm.treeConfigs.push($treeProvider.config()
                                .setHName("common.priority")
                                .setDName("priorityName")
                                .setDType("renderer")
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("COMMON_PRIORITY"));
                            break;
                        case "SEVERITY" :   //  중요도
                            $scope.vm.treeConfigs.push($treeProvider.config()
                                .setHName("common.importance")
                                .setDName("severityName")
                                .setDType("renderer")
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("COMMON_SEVERITY"));
                            break;
                        case "ISSUE_TYPE" : //  이슈 타입
                            $scope.vm.treeConfigs.push($treeProvider.config()
                                .setHName("issue.issueType")
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDName("issueTypeName"));
                            break;
                        case "ASSIGNEE_TEAM" :   //  담당부서
                            $scope.vm.treeConfigs.push($treeProvider.config()
                                .setHName("common.assigneeTeam")
                                .setDType("renderer")
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("ISSUE_DEPARTMENT"));
                            break;
                        case "REGISTER" :   //  등록자
                            $scope.vm.treeConfigs.push($treeProvider.config()
                                .setHName("common.register")
                                .setDType("renderer")
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("REGISTER"));
                            break;
                        case "PERIOD" : //  기간
                            $scope.vm.treeConfigs.push($treeProvider.config()
                                .setHName("common.period")
                                .setDType("renderer")
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("ISSUE_DUE_DATE"));
                            break;
                        case "MODIFY_DATE" : //  최근 변경일
                            $scope.vm.treeConfigs.push($treeProvider.config()
                                .setHName("common.lastChangeDate")
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDName("modifyDate"));
                            break;
                        case "COUNT_DOWN_ISSUE" : //  하위 이슈 개수
                            $scope.vm.treeConfigs.push($treeProvider.config()
                                .setHName("common.countDownIssue")
                                .setDType("renderer")
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("DOWN_ISSUE_COUNT"));
                            break;
                    }
                    //  사용자 정의 필드 컬럼
                    if (issueTableConfig.key.indexOf("CUSTOM_FIELD_") !== -1) {
                        //  만약 이슈 테이블 컬럼명이 표시되지 않으면 이쪽이 문제
                        for (var count in $scope.vm.customFields) {
                            var customField = $scope.vm.customFields[count];
                            if (customField.id === Number(issueTableConfig.key.substring(13))) {
                                $scope.vm.treeConfigs.push($treeProvider.config()
                                    .setHName(customField.name)
                                    .setDType("renderer")
                                    .setHWidth("bold " + issueTableConfig.width)
                                    .setDAlign("text-center")
                                    .setColumnHint(customField)
                                    .setDRenderer("ISSUE_CUSTOM_FIELD_VALUE_VIEW"));
                                break;
                            }
                        }
                    }
                }
                //  테이블의 컬럼을 만들어준다.
@@ -367,6 +472,7 @@
                        beginCompleteDate : "",
                        endCompleteDate : "",
                        hideIssue : $scope.vm.hideIssue,
                        isTree : $scope.vm.listMode === 1,
                        projectIds : (function () {
                            var projectIds = [];
@@ -485,6 +591,10 @@
                    }
                    return conditions;
                }
                function getTreeList() {
                }
                //  이슈 목록을 조회한다.
@@ -1055,6 +1165,19 @@
                    return deferred.promise;
                }
                // 리스트 모드 변경시 실행
                function onClickListMode(listMode) {
                    if ($scope.vm.listMode !== listMode) {
                        $scope.vm.listMode = listMode;
                        if ($scope.vm.listMode === 0) {
                            $scope.fn.getPageList(0);
                        } else {
                            $scope.fn.getPageList(0);
                        }
                    }
                }
                //  최초 실행
                function startExecute() {
                    // 파라미터 읽기
src/main/webapp/scripts/main.js
@@ -55,12 +55,14 @@
        'angularTranslate' : '../custom_components/angular-translate/angular-translate', //  다국어 처리에 사용
        'commonController' : 'app/common/common.controller', //  공통 컨트롤러
        'tableProvider' : '../custom_components/js-table/table.provider', //  테이블 속성 값을 관리한다.
        'treeProvider' : '../custom_components/js-tree/tree.provider', //   트리 속성 값을 관리한다.
        'resourceProvider' : 'components/utils/resource.provider',   //  공통적으로 서버 json 전송에 사용
        'lodash' : '../bower_components/lodash/lodash.min', //  멀티 셀렉트, auto complete 컴포넌트들에서 사용
        'angularDropMultiSelect' : '../custom_components/angular-multi-select/angularjs-dropdown-multiselect',  //  멀티 셀렉트 컴포넌트
        'jsTable' : '../custom_components/js-table/js-table.directive',   //  목록 화면에서 사용되는 테이블을 호출한다.
        'jsTreeTable' : '../custom_components/js-table/js-tree-table.directive',   //  목록 화면에서 사용되는 테이블(트리구조)을 호출한다.
        'jsTree' : '../custom_components/js-tree/js-tree.directive',   //  목록 화면에서 사용되는 테이블(트리구조)을 호출한다.
        'tableColumnGenerator' : '../custom_components/js-table/tableColumnGenerator.directive', //  테이블 랜더러를 담당한다.
        'treeColumnGenerator' : '../custom_components/js-tree/treeColumnGenerator.directive', //  테이블 랜더러를 담당한다.
        'jsAutoCompleteMulti' : '../custom_components/js-autocomplete-multi/js-autocomplete-multi', //  다중 선택이 가능한 autoComplete 컴포넌트
        'jsInputAutoComplete' : '../custom_components/js-input-autocomplete/js-input-autocomplete',   //  input 박스에 autoComplete 기능이 붙은 컴포넌트
        'jsAutoCompleteSingle' : '../custom_components/js-autocomplete-single/js-autocomplete-single',   //  input 박스에 한개의 대상만 선택 가능할수 있는 autoComplete 기능이 붙은 컴포넌트
@@ -400,10 +402,13 @@
        'jsTable' : {
            deps : ['app']
        },
        'jsTreeTable' : {
        'jsTree' : {
            deps : ['app']
        },
        'tableColumnGenerator' : {
            deps : ['app']
        },
        'treeColumnGenerator' : {
            deps : ['app']
        },
        'ngStomp' : {
@@ -523,6 +528,7 @@
    'authInterceptor',
    'resourceProvider',
    'tableProvider',
    'treeProvider',
    'permissionService',
    'authService',
    'userInviteService',
src/main/webapp/views/companyField/companyFieldAdd.html
@@ -32,14 +32,15 @@
            <div class="form-group">
                <label class="issue-label">
                    <span translate="ispField.name">ISP 이름</span>
                    <span translate="ispField.select">ISP 선택</span>
                </label>
                <js-autocomplete-single data-input-name="ispField"
                                    selected-model="vm.form.issueIspFields"
                                    ng-model="vm.form.ispName"
                                    search="vm.form.ispName"
                                    source="fn.getIssueIspFieldList(vm.ispName, vm.form.issueIspFields, vm.autoCompletePage.issueIspFields.page, fn.getIssueIspFieldListCallBack)"
                                    page="vm.autoCompletePage.IspField.page"
                                    total-page="vm.autoCompletePage.IspField.totalPage"
                                    page="vm.autoCompletePage.ispField.page"
                                    total-page="vm.autoCompletePage.ispField.totalPage"
                                    input-disabled="false"
                                    translation-texts="{ empty : 'common.emptyHosting' }"
                                    broad-cast="ispFieldEvent"
@@ -49,7 +50,7 @@
            <div class="form-group">
                <label class="issue-label">
                    <span translate="hostingField.name">호스팅 이름</span>
                    <span translate="hostingField.select">호스팅 선택</span>
                </label>
                <js-autocomplete-single data-input-name="hostingField"
                                    selected-model="vm.form.issueHostingFields"
src/main/webapp/views/companyField/companyFieldModify.html
@@ -29,6 +29,41 @@
                       required>
                <small translate="companyField.enterSpecialCharacters">업체 이름에는 특수 문자를 입력 할수 없습니다.</small>
            </div>
            <div class="form-group">
                <label class="issue-label">
                    <span translate="ispField.select">ISP 선택</span>
                </label>
                <js-autocomplete-single data-input-name="ispField"
                                        selected-model="vm.form.issueIspFields"
                                        ng-model="vm.form.ispName"
                                        search="vm.form.ispName"
                                        source="fn.getIssueIspFieldList(vm.ispName, vm.form.issueIspFields, vm.autoCompletePage.issueIspFields.page, fn.getIssueIspFieldListCallBack)"
                                        page="vm.autoCompletePage.ispField.page"
                                        total-page="vm.autoCompletePage.ispField.totalPage"
                                        input-disabled="false"
                                        translation-texts="{ empty : 'common.emptyHosting' }"
                                        broad-cast="ispFieldEvent"
                                        extra-settings="{ displayProp : 'name' , idProp : 'id', imageable : false, imagePathProp : '',
                    type : '', maxlength : 200, autoResize : false, stopRemoveBodyEvent : true }"></js-autocomplete-single>
            </div>
            <div class="form-group">
                <label class="issue-label">
                    <span translate="hostingField.select">호스팅 선택</span>
                </label>
                <js-autocomplete-single data-input-name="hostingField"
                                        selected-model="vm.form.issueHostingFields"
                                        ng-model="vm.form.hostingName"
                                        search="vm.form.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="form-group">
                    <label for="companyFieldAddForm10" class="issue-label">
src/main/webapp/views/issue/issueAdd.html
@@ -373,7 +373,7 @@
                <div class="col-lg-8 fontcolor_green">
                    <label class="issue-label"><span class="fontcolor_green" translate="companyField.info">업체정보</span>
                        &nbsp;&nbsp;<span class="select3-selection__choice" style="position: relative; bottom: 2px;"><code class="highlighter-rouge">*</code>&nbsp;
                            &nbsp;업체 이름 클릭시 선택된 이름의 업체 정보가 조회되며, 같은 url을 가지고있는 ISP, 호스팅 정보를 불러옵니다.</span>
                            &nbsp;업체 이름 클릭시 선택된 이름의 업체 정보가 조회되며, 업체정보에서 추가한 ISP, 호스팅 정보를 불러옵니다.</span>
                    </label>
                </div>
            </div>
src/main/webapp/views/issue/issueListNormal.html
@@ -3,312 +3,312 @@
        <div class="col-sm-12" >
            <div class="element-wrapper" ng-if="!vm.detailView">
                <div class="element-box">
                  <div class="row" >
                    <div class="col-sm-12">
                        <div class="searchdiv">
                            <form name="issueSearchForm" role="form" ng-enter="fn.getPageList(0)">
                                <div class="row">
                                    <div class="col-sm-6">
                                        <div class="input-group">
                                            <input class="form-control"
                                                   type="text"
                                                   tabindex="-1"
                                                   maxlength="300"
                                                   kr-input
                                                   owl-auto-focus
                                                   ng-model="vm.search.keyWord"
                                                   placeholder="{{'issue.pleaseEnterIssueKeyWord' | translate}}">
                                            <div class="input-group-prepend ml-10">
                                                <button class="btn btn-navy" ng-click="fn.getPageList(0)"> <span translate="common.search">검색</span></button>
                    <div class="row" >
                        <div class="col-sm-12">
                            <div class="searchdiv">
                                <form name="issueSearchForm" role="form" ng-enter="fn.getPageList(0)">
                                    <div class="row">
                                        <div class="col-sm-6">
                                            <div class="input-group">
                                                <input class="form-control"
                                                       type="text"
                                                       tabindex="-1"
                                                       maxlength="300"
                                                       kr-input
                                                       owl-auto-focus
                                                       ng-model="vm.search.keyWord"
                                                       placeholder="{{'issue.pleaseEnterIssueKeyWord' | translate}}">
                                                <div class="input-group-prepend ml-10">
                                                    <button class="btn btn-navy" ng-click="fn.getPageList(0)"> <span translate="common.search">검색</span></button>
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                                <div class="row">
                                    <div class="col-sm-12">
                                        <div class="tabfilter">
                                            <div translate="common.selectedSearchCriteria">선택한 검색 조건</div>
                                            <!--    이슈 제목   -->
                                            <p ng-if="$root.isDefined(vm.search.title)">
                                                {{vm.search.title}}
                                                <span ng-click="vm.search.title = ''">×</span>
                                            </p>
                                    <div class="row">
                                        <div class="col-sm-12">
                                            <div class="tabfilter">
                                                <div translate="common.selectedSearchCriteria">선택한 검색 조건</div>
                                                <!--    이슈 제목   -->
                                                <p ng-if="$root.isDefined(vm.search.title)">
                                                    {{vm.search.title}}
                                                    <span ng-click="vm.search.title = ''">×</span>
                                                </p>
                                            <!--    프로젝트    -->
                                            <issue-search-array-view-element lists="vm.projects"
                                                                             type="'project'"></issue-search-array-view-element>
                                                <!--    프로젝트    -->
                                                <issue-search-array-view-element lists="vm.projects"
                                                                                 type="'project'"></issue-search-array-view-element>
                                            <!--    이슈 타입   -->
                                            <issue-search-field-key-view-element lists="vm.issueTypes"
                                                                                 keys="vm.search.issueTypeIds"></issue-search-field-key-view-element>
                                                <!--    이슈 타입   -->
                                                <issue-search-field-key-view-element lists="vm.issueTypes"
                                                                                     keys="vm.search.issueTypeIds"></issue-search-field-key-view-element>
                                            <!--    이슈 상태   -->
                                            <issue-search-field-key-view-element lists="vm.issueStatuses"
                                                                                 keys="vm.search.issueStatusIds"></issue-search-field-key-view-element>
                                                <!--    이슈 상태   -->
                                                <issue-search-field-key-view-element lists="vm.issueStatuses"
                                                                                     keys="vm.search.issueStatusIds"></issue-search-field-key-view-element>
                                            <!--    이슈 내용   -->
                                            <p ng-if="$root.isDefined(vm.search.description)">
                                                {{vm.search.description}}
                                                <span ng-click="vm.search.description = ''">×</span>
                                            </p>
                                                <!--    이슈 내용   -->
                                                <p ng-if="$root.isDefined(vm.search.description)">
                                                    {{vm.search.description}}
                                                    <span ng-click="vm.search.description = ''">×</span>
                                                </p>
                                            <!--    우선 순위   -->
                                            <issue-search-field-key-view-element lists="vm.priorities"
                                                                                 keys="vm.search.priorityIds"></issue-search-field-key-view-element>
                                                <!--    우선 순위   -->
                                                <issue-search-field-key-view-element lists="vm.priorities"
                                                                                     keys="vm.search.priorityIds"></issue-search-field-key-view-element>
                                            <!--    중요도   -->
                                            <issue-search-field-key-view-element lists="vm.severities"
                                                                                 keys="vm.search.severityIds"></issue-search-field-key-view-element>
                                                <!--    중요도   -->
                                                <issue-search-field-key-view-element lists="vm.severities"
                                                                                     keys="vm.search.severityIds"></issue-search-field-key-view-element>
                                            <!--    담당자   -->
                                            <issue-search-array-view-element lists="vm.users"
                                                                             type="'user'"></issue-search-array-view-element>
                                                <!--    담당자   -->
                                                <issue-search-array-view-element lists="vm.users"
                                                                                 type="'user'"></issue-search-array-view-element>
                                            <!--    등록자   -->
                                            <issue-search-array-view-element lists="vm.registers"
                                                                             type="'user'"></issue-search-array-view-element>
                                                <!--    등록자   -->
                                                <issue-search-array-view-element lists="vm.registers"
                                                                                 type="'user'"></issue-search-array-view-element>
                                            <!--    시작일   -->
                                            <p ng-if="$root.isDefined(vm.search.startDateRange)">
                                                {{vm.search.startDateRange}}
                                                <span ng-click="vm.search.startDateRange = ''">×</span>
                                            </p>
                                                <!--    시작일   -->
                                                <p ng-if="$root.isDefined(vm.search.startDateRange)">
                                                    {{vm.search.startDateRange}}
                                                    <span ng-click="vm.search.startDateRange = ''">×</span>
                                                </p>
                                            <!--    종료일   -->
                                            <p ng-if="$root.isDefined(vm.search.completeDateRange)">
                                                {{vm.search.completeDateRange}}
                                                <span ng-click="vm.search.completeDateRange = ''">×</span>
                                            </p>
                                                <!--    종료일   -->
                                                <p ng-if="$root.isDefined(vm.search.completeDateRange)">
                                                    {{vm.search.completeDateRange}}
                                                    <span ng-click="vm.search.completeDateRange = ''">×</span>
                                                </p>
                                            <!--    등록일   -->
                                            <p ng-if="$root.isDefined(vm.search.registerDateRange)">
                                                {{vm.search.registerDateRange}}
                                                <span ng-click="vm.search.registerDateRange = ''">×</span>
                                            </p>
                                                <!--    등록일   -->
                                                <p ng-if="$root.isDefined(vm.search.registerDateRange)">
                                                    {{vm.search.registerDateRange}}
                                                    <span ng-click="vm.search.registerDateRange = ''">×</span>
                                                </p>
                                            <!--    텍스트 입력 필드   -->
                                            <issue-search-custom-field-view-element
                                                    custom-fields="vm.customFields"></issue-search-custom-field-view-element>
                                                <!--    텍스트 입력 필드   -->
                                                <issue-search-custom-field-view-element
                                                        custom-fields="vm.customFields"></issue-search-custom-field-view-element>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                                <div class="">
                                    <label class="issue-search-label" ng-class="{ 'icon-reverse' : vm.searchView }"
                                           ng-click="vm.searchView = !vm.searchView">
                                        <span translate="common.detailedSearch">상세검색</span>
                                    </label>
                                    <div ng-if="vm.searchView">
                                        <div class="row">
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="issue.issueTitle">이슈 제목</span></label>
                                                    <input type="text"
                                                           name="title"
                                                           class="form-control input-sm"
                                                           kr-input
                                                           maxlength="20"
                                                           autocomplete="off"
                                                           ng-model="vm.search.title">
                                                </div>
                                            </div>
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="common.project">프로젝트</span></label>
                                                    <js-autocomplete-multi data-input-name="projects"
                                                                           selected-model="vm.projects"
                                                                           search="vm.projectName"
                                                                           input-disabled="false"
                                                                           translation-texts="{ empty : 'common.emptyProject', selectList : 'common.' }"
                                                                           source="fn.getProjectList(vm.projectName, vm.projects, null, null, ['01', '02', '03'])"
                                                                           extra-settings="{ displayProp : 'name' , idProp : 'id', imageable : false, imagePathProp : '', type : '', maxlength : 100}"></js-autocomplete-multi>
                                                </div>
                                            </div>
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="issue.issueType">이슈 타입</span></label>
                                                    <ng-dropdown-multiselect class="multiSelect cursor"
                                                                             data-input-name="issueStatuses"
                                                                             selected-model="vm.search.issueTypeIds"
                                                                             options="::vm.issueTypes"></ng-dropdown-multiselect>
                                                </div>
                                            </div>
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="issue.issueStatus">이슈 상태</span></label>
                                                    <ng-dropdown-multiselect class="multiSelect cursor"
                                                                             data-input-name="issueStatuses"
                                                                             selected-model="vm.search.issueStatusIds"
                                                                             options="::vm.issueStatuses"></ng-dropdown-multiselect>
                                                </div>
                                            </div>
                                        </div>
                                        <div class="row">
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="issue.issueNumber">이슈 번호</span></label>
                                                    <input type="text"
                                                           name="name"
                                                           class="form-control input-sm"
                                                           autocomplete="off"
                                                           kr-input
                                                           maxlength="20"
                                                           ng-model="vm.search.combinationIssueNumber">
                                                </div>
                                            </div>
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="issue.issueContent">이슈 내용</span></label>
                                                    <input type="text"
                                                           name="description"
                                                           class="form-control input-sm"
                                                           kr-input
                                                           maxlength="20"
                                                           autocomplete="off"
                                                           ng-model="vm.search.description">
                                                </div>
                                            </div>
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="common.priority">우선 순위</span></label>
                                                    <ng-dropdown-multiselect class="multiSelect cursor"
                                                                             data-input-name="priorities"
                                                                             selected-model="vm.search.priorityIds"
                                                                             options="::vm.priorities"></ng-dropdown-multiselect>
                                                </div>
                                            </div>
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="common.importance">중요도</span></label>
                                                    <ng-dropdown-multiselect class="multiSelect cursor"
                                                                             data-input-name="severities"
                                                                             selected-model="vm.search.severityIds"
                                                                             options="::vm.severities"></ng-dropdown-multiselect>
                                                </div>
                                            </div>
                                        </div>
                                        <div class="row">
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="common.register">등록자</span></label>
                                                    <js-autocomplete-multi data-input-name="registers"
                                                                           selected-model="vm.registers"
                                                                           search="vm.registerName"
                                                                           input-disabled="false"
                                                                           source="fn.getUserList(vm.registerName, vm.registers)"
                                                                           translation-texts="{ count : 'common.userNum', empty : 'common.emptyUser' }"
                                                                           extra-settings="{ displayProp : 'byName' , idProp : 'id', widthable : false, width : '', imageable : true, imagePathProp : 'profile', type : 'user', maxlength : 100 }">
                                                    </js-autocomplete-multi>
                                                </div>
                                            </div>
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="common.registrationDate">등록일</span></label>
                                                    <input type="text"
                                                           readonly
                                                           class="form-control input-sm input-readonly"
                                                           ng-model="vm.search.registerDateRange"
                                                           modal-form-auto-scroll
                                                           date-format="YY-MM-DD"
                                                           parent-el="'#createdWidget'"
                                                           date-range-picker>
                                                </div>
                                            </div>
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="common.startDate">시작일</span></label>
                                                    <input type="text"
                                                           readonly
                                                           class="form-control input-sm input-readonly"
                                                           ng-model="vm.search.startDateRange"
                                                           date-format="YY-MM-DD"
                                                           parent-el="'#createdWidget'"
                                                           date-range-picker>
                                                </div>
                                            </div>
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="common.endDate">종료일</span></label>
                                                    <input type="text"
                                                           readonly
                                                           class="form-control input-sm input-readonly"
                                                           ng-model="vm.search.completeDateRange"
                                                           modal-form-auto-scroll
                                                           date-format="YY-MM-DD"
                                                           parent-el="'#createdWidget'"
                                                           date-range-picker>
                                                </div>
                                            </div>
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="common.assigneeTeam">담당부서</span></label>
                                                    <js-autocomplete-multi data-input-name="departments"
                                                                           selected-model="vm.departments"
                                                                           search="vm.departmentName"
                                                                           input-disabled="false"
                                                                           source="fn.getUserDepartmentList(vm.departmentName, vm.departments)"
                                                                           translation-texts="{ count : 'common.userNum', empty : 'common.emptyProjectDepartment' }"
                                                                           extra-settings="{ displayProp : 'byName' , idProp : 'id', widthable : false, width : '', imageable : true, imagePathProp : 'profile', type : 'department', maxlength : 100 }">
                                                    </js-autocomplete-multi>
                                                </div>
                                            </div>
                                            <div class="col-lg-3" ng-repeat="customField in vm.customFields">
                                                <label>{{::customField.name}}</label>
                                                <div ng-switch on="customField.customFieldType">
                                                    <div ng-switch-when="INPUT">
                                                        <input type="text" class="form-control input-sm"
                                                               ng-model="customField.useValues"
                                                               maxlength="100">
                                    <div class="">
                                        <label class="issue-search-label" ng-class="{ 'icon-reverse' : vm.searchView }"
                                               ng-click="vm.searchView = !vm.searchView">
                                            <span translate="common.detailedSearch">상세검색</span>
                                        </label>
                                        <div ng-if="vm.searchView">
                                            <div class="row">
                                                <div class="col-lg-3">
                                                    <div class="form-group">
                                                        <label> <span translate="issue.issueTitle">이슈 제목</span></label>
                                                        <input type="text"
                                                               name="title"
                                                               class="form-control input-sm"
                                                               kr-input
                                                               maxlength="20"
                                                               autocomplete="off"
                                                               ng-model="vm.search.title">
                                                    </div>
                                                </div>
                                                    <div ng-switch-default>
                                                <div class="col-lg-3">
                                                    <div class="form-group">
                                                        <label> <span translate="common.project">프로젝트</span></label>
                                                        <js-autocomplete-multi data-input-name="projects"
                                                                               selected-model="vm.projects"
                                                                               search="vm.projectName"
                                                                               input-disabled="false"
                                                                               translation-texts="{ empty : 'common.emptyProject', selectList : 'common.' }"
                                                                               source="fn.getProjectList(vm.projectName, vm.projects, null, null, ['01', '02', '03'])"
                                                                               extra-settings="{ displayProp : 'name' , idProp : 'id', imageable : false, imagePathProp : '', type : '', maxlength : 100}"></js-autocomplete-multi>
                                                    </div>
                                                </div>
                                                <div class="col-lg-3">
                                                    <div class="form-group">
                                                        <label> <span translate="issue.issueType">이슈 타입</span></label>
                                                        <ng-dropdown-multiselect class="multiSelect cursor"
                                                                                 data-input-name="customField.name"
                                                                                 selected-model="customField.useValues"
                                                                                 extra-settings="{ 'idProp' : 'value', 'externalIdProp' : 'value', 'displayProp' : 'value', 'stringTypeOption' : 'true', 'stopRemoveBodyEvent' : 'true' }"
                                                                                 options="::customField.customFieldValueVos"></ng-dropdown-multiselect>
                                                                                 data-input-name="issueStatuses"
                                                                                 selected-model="vm.search.issueTypeIds"
                                                                                 options="::vm.issueTypes"></ng-dropdown-multiselect>
                                                    </div>
                                                </div>
                                                <div class="col-lg-3">
                                                    <div class="form-group">
                                                        <label> <span translate="issue.issueStatus">이슈 상태</span></label>
                                                        <ng-dropdown-multiselect class="multiSelect cursor"
                                                                                 data-input-name="issueStatuses"
                                                                                 selected-model="vm.search.issueStatusIds"
                                                                                 options="::vm.issueStatuses"></ng-dropdown-multiselect>
                                                    </div>
                                                </div>
                                            </div>
                                        </div>
                                            <div class="row">
                                                <div class="col-lg-3">
                                                    <div class="form-group">
                                                        <label> <span translate="issue.issueNumber">이슈 번호</span></label>
                                                        <input type="text"
                                                               name="name"
                                                               class="form-control input-sm"
                                                               autocomplete="off"
                                                               kr-input
                                                               maxlength="20"
                                                               ng-model="vm.search.combinationIssueNumber">
                                                    </div>
                                                </div>
                                        <div class="row">
                                            <div class="col-sm-12">
                                                <div class="form-buttons-w text-center mb-20">
                                                    <button class="btn btn-xlg btn-navy" ng-click="fn.getPageList(0)">
                                                        <i class="os-icon os-icon-ui-37"></i> &nbsp;
                                                        &nbsp; <span translate="common.search">검색</span> &nbsp; &nbsp;
                                                    </button>
                                                    <button class="btn btn-xlg btn-white" ng-click="fn.initSearch()">
                                                        <i class="os-icon os-icon-grid-18"></i> &nbsp;
                                                        &nbsp; <span translate="common.reset">초기화</span> &nbsp; &nbsp;
                                                    </button>
                                                <div class="col-lg-3">
                                                    <div class="form-group">
                                                        <label> <span translate="issue.issueContent">이슈 내용</span></label>
                                                        <input type="text"
                                                               name="description"
                                                               class="form-control input-sm"
                                                               kr-input
                                                               maxlength="20"
                                                               autocomplete="off"
                                                               ng-model="vm.search.description">
                                                    </div>
                                                </div>
                                                <div class="col-lg-3">
                                                    <div class="form-group">
                                                        <label> <span translate="common.priority">우선 순위</span></label>
                                                        <ng-dropdown-multiselect class="multiSelect cursor"
                                                                                 data-input-name="priorities"
                                                                                 selected-model="vm.search.priorityIds"
                                                                                 options="::vm.priorities"></ng-dropdown-multiselect>
                                                    </div>
                                                </div>
                                                <div class="col-lg-3">
                                                    <div class="form-group">
                                                        <label> <span translate="common.importance">중요도</span></label>
                                                        <ng-dropdown-multiselect class="multiSelect cursor"
                                                                                 data-input-name="severities"
                                                                                 selected-model="vm.search.severityIds"
                                                                                 options="::vm.severities"></ng-dropdown-multiselect>
                                                    </div>
                                                </div>
                                            </div>
                                            <div class="row">
                                                <div class="col-lg-3">
                                                    <div class="form-group">
                                                        <label> <span translate="common.register">등록자</span></label>
                                                        <js-autocomplete-multi data-input-name="registers"
                                                                               selected-model="vm.registers"
                                                                               search="vm.registerName"
                                                                               input-disabled="false"
                                                                               source="fn.getUserList(vm.registerName, vm.registers)"
                                                                               translation-texts="{ count : 'common.userNum', empty : 'common.emptyUser' }"
                                                                               extra-settings="{ displayProp : 'byName' , idProp : 'id', widthable : false, width : '', imageable : true, imagePathProp : 'profile', type : 'user', maxlength : 100 }">
                                                        </js-autocomplete-multi>
                                                    </div>
                                                </div>
                                                <div class="col-lg-3">
                                                    <div class="form-group">
                                                        <label> <span translate="common.registrationDate">등록일</span></label>
                                                        <input type="text"
                                                               readonly
                                                               class="form-control input-sm input-readonly"
                                                               ng-model="vm.search.registerDateRange"
                                                               modal-form-auto-scroll
                                                               date-format="YY-MM-DD"
                                                               parent-el="'#createdWidget'"
                                                               date-range-picker>
                                                    </div>
                                                </div>
                                                <div class="col-lg-3">
                                                    <div class="form-group">
                                                        <label> <span translate="common.startDate">시작일</span></label>
                                                        <input type="text"
                                                               readonly
                                                               class="form-control input-sm input-readonly"
                                                               ng-model="vm.search.startDateRange"
                                                               date-format="YY-MM-DD"
                                                               parent-el="'#createdWidget'"
                                                               date-range-picker>
                                                    </div>
                                                </div>
                                                <div class="col-lg-3">
                                                    <div class="form-group">
                                                        <label> <span translate="common.endDate">종료일</span></label>
                                                        <input type="text"
                                                               readonly
                                                               class="form-control input-sm input-readonly"
                                                               ng-model="vm.search.completeDateRange"
                                                               modal-form-auto-scroll
                                                               date-format="YY-MM-DD"
                                                               parent-el="'#createdWidget'"
                                                               date-range-picker>
                                                    </div>
                                                </div>
                                                <div class="col-lg-3">
                                                    <div class="form-group">
                                                        <label> <span translate="common.assigneeTeam">담당부서</span></label>
                                                        <js-autocomplete-multi data-input-name="departments"
                                                                               selected-model="vm.departments"
                                                                               search="vm.departmentName"
                                                                               input-disabled="false"
                                                                               source="fn.getUserDepartmentList(vm.departmentName, vm.departments)"
                                                                               translation-texts="{ count : 'common.userNum', empty : 'common.emptyProjectDepartment' }"
                                                                               extra-settings="{ displayProp : 'byName' , idProp : 'id', widthable : false, width : '', imageable : true, imagePathProp : 'profile', type : 'department', maxlength : 100 }">
                                                        </js-autocomplete-multi>
                                                    </div>
                                                </div>
                                                <div class="col-lg-3" ng-repeat="customField in vm.customFields">
                                                    <label>{{::customField.name}}</label>
                                                    <div ng-switch on="customField.customFieldType">
                                                        <div ng-switch-when="INPUT">
                                                            <input type="text" class="form-control input-sm"
                                                                   ng-model="customField.useValues"
                                                                   maxlength="100">
                                                        </div>
                                                        <div ng-switch-default>
                                                            <ng-dropdown-multiselect class="multiSelect cursor"
                                                                                     data-input-name="customField.name"
                                                                                     selected-model="customField.useValues"
                                                                                     extra-settings="{ 'idProp' : 'value', 'externalIdProp' : 'value', 'displayProp' : 'value', 'stringTypeOption' : 'true', 'stopRemoveBodyEvent' : 'true' }"
                                                                                     options="::customField.customFieldValueVos"></ng-dropdown-multiselect>
                                                        </div>
                                                    </div>
                                                </div>
                                            </div>
                                            <div class="row">
                                                <div class="col-sm-12">
                                                    <div class="form-buttons-w text-center mb-20">
                                                        <button class="btn btn-xlg btn-navy" ng-click="fn.getPageList(0)">
                                                            <i class="os-icon os-icon-ui-37"></i> &nbsp;
                                                            &nbsp; <span translate="common.search">검색</span> &nbsp; &nbsp;
                                                        </button>
                                                        <button class="btn btn-xlg btn-white" ng-click="fn.initSearch()">
                                                            <i class="os-icon os-icon-grid-18"></i> &nbsp;
                                                            &nbsp; <span translate="common.reset">초기화</span> &nbsp; &nbsp;
                                                        </button>
                                                    </div>
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            </form>
                                </form>
                            </div>
                        </div>
                    </div>
                </div>
                    <div class="controls-above-table mt-30">
                        <div class="row">
                            <!--    좌측  -->
@@ -326,15 +326,20 @@
                                        </select> {{vm.page.selectedPage}}-{{vm.responseData.page.totalPage}} /
                                        {{vm.responseData.page.totalCount | number}}<span translate="common.few">건</span>
                                    </label>
                                    <div class="btn-group ml-2" role="group" aria-label="list-group">
                                        <button class="btn" ng-class="{'btn-info': vm.listMode === 0, 'btn-light': vm.listMode !== 0}" ng-click="fn.onClickListMode(0)">List</button>
                                        <button class="btn" ng-class="{'btn-info': vm.listMode === 1, 'btn-light': vm.listMode !== 1}" ng-click="fn.onClickListMode(1)">Tree</button>
                                    </div>
                                </div>
                            </div>
                            <!--    우측  -->
                            <div class="col-5 " >
                                <span class="issue-detail-label" style="position: relative; left: 18.8rem; bottom: 5px"><span style="color: #0a7cf8">◆</span> 완료 이슈 숨기기</span>
                                    <label class='switch' style="left: 19.3rem"><input type='checkbox' ng-model='vm.hideIssue' ng-click='fn.getPageList(0)'>
                                        <span class='slider round'></span>
                                    </label>
                            <!--    우측  -->
                            <div class="col-5" >
                                <span class="issue-detail-label" style="position: relative; left: 18.8rem; bottom: 5px"><span style="color: #0a7cf8">◆</span> 완료 이슈 숨기기</span>
                                <label class='switch' style="left: 19.3rem"><input type='checkbox' ng-model='vm.hideIssue' ng-click='fn.getPageList(0)'>
                                    <span class='slider round'></span>
                                </label>
                                <form class="form-inline justify-content-sm-end  pull-right" method="post" action="/issue/downloadExcel" name="issueListForm" >
                                    <!--span class="badge-tip" function-tool-tip  data-placement="top" data-toggle="tooltip" data-original-title="엑셀 다운로드, 일괄 변경 등 다양한 기능을 제공합니다.">?</span-->
                                    <input type="hidden" name="conditions">
@@ -359,26 +364,31 @@
                        </div>
                    </div>
                    <div class="table-responsive">
                        <js-table data="vm.responseData.data" table-configs="vm.tableConfigs"
                    <div class="table-responsive" >
                        <js-table ng-if="vm.listMode === 0"
                                  data="vm.responseData.data" table-configs="vm.tableConfigs"
                                  event="tableEvent" detail-view="vm.detailView"></js-table>
                        <js-tree ng-if="vm.listMode === 1"
                                  data="vm.responseData.data" table-configs="vm.tableConfigs"
                                  event="tableEvent" detail-view="vm.detailView"></js-tree>
                    </div>
                <div class="controls-below-table text-center">
                    <ul uib-pagination
                        boundary-links-numbes="true"
                        items-per-page="vm.page.selectedPageRowCount"
                        total-items="vm.responseData.page.totalCount"
                        ng-model="vm.page.selectedPage"
                        max-size="10"
                        ng-click="fn.getPageList(vm.page.selectedPage - 1)"
                        class="pagination pagination-sm"
                        previous-text="&lt;"
                        next-text="&gt;"
                        first-text=""
                        last-text="">
                    </ul>
                    <div class="controls-below-table text-center">
                        <ul uib-pagination
                            boundary-links-numbes="true"
                            items-per-page="vm.page.selectedPageRowCount"
                            total-items="vm.responseData.page.totalCount"
                            ng-model="vm.page.selectedPage"
                            max-size="10"
                            ng-click="fn.getPageList(vm.page.selectedPage - 1)"
                            class="pagination pagination-sm"
                            previous-text="&lt;"
                            next-text="&gt;"
                            first-text=""
                            last-text="">
                        </ul>
                    </div>
                </div>
            </div>
            </div>
        </div>
    </div>