OWL ITS + 탐지시스템(인터넷 진흥원)
이민희
2021-12-01 5fb1952ae91f1a739803247266e87dbd15ea1f27
충돌 수정
3개 파일 추가됨
40개 파일 변경됨
1253 ■■■■ 파일 변경됨
src/main/java/kr/wisestone/owl/config/SecurityConfiguration.java 4 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/constant/MsgConstants.java 5 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/domain/Issue.java 17 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/mapper/IssueMapper.java 3 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/ApiTokenService.java 8 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/AttachedFileService.java 2 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/CustomFieldApiOverlapService.java 2 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/IssueApiDefaultService.java 1 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/IssueCustomFieldValueService.java 3 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/IssueDepartmentService.java 3 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/IssueHistoryService.java 4 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/IssueService.java 7 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/WorkspaceService.java 2 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/ApiTokenServiceImpl.java 33 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/AttachedFileServiceImpl.java 29 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/CustomFieldApiOverlapServiceImpl.java 8 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/IssueApiDefaultServiceImpl.java 19 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/IssueCustomFieldValueServiceImpl.java 23 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/IssueDepartmentServiceImpl.java 17 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/IssueHistoryServiceImpl.java 32 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/IssueServiceImpl.java 161 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/service/impl/WorkspaceServiceImpl.java 28 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/util/CommonUtil.java 54 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/controller/ApiController.java 40 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/controller/IssueController.java 2 ●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/form/CustomFieldValueForm.java 24 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/form/IssueApiForm.java 195 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/form/IssueCustomFieldValueForm.java 24 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/form/IssueForm.java 23 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/web/resolver/OwlResponseEntityExceptionHandler.java 2 ●●● 패치 | 보기 | raw | blame | 히스토리
src/main/resources/migration/V1_11__Alter_Table.sql 3 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/resources/mybatis/query-template/issue-template.xml 17 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/issue/issueDetail.controller.js 57 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/issue/issueList.controller.js 36 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/app/issue/issueSendMail.controller.js 82 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/components/issue/issue.service.js 6 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/components/utils/autoComplete.controller.js 43 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/components/utils/issueDetailImagePreview.directive.js 2 ●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/components/utils/issueSearchFieldKeyViewElement.directive.js 11 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/scripts/config.js 32 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/issue/issueDetail.html 19 ●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/issue/issueListNormal.html 49 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/webapp/views/issue/issueSendMail.html 121 ●●●●● 패치 | 보기 | raw | blame | 히스토리
src/main/java/kr/wisestone/owl/config/SecurityConfiguration.java
@@ -22,6 +22,7 @@
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.session.ConcurrentSessionFilter;
import org.springframework.session.security.web.authentication.SpringSessionRememberMeServices;
@@ -143,7 +144,10 @@
                .antMatchers("/guide/detail").permitAll()
                .antMatchers("/language/change").permitAll()
                .antMatchers("/security/*").permitAll()
                .antMatchers("/api/issue").permitAll()
                .antMatchers("/**/*").authenticated();
//        http.addFilter(new CustomAuthenticationFilter());
//        http.addFilterBefore(new CustomAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        http.rememberMe()
src/main/java/kr/wisestone/owl/constant/MsgConstants.java
@@ -150,6 +150,7 @@
    public static final String USER_NOT_EQUAL_PASSWORD = "USER_NOT_EQUAL_PASSWORD"; //  비밀번호가 맞지 않습니다.
    public static final String USER_NOT_EXIST = "USER_NOT_EXIST";   //  사용자가 존재하지 않습니다.
    public static final String USER_NOT_AUTHORIZED = "USER_NOT_AUTHORIZED"; //  사용자 인증 권한이 없습니다.
    public static final String ERROR_TOKEN = "USER_NOT_AUTHORIZED_TOKEN"; //  유효하지 않은 토큰입니다.
    public static final String USER_EXPIRED_PASSWORD = "USER_EXPIRED_PASSWORD"; //  비밀번호가 만료되었습니다.
    public static final String USER_RETURN_PASSWORD_NOT_PROVIDER_SOCIAL_JOIN_USER = "USER_RETURN_PASSWORD_NOT_PROVIDER_SOCIAL_JOIN_USER";   //  비밀번호 찾기 기능을 소셜 계정 가입 사용자는 사용할 수 없습니다.
    public static final String USER_NOT_USE_ACTIVE_STATUS = "USER_NOT_USE_ACTIVE_STATUS";   //  사용자는 활성 상태가 아니면 로그인할 수 없습니다.
@@ -227,4 +228,8 @@
    public static final String ISP_NOT_EXIST = "ISP_NOT_EXIST";   // ISP가 존재하지 않습니다.
    public static final String ISP_REMOVE_NOT_SELECT = "ISP_REMOVE_NOT_SELECT";   // 삭제할 ISP가 선택되지 않았습니다.
    public static final String PROJECT_NOT_INCLUDE_DEPARTMENT = "PROJECT_NOT_INCLUDE_DEPARTMENT";   // 선택한 부서 중 프로젝트에 참여하고 있지 않은 부서가 있습니다.
    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_USER_ERROR = "API_USER_ERROR";     // api 사용자 오류
}
src/main/java/kr/wisestone/owl/domain/Issue.java
@@ -1,5 +1,8 @@
package kr.wisestone.owl.domain;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
@@ -9,6 +12,8 @@
 * Created by wisestone on 2018-01-03.
 */
@Entity
@DynamicInsert
@DynamicUpdate
public class Issue extends BaseEntity implements Serializable {
    private static final long serialVersionUID = 1L;
    public static final String WORKSPACE_MANAGER = "WORKSPACE_MANAGER"; //  업무 공간 관리자
@@ -16,6 +21,9 @@
    public static final String REGISTER = "REGISTER";   //  이슈 등록자
    public static final String ASSIGNEE = "ASSIGNEE";   //  이슈 담당자
    public static final String DEPARTMENT = "DEPARTMENT";   //  이슈 담당부서
    public static final String IS_API_YES = "Y";
    public static final String IS_API_NO = "N";
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -26,6 +34,7 @@
    private Long issueNumber;
    private String startDate;
    private String completeDate;
    private String isApi;
    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "project_id")
@@ -330,4 +339,12 @@
    public void setParentIssue(Issue parentIssue) {
        this.parentIssue = parentIssue;
    }
    public String isApi() {
        return isApi;
    }
    public void setApi(String api) {
        isApi = api;
    }
}
src/main/java/kr/wisestone/owl/mapper/IssueMapper.java
@@ -1,6 +1,7 @@
package kr.wisestone.owl.mapper;
import kr.wisestone.owl.web.condition.IssueCondition;
import kr.wisestone.owl.web.condition.IssueCustomFieldValueCondition;
import kr.wisestone.owl.web.form.IssueForm;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@@ -15,6 +16,8 @@
public interface IssueMapper {
    List<Map<String, Object>> find(IssueCondition issueCondition);
    List<Map<String, Object>> findByCustomFieldValue(IssueCustomFieldValueCondition issueCustomFieldValueCondition);
    Long count(IssueCondition issueCondition);
    void insertBatch(@Param("issueForms") List<IssueForm> issueForms);
src/main/java/kr/wisestone/owl/service/ApiTokenService.java
@@ -1,12 +1,10 @@
package kr.wisestone.owl.service;
import kr.wisestone.owl.domain.ApiToken;
import kr.wisestone.owl.domain.CompanyField;
import kr.wisestone.owl.domain.CustomField;
import kr.wisestone.owl.domain.Event;
import kr.wisestone.owl.domain.*;
import kr.wisestone.owl.vo.ApiTokenVo;
import kr.wisestone.owl.vo.CompanyFieldVo;
import kr.wisestone.owl.vo.EventVo;
import kr.wisestone.owl.vo.UserVo;
import kr.wisestone.owl.web.condition.ApiTokenCondition;
import kr.wisestone.owl.web.condition.CompanyFieldCondition;
import kr.wisestone.owl.web.condition.EventCondition;
@@ -30,5 +28,7 @@
    ApiTokenVo find();
    UserVo certification(String token);
    void remove(ApiTokenForm apiTokenForm);
}
src/main/java/kr/wisestone/owl/service/AttachedFileService.java
@@ -19,6 +19,8 @@
    List<AttachedFile> addAttachedFile(List<MultipartFile> multipartFiles, Map<String, Object> content);
    List<AttachedFile> addAttachedFile(Workspace workspace, Issue issue, List<Map<String, Object>> files);
    List<AttachedFileVo> findAttachedFile(Map<String, Object> resJsonData, AttachedFileCondition condition);
    List<AttachedFile> findByIssueId(Long issueId);
src/main/java/kr/wisestone/owl/service/CustomFieldApiOverlapService.java
@@ -6,9 +6,11 @@
import kr.wisestone.owl.web.form.IssueApiDefaultForm;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Map;
public interface CustomFieldApiOverlapService extends AbstractService<CustomFieldApiOverlap, Long, JpaRepository<CustomFieldApiOverlap, Long>> {
    void find(Map<String, Object> resJsonData, CustomFieldApiOverlapForm form);
    List<CustomFieldApiOverlap> find(Long userId, Long issueTypeId);
    boolean modify(Map<String, Object> resJsonData, CustomFieldApiOverlapForm form);
}
src/main/java/kr/wisestone/owl/service/IssueApiDefaultService.java
@@ -9,5 +9,6 @@
public interface IssueApiDefaultService extends AbstractService<IssueApiDefault, Long, JpaRepository<IssueApiDefault, Long>> {
    IssueApiDefault find(Map<String, Object> resJsonData, IssueApiDefaultForm form);
    IssueApiDefault find(IssueApiDefaultForm form);
    boolean modify(Map<String, Object> resJsonData, IssueApiDefaultForm form);
}
src/main/java/kr/wisestone/owl/service/IssueCustomFieldValueService.java
@@ -6,6 +6,7 @@
import kr.wisestone.owl.domain.enumType.CustomFieldType;
import kr.wisestone.owl.vo.IssueCustomFieldValueVo;
import kr.wisestone.owl.web.condition.IssueCondition;
import kr.wisestone.owl.web.condition.IssueCustomFieldValueCondition;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
@@ -27,6 +28,8 @@
    boolean find(IssueCondition condition, Set<String> issueIds);
    Map<String, Object> find(IssueCustomFieldValueCondition issueCustomFieldValueCondition);
    List<Map<String, Object>> findInIssueIds(IssueCondition issueCondition);
    void removeIssueCustomFieldValuesByCustomFieldId(CustomField customField);
src/main/java/kr/wisestone/owl/service/IssueDepartmentService.java
@@ -2,6 +2,7 @@
import kr.wisestone.owl.domain.Issue;
import kr.wisestone.owl.domain.IssueDepartment;
import kr.wisestone.owl.domain.User;
import kr.wisestone.owl.domain.Workspace;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -13,6 +14,8 @@
    //담당부서
    void modifyIssueDepartment(Issue issue, Workspace workspace, List<Long> departmentIds);
    void modifyIssueDepartment(Issue issue, User user, Workspace workspace, List<Long> departmentIds);
    void insertIssueDepartment(List<Map<String, Long>> issueAssigneeMaps);
    void removeIssueDepartment(Long projectId, List<Long> excludeUserIds);
src/main/java/kr/wisestone/owl/service/IssueHistoryService.java
@@ -18,8 +18,12 @@
    void addIssueHistory(Issue issue, IssueHistoryType issueHistoryType, String issueChangeDescription);
    void addIssueHistory(Issue issue, User user, IssueHistoryType issueHistoryType, String issueChangeDescription);
    void makeDescription(StringBuilder description, IssueHistoryType issueHistoryType, String issueChangeDescription);
    void makeDescription(User user, StringBuilder description, IssueHistoryType issueHistoryType, String issueChangeDescription);
    void findIssueHistory(Map<String, Object> resJsonData, IssueHistoryCondition issueHistoryCondition);
    List<IssueHistoryVo> findIssueHistory(Long issueId);
src/main/java/kr/wisestone/owl/service/IssueService.java
@@ -2,10 +2,12 @@
import kr.wisestone.owl.domain.Issue;
import kr.wisestone.owl.domain.IssueType;
import kr.wisestone.owl.domain.User;
import kr.wisestone.owl.domain.Workflow;
import kr.wisestone.owl.vo.IssueVo;
import kr.wisestone.owl.web.condition.IssueCondition;
import kr.wisestone.owl.web.condition.ProjectCondition;
import kr.wisestone.owl.web.form.IssueApiForm;
import kr.wisestone.owl.web.form.IssueForm;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -22,6 +24,11 @@
    Issue addIssue(IssueForm issueForm, List<MultipartFile> files);
    Issue addIssue(User user, IssueForm issueForm, List<MultipartFile> multipartFiles);
    Issue addApiIssue(IssueApiForm issueApiForm);
    List<IssueVo> findIssue(Map<String, Object> resJsonData,
                            IssueCondition condition, Pageable pageable);
src/main/java/kr/wisestone/owl/service/WorkspaceService.java
@@ -42,6 +42,8 @@
    void checkUseWorkspace();
    Workspace checkUseWorkspace(User user, Long workspaceId);
    ModelAndView checkUseExcelDownload(Model model);
    boolean checkUseTraffic(Long fileSize);
src/main/java/kr/wisestone/owl/service/impl/ApiTokenServiceImpl.java
@@ -4,8 +4,10 @@
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import kr.wisestone.owl.constant.MsgConstants;
import kr.wisestone.owl.domain.ApiToken;
import kr.wisestone.owl.domain.User;
import kr.wisestone.owl.exception.OwlRuntimeException;
import kr.wisestone.owl.repository.ApiTokenRepository;
import kr.wisestone.owl.service.ApiTokenService;
import kr.wisestone.owl.util.ConvertUtil;
@@ -84,8 +86,19 @@
    }
    //JWT 복호화
    public UserVo getUser(String jwt) {
    private UserVo getUserVo(String jwt) {
        //결과값 = Claims
        Jws<Claims> claims = decryption(jwt);
        if (claims == null)
            return  null;
        ObjectMapper objectMapper = new ObjectMapper();
        //반환 타입은 LinkedHashMap 이다. 이를 User 타입으로 변환하기 위해 ObjectMapper 사용
        return objectMapper.convertValue(claims.getBody().get(DATA_KEY), UserVo.class);
    }
    private Jws<Claims> decryption(String jwt) {
        //결과값 = Claims
        Jws<Claims> claims = null;
@@ -98,11 +111,9 @@
        } catch (Exception e) {
            log.debug(e.getMessage(), e);
        }
        ObjectMapper objectMapper = new ObjectMapper();
        //반환 타입은 LinkedHashMap 이다. 이를 User 타입으로 변환하기 위해 ObjectMapper 사용
        return objectMapper.convertValue(claims.getBody().get(DATA_KEY), UserVo.class);
        return claims;
    }
    // 토큰 조회
    @Override
@@ -115,6 +126,18 @@
        return null;
    }
    // 토큰 사용자 인증
    @Override
    public UserVo certification(String token) {
         UserVo userVo = this.getUserVo(token);
         if (userVo != null){
             return userVo;
         } else  {
             throw new OwlRuntimeException(
                     this.messageAccessor.getMessage(MsgConstants.ERROR_TOKEN));
         }
    }
    // 토큰 삭제
    @Override
    public void remove(ApiTokenForm apiTokenForm) {
src/main/java/kr/wisestone/owl/service/impl/AttachedFileServiceImpl.java
@@ -119,6 +119,35 @@
        }
    }
    //  첨부 파일을 등록한다. - API 에서 사용
    @Override
    @Transactional
    public List<AttachedFile> addAttachedFile(Workspace workspace, Issue issue, List<Map<String, Object>> files) {
        if (workspace == null) {
            throw new OwlRuntimeException(
                    this.messageAccessor.getMessage(MsgConstants.WORKSPACE_NOT_EXIST));
        }
        if (issue == null) {
            throw new OwlRuntimeException(
                    this.messageAccessor.getMessage(MsgConstants.ISSUE_NOT_EXIST));
        }
        if (files != null && files.size() > 0) {
            List<Map<String, Object>> convertFileMaps = Lists.newArrayList();
            for (Map<String, Object> file : files) {
                String fileName = MapUtil.getString(file, "fileName");
                String fileStr = MapUtil.getString(file, "file");
                String contentType = MapUtil.getString(file, "contentType");
                convertFileMaps.add(CommonUtil.makeFileMap(fileName, fileStr, contentType));
            }
            return this.addAttachedFiles(workspace, convertFileMaps, issue, null, AttachedType.SUMMER);
        }
        return null;
    }
    //  첨부 파일을 등록한다. - 이슈 생성, 수정에서 사용
    @Override
    @Transactional
src/main/java/kr/wisestone/owl/service/impl/CustomFieldApiOverlapServiceImpl.java
@@ -41,10 +41,14 @@
    }
    @Override
    public List<CustomFieldApiOverlap> find(Long userId, Long issueTypeId) {
        return this.customFieldApiOverlapRepository.findByUserIdAndIssueTypeId(userId, issueTypeId);
    }
    @Override
    @Transactional
    public void find(Map<String, Object> resJsonData, CustomFieldApiOverlapForm form) {
        UserVo userVo = this.webAppUtil.getLoginUser();
        List<CustomFieldApiOverlap> customFieldApiOverlaps = this.customFieldApiOverlapRepository.findByUserIdAndIssueTypeId(userVo.getId(), form.getIssueTypeId());
        List<CustomFieldApiOverlap> customFieldApiOverlaps = this.find(form.getUserId(), form.getIssueTypeId());
        if (customFieldApiOverlaps != null && customFieldApiOverlaps.size() > 0) {
            List<CustomFieldApiOverlapVo> customFieldApiOverlapVos =  Lists.newArrayList();
src/main/java/kr/wisestone/owl/service/impl/IssueApiDefaultServiceImpl.java
@@ -46,10 +46,9 @@
    @Override
    @Transactional
    public IssueApiDefault find(Map<String, Object> resJsonData, IssueApiDefaultForm form) {
        UserVo userVo = this.webAppUtil.getLoginUser();
        List<IssueApiDefault> issueApiDefaults = this.issueApiDefaultRepository.findByUserIdAndIssueTypeId(userVo.getId(), form.getIssueTypeId());
        if (issueApiDefaults != null && issueApiDefaults.size() > 0) {
            IssueApiDefault issueApiDefault = issueApiDefaults.get(0);
        form.setUserId(this.webAppUtil.getLoginId());
        IssueApiDefault issueApiDefault = this.find(form);
        if (issueApiDefault != null) {
            IssueApiDefaultVo issueApiDefaultVo = ConvertUtil.copyProperties(issueApiDefault, IssueApiDefaultVo.class);
            Project project = issueApiDefault.getProject();
            if (project != null) {
@@ -67,7 +66,17 @@
            }
            resJsonData.put(Constants.RES_KEY_CONTENTS, issueApiDefaultVo);
            return issueApiDefault;
        }
        return issueApiDefault;
    }
    @Override
    public IssueApiDefault find(IssueApiDefaultForm form) {
        if (form.getUserId() != null && form.getIssueTypeId() != null) {
            List<IssueApiDefault> issueApiDefaults = this.issueApiDefaultRepository.findByUserIdAndIssueTypeId(form.getUserId(), form.getIssueTypeId());
            if (issueApiDefaults != null && issueApiDefaults.size() > 0) {
                return issueApiDefaults.get(0);
            }
        }
        return null;
    }
src/main/java/kr/wisestone/owl/service/impl/IssueCustomFieldValueServiceImpl.java
@@ -333,6 +333,29 @@
        return customFieldSearch;
    }
    @Override
    public Map<String, Object> find(IssueCustomFieldValueCondition issueCustomFieldValueCondition) {
        if (issueCustomFieldValueCondition.getUseValues().size() > 0 || !StringUtils.isEmpty(issueCustomFieldValueCondition.getUseValue())) {
            issueCustomFieldValueCondition.setWorkspaceId(this.userService.getUser(this.webAppUtil.getLoginId()).getLastWorkspaceId());
            //  사용자 정의 필드 값 검색 시작
            Map<String, Object> result = new HashMap<>();
            switch (CustomFieldType.valueOf(issueCustomFieldValueCondition.getCustomFieldType())) {
                case INPUT:
                    result = this.issueCustomFieldValueMapper.findLikeUseValue(issueCustomFieldValueCondition);
                    break;
                case MULTI_SELECT:
                case SINGLE_SELECT:
                    result = this.issueCustomFieldValueMapper.findByUseValue(issueCustomFieldValueCondition);
                    break;
            }
            return result;
        }
        return null;
    }
    //  이슈에서 저장한 사용자 정의 필드 값을 조회한다.
    @Override
    @Transactional(readOnly = true)
src/main/java/kr/wisestone/owl/service/impl/IssueDepartmentServiceImpl.java
@@ -1,10 +1,7 @@
package kr.wisestone.owl.service.impl;
import com.google.common.collect.Lists;
import kr.wisestone.owl.domain.Issue;
import kr.wisestone.owl.domain.IssueDepartment;
import kr.wisestone.owl.domain.IssueUser;
import kr.wisestone.owl.domain.Workspace;
import kr.wisestone.owl.domain.*;
import kr.wisestone.owl.mapper.IssueDepartmentMapper;
import kr.wisestone.owl.mapper.IssueUserMapper;
import kr.wisestone.owl.repository.IssueDepartmentRepository;
@@ -42,6 +39,14 @@
    @Override
    @Transactional
    public void modifyIssueDepartment(Issue issue, Workspace workspace, List<Long> departmentIds) {
        User user = this.webAppUtil.getLoginUserObject();
        modifyIssueDepartment(issue, user, workspace, departmentIds);
    }
    //  이슈 담당부서를 변경한다.
    @Override
    @Transactional
    public void modifyIssueDepartment(Issue issue, User user, Workspace workspace, List<Long> departmentIds) {
        List<Long> oldDepartmentIds = Lists.newArrayList();
        //  이전 담당 부서
@@ -69,8 +74,8 @@
                issueAssigneeMap.put("departmentId", departmentId); //담당부서
                issueAssigneeMap.put("issueId", issue.getId());
                issueAssigneeMap.put("workspaceId", workspace.getId());
                issueAssigneeMap.put("registerId", this.webAppUtil.getLoginId());
                issueAssigneeMap.put("modifyId", this.webAppUtil.getLoginId());
                issueAssigneeMap.put("registerId", user.getId());
                issueAssigneeMap.put("modifyId", user.getId());
                addIssueAssigneeMaps.add(issueAssigneeMap);
            }
src/main/java/kr/wisestone/owl/service/impl/IssueHistoryServiceImpl.java
@@ -63,13 +63,22 @@
    @Override
    @Transactional
    public void addIssueHistory(Issue issue, IssueHistoryType issueHistoryType, String issueChangeDescription) {
        User user = this.webAppUtil.getLoginUserObject();
        addIssueHistory(issue, user, issueHistoryType, issueChangeDescription);
    }
    //  이력 생성
    @Override
    @Transactional
    public void addIssueHistory(Issue issue, User user, IssueHistoryType issueHistoryType, String issueChangeDescription) {
        IssueHistory issueHistory = new IssueHistory();
        issueHistory.setIssue(issue);
        issueHistory.setProject(issue.getProject());
        issueHistory.setIssueHistoryType(issueHistoryType);
        StringBuilder description = new StringBuilder();
        //  이력 정보를 만들어 낸다.
        this.makeDescription(description, issueHistoryType, issueChangeDescription);
        this.makeDescription(user, description, issueHistoryType, issueChangeDescription);
        issueHistory.setDescription(description.toString());
        this.issueHistoryRepository.saveAndFlush(issueHistory);
@@ -81,6 +90,13 @@
    //  이력 정보를 만들어 낸다.
    @Override
    public void makeDescription(StringBuilder description, IssueHistoryType issueHistoryType, String issueChangeDescription) {
        User user = this.webAppUtil.getLoginUserObject();
        makeDescription(user, description, issueHistoryType, issueChangeDescription);
    }
    //  이력 정보를 만들어 낸다.
    @Override
    public void makeDescription(User user, StringBuilder description, IssueHistoryType issueHistoryType, String issueChangeDescription) {
        description.append("<div class=\"activity-text\">");
        //  생성, 수정, 삭제에 대해 기록을 남긴다.
@@ -90,9 +106,9 @@
                description.append("<span class='activity-timestamp'>");
                description.append(DateUtil.convertDateToStr(new Date()));
                description.append(" (");
                description.append(this.webAppUtil.getLoginUser().getName());
                description.append(user.getName());
                description.append(" - ");
                description.append(CommonUtil.decryptAES128(this.webAppUtil.getLoginUser().getAccount()));
                description.append(CommonUtil.decryptAES128(user.getAccount()));
                description.append(")");
                description.append("</span></h6>");
                break;
@@ -103,10 +119,10 @@
                description.append(DateUtil.convertDateToStr(new Date()));
                description.append(" (");
                if (this.webAppUtil.getLoginUser() != null) {
                    description.append(this.webAppUtil.getLoginUser().getName());
                if (user != null) {
                    description.append(user.getName());
                    description.append(" - ");
                    description.append(CommonUtil.decryptAES128(this.webAppUtil.getLoginUser().getAccount()));
                    description.append(CommonUtil.decryptAES128(user.getAccount()));
                }
                else {
                    description.append("OWL-ITS-SYSTEM");
@@ -124,9 +140,9 @@
                description.append("<span class=\"activity-timestamp\">");
                description.append(DateUtil.convertDateToStr(new Date()));
                description.append(" (");
                description.append(this.webAppUtil.getLoginUser().getName());
                description.append(user.getName());
                description.append(" - ");
                description.append(CommonUtil.decryptAES128(this.webAppUtil.getLoginUser().getAccount()));
                description.append(CommonUtil.decryptAES128(user.getAccount()));
                description.append(")");
                description.append("</span></h6>");
                break;
src/main/java/kr/wisestone/owl/service/impl/IssueServiceImpl.java
@@ -21,10 +21,10 @@
import kr.wisestone.owl.util.DateUtil;
import kr.wisestone.owl.vo.*;
import kr.wisestone.owl.web.condition.IssueCondition;
import kr.wisestone.owl.web.condition.IssueCustomFieldValueCondition;
import kr.wisestone.owl.web.condition.IssueTypeCustomFieldCondition;
import kr.wisestone.owl.web.condition.ProjectCondition;
import kr.wisestone.owl.web.form.IssueCommentForm;
import kr.wisestone.owl.web.form.IssueForm;
import kr.wisestone.owl.web.form.*;
import kr.wisestone.owl.web.view.ExcelView;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
@@ -71,6 +71,15 @@
    @Autowired
    private SeverityService severityService;
    @Autowired
    private CustomFieldApiOverlapService customFieldApiOverlapService;
    @Autowired
    private IssueApiDefaultService issueApiDefaultService;
    @Autowired
    private ApiTokenService apiTokenService;
    @Autowired
    private CompanyFieldService companyFieldService;
@@ -182,12 +191,109 @@
    }
    //  API 를 통해 이슈 추가.
    @Override
    @Transactional
    public Issue addApiIssue(IssueApiForm issueApiForm) {
        if (issueApiForm.getIssueTypeId() == null) {
            throw new OwlRuntimeException(this.messageAccessor.getMessage(MsgConstants.API_PARAMETER_ISSUE_TYPE_ERROR));
        }
        IssueForm issueForm = ConvertUtil.copyProperties(issueApiForm, IssueForm.class);
//        issueForm.setFiles(issueApiForm.getFiles());
        IssueType issueType = this.issueTypeService.getIssueType(issueApiForm.getIssueTypeId());
        if (issueType == null){
            throw new OwlRuntimeException(this.messageAccessor.getMessage(MsgConstants.API_PARAMETER_ISSUE_TYPE_ERROR));
        }
        // 프로젝트 입력
        Project project = issueType.getProject();
        if (project == null){
            throw new OwlRuntimeException(this.messageAccessor.getMessage(MsgConstants.API_PARAMETER_PROJECT_ERROR));
        }
        issueForm.setProjectId(project.getId());
        // 토큰으로 유저 정보 가져오기
        String token = issueApiForm.getToken();
        UserVo userVo = this.apiTokenService.certification(token);
        // 해당 유저 정보가 현재 db에 있는지 확인
        User user = this.userService.getUser(userVo.getId());
        if (user != null) {
            // 기본값 입력하기
            IssueApiDefaultForm issueApiDefaultForm = new IssueApiDefaultForm();
            issueApiDefaultForm.setUserId(user.getId());
            issueApiDefaultForm.setIssueTypeId(issueForm.getIssueTypeId());
            IssueApiDefault issueApiDefault = this.issueApiDefaultService.find(issueApiDefaultForm);
            if (issueApiDefault != null) {
                ConvertUtil.copyProperties(issueApiDefault, issueForm);
                issueForm.setPriorityId(issueApiDefault.getPriority().getId());
                issueForm.setSeverityId(issueApiDefault.getSeverity().getId());
            }
            // 중복 값 하위 이슈로 처리하기
            CustomFieldApiOverlapForm customFieldApiOverlapForm = new CustomFieldApiOverlapForm();
            customFieldApiOverlapForm.setUserId(user.getId());
            customFieldApiOverlapForm.setIssueTypeId(issueForm.getIssueTypeId());
            IssueVo issueVo = this.findIssue(issueApiForm, user.getId());
            if (issueVo != null) {
                issueForm.setParentIssueId(issueVo.getId());
            }
            issueForm.setIsApi(Issue.IS_API_YES);
            // 사용자 정의 필드 설정
            issueForm.setIssueCustomFields(issueApiForm.getCustomFieldValues());
            // api 입력값 적용
            ConvertUtil.copyProperties(issueApiForm, issueForm);
            return addIssue(user, issueForm, issueApiForm.getMultipartFiles());
        } else {
            throw new OwlRuntimeException(this.messageAccessor.getMessage(MsgConstants.API_USER_ERROR));
        }
    }
    // 중복된 상위 이슈 검색
    private IssueVo findIssue(IssueApiForm issueApiForm, Long userId) {
        IssueCustomFieldValueCondition issueCustomFieldValueCondition = new IssueCustomFieldValueCondition();
        List<CustomFieldApiOverlap> customFieldApiOverlaps = this.customFieldApiOverlapService.find(userId, issueApiForm.getIssueTypeId());
        if (customFieldApiOverlaps != null && customFieldApiOverlaps.size() > 0) {
            for (CustomFieldApiOverlap customFieldApiOverlap : customFieldApiOverlaps) {
                for (IssueCustomFieldValueForm issueCustomFieldValue : issueApiForm.getIssueCustomFieldValues()) {
                    if (customFieldApiOverlap.getCustomField().getId().equals(issueCustomFieldValue.getCustomFieldId())) {
                        issueCustomFieldValueCondition.addUseValues(issueCustomFieldValue.getUseValue());
                    }
                }
            }
            List<Map<String, Object>> results = this.issueMapper.findByCustomFieldValue(issueCustomFieldValueCondition);
            if (results != null && results.size() > 0) {
                IssueVo issueVo = new IssueVo();
                ConvertUtil.convertMapToObject(results.get(0), issueVo);
                return issueVo;
            }
        }
        return null;
    }
    //  이슈를 생성한다.
    @Override
    @Transactional
    public Issue addIssue(IssueForm issueForm, List<MultipartFile> multipartFiles) {
        User user = this.webAppUtil.getLoginUserObject();
        return addIssue(user, issueForm, multipartFiles);
    }
    //  이슈를 생성한다.
    @Override
    @Transactional
    public Issue addIssue(User user, IssueForm issueForm, List<MultipartFile> multipartFiles) {
        //  사용하고 있는 업무 공간이 활성 상태인지 확인한다. 사용 공간에서 로그인한 사용자가 비활성인지 확인한다.
        this.workspaceService.checkUseWorkspace();
        Workspace workspace = this.workspaceService.checkUseWorkspace(user, user.getLastWorkspaceId());
        //  프로젝트 유효성 체크
        Project project = this.projectService.getProject(issueForm.getProjectId());
        //  이슈 유형 유효성 체크
@@ -216,7 +322,7 @@
        issue.setIssueNumber(this.issueNumberGeneratorService.generateIssueNumber(project));    //  각 프로젝트의 고유 이슈 번호 생성
        this.issueRepository.saveAndFlush(issue);
        issue = this.issueRepository.saveAndFlush(issue);
        issue.setReverseIndex(issue.getId() * -1);  //  쿼리 속도 개선을 위해 리버스 인덱스 생성
        //  담당자 지정
@@ -230,32 +336,34 @@
        //  HOSTING 정보 저장
        this.issueHostingService.modifyIssueHostingField(issue, issueForm.getIssueHostingFields());
        //  첨부 파일 저장
        //  multipartFile 을 file Map List 객체로 변경한다.
        List<Map<String, Object>> convertFileMaps = this.convertMultipartFileToFile(multipartFiles);
        //  첨부 파일 저장
        this.attachedFileService.addAttachedFile(convertFileMaps, issue, this.webAppUtil.getLoginUser().getAccount());
        this.attachedFileService.addAttachedFile(convertFileMaps, issue, user.getAccount());
        //  텍스트 에디터에 첨부한 파일을 이슈와 연결
        this.checkNotHaveIssueIdAttachedFile(issue, issueForm);
        //  사용자 정의 필드 저장
        this.issueCustomFieldValueService.modifyIssueCustomFieldValue(issue, issueForm.getIssueCustomFields());
        //  이슈 이력 생성
        this.issueHistoryService.addIssueHistory(issue, IssueHistoryType.ADD, null);
        this.issueHistoryService.addIssueHistory(issue, user, IssueHistoryType.ADD, null);
        //  이슈 위험 관리 생성
        this.issueRiskService.addIssueRisk(issue, project.getWorkspace());
        //  영속성 컨텍스트 비우기
        this.clear();
        //  이슈 생성, 삭제시 예약 이메일에 등록해놓는다.
        this.reservationIssueEmail(issue.getId(), EmailType.ISSUE_ADD);
        this.reservationIssueEmail(issue, EmailType.ISSUE_ADD);
        //  사용자 시스템 기능 사용 정보 수집
        log.info(ElasticSearchUtil.makeUserActiveHistoryMessage(this.webAppUtil.getLoginUser(), ElasticSearchConstants.ISSUE_ADD));
        UserVo userVo = ConvertUtil.copyProperties(user, UserVo.class);
        log.info(ElasticSearchUtil.makeUserActiveHistoryMessage(userVo, ElasticSearchConstants.ISSUE_ADD));
        return issue;
    }
    //  이슈 생성, 삭제시 예약 이메일에 등록해놓는다.
    private void reservationIssueEmail(Long id, EmailType emailType) {
        Issue issue = this.getIssue(id);
    private void reservationIssueEmail(Issue issue, EmailType emailType) {
        Map<String, Object> issueMap = new HashMap<>();
        //  이슈 정보를 이메일 전송에 사용하기 위해 Map 형태로 변환한다.
        this.makeIssueMapToIssue(issue, issueMap);
@@ -815,6 +923,7 @@
                case "02": //  프로젝트, 이슈 유형, 이슈 상태,  우선순위, 중요도, 담당자, 첨부파일, 사용자 정의 필드 정보, 댓글, 기록을 셋팅한다.
                    this.setIssueDetail(issueVo, issue);    //  이슈 상세 정보를 셋팅한다.
                    issueVo.setProjectVo(ConvertUtil.copyProperties(issue.getProject(), ProjectVo.class));
                    break;
            }
        }
@@ -1125,12 +1234,14 @@
    private List<Map<String, Object>> convertMultipartFileToFile(List<MultipartFile> multipartFiles) {
        List<Map<String, Object>> convertFileMaps = Lists.newArrayList();
        for (MultipartFile multipartFile : multipartFiles) {
            try {
                Map<String, Object> fileMap = CommonUtil.makeFileMap(multipartFile);
                convertFileMaps.add(fileMap);
            } catch (Exception e) {
                log.debug("multipartFile -> file 변환 오류" + e.getMessage());
        if (multipartFiles != null && multipartFiles.size() > 0) {
            for (MultipartFile multipartFile : multipartFiles) {
                try {
                    Map<String, Object> fileMap = CommonUtil.makeFileMap(multipartFile);
                    convertFileMaps.add(fileMap);
                } catch (Exception e) {
                    log.debug("multipartFile -> file 변환 오류" + e.getMessage());
                }
            }
        }
@@ -1484,7 +1595,7 @@
        }
        //  이슈 생성, 삭제시 예약 이메일에 등록해놓는다.
        this.reservationIssueEmail(issue.getId(), EmailType.ISSUE_REMOVE);
        this.reservationIssueEmail(issue, EmailType.ISSUE_REMOVE);
        //  이슈 삭제
        this.issueRepository.delete(issue);
@@ -2521,7 +2632,6 @@
    public void reservationIssue() {
        List<IssueReservation> issueReservations = this.issueReservationService.findByIssueReservationTypeNotNull();
        Calendar calendar = Calendar.getInstance();
        int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
        int month = calendar.get(Calendar.MONTH) + 1;
@@ -2685,15 +2795,10 @@
    @Override
    public void modifyParentIssue(IssueForm issueDownForm) {
        Issue issue = this.getIssue(issueDownForm.getId()); //하위 이슈
        Long newParentIssueId = issueDownForm.getParentIssueId(); //변경할 하위이슈의 상위이슈
        StringBuilder sb = new StringBuilder();
        Issue parentIssue = issue.getParentIssue(); //변경 전 하위이슈의 상위이슈
        if(parentIssue != null){ //변경 전 하위이슈의 상위이슈가 존재 할 경우
            this.issueHistoryService.detectDownIssues(IssueHistoryType.DELETE, issue, sb);
            this.issueHistoryService.addIssueHistory(parentIssue, IssueHistoryType.MODIFY, sb.toString());
        }
        Long newParentIssueId = issueDownForm.getParentIssueId(); //변경할 하위이슈의 상위이슈
        StringBuilder sb = new StringBuilder();
        if (newParentIssueId != null) { // 추가 할 경우
            parentIssue = this.getIssue(newParentIssueId); //상위이슈(myIssue)
src/main/java/kr/wisestone/owl/service/impl/WorkspaceServiceImpl.java
@@ -553,25 +553,47 @@
    @Override
    @Transactional
    public void checkUseWorkspace() {
        Workspace workspace = this.getWorkspace(this.userService.getUser(this.webAppUtil.getLoginId()).getLastWorkspaceId());
        User user = this.webAppUtil.getLoginUserObject();
        Workspace workspace = this.getWorkspace(user.getLastWorkspaceId());
        this.checkUseWorkspace(user, workspace.getId());
    }
    //  사용하고 있는 업무 공간이 활성 상태인지 확인한다. 사용 공간에서 로그인한 사용자가 비활성인지 확인한다.
    @Override
    @Transactional
    public Workspace checkUseWorkspace(User user, Long workspaceId) {
        if (user == null) {
            throw new OwlRuntimeException(
                    this.messageAccessor.getMessage(MsgConstants.USER_NOT_EXIST));
        }
        if (workspaceId == null) {
            throw new OwlRuntimeException(
                    this.messageAccessor.getMessage(MsgConstants.WORKSPACE_NOT_EXIST));
        }
        Workspace workspace = this.getWorkspace(workspaceId);
        if (workspace.getServiceType().equals(ServiceType.UNUSED)) {
            throw new OwlRuntimeException(
                    this.messageAccessor.getMessage(MsgConstants.WORKSPACE_USE_PERIOD_EXCESS));
        }
        UserWorkspace userWorkspace = this.userWorkspaceService.findByUserIdAndWorkspaceId(this.webAppUtil.getLoginId(), workspace.getId());
        UserWorkspace userWorkspace = this.userWorkspaceService.findByUserIdAndWorkspaceId(user.getId(), workspace.getId());
        if (!userWorkspace.getUseYn()) {
            //  해당 사용자가 관리하는 업무 공간를 마지막 접근 업무 공간로 변경한다.
            this.userService.updateLastMyWorkspace(userWorkspace.getUser());
            //  비활성 사용자는 더이상 해당 업무 공간에 있으면 안된다.
            this.simpMessagingTemplate.convertAndSendToUser(this.webAppUtil.getLoginUser().getAccount(), "/notification/workspace-remove", this.messageAccessor.getMessage(MsgConstants.WORKSPACE_OUT, workspace.getName()));
            this.simpMessagingTemplate.convertAndSendToUser(user.getAccount(), "/notification/workspace-remove", this.messageAccessor.getMessage(MsgConstants.WORKSPACE_OUT, workspace.getName()));
            throw new OwlRuntimeException(
                    this.messageAccessor.getMessage(MsgConstants.WORKSPACE_INCLUDE_DISABLED));
        }
        return workspace;
    }
    //  사용 공간에서 로그인한 사용자가 비활성인지 확인하고 비활성일 경우 엑셀 다운로드를 금지한다.
src/main/java/kr/wisestone/owl/util/CommonUtil.java
@@ -344,6 +344,24 @@
        return fileMap;
    }
    //  string file 정보를 file Map 형태로 변경한다.
    public static Map<String, Object> makeFileMap(String fileName, String file, String contentType) {
        Map<String, Object> fileMap = new HashMap<>();
        try {
            byte[] bytes = Base64.decodeBase64(file);
            fileMap.put("fileName", fileName);
            fileMap.put("fileSize", bytes.length);
            fileMap.put("contentType", contentType);
            fileMap.put("file", CommonUtil.bytesToFile(fileName, bytes));
        } catch (Exception e) {
            LOGGER.debug(e.getMessage());
        }
        return fileMap;
    }
    public static String getPostDataString(Map<String, String> params) throws UnsupportedEncodingException {
        StringBuilder result = new StringBuilder();
        boolean first = true;
@@ -378,6 +396,42 @@
        return convertFile;
    }
    // string을 파일로 변환
    public static File stringToFile(String fileName, String file) {
        byte[] bytes = null;
        try {
            bytes = Base64.decodeBase64(file);
        } catch (Exception ex) {
            LOGGER.debug("string to bytes 변환 오류");
        }
        if (bytes != null) {
            return bytesToFile(fileName, bytes);
        }
        return  null;
    }
    // bytes를 파일로 변환
    public static File bytesToFile(String fileName, byte[] bytes) {
        File convertFile = new File(WebAppUtil.getContextRealPath() + TMP_UPLOAD_FOLDER + getFileNameByUUID(fileName));
        if (!convertFile.exists()) {
            convertFile.mkdirs();
        }
        try{
            FileOutputStream lFileOutputStream = new FileOutputStream(convertFile);
            lFileOutputStream.write(bytes);
            lFileOutputStream.close();
        }catch (IllegalStateException | IOException e) {
            LOGGER.debug("bytes 파일 file 변환 오류");
        }
        return convertFile;
    }
    public static InputStream getFileInputStream(String strPath, String strNewFileName){
        InputStream objInputStream = null;
src/main/java/kr/wisestone/owl/web/controller/ApiController.java
@@ -1,9 +1,16 @@
package kr.wisestone.owl.web.controller;
import kr.wisestone.owl.constant.Constants;
import kr.wisestone.owl.domain.Issue;
import kr.wisestone.owl.exception.OwlRuntimeException;
import kr.wisestone.owl.service.GuideService;
import kr.wisestone.owl.service.IssueService;
import kr.wisestone.owl.util.ConvertUtil;
import kr.wisestone.owl.util.MapUtil;
import kr.wisestone.owl.web.condition.GuideCondition;
import kr.wisestone.owl.web.form.GuideForm;
import kr.wisestone.owl.web.form.IssueApiForm;
import kr.wisestone.owl.web.form.IssueForm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
@@ -12,30 +19,49 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Stack;
@Controller
public class ApiController extends BaseController {
    @Autowired
    private GuideService guideService;
    private IssueService issueService;
    //  이슈 추가
    @RequestMapping(value = "api/issue", produces = MediaType.APPLICATION_JSON_VALUE)
    //  이슈 추가(json 방식으로 파일전송)
//    @RequestMapping(value = "api/issue", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
//    public
//    @ResponseBody
//    Map<String, Object> addIssue(@RequestBody Map<String, Map<String, Object>> params) {
//        Map<String, Object> resJsonData = new HashMap<>();
//
//        IssueApiForm issueForm = IssueApiForm.make(params.get(Constants.REQ_KEY_CONTENT));
//        Issue issue = this.issueService.addApiIssue(issueForm);
//        //  버전 생성
//        this.issueService.addIssueVersion(issue.getId());
//        return this.setSuccessMessage(resJsonData);
//    }
    @RequestMapping(value = "api/issue", method = RequestMethod.POST)
    public
    @ResponseBody
    Map<String, Object> add(@RequestBody Map<String, Map<String, Object>> params) {
    Map<String, Object> addIssue(MultipartHttpServletRequest request) throws OwlRuntimeException {
        Map<String, Object> resJsonData = new HashMap<>();
        // todo
        IssueApiForm issueForm = IssueApiForm.make(ConvertUtil.convertJsonToMap(request.getParameter(Constants.REQ_KEY_CONTENT)), request.getFiles("file"));
        Issue issue = this.issueService.addApiIssue(issueForm);
        //  버전 생성
        this.issueService.addIssueVersion(issue.getId());
        return this.setSuccessMessage(resJsonData);
    }
    //  이슈 조회
    @RequestMapping(value = "/api/issuelist", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @RequestMapping(value = "/api/issueList", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public
    @ResponseBody
    Map<String, Object> find(@RequestBody Map<String, Map<String, Object>> params) {
src/main/java/kr/wisestone/owl/web/controller/IssueController.java
@@ -172,7 +172,7 @@
        Map<String, Object> resJsonData = new HashMap<>();
        Pageable pageable = this.pageUtil.convertPageable(this.getPageVo(params));
        this.issueService.findMailTargetAll(resJsonData, IssueCondition.make(params.get(Constants.REQ_KEY_CONTENT)), pageable);
//        this.issueService.findMailTargetAll(resJsonData, IssueCondition.make(params.get(Constants.REQ_KEY_CONTENT)), pageable);
        return this.setSuccessMessage(resJsonData);
    }*/
src/main/java/kr/wisestone/owl/web/form/CustomFieldValueForm.java
New file
@@ -0,0 +1,24 @@
package kr.wisestone.owl.web.form;
public class CustomFieldValueForm {
    private Long customFieldId;
    private String value;
    public CustomFieldValueForm(){}
    public Long getCustomFieldId() {
        return customFieldId;
    }
    public void setCustomFieldId(Long customFieldId) {
        this.customFieldId = customFieldId;
    }
    public String getValue() {
        return value;
    }
    public void setValue(String value) {
        this.value = value;
    }
}
src/main/java/kr/wisestone/owl/web/form/IssueApiForm.java
New file
@@ -0,0 +1,195 @@
package kr.wisestone.owl.web.form;
import com.google.common.collect.Lists;
import kr.wisestone.owl.domain.IssueCustomFieldValue;
import kr.wisestone.owl.util.ConvertUtil;
import kr.wisestone.owl.util.MapUtil;
import kr.wisestone.owl.vo.CustomFieldVo;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
public class IssueApiForm {
    private String token;
    private String title;
    private String projectKey;
    private String description;
    private Long issueTypeId;
    private Long priorityId;
    private Long severityId;
    private Date startDate;
    private Date endDate;
    private Date searchTime;
    private List<DepartmentForm> departments = Lists.newArrayList();
    private List<IssueCustomFieldValueForm> issueCustomFieldValues = Lists.newArrayList();
    private List<Map<String, Object>> CustomFieldValues = Lists.newArrayList();
//    private List<Map<String, Object>> files = Lists.newArrayList();
    private List<MultipartFile> multipartFiles = Lists.newArrayList();
    public IssueApiForm() {
    }
    public static IssueApiForm make(Map<String, Object> content, List<MultipartFile> files) {
        IssueApiForm form = ConvertUtil.convertMapToClass(content, IssueApiForm.class);
        form.setMultipartFiles(files);
        //  사용자 필드 정보
        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() {
        return token;
    }
    public void setToken(String token) {
        this.token = token;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getProjectKey() {
        return projectKey;
    }
    public void setProjectKey(String projectKey) {
        this.projectKey = projectKey;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public Long getIssueTypeId() {
        return issueTypeId;
    }
    public void setIssueTypeId(Long issueTypeId) {
        this.issueTypeId = issueTypeId;
    }
    public Long getPriorityId() {
        return priorityId;
    }
    public void setPriorityId(Long priorityId) {
        this.priorityId = priorityId;
    }
    public Long getSeverityId() {
        return severityId;
    }
    public void setSeverityId(Long severityId) {
        this.severityId = severityId;
    }
    public List<DepartmentForm> getDepartments() {
        return departments;
    }
    public void setDepartments(List<DepartmentForm> departments) {
        this.departments = departments;
    }
    public Date getStartDate() {
        return startDate;
    }
    public void setStartDate(Date startDate) {
        this.startDate = startDate;
    }
    public Date getEndDate() {
        return endDate;
    }
    public void setEndDate(Date endDate) {
        this.endDate = endDate;
    }
    public Date getSearchTime() {
        return searchTime;
    }
    public void setSearchTime(Date searchTime) {
        this.searchTime = searchTime;
    }
    public List<IssueCustomFieldValueForm> getIssueCustomFieldValues() {
        return issueCustomFieldValues;
    }
    public void setIssueCustomFieldValues(List<IssueCustomFieldValueForm> issueCustomFieldValues) {
        this.issueCustomFieldValues = issueCustomFieldValues;
    }
    public void addIssueCustomFieldValue(IssueCustomFieldValueForm issueCustomFieldValueForm) {
        if (this.issueCustomFieldValues != null) {
            this.issueCustomFieldValues.add(issueCustomFieldValueForm);
        }
    }
    public void addCustomFieldValue(Map<String, Object> map) {
        if (this.CustomFieldValues != null) {
            this.CustomFieldValues.add(map);
        }
    }
    public List<Map<String, Object>> getCustomFieldValues() {
        return CustomFieldValues;
    }
    public void setCustomFieldValues(List<Map<String, Object>> customFieldValues) {
        CustomFieldValues = customFieldValues;
    }
//    public List<Map<String, Object>> getFiles() {
//        return files;
//    }
//
//    public void setFiles(List<Map<String, Object>> files) {
//        this.files = files;
//    }
    public List<MultipartFile> getMultipartFiles() {
        return multipartFiles;
    }
    public void setMultipartFiles(List<MultipartFile> multipartFiles) {
        this.multipartFiles = multipartFiles;
    }
}
src/main/java/kr/wisestone/owl/web/form/IssueCustomFieldValueForm.java
New file
@@ -0,0 +1,24 @@
package kr.wisestone.owl.web.form;
public class IssueCustomFieldValueForm {
    private Long customFieldId;
    private String useValue;
    public IssueCustomFieldValueForm(){}
    public Long getCustomFieldId() {
        return customFieldId;
    }
    public void setCustomFieldId(Long customFieldId) {
        this.customFieldId = customFieldId;
    }
    public String getUseValue() {
        return useValue;
    }
    public void setUseValue(String useValue) {
        this.useValue = useValue;
    }
}
src/main/java/kr/wisestone/owl/web/form/IssueForm.java
@@ -41,7 +41,9 @@
    private List<Map<String, Object>> issueCompanyFields = Lists.newArrayList();
    private List<Map<String, Object>> issueIspFields = Lists.newArrayList();
    private List<Map<String, Object>> issueHostingFields = Lists.newArrayList();
    private List<Map<String, Object>> files = Lists.newArrayList(); // api용 첨부파일
    private Long parentIssueId; // 상위 이슈
    private String isApi;
    public IssueForm() {
    }
@@ -116,6 +118,11 @@
        //  HOSTING 필드 정보
        if (MapUtil.getObject(params, "issueHostingFields") != null){
            form.setIssueHostingFields((List)MapUtil.getObject(params, "issueHostingFields"));
        }
        //  api 첨부파일
        if (MapUtil.getObject(params, "files") != null){
            form.setFiles((List)MapUtil.getObject(params, "files"));
        }
        return form;
    }
@@ -340,6 +347,22 @@
        this.parentIssueId = parentIssueId;
    }
    public String getIsApi() {
        return isApi;
    }
    public void setIsApi(String isApi) {
        this.isApi = isApi;
    }
    public List<Map<String, Object>> getFiles() {
        return files;
    }
    public void setFiles(List<Map<String, Object>> files) {
        this.files = files;
    }
    public String getTemplate() {
        return template;
    }
src/main/java/kr/wisestone/owl/web/resolver/OwlResponseEntityExceptionHandler.java
@@ -39,7 +39,7 @@
        Map<String, Object> resJsonData = new HashMap<String, Object>();
        resJsonData.put(Constants.RES_KEY_MESSAGE, this.messageAccessor.getResMessage(ex, Constants.RES_KEY_MSG_FAIL));
        return this.handleExceptionInternal(ex, resJsonData, new HttpHeaders(), HttpStatus.OK, request);
        return this.handleExceptionInternal(ex, resJsonData, new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
    }
    @ExceptionHandler({ StackOverflowError.class,
src/main/resources/migration/V1_11__Alter_Table.sql
@@ -209,3 +209,6 @@
-- issue_type 테이블 업체,ISP,호스팅 컬럼 추가
ALTER TABLE `issue_type` ADD COLUMN  `use_partner` BIGINT(11) NOT NULL DEFAULT '0';
-- api 이슈 여부
ALTER TABLE `issue` ADD COLUMN  `is_api` VARCHAR(1) NOT NULL DEFAULT 'N';
src/main/resources/mybatis/query-template/issue-template.xml
@@ -482,5 +482,22 @@
        issue WHERE issue_status_id = #{issueStatusId};
    </select>
    <!--    특정 사용자 정의 필드 값이 같은 이슈를 조회 -->
    <select id="findByCustomFieldValue" resultType="java.util.HashMap" parameterType="kr.wisestone.owl.web.condition.IssueCustomFieldValueCondition">
        SELECT
        id
        FROM issue
        LEFT OUTER JOIN issue_custom_field_value issue_custom FORCE INDEX(issueIdIndex) ON issue.id = issue_custom.issue_id
        WHERE 1=1
        AND issue.parent_issue_id IS NULL
        <choose>
            <when test="useValues.size != 0">
                AND issue_custom.use_value IN
                <foreach collection="useValues" item="item" index="index" separator="," open="(" close=")">
                    #{item}
                </foreach>
            </when>
        </choose>
    </select>
</mapper>
src/main/webapp/scripts/app/issue/issueDetail.controller.js
@@ -43,6 +43,7 @@
                $scope.fn.getDownTableConfigs = getDownTableConfigs;
                $scope.fn.containsPartner = containsPartner;
                $scope.fn.onActivate = onActivate;
                $scope.fn.makePartnersEmail = makePartnersEmail;
                //  이슈 목록 컨트롤러 vm, fn 상속 중
                $scope.vm.viewer = {};
@@ -72,7 +73,10 @@
                $scope.vm.form = {
                    issues : [], //연관 일감
                    issuesDown : []  // 하위 일감
                    issuesDown : [],  // 하위 일감
                    issueCompanyVos : [],
                    issueIspVos : [],
                    issueHostingVos : []
                };
@@ -636,16 +640,25 @@
                }
                //  이슈명을 클릭하면 이슈 상세 정보를 조회한다.
                $rootScope.$on("getIssueDetail", function (event, args) {
                    $scope.vm.viewer.id = args["id"];
                    $scope.fn.getIssueDetail();
                });
                // $rootScope.$on("getIssueDetail", function (event, args) {
                //     $scope.vm.viewer.id = args["id"];
                //     $scope.fn.getIssueDetail();
                // });
                //  이슈명을 클릭하면 이슈 상세 정보를 조회한다.
                $scope.$on("getIssueDetail", function (event, args) {
                    $scope.vm.viewer.id = args["id"];
                    $scope.fn.getIssueDetail();
                });
                // $scope.$on("getIssueDetail", function (event, args) {
                //     $scope.vm.viewer.id = args["id"];
                //     $scope.fn.getIssueDetail();
                // });
                $scope.$watch(function() {
                    return $rootScope.currentDetailIssueId;
                }, function() {
                    if ($rootScope.currentDetailIssueId != null) {
                        $scope.vm.viewer.id = $rootScope.currentDetailIssueId;
                        $scope.fn.getIssueDetail();
                    }
                }, true);
                //  초기화 해야할 할목을 지정하여 다른 이슈를 클릭할 때 초기화해준다.
                function initReload() {
@@ -653,9 +666,9 @@
                    $scope.vm.issueForm.issueStatusList = [];
                    $scope.vm.issueForm.issueStatusId = "";
                    $scope.vm.issueTypeId = $rootScope.getCurrentIssueTypeId();
                    /*if ($scope.$root.$$phase !== '$apply' && $scope.$root.$$phase !== '$digest') {
                        $scope.$apply();
                    }*/
                    // if ($scope.$root.$$phase !== '$apply' && $scope.$root.$$phase !== '$digest') {
                    //     $scope.$apply();
                    // }
                }
                //  이슈 상태 변경
@@ -804,7 +817,7 @@
                    $scope.fn.getDownTableConfigs();
                        Issue.detail($resourceProvider.getContent(
                        {id : $scope.vm.viewer.id, deep : "02", customFields : $scope.vm.customFields},
                        {id : $scope.vm.viewer.id, deep : "02"},
                        $resourceProvider.getPageContent(0, 1))).then(function (result) {
                        if (result.data.message.status === "success") {
@@ -846,6 +859,10 @@
                                }
                                $scope.vm.viewer.issueRelationVos = result.data.data.issueRelationVos;
                                $scope.vm.viewer.issueDownVos = result.data.data.issueDownVos;
                                if ($rootScope.workProject.id > -1) {
                                    $rootScope.changeLastProject(result.data.data.projectVo.id);
                                }
                            }
                        }
                        else {
@@ -1038,8 +1055,15 @@
                    });
                }
                // 업체/ISP/호스팅 배열값을 하나로 합쳐서 메일 클릭시 전달
                function makePartnersEmail() {
                    $scope.vm.form.partnersEmail = $scope.vm.viewer.issueCompanyVos.concat($scope.vm.viewer.issueIspVos, $scope.vm.viewer.issueHostingVos)
                    return $scope.vm.form.partnersEmail;
                }
                //  특정 사용자에게 이슈를 메일로 발송
                function sendMail(issueId, projectId) {
                    makePartnersEmail();
                    $uibModal.open({
                        templateUrl : 'views/issue/issueSendMail.html',
                        size : "md",
@@ -1049,11 +1073,12 @@
                            parameter : function () {
                                return {
                                    departmentVos : [$scope.vm.form.issues[0].departmentVos],
                                    issueCompanyFields : [$scope.vm.form.issues[0].issueCompanyVos],
                                    issueIspFields : [$scope.vm.form.issues[0].issueIspVos],
                                    issueHostingFields : [$scope.vm.form.issues[0].issueHostingVos],
                                    partners : $scope.vm.form.partnersEmail,
                                    issueId : issueId,
                                    projectId : projectId
                                    // issueCompanyFields : [$scope.vm.viewer.issueCompanyVos[0]],
                                    // issueIspFields : [$scope.vm.viewer.issueIspVos[0]],
                                    // issueHostingFields : [$scope.vm.viewer.issueHostingVos[0]],
                                };
                            }
                        }
src/main/webapp/scripts/app/issue/issueList.controller.js
@@ -169,10 +169,11 @@
                    //  현재 상세화면으로 보려고하는 이슈 id를 기억한다.
                    $rootScope.currentDetailIssueId = id;
                    //  이슈 상세 화면 요청
                    $rootScope.$broadcast("getIssueDetail", {
                        id : id
                    });
                    // $scope.$broadcast("getIssueDetail", {
                    //     id : id
                    // });
                }
                //  목록 화면으로 변경한다.
                function listView() {
@@ -183,6 +184,7 @@
                    $scope.vm.responseData.data = angular.copy(temp);
                    //  마지막으로 보고있던 이슈 id를 초기화한다.
                    $rootScope.currentDetailIssueId = null;
                    $scope.$broadcast("getIssueList", {id: $rootScope.currentDetailIssueId});
                }
                //  이슈 테이블 설정
@@ -508,6 +510,31 @@
                                if (detail) {
                                    changeDetailView(result.data.data[0].id);
                                }
                            }
                        }
                        else {
                            SweetAlert.error($filter("translate")("issue.failedIssueLookup"), result.data.message.message); // 이슈 조회 실패
                        }
                    });
                }
                // 이메일 프로젝트 경로로 이동 후 상세 진입시 조회
                function getDetailList(projectKey, number) {
                    var conditions = {
                        projectKey : projectKey,
                        combinationIssueNumber : number
                    };
                    Issue.find($resourceProvider.getContent(conditions,
                        $resourceProvider.getPageContent(0, 1))).then(function (result) {
                        if (result.data.message.status === "success") {
                            if (result.data.data !=  null && result.data.data.length > 0) {
                                $scope.vm.projectKey = result.data.data[0].projectKey;
                                $scope.vm.issueNumber = result.data.data[0].issueNumber;
                                $scope.vm.responseData = result.data;
                                changeDetailView(result.data.data[0].id);
                            }
                        }
                        else {
@@ -885,7 +912,8 @@
                    // 파라미터 읽기
                    var params = $rootScope.previousGetParams;
                    if ($rootScope.isDefined(params)) {
                        $rootScope.$broadcast("makeIssueSearch", {  projectKey : params.projectKey, issueNumber : params.issueNumber });
                        // $rootScope.$broadcast("makeIssueSearch", {  projectKey : params.projectKey, issueNumber : params.issueNumber });
                        getDetailList(params.projectKey, params.issueNumber);
                        $rootScope.previousGetParams = null;
                        // $rootScope.issueTypeId = $rootScope.issueTypeMenu.id;
                        return;
src/main/webapp/scripts/app/issue/issueSendMail.controller.js
@@ -13,49 +13,63 @@
                    removeManager : removeManager,  //  전송 대상자 삭제
                    cancel : cancel,    //  팝업 창 닫기
                    formSubmit : formSubmit,    //  폼 전송
                    formCheck : formCheck   //  폼 체크
                    // formCheck : formCheck  //  폼 체크
                };
                $scope.vm = {
                    form : {
                        id : parameter.issueId,  //  이슈 번호
                        projects : [{ id : parameter.projectId}],  //  프로젝트
                        users : []    //  메일 전송받는 사용자
                        partners : parameter.partners,
                        users : [],    //  메일 전송받는 사용자
                        issueCompanyVos : [],
                        issueIspVos : [],
                        issueHostingVos : []
                        // companyFields : parameter.issueCompanyFields,  //  업체 이메일
                        // ispFields : parameter.issueIspFields,  //  ISP 이메일
                        // hostingFields : parameter.issueHostingFields,  //  호스팅 이메일
                    },
                    userName : "",
                    partnerName : "",
                    autoCompletePage : {
                        user : {
                            page : 0,
                            totalPage : 0
                        },
                        partnersMail : {
                            page : 0,
                            totalPage :0
                        }
                    }
                };
                angular.extend(this, $controller('autoCompleteController', {$scope : $scope, $injector : $injector}));
                function formCheck(formInvalid) {
                    if (formInvalid) {
                        return true;
                    }
                    if ($scope.vm.form.users.length < 1) {
                        return true;
                    }
                    return false;
                }
                // function formCheck(formInvalid) {
                //     if (formInvalid) {
                //         return true;
                //     }
                //
                //     if ($scope.vm.form.users.length < 1) {
                //         return true;
                //     }
                //
                //     return false;
                // }
                //  폼 전송
                function formSubmit() {
                    $rootScope.spinner = true;
                    var content = {
                        id : $scope.vm.form.id,
                        sendEmails : (function () {
                            var sendEmails = [];
                            angular.forEach($scope.vm.form.users, function (user) {
                                sendEmails.push($rootScope.encryption(user.account));
                            angular.forEach($scope.vm.form.projects, function (project) {
                                sendEmails.push(project.id);
                            });
                            return sendEmails;
@@ -78,6 +92,42 @@
                    });
                }
                // function formSubmit() {
                //     $rootScope.spinner = true;
                //
                //     var content = {
                //         id : $scope.vm.form.id,
                //         companyFieldsEmail : $scope.vm.form.companyFieldsEmail,
                //         ispFieldsEmail : $scope.vm.form.ispFieldsEmail,
                //         hostingFieldsEmail : $scope.vm.form.hostingFieldsEmail,
                //         sendEmails : (function () {
                //             var sendEmails = [];
                //
                //             angular.forEach($scope.vm.form.projects, function (project) {
                //                 sendEmails.push(project.id);
                //             });
                //
                //             return sendEmails;
                //         })()
                //     };
                //
                //     Issue.findMailTargetAll($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;
                //     });
                // }
                //  사용자 auto complete callback function
                function getUserListCallBack(result) {
                    $scope.vm.autoCompletePage.user.totalPage = result.data.page.totalPage;
src/main/webapp/scripts/components/issue/issue.service.js
@@ -98,6 +98,12 @@
                    return response;
                });
            },
            findMailTargetAll : function (conditions) {
                return $http.post("issue/findMailTargetAll", conditions).then(function (response) {
                    $log.debug("이슈 이메일 발송 결과 : ", response);
                    return response;
                });
            }
        }
    }
    ])
src/main/webapp/scripts/components/utils/autoComplete.controller.js
@@ -40,6 +40,7 @@
                $scope.fn.getPartnerList = getPartnerList;          // 업체/isp/호스팅 목록 조회
                $scope.fn.getIssueTypeList = getIssueTypeList;          // 이슈 유형 목록 조회
                $scope.fn.getCustomFieldList = getCustomFieldList;          // 사용자 정의 필드 목록 조회
                $scope.fn.getMailTargetAll = getMailTargetAll;          // 사용자 정의 필드 목록 조회
                function getUserList(query, excludeList, page, callBack) {
                    var conditions = {
@@ -480,6 +481,48 @@
                    return deferred.promise;
                }
                function getMailTargetAll(query, excludeList, page, callBack) {
                    var conditions = {
                        id : query,
                        excludeIds : (function () {
                            var excludeIds = [];
                            angular.forEach(excludeList, function (exclude) {
                                excludeIds.push(exclude.id);
                            });
                            return excludeIds;
                        })(),
                        partnersEmailIds : (function () {
                            var partnersEmailIds = [];
                            angular.forEach($scope.vm.form.partners, function (partner) {
                                partnersEmailIds.push(partner.email);
                            });
                            return partnersEmailIds;
                        })(),
                    };
                    var deferred = $q.defer();
                    Issue.findMailTargetAll($resourceProvider.getContent(         //  페이징 업데이트가 필요한 컴포넌트 일경우, page 업데이트가 있을 경우 기본 10개씩 가져오고 아닐경우 25개씩 가져온다.
                        conditions, $resourceProvider.getPageContent($rootScope.isDefined(page) ? page : 0, $rootScope.isDefined(page) ? 10 : 25))).then(function (result) {
                        if (result.data.message.status === "success") {
                            if ($rootScope.isDefined(callBack)) {
                                callBack(result);
                            }
                            deferred.resolve(result.data.data);
                        }
                        else {
                            SweetAlert.swal($filter("translate")("issue.failedToIssueTypeListLookup"), result.data.message.message, "error"); // "이슈 유형 목록 조회 실패"
                        }
                    });
                    return deferred.promise;
                }
            }
        ]);
    }
src/main/webapp/scripts/components/utils/issueDetailImagePreview.directive.js
@@ -25,7 +25,7 @@
                        //  이미지 갤러리 만들기
                        function makeNgImageGallery() {
                            var makeTag = '<ng-image-gallery images="images" thumb-size="80" bubbles="true" bubble-size="50" img-bubbles="true"></ng-image-gallery>';
                            var makeTag = '<ng-image-gallery images="images" thumb-size="60" bubbles="true" bubble-size="50" img-bubbles="true"></ng-image-gallery>';
                            var linkFn = $compile(makeTag);
                            var content = linkFn($scope);
                            $element.append(content);
src/main/webapp/scripts/components/utils/issueSearchFieldKeyViewElement.directive.js
@@ -51,9 +51,14 @@
                                        break;
                                    }
                                }
                                makeTag += "<span ng-click='fn.remove(" + key.fieldKey + ")'>×</span>";
                                makeTag += "</p>";
                                if (target.fieldValue !== null) {
                                    makeTag += "<span></span>";
                                    makeTag += "</p>";
                                }
                                else {
                                    makeTag += "<span ng-click='fn.remove(" + key.fieldKey + ")'>×</span>";
                                    makeTag += "</p>";
                                }
                            });
                            var linkFn = $compile(makeTag);
src/main/webapp/scripts/config.js
@@ -318,23 +318,25 @@
                };
                $rootScope.changeLastProject = function (projectId, reload = true) {
                    if (User != null) {
                        User.updateLastProject($resourceProvider.getContent(
                            {lastProjectId: projectId},
                            $resourceProvider.getPageContent(0, 0))).then(function (result) {
                    if ($rootScope.workProject == null || $rootScope.workProject.id !== projectId ) {
                        if (User != null) {
                            User.updateLastProject($resourceProvider.getContent(
                                {lastProjectId: projectId},
                                $resourceProvider.getPageContent(0, 0))).then(function (result) {
                            if (result.data.message.status === "success") {
                                $rootScope.user = result.data.data;
                                $rootScope.projects.forEach(function (el) {
                                    if (el.id == projectId) {
                                        $rootScope.workProject = el;
                                if (result.data.message.status === "success") {
                                    $rootScope.user = result.data.data;
                                    $rootScope.projects.forEach(function (el) {
                                        if (el.id == projectId) {
                                            $rootScope.workProject = el;
                                        $rootScope.$broadcast("changeLastProject", { id : el.id });
                                    }
                                });
                                $state.go($state.current, {}, {reload: reload});
                            }
                        });
                                            $rootScope.$broadcast("changeLastProject", {id: el.id});
                                        }
                                    });
                                    $state.go($state.current, {}, {reload: reload});
                                }
                            });
                        }
                    }
                };
src/main/webapp/views/issue/issueDetail.html
@@ -192,6 +192,11 @@
                            <span class="issue-detail-label" style="position: relative; top: 1rem" ng-show="vm.viewer.issueCustomFields == ''">사용자 정의 필드값이 없습니다.</span>
                        </div>
                    </div>
                    <div class="col-md-2">
                        <issue-detail-image-preview images="vm.images"></issue-detail-image-preview>
                    </div>
                    <div ng-show="vm.images.length < 1" class="detail-not-elements width-100">
                    </div>
                </div>
                <!-- 사용자 정의 필드 -->
                <div class="row">
@@ -256,13 +261,9 @@
                            </div>
                        </div>
                    </div>
                    <div class="pdt0" style="position: relative; bottom: 1rem">
                        <issue-detail-image-preview images="vm.images"></issue-detail-image-preview>
                    </div>
                    <div ng-show="vm.images.length < 1" class="detail-not-elements width-100">
                        <span></span>
                    </div>
                </div>
                           <!-- <div class="">
                                <label class="issue-detail-label"><span>{{vm.viewer.issueCustomFieldValueVos[0].useValue}}</span> </label>
@@ -312,7 +313,7 @@
                        <div class="form-group">
                            <div class="">
                                <span translate="companyField.email">이메일</span>:
                                <span class="email_color">{{vm.viewer.issueCompanyVos[0].email}}</span>
                                <span class="email_color cursor" ng-click="fn.sendMail(vm.viewer.id, vm.viewer.projectVo.id)">{{vm.viewer.issueCompanyVos[0].email}}</span>
                            </div>
                        </div>
                    </div>
@@ -372,7 +373,7 @@
                        <div class="form-group">
                            <div class="">
                                <span translate="ispField.email">이메일</span>:
                                <span class="email_color">{{vm.viewer.issueIspVos[0].email}}</span>
                                <span class="email_color cursor" ng-click="fn.sendMail(vm.viewer.id, vm.viewer.projectVo.id)">{{vm.viewer.issueIspVos[0].email}}</span>
                            </div>
                        </div>
                    </div>
@@ -433,7 +434,7 @@
                        <div class="form-group">
                            <div class="">
                                <span translate="hostingField.email">이메일</span>:
                                <span class="email_color">{{vm.viewer.issueHostingVos[0].email}}</span>
                                <span class="email_color cursor" ng-click="fn.sendMail(vm.viewer.id, vm.viewer.projectVo.id)">{{vm.viewer.issueHostingVos[0].email}}</span>
                            </div>
                        </div>
                    </div>
src/main/webapp/views/issue/issueListNormal.html
@@ -38,9 +38,7 @@
                                            <!--    프로젝트    -->
                                            <issue-search-array-view-element lists="vm.projects"
                                                                             type="'project'"></issue-search-array-view-element>
                                            <p ng-if="$root.isDefined(vm.projectKey)">
                                                {{vm.projectKey}}
                                            </p>
                                            <!--    이슈 타입   -->
                                            <issue-search-field-key-view-element lists="vm.issueTypes"
                                                                                 keys="vm.search.issueTypeIds"></issue-search-field-key-view-element>
@@ -48,16 +46,6 @@
                                            <!--    이슈 상태   -->
                                            <issue-search-field-key-view-element lists="vm.issueStatuses"
                                                                                 keys="vm.search.issueStatusIds"></issue-search-field-key-view-element>
                                            <!--    이슈 번호   -->
                                            <p ng-if="$root.isDefined(vm.issueNumber)">
                                                {{vm.issueNumber}}
                                            </p>
                                            <p ng-if="$root.isDefined(vm.search.combinationIssueNumber)">
                                                {{vm.search.combinationIssueNumber}}
                                                <span ng-click="vm.search.combinationIssueNumber = ''">×</span>
                                            </p>
                                            <!--    이슈 내용   -->
                                            <p ng-if="$root.isDefined(vm.search.description)">
@@ -159,7 +147,9 @@
                                                                             options="::vm.issueStatuses"></ng-dropdown-multiselect>
                                                </div>
                                            </div>
                                        </div>
                                        <div class="row">
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="issue.issueNumber">이슈 번호</span></label>
@@ -169,13 +159,10 @@
                                                           autocomplete="off"
                                                           kr-input
                                                           maxlength="20"
                                                           ng-model="vm.issueNumber">
<!--                                                           ng-model="vm.search.combinationIssueNumber">-->
                                                           ng-model="vm.search.combinationIssueNumber">
                                                </div>
                                            </div>
                                        </div>
                                        <div class="row">
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="issue.issueContent">이슈 내용</span></label>
@@ -206,20 +193,6 @@
                                                                             data-input-name="severities"
                                                                             selected-model="vm.search.severityIds"
                                                                             options="::vm.severities"></ng-dropdown-multiselect>
                                                </div>
                                            </div>
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="common.assigneeTeam">담당부서</span></label>
                                                    <js-autocomplete-multi data-input-name="departments"
                                                                           selected-model="vm.departments"
                                                                           search="vm.departmentName"
                                                                           input-disabled="false"
                                                                           source="fn.getUserDepartmentList(vm.departmentName, vm.departments)"
                                                                           translation-texts="{ count : 'common.userNum', empty : 'common.emptyProjectDepartment' }"
                                                                           extra-settings="{ displayProp : 'byName' , idProp : 'id', widthable : false, width : '', imageable : true, imagePathProp : 'profile', type : 'department', maxlength : 100 }">
                                                    </js-autocomplete-multi>
                                                </div>
                                            </div>
                                        </div>
@@ -280,6 +253,20 @@
                                                </div>
                                            </div>
                                            <div class="col-lg-3">
                                                <div class="form-group">
                                                    <label> <span translate="common.assigneeTeam">담당부서</span></label>
                                                    <js-autocomplete-multi data-input-name="departments"
                                                                           selected-model="vm.departments"
                                                                           search="vm.departmentName"
                                                                           input-disabled="false"
                                                                           source="fn.getUserDepartmentList(vm.departmentName, vm.departments)"
                                                                           translation-texts="{ count : 'common.userNum', empty : 'common.emptyProjectDepartment' }"
                                                                           extra-settings="{ displayProp : 'byName' , idProp : 'id', widthable : false, width : '', imageable : true, imagePathProp : 'profile', type : 'department', maxlength : 100 }">
                                                    </js-autocomplete-multi>
                                                </div>
                                            </div>
                                            <div class="col-lg-3" ng-repeat="customField in vm.customFields">
                                                <label>{{::customField.name}}</label>
src/main/webapp/views/issue/issueSendMail.html
@@ -20,21 +20,97 @@
                                      ng-click="fn.removeManager($index)">×</span>
                            </span>
                </div>
                <js-input-autocomplete data-input-name="users"
                                       owl-auto-focus
                                       target=".auto-complete-input"
                                       selected-model="vm.form.users"
                                       search="vm.userName"
                                       page="vm.autoCompletePage.user.page"
                                       total-page="vm.autoCompletePage.user.totalPage"
                                       source="fn.getUserList(vm.userName, vm.form.users, vm.autoCompletePage.user.page, fn.getUserListCallBack)"
                                       translation-texts="{ empty : 'common.emptyUser'}"
                                       input-disabled="vm.form.projects.length == 0"
                                       extra-settings="{ displayProp : 'byName' , idProp : 'id', imageable : true, imagePathProp : 'profile',
                                               type : 'user', maxlength : 100, autoResize : true, stopRemoveBodyEvent : true }"></js-input-autocomplete>
<!--                <js-autocomplete-single data-input-name="issue"-->
<!--                                        selected-model="vm.form.issues"-->
<!--                                        search="vm.issueName"-->
<!--                                        source="fn.getIssueList(vm.issueName, vm.issueTypeId, vm.form.issues, vm.autoCompletePage.issue.page, fn.getIssueListCallBack)"-->
<!--                                        page="vm.autoCompletePage.issue.page"-->
<!--                                        total-page="vm.autoCompletePage.issue.totalPage"-->
<!--                                        input-disabled="false"-->
<!--                                        translation-texts="{ empty : 'common.emptyIssue' }"-->
<!--                                        extra-settings="{ displayProp : 'title' , idProp : 'id', imageable : false, imagePathProp : '',-->
<!--                                            type : '', maxlength : 200, autoResize : true, stopRemoveBodyEvent : true }"></js-autocomplete-single>-->
<!--                {{vm.form.companyFieldsEmail}}-->
                <label><span>협력사 메일</span></label>
                <js-autocomplete-multi data-input-name="partnersEmail"
                                       selected-model="vm.form.partnersEmail"
                                       search="vm.partnerName"
                                       source="fn.getMailTargetAll(vm.form.partnersEmail)"
                                       input-disabled="false"
                                       page="vm.autoCompletePage.partnersMail.page"
                                       total-page="vm.autoCompletePage.partnersMail.totalPage"
                                       modal-form-auto-scroll
                                       extra-settings="{ displayProp : 'byName' , idProp : 'email', imageable : false, maxlength : 100, autoResize : true }"></js-autocomplete-multi>
<!--                <ng-dropdown-multiselect class="multiSelect cursor"-->
<!--                                         data-input-name="companyFieldsEmail"-->
<!--                                         selected-model="vm.form.companyFieldsEmail.concat(vm.form.ispFieldsEmail,vm.form.hostingFieldsEmail)"-->
<!--                                         extra-settings="{ stringTypeOption : true }"-->
<!--                                         options="vm.options.companyFieldsEmail"></ng-dropdown-multiselect>-->
<!--                <span class="issue-detail-label" translate="companyField.info"></span>-->
<!--                <input ng-if="vm.form.companyFieldsEmail != null"-->
<!--                       type="text"-->
<!--                       class="form-control"-->
<!--                       kr-input-->
<!--                       autocomplete="off"-->
<!--                       ng-model="vm.form.companyFieldsEmail">-->
<!--                <span class="issue-detail-label" translate="ispField.info"></span>-->
<!--                <input ng-if="vm.form.ispFieldsEmail != null"-->
<!--                       type="text"-->
<!--                       class="form-control"-->
<!--                       kr-input-->
<!--                       autocomplete="off"-->
<!--                       ng-model="vm.form.ispFieldsEmail">-->
<!--                <span class="issue-detail-label" translate="hostingField.info"></span>-->
<!--                <input ng-if="vm.form.hostingFieldsEmail != null"-->
<!--                       type="text"-->
<!--                       class="form-control"-->
<!--                       kr-input-->
<!--                       autocomplete="off"-->
<!--                       ng-model="vm.form.hostingFieldsEmail">-->
<!--                <a style="display: flex; text-align: center; justify-content: center;">-->
<!--                    <i class="os-icon os-icon-email-forward mr-20 mt-20 cursor" ng-click="fn.formSubmit(data.id)">1</i>-->
<!--                    <i class="os-icon os-icon-email-forward mr-20 mt-20 cursor" ng-click="fn.formSubmit(data.id)">2</i>-->
<!--                    <i class="os-icon os-icon-email-forward mr-20 mt-20 cursor" ng-click="fn.formSubmit(data.id)">3</i>-->
<!--                </a>-->
<!--                <div class="modal-content">-->
<!--                    <a style="display: flex; text-align: center; justify-content: center;">-->
<!--                        <img onclick="emailTemplate()" id="template1" class="cursor" src="assets/images/btn_facebook.png">-->
<!--                        <img onclick="emailTemplate()" id="template2" class="cursor" src="assets/images/btn_kakao.png">-->
<!--                        <img onclick="emailTemplate()" id="template3" class="cursor" src="assets/images/btn_google.png">-->
<!--                    </a>-->
<!--                </div>-->
<!--                <form action="${pageContext.request.contextPath}/updatetNoticePro.do" method="post" enctype="multipart/form-data" name="noticeForm">-->
<!--                    <input type='file' id="filename" name="filename"/>-->
<!--                    <img id="preImage" src="${pageContext.request.contextPath}/saveFile/${noticeVO.filename}" alt="image_title" onerror='this.src="${pageContext.request.contextPath}/images/no_img.jpg"'/>-->
<!--                </form>-->
            </div>
        </form>
    </div>
<!--    <h6>{{vm.form.companyFieldsEmail}}</h6>-->
<!--   <span>{{vm.form.id}}===============</span>-->
<!--   <h6>{{vm.form.ispFieldsEmail}}===============</h6>-->
<!--   <span>===============</span>-->
<!--   <h6>{{vm.form.hostingFieldsEmail}}</h6>-->
<!--    <div style="display: flex; text-align: center">-->
<!--            템플릿1-->
<!--        </div>-->
<!--        <div style="border: 1px #111111; margin-left: 20px; box-sizing: border-box;">-->
<!--            템플릿2-->
    <!--        <div style="border: 1px #111111; margin-left: 20px; box-sizing: border-box;">-->
<!--        </div>-->
<!--        <div style="border: 1px #111111; margin-left: 20px; box-sizing: border-box;">-->
<!--            템플릿3-->
<!--        </div>-->
<!--    </div>-->
    <div class="modal-footer buttons-on-right">
        <button type="button" class="btn btn-md btn-grey" ng-click="fn.cancel()"><span
@@ -45,3 +121,24 @@
        </button>
    </div>
</div>
<script>
    function emailTemplate() {
        $('#template1').click(function() {
            $('#template1').hide();
        });
        $('#template2').click(function() {
            $('#template2').hide();
        });
        $('#template3').click(function() {
            $('#template3').hide();
        });
    }
    function changeIMG() {
        $('#template1').attr("src", "assets/images/previewTemplate.png")
    }
</script>