OWL ITS + 탐지시스템(인터넷 진흥원)
wyu
2021-12-22 6bd87f2bc7af7361f126451b95dc279b44e9f138
Merge branch 'master' of http://192.168.0.25:9001/r/owl-kisa
8개 파일 추가됨
23개 파일 변경됨
853 ■■■■■ 파일 변경됨
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/assets/styles/main.css 7 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/js-table/js-table.html 4 ●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/js-table/table.provider.js 5 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/js-tree/tree.provider.js 5 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/widget/member-progress-widget/member-progress-widget.html 2 ●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/api/apiSetting.controller.js 191 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/issue/issueList.controller.js 1 ●●●● 패치 | 보기 | 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 8 ●●●● 패치 | 보기 | 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/assets/styles/main.css
@@ -30152,6 +30152,13 @@
    font-weight: 800;
}
.custom-detail-label {
    color: #777;
    font-weight: 600;
    position: relative;
    top: -6px;
}
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
    -webkit-appearance: none;
src/main/webapp/custom_components/js-table/js-table.html
@@ -15,7 +15,9 @@
                    <input type="checkbox" tabindex="-1" ng-model="tableConfig.hChecked" ng-click="$root.$tableProvider.toggleChecked(tableConfig.hChecked, fn.getResponseData())">
                </div>
                <div bo-switch-default>
                    <span translate="{{tableConfig.hName}}"></span>
                    <span ng-if="tableConfig.columnTooltip != ''" function-tool-tip data-placement="top" data-toggle="tooltip" data-original-title="{{tableConfig.columnTooltip}}"
                          translate="{{tableConfig.hName}}"></span>
                    <span ng-if="tableConfig.columnTooltip == ''" translate="{{tableConfig.hName}}"></span>
                    <span ng-if="($root.$tableProvider.orderByColumn == tableConfig.dName) && (tableConfig.dName != '')"><i class="fa fa-arrow-circle-down" ng-show="!$root.$tableProvider.reverse"></i>
                        <i class="fa fa-arrow-circle-up" ng-show="$root.$tableProvider.reverse"></i>
                    </span>
src/main/webapp/custom_components/js-table/table.provider.js
@@ -23,6 +23,7 @@
                                colSpan : 0,    //  colspan 을 지원한다.
                                subHead : false,    //  만약 rowspan, colspan 을 사용하게 되면 true 로 셋팅.
                                columnHint : "",    //  컬럼 정보를 추출하기 위한 힌트 정보를 준다 - tableColumnGenerator 의 사용자 정의 필드 부분에서 사용
                                columnTooltip : "", // hover 시 툴팁 보여주기
                                setHName : function (hName) {
                                    this.hName = hName;
                                    return this;
@@ -82,6 +83,10 @@
                                setColumnHint : function (dColumnHint) {
                                    this.columnHint = dColumnHint;
                                    return this;
                                },
                                setColumnTooltip : function (hTooltip) {
                                    this.columnTooltip = hTooltip;
                                    return this;
                                }
                            };
src/main/webapp/custom_components/js-tree/tree.provider.js
@@ -16,6 +16,7 @@
                                dDateFormat : "",   //  날짜 형식
                                subHead : false,    //  만약 rowspan, colspan 을 사용하게 되면 true 로 셋팅.
                                columnHint : "",    //  컬럼 정보를 추출하기 위한 힌트 정보를 준다 - tableColumnGenerator 의 사용자 정의 필드 부분에서 사용
                                columnTooltip : "", // hover 시 툴팁 보여주기
                                setHName : function (hName) {
                                    this.hName = hName;
                                    return this;
@@ -47,6 +48,10 @@
                                setColumnHint : function (dColumnHint) {
                                    this.columnHint = dColumnHint;
                                    return this;
                                },
                                setColumnTooltip : function (hTooltip) {
                                    this.columnTooltip = hTooltip;
                                    return this;
                                }
                            };
src/main/webapp/custom_components/widget/member-progress-widget/member-progress-widget.html
@@ -3,7 +3,7 @@
        <div class="os-progress-bar">
            <div class="bar-labels">
                <div class="bar-label-left">
                    <h5 translate="dashboard.progressByMember">멤버별 진행률</h5>
                    <h5 translate="dashboard.progressByMember">부서별 진행률</h5>
                </div>
                <div class="bar-label-right">
                    <form class="form-inline justify-content-sm-end" method="post" action="/widget/downloadExcel" name="memberProgressWidgetForm">
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/app/issue/issueList.controller.js
@@ -411,6 +411,7 @@
                                .setDType("renderer")
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setColumnTooltip("미완료 하위이슈/전체 하위이슈")
                                .setDRenderer("DOWN_ISSUE_COUNT"));
                            break;
                    }
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
@@ -34,7 +34,7 @@
            <div class="form-group">
                <label for="customFieldAddForm2"><span translate="customField.fieldType">필드 유형</span> <code
                        class="highlighter-rouge">*</code></label>
                &nbsp;&nbsp; <label for="customFieldAddForm2"> <input type="checkbox" ng-model="vm.form.requiredData"> 필수 데이터 </label>
                <!--&nbsp;&nbsp; <label for="customFieldAddForm2"> <input type="checkbox" ng-model="vm.form.requiredData"> 필수 데이터 </label>-->
                <select id="customFieldAddForm2" class="form-control" ng-model="vm.form.customFieldType"
                        ng-change="fn.changeFieldType()">
                    <option value="INPUT" translate="common.stringField">문자열 필드</option>
@@ -47,6 +47,12 @@
                    <option value="SITE" translate="common.siteField">홈페이지 주소 필드</option>
                    <option value="TEL" translate="common.telField">전화번호 필드</option>
                </select>
                <div class="mt-3">
                    <span class="custom-detail-label">필수 데이터</span>
                    <label class='switch'><input type='checkbox' ng-model="vm.form.requiredData">
                        <span class='slider round'></span>
                    </label>
                </div>
            </div>
            <div class="form-group" ng-show="vm.form.customFieldType == 'SINGLE_SELECT' || vm.form.customFieldType == 'MULTI_SELECT'">