OWL ITS + 탐지시스템(인터넷 진흥원)
이민희
2021-12-22 57fd755996962af22fbf8f322ee03d1380044f1f
Merge branch 'master' of http://192.168.0.25:9001/r/owl-kisa
8개 파일 추가됨
21개 파일 변경됨
973 ■■■■■ 파일 변경됨
src/main/java/kr/wisestone/owl/domain/CustomFieldApiOverlap.java 13 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/domain/IssueType.java 13 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/domain/IssueTypeApiEndStatus.java 76 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/repository/IssueTypeApiEndStatusRepository.java 23 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/IssueTypeApiEndStatusService.java 9 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/IssueTypeService.java 2 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/CustomFieldApiOverlapServiceImpl.java 5 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/IssueServiceImpl.java 16 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/IssueTypeApiEndStatusServiceImpl.java 92 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/IssueTypeServiceImpl.java 29 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/controller/IssueTypeApiEndStatusController.java 46 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/controller/IssueTypeController.java 10 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/form/CustomFieldApiOverlapForm.java 9 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/form/IssueTypeApiEndStatusForm.java 43 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/form/IssueTypeForm.java 9 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/resources/migration/V1_15__Alter_Table.sql 22 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/resources/mybatis/query-template/issueType-template.xml 3 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/i18n/ko/global.json 15 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/api/apiSetting.controller.js 191 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/config.js 24 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/api/apiSetting.html 3 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/api/apiSettingColumn.html 47 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/api/apiSettingHeader.html 42 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/api/apiSettingOverlap.html 52 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/api/apiSettingSpec.html 42 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/customField/customFieldAdd.html 10 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/customField/customFieldModify.html 20 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/issue/issueAdd.html 58 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/issue/issueModify.html 49 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/domain/CustomFieldApiOverlap.java
@@ -16,6 +16,11 @@
    private User user;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "project_id")
    private Project project;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "issue_type_id")
    private IssueType issueType;
@@ -60,4 +65,12 @@
    public void setCustomField(CustomField customField) {
        this.customField = customField;
    }
    public Project getProject() {
        return project;
    }
    public void setProject(Project project) {
        this.project = project;
    }
}
src/main/java/kr/wisestone/owl/domain/IssueType.java
@@ -35,9 +35,8 @@
    @JoinColumn(name = "project_id")
    private Project project;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "complete_issue_status_id")
    private IssueStatus issueStatus;
    @OneToMany(mappedBy = "issueType", cascade = {CascadeType.ALL}, orphanRemoval = true)
    private Set<IssueTypeApiEndStatus> issueTypeApiEndStatuses;
    /*@ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "company_id")
@@ -138,11 +137,11 @@
        this.usePartner = usePartner;
    }
    public IssueStatus getIssueStatus() {
        return issueStatus;
    public Set<IssueTypeApiEndStatus> getIssueTypeApiEndStatuses() {
        return issueTypeApiEndStatuses;
    }
    public void setIssueStatus(IssueStatus issueStatus) {
        this.issueStatus = issueStatus;
    public void setIssueTypeApiEndStatuses(Set<IssueTypeApiEndStatus> issueTypeApiEndStatuses) {
        this.issueTypeApiEndStatuses = issueTypeApiEndStatuses;
    }
}
src/main/java/kr/wisestone/owl/domain/IssueTypeApiEndStatus.java
New file
@@ -0,0 +1,76 @@
package kr.wisestone.owl.domain;
import javax.persistence.*;
import java.io.Serializable;
@Entity
public class IssueTypeApiEndStatus extends BaseEntity implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "project_id")
    private Project project;
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "issue_type_id")
    private IssueType issueType;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "issue_status_id")
    private IssueStatus issueStatus;
    public IssueTypeApiEndStatus(){}
    public static long getSerialVersionUID() {
        return serialVersionUID;
    }
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public User getUser() {
        return user;
    }
    public void setUser(User user) {
        this.user = user;
    }
    public IssueType getIssueType() {
        return issueType;
    }
    public void setIssueType(IssueType issueType) {
        this.issueType = issueType;
    }
    public Project getProject() {
        return project;
    }
    public void setProject(Project project) {
        this.project = project;
    }
    public IssueStatus getIssueStatus() {
        return issueStatus;
    }
    public void setIssueStatus(IssueStatus issueStatus) {
        this.issueStatus = issueStatus;
    }
}
src/main/java/kr/wisestone/owl/repository/IssueTypeApiEndStatusRepository.java
New file
@@ -0,0 +1,23 @@
package kr.wisestone.owl.repository;
import kr.wisestone.owl.domain.IssueType;
import kr.wisestone.owl.domain.IssueTypeApiEndStatus;
import kr.wisestone.owl.domain.Project;
import kr.wisestone.owl.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import javax.inject.Named;
import javax.inject.Qualifier;
import javax.persistence.NamedQuery;
import java.util.List;
public interface IssueTypeApiEndStatusRepository extends JpaRepository<IssueTypeApiEndStatus, Long> {
    IssueTypeApiEndStatus findByUserIdAndProjectIdAndIssueTypeId(
            @Param("user_id") Long userId, @Param("project_id") Long projectId, @Param("issue_type_id") Long issueTypeId);
    void deleteByUserIdAndProjectIdAndIssueTypeId(
            @Param("user_id") Long userId, @Param("project_id") Long projectId, @Param("issue_type_id") Long issueTypeId);
}
src/main/java/kr/wisestone/owl/service/IssueTypeApiEndStatusService.java
New file
@@ -0,0 +1,9 @@
package kr.wisestone.owl.service;
import kr.wisestone.owl.domain.IssueTypeApiEndStatus;
import kr.wisestone.owl.web.form.IssueTypeApiEndStatusForm;
import org.springframework.data.jpa.repository.JpaRepository;
public interface IssueTypeApiEndStatusService extends AbstractService<IssueTypeApiEndStatus, Long, JpaRepository<IssueTypeApiEndStatus, Long>> {
    void setApiEndStatus(IssueTypeApiEndStatusForm issueTypeApiEndStatusForm);
}
src/main/java/kr/wisestone/owl/service/IssueTypeService.java
@@ -33,8 +33,6 @@
    IssueType modifyIssueType(IssueTypeForm issueTypeForm);
    IssueType modifyIssueTypeCompleteIssueStatus(IssueTypeForm issueTypeForm);
    IssueType getIssueType(Long id);
    List<IssueType> findByProjectId(Long projectId);
src/main/java/kr/wisestone/owl/service/impl/CustomFieldApiOverlapServiceImpl.java
@@ -33,6 +33,9 @@
    private IssueTypeService issueTypeService;
    @Autowired
    private ProjectService projectService;
    @Autowired
    private CustomFieldService customFieldService;
    @Override
@@ -72,6 +75,7 @@
    @Transactional
    public boolean modify(Map<String, Object> resJsonData, CustomFieldApiOverlapForm form) {
        User user = this.webAppUtil.getLoginUserObject();
        Project project = this.projectService.getProject(form.getProjectId());
        List<CustomFieldApiOverlap> customFieldApiOverlaps = this.customFieldApiOverlapRepository.findByUserIdAndIssueTypeId(user.getId(), form.getIssueTypeId());
        if (customFieldApiOverlaps != null && customFieldApiOverlaps.size() > 0) {
            this.customFieldApiOverlapRepository.deleteAll(customFieldApiOverlaps);
@@ -84,6 +88,7 @@
            customFieldApiOverlap.setCustomField(customField);
            customFieldApiOverlap.setUser(user);
            customFieldApiOverlap.setIssueType(this.issueTypeService.getIssueType(form.getIssueTypeId()));
            customFieldApiOverlap.setProject(project);
            customFieldApiOverlapList.add(customFieldApiOverlap);
        }
src/main/java/kr/wisestone/owl/service/impl/IssueServiceImpl.java
@@ -1796,16 +1796,22 @@
                Issue modifyIssue = this.modifyIssueForApi(user, issueForm, files);
                Issue parentIssue = modifyIssue.getParentIssue();
                IssueType issueType = modifyIssue.getIssueType();
                IssueStatus issueStatus = issueType.getIssueStatus();
                Set<IssueTypeApiEndStatus> issueTypeApiEndStatuses = issueType.getIssueTypeApiEndStatuses();
                IssueTypeApiEndStatus issueStatus = null;
                if (issueTypeApiEndStatuses != null && issueTypeApiEndStatuses.size() > 0) {
                    issueStatus = issueTypeApiEndStatuses.iterator().next();
                } else {
                    throw new OwlRuntimeException(this.messageAccessor.getMessage(MsgConstants.API_COMPLETE_ISSUE_STATUS_NOT_EXIST));
                }
                if (parentIssue != null) {
                    if (issueStatus == null) {
                        throw new OwlRuntimeException(this.messageAccessor.getMessage(MsgConstants.API_COMPLETE_ISSUE_STATUS_NOT_EXIST));
                    }
                    IssueCondition issueCondition = new IssueCondition(issueVo.getId(), parentIssue.getId());
                    List<Map<String, Object>> results = this.issueMapper.findNotCompleteByParentIssueId(issueCondition);
                    // 하위 일감이 모두 종료 상태일때 상위 일감도 종료 처리
                    if (results == null || results.size() == 0) {
                        parentIssue.setIssueStatus(issueType.getIssueStatus());
                        parentIssue.setIssueStatus(issueStatus.getIssueStatus());
                        this.issueRepository.saveAndFlush(parentIssue);
                    }
                }
src/main/java/kr/wisestone/owl/service/impl/IssueTypeApiEndStatusServiceImpl.java
New file
@@ -0,0 +1,92 @@
package kr.wisestone.owl.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import kr.wisestone.owl.constant.MsgConstants;
import kr.wisestone.owl.domain.*;
import kr.wisestone.owl.exception.OwlRuntimeException;
import kr.wisestone.owl.repository.ApiTokenRepository;
import kr.wisestone.owl.repository.IssueTypeApiEndStatusRepository;
import kr.wisestone.owl.service.IssueStatusService;
import kr.wisestone.owl.service.IssueTypeApiEndStatusService;
import kr.wisestone.owl.service.IssueTypeService;
import kr.wisestone.owl.service.ProjectService;
import kr.wisestone.owl.util.ConvertUtil;
import kr.wisestone.owl.util.DateUtil;
import kr.wisestone.owl.util.WebAppUtil;
import kr.wisestone.owl.vo.ApiTokenVo;
import kr.wisestone.owl.vo.UserVo;
import kr.wisestone.owl.web.form.ApiTokenForm;
import kr.wisestone.owl.web.form.IssueTypeApiEndStatusForm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.List;
@Service
public class IssueTypeApiEndStatusServiceImpl extends AbstractServiceImpl<IssueTypeApiEndStatus, Long, JpaRepository<IssueTypeApiEndStatus, Long>> implements IssueTypeApiEndStatusService {
    @Autowired
    private IssueTypeApiEndStatusRepository issueTypeApiEndStatusRepository;
    @Autowired
    private ProjectService projectService;
    @Autowired
    private IssueTypeService issueTypeService;
    @Autowired
    private IssueStatusService issueStatusService;
    @Override
    protected JpaRepository<IssueTypeApiEndStatus, Long> getRepository() {
        return this.issueTypeApiEndStatusRepository;
    }
    // 종료 상태 설정
    @Override
    @Transactional
    public void setApiEndStatus(IssueTypeApiEndStatusForm issueTypeApiEndStatusForm) {
        User user = this.webAppUtil.getLoginUserObject();
        Project project = this.projectService.getProject(issueTypeApiEndStatusForm.getProjectId());
        IssueType issueType = this.issueTypeService.getIssueType(issueTypeApiEndStatusForm.getIssueTypeId());
        if (issueTypeApiEndStatusForm.getIssueStatusId() == null) {
            // 기존 설정 삭제
            this.issueTypeApiEndStatusRepository.deleteByUserIdAndProjectIdAndIssueTypeId(
                    user.getId(), issueTypeApiEndStatusForm.getProjectId(), issueTypeApiEndStatusForm.getIssueTypeId());
        } else {
            IssueStatus issueStatus = this.issueStatusService.getIssueStatus(issueTypeApiEndStatusForm.getIssueStatusId());
            // 기존 설정 찾기
            IssueTypeApiEndStatus issueTypeApiEndStatus = this.issueTypeApiEndStatusRepository.findByUserIdAndProjectIdAndIssueTypeId(
                    user.getId(), issueTypeApiEndStatusForm.getProjectId(), issueTypeApiEndStatusForm.getIssueTypeId());
            if (issueTypeApiEndStatus != null) {
                issueTypeApiEndStatus.setIssueStatus(issueStatus);
                this.issueTypeApiEndStatusRepository.save(issueTypeApiEndStatus);
            } else {
                // 새로 설정
                IssueTypeApiEndStatus newIssueTypeApiEndStatus = ConvertUtil.copyProperties(issueTypeApiEndStatusForm, IssueTypeApiEndStatus.class);
                newIssueTypeApiEndStatus.setUser(user);
                newIssueTypeApiEndStatus.setProject(project);
                newIssueTypeApiEndStatus.setIssueType(issueType);
                newIssueTypeApiEndStatus.setIssueStatus(issueStatus);
                this.issueTypeApiEndStatusRepository.save(newIssueTypeApiEndStatus);
            }
        }
    }
}
src/main/java/kr/wisestone/owl/service/impl/IssueTypeServiceImpl.java
@@ -33,6 +33,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Service
public class IssueTypeServiceImpl extends AbstractServiceImpl<IssueType, Long, JpaRepository<IssueType, Long>> implements IssueTypeService {
@@ -270,9 +271,11 @@
        for (IssueTypeVo issueTypeVo : issueTypeVos) {
            IssueType issueType = this.getIssueType(issueTypeVo.getId());
            IssueStatus issueStatus = issueType.getIssueStatus();
            if (issueStatus != null) {
                issueTypeVo.setCompleteIssueStatusVo(ConvertUtil.copyProperties(issueType.getIssueStatus(), IssueStatusVo.class));
            Set<IssueTypeApiEndStatus> issueTypeApiEndStatuses = issueType.getIssueTypeApiEndStatuses();
            if (issueTypeApiEndStatuses != null && issueTypeApiEndStatuses.size() > 0) {
                IssueTypeApiEndStatus issueTypeApiEndStatus = issueTypeApiEndStatuses.iterator().next();
                issueTypeVo.setCompleteIssueStatusVo(ConvertUtil.copyProperties(issueTypeApiEndStatus.getIssueStatus(), IssueStatusVo.class));
            }
        }
    }
@@ -323,26 +326,6 @@
        }
        resJsonData.put(Constants.RES_KEY_CONTENTS, issueTypeVo);
    }
    // 이슈 유형을 수정한다. 자동 종료 설정만 수정
    @Override
    @Transactional
    public IssueType modifyIssueTypeCompleteIssueStatus(IssueTypeForm issueTypeForm) {
        //  사용하고 있는 업무 공간이 활성 상태인지 확인한다. 사용 공간에서 로그인한 사용자가 비활성인지 확인한다.
        this.workspaceService.checkUseWorkspace();
        IssueType issueType = this.getIssueType(issueTypeForm.getId());
        if (issueTypeForm.getCompleteIssueStatusId() != null) {
            // api에서 사용하는 자동 종료 이슈 상태
            IssueStatus issueStatus = this.issueStatusService.getIssueStatus(issueTypeForm.getCompleteIssueStatusId());
            issueType.setIssueStatus(issueStatus);
        }else {
            issueType.setIssueStatus(null);
        }
        this.issueTypeRepository.saveAndFlush(issueType);
        return issueType;
    }
    //  이슈 유형을 수정한다.
src/main/java/kr/wisestone/owl/web/controller/IssueTypeApiEndStatusController.java
New file
@@ -0,0 +1,46 @@
package kr.wisestone.owl.web.controller;
import kr.wisestone.owl.constant.Constants;
import kr.wisestone.owl.service.GuideService;
import kr.wisestone.owl.service.IssueTypeApiEndStatusService;
import kr.wisestone.owl.util.ConvertUtil;
import kr.wisestone.owl.web.condition.GuideCondition;
import kr.wisestone.owl.web.form.GuideForm;
import kr.wisestone.owl.web.form.IssueTypeApiEndStatusForm;
import kr.wisestone.owl.web.form.IssueTypeForm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.Map;
/**
 * Create By J E O N G - S U N / 2019-05-24
 */
@Controller
public class IssueTypeApiEndStatusController extends BaseController {
    @Autowired
    private IssueTypeApiEndStatusService issueTypeApiEndStatusService;
    //  api 자동 종료 이슈상태 설정
    @RequestMapping(value = "/api/saveCompleteIssueStatus", produces = MediaType.APPLICATION_JSON_VALUE)
    public
    @ResponseBody
    Map<String, Object> saveCompleteIssueStatus(@RequestBody Map<String, Map<String, Object>> params) {
        Map<String, Object> resJsonData = new HashMap<>();
        IssueTypeApiEndStatusForm issueTypeApiEndStatusForm =
                ConvertUtil.convertMapToClass(params.get(Constants.REQ_KEY_CONTENT), IssueTypeApiEndStatusForm.class);
        this.issueTypeApiEndStatusService.setApiEndStatus(issueTypeApiEndStatusForm);
        return this.setSuccessMessage(resJsonData);
    }
}
src/main/java/kr/wisestone/owl/web/controller/IssueTypeController.java
@@ -95,14 +95,4 @@
        return this.issueTypeService.downloadExcel(request, model);
    }
    //  api 자동 종료 이슈상태 설정
    @RequestMapping(value = "/api/saveCompleteIssueStatus", produces = MediaType.APPLICATION_JSON_VALUE)
    public
    @ResponseBody
    Map<String, Object> saveCompleteIssueStatus(@RequestBody Map<String, Map<String, Object>> params) {
        Map<String, Object> resJsonData = new HashMap<>();
        IssueTypeForm issueTypeForm = IssueTypeForm.make(params.get(Constants.REQ_KEY_CONTENT));
        this.issueTypeService.modifyIssueTypeCompleteIssueStatus(issueTypeForm);
        return this.setSuccessMessage(resJsonData);
    }
 }
src/main/java/kr/wisestone/owl/web/form/CustomFieldApiOverlapForm.java
@@ -12,6 +12,7 @@
    private Long id;
    private Long userId;
    private Long issueTypeId;
    private Long projectId;
    private List<Long> customFieldIds;
    public CustomFieldApiOverlapForm(){}
@@ -64,4 +65,12 @@
            this.customFieldIds.add(customFieldId);
        }
    }
    public Long getProjectId() {
        return projectId;
    }
    public void setProjectId(Long projectId) {
        this.projectId = projectId;
    }
}
src/main/java/kr/wisestone/owl/web/form/IssueTypeApiEndStatusForm.java
New file
@@ -0,0 +1,43 @@
package kr.wisestone.owl.web.form;
public class IssueTypeApiEndStatusForm {
    private Long userId;
    private Long projectId;
    private Long issueTypeId;
    private Long issueStatusId;
    public IssueTypeApiEndStatusForm() {
    }
    public Long getUserId() {
        return userId;
    }
    public void setUserId(Long userId) {
        this.userId = userId;
    }
    public Long getProjectId() {
        return projectId;
    }
    public void setProjectId(Long projectId) {
        this.projectId = projectId;
    }
    public Long getIssueTypeId() {
        return issueTypeId;
    }
    public void setIssueTypeId(Long issueTypeId) {
        this.issueTypeId = issueTypeId;
    }
    public Long getIssueStatusId() {
        return issueStatusId;
    }
    public void setIssueStatusId(Long issueStatusId) {
        this.issueStatusId = issueStatusId;
    }
}
src/main/java/kr/wisestone/owl/web/form/IssueTypeForm.java
@@ -18,7 +18,6 @@
    private String color;
    private Long workflowId;
    private Long projectId;
    private Long completeIssueStatusId;
    private List<Long> removeIds = Lists.newArrayList();
    private Long usePartner;
@@ -114,13 +113,5 @@
    public void setUsePartner(Long usePartner) {
        this.usePartner = usePartner;
    }
    public Long getCompleteIssueStatusId() {
        return completeIssueStatusId;
    }
    public void setCompleteIssueStatusId(Long completeIssueStatusId) {
        this.completeIssueStatusId = completeIssueStatusId;
    }
}
src/main/resources/migration/V1_15__Alter_Table.sql
New file
@@ -0,0 +1,22 @@
ALTER TABLE `custom_field_api_overlap` ADD COLUMN `project_id` bigint(20) DEFAULT NULL;
ALTER TABLE `custom_field_api_overlap` ADD INDEX `projectIdIndex`(`project_id`);
ALTER TABLE `issue_type` DROP COLUMN `complete_issue_status_id`;
-- 이슈 자동 종료 설정 테이블
CREATE TABLE IF NOT EXISTS `issue_type_api_end_status` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `user_id` bigint(20) DEFAULT NULL,
    `project_id` bigint(20) DEFAULT NULL,
    `issue_type_id` bigint(20) DEFAULT NULL,
    `issue_status_id` bigint(20) DEFAULT NULL,
    `register_id` bigint(20) NOT NULL COMMENT 'register_id',
    `register_date` timestamp NULL DEFAULT NULL COMMENT 'register_date',
    `modify_id` bigint(20) NOT NULL COMMENT 'modify_id',
    `modify_date` timestamp NULL DEFAULT NULL COMMENT 'modify_date',
    PRIMARY KEY (`id`),
    KEY `userIdIndex` (`user_id`),
    KEY `projectIdIndex` (`project_id`),
    KEY `issueTypeIdIndex` (`issue_type_id`),
    KEY `issueStatusIdIndex` (`issue_status_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
src/main/resources/mybatis/query-template/issueType-template.xml
@@ -9,8 +9,7 @@
        it.name as name,
        it.description as description,
        it.color as color,
        it.project_id as projectId,
        it.complete_issue_status_id as completeIssueStatusId
        it.project_id as projectId
        FROM
        issue_type it
        INNER JOIN workspace ws on it.workspace_id = ws.id
src/main/webapp/i18n/ko/global.json
@@ -858,13 +858,14 @@
        "emailField": "이메일 선택 필드",
        "siteField": "URL 선택 필드",
        "telField": "전화번호 선택 필드",
        "invalidipAdressFormat": "IP 주소 형식이 맞지 않습니다.",
        "invalidNumberFormat": "숫자만 입력 가능합니다.",
        "invalidDateFormat": "날짜 형식이 맞지 않습니다.(xxxx-xx-xx)",
        "invalidEmailFormat": "이메일 형식이 맞지 않습니다.",
        "invalidSiteFormat": "홈페이지 주소 형식이 맞지 않습니다.(http://로 시작하셔야합니다)",
        "invalidTelFormat": "전화번호 형식이 맞지 않습니다(xxx-xxxx-xxxx).",
        "invalidUrlFormat": "url 형식이 맞지 않습니다."
        "invalidipAdressFormat": "* IP 주소 형식이 맞지 않습니다.",
        "invalidNumberFormat": "* 숫자만 입력 가능합니다.",
        "invalidDateFormat": "* 날짜 형식이 맞지 않습니다.(xxxx-xx-xx)",
        "invalidEmailFormat": "* 이메일 형식이 맞지 않습니다.",
        "invalidSiteFormat": "* 홈페이지 주소 형식이 맞지 않습니다.(http:// 또는 www 로 시작하셔야합니다)",
        "invalidTelFormat": "* 전화번호 형식이 맞지 않습니다(xxx-xxxx-xxxx).",
        "invalidUrlFormat": "* url 형식이 맞지 않습니다.",
        "notIssueType": "이슈 유형이 없습니다."
    },
    "tasks": {
        "agileBoardTitle": "칸반 보드"
src/main/webapp/scripts/app/api/apiSetting.controller.js
@@ -9,23 +9,23 @@
    function (app, angular) {
        app.controller('apiSettingController', ['$scope', '$rootScope', '$log', '$resourceProvider','$uibModal', 'SweetAlert',
            '$timeout', '$filter', '$injector', '$controller', 'Api', 'Priority', 'Severity', 'IssueType', 'IssueTypeCustomField',
            'IssueStatus',
            'IssueStatus', '$q',
            function ($scope, $rootScope, $log, $resourceProvider, $uibModal, SweetAlert,
                      $timeout, $filter, $injector, $controller, Api, Priority, Severity, IssueType, IssueTypeCustomField,
                      IssueStatus) {
                      IssueStatus, $q) {
                $scope.fn = {
                    changeTab : changeTab,
                    getIssueTypeCallback : getIssueTypeCallback,
                    getProjectListCallback : getProjectListCallback,
                    formSubmit : formSubmit,
                    formSubmitColumnSetting : formSubmitColumnSetting,
                    formCheck : formCheck,
                    initForm : initForm,
                    getPriorities : getPriorities,
                    getSeverities : getSeverities,
                    onChangeColumnSetting : onChangeColumnSetting,
                    onChangeEndIssueStatus : onChangeEndIssueStatus,
                    getIssueTypes : getIssueTypes,
                    onChangeIssueType : onChangeIssueType,
                    reset : reset,
                    resetOverlap : resetOverlap,
                    formSubmitOverlap : formSubmitOverlap,
@@ -42,7 +42,10 @@
                    getProjects : getProjects,
                    formSubmitCompleteIssueStatus : formSubmitCompleteIssueStatus,
                    loadPage : loadPage,
                    resetCompleteIssueStatus : resetCompleteIssueStatus
                    resetCompleteIssueStatus : resetCompleteIssueStatus,
                    findIssueList : findIssueList,
                    onChangeProject : onChangeProject,
                    setApiIssueTypeStatus : setApiIssueTypeStatus
                };
                $scope.vm = {
@@ -103,7 +106,7 @@
                function reset() {
                    $scope.fn.initForm();
                    $scope.fn.formSubmit();
                    $scope.fn.formSubmitColumnSetting();
                }
                // 자동종료 설정 초기화
@@ -158,9 +161,7 @@
                                if ($scope.vm.issueTypeId === "") {
                                    $scope.vm.issueTypeId = $scope.vm.issueTypes[0].id.toString();
                                }
                                if ($scope.vm.tab === "") {
                                    $scope.fn.changeTab("API_COL_SETTING");
                                }
                                // if ($scope.vm.tab === "API_COL_SETTING") {
                                //     $scope.fn.onChangeIssueType();
@@ -188,46 +189,23 @@
                    return issueTypeVo;
                }
                function onChangeIssueTypeOverlap() {
                    $scope.fn.getIssueTypeCustomFields();
                    $scope.fn.getOverlapList();
                // 자동 동료 이슈 상태 변경시
                function onChangeEndIssueStatus() {
                }
                function onChangeIssueType() {
                    if ($scope.vm.issueTypeId != null) {
                        let conditions = {
                            issueTypeId: $scope.vm.issueTypeId
                        }
                        Api.findApiDefault($resourceProvider.getContent(
                            conditions, $resourceProvider.getPageContent(0, 1000))).then(function (result) {
                            $scope.fn.initForm();
                            if (result.data.message.status === "success") {
                                if (angular.isDefined(result.data.data)) {
                                    $scope.vm.form.issueApiDefault = result.data.data;
                                    $scope.vm.form.issueApiDefault.priorityId = result.data.data.priorityId != null ? result.data.data.priorityId.toString() : "";
                                    $scope.vm.form.issueApiDefault.severityId = result.data.data.severityId != null ? result.data.data.severityId.toString() : "";
                                    // if (angular.isDefined(result.data.data.projectVo)) {
                                    //     $scope.vm.projects = [];
                                    //     $scope.vm.projects.push(result.data.data.projectVo);
                                    // }
                                }
                            } else {
                                SweetAlert.swal($filter("translate")("common.failedToIssueTypeDefault"), result.data.message.message, "error"); // "프로젝트 목록 조회 실패"
                            }
                        });
                    }
                function onChangeIssueTypeOverlap() {
                    $scope.fn.getIssueStatuses();
                    $scope.fn.getIssueTypeCustomFields();
                    $scope.fn.getOverlapList();
                }
                function formSubmitCompleteIssueStatus() {
                    if ($scope.vm.issueTypeId != null) {
                        let content = {
                            id: $scope.vm.issueTypeId,
                            completeIssueStatusId: $scope.vm.completeIssueStatusId
                            issueTypeId : $scope.vm.issueTypeId,
                            projectId : $scope.vm.projectId,
                            issueStatusId: $scope.vm.completeIssueStatusId === "none" ? null : $scope.vm.completeIssueStatusId
                        }
                        Api.saveCompleteIssueStatus($resourceProvider.getContent(
@@ -235,7 +213,8 @@
                            if (result.data.message.status === "success") {
                                SweetAlert.swal($filter("translate")("api.successToApiAutoCompleteIssueStatus"), result.data.message.message, "success"); // "설정 성공"
                                $scope.fn.getIssueTypes();
                                //$scope.fn.getIssueTypes();
                                $scope.fn.findIssueList($scope.vm.projectId);
                            } else {
                                SweetAlert.swal($filter("translate")("api.failedToApiAutoCompleteIssueStatus"), result.data.message.message, "error"); // "설정 실패"
                            }
@@ -252,14 +231,14 @@
                    return false;
                }
                function formSubmit() {
                function formSubmitColumnSetting() {
                    if ($scope.vm.issueTypeId == null)
                        return;
                    let condition = {
                        issueTypeId : $scope.vm.issueTypeId,
                        title : $scope.vm.form.issueApiDefault.title,
                        // projectId : $scope.vm.projects != null && $scope.vm.projects.length > 0 ? $scope.vm.projects[0].id : null,
                        projectId : $scope.vm.projectId,
                        priorityId : $scope.vm.form.issueApiDefault.priorityId,
                        severityId : $scope.vm.form.issueApiDefault.severityId,
                        description : $scope.vm.form.issueApiDefault.description,
@@ -289,6 +268,7 @@
                    let condition = {
                        issueTypeId : $scope.vm.issueTypeId,
                        projectId : $scope.vm.projectId,
                        customFieldIds : (function () {
                            var ids = [];
@@ -400,37 +380,35 @@
                    if (tab === "API_COL_SETTING") {
                        $scope.fn.onChangeColumnSetting();
                    } else if (tab === "API_OVERLAP_SETTING") {
                        $scope.fn.getIssueStatuses();
                        $scope.fn.onChangeIssueTypeOverlap();
                    } else if (tab === "API_SPEC_SETTING") {
                        $scope.fn.onChangeIssueTypeSpec();
                    }
                }
                $scope.$on("getIssueStatusComplete", function (event, args){
                    if ($scope.vm.tab === "API_OVERLAP_SETTING") {
                        if ($scope.vm.issueStatuses != null) {
                            $scope.vm.completeIssueStatuses = [];
                // 자동 종료 이슈 상태 적용
                function setApiIssueTypeStatus() {
                    if ($scope.vm.issueStatuses != null) {
                        $scope.vm.completeIssueStatuses = [];
                            $scope.vm.issueStatuses.forEach(function (issueStatus) {
                                if (issueStatus.issueStatusType === "CLOSE") {
                                    $scope.vm.completeIssueStatuses.push(issueStatus);
                        $scope.vm.issueStatuses.forEach(function (issueStatus) {
                            if (issueStatus.issueStatusType === "CLOSE") {
                                $scope.vm.completeIssueStatuses.push(issueStatus);
                            }
                        });
                        // 설정된 상태 지정
                        $scope.vm.completeIssueStatusId = "none";
                        let issueTypeVo = $scope.fn.getCurrentIssueTypeVo();
                        if (issueTypeVo.completeIssueStatusVo != null) {
                            $scope.vm.completeIssueStatuses.forEach(function (issueStatus) {
                                if (issueStatus.id === issueTypeVo.completeIssueStatusVo.id) {
                                    $scope.vm.completeIssueStatusId = issueStatus.id.toString();
                                }
                            });
                            // 설정된 상태 지정
                            $scope.vm.completeIssueStatusId = "";
                            let issueTypeVo = $scope.fn.getCurrentIssueTypeVo();
                            if (issueTypeVo.completeIssueStatusVo != null) {
                                $scope.vm.completeIssueStatuses.forEach(function (issueStatus) {
                                    if (issueStatus.id === issueTypeVo.completeIssueStatusVo.id) {
                                        $scope.vm.completeIssueStatusId = issueStatus.id.toString();
                                    }
                                });
                            }
                        }
                    }
                });
                }
                function getIssueStatuses() {
                    var condition = {
@@ -443,11 +421,14 @@
                            $scope.vm.issueStatuses = result.data.data;
                            // $scope.vm.issueStatusId = "";
                            if ($scope.vm.issueTypeId === "") {
                                if ($scope.vm.issueStatuses != null && $scope.vm.issueStatuses.length > 0) {
                                    $scope.vm.issueStatusId = $scope.vm.issueStatuses[0].id.toString();
                                }
                            if ($scope.vm.issueStatuses != null && $scope.vm.issueStatuses.length > 0) {
                                $scope.vm.issueStatusId = $scope.vm.issueStatuses[0].id.toString();
                            }
                            if ($scope.vm.tab === "API_OVERLAP_SETTING") {
                                $scope.fn.setApiIssueTypeStatus();
                            }
                            $scope.$broadcast("getIssueStatusComplete", $scope.vm.issueStatuses);
                        } else {
                            SweetAlert.swal($filter("translate")("issue.failedToCriticalListLookup"), result.data.message.message, "error"); // 중요도 목록 조회 실패
@@ -456,11 +437,70 @@
                }
                function onChangeColumnSetting() {
                    $scope.fn.getSeverities();
                    $scope.fn.getPriorities();
                    $scope.fn.onChangeIssueType();
                    var promises = {
                        severities : $scope.fn.getSeverities(),
                        priorities : $scope.fn.getPriorities(),
                    }
                    $q.all(promises).then(function (results) {
                        if ($scope.vm.issueTypeId != null && $scope.vm.issueTypeId !== "none") {
                            let conditions = {
                                issueTypeId: $scope.vm.issueTypeId
                            }
                            Api.findApiDefault($resourceProvider.getContent(
                                conditions, $resourceProvider.getPageContent(0, 1000))).then(function (result) {
                                $scope.fn.initForm();
                                if (result.data.message.status === "success") {
                                    if (angular.isDefined(result.data.data)) {
                                        $scope.vm.form.issueApiDefault = result.data.data;
                                        $scope.vm.form.issueApiDefault.priorityId = result.data.data.priorityId != null ? result.data.data.priorityId.toString() : "";
                                        $scope.vm.form.issueApiDefault.severityId = result.data.data.severityId != null ? result.data.data.severityId.toString() : "";
                                    }
                                } else {
                                    SweetAlert.swal($filter("translate")("common.failedToIssueTypeDefault"), result.data.message.message, "error"); // "프로젝트 목록 조회 실패"
                                }
                            });
                        }
                    });
                }
                // 프로젝트 변경시
                function onChangeProject() {
                    $scope.fn.findIssueList($scope.vm.projectId);
                }
                // 이슈 유형 목록 가져오기
                function findIssueList(projectId) {
                    if ($rootScope.projects == null || $rootScope.projects.length <= 1)
                        return;
                    //  이슈 타입 목록 검색 조건을 만든다.
                    var conditions = {
                        projectId : projectId > -1 ? projectId : null,
                        useProject : true,
                        deep : "01" //  이슈 유형에 연결된 워크플로우 정보를 찾는다.
                    }
                    IssueType.find($resourceProvider.getContent(conditions,
                        $resourceProvider.getPageContent(0, 100))).then(function (result) {
                        if (result.data.message.status === "success") {
                            $scope.vm.issueTypes = result.data.data;
                            if ($scope.vm.issueTypes != null && $scope.vm.issueTypes.length > 0) {
                                $scope.vm.issueTypeId = $scope.vm.issueTypes[0].id.toString();
                                $scope.fn.getIssueStatuses();
                                $scope.fn.getIssueTypeCustomFields();
                            } else {
                                $scope.vm.issueTypeId = "none";
                            }
                        }
                        else {
                            SweetAlert.error($filter("translate")("managementType.failedToIssueTypeList"), result.data.message.message); // "이슈 유형 목록 조회 실패"
                        }
                    });
                }
                function onChangeIssueTypeSpec() {
                    $scope.fn.getIssueStatuses();
@@ -514,7 +554,8 @@
                    if ($rootScope.projects != null && $rootScope.projects.length > 0) {
                        // 공통 데이터 불러오기
                        $scope.fn.getProjects();
                        $scope.fn.getIssueTypes();
                        $scope.fn.findIssueList($scope.vm.projectId);
                        // $scope.fn.getIssueTypes();
                    }
                }, true);
@@ -534,6 +575,8 @@
                    }
                }
                if ($scope.vm.tab === "") {
                    $scope.fn.changeTab("API_COL_SETTING");
                }
            }]);
    });
src/main/webapp/scripts/config.js
@@ -285,17 +285,17 @@
                    return false;
                };
                $rootScope.getMyInfo = function () {
                    User.findMyLevelAndDepartment($resourceProvider.getContent({},
                        $resourceProvider.getPageContent(0, 0))).then(function (result) {
                        if (result.data.message.status === "success") {
                            $rootScope.myLevel = result.data.data.levelName
                            $rootScope.myDepartments = result.data.data.departmentName
                        }
                    });
                }
                // $rootScope.getMyInfo = function () {
                //
                //     User.findMyLevelAndDepartment($resourceProvider.getContent({},
                //         $resourceProvider.getPageContent(0, 0))).then(function (result) {
                //
                //         if (result.data.message.status === "success") {
                //             $rootScope.myLevel = result.data.data.levelName
                //             $rootScope.myDepartments = result.data.data.departmentName
                //         }
                //     });
                // }
                /*$rootScope.checkMngPermissionViewIssueAndProject = function (userPermission) {
                    if (!$rootScope.isDefined($rootScope.user)) {
@@ -549,7 +549,7 @@
                    //  이슈 목록->상세화면에서 마지막으로 접근한 이슈 아이디 - 라우트 탈때마다 초기화
                    $rootScope.currentDetailIssueId = null;
                    // 사용자 정보를 가져온다.
                    $rootScope.getMyInfo();
                    // $rootScope.getMyInfo();
                    $log.debug("toState.name : ", toState.name);
                    $log.debug("$rootScope.previousStateName 확인 : ", $rootScope.previousStateName);
src/main/webapp/views/api/apiSetting.html
@@ -25,12 +25,15 @@
            <div class="tab-content mt-30">
                <div ng-show="vm.tab == 'API_SPEC_SETTING'">
                    <div ng-include include-replace src="'views/api/apiSettingHeader.html'"></div>
                    <div ng-include include-replace src="'views/api/apiSettingSpec.html'"></div>
                </div>
                <div ng-show="vm.tab == 'API_COL_SETTING'">
                    <div ng-include include-replace src="'views/api/apiSettingHeader.html'"></div>
                    <div ng-include include-replace src="'views/api/apiSettingColumn.html'"></div>
                </div>
                <div ng-show="vm.tab == 'API_OVERLAP_SETTING'">
                    <div ng-include include-replace src="'views/api/apiSettingHeader.html'"></div>
                    <div ng-include include-replace src="'views/api/apiSettingOverlap.html'"></div>
                </div>
              </div>
src/main/webapp/views/api/apiSettingColumn.html
@@ -1,46 +1,3 @@
<div class="row">
    <div class="col-md-4" ng-if="false">
        <div class="form-group mb10">
            <label for="projectForm" class="issue-label">
                <span translate="common.project">프로젝트</span>
            </label>
            <select id="projectForm"
                    name="project"
                    class="form-control input-sm issue-select-label"
                    ng-model="vm.projectId"
                    ng-change="fn.onChangeIssueTypeSpec()"
                    required>
                <option ng-repeat="project in vm.projects"
                        value="{{project.id}}"
                        translate="{{project.name}}(id:{{project.id}})">
                </option>
            </select>
        </div>
    </div>
    <div class="col-md-4">
        <div class="form-group mb10">
            <label for="issueTypeForm" class="issue-label">
                <span translate="issue.issueType">이슈 유형</span>
            </label>
            <select id="issueTypeForm"
                    name="issueType"
                    class="form-control input-sm issue-select-label"
                    ng-model="vm.issueTypeId"
                    ng-style="{ 'color' : fn.getOptionColor(vm.issueTypes, vm.issueTypeId) }"
                    ng-change="fn.onChangeIssueType()"
                    required>
                <option ng-repeat="issueType in vm.issueTypes"
                        ng-style="{ 'color' : issueType.color, 'font-weight': 600 }"
                        value="{{issueType.id}}"
                        translate="{{issueType.name}}(id:{{issueType.id}})">
                </option>
            </select>
        </div>
    </div>
</div>
<div class="element-box">
    <form role="form" name="apiSettingColumnForm">
        <div class="form-group mb10">
@@ -140,8 +97,8 @@
        </button>
        <button type="button" class="btn btn-md btn-primary bold"
                js-short-cut
                js-short-cut-action="(fn.formCheck(apiSettingColumnForm.$invalid) || $root.spinner) ? null : fn.formSubmit()"
                ng-click="fn.formSubmit()"><span translate="common.saved">생성</span>
                js-short-cut-action="(fn.formCheck(apiSettingColumnForm.$invalid) || $root.spinner) ? null : fn.formSubmitColumnSetting()"
                ng-click="fn.formSubmitColumnSetting()"><span translate="common.saved">생성</span>
        </button>
    </div>
</div>
src/main/webapp/views/api/apiSettingHeader.html
New file
@@ -0,0 +1,42 @@
<div class="row">
    <div class="col-md-4">
        <div class="form-group mb10">
            <label for="projectForm" class="issue-label">
                <span translate="common.project">프로젝트</span>
            </label>
            <select id="projectForm"
                    name="project"
                    class="form-control input-sm issue-select-label"
                    ng-model="vm.projectId"
                    ng-change="fn.onChangeProject()"
                    required>
                <option ng-repeat="project in vm.projects"
                        value="{{project.id}}"
                        translate="{{project.name}}(id:{{project.id}})">
                </option>
            </select>
        </div>
    </div>
    <div class="col-md-4">
        <div class="form-group mb10">
            <label for="issueTypeForm" class="issue-label">
                <span translate="issue.issueType">이슈 유형</span>
            </label>
            <select id="issueTypeForm"
                    name="issueType"
                    class="form-control input-sm issue-select-label"
                    ng-style="{ 'color' : fn.getOptionColor(vm.issueTypes, vm.issueTypeId) }"
                    ng-model="vm.issueTypeId"
                    ng-change="fn.loadPage()"
                    required>
                <option ng-if="vm.issueTypes == null || vm.issueTypes.length == 0" value="none" translate="common.none"></option>
                <option ng-repeat="issueType in vm.issueTypes"
                        ng-style="{ 'color' : issueType.color, 'font-weight': 600 }"
                        value="{{issueType.id}}"
                        translate="{{issueType.name}}(id:{{issueType.id}})">
                </option>
            </select>
        </div>
    </div>
</div>
src/main/webapp/views/api/apiSettingOverlap.html
@@ -1,52 +1,3 @@
<div class="row">
    <div class="col-md-4" ng-if="false">
        <div class="form-group mb10">
            <label for="projectForm" class="issue-label">
                <span translate="common.project">프로젝트</span>
            </label>
            <select id="projectForm"
                    name="project"
                    class="form-control input-sm issue-select-label"
                    ng-model="vm.projectId"
                    ng-change="fn.onChangeIssueTypeSpec()"
                    required>
                <option ng-repeat="project in vm.projects"
                        value="{{project.id}}"
                        translate="{{project.name}}(id:{{project.id}})">
                </option>
            </select>
        </div>
    </div>
    <div class="col-sm-4">
        <div class="element-wrapper">
            <div class="form-group mb10">
                <label for="issueTypeForm" class="issue-label">
                    <span translate="issue.issueType">이슈 유형</span>
                </label>
                <select id="issueTypeForm"
                        name="issueType"
                        class="form-control input-sm issue-select-label"
                        ng-model="vm.issueTypeId"
                        ng-style="{ 'color' : fn.getOptionColor(vm.issueTypes, vm.issueTypeId) }"
                        ng-change="fn.onChangeIssueTypeOverlap()"
                        required>
                    <option ng-repeat="issueType in vm.issueTypes"
                            ng-style="{ 'color' : issueType.color, 'font-weight': 600 }"
                            value="{{issueType.id}}"
                            translate="{{issueType.name}}">
                    </option>
                </select>
            </div>
        </div>
    </div>
</div>
<label for="issueTypeForm" class="issue-label">
    <span translate="api.upIssueCompleteIssueStatus">상위 이슈 자동종료 이슈 상태 설정</span>
</label>
@@ -62,8 +13,9 @@
                    class="form-control input-sm issue-select-label"
                    ng-style="{ 'color' : fn.getOptionColor(vm.completeIssueStatuses, vm.completeIssueStatusId) }"
                    ng-model="vm.completeIssueStatusId"
                    ng-change="fn.onChangeIssueTypeOverlap()"
                    ng-change="fn.onChangeEndIssueStatus()"
                    required>
                <option value="none" translate="common.select"></option>
                <option ng-repeat="issueStatus in vm.completeIssueStatuses"
                        ng-style="{ 'color' : issueStatus.color, 'font-weight': 600 }"
                        value="{{issueStatus.id}}"
src/main/webapp/views/api/apiSettingSpec.html
@@ -1,45 +1,3 @@
<div class="row">
    <div class="col-md-4">
        <div class="form-group mb10">
            <label for="projectForm" class="issue-label">
                <span translate="common.project">프로젝트</span>
            </label>
            <select id="projectForm"
                    name="project"
                    class="form-control input-sm issue-select-label"
                    ng-model="vm.projectId"
                    ng-change="fn.onChangeIssueTypeSpec()"
                    required>
                <option ng-repeat="project in vm.projects"
                        value="{{project.id}}"
                        translate="{{project.name}}(id:{{project.id}})">
                </option>
            </select>
        </div>
    </div>
    <div class="col-md-4">
        <div class="form-group mb10">
            <label for="issueTypeForm" class="issue-label">
                <span translate="issue.issueType">이슈 유형</span>
            </label>
            <select id="issueTypeForm"
                    name="issueType"
                    class="form-control input-sm issue-select-label"
                    ng-style="{ 'color' : fn.getOptionColor(vm.issueTypes, vm.issueTypeId) }"
                    ng-model="vm.issueTypeId"
                    ng-change="fn.onChangeIssueTypeSpec()"
                    required>
                <option ng-repeat="issueType in vm.issueTypes"
                        ng-style="{ 'color' : issueType.color, 'font-weight': 600 }"
                        value="{{issueType.id}}"
                        translate="{{issueType.name}}(id:{{issueType.id}})">
                </option>
            </select>
        </div>
    </div>
</div>
<div class="element-box">
    <form role="form" name="apiSettingColumnForm">
        <div class="form-group mb10">
src/main/webapp/views/customField/customFieldAdd.html
@@ -131,7 +131,7 @@
                </div>
                <input ng-if="vm.form.customFieldType == 'IP_ADDRESS'"
                       name="ipAdress"
                       name="ipAddress"
                       type="text"
                       class="form-control"
                       kr-input
@@ -139,7 +139,7 @@
                       placeholder="IP 주소 형식만 입력 가능합니다."
                       autocomplete="off"
                       ng-model="vm.form.defaultValue">
                <div ng-show="customFieldAddForm.ipAdress.$error.pattern" class="help-block form-text text-danger"
                <div ng-show="customFieldAddForm.ipAddress.$error.pattern" class="help-block form-text text-danger"
                     translate="common.invalidipAdressFormat">IP주소 형식이 맞지 않습니다.
                </div>
@@ -161,9 +161,9 @@
                       name="site"
                       type="text"
                       class="form-control"
                       maxlength="30"
                       maxlength="100"
                       kr-input
                       ng-pattern="/((http|https):\/\/)?(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/"
                       ng-pattern="/(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/"
                       placeholder="홈페이지 주소 형식만 입력 가능합니다."
                       autocomplete="off"
                       ng-model="vm.form.defaultValue">
@@ -171,10 +171,10 @@
                     translate="common.invalidSiteFormat">홈페이지 주소 형식이 맞지 않습니다.(http://로 시작하셔야합니다)
                </div>
                <input ng-if="vm.form.customFieldType == 'TEL'"
                       name="tel"
                       type="text"
                       maxlength="30"
                       class="form-control"
                       kr-input
                       ng-pattern="/^\d{2,3}-\d{3,4}-\d{4}$/"
src/main/webapp/views/customField/customFieldModify.html
@@ -107,17 +107,17 @@
                       placeholder="숫자만 입력 가능합니다."
                       autocomplete="off"
                       ng-model="vm.form.defaultValue">
                <div ng-show="customFieldAddForm.ipAdress.$error.pattern" class="help-block form-text text-danger"
                <div ng-show="customFieldModifyForm.numberType.$error.pattern" class="help-block form-text text-danger"
                     translate="common.invalidNumberFormat">숫자만 입력 가능합니다.
                </div>
                <input ng-if="vm.form.customFieldType == 'DATETIME'"
                       name="dateTime"
                       class="form-control input-readonly"
                       placeholder="{{'issue.clickToSelectDate' | translate}}"
                       ng-model="vm.form.defaultValue"
                       modal-form-auto-scroll
                       range-type="singleDate"
                       onfocus="this.blur()"
                       date-range-picker>
                <div class="row">
                    <div class="col-xs-12">
@@ -126,7 +126,7 @@
                </div>
                <input ng-if="vm.form.customFieldType == 'IP_ADDRESS'"
                       name="ipAdress"
                       name="ipAddress"
                       type="text"
                       class="form-control"
                       kr-input
@@ -134,7 +134,7 @@
                       placeholder="IP 주소 형식만 입력 가능합니다."
                       autocomplete="off"
                       ng-model="vm.form.defaultValue">
                <div ng-show="customFieldAddForm.ipAdress.$error.pattern" class="help-block form-text text-danger"
                <div ng-show="customFieldModifyForm.ipAddress.$error.pattern" class="help-block form-text text-danger"
                     translate="common.invalidipAdressFormat">IP주소 형식이 맞지 않습니다.
                </div>
@@ -148,7 +148,7 @@
                       placeholder="이메일 형식만 입력 가능합니다."
                       autocomplete="off"
                       ng-model="vm.form.defaultValue">
                <div ng-show="customFieldAddForm.email.$error.pattern" class="help-block form-text text-danger"
                <div ng-show="customFieldModifyForm.email.$error.pattern" class="help-block form-text text-danger"
                     translate="common.invalidEmailFormat">이메일 형식이 맞지 않습니다.
                </div>
@@ -156,27 +156,27 @@
                       name="site"
                       type="text"
                       class="form-control"
                       maxlength="30"
                       maxlength="100"
                       kr-input
                       ng-pattern="/(http(s)?:\/\/)([a-z0-9\w]+\.*)+[a-z0-9]{2,4}/gi"
                       ng-pattern="/(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/"
                       placeholder="홈페이지 주소 형식만 입력 가능합니다."
                       autocomplete="off"
                       ng-model="vm.form.defaultValue">
                <div ng-show="customFieldAddForm.site.$error.pattern" class="help-block form-text text-danger"
                <div ng-show="customFieldModifyForm.site.$error.pattern" class="help-block form-text text-danger"
                     translate="common.invalidSiteFormat">홈페이지 주소 형식이 맞지 않습니다.(http://로 시작하셔야합니다)
                </div>
                <input ng-if="vm.form.customFieldType == 'TEL'"
                       name="tel"
                       type="text"
                       maxlength="30"
                       class="form-control"
                       kr-input
                       ng-pattern="/^\d{2,3}-\d{3,4}-\d{4}$/"
                       placeholder="연락처 형식만 입력 가능합니다."
                       autocomplete="off"
                       ng-model="vm.form.defaultValue">
                <div ng-show="customFieldAddForm.tel.$error.pattern" class="help-block form-text text-danger"
                <div ng-show="customFieldModifyForm.tel.$error.pattern" class="help-block form-text text-danger"
                     translate="common.invalidTelFormat">전화번호 형식이 맞지 않습니다.
                </div>
src/main/webapp/views/issue/issueAdd.html
@@ -236,7 +236,7 @@
                                <!-- 기본 입력 -->
                                <div ng-switch-when="INPUT">
                                    <input type="text" class="form-control input-sm"
                                           name="input"
                                           name="inputValue"
                                           ng-model="issueCustomField.useValues"
                                           maxlength="100"
                                           autocomplete="off"
@@ -244,32 +244,39 @@
                                           ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'">
                                    <small class="help-block form-text text-danger"
                                           ng-show="issueCustomField.customFieldVo.requiredData == 'Y'"
                                           ng-if="issueAddForm.input.$error.required"
                                           ng-if="issueAddForm.inputValue.$error.required"
                                           translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자정의필드는 필수 입력 값 입니다.
                                    </small>
                                </div>
                                <div ng-switch-when="NUMBER">
                                    <input type="text" class="form-control input-sm"
                                           name="number"
                                           name="numberType"
                                           ng-model="issueCustomField.useValues"
                                           maxlength="100"
                                           autocomplete="off"
                                           kr-input
                                           ng-pattern="/^[0-9]*$/"
                                           placeholder="숫자만 입력 가능합니다."
                                           ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'">
                                    <small class="help-block form-text text-danger"
                                           ng-show="issueCustomField.customFieldVo.requiredData == 'Y'"
                                           ng-if="issueAddForm.number.$error.required"
                                           ng-if="issueAddForm.numberType.$error.required"
                                           translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다.
                                    </small>
                                    <div ng-show="issueAddForm.numberType.$error.pattern" class="help-block form-text text-danger"
                                         translate="common.invalidNumberFormat">숫자만 입력 가능합니다.
                                    </div>
                                </div>
                                <div ng-switch-when="DATETIME">
                                    <input type="text" class="form-control input-sm"
                                    <input class="form-control input-sm input-readonly"
                                           name="dateTime"
                                           ng-model="issueCustomField.useValues"
                                           maxlength="100"
                                           autocomplete="off"
                                           placeholder="{{'issue.clickToSelectDate' | translate}}"
                                           modal-form-auto-scroll
                                           range-type="singleDate"
                                           date-range-picker
                                           kr-input
                                           ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'">
                                    <small class="help-block form-text text-danger"
@@ -283,15 +290,38 @@
                                    <input type="text" class="form-control input-sm"
                                           name="ipAddress"
                                           ng-model="issueCustomField.useValues"
                                           maxlength="100"
                                           autocomplete="off"
                                           kr-input
                                           ng-pattern="/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/"
                                           placeholder="IP 주소 형식만 입력 가능합니다."
                                           ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'">
                                    <small class="help-block form-text text-danger"
                                           ng-show="issueCustomField.customFieldVo.requiredData == 'Y'"
                                           ng-if="issueAddForm.ipAddress.$error.required"
                                           translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다.
                                    </small>
                                    <div ng-show="issueAddForm.ipAddress.$error.pattern" class="help-block form-text text-danger"
                                         translate="common.invalidipAdressFormat">IP주소 형식이 맞지 않습니다.
                                    </div>
                                </div>
                                <div ng-switch-when="EMAIL">
                                    <input type="email" class="form-control input-sm"
                                           name="email"
                                           maxlength="30"
                                           ng-model="issueCustomField.useValues"
                                           kr-input
                                           ng-pattern="/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/"
                                           placeholder="이메일 형식만 입력 가능합니다."
                                           ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'">
                                    <small class="help-block form-text text-danger"
                                           ng-show="issueCustomField.customFieldVo.requiredData == 'Y'"
                                           ng-if="issueAddForm.ipAddress.$error.required"
                                           translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다.
                                    </small>
                                    <div ng-show="issueAddForm.email.$error.pattern" class="help-block form-text text-danger"
                                         translate="common.invalidEmailFormat">이메일 형식이 맞지 않습니다.
                                    </div>
                                </div>
                                <div ng-switch-when="SITE">
@@ -301,27 +331,37 @@
                                           maxlength="100"
                                           autocomplete="off"
                                           kr-input
                                           ng-pattern="/(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/"
                                           placeholder="홈페이지 주소 형식만 입력 가능합니다."
                                           ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'">
                                    <small class="help-block form-text text-danger"
                                           ng-show="issueCustomField.customFieldVo.requiredData == 'Y'"
                                           ng-if="issueAddForm.site.$error.required"
                                           translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다.
                                    </small>
                                    <div ng-show="issueAddForm.site.$error.pattern" class="help-block form-text text-danger"
                                         translate="common.invalidSiteFormat">홈페이지 주소 형식이 맞지 않습니다.(http:// 또는 www 로 시작하셔야합니다)
                                    </div>
                                </div>
                                <div ng-switch-when="TEL">
                                    <input type="text" class="form-control input-sm"
                                           name="tel"
                                           ng-model="issueCustomField.useValues"
                                           maxlength="100"
                                           maxlength="30"
                                           autocomplete="off"
                                           kr-input
                                           ng-pattern="/^\d{2,3}-\d{3,4}-\d{4}$/"
                                           placeholder="연락처 형식만 입력 가능합니다."
                                           ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'">
                                    <small class="help-block form-text text-danger"
                                           ng-show="issueCustomField.customFieldVo.requiredData == 'Y'"
                                           ng-if="issueAddForm.tel.$error.required"
                                           translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다.
                                    </small>
                                    <div ng-show="issueAddForm.tel.$error.pattern" class="help-block form-text text-danger"
                                         translate="common.invalidTelFormat">전화번호 형식이 맞지 않습니다.
                                    </div>
                                </div>
                                <!-- 단일 셀렉트 -->
src/main/webapp/views/issue/issueModify.html
@@ -236,7 +236,7 @@
                                <!-- 기본 입력 -->
                                <div ng-switch-when="INPUT">
                                    <input type="text" class="form-control input-sm"
                                           name="input"
                                           name="inputValue"
                                           ng-model="issueCustomField.useValues"
                                           maxlength="100"
                                           autocomplete="off"
@@ -244,24 +244,29 @@
                                           ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'">
                                    <small class="help-block form-text text-danger"
                                           ng-show="issueCustomField.customFieldVo.requiredData == 'Y'"
                                           ng-if="issueModifyForm.input.$error.required"
                                           ng-if="issueModifyForm.inputValue.$error.required"
                                           translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자정의필드는 필수 입력 값 입니다.
                                    </small>
                                </div>
                                <div ng-switch-when="NUMBER">
                                    <input type="text" class="form-control input-sm"
                                           name="number"
                                           name="numberType"
                                           ng-model="issueCustomField.useValues"
                                           maxlength="100"
                                           autocomplete="off"
                                           kr-input
                                           ng-pattern="/^[0-9]*$/"
                                           placeholder="숫자만 입력 가능합니다."
                                           ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'">
                                    <small class="help-block form-text text-danger"
                                           ng-show="issueCustomField.customFieldVo.requiredData == 'Y'"
                                           ng-if="issueModifyForm.number.$error.required"
                                           ng-if="issueModifyForm.numberType.$error.required"
                                           translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다.
                                    </small>
                                    <div ng-show="issueModifyForm.numberType.$error.pattern" class="help-block form-text text-danger"
                                         translate="common.invalidNumberFormat">숫자만 입력 가능합니다.
                                    </div>
                                </div>
                                <div ng-switch-when="DATETIME">
@@ -286,12 +291,36 @@
                                           maxlength="100"
                                           autocomplete="off"
                                           kr-input
                                           ng-pattern="/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/"
                                           placeholder="IP 주소 형식만 입력 가능합니다."
                                           ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'">
                                    <small class="help-block form-text text-danger"
                                           ng-show="issueCustomField.customFieldVo.requiredData == 'Y'"
                                           ng-if="issueModifyForm.ipAddress.$error.required"
                                           translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다.
                                    </small>
                                    <div ng-show="issueModifyForm.ipAddress.$error.pattern" class="help-block form-text text-danger"
                                         translate="common.invalidipAdressFormat">IP주소 형식이 맞지 않습니다.
                                    </div>
                                </div>
                                <div ng-switch-when="EMAIL">
                                    <input type="email" class="form-control input-sm"
                                           name="email"
                                           maxlength="30"
                                           ng-model="issueCustomField.useValues"
                                           kr-input
                                           ng-pattern="/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/"
                                           placeholder="이메일 형식만 입력 가능합니다."
                                           ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'">
                                    <small class="help-block form-text text-danger"
                                           ng-show="issueCustomField.customFieldVo.requiredData == 'Y'"
                                           ng-if="issueModifyForm.ipAddress.$error.required"
                                           translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다.
                                    </small>
                                    <div ng-show="issueModifyForm.email.$error.pattern" class="help-block form-text text-danger"
                                         translate="common.invalidEmailFormat">이메일 형식이 맞지 않습니다.
                                    </div>
                                </div>
                                <div ng-switch-when="SITE">
@@ -301,27 +330,37 @@
                                           maxlength="100"
                                           autocomplete="off"
                                           kr-input
                                           ng-pattern="/(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/"
                                           placeholder="홈페이지 주소 형식만 입력 가능합니다."
                                           ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'">
                                    <small class="help-block form-text text-danger"
                                           ng-show="issueCustomField.customFieldVo.requiredData == 'Y'"
                                           ng-if="issueModifyForm.site.$error.required"
                                           translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다.
                                    </small>
                                    <div ng-show="issueModifyForm.site.$error.pattern" class="help-block form-text text-danger"
                                         translate="common.invalidSiteFormat">홈페이지 주소 형식이 맞지 않습니다.(http://로 시작하셔야합니다)
                                    </div>
                                </div>
                                <div ng-switch-when="TEL">
                                    <input type="text" class="form-control input-sm"
                                           name="tel"
                                           ng-model="issueCustomField.useValues"
                                           maxlength="100"
                                           maxlength="30"
                                           autocomplete="off"
                                           kr-input
                                           ng-pattern="/^\d{2,3}-\d{3,4}-\d{4}$/"
                                           placeholder="연락처 형식만 입력 가능합니다."
                                           ng-required="issueCustomField.fieldOption == '01' || issueCustomField.customFieldVo.requiredData == 'Y'">
                                    <small class="help-block form-text text-danger"
                                           ng-show="issueCustomField.customFieldVo.requiredData == 'Y'"
                                           ng-if="issueModifyForm.tel.$error.required"
                                           translate="issue.pleaseEnterIssueTypeCustomFields">해당 사용자 정의 필드는 필수 입력 값 입니다.
                                    </small>
                                    <div ng-show="issueModifyForm.tel.$error.pattern" class="help-block form-text text-danger"
                                         translate="common.invalidTelFormat">전화번호 형식이 맞지 않습니다.
                                    </div>
                                </div>
                                <!-- 단일 셀렉트 -->