OWL ITS + 탐지시스템(인터넷 진흥원)
이민희
2022-01-06 6adb341db180240e0af34ace40100912d4ed5257
Merge branch 'master' of http://192.168.0.25:9001/r/owl-kisa

 Conflicts:
 src/main/webapp/scripts/app/issue/issueDetail.controller.js
26개 파일 변경됨
16개 파일 추가됨
1개 파일 삭제됨
2440 ■■■■ 파일 변경됨
pom.xml 33 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/config/SecurityConfiguration.java 2 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/config/SwaggerConfig.java 94 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/constant/MsgConstants.java 61 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/IssueService.java 3 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/IssueServiceImpl.java 36 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/condition/IssueCustomFieldValueCondition.java 23 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/controller/Api/ApiController.java 98 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/controller/ApiController.java 71 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/controller/ApiTokenController.java 21 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/controller/IssueController.java 12 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/form/ApiIssueAddForm.java 53 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/form/ApiIssueModifyForm.java 63 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/form/ApiTokenForm.java 8 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/form/EmailCommonForm.java 55 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/form/IssueApiForm.java 65 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/package-info.java 4 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/resources/log4j2.xml 8 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/resources/mybatis/query-template/issue-template.xml 8 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/assets/styles/main.css 8 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/js-down/down.provider.js 232 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/js-down/downColumnGenerator.directive.js 141 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/js-down/js-down.directive.js 36 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/js-down/js-down.html 47 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/js-rel/js-rel.directive.js 36 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/js-rel/js-rel.html 47 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/js-rel/rel.provider.js 239 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/js-rel/relColumnGenerator.directive.js 142 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/custom_components/js-table/tableColumnGenerator.directive.js 234 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/i18n/ko/global.json 13 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/issue/issue.js 4 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/issue/issueCommonSendMail.controller.js 111 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/issue/issueDetail.controller.js 202 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/issue/issueList.controller.js 5 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/issue/issueSendMailPartners.controller.js 21 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/project/projectModify.controller.js 2 ●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/components/auth/auth.interceptor.js 10 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/components/issue/issue.service.js 6 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/config.js 11 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/main.js 26 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/issue/issueCommonSendMail.html 69 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/issue/issueDetail.html 11 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/issue/issueSendMailPartners.html 69 ●●●●● 패치 | 보기 | raw | blame | 히스토리
pom.xml
@@ -48,7 +48,7 @@
        <!-- Logging -->
        <slf4j.version>1.7.28</slf4j.version>
        <log4j.version>2.12.1</log4j.version>
        <log4j.version>2.17.1</log4j.version>
        <!-- Spring Session Redis -->
        <spring.session.version>2.1.8.RELEASE</spring.session.version>
@@ -67,6 +67,9 @@
        <!-- Elastic Search -->
        <elastic.search.version>7.3.0</elastic.search.version>
        <!-- jsoup -->
        <jsoup.version>1.8.3</jsoup.version>
        <!-- Util -->
        <jackson.version>2.9.9</jackson.version>
@@ -151,6 +154,11 @@
            <version>${java.mail.version}</version>
        </dependency>
        <dependency>
            <groupId>javax.interceptor</groupId>
            <artifactId>javax.interceptor-api</artifactId>
            <version>1.2</version>
        </dependency>
        <!--    Excel import    -->
        <dependency>
            <groupId>org.apache.poi</groupId>
@@ -255,6 +263,14 @@
            <artifactId>commons-validator</artifactId>
            <version>${common.validator.version}</version>
        </dependency>
        <!-- jsoup -->
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>${jsoup.version}</version>
        </dependency>
        <!-- spring -->
        <dependency>
@@ -505,6 +521,17 @@
            <version>0.9.1</version>
        </dependency>
        <!-- Swagger(api 문서화) -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
    </dependencies>
    <repositories>
@@ -545,10 +572,6 @@
            <url>https://maven.java.net/content/repositories/public/</url>
        </repository>
        <repository>
            <id>JBoss repository</id>
            <url>http://repository.jboss.org/nexus/content/groups/public/</url>
        </repository>
    </repositories>
    <build>
src/main/java/kr/wisestone/owl/config/SecurityConfiguration.java
@@ -145,7 +145,9 @@
                .antMatchers("/language/change").permitAll()
                .antMatchers("/security/*").permitAll()
                .antMatchers("/api/issue").permitAll()
                .antMatchers("/api/issue/*").permitAll()
                .antMatchers("/**/*").authenticated();
//        http.addFilter(new CustomAuthenticationFilter());
//        http.addFilterBefore(new CustomAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
src/main/java/kr/wisestone/owl/config/SwaggerConfig.java
New file
@@ -0,0 +1,94 @@
package kr.wisestone.owl.config;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
 * API 문서 제작 패키지 Swagger 설정 클래스
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    private static final String API_NAME = "OWL API";
    private static final String API_VERSION = "1.0.0";
    private static final String API_DESCRIPTION = "OWL API 명세서";
    /**
     * API 설정
     * @return docket class
     */
    @Bean
    public Docket api() {
//        Parameter parameterBuilder = new ParameterBuilder()
//                .name(HttpHeaders.AUTHORIZATION)
//                .description("Access Tocken")
//                .modelRef(new ModelRef("string"))
//                .parameterType("header")
//                .required(false)
//                .build();
        List<Parameter> globalParameters = new ArrayList<>();
//        globalParameters.add(parameterBuilder);
        return new Docket(DocumentationType.SWAGGER_2)
                .globalOperationParameters(globalParameters)
                .consumes(getConsumeContentTypes())
                .produces(getProduceContentTypes())
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("kr.wisestone.owl.web.controller.Api"))
                .paths(PathSelectors.any())
                .build();
    }
    /**
     * API 문서 타이틀/버전/설명 설정
     * @return ApiInfo 클래스
     */
    public ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title(API_NAME)
                .version(API_VERSION)
                .description(API_DESCRIPTION)
                .build();
    }
    /**
     * API 문서 테스트 Request Content-Type 설정 추가
     * @return consumes content-type 목록
     */
    private Set<String> getConsumeContentTypes() {
        Set<String> consumes = new HashSet<>();
        consumes.add("application/json;charset=UTF-8");
        consumes.add("application/x-www-form-urlencoded");
        return consumes;
    }
    /**
     * API 문서 테스트 Response content-type 설정 추가
     * @return produce content-type 목록
     */
    private Set<String> getProduceContentTypes() {
        Set<String> produces = new HashSet<>();
        produces.add("application/json;charset=UTF-8");
        return produces;
    }
}
src/main/java/kr/wisestone/owl/constant/MsgConstants.java
@@ -1,6 +1,7 @@
package kr.wisestone.owl.constant;
/**
 * exception 용 메세지 클래스
 * Created by jeong on 2017-08-02.
 */
public class MsgConstants {
@@ -242,16 +243,52 @@
    public static final String EMAIL_TEMPLATE_NOT_EXIST = "EMAIL_TEMPLATE_NOT_EXIST"; //  이메일 템플릿을 찾을수 없습니다.
    public static final String API_PARAMETER_ISSUE_TYPE_ERROR = "API_PARAMETER_ISSUE_TYPE_ERROR";     // api 파라미터 오류(이슈타입)
    public static final String API_PARAMETER_PROJECT_ERROR = "API_PARAMETER_PROJECT_ERROR";     // api 파라미터 오류(프로젝트)
    public static final String API_PARAMETER_ERROR = "API_PARAMETER_ERROR";     // api 파라미터 오류
    public static final String API_USER_ERROR = "API_USER_ERROR";     // api 사용자 오류
    public static final String API_OVERLAP_ERROR = "API_OVERLAP_ERROR";     // API 중복된 상위 이슈가 여러개일 경우
    public static final String API_OVERLAP_SETTING_NOT_EXIST = "API_OVERLAP_SETTING_NOT_EXIST";     // API 중복된 설정이 안되어 있을 경우
    public static final String API_ISSUE_NOT_EXIST = "API_ISSUE_NOT_EXIST";     // 수정할 이슈를 찾을수 없습니다.
    public static final String API_COMPLETE_ISSUE_STATUS_NOT_EXIST = "API_COMPLETE_ISSUE_STATUS_NOT_EXIST";     // 자동 종료 처리할 상태가 설정되지 않았습니다.
    public static final String API_ISSUE_STATUS_NOT_EXIST = "API_ISSUE_STATUS_NOT_EXIST";     // 이슈 상태를 찾을수 없습니다
    public static final String API_ISSUE_STATUS_IS_NULL = "API_ISSUE_STATUS_IS_NULL";     // 이슈 상태 값이 없습니다.
    public static final String API_CUSTOM_FIELD_NOT_EXIST = "API_CUSTOM_FIELD_NOT_EXIST";     // 사용자 정의 필드를 존재하지 않습니다.
    public static final String API_ISSUE_STATUS_NOT_IN_WORKFLOW = "API_ISSUE_STATUS_NOT_IN_WORKFLOW";     // 이슈 상태가 워크플로우에 포함되어 있지 않습니다
    /**
     * api 파라미터 오류(이슈타입)
     */
    public static final String API_PARAMETER_ISSUE_TYPE_ERROR = "API_PARAMETER_ISSUE_TYPE_ERROR";
    /**
     * api 파라미터 오류(프로젝트)
     */
    public static final String API_PARAMETER_PROJECT_ERROR = "API_PARAMETER_PROJECT_ERROR";
    /**
     * api 파라미터 오류
     */
    public static final String API_PARAMETER_ERROR = "API_PARAMETER_ERROR";
    /**
     * api 사용자 오류
     */
    public static final String API_USER_ERROR = "API_USER_ERROR";
    /**
     * API 중복된 상위 이슈가 여러개일 경우
     */
    public static final String API_OVERLAP_ERROR = "API_OVERLAP_ERROR";
    /**
     * API 중복된 설정이 안되어 있을 경우
     */
    public static final String API_OVERLAP_SETTING_NOT_EXIST = "API_OVERLAP_SETTING_NOT_EXIST";
    /**
     * 수정할 이슈를 찾을수 없습니다.
     */
    public static final String API_ISSUE_NOT_EXIST = "API_ISSUE_NOT_EXIST";
    /**
     * 자동 종료 처리할 상태가 설정되지 않았습니다.
     */
    public static final String API_COMPLETE_ISSUE_STATUS_NOT_EXIST = "API_COMPLETE_ISSUE_STATUS_NOT_EXIST";
    /**
     * 이슈 상태를 찾을수 없습니다
     */
    public static final String API_ISSUE_STATUS_NOT_EXIST = "API_ISSUE_STATUS_NOT_EXIST";
    /**
     * 이슈 상태 값이 없습니다.
     */
    public static final String API_ISSUE_STATUS_IS_NULL = "API_ISSUE_STATUS_IS_NULL";
    /**
     * 사용자 정의 필드를 존재하지 않습니다.
     */
    public static final String API_CUSTOM_FIELD_NOT_EXIST = "API_CUSTOM_FIELD_NOT_EXIST";
    /**
     * 이슈 상태가 워크플로우에 포함되어 있지 않습니다
     */
    public static final String API_ISSUE_STATUS_NOT_IN_WORKFLOW = "API_ISSUE_STATUS_NOT_IN_WORKFLOW";
}
src/main/java/kr/wisestone/owl/service/IssueService.java
@@ -9,6 +9,7 @@
import kr.wisestone.owl.web.condition.DepartmentCondition;
import kr.wisestone.owl.web.condition.IssueCondition;
import kr.wisestone.owl.web.condition.ProjectCondition;
import kr.wisestone.owl.web.form.EmailCommonForm;
import kr.wisestone.owl.web.form.EmailTemplateForm;
import kr.wisestone.owl.web.form.IssueApiForm;
import kr.wisestone.owl.web.form.IssueForm;
@@ -112,4 +113,6 @@
    void setCountDownIssues(List<IssueVo> issueVos);
    void makeIssueMapToIssue(Issue issue, Map<String, Object> issueMap);
    void sendCommonEmail(EmailCommonForm make);
}
src/main/java/kr/wisestone/owl/service/impl/IssueServiceImpl.java
@@ -289,7 +289,7 @@
                    issueApiForm.addUseIssueCustomFieldId(customFieldApiOverlap.getCustomField().getId());
                }
                // 중복된 이슈검색
                // 종료상태가 아닌 중복된 상위 이슈검색
                List<Issue> issues = this.findIssue(issueApiForm, customFieldApiOverlaps, user.getId());
                int size = issues.size();
                if (size > 0) {
@@ -446,6 +446,7 @@
            issueCustomFieldValueCondition.setUseValue(concatUseValue);
            issueCustomFieldValueCondition.setUseValues(userValues);
            issueCustomFieldValueCondition.setIssueTypeId(issueApiform.getIssueTypeId());
            issueCustomFieldValueCondition.setIssueStatusType("CLOSE");
            List<Map<String, Object>> results = this.issueMapper.findByCustomFieldValue(issueCustomFieldValueCondition);
            if (results != null && results.size() > 0) {
                for (Map<String, Object> result : results) {
@@ -944,7 +945,7 @@
//        if (!this.userWorkspaceService.checkWorkspaceManager(user)
//                && !MngPermission.checkMngPermission(userLevel.getPermission(), MngPermission.USER_PERMISSION_MNG_ISSUE)) { //최고관리자 & 프로젝트,이슈 관리자 일 경우 모든 이슈 보기
//            this.SetMyDepartmentId(issueCondition);
            //this.SetAllDepartmentId(issueCondition);
        //this.SetAllDepartmentId(issueCondition);
//        } /*else{
//            results = this.issueMapper.findByDepartment(issueCondition);
//            totalCount = this.issueMapper.countByDepartment(issueCondition);
@@ -3593,6 +3594,37 @@
        this.issueHistoryService.addIssueHistory(issue, IssueHistoryType.SEND, sb.toString());
    }
    @Override
    public void sendCommonEmail(EmailCommonForm emailCommonForm) {
        if (emailCommonForm.getSendEmails().size() < 1) {
            throw new OwlRuntimeException(
                    this.messageAccessor.getMessage(MsgConstants.ISSUE_NOT_SEND_USER));
        } else if (emailCommonForm.getIssueId() == null) {
            throw new OwlRuntimeException(
                    this.messageAccessor.getMessage(MsgConstants.ISSUE_NOT_EXIST));
        }
        Issue issue = this.getIssue(emailCommonForm.getIssueId());
        //  발신자 표시
        User user = this.webAppUtil.getLoginUserObject();
        UserVo toUser = this.webAppUtil.getLoginUser();
        //  사용자 시스템 기능 사용 정보 수집
        log.info(ElasticSearchUtil.makeUserActiveHistoryMessage(this.webAppUtil.getLoginUser(), ElasticSearchConstants.ISSUE_ANOTHER_USER_SEND_EMAIL));
        StringBuilder sb = new StringBuilder();
        Locale locale = CommonUtil.getUserLanguage(user.getLanguage());
        String[] sendMails = ConvertUtil.ToArray(emailCommonForm.getSendEmails());
        for(int i=0; i < sendMails.length; i++) {
            sendMails[i] = CommonUtil.decryptAES128(sendMails[i]);
        }
        this.systemEmailService.sendEmail(emailCommonForm.getTitle(), emailCommonForm.getDescription(), sendMails, null);
        this.issueHistoryService.detectSendIssueMail(IssueHistoryType.SEND, emailCommonForm.getSendEmails(), sb);
        this.issueHistoryService.addIssueHistory(issue, IssueHistoryType.SEND, sb.toString());
    }
    //  예약 발생 이슈를 실행한다
    @Override
    @Transactional
src/main/java/kr/wisestone/owl/web/condition/IssueCustomFieldValueCondition.java
@@ -10,6 +10,7 @@
import java.util.Map;
/**
 * 이슈 사용자 정의 필드 값 검색 조건 클래스
 * Created by wisestone on 2018-06-07.
 */
public class IssueCustomFieldValueCondition {
@@ -17,9 +18,19 @@
    private Long workspaceId;
    private Long customFieldId;
    private String customFieldType;
    private List<String> useValues = Lists.newArrayList();  //  단일, 다중 일때 검색 값
    private String useValue;    //  텍스트 필드일 때 검색 값
    /**
     * 사용자 정의 필드 사용 값
     */
    private List<String> useValues = Lists.newArrayList();
    /**
     * 텍스트 필드일 때 검색 값
     */
    private String useValue;
    private boolean useParentIssueId = true;
    /**
     * 이슈 상태 유형(READY / OPEN / CLOSE)
     */
    private String issueStatusType;
    public IssueCustomFieldValueCondition(){}
@@ -62,6 +73,14 @@
        return condition;
    }
    public String getIssueStatusType() {
        return issueStatusType;
    }
    public void setIssueStatusType(String issueStatusType) {
        this.issueStatusType = issueStatusType;
    }
    public Long getIssueTypeId() {
        return issueTypeId;
    }
src/main/java/kr/wisestone/owl/web/controller/Api/ApiController.java
New file
@@ -0,0 +1,98 @@
package kr.wisestone.owl.web.controller.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import kr.wisestone.owl.constant.MsgConstants;
import kr.wisestone.owl.domain.Issue;
import kr.wisestone.owl.exception.OwlRuntimeException;
import kr.wisestone.owl.service.IssueService;
import kr.wisestone.owl.util.ConvertUtil;
import kr.wisestone.owl.web.controller.BaseController;
import kr.wisestone.owl.web.form.ApiIssueAddForm;
import kr.wisestone.owl.web.form.ApiIssueModifyForm;
import kr.wisestone.owl.web.form.IssueApiForm;
import org.json.simple.parser.ParseException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * OWL-API 컨트롤러
 */
@Controller
@RequestMapping("/api")
public class ApiController extends BaseController {
    @Autowired
    private IssueService issueService;
    /**
     * 이슈 추가
     * @param apiIssueAddForm 입력 폼 데이터
     * @param files 파일
     * @return JSON
     * @throws OwlRuntimeException 파라미터 오류시 발생
     * @throws CloneNotSupportedException 이슈 복사 시에 발생
     */
    @PostMapping(value = "/issue")
    @ApiOperation(value = "이슈 추가", notes = "새로운 이슈를 추가한다.")
    @ApiImplicitParam(name = "files", required = false, dataType = "file")
    public
    @ResponseBody
    Map<String, Object> addIssue(ApiIssueAddForm apiIssueAddForm, @RequestParam("files") List<MultipartFile> files) throws OwlRuntimeException, CloneNotSupportedException, ParseException {
        Map<String, Object> resJsonData = new HashMap<>();
        IssueApiForm issueApiForm = ConvertUtil.copyProperties(apiIssueAddForm, IssueApiForm.class);
//        String str = request.getParameter(Constants.REQ_KEY_CONTENT);
        issueApiForm.setMultipartFiles(files);
        issueApiForm.parseCustomFields(apiIssueAddForm.getCustomFields());
        issueApiForm.setApiType(IssueApiForm.ApiType.add);
        // 사용자 정의 필드가 없을 경우 검색을 할 수 없기 때문에 예외처리
        if (issueApiForm.getCustomFieldValues() == null || issueApiForm.getCustomFieldValues().size() == 0) {
            throw new OwlRuntimeException(this.messageAccessor.getMessage(MsgConstants.API_CUSTOM_FIELD_NOT_EXIST));
        }
        List<Issue> issues = this.issueService.addApiIssue(issueApiForm);
        //  버전 생성
        for (Issue issue : issues) {
            this.issueService.addIssueVersion(issue.getId(), issue.getRegisterId());
        }
        return this.setSuccessMessage(resJsonData);
    }
    /**
     * 이슈 상태 수정
     * @param apiIssueModifyForm 수정 폼 데이터
     * @return JSON
     * @throws OwlRuntimeException 파라미터 오류시 발생
     * @throws CloneNotSupportedException 이슈 복사 시에 발생
     */
    @PostMapping(value = "/issue/1")
    @ApiOperation(value = "이슈 상태 수정", notes = "사용자 정의 필드가 동일한 기존 이슈를 변경한다.")
    public
    @ResponseBody
    Map<String, Object> modifyIssue(ApiIssueModifyForm apiIssueModifyForm) throws OwlRuntimeException, CloneNotSupportedException, ParseException {
        Map<String, Object> resJsonData = new HashMap<>();
        IssueApiForm issueApiForm = ConvertUtil.copyProperties(apiIssueModifyForm, IssueApiForm.class);
        issueApiForm.parseCustomFields(apiIssueModifyForm.getCustomFields());
        issueApiForm.setApiType(IssueApiForm.ApiType.add);
        // 사용자 정의 필드가 없을 경우 검색을 할 수 없기 때문에 예외처리
        if (issueApiForm.getCustomFieldValues() == null || issueApiForm.getCustomFieldValues().size() == 0) {
            throw new OwlRuntimeException(this.messageAccessor.getMessage(MsgConstants.API_CUSTOM_FIELD_NOT_EXIST));
        }
        this.issueService.modifyIssue(issueApiForm, new ArrayList<>());
        return this.setSuccessMessage(resJsonData);
    }
}
src/main/java/kr/wisestone/owl/web/controller/ApiController.java
File was deleted
src/main/java/kr/wisestone/owl/web/controller/ApiTokenController.java
@@ -6,6 +6,7 @@
import kr.wisestone.owl.util.ConvertUtil;
import kr.wisestone.owl.vo.ApiTokenVo;
import kr.wisestone.owl.web.condition.ApiTokenCondition;
import kr.wisestone.owl.web.controller.BaseController;
import kr.wisestone.owl.web.form.ApiTokenForm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
@@ -20,13 +21,20 @@
import java.util.List;
import java.util.Map;
/**
 * API 토큰 컨트롤러 클래스
 */
@Controller
public class ApiTokenController extends BaseController {
    @Autowired
    private ApiTokenService apiTokenService;
    //  토큰 생성
    /**
     * 토큰 생성
     * @param params 토큰 생성에 필요한 파라미터
     * @return 생성 결과 Map<String, Object>
     */
    @RequestMapping(value = "/apiToken/add", produces = MediaType.APPLICATION_JSON_VALUE)
    public
    @ResponseBody
@@ -41,6 +49,12 @@
        return this.setSuccessMessage(resJsonData);
    }
    /**
     * 토큰 조회
     * @param params 토큰 조회에 필요한 파라미터
     * @return 조회 결과, Map<String, ApiTokenVo>
     */
    //  토큰 조회
    @RequestMapping(value = "/apiToken/find", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
    public
@@ -55,6 +69,11 @@
        return this.setSuccessMessage(resJsonData);
    }
    /**
     * 토큰 삭제
     * @param params 토큰 삭제에 필요한 파라미터
     * @return 삭제 결과, Map<String, Object>
     */
    //  토큰 삭제
    @RequestMapping(value = "/apiToken/remove", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
    public
src/main/java/kr/wisestone/owl/web/controller/IssueController.java
@@ -7,6 +7,7 @@
import kr.wisestone.owl.web.condition.ApiMonitorCondition;
import kr.wisestone.owl.web.condition.DepartmentCondition;
import kr.wisestone.owl.web.condition.IssueCondition;
import kr.wisestone.owl.web.form.EmailCommonForm;
import kr.wisestone.owl.web.form.EmailTemplateForm;
import kr.wisestone.owl.web.form.IssueForm;
import org.slf4j.Logger;
@@ -248,6 +249,17 @@
    }
    // 일반 메일 발송 (사용자 직접 작성)
    @RequestMapping(value = "/issue/sendCommonEmail", produces = MediaType.APPLICATION_JSON_VALUE)
    public
    @ResponseBody
    Map<String, Object> sendCommonEmail(@RequestBody Map<String, Map<String, Object>> params) {
        Map<String, Object> resJsonData = new HashMap<>();
        this.issueService.sendCommonEmail(EmailCommonForm.make(params.get(Constants.REQ_KEY_CONTENT)));
        return this.setSuccessMessage(resJsonData);
    }
    //  api 기록 조회
    @RequestMapping(value = "/api/findHistory", produces = MediaType.APPLICATION_JSON_VALUE)
    public
src/main/java/kr/wisestone/owl/web/form/ApiIssueAddForm.java
New file
@@ -0,0 +1,53 @@
package kr.wisestone.owl.web.form;
import io.swagger.annotations.ApiParam;
/**
 * API 이슈 추가용 Form class
 */
public class ApiIssueAddForm {
    @ApiParam(value = "사용자 토큰", required = true)
    private String token;
    @ApiParam(value = "이슈 제목")
    private String title;
    @ApiParam(value = "이슈 타입 ID", required = true)
    private Long issueTypeId;
    @ApiParam(value = "사용자 정의 필드", required = true)
    private String customFields;
    public ApiIssueAddForm() {
    }
    public String getCustomFields() {
        return customFields;
    }
    public void setCustomFields(String customFields) {
        this.customFields = customFields;
    }
    public String getToken() {
        return token;
    }
    public void setToken(String token) {
        this.token = token;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public Long getIssueTypeId() {
        return issueTypeId;
    }
    public void setIssueTypeId(Long issueTypeId) {
        this.issueTypeId = issueTypeId;
    }
}
src/main/java/kr/wisestone/owl/web/form/ApiIssueModifyForm.java
New file
@@ -0,0 +1,63 @@
package kr.wisestone.owl.web.form;
import io.swagger.annotations.ApiParam;
/**
 * API 이슈 상태 수정용 Form class
 */
public class ApiIssueModifyForm {
    @ApiParam(value = "사용자 토큰", required = true)
    private String token;
    @ApiParam(value = "이슈 타입 ID", required = true)
    private Long issueTypeId;
    @ApiParam(value = "이슈 상태 ID", required = true)
    private Long issueStatusId;
    @ApiParam(value = "댓글", required = true)
    private String comment;
    @ApiParam(value = "사용자 정의 필드", required = true)
    private String customFields;
    public ApiIssueModifyForm() {
    }
    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;
    }
    public String getToken() {
        return token;
    }
    public void setToken(String token) {
        this.token = token;
    }
    public String getComment() {
        return comment;
    }
    public void setComment(String comment) {
        this.comment = comment;
    }
    public String getCustomFields() {
        return customFields;
    }
    public void setCustomFields(String customFields) {
        this.customFields = customFields;
    }
}
src/main/java/kr/wisestone/owl/web/form/ApiTokenForm.java
@@ -1,13 +1,11 @@
package kr.wisestone.owl.web.form;
import com.google.common.collect.Lists;
import kr.wisestone.owl.domain.User;
import kr.wisestone.owl.util.ConvertUtil;
import kr.wisestone.owl.util.MapUtil;
import java.util.List;
import java.util.Map;
/**
 * API 토큰 Form class
 */
public class ApiTokenForm {
    private Long id;
    private User user;
src/main/java/kr/wisestone/owl/web/form/EmailCommonForm.java
New file
@@ -0,0 +1,55 @@
package kr.wisestone.owl.web.form;
import com.google.common.collect.Lists;
import kr.wisestone.owl.util.ConvertUtil;
import kr.wisestone.owl.util.MapUtil;
import java.util.List;
import java.util.Map;
public class EmailCommonForm {
    private List<String> sendEmails = Lists.newArrayList();
    private String title;
    private String description;
    private Long issueId;
    public static EmailCommonForm make(Map<String, Object> params) {
        EmailCommonForm emailCommonForm = ConvertUtil.convertMapToClass(params, EmailCommonForm.class);
        if (MapUtil.getStrings(params, "sendEmails") != null) {
            emailCommonForm.setSendEmails(MapUtil.getStrings(params, "sendEmails"));
        }
        return emailCommonForm;
    }
    public List<String> getSendEmails() {
        return sendEmails;
    }
    public void setSendEmails(List<String> sendEmails) {
        this.sendEmails = sendEmails;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public Long getIssueId() {
        return issueId;
    }
    public void setIssueId(Long issueId) {
        this.issueId = issueId;
    }
}
src/main/java/kr/wisestone/owl/web/form/IssueApiForm.java
@@ -4,10 +4,13 @@
import kr.wisestone.owl.util.ConvertUtil;
import kr.wisestone.owl.util.MapUtil;
import org.springframework.web.multipart.MultipartFile;
import java.io.Serializable;
import java.util.*;
public class IssueApiForm {
/**
 * API 이슈 추가 / 수정용 form class
 */
public class IssueApiForm implements Serializable {
    public enum ApiType {
        add,
@@ -37,44 +40,30 @@
    public IssueApiForm() {
    }
    public static IssueApiForm make(Map<String, Object> content, List<MultipartFile> files) {
        IssueApiForm form = ConvertUtil.convertMapToClass(content, IssueApiForm.class);
        form.setMultipartFiles(files);
        // api 타입
        if (MapUtil.getString(content, "apiType") != null) {
            try {
                form.setApiType(ApiType.valueOf(MapUtil.getString(content, "apiType")));
            } catch (Exception ex) {
                return null;
            }
    /**
     * 사용자 정의 필드 변환
     * @param customFieldJson 사용자 정의 필드 json
     */
    public void parseCustomFields(String customFieldJson) {
        Map<String, Object> json = ConvertUtil.convertJsonToMap(customFieldJson);
        List<Map<String, Object>> customFields = (List) MapUtil.getObject(json, "customFields");
        for (Map<String, Object> customField : customFields) {
            IssueCustomFieldValueForm issueCustomFieldValueForm = ConvertUtil.convertMapToClass(customField, IssueCustomFieldValueForm.class);
            this.addIssueCustomFieldValue(issueCustomFieldValueForm);
            Map<String, Object> customFieldVo = new HashMap<>();
            customFieldVo.put("id", issueCustomFieldValueForm.getCustomFieldId());
            customField.put("customFieldVo", customFieldVo);
            List<String> useValues = Lists.newArrayList();
            useValues.add(issueCustomFieldValueForm.getUseValue());
            customField.put("useValues", useValues);
            this.addCustomFieldValue(customField);
        }
        //  사용자 필드 정보
        if (MapUtil.getObject(content, "customFields") != null){
            List<Map<String, Object>> customFields = (List)MapUtil.getObject(content, "customFields");
            for (Map<String, Object> customField : customFields) {
                IssueCustomFieldValueForm issueCustomFieldValueForm = ConvertUtil.convertMapToClass(customField, IssueCustomFieldValueForm.class);
                form.addIssueCustomFieldValue(issueCustomFieldValueForm);
                Map<String, Object> customFieldVo = new HashMap<>();
                customFieldVo.put("id", issueCustomFieldValueForm.getCustomFieldId());
                customField.put("customFieldVo", customFieldVo);
                List<String> useValues = Lists.newArrayList();
                useValues.add(issueCustomFieldValueForm.getUseValue());
                customField.put("useValues", useValues);
                form.addCustomFieldValue(customField);
            }
        }
        // 첨부 파일
//        if (MapUtil.getObject(content, "files") != null){
//            form.setFiles((List)MapUtil.getObject(content, "files"));
//        }
        return form;
    }
    public String getToken() {
src/main/java/package-info.java
New file
@@ -0,0 +1,4 @@
/**
 * OWL-ITS
 */
package kr.wisestone.owl;
src/main/resources/log4j2.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" name="owl-its" monitorInterval="600">
<Configuration status="ERROR" name="owl-its" monitorInterval="600">
    <Properties>
        <Property name="LOG_FORMAT">%d{yyyy-MM-dd HH:mm:ss} [%level] - %msg%n</Property>
        <Property name="BASE_DIR">/owl-enterprise-logs</Property>
@@ -50,6 +50,12 @@
            <AppenderRef ref="File"/>
        </Logger>
        <!-- Crawling Logger -->
        <Logger name="kr.wisestone.owl.crawling" level="debug" additivity="false">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="File"/>
        </Logger>
        <!-- Root Loggers -->
        <Root level="warn">
            <AppenderRef ref="Console"/>
src/main/resources/mybatis/query-template/issue-template.xml
@@ -1115,8 +1115,12 @@
                </when>
            </choose>
            ) customFieldValue ON customFieldValue.issueId = issue.id
        WHERE issStatus.issue_status_type != 'CLOSE'
          AND issue.issue_type_id = #{issueTypeId}
        WHERE issue.issue_type_id = #{issueTypeId}
        <choose>
            <when test="issueStatusType != null">
                AND issStatus.issue_status_type != #{issueStatusType}
            </when>
        </choose>
        GROUP BY issue.id
        HAVING concatUseValue LIKE CONCAT('%', #{useValue}, '%')
    </select>
src/main/webapp/assets/styles/main.css
@@ -11310,6 +11310,14 @@
    font-size: 0.79rem;
}
.select3-selection__email__remove {
    color: #0066ff;
    margin-left: 8px;
    margin-top: 7px;
    cursor: pointer;
    font-size: 0.79rem;
}
.select4-selection__choice {
    font-size: 0.66rem;
    letter-spacing: -0.01em;
src/main/webapp/custom_components/js-down/down.provider.js
New file
@@ -0,0 +1,232 @@
'use strict';
define(['app', 'angular'],
    function (app, angular) {
        app.provider("$downProvider", function () {
            return {
                $get : function ($log) {
                    return {
                        config : function () {
                            var tableConfig = {
                                hName : "",    //    헤더 이름
                                hWidth : "",    //    칼럼 길이
                                hChecked : false,    //    체크 박스 선택 여부
                                hAlign : "text-center",    //    헤더 정렬 기준
                                hSort : true,    //    정렬 가능 여부
                                dName : "",    //    데이터 이름
                                dAlign : "text-left",    //    데이터 정렬 기준
                                dRenderer : "",    //    렌더러 여부
                                dVisible : "",  //      bootstrap 반응형 컬럼 표시 여부
                                dType : "none",        // 태그 타입
                                dDateFormat : "",   //  날짜 형식
                                rowSpan : 0,    //  rowspan 을 지원한다.
                                colSpan : 0,    //  colspan 을 지원한다.
                                subHead : false,    //  만약 rowspan, colspan 을 사용하게 되면 true 로 셋팅.
                                columnHint : "",    //  컬럼 정보를 추출하기 위한 힌트 정보를 준다 - downColumnGenerator 의 사용자 정의 필드 부분에서 사용
                                columnTooltip : "", // hover 시 툴팁 보여주기
                                setHName : function (hName) {
                                    this.hName = hName;
                                    return this;
                                },
                                setHWidth : function (hWidth) {
                                    this.hWidth = hWidth;
                                    return this;
                                },
                                setHChecked : function (hChecked) {
                                    this.hChecked = hChecked;
                                    return this;
                                },
                                setHAlign : function (hAlign) {
                                    this.hAlign = hAlign;
                                    return this;
                                },
                                setHSort : function (hSort) {
                                    this.hSort = hSort;
                                    return this;
                                },
                                setDName : function (dName) {
                                    this.dName = dName;
                                    return this;
                                },
                                setDAlign : function (dAlign) {
                                    this.dAlign = dAlign;
                                    return this;
                                },
                                setDRenderer : function (dRenderer) {
                                    this.dRenderer = dRenderer;
                                    return this;
                                },
                                setDVisible : function (dVisible) {
                                    this.dVisible = dVisible;
                                    return this;
                                },
                                setDType : function (dType) {
                                    this.dType = dType;
                                    return this;
                                },
                                setDDateFormat : function (dDateFormat) {
                                    this.dDateFormat = dDateFormat;
                                    return this;
                                },
                                setRowSpan : function (dRowSpan) {
                                    this.rowSpan = dRowSpan;
                                    return this;
                                },
                                setColSpan : function (dColSpan) {
                                    this.colSpan = dColSpan;
                                    return this;
                                },
                                setSubHead : function (dSubHead) {
                                    this.subHead = dSubHead;
                                    return this;
                                },
                                setColumnHint : function (dColumnHint) {
                                    this.columnHint = dColumnHint;
                                    return this;
                                },
                                setColumnTooltip : function (hTooltip) {
                                    this.columnTooltip = hTooltip;
                                    return this;
                                }
                            };
                            return tableConfig;
                        },
                        toggleChecked : function (checkStatus, datas) {
                            //  전체 선택 체크 박스를 클릭했을 경우
                            angular.forEach(datas, function (data) {
                                data.checked = checkStatus;
                            });
                        },
                        radioChecked : function (target, datas) {
                            //  해당 row 가 라디오 버튼일 경우
                            angular.forEach(datas, function (data) {
                                if (target.id == data.id) {
                                    data.checked = true;
                                }
                                else {
                                    data.checked = false;
                                }
                            });
                        },
                        rowChecked : function (tableConfig, target, datas) {
                            //  각 row 의 체크박스/라디오 버튼을 클릭했을 경우
                            if (tableConfig[0].dType == "checkbox") {
                                target.checked = !target.checked;
                                for (var data in datas) {
                                    if (!data.checked) {
                                        this.hChecked = false;
                                        break;
                                    }
                                }
                            }
                            else if (tableConfig[0].dType == "radio") {
                                this.radioChecked(target, datas);
                            }
                        },
                        orderByColumn : "",  // table order By column name
                        reverse : true,
                        setOrderByColumn : function (column) {
                            if (column == "") {
                                return;
                            }
                            if (this.orderByColumn == column) {
                                this.reverse = !this.reverse;
                            }
                            else {
                                this.reverse = true;
                            }
                            this.orderByColumn = column;
                            return this;
                        },
                        getDateFormat : function (formatType, date) {
                            if (formatType == "" || formatType == null) {
                                formatType = "01";
                            }
                            Date.prototype.format = function (f) {
                                if (!this.valueOf()) {
                                    return " ";
                                }
                                var weekName = ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"];
                                var d = this;
                                return f.replace(/(yyyy|yy|MM|dd|E|hh|mm|ss|a\/p)/gi, function ($1) {
                                    switch ($1) {
                                        case "yyyy":
                                            return d.getFullYear();
                                        case "yy":
                                            return (d.getFullYear() % 1000).zf(2);
                                        case "MM":
                                            return (d.getMonth() + 1).zf(2);
                                        case "dd":
                                            return d.getDate().zf(2);
                                        case "E":
                                            return weekName[d.getDay()];
                                        case "HH":
                                            return d.getHours().zf(2);
                                        case "hh":
                                            var h = d.getHours();
                                            return ((h = d.getHours() % 12) ? h : 12).zf(2);
                                        case "mm":
                                            return d.getMinutes().zf(2);
                                        case "ss":
                                            return d.getSeconds().zf(2);
                                        case "a/p":
                                            return d.getHours() < 12 ? "오전" : "오후";
                                        default:
                                            return $1;
                                    }
                                });
                            };
                            String.prototype.string = function (len) {
                                var s = '', i = 0;
                                while (i++ < len) {
                                    s += this;
                                }
                                return s;
                            };
                            String.prototype.zf = function (len) {
                                return "0".string(len - this.length) + this;
                            };
                            Number.prototype.zf = function (len) {
                                return this.toString().zf(len);
                            };
                            var dateFormat = "";
                            var dynamicTime = false;
                            var today = new Date().format("yyyy-MM-dd");
                            var compareDate = new Date(date).format("yyyy-MM-dd");
                            if (today == compareDate) {
                                dynamicTime = true;
                            }
                            switch (formatType) {
                                case "01":  //  날짜
                                    dateFormat = "yyyy-MM-dd";
                                    break;
                                case "02":  //  날짜 + 시간
                                    dateFormat = "yyyy-MM-dd HH:mm";
                                    break;
                                case "03":  //  유동적 표시
                                    if (dynamicTime) {
                                        dateFormat = "HH:mm";
                                    }
                                    else {
                                        dateFormat = "yyyy-MM-dd HH:mm";
                                    }
                                    break;
                            }
                            return dateFormat;
                        }
                    }
                }
            }
        });
    });
src/main/webapp/custom_components/js-down/downColumnGenerator.directive.js
New file
@@ -0,0 +1,141 @@
'use strict';
define(['app', 'angular'],
    function (app, angular) {
        app.directive('downColumnGenerator', ['$compile', '$log', '$rootScope', '$downProvider', '$filter',
            function ($compile, $log, $rootScope, $downProvider, $filter) {
                return {
                    restrict : "A",
                    compile : function (tElement, tAttrs) {
                        return function (scope, element, attrs) {
                            scope.data = scope[attrs["downColumnGenerator"]];
                            var myData = scope.data;
                            var makeTag = "";
                            scope.tableConfigs.forEach(function (tableConfig, index) {
                                if (tableConfig.colSpan > 0) {
                                    return;
                                }
                                // 하위 단계 표시 추가
                                var myToken = "";
                                if ( scope.data.depth > 0)  {
                                    for(var i=0; i<scope.data.depth; i++) {
                                        if (i == scope.data.depth - 1) {
                                            myToken += treeStartToken;
                                        } else {
                                            myToken += "&emsp;";
                                        }
                                    }
                                    myToken += "&nbsp;";
                                }
                                makeTag = '<td class="' + tableConfig.dAlign + ' ' + tableConfig.dVisible + '">';
                                if (tableConfig.dType === "checkbox") {
                                    //  체크 박스일때
                                    /*if (scope.data.defaultYn) {
                                        makeTag += '<input type="checkbox" ng-checked="data.checked == true ? true : false" disabled ng-click="$root.$downProvider.rowChecked(tableConfigs, data, fn.getResponseData())">';
                                    }
                                    else {*/
                                    makeTag += '<input type="checkbox" ng-checked="data.checked == true ? true : false" ng-click="$root.$downProvider.rowChecked(tableConfigs, data, fn.getResponseData())">';
                                    //}
                                    tableConfig.hChecked = false;
                                }
                                else if (tableConfig.dType === "radio") {
                                    //  라디오 버튼일때
                                    makeTag += '<input type="radio" ng-checked="data.checked == true ? true : false" ng-click="$root.$downProvider.rowChecked(tableConfigs, data, fn.getResponseData())">';
                                }
                                else if (tableConfig.dType === "renderer") {
                                    //  랜더러 일때
                                    switch (tableConfig.dRenderer) {
                                        // 하위 이슈 이동(제목)
                                        case "ISSUE_DOWN_MOVE" :
                                            makeTag += "<span class=\"titlename cursor\" ng-click=\"event.changeDetailView(data)\">" + scope.data.title + "</span></a>";
                                            break;
                                        // 하위 이슈 타입
                                        case "ISSUE_DOWN_STATUS_TYPE" :
                                            makeTag += "<span class='badge' ng-style='{ \"background-color\" : \"" + scope.data.issueStatusVo.color + "\"," + "\"border-color\"" + " : \"" + scope.data.issueStatusVo.color + "\", \"color\": \"#FFFFFF\" }'>" + scope.data.issueStatusVo.name + "</span>";
                                            break;
                                        // 하위 이슈 삭제
                                        case "ISSUE_DOWN_DELETE":
                                            if (scope.data.modifyPermissionCheck) {
                                                makeTag += '<img class="cursor" src="/assets/images/delete-icon.png" ng-click="event.removeDownIssue(data.id)">';
                                            }
                                            break;
                                        // 하위 이슈 우선 순위
                                        case "DOWN_COMMON_PRIORITY" :
                                            makeTag += "<span class='badge' ng-style='{ \"background-color\" : \"" + scope.data.priorityVo.color + "\"," + "\"border-color\"" + " : \"" + scope.data.priorityVo.color + "\", \"color\": \"#FFFFFF\" }' translate='" + scope.data.priorityVo.name + "'></span>";
                                            break;
                                        // 하위 이슈 중요도
                                        case "DOWN_COMMON_SEVERITY" :
                                            makeTag += "<span class='badge' ng-style='{ \"background-color\" : \"" + scope.data.severityVo.color + "\"," + "\"border-color\"" + " : \"" + scope.data.severityVo.color + "\", \"color\": \"#FFFFFF\" }' translate='" + scope.data.severityVo.name + "'></span>";
                                            break;
                                        // 하위 이슈 담당부서
                                        case "DOWN_ISSUE_DEPARTMENT" :
                                            makeTag += "<ul class='ul-not-comma'>";
                                            makeTag += "<div style='color: #000000'>";
                                            angular.forEach(scope.data.departmentVos, function (departments) {
                                                makeTag += "<li>" + departments.departmentName + "</li>";
                                            });
                                            makeTag += "</div>";
                                            makeTag += "</ul>";
                                            break;
                                        // 하위 이슈 등록자
                                        case "DOWN_REGISTER":
                                            scope.data.registerVos = [scope.data.registerVo];
                                            makeTag += '<div owl-profile-over class="" table-user-image="data" target="registerVos"></div>';
                                            break;
                                        // 하위 이슈 기간
                                        case "DOWN_ISSUE_DUE_DATE" :
                                            if (!$rootScope.isDefined(scope.data.startDate) && !$rootScope.isDefined(scope.data.completeDate)) {
                                                makeTag += "<span translate='common.noDate'>기간 없음</span>";
                                            }
                                            else {
                                                makeTag += "<span>" + scope.data.startDate + " ~ " + scope.data.completeDate + "</span>";
                                            }
                                            break;
                                        // 하위 이슈 사용자 정의 필드
                                        case "DOWN_ISSUE_CUSTOM_FIELD_VALUE_VIEW" :
                                            var values = [];
                                            for (var count in scope.data.issueCustomFieldValueVos) {
                                                var issueCustomFieldValueVo = scope.data.issueCustomFieldValueVos[count];
                                                //  테이블 설정에서 dName 부분에 사용자 정의 필드 id 를 넣고 해당 값을 추출한다.
                                                if (tableConfig.columnHint.id == issueCustomFieldValueVo.customFieldVo.id) {
                                                    values.push(issueCustomFieldValueVo.useValue);
                                                }
                                            }
                                            angular.forEach(values, function (value) {
                                                makeTag += '<span class="table-word-break-all">' + value + '<span><br>';
                                            });
                                            break;
                                    }
                                }
                                makeTag += '</td>';
                                var linkFn = $compile(makeTag);
                                var content = linkFn(scope);
                                element.append(content);
                            });
                        }
                    }
                }
        }]);
    });
src/main/webapp/custom_components/js-down/js-down.directive.js
New file
@@ -0,0 +1,36 @@
'use strict';
define(['app'],
    function (app) {
        app.directive('jsDown', ['$log',
            function ($log) {
                return {
                    restrict : 'E',
                    scope : {
                        event : '=',
                        data : '=',
                        tableConfigs : '=',
                        hideHeader : '=', // 헤더 부분 숨김 여부
                        useSort : '=', // 정령 기능 사용 여부
                        detailView : "="    //  이슈 목록 상세형 변경을 위해 사용. 다른 화면은 사용하지 않음.
                    },
                    replace : true,
                    templateUrl : '/custom_components/js-down/js-down.html',
                    controller : function ($scope, $element, $attrs) {
                        $scope.fn = {
                            getResponseData : getResponseData
                        };
                        //  테이블 정보 가져오기
                        function getResponseData() {
                            return $scope.data;
                        }
                    },
                    link : function (scope, element, attrs) {
                    }
                };
            }])
    });
src/main/webapp/custom_components/js-down/js-down.html
New file
@@ -0,0 +1,47 @@
<table class="table table-striped table-layout-fixed" ng-class="{ 'width768' : !detailView }" bindonce>
    <!-- 테이블 머리 -->
    <thead>
    <tr ng-if="hideHeader != true">
        <th bindonce ng-repeat="tableConfig in tableConfigs"
            bo-class="[tableConfig.hAlign, tableConfig.hWidth, tableConfig.dVisible]"
            ng-click="$root.$downProvider.setOrderByColumn(tableConfig.dName)"
            bo-style="{ 'cursor' : tableConfig.dName != '' ? 'pointer' : '' }"
            rowspan="{{tableConfig.rowSpan}}"
            colspan="{{tableConfig.colSpan}}"
            ng-if="!tableConfig.subHead">
            <!-- 체크 박스일 경우 -->
            <div bo-switch="tableConfig.dType">
                <div bo-switch-when="checkbox">
                    <input type="checkbox" tabindex="-1" ng-model="tableConfig.hChecked" ng-click="$root.$downProvider.toggleChecked(tableConfig.hChecked, fn.getResponseData())">
                </div>
                <div bo-switch-default>
                    <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.$downProvider.orderByColumn == tableConfig.dName) && (tableConfig.dName != '')"><i class="fa fa-arrow-circle-down" ng-show="!$root.$downProvider.reverse"></i>
                        <i class="fa fa-arrow-circle-up" ng-show="$root.$downProvider.reverse"></i>
                    </span>
                </div>
            </div>
        </th>
    </tr>
    </thead>
    <tbody>
    <tr ng-if="useSort != false" ng-repeat="row in fn.getResponseData() | orderBy:$root.$downProvider.orderByColumn:$root.$downProvider.reverse"
        bo-class="[row.checked == true ? 'table-active' : '', tableConfig.hWidth]"
        down-column-generator="row">
    </tr>
    <tr ng-if="useSort == false" ng-repeat="row in fn.getResponseData()"
        bo-class="[row.checked == true ? 'table-active' : '', tableConfig.hWidth]"
        down-column-generator="row">
    </tr>
    <tr ng-if="fn.getResponseData().length == 0">
        <td colspan="{{tableConfigs.length}}">
            <span translate="common.noData">데이터가 없습니다.</span>
        </td>
    </tr>
    </tbody>
</table>
src/main/webapp/custom_components/js-rel/js-rel.directive.js
New file
@@ -0,0 +1,36 @@
'use strict';
define(['app'],
    function (app) {
        app.directive('jsRel', ['$log',
            function ($log) {
                return {
                    restrict : 'E',
                    scope : {
                        event : '=',
                        data : '=',
                        tableConfigs : '=',
                        hideHeader : '=', // 헤더 부분 숨김 여부
                        useSort : '=', // 정령 기능 사용 여부
                        detailView : "="    //  이슈 목록 상세형 변경을 위해 사용. 다른 화면은 사용하지 않음.
                    },
                    replace : true,
                    templateUrl : '/custom_components/js-rel/js-rel.html',
                    controller : function ($scope, $element, $attrs) {
                        $scope.fn = {
                            getResponseData : getResponseData
                        };
                        //  테이블 정보 가져오기
                        function getResponseData() {
                            return $scope.data;
                        }
                    },
                    link : function (scope, element, attrs) {
                    }
                };
            }])
    });
src/main/webapp/custom_components/js-rel/js-rel.html
New file
@@ -0,0 +1,47 @@
<table class="table table-striped table-layout-fixed" ng-class="{ 'width768' : !detailView }" bindonce>
    <!-- 테이블 머리 -->
    <thead>
    <tr ng-if="hideHeader != true">
        <th bindonce ng-repeat="tableConfig in tableConfigs"
            bo-class="[tableConfig.hAlign, tableConfig.hWidth, tableConfig.dVisible]"
            ng-click="$root.$relProvider.setOrderByColumn(tableConfig.dName)"
            bo-style="{ 'cursor' : tableConfig.dName != '' ? 'pointer' : '' }"
            rowspan="{{tableConfig.rowSpan}}"
            colspan="{{tableConfig.colSpan}}"
            ng-if="!tableConfig.subHead">
            <!-- 체크 박스일 경우 -->
            <div bo-switch="tableConfig.dType">
                <div bo-switch-when="checkbox">
                    <input type="checkbox" tabindex="-1" ng-model="tableConfig.hChecked" ng-click="$root.$relProvider.toggleChecked(tableConfig.hChecked, fn.getResponseData())">
                </div>
                <div bo-switch-default>
                    <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.$relProvider.orderByColumn == tableConfig.dName) && (tableConfig.dName != '')"><i class="fa fa-arrow-circle-down" ng-show="!$root.$relProvider.reverse"></i>
                        <i class="fa fa-arrow-circle-up" ng-show="$root.$relProvider.reverse"></i>
                    </span>
                </div>
            </div>
        </th>
    </tr>
    </thead>
    <tbody>
    <tr ng-if="useSort != false" ng-repeat="row in fn.getResponseData() | orderBy:$root.$relProvider.orderByColumn:$root.$relProvider.reverse"
        bo-class="[row.checked == true ? 'table-active' : '', tableConfig.hWidth]"
        rel-column-generator="row">
    </tr>
    <tr ng-if="useSort == false" ng-repeat="row in fn.getResponseData()"
        bo-class="[row.checked == true ? 'table-active' : '', tableConfig.hWidth]"
        rel-column-generator="row">
    </tr>
    <tr ng-if="fn.getResponseData().length == 0">
        <td colspan="{{tableConfigs.length}}">
            <span translate="common.noData">데이터가 없습니다.</span>
        </td>
    </tr>
    </tbody>
</table>
src/main/webapp/custom_components/js-rel/rel.provider.js
New file
@@ -0,0 +1,239 @@
'use strict';
define(['app', 'angular'],
    function (app, angular) {
        app.provider("$relProvider", function () {
            return {
                $get : function ($log) {
                    return {
                        config : function () {
                            var tableConfig = {
                                hName : "",    //    헤더 이름
                                hWidth : "",    //    칼럼 길이
                                hChecked : false,    //    체크 박스 선택 여부
                                hAlign : "text-center",    //    헤더 정렬 기준
                                hSort : true,    //    정렬 가능 여부
                                dName : "",    //    데이터 이름
                                dAlign : "text-left",    //    데이터 정렬 기준
                                dRenderer : "",    //    렌더러 여부
                                dVisible : "",  //      bootstrap 반응형 컬럼 표시 여부
                                dType : "none",        // 태그 타입
                                dDateFormat : "",   //  날짜 형식
                                rowSpan : 0,    //  rowspan 을 지원한다.
                                colSpan : 0,    //  colspan 을 지원한다.
                                subHead : false,    //  만약 rowspan, colspan 을 사용하게 되면 true 로 셋팅.
                                columnHint : "",    //  컬럼 정보를 추출하기 위한 힌트 정보를 준다 - relColumnGenerator 의 사용자 정의 필드 부분에서 사용
                                columnTooltip : "", // hover 시 툴팁 보여주기
                                setHName : function (hName) {
                                    this.hName = hName;
                                    return this;
                                },
                                setHWidth : function (hWidth) {
                                    this.hWidth = hWidth;
                                    return this;
                                },
                                setHChecked : function (hChecked) {
                                    this.hChecked = hChecked;
                                    return this;
                                },
                                setHAlign : function (hAlign) {
                                    this.hAlign = hAlign;
                                    return this;
                                },
                                setHSort : function (hSort) {
                                    this.hSort = hSort;
                                    return this;
                                },
                                setDName : function (dName) {
                                    this.dName = dName;
                                    return this;
                                },
                                setDAlign : function (dAlign) {
                                    this.dAlign = dAlign;
                                    return this;
                                },
                                setDRenderer : function (dRenderer) {
                                    this.dRenderer = dRenderer;
                                    return this;
                                },
                                setDVisible : function (dVisible) {
                                    this.dVisible = dVisible;
                                    return this;
                                },
                                setDType : function (dType) {
                                    this.dType = dType;
                                    return this;
                                },
                                setDDateFormat : function (dDateFormat) {
                                    this.dDateFormat = dDateFormat;
                                    return this;
                                },
                                setRowSpan : function (dRowSpan) {
                                    this.rowSpan = dRowSpan;
                                    return this;
                                },
                                setColSpan : function (dColSpan) {
                                    this.colSpan = dColSpan;
                                    return this;
                                },
                                setSubHead : function (dSubHead) {
                                    this.subHead = dSubHead;
                                    return this;
                                },
                                setColumnHint : function (dColumnHint) {
                                    this.columnHint = dColumnHint;
                                    return this;
                                },
                                setColumnTooltip : function (hTooltip) {
                                    this.columnTooltip = hTooltip;
                                    return this;
                                }
                            };
                            return tableConfig;
                        },
                        toggleChecked : function (checkStatus, datas) {
                            //  전체 선택 체크 박스를 클릭했을 경우
                            angular.forEach(datas, function (data) {
                                /*if (angular.isDefined(data.defaultYn)) {
                                    if (!data.defaultYn) {
                                        data.checked = checkStatus;
                                    }
                                }
                                else {*/
                                    data.checked = checkStatus;
                                //}
                            });
                        },
                        radioChecked : function (target, datas) {
                            //  해당 row 가 라디오 버튼일 경우
                            angular.forEach(datas, function (data) {
                                if (target.id == data.id) {
                                    data.checked = true;
                                }
                                else {
                                    data.checked = false;
                                }
                            });
                        },
                        rowChecked : function (tableConfig, target, datas) {
                            //  각 row 의 체크박스/라디오 버튼을 클릭했을 경우
                            if (tableConfig[0].dType == "checkbox") {
                                target.checked = !target.checked;
                                for (var data in datas) {
                                    if (!data.checked) {
                                        this.hChecked = false;
                                        break;
                                    }
                                }
                            }
                            else if (tableConfig[0].dType == "radio") {
                                this.radioChecked(target, datas);
                            }
                        },
                        orderByColumn : "",  // table order By column name
                        reverse : true,
                        setOrderByColumn : function (column) {
                            if (column == "") {
                                return;
                            }
                            if (this.orderByColumn == column) {
                                this.reverse = !this.reverse;
                            }
                            else {
                                this.reverse = true;
                            }
                            this.orderByColumn = column;
                            return this;
                        },
                        getDateFormat : function (formatType, date) {
                            if (formatType == "" || formatType == null) {
                                formatType = "01";
                            }
                            Date.prototype.format = function (f) {
                                if (!this.valueOf()) {
                                    return " ";
                                }
                                var weekName = ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"];
                                var d = this;
                                return f.replace(/(yyyy|yy|MM|dd|E|hh|mm|ss|a\/p)/gi, function ($1) {
                                    switch ($1) {
                                        case "yyyy":
                                            return d.getFullYear();
                                        case "yy":
                                            return (d.getFullYear() % 1000).zf(2);
                                        case "MM":
                                            return (d.getMonth() + 1).zf(2);
                                        case "dd":
                                            return d.getDate().zf(2);
                                        case "E":
                                            return weekName[d.getDay()];
                                        case "HH":
                                            return d.getHours().zf(2);
                                        case "hh":
                                            var h = d.getHours();
                                            return ((h = d.getHours() % 12) ? h : 12).zf(2);
                                        case "mm":
                                            return d.getMinutes().zf(2);
                                        case "ss":
                                            return d.getSeconds().zf(2);
                                        case "a/p":
                                            return d.getHours() < 12 ? "오전" : "오후";
                                        default:
                                            return $1;
                                    }
                                });
                            };
                            String.prototype.string = function (len) {
                                var s = '', i = 0;
                                while (i++ < len) {
                                    s += this;
                                }
                                return s;
                            };
                            String.prototype.zf = function (len) {
                                return "0".string(len - this.length) + this;
                            };
                            Number.prototype.zf = function (len) {
                                return this.toString().zf(len);
                            };
                            var dateFormat = "";
                            var dynamicTime = false;
                            var today = new Date().format("yyyy-MM-dd");
                            var compareDate = new Date(date).format("yyyy-MM-dd");
                            if (today == compareDate) {
                                dynamicTime = true;
                            }
                            switch (formatType) {
                                case "01":  //  날짜
                                    dateFormat = "yyyy-MM-dd";
                                    break;
                                case "02":  //  날짜 + 시간
                                    dateFormat = "yyyy-MM-dd HH:mm";
                                    break;
                                case "03":  //  유동적 표시
                                    if (dynamicTime) {
                                        dateFormat = "HH:mm";
                                    }
                                    else {
                                        dateFormat = "yyyy-MM-dd HH:mm";
                                    }
                                    break;
                            }
                            return dateFormat;
                        }
                    }
                }
            }
        });
    });
src/main/webapp/custom_components/js-rel/relColumnGenerator.directive.js
New file
@@ -0,0 +1,142 @@
'use strict';
define(['app', 'angular'],
    function (app, angular) {
        app.directive('relColumnGenerator', ['$compile', '$log', '$rootScope', '$relProvider', '$filter',
            function ($compile, $log, $rootScope, $relProvider, $filter) {
                return {
                    restrict : "A",
                    compile : function (tElement, tAttrs) {
                        return function (scope, element, attrs) {
                            scope.data = scope[attrs["relColumnGenerator"]];
                            var myData = scope.data;
                            var makeTag = "";
                            scope.tableConfigs.forEach(function (tableConfig, index) {
                                if (tableConfig.colSpan > 0) {
                                    return;
                                }
                                // 하위 단계 표시 추가
                                var myToken = "";
                                if ( scope.data.depth > 0)  {
                                    for(var i=0; i<scope.data.depth; i++) {
                                        if (i == scope.data.depth - 1) {
                                            myToken += treeStartToken;
                                        } else {
                                            myToken += "&emsp;";
                                        }
                                    }
                                    myToken += "&nbsp;";
                                }
                                makeTag = '<td class="' + tableConfig.dAlign + ' ' + tableConfig.dVisible + '">';
                                if (tableConfig.dType === "checkbox") {
                                    //  체크 박스일때
                                    /*if (scope.data.defaultYn) {
                                        makeTag += '<input type="checkbox" ng-checked="data.checked == true ? true : false" disabled ng-click="$root.$relProvider.rowChecked(tableConfigs, data, fn.getResponseData())">';
                                    }
                                    else {*/
                                    makeTag += '<input type="checkbox" ng-checked="data.checked == true ? true : false" ng-click="$root.$relProvider.rowChecked(tableConfigs, data, fn.getResponseData())">';
                                    //}
                                    tableConfig.hChecked = false;
                                }
                                else if (tableConfig.dType === "radio") {
                                    //  라디오 버튼일때
                                    makeTag += '<input type="radio" ng-checked="data.checked == true ? true : false" ng-click="$root.$relProvider.rowChecked(tableConfigs, data, fn.getResponseData())">';
                                }
                                else if (tableConfig.dType === "renderer") {
                                    //  랜더러 일때
                                    switch (tableConfig.dRenderer) {
                                        // 연관 이슈 이동(제목)
                                        case "ISSUE_RELATION_MOVE" :
                                            makeTag += "<span class=\"titlename cursor text-center\" ng-click=\"event.changeDetailView(data.issueRelation)\">" + scope.data.title + "</span></a>";
                                            break;
                                        // 연관이슈 구분
                                        case "ISSUE_RELATION_TYPE":
                                            makeTag += "<span>" + scope.data.relationIssueTypeName + "</span>";
                                            break;
                                        // 연관이슈 삭제
                                        case "ISSUE_RELATION_DELETE":
                                            if (scope.data.modifyPermissionCheck) {
                                                makeTag += '<img class="cursor" src="/assets/images/delete-icon.png" ng-click="event.removeRelationIssue(data.id)">';
                                            }
                                            break;
                                        // 연관 이슈 우선순위
                                        case "REL_COMMON_PRIORITY" :
                                            makeTag += "<span class='badge' ng-style='{ \"background-color\" : \"" + scope.data.priorityVo.color + "\"," + "\"border-color\"" + " : \"" + scope.data.priorityVo.color + "\", \"color\": \"#FFFFFF\" }' translate='" + scope.data.priorityVo.name + "'></span>";
                                            break;
                                        // 연관 이슈 중요도
                                        case "REL_COMMON_SEVERITY" :
                                            makeTag += "<span class='badge' ng-style='{ \"background-color\" : \"" + scope.data.severityVo.color + "\"," + "\"border-color\"" + " : \"" + scope.data.severityVo.color + "\", \"color\": \"#FFFFFF\" }' translate='" + scope.data.severityVo.name + "'></span>";
                                            break;
                                        // 연관 이슈 등록자
                                        case "REL_REGISTER":
                                            scope.data.registerVos = [scope.data.issueRelation.registerVo];
                                            makeTag += '<div owl-profile-over class="" table-user-image="data" target="registerVos"></div>';
                                            break;
                                            break;
                                        // 연관 이슈 담당부서
                                        case "REL_ISSUE_DEPARTMENT" :
                                            makeTag += "<ul class='ul-not-comma'>";
                                            makeTag += "<div style='color: #000000'>";
                                            angular.forEach(scope.data.issueRelation.departmentVos, function (departments) {
                                                makeTag += "<li>" + departments.departmentName + "</li>";
                                            });
                                            makeTag += "</div>";
                                            makeTag += "</ul>";
                                            break;
                                        // 연관 이슈 목록에서 기간 표시
                                        case "REL_ISSUE_DUE_DATE" :
                                            if (!$rootScope.isDefined(scope.data.issueRelation.startDate) && !$rootScope.isDefined(scope.data.issueRelation.completeDate)) {
                                                makeTag += "<span translate='common.noDate'>기간 없음</span>";
                                            } else {
                                                makeTag += "<span>" + scope.data.issueRelation.startDate + " ~ " + scope.data.issueRelation.completeDate + "</span>";
                                            }
                                            break;
                                        // 연관 이슈 사용자 정의 필드
                                        case "REL_ISSUE_CUSTOM_FIELD_VALUE_VIEW" :
                                            var values = [];
                                            for (var count in scope.data.issueRelation.issueCustomFieldValueVos) {
                                                var issueCustomFieldValueVo = scope.data.issueRelation.issueCustomFieldValueVos[count];
                                                //  테이블 설정에서 dName 부분에 사용자 정의 필드 id 를 넣고 해당 값을 추출한다.
                                                if (tableConfig.columnHint.id == issueCustomFieldValueVo.customFieldVo.id) {
                                                    values.push(issueCustomFieldValueVo.useValue);
                                                }
                                            }
                                            angular.forEach(values, function (value) {
                                                makeTag += '<span class="table-word-break-all cursor">' + value + '<span><br>';
                                            });
                                            break;
                                    }
                                }
                                makeTag += '</td>';
                                var linkFn = $compile(makeTag);
                                var content = linkFn(scope);
                                element.append(content);
                            });
                        }
                    }
                }
        }]);
    });
src/main/webapp/custom_components/js-table/tableColumnGenerator.directive.js
@@ -235,148 +235,148 @@
                                            break;*/
                                        // 연관 이슈 이동(제목)
                                        case "ISSUE_RELATION_MOVE" :
                                            makeTag += "<span class=\"titlename cursor text-center\" ng-click=\"event.changeDetailView(data.issueRelation)\">" + scope.data.title + "</span></a>";
                                            break;
                                        // case "ISSUE_RELATION_MOVE" :
                                        //     makeTag += "<span class=\"titlename cursor text-center\" ng-click=\"event.changeDetailView(data.issueRelation)\">" + scope.data.title + "</span></a>";
                                        //     break;
                                        // 연관이슈 구분
                                        case "ISSUE_RELATION_TYPE":
                                            makeTag += "<span>" + scope.data.relationIssueTypeName + "</span>";
                                            break;
                                        // case "ISSUE_RELATION_TYPE":
                                        //     makeTag += "<span>" + scope.data.relationIssueTypeName + "</span>";
                                        //     break;
                                        // 연관이슈 삭제
                                        case "ISSUE_RELATION_DELETE":
                                            if (scope.data.modifyPermissionCheck) {
                                                makeTag += '<img class="cursor" src="/assets/images/delete-icon.png" ng-click="event.removeRelationIssue(data.id)">';
                                            }
                                            break;
                                        // case "ISSUE_RELATION_DELETE":
                                        //     if (scope.data.modifyPermissionCheck) {
                                        //         makeTag += '<img class="cursor" src="/assets/images/delete-icon.png" ng-click="event.removeRelationIssue(data.id)">';
                                        //     }
                                        //     break;
                                        // 연관 이슈 우선순위
                                        case "REL_COMMON_PRIORITY" :
                                            makeTag += "<span class='badge' ng-style='{ \"background-color\" : \"" + scope.data.priorityVo.color + "\"," + "\"border-color\"" + " : \"" + scope.data.priorityVo.color + "\", \"color\": \"#FFFFFF\" }' translate='" + scope.data.priorityVo.name + "'></span>";
                                            break;
                                        // case "REL_COMMON_PRIORITY" :
                                        //     makeTag += "<span class='badge' ng-style='{ \"background-color\" : \"" + scope.data.priorityVo.color + "\"," + "\"border-color\"" + " : \"" + scope.data.priorityVo.color + "\", \"color\": \"#FFFFFF\" }' translate='" + scope.data.priorityVo.name + "'></span>";
                                        //     break;
                                        // 연관 이슈 중요도
                                        case "REL_COMMON_SEVERITY" :
                                            makeTag += "<span class='badge' ng-style='{ \"background-color\" : \"" + scope.data.severityVo.color + "\"," + "\"border-color\"" + " : \"" + scope.data.severityVo.color + "\", \"color\": \"#FFFFFF\" }' translate='" + scope.data.severityVo.name + "'></span>";
                                            break;
                                        // case "REL_COMMON_SEVERITY" :
                                        //     makeTag += "<span class='badge' ng-style='{ \"background-color\" : \"" + scope.data.severityVo.color + "\"," + "\"border-color\"" + " : \"" + scope.data.severityVo.color + "\", \"color\": \"#FFFFFF\" }' translate='" + scope.data.severityVo.name + "'></span>";
                                        //     break;
                                        // 연관 이슈 등록자
                                        case "REL_REGISTER":
                                            scope.data.registerVos = [scope.data.issueRelation.registerVo];
                                            makeTag += '<div owl-profile-over class="" table-user-image="data" target="registerVos"></div>';
                                            break;
                                            break;
                                        // case "REL_REGISTER":
                                        //     scope.data.registerVos = [scope.data.issueRelation.registerVo];
                                        //     makeTag += '<div owl-profile-over class="" table-user-image="data" target="registerVos"></div>';
                                        //     break;
                                        //
                                        //     break;
                                        // 연관 이슈 담당부서
                                        case "REL_ISSUE_DEPARTMENT" :
                                            makeTag += "<ul class='ul-not-comma'>";
                                            makeTag += "<div style='color: #000000'>";
                                            angular.forEach(scope.data.issueRelation.departmentVos, function (departments) {
                                                makeTag += "<li>" + departments.departmentName + "</li>";
                                            });
                                            makeTag += "</div>";
                                            makeTag += "</ul>";
                                            break;
                                        // case "REL_ISSUE_DEPARTMENT" :
                                        //     makeTag += "<ul class='ul-not-comma'>";
                                        //     makeTag += "<div style='color: #000000'>";
                                        //     angular.forEach(scope.data.issueRelation.departmentVos, function (departments) {
                                        //         makeTag += "<li>" + departments.departmentName + "</li>";
                                        //     });
                                        //     makeTag += "</div>";
                                        //     makeTag += "</ul>";
                                        //     break;
                                        // 연관 이슈 목록에서 기간 표시
                                        case "REL_ISSUE_DUE_DATE" :
                                            if (!$rootScope.isDefined(scope.data.issueRelation.startDate) && !$rootScope.isDefined(scope.data.issueRelation.completeDate)) {
                                                makeTag += "<span translate='common.noDate'>기간 없음</span>";
                                            }
                                            else {
                                                makeTag += "<span>" + scope.data.issueRelation.startDate + " ~ " + scope.data.issueRelation.completeDate + "</span>";
                                            }
                                            break;
                                        // case "REL_ISSUE_DUE_DATE" :
                                        //     if (!$rootScope.isDefined(scope.data.issueRelation.startDate) && !$rootScope.isDefined(scope.data.issueRelation.completeDate)) {
                                        //         makeTag += "<span translate='common.noDate'>기간 없음</span>";
                                        //     }
                                        //     else {
                                        //         makeTag += "<span>" + scope.data.issueRelation.startDate + " ~ " + scope.data.issueRelation.completeDate + "</span>";
                                        //     }
                                        //     break;
                                        // 연관 이슈 사용자 정의 필드
                                        case "REL_ISSUE_CUSTOM_FIELD_VALUE_VIEW" :
                                            var values = [];
                                            for (var count in scope.data.issueRelation.issueCustomFieldValueVos) {
                                                var issueCustomFieldValueVo = scope.data.issueRelation.issueCustomFieldValueVos[count];
                                                //  테이블 설정에서 dName 부분에 사용자 정의 필드 id 를 넣고 해당 값을 추출한다.
                                                if (tableConfig.columnHint.id == issueCustomFieldValueVo.customFieldVo.id) {
                                                    values.push(issueCustomFieldValueVo.useValue);
                                                }
                                            }
                                            angular.forEach(values, function (value) {
                                                makeTag += '<span class="table-word-break-all cursor">' + value + '<span><br>';
                                            });
                                            break;
                                        // case "REL_ISSUE_CUSTOM_FIELD_VALUE_VIEW" :
                                        //     var values = [];
                                        //
                                        //     for (var count in scope.data.issueRelation.issueCustomFieldValueVos) {
                                        //         var issueCustomFieldValueVo = scope.data.issueRelation.issueCustomFieldValueVos[count];
                                        //         //  테이블 설정에서 dName 부분에 사용자 정의 필드 id 를 넣고 해당 값을 추출한다.
                                        //         if (tableConfig.columnHint.id == issueCustomFieldValueVo.customFieldVo.id) {
                                        //             values.push(issueCustomFieldValueVo.useValue);
                                        //         }
                                        //     }
                                        //     angular.forEach(values, function (value) {
                                        //         makeTag += '<span class="table-word-break-all cursor">' + value + '<span><br>';
                                        //     });
                                        //
                                        //     break;
                                        // 하위 이슈 이동(제목)
                                        case "ISSUE_DOWN_MOVE" :
                                            makeTag += "<span class=\"titlename cursor\" ng-click=\"event.changeDetailView(data)\">" + scope.data.title + "</span></a>";
                                            break;
                                        // 하위 이슈 타입
                                        case "ISSUE_DOWN_STATUS_TYPE" :
                                            makeTag += "<span class='badge' ng-style='{ \"background-color\" : \"" + scope.data.issueStatusVo.color + "\"," + "\"border-color\"" + " : \"" + scope.data.issueStatusVo.color + "\", \"color\": \"#FFFFFF\" }'>" + scope.data.issueStatusVo.name + "</span>";
                                            break;
                                        // case "ISSUE_DOWN_MOVE" :
                                        //     makeTag += "<span class=\"titlename cursor\" ng-click=\"event.changeDetailView(data)\">" + scope.data.title + "</span></a>";
                                        //     break;
                                        //
                                        // // 하위 이슈 타입
                                        // case "ISSUE_DOWN_STATUS_TYPE" :
                                        //     makeTag += "<span class='badge' ng-style='{ \"background-color\" : \"" + scope.data.issueStatusVo.color + "\"," + "\"border-color\"" + " : \"" + scope.data.issueStatusVo.color + "\", \"color\": \"#FFFFFF\" }'>" + scope.data.issueStatusVo.name + "</span>";
                                        //     break;
                                        //
                                        // 하위 이슈 삭제
                                        case "ISSUE_DOWN_DELETE":
                                            if (scope.data.modifyPermissionCheck) {
                                                makeTag += '<img class="cursor" src="/assets/images/delete-icon.png" ng-click="event.removeDownIssue(data.id)">';
                                            }
                                            break;
                                        // case "ISSUE_DOWN_DELETE":
                                        //     if (scope.data.modifyPermissionCheck) {
                                        //         makeTag += '<img class="cursor" src="/assets/images/delete-icon.png" ng-click="event.removeDownIssue(data.id)">';
                                        //     }
                                        //     break;
                                        //
                                        // 하위 이슈 우선 순위
                                        case "DOWN_COMMON_PRIORITY" :
                                            makeTag += "<span class='badge' ng-style='{ \"background-color\" : \"" + scope.data.priorityVo.color + "\"," + "\"border-color\"" + " : \"" + scope.data.priorityVo.color + "\", \"color\": \"#FFFFFF\" }' translate='" + scope.data.priorityVo.name + "'></span>";
                                            break;
                                        // case "DOWN_COMMON_PRIORITY" :
                                        //     makeTag += "<span class='badge' ng-style='{ \"background-color\" : \"" + scope.data.priorityVo.color + "\"," + "\"border-color\"" + " : \"" + scope.data.priorityVo.color + "\", \"color\": \"#FFFFFF\" }' translate='" + scope.data.priorityVo.name + "'></span>";
                                        //     break;
                                        //
                                        // 하위 이슈 중요도
                                        case "DOWN_COMMON_SEVERITY" :
                                            makeTag += "<span class='badge' ng-style='{ \"background-color\" : \"" + scope.data.severityVo.color + "\"," + "\"border-color\"" + " : \"" + scope.data.severityVo.color + "\", \"color\": \"#FFFFFF\" }' translate='" + scope.data.severityVo.name + "'></span>";
                                            break;
                                        // case "DOWN_COMMON_SEVERITY" :
                                        //     makeTag += "<span class='badge' ng-style='{ \"background-color\" : \"" + scope.data.severityVo.color + "\"," + "\"border-color\"" + " : \"" + scope.data.severityVo.color + "\", \"color\": \"#FFFFFF\" }' translate='" + scope.data.severityVo.name + "'></span>";
                                        //     break;
                                        //
                                        // 하위 이슈 담당부서
                                        case "DOWN_ISSUE_DEPARTMENT" :
                                            makeTag += "<ul class='ul-not-comma'>";
                                            makeTag += "<div style='color: #000000'>";
                                            angular.forEach(scope.data.departmentVos, function (departments) {
                                                makeTag += "<li>" + departments.departmentName + "</li>";
                                            });
                                            makeTag += "</div>";
                                            makeTag += "</ul>";
                                            break;
                                        // case "DOWN_ISSUE_DEPARTMENT" :
                                        //     makeTag += "<ul class='ul-not-comma'>";
                                        //     makeTag += "<div style='color: #000000'>";
                                        //     angular.forEach(scope.data.departmentVos, function (departments) {
                                        //         makeTag += "<li>" + departments.departmentName + "</li>";
                                        //     });
                                        //     makeTag += "</div>";
                                        //     makeTag += "</ul>";
                                        //     break;
                                        //
                                        // 하위 이슈 등록자
                                        case "DOWN_REGISTER":
                                            scope.data.registerVos = [scope.data.registerVo];
                                            makeTag += '<div owl-profile-over class="" table-user-image="data" target="registerVos"></div>';
                                            break;
                                        // case "DOWN_REGISTER":
                                        //     scope.data.registerVos = [scope.data.registerVo];
                                        //     makeTag += '<div owl-profile-over class="" table-user-image="data" target="registerVos"></div>';
                                        //     break;
                                        //
                                        // 하위 이슈 기간
                                        case "DOWN_ISSUE_DUE_DATE" :
                                            if (!$rootScope.isDefined(scope.data.startDate) && !$rootScope.isDefined(scope.data.completeDate)) {
                                                makeTag += "<span translate='common.noDate'>기간 없음</span>";
                                            }
                                            else {
                                                makeTag += "<span>" + scope.data.startDate + " ~ " + scope.data.completeDate + "</span>";
                                            }
                                            break;
                                        // case "DOWN_ISSUE_DUE_DATE" :
                                        //     if (!$rootScope.isDefined(scope.data.startDate) && !$rootScope.isDefined(scope.data.completeDate)) {
                                        //         makeTag += "<span translate='common.noDate'>기간 없음</span>";
                                        //     }
                                        //     else {
                                        //         makeTag += "<span>" + scope.data.startDate + " ~ " + scope.data.completeDate + "</span>";
                                        //     }
                                        //     break;
                                        //
                                        // 하위 이슈 사용자 정의 필드
                                        case "DOWN_ISSUE_CUSTOM_FIELD_VALUE_VIEW" :
                                            var values = [];
                                            for (var count in scope.data.issueCustomFieldValueVos) {
                                                var issueCustomFieldValueVo = scope.data.issueCustomFieldValueVos[count];
                                                //  테이블 설정에서 dName 부분에 사용자 정의 필드 id 를 넣고 해당 값을 추출한다.
                                                if (tableConfig.columnHint.id == issueCustomFieldValueVo.customFieldVo.id) {
                                                    values.push(issueCustomFieldValueVo.useValue);
                                                }
                                            }
                                            angular.forEach(values, function (value) {
                                                makeTag += '<span class="table-word-break-all">' + value + '<span><br>';
                                            });
                                            break;
                                        // case "DOWN_ISSUE_CUSTOM_FIELD_VALUE_VIEW" :
                                        //     var values = [];
                                        //
                                        //     for (var count in scope.data.issueCustomFieldValueVos) {
                                        //         var issueCustomFieldValueVo = scope.data.issueCustomFieldValueVos[count];
                                        //         //  테이블 설정에서 dName 부분에 사용자 정의 필드 id 를 넣고 해당 값을 추출한다.
                                        //         if (tableConfig.columnHint.id == issueCustomFieldValueVo.customFieldVo.id) {
                                        //             values.push(issueCustomFieldValueVo.useValue);
                                        //         }
                                        //     }
                                        //     angular.forEach(values, function (value) {
                                        //         makeTag += '<span class="table-word-break-all">' + value + '<span><br>';
                                        //     });
                                        //
                                        //     break;
                                        //  이름을 클릭하면 수정 팝업 표시
                                        case "COMMON_MODIFY" :
src/main/webapp/i18n/ko/global.json
@@ -281,12 +281,16 @@
        "setIssueRelationTableDisplay": "연관 이슈 테이블 표시 설정",
        "setIssueDownTableDisplay": "하위 이슈 테이블 표시 설정",
        "columnName": "컬럼명",
        "selectPartners": "업체 선택",
        "partners" : "업체 이메일",
        "area": "넓이",
        "displayed": "표시 여부",
        "sequence": "순서",
        "cannotChangedIssueTitle": "이슈 제목은 변경할 수 없습니다.",
        "cannotChangedIssueType": "이슈 구분은 변경할 수 없습니다.",
        "selectSendIssueMail": "이슈 메일 발송 대상자 선택",
        "selectSendIssueMail": "업체 메일 발송",
        "CommonSendIssueMail": "일반 메일 발송",
        "SendIssueMail": "메일 발송 입력",
        "sendIssueSelectedUsers": "프로젝트에 참여하고 있는 다른 사용자에게 이슈 정보를 보냅니다.",
        "sendMail": "이메일 발송",
        "changedHistory": "이슈 변경 이력 상세정보",
@@ -327,6 +331,10 @@
        "succeededIssueMail": "이슈 메일 발송 완료",
        "sentToTheSelectedUser": "선택한 사용자에게 이메일이 발송되었습니다.",
        "failedIssueMail": "이슈 메일 발송 실패",
        "selectedPartnersMail": "업체 이메일을 선택하세요.",
        "selectedPartnersTemplate": "업체 이메일 템플릿을 선택해주세요.",
        "writeIssueMail": "이메일을 입력해주세요.",
        "writeMail": "이메일을 입력하셔야 추가할수 있습니다.",
        "issueVersionLookupFailed": "이슈 버전 조회 실패",
        "relationIssueType1" : "다음 이슈와 관련됨",
        "relationIssueType2" : "다음 이슈에 중복됨",
@@ -694,6 +702,8 @@
        "checkAll": "전체 선택",
        "unCheckAll": "전체 해제",
        "send": "보내기",
        "emailExplain": "받는 사람의 이메일 형식을 입력하셔야 합니다.",
        "sendToPerson" : "다른 사용자에게 메일을 보냅니다.",
        "selected": "선택됨",
        "selectable": "선택 가능",
        "password": "비밀번호",
@@ -710,6 +720,7 @@
        "priority": "우선 순위",
        "importance": "중요도",
        "assignee": "담당자",
        "toPerson": "받는 사람",
        "assigneeTeam" : "담당부서",
        "register": "등록자",
        "startDate": "시작일",
src/main/webapp/scripts/app/issue/issue.js
@@ -34,10 +34,10 @@
                            var deferred = $q.defer();
                            require([
                                'issueListTimelineController', 'issueManagerController', 'issueListController', 'issueAddController', 'issueModifyController', 'issueDetailController', 'issueAddRelationController', 'issueAddDownController', 'issueImportExcelController',
                                'chartLoader', 'jsTable', 'jsTree', 'tableColumnGenerator', 'treeColumnGenerator', 'modalFormAutoScroll', 'summerNote', 'summerNote-ko-KR', 'fullScroll', 'workflowService', 'priorityService', 'issueSearchService', 'issueTableConfigService', 'inputRegex',
                                'chartLoader', 'jsTable', 'jsTree', 'jsRel', 'jsDown', 'tableColumnGenerator', 'treeColumnGenerator', 'relColumnGenerator', 'downColumnGenerator', 'modalFormAutoScroll', 'summerNote', 'summerNote-ko-KR', 'fullScroll', 'workflowService', 'priorityService', 'issueSearchService', 'issueTableConfigService', 'inputRegex',
                                'severityService', 'issueTypeService', 'issueTypeCustomFieldService', 'issueService', 'issueStatusService', 'emailTemplateService','issueUserService','issueDepartmentService','issueModifyUserController', 'issueModifyDepartmentController', 'customFieldService', 'issueSearchFieldKeyViewElement',
                                'issueSearchCustomFieldViewElement', 'tableUserImage', 'fullScroll', 'issueCommentService', 'detectIssueEditor', 'formSubmit', 'issueModifyStatusController', 'downIssueModifyStatusController', 'jsShortCut',
                                'issueAddTableConfigController','issueAddRelationTableConfigController','issueAddDownTableConfigController','domAppend', 'issueDetailImagePreview', 'issueSendMailPartnersController', 'htmlDiff', 'issueVersionViewController', 'issueVersionService',
                                'issueAddTableConfigController','issueAddRelationTableConfigController','issueAddDownTableConfigController','domAppend', 'issueDetailImagePreview', 'issueSendMailPartnersController', 'issueCommonSendMailController', 'htmlDiff', 'issueVersionViewController', 'issueVersionService',
                                'jsHtmlDiff', 'issueReservationController', 'issueReservationService', 'issueVersionService', 'issueStatusAutoFocus', 'issueRelationService'
                            ], function () {
                                deferred.resolve();
src/main/webapp/scripts/app/issue/issueCommonSendMail.controller.js
New file
@@ -0,0 +1,111 @@
'use strict';
define([
        'app'
    ],
    function (app) {
        app.controller('issueCommonSendMailController', ['$scope', '$rootScope', '$state', '$log', '$resourceProvider', '$uibModalInstance', '$controller', '$injector', '$q','SweetAlert', '$filter', 'Issue', 'parameter',
            function ($scope, $rootScope, $state, $log, $resourceProvider, $uibModalInstance, $controller, $injector, $q, SweetAlert, $filter, Issue, parameter) {
                $scope.fn = {
                    cancel : cancel,    //  팝업 창 닫기
                    formSubmit : formSubmit,    //  폼 전송
                    formCheck : formCheck,   //  폼 체크
                    addInput : addInput,
                    removeInput : removeInput
                };
                $scope.vm = {
                    form : {
                        issueId : parameter.issueId,
                        title : "",
                        description: "",
                        inputs : [0],
                        emails : {},    //  입력된 사용자 이메일
                    },
                    userName : "",
                    autoCompletePage : {
                        user : {
                            page : 0,
                            totalPage : 0
                        }
                    }
                };
                // 폼 체크
                function formCheck(formInvalid) {
                    if (formInvalid) {
                        return true;
                    }
                    return false;
                }
                // 메일 주소 input 창 추가 버튼
                function addInput() {
                    var arrayFull = true;      // 배열이 가득 차 있는지 여부
                    var index = 0;
                    $scope.vm.form.inputs.forEach(function (email) {
                        if (!$rootScope.isDefined($scope.vm.form.emails[index])) {
                            arrayFull = false;
                        }
                        index++;
                    });
                    if (arrayFull) {
                        $scope.vm.form.inputs.push(index);
                        $scope.vm.form.emails[index] = ""
                    } else {
                        SweetAlert.warning($filter("translate")("issue.writeIssueMail"), $filter("translate")("issue.writeMail")); // 추가버튼 경고
                    }
                }
                //  폼 전송
                function formSubmit() {
                    $rootScope.spinner = true;
                    var content = {
                        issueId : $scope.vm.form.issueId,
                        title : $scope.vm.form.title,
                        description : $scope.vm.form.description,
                        sendEmails : (function () {
                            var sendEmails = [];
                            var index = 0
                            $scope.vm.form.inputs.forEach(function (email) {
                                if ($rootScope.isDefined($scope.vm.form.emails[index])) {
                                    sendEmails.push($rootScope.encryption($scope.vm.form.emails[index]));   // 이메일주소 암호화
                                }
                                index++;
                            });
                            return sendEmails;
                        })()
                    };
                    Issue.sendCommonEmail($resourceProvider.getContent(
                        content,
                        $resourceProvider.getPageContent(0, 10))).then(function (result) {
                        if (result.data.message.status === "success") {
                            SweetAlert.success($filter("translate")("issue.succeededIssueMail"), $filter("translate")("issue.sentToTheSelectedUser")); // "이슈 메일 발송 완료"
                            $scope.fn.cancel();
                        }
                        else {
                            SweetAlert.error($filter("translate")("issue.failedIssueMail"), result.data.message.message); // "이슈 메일 발송 실패"
                        }
                        $rootScope.spinner = false;
                    });
                }
                // 이메일 주소 input 삭제
                function removeInput(index) {
                    $scope.vm.form.inputs.splice(index, 1);
                }
                function cancel() {
                    $rootScope.$broadcast("closeLayer");    //  팝업이 열리고 나서 js-multi, js-single 등에서 body 이벤트가 날아가는 현상 수정
                    $uibModalInstance.dismiss('cancel');
                    $(document).unbind("keydown");  //  단축키 이벤트 제거
                }
            }]);
    });
src/main/webapp/scripts/app/issue/issueDetail.controller.js
@@ -8,9 +8,9 @@
        'angular'
    ],
    function (app, angular) {
        app.controller('issueDetailController', ['$scope', '$rootScope', '$log', '$resourceProvider', '$tableProvider', '$state', '$uibModal', '$q',
        app.controller('issueDetailController', ['$scope', '$rootScope', '$log', '$resourceProvider', '$tableProvider', '$relProvider', '$downProvider', '$state', '$uibModal', '$q',
            '$controller', '$injector', 'SweetAlert', '$timeout', 'Issue', 'IssueComment', 'IssueRelation', 'AttachedFile',  'Priority', 'Severity','IssueStatus', 'IssueTableConfig', '$filter',
            function ($scope, $rootScope, $log, $resourceProvider, $tableProvider, $state, $uibModal, $q, $controller, $injector, SweetAlert, $timeout, Issue, IssueComment, IssueRelation, AttachedFile, Priority, Severity, IssueStatus, IssueTableConfig, $filter) {
            function ($scope, $rootScope, $log, $resourceProvider, $tableProvider, $relProvider, $downProvider, $state, $uibModal, $q, $controller, $injector, SweetAlert, $timeout, Issue, IssueComment, IssueRelation, AttachedFile, Priority, Severity, IssueStatus, IssueTableConfig, $filter) {
                //  IssueListController vm, fn 변수 상속.
@@ -54,6 +54,7 @@
                $scope.fn.removeRelationIssue = removeRelationIssue;
                $scope.fn.removeDownIssue = removeDownIssue;
                $scope.fn.changeDetailPageRowCount = changeDetailPageRowCount;    //  페이지 변경
                $scope.fn.sendCommonMail = sendCommonMail;
                //  이슈 목록 컨트롤러 vm, fn 상속 중
                $scope.vm.viewer = {};      // 현재 이슈
@@ -327,90 +328,98 @@
                }
                //  테이블의 연관 이슈 컬럼을 만들어준다.
                function setRelTableColumn(Rel_issueTableConfig) {
                function setRelTableColumn(issueTableConfig) {
                    //  연관 이슈 컬럼
                    switch(Rel_issueTableConfig.key) {
                    switch(issueTableConfig.key) {
                        case "RELATION_ISSUE_TYPE" :    // 연관 이슈 구분
                            $scope.vm.relTableConfigs.push($tableProvider.config()
                            $scope.vm.relTableConfigs.push($relProvider.config()
                                .setHName("issue.relationIssueType")
                                .setDName("relationIssueType")
                                .setDType("renderer")
                                .setDAlign("text-center")
                                .setHWidth("bold " + Rel_issueTableConfig.width)
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDRenderer("ISSUE_RELATION_TYPE"))
                                /*.setHWidth("width-30 bold")*/
                                /*.setHSort(false)*/
                            break;
                        case "RELATION_ISSUE_TITLE" :   // 연관 이슈 제목
                            $scope.vm.relTableConfigs.push($tableProvider.config()
                            $scope.vm.relTableConfigs.push($relProvider.config()
                                .setHName("issue.relationIssueTitle")
                                .setDName("title")
                                .setDType("renderer")
                                .setDAlign("text-center")
                                .setHWidth("bold " + Rel_issueTableConfig.width)
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDRenderer("ISSUE_RELATION_MOVE"))
                                /*.setHWidth("width-60 bold")*/
                                /*.setHSort(true)*/
                            break;
                        case "RELATION_PRIORITY" :   // 연관 이슈 우선순위
                            $scope.vm.relTableConfigs.push($tableProvider.config()
                            $scope.vm.relTableConfigs.push($relProvider.config()
                                .setHName("common.priority")
                                .setDName("priorityVo.id")
                                .setDType("renderer")
                                .setHWidth("bold " + Rel_issueTableConfig.width)
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("REL_COMMON_PRIORITY"));
                            break;
                        case "RELATION_SEVERITY" :   //  연관 이슈 중요도
                            $scope.vm.relTableConfigs.push($tableProvider.config()
                            $scope.vm.relTableConfigs.push($relProvider.config()
                                .setHName("common.importance")
                                .setDName("severityVo.id")
                                .setDType("renderer")
                                .setHWidth("bold " + Rel_issueTableConfig.width)
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("REL_COMMON_SEVERITY"));
                            break;
                        case "RELATION_ASSIGNEE_TEAM" :   // 연관 이슈 담당부서
                            $scope.vm.relTableConfigs.push($tableProvider.config()
                            $scope.vm.relTableConfigs.push($relProvider.config()
                                .setHName("common.assigneeTeam")
                                .setDName("departmentVos.departmentName")
                                .setDType("renderer")
                                .setHWidth("bold " + Rel_issueTableConfig.width)
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("REL_ISSUE_DEPARTMENT"));
                            break;
                        case "RELATION_REGISTER" :   // 연관 이슈  등록자
                            $scope.vm.relTableConfigs.push($tableProvider.config()
                        case "RELATION_REGISTER" :   // 연관 이슈 등록자
                            $scope.vm.relTableConfigs.push($relProvider.config()
                                .setHName("common.register")
                                .setDName("registerVo.id")
                                .setDType("renderer")
                                .setHWidth("bold " + Rel_issueTableConfig.width)
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("REL_REGISTER"));
                            break;
                        case "RELATION_PERIOD" : // 연관 이슈 기간
                            $scope.vm.relTableConfigs.push($tableProvider.config()
                            $scope.vm.relTableConfigs.push($relProvider.config()
                                .setHName("common.period")
                                .setDType("renderer")
                                .setHWidth("bold " + Rel_issueTableConfig.width)
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("REL_ISSUE_DUE_DATE"));
                            break;
                        case "RELATION_MODIFY_DATE" : // 연관 이슈  최근 변경일
                            $scope.vm.relTableConfigs.push($tableProvider.config()
                        case "RELATION_MODIFY_DATE" : // 연관 이슈 최근 변경일
                            $scope.vm.relTableConfigs.push($relProvider.config()
                                .setHName("common.lastChangeDate")
                                .setHWidth("bold " + Rel_issueTableConfig.width)
                                .setDAlign("text-center"));
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDName("modifyDate"));
                            break;
                    }
                    //  사용자 정의 필드 컬럼
                    if (Rel_issueTableConfig.key.indexOf("CUSTOM_FIELD_") !== -1) {
                    if (issueTableConfig.key.indexOf("CUSTOM_FIELD_") !== -1) {
                        //  만약 이슈 테이블 컬럼명이 표시되지 않으면 이쪽이 문제
                        for (var count in $scope.vm.customFields) {
                            var customField = $scope.vm.customFields[count];
                            if (customField.id === Number(Rel_issueTableConfig.key.substring(13))) {
                                $scope.vm.relTableConfigs.push($tableProvider.config()
                            if (customField.id === Number(issueTableConfig.key.substring(13))) {
                                $scope.vm.relTableConfigs.push($relProvider.config()
                                    .setHName(customField.name)
                                    .setDName("relCustomFieldName" + [count])
                                    .setDType("renderer")
                                    .setHWidth("bold " + Rel_issueTableConfig.width)
                                    .setHWidth("bold " + issueTableConfig.width)
                                    .setDAlign("text-center")
                                    .setColumnHint(customField)
                                    .setDRenderer("REL_ISSUE_CUSTOM_FIELD_VALUE_VIEW"));
@@ -422,88 +431,96 @@
                //  테이블의 하위 이슈 컬럼을 만들어준다.
                function setDownTableColumn(Down_issueTableConfig) {
                function setDownTableColumn(issueTableConfig) {
                    // if (issueTableConfig == null) return;
                    //  하위 이슈 컬럼
                    switch(Down_issueTableConfig.key) {
                    switch(issueTableConfig.key) {
                        case "DOWN_ISSUE_TITLE" :   //  하위 이슈 제목
                            $scope.vm.downTableConfigs.push($tableProvider.config()
                            $scope.vm.downTableConfigs.push($downProvider.config()
                                .setHName("issue.downIssueTitle")
                                .setDName("title")
                                .setDType("renderer")
                                .setHWidth("bold " + Down_issueTableConfig.width)
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("ISSUE_DOWN_MOVE"));
                            break;
                        case "ISSUE_DOWN_STATUS_TYPE" : //  이슈 상태
                            $scope.vm.downTableConfigs.push($tableProvider.config()
                        case "ISSUE_DOWN_STATUS_TYPE" : // 하위 이슈 상태
                            $scope.vm.downTableConfigs.push($downProvider.config()
                                .setHName("issue.issueStatus")
                                .setDName("issueStatusVo.id")
                                .setDType("renderer")
                                .setHWidth("bold " + Down_issueTableConfig.width)
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("ISSUE_DOWN_STATUS_TYPE"));
                            break;
                        case "DOWN_PRIORITY" :   // 하위 이슈 우선순위
                            $scope.vm.downTableConfigs.push($tableProvider.config()
                            $scope.vm.downTableConfigs.push($downProvider.config()
                                .setHName("common.priority")
                                .setDName("priorityVo.id")
                                .setDType("renderer")
                                .setHWidth("bold " + Down_issueTableConfig.width)
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("DOWN_COMMON_PRIORITY"));
                            break;
                        case "DOWN_SEVERITY" :   //  중요도
                            $scope.vm.downTableConfigs.push($tableProvider.config()
                        case "DOWN_SEVERITY" :   // 하위 이슈 중요도
                            $scope.vm.downTableConfigs.push($downProvider.config()
                                .setHName("common.importance")
                                .setDName("severityVo.id")
                                .setDType("renderer")
                                .setHWidth("bold " + Down_issueTableConfig.width)
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("DOWN_COMMON_SEVERITY"));
                            break;
                        case "DOWN_ASSIGNEE_TEAM" :   //  담당부서
                            $scope.vm.downTableConfigs.push($tableProvider.config()
                        case "DOWN_ASSIGNEE_TEAM" :   // 하위 이슈 담당부서
                            $scope.vm.downTableConfigs.push($downProvider.config()
                                .setHName("common.assigneeTeam")
                                .setDName("departmentVos.departmentName")
                                .setDType("renderer")
                                .setHWidth("bold " + Down_issueTableConfig.width)
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("DOWN_ISSUE_DEPARTMENT"));
                            break;
                        case "DOWN_REGISTER" :   //  등록자
                            $scope.vm.downTableConfigs.push($tableProvider.config()
                        case "DOWN_REGISTER" :   // 하위 이슈 등록자
                            $scope.vm.downTableConfigs.push($downProvider.config()
                                .setHName("common.register")
                                .setDName("registerVo.id")
                                .setDType("renderer")
                                .setHWidth("bold " + Down_issueTableConfig.width)
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("DOWN_REGISTER"));
                            break;
                        case "DOWN_PERIOD" : //  기간
                            $scope.vm.downTableConfigs.push($tableProvider.config()
                        case "DOWN_PERIOD" : // 하위 이슈 기간
                            $scope.vm.downTableConfigs.push($downProvider.config()
                                .setHName("common.period")
                                .setDType("renderer")
                                .setHWidth("bold " + Down_issueTableConfig.width)
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDRenderer("DOWN_ISSUE_DUE_DATE"));
                            break;
                        case "DOWN_MODIFY_DATE" : //  최근 변경일
                            $scope.vm.downTableConfigs.push($tableProvider.config()
                        case "DOWN_MODIFY_DATE" : // 하위 이슈 최근 변경일
                            $scope.vm.downTableConfigs.push($downProvider.config()
                                .setHName("common.lastChangeDate")
                                .setHWidth("bold " + Down_issueTableConfig.width)
                                .setDAlign("text-center"));
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
                                .setDName("modifyDate"));
                            break;
                    }
                    //  사용자 정의 필드 컬럼
                    if (Down_issueTableConfig.key.indexOf("CUSTOM_FIELD_") !== -1) {
                    if (issueTableConfig.key.indexOf("CUSTOM_FIELD_") !== -1) {
                        //  만약 이슈 테이블 컬럼명이 표시되지 않으면 이쪽이 문제
                        for (var count in $scope.vm.customFields) {
                            var customField = $scope.vm.customFields[count];
                            if (customField.id === Number(Down_issueTableConfig.key.substring(13))) {
                                $scope.vm.downTableConfigs.push($tableProvider.config()
                            if (customField.id === Number(issueTableConfig.key.substring(13))) {
                                $scope.vm.downTableConfigs.push($downProvider.config()
                                    .setHName(customField.name)
                                    .setDName("downCustomFieldName" + [count])
                                    .setDType("renderer")
                                    .setHWidth("bold " + Down_issueTableConfig.width)
                                    .setHWidth("bold " + issueTableConfig.width)
                                    .setDAlign("text-center")
                                    .setColumnHint(customField)
                                    .setDRenderer("DOWN_ISSUE_CUSTOM_FIELD_VALUE_VIEW"));
@@ -517,18 +534,18 @@
                //  연관 이슈 테이블 설정
                function makeRelTableConfigs() {
                    $scope.vm.relTableConfigs = [];
                    $scope.vm.relTableConfigs.push($tableProvider.config()
                    $scope.vm.relTableConfigs.push($relProvider.config()
                        .setDType("checkbox")
                        .setHWidth("width-20-p")
                        .setDAlign("text-center"))
                    $scope.vm.relTableConfigs.push($tableProvider.config()
                    $scope.vm.relTableConfigs.push($relProvider.config()
                        .setHName("issue.relationIssueType")
                        .setDType("renderer")
                        .setDAlign("text-center")
                        .setHWidth("width-60-p bold")
                        .setHSort(false)
                        .setDRenderer("ISSUE_RELATION_TYPE"))
                    $scope.vm.relTableConfigs.push($tableProvider.config()
                    $scope.vm.relTableConfigs.push($relProvider.config()
                        .setHName("issue.relationIssueTitle")
                        .setDType("renderer")
                        .setDAlign("text-center")
@@ -536,7 +553,7 @@
                        .setHSort(false)
                        .setDRenderer("ISSUE_RELATION_MOVE"))
                    /*if($scope.vm.viewer.modifyPermissionCheck) {
                        $scope.vm.relTableConfigs.push($tableProvider.config()
                        $scope.vm.relTableConfigs.push($relProvider.config()
                            .setHName("issue.relationIssueDelete")
                            .setDType("renderer")
                            .setDAlign("text-center")
@@ -545,11 +562,11 @@
                            .setHSort(false)
                            .setDAlign("text-center"))
                    }*/
                    angular.forEach($scope.vm.relTableConfigs, function (Rel_issueTableConfig) {
                    angular.forEach($scope.vm.relTableConfigs, function (issueTableConfig) {
                        //  표시 대상인 컬럼만 화면에 그려준다.
                        if (Rel_issueTableConfig.display) {
                        if (issueTableConfig.display) {
                            //  테이블의 컬럼을 만들어준다.
                            $scope.fn.setRelTableColumn(Rel_issueTableConfig);
                            $scope.fn.setRelTableColumn(issueTableConfig);
                        }
                    });
                }
@@ -557,11 +574,11 @@
                //  하위 이슈 테이블 설정
                function makeDownTableConfigs() {
                    $scope.vm.downTableConfigs = [];
                    $scope.vm.downTableConfigs.push($tableProvider.config()
                    $scope.vm.downTableConfigs.push($downProvider.config()
                        .setDType("checkbox")
                        .setHWidth("width-20-p")
                        .setDAlign("text-center"))
                    $scope.vm.downTableConfigs.push($tableProvider.config()
                    $scope.vm.downTableConfigs.push($downProvider.config()
                        .setHName("issue.downIssueTitle")
                        .setDType("renderer")
                        .setDAlign("text-center")
@@ -569,7 +586,7 @@
                        .setHSort(false)
                        .setDRenderer("ISSUE_DOWN_MOVE"))
                    /*if($scope.vm.viewer.modifyPermissionCheck){
                        $scope.vm.downTableConfigs.push($tableProvider.config()
                        $scope.vm.downTableConfigs.push($downProvider.config()
                            .setHName("issue.relationIssueDelete")
                            .setDType("renderer")
                            .setDAlign("text-center")
@@ -579,11 +596,11 @@
                            .setDAlign("text-center"))
                    }*/
                    angular.forEach($scope.vm.downTableConfigs, function (Down_issueTableConfig) {
                    angular.forEach($scope.vm.downTableConfigs, function (issueTableConfig) {
                        //  표시 대상인 컬럼만 화면에 그려준다.
                        if (Down_issueTableConfig.display) {
                        if (issueTableConfig.display) {
                            //  테이블의 컬럼을 만들어준다.
                            $scope.fn.setDownTableColumn(Down_issueTableConfig);
                            $scope.fn.setDownTableColumn(issueTableConfig);
                        }
                    });
                }
@@ -596,7 +613,7 @@
                    }
                    var issueTableConfigs = issueTableConfigVo.issueTableConfigs;
                    //  연관 슈 목록 테이블 설정 값을 가져와서 적용한다.
                    //  연관 이슈 목록 테이블 설정 값을 가져와서 적용한다.
                    if ($rootScope.isDefined(issueTableConfigs)) {
                        //  이슈 테이블 설정 정보를 저장 한다.
@@ -607,34 +624,34 @@
                        });
                        $scope.vm.relTableConfigs = [];
    /*                  $scope.vm.relTableConfigs.push($tableProvider.config()
    /*                  $scope.vm.relTableConfigs.push($relProvider.config()
                           .setHName("issue.relationIssueType")
                           .setDType("renderer")
                           .setDAlign("text-center")
                           .setHWidth("width-30-p bold")
                           .setHSort(false)
                           .setDRenderer("ISSUE_RELATION_TYPE"))
                        $scope.vm.relTableConfigs.push($tableProvider.config()
                        $scope.vm.relTableConfigs.push($relProvider.config()
                           .setHName("issue.relationIssueTitle")
                           .setDType("renderer")
                           .setDAlign("text-center")
                           .setHWidth("width-60-p bold")
                           .setHSort(false)
                           .setDRenderer("ISSUE_RELATION_MOVE"))*/
                        $scope.vm.relTableConfigs.push($tableProvider.config()
                        $scope.vm.relTableConfigs.push($relProvider.config()
                            .setDType("checkbox")
                            .setHWidth("width-20-p")
                            .setDAlign("text-center"))
                        angular.forEach($scope.vm.issueRelTableConfigs, function (Rel_issueTableConfig) {
                        angular.forEach($scope.vm.issueRelTableConfigs, function (issueTableConfig) {
                            //  표시 대상인 컬럼만 화면에 그려준다.
                            if (Rel_issueTableConfig.display) {
                            if (issueTableConfig.display) {
                                //  테이블의 컬럼을 만들어준다.
                                $scope.fn.setRelTableColumn(Rel_issueTableConfig);
                                $scope.fn.setRelTableColumn(issueTableConfig);
                            }
                        });
                        /*if($scope.vm.viewer.modifyPermissionCheck) {
                            $scope.vm.relTableConfigs.push($tableProvider.config()
                            $scope.vm.relTableConfigs.push($relProvider.config()
                                .setHName("issue.relationIssueDelete")
                                .setDType("renderer")
                                .setHWidth("width-40-p bold")
@@ -653,7 +670,7 @@
                    if (issueTableConfigVo == null) return;
                    var issueTableConfigs = issueTableConfigVo.issueTableConfigs;
                    //  연관 슈 목록 테이블 설정 값을 가져와서 적용한다.
                    //  연관 이슈 목록 테이블 설정 값을 가져와서 적용한다.
                    if ($rootScope.isDefined(issueTableConfigs)) {
                        //  이슈 테이블 설정 정보를 저장 한다.
                        $scope.vm.issueDownTableConfigs = [];
@@ -662,26 +679,26 @@
                            return a.position < b.position ? -1 : a.position > b.position ? 1 : 0;
                        });
                        $scope.vm.downTableConfigs = [];
/*                      $scope.vm.downTableConfigs.push($tableProvider.config()
/*                      $scope.vm.downTableConfigs.push($downProvider.config()
                            .setHName("issue.downIssueTitle")
                            .setDType("renderer")
                            .setDAlign("text-center")
                            .setHWidth("width-60-p bold")
                            .setHSort(false)
                            .setDRenderer("ISSUE_DOWN_MOVE"))*/
                        $scope.vm.downTableConfigs.push($tableProvider.config()
                        $scope.vm.downTableConfigs.push($downProvider.config()
                            .setDType("checkbox")
                            .setHWidth("width-20-p")
                            .setDAlign("text-center"))
                        angular.forEach($scope.vm.issueDownTableConfigs, function (Down_issueTableConfig) {
                        angular.forEach($scope.vm.issueDownTableConfigs, function (issueTableConfig) {
                            //  표시 대상인 컬럼만 화면에 그려준다.
                            if (Down_issueTableConfig.display) {
                            if (issueTableConfig.display) {
                                //  테이블의 컬럼을 만들어준다.
                                $scope.fn.setDownTableColumn(Down_issueTableConfig);
                                $scope.fn.setDownTableColumn(issueTableConfig);
                            }
                        });
                        /*if($scope.vm.viewer.modifyPermissionCheck) {
                            $scope.vm.downTableConfigs.push($tableProvider.config()
                            $scope.vm.downTableConfigs.push($downProvider.config()
                                .setHName("issue.relationIssueDelete")
                                .setDType("renderer")
                                .setHWidth("width-40-p bold")
@@ -1023,6 +1040,8 @@
                                $scope.vm.viewer.startDate = result.data.data.startDate == null ? "common.unspecified" : result.data.data.startDate; // 미지정
                                $scope.vm.viewer.completeDate = result.data.data.completeDate == null ? "common.unspecified" : result.data.data.completeDate; // 미지정
                                $scope.vm.rangeDate = result.data.data.startDate + "~" + result.data.data.completeDate
                                //  이슈 유형에 연결된 사용자 정의 필드 정보를 입력 폼에서 사용할 수 있게 가공한다.
                                $scope.fn.setFormByIssueTypeCustomFields(result.data.data.issueTypeCustomFieldVos);
                                //  이슈에서 사용자가 선택한 사용자 정의 필드 값을 입력 폼에 셋팅한다.
@@ -1339,6 +1358,21 @@
                    });
                }
                // 일반 메일 발송 (사용자 직접 작성)
                function sendCommonMail() {
                    $uibModal.open({
                        templateUrl : 'views/issue/issueCommonSendMail.html',
                        size : "md",
                        controller : 'issueCommonSendMailController',
                        backdrop : 'static',
                        resolve : {
                            parameter : {
                                issueId : $scope.vm.viewer.id,
                            }
                        }
                    });
                }
                function getParametersAll() {
                    var params = $scope.fn.getParameters(null, $scope.vm.viewer.issueCompanyVos);
                    params = $scope.fn.getParameters(params, $scope.vm.viewer.issueIspVos);
src/main/webapp/scripts/app/issue/issueList.controller.js
@@ -336,7 +336,7 @@
                        case "ISSUE_TITLE" :   //  이슈 제목
                            $scope.vm.tableConfigs.push($tableProvider.config()
                                .setHName("issue.issueTitle")
                                .setDName("issueNumber") /* todo 이건 타이틀로 변경해야하는데*/
                                .setDName("issueTitle")
                                .setDType("renderer")
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
@@ -373,7 +373,7 @@
                        case "ASSIGNEE_TEAM" :   //  담당부서
                            $scope.vm.tableConfigs.push($tableProvider.config()
                                .setHName("common.assigneeTeam")
                                .setDName("departmentName") /* todo 체크*/
                                .setDName("departmentName")
                                .setDType("renderer")
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
@@ -391,7 +391,6 @@
                        case "PERIOD" : //  기간
                            $scope.vm.tableConfigs.push($tableProvider.config()
                                .setHName("common.period")
                                .setDName("startDate")
                                .setDType("renderer")
                                .setHWidth("bold " + issueTableConfig.width)
                                .setDAlign("text-center")
src/main/webapp/scripts/app/issue/issueSendMailPartners.controller.js
@@ -17,7 +17,6 @@
                    formCheck : formCheck,   //  폼 체크
                    onChangeEmailTemplate : onChangeEmailTemplate, // 이메일 템플릿 선택시 실행
                    getEmailTemplateList : getEmailTemplateList // 이메일 템플릿 목록 가져오기
                    // showEmailTemplate : showEmailTemplate,
                };
                $scope.vm = {
@@ -26,6 +25,7 @@
                    hostingVos : parameter.hostingVos,
                    partners : parameter.partnersAll.slice(),
                    html : "",
                    tab : "SEND_TEMPLATE",
                    form : {
                        id : parameter.issueId,  //  이슈 번호
                        projects : [{ id : parameter.projectId}],  //  프로젝트
@@ -59,7 +59,8 @@
                            $scope.vm.html = result.data.data.template;
                        }
                        else {
                            SweetAlert.error($filter("translate")("issue.failedIssueMail"), result.data.message.message); // "이슈 메일 발송 실패"
                            $scope.vm.html = "";
                            //SweetAlert.warning($filter("translate")("issue.selectedPartnersMail"),$filter("translate")("issue.selectedPartnersTemplate")); // option 선택 경고
                        }
                        $rootScope.spinner = false;
@@ -85,14 +86,13 @@
                    $scope.vm.emailTemplateId = 1;
                    $scope.vm.emailTitle = "";
                    $scope.vm.emailTemplates.forEach(function (emailTemplate) {
                        /*if (emailTemplate.id === $scope.vm.emailTemplateId) {
                            $scope.vm.emailTitle = emailTemplate.title;
                        }*/
                        if (emailTemplate.templateType === $scope.vm.emailTemplateType) {
                            $scope.vm.emailTitle = emailTemplate.title;
                        }
                    })
                    if ($rootScope.isDefined($scope.vm.emailTemplateType)) {
                        $scope.vm.emailTemplates.forEach(function (emailTemplate) {
                            if (emailTemplate.templateType === $scope.vm.emailTemplateType) {
                                $scope.vm.emailTitle = emailTemplate.title;
                            }
                        })
                    }
                }
@@ -182,6 +182,7 @@
                }
                $scope.fn.getEmailTemplateList();
            }]);
    });
src/main/webapp/scripts/app/project/projectModify.controller.js
@@ -182,7 +182,7 @@
                            if (angular.isDefined(result.data.data)) {
                                $scope.vm.form.name = result.data.data.name;
                                $scope.vm.form.status = result.data.data.status;
                                if (result.data.data.startDate == "" && result.data.data.endDate == "") {
                                if (result.data.data.startDate == "" && result.data.data.endDate == "" || result.data.data.startDate == "null" && result.data.data.endDate == "null") {
                                    $scope.vm.form.startEndDateRange = ""
                                } else {
                                    $scope.vm.form.startEndDateRange = result.data.data.startDate + " ~ " + result.data.data.endDate;
src/main/webapp/scripts/components/auth/auth.interceptor.js
@@ -11,6 +11,16 @@
                        $tableProvider.setOrderByColumn();
                        $tableProvider.reverse = false;
                        //  연관 테이블 컬럼 정렬 초기화
                        var $relProvider = $injector.get('$relProvider');
                        $relProvider.setOrderByColumn();
                        $relProvider.reverse = false;
                        //  하위 테이블 컬럼 정렬 초기화
                        var $downProvider = $injector.get('$downProvider');
                        $downProvider.setOrderByColumn();
                        $downProvider.reverse = false;
                        if (angular.isDefined(config.data)) {
                            $log.debug(config.url + " : ", config.data);
                        }
src/main/webapp/scripts/components/issue/issue.service.js
@@ -144,6 +144,12 @@
                    $log.debug("이슈 이메일 발송 결과 : ", response);
                    return response;
                });
            },
            sendCommonEmail : function (conditions) {
                return $http.post("issue/sendCommonEmail", conditions).then(function (response) {
                    $log.debug("이슈 이메일 발송 결과 : ", response);
                    return response;
                });
            }
        }
    }
src/main/webapp/scripts/config.js
@@ -35,7 +35,7 @@
                //  로그 제어
                $logProvider.debugEnabled(true);
            })
            .run(function ($rootScope, $state, $sce, $log, $injector, $translate, $tableProvider, Principal, Auth, Language, SweetAlert, $filter, Workspace, $resourceProvider, User, constants, Project) {
            .run(function ($rootScope, $state, $sce, $log, $injector, $translate, $tableProvider, $relProvider, $downProvider, Principal, Auth, Language, SweetAlert, $filter, Workspace, $resourceProvider, User, constants, Project) {
                $rootScope.$state = $state;
                //  html 태그 웹 보안 적용하여 바인딩.
@@ -117,6 +117,12 @@
                //  테이블 설정 및 기능을 관리하는 서비스
                $rootScope.$tableProvider = $tableProvider;
                //  연관테이블 설정 및 기능 관리하는 서비스
                $rootScope.$relProvider = $relProvider;
                //  하위테이블 설정 및 기능 관리하는 서비스
                $rootScope.$downProvider = $downProvider;
                //  html tag convert - 신뢰할 수 있는 입력 값일 때만 사용, 사용자가 등록하는 값에는 사용 금지
                $rootScope.trustAsHtml = function (string) {
@@ -568,6 +574,9 @@
                    //  table orderBy column init setting
                    $tableProvider.setOrderByColumn();
                    $tableProvider.reverse = false;
                    $relProvider.setOrderByColumn();
                    $relProvider.reverse = false;
                    //  이슈 목록->상세화면에서 마지막으로 접근한 이슈 아이디 - 라우트 탈때마다 초기화
                    $rootScope.currentDetailIssueId = null;
                    // 사용자 정보를 가져온다.
src/main/webapp/scripts/main.js
@@ -56,13 +56,19 @@
        'commonController' : 'app/common/common.controller', //  공통 컨트롤러
        'tableProvider' : '../custom_components/js-table/table.provider', //  테이블 속성 값을 관리한다.
        'treeProvider' : '../custom_components/js-tree/tree.provider', //   트리 속성 값을 관리한다.
        'relProvider' : '../custom_components/js-rel/rel.provider', //   연관 테이블 속성 값을 관리한다.
        'downProvider' : '../custom_components/js-down/down.provider', //   하위 테이블 속성 값을 관리한다.
        'resourceProvider' : 'components/utils/resource.provider',   //  공통적으로 서버 json 전송에 사용
        'lodash' : '../bower_components/lodash/lodash.min', //  멀티 셀렉트, auto complete 컴포넌트들에서 사용
        'angularDropMultiSelect' : '../custom_components/angular-multi-select/angularjs-dropdown-multiselect',  //  멀티 셀렉트 컴포넌트
        'jsTable' : '../custom_components/js-table/js-table.directive',   //  목록 화면에서 사용되는 테이블을 호출한다.
        'jsTree' : '../custom_components/js-tree/js-tree.directive',   //  목록 화면에서 사용되는 테이블(트리구조)을 호출한다.
        'jsRel' : '../custom_components/js-rel/js-rel.directive',   //  이슈상세 화면에서 사용되는 연관 테이블을 호출한다.
        'jsDown' : '../custom_components/js-down/js-down.directive',   //  이슈상세 화면에서 사용되는 하위 테이블을 호출한다.
        'tableColumnGenerator' : '../custom_components/js-table/tableColumnGenerator.directive', //  테이블 랜더러를 담당한다.
        'treeColumnGenerator' : '../custom_components/js-tree/treeColumnGenerator.directive', //  테이블 랜더러를 담당한다.
        'treeColumnGenerator' : '../custom_components/js-tree/treeColumnGenerator.directive', //  이슈리스트 트리 랜더러를 담당한다.
        'relColumnGenerator' : '../custom_components/js-rel/relColumnGenerator.directive', //  이슈상세 연관 랜더러를 담당한다.
        'downColumnGenerator' : '../custom_components/js-down/downColumnGenerator.directive', //  이슈상세 하위 랜더러를 담당한다.
        'jsAutoCompleteMulti' : '../custom_components/js-autocomplete-multi/js-autocomplete-multi', //  다중 선택이 가능한 autoComplete 컴포넌트
        'jsInputAutoComplete' : '../custom_components/js-input-autocomplete/js-input-autocomplete',   //  input 박스에 autoComplete 기능이 붙은 컴포넌트
        'jsAutoCompleteSingle' : '../custom_components/js-autocomplete-single/js-autocomplete-single',   //  input 박스에 한개의 대상만 선택 가능할수 있는 autoComplete 기능이 붙은 컴포넌트
@@ -191,7 +197,8 @@
        'issueAddTableConfigController' : 'app/issue/issueAddTableConfig.controller',   //  이슈 테이블 설정 컨트롤러
        'issueAddRelationTableConfigController' : 'app/issue/issueAddRelationTableConfig.controller',   //  이슈 테이블 설정 컨트롤러
        'issueAddDownTableConfigController' : 'app/issue/issueAddDownTableConfig.controller',   //  이슈 테이블 설정 컨트롤러
        'issueSendMailPartnersController' : 'app/issue/issueSendMailPartners.controller',   //  이슈 이메일 발송 컨트롤러
        'issueSendMailPartnersController' : 'app/issue/issueSendMailPartners.controller',   //  업체 이메일 발송 컨트롤러
        'issueCommonSendMailController' : 'app/issue/issueCommonSendMail.controller',   //  일반 이메일 발송 컨트롤러
        'issueVersionViewController' : 'app/issue/issueVersionView.controller', //  이슈 버전 확인 컨트롤러
        'issueReservationController' : 'app/issue/issueReservation.controller', //  이슈 발생 예약 컨트롤러
        'issueModifyUserController' : 'app/issue/issueModifyUser.controller', // 이슈 담당자 컨트롤러
@@ -409,10 +416,22 @@
        'jsTree' : {
            deps : ['app']
        },
        'jsRel' : {
            deps : ['app']
        },
        'jsDown' : {
            deps : ['app']
        },
        'tableColumnGenerator' : {
            deps : ['app']
        },
        'treeColumnGenerator' : {
            deps : ['app']
        },
        'relColumnGenerator' : {
            deps : ['app']
        },
        'downColumnGenerator' : {
            deps : ['app']
        },
        'ngStomp' : {
@@ -506,6 +525,7 @@
    'config',   //  angularJs 설정 - route 이동 관련, 이동시 초기화 로직이 들어있다. - 직접 로드
    'constants',
    'commonController',
    'issueCommonSendMailController',
    'autoCompleteController',
    'userInviteController',
    'issueAddController',   //  이슈 만들기에서 사용
@@ -534,6 +554,8 @@
    'resourceProvider',
    'tableProvider',
    'treeProvider',
    'relProvider',
    'downProvider',
    'permissionService',
    'authService',
    'userInviteService',
src/main/webapp/views/issue/issueCommonSendMail.html
New file
@@ -0,0 +1,69 @@
<div class="formModal">
    <div class="modal-header faded smaller">
        <div class="modal-title">
            <strong translate="issue.CommonSendIssueMail">일반 메일 발송</strong>
        </div>
        <button aria-label="Close" class="close" type="button" ng-click="fn.cancel()">
            <span aria-hidden="true"> &times;</span>
        </button>
    </div>
    <div class="modal-body">
        <form role="form" name="issueSendForm">
            <button type="button" class="btn btn-secondary mr-3 float-right mb-1" ng-click="fn.addInput()">
                <span translate="common.add">추가</span>
            </button>
            <div class="form-group">
                <label class="issue-label mt-2"><span translate="common.toPerson">받는 사람</span>&nbsp;<code class="highlighter-rouge">*</code></label>
                <div class="input-group" ng-repeat="i in vm.form.inputs">
                    <input type="text"
                           name="email"
                           class="form-control mt-1"
                           kr-input
                           ng-model="vm.form.emails[$index]"
                           ng-pattern="/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/"
                           required
                           autocomplete="off">
                    <span class="select3-selection__email__remove" ng-click="fn.removeInput($index)">×</span>
                </div>
                <small translate="common.emailExplain">받는 사람의 이메일 형식을 입력하셔야 합니다.</small>
                <div ng-if="issueSendForm.email.$error.pattern" class="help-block form-text text-danger"
                     translate="common.invalidEmailFormat">이메일 형식이 맞지 않습니다.
                </div>
            </div>
            <label class="issue-label"><span translate="common.title">제목</span></label>
            <input id="title"
                   type="text"
                   name="title"
                   class="form-control mt-1"
                   kr-input
                   ng-model="vm.form.title"
                   autocomplete="off">
            <div class="form-group mb10 mt-10">
                <label class="issue-label"><span translate="common.content">내용</span></label>
                <summernote
                        class="summernote mt-1"
                        lang="ko-KR"
                        summer-note-auto-focus
                        ng-model="vm.form.description"
                        data-editor="vm.summerNote.editor"
                        data-editable="vm.summerNote.editable"
                        on-image-upload="fn.imageUpload(files)"
                        target=".note-editable"></summernote>
            </div>
            <small class="mt-1" translate="common.sendToPerson">다른 사용자에게 메일을 보냅니다.</small>
        </form>
    </div>
    <div class="modal-footer buttons-on-right">
        <button type="button" class="btn btn-md btn-grey" ng-click="fn.cancel()"><span
                translate="common.cancel">취소</span></button>
        <button type="button" class="btn btn-md btn-primary bold"
                ng-disabled="fn.formCheck(issueSendForm.$invalid)"
                ng-click="fn.formSubmit()"><span translate="issue.sendMail">이메일 발송</span>
        </button>
    </div>
</div>
src/main/webapp/views/issue/issueDetail.html
@@ -102,7 +102,8 @@
                </div>
            </span>
            <a class="show-ticket-info cursor">
                <i class="os-icon os-icon-email-forward mr-20" ng-if="vm.viewer.modifyPermissionCheck" ng-click="fn.sendMailAll()" data-toggle="tooltip" data-placement="right" title="다른 사용자에게 이슈 보내기"></i>
                <i class="os-icon os-icon-email-2-at2 mr-20" ng-if="vm.viewer.modifyPermissionCheck" ng-click="fn.sendCommonMail()" data-toggle="tooltip" data-placement="right" title="직접 작성한 메일 보내기"></i>
                <i class="os-icon os-icon-email-forward mr-20" ng-if="vm.viewer.modifyPermissionCheck" ng-click="fn.sendMailAll()" data-toggle="tooltip" data-placement="right" title="업체 메일 보내기"></i>
                <i class="os-icon os-icon-airplay mr-20" ng-click="fn.versionView(vm.viewer.id)" data-toggle="tooltip" data-placement="right" title="이슈 변경 이력 상세 보기"></i>
                <i class="os-icon os-icon-calendar-time mr-20" ng-if="vm.viewer.modifyPermissionCheck"
                   ng-click="fn.reservation(vm.viewer.id)" data-toggle="tooltip" data-placement="right" title="이슈 발생 예약 하기"></i>
@@ -524,8 +525,8 @@
                <!--  연관 이슈 테이블 -->
                <div class="mt-10 issue-detail-word-break width-100">
                    <js-table data="vm.viewer.issueRelationVos" table-configs="vm.relTableConfigs"
                              event="relTableEvent" detail-view="true" hide-header="false" use-sort="true"></js-table>
                    <js-rel data="vm.viewer.issueRelationVos" table-configs="vm.relTableConfigs"
                              event="relTableEvent" detail-view="true" hide-header="false" use-sort="true"></js-rel>
                    <div class="row" ng-if="vm.viewer.modifyPermissionCheck">
                        <div class="col-sm-4">
@@ -625,8 +626,8 @@
                <!-- 하위 이슈 테이블 -->
                <div class="mt-10 issue-detail-word-break width-100">
                    <js-table data="vm.viewer.issueDownVos" table-configs="vm.downTableConfigs"
                              event="downTableEvent" detail-view="true" hide-header="false" use-sort="true"></js-table>
                    <js-down data="vm.viewer.issueDownVos" table-configs="vm.downTableConfigs"
                              event="downTableEvent" detail-view="true" hide-header="false" use-sort="true"></js-down>
                    <div class="row" ng-if="vm.viewer.modifyPermissionCheck">
                        <div class="col-sm-6">
src/main/webapp/views/issue/issueSendMailPartners.html
@@ -1,7 +1,7 @@
<div class="formModal">
    <div class="modal-header faded smaller">
        <div class="modal-title">
            <strong translate="issue.selectSendIssueMail">이슈 메일 발송 대상자 선택</strong>
            <strong translate="issue.selectSendIssueMail">이슈 메일 발송</strong>
        </div>
        <button aria-label="Close" class="close" type="button" ng-click="fn.cancel()">
            <span aria-hidden="true"> &times;</span>
@@ -20,6 +20,9 @@
                    </span>
                </div>
                <label class="issue-label mt-10">
                    <span translate="issue.selectPartners">업체 선택</span>&nbsp;<code class="highlighter-rouge">*</code>
                </label>
                <js-input-autocomplete data-input-name="mailUsers"
                                       owl-auto-focus
                                       target=".auto-complete-i0nput"
@@ -29,38 +32,50 @@
                                       page="vm.autoCompletePage.user.page"
                                       total-page="vm.autoCompletePage.user.totalPage"
                                       source="fn.getMailTargetAll(vm.form.mailUsers)"
                                       translation-texts="{ empty : 'common.emptyUser'}"
                                       translation-texts="{ empty : 'common.emptyCompanyPartners'}"
                                       input-disabled="vm.form.mailUsers == null"
                                       extra-settings="{ displayProp : 'name' , idProp : 'id', imageable : false, imagePathProp : 'profile',
                                       type : 'partner', maxlength : 100, autoResize : true, stopRemoveBodyEvent : true }"></js-input-autocomplete>
                    <div class="Template-area mt-20">
                        <div class="form-group mb10">
                            <label for="emailTemplateForm" class="issue-label">
                                <span translate="common.emailTemplate">이메일 템플릿</span>
                            </label>
                            <select id="emailTemplateForm"
                                    name="emailTemplate"
                                    class="form-control input-sm issue-select-label"
                                    ng-model="vm.emailTemplateType"
                                    ng-change="fn.onChangeEmailTemplate()"
                                    required>
                                <option ng-repeat="emailTemplate in vm.emailTemplates"
                                        value="{{emailTemplate.templateType}}"
                                        translate="{{emailTemplate.title}}">
                                </option>
                            </select>
                        </div>
                    </div>
                <summernote
                        class="summernote"
                        lang="ko-KR"
                        config="vm.options"
                        ng-model="vm.html"
                        target=".note-editable"></summernote>
                <div class="form-group mb10 mt-20">
                    <label for="emailTemplateForm" class="issue-label">
                        <span translate="issue.partners">업체 이메일</span>&nbsp;<code class="highlighter-rouge">*</code>
                    </label>
                    <select id="emailTemplateForm"
                            name="emailTemplate"
                            class="form-control input-sm issue-select-label"
                            ng-model="vm.emailTemplateType"
                            ng-change="fn.onChangeEmailTemplate()"
                            required>
                        <option value="" ng-selected="true">선택하세요</option>
                        <option ng-repeat="emailTemplate in vm.emailTemplates"
                                value="{{emailTemplate.templateType}}"
                                translate="{{emailTemplate.title}}">
                        </option>
                    </select>
                </div>
                <label class="issue-label"><span translate="common.title">제목</span></label>
                <input id="title"
                       type="text"
                       name="title"
                       class="form-control"
                       kr-input
                       ng-model="vm.emailTitle"
                       autocomplete="off">
                <div class="form-group mb10 mt-10">
                    <label class="issue-label"><span translate="common.content">내용</span></label>
                    <summernote
                            class="summernote"
                            lang="ko-KR"
                            config="vm.options"
                            ng-model="vm.html"
                            target=".note-editable"></summernote>
                </div>
            </div>
        </form>
    </div>
    <div class="modal-footer buttons-on-right">
        <button type="button" class="btn btn-md btn-grey" ng-click="fn.cancel()"><span