본문 바로가기

반응형
반응형

오늘은 검색까지 마저 구현하도록 하겠습니다. 다음은 오늘 필요한 요구사항입니다.

요구 사항

  • 검색 타입 : 작성자, 결재자, 제목+내용
  • 결재 상태1 : 임시 저장, 결재 대기, 결재 중, 결재 완료, 반려
  • 결재 상태2 : 비동기식 검색

다음은 위 요구사항을 기반으로 오늘 포스팅 순서 입니다.

검색 타입 구현 > 결재 상태1 > 결재 상태2

비동기식 검색은 아직 제가 초보인 만큼 바로 Ajax로 구현이 힘들다고 판단이 됐습니다. 그래서 동기식으로 먼저 구현한 뒤에 비동기식으로 구현하도록 하겠습니다.

검색 타입 구현 > 결재 상태1 > 결재 상태2

1-1. View (list.jsp)

<div class="search-form">
    <h3>검색</h3>
    <form id="searchForm">
        <select name="searchType">
            <option value="choice">검색 조건</option>
            <option value="writerName">작성자</option>
            <option value="apperName">결재자</option>
            <option value="appTitleContent">제목+내용</option>
        </select>
        <input id="keyword" name="keyword"> <br>
        <select name="appStatus" id="appStatus">
            <option value="choice">결재 상태</option>
            <option value="saveApp">임시 저장</option>
            <option value="propApp">결재 대기</option>
            <option value="ingApp">결재 중</option>
            <option value="doneApp">결재 완료</option>
            <option value="cancelApp">반 려</option>
        </select>
        <br>
        <input type="date" id="startDate" name="startDate">~ <input type="date" id="endDate" name="endDate">
        <button id="searchBtn">검 색</button>
    </form>
</div>

각 option당 value를 부여해 줌으로써 select의 name이 key 값으로, option의 value가 value값으로 HashMap형식으로 서버로 넘어가게 될 것 입니다. 아직은 임시 저장과 반려를 구현하지는 않았지만 나중에 구현 될 것이니 미리 넣어주도록 합니다.

1-2. JQuery

<script src="https://code.jquery.com/jquery-3.7.0.js"></script>
<script>
    $(function(){
        $("#searchBtn").on('click', function(){
            $("#searchForm").attr({
                "action" : "/list",
                "method" : "get"
            }).submit();
        });
    })
</script>

id="searchBtn" 을 누름으로써 id="searchForm"안의 내용들을 모두 서버로 넘겨줄 수 있도록 합니다.

1-3. Controller

	@RequestMapping(value = "/list", method = RequestMethod.GET)
	public String list(HttpServletRequest request, RedirectAttributes redirectAttributes, Model model
			, @RequestParam Map<String, Object> searchParam /*추가*/) {
		HttpSession session = request.getSession();
		String sessionId = (String) session.getAttribute("sessionId");
		String sessionPst = (String) session.getAttribute("sessionPst");

		if (sessionId == null) {
			redirectAttributes.addFlashAttribute("msg", "로그인 하세요");
			return "redirect:/login";
		}
		Map<String, Object> listCdt = new HashMap<String, Object>();
		listCdt.put("writerId", sessionId);
		listCdt.put("sessionPst", sessionPst);
		listCdt.putAll(searchParam); // 추가
		
		List<Map<String, Object>> appList = appSer.getAppList(listCdt);

		model.addAttribute("appList", appList);
		
		return "list";
	}

검색 조건을 searchParam으로 받은 뒤에 검색 조건을 위해 만들어 뒀던 listCdt에 putAll 해주도록 합니다. putAll 해주면 searchParam의 모든 것들이 listCdt로 들어가게 됩니다. (ex. listCdt["a":1, "b":2], searchParam["c":3, "d":4] > listCdt[ "a":1, "b":2, "c":3, "d":4])

1-4. Mapper

	<select id="getAppList" resultMap="App">
		select
			eaa.app_seq,
			eamw.mem_name WRITER_NAME,
			eaa.app_title,
			eaa.app_content,
			to_char(eaa.reg_date, 'yyyy-mm-dd') reg_date,
			to_char(eaa.app_date, 'yyyy-mm-dd') app_date,
			eama.mem_name APPER_NAME,
			decode(eaa.app_status,'A','결재 대기','B','결재 중','C','결재 완료') app_status
		from e_approval_app eaa
		left join e_approval_member eamw on eaa.writer_seq = eamw.mem_seq
		left join e_approval_member eama on eaa.apper_seq = eama.mem_seq
		<!-- 본인이 작성한 글만 보이도록 -->
		where (eamw.id = #{writerId}
		<!-- 각 직급별로 볼 수 있는 글 -->
		<if test="sessionPst =='부장' ">
			or (eaa.app_status in ('C', 'B'))
		</if>
		<if test="sessionPst == '과장' ">
			or (eaa.app_status = 'A')
		</if>
		)
		<!-- 검색 조건 -->
		<choose>
			<when test="searchType == 'writerName' ">
				and eamw.mem_name like '%' || #{keyword} || '%'
			</when>
			<when test="searchType == 'apperName' ">
				and eama.mem_name like '%' || #{keyword} || '%'
			</when>
			<when test="searchType == 'appTitleContent' ">
				and (eaa.app_title like '%' || #{keyword} || '%' or eaa.app_content like '%' || #{keyword} || '%')
			</when>
		</choose>
		<!-- 날짜 검색 -->
		<if test="startDate != null and startDate != '' ">
			and to_char(reg_date, 'yyyy-mm-dd') between #{startDate} and #{endDate}
		</if>
		order by eaa.app_seq desc
	</select>

검색 조건에 "제목+내용" 이 생겼습니다. 그에 따라 결재글의 내용도 필요하니 app_content도 추가해줬습니다.

1-5. 결과물

검색어 검색 전 후
날짜 검색 전 
복합 검색 전 후

 

검색 타입 구현 > 결재 상태1 > 결재 상태2

이전에 미리 JSP에서 구현을 해줬습니다. 이제는 데이터 가공과 쿼리문을 이용해서 구현하도록 하겠습니다.

2-1. Service Impl

	@Override
	public List<Map<String, Object>> getAppList(Map<String, Object> param){
		
		if(param.get("appStatus") != null) {
			String appStatus = param.get("appStatus").toString();
			
			if("saveApp".equals(appStatus)) {
				appStatus = "W";
			} else if ("propApp".equals(appStatus)) {
				appStatus = "A";
			} else if ("ingApp".equals(appStatus)) {
				appStatus = "B";
			} else if ("doneApp".equals(appStatus)) {
				appStatus = "C";
			} else if ("cancelApp".equals(appStatus)) {
				appStatus = "X";
			}
			
			param.put("appStatus", appStatus);
		}
		
		return appDao.getAppList(param);
	}

appStatus의 값의 유무를 통해 값과 데이터가 있을 때, 데이터 가공을 해줍니다. 새로운 FLAG값들이 보이죠? 임시 저장과 반려를 W와 X로 할당해주도록 합니다.

2-2. Mapper

	<select id="getAppList" resultMap="App">
		select
			eaa.app_seq,
			eamw.mem_name WRITER_NAME,
			eaa.app_title,
			eaa.app_content,
			to_char(eaa.reg_date, 'yyyy-mm-dd') reg_date,
			to_char(eaa.app_date, 'yyyy-mm-dd') app_date,
			eama.mem_name APPER_NAME,
			decode(eaa.app_status,'A','결재 대기','B','결재 중','C','결재 완료') app_status
		from e_approval_app eaa
		left join e_approval_member eamw on eaa.writer_seq = eamw.mem_seq
		left join e_approval_member eama on eaa.apper_seq = eama.mem_seq
		<!-- 본인이 작성한 글만 보이도록 -->
		where (eamw.id = #{writerId}
		<!-- 각 직급별로 볼 수 있는 글 -->
		<if test="sessionPst =='부장' ">
			or (eaa.app_status in ('C', 'B'))
		</if>
		<if test="sessionPst == '과장' ">
			or (eaa.app_status = 'A')
		</if>
		)
		<!-- 검색 조건 -->
		<choose>
			<when test="searchType == 'writerName' ">
				and eamw.mem_name like '%' || #{keyword} || '%'
			</when>
			<when test="searchType == 'apperName' ">
				and eama.mem_name like '%' || #{keyword} || '%'
			</when>
			<when test="searchType == 'appTitleContent' ">
				and (eaa.app_title like '%' || #{keyword} || '%' or eaa.app_content like '%' || #{keyword} || '%')
			</when>
		</choose>
		<!-- 날짜 검색 -->
		<if test="startDate != null and startDate != '' ">
			and to_char(reg_date, 'yyyy-mm-dd') between #{startDate} and #{endDate}
		</if>
		<!-- 결재 상태 -->
		<if test="appStatus != 'choice' and appStatus != null and appStatus != '' ">
			and eaa.app_status = #{appStatus}
		</if>
		order by eaa.app_seq desc
	</select>

마지막 조건으로 appStatus가 없거나 아무런 값이 없거나 choice일 때, Service에서 가공해준 값을 바탕으로 조회를 해줍니다. 

2-3. 결과물

결재 상태 검색 전 후

 

검색 타입 구현 > 결재 상태1 > 결재 상태2

Ajax를 사용하며 JSON을 이용할 것입니다. 그를 위해 dependency를 추가해주도록 하겠습니다.

3-1. pom.xml

<!-- jackson -->
<!-- https://mvnrepository.com/artifact/org.codehaus.jackson/jackson-mapper-asl -->
<dependency>
    <groupId>org.codehaus.jackson</groupId>
    <artifactId>jackson-mapper-asl</artifactId>
    <version>1.9.13</version>
</dependency>

 

3-2. list.jsp

<tbody id='listZone'>

<tbody> 안의 내용을 수정해줄 것이기 때문에 id를 부여해줍니다.

3-3. JQuery

$(function(){
    $("#searchBtn").on('click', function(event){
        event.preventDefault();

        $.ajax({
            url : "/ajaxList",
            type : "post",
            data : $("#searchForm").serialize(),

            success : function(data){
                let reTable = '';
                var list = data;

                for(i = 0; i < list.length; i++){
                    reTable += "<tr>";
                    reTable += "<td>"+list[i].appSeq+"</td>";
                    reTable += "<td>"+list[i].writerName+"</td>";
                    reTable += "<td>"+list[i].appTitle+"</td>";
                    reTable += "<td>"+list[i].regDate+"</td>";
                    reTable += "<td>"+list[i].appDate+"</td>";
                    reTable += "<td>"+list[i].apperName+"</td>";
                    reTable += "<td>"+list[i].appStatus+"</td>";
                    reTable += "</tr>";
                }

                $("#listZone").html(reTable);

                console.log("Ajax 성공");
            }, error : function(xhr, textStatus, errorThrown) {
                console.log(xhr.status);
            }
        });
    });
})

기존에 .attr.submit(); 했던 것을 ajax로 고쳐줬습니다. searchForm 안의 것들을 서버에 넘겨준 뒤 반환에 성공하면 <tbody>안에 것들을 바꿔주는 것입니다.

3-4. Controller

@RequestMapping(value = "/ajaxList")
@ResponseBody
public List<Map<String, Object>> searchList(HttpServletRequest request, @RequestParam Map<String, Object> param) {
    HttpSession session = request.getSession();
    String sessionId = (String) session.getAttribute("sessionId");
    String sessionPst = (String) session.getAttribute("sessionPst");

    param.put("writerId", sessionId);
    param.put("sessionPst", sessionPst);

    List<Map<String, Object>> appList = appSer.getAppList(param);

    return appList;
}

지금과 같은 방식을 사용할 때엔 @ResponseBody 어노테이션은 필수입니다. 빼먹으면 안됩니다. 리스트를 조회하고 반환해 주도록 합니다.

3-5. 결과물

URL까지 확인한 결과 Ajax가 무사히 작동하는 것을 확인했습니다!

다음 시간에는 글쓰기로 돌아오도록 하겠습니다. 빠잇~!

후기

오늘은 정말 힘들게 고생한 한 시간이었습니다. Ajax 이전까지는 원활하게 진행되었지만, Ajax에서 막힌 부분에서 한창을 애를 썼습니다. 검색 인자들을 서버에 전달하여 조회까지는 성공적으로 이뤄졌는데, 계속해서 406 에러가 발생하여 모든 방법을 동원해도 문제를 해결하지 못했습니다. F12를 눌러 Network 창에서 Payload와 Headers를 살펴보고, Console.log를 확인하며 컨트롤러와 Ajax 코드에 DataType을 명시해보기도 했지만, 모든 시도가 무산되어 정말로 난감한 상황에 처했습니다.

마침내 원인을 찾아보니, 의존성 파일(pom.xml)에서 jackson이 빠져 있는 것을 발견했습니다. 다행히 Ajax 코드에는 문제가 없었지만, 이 작은 부분이 큰 에러를 일으켰다는 사실에 당황스러웠습니다. 원인을 찾아 해결하게 되어 다행이었지만, 이번 경험을 통해 Network에서 다양한 정보를 확인하는 방법과 DataType을 명시하는 중요성을 깨달을 수 있었습니다. 또한, 코드에 문제가 없어 보일 때에도 설정 파일들을 꼼꼼히 살펴봐야 한다는 교훈을 얻었습니다. 이러한 어려움을 극복하며 배운 것들이 더욱 가치있는 경험이 되었습니다.
반응형
댓글